Package Exports
- nx-content
Readme
nx-content
Content Manager — resolve instruction, prompt, and block content from a local folder or a Git repo. Built for Node.js 18+, TypeScript strict mode, ESM + CJS.
Use it to:
- Resolve content keys (e.g.
skills/my-skill.instructions) to file content - Support local and Git backends with configurable precedence (
dev= local wins,prod= git wins) - Inline block includes with
<< path >>in files - Cache resolved content in memory (TTL configurable)
- Drop in where a content registry is expected (e.g. ai-gateway)
Install
npm install nx-contentRequirements: Node.js >= 18.
Quick start
import { ContentResolver, init, getDefaultResolver } from 'nx-content';
// 1. Initialize (creates content root + readme)
const manager = await init({ localRoot: './.content' });
// 2. Resolver (recommended API)
const resolver = new ContentResolver({ localRoot: './.content' });
const { text } = await resolver.resolveInstructions('skills/my-skill.instructions');
// 3. Singleton from env — reads all options from .env automatically
const defaultResolver = getDefaultResolver();
const prompt = (await defaultResolver.resolvePrompt('prompts/default')).text;Env-ready (works out of the box)
- Defaults: Works with no config: local root
<cwd>/.content, default mode isdev(local wins, Git is fallback), cache 1 year, no git unless you setGITHUB_REPO_URL. - .env: On first import, the package loads
.envfrom the current working directory (viadotenv). All options can be set there; see.env.example. No nx-config2 (or any other config layer) is required — use it only if you already rely on it elsewhere.
Content keys
- A key is a path relative to the content root, without file extension.
- Resolution tries
.mdthen.jsonwhen the key has no extension (or when the extension is not.md/.json).
Examples:
| Key | Resolved file |
|---|---|
skills/my-skill.instructions |
skills/my-skill.instructions.md (or .json) |
prompts/summarize |
prompts/summarize.md |
blocks/greeting |
blocks/greeting.md |
readme.md |
readme.md (exact) |
Key detection: A string is treated as a key only if, after trim, it is non-empty and contains no whitespace. Otherwise it is treated as literal text and returned as-is.
Block includes
Inside any content file you can include another file with double angle brackets:
<< blocks/greeting >>The manager replaces << path >> with that file’s full content. Paths use / or \; they are relative to the content root. Nested includes are supported (max depth 10; circular includes throw).
API
ContentResolver (recommended)
| Method | Description |
|---|---|
resolveInstructions(key) |
Resolve instruction key → ResolvedContent. If not a key, returns { text: key }. |
resolvePrompt(key) |
Same for prompt key. |
hasInstructionKey(key) |
true if the key exists (no full read). |
resolveInstructionsBlock(blockName, agentId, taskTypeId?, configOverride?) |
Resolve a block with fallback: task → agent → generic. If configOverride is non-empty, returns it. |
normalizeSkillId(skillId) |
Normalize to canonical key (e.g. skills/<id>.instructions). |
isKey(value) |
Whether the string is a content key (no whitespace, non-empty). |
getContentRoot() |
Absolute content root path. |
getContentRegistry() / getContentManager() |
Underlying ContentManager when enabled. |
isEnabled() |
Whether the manager is configured. |
ContentManager
| Method | Description |
|---|---|
resolve(key) |
Resolve key to ResolvedContent (cache + block includes). |
exists(key) |
Whether the key exists. |
normalizeSkillId(skillId) |
Canonical key for skill. |
resolveInstructionsBlock(blockName, agentId, taskTypeId?, configOverride?) |
Block resolution with fallback. |
ping() |
Backend reachable (e.g. root exists). |
listAll() |
All relative paths under the root. |
invalidateKey(key) / invalidateAll() |
Clear cache. |
getContentRoot() |
Absolute local root. |
isEnabled() |
Whether a backend is configured. |
Functions
| Function | Description |
|---|---|
init(config?) |
Create content root dir, write readme, return ContentManager. Idempotent. |
isKey(value) |
Key detection (no whitespace, non-empty). |
isKeyLike(value) |
Heuristic: content looks like a key (safety net). |
runDiagnostics(manager) |
Returns DiagnosticsResult: enabled, localRoot, backendReachable, availableKeys, error. |
getDefaultResolver() |
Singleton ContentResolver from env. |
Types
- ContentManagerConfig —
localRoot,gitRepoUrl,gitToken,gitBranch,gitClonePath,mode,cacheTtlMs - ResolvedContent —
{ text: string; metadata?: ContentMetadata } - ContentMetadata —
key,path,backend,version,rawContent - ContentBackend — interface:
read,exists,listAll,ping,location - DiagnosticsResult —
enabled,localRoot,backendReachable,availableKeys,error - GitBackendConfig —
repoUrl,token?,branch?,clonePath?
Errors (all extend ContentManagerError, have .code)
| Class | Code | When |
|---|---|---|
ContentNotFoundError |
CONTENT_NOT_FOUND |
Key not found in any backend. |
ContentManagerNotAvailableError |
CONTENT_MANAGER_NOT_AVAILABLE |
No backend configured. |
ContentBackendError |
CONTENT_BACKEND_ERROR |
I/O or git failure. |
ContentInvalidError |
CONTENT_INVALID |
Stored content is key-like or empty (never return key as content). |
Configuration
Options (ContentManagerConfig)
| Option | Env fallback | Default | Description |
|---|---|---|---|
localRoot |
CONTENT_REGISTRY_LOCAL_ROOT |
<cwd>/.content |
Content root (resolved to absolute). |
gitRepoUrl |
GITHUB_REPO_URL (only when no config or empty config) |
— | Git repo URL (SSH or HTTPS). When you pass a config object, omitting gitRepoUrl means no Git (env is not used). Set to null or '' to explicitly disable Git. |
gitToken |
GITHUB_TOKEN |
— | GitHub PAT for HTTPS. |
gitBranch |
CONTENT_REGISTRY_GIT_BRANCH |
main |
Branch to use. |
gitClonePath |
CONTENT_REGISTRY_GIT_LOCAL_CLONE_PATH |
os.tmpdir()/nx-content-git |
Local clone path. |
mode |
CONTENT_REGISTRY_MODE |
dev |
Default is dev (local-first: local wins, Git is fallback). prod = git wins. |
cacheTtlMs |
CONTENT_REGISTRY_CACHE_TTL_MS |
1 year | Cache TTL in ms. |
Environment variables
Same names as above; used when the corresponding option is not passed. All paths from config/env are resolved to absolute at init.
Recommended folder layout
.content/
skills/
my-skill.instructions.md
prompts/
default.md
blocks/
greeting.md
disclaimer.mdTesting
# Unit + integration (local)
npm test
# Integration only
npm run test:integration
# Watch
npm run test:watchGit backend integration tests run only when GITHUB_TOKEN (and optionally GITHUB_REPO_URL) are set; otherwise they are skipped.
Build
npm run buildProduces:
dist/esm/— ESMdist/cjs/— CommonJS ("type": "commonjs")dist/types/— Declaration files
License
MIT