@ghostpaw/affinity 0.4.0 → 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 +174 -99
- package/SKILL.md +217 -33
- package/dist/cli.js +2471 -927
- package/dist/index.cjs +1410 -493
- package/dist/index.d.cts +556 -55
- package/dist/index.d.ts +556 -55
- package/dist/index.js +1410 -493
- package/dist/mcp-server.js +1845 -704
- package/dist/mcp.cjs +1631 -609
- package/dist/mcp.js +1632 -610
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +217 -33
package/dist/mcp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
|
|
6
6
|
var __defProp = Object.defineProperty;
|
|
@@ -257,10 +257,10 @@ async function serveMcpStdio(server, options = {}) {
|
|
|
257
257
|
const response = await server.handleLine(line.replace(/\r$/, ""));
|
|
258
258
|
if (response !== void 0)
|
|
259
259
|
await new Promise(
|
|
260
|
-
(
|
|
260
|
+
(resolve2, reject) => output.write(
|
|
261
261
|
`${response}
|
|
262
262
|
`,
|
|
263
|
-
(error) => error ? reject(error) :
|
|
263
|
+
(error) => error ? reject(error) : resolve2()
|
|
264
264
|
)
|
|
265
265
|
);
|
|
266
266
|
};
|
|
@@ -285,7 +285,7 @@ var init_mcp_stdio = __esm({
|
|
|
285
285
|
var affinityVersion;
|
|
286
286
|
var init_version = __esm({
|
|
287
287
|
"src/version.ts"() {
|
|
288
|
-
affinityVersion = "0.
|
|
288
|
+
affinityVersion = "0.5.0";
|
|
289
289
|
}
|
|
290
290
|
});
|
|
291
291
|
|
|
@@ -297,6 +297,16 @@ var init_affinity_mcp_instructions = __esm({
|
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
// src/surfaces/cli_args.ts
|
|
301
|
+
var CliUsageError;
|
|
302
|
+
var init_cli_args = __esm({
|
|
303
|
+
"src/surfaces/cli_args.ts"() {
|
|
304
|
+
CliUsageError = class extends Error {
|
|
305
|
+
exitCode = 2;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
300
310
|
// src/lib/errors/affinity_error.ts
|
|
301
311
|
var AffinityError;
|
|
302
312
|
var init_affinity_error = __esm({
|
|
@@ -311,6 +321,78 @@ var init_affinity_error = __esm({
|
|
|
311
321
|
}
|
|
312
322
|
});
|
|
313
323
|
|
|
324
|
+
// src/lib/errors/affinity_validation_error.ts
|
|
325
|
+
var AffinityValidationError;
|
|
326
|
+
var init_affinity_validation_error = __esm({
|
|
327
|
+
"src/lib/errors/affinity_validation_error.ts"() {
|
|
328
|
+
init_affinity_error();
|
|
329
|
+
AffinityValidationError = class extends AffinityError {
|
|
330
|
+
code = "VALIDATION";
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// src/lib/read/assert_default_ordering.ts
|
|
336
|
+
function assertDefaultOrdering(_readName, options) {
|
|
337
|
+
if (options?.sort !== void 0 || options?.descending !== void 0) {
|
|
338
|
+
throw new AffinityValidationError(
|
|
339
|
+
"This review does not support custom sort ordering"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
var init_assert_default_ordering = __esm({
|
|
344
|
+
"src/lib/read/assert_default_ordering.ts"() {
|
|
345
|
+
init_affinity_validation_error();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// src/lib/types/read_list_options.ts
|
|
350
|
+
var AFFINITY_DEFAULT_LIST_LIMIT, AFFINITY_MAX_LIST_LIMIT;
|
|
351
|
+
var init_read_list_options = __esm({
|
|
352
|
+
"src/lib/types/read_list_options.ts"() {
|
|
353
|
+
AFFINITY_DEFAULT_LIST_LIMIT = 50;
|
|
354
|
+
AFFINITY_MAX_LIST_LIMIT = 250;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// src/lib/types/resolve_affinity_list_limit.ts
|
|
359
|
+
function resolveAffinityListLimit(limit) {
|
|
360
|
+
if (limit === void 0) {
|
|
361
|
+
return AFFINITY_DEFAULT_LIST_LIMIT;
|
|
362
|
+
}
|
|
363
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
364
|
+
throw new AffinityValidationError("limit must be a positive integer");
|
|
365
|
+
}
|
|
366
|
+
if (limit > AFFINITY_MAX_LIST_LIMIT) {
|
|
367
|
+
throw new AffinityValidationError(
|
|
368
|
+
`limit must be <= ${String(AFFINITY_MAX_LIST_LIMIT)}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return limit;
|
|
372
|
+
}
|
|
373
|
+
var init_resolve_affinity_list_limit = __esm({
|
|
374
|
+
"src/lib/types/resolve_affinity_list_limit.ts"() {
|
|
375
|
+
init_affinity_validation_error();
|
|
376
|
+
init_read_list_options();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// src/lib/read/resolve_list_limit_offset.ts
|
|
381
|
+
function resolveListLimitOffset(options) {
|
|
382
|
+
const limit = resolveAffinityListLimit(options?.limit);
|
|
383
|
+
const offset = options?.offset ?? 0;
|
|
384
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
385
|
+
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
386
|
+
}
|
|
387
|
+
return { limit, offset };
|
|
388
|
+
}
|
|
389
|
+
var init_resolve_list_limit_offset = __esm({
|
|
390
|
+
"src/lib/read/resolve_list_limit_offset.ts"() {
|
|
391
|
+
init_affinity_validation_error();
|
|
392
|
+
init_resolve_affinity_list_limit();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
314
396
|
// src/lib/errors/affinity_invariant_error.ts
|
|
315
397
|
var AffinityInvariantError;
|
|
316
398
|
var init_affinity_invariant_error = __esm({
|
|
@@ -322,6 +404,17 @@ var init_affinity_invariant_error = __esm({
|
|
|
322
404
|
}
|
|
323
405
|
});
|
|
324
406
|
|
|
407
|
+
// src/lib/errors/affinity_not_found_error.ts
|
|
408
|
+
var AffinityNotFoundError;
|
|
409
|
+
var init_affinity_not_found_error = __esm({
|
|
410
|
+
"src/lib/errors/affinity_not_found_error.ts"() {
|
|
411
|
+
init_affinity_error();
|
|
412
|
+
AffinityNotFoundError = class extends AffinityError {
|
|
413
|
+
code = "NOT_FOUND";
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
325
418
|
// src/contacts/queries.ts
|
|
326
419
|
function getContactRowById(db, id) {
|
|
327
420
|
const row = db.prepare(
|
|
@@ -358,17 +451,6 @@ var init_queries = __esm({
|
|
|
358
451
|
}
|
|
359
452
|
});
|
|
360
453
|
|
|
361
|
-
// src/lib/errors/affinity_not_found_error.ts
|
|
362
|
-
var AffinityNotFoundError;
|
|
363
|
-
var init_affinity_not_found_error = __esm({
|
|
364
|
-
"src/lib/errors/affinity_not_found_error.ts"() {
|
|
365
|
-
init_affinity_error();
|
|
366
|
-
AffinityNotFoundError = class extends AffinityError {
|
|
367
|
-
code = "NOT_FOUND";
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
|
|
372
454
|
// src/lib/errors/affinity_state_error.ts
|
|
373
455
|
var AffinityStateError;
|
|
374
456
|
var init_affinity_state_error = __esm({
|
|
@@ -380,17 +462,6 @@ var init_affinity_state_error = __esm({
|
|
|
380
462
|
}
|
|
381
463
|
});
|
|
382
464
|
|
|
383
|
-
// src/lib/errors/affinity_validation_error.ts
|
|
384
|
-
var AffinityValidationError;
|
|
385
|
-
var init_affinity_validation_error = __esm({
|
|
386
|
-
"src/lib/errors/affinity_validation_error.ts"() {
|
|
387
|
-
init_affinity_error();
|
|
388
|
-
AffinityValidationError = class extends AffinityError {
|
|
389
|
-
code = "VALIDATION";
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
465
|
// src/events/validators.ts
|
|
395
466
|
function assertOwnerParticipates(ownerId, participantIds) {
|
|
396
467
|
if (!participantIds.includes(ownerId)) {
|
|
@@ -404,7 +475,10 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
404
475
|
throw new AffinityNotFoundError("contact not found");
|
|
405
476
|
}
|
|
406
477
|
if (row.lifecycle_state === "merged") {
|
|
407
|
-
|
|
478
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
479
|
+
throw new AffinityStateError(
|
|
480
|
+
`Contact #${id} was merged into #${row.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row.merged_into_contact_id}`
|
|
481
|
+
);
|
|
408
482
|
}
|
|
409
483
|
}
|
|
410
484
|
}
|
|
@@ -615,6 +689,149 @@ var init_queries2 = __esm({
|
|
|
615
689
|
}
|
|
616
690
|
});
|
|
617
691
|
|
|
692
|
+
// src/events/loaders.ts
|
|
693
|
+
function loadEventParticipantViews(db, eventId) {
|
|
694
|
+
const rows = db.prepare(
|
|
695
|
+
`SELECT contact_id, role, directionality
|
|
696
|
+
FROM event_participants
|
|
697
|
+
WHERE event_id = ?`
|
|
698
|
+
).all(eventId);
|
|
699
|
+
return rows.map((row) => ({
|
|
700
|
+
contactId: row.contact_id,
|
|
701
|
+
role: row.role,
|
|
702
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
function loadEventRecord(db, eventId) {
|
|
706
|
+
const row = getEventRowById(db, eventId);
|
|
707
|
+
if (!row) {
|
|
708
|
+
throw new AffinityInvariantError("event not found");
|
|
709
|
+
}
|
|
710
|
+
const participants = loadEventParticipantViews(db, eventId);
|
|
711
|
+
return mapEventRowToEventRecord(row, participants);
|
|
712
|
+
}
|
|
713
|
+
function batchLoadEventRecords(db, eventIds) {
|
|
714
|
+
if (eventIds.length === 0) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
718
|
+
const eventRows = db.prepare(
|
|
719
|
+
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
720
|
+
recurrence_kind, anchor_month, anchor_day,
|
|
721
|
+
anchor_contact_id, anchor_link_id,
|
|
722
|
+
created_at, updated_at, deleted_at
|
|
723
|
+
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
724
|
+
).all(...eventIds);
|
|
725
|
+
const partRows = db.prepare(
|
|
726
|
+
`SELECT event_id, contact_id, role, directionality
|
|
727
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
728
|
+
).all(...eventIds);
|
|
729
|
+
const partsByEvent = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const p of partRows) {
|
|
731
|
+
let arr = partsByEvent.get(p.event_id);
|
|
732
|
+
if (!arr) {
|
|
733
|
+
arr = [];
|
|
734
|
+
partsByEvent.set(p.event_id, arr);
|
|
735
|
+
}
|
|
736
|
+
arr.push({
|
|
737
|
+
contactId: p.contact_id,
|
|
738
|
+
role: p.role,
|
|
739
|
+
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
743
|
+
const out = [];
|
|
744
|
+
for (const id of eventIds) {
|
|
745
|
+
const row = rowById.get(id);
|
|
746
|
+
if (row) {
|
|
747
|
+
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return out;
|
|
751
|
+
}
|
|
752
|
+
function batchLoadEventParticipantViews(db, eventIds) {
|
|
753
|
+
const result = /* @__PURE__ */ new Map();
|
|
754
|
+
if (eventIds.length === 0) {
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
758
|
+
const rows = db.prepare(
|
|
759
|
+
`SELECT event_id, contact_id, role, directionality
|
|
760
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
761
|
+
).all(...eventIds);
|
|
762
|
+
for (const row of rows) {
|
|
763
|
+
let arr = result.get(row.event_id);
|
|
764
|
+
if (!arr) {
|
|
765
|
+
arr = [];
|
|
766
|
+
result.set(row.event_id, arr);
|
|
767
|
+
}
|
|
768
|
+
arr.push({
|
|
769
|
+
contactId: row.contact_id,
|
|
770
|
+
role: row.role,
|
|
771
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
776
|
+
function requireDateAnchorEvent(db, eventId) {
|
|
777
|
+
const row = getEventRowById(db, eventId);
|
|
778
|
+
if (!row) {
|
|
779
|
+
throw new AffinityNotFoundError("event not found");
|
|
780
|
+
}
|
|
781
|
+
if (row.type !== "date_anchor") {
|
|
782
|
+
throw new AffinityInvariantError("not a date anchor");
|
|
783
|
+
}
|
|
784
|
+
return row;
|
|
785
|
+
}
|
|
786
|
+
var init_loaders = __esm({
|
|
787
|
+
"src/events/loaders.ts"() {
|
|
788
|
+
init_affinity_invariant_error();
|
|
789
|
+
init_affinity_not_found_error();
|
|
790
|
+
init_mappers();
|
|
791
|
+
init_queries2();
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// src/events/get_contact_journal.ts
|
|
796
|
+
function getContactJournal(db, contactId, options) {
|
|
797
|
+
assertDefaultOrdering("getContactJournal", options);
|
|
798
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
799
|
+
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
800
|
+
const params = [contactId];
|
|
801
|
+
if (options?.since !== void 0) {
|
|
802
|
+
clauses.push("e.occurred_at >= ?");
|
|
803
|
+
params.push(options.since);
|
|
804
|
+
}
|
|
805
|
+
if (options?.until !== void 0) {
|
|
806
|
+
clauses.push("e.occurred_at <= ?");
|
|
807
|
+
params.push(options.until);
|
|
808
|
+
}
|
|
809
|
+
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
810
|
+
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
811
|
+
clauses.push(`e.type IN (${ph})`);
|
|
812
|
+
params.push(...options.eventTypes);
|
|
813
|
+
}
|
|
814
|
+
const where = clauses.join(" AND ");
|
|
815
|
+
const rows = db.prepare(
|
|
816
|
+
`SELECT e.id FROM events e
|
|
817
|
+
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
818
|
+
WHERE ${where}
|
|
819
|
+
ORDER BY e.occurred_at DESC, e.id DESC
|
|
820
|
+
LIMIT ? OFFSET ?`
|
|
821
|
+
).all(...params, limit, offset);
|
|
822
|
+
return batchLoadEventRecords(
|
|
823
|
+
db,
|
|
824
|
+
rows.map((r) => r.id)
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
var init_get_contact_journal = __esm({
|
|
828
|
+
"src/events/get_contact_journal.ts"() {
|
|
829
|
+
init_assert_default_ordering();
|
|
830
|
+
init_resolve_list_limit_offset();
|
|
831
|
+
init_loaders();
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
618
835
|
// src/resolve_now.ts
|
|
619
836
|
function resolveNow(now) {
|
|
620
837
|
if (now !== void 0) {
|
|
@@ -633,9 +850,98 @@ var init_resolve_now = __esm({
|
|
|
633
850
|
}
|
|
634
851
|
});
|
|
635
852
|
|
|
636
|
-
// src/
|
|
637
|
-
function
|
|
638
|
-
|
|
853
|
+
// src/events/list_open_commitments.ts
|
|
854
|
+
function mapResolution(resolvedAt, resolution) {
|
|
855
|
+
if (resolvedAt === null) {
|
|
856
|
+
return "open";
|
|
857
|
+
}
|
|
858
|
+
if (resolution === "cancelled") {
|
|
859
|
+
return "cancelled";
|
|
860
|
+
}
|
|
861
|
+
return "resolved";
|
|
862
|
+
}
|
|
863
|
+
function listOpenCommitments(db, filters, options) {
|
|
864
|
+
assertDefaultOrdering("listOpenCommitments", options);
|
|
865
|
+
const now = resolveNow(options?.now);
|
|
866
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
867
|
+
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
868
|
+
const params = [];
|
|
869
|
+
if (options?.since !== void 0) {
|
|
870
|
+
clauses.push("e.occurred_at >= ?");
|
|
871
|
+
params.push(options.since);
|
|
872
|
+
}
|
|
873
|
+
if (options?.until !== void 0) {
|
|
874
|
+
clauses.push("e.occurred_at <= ?");
|
|
875
|
+
params.push(options.until);
|
|
876
|
+
}
|
|
877
|
+
if (filters?.commitmentType !== void 0) {
|
|
878
|
+
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
879
|
+
throw new AffinityValidationError("invalid commitmentType filter");
|
|
880
|
+
}
|
|
881
|
+
clauses.push("oc.commitment_type = ?");
|
|
882
|
+
params.push(filters.commitmentType);
|
|
883
|
+
}
|
|
884
|
+
if (filters?.contactId !== void 0) {
|
|
885
|
+
clauses.push(
|
|
886
|
+
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
887
|
+
);
|
|
888
|
+
params.push(filters.contactId);
|
|
889
|
+
}
|
|
890
|
+
if (filters?.linkId !== void 0) {
|
|
891
|
+
clauses.push(
|
|
892
|
+
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
893
|
+
);
|
|
894
|
+
params.push(filters.linkId);
|
|
895
|
+
}
|
|
896
|
+
if (filters?.horizonDays !== void 0) {
|
|
897
|
+
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
898
|
+
throw new AffinityValidationError(
|
|
899
|
+
"horizonDays must be a non-negative integer"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
const anchor = options?.since ?? now;
|
|
903
|
+
clauses.push("oc.due_at IS NOT NULL");
|
|
904
|
+
clauses.push("oc.due_at <= ?");
|
|
905
|
+
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
906
|
+
}
|
|
907
|
+
const where = clauses.join(" AND ");
|
|
908
|
+
const rows = db.prepare(
|
|
909
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
910
|
+
e.type, e.summary
|
|
911
|
+
FROM open_commitments oc
|
|
912
|
+
INNER JOIN events e ON e.id = oc.event_id
|
|
913
|
+
WHERE ${where}
|
|
914
|
+
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
915
|
+
LIMIT ? OFFSET ?`
|
|
916
|
+
).all(...params, limit, offset);
|
|
917
|
+
const eventIds = rows.map((r) => r.event_id);
|
|
918
|
+
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
919
|
+
return rows.map((r) => ({
|
|
920
|
+
eventId: r.event_id,
|
|
921
|
+
type: r.type,
|
|
922
|
+
summary: r.summary,
|
|
923
|
+
participants: partsByEvent.get(r.event_id) ?? [],
|
|
924
|
+
dueAt: r.due_at,
|
|
925
|
+
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
926
|
+
overdue: r.due_at !== null && r.due_at < now,
|
|
927
|
+
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
var DAY_MS;
|
|
931
|
+
var init_list_open_commitments = __esm({
|
|
932
|
+
"src/events/list_open_commitments.ts"() {
|
|
933
|
+
init_affinity_validation_error();
|
|
934
|
+
init_assert_default_ordering();
|
|
935
|
+
init_resolve_list_limit_offset();
|
|
936
|
+
init_resolve_now();
|
|
937
|
+
init_loaders();
|
|
938
|
+
DAY_MS = 864e5;
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// src/with_transaction.ts
|
|
943
|
+
function withTransaction(db, fn) {
|
|
944
|
+
const depth = transactionDepth.get(db) ?? 0;
|
|
639
945
|
const savepoint = `affinity_${String(depth)}`;
|
|
640
946
|
if (depth === 0) {
|
|
641
947
|
db.exec("BEGIN IMMEDIATE");
|
|
@@ -937,13 +1243,20 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
937
1243
|
const topLinks = linkRows.map((l) => mapLinkRowToLinkListItem(l));
|
|
938
1244
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
939
1245
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1246
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
940
1247
|
return {
|
|
1248
|
+
...winner ? { mergedInto: { id: winner.id, name: winner.name } } : {},
|
|
941
1249
|
contact: mapContactRowToContactCore(row),
|
|
942
1250
|
identities,
|
|
943
1251
|
attributes,
|
|
944
1252
|
topLinks,
|
|
945
1253
|
activeDates,
|
|
946
|
-
warnings:
|
|
1254
|
+
warnings: [
|
|
1255
|
+
...contactRollup?.warnings ?? [],
|
|
1256
|
+
...winner ? [
|
|
1257
|
+
`Contact #${row.id} was merged into #${winner.id} ${winner.name}; use contactId ${winner.id}`
|
|
1258
|
+
] : []
|
|
1259
|
+
],
|
|
947
1260
|
rollups: contactRollup?.rollup ?? null
|
|
948
1261
|
};
|
|
949
1262
|
}
|
|
@@ -961,10 +1274,32 @@ var init_load_contact_profile_record = __esm({
|
|
|
961
1274
|
// src/contacts/get_contact_profile.ts
|
|
962
1275
|
function getContactProfile(db, contactId, options) {
|
|
963
1276
|
const t = options?.topLinksLimit ?? 8;
|
|
964
|
-
|
|
1277
|
+
const profile = loadContactProfileRecord(db, contactId, t);
|
|
1278
|
+
if (!profile) return null;
|
|
1279
|
+
const ties = db.prepare(`SELECT l.id, l.kind, l.role, c.id AS contactId, c.name
|
|
1280
|
+
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
|
|
1281
|
+
WHERE l.is_structural = 1 AND l.removed_at IS NULL AND (l.from_contact_id = ? OR l.to_contact_id = ?)
|
|
1282
|
+
AND c.deleted_at IS NULL ORDER BY l.id`).all(contactId, contactId, contactId);
|
|
1283
|
+
return {
|
|
1284
|
+
...profile,
|
|
1285
|
+
recentEvents: getContactJournal(db, contactId, {
|
|
1286
|
+
limit: options?.recentEventsLimit ?? 5
|
|
1287
|
+
}).map((event) => ({
|
|
1288
|
+
...event,
|
|
1289
|
+
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
1290
|
+
})),
|
|
1291
|
+
openCommitments: listOpenCommitments(
|
|
1292
|
+
db,
|
|
1293
|
+
{ contactId },
|
|
1294
|
+
options?.now === void 0 ? void 0 : { now: options.now }
|
|
1295
|
+
),
|
|
1296
|
+
ties
|
|
1297
|
+
};
|
|
965
1298
|
}
|
|
966
1299
|
var init_get_contact_profile = __esm({
|
|
967
1300
|
"src/contacts/get_contact_profile.ts"() {
|
|
1301
|
+
init_get_contact_journal();
|
|
1302
|
+
init_list_open_commitments();
|
|
968
1303
|
init_load_contact_profile_record();
|
|
969
1304
|
}
|
|
970
1305
|
});
|
|
@@ -985,67 +1320,6 @@ var init_get_owner_profile = __esm({
|
|
|
985
1320
|
}
|
|
986
1321
|
});
|
|
987
1322
|
|
|
988
|
-
// src/lib/read/assert_default_ordering.ts
|
|
989
|
-
function assertDefaultOrdering(readName, options) {
|
|
990
|
-
if (options?.sort !== void 0 || options?.descending !== void 0) {
|
|
991
|
-
throw new AffinityValidationError(
|
|
992
|
-
`${readName} does not support custom sort ordering`
|
|
993
|
-
);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
var init_assert_default_ordering = __esm({
|
|
997
|
-
"src/lib/read/assert_default_ordering.ts"() {
|
|
998
|
-
init_affinity_validation_error();
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
// src/lib/types/read_list_options.ts
|
|
1003
|
-
var AFFINITY_DEFAULT_LIST_LIMIT, AFFINITY_MAX_LIST_LIMIT;
|
|
1004
|
-
var init_read_list_options = __esm({
|
|
1005
|
-
"src/lib/types/read_list_options.ts"() {
|
|
1006
|
-
AFFINITY_DEFAULT_LIST_LIMIT = 50;
|
|
1007
|
-
AFFINITY_MAX_LIST_LIMIT = 250;
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
// src/lib/types/resolve_affinity_list_limit.ts
|
|
1012
|
-
function resolveAffinityListLimit(limit) {
|
|
1013
|
-
if (limit === void 0) {
|
|
1014
|
-
return AFFINITY_DEFAULT_LIST_LIMIT;
|
|
1015
|
-
}
|
|
1016
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
1017
|
-
throw new AffinityValidationError("limit must be a positive integer");
|
|
1018
|
-
}
|
|
1019
|
-
if (limit > AFFINITY_MAX_LIST_LIMIT) {
|
|
1020
|
-
throw new AffinityValidationError(
|
|
1021
|
-
`limit must be <= ${String(AFFINITY_MAX_LIST_LIMIT)}`
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
return limit;
|
|
1025
|
-
}
|
|
1026
|
-
var init_resolve_affinity_list_limit = __esm({
|
|
1027
|
-
"src/lib/types/resolve_affinity_list_limit.ts"() {
|
|
1028
|
-
init_affinity_validation_error();
|
|
1029
|
-
init_read_list_options();
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
// src/lib/read/resolve_list_limit_offset.ts
|
|
1034
|
-
function resolveListLimitOffset(options) {
|
|
1035
|
-
const limit = resolveAffinityListLimit(options?.limit);
|
|
1036
|
-
const offset = options?.offset ?? 0;
|
|
1037
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
1038
|
-
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
1039
|
-
}
|
|
1040
|
-
return { limit, offset };
|
|
1041
|
-
}
|
|
1042
|
-
var init_resolve_list_limit_offset = __esm({
|
|
1043
|
-
"src/lib/read/resolve_list_limit_offset.ts"() {
|
|
1044
|
-
init_affinity_validation_error();
|
|
1045
|
-
init_resolve_affinity_list_limit();
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
1323
|
// src/events/list_moments.ts
|
|
1050
1324
|
function listMoments(db, filters, options) {
|
|
1051
1325
|
assertDefaultOrdering("listMoments", options);
|
|
@@ -1107,109 +1381,6 @@ var init_list_moments = __esm({
|
|
|
1107
1381
|
}
|
|
1108
1382
|
});
|
|
1109
1383
|
|
|
1110
|
-
// src/events/loaders.ts
|
|
1111
|
-
function loadEventParticipantViews(db, eventId) {
|
|
1112
|
-
const rows = db.prepare(
|
|
1113
|
-
`SELECT contact_id, role, directionality
|
|
1114
|
-
FROM event_participants
|
|
1115
|
-
WHERE event_id = ?`
|
|
1116
|
-
).all(eventId);
|
|
1117
|
-
return rows.map((row) => ({
|
|
1118
|
-
contactId: row.contact_id,
|
|
1119
|
-
role: row.role,
|
|
1120
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1121
|
-
}));
|
|
1122
|
-
}
|
|
1123
|
-
function loadEventRecord(db, eventId) {
|
|
1124
|
-
const row = getEventRowById(db, eventId);
|
|
1125
|
-
if (!row) {
|
|
1126
|
-
throw new AffinityInvariantError("event not found");
|
|
1127
|
-
}
|
|
1128
|
-
const participants = loadEventParticipantViews(db, eventId);
|
|
1129
|
-
return mapEventRowToEventRecord(row, participants);
|
|
1130
|
-
}
|
|
1131
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
1132
|
-
if (eventIds.length === 0) {
|
|
1133
|
-
return [];
|
|
1134
|
-
}
|
|
1135
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
1136
|
-
const eventRows = db.prepare(
|
|
1137
|
-
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
1138
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
1139
|
-
anchor_contact_id, anchor_link_id,
|
|
1140
|
-
created_at, updated_at, deleted_at
|
|
1141
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
1142
|
-
).all(...eventIds);
|
|
1143
|
-
const partRows = db.prepare(
|
|
1144
|
-
`SELECT event_id, contact_id, role, directionality
|
|
1145
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
1146
|
-
).all(...eventIds);
|
|
1147
|
-
const partsByEvent = /* @__PURE__ */ new Map();
|
|
1148
|
-
for (const p of partRows) {
|
|
1149
|
-
let arr = partsByEvent.get(p.event_id);
|
|
1150
|
-
if (!arr) {
|
|
1151
|
-
arr = [];
|
|
1152
|
-
partsByEvent.set(p.event_id, arr);
|
|
1153
|
-
}
|
|
1154
|
-
arr.push({
|
|
1155
|
-
contactId: p.contact_id,
|
|
1156
|
-
role: p.role,
|
|
1157
|
-
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
1161
|
-
const out = [];
|
|
1162
|
-
for (const id of eventIds) {
|
|
1163
|
-
const row = rowById.get(id);
|
|
1164
|
-
if (row) {
|
|
1165
|
-
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
return out;
|
|
1169
|
-
}
|
|
1170
|
-
function batchLoadEventParticipantViews(db, eventIds) {
|
|
1171
|
-
const result = /* @__PURE__ */ new Map();
|
|
1172
|
-
if (eventIds.length === 0) {
|
|
1173
|
-
return result;
|
|
1174
|
-
}
|
|
1175
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
1176
|
-
const rows = db.prepare(
|
|
1177
|
-
`SELECT event_id, contact_id, role, directionality
|
|
1178
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
1179
|
-
).all(...eventIds);
|
|
1180
|
-
for (const row of rows) {
|
|
1181
|
-
let arr = result.get(row.event_id);
|
|
1182
|
-
if (!arr) {
|
|
1183
|
-
arr = [];
|
|
1184
|
-
result.set(row.event_id, arr);
|
|
1185
|
-
}
|
|
1186
|
-
arr.push({
|
|
1187
|
-
contactId: row.contact_id,
|
|
1188
|
-
role: row.role,
|
|
1189
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
return result;
|
|
1193
|
-
}
|
|
1194
|
-
function requireDateAnchorEvent(db, eventId) {
|
|
1195
|
-
const row = getEventRowById(db, eventId);
|
|
1196
|
-
if (!row) {
|
|
1197
|
-
throw new AffinityNotFoundError("event not found");
|
|
1198
|
-
}
|
|
1199
|
-
if (row.type !== "date_anchor") {
|
|
1200
|
-
throw new AffinityInvariantError("not a date anchor");
|
|
1201
|
-
}
|
|
1202
|
-
return row;
|
|
1203
|
-
}
|
|
1204
|
-
var init_loaders = __esm({
|
|
1205
|
-
"src/events/loaders.ts"() {
|
|
1206
|
-
init_affinity_invariant_error();
|
|
1207
|
-
init_affinity_not_found_error();
|
|
1208
|
-
init_mappers();
|
|
1209
|
-
init_queries2();
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
1384
|
// src/links/queries.ts
|
|
1214
1385
|
function getLinkRowById(db, id) {
|
|
1215
1386
|
const row = db.prepare(
|
|
@@ -1482,7 +1653,7 @@ function recencyScore(occurredAt, cadenceDays, now) {
|
|
|
1482
1653
|
return 0;
|
|
1483
1654
|
}
|
|
1484
1655
|
const cadence = Math.max(cadenceDays ?? 0, 1);
|
|
1485
|
-
const daysSince = Math.max(0, (now - occurredAt) /
|
|
1656
|
+
const daysSince = Math.max(0, (now - occurredAt) / DAY_MS2);
|
|
1486
1657
|
return Math.max(0, 1 - daysSince / cadence);
|
|
1487
1658
|
}
|
|
1488
1659
|
function refreshLinkRollup(db, linkId, now) {
|
|
@@ -1493,15 +1664,15 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1493
1664
|
const latest = db.prepare(
|
|
1494
1665
|
`SELECT event_id, occurred_at
|
|
1495
1666
|
FROM link_event_effects
|
|
1496
|
-
WHERE link_id = ?
|
|
1667
|
+
WHERE link_id = ? AND counts_for_recency = 1
|
|
1497
1668
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1498
1669
|
LIMIT 1`
|
|
1499
1670
|
).get(linkId);
|
|
1500
|
-
const trailingWindowStart = now - 180 *
|
|
1671
|
+
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1501
1672
|
const counts = db.prepare(
|
|
1502
1673
|
`SELECT
|
|
1503
1674
|
COUNT(*) AS total_meaningful_events,
|
|
1504
|
-
SUM(CASE WHEN affinity_delta > 0 OR trust_delta > 0 THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1675
|
+
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1505
1676
|
FROM link_event_effects
|
|
1506
1677
|
WHERE link_id = ? AND occurred_at >= ?`
|
|
1507
1678
|
).get(linkId, trailingWindowStart);
|
|
@@ -1509,7 +1680,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1509
1680
|
`SELECT ep.directionality
|
|
1510
1681
|
FROM link_event_effects le
|
|
1511
1682
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1512
|
-
WHERE le.link_id = ? AND ep.contact_id = ?
|
|
1683
|
+
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1513
1684
|
AND ep.directionality IS NOT NULL
|
|
1514
1685
|
AND le.occurred_at >= ?`
|
|
1515
1686
|
).all(linkId, link.from_contact_id, trailingWindowStart);
|
|
@@ -1535,7 +1706,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1535
1706
|
link.cadence_days,
|
|
1536
1707
|
now
|
|
1537
1708
|
);
|
|
1538
|
-
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) /
|
|
1709
|
+
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) / DAY_MS2);
|
|
1539
1710
|
const driftPriorityValue = driftPriority({
|
|
1540
1711
|
daysSinceLastMeaningfulEvent,
|
|
1541
1712
|
cadenceDays: link.cadence_days ?? 1,
|
|
@@ -1618,13 +1789,13 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1618
1789
|
refreshContactRollup(db, link.from_contact_id, now);
|
|
1619
1790
|
refreshContactRollup(db, link.to_contact_id, now);
|
|
1620
1791
|
}
|
|
1621
|
-
var
|
|
1792
|
+
var DAY_MS2;
|
|
1622
1793
|
var init_rollups2 = __esm({
|
|
1623
1794
|
"src/links/rollups.ts"() {
|
|
1624
1795
|
init_rollups();
|
|
1625
1796
|
init_mechanics();
|
|
1626
1797
|
init_queries3();
|
|
1627
|
-
|
|
1798
|
+
DAY_MS2 = 864e5;
|
|
1628
1799
|
}
|
|
1629
1800
|
});
|
|
1630
1801
|
|
|
@@ -1682,8 +1853,61 @@ var init_get_link_detail = __esm({
|
|
|
1682
1853
|
}
|
|
1683
1854
|
});
|
|
1684
1855
|
|
|
1856
|
+
// src/tools/parse_time_input.ts
|
|
1857
|
+
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
1858
|
+
const fail = () => {
|
|
1859
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1860
|
+
};
|
|
1861
|
+
if (typeof value === "number")
|
|
1862
|
+
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
1863
|
+
if (typeof value !== "string") return fail();
|
|
1864
|
+
const text = value.trim();
|
|
1865
|
+
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
1866
|
+
let day;
|
|
1867
|
+
if (relative) {
|
|
1868
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1869
|
+
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
1870
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
1871
|
+
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
1872
|
+
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
1873
|
+
return fail();
|
|
1874
|
+
} else {
|
|
1875
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1876
|
+
text
|
|
1877
|
+
))
|
|
1878
|
+
return fail();
|
|
1879
|
+
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
1880
|
+
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
1881
|
+
return fail();
|
|
1882
|
+
const parsed = Date.parse(text);
|
|
1883
|
+
return Number.isFinite(parsed) ? parsed : fail();
|
|
1884
|
+
}
|
|
1885
|
+
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
1886
|
+
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
1887
|
+
}
|
|
1888
|
+
var timeInputForms;
|
|
1889
|
+
var init_parse_time_input = __esm({
|
|
1890
|
+
"src/tools/parse_time_input.ts"() {
|
|
1891
|
+
init_affinity_validation_error();
|
|
1892
|
+
timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1685
1896
|
// src/tools/tool_metadata.ts
|
|
1686
1897
|
function defineAffinityTool(definition) {
|
|
1898
|
+
const visit = (schema) => {
|
|
1899
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
1900
|
+
if ((key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.type === "integer") {
|
|
1901
|
+
child.type = void 0;
|
|
1902
|
+
child.oneOf = [{ type: "integer" }, { type: "string" }];
|
|
1903
|
+
child.description = `${child.description ?? "Time."} Accepts ${timeInputForms}. UTC midnight for dates; dueAt uses UTC end of day.`;
|
|
1904
|
+
}
|
|
1905
|
+
visit(child);
|
|
1906
|
+
}
|
|
1907
|
+
if (schema.items) visit(schema.items);
|
|
1908
|
+
for (const child of schema.oneOf ?? schema.anyOf ?? []) visit(child);
|
|
1909
|
+
};
|
|
1910
|
+
visit(definition.inputSchema);
|
|
1687
1911
|
return definition;
|
|
1688
1912
|
}
|
|
1689
1913
|
function stringSchema(description, options = {}) {
|
|
@@ -1730,8 +1954,15 @@ function objectSchema(properties, required, description) {
|
|
|
1730
1954
|
function nullableStringSchema(description) {
|
|
1731
1955
|
return { anyOf: [{ type: "string" }, { type: "null" }], description };
|
|
1732
1956
|
}
|
|
1957
|
+
function oneOfSchema(schemas, description) {
|
|
1958
|
+
const schema = {
|
|
1959
|
+
oneOf: schemas
|
|
1960
|
+
};
|
|
1961
|
+
return schema;
|
|
1962
|
+
}
|
|
1733
1963
|
var init_tool_metadata = __esm({
|
|
1734
1964
|
"src/tools/tool_metadata.ts"() {
|
|
1965
|
+
init_parse_time_input();
|
|
1735
1966
|
}
|
|
1736
1967
|
});
|
|
1737
1968
|
|
|
@@ -1772,6 +2003,7 @@ function toContactEntityRef(value) {
|
|
|
1772
2003
|
function toContactListToolItem(value) {
|
|
1773
2004
|
return {
|
|
1774
2005
|
...toContactEntityRef(value),
|
|
2006
|
+
..."matchedOn" in value ? { matchedOn: value.matchedOn } : {},
|
|
1775
2007
|
subtitle: "kind" in value ? value.kind : void 0
|
|
1776
2008
|
};
|
|
1777
2009
|
}
|
|
@@ -1803,7 +2035,9 @@ function toEventEntityRef(value) {
|
|
|
1803
2035
|
}
|
|
1804
2036
|
function toEventListToolItem(value) {
|
|
1805
2037
|
return {
|
|
2038
|
+
...value,
|
|
1806
2039
|
...toEventEntityRef(value),
|
|
2040
|
+
occurredAtIso: new Date(value.occurredAt).toISOString(),
|
|
1807
2041
|
snippet: `${value.participants.length} participant(s)`
|
|
1808
2042
|
};
|
|
1809
2043
|
}
|
|
@@ -1865,7 +2099,7 @@ function withOptionalFields(result, options) {
|
|
|
1865
2099
|
if (options.warnings && options.warnings.length > 0) {
|
|
1866
2100
|
result.warnings = options.warnings;
|
|
1867
2101
|
}
|
|
1868
|
-
return result;
|
|
2102
|
+
return roundToolValues(result);
|
|
1869
2103
|
}
|
|
1870
2104
|
function toolWarning(code, message) {
|
|
1871
2105
|
return { code, message };
|
|
@@ -1929,13 +2163,35 @@ function toolFailure(kind, code, summary, message, options = {}) {
|
|
|
1929
2163
|
options
|
|
1930
2164
|
);
|
|
1931
2165
|
}
|
|
2166
|
+
function roundToolValues(value) {
|
|
2167
|
+
if (typeof value === "number")
|
|
2168
|
+
return Number.isInteger(value) ? value : Math.round(value * 1e3) / 1e3;
|
|
2169
|
+
if (Array.isArray(value)) return value.map(roundToolValues);
|
|
2170
|
+
if (value !== null && typeof value === "object")
|
|
2171
|
+
return Object.fromEntries(
|
|
2172
|
+
Object.entries(value).map(([key, child]) => [
|
|
2173
|
+
key,
|
|
2174
|
+
roundToolValues(child)
|
|
2175
|
+
])
|
|
2176
|
+
);
|
|
2177
|
+
return value;
|
|
2178
|
+
}
|
|
1932
2179
|
var init_tool_types = __esm({
|
|
1933
2180
|
"src/tools/tool_types.ts"() {
|
|
1934
2181
|
}
|
|
1935
2182
|
});
|
|
1936
2183
|
|
|
1937
2184
|
// src/tools/tool_resolvers.ts
|
|
1938
|
-
function resolveContactLocator(db, locator, field = "contact") {
|
|
2185
|
+
function resolveContactLocator(db, locator, field = "contact", allowMerged = false) {
|
|
2186
|
+
if (!locator || typeof locator !== "object")
|
|
2187
|
+
return {
|
|
2188
|
+
ok: false,
|
|
2189
|
+
result: toolNeedsClarification(
|
|
2190
|
+
"missing_required_choice",
|
|
2191
|
+
`Provide ${field} locator.`,
|
|
2192
|
+
[field]
|
|
2193
|
+
)
|
|
2194
|
+
};
|
|
1939
2195
|
if ("contactId" in locator) {
|
|
1940
2196
|
const row2 = getContactRowById(db, locator.contactId);
|
|
1941
2197
|
if (!row2) {
|
|
@@ -1949,8 +2205,25 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
1949
2205
|
)
|
|
1950
2206
|
};
|
|
1951
2207
|
}
|
|
2208
|
+
if (!allowMerged && row2.merged_into_contact_id !== null) {
|
|
2209
|
+
const winner = getContactRowById(db, row2.merged_into_contact_id);
|
|
2210
|
+
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
2211
|
+
return {
|
|
2212
|
+
ok: false,
|
|
2213
|
+
result: toolFailure("domain", "invalid_state", message, message)
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
1952
2216
|
return { ok: true, value: row2 };
|
|
1953
2217
|
}
|
|
2218
|
+
if (!("identity" in locator) || !locator.identity)
|
|
2219
|
+
return {
|
|
2220
|
+
ok: false,
|
|
2221
|
+
result: toolNeedsClarification(
|
|
2222
|
+
"missing_required_choice",
|
|
2223
|
+
`Provide ${field}.contactId or identity.`,
|
|
2224
|
+
[field]
|
|
2225
|
+
)
|
|
2226
|
+
};
|
|
1954
2227
|
const normalizedKey = normalizeIdentityKey(
|
|
1955
2228
|
locator.identity.type,
|
|
1956
2229
|
locator.identity.value
|
|
@@ -2008,6 +2281,15 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
2008
2281
|
return { ok: true, value: matchedContact };
|
|
2009
2282
|
}
|
|
2010
2283
|
function resolveLinkLocator(db, locator, field = "link") {
|
|
2284
|
+
if (!locator || typeof locator !== "object")
|
|
2285
|
+
return {
|
|
2286
|
+
ok: false,
|
|
2287
|
+
result: toolNeedsClarification(
|
|
2288
|
+
"missing_required_choice",
|
|
2289
|
+
`Provide ${field} locator.`,
|
|
2290
|
+
[field]
|
|
2291
|
+
)
|
|
2292
|
+
};
|
|
2011
2293
|
if ("linkId" in locator) {
|
|
2012
2294
|
const row = getLinkRowById(db, locator.linkId);
|
|
2013
2295
|
if (!row) {
|
|
@@ -2023,6 +2305,15 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
2023
2305
|
}
|
|
2024
2306
|
return { ok: true, value: row };
|
|
2025
2307
|
}
|
|
2308
|
+
if (!("endpoints" in locator) || !locator.endpoints)
|
|
2309
|
+
return {
|
|
2310
|
+
ok: false,
|
|
2311
|
+
result: toolNeedsClarification(
|
|
2312
|
+
"missing_required_choice",
|
|
2313
|
+
`Provide ${field}.linkId or endpoints.`,
|
|
2314
|
+
[field]
|
|
2315
|
+
)
|
|
2316
|
+
};
|
|
2026
2317
|
const { endpoints } = locator;
|
|
2027
2318
|
const clauses = [
|
|
2028
2319
|
"removed_at IS NULL",
|
|
@@ -2151,6 +2442,97 @@ var init_tool_resolvers = __esm({
|
|
|
2151
2442
|
}
|
|
2152
2443
|
});
|
|
2153
2444
|
|
|
2445
|
+
// src/tools/validate_tool_input.ts
|
|
2446
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2447
|
+
if (schema.anyOf || schema.oneOf) {
|
|
2448
|
+
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
2449
|
+
return choices.some(
|
|
2450
|
+
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2451
|
+
) ? null : `${path}: invalid value`;
|
|
2452
|
+
}
|
|
2453
|
+
if (schema.type === "object") {
|
|
2454
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
2455
|
+
return `${path}: missing input or expected object`;
|
|
2456
|
+
const object = value;
|
|
2457
|
+
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2458
|
+
for (const key of Object.keys(object)) {
|
|
2459
|
+
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
2460
|
+
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(", ")}`;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
2464
|
+
return `${path}.patch: missing required field`;
|
|
2465
|
+
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
2466
|
+
return `${path}.patch: unknown patch key; allowed keys: none (use the action's named fields)`;
|
|
2467
|
+
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
2468
|
+
try {
|
|
2469
|
+
return parseTimeInput(
|
|
2470
|
+
object.now,
|
|
2471
|
+
`${path}.now`,
|
|
2472
|
+
injectedNow
|
|
2473
|
+
);
|
|
2474
|
+
} catch {
|
|
2475
|
+
return Date.now();
|
|
2476
|
+
}
|
|
2477
|
+
})();
|
|
2478
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2479
|
+
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
2480
|
+
try {
|
|
2481
|
+
object[key] = parseTimeInput(
|
|
2482
|
+
object[key],
|
|
2483
|
+
`${path}.${key}`,
|
|
2484
|
+
referenceNow
|
|
2485
|
+
);
|
|
2486
|
+
} catch (error) {
|
|
2487
|
+
return error.message;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
for (const key of schema.required ?? []) {
|
|
2492
|
+
if (object[key] === void 0)
|
|
2493
|
+
return `${path}.${key}: missing required field`;
|
|
2494
|
+
}
|
|
2495
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2496
|
+
if (object[key] === void 0) continue;
|
|
2497
|
+
const error = validateToolInput(
|
|
2498
|
+
child,
|
|
2499
|
+
object[key],
|
|
2500
|
+
`${path}.${key}`,
|
|
2501
|
+
referenceNow
|
|
2502
|
+
);
|
|
2503
|
+
if (error) return error;
|
|
2504
|
+
}
|
|
2505
|
+
return null;
|
|
2506
|
+
}
|
|
2507
|
+
if (schema.type === "array") {
|
|
2508
|
+
if (!Array.isArray(value)) return `${path}: expected array`;
|
|
2509
|
+
for (const [index, item] of value.entries()) {
|
|
2510
|
+
const error = validateToolInput(
|
|
2511
|
+
schema.items ?? {},
|
|
2512
|
+
item,
|
|
2513
|
+
`${path}[${index}]`,
|
|
2514
|
+
injectedNow
|
|
2515
|
+
);
|
|
2516
|
+
if (error) return error;
|
|
2517
|
+
}
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
2521
|
+
return `${path}: expected ${schema.enum.join(", ")}`;
|
|
2522
|
+
if (schema.type === "null")
|
|
2523
|
+
return value === null ? null : `${path}: expected null`;
|
|
2524
|
+
if (schema.type === "integer")
|
|
2525
|
+
return Number.isInteger(value) ? null : `${path}: expected integer`;
|
|
2526
|
+
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";
|
|
2527
|
+
if (!valid) return `${path}: expected ${schema.type}`;
|
|
2528
|
+
return null;
|
|
2529
|
+
}
|
|
2530
|
+
var init_validate_tool_input = __esm({
|
|
2531
|
+
"src/tools/validate_tool_input.ts"() {
|
|
2532
|
+
init_parse_time_input();
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2154
2536
|
// src/tools/inspect_affinity_item_tool.ts
|
|
2155
2537
|
function contactLocatorSchema(description) {
|
|
2156
2538
|
return {
|
|
@@ -2186,7 +2568,19 @@ function linkLocatorSchema(description) {
|
|
|
2186
2568
|
description
|
|
2187
2569
|
};
|
|
2188
2570
|
}
|
|
2189
|
-
function inspectAffinityItemToolHandler(db,
|
|
2571
|
+
function inspectAffinityItemToolHandler(db, rawInput) {
|
|
2572
|
+
const input = structuredClone(rawInput);
|
|
2573
|
+
const inputError = validateToolInput(
|
|
2574
|
+
inspectAffinityItemTool.inputSchema,
|
|
2575
|
+
input
|
|
2576
|
+
);
|
|
2577
|
+
if (inputError)
|
|
2578
|
+
return toolFailure(
|
|
2579
|
+
"protocol",
|
|
2580
|
+
"invalid_input",
|
|
2581
|
+
"Invalid input.",
|
|
2582
|
+
inputError
|
|
2583
|
+
);
|
|
2190
2584
|
return withToolHandling(() => {
|
|
2191
2585
|
if (input.kind === "owner_profile") {
|
|
2192
2586
|
const detail2 = getOwnerProfile(
|
|
@@ -2204,19 +2598,21 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
2204
2598
|
return toolSuccess(
|
|
2205
2599
|
`Loaded owner profile for ${detail2.contact.name}.`,
|
|
2206
2600
|
{ kind: input.kind, detail: detail2 },
|
|
2207
|
-
{
|
|
2601
|
+
{
|
|
2602
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
2603
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
2604
|
+
}
|
|
2208
2605
|
);
|
|
2209
2606
|
}
|
|
2210
2607
|
if (input.kind === "contact_profile") {
|
|
2211
|
-
const contact = resolveContactLocator(db, input.contact, "contact");
|
|
2608
|
+
const contact = resolveContactLocator(db, input.contact, "contact", true);
|
|
2212
2609
|
if (!contact.ok) {
|
|
2213
2610
|
return contact.result;
|
|
2214
2611
|
}
|
|
2215
|
-
const detail2 = getContactProfile(
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
);
|
|
2612
|
+
const detail2 = getContactProfile(db, contact.value.id, {
|
|
2613
|
+
...input.topLinksLimit === void 0 ? {} : { topLinksLimit: input.topLinksLimit },
|
|
2614
|
+
...input.recentEventsLimit === void 0 ? {} : { recentEventsLimit: input.recentEventsLimit }
|
|
2615
|
+
});
|
|
2220
2616
|
if (!detail2) {
|
|
2221
2617
|
return toolFailure(
|
|
2222
2618
|
"domain",
|
|
@@ -2228,7 +2624,10 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
2228
2624
|
return toolSuccess(
|
|
2229
2625
|
`Loaded contact profile for ${detail2.contact.name}.`,
|
|
2230
2626
|
{ kind: input.kind, detail: detail2 },
|
|
2231
|
-
{
|
|
2627
|
+
{
|
|
2628
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
2629
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
2630
|
+
}
|
|
2232
2631
|
);
|
|
2233
2632
|
}
|
|
2234
2633
|
const link = resolveLinkLocator(db, input.link, "link");
|
|
@@ -2271,6 +2670,8 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2271
2670
|
init_tool_ref();
|
|
2272
2671
|
init_tool_resolvers();
|
|
2273
2672
|
init_tool_types();
|
|
2673
|
+
init_tool_types();
|
|
2674
|
+
init_validate_tool_input();
|
|
2274
2675
|
inspectAffinityItemTool = defineAffinityTool({
|
|
2275
2676
|
name: inspectAffinityItemToolName,
|
|
2276
2677
|
description: "Inspect one exact affinity item such as the owner profile, a contact profile, or one link detail.",
|
|
@@ -2762,25 +3163,18 @@ var init_unset_attribute = __esm({
|
|
|
2762
3163
|
}
|
|
2763
3164
|
});
|
|
2764
3165
|
|
|
2765
|
-
// src/tools/tool_summary.ts
|
|
2766
|
-
function summarizeCount(count, singular) {
|
|
2767
|
-
if (count === 0) {
|
|
2768
|
-
return `No ${singular}s found.`;
|
|
2769
|
-
}
|
|
2770
|
-
if (count === 1) {
|
|
2771
|
-
return `Found 1 ${singular}.`;
|
|
2772
|
-
}
|
|
2773
|
-
return `Found ${count} ${singular}s.`;
|
|
2774
|
-
}
|
|
2775
|
-
var init_tool_summary = __esm({
|
|
2776
|
-
"src/tools/tool_summary.ts"() {
|
|
2777
|
-
}
|
|
2778
|
-
});
|
|
2779
|
-
|
|
2780
3166
|
// src/tools/tool_mutation.ts
|
|
2781
|
-
function mutationToolResult(action, receipt, entitySummary) {
|
|
3167
|
+
function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
2782
3168
|
const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
|
|
2783
3169
|
const data = {
|
|
3170
|
+
changes: ["created", "updated", "archived", "removed"].flatMap(
|
|
3171
|
+
(verb) => receipt[verb].map((ref) => ({
|
|
3172
|
+
entity: ref.kind,
|
|
3173
|
+
id: ref.id,
|
|
3174
|
+
action: verb,
|
|
3175
|
+
...verb === "updated" && fields ? { fields } : {}
|
|
3176
|
+
}))
|
|
3177
|
+
),
|
|
2784
3178
|
action,
|
|
2785
3179
|
primary: receipt.primary,
|
|
2786
3180
|
created: receipt.created,
|
|
@@ -2790,7 +3184,7 @@ function mutationToolResult(action, receipt, entitySummary) {
|
|
|
2790
3184
|
affectedLinks: receipt.affectedLinks,
|
|
2791
3185
|
derivedEffects: receipt.derivedEffects
|
|
2792
3186
|
};
|
|
2793
|
-
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` :
|
|
3187
|
+
const summary = changedCount === 0 ? `No ${entitySummary} changes were needed.` : describeChanges(receipt);
|
|
2794
3188
|
if (changedCount === 0) {
|
|
2795
3189
|
return toolNoOp(summary, data, {
|
|
2796
3190
|
entities: toReceiptEntityRefs(receipt)
|
|
@@ -2800,11 +3194,28 @@ function mutationToolResult(action, receipt, entitySummary) {
|
|
|
2800
3194
|
entities: toReceiptEntityRefs(receipt)
|
|
2801
3195
|
});
|
|
2802
3196
|
}
|
|
3197
|
+
function describeChanges(receipt) {
|
|
3198
|
+
const parts = [];
|
|
3199
|
+
for (const verb of ["created", "updated", "archived", "removed"]) {
|
|
3200
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3201
|
+
for (const ref of receipt[verb]) {
|
|
3202
|
+
counts.set(ref.kind, (counts.get(ref.kind) ?? 0) + 1);
|
|
3203
|
+
}
|
|
3204
|
+
if (counts.size === 0) continue;
|
|
3205
|
+
const items = [...counts].map(
|
|
3206
|
+
([kind, n]) => `${n} ${n === 1 ? kind : PLURALS[kind] ?? `${kind}s`}`
|
|
3207
|
+
);
|
|
3208
|
+
parts.push(`${verb} ${items.join(", ")}`);
|
|
3209
|
+
}
|
|
3210
|
+
const text = parts.join("; ");
|
|
3211
|
+
return `${text.charAt(0).toUpperCase()}${text.slice(1)}.`;
|
|
3212
|
+
}
|
|
3213
|
+
var PLURALS;
|
|
2803
3214
|
var init_tool_mutation = __esm({
|
|
2804
3215
|
"src/tools/tool_mutation.ts"() {
|
|
2805
3216
|
init_tool_ref();
|
|
2806
|
-
init_tool_summary();
|
|
2807
3217
|
init_tool_types();
|
|
3218
|
+
PLURALS = { identity: "identities" };
|
|
2808
3219
|
}
|
|
2809
3220
|
});
|
|
2810
3221
|
|
|
@@ -2857,7 +3268,23 @@ function resolveAttributeTarget(db, target) {
|
|
|
2857
3268
|
}
|
|
2858
3269
|
return { kind: "ok", target: { kind: "link", id: link.value.id } };
|
|
2859
3270
|
}
|
|
2860
|
-
function manageAttributeToolHandler(db,
|
|
3271
|
+
function manageAttributeToolHandler(db, rawInput) {
|
|
3272
|
+
const input = structuredClone(rawInput);
|
|
3273
|
+
const inputError = validateToolInput(manageAttributeTool.inputSchema, input);
|
|
3274
|
+
if (inputError)
|
|
3275
|
+
return toolFailure(
|
|
3276
|
+
"protocol",
|
|
3277
|
+
"invalid_input",
|
|
3278
|
+
"Invalid input.",
|
|
3279
|
+
inputError
|
|
3280
|
+
);
|
|
3281
|
+
if (!input.target)
|
|
3282
|
+
return toolFailure(
|
|
3283
|
+
"protocol",
|
|
3284
|
+
"invalid_input",
|
|
3285
|
+
"Missing target.",
|
|
3286
|
+
"target is required."
|
|
3287
|
+
);
|
|
2861
3288
|
return withToolHandling(() => {
|
|
2862
3289
|
const resolved = resolveAttributeTarget(db, input.target);
|
|
2863
3290
|
if (resolved.kind === "error") {
|
|
@@ -2895,6 +3322,8 @@ var init_manage_attribute_tool = __esm({
|
|
|
2895
3322
|
init_tool_mutation();
|
|
2896
3323
|
init_tool_names();
|
|
2897
3324
|
init_tool_resolvers();
|
|
3325
|
+
init_tool_types();
|
|
3326
|
+
init_validate_tool_input();
|
|
2898
3327
|
manageAttributeTool = defineAffinityTool({
|
|
2899
3328
|
name: manageAttributeToolName,
|
|
2900
3329
|
description: "Set, unset, or replace custom metadata attributes on contacts and links. For emails, phones, handles, and URLs use manage_identity instead \u2014 those are routing keys, not attributes.",
|
|
@@ -3527,7 +3956,7 @@ function clamp3(value, min, max) {
|
|
|
3527
3956
|
}
|
|
3528
3957
|
function countSameDaySimilarEvents(db, linkId, eventType, occurredAt) {
|
|
3529
3958
|
const dayStart = utcStartOfDayMs(occurredAt);
|
|
3530
|
-
const dayEnd = dayStart +
|
|
3959
|
+
const dayEnd = dayStart + DAY_MS3;
|
|
3531
3960
|
const row = db.prepare(
|
|
3532
3961
|
`SELECT COUNT(*) AS c
|
|
3533
3962
|
FROM link_event_effects le
|
|
@@ -3540,7 +3969,7 @@ function countSameDaySimilarEvents(db, linkId, eventType, occurredAt) {
|
|
|
3540
3969
|
return Number(row.c ?? 0);
|
|
3541
3970
|
}
|
|
3542
3971
|
function weeklyPositiveBaseWeight(db, linkId, occurredAt) {
|
|
3543
|
-
const weekStart = occurredAt - 7 *
|
|
3972
|
+
const weekStart = occurredAt - 7 * DAY_MS3;
|
|
3544
3973
|
const row = db.prepare(
|
|
3545
3974
|
`SELECT SUM(base_weight) AS total
|
|
3546
3975
|
FROM link_event_effects
|
|
@@ -3552,7 +3981,7 @@ function weeklyPositiveBaseWeight(db, linkId, occurredAt) {
|
|
|
3552
3981
|
return Number(row.total ?? 0);
|
|
3553
3982
|
}
|
|
3554
3983
|
function countRepairContext(db, linkId, occurredAt) {
|
|
3555
|
-
const thirtyDaysAgo = occurredAt - 30 *
|
|
3984
|
+
const thirtyDaysAgo = occurredAt - 30 * DAY_MS3;
|
|
3556
3985
|
const hasPriorDamage = db.prepare(
|
|
3557
3986
|
`SELECT 1 FROM link_event_effects
|
|
3558
3987
|
WHERE link_id = ? AND trust_delta < 0
|
|
@@ -3627,8 +4056,8 @@ function computePreferenceMatch(db, linkId, contactId, eventType, provenance) {
|
|
|
3627
4056
|
return preferenceMatch(liked, disliked);
|
|
3628
4057
|
}
|
|
3629
4058
|
function computeDateSalienceMultiplier(db, contactId, occurredAt, significance) {
|
|
3630
|
-
const windowBefore = 7 *
|
|
3631
|
-
const windowAfter = 2 *
|
|
4059
|
+
const windowBefore = 7 * DAY_MS3;
|
|
4060
|
+
const windowAfter = 2 * DAY_MS3;
|
|
3632
4061
|
const row = db.prepare(
|
|
3633
4062
|
`SELECT 1 FROM upcoming_occurrences uo
|
|
3634
4063
|
JOIN events e ON e.id = uo.event_id
|
|
@@ -3795,9 +4224,9 @@ function applyEventEffectToLink(db, params) {
|
|
|
3795
4224
|
).get(params.linkId);
|
|
3796
4225
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
3797
4226
|
1,
|
|
3798
|
-
Math.round((params.occurredAt - latest.occurred_at) /
|
|
4227
|
+
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
3799
4228
|
);
|
|
3800
|
-
const cadenceAfter = nextCadenceDays({
|
|
4229
|
+
const cadenceAfter = commitmentResolution === "broken" || commitmentResolution === "cancelled" ? cadenceBefore : nextCadenceDays({
|
|
3801
4230
|
kind: linkKind,
|
|
3802
4231
|
currentCadenceDays: cadenceBefore,
|
|
3803
4232
|
gapDays
|
|
@@ -3860,6 +4289,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
3860
4289
|
params.now,
|
|
3861
4290
|
params.now
|
|
3862
4291
|
);
|
|
4292
|
+
if (commitmentResolution === "broken" || commitmentResolution === "cancelled") {
|
|
4293
|
+
db.prepare(
|
|
4294
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4295
|
+
).run(params.eventId, params.linkId);
|
|
4296
|
+
}
|
|
3863
4297
|
refreshLinkRollup(db, params.linkId, params.now);
|
|
3864
4298
|
return {
|
|
3865
4299
|
linkId: params.linkId,
|
|
@@ -3883,7 +4317,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3883
4317
|
momentKind
|
|
3884
4318
|
};
|
|
3885
4319
|
}
|
|
3886
|
-
var
|
|
4320
|
+
var DAY_MS3;
|
|
3887
4321
|
var init_effects = __esm({
|
|
3888
4322
|
"src/links/effects.ts"() {
|
|
3889
4323
|
init_calendar();
|
|
@@ -3893,7 +4327,7 @@ var init_effects = __esm({
|
|
|
3893
4327
|
init_queries3();
|
|
3894
4328
|
init_rollups2();
|
|
3895
4329
|
init_validators2();
|
|
3896
|
-
|
|
4330
|
+
DAY_MS3 = 864e5;
|
|
3897
4331
|
}
|
|
3898
4332
|
});
|
|
3899
4333
|
|
|
@@ -4274,12 +4708,16 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4274
4708
|
if (row.type !== "promise" && row.type !== "agreement") {
|
|
4275
4709
|
throw new AffinityInvariantError("not a commitment event");
|
|
4276
4710
|
}
|
|
4277
|
-
const open = db.prepare(
|
|
4711
|
+
const open = db.prepare(
|
|
4712
|
+
"SELECT resolved_at, resolution FROM open_commitments WHERE event_id = ?"
|
|
4713
|
+
).get(commitmentEventId);
|
|
4278
4714
|
if (!open) {
|
|
4279
4715
|
throw new AffinityNotFoundError("open commitment not found");
|
|
4280
4716
|
}
|
|
4281
4717
|
if (open.resolved_at !== null) {
|
|
4282
|
-
throw new
|
|
4718
|
+
throw new AffinityConflictError(
|
|
4719
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4720
|
+
);
|
|
4283
4721
|
}
|
|
4284
4722
|
const now = resolveNow(options?.now);
|
|
4285
4723
|
const originalParticipants = loadEventParticipantViews(
|
|
@@ -4292,7 +4730,9 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4292
4730
|
WHERE event_id = ? AND resolved_at IS NULL`
|
|
4293
4731
|
).run(resolution, now, now, commitmentEventId);
|
|
4294
4732
|
if (Number(result.changes ?? 0) === 0) {
|
|
4295
|
-
throw new
|
|
4733
|
+
throw new AffinityConflictError(
|
|
4734
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4735
|
+
);
|
|
4296
4736
|
}
|
|
4297
4737
|
const shouldCreateResolvingEvent = options?.resolvingEvent !== void 0 || resolution === "broken";
|
|
4298
4738
|
const created = [];
|
|
@@ -4362,9 +4802,9 @@ var init_resolve_commitment = __esm({
|
|
|
4362
4802
|
init_queries2();
|
|
4363
4803
|
init_receipts2();
|
|
4364
4804
|
init_validators();
|
|
4805
|
+
init_affinity_conflict_error();
|
|
4365
4806
|
init_affinity_invariant_error();
|
|
4366
4807
|
init_affinity_not_found_error();
|
|
4367
|
-
init_affinity_state_error();
|
|
4368
4808
|
init_affinity_validation_error();
|
|
4369
4809
|
init_resolve_now();
|
|
4370
4810
|
init_with_transaction();
|
|
@@ -4397,9 +4837,25 @@ function ensureOwnerParticipates(db, participants) {
|
|
|
4397
4837
|
function prepareParticipants(db, raw) {
|
|
4398
4838
|
return ensureOwnerParticipates(db, raw.map(normalizeParticipant));
|
|
4399
4839
|
}
|
|
4400
|
-
function manageCommitmentToolHandler(db,
|
|
4840
|
+
function manageCommitmentToolHandler(db, rawInput) {
|
|
4841
|
+
const input = structuredClone(rawInput);
|
|
4842
|
+
const inputError = validateToolInput(manageCommitmentTool.inputSchema, input);
|
|
4843
|
+
if (inputError)
|
|
4844
|
+
return toolFailure(
|
|
4845
|
+
"protocol",
|
|
4846
|
+
"invalid_input",
|
|
4847
|
+
"Invalid input.",
|
|
4848
|
+
inputError
|
|
4849
|
+
);
|
|
4401
4850
|
return withToolHandling(() => {
|
|
4402
4851
|
if (input.action === "record") {
|
|
4852
|
+
if (!input.input)
|
|
4853
|
+
return toolFailure(
|
|
4854
|
+
"protocol",
|
|
4855
|
+
"invalid_input",
|
|
4856
|
+
"Missing input.",
|
|
4857
|
+
"input is required for record."
|
|
4858
|
+
);
|
|
4403
4859
|
const patched = {
|
|
4404
4860
|
...input.input,
|
|
4405
4861
|
participants: prepareParticipants(
|
|
@@ -4435,6 +4891,8 @@ var init_manage_commitment_tool = __esm({
|
|
|
4435
4891
|
init_tool_mutation();
|
|
4436
4892
|
init_tool_names();
|
|
4437
4893
|
init_tool_resolvers();
|
|
4894
|
+
init_tool_types();
|
|
4895
|
+
init_validate_tool_input();
|
|
4438
4896
|
manageCommitmentTool = defineAffinityTool({
|
|
4439
4897
|
name: manageCommitmentToolName,
|
|
4440
4898
|
description: "Record or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.",
|
|
@@ -4476,7 +4934,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4476
4934
|
role: stringSchema("Participant role."),
|
|
4477
4935
|
directionality: stringSchema("Optional directionality.")
|
|
4478
4936
|
},
|
|
4479
|
-
["
|
|
4937
|
+
["role"]
|
|
4480
4938
|
),
|
|
4481
4939
|
"Participant list. Must be non-empty and include the owner contact."
|
|
4482
4940
|
),
|
|
@@ -4525,6 +4983,163 @@ var init_manage_commitment_tool = __esm({
|
|
|
4525
4983
|
}
|
|
4526
4984
|
});
|
|
4527
4985
|
|
|
4986
|
+
// src/contacts/delete_contact.ts
|
|
4987
|
+
function deleteContact(db, locatorId, options) {
|
|
4988
|
+
return withTransaction(db, () => {
|
|
4989
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
4990
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4991
|
+
if (contact.is_owner)
|
|
4992
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
4993
|
+
const tables = [
|
|
4994
|
+
"contacts",
|
|
4995
|
+
"identities",
|
|
4996
|
+
"attributes",
|
|
4997
|
+
"links",
|
|
4998
|
+
"events",
|
|
4999
|
+
"event_participants",
|
|
5000
|
+
"open_commitments",
|
|
5001
|
+
"contact_merges",
|
|
5002
|
+
"dismissed_duplicates",
|
|
5003
|
+
"link_event_effects",
|
|
5004
|
+
"upcoming_occurrences",
|
|
5005
|
+
"link_rollups",
|
|
5006
|
+
"contact_rollups"
|
|
5007
|
+
];
|
|
5008
|
+
const before = new Map(
|
|
5009
|
+
tables.map((table) => [
|
|
5010
|
+
table,
|
|
5011
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
5012
|
+
])
|
|
5013
|
+
);
|
|
5014
|
+
db.prepare(
|
|
5015
|
+
"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))"
|
|
5016
|
+
).run(locatorId, locatorId);
|
|
5017
|
+
db.prepare(
|
|
5018
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
5019
|
+
).run(locatorId, locatorId);
|
|
5020
|
+
db.prepare(
|
|
5021
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
5022
|
+
).run(locatorId, locatorId);
|
|
5023
|
+
db.prepare(
|
|
5024
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
5025
|
+
).run(locatorId);
|
|
5026
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
5027
|
+
const now = Date.now();
|
|
5028
|
+
const affectedLinks = db.prepare(
|
|
5029
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
5030
|
+
).all().map((row) => row.id);
|
|
5031
|
+
refreshAllBridgeScores(db, now);
|
|
5032
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
5033
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
5034
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
5035
|
+
const removed = tables.map((table) => {
|
|
5036
|
+
const remaining = new Set(
|
|
5037
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
5038
|
+
);
|
|
5039
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
5040
|
+
return { entity: table, count: ids.length, ids };
|
|
5041
|
+
}).filter((item) => item.count > 0);
|
|
5042
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
5043
|
+
});
|
|
5044
|
+
}
|
|
5045
|
+
var init_delete_contact = __esm({
|
|
5046
|
+
"src/contacts/delete_contact.ts"() {
|
|
5047
|
+
init_bridge_scores();
|
|
5048
|
+
init_affinity_not_found_error();
|
|
5049
|
+
init_affinity_state_error();
|
|
5050
|
+
init_affinity_validation_error();
|
|
5051
|
+
init_rollups2();
|
|
5052
|
+
init_with_transaction();
|
|
5053
|
+
init_rollups();
|
|
5054
|
+
}
|
|
5055
|
+
});
|
|
5056
|
+
|
|
5057
|
+
// src/contacts/export_contact.ts
|
|
5058
|
+
function exportContact(db, id) {
|
|
5059
|
+
const query = (sql, ...params) => db.prepare(sql).all(...params).map((row) => {
|
|
5060
|
+
const result = {};
|
|
5061
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5062
|
+
const name = key.replace(
|
|
5063
|
+
/_([a-z])/g,
|
|
5064
|
+
(_, letter) => letter.toUpperCase()
|
|
5065
|
+
);
|
|
5066
|
+
result[name] = value;
|
|
5067
|
+
if ((name.endsWith("At") || name === "dueAt") && typeof value === "number")
|
|
5068
|
+
result[`${name}Iso`] = new Date(value).toISOString();
|
|
5069
|
+
if (key.endsWith("_json") && typeof value === "string")
|
|
5070
|
+
result[name] = JSON.parse(value);
|
|
5071
|
+
}
|
|
5072
|
+
return result;
|
|
5073
|
+
});
|
|
5074
|
+
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
5075
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
5076
|
+
const events = query(
|
|
5077
|
+
"SELECT DISTINCT e.* FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? ORDER BY e.id",
|
|
5078
|
+
id
|
|
5079
|
+
).map((event) => ({
|
|
5080
|
+
...event,
|
|
5081
|
+
participants: query(
|
|
5082
|
+
"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",
|
|
5083
|
+
event.id
|
|
5084
|
+
)
|
|
5085
|
+
}));
|
|
5086
|
+
return {
|
|
5087
|
+
contact,
|
|
5088
|
+
identities: query(
|
|
5089
|
+
"SELECT * FROM identities WHERE contact_id = ? ORDER BY id",
|
|
5090
|
+
id
|
|
5091
|
+
),
|
|
5092
|
+
attributes: query(
|
|
5093
|
+
"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",
|
|
5094
|
+
id,
|
|
5095
|
+
id,
|
|
5096
|
+
id
|
|
5097
|
+
),
|
|
5098
|
+
links: query(
|
|
5099
|
+
"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",
|
|
5100
|
+
id,
|
|
5101
|
+
id
|
|
5102
|
+
),
|
|
5103
|
+
events,
|
|
5104
|
+
commitments: query(
|
|
5105
|
+
"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",
|
|
5106
|
+
id
|
|
5107
|
+
),
|
|
5108
|
+
dates: query(
|
|
5109
|
+
"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",
|
|
5110
|
+
id,
|
|
5111
|
+
id,
|
|
5112
|
+
id
|
|
5113
|
+
),
|
|
5114
|
+
mergeHistory: query(
|
|
5115
|
+
"SELECT * FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ? ORDER BY id",
|
|
5116
|
+
id,
|
|
5117
|
+
id
|
|
5118
|
+
)
|
|
5119
|
+
};
|
|
5120
|
+
}
|
|
5121
|
+
var init_export_contact = __esm({
|
|
5122
|
+
"src/contacts/export_contact.ts"() {
|
|
5123
|
+
init_affinity_not_found_error();
|
|
5124
|
+
}
|
|
5125
|
+
});
|
|
5126
|
+
|
|
5127
|
+
// src/contacts/export_affinity.ts
|
|
5128
|
+
function exportAffinity(db) {
|
|
5129
|
+
const now = Date.now();
|
|
5130
|
+
return {
|
|
5131
|
+
version: 1,
|
|
5132
|
+
exportedAt: now,
|
|
5133
|
+
exportedAtIso: new Date(now).toISOString(),
|
|
5134
|
+
contacts: db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id))
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
var init_export_affinity = __esm({
|
|
5138
|
+
"src/contacts/export_affinity.ts"() {
|
|
5139
|
+
init_export_contact();
|
|
5140
|
+
}
|
|
5141
|
+
});
|
|
5142
|
+
|
|
4528
5143
|
// src/contacts/receipts.ts
|
|
4529
5144
|
function buildContactMutationReceipt(primary, partial) {
|
|
4530
5145
|
const normalized = normalizeMutationReceiptPartial(partial);
|
|
@@ -4556,66 +5171,6 @@ var init_receipts4 = __esm({
|
|
|
4556
5171
|
}
|
|
4557
5172
|
});
|
|
4558
5173
|
|
|
4559
|
-
// src/contacts/create_contact.ts
|
|
4560
|
-
function createContact(db, input) {
|
|
4561
|
-
return withTransaction(db, () => {
|
|
4562
|
-
const name = input.name.trim();
|
|
4563
|
-
if (name.length === 0) {
|
|
4564
|
-
throw new AffinityValidationError("name must be non-empty");
|
|
4565
|
-
}
|
|
4566
|
-
if (!KINDS.has(input.kind)) {
|
|
4567
|
-
throw new AffinityValidationError("invalid contact kind");
|
|
4568
|
-
}
|
|
4569
|
-
const now = resolveNow(input.now);
|
|
4570
|
-
if (input.bootstrapOwner) {
|
|
4571
|
-
const existing = db.prepare(
|
|
4572
|
-
"SELECT id FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
4573
|
-
).get();
|
|
4574
|
-
if (existing !== void 0) {
|
|
4575
|
-
throw new AffinityConflictError("owner already exists");
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
4579
|
-
const result = db.prepare(
|
|
4580
|
-
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
4581
|
-
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
4582
|
-
).run(name, input.kind, isOwner, now, now);
|
|
4583
|
-
const id = Number(result.lastInsertRowid);
|
|
4584
|
-
const row = getContactRowById(db, id);
|
|
4585
|
-
if (!row) {
|
|
4586
|
-
throw new AffinityInvariantError("inserted contact row not found");
|
|
4587
|
-
}
|
|
4588
|
-
refreshContactRollup(db, id, now);
|
|
4589
|
-
const primary = mapContactRowToContactListItem(row);
|
|
4590
|
-
return buildContactMutationReceipt(primary, {
|
|
4591
|
-
created: [{ kind: "contact", id }]
|
|
4592
|
-
});
|
|
4593
|
-
});
|
|
4594
|
-
}
|
|
4595
|
-
var KINDS;
|
|
4596
|
-
var init_create_contact = __esm({
|
|
4597
|
-
"src/contacts/create_contact.ts"() {
|
|
4598
|
-
init_mappers4();
|
|
4599
|
-
init_queries();
|
|
4600
|
-
init_receipts4();
|
|
4601
|
-
init_rollups();
|
|
4602
|
-
init_affinity_conflict_error();
|
|
4603
|
-
init_affinity_invariant_error();
|
|
4604
|
-
init_affinity_validation_error();
|
|
4605
|
-
init_resolve_now();
|
|
4606
|
-
init_with_transaction();
|
|
4607
|
-
KINDS = /* @__PURE__ */ new Set([
|
|
4608
|
-
"human",
|
|
4609
|
-
"group",
|
|
4610
|
-
"company",
|
|
4611
|
-
"team",
|
|
4612
|
-
"pet",
|
|
4613
|
-
"service",
|
|
4614
|
-
"other"
|
|
4615
|
-
]);
|
|
4616
|
-
}
|
|
4617
|
-
});
|
|
4618
|
-
|
|
4619
5174
|
// src/contacts/revise_contact.ts
|
|
4620
5175
|
function reviseContact(db, contactId, patch) {
|
|
4621
5176
|
return withTransaction(db, () => {
|
|
@@ -4671,7 +5226,9 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
4671
5226
|
throw new AffinityStateError("merged contact is read-only");
|
|
4672
5227
|
}
|
|
4673
5228
|
if (to === "merged") {
|
|
4674
|
-
throw new AffinityInvariantError(
|
|
5229
|
+
throw new AffinityInvariantError(
|
|
5230
|
+
"use manage_contact merge (CLI: contact merge) to reach merged state"
|
|
5231
|
+
);
|
|
4675
5232
|
}
|
|
4676
5233
|
if (from === to) {
|
|
4677
5234
|
return;
|
|
@@ -4704,42 +5261,360 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
4704
5261
|
}
|
|
4705
5262
|
const row = getContactRowById(db, contactId);
|
|
4706
5263
|
if (!row) {
|
|
4707
|
-
throw new AffinityNotFoundError("contact not found");
|
|
4708
|
-
}
|
|
4709
|
-
const from = row.lifecycle_state;
|
|
4710
|
-
assertValidLifecycleTransition(from, lifecycleState);
|
|
4711
|
-
const now = resolveNow(options?.now);
|
|
4712
|
-
if (from === lifecycleState) {
|
|
4713
|
-
return buildContactMutationReceipt(mapContactRowToContactListItem(row), {
|
|
4714
|
-
updated: []
|
|
4715
|
-
});
|
|
5264
|
+
throw new AffinityNotFoundError("contact not found");
|
|
5265
|
+
}
|
|
5266
|
+
const from = row.lifecycle_state;
|
|
5267
|
+
assertValidLifecycleTransition(from, lifecycleState);
|
|
5268
|
+
const now = resolveNow(options?.now);
|
|
5269
|
+
if (from === lifecycleState) {
|
|
5270
|
+
return buildContactMutationReceipt(mapContactRowToContactListItem(row), {
|
|
5271
|
+
updated: []
|
|
5272
|
+
});
|
|
5273
|
+
}
|
|
5274
|
+
db.prepare(
|
|
5275
|
+
"UPDATE contacts SET lifecycle_state = ?, updated_at = ? WHERE id = ?"
|
|
5276
|
+
).run(lifecycleState, now, contactId);
|
|
5277
|
+
refreshContactRollup(db, contactId, now);
|
|
5278
|
+
const next = getContactRowById(db, contactId);
|
|
5279
|
+
if (!next) {
|
|
5280
|
+
throw new AffinityNotFoundError("contact not found");
|
|
5281
|
+
}
|
|
5282
|
+
return buildContactMutationReceipt(mapContactRowToContactListItem(next), {
|
|
5283
|
+
updated: [{ kind: "contact", id: contactId }]
|
|
5284
|
+
});
|
|
5285
|
+
});
|
|
5286
|
+
}
|
|
5287
|
+
var STATES2;
|
|
5288
|
+
var init_set_contact_lifecycle = __esm({
|
|
5289
|
+
"src/contacts/set_contact_lifecycle.ts"() {
|
|
5290
|
+
init_mappers4();
|
|
5291
|
+
init_queries();
|
|
5292
|
+
init_receipts4();
|
|
5293
|
+
init_rollups();
|
|
5294
|
+
init_validators4();
|
|
5295
|
+
init_affinity_not_found_error();
|
|
5296
|
+
init_affinity_validation_error();
|
|
5297
|
+
init_resolve_now();
|
|
5298
|
+
init_with_transaction();
|
|
5299
|
+
STATES2 = /* @__PURE__ */ new Set(["active", "dormant", "merged", "lost"]);
|
|
5300
|
+
}
|
|
5301
|
+
});
|
|
5302
|
+
|
|
5303
|
+
// src/contacts/create_contact.ts
|
|
5304
|
+
function createContact(db, input) {
|
|
5305
|
+
return withTransaction(db, () => {
|
|
5306
|
+
const name = input.name.trim();
|
|
5307
|
+
if (name.length === 0) {
|
|
5308
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
5309
|
+
}
|
|
5310
|
+
if (!KINDS.has(input.kind)) {
|
|
5311
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
5312
|
+
}
|
|
5313
|
+
if (input.isOwner !== void 0) {
|
|
5314
|
+
throw new AffinityValidationError(
|
|
5315
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
5316
|
+
);
|
|
5317
|
+
}
|
|
5318
|
+
const now = resolveNow(input.now);
|
|
5319
|
+
if (input.bootstrapOwner) {
|
|
5320
|
+
const existing = db.prepare(
|
|
5321
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
5322
|
+
).get();
|
|
5323
|
+
if (existing !== void 0) {
|
|
5324
|
+
throw new AffinityConflictError(
|
|
5325
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
5326
|
+
);
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
5330
|
+
const result = db.prepare(
|
|
5331
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
5332
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
5333
|
+
).run(name, input.kind, isOwner, now, now);
|
|
5334
|
+
const id = Number(result.lastInsertRowid);
|
|
5335
|
+
const row = getContactRowById(db, id);
|
|
5336
|
+
if (!row) {
|
|
5337
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
5338
|
+
}
|
|
5339
|
+
refreshContactRollup(db, id, now);
|
|
5340
|
+
const primary = mapContactRowToContactListItem(row);
|
|
5341
|
+
return buildContactMutationReceipt(primary, {
|
|
5342
|
+
created: [{ kind: "contact", id }]
|
|
5343
|
+
});
|
|
5344
|
+
});
|
|
5345
|
+
}
|
|
5346
|
+
var KINDS;
|
|
5347
|
+
var init_create_contact = __esm({
|
|
5348
|
+
"src/contacts/create_contact.ts"() {
|
|
5349
|
+
init_mappers4();
|
|
5350
|
+
init_queries();
|
|
5351
|
+
init_receipts4();
|
|
5352
|
+
init_rollups();
|
|
5353
|
+
init_affinity_conflict_error();
|
|
5354
|
+
init_affinity_invariant_error();
|
|
5355
|
+
init_affinity_validation_error();
|
|
5356
|
+
init_resolve_now();
|
|
5357
|
+
init_with_transaction();
|
|
5358
|
+
KINDS = /* @__PURE__ */ new Set([
|
|
5359
|
+
"human",
|
|
5360
|
+
"group",
|
|
5361
|
+
"company",
|
|
5362
|
+
"team",
|
|
5363
|
+
"pet",
|
|
5364
|
+
"service",
|
|
5365
|
+
"other"
|
|
5366
|
+
]);
|
|
5367
|
+
}
|
|
5368
|
+
});
|
|
5369
|
+
|
|
5370
|
+
// src/contacts/fold_search_text.ts
|
|
5371
|
+
function foldSearchText(value) {
|
|
5372
|
+
let folded = value.toLowerCase();
|
|
5373
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
5374
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
5375
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
5376
|
+
}
|
|
5377
|
+
var init_fold_search_text = __esm({
|
|
5378
|
+
"src/contacts/fold_search_text.ts"() {
|
|
5379
|
+
}
|
|
5380
|
+
});
|
|
5381
|
+
|
|
5382
|
+
// src/identities/queries.ts
|
|
5383
|
+
function getIdentityRowById(db, id) {
|
|
5384
|
+
const row = db.prepare(
|
|
5385
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5386
|
+
created_at, updated_at, removed_at
|
|
5387
|
+
FROM identities
|
|
5388
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
5389
|
+
).get(id);
|
|
5390
|
+
if (row === void 0) {
|
|
5391
|
+
return null;
|
|
5392
|
+
}
|
|
5393
|
+
return row;
|
|
5394
|
+
}
|
|
5395
|
+
var init_queries5 = __esm({
|
|
5396
|
+
"src/identities/queries.ts"() {
|
|
5397
|
+
}
|
|
5398
|
+
});
|
|
5399
|
+
|
|
5400
|
+
// src/identities/receipts.ts
|
|
5401
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
5402
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5403
|
+
return {
|
|
5404
|
+
primary,
|
|
5405
|
+
created: normalized.created,
|
|
5406
|
+
updated: normalized.updated,
|
|
5407
|
+
archived: normalized.archived,
|
|
5408
|
+
removed: normalized.removed,
|
|
5409
|
+
affectedLinks: normalized.affectedLinks,
|
|
5410
|
+
derivedEffects: normalized.derivedEffects
|
|
5411
|
+
};
|
|
5412
|
+
}
|
|
5413
|
+
var init_receipts5 = __esm({
|
|
5414
|
+
"src/identities/receipts.ts"() {
|
|
5415
|
+
init_normalize_mutation_receipt();
|
|
5416
|
+
}
|
|
5417
|
+
});
|
|
5418
|
+
|
|
5419
|
+
// src/identities/validators.ts
|
|
5420
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5421
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5422
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5423
|
+
).get(normalizedKey) : db.prepare(
|
|
5424
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
5425
|
+
).get(normalizedKey, excludeIdentityId);
|
|
5426
|
+
if (row !== void 0) {
|
|
5427
|
+
const owner = db.prepare(
|
|
5428
|
+
"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"
|
|
5429
|
+
).get(normalizedKey);
|
|
5430
|
+
throw new AffinityConflictError(
|
|
5431
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
5432
|
+
);
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
var init_validators5 = __esm({
|
|
5436
|
+
"src/identities/validators.ts"() {
|
|
5437
|
+
init_affinity_conflict_error();
|
|
5438
|
+
}
|
|
5439
|
+
});
|
|
5440
|
+
|
|
5441
|
+
// src/identities/validate_identity_input.ts
|
|
5442
|
+
function validateIdentityInput(type, value) {
|
|
5443
|
+
const kind = type.toLowerCase();
|
|
5444
|
+
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
5445
|
+
throw new AffinityValidationError(
|
|
5446
|
+
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
5447
|
+
);
|
|
5448
|
+
if (!value.trim())
|
|
5449
|
+
throw new AffinityValidationError("identity value must be non-empty");
|
|
5450
|
+
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
5451
|
+
throw new AffinityValidationError(
|
|
5452
|
+
"email value must match local@domain.tld"
|
|
5453
|
+
);
|
|
5454
|
+
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
5455
|
+
throw new AffinityValidationError(
|
|
5456
|
+
"phone value must contain at least 5 digits"
|
|
5457
|
+
);
|
|
5458
|
+
if (kind === "url" || kind === "website") {
|
|
5459
|
+
try {
|
|
5460
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
5461
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
5462
|
+
throw new Error();
|
|
5463
|
+
} catch {
|
|
5464
|
+
throw new AffinityValidationError(
|
|
5465
|
+
"url/website value must be an http/https URL or bare domain"
|
|
5466
|
+
);
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
var identityTypes;
|
|
5471
|
+
var init_validate_identity_input = __esm({
|
|
5472
|
+
"src/identities/validate_identity_input.ts"() {
|
|
5473
|
+
init_affinity_validation_error();
|
|
5474
|
+
identityTypes = [
|
|
5475
|
+
"email",
|
|
5476
|
+
"phone",
|
|
5477
|
+
"url",
|
|
5478
|
+
"website",
|
|
5479
|
+
"handle",
|
|
5480
|
+
"x",
|
|
5481
|
+
"twitter",
|
|
5482
|
+
"linkedin",
|
|
5483
|
+
"github",
|
|
5484
|
+
"mastodon",
|
|
5485
|
+
"bluesky",
|
|
5486
|
+
"telegram",
|
|
5487
|
+
"whatsapp",
|
|
5488
|
+
"signal",
|
|
5489
|
+
"matrix",
|
|
5490
|
+
"discord",
|
|
5491
|
+
"slack",
|
|
5492
|
+
"instagram",
|
|
5493
|
+
"facebook",
|
|
5494
|
+
"youtube",
|
|
5495
|
+
"tiktok",
|
|
5496
|
+
"address",
|
|
5497
|
+
"other",
|
|
5498
|
+
"account_id",
|
|
5499
|
+
"alias"
|
|
5500
|
+
];
|
|
5501
|
+
}
|
|
5502
|
+
});
|
|
5503
|
+
|
|
5504
|
+
// src/identities/add_identity.ts
|
|
5505
|
+
function addIdentity(db, contactId, input) {
|
|
5506
|
+
return withTransaction(db, () => {
|
|
5507
|
+
const contact = getContactRowById(db, contactId);
|
|
5508
|
+
if (!contact) {
|
|
5509
|
+
throw new AffinityNotFoundError("contact not found");
|
|
5510
|
+
}
|
|
5511
|
+
if (contact.lifecycle_state === "merged") {
|
|
5512
|
+
throw new AffinityStateError("merged contact is read-only");
|
|
5513
|
+
}
|
|
5514
|
+
const type = input.type.trim();
|
|
5515
|
+
const value = input.value.trim();
|
|
5516
|
+
if (type.length === 0 || value.length === 0) {
|
|
5517
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
5518
|
+
}
|
|
5519
|
+
validateIdentityInput(type, value);
|
|
5520
|
+
const nk = normalizeIdentityKey(type, value);
|
|
5521
|
+
assertNoIdentityCollision(db, nk);
|
|
5522
|
+
const now = resolveNow(input.now);
|
|
5523
|
+
const verified = input.verified ? 1 : 0;
|
|
5524
|
+
const verifiedAt = verified === 1 ? now : null;
|
|
5525
|
+
const result = db.prepare(
|
|
5526
|
+
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
5527
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5528
|
+
).run(
|
|
5529
|
+
contactId,
|
|
5530
|
+
type,
|
|
5531
|
+
value,
|
|
5532
|
+
input.label ?? null,
|
|
5533
|
+
nk,
|
|
5534
|
+
verified,
|
|
5535
|
+
verifiedAt,
|
|
5536
|
+
now,
|
|
5537
|
+
now
|
|
5538
|
+
);
|
|
5539
|
+
const id = Number(result.lastInsertRowid);
|
|
5540
|
+
const row = getIdentityRowById(db, id);
|
|
5541
|
+
if (!row) {
|
|
5542
|
+
throw new AffinityInvariantError("inserted identity not found");
|
|
4716
5543
|
}
|
|
4717
|
-
db.prepare(
|
|
4718
|
-
"UPDATE contacts SET lifecycle_state = ?, updated_at = ? WHERE id = ?"
|
|
4719
|
-
).run(lifecycleState, now, contactId);
|
|
4720
5544
|
refreshContactRollup(db, contactId, now);
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
updated: [{ kind: "contact", id: contactId }]
|
|
5545
|
+
db.prepare(
|
|
5546
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
5547
|
+
).run(contactId, contactId);
|
|
5548
|
+
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
5549
|
+
created: [{ kind: "identity", id }]
|
|
4727
5550
|
});
|
|
4728
5551
|
});
|
|
4729
5552
|
}
|
|
4730
|
-
var
|
|
4731
|
-
|
|
4732
|
-
"src/contacts/set_contact_lifecycle.ts"() {
|
|
4733
|
-
init_mappers4();
|
|
5553
|
+
var init_add_identity = __esm({
|
|
5554
|
+
"src/identities/add_identity.ts"() {
|
|
4734
5555
|
init_queries();
|
|
4735
|
-
init_receipts4();
|
|
4736
5556
|
init_rollups();
|
|
4737
|
-
|
|
5557
|
+
init_mappers2();
|
|
5558
|
+
init_normalize();
|
|
5559
|
+
init_queries5();
|
|
5560
|
+
init_receipts5();
|
|
5561
|
+
init_validators5();
|
|
5562
|
+
init_affinity_invariant_error();
|
|
4738
5563
|
init_affinity_not_found_error();
|
|
5564
|
+
init_affinity_state_error();
|
|
4739
5565
|
init_affinity_validation_error();
|
|
4740
5566
|
init_resolve_now();
|
|
4741
5567
|
init_with_transaction();
|
|
4742
|
-
|
|
5568
|
+
init_validate_identity_input();
|
|
5569
|
+
}
|
|
5570
|
+
});
|
|
5571
|
+
|
|
5572
|
+
// src/tools/create_contact_tool_result.ts
|
|
5573
|
+
function createContactToolResult(db, input) {
|
|
5574
|
+
return withTransaction(db, () => {
|
|
5575
|
+
const receipt = createContact(db, input);
|
|
5576
|
+
const identityOwners = /* @__PURE__ */ new Set();
|
|
5577
|
+
for (const identity of input.identities ?? []) {
|
|
5578
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
5579
|
+
const existing = db.prepare(
|
|
5580
|
+
"SELECT contact_id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5581
|
+
).get(normalizeIdentityKey(identity.type, identity.value));
|
|
5582
|
+
if (existing) {
|
|
5583
|
+
if (Number(existing.contact_id) !== receipt.primary.id)
|
|
5584
|
+
identityOwners.add(Number(existing.contact_id));
|
|
5585
|
+
continue;
|
|
5586
|
+
}
|
|
5587
|
+
const added = addIdentity(db, receipt.primary.id, identity);
|
|
5588
|
+
receipt.created.push(...added.created);
|
|
5589
|
+
}
|
|
5590
|
+
const result = mutationToolResult("create", receipt, "contact");
|
|
5591
|
+
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
5592
|
+
const warnings = db.prepare(
|
|
5593
|
+
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
5594
|
+
).all(receipt.primary.id).flatMap((row) => {
|
|
5595
|
+
const score = identityOwners.has(row.id) ? 1 : fold(row.name) === fold(input.name) ? 0.92 : void 0;
|
|
5596
|
+
return score === void 0 ? [] : [
|
|
5597
|
+
{
|
|
5598
|
+
code: "possible_duplicate",
|
|
5599
|
+
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
5600
|
+
contactId: row.id,
|
|
5601
|
+
score
|
|
5602
|
+
}
|
|
5603
|
+
];
|
|
5604
|
+
});
|
|
5605
|
+
if (warnings.length) result.warnings = warnings;
|
|
5606
|
+
return result;
|
|
5607
|
+
});
|
|
5608
|
+
}
|
|
5609
|
+
var init_create_contact_tool_result = __esm({
|
|
5610
|
+
"src/tools/create_contact_tool_result.ts"() {
|
|
5611
|
+
init_create_contact();
|
|
5612
|
+
init_fold_search_text();
|
|
5613
|
+
init_add_identity();
|
|
5614
|
+
init_normalize();
|
|
5615
|
+
init_validate_identity_input();
|
|
5616
|
+
init_with_transaction();
|
|
5617
|
+
init_tool_mutation();
|
|
4743
5618
|
}
|
|
4744
5619
|
});
|
|
4745
5620
|
|
|
@@ -4761,24 +5636,76 @@ function contactLocatorSchema3(description) {
|
|
|
4761
5636
|
description
|
|
4762
5637
|
};
|
|
4763
5638
|
}
|
|
4764
|
-
function manageContactToolHandler(db,
|
|
5639
|
+
function manageContactToolHandler(db, rawInput) {
|
|
5640
|
+
const input = structuredClone(rawInput);
|
|
5641
|
+
const inputError = validateToolInput(manageContactTool.inputSchema, input);
|
|
5642
|
+
if (inputError)
|
|
5643
|
+
return toolFailure(
|
|
5644
|
+
"protocol",
|
|
5645
|
+
"invalid_input",
|
|
5646
|
+
"Invalid input.",
|
|
5647
|
+
inputError
|
|
5648
|
+
);
|
|
4765
5649
|
return withToolHandling(() => {
|
|
4766
|
-
if (input.action === "
|
|
4767
|
-
|
|
4768
|
-
"
|
|
4769
|
-
|
|
4770
|
-
|
|
5650
|
+
if (input.action === "export") {
|
|
5651
|
+
if (!input.contact)
|
|
5652
|
+
return toolSuccess("Exported Affinity.", {
|
|
5653
|
+
action: "export",
|
|
5654
|
+
export: exportAffinity(db)
|
|
5655
|
+
});
|
|
5656
|
+
const target = resolveContactLocator(db, input.contact, "contact", true);
|
|
5657
|
+
if (!target.ok) return target.result;
|
|
5658
|
+
return toolSuccess("Exported contact.", {
|
|
5659
|
+
action: "export",
|
|
5660
|
+
export: exportContact(db, target.value.id)
|
|
5661
|
+
});
|
|
5662
|
+
}
|
|
5663
|
+
if (input.action === "delete" && input.confirm !== true)
|
|
5664
|
+
return toolFailure(
|
|
5665
|
+
"protocol",
|
|
5666
|
+
"invalid_input",
|
|
5667
|
+
"Delete requires confirmation.",
|
|
5668
|
+
"delete is permanent; pass confirm: true (CLI: --confirm)"
|
|
4771
5669
|
);
|
|
5670
|
+
if (input.action === "create") {
|
|
5671
|
+
if (!input.input)
|
|
5672
|
+
return toolFailure(
|
|
5673
|
+
"protocol",
|
|
5674
|
+
"invalid_input",
|
|
5675
|
+
"Missing input.",
|
|
5676
|
+
"input is required for create."
|
|
5677
|
+
);
|
|
5678
|
+
return createContactToolResult(db, input.input);
|
|
4772
5679
|
}
|
|
4773
|
-
const contact = resolveContactLocator(
|
|
5680
|
+
const contact = resolveContactLocator(
|
|
5681
|
+
db,
|
|
5682
|
+
input.contact,
|
|
5683
|
+
"contact",
|
|
5684
|
+
input.action === "delete"
|
|
5685
|
+
);
|
|
4774
5686
|
if (!contact.ok) {
|
|
4775
5687
|
return contact.result;
|
|
4776
5688
|
}
|
|
5689
|
+
if (input.action === "delete") {
|
|
5690
|
+
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
5691
|
+
return toolSuccess("Permanently deleted contact.", {
|
|
5692
|
+
action: "delete",
|
|
5693
|
+
deletion,
|
|
5694
|
+
changes: deletion.removed.flatMap(
|
|
5695
|
+
(item) => item.ids.map((id) => ({
|
|
5696
|
+
entity: item.entity,
|
|
5697
|
+
id,
|
|
5698
|
+
action: "removed"
|
|
5699
|
+
}))
|
|
5700
|
+
)
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
4777
5703
|
if (input.action === "revise") {
|
|
4778
5704
|
return mutationToolResult(
|
|
4779
5705
|
"revise",
|
|
4780
5706
|
reviseContact(db, contact.value.id, input.patch),
|
|
4781
|
-
"contact"
|
|
5707
|
+
"contact",
|
|
5708
|
+
Object.keys(input.patch)
|
|
4782
5709
|
);
|
|
4783
5710
|
}
|
|
4784
5711
|
return mutationToolResult(
|
|
@@ -4796,16 +5723,22 @@ function manageContactToolHandler(db, input) {
|
|
|
4796
5723
|
var manageContactTool;
|
|
4797
5724
|
var init_manage_contact_tool = __esm({
|
|
4798
5725
|
"src/tools/manage_contact_tool.ts"() {
|
|
4799
|
-
|
|
5726
|
+
init_delete_contact();
|
|
5727
|
+
init_export_affinity();
|
|
5728
|
+
init_export_contact();
|
|
4800
5729
|
init_revise_contact();
|
|
4801
5730
|
init_set_contact_lifecycle();
|
|
5731
|
+
init_create_contact_tool_result();
|
|
4802
5732
|
init_tool_metadata();
|
|
4803
5733
|
init_tool_mutation();
|
|
4804
5734
|
init_tool_names();
|
|
4805
5735
|
init_tool_resolvers();
|
|
5736
|
+
init_tool_types();
|
|
5737
|
+
init_tool_types();
|
|
5738
|
+
init_validate_tool_input();
|
|
4806
5739
|
manageContactTool = defineAffinityTool({
|
|
4807
5740
|
name: manageContactToolName,
|
|
4808
|
-
description: "Create a contact
|
|
5741
|
+
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.",
|
|
4809
5742
|
whenToUse: "Use this for contact creation and lifecycle-safe contact maintenance.",
|
|
4810
5743
|
whenNotToUse: "Do not use this for identities, links, attributes, or merges.",
|
|
4811
5744
|
sideEffects: "writes_state",
|
|
@@ -4826,10 +5759,25 @@ var init_manage_contact_tool = __esm({
|
|
|
4826
5759
|
action: enumSchema("Operation to perform.", [
|
|
4827
5760
|
"create",
|
|
4828
5761
|
"revise",
|
|
4829
|
-
"set_lifecycle"
|
|
5762
|
+
"set_lifecycle",
|
|
5763
|
+
"export",
|
|
5764
|
+
"delete"
|
|
4830
5765
|
]),
|
|
4831
5766
|
input: objectSchema(
|
|
4832
5767
|
{
|
|
5768
|
+
identities: arraySchema(
|
|
5769
|
+
objectSchema(
|
|
5770
|
+
{
|
|
5771
|
+
type: stringSchema("Identity type."),
|
|
5772
|
+
value: stringSchema("Identity value."),
|
|
5773
|
+
label: stringSchema("Optional label."),
|
|
5774
|
+
verified: booleanSchema("Verified identity."),
|
|
5775
|
+
now: integerSchema("Optional timestamp.")
|
|
5776
|
+
},
|
|
5777
|
+
["type", "value"]
|
|
5778
|
+
),
|
|
5779
|
+
"Optional identities to attach. Identities already owned by another contact are left with that owner and reported as possible duplicates."
|
|
5780
|
+
),
|
|
4833
5781
|
name: stringSchema("Contact name."),
|
|
4834
5782
|
kind: enumSchema("Contact kind.", [
|
|
4835
5783
|
"human",
|
|
@@ -4848,6 +5796,7 @@ var init_manage_contact_tool = __esm({
|
|
|
4848
5796
|
["name", "kind"],
|
|
4849
5797
|
"New contact payload. Required when action=create."
|
|
4850
5798
|
),
|
|
5799
|
+
confirm: booleanSchema("Required true for permanent deletion."),
|
|
4851
5800
|
contact: contactLocatorSchema3(
|
|
4852
5801
|
"Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity."
|
|
4853
5802
|
),
|
|
@@ -5117,9 +6066,32 @@ function resolveDateAnchorTarget(db, target) {
|
|
|
5117
6066
|
}
|
|
5118
6067
|
return { kind: "ok", target: { kind: "link", linkId: link.value.id } };
|
|
5119
6068
|
}
|
|
5120
|
-
function manageDateAnchorToolHandler(db,
|
|
6069
|
+
function manageDateAnchorToolHandler(db, rawInput) {
|
|
6070
|
+
const input = structuredClone(rawInput);
|
|
6071
|
+
const inputError = validateToolInput(manageDateAnchorTool.inputSchema, input);
|
|
6072
|
+
if (inputError)
|
|
6073
|
+
return toolFailure(
|
|
6074
|
+
"protocol",
|
|
6075
|
+
"invalid_input",
|
|
6076
|
+
"Invalid input.",
|
|
6077
|
+
inputError
|
|
6078
|
+
);
|
|
6079
|
+
if (input.action !== "add" && input.anchorEventId === void 0)
|
|
6080
|
+
return toolFailure(
|
|
6081
|
+
"protocol",
|
|
6082
|
+
"invalid_input",
|
|
6083
|
+
"Missing anchorEventId.",
|
|
6084
|
+
"anchorEventId is required."
|
|
6085
|
+
);
|
|
5121
6086
|
return withToolHandling(() => {
|
|
5122
6087
|
if (input.action === "add") {
|
|
6088
|
+
if (!input.input)
|
|
6089
|
+
return toolFailure(
|
|
6090
|
+
"protocol",
|
|
6091
|
+
"invalid_input",
|
|
6092
|
+
"Missing input.",
|
|
6093
|
+
"input is required for add."
|
|
6094
|
+
);
|
|
5123
6095
|
const resolved = resolveDateAnchorTarget(db, input.input.target);
|
|
5124
6096
|
if (resolved.kind === "error") {
|
|
5125
6097
|
return resolved.result;
|
|
@@ -5134,7 +6106,8 @@ function manageDateAnchorToolHandler(db, input) {
|
|
|
5134
6106
|
return mutationToolResult(
|
|
5135
6107
|
"revise",
|
|
5136
6108
|
reviseDateAnchor(db, input.anchorEventId, input.patch, input.options),
|
|
5137
|
-
"date anchor"
|
|
6109
|
+
"date anchor",
|
|
6110
|
+
Object.keys(input.patch)
|
|
5138
6111
|
);
|
|
5139
6112
|
}
|
|
5140
6113
|
return mutationToolResult(
|
|
@@ -5154,6 +6127,8 @@ var init_manage_date_anchor_tool = __esm({
|
|
|
5154
6127
|
init_tool_mutation();
|
|
5155
6128
|
init_tool_names();
|
|
5156
6129
|
init_tool_resolvers();
|
|
6130
|
+
init_tool_types();
|
|
6131
|
+
init_validate_tool_input();
|
|
5157
6132
|
manageDateAnchorTool = defineAffinityTool({
|
|
5158
6133
|
name: manageDateAnchorToolName,
|
|
5159
6134
|
description: "Add, revise, or remove recurring date anchors on contacts or links. Use this for birthdays, anniversaries, and yearly events \u2014 not manage_attribute.",
|
|
@@ -5267,7 +6242,7 @@ function buildDismissalMutationReceipt(primary, partial) {
|
|
|
5267
6242
|
derivedEffects: normalized.derivedEffects
|
|
5268
6243
|
};
|
|
5269
6244
|
}
|
|
5270
|
-
var
|
|
6245
|
+
var init_receipts6 = __esm({
|
|
5271
6246
|
"src/merges/receipts.ts"() {
|
|
5272
6247
|
init_normalize_mutation_receipt();
|
|
5273
6248
|
}
|
|
@@ -5329,7 +6304,7 @@ var init_dismiss_duplicate = __esm({
|
|
|
5329
6304
|
init_affinity_validation_error();
|
|
5330
6305
|
init_resolve_now();
|
|
5331
6306
|
init_with_transaction();
|
|
5332
|
-
|
|
6307
|
+
init_receipts6();
|
|
5333
6308
|
}
|
|
5334
6309
|
});
|
|
5335
6310
|
|
|
@@ -5368,7 +6343,7 @@ var init_undismiss_duplicate = __esm({
|
|
|
5368
6343
|
init_affinity_not_found_error();
|
|
5369
6344
|
init_affinity_validation_error();
|
|
5370
6345
|
init_with_transaction();
|
|
5371
|
-
|
|
6346
|
+
init_receipts6();
|
|
5372
6347
|
}
|
|
5373
6348
|
});
|
|
5374
6349
|
|
|
@@ -5390,7 +6365,19 @@ function contactLocatorSchema5(description) {
|
|
|
5390
6365
|
description
|
|
5391
6366
|
};
|
|
5392
6367
|
}
|
|
5393
|
-
function manageDuplicateDismissalToolHandler(db,
|
|
6368
|
+
function manageDuplicateDismissalToolHandler(db, rawInput) {
|
|
6369
|
+
const input = structuredClone(rawInput);
|
|
6370
|
+
const inputError = validateToolInput(
|
|
6371
|
+
manageDuplicateDismissalTool.inputSchema,
|
|
6372
|
+
input
|
|
6373
|
+
);
|
|
6374
|
+
if (inputError)
|
|
6375
|
+
return toolFailure(
|
|
6376
|
+
"protocol",
|
|
6377
|
+
"invalid_input",
|
|
6378
|
+
"Invalid input.",
|
|
6379
|
+
inputError
|
|
6380
|
+
);
|
|
5394
6381
|
return withToolHandling(() => {
|
|
5395
6382
|
const left = resolveContactLocator(db, input.left, "left");
|
|
5396
6383
|
if (!left.ok) {
|
|
@@ -5400,6 +6387,11 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
5400
6387
|
if (!right.ok) {
|
|
5401
6388
|
return right.result;
|
|
5402
6389
|
}
|
|
6390
|
+
const leftContactId = Math.min(left.value.id, right.value.id);
|
|
6391
|
+
const rightContactId = Math.max(left.value.id, right.value.id);
|
|
6392
|
+
const existed = db.prepare(
|
|
6393
|
+
"SELECT 1 FROM dismissed_duplicates WHERE left_id = ? AND right_id = ?"
|
|
6394
|
+
).get(leftContactId, rightContactId) !== void 0;
|
|
5403
6395
|
if (input.action === "dismiss") {
|
|
5404
6396
|
const receipt2 = dismissDuplicate(
|
|
5405
6397
|
db,
|
|
@@ -5409,13 +6401,35 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
5409
6401
|
);
|
|
5410
6402
|
return toolSuccess(
|
|
5411
6403
|
`Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
|
|
5412
|
-
{
|
|
6404
|
+
{
|
|
6405
|
+
...receipt2,
|
|
6406
|
+
action: "dismiss",
|
|
6407
|
+
changes: [
|
|
6408
|
+
{
|
|
6409
|
+
entity: "duplicate_dismissal",
|
|
6410
|
+
leftContactId,
|
|
6411
|
+
rightContactId,
|
|
6412
|
+
action: existed ? "updated" : "created"
|
|
6413
|
+
}
|
|
6414
|
+
]
|
|
6415
|
+
}
|
|
5413
6416
|
);
|
|
5414
6417
|
}
|
|
5415
6418
|
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
5416
6419
|
return toolSuccess(
|
|
5417
6420
|
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
5418
|
-
{
|
|
6421
|
+
{
|
|
6422
|
+
...receipt,
|
|
6423
|
+
action: "undismiss",
|
|
6424
|
+
changes: existed ? [
|
|
6425
|
+
{
|
|
6426
|
+
entity: "duplicate_dismissal",
|
|
6427
|
+
leftContactId,
|
|
6428
|
+
rightContactId,
|
|
6429
|
+
action: "removed"
|
|
6430
|
+
}
|
|
6431
|
+
] : []
|
|
6432
|
+
}
|
|
5419
6433
|
);
|
|
5420
6434
|
}, "Duplicate dismissal failed.");
|
|
5421
6435
|
}
|
|
@@ -5428,9 +6442,11 @@ var init_manage_duplicate_dismissal_tool = __esm({
|
|
|
5428
6442
|
init_tool_names();
|
|
5429
6443
|
init_tool_resolvers();
|
|
5430
6444
|
init_tool_types();
|
|
6445
|
+
init_tool_types();
|
|
6446
|
+
init_validate_tool_input();
|
|
5431
6447
|
manageDuplicateDismissalTool = defineAffinityTool({
|
|
5432
6448
|
name: manageDuplicateDismissalToolName,
|
|
5433
|
-
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in
|
|
6449
|
+
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.",
|
|
5434
6450
|
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
5435
6451
|
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
5436
6452
|
sideEffects: "writes_state",
|
|
@@ -5463,128 +6479,6 @@ var init_manage_duplicate_dismissal_tool = __esm({
|
|
|
5463
6479
|
}
|
|
5464
6480
|
});
|
|
5465
6481
|
|
|
5466
|
-
// src/identities/queries.ts
|
|
5467
|
-
function getIdentityRowById(db, id) {
|
|
5468
|
-
const row = db.prepare(
|
|
5469
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5470
|
-
created_at, updated_at, removed_at
|
|
5471
|
-
FROM identities
|
|
5472
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
5473
|
-
).get(id);
|
|
5474
|
-
if (row === void 0) {
|
|
5475
|
-
return null;
|
|
5476
|
-
}
|
|
5477
|
-
return row;
|
|
5478
|
-
}
|
|
5479
|
-
var init_queries5 = __esm({
|
|
5480
|
-
"src/identities/queries.ts"() {
|
|
5481
|
-
}
|
|
5482
|
-
});
|
|
5483
|
-
|
|
5484
|
-
// src/identities/receipts.ts
|
|
5485
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
5486
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5487
|
-
return {
|
|
5488
|
-
primary,
|
|
5489
|
-
created: normalized.created,
|
|
5490
|
-
updated: normalized.updated,
|
|
5491
|
-
archived: normalized.archived,
|
|
5492
|
-
removed: normalized.removed,
|
|
5493
|
-
affectedLinks: normalized.affectedLinks,
|
|
5494
|
-
derivedEffects: normalized.derivedEffects
|
|
5495
|
-
};
|
|
5496
|
-
}
|
|
5497
|
-
var init_receipts6 = __esm({
|
|
5498
|
-
"src/identities/receipts.ts"() {
|
|
5499
|
-
init_normalize_mutation_receipt();
|
|
5500
|
-
}
|
|
5501
|
-
});
|
|
5502
|
-
|
|
5503
|
-
// src/identities/validators.ts
|
|
5504
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5505
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5506
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5507
|
-
).get(normalizedKey) : db.prepare(
|
|
5508
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
5509
|
-
).get(normalizedKey, excludeIdentityId);
|
|
5510
|
-
if (row !== void 0) {
|
|
5511
|
-
throw new AffinityConflictError(
|
|
5512
|
-
"another contact already owns this normalized identity"
|
|
5513
|
-
);
|
|
5514
|
-
}
|
|
5515
|
-
}
|
|
5516
|
-
var init_validators5 = __esm({
|
|
5517
|
-
"src/identities/validators.ts"() {
|
|
5518
|
-
init_affinity_conflict_error();
|
|
5519
|
-
}
|
|
5520
|
-
});
|
|
5521
|
-
|
|
5522
|
-
// src/identities/add_identity.ts
|
|
5523
|
-
function addIdentity(db, contactId, input) {
|
|
5524
|
-
return withTransaction(db, () => {
|
|
5525
|
-
const contact = getContactRowById(db, contactId);
|
|
5526
|
-
if (!contact) {
|
|
5527
|
-
throw new AffinityNotFoundError("contact not found");
|
|
5528
|
-
}
|
|
5529
|
-
if (contact.lifecycle_state === "merged") {
|
|
5530
|
-
throw new AffinityStateError("merged contact is read-only");
|
|
5531
|
-
}
|
|
5532
|
-
const type = input.type.trim();
|
|
5533
|
-
const value = input.value.trim();
|
|
5534
|
-
if (type.length === 0 || value.length === 0) {
|
|
5535
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
5536
|
-
}
|
|
5537
|
-
const nk = normalizeIdentityKey(type, value);
|
|
5538
|
-
assertNoIdentityCollision(db, nk);
|
|
5539
|
-
const now = resolveNow(input.now);
|
|
5540
|
-
const verified = input.verified ? 1 : 0;
|
|
5541
|
-
const verifiedAt = verified === 1 ? now : null;
|
|
5542
|
-
const result = db.prepare(
|
|
5543
|
-
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
5544
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5545
|
-
).run(
|
|
5546
|
-
contactId,
|
|
5547
|
-
type,
|
|
5548
|
-
value,
|
|
5549
|
-
input.label ?? null,
|
|
5550
|
-
nk,
|
|
5551
|
-
verified,
|
|
5552
|
-
verifiedAt,
|
|
5553
|
-
now,
|
|
5554
|
-
now
|
|
5555
|
-
);
|
|
5556
|
-
const id = Number(result.lastInsertRowid);
|
|
5557
|
-
const row = getIdentityRowById(db, id);
|
|
5558
|
-
if (!row) {
|
|
5559
|
-
throw new AffinityInvariantError("inserted identity not found");
|
|
5560
|
-
}
|
|
5561
|
-
refreshContactRollup(db, contactId, now);
|
|
5562
|
-
db.prepare(
|
|
5563
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
5564
|
-
).run(contactId, contactId);
|
|
5565
|
-
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
5566
|
-
created: [{ kind: "identity", id }]
|
|
5567
|
-
});
|
|
5568
|
-
});
|
|
5569
|
-
}
|
|
5570
|
-
var init_add_identity = __esm({
|
|
5571
|
-
"src/identities/add_identity.ts"() {
|
|
5572
|
-
init_queries();
|
|
5573
|
-
init_rollups();
|
|
5574
|
-
init_mappers2();
|
|
5575
|
-
init_normalize();
|
|
5576
|
-
init_queries5();
|
|
5577
|
-
init_receipts6();
|
|
5578
|
-
init_validators5();
|
|
5579
|
-
init_affinity_invariant_error();
|
|
5580
|
-
init_affinity_not_found_error();
|
|
5581
|
-
init_affinity_state_error();
|
|
5582
|
-
init_affinity_validation_error();
|
|
5583
|
-
init_resolve_now();
|
|
5584
|
-
init_with_transaction();
|
|
5585
|
-
}
|
|
5586
|
-
});
|
|
5587
|
-
|
|
5588
6482
|
// src/identities/remove_identity.ts
|
|
5589
6483
|
function removeIdentity(db, identityId, removedAt) {
|
|
5590
6484
|
return withTransaction(db, () => {
|
|
@@ -5616,7 +6510,7 @@ var init_remove_identity = __esm({
|
|
|
5616
6510
|
init_rollups();
|
|
5617
6511
|
init_mappers2();
|
|
5618
6512
|
init_queries5();
|
|
5619
|
-
|
|
6513
|
+
init_receipts5();
|
|
5620
6514
|
init_affinity_not_found_error();
|
|
5621
6515
|
init_affinity_state_error();
|
|
5622
6516
|
init_resolve_now();
|
|
@@ -5649,6 +6543,7 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
5649
6543
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
5650
6544
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5651
6545
|
}
|
|
6546
|
+
validateIdentityInput(nextType, nextValue);
|
|
5652
6547
|
const nextLabel = patch.label !== void 0 ? patch.label : row.label;
|
|
5653
6548
|
const nkNext = normalizeIdentityKey(nextType, nextValue);
|
|
5654
6549
|
if (nkNext !== row.normalized_key) {
|
|
@@ -5674,13 +6569,14 @@ var init_revise_identity = __esm({
|
|
|
5674
6569
|
init_mappers2();
|
|
5675
6570
|
init_normalize();
|
|
5676
6571
|
init_queries5();
|
|
5677
|
-
|
|
6572
|
+
init_receipts5();
|
|
5678
6573
|
init_validators5();
|
|
5679
6574
|
init_affinity_not_found_error();
|
|
5680
6575
|
init_affinity_state_error();
|
|
5681
6576
|
init_affinity_validation_error();
|
|
5682
6577
|
init_resolve_now();
|
|
5683
6578
|
init_with_transaction();
|
|
6579
|
+
init_validate_identity_input();
|
|
5684
6580
|
}
|
|
5685
6581
|
});
|
|
5686
6582
|
|
|
@@ -5714,7 +6610,7 @@ var init_verify_identity = __esm({
|
|
|
5714
6610
|
init_queries();
|
|
5715
6611
|
init_mappers2();
|
|
5716
6612
|
init_queries5();
|
|
5717
|
-
|
|
6613
|
+
init_receipts5();
|
|
5718
6614
|
init_affinity_not_found_error();
|
|
5719
6615
|
init_affinity_state_error();
|
|
5720
6616
|
init_resolve_now();
|
|
@@ -5730,7 +6626,9 @@ function contactLocatorSchema6(description) {
|
|
|
5730
6626
|
contactId: integerSchema("Exact contact id."),
|
|
5731
6627
|
identity: objectSchema(
|
|
5732
6628
|
{
|
|
5733
|
-
type: stringSchema(
|
|
6629
|
+
type: stringSchema(
|
|
6630
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6631
|
+
),
|
|
5734
6632
|
value: stringSchema("Identity value.")
|
|
5735
6633
|
},
|
|
5736
6634
|
["type", "value"],
|
|
@@ -5740,7 +6638,23 @@ function contactLocatorSchema6(description) {
|
|
|
5740
6638
|
description
|
|
5741
6639
|
};
|
|
5742
6640
|
}
|
|
5743
|
-
function manageIdentityToolHandler(db,
|
|
6641
|
+
function manageIdentityToolHandler(db, rawInput) {
|
|
6642
|
+
const input = structuredClone(rawInput);
|
|
6643
|
+
const inputError = validateToolInput(manageIdentityTool.inputSchema, input);
|
|
6644
|
+
if (inputError)
|
|
6645
|
+
return toolFailure(
|
|
6646
|
+
"protocol",
|
|
6647
|
+
"invalid_input",
|
|
6648
|
+
"Invalid input.",
|
|
6649
|
+
inputError
|
|
6650
|
+
);
|
|
6651
|
+
if (input.action !== "add" && input.identityId === void 0)
|
|
6652
|
+
return toolFailure(
|
|
6653
|
+
"protocol",
|
|
6654
|
+
"invalid_input",
|
|
6655
|
+
"Missing identityId.",
|
|
6656
|
+
"identityId is required."
|
|
6657
|
+
);
|
|
5744
6658
|
return withToolHandling(() => {
|
|
5745
6659
|
switch (input.action) {
|
|
5746
6660
|
case "add": {
|
|
@@ -5758,7 +6672,8 @@ function manageIdentityToolHandler(db, input) {
|
|
|
5758
6672
|
return mutationToolResult(
|
|
5759
6673
|
"revise",
|
|
5760
6674
|
reviseIdentity(db, input.identityId, input.patch),
|
|
5761
|
-
"identity"
|
|
6675
|
+
"identity",
|
|
6676
|
+
Object.keys(input.patch)
|
|
5762
6677
|
);
|
|
5763
6678
|
case "verify":
|
|
5764
6679
|
return mutationToolResult(
|
|
@@ -5781,11 +6696,14 @@ var init_manage_identity_tool = __esm({
|
|
|
5781
6696
|
init_add_identity();
|
|
5782
6697
|
init_remove_identity();
|
|
5783
6698
|
init_revise_identity();
|
|
6699
|
+
init_validate_identity_input();
|
|
5784
6700
|
init_verify_identity();
|
|
5785
6701
|
init_tool_metadata();
|
|
5786
6702
|
init_tool_mutation();
|
|
5787
6703
|
init_tool_names();
|
|
5788
6704
|
init_tool_resolvers();
|
|
6705
|
+
init_tool_types();
|
|
6706
|
+
init_validate_tool_input();
|
|
5789
6707
|
manageIdentityTool = defineAffinityTool({
|
|
5790
6708
|
name: manageIdentityToolName,
|
|
5791
6709
|
description: "Add, revise, verify, or remove one routing identity on a contact. Use this for emails, phone numbers, social handles, and URLs \u2014 anything that could locate a person. Do not use manage_attribute for these.",
|
|
@@ -5818,7 +6736,9 @@ var init_manage_identity_tool = __esm({
|
|
|
5818
6736
|
),
|
|
5819
6737
|
input: objectSchema(
|
|
5820
6738
|
{
|
|
5821
|
-
type: stringSchema(
|
|
6739
|
+
type: stringSchema(
|
|
6740
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6741
|
+
),
|
|
5822
6742
|
value: stringSchema("Identity value."),
|
|
5823
6743
|
label: stringSchema("Optional label."),
|
|
5824
6744
|
verified: booleanSchema("Whether the identity starts verified."),
|
|
@@ -5832,7 +6752,9 @@ var init_manage_identity_tool = __esm({
|
|
|
5832
6752
|
),
|
|
5833
6753
|
patch: objectSchema(
|
|
5834
6754
|
{
|
|
5835
|
-
type: stringSchema(
|
|
6755
|
+
type: stringSchema(
|
|
6756
|
+
`Optional identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6757
|
+
),
|
|
5836
6758
|
value: stringSchema("Optional identity value."),
|
|
5837
6759
|
label: stringSchema("Optional label.")
|
|
5838
6760
|
},
|
|
@@ -6103,7 +7025,19 @@ function linkLocatorSchema4(description) {
|
|
|
6103
7025
|
description
|
|
6104
7026
|
};
|
|
6105
7027
|
}
|
|
6106
|
-
function manageRelationshipToolHandler(db,
|
|
7028
|
+
function manageRelationshipToolHandler(db, rawInput) {
|
|
7029
|
+
const input = structuredClone(rawInput);
|
|
7030
|
+
const inputError = validateToolInput(
|
|
7031
|
+
manageRelationshipTool.inputSchema,
|
|
7032
|
+
input
|
|
7033
|
+
);
|
|
7034
|
+
if (inputError)
|
|
7035
|
+
return toolFailure(
|
|
7036
|
+
"protocol",
|
|
7037
|
+
"invalid_input",
|
|
7038
|
+
"Invalid input.",
|
|
7039
|
+
inputError
|
|
7040
|
+
);
|
|
6107
7041
|
return withToolHandling(() => {
|
|
6108
7042
|
switch (input.action) {
|
|
6109
7043
|
case "seed_social_link": {
|
|
@@ -6202,6 +7136,8 @@ var init_manage_relationship_tool = __esm({
|
|
|
6202
7136
|
init_tool_mutation();
|
|
6203
7137
|
init_tool_names();
|
|
6204
7138
|
init_tool_resolvers();
|
|
7139
|
+
init_tool_types();
|
|
7140
|
+
init_validate_tool_input();
|
|
6205
7141
|
manageRelationshipTool = defineAffinityTool({
|
|
6206
7142
|
name: manageRelationshipToolName,
|
|
6207
7143
|
description: "Seed social links, revise bond and state, and manage structural ties. Seed a link as soon as a relationship is mentioned \u2014 do not wait for repeated evidence.",
|
|
@@ -6507,7 +7443,16 @@ function contactLocatorSchema8(description) {
|
|
|
6507
7443
|
description
|
|
6508
7444
|
};
|
|
6509
7445
|
}
|
|
6510
|
-
function mergeContactsToolHandler(db,
|
|
7446
|
+
function mergeContactsToolHandler(db, rawInput) {
|
|
7447
|
+
const input = structuredClone(rawInput);
|
|
7448
|
+
const inputError = validateToolInput(mergeContactsTool.inputSchema, input);
|
|
7449
|
+
if (inputError)
|
|
7450
|
+
return toolFailure(
|
|
7451
|
+
"protocol",
|
|
7452
|
+
"invalid_input",
|
|
7453
|
+
"Invalid input.",
|
|
7454
|
+
inputError
|
|
7455
|
+
);
|
|
6511
7456
|
return withToolHandling(() => {
|
|
6512
7457
|
const winner = resolveContactLocator(db, input.winner, "winner");
|
|
6513
7458
|
if (!winner.ok) {
|
|
@@ -6537,6 +7482,8 @@ var init_merge_contacts_tool = __esm({
|
|
|
6537
7482
|
init_tool_mutation();
|
|
6538
7483
|
init_tool_names();
|
|
6539
7484
|
init_tool_resolvers();
|
|
7485
|
+
init_tool_types();
|
|
7486
|
+
init_validate_tool_input();
|
|
6540
7487
|
mergeContactsTool = defineAffinityTool({
|
|
6541
7488
|
name: mergeContactsToolName,
|
|
6542
7489
|
description: "Merge one loser contact into one winner contact using explicit contact locators.",
|
|
@@ -6796,7 +7743,16 @@ function prepareParticipants2(db, raw, injectOwner) {
|
|
|
6796
7743
|
const normalized = raw.map(normalizeParticipant2);
|
|
6797
7744
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
6798
7745
|
}
|
|
6799
|
-
function recordEventToolHandler(db,
|
|
7746
|
+
function recordEventToolHandler(db, rawInput) {
|
|
7747
|
+
const input = structuredClone(rawInput);
|
|
7748
|
+
const inputError = validateToolInput(recordEventTool.inputSchema, input);
|
|
7749
|
+
if (inputError)
|
|
7750
|
+
return toolFailure(
|
|
7751
|
+
"protocol",
|
|
7752
|
+
"invalid_input",
|
|
7753
|
+
"Invalid input.",
|
|
7754
|
+
inputError
|
|
7755
|
+
);
|
|
6800
7756
|
return withToolHandling(() => {
|
|
6801
7757
|
const patched = {
|
|
6802
7758
|
...input.input,
|
|
@@ -6846,6 +7802,8 @@ var init_record_event_tool = __esm({
|
|
|
6846
7802
|
init_tool_mutation();
|
|
6847
7803
|
init_tool_names();
|
|
6848
7804
|
init_tool_resolvers();
|
|
7805
|
+
init_tool_types();
|
|
7806
|
+
init_validate_tool_input();
|
|
6849
7807
|
recordEventTool = defineAffinityTool({
|
|
6850
7808
|
name: recordEventToolName,
|
|
6851
7809
|
description: "Record one social event such as an interaction, observation, milestone, or transaction. For interactions, milestones, and transactions the owner contact is auto-injected into participants if missing. Observations may omit the owner when recording third-party evidence.",
|
|
@@ -6906,7 +7864,7 @@ var init_record_event_tool = __esm({
|
|
|
6906
7864
|
"observed"
|
|
6907
7865
|
])
|
|
6908
7866
|
},
|
|
6909
|
-
["
|
|
7867
|
+
["role"]
|
|
6910
7868
|
),
|
|
6911
7869
|
"Participant list. Must be non-empty and include the owner contact."
|
|
6912
7870
|
),
|
|
@@ -6924,6 +7882,27 @@ var init_record_event_tool = __esm({
|
|
|
6924
7882
|
}
|
|
6925
7883
|
});
|
|
6926
7884
|
|
|
7885
|
+
// src/contacts/contact_attribute_clauses.ts
|
|
7886
|
+
function contactAttributeClauses(filters) {
|
|
7887
|
+
const clauses = [];
|
|
7888
|
+
const params = [];
|
|
7889
|
+
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
7890
|
+
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
7891
|
+
for (const filter of [...attributes, ...tags.map((tag) => `tag:${tag}`)]) {
|
|
7892
|
+
const separator = filter.indexOf("=");
|
|
7893
|
+
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
7894
|
+
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
7895
|
+
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(?)"})`);
|
|
7896
|
+
params.push(name);
|
|
7897
|
+
if (separator >= 0) params.push(filter.slice(separator + 1));
|
|
7898
|
+
}
|
|
7899
|
+
return { clauses, params };
|
|
7900
|
+
}
|
|
7901
|
+
var init_contact_attribute_clauses = __esm({
|
|
7902
|
+
"src/contacts/contact_attribute_clauses.ts"() {
|
|
7903
|
+
}
|
|
7904
|
+
});
|
|
7905
|
+
|
|
6927
7906
|
// src/contacts/list_contacts.ts
|
|
6928
7907
|
function listContacts(db, filters, options) {
|
|
6929
7908
|
assertDefaultOrdering("listContacts", options);
|
|
@@ -6947,6 +7926,9 @@ function listContacts(db, filters, options) {
|
|
|
6947
7926
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
6948
7927
|
clauses.push("lifecycle_state != 'dormant'");
|
|
6949
7928
|
}
|
|
7929
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
7930
|
+
clauses.push(...attributeFilters.clauses);
|
|
7931
|
+
params.push(...attributeFilters.params);
|
|
6950
7932
|
const where = clauses.join(" AND ");
|
|
6951
7933
|
const rows = db.prepare(
|
|
6952
7934
|
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
@@ -6975,6 +7957,7 @@ var init_list_contacts = __esm({
|
|
|
6975
7957
|
"src/contacts/list_contacts.ts"() {
|
|
6976
7958
|
init_assert_default_ordering();
|
|
6977
7959
|
init_resolve_list_limit_offset();
|
|
7960
|
+
init_contact_attribute_clauses();
|
|
6978
7961
|
init_mappers4();
|
|
6979
7962
|
}
|
|
6980
7963
|
});
|
|
@@ -7202,131 +8185,6 @@ var init_list_upcoming_dates = __esm({
|
|
|
7202
8185
|
}
|
|
7203
8186
|
});
|
|
7204
8187
|
|
|
7205
|
-
// src/events/get_contact_journal.ts
|
|
7206
|
-
function getContactJournal(db, contactId, options) {
|
|
7207
|
-
assertDefaultOrdering("getContactJournal", options);
|
|
7208
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
7209
|
-
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
7210
|
-
const params = [contactId];
|
|
7211
|
-
if (options?.since !== void 0) {
|
|
7212
|
-
clauses.push("e.occurred_at >= ?");
|
|
7213
|
-
params.push(options.since);
|
|
7214
|
-
}
|
|
7215
|
-
if (options?.until !== void 0) {
|
|
7216
|
-
clauses.push("e.occurred_at <= ?");
|
|
7217
|
-
params.push(options.until);
|
|
7218
|
-
}
|
|
7219
|
-
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
7220
|
-
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
7221
|
-
clauses.push(`e.type IN (${ph})`);
|
|
7222
|
-
params.push(...options.eventTypes);
|
|
7223
|
-
}
|
|
7224
|
-
const where = clauses.join(" AND ");
|
|
7225
|
-
const rows = db.prepare(
|
|
7226
|
-
`SELECT e.id FROM events e
|
|
7227
|
-
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
7228
|
-
WHERE ${where}
|
|
7229
|
-
ORDER BY e.occurred_at DESC, e.id DESC
|
|
7230
|
-
LIMIT ? OFFSET ?`
|
|
7231
|
-
).all(...params, limit, offset);
|
|
7232
|
-
return batchLoadEventRecords(
|
|
7233
|
-
db,
|
|
7234
|
-
rows.map((r) => r.id)
|
|
7235
|
-
);
|
|
7236
|
-
}
|
|
7237
|
-
var init_get_contact_journal = __esm({
|
|
7238
|
-
"src/events/get_contact_journal.ts"() {
|
|
7239
|
-
init_assert_default_ordering();
|
|
7240
|
-
init_resolve_list_limit_offset();
|
|
7241
|
-
init_loaders();
|
|
7242
|
-
}
|
|
7243
|
-
});
|
|
7244
|
-
|
|
7245
|
-
// src/events/list_open_commitments.ts
|
|
7246
|
-
function mapResolution(resolvedAt, resolution) {
|
|
7247
|
-
if (resolvedAt === null) {
|
|
7248
|
-
return "open";
|
|
7249
|
-
}
|
|
7250
|
-
if (resolution === "cancelled") {
|
|
7251
|
-
return "cancelled";
|
|
7252
|
-
}
|
|
7253
|
-
return "resolved";
|
|
7254
|
-
}
|
|
7255
|
-
function listOpenCommitments(db, filters, options) {
|
|
7256
|
-
assertDefaultOrdering("listOpenCommitments", options);
|
|
7257
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
7258
|
-
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
7259
|
-
const params = [];
|
|
7260
|
-
if (options?.since !== void 0) {
|
|
7261
|
-
clauses.push("e.occurred_at >= ?");
|
|
7262
|
-
params.push(options.since);
|
|
7263
|
-
}
|
|
7264
|
-
if (options?.until !== void 0) {
|
|
7265
|
-
clauses.push("e.occurred_at <= ?");
|
|
7266
|
-
params.push(options.until);
|
|
7267
|
-
}
|
|
7268
|
-
if (filters?.commitmentType !== void 0) {
|
|
7269
|
-
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
7270
|
-
throw new AffinityValidationError("invalid commitmentType filter");
|
|
7271
|
-
}
|
|
7272
|
-
clauses.push("oc.commitment_type = ?");
|
|
7273
|
-
params.push(filters.commitmentType);
|
|
7274
|
-
}
|
|
7275
|
-
if (filters?.contactId !== void 0) {
|
|
7276
|
-
clauses.push(
|
|
7277
|
-
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
7278
|
-
);
|
|
7279
|
-
params.push(filters.contactId);
|
|
7280
|
-
}
|
|
7281
|
-
if (filters?.linkId !== void 0) {
|
|
7282
|
-
clauses.push(
|
|
7283
|
-
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
7284
|
-
);
|
|
7285
|
-
params.push(filters.linkId);
|
|
7286
|
-
}
|
|
7287
|
-
if (filters?.horizonDays !== void 0) {
|
|
7288
|
-
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
7289
|
-
throw new AffinityValidationError(
|
|
7290
|
-
"horizonDays must be a non-negative integer"
|
|
7291
|
-
);
|
|
7292
|
-
}
|
|
7293
|
-
const anchor = options?.since ?? Date.now();
|
|
7294
|
-
clauses.push("oc.due_at IS NOT NULL");
|
|
7295
|
-
clauses.push("oc.due_at <= ?");
|
|
7296
|
-
params.push(anchor + filters.horizonDays * DAY_MS3);
|
|
7297
|
-
}
|
|
7298
|
-
const where = clauses.join(" AND ");
|
|
7299
|
-
const rows = db.prepare(
|
|
7300
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
7301
|
-
e.type, e.summary
|
|
7302
|
-
FROM open_commitments oc
|
|
7303
|
-
INNER JOIN events e ON e.id = oc.event_id
|
|
7304
|
-
WHERE ${where}
|
|
7305
|
-
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
7306
|
-
LIMIT ? OFFSET ?`
|
|
7307
|
-
).all(...params, limit, offset);
|
|
7308
|
-
const eventIds = rows.map((r) => r.event_id);
|
|
7309
|
-
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
7310
|
-
return rows.map((r) => ({
|
|
7311
|
-
eventId: r.event_id,
|
|
7312
|
-
type: r.type,
|
|
7313
|
-
summary: r.summary,
|
|
7314
|
-
participants: partsByEvent.get(r.event_id) ?? [],
|
|
7315
|
-
dueAt: r.due_at,
|
|
7316
|
-
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
7317
|
-
}));
|
|
7318
|
-
}
|
|
7319
|
-
var DAY_MS3;
|
|
7320
|
-
var init_list_open_commitments = __esm({
|
|
7321
|
-
"src/events/list_open_commitments.ts"() {
|
|
7322
|
-
init_affinity_validation_error();
|
|
7323
|
-
init_assert_default_ordering();
|
|
7324
|
-
init_resolve_list_limit_offset();
|
|
7325
|
-
init_loaders();
|
|
7326
|
-
DAY_MS3 = 864e5;
|
|
7327
|
-
}
|
|
7328
|
-
});
|
|
7329
|
-
|
|
7330
8188
|
// src/graph/get_affinity_chart.ts
|
|
7331
8189
|
function getAffinityChart(db, options) {
|
|
7332
8190
|
const contactClauses = [
|
|
@@ -7560,13 +8418,15 @@ function listRadar(db, filters, options) {
|
|
|
7560
8418
|
if (ownerId === null) {
|
|
7561
8419
|
return [];
|
|
7562
8420
|
}
|
|
8421
|
+
const now = resolveNow(options?.now);
|
|
8422
|
+
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})`;
|
|
7563
8423
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
7564
8424
|
const clauses = [
|
|
7565
8425
|
"l.removed_at IS NULL",
|
|
7566
8426
|
"l.is_structural = 0",
|
|
7567
|
-
"l.from_contact_id = ?"
|
|
8427
|
+
"(l.from_contact_id = ? OR l.to_contact_id = ?)"
|
|
7568
8428
|
];
|
|
7569
|
-
const params = [ownerId];
|
|
8429
|
+
const params = [ownerId, ownerId];
|
|
7570
8430
|
if (filters?.kind !== void 0) {
|
|
7571
8431
|
clauses.push("l.kind = ?");
|
|
7572
8432
|
params.push(filters.kind);
|
|
@@ -7588,6 +8448,7 @@ function listRadar(db, filters, options) {
|
|
|
7588
8448
|
const rows = db.prepare(
|
|
7589
8449
|
`SELECT
|
|
7590
8450
|
l.id,
|
|
8451
|
+
${overdue} AS overdue_commitment,
|
|
7591
8452
|
l.from_contact_id,
|
|
7592
8453
|
l.to_contact_id,
|
|
7593
8454
|
l.trust,
|
|
@@ -7596,17 +8457,18 @@ function listRadar(db, filters, options) {
|
|
|
7596
8457
|
lr.recency_score,
|
|
7597
8458
|
lr.normalized_rank
|
|
7598
8459
|
FROM links l
|
|
7599
|
-
INNER JOIN contacts c_to ON c_to.id = l.to_contact_id
|
|
8460
|
+
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
|
|
7600
8461
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
7601
8462
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
7602
8463
|
WHERE ${where}
|
|
7603
|
-
ORDER BY COALESCE(lr.radar_score, 0) DESC
|
|
8464
|
+
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
7604
8465
|
LIMIT ? OFFSET ?`
|
|
7605
8466
|
).all(...params, limit, offset);
|
|
7606
8467
|
return rows.map((row) => ({
|
|
7607
8468
|
linkId: row.id,
|
|
7608
8469
|
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
7609
|
-
|
|
8470
|
+
reasons: row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
8471
|
+
driftPriority: row.overdue_commitment ? Math.max(1, row.drift_priority ?? 0) : row.drift_priority ?? 0,
|
|
7610
8472
|
recencyScore: row.recency_score ?? 0,
|
|
7611
8473
|
normalizedRank: row.normalized_rank ?? 0,
|
|
7612
8474
|
trust: row.trust ?? 0,
|
|
@@ -7622,6 +8484,7 @@ var init_list_radar = __esm({
|
|
|
7622
8484
|
init_queries();
|
|
7623
8485
|
init_assert_default_ordering();
|
|
7624
8486
|
init_resolve_list_limit_offset();
|
|
8487
|
+
init_resolve_now();
|
|
7625
8488
|
}
|
|
7626
8489
|
});
|
|
7627
8490
|
|
|
@@ -7675,9 +8538,79 @@ var init_list_dismissed_duplicates = __esm({
|
|
|
7675
8538
|
}
|
|
7676
8539
|
});
|
|
7677
8540
|
|
|
8541
|
+
// src/tools/enrich_review_item.ts
|
|
8542
|
+
function enrichReviewItem(db, item) {
|
|
8543
|
+
const name = (id) => {
|
|
8544
|
+
if (typeof id !== "number") return "";
|
|
8545
|
+
return db.prepare("SELECT name FROM contacts WHERE id = ?").get(id)?.name ?? `Contact #${id}`;
|
|
8546
|
+
};
|
|
8547
|
+
const result = { ...item };
|
|
8548
|
+
if (item.kind === "link") {
|
|
8549
|
+
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,
|
|
8550
|
+
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);
|
|
8551
|
+
if (link) {
|
|
8552
|
+
result.title = link.from_owner ? String(link.to_name) : link.to_owner ? String(link.from_name) : `${link.from_name} \xB7 ${link.to_name}`;
|
|
8553
|
+
result.subtitle = [link.kind, link.role].filter(Boolean).join(" \xB7 ");
|
|
8554
|
+
for (const key of ["affinity", "trust", "rank"])
|
|
8555
|
+
if (typeof link[key] === "number") result[key] = link[key];
|
|
8556
|
+
result.recencyScore ??= link.recency_score;
|
|
8557
|
+
result.driftPriority ??= link.drift_priority;
|
|
8558
|
+
result.readinessScore = link.readiness_score;
|
|
8559
|
+
result.normalizedRank ??= link.normalized_rank;
|
|
8560
|
+
result.reciprocityScore = link.reciprocity_score;
|
|
8561
|
+
result.radarScore = link.radar_score;
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
if (item.kind === "event" && typeof item.linkId === "number" && !Array.isArray(item.participants)) {
|
|
8565
|
+
const link = db.prepare("SELECT from_contact_id, to_contact_id FROM links WHERE id = ?").get(item.linkId);
|
|
8566
|
+
if (link)
|
|
8567
|
+
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8568
|
+
}
|
|
8569
|
+
if (Array.isArray(item.participants)) {
|
|
8570
|
+
result.counterpartyNames = item.participants.map(
|
|
8571
|
+
(p) => name(p.contactId)
|
|
8572
|
+
);
|
|
8573
|
+
result.subtitle = [
|
|
8574
|
+
item.subtitle,
|
|
8575
|
+
result.counterpartyNames.join(", ")
|
|
8576
|
+
].filter(Boolean).join(" \xB7 ");
|
|
8577
|
+
}
|
|
8578
|
+
if (typeof item.leftContactId === "number")
|
|
8579
|
+
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
8580
|
+
if (typeof item.winnerContactId === "number")
|
|
8581
|
+
result.title = `Merged ${name(item.loserContactId)} into ${name(item.winnerContactId)}`;
|
|
8582
|
+
if (item.occursAt !== void 0 && item.kind === "contact")
|
|
8583
|
+
result.title = `${name(item.id)} \xB7 ${item.title}`;
|
|
8584
|
+
for (const [key, value] of Object.entries(result)) {
|
|
8585
|
+
if (typeof value === "number" && /score|priority|affinity|trust|rank/i.test(key))
|
|
8586
|
+
result[key] = Math.round(value * 1e3) / 1e3;
|
|
8587
|
+
}
|
|
8588
|
+
return result;
|
|
8589
|
+
}
|
|
8590
|
+
var init_enrich_review_item = __esm({
|
|
8591
|
+
"src/tools/enrich_review_item.ts"() {
|
|
8592
|
+
}
|
|
8593
|
+
});
|
|
8594
|
+
|
|
8595
|
+
// src/tools/tool_summary.ts
|
|
8596
|
+
function summarizeCount(count, singular) {
|
|
8597
|
+
if (count === 0) {
|
|
8598
|
+
return `No ${singular}s found.`;
|
|
8599
|
+
}
|
|
8600
|
+
if (count === 1) {
|
|
8601
|
+
return `Found 1 ${singular}.`;
|
|
8602
|
+
}
|
|
8603
|
+
return `Found ${count} ${singular}s.`;
|
|
8604
|
+
}
|
|
8605
|
+
var init_tool_summary = __esm({
|
|
8606
|
+
"src/tools/tool_summary.ts"() {
|
|
8607
|
+
}
|
|
8608
|
+
});
|
|
8609
|
+
|
|
7678
8610
|
// src/tools/review_affinity_tool.ts
|
|
7679
8611
|
function buildReadOptions(input) {
|
|
7680
8612
|
return {
|
|
8613
|
+
...input.now === void 0 ? {} : { now: input.now },
|
|
7681
8614
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7682
8615
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
7683
8616
|
...input.includeArchived === void 0 ? {} : { includeArchived: input.includeArchived },
|
|
@@ -7797,6 +8730,7 @@ function validateReviewInput(input) {
|
|
|
7797
8730
|
}
|
|
7798
8731
|
function duplicateCandidateItem(candidate) {
|
|
7799
8732
|
return {
|
|
8733
|
+
...candidate,
|
|
7800
8734
|
kind: "contact",
|
|
7801
8735
|
id: candidate.leftContactId,
|
|
7802
8736
|
title: `Possible duplicate: #${candidate.leftContactId} and #${candidate.rightContactId}`,
|
|
@@ -7806,24 +8740,28 @@ function duplicateCandidateItem(candidate) {
|
|
|
7806
8740
|
}
|
|
7807
8741
|
function momentItem(moment) {
|
|
7808
8742
|
return {
|
|
8743
|
+
...moment,
|
|
8744
|
+
occurredAtIso: new Date(moment.occurredAt).toISOString(),
|
|
7809
8745
|
kind: "event",
|
|
7810
8746
|
id: moment.eventId,
|
|
7811
8747
|
title: moment.summary,
|
|
7812
8748
|
subtitle: moment.momentKind,
|
|
7813
|
-
snippet:
|
|
8749
|
+
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
7814
8750
|
};
|
|
7815
8751
|
}
|
|
7816
8752
|
function commitmentItem(commitment) {
|
|
7817
8753
|
return {
|
|
8754
|
+
...commitment,
|
|
7818
8755
|
kind: "event",
|
|
7819
8756
|
id: commitment.eventId,
|
|
7820
8757
|
title: commitment.summary,
|
|
7821
8758
|
subtitle: commitment.type,
|
|
7822
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt
|
|
8759
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
7823
8760
|
};
|
|
7824
8761
|
}
|
|
7825
8762
|
function radarItem(radar) {
|
|
7826
8763
|
return {
|
|
8764
|
+
...radar,
|
|
7827
8765
|
kind: "link",
|
|
7828
8766
|
id: radar.linkId,
|
|
7829
8767
|
title: `Link #${radar.linkId}`,
|
|
@@ -7833,29 +8771,34 @@ function radarItem(radar) {
|
|
|
7833
8771
|
}
|
|
7834
8772
|
function upcomingDateItem(date) {
|
|
7835
8773
|
return {
|
|
8774
|
+
...date,
|
|
8775
|
+
occursAt: date.occursOn,
|
|
8776
|
+
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
7836
8777
|
kind: date.targetRef.kind,
|
|
7837
8778
|
id: date.targetRef.id,
|
|
7838
8779
|
title: date.summary,
|
|
7839
8780
|
subtitle: date.recurrenceKind,
|
|
7840
|
-
snippet: `occurs ${date.occursOn}`
|
|
8781
|
+
snippet: `occurs ${new Date(date.occursOn).toISOString().slice(0, 10)}`
|
|
7841
8782
|
};
|
|
7842
8783
|
}
|
|
7843
8784
|
function mergeHistoryItem(record) {
|
|
7844
8785
|
return {
|
|
8786
|
+
...record,
|
|
7845
8787
|
kind: "contact",
|
|
7846
8788
|
id: record.winnerContactId,
|
|
7847
8789
|
title: `Merged #${record.loserContactId} into #${record.winnerContactId}`,
|
|
7848
|
-
subtitle:
|
|
8790
|
+
subtitle: new Date(record.mergedAt).toISOString().slice(0, 10),
|
|
7849
8791
|
snippet: record.reasonSummary ?? void 0
|
|
7850
8792
|
};
|
|
7851
8793
|
}
|
|
7852
8794
|
function dismissedPairItem(record) {
|
|
7853
8795
|
return {
|
|
8796
|
+
...record,
|
|
7854
8797
|
kind: "contact",
|
|
7855
8798
|
id: record.leftContactId,
|
|
7856
8799
|
title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
|
|
7857
8800
|
subtitle: record.reason ?? "no reason given",
|
|
7858
|
-
snippet: `dismissed at ${record.dismissedAt}`
|
|
8801
|
+
snippet: `dismissed at ${new Date(record.dismissedAt).toISOString().slice(0, 10)}`
|
|
7859
8802
|
};
|
|
7860
8803
|
}
|
|
7861
8804
|
function compactFilters(input) {
|
|
@@ -7865,7 +8808,16 @@ function compactFilters(input) {
|
|
|
7865
8808
|
)
|
|
7866
8809
|
);
|
|
7867
8810
|
}
|
|
7868
|
-
function reviewAffinityToolHandler(db,
|
|
8811
|
+
function reviewAffinityToolHandler(db, rawInput) {
|
|
8812
|
+
const input = structuredClone(rawInput);
|
|
8813
|
+
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8814
|
+
if (inputError)
|
|
8815
|
+
return toolFailure(
|
|
8816
|
+
"protocol",
|
|
8817
|
+
"invalid_input",
|
|
8818
|
+
"Invalid input.",
|
|
8819
|
+
inputError
|
|
8820
|
+
);
|
|
7869
8821
|
return withToolHandling(() => {
|
|
7870
8822
|
const invalid = validateReviewInput(input);
|
|
7871
8823
|
if (invalid) {
|
|
@@ -7879,6 +8831,9 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7879
8831
|
items = listContacts(
|
|
7880
8832
|
db,
|
|
7881
8833
|
{
|
|
8834
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
8835
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
8836
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
7882
8837
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
7883
8838
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
7884
8839
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -7909,7 +8864,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7909
8864
|
"Provide `contact` when `view` is `events.contact_journal`."
|
|
7910
8865
|
);
|
|
7911
8866
|
}
|
|
7912
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
8867
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
7913
8868
|
if (!contact.ok) {
|
|
7914
8869
|
return contact.result;
|
|
7915
8870
|
}
|
|
@@ -7944,7 +8899,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7944
8899
|
let contactId;
|
|
7945
8900
|
let linkId;
|
|
7946
8901
|
if (input.contact !== void 0) {
|
|
7947
|
-
const contact = resolveContactLocator(
|
|
8902
|
+
const contact = resolveContactLocator(
|
|
8903
|
+
db,
|
|
8904
|
+
input.contact,
|
|
8905
|
+
"contact",
|
|
8906
|
+
true
|
|
8907
|
+
);
|
|
7948
8908
|
if (!contact.ok) {
|
|
7949
8909
|
return contact.result;
|
|
7950
8910
|
}
|
|
@@ -7973,7 +8933,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7973
8933
|
let contactId;
|
|
7974
8934
|
let linkId;
|
|
7975
8935
|
if (input.contact !== void 0) {
|
|
7976
|
-
const contact = resolveContactLocator(
|
|
8936
|
+
const contact = resolveContactLocator(
|
|
8937
|
+
db,
|
|
8938
|
+
input.contact,
|
|
8939
|
+
"contact",
|
|
8940
|
+
true
|
|
8941
|
+
);
|
|
7977
8942
|
if (!contact.ok) {
|
|
7978
8943
|
return contact.result;
|
|
7979
8944
|
}
|
|
@@ -8066,7 +9031,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8066
9031
|
"Provide `contact` when `view` is `merges.history`."
|
|
8067
9032
|
);
|
|
8068
9033
|
}
|
|
8069
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
9034
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
8070
9035
|
if (!contact.ok) {
|
|
8071
9036
|
return contact.result;
|
|
8072
9037
|
}
|
|
@@ -8079,6 +9044,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8079
9044
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
8080
9045
|
break;
|
|
8081
9046
|
}
|
|
9047
|
+
items = items.map((item) => enrichReviewItem(db, item));
|
|
8082
9048
|
return toolSuccess(
|
|
8083
9049
|
summarizeCount(items.length, "item"),
|
|
8084
9050
|
{
|
|
@@ -8117,12 +9083,15 @@ var init_review_affinity_tool = __esm({
|
|
|
8117
9083
|
init_list_radar();
|
|
8118
9084
|
init_get_merge_history();
|
|
8119
9085
|
init_list_dismissed_duplicates();
|
|
9086
|
+
init_enrich_review_item();
|
|
8120
9087
|
init_tool_metadata();
|
|
8121
9088
|
init_tool_names();
|
|
8122
9089
|
init_tool_ref();
|
|
8123
9090
|
init_tool_resolvers();
|
|
8124
9091
|
init_tool_summary();
|
|
8125
9092
|
init_tool_types();
|
|
9093
|
+
init_tool_types();
|
|
9094
|
+
init_validate_tool_input();
|
|
8126
9095
|
reviewAffinityTool = defineAffinityTool({
|
|
8127
9096
|
name: reviewAffinityToolName,
|
|
8128
9097
|
description: "Review one affinity surface such as contacts, journals, link queues, commitments, dates, graph state, or merge history.",
|
|
@@ -8134,12 +9103,16 @@ var init_review_affinity_tool = __esm({
|
|
|
8134
9103
|
targetKinds: ["contact", "link", "event"],
|
|
8135
9104
|
inputDescriptions: {
|
|
8136
9105
|
view: "Which review surface to return.",
|
|
9106
|
+
now: "Optional current timestamp for overdue calculations.",
|
|
8137
9107
|
contact: "Target contact locator for views that require one.",
|
|
8138
9108
|
link: "Target link locator for views that require one.",
|
|
8139
9109
|
contactIds: "Optional contact id subset for duplicate review or graph review.",
|
|
8140
9110
|
kind: "Optional contact kind or link kind filter, depending on the view.",
|
|
8141
9111
|
lifecycleState: "Optional contact lifecycle filter for the contact list view.",
|
|
8142
9112
|
linkState: "Optional link state filter for link review views.",
|
|
9113
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9114
|
+
tag: "Tag name; arrays require all tags.",
|
|
9115
|
+
includeLost: "Whether lost contacts are included.",
|
|
8143
9116
|
includeOwner: "Whether owner contacts should be included in contact review.",
|
|
8144
9117
|
includeDormant: "Whether dormant links or contacts should be included where supported.",
|
|
8145
9118
|
includeArchived: "Whether archived links should be included where supported.",
|
|
@@ -8176,6 +9149,9 @@ var init_review_affinity_tool = __esm({
|
|
|
8176
9149
|
"merges.history",
|
|
8177
9150
|
"merges.dismissed"
|
|
8178
9151
|
]),
|
|
9152
|
+
now: integerSchema(
|
|
9153
|
+
"Optional current timestamp for overdue calculations."
|
|
9154
|
+
),
|
|
8179
9155
|
contact: contactLocatorSchema9("Optional target contact locator."),
|
|
8180
9156
|
link: linkLocatorSchema5("Optional target link locator."),
|
|
8181
9157
|
contactIds: arraySchema(
|
|
@@ -8198,6 +9174,17 @@ var init_review_affinity_tool = __esm({
|
|
|
8198
9174
|
"broken",
|
|
8199
9175
|
"archived"
|
|
8200
9176
|
]),
|
|
9177
|
+
attribute: oneOfSchema([
|
|
9178
|
+
stringSchema("Attribute filter."),
|
|
9179
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9180
|
+
]),
|
|
9181
|
+
tag: oneOfSchema([
|
|
9182
|
+
stringSchema("Tag filter."),
|
|
9183
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9184
|
+
]),
|
|
9185
|
+
includeLost: booleanSchema(
|
|
9186
|
+
"Include lost contacts (search excludes them by default)."
|
|
9187
|
+
),
|
|
8201
9188
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8202
9189
|
includeDormant: booleanSchema("Whether dormant rows are included."),
|
|
8203
9190
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
@@ -8244,99 +9231,110 @@ var init_review_affinity_tool = __esm({
|
|
|
8244
9231
|
});
|
|
8245
9232
|
|
|
8246
9233
|
// src/contacts/search_contacts.ts
|
|
8247
|
-
function relevanceScore(name, queryLower, matchedName, matchedNormalizedIdentity) {
|
|
8248
|
-
const nl = name.toLowerCase();
|
|
8249
|
-
if (matchedNormalizedIdentity) return 1.1;
|
|
8250
|
-
if (nl.startsWith(queryLower)) return 1;
|
|
8251
|
-
if (matchedName) return 0.85;
|
|
8252
|
-
return 0.7;
|
|
8253
|
-
}
|
|
8254
9234
|
function searchContacts(db, query, filters, options) {
|
|
8255
|
-
const q = query.trim();
|
|
8256
|
-
if (q
|
|
8257
|
-
return [];
|
|
8258
|
-
}
|
|
9235
|
+
const q = foldSearchText(query.trim());
|
|
9236
|
+
if (!q) return [];
|
|
8259
9237
|
assertDefaultOrdering("searchContacts", options);
|
|
8260
9238
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
8261
|
-
const
|
|
8262
|
-
const
|
|
8263
|
-
|
|
9239
|
+
const fields = /* @__PURE__ */ new Map();
|
|
9240
|
+
const rows = db.prepare(`
|
|
9241
|
+
SELECT contact_id, value || ' ' || normalized_key AS text, 'identity:' || type AS source FROM identities WHERE removed_at IS NULL
|
|
9242
|
+
UNION ALL SELECT contact_id, name || ' ' || COALESCE(value, ''), 'attribute:' || name FROM attributes WHERE deleted_at IS NULL AND contact_id IS NOT NULL
|
|
9243
|
+
UNION ALL SELECT from_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
9244
|
+
UNION ALL SELECT to_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
9245
|
+
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
|
|
9246
|
+
`).all();
|
|
9247
|
+
for (const row of rows) {
|
|
9248
|
+
const values = fields.get(row.contact_id) ?? [];
|
|
9249
|
+
values.push({ text: foldSearchText(row.text), source: row.source });
|
|
9250
|
+
fields.set(row.contact_id, values);
|
|
9251
|
+
}
|
|
9252
|
+
const tokens = q.split(/\s+/);
|
|
9253
|
+
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
9254
|
+
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
9255
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
8264
9256
|
const clauses = [
|
|
8265
|
-
"
|
|
8266
|
-
"
|
|
9257
|
+
"deleted_at IS NULL",
|
|
9258
|
+
"lifecycle_state != 'merged'",
|
|
9259
|
+
...attributeFilters.clauses
|
|
8267
9260
|
];
|
|
8268
|
-
const params = [];
|
|
9261
|
+
const params = [...attributeFilters.params];
|
|
8269
9262
|
if (filters?.kind !== void 0) {
|
|
8270
|
-
clauses.push("
|
|
9263
|
+
clauses.push("kind = ?");
|
|
8271
9264
|
params.push(filters.kind);
|
|
8272
9265
|
}
|
|
8273
9266
|
if (filters?.lifecycleState !== void 0) {
|
|
8274
|
-
clauses.push("
|
|
9267
|
+
clauses.push("lifecycle_state = ?");
|
|
8275
9268
|
params.push(filters.lifecycleState);
|
|
8276
9269
|
}
|
|
8277
|
-
if (filters?.includeOwner === false)
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
const
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
Number(
|
|
8316
|
-
r.matched_normalized_identity ?? 0
|
|
8317
|
-
) > 0
|
|
9270
|
+
if (filters?.includeOwner === false) clauses.push("is_owner = 0");
|
|
9271
|
+
const contacts = db.prepare(
|
|
9272
|
+
`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 ")}`
|
|
9273
|
+
).all(...params).map((row) => ({
|
|
9274
|
+
...mapContactRowToContactListItem(row),
|
|
9275
|
+
primaryIdentity: row.primary_identity
|
|
9276
|
+
}));
|
|
9277
|
+
const scored = [];
|
|
9278
|
+
for (const contact of contacts) {
|
|
9279
|
+
if (contact.lifecycleState === "dormant" && !(filters?.includeDormant ?? options?.includeDormant))
|
|
9280
|
+
continue;
|
|
9281
|
+
if (contact.lifecycleState === "lost" && !filters?.includeLost) continue;
|
|
9282
|
+
const name = foldSearchText(contact.name);
|
|
9283
|
+
const values = [
|
|
9284
|
+
{ text: name, source: "name" },
|
|
9285
|
+
...(fields.get(contact.id) ?? []).filter(
|
|
9286
|
+
(field) => !contact.isOwner || field.source !== "event" && field.source !== "link_role"
|
|
9287
|
+
)
|
|
9288
|
+
];
|
|
9289
|
+
const phoneFields = phone ? values.filter(
|
|
9290
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
9291
|
+
) : [];
|
|
9292
|
+
if (!phoneFields.length && !tokens.every(
|
|
9293
|
+
(token) => values.some((field) => field.text.includes(token))
|
|
9294
|
+
))
|
|
9295
|
+
continue;
|
|
9296
|
+
const matchedOn = [
|
|
9297
|
+
...new Set(
|
|
9298
|
+
[
|
|
9299
|
+
...values.filter(
|
|
9300
|
+
(field) => tokens.some((token) => field.text.includes(token))
|
|
9301
|
+
),
|
|
9302
|
+
...phoneFields
|
|
9303
|
+
].map((field) => field.source)
|
|
9304
|
+
)
|
|
9305
|
+
];
|
|
9306
|
+
const identities = values.filter(
|
|
9307
|
+
(field) => field.source.startsWith("identity:")
|
|
8318
9308
|
);
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
});
|
|
8327
|
-
scored.sort((a, b) => b.score - a.score);
|
|
8328
|
-
return scored.slice(offset, offset + limit).map((s) => s.item);
|
|
9309
|
+
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(
|
|
9310
|
+
(field) => tokens.every((token) => field.text.includes(token))
|
|
9311
|
+
) ? 2 : 1;
|
|
9312
|
+
scored.push({ item: { ...contact, matchedOn }, score });
|
|
9313
|
+
}
|
|
9314
|
+
scored.sort((a, b) => b.score - a.score || a.item.id - b.item.id);
|
|
9315
|
+
return scored.slice(offset, offset + limit).map(({ item }) => item);
|
|
8329
9316
|
}
|
|
8330
9317
|
var init_search_contacts = __esm({
|
|
8331
9318
|
"src/contacts/search_contacts.ts"() {
|
|
8332
9319
|
init_assert_default_ordering();
|
|
8333
9320
|
init_resolve_list_limit_offset();
|
|
9321
|
+
init_contact_attribute_clauses();
|
|
9322
|
+
init_fold_search_text();
|
|
8334
9323
|
init_mappers4();
|
|
8335
9324
|
}
|
|
8336
9325
|
});
|
|
8337
9326
|
|
|
8338
9327
|
// src/tools/search_affinity_tool.ts
|
|
8339
|
-
function searchAffinityToolHandler(db,
|
|
9328
|
+
function searchAffinityToolHandler(db, rawInput) {
|
|
9329
|
+
const input = structuredClone(rawInput);
|
|
9330
|
+
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9331
|
+
if (inputError)
|
|
9332
|
+
return toolFailure(
|
|
9333
|
+
"protocol",
|
|
9334
|
+
"invalid_input",
|
|
9335
|
+
"Invalid input.",
|
|
9336
|
+
inputError
|
|
9337
|
+
);
|
|
8340
9338
|
return withToolHandling(() => {
|
|
8341
9339
|
const options = {
|
|
8342
9340
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
@@ -8346,6 +9344,9 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8346
9344
|
db,
|
|
8347
9345
|
input.query,
|
|
8348
9346
|
{
|
|
9347
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
9348
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
9349
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8349
9350
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8350
9351
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8351
9352
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8377,9 +9378,11 @@ var init_search_affinity_tool = __esm({
|
|
|
8377
9378
|
init_tool_resolvers();
|
|
8378
9379
|
init_tool_summary();
|
|
8379
9380
|
init_tool_types();
|
|
9381
|
+
init_tool_types();
|
|
9382
|
+
init_validate_tool_input();
|
|
8380
9383
|
searchAffinityTool = defineAffinityTool({
|
|
8381
9384
|
name: searchAffinityToolName,
|
|
8382
|
-
description: "Search contacts by name
|
|
9385
|
+
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.",
|
|
8383
9386
|
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.",
|
|
8384
9387
|
whenNotToUse: "Do not use this when you already know the exact contact you want to inspect in depth.",
|
|
8385
9388
|
sideEffects: "none",
|
|
@@ -8390,6 +9393,9 @@ var init_search_affinity_tool = __esm({
|
|
|
8390
9393
|
query: "Free-text or natural-key search text.",
|
|
8391
9394
|
kind: "Optional contact kind filter.",
|
|
8392
9395
|
lifecycleState: "Optional lifecycle state filter.",
|
|
9396
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9397
|
+
tag: "Tag name; arrays require all tags.",
|
|
9398
|
+
includeLost: "Whether lost contacts are included in search.",
|
|
8393
9399
|
includeOwner: "Whether owner contacts are eligible for results.",
|
|
8394
9400
|
includeDormant: "Whether dormant contacts should be eligible for results.",
|
|
8395
9401
|
limit: "Optional result limit.",
|
|
@@ -8414,6 +9420,17 @@ var init_search_affinity_tool = __esm({
|
|
|
8414
9420
|
"merged",
|
|
8415
9421
|
"lost"
|
|
8416
9422
|
]),
|
|
9423
|
+
attribute: oneOfSchema([
|
|
9424
|
+
stringSchema("Attribute filter."),
|
|
9425
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9426
|
+
]),
|
|
9427
|
+
tag: oneOfSchema([
|
|
9428
|
+
stringSchema("Tag filter."),
|
|
9429
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9430
|
+
]),
|
|
9431
|
+
includeLost: booleanSchema(
|
|
9432
|
+
"Include lost contacts (search excludes them by default)."
|
|
9433
|
+
),
|
|
8417
9434
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8418
9435
|
includeDormant: booleanSchema("Whether dormant contacts are included."),
|
|
8419
9436
|
limit: integerSchema("Optional result limit."),
|
|
@@ -8460,11 +9477,112 @@ var init_tool_registry = __esm({
|
|
|
8460
9477
|
}
|
|
8461
9478
|
});
|
|
8462
9479
|
|
|
9480
|
+
// src/surface/plain_dates.ts
|
|
9481
|
+
function parsePlainDue(raw, now = Date.now(), endOfDay = true) {
|
|
9482
|
+
const invalid = () => new CliUsageError(
|
|
9483
|
+
`Invalid timestamp "${raw}". Use YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d, -2d or an ISO date-time.`
|
|
9484
|
+
);
|
|
9485
|
+
if (/^-?\d+$/.test(raw)) {
|
|
9486
|
+
const value = Number(raw);
|
|
9487
|
+
if (Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()))
|
|
9488
|
+
return value;
|
|
9489
|
+
throw invalid();
|
|
9490
|
+
}
|
|
9491
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
9492
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
|
|
9493
|
+
let date;
|
|
9494
|
+
if (iso || european) {
|
|
9495
|
+
const year = Number(iso?.[1] ?? european?.[3]);
|
|
9496
|
+
const month = Number(iso?.[2] ?? european?.[2]);
|
|
9497
|
+
const day = Number(iso?.[3] ?? european?.[1]);
|
|
9498
|
+
date = /* @__PURE__ */ new Date(0);
|
|
9499
|
+
date.setFullYear(year, month - 1, day);
|
|
9500
|
+
date.setHours(
|
|
9501
|
+
endOfDay ? 23 : 0,
|
|
9502
|
+
endOfDay ? 59 : 0,
|
|
9503
|
+
endOfDay ? 59 : 0,
|
|
9504
|
+
endOfDay ? 999 : 0
|
|
9505
|
+
);
|
|
9506
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day)
|
|
9507
|
+
throw invalid();
|
|
9508
|
+
} else if (raw === "today" || raw === "tomorrow" || raw === "yesterday" || /^[+-]\d+d$/.test(raw)) {
|
|
9509
|
+
date = new Date(now);
|
|
9510
|
+
date.setDate(
|
|
9511
|
+
date.getDate() + (raw === "today" ? 0 : raw === "tomorrow" ? 1 : raw === "yesterday" ? -1 : Number(raw.slice(0, -1)))
|
|
9512
|
+
);
|
|
9513
|
+
date.setHours(
|
|
9514
|
+
endOfDay ? 23 : 0,
|
|
9515
|
+
endOfDay ? 59 : 0,
|
|
9516
|
+
endOfDay ? 59 : 0,
|
|
9517
|
+
endOfDay ? 999 : 0
|
|
9518
|
+
);
|
|
9519
|
+
} else {
|
|
9520
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
9521
|
+
raw
|
|
9522
|
+
))
|
|
9523
|
+
throw invalid();
|
|
9524
|
+
parsePlainDue(raw.slice(0, 10), now);
|
|
9525
|
+
date = new Date(raw);
|
|
9526
|
+
}
|
|
9527
|
+
if (!Number.isFinite(date.getTime())) throw invalid();
|
|
9528
|
+
return date.getTime();
|
|
9529
|
+
}
|
|
9530
|
+
function parsePlainStart(raw, now = Date.now()) {
|
|
9531
|
+
return parsePlainDue(raw, now, false);
|
|
9532
|
+
}
|
|
9533
|
+
function normalizeTimeInputs(value, now = Date.now()) {
|
|
9534
|
+
if (Array.isArray(value))
|
|
9535
|
+
return value.map((item) => normalizeTimeInputs(item, now));
|
|
9536
|
+
if (!value || typeof value !== "object") return value;
|
|
9537
|
+
return Object.fromEntries(
|
|
9538
|
+
Object.entries(value).map(([key, item]) => [
|
|
9539
|
+
key,
|
|
9540
|
+
typeof item === "string" && (key.endsWith("At") || ["asOf", "now"].includes(key)) ? key === "dueAt" ? parsePlainDue(item, now) : parsePlainStart(item, now) : normalizeTimeInputs(item, now)
|
|
9541
|
+
])
|
|
9542
|
+
);
|
|
9543
|
+
}
|
|
9544
|
+
var init_plain_dates = __esm({
|
|
9545
|
+
"src/surface/plain_dates.ts"() {
|
|
9546
|
+
init_cli_args();
|
|
9547
|
+
}
|
|
9548
|
+
});
|
|
9549
|
+
|
|
9550
|
+
// src/surface/timestamp_schema.ts
|
|
9551
|
+
function acceptsString(schema) {
|
|
9552
|
+
return schema.type === "string" || Array.isArray(schema.type) && schema.type.includes("string") || (schema.oneOf?.some(acceptsString) ?? false);
|
|
9553
|
+
}
|
|
9554
|
+
function timestampSchema(schema) {
|
|
9555
|
+
return {
|
|
9556
|
+
...schema,
|
|
9557
|
+
...schema.properties ? {
|
|
9558
|
+
properties: Object.fromEntries(
|
|
9559
|
+
Object.entries(schema.properties).map(([key, value]) => [
|
|
9560
|
+
key,
|
|
9561
|
+
key.endsWith("At") || ["asOf", "now"].includes(key) ? acceptsString(value) ? value : {
|
|
9562
|
+
description: value.description,
|
|
9563
|
+
oneOf: [value, { type: "string" }]
|
|
9564
|
+
} : timestampSchema(value)
|
|
9565
|
+
])
|
|
9566
|
+
)
|
|
9567
|
+
} : {},
|
|
9568
|
+
...schema.items ? { items: timestampSchema(schema.items) } : {},
|
|
9569
|
+
...schema.oneOf ? { oneOf: schema.oneOf.map(timestampSchema) } : {}
|
|
9570
|
+
};
|
|
9571
|
+
}
|
|
9572
|
+
var init_timestamp_schema = __esm({
|
|
9573
|
+
"src/surface/timestamp_schema.ts"() {
|
|
9574
|
+
}
|
|
9575
|
+
});
|
|
9576
|
+
|
|
8463
9577
|
// src/surface/affinity_surface_tools.ts
|
|
8464
9578
|
var affinitySurfaceTools;
|
|
8465
9579
|
var init_affinity_surface_tools = __esm({
|
|
8466
9580
|
"src/surface/affinity_surface_tools.ts"() {
|
|
9581
|
+
init_cli_args();
|
|
8467
9582
|
init_tool_registry();
|
|
9583
|
+
init_tool_types();
|
|
9584
|
+
init_plain_dates();
|
|
9585
|
+
init_timestamp_schema();
|
|
8468
9586
|
affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
8469
9587
|
name: tool.name,
|
|
8470
9588
|
description: tool.description,
|
|
@@ -8472,8 +9590,24 @@ var init_affinity_surface_tools = __esm({
|
|
|
8472
9590
|
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
8473
9591
|
readOnly: tool.readOnly,
|
|
8474
9592
|
sideEffects: tool.sideEffects,
|
|
8475
|
-
inputSchema: {
|
|
8476
|
-
|
|
9593
|
+
inputSchema: {
|
|
9594
|
+
...timestampSchema({ ...tool.inputSchema, type: "object" }),
|
|
9595
|
+
type: "object"
|
|
9596
|
+
},
|
|
9597
|
+
handler: (db, input) => {
|
|
9598
|
+
try {
|
|
9599
|
+
return tool.handler(db, normalizeTimeInputs(input));
|
|
9600
|
+
} catch (error) {
|
|
9601
|
+
if (error instanceof CliUsageError)
|
|
9602
|
+
return toolFailure(
|
|
9603
|
+
"protocol",
|
|
9604
|
+
"invalid_input",
|
|
9605
|
+
"Invalid input.",
|
|
9606
|
+
error.message
|
|
9607
|
+
);
|
|
9608
|
+
throw error;
|
|
9609
|
+
}
|
|
9610
|
+
}
|
|
8477
9611
|
}));
|
|
8478
9612
|
}
|
|
8479
9613
|
});
|
|
@@ -8796,6 +9930,12 @@ function initLinkEventEffectsTables(db) {
|
|
|
8796
9930
|
)
|
|
8797
9931
|
)
|
|
8798
9932
|
`);
|
|
9933
|
+
const columns = db.prepare("PRAGMA table_info(link_event_effects)").all();
|
|
9934
|
+
if (!columns.some((column) => column.name === "counts_for_recency")) {
|
|
9935
|
+
db.exec(
|
|
9936
|
+
"ALTER TABLE link_event_effects ADD COLUMN counts_for_recency INTEGER NOT NULL DEFAULT 1"
|
|
9937
|
+
);
|
|
9938
|
+
}
|
|
8799
9939
|
db.exec(
|
|
8800
9940
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
8801
9941
|
);
|
|
@@ -9027,7 +10167,8 @@ var init_open_affinity_db = __esm({
|
|
|
9027
10167
|
}
|
|
9028
10168
|
});
|
|
9029
10169
|
function resolveAffinityDbPath(options = {}) {
|
|
9030
|
-
|
|
10170
|
+
const path = options.flag ?? ((options.env ?? process.env).GHOSTPAW_AFFINITY_DB || join(homedir(), ".ghostpaw", "affinity.db"));
|
|
10171
|
+
return path === ":memory:" ? path : resolve(path);
|
|
9031
10172
|
}
|
|
9032
10173
|
var init_resolve_affinity_db_path = __esm({
|
|
9033
10174
|
"src/surface/resolve_affinity_db_path.ts"() {
|