Skip to content

feat: hierarchical outline index for long markdown artifacts#241

Open
MainQuest99 wants to merge 4 commits intorohitg00:mainfrom
MainQuest99:feature/outline-index
Open

feat: hierarchical outline index for long markdown artifacts#241
MainQuest99 wants to merge 4 commits intorohitg00:mainfrom
MainQuest99:feature/outline-index

Conversation

@MainQuest99
Copy link
Copy Markdown

@MainQuest99 MainQuest99 commented May 8, 2026

Summary

  • Adds a PageIndex-inspired outline index so agents can fetch a heading tree + a single section instead of loading entire long artifacts (CLAUDE.md, briefs, audits).
  • 3 new MCP tools (memory_build_outline, memory_get_outline, memory_get_section) + 3 REST endpoints under /agentmemory/outline/*.
  • Optional PostToolUse hook that regenerates the outline whenever a tracked file is written; CLI backfill script for first-time indexing across all CLAUDE.md / MEMORY.md / AGENTS.md files in the user's repos.

Schema

New KV scope mem:outlines keyed by artifact_id (absolute path). Outline stores the heading tree, source mtime + size for staleness detection, and positional node_ids like 1.2.3. Markdown ATX headings only; fenced code blocks are skipped.

Performance

Round-trip on a real 597-line / 37 KB CLAUDE.md averages 0.16 ms for parse + node lookup + line slice (well under the 100 ms target). Pure parser on a 600-line synthetic doc: <30 ms.

Test plan

  • npm test — 735 / 735 passing (16 new outline tests)
  • Parser unit tests: simple doc, fenced code blocks, 4-level nesting, sibling H1s, no-heading fallback, DFS lookup
  • KV roundtrip: build → get → section + stale detection (mtime/size mismatch) + missing-file error path
  • Integration on 3 realistic CLAUDE-style fixtures (project, brief, audit) + 500-line latency probe
  • Manual: run npx tsx scripts/outline-backfill.ts once the engine is up locally

Out of scope

No embeddings, no PDF support, no semantic summary on nodes (summary field reserved). See docs/outline.md for full schema, hook setup, and limitations.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Outline system for indexing and navigating large Markdown documents with outline building, retrieval, and section extraction capabilities
    • Expanded platform: 46 MCP tools (previously 43), 106 REST endpoints (previously 103)
  • Documentation

    • Added comprehensive Outline feature specification and API documentation

Mickael Mika and others added 4 commits May 8, 2026 18:22
Adds a hierarchical outline index for long markdown artefacts (CLAUDE.md,
briefs, audits) inspired by PageIndex. Agents can fetch the heading tree
plus extract a single section without loading the whole file.

- new mem:outlines KV scope (schema.ts)
- Outline / OutlineNode types
- buildOutline: ATX heading parser, ignores fenced code blocks, positional
  node ids ("1.2.3"), line_end = previous line of next heading <= level
- mem::outline-build / -get / -section iii functions with mtime+size
  staleness detection
- 6 unit tests for the parser (flat, code-blocks, 4-level nesting, DFS,
  no-headings, sibling H1s)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wires memory_build_outline, memory_get_outline, memory_get_section into
the MCP server and the REST API:

- new V089_OUTLINE_TOOLS registry block (3 tools)
- mcp::tools::call switch handlers (path/artifact_id/node_id validation)
- POST /agentmemory/outline/build, GET /agentmemory/outline,
  POST /agentmemory/outline/section
- 5 KV-roundtrip tests (build, get, section extract, stale detection,
  missing node) + 5 integration tests on realistic CLAUDE-style fixtures
  including a 500-line latency probe

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- src/hooks/outline-regen.ts: standalone PostToolUse hook. When Write /
  Edit / MultiEdit touches a tracked path (CLAUDE.md, MEMORY.md, AGENTS.md
  by default; whitelist at ~/.config/agentmemory/outline-tracked.txt),
  POSTs /agentmemory/outline/build with a 2s timeout. Best-effort, never
  blocks the agent.
- scripts/outline-backfill.ts: walks ~/CaptainAgent, ~/.claude/projects,
  and ~ (depth-limited, skips node_modules / .git / dist), builds an
  outline for every CLAUDE.md / MEMORY.md / AGENTS.md it finds and prints
  built/failed counts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AGENTS.md: 43 -> 46 MCP tools, 103 -> 106 REST endpoints
- README.md: 4x "43 MCP tools" -> "46", 1x "43 tools, 6 resources" -> "46",
  "109 endpoints on port" -> "112"
- plugin/.claude-plugin/plugin.json: 43 MCP tools -> 46
- src/index.ts: register registerOutlineFunctions + update startup log line
- docs/outline.md: feature guide (why, schema, MCP tool signatures, REST
  endpoints, hook + backfill, limitations)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

Someone is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces an Outline Index system for large Markdown artifacts. It adds heading-tree parsing and persistent storage for Markdown files, three new MCP tools and REST endpoints to build/retrieve outlines and extract sections, auto-regeneration hooks that watch specific agent tool writes, a backfill utility for bulk outline construction, and comprehensive tests covering parsing, storage staleness, and error handling. Tool count increases from 43 to 46; REST endpoints from 103 to 106.

Changes

Outline Index Feature Implementation

Layer / File(s) Summary
Type Contracts
src/types.ts
Exports OutlineNode (hierarchical node with ID, title, level, line bounds, optional summary, children) and Outline (artifact metadata plus nodes array).
Core Parsing and Building
src/functions/outline-build.ts
Implements parseHeadings (ATX regex, skip code fences), buildOutline (convert headings to tree with dotted node IDs, compute line ranges), findNode (DFS lookup), and deriveTitle (fallback to filename).
State Schema
src/state/schema.ts
Adds outlines: "mem:outlines" KV namespace for persisting outline data.
SDK Function Registration
src/functions/outline-build.ts
Registers three internal SDK functions: mem::outline-build (reads file, parses, stores outline), mem::outline-get (retrieves outline by artifact_id), mem::outline-section (extracts text slice, validates staleness via mtime/size).
MCP Tool Definitions
src/mcp/tools-registry.ts
Exports V089_OUTLINE_TOOLS with three tool defs (memory_build_outline, memory_get_outline, memory_get_section) including JSON-schema inputs; integrates into getAllTools().
MCP Tool Handler
src/mcp/server.ts
Adds three switch cases to mcp::tools::call: validate inputs, invoke SDK functions, return 200 with result or 400 on missing fields.
REST Endpoints
src/triggers/api.ts
Registers three new endpoints under /agentmemory/outline: POST /build (path), GET / (artifact_id query), POST /section (artifact_id + node_id). Each enforces Bearer auth and field validation.
Worker Integration
src/index.ts
Imports registerOutlineFunctions, invokes it during SDK setup, updates readiness log from "106 REST + 43 tools" to "106 REST + 46 tools + ...".
Auto-regeneration Hook
src/hooks/outline-regen.ts
CLI script reading stdin JSON; extracts file_path only for Write/Edit/MultiEdit tools; checks whitelist at ~/.config/agentmemory/outline-tracked.txt (or defaults to CLAUDE.md/MEMORY.md/AGENTS.md); POSTs to outline build endpoint with 2-second best-effort timeout.
Backfill Script
scripts/outline-backfill.ts
Recursively walks configurable root directories, finds markdown files matching patterns, deduplicates paths, sequentially POSTs each to outline build API, logs success/failure, exits code 1 on any failures.
Documentation
docs/outline.md
Complete spec: KV schema, node ID numbering, MCP/REST request/response shapes, staleness behavior, whitelist tracking, limitations (ATX-only, Markdown-only, mtime+size staleness detection).
Metadata Updates
AGENTS.md, README.md, plugin/.claude-plugin/plugin.json
Update tool counts from 43 to 46; REST endpoints from 103/109 to 106/112.
Unit Tests
test/outline-build.test.ts
Vitest suite mocking iii-sdk logger; validates parseHeadings/buildOutline on flat/nested/code-fenced documents; validates findNode DFS traversal; tests fallback title derivation.
Integration Tests
test/outline-integration.test.ts
Mocks SDK and KV; tests outline build/get/section roundtrip on temporary fixtures; validates latency thresholds; tests buildOutline performance on large input.
End-to-End Tests
test/outline-tools.test.ts
Mocks SDK/KV; tests full registerOutlineFunctions flow: build from temp file, retrieve outline, extract sections; tests stale detection (file modified post-build), missing node errors, missing file build failure.

Sequence Diagrams

sequenceDiagram
  participant Client
  participant BuildService as outlineBuild
  participant Filesystem as FS
  participant KVStore as KV
  Client->>BuildService: path
  BuildService->>FS: readFile
  BuildService->>BuildService: parseHeadings
  BuildService->>BuildService: buildOutline tree
  BuildService->>KVStore: persist outline
  BuildService-->>Client: { success, nodeCount }
Loading
sequenceDiagram
  participant Client
  participant SectionService as outlineSection
  participant KVStore as KV
  participant Filesystem as FS
  Client->>SectionService: artifact_id, node_id
  SectionService->>KVStore: loadOutline
  SectionService->>SectionService: findNode
  SectionService->>FS: readFile current
  SectionService->>SectionService: validateStaleness
  alt stale file
    SectionService-->>Client: error { stale: true }
  else fresh
    SectionService-->>Client: { text, length }
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • rohitg00/agentmemory#82: Both PRs modify the MCP tool registry and add new MCP tools with updated metadata and documentation including tool counts.

Poem

🐰 A rabbit hops through headings bright,
Building outlines, parsing right—
Trees of nodes in order grew,
Three new tools to serve what's due.
Sections sliced with staleness test,
Outline Index passes every quest! 📚

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: a hierarchical outline index system for long markdown artifacts. It aligns with the detailed PR objectives of adding outline parsing, indexing, and retrieval capabilities.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/outline-index

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
AGENTS.md (1)

108-113: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale test count in the "Current Stats" block.

Line 113 still shows 627 tests and line 101 still reads (613+ tests), but the PR reports 735/735 passing (16 new outline tests added). These two counts were missed while the adjacent tool/endpoint counts on lines 108–109 were updated.

🔧 Proposed fix
-All tests must pass before PR: `npm test` (613+ tests)
+All tests must pass before PR: `npm test` (735+ tests)
-- 627 tests
+- 735 tests
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 108 - 113, Update the stale test counts in the
"Current Stats" block: replace the occurrences of "627 tests" and "(613+ tests)"
with the current total reported by the PR (e.g., "735 tests" and "(735 tests)"
or consistent phrasing used elsewhere), ensuring the strings matching "627
tests" and "(613+ tests)" in the AGENTS.md "Current Stats" section are updated
to reflect the new total so counts and phrasing remain consistent with the PR
status.
README.md (2)

136-136: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale endpoint badge in the "Works with every agent" table.

The "Any agent" cell badge still reads 109-endpoints. The API section (line 755) was updated to 112, but this shield badge was missed.

🔧 Proposed fix
-<img src="https://img.shields.io/badge/109-endpoints-1f6feb?style=flat-square" alt="REST API" width="48" />
+<img src="https://img.shields.io/badge/112-endpoints-1f6feb?style=flat-square" alt="REST API" width="48" />

Also worth updating line 747 while here:

-# Tool visibility: "core" (7 tools) or "all" (43 tools)
+# Tool visibility: "core" (7 tools) or "all" (46 tools)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 136, Update the stale shields.io badge that still reads
"109-endpoints" in the "Any agent" table cell: find the <img ...> tag whose src
contains "shields.io" and the text "109-endpoints" and change it to
"112-endpoints" (and update any alt/title text that mentions 109 accordingly);
also update the other badge referenced near the "line 747" mention the same way
so all endpoint badges consistently show 112.

565-585: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale tool counts in MCP Server section.

Line 565 was updated to "46 tools" but the section heading (line 567) and the collapsible details label (line 585) still reference the old count of 43.

🔧 Proposed fix
-### 43 Tools
+### 46 Tools
-<summary>Extended tools (43 total — set AGENTMEMORY_TOOLS=all)</summary>
+<summary>Extended tools (46 total — set AGENTMEMORY_TOOLS=all)</summary>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 565 - 585, Update the stale tool-counts in the README
so they match the new total (46): change the heading text "### 43 Tools" to "###
46 Tools" and update the collapsible details summary "Extended tools (43 total —
set AGENTMEMORY_TOOLS=all)" to "Extended tools (46 total — set
AGENTMEMORY_TOOLS=all)"; ensure both exact strings are replaced so the visible
counts are consistent.
src/types.ts (1)

449-495: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Record audits for outline operations as required by the pattern.

The outline-build, outline-get, and outline-section functions register via sdk.registerFunction() but skip recordAudit() calls, violating the required pattern. Each function performs state mutations (kv.set, kv.get) and should record these operations with entries like "outline_build", "outline_get", "outline_section" added to AuditEntry.operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types.ts` around lines 449 - 495, The AuditEntry.operation union in the
AuditEntry interface is missing entries for outline operations used elsewhere;
update the AuditEntry type (the operation union in the AuditEntry interface) to
include "outline_build", "outline_get", and "outline_section" so the functions
that call kv.set/kv.get and recordAudit() can log those operations (refer to
AuditEntry and its operation union to locate where to add the three new string
literals).
🧹 Nitpick comments (4)
scripts/outline-backfill.ts (2)

99-108: 💤 Low value

Sequential fetches will be slow on large backfills.

for (const path of targets) awaits each POST in turn. For dozens to hundreds of artifacts plus a 5s per-request timeout, this becomes the dominant runtime. Consider a small concurrency pool (e.g., 4–8 in flight) using Promise.all over chunked batches; this keeps memory bounded but materially shortens wall time. Not a blocker for correctness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/outline-backfill.ts` around lines 99 - 108, The current sequential
loop over targets calling buildOne(path) is slow; change the loop to process
targets with a small concurrency pool (e.g., 4–8) so multiple buildOne POSTs run
in parallel: implement chunking or a simple worker-pool that launches up to N
concurrent buildOne calls and uses Promise.all to await each batch, updating
built/failed counters and logging inside the resolved promises; reference the
existing variables/identifiers targets, buildOne(path), built, failed and the
console.log messages to preserve result handling and error reporting.

70-93: 💤 Low value

Consider gating the $HOME root behind an opt-in flag.

Even with depthCap = 2, walk(home, 0, 2) issues readdir against every non-hidden top-level dir under $HOME (Documents, Desktop, Downloads, source-tree roots, etc.) on every backfill run. For a typical developer machine this is a lot of stat traffic for what is usually a small set of CLAUDE.md/MEMORY.md/AGENTS.md files, and it can pull in personal/non-project markdown that the user may not want indexed via the API.

Suggested options (any one is fine):

  • Make $HOME opt-in via a CLI flag or env var (e.g., BACKFILL_INCLUDE_HOME=1).
  • Replace the broad home walk with a curated list (e.g., ~/dev, ~/code, ~/projects) discovered from config.
  • Print the resolved root list before scanning so the user can ^C if it surprises them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/outline-backfill.ts` around lines 70 - 93, The current main function
unconditionally includes the user's home directory in the roots array (roots,
used by walk and depthCap) which causes broad readdir/stat traffic; make
inclusion of home opt-in by checking a CLI flag or environment variable (e.g.,
BACKFILL_INCLUDE_HOME) before adding join(home, ...) to roots, or alternatively
replace that array entry with a curated list when a different opt-in flag is
absent; update the logic around where roots is built (referencing main, roots,
depthCap, and walk) to print or log the final resolved roots before scanning so
the user can abort if unexpected.
src/functions/outline-build.ts (2)

208-218: ⚡ Quick win

mem::outline-section assumes artifact_id is a filesystem path.

docs/outline.md describes artifact_id as "absolute path or stable id", and mem::outline-build accepts an explicit artifact_id distinct from path. However, this handler then re-reads the source via fs.stat(data.artifact_id) / fs.readFile(data.artifact_id, ...), so any non-path id will always fail here as a (misleading) "outline stale, rebuild needed" error.

Either persist the original path on the stored Outline and re-read from that, or document that artifact_id for section retrieval must be the on-disk path. Worth a follow-up since it widens the working contract beyond what tests currently cover.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/outline-build.ts` around lines 208 - 218, The handler in
outline-build.ts treats data.artifact_id as a filesystem path (using
fs.stat/fs.readFile) which breaks when artifact_id is a stable id rather than a
path; update the code to read from the original on-disk path stored on the
Outline (persist the original path when creating/storing the Outline in
mem::outline-build) and use that stored path (e.g., outline.path or
originalPath) when calling fs.stat/fs.readFile instead of data.artifact_id, or
alternatively enforce/document that artifact_id must be a path and
validate/throw a clear error if it is not; locate usages around the
data.artifact_id reads in the outline-section/outline-build handlers and the
Outline construction to make the change consistently.

139-146: ⚡ Quick win

Parallelize fs.stat + fs.readFile.

The two filesystem reads are independent; running them in parallel halves the IO wait on the hot path (build and section retrieval).

♻️ Proposed change for both call sites
-      let stat;
-      let content: string;
-      try {
-        stat = await fs.stat(data.path);
-        content = await fs.readFile(data.path, "utf-8");
-      } catch (err) {
-        return { success: false, error: `read failed: ${String(err)}` };
-      }
+      let stat: Awaited<ReturnType<typeof fs.stat>>;
+      let content: string;
+      try {
+        [stat, content] = await Promise.all([
+          fs.stat(data.path),
+          fs.readFile(data.path, "utf-8"),
+        ]);
+      } catch (err) {
+        return { success: false, error: `read failed: ${String(err)}` };
+      }

The same change applies to lines 208-218 in mem::outline-section.

As per coding guidelines: "Use parallel operations where possible with Promise.all() for independent key-value writes and reads".

Also applies to: 208-218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/outline-build.ts` around lines 139 - 146, Parallelize the
independent filesystem operations by replacing the sequential await calls for
fs.stat and fs.readFile in the outline build logic: run them via Promise.all
(e.g., const [stat, content] = await Promise.all([fs.stat(data.path),
fs.readFile(data.path, "utf-8")])), preserve the existing try/catch and return
shape on error (return { success: false, error: `read failed: ${String(err)}`
}), and update the analogous code in the mem::outline-section site to the same
Promise.all pattern to avoid duplicate IO waits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/outline.md`:
- Around line 86-90: Update the fenced code block containing the REST endpoints
(the lines starting with "POST /agentmemory/outline/build", "GET 
/agentmemory/outline?artifact_id=<id>", "POST /agentmemory/outline/section") so
the opening fence is tagged with a language hint (e.g., replace the leading
"```" with "```text" or "```http") to satisfy markdownlint MD040 and prevent
code highlighting.

In `@src/functions/outline-build.ts`:
- Around line 132-166: The mem::outline-build handler in
registerOutlineFunctions currently writes to KV.outlines via kv.set but doesn't
call recordAudit; fix by capturing a single timestamp (const ts = new
Date().toISOString()), pass that ts into buildOutline (so outline.generated_at
is set from the same ts rather than internally) and after the successful kv.set
call invoke recordAudit(...) with operation "outline-build" (or add this
operation to AuditEntry.operation union in src/types.ts), the artifact_id,
actor/context info, and the same ts; update registerOutlineFunctions to perform
recordAudit before returning success and ensure buildOutline, kv.set,
recordAudit, and KV.outlines are the referenced spots to modify.

In `@src/hooks/outline-regen.ts`:
- Around line 36-41: Update shouldTrack to only treat whitelist entries as
matching path components by anchoring suffix checks at a path separator: instead
of using filePath.endsWith(p) use a separator-aware check (e.g., filePath === p
|| filePath.endsWith(path.sep + p) or use path.posix/path.sep normalization) and
ensure path is normalized before comparison; reference the shouldTrack function
and import/use Node's path utilities if needed so entries like "notes.md" only
match ".../notes.md" and not "...mynotes.md".

In `@src/mcp/server.ts`:
- Around line 974-1007: The MCP handler for "memory_build_outline" currently
forwards arbitrary paths to mem::outline-build; update the
"memory_build_outline" case in src/mcp/server.ts to validate args.path
endsWith(".md") (and is non-empty) before calling sdk.trigger, rejecting
requests with a 400 error if not; additionally, normalize and constrain the path
to a safe prefix (e.g., resolve and ensure it is inside the server's
working/project directory) or consult a configurable allowlist of allowed base
directories, and mention these same constraints in any error responses so
callers get clear feedback; keep the existing calls to mem::outline-build and
mem::outline-section but enforce this validation in the memory_build_outline
handler to prevent arbitrary file reads.

---

Outside diff comments:
In `@AGENTS.md`:
- Around line 108-113: Update the stale test counts in the "Current Stats"
block: replace the occurrences of "627 tests" and "(613+ tests)" with the
current total reported by the PR (e.g., "735 tests" and "(735 tests)" or
consistent phrasing used elsewhere), ensuring the strings matching "627 tests"
and "(613+ tests)" in the AGENTS.md "Current Stats" section are updated to
reflect the new total so counts and phrasing remain consistent with the PR
status.

In `@README.md`:
- Line 136: Update the stale shields.io badge that still reads "109-endpoints"
in the "Any agent" table cell: find the <img ...> tag whose src contains
"shields.io" and the text "109-endpoints" and change it to "112-endpoints" (and
update any alt/title text that mentions 109 accordingly); also update the other
badge referenced near the "line 747" mention the same way so all endpoint badges
consistently show 112.
- Around line 565-585: Update the stale tool-counts in the README so they match
the new total (46): change the heading text "### 43 Tools" to "### 46 Tools" and
update the collapsible details summary "Extended tools (43 total — set
AGENTMEMORY_TOOLS=all)" to "Extended tools (46 total — set
AGENTMEMORY_TOOLS=all)"; ensure both exact strings are replaced so the visible
counts are consistent.

In `@src/types.ts`:
- Around line 449-495: The AuditEntry.operation union in the AuditEntry
interface is missing entries for outline operations used elsewhere; update the
AuditEntry type (the operation union in the AuditEntry interface) to include
"outline_build", "outline_get", and "outline_section" so the functions that call
kv.set/kv.get and recordAudit() can log those operations (refer to AuditEntry
and its operation union to locate where to add the three new string literals).

---

Nitpick comments:
In `@scripts/outline-backfill.ts`:
- Around line 99-108: The current sequential loop over targets calling
buildOne(path) is slow; change the loop to process targets with a small
concurrency pool (e.g., 4–8) so multiple buildOne POSTs run in parallel:
implement chunking or a simple worker-pool that launches up to N concurrent
buildOne calls and uses Promise.all to await each batch, updating built/failed
counters and logging inside the resolved promises; reference the existing
variables/identifiers targets, buildOne(path), built, failed and the console.log
messages to preserve result handling and error reporting.
- Around line 70-93: The current main function unconditionally includes the
user's home directory in the roots array (roots, used by walk and depthCap)
which causes broad readdir/stat traffic; make inclusion of home opt-in by
checking a CLI flag or environment variable (e.g., BACKFILL_INCLUDE_HOME) before
adding join(home, ...) to roots, or alternatively replace that array entry with
a curated list when a different opt-in flag is absent; update the logic around
where roots is built (referencing main, roots, depthCap, and walk) to print or
log the final resolved roots before scanning so the user can abort if
unexpected.

In `@src/functions/outline-build.ts`:
- Around line 208-218: The handler in outline-build.ts treats data.artifact_id
as a filesystem path (using fs.stat/fs.readFile) which breaks when artifact_id
is a stable id rather than a path; update the code to read from the original
on-disk path stored on the Outline (persist the original path when
creating/storing the Outline in mem::outline-build) and use that stored path
(e.g., outline.path or originalPath) when calling fs.stat/fs.readFile instead of
data.artifact_id, or alternatively enforce/document that artifact_id must be a
path and validate/throw a clear error if it is not; locate usages around the
data.artifact_id reads in the outline-section/outline-build handlers and the
Outline construction to make the change consistently.
- Around line 139-146: Parallelize the independent filesystem operations by
replacing the sequential await calls for fs.stat and fs.readFile in the outline
build logic: run them via Promise.all (e.g., const [stat, content] = await
Promise.all([fs.stat(data.path), fs.readFile(data.path, "utf-8")])), preserve
the existing try/catch and return shape on error (return { success: false,
error: `read failed: ${String(err)}` }), and update the analogous code in the
mem::outline-section site to the same Promise.all pattern to avoid duplicate IO
waits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24327f8c-4747-468c-a9e2-fb9e75017554

📥 Commits

Reviewing files that changed from the base of the PR and between 1effa2c and 6af2579.

📒 Files selected for processing (16)
  • AGENTS.md
  • README.md
  • docs/outline.md
  • plugin/.claude-plugin/plugin.json
  • scripts/outline-backfill.ts
  • src/functions/outline-build.ts
  • src/hooks/outline-regen.ts
  • src/index.ts
  • src/mcp/server.ts
  • src/mcp/tools-registry.ts
  • src/state/schema.ts
  • src/triggers/api.ts
  • src/types.ts
  • test/outline-build.test.ts
  • test/outline-integration.test.ts
  • test/outline-tools.test.ts

Comment thread docs/outline.md
Comment on lines +86 to +90
```
POST /agentmemory/outline/build { path, artifact_id? }
GET /agentmemory/outline?artifact_id=<id>
POST /agentmemory/outline/section { artifact_id, node_id }
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language hint to the REST endpoints code block.

markdownlint flags MD040 for the fenced block on line 86. Tag it with text (or http) so renderers don't try to highlight it as code:

📝 Suggested change
-```
-POST /agentmemory/outline/build      { path, artifact_id? }
-GET  /agentmemory/outline?artifact_id=<id>
-POST /agentmemory/outline/section    { artifact_id, node_id }
-```
+```text
+POST /agentmemory/outline/build      { path, artifact_id? }
+GET  /agentmemory/outline?artifact_id=<id>
+POST /agentmemory/outline/section    { artifact_id, node_id }
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
POST /agentmemory/outline/build { path, artifact_id? }
GET /agentmemory/outline?artifact_id=<id>
POST /agentmemory/outline/section { artifact_id, node_id }
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 86-86: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/outline.md` around lines 86 - 90, Update the fenced code block
containing the REST endpoints (the lines starting with "POST
/agentmemory/outline/build", "GET  /agentmemory/outline?artifact_id=<id>", "POST
/agentmemory/outline/section") so the opening fence is tagged with a language
hint (e.g., replace the leading "```" with "```text" or "```http") to satisfy
markdownlint MD040 and prevent code highlighting.

Comment on lines +132 to +166
export function registerOutlineFunctions(sdk: ISdk, kv: StateKV): void {
sdk.registerFunction(
{ id: "mem::outline-build" },
async (data: { path: string; artifact_id?: string }) => {
if (!data.path || typeof data.path !== "string") {
return { success: false, error: "path is required" };
}
let stat;
let content: string;
try {
stat = await fs.stat(data.path);
content = await fs.readFile(data.path, "utf-8");
} catch (err) {
return { success: false, error: `read failed: ${String(err)}` };
}

const artifact_id = data.artifact_id || data.path;
const outline = buildOutline(
content,
artifact_id,
stat.mtime.toISOString(),
stat.size,
);

await kv.set(KV.outlines, artifact_id, outline);

return {
success: true,
artifact_id,
title: outline.title,
nodeCount: countNodes(outline.nodes),
rootCount: outline.nodes.length,
};
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Missing recordAudit() on the state-changing build operation.

mem::outline-build writes to KV (kv.set(KV.outlines, ...) on line 156) but does not record an audit entry. As per coding guidelines, mem-functions should "validate inputs, do work via kv operations, record audit via recordAudit(), return success object" and "Use recordAudit() for all state-changing operations". Capture the timestamp once and reuse it for both outline.generated_at (currently set inside buildOutline) and the audit entry, per the "Capture timestamps once with new Date().toISOString()" guideline.

A new outline-build (or similar) operation will likely also need to be added to the AuditEntry.operation union in src/types.ts.

As per coding guidelines: "Register functions using the pattern: sdk.registerFunction(...) ... record audit via recordAudit()" and "Use recordAudit() for all state-changing operations".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/outline-build.ts` around lines 132 - 166, The
mem::outline-build handler in registerOutlineFunctions currently writes to
KV.outlines via kv.set but doesn't call recordAudit; fix by capturing a single
timestamp (const ts = new Date().toISOString()), pass that ts into buildOutline
(so outline.generated_at is set from the same ts rather than internally) and
after the successful kv.set call invoke recordAudit(...) with operation
"outline-build" (or add this operation to AuditEntry.operation union in
src/types.ts), the artifact_id, actor/context info, and the same ts; update
registerOutlineFunctions to perform recordAudit before returning success and
ensure buildOutline, kv.set, recordAudit, and KV.outlines are the referenced
spots to modify.

Comment on lines +36 to +41
function shouldTrack(filePath: string, whitelist: string[]): boolean {
if (whitelist.length > 0) {
return whitelist.some((p) => filePath === p || filePath.endsWith(p));
}
return DEFAULT_PATTERNS.some((re) => re.test(filePath));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Anchor whitelist suffix matches at a path separator.

filePath.endsWith(p) matches any string suffix, not a path-component suffix. A whitelist line like notes.md will incorrectly match /repo/mynotes.md, and docs/api.md will match /repo/legacy-docs/api.md. Anchor at / to avoid this:

🛡️ Suggested fix
-function shouldTrack(filePath: string, whitelist: string[]): boolean {
-  if (whitelist.length > 0) {
-    return whitelist.some((p) => filePath === p || filePath.endsWith(p));
-  }
-  return DEFAULT_PATTERNS.some((re) => re.test(filePath));
-}
+function shouldTrack(filePath: string, whitelist: string[]): boolean {
+  if (whitelist.length > 0) {
+    return whitelist.some(
+      (p) => filePath === p || filePath.endsWith(`/${p}`),
+    );
+  }
+  return DEFAULT_PATTERNS.some((re) => re.test(filePath));
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/outline-regen.ts` around lines 36 - 41, Update shouldTrack to only
treat whitelist entries as matching path components by anchoring suffix checks
at a path separator: instead of using filePath.endsWith(p) use a separator-aware
check (e.g., filePath === p || filePath.endsWith(path.sep + p) or use
path.posix/path.sep normalization) and ensure path is normalized before
comparison; reference the shouldTrack function and import/use Node's path
utilities if needed so entries like "notes.md" only match ".../notes.md" and not
"...mynotes.md".

Comment thread src/mcp/server.ts
Comment on lines +974 to +1007
case "memory_build_outline": {
if (typeof args.path !== "string" || !args.path.trim()) {
return { status_code: 400, body: { error: "path is required" } };
}
const outlineBuildResult = await sdk.trigger("mem::outline-build", {
path: args.path,
artifact_id: typeof args.artifact_id === "string" ? args.artifact_id : undefined,
});
return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(outlineBuildResult, null, 2) }] } };
}

case "memory_get_outline": {
if (typeof args.artifact_id !== "string" || !args.artifact_id.trim()) {
return { status_code: 400, body: { error: "artifact_id is required" } };
}
const outlineGetResult = await sdk.trigger("mem::outline-get", {
artifact_id: args.artifact_id,
});
return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(outlineGetResult, null, 2) }] } };
}

case "memory_get_section": {
if (typeof args.artifact_id !== "string" || !args.artifact_id.trim()) {
return { status_code: 400, body: { error: "artifact_id is required" } };
}
if (typeof args.node_id !== "string" || !args.node_id.trim()) {
return { status_code: 400, body: { error: "node_id is required" } };
}
const outlineSectionResult = await sdk.trigger("mem::outline-section", {
artifact_id: args.artifact_id,
node_id: args.node_id,
});
return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(outlineSectionResult, null, 2) }] } };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check backend path validation in outline-build.ts
fd -e ts "outline-build" --exec cat {}

Repository: rohitg00/agentmemory

Length of output: 10968


Add .md extension validation to memory_build_outline MCP handler to restrict file reads.

The backend function mem::outline-build in src/functions/outline-build.ts performs no validation on the path argument—it directly calls fs.stat() and fs.readFile() without extension or directory checks. Combined with the line offset storage in mem::outline-section, this enables arbitrary file reads:

  1. Call memory_build_outline({ path: "/home/user/.ssh/config" }) — any file with ATX-style headings (e.g., # Host) gets indexed.
  2. Retrieve the outline and node offsets.
  3. Call memory_get_section() with those offsets to extract raw file content.

The MCP handler is the security boundary and must validate. At minimum, enforce .md extension:

  case "memory_build_outline": {
    if (typeof args.path !== "string" || !args.path.trim()) {
      return { status_code: 400, body: { error: "path is required" } };
    }
+   if (!args.path.trim().toLowerCase().endsWith(".md")) {
+     return { status_code: 400, body: { error: "path must point to a .md file" } };
+   }
    const outlineBuildResult = await sdk.trigger("mem::outline-build", {

A stronger fix restricts path against a configurable allowlist of safe prefixes (e.g., project working directory only).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/server.ts` around lines 974 - 1007, The MCP handler for
"memory_build_outline" currently forwards arbitrary paths to mem::outline-build;
update the "memory_build_outline" case in src/mcp/server.ts to validate
args.path endsWith(".md") (and is non-empty) before calling sdk.trigger,
rejecting requests with a 400 error if not; additionally, normalize and
constrain the path to a safe prefix (e.g., resolve and ensure it is inside the
server's working/project directory) or consult a configurable allowlist of
allowed base directories, and mention these same constraints in any error
responses so callers get clear feedback; keep the existing calls to
mem::outline-build and mem::outline-section but enforce this validation in the
memory_build_outline handler to prevent arbitrary file reads.

@rohitg00
Copy link
Copy Markdown
Owner

rohitg00 commented May 8, 2026

Thanks @MainQuest99 — outline indexing for long markdown artifacts is a great idea, and PageIndex is exactly the right reference. Three new MCP tools + REST endpoints + a hook + a backfill script is a lot of surface, but it all looks coherent.

Two things before merge:

1. Rebase needed. Branch is currently CONFLICTING against main — I landed several README + plugin.json changes recently (PRs #242, #243, #244 + the iii console fixes). Could you rebase onto latest main and resolve? The conflicts look mostly README region clashes, should be straightforward.

2. Consistency-rule audit per AGENTS.md. When new MCP tools are added, all of these need to stay in sync. Confirm each:

  • src/mcp/tools-registry.ts — tool definitions + getAllTools() includes the 3 new tools
  • src/mcp/server.ts — handler cases in the mcp::tools::call switch
  • src/triggers/api.ts — REST endpoint registration for /agentmemory/outline/*
  • src/index.ts — function registration + endpoint count in the log line
  • test/mcp-standalone.test.ts — tool count assertion bumped
  • README.md — tool counts (search "MCP tools")
  • plugin/.claude-plugin/plugin.json — tool count

I see a few of these touched in the diff; please confirm all are. Once rebased + green, this is in scope for Q2 (Depth) and we can take it.

One scope question while you're here: is the outline-regen PostToolUse hook gated behind an env flag? On a busy session with many file writes, regenerating an outline on every PostToolUse could be expensive. If it isn't already off-by-default, I'd ask for AGENTMEMORY_OUTLINE_AUTO_REGEN=true opt-in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants