@ghostpaw/affinity 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,117 +6,111 @@
6
6
 
7
7
  **Agent contacts: people, promises and yearly dates, with a radar for who is drifting.**
8
8
 
9
- ```bash
10
- npm install @ghostpaw/affinity
11
- ```
9
+ A local social CRM: contacts, identities, relationships, journal events,
10
+ commitments and recurring dates in one SQLite file. One self-contained binary
11
+ per platform, an MCP server, and a zero-dependency TypeScript library.
12
12
 
13
- A standalone social CRM core for Node.js, built on SQLite.
13
+ ## Install
14
14
 
15
- Affinity treats contacts, identities, relationship mechanics, structural ties,
16
- journal evidence, commitments, recurring dates, attributes, and graph review as
17
- one coherent model instead of separate systems. It ships as a single prebundled
18
- blob with zero runtime dependencies, designed for two audiences at once: human
19
- developers working directly in code, and LLM agents operating through a
20
- structured `soul` / `tools` / `skills` runtime.
15
+ Each line downloads the latest release for one platform, checks it against
16
+ `SHA256SUMS`, puts `ghostpaw-affinity` on your `PATH` and prints the version.
21
17
 
22
- ## MCP server
18
+ **Linux x64**
23
19
 
24
- Every client below starts the same stdio server: `npx -y @ghostpaw/affinity mcp`.
25
- Contacts live in `~/.ghostpaw/affinity.db` unless the `GHOSTPAW_AFFINITY_DB`
26
- environment variable (absolute path) or `--db <path>` says otherwise.
20
+ ```sh
21
+ 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
22
+ ```
27
23
 
28
- **Claude Code**
24
+ **Linux arm64**
29
25
 
30
- ```bash
31
- claude mcp add ghostpaw-affinity -- npx -y @ghostpaw/affinity mcp
26
+ ```sh
27
+ 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
32
28
  ```
33
29
 
34
- or as a plugin, which also installs the skill:
30
+ **macOS Apple silicon**
35
31
 
36
- ```bash
37
- /plugin marketplace add GhostPawJS/affinity
38
- /plugin install ghostpaw-affinity@ghostpaw-affinity
32
+ ```sh
33
+ 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
39
34
  ```
40
35
 
41
- **Codex CLI** (`~/.affinity/config.toml`)
36
+ **macOS Intel**
42
37
 
43
- ```toml
44
- [mcp_servers.ghostpaw-affinity]
45
- command = "npx"
46
- args = ["-y", "@ghostpaw/affinity", "mcp"]
38
+ ```sh
39
+ 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
47
40
  ```
48
41
 
49
- **Cursor** (`.cursor/mcp.json`), **Claude Desktop** (`claude_desktop_config.json`)
50
- or a project `.mcp.json`:
42
+ **Windows x64 (PowerShell)**
51
43
 
52
- ```json
53
- {
54
- "mcpServers": {
55
- "ghostpaw-affinity": {
56
- "command": "npx",
57
- "args": ["-y", "@ghostpaw/affinity", "mcp"]
58
- }
59
- }
60
- }
44
+ ```powershell
45
+ $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
61
46
  ```
62
47
 
63
- Claude Desktop users can also install `server.mcpb` from the
64
- [latest GitHub release](https://github.com/GhostPawJS/affinity/releases/latest).
65
- A Dockerfile is included for container hosts:
66
- `docker build -t ghostpaw-affinity . && docker run -i --rm -v affinity:/data ghostpaw-affinity`.
67
-
68
- The server speaks MCP 2026-07-28 (stateless, `server/discover`) and the
69
- initialize-based revisions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05.
48
+ 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).
70
49
 
71
- ### CLI
50
+ 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`):
72
51
 
73
- ```bash
74
- npx -y @ghostpaw/affinity tools
75
- npx -y @ghostpaw/affinity call manage_contact '{"action":"create","input":{"name":"Sam","kind":"human"}}'
76
- npx -y @ghostpaw/affinity call search_affinity '{"query":"Sam"}'
52
+ ```sh
53
+ V=0.6.0
54
+ T=linux-x64
55
+ F=ghostpaw-affinity-$V-$T.tar.gz
56
+ U=https://github.com/GhostPawJS/affinity/releases/download/v$V
57
+ curl -fsSL -O "$U/$F" -O "$U/SHA256SUMS" &&
58
+ { case "$T" in darwin-*) grep " $F\$" SHA256SUMS | shasum -a 256 -c - ;; *) grep " $F\$" SHA256SUMS | sha256sum -c - ;; esac; } &&
59
+ tar -xzf "$F" ghostpaw-affinity && ./ghostpaw-affinity --version
77
60
  ```
78
61
 
79
- Install globally for the `ghostpaw-affinity` binary: `npm i -g @ghostpaw/affinity`.
80
- `ghostpaw-affinity mcp` starts the stdio server.
62
+ Then:
81
63
 
82
- ### Agent Skill
64
+ ```sh
65
+ ghostpaw-affinity contact create '{"name":"Sam","kind":"human"}'
66
+ ghostpaw-affinity search sam
67
+ ghostpaw-affinity skill # cheat sheet and tool reference
68
+ ```
83
69
 
84
- `SKILL.md` (repo root) and `skills/affinity/SKILL.md` follow the
85
- [Agent Skills](https://agentskills.io) format and ship in the npm package.
86
- Copy `skills/affinity/` into your agent's skills folder, or print it:
70
+ Contacts live in `~/.ghostpaw/affinity.db` unless `--db <path>` or the
71
+ `GHOSTPAW_AFFINITY_DB` environment variable says otherwise; relative paths
72
+ resolve against the current directory.
87
73
 
88
- ```bash
89
- npx -y @ghostpaw/affinity skill > SKILL.md
90
- ```
74
+ A session with the linux-x64 binary against a fresh database:
91
75
 
92
- ## Install
76
+ ![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](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.png)
77
+
78
+ The same session as an [animated GIF](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.gif).
93
79
 
80
+ ### npx / npm (for npm-based environments)
94
81
 
95
- The [npm package](https://www.npmjs.com/package/@ghostpaw/affinity) requires Node 24+:
82
+ The [npm package](https://www.npmjs.com/package/@ghostpaw/affinity) needs Node 24+
83
+ and has zero runtime dependencies:
96
84
 
97
85
  ```sh
98
- npm install -g @ghostpaw/affinity
99
86
  npx -y @ghostpaw/affinity --help
87
+ npm install -g @ghostpaw/affinity # provides ghostpaw-affinity
100
88
  ```
101
89
 
102
- Standalone binaries are available from [GitHub Releases](https://github.com/GhostPawJS/affinity/releases)
103
- for linux-x64, linux-arm64, darwin-arm64, darwin-x64 and windows-x64.
90
+ ### Library (for harnesses)
104
91
 
105
92
  ```sh
106
- curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh | sh
93
+ npm install @ghostpaw/affinity
107
94
  ```
108
95
 
109
- ```powershell
110
- irm https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.ps1 | iex
96
+ ```ts
97
+ import { DatabaseSync } from "node:sqlite";
98
+ import { initAffinityTables, read, write, tools } from "@ghostpaw/affinity";
111
99
  ```
112
100
 
113
- A session with the 0.4.0 linux-x64 binary against a fresh database (output piped through `jq` to keep it short):
101
+ See [Quick Start](#quick-start) and [docs/LLM.md](docs/LLM.md) for the tool facade.
114
102
 
115
- ![ghostpaw-affinity 0.4.0: version, contact create, identity add, event and commitment record, search, review commitments.open and show contact_profile](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.png)
103
+ ### Other installers and verification
116
104
 
117
- The same session as an [animated GIF](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.gif).
105
+ Interactive installers (also verify `SHA256SUMS`):
106
+
107
+ ```sh
108
+ curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh | sh
109
+ ```
118
110
 
119
- Other release installers:
111
+ ```powershell
112
+ irm https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.ps1 | iex
113
+ ```
120
114
 
121
115
  ```sh
122
116
  mise use -g github:GhostPawJS/affinity
@@ -155,8 +149,8 @@ packages:
155
149
  - name: ghostpaw-affinity
156
150
  ```
157
151
 
158
- Install scripts verify SHA256SUMS. To verify a downloaded archive's
159
- [build attestation](https://cli.github.com/manual/gh_attestation_verify):
152
+ `SHA256SUMS` covers every archive and `server.mcpb`. To verify a downloaded
153
+ archive's [build attestation](https://cli.github.com/manual/gh_attestation_verify):
160
154
 
161
155
  ```sh
162
156
  gh attestation verify <archive> -R GhostPawJS/affinity
@@ -169,80 +163,229 @@ download on macOS, remove quarantine with
169
163
  Linux binaries require glibc 2.28+; use npx on Alpine.
170
164
  See the [binary build workflow](.github/workflows/release-binaries.yml).
171
165
 
172
- Sizes for 0.4.0 (Node 26.12.1 inside each binary):
166
+ Each archive is roughly 40-46 MB (the binary embeds the Node runtime, 105-150 MB
167
+ unpacked); the npm package is about 2 MB unpacked. Every release, including the
168
+ one-liners above, is installed and run on ubuntu-24.04, macos-15 and
169
+ windows-2025 by the [install check](.github/workflows/install-check.yml).
173
170
 
174
- | Target | Archive | Binary |
175
- |---|---|---|
176
- | linux-x64 | 41.7 MB tar.gz | 117.6 MB |
177
- | linux-arm64 | 41.2 MB tar.gz | 115.3 MB |
178
- | darwin-arm64 | 44.9 MB tar.gz | 146.8 MB |
179
- | darwin-x64 | 46.4 MB tar.gz | 149.7 MB |
180
- | windows-x64 | 38.4 MB zip | 105.7 MB |
171
+ ## MCP server
181
172
 
182
- The npm package is 2003 KB unpacked and needs Node 24+. Every release is
183
- installed and run on ubuntu-24.04, macos-15 and windows-2025 by the
184
- [install check](.github/workflows/install-check.yml).
173
+ Every client below starts the same stdio server: `ghostpaw-affinity mcp`.
174
+ Without the binary, use `npx -y @ghostpaw/affinity mcp` as the command instead.
185
175
 
186
- The install check runs on ubuntu-24.04, macos-15 and windows-2025.
176
+ **Claude Code**
177
+
178
+ ```bash
179
+ claude mcp add ghostpaw-affinity -- ghostpaw-affinity mcp
180
+ ```
181
+
182
+ or as a plugin, which also installs the skill:
183
+
184
+ ```bash
185
+ /plugin marketplace add GhostPawJS/affinity
186
+ /plugin install ghostpaw-affinity@ghostpaw-affinity
187
+ ```
188
+
189
+ **Codex CLI** (`~/.codex/config.toml`)
190
+
191
+ ```toml
192
+ [mcp_servers.ghostpaw-affinity]
193
+ command = "ghostpaw-affinity"
194
+ args = ["mcp"]
195
+ ```
196
+
197
+ **Cursor** (`.cursor/mcp.json`), **Claude Desktop** (`claude_desktop_config.json`)
198
+ or a project `.mcp.json`:
199
+
200
+ ```json
201
+ {
202
+ "mcpServers": {
203
+ "ghostpaw-affinity": {
204
+ "command": "ghostpaw-affinity",
205
+ "args": ["mcp"]
206
+ }
207
+ }
208
+ }
209
+ ```
210
+
211
+ Claude Desktop users can also install `server.mcpb` from the
212
+ [latest GitHub release](https://github.com/GhostPawJS/affinity/releases/latest).
213
+ A Dockerfile is included for container hosts:
214
+ `docker build -t ghostpaw-affinity . && docker run -i --rm -v affinity:/data ghostpaw-affinity`.
215
+
216
+ The server speaks MCP 2026-07-28 (stateless, `server/discover`) and the
217
+ initialize-based revisions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05.
218
+
219
+ ### Agent Skill
220
+
221
+ `SKILL.md` (repo root) and `skills/affinity/SKILL.md` follow the
222
+ [Agent Skills](https://agentskills.io) format and ship in the npm package.
223
+ Copy `skills/affinity/` into your agent's skills folder, or print it:
224
+
225
+ ```bash
226
+ ghostpaw-affinity skill > SKILL.md
227
+ ghostpaw-affinity skill --list # workflow guides
228
+ ghostpaw-affinity skill bootstrap-and-cold-start
229
+ ```
230
+
231
+ ## Events
232
+
233
+ Record events and commitments with an optional `externalRef: {source, id}`.
234
+ Both values are trimmed and non-empty; source is lowercased. The pair is unique
235
+ across the database. Repeated intake returns the original event and `no_op`,
236
+ without creating rows or relationship effects.
237
+
238
+ `content` preserves text exactly, including whitespace, empty strings and NUL,
239
+ up to 1 MB (1,048,576 UTF-8 bytes). Supply a short `summary`, or omit it to derive
240
+ one from the first non-empty content line (at most 140 characters; empty text
241
+ becomes `(no text)`). Unknown event fields and moment kinds are validation errors.
242
+ Journal/timeline rows omit content by default; use `{includeContent: true}` in
243
+ library/tool options or CLI `--with-content`. Content appears once in review JSON.
244
+
245
+ Events support a free-text `channel` (trimmed, lowercased, at most 40 characters),
246
+ `parentRef`, `parentEventId`, and a root `threadRef`. Journal, timeline, moments,
247
+ and open commitment reviews accept a channel filter. Set event attributes with
248
+ `write.setAttribute(db, {kind: "event", eventId}, name, value)`; unset and replace
249
+ use the same target and validation as contact/link attributes.
250
+
251
+ Read exact events with `read.getEventById(db, eventId)` or
252
+ `read.getEventByExternalRef(db, source, id)`. `read.searchEvents` searches folded
253
+ summary, content, reference and channel text. The search tool's `scope` is
254
+ `contacts` by default, with additive `events` and `all` modes.
255
+ `read.getEventThread(db, source, id)` returns a root and its descendants in time
256
+ order, including parents imported after their children.
257
+
258
+ `write.reviseEvent(db, eventIdOrTarget, patch)` accepts summary, content,
259
+ occurredAt, significance, channel, type within the same event family, and an
260
+ externalRef only when absent. `write.deleteEvent(db, eventIdOrTarget,
261
+ {confirm: true})` removes evidence, participants, attributes and link effects,
262
+ then replays surviving effects and refreshes rollups. Targets can also be
263
+ `{ref: {source, id}}`. Existing 0.5.x databases migrate through
264
+ `initAffinityTables`. Historical affinity/trust/rank baselines are recovered
265
+ from saved deltas; historical cadence and state before evidence were not stored
266
+ and use the default cadence and active state during replay.
187
267
 
188
268
  ## CLI
189
269
 
270
+ ```sh
271
+ export GHOSTPAW_AFFINITY_DB=~/crm/affinity.db # or --db <path>
272
+ ghostpaw-affinity contact create '{"name":"Fox","kind":"human","bootstrapOwner":true}'
273
+ ghostpaw-affinity search "jana" # names, identities, tags, attributes, roles, events
274
+ ghostpaw-affinity contact create '{"name":"Jana Petersen","kind":"human"}'
275
+ ghostpaw-affinity identity add --contact '{"contactId":2}' --input '{"type":"email","value":"jana@example.com"}'
276
+ ghostpaw-affinity event record --kind interaction --input '{"type":"conversation","occurredAt":"today","summary":"Call re pilot","significance":6,"participants":[{"contactId":2,"role":"recipient","directionality":"mutual"}]}'
277
+ ghostpaw-affinity commitment record --input '{"commitmentType":"promise","occurredAt":"today","dueAt":"+3d","summary":"Send proposal","significance":6,"participants":[{"contactId":2,"role":"recipient"}]}'
278
+ ghostpaw-affinity attribute set --target '{"kind":"contact","contact":{"contactId":2}}' --name tag:gastro --value null
279
+ ghostpaw-affinity review contacts.list --tag gastro
280
+ ghostpaw-affinity review contacts.list --attribute pipeline_stage=proposal
281
+ ghostpaw-affinity review commitments.open # overdue promises are flagged
282
+ ghostpaw-affinity review links.radar # who is drifting, with reasons
283
+ ghostpaw-affinity show contact_profile --contact '{"contactId":2}'
284
+ ghostpaw-affinity contact export --contact '{"contactId":2}' # omit --contact for the whole database
285
+ ghostpaw-affinity contact delete --contact '{"contactId":2}' --confirm
286
+ ```
287
+
288
+ `ghostpaw-affinity skill` prints the full cheat sheet; `skill --list` and
289
+ `skill <guide>` print the workflow guides.
290
+
190
291
  ```text
191
- ghostpaw-affinity search <query>
192
- ghostpaw-affinity review <view>
292
+ ghostpaw-affinity search <query> [--tag T] [--attribute name[=value]]
293
+ ghostpaw-affinity review <view> [--tag T] [--attribute name[=value]]
193
294
  ghostpaw-affinity show <owner_profile|contact_profile|link> [--contact JSON --link JSON]
194
- ghostpaw-affinity contact create <input-json>
195
- ghostpaw-affinity contact revise <contact-json> --patch JSON
196
- ghostpaw-affinity contact lifecycle <contact-json> <lifecycleState>
197
- ghostpaw-affinity contact merge [flags]
295
+ ghostpaw-affinity contact create|revise|lifecycle|export|delete|merge
198
296
  ghostpaw-affinity identity add|revise|verify|remove [flags]
199
297
  ghostpaw-affinity relationship add|bond|state|tie|untie [flags]
200
298
  ghostpaw-affinity commitment record|resolve [flags]
201
299
  ghostpaw-affinity attribute set|unset|replace [flags]
202
300
  ghostpaw-affinity date add|revise|remove [flags]
203
301
  ghostpaw-affinity event record [flags]
302
+ ghostpaw-affinity show event --event-id 12
303
+ ghostpaw-affinity show event --ref-source bluesky --ref-id at://record
304
+ ghostpaw-affinity search --scope events "text or record id"
305
+ ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}' --with-content --channel bluesky
306
+ ghostpaw-affinity review events.thread --ref-source bluesky --ref-id at://root
307
+ ghostpaw-affinity event revise --event-id 12 --patch '{"summary":"Corrected"}'
308
+ ghostpaw-affinity event delete --event-id 12 --confirm
204
309
  ghostpaw-affinity duplicate dismiss|undismiss [flags]
205
310
  ghostpaw-affinity mcp|skill|tools|describe|call|completions|version|help
206
311
  ```
207
312
 
208
- There are 27 domain commands. `review` accepts the existing view names,
313
+ There are 29 domain commands. `review` accepts the existing view names,
209
314
  including `contacts.list`, `contacts.duplicates` and `dates.upcoming`.
210
315
  `show` selects an inspection kind; contact and link locators are JSON objects.
211
- Use `help contact` or `contact create --help` for schema-derived help.
212
- Nested payloads use JSON or `@path` to read a file.
316
+ Use `help contact` or `contact create --help` for schema-derived help; a
317
+ mistyped command suggests the closest one. Nested payloads use JSON or `@path`
318
+ to read a file; repeated array flags accept JSON objects
319
+ (`--entries '{"name":"a","value":"x"}'`) or one JSON array.
320
+
321
+ Every time field accepts epoch milliseconds, ISO 8601 with `Z` or an offset,
322
+ `YYYY-MM-DD`, `DD.MM.YYYY`, `today`, `tomorrow`, `yesterday` and signed day offsets
323
+ such as `+3d` or `-2d`, with the same grammar in CLI, MCP and direct tool calls.
324
+ Date-only values preserve their calendar day: timestamps use UTC midnight, or
325
+ 23:59:59.999Z for `dueAt`. Keywords first resolve against the process local
326
+ calendar date; this is the only time-zone-dependent parsing step. `occurredOn`
327
+ and `dueOn` retain the input day (the written offset day for ISO inputs; UTC day
328
+ for epoch inputs). Existing rows derive these fields from their UTC timestamps.
329
+ Review rows for dates include `occursOn`, the anchor occurrence day; the library
330
+ keeps its numeric `occursOn` sorting key and adds `occursOnDate`.
331
+ The existing `*Iso` fields remain UTC. Overdue commitments compare the local
332
+ date to `dueOn`. Future events carry a `future_event` warning and do not count
333
+ as contact until their timestamp and day arrive. Commitments and their
334
+ resolutions do not reset recency or cadence. Radar marks `drift` after a gap
335
+ exceeds 1.5 times the expected cadence and explains its reasons in the snippet.
213
336
 
214
337
  Global flags: `--db <path>`, `--json`, `--quiet`, `--compact`,
215
338
  `--help`/`-h`, `--version`/`-V`/`-v`, `--no-color`.
216
- `NO_COLOR` is accepted; output uses no terminal colors.
217
- Noun-verb commands print the full tool result as JSON with `--json` or piped
218
- stdout. Terminal output shows the summary and payload. `--quiet` suppresses
219
- stdout. `--compact` removes JSON indentation.
220
- Array flags repeat; kebab-case flags map to schema properties.
221
- `--` ends flags. A positional `-`, or piped stdin with no input flags, reads a
222
- JSON object. Explicit flags override stdin fields.
339
+ `NO_COLOR` is accepted. In a terminal, commands print a readable summary with
340
+ names and dates. With `--json` or piped stdout they print the tool result as
341
+ JSON with ISO date siblings and floats rounded to three decimals. Journal review rows
342
+ keep one summary and compact `{id,name,role}` participants. Radar rows keep one link
343
+ id, counterparty name, reasons and rounded scores; upcoming link dates omit radar
344
+ scores. Default piped writes and `--compact` keep primary and changed ids plus
345
+ warnings; explicit `--json` retains full mutation detail. `--compact`
346
+ removes indentation and empty receipt arrays. `--quiet` suppresses stdout.
347
+ Writes list what changed in `data.changes`, and creates warn about likely
348
+ duplicates. Kebab-case flags map to schema properties. `--` ends flags.
349
+ A positional `-`, or piped stdin when no other arguments are given, reads a
350
+ JSON object (`echo '{"name":"Sam","kind":"human"}' | ghostpaw-affinity contact create`).
351
+ Explicit flags override stdin fields.
223
352
  Database selection uses `--db`, `GHOSTPAW_AFFINITY_DB`, then
224
- `~/.ghostpaw/affinity.db`. These commands work offline.
353
+ `~/.ghostpaw/affinity.db`; relative paths resolve against the current
354
+ directory, and `--help` shows the resolved path. A new database is reported on
355
+ stderr. These commands work offline.
225
356
 
226
- | Exit | Noun-verb result |
357
+ | Exit | Result |
227
358
  |---|---|
228
359
  | 0 | Success, including no_op |
229
- | 1 | Tool error |
230
- | 2 | Usage or schema error |
360
+ | 1 | Other tool error |
361
+ | 2 | Usage, schema or invalid input |
231
362
  | 3 | Clarification needed or not found |
232
- | 4 | Conflict or version mismatch |
363
+ | 4 | Conflict, already resolved or version mismatch |
233
364
  | 70 | Internal error |
234
365
 
235
366
  Errors go to stderr. With `--json`, errors also print to stdout.
236
- Legacy `tools [--json]`, `describe <tool>`, `call <tool> [JSON|-]` and
237
- `<tool_name> --parameter value` retain their output and exit codes.
367
+ `--version` names the variant: `affinity 0.5.0 (binary, linux-x64)`,
368
+ `(npm)` or `(library)`. Use `--version --short` in scripts for exact semver
369
+ matching. `--version --json` returns `name`, `version`, `variant` and `target`
370
+ (the target is null outside a binary). `version --short` and `version --json`
371
+ are equivalent.
372
+
373
+ Read commands (search, show, review and contact export, including direct tool
374
+ calls) exit 3 on a missing database without creating files:
375
+ `Database not found: <path> (it is created by the first write)`.
376
+ The first write creates the database and reports `created <path>` on stderr.
377
+ The MCP server continues to create its database on startup.
378
+
379
+ String-valued flags such as `--value Hamburg`, `--bond 'Warm lead'` and
380
+ `--reason 'Different people'` accept plain strings or JSON strings/null.
381
+ Object and array flags still require JSON. Tag filters accept both
382
+ `--tag gastro-radar` and `--tag tag:gastro-radar`.
383
+ Per-command `--help` lists the fields used by that action.
384
+
385
+ Legacy `tools [--json]`, `describe <tool>`,
386
+ `call <tool> [JSON|-]` and `<tool_name> --parameter value` remain available.
238
387
  `completions bash|zsh|fish` prints shell scripts.
239
388
 
240
- ```sh
241
- ghostpaw-affinity contact create '{"name":"Sam","kind":"human"}' --db /tmp/affinity-example.db --json
242
- ghostpaw-affinity search Sam --db /tmp/affinity-example.db --json
243
- ghostpaw-affinity review contacts.list --db /tmp/affinity-example.db --json
244
- ```
245
-
246
389
  ## Quick Start
247
390
 
248
391
  ```ts
@@ -479,3 +622,92 @@ If this package helps your project, consider sponsoring its maintenance:
479
622
  ---
480
623
 
481
624
  **[Anonyfox](https://anonyfox.com) • [MIT License](LICENSE)**
625
+
626
+ ### Social imports and batch writes
627
+
628
+ Contact creation accepts `identities` and `ifExists: "error" | "return"` in the
629
+ library and tools. Identity ownership conflicts fail atomically (CLI exit 4),
630
+ including `data.existingContactId` and `error.details.existingContactId` in tool
631
+ results. `contact create '<JSON>' --if-exists return` returns the existing contact
632
+ with `outcome: "no_op"` and `matchedIdentities`; it attaches nothing, including
633
+ other free identities. Name-only duplicates still receive a possible-duplicate
634
+ warning. Links can opt into `input.ifExists: "return"` when seeding repeatedly. Identity
635
+ addition supports the same opt-in policy for an identity already on that contact;
636
+ another contact still conflicts. Importers add changed handles separately after
637
+ contact creation.
638
+
639
+ Handle identities strip a leading `@` and lowercase on add, revise and lookup.
640
+ Bluesky handles must be domain-like, X/Twitter handles have 1–15 letters, digits
641
+ or underscores, and Mastodon handles use `user@host.tld`. Use native `did` (alias
642
+ `atproto_did`) for stable AT Protocol identities. Locators also find legacy
643
+ `custom:atproto-did` rows. Keep former handles as additional identities.
644
+ `search @handle` works because CLI values are literal.
645
+
646
+ Only the JSON payload arguments (`--input`, the positional `<input>`, `--patch`
647
+ and the `call` argument) read files. Their help line ends with
648
+ `(@file, @- stdin, @@ literal @)`: `@path` reads the file as UTF-8 relative to
649
+ the current directory, `@-` reads stdin, and `@@text` passes the literal
650
+ `@text`. An empty path (`@`) or an unreadable file exits 2. Every other value,
651
+ including names, notes, attribute values and search queries, is taken byte for
652
+ byte, even with a leading `@` or `@@`.
653
+
654
+ For large payloads use stdin or `@file` to avoid the operating system's argv
655
+ size limit (`E2BIG`). JSON flag values support `-` too:
656
+
657
+ ```sh
658
+ ghostpaw-affinity event record --kind interaction --input - < event.json
659
+ ghostpaw-affinity event record --kind interaction --input @event.json
660
+ ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}' --event-types '["conversation","observation"]'
661
+ ```
662
+
663
+ Array flags accept JSON arrays, comma lists, or repeated flags. Unknown event
664
+ types fail with exit 2 and allowed values. SQLite timestamps such as
665
+ `2026-10-27 17:51:33` use UTC; explicit offsets are honored. Sub-millisecond
666
+ fractions are rounded silently to milliseconds, including carry into the next
667
+ second. Keep the original timestamp in the source archive if finer precision
668
+ matters. Null `occurredAt` is invalid.
669
+
670
+ Record likes, reposts and follows as observations with `affectsBond: false`.
671
+ Observations with significance <= 2 default to false; other events default to
672
+ true. These ambient records stay in the journal without changing affinity or
673
+ recency. `provenance.ambient: true` also suppresses bond effects. Explicitly
674
+ outbound-only evidence contributes much less and does not reset radar recency;
675
+ without reciprocal contact it generates no routine check-in or drift prompt.
676
+ Include the owner explicitly with the correct actor/recipient role and use
677
+ `owner_initiated`, `other_initiated`, or `mutual` directionality. Observations and
678
+ follow-like milestones never derive breakthroughs; low-significance milestones
679
+ do not derive moments. An explicit `momentKind` is honored.
680
+
681
+ List reviews default to 50 rows, accept `--limit` (up to 250) and `--offset`,
682
+ and include `total` and `hasMore`. Follow `hasMore` rather than assuming the
683
+ first page is complete.
684
+
685
+ `batch` reads one tool invocation per JSONL line, with one transaction for the
686
+ whole input by default, or one per `--chunk N` lines. Each line has its own
687
+ savepoint: failed lines roll back and later lines continue. `--stop-on-error`
688
+ stops at the first failure. Results are JSONL `{line, ok, outcome, id, error?}`
689
+ plus `{done, created, noOp, failed}`; created counts successful non-no-op lines.
690
+ Exit status is 0 for success, 1 for failures, or 2 if a line is invalid JSON.
691
+ Committed chunks remain committed if a later chunk fails.
692
+
693
+ ```jsonl
694
+ {"tool":"manage_contact","args":{"action":"create","input":{"name":"Owner","kind":"human","bootstrapOwner":true,"ifExists":"return","identities":[{"type":"did","value":"did:plc:owner"}]}}}
695
+ {"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"}]}}
696
+ ```
697
+
698
+ ```sh
699
+ ghostpaw-affinity batch --chunk 500 < import.jsonl
700
+ node scripts/examples/import_social_jsonl.mjs scripts/examples/fixtures/social_sample.json | ghostpaw-affinity --db /tmp/social-demo.db batch
701
+ node scripts/examples/import_dry_run.mjs
702
+ ```
703
+
704
+ The dependency-free example consumes a JSON export shaped as accounts, posts,
705
+ interactions and follows; it never opens the source database. It resolves handle
706
+ aliases to DIDs, deduplicates record URIs, keeps content verbatim, sets channel,
707
+ parent references and directionality, and uses first_seen for undated follows
708
+ or counts a skipped timestamp. The dry run checks two imports, DID uniqueness
709
+ and byte-identical content, and reports journal JSON bytes versus raw text bytes.
710
+ Examples and synthetic fixtures are development files, outside the npm package.
711
+
712
+ `ghostpaw-affinity skill --short` prints the compact command reference without
713
+ frontmatter or an appendix. `skill --list` lists guides; `skill <guide>` reads one.