GhostPaw Affinity

io.github.GhostPawJS由 GitHub 帳號 GhostPawJS 發布v0.7.0更新於 Nov 5, 2026

Local relationship model and social CRM for contacts, relationships, evidence, and commitments.

已驗證STDIO僅桌面效率與工作流知識與記憶

概覽

AI 產生的概覽

本機社交 CRM,讓助理在單一 SQLite 檔案中管理聯絡人、關係、日誌事件、承諾與週期性日期。

可以做什麼
  • 搜尋並檢視聯絡人、日誌、關係佇列、承諾、日期與圖譜狀態
  • 檢視單一檔案、關係或項目的詳細內容
  • 建立、修改、匯出、軟刪除或永久刪除聯絡人,並合併重複聯絡人
  • 管理身分資訊(電子郵件、電話、帳號、網址)、社交關係、連結強度與結構性關係
  • 記錄、修改與刪除事件、承諾及週期性日期錨點
  • 為聯絡人、關係與事件設定或取消自訂屬性
適用情境
適合希望助理在本機私密記錄人物、欠下哪些承諾、以及多久沒聯絡,並能跨工作階段查詢這些資訊的情境。偏向個人或小型關係維護,而非團隊共用的 CRM。
執行需求
以本機 stdio 程序在桌面環境執行;需安裝 ghostpaw-affinity 執行檔,或使用需要 Node 24+ 的 npm 套件。資料存於 SQLite 檔案,路徑由環境變數 GHOSTPAW_AFFINITY_DB 指定(預設 ~/.ghostpaw/affinity.db)。未宣告需要帳號或 API 金鑰。
安裝前請注意
會在本機資料庫檔案中保存真實人物的個人資料,包括電子郵件、電話與帳號,需妥善保護並備份。寫入操作會變更或刪除資料:確認後刪除聯絡人為永久性,未指定聯絡人匯出會匯出整個資料庫。合併聯絡人與刪除事件會重新計算關係資料。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 GhostPaw Affinity,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

[GhostPaw affinity: Who have you been meaning to call?]

@ghostpaw/affinity

[npm] [CI] [site] [runtime deps] [MCP Registry] [node] [license]

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.

Install

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

sh
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

sh
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

sh
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

sh
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)

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):

sh
V=0.6.0T=linux-x64F=ghostpaw-affinity-$V-$T.tar.gzU=https://github.com/GhostPawJS/affinity/releases/download/v$Vcurl -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:

sh
ghostpaw-affinity contact create '{"name":"Sam","kind":"human"}'ghostpaw-affinity search samghostpaw-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:

[ghostpaw-affinity session: version, contact create, a tag, an event and a promise with relative dates, search, an overdue promise in review commitments.open, and the radar]

The same session as an animated GIF.

npx / npm (for npm-based environments)

The npm package needs Node 24+ and has zero runtime dependencies:

sh
npx -y @ghostpaw/affinity --helpnpm install -g @ghostpaw/affinity   # provides ghostpaw-affinity

Library (for harnesses)

sh
npm install @ghostpaw/affinity
ts
import { DatabaseSync } from "node:sqlite";import { initAffinityTables, read, write, tools } from "@ghostpaw/affinity";

See Quick Start and docs/LLM.md for the tool facade.

Other installers and verification

Interactive installers (also verify SHA256SUMS):

sh
curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh | sh
powershell
irm https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.ps1 | iex
sh
mise use -g github:GhostPawJS/affinityubi --project GhostPawJS/affinity --exe ghostpaw-affinity --in ~/.local/bineget GhostPawJS/affinity

For aqua, define a github_release package in aqua.yaml:

yaml
registries:  - name: affinity    type: local    path: affinity-registry.yamlpackages:  - name: GhostPawJS/affinity@v<VERSION>    registry: affinity

affinity-registry.yaml:

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:

sh
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.

MCP server

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

bash
claude mcp add ghostpaw-affinity -- ghostpaw-affinity mcp

or as a plugin, which also installs the skill:

bash
/plugin marketplace add GhostPawJS/affinity/plugin install ghostpaw-affinity@ghostpaw-affinity

Codex CLI (~/.codex/config.toml)

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:

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.

Agent Skill

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:

bash
ghostpaw-affinity skill > SKILL.mdghostpaw-affinity skill --list       # workflow guidesghostpaw-affinity skill bootstrap-and-cold-start

Events

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.

CLI

sh
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, eventsghostpaw-affinity contact create '{"name":"Jana Petersen","kind":"human"}'ghostpaw-affinity identity add --contact '{"contactId":2}' --input '{"type":"email","value":"[email protected]"}'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 nullghostpaw-affinity review contacts.list --tag gastroghostpaw-affinity review contacts.list --attribute pipeline_stage=proposalghostpaw-affinity review commitments.open            # overdue promises are flaggedghostpaw-affinity review links.radar                 # who is drifting, with reasonsghostpaw-affinity show contact_profile --contact '{"contactId":2}'ghostpaw-affinity contact export --contact '{"contactId":2}'   # omit --contact for the whole databaseghostpaw-affinity contact delete --contact '{"contactId":2}' --confirm

ghostpaw-affinity skill prints the full cheat sheet; skill --list and skill <guide> print the workflow guides.

text
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|mergeghostpaw-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 12ghostpaw-affinity show event --ref-source bluesky --ref-id at://recordghostpaw-affinity search --scope events "text or record id"ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}' --with-content --channel blueskyghostpaw-affinity review events.thread --ref-source bluesky --ref-id at://rootghostpaw-affinity event revise --event-id 12 --patch '{"summary":"Corrected"}'ghostpaw-affinity event delete --event-id 12 --confirmghostpaw-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. When a command opens the default database, the CLI prints using default db <path> to stderr. Suppress this notice with --quiet or GHOSTPAW_QUIET=1. 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.

ExitResult
0Success, including no_op
1Other tool error
2Usage, schema or invalid input
3Clarification needed or not found
4Conflict, already resolved or version mismatch
70Internal 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.

Quick Start

ts
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: "[email protected]",});
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);

Docs

The live demo runs the library in your browser against an in-memory SQLite database. Each view below opens a different part of it:

The Model

Eight concepts, strict separation of concerns:

ConceptPurpose
ContactAny tracked entity: person, company, team, service, pet, household, or group
IdentityExact recognition and routing handle such as email, phone, URL, or account id
Social LinkA live relationship track with Rank, Affinity, Trust, cadence, Bond, and state
TieA structural fact such as works_at, belongs_to, reports_to, or kinship
Journal EntryRecorded evidence: interaction, observation, milestone, transaction, or commitment
Date AnchorA recurring yearly date such as a birthday, anniversary, renewal, or memorial
AttributeFlexible metadata and preference layer for contacts and links
Affinity ChartDerived graph view of the active relationship network

The model means each kind of truth has its own home:

What it looks likeWhat it actually is
A person, company, team, service, or householdA Contact
An email address or handleAn Identity
A friendship or business relationship that changes over timeA Social Link
Employment, hierarchy, family, or membershipA 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.

Two Audiences

Human developers

Use the read and write namespaces for direct-code access to the domain:

ts
import { read, write } from "@ghostpaw/affinity";
write.createContact(db, { name: "Ada", kind: "human" });write.addIdentity(db, contactId, { type: "email", value: "[email protected]" });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.

LLM agents

Use the tools, skills, and soul namespaces for a structured runtime surface designed to minimize LLM cognitive load:

ts
import { tools, skills, soul } from "@ghostpaw/affinity";
const allTools = tools.affinityTools;const searchTool = tools.getAffinityToolByName("search_affinity")!;const result = searchTool.handler(db, { query: "email:[email protected]" });
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.

Tools

Twelve tools shaped around operator intent, not raw storage operations:

ToolWhat it does
search_affinitySearch contacts by natural key or name
review_affinityDashboard-style list and graph review views
inspect_affinity_itemOpen one exact profile or link detail
manage_contactCreate, revise, or change contact lifecycle
merge_contactsMerge duplicate contacts safely
manage_duplicate_dismissalDismiss or restore a suspected duplicate pair
manage_identityAdd, revise, verify, or remove identities
manage_relationshipSeed social links, revise bond/state, manage structural ties
record_eventRecord direct evidence, observations, milestones, or transactions
manage_commitmentRecord or resolve obligations
manage_date_anchorAdd, revise, or remove recurring important dates
manage_attributeSet, 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.

Key Properties

  • Zero runtime dependencies. Only node:sqlite (built into Node 24+).
  • Single prebundled blob. One ESM + one CJS entry in dist/. No subpath exports, no code splitting.
  • Pure SQLite storage. CHECK-constrained state, trigger-maintained support tables, and deterministic derived reads. Bring your own DatabaseSync.
  • Derived mechanics. Rank, hidden Affinity, Trust, cadence, Moments, Radar, and bridge significance are computed from evidence and structure, never directly caller-set.
  • Intention-shaped writes. createContact, setStructuralTie, recordInteraction, recordCommitment, addDateAnchor, mergeContacts: operations that say what happened or what truth changed, not generic CRUD.
  • Additive AI runtime. soul for posture, tools for actions, skills for workflow guidance.
  • Colocated tests. Every non-type module has a colocated .test.ts file. The documented behavior is backed by executable coverage.

Package Surface

ts
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:

ts
import type {  AffinityDb,  ContactKind,  LinkState,  ContactProfileRecord,  AffinityChartRecord,  AffinityToolDefinition,  AffinitySkill,  AffinitySoul,} from "@ghostpaw/affinity";

Documentation

DocumentAudience
HUMAN.mdHuman developers using the low-level read / write API
LLM.mdAgent builders wiring soul, tools, and skills into LLM systems
docs/README.mdArchitecture overview: model, invariants, mechanics, and source layout
docs/entities/Per-concept manuals with exact inlined public API listings
docs/USECASES.mdLong-form lifecycle narratives across personal, business, and AI-memory angles

Development

bash
npm installnpm test            # node:test runnernpm run typecheck   # tsc --noEmitnpm run lint        # biome checknpm 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.

Support

If this package helps your project, consider sponsoring its maintenance:

[GitHub Sponsors]


Anonyfox • MIT License

Social imports and batch writes

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 [email protected]. 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:

sh
ghostpaw-affinity event record --kind interaction --input - < event.jsonghostpaw-affinity event record --kind interaction --input @event.jsonghostpaw-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.

jsonl
{"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"}]}}
sh
ghostpaw-affinity batch --chunk 500 < import.jsonlnode scripts/examples/import_social_jsonl.mjs scripts/examples/fixtures/social_sample.json | ghostpaw-affinity --db /tmp/social-demo.db batchnode 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.

Agent relationships

Contacts also support kind agent for a non-human operator or bot. Use a stable identity type custom:<system>-agent with the agent id as its value. Read ghostpaw-affinity skill operate-agent-relationships for the coaching workflow: record each 1:1 as an interaction, track agreed changes as agent-actor promises due at the next slot, and resolve kept or broken at verification. Track owner decisions without due dates on the owner-side contact. Scheduling stays in a task tool.

Pin a fixed rhythm with manage_relationship action seed_social_link, input: {kind: "professional", cadenceDays: 7}. Revise existing links with action revise, link: {linkId}, patch: {cadenceDays: 7}; cadenceDays: null unpins and restores derived cadence. Positive fractional days are accepted. When upgrading a database with the previous contact-kind constraint, initialize outside a caller transaction so SQLite can preserve foreign-key references while rebuilding the contacts table. CLI and MCP handle this during startup. Link results expose effective cadenceDays and cadencePinned. Contact profiles include open commitments with id (eventId), type, summary, due and overdue status. describe manage_relationship lists nested seed and revise fields.

Payload fields and examples are included in each command's --help. Inspect the JSON schema with ghostpaw-affinity describe event record (or describe "event record"), and select a variant with --kind interaction. describe <tool> retains its tool details. JSON patches and other object arguments are documented in command help; batch accepts JSONL {tool,args} or {op:"event",kind,input} records using the same tool schemas.

來源:README.md,提交 7098403

工具

13

search_affinity

Search contacts by name, identity, attributes, link roles, bonds and event text. All whitespace tokens must match. Dormant and lost contacts are excluded by default; use includeDormant and includeLost to include them.

輸入參數tztagkindlimitquery必填scopeoffsetattributeincludeLostincludeOwnerincludeDormantlifecycleState

review_affinity

Review one affinity surface such as contacts, journals, link queues, commitments, dates, graph state, or merge history.

輸入參數tznowreftagkindlinkview必填limitrefIdsinceuntiloffset另有 23 個

inspect_affinity_item

Inspect one exact affinity item such as the owner profile, a contact profile, or one link detail.

輸入參數tzrefkind必填linkrefIdcontacteventIdrefSourcetopLinksLimitrecentEventsLimit

manage_contact

Create, revise, export or permanently delete a contact (confirm: true required; owner cannot be deleted). Export without contact exports the whole database. Lifecycle lost is soft delete. ALWAYS search_affinity first before creating — duplicates corrupt the graph.

輸入參數tzinputpatchaction必填confirmcontactoptionslifecycleState

merge_contacts

Merge one loser contact into one winner contact using explicit contact locators.

輸入參數tznowloser必填winner必填reasonSummary

manage_duplicate_dismissal

Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.

輸入參數tzleft必填right必填action必填reason

manage_identity

Add, revise, verify, or remove one routing identity on a contact. Use this for emails, phone numbers, social handles, and URLs — anything that could locate a person. Do not use manage_attribute for these.

輸入參數tzinputpatchaction必填contactremovedAtidentityIdverifiedAt

manage_relationship

Seed social links, revise bond and state, and manage structural ties. Seed a link as soon as a relationship is mentioned — do not wait for repeated evidence.

輸入參數totzbondfromkindlinkroleinputpatchstateaction必填options另有 1 個

record_event

Record one social event such as an interaction, observation, milestone, or transaction. For interactions, milestones, and transactions the owner contact is auto-injected into participants if missing. Observations may omit the owner when recording third-party evidence.

輸入參數tzkind必填input必填

manage_commitment

Record or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.

輸入參數tzinputaction必填optionsresolutioncommitmentEventId

manage_date_anchor

Add, revise, or remove recurring date anchors on contacts or links. Use this for birthdays, anniversaries, and yearly events — not manage_attribute.

輸入參數tzinputpatchaction必填optionsremovedAtanchorEventId

manage_attribute

Set, unset, or replace custom metadata attributes on contacts, links and events. For emails, phones, handles, and URLs use manage_identity instead — those are routing keys, not attributes.

輸入參數tznamevalueaction必填targetentriesremovedAt

manage_event

Revise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.

輸入參數tzrefpatchrefIdaction必填confirmeventIdrefSource

版本歷史

4
  1. v0.7.0最新Nov 5, 2026
  2. v0.6.2Nov 5, 2026
  3. v0.6.1Oct 29, 2026
  4. v0.3.0Oct 25, 2026

評分

已滿足適用檢查項的 90%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分
  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分