
Threat Research
/Security News
Searchjacked: Hidden Browser Extensions Hijack Searches for 13 Million Users
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.
@ghostpaw/affinity
Advanced tools
Relationship intelligence and social CRM model for Node.js — SQLite, zero dependencies, with a built-in LLM tool facade
Agent contacts: people, promises and yearly dates, with a radar for who is drifting.
A local social CRM: contacts, identities, relationships, journal events, commitments and recurring dates in one SQLite file. One self-contained binary per platform, an MCP server, and a zero-dependency TypeScript library.
Each line downloads the latest release for one platform, checks it against
SHA256SUMS, puts ghostpaw-affinity on your PATH and prints the version.
Linux x64
cd "$(mktemp -d)" && T=linux-x64 && V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/GhostPawJS/affinity/releases/latest | sed 's|.*/v||') && F=ghostpaw-affinity-$V-$T.tar.gz && U=https://github.com/GhostPawJS/affinity/releases/download/v$V && curl -fsSL -O "$U/$F" -O "$U/SHA256SUMS" && grep " $F\$" SHA256SUMS | sha256sum -c - && tar -xzf "$F" ghostpaw-affinity && mkdir -p ~/.local/bin && mv ghostpaw-affinity ~/.local/bin/ && { case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile; export PATH="$HOME/.local/bin:$PATH";; esac; } && ghostpaw-affinity --version
Linux arm64
cd "$(mktemp -d)" && T=linux-arm64 && V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/GhostPawJS/affinity/releases/latest | sed 's|.*/v||') && F=ghostpaw-affinity-$V-$T.tar.gz && U=https://github.com/GhostPawJS/affinity/releases/download/v$V && curl -fsSL -O "$U/$F" -O "$U/SHA256SUMS" && grep " $F\$" SHA256SUMS | sha256sum -c - && tar -xzf "$F" ghostpaw-affinity && mkdir -p ~/.local/bin && mv ghostpaw-affinity ~/.local/bin/ && { case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile; export PATH="$HOME/.local/bin:$PATH";; esac; } && ghostpaw-affinity --version
macOS Apple silicon
cd "$(mktemp -d)" && T=darwin-arm64 && V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/GhostPawJS/affinity/releases/latest | sed 's|.*/v||') && F=ghostpaw-affinity-$V-$T.tar.gz && U=https://github.com/GhostPawJS/affinity/releases/download/v$V && curl -fsSL -O "$U/$F" -O "$U/SHA256SUMS" && grep " $F\$" SHA256SUMS | shasum -a 256 -c - && tar -xzf "$F" ghostpaw-affinity && sudo mkdir -p /usr/local/bin && sudo mv ghostpaw-affinity /usr/local/bin/ && ghostpaw-affinity --version
macOS Intel
cd "$(mktemp -d)" && T=darwin-x64 && V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/GhostPawJS/affinity/releases/latest | sed 's|.*/v||') && F=ghostpaw-affinity-$V-$T.tar.gz && U=https://github.com/GhostPawJS/affinity/releases/download/v$V && curl -fsSL -O "$U/$F" -O "$U/SHA256SUMS" && grep " $F\$" SHA256SUMS | shasum -a 256 -c - && tar -xzf "$F" ghostpaw-affinity && sudo mkdir -p /usr/local/bin && sudo mv ghostpaw-affinity /usr/local/bin/ && ghostpaw-affinity --version
Windows x64 (PowerShell)
$r='GhostPawJS/affinity'; $v=(irm "https://api.github.com/repos/$r/releases/latest").tag_name; $f="ghostpaw-affinity-$($v.TrimStart('v'))-windows-x64.zip"; $u="https://github.com/$r/releases/download/$v"; $t=(New-Item -ItemType Directory (Join-Path $env:TEMP ([guid]::NewGuid()))).FullName; iwr "$u/$f" -OutFile "$t\$f" -UseBasicParsing; iwr "$u/SHA256SUMS" -OutFile "$t\SHA256SUMS" -UseBasicParsing; $want=((Get-Content "$t\SHA256SUMS" | Where-Object { $_ -like "* $f" }) -split '\s+')[0]; if (-not $want -or (Get-FileHash "$t\$f" -Algorithm SHA256).Hash -ne $want) { throw "checksum mismatch for $f" }; $d=Join-Path $env:LOCALAPPDATA 'Programs\ghostpaw\bin'; Expand-Archive "$t\$f" $t -Force; New-Item -ItemType Directory -Force $d | Out-Null; Copy-Item "$t\ghostpaw-affinity.exe" $d -Force; $p=[Environment]::GetEnvironmentVariable('Path','User'); if (";$p;" -notlike "*;$d;*") { [Environment]::SetEnvironmentVariable('Path',"$p;$d",'User') }; $env:Path="$d;$env:Path"; ghostpaw-affinity --version
These commands install the latest release in ~/.local/bin on Linux, /usr/local/bin on macOS (asks for sudo), or %LOCALAPPDATA%\Programs\ghostpaw\bin on Windows; Linux adds it to PATH via ~/.profile only if missing, and Windows adds it to the user PATH (open a new terminal afterwards). To pin a version, set V=0.6.0 instead of resolving latest ($v='v0.6.0' in PowerShell).
For a pinned Linux/macOS download to the current directory with no profile or PATH edits, choose the matching T (linux-x64, linux-arm64, darwin-arm64, or darwin-x64):
V=0.6.0
T=linux-x64
F=ghostpaw-affinity-$V-$T.tar.gz
U=https://github.com/GhostPawJS/affinity/releases/download/v$V
curl -fsSL -O "$U/$F" -O "$U/SHA256SUMS" &&
{ case "$T" in darwin-*) grep " $F\$" SHA256SUMS | shasum -a 256 -c - ;; *) grep " $F\$" SHA256SUMS | sha256sum -c - ;; esac; } &&
tar -xzf "$F" ghostpaw-affinity && ./ghostpaw-affinity --version
Then:
ghostpaw-affinity contact create '{"name":"Sam","kind":"human"}'
ghostpaw-affinity search sam
ghostpaw-affinity skill # cheat sheet and tool reference
Contacts live in ~/.ghostpaw/affinity.db unless --db <path> or the
GHOSTPAW_AFFINITY_DB environment variable says otherwise; relative paths
resolve against the current directory.
A session with the linux-x64 binary against a fresh database:

The same session as an animated GIF.
The npm package needs Node 24+ and has zero runtime dependencies:
npx -y @ghostpaw/affinity --help
npm install -g @ghostpaw/affinity # provides ghostpaw-affinity
npm install @ghostpaw/affinity
import { DatabaseSync } from "node:sqlite";
import { initAffinityTables, read, write, tools } from "@ghostpaw/affinity";
See Quick Start and docs/LLM.md for the tool facade.
Interactive installers (also verify SHA256SUMS):
curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh | sh
irm https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.ps1 | iex
mise use -g github:GhostPawJS/affinity
ubi --project GhostPawJS/affinity --exe ghostpaw-affinity --in ~/.local/bin
eget GhostPawJS/affinity
For aqua, define a
github_release package in aqua.yaml:
registries:
- name: affinity
type: local
path: affinity-registry.yaml
packages:
- name: GhostPawJS/affinity@v<VERSION>
registry: affinity
affinity-registry.yaml:
packages:
- type: github_release
repo_owner: GhostPawJS
repo_name: affinity
asset: ghostpaw-affinity-{{trimV .Version}}-{{.OS}}-{{.Arch}}.tar.gz
format: tar.gz
replacements: { amd64: x64 }
overrides:
- goos: windows
asset: ghostpaw-affinity-{{trimV .Version}}-windows-{{.Arch}}.zip
format: zip
files:
- name: ghostpaw-affinity
SHA256SUMS covers every archive and server.mcpb. To verify a downloaded
archive's build attestation:
gh attestation verify <archive> -R GhostPawJS/affinity
Set GHOSTPAW_VERIFY=attestation for attestation verification in install.sh.
Binaries are unsigned; macOS binaries carry an ad-hoc signature. For a browser
download on macOS, remove quarantine with
xattr -d com.apple.quarantine /path/to/ghostpaw-affinity.
Linux binaries require glibc 2.28+; use npx on Alpine.
See the binary build workflow.
Each archive is roughly 40-46 MB (the binary embeds the Node runtime, 105-150 MB unpacked); the npm package is about 2 MB unpacked. Every release, including the one-liners above, is installed and run on ubuntu-24.04, macos-15 and windows-2025 by the install check.
Every client below starts the same stdio server: ghostpaw-affinity mcp.
Without the binary, use npx -y @ghostpaw/affinity mcp as the command instead.
Claude Code
claude mcp add ghostpaw-affinity -- ghostpaw-affinity mcp
or as a plugin, which also installs the skill:
/plugin marketplace add GhostPawJS/affinity
/plugin install ghostpaw-affinity@ghostpaw-affinity
Codex CLI (~/.codex/config.toml)
[mcp_servers.ghostpaw-affinity]
command = "ghostpaw-affinity"
args = ["mcp"]
Cursor (.cursor/mcp.json), Claude Desktop (claude_desktop_config.json)
or a project .mcp.json:
{
"mcpServers": {
"ghostpaw-affinity": {
"command": "ghostpaw-affinity",
"args": ["mcp"]
}
}
}
Claude Desktop users can also install server.mcpb from the
latest GitHub release.
A Dockerfile is included for container hosts:
docker build -t ghostpaw-affinity . && docker run -i --rm -v affinity:/data ghostpaw-affinity.
The server speaks MCP 2026-07-28 (stateless, server/discover) and the
initialize-based revisions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05.
SKILL.md (repo root) and skills/affinity/SKILL.md follow the
Agent Skills format and ship in the npm package.
Copy skills/affinity/ into your agent's skills folder, or print it:
ghostpaw-affinity skill > SKILL.md
ghostpaw-affinity skill --list # workflow guides
ghostpaw-affinity skill bootstrap-and-cold-start
Record events and commitments with an optional externalRef: {source, id}.
Both values are trimmed and non-empty; source is lowercased. The pair is unique
across the database. Repeated intake returns the original event and no_op,
without creating rows or relationship effects.
content preserves text exactly, including whitespace, empty strings and NUL,
up to 1 MB (1,048,576 UTF-8 bytes). Supply a short summary, or omit it to derive
one from the first non-empty content line (at most 140 characters; empty text
becomes (no text)). Unknown event fields and moment kinds are validation errors.
Journal/timeline rows omit content by default; use {includeContent: true} in
library/tool options or CLI --with-content. Content appears once in review JSON.
Events support a free-text channel (trimmed, lowercased, at most 40 characters),
parentRef, parentEventId, and a root threadRef. Journal, timeline, moments,
and open commitment reviews accept a channel filter. Set event attributes with
write.setAttribute(db, {kind: "event", eventId}, name, value); unset and replace
use the same target and validation as contact/link attributes.
Read exact events with read.getEventById(db, eventId) or
read.getEventByExternalRef(db, source, id). read.searchEvents searches folded
summary, content, reference and channel text. The search tool's scope is
contacts by default, with additive events and all modes.
read.getEventThread(db, source, id) returns a root and its descendants in time
order, including parents imported after their children.
write.reviseEvent(db, eventIdOrTarget, patch) accepts summary, content,
occurredAt, significance, channel, type within the same event family, and an
externalRef only when absent. write.deleteEvent(db, eventIdOrTarget, {confirm: true}) removes evidence, participants, attributes and link effects,
then replays surviving effects and refreshes rollups. Targets can also be
{ref: {source, id}}. Existing 0.5.x databases migrate through
initAffinityTables. Historical affinity/trust/rank baselines are recovered
from saved deltas; historical cadence and state before evidence were not stored
and use the default cadence and active state during replay.
export GHOSTPAW_AFFINITY_DB=~/crm/affinity.db # or --db <path>
ghostpaw-affinity contact create '{"name":"Fox","kind":"human","bootstrapOwner":true}'
ghostpaw-affinity search "jana" # names, identities, tags, attributes, roles, events
ghostpaw-affinity contact create '{"name":"Jana Petersen","kind":"human"}'
ghostpaw-affinity identity add --contact '{"contactId":2}' --input '{"type":"email","value":"jana@example.com"}'
ghostpaw-affinity event record --kind interaction --input '{"type":"conversation","occurredAt":"today","summary":"Call re pilot","significance":6,"participants":[{"contactId":2,"role":"recipient","directionality":"mutual"}]}'
ghostpaw-affinity commitment record --input '{"commitmentType":"promise","occurredAt":"today","dueAt":"+3d","summary":"Send proposal","significance":6,"participants":[{"contactId":2,"role":"recipient"}]}'
ghostpaw-affinity attribute set --target '{"kind":"contact","contact":{"contactId":2}}' --name tag:gastro --value null
ghostpaw-affinity review contacts.list --tag gastro
ghostpaw-affinity review contacts.list --attribute pipeline_stage=proposal
ghostpaw-affinity review commitments.open # overdue promises are flagged
ghostpaw-affinity review links.radar # who is drifting, with reasons
ghostpaw-affinity show contact_profile --contact '{"contactId":2}'
ghostpaw-affinity contact export --contact '{"contactId":2}' # omit --contact for the whole database
ghostpaw-affinity contact delete --contact '{"contactId":2}' --confirm
ghostpaw-affinity skill prints the full cheat sheet; skill --list and
skill <guide> print the workflow guides.
ghostpaw-affinity search <query> [--tag T] [--attribute name[=value]]
ghostpaw-affinity review <view> [--tag T] [--attribute name[=value]]
ghostpaw-affinity show <owner_profile|contact_profile|link> [--contact JSON --link JSON]
ghostpaw-affinity contact create|revise|lifecycle|export|delete|merge
ghostpaw-affinity identity add|revise|verify|remove [flags]
ghostpaw-affinity relationship add|bond|state|tie|untie [flags]
ghostpaw-affinity commitment record|resolve [flags]
ghostpaw-affinity attribute set|unset|replace [flags]
ghostpaw-affinity date add|revise|remove [flags]
ghostpaw-affinity event record [flags]
ghostpaw-affinity show event --event-id 12
ghostpaw-affinity show event --ref-source bluesky --ref-id at://record
ghostpaw-affinity search --scope events "text or record id"
ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}' --with-content --channel bluesky
ghostpaw-affinity review events.thread --ref-source bluesky --ref-id at://root
ghostpaw-affinity event revise --event-id 12 --patch '{"summary":"Corrected"}'
ghostpaw-affinity event delete --event-id 12 --confirm
ghostpaw-affinity duplicate dismiss|undismiss [flags]
ghostpaw-affinity mcp|skill|tools|describe|call|completions|version|help
There are 29 domain commands. review accepts the existing view names,
including contacts.list, contacts.duplicates and dates.upcoming.
show selects an inspection kind; contact and link locators are JSON objects.
Use help contact or contact create --help for schema-derived help; a
mistyped command suggests the closest one. Nested payloads use JSON or @path
to read a file; repeated array flags accept JSON objects
(--entries '{"name":"a","value":"x"}') or one JSON array.
Every time field accepts epoch milliseconds, ISO 8601 with Z or an offset,
YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday and signed day offsets
such as +3d or -2d, with the same grammar in CLI, MCP and direct tool calls.
Date-only values preserve their calendar day: timestamps use UTC midnight, or
23:59:59.999Z for dueAt. Keywords first resolve against the process local
calendar date; this is the only time-zone-dependent parsing step. occurredOn
and dueOn retain the input day (the written offset day for ISO inputs; UTC day
for epoch inputs). Existing rows derive these fields from their UTC timestamps.
Review rows for dates include occursOn, the anchor occurrence day; the library
keeps its numeric occursOn sorting key and adds occursOnDate.
The existing *Iso fields remain UTC. Overdue commitments compare the local
date to dueOn. Future events carry a future_event warning and do not count
as contact until their timestamp and day arrive. Commitments and their
resolutions do not reset recency or cadence. Radar marks drift after a gap
exceeds 1.5 times the expected cadence and explains its reasons in the snippet.
Global flags: --db <path>, --json, --quiet, --compact,
--help/-h, --version/-V/-v, --no-color.
NO_COLOR is accepted. In a terminal, commands print a readable summary with
names and dates. With --json or piped stdout they print the tool result as
JSON with ISO date siblings and floats rounded to three decimals. Journal review rows
keep one summary and compact {id,name,role} participants. Radar rows keep one link
id, counterparty name, reasons and rounded scores; upcoming link dates omit radar
scores. Default piped writes and --compact keep primary and changed ids plus
warnings; explicit --json retains full mutation detail. --compact
removes indentation and empty receipt arrays. --quiet suppresses stdout.
Writes list what changed in data.changes, and creates warn about likely
duplicates. Kebab-case flags map to schema properties. -- ends flags.
A positional -, or piped stdin when no other arguments are given, reads a
JSON object (echo '{"name":"Sam","kind":"human"}' | ghostpaw-affinity contact create).
Explicit flags override stdin fields.
Database selection uses --db, GHOSTPAW_AFFINITY_DB, then
~/.ghostpaw/affinity.db; relative paths resolve against the current
directory, and --help shows the resolved path. A new database is reported on
stderr. These commands work offline.
| Exit | Result |
|---|---|
| 0 | Success, including no_op |
| 1 | Other tool error |
| 2 | Usage, schema or invalid input |
| 3 | Clarification needed or not found |
| 4 | Conflict, already resolved or version mismatch |
| 70 | Internal error |
Errors go to stderr. With --json, errors also print to stdout.
--version names the variant: affinity 0.5.0 (binary, linux-x64),
(npm) or (library). Use --version --short in scripts for exact semver
matching. --version --json returns name, version, variant and target
(the target is null outside a binary). version --short and version --json
are equivalent.
Read commands (search, show, review and contact export, including direct tool
calls) exit 3 on a missing database without creating files:
Database not found: <path> (it is created by the first write).
The first write creates the database and reports created <path> on stderr.
The MCP server continues to create its database on startup.
String-valued flags such as --value Hamburg, --bond 'Warm lead' and
--reason 'Different people' accept plain strings or JSON strings/null.
Object and array flags still require JSON. Tag filters accept both
--tag gastro-radar and --tag tag:gastro-radar.
Per-command --help lists the fields used by that action.
Legacy tools [--json], describe <tool>,
call <tool> [JSON|-] and <tool_name> --parameter value remain available.
completions bash|zsh|fish prints shell scripts.
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 profile = read.getContactProfile(db, ada.id);
const radar = read.listRadar(db);
The live demo runs the library in your browser against an in-memory SQLite database. Each view below opens a different part of it:
Eight concepts, strict separation of concerns:
| Concept | Purpose |
|---|---|
| Contact | Any tracked entity: person, company, team, service, pet, household, or group |
| Identity | Exact recognition and routing handle such as email, phone, URL, or account id |
| Social Link | A live relationship track with Rank, Affinity, Trust, cadence, Bond, and state |
| Tie | A structural fact such as works_at, belongs_to, reports_to, or kinship |
| Journal Entry | Recorded evidence: interaction, observation, milestone, transaction, or commitment |
| Date Anchor | A recurring yearly date such as a birthday, anniversary, renewal, or memorial |
| Attribute | Flexible metadata and preference layer for contacts and links |
| Affinity Chart | Derived graph view of the active relationship network |
The model means each kind of truth has its own home:
| What it looks like | What it actually is |
|---|---|
| A person, company, team, service, or household | A Contact |
| An email address or handle | An Identity |
| A friendship or business relationship that changes over time | A Social Link |
| Employment, hierarchy, family, or membership | A Tie |
| “We talked yesterday” | A Journal entry |
| “I promised to send the recap” | A commitment-shaped Journal entry |
| “Their birthday is important every year” | A Date Anchor |
State is derived, not hand-toggled. Rank, hidden Affinity, Trust movement, Moments, drift, readiness, and graph bridge significance are computed from evidence and explicit structural truth, not caller-written status flags.
Use the read and write namespaces for direct-code access to the domain:
import { read, write } from "@ghostpaw/affinity";
write.createContact(db, { name: "Ada", kind: "human" });
write.addIdentity(db, contactId, { type: "email", value: "ada@example.com" });
write.recordInteraction(db, {
type: "support",
occurredAt: now,
summary: "Helped prepare for a difficult meeting",
significance: 7,
participants: [
{ contactId: ownerId, role: "actor", directionality: "mutual" },
{ contactId, role: "recipient", directionality: "mutual" },
],
});
const profile = read.getContactProfile(db, contactId);
const radar = read.listRadar(db);
See HUMAN.md for the full direct-code guide with modeling boundaries and worked examples.
Use the tools, skills, and soul namespaces for a structured runtime
surface designed to minimize LLM cognitive load:
import { tools, skills, soul } from "@ghostpaw/affinity";
const allTools = tools.affinityTools;
const searchTool = tools.getAffinityToolByName("search_affinity")!;
const result = searchTool.handler(db, { query: "email:ada@example.com" });
const allSkills = skills.affinitySkills;
const prompt = soul.renderAffinitySoulPromptFoundation();
Every tool returns a discriminated result with outcome: "success" | "no_op" | "needs_clarification" | "error", structured entities, next-step hints, and
actionable recovery signals.
See LLM.md for the full AI-facing guide covering soul, tools, and skills.
Twelve tools shaped around operator intent, not raw storage operations:
| Tool | What it does |
|---|---|
search_affinity | Search contacts by natural key or name |
review_affinity | Dashboard-style list and graph review views |
inspect_affinity_item | Open one exact profile or link detail |
manage_contact | Create, revise, or change contact lifecycle |
merge_contacts | Merge duplicate contacts safely |
manage_duplicate_dismissal | Dismiss or restore a suspected duplicate pair |
manage_identity | Add, revise, verify, or remove identities |
manage_relationship | Seed social links, revise bond/state, manage structural ties |
record_event | Record direct evidence, observations, milestones, or transactions |
manage_commitment | Record or resolve obligations |
manage_date_anchor | Add, revise, or remove recurring important dates |
manage_attribute | Set, unset, or replace contact/link metadata |
Each tool exports runtime metadata, JSON-Schema-compatible inputs, and structured outputs so harnesses can wire them without parsing vague prose.
node:sqlite (built into Node 24+).dist/. No subpath
exports, no code splitting.DatabaseSync.createContact, setStructuralTie,
recordInteraction, recordCommitment, addDateAnchor, mergeContacts:
operations that say what happened or what truth changed, not generic CRUD.soul for posture, tools for actions, skills for
workflow guidance..test.ts file.
The documented behavior is backed by executable coverage.import {
initAffinityTables, // schema setup
read, // all query functions
write, // all mutation functions
tools, // LLM tool definitions + registry
skills, // LLM workflow skills + registry
soul, // thinking foundation for system prompts
} from "@ghostpaw/affinity";
All domain and runtime types are also available at the root for TypeScript consumers:
import type {
AffinityDb,
ContactKind,
LinkState,
ContactProfileRecord,
AffinityChartRecord,
AffinityToolDefinition,
AffinitySkill,
AffinitySoul,
} from "@ghostpaw/affinity";
| Document | Audience |
|---|---|
| HUMAN.md | Human developers using the low-level read / write API |
| LLM.md | Agent builders wiring soul, tools, and skills into LLM systems |
| docs/README.md | Architecture overview: model, invariants, mechanics, and source layout |
| docs/entities/ | Per-concept manuals with exact inlined public API listings |
| docs/USECASES.md | Long-form lifecycle narratives across personal, business, and AI-memory angles |
npm install
npm test # node:test runner
npm run typecheck # tsc --noEmit
npm run lint # biome check
npm run build # ESM + CJS + declarations via tsup
The repo is pinned to Node 24.14.0 via .nvmrc / .node-version /
.tool-versions / mise.toml / Volta. Use whichever version manager you
prefer.
If this package helps your project, consider sponsoring its maintenance:
Contact creation accepts identities and ifExists: "error" | "return" in the
library and tools. Identity ownership conflicts fail atomically (CLI exit 4),
including data.existingContactId and error.details.existingContactId in tool
results. contact create '<JSON>' --if-exists return returns the existing contact
with outcome: "no_op" and matchedIdentities; it attaches nothing, including
other free identities. Name-only duplicates still receive a possible-duplicate
warning. Links can opt into input.ifExists: "return" when seeding repeatedly. Identity
addition supports the same opt-in policy for an identity already on that contact;
another contact still conflicts. Importers add changed handles separately after
contact creation.
Handle identities strip a leading @ and lowercase on add, revise and lookup.
Bluesky handles must be domain-like, X/Twitter handles have 1–15 letters, digits
or underscores, and Mastodon handles use user@host.tld. Use native did (alias
atproto_did) for stable AT Protocol identities. Locators also find legacy
custom:atproto-did rows. Keep former handles as additional identities.
search @handle works because CLI values are literal.
Only the JSON payload arguments (--input, the positional <input>, --patch
and the call argument) read files. Their help line ends with
(@file, @- stdin, @@ literal @): @path reads the file as UTF-8 relative to
the current directory, @- reads stdin, and @@text passes the literal
@text. An empty path (@) or an unreadable file exits 2. Every other value,
including names, notes, attribute values and search queries, is taken byte for
byte, even with a leading @ or @@.
For large payloads use stdin or @file to avoid the operating system's argv
size limit (E2BIG). JSON flag values support - too:
ghostpaw-affinity event record --kind interaction --input - < event.json
ghostpaw-affinity event record --kind interaction --input @event.json
ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}' --event-types '["conversation","observation"]'
Array flags accept JSON arrays, comma lists, or repeated flags. Unknown event
types fail with exit 2 and allowed values. SQLite timestamps such as
2026-10-27 17:51:33 use UTC; explicit offsets are honored. Sub-millisecond
fractions are truncated to milliseconds. CLI/MCP and tool intake preserve
high-precision occurredAt/dueAt strings in provenance.rawOccurredAt/rawDueAt.
Inspect an event to read its provenance. Null occurredAt is invalid.
Record likes, reposts and follows as observations with affectsBond: false.
Labels beginning with like/liked, follow/followed/following or repost/reposted
(case insensitive, followed by a word boundary, hyphen or underscore) default to
false for activity, milestone and observation events (except “follow up”). Observations with significance <= 2 also default to
false; other events default to true. Explicit affectsBond:true opts in. These ambient records stay in the journal without changing affinity or
recency. provenance.ambient: true also suppresses bond effects. Explicitly
outbound-only evidence contributes much less and does not reset radar recency;
without reciprocal contact it generates no routine check-in or drift prompt.
Include the owner explicitly with the correct actor/recipient role and use
owner_initiated, other_initiated, or mutual directionality. Observations and
follow-like milestones never derive breakthroughs; low-significance milestones
do not derive moments. An explicit momentKind is honored.
List reviews default to 50 rows, accept --limit (up to 10,000) and --offset,
and include total and hasMore. Follow hasMore rather than assuming the
first page is complete.
Thread reviews honor --event-types before paging. --with-content includes
verbatim text once in CLI JSON; --include-content remains a deprecated alias.
Duplicate reviews default to --min-score 0.5; pass --min-score 0 for weaker
candidates. Library duplicate candidate reads retain their existing default.
Event attribute tool targets accept {kind:"event",ref:{source:"bsky",id:"uri"}}
as an alternative to {kind:"event",eventId:1}. Setting an unchanged attribute
returns no_op without changing its timestamp; batch retries count as noOp.
Exports resolve parent references to parentEventId, including parents imported
later. Existing stored IDs and external-reference fields remain available.
batch reads one tool invocation per JSONL line, with one transaction for the
whole input by default, or one per --chunk N lines. Each line has its own
savepoint: failed lines roll back and later lines continue. --stop-on-error
stops at the first failure. Results are JSONL {line, ok, outcome, id, error?}
plus {done, created, noOp, failed}; created counts successful non-no-op lines.
Exit status is 0 for success, 1 for failures, or 2 if a line is invalid JSON.
Committed chunks remain committed if a later chunk fails.
{"tool":"manage_contact","args":{"action":"create","input":{"name":"Owner","kind":"human","bootstrapOwner":true,"ifExists":"return","identities":[{"type":"did","value":"did:plc:owner"}]}}}
{"op":"event","kind":"observation","input":{"occurredAt":"2026-10-27","content":"","significance":1,"affectsBond":false,"externalRef":{"source":"bluesky","id":"at://example/like/1"},"participants":[{"contactId":1,"role":"actor"}]}}
ghostpaw-affinity batch --chunk 500 < import.jsonl
node scripts/examples/import_social_jsonl.mjs scripts/examples/fixtures/social_sample.json | ghostpaw-affinity --db /tmp/social-demo.db batch
node scripts/examples/import_dry_run.mjs
The dependency-free example consumes a JSON export shaped as accounts, posts, interactions and follows; it never opens the source database. It resolves handle aliases to DIDs, deduplicates record URIs, keeps content verbatim, sets channel, parent references and directionality, and uses first_seen for undated follows or counts a skipped timestamp. The dry run checks two imports, DID uniqueness and byte-identical content, and reports journal JSON bytes versus raw text bytes. Examples and synthetic fixtures are development files, outside the npm package.
ghostpaw-affinity skill --short prints the compact command reference without
frontmatter or an appendix. skill --list lists guides; skill <guide> reads one.
FAQs
Relationship intelligence and social CRM model for Node.js — SQLite, zero dependencies, with a built-in LLM tool facade
The npm package @ghostpaw/affinity receives a total of 0 weekly downloads. As such, @ghostpaw/affinity popularity was classified as not popular.
We found that @ghostpaw/affinity demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Threat Research
/Security News
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.

Threat Research
Malicious Firefox extensions mimic Phantom, Jupiter, and Bitget wallets to steal recovery phrases and private keys, with one fetching its payload remotely.

Product
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.