@ghostpaw/affinity 0.4.0 → 0.4.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/README.md CHANGED
@@ -110,6 +110,12 @@ curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh
110
110
  irm https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.ps1 | iex
111
111
  ```
112
112
 
113
+ A session with the 0.4.0 linux-x64 binary against a fresh database (output piped through `jq` to keep it short):
114
+
115
+ ![ghostpaw-affinity 0.4.0: version, contact create, identity add, event and commitment record, search, review commitments.open and show contact_profile](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.png)
116
+
117
+ The same session as an [animated GIF](https://raw.githubusercontent.com/GhostPawJS/affinity/main/docs/assets/cli.gif).
118
+
113
119
  Other release installers:
114
120
 
115
121
  ```sh
@@ -163,7 +169,19 @@ download on macOS, remove quarantine with
163
169
  Linux binaries require glibc 2.28+; use npx on Alpine.
164
170
  See the [binary build workflow](.github/workflows/release-binaries.yml).
165
171
 
166
- <!-- sizes -->
172
+ Sizes for 0.4.0 (Node 26.12.1 inside each binary):
173
+
174
+ | Target | Archive | Binary |
175
+ |---|---|---|
176
+ | linux-x64 | 41.7 MB tar.gz | 117.6 MB |
177
+ | linux-arm64 | 41.2 MB tar.gz | 115.3 MB |
178
+ | darwin-arm64 | 44.9 MB tar.gz | 146.8 MB |
179
+ | darwin-x64 | 46.4 MB tar.gz | 149.7 MB |
180
+ | windows-x64 | 38.4 MB zip | 105.7 MB |
181
+
182
+ The npm package is 2003 KB unpacked and needs Node 24+. Every release is
183
+ installed and run on ubuntu-24.04, macos-15 and windows-2025 by the
184
+ [install check](.github/workflows/install-check.yml).
167
185
 
168
186
  The install check runs on ubuntu-24.04, macos-15 and windows-2025.
169
187
 
package/dist/cli.js CHANGED
@@ -23,7 +23,7 @@ var __export = (target, all) => {
23
23
  var affinityVersion;
24
24
  var init_version = __esm({
25
25
  "src/version.ts"() {
26
- affinityVersion = "0.4.0";
26
+ affinityVersion = "0.4.1";
27
27
  }
28
28
  });
29
29
 
@@ -2492,21 +2492,6 @@ var init_unset_attribute = __esm({
2492
2492
  }
2493
2493
  });
2494
2494
 
2495
- // src/tools/tool_summary.ts
2496
- function summarizeCount(count, singular) {
2497
- if (count === 0) {
2498
- return `No ${singular}s found.`;
2499
- }
2500
- if (count === 1) {
2501
- return `Found 1 ${singular}.`;
2502
- }
2503
- return `Found ${count} ${singular}s.`;
2504
- }
2505
- var init_tool_summary = __esm({
2506
- "src/tools/tool_summary.ts"() {
2507
- }
2508
- });
2509
-
2510
2495
  // src/tools/tool_mutation.ts
2511
2496
  function mutationToolResult(action, receipt, entitySummary) {
2512
2497
  const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
@@ -2520,7 +2505,7 @@ function mutationToolResult(action, receipt, entitySummary) {
2520
2505
  affectedLinks: receipt.affectedLinks,
2521
2506
  derivedEffects: receipt.derivedEffects
2522
2507
  };
2523
- const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : summarizeCount(changedCount, `${entitySummary} change`);
2508
+ const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
2524
2509
  if (changedCount === 0) {
2525
2510
  return toolNoOp(summary, data, {
2526
2511
  entities: toReceiptEntityRefs(receipt)
@@ -2530,11 +2515,28 @@ function mutationToolResult(action, receipt, entitySummary) {
2530
2515
  entities: toReceiptEntityRefs(receipt)
2531
2516
  });
2532
2517
  }
2518
+ function describeChanges(receipt) {
2519
+ const parts = [];
2520
+ for (const verb of ["created", "updated", "archived", "removed"]) {
2521
+ const counts = /* @__PURE__ */ new Map();
2522
+ for (const ref of receipt[verb]) {
2523
+ counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
2524
+ }
2525
+ if (counts.size === 0) continue;
2526
+ const items = [...counts].map(
2527
+ ([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
2528
+ );
2529
+ parts.push(`${verb} ${items.join(", ")}`);
2530
+ }
2531
+ const text = parts.join("; ");
2532
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
2533
+ }
2534
+ var PLURALS;
2533
2535
  var init_tool_mutation = __esm({
2534
2536
  "src/tools/tool_mutation.ts"() {
2535
2537
  init_tool_ref();
2536
- init_tool_summary();
2537
2538
  init_tool_types();
2539
+ PLURALS = { identity: "identities" };
2538
2540
  }
2539
2541
  });
2540
2542
 
@@ -4296,13 +4298,20 @@ function createContact(db, input) {
4296
4298
  if (!KINDS.has(input.kind)) {
4297
4299
  throw new AffinityValidationError("invalid contact kind");
4298
4300
  }
4301
+ if (input.isOwner !== void 0) {
4302
+ throw new AffinityValidationError(
4303
+ "isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
4304
+ );
4305
+ }
4299
4306
  const now = resolveNow(input.now);
4300
4307
  if (input.bootstrapOwner) {
4301
4308
  const existing = db.prepare(
4302
- "SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
4309
+ "SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
4303
4310
  ).get();
4304
4311
  if (existing !== void 0) {
4305
- throw new AffinityConflictError("owner already exists");
4312
+ throw new AffinityConflictError(
4313
+ `owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
4314
+ );
4306
4315
  }
4307
4316
  }
4308
4317
  const isOwner = input.bootstrapOwner ? 1 : 0;
@@ -7405,6 +7414,21 @@ var init_list_dismissed_duplicates = __esm({
7405
7414
  }
7406
7415
  });
7407
7416
 
7417
+ // src/tools/tool_summary.ts
7418
+ function summarizeCount(count, singular) {
7419
+ if (count === 0) {
7420
+ return `No ${singular}s found.`;
7421
+ }
7422
+ if (count === 1) {
7423
+ return `Found 1 ${singular}.`;
7424
+ }
7425
+ return `Found ${count} ${singular}s.`;
7426
+ }
7427
+ var init_tool_summary = __esm({
7428
+ "src/tools/tool_summary.ts"() {
7429
+ }
7430
+ });
7431
+
7408
7432
  // src/tools/review_affinity_tool.ts
7409
7433
  function buildReadOptions(input) {
7410
7434
  return {
@@ -10107,7 +10131,7 @@ function parseCliArgs(argv, spec) {
10107
10131
  raw = negative ? "false" : "true";
10108
10132
  if (raw === void 0) {
10109
10133
  const next = argv[i + 1];
10110
- if (next === void 0 || next.startsWith("--"))
10134
+ if (next === void 0 || /^--[a-z]/i.test(next))
10111
10135
  throw new CliUsageError(`Missing value: --${arg.name}`);
10112
10136
  raw = next;
10113
10137
  i++;
package/dist/index.cjs CHANGED
@@ -788,13 +788,20 @@ function createContact(db, input) {
788
788
  if (!KINDS.has(input.kind)) {
789
789
  throw new AffinityValidationError("invalid contact kind");
790
790
  }
791
+ if (input.isOwner !== void 0) {
792
+ throw new AffinityValidationError(
793
+ "isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
794
+ );
795
+ }
791
796
  const now = resolveNow(input.now);
792
797
  if (input.bootstrapOwner) {
793
798
  const existing = db.prepare(
794
- "SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
799
+ "SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
795
800
  ).get();
796
801
  if (existing !== void 0) {
797
- throw new AffinityConflictError("owner already exists");
802
+ throw new AffinityConflictError(
803
+ `owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
804
+ );
798
805
  }
799
806
  }
800
807
  const isOwner = input.bootstrapOwner ? 1 : 0;
@@ -6453,17 +6460,6 @@ function unsetAttribute(db, target, name, removedAt) {
6453
6460
  });
6454
6461
  }
6455
6462
 
6456
- // src/tools/tool_summary.ts
6457
- function summarizeCount(count, singular) {
6458
- if (count === 0) {
6459
- return `No ${singular}s found.`;
6460
- }
6461
- if (count === 1) {
6462
- return `Found 1 ${singular}.`;
6463
- }
6464
- return `Found ${count} ${singular}s.`;
6465
- }
6466
-
6467
6463
  // src/tools/tool_mutation.ts
6468
6464
  function mutationToolResult(action, receipt, entitySummary) {
6469
6465
  const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
@@ -6477,7 +6473,7 @@ function mutationToolResult(action, receipt, entitySummary) {
6477
6473
  affectedLinks: receipt.affectedLinks,
6478
6474
  derivedEffects: receipt.derivedEffects
6479
6475
  };
6480
- const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : summarizeCount(changedCount, `${entitySummary} change`);
6476
+ const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
6481
6477
  if (changedCount === 0) {
6482
6478
  return toolNoOp(summary, data, {
6483
6479
  entities: toReceiptEntityRefs(receipt)
@@ -6487,6 +6483,23 @@ function mutationToolResult(action, receipt, entitySummary) {
6487
6483
  entities: toReceiptEntityRefs(receipt)
6488
6484
  });
6489
6485
  }
6486
+ var PLURALS = { identity: "identities" };
6487
+ function describeChanges(receipt) {
6488
+ const parts = [];
6489
+ for (const verb of ["created", "updated", "archived", "removed"]) {
6490
+ const counts = /* @__PURE__ */ new Map();
6491
+ for (const ref of receipt[verb]) {
6492
+ counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
6493
+ }
6494
+ if (counts.size === 0) continue;
6495
+ const items = [...counts].map(
6496
+ ([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
6497
+ );
6498
+ parts.push(`${verb} ${items.join(", ")}`);
6499
+ }
6500
+ const text = parts.join("; ");
6501
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
6502
+ }
6490
6503
 
6491
6504
  // src/tools/manage_attribute_tool.ts
6492
6505
  function contactLocatorSchema2(description) {
@@ -7868,6 +7881,17 @@ var recordEventTool = defineAffinityTool({
7868
7881
  handler: recordEventToolHandler
7869
7882
  });
7870
7883
 
7884
+ // src/tools/tool_summary.ts
7885
+ function summarizeCount(count, singular) {
7886
+ if (count === 0) {
7887
+ return `No ${singular}s found.`;
7888
+ }
7889
+ if (count === 1) {
7890
+ return `Found 1 ${singular}.`;
7891
+ }
7892
+ return `Found ${count} ${singular}s.`;
7893
+ }
7894
+
7871
7895
  // src/tools/review_affinity_tool.ts
7872
7896
  function buildReadOptions(input) {
7873
7897
  return {
package/dist/index.js CHANGED
@@ -786,13 +786,20 @@ function createContact(db, input) {
786
786
  if (!KINDS.has(input.kind)) {
787
787
  throw new AffinityValidationError("invalid contact kind");
788
788
  }
789
+ if (input.isOwner !== void 0) {
790
+ throw new AffinityValidationError(
791
+ "isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
792
+ );
793
+ }
789
794
  const now = resolveNow(input.now);
790
795
  if (input.bootstrapOwner) {
791
796
  const existing = db.prepare(
792
- "SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
797
+ "SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
793
798
  ).get();
794
799
  if (existing !== void 0) {
795
- throw new AffinityConflictError("owner already exists");
800
+ throw new AffinityConflictError(
801
+ `owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
802
+ );
796
803
  }
797
804
  }
798
805
  const isOwner = input.bootstrapOwner ? 1 : 0;
@@ -6451,17 +6458,6 @@ function unsetAttribute(db, target, name, removedAt) {
6451
6458
  });
6452
6459
  }
6453
6460
 
6454
- // src/tools/tool_summary.ts
6455
- function summarizeCount(count, singular) {
6456
- if (count === 0) {
6457
- return `No ${singular}s found.`;
6458
- }
6459
- if (count === 1) {
6460
- return `Found 1 ${singular}.`;
6461
- }
6462
- return `Found ${count} ${singular}s.`;
6463
- }
6464
-
6465
6461
  // src/tools/tool_mutation.ts
6466
6462
  function mutationToolResult(action, receipt, entitySummary) {
6467
6463
  const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
@@ -6475,7 +6471,7 @@ function mutationToolResult(action, receipt, entitySummary) {
6475
6471
  affectedLinks: receipt.affectedLinks,
6476
6472
  derivedEffects: receipt.derivedEffects
6477
6473
  };
6478
- const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : summarizeCount(changedCount, `${entitySummary} change`);
6474
+ const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
6479
6475
  if (changedCount === 0) {
6480
6476
  return toolNoOp(summary, data, {
6481
6477
  entities: toReceiptEntityRefs(receipt)
@@ -6485,6 +6481,23 @@ function mutationToolResult(action, receipt, entitySummary) {
6485
6481
  entities: toReceiptEntityRefs(receipt)
6486
6482
  });
6487
6483
  }
6484
+ var PLURALS = { identity: "identities" };
6485
+ function describeChanges(receipt) {
6486
+ const parts = [];
6487
+ for (const verb of ["created", "updated", "archived", "removed"]) {
6488
+ const counts = /* @__PURE__ */ new Map();
6489
+ for (const ref of receipt[verb]) {
6490
+ counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
6491
+ }
6492
+ if (counts.size === 0) continue;
6493
+ const items = [...counts].map(
6494
+ ([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
6495
+ );
6496
+ parts.push(`${verb} ${items.join(", ")}`);
6497
+ }
6498
+ const text = parts.join("; ");
6499
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
6500
+ }
6488
6501
 
6489
6502
  // src/tools/manage_attribute_tool.ts
6490
6503
  function contactLocatorSchema2(description) {
@@ -7866,6 +7879,17 @@ var recordEventTool = defineAffinityTool({
7866
7879
  handler: recordEventToolHandler
7867
7880
  });
7868
7881
 
7882
+ // src/tools/tool_summary.ts
7883
+ function summarizeCount(count, singular) {
7884
+ if (count === 0) {
7885
+ return `No ${singular}s found.`;
7886
+ }
7887
+ if (count === 1) {
7888
+ return `Found 1 ${singular}.`;
7889
+ }
7890
+ return `Found ${count} ${singular}s.`;
7891
+ }
7892
+
7869
7893
  // src/tools/review_affinity_tool.ts
7870
7894
  function buildReadOptions(input) {
7871
7895
  return {
@@ -285,7 +285,7 @@ var init_mcp_stdio = __esm({
285
285
  var affinityVersion;
286
286
  var init_version = __esm({
287
287
  "src/version.ts"() {
288
- affinityVersion = "0.4.0";
288
+ affinityVersion = "0.4.1";
289
289
  }
290
290
  });
291
291
 
@@ -2762,21 +2762,6 @@ var init_unset_attribute = __esm({
2762
2762
  }
2763
2763
  });
2764
2764
 
2765
- // src/tools/tool_summary.ts
2766
- function summarizeCount(count, singular) {
2767
- if (count === 0) {
2768
- return `No ${singular}s found.`;
2769
- }
2770
- if (count === 1) {
2771
- return `Found 1 ${singular}.`;
2772
- }
2773
- return `Found ${count} ${singular}s.`;
2774
- }
2775
- var init_tool_summary = __esm({
2776
- "src/tools/tool_summary.ts"() {
2777
- }
2778
- });
2779
-
2780
2765
  // src/tools/tool_mutation.ts
2781
2766
  function mutationToolResult(action, receipt, entitySummary) {
2782
2767
  const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
@@ -2790,7 +2775,7 @@ function mutationToolResult(action, receipt, entitySummary) {
2790
2775
  affectedLinks: receipt.affectedLinks,
2791
2776
  derivedEffects: receipt.derivedEffects
2792
2777
  };
2793
- const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : summarizeCount(changedCount, `${entitySummary} change`);
2778
+ const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
2794
2779
  if (changedCount === 0) {
2795
2780
  return toolNoOp(summary, data, {
2796
2781
  entities: toReceiptEntityRefs(receipt)
@@ -2800,11 +2785,28 @@ function mutationToolResult(action, receipt, entitySummary) {
2800
2785
  entities: toReceiptEntityRefs(receipt)
2801
2786
  });
2802
2787
  }
2788
+ function describeChanges(receipt) {
2789
+ const parts = [];
2790
+ for (const verb of ["created", "updated", "archived", "removed"]) {
2791
+ const counts = /* @__PURE__ */ new Map();
2792
+ for (const ref of receipt[verb]) {
2793
+ counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
2794
+ }
2795
+ if (counts.size === 0) continue;
2796
+ const items = [...counts].map(
2797
+ ([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
2798
+ );
2799
+ parts.push(`${verb} ${items.join(", ")}`);
2800
+ }
2801
+ const text = parts.join("; ");
2802
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
2803
+ }
2804
+ var PLURALS;
2803
2805
  var init_tool_mutation = __esm({
2804
2806
  "src/tools/tool_mutation.ts"() {
2805
2807
  init_tool_ref();
2806
- init_tool_summary();
2807
2808
  init_tool_types();
2809
+ PLURALS = { identity: "identities" };
2808
2810
  }
2809
2811
  });
2810
2812
 
@@ -4566,13 +4568,20 @@ function createContact(db, input) {
4566
4568
  if (!KINDS.has(input.kind)) {
4567
4569
  throw new AffinityValidationError("invalid contact kind");
4568
4570
  }
4571
+ if (input.isOwner !== void 0) {
4572
+ throw new AffinityValidationError(
4573
+ "isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
4574
+ );
4575
+ }
4569
4576
  const now = resolveNow(input.now);
4570
4577
  if (input.bootstrapOwner) {
4571
4578
  const existing = db.prepare(
4572
- "SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
4579
+ "SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
4573
4580
  ).get();
4574
4581
  if (existing !== void 0) {
4575
- throw new AffinityConflictError("owner already exists");
4582
+ throw new AffinityConflictError(
4583
+ `owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
4584
+ );
4576
4585
  }
4577
4586
  }
4578
4587
  const isOwner = input.bootstrapOwner ? 1 : 0;
@@ -7675,6 +7684,21 @@ var init_list_dismissed_duplicates = __esm({
7675
7684
  }
7676
7685
  });
7677
7686
 
7687
+ // src/tools/tool_summary.ts
7688
+ function summarizeCount(count, singular) {
7689
+ if (count === 0) {
7690
+ return `No ${singular}s found.`;
7691
+ }
7692
+ if (count === 1) {
7693
+ return `Found 1 ${singular}.`;
7694
+ }
7695
+ return `Found ${count} ${singular}s.`;
7696
+ }
7697
+ var init_tool_summary = __esm({
7698
+ "src/tools/tool_summary.ts"() {
7699
+ }
7700
+ });
7701
+
7678
7702
  // src/tools/review_affinity_tool.ts
7679
7703
  function buildReadOptions(input) {
7680
7704
  return {
package/dist/mcp.cjs CHANGED
@@ -5,7 +5,7 @@ var node_path = require('node:path');
5
5
  var node_os = require('node:os');
6
6
 
7
7
  // src/version.ts
8
- var affinityVersion = "0.4.0";
8
+ var affinityVersion = "0.4.1";
9
9
 
10
10
  // src/surface/affinity_mcp_instructions.ts
11
11
  var affinityMcpInstructions = "Affinity 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.";
@@ -2160,17 +2160,6 @@ function unsetAttribute(db, target, name, removedAt) {
2160
2160
  });
2161
2161
  }
2162
2162
 
2163
- // src/tools/tool_summary.ts
2164
- function summarizeCount(count, singular) {
2165
- if (count === 0) {
2166
- return `No ${singular}s found.`;
2167
- }
2168
- if (count === 1) {
2169
- return `Found 1 ${singular}.`;
2170
- }
2171
- return `Found ${count} ${singular}s.`;
2172
- }
2173
-
2174
2163
  // src/tools/tool_mutation.ts
2175
2164
  function mutationToolResult(action, receipt, entitySummary) {
2176
2165
  const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
@@ -2184,7 +2173,7 @@ function mutationToolResult(action, receipt, entitySummary) {
2184
2173
  affectedLinks: receipt.affectedLinks,
2185
2174
  derivedEffects: receipt.derivedEffects
2186
2175
  };
2187
- const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : summarizeCount(changedCount, `${entitySummary} change`);
2176
+ const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
2188
2177
  if (changedCount === 0) {
2189
2178
  return toolNoOp(summary, data, {
2190
2179
  entities: toReceiptEntityRefs(receipt)
@@ -2194,6 +2183,23 @@ function mutationToolResult(action, receipt, entitySummary) {
2194
2183
  entities: toReceiptEntityRefs(receipt)
2195
2184
  });
2196
2185
  }
2186
+ var PLURALS = { identity: "identities" };
2187
+ function describeChanges(receipt) {
2188
+ const parts = [];
2189
+ for (const verb of ["created", "updated", "archived", "removed"]) {
2190
+ const counts = /* @__PURE__ */ new Map();
2191
+ for (const ref of receipt[verb]) {
2192
+ counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
2193
+ }
2194
+ if (counts.size === 0) continue;
2195
+ const items = [...counts].map(
2196
+ ([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
2197
+ );
2198
+ parts.push(`${verb} ${items.join(", ")}`);
2199
+ }
2200
+ const text = parts.join("; ");
2201
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
2202
+ }
2197
2203
 
2198
2204
  // src/tools/manage_attribute_tool.ts
2199
2205
  function contactLocatorSchema2(description) {
@@ -3825,13 +3831,20 @@ function createContact(db, input) {
3825
3831
  if (!KINDS.has(input.kind)) {
3826
3832
  throw new AffinityValidationError("invalid contact kind");
3827
3833
  }
3834
+ if (input.isOwner !== void 0) {
3835
+ throw new AffinityValidationError(
3836
+ "isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
3837
+ );
3838
+ }
3828
3839
  const now = resolveNow(input.now);
3829
3840
  if (input.bootstrapOwner) {
3830
3841
  const existing = db.prepare(
3831
- "SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
3842
+ "SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
3832
3843
  ).get();
3833
3844
  if (existing !== void 0) {
3834
- throw new AffinityConflictError("owner already exists");
3845
+ throw new AffinityConflictError(
3846
+ `owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
3847
+ );
3835
3848
  }
3836
3849
  }
3837
3850
  const isOwner = input.bootstrapOwner ? 1 : 0;
@@ -6451,6 +6464,17 @@ function listDismissedDuplicates(db, options) {
6451
6464
  }));
6452
6465
  }
6453
6466
 
6467
+ // src/tools/tool_summary.ts
6468
+ function summarizeCount(count, singular) {
6469
+ if (count === 0) {
6470
+ return `No ${singular}s found.`;
6471
+ }
6472
+ if (count === 1) {
6473
+ return `Found 1 ${singular}.`;
6474
+ }
6475
+ return `Found ${count} ${singular}s.`;
6476
+ }
6477
+
6454
6478
  // src/tools/review_affinity_tool.ts
6455
6479
  function buildReadOptions(input) {
6456
6480
  return {
package/dist/mcp.js CHANGED
@@ -3,7 +3,7 @@ import { dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
 
5
5
  // src/version.ts
6
- var affinityVersion = "0.4.0";
6
+ var affinityVersion = "0.4.1";
7
7
 
8
8
  // src/surface/affinity_mcp_instructions.ts
9
9
  var affinityMcpInstructions = "Affinity 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.";
@@ -2158,17 +2158,6 @@ function unsetAttribute(db, target, name, removedAt) {
2158
2158
  });
2159
2159
  }
2160
2160
 
2161
- // src/tools/tool_summary.ts
2162
- function summarizeCount(count, singular) {
2163
- if (count === 0) {
2164
- return `No ${singular}s found.`;
2165
- }
2166
- if (count === 1) {
2167
- return `Found 1 ${singular}.`;
2168
- }
2169
- return `Found ${count} ${singular}s.`;
2170
- }
2171
-
2172
2161
  // src/tools/tool_mutation.ts
2173
2162
  function mutationToolResult(action, receipt, entitySummary) {
2174
2163
  const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
@@ -2182,7 +2171,7 @@ function mutationToolResult(action, receipt, entitySummary) {
2182
2171
  affectedLinks: receipt.affectedLinks,
2183
2172
  derivedEffects: receipt.derivedEffects
2184
2173
  };
2185
- const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : summarizeCount(changedCount, `${entitySummary} change`);
2174
+ const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
2186
2175
  if (changedCount === 0) {
2187
2176
  return toolNoOp(summary, data, {
2188
2177
  entities: toReceiptEntityRefs(receipt)
@@ -2192,6 +2181,23 @@ function mutationToolResult(action, receipt, entitySummary) {
2192
2181
  entities: toReceiptEntityRefs(receipt)
2193
2182
  });
2194
2183
  }
2184
+ var PLURALS = { identity: "identities" };
2185
+ function describeChanges(receipt) {
2186
+ const parts = [];
2187
+ for (const verb of ["created", "updated", "archived", "removed"]) {
2188
+ const counts = /* @__PURE__ */ new Map();
2189
+ for (const ref of receipt[verb]) {
2190
+ counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
2191
+ }
2192
+ if (counts.size === 0) continue;
2193
+ const items = [...counts].map(
2194
+ ([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
2195
+ );
2196
+ parts.push(`${verb} ${items.join(", ")}`);
2197
+ }
2198
+ const text = parts.join("; ");
2199
+ return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
2200
+ }
2195
2201
 
2196
2202
  // src/tools/manage_attribute_tool.ts
2197
2203
  function contactLocatorSchema2(description) {
@@ -3823,13 +3829,20 @@ function createContact(db, input) {
3823
3829
  if (!KINDS.has(input.kind)) {
3824
3830
  throw new AffinityValidationError("invalid contact kind");
3825
3831
  }
3832
+ if (input.isOwner !== void 0) {
3833
+ throw new AffinityValidationError(
3834
+ "isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
3835
+ );
3836
+ }
3826
3837
  const now = resolveNow(input.now);
3827
3838
  if (input.bootstrapOwner) {
3828
3839
  const existing = db.prepare(
3829
- "SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
3840
+ "SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
3830
3841
  ).get();
3831
3842
  if (existing !== void 0) {
3832
- throw new AffinityConflictError("owner already exists");
3843
+ throw new AffinityConflictError(
3844
+ `owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
3845
+ );
3833
3846
  }
3834
3847
  }
3835
3848
  const isOwner = input.bootstrapOwner ? 1 : 0;
@@ -6449,6 +6462,17 @@ function listDismissedDuplicates(db, options) {
6449
6462
  }));
6450
6463
  }
6451
6464
 
6465
+ // src/tools/tool_summary.ts
6466
+ function summarizeCount(count, singular) {
6467
+ if (count === 0) {
6468
+ return `No ${singular}s found.`;
6469
+ }
6470
+ if (count === 1) {
6471
+ return `Found 1 ${singular}.`;
6472
+ }
6473
+ return `Found ${count} ${singular}s.`;
6474
+ }
6475
+
6452
6476
  // src/tools/review_affinity_tool.ts
6453
6477
  function buildReadOptions(input) {
6454
6478
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostpaw/affinity",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Relationship intelligence and social CRM model for Node.js — SQLite, zero dependencies, with a built-in LLM tool facade",
5
5
  "license": "MIT",
6
6
  "author": {