@ghostpaw/affinity 0.5.0 → 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
@@ -45,9 +45,19 @@ cd "$(mktemp -d)" && T=darwin-x64 && V=$(curl -fsSLI -o /dev/null -w '%{url_effe
45
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
46
46
  ```
47
47
 
48
- Linux installs to `~/.local/bin` (added to `~/.profile` if missing), macOS to
49
- `/usr/local/bin` (asks for sudo), Windows to `%LOCALAPPDATA%\Programs\ghostpaw\bin`
50
- (added to the user `PATH`; open a new terminal afterwards).
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).
49
+
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`):
51
+
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
60
+ ```
51
61
 
52
62
  Then:
53
63
 
@@ -63,7 +73,7 @@ resolve against the current directory.
63
73
 
64
74
  A session with the linux-x64 binary against a fresh database:
65
75
 
66
- ![ghostpaw-affinity session: 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)
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)
67
77
 
68
78
  The same session as an [animated GIF](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.gif).
69
79
 
@@ -218,6 +228,43 @@ ghostpaw-affinity skill --list # workflow guides
218
228
  ghostpaw-affinity skill bootstrap-and-cold-start
219
229
  ```
220
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.
267
+
221
268
  ## CLI
222
269
 
223
270
  ```sh
@@ -252,6 +299,13 @@ ghostpaw-affinity commitment record|resolve [flags]
252
299
  ghostpaw-affinity attribute set|unset|replace [flags]
253
300
  ghostpaw-affinity date add|revise|remove [flags]
254
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
255
309
  ghostpaw-affinity duplicate dismiss|undismiss [flags]
256
310
  ghostpaw-affinity mcp|skill|tools|describe|call|completions|version|help
257
311
  ```
@@ -265,15 +319,30 @@ to read a file; repeated array flags accept JSON objects
265
319
  (`--entries '{"name":"a","value":"x"}'`) or one JSON array.
266
320
 
267
321
  Every time field accepts epoch milliseconds, ISO 8601 with `Z` or an offset,
268
- `YYYY-MM-DD`, `DD.MM.YYYY` (CLI), `today`, `tomorrow`, `yesterday` and signed day offsets
269
- such as `+3d` or `-2d`. On the CLI, date-only values use local time (midnight,
270
- or end of day for `dueAt`); through MCP and the tool facade they use UTC.
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.
271
336
 
272
337
  Global flags: `--db <path>`, `--json`, `--quiet`, `--compact`,
273
338
  `--help`/`-h`, `--version`/`-V`/`-v`, `--no-color`.
274
339
  `NO_COLOR` is accepted. In a terminal, commands print a readable summary with
275
340
  names and dates. With `--json` or piped stdout they print the tool result as
276
- JSON with ISO date siblings and floats rounded to three decimals; `--compact`
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`
277
346
  removes indentation and empty receipt arrays. `--quiet` suppresses stdout.
278
347
  Writes list what changed in `data.changes`, and creates warn about likely
279
348
  duplicates. Kebab-case flags map to schema properties. `--` ends flags.
@@ -296,7 +365,24 @@ stderr. These commands work offline.
296
365
 
297
366
  Errors go to stderr. With `--json`, errors also print to stdout.
298
367
  `--version` names the variant: `affinity 0.5.0 (binary, linux-x64)`,
299
- `(npm)` or `(library)`. Legacy `tools [--json]`, `describe <tool>`,
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>`,
300
386
  `call <tool> [JSON|-]` and `<tool_name> --parameter value` remain available.
301
387
  `completions bash|zsh|fish` prints shell scripts.
302
388
 
@@ -536,3 +622,92 @@ If this package helps your project, consider sponsoring its maintenance:
536
622
  ---
537
623
 
538
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.