@ghostpaw/affinity 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -114
- package/SKILL.md +217 -33
- package/dist/cli.js +2433 -913
- package/dist/index.cjs +1397 -504
- package/dist/index.d.cts +556 -55
- package/dist/index.d.ts +556 -55
- package/dist/index.js +1397 -504
- package/dist/mcp-server.js +1811 -694
- package/dist/mcp.cjs +1604 -606
- package/dist/mcp.js +1605 -607
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +217 -33
package/dist/index.js
CHANGED
|
@@ -263,6 +263,12 @@ function initLinkEventEffectsTables(db) {
|
|
|
263
263
|
)
|
|
264
264
|
)
|
|
265
265
|
`);
|
|
266
|
+
const columns = db.prepare("PRAGMA table_info(link_event_effects)").all();
|
|
267
|
+
if (!columns.some((column) => column.name === "counts_for_recency")) {
|
|
268
|
+
db.exec(
|
|
269
|
+
"ALTER TABLE link_event_effects ADD COLUMN counts_for_recency INTEGER NOT NULL DEFAULT 1"
|
|
270
|
+
);
|
|
271
|
+
}
|
|
266
272
|
db.exec(
|
|
267
273
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
268
274
|
);
|
|
@@ -513,6 +519,8 @@ function resolveAffinityListLimit(limit) {
|
|
|
513
519
|
// src/read.ts
|
|
514
520
|
var read_exports = {};
|
|
515
521
|
__export(read_exports, {
|
|
522
|
+
exportAffinity: () => exportAffinity,
|
|
523
|
+
exportContact: () => exportContact,
|
|
516
524
|
getAffinityChart: () => getAffinityChart,
|
|
517
525
|
getContactJournal: () => getContactJournal,
|
|
518
526
|
getContactProfile: () => getContactProfile,
|
|
@@ -820,6 +828,25 @@ function createContact(db, input) {
|
|
|
820
828
|
});
|
|
821
829
|
}
|
|
822
830
|
|
|
831
|
+
// src/lib/read/assert_default_ordering.ts
|
|
832
|
+
function assertDefaultOrdering(_readName, options) {
|
|
833
|
+
if (options?.sort !== void 0 || options?.descending !== void 0) {
|
|
834
|
+
throw new AffinityValidationError(
|
|
835
|
+
"This review does not support custom sort ordering"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/lib/read/resolve_list_limit_offset.ts
|
|
841
|
+
function resolveListLimitOffset(options) {
|
|
842
|
+
const limit = resolveAffinityListLimit(options?.limit);
|
|
843
|
+
const offset = options?.offset ?? 0;
|
|
844
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
845
|
+
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
846
|
+
}
|
|
847
|
+
return { limit, offset };
|
|
848
|
+
}
|
|
849
|
+
|
|
823
850
|
// src/events/validators.ts
|
|
824
851
|
var PARTICIPANT_ROLES = /* @__PURE__ */ new Set([
|
|
825
852
|
"actor",
|
|
@@ -853,7 +880,10 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
853
880
|
throw new AffinityNotFoundError("contact not found");
|
|
854
881
|
}
|
|
855
882
|
if (row.lifecycle_state === "merged") {
|
|
856
|
-
|
|
883
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
884
|
+
throw new AffinityStateError(
|
|
885
|
+
`Contact #${id} was merged into #${row.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row.merged_into_contact_id}`
|
|
886
|
+
);
|
|
857
887
|
}
|
|
858
888
|
}
|
|
859
889
|
}
|
|
@@ -1025,6 +1055,213 @@ function findDuplicateDateAnchor(db, params) {
|
|
|
1025
1055
|
return null;
|
|
1026
1056
|
}
|
|
1027
1057
|
|
|
1058
|
+
// src/events/loaders.ts
|
|
1059
|
+
function loadEventParticipantViews(db, eventId) {
|
|
1060
|
+
const rows = db.prepare(
|
|
1061
|
+
`SELECT contact_id, role, directionality
|
|
1062
|
+
FROM event_participants
|
|
1063
|
+
WHERE event_id = ?`
|
|
1064
|
+
).all(eventId);
|
|
1065
|
+
return rows.map((row) => ({
|
|
1066
|
+
contactId: row.contact_id,
|
|
1067
|
+
role: row.role,
|
|
1068
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1069
|
+
}));
|
|
1070
|
+
}
|
|
1071
|
+
function loadEventRecord(db, eventId) {
|
|
1072
|
+
const row = getEventRowById(db, eventId);
|
|
1073
|
+
if (!row) {
|
|
1074
|
+
throw new AffinityInvariantError("event not found");
|
|
1075
|
+
}
|
|
1076
|
+
const participants = loadEventParticipantViews(db, eventId);
|
|
1077
|
+
return mapEventRowToEventRecord(row, participants);
|
|
1078
|
+
}
|
|
1079
|
+
function batchLoadEventRecords(db, eventIds) {
|
|
1080
|
+
if (eventIds.length === 0) {
|
|
1081
|
+
return [];
|
|
1082
|
+
}
|
|
1083
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
1084
|
+
const eventRows = db.prepare(
|
|
1085
|
+
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
1086
|
+
recurrence_kind, anchor_month, anchor_day,
|
|
1087
|
+
anchor_contact_id, anchor_link_id,
|
|
1088
|
+
created_at, updated_at, deleted_at
|
|
1089
|
+
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
1090
|
+
).all(...eventIds);
|
|
1091
|
+
const partRows = db.prepare(
|
|
1092
|
+
`SELECT event_id, contact_id, role, directionality
|
|
1093
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
1094
|
+
).all(...eventIds);
|
|
1095
|
+
const partsByEvent = /* @__PURE__ */ new Map();
|
|
1096
|
+
for (const p of partRows) {
|
|
1097
|
+
let arr = partsByEvent.get(p.event_id);
|
|
1098
|
+
if (!arr) {
|
|
1099
|
+
arr = [];
|
|
1100
|
+
partsByEvent.set(p.event_id, arr);
|
|
1101
|
+
}
|
|
1102
|
+
arr.push({
|
|
1103
|
+
contactId: p.contact_id,
|
|
1104
|
+
role: p.role,
|
|
1105
|
+
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
1109
|
+
const out = [];
|
|
1110
|
+
for (const id of eventIds) {
|
|
1111
|
+
const row = rowById.get(id);
|
|
1112
|
+
if (row) {
|
|
1113
|
+
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return out;
|
|
1117
|
+
}
|
|
1118
|
+
function batchLoadEventParticipantViews(db, eventIds) {
|
|
1119
|
+
const result = /* @__PURE__ */ new Map();
|
|
1120
|
+
if (eventIds.length === 0) {
|
|
1121
|
+
return result;
|
|
1122
|
+
}
|
|
1123
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
1124
|
+
const rows = db.prepare(
|
|
1125
|
+
`SELECT event_id, contact_id, role, directionality
|
|
1126
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
1127
|
+
).all(...eventIds);
|
|
1128
|
+
for (const row of rows) {
|
|
1129
|
+
let arr = result.get(row.event_id);
|
|
1130
|
+
if (!arr) {
|
|
1131
|
+
arr = [];
|
|
1132
|
+
result.set(row.event_id, arr);
|
|
1133
|
+
}
|
|
1134
|
+
arr.push({
|
|
1135
|
+
contactId: row.contact_id,
|
|
1136
|
+
role: row.role,
|
|
1137
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
return result;
|
|
1141
|
+
}
|
|
1142
|
+
function requireDateAnchorEvent(db, eventId) {
|
|
1143
|
+
const row = getEventRowById(db, eventId);
|
|
1144
|
+
if (!row) {
|
|
1145
|
+
throw new AffinityNotFoundError("event not found");
|
|
1146
|
+
}
|
|
1147
|
+
if (row.type !== "date_anchor") {
|
|
1148
|
+
throw new AffinityInvariantError("not a date anchor");
|
|
1149
|
+
}
|
|
1150
|
+
return row;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/events/get_contact_journal.ts
|
|
1154
|
+
function getContactJournal(db, contactId, options) {
|
|
1155
|
+
assertDefaultOrdering("getContactJournal", options);
|
|
1156
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
1157
|
+
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
1158
|
+
const params = [contactId];
|
|
1159
|
+
if (options?.since !== void 0) {
|
|
1160
|
+
clauses.push("e.occurred_at >= ?");
|
|
1161
|
+
params.push(options.since);
|
|
1162
|
+
}
|
|
1163
|
+
if (options?.until !== void 0) {
|
|
1164
|
+
clauses.push("e.occurred_at <= ?");
|
|
1165
|
+
params.push(options.until);
|
|
1166
|
+
}
|
|
1167
|
+
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
1168
|
+
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
1169
|
+
clauses.push(`e.type IN (${ph})`);
|
|
1170
|
+
params.push(...options.eventTypes);
|
|
1171
|
+
}
|
|
1172
|
+
const where = clauses.join(" AND ");
|
|
1173
|
+
const rows = db.prepare(
|
|
1174
|
+
`SELECT e.id FROM events e
|
|
1175
|
+
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
1176
|
+
WHERE ${where}
|
|
1177
|
+
ORDER BY e.occurred_at DESC, e.id DESC
|
|
1178
|
+
LIMIT ? OFFSET ?`
|
|
1179
|
+
).all(...params, limit, offset);
|
|
1180
|
+
return batchLoadEventRecords(
|
|
1181
|
+
db,
|
|
1182
|
+
rows.map((r) => r.id)
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/events/list_open_commitments.ts
|
|
1187
|
+
var DAY_MS = 864e5;
|
|
1188
|
+
function mapResolution(resolvedAt, resolution) {
|
|
1189
|
+
if (resolvedAt === null) {
|
|
1190
|
+
return "open";
|
|
1191
|
+
}
|
|
1192
|
+
if (resolution === "cancelled") {
|
|
1193
|
+
return "cancelled";
|
|
1194
|
+
}
|
|
1195
|
+
return "resolved";
|
|
1196
|
+
}
|
|
1197
|
+
function listOpenCommitments(db, filters, options) {
|
|
1198
|
+
assertDefaultOrdering("listOpenCommitments", options);
|
|
1199
|
+
const now = resolveNow(options?.now);
|
|
1200
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
1201
|
+
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
1202
|
+
const params = [];
|
|
1203
|
+
if (options?.since !== void 0) {
|
|
1204
|
+
clauses.push("e.occurred_at >= ?");
|
|
1205
|
+
params.push(options.since);
|
|
1206
|
+
}
|
|
1207
|
+
if (options?.until !== void 0) {
|
|
1208
|
+
clauses.push("e.occurred_at <= ?");
|
|
1209
|
+
params.push(options.until);
|
|
1210
|
+
}
|
|
1211
|
+
if (filters?.commitmentType !== void 0) {
|
|
1212
|
+
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
1213
|
+
throw new AffinityValidationError("invalid commitmentType filter");
|
|
1214
|
+
}
|
|
1215
|
+
clauses.push("oc.commitment_type = ?");
|
|
1216
|
+
params.push(filters.commitmentType);
|
|
1217
|
+
}
|
|
1218
|
+
if (filters?.contactId !== void 0) {
|
|
1219
|
+
clauses.push(
|
|
1220
|
+
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
1221
|
+
);
|
|
1222
|
+
params.push(filters.contactId);
|
|
1223
|
+
}
|
|
1224
|
+
if (filters?.linkId !== void 0) {
|
|
1225
|
+
clauses.push(
|
|
1226
|
+
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
1227
|
+
);
|
|
1228
|
+
params.push(filters.linkId);
|
|
1229
|
+
}
|
|
1230
|
+
if (filters?.horizonDays !== void 0) {
|
|
1231
|
+
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
1232
|
+
throw new AffinityValidationError(
|
|
1233
|
+
"horizonDays must be a non-negative integer"
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const anchor = options?.since ?? now;
|
|
1237
|
+
clauses.push("oc.due_at IS NOT NULL");
|
|
1238
|
+
clauses.push("oc.due_at <= ?");
|
|
1239
|
+
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
1240
|
+
}
|
|
1241
|
+
const where = clauses.join(" AND ");
|
|
1242
|
+
const rows = db.prepare(
|
|
1243
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
1244
|
+
e.type, e.summary
|
|
1245
|
+
FROM open_commitments oc
|
|
1246
|
+
INNER JOIN events e ON e.id = oc.event_id
|
|
1247
|
+
WHERE ${where}
|
|
1248
|
+
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
1249
|
+
LIMIT ? OFFSET ?`
|
|
1250
|
+
).all(...params, limit, offset);
|
|
1251
|
+
const eventIds = rows.map((r) => r.event_id);
|
|
1252
|
+
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
1253
|
+
return rows.map((r) => ({
|
|
1254
|
+
eventId: r.event_id,
|
|
1255
|
+
type: r.type,
|
|
1256
|
+
summary: r.summary,
|
|
1257
|
+
participants: partsByEvent.get(r.event_id) ?? [],
|
|
1258
|
+
dueAt: r.due_at,
|
|
1259
|
+
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
1260
|
+
overdue: r.due_at !== null && r.due_at < now,
|
|
1261
|
+
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
1262
|
+
}));
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1028
1265
|
// src/dates/calendar.ts
|
|
1029
1266
|
function utcStartOfDayMs(ms) {
|
|
1030
1267
|
const date = new Date(ms);
|
|
@@ -1191,13 +1428,20 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1191
1428
|
const topLinks = linkRows.map((l) => mapLinkRowToLinkListItem(l));
|
|
1192
1429
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
1193
1430
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1431
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
1194
1432
|
return {
|
|
1433
|
+
...winner ? { mergedInto: { id: winner.id, name: winner.name } } : {},
|
|
1195
1434
|
contact: mapContactRowToContactCore(row),
|
|
1196
1435
|
identities,
|
|
1197
1436
|
attributes,
|
|
1198
1437
|
topLinks,
|
|
1199
1438
|
activeDates,
|
|
1200
|
-
warnings:
|
|
1439
|
+
warnings: [
|
|
1440
|
+
...contactRollup?.warnings ?? [],
|
|
1441
|
+
...winner ? [
|
|
1442
|
+
`Contact #${row.id} was merged into #${winner.id} ${winner.name}; use contactId ${winner.id}`
|
|
1443
|
+
] : []
|
|
1444
|
+
],
|
|
1201
1445
|
rollups: contactRollup?.rollup ?? null
|
|
1202
1446
|
};
|
|
1203
1447
|
}
|
|
@@ -1205,7 +1449,27 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
1205
1449
|
// src/contacts/get_contact_profile.ts
|
|
1206
1450
|
function getContactProfile(db, contactId, options) {
|
|
1207
1451
|
const t = options?.topLinksLimit ?? 8;
|
|
1208
|
-
|
|
1452
|
+
const profile = loadContactProfileRecord(db, contactId, t);
|
|
1453
|
+
if (!profile) return null;
|
|
1454
|
+
const ties = db.prepare(`SELECT l.id, l.kind, l.role, c.id AS contactId, c.name
|
|
1455
|
+
FROM links l JOIN contacts c ON c.id = CASE WHEN l.from_contact_id = ? THEN l.to_contact_id ELSE l.from_contact_id END
|
|
1456
|
+
WHERE l.is_structural = 1 AND l.removed_at IS NULL AND (l.from_contact_id = ? OR l.to_contact_id = ?)
|
|
1457
|
+
AND c.deleted_at IS NULL ORDER BY l.id`).all(contactId, contactId, contactId);
|
|
1458
|
+
return {
|
|
1459
|
+
...profile,
|
|
1460
|
+
recentEvents: getContactJournal(db, contactId, {
|
|
1461
|
+
limit: options?.recentEventsLimit ?? 5
|
|
1462
|
+
}).map((event) => ({
|
|
1463
|
+
...event,
|
|
1464
|
+
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
1465
|
+
})),
|
|
1466
|
+
openCommitments: listOpenCommitments(
|
|
1467
|
+
db,
|
|
1468
|
+
{ contactId },
|
|
1469
|
+
options?.now === void 0 ? void 0 : { now: options.now }
|
|
1470
|
+
),
|
|
1471
|
+
ties
|
|
1472
|
+
};
|
|
1209
1473
|
}
|
|
1210
1474
|
|
|
1211
1475
|
// src/contacts/get_owner_profile.ts
|
|
@@ -1218,23 +1482,21 @@ function getOwnerProfile(db, options) {
|
|
|
1218
1482
|
return loadContactProfileRecord(db, ownerId, t);
|
|
1219
1483
|
}
|
|
1220
1484
|
|
|
1221
|
-
// src/
|
|
1222
|
-
function
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
1235
|
-
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
1485
|
+
// src/contacts/contact_attribute_clauses.ts
|
|
1486
|
+
function contactAttributeClauses(filters) {
|
|
1487
|
+
const clauses = [];
|
|
1488
|
+
const params = [];
|
|
1489
|
+
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
1490
|
+
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
1491
|
+
for (const filter of [...attributes, ...tags.map((tag) => `tag:${tag}`)]) {
|
|
1492
|
+
const separator = filter.indexOf("=");
|
|
1493
|
+
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
1494
|
+
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
1495
|
+
AND a.deleted_at IS NULL AND lower(a.name) = lower(?)${separator < 0 ? "" : " AND lower(CAST(CASE WHEN json_valid(a.value) THEN json_extract(a.value, '$') ELSE a.value END AS TEXT)) = lower(?)"})`);
|
|
1496
|
+
params.push(name);
|
|
1497
|
+
if (separator >= 0) params.push(filter.slice(separator + 1));
|
|
1236
1498
|
}
|
|
1237
|
-
return {
|
|
1499
|
+
return { clauses, params };
|
|
1238
1500
|
}
|
|
1239
1501
|
|
|
1240
1502
|
// src/contacts/list_contacts.ts
|
|
@@ -1260,6 +1522,9 @@ function listContacts(db, filters, options) {
|
|
|
1260
1522
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
1261
1523
|
clauses.push("lifecycle_state != 'dormant'");
|
|
1262
1524
|
}
|
|
1525
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
1526
|
+
clauses.push(...attributeFilters.clauses);
|
|
1527
|
+
params.push(...attributeFilters.params);
|
|
1263
1528
|
const where = clauses.join(" AND ");
|
|
1264
1529
|
const rows = db.prepare(
|
|
1265
1530
|
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
@@ -1442,89 +1707,97 @@ function reviseContact(db, contactId, patch) {
|
|
|
1442
1707
|
});
|
|
1443
1708
|
}
|
|
1444
1709
|
|
|
1445
|
-
// src/contacts/
|
|
1446
|
-
function
|
|
1447
|
-
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
return 0.7;
|
|
1710
|
+
// src/contacts/fold_search_text.ts
|
|
1711
|
+
function foldSearchText(value) {
|
|
1712
|
+
let folded = value.toLowerCase();
|
|
1713
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
1714
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
1715
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
1452
1716
|
}
|
|
1717
|
+
|
|
1718
|
+
// src/contacts/search_contacts.ts
|
|
1453
1719
|
function searchContacts(db, query, filters, options) {
|
|
1454
|
-
const q = query.trim();
|
|
1455
|
-
if (q
|
|
1456
|
-
return [];
|
|
1457
|
-
}
|
|
1720
|
+
const q = foldSearchText(query.trim());
|
|
1721
|
+
if (!q) return [];
|
|
1458
1722
|
assertDefaultOrdering("searchContacts", options);
|
|
1459
1723
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
1460
|
-
const
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1724
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1725
|
+
const rows = db.prepare(`
|
|
1726
|
+
SELECT contact_id, value || ' ' || normalized_key AS text, 'identity:' || type AS source FROM identities WHERE removed_at IS NULL
|
|
1727
|
+
UNION ALL SELECT contact_id, name || ' ' || COALESCE(value, ''), 'attribute:' || name FROM attributes WHERE deleted_at IS NULL AND contact_id IS NOT NULL
|
|
1728
|
+
UNION ALL SELECT from_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
1729
|
+
UNION ALL SELECT to_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
1730
|
+
UNION ALL SELECT ep.contact_id, e.summary, 'event' FROM event_participants ep JOIN events e ON e.id = ep.event_id WHERE e.deleted_at IS NULL
|
|
1731
|
+
`).all();
|
|
1732
|
+
for (const row of rows) {
|
|
1733
|
+
const values = fields.get(row.contact_id) ?? [];
|
|
1734
|
+
values.push({ text: foldSearchText(row.text), source: row.source });
|
|
1735
|
+
fields.set(row.contact_id, values);
|
|
1736
|
+
}
|
|
1737
|
+
const tokens = q.split(/\s+/);
|
|
1738
|
+
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
1739
|
+
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
1740
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
1463
1741
|
const clauses = [
|
|
1464
|
-
"
|
|
1465
|
-
"
|
|
1742
|
+
"deleted_at IS NULL",
|
|
1743
|
+
"lifecycle_state != 'merged'",
|
|
1744
|
+
...attributeFilters.clauses
|
|
1466
1745
|
];
|
|
1467
|
-
const params = [];
|
|
1746
|
+
const params = [...attributeFilters.params];
|
|
1468
1747
|
if (filters?.kind !== void 0) {
|
|
1469
|
-
clauses.push("
|
|
1748
|
+
clauses.push("kind = ?");
|
|
1470
1749
|
params.push(filters.kind);
|
|
1471
1750
|
}
|
|
1472
1751
|
if (filters?.lifecycleState !== void 0) {
|
|
1473
|
-
clauses.push("
|
|
1752
|
+
clauses.push("lifecycle_state = ?");
|
|
1474
1753
|
params.push(filters.lifecycleState);
|
|
1475
1754
|
}
|
|
1476
|
-
if (filters?.includeOwner === false)
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
Number(
|
|
1515
|
-
r.matched_normalized_identity ?? 0
|
|
1516
|
-
) > 0
|
|
1755
|
+
if (filters?.includeOwner === false) clauses.push("is_owner = 0");
|
|
1756
|
+
const contacts = db.prepare(
|
|
1757
|
+
`SELECT id, name, kind, lifecycle_state, is_owner, (SELECT value FROM identities i WHERE i.contact_id = contacts.id AND i.removed_at IS NULL ORDER BY i.id LIMIT 1) AS primary_identity FROM contacts WHERE ${clauses.join(" AND ")}`
|
|
1758
|
+
).all(...params).map((row) => ({
|
|
1759
|
+
...mapContactRowToContactListItem(row),
|
|
1760
|
+
primaryIdentity: row.primary_identity
|
|
1761
|
+
}));
|
|
1762
|
+
const scored = [];
|
|
1763
|
+
for (const contact of contacts) {
|
|
1764
|
+
if (contact.lifecycleState === "dormant" && !(filters?.includeDormant ?? options?.includeDormant))
|
|
1765
|
+
continue;
|
|
1766
|
+
if (contact.lifecycleState === "lost" && !filters?.includeLost) continue;
|
|
1767
|
+
const name = foldSearchText(contact.name);
|
|
1768
|
+
const values = [
|
|
1769
|
+
{ text: name, source: "name" },
|
|
1770
|
+
...(fields.get(contact.id) ?? []).filter(
|
|
1771
|
+
(field) => !contact.isOwner || field.source !== "event" && field.source !== "link_role"
|
|
1772
|
+
)
|
|
1773
|
+
];
|
|
1774
|
+
const phoneFields = phone ? values.filter(
|
|
1775
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
1776
|
+
) : [];
|
|
1777
|
+
if (!phoneFields.length && !tokens.every(
|
|
1778
|
+
(token) => values.some((field) => field.text.includes(token))
|
|
1779
|
+
))
|
|
1780
|
+
continue;
|
|
1781
|
+
const matchedOn = [
|
|
1782
|
+
...new Set(
|
|
1783
|
+
[
|
|
1784
|
+
...values.filter(
|
|
1785
|
+
(field) => tokens.some((token) => field.text.includes(token))
|
|
1786
|
+
),
|
|
1787
|
+
...phoneFields
|
|
1788
|
+
].map((field) => field.source)
|
|
1789
|
+
)
|
|
1790
|
+
];
|
|
1791
|
+
const identities = values.filter(
|
|
1792
|
+
(field) => field.source.startsWith("identity:")
|
|
1517
1793
|
);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
});
|
|
1526
|
-
scored.sort((a, b) => b.score - a.score);
|
|
1527
|
-
return scored.slice(offset, offset + limit).map((s) => s.item);
|
|
1794
|
+
const score = name === q ? 5 : name.startsWith(q) || tokens.every((token) => name.includes(token)) ? 4 : phoneFields.length || identities.some((field) => field.text.split(" ").includes(q)) ? 3 : identities.some(
|
|
1795
|
+
(field) => tokens.every((token) => field.text.includes(token))
|
|
1796
|
+
) ? 2 : 1;
|
|
1797
|
+
scored.push({ item: { ...contact, matchedOn }, score });
|
|
1798
|
+
}
|
|
1799
|
+
scored.sort((a, b) => b.score - a.score || a.item.id - b.item.id);
|
|
1800
|
+
return scored.slice(offset, offset + limit).map(({ item }) => item);
|
|
1528
1801
|
}
|
|
1529
1802
|
|
|
1530
1803
|
// src/contacts/validators.ts
|
|
@@ -1533,7 +1806,9 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
1533
1806
|
throw new AffinityStateError("merged contact is read-only");
|
|
1534
1807
|
}
|
|
1535
1808
|
if (to === "merged") {
|
|
1536
|
-
throw new AffinityInvariantError(
|
|
1809
|
+
throw new AffinityInvariantError(
|
|
1810
|
+
"use manage_contact merge (CLI: contact merge) to reach merged state"
|
|
1811
|
+
);
|
|
1537
1812
|
}
|
|
1538
1813
|
if (from === to) {
|
|
1539
1814
|
return;
|
|
@@ -1584,101 +1859,6 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
1584
1859
|
});
|
|
1585
1860
|
}
|
|
1586
1861
|
|
|
1587
|
-
// src/events/loaders.ts
|
|
1588
|
-
function loadEventParticipantViews(db, eventId) {
|
|
1589
|
-
const rows = db.prepare(
|
|
1590
|
-
`SELECT contact_id, role, directionality
|
|
1591
|
-
FROM event_participants
|
|
1592
|
-
WHERE event_id = ?`
|
|
1593
|
-
).all(eventId);
|
|
1594
|
-
return rows.map((row) => ({
|
|
1595
|
-
contactId: row.contact_id,
|
|
1596
|
-
role: row.role,
|
|
1597
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1598
|
-
}));
|
|
1599
|
-
}
|
|
1600
|
-
function loadEventRecord(db, eventId) {
|
|
1601
|
-
const row = getEventRowById(db, eventId);
|
|
1602
|
-
if (!row) {
|
|
1603
|
-
throw new AffinityInvariantError("event not found");
|
|
1604
|
-
}
|
|
1605
|
-
const participants = loadEventParticipantViews(db, eventId);
|
|
1606
|
-
return mapEventRowToEventRecord(row, participants);
|
|
1607
|
-
}
|
|
1608
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
1609
|
-
if (eventIds.length === 0) {
|
|
1610
|
-
return [];
|
|
1611
|
-
}
|
|
1612
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
1613
|
-
const eventRows = db.prepare(
|
|
1614
|
-
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
1615
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
1616
|
-
anchor_contact_id, anchor_link_id,
|
|
1617
|
-
created_at, updated_at, deleted_at
|
|
1618
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
1619
|
-
).all(...eventIds);
|
|
1620
|
-
const partRows = db.prepare(
|
|
1621
|
-
`SELECT event_id, contact_id, role, directionality
|
|
1622
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
1623
|
-
).all(...eventIds);
|
|
1624
|
-
const partsByEvent = /* @__PURE__ */ new Map();
|
|
1625
|
-
for (const p of partRows) {
|
|
1626
|
-
let arr = partsByEvent.get(p.event_id);
|
|
1627
|
-
if (!arr) {
|
|
1628
|
-
arr = [];
|
|
1629
|
-
partsByEvent.set(p.event_id, arr);
|
|
1630
|
-
}
|
|
1631
|
-
arr.push({
|
|
1632
|
-
contactId: p.contact_id,
|
|
1633
|
-
role: p.role,
|
|
1634
|
-
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1637
|
-
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
1638
|
-
const out = [];
|
|
1639
|
-
for (const id of eventIds) {
|
|
1640
|
-
const row = rowById.get(id);
|
|
1641
|
-
if (row) {
|
|
1642
|
-
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
return out;
|
|
1646
|
-
}
|
|
1647
|
-
function batchLoadEventParticipantViews(db, eventIds) {
|
|
1648
|
-
const result = /* @__PURE__ */ new Map();
|
|
1649
|
-
if (eventIds.length === 0) {
|
|
1650
|
-
return result;
|
|
1651
|
-
}
|
|
1652
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
1653
|
-
const rows = db.prepare(
|
|
1654
|
-
`SELECT event_id, contact_id, role, directionality
|
|
1655
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
1656
|
-
).all(...eventIds);
|
|
1657
|
-
for (const row of rows) {
|
|
1658
|
-
let arr = result.get(row.event_id);
|
|
1659
|
-
if (!arr) {
|
|
1660
|
-
arr = [];
|
|
1661
|
-
result.set(row.event_id, arr);
|
|
1662
|
-
}
|
|
1663
|
-
arr.push({
|
|
1664
|
-
contactId: row.contact_id,
|
|
1665
|
-
role: row.role,
|
|
1666
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
|
-
return result;
|
|
1670
|
-
}
|
|
1671
|
-
function requireDateAnchorEvent(db, eventId) {
|
|
1672
|
-
const row = getEventRowById(db, eventId);
|
|
1673
|
-
if (!row) {
|
|
1674
|
-
throw new AffinityNotFoundError("event not found");
|
|
1675
|
-
}
|
|
1676
|
-
if (row.type !== "date_anchor") {
|
|
1677
|
-
throw new AffinityInvariantError("not a date anchor");
|
|
1678
|
-
}
|
|
1679
|
-
return row;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
1862
|
// src/events/persistence.ts
|
|
1683
1863
|
function insertJournalEvent(db, params) {
|
|
1684
1864
|
const now = resolveNow(params.now);
|
|
@@ -2542,7 +2722,7 @@ function getLinkTimeline(db, linkId, options) {
|
|
|
2542
2722
|
}
|
|
2543
2723
|
|
|
2544
2724
|
// src/links/rollups.ts
|
|
2545
|
-
var
|
|
2725
|
+
var DAY_MS2 = 864e5;
|
|
2546
2726
|
function parseJsonObject2(raw) {
|
|
2547
2727
|
try {
|
|
2548
2728
|
const parsed = JSON.parse(raw);
|
|
@@ -2600,7 +2780,7 @@ function recencyScore(occurredAt, cadenceDays, now) {
|
|
|
2600
2780
|
return 0;
|
|
2601
2781
|
}
|
|
2602
2782
|
const cadence = Math.max(cadenceDays ?? 0, 1);
|
|
2603
|
-
const daysSince = Math.max(0, (now - occurredAt) /
|
|
2783
|
+
const daysSince = Math.max(0, (now - occurredAt) / DAY_MS2);
|
|
2604
2784
|
return Math.max(0, 1 - daysSince / cadence);
|
|
2605
2785
|
}
|
|
2606
2786
|
function refreshLinkRollup(db, linkId, now) {
|
|
@@ -2611,15 +2791,15 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2611
2791
|
const latest = db.prepare(
|
|
2612
2792
|
`SELECT event_id, occurred_at
|
|
2613
2793
|
FROM link_event_effects
|
|
2614
|
-
WHERE link_id = ?
|
|
2794
|
+
WHERE link_id = ? AND counts_for_recency = 1
|
|
2615
2795
|
ORDER BY occurred_at DESC, event_id DESC
|
|
2616
2796
|
LIMIT 1`
|
|
2617
2797
|
).get(linkId);
|
|
2618
|
-
const trailingWindowStart = now - 180 *
|
|
2798
|
+
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
2619
2799
|
const counts = db.prepare(
|
|
2620
2800
|
`SELECT
|
|
2621
2801
|
COUNT(*) AS total_meaningful_events,
|
|
2622
|
-
SUM(CASE WHEN affinity_delta > 0 OR trust_delta > 0 THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
2802
|
+
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
2623
2803
|
FROM link_event_effects
|
|
2624
2804
|
WHERE link_id = ? AND occurred_at >= ?`
|
|
2625
2805
|
).get(linkId, trailingWindowStart);
|
|
@@ -2627,7 +2807,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2627
2807
|
`SELECT ep.directionality
|
|
2628
2808
|
FROM link_event_effects le
|
|
2629
2809
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
2630
|
-
WHERE le.link_id = ? AND ep.contact_id = ?
|
|
2810
|
+
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
2631
2811
|
AND ep.directionality IS NOT NULL
|
|
2632
2812
|
AND le.occurred_at >= ?`
|
|
2633
2813
|
).all(linkId, link.from_contact_id, trailingWindowStart);
|
|
@@ -2653,7 +2833,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
2653
2833
|
link.cadence_days,
|
|
2654
2834
|
now
|
|
2655
2835
|
);
|
|
2656
|
-
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) /
|
|
2836
|
+
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) / DAY_MS2);
|
|
2657
2837
|
const driftPriorityValue = driftPriority({
|
|
2658
2838
|
daysSinceLastMeaningfulEvent,
|
|
2659
2839
|
cadenceDays: link.cadence_days ?? 1,
|
|
@@ -2970,13 +3150,15 @@ function listRadar(db, filters, options) {
|
|
|
2970
3150
|
if (ownerId === null) {
|
|
2971
3151
|
return [];
|
|
2972
3152
|
}
|
|
3153
|
+
const now = resolveNow(options?.now);
|
|
3154
|
+
const overdue = `EXISTS (SELECT 1 FROM open_commitments oc JOIN events e ON e.id = oc.event_id JOIN event_participants ep ON ep.event_id = e.id WHERE ep.contact_id = CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END AND e.deleted_at IS NULL AND oc.resolved_at IS NULL AND oc.due_at < ${now})`;
|
|
2973
3155
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
2974
3156
|
const clauses = [
|
|
2975
3157
|
"l.removed_at IS NULL",
|
|
2976
3158
|
"l.is_structural = 0",
|
|
2977
|
-
"l.from_contact_id = ?"
|
|
3159
|
+
"(l.from_contact_id = ? OR l.to_contact_id = ?)"
|
|
2978
3160
|
];
|
|
2979
|
-
const params = [ownerId];
|
|
3161
|
+
const params = [ownerId, ownerId];
|
|
2980
3162
|
if (filters?.kind !== void 0) {
|
|
2981
3163
|
clauses.push("l.kind = ?");
|
|
2982
3164
|
params.push(filters.kind);
|
|
@@ -2998,6 +3180,7 @@ function listRadar(db, filters, options) {
|
|
|
2998
3180
|
const rows = db.prepare(
|
|
2999
3181
|
`SELECT
|
|
3000
3182
|
l.id,
|
|
3183
|
+
${overdue} AS overdue_commitment,
|
|
3001
3184
|
l.from_contact_id,
|
|
3002
3185
|
l.to_contact_id,
|
|
3003
3186
|
l.trust,
|
|
@@ -3006,17 +3189,18 @@ function listRadar(db, filters, options) {
|
|
|
3006
3189
|
lr.recency_score,
|
|
3007
3190
|
lr.normalized_rank
|
|
3008
3191
|
FROM links l
|
|
3009
|
-
INNER JOIN contacts c_to ON c_to.id = l.to_contact_id
|
|
3192
|
+
INNER JOIN contacts c_to ON c_to.id = CASE WHEN l.from_contact_id = ${ownerId} THEN l.to_contact_id ELSE l.from_contact_id END
|
|
3010
3193
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
3011
3194
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
3012
3195
|
WHERE ${where}
|
|
3013
|
-
ORDER BY COALESCE(lr.radar_score, 0) DESC
|
|
3196
|
+
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
3014
3197
|
LIMIT ? OFFSET ?`
|
|
3015
3198
|
).all(...params, limit, offset);
|
|
3016
3199
|
return rows.map((row) => ({
|
|
3017
3200
|
linkId: row.id,
|
|
3018
3201
|
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
3019
|
-
|
|
3202
|
+
reasons: row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
3203
|
+
driftPriority: row.overdue_commitment ? Math.max(1, row.drift_priority ?? 0) : row.drift_priority ?? 0,
|
|
3020
3204
|
recencyScore: row.recency_score ?? 0,
|
|
3021
3205
|
normalizedRank: row.normalized_rank ?? 0,
|
|
3022
3206
|
trust: row.trust ?? 0,
|
|
@@ -3627,39 +3811,6 @@ function listDismissedDuplicates(db, options) {
|
|
|
3627
3811
|
}));
|
|
3628
3812
|
}
|
|
3629
3813
|
|
|
3630
|
-
// src/events/get_contact_journal.ts
|
|
3631
|
-
function getContactJournal(db, contactId, options) {
|
|
3632
|
-
assertDefaultOrdering("getContactJournal", options);
|
|
3633
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
3634
|
-
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
3635
|
-
const params = [contactId];
|
|
3636
|
-
if (options?.since !== void 0) {
|
|
3637
|
-
clauses.push("e.occurred_at >= ?");
|
|
3638
|
-
params.push(options.since);
|
|
3639
|
-
}
|
|
3640
|
-
if (options?.until !== void 0) {
|
|
3641
|
-
clauses.push("e.occurred_at <= ?");
|
|
3642
|
-
params.push(options.until);
|
|
3643
|
-
}
|
|
3644
|
-
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
3645
|
-
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
3646
|
-
clauses.push(`e.type IN (${ph})`);
|
|
3647
|
-
params.push(...options.eventTypes);
|
|
3648
|
-
}
|
|
3649
|
-
const where = clauses.join(" AND ");
|
|
3650
|
-
const rows = db.prepare(
|
|
3651
|
-
`SELECT e.id FROM events e
|
|
3652
|
-
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
3653
|
-
WHERE ${where}
|
|
3654
|
-
ORDER BY e.occurred_at DESC, e.id DESC
|
|
3655
|
-
LIMIT ? OFFSET ?`
|
|
3656
|
-
).all(...params, limit, offset);
|
|
3657
|
-
return batchLoadEventRecords(
|
|
3658
|
-
db,
|
|
3659
|
-
rows.map((r) => r.id)
|
|
3660
|
-
);
|
|
3661
|
-
}
|
|
3662
|
-
|
|
3663
3814
|
// src/events/mechanics.ts
|
|
3664
3815
|
function clamp2(value, min, max) {
|
|
3665
3816
|
return Math.min(max, Math.max(min, value));
|
|
@@ -3859,111 +4010,35 @@ function warmthMatch(eventType, eventValence) {
|
|
|
3859
4010
|
if (eventType === "transaction") {
|
|
3860
4011
|
return 0.5;
|
|
3861
4012
|
}
|
|
3862
|
-
if (eventType === "conflict") {
|
|
3863
|
-
return 0;
|
|
3864
|
-
}
|
|
3865
|
-
return 0.35;
|
|
3866
|
-
}
|
|
3867
|
-
function reliabilityMatch(params) {
|
|
3868
|
-
if (params?.supportDelivered === true) {
|
|
3869
|
-
return 1;
|
|
3870
|
-
}
|
|
3871
|
-
if (params?.resolution === "kept") {
|
|
3872
|
-
return 1;
|
|
3873
|
-
}
|
|
3874
|
-
if (params?.resolution === "broken") {
|
|
3875
|
-
return 0;
|
|
3876
|
-
}
|
|
3877
|
-
return 0.45;
|
|
3878
|
-
}
|
|
3879
|
-
function baseWeight(params) {
|
|
3880
|
-
return typeWeight(params.eventType) * (0.35 + 0.65 * intensity(params.significance)) * params.directness * params.preferenceMatch * params.novelty;
|
|
3881
|
-
}
|
|
3882
|
-
function massPenalty(weeklyPositiveBaseWeight2) {
|
|
3883
|
-
const excessWeeklyMass = Math.max(0, weeklyPositiveBaseWeight2 - 8);
|
|
3884
|
-
return 1 / (1 + excessWeeklyMass / 8);
|
|
3885
|
-
}
|
|
3886
|
-
function dateSalienceBonus(significance) {
|
|
3887
|
-
return 1 + Math.min(0.25, significance / 40);
|
|
3888
|
-
}
|
|
3889
|
-
function repairBonus(consecutiveRepairEvents) {
|
|
3890
|
-
return Math.min(0.12, consecutiveRepairEvents * 0.02);
|
|
3891
|
-
}
|
|
3892
|
-
|
|
3893
|
-
// src/events/list_open_commitments.ts
|
|
3894
|
-
var DAY_MS2 = 864e5;
|
|
3895
|
-
function mapResolution(resolvedAt, resolution) {
|
|
3896
|
-
if (resolvedAt === null) {
|
|
3897
|
-
return "open";
|
|
3898
|
-
}
|
|
3899
|
-
if (resolution === "cancelled") {
|
|
3900
|
-
return "cancelled";
|
|
3901
|
-
}
|
|
3902
|
-
return "resolved";
|
|
3903
|
-
}
|
|
3904
|
-
function listOpenCommitments(db, filters, options) {
|
|
3905
|
-
assertDefaultOrdering("listOpenCommitments", options);
|
|
3906
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
3907
|
-
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
3908
|
-
const params = [];
|
|
3909
|
-
if (options?.since !== void 0) {
|
|
3910
|
-
clauses.push("e.occurred_at >= ?");
|
|
3911
|
-
params.push(options.since);
|
|
3912
|
-
}
|
|
3913
|
-
if (options?.until !== void 0) {
|
|
3914
|
-
clauses.push("e.occurred_at <= ?");
|
|
3915
|
-
params.push(options.until);
|
|
3916
|
-
}
|
|
3917
|
-
if (filters?.commitmentType !== void 0) {
|
|
3918
|
-
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
3919
|
-
throw new AffinityValidationError("invalid commitmentType filter");
|
|
3920
|
-
}
|
|
3921
|
-
clauses.push("oc.commitment_type = ?");
|
|
3922
|
-
params.push(filters.commitmentType);
|
|
3923
|
-
}
|
|
3924
|
-
if (filters?.contactId !== void 0) {
|
|
3925
|
-
clauses.push(
|
|
3926
|
-
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
3927
|
-
);
|
|
3928
|
-
params.push(filters.contactId);
|
|
3929
|
-
}
|
|
3930
|
-
if (filters?.linkId !== void 0) {
|
|
3931
|
-
clauses.push(
|
|
3932
|
-
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
3933
|
-
);
|
|
3934
|
-
params.push(filters.linkId);
|
|
3935
|
-
}
|
|
3936
|
-
if (filters?.horizonDays !== void 0) {
|
|
3937
|
-
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
3938
|
-
throw new AffinityValidationError(
|
|
3939
|
-
"horizonDays must be a non-negative integer"
|
|
3940
|
-
);
|
|
3941
|
-
}
|
|
3942
|
-
const anchor = options?.since ?? Date.now();
|
|
3943
|
-
clauses.push("oc.due_at IS NOT NULL");
|
|
3944
|
-
clauses.push("oc.due_at <= ?");
|
|
3945
|
-
params.push(anchor + filters.horizonDays * DAY_MS2);
|
|
3946
|
-
}
|
|
3947
|
-
const where = clauses.join(" AND ");
|
|
3948
|
-
const rows = db.prepare(
|
|
3949
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
3950
|
-
e.type, e.summary
|
|
3951
|
-
FROM open_commitments oc
|
|
3952
|
-
INNER JOIN events e ON e.id = oc.event_id
|
|
3953
|
-
WHERE ${where}
|
|
3954
|
-
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
3955
|
-
LIMIT ? OFFSET ?`
|
|
3956
|
-
).all(...params, limit, offset);
|
|
3957
|
-
const eventIds = rows.map((r) => r.event_id);
|
|
3958
|
-
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
3959
|
-
return rows.map((r) => ({
|
|
3960
|
-
eventId: r.event_id,
|
|
3961
|
-
type: r.type,
|
|
3962
|
-
summary: r.summary,
|
|
3963
|
-
participants: partsByEvent.get(r.event_id) ?? [],
|
|
3964
|
-
dueAt: r.due_at,
|
|
3965
|
-
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
3966
|
-
}));
|
|
4013
|
+
if (eventType === "conflict") {
|
|
4014
|
+
return 0;
|
|
4015
|
+
}
|
|
4016
|
+
return 0.35;
|
|
4017
|
+
}
|
|
4018
|
+
function reliabilityMatch(params) {
|
|
4019
|
+
if (params?.supportDelivered === true) {
|
|
4020
|
+
return 1;
|
|
4021
|
+
}
|
|
4022
|
+
if (params?.resolution === "kept") {
|
|
4023
|
+
return 1;
|
|
4024
|
+
}
|
|
4025
|
+
if (params?.resolution === "broken") {
|
|
4026
|
+
return 0;
|
|
4027
|
+
}
|
|
4028
|
+
return 0.45;
|
|
4029
|
+
}
|
|
4030
|
+
function baseWeight(params) {
|
|
4031
|
+
return typeWeight(params.eventType) * (0.35 + 0.65 * intensity(params.significance)) * params.directness * params.preferenceMatch * params.novelty;
|
|
4032
|
+
}
|
|
4033
|
+
function massPenalty(weeklyPositiveBaseWeight2) {
|
|
4034
|
+
const excessWeeklyMass = Math.max(0, weeklyPositiveBaseWeight2 - 8);
|
|
4035
|
+
return 1 / (1 + excessWeeklyMass / 8);
|
|
4036
|
+
}
|
|
4037
|
+
function dateSalienceBonus(significance) {
|
|
4038
|
+
return 1 + Math.min(0.25, significance / 40);
|
|
4039
|
+
}
|
|
4040
|
+
function repairBonus(consecutiveRepairEvents) {
|
|
4041
|
+
return Math.min(0.12, consecutiveRepairEvents * 0.02);
|
|
3967
4042
|
}
|
|
3968
4043
|
|
|
3969
4044
|
// src/links/effects.ts
|
|
@@ -4243,7 +4318,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
4243
4318
|
1,
|
|
4244
4319
|
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
4245
4320
|
);
|
|
4246
|
-
const cadenceAfter = nextCadenceDays({
|
|
4321
|
+
const cadenceAfter = commitmentResolution === "broken" || commitmentResolution === "cancelled" ? cadenceBefore : nextCadenceDays({
|
|
4247
4322
|
kind: linkKind,
|
|
4248
4323
|
currentCadenceDays: cadenceBefore,
|
|
4249
4324
|
gapDays
|
|
@@ -4306,6 +4381,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
4306
4381
|
params.now,
|
|
4307
4382
|
params.now
|
|
4308
4383
|
);
|
|
4384
|
+
if (commitmentResolution === "broken" || commitmentResolution === "cancelled") {
|
|
4385
|
+
db.prepare(
|
|
4386
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4387
|
+
).run(params.eventId, params.linkId);
|
|
4388
|
+
}
|
|
4309
4389
|
refreshLinkRollup(db, params.linkId, params.now);
|
|
4310
4390
|
return {
|
|
4311
4391
|
linkId: params.linkId,
|
|
@@ -4685,12 +4765,16 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4685
4765
|
if (row.type !== "promise" && row.type !== "agreement") {
|
|
4686
4766
|
throw new AffinityInvariantError("not a commitment event");
|
|
4687
4767
|
}
|
|
4688
|
-
const open = db.prepare(
|
|
4768
|
+
const open = db.prepare(
|
|
4769
|
+
"SELECT resolved_at, resolution FROM open_commitments WHERE event_id = ?"
|
|
4770
|
+
).get(commitmentEventId);
|
|
4689
4771
|
if (!open) {
|
|
4690
4772
|
throw new AffinityNotFoundError("open commitment not found");
|
|
4691
4773
|
}
|
|
4692
4774
|
if (open.resolved_at !== null) {
|
|
4693
|
-
throw new
|
|
4775
|
+
throw new AffinityConflictError(
|
|
4776
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4777
|
+
);
|
|
4694
4778
|
}
|
|
4695
4779
|
const now = resolveNow(options?.now);
|
|
4696
4780
|
const originalParticipants = loadEventParticipantViews(
|
|
@@ -4703,7 +4787,9 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4703
4787
|
WHERE event_id = ? AND resolved_at IS NULL`
|
|
4704
4788
|
).run(resolution, now, now, commitmentEventId);
|
|
4705
4789
|
if (Number(result.changes ?? 0) === 0) {
|
|
4706
|
-
throw new
|
|
4790
|
+
throw new AffinityConflictError(
|
|
4791
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4792
|
+
);
|
|
4707
4793
|
}
|
|
4708
4794
|
const shouldCreateResolvingEvent = options?.resolvingEvent !== void 0 || resolution === "broken";
|
|
4709
4795
|
const created = [];
|
|
@@ -4766,6 +4852,82 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4766
4852
|
});
|
|
4767
4853
|
}
|
|
4768
4854
|
|
|
4855
|
+
// src/contacts/export_contact.ts
|
|
4856
|
+
function exportContact(db, id) {
|
|
4857
|
+
const query = (sql, ...params) => db.prepare(sql).all(...params).map((row) => {
|
|
4858
|
+
const result = {};
|
|
4859
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4860
|
+
const name = key.replace(
|
|
4861
|
+
/_([a-z])/g,
|
|
4862
|
+
(_, letter) => letter.toUpperCase()
|
|
4863
|
+
);
|
|
4864
|
+
result[name] = value;
|
|
4865
|
+
if ((name.endsWith("At") || name === "dueAt") && typeof value === "number")
|
|
4866
|
+
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4867
|
+
if (key.endsWith("_json") && typeof value === "string")
|
|
4868
|
+
result[name] = JSON.parse(value);
|
|
4869
|
+
}
|
|
4870
|
+
return result;
|
|
4871
|
+
});
|
|
4872
|
+
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4873
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4874
|
+
const events = query(
|
|
4875
|
+
"SELECT DISTINCT e.* FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? ORDER BY e.id",
|
|
4876
|
+
id
|
|
4877
|
+
).map((event) => ({
|
|
4878
|
+
...event,
|
|
4879
|
+
participants: query(
|
|
4880
|
+
"SELECT p.*, c.name AS contact_name FROM event_participants p JOIN contacts c ON c.id = p.contact_id WHERE event_id = ? ORDER BY p.contact_id",
|
|
4881
|
+
event.id
|
|
4882
|
+
)
|
|
4883
|
+
}));
|
|
4884
|
+
return {
|
|
4885
|
+
contact,
|
|
4886
|
+
identities: query(
|
|
4887
|
+
"SELECT * FROM identities WHERE contact_id = ? ORDER BY id",
|
|
4888
|
+
id
|
|
4889
|
+
),
|
|
4890
|
+
attributes: query(
|
|
4891
|
+
"SELECT * FROM attributes WHERE contact_id = ? OR link_id IN (SELECT id FROM links WHERE from_contact_id = ? OR to_contact_id = ?) ORDER BY id",
|
|
4892
|
+
id,
|
|
4893
|
+
id,
|
|
4894
|
+
id
|
|
4895
|
+
),
|
|
4896
|
+
links: query(
|
|
4897
|
+
"SELECT l.*, f.name AS from_contact_name, t.name AS to_contact_name FROM links l JOIN contacts f ON f.id = l.from_contact_id JOIN contacts t ON t.id = l.to_contact_id WHERE from_contact_id = ? OR to_contact_id = ? ORDER BY l.id",
|
|
4898
|
+
id,
|
|
4899
|
+
id
|
|
4900
|
+
),
|
|
4901
|
+
events,
|
|
4902
|
+
commitments: query(
|
|
4903
|
+
"SELECT o.* FROM open_commitments o JOIN event_participants p ON p.event_id = o.event_id WHERE p.contact_id = ? ORDER BY o.event_id",
|
|
4904
|
+
id
|
|
4905
|
+
),
|
|
4906
|
+
dates: query(
|
|
4907
|
+
"SELECT * FROM events WHERE anchor_contact_id = ? OR anchor_link_id IN (SELECT id FROM links WHERE from_contact_id = ? OR to_contact_id = ?) ORDER BY id",
|
|
4908
|
+
id,
|
|
4909
|
+
id,
|
|
4910
|
+
id
|
|
4911
|
+
),
|
|
4912
|
+
mergeHistory: query(
|
|
4913
|
+
"SELECT * FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ? ORDER BY id",
|
|
4914
|
+
id,
|
|
4915
|
+
id
|
|
4916
|
+
)
|
|
4917
|
+
};
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
// src/contacts/export_affinity.ts
|
|
4921
|
+
function exportAffinity(db) {
|
|
4922
|
+
const now = Date.now();
|
|
4923
|
+
return {
|
|
4924
|
+
version: 1,
|
|
4925
|
+
exportedAt: now,
|
|
4926
|
+
exportedAtIso: new Date(now).toISOString(),
|
|
4927
|
+
contacts: db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id))
|
|
4928
|
+
};
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4769
4931
|
// src/soul.ts
|
|
4770
4932
|
var soul_exports = {};
|
|
4771
4933
|
__export(soul_exports, {
|
|
@@ -5533,6 +5695,7 @@ __export(tools_exports, {
|
|
|
5533
5695
|
numberSchema: () => numberSchema,
|
|
5534
5696
|
objectSchema: () => objectSchema,
|
|
5535
5697
|
oneOfSchema: () => oneOfSchema,
|
|
5698
|
+
parseTimeInput: () => parseTimeInput,
|
|
5536
5699
|
recordEventTool: () => recordEventTool,
|
|
5537
5700
|
recordEventToolHandler: () => recordEventToolHandler,
|
|
5538
5701
|
recordEventToolName: () => recordEventToolName,
|
|
@@ -5566,8 +5729,55 @@ __export(tools_exports, {
|
|
|
5566
5729
|
withToolHandling: () => withToolHandling
|
|
5567
5730
|
});
|
|
5568
5731
|
|
|
5732
|
+
// src/tools/parse_time_input.ts
|
|
5733
|
+
var timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
5734
|
+
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
5735
|
+
const fail = () => {
|
|
5736
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
5737
|
+
};
|
|
5738
|
+
if (typeof value === "number")
|
|
5739
|
+
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
5740
|
+
if (typeof value !== "string") return fail();
|
|
5741
|
+
const text = value.trim();
|
|
5742
|
+
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
5743
|
+
let day;
|
|
5744
|
+
if (relative) {
|
|
5745
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
5746
|
+
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
5747
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
5748
|
+
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
5749
|
+
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
5750
|
+
return fail();
|
|
5751
|
+
} else {
|
|
5752
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
5753
|
+
text
|
|
5754
|
+
))
|
|
5755
|
+
return fail();
|
|
5756
|
+
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
5757
|
+
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
5758
|
+
return fail();
|
|
5759
|
+
const parsed = Date.parse(text);
|
|
5760
|
+
return Number.isFinite(parsed) ? parsed : fail();
|
|
5761
|
+
}
|
|
5762
|
+
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
5763
|
+
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5569
5766
|
// src/tools/tool_metadata.ts
|
|
5570
5767
|
function defineAffinityTool(definition) {
|
|
5768
|
+
const visit = (schema) => {
|
|
5769
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
5770
|
+
if ((key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.type === "integer") {
|
|
5771
|
+
child.type = void 0;
|
|
5772
|
+
child.oneOf = [{ type: "integer" }, { type: "string" }];
|
|
5773
|
+
child.description = `${child.description ?? "Time."} Accepts ${timeInputForms}. UTC midnight for dates; dueAt uses UTC end of day.`;
|
|
5774
|
+
}
|
|
5775
|
+
visit(child);
|
|
5776
|
+
}
|
|
5777
|
+
if (schema.items) visit(schema.items);
|
|
5778
|
+
for (const child of schema.oneOf ?? schema.anyOf ?? []) visit(child);
|
|
5779
|
+
};
|
|
5780
|
+
visit(definition.inputSchema);
|
|
5571
5781
|
return definition;
|
|
5572
5782
|
}
|
|
5573
5783
|
function stringSchema(description, options = {}) {
|
|
@@ -5666,6 +5876,7 @@ function toContactEntityRef(value) {
|
|
|
5666
5876
|
function toContactListToolItem(value) {
|
|
5667
5877
|
return {
|
|
5668
5878
|
...toContactEntityRef(value),
|
|
5879
|
+
..."matchedOn" in value ? { matchedOn: value.matchedOn } : {},
|
|
5669
5880
|
subtitle: "kind" in value ? value.kind : void 0
|
|
5670
5881
|
};
|
|
5671
5882
|
}
|
|
@@ -5712,7 +5923,9 @@ function toEventEntityRef(value) {
|
|
|
5712
5923
|
}
|
|
5713
5924
|
function toEventListToolItem(value) {
|
|
5714
5925
|
return {
|
|
5926
|
+
...value,
|
|
5715
5927
|
...toEventEntityRef(value),
|
|
5928
|
+
occurredAtIso: new Date(value.occurredAt).toISOString(),
|
|
5716
5929
|
snippet: `${value.participants.length} participant(s)`
|
|
5717
5930
|
};
|
|
5718
5931
|
}
|
|
@@ -5769,7 +5982,7 @@ function withOptionalFields(result, options) {
|
|
|
5769
5982
|
if (options.warnings && options.warnings.length > 0) {
|
|
5770
5983
|
result.warnings = options.warnings;
|
|
5771
5984
|
}
|
|
5772
|
-
return result;
|
|
5985
|
+
return roundToolValues(result);
|
|
5773
5986
|
}
|
|
5774
5987
|
function toolWarning(code, message) {
|
|
5775
5988
|
return { code, message };
|
|
@@ -5833,9 +6046,31 @@ function toolFailure(kind, code, summary, message, options = {}) {
|
|
|
5833
6046
|
options
|
|
5834
6047
|
);
|
|
5835
6048
|
}
|
|
6049
|
+
function roundToolValues(value) {
|
|
6050
|
+
if (typeof value === "number")
|
|
6051
|
+
return Number.isInteger(value) ? value : Math.round(value * 1e3) / 1e3;
|
|
6052
|
+
if (Array.isArray(value)) return value.map(roundToolValues);
|
|
6053
|
+
if (value !== null && typeof value === "object")
|
|
6054
|
+
return Object.fromEntries(
|
|
6055
|
+
Object.entries(value).map(([key, child]) => [
|
|
6056
|
+
key,
|
|
6057
|
+
roundToolValues(child)
|
|
6058
|
+
])
|
|
6059
|
+
);
|
|
6060
|
+
return value;
|
|
6061
|
+
}
|
|
5836
6062
|
|
|
5837
6063
|
// src/tools/tool_resolvers.ts
|
|
5838
|
-
function resolveContactLocator(db, locator, field = "contact") {
|
|
6064
|
+
function resolveContactLocator(db, locator, field = "contact", allowMerged = false) {
|
|
6065
|
+
if (!locator || typeof locator !== "object")
|
|
6066
|
+
return {
|
|
6067
|
+
ok: false,
|
|
6068
|
+
result: toolNeedsClarification(
|
|
6069
|
+
"missing_required_choice",
|
|
6070
|
+
`Provide ${field} locator.`,
|
|
6071
|
+
[field]
|
|
6072
|
+
)
|
|
6073
|
+
};
|
|
5839
6074
|
if ("contactId" in locator) {
|
|
5840
6075
|
const row2 = getContactRowById(db, locator.contactId);
|
|
5841
6076
|
if (!row2) {
|
|
@@ -5849,8 +6084,25 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
5849
6084
|
)
|
|
5850
6085
|
};
|
|
5851
6086
|
}
|
|
6087
|
+
if (!allowMerged && row2.merged_into_contact_id !== null) {
|
|
6088
|
+
const winner = getContactRowById(db, row2.merged_into_contact_id);
|
|
6089
|
+
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
6090
|
+
return {
|
|
6091
|
+
ok: false,
|
|
6092
|
+
result: toolFailure("domain", "invalid_state", message, message)
|
|
6093
|
+
};
|
|
6094
|
+
}
|
|
5852
6095
|
return { ok: true, value: row2 };
|
|
5853
6096
|
}
|
|
6097
|
+
if (!("identity" in locator) || !locator.identity)
|
|
6098
|
+
return {
|
|
6099
|
+
ok: false,
|
|
6100
|
+
result: toolNeedsClarification(
|
|
6101
|
+
"missing_required_choice",
|
|
6102
|
+
`Provide ${field}.contactId or identity.`,
|
|
6103
|
+
[field]
|
|
6104
|
+
)
|
|
6105
|
+
};
|
|
5854
6106
|
const normalizedKey = normalizeIdentityKey(
|
|
5855
6107
|
locator.identity.type,
|
|
5856
6108
|
locator.identity.value
|
|
@@ -5908,6 +6160,15 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
5908
6160
|
return { ok: true, value: matchedContact };
|
|
5909
6161
|
}
|
|
5910
6162
|
function resolveLinkLocator(db, locator, field = "link") {
|
|
6163
|
+
if (!locator || typeof locator !== "object")
|
|
6164
|
+
return {
|
|
6165
|
+
ok: false,
|
|
6166
|
+
result: toolNeedsClarification(
|
|
6167
|
+
"missing_required_choice",
|
|
6168
|
+
`Provide ${field} locator.`,
|
|
6169
|
+
[field]
|
|
6170
|
+
)
|
|
6171
|
+
};
|
|
5911
6172
|
if ("linkId" in locator) {
|
|
5912
6173
|
const row = getLinkRowById(db, locator.linkId);
|
|
5913
6174
|
if (!row) {
|
|
@@ -5923,6 +6184,15 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
5923
6184
|
}
|
|
5924
6185
|
return { ok: true, value: row };
|
|
5925
6186
|
}
|
|
6187
|
+
if (!("endpoints" in locator) || !locator.endpoints)
|
|
6188
|
+
return {
|
|
6189
|
+
ok: false,
|
|
6190
|
+
result: toolNeedsClarification(
|
|
6191
|
+
"missing_required_choice",
|
|
6192
|
+
`Provide ${field}.linkId or endpoints.`,
|
|
6193
|
+
[field]
|
|
6194
|
+
)
|
|
6195
|
+
};
|
|
5926
6196
|
const { endpoints } = locator;
|
|
5927
6197
|
const clauses = [
|
|
5928
6198
|
"removed_at IS NULL",
|
|
@@ -6041,6 +6311,92 @@ function affinityErrorCodeToToolCode(error) {
|
|
|
6041
6311
|
}
|
|
6042
6312
|
}
|
|
6043
6313
|
|
|
6314
|
+
// src/tools/validate_tool_input.ts
|
|
6315
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
6316
|
+
if (schema.anyOf || schema.oneOf) {
|
|
6317
|
+
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
6318
|
+
return choices.some(
|
|
6319
|
+
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
6320
|
+
) ? null : `${path}: invalid value`;
|
|
6321
|
+
}
|
|
6322
|
+
if (schema.type === "object") {
|
|
6323
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
6324
|
+
return `${path}: missing input or expected object`;
|
|
6325
|
+
const object = value;
|
|
6326
|
+
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
6327
|
+
for (const key of Object.keys(object)) {
|
|
6328
|
+
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
6329
|
+
return key === "kind" && "name" in (schema.properties ?? {}) ? "kind cannot be changed after creation" : `${path}.${key}: unknown patch key; allowed keys: ${Object.keys(schema.properties ?? {}).join(", ")}`;
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6332
|
+
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
6333
|
+
return `${path}.patch: missing required field`;
|
|
6334
|
+
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
6335
|
+
return `${path}.patch: unknown patch key; allowed keys: none (use the action's named fields)`;
|
|
6336
|
+
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
6337
|
+
try {
|
|
6338
|
+
return parseTimeInput(
|
|
6339
|
+
object.now,
|
|
6340
|
+
`${path}.now`,
|
|
6341
|
+
injectedNow
|
|
6342
|
+
);
|
|
6343
|
+
} catch {
|
|
6344
|
+
return Date.now();
|
|
6345
|
+
}
|
|
6346
|
+
})();
|
|
6347
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6348
|
+
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
6349
|
+
try {
|
|
6350
|
+
object[key] = parseTimeInput(
|
|
6351
|
+
object[key],
|
|
6352
|
+
`${path}.${key}`,
|
|
6353
|
+
referenceNow
|
|
6354
|
+
);
|
|
6355
|
+
} catch (error) {
|
|
6356
|
+
return error.message;
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
for (const key of schema.required ?? []) {
|
|
6361
|
+
if (object[key] === void 0)
|
|
6362
|
+
return `${path}.${key}: missing required field`;
|
|
6363
|
+
}
|
|
6364
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
6365
|
+
if (object[key] === void 0) continue;
|
|
6366
|
+
const error = validateToolInput(
|
|
6367
|
+
child,
|
|
6368
|
+
object[key],
|
|
6369
|
+
`${path}.${key}`,
|
|
6370
|
+
referenceNow
|
|
6371
|
+
);
|
|
6372
|
+
if (error) return error;
|
|
6373
|
+
}
|
|
6374
|
+
return null;
|
|
6375
|
+
}
|
|
6376
|
+
if (schema.type === "array") {
|
|
6377
|
+
if (!Array.isArray(value)) return `${path}: expected array`;
|
|
6378
|
+
for (const [index, item] of value.entries()) {
|
|
6379
|
+
const error = validateToolInput(
|
|
6380
|
+
schema.items ?? {},
|
|
6381
|
+
item,
|
|
6382
|
+
`${path}[${index}]`,
|
|
6383
|
+
injectedNow
|
|
6384
|
+
);
|
|
6385
|
+
if (error) return error;
|
|
6386
|
+
}
|
|
6387
|
+
return null;
|
|
6388
|
+
}
|
|
6389
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
6390
|
+
return `${path}: expected ${schema.enum.join(", ")}`;
|
|
6391
|
+
if (schema.type === "null")
|
|
6392
|
+
return value === null ? null : `${path}: expected null`;
|
|
6393
|
+
if (schema.type === "integer")
|
|
6394
|
+
return Number.isInteger(value) ? null : `${path}: expected integer`;
|
|
6395
|
+
const valid = schema.type === void 0 || schema.type === "string" && typeof value === "string" || schema.type === "number" && typeof value === "number" && Number.isFinite(value) || schema.type === "boolean" && typeof value === "boolean";
|
|
6396
|
+
if (!valid) return `${path}: expected ${schema.type}`;
|
|
6397
|
+
return null;
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6044
6400
|
// src/tools/inspect_affinity_item_tool.ts
|
|
6045
6401
|
function contactLocatorSchema(description) {
|
|
6046
6402
|
return {
|
|
@@ -6076,7 +6432,19 @@ function linkLocatorSchema(description) {
|
|
|
6076
6432
|
description
|
|
6077
6433
|
};
|
|
6078
6434
|
}
|
|
6079
|
-
function inspectAffinityItemToolHandler(db,
|
|
6435
|
+
function inspectAffinityItemToolHandler(db, rawInput) {
|
|
6436
|
+
const input = structuredClone(rawInput);
|
|
6437
|
+
const inputError = validateToolInput(
|
|
6438
|
+
inspectAffinityItemTool.inputSchema,
|
|
6439
|
+
input
|
|
6440
|
+
);
|
|
6441
|
+
if (inputError)
|
|
6442
|
+
return toolFailure(
|
|
6443
|
+
"protocol",
|
|
6444
|
+
"invalid_input",
|
|
6445
|
+
"Invalid input.",
|
|
6446
|
+
inputError
|
|
6447
|
+
);
|
|
6080
6448
|
return withToolHandling(() => {
|
|
6081
6449
|
if (input.kind === "owner_profile") {
|
|
6082
6450
|
const detail2 = getOwnerProfile(
|
|
@@ -6094,19 +6462,21 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
6094
6462
|
return toolSuccess(
|
|
6095
6463
|
`Loaded owner profile for ${detail2.contact.name}.`,
|
|
6096
6464
|
{ kind: input.kind, detail: detail2 },
|
|
6097
|
-
{
|
|
6465
|
+
{
|
|
6466
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
6467
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
6468
|
+
}
|
|
6098
6469
|
);
|
|
6099
6470
|
}
|
|
6100
6471
|
if (input.kind === "contact_profile") {
|
|
6101
|
-
const contact = resolveContactLocator(db, input.contact, "contact");
|
|
6472
|
+
const contact = resolveContactLocator(db, input.contact, "contact", true);
|
|
6102
6473
|
if (!contact.ok) {
|
|
6103
6474
|
return contact.result;
|
|
6104
6475
|
}
|
|
6105
|
-
const detail2 = getContactProfile(
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
);
|
|
6476
|
+
const detail2 = getContactProfile(db, contact.value.id, {
|
|
6477
|
+
...input.topLinksLimit === void 0 ? {} : { topLinksLimit: input.topLinksLimit },
|
|
6478
|
+
...input.recentEventsLimit === void 0 ? {} : { recentEventsLimit: input.recentEventsLimit }
|
|
6479
|
+
});
|
|
6110
6480
|
if (!detail2) {
|
|
6111
6481
|
return toolFailure(
|
|
6112
6482
|
"domain",
|
|
@@ -6118,7 +6488,10 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
6118
6488
|
return toolSuccess(
|
|
6119
6489
|
`Loaded contact profile for ${detail2.contact.name}.`,
|
|
6120
6490
|
{ kind: input.kind, detail: detail2 },
|
|
6121
|
-
{
|
|
6491
|
+
{
|
|
6492
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
6493
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
6494
|
+
}
|
|
6122
6495
|
);
|
|
6123
6496
|
}
|
|
6124
6497
|
const link = resolveLinkLocator(db, input.link, "link");
|
|
@@ -6459,9 +6832,17 @@ function unsetAttribute(db, target, name, removedAt) {
|
|
|
6459
6832
|
}
|
|
6460
6833
|
|
|
6461
6834
|
// src/tools/tool_mutation.ts
|
|
6462
|
-
function mutationToolResult(action, receipt, entitySummary) {
|
|
6835
|
+
function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
6463
6836
|
const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
|
|
6464
6837
|
const data = {
|
|
6838
|
+
changes: ["created", "updated", "archived", "removed"].flatMap(
|
|
6839
|
+
(verb) => receipt[verb].map((ref) => ({
|
|
6840
|
+
entity: ref.kind,
|
|
6841
|
+
id: ref.id,
|
|
6842
|
+
action: verb,
|
|
6843
|
+
...verb === "updated" && fields ? { fields } : {}
|
|
6844
|
+
}))
|
|
6845
|
+
),
|
|
6465
6846
|
action,
|
|
6466
6847
|
primary: receipt.primary,
|
|
6467
6848
|
created: receipt.created,
|
|
@@ -6548,7 +6929,23 @@ function resolveAttributeTarget(db, target) {
|
|
|
6548
6929
|
}
|
|
6549
6930
|
return { kind: "ok", target: { kind: "link", id: link.value.id } };
|
|
6550
6931
|
}
|
|
6551
|
-
function manageAttributeToolHandler(db,
|
|
6932
|
+
function manageAttributeToolHandler(db, rawInput) {
|
|
6933
|
+
const input = structuredClone(rawInput);
|
|
6934
|
+
const inputError = validateToolInput(manageAttributeTool.inputSchema, input);
|
|
6935
|
+
if (inputError)
|
|
6936
|
+
return toolFailure(
|
|
6937
|
+
"protocol",
|
|
6938
|
+
"invalid_input",
|
|
6939
|
+
"Invalid input.",
|
|
6940
|
+
inputError
|
|
6941
|
+
);
|
|
6942
|
+
if (!input.target)
|
|
6943
|
+
return toolFailure(
|
|
6944
|
+
"protocol",
|
|
6945
|
+
"invalid_input",
|
|
6946
|
+
"Missing target.",
|
|
6947
|
+
"target is required."
|
|
6948
|
+
);
|
|
6552
6949
|
return withToolHandling(() => {
|
|
6553
6950
|
const resolved = resolveAttributeTarget(db, input.target);
|
|
6554
6951
|
if (resolved.kind === "error") {
|
|
@@ -6659,9 +7056,25 @@ function ensureOwnerParticipates(db, participants) {
|
|
|
6659
7056
|
function prepareParticipants(db, raw) {
|
|
6660
7057
|
return ensureOwnerParticipates(db, raw.map(normalizeParticipant));
|
|
6661
7058
|
}
|
|
6662
|
-
function manageCommitmentToolHandler(db,
|
|
7059
|
+
function manageCommitmentToolHandler(db, rawInput) {
|
|
7060
|
+
const input = structuredClone(rawInput);
|
|
7061
|
+
const inputError = validateToolInput(manageCommitmentTool.inputSchema, input);
|
|
7062
|
+
if (inputError)
|
|
7063
|
+
return toolFailure(
|
|
7064
|
+
"protocol",
|
|
7065
|
+
"invalid_input",
|
|
7066
|
+
"Invalid input.",
|
|
7067
|
+
inputError
|
|
7068
|
+
);
|
|
6663
7069
|
return withToolHandling(() => {
|
|
6664
7070
|
if (input.action === "record") {
|
|
7071
|
+
if (!input.input)
|
|
7072
|
+
return toolFailure(
|
|
7073
|
+
"protocol",
|
|
7074
|
+
"invalid_input",
|
|
7075
|
+
"Missing input.",
|
|
7076
|
+
"input is required for record."
|
|
7077
|
+
);
|
|
6665
7078
|
const patched = {
|
|
6666
7079
|
...input.input,
|
|
6667
7080
|
participants: prepareParticipants(
|
|
@@ -6728,7 +7141,7 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
6728
7141
|
role: stringSchema("Participant role."),
|
|
6729
7142
|
directionality: stringSchema("Optional directionality.")
|
|
6730
7143
|
},
|
|
6731
|
-
["
|
|
7144
|
+
["role"]
|
|
6732
7145
|
),
|
|
6733
7146
|
"Participant list. Must be non-empty and include the owner contact."
|
|
6734
7147
|
),
|
|
@@ -6752,28 +7165,282 @@ var manageCommitmentTool = defineAffinityTool({
|
|
|
6752
7165
|
),
|
|
6753
7166
|
options: objectSchema(
|
|
6754
7167
|
{
|
|
6755
|
-
now: integerSchema("Optional timestamp."),
|
|
6756
|
-
resolvingEvent: objectSchema(
|
|
6757
|
-
{
|
|
6758
|
-
type: stringSchema("Optional resolving event type."),
|
|
6759
|
-
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
6760
|
-
summary: stringSchema("Optional resolving event summary."),
|
|
6761
|
-
significance: integerSchema(
|
|
6762
|
-
"Optional resolving event significance."
|
|
6763
|
-
)
|
|
6764
|
-
},
|
|
6765
|
-
[]
|
|
6766
|
-
)
|
|
6767
|
-
},
|
|
6768
|
-
[],
|
|
6769
|
-
"Optional metadata."
|
|
6770
|
-
)
|
|
6771
|
-
},
|
|
6772
|
-
["action"],
|
|
6773
|
-
"Record or resolve one commitment."
|
|
6774
|
-
),
|
|
6775
|
-
handler: manageCommitmentToolHandler
|
|
6776
|
-
});
|
|
7168
|
+
now: integerSchema("Optional timestamp."),
|
|
7169
|
+
resolvingEvent: objectSchema(
|
|
7170
|
+
{
|
|
7171
|
+
type: stringSchema("Optional resolving event type."),
|
|
7172
|
+
occurredAt: integerSchema("Optional resolving event timestamp."),
|
|
7173
|
+
summary: stringSchema("Optional resolving event summary."),
|
|
7174
|
+
significance: integerSchema(
|
|
7175
|
+
"Optional resolving event significance."
|
|
7176
|
+
)
|
|
7177
|
+
},
|
|
7178
|
+
[]
|
|
7179
|
+
)
|
|
7180
|
+
},
|
|
7181
|
+
[],
|
|
7182
|
+
"Optional metadata."
|
|
7183
|
+
)
|
|
7184
|
+
},
|
|
7185
|
+
["action"],
|
|
7186
|
+
"Record or resolve one commitment."
|
|
7187
|
+
),
|
|
7188
|
+
handler: manageCommitmentToolHandler
|
|
7189
|
+
});
|
|
7190
|
+
|
|
7191
|
+
// src/contacts/delete_contact.ts
|
|
7192
|
+
function deleteContact(db, locatorId, options) {
|
|
7193
|
+
if (options?.confirm !== true)
|
|
7194
|
+
throw new AffinityValidationError(
|
|
7195
|
+
"delete is permanent; pass confirm: true"
|
|
7196
|
+
);
|
|
7197
|
+
return withTransaction(db, () => {
|
|
7198
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
7199
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
7200
|
+
if (contact.is_owner)
|
|
7201
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
7202
|
+
const tables = [
|
|
7203
|
+
"contacts",
|
|
7204
|
+
"identities",
|
|
7205
|
+
"attributes",
|
|
7206
|
+
"links",
|
|
7207
|
+
"events",
|
|
7208
|
+
"event_participants",
|
|
7209
|
+
"open_commitments",
|
|
7210
|
+
"contact_merges",
|
|
7211
|
+
"dismissed_duplicates",
|
|
7212
|
+
"link_event_effects",
|
|
7213
|
+
"upcoming_occurrences",
|
|
7214
|
+
"link_rollups",
|
|
7215
|
+
"contact_rollups"
|
|
7216
|
+
];
|
|
7217
|
+
const before = new Map(
|
|
7218
|
+
tables.map((table) => [
|
|
7219
|
+
table,
|
|
7220
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
7221
|
+
])
|
|
7222
|
+
);
|
|
7223
|
+
db.prepare(
|
|
7224
|
+
"DELETE FROM events WHERE id IN (SELECT p.event_id FROM event_participants p WHERE p.contact_id = ? AND NOT EXISTS (SELECT 1 FROM event_participants q JOIN contacts c ON c.id = q.contact_id WHERE q.event_id = p.event_id AND q.contact_id != ? AND c.is_owner = 0))"
|
|
7225
|
+
).run(locatorId, locatorId);
|
|
7226
|
+
db.prepare(
|
|
7227
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
7228
|
+
).run(locatorId, locatorId);
|
|
7229
|
+
db.prepare(
|
|
7230
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7231
|
+
).run(locatorId, locatorId);
|
|
7232
|
+
db.prepare(
|
|
7233
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
7234
|
+
).run(locatorId);
|
|
7235
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
7236
|
+
const now = Date.now();
|
|
7237
|
+
const affectedLinks = db.prepare(
|
|
7238
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
7239
|
+
).all().map((row) => row.id);
|
|
7240
|
+
refreshAllBridgeScores(db, now);
|
|
7241
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
7242
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
7243
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
7244
|
+
const removed = tables.map((table) => {
|
|
7245
|
+
const remaining = new Set(
|
|
7246
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
7247
|
+
);
|
|
7248
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
7249
|
+
return { entity: table, count: ids.length, ids };
|
|
7250
|
+
}).filter((item) => item.count > 0);
|
|
7251
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
7252
|
+
});
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
// src/identities/queries.ts
|
|
7256
|
+
function getIdentityRowById(db, id) {
|
|
7257
|
+
const row = db.prepare(
|
|
7258
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
7259
|
+
created_at, updated_at, removed_at
|
|
7260
|
+
FROM identities
|
|
7261
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
7262
|
+
).get(id);
|
|
7263
|
+
if (row === void 0) {
|
|
7264
|
+
return null;
|
|
7265
|
+
}
|
|
7266
|
+
return row;
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
// src/identities/receipts.ts
|
|
7270
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
7271
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
7272
|
+
return {
|
|
7273
|
+
primary,
|
|
7274
|
+
created: normalized.created,
|
|
7275
|
+
updated: normalized.updated,
|
|
7276
|
+
archived: normalized.archived,
|
|
7277
|
+
removed: normalized.removed,
|
|
7278
|
+
affectedLinks: normalized.affectedLinks,
|
|
7279
|
+
derivedEffects: normalized.derivedEffects
|
|
7280
|
+
};
|
|
7281
|
+
}
|
|
7282
|
+
|
|
7283
|
+
// src/identities/validators.ts
|
|
7284
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
7285
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
7286
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7287
|
+
).get(normalizedKey) : db.prepare(
|
|
7288
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
7289
|
+
).get(normalizedKey, excludeIdentityId);
|
|
7290
|
+
if (row !== void 0) {
|
|
7291
|
+
const owner = db.prepare(
|
|
7292
|
+
"SELECT c.id, c.name FROM contacts c JOIN identities i ON i.contact_id = c.id WHERE i.normalized_key = ? AND i.removed_at IS NULL"
|
|
7293
|
+
).get(normalizedKey);
|
|
7294
|
+
throw new AffinityConflictError(
|
|
7295
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
7296
|
+
);
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
// src/identities/validate_identity_input.ts
|
|
7301
|
+
var identityTypes = [
|
|
7302
|
+
"email",
|
|
7303
|
+
"phone",
|
|
7304
|
+
"url",
|
|
7305
|
+
"website",
|
|
7306
|
+
"handle",
|
|
7307
|
+
"x",
|
|
7308
|
+
"twitter",
|
|
7309
|
+
"linkedin",
|
|
7310
|
+
"github",
|
|
7311
|
+
"mastodon",
|
|
7312
|
+
"bluesky",
|
|
7313
|
+
"telegram",
|
|
7314
|
+
"whatsapp",
|
|
7315
|
+
"signal",
|
|
7316
|
+
"matrix",
|
|
7317
|
+
"discord",
|
|
7318
|
+
"slack",
|
|
7319
|
+
"instagram",
|
|
7320
|
+
"facebook",
|
|
7321
|
+
"youtube",
|
|
7322
|
+
"tiktok",
|
|
7323
|
+
"address",
|
|
7324
|
+
"other",
|
|
7325
|
+
"account_id",
|
|
7326
|
+
"alias"
|
|
7327
|
+
];
|
|
7328
|
+
function validateIdentityInput(type, value) {
|
|
7329
|
+
const kind = type.toLowerCase();
|
|
7330
|
+
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
7331
|
+
throw new AffinityValidationError(
|
|
7332
|
+
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
7333
|
+
);
|
|
7334
|
+
if (!value.trim())
|
|
7335
|
+
throw new AffinityValidationError("identity value must be non-empty");
|
|
7336
|
+
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
7337
|
+
throw new AffinityValidationError(
|
|
7338
|
+
"email value must match local@domain.tld"
|
|
7339
|
+
);
|
|
7340
|
+
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
7341
|
+
throw new AffinityValidationError(
|
|
7342
|
+
"phone value must contain at least 5 digits"
|
|
7343
|
+
);
|
|
7344
|
+
if (kind === "url" || kind === "website") {
|
|
7345
|
+
try {
|
|
7346
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
7347
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
7348
|
+
throw new Error();
|
|
7349
|
+
} catch {
|
|
7350
|
+
throw new AffinityValidationError(
|
|
7351
|
+
"url/website value must be an http/https URL or bare domain"
|
|
7352
|
+
);
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
|
|
7357
|
+
// src/identities/add_identity.ts
|
|
7358
|
+
function addIdentity(db, contactId, input) {
|
|
7359
|
+
return withTransaction(db, () => {
|
|
7360
|
+
const contact = getContactRowById(db, contactId);
|
|
7361
|
+
if (!contact) {
|
|
7362
|
+
throw new AffinityNotFoundError("contact not found");
|
|
7363
|
+
}
|
|
7364
|
+
if (contact.lifecycle_state === "merged") {
|
|
7365
|
+
throw new AffinityStateError("merged contact is read-only");
|
|
7366
|
+
}
|
|
7367
|
+
const type = input.type.trim();
|
|
7368
|
+
const value = input.value.trim();
|
|
7369
|
+
if (type.length === 0 || value.length === 0) {
|
|
7370
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
7371
|
+
}
|
|
7372
|
+
validateIdentityInput(type, value);
|
|
7373
|
+
const nk = normalizeIdentityKey(type, value);
|
|
7374
|
+
assertNoIdentityCollision(db, nk);
|
|
7375
|
+
const now = resolveNow(input.now);
|
|
7376
|
+
const verified = input.verified ? 1 : 0;
|
|
7377
|
+
const verifiedAt = verified === 1 ? now : null;
|
|
7378
|
+
const result = db.prepare(
|
|
7379
|
+
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
7380
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7381
|
+
).run(
|
|
7382
|
+
contactId,
|
|
7383
|
+
type,
|
|
7384
|
+
value,
|
|
7385
|
+
input.label ?? null,
|
|
7386
|
+
nk,
|
|
7387
|
+
verified,
|
|
7388
|
+
verifiedAt,
|
|
7389
|
+
now,
|
|
7390
|
+
now
|
|
7391
|
+
);
|
|
7392
|
+
const id = Number(result.lastInsertRowid);
|
|
7393
|
+
const row = getIdentityRowById(db, id);
|
|
7394
|
+
if (!row) {
|
|
7395
|
+
throw new AffinityInvariantError("inserted identity not found");
|
|
7396
|
+
}
|
|
7397
|
+
refreshContactRollup(db, contactId, now);
|
|
7398
|
+
db.prepare(
|
|
7399
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7400
|
+
).run(contactId, contactId);
|
|
7401
|
+
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
7402
|
+
created: [{ kind: "identity", id }]
|
|
7403
|
+
});
|
|
7404
|
+
});
|
|
7405
|
+
}
|
|
7406
|
+
|
|
7407
|
+
// src/tools/create_contact_tool_result.ts
|
|
7408
|
+
function createContactToolResult(db, input) {
|
|
7409
|
+
return withTransaction(db, () => {
|
|
7410
|
+
const receipt = createContact(db, input);
|
|
7411
|
+
const identityOwners = /* @__PURE__ */ new Set();
|
|
7412
|
+
for (const identity of input.identities ?? []) {
|
|
7413
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
7414
|
+
const existing = db.prepare(
|
|
7415
|
+
"SELECT contact_id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7416
|
+
).get(normalizeIdentityKey(identity.type, identity.value));
|
|
7417
|
+
if (existing) {
|
|
7418
|
+
if (Number(existing.contact_id) !== receipt.primary.id)
|
|
7419
|
+
identityOwners.add(Number(existing.contact_id));
|
|
7420
|
+
continue;
|
|
7421
|
+
}
|
|
7422
|
+
const added = addIdentity(db, receipt.primary.id, identity);
|
|
7423
|
+
receipt.created.push(...added.created);
|
|
7424
|
+
}
|
|
7425
|
+
const result = mutationToolResult("create", receipt, "contact");
|
|
7426
|
+
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
7427
|
+
const warnings = db.prepare(
|
|
7428
|
+
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
7429
|
+
).all(receipt.primary.id).flatMap((row) => {
|
|
7430
|
+
const score = identityOwners.has(row.id) ? 1 : fold(row.name) === fold(input.name) ? 0.92 : void 0;
|
|
7431
|
+
return score === void 0 ? [] : [
|
|
7432
|
+
{
|
|
7433
|
+
code: "possible_duplicate",
|
|
7434
|
+
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
7435
|
+
contactId: row.id,
|
|
7436
|
+
score
|
|
7437
|
+
}
|
|
7438
|
+
];
|
|
7439
|
+
});
|
|
7440
|
+
if (warnings.length) result.warnings = warnings;
|
|
7441
|
+
return result;
|
|
7442
|
+
});
|
|
7443
|
+
}
|
|
6777
7444
|
|
|
6778
7445
|
// src/tools/manage_contact_tool.ts
|
|
6779
7446
|
function contactLocatorSchema3(description) {
|
|
@@ -6793,24 +7460,76 @@ function contactLocatorSchema3(description) {
|
|
|
6793
7460
|
description
|
|
6794
7461
|
};
|
|
6795
7462
|
}
|
|
6796
|
-
function manageContactToolHandler(db,
|
|
7463
|
+
function manageContactToolHandler(db, rawInput) {
|
|
7464
|
+
const input = structuredClone(rawInput);
|
|
7465
|
+
const inputError = validateToolInput(manageContactTool.inputSchema, input);
|
|
7466
|
+
if (inputError)
|
|
7467
|
+
return toolFailure(
|
|
7468
|
+
"protocol",
|
|
7469
|
+
"invalid_input",
|
|
7470
|
+
"Invalid input.",
|
|
7471
|
+
inputError
|
|
7472
|
+
);
|
|
6797
7473
|
return withToolHandling(() => {
|
|
6798
|
-
if (input.action === "
|
|
6799
|
-
|
|
6800
|
-
"
|
|
6801
|
-
|
|
6802
|
-
|
|
7474
|
+
if (input.action === "export") {
|
|
7475
|
+
if (!input.contact)
|
|
7476
|
+
return toolSuccess("Exported Affinity.", {
|
|
7477
|
+
action: "export",
|
|
7478
|
+
export: exportAffinity(db)
|
|
7479
|
+
});
|
|
7480
|
+
const target = resolveContactLocator(db, input.contact, "contact", true);
|
|
7481
|
+
if (!target.ok) return target.result;
|
|
7482
|
+
return toolSuccess("Exported contact.", {
|
|
7483
|
+
action: "export",
|
|
7484
|
+
export: exportContact(db, target.value.id)
|
|
7485
|
+
});
|
|
7486
|
+
}
|
|
7487
|
+
if (input.action === "delete" && input.confirm !== true)
|
|
7488
|
+
return toolFailure(
|
|
7489
|
+
"protocol",
|
|
7490
|
+
"invalid_input",
|
|
7491
|
+
"Delete requires confirmation.",
|
|
7492
|
+
"delete is permanent; pass confirm: true (CLI: --confirm)"
|
|
6803
7493
|
);
|
|
7494
|
+
if (input.action === "create") {
|
|
7495
|
+
if (!input.input)
|
|
7496
|
+
return toolFailure(
|
|
7497
|
+
"protocol",
|
|
7498
|
+
"invalid_input",
|
|
7499
|
+
"Missing input.",
|
|
7500
|
+
"input is required for create."
|
|
7501
|
+
);
|
|
7502
|
+
return createContactToolResult(db, input.input);
|
|
6804
7503
|
}
|
|
6805
|
-
const contact = resolveContactLocator(
|
|
7504
|
+
const contact = resolveContactLocator(
|
|
7505
|
+
db,
|
|
7506
|
+
input.contact,
|
|
7507
|
+
"contact",
|
|
7508
|
+
input.action === "delete"
|
|
7509
|
+
);
|
|
6806
7510
|
if (!contact.ok) {
|
|
6807
7511
|
return contact.result;
|
|
6808
7512
|
}
|
|
7513
|
+
if (input.action === "delete") {
|
|
7514
|
+
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
7515
|
+
return toolSuccess("Permanently deleted contact.", {
|
|
7516
|
+
action: "delete",
|
|
7517
|
+
deletion,
|
|
7518
|
+
changes: deletion.removed.flatMap(
|
|
7519
|
+
(item) => item.ids.map((id) => ({
|
|
7520
|
+
entity: item.entity,
|
|
7521
|
+
id,
|
|
7522
|
+
action: "removed"
|
|
7523
|
+
}))
|
|
7524
|
+
)
|
|
7525
|
+
});
|
|
7526
|
+
}
|
|
6809
7527
|
if (input.action === "revise") {
|
|
6810
7528
|
return mutationToolResult(
|
|
6811
7529
|
"revise",
|
|
6812
7530
|
reviseContact(db, contact.value.id, input.patch),
|
|
6813
|
-
"contact"
|
|
7531
|
+
"contact",
|
|
7532
|
+
Object.keys(input.patch)
|
|
6814
7533
|
);
|
|
6815
7534
|
}
|
|
6816
7535
|
return mutationToolResult(
|
|
@@ -6827,7 +7546,7 @@ function manageContactToolHandler(db, input) {
|
|
|
6827
7546
|
}
|
|
6828
7547
|
var manageContactTool = defineAffinityTool({
|
|
6829
7548
|
name: manageContactToolName,
|
|
6830
|
-
description: "Create a contact
|
|
7549
|
+
description: "Create, revise, export or permanently delete a contact (confirm: true required; owner cannot be deleted). Export without contact exports the whole database. Lifecycle lost is soft delete. ALWAYS search_affinity first before creating \u2014 duplicates corrupt the graph.",
|
|
6831
7550
|
whenToUse: "Use this for contact creation and lifecycle-safe contact maintenance.",
|
|
6832
7551
|
whenNotToUse: "Do not use this for identities, links, attributes, or merges.",
|
|
6833
7552
|
sideEffects: "writes_state",
|
|
@@ -6848,10 +7567,25 @@ var manageContactTool = defineAffinityTool({
|
|
|
6848
7567
|
action: enumSchema("Operation to perform.", [
|
|
6849
7568
|
"create",
|
|
6850
7569
|
"revise",
|
|
6851
|
-
"set_lifecycle"
|
|
7570
|
+
"set_lifecycle",
|
|
7571
|
+
"export",
|
|
7572
|
+
"delete"
|
|
6852
7573
|
]),
|
|
6853
7574
|
input: objectSchema(
|
|
6854
7575
|
{
|
|
7576
|
+
identities: arraySchema(
|
|
7577
|
+
objectSchema(
|
|
7578
|
+
{
|
|
7579
|
+
type: stringSchema("Identity type."),
|
|
7580
|
+
value: stringSchema("Identity value."),
|
|
7581
|
+
label: stringSchema("Optional label."),
|
|
7582
|
+
verified: booleanSchema("Verified identity."),
|
|
7583
|
+
now: integerSchema("Optional timestamp.")
|
|
7584
|
+
},
|
|
7585
|
+
["type", "value"]
|
|
7586
|
+
),
|
|
7587
|
+
"Optional identities to attach. Identities already owned by another contact are left with that owner and reported as possible duplicates."
|
|
7588
|
+
),
|
|
6855
7589
|
name: stringSchema("Contact name."),
|
|
6856
7590
|
kind: enumSchema("Contact kind.", [
|
|
6857
7591
|
"human",
|
|
@@ -6870,6 +7604,7 @@ var manageContactTool = defineAffinityTool({
|
|
|
6870
7604
|
["name", "kind"],
|
|
6871
7605
|
"New contact payload. Required when action=create."
|
|
6872
7606
|
),
|
|
7607
|
+
confirm: booleanSchema("Required true for permanent deletion."),
|
|
6873
7608
|
contact: contactLocatorSchema3(
|
|
6874
7609
|
"Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity."
|
|
6875
7610
|
),
|
|
@@ -6916,7 +7651,19 @@ function contactLocatorSchema4(description) {
|
|
|
6916
7651
|
description
|
|
6917
7652
|
};
|
|
6918
7653
|
}
|
|
6919
|
-
function manageDuplicateDismissalToolHandler(db,
|
|
7654
|
+
function manageDuplicateDismissalToolHandler(db, rawInput) {
|
|
7655
|
+
const input = structuredClone(rawInput);
|
|
7656
|
+
const inputError = validateToolInput(
|
|
7657
|
+
manageDuplicateDismissalTool.inputSchema,
|
|
7658
|
+
input
|
|
7659
|
+
);
|
|
7660
|
+
if (inputError)
|
|
7661
|
+
return toolFailure(
|
|
7662
|
+
"protocol",
|
|
7663
|
+
"invalid_input",
|
|
7664
|
+
"Invalid input.",
|
|
7665
|
+
inputError
|
|
7666
|
+
);
|
|
6920
7667
|
return withToolHandling(() => {
|
|
6921
7668
|
const left = resolveContactLocator(db, input.left, "left");
|
|
6922
7669
|
if (!left.ok) {
|
|
@@ -6926,6 +7673,11 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
6926
7673
|
if (!right.ok) {
|
|
6927
7674
|
return right.result;
|
|
6928
7675
|
}
|
|
7676
|
+
const leftContactId = Math.min(left.value.id, right.value.id);
|
|
7677
|
+
const rightContactId = Math.max(left.value.id, right.value.id);
|
|
7678
|
+
const existed = db.prepare(
|
|
7679
|
+
"SELECT 1 FROM dismissed_duplicates WHERE left_id = ? AND right_id = ?"
|
|
7680
|
+
).get(leftContactId, rightContactId) !== void 0;
|
|
6929
7681
|
if (input.action === "dismiss") {
|
|
6930
7682
|
const receipt2 = dismissDuplicate(
|
|
6931
7683
|
db,
|
|
@@ -6935,19 +7687,41 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
6935
7687
|
);
|
|
6936
7688
|
return toolSuccess(
|
|
6937
7689
|
`Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
|
|
6938
|
-
{
|
|
7690
|
+
{
|
|
7691
|
+
...receipt2,
|
|
7692
|
+
action: "dismiss",
|
|
7693
|
+
changes: [
|
|
7694
|
+
{
|
|
7695
|
+
entity: "duplicate_dismissal",
|
|
7696
|
+
leftContactId,
|
|
7697
|
+
rightContactId,
|
|
7698
|
+
action: existed ? "updated" : "created"
|
|
7699
|
+
}
|
|
7700
|
+
]
|
|
7701
|
+
}
|
|
6939
7702
|
);
|
|
6940
7703
|
}
|
|
6941
7704
|
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
6942
7705
|
return toolSuccess(
|
|
6943
7706
|
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
6944
|
-
{
|
|
7707
|
+
{
|
|
7708
|
+
...receipt,
|
|
7709
|
+
action: "undismiss",
|
|
7710
|
+
changes: existed ? [
|
|
7711
|
+
{
|
|
7712
|
+
entity: "duplicate_dismissal",
|
|
7713
|
+
leftContactId,
|
|
7714
|
+
rightContactId,
|
|
7715
|
+
action: "removed"
|
|
7716
|
+
}
|
|
7717
|
+
] : []
|
|
7718
|
+
}
|
|
6945
7719
|
);
|
|
6946
7720
|
}, "Duplicate dismissal failed.");
|
|
6947
7721
|
}
|
|
6948
7722
|
var manageDuplicateDismissalTool = defineAffinityTool({
|
|
6949
7723
|
name: manageDuplicateDismissalToolName,
|
|
6950
|
-
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in
|
|
7724
|
+
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.",
|
|
6951
7725
|
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
6952
7726
|
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
6953
7727
|
sideEffects: "writes_state",
|
|
@@ -7033,9 +7807,32 @@ function resolveDateAnchorTarget(db, target) {
|
|
|
7033
7807
|
}
|
|
7034
7808
|
return { kind: "ok", target: { kind: "link", linkId: link.value.id } };
|
|
7035
7809
|
}
|
|
7036
|
-
function manageDateAnchorToolHandler(db,
|
|
7810
|
+
function manageDateAnchorToolHandler(db, rawInput) {
|
|
7811
|
+
const input = structuredClone(rawInput);
|
|
7812
|
+
const inputError = validateToolInput(manageDateAnchorTool.inputSchema, input);
|
|
7813
|
+
if (inputError)
|
|
7814
|
+
return toolFailure(
|
|
7815
|
+
"protocol",
|
|
7816
|
+
"invalid_input",
|
|
7817
|
+
"Invalid input.",
|
|
7818
|
+
inputError
|
|
7819
|
+
);
|
|
7820
|
+
if (input.action !== "add" && input.anchorEventId === void 0)
|
|
7821
|
+
return toolFailure(
|
|
7822
|
+
"protocol",
|
|
7823
|
+
"invalid_input",
|
|
7824
|
+
"Missing anchorEventId.",
|
|
7825
|
+
"anchorEventId is required."
|
|
7826
|
+
);
|
|
7037
7827
|
return withToolHandling(() => {
|
|
7038
7828
|
if (input.action === "add") {
|
|
7829
|
+
if (!input.input)
|
|
7830
|
+
return toolFailure(
|
|
7831
|
+
"protocol",
|
|
7832
|
+
"invalid_input",
|
|
7833
|
+
"Missing input.",
|
|
7834
|
+
"input is required for add."
|
|
7835
|
+
);
|
|
7039
7836
|
const resolved = resolveDateAnchorTarget(db, input.input.target);
|
|
7040
7837
|
if (resolved.kind === "error") {
|
|
7041
7838
|
return resolved.result;
|
|
@@ -7050,7 +7847,8 @@ function manageDateAnchorToolHandler(db, input) {
|
|
|
7050
7847
|
return mutationToolResult(
|
|
7051
7848
|
"revise",
|
|
7052
7849
|
reviseDateAnchor(db, input.anchorEventId, input.patch, input.options),
|
|
7053
|
-
"date anchor"
|
|
7850
|
+
"date anchor",
|
|
7851
|
+
Object.keys(input.patch)
|
|
7054
7852
|
);
|
|
7055
7853
|
}
|
|
7056
7854
|
return mutationToolResult(
|
|
@@ -7158,97 +7956,6 @@ var manageDateAnchorTool = defineAffinityTool({
|
|
|
7158
7956
|
handler: manageDateAnchorToolHandler
|
|
7159
7957
|
});
|
|
7160
7958
|
|
|
7161
|
-
// src/identities/queries.ts
|
|
7162
|
-
function getIdentityRowById(db, id) {
|
|
7163
|
-
const row = db.prepare(
|
|
7164
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
7165
|
-
created_at, updated_at, removed_at
|
|
7166
|
-
FROM identities
|
|
7167
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
7168
|
-
).get(id);
|
|
7169
|
-
if (row === void 0) {
|
|
7170
|
-
return null;
|
|
7171
|
-
}
|
|
7172
|
-
return row;
|
|
7173
|
-
}
|
|
7174
|
-
|
|
7175
|
-
// src/identities/receipts.ts
|
|
7176
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
7177
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
7178
|
-
return {
|
|
7179
|
-
primary,
|
|
7180
|
-
created: normalized.created,
|
|
7181
|
-
updated: normalized.updated,
|
|
7182
|
-
archived: normalized.archived,
|
|
7183
|
-
removed: normalized.removed,
|
|
7184
|
-
affectedLinks: normalized.affectedLinks,
|
|
7185
|
-
derivedEffects: normalized.derivedEffects
|
|
7186
|
-
};
|
|
7187
|
-
}
|
|
7188
|
-
|
|
7189
|
-
// src/identities/validators.ts
|
|
7190
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
7191
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
7192
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
7193
|
-
).get(normalizedKey) : db.prepare(
|
|
7194
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
7195
|
-
).get(normalizedKey, excludeIdentityId);
|
|
7196
|
-
if (row !== void 0) {
|
|
7197
|
-
throw new AffinityConflictError(
|
|
7198
|
-
"another contact already owns this normalized identity"
|
|
7199
|
-
);
|
|
7200
|
-
}
|
|
7201
|
-
}
|
|
7202
|
-
|
|
7203
|
-
// src/identities/add_identity.ts
|
|
7204
|
-
function addIdentity(db, contactId, input) {
|
|
7205
|
-
return withTransaction(db, () => {
|
|
7206
|
-
const contact = getContactRowById(db, contactId);
|
|
7207
|
-
if (!contact) {
|
|
7208
|
-
throw new AffinityNotFoundError("contact not found");
|
|
7209
|
-
}
|
|
7210
|
-
if (contact.lifecycle_state === "merged") {
|
|
7211
|
-
throw new AffinityStateError("merged contact is read-only");
|
|
7212
|
-
}
|
|
7213
|
-
const type = input.type.trim();
|
|
7214
|
-
const value = input.value.trim();
|
|
7215
|
-
if (type.length === 0 || value.length === 0) {
|
|
7216
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
7217
|
-
}
|
|
7218
|
-
const nk = normalizeIdentityKey(type, value);
|
|
7219
|
-
assertNoIdentityCollision(db, nk);
|
|
7220
|
-
const now = resolveNow(input.now);
|
|
7221
|
-
const verified = input.verified ? 1 : 0;
|
|
7222
|
-
const verifiedAt = verified === 1 ? now : null;
|
|
7223
|
-
const result = db.prepare(
|
|
7224
|
-
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
7225
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
7226
|
-
).run(
|
|
7227
|
-
contactId,
|
|
7228
|
-
type,
|
|
7229
|
-
value,
|
|
7230
|
-
input.label ?? null,
|
|
7231
|
-
nk,
|
|
7232
|
-
verified,
|
|
7233
|
-
verifiedAt,
|
|
7234
|
-
now,
|
|
7235
|
-
now
|
|
7236
|
-
);
|
|
7237
|
-
const id = Number(result.lastInsertRowid);
|
|
7238
|
-
const row = getIdentityRowById(db, id);
|
|
7239
|
-
if (!row) {
|
|
7240
|
-
throw new AffinityInvariantError("inserted identity not found");
|
|
7241
|
-
}
|
|
7242
|
-
refreshContactRollup(db, contactId, now);
|
|
7243
|
-
db.prepare(
|
|
7244
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
7245
|
-
).run(contactId, contactId);
|
|
7246
|
-
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
7247
|
-
created: [{ kind: "identity", id }]
|
|
7248
|
-
});
|
|
7249
|
-
});
|
|
7250
|
-
}
|
|
7251
|
-
|
|
7252
7959
|
// src/identities/remove_identity.ts
|
|
7253
7960
|
function removeIdentity(db, identityId, removedAt) {
|
|
7254
7961
|
return withTransaction(db, () => {
|
|
@@ -7300,6 +8007,7 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
7300
8007
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
7301
8008
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
7302
8009
|
}
|
|
8010
|
+
validateIdentityInput(nextType, nextValue);
|
|
7303
8011
|
const nextLabel = patch.label !== void 0 ? patch.label : row.label;
|
|
7304
8012
|
const nkNext = normalizeIdentityKey(nextType, nextValue);
|
|
7305
8013
|
if (nkNext !== row.normalized_key) {
|
|
@@ -7354,7 +8062,9 @@ function contactLocatorSchema6(description) {
|
|
|
7354
8062
|
contactId: integerSchema("Exact contact id."),
|
|
7355
8063
|
identity: objectSchema(
|
|
7356
8064
|
{
|
|
7357
|
-
type: stringSchema(
|
|
8065
|
+
type: stringSchema(
|
|
8066
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8067
|
+
),
|
|
7358
8068
|
value: stringSchema("Identity value.")
|
|
7359
8069
|
},
|
|
7360
8070
|
["type", "value"],
|
|
@@ -7364,7 +8074,23 @@ function contactLocatorSchema6(description) {
|
|
|
7364
8074
|
description
|
|
7365
8075
|
};
|
|
7366
8076
|
}
|
|
7367
|
-
function manageIdentityToolHandler(db,
|
|
8077
|
+
function manageIdentityToolHandler(db, rawInput) {
|
|
8078
|
+
const input = structuredClone(rawInput);
|
|
8079
|
+
const inputError = validateToolInput(manageIdentityTool.inputSchema, input);
|
|
8080
|
+
if (inputError)
|
|
8081
|
+
return toolFailure(
|
|
8082
|
+
"protocol",
|
|
8083
|
+
"invalid_input",
|
|
8084
|
+
"Invalid input.",
|
|
8085
|
+
inputError
|
|
8086
|
+
);
|
|
8087
|
+
if (input.action !== "add" && input.identityId === void 0)
|
|
8088
|
+
return toolFailure(
|
|
8089
|
+
"protocol",
|
|
8090
|
+
"invalid_input",
|
|
8091
|
+
"Missing identityId.",
|
|
8092
|
+
"identityId is required."
|
|
8093
|
+
);
|
|
7368
8094
|
return withToolHandling(() => {
|
|
7369
8095
|
switch (input.action) {
|
|
7370
8096
|
case "add": {
|
|
@@ -7382,7 +8108,8 @@ function manageIdentityToolHandler(db, input) {
|
|
|
7382
8108
|
return mutationToolResult(
|
|
7383
8109
|
"revise",
|
|
7384
8110
|
reviseIdentity(db, input.identityId, input.patch),
|
|
7385
|
-
"identity"
|
|
8111
|
+
"identity",
|
|
8112
|
+
Object.keys(input.patch)
|
|
7386
8113
|
);
|
|
7387
8114
|
case "verify":
|
|
7388
8115
|
return mutationToolResult(
|
|
@@ -7431,7 +8158,9 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
7431
8158
|
),
|
|
7432
8159
|
input: objectSchema(
|
|
7433
8160
|
{
|
|
7434
|
-
type: stringSchema(
|
|
8161
|
+
type: stringSchema(
|
|
8162
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8163
|
+
),
|
|
7435
8164
|
value: stringSchema("Identity value."),
|
|
7436
8165
|
label: stringSchema("Optional label."),
|
|
7437
8166
|
verified: booleanSchema("Whether the identity starts verified."),
|
|
@@ -7445,7 +8174,9 @@ var manageIdentityTool = defineAffinityTool({
|
|
|
7445
8174
|
),
|
|
7446
8175
|
patch: objectSchema(
|
|
7447
8176
|
{
|
|
7448
|
-
type: stringSchema(
|
|
8177
|
+
type: stringSchema(
|
|
8178
|
+
`Optional identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
8179
|
+
),
|
|
7449
8180
|
value: stringSchema("Optional identity value."),
|
|
7450
8181
|
label: stringSchema("Optional label.")
|
|
7451
8182
|
},
|
|
@@ -7503,7 +8234,19 @@ function linkLocatorSchema4(description) {
|
|
|
7503
8234
|
description
|
|
7504
8235
|
};
|
|
7505
8236
|
}
|
|
7506
|
-
function manageRelationshipToolHandler(db,
|
|
8237
|
+
function manageRelationshipToolHandler(db, rawInput) {
|
|
8238
|
+
const input = structuredClone(rawInput);
|
|
8239
|
+
const inputError = validateToolInput(
|
|
8240
|
+
manageRelationshipTool.inputSchema,
|
|
8241
|
+
input
|
|
8242
|
+
);
|
|
8243
|
+
if (inputError)
|
|
8244
|
+
return toolFailure(
|
|
8245
|
+
"protocol",
|
|
8246
|
+
"invalid_input",
|
|
8247
|
+
"Invalid input.",
|
|
8248
|
+
inputError
|
|
8249
|
+
);
|
|
7507
8250
|
return withToolHandling(() => {
|
|
7508
8251
|
switch (input.action) {
|
|
7509
8252
|
case "seed_social_link": {
|
|
@@ -7684,7 +8427,16 @@ function contactLocatorSchema8(description) {
|
|
|
7684
8427
|
description
|
|
7685
8428
|
};
|
|
7686
8429
|
}
|
|
7687
|
-
function mergeContactsToolHandler(db,
|
|
8430
|
+
function mergeContactsToolHandler(db, rawInput) {
|
|
8431
|
+
const input = structuredClone(rawInput);
|
|
8432
|
+
const inputError = validateToolInput(mergeContactsTool.inputSchema, input);
|
|
8433
|
+
if (inputError)
|
|
8434
|
+
return toolFailure(
|
|
8435
|
+
"protocol",
|
|
8436
|
+
"invalid_input",
|
|
8437
|
+
"Invalid input.",
|
|
8438
|
+
inputError
|
|
8439
|
+
);
|
|
7688
8440
|
return withToolHandling(() => {
|
|
7689
8441
|
const winner = resolveContactLocator(db, input.winner, "winner");
|
|
7690
8442
|
if (!winner.ok) {
|
|
@@ -7765,7 +8517,16 @@ function prepareParticipants2(db, raw, injectOwner) {
|
|
|
7765
8517
|
const normalized = raw.map(normalizeParticipant2);
|
|
7766
8518
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
7767
8519
|
}
|
|
7768
|
-
function recordEventToolHandler(db,
|
|
8520
|
+
function recordEventToolHandler(db, rawInput) {
|
|
8521
|
+
const input = structuredClone(rawInput);
|
|
8522
|
+
const inputError = validateToolInput(recordEventTool.inputSchema, input);
|
|
8523
|
+
if (inputError)
|
|
8524
|
+
return toolFailure(
|
|
8525
|
+
"protocol",
|
|
8526
|
+
"invalid_input",
|
|
8527
|
+
"Invalid input.",
|
|
8528
|
+
inputError
|
|
8529
|
+
);
|
|
7769
8530
|
return withToolHandling(() => {
|
|
7770
8531
|
const patched = {
|
|
7771
8532
|
...input.input,
|
|
@@ -7863,7 +8624,7 @@ var recordEventTool = defineAffinityTool({
|
|
|
7863
8624
|
"observed"
|
|
7864
8625
|
])
|
|
7865
8626
|
},
|
|
7866
|
-
["
|
|
8627
|
+
["role"]
|
|
7867
8628
|
),
|
|
7868
8629
|
"Participant list. Must be non-empty and include the owner contact."
|
|
7869
8630
|
),
|
|
@@ -7879,6 +8640,56 @@ var recordEventTool = defineAffinityTool({
|
|
|
7879
8640
|
handler: recordEventToolHandler
|
|
7880
8641
|
});
|
|
7881
8642
|
|
|
8643
|
+
// src/tools/enrich_review_item.ts
|
|
8644
|
+
function enrichReviewItem(db, item) {
|
|
8645
|
+
const name = (id) => {
|
|
8646
|
+
if (typeof id !== "number") return "";
|
|
8647
|
+
return db.prepare("SELECT name FROM contacts WHERE id = ?").get(id)?.name ?? `Contact #${id}`;
|
|
8648
|
+
};
|
|
8649
|
+
const result = { ...item };
|
|
8650
|
+
if (item.kind === "link") {
|
|
8651
|
+
const link = db.prepare(`SELECT l.*, a.name AS from_name, b.name AS to_name, a.is_owner AS from_owner, b.is_owner AS to_owner,
|
|
8652
|
+
lr.recency_score, lr.drift_priority, lr.readiness_score, lr.normalized_rank, lr.reciprocity_score, lr.radar_score FROM links l JOIN contacts a ON a.id = l.from_contact_id JOIN contacts b ON b.id = l.to_contact_id LEFT JOIN link_rollups lr ON lr.link_id = l.id WHERE l.id = ?`).get(item.id);
|
|
8653
|
+
if (link) {
|
|
8654
|
+
result.title = link.from_owner ? String(link.to_name) : link.to_owner ? String(link.from_name) : `${link.from_name} \xB7 ${link.to_name}`;
|
|
8655
|
+
result.subtitle = [link.kind, link.role].filter(Boolean).join(" \xB7 ");
|
|
8656
|
+
for (const key of ["affinity", "trust", "rank"])
|
|
8657
|
+
if (typeof link[key] === "number") result[key] = link[key];
|
|
8658
|
+
result.recencyScore ??= link.recency_score;
|
|
8659
|
+
result.driftPriority ??= link.drift_priority;
|
|
8660
|
+
result.readinessScore = link.readiness_score;
|
|
8661
|
+
result.normalizedRank ??= link.normalized_rank;
|
|
8662
|
+
result.reciprocityScore = link.reciprocity_score;
|
|
8663
|
+
result.radarScore = link.radar_score;
|
|
8664
|
+
}
|
|
8665
|
+
}
|
|
8666
|
+
if (item.kind === "event" && typeof item.linkId === "number" && !Array.isArray(item.participants)) {
|
|
8667
|
+
const link = db.prepare("SELECT from_contact_id, to_contact_id FROM links WHERE id = ?").get(item.linkId);
|
|
8668
|
+
if (link)
|
|
8669
|
+
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8670
|
+
}
|
|
8671
|
+
if (Array.isArray(item.participants)) {
|
|
8672
|
+
result.counterpartyNames = item.participants.map(
|
|
8673
|
+
(p) => name(p.contactId)
|
|
8674
|
+
);
|
|
8675
|
+
result.subtitle = [
|
|
8676
|
+
item.subtitle,
|
|
8677
|
+
result.counterpartyNames.join(", ")
|
|
8678
|
+
].filter(Boolean).join(" \xB7 ");
|
|
8679
|
+
}
|
|
8680
|
+
if (typeof item.leftContactId === "number")
|
|
8681
|
+
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
8682
|
+
if (typeof item.winnerContactId === "number")
|
|
8683
|
+
result.title = `Merged ${name(item.loserContactId)} into ${name(item.winnerContactId)}`;
|
|
8684
|
+
if (item.occursAt !== void 0 && item.kind === "contact")
|
|
8685
|
+
result.title = `${name(item.id)} \xB7 ${item.title}`;
|
|
8686
|
+
for (const [key, value] of Object.entries(result)) {
|
|
8687
|
+
if (typeof value === "number" && /score|priority|affinity|trust|rank/i.test(key))
|
|
8688
|
+
result[key] = Math.round(value * 1e3) / 1e3;
|
|
8689
|
+
}
|
|
8690
|
+
return result;
|
|
8691
|
+
}
|
|
8692
|
+
|
|
7882
8693
|
// src/tools/tool_summary.ts
|
|
7883
8694
|
function summarizeCount(count, singular) {
|
|
7884
8695
|
if (count === 0) {
|
|
@@ -7893,6 +8704,7 @@ function summarizeCount(count, singular) {
|
|
|
7893
8704
|
// src/tools/review_affinity_tool.ts
|
|
7894
8705
|
function buildReadOptions(input) {
|
|
7895
8706
|
return {
|
|
8707
|
+
...input.now === void 0 ? {} : { now: input.now },
|
|
7896
8708
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7897
8709
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
7898
8710
|
...input.includeArchived === void 0 ? {} : { includeArchived: input.includeArchived },
|
|
@@ -8012,6 +8824,7 @@ function validateReviewInput(input) {
|
|
|
8012
8824
|
}
|
|
8013
8825
|
function duplicateCandidateItem(candidate) {
|
|
8014
8826
|
return {
|
|
8827
|
+
...candidate,
|
|
8015
8828
|
kind: "contact",
|
|
8016
8829
|
id: candidate.leftContactId,
|
|
8017
8830
|
title: `Possible duplicate: #${candidate.leftContactId} and #${candidate.rightContactId}`,
|
|
@@ -8021,24 +8834,28 @@ function duplicateCandidateItem(candidate) {
|
|
|
8021
8834
|
}
|
|
8022
8835
|
function momentItem(moment) {
|
|
8023
8836
|
return {
|
|
8837
|
+
...moment,
|
|
8838
|
+
occurredAtIso: new Date(moment.occurredAt).toISOString(),
|
|
8024
8839
|
kind: "event",
|
|
8025
8840
|
id: moment.eventId,
|
|
8026
8841
|
title: moment.summary,
|
|
8027
8842
|
subtitle: moment.momentKind,
|
|
8028
|
-
snippet:
|
|
8843
|
+
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
8029
8844
|
};
|
|
8030
8845
|
}
|
|
8031
8846
|
function commitmentItem(commitment) {
|
|
8032
8847
|
return {
|
|
8848
|
+
...commitment,
|
|
8033
8849
|
kind: "event",
|
|
8034
8850
|
id: commitment.eventId,
|
|
8035
8851
|
title: commitment.summary,
|
|
8036
8852
|
subtitle: commitment.type,
|
|
8037
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt
|
|
8853
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
8038
8854
|
};
|
|
8039
8855
|
}
|
|
8040
8856
|
function radarItem(radar) {
|
|
8041
8857
|
return {
|
|
8858
|
+
...radar,
|
|
8042
8859
|
kind: "link",
|
|
8043
8860
|
id: radar.linkId,
|
|
8044
8861
|
title: `Link #${radar.linkId}`,
|
|
@@ -8048,29 +8865,34 @@ function radarItem(radar) {
|
|
|
8048
8865
|
}
|
|
8049
8866
|
function upcomingDateItem(date) {
|
|
8050
8867
|
return {
|
|
8868
|
+
...date,
|
|
8869
|
+
occursAt: date.occursOn,
|
|
8870
|
+
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
8051
8871
|
kind: date.targetRef.kind,
|
|
8052
8872
|
id: date.targetRef.id,
|
|
8053
8873
|
title: date.summary,
|
|
8054
8874
|
subtitle: date.recurrenceKind,
|
|
8055
|
-
snippet: `occurs ${date.occursOn}`
|
|
8875
|
+
snippet: `occurs ${new Date(date.occursOn).toISOString().slice(0, 10)}`
|
|
8056
8876
|
};
|
|
8057
8877
|
}
|
|
8058
8878
|
function mergeHistoryItem(record) {
|
|
8059
8879
|
return {
|
|
8880
|
+
...record,
|
|
8060
8881
|
kind: "contact",
|
|
8061
8882
|
id: record.winnerContactId,
|
|
8062
8883
|
title: `Merged #${record.loserContactId} into #${record.winnerContactId}`,
|
|
8063
|
-
subtitle:
|
|
8884
|
+
subtitle: new Date(record.mergedAt).toISOString().slice(0, 10),
|
|
8064
8885
|
snippet: record.reasonSummary ?? void 0
|
|
8065
8886
|
};
|
|
8066
8887
|
}
|
|
8067
8888
|
function dismissedPairItem(record) {
|
|
8068
8889
|
return {
|
|
8890
|
+
...record,
|
|
8069
8891
|
kind: "contact",
|
|
8070
8892
|
id: record.leftContactId,
|
|
8071
8893
|
title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
|
|
8072
8894
|
subtitle: record.reason ?? "no reason given",
|
|
8073
|
-
snippet: `dismissed at ${record.dismissedAt}`
|
|
8895
|
+
snippet: `dismissed at ${new Date(record.dismissedAt).toISOString().slice(0, 10)}`
|
|
8074
8896
|
};
|
|
8075
8897
|
}
|
|
8076
8898
|
function compactFilters(input) {
|
|
@@ -8080,7 +8902,16 @@ function compactFilters(input) {
|
|
|
8080
8902
|
)
|
|
8081
8903
|
);
|
|
8082
8904
|
}
|
|
8083
|
-
function reviewAffinityToolHandler(db,
|
|
8905
|
+
function reviewAffinityToolHandler(db, rawInput) {
|
|
8906
|
+
const input = structuredClone(rawInput);
|
|
8907
|
+
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8908
|
+
if (inputError)
|
|
8909
|
+
return toolFailure(
|
|
8910
|
+
"protocol",
|
|
8911
|
+
"invalid_input",
|
|
8912
|
+
"Invalid input.",
|
|
8913
|
+
inputError
|
|
8914
|
+
);
|
|
8084
8915
|
return withToolHandling(() => {
|
|
8085
8916
|
const invalid = validateReviewInput(input);
|
|
8086
8917
|
if (invalid) {
|
|
@@ -8094,6 +8925,9 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8094
8925
|
items = listContacts(
|
|
8095
8926
|
db,
|
|
8096
8927
|
{
|
|
8928
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
8929
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
8930
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8097
8931
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8098
8932
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8099
8933
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8124,7 +8958,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8124
8958
|
"Provide `contact` when `view` is `events.contact_journal`."
|
|
8125
8959
|
);
|
|
8126
8960
|
}
|
|
8127
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
8961
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
8128
8962
|
if (!contact.ok) {
|
|
8129
8963
|
return contact.result;
|
|
8130
8964
|
}
|
|
@@ -8159,7 +8993,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8159
8993
|
let contactId;
|
|
8160
8994
|
let linkId;
|
|
8161
8995
|
if (input.contact !== void 0) {
|
|
8162
|
-
const contact = resolveContactLocator(
|
|
8996
|
+
const contact = resolveContactLocator(
|
|
8997
|
+
db,
|
|
8998
|
+
input.contact,
|
|
8999
|
+
"contact",
|
|
9000
|
+
true
|
|
9001
|
+
);
|
|
8163
9002
|
if (!contact.ok) {
|
|
8164
9003
|
return contact.result;
|
|
8165
9004
|
}
|
|
@@ -8188,7 +9027,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8188
9027
|
let contactId;
|
|
8189
9028
|
let linkId;
|
|
8190
9029
|
if (input.contact !== void 0) {
|
|
8191
|
-
const contact = resolveContactLocator(
|
|
9030
|
+
const contact = resolveContactLocator(
|
|
9031
|
+
db,
|
|
9032
|
+
input.contact,
|
|
9033
|
+
"contact",
|
|
9034
|
+
true
|
|
9035
|
+
);
|
|
8192
9036
|
if (!contact.ok) {
|
|
8193
9037
|
return contact.result;
|
|
8194
9038
|
}
|
|
@@ -8281,7 +9125,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8281
9125
|
"Provide `contact` when `view` is `merges.history`."
|
|
8282
9126
|
);
|
|
8283
9127
|
}
|
|
8284
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
9128
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
8285
9129
|
if (!contact.ok) {
|
|
8286
9130
|
return contact.result;
|
|
8287
9131
|
}
|
|
@@ -8294,6 +9138,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8294
9138
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
8295
9139
|
break;
|
|
8296
9140
|
}
|
|
9141
|
+
items = items.map((item) => enrichReviewItem(db, item));
|
|
8297
9142
|
return toolSuccess(
|
|
8298
9143
|
summarizeCount(items.length, "item"),
|
|
8299
9144
|
{
|
|
@@ -8326,12 +9171,16 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8326
9171
|
targetKinds: ["contact", "link", "event"],
|
|
8327
9172
|
inputDescriptions: {
|
|
8328
9173
|
view: "Which review surface to return.",
|
|
9174
|
+
now: "Optional current timestamp for overdue calculations.",
|
|
8329
9175
|
contact: "Target contact locator for views that require one.",
|
|
8330
9176
|
link: "Target link locator for views that require one.",
|
|
8331
9177
|
contactIds: "Optional contact id subset for duplicate review or graph review.",
|
|
8332
9178
|
kind: "Optional contact kind or link kind filter, depending on the view.",
|
|
8333
9179
|
lifecycleState: "Optional contact lifecycle filter for the contact list view.",
|
|
8334
9180
|
linkState: "Optional link state filter for link review views.",
|
|
9181
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9182
|
+
tag: "Tag name; arrays require all tags.",
|
|
9183
|
+
includeLost: "Whether lost contacts are included.",
|
|
8335
9184
|
includeOwner: "Whether owner contacts should be included in contact review.",
|
|
8336
9185
|
includeDormant: "Whether dormant links or contacts should be included where supported.",
|
|
8337
9186
|
includeArchived: "Whether archived links should be included where supported.",
|
|
@@ -8368,6 +9217,9 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8368
9217
|
"merges.history",
|
|
8369
9218
|
"merges.dismissed"
|
|
8370
9219
|
]),
|
|
9220
|
+
now: integerSchema(
|
|
9221
|
+
"Optional current timestamp for overdue calculations."
|
|
9222
|
+
),
|
|
8371
9223
|
contact: contactLocatorSchema9("Optional target contact locator."),
|
|
8372
9224
|
link: linkLocatorSchema5("Optional target link locator."),
|
|
8373
9225
|
contactIds: arraySchema(
|
|
@@ -8390,6 +9242,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8390
9242
|
"broken",
|
|
8391
9243
|
"archived"
|
|
8392
9244
|
]),
|
|
9245
|
+
attribute: oneOfSchema([
|
|
9246
|
+
stringSchema("Attribute filter."),
|
|
9247
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9248
|
+
]),
|
|
9249
|
+
tag: oneOfSchema([
|
|
9250
|
+
stringSchema("Tag filter."),
|
|
9251
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9252
|
+
]),
|
|
9253
|
+
includeLost: booleanSchema(
|
|
9254
|
+
"Include lost contacts (search excludes them by default)."
|
|
9255
|
+
),
|
|
8393
9256
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8394
9257
|
includeDormant: booleanSchema("Whether dormant rows are included."),
|
|
8395
9258
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
@@ -8434,7 +9297,16 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
8434
9297
|
});
|
|
8435
9298
|
|
|
8436
9299
|
// src/tools/search_affinity_tool.ts
|
|
8437
|
-
function searchAffinityToolHandler(db,
|
|
9300
|
+
function searchAffinityToolHandler(db, rawInput) {
|
|
9301
|
+
const input = structuredClone(rawInput);
|
|
9302
|
+
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9303
|
+
if (inputError)
|
|
9304
|
+
return toolFailure(
|
|
9305
|
+
"protocol",
|
|
9306
|
+
"invalid_input",
|
|
9307
|
+
"Invalid input.",
|
|
9308
|
+
inputError
|
|
9309
|
+
);
|
|
8438
9310
|
return withToolHandling(() => {
|
|
8439
9311
|
const options = {
|
|
8440
9312
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
@@ -8444,6 +9316,9 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8444
9316
|
db,
|
|
8445
9317
|
input.query,
|
|
8446
9318
|
{
|
|
9319
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
9320
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
9321
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8447
9322
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8448
9323
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8449
9324
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8467,7 +9342,7 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8467
9342
|
}
|
|
8468
9343
|
var searchAffinityTool = defineAffinityTool({
|
|
8469
9344
|
name: searchAffinityToolName,
|
|
8470
|
-
description: "Search contacts by name
|
|
9345
|
+
description: "Search contacts by name, identity, attributes, link roles, bonds and event text. All whitespace tokens must match. Dormant and lost contacts are excluded by default; use includeDormant and includeLost to include them.",
|
|
8471
9346
|
whenToUse: "Use this when you want to find contacts from free text or a known routing identity such as an email or URL-like handle.",
|
|
8472
9347
|
whenNotToUse: "Do not use this when you already know the exact contact you want to inspect in depth.",
|
|
8473
9348
|
sideEffects: "none",
|
|
@@ -8478,6 +9353,9 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8478
9353
|
query: "Free-text or natural-key search text.",
|
|
8479
9354
|
kind: "Optional contact kind filter.",
|
|
8480
9355
|
lifecycleState: "Optional lifecycle state filter.",
|
|
9356
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9357
|
+
tag: "Tag name; arrays require all tags.",
|
|
9358
|
+
includeLost: "Whether lost contacts are included in search.",
|
|
8481
9359
|
includeOwner: "Whether owner contacts are eligible for results.",
|
|
8482
9360
|
includeDormant: "Whether dormant contacts should be eligible for results.",
|
|
8483
9361
|
limit: "Optional result limit.",
|
|
@@ -8502,6 +9380,17 @@ var searchAffinityTool = defineAffinityTool({
|
|
|
8502
9380
|
"merged",
|
|
8503
9381
|
"lost"
|
|
8504
9382
|
]),
|
|
9383
|
+
attribute: oneOfSchema([
|
|
9384
|
+
stringSchema("Attribute filter."),
|
|
9385
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9386
|
+
]),
|
|
9387
|
+
tag: oneOfSchema([
|
|
9388
|
+
stringSchema("Tag filter."),
|
|
9389
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9390
|
+
]),
|
|
9391
|
+
includeLost: booleanSchema(
|
|
9392
|
+
"Include lost contacts (search excludes them by default)."
|
|
9393
|
+
),
|
|
8505
9394
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8506
9395
|
includeDormant: booleanSchema("Whether dormant contacts are included."),
|
|
8507
9396
|
limit: integerSchema("Optional result limit."),
|
|
@@ -8537,6 +9426,9 @@ function getAffinityToolByName(name) {
|
|
|
8537
9426
|
|
|
8538
9427
|
// src/tools/tool_mapping.ts
|
|
8539
9428
|
var affinityToolMappings = [
|
|
9429
|
+
{ source: "exportContact", tool: manageContactToolName, action: "export" },
|
|
9430
|
+
{ source: "exportAffinity", tool: manageContactToolName, action: "export" },
|
|
9431
|
+
{ source: "deleteContact", tool: manageContactToolName, action: "delete" },
|
|
8540
9432
|
{ source: "searchContacts", tool: searchAffinityToolName },
|
|
8541
9433
|
{
|
|
8542
9434
|
source: "getOwnerProfile",
|
|
@@ -8723,6 +9615,7 @@ __export(write_exports, {
|
|
|
8723
9615
|
addDateAnchor: () => addDateAnchor,
|
|
8724
9616
|
addIdentity: () => addIdentity,
|
|
8725
9617
|
createContact: () => createContact,
|
|
9618
|
+
deleteContact: () => deleteContact,
|
|
8726
9619
|
dismissDuplicate: () => dismissDuplicate,
|
|
8727
9620
|
mergeContacts: () => mergeContacts,
|
|
8728
9621
|
overrideLinkState: () => overrideLinkState,
|