Skip to content

feat(cli): npm tarball dist/docs support with monorepo disambiguation#35

Merged
amondnet merged 17 commits into
mainfrom
33-track-npm-tarball-docs-20260408
Apr 8, 2026
Merged

feat(cli): npm tarball dist/docs support with monorepo disambiguation#35
amondnet merged 17 commits into
mainfrom
33-track-npm-tarball-docs-20260408

Conversation

@amondnet

@amondnet amondnet commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #33.

Make ASK exploit dist/docs/ directories that npm libraries (ai, @mastra/core, @mastra/memory, next canary, ...) ship inside their published tarballs. Today the CLI delegates npm specs to the GitHub source even when an npm strategy is registered, and the NpmSource class is @deprecated and lacks any local-node_modules shortcut. The result is unnecessary network IO and missed first-party docs.

What changed

Two-tier strategy:

  • Tier 1 — curated registry entry (fast path): Register an npm strategy with docsPath in the ASK Registry, the CLI executes it deterministically. Seed entries added for vercel/ai and mastra-ai/mastra (with @mastra/core and @mastra/memory aliases).
  • Tier 2 — agent fallback: When no registry entry exists, the generated *-docs SKILL.md guides the agent to walk node_modules/<pkg>/{dist/docs,docs,*.md} at runtime and suggest formal registration via ask docs add npm:<pkg>.

CLI changes:

  • Replaced static SOURCE_PRIORITY with rule-layered selectBestStrategy — a curated npm strategy (source: npm with docsPath) outranks github regardless of declaration order. Without docsPath, the static priority table still wins.
  • NpmSource.fetch is now local-first: it reads node_modules/<pkg>/<docsPath> directly when the installed package.json version satisfies the request, and only falls through to a tarball download on miss. NpmSource is no longer marked @deprecated.
  • NpmLockEntry schema accepts either tarball (network fetch) or installPath (local read); the invariant is enforced in buildLockEntry.
  • Generated SKILL.md now includes a "When the docs cannot be found" fallback section.

Server-side changes (apps/registry/server/api/registry/[...slug].get.ts):

  • For alias-based lookups (ecosystem/name), the server now disambiguates strategies — the curated npm strategy whose package field matches the requested alias is moved to the head of the response.
  • New resolvedName field returned in the response: for monorepo entries (multiple npm strategies under one repo), the server returns a slugified package name (@mastra/memorymastra-memory); for non-monorepo entries it equals the entry's display name. This keeps the disambiguation rule centralized in one place that all clients (CLI today, future SDKs tomorrow) benefit from.

User-facing skill (skills/add-docs/):

  • Step 3 now documents the npm-vs-github decision tree based on manifest/lockfile evidence.
  • Recovery details (error classification, resource ladder) extracted to references/recovery.md so SKILL.md stays focused on the happy path.

Telemetry design:

  • design/telemetry.md in the track directory captures the opt-in model, collected fields, aggregation flow, and prior-art analysis of vercel-labs/skills/src/telemetry.ts. Implementation is a follow-up track.

Key files

  • packages/cli/src/registry.tsselectBestStrategy rule layering, RegistryApiResponse type, resolvedName consumption
  • packages/cli/src/sources/npm.tstryLocalRead + tarball fallback
  • packages/cli/src/index.tsbuildLockEntry accepts tarball or installPath
  • packages/schema/src/lock.tsNpmLockEntry shape relaxed
  • packages/cli/src/skill.ts — fallback section in generated SKILL.md
  • apps/registry/server/api/registry/[...slug].get.ts — server-side disambiguation + slugify
  • apps/registry/content/registry/{vercel/ai,mastra-ai/mastra}.md — seed registry entries
  • skills/add-docs/SKILL.md + references/recovery.md — user-facing skill update
  • .please/docs/tracks/active/npm-tarball-docs-20260408/design/telemetry.md — telemetry design

Notable findings during implementation

  • The schema already supported source: 'npm' + docsPath — the real blocker was SOURCE_PRIORITY = { github: 0, npm: 1 } killing the existing vercel/next.js entry that was already shipping the npm strategy. Fixing the priority lit up dead code.
  • getSource() already dispatched to NpmSource, so no separate dispatcher was needed once the priority bug was fixed.
  • Two cycles of /review:code-review-loop caught and fixed: (1) the mastra entry conflated @mastra/core and @mastra/memory, (2) the resulting scoped names broke filesystem and skill name slugs. Both fixes were then refactored to live on the registry server instead of in the CLI client.

Test plan

  • All 252 CLI tests + 14 schema tests pass
  • CLI builds clean (tsc)
  • CLI lint clean (eslint)
  • Registry app builds clean (nuxt build with experimental.sqliteConnector: native)
  • Manual: in a scratch project with bun add ai, run node packages/cli/dist/index.js docs add npm:ai and verify the log shows Using local node_modules for ai@<ver> and .ask/docs/... mirrors node_modules/ai/dist/docs
  • Manual: same flow for npm:@mastra/core and npm:@mastra/memory — confirm they land in distinct slugs (e.g. mastra-core@<ver> and mastra-memory@<ver>)
  • Manual: regression check — ask docs sync on a project with lodash, axios, jquery produces the same file count as before this branch
  • CI green on this branch

Follow-up tracks (out of scope here)


Summary by cubic

Adds npm tarball dist/docs support with a local node_modules fast path and monorepo-aware disambiguation, preferring curated npm docs over GitHub. Also hardens NpmSource with a stronger docsPath traversal guard. Closes #33.

  • New Features

    • Strategy selection now prefers curated npm (source: 'npm' with docsPath) over GitHub; otherwise the existing priority table applies.
    • NpmSource is local-first: reads node_modules/<pkg>/<docsPath> when the installed version satisfies the request, falling back to tarball; lock entries may record installPath (local) or tarball (network).
    • Registry API reorders strategies for monorepos to match the requested npm alias and returns resolvedName (slug, e.g. mastra-memory) for consistent client naming; seeded entries for vercel/ai and mastra-ai/mastra enable the npm fast path.
    • Generated SKILL.md adds a fallback section to scan node_modules/<pkg>/{dist/docs,docs,*.md} and suggests ask docs add npm:<pkg>; skills/add-docs documents the npm-vs-GitHub decision tree.
    • Added a telemetry design doc (opt-in model and promotion flow); no runtime code.
  • Bug Fixes

    • Strengthened path traversal guard in NpmSource.tryLocalRead: ensure docsPath resolves inside node_modules/<pkg> and reject absolute/escaping paths, including symlink-based escapes via a realpath containment check; tests cover both cases.

Written for commit 90087cc. Summary will update on new commits.

amondnet added 14 commits April 8, 2026 19:22
…stra-ai/mastra

These libraries ship author-curated agent docs in dist/docs of their npm
tarballs. Registering them with an explicit npm strategy lets ASK pull
docs from node_modules (or the tarball) instead of GitHub, matching the
installed version exactly.

Refs #33
An npm strategy carrying an explicit docsPath is treated as
author-curated (e.g. vercel/ai's dist/docs) and outranks every other
strategy in the same entry. Without docsPath, the static SOURCE_PRIORITY
table still wins, so non-curated npm entries do not regress github-first
behavior.

This unblocks the existing vercel/next.js entry that was already shipping
an npm strategy with dist/docs but was being shadowed by the github
strategy in selectBestStrategy.

Refs #33
When the requested package is already installed and its package.json
version satisfies the request, NpmSource.fetch now reads docs directly
from node_modules/<pkg>/<docsPath> and skips the tarball download
entirely. This makes 'ask docs add npm:ai' work fully offline whenever
the package is in the project's dependency tree.

The npm lock entry schema now accepts either tarball (network fetch)
or installPath (local read). Callers must supply at least one — the
invariant is enforced in buildLockEntry rather than via Zod refine
because discriminatedUnion does not accept ZodEffects branches.

NpmSource is no longer marked @deprecated. The deprecation rationale
(prefer resolver + github source) does not apply to author-curated
dist/docs cases.

Refs #33
When the .ask/docs/<pkg> directory is missing or stale, the agent now
has explicit guidance to walk node_modules/<pkg>/{dist/docs,docs,*.md}
as a discovery path, and to suggest 'ask docs add npm:<pkg>' for formal
registration once usable docs are found.

This is the Tier 2 of the npm-tarball-docs strategy: Tier 1 is the
curated registry entry, Tier 2 is the LLM walking node_modules at
runtime when no entry exists.

Refs #33
- design/telemetry.md: full opt-in telemetry design for the future
  Registry auto-promotion loop, with prior-art analysis of
  vercel-labs/skills src/telemetry.ts. No code in this track.
- CLAUDE.md gotchas: document the curated-npm > github selection rule
  and the local-first NpmSource behavior with the new installPath
  lock entry shape.
- plan.md: mark all 15 tasks complete.

Refs #33
Make Step 3 of add-docs explicit: when the package appears in the
project's manifest/lockfile, prefer the ecosystem prefix (npm:<name>)
so the CLI's NpmSource can short-circuit to the local node_modules
read with no network call. Only fall back to GitHub shorthand when
there is no manifest evidence.

Refs #33
The CLI is deterministic — it does not fall back between sources. The
LLM driving add-docs must read the CLI error, classify it, and retry
with a different spec when appropriate. The most common case: an npm
strategy was selected but the tarball did not actually ship the
curated docs directory; the skill must then resolve the package's
GitHub repo and retry as <owner>/<repo>.

Resource ladder for finding owner/repo uses only always-available
resources (training knowledge, node_modules/<pkg>/package.json,
WebFetch of registry.npmjs.org, WebSearch, AskUserQuestion). MCP
servers like deepwiki or context_grep are intentionally excluded — we
cannot assume they are installed in the user's environment.

Includes a recovery decision tree per error class, a cost-ordered
resource ladder, and explicit limits (max 1 retry, preserve user
version intent, always report which path was taken).

Refs #33
The Recovery section's error classification table, resource ladder, and
worked examples are only needed when the CLI actually fails — which
should be rare given proper upfront planning in Step 3. Keep SKILL.md
focused on the happy path and link out for the exception path.

Refs #33
.claude/agent-memory/ is version controlled per CLAUDE.md so agent
discoveries persist across sessions. These files were left untracked
from a prior session.
The mastra-ai/mastra registry entry holds two npm strategies, one for
@mastra/core and one for @mastra/memory. selectBestStrategy was picking
the first curated npm regardless of which package the user asked for, so
'ask docs add npm:@mastra/memory' returned @mastra/core docs.

Fix:
- selectBestStrategy now accepts an optional context with requestedPackage.
  When set and a curated npm strategy has a matching package field, that
  strategy wins (Rule 1). The previous 'first curated npm' behavior is
  retained as Rule 2 fallback.
- resolveFromRegistry threads the requested name into selectBestStrategy.
- For monorepo entries (>1 npm strategy), the resolved name is the
  requested package name rather than the entry's display name, so docs
  for @mastra/core and @mastra/memory are saved under distinct slugs
  instead of overwriting each other.

Caught by /review:code-review-loop iteration 1.

Refs #33
When the monorepo disambiguation path returns the requested package name
(e.g. '@mastra/memory'), that name flows into both the storage path
('.ask/docs/<name>@<ver>/') and the Claude Code skill name. Both
surfaces reject scoped-name characters: '@' and '/' would create stray
nested directories and the Claude Code skill name regex is [a-z0-9-]+
only.

slugifyPackageName converts '@scope/pkg' → 'scope-pkg' for both
surfaces. Unscoped names pass through unchanged so existing entries
keep their slugs.

Caught by /review:code-review-loop iteration 2.

Refs #33
The registry server is the right place to handle scoped-package
disambiguation for monorepo entries. It already has full context
(which alias matched, the entry, all strategies) and serving every
client (CLI today, future SDKs) from one source of truth is cleaner
than asking each client to reimplement the rule.

Server changes:
- New disambiguateStrategies(all, requestedPackage): for alias-based
  lookups, reorder strategies so the curated npm strategy whose
  package field matches the requested alias comes first.
- New slugifyPackageName: converts @scope/pkg -> scope-pkg.
- New resolvedName response field: server-computed slug for monorepo
  entries, falls back to entry display name otherwise.

Client changes:
- selectBestStrategy reverts to single-arg signature; server has
  already put the right strategy first.
- slugifyPackageName removed from client (lives on server now).
- resolveFromRegistry trusts entry.resolvedName and falls back to
  entry.name for backward compat with older server responses.
- New RegistryApiResponse type augments RegistryEntry with resolvedName.

Tests:
- Drop ctx-based and slugify tests (moved server-side).
- Add test documenting that selectBestStrategy honors server's
  declared order for monorepo entries.

Refs #33
@amondnet amondnet linked an issue Apr 8, 2026 that may be closed by this pull request
17 tasks
@amondnet amondnet mentioned this pull request Apr 8, 2026
17 tasks
@codecov

codecov Bot commented Apr 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.37209% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/cli/src/sources/npm.ts 87.65% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

- metadata.json: status review, pr #35
- tracks.jsonl: section completed
- plan.md: Outcomes & Retrospective appended
- product-specs/cli/sources.md created via sync-product-specs

Refs #33
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 8, 2026

Copy link
Copy Markdown

Deploying ask-registry with  Cloudflare Pages  Cloudflare Pages

Latest commit: 90087cc
Status: ✅  Deploy successful!
Preview URL: https://b81c156a.ask-registry.pages.dev
Branch Preview URL: https://33-track-npm-tarball-docs-20.ask-registry.pages.dev

View logs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 22 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/sources/npm.ts">

<violation number="1" location="packages/cli/src/sources/npm.ts:93">
P1: Validate `docsPath` stays inside the installed package directory before reading from disk to prevent path traversal in local-first docs loading.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Agent as Agent / User
    participant CLI as CLI (Registry & Dispatcher)
    participant RegSvr as Registry API (Nuxt)
    participant NpmSrc as NpmSource (Local-First)
    participant LocalFS as Local node_modules
    participant NpmReg as npm Registry (Tarball)

    Note over Agent,NpmReg: Documentation Addition Flow (npm Strategy)

    Agent->>CLI: ask docs add npm:<package>
    CLI->>RegSvr: NEW: GET /api/registry/ecosystem/name
    
    Note over RegSvr: Disambiguate strategies<br/>(Move matching npm to head)
    RegSvr-->>CLI: Entry + NEW: resolvedName + prioritized strategies

    CLI->>CLI: CHANGED: selectBestStrategy()<br/>(Prioritize curated npm + docsPath)

    CLI->>NpmSrc: fetch(strategy)
    
    NpmSrc->>LocalFS: NEW: tryLocalRead(node_modules/pkg/docsPath)
    
    alt NEW: Local docs found (Satisfies version)
        LocalFS-->>NpmSrc: return markdown files
        Note right of NpmSrc: Meta: source = 'node_modules'
    else Local miss or version mismatch
        NpmSrc->>NpmReg: download tarball
        NpmReg-->>NpmSrc: tarball stream
        NpmSrc->>NpmSrc: extract docsPath
    end

    NpmSrc-->>CLI: FetchResult (files, resolvedVersion)
    
    CLI->>CLI: CHANGED: buildLockEntry()
    Note right of CLI: NEW: Lock accepts 'installPath' (local)<br/>OR 'tarball' (network)

    CLI->>LocalFS: Write .ask/docs/<resolvedName>@<ver>/
    CLI-->>Agent: Success

    Note over Agent,LocalFS: Tier 2 Fallback (No Registry Entry)
    
    opt Registry 404
        CLI->>CLI: NEW: generate SKILL.md with fallback instructions
        CLI-->>Agent: Generated skill guides Agent to walk node_modules
        Agent->>LocalFS: NEW: Agent manually scans node_modules/<pkg>/{dist/docs,docs,*.md}
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/cli/src/sources/npm.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".please/docs/product-specs/cli/sources.md">

<violation number="1" location=".please/docs/product-specs/cli/sources.md:24">
P2: Use `docsPath` consistently in the normative requirement text; `docspath` conflicts with the stated field name and can produce incorrect implementations.</violation>

<violation number="2" location=".please/docs/product-specs/cli/sources.md:106">
P2: The requirement body conflicts with its MUST-level heading (`SHOULD` vs `MUST`) and changes contract naming; align the normative sentence to avoid ambiguous implementation expectations.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .please/docs/product-specs/cli/sources.md Outdated
Comment thread .please/docs/product-specs/cli/sources.md Outdated
@amondnet amondnet self-assigned this Apr 8, 2026
…Path

cubic-dev-ai review on PR #35 flagged that path.join(pkgDir, docsPath)
could escape the installed package directory if a registry strategy
declared a malicious docsPath like '../../../etc/passwd'. Although
docsPath comes from a trusted registry, defense-in-depth is the right
move — the local-first read now path.resolve()s the join and rejects
anything that lands outside pkgDir or is absolute.

Two tests added covering both rejection cases.

Also remove the malformed product-specs/cli/sources.md generated by
sync-product-specs during finalize. The script (in the please plugin
cache, not this repo) corrupts requirement bodies (lowercases code
identifiers, downgrades MUST → SHOULD, double-period typos). Will be
regenerated correctly once the upstream generator is fixed.

Refs #33

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/sources/npm.ts">

<violation number="1" location="packages/cli/src/sources/npm.ts:98">
P1: The new docsPath boundary check is bypassable via symlinks because it compares unresolved paths. Validate containment using real paths (`fs.realpathSync`) after existence checks.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/cli/src/sources/npm.ts
cubic-dev-ai noted that the previous string-level check is bypassable
via symlinks: a symlink at node_modules/<pkg>/dist/docs pointing to
/etc would lexically pass the path.relative check but still escape
the package directory at read time.

Two-stage check now:
1. String-level pre-check (cheap, blocks obvious traversal before
   touching the filesystem).
2. fs.realpathSync on both pkgDir and docsDir after existence check,
   re-running the containment test against the resolved real paths.

Wrapped in try/catch so a broken symlink (realpath throws) is treated
as a miss rather than crashing the read.

New test plants a symlink at node_modules/ai/dist/docs pointing to a
temp dir outside the project and asserts tryLocalRead returns null.

Refs #33

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: This PR introduces significant logic changes to core CLI source resolution, Registry API response shapes, and the lockfile schema, which requires a human review for potential side effects.

@amondnet
amondnet merged commit 4fd822b into main Apr 8, 2026
5 checks passed
@amondnet
amondnet deleted the 33-track-npm-tarball-docs-20260408 branch April 8, 2026 11:50
This was referenced Apr 8, 2026
@github-actions github-actions Bot mentioned this pull request Apr 12, 2026
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.

track: npm-tarball-docs-20260408

1 participant