@ghostpaw/affinity 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -114
- package/SKILL.md +217 -33
- package/dist/cli.js +2433 -913
- package/dist/index.cjs +1397 -504
- package/dist/index.d.cts +556 -55
- package/dist/index.d.ts +556 -55
- package/dist/index.js +1397 -504
- package/dist/mcp-server.js +1811 -694
- package/dist/mcp.cjs +1604 -606
- package/dist/mcp.js +1605 -607
- package/package.json +1 -1
- package/skills/affinity/SKILL.md +217 -33
package/dist/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.",
|
|
@@ -2763,9 +3164,17 @@ var init_unset_attribute = __esm({
|
|
|
2763
3164
|
});
|
|
2764
3165
|
|
|
2765
3166
|
// src/tools/tool_mutation.ts
|
|
2766
|
-
function mutationToolResult(action, receipt, entitySummary) {
|
|
3167
|
+
function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
2767
3168
|
const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
|
|
2768
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
|
+
),
|
|
2769
3178
|
action,
|
|
2770
3179
|
primary: receipt.primary,
|
|
2771
3180
|
created: receipt.created,
|
|
@@ -2859,7 +3268,23 @@ function resolveAttributeTarget(db, target) {
|
|
|
2859
3268
|
}
|
|
2860
3269
|
return { kind: "ok", target: { kind: "link", id: link.value.id } };
|
|
2861
3270
|
}
|
|
2862
|
-
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
|
+
);
|
|
2863
3288
|
return withToolHandling(() => {
|
|
2864
3289
|
const resolved = resolveAttributeTarget(db, input.target);
|
|
2865
3290
|
if (resolved.kind === "error") {
|
|
@@ -2897,6 +3322,8 @@ var init_manage_attribute_tool = __esm({
|
|
|
2897
3322
|
init_tool_mutation();
|
|
2898
3323
|
init_tool_names();
|
|
2899
3324
|
init_tool_resolvers();
|
|
3325
|
+
init_tool_types();
|
|
3326
|
+
init_validate_tool_input();
|
|
2900
3327
|
manageAttributeTool = defineAffinityTool({
|
|
2901
3328
|
name: manageAttributeToolName,
|
|
2902
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.",
|
|
@@ -3529,7 +3956,7 @@ function clamp3(value, min, max) {
|
|
|
3529
3956
|
}
|
|
3530
3957
|
function countSameDaySimilarEvents(db, linkId, eventType, occurredAt) {
|
|
3531
3958
|
const dayStart = utcStartOfDayMs(occurredAt);
|
|
3532
|
-
const dayEnd = dayStart +
|
|
3959
|
+
const dayEnd = dayStart + DAY_MS3;
|
|
3533
3960
|
const row = db.prepare(
|
|
3534
3961
|
`SELECT COUNT(*) AS c
|
|
3535
3962
|
FROM link_event_effects le
|
|
@@ -3542,7 +3969,7 @@ function countSameDaySimilarEvents(db, linkId, eventType, occurredAt) {
|
|
|
3542
3969
|
return Number(row.c ?? 0);
|
|
3543
3970
|
}
|
|
3544
3971
|
function weeklyPositiveBaseWeight(db, linkId, occurredAt) {
|
|
3545
|
-
const weekStart = occurredAt - 7 *
|
|
3972
|
+
const weekStart = occurredAt - 7 * DAY_MS3;
|
|
3546
3973
|
const row = db.prepare(
|
|
3547
3974
|
`SELECT SUM(base_weight) AS total
|
|
3548
3975
|
FROM link_event_effects
|
|
@@ -3554,7 +3981,7 @@ function weeklyPositiveBaseWeight(db, linkId, occurredAt) {
|
|
|
3554
3981
|
return Number(row.total ?? 0);
|
|
3555
3982
|
}
|
|
3556
3983
|
function countRepairContext(db, linkId, occurredAt) {
|
|
3557
|
-
const thirtyDaysAgo = occurredAt - 30 *
|
|
3984
|
+
const thirtyDaysAgo = occurredAt - 30 * DAY_MS3;
|
|
3558
3985
|
const hasPriorDamage = db.prepare(
|
|
3559
3986
|
`SELECT 1 FROM link_event_effects
|
|
3560
3987
|
WHERE link_id = ? AND trust_delta < 0
|
|
@@ -3629,8 +4056,8 @@ function computePreferenceMatch(db, linkId, contactId, eventType, provenance) {
|
|
|
3629
4056
|
return preferenceMatch(liked, disliked);
|
|
3630
4057
|
}
|
|
3631
4058
|
function computeDateSalienceMultiplier(db, contactId, occurredAt, significance) {
|
|
3632
|
-
const windowBefore = 7 *
|
|
3633
|
-
const windowAfter = 2 *
|
|
4059
|
+
const windowBefore = 7 * DAY_MS3;
|
|
4060
|
+
const windowAfter = 2 * DAY_MS3;
|
|
3634
4061
|
const row = db.prepare(
|
|
3635
4062
|
`SELECT 1 FROM upcoming_occurrences uo
|
|
3636
4063
|
JOIN events e ON e.id = uo.event_id
|
|
@@ -3797,9 +4224,9 @@ function applyEventEffectToLink(db, params) {
|
|
|
3797
4224
|
).get(params.linkId);
|
|
3798
4225
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
3799
4226
|
1,
|
|
3800
|
-
Math.round((params.occurredAt - latest.occurred_at) /
|
|
4227
|
+
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
3801
4228
|
);
|
|
3802
|
-
const cadenceAfter = nextCadenceDays({
|
|
4229
|
+
const cadenceAfter = commitmentResolution === "broken" || commitmentResolution === "cancelled" ? cadenceBefore : nextCadenceDays({
|
|
3803
4230
|
kind: linkKind,
|
|
3804
4231
|
currentCadenceDays: cadenceBefore,
|
|
3805
4232
|
gapDays
|
|
@@ -3862,6 +4289,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
3862
4289
|
params.now,
|
|
3863
4290
|
params.now
|
|
3864
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
|
+
}
|
|
3865
4297
|
refreshLinkRollup(db, params.linkId, params.now);
|
|
3866
4298
|
return {
|
|
3867
4299
|
linkId: params.linkId,
|
|
@@ -3885,7 +4317,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3885
4317
|
momentKind
|
|
3886
4318
|
};
|
|
3887
4319
|
}
|
|
3888
|
-
var
|
|
4320
|
+
var DAY_MS3;
|
|
3889
4321
|
var init_effects = __esm({
|
|
3890
4322
|
"src/links/effects.ts"() {
|
|
3891
4323
|
init_calendar();
|
|
@@ -3895,7 +4327,7 @@ var init_effects = __esm({
|
|
|
3895
4327
|
init_queries3();
|
|
3896
4328
|
init_rollups2();
|
|
3897
4329
|
init_validators2();
|
|
3898
|
-
|
|
4330
|
+
DAY_MS3 = 864e5;
|
|
3899
4331
|
}
|
|
3900
4332
|
});
|
|
3901
4333
|
|
|
@@ -4276,12 +4708,16 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4276
4708
|
if (row.type !== "promise" && row.type !== "agreement") {
|
|
4277
4709
|
throw new AffinityInvariantError("not a commitment event");
|
|
4278
4710
|
}
|
|
4279
|
-
const open = db.prepare(
|
|
4711
|
+
const open = db.prepare(
|
|
4712
|
+
"SELECT resolved_at, resolution FROM open_commitments WHERE event_id = ?"
|
|
4713
|
+
).get(commitmentEventId);
|
|
4280
4714
|
if (!open) {
|
|
4281
4715
|
throw new AffinityNotFoundError("open commitment not found");
|
|
4282
4716
|
}
|
|
4283
4717
|
if (open.resolved_at !== null) {
|
|
4284
|
-
throw new
|
|
4718
|
+
throw new AffinityConflictError(
|
|
4719
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4720
|
+
);
|
|
4285
4721
|
}
|
|
4286
4722
|
const now = resolveNow(options?.now);
|
|
4287
4723
|
const originalParticipants = loadEventParticipantViews(
|
|
@@ -4294,7 +4730,9 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4294
4730
|
WHERE event_id = ? AND resolved_at IS NULL`
|
|
4295
4731
|
).run(resolution, now, now, commitmentEventId);
|
|
4296
4732
|
if (Number(result.changes ?? 0) === 0) {
|
|
4297
|
-
throw new
|
|
4733
|
+
throw new AffinityConflictError(
|
|
4734
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4735
|
+
);
|
|
4298
4736
|
}
|
|
4299
4737
|
const shouldCreateResolvingEvent = options?.resolvingEvent !== void 0 || resolution === "broken";
|
|
4300
4738
|
const created = [];
|
|
@@ -4364,9 +4802,9 @@ var init_resolve_commitment = __esm({
|
|
|
4364
4802
|
init_queries2();
|
|
4365
4803
|
init_receipts2();
|
|
4366
4804
|
init_validators();
|
|
4805
|
+
init_affinity_conflict_error();
|
|
4367
4806
|
init_affinity_invariant_error();
|
|
4368
4807
|
init_affinity_not_found_error();
|
|
4369
|
-
init_affinity_state_error();
|
|
4370
4808
|
init_affinity_validation_error();
|
|
4371
4809
|
init_resolve_now();
|
|
4372
4810
|
init_with_transaction();
|
|
@@ -4399,9 +4837,25 @@ function ensureOwnerParticipates(db, participants) {
|
|
|
4399
4837
|
function prepareParticipants(db, raw) {
|
|
4400
4838
|
return ensureOwnerParticipates(db, raw.map(normalizeParticipant));
|
|
4401
4839
|
}
|
|
4402
|
-
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
|
+
);
|
|
4403
4850
|
return withToolHandling(() => {
|
|
4404
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
|
+
);
|
|
4405
4859
|
const patched = {
|
|
4406
4860
|
...input.input,
|
|
4407
4861
|
participants: prepareParticipants(
|
|
@@ -4437,6 +4891,8 @@ var init_manage_commitment_tool = __esm({
|
|
|
4437
4891
|
init_tool_mutation();
|
|
4438
4892
|
init_tool_names();
|
|
4439
4893
|
init_tool_resolvers();
|
|
4894
|
+
init_tool_types();
|
|
4895
|
+
init_validate_tool_input();
|
|
4440
4896
|
manageCommitmentTool = defineAffinityTool({
|
|
4441
4897
|
name: manageCommitmentToolName,
|
|
4442
4898
|
description: "Record or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.",
|
|
@@ -4478,7 +4934,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4478
4934
|
role: stringSchema("Participant role."),
|
|
4479
4935
|
directionality: stringSchema("Optional directionality.")
|
|
4480
4936
|
},
|
|
4481
|
-
["
|
|
4937
|
+
["role"]
|
|
4482
4938
|
),
|
|
4483
4939
|
"Participant list. Must be non-empty and include the owner contact."
|
|
4484
4940
|
),
|
|
@@ -4527,6 +4983,163 @@ var init_manage_commitment_tool = __esm({
|
|
|
4527
4983
|
}
|
|
4528
4984
|
});
|
|
4529
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
|
+
|
|
4530
5143
|
// src/contacts/receipts.ts
|
|
4531
5144
|
function buildContactMutationReceipt(primary, partial) {
|
|
4532
5145
|
const normalized = normalizeMutationReceiptPartial(partial);
|
|
@@ -4558,73 +5171,6 @@ var init_receipts4 = __esm({
|
|
|
4558
5171
|
}
|
|
4559
5172
|
});
|
|
4560
5173
|
|
|
4561
|
-
// src/contacts/create_contact.ts
|
|
4562
|
-
function createContact(db, input) {
|
|
4563
|
-
return withTransaction(db, () => {
|
|
4564
|
-
const name = input.name.trim();
|
|
4565
|
-
if (name.length === 0) {
|
|
4566
|
-
throw new AffinityValidationError("name must be non-empty");
|
|
4567
|
-
}
|
|
4568
|
-
if (!KINDS.has(input.kind)) {
|
|
4569
|
-
throw new AffinityValidationError("invalid contact kind");
|
|
4570
|
-
}
|
|
4571
|
-
if (input.isOwner !== void 0) {
|
|
4572
|
-
throw new AffinityValidationError(
|
|
4573
|
-
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
4574
|
-
);
|
|
4575
|
-
}
|
|
4576
|
-
const now = resolveNow(input.now);
|
|
4577
|
-
if (input.bootstrapOwner) {
|
|
4578
|
-
const existing = db.prepare(
|
|
4579
|
-
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
4580
|
-
).get();
|
|
4581
|
-
if (existing !== void 0) {
|
|
4582
|
-
throw new AffinityConflictError(
|
|
4583
|
-
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
4584
|
-
);
|
|
4585
|
-
}
|
|
4586
|
-
}
|
|
4587
|
-
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
4588
|
-
const result = db.prepare(
|
|
4589
|
-
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
4590
|
-
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
4591
|
-
).run(name, input.kind, isOwner, now, now);
|
|
4592
|
-
const id = Number(result.lastInsertRowid);
|
|
4593
|
-
const row = getContactRowById(db, id);
|
|
4594
|
-
if (!row) {
|
|
4595
|
-
throw new AffinityInvariantError("inserted contact row not found");
|
|
4596
|
-
}
|
|
4597
|
-
refreshContactRollup(db, id, now);
|
|
4598
|
-
const primary = mapContactRowToContactListItem(row);
|
|
4599
|
-
return buildContactMutationReceipt(primary, {
|
|
4600
|
-
created: [{ kind: "contact", id }]
|
|
4601
|
-
});
|
|
4602
|
-
});
|
|
4603
|
-
}
|
|
4604
|
-
var KINDS;
|
|
4605
|
-
var init_create_contact = __esm({
|
|
4606
|
-
"src/contacts/create_contact.ts"() {
|
|
4607
|
-
init_mappers4();
|
|
4608
|
-
init_queries();
|
|
4609
|
-
init_receipts4();
|
|
4610
|
-
init_rollups();
|
|
4611
|
-
init_affinity_conflict_error();
|
|
4612
|
-
init_affinity_invariant_error();
|
|
4613
|
-
init_affinity_validation_error();
|
|
4614
|
-
init_resolve_now();
|
|
4615
|
-
init_with_transaction();
|
|
4616
|
-
KINDS = /* @__PURE__ */ new Set([
|
|
4617
|
-
"human",
|
|
4618
|
-
"group",
|
|
4619
|
-
"company",
|
|
4620
|
-
"team",
|
|
4621
|
-
"pet",
|
|
4622
|
-
"service",
|
|
4623
|
-
"other"
|
|
4624
|
-
]);
|
|
4625
|
-
}
|
|
4626
|
-
});
|
|
4627
|
-
|
|
4628
5174
|
// src/contacts/revise_contact.ts
|
|
4629
5175
|
function reviseContact(db, contactId, patch) {
|
|
4630
5176
|
return withTransaction(db, () => {
|
|
@@ -4680,7 +5226,9 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
4680
5226
|
throw new AffinityStateError("merged contact is read-only");
|
|
4681
5227
|
}
|
|
4682
5228
|
if (to === "merged") {
|
|
4683
|
-
throw new AffinityInvariantError(
|
|
5229
|
+
throw new AffinityInvariantError(
|
|
5230
|
+
"use manage_contact merge (CLI: contact merge) to reach merged state"
|
|
5231
|
+
);
|
|
4684
5232
|
}
|
|
4685
5233
|
if (from === to) {
|
|
4686
5234
|
return;
|
|
@@ -4713,42 +5261,360 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
4713
5261
|
}
|
|
4714
5262
|
const row = getContactRowById(db, contactId);
|
|
4715
5263
|
if (!row) {
|
|
4716
|
-
throw new AffinityNotFoundError("contact not found");
|
|
4717
|
-
}
|
|
4718
|
-
const from = row.lifecycle_state;
|
|
4719
|
-
assertValidLifecycleTransition(from, lifecycleState);
|
|
4720
|
-
const now = resolveNow(options?.now);
|
|
4721
|
-
if (from === lifecycleState) {
|
|
4722
|
-
return buildContactMutationReceipt(mapContactRowToContactListItem(row), {
|
|
4723
|
-
updated: []
|
|
4724
|
-
});
|
|
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");
|
|
4725
5543
|
}
|
|
4726
|
-
db.prepare(
|
|
4727
|
-
"UPDATE contacts SET lifecycle_state = ?, updated_at = ? WHERE id = ?"
|
|
4728
|
-
).run(lifecycleState, now, contactId);
|
|
4729
5544
|
refreshContactRollup(db, contactId, now);
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
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 }]
|
|
4736
5550
|
});
|
|
4737
5551
|
});
|
|
4738
5552
|
}
|
|
4739
|
-
var
|
|
4740
|
-
|
|
4741
|
-
"src/contacts/set_contact_lifecycle.ts"() {
|
|
4742
|
-
init_mappers4();
|
|
5553
|
+
var init_add_identity = __esm({
|
|
5554
|
+
"src/identities/add_identity.ts"() {
|
|
4743
5555
|
init_queries();
|
|
4744
|
-
init_receipts4();
|
|
4745
5556
|
init_rollups();
|
|
4746
|
-
|
|
5557
|
+
init_mappers2();
|
|
5558
|
+
init_normalize();
|
|
5559
|
+
init_queries5();
|
|
5560
|
+
init_receipts5();
|
|
5561
|
+
init_validators5();
|
|
5562
|
+
init_affinity_invariant_error();
|
|
4747
5563
|
init_affinity_not_found_error();
|
|
5564
|
+
init_affinity_state_error();
|
|
4748
5565
|
init_affinity_validation_error();
|
|
4749
5566
|
init_resolve_now();
|
|
4750
5567
|
init_with_transaction();
|
|
4751
|
-
|
|
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();
|
|
4752
5618
|
}
|
|
4753
5619
|
});
|
|
4754
5620
|
|
|
@@ -4770,24 +5636,76 @@ function contactLocatorSchema3(description) {
|
|
|
4770
5636
|
description
|
|
4771
5637
|
};
|
|
4772
5638
|
}
|
|
4773
|
-
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
|
+
);
|
|
4774
5649
|
return withToolHandling(() => {
|
|
4775
|
-
if (input.action === "
|
|
4776
|
-
|
|
4777
|
-
"
|
|
4778
|
-
|
|
4779
|
-
|
|
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)"
|
|
4780
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);
|
|
4781
5679
|
}
|
|
4782
|
-
const contact = resolveContactLocator(
|
|
5680
|
+
const contact = resolveContactLocator(
|
|
5681
|
+
db,
|
|
5682
|
+
input.contact,
|
|
5683
|
+
"contact",
|
|
5684
|
+
input.action === "delete"
|
|
5685
|
+
);
|
|
4783
5686
|
if (!contact.ok) {
|
|
4784
5687
|
return contact.result;
|
|
4785
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
|
+
}
|
|
4786
5703
|
if (input.action === "revise") {
|
|
4787
5704
|
return mutationToolResult(
|
|
4788
5705
|
"revise",
|
|
4789
5706
|
reviseContact(db, contact.value.id, input.patch),
|
|
4790
|
-
"contact"
|
|
5707
|
+
"contact",
|
|
5708
|
+
Object.keys(input.patch)
|
|
4791
5709
|
);
|
|
4792
5710
|
}
|
|
4793
5711
|
return mutationToolResult(
|
|
@@ -4805,16 +5723,22 @@ function manageContactToolHandler(db, input) {
|
|
|
4805
5723
|
var manageContactTool;
|
|
4806
5724
|
var init_manage_contact_tool = __esm({
|
|
4807
5725
|
"src/tools/manage_contact_tool.ts"() {
|
|
4808
|
-
|
|
5726
|
+
init_delete_contact();
|
|
5727
|
+
init_export_affinity();
|
|
5728
|
+
init_export_contact();
|
|
4809
5729
|
init_revise_contact();
|
|
4810
5730
|
init_set_contact_lifecycle();
|
|
5731
|
+
init_create_contact_tool_result();
|
|
4811
5732
|
init_tool_metadata();
|
|
4812
5733
|
init_tool_mutation();
|
|
4813
5734
|
init_tool_names();
|
|
4814
5735
|
init_tool_resolvers();
|
|
5736
|
+
init_tool_types();
|
|
5737
|
+
init_tool_types();
|
|
5738
|
+
init_validate_tool_input();
|
|
4815
5739
|
manageContactTool = defineAffinityTool({
|
|
4816
5740
|
name: manageContactToolName,
|
|
4817
|
-
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.",
|
|
4818
5742
|
whenToUse: "Use this for contact creation and lifecycle-safe contact maintenance.",
|
|
4819
5743
|
whenNotToUse: "Do not use this for identities, links, attributes, or merges.",
|
|
4820
5744
|
sideEffects: "writes_state",
|
|
@@ -4835,10 +5759,25 @@ var init_manage_contact_tool = __esm({
|
|
|
4835
5759
|
action: enumSchema("Operation to perform.", [
|
|
4836
5760
|
"create",
|
|
4837
5761
|
"revise",
|
|
4838
|
-
"set_lifecycle"
|
|
5762
|
+
"set_lifecycle",
|
|
5763
|
+
"export",
|
|
5764
|
+
"delete"
|
|
4839
5765
|
]),
|
|
4840
5766
|
input: objectSchema(
|
|
4841
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
|
+
),
|
|
4842
5781
|
name: stringSchema("Contact name."),
|
|
4843
5782
|
kind: enumSchema("Contact kind.", [
|
|
4844
5783
|
"human",
|
|
@@ -4857,6 +5796,7 @@ var init_manage_contact_tool = __esm({
|
|
|
4857
5796
|
["name", "kind"],
|
|
4858
5797
|
"New contact payload. Required when action=create."
|
|
4859
5798
|
),
|
|
5799
|
+
confirm: booleanSchema("Required true for permanent deletion."),
|
|
4860
5800
|
contact: contactLocatorSchema3(
|
|
4861
5801
|
"Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity."
|
|
4862
5802
|
),
|
|
@@ -5126,9 +6066,32 @@ function resolveDateAnchorTarget(db, target) {
|
|
|
5126
6066
|
}
|
|
5127
6067
|
return { kind: "ok", target: { kind: "link", linkId: link.value.id } };
|
|
5128
6068
|
}
|
|
5129
|
-
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
|
+
);
|
|
5130
6086
|
return withToolHandling(() => {
|
|
5131
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
|
+
);
|
|
5132
6095
|
const resolved = resolveDateAnchorTarget(db, input.input.target);
|
|
5133
6096
|
if (resolved.kind === "error") {
|
|
5134
6097
|
return resolved.result;
|
|
@@ -5143,7 +6106,8 @@ function manageDateAnchorToolHandler(db, input) {
|
|
|
5143
6106
|
return mutationToolResult(
|
|
5144
6107
|
"revise",
|
|
5145
6108
|
reviseDateAnchor(db, input.anchorEventId, input.patch, input.options),
|
|
5146
|
-
"date anchor"
|
|
6109
|
+
"date anchor",
|
|
6110
|
+
Object.keys(input.patch)
|
|
5147
6111
|
);
|
|
5148
6112
|
}
|
|
5149
6113
|
return mutationToolResult(
|
|
@@ -5163,6 +6127,8 @@ var init_manage_date_anchor_tool = __esm({
|
|
|
5163
6127
|
init_tool_mutation();
|
|
5164
6128
|
init_tool_names();
|
|
5165
6129
|
init_tool_resolvers();
|
|
6130
|
+
init_tool_types();
|
|
6131
|
+
init_validate_tool_input();
|
|
5166
6132
|
manageDateAnchorTool = defineAffinityTool({
|
|
5167
6133
|
name: manageDateAnchorToolName,
|
|
5168
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.",
|
|
@@ -5276,7 +6242,7 @@ function buildDismissalMutationReceipt(primary, partial) {
|
|
|
5276
6242
|
derivedEffects: normalized.derivedEffects
|
|
5277
6243
|
};
|
|
5278
6244
|
}
|
|
5279
|
-
var
|
|
6245
|
+
var init_receipts6 = __esm({
|
|
5280
6246
|
"src/merges/receipts.ts"() {
|
|
5281
6247
|
init_normalize_mutation_receipt();
|
|
5282
6248
|
}
|
|
@@ -5338,7 +6304,7 @@ var init_dismiss_duplicate = __esm({
|
|
|
5338
6304
|
init_affinity_validation_error();
|
|
5339
6305
|
init_resolve_now();
|
|
5340
6306
|
init_with_transaction();
|
|
5341
|
-
|
|
6307
|
+
init_receipts6();
|
|
5342
6308
|
}
|
|
5343
6309
|
});
|
|
5344
6310
|
|
|
@@ -5377,7 +6343,7 @@ var init_undismiss_duplicate = __esm({
|
|
|
5377
6343
|
init_affinity_not_found_error();
|
|
5378
6344
|
init_affinity_validation_error();
|
|
5379
6345
|
init_with_transaction();
|
|
5380
|
-
|
|
6346
|
+
init_receipts6();
|
|
5381
6347
|
}
|
|
5382
6348
|
});
|
|
5383
6349
|
|
|
@@ -5399,7 +6365,19 @@ function contactLocatorSchema5(description) {
|
|
|
5399
6365
|
description
|
|
5400
6366
|
};
|
|
5401
6367
|
}
|
|
5402
|
-
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
|
+
);
|
|
5403
6381
|
return withToolHandling(() => {
|
|
5404
6382
|
const left = resolveContactLocator(db, input.left, "left");
|
|
5405
6383
|
if (!left.ok) {
|
|
@@ -5409,6 +6387,11 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
5409
6387
|
if (!right.ok) {
|
|
5410
6388
|
return right.result;
|
|
5411
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;
|
|
5412
6395
|
if (input.action === "dismiss") {
|
|
5413
6396
|
const receipt2 = dismissDuplicate(
|
|
5414
6397
|
db,
|
|
@@ -5418,13 +6401,35 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
5418
6401
|
);
|
|
5419
6402
|
return toolSuccess(
|
|
5420
6403
|
`Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
|
|
5421
|
-
{
|
|
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
|
+
}
|
|
5422
6416
|
);
|
|
5423
6417
|
}
|
|
5424
6418
|
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
5425
6419
|
return toolSuccess(
|
|
5426
6420
|
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
5427
|
-
{
|
|
6421
|
+
{
|
|
6422
|
+
...receipt,
|
|
6423
|
+
action: "undismiss",
|
|
6424
|
+
changes: existed ? [
|
|
6425
|
+
{
|
|
6426
|
+
entity: "duplicate_dismissal",
|
|
6427
|
+
leftContactId,
|
|
6428
|
+
rightContactId,
|
|
6429
|
+
action: "removed"
|
|
6430
|
+
}
|
|
6431
|
+
] : []
|
|
6432
|
+
}
|
|
5428
6433
|
);
|
|
5429
6434
|
}, "Duplicate dismissal failed.");
|
|
5430
6435
|
}
|
|
@@ -5437,9 +6442,11 @@ var init_manage_duplicate_dismissal_tool = __esm({
|
|
|
5437
6442
|
init_tool_names();
|
|
5438
6443
|
init_tool_resolvers();
|
|
5439
6444
|
init_tool_types();
|
|
6445
|
+
init_tool_types();
|
|
6446
|
+
init_validate_tool_input();
|
|
5440
6447
|
manageDuplicateDismissalTool = defineAffinityTool({
|
|
5441
6448
|
name: manageDuplicateDismissalToolName,
|
|
5442
|
-
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.",
|
|
5443
6450
|
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
5444
6451
|
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
5445
6452
|
sideEffects: "writes_state",
|
|
@@ -5472,128 +6479,6 @@ var init_manage_duplicate_dismissal_tool = __esm({
|
|
|
5472
6479
|
}
|
|
5473
6480
|
});
|
|
5474
6481
|
|
|
5475
|
-
// src/identities/queries.ts
|
|
5476
|
-
function getIdentityRowById(db, id) {
|
|
5477
|
-
const row = db.prepare(
|
|
5478
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5479
|
-
created_at, updated_at, removed_at
|
|
5480
|
-
FROM identities
|
|
5481
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
5482
|
-
).get(id);
|
|
5483
|
-
if (row === void 0) {
|
|
5484
|
-
return null;
|
|
5485
|
-
}
|
|
5486
|
-
return row;
|
|
5487
|
-
}
|
|
5488
|
-
var init_queries5 = __esm({
|
|
5489
|
-
"src/identities/queries.ts"() {
|
|
5490
|
-
}
|
|
5491
|
-
});
|
|
5492
|
-
|
|
5493
|
-
// src/identities/receipts.ts
|
|
5494
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
5495
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5496
|
-
return {
|
|
5497
|
-
primary,
|
|
5498
|
-
created: normalized.created,
|
|
5499
|
-
updated: normalized.updated,
|
|
5500
|
-
archived: normalized.archived,
|
|
5501
|
-
removed: normalized.removed,
|
|
5502
|
-
affectedLinks: normalized.affectedLinks,
|
|
5503
|
-
derivedEffects: normalized.derivedEffects
|
|
5504
|
-
};
|
|
5505
|
-
}
|
|
5506
|
-
var init_receipts6 = __esm({
|
|
5507
|
-
"src/identities/receipts.ts"() {
|
|
5508
|
-
init_normalize_mutation_receipt();
|
|
5509
|
-
}
|
|
5510
|
-
});
|
|
5511
|
-
|
|
5512
|
-
// src/identities/validators.ts
|
|
5513
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5514
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5515
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5516
|
-
).get(normalizedKey) : db.prepare(
|
|
5517
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
5518
|
-
).get(normalizedKey, excludeIdentityId);
|
|
5519
|
-
if (row !== void 0) {
|
|
5520
|
-
throw new AffinityConflictError(
|
|
5521
|
-
"another contact already owns this normalized identity"
|
|
5522
|
-
);
|
|
5523
|
-
}
|
|
5524
|
-
}
|
|
5525
|
-
var init_validators5 = __esm({
|
|
5526
|
-
"src/identities/validators.ts"() {
|
|
5527
|
-
init_affinity_conflict_error();
|
|
5528
|
-
}
|
|
5529
|
-
});
|
|
5530
|
-
|
|
5531
|
-
// src/identities/add_identity.ts
|
|
5532
|
-
function addIdentity(db, contactId, input) {
|
|
5533
|
-
return withTransaction(db, () => {
|
|
5534
|
-
const contact = getContactRowById(db, contactId);
|
|
5535
|
-
if (!contact) {
|
|
5536
|
-
throw new AffinityNotFoundError("contact not found");
|
|
5537
|
-
}
|
|
5538
|
-
if (contact.lifecycle_state === "merged") {
|
|
5539
|
-
throw new AffinityStateError("merged contact is read-only");
|
|
5540
|
-
}
|
|
5541
|
-
const type = input.type.trim();
|
|
5542
|
-
const value = input.value.trim();
|
|
5543
|
-
if (type.length === 0 || value.length === 0) {
|
|
5544
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
5545
|
-
}
|
|
5546
|
-
const nk = normalizeIdentityKey(type, value);
|
|
5547
|
-
assertNoIdentityCollision(db, nk);
|
|
5548
|
-
const now = resolveNow(input.now);
|
|
5549
|
-
const verified = input.verified ? 1 : 0;
|
|
5550
|
-
const verifiedAt = verified === 1 ? now : null;
|
|
5551
|
-
const result = db.prepare(
|
|
5552
|
-
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
5553
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5554
|
-
).run(
|
|
5555
|
-
contactId,
|
|
5556
|
-
type,
|
|
5557
|
-
value,
|
|
5558
|
-
input.label ?? null,
|
|
5559
|
-
nk,
|
|
5560
|
-
verified,
|
|
5561
|
-
verifiedAt,
|
|
5562
|
-
now,
|
|
5563
|
-
now
|
|
5564
|
-
);
|
|
5565
|
-
const id = Number(result.lastInsertRowid);
|
|
5566
|
-
const row = getIdentityRowById(db, id);
|
|
5567
|
-
if (!row) {
|
|
5568
|
-
throw new AffinityInvariantError("inserted identity not found");
|
|
5569
|
-
}
|
|
5570
|
-
refreshContactRollup(db, contactId, now);
|
|
5571
|
-
db.prepare(
|
|
5572
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
5573
|
-
).run(contactId, contactId);
|
|
5574
|
-
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
5575
|
-
created: [{ kind: "identity", id }]
|
|
5576
|
-
});
|
|
5577
|
-
});
|
|
5578
|
-
}
|
|
5579
|
-
var init_add_identity = __esm({
|
|
5580
|
-
"src/identities/add_identity.ts"() {
|
|
5581
|
-
init_queries();
|
|
5582
|
-
init_rollups();
|
|
5583
|
-
init_mappers2();
|
|
5584
|
-
init_normalize();
|
|
5585
|
-
init_queries5();
|
|
5586
|
-
init_receipts6();
|
|
5587
|
-
init_validators5();
|
|
5588
|
-
init_affinity_invariant_error();
|
|
5589
|
-
init_affinity_not_found_error();
|
|
5590
|
-
init_affinity_state_error();
|
|
5591
|
-
init_affinity_validation_error();
|
|
5592
|
-
init_resolve_now();
|
|
5593
|
-
init_with_transaction();
|
|
5594
|
-
}
|
|
5595
|
-
});
|
|
5596
|
-
|
|
5597
6482
|
// src/identities/remove_identity.ts
|
|
5598
6483
|
function removeIdentity(db, identityId, removedAt) {
|
|
5599
6484
|
return withTransaction(db, () => {
|
|
@@ -5625,7 +6510,7 @@ var init_remove_identity = __esm({
|
|
|
5625
6510
|
init_rollups();
|
|
5626
6511
|
init_mappers2();
|
|
5627
6512
|
init_queries5();
|
|
5628
|
-
|
|
6513
|
+
init_receipts5();
|
|
5629
6514
|
init_affinity_not_found_error();
|
|
5630
6515
|
init_affinity_state_error();
|
|
5631
6516
|
init_resolve_now();
|
|
@@ -5658,6 +6543,7 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
5658
6543
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
5659
6544
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5660
6545
|
}
|
|
6546
|
+
validateIdentityInput(nextType, nextValue);
|
|
5661
6547
|
const nextLabel = patch.label !== void 0 ? patch.label : row.label;
|
|
5662
6548
|
const nkNext = normalizeIdentityKey(nextType, nextValue);
|
|
5663
6549
|
if (nkNext !== row.normalized_key) {
|
|
@@ -5683,13 +6569,14 @@ var init_revise_identity = __esm({
|
|
|
5683
6569
|
init_mappers2();
|
|
5684
6570
|
init_normalize();
|
|
5685
6571
|
init_queries5();
|
|
5686
|
-
|
|
6572
|
+
init_receipts5();
|
|
5687
6573
|
init_validators5();
|
|
5688
6574
|
init_affinity_not_found_error();
|
|
5689
6575
|
init_affinity_state_error();
|
|
5690
6576
|
init_affinity_validation_error();
|
|
5691
6577
|
init_resolve_now();
|
|
5692
6578
|
init_with_transaction();
|
|
6579
|
+
init_validate_identity_input();
|
|
5693
6580
|
}
|
|
5694
6581
|
});
|
|
5695
6582
|
|
|
@@ -5723,7 +6610,7 @@ var init_verify_identity = __esm({
|
|
|
5723
6610
|
init_queries();
|
|
5724
6611
|
init_mappers2();
|
|
5725
6612
|
init_queries5();
|
|
5726
|
-
|
|
6613
|
+
init_receipts5();
|
|
5727
6614
|
init_affinity_not_found_error();
|
|
5728
6615
|
init_affinity_state_error();
|
|
5729
6616
|
init_resolve_now();
|
|
@@ -5739,7 +6626,9 @@ function contactLocatorSchema6(description) {
|
|
|
5739
6626
|
contactId: integerSchema("Exact contact id."),
|
|
5740
6627
|
identity: objectSchema(
|
|
5741
6628
|
{
|
|
5742
|
-
type: stringSchema(
|
|
6629
|
+
type: stringSchema(
|
|
6630
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6631
|
+
),
|
|
5743
6632
|
value: stringSchema("Identity value.")
|
|
5744
6633
|
},
|
|
5745
6634
|
["type", "value"],
|
|
@@ -5749,7 +6638,23 @@ function contactLocatorSchema6(description) {
|
|
|
5749
6638
|
description
|
|
5750
6639
|
};
|
|
5751
6640
|
}
|
|
5752
|
-
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
|
+
);
|
|
5753
6658
|
return withToolHandling(() => {
|
|
5754
6659
|
switch (input.action) {
|
|
5755
6660
|
case "add": {
|
|
@@ -5767,7 +6672,8 @@ function manageIdentityToolHandler(db, input) {
|
|
|
5767
6672
|
return mutationToolResult(
|
|
5768
6673
|
"revise",
|
|
5769
6674
|
reviseIdentity(db, input.identityId, input.patch),
|
|
5770
|
-
"identity"
|
|
6675
|
+
"identity",
|
|
6676
|
+
Object.keys(input.patch)
|
|
5771
6677
|
);
|
|
5772
6678
|
case "verify":
|
|
5773
6679
|
return mutationToolResult(
|
|
@@ -5790,11 +6696,14 @@ var init_manage_identity_tool = __esm({
|
|
|
5790
6696
|
init_add_identity();
|
|
5791
6697
|
init_remove_identity();
|
|
5792
6698
|
init_revise_identity();
|
|
6699
|
+
init_validate_identity_input();
|
|
5793
6700
|
init_verify_identity();
|
|
5794
6701
|
init_tool_metadata();
|
|
5795
6702
|
init_tool_mutation();
|
|
5796
6703
|
init_tool_names();
|
|
5797
6704
|
init_tool_resolvers();
|
|
6705
|
+
init_tool_types();
|
|
6706
|
+
init_validate_tool_input();
|
|
5798
6707
|
manageIdentityTool = defineAffinityTool({
|
|
5799
6708
|
name: manageIdentityToolName,
|
|
5800
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.",
|
|
@@ -5827,7 +6736,9 @@ var init_manage_identity_tool = __esm({
|
|
|
5827
6736
|
),
|
|
5828
6737
|
input: objectSchema(
|
|
5829
6738
|
{
|
|
5830
|
-
type: stringSchema(
|
|
6739
|
+
type: stringSchema(
|
|
6740
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6741
|
+
),
|
|
5831
6742
|
value: stringSchema("Identity value."),
|
|
5832
6743
|
label: stringSchema("Optional label."),
|
|
5833
6744
|
verified: booleanSchema("Whether the identity starts verified."),
|
|
@@ -5841,7 +6752,9 @@ var init_manage_identity_tool = __esm({
|
|
|
5841
6752
|
),
|
|
5842
6753
|
patch: objectSchema(
|
|
5843
6754
|
{
|
|
5844
|
-
type: stringSchema(
|
|
6755
|
+
type: stringSchema(
|
|
6756
|
+
`Optional identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6757
|
+
),
|
|
5845
6758
|
value: stringSchema("Optional identity value."),
|
|
5846
6759
|
label: stringSchema("Optional label.")
|
|
5847
6760
|
},
|
|
@@ -6112,7 +7025,19 @@ function linkLocatorSchema4(description) {
|
|
|
6112
7025
|
description
|
|
6113
7026
|
};
|
|
6114
7027
|
}
|
|
6115
|
-
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
|
+
);
|
|
6116
7041
|
return withToolHandling(() => {
|
|
6117
7042
|
switch (input.action) {
|
|
6118
7043
|
case "seed_social_link": {
|
|
@@ -6211,6 +7136,8 @@ var init_manage_relationship_tool = __esm({
|
|
|
6211
7136
|
init_tool_mutation();
|
|
6212
7137
|
init_tool_names();
|
|
6213
7138
|
init_tool_resolvers();
|
|
7139
|
+
init_tool_types();
|
|
7140
|
+
init_validate_tool_input();
|
|
6214
7141
|
manageRelationshipTool = defineAffinityTool({
|
|
6215
7142
|
name: manageRelationshipToolName,
|
|
6216
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.",
|
|
@@ -6516,7 +7443,16 @@ function contactLocatorSchema8(description) {
|
|
|
6516
7443
|
description
|
|
6517
7444
|
};
|
|
6518
7445
|
}
|
|
6519
|
-
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
|
+
);
|
|
6520
7456
|
return withToolHandling(() => {
|
|
6521
7457
|
const winner = resolveContactLocator(db, input.winner, "winner");
|
|
6522
7458
|
if (!winner.ok) {
|
|
@@ -6546,6 +7482,8 @@ var init_merge_contacts_tool = __esm({
|
|
|
6546
7482
|
init_tool_mutation();
|
|
6547
7483
|
init_tool_names();
|
|
6548
7484
|
init_tool_resolvers();
|
|
7485
|
+
init_tool_types();
|
|
7486
|
+
init_validate_tool_input();
|
|
6549
7487
|
mergeContactsTool = defineAffinityTool({
|
|
6550
7488
|
name: mergeContactsToolName,
|
|
6551
7489
|
description: "Merge one loser contact into one winner contact using explicit contact locators.",
|
|
@@ -6805,7 +7743,16 @@ function prepareParticipants2(db, raw, injectOwner) {
|
|
|
6805
7743
|
const normalized = raw.map(normalizeParticipant2);
|
|
6806
7744
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
6807
7745
|
}
|
|
6808
|
-
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
|
+
);
|
|
6809
7756
|
return withToolHandling(() => {
|
|
6810
7757
|
const patched = {
|
|
6811
7758
|
...input.input,
|
|
@@ -6855,6 +7802,8 @@ var init_record_event_tool = __esm({
|
|
|
6855
7802
|
init_tool_mutation();
|
|
6856
7803
|
init_tool_names();
|
|
6857
7804
|
init_tool_resolvers();
|
|
7805
|
+
init_tool_types();
|
|
7806
|
+
init_validate_tool_input();
|
|
6858
7807
|
recordEventTool = defineAffinityTool({
|
|
6859
7808
|
name: recordEventToolName,
|
|
6860
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.",
|
|
@@ -6915,7 +7864,7 @@ var init_record_event_tool = __esm({
|
|
|
6915
7864
|
"observed"
|
|
6916
7865
|
])
|
|
6917
7866
|
},
|
|
6918
|
-
["
|
|
7867
|
+
["role"]
|
|
6919
7868
|
),
|
|
6920
7869
|
"Participant list. Must be non-empty and include the owner contact."
|
|
6921
7870
|
),
|
|
@@ -6933,6 +7882,27 @@ var init_record_event_tool = __esm({
|
|
|
6933
7882
|
}
|
|
6934
7883
|
});
|
|
6935
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
|
+
|
|
6936
7906
|
// src/contacts/list_contacts.ts
|
|
6937
7907
|
function listContacts(db, filters, options) {
|
|
6938
7908
|
assertDefaultOrdering("listContacts", options);
|
|
@@ -6956,6 +7926,9 @@ function listContacts(db, filters, options) {
|
|
|
6956
7926
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
6957
7927
|
clauses.push("lifecycle_state != 'dormant'");
|
|
6958
7928
|
}
|
|
7929
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
7930
|
+
clauses.push(...attributeFilters.clauses);
|
|
7931
|
+
params.push(...attributeFilters.params);
|
|
6959
7932
|
const where = clauses.join(" AND ");
|
|
6960
7933
|
const rows = db.prepare(
|
|
6961
7934
|
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
@@ -6984,6 +7957,7 @@ var init_list_contacts = __esm({
|
|
|
6984
7957
|
"src/contacts/list_contacts.ts"() {
|
|
6985
7958
|
init_assert_default_ordering();
|
|
6986
7959
|
init_resolve_list_limit_offset();
|
|
7960
|
+
init_contact_attribute_clauses();
|
|
6987
7961
|
init_mappers4();
|
|
6988
7962
|
}
|
|
6989
7963
|
});
|
|
@@ -7211,131 +8185,6 @@ var init_list_upcoming_dates = __esm({
|
|
|
7211
8185
|
}
|
|
7212
8186
|
});
|
|
7213
8187
|
|
|
7214
|
-
// src/events/get_contact_journal.ts
|
|
7215
|
-
function getContactJournal(db, contactId, options) {
|
|
7216
|
-
assertDefaultOrdering("getContactJournal", options);
|
|
7217
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
7218
|
-
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
7219
|
-
const params = [contactId];
|
|
7220
|
-
if (options?.since !== void 0) {
|
|
7221
|
-
clauses.push("e.occurred_at >= ?");
|
|
7222
|
-
params.push(options.since);
|
|
7223
|
-
}
|
|
7224
|
-
if (options?.until !== void 0) {
|
|
7225
|
-
clauses.push("e.occurred_at <= ?");
|
|
7226
|
-
params.push(options.until);
|
|
7227
|
-
}
|
|
7228
|
-
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
7229
|
-
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
7230
|
-
clauses.push(`e.type IN (${ph})`);
|
|
7231
|
-
params.push(...options.eventTypes);
|
|
7232
|
-
}
|
|
7233
|
-
const where = clauses.join(" AND ");
|
|
7234
|
-
const rows = db.prepare(
|
|
7235
|
-
`SELECT e.id FROM events e
|
|
7236
|
-
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
7237
|
-
WHERE ${where}
|
|
7238
|
-
ORDER BY e.occurred_at DESC, e.id DESC
|
|
7239
|
-
LIMIT ? OFFSET ?`
|
|
7240
|
-
).all(...params, limit, offset);
|
|
7241
|
-
return batchLoadEventRecords(
|
|
7242
|
-
db,
|
|
7243
|
-
rows.map((r) => r.id)
|
|
7244
|
-
);
|
|
7245
|
-
}
|
|
7246
|
-
var init_get_contact_journal = __esm({
|
|
7247
|
-
"src/events/get_contact_journal.ts"() {
|
|
7248
|
-
init_assert_default_ordering();
|
|
7249
|
-
init_resolve_list_limit_offset();
|
|
7250
|
-
init_loaders();
|
|
7251
|
-
}
|
|
7252
|
-
});
|
|
7253
|
-
|
|
7254
|
-
// src/events/list_open_commitments.ts
|
|
7255
|
-
function mapResolution(resolvedAt, resolution) {
|
|
7256
|
-
if (resolvedAt === null) {
|
|
7257
|
-
return "open";
|
|
7258
|
-
}
|
|
7259
|
-
if (resolution === "cancelled") {
|
|
7260
|
-
return "cancelled";
|
|
7261
|
-
}
|
|
7262
|
-
return "resolved";
|
|
7263
|
-
}
|
|
7264
|
-
function listOpenCommitments(db, filters, options) {
|
|
7265
|
-
assertDefaultOrdering("listOpenCommitments", options);
|
|
7266
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
7267
|
-
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
7268
|
-
const params = [];
|
|
7269
|
-
if (options?.since !== void 0) {
|
|
7270
|
-
clauses.push("e.occurred_at >= ?");
|
|
7271
|
-
params.push(options.since);
|
|
7272
|
-
}
|
|
7273
|
-
if (options?.until !== void 0) {
|
|
7274
|
-
clauses.push("e.occurred_at <= ?");
|
|
7275
|
-
params.push(options.until);
|
|
7276
|
-
}
|
|
7277
|
-
if (filters?.commitmentType !== void 0) {
|
|
7278
|
-
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
7279
|
-
throw new AffinityValidationError("invalid commitmentType filter");
|
|
7280
|
-
}
|
|
7281
|
-
clauses.push("oc.commitment_type = ?");
|
|
7282
|
-
params.push(filters.commitmentType);
|
|
7283
|
-
}
|
|
7284
|
-
if (filters?.contactId !== void 0) {
|
|
7285
|
-
clauses.push(
|
|
7286
|
-
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
7287
|
-
);
|
|
7288
|
-
params.push(filters.contactId);
|
|
7289
|
-
}
|
|
7290
|
-
if (filters?.linkId !== void 0) {
|
|
7291
|
-
clauses.push(
|
|
7292
|
-
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
7293
|
-
);
|
|
7294
|
-
params.push(filters.linkId);
|
|
7295
|
-
}
|
|
7296
|
-
if (filters?.horizonDays !== void 0) {
|
|
7297
|
-
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
7298
|
-
throw new AffinityValidationError(
|
|
7299
|
-
"horizonDays must be a non-negative integer"
|
|
7300
|
-
);
|
|
7301
|
-
}
|
|
7302
|
-
const anchor = options?.since ?? Date.now();
|
|
7303
|
-
clauses.push("oc.due_at IS NOT NULL");
|
|
7304
|
-
clauses.push("oc.due_at <= ?");
|
|
7305
|
-
params.push(anchor + filters.horizonDays * DAY_MS3);
|
|
7306
|
-
}
|
|
7307
|
-
const where = clauses.join(" AND ");
|
|
7308
|
-
const rows = db.prepare(
|
|
7309
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
7310
|
-
e.type, e.summary
|
|
7311
|
-
FROM open_commitments oc
|
|
7312
|
-
INNER JOIN events e ON e.id = oc.event_id
|
|
7313
|
-
WHERE ${where}
|
|
7314
|
-
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
7315
|
-
LIMIT ? OFFSET ?`
|
|
7316
|
-
).all(...params, limit, offset);
|
|
7317
|
-
const eventIds = rows.map((r) => r.event_id);
|
|
7318
|
-
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
7319
|
-
return rows.map((r) => ({
|
|
7320
|
-
eventId: r.event_id,
|
|
7321
|
-
type: r.type,
|
|
7322
|
-
summary: r.summary,
|
|
7323
|
-
participants: partsByEvent.get(r.event_id) ?? [],
|
|
7324
|
-
dueAt: r.due_at,
|
|
7325
|
-
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
7326
|
-
}));
|
|
7327
|
-
}
|
|
7328
|
-
var DAY_MS3;
|
|
7329
|
-
var init_list_open_commitments = __esm({
|
|
7330
|
-
"src/events/list_open_commitments.ts"() {
|
|
7331
|
-
init_affinity_validation_error();
|
|
7332
|
-
init_assert_default_ordering();
|
|
7333
|
-
init_resolve_list_limit_offset();
|
|
7334
|
-
init_loaders();
|
|
7335
|
-
DAY_MS3 = 864e5;
|
|
7336
|
-
}
|
|
7337
|
-
});
|
|
7338
|
-
|
|
7339
8188
|
// src/graph/get_affinity_chart.ts
|
|
7340
8189
|
function getAffinityChart(db, options) {
|
|
7341
8190
|
const contactClauses = [
|
|
@@ -7569,13 +8418,15 @@ function listRadar(db, filters, options) {
|
|
|
7569
8418
|
if (ownerId === null) {
|
|
7570
8419
|
return [];
|
|
7571
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})`;
|
|
7572
8423
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
7573
8424
|
const clauses = [
|
|
7574
8425
|
"l.removed_at IS NULL",
|
|
7575
8426
|
"l.is_structural = 0",
|
|
7576
|
-
"l.from_contact_id = ?"
|
|
8427
|
+
"(l.from_contact_id = ? OR l.to_contact_id = ?)"
|
|
7577
8428
|
];
|
|
7578
|
-
const params = [ownerId];
|
|
8429
|
+
const params = [ownerId, ownerId];
|
|
7579
8430
|
if (filters?.kind !== void 0) {
|
|
7580
8431
|
clauses.push("l.kind = ?");
|
|
7581
8432
|
params.push(filters.kind);
|
|
@@ -7597,6 +8448,7 @@ function listRadar(db, filters, options) {
|
|
|
7597
8448
|
const rows = db.prepare(
|
|
7598
8449
|
`SELECT
|
|
7599
8450
|
l.id,
|
|
8451
|
+
${overdue} AS overdue_commitment,
|
|
7600
8452
|
l.from_contact_id,
|
|
7601
8453
|
l.to_contact_id,
|
|
7602
8454
|
l.trust,
|
|
@@ -7605,17 +8457,18 @@ function listRadar(db, filters, options) {
|
|
|
7605
8457
|
lr.recency_score,
|
|
7606
8458
|
lr.normalized_rank
|
|
7607
8459
|
FROM links l
|
|
7608
|
-
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
|
|
7609
8461
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
7610
8462
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
7611
8463
|
WHERE ${where}
|
|
7612
|
-
ORDER BY COALESCE(lr.radar_score, 0) DESC
|
|
8464
|
+
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
7613
8465
|
LIMIT ? OFFSET ?`
|
|
7614
8466
|
).all(...params, limit, offset);
|
|
7615
8467
|
return rows.map((row) => ({
|
|
7616
8468
|
linkId: row.id,
|
|
7617
8469
|
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
7618
|
-
|
|
8470
|
+
reasons: row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
8471
|
+
driftPriority: row.overdue_commitment ? Math.max(1, row.drift_priority ?? 0) : row.drift_priority ?? 0,
|
|
7619
8472
|
recencyScore: row.recency_score ?? 0,
|
|
7620
8473
|
normalizedRank: row.normalized_rank ?? 0,
|
|
7621
8474
|
trust: row.trust ?? 0,
|
|
@@ -7631,6 +8484,7 @@ var init_list_radar = __esm({
|
|
|
7631
8484
|
init_queries();
|
|
7632
8485
|
init_assert_default_ordering();
|
|
7633
8486
|
init_resolve_list_limit_offset();
|
|
8487
|
+
init_resolve_now();
|
|
7634
8488
|
}
|
|
7635
8489
|
});
|
|
7636
8490
|
|
|
@@ -7684,6 +8538,60 @@ var init_list_dismissed_duplicates = __esm({
|
|
|
7684
8538
|
}
|
|
7685
8539
|
});
|
|
7686
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
|
+
|
|
7687
8595
|
// src/tools/tool_summary.ts
|
|
7688
8596
|
function summarizeCount(count, singular) {
|
|
7689
8597
|
if (count === 0) {
|
|
@@ -7702,6 +8610,7 @@ var init_tool_summary = __esm({
|
|
|
7702
8610
|
// src/tools/review_affinity_tool.ts
|
|
7703
8611
|
function buildReadOptions(input) {
|
|
7704
8612
|
return {
|
|
8613
|
+
...input.now === void 0 ? {} : { now: input.now },
|
|
7705
8614
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7706
8615
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
7707
8616
|
...input.includeArchived === void 0 ? {} : { includeArchived: input.includeArchived },
|
|
@@ -7821,6 +8730,7 @@ function validateReviewInput(input) {
|
|
|
7821
8730
|
}
|
|
7822
8731
|
function duplicateCandidateItem(candidate) {
|
|
7823
8732
|
return {
|
|
8733
|
+
...candidate,
|
|
7824
8734
|
kind: "contact",
|
|
7825
8735
|
id: candidate.leftContactId,
|
|
7826
8736
|
title: `Possible duplicate: #${candidate.leftContactId} and #${candidate.rightContactId}`,
|
|
@@ -7830,24 +8740,28 @@ function duplicateCandidateItem(candidate) {
|
|
|
7830
8740
|
}
|
|
7831
8741
|
function momentItem(moment) {
|
|
7832
8742
|
return {
|
|
8743
|
+
...moment,
|
|
8744
|
+
occurredAtIso: new Date(moment.occurredAt).toISOString(),
|
|
7833
8745
|
kind: "event",
|
|
7834
8746
|
id: moment.eventId,
|
|
7835
8747
|
title: moment.summary,
|
|
7836
8748
|
subtitle: moment.momentKind,
|
|
7837
|
-
snippet:
|
|
8749
|
+
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
7838
8750
|
};
|
|
7839
8751
|
}
|
|
7840
8752
|
function commitmentItem(commitment) {
|
|
7841
8753
|
return {
|
|
8754
|
+
...commitment,
|
|
7842
8755
|
kind: "event",
|
|
7843
8756
|
id: commitment.eventId,
|
|
7844
8757
|
title: commitment.summary,
|
|
7845
8758
|
subtitle: commitment.type,
|
|
7846
|
-
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)}`
|
|
7847
8760
|
};
|
|
7848
8761
|
}
|
|
7849
8762
|
function radarItem(radar) {
|
|
7850
8763
|
return {
|
|
8764
|
+
...radar,
|
|
7851
8765
|
kind: "link",
|
|
7852
8766
|
id: radar.linkId,
|
|
7853
8767
|
title: `Link #${radar.linkId}`,
|
|
@@ -7857,29 +8771,34 @@ function radarItem(radar) {
|
|
|
7857
8771
|
}
|
|
7858
8772
|
function upcomingDateItem(date) {
|
|
7859
8773
|
return {
|
|
8774
|
+
...date,
|
|
8775
|
+
occursAt: date.occursOn,
|
|
8776
|
+
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
7860
8777
|
kind: date.targetRef.kind,
|
|
7861
8778
|
id: date.targetRef.id,
|
|
7862
8779
|
title: date.summary,
|
|
7863
8780
|
subtitle: date.recurrenceKind,
|
|
7864
|
-
snippet: `occurs ${date.occursOn}`
|
|
8781
|
+
snippet: `occurs ${new Date(date.occursOn).toISOString().slice(0, 10)}`
|
|
7865
8782
|
};
|
|
7866
8783
|
}
|
|
7867
8784
|
function mergeHistoryItem(record) {
|
|
7868
8785
|
return {
|
|
8786
|
+
...record,
|
|
7869
8787
|
kind: "contact",
|
|
7870
8788
|
id: record.winnerContactId,
|
|
7871
8789
|
title: `Merged #${record.loserContactId} into #${record.winnerContactId}`,
|
|
7872
|
-
subtitle:
|
|
8790
|
+
subtitle: new Date(record.mergedAt).toISOString().slice(0, 10),
|
|
7873
8791
|
snippet: record.reasonSummary ?? void 0
|
|
7874
8792
|
};
|
|
7875
8793
|
}
|
|
7876
8794
|
function dismissedPairItem(record) {
|
|
7877
8795
|
return {
|
|
8796
|
+
...record,
|
|
7878
8797
|
kind: "contact",
|
|
7879
8798
|
id: record.leftContactId,
|
|
7880
8799
|
title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
|
|
7881
8800
|
subtitle: record.reason ?? "no reason given",
|
|
7882
|
-
snippet: `dismissed at ${record.dismissedAt}`
|
|
8801
|
+
snippet: `dismissed at ${new Date(record.dismissedAt).toISOString().slice(0, 10)}`
|
|
7883
8802
|
};
|
|
7884
8803
|
}
|
|
7885
8804
|
function compactFilters(input) {
|
|
@@ -7889,7 +8808,16 @@ function compactFilters(input) {
|
|
|
7889
8808
|
)
|
|
7890
8809
|
);
|
|
7891
8810
|
}
|
|
7892
|
-
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
|
+
);
|
|
7893
8821
|
return withToolHandling(() => {
|
|
7894
8822
|
const invalid = validateReviewInput(input);
|
|
7895
8823
|
if (invalid) {
|
|
@@ -7903,6 +8831,9 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7903
8831
|
items = listContacts(
|
|
7904
8832
|
db,
|
|
7905
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 },
|
|
7906
8837
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
7907
8838
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
7908
8839
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -7933,7 +8864,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7933
8864
|
"Provide `contact` when `view` is `events.contact_journal`."
|
|
7934
8865
|
);
|
|
7935
8866
|
}
|
|
7936
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
8867
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
7937
8868
|
if (!contact.ok) {
|
|
7938
8869
|
return contact.result;
|
|
7939
8870
|
}
|
|
@@ -7968,7 +8899,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7968
8899
|
let contactId;
|
|
7969
8900
|
let linkId;
|
|
7970
8901
|
if (input.contact !== void 0) {
|
|
7971
|
-
const contact = resolveContactLocator(
|
|
8902
|
+
const contact = resolveContactLocator(
|
|
8903
|
+
db,
|
|
8904
|
+
input.contact,
|
|
8905
|
+
"contact",
|
|
8906
|
+
true
|
|
8907
|
+
);
|
|
7972
8908
|
if (!contact.ok) {
|
|
7973
8909
|
return contact.result;
|
|
7974
8910
|
}
|
|
@@ -7997,7 +8933,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7997
8933
|
let contactId;
|
|
7998
8934
|
let linkId;
|
|
7999
8935
|
if (input.contact !== void 0) {
|
|
8000
|
-
const contact = resolveContactLocator(
|
|
8936
|
+
const contact = resolveContactLocator(
|
|
8937
|
+
db,
|
|
8938
|
+
input.contact,
|
|
8939
|
+
"contact",
|
|
8940
|
+
true
|
|
8941
|
+
);
|
|
8001
8942
|
if (!contact.ok) {
|
|
8002
8943
|
return contact.result;
|
|
8003
8944
|
}
|
|
@@ -8090,7 +9031,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8090
9031
|
"Provide `contact` when `view` is `merges.history`."
|
|
8091
9032
|
);
|
|
8092
9033
|
}
|
|
8093
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
9034
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
8094
9035
|
if (!contact.ok) {
|
|
8095
9036
|
return contact.result;
|
|
8096
9037
|
}
|
|
@@ -8103,6 +9044,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
8103
9044
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
8104
9045
|
break;
|
|
8105
9046
|
}
|
|
9047
|
+
items = items.map((item) => enrichReviewItem(db, item));
|
|
8106
9048
|
return toolSuccess(
|
|
8107
9049
|
summarizeCount(items.length, "item"),
|
|
8108
9050
|
{
|
|
@@ -8141,12 +9083,15 @@ var init_review_affinity_tool = __esm({
|
|
|
8141
9083
|
init_list_radar();
|
|
8142
9084
|
init_get_merge_history();
|
|
8143
9085
|
init_list_dismissed_duplicates();
|
|
9086
|
+
init_enrich_review_item();
|
|
8144
9087
|
init_tool_metadata();
|
|
8145
9088
|
init_tool_names();
|
|
8146
9089
|
init_tool_ref();
|
|
8147
9090
|
init_tool_resolvers();
|
|
8148
9091
|
init_tool_summary();
|
|
8149
9092
|
init_tool_types();
|
|
9093
|
+
init_tool_types();
|
|
9094
|
+
init_validate_tool_input();
|
|
8150
9095
|
reviewAffinityTool = defineAffinityTool({
|
|
8151
9096
|
name: reviewAffinityToolName,
|
|
8152
9097
|
description: "Review one affinity surface such as contacts, journals, link queues, commitments, dates, graph state, or merge history.",
|
|
@@ -8158,12 +9103,16 @@ var init_review_affinity_tool = __esm({
|
|
|
8158
9103
|
targetKinds: ["contact", "link", "event"],
|
|
8159
9104
|
inputDescriptions: {
|
|
8160
9105
|
view: "Which review surface to return.",
|
|
9106
|
+
now: "Optional current timestamp for overdue calculations.",
|
|
8161
9107
|
contact: "Target contact locator for views that require one.",
|
|
8162
9108
|
link: "Target link locator for views that require one.",
|
|
8163
9109
|
contactIds: "Optional contact id subset for duplicate review or graph review.",
|
|
8164
9110
|
kind: "Optional contact kind or link kind filter, depending on the view.",
|
|
8165
9111
|
lifecycleState: "Optional contact lifecycle filter for the contact list view.",
|
|
8166
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.",
|
|
8167
9116
|
includeOwner: "Whether owner contacts should be included in contact review.",
|
|
8168
9117
|
includeDormant: "Whether dormant links or contacts should be included where supported.",
|
|
8169
9118
|
includeArchived: "Whether archived links should be included where supported.",
|
|
@@ -8200,6 +9149,9 @@ var init_review_affinity_tool = __esm({
|
|
|
8200
9149
|
"merges.history",
|
|
8201
9150
|
"merges.dismissed"
|
|
8202
9151
|
]),
|
|
9152
|
+
now: integerSchema(
|
|
9153
|
+
"Optional current timestamp for overdue calculations."
|
|
9154
|
+
),
|
|
8203
9155
|
contact: contactLocatorSchema9("Optional target contact locator."),
|
|
8204
9156
|
link: linkLocatorSchema5("Optional target link locator."),
|
|
8205
9157
|
contactIds: arraySchema(
|
|
@@ -8222,6 +9174,17 @@ var init_review_affinity_tool = __esm({
|
|
|
8222
9174
|
"broken",
|
|
8223
9175
|
"archived"
|
|
8224
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
|
+
),
|
|
8225
9188
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8226
9189
|
includeDormant: booleanSchema("Whether dormant rows are included."),
|
|
8227
9190
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
@@ -8268,99 +9231,110 @@ var init_review_affinity_tool = __esm({
|
|
|
8268
9231
|
});
|
|
8269
9232
|
|
|
8270
9233
|
// src/contacts/search_contacts.ts
|
|
8271
|
-
function relevanceScore(name, queryLower, matchedName, matchedNormalizedIdentity) {
|
|
8272
|
-
const nl = name.toLowerCase();
|
|
8273
|
-
if (matchedNormalizedIdentity) return 1.1;
|
|
8274
|
-
if (nl.startsWith(queryLower)) return 1;
|
|
8275
|
-
if (matchedName) return 0.85;
|
|
8276
|
-
return 0.7;
|
|
8277
|
-
}
|
|
8278
9234
|
function searchContacts(db, query, filters, options) {
|
|
8279
|
-
const q = query.trim();
|
|
8280
|
-
if (q
|
|
8281
|
-
return [];
|
|
8282
|
-
}
|
|
9235
|
+
const q = foldSearchText(query.trim());
|
|
9236
|
+
if (!q) return [];
|
|
8283
9237
|
assertDefaultOrdering("searchContacts", options);
|
|
8284
9238
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
8285
|
-
const
|
|
8286
|
-
const
|
|
8287
|
-
|
|
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);
|
|
8288
9256
|
const clauses = [
|
|
8289
|
-
"
|
|
8290
|
-
"
|
|
9257
|
+
"deleted_at IS NULL",
|
|
9258
|
+
"lifecycle_state != 'merged'",
|
|
9259
|
+
...attributeFilters.clauses
|
|
8291
9260
|
];
|
|
8292
|
-
const params = [];
|
|
9261
|
+
const params = [...attributeFilters.params];
|
|
8293
9262
|
if (filters?.kind !== void 0) {
|
|
8294
|
-
clauses.push("
|
|
9263
|
+
clauses.push("kind = ?");
|
|
8295
9264
|
params.push(filters.kind);
|
|
8296
9265
|
}
|
|
8297
9266
|
if (filters?.lifecycleState !== void 0) {
|
|
8298
|
-
clauses.push("
|
|
9267
|
+
clauses.push("lifecycle_state = ?");
|
|
8299
9268
|
params.push(filters.lifecycleState);
|
|
8300
9269
|
}
|
|
8301
|
-
if (filters?.includeOwner === false)
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
const
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
Number(
|
|
8340
|
-
r.matched_normalized_identity ?? 0
|
|
8341
|
-
) > 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:")
|
|
8342
9308
|
);
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
});
|
|
8351
|
-
scored.sort((a, b) => b.score - a.score);
|
|
8352
|
-
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);
|
|
8353
9316
|
}
|
|
8354
9317
|
var init_search_contacts = __esm({
|
|
8355
9318
|
"src/contacts/search_contacts.ts"() {
|
|
8356
9319
|
init_assert_default_ordering();
|
|
8357
9320
|
init_resolve_list_limit_offset();
|
|
9321
|
+
init_contact_attribute_clauses();
|
|
9322
|
+
init_fold_search_text();
|
|
8358
9323
|
init_mappers4();
|
|
8359
9324
|
}
|
|
8360
9325
|
});
|
|
8361
9326
|
|
|
8362
9327
|
// src/tools/search_affinity_tool.ts
|
|
8363
|
-
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
|
+
);
|
|
8364
9338
|
return withToolHandling(() => {
|
|
8365
9339
|
const options = {
|
|
8366
9340
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
@@ -8370,6 +9344,9 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8370
9344
|
db,
|
|
8371
9345
|
input.query,
|
|
8372
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 },
|
|
8373
9350
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8374
9351
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8375
9352
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8401,9 +9378,11 @@ var init_search_affinity_tool = __esm({
|
|
|
8401
9378
|
init_tool_resolvers();
|
|
8402
9379
|
init_tool_summary();
|
|
8403
9380
|
init_tool_types();
|
|
9381
|
+
init_tool_types();
|
|
9382
|
+
init_validate_tool_input();
|
|
8404
9383
|
searchAffinityTool = defineAffinityTool({
|
|
8405
9384
|
name: searchAffinityToolName,
|
|
8406
|
-
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.",
|
|
8407
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.",
|
|
8408
9387
|
whenNotToUse: "Do not use this when you already know the exact contact you want to inspect in depth.",
|
|
8409
9388
|
sideEffects: "none",
|
|
@@ -8414,6 +9393,9 @@ var init_search_affinity_tool = __esm({
|
|
|
8414
9393
|
query: "Free-text or natural-key search text.",
|
|
8415
9394
|
kind: "Optional contact kind filter.",
|
|
8416
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.",
|
|
8417
9399
|
includeOwner: "Whether owner contacts are eligible for results.",
|
|
8418
9400
|
includeDormant: "Whether dormant contacts should be eligible for results.",
|
|
8419
9401
|
limit: "Optional result limit.",
|
|
@@ -8438,6 +9420,17 @@ var init_search_affinity_tool = __esm({
|
|
|
8438
9420
|
"merged",
|
|
8439
9421
|
"lost"
|
|
8440
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
|
+
),
|
|
8441
9434
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8442
9435
|
includeDormant: booleanSchema("Whether dormant contacts are included."),
|
|
8443
9436
|
limit: integerSchema("Optional result limit."),
|
|
@@ -8484,11 +9477,112 @@ var init_tool_registry = __esm({
|
|
|
8484
9477
|
}
|
|
8485
9478
|
});
|
|
8486
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
|
+
|
|
8487
9577
|
// src/surface/affinity_surface_tools.ts
|
|
8488
9578
|
var affinitySurfaceTools;
|
|
8489
9579
|
var init_affinity_surface_tools = __esm({
|
|
8490
9580
|
"src/surface/affinity_surface_tools.ts"() {
|
|
9581
|
+
init_cli_args();
|
|
8491
9582
|
init_tool_registry();
|
|
9583
|
+
init_tool_types();
|
|
9584
|
+
init_plain_dates();
|
|
9585
|
+
init_timestamp_schema();
|
|
8492
9586
|
affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
8493
9587
|
name: tool.name,
|
|
8494
9588
|
description: tool.description,
|
|
@@ -8496,8 +9590,24 @@ var init_affinity_surface_tools = __esm({
|
|
|
8496
9590
|
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
8497
9591
|
readOnly: tool.readOnly,
|
|
8498
9592
|
sideEffects: tool.sideEffects,
|
|
8499
|
-
inputSchema: {
|
|
8500
|
-
|
|
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
|
+
}
|
|
8501
9611
|
}));
|
|
8502
9612
|
}
|
|
8503
9613
|
});
|
|
@@ -8820,6 +9930,12 @@ function initLinkEventEffectsTables(db) {
|
|
|
8820
9930
|
)
|
|
8821
9931
|
)
|
|
8822
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
|
+
}
|
|
8823
9939
|
db.exec(
|
|
8824
9940
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
8825
9941
|
);
|
|
@@ -9051,7 +10167,8 @@ var init_open_affinity_db = __esm({
|
|
|
9051
10167
|
}
|
|
9052
10168
|
});
|
|
9053
10169
|
function resolveAffinityDbPath(options = {}) {
|
|
9054
|
-
|
|
10170
|
+
const path = options.flag ?? ((options.env ?? process.env).GHOSTPAW_AFFINITY_DB || join(homedir(), ".ghostpaw", "affinity.db"));
|
|
10171
|
+
return path === ":memory:" ? path : resolve(path);
|
|
9055
10172
|
}
|
|
9056
10173
|
var init_resolve_affinity_db_path = __esm({
|
|
9057
10174
|
"src/surface/resolve_affinity_db_path.ts"() {
|