JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 50
  • Score
    100M100P100Q65500F
  • License MIT

Content Manager — fetch instruction, prompt, and block content from local folder or git repo

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:

  • Read/write by keyget(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 keysskillTaskPromptKey(skillKey), skillInstructionsKey(skillKey)
  • Variant resolution — optional variant (e.g. prod, beta); tries variants/<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 .metadataexportToMetadata(options) to sync content to a local metadata tree (files or manifest)
  • Push to GitpushToRemote(options?) to commit and push local content to the remote (content root must be a git repo)
  • Cache resolved content in memory (TTL configurable)
  • Drop in where a content registry is expected (e.g. ai-gateway)

Install

npm install nx-content

You can also install under the alias content-x (same package, same API):

npm install content-x@npm:nx-content

Requirements: 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 is dev (local wins, Git is fallback), cache 1 year, no git unless you set GITHUB_REPO_URL.
  • .env: On first import, the package loads .env from the current working directory (via dotenv). 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 an exact file match for keys that do not end with .md/.json, then falls back to ${key}.md, then ${key}.json.

Examples:

Key Resolved file
skills/my-skill.instructions skills/my-skill.instructions (exact) or skills/my-skill.instructions.md (or .json)
prompts/summarize prompts/summarize.md
blocks/greeting blocks/greeting.md
readme.md readme.md (exact)

Key–path contract: Key K corresponds to path P under the content root: P is the key with normalized separators (forward slashes). Use keyToPath(key) (sync, no I/O) for the canonical path (K if it ends with .md/.json, otherwise K + '.md'), or getPathForKey(key) (async) for the actual path when the file exists locally (or the canonical path otherwise). This lets you run git commands (e.g. git log -- <path>) or implement version history yourself.

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>/prompt
  • skillInstructionsKey(skillKey)skills/<skill-key>/instructions
  • normalizeKeySegment(segment) strips skills/ prefix and .instructions suffix 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?) — on ContentManager or ContentResolver.
  • Options: basePath (default '.metadata'), keys (default: all from listKeys()), format: 'files' (one file per key under basePath/content/) or 'manifest' (single content-manifest.json).
  • Returns { written: string[]; errors: Array<{ key, error }> }. Missing or invalid keys are in errors; successful keys in written.

Push to Git

Commit and push content written with set() to a Git remote:

  • pushToRemote(options?) — on ContentManager or ContentResolver. Requires localRoot to be a git repository (clone the repo and use it as content root, or run git init there).
  • Options: message (default "Update content (nx-content)"), remote (default "origin"), branch (default from config or "main").
  • When gitRepoUrl and gitToken are configured, the remote URL is set with auth before push so HTTPS push works.
  • Returns { pushed: boolean; commitHash?: string; noChanges?: boolean }. noChanges: true when there was nothing to commit.

Version history (git)

When the content root is a git repository, you can list versions, read at a ref, and restore a file to a previous version:

  • getPathForKey(key) — Returns the relative path under the content root for the key (key–path contract). Use for git commands or custom tooling.
  • getVersions(key) — Returns VersionEntry[] (sha, message, date, author) for the file backing the key. Empty if not a git repo.
  • getAtRef(key, ref) — Returns raw content of the file at git ref ref (commit sha, tag, or branch). Does not change the working tree. Throws if the file did not exist at that ref.
  • setActiveVersion(key, ref, options?) — Checkouts the file at ref so the working tree matches that version; optionally pass { commit: true, message?: string } to commit. Does not push (call pushToRemote() when you want to publish).
await resolver.set('prompts/new', 'Content here.');
const result = await resolver.pushToRemote({ message: 'Add new prompt' });
// result.pushed === true, result.commitHash set

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

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).
keyToPath(key) Canonical relative path for key (sync, no I/O). See key–path contract.
getContentRoot() Absolute content root path.
getContentRegistry() / getContentManager() Underlying ContentManager when enabled.
getPathForKey(key) Relative path under content root for the key (key–path contract). null if no local backend.
getVersions(key) Git version history for the file backing the key. Empty if not a git repo.
getAtRef(key, ref) Raw content of the file at git ref (sha, tag, or branch).
setActiveVersion(key, ref, options?) Checkout file at ref; optional { commit?, message? }.
exportToMetadata(options?) Sync/export content to local .metadata path (files or manifest).
pushToRemote(options?) Commit and push local content to Git (content root must be a repo).
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.
getPathForKey(key) Relative path under content root for the key. null if no local backend.
getVersions(key) Git version history for the file (VersionEntry[]). Empty if not a git repo.
getAtRef(key, ref) Raw content at git ref.
setActiveVersion(key, ref, options?) Checkout file at ref; optional commit.
invalidateKey(key) / invalidateAll() Clear cache.
exportToMetadata(options?) Sync/export content to local .metadata path (files or manifest).
pushToRemote(options?) Commit and push local content to Git (content root must be a repo).
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); values containing { are treated as content.
keyToPath(key) Canonical relative path for key (key–path contract, no I/O).
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

  • ContentManagerConfiglocalRoot, gitRepoUrl, gitToken, gitBranch, gitClonePath, mode, variant, cacheTtlMs
  • ResolvedContent{ text: string; metadata?: ContentMetadata }
  • ContentMetadatakey, path, backend, version, variant, rawContent
  • ExportToMetadataOptionsbasePath? (default '.metadata'), keys? (default: all from listKeys()), format?: 'files' | 'manifest'
  • ExportToMetadataResult{ written: string[]; errors: Array<{ key: string; error: Error }> }
  • PushToRemoteOptionsmessage?, remote?, branch?
  • PushToRemoteResult{ pushed: boolean; commitHash?; noChanges? }
  • VersionEntry{ sha: string; message: string; date: string; author?: string }
  • SetActiveVersionOptions{ commit?: boolean; message?: string }
  • SetActiveVersionResult{ updated: boolean }
  • ContentBackend — interface: read, exists, listAll, listKeys?, ping, location
  • DiagnosticsResultenabled, localRoot, backendReachable, availableKeys, error
  • GitBackendConfigrepoUrl, 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). Shorthand: owner/repo is normalized to https://github.com/owner/repo. 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.


.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.md

Testing

# Unit + integration (local)
npm test

# Integration only
npm run test:integration

# Watch
npm run test:watch
  • Git (read): Integration tests that clone a public repo run without a token. Tests that need a private repo or token run only when GITHUB_TOKEN (and optionally GITHUB_REPO_URL) are set; otherwise skipped.
  • Git (push): The push-to-remote integration test runs only when both GITHUB_TOKEN and GITHUB_PUSH_REPO_URL are set (use a repo you have write access to); otherwise skipped.

Build

npm run build

Produces:

  • dist/esm/ — ESM
  • dist/cjs/ — CommonJS ("type": "commonjs")
  • dist/types/ — Declaration files

License

MIT