akm-cli 0.8.0-rc2 → 0.8.0
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} +191 -3
- package/README.md +22 -6
- 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 +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -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 +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- 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 +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -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 +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- 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 +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- 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 +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
package/dist/commands/tasks.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm tasks` — register, inspect, run, and remove scheduled task assets.
|
|
3
6
|
*
|
|
@@ -7,7 +10,7 @@
|
|
|
7
10
|
*/
|
|
8
11
|
import fs from "node:fs";
|
|
9
12
|
import path from "node:path";
|
|
10
|
-
import {
|
|
13
|
+
import { stringify as yamlStringify } from "yaml";
|
|
11
14
|
import { resolveAssetPathFromName } from "../core/asset-spec";
|
|
12
15
|
import { isWithin, resolveStashDir } from "../core/common";
|
|
13
16
|
import { loadConfig } from "../core/config";
|
|
@@ -21,10 +24,16 @@ import { resolveAkmInvocation } from "../tasks/resolveAkmBin";
|
|
|
21
24
|
import { exitCodeForStatus, readTaskHistory, runTask } from "../tasks/runner";
|
|
22
25
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT, translateToCron } from "../tasks/schedule";
|
|
23
26
|
import { validateTaskDocument } from "../tasks/validator";
|
|
27
|
+
import { resolveImproveProfile } from "./improve-profiles";
|
|
24
28
|
export async function akmTasksAdd(input) {
|
|
25
29
|
const id = normaliseTaskId(input.id);
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const hasCommand = input.command !== undefined &&
|
|
31
|
+
input.command !== null &&
|
|
32
|
+
!(typeof input.command === "string" && input.command.trim() === "") &&
|
|
33
|
+
!(Array.isArray(input.command) && input.command.length === 0);
|
|
34
|
+
const targetCount = [Boolean(input.workflow), Boolean(input.prompt), hasCommand].filter(Boolean).length;
|
|
35
|
+
if (targetCount !== 1) {
|
|
36
|
+
throw new UsageError("Pass exactly one of --workflow <ref>, --prompt <asset-ref|./file.md|text>, or --command <shell-command>.", "INVALID_FLAG_VALUE");
|
|
28
37
|
}
|
|
29
38
|
// Validate the schedule for the active backend before writing anything.
|
|
30
39
|
const backend = backendNameForPlatform();
|
|
@@ -39,20 +48,23 @@ export async function akmTasksAdd(input) {
|
|
|
39
48
|
if (fs.existsSync(assetPath) && !input.force) {
|
|
40
49
|
throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or use \`akm tasks remove ${id}\` first.`, "RESOURCE_ALREADY_EXISTS");
|
|
41
50
|
}
|
|
42
|
-
const
|
|
51
|
+
const yaml = renderTaskYaml({
|
|
43
52
|
id,
|
|
44
53
|
schedule: input.schedule,
|
|
45
54
|
workflow: input.workflow,
|
|
46
55
|
prompt: input.prompt,
|
|
56
|
+
command: input.command,
|
|
47
57
|
profile: input.profile,
|
|
48
58
|
params: input.params,
|
|
59
|
+
name: input.name,
|
|
49
60
|
description: input.description,
|
|
61
|
+
when_to_use: input.when_to_use,
|
|
50
62
|
tags: input.tags,
|
|
51
63
|
enabled: input.disabled !== true,
|
|
52
64
|
});
|
|
53
|
-
const task = parseTaskDocument({
|
|
65
|
+
const task = parseTaskDocument({ yaml, filePath: assetPath, id });
|
|
54
66
|
await validateTaskDocument(task, { backend, stashDir });
|
|
55
|
-
fs.writeFileSync(assetPath,
|
|
67
|
+
fs.writeFileSync(assetPath, yaml.endsWith("\n") ? yaml : `${yaml}\n`, "utf8");
|
|
56
68
|
// Install in the OS scheduler. If install fails after the file was written,
|
|
57
69
|
// delete the file so the on-disk state never claims a task is registered
|
|
58
70
|
// when it isn't.
|
|
@@ -80,19 +92,58 @@ export async function akmTasksAdd(input) {
|
|
|
80
92
|
target: task.target,
|
|
81
93
|
};
|
|
82
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Emit a single grouped stderr warning for legacy `.md` task files in the
|
|
97
|
+
* tasks directory. 0.8.0 requires task definitions to be pure `.yml`; any
|
|
98
|
+
* leftover `.md` files from 0.7.x would otherwise be silently skipped, which
|
|
99
|
+
* makes scheduled tasks vanish without operator notice. We do NOT auto-migrate
|
|
100
|
+
* — that is a separate workstream — but operators must see the affected files.
|
|
101
|
+
*
|
|
102
|
+
* `seen` is module-level so the warning is emitted at most once per process,
|
|
103
|
+
* even when both `akm tasks list` and `akm tasks sync` are invoked in the same
|
|
104
|
+
* akm run.
|
|
105
|
+
*/
|
|
106
|
+
const warnedLegacyMdDirs = new Set();
|
|
107
|
+
function warnLegacyMdTaskFiles(typeRoot) {
|
|
108
|
+
if (warnedLegacyMdDirs.has(typeRoot))
|
|
109
|
+
return;
|
|
110
|
+
let mdFiles;
|
|
111
|
+
try {
|
|
112
|
+
mdFiles = fs.readdirSync(typeRoot).filter((f) => f.endsWith(".md"));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (mdFiles.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
warnedLegacyMdDirs.add(typeRoot);
|
|
120
|
+
const affected = mdFiles.map((f) => `tasks/${f}`).join(", ");
|
|
121
|
+
process.stderr.write(`WARNING: ${mdFiles.length} task file(s) use the legacy .md format and were ignored.\n` +
|
|
122
|
+
` AKM 0.8.0 requires tasks as pure .yml. See docs/migration/v0.7-to-v0.8.md#task-definition-files-mdfrontmatter--yml.\n` +
|
|
123
|
+
` Affected: ${affected}\n`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reset the legacy `.md` task warning de-duplication state. Test-only escape
|
|
127
|
+
* hatch — production code should never call this.
|
|
128
|
+
*/
|
|
129
|
+
export function _resetLegacyMdTaskWarningStateForTests() {
|
|
130
|
+
warnedLegacyMdDirs.clear();
|
|
131
|
+
}
|
|
83
132
|
export async function akmTasksList() {
|
|
84
133
|
const stashDir = resolveStashDir();
|
|
85
134
|
const typeRoot = path.join(stashDir, "tasks");
|
|
86
135
|
if (!fs.existsSync(typeRoot))
|
|
87
136
|
return { tasks: [] };
|
|
88
|
-
const
|
|
137
|
+
const entries = fs.readdirSync(typeRoot);
|
|
138
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
139
|
+
const files = entries.filter((f) => f.endsWith(".yml"));
|
|
89
140
|
const tasks = [];
|
|
90
141
|
for (const file of files) {
|
|
91
|
-
const id = file.slice(0, -
|
|
142
|
+
const id = file.slice(0, -4);
|
|
92
143
|
const filePath = path.join(typeRoot, file);
|
|
93
144
|
let task;
|
|
94
145
|
try {
|
|
95
|
-
task = parseTaskDocument({
|
|
146
|
+
task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
|
|
96
147
|
}
|
|
97
148
|
catch {
|
|
98
149
|
continue; // skip malformed files; `akm tasks show <id>` will surface the error
|
|
@@ -104,7 +155,9 @@ export async function akmTasksList() {
|
|
|
104
155
|
schedule: task.schedule,
|
|
105
156
|
enabled: task.enabled,
|
|
106
157
|
target: task.target,
|
|
158
|
+
name: task.name,
|
|
107
159
|
description: task.description,
|
|
160
|
+
when_to_use: task.when_to_use,
|
|
108
161
|
tags: task.tags,
|
|
109
162
|
});
|
|
110
163
|
}
|
|
@@ -113,9 +166,12 @@ export async function akmTasksList() {
|
|
|
113
166
|
export async function akmTasksShow(id) {
|
|
114
167
|
const normalised = normaliseTaskId(id);
|
|
115
168
|
const stashDir = resolveStashDir();
|
|
169
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
170
|
+
if (fs.existsSync(typeRoot))
|
|
171
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
116
172
|
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
117
173
|
const task = parseTaskDocument({
|
|
118
|
-
|
|
174
|
+
yaml: fs.readFileSync(filePath, "utf8"),
|
|
119
175
|
filePath,
|
|
120
176
|
id: normalised,
|
|
121
177
|
});
|
|
@@ -128,13 +184,18 @@ export async function akmTasksShow(id) {
|
|
|
128
184
|
cron: translateToCron(spec),
|
|
129
185
|
enabled: task.enabled,
|
|
130
186
|
target: task.target,
|
|
187
|
+
name: task.name,
|
|
131
188
|
description: task.description,
|
|
189
|
+
when_to_use: task.when_to_use,
|
|
132
190
|
tags: task.tags,
|
|
133
191
|
};
|
|
134
192
|
}
|
|
135
193
|
export async function akmTasksRemove(id) {
|
|
136
194
|
const normalised = normaliseTaskId(id);
|
|
137
195
|
const stashDir = resolveStashDir();
|
|
196
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
197
|
+
if (fs.existsSync(typeRoot))
|
|
198
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
138
199
|
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
139
200
|
const sched = selectBackend();
|
|
140
201
|
try {
|
|
@@ -148,24 +209,31 @@ export async function akmTasksRemove(id) {
|
|
|
148
209
|
export async function akmTasksSetEnabled(id, enabled) {
|
|
149
210
|
const normalised = normaliseTaskId(id);
|
|
150
211
|
const stashDir = resolveStashDir();
|
|
212
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
213
|
+
if (fs.existsSync(typeRoot))
|
|
214
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
151
215
|
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
152
|
-
const
|
|
153
|
-
const updated =
|
|
216
|
+
const yaml = fs.readFileSync(filePath, "utf8");
|
|
217
|
+
const updated = setEnabledInYaml(yaml, enabled);
|
|
154
218
|
fs.writeFileSync(filePath, updated, "utf8");
|
|
155
219
|
const sched = selectBackend();
|
|
156
220
|
try {
|
|
157
221
|
await sched.setEnabled(normalised, enabled);
|
|
158
222
|
}
|
|
159
223
|
catch (err) {
|
|
160
|
-
// Roll the file back so the
|
|
224
|
+
// Roll the file back so the YAML source-of-truth and the OS
|
|
161
225
|
// scheduler don't diverge silently when the backend call fails.
|
|
162
|
-
fs.writeFileSync(filePath,
|
|
226
|
+
fs.writeFileSync(filePath, yaml, "utf8");
|
|
163
227
|
throw err;
|
|
164
228
|
}
|
|
165
229
|
return { id: normalised, enabled, backend: sched.name };
|
|
166
230
|
}
|
|
167
231
|
export async function akmTasksRun(id) {
|
|
168
232
|
const normalised = normaliseTaskId(id);
|
|
233
|
+
const stashDir = resolveStashDir();
|
|
234
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
235
|
+
if (fs.existsSync(typeRoot))
|
|
236
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
169
237
|
const result = await runTask(normalised);
|
|
170
238
|
return {
|
|
171
239
|
ok: result.status === "completed" || result.status === "disabled",
|
|
@@ -176,6 +244,10 @@ export async function akmTasksRun(id) {
|
|
|
176
244
|
export async function akmTasksHistory(input) {
|
|
177
245
|
const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
|
|
178
246
|
const id = input.id ? normaliseTaskId(input.id) : undefined;
|
|
247
|
+
const stashDir = resolveStashDir();
|
|
248
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
249
|
+
if (fs.existsSync(typeRoot))
|
|
250
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
179
251
|
return { rows: readTaskHistory({ id, limit }) };
|
|
180
252
|
}
|
|
181
253
|
/**
|
|
@@ -187,11 +259,13 @@ export async function akmTasksHistory(input) {
|
|
|
187
259
|
export async function akmTasksSync() {
|
|
188
260
|
const stashDir = resolveStashDir();
|
|
189
261
|
const typeRoot = path.join(stashDir, "tasks");
|
|
262
|
+
if (fs.existsSync(typeRoot))
|
|
263
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
190
264
|
const fileIds = fs.existsSync(typeRoot)
|
|
191
265
|
? fs
|
|
192
266
|
.readdirSync(typeRoot)
|
|
193
|
-
.filter((f) => f.endsWith(".
|
|
194
|
-
.map((f) => f.slice(0, -
|
|
267
|
+
.filter((f) => f.endsWith(".yml"))
|
|
268
|
+
.map((f) => f.slice(0, -4))
|
|
195
269
|
: [];
|
|
196
270
|
const sched = selectBackend();
|
|
197
271
|
const backend = backendNameForPlatform();
|
|
@@ -200,10 +274,10 @@ export async function akmTasksSync() {
|
|
|
200
274
|
const unchanged = [];
|
|
201
275
|
const skipped = [];
|
|
202
276
|
for (const id of fileIds) {
|
|
203
|
-
const filePath = path.join(typeRoot, `${id}.
|
|
277
|
+
const filePath = path.join(typeRoot, `${id}.yml`);
|
|
204
278
|
let task;
|
|
205
279
|
try {
|
|
206
|
-
task = parseTaskDocument({
|
|
280
|
+
task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
|
|
207
281
|
}
|
|
208
282
|
catch (err) {
|
|
209
283
|
skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
|
|
@@ -243,10 +317,32 @@ export async function akmTasksDoctor() {
|
|
|
243
317
|
catch (err) {
|
|
244
318
|
warnings.push(err instanceof Error ? err.message : String(err));
|
|
245
319
|
}
|
|
320
|
+
try {
|
|
321
|
+
const stashDir = resolveStashDir();
|
|
322
|
+
const typeRoot = path.join(stashDir, "tasks");
|
|
323
|
+
if (fs.existsSync(typeRoot))
|
|
324
|
+
warnLegacyMdTaskFiles(typeRoot);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// doctor must never fail on stash-resolution; the warning is best-effort
|
|
328
|
+
}
|
|
246
329
|
const backend = backendNameForPlatform();
|
|
247
330
|
const config = loadConfig();
|
|
248
|
-
|
|
249
|
-
const
|
|
331
|
+
// v2: prefer profiles.agent / defaults.agent; fall back to legacy agent.default
|
|
332
|
+
const defaultProfile = config.defaults?.agent;
|
|
333
|
+
const profiles = config.profiles?.agent ? Object.keys(config.profiles.agent) : listAgentProfileNames(config);
|
|
334
|
+
// §6.1: surface the effective triage settings for the default improve
|
|
335
|
+
// profile. The struct is a fixed shape, so this is a deliberate addition.
|
|
336
|
+
const improveProfileName = typeof config.defaults?.improve === "string" ? config.defaults.improve : "default";
|
|
337
|
+
const triage = resolveImproveProfile(config.defaults?.improve, config).processes?.triage;
|
|
338
|
+
const improveTriage = triage
|
|
339
|
+
? {
|
|
340
|
+
defaultProfile: improveProfileName,
|
|
341
|
+
enabled: triage.enabled === true,
|
|
342
|
+
applyMode: triage.applyMode ?? "queue",
|
|
343
|
+
policy: triage.policy ?? "personal-stash",
|
|
344
|
+
}
|
|
345
|
+
: undefined;
|
|
250
346
|
return {
|
|
251
347
|
backend,
|
|
252
348
|
akm: invocation,
|
|
@@ -255,12 +351,15 @@ export async function akmTasksDoctor() {
|
|
|
255
351
|
agent: { defaultProfile, available: profiles },
|
|
256
352
|
scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
|
|
257
353
|
warnings,
|
|
354
|
+
...(improveTriage ? { improveTriage } : {}),
|
|
258
355
|
};
|
|
259
356
|
}
|
|
260
357
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
261
358
|
const VALID_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
262
359
|
function normaliseTaskId(raw) {
|
|
263
|
-
|
|
360
|
+
// Accept both .yml and .md suffixes from users so muscle memory from the
|
|
361
|
+
// pre-0.8.0 markdown task format doesn't produce a confusing "task not found".
|
|
362
|
+
const id = raw.trim().replace(/\.(yml|md)$/, "");
|
|
264
363
|
if (!id) {
|
|
265
364
|
throw new UsageError("Task id must be non-empty.", "MISSING_REQUIRED_ARGUMENT");
|
|
266
365
|
}
|
|
@@ -269,63 +368,35 @@ function normaliseTaskId(raw) {
|
|
|
269
368
|
}
|
|
270
369
|
return id;
|
|
271
370
|
}
|
|
272
|
-
function
|
|
273
|
-
const
|
|
274
|
-
lines.push(`schedule: ${yamlQuote(input.schedule)}`);
|
|
371
|
+
function renderTaskYaml(input) {
|
|
372
|
+
const obj = { schedule: input.schedule };
|
|
275
373
|
if (input.workflow) {
|
|
276
|
-
|
|
374
|
+
obj.workflow = input.workflow;
|
|
277
375
|
if (input.params) {
|
|
278
|
-
|
|
279
|
-
lines.push("params:");
|
|
280
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
281
|
-
lines.push(` ${k}: ${yamlScalarValue(v)}`);
|
|
282
|
-
}
|
|
376
|
+
obj.params = parseJsonObjectArg(input.params);
|
|
283
377
|
}
|
|
284
378
|
}
|
|
285
379
|
else if (input.prompt) {
|
|
286
|
-
|
|
287
|
-
lines.push(`prompt: ${yamlQuote(input.prompt)}`);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
lines.push(`prompt: inline`);
|
|
291
|
-
}
|
|
380
|
+
obj.prompt = input.prompt;
|
|
292
381
|
if (input.profile)
|
|
293
|
-
|
|
382
|
+
obj.profile = input.profile;
|
|
294
383
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
384
|
+
else if (input.command !== undefined) {
|
|
385
|
+
// Emit a string when given a string, an array when given an array. The
|
|
386
|
+
// parser accepts both forms; preserving the caller's shape keeps the YAML
|
|
387
|
+
// ergonomic for humans editing the file later.
|
|
388
|
+
obj.command = input.command;
|
|
300
389
|
}
|
|
301
|
-
|
|
302
|
-
if (input.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
lines.push(input.prompt.trim(), "");
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return lines.join("\n");
|
|
315
|
-
}
|
|
316
|
-
function yamlQuote(value) {
|
|
317
|
-
if (/^[A-Za-z_][A-Za-z0-9_.\-/:]*$/.test(value))
|
|
318
|
-
return value;
|
|
319
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
320
|
-
}
|
|
321
|
-
function yamlScalarValue(v) {
|
|
322
|
-
if (typeof v === "string")
|
|
323
|
-
return yamlQuote(v);
|
|
324
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
325
|
-
return String(v);
|
|
326
|
-
if (v === null)
|
|
327
|
-
return "null";
|
|
328
|
-
return JSON.stringify(v);
|
|
390
|
+
obj.enabled = input.enabled;
|
|
391
|
+
if (input.name)
|
|
392
|
+
obj.name = input.name;
|
|
393
|
+
if (input.description)
|
|
394
|
+
obj.description = input.description;
|
|
395
|
+
if (input.when_to_use)
|
|
396
|
+
obj.when_to_use = input.when_to_use;
|
|
397
|
+
if (input.tags && input.tags.length > 0)
|
|
398
|
+
obj.tags = input.tags;
|
|
399
|
+
return yamlStringify(obj);
|
|
329
400
|
}
|
|
330
401
|
function parseJsonObjectArg(raw) {
|
|
331
402
|
let parsed;
|
|
@@ -340,31 +411,26 @@ function parseJsonObjectArg(raw) {
|
|
|
340
411
|
}
|
|
341
412
|
return parsed;
|
|
342
413
|
}
|
|
343
|
-
function looksLikeAssetRef(s) {
|
|
344
|
-
return /^[a-z][a-z0-9_-]*:[^\s]/i.test(s) && !s.startsWith("./") && !s.startsWith("/");
|
|
345
|
-
}
|
|
346
|
-
function isFilePath(s) {
|
|
347
|
-
return s.startsWith("./") || s.startsWith("../") || path.isAbsolute(s);
|
|
348
|
-
}
|
|
349
|
-
function humanise(id) {
|
|
350
|
-
return id.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
351
|
-
}
|
|
352
414
|
/**
|
|
353
|
-
* Toggle the `enabled:` value in a task
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
415
|
+
* Toggle the `enabled:` value in a task YAML file in-place without a full
|
|
416
|
+
* parse/render round-trip (which would reformat the file). Appends the key
|
|
417
|
+
* if absent.
|
|
418
|
+
*
|
|
419
|
+
* Preserves inline comments (e.g. `enabled: true # important`) and uses
|
|
420
|
+
* case-sensitive matching (YAML keys are case-sensitive).
|
|
357
421
|
*/
|
|
358
|
-
export function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
422
|
+
export function setEnabledInYaml(yaml, enabled) {
|
|
423
|
+
// Match: key prefix (group 1), value (group 2), optional trailing comment (group 3)
|
|
424
|
+
const pattern = /^(enabled:\s*)([^\s#\r\n][^\r\n]*?)(\s*(?:#[^\r\n]*))?$/m;
|
|
425
|
+
if (pattern.test(yaml)) {
|
|
426
|
+
return yaml.replace(pattern, `$1${enabled}$3`);
|
|
427
|
+
}
|
|
428
|
+
// Handle the case where enabled: has no value yet (bare key)
|
|
429
|
+
const simplePattern = /^(enabled:)\s*$/m;
|
|
430
|
+
if (simplePattern.test(yaml)) {
|
|
431
|
+
return yaml.replace(simplePattern, `$1 ${enabled}`);
|
|
362
432
|
}
|
|
363
|
-
|
|
364
|
-
const replaced = fmBody.match(/(^|\r?\n)enabled:\s*[^\r\n]*/i)
|
|
365
|
-
? fmBody.replace(/(^|\r?\n)enabled:\s*[^\r\n]*/i, `$1enabled: ${enabled}`)
|
|
366
|
-
: `${fmBody}\nenabled: ${enabled}`;
|
|
367
|
-
return `${openFence}${replaced}${closeFence}${body}`;
|
|
433
|
+
return `${yaml.trimEnd()}\nenabled: ${enabled}\n`;
|
|
368
434
|
}
|
|
369
435
|
// Re-exported so tests can verify the validator path directly.
|
|
370
436
|
// Re-export error classes consumed by callers that want to instanceof-check.
|
|
@@ -375,11 +441,11 @@ export { ConfigError, exitCodeForStatus, NotFoundError, parseTaskDocument, Usage
|
|
|
375
441
|
// user passes a ref, we accept the bare name part too.
|
|
376
442
|
export function parseTaskRef(input) {
|
|
377
443
|
if (input.includes(":")) {
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
444
|
+
const [typePart, ...rest] = input.split(":");
|
|
445
|
+
if (typePart !== "task" || rest.length === 0) {
|
|
380
446
|
throw new UsageError(`Expected a task id or task:<id> ref, got "${input}".`, "INVALID_FLAG_VALUE");
|
|
381
447
|
}
|
|
382
|
-
return { id: normaliseTaskId(
|
|
448
|
+
return { id: normaliseTaskId(rest.join(":")) };
|
|
383
449
|
}
|
|
384
450
|
return { id: normaliseTaskId(input) };
|
|
385
451
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
|
|
2
5
|
const TIMEOUT_MS = 5000;
|
|
3
6
|
const MAX_URLS = 20;
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
import { defaultRendererRegistry } from "./asset-registry";
|
|
2
5
|
function registryActionContributor(registry) {
|
|
3
6
|
return {
|
package/dist/core/asset-ref.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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/.
|
|
1
4
|
import path from "node:path";
|
|
2
5
|
import { isAssetType } from "./common";
|
|
3
6
|
import { UsageError } from "./errors";
|
|
7
|
+
/** Accepted spelling aliases mapping to a canonical asset type. */
|
|
8
|
+
const TYPE_ALIASES = {
|
|
9
|
+
environment: "env",
|
|
10
|
+
};
|
|
4
11
|
// ── Construction ────────────────────────────────────────────────────────────
|
|
5
12
|
/**
|
|
6
13
|
* Build a ref string from components.
|
|
@@ -46,12 +53,16 @@ export function parseAssetRef(ref) {
|
|
|
46
53
|
}
|
|
47
54
|
const rawType = body.slice(0, colon);
|
|
48
55
|
const rawName = body.slice(colon + 1);
|
|
49
|
-
|
|
56
|
+
// Type aliases: `environment:` is an accepted spelling of the canonical
|
|
57
|
+
// `env:` type. (`vault:` remains its own deprecated type so the frozen
|
|
58
|
+
// `vaults/` copy keeps resolving through the 0.8.x window.)
|
|
59
|
+
const resolvedType = TYPE_ALIASES[rawType] ?? rawType;
|
|
60
|
+
if (!isAssetType(resolvedType)) {
|
|
50
61
|
throw new UsageError(`Invalid asset type: "${rawType}".`, "MISSING_REQUIRED_ARGUMENT");
|
|
51
62
|
}
|
|
52
63
|
validateName(rawName);
|
|
53
64
|
const name = normalizeName(rawName);
|
|
54
|
-
return { type:
|
|
65
|
+
return { type: resolvedType, name, origin: origin || undefined };
|
|
55
66
|
}
|
|
56
67
|
// ── Validation ──────────────────────────────────────────────────────────────
|
|
57
68
|
function validateName(name) {
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Central registry for asset type renderer and action builder maps.
|
|
3
6
|
*
|
|
@@ -21,9 +24,11 @@ export const TYPE_TO_RENDERER = {
|
|
|
21
24
|
lesson: "lesson-md",
|
|
22
25
|
memory: "memory-md",
|
|
23
26
|
workflow: "workflow-md",
|
|
27
|
+
env: "env-file",
|
|
24
28
|
vault: "vault-env",
|
|
29
|
+
secret: "secret-file",
|
|
25
30
|
wiki: "wiki-md",
|
|
26
|
-
task: "task-
|
|
31
|
+
task: "task-yaml",
|
|
27
32
|
};
|
|
28
33
|
/** Map asset types to action builder functions for search results. */
|
|
29
34
|
export const ACTION_BUILDERS = {
|
|
@@ -35,7 +40,9 @@ export const ACTION_BUILDERS = {
|
|
|
35
40
|
lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
36
41
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
37
42
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
38
|
-
|
|
43
|
+
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (the agent-safe path — values never reach stdout). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
|
|
44
|
+
vault: (ref) => `DEPRECATED (use env): akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with injected env`,
|
|
45
|
+
secret: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
|
|
39
46
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
40
47
|
task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
|
|
41
48
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Canonical asset-on-disk serialization.
|
|
6
|
+
*
|
|
7
|
+
* Before this module, 9+ call sites across `src/` independently reimplemented
|
|
8
|
+
* `yamlStringify(fm).trimEnd() + "---\n…\n---\n\n${body}"` to assemble a
|
|
9
|
+
* Markdown asset. The reimplementations drifted (different body normalization,
|
|
10
|
+
* different separator newlines, different trailing-newline policy), which is
|
|
11
|
+
* exactly the kind of silent format-shift the proposal-quality validators end
|
|
12
|
+
* up chasing downstream. This file is the single point of truth for
|
|
13
|
+
* "what does a well-formed AKM asset look like on disk".
|
|
14
|
+
*
|
|
15
|
+
* Two helpers are exported:
|
|
16
|
+
* - `serializeFrontmatter(fm)` — YAML for the frontmatter block only, with
|
|
17
|
+
* no `---` fences and no trailing newline. Single home for quoting style,
|
|
18
|
+
* field-order policy, and trailing-whitespace rules.
|
|
19
|
+
* - `assembleAsset(fm, body)` — frontmatter wrapped in `---` fences with a
|
|
20
|
+
* blank line between the closing fence and the body, and exactly one
|
|
21
|
+
* trailing `\n`. Single home for body normalization and the file-shape
|
|
22
|
+
* contract.
|
|
23
|
+
*
|
|
24
|
+
* Contract (must hold for the dedup to be safe):
|
|
25
|
+
* - Idempotent: `parseFrontmatter(assembleAsset(fm, body))` re-assembled
|
|
26
|
+
* reproduces the same bytes.
|
|
27
|
+
* - Field order: insertion order of `fm` is preserved (the caller controls
|
|
28
|
+
* ordering; the helper never reorders).
|
|
29
|
+
* - Quoting: `yaml.stringify` defaults — no custom quoting logic.
|
|
30
|
+
* - Trailing newline: exactly one `\n` at end of output.
|
|
31
|
+
* - Body normalization: leading newlines are stripped (`/^\n+/`). This
|
|
32
|
+
* collapses the assorted `body.replace(/^\n+/, "")` /
|
|
33
|
+
* `body.startsWith("\n") ? "" : "\n" + body` / bare `${body}` patterns
|
|
34
|
+
* onto the most aggressive existing normalizer.
|
|
35
|
+
*/
|
|
36
|
+
import { stringify as yamlStringify } from "yaml";
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a frontmatter object to its on-disk YAML form, without `---`
|
|
39
|
+
* fences and without a trailing newline.
|
|
40
|
+
*
|
|
41
|
+
* Two calls with the same input produce byte-identical output. Field order is
|
|
42
|
+
* preserved from the input object's insertion order — callers control
|
|
43
|
+
* ordering, the helper never reorders.
|
|
44
|
+
*/
|
|
45
|
+
export function serializeFrontmatter(frontmatter) {
|
|
46
|
+
return yamlStringify(frontmatter).trimEnd();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Assemble a complete asset file string from a frontmatter object and a body.
|
|
50
|
+
*
|
|
51
|
+
* Output shape: `---\n<yaml>\n---\n\n<body>\n` where:
|
|
52
|
+
* - `<yaml>` is `serializeFrontmatter(frontmatter)`.
|
|
53
|
+
* - `<body>` has any leading `\n` characters stripped.
|
|
54
|
+
* - Exactly one `\n` terminates the file.
|
|
55
|
+
*
|
|
56
|
+
* Idempotent under round-trip through the project's `parseFrontmatter`.
|
|
57
|
+
*/
|
|
58
|
+
export function assembleAsset(frontmatter, body) {
|
|
59
|
+
return assembleAssetFromString(serializeFrontmatter(frontmatter), body);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Same fence/body assembly as `assembleAsset` but takes a pre-serialized
|
|
63
|
+
* frontmatter string. Use this when a caller needs its own frontmatter
|
|
64
|
+
* serializer (e.g. defensive single-line flattening for untrusted LLM
|
|
65
|
+
* output, or JSON.stringify-per-value for guaranteed-quoted scalars) while
|
|
66
|
+
* still sharing the canonical fence-and-body template.
|
|
67
|
+
*
|
|
68
|
+
* The `serializedFm` argument must already match `serializeFrontmatter`'s
|
|
69
|
+
* contract: no `---` fences, no trailing newline. Trailing whitespace is
|
|
70
|
+
* trimmed defensively.
|
|
71
|
+
*
|
|
72
|
+
* Output contract — identical to `assembleAsset`:
|
|
73
|
+
* - `---\n<serializedFm>\n---\n\n<body>\n`
|
|
74
|
+
* - body has leading `\n` characters stripped
|
|
75
|
+
* - exactly one `\n` terminates the file
|
|
76
|
+
*
|
|
77
|
+
* This helper is the single point of truth for the fence-and-body template.
|
|
78
|
+
* Three command surfaces (`reflect`, `distill`, `consolidate`) call it
|
|
79
|
+
* directly because their inputs are pre-validated LLM payloads where the
|
|
80
|
+
* full `yamlStringify` may emit shapes (`|`-block scalars, anchors) that
|
|
81
|
+
* the project's hand-rolled `parseFrontmatter` subset parser cannot read.
|
|
82
|
+
*/
|
|
83
|
+
export function assembleAssetFromString(serializedFm, body) {
|
|
84
|
+
const yaml = serializedFm.replace(/\s+$/, "");
|
|
85
|
+
const normalizedBody = body.replace(/^\n+/, "");
|
|
86
|
+
const withTrailingNewline = normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`;
|
|
87
|
+
return `---\n${yaml}\n---\n\n${withTrailingNewline}`;
|
|
88
|
+
}
|