akm-cli 0.9.25-alpha.1 → 0.9.25-alpha.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/CHANGELOG.md +243 -280
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -9
- package/dist/assets/prompts/reflect-llm-schema-contract.md +1 -3
- package/dist/assets/prompts/reflect-output-repair.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/improve/consolidate/pair-pass.js +1 -0
- package/dist/commands/improve/consolidate.js +7 -2
- package/dist/commands/improve/execution.js +2 -3
- package/dist/commands/improve/extract-cli.js +3 -2
- package/dist/commands/improve/extract.js +2 -1
- package/dist/commands/improve/improve-cli.js +33 -1
- package/dist/commands/improve/loop-stages.js +3 -0
- package/dist/commands/improve/reflect-noise.js +125 -0
- package/dist/commands/improve/reflect.js +150 -333
- package/dist/commands/improve/retrieval-gate.js +7 -2
- package/dist/commands/improve/session-asset.js +6 -0
- package/dist/commands/improve/stage.js +31 -39
- package/dist/commands/proposal/drain.js +4 -7
- package/dist/commands/proposal/propose.js +2 -11
- package/dist/commands/proposal/validators/proposal-quality-validators.js +11 -5
- package/dist/commands/proposal/validators/proposal-validators.js +4 -5
- package/dist/commands/read/search-cli.js +0 -38
- package/dist/core/asset/asset-serialize.js +1 -1
- package/dist/core/config/schema/engines.js +15 -33
- package/dist/core/config/schema/improve-processes.js +16 -0
- package/dist/core/content-safety.js +0 -24
- package/dist/core/redaction.js +4 -0
- package/dist/core/spawn-env.js +25 -0
- package/dist/core/structured.js +1 -1
- package/dist/execution/source.js +8 -12
- package/dist/integrations/agent/config.js +1 -3
- package/dist/integrations/agent/engine-resolution.js +0 -3
- package/dist/integrations/agent/execution.js +14 -13
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/model-map.js +15 -16
- package/dist/integrations/agent/profiles.js +2 -2
- package/dist/integrations/agent/prompts.js +51 -127
- package/dist/integrations/agent/request-lowering.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +25 -31
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -2
- package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -2
- package/dist/integrations/harnesses/claude/agent-builder.js +4 -16
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
- package/dist/integrations/harnesses/codex/index.js +6 -11
- package/dist/integrations/harnesses/codex/session-log.js +211 -0
- package/dist/integrations/harnesses/ids.js +10 -16
- package/dist/integrations/harnesses/opencode/agent-builder.js +14 -24
- package/dist/integrations/harnesses/opencode/model-config.js +15 -62
- package/dist/integrations/harnesses/opencode/model-work-agent.js +71 -36
- package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +32 -90
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -2
- package/dist/integrations/harnesses/pi/agent-builder.js +1 -2
- package/dist/integrations/harnesses/types.js +3 -3
- package/dist/llm/feature-gate.js +2 -5
- package/dist/llm/index-passes.js +2 -2
- package/dist/llm/structured-call.js +5 -5
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/akm-migrate-node.js +381 -239
- package/dist/scripts/akm-migrate.js +381 -239
- package/dist/workflows/exec/unit-dispatch.js +4 -13
- package/docs/reference/cli.md +29 -16
- package/docs/reference/configuration.md +79 -85
- package/docs/reference/data-and-telemetry.md +2 -3
- package/docs/reference/workflow-schema.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +108 -36
|
@@ -53,6 +53,10 @@ import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strateg
|
|
|
53
53
|
import { isContentDrivenRow, isLedgerBlocked, ledgerKey, loadLedgerSnapshot, recordLedgerAttempt } from "./ledger.js";
|
|
54
54
|
import { isInRetrievalScope, loadRetrievalScope } from "./retrieval-scope.js";
|
|
55
55
|
import { callStage, mintProposal, noticeSet, stageRunner } from "./stage.js";
|
|
56
|
+
function parsePlan(raw) {
|
|
57
|
+
const plan = parseEmbeddedJsonResponse(raw);
|
|
58
|
+
return plan && Array.isArray(plan.operations) ? { ...plan, operations: plan.operations } : undefined;
|
|
59
|
+
}
|
|
56
60
|
/** A plan op worth acting on. Retired advisory ops (merge/delete/contradict) are dropped, never thrown on. */
|
|
57
61
|
export function isValidOp(op) {
|
|
58
62
|
if (typeof op !== "object" || op === null)
|
|
@@ -570,6 +574,7 @@ async function judgeConsolidationChunks(args) {
|
|
|
570
574
|
...(Object.hasOwn(llmRunner, "timeoutMs") ? { timeoutMs: llmRunner.timeoutMs } : {}),
|
|
571
575
|
signal: opts.signal,
|
|
572
576
|
},
|
|
577
|
+
parse: parsePlan,
|
|
573
578
|
...(opts.onNotices ? { onNotices: opts.onNotices } : {}),
|
|
574
579
|
});
|
|
575
580
|
if (!outcome.ok) {
|
|
@@ -577,8 +582,8 @@ async function judgeConsolidationChunks(args) {
|
|
|
577
582
|
continue;
|
|
578
583
|
}
|
|
579
584
|
warnVerbose(`[akm:consolidate] ${label} raw response (first 500 chars): ${outcome.raw.slice(0, 500)}`);
|
|
580
|
-
const parsed =
|
|
581
|
-
if (!parsed
|
|
585
|
+
const parsed = parsePlan(outcome.raw);
|
|
586
|
+
if (!parsed) {
|
|
582
587
|
const hint = outcome.raw.trim() === "" ? " (empty response — if using a thinking model, disable thinking mode)" : "";
|
|
583
588
|
const msg = `Chunk ${chunkIdx + 1}: invalid plan from AI — skipping.${hint}`;
|
|
584
589
|
warn(msg);
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { deepMergeConfig } from "../../core/config/deep-merge.js";
|
|
5
|
-
import { MODEL_WORK_TOOLS } from "../../execution/source.js";
|
|
6
5
|
import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
|
|
7
6
|
function own(value, key) {
|
|
8
7
|
return value !== undefined && Object.hasOwn(value, key);
|
|
@@ -36,12 +35,12 @@ export function resolveImproveExecution(options) {
|
|
|
36
35
|
if (selectedEngine === undefined || selectedEngine === null)
|
|
37
36
|
return null;
|
|
38
37
|
const invocationDefaults = mergeDefaults(mergeDefaults(defaultEngine ? { engine: defaultEngine } : {}, profileDefaults), indexDefaults);
|
|
39
|
-
const current = { ...mergeDefaults(processDefaults, currentDefaults), tools: MODEL_WORK_TOOLS };
|
|
40
38
|
const prepared = resolveExecution({
|
|
41
39
|
content: `improve ${options.processName} execution selection`,
|
|
42
40
|
config: options.config,
|
|
43
41
|
invocationDefaults,
|
|
44
|
-
current,
|
|
42
|
+
current: mergeDefaults(processDefaults, currentDefaults),
|
|
43
|
+
modelWork: true,
|
|
45
44
|
});
|
|
46
45
|
const lowered = buildExecution(prepared.request, prepared.runner);
|
|
47
46
|
return Object.freeze({ runner: lowered.runner, notices: lowered.notices });
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* akm proposal extract --type claude --session-id <id>
|
|
11
11
|
* akm proposal extract --type claude --since 24h
|
|
12
12
|
* akm proposal extract --type opencode --since 7d --dry-run
|
|
13
|
+
* akm proposal extract --type codex --since 24h
|
|
13
14
|
* akm proposal extract --auto # iterate all available harnesses
|
|
14
15
|
* akm proposal extract --type claude --location /custom/path --session-id <id>
|
|
15
16
|
*
|
|
@@ -25,12 +26,12 @@ import { akmExtract, resolveStandaloneExtractPlan } from "./extract.js";
|
|
|
25
26
|
export const extractCommand = defineJsonCommand({
|
|
26
27
|
meta: {
|
|
27
28
|
name: "extract",
|
|
28
|
-
description: "Extract durable insights from native session files (claude, opencode) and queue them as proposals.",
|
|
29
|
+
description: "Extract durable insights from native session files (claude, codex, opencode) and queue them as proposals.",
|
|
29
30
|
},
|
|
30
31
|
args: {
|
|
31
32
|
type: {
|
|
32
33
|
type: "string",
|
|
33
|
-
description: "Harness name (claude, opencode). Required unless --auto.",
|
|
34
|
+
description: "Harness name (claude, codex, opencode). Required unless --auto.",
|
|
34
35
|
},
|
|
35
36
|
"session-id": {
|
|
36
37
|
type: "string",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* `akm extract` — read native session logs (claude, opencode) through the
|
|
5
|
+
* `akm extract` — read native session logs (claude, codex, opencode) through the
|
|
6
6
|
* session-log harnesses, pre-filter the noise, and ask the model for
|
|
7
7
|
* memory/lesson/knowledge candidates the agent did not already save. Each
|
|
8
8
|
* candidate is queued as a proposal (`source: "extract"`), never written.
|
|
@@ -741,6 +741,7 @@ function resolveExtractRun(options, config, process, activeProfile) {
|
|
|
741
741
|
...(options.signal ? { signal: options.signal } : {}),
|
|
742
742
|
...(options.chat ? { chat: options.chat } : {}),
|
|
743
743
|
},
|
|
744
|
+
parse: parseSessionSummary,
|
|
744
745
|
onNotices: notices.add,
|
|
745
746
|
});
|
|
746
747
|
return parseSessionSummary(outcome.ok ? outcome.raw : "");
|
|
@@ -20,13 +20,15 @@ import { collectEngineCredentialValues } from "../../integrations/agent/engine-r
|
|
|
20
20
|
import { probeLlmReachable } from "../../llm/client.js";
|
|
21
21
|
import { getOutputMode } from "../../output/context.js";
|
|
22
22
|
import { deliverRendered } from "../../output/html-render.js";
|
|
23
|
+
import { readStdin } from "../../runtime.js";
|
|
23
24
|
import { akmImprove, IMPROVE_TARGET_FLAG, resolveImproveReadSource } from "./improve.js";
|
|
24
25
|
import { runImproveReportQuery } from "./improve-report.js";
|
|
25
26
|
import { buildImproveRunId, recordImproveRunResult, recordTerminatedImproveRun, } from "./improve-result-file.js";
|
|
26
27
|
import { runImproveSession } from "./improve-session.js";
|
|
27
|
-
import { resolveImprovePlan, } from "./improve-strategies.js";
|
|
28
|
+
import { resolveImprovePlan, resolveImproveStrategy, } from "./improve-strategies.js";
|
|
28
29
|
import { formatUsageReportTable } from "./improve-usage-report.js";
|
|
29
30
|
import { renderReflectPromptPreview } from "./reflect.js";
|
|
31
|
+
import { resolveQualityGateJudge, runReflectQualityJudge } from "./stage.js";
|
|
30
32
|
let akmImproveForRun = akmImprove;
|
|
31
33
|
/** Swap the CLI's improve work implementation in deterministic subprocess tests. */
|
|
32
34
|
export function _setAkmImproveForTests(fake) {
|
|
@@ -188,6 +190,32 @@ function rejectReportOnlyFlags(args) {
|
|
|
188
190
|
return;
|
|
189
191
|
throw new UsageError(`\`${flag}\` only applies to \`akm improve report\`. Use \`akm improve report ${flag} <value>\` instead.`, "INVALID_FLAG_VALUE");
|
|
190
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* `akm improve judge`: reflect's quality judge on one revision, read as
|
|
195
|
+
* `{"source", "candidate", "feedback"}` JSON from stdin, with the engine the
|
|
196
|
+
* strategy's reflect quality gate names. It writes nothing.
|
|
197
|
+
*/
|
|
198
|
+
async function runImproveJudgeCli(strategyName) {
|
|
199
|
+
const input = process.stdin.isTTY
|
|
200
|
+
? {}
|
|
201
|
+
: JSON.parse((await readStdin()).toString("utf8"));
|
|
202
|
+
const { source, candidate, feedback, ref } = input;
|
|
203
|
+
if (typeof source !== "string" || typeof candidate !== "string") {
|
|
204
|
+
throw new UsageError('`akm improve judge` reads {"source": "...", "candidate": "...", "feedback": "...", "ref": "..."} JSON from stdin.', "MISSING_REQUIRED_ARGUMENT");
|
|
205
|
+
}
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
const judge = resolveQualityGateJudge(config, resolveImproveStrategy(strategyName, config).config, "reflect");
|
|
208
|
+
if (!judge) {
|
|
209
|
+
throw new ConfigError("`akm improve judge` judges with the reflect quality gate's engine. Set processes.reflect.qualityGate.engine.", "INVALID_CONFIG_FILE");
|
|
210
|
+
}
|
|
211
|
+
const notes = typeof feedback === "string" && feedback.trim() !== "" ? [feedback.trim()] : [];
|
|
212
|
+
const verdict = await runReflectQualityJudge(config, candidate, source, notes, undefined, {
|
|
213
|
+
runnerSelectionFrozen: true,
|
|
214
|
+
llmRunner: judge,
|
|
215
|
+
...(typeof ref === "string" && ref ? { ref } : {}),
|
|
216
|
+
});
|
|
217
|
+
output("improve-judge", { engine: judge.engine, ...verdict });
|
|
218
|
+
}
|
|
191
219
|
export const improveCommand = defineCommand({
|
|
192
220
|
meta: {
|
|
193
221
|
name: "improve",
|
|
@@ -270,6 +298,10 @@ export const improveCommand = defineCommand({
|
|
|
270
298
|
return;
|
|
271
299
|
}
|
|
272
300
|
rejectReportOnlyFlags(args);
|
|
301
|
+
if (getStringArg(args, "scope") === "judge") {
|
|
302
|
+
await runImproveJudgeCli(getStringArg(args, "strategy"));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
273
305
|
rejectRetiredImproveTargetFlag();
|
|
274
306
|
const jsonToStdout = args["json-to-stdout"];
|
|
275
307
|
const targetArg = getStringArg(args, "bundle");
|
|
@@ -149,6 +149,9 @@ async function runLoopReflectPass(planned, env, tally) {
|
|
|
149
149
|
...(reflectErrors.length > 0 ? { avoidPatterns: [...reflectErrors] } : {}),
|
|
150
150
|
eventSource: "improve",
|
|
151
151
|
lowValueFilter: improveProfile.processes?.reflect?.lowValueFilter?.enabled === true,
|
|
152
|
+
...(improveProfile.processes?.reflect?.defectFilter
|
|
153
|
+
? { defectFilter: improveProfile.processes.reflect.defectFilter }
|
|
154
|
+
: {}),
|
|
152
155
|
...(budgetMs > 0 ? { timeoutMs: budgetMs } : {}),
|
|
153
156
|
signal: env.budgetSignal,
|
|
154
157
|
eventsCtx: env.eventsCtx,
|
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
* cosmetic edit through costs a review, suppressing a real fix loses work — so
|
|
10
10
|
* anything uncertain is `substantive`: code (fenced or indented) compares
|
|
11
11
|
* verbatim, and headings, tables and breaks never absorb the next line.
|
|
12
|
+
*
|
|
13
|
+
* `findReflectDefect` is its counterpart for an edit that is not noise but a
|
|
14
|
+
* defect no judge needs to weigh: placeholder text, talk about the edit itself,
|
|
15
|
+
* or frontmatter copied into the body.
|
|
12
16
|
*/
|
|
13
17
|
import { parse as yamlParse } from "yaml";
|
|
18
|
+
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
14
19
|
/**
|
|
15
20
|
* `noop` (identical up to trailing whitespace) and `cosmetic` (identical after
|
|
16
21
|
* normalizing frontmatter as parsed YAML and prose as unwrapped text) never
|
|
@@ -36,6 +41,126 @@ export function classifyReflectChange(sourceContent, candidateContent) {
|
|
|
36
41
|
}
|
|
37
42
|
return "substantive";
|
|
38
43
|
}
|
|
44
|
+
/** Not TODO, TBD or FIXME: an asset may carry those on purpose, and the owner does not want them refused. */
|
|
45
|
+
const DEFAULT_PLACEHOLDERS = [
|
|
46
|
+
"please confirm",
|
|
47
|
+
"please verify",
|
|
48
|
+
"to be confirmed",
|
|
49
|
+
"to be determined",
|
|
50
|
+
"to be verified",
|
|
51
|
+
];
|
|
52
|
+
const DEFAULT_META_COMMENTARY = [
|
|
53
|
+
"feedback signal",
|
|
54
|
+
"feedback signals",
|
|
55
|
+
"feedback indicate",
|
|
56
|
+
"feedback indicates",
|
|
57
|
+
"feedback suggest",
|
|
58
|
+
"feedback suggests",
|
|
59
|
+
"feedback ask",
|
|
60
|
+
"feedback asks",
|
|
61
|
+
"feedback says",
|
|
62
|
+
"feedback report",
|
|
63
|
+
"feedback reports",
|
|
64
|
+
"feedback request",
|
|
65
|
+
"feedback requests",
|
|
66
|
+
"this revision",
|
|
67
|
+
"the source asset",
|
|
68
|
+
"the source note",
|
|
69
|
+
"the source memory",
|
|
70
|
+
"the original asset",
|
|
71
|
+
"the original note",
|
|
72
|
+
"the original memory",
|
|
73
|
+
"the original version of this",
|
|
74
|
+
"quality gate rejected",
|
|
75
|
+
"proposal rejected",
|
|
76
|
+
];
|
|
77
|
+
const DEFAULT_FRONTMATTER_KEYS = [
|
|
78
|
+
"sources",
|
|
79
|
+
"updated",
|
|
80
|
+
"inferenceProcessed",
|
|
81
|
+
"captureMode",
|
|
82
|
+
"beliefState",
|
|
83
|
+
"xrefs",
|
|
84
|
+
"contradictedBy",
|
|
85
|
+
"outcomeData",
|
|
86
|
+
"orderedActions",
|
|
87
|
+
"generated",
|
|
88
|
+
"verified",
|
|
89
|
+
"description",
|
|
90
|
+
"when_to_use",
|
|
91
|
+
"tags",
|
|
92
|
+
"searchHints",
|
|
93
|
+
"quality",
|
|
94
|
+
"salience",
|
|
95
|
+
"salienceInputs",
|
|
96
|
+
"lint_skip",
|
|
97
|
+
"type",
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* The first defect the candidate has and its source lacks, or `undefined`. Each
|
|
101
|
+
* rule counts only what the revision adds, so text the asset already carried is
|
|
102
|
+
* not held against it. On 396 labelled reflect edits (83 good, 313 bad) the
|
|
103
|
+
* default lists hit 22 bad edits and no good one, so a hit is refused unjudged.
|
|
104
|
+
*/
|
|
105
|
+
export function findReflectDefect(sourceContent, candidateContent, filter = {}) {
|
|
106
|
+
if (gainsPhrases(filter.placeholders ?? DEFAULT_PLACEHOLDERS, sourceContent, candidateContent)) {
|
|
107
|
+
return "placeholder_added";
|
|
108
|
+
}
|
|
109
|
+
if (gainsPhrases(filter.metaCommentary ?? DEFAULT_META_COMMENTARY, sourceContent, candidateContent)) {
|
|
110
|
+
return "meta_commentary_added";
|
|
111
|
+
}
|
|
112
|
+
if (frontmatterCopiedIntoBody(sourceContent, candidateContent, filter.frontmatterKeys ?? DEFAULT_FRONTMATTER_KEYS)) {
|
|
113
|
+
return "frontmatter_copied_into_body";
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/** Frontmatter fields that name other assets; one of their values newly in the body is provenance copied over. */
|
|
118
|
+
const PROVENANCE_KEYS = ["sources", "xrefs", "contradictedBy"];
|
|
119
|
+
/** Shorter values (a bare name or id) say too little to find in a body. */
|
|
120
|
+
const PROVENANCE_VALUE_MIN_CHARS = 12;
|
|
121
|
+
function escapeRegExp(text) {
|
|
122
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
123
|
+
}
|
|
124
|
+
/** Whether the candidate holds more of the phrases than the source: whole words, any case, any whitespace between words. */
|
|
125
|
+
function gainsPhrases(phrases, source, candidate) {
|
|
126
|
+
const alternatives = phrases
|
|
127
|
+
.map((phrase) => phrase.trim().split(/\s+/).map(escapeRegExp).join("\\s+"))
|
|
128
|
+
.filter((alternative) => alternative !== "");
|
|
129
|
+
if (alternatives.length === 0)
|
|
130
|
+
return false;
|
|
131
|
+
const pattern = new RegExp(`(?<!\\w)(?:${alternatives.join("|")})(?!\\w)`, "gi");
|
|
132
|
+
return (candidate.match(pattern)?.length ?? 0) > (source.match(pattern)?.length ?? 0);
|
|
133
|
+
}
|
|
134
|
+
/** The body gains a line that starts with one of the frontmatter keys outside code, or a provenance value from either asset's frontmatter. */
|
|
135
|
+
function frontmatterCopiedIntoBody(source, candidate, keys) {
|
|
136
|
+
if (keys.length === 0)
|
|
137
|
+
return false;
|
|
138
|
+
const keyLine = new RegExp(`^(?:${keys.map(escapeRegExp).join("|")}):(?:[ \\t].*)?$`);
|
|
139
|
+
const keyLines = (text) => parseLowValueSections(text).proseLines.filter((line) => keyLine.test(line)).length;
|
|
140
|
+
if (keyLines(candidate) > keyLines(source))
|
|
141
|
+
return true;
|
|
142
|
+
const sourceBody = lettersAndDigits(splitFrontmatter(source).body);
|
|
143
|
+
const candidateBody = lettersAndDigits(splitFrontmatter(candidate).body);
|
|
144
|
+
return [...provenanceValues(source), ...provenanceValues(candidate)].some((value) => {
|
|
145
|
+
const v = lettersAndDigits(value);
|
|
146
|
+
return v.length >= PROVENANCE_VALUE_MIN_CHARS && candidateBody.includes(v) && !sourceBody.includes(v);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Every string under the provenance keys of a frontmatter block: a scalar, or the items of a list. */
|
|
150
|
+
function provenanceValues(content) {
|
|
151
|
+
const { data } = parseFrontmatter(content);
|
|
152
|
+
return PROVENANCE_KEYS.flatMap((key) => {
|
|
153
|
+
const value = data[key];
|
|
154
|
+
return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string");
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Lower-case letters and digits, each run of anything else a single space. */
|
|
158
|
+
function lettersAndDigits(text) {
|
|
159
|
+
return text
|
|
160
|
+
.toLowerCase()
|
|
161
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
162
|
+
.trim();
|
|
163
|
+
}
|
|
39
164
|
/** A `---` frontmatter block and the rest (`fmText: null` when there is none). */
|
|
40
165
|
export function splitFrontmatter(raw) {
|
|
41
166
|
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|