@ghostpaw/affinity 0.6.0 → 0.6.1

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/dist/cli.js CHANGED
@@ -25,7 +25,7 @@ var __export = (target, all) => {
25
25
  var affinityVersion;
26
26
  var init_version = __esm({
27
27
  "src/version.ts"() {
28
- affinityVersion = "0.6.0";
28
+ affinityVersion = "0.6.1";
29
29
  }
30
30
  });
31
31
  function readFileValue(raw, name) {
@@ -624,6 +624,8 @@ function mapEventRowToEventRecord(row, participants) {
624
624
  momentKind: row.moment_kind,
625
625
  participants
626
626
  };
627
+ if (row.provenance_json != null)
628
+ base.provenance = JSON.parse(row.provenance_json);
627
629
  if (row.content != null) base.content = decodeEventText(row.content);
628
630
  if (row.channel != null) base.channel = row.channel;
629
631
  if (row.external_source != null && row.external_id != null)
@@ -939,14 +941,9 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
939
941
  text = text.replace(" ", "T");
940
942
  if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
941
943
  }
942
- let rounding = 0;
943
944
  text = text.replace(
944
945
  /\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
945
- (_, digits) => {
946
- const ms = Math.round(Number(`0.${digits}`) * 1e3);
947
- rounding = ms === 1e3 ? 1 : 0;
948
- return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
949
- }
946
+ (_, digits) => `.${digits.slice(0, 3)}`
950
947
  );
951
948
  if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
952
949
  const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
@@ -966,7 +963,7 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
966
963
  const midnight = Date.parse(`${day}T00:00:00Z`);
967
964
  if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
968
965
  return fail();
969
- const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text) + rounding;
966
+ const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
970
967
  if (!Number.isFinite(timestamp)) return fail();
971
968
  return { timestamp, day };
972
969
  }
@@ -2146,7 +2143,7 @@ var init_event_metadata_schema = __esm({
2146
2143
  );
2147
2144
  eventMetadataSchema = {
2148
2145
  affectsBond: booleanSchema(
2149
- "Whether evidence affects the relationship; defaults false for observations with significance <=2."
2146
+ "Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
2150
2147
  ),
2151
2148
  content: stringSchema(
2152
2149
  "Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
@@ -2733,6 +2730,13 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
2733
2730
  `${path}.${key}`,
2734
2731
  referenceNow
2735
2732
  );
2733
+ if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
2734
+ const provenance = object.provenance;
2735
+ object.provenance = {
2736
+ ...provenance && typeof provenance === "object" ? provenance : {},
2737
+ [`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
2738
+ };
2739
+ }
2736
2740
  object[key] = parsed.timestamp;
2737
2741
  if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
2738
2742
  object[key.replace(/At$/, "On")] ??= parsed.day;
@@ -2750,7 +2754,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
2750
2754
  const error = validateToolInput(
2751
2755
  child,
2752
2756
  object[key],
2753
- `${path}.${key}`,
2757
+ key === "input" && path === "input" ? path : `${path}.${key}`,
2754
2758
  referenceNow,
2755
2759
  strict
2756
2760
  );
@@ -3265,6 +3269,11 @@ function setAttribute(db, target, name, value) {
3265
3269
  const stored = normalizeAttributeValue(value);
3266
3270
  const now = resolveNow();
3267
3271
  const existing = getLiveAttributeRow(db, target, key);
3272
+ if (existing?.value === stored)
3273
+ return buildAttributeMutationReceipt(
3274
+ mapAttributeRowToAttributeRecord(existing),
3275
+ {}
3276
+ );
3268
3277
  if (existing) {
3269
3278
  db.prepare(
3270
3279
  "UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
@@ -3435,7 +3444,30 @@ function linkLocatorSchema2(description) {
3435
3444
  };
3436
3445
  }
3437
3446
  function resolveAttributeTarget(db, target) {
3438
- if (target.kind === "event") return { kind: "ok", target };
3447
+ if (target.kind === "event") {
3448
+ if (target.eventId === void 0 === (target.ref === void 0))
3449
+ return {
3450
+ kind: "error",
3451
+ result: toolFailure(
3452
+ "protocol",
3453
+ "invalid_input",
3454
+ "Invalid event target.",
3455
+ "Provide exactly one of target.eventId or target.ref."
3456
+ )
3457
+ };
3458
+ const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
3459
+ if (eventId === void 0)
3460
+ return {
3461
+ kind: "error",
3462
+ result: toolFailure(
3463
+ "domain",
3464
+ "not_found",
3465
+ "Event not found.",
3466
+ "No event matches target.ref."
3467
+ )
3468
+ };
3469
+ return { kind: "ok", target: { kind: "event", eventId } };
3470
+ }
3439
3471
  if (target.kind === "contact") {
3440
3472
  const contact = resolveContactLocator(db, target.contact, "target.contact");
3441
3473
  if (!contact.ok) {
@@ -3499,6 +3531,8 @@ var init_manage_attribute_tool = __esm({
3499
3531
  init_replace_attributes();
3500
3532
  init_set_attribute();
3501
3533
  init_unset_attribute();
3534
+ init_event_metadata();
3535
+ init_event_metadata_schema();
3502
3536
  init_tool_metadata();
3503
3537
  init_tool_mutation();
3504
3538
  init_tool_names();
@@ -3529,7 +3563,8 @@ var init_manage_attribute_tool = __esm({
3529
3563
  target: objectSchema(
3530
3564
  {
3531
3565
  kind: enumSchema("Target kind.", ["contact", "link", "event"]),
3532
- eventId: integerSchema("Target event id."),
3566
+ eventId: integerSchema("Target event id; provide eventId or ref."),
3567
+ ref: externalRefSchema,
3533
3568
  contact: contactLocatorSchema2(
3534
3569
  "Contact locator. Required when kind=contact. Provide contactId or identity."
3535
3570
  ),
@@ -3587,7 +3622,9 @@ function insertJournalEvent(db, params) {
3587
3622
  );
3588
3623
  const eventId = Number(result.lastInsertRowid);
3589
3624
  db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
3590
- params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 ? 0 : 1 : Number(params.affectsBond),
3625
+ params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
3626
+ summary
3627
+ ) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
3591
3628
  eventId
3592
3629
  );
3593
3630
  db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
@@ -5395,7 +5432,7 @@ function exportContact(db, id) {
5395
5432
  const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
5396
5433
  if (!contact) throw new AffinityNotFoundError("contact not found");
5397
5434
  const events = query(
5398
- "SELECT DISTINCT e.*, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
5435
+ "SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
5399
5436
  id
5400
5437
  ).map((event) => ({
5401
5438
  ...event,
@@ -9057,12 +9094,14 @@ function getEventThread(db, source, id, options) {
9057
9094
  const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
9058
9095
  SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
9059
9096
  WHERE e.deleted_at IS NULL AND (e.parent_event_id=p.id OR (e.parent_source=p.external_source AND e.parent_ref_id=p.external_id) OR (e.thread_source=? AND e.thread_id=?))
9060
- ) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
9097
+ ) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
9061
9098
  root.id,
9062
9099
  root.externalRef?.source ?? null,
9063
9100
  root.externalRef?.id ?? null,
9064
9101
  options?.channel ?? null,
9065
9102
  options?.channel?.trim().toLowerCase() ?? null,
9103
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
9104
+ options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
9066
9105
  limit,
9067
9106
  offset
9068
9107
  );
@@ -9780,12 +9819,12 @@ function reviewPage(db, rawInput, pageOnly) {
9780
9819
  if (!pageOnly && input.view !== "graph.chart") {
9781
9820
  const limit = input.limit ?? 50;
9782
9821
  const offset = input.offset ?? 0;
9783
- if (!Number.isInteger(limit) || limit < 1 || limit > 250 || !Number.isInteger(offset) || offset < 0)
9822
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
9784
9823
  return toolFailure(
9785
9824
  "protocol",
9786
9825
  "invalid_input",
9787
9826
  "Invalid pagination.",
9788
- "limit must be 1..250; offset must be non-negative"
9827
+ "limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
9789
9828
  );
9790
9829
  const all = [];
9791
9830
  let page;
@@ -9837,7 +9876,7 @@ function reviewPage(db, rawInput, pageOnly) {
9837
9876
  {
9838
9877
  ...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
9839
9878
  ...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
9840
- ...input.minScore === void 0 ? {} : { minScore: input.minScore },
9879
+ ...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
9841
9880
  ...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
9842
9881
  },
9843
9882
  options
@@ -9851,9 +9890,10 @@ function reviewPage(db, rawInput, pageOnly) {
9851
9890
  "Missing thread reference.",
9852
9891
  "ref is required for events.thread."
9853
9892
  );
9854
- items = getEventThread(db, input.ref.source, input.ref.id, options).map(
9855
- toEventListToolItem
9856
- );
9893
+ items = getEventThread(db, input.ref.source, input.ref.id, {
9894
+ ...options,
9895
+ ...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
9896
+ }).map(toEventListToolItem);
9857
9897
  break;
9858
9898
  }
9859
9899
  case "events.contact_journal": {
@@ -10141,7 +10181,7 @@ var init_review_affinity_tool = __esm({
10141
10181
  includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
10142
10182
  since: "Optional lower timestamp bound for time-based views.",
10143
10183
  until: "Optional upper timestamp bound for time-based views.",
10144
- limit: "Optional result limit.",
10184
+ limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
10145
10185
  offset: "Optional result offset."
10146
10186
  },
10147
10187
  outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
@@ -10246,13 +10286,17 @@ var init_review_affinity_tool = __esm({
10246
10286
  ]),
10247
10287
  horizonDays: integerSchema("Optional horizon-days filter."),
10248
10288
  exactOnly: booleanSchema("Whether duplicate review is exact-only."),
10249
- minScore: numberSchema("Optional duplicate minimum score."),
10289
+ minScore: numberSchema(
10290
+ "Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
10291
+ ),
10250
10292
  includeDismissed: booleanSchema(
10251
10293
  "Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
10252
10294
  ),
10253
10295
  since: integerSchema("Optional lower timestamp bound."),
10254
10296
  until: integerSchema("Optional upper timestamp bound."),
10255
- limit: integerSchema("Optional result limit."),
10297
+ limit: integerSchema(
10298
+ "Optional result limit, 1..10000 (default 50); use offset for paging."
10299
+ ),
10256
10300
  offset: integerSchema("Optional result offset.")
10257
10301
  },
10258
10302
  ["view"],
@@ -10630,10 +10674,18 @@ function normalizeTimeInputs(value, now = Date.now(), inPatch = false) {
10630
10674
  `Invalid timestamp "${item}". ${error.message}`
10631
10675
  );
10632
10676
  }
10677
+ if (!inPatch && ["occurredAt", "dueAt"].includes(key) && typeof item === "string" && /\.\d{4,}/.test(item)) {
10678
+ const provenance = result.provenance ?? value.provenance;
10679
+ result.provenance = {
10680
+ ...provenance && typeof provenance === "object" ? provenance : {},
10681
+ [`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: item
10682
+ };
10683
+ }
10633
10684
  result[key] = parsed.timestamp;
10634
10685
  if (!inPatch && ["occurredAt", "dueAt"].includes(key))
10635
10686
  result[key.replace(/At$/, "On")] = parsed.day;
10636
- } else result[key] = normalizeTimeInputs(item, now, key === "patch");
10687
+ } else if (key !== "provenance" || result.provenance === void 0)
10688
+ result[key] = key === "provenance" ? item : normalizeTimeInputs(item, now, key === "patch");
10637
10689
  }
10638
10690
  return result;
10639
10691
  }
@@ -10675,6 +10727,12 @@ var init_timestamp_schema = __esm({
10675
10727
  function surfaceProperties(tool) {
10676
10728
  return {
10677
10729
  ...tool.inputSchema.properties,
10730
+ ...tool.name === "review_affinity" ? {
10731
+ includeContent: {
10732
+ type: "boolean",
10733
+ description: "Deprecated alias for --with-content; includes verbatim event content."
10734
+ }
10735
+ } : {},
10678
10736
  ...["inspect_affinity_item", "review_affinity", "manage_event"].includes(
10679
10737
  tool.name
10680
10738
  ) ? {
@@ -11457,9 +11515,9 @@ async function openAffinityDb(path) {
11457
11515
  }
11458
11516
  const db = new sqlite.DatabaseSync(path);
11459
11517
  try {
11460
- db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
11518
+ db.exec("PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL;");
11461
11519
  const context = db;
11462
- initAffinityTables(context);
11520
+ withTransaction(context, () => initAffinityTables(context));
11463
11521
  return context;
11464
11522
  } catch (error) {
11465
11523
  db.close();
@@ -11469,6 +11527,7 @@ async function openAffinityDb(path) {
11469
11527
  var init_open_affinity_db = __esm({
11470
11528
  "src/surface/open_affinity_db.ts"() {
11471
11529
  init_init_affinity_tables();
11530
+ init_with_transaction();
11472
11531
  }
11473
11532
  });
11474
11533
 
@@ -12009,7 +12068,11 @@ function plainJson(value, compact = false, formatError) {
12009
12068
  );
12010
12069
  }
12011
12070
  const data = result.data;
12012
- if (data?.items && JSON.stringify(result.entities) === JSON.stringify(data.items))
12071
+ if (data?.items && (JSON.stringify(result.entities) === JSON.stringify(data.items) || JSON.stringify(result.entities) === JSON.stringify(
12072
+ data.items.map(
12073
+ ({ content: _content, ...item }) => item
12074
+ )
12075
+ )))
12013
12076
  result.entities = void 0;
12014
12077
  if (compact && data?.primary) {
12015
12078
  result.entities = void 0;
@@ -12255,7 +12318,7 @@ var init_skill_markdown = __esm({
12255
12318
  var affinitySkillReferences;
12256
12319
  var init_affinity_skill_references = __esm({
12257
12320
  "src/surface/affinity_skill_references.ts"() {
12258
- affinitySkillReferences = { "bootstrap-and-cold-start": { "description": "Establish a clean Affinity baseline with one owner, one verified identity, and the minimum structure needed for later evidence and review work.", "content": '# Bootstrap And Cold Start\n\nPrimary tools:\n- `manage_contact`\n- `manage_identity`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Establish one clean owner record, make it reachable, and verify the system is ready for later relationship work.\n\nWhen to use:\n- The database is new or logically empty.\n- A harness needs to initialize its own operator profile before doing anything else.\n\nWhen not to use:\n- The owner already exists and you only need to update profile fields.\n- You are importing historical relationships; use the import-focused skill after bootstrap.\n\nStep-by-step sequence:\n1. Assume the schema is already initialized and the database is live before using any tool.\n2. Use `manage_contact` with `action: "create"` and `bootstrapOwner: true` exactly once for the operator.\n3. Immediately attach at least one routing identity with `manage_identity` so future lookups can use a stable natural key.\n4. Use `inspect_affinity_item` with `kind: "owner_profile"` to confirm that the owner profile exists and reads back cleanly.\n5. Use `review_affinity` with `view: "contacts.list"` to confirm that the portfolio is still minimal and no duplicate owner-like records were created during setup.\n\nValidation checks:\n- The owner profile loads successfully.\n- The owner has at least one live identity.\n- The contact list shows exactly the records you expect at this early stage.\n\nPitfalls:\n- Do not create a non-owner placeholder and plan to convert it later if the harness can bootstrap the real owner immediately.\n- Do not skip identity attachment, or later natural-key lookup will be weaker and duplicate-prone.\n- Do not seed relationships before the owner exists if later evidence is meant to be owner-facing.\n\nTips and tricks:\n- Prefer an identity that is stable across sessions, such as a primary email or canonical profile URL.\n- Keep the first dataset tiny. Bootstrap is for a trustworthy base, not a full import.\n- If setup feels ambiguous, inspect first and add less rather than more.\n\nTool calls to prefer:\n- `manage_contact` for owner creation\n- `manage_identity` for first routing key\n- `inspect_affinity_item` for confirmation\n- `review_affinity` for early portfolio sanity checks\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `import-history-without-faking-evidence`' }, "identify-and-locate-contacts": { "description": "Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.", "content": '# Identify And Locate Contacts\n\nHandle identities normalize leading @ and case. Bluesky uses a domain handle,\nX/Twitter uses 1\u201315 letters/digits/underscores, Mastodon uses user@host.tld.\nNative did (alias atproto_did) validates did:<method>:<identifier>; legacy\ncustom:atproto-did locators find the same identity. Keep old handles as identities.\nAn owned identity makes contact creation fail atomically with conflict and\nexistingContactId. input.ifExists="return" returns the matching contact and\nmatchedIdentities without attaching any free identities. CLI: --if-exists return.\nName-only possible duplicates still warn. Search @handle is always literal.\n\nPrimary tools:\n- `search_affinity`\n- `inspect_affinity_item`\n- `manage_identity`\n- `manage_contact`\n\nGoal:\n- Resolve the right contact before mutating anything, using the strongest available locator.\n\nWhen to use:\n- A harness receives a name, email, phone number, URL, or ambiguous reference to a person or organization.\n- You need to confirm whether a contact already exists before creating a new one.\n\nWhen not to use:\n- You already have one exact internal id from a prior successful tool result.\n- You are reviewing a list surface rather than identifying one exact target.\n\nStep-by-step sequence:\n1. Start with `search_affinity` whenever the target might already exist.\n2. If you have a strong natural key such as email, phone, or URL-like handle, use that query first rather than a vague display name.\n3. If search returns one strong candidate, use `inspect_affinity_item` to confirm the profile before mutating.\n4. If the target does not exist, create it with `manage_contact`.\n5. Immediately add the best routing key with `manage_identity` so future lookup becomes exact rather than fuzzy.\n6. If the system returns ambiguity, stop and clarify instead of guessing.\n\nValidation checks:\n- The inspected profile matches the intended person or organization.\n- The best known routing identity is attached and, when appropriate, verified.\n- Later searches by that identity return the same contact cleanly.\n\nPitfalls:\n- Do not trust names alone when a natural key is available.\n- Do not create a second contact just because search returned multiple plausible matches.\n- Do not remove or revise identities casually; they are your best future locator surface.\n\nTips and tricks:\n- Prefer exact identities over integer ids when the harness needs to survive across sessions or partial memory loss.\n- After creating a contact from a vague mention, upgrade it with a real identity as soon as possible.\n- If multiple weak matches appear, inspect before mutating.\n\nTool calls to prefer:\n- `search_affinity` for candidate discovery\n- `inspect_affinity_item` for exact confirmation\n- `manage_identity` for routing-key upgrades\n- `manage_contact` only after lookup fails honestly\n\nRelated skills:\n- `bootstrap-and-cold-start`\n- `reconcile-duplicates-and-merge-safely`' }, "import-history-without-faking-evidence": { "description": "Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.", "content": '# Import History Without Faking Evidence\n\nFor social imports, use native did identities (atproto_did is an alias; legacy\ncustom:atproto-did locators remain equivalent). Create with input.ifExists="return"\nfor safe retries: this attaches nothing to an existing contact. For handle changes,\ncall manage_identity add separately with input.ifExists="return"; an identity already\non that contact is a no-op, while another owner still conflicts. Seed links with\ninput.ifExists="return" to reuse them without changing their state.\nUse ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or\n{op:"event",kind:"interaction",input:{...}}. Per-line savepoints isolate failures;\n--stop-on-error stops early. Exit 2 identifies malformed JSON; other failures exit 1.\nUse externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.\nParticipant contact locators may use identities, including did, instead of ids.\nRead JSON flags from stdin with --input - or from @file for large payloads (E2BIG).\nSQLite date-times without offsets mean UTC; microseconds round to milliseconds.\nMissing inbound-follow timestamps should use a known first_seen or be counted as skipped.\nThe synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs\nshow the mapping and verify an idempotent retry and byte-identical text.\n\nFor archived messages with known timestamps, record actual evidence with externalRef:\n{source, id} deduplicates retries across event families and commitments. Keep original\nmessage text in content, use channel and parentRef/threadRef for context, and keep\nsummary short or let it derive from content. Content is limited to 1 MB UTF-8;\nkeep larger raw archives in the source system. Inspect events by external reference,\nsearch scope "events", and use manage_event revise/delete to correct imports.\nSee the tools guide for field details. Existing 0.5.x databases upgrade in place.\n\nPrimary tools:\n- `manage_contact`\n- `manage_identity`\n- `manage_relationship`\n- `inspect_affinity_item`\n\nGoal:\n- Represent real pre-system relationship state without fabricating a timeline of events that never existed inside Affinity.\n\nWhen to use:\n- You are migrating from another CRM, notes system, or memory set.\n- You already know a relationship exists and roughly how strong or active it is, but you do not want to reconstruct every past interaction.\n\nWhen not to use:\n- A new event just happened and should affect mechanics now.\n- You are recording a specific conversation, milestone, transaction, or observation.\n\nStep-by-step sequence:\n1. Create or locate the relevant contacts first.\n2. Add the strongest identities you know so the imported graph stays addressable later.\n3. Use `manage_relationship` with `action: "seed_social_link"` to establish a relational link that already existed before the system.\n4. Set only the fields you can justify: kind, rough rank, trust, cadence, state, and optional bond.\n5. Inspect the result with `inspect_affinity_item` if you need to verify the exact post-import state.\n6. From that point forward, switch to evidence tools for all new real-world activity.\n\nValidation checks:\n- The imported contact pair now has the expected relational link.\n- The seeded state looks plausible when inspected.\n- Later direct events continue progression from the seeded baseline rather than from zero.\n\nPitfalls:\n- Do not backfill dozens of fake conversations just to reach a desired rank.\n- Do not use import seeding for ordinary day-to-day relationship maintenance.\n- Do not overstate confidence on rank or trust when the historical truth is actually vague.\n\nTips and tricks:\n- Seed conservatively if you are unsure. It is safer to underclaim and let real future evidence move the link.\n- Use bond text sparingly as a narrative reminder of why the imported state is what it is.\n- Prefer one honest seed over many synthetic event reconstructions.\n\nTool calls to prefer:\n- `manage_relationship` with `seed_social_link`\n- `manage_contact` and `manage_identity` before seeding\n- `inspect_affinity_item` for post-import verification\n\nRelated skills:\n- `bootstrap-and-cold-start`\n- `record-direct-evidence-well`' }, "model-structure-orgs-and-households": { "description": "Model factual structure such as employment, membership, hierarchy, and household ties without confusing it with live relational mechanics.", "content": '# Model Structure, Orgs, And Households\n\nPrimary tools:\n- `manage_contact`\n- `manage_relationship`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Capture the stable factual skeleton of the world: who belongs where, who reports to whom, and what entities are structurally connected.\n\nWhen to use:\n- You need to represent employment, family structure, team membership, households, or service affiliation.\n- The fact should exist even if no emotional or operational relationship score is implied.\n\nWhen not to use:\n- You want to express how strong, strained, warm, or active a relationship feels.\n- You are recording something that happened in time.\n\nStep-by-step sequence:\n1. Create or locate the entities involved.\n2. Decide whether the fact is structural rather than relational. If the answer survives even with zero interaction history, it is usually structural.\n3. Use `manage_relationship` with `action: "set_structural_tie"` to record the factual tie.\n4. Include a role when the role clarifies the structure, such as job title or household role.\n5. Inspect relevant profiles or review lists after large structural batches to confirm the world model is clean.\n6. Remove or replace outdated ties when the world changes, such as a job move or team change.\n\nValidation checks:\n- The structural tie is visible and stable.\n- No fake progression fields were introduced just to represent a factual tie.\n- The chart and review surfaces now reflect the world structure you expect.\n\nPitfalls:\n- Do not use structural ties to stand in for closeness, trust, or cadence.\n- Do not seed social links just because a structural tie exists; only do that if a live relational track also matters.\n- Do not leave outdated structural ties hanging after the factual world changes.\n\nTips and tricks:\n- Structural and relational links can coexist. Use both when both truths matter.\n- Groups, households, companies, teams, and services often deserve their own contacts.\n- If you are unsure, ask whether the statement is about belonging or about lived relationship quality.\n\nTool calls to prefer:\n- `manage_relationship` with `set_structural_tie`\n- `inspect_affinity_item` to verify exact targets\n- `review_affinity` for broader structural review\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `record-direct-evidence-well`' }, "record-direct-evidence-well": { "description": "Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.", "content": '# Record Direct Evidence Well\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d). Date-only inputs\npreserve the calendar day at UTC midnight (UTC end of day for dueAt).\nKeywords use the process local date. Read occurredOn/dueOn and review occursOn\nfor calendar dates; *Iso fields remain UTC. Future events carry future_event\nwarnings and do not count for recency, cadence or drift until their time and day\narrive. Commitments and resolutions never reset contact recency or cadence.\n\nImported text and event maintenance:\n- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.\n- Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.\n- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.\n- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.\n- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.\n- Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.\n- Use manage_event delete with confirm: true to remove incorrect evidence and recompute affected relationship scores.\n- Send large payloads through whole-payload stdin or @file rather than argv. For example, affinity event record < payload.json or affinity call record_event - < payload.json.\n\nPrimary tools:\n- `record_event`\n- `inspect_affinity_item`\n- `review_affinity`\n- `manage_relationship`\n\nGoal:\n- Capture what really happened between the owner and other contacts so Affinity can derive progression, trust, cadence, and moments cleanly.\n\nWhen to use:\n- A conversation, activity, support exchange, conflict, correction, milestone, or direct operational interaction just happened.\n- You want future radar, progression, journal, and chart views to reflect the event truthfully.\n\nWhen not to use:\n- You are importing pre-system history.\n- You only observed third parties and were not directly part of the relationship evidence.\n\nStep-by-step sequence:\n1. Confirm the participants first. Use exact contacts whenever possible.\n2. Use `record_event` with the smallest honest event family: `interaction` for ordinary direct evidence, `milestone` when the event is truly milestone-shaped, or another event family only when it better matches reality.\n3. Set significance conservatively. Use high significance only when the event genuinely changed the relationship.\n4. Let the system auto-create owner-facing links when appropriate instead of manually seeding a link just because a direct event happened.\n5. Review the receipt or inspect the resulting profile when you need to understand the effect.\n6. Use `review_affinity` on journal, timeline, moments, or readiness views when you need context after a sequence of events.\n\nValidation checks:\n- The right contacts were involved.\n- The event family matches what really happened.\n- The resulting mechanics feel plausible when reviewed.\n\nPitfalls:\n- Do not inflate significance to force a rank-up.\n- Do not turn ordinary direct evidence into a milestone just to get a dramatic moment.\n- Do not use bond text as evidence. If the bond changed but no event happened, revise the bond separately.\n- Do not guess through ambiguity; clarify the target first.\n\nTips and tricks:\n- Conflict and correction are both first-class direct evidence. Use them honestly rather than hiding them inside generic summaries.\n- Multi-participant owner events can refresh multiple links at once. That is a feature, not a bug.\n- If you need to label the current narrative reading without changing mechanics, use `manage_relationship` with `revise_bond` after the evidence is recorded.\n\nTool calls to prefer:\n- `record_event` for the evidence itself\n- `inspect_affinity_item` for exact read-back\n- `review_affinity` for journals, timelines, moments, and readiness\n- `manage_relationship` only for bond interpretation after the fact\n\nRelated skills:\n- `record-observations-and-referrals`\n- `review-radar-progression-and-graph`' }, "record-observations-and-referrals": { "description": "Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.", "content": '# Record Observations And Referrals\n\nLikes, reposts and follows are ambient observations, not milestones. Record them\nwith affectsBond:false; observations with significance <=2 default to false.\nThey stay in journals but do not change affinity or meaningful-contact recency.\nprovenance.ambient:true also suppresses effects. Use explicit owner and other\nparticipants, actor/recipient roles, and the correct directionality.\nUnanswered owner_initiated evidence contributes much less and does not reset\nradar recency. A relationship with only outbound activity gets no routine check-in\nor drift prompt. other_initiated evidence counts fully. Observations and follow-like\nmilestones never derive breakthroughs; low-significance milestones derive no moment.\n\nPrimary tools:\n- `record_event`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Add truthful third-party graph information while keeping observational evidence weaker than direct relationship evidence.\n\nWhen to use:\n- You learned that two people know each other, one person referred another, or you observed a third-party relationship pattern.\n- You want the graph to reflect world truth without pretending you had the direct interaction yourself.\n\nWhen not to use:\n- The owner directly participated in the event and should record direct evidence instead.\n- You merely suspect a relationship with no strong enough fact to justify recording it.\n\nStep-by-step sequence:\n1. Confirm the third-party contacts you are referring to.\n2. Use `record_event` with `kind: "observation"` when the truth is observational rather than direct.\n3. Keep significance modest unless the observed fact is genuinely consequential.\n4. After recording, use `review_affinity` to inspect observed-link or graph views and verify the resulting topology.\n5. If later direct evidence exists, record that separately rather than expecting the observation to do all the work.\n\nValidation checks:\n- The graph now reflects the third-party fact you meant to capture.\n- Observed links stay appropriately weaker than direct links.\n- No accidental owner-facing intimacy was invented.\n\nPitfalls:\n- Do not use observations to fake deep trust or rank.\n- Do not create a clique from vague group knowledge when the system only has justified pairwise facts.\n- Do not promote a rumor to structure or direct evidence without better confirmation.\n\nTips and tricks:\n- Referral chains are excellent observational facts because they help later review and graph interpretation.\n- Use chart and observed-link review views together after batches of observation work.\n- Observations are often better than silence, but they should remain humble.\n\nTool calls to prefer:\n- `record_event` with `observation`\n- `review_affinity` with observed-link and graph views\n- `inspect_affinity_item` if you need exact follow-up on one target\n\nRelated skills:\n- `record-direct-evidence-well`\n- `review-radar-progression-and-graph`' }, "capture-transactions-and-commercial-events": { "description": "Capture commercially meaningful interactions as transactions so operational and service relationships accumulate the right evidence shape.", "content": '# Capture Transactions And Commercial Events\n\nPrimary tools:\n- `record_event`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Represent deals, placements, closings, sponsorships, and operationally meaningful exchanges as transactions instead of flattening them into generic social notes.\n\nWhen to use:\n- Money, deliverables, placements, closings, or other commercially meaningful exchanges just occurred.\n- The relationship is service-oriented, professional, or business-critical.\n\nWhen not to use:\n- The main truth is a personal conversation or ordinary activity.\n- The important action is really a promise or agreement that still needs later resolution.\n\nStep-by-step sequence:\n1. Confirm the right counterparties first.\n2. Use `record_event` with `kind: "transaction"` for the commercial event itself.\n3. Keep the summary concrete enough that future review explains what was exchanged and why it mattered.\n4. Use significance according to the real business weight, not just enthusiasm.\n5. Review the affected relationship if the commercial event should meaningfully change trust, cadence, or graph visibility.\n\nValidation checks:\n- The transaction now appears in the relevant timeline or journal views.\n- The resulting relationship effect matches the commercial importance of the event.\n- Later maintenance views now have the right recency signal.\n\nPitfalls:\n- Do not bury closings, placements, or sponsorship deliveries inside generic conversations.\n- Do not record a transaction when the truth is still only a pending agreement.\n- Do not expect transactions to create the same kind of affinity movement as rich direct social evidence.\n\nTips and tricks:\n- A relationship can be commercially strong and emotionally shallow. That is normal.\n- Use transactions to preserve crisp operational history for later portfolio review.\n- Follow major transactions with review views to confirm the graph and maintenance surfaces updated as expected.\n\nTool calls to prefer:\n- `record_event` with `transaction`\n- `inspect_affinity_item` for exact relationship read-back\n- `review_affinity` for owner-link portfolio review\n\nRelated skills:\n- `manage-promises-and-agreements`\n- `review-radar-progression-and-graph`' }, "manage-promises-and-agreements": { "description": "Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.", "content": '# Manage Promises And Agreements\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d).\nDate-only values keep their day with UTC boundaries; keywords use the process\nlocal date. Read occurredOn/dueOn and review occursOn for calendar dates;\n*Iso fields stay UTC. Future events warn and count as contact only when their\ntime and day arrive. Commitments and resolutions never reset recency/cadence.\nRadar explains overdue promises and drift (gap > 1.5 times cadence).\n\nPrimary tools:\n- `manage_commitment`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Keep promises and agreements visible, resolvable, and mechanically meaningful instead of letting them disappear into narrative history.\n\nWhen to use:\n- Someone committed to doing something later.\n- A promise, agreement, or operational obligation needs follow-through.\n\nWhen not to use:\n- The event is complete already and no unresolved obligation exists.\n- A date is recurring and belongs in the date-anchor surface instead.\n\nStep-by-step sequence:\n1. When the commitment is made, use `manage_commitment` with `action: "record"`.\n2. Choose `promise` vs `agreement` honestly rather than treating them as stylistic synonyms.\n3. Include a due time when the obligation has a meaningful time boundary.\n4. Use `review_affinity` with the open-commitment view during regular review cycles.\n5. When the outcome is known, resolve it with `manage_commitment` as `kept`, `cancelled`, or `broken`.\n6. Inspect the relevant profile or review the relevant queue again if you need to confirm closure.\n\nValidation checks:\n- Open commitments appear when they should and disappear when resolved.\n- Resolution state matches real-world truth.\n- Trust effects feel consistent with whether the promise was kept or broken.\n\nPitfalls:\n- Do not hide real obligations inside ordinary event summaries.\n- Do not resolve a commitment optimistically before the outcome is actually known.\n- Do not misuse `cancelled` when the truth is actually `broken`.\n\nTips and tricks:\n- If an obligation matters enough to disappoint someone when missed, record it.\n- Review open commitments regularly; the value of the system drops fast if this queue goes stale.\n- When a commitment resolution also deserves narrative context, pair the resolution with an appropriate event in the surrounding workflow.\n\nTool calls to prefer:\n- `manage_commitment` for record and resolve\n- `review_affinity` for the open-commitment queue\n- `inspect_affinity_item` when you need exact context on the affected relationship\n\nRelated skills:\n- `capture-transactions-and-commercial-events`\n- `manage-recurring-dates-and-reminders`' }, "manage-recurring-dates-and-reminders": { "description": "Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.", "content": '# Manage Recurring Dates And Reminders\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d).\nDate-only values keep their day with UTC boundaries; keywords use the process\nlocal date. Read occurredOn/dueOn and review occursOn for calendar dates;\n*Iso fields stay UTC. Future events warn and count as contact only when their\ntime and day arrive. Commitments and resolutions never reset recency/cadence.\nRadar explains overdue promises and drift (gap > 1.5 times cadence).\n\nPrimary tools:\n- `manage_date_anchor`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Materialize recurring important dates into a durable reminder surface that later review and relationship maintenance can trust.\n\nWhen to use:\n- A date recurs yearly and matters to a contact or link.\n- You want reminders for birthdays, anniversaries, renewals, memorials, or other yearly anchors.\n\nWhen not to use:\n- The date is a one-off event.\n- The fact is a preference or metadata note rather than a recurring anchor.\n\nStep-by-step sequence:\n1. Confirm the target contact or link first.\n2. Use `manage_date_anchor` with `action: "add"` and the right recurrence kind.\n3. Keep the summary operator-friendly so future reminders are immediately understandable.\n4. Use `review_affinity` with the upcoming-dates view and an explicit horizon during regular review work.\n5. Revise anchors when recurrence details change.\n6. Remove anchors when the reminder surface should no longer treat the date as active.\n\nValidation checks:\n- The anchor appears in upcoming-date review when it falls within the chosen horizon.\n- Duplicate anchors are not silently piling up.\n- The summary and significance are still meaningful for later action.\n\nPitfalls:\n- Do not store important recurring dates only as loose attributes.\n- Do not create duplicate yearly anchors for the same target and meaning.\n- Do not use recurring anchors for commitments; those belong in the commitment flow.\n\nTips and tricks:\n- Tune horizons by workflow: short for weekly review, longer for quarterly planning.\n- Use significance to help order upcoming-date review meaningfully.\n- Link-level anchors are valuable when the date belongs to a relationship rather than one person.\n\nTool calls to prefer:\n- `manage_date_anchor` for add, revise, and remove\n- `review_affinity` with the upcoming-dates view\n- `inspect_affinity_item` when you need exact context on the target\n\nRelated skills:\n- `manage-promises-and-agreements`\n- `review-radar-progression-and-graph`' }, "review-radar-progression-and-graph": { "description": "Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.", "content": "# Review Radar, Progression, And Graph\n\nList reviews accept limit (default 50, maximum 250) and offset. Results include\ntotal and hasMore; continue paging while hasMore is true. Event type array flags\naccept JSON arrays, comma lists, or repeated --event-types flags; invalid types\nreport allowed values with exit 2. Use skill --short for a compact CLI reference.\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d).\nDate-only values keep their day with UTC boundaries; keywords use the process\nlocal date. Read occurredOn/dueOn and review occursOn for calendar dates;\n*Iso fields stay UTC. Future events warn and count as contact only when their\ntime and day arrive. Commitments and resolutions never reset recency/cadence.\nRadar explains overdue promises and drift (gap > 1.5 times cadence).\n\nPrimary tools:\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Turn review surfaces into a disciplined operating loop for outreach, prioritization, and graph understanding.\n\nWhen to use:\n- You need a daily or weekly maintenance pass.\n- You want to know who is drifting, who is ready for deeper investment, or how the network is structured.\n\nWhen not to use:\n- You already know the one exact item you need to inspect in depth.\n- You are mutating state and only need confirmation of a single write result.\n\nStep-by-step sequence:\n1. Start with `review_affinity` on the highest-level view that matches the question: radar, owner links, progression readiness, graph, moments, or a timeline view.\n2. Use radar when the question is who needs attention now.\n3. Use progression readiness when the question is where additional positive investment is most likely to deepen a relationship.\n4. Use graph review when the question is network shape, bridges, clusters, or hidden centrality.\n5. Use journal or timeline views when a score needs narrative context before action.\n6. Escalate to `inspect_affinity_item` only after the review surface identifies the exact target worth opening deeply.\n\nValidation checks:\n- The chosen review surface actually matches the decision you are trying to make.\n- You inspected context before acting on a surprising score.\n- Follow-up work targets the right contact or link rather than the loudest metric.\n\nPitfalls:\n- Do not treat radar as a command to act blindly without context.\n- Do not confuse progression readiness with relationship importance; they answer different questions.\n- Do not rely on one score when a timeline or graph view would explain the situation better.\n\nTips and tricks:\n- Review flows are strongest when used rhythmically rather than sporadically.\n- Use graph review to understand bridges and clusters, then use timeline or profile inspection for the exact story behind them.\n- Empty results can still be informative: they may confirm that maintenance debt is currently low.\n\nTool calls to prefer:\n- `review_affinity` for all dashboard-style surfaces\n- `inspect_affinity_item` for exact deep follow-up\n\nRelated skills:\n- `record-direct-evidence-well`\n- `manage-recurring-dates-and-reminders`\n- `reconcile-duplicates-and-merge-safely`" }, "reconcile-duplicates-and-merge-safely": { "description": "Review duplicate candidates carefully and merge only after the identity, history, and structural consequences are understood.", "content": "# Reconcile Duplicates And Merge Safely\n\nPrimary tools:\n- `review_affinity`\n- `inspect_affinity_item`\n- `merge_contacts`\n\nGoal:\n- Collapse true duplicates without losing lineage, identities, participants, or relationship truth.\n\nWhen to use:\n- Duplicate-candidate review surfaces indicate a likely duplicate.\n- A harness or operator has strong evidence that two contacts are the same real entity.\n\nWhen not to use:\n- The match is still speculative and unresolved.\n- The real need is just better identities or labels rather than an actual merge.\n\nStep-by-step sequence:\n1. Start with `review_affinity` on the duplicate-candidate view.\n2. Inspect both candidate contacts before merging.\n3. Confirm the winner explicitly rather than letting recency or convenience decide by accident.\n4. Use `merge_contacts` only when the identity and history picture is clear.\n5. Inspect the surviving contact after the merge to confirm the merged state looks right.\n6. Review merge history when you need lineage confirmation later.\n\nValidation checks:\n- The surviving contact now contains the expected identities and visible history.\n- The loser is no longer treated as a live normal contact.\n- The merge reason is still understandable when reviewed later.\n\nPitfalls:\n- Do not merge on a weak fuzzy-name match alone.\n- Do not skip inspection of both sides before merging.\n- Do not assume merges are easy to conceptually undo just because the system preserves lineage.\n\nTips and tricks:\n- Prefer a winner that already carries the stronger routing identity and richer history unless another reason dominates.\n- If the system is uncertain, clarify first instead of forcing a merge.\n- Review merge history after sensitive merges to confirm the lineage chain is clean.\n\nTool calls to prefer:\n- `review_affinity` for duplicate-candidate discovery and merge-history review\n- `inspect_affinity_item` for both candidate contacts\n- `merge_contacts` only after confirmation\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `import-history-without-faking-evidence`" }, "tools": { "description": "Tool parameters, nested fields, and examples.", "content": '---\nname: affinity\ndescription: "Search and maintain local contacts, identities, relationships, events, commitments, and recurring dates. Use when recording relationship evidence or reviewing a social CRM."\nlicense: "MIT"\ncompatibility: "The release binary needs no runtime. MCP stdio or ghostpaw-affinity CLI."\n---\n\n# affinity\n\nAffinity is a local relationship model and social CRM. Search before creating contacts. Inspect before revising or merging. Record direct evidence honestly, keep identities and structural ties distinct from social links, and ask for clarification when a target is ambiguous. Review commitments and recurring dates regularly. Derived scores guide attention and do not prove relationship quality.\n\n## Calling tools\n\nMCP: connect to ghostpaw-affinity, then call a tool by name with its JSON arguments.\n\nCLI: `ghostpaw-affinity call <tool> \'<JSON object>\'` or `ghostpaw-affinity <tool> --parameter value` (without a global install: `npx -y @ghostpaw/affinity <tool> ...`).\n\nInspect schemas with `ghostpaw-affinity describe <tool>`. Use `--db <path>` to select a database.\n\n## search_affinity\n\nSearch 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.\n\nUse when:\n\nNot for:\n\nRead-only.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| scope | string | no | contacts, events, all. Search scope; defaults to contacts. |\n| query | string | yes | Free-text or natural-key query. |\n| kind | string | no | human, group, company, team, pet, service, other. Optional contact kind filter. |\n| lifecycleState | string | no | active, dormant, merged, lost. Optional lifecycle state filter. |\n| attribute | string / array | no | |\n| tag | string / array | no | |\n| includeLost | boolean | no | Include lost contacts (search excludes them by default). |\n| includeOwner | boolean | no | Whether owner contacts are included. |\n| includeDormant | boolean | no | Whether dormant contacts are included. |\n| limit | integer | no | Optional result limit. |\n| offset | integer | no | Optional result offset. |\n\n```sh\nghostpaw-affinity search "Jana"\n```\n\n## review_affinity\n\nReview one affinity surface such as contacts, journals, link queues, commitments, dates, graph state, or merge history.\n\nUse when:\n\nNot for:\n\nRead-only.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| ref | object | no | |\n| includeContent | boolean | no | Include verbatim content in event rows; defaults to false. |\n| channel | string | no | Filter events by channel. |\n| view | string | yes | contacts.list, contacts.duplicates, events.thread, events.contact_journal, events.link_timeline, events.moments, commitments.open, links.owner, links.observed, links.progression_readiness, links.radar, dates.upcoming, graph.chart, merges.history, merges.dismissed. Which review surface to return. |\n| now | integer / string | no | Optional current timestamp for overdue calculations. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| contact | object | no | Optional target contact locator. |\n| link | object | no | Optional target link locator. |\n| contactIds | array | no | Optional contact-id subset. |\n| kind | string | no | Optional contact kind or link kind filter, depending on the view. |\n| lifecycleState | string | no | active, dormant, merged, lost. Optional contact lifecycle-state filter. |\n| linkState | string | no | active, dormant, strained, broken, archived. Optional link state filter. |\n| attribute | string / array | no | |\n| tag | string / array | no | |\n| includeLost | boolean | no | Include lost contacts (search excludes them by default). |\n| includeOwner | boolean | no | Whether owner contacts are included. |\n| includeDormant | boolean | no | Whether dormant rows are included. |\n| includeArchived | boolean | no | Whether archived links are included. |\n| includeObserved | boolean | no | Whether observed links are included. |\n| eventTypes | array | no | Optional event-type filter. |\n| momentKind | string | no | breakthrough, rupture, reconciliation, milestone, turning_point. Optional moment kind filter. |\n| commitmentType | string | no | promise, agreement. Optional commitment type filter. |\n| recurrenceKind | string | no | birthday, anniversary, renewal, memorial, custom_yearly. Optional recurrence-kind filter. |\n| horizonDays | integer | no | Optional horizon-days filter. |\n| exactOnly | boolean | no | Whether duplicate review is exact-only. |\n| minScore | number | no | Optional duplicate minimum score. |\n| includeDismissed | boolean | no | Whether dismissed pairs are included in duplicate review (marked with dismissed flag). |\n| since | integer / string | no | Optional lower timestamp bound. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| until | integer / string | no | Optional upper timestamp bound. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| limit | integer | no | Optional result limit. |\n| offset | integer | no | Optional result offset. |\n| refSource | string | no | External reference source. |\n| refId | string | no | External reference id. |\n| withContent | boolean | no | Include verbatim event content. |\n| ref.source | string | yes | External source; trimmed and lowercased. |\n| ref.id | string | yes | Non-empty external id. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| link.linkId | integer | no | Exact link id. |\n| link.endpoints | object | no | Endpoint-based link locator. |\n\n```sh\nghostpaw-affinity review events.contact_journal --with-content --channel bluesky --contact \'{"contactId":2}\'\n```\n\n## inspect_affinity_item\n\nInspect one exact affinity item such as the owner profile, a contact profile, or one link detail.\n\nUse when:\n\nNot for:\n\nRead-only.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| kind | string | yes | event, owner_profile, contact_profile, link. What to inspect. |\n| eventId | integer | no | Exact event id. |\n| ref | object | no | |\n| contact | object | no | Target contact. Required when kind=contact_profile. Provide contactId or identity. |\n| link | object | no | Target link. Required when kind=link. Provide linkId or endpoints. |\n| topLinksLimit | integer | no | Optional top-links cap. |\n| recentEventsLimit | integer | no | Optional recent-events cap. |\n| refSource | string | no | External reference source. |\n| refId | string | no | External reference id. |\n| ref.source | string | yes | External source; trimmed and lowercased. |\n| ref.id | string | yes | Non-empty external id. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| link.linkId | integer | no | Exact link id. |\n| link.endpoints | object | no | Endpoint-based link locator. |\n\n```sh\nghostpaw-affinity show contact_profile --contact \'{"contactId":2}\'\n```\n\n## manage_contact\n\nCreate, 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 \u2014 duplicates corrupt the graph.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | create, revise, set_lifecycle, export, delete. Operation to perform. |\n| input | object | no | New contact payload. Required when action=create. |\n| confirm | boolean | no | Required true for permanent deletion. |\n| contact | object | no | Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity. |\n| patch | object | no | Allowed contact patch. Required when action=revise. |\n| lifecycleState | string | no | active, dormant, merged, lost. Target lifecycle state. Required when action=set_lifecycle. |\n| options | object | no | Optional lifecycle metadata. |\n| input.identities | array | no | Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return. |\n| input.ifExists | string | no | Identity collision policy; return attaches nothing. |\n| input.name | string | yes | Contact name. |\n| input.kind | string | yes | Contact kind. |\n| input.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.bootstrapOwner | boolean | no | Whether this contact should be the owner. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| patch.name | string | no | Optional replacement name. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n\n```sh\nghostpaw-affinity contact create \'{"name":"Fox","kind":"human","bootstrapOwner":true}\'\n```\n\n## merge_contacts\n\nMerge one loser contact into one winner contact using explicit contact locators.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| winner | object | yes | Winner contact. Provide contactId or identity. |\n| loser | object | yes | Loser contact. Provide contactId or identity. |\n| reasonSummary | string | no | Optional merge reason. |\n| now | integer / string | no | Optional merge timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| winner.contactId | integer | no | Exact contact id. |\n| winner.identity | object | no | Contact identity locator. |\n| loser.contactId | integer | no | Exact contact id. |\n| loser.identity | object | no | Contact identity locator. |\n\n```sh\nghostpaw-affinity contact merge --winner \'{"contactId":2}\' --loser \'{"contactId":8}\' --reason-summary "same person"\n```\n\n## manage_duplicate_dismissal\n\nDismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | dismiss, undismiss. Action to perform. |\n| left | object | yes | Left contact. Provide contactId or identity. |\n| right | object | yes | Right contact. Provide contactId or identity. |\n| reason | any | no | Optional reason for the dismissal. |\n| left.contactId | integer | no | Exact contact id. |\n| left.identity | object | no | Contact identity locator. |\n| right.contactId | integer | no | Exact contact id. |\n| right.identity | object | no | Contact identity locator. |\n\n```sh\nghostpaw-affinity duplicate dismiss --left \'{"contactId":5}\' --right \'{"contactId":10}\' --reason \'"different people"\'\n```\n\n## manage_identity\n\nAdd, revise, verify, or remove one routing identity on a contact. Use this for emails, phone numbers, social handles, and URLs \u2014 anything that could locate a person. Do not use manage_attribute for these.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | add, revise, verify, remove. Operation to perform. |\n| contact | object | no | Target contact. Required when action=add. Provide contactId or identity. |\n| input | object | no | New identity payload. Required when action=add. |\n| identityId | integer | no | Identity id. Required when action=revise, verify, or remove. |\n| patch | object | no | Identity patch. Required when action=revise. |\n| verifiedAt | integer / string | no | Verification timestamp. Optional for action=verify. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=remove. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| input.ifExists | string | no | Return an existing identity on this contact without changes. |\n| input.type | string | yes | Identity type: did, atproto_did, email, phone, url, website, handle, x, twitter, linkedin, github, mastodon, bluesky, telegram, whatsapp, signal, matrix, discord, slack, instagram, facebook, youtube, tiktok, address, other, account_id, alias, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen). |\n| input.value | string | yes | Identity value. |\n| input.label | string | no | Optional label. |\n| input.verified | boolean | no | Whether the identity starts verified. |\n| input.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| patch.type | string | no | Optional identity type: did, atproto_did, email, phone, url, website, handle, x, twitter, linkedin, github, mastodon, bluesky, telegram, whatsapp, signal, matrix, discord, slack, instagram, facebook, youtube, tiktok, address, other, account_id, alias, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen). |\n| patch.value | string | no | Optional identity value. |\n| patch.label | string | no | Optional label. |\n\n```sh\nghostpaw-affinity identity add --contact \'{"contactId":2}\' --input \'{"type":"email","value":"jana@x.de"}\'\n```\n\n## manage_relationship\n\nSeed social links, revise bond and state, and manage structural ties. Seed a link as soon as a relationship is mentioned \u2014 do not wait for repeated evidence.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | seed_social_link, revise_bond, override_state, set_structural_tie, remove_structural_tie. Operation to perform. |\n| from | object | no | Source contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity. |\n| to | object | no | Target contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity. |\n| kind | string | no | Link kind. Required for seed_social_link and set_structural_tie. Can be passed here or inside input. For seed_social_link: personal, family, professional, romantic, care, service, observed, other_relational. For set_structural_tie: works_at, manages, member_of, married_to, partner_of, parent_of, child_of, sibling_of, friend_of, client_of, vendor_of, reports_to, belongs_to, other_structural. |\n| role | string | no | Optional role for seed_social_link or set_structural_tie. Can be passed here or inside input. |\n| input | object | no | Full payload for seed_social_link or set_structural_tie. Alternative to top-level kind/role. |\n| link | object | no | Target link. Required when action=revise_bond, override_state, or remove_structural_tie. Provide linkId or endpoints. |\n| bond | any | no | Bond text. Required when action=revise_bond. Null to clear. |\n| state | string | no | active, dormant, strained, broken, archived. Link state. Required when action=override_state. |\n| options | object | no | Optional metadata. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=remove_structural_tie. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| from.contactId | integer | no | Exact contact id. |\n| from.identity | object | no | Contact identity locator. |\n| to.contactId | integer | no | Exact contact id. |\n| to.identity | object | no | Contact identity locator. |\n| link.linkId | integer | no | Exact link id. |\n| link.endpoints | object | no | Endpoint-based link locator. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n\n```sh\nghostpaw-affinity relationship tie --from \'{"contactId":2}\' --to \'{"contactId":3}\' --kind works_at --role Founder\n```\n\n## record_event\n\nRecord 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.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| kind | string | yes | interaction, observation, milestone, transaction. Event kind. |\n| input | object | yes | Event payload. Always required. Interaction events also require `type`. |\n| input.type | string | no | Interaction subtype. Required when kind=interaction, omit otherwise. |\n| input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for observations with significance <=2. |\n| input.content | string | no | Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied. |\n| input.externalRef | object | no | |\n| input.channel | string | no | Free-text channel, trimmed and lowercased, at most 40 characters. |\n| input.parentRef | object | no | |\n| input.parentEventId | integer | no | Parent event id. |\n| input.threadRef | object | no | |\n| input.momentKind | string | no | Optional moment kind. |\n| input.occurredOn | string | no | Calendar day. |\n| input.provenance | oneOf | no | |\n| input.occurredAt | oneOf | yes | When the event occurred, Unix timestamp in milliseconds. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.summary | string | no | Event summary. Must be non-empty. |\n| input.significance | integer | yes | Event significance from 1 to 10. |\n| input.participants | array | yes | Participant list. Must be non-empty and include the owner contact. |\n| input.now | oneOf | no | Optional timestamp override. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n\n```sh\nghostpaw-affinity event record --kind interaction --input \'{"type":"conversation","occurredAt":"today","summary":"Call re pilot","significance":6,"participants":[{"contactId":2,"role":"recipient","directionality":"mutual"}]}\'\n```\n\n## manage_commitment\n\nRecord or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | record, resolve. Operation to perform. |\n| input | object | no | Commitment payload. Required when action=record. |\n| commitmentEventId | integer | no | Commitment event id. Required when action=resolve. |\n| resolution | string | no | kept, cancelled, broken. How the commitment was resolved. Required when action=resolve. |\n| options | object | no | Optional metadata. |\n| input.commitmentType | string | yes | Promise or agreement. |\n| input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for observations with significance <=2. |\n| input.content | string | no | Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied. |\n| input.externalRef | object | no | |\n| input.channel | string | no | Free-text channel, trimmed and lowercased, at most 40 characters. |\n| input.parentRef | object | no | |\n| input.parentEventId | integer | no | Parent event id. |\n| input.threadRef | object | no | |\n| input.momentKind | string | no | Optional moment kind. |\n| input.occurredOn | string | no | Calendar day. |\n| input.provenance | oneOf | no | |\n| input.occurredAt | oneOf | yes | When the commitment was made, Unix timestamp in milliseconds. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.summary | string | no | Commitment summary. Must be non-empty. |\n| input.significance | integer | yes | Commitment significance from 1 to 10. |\n| input.dueAt | oneOf | no | Optional due timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.dueOn | string | no | Due calendar day. |\n| input.participants | array | yes | Participant list. Must be non-empty and include the owner contact. |\n| input.now | oneOf | no | Optional timestamp override. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| options.resolvingEvent | object | no | |\n\n```sh\nghostpaw-affinity commitment record --input \'{"commitmentType":"promise","occurredAt":"today","dueAt":"+3d","summary":"Send proposal","significance":6,"participants":[{"contactId":2,"role":"recipient"}]}\'\n```\n\n## manage_date_anchor\n\nAdd, revise, or remove recurring date anchors on contacts or links. Use this for birthdays, anniversaries, and yearly events \u2014 not manage_attribute.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | add, revise, remove. Operation to perform. |\n| input | object | no | Date anchor payload. Required when action=add. |\n| anchorEventId | integer | no | Anchor event id. Required when action=revise or remove. |\n| patch | object | no | Anchor patch. Required when action=revise. |\n| options | object | no | Optional metadata. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=remove. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.target | object | yes | Date-anchor target. |\n| input.recurrenceKind | string | yes | Recurrence kind. |\n| input.anchorMonth | integer | yes | Anchor month, 1\u201312. |\n| input.anchorDay | integer | yes | Anchor day, 1\u201331 (validated per month). |\n| input.summary | string | yes | Summary. Must be non-empty. |\n| input.significance | integer | yes | Significance from 1 to 10. |\n| input.now | oneOf | no | Optional timestamp override. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.force | boolean | no | Whether to bypass duplicate checks. |\n| patch.recurrenceKind | string | no | Optional recurrence kind. |\n| patch.anchorMonth | integer | no | Optional month, 1\u201312. |\n| patch.anchorDay | integer | no | Optional day, 1\u201331. |\n| patch.summary | string | no | Optional summary. |\n| patch.significance | integer | no | Optional significance, 1\u201310. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| options.force | boolean | no | Whether duplicate checks should be bypassed. |\n\n```sh\nghostpaw-affinity date add --input \'{"target":{"kind":"contact","contact":{"contactId":2}},"recurrenceKind":"birthday","anchorMonth":11,"anchorDay":3,"summary":"Jana bday","significance":4}\'\n```\n\n## manage_attribute\n\nSet, unset, or replace custom metadata attributes on contacts, links and events. For emails, phones, handles, and URLs use manage_identity instead \u2014 those are routing keys, not attributes.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | set, unset, replace. Operation to perform. |\n| target | object | no | Attribute target. Always required. |\n| name | string | no | Attribute name. Required when action=set or unset. |\n| value | any | no | Attribute value. Required when action=set. Null for presence/tag semantics. |\n| entries | array | no | Key-value map to replace all attributes. Required when action=replace. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=unset. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| target.kind | string | yes | Target kind. |\n| target.eventId | integer | no | Target event id. |\n| target.contact | object | no | Contact locator. Required when kind=contact. Provide contactId or identity. |\n| target.link | object | no | Link locator. Required when kind=link. Provide linkId or endpoints. |\n| entries.name | string | yes | Attribute name. |\n| entries.value | oneOf | no | Attribute value, or null for presence/tag semantics. |\n\n```sh\nghostpaw-affinity attribute set --target \'{"kind":"contact","contact":{"contactId":2}}\' --name pipeline_stage --value \'"proposal"\'\n```\n\n## manage_event\n\nRevise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | revise, delete. Action. |\n| eventId | integer | no | Event id; provide eventId or ref. |\n| ref | object | no | |\n| confirm | boolean | no | Must be true to delete. |\n| patch | object | no | Fields to revise; externalRef may only be added when absent. |\n| refSource | string | no | External reference source. |\n| refId | string | no | External reference id. |\n| ref.source | string | yes | External source; trimmed and lowercased. |\n| ref.id | string | yes | Non-empty external id. |\n| patch.summary | string | no | Short non-empty label. |\n| patch.content | string | no | Verbatim content, at most 1 MB UTF-8. |\n| patch.occurredAt | oneOf | no | Event timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| patch.significance | integer | no | Significance 1..10. |\n| patch.channel | string | no | Channel, at most 40 characters. |\n| patch.type | string | no | Type within the same event kind. |\n| patch.externalRef | object | no | |\n\n```sh\nghostpaw-affinity event revise --event-id 1 --patch \'{"summary":"Corrected"}\'\n```\n\n## Workflow guides\n\n- `ghostpaw-affinity skill bootstrap-and-cold-start`: Establish a clean Affinity baseline with one owner, one verified identity, and the minimum structure needed for later evidence and review work.\n- `ghostpaw-affinity skill identify-and-locate-contacts`: Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.\n- `ghostpaw-affinity skill import-history-without-faking-evidence`: Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.\n- `ghostpaw-affinity skill model-structure-orgs-and-households`: Model factual structure such as employment, membership, hierarchy, and household ties without confusing it with live relational mechanics.\n- `ghostpaw-affinity skill record-direct-evidence-well`: Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.\n- `ghostpaw-affinity skill record-observations-and-referrals`: Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.\n- `ghostpaw-affinity skill capture-transactions-and-commercial-events`: Capture commercially meaningful interactions as transactions so operational and service relationships accumulate the right evidence shape.\n- `ghostpaw-affinity skill manage-promises-and-agreements`: Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.\n- `ghostpaw-affinity skill manage-recurring-dates-and-reminders`: Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.\n- `ghostpaw-affinity skill review-radar-progression-and-graph`: Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.\n- `ghostpaw-affinity skill reconcile-duplicates-and-merge-safely`: Review duplicate candidates carefully and merge only after the identity, history, and structural consequences are understood.\n- `ghostpaw-affinity skill tools`: Tool parameters, nested fields, and examples.\n\n## Principles\n\n- Honor evidence before narrative.\n- Clarify before you act.\n- Keep truths in their proper layer.\n- Read derived signals with judgment.\n- Protect relationship integrity over time.\n- Follow the operational protocol.\n' } };
12321
+ affinitySkillReferences = { "bootstrap-and-cold-start": { "description": "Establish a clean Affinity baseline with one owner, one verified identity, and the minimum structure needed for later evidence and review work.", "content": '# Bootstrap And Cold Start\n\nPrimary tools:\n- `manage_contact`\n- `manage_identity`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Establish one clean owner record, make it reachable, and verify the system is ready for later relationship work.\n\nWhen to use:\n- The database is new or logically empty.\n- A harness needs to initialize its own operator profile before doing anything else.\n\nWhen not to use:\n- The owner already exists and you only need to update profile fields.\n- You are importing historical relationships; use the import-focused skill after bootstrap.\n\nStep-by-step sequence:\n1. Assume the schema is already initialized and the database is live before using any tool.\n2. Use `manage_contact` with `action: "create"` and `bootstrapOwner: true` exactly once for the operator.\n3. Immediately attach at least one routing identity with `manage_identity` so future lookups can use a stable natural key.\n4. Use `inspect_affinity_item` with `kind: "owner_profile"` to confirm that the owner profile exists and reads back cleanly.\n5. Use `review_affinity` with `view: "contacts.list"` to confirm that the portfolio is still minimal and no duplicate owner-like records were created during setup.\n\nValidation checks:\n- The owner profile loads successfully.\n- The owner has at least one live identity.\n- The contact list shows exactly the records you expect at this early stage.\n\nPitfalls:\n- Do not create a non-owner placeholder and plan to convert it later if the harness can bootstrap the real owner immediately.\n- Do not skip identity attachment, or later natural-key lookup will be weaker and duplicate-prone.\n- Do not seed relationships before the owner exists if later evidence is meant to be owner-facing.\n\nTips and tricks:\n- Prefer an identity that is stable across sessions, such as a primary email or canonical profile URL.\n- Keep the first dataset tiny. Bootstrap is for a trustworthy base, not a full import.\n- If setup feels ambiguous, inspect first and add less rather than more.\n\nTool calls to prefer:\n- `manage_contact` for owner creation\n- `manage_identity` for first routing key\n- `inspect_affinity_item` for confirmation\n- `review_affinity` for early portfolio sanity checks\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `import-history-without-faking-evidence`' }, "identify-and-locate-contacts": { "description": "Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.", "content": '# Identify And Locate Contacts\n\nHandle identities normalize leading @ and case. Bluesky uses a domain handle,\nX/Twitter uses 1\u201315 letters/digits/underscores, Mastodon uses user@host.tld.\nNative did (alias atproto_did) validates did:<method>:<identifier>; legacy\ncustom:atproto-did locators find the same identity. Keep old handles as identities.\nAn owned identity makes contact creation fail atomically with conflict and\nexistingContactId. input.ifExists="return" returns the matching contact and\nmatchedIdentities without attaching any free identities. CLI: --if-exists return.\nName-only possible duplicates still warn. Search @handle is always literal.\n\nPrimary tools:\n- `search_affinity`\n- `inspect_affinity_item`\n- `manage_identity`\n- `manage_contact`\n\nGoal:\n- Resolve the right contact before mutating anything, using the strongest available locator.\n\nWhen to use:\n- A harness receives a name, email, phone number, URL, or ambiguous reference to a person or organization.\n- You need to confirm whether a contact already exists before creating a new one.\n\nWhen not to use:\n- You already have one exact internal id from a prior successful tool result.\n- You are reviewing a list surface rather than identifying one exact target.\n\nStep-by-step sequence:\n1. Start with `search_affinity` whenever the target might already exist.\n2. If you have a strong natural key such as email, phone, or URL-like handle, use that query first rather than a vague display name.\n3. If search returns one strong candidate, use `inspect_affinity_item` to confirm the profile before mutating.\n4. If the target does not exist, create it with `manage_contact`.\n5. Immediately add the best routing key with `manage_identity` so future lookup becomes exact rather than fuzzy.\n6. If the system returns ambiguity, stop and clarify instead of guessing.\n\nValidation checks:\n- The inspected profile matches the intended person or organization.\n- The best known routing identity is attached and, when appropriate, verified.\n- Later searches by that identity return the same contact cleanly.\n\nPitfalls:\n- Do not trust names alone when a natural key is available.\n- Do not create a second contact just because search returned multiple plausible matches.\n- Do not remove or revise identities casually; they are your best future locator surface.\n\nTips and tricks:\n- Prefer exact identities over integer ids when the harness needs to survive across sessions or partial memory loss.\n- After creating a contact from a vague mention, upgrade it with a real identity as soon as possible.\n- If multiple weak matches appear, inspect before mutating.\n\nTool calls to prefer:\n- `search_affinity` for candidate discovery\n- `inspect_affinity_item` for exact confirmation\n- `manage_identity` for routing-key upgrades\n- `manage_contact` only after lookup fails honestly\n\nRelated skills:\n- `bootstrap-and-cold-start`\n- `reconcile-duplicates-and-merge-safely`' }, "import-history-without-faking-evidence": { "description": "Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.", "content": '# Import History Without Faking Evidence\n\nFor social imports, use native did identities (atproto_did is an alias; legacy\ncustom:atproto-did locators remain equivalent). Create with input.ifExists="return"\nfor safe retries: this attaches nothing to an existing contact. For handle changes,\ncall manage_identity add separately with input.ifExists="return"; an identity already\non that contact is a no-op, while another owner still conflicts. Seed links with\ninput.ifExists="return" to reuse them without changing their state.\nUse ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or\n{op:"event",kind:"interaction",input:{...}}. Per-line savepoints isolate failures;\n--stop-on-error stops early. Exit 2 identifies malformed JSON; other failures exit 1.\nUse externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.\nParticipant contact locators may use identities, including did, instead of ids.\nRead JSON flags from stdin with --input - or from @file for large payloads (E2BIG).\nSQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.\nMissing inbound-follow timestamps should use a known first_seen or be counted as skipped.\nThe synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs\nshow the mapping and verify an idempotent retry and byte-identical text.\n\nFor archived messages with known timestamps, record actual evidence with externalRef:\n{source, id} deduplicates retries across event families and commitments. Keep original\nmessage text in content, use channel and parentRef/threadRef for context, and keep\nsummary short or let it derive from content. Content is limited to 1 MB UTF-8;\nkeep larger raw archives in the source system. Inspect events by external reference,\nsearch scope "events", and use manage_event revise/delete to correct imports.\nSee the tools guide for field details. Existing 0.5.x databases upgrade in place.\n\nPrimary tools:\n- `manage_contact`\n- `manage_identity`\n- `manage_relationship`\n- `inspect_affinity_item`\n\nGoal:\n- Represent real pre-system relationship state without fabricating a timeline of events that never existed inside Affinity.\n\nWhen to use:\n- You are migrating from another CRM, notes system, or memory set.\n- You already know a relationship exists and roughly how strong or active it is, but you do not want to reconstruct every past interaction.\n\nWhen not to use:\n- A new event just happened and should affect mechanics now.\n- You are recording a specific conversation, milestone, transaction, or observation.\n\nStep-by-step sequence:\n1. Create or locate the relevant contacts first.\n2. Add the strongest identities you know so the imported graph stays addressable later.\n3. Use `manage_relationship` with `action: "seed_social_link"` to establish a relational link that already existed before the system.\n4. Set only the fields you can justify: kind, rough rank, trust, cadence, state, and optional bond.\n5. Inspect the result with `inspect_affinity_item` if you need to verify the exact post-import state.\n6. From that point forward, switch to evidence tools for all new real-world activity.\n\nValidation checks:\n- The imported contact pair now has the expected relational link.\n- The seeded state looks plausible when inspected.\n- Later direct events continue progression from the seeded baseline rather than from zero.\n\nPitfalls:\n- Do not backfill dozens of fake conversations just to reach a desired rank.\n- Do not use import seeding for ordinary day-to-day relationship maintenance.\n- Do not overstate confidence on rank or trust when the historical truth is actually vague.\n\nTips and tricks:\n- Seed conservatively if you are unsure. It is safer to underclaim and let real future evidence move the link.\n- Use bond text sparingly as a narrative reminder of why the imported state is what it is.\n- Prefer one honest seed over many synthetic event reconstructions.\n\nTool calls to prefer:\n- `manage_relationship` with `seed_social_link`\n- `manage_contact` and `manage_identity` before seeding\n- `inspect_affinity_item` for post-import verification\n\nRelated skills:\n- `bootstrap-and-cold-start`\n- `record-direct-evidence-well`' }, "model-structure-orgs-and-households": { "description": "Model factual structure such as employment, membership, hierarchy, and household ties without confusing it with live relational mechanics.", "content": '# Model Structure, Orgs, And Households\n\nPrimary tools:\n- `manage_contact`\n- `manage_relationship`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Capture the stable factual skeleton of the world: who belongs where, who reports to whom, and what entities are structurally connected.\n\nWhen to use:\n- You need to represent employment, family structure, team membership, households, or service affiliation.\n- The fact should exist even if no emotional or operational relationship score is implied.\n\nWhen not to use:\n- You want to express how strong, strained, warm, or active a relationship feels.\n- You are recording something that happened in time.\n\nStep-by-step sequence:\n1. Create or locate the entities involved.\n2. Decide whether the fact is structural rather than relational. If the answer survives even with zero interaction history, it is usually structural.\n3. Use `manage_relationship` with `action: "set_structural_tie"` to record the factual tie.\n4. Include a role when the role clarifies the structure, such as job title or household role.\n5. Inspect relevant profiles or review lists after large structural batches to confirm the world model is clean.\n6. Remove or replace outdated ties when the world changes, such as a job move or team change.\n\nValidation checks:\n- The structural tie is visible and stable.\n- No fake progression fields were introduced just to represent a factual tie.\n- The chart and review surfaces now reflect the world structure you expect.\n\nPitfalls:\n- Do not use structural ties to stand in for closeness, trust, or cadence.\n- Do not seed social links just because a structural tie exists; only do that if a live relational track also matters.\n- Do not leave outdated structural ties hanging after the factual world changes.\n\nTips and tricks:\n- Structural and relational links can coexist. Use both when both truths matter.\n- Groups, households, companies, teams, and services often deserve their own contacts.\n- If you are unsure, ask whether the statement is about belonging or about lived relationship quality.\n\nTool calls to prefer:\n- `manage_relationship` with `set_structural_tie`\n- `inspect_affinity_item` to verify exact targets\n- `review_affinity` for broader structural review\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `record-direct-evidence-well`' }, "record-direct-evidence-well": { "description": "Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.", "content": '# Record Direct Evidence Well\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d). Date-only inputs\npreserve the calendar day at UTC midnight (UTC end of day for dueAt).\nKeywords use the process local date. Read occurredOn/dueOn and review occursOn\nfor calendar dates; *Iso fields remain UTC. Future events carry future_event\nwarnings and do not count for recency, cadence or drift until their time and day\narrive. Commitments and resolutions never reset contact recency or cadence.\n\nImported text and event maintenance:\n- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.\n- Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.\n- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.\n- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.\n- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.\n- Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.\n- Use manage_event delete with confirm: true to remove incorrect evidence and recompute affected relationship scores.\n- Send large payloads through whole-payload stdin or @file rather than argv. For example, affinity event record < payload.json or affinity call record_event - < payload.json.\n\nPrimary tools:\n- `record_event`\n- `inspect_affinity_item`\n- `review_affinity`\n- `manage_relationship`\n\nGoal:\n- Capture what really happened between the owner and other contacts so Affinity can derive progression, trust, cadence, and moments cleanly.\n\nWhen to use:\n- A conversation, activity, support exchange, conflict, correction, milestone, or direct operational interaction just happened.\n- You want future radar, progression, journal, and chart views to reflect the event truthfully.\n\nWhen not to use:\n- You are importing pre-system history.\n- You only observed third parties and were not directly part of the relationship evidence.\n\nStep-by-step sequence:\n1. Confirm the participants first. Use exact contacts whenever possible.\n2. Use `record_event` with the smallest honest event family: `interaction` for ordinary direct evidence, `milestone` when the event is truly milestone-shaped, or another event family only when it better matches reality.\n3. Set significance conservatively. Use high significance only when the event genuinely changed the relationship.\n4. Let the system auto-create owner-facing links when appropriate instead of manually seeding a link just because a direct event happened.\n5. Review the receipt or inspect the resulting profile when you need to understand the effect.\n6. Use `review_affinity` on journal, timeline, moments, or readiness views when you need context after a sequence of events.\n\nValidation checks:\n- The right contacts were involved.\n- The event family matches what really happened.\n- The resulting mechanics feel plausible when reviewed.\n\nPitfalls:\n- Do not inflate significance to force a rank-up.\n- Do not turn ordinary direct evidence into a milestone just to get a dramatic moment.\n- Do not use bond text as evidence. If the bond changed but no event happened, revise the bond separately.\n- Do not guess through ambiguity; clarify the target first.\n\nTips and tricks:\n- Conflict and correction are both first-class direct evidence. Use them honestly rather than hiding them inside generic summaries.\n- Multi-participant owner events can refresh multiple links at once. That is a feature, not a bug.\n- If you need to label the current narrative reading without changing mechanics, use `manage_relationship` with `revise_bond` after the evidence is recorded.\n\nTool calls to prefer:\n- `record_event` for the evidence itself\n- `inspect_affinity_item` for exact read-back\n- `review_affinity` for journals, timelines, moments, and readiness\n- `manage_relationship` only for bond interpretation after the fact\n\nRelated skills:\n- `record-observations-and-referrals`\n- `review-radar-progression-and-graph`' }, "record-observations-and-referrals": { "description": "Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.", "content": '# Record Observations And Referrals\n\nLikes, reposts and follows are ambient observations, not milestones. Record them\nwith affectsBond:false. Like/follow/repost labels default to false across families;\nexplicit true opts in. Observations with significance <=2 also default to false.\nThey stay in journals but do not change affinity or meaningful-contact recency.\nprovenance.ambient:true also suppresses effects. Use explicit owner and other\nparticipants, actor/recipient roles, and the correct directionality.\nUnanswered owner_initiated evidence contributes much less and does not reset\nradar recency. A relationship with only outbound activity gets no routine check-in\nor drift prompt. other_initiated evidence counts fully. Observations and follow-like\nmilestones never derive breakthroughs; low-significance milestones derive no moment.\n\nPrimary tools:\n- `record_event`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Add truthful third-party graph information while keeping observational evidence weaker than direct relationship evidence.\n\nWhen to use:\n- You learned that two people know each other, one person referred another, or you observed a third-party relationship pattern.\n- You want the graph to reflect world truth without pretending you had the direct interaction yourself.\n\nWhen not to use:\n- The owner directly participated in the event and should record direct evidence instead.\n- You merely suspect a relationship with no strong enough fact to justify recording it.\n\nStep-by-step sequence:\n1. Confirm the third-party contacts you are referring to.\n2. Use `record_event` with `kind: "observation"` when the truth is observational rather than direct.\n3. Keep significance modest unless the observed fact is genuinely consequential.\n4. After recording, use `review_affinity` to inspect observed-link or graph views and verify the resulting topology.\n5. If later direct evidence exists, record that separately rather than expecting the observation to do all the work.\n\nValidation checks:\n- The graph now reflects the third-party fact you meant to capture.\n- Observed links stay appropriately weaker than direct links.\n- No accidental owner-facing intimacy was invented.\n\nPitfalls:\n- Do not use observations to fake deep trust or rank.\n- Do not create a clique from vague group knowledge when the system only has justified pairwise facts.\n- Do not promote a rumor to structure or direct evidence without better confirmation.\n\nTips and tricks:\n- Referral chains are excellent observational facts because they help later review and graph interpretation.\n- Use chart and observed-link review views together after batches of observation work.\n- Observations are often better than silence, but they should remain humble.\n\nTool calls to prefer:\n- `record_event` with `observation`\n- `review_affinity` with observed-link and graph views\n- `inspect_affinity_item` if you need exact follow-up on one target\n\nRelated skills:\n- `record-direct-evidence-well`\n- `review-radar-progression-and-graph`' }, "capture-transactions-and-commercial-events": { "description": "Capture commercially meaningful interactions as transactions so operational and service relationships accumulate the right evidence shape.", "content": '# Capture Transactions And Commercial Events\n\nPrimary tools:\n- `record_event`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Represent deals, placements, closings, sponsorships, and operationally meaningful exchanges as transactions instead of flattening them into generic social notes.\n\nWhen to use:\n- Money, deliverables, placements, closings, or other commercially meaningful exchanges just occurred.\n- The relationship is service-oriented, professional, or business-critical.\n\nWhen not to use:\n- The main truth is a personal conversation or ordinary activity.\n- The important action is really a promise or agreement that still needs later resolution.\n\nStep-by-step sequence:\n1. Confirm the right counterparties first.\n2. Use `record_event` with `kind: "transaction"` for the commercial event itself.\n3. Keep the summary concrete enough that future review explains what was exchanged and why it mattered.\n4. Use significance according to the real business weight, not just enthusiasm.\n5. Review the affected relationship if the commercial event should meaningfully change trust, cadence, or graph visibility.\n\nValidation checks:\n- The transaction now appears in the relevant timeline or journal views.\n- The resulting relationship effect matches the commercial importance of the event.\n- Later maintenance views now have the right recency signal.\n\nPitfalls:\n- Do not bury closings, placements, or sponsorship deliveries inside generic conversations.\n- Do not record a transaction when the truth is still only a pending agreement.\n- Do not expect transactions to create the same kind of affinity movement as rich direct social evidence.\n\nTips and tricks:\n- A relationship can be commercially strong and emotionally shallow. That is normal.\n- Use transactions to preserve crisp operational history for later portfolio review.\n- Follow major transactions with review views to confirm the graph and maintenance surfaces updated as expected.\n\nTool calls to prefer:\n- `record_event` with `transaction`\n- `inspect_affinity_item` for exact relationship read-back\n- `review_affinity` for owner-link portfolio review\n\nRelated skills:\n- `manage-promises-and-agreements`\n- `review-radar-progression-and-graph`' }, "manage-promises-and-agreements": { "description": "Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.", "content": '# Manage Promises And Agreements\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d).\nDate-only values keep their day with UTC boundaries; keywords use the process\nlocal date. Read occurredOn/dueOn and review occursOn for calendar dates;\n*Iso fields stay UTC. Future events warn and count as contact only when their\ntime and day arrive. Commitments and resolutions never reset recency/cadence.\nRadar explains overdue promises and drift (gap > 1.5 times cadence).\n\nPrimary tools:\n- `manage_commitment`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Keep promises and agreements visible, resolvable, and mechanically meaningful instead of letting them disappear into narrative history.\n\nWhen to use:\n- Someone committed to doing something later.\n- A promise, agreement, or operational obligation needs follow-through.\n\nWhen not to use:\n- The event is complete already and no unresolved obligation exists.\n- A date is recurring and belongs in the date-anchor surface instead.\n\nStep-by-step sequence:\n1. When the commitment is made, use `manage_commitment` with `action: "record"`.\n2. Choose `promise` vs `agreement` honestly rather than treating them as stylistic synonyms.\n3. Include a due time when the obligation has a meaningful time boundary.\n4. Use `review_affinity` with the open-commitment view during regular review cycles.\n5. When the outcome is known, resolve it with `manage_commitment` as `kept`, `cancelled`, or `broken`.\n6. Inspect the relevant profile or review the relevant queue again if you need to confirm closure.\n\nValidation checks:\n- Open commitments appear when they should and disappear when resolved.\n- Resolution state matches real-world truth.\n- Trust effects feel consistent with whether the promise was kept or broken.\n\nPitfalls:\n- Do not hide real obligations inside ordinary event summaries.\n- Do not resolve a commitment optimistically before the outcome is actually known.\n- Do not misuse `cancelled` when the truth is actually `broken`.\n\nTips and tricks:\n- If an obligation matters enough to disappoint someone when missed, record it.\n- Review open commitments regularly; the value of the system drops fast if this queue goes stale.\n- When a commitment resolution also deserves narrative context, pair the resolution with an appropriate event in the surrounding workflow.\n\nTool calls to prefer:\n- `manage_commitment` for record and resolve\n- `review_affinity` for the open-commitment queue\n- `inspect_affinity_item` when you need exact context on the affected relationship\n\nRelated skills:\n- `capture-transactions-and-commercial-events`\n- `manage-recurring-dates-and-reminders`' }, "manage-recurring-dates-and-reminders": { "description": "Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.", "content": '# Manage Recurring Dates And Reminders\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d).\nDate-only values keep their day with UTC boundaries; keywords use the process\nlocal date. Read occurredOn/dueOn and review occursOn for calendar dates;\n*Iso fields stay UTC. Future events warn and count as contact only when their\ntime and day arrive. Commitments and resolutions never reset recency/cadence.\nRadar explains overdue promises and drift (gap > 1.5 times cadence).\n\nPrimary tools:\n- `manage_date_anchor`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Materialize recurring important dates into a durable reminder surface that later review and relationship maintenance can trust.\n\nWhen to use:\n- A date recurs yearly and matters to a contact or link.\n- You want reminders for birthdays, anniversaries, renewals, memorials, or other yearly anchors.\n\nWhen not to use:\n- The date is a one-off event.\n- The fact is a preference or metadata note rather than a recurring anchor.\n\nStep-by-step sequence:\n1. Confirm the target contact or link first.\n2. Use `manage_date_anchor` with `action: "add"` and the right recurrence kind.\n3. Keep the summary operator-friendly so future reminders are immediately understandable.\n4. Use `review_affinity` with the upcoming-dates view and an explicit horizon during regular review work.\n5. Revise anchors when recurrence details change.\n6. Remove anchors when the reminder surface should no longer treat the date as active.\n\nValidation checks:\n- The anchor appears in upcoming-date review when it falls within the chosen horizon.\n- Duplicate anchors are not silently piling up.\n- The summary and significance are still meaningful for later action.\n\nPitfalls:\n- Do not store important recurring dates only as loose attributes.\n- Do not create duplicate yearly anchors for the same target and meaning.\n- Do not use recurring anchors for commitments; those belong in the commitment flow.\n\nTips and tricks:\n- Tune horizons by workflow: short for weekly review, longer for quarterly planning.\n- Use significance to help order upcoming-date review meaningfully.\n- Link-level anchors are valuable when the date belongs to a relationship rather than one person.\n\nTool calls to prefer:\n- `manage_date_anchor` for add, revise, and remove\n- `review_affinity` with the upcoming-dates view\n- `inspect_affinity_item` when you need exact context on the target\n\nRelated skills:\n- `manage-promises-and-agreements`\n- `review-radar-progression-and-graph`' }, "review-radar-progression-and-graph": { "description": "Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.", "content": "# Review Radar, Progression, And Graph\n\nList reviews accept limit (default 50, maximum 250) and offset. Results include\ntotal and hasMore; continue paging while hasMore is true. Event type array flags\naccept JSON arrays, comma lists, or repeated --event-types flags; invalid types\nreport allowed values with exit 2. Use skill --short for a compact CLI reference.\n\nCLI and MCP share epoch ms, ISO with Z/offset, YYYY-MM-DD, DD.MM.YYYY,\ntoday/yesterday/tomorrow and signed day offsets (+3d/-2d).\nDate-only values keep their day with UTC boundaries; keywords use the process\nlocal date. Read occurredOn/dueOn and review occursOn for calendar dates;\n*Iso fields stay UTC. Future events warn and count as contact only when their\ntime and day arrive. Commitments and resolutions never reset recency/cadence.\nRadar explains overdue promises and drift (gap > 1.5 times cadence).\n\nPrimary tools:\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Turn review surfaces into a disciplined operating loop for outreach, prioritization, and graph understanding.\n\nWhen to use:\n- You need a daily or weekly maintenance pass.\n- You want to know who is drifting, who is ready for deeper investment, or how the network is structured.\n\nWhen not to use:\n- You already know the one exact item you need to inspect in depth.\n- You are mutating state and only need confirmation of a single write result.\n\nStep-by-step sequence:\n1. Start with `review_affinity` on the highest-level view that matches the question: radar, owner links, progression readiness, graph, moments, or a timeline view.\n2. Use radar when the question is who needs attention now.\n3. Use progression readiness when the question is where additional positive investment is most likely to deepen a relationship.\n4. Use graph review when the question is network shape, bridges, clusters, or hidden centrality.\n5. Use journal or timeline views when a score needs narrative context before action.\n6. Escalate to `inspect_affinity_item` only after the review surface identifies the exact target worth opening deeply.\n\nValidation checks:\n- The chosen review surface actually matches the decision you are trying to make.\n- You inspected context before acting on a surprising score.\n- Follow-up work targets the right contact or link rather than the loudest metric.\n\nPitfalls:\n- Do not treat radar as a command to act blindly without context.\n- Do not confuse progression readiness with relationship importance; they answer different questions.\n- Do not rely on one score when a timeline or graph view would explain the situation better.\n\nTips and tricks:\n- Review flows are strongest when used rhythmically rather than sporadically.\n- Use graph review to understand bridges and clusters, then use timeline or profile inspection for the exact story behind them.\n- Empty results can still be informative: they may confirm that maintenance debt is currently low.\n\nTool calls to prefer:\n- `review_affinity` for all dashboard-style surfaces\n- `inspect_affinity_item` for exact deep follow-up\n\nRelated skills:\n- `record-direct-evidence-well`\n- `manage-recurring-dates-and-reminders`\n- `reconcile-duplicates-and-merge-safely`" }, "reconcile-duplicates-and-merge-safely": { "description": "Review duplicate candidates carefully and merge only after the identity, history, and structural consequences are understood.", "content": "# Reconcile Duplicates And Merge Safely\n\nPrimary tools:\n- `review_affinity`\n- `inspect_affinity_item`\n- `merge_contacts`\n\nGoal:\n- Collapse true duplicates without losing lineage, identities, participants, or relationship truth.\n\nWhen to use:\n- Duplicate-candidate review surfaces indicate a likely duplicate.\n- A harness or operator has strong evidence that two contacts are the same real entity.\n\nWhen not to use:\n- The match is still speculative and unresolved.\n- The real need is just better identities or labels rather than an actual merge.\n\nStep-by-step sequence:\n1. Start with `review_affinity` on the duplicate-candidate view.\n2. Inspect both candidate contacts before merging.\n3. Confirm the winner explicitly rather than letting recency or convenience decide by accident.\n4. Use `merge_contacts` only when the identity and history picture is clear.\n5. Inspect the surviving contact after the merge to confirm the merged state looks right.\n6. Review merge history when you need lineage confirmation later.\n\nValidation checks:\n- The surviving contact now contains the expected identities and visible history.\n- The loser is no longer treated as a live normal contact.\n- The merge reason is still understandable when reviewed later.\n\nPitfalls:\n- Do not merge on a weak fuzzy-name match alone.\n- Do not skip inspection of both sides before merging.\n- Do not assume merges are easy to conceptually undo just because the system preserves lineage.\n\nTips and tricks:\n- Prefer a winner that already carries the stronger routing identity and richer history unless another reason dominates.\n- If the system is uncertain, clarify first instead of forcing a merge.\n- Review merge history after sensitive merges to confirm the lineage chain is clean.\n\nTool calls to prefer:\n- `review_affinity` for duplicate-candidate discovery and merge-history review\n- `inspect_affinity_item` for both candidate contacts\n- `merge_contacts` only after confirmation\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `import-history-without-faking-evidence`" }, "tools": { "description": "Tool parameters, nested fields, and examples.", "content": '---\nname: affinity\ndescription: "Search and maintain local contacts, identities, relationships, events, commitments, and recurring dates. Use when recording relationship evidence or reviewing a social CRM."\nlicense: "MIT"\ncompatibility: "The release binary needs no runtime. MCP stdio or ghostpaw-affinity CLI."\n---\n\n# affinity\n\nAffinity is a local relationship model and social CRM. Search before creating contacts. Inspect before revising or merging. Record direct evidence honestly, keep identities and structural ties distinct from social links, and ask for clarification when a target is ambiguous. Review commitments and recurring dates regularly. Derived scores guide attention and do not prove relationship quality.\n\n## Calling tools\n\nMCP: connect to ghostpaw-affinity, then call a tool by name with its JSON arguments.\n\nCLI: `ghostpaw-affinity call <tool> \'<JSON object>\'` or `ghostpaw-affinity <tool> --parameter value` (without a global install: `npx -y @ghostpaw/affinity <tool> ...`).\n\nInspect schemas with `ghostpaw-affinity describe <tool>`. Use `--db <path>` to select a database.\n\n## search_affinity\n\nSearch 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.\n\nUse when:\n\nNot for:\n\nRead-only.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| scope | string | no | contacts, events, all. Search scope; defaults to contacts. |\n| query | string | yes | Free-text or natural-key query. |\n| kind | string | no | human, group, company, team, pet, service, other. Optional contact kind filter. |\n| lifecycleState | string | no | active, dormant, merged, lost. Optional lifecycle state filter. |\n| attribute | string / array | no | |\n| tag | string / array | no | |\n| includeLost | boolean | no | Include lost contacts (search excludes them by default). |\n| includeOwner | boolean | no | Whether owner contacts are included. |\n| includeDormant | boolean | no | Whether dormant contacts are included. |\n| limit | integer | no | Optional result limit. |\n| offset | integer | no | Optional result offset. |\n\n```sh\nghostpaw-affinity search "Jana"\n```\n\n## review_affinity\n\nReview one affinity surface such as contacts, journals, link queues, commitments, dates, graph state, or merge history.\n\nUse when:\n\nNot for:\n\nRead-only.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| ref | object | no | |\n| includeContent | boolean | no | Deprecated alias for --with-content; includes verbatim event content. |\n| channel | string | no | Filter events by channel. |\n| view | string | yes | contacts.list, contacts.duplicates, events.thread, events.contact_journal, events.link_timeline, events.moments, commitments.open, links.owner, links.observed, links.progression_readiness, links.radar, dates.upcoming, graph.chart, merges.history, merges.dismissed. Which review surface to return. |\n| now | integer / string | no | Optional current timestamp for overdue calculations. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| contact | object | no | Optional target contact locator. |\n| link | object | no | Optional target link locator. |\n| contactIds | array | no | Optional contact-id subset. |\n| kind | string | no | Optional contact kind or link kind filter, depending on the view. |\n| lifecycleState | string | no | active, dormant, merged, lost. Optional contact lifecycle-state filter. |\n| linkState | string | no | active, dormant, strained, broken, archived. Optional link state filter. |\n| attribute | string / array | no | |\n| tag | string / array | no | |\n| includeLost | boolean | no | Include lost contacts (search excludes them by default). |\n| includeOwner | boolean | no | Whether owner contacts are included. |\n| includeDormant | boolean | no | Whether dormant rows are included. |\n| includeArchived | boolean | no | Whether archived links are included. |\n| includeObserved | boolean | no | Whether observed links are included. |\n| eventTypes | array | no | Optional event-type filter. |\n| momentKind | string | no | breakthrough, rupture, reconciliation, milestone, turning_point. Optional moment kind filter. |\n| commitmentType | string | no | promise, agreement. Optional commitment type filter. |\n| recurrenceKind | string | no | birthday, anniversary, renewal, memorial, custom_yearly. Optional recurrence-kind filter. |\n| horizonDays | integer | no | Optional horizon-days filter. |\n| exactOnly | boolean | no | Whether duplicate review is exact-only. |\n| minScore | number | no | Optional duplicate minimum score (default 0.5; use 0 for all candidates). |\n| includeDismissed | boolean | no | Whether dismissed pairs are included in duplicate review (marked with dismissed flag). |\n| since | integer / string | no | Optional lower timestamp bound. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| until | integer / string | no | Optional upper timestamp bound. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| limit | integer | no | Optional result limit, 1..10000 (default 50); use offset for paging. |\n| offset | integer | no | Optional result offset. |\n| refSource | string | no | External reference source. |\n| refId | string | no | External reference id. |\n| withContent | boolean | no | Include verbatim event content. |\n| ref.source | string | yes | External source; trimmed and lowercased. |\n| ref.id | string | yes | Non-empty external id. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| link.linkId | integer | no | Exact link id. |\n| link.endpoints | object | no | Endpoint-based link locator. |\n\n```sh\nghostpaw-affinity review events.contact_journal --with-content --channel bluesky --contact \'{"contactId":2}\'\n```\n\n## inspect_affinity_item\n\nInspect one exact affinity item such as the owner profile, a contact profile, or one link detail.\n\nUse when:\n\nNot for:\n\nRead-only.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| kind | string | yes | event, owner_profile, contact_profile, link. What to inspect. |\n| eventId | integer | no | Exact event id. |\n| ref | object | no | |\n| contact | object | no | Target contact. Required when kind=contact_profile. Provide contactId or identity. |\n| link | object | no | Target link. Required when kind=link. Provide linkId or endpoints. |\n| topLinksLimit | integer | no | Optional top-links cap. |\n| recentEventsLimit | integer | no | Optional recent-events cap. |\n| refSource | string | no | External reference source. |\n| refId | string | no | External reference id. |\n| ref.source | string | yes | External source; trimmed and lowercased. |\n| ref.id | string | yes | Non-empty external id. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| link.linkId | integer | no | Exact link id. |\n| link.endpoints | object | no | Endpoint-based link locator. |\n\n```sh\nghostpaw-affinity show contact_profile --contact \'{"contactId":2}\'\n```\n\n## manage_contact\n\nCreate, 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 \u2014 duplicates corrupt the graph.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | create, revise, set_lifecycle, export, delete. Operation to perform. |\n| input | object | no | New contact payload. Required when action=create. |\n| confirm | boolean | no | Required true for permanent deletion. |\n| contact | object | no | Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity. |\n| patch | object | no | Allowed contact patch. Required when action=revise. |\n| lifecycleState | string | no | active, dormant, merged, lost. Target lifecycle state. Required when action=set_lifecycle. |\n| options | object | no | Optional lifecycle metadata. |\n| input.identities | array | no | Optional identities to attach. An identity owned by another live contact causes a conflict, or returns that contact when ifExists=return. |\n| input.ifExists | string | no | Identity collision policy; return attaches nothing. |\n| input.name | string | yes | Contact name. |\n| input.kind | string | yes | Contact kind. |\n| input.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.bootstrapOwner | boolean | no | Whether this contact should be the owner. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| patch.name | string | no | Optional replacement name. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n\n```sh\nghostpaw-affinity contact create \'{"name":"Fox","kind":"human","bootstrapOwner":true}\'\n```\n\n## merge_contacts\n\nMerge one loser contact into one winner contact using explicit contact locators.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| winner | object | yes | Winner contact. Provide contactId or identity. |\n| loser | object | yes | Loser contact. Provide contactId or identity. |\n| reasonSummary | string | no | Optional merge reason. |\n| now | integer / string | no | Optional merge timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| winner.contactId | integer | no | Exact contact id. |\n| winner.identity | object | no | Contact identity locator. |\n| loser.contactId | integer | no | Exact contact id. |\n| loser.identity | object | no | Contact identity locator. |\n\n```sh\nghostpaw-affinity contact merge --winner \'{"contactId":2}\' --loser \'{"contactId":8}\' --reason-summary "same person"\n```\n\n## manage_duplicate_dismissal\n\nDismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | dismiss, undismiss. Action to perform. |\n| left | object | yes | Left contact. Provide contactId or identity. |\n| right | object | yes | Right contact. Provide contactId or identity. |\n| reason | any | no | Optional reason for the dismissal. |\n| left.contactId | integer | no | Exact contact id. |\n| left.identity | object | no | Contact identity locator. |\n| right.contactId | integer | no | Exact contact id. |\n| right.identity | object | no | Contact identity locator. |\n\n```sh\nghostpaw-affinity duplicate dismiss --left \'{"contactId":5}\' --right \'{"contactId":10}\' --reason \'"different people"\'\n```\n\n## manage_identity\n\nAdd, revise, verify, or remove one routing identity on a contact. Use this for emails, phone numbers, social handles, and URLs \u2014 anything that could locate a person. Do not use manage_attribute for these.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | add, revise, verify, remove. Operation to perform. |\n| contact | object | no | Target contact. Required when action=add. Provide contactId or identity. |\n| input | object | no | New identity payload. Required when action=add. |\n| identityId | integer | no | Identity id. Required when action=revise, verify, or remove. |\n| patch | object | no | Identity patch. Required when action=revise. |\n| verifiedAt | integer / string | no | Verification timestamp. Optional for action=verify. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=remove. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| contact.contactId | integer | no | Exact contact id. |\n| contact.identity | object | no | Contact identity locator. |\n| input.ifExists | string | no | Return an existing identity on this contact without changes. |\n| input.type | string | yes | Identity type: did, atproto_did, email, phone, url, website, handle, x, twitter, linkedin, github, mastodon, bluesky, telegram, whatsapp, signal, matrix, discord, slack, instagram, facebook, youtube, tiktok, address, other, account_id, alias, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen). |\n| input.value | string | yes | Identity value. |\n| input.label | string | no | Optional label. |\n| input.verified | boolean | no | Whether the identity starts verified. |\n| input.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| patch.type | string | no | Optional identity type: did, atproto_did, email, phone, url, website, handle, x, twitter, linkedin, github, mastodon, bluesky, telegram, whatsapp, signal, matrix, discord, slack, instagram, facebook, youtube, tiktok, address, other, account_id, alias, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen). |\n| patch.value | string | no | Optional identity value. |\n| patch.label | string | no | Optional label. |\n\n```sh\nghostpaw-affinity identity add --contact \'{"contactId":2}\' --input \'{"type":"email","value":"jana@x.de"}\'\n```\n\n## manage_relationship\n\nSeed social links, revise bond and state, and manage structural ties. Seed a link as soon as a relationship is mentioned \u2014 do not wait for repeated evidence.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | seed_social_link, revise_bond, override_state, set_structural_tie, remove_structural_tie. Operation to perform. |\n| from | object | no | Source contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity. |\n| to | object | no | Target contact. Required when action=seed_social_link or set_structural_tie. Provide contactId or identity. |\n| kind | string | no | Link kind. Required for seed_social_link and set_structural_tie. Can be passed here or inside input. For seed_social_link: personal, family, professional, romantic, care, service, observed, other_relational. For set_structural_tie: works_at, manages, member_of, married_to, partner_of, parent_of, child_of, sibling_of, friend_of, client_of, vendor_of, reports_to, belongs_to, other_structural. |\n| role | string | no | Optional role for seed_social_link or set_structural_tie. Can be passed here or inside input. |\n| input | object | no | Full payload for seed_social_link or set_structural_tie. Alternative to top-level kind/role. |\n| link | object | no | Target link. Required when action=revise_bond, override_state, or remove_structural_tie. Provide linkId or endpoints. |\n| bond | any | no | Bond text. Required when action=revise_bond. Null to clear. |\n| state | string | no | active, dormant, strained, broken, archived. Link state. Required when action=override_state. |\n| options | object | no | Optional metadata. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=remove_structural_tie. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| from.contactId | integer | no | Exact contact id. |\n| from.identity | object | no | Contact identity locator. |\n| to.contactId | integer | no | Exact contact id. |\n| to.identity | object | no | Contact identity locator. |\n| link.linkId | integer | no | Exact link id. |\n| link.endpoints | object | no | Endpoint-based link locator. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n\n```sh\nghostpaw-affinity relationship tie --from \'{"contactId":2}\' --to \'{"contactId":3}\' --kind works_at --role Founder\n```\n\n## record_event\n\nRecord 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.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| kind | string | yes | interaction, observation, milestone, transaction. Event kind. |\n| input | object | yes | Event payload. Always required. Interaction events also require `type`. |\n| input.type | string | no | Interaction subtype. Required when kind=interaction, omit otherwise. |\n| input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in. |\n| input.content | string | no | Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied. |\n| input.externalRef | object | no | |\n| input.channel | string | no | Free-text channel, trimmed and lowercased, at most 40 characters. |\n| input.parentRef | object | no | |\n| input.parentEventId | integer | no | Parent event id. |\n| input.threadRef | object | no | |\n| input.momentKind | string | no | Optional moment kind. |\n| input.occurredOn | string | no | Calendar day. |\n| input.provenance | oneOf | no | |\n| input.occurredAt | oneOf | yes | When the event occurred, Unix timestamp in milliseconds. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.summary | string | no | Event summary. Must be non-empty. |\n| input.significance | integer | yes | Event significance from 1 to 10. |\n| input.participants | array | yes | Participant list. Must be non-empty and include the owner contact. |\n| input.now | oneOf | no | Optional timestamp override. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n\n```sh\nghostpaw-affinity event record --kind interaction --input \'{"type":"conversation","occurredAt":"today","summary":"Call re pilot","significance":6,"participants":[{"contactId":2,"role":"recipient","directionality":"mutual"}]}\'\n```\n\n## manage_commitment\n\nRecord or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | record, resolve. Operation to perform. |\n| input | object | no | Commitment payload. Required when action=record. |\n| commitmentEventId | integer | no | Commitment event id. Required when action=resolve. |\n| resolution | string | no | kept, cancelled, broken. How the commitment was resolved. Required when action=resolve. |\n| options | object | no | Optional metadata. |\n| input.commitmentType | string | yes | Promise or agreement. |\n| input.affectsBond | boolean | no | Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in. |\n| input.content | string | no | Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied. |\n| input.externalRef | object | no | |\n| input.channel | string | no | Free-text channel, trimmed and lowercased, at most 40 characters. |\n| input.parentRef | object | no | |\n| input.parentEventId | integer | no | Parent event id. |\n| input.threadRef | object | no | |\n| input.momentKind | string | no | Optional moment kind. |\n| input.occurredOn | string | no | Calendar day. |\n| input.provenance | oneOf | no | |\n| input.occurredAt | oneOf | yes | When the commitment was made, Unix timestamp in milliseconds. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.summary | string | no | Commitment summary. Must be non-empty. |\n| input.significance | integer | yes | Commitment significance from 1 to 10. |\n| input.dueAt | oneOf | no | Optional due timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.dueOn | string | no | Due calendar day. |\n| input.participants | array | yes | Participant list. Must be non-empty and include the owner contact. |\n| input.now | oneOf | no | Optional timestamp override. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| options.resolvingEvent | object | no | |\n\n```sh\nghostpaw-affinity commitment record --input \'{"commitmentType":"promise","occurredAt":"today","dueAt":"+3d","summary":"Send proposal","significance":6,"participants":[{"contactId":2,"role":"recipient"}]}\'\n```\n\n## manage_date_anchor\n\nAdd, revise, or remove recurring date anchors on contacts or links. Use this for birthdays, anniversaries, and yearly events \u2014 not manage_attribute.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | add, revise, remove. Operation to perform. |\n| input | object | no | Date anchor payload. Required when action=add. |\n| anchorEventId | integer | no | Anchor event id. Required when action=revise or remove. |\n| patch | object | no | Anchor patch. Required when action=revise. |\n| options | object | no | Optional metadata. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=remove. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.target | object | yes | Date-anchor target. |\n| input.recurrenceKind | string | yes | Recurrence kind. |\n| input.anchorMonth | integer | yes | Anchor month, 1\u201312. |\n| input.anchorDay | integer | yes | Anchor day, 1\u201331 (validated per month). |\n| input.summary | string | yes | Summary. Must be non-empty. |\n| input.significance | integer | yes | Significance from 1 to 10. |\n| input.now | oneOf | no | Optional timestamp override. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| input.force | boolean | no | Whether to bypass duplicate checks. |\n| patch.recurrenceKind | string | no | Optional recurrence kind. |\n| patch.anchorMonth | integer | no | Optional month, 1\u201312. |\n| patch.anchorDay | integer | no | Optional day, 1\u201331. |\n| patch.summary | string | no | Optional summary. |\n| patch.significance | integer | no | Optional significance, 1\u201310. |\n| options.now | oneOf | no | Optional timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| options.force | boolean | no | Whether duplicate checks should be bypassed. |\n\n```sh\nghostpaw-affinity date add --input \'{"target":{"kind":"contact","contact":{"contactId":2}},"recurrenceKind":"birthday","anchorMonth":11,"anchorDay":3,"summary":"Jana bday","significance":4}\'\n```\n\n## manage_attribute\n\nSet, unset, or replace custom metadata attributes on contacts, links and events. For emails, phones, handles, and URLs use manage_identity instead \u2014 those are routing keys, not attributes.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | set, unset, replace. Operation to perform. |\n| target | object | no | Attribute target. Always required. |\n| name | string | no | Attribute name. Required when action=set or unset. |\n| value | any | no | Attribute value. Required when action=set. Null for presence/tag semantics. |\n| entries | array | no | Key-value map to replace all attributes. Required when action=replace. |\n| removedAt | integer / string | no | Removal timestamp. Optional for action=unset. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| target.kind | string | yes | Target kind. |\n| target.eventId | integer | no | Target event id; provide eventId or ref. |\n| target.ref | object | no | |\n| target.contact | object | no | Contact locator. Required when kind=contact. Provide contactId or identity. |\n| target.link | object | no | Link locator. Required when kind=link. Provide linkId or endpoints. |\n| entries.name | string | yes | Attribute name. |\n| entries.value | oneOf | no | Attribute value, or null for presence/tag semantics. |\n\n```sh\nghostpaw-affinity attribute set --target \'{"kind":"contact","contact":{"contactId":2}}\' --name pipeline_stage --value \'"proposal"\'\n```\n\n## manage_event\n\nRevise or delete one event by eventId or external reference. Delete requires confirm=true and recomputes affected relationships.\n\nUse when:\n\nNot for:\n\nWrites: writes_state.\n\n| Parameter | Type | Required | Values / description |\n| --- | --- | --- | --- |\n| action | string | yes | revise, delete. Action. |\n| eventId | integer | no | Event id; provide eventId or ref. |\n| ref | object | no | |\n| confirm | boolean | no | Must be true to delete. |\n| patch | object | no | Fields to revise; externalRef may only be added when absent. |\n| refSource | string | no | External reference source. |\n| refId | string | no | External reference id. |\n| ref.source | string | yes | External source; trimmed and lowercased. |\n| ref.id | string | yes | Non-empty external id. |\n| patch.summary | string | no | Short non-empty label. |\n| patch.content | string | no | Verbatim content, at most 1 MB UTF-8. |\n| patch.occurredAt | oneOf | no | Event timestamp. Accepts epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d or -2d. UTC midnight for dates; dueAt uses UTC end of day. |\n| patch.significance | integer | no | Significance 1..10. |\n| patch.channel | string | no | Channel, at most 40 characters. |\n| patch.type | string | no | Type within the same event kind. |\n| patch.externalRef | object | no | |\n\n```sh\nghostpaw-affinity event revise --event-id 1 --patch \'{"summary":"Corrected"}\'\n```\n\n## Workflow guides\n\n- `ghostpaw-affinity skill bootstrap-and-cold-start`: Establish a clean Affinity baseline with one owner, one verified identity, and the minimum structure needed for later evidence and review work.\n- `ghostpaw-affinity skill identify-and-locate-contacts`: Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.\n- `ghostpaw-affinity skill import-history-without-faking-evidence`: Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.\n- `ghostpaw-affinity skill model-structure-orgs-and-households`: Model factual structure such as employment, membership, hierarchy, and household ties without confusing it with live relational mechanics.\n- `ghostpaw-affinity skill record-direct-evidence-well`: Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.\n- `ghostpaw-affinity skill record-observations-and-referrals`: Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.\n- `ghostpaw-affinity skill capture-transactions-and-commercial-events`: Capture commercially meaningful interactions as transactions so operational and service relationships accumulate the right evidence shape.\n- `ghostpaw-affinity skill manage-promises-and-agreements`: Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.\n- `ghostpaw-affinity skill manage-recurring-dates-and-reminders`: Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.\n- `ghostpaw-affinity skill review-radar-progression-and-graph`: Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.\n- `ghostpaw-affinity skill reconcile-duplicates-and-merge-safely`: Review duplicate candidates carefully and merge only after the identity, history, and structural consequences are understood.\n- `ghostpaw-affinity skill tools`: Tool parameters, nested fields, and examples.\n\n## Principles\n\n- Honor evidence before narrative.\n- Clarify before you act.\n- Keep truths in their proper layer.\n- Read derived signals with judgment.\n- Protect relationship integrity over time.\n- Follow the operational protocol.\n' } };
12259
12322
  }
12260
12323
  });
12261
12324
 
@@ -12484,7 +12547,7 @@ Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
12484
12547
  Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
12485
12548
  Participant contact locators may use identities, including did, instead of ids.
12486
12549
  Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
12487
- SQLite date-times without offsets mean UTC; microseconds round to milliseconds.
12550
+ SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
12488
12551
  Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
12489
12552
  The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
12490
12553
  show the mapping and verify an idempotent retry and byte-identical text.
@@ -12831,7 +12894,7 @@ arrive. Commitments and resolutions never reset contact recency or cadence.
12831
12894
  Imported text and event maintenance:
12832
12895
  - Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
12833
12896
  - Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.
12834
- - Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
12897
+ - Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.
12835
12898
  - Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
12836
12899
  - Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
12837
12900
  - Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.
@@ -12903,7 +12966,8 @@ var init_record_observations_and_referrals = __esm({
12903
12966
  content: `# Record Observations And Referrals
12904
12967
 
12905
12968
  Likes, reposts and follows are ambient observations, not milestones. Record them
12906
- with affectsBond:false; observations with significance <=2 default to false.
12969
+ with affectsBond:false. Like/follow/repost labels default to false across families;
12970
+ explicit true opts in. Observations with significance <=2 also default to false.
12907
12971
  They stay in journals but do not change affinity or meaningful-contact recency.
12908
12972
  provenance.ambient:true also suppresses effects. Use explicit owner and other
12909
12973
  participants, actor/recipient roles, and the correct directionality.
@@ -13180,7 +13244,7 @@ For bots and agents: run \`ghostpaw-affinity mcp\` for MCP stdio, or use this CL
13180
13244
  affinityShortCheatSheet = [
13181
13245
  "## CLI cheat sheet",
13182
13246
  "",
13183
- "Install: npm install -g @ghostpaw/affinity (Node 24+)",
13247
+ "Install binary: curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh | sh (npm alternative: npm install -g @ghostpaw/affinity; Node 24+)",
13184
13248
  "DB: --db <path>, GHOSTPAW_AFFINITY_DB, ~/.ghostpaw/affinity.db (in order).",
13185
13249
  "Dates: epoch ms, ISO Z/offset, SQLite UTC, YYYY-MM-DD, DD.MM.YYYY, today, yesterday, tomorrow, +3d, -2d.",
13186
13250
  "Exit: 0 success/no_op, 1 failure, 2 invalid input, 3 missing/clarification, 4 conflict, 70 internal.",
@@ -13546,7 +13610,7 @@ ${options.description}
13546
13610
 
13547
13611
  Commands:
13548
13612
  ${matches.map((command) => ` ${command.path}`).join("\n")}
13549
- mcp, skill, tools [--json], describe <tool>, call <tool> [JSON|-], completions <bash|zsh|fish>, version
13613
+ batch [--chunk N] [--stop-on-error], mcp, skill, tools [--json], describe <tool>, call <tool> [JSON|-], completions <bash|zsh|fish>, version
13550
13614
  Global flags: --db <path>, --json, --quiet, --compact, --help/-h, --version/-V/-v, --no-color
13551
13615
  `
13552
13616
  );
@@ -13828,7 +13892,7 @@ async function runToolCliInternal(argv, providedOptions) {
13828
13892
  ${options.description}
13829
13893
 
13830
13894
  Usage: ${options.bin} [--db path] <command>
13831
- Commands: tools [--json], describe <tool>, call <tool> [JSON|-], <tool> --parameter value, mcp, skill
13895
+ Commands: tools [--json], describe <tool>, call <tool> [JSON|-], <tool> --parameter value, mcp, skill, batch [--chunk N] [--stop-on-error]
13832
13896
  Options: --help, --version, --compact
13833
13897
 
13834
13898
  ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")}