nx-content
v1.2.0
Published
Content Manager — fetch instruction, prompt, and block content from local folder or git repo
Maintainers
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:
- Read/write by key —
get(key)returns string content;set(key, content)writes to local backend - Resolve content keys (e.g.
skills/my-skill.instructions) to file content - List keys with optional prefix —
listKeys(prefix?)for catalog and audits - Deterministic namespaced keys —
skillTaskPromptKey(skillKey),skillInstructionsKey(skillKey) - Variant resolution — optional
variant(e.g. prod, beta); triesvariants/<variant>/<key>then<key> - Support local and Git backends with configurable precedence (
dev= local wins,prod= git wins) - Inline block includes with
<< path >>in files - Export to .metadata —
exportToMetadata(options)to sync content to a local metadata tree (files or manifest) - Cache resolved content in memory (TTL configurable)
- Drop in where a content registry is expected (e.g. ai-gateway)
Install
npm install nx-contentYou can also install under the alias content-x (same package, same API):
npm install content-x@npm: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.
Deterministic namespaced keys: For catalog enrichment, validation, and audits, use the helpers so keys are consistent:
skillTaskPromptKey(skillKey)→skills/tasks/<skill-key>/promptskillInstructionsKey(skillKey)→skills/<skill-key>/instructionsnormalizeKeySegment(segment)stripsskills/prefix and.instructionssuffix and sanitizes for path use.
Example: skillTaskPromptKey('my-skill') → skills/tasks/my-skill/prompt; store the file at .content/skills/tasks/my-skill/prompt.md.
Export to metadata
Sync resolved content to a local path for integration with metadata pipelines:
exportToMetadata(options?)— onContentManagerorContentResolver.- Options:
basePath(default'.metadata'),keys(default: all fromlistKeys()),format:'files'(one file per key underbasePath/content/) or'manifest'(singlecontent-manifest.json). - Returns
{ written: string[]; errors: Array<{ key, error }> }. Missing or invalid keys are inerrors; successful keys inwritten.
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 |
|--------|-------------|
| get(key) | Read content by key; returns raw string. Non-keys returned as-is. |
| set(key, content) | Write content by key (local backend only). |
| 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). |
| listKeys(prefix?) | List keys, optionally filtered by prefix (e.g. skills/). |
| 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. |
| exportToMetadata(options?) | Sync/export content to local .metadata path (files or manifest). |
| isEnabled() | Whether the manager is configured. |
ContentManager
| Method | Description |
|--------|-------------|
| get(key) | Read content by key; returns raw string. |
| set(key, content) | Write content by key (local backend only). |
| resolve(key) | Resolve key to ResolvedContent (cache + block includes). |
| exists(key) | Whether the key exists. |
| listKeys(prefix?) | List keys, optionally filtered by prefix. |
| 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. |
| exportToMetadata(options?) | Sync/export content to local .metadata path (files or manifest). |
| 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). |
| normalizeKeySegment(segment) | Normalize segment for namespaced keys (strip skills/ prefix, .instructions suffix). |
| skillTaskPromptKey(skillKey) | Deterministic key skills/tasks/<skill-key>/prompt. |
| skillInstructionsKey(skillKey) | Deterministic key skills/<skill-key>/instructions. |
| runDiagnostics(manager) | Returns DiagnosticsResult: enabled, localRoot, backendReachable, availableKeys, error. |
| getDefaultResolver() | Singleton ContentResolver from env. |
Types
- ContentManagerConfig —
localRoot,gitRepoUrl,gitToken,gitBranch,gitClonePath,mode,variant,cacheTtlMs - ResolvedContent —
{ text: string; metadata?: ContentMetadata } - ContentMetadata —
key,path,backend,version,variant,rawContent - ExportToMetadataOptions —
basePath?(default'.metadata'),keys?(default: all fromlistKeys()),format?: 'files' | 'manifest' - ExportToMetadataResult —
{ written: string[]; errors: Array<{ key: string; error: Error }> } - ContentBackend — interface:
read,exists,listAll,listKeys?,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. |
| variant | CONTENT_REGISTRY_VARIANT | — | Optional. When set, resolution tries variants/<variant>/<key> then <key>. |
| 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.md
variants/ # optional: when variant is set (e.g. prod, beta)
prod/
prompt.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
