akm-cli 0.6.0-rc1 → 0.6.0-rc2

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +9 -9
  3. package/dist/cli.js +181 -111
  4. package/dist/{completions.js → commands/completions.js} +1 -1
  5. package/dist/{config-cli.js → commands/config-cli.js} +109 -11
  6. package/dist/{curate.js → commands/curate.js} +8 -3
  7. package/dist/{info.js → commands/info.js} +15 -9
  8. package/dist/{init.js → commands/init.js} +4 -4
  9. package/dist/{install-audit.js → commands/install-audit.js} +4 -7
  10. package/dist/{installed-stashes.js → commands/installed-stashes.js} +77 -31
  11. package/dist/{migration-help.js → commands/migration-help.js} +2 -2
  12. package/dist/{registry-search.js → commands/registry-search.js} +8 -6
  13. package/dist/{remember.js → commands/remember.js} +55 -49
  14. package/dist/{stash-search.js → commands/search.js} +28 -69
  15. package/dist/{self-update.js → commands/self-update.js} +3 -3
  16. package/dist/{stash-show.js → commands/show.js} +103 -78
  17. package/dist/{stash-add.js → commands/source-add.js} +42 -32
  18. package/dist/{stash-clone.js → commands/source-clone.js} +12 -10
  19. package/dist/{stash-source-manage.js → commands/source-manage.js} +24 -24
  20. package/dist/{vault.js → commands/vault.js} +43 -0
  21. package/dist/{stash-ref.js → core/asset-ref.js} +4 -4
  22. package/dist/{asset-registry.js → core/asset-registry.js} +1 -1
  23. package/dist/{asset-spec.js → core/asset-spec.js} +1 -1
  24. package/dist/{config.js → core/config.js} +79 -31
  25. package/dist/core/errors.js +90 -0
  26. package/dist/{frontmatter.js → core/frontmatter.js} +5 -3
  27. package/dist/core/write-source.js +280 -0
  28. package/dist/{db-search.js → indexer/db-search.js} +25 -19
  29. package/dist/{db.js → indexer/db.js} +70 -47
  30. package/dist/{file-context.js → indexer/file-context.js} +3 -3
  31. package/dist/{indexer.js → indexer/indexer.js} +123 -31
  32. package/dist/{manifest.js → indexer/manifest.js} +10 -10
  33. package/dist/{matchers.js → indexer/matchers.js} +3 -6
  34. package/dist/{metadata.js → indexer/metadata.js} +9 -5
  35. package/dist/{search-source.js → indexer/search-source.js} +52 -41
  36. package/dist/{semantic-status.js → indexer/semantic-status.js} +2 -2
  37. package/dist/{walker.js → indexer/walker.js} +1 -1
  38. package/dist/{lockfile.js → integrations/lockfile.js} +1 -1
  39. package/dist/{llm-client.js → llm/client.js} +1 -1
  40. package/dist/{embedders → llm/embedders}/local.js +2 -2
  41. package/dist/{embedders → llm/embedders}/remote.js +1 -1
  42. package/dist/{embedders → llm/embedders}/types.js +1 -1
  43. package/dist/{metadata-enhance.js → llm/metadata-enhance.js} +2 -2
  44. package/dist/{cli-hints.js → output/cli-hints.js} +1 -0
  45. package/dist/{output-context.js → output/context.js} +21 -3
  46. package/dist/{renderers.js → output/renderers.js} +9 -65
  47. package/dist/{output-shapes.js → output/shapes.js} +18 -4
  48. package/dist/{output-text.js → output/text.js} +1 -1
  49. package/dist/{registry-build-index.js → registry/build-index.js} +16 -7
  50. package/dist/{create-provider-registry.js → registry/create-provider-registry.js} +6 -2
  51. package/dist/registry/factory.js +33 -0
  52. package/dist/{origin-resolve.js → registry/origin-resolve.js} +1 -1
  53. package/dist/{providers → registry/providers}/index.js +1 -1
  54. package/dist/{providers → registry/providers}/skills-sh.js +59 -3
  55. package/dist/{providers → registry/providers}/static-index.js +80 -12
  56. package/dist/registry/providers/types.js +25 -0
  57. package/dist/{registry-resolve.js → registry/resolve.js} +3 -3
  58. package/dist/{detect.js → setup/detect.js} +0 -27
  59. package/dist/{ripgrep-install.js → setup/ripgrep-install.js} +1 -1
  60. package/dist/{ripgrep-resolve.js → setup/ripgrep-resolve.js} +2 -2
  61. package/dist/{setup.js → setup/setup.js} +16 -56
  62. package/dist/{stash-include.js → sources/include.js} +1 -1
  63. package/dist/sources/provider-factory.js +36 -0
  64. package/dist/sources/provider.js +21 -0
  65. package/dist/sources/providers/filesystem.js +35 -0
  66. package/dist/{stash-providers → sources/providers}/git.js +53 -64
  67. package/dist/{stash-providers → sources/providers}/index.js +3 -4
  68. package/dist/sources/providers/install-types.js +14 -0
  69. package/dist/{stash-providers → sources/providers}/npm.js +42 -41
  70. package/dist/{stash-providers → sources/providers}/provider-utils.js +3 -3
  71. package/dist/{stash-providers → sources/providers}/sync-from-ref.js +2 -2
  72. package/dist/{stash-providers → sources/providers}/tar-utils.js +11 -8
  73. package/dist/{stash-providers → sources/providers}/website.js +29 -65
  74. package/dist/{stash-resolve.js → sources/resolve.js} +8 -7
  75. package/dist/{wiki.js → wiki/wiki.js} +12 -11
  76. package/dist/{workflow-authoring.js → workflows/authoring.js} +37 -14
  77. package/dist/{workflow-cli.js → workflows/cli.js} +2 -1
  78. package/dist/{workflow-db.js → workflows/db.js} +1 -1
  79. package/dist/workflows/document-cache.js +20 -0
  80. package/dist/workflows/parser.js +379 -0
  81. package/dist/workflows/renderer.js +78 -0
  82. package/dist/{workflow-runs.js → workflows/runs.js} +72 -28
  83. package/dist/workflows/schema.js +11 -0
  84. package/dist/workflows/validator.js +48 -0
  85. package/docs/migration/release-notes/0.6.0.md +69 -23
  86. package/package.json +1 -1
  87. package/dist/errors.js +0 -45
  88. package/dist/llm.js +0 -16
  89. package/dist/registry-factory.js +0 -19
  90. package/dist/ripgrep.js +0 -2
  91. package/dist/stash-provider-factory.js +0 -35
  92. package/dist/stash-provider.js +0 -3
  93. package/dist/stash-providers/filesystem.js +0 -71
  94. package/dist/stash-providers/openviking.js +0 -348
  95. package/dist/stash-types.js +0 -1
  96. package/dist/workflow-markdown.js +0 -260
  97. /package/dist/{common.js → core/common.js} +0 -0
  98. /package/dist/{markdown.js → core/markdown.js} +0 -0
  99. /package/dist/{paths.js → core/paths.js} +0 -0
  100. /package/dist/{warn.js → core/warn.js} +0 -0
  101. /package/dist/{search-fields.js → indexer/search-fields.js} +0 -0
  102. /package/dist/{usage-events.js → indexer/usage-events.js} +0 -0
  103. /package/dist/{github.js → integrations/github.js} +0 -0
  104. /package/dist/{embedder.js → llm/embedder.js} +0 -0
  105. /package/dist/{embedders → llm/embedders}/cache.js +0 -0
  106. /package/dist/{registry-provider.js → registry/types.js} +0 -0
  107. /package/dist/{setup-steps.js → setup/steps.js} +0 -0
  108. /package/dist/{registry-types.js → sources/types.js} +0 -0
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { resolveAssetPathFromName } from "./asset-spec";
4
- import { isWithin, resolveStashDir } from "./common";
5
- import { UsageError } from "./errors";
6
- import { warn } from "./warn";
7
- import { parseWorkflowMarkdown, WorkflowValidationError } from "./workflow-markdown";
3
+ import { resolveAssetPathFromName } from "../core/asset-spec";
4
+ import { isWithin, resolveStashDir } from "../core/common";
5
+ import { UsageError } from "../core/errors";
6
+ import { warn } from "../core/warn";
7
+ import { parseWorkflow } from "./parser";
8
8
  const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate({
9
9
  title: "Example Workflow",
10
10
  firstStepTitle: "First Step",
@@ -23,7 +23,10 @@ export function buildWorkflowTemplate(name) {
23
23
  firstStepTitle: `${title} Setup`,
24
24
  firstStepId: `${stepId}-setup`,
25
25
  });
26
- parseWorkflowMarkdown(customized);
26
+ const result = parseWorkflow(customized, { path: `<template:${name}>` });
27
+ if (!result.ok) {
28
+ throw new UsageError(formatWorkflowErrors(`<template:${name}>`, result.errors));
29
+ }
27
30
  return customized;
28
31
  }
29
32
  export function createWorkflowAsset(input) {
@@ -41,14 +44,10 @@ export function createWorkflowAsset(input) {
41
44
  const content = input.from
42
45
  ? readWorkflowSource(input.from, stashDir)
43
46
  : (input.content ?? buildWorkflowTemplate(normalizedName));
44
- try {
45
- parseWorkflowMarkdown(content);
46
- }
47
- catch (error) {
48
- if (error instanceof WorkflowValidationError) {
49
- throw new UsageError(error.message);
50
- }
51
- throw error;
47
+ const sourcePath = input.from ?? `workflows/${normalizedName}.md`;
48
+ const result = parseWorkflow(content, { path: sourcePath });
49
+ if (!result.ok) {
50
+ throw new UsageError(formatWorkflowErrors(sourcePath, result.errors));
52
51
  }
53
52
  fs.mkdirSync(path.dirname(assetPath), { recursive: true });
54
53
  fs.writeFileSync(assetPath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
@@ -110,6 +109,30 @@ function slugifyWorkflowStepId(name) {
110
109
  .replace(/[^a-z0-9]+/g, "-")
111
110
  .replace(/^-+|-+$/g, "") || "workflow");
112
111
  }
112
+ export function formatWorkflowErrors(path, errors) {
113
+ const lines = errors.map((e) => ` ${path}:${e.line} — ${e.message}`);
114
+ const heading = errors.length === 1 ? "Workflow has 1 error:" : `Workflow has ${errors.length} errors:`;
115
+ return [heading, ...lines].join("\n");
116
+ }
117
+ /**
118
+ * Validate a workflow by ref (`workflow:<name>`) or filesystem path.
119
+ *
120
+ * Returns the parse result plus the source-relative path used. Throws
121
+ * `UsageError` only when the target cannot be located on disk; parse
122
+ * failures are returned as `{ ok: false, errors }` so callers can
123
+ * format them however they like.
124
+ */
125
+ export function validateWorkflowSource(target) {
126
+ if (target.startsWith("workflow:")) {
127
+ throw new UsageError(`validateWorkflowSource expects a filesystem path; resolve refs to paths in the caller before invoking.`);
128
+ }
129
+ const resolved = path.resolve(target);
130
+ if (!fs.existsSync(resolved)) {
131
+ throw new UsageError(`Workflow file not found: "${target}".`);
132
+ }
133
+ const content = fs.readFileSync(resolved, "utf8");
134
+ return { path: target, parse: parseWorkflow(content, { path: target }) };
135
+ }
113
136
  function renderWorkflowTemplate(input) {
114
137
  return `---
115
138
  description: Describe what this workflow accomplishes
@@ -1,4 +1,4 @@
1
- import { UsageError } from "./errors";
1
+ import { UsageError } from "../core/errors";
2
2
  export const WORKFLOW_STEP_STATES = [
3
3
  "completed",
4
4
  "blocked",
@@ -14,6 +14,7 @@ export const WORKFLOW_SUBCOMMANDS = new Set([
14
14
  "create",
15
15
  "template",
16
16
  "resume",
17
+ "validate",
17
18
  ]);
18
19
  export function parseWorkflowJsonObject(raw, flagName) {
19
20
  if (!raw)
@@ -1,7 +1,7 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { getWorkflowDbPath } from "./paths";
4
+ import { getWorkflowDbPath } from "../core/paths";
5
5
  export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
6
6
  const dir = path.dirname(dbPath);
7
7
  if (!fs.existsSync(dir)) {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Side-channel cache that lets the workflow renderer hand a validated
3
+ * `WorkflowDocument` to the indexer without persisting it through the
4
+ * `entry_json` column or widening `StashEntry` with a workflow-shaped field.
5
+ *
6
+ * The renderer is called during metadata generation; the indexer writes the
7
+ * document to `workflow_documents` after `upsertEntry` returns the row id.
8
+ * A WeakMap keyed by the entry object preserves the parse work between the
9
+ * two phases without leaking memory if the entry is dropped.
10
+ */
11
+ const cache = new WeakMap();
12
+ export function cacheWorkflowDocument(entry, doc) {
13
+ cache.set(entry, doc);
14
+ }
15
+ export function takeWorkflowDocument(entry) {
16
+ const doc = cache.get(entry);
17
+ if (doc !== undefined)
18
+ cache.delete(entry);
19
+ return doc;
20
+ }
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Workflow markdown → WorkflowDocument JSON.
3
+ *
4
+ * Composition over invention: frontmatter is parsed with the `yaml` package,
5
+ * heading discovery with `parseMarkdownToc`, and section bodies with
6
+ * `extractLineRange` — all already in the codebase. The parser walks the
7
+ * heading list once to assemble a `WorkflowDocument` with `SourceRef`
8
+ * line spans, accumulating `WorkflowError`s rather than throwing.
9
+ */
10
+ import { parse as yamlParse } from "yaml";
11
+ import { parseFrontmatterBlock } from "../core/frontmatter";
12
+ import { parseMarkdownToc } from "../core/markdown";
13
+ import { WORKFLOW_SCHEMA_VERSION, } from "./schema";
14
+ import { runSemanticChecks } from "./validator";
15
+ const WORKFLOW_TITLE_PREFIX = "Workflow:";
16
+ const STEP_PREFIX = "Step:";
17
+ const STEP_ID_LINE = /^Step ID:\s+(.+?)\s*$/;
18
+ const BULLET_LINE = /^[-*]\s+(.+)$/;
19
+ const SUBSECTION_INSTRUCTIONS = "Instructions";
20
+ const SUBSECTION_COMPLETION_CRITERIA = "Completion Criteria";
21
+ /**
22
+ * Cheap structural probe for the matcher. Returns true if the body has the
23
+ * unmistakable shape of a workflow file. Used in `src/indexer/matchers.ts` so
24
+ * the matcher and parser cannot drift.
25
+ */
26
+ export function looksLikeWorkflow(body) {
27
+ return (/^#\s+Workflow:\s+/m.test(body) &&
28
+ /^##\s+Step:\s+/m.test(body) &&
29
+ /^Step ID:\s+/m.test(body) &&
30
+ /^###\s+Instructions\s*$/m.test(body));
31
+ }
32
+ export function parseWorkflow(markdown, source) {
33
+ const errors = [];
34
+ const path = source.path;
35
+ const lines = markdown.split(/\r?\n/);
36
+ const totalLines = lines.length;
37
+ const fmBlock = parseFrontmatterBlock(markdown);
38
+ const frontmatterEndLine = fmBlock ? Math.max(1, fmBlock.bodyStartLine - 1) : 1;
39
+ const fmData = readFrontmatter(fmBlock?.frontmatter, errors);
40
+ const description = readDescription(fmData);
41
+ const tags = readTags(fmData, errors, frontmatterEndLine);
42
+ const parameters = readParameters(fmData, errors, frontmatterEndLine, path);
43
+ const toc = parseMarkdownToc(markdown);
44
+ const { title, titleLine } = extractTitle(toc.headings, errors);
45
+ // Disallow stray level-1 and non-Step level-2 headings.
46
+ for (const h of toc.headings) {
47
+ if (h.level === 1 && !h.text.startsWith(WORKFLOW_TITLE_PREFIX)) {
48
+ errors.push({
49
+ line: h.line,
50
+ message: `Unexpected top-level heading "# ${h.text}" on line ${h.line}. A workflow file may only contain one "# Workflow: <title>" heading.`,
51
+ });
52
+ }
53
+ if (h.level === 2 && !h.text.startsWith(STEP_PREFIX)) {
54
+ errors.push({
55
+ line: h.line,
56
+ message: `Unexpected level-2 heading "## ${h.text}" on line ${h.line}. Only "## Step: <title>" sections are allowed.`,
57
+ });
58
+ }
59
+ }
60
+ const steps = extractSteps(toc.headings, lines, totalLines, path, errors);
61
+ if (steps.length === 0 && titleLine > 0) {
62
+ errors.push({
63
+ line: titleLine,
64
+ message: `Workflow has no "## Step: <title>" sections. Add at least one step.`,
65
+ });
66
+ }
67
+ const draft = {
68
+ schemaVersion: WORKFLOW_SCHEMA_VERSION,
69
+ title,
70
+ ...(description ? { description } : {}),
71
+ ...(tags ? { tags } : {}),
72
+ ...(parameters ? { parameters } : {}),
73
+ steps,
74
+ source: { path, lineCount: totalLines },
75
+ };
76
+ runSemanticChecks(draft, fmData, frontmatterEndLine, errors);
77
+ if (errors.length > 0) {
78
+ return { ok: false, errors: sortErrors(errors) };
79
+ }
80
+ return { ok: true, document: draft };
81
+ }
82
+ // ── Title ───────────────────────────────────────────────────────────────────
83
+ function extractTitle(headings, errors) {
84
+ const titleHeadings = headings.filter((h) => h.level === 1 && h.text.startsWith(WORKFLOW_TITLE_PREFIX));
85
+ if (titleHeadings.length === 0) {
86
+ errors.push({
87
+ line: 1,
88
+ message: `Workflow markdown must start with a "# Workflow: <title>" heading. Add one at the top of the file.`,
89
+ });
90
+ return { title: "", titleLine: 0 };
91
+ }
92
+ if (titleHeadings.length > 1) {
93
+ for (const extra of titleHeadings.slice(1)) {
94
+ errors.push({
95
+ line: extra.line,
96
+ message: `Found a second "# Workflow:" heading on line ${extra.line}. A workflow file must contain exactly one.`,
97
+ });
98
+ }
99
+ }
100
+ const first = titleHeadings[0];
101
+ const title = first.text.slice(WORKFLOW_TITLE_PREFIX.length).trim();
102
+ if (!title) {
103
+ errors.push({
104
+ line: first.line,
105
+ message: `The "# Workflow:" heading on line ${first.line} is missing a title. Use "# Workflow: <title>".`,
106
+ });
107
+ }
108
+ return { title, titleLine: first.line };
109
+ }
110
+ // ── Steps ───────────────────────────────────────────────────────────────────
111
+ function extractSteps(headings, lines, totalLines, path, errors) {
112
+ const steps = [];
113
+ let sequenceIndex = 0;
114
+ for (let i = 0; i < headings.length; i++) {
115
+ const h = headings[i];
116
+ if (h.level !== 2 || !h.text.startsWith(STEP_PREFIX))
117
+ continue;
118
+ const stepTitle = h.text.slice(STEP_PREFIX.length).trim();
119
+ if (!stepTitle) {
120
+ errors.push({
121
+ line: h.line,
122
+ message: `The "## Step:" heading on line ${h.line} is missing a title. Use "## Step: <title>".`,
123
+ });
124
+ continue;
125
+ }
126
+ const stepEnd = Math.min(findNextSiblingOrParentLine(headings, i, 2) - 1, totalLines);
127
+ const stepSource = { path, start: h.line, end: stepEnd };
128
+ const subsections = collectSubsections(headings, i, stepEnd);
129
+ const stepIdSearchEnd = subsections.length > 0 ? subsections[0].headingLine - 1 : stepEnd;
130
+ const stepId = scanStepId(lines, h.line + 1, stepIdSearchEnd, stepTitle, errors);
131
+ const { instructions, completionCriteria } = collectStepBody(subsections, lines, path, stepTitle, errors);
132
+ if (!stepId)
133
+ continue; // scanStepId already pushed the missing-id error
134
+ if (!instructions) {
135
+ errors.push({
136
+ line: h.line,
137
+ message: `Step "${stepTitle}" is missing the required "### Instructions" section. Add one under the step.`,
138
+ });
139
+ continue;
140
+ }
141
+ steps.push({
142
+ id: stepId,
143
+ title: stepTitle,
144
+ sequenceIndex: sequenceIndex++,
145
+ instructions,
146
+ ...(completionCriteria ? { completionCriteria } : {}),
147
+ source: stepSource,
148
+ });
149
+ }
150
+ return steps;
151
+ }
152
+ function collectSubsections(headings, stepIndex, stepEnd) {
153
+ const subs = [];
154
+ for (let j = stepIndex + 1; j < headings.length; j++) {
155
+ const sub = headings[j];
156
+ if (sub.level <= 2)
157
+ break;
158
+ if (sub.level !== 3)
159
+ continue;
160
+ const next = headings[j + 1];
161
+ const rawEnd = next ? next.line - 1 : stepEnd;
162
+ subs.push({
163
+ name: sub.text,
164
+ headingLine: sub.line,
165
+ bodyStart: sub.line + 1,
166
+ bodyEnd: Math.min(rawEnd, stepEnd),
167
+ });
168
+ }
169
+ return subs;
170
+ }
171
+ function collectStepBody(subsections, lines, path, stepTitle, errors) {
172
+ let instructions;
173
+ let completionCriteria;
174
+ for (const sub of subsections) {
175
+ if (sub.name === SUBSECTION_INSTRUCTIONS) {
176
+ if (instructions) {
177
+ errors.push({
178
+ line: sub.headingLine,
179
+ message: `Step "${stepTitle}" has more than one "### Instructions" section (line ${sub.headingLine}). Keep only one.`,
180
+ });
181
+ continue;
182
+ }
183
+ const text = sliceLines(lines, sub.bodyStart, sub.bodyEnd).trim();
184
+ if (!text) {
185
+ errors.push({
186
+ line: sub.headingLine,
187
+ message: `Step "${stepTitle}" has an empty "### Instructions" section. Add the instructions text below the heading.`,
188
+ });
189
+ continue;
190
+ }
191
+ instructions = {
192
+ text,
193
+ source: { path, start: sub.bodyStart, end: sub.bodyEnd },
194
+ };
195
+ continue;
196
+ }
197
+ if (sub.name === SUBSECTION_COMPLETION_CRITERIA) {
198
+ if (completionCriteria) {
199
+ errors.push({
200
+ line: sub.headingLine,
201
+ message: `Step "${stepTitle}" has more than one "### Completion Criteria" section (line ${sub.headingLine}). Keep only one.`,
202
+ });
203
+ continue;
204
+ }
205
+ const items = collectBullets(lines, sub.bodyStart, sub.bodyEnd, path);
206
+ if (items.length === 0) {
207
+ errors.push({
208
+ line: sub.headingLine,
209
+ message: `Step "${stepTitle}" has an empty "### Completion Criteria" section. Add at least one "- criterion" bullet.`,
210
+ });
211
+ continue;
212
+ }
213
+ completionCriteria = items;
214
+ continue;
215
+ }
216
+ errors.push({
217
+ line: sub.headingLine,
218
+ message: `Step "${stepTitle}" has an unknown "### ${sub.name}" section. Only "### Instructions" and "### Completion Criteria" are supported.`,
219
+ });
220
+ }
221
+ return {
222
+ ...(instructions ? { instructions } : {}),
223
+ ...(completionCriteria ? { completionCriteria } : {}),
224
+ };
225
+ }
226
+ function scanStepId(lines, startLineInclusive, endLineInclusive, stepTitle, errors) {
227
+ let foundId;
228
+ let foundLine = -1;
229
+ for (let lineNum = startLineInclusive; lineNum <= endLineInclusive; lineNum++) {
230
+ const trimmed = (lines[lineNum - 1] ?? "").trim();
231
+ if (!trimmed)
232
+ continue;
233
+ const match = trimmed.match(STEP_ID_LINE);
234
+ if (!match)
235
+ continue;
236
+ if (foundId !== undefined) {
237
+ errors.push({
238
+ line: lineNum,
239
+ message: `Step "${stepTitle}" has more than one "Step ID:" line (first on line ${foundLine}). Keep only one.`,
240
+ });
241
+ continue;
242
+ }
243
+ foundId = match[1].trim();
244
+ foundLine = lineNum;
245
+ }
246
+ if (!foundId) {
247
+ errors.push({
248
+ line: startLineInclusive,
249
+ message: `Step "${stepTitle}" is missing a "Step ID: <id>" line. Add one between the step heading and its subsections.`,
250
+ });
251
+ }
252
+ return foundId;
253
+ }
254
+ function collectBullets(lines, startLineInclusive, endLineInclusive, path) {
255
+ const items = [];
256
+ for (let lineNum = startLineInclusive; lineNum <= endLineInclusive; lineNum++) {
257
+ const trimmed = (lines[lineNum - 1] ?? "").trim();
258
+ if (!trimmed)
259
+ continue;
260
+ const match = trimmed.match(BULLET_LINE);
261
+ if (!match)
262
+ continue;
263
+ items.push({
264
+ text: match[1].trim(),
265
+ source: { path, start: lineNum, end: lineNum },
266
+ });
267
+ }
268
+ return items;
269
+ }
270
+ function findNextSiblingOrParentLine(headings, fromIndex, level) {
271
+ for (let i = fromIndex + 1; i < headings.length; i++) {
272
+ if (headings[i].level <= level)
273
+ return headings[i].line;
274
+ }
275
+ return Number.MAX_SAFE_INTEGER;
276
+ }
277
+ function sliceLines(lines, startLineInclusive, endLineInclusive) {
278
+ if (endLineInclusive < startLineInclusive)
279
+ return "";
280
+ const s = Math.max(1, startLineInclusive);
281
+ const e = Math.min(endLineInclusive, lines.length);
282
+ return lines.slice(s - 1, e).join("\n");
283
+ }
284
+ // ── Frontmatter ─────────────────────────────────────────────────────────────
285
+ function readFrontmatter(frontmatter, errors) {
286
+ if (!frontmatter)
287
+ return {};
288
+ let parsed;
289
+ try {
290
+ parsed = yamlParse(frontmatter);
291
+ }
292
+ catch (err) {
293
+ const msg = err instanceof Error ? err.message : String(err);
294
+ errors.push({
295
+ line: 1,
296
+ message: `Workflow frontmatter is not valid YAML: ${msg}`,
297
+ });
298
+ return {};
299
+ }
300
+ if (parsed === null || parsed === undefined)
301
+ return {};
302
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
303
+ errors.push({
304
+ line: 1,
305
+ message: `Workflow frontmatter must be a YAML mapping (key: value pairs). Use "key: value" lines between the --- markers.`,
306
+ });
307
+ return {};
308
+ }
309
+ return parsed;
310
+ }
311
+ function readDescription(data) {
312
+ const v = data.description;
313
+ if (typeof v !== "string")
314
+ return undefined;
315
+ const trimmed = v.trim();
316
+ return trimmed || undefined;
317
+ }
318
+ function readTags(data, errors, fmEndLine) {
319
+ const v = data.tags;
320
+ if (v === undefined || v === null)
321
+ return undefined;
322
+ if (typeof v === "string") {
323
+ const t = v.trim();
324
+ return t ? [t] : undefined;
325
+ }
326
+ if (!Array.isArray(v) || !v.every((tag) => typeof tag === "string" && tag.trim().length > 0)) {
327
+ errors.push({
328
+ line: fmEndLine,
329
+ message: `Workflow frontmatter "tags" must be a string or a list of non-empty strings.`,
330
+ });
331
+ return undefined;
332
+ }
333
+ return v.map((tag) => tag.trim());
334
+ }
335
+ function readParameters(data, errors, fmEndLine, path) {
336
+ const v = data.params;
337
+ if (v === undefined || v === null)
338
+ return undefined;
339
+ if (typeof v !== "object" || Array.isArray(v)) {
340
+ errors.push({
341
+ line: fmEndLine,
342
+ message: `Workflow frontmatter "params" must be a mapping of parameter names to descriptions.`,
343
+ });
344
+ return undefined;
345
+ }
346
+ const entries = Object.entries(v);
347
+ if (entries.length === 0)
348
+ return undefined;
349
+ const out = [];
350
+ for (const [name, desc] of entries) {
351
+ const trimmedName = name.trim();
352
+ if (!trimmedName) {
353
+ errors.push({
354
+ line: fmEndLine,
355
+ message: `Workflow parameter names must be non-empty.`,
356
+ });
357
+ continue;
358
+ }
359
+ if (typeof desc !== "string" || !desc.trim()) {
360
+ errors.push({
361
+ line: fmEndLine,
362
+ message: `Workflow parameter "${trimmedName}" must have a non-empty string description in frontmatter "params".`,
363
+ });
364
+ continue;
365
+ }
366
+ out.push({
367
+ name: trimmedName,
368
+ description: desc.trim(),
369
+ // The frontmatter parser doesn't track per-key line numbers; anchor to the
370
+ // frontmatter block end so editors land somewhere sensible.
371
+ source: { path, start: 1, end: fmEndLine },
372
+ });
373
+ }
374
+ return out.length > 0 ? out : undefined;
375
+ }
376
+ // ── Error sorting ───────────────────────────────────────────────────────────
377
+ function sortErrors(errors) {
378
+ return [...errors].sort((a, b) => a.line - b.line);
379
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Show + indexing renderer for workflow assets.
3
+ *
4
+ * Reads the markdown via `parseWorkflow` and projects the validated
5
+ * `WorkflowDocument` down to the public `ShowResponse` shape (which still
6
+ * uses the flat `WorkflowStepDefinition` type for backwards compatibility)
7
+ * and into search hints for the indexer.
8
+ */
9
+ import { makeAssetRef } from "../core/asset-ref";
10
+ import { UsageError } from "../core/errors";
11
+ import { cacheWorkflowDocument } from "./document-cache";
12
+ import { parseWorkflow } from "./parser";
13
+ function shellQuote(value) {
14
+ return `'${value.replace(/'/g, `'\\''`)}'`;
15
+ }
16
+ export function buildWorkflowAction(ref) {
17
+ return `Resume the active run or start a new run with \`akm workflow next ${shellQuote(ref)}\`.`;
18
+ }
19
+ function deriveName(ctx) {
20
+ const metaName = ctx.matchResult.meta?.name;
21
+ if (typeof metaName === "string" && metaName)
22
+ return metaName;
23
+ const ext = ctx.relPath.lastIndexOf(".");
24
+ return ext > 0 ? ctx.relPath.slice(0, ext) : ctx.relPath;
25
+ }
26
+ function loadDocument(ctx) {
27
+ const result = parseWorkflow(ctx.content(), { path: ctx.relPath });
28
+ if (result.ok)
29
+ return result.document;
30
+ const summary = result.errors.map((e) => `${ctx.relPath}:${e.line} — ${e.message}`).join("\n");
31
+ throw new UsageError(`Workflow has errors:\n${summary}`);
32
+ }
33
+ export const workflowMdRenderer = {
34
+ name: "workflow-md",
35
+ buildShowResponse(ctx) {
36
+ const name = deriveName(ctx);
37
+ const doc = loadDocument(ctx);
38
+ const ref = makeAssetRef("workflow", name, ctx.origin);
39
+ return {
40
+ type: "workflow",
41
+ name,
42
+ path: ctx.absPath,
43
+ action: buildWorkflowAction(ref),
44
+ description: doc.description,
45
+ workflowTitle: doc.title,
46
+ parameters: doc.parameters?.map((p) => p.name),
47
+ workflowParameters: doc.parameters?.map((p) => ({ name: p.name, description: p.description })),
48
+ steps: doc.steps.map((s) => ({
49
+ id: s.id,
50
+ title: s.title,
51
+ instructions: s.instructions.text,
52
+ ...(s.completionCriteria ? { completionCriteria: s.completionCriteria.map((c) => c.text) } : {}),
53
+ sequenceIndex: s.sequenceIndex,
54
+ })),
55
+ };
56
+ },
57
+ extractMetadata(entry, ctx) {
58
+ const doc = loadDocument(ctx);
59
+ const hints = new Set(entry.searchHints ?? []);
60
+ hints.add(doc.title);
61
+ for (const step of doc.steps) {
62
+ hints.add(step.title);
63
+ hints.add(step.id);
64
+ hints.add(step.instructions.text);
65
+ for (const criterion of step.completionCriteria ?? []) {
66
+ hints.add(criterion.text);
67
+ }
68
+ }
69
+ entry.searchHints = Array.from(hints).filter(Boolean);
70
+ if (doc.parameters?.length) {
71
+ entry.parameters = doc.parameters.map((p) => ({
72
+ name: p.name,
73
+ ...(p.description ? { description: p.description } : {}),
74
+ }));
75
+ }
76
+ cacheWorkflowDocument(entry, doc);
77
+ },
78
+ };