@dinesh-gamage/pulse
v0.10.0
Published
PULSE — Persistent Unified Layer for Software Engineering. Shared context and control for AI coding agents.
Maintainers
Readme
PULSE
Persistent Unified Layer for Software Engineering
Shared context and control for AI coding agents.
Pulse is a local CLI that maintains engineering knowledge across repositories and supplies relevant context to coding agents. The current MVP integrates with Claude Code. Desktop work follows the completed CLI product.
Run from this repository
Requires Node.js 18.20+ and Git. Claude Code is needed only for runtime integration.
npm install
npm run build
node bin/pulse.mjs --helpFor the examples below, make the executable available as pulse. You can use npm link, or define a shell alias to node /absolute/path/to/pulse/bin/pulse.mjs. Development commands also work through npm run dev -- <command>.
The CLI uses @dinesh-gamage/cli-kit for tables, colors, progress feedback, and logging, with Commander for argument parsing. Use --json for machine-readable output. Logs and progress go to stderr; hooks and MCP emit only their protocol on stdout.
New to it? Using Pulse is the whole workflow in order, in plain language.
Register your software system
pulse init
pulse project create acme --name "Acme Platform"
pulse repo add ~/code/acme/backend --project acme --name backend --purpose "HTTP API"
pulse repo add ~/code/acme/auth --project acme --name auth --purpose "Authentication and sessions"
pulse repo add ~/code/acme/frontend --project acme --name frontend --purpose "Browser application"
pulse component add token-service --project acme --repo auth --purpose "Token generation and refresh"
pulse relationship add repo:backend calls component:token-service --project acme
pulse repo list --project acmeRepositories stay where they are. Register their Git roots; nested or overlapping registrations are rejected. Once inside a registered repository, Pulse infers its project and repository. Explicit --project and --repo flags override that inference.
Project creation accepts --directory /path/to/canonical-context for knowledge you want to version yourself. Existing Pulse manifests can be registered through the same command, then their repository IDs rebound with repo add or repo edit --path. Pulse never commits these files.
Capture durable knowledge
pulse knowledge add --project acme --scope project --type rule --title "API compatibility" \
--body "All API changes must remain backward compatible."
pulse knowledge add --project acme --repo auth --type architecture \
--title "Token refresh" --file ./auth-architecture.md
pulse knowledge list --project acme
pulse knowledge show <id>
pulse knowledge edit <id>Supported types: fact, rule, decision, architecture, finding, and diagram. A diagram is a description and one mermaid block. Findings begin as provisional. Other manually authored records default to confirmed; use --status to choose explicitly. knowledge edit <id> --status confirmed promotes a reviewed finding.
Without --body or --file, add/edit opens $VISUAL or $EDITOR, falling back to vi. Editor commands require an interactive terminal. knowledge import <file> --type architecture --title "..." imports Markdown as a new record with provenance. A repository flag scopes knowledge to that repository; --scope project creates project-wide knowledge even when your terminal is inside a repository.
Canonical knowledge is Markdown with YAML frontmatter. External edits are incorporated by the next pulse index. Agent MCP tools are read-only in this MVP; saving discoveries is a CLI operation.
Index and inspect context
pulse index --project acme
pulse context search "token refresh" --project acme
pulse context resolve "Fix the login timeout" --project acme --repo backend --explain
pulse context get <id> --project acmeThe index includes documentation, selected configuration, source chunks, and declaration names extracted per language (JavaScript/TypeScript, Python, Go, Rust, C#, Java, Kotlin, Ruby, PHP, C/C++, Elixir, SQL, Terraform, shell and more, with a keyword fallback elsewhere). It excludes ignored files, common secret files, generated/dependency directories, binaries, files over 256 KiB, and external symlinks. Add patterns to a repository's .pulseignore, with pulse settings add exclusions <pattern> for one project, or with pulse config set exclusions <patterns...> for every project. Language detection counts tracked files rather than reading one ecosystem's manifest, so a repository with no manifest is still classified.
Indexing refreshes changed/deleted chunks and preserves unchanged embeddings. Run it after switching branches or editing relevant source files. Changed source content is flagged during retrieval and omitted from automatic task context until refreshed. There is no background file watcher yet.
Context packets carry relevant knowledge and source pointers. Rules are not among them — see below. --explain reports selection reasons, omissions, index timestamps, and estimated cost. Default prompt ceiling: 1,000 estimated tokens, enforced as 3,000 UTF-8 bytes; change it with pulse config set contextTokens <n>, override it per project with pulse settings set contextTokens <n>, or use context resolve --budget. These counts are estimates, not provider tokenizer measurements.
Rules are retrieved, never injected. Re-sending them on every prompt spent 82% of a measured packet on bytes the conversation already held, so packets and session maps now carry the obligation and the count, and the agent calls context_rules — narrowed by tag — before it plans or edits. Rules provide guidance; Pulse does not enforce source-edit policies in this MVP.
At session start Pulse delivers a project map instead of a bare identity line: every repository with
its language, the purpose of the ones marked mandatory, the confirmed relationships between them, and
the rule and index state. This is the context you would otherwise retype at the start of every
session. It is budgeted separately from prompt packets by sessionTokens (global default 1,000
estimated tokens, per-project override under settings.sessionTokens). Set it to 0 to fall back to
the identity line.
A repository's purpose reaches the model one of two ways. Marked --mandatory it is stated in full
in every map and is never trimmed. Otherwise the map lists the repository by name, and its purpose
arrives on a prompt that names one of its --tags. On a project of twenty repositories, most
purposes are never relevant to the prompt at hand, and sending them all forced every one of them down
to a truncated clause.
The map degrades by shortening and then dropping non-mandatory purposes, then collapsing relationships to a pointer. If the roster plus the mandatory purposes still do not fit, it is sent over budget rather than losing repositories, and the overshoot is recorded in the session log.
Tune one project without affecting the others
Anything that differs between projects belongs in that project's manifest, not in global configuration. All fields are optional:
pulse settings # current values and what they resolve to
pulse settings add exclusions "build/generated/" # gitignore syntax, this project only
pulse settings add stopwords widget # extends the built-in query stopwords
pulse settings add extensions .liquid # extra indexable file types
pulse settings add deprioritise "**/vendor-ui/**" # searchable, ranked below first-party code
pulse settings set contextTokens 1500 # overrides the global default
pulse settings set sessionTokens 1500 # overrides the session-start map budget
pulse settings set transcript full # off | basic | full
pulse settings unset contextTokens # fall back to the global defaultEvery value is validated on write, so the manifest cannot be left invalid. pulse config does the
same for the global defaults, and pulse config price <model> sets the rates used for cost
reporting. Nothing here requires editing a file by hand.
Built-in defaults are extended, never replaced. The global exclusions array stays for patterns that
genuinely apply to every project you register.
Enable local semantic search
pulse semantic setup
pulse index --project acme
pulse semantic statusSetup explicitly downloads a pinned quantized Xenova/all-MiniLM-L6-v2 model. Inference then runs locally; normal retrieval never downloads weights or sends source text to an embedding service. The Transformers.js dependency is optional so lexical operation remains available if its native runtime cannot be installed.
CLI search and MCP use hybrid retrieval when the model and indexed vectors are available. Hooks use lexical retrieval to keep fresh-process latency bounded; they never initialize the model. --no-semantic selects lexical-only indexing/search. If semantic retrieval fails, search reports a warning and returns lexical results.
Connect Claude Code
cd ~/code/acme/backend
pulse integration install claude --dry-run
pulse integration install claude
pulse doctor
claude
# Or: pulse run claude -- --model <model>Installation merges three hooks into .claude/settings.local.json: SessionStart, UserPromptSubmit, and SessionEnd. The MCP server is registered at local scope in ~/.claude.json, under the repository path; CLAUDE_CONFIG_DIR is respected. Existing hooks, unrelated servers, and permissions are preserved. Setup does not grant workspace trust or bypass runtime permissions.
Repeat installation safely. pulse integration uninstall claude removes Pulse-owned entries while retaining other configuration. If the MCP entry was independently changed, Pulse asks you to resolve the conflict instead of overwriting it. Configuration backups and installation records are kept in Pulse's home; an interrupted installation is reported by doctor and can be retried.
After setup, ordinary claude sessions receive context. Session start/recovery supplies identity; prompt submission supplies bounded relevant context. Nothing is injected twice in a session — knowledge, repository purposes and relationships all pass through the same delivery ledger — and everything becomes eligible again after session restart, clear, or compaction. Applicable rules are counted and named, and retrieved with context_rules. Hook failures time out and allow the coding session to continue.
Agent tools:
| Tool | Purpose |
| --- | --- |
| context_search | Search the current project's indexed context |
| context_get | Retrieve a source by stable ID, with pagination |
| context_relationships | Inspect confirmed component/repository relationships |
| context_rules | Retrieve applicable confirmed rules in full, narrowed by tag |
Runtime references: Claude hook lifecycle, Claude MCP configuration scopes.
Scripting Pulse
Every command takes --json. The contract is small and worth stating exactly, because other tools
depend on it.
One document on stdout, per process. No streaming, no envelope around a successful result — the
command's value is the payload. pulse project list --json returns the array it always did.
A failure is a document too. It is the only output with an ok key:
{"ok": false, "error": {"code": "unknown_project", "message": "Unknown project \"x\". Use pulse project list.", "details": null}}Codes: invalid_input (with details carrying the failing fields), unknown_project,
no_project, database_newer, busy, live_sessions, usage, error. The human-readable line
still goes to stderr, so a terminal session is unchanged.
Parse stdout first; the exit code is advisory. doctor exits 1 with a complete, valid report
when it finds issues, and policy test exits 2 on a deny. Neither is a failure. If stdout parses,
the command produced a result.
Progress goes to stderr. Under --json, long commands emit one JSON object per line as they
work, so a caller is not blind while an index runs:
{"v":1,"at":"…","event":"progress","command":"Indexing acme","step":"indexing backend","done":2,"total":4}Any stderr line that parses as JSON with "event":"progress" is progress; everything else is
diagnostics.
Start with pulse overview --json. One call returning the whole installation: home, config,
every project with its repositories, their local paths and index state, components, relationships,
resolved effective settings, observed sessions, and logging and embedding state. It deliberately
skips the runtime version check and the database checksum that make doctor slow, so it answers in
about 200 milliseconds. Use doctor when you want health, where that cost belongs.
Reading a session log. pulse transcript show <session> --resolve --json adds a documents
map turning every document id in the log into a title, repository and kind, in one query. An id
whose document has since been re-indexed comes back as {"missing": true} rather than failing the
whole read.
Indexing refuses to run under a live coding session. The hook waits only 100 ms for the
database; indexing holds a write lock for much longer, so an index started mid-session would make
that session silently lose its context. pulse index reports code live_sessions and names them.
--force overrides.
Storage and maintenance
By default, Pulse stores data in ~/.pulse; set PULSE_HOME for an isolated workspace.
config.json Global defaults: context budget, exclusions, logging, prices
state.sqlite Registration, index, embeddings, sessions, delivery state
logs/<project>/<session>.jsonl Optional per-session log (off by default)
projects/<id>/project.yaml Portable project/repository/component metadata and per-project settings
projects/<id>/relationships.yaml
projects/<id>/layout.yaml Where the relationship graph puts things
projects/<id>/knowledge/*.md Durable knowledge
cache/ Downloaded embedding model
integrations/ Configuration ownership and recovery records
backups/ Original runtime configuration backupsproject remove unregisters a project and retains canonical files. repo remove removes its association and indexed source, preserving the source tree; dependent components, relationships, and scoped knowledge must be removed or reassigned first. knowledge remove deletes that knowledge file. Full database deletion also loses local bindings and session state; it is not an index-reset command.
Pulse records no prompts and no transcripts by default. Session logging is opt-in: pulse config set transcript basic|full, or per project with pulse settings set transcript. basic records what was retrieved and why — document IDs, delivery mode, omission reasons, counts and timings — and nothing derived from the words of your prompt. full additionally records each prompt and the injected packet verbatim, so a session can be read back exactly as the model received it. Logs are plain JSONL under ~/.pulse/logs/, pruned after transcriptDays (default 30), and never leave the machine. pulse status shows the active level at a glance.
Logging never changes what is injected. The packet is byte-identical with it on or off, and a test enforces that.
pulse transcript list # logged sessions, what was injected, what they billed
pulse transcript audit <session> # what was injected, and what the model then did with it
pulse transcript show <session> --context # the injected text, exactly as delivered
pulse transcript summary # injected context measured against actual usage
pulse transcript prune # apply the retention window nowToken usage is read from the coding runtime's own session file, on demand, never inside a hook — those files reach hundreds of megabytes. Dollar figures need rates you set with pulse config price <model> (the model id is matched by substring, rates are per million tokens); Pulse ships none of its own, because rates change. Any figure shown is a list-price estimate over per-request usage, not an invoice.
Sessions show last observed activity; sessions without a reliable end signal become stale/unknown rather than appearing permanently active.
Validation and current boundary
Experimental local rule checks are available with pulse run claude --policy <file>. They check package dependencies and task-specific write paths before edits. This opt-in mode blocks shell and unsupported tools; builds/tests run in a separate terminal. See policy hooks, editable examples, and trial results.
npm test
npm run check
npm run benchmark
# Optional, after downloading into an isolated validation home:
PULSE_HOME="$PWD/.pulse/validation" node bin/pulse.mjs semantic setup
npm run benchmark -- --semantic
node --import tsx scripts/live-smoke.tsTests cover persistence, scoping, exclusions, invalidation, packet budgets, recovery, configuration preservation, MCP, and concurrent writers. The 20-prompt synthetic benchmark reports retrieval recall/precision and hook latency in eval/results/synthetic/benchmark-*.json. The live smoke uses the installed Claude runtime's initialization lifecycle without making a provider request.
See current state for measured results and remaining real-project validation. Synthetic relevance and latency results do not establish token savings on real work. Agents that own repositories, with per-stage models, are in 0.9.0 (see Using Pulse). Task orchestration beyond one lead, file claims and other runtimes are later phases in the MVP plan.
