@ghostpaw/affinity 0.3.0
Who have you been meaning to call?
affinity keeps the people your agent deals with, the promises made to them and the dates that matter in your SQLite file. It works out who is drifting from what happened, so nobody has to keep a list of who to call.
As a library
As a local MCP server
Replay the run Read the source on GitHub
Free and MIT licensed for Node.js 24 or newer, with zero runtime dependencies. The MCP server keeps everything in ~/.ghostpaw/affinity.db on your machine.

0runtime dependencies
Three people, five calls
Jun is at the top of the radar. One evening out moves him to the bottom.
This is a replay of a real run. We made these calls against affinity 0.3.0 on 2026-10-24 with a fixed clock, and every number below is what came back. Mika knows three people: Ren, an old roommate she called six days ago; Jun, a climbing partner she last saw 27 days ago; and Ada, who she met at a conference 41 days ago. Run the steps in order.
Radar and open items
- Jun0.347 · drifting
- Ada0.124 · routine
- Ren0.000 · routine
- Open promisesnone
- Next 60 daysnothing
read.listRadar(db), highest drift first
-
01 An evening out with Jun
Mika records what happened. She does not set a score.
write.recordInteraction(db, { type: 'activity', occurredAt: now, summary: 'Climbing with Jun, then dinner', significance: 6, participants: [ { contactId: mika.id, role: 'actor', directionality: 'mutual' }, { contactId: jun.id, role: 'recipient', directionality: 'mutual' }, ], });// derivedEffects [{ affinityDelta: 0.245, trustDelta: 0.025, rankAfter: 2, cadenceAfter: 17 }] read.listRadar(db).map((r) => r.name) // ['Ada', 'Ren', 'Jun']Jun goes from the top of the radar to the bottom. The library also stretched his expected cadence from 14 to 17 days.
-
02 A promise to Ren
A commitment is a journal entry with a resolution that starts out open.
const promise = write.recordCommitment(db, { commitmentType: 'promise', occurredAt: now, summary: 'Send Ren the notes on the offer letter', significance: 5, participants: [ { contactId: mika.id, role: 'actor', directionality: 'owner_initiated' }, { contactId: ren.id, role: 'recipient' }, ], });// derivedEffects [{ affinityDelta: 0.031, trustDelta: -0.044, rankAfter: 3, cadenceAfter: 12 }] read.listOpenCommitments(db).length // 1Ren's trust went down by 0.044 when the promise was recorded. It now shows as open.
-
03 Close it as “done”
This is how an agent would naturally put it.
write.resolveCommitment(db, promise.primary.id, 'done');// AffinityValidationError // resolution must be kept, cancelled, or brokenRefused. A promise is kept, cancelled or broken, and “done” is none of those.
-
04 Close it as kept
write.resolveCommitment(db, promise.primary.id, 'kept');// derivedEffects [] read.listOpenCommitments(db).length // 0The promise is closed. Keeping it did not add anything back in this run: Ren's trust stays at 0.669.
-
05 Jun's birthday
A date anchor is a yearly date on a contact.
write.addDateAnchor(db, { target: { kind: 'contact', contactId: jun.id }, recurrenceKind: 'birthday', anchorMonth: 11, anchorDay: 12, summary: "Jun's birthday", significance: 7, });read.listUpcomingDates(db, { horizonDays: 60 }) // [{ summary: "Jun's birthday", recurrenceKind: 'birthday', occursOn: '2026-11-12' }]It turns up 19 days ahead, with no calendar entry needed.
The radar is refreshed when an event is written, not when you read it. That is why this replay never moves the clock: without a new write, the order stays where the last write left it.
Every write returns derivedEffects, so your agent can say what changed instead of guessing.
What it keeps, and what it works out
A relationship is its own record, not a field on a contact card.
- Contact
- A person, company, team, service, pet, household or group, plus one owner contact for you.
- Identity
- An email, phone number, URL or account id that points at exactly one contact. Duplicates can be merged or dismissed.
- Social link
- The relationship itself, with rank, affinity, trust, cadence, a short bond note and a state.
- Tie
- A structural fact such as
works_at,reports_toor kinship. - Journal entry
- What happened: an interaction, observation, milestone, transaction or commitment.
- Date anchor
- A yearly date such as a birthday, anniversary or renewal.
- Attribute
- Free keys and values for preferences, on contacts and on links.
Rank, trust, drift and the rest are computed from the journal and the ties. None of them is a status flag your code sets by hand.
If a people database makes you uneasy
The usual cost is the client or friend you forgot to follow up with. The usual fear is the opposite: an assistant that keeps a page on everyone you mention, which sounds like a dossier, and recent coverage of exactly that is fair warning.
- It is a library and a local MCP server. There is no hosted service and nothing leaves the SQLite file unless your code sends it.
- It stores what you or your agent record. It does not read your mail, messages or contacts on its own.
- The MCP server's file is
~/.ghostpaw/affinity.db, or whereverGHOSTPAW_AFFINITY_DBor--dbpoints. - Twelve tools with JSON Schemas, among them
record_event,manage_commitmentandreview_affinity. Each returns success, no_op, needs_clarification or error. - Node.js 24 or newer, because it uses the built-in
node:sqlite.
What it costs to add
- Runtime dependencies
- 0
- Node.js
- 24 or newer (node:sqlite)
- Size of dist/index.js, gzip
- 56.1 KB
- Import time
- about 5.7 ms
- Tests
- 459 passing
- Line coverage
- 95.3%
- Agent tools
- 12
Measured 2026-10-24 on the maintainer's machine with Node.js 24.14.0 against affinity 0.3.0: size of the published dist/index.js at gzip -9, median of 11 cold imports, tests and coverage from the v0.3.0 tag.
Start from the README
import { DatabaseSync } from 'node:sqlite';
import { initAffinityTables, read, write } from '@ghostpaw/affinity';
const db = new DatabaseSync(':memory:');
db.exec('PRAGMA foreign_keys = ON');
initAffinityTables(db);
const { primary: owner } = write.createContact(db, {
name: 'Mika',
kind: 'human',
bootstrapOwner: true,
});
const { primary: ada } = write.createContact(db, { name: 'Ada Lovelace', kind: 'human' });
write.addIdentity(db, ada.id, { type: 'email', value: 'ada@example.com' });
write.recordInteraction(db, {
type: 'conversation',
occurredAt: Date.now(),
summary: 'Coffee together',
significance: 6,
participants: [
{ contactId: owner.id, role: 'actor', directionality: 'mutual' },
{ contactId: ada.id, role: 'recipient', directionality: 'mutual' },
],
});
const radar = read.listRadar(db);
To run it as an MCP server instead, add npx -y @ghostpaw/affinity mcp to your client. npx -y @ghostpaw/affinity skill prints the Agent Skill that ships in the package.
Read the READMEOpen the live demoaffinity on npm
All seven GhostPaw librariescodex, for memoryquestlog, for taskssouls, for identitygrimoire, for skillsemail, for mailtelegram, for chats