@aidemd-mcp/server 0.2.3 → 0.3.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.
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import type { AideFile, AideFrontmatter } from "../../../types/index.js";
3
+ interface RenderPlanDetailProps {
4
+ file: AideFile;
5
+ frontmatter: AideFrontmatter | null;
6
+ /** Controls which rendering mode is active in the right panel. */
7
+ mode: "preview" | "drill-in";
8
+ /** Raw body content of the plan file. */
9
+ body: string;
10
+ /** File path shown as the panel title during drill-in, or null in preview mode. */
11
+ drilledFilePath: string | null;
12
+ }
13
+ /**
14
+ * Detail panel renderer for plan files (.aide files of type "plan").
15
+ *
16
+ * Preview mode: description from frontmatter, progress fraction, remaining count.
17
+ * Drill-in mode: grouped checklist items by step heading, completion summary at top.
18
+ */
19
+ export default function RenderPlanDetail({ file, frontmatter, mode, body, drilledFilePath, }: RenderPlanDetailProps): React.ReactElement;
20
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import parsePlanItems from "./parsePlanItems/index.js";
4
+ /** Renders a compact ASCII progress bar of fixed width. */
5
+ function ProgressBar({ done, total, width }) {
6
+ const filled = total > 0 ? Math.round((done / total) * width) : 0;
7
+ const bar = "█".repeat(filled) + "░".repeat(width - filled);
8
+ return _jsxs(Text, { color: "cyan", children: ["[", bar, "]"] });
9
+ }
10
+ /**
11
+ * Detail panel renderer for plan files (.aide files of type "plan").
12
+ *
13
+ * Preview mode: description from frontmatter, progress fraction, remaining count.
14
+ * Drill-in mode: grouped checklist items by step heading, completion summary at top.
15
+ */
16
+ export default function RenderPlanDetail({ file, frontmatter, mode, body, drilledFilePath, }) {
17
+ const steps = parsePlanItems(body);
18
+ const allItems = steps.flatMap((s) => s.items);
19
+ const doneCount = allItems.filter((i) => i.done).length;
20
+ const totalCount = allItems.length;
21
+ const remainingCount = totalCount - doneCount;
22
+ // --- Preview mode ---
23
+ if (mode === "preview") {
24
+ const description = frontmatter?.description ?? file.description ?? "";
25
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [description !== "" && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: description }) })), _jsxs(Box, { flexDirection: "row", gap: 1, alignItems: "center", children: [_jsx(ProgressBar, { done: doneCount, total: totalCount, width: 20 }), _jsxs(Text, { children: [doneCount, "/", totalCount, " complete"] })] }), remainingCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", children: [remainingCount, " item", remainingCount !== 1 ? "s" : "", " remaining"] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[\u2191\u2193] navigate [enter] drill in [tab] deep view" }) })] }));
26
+ }
27
+ // --- Drill-in mode ---
28
+ const title = drilledFilePath ?? file.relativePath;
29
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, children: title }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { bold: true, color: doneCount === totalCount && totalCount > 0 ? "green" : "cyan", children: ["Progress: ", doneCount, "/", totalCount, " step", totalCount !== 1 ? "s" : "", " complete"] }) }), steps.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: steps.map((s, si) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [s.step !== "" && (_jsx(Text, { bold: true, children: s.step })), s.items.map((item, ii) => (_jsxs(Box, { flexDirection: "row", marginLeft: s.step !== "" ? 2 : 0, children: [item.done ? (_jsx(Text, { color: "green", children: "\u2713 " })) : (_jsx(Text, { color: "gray", children: "\u25CB " })), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: item.done ? "gray" : undefined, wrap: "wrap", children: item.text }) })] }, ii)))] }, si))) })), totalCount === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No checklist items found." }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[esc] back [e] open in editor" }) })] }));
30
+ }
@@ -0,0 +1,32 @@
1
+ /** A single checklist item within a plan step. */
2
+ export interface PlanItem {
3
+ text: string;
4
+ done: boolean;
5
+ }
6
+ /** A plan step grouping with its heading label and checklist items. */
7
+ export interface PlanStep {
8
+ step: string;
9
+ done: boolean;
10
+ items: PlanItem[];
11
+ }
12
+ /**
13
+ * Parse a plan file body string into structured step groups with checklist items.
14
+ *
15
+ * Recognises three heading formats found in this project's plan files:
16
+ *
17
+ * 1. `### N. [x] Step title` — numbered with inline checkbox (cli/plan.aide)
18
+ * 2. `### N. Step title` — numbered without inline checkbox
19
+ * 3. `### Phase N — Title` — Phase-prefixed with em-dash (init/plan.aide)
20
+ *
21
+ * Checklist items below a heading are lines matching `- [x]` or `- [ ]`.
22
+ * Prose items (non-checklist bullet lines) and all other non-heading lines
23
+ * are ignored. Items that appear before any heading are grouped under an
24
+ * empty-string step.
25
+ *
26
+ * The `done` field on a `PlanStep` is determined as follows:
27
+ * - If the heading carries an inline `[x]`/`[ ]` marker (format 1), that
28
+ * marker is authoritative — `done` is not overridden by item states.
29
+ * - Otherwise `done` is derived: true when all contained items are done,
30
+ * false when any item is undone or the step has no items.
31
+ */
32
+ export default function parsePlanItems(body: string): PlanStep[];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Parse a plan file body string into structured step groups with checklist items.
3
+ *
4
+ * Recognises three heading formats found in this project's plan files:
5
+ *
6
+ * 1. `### N. [x] Step title` — numbered with inline checkbox (cli/plan.aide)
7
+ * 2. `### N. Step title` — numbered without inline checkbox
8
+ * 3. `### Phase N — Title` — Phase-prefixed with em-dash (init/plan.aide)
9
+ *
10
+ * Checklist items below a heading are lines matching `- [x]` or `- [ ]`.
11
+ * Prose items (non-checklist bullet lines) and all other non-heading lines
12
+ * are ignored. Items that appear before any heading are grouped under an
13
+ * empty-string step.
14
+ *
15
+ * The `done` field on a `PlanStep` is determined as follows:
16
+ * - If the heading carries an inline `[x]`/`[ ]` marker (format 1), that
17
+ * marker is authoritative — `done` is not overridden by item states.
18
+ * - Otherwise `done` is derived: true when all contained items are done,
19
+ * false when any item is undone or the step has no items.
20
+ */
21
+ export default function parsePlanItems(body) {
22
+ const lines = body.split("\n");
23
+ const steps = [];
24
+ let current = null;
25
+ for (const raw of lines) {
26
+ const line = raw.trim();
27
+ // Format 1: ### N. [x] Step title (inline checkbox in heading)
28
+ const inlineCheckHeading = line.match(/^###\s+\d+\.\s+\[(x| )\]\s+(.+)$/i);
29
+ if (inlineCheckHeading) {
30
+ current = {
31
+ step: inlineCheckHeading[2].trim(),
32
+ done: inlineCheckHeading[1].toLowerCase() === "x",
33
+ items: [],
34
+ _headingCheckbox: true,
35
+ };
36
+ steps.push(current);
37
+ continue;
38
+ }
39
+ // Format 2: ### N. Step title (no inline checkbox)
40
+ const numberedHeading = line.match(/^###\s+\d+\.\s+(.+)$/);
41
+ if (numberedHeading) {
42
+ current = { step: numberedHeading[1].trim(), done: false, items: [], _headingCheckbox: false };
43
+ steps.push(current);
44
+ continue;
45
+ }
46
+ // Format 3: ### Phase N — Title (Phase prefix with em-dash, en-dash, or hyphen)
47
+ const phaseHeading = line.match(/^###\s+Phase\s+\d+\s+[—–-]+\s+(.+)$/i);
48
+ if (phaseHeading) {
49
+ current = { step: phaseHeading[1].trim(), done: false, items: [], _headingCheckbox: false };
50
+ steps.push(current);
51
+ continue;
52
+ }
53
+ const checkedMatch = line.match(/^-\s+\[x\]\s+(.+)$/i);
54
+ if (checkedMatch) {
55
+ if (!current) {
56
+ current = { step: "", done: false, items: [], _headingCheckbox: false };
57
+ steps.push(current);
58
+ }
59
+ current.items.push({ text: checkedMatch[1].trim(), done: true });
60
+ continue;
61
+ }
62
+ const uncheckedMatch = line.match(/^-\s+\[ \]\s+(.+)$/);
63
+ if (uncheckedMatch) {
64
+ if (!current) {
65
+ current = { step: "", done: false, items: [], _headingCheckbox: false };
66
+ steps.push(current);
67
+ }
68
+ current.items.push({ text: uncheckedMatch[1].trim(), done: false });
69
+ }
70
+ }
71
+ // Derive done for steps whose heading had no inline checkbox:
72
+ // true only when all items are done and at least one item exists.
73
+ for (const step of steps) {
74
+ if (!step._headingCheckbox && step.items.length > 0) {
75
+ step.done = step.items.every((item) => item.done);
76
+ }
77
+ }
78
+ // Strip the internal field before returning.
79
+ return steps.map(({ _headingCheckbox: _hc, ...rest }) => rest);
80
+ }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ interface RenderTodoDetailProps {
3
+ /** Frontmatter description field, shown as the preview summary. */
4
+ description: string;
5
+ /** Raw body content of the todo file. */
6
+ body: string;
7
+ /** Controls which rendering mode is active. */
8
+ mode: "preview" | "drill-in";
9
+ /** File path, used as the drill-in panel title. */
10
+ filePath: string;
11
+ }
12
+ /**
13
+ * Renders a todo.aide file in preview mode (summary counts) or drill-in mode
14
+ * (full issue list with misalignment annotations highlighted and completion state).
15
+ */
16
+ export default function RenderTodoDetail({ description, body, mode, filePath, }: RenderTodoDetailProps): React.ReactElement;
17
+ export {};
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import parseTodoItems from "./parseTodoItems/index.js";
4
+ /**
5
+ * Renders a todo.aide file in preview mode (summary counts) or drill-in mode
6
+ * (full issue list with misalignment annotations highlighted and completion state).
7
+ */
8
+ export default function RenderTodoDetail({ description, body, mode, filePath, }) {
9
+ const items = parseTodoItems(body);
10
+ const totalCount = items.length;
11
+ const doneCount = items.filter((item) => item.done).length;
12
+ const openCount = totalCount - doneCount;
13
+ const misalignmentCount = items.filter((item) => item.misalignment !== undefined).length;
14
+ if (mode === "preview") {
15
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [description ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "wrap", children: description }) })) : null, _jsxs(Text, { children: ["Issues:", " ", _jsxs(Text, { color: openCount > 0 ? "yellow" : "green", children: [openCount, " open"] }), ", ", _jsxs(Text, { color: "green", children: [doneCount, " resolved"] }), " (", _jsxs(Text, { children: [totalCount, " total"] }), ")"] }), misalignmentCount > 0 && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2691 ", misalignmentCount, " misalignment annotation", misalignmentCount !== 1 ? "s" : ""] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[\u2191\u2193] navigate [enter] drill in [tab] deep view" }) })] }));
16
+ }
17
+ // Drill-in mode — full issue list
18
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, children: filePath }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Issues:", " ", _jsxs(Text, { color: openCount > 0 ? "yellow" : "green", children: [openCount, " open"] }), ", ", _jsxs(Text, { color: "green", children: [doneCount, " resolved"] }), " (", _jsxs(Text, { children: [totalCount, " total"] }), ")"] }) }), items.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No checklist items found." }) })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: items.map((item, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", children: [item.done ? (_jsx(Text, { color: "green", children: "\u2713 " })) : (_jsx(Text, { color: "gray", children: "\u25CB " })), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: item.misalignment !== undefined ? "yellow" : undefined, wrap: "wrap", children: item.text }) })] }), item.misalignment !== undefined && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "red", children: ["\u2691 Misalignment: ", item.misalignment] }) }))] }, i))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "[esc] back [e] open in editor" }) })] }));
19
+ }
@@ -0,0 +1,26 @@
1
+ /** A single parsed checklist item from a todo.aide body. */
2
+ export interface TodoItem {
3
+ /** The main text of the checklist item (first line only, not continuation lines). */
4
+ text: string;
5
+ /** Whether the item is checked (`- [x]`). */
6
+ done: boolean;
7
+ /**
8
+ * The Misalignment annotation extracted from a continuation line, if present.
9
+ * e.g. "implementation-drift" from a line like " Misalignment: implementation-drift"
10
+ */
11
+ misalignment?: string;
12
+ }
13
+ /**
14
+ * Parses the body of a todo.aide file into a structured array of checklist items.
15
+ *
16
+ * Format recognised:
17
+ * - `- [x] item text` — completed item
18
+ * - `- [ ] item text` — open item
19
+ * Continuation lines (indented lines that follow a checklist item) are scanned
20
+ * for a `Misalignment:` annotation. The first such annotation found is attached
21
+ * to the preceding item. All other continuation lines are ignored.
22
+ *
23
+ * Lines that are not checklist items and not continuations of a checklist item
24
+ * (e.g. `##` headings, blank lines, Retro sections) are ignored.
25
+ */
26
+ export default function parseTodoItems(body: string): TodoItem[];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Parses the body of a todo.aide file into a structured array of checklist items.
3
+ *
4
+ * Format recognised:
5
+ * - `- [x] item text` — completed item
6
+ * - `- [ ] item text` — open item
7
+ * Continuation lines (indented lines that follow a checklist item) are scanned
8
+ * for a `Misalignment:` annotation. The first such annotation found is attached
9
+ * to the preceding item. All other continuation lines are ignored.
10
+ *
11
+ * Lines that are not checklist items and not continuations of a checklist item
12
+ * (e.g. `##` headings, blank lines, Retro sections) are ignored.
13
+ */
14
+ export default function parseTodoItems(body) {
15
+ const lines = body.split("\n");
16
+ const items = [];
17
+ let current = null;
18
+ for (const line of lines) {
19
+ const checkboxMatch = line.match(/^- \[(x| )\] (.+)/);
20
+ if (checkboxMatch) {
21
+ // Flush previous item before starting a new one
22
+ if (current)
23
+ items.push(current);
24
+ current = {
25
+ text: checkboxMatch[2].trim(),
26
+ done: checkboxMatch[1] === "x",
27
+ };
28
+ continue;
29
+ }
30
+ // Continuation line — only meaningful after a checklist item
31
+ if (current && line.match(/^\s+/)) {
32
+ const misalignmentMatch = line.match(/\bMisalignment:\s*(.+)/);
33
+ if (misalignmentMatch && !current.misalignment) {
34
+ current.misalignment = misalignmentMatch[1].trim();
35
+ }
36
+ continue;
37
+ }
38
+ // Non-item, non-continuation line — flush any pending item
39
+ if (current) {
40
+ items.push(current);
41
+ current = null;
42
+ }
43
+ }
44
+ // Flush the last item if body ended while an item was open
45
+ if (current)
46
+ items.push(current);
47
+ return items;
48
+ }
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  /** Type-tag display labels keyed by AideFileType. */
4
4
  const TYPE_TAG = {
@@ -22,7 +22,24 @@ function renderRow(flatNode, index, cursorIndex, isDeepView, expandedDirs, width
22
22
  if (node.kind === "dir") {
23
23
  const label = node.path === "." ? ". /" : `${node.path}/`;
24
24
  const expandIndicator = expandedDirs.has(node.path) ? "v " : "> ";
25
- return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, color: isCursor ? "white" : "gray", children: [indent, expandIndicator, label] }) }, `dir-${node.path}-${index}`));
25
+ // Find the first intent child to surface its status and description on the dir row.
26
+ const intentChild = node.children.find((c) => c.kind === "file" && c.file.type === "intent");
27
+ const intentFile = intentChild?.kind === "file" ? intentChild.file : null;
28
+ const dirStatusBadge = intentFile?.status === "aligned"
29
+ ? { text: " [aligned]", color: "green" }
30
+ : intentFile?.status === "misaligned"
31
+ ? { text: " [misaligned]", color: "red" }
32
+ : null;
33
+ const dirDescription = intentFile?.description ?? "";
34
+ // Truncate description to fit within available panel width.
35
+ const dirFixedLen = indent.length + expandIndicator.length + label.length + (dirStatusBadge?.text.length ?? 0);
36
+ const dirRemaining = width - dirFixedLen;
37
+ let dirSummaryText = "";
38
+ if (dirDescription && dirRemaining > 10) {
39
+ const full = ` — ${dirDescription}`;
40
+ dirSummaryText = full.length <= dirRemaining ? full : `${full.slice(0, dirRemaining - 3)}…`;
41
+ }
42
+ return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, color: isCursor ? "white" : "gray", wrap: "truncate", children: [indent, expandIndicator, label, dirStatusBadge ? _jsx(Text, { color: dirStatusBadge.color, children: dirStatusBadge.text }) : null, dirSummaryText ? _jsx(Text, { color: "gray", children: dirSummaryText }) : null] }) }, `dir-${node.path}-${index}`));
26
43
  }
27
44
  // File node — render as a single <Text> to prevent Ink from wrapping mid-element.
28
45
  const { file } = node;
@@ -32,15 +49,26 @@ function renderRow(flatNode, index, cursorIndex, isDeepView, expandedDirs, width
32
49
  const tagColor = TYPE_COLOR[file.type] ?? "white";
33
50
  const prefix = `${indent}${connector}${filename} `;
34
51
  const tagStr = `[${tag}]`;
35
- // Truncate summary to fit within available panel width.
36
- const fixedLen = prefix.length + tagStr.length;
52
+ // Determine status badge text and color when a status flag is present.
53
+ const statusBadge = file.status
54
+ ? file.status === "aligned"
55
+ ? { text: " [aligned]", color: "green" }
56
+ : { text: " [misaligned]", color: "red" }
57
+ : null;
58
+ // Truncate summary/description to fit within available panel width.
59
+ const statusLen = statusBadge ? statusBadge.text.length : 0;
60
+ const fixedLen = prefix.length + tagStr.length + statusLen;
37
61
  const remaining = width - fixedLen;
38
- let summary = "";
39
- if (isDeepView && file.summary && remaining > 10) {
62
+ let summaryText = "";
63
+ if (file.description && remaining > 10) {
64
+ const full = ` — ${file.description}`;
65
+ summaryText = full.length <= remaining ? full : `${full.slice(0, remaining - 3)}…`;
66
+ }
67
+ else if (!file.description && isDeepView && file.summary && remaining > 10) {
40
68
  const full = ` — ${file.summary}`;
41
- summary = full.length <= remaining ? full : `${full.slice(0, remaining - 3)}…`;
69
+ summaryText = full.length <= remaining ? full : `${full.slice(0, remaining - 3)}…`;
42
70
  }
43
- return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, backgroundColor: isCursor ? "blue" : undefined, wrap: "truncate", children: [prefix, _jsx(Text, { color: tagColor, children: tagStr }), summary ? _jsx(Text, { color: "gray", children: summary }) : null] }) }, `file-${file.relativePath}-${index}`));
71
+ return (_jsx(Box, { children: _jsxs(Text, { bold: isCursor, backgroundColor: isCursor ? "blue" : undefined, wrap: "truncate", children: [prefix, _jsx(Text, { color: tagColor, children: tagStr }), statusBadge ? _jsx(Text, { color: statusBadge.color, children: statusBadge.text }) : null, summaryText ? _jsx(Text, { color: "gray", children: summaryText }) : null] }) }, `file-${file.relativePath}-${index}`));
44
72
  }
45
73
  /**
46
74
  * Renders the left-panel tree of .aide files with cursor highlighting and optional summaries.
@@ -76,7 +76,7 @@ export default async function buildAncestorChain(root, targetPath) {
76
76
  if (!spec)
77
77
  continue;
78
78
  const { frontmatter } = parseFrontmatter(spec.content);
79
- const description = frontmatter?.description;
79
+ const description = frontmatter?.description || (frontmatter?.intent ? frontmatter.intent.split(/[.\n]/)[0] : undefined);
80
80
  const status = frontmatter?.status;
81
81
  // Compute a display path relative to the project root
82
82
  const rel = relative(absRoot, spec.specPath).replace(/\\/g, "/");
@@ -39,6 +39,10 @@ export interface AideFile {
39
39
  type: AideFileType;
40
40
  /** First ~80 chars of the first paragraph, for tree summaries. */
41
41
  summary: string;
42
+ /** Frontmatter description field — one-line human-readable summary. Empty string when absent. */
43
+ description?: string;
44
+ /** Alignment status from frontmatter — present only when explicitly set. */
45
+ status?: "aligned" | "misaligned";
42
46
  }
43
47
  /** Result of scanning a directory tree for .aide files. */
44
48
  export interface ScanResult {
@@ -2,6 +2,8 @@ import type { ScanResult } from "../../types/index.js";
2
2
  /**
3
3
  * Recursively walk the filesystem from `root` and collect all .aide files.
4
4
  * Skips node_modules, .git, dist, build, .next, coverage, __pycache__.
5
- * Reads the first ~1000 bytes of each file to extract the first meaningful body line as summary.
5
+ * In deep mode: reads full content to extract summary, description, and status.
6
+ * In shallow mode: reads only the first ~500 bytes per file to extract frontmatter
7
+ * description and status — summary stays empty but descriptions appear unconditionally.
6
8
  */
7
9
  export default function scan(root: string, path?: string, shallow?: boolean): Promise<ScanResult>;
@@ -2,6 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative } from "node:path";
3
3
  import { classifyFile } from "../../util/classify/index.js";
4
4
  import { SKIP_DIRS } from "../../types/index.js";
5
+ import parseFrontmatter from "../../util/parseFrontmatter/index.js";
5
6
  /** Extract the first meaningful body line as the summary, truncated to ~80 chars. */
6
7
  function extractSummary(content) {
7
8
  const lines = content.split("\n");
@@ -32,6 +33,17 @@ function extractSummary(content) {
32
33
  function toPosix(p) {
33
34
  return p.split("\\").join("/");
34
35
  }
36
+ /**
37
+ * Derive a description from frontmatter, falling back to the first sentence of
38
+ * `intent` when `description` is absent — matching the logic in buildAncestorChain.
39
+ */
40
+ function deriveDescription(frontmatter) {
41
+ if (frontmatter?.description)
42
+ return frontmatter.description;
43
+ if (frontmatter?.intent)
44
+ return frontmatter.intent.split(/[.\n]/)[0] ?? "";
45
+ return "";
46
+ }
35
47
  /** Recursively walk a directory and collect all .aide files. */
36
48
  async function walk(dir, root, files, shallow) {
37
49
  let entries;
@@ -52,10 +64,31 @@ async function walk(dir, root, files, shallow) {
52
64
  if (!entry.name.endsWith(".aide"))
53
65
  continue;
54
66
  let summary = "";
67
+ let description = "";
68
+ let status;
55
69
  if (!shallow) {
56
70
  try {
57
71
  const buf = await readFile(fullPath, { encoding: "utf-8" });
58
72
  summary = extractSummary(buf.slice(0, 1000));
73
+ const { frontmatter } = parseFrontmatter(buf);
74
+ description = deriveDescription(frontmatter);
75
+ if (frontmatter?.status)
76
+ status = frontmatter.status;
77
+ }
78
+ catch {
79
+ // skip unreadable files
80
+ }
81
+ }
82
+ else {
83
+ // In shallow mode, read only the first ~500 bytes to capture frontmatter
84
+ // without loading the full body — keeps startup fast for large projects.
85
+ try {
86
+ const buf = await readFile(fullPath, { encoding: "utf-8" });
87
+ const head = buf.slice(0, 500);
88
+ const { frontmatter } = parseFrontmatter(head);
89
+ description = deriveDescription(frontmatter);
90
+ if (frontmatter?.status)
91
+ status = frontmatter.status;
59
92
  }
60
93
  catch {
61
94
  // skip unreadable files
@@ -66,13 +99,17 @@ async function walk(dir, root, files, shallow) {
66
99
  relativePath: toPosix(relative(root, fullPath)),
67
100
  type: classifyFile(entry.name),
68
101
  summary,
102
+ description,
103
+ status,
69
104
  });
70
105
  }
71
106
  }
72
107
  /**
73
108
  * Recursively walk the filesystem from `root` and collect all .aide files.
74
109
  * Skips node_modules, .git, dist, build, .next, coverage, __pycache__.
75
- * Reads the first ~1000 bytes of each file to extract the first meaningful body line as summary.
110
+ * In deep mode: reads full content to extract summary, description, and status.
111
+ * In shallow mode: reads only the first ~500 bytes per file to extract frontmatter
112
+ * description and status — summary stays empty but descriptions appear unconditionally.
76
113
  */
77
114
  export default async function scan(root, path, shallow) {
78
115
  const scanRoot = path ? join(root, path) : root;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aidemd-mcp/server",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that teaches any agent the AIDE methodology through tool descriptions and progressive disclosure tooling",
5
5
  "type": "module",
6
6
  "bin": {