Thanks for helping out. ContextSpin is intentionally small and dependency-light; please keep it that way.
You need Node.js >= 18 (the code relies on the built-in global fetch).
git clone <your-fork-url> contextspin
cd contextspin
npm install
npm testnpm install pulls a single runtime dependency (commander) plus the optional node-lief (used only by the experimental patcher — everything works without it). npm test runs the test suite with node --test.
Handy scripts:
npm start # node src/cli.js (run the CLI from source)
npm run daemon # node src/daemon-entry.js (run the poll loop in the foreground)contextspin/
├── package.json
├── .contextspin.example.json # seed config copied by `setup`
├── LICENSE
├── README.md
├── CONTRIBUTING.md
├── src/
│ ├── cli.js # commander entrypoint (bin)
│ ├── config.js # PATH constants, defaults, load/validate/normalize/save
│ ├── formatter.js # getPath, interpolate, applyFilter (safe, no eval)
│ ├── runner.js # runSource: dispatch -> filter -> format -> Snippet
│ ├── daemon.js # cache read/write, mergeSnippets, poll loop, detach/stop
│ ├── daemon-entry.js # detached entrypoint that calls runDaemonLoop
│ ├── sources/
│ │ ├── cli.js # fetchCli (spawn shell, parse stdout)
│ │ ├── http.js # fetchHttp + jqPath (minimal jq subset)
│ │ └── mcp.js # fetchMcp + discoverMcpServers + expandEnv (stdio JSON-RPC)
│ └── inject/
│ ├── statusline.js # install/uninstall statusline.sh + settings.json patch
│ └── patcher.js # EXPERIMENTAL binary/text spinner-word patcher
└── test/
├── formatter.test.js
├── config.test.js
├── cli-source.test.js
├── runner.test.js
├── daemon-merge.test.js
└── sources-pure.test.js
All filesystem paths (CONFIG_PATH, CACHE_PATH, STATE_DIR, etc.) are defined and exported only from src/config.js. Every other module imports them from there — never hard-code a path.
A source type is one async fetch function that turns a source config into a list of plain-object records. To add one (say foo):
- Create
src/sources/foo.js. Exportasync fetchFoo(source, opts). It must returnArray<object>— one record per item. Accept anoptsobject and honoropts.timeoutMs(default to a sane value) so the daemon can't hang. Throw a clearErroron failure; the runner lets it propagate. Reuse the normalization convention of the existing sources (array → elements, object → single record, primitive →{ value, text }). - Wire it into
src/runner.js. ImportfetchFooand add a branch torunSource's dispatch onsource.type. The runner already handles filtering (applyFilter), formatting (interpolate(source.format, record)), the empty-text skip,maxSnippetsslicing, and Snippet shaping — your function only produces records. - Extend validation in
src/config.js. Addfooto the allowedtypeset invalidateConfig, throw on any required field your type needs (mirror howmcp/cli/httprequiretool/command/url), and add alabelderivation innormalizeConfig. - Add tests under
test/for the pure parts (parsing, normalization) and forrunSourceover the new type. Keep them hermetic — no network. - Document it in the README's "Source types" section with a JSON example using
{{ }}templating.
Keep fetchFoo pure-ish where possible and free of global state, so it's easy to test.
- ESM only.
package.jsonhas"type": "module"; useimport/export. No CommonJS. - No TypeScript syntax. Plain JavaScript. Use JSDoc on every export to document parameters and return types.
commanderis the only allowed runtime dependency (node-liefis an optional dep used solely bysrc/inject/patcher.jsbehind a guarded dynamic import — all code must work without it). Do not add other dependencies, and do not pull in@modelcontextprotocol/sdk; the MCP client is hand-rolled over stdio. Everything else comes from Node built-ins (node:fs,node:path,node:os,node:child_process,node:readline/promises,node:crypto, …).- Small, focused functions with clear, actionable
Errormessages. - Each file starts with a one-line comment naming the file and its role.
- Match the simple, approachable tone of the existing code.
npm test # run everything (node --test)
node --test test/formatter.test.js # run one fileTests use node:test and node:assert/strict. Guidelines:
- Hermetic and offline. No network calls. For
cli/http/mcp, drive behavior with local fixtures — e.g. spawnprocess.execPath -e "<script that prints JSON>"for CLI tests. - Use temp files, built from
os.tmpdir()+ acryptorandom suffix, and clean them up. To exercise config loading, setprocess.env.CONTEXTSPIN_CONFIGto a temp path before importingsrc/config.js, or pass the path argument explicitly. - Don't import
src/cli.jsfrom tests — keep tests independent of commander and side effects. Test the underlying functions directly. - Cover the pure logic thoroughly:
getPath/interpolate/applyFilter,normalizeConfig/validateConfig,jqPath/expandEnv,mergeSnippets, and the source parsers.
- One focused change per PR. Keep the diff small.
npm testmust pass, and add tests for any new behavior or bug fix.- Update the README when you change user-facing behavior, config fields, or CLI commands.
- Don't add dependencies or introduce TypeScript.
- Stay within the current stage's scope — see the roadmap in the README. Features beyond Stage 1 (non-stdio MCP transports, OAuth connectors, etc.) are explicitly out of scope for now.
- Describe what changed and why, and note anything you couldn't test.
By contributing you agree your contributions are licensed under the project's MIT license.