akm-cli 0.7.5 → 0.8.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
- package/README.md +20 -4
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +133 -0
- package/dist/cli.js +1995 -551
- package/dist/commands/agent-dispatch.js +110 -0
- package/dist/commands/agent-support.js +68 -0
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +130 -534
- package/dist/commands/consolidate.js +1531 -0
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +660 -0
- package/dist/commands/distill.js +990 -75
- package/dist/commands/eval-cases.js +43 -0
- package/dist/commands/events.js +5 -23
- package/dist/commands/graph.js +477 -0
- package/dist/commands/health.js +400 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +77 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +54 -46
- package/dist/commands/improve-profiles.js +146 -0
- package/dist/commands/improve-result-file.js +103 -0
- package/dist/commands/improve.js +2175 -0
- package/dist/commands/info.js +5 -2
- package/dist/commands/init.js +50 -2
- package/dist/commands/installed-stashes.js +102 -139
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/lint/agent-linter.js +49 -0
- package/dist/commands/lint/base-linter.js +479 -0
- package/dist/commands/lint/command-linter.js +49 -0
- package/dist/commands/lint/default-linter.js +16 -0
- package/dist/commands/lint/index.js +183 -0
- package/dist/commands/lint/knowledge-linter.js +16 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +61 -0
- package/dist/commands/lint/registry.js +36 -0
- package/dist/commands/lint/skill-linter.js +45 -0
- package/dist/commands/lint/task-linter.js +50 -0
- package/dist/commands/lint/types.js +4 -0
- package/dist/commands/lint/vault-key-rules.js +139 -0
- package/dist/commands/lint/workflow-linter.js +56 -0
- package/dist/commands/lint.js +4 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal.js +66 -12
- package/dist/commands/propose.js +86 -31
- package/dist/commands/reflect.js +1119 -73
- package/dist/commands/registry-search.js +5 -2
- package/dist/commands/remember.js +69 -6
- package/dist/commands/schema-repair.js +203 -0
- package/dist/commands/search.js +115 -14
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +144 -25
- package/dist/commands/source-add.js +17 -45
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +14 -19
- package/dist/commands/tasks.js +438 -0
- package/dist/commands/url-checker.js +42 -0
- package/dist/commands/vault.js +130 -77
- package/dist/core/action-contributors.js +28 -0
- package/dist/core/asset-ref.js +7 -0
- package/dist/core/asset-registry.js +7 -16
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +22 -0
- package/dist/core/common.js +157 -0
- package/dist/core/concurrent.js +25 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +625 -0
- package/dist/core/config-schema.js +501 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +327 -987
- package/dist/core/errors.js +40 -19
- package/dist/core/events.js +91 -138
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +3 -6
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +20 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +806 -0
- package/dist/core/parse.js +158 -0
- package/dist/core/paths.js +326 -14
- package/dist/core/proposal-quality-validators.js +364 -0
- package/dist/core/proposal-validators.js +69 -0
- package/dist/core/proposals.js +498 -42
- package/dist/core/state-db.js +927 -0
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +54 -0
- package/dist/core/warn.js +62 -1
- package/dist/core/write-source.js +3 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +152 -253
- package/dist/indexer/db.js +933 -103
- package/dist/indexer/ensure-index.js +64 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +376 -101
- package/dist/indexer/graph-db.js +391 -0
- package/dist/indexer/graph-dedup.js +95 -0
- package/dist/indexer/graph-extraction.js +550 -124
- package/dist/indexer/index-context.js +4 -0
- package/dist/indexer/indexer.js +506 -291
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +148 -160
- package/dist/indexer/memory-inference.js +99 -74
- package/dist/indexer/metadata-contributors.js +29 -0
- package/dist/indexer/metadata.js +255 -196
- package/dist/indexer/path-resolver.js +92 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +331 -0
- package/dist/indexer/ranking.js +81 -0
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +111 -0
- package/dist/indexer/search-source.js +44 -10
- package/dist/indexer/semantic-status.js +5 -16
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +28 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +122 -230
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +7 -13
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +70 -5
- package/dist/integrations/agent/prompts.js +150 -74
- package/dist/integrations/agent/runner.js +151 -0
- package/dist/integrations/agent/sdk-runner.js +126 -0
- package/dist/integrations/agent/spawn.js +118 -23
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +32 -69
- package/dist/integrations/session-logs/index.js +68 -0
- package/dist/integrations/session-logs/providers/claude-code.js +59 -0
- package/dist/integrations/session-logs/providers/opencode.js +55 -0
- package/dist/integrations/session-logs/types.js +4 -0
- package/dist/llm/call-ai.js +62 -0
- package/dist/llm/client.js +72 -124
- package/dist/llm/embedder.js +3 -19
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +3 -0
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +89 -48
- package/dist/llm/graph-extract.js +676 -70
- package/dist/llm/index-passes.js +9 -23
- package/dist/llm/memory-infer.js +52 -71
- package/dist/llm/metadata-enhance.js +42 -29
- package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/output/cli-hints-full.md +281 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +5 -318
- package/dist/output/context.js +3 -0
- package/dist/output/renderers.js +223 -256
- package/dist/output/shapes.js +150 -105
- package/dist/output/text.js +318 -30
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +3 -0
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +70 -49
- package/dist/registry/providers/static-index.js +53 -48
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17307 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +775 -37
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +5 -12
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +7 -5
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +7 -0
- package/dist/tasks/backends/cron.js +203 -0
- package/dist/tasks/backends/exec-utils.js +28 -0
- package/dist/tasks/backends/index.js +24 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +187 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +215 -0
- package/dist/tasks/parser.js +211 -0
- package/dist/tasks/resolveAkmBin.js +87 -0
- package/dist/tasks/runner.js +458 -0
- package/dist/tasks/schedule.js +211 -0
- package/dist/tasks/schema.js +15 -0
- package/dist/tasks/validator.js +62 -0
- package/dist/version.js +3 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +15 -0
- package/dist/wiki/wiki.js +13 -61
- package/dist/workflows/authoring.js +8 -25
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +11 -3
- package/dist/workflows/runs.js +62 -91
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +4 -8
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +9 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +48 -0
- package/docs/migration/v0.7-to-v0.8.md +1307 -0
- package/package.json +20 -8
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -381
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
// CONTRACT: ref-resolver
|
|
5
|
+
// ----------------------------------------------------------------------------
|
|
6
|
+
// The `refExistsInAnyStash` and `refToRelPath` helpers below are contract-
|
|
7
|
+
// locked: a sister copy lives in the akm-plugins repo at
|
|
8
|
+
// `shared/ref-extraction.ts` (and the runtime-shipped duplicate at
|
|
9
|
+
// `claude/shared/ref-extraction.ts`). Both implementations resolve the same
|
|
10
|
+
// `<type>:<slug>` -> on-disk-asset question and MUST agree on the set of
|
|
11
|
+
// reachable refs for any given stash layout.
|
|
12
|
+
//
|
|
13
|
+
// The lock is enforced by `tests/contracts/ref-resolver-contract.test.ts`,
|
|
14
|
+
// which drives this implementation through a canonical fixture set. The
|
|
15
|
+
// akm-plugins repo ships an equivalent test that drives its copy through the
|
|
16
|
+
// SAME inputs and asserts identical outcomes. Any change to the resolver
|
|
17
|
+
// behavior on either side MUST update both contract tests in lockstep, or one
|
|
18
|
+
// will fail.
|
|
19
|
+
//
|
|
20
|
+
// Cases the contract covers (see fixture in the contract test):
|
|
21
|
+
// - existing memory / knowledge / agent / workflow / skill / vault refs
|
|
22
|
+
// - knowledge subdirectory layout (knowledge/<category>/<slug>.md)
|
|
23
|
+
// - skill multi-file layout (skills/<slug>/SKILL.md)
|
|
24
|
+
// - memory `.derived.md` sibling
|
|
25
|
+
// - vault default vs named (.env vs <name>.env)
|
|
26
|
+
// - namespaced slugs containing `/`
|
|
27
|
+
// - non-existent refs
|
|
28
|
+
// - script type (unresolvable by design — both must return false)
|
|
29
|
+
// ----------------------------------------------------------------------------
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { findSafeInsertionPoint } from "./markdown-insertion";
|
|
33
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
34
|
+
function formatDate(d) {
|
|
35
|
+
const y = d.getFullYear();
|
|
36
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
37
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
38
|
+
return `${y}-${m}-${day}`;
|
|
39
|
+
}
|
|
40
|
+
function checkUnquotedColon(frontmatterText) {
|
|
41
|
+
if (!frontmatterText)
|
|
42
|
+
return null;
|
|
43
|
+
for (const line of frontmatterText.split(/\r?\n/)) {
|
|
44
|
+
const match = line.match(/^description:\s*(.*)/);
|
|
45
|
+
if (!match)
|
|
46
|
+
continue;
|
|
47
|
+
const value = match[1].trim();
|
|
48
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (value.includes(":")) {
|
|
52
|
+
return `description value contains unquoted colon: ${value}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function fixUnquotedColon(raw) {
|
|
58
|
+
return raw.replace(/^(description:\s*)(.*)/m, (_match, prefix, value) => {
|
|
59
|
+
const trimmed = value.trim();
|
|
60
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
61
|
+
return _match;
|
|
62
|
+
}
|
|
63
|
+
const escaped = trimmed.replace(/"/g, '\\"');
|
|
64
|
+
return `${prefix}"${escaped}"`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function checkMissingUpdated(data, frontmatterText) {
|
|
68
|
+
return frontmatterText !== null && !("updated" in data);
|
|
69
|
+
}
|
|
70
|
+
function fixMissingUpdated(raw, mtime) {
|
|
71
|
+
const dateStr = formatDate(mtime);
|
|
72
|
+
return raw.replace(/^(---\n[\s\S]*?)\n---/m, `$1\nupdated: ${dateStr}\n---`);
|
|
73
|
+
}
|
|
74
|
+
// ── stale-path helpers ────────────────────────────────────────────────────────
|
|
75
|
+
function checkStalePath(body) {
|
|
76
|
+
const pathRe = /\/home\/[^\s"'`)\]>,]+/g;
|
|
77
|
+
let match;
|
|
78
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
|
|
79
|
+
while ((match = pathRe.exec(body)) !== null) {
|
|
80
|
+
const candidate = match[0];
|
|
81
|
+
if (!fs.existsSync(candidate)) {
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// ── missing-ref helpers ───────────────────────────────────────────────────────
|
|
88
|
+
const REF_RE = /(?:^|[\s`"'(])((agent|command|knowledge|memory|script|skill|workflow|lesson|task|wiki|vault):[^\s"'`)\]>,\n]+)/gm;
|
|
89
|
+
/**
|
|
90
|
+
* Map from ref type to relative path pattern within stashRoot. Returns null to skip.
|
|
91
|
+
*
|
|
92
|
+
* Exported for contract testing — see header CONTRACT block.
|
|
93
|
+
*/
|
|
94
|
+
export function refToRelPath(refType, refName) {
|
|
95
|
+
switch (refType) {
|
|
96
|
+
case "agent":
|
|
97
|
+
return path.join("agents", `${refName}.md`);
|
|
98
|
+
case "command":
|
|
99
|
+
return path.join("commands", `${refName}.md`);
|
|
100
|
+
case "knowledge":
|
|
101
|
+
return path.join("knowledge", `${refName}.md`);
|
|
102
|
+
case "memory":
|
|
103
|
+
return path.join("memories", `${refName}.md`);
|
|
104
|
+
case "script":
|
|
105
|
+
return null; // scripts live in nested dirs — skip
|
|
106
|
+
case "skill":
|
|
107
|
+
return path.join("skills", refName, "SKILL.md");
|
|
108
|
+
case "workflow":
|
|
109
|
+
return path.join("workflows", `${refName}.md`);
|
|
110
|
+
case "lesson":
|
|
111
|
+
return path.join("lessons", `${refName}.md`);
|
|
112
|
+
case "task":
|
|
113
|
+
return path.join("tasks", `${refName}.md`);
|
|
114
|
+
case "wiki":
|
|
115
|
+
return path.join("wikis", `${refName}.md`);
|
|
116
|
+
case "vault":
|
|
117
|
+
// Vaults are .env files. The canonical name "default" (or empty) maps to
|
|
118
|
+
// ".env"; any other name maps to "<name>.env". This mirrors the vault
|
|
119
|
+
// asset-spec toAssetPath logic in src/core/asset-spec.ts.
|
|
120
|
+
if (!refName || refName === "default") {
|
|
121
|
+
return path.join("vaults", ".env");
|
|
122
|
+
}
|
|
123
|
+
return path.join("vaults", `${refName}.env`);
|
|
124
|
+
default:
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns true if `relPath` resolves to a real file (or multi-file directory
|
|
130
|
+
* primary) in ANY of the provided stash roots.
|
|
131
|
+
*
|
|
132
|
+
* Exported for contract testing — see header CONTRACT block.
|
|
133
|
+
*/
|
|
134
|
+
export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
135
|
+
for (const root of stashRoots) {
|
|
136
|
+
const absPath = path.join(root, relPath);
|
|
137
|
+
if (fs.existsSync(absPath))
|
|
138
|
+
return true;
|
|
139
|
+
// Multi-file skill layout: directory containing SKILL.md
|
|
140
|
+
const bareDir = absPath.replace(/\.md$/, "");
|
|
141
|
+
if (fs.existsSync(bareDir) && fs.existsSync(path.join(bareDir, "SKILL.md")))
|
|
142
|
+
return true;
|
|
143
|
+
// .derived.md variant for memory refs
|
|
144
|
+
if (refType === "memory") {
|
|
145
|
+
const derivedPath = path.join(root, "memories", `${refName}.derived.md`);
|
|
146
|
+
if (fs.existsSync(derivedPath))
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
// Knowledge-specific: search subdirectories like knowledge/projects/, knowledge/tools/, etc.
|
|
150
|
+
if (refType === "knowledge") {
|
|
151
|
+
try {
|
|
152
|
+
const knowledgeDir = path.join(root, "knowledge");
|
|
153
|
+
if (fs.existsSync(knowledgeDir) && fs.statSync(knowledgeDir).isDirectory()) {
|
|
154
|
+
const entries = fs.readdirSync(knowledgeDir);
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
const subPath = path.join(knowledgeDir, entry, `${refName}.md`);
|
|
157
|
+
if (fs.existsSync(subPath))
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Ignore errors reading directory
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Fallback: the refName may already encode the full stash-relative path
|
|
167
|
+
// (e.g. knowledge:skills/foo/references/bar where the file lives at
|
|
168
|
+
// <stash>/skills/foo/references/bar.md, not <stash>/knowledge/skills/...).
|
|
169
|
+
const directPath = path.join(root, `${refName}.md`);
|
|
170
|
+
if (fs.existsSync(directPath))
|
|
171
|
+
return true;
|
|
172
|
+
const directDir = path.join(root, refName);
|
|
173
|
+
if (fs.existsSync(directDir) && fs.existsSync(path.join(directDir, "SKILL.md")))
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Returns an array of {ref, resolvedRelPath} for every local AKM ref in the
|
|
180
|
+
* body that does not resolve to a real file under any of the provided stash roots.
|
|
181
|
+
*
|
|
182
|
+
* Skips false-positive patterns:
|
|
183
|
+
* - Shell variables: memory:$(cmd) or knowledge:${VAR}
|
|
184
|
+
* - ACP type notation: agent::Type (double colons are C++/ACP syntax)
|
|
185
|
+
* - Incomplete/placeholder refs: slug is single character or "**"
|
|
186
|
+
*/
|
|
187
|
+
function checkMissingRefs(body, stashRoot, extraStashRoots = []) {
|
|
188
|
+
const allRoots = [stashRoot, ...extraStashRoots];
|
|
189
|
+
const missing = [];
|
|
190
|
+
let match;
|
|
191
|
+
const re = new RegExp(REF_RE.source, REF_RE.flags);
|
|
192
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex loop
|
|
193
|
+
while ((match = re.exec(body)) !== null) {
|
|
194
|
+
const fullRef = match[1]; // e.g. "workflow:foo" or "local//workflow:foo"
|
|
195
|
+
// Skip shell variables: memory:$(cmd) or knowledge:${VAR}
|
|
196
|
+
if (fullRef.includes("$(") || fullRef.includes("${")) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// Skip ACP type notation: agent::Type (double colons)
|
|
200
|
+
if (fullRef.includes("::")) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// Strip leading "local//" prefix if present
|
|
204
|
+
let ref = fullRef;
|
|
205
|
+
if (ref.startsWith("local//")) {
|
|
206
|
+
ref = ref.slice("local//".length);
|
|
207
|
+
}
|
|
208
|
+
else if (fullRef.includes("//")) {
|
|
209
|
+
// Has a remote origin prefix (e.g. "npm:", "github:", "owner/repo//") — skip
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// Skip refs that start with obvious remote prefixes
|
|
213
|
+
const colonIdx = ref.indexOf(":");
|
|
214
|
+
if (colonIdx === -1)
|
|
215
|
+
continue;
|
|
216
|
+
const refType = ref.slice(0, colonIdx);
|
|
217
|
+
const refName = ref.slice(colonIdx + 1);
|
|
218
|
+
// Guard against empty names or names that look like paths/URLs
|
|
219
|
+
if (!refName || refName.startsWith("/") || refName.startsWith("~") || refName.startsWith("http")) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Skip placeholder/incomplete refs: single character slug or "**"
|
|
223
|
+
if (refName.length <= 1 || refName === "**") {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const relPath = refToRelPath(refType, refName);
|
|
227
|
+
if (relPath === null)
|
|
228
|
+
continue; // type is skipped
|
|
229
|
+
if (!refExistsInAnyStash(relPath, refType, refName, allRoots)) {
|
|
230
|
+
missing.push({ ref: fullRef, resolvedRelPath: relPath });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return missing;
|
|
234
|
+
}
|
|
235
|
+
// ── frontmatter refs ─────────────────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* Return the `refs:` array from frontmatter when it is present and is an
|
|
238
|
+
* array of strings; otherwise return `null` to signal the caller should
|
|
239
|
+
* fall back to scanning the body. An empty array (`refs: []`) is also
|
|
240
|
+
* treated as authoritative — it explicitly declares "this asset has no
|
|
241
|
+
* outbound refs" and suppresses the body scan.
|
|
242
|
+
*
|
|
243
|
+
* The `refs:` frontmatter key is used by the claude-code session-capture
|
|
244
|
+
* hook (see `shared/ref-extraction.ts` in the akm-plugins repo) to
|
|
245
|
+
* persist a validated outbound-ref list alongside the raw transcript.
|
|
246
|
+
* Hand-written memories rarely populate this key — for those the body
|
|
247
|
+
* scan remains the source of truth.
|
|
248
|
+
*
|
|
249
|
+
* Session-checkpoint memories use a nested frontmatter pattern: `akm
|
|
250
|
+
* remember` wraps the file in `---\n…\n---` and the hook's own
|
|
251
|
+
* `---\nakm_memory_kind: session_checkpoint\n…\n---` block is preserved
|
|
252
|
+
* inside the body. We look in both places so the `refs:` key works
|
|
253
|
+
* regardless of where the producer wrote it.
|
|
254
|
+
*/
|
|
255
|
+
function extractFrontmatterRefs(data, body) {
|
|
256
|
+
const fromOuter = readRefsArray(data.refs);
|
|
257
|
+
if (fromOuter !== null)
|
|
258
|
+
return fromOuter;
|
|
259
|
+
const innerData = parseInnerFrontmatterBlock(body);
|
|
260
|
+
if (innerData) {
|
|
261
|
+
const fromInner = readRefsArray(innerData.refs);
|
|
262
|
+
if (fromInner !== null)
|
|
263
|
+
return fromInner;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
function readRefsArray(value) {
|
|
268
|
+
if (!Array.isArray(value))
|
|
269
|
+
return null;
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const entry of value) {
|
|
272
|
+
if (typeof entry === "string" && entry.trim())
|
|
273
|
+
out.push(entry.trim());
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Detect a leading nested frontmatter block in `body` (i.e. a `---\n…\n---`
|
|
279
|
+
* pair that opens within the first few lines of the body). When present,
|
|
280
|
+
* parse a minimal subset of YAML — top-level scalars and block-list
|
|
281
|
+
* arrays — sufficient to recognise the `refs:` key. Anything fancier is
|
|
282
|
+
* silently ignored.
|
|
283
|
+
*
|
|
284
|
+
* This is a deliberately narrow parser: lint must never throw on
|
|
285
|
+
* unexpected YAML, and the only key we care about here is `refs:`.
|
|
286
|
+
*/
|
|
287
|
+
function parseInnerFrontmatterBlock(body) {
|
|
288
|
+
// Skip up to three blank/header lines, then require `---` to open the block.
|
|
289
|
+
const lines = body.split(/\r?\n/);
|
|
290
|
+
let i = 0;
|
|
291
|
+
while (i < lines.length && i < 3 && lines[i].trim() === "")
|
|
292
|
+
i += 1;
|
|
293
|
+
if (lines[i] !== "---")
|
|
294
|
+
return null;
|
|
295
|
+
const open = i;
|
|
296
|
+
let close = -1;
|
|
297
|
+
for (let j = open + 1; j < lines.length; j += 1) {
|
|
298
|
+
if (lines[j] === "---") {
|
|
299
|
+
close = j;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (close === -1)
|
|
304
|
+
return null;
|
|
305
|
+
const block = lines.slice(open + 1, close);
|
|
306
|
+
const data = {};
|
|
307
|
+
let currentKey = null;
|
|
308
|
+
let currentList = null;
|
|
309
|
+
for (const line of block) {
|
|
310
|
+
const listItem = line.match(/^(?: {2})?- (.*)$/);
|
|
311
|
+
if (listItem && currentList) {
|
|
312
|
+
currentList.push(listItem[1].trim().replace(/^["'](.*)["']$/, "$1"));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const inlineFlow = line.match(/^(\w[\w-]*):\s*\[(.*)\]\s*$/);
|
|
316
|
+
if (inlineFlow) {
|
|
317
|
+
currentKey = inlineFlow[1];
|
|
318
|
+
const items = inlineFlow[2]
|
|
319
|
+
.split(",")
|
|
320
|
+
.map((s) => s.trim().replace(/^["'](.*)["']$/, "$1"))
|
|
321
|
+
.filter(Boolean);
|
|
322
|
+
data[currentKey] = items;
|
|
323
|
+
currentList = null;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
327
|
+
if (!kv)
|
|
328
|
+
continue;
|
|
329
|
+
currentKey = kv[1];
|
|
330
|
+
const value = kv[2].trim();
|
|
331
|
+
if (value === "") {
|
|
332
|
+
currentList = [];
|
|
333
|
+
data[currentKey] = currentList;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
data[currentKey] = value.replace(/^["'](.*)["']$/, "$1");
|
|
337
|
+
currentList = null;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return data;
|
|
341
|
+
}
|
|
342
|
+
// ── BaseLinter ────────────────────────────────────────────────────────────────
|
|
343
|
+
/**
|
|
344
|
+
* Abstract base class providing the two cross-type checks shared by all asset
|
|
345
|
+
* linters: `unquoted-colon` and `missing-updated`.
|
|
346
|
+
*
|
|
347
|
+
* Subclasses call `runBaseChecks(ctx)` and append any type-specific issues.
|
|
348
|
+
* File mutations triggered by base checks are flushed to disk inside this
|
|
349
|
+
* method; subclasses must re-read `ctx.raw` if they need the post-fix content
|
|
350
|
+
* (in practice the base class updates `ctx.raw` in place when `fix` is true).
|
|
351
|
+
*/
|
|
352
|
+
export class BaseLinter {
|
|
353
|
+
/**
|
|
354
|
+
* Insert one or more lines into a markdown body at a safe location.
|
|
355
|
+
*
|
|
356
|
+
* "Safe" means: not inside a markdown table, HTML table, fenced code block,
|
|
357
|
+
* or indented code block. If `proposedLineNumber` falls inside one of those
|
|
358
|
+
* regions, the helper pushes the insertion to immediately after the region.
|
|
359
|
+
* This is a regression guard against the class of bug where an auto-fix
|
|
360
|
+
* splits a table fence by injecting a callout between the separator row
|
|
361
|
+
* and the first data row (broke `knowledge/akm-cli-reference.md` in 0.8.0).
|
|
362
|
+
*
|
|
363
|
+
* Subclasses that perform line-based body insertion MUST route through this
|
|
364
|
+
* helper instead of calling `splice` directly. Insertion fixers must NOT
|
|
365
|
+
* touch frontmatter — use `fixMissingUpdated` / `fixUnquotedColon` style
|
|
366
|
+
* regex edits for that case (those already operate inside the `---…---`
|
|
367
|
+
* fence and don't intersect with body line numbers).
|
|
368
|
+
*
|
|
369
|
+
* @param raw Full file contents (frontmatter + body).
|
|
370
|
+
* @param newLines Lines to insert (without trailing newlines).
|
|
371
|
+
* @param proposedLineNumber 0-based line index within `raw` where the
|
|
372
|
+
* caller wants the new content to appear.
|
|
373
|
+
* @returns The mutated file contents with `newLines` spliced at the
|
|
374
|
+
* adjusted safe position.
|
|
375
|
+
*/
|
|
376
|
+
insertLinesSafely(raw, newLines, proposedLineNumber) {
|
|
377
|
+
const lines = raw.split(/\r?\n/);
|
|
378
|
+
const safeIdx = findSafeInsertionPoint(lines, proposedLineNumber);
|
|
379
|
+
lines.splice(safeIdx, 0, ...newLines);
|
|
380
|
+
return lines.join("\n");
|
|
381
|
+
}
|
|
382
|
+
runBaseChecks(ctx) {
|
|
383
|
+
const issues = [];
|
|
384
|
+
let currentRaw = ctx.raw;
|
|
385
|
+
let modified = false;
|
|
386
|
+
// ── 1. unquoted-colon ──────────────────────────────────────────────────
|
|
387
|
+
const unquotedColonDetail = checkUnquotedColon(ctx.frontmatter);
|
|
388
|
+
if (unquotedColonDetail) {
|
|
389
|
+
if (ctx.fix) {
|
|
390
|
+
currentRaw = fixUnquotedColon(currentRaw);
|
|
391
|
+
modified = true;
|
|
392
|
+
issues.push({
|
|
393
|
+
file: ctx.relPath,
|
|
394
|
+
issue: "unquoted-colon",
|
|
395
|
+
detail: unquotedColonDetail,
|
|
396
|
+
fixed: true,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
issues.push({
|
|
401
|
+
file: ctx.relPath,
|
|
402
|
+
issue: "unquoted-colon",
|
|
403
|
+
detail: unquotedColonDetail,
|
|
404
|
+
fixed: false,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ── 2. missing-updated ─────────────────────────────────────────────────
|
|
409
|
+
if (checkMissingUpdated(ctx.data, ctx.frontmatter)) {
|
|
410
|
+
if (ctx.fix) {
|
|
411
|
+
let mtime;
|
|
412
|
+
try {
|
|
413
|
+
mtime = fs.statSync(ctx.filePath).mtime;
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
mtime = new Date();
|
|
417
|
+
}
|
|
418
|
+
currentRaw = fixMissingUpdated(currentRaw, mtime);
|
|
419
|
+
modified = true;
|
|
420
|
+
issues.push({
|
|
421
|
+
file: ctx.relPath,
|
|
422
|
+
issue: "missing-updated",
|
|
423
|
+
detail: `stamped updated: ${formatDate(mtime)}`,
|
|
424
|
+
fixed: true,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
issues.push({
|
|
429
|
+
file: ctx.relPath,
|
|
430
|
+
issue: "missing-updated",
|
|
431
|
+
detail: "no updated field in frontmatter",
|
|
432
|
+
fixed: false,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (modified) {
|
|
437
|
+
fs.writeFileSync(ctx.filePath, currentRaw, "utf8");
|
|
438
|
+
// Propagate the mutated raw back so subclasses can re-parse if needed
|
|
439
|
+
ctx.raw = currentRaw;
|
|
440
|
+
}
|
|
441
|
+
// ── 3. stale-path ──────────────────────────────────────────────────────
|
|
442
|
+
const stalePathMatch = checkStalePath(ctx.body);
|
|
443
|
+
if (stalePathMatch) {
|
|
444
|
+
issues.push({
|
|
445
|
+
file: ctx.relPath,
|
|
446
|
+
issue: "stale-path",
|
|
447
|
+
detail: `nonexistent path: ${stalePathMatch}`,
|
|
448
|
+
fixed: false,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
452
|
+
// Carve-out for assets that declare an explicit `refs:` array in
|
|
453
|
+
// frontmatter (e.g. session-checkpoint memories captured by the
|
|
454
|
+
// claude-code hook). The frontmatter array is the *authoritative*
|
|
455
|
+
// ref list — any ref-shaped tokens in the body are treated as
|
|
456
|
+
// literal strings (heredocs, grep patterns, JSON values, regex
|
|
457
|
+
// patterns embedded in tool transcripts). Without this carve-out
|
|
458
|
+
// every session capture produces a fresh batch of `missing-ref`
|
|
459
|
+
// flags on every literal `<type>:<slug>` token in a transcript.
|
|
460
|
+
//
|
|
461
|
+
// The producer guarantees that entries in `refs:` already resolve
|
|
462
|
+
// (it validates against the live stash before writing), so we
|
|
463
|
+
// still run `checkMissingRefs` against the array itself to catch
|
|
464
|
+
// refs that were valid at capture time but later removed from the
|
|
465
|
+
// stash.
|
|
466
|
+
const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
|
|
467
|
+
const refSource = explicitRefs !== null ? explicitRefs.join("\n") : ctx.body;
|
|
468
|
+
const missingRefs = checkMissingRefs(refSource, ctx.stashRoot, ctx.extraStashRoots);
|
|
469
|
+
for (const { ref, resolvedRelPath } of missingRefs) {
|
|
470
|
+
issues.push({
|
|
471
|
+
file: ctx.relPath,
|
|
472
|
+
issue: "missing-ref",
|
|
473
|
+
detail: `missing ref: ${ref} (resolved to ${resolvedRelPath})`,
|
|
474
|
+
fixed: false,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return issues;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { BaseLinter } from "./base-linter";
|
|
6
|
+
/**
|
|
7
|
+
* Linter for `commands/` assets.
|
|
8
|
+
*
|
|
9
|
+
* Extra check beyond base:
|
|
10
|
+
* - `missing-name-or-type`: frontmatter exists but `name` or `type` field is
|
|
11
|
+
* absent. Not auto-fixable; detail includes a suggested slug.
|
|
12
|
+
*/
|
|
13
|
+
export class CommandLinter extends BaseLinter {
|
|
14
|
+
types = ["commands"];
|
|
15
|
+
lint(ctx) {
|
|
16
|
+
const issues = this.runBaseChecks(ctx);
|
|
17
|
+
const missingFieldDetail = this.#checkMissingNameOrType(ctx.data, ctx.frontmatter);
|
|
18
|
+
if (missingFieldDetail) {
|
|
19
|
+
const slug = this.#suggestSlug(ctx.filePath);
|
|
20
|
+
issues.push({
|
|
21
|
+
file: ctx.relPath,
|
|
22
|
+
issue: "missing-name-or-type",
|
|
23
|
+
detail: `${missingFieldDetail}; suggested slug: ${slug}`,
|
|
24
|
+
fixed: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return issues;
|
|
28
|
+
}
|
|
29
|
+
#checkMissingNameOrType(data, frontmatterText) {
|
|
30
|
+
if (!frontmatterText)
|
|
31
|
+
return null;
|
|
32
|
+
const missingFields = [];
|
|
33
|
+
if (!("name" in data) || !data.name)
|
|
34
|
+
missingFields.push("name");
|
|
35
|
+
if (!("type" in data) || !data.type)
|
|
36
|
+
missingFields.push("type");
|
|
37
|
+
if (missingFields.length === 0)
|
|
38
|
+
return null;
|
|
39
|
+
return `missing fields: ${missingFields.join(", ")}`;
|
|
40
|
+
}
|
|
41
|
+
#suggestSlug(filePath) {
|
|
42
|
+
return path
|
|
43
|
+
.basename(filePath, ".md")
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
46
|
+
.replace(/-+/g, "-")
|
|
47
|
+
.replace(/^-|-$/g, "");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { BaseLinter } from "./base-linter";
|
|
5
|
+
/**
|
|
6
|
+
* Default linter for asset types that have no type-specific rules beyond the
|
|
7
|
+
* base checks (`unquoted-colon`, `missing-updated`).
|
|
8
|
+
*
|
|
9
|
+
* Covers: `lessons`.
|
|
10
|
+
*/
|
|
11
|
+
export class DefaultLinter extends BaseLinter {
|
|
12
|
+
types = ["lessons"];
|
|
13
|
+
lint(ctx) {
|
|
14
|
+
return this.runBaseChecks(ctx);
|
|
15
|
+
}
|
|
16
|
+
}
|