A graph-shaped knowledge store for Node. Leaves hold facts. Tags index them. Stems connect them. The store persists to one JSON file and exposes a library API, CLI, small HTTP API, and optional LLM context layer.
- Library:
require('rosemary-js') - CLI:
rosemary <command> - Optional LLM layer:
require('rosemary-js/llm')
MIT. Node 20+.
npm install rosemary-jsFor the CLI globally:
npm install -g rosemary-jsconst Rosemary = require('rosemary-js');
const brain = new Rosemary({ dataFile: './my-data.json' });
brain.loadData();
const a = brain.addLeaf('JavaScript runs in browsers and Node', ['programming', 'web']);
const b = brain.addLeaf('HTML structures web documents', ['programming', 'web']);
brain.connectLeaves(a, b, 'co-occurs-with');
console.log(brain.getRelatedLeaves(a).map(l => l.content));Current: 3.0.2.
Version 3.0.2 is a documentation-rendering patch for 3.0.0 (Agent Context). Version 3.0.0 adds schema-versioned graph persistence, typed relationships, graph traversal, concept resolution, prompt-context packing, TypeScript declarations, and agent-context evals. Version 2.0.0 remains the clean Node 20 baseline. The full version table lives in CHANGELOG.md.
- Leaf — a node with
content,tags,metadata,id, and timestamps. - Stem — the relationship map between leaves.
- Tag — a free-form label used for lookup and fuzzy search.
- Connection — a typed relationship between two leaves.
Persistence is a single JSON file at options.dataFile (default ./rosemary-data.json). The file is rewritten on every mutation when autoSave is true (default).
new Rosemary({ dataFile: './data.json', autoSave: true })| Method | Returns | Notes |
|---|---|---|
addLeaf(content, tags = [], metadata = {}) |
string (id) |
nanoid-style id |
getLeafById(id) |
Leaf |
throws if missing |
getAllLeaves() |
Leaf[] |
|
updateLeaf(id, { content?, tags?, metadata? }) |
Leaf |
atomic |
removeLeaf(id) / deleteLeaf(id) |
void / boolean |
also removes connections |
getLeavesByConnection(id) |
Leaf[] |
|
getLeavesByContent(query) |
Leaf[] |
substring match |
fuzzySearch(query, fuseOptions?) |
{ item, score }[] |
content + tags |
| Method | Returns |
|---|---|
tagLeaf(id, ...tags) |
void |
getAllTags() |
{ name, count, leaves }[] |
getLeavesByTag(tag) |
Leaf[] |
getMostUsedTags(limit = 5) |
{ name, count, ... }[] |
suggestTags(partial, limit = 5) |
string[] |
| Method | Returns |
|---|---|
connectLeaves(idA, idB, relationshipType = '') |
void |
connectDirectedLeaves(fromId, toId, relationshipType = '') |
void |
getRelatedLeaves(id, maxDistance = 2) |
Leaf[] (BFS) |
infer(id, relationshipType = 'implies') |
{ leaf, relationship, distance, path }[] |
walk(startId?, maxLength = 5, mode = 'random') |
Leaf[] |
bridge(fromId, toId) |
{ path, relationships, distance } | null |
connectSimilarLeaves(threshold = 1) |
void (auto-connects on shared tags) |
getMostConnectedLeaves(limit = 5) |
Leaf[] |
getRandomConnectedChain(startId?, maxLength = 5) |
Leaf[] |
Reserved relationship types are exported from require('rosemary-js/edges'): implies, prerequisite-of, subset-of, co-occurs-with, contradicts, and aka. Custom relationship strings still work.
const result = brain.resolve('JS');
// => { canonical, candidates, confidence }resolve combines exact content matches, tags, metadata aliases, aka relationships, and Fuse.js fuzzy matches. RosemaryLLM.resolve() also adds semantic candidates when embeddings are available.
getLeavesSortedByCreationDate(asc?), getLeavesSortedByLastModified(asc?), getLeavesSortedByTagCount(asc?), getLeavesSortedByConnectionCount(asc?), getTagsSortedByLeafCount(asc?).
| Method | Notes |
|---|---|
loadData(file?) / saveData() |
reads/writes schema-versioned dataFile |
importData(jsonString) |
parses an in-memory JSON string |
exportToJSON(file) / importFromJSON(file) |
full snapshot |
exportToCSV(file, { delimiter = ',' }) |
leaves only (not connections) |
importFromCSV(file, { delimiter = ',' }) |
returns a Promise |
getLeafContentAsHTML(id) |
Markdown → sanitized HTML (DOMPurify) |
buildNetworkDataset() |
{ nodes, edges } for visualizations |
clearAllData() |
resets to default seed leaf |
rosemary add # interactive prompt
rosemary report # list all leaves
rosemary search <query> # substring search
rosemary connect <id1> <id2> [-r <rel>]
rosemary related <id> [-d <distance>]
rosemary delete <id>
rosemary clear
rosemary import-csv <file> [-s <sep>]
rosemary export-csv <file> [-s <sep>]All commands accept -d, --data-file <path> to point at a specific data file. The env var ROSEMARY_DATA_FILE works too.
rosemary report -d ./project-a.json
ROSEMARY_DATA_FILE=./project-a.json rosemary reportGenerate a network HTML from the current data file:
ROSEMARY_DATA_FILE=./rosemary-data.json npm run visualizeProgrammatic:
const Rosemary = require('rosemary-js');
const Builder = require('rosemary-js/builder');
const brain = new Rosemary({ dataFile: './rosemary-data.json' });
brain.loadData();
const data = brain.buildNetworkDataset();
new Builder(brain)
.useTemplate('dashboard')
.addVisualization('network', data, {})
.build('./graph.html');Builder requires http-server only if you call .build(path, { serve: true }). Install it on demand: npm install http-server.
const RosemaryLLM = require('rosemary-js/llm');
const brain = new RosemaryLLM({ autoSave: false });
const id = await brain.addEnhancedLeaf('Tokens expire after 24 hours', ['auth']);
await brain.addEnhancedLeaf('401 means authentication failed', ['auth', 'error']);
const results = await brain.semanticSearch('token expires');
const ctx = brain.buildPromptContext(id, { depth: 2, maxTokens: 500 });
const res = await brain.complete('What happens when my token expires?', id);The default generateEmbedding is a 64-dimensional character n-gram hash. It is fast and zero-dependency but it is not a real semantic embedding. For real semantic recall, pass a config.embedder function:
const brain = new RosemaryLLM({
embedder: async (text) => myTransformersPipeline(text)
});Providers (ClaudeProvider) are stubs by default. They return the prompt for inspection unless constructed with { live: true } and an API key. Never hardcode keys; read from environment variables or a secret manager.
const ClaudeProvider = require('rosemary-js/llm/providers/ClaudeProvider');
const brain = new RosemaryLLM({
claudeProvider: process.env.CLAUDE_API_KEY
? new ClaudeProvider(process.env.CLAUDE_API_KEY, { live: true })
: null
});src/api.js is an Express server exposing /api/v1/* and Swagger UI at /api-docs. Requires express, body-parser, swagger-jsdoc, swagger-ui-express (declared as optionalDependencies from 2.0.0 onwards). Run with:
ROSEMARY_DATA_FILE=./data.json node src/api.jsexamples/botany_paper_network/— paper / author / tag graph (CSV and JSON variants)examples/thailand_mindmap/— concept mindmapexamples/llm/minimal_llm_example.js— opt-in LLM context-buildingexamples/mcp/— dependency-free prototype of the future MCP package
Use Rosemary when an agent needs structured recall without loading a full knowledge base into the prompt. Store facts, decisions, test commands, release constraints, and known failure modes as leaves. Connect them with typed relationships. Ask for a compact context pack.
flowchart LR
dataFile[JSON data file] --> store[Leaves tags relationships]
store --> graphOps[resolve infer bridge]
store --> contextPack[buildPromptContext]
contextPack --> codingAgent[Coding agent]
codingAgent --> releaseChecks[tests evals release gates]
Run the included context eval:
npm run eval:agent-contextForward-looking notes live in docs/direction.md. Non-goals live in docs/non-goals.md.
A single command, governed by RELEASE.md:
npm run release -- patch # or minor, or majorThis runs the test suite, asserts the working tree is clean, bumps the version, creates the matching git tag, pushes with --follow-tags, and publishes to npm. The version in package.json is the single source of truth; git tag and npm registry must match.
See CONTRIBUTING.md. Run npm test before opening a PR.
MIT. See LICENSE.md.
Maintained by @luisfer.
