@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/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
4
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { isSea } from 'node:sea';
|
|
5
6
|
import { Writable } from 'node:stream';
|
|
6
7
|
|
|
7
8
|
var __defProp = Object.defineProperty;
|
|
@@ -23,7 +24,122 @@ var __export = (target, all) => {
|
|
|
23
24
|
var affinityVersion;
|
|
24
25
|
var init_version = __esm({
|
|
25
26
|
"src/version.ts"() {
|
|
26
|
-
affinityVersion = "0.
|
|
27
|
+
affinityVersion = "0.5.0";
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
function parseCliArgs(argv, spec) {
|
|
31
|
+
const values = {};
|
|
32
|
+
const positions = [];
|
|
33
|
+
const flags = spec.flags ?? [];
|
|
34
|
+
const convert = (raw, arg) => {
|
|
35
|
+
let value = raw;
|
|
36
|
+
if (raw.startsWith("@")) {
|
|
37
|
+
try {
|
|
38
|
+
value = readFileSync(raw.slice(1), "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
throw new CliUsageError(`Cannot read value file: ${raw.slice(1)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let result = value;
|
|
44
|
+
if (arg.type === "number" || arg.type === "integer") {
|
|
45
|
+
result = Number(value);
|
|
46
|
+
if (!value.trim() || !Number.isFinite(result) || arg.type === "integer" && !Number.isInteger(result))
|
|
47
|
+
throw new CliUsageError(
|
|
48
|
+
`Expected ${arg.type} for ${arg.name}: ${value}`
|
|
49
|
+
);
|
|
50
|
+
} else if (arg.type === "boolean") {
|
|
51
|
+
if (!["true", "false"].includes(value))
|
|
52
|
+
throw new CliUsageError(`Expected boolean for ${arg.name}`);
|
|
53
|
+
result = value === "true";
|
|
54
|
+
} else if (arg.type === "json") {
|
|
55
|
+
try {
|
|
56
|
+
result = JSON.parse(value);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new CliUsageError(`Invalid JSON for ${arg.name}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (arg.enum && !arg.enum.includes(result))
|
|
62
|
+
throw new CliUsageError(
|
|
63
|
+
`${arg.name} must be one of: ${arg.enum.join(", ")}`
|
|
64
|
+
);
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
let ended = false;
|
|
68
|
+
for (let i = 0; i < argv.length; i++) {
|
|
69
|
+
const token = argv[i];
|
|
70
|
+
if (!ended && token === "--") {
|
|
71
|
+
ended = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ended || token === "-" || !token.startsWith("-")) {
|
|
75
|
+
positions.push(token);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const [flag, inline] = token.split(/=(.*)/s);
|
|
79
|
+
const negative = flag?.startsWith("--no-");
|
|
80
|
+
const name = flag?.replace(/^--?/, "").replace(/^no-/, "");
|
|
81
|
+
const arg = flags.find(
|
|
82
|
+
(entry) => entry.name === name || keyOf(entry.name) === name || entry.short === name
|
|
83
|
+
);
|
|
84
|
+
if (!arg) {
|
|
85
|
+
const prefix = (name ?? "").slice(0, 3);
|
|
86
|
+
const suggestion = prefix ? flags.find((entry) => entry.name.startsWith(prefix)) : void 0;
|
|
87
|
+
throw new CliUsageError(
|
|
88
|
+
`Unknown flag: ${flag}${suggestion ? `; did you mean --${suggestion.name}?` : ""}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (negative && arg.type !== "boolean")
|
|
92
|
+
throw new CliUsageError(`--no-${arg.name} requires a boolean flag`);
|
|
93
|
+
let raw = inline;
|
|
94
|
+
if (arg.type === "boolean" && raw === void 0)
|
|
95
|
+
raw = negative ? "false" : "true";
|
|
96
|
+
if (raw === void 0) {
|
|
97
|
+
const next = argv[i + 1];
|
|
98
|
+
if (next === void 0 || /^--[a-z]/i.test(next))
|
|
99
|
+
throw new CliUsageError(`Missing value: --${arg.name}`);
|
|
100
|
+
raw = next;
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
const key = keyOf(arg.name);
|
|
104
|
+
if (arg.type === "array") {
|
|
105
|
+
const item = convert(raw, { ...arg, type: arg.itemType ?? "string" });
|
|
106
|
+
values[key] = [...values[key] ?? [], item];
|
|
107
|
+
} else values[key] = convert(raw, arg);
|
|
108
|
+
}
|
|
109
|
+
let index = 0;
|
|
110
|
+
for (const arg of spec.positionals ?? []) {
|
|
111
|
+
if (index >= positions.length) continue;
|
|
112
|
+
values[keyOf(arg.name)] = arg.variadic ? positions.slice(index).map(
|
|
113
|
+
(value) => convert(value, { ...arg, type: arg.itemType ?? arg.type })
|
|
114
|
+
) : convert(positions[index], arg);
|
|
115
|
+
index = arg.variadic ? positions.length : index + 1;
|
|
116
|
+
}
|
|
117
|
+
if (index < positions.length)
|
|
118
|
+
throw new CliUsageError(`Unexpected positional: ${positions[index]}`);
|
|
119
|
+
for (const arg of [...flags, ...spec.positionals ?? []]) {
|
|
120
|
+
if (arg.required && values[keyOf(arg.name)] === void 0)
|
|
121
|
+
throw new CliUsageError(`Missing required argument: ${arg.name}`);
|
|
122
|
+
}
|
|
123
|
+
return values;
|
|
124
|
+
}
|
|
125
|
+
function renderCliHelp(spec) {
|
|
126
|
+
const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""} ${arg.description ?? ""}`;
|
|
127
|
+
return `Usage: ${spec.command} ${(spec.positionals ?? []).map((arg) => `<${arg.name}${arg.variadic ? "..." : ""}>`).join(" ")} [flags]
|
|
128
|
+
${spec.description ?? ""}
|
|
129
|
+
|
|
130
|
+
Positionals:
|
|
131
|
+
${(spec.positionals ?? []).map((arg) => ` ${arg.name} ${describe(arg)}`).join("\n")}
|
|
132
|
+
Flags:
|
|
133
|
+
${(spec.flags ?? []).map((arg) => ` --${arg.name}${arg.short ? `, -${arg.short}` : ""} ${describe(arg)}`).join("\n")}
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
var CliUsageError, keyOf;
|
|
137
|
+
var init_cli_args = __esm({
|
|
138
|
+
"src/surfaces/cli_args.ts"() {
|
|
139
|
+
CliUsageError = class extends Error {
|
|
140
|
+
exitCode = 2;
|
|
141
|
+
};
|
|
142
|
+
keyOf = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
27
143
|
}
|
|
28
144
|
});
|
|
29
145
|
|
|
@@ -41,6 +157,78 @@ var init_affinity_error = __esm({
|
|
|
41
157
|
}
|
|
42
158
|
});
|
|
43
159
|
|
|
160
|
+
// src/lib/errors/affinity_validation_error.ts
|
|
161
|
+
var AffinityValidationError;
|
|
162
|
+
var init_affinity_validation_error = __esm({
|
|
163
|
+
"src/lib/errors/affinity_validation_error.ts"() {
|
|
164
|
+
init_affinity_error();
|
|
165
|
+
AffinityValidationError = class extends AffinityError {
|
|
166
|
+
code = "VALIDATION";
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// src/lib/read/assert_default_ordering.ts
|
|
172
|
+
function assertDefaultOrdering(_readName, options) {
|
|
173
|
+
if (options?.sort !== void 0 || options?.descending !== void 0) {
|
|
174
|
+
throw new AffinityValidationError(
|
|
175
|
+
"This review does not support custom sort ordering"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
var init_assert_default_ordering = __esm({
|
|
180
|
+
"src/lib/read/assert_default_ordering.ts"() {
|
|
181
|
+
init_affinity_validation_error();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// src/lib/types/read_list_options.ts
|
|
186
|
+
var AFFINITY_DEFAULT_LIST_LIMIT, AFFINITY_MAX_LIST_LIMIT;
|
|
187
|
+
var init_read_list_options = __esm({
|
|
188
|
+
"src/lib/types/read_list_options.ts"() {
|
|
189
|
+
AFFINITY_DEFAULT_LIST_LIMIT = 50;
|
|
190
|
+
AFFINITY_MAX_LIST_LIMIT = 250;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// src/lib/types/resolve_affinity_list_limit.ts
|
|
195
|
+
function resolveAffinityListLimit(limit) {
|
|
196
|
+
if (limit === void 0) {
|
|
197
|
+
return AFFINITY_DEFAULT_LIST_LIMIT;
|
|
198
|
+
}
|
|
199
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
200
|
+
throw new AffinityValidationError("limit must be a positive integer");
|
|
201
|
+
}
|
|
202
|
+
if (limit > AFFINITY_MAX_LIST_LIMIT) {
|
|
203
|
+
throw new AffinityValidationError(
|
|
204
|
+
`limit must be <= ${String(AFFINITY_MAX_LIST_LIMIT)}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return limit;
|
|
208
|
+
}
|
|
209
|
+
var init_resolve_affinity_list_limit = __esm({
|
|
210
|
+
"src/lib/types/resolve_affinity_list_limit.ts"() {
|
|
211
|
+
init_affinity_validation_error();
|
|
212
|
+
init_read_list_options();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// src/lib/read/resolve_list_limit_offset.ts
|
|
217
|
+
function resolveListLimitOffset(options) {
|
|
218
|
+
const limit = resolveAffinityListLimit(options?.limit);
|
|
219
|
+
const offset = options?.offset ?? 0;
|
|
220
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
221
|
+
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
222
|
+
}
|
|
223
|
+
return { limit, offset };
|
|
224
|
+
}
|
|
225
|
+
var init_resolve_list_limit_offset = __esm({
|
|
226
|
+
"src/lib/read/resolve_list_limit_offset.ts"() {
|
|
227
|
+
init_affinity_validation_error();
|
|
228
|
+
init_resolve_affinity_list_limit();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
44
232
|
// src/lib/errors/affinity_invariant_error.ts
|
|
45
233
|
var AffinityInvariantError;
|
|
46
234
|
var init_affinity_invariant_error = __esm({
|
|
@@ -52,6 +240,17 @@ var init_affinity_invariant_error = __esm({
|
|
|
52
240
|
}
|
|
53
241
|
});
|
|
54
242
|
|
|
243
|
+
// src/lib/errors/affinity_not_found_error.ts
|
|
244
|
+
var AffinityNotFoundError;
|
|
245
|
+
var init_affinity_not_found_error = __esm({
|
|
246
|
+
"src/lib/errors/affinity_not_found_error.ts"() {
|
|
247
|
+
init_affinity_error();
|
|
248
|
+
AffinityNotFoundError = class extends AffinityError {
|
|
249
|
+
code = "NOT_FOUND";
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
55
254
|
// src/contacts/queries.ts
|
|
56
255
|
function getContactRowById(db, id) {
|
|
57
256
|
const row = db.prepare(
|
|
@@ -88,17 +287,6 @@ var init_queries = __esm({
|
|
|
88
287
|
}
|
|
89
288
|
});
|
|
90
289
|
|
|
91
|
-
// src/lib/errors/affinity_not_found_error.ts
|
|
92
|
-
var AffinityNotFoundError;
|
|
93
|
-
var init_affinity_not_found_error = __esm({
|
|
94
|
-
"src/lib/errors/affinity_not_found_error.ts"() {
|
|
95
|
-
init_affinity_error();
|
|
96
|
-
AffinityNotFoundError = class extends AffinityError {
|
|
97
|
-
code = "NOT_FOUND";
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
290
|
// src/lib/errors/affinity_state_error.ts
|
|
103
291
|
var AffinityStateError;
|
|
104
292
|
var init_affinity_state_error = __esm({
|
|
@@ -110,17 +298,6 @@ var init_affinity_state_error = __esm({
|
|
|
110
298
|
}
|
|
111
299
|
});
|
|
112
300
|
|
|
113
|
-
// src/lib/errors/affinity_validation_error.ts
|
|
114
|
-
var AffinityValidationError;
|
|
115
|
-
var init_affinity_validation_error = __esm({
|
|
116
|
-
"src/lib/errors/affinity_validation_error.ts"() {
|
|
117
|
-
init_affinity_error();
|
|
118
|
-
AffinityValidationError = class extends AffinityError {
|
|
119
|
-
code = "VALIDATION";
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
301
|
// src/events/validators.ts
|
|
125
302
|
function assertOwnerParticipates(ownerId, participantIds) {
|
|
126
303
|
if (!participantIds.includes(ownerId)) {
|
|
@@ -134,7 +311,10 @@ function assertParticipantContactsLive(db, contactIds) {
|
|
|
134
311
|
throw new AffinityNotFoundError("contact not found");
|
|
135
312
|
}
|
|
136
313
|
if (row.lifecycle_state === "merged") {
|
|
137
|
-
|
|
314
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
315
|
+
throw new AffinityStateError(
|
|
316
|
+
`Contact #${id} was merged into #${row.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row.merged_into_contact_id}`
|
|
317
|
+
);
|
|
138
318
|
}
|
|
139
319
|
}
|
|
140
320
|
}
|
|
@@ -345,28 +525,260 @@ var init_queries2 = __esm({
|
|
|
345
525
|
}
|
|
346
526
|
});
|
|
347
527
|
|
|
348
|
-
// src/
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
528
|
+
// src/events/loaders.ts
|
|
529
|
+
function loadEventParticipantViews(db, eventId) {
|
|
530
|
+
const rows = db.prepare(
|
|
531
|
+
`SELECT contact_id, role, directionality
|
|
532
|
+
FROM event_participants
|
|
533
|
+
WHERE event_id = ?`
|
|
534
|
+
).all(eventId);
|
|
535
|
+
return rows.map((row) => ({
|
|
536
|
+
contactId: row.contact_id,
|
|
537
|
+
role: row.role,
|
|
538
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
function loadEventRecord(db, eventId) {
|
|
542
|
+
const row = getEventRowById(db, eventId);
|
|
543
|
+
if (!row) {
|
|
544
|
+
throw new AffinityInvariantError("event not found");
|
|
357
545
|
}
|
|
358
|
-
|
|
546
|
+
const participants = loadEventParticipantViews(db, eventId);
|
|
547
|
+
return mapEventRowToEventRecord(row, participants);
|
|
359
548
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
549
|
+
function batchLoadEventRecords(db, eventIds) {
|
|
550
|
+
if (eventIds.length === 0) {
|
|
551
|
+
return [];
|
|
363
552
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
553
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
554
|
+
const eventRows = db.prepare(
|
|
555
|
+
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
556
|
+
recurrence_kind, anchor_month, anchor_day,
|
|
557
|
+
anchor_contact_id, anchor_link_id,
|
|
558
|
+
created_at, updated_at, deleted_at
|
|
559
|
+
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
560
|
+
).all(...eventIds);
|
|
561
|
+
const partRows = db.prepare(
|
|
562
|
+
`SELECT event_id, contact_id, role, directionality
|
|
563
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
564
|
+
).all(...eventIds);
|
|
565
|
+
const partsByEvent = /* @__PURE__ */ new Map();
|
|
566
|
+
for (const p of partRows) {
|
|
567
|
+
let arr = partsByEvent.get(p.event_id);
|
|
568
|
+
if (!arr) {
|
|
569
|
+
arr = [];
|
|
570
|
+
partsByEvent.set(p.event_id, arr);
|
|
571
|
+
}
|
|
572
|
+
arr.push({
|
|
573
|
+
contactId: p.contact_id,
|
|
574
|
+
role: p.role,
|
|
575
|
+
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
579
|
+
const out = [];
|
|
580
|
+
for (const id of eventIds) {
|
|
581
|
+
const row = rowById.get(id);
|
|
582
|
+
if (row) {
|
|
583
|
+
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
function batchLoadEventParticipantViews(db, eventIds) {
|
|
589
|
+
const result = /* @__PURE__ */ new Map();
|
|
590
|
+
if (eventIds.length === 0) {
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
const ph = eventIds.map(() => "?").join(", ");
|
|
594
|
+
const rows = db.prepare(
|
|
595
|
+
`SELECT event_id, contact_id, role, directionality
|
|
596
|
+
FROM event_participants WHERE event_id IN (${ph})`
|
|
597
|
+
).all(...eventIds);
|
|
598
|
+
for (const row of rows) {
|
|
599
|
+
let arr = result.get(row.event_id);
|
|
600
|
+
if (!arr) {
|
|
601
|
+
arr = [];
|
|
602
|
+
result.set(row.event_id, arr);
|
|
603
|
+
}
|
|
604
|
+
arr.push({
|
|
605
|
+
contactId: row.contact_id,
|
|
606
|
+
role: row.role,
|
|
607
|
+
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
function requireDateAnchorEvent(db, eventId) {
|
|
613
|
+
const row = getEventRowById(db, eventId);
|
|
614
|
+
if (!row) {
|
|
615
|
+
throw new AffinityNotFoundError("event not found");
|
|
616
|
+
}
|
|
617
|
+
if (row.type !== "date_anchor") {
|
|
618
|
+
throw new AffinityInvariantError("not a date anchor");
|
|
619
|
+
}
|
|
620
|
+
return row;
|
|
621
|
+
}
|
|
622
|
+
var init_loaders = __esm({
|
|
623
|
+
"src/events/loaders.ts"() {
|
|
624
|
+
init_affinity_invariant_error();
|
|
625
|
+
init_affinity_not_found_error();
|
|
626
|
+
init_mappers();
|
|
627
|
+
init_queries2();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// src/events/get_contact_journal.ts
|
|
632
|
+
function getContactJournal(db, contactId, options) {
|
|
633
|
+
assertDefaultOrdering("getContactJournal", options);
|
|
634
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
635
|
+
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
636
|
+
const params = [contactId];
|
|
637
|
+
if (options?.since !== void 0) {
|
|
638
|
+
clauses.push("e.occurred_at >= ?");
|
|
639
|
+
params.push(options.since);
|
|
640
|
+
}
|
|
641
|
+
if (options?.until !== void 0) {
|
|
642
|
+
clauses.push("e.occurred_at <= ?");
|
|
643
|
+
params.push(options.until);
|
|
644
|
+
}
|
|
645
|
+
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
646
|
+
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
647
|
+
clauses.push(`e.type IN (${ph})`);
|
|
648
|
+
params.push(...options.eventTypes);
|
|
649
|
+
}
|
|
650
|
+
const where = clauses.join(" AND ");
|
|
651
|
+
const rows = db.prepare(
|
|
652
|
+
`SELECT e.id FROM events e
|
|
653
|
+
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
654
|
+
WHERE ${where}
|
|
655
|
+
ORDER BY e.occurred_at DESC, e.id DESC
|
|
656
|
+
LIMIT ? OFFSET ?`
|
|
657
|
+
).all(...params, limit, offset);
|
|
658
|
+
return batchLoadEventRecords(
|
|
659
|
+
db,
|
|
660
|
+
rows.map((r) => r.id)
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
var init_get_contact_journal = __esm({
|
|
664
|
+
"src/events/get_contact_journal.ts"() {
|
|
665
|
+
init_assert_default_ordering();
|
|
666
|
+
init_resolve_list_limit_offset();
|
|
667
|
+
init_loaders();
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// src/resolve_now.ts
|
|
672
|
+
function resolveNow(now) {
|
|
673
|
+
if (now !== void 0) {
|
|
674
|
+
if (!Number.isFinite(now) || now < 0) {
|
|
675
|
+
throw new AffinityValidationError(
|
|
676
|
+
"now must be a finite non-negative number"
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return now;
|
|
680
|
+
}
|
|
681
|
+
return Date.now();
|
|
682
|
+
}
|
|
683
|
+
var init_resolve_now = __esm({
|
|
684
|
+
"src/resolve_now.ts"() {
|
|
685
|
+
init_affinity_validation_error();
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// src/events/list_open_commitments.ts
|
|
690
|
+
function mapResolution(resolvedAt, resolution) {
|
|
691
|
+
if (resolvedAt === null) {
|
|
692
|
+
return "open";
|
|
693
|
+
}
|
|
694
|
+
if (resolution === "cancelled") {
|
|
695
|
+
return "cancelled";
|
|
696
|
+
}
|
|
697
|
+
return "resolved";
|
|
698
|
+
}
|
|
699
|
+
function listOpenCommitments(db, filters, options) {
|
|
700
|
+
assertDefaultOrdering("listOpenCommitments", options);
|
|
701
|
+
const now = resolveNow(options?.now);
|
|
702
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
703
|
+
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
704
|
+
const params = [];
|
|
705
|
+
if (options?.since !== void 0) {
|
|
706
|
+
clauses.push("e.occurred_at >= ?");
|
|
707
|
+
params.push(options.since);
|
|
708
|
+
}
|
|
709
|
+
if (options?.until !== void 0) {
|
|
710
|
+
clauses.push("e.occurred_at <= ?");
|
|
711
|
+
params.push(options.until);
|
|
712
|
+
}
|
|
713
|
+
if (filters?.commitmentType !== void 0) {
|
|
714
|
+
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
715
|
+
throw new AffinityValidationError("invalid commitmentType filter");
|
|
716
|
+
}
|
|
717
|
+
clauses.push("oc.commitment_type = ?");
|
|
718
|
+
params.push(filters.commitmentType);
|
|
719
|
+
}
|
|
720
|
+
if (filters?.contactId !== void 0) {
|
|
721
|
+
clauses.push(
|
|
722
|
+
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
723
|
+
);
|
|
724
|
+
params.push(filters.contactId);
|
|
725
|
+
}
|
|
726
|
+
if (filters?.linkId !== void 0) {
|
|
727
|
+
clauses.push(
|
|
728
|
+
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
729
|
+
);
|
|
730
|
+
params.push(filters.linkId);
|
|
731
|
+
}
|
|
732
|
+
if (filters?.horizonDays !== void 0) {
|
|
733
|
+
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
734
|
+
throw new AffinityValidationError(
|
|
735
|
+
"horizonDays must be a non-negative integer"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
const anchor = options?.since ?? now;
|
|
739
|
+
clauses.push("oc.due_at IS NOT NULL");
|
|
740
|
+
clauses.push("oc.due_at <= ?");
|
|
741
|
+
params.push(anchor + filters.horizonDays * DAY_MS);
|
|
742
|
+
}
|
|
743
|
+
const where = clauses.join(" AND ");
|
|
744
|
+
const rows = db.prepare(
|
|
745
|
+
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
746
|
+
e.type, e.summary
|
|
747
|
+
FROM open_commitments oc
|
|
748
|
+
INNER JOIN events e ON e.id = oc.event_id
|
|
749
|
+
WHERE ${where}
|
|
750
|
+
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
751
|
+
LIMIT ? OFFSET ?`
|
|
752
|
+
).all(...params, limit, offset);
|
|
753
|
+
const eventIds = rows.map((r) => r.event_id);
|
|
754
|
+
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
755
|
+
return rows.map((r) => ({
|
|
756
|
+
eventId: r.event_id,
|
|
757
|
+
type: r.type,
|
|
758
|
+
summary: r.summary,
|
|
759
|
+
participants: partsByEvent.get(r.event_id) ?? [],
|
|
760
|
+
dueAt: r.due_at,
|
|
761
|
+
dueAtIso: r.due_at === null ? null : new Date(r.due_at).toISOString(),
|
|
762
|
+
overdue: r.due_at !== null && r.due_at < now,
|
|
763
|
+
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
var DAY_MS;
|
|
767
|
+
var init_list_open_commitments = __esm({
|
|
768
|
+
"src/events/list_open_commitments.ts"() {
|
|
769
|
+
init_affinity_validation_error();
|
|
770
|
+
init_assert_default_ordering();
|
|
771
|
+
init_resolve_list_limit_offset();
|
|
772
|
+
init_resolve_now();
|
|
773
|
+
init_loaders();
|
|
774
|
+
DAY_MS = 864e5;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// src/with_transaction.ts
|
|
779
|
+
function withTransaction(db, fn) {
|
|
780
|
+
const depth = transactionDepth.get(db) ?? 0;
|
|
781
|
+
const savepoint = `affinity_${String(depth)}`;
|
|
370
782
|
if (depth === 0) {
|
|
371
783
|
db.exec("BEGIN IMMEDIATE");
|
|
372
784
|
} else {
|
|
@@ -667,13 +1079,20 @@ function loadContactProfileRecord(db, contactId, topLinksLimit) {
|
|
|
667
1079
|
const topLinks = linkRows.map((l) => mapLinkRowToLinkListItem(l));
|
|
668
1080
|
const activeDates = loadActiveDatesForContact(db, contactId);
|
|
669
1081
|
const contactRollup = loadContactRollup(db, contactId);
|
|
1082
|
+
const winner = row.merged_into_contact_id === null ? null : getContactRowById(db, row.merged_into_contact_id);
|
|
670
1083
|
return {
|
|
1084
|
+
...winner ? { mergedInto: { id: winner.id, name: winner.name } } : {},
|
|
671
1085
|
contact: mapContactRowToContactCore(row),
|
|
672
1086
|
identities,
|
|
673
1087
|
attributes,
|
|
674
1088
|
topLinks,
|
|
675
1089
|
activeDates,
|
|
676
|
-
warnings:
|
|
1090
|
+
warnings: [
|
|
1091
|
+
...contactRollup?.warnings ?? [],
|
|
1092
|
+
...winner ? [
|
|
1093
|
+
`Contact #${row.id} was merged into #${winner.id} ${winner.name}; use contactId ${winner.id}`
|
|
1094
|
+
] : []
|
|
1095
|
+
],
|
|
677
1096
|
rollups: contactRollup?.rollup ?? null
|
|
678
1097
|
};
|
|
679
1098
|
}
|
|
@@ -691,10 +1110,32 @@ var init_load_contact_profile_record = __esm({
|
|
|
691
1110
|
// src/contacts/get_contact_profile.ts
|
|
692
1111
|
function getContactProfile(db, contactId, options) {
|
|
693
1112
|
const t = options?.topLinksLimit ?? 8;
|
|
694
|
-
|
|
1113
|
+
const profile = loadContactProfileRecord(db, contactId, t);
|
|
1114
|
+
if (!profile) return null;
|
|
1115
|
+
const ties = db.prepare(`SELECT l.id, l.kind, l.role, c.id AS contactId, c.name
|
|
1116
|
+
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
|
|
1117
|
+
WHERE l.is_structural = 1 AND l.removed_at IS NULL AND (l.from_contact_id = ? OR l.to_contact_id = ?)
|
|
1118
|
+
AND c.deleted_at IS NULL ORDER BY l.id`).all(contactId, contactId, contactId);
|
|
1119
|
+
return {
|
|
1120
|
+
...profile,
|
|
1121
|
+
recentEvents: getContactJournal(db, contactId, {
|
|
1122
|
+
limit: options?.recentEventsLimit ?? 5
|
|
1123
|
+
}).map((event) => ({
|
|
1124
|
+
...event,
|
|
1125
|
+
occurredAtIso: new Date(event.occurredAt).toISOString()
|
|
1126
|
+
})),
|
|
1127
|
+
openCommitments: listOpenCommitments(
|
|
1128
|
+
db,
|
|
1129
|
+
{ contactId },
|
|
1130
|
+
options?.now === void 0 ? void 0 : { now: options.now }
|
|
1131
|
+
),
|
|
1132
|
+
ties
|
|
1133
|
+
};
|
|
695
1134
|
}
|
|
696
1135
|
var init_get_contact_profile = __esm({
|
|
697
1136
|
"src/contacts/get_contact_profile.ts"() {
|
|
1137
|
+
init_get_contact_journal();
|
|
1138
|
+
init_list_open_commitments();
|
|
698
1139
|
init_load_contact_profile_record();
|
|
699
1140
|
}
|
|
700
1141
|
});
|
|
@@ -715,83 +1156,22 @@ var init_get_owner_profile = __esm({
|
|
|
715
1156
|
}
|
|
716
1157
|
});
|
|
717
1158
|
|
|
718
|
-
// src/
|
|
719
|
-
function
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1159
|
+
// src/events/list_moments.ts
|
|
1160
|
+
function listMoments(db, filters, options) {
|
|
1161
|
+
assertDefaultOrdering("listMoments", options);
|
|
1162
|
+
const { limit, offset } = resolveListLimitOffset(options);
|
|
1163
|
+
const clauses = [
|
|
1164
|
+
"e.deleted_at IS NULL",
|
|
1165
|
+
"le.moment_kind IS NOT NULL"
|
|
1166
|
+
];
|
|
1167
|
+
const params = [];
|
|
1168
|
+
if (options?.since !== void 0) {
|
|
1169
|
+
clauses.push("le.occurred_at >= ?");
|
|
1170
|
+
params.push(options.since);
|
|
724
1171
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
init_affinity_validation_error();
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
// src/lib/types/read_list_options.ts
|
|
733
|
-
var AFFINITY_DEFAULT_LIST_LIMIT, AFFINITY_MAX_LIST_LIMIT;
|
|
734
|
-
var init_read_list_options = __esm({
|
|
735
|
-
"src/lib/types/read_list_options.ts"() {
|
|
736
|
-
AFFINITY_DEFAULT_LIST_LIMIT = 50;
|
|
737
|
-
AFFINITY_MAX_LIST_LIMIT = 250;
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
// src/lib/types/resolve_affinity_list_limit.ts
|
|
742
|
-
function resolveAffinityListLimit(limit) {
|
|
743
|
-
if (limit === void 0) {
|
|
744
|
-
return AFFINITY_DEFAULT_LIST_LIMIT;
|
|
745
|
-
}
|
|
746
|
-
if (!Number.isInteger(limit) || limit < 1) {
|
|
747
|
-
throw new AffinityValidationError("limit must be a positive integer");
|
|
748
|
-
}
|
|
749
|
-
if (limit > AFFINITY_MAX_LIST_LIMIT) {
|
|
750
|
-
throw new AffinityValidationError(
|
|
751
|
-
`limit must be <= ${String(AFFINITY_MAX_LIST_LIMIT)}`
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
return limit;
|
|
755
|
-
}
|
|
756
|
-
var init_resolve_affinity_list_limit = __esm({
|
|
757
|
-
"src/lib/types/resolve_affinity_list_limit.ts"() {
|
|
758
|
-
init_affinity_validation_error();
|
|
759
|
-
init_read_list_options();
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
// src/lib/read/resolve_list_limit_offset.ts
|
|
764
|
-
function resolveListLimitOffset(options) {
|
|
765
|
-
const limit = resolveAffinityListLimit(options?.limit);
|
|
766
|
-
const offset = options?.offset ?? 0;
|
|
767
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
768
|
-
throw new AffinityValidationError("offset must be a non-negative integer");
|
|
769
|
-
}
|
|
770
|
-
return { limit, offset };
|
|
771
|
-
}
|
|
772
|
-
var init_resolve_list_limit_offset = __esm({
|
|
773
|
-
"src/lib/read/resolve_list_limit_offset.ts"() {
|
|
774
|
-
init_affinity_validation_error();
|
|
775
|
-
init_resolve_affinity_list_limit();
|
|
776
|
-
}
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// src/events/list_moments.ts
|
|
780
|
-
function listMoments(db, filters, options) {
|
|
781
|
-
assertDefaultOrdering("listMoments", options);
|
|
782
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
783
|
-
const clauses = [
|
|
784
|
-
"e.deleted_at IS NULL",
|
|
785
|
-
"le.moment_kind IS NOT NULL"
|
|
786
|
-
];
|
|
787
|
-
const params = [];
|
|
788
|
-
if (options?.since !== void 0) {
|
|
789
|
-
clauses.push("le.occurred_at >= ?");
|
|
790
|
-
params.push(options.since);
|
|
791
|
-
}
|
|
792
|
-
if (options?.until !== void 0) {
|
|
793
|
-
clauses.push("le.occurred_at <= ?");
|
|
794
|
-
params.push(options.until);
|
|
1172
|
+
if (options?.until !== void 0) {
|
|
1173
|
+
clauses.push("le.occurred_at <= ?");
|
|
1174
|
+
params.push(options.until);
|
|
795
1175
|
}
|
|
796
1176
|
if (filters?.contactId !== void 0) {
|
|
797
1177
|
clauses.push(
|
|
@@ -837,109 +1217,6 @@ var init_list_moments = __esm({
|
|
|
837
1217
|
}
|
|
838
1218
|
});
|
|
839
1219
|
|
|
840
|
-
// src/events/loaders.ts
|
|
841
|
-
function loadEventParticipantViews(db, eventId) {
|
|
842
|
-
const rows = db.prepare(
|
|
843
|
-
`SELECT contact_id, role, directionality
|
|
844
|
-
FROM event_participants
|
|
845
|
-
WHERE event_id = ?`
|
|
846
|
-
).all(eventId);
|
|
847
|
-
return rows.map((row) => ({
|
|
848
|
-
contactId: row.contact_id,
|
|
849
|
-
role: row.role,
|
|
850
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
851
|
-
}));
|
|
852
|
-
}
|
|
853
|
-
function loadEventRecord(db, eventId) {
|
|
854
|
-
const row = getEventRowById(db, eventId);
|
|
855
|
-
if (!row) {
|
|
856
|
-
throw new AffinityInvariantError("event not found");
|
|
857
|
-
}
|
|
858
|
-
const participants = loadEventParticipantViews(db, eventId);
|
|
859
|
-
return mapEventRowToEventRecord(row, participants);
|
|
860
|
-
}
|
|
861
|
-
function batchLoadEventRecords(db, eventIds) {
|
|
862
|
-
if (eventIds.length === 0) {
|
|
863
|
-
return [];
|
|
864
|
-
}
|
|
865
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
866
|
-
const eventRows = db.prepare(
|
|
867
|
-
`SELECT id, type, occurred_at, summary, significance, moment_kind,
|
|
868
|
-
recurrence_kind, anchor_month, anchor_day,
|
|
869
|
-
anchor_contact_id, anchor_link_id,
|
|
870
|
-
created_at, updated_at, deleted_at
|
|
871
|
-
FROM events WHERE id IN (${ph}) AND deleted_at IS NULL`
|
|
872
|
-
).all(...eventIds);
|
|
873
|
-
const partRows = db.prepare(
|
|
874
|
-
`SELECT event_id, contact_id, role, directionality
|
|
875
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
876
|
-
).all(...eventIds);
|
|
877
|
-
const partsByEvent = /* @__PURE__ */ new Map();
|
|
878
|
-
for (const p of partRows) {
|
|
879
|
-
let arr = partsByEvent.get(p.event_id);
|
|
880
|
-
if (!arr) {
|
|
881
|
-
arr = [];
|
|
882
|
-
partsByEvent.set(p.event_id, arr);
|
|
883
|
-
}
|
|
884
|
-
arr.push({
|
|
885
|
-
contactId: p.contact_id,
|
|
886
|
-
role: p.role,
|
|
887
|
-
...p.directionality === null ? {} : { directionality: p.directionality }
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
const rowById = new Map(eventRows.map((r) => [r.id, r]));
|
|
891
|
-
const out = [];
|
|
892
|
-
for (const id of eventIds) {
|
|
893
|
-
const row = rowById.get(id);
|
|
894
|
-
if (row) {
|
|
895
|
-
out.push(mapEventRowToEventRecord(row, partsByEvent.get(id) ?? []));
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return out;
|
|
899
|
-
}
|
|
900
|
-
function batchLoadEventParticipantViews(db, eventIds) {
|
|
901
|
-
const result = /* @__PURE__ */ new Map();
|
|
902
|
-
if (eventIds.length === 0) {
|
|
903
|
-
return result;
|
|
904
|
-
}
|
|
905
|
-
const ph = eventIds.map(() => "?").join(", ");
|
|
906
|
-
const rows = db.prepare(
|
|
907
|
-
`SELECT event_id, contact_id, role, directionality
|
|
908
|
-
FROM event_participants WHERE event_id IN (${ph})`
|
|
909
|
-
).all(...eventIds);
|
|
910
|
-
for (const row of rows) {
|
|
911
|
-
let arr = result.get(row.event_id);
|
|
912
|
-
if (!arr) {
|
|
913
|
-
arr = [];
|
|
914
|
-
result.set(row.event_id, arr);
|
|
915
|
-
}
|
|
916
|
-
arr.push({
|
|
917
|
-
contactId: row.contact_id,
|
|
918
|
-
role: row.role,
|
|
919
|
-
...row.directionality === null ? {} : { directionality: row.directionality }
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
return result;
|
|
923
|
-
}
|
|
924
|
-
function requireDateAnchorEvent(db, eventId) {
|
|
925
|
-
const row = getEventRowById(db, eventId);
|
|
926
|
-
if (!row) {
|
|
927
|
-
throw new AffinityNotFoundError("event not found");
|
|
928
|
-
}
|
|
929
|
-
if (row.type !== "date_anchor") {
|
|
930
|
-
throw new AffinityInvariantError("not a date anchor");
|
|
931
|
-
}
|
|
932
|
-
return row;
|
|
933
|
-
}
|
|
934
|
-
var init_loaders = __esm({
|
|
935
|
-
"src/events/loaders.ts"() {
|
|
936
|
-
init_affinity_invariant_error();
|
|
937
|
-
init_affinity_not_found_error();
|
|
938
|
-
init_mappers();
|
|
939
|
-
init_queries2();
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
|
|
943
1220
|
// src/links/queries.ts
|
|
944
1221
|
function getLinkRowById(db, id) {
|
|
945
1222
|
const row = db.prepare(
|
|
@@ -1212,7 +1489,7 @@ function recencyScore(occurredAt, cadenceDays, now) {
|
|
|
1212
1489
|
return 0;
|
|
1213
1490
|
}
|
|
1214
1491
|
const cadence = Math.max(cadenceDays ?? 0, 1);
|
|
1215
|
-
const daysSince = Math.max(0, (now - occurredAt) /
|
|
1492
|
+
const daysSince = Math.max(0, (now - occurredAt) / DAY_MS2);
|
|
1216
1493
|
return Math.max(0, 1 - daysSince / cadence);
|
|
1217
1494
|
}
|
|
1218
1495
|
function refreshLinkRollup(db, linkId, now) {
|
|
@@ -1223,15 +1500,15 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1223
1500
|
const latest = db.prepare(
|
|
1224
1501
|
`SELECT event_id, occurred_at
|
|
1225
1502
|
FROM link_event_effects
|
|
1226
|
-
WHERE link_id = ?
|
|
1503
|
+
WHERE link_id = ? AND counts_for_recency = 1
|
|
1227
1504
|
ORDER BY occurred_at DESC, event_id DESC
|
|
1228
1505
|
LIMIT 1`
|
|
1229
1506
|
).get(linkId);
|
|
1230
|
-
const trailingWindowStart = now - 180 *
|
|
1507
|
+
const trailingWindowStart = now - 180 * DAY_MS2;
|
|
1231
1508
|
const counts = db.prepare(
|
|
1232
1509
|
`SELECT
|
|
1233
1510
|
COUNT(*) AS total_meaningful_events,
|
|
1234
|
-
SUM(CASE WHEN affinity_delta > 0 OR trust_delta > 0 THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1511
|
+
SUM(CASE WHEN counts_for_recency = 1 AND (affinity_delta > 0 OR trust_delta > 0) THEN 1 ELSE 0 END) AS positive_meaningful_events
|
|
1235
1512
|
FROM link_event_effects
|
|
1236
1513
|
WHERE link_id = ? AND occurred_at >= ?`
|
|
1237
1514
|
).get(linkId, trailingWindowStart);
|
|
@@ -1239,7 +1516,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1239
1516
|
`SELECT ep.directionality
|
|
1240
1517
|
FROM link_event_effects le
|
|
1241
1518
|
INNER JOIN event_participants ep ON ep.event_id = le.event_id
|
|
1242
|
-
WHERE le.link_id = ? AND ep.contact_id = ?
|
|
1519
|
+
WHERE le.link_id = ? AND le.counts_for_recency = 1 AND ep.contact_id = ?
|
|
1243
1520
|
AND ep.directionality IS NOT NULL
|
|
1244
1521
|
AND le.occurred_at >= ?`
|
|
1245
1522
|
).all(linkId, link.from_contact_id, trailingWindowStart);
|
|
@@ -1265,7 +1542,7 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1265
1542
|
link.cadence_days,
|
|
1266
1543
|
now
|
|
1267
1544
|
);
|
|
1268
|
-
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) /
|
|
1545
|
+
const daysSinceLastMeaningfulEvent = latest === void 0 ? 0 : Math.max(0, (now - latest.occurred_at) / DAY_MS2);
|
|
1269
1546
|
const driftPriorityValue = driftPriority({
|
|
1270
1547
|
daysSinceLastMeaningfulEvent,
|
|
1271
1548
|
cadenceDays: link.cadence_days ?? 1,
|
|
@@ -1348,13 +1625,13 @@ function refreshLinkRollup(db, linkId, now) {
|
|
|
1348
1625
|
refreshContactRollup(db, link.from_contact_id, now);
|
|
1349
1626
|
refreshContactRollup(db, link.to_contact_id, now);
|
|
1350
1627
|
}
|
|
1351
|
-
var
|
|
1628
|
+
var DAY_MS2;
|
|
1352
1629
|
var init_rollups2 = __esm({
|
|
1353
1630
|
"src/links/rollups.ts"() {
|
|
1354
1631
|
init_rollups();
|
|
1355
1632
|
init_mechanics();
|
|
1356
1633
|
init_queries3();
|
|
1357
|
-
|
|
1634
|
+
DAY_MS2 = 864e5;
|
|
1358
1635
|
}
|
|
1359
1636
|
});
|
|
1360
1637
|
|
|
@@ -1412,8 +1689,61 @@ var init_get_link_detail = __esm({
|
|
|
1412
1689
|
}
|
|
1413
1690
|
});
|
|
1414
1691
|
|
|
1692
|
+
// src/tools/parse_time_input.ts
|
|
1693
|
+
function parseTimeInput(value, field = "time", now = Date.now()) {
|
|
1694
|
+
const fail = () => {
|
|
1695
|
+
throw new AffinityValidationError(`${field}: expected ${timeInputForms}`);
|
|
1696
|
+
};
|
|
1697
|
+
if (typeof value === "number")
|
|
1698
|
+
return Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()) ? value : fail();
|
|
1699
|
+
if (typeof value !== "string") return fail();
|
|
1700
|
+
const text = value.trim();
|
|
1701
|
+
const relative = /^(?:today|tomorrow|yesterday|[+-]\d+d)$/.test(text);
|
|
1702
|
+
let day;
|
|
1703
|
+
if (relative) {
|
|
1704
|
+
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
1705
|
+
day = Math.floor(now / 864e5) * 864e5 + offset * 864e5;
|
|
1706
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
1707
|
+
day = Date.parse(`${text}T00:00:00.000Z`);
|
|
1708
|
+
if (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== text)
|
|
1709
|
+
return fail();
|
|
1710
|
+
} else {
|
|
1711
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
1712
|
+
text
|
|
1713
|
+
))
|
|
1714
|
+
return fail();
|
|
1715
|
+
const calendar = Date.parse(`${text.slice(0, 10)}T00:00:00Z`);
|
|
1716
|
+
if (!Number.isFinite(calendar) || new Date(calendar).toISOString().slice(0, 10) !== text.slice(0, 10))
|
|
1717
|
+
return fail();
|
|
1718
|
+
const parsed = Date.parse(text);
|
|
1719
|
+
return Number.isFinite(parsed) ? parsed : fail();
|
|
1720
|
+
}
|
|
1721
|
+
const result = day + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0);
|
|
1722
|
+
return Number.isSafeInteger(result) && Number.isFinite(new Date(result).getTime()) ? result : fail();
|
|
1723
|
+
}
|
|
1724
|
+
var timeInputForms;
|
|
1725
|
+
var init_parse_time_input = __esm({
|
|
1726
|
+
"src/tools/parse_time_input.ts"() {
|
|
1727
|
+
init_affinity_validation_error();
|
|
1728
|
+
timeInputForms = "epoch milliseconds integer, ISO 8601 date-time with Z/offset, YYYY-MM-DD, today, tomorrow, yesterday, +3d or -2d";
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1415
1732
|
// src/tools/tool_metadata.ts
|
|
1416
1733
|
function defineAffinityTool(definition) {
|
|
1734
|
+
const visit = (schema) => {
|
|
1735
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
1736
|
+
if ((key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.type === "integer") {
|
|
1737
|
+
child.type = void 0;
|
|
1738
|
+
child.oneOf = [{ type: "integer" }, { type: "string" }];
|
|
1739
|
+
child.description = `${child.description ?? "Time."} Accepts ${timeInputForms}. UTC midnight for dates; dueAt uses UTC end of day.`;
|
|
1740
|
+
}
|
|
1741
|
+
visit(child);
|
|
1742
|
+
}
|
|
1743
|
+
if (schema.items) visit(schema.items);
|
|
1744
|
+
for (const child of schema.oneOf ?? schema.anyOf ?? []) visit(child);
|
|
1745
|
+
};
|
|
1746
|
+
visit(definition.inputSchema);
|
|
1417
1747
|
return definition;
|
|
1418
1748
|
}
|
|
1419
1749
|
function stringSchema(description, options = {}) {
|
|
@@ -1460,8 +1790,15 @@ function objectSchema(properties, required, description) {
|
|
|
1460
1790
|
function nullableStringSchema(description) {
|
|
1461
1791
|
return { anyOf: [{ type: "string" }, { type: "null" }], description };
|
|
1462
1792
|
}
|
|
1793
|
+
function oneOfSchema(schemas, description) {
|
|
1794
|
+
const schema = {
|
|
1795
|
+
oneOf: schemas
|
|
1796
|
+
};
|
|
1797
|
+
return schema;
|
|
1798
|
+
}
|
|
1463
1799
|
var init_tool_metadata = __esm({
|
|
1464
1800
|
"src/tools/tool_metadata.ts"() {
|
|
1801
|
+
init_parse_time_input();
|
|
1465
1802
|
}
|
|
1466
1803
|
});
|
|
1467
1804
|
|
|
@@ -1502,6 +1839,7 @@ function toContactEntityRef(value) {
|
|
|
1502
1839
|
function toContactListToolItem(value) {
|
|
1503
1840
|
return {
|
|
1504
1841
|
...toContactEntityRef(value),
|
|
1842
|
+
..."matchedOn" in value ? { matchedOn: value.matchedOn } : {},
|
|
1505
1843
|
subtitle: "kind" in value ? value.kind : void 0
|
|
1506
1844
|
};
|
|
1507
1845
|
}
|
|
@@ -1533,7 +1871,9 @@ function toEventEntityRef(value) {
|
|
|
1533
1871
|
}
|
|
1534
1872
|
function toEventListToolItem(value) {
|
|
1535
1873
|
return {
|
|
1874
|
+
...value,
|
|
1536
1875
|
...toEventEntityRef(value),
|
|
1876
|
+
occurredAtIso: new Date(value.occurredAt).toISOString(),
|
|
1537
1877
|
snippet: `${value.participants.length} participant(s)`
|
|
1538
1878
|
};
|
|
1539
1879
|
}
|
|
@@ -1595,7 +1935,7 @@ function withOptionalFields(result, options) {
|
|
|
1595
1935
|
if (options.warnings && options.warnings.length > 0) {
|
|
1596
1936
|
result.warnings = options.warnings;
|
|
1597
1937
|
}
|
|
1598
|
-
return result;
|
|
1938
|
+
return roundToolValues(result);
|
|
1599
1939
|
}
|
|
1600
1940
|
function toolWarning(code, message) {
|
|
1601
1941
|
return { code, message };
|
|
@@ -1659,13 +1999,35 @@ function toolFailure(kind, code, summary, message, options = {}) {
|
|
|
1659
1999
|
options
|
|
1660
2000
|
);
|
|
1661
2001
|
}
|
|
2002
|
+
function roundToolValues(value) {
|
|
2003
|
+
if (typeof value === "number")
|
|
2004
|
+
return Number.isInteger(value) ? value : Math.round(value * 1e3) / 1e3;
|
|
2005
|
+
if (Array.isArray(value)) return value.map(roundToolValues);
|
|
2006
|
+
if (value !== null && typeof value === "object")
|
|
2007
|
+
return Object.fromEntries(
|
|
2008
|
+
Object.entries(value).map(([key, child]) => [
|
|
2009
|
+
key,
|
|
2010
|
+
roundToolValues(child)
|
|
2011
|
+
])
|
|
2012
|
+
);
|
|
2013
|
+
return value;
|
|
2014
|
+
}
|
|
1662
2015
|
var init_tool_types = __esm({
|
|
1663
2016
|
"src/tools/tool_types.ts"() {
|
|
1664
2017
|
}
|
|
1665
2018
|
});
|
|
1666
2019
|
|
|
1667
2020
|
// src/tools/tool_resolvers.ts
|
|
1668
|
-
function resolveContactLocator(db, locator, field = "contact") {
|
|
2021
|
+
function resolveContactLocator(db, locator, field = "contact", allowMerged = false) {
|
|
2022
|
+
if (!locator || typeof locator !== "object")
|
|
2023
|
+
return {
|
|
2024
|
+
ok: false,
|
|
2025
|
+
result: toolNeedsClarification(
|
|
2026
|
+
"missing_required_choice",
|
|
2027
|
+
`Provide ${field} locator.`,
|
|
2028
|
+
[field]
|
|
2029
|
+
)
|
|
2030
|
+
};
|
|
1669
2031
|
if ("contactId" in locator) {
|
|
1670
2032
|
const row2 = getContactRowById(db, locator.contactId);
|
|
1671
2033
|
if (!row2) {
|
|
@@ -1679,8 +2041,25 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
1679
2041
|
)
|
|
1680
2042
|
};
|
|
1681
2043
|
}
|
|
2044
|
+
if (!allowMerged && row2.merged_into_contact_id !== null) {
|
|
2045
|
+
const winner = getContactRowById(db, row2.merged_into_contact_id);
|
|
2046
|
+
const message = `Contact #${row2.id} was merged into #${row2.merged_into_contact_id} ${winner?.name ?? ""}; use contactId ${row2.merged_into_contact_id}`;
|
|
2047
|
+
return {
|
|
2048
|
+
ok: false,
|
|
2049
|
+
result: toolFailure("domain", "invalid_state", message, message)
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
1682
2052
|
return { ok: true, value: row2 };
|
|
1683
2053
|
}
|
|
2054
|
+
if (!("identity" in locator) || !locator.identity)
|
|
2055
|
+
return {
|
|
2056
|
+
ok: false,
|
|
2057
|
+
result: toolNeedsClarification(
|
|
2058
|
+
"missing_required_choice",
|
|
2059
|
+
`Provide ${field}.contactId or identity.`,
|
|
2060
|
+
[field]
|
|
2061
|
+
)
|
|
2062
|
+
};
|
|
1684
2063
|
const normalizedKey = normalizeIdentityKey(
|
|
1685
2064
|
locator.identity.type,
|
|
1686
2065
|
locator.identity.value
|
|
@@ -1738,6 +2117,15 @@ function resolveContactLocator(db, locator, field = "contact") {
|
|
|
1738
2117
|
return { ok: true, value: matchedContact };
|
|
1739
2118
|
}
|
|
1740
2119
|
function resolveLinkLocator(db, locator, field = "link") {
|
|
2120
|
+
if (!locator || typeof locator !== "object")
|
|
2121
|
+
return {
|
|
2122
|
+
ok: false,
|
|
2123
|
+
result: toolNeedsClarification(
|
|
2124
|
+
"missing_required_choice",
|
|
2125
|
+
`Provide ${field} locator.`,
|
|
2126
|
+
[field]
|
|
2127
|
+
)
|
|
2128
|
+
};
|
|
1741
2129
|
if ("linkId" in locator) {
|
|
1742
2130
|
const row = getLinkRowById(db, locator.linkId);
|
|
1743
2131
|
if (!row) {
|
|
@@ -1753,6 +2141,15 @@ function resolveLinkLocator(db, locator, field = "link") {
|
|
|
1753
2141
|
}
|
|
1754
2142
|
return { ok: true, value: row };
|
|
1755
2143
|
}
|
|
2144
|
+
if (!("endpoints" in locator) || !locator.endpoints)
|
|
2145
|
+
return {
|
|
2146
|
+
ok: false,
|
|
2147
|
+
result: toolNeedsClarification(
|
|
2148
|
+
"missing_required_choice",
|
|
2149
|
+
`Provide ${field}.linkId or endpoints.`,
|
|
2150
|
+
[field]
|
|
2151
|
+
)
|
|
2152
|
+
};
|
|
1756
2153
|
const { endpoints } = locator;
|
|
1757
2154
|
const clauses = [
|
|
1758
2155
|
"removed_at IS NULL",
|
|
@@ -1881,6 +2278,97 @@ var init_tool_resolvers = __esm({
|
|
|
1881
2278
|
}
|
|
1882
2279
|
});
|
|
1883
2280
|
|
|
2281
|
+
// src/tools/validate_tool_input.ts
|
|
2282
|
+
function validateToolInput(schema, value, path = "input", injectedNow = Date.now()) {
|
|
2283
|
+
if (schema.anyOf || schema.oneOf) {
|
|
2284
|
+
const choices = schema.anyOf ?? schema.oneOf ?? [];
|
|
2285
|
+
return choices.some(
|
|
2286
|
+
(choice) => validateToolInput(choice, value, path, injectedNow) === null
|
|
2287
|
+
) ? null : `${path}: invalid value`;
|
|
2288
|
+
}
|
|
2289
|
+
if (schema.type === "object") {
|
|
2290
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
2291
|
+
return `${path}: missing input or expected object`;
|
|
2292
|
+
const object = value;
|
|
2293
|
+
if (path.endsWith(".patch") && schema.additionalProperties === false) {
|
|
2294
|
+
for (const key of Object.keys(object)) {
|
|
2295
|
+
if (!Object.hasOwn(schema.properties ?? {}, key))
|
|
2296
|
+
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(", ")}`;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (object.action === "revise" && schema.properties?.patch && object.patch === void 0)
|
|
2300
|
+
return `${path}.patch: missing required field`;
|
|
2301
|
+
if (object.patch !== void 0 && !schema.properties?.patch)
|
|
2302
|
+
return `${path}.patch: unknown patch key; allowed keys: none (use the action's named fields)`;
|
|
2303
|
+
const referenceNow = object.now === void 0 ? injectedNow : (() => {
|
|
2304
|
+
try {
|
|
2305
|
+
return parseTimeInput(
|
|
2306
|
+
object.now,
|
|
2307
|
+
`${path}.now`,
|
|
2308
|
+
injectedNow
|
|
2309
|
+
);
|
|
2310
|
+
} catch {
|
|
2311
|
+
return Date.now();
|
|
2312
|
+
}
|
|
2313
|
+
})();
|
|
2314
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2315
|
+
if (object[key] !== void 0 && (key.endsWith("At") || ["now", "asOf", "since", "until"].includes(key)) && child.oneOf?.some((choice) => choice.type === "integer")) {
|
|
2316
|
+
try {
|
|
2317
|
+
object[key] = parseTimeInput(
|
|
2318
|
+
object[key],
|
|
2319
|
+
`${path}.${key}`,
|
|
2320
|
+
referenceNow
|
|
2321
|
+
);
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
return error.message;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
for (const key of schema.required ?? []) {
|
|
2328
|
+
if (object[key] === void 0)
|
|
2329
|
+
return `${path}.${key}: missing required field`;
|
|
2330
|
+
}
|
|
2331
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2332
|
+
if (object[key] === void 0) continue;
|
|
2333
|
+
const error = validateToolInput(
|
|
2334
|
+
child,
|
|
2335
|
+
object[key],
|
|
2336
|
+
`${path}.${key}`,
|
|
2337
|
+
referenceNow
|
|
2338
|
+
);
|
|
2339
|
+
if (error) return error;
|
|
2340
|
+
}
|
|
2341
|
+
return null;
|
|
2342
|
+
}
|
|
2343
|
+
if (schema.type === "array") {
|
|
2344
|
+
if (!Array.isArray(value)) return `${path}: expected array`;
|
|
2345
|
+
for (const [index, item] of value.entries()) {
|
|
2346
|
+
const error = validateToolInput(
|
|
2347
|
+
schema.items ?? {},
|
|
2348
|
+
item,
|
|
2349
|
+
`${path}[${index}]`,
|
|
2350
|
+
injectedNow
|
|
2351
|
+
);
|
|
2352
|
+
if (error) return error;
|
|
2353
|
+
}
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
2357
|
+
return `${path}: expected ${schema.enum.join(", ")}`;
|
|
2358
|
+
if (schema.type === "null")
|
|
2359
|
+
return value === null ? null : `${path}: expected null`;
|
|
2360
|
+
if (schema.type === "integer")
|
|
2361
|
+
return Number.isInteger(value) ? null : `${path}: expected integer`;
|
|
2362
|
+
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";
|
|
2363
|
+
if (!valid) return `${path}: expected ${schema.type}`;
|
|
2364
|
+
return null;
|
|
2365
|
+
}
|
|
2366
|
+
var init_validate_tool_input = __esm({
|
|
2367
|
+
"src/tools/validate_tool_input.ts"() {
|
|
2368
|
+
init_parse_time_input();
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
|
|
1884
2372
|
// src/tools/inspect_affinity_item_tool.ts
|
|
1885
2373
|
function contactLocatorSchema(description) {
|
|
1886
2374
|
return {
|
|
@@ -1916,7 +2404,19 @@ function linkLocatorSchema(description) {
|
|
|
1916
2404
|
description
|
|
1917
2405
|
};
|
|
1918
2406
|
}
|
|
1919
|
-
function inspectAffinityItemToolHandler(db,
|
|
2407
|
+
function inspectAffinityItemToolHandler(db, rawInput) {
|
|
2408
|
+
const input = structuredClone(rawInput);
|
|
2409
|
+
const inputError = validateToolInput(
|
|
2410
|
+
inspectAffinityItemTool.inputSchema,
|
|
2411
|
+
input
|
|
2412
|
+
);
|
|
2413
|
+
if (inputError)
|
|
2414
|
+
return toolFailure(
|
|
2415
|
+
"protocol",
|
|
2416
|
+
"invalid_input",
|
|
2417
|
+
"Invalid input.",
|
|
2418
|
+
inputError
|
|
2419
|
+
);
|
|
1920
2420
|
return withToolHandling(() => {
|
|
1921
2421
|
if (input.kind === "owner_profile") {
|
|
1922
2422
|
const detail2 = getOwnerProfile(
|
|
@@ -1934,19 +2434,21 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
1934
2434
|
return toolSuccess(
|
|
1935
2435
|
`Loaded owner profile for ${detail2.contact.name}.`,
|
|
1936
2436
|
{ kind: input.kind, detail: detail2 },
|
|
1937
|
-
{
|
|
2437
|
+
{
|
|
2438
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
2439
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
2440
|
+
}
|
|
1938
2441
|
);
|
|
1939
2442
|
}
|
|
1940
2443
|
if (input.kind === "contact_profile") {
|
|
1941
|
-
const contact = resolveContactLocator(db, input.contact, "contact");
|
|
2444
|
+
const contact = resolveContactLocator(db, input.contact, "contact", true);
|
|
1942
2445
|
if (!contact.ok) {
|
|
1943
2446
|
return contact.result;
|
|
1944
2447
|
}
|
|
1945
|
-
const detail2 = getContactProfile(
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
);
|
|
2448
|
+
const detail2 = getContactProfile(db, contact.value.id, {
|
|
2449
|
+
...input.topLinksLimit === void 0 ? {} : { topLinksLimit: input.topLinksLimit },
|
|
2450
|
+
...input.recentEventsLimit === void 0 ? {} : { recentEventsLimit: input.recentEventsLimit }
|
|
2451
|
+
});
|
|
1950
2452
|
if (!detail2) {
|
|
1951
2453
|
return toolFailure(
|
|
1952
2454
|
"domain",
|
|
@@ -1958,7 +2460,10 @@ function inspectAffinityItemToolHandler(db, input) {
|
|
|
1958
2460
|
return toolSuccess(
|
|
1959
2461
|
`Loaded contact profile for ${detail2.contact.name}.`,
|
|
1960
2462
|
{ kind: input.kind, detail: detail2 },
|
|
1961
|
-
{
|
|
2463
|
+
{
|
|
2464
|
+
entities: [toContactEntityRef(detail2.contact)],
|
|
2465
|
+
warnings: detail2.mergedInto ? [{ code: "partial_match", message: detail2.warnings.join("; ") }] : void 0
|
|
2466
|
+
}
|
|
1962
2467
|
);
|
|
1963
2468
|
}
|
|
1964
2469
|
const link = resolveLinkLocator(db, input.link, "link");
|
|
@@ -2001,6 +2506,8 @@ var init_inspect_affinity_item_tool = __esm({
|
|
|
2001
2506
|
init_tool_ref();
|
|
2002
2507
|
init_tool_resolvers();
|
|
2003
2508
|
init_tool_types();
|
|
2509
|
+
init_tool_types();
|
|
2510
|
+
init_validate_tool_input();
|
|
2004
2511
|
inspectAffinityItemTool = defineAffinityTool({
|
|
2005
2512
|
name: inspectAffinityItemToolName,
|
|
2006
2513
|
description: "Inspect one exact affinity item such as the owner profile, a contact profile, or one link detail.",
|
|
@@ -2493,9 +3000,17 @@ var init_unset_attribute = __esm({
|
|
|
2493
3000
|
});
|
|
2494
3001
|
|
|
2495
3002
|
// src/tools/tool_mutation.ts
|
|
2496
|
-
function mutationToolResult(action, receipt, entitySummary) {
|
|
3003
|
+
function mutationToolResult(action, receipt, entitySummary, fields) {
|
|
2497
3004
|
const changedCount = receipt.created.length + receipt.updated.length + receipt.archived.length + receipt.removed.length;
|
|
2498
3005
|
const data = {
|
|
3006
|
+
changes: ["created", "updated", "archived", "removed"].flatMap(
|
|
3007
|
+
(verb) => receipt[verb].map((ref) => ({
|
|
3008
|
+
entity: ref.kind,
|
|
3009
|
+
id: ref.id,
|
|
3010
|
+
action: verb,
|
|
3011
|
+
...verb === "updated" && fields ? { fields } : {}
|
|
3012
|
+
}))
|
|
3013
|
+
),
|
|
2499
3014
|
action,
|
|
2500
3015
|
primary: receipt.primary,
|
|
2501
3016
|
created: receipt.created,
|
|
@@ -2589,7 +3104,23 @@ function resolveAttributeTarget(db, target) {
|
|
|
2589
3104
|
}
|
|
2590
3105
|
return { kind: "ok", target: { kind: "link", id: link.value.id } };
|
|
2591
3106
|
}
|
|
2592
|
-
function manageAttributeToolHandler(db,
|
|
3107
|
+
function manageAttributeToolHandler(db, rawInput) {
|
|
3108
|
+
const input = structuredClone(rawInput);
|
|
3109
|
+
const inputError = validateToolInput(manageAttributeTool.inputSchema, input);
|
|
3110
|
+
if (inputError)
|
|
3111
|
+
return toolFailure(
|
|
3112
|
+
"protocol",
|
|
3113
|
+
"invalid_input",
|
|
3114
|
+
"Invalid input.",
|
|
3115
|
+
inputError
|
|
3116
|
+
);
|
|
3117
|
+
if (!input.target)
|
|
3118
|
+
return toolFailure(
|
|
3119
|
+
"protocol",
|
|
3120
|
+
"invalid_input",
|
|
3121
|
+
"Missing target.",
|
|
3122
|
+
"target is required."
|
|
3123
|
+
);
|
|
2593
3124
|
return withToolHandling(() => {
|
|
2594
3125
|
const resolved = resolveAttributeTarget(db, input.target);
|
|
2595
3126
|
if (resolved.kind === "error") {
|
|
@@ -2627,6 +3158,8 @@ var init_manage_attribute_tool = __esm({
|
|
|
2627
3158
|
init_tool_mutation();
|
|
2628
3159
|
init_tool_names();
|
|
2629
3160
|
init_tool_resolvers();
|
|
3161
|
+
init_tool_types();
|
|
3162
|
+
init_validate_tool_input();
|
|
2630
3163
|
manageAttributeTool = defineAffinityTool({
|
|
2631
3164
|
name: manageAttributeToolName,
|
|
2632
3165
|
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.",
|
|
@@ -3259,7 +3792,7 @@ function clamp3(value, min, max) {
|
|
|
3259
3792
|
}
|
|
3260
3793
|
function countSameDaySimilarEvents(db, linkId, eventType, occurredAt) {
|
|
3261
3794
|
const dayStart = utcStartOfDayMs(occurredAt);
|
|
3262
|
-
const dayEnd = dayStart +
|
|
3795
|
+
const dayEnd = dayStart + DAY_MS3;
|
|
3263
3796
|
const row = db.prepare(
|
|
3264
3797
|
`SELECT COUNT(*) AS c
|
|
3265
3798
|
FROM link_event_effects le
|
|
@@ -3272,7 +3805,7 @@ function countSameDaySimilarEvents(db, linkId, eventType, occurredAt) {
|
|
|
3272
3805
|
return Number(row.c ?? 0);
|
|
3273
3806
|
}
|
|
3274
3807
|
function weeklyPositiveBaseWeight(db, linkId, occurredAt) {
|
|
3275
|
-
const weekStart = occurredAt - 7 *
|
|
3808
|
+
const weekStart = occurredAt - 7 * DAY_MS3;
|
|
3276
3809
|
const row = db.prepare(
|
|
3277
3810
|
`SELECT SUM(base_weight) AS total
|
|
3278
3811
|
FROM link_event_effects
|
|
@@ -3284,7 +3817,7 @@ function weeklyPositiveBaseWeight(db, linkId, occurredAt) {
|
|
|
3284
3817
|
return Number(row.total ?? 0);
|
|
3285
3818
|
}
|
|
3286
3819
|
function countRepairContext(db, linkId, occurredAt) {
|
|
3287
|
-
const thirtyDaysAgo = occurredAt - 30 *
|
|
3820
|
+
const thirtyDaysAgo = occurredAt - 30 * DAY_MS3;
|
|
3288
3821
|
const hasPriorDamage = db.prepare(
|
|
3289
3822
|
`SELECT 1 FROM link_event_effects
|
|
3290
3823
|
WHERE link_id = ? AND trust_delta < 0
|
|
@@ -3359,8 +3892,8 @@ function computePreferenceMatch(db, linkId, contactId, eventType, provenance) {
|
|
|
3359
3892
|
return preferenceMatch(liked, disliked);
|
|
3360
3893
|
}
|
|
3361
3894
|
function computeDateSalienceMultiplier(db, contactId, occurredAt, significance) {
|
|
3362
|
-
const windowBefore = 7 *
|
|
3363
|
-
const windowAfter = 2 *
|
|
3895
|
+
const windowBefore = 7 * DAY_MS3;
|
|
3896
|
+
const windowAfter = 2 * DAY_MS3;
|
|
3364
3897
|
const row = db.prepare(
|
|
3365
3898
|
`SELECT 1 FROM upcoming_occurrences uo
|
|
3366
3899
|
JOIN events e ON e.id = uo.event_id
|
|
@@ -3527,9 +4060,9 @@ function applyEventEffectToLink(db, params) {
|
|
|
3527
4060
|
).get(params.linkId);
|
|
3528
4061
|
const gapDays = latest === void 0 ? cadenceBefore : Math.max(
|
|
3529
4062
|
1,
|
|
3530
|
-
Math.round((params.occurredAt - latest.occurred_at) /
|
|
4063
|
+
Math.round((params.occurredAt - latest.occurred_at) / DAY_MS3)
|
|
3531
4064
|
);
|
|
3532
|
-
const cadenceAfter = nextCadenceDays({
|
|
4065
|
+
const cadenceAfter = commitmentResolution === "broken" || commitmentResolution === "cancelled" ? cadenceBefore : nextCadenceDays({
|
|
3533
4066
|
kind: linkKind,
|
|
3534
4067
|
currentCadenceDays: cadenceBefore,
|
|
3535
4068
|
gapDays
|
|
@@ -3592,6 +4125,11 @@ function applyEventEffectToLink(db, params) {
|
|
|
3592
4125
|
params.now,
|
|
3593
4126
|
params.now
|
|
3594
4127
|
);
|
|
4128
|
+
if (commitmentResolution === "broken" || commitmentResolution === "cancelled") {
|
|
4129
|
+
db.prepare(
|
|
4130
|
+
"UPDATE link_event_effects SET counts_for_recency = 0 WHERE event_id = ? AND link_id = ?"
|
|
4131
|
+
).run(params.eventId, params.linkId);
|
|
4132
|
+
}
|
|
3595
4133
|
refreshLinkRollup(db, params.linkId, params.now);
|
|
3596
4134
|
return {
|
|
3597
4135
|
linkId: params.linkId,
|
|
@@ -3615,7 +4153,7 @@ function applyEventEffectToLink(db, params) {
|
|
|
3615
4153
|
momentKind
|
|
3616
4154
|
};
|
|
3617
4155
|
}
|
|
3618
|
-
var
|
|
4156
|
+
var DAY_MS3;
|
|
3619
4157
|
var init_effects = __esm({
|
|
3620
4158
|
"src/links/effects.ts"() {
|
|
3621
4159
|
init_calendar();
|
|
@@ -3625,7 +4163,7 @@ var init_effects = __esm({
|
|
|
3625
4163
|
init_queries3();
|
|
3626
4164
|
init_rollups2();
|
|
3627
4165
|
init_validators2();
|
|
3628
|
-
|
|
4166
|
+
DAY_MS3 = 864e5;
|
|
3629
4167
|
}
|
|
3630
4168
|
});
|
|
3631
4169
|
|
|
@@ -4006,12 +4544,16 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4006
4544
|
if (row.type !== "promise" && row.type !== "agreement") {
|
|
4007
4545
|
throw new AffinityInvariantError("not a commitment event");
|
|
4008
4546
|
}
|
|
4009
|
-
const open = db.prepare(
|
|
4547
|
+
const open = db.prepare(
|
|
4548
|
+
"SELECT resolved_at, resolution FROM open_commitments WHERE event_id = ?"
|
|
4549
|
+
).get(commitmentEventId);
|
|
4010
4550
|
if (!open) {
|
|
4011
4551
|
throw new AffinityNotFoundError("open commitment not found");
|
|
4012
4552
|
}
|
|
4013
4553
|
if (open.resolved_at !== null) {
|
|
4014
|
-
throw new
|
|
4554
|
+
throw new AffinityConflictError(
|
|
4555
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4556
|
+
);
|
|
4015
4557
|
}
|
|
4016
4558
|
const now = resolveNow(options?.now);
|
|
4017
4559
|
const originalParticipants = loadEventParticipantViews(
|
|
@@ -4024,7 +4566,9 @@ function resolveCommitment(db, commitmentEventId, resolution, options) {
|
|
|
4024
4566
|
WHERE event_id = ? AND resolved_at IS NULL`
|
|
4025
4567
|
).run(resolution, now, now, commitmentEventId);
|
|
4026
4568
|
if (Number(result.changes ?? 0) === 0) {
|
|
4027
|
-
throw new
|
|
4569
|
+
throw new AffinityConflictError(
|
|
4570
|
+
`Commitment #${commitmentEventId} is already resolved (${open.resolution})`
|
|
4571
|
+
);
|
|
4028
4572
|
}
|
|
4029
4573
|
const shouldCreateResolvingEvent = options?.resolvingEvent !== void 0 || resolution === "broken";
|
|
4030
4574
|
const created = [];
|
|
@@ -4094,9 +4638,9 @@ var init_resolve_commitment = __esm({
|
|
|
4094
4638
|
init_queries2();
|
|
4095
4639
|
init_receipts2();
|
|
4096
4640
|
init_validators();
|
|
4641
|
+
init_affinity_conflict_error();
|
|
4097
4642
|
init_affinity_invariant_error();
|
|
4098
4643
|
init_affinity_not_found_error();
|
|
4099
|
-
init_affinity_state_error();
|
|
4100
4644
|
init_affinity_validation_error();
|
|
4101
4645
|
init_resolve_now();
|
|
4102
4646
|
init_with_transaction();
|
|
@@ -4129,9 +4673,25 @@ function ensureOwnerParticipates(db, participants) {
|
|
|
4129
4673
|
function prepareParticipants(db, raw) {
|
|
4130
4674
|
return ensureOwnerParticipates(db, raw.map(normalizeParticipant));
|
|
4131
4675
|
}
|
|
4132
|
-
function manageCommitmentToolHandler(db,
|
|
4676
|
+
function manageCommitmentToolHandler(db, rawInput) {
|
|
4677
|
+
const input = structuredClone(rawInput);
|
|
4678
|
+
const inputError = validateToolInput(manageCommitmentTool.inputSchema, input);
|
|
4679
|
+
if (inputError)
|
|
4680
|
+
return toolFailure(
|
|
4681
|
+
"protocol",
|
|
4682
|
+
"invalid_input",
|
|
4683
|
+
"Invalid input.",
|
|
4684
|
+
inputError
|
|
4685
|
+
);
|
|
4133
4686
|
return withToolHandling(() => {
|
|
4134
4687
|
if (input.action === "record") {
|
|
4688
|
+
if (!input.input)
|
|
4689
|
+
return toolFailure(
|
|
4690
|
+
"protocol",
|
|
4691
|
+
"invalid_input",
|
|
4692
|
+
"Missing input.",
|
|
4693
|
+
"input is required for record."
|
|
4694
|
+
);
|
|
4135
4695
|
const patched = {
|
|
4136
4696
|
...input.input,
|
|
4137
4697
|
participants: prepareParticipants(
|
|
@@ -4167,6 +4727,8 @@ var init_manage_commitment_tool = __esm({
|
|
|
4167
4727
|
init_tool_mutation();
|
|
4168
4728
|
init_tool_names();
|
|
4169
4729
|
init_tool_resolvers();
|
|
4730
|
+
init_tool_types();
|
|
4731
|
+
init_validate_tool_input();
|
|
4170
4732
|
manageCommitmentTool = defineAffinityTool({
|
|
4171
4733
|
name: manageCommitmentToolName,
|
|
4172
4734
|
description: "Record or resolve one commitment such as a promise or agreement. The owner contact is auto-injected into participants if missing.",
|
|
@@ -4208,7 +4770,7 @@ var init_manage_commitment_tool = __esm({
|
|
|
4208
4770
|
role: stringSchema("Participant role."),
|
|
4209
4771
|
directionality: stringSchema("Optional directionality.")
|
|
4210
4772
|
},
|
|
4211
|
-
["
|
|
4773
|
+
["role"]
|
|
4212
4774
|
),
|
|
4213
4775
|
"Participant list. Must be non-empty and include the owner contact."
|
|
4214
4776
|
),
|
|
@@ -4257,6 +4819,163 @@ var init_manage_commitment_tool = __esm({
|
|
|
4257
4819
|
}
|
|
4258
4820
|
});
|
|
4259
4821
|
|
|
4822
|
+
// src/contacts/delete_contact.ts
|
|
4823
|
+
function deleteContact(db, locatorId, options) {
|
|
4824
|
+
return withTransaction(db, () => {
|
|
4825
|
+
const contact = db.prepare("SELECT is_owner FROM contacts WHERE id = ?").get(locatorId);
|
|
4826
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4827
|
+
if (contact.is_owner)
|
|
4828
|
+
throw new AffinityStateError("The owner contact cannot be deleted");
|
|
4829
|
+
const tables = [
|
|
4830
|
+
"contacts",
|
|
4831
|
+
"identities",
|
|
4832
|
+
"attributes",
|
|
4833
|
+
"links",
|
|
4834
|
+
"events",
|
|
4835
|
+
"event_participants",
|
|
4836
|
+
"open_commitments",
|
|
4837
|
+
"contact_merges",
|
|
4838
|
+
"dismissed_duplicates",
|
|
4839
|
+
"link_event_effects",
|
|
4840
|
+
"upcoming_occurrences",
|
|
4841
|
+
"link_rollups",
|
|
4842
|
+
"contact_rollups"
|
|
4843
|
+
];
|
|
4844
|
+
const before = new Map(
|
|
4845
|
+
tables.map((table) => [
|
|
4846
|
+
table,
|
|
4847
|
+
db.prepare(`SELECT rowid AS row_id, * FROM ${table}`).all()
|
|
4848
|
+
])
|
|
4849
|
+
);
|
|
4850
|
+
db.prepare(
|
|
4851
|
+
"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))"
|
|
4852
|
+
).run(locatorId, locatorId);
|
|
4853
|
+
db.prepare(
|
|
4854
|
+
"DELETE FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ?"
|
|
4855
|
+
).run(locatorId, locatorId);
|
|
4856
|
+
db.prepare(
|
|
4857
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
4858
|
+
).run(locatorId, locatorId);
|
|
4859
|
+
db.prepare(
|
|
4860
|
+
"UPDATE contacts SET merged_into_contact_id = NULL WHERE merged_into_contact_id = ?"
|
|
4861
|
+
).run(locatorId);
|
|
4862
|
+
db.prepare("DELETE FROM contacts WHERE id = ?").run(locatorId);
|
|
4863
|
+
const now = Date.now();
|
|
4864
|
+
const affectedLinks = db.prepare(
|
|
4865
|
+
"SELECT id FROM links WHERE removed_at IS NULL AND is_structural = 0"
|
|
4866
|
+
).all().map((row) => row.id);
|
|
4867
|
+
refreshAllBridgeScores(db, now);
|
|
4868
|
+
for (const id of affectedLinks) refreshLinkRollup(db, id, now);
|
|
4869
|
+
for (const row of db.prepare("SELECT id FROM contacts").all())
|
|
4870
|
+
refreshContactRollup(db, Number(row.id), now);
|
|
4871
|
+
const removed = tables.map((table) => {
|
|
4872
|
+
const remaining = new Set(
|
|
4873
|
+
db.prepare(`SELECT rowid AS row_id FROM ${table}`).all().map((row) => row.row_id)
|
|
4874
|
+
);
|
|
4875
|
+
const ids = (before.get(table) ?? []).filter((row) => !remaining.has(row.row_id)).map((row) => Number(row.id ?? row.event_id ?? row.row_id));
|
|
4876
|
+
return { entity: table, count: ids.length, ids };
|
|
4877
|
+
}).filter((item) => item.count > 0);
|
|
4878
|
+
return { contactId: locatorId, removed, affectedLinks };
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4881
|
+
var init_delete_contact = __esm({
|
|
4882
|
+
"src/contacts/delete_contact.ts"() {
|
|
4883
|
+
init_bridge_scores();
|
|
4884
|
+
init_affinity_not_found_error();
|
|
4885
|
+
init_affinity_state_error();
|
|
4886
|
+
init_affinity_validation_error();
|
|
4887
|
+
init_rollups2();
|
|
4888
|
+
init_with_transaction();
|
|
4889
|
+
init_rollups();
|
|
4890
|
+
}
|
|
4891
|
+
});
|
|
4892
|
+
|
|
4893
|
+
// src/contacts/export_contact.ts
|
|
4894
|
+
function exportContact(db, id) {
|
|
4895
|
+
const query = (sql, ...params) => db.prepare(sql).all(...params).map((row) => {
|
|
4896
|
+
const result = {};
|
|
4897
|
+
for (const [key, value] of Object.entries(row)) {
|
|
4898
|
+
const name = key.replace(
|
|
4899
|
+
/_([a-z])/g,
|
|
4900
|
+
(_, letter) => letter.toUpperCase()
|
|
4901
|
+
);
|
|
4902
|
+
result[name] = value;
|
|
4903
|
+
if ((name.endsWith("At") || name === "dueAt") && typeof value === "number")
|
|
4904
|
+
result[`${name}Iso`] = new Date(value).toISOString();
|
|
4905
|
+
if (key.endsWith("_json") && typeof value === "string")
|
|
4906
|
+
result[name] = JSON.parse(value);
|
|
4907
|
+
}
|
|
4908
|
+
return result;
|
|
4909
|
+
});
|
|
4910
|
+
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
4911
|
+
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
4912
|
+
const events = query(
|
|
4913
|
+
"SELECT DISTINCT e.* FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? ORDER BY e.id",
|
|
4914
|
+
id
|
|
4915
|
+
).map((event) => ({
|
|
4916
|
+
...event,
|
|
4917
|
+
participants: query(
|
|
4918
|
+
"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",
|
|
4919
|
+
event.id
|
|
4920
|
+
)
|
|
4921
|
+
}));
|
|
4922
|
+
return {
|
|
4923
|
+
contact,
|
|
4924
|
+
identities: query(
|
|
4925
|
+
"SELECT * FROM identities WHERE contact_id = ? ORDER BY id",
|
|
4926
|
+
id
|
|
4927
|
+
),
|
|
4928
|
+
attributes: query(
|
|
4929
|
+
"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",
|
|
4930
|
+
id,
|
|
4931
|
+
id,
|
|
4932
|
+
id
|
|
4933
|
+
),
|
|
4934
|
+
links: query(
|
|
4935
|
+
"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",
|
|
4936
|
+
id,
|
|
4937
|
+
id
|
|
4938
|
+
),
|
|
4939
|
+
events,
|
|
4940
|
+
commitments: query(
|
|
4941
|
+
"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",
|
|
4942
|
+
id
|
|
4943
|
+
),
|
|
4944
|
+
dates: query(
|
|
4945
|
+
"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",
|
|
4946
|
+
id,
|
|
4947
|
+
id,
|
|
4948
|
+
id
|
|
4949
|
+
),
|
|
4950
|
+
mergeHistory: query(
|
|
4951
|
+
"SELECT * FROM contact_merges WHERE winner_contact_id = ? OR loser_contact_id = ? ORDER BY id",
|
|
4952
|
+
id,
|
|
4953
|
+
id
|
|
4954
|
+
)
|
|
4955
|
+
};
|
|
4956
|
+
}
|
|
4957
|
+
var init_export_contact = __esm({
|
|
4958
|
+
"src/contacts/export_contact.ts"() {
|
|
4959
|
+
init_affinity_not_found_error();
|
|
4960
|
+
}
|
|
4961
|
+
});
|
|
4962
|
+
|
|
4963
|
+
// src/contacts/export_affinity.ts
|
|
4964
|
+
function exportAffinity(db) {
|
|
4965
|
+
const now = Date.now();
|
|
4966
|
+
return {
|
|
4967
|
+
version: 1,
|
|
4968
|
+
exportedAt: now,
|
|
4969
|
+
exportedAtIso: new Date(now).toISOString(),
|
|
4970
|
+
contacts: db.prepare("SELECT id FROM contacts ORDER BY id").all().map((row) => exportContact(db, row.id))
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
var init_export_affinity = __esm({
|
|
4974
|
+
"src/contacts/export_affinity.ts"() {
|
|
4975
|
+
init_export_contact();
|
|
4976
|
+
}
|
|
4977
|
+
});
|
|
4978
|
+
|
|
4260
4979
|
// src/contacts/receipts.ts
|
|
4261
4980
|
function buildContactMutationReceipt(primary, partial) {
|
|
4262
4981
|
const normalized = normalizeMutationReceiptPartial(partial);
|
|
@@ -4288,73 +5007,6 @@ var init_receipts4 = __esm({
|
|
|
4288
5007
|
}
|
|
4289
5008
|
});
|
|
4290
5009
|
|
|
4291
|
-
// src/contacts/create_contact.ts
|
|
4292
|
-
function createContact(db, input) {
|
|
4293
|
-
return withTransaction(db, () => {
|
|
4294
|
-
const name = input.name.trim();
|
|
4295
|
-
if (name.length === 0) {
|
|
4296
|
-
throw new AffinityValidationError("name must be non-empty");
|
|
4297
|
-
}
|
|
4298
|
-
if (!KINDS.has(input.kind)) {
|
|
4299
|
-
throw new AffinityValidationError("invalid contact kind");
|
|
4300
|
-
}
|
|
4301
|
-
if (input.isOwner !== void 0) {
|
|
4302
|
-
throw new AffinityValidationError(
|
|
4303
|
-
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
4304
|
-
);
|
|
4305
|
-
}
|
|
4306
|
-
const now = resolveNow(input.now);
|
|
4307
|
-
if (input.bootstrapOwner) {
|
|
4308
|
-
const existing = db.prepare(
|
|
4309
|
-
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
4310
|
-
).get();
|
|
4311
|
-
if (existing !== void 0) {
|
|
4312
|
-
throw new AffinityConflictError(
|
|
4313
|
-
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
4314
|
-
);
|
|
4315
|
-
}
|
|
4316
|
-
}
|
|
4317
|
-
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
4318
|
-
const result = db.prepare(
|
|
4319
|
-
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
4320
|
-
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
4321
|
-
).run(name, input.kind, isOwner, now, now);
|
|
4322
|
-
const id = Number(result.lastInsertRowid);
|
|
4323
|
-
const row = getContactRowById(db, id);
|
|
4324
|
-
if (!row) {
|
|
4325
|
-
throw new AffinityInvariantError("inserted contact row not found");
|
|
4326
|
-
}
|
|
4327
|
-
refreshContactRollup(db, id, now);
|
|
4328
|
-
const primary = mapContactRowToContactListItem(row);
|
|
4329
|
-
return buildContactMutationReceipt(primary, {
|
|
4330
|
-
created: [{ kind: "contact", id }]
|
|
4331
|
-
});
|
|
4332
|
-
});
|
|
4333
|
-
}
|
|
4334
|
-
var KINDS;
|
|
4335
|
-
var init_create_contact = __esm({
|
|
4336
|
-
"src/contacts/create_contact.ts"() {
|
|
4337
|
-
init_mappers4();
|
|
4338
|
-
init_queries();
|
|
4339
|
-
init_receipts4();
|
|
4340
|
-
init_rollups();
|
|
4341
|
-
init_affinity_conflict_error();
|
|
4342
|
-
init_affinity_invariant_error();
|
|
4343
|
-
init_affinity_validation_error();
|
|
4344
|
-
init_resolve_now();
|
|
4345
|
-
init_with_transaction();
|
|
4346
|
-
KINDS = /* @__PURE__ */ new Set([
|
|
4347
|
-
"human",
|
|
4348
|
-
"group",
|
|
4349
|
-
"company",
|
|
4350
|
-
"team",
|
|
4351
|
-
"pet",
|
|
4352
|
-
"service",
|
|
4353
|
-
"other"
|
|
4354
|
-
]);
|
|
4355
|
-
}
|
|
4356
|
-
});
|
|
4357
|
-
|
|
4358
5010
|
// src/contacts/revise_contact.ts
|
|
4359
5011
|
function reviseContact(db, contactId, patch) {
|
|
4360
5012
|
return withTransaction(db, () => {
|
|
@@ -4410,7 +5062,9 @@ function assertValidLifecycleTransition(from, to) {
|
|
|
4410
5062
|
throw new AffinityStateError("merged contact is read-only");
|
|
4411
5063
|
}
|
|
4412
5064
|
if (to === "merged") {
|
|
4413
|
-
throw new AffinityInvariantError(
|
|
5065
|
+
throw new AffinityInvariantError(
|
|
5066
|
+
"use manage_contact merge (CLI: contact merge) to reach merged state"
|
|
5067
|
+
);
|
|
4414
5068
|
}
|
|
4415
5069
|
if (from === to) {
|
|
4416
5070
|
return;
|
|
@@ -4441,44 +5095,362 @@ function setContactLifecycle(db, contactId, lifecycleState, options) {
|
|
|
4441
5095
|
if (!STATES2.has(lifecycleState)) {
|
|
4442
5096
|
throw new AffinityValidationError("invalid lifecycle state");
|
|
4443
5097
|
}
|
|
4444
|
-
const row = getContactRowById(db, contactId);
|
|
5098
|
+
const row = getContactRowById(db, contactId);
|
|
5099
|
+
if (!row) {
|
|
5100
|
+
throw new AffinityNotFoundError("contact not found");
|
|
5101
|
+
}
|
|
5102
|
+
const from = row.lifecycle_state;
|
|
5103
|
+
assertValidLifecycleTransition(from, lifecycleState);
|
|
5104
|
+
const now = resolveNow(options?.now);
|
|
5105
|
+
if (from === lifecycleState) {
|
|
5106
|
+
return buildContactMutationReceipt(mapContactRowToContactListItem(row), {
|
|
5107
|
+
updated: []
|
|
5108
|
+
});
|
|
5109
|
+
}
|
|
5110
|
+
db.prepare(
|
|
5111
|
+
"UPDATE contacts SET lifecycle_state = ?, updated_at = ? WHERE id = ?"
|
|
5112
|
+
).run(lifecycleState, now, contactId);
|
|
5113
|
+
refreshContactRollup(db, contactId, now);
|
|
5114
|
+
const next = getContactRowById(db, contactId);
|
|
5115
|
+
if (!next) {
|
|
5116
|
+
throw new AffinityNotFoundError("contact not found");
|
|
5117
|
+
}
|
|
5118
|
+
return buildContactMutationReceipt(mapContactRowToContactListItem(next), {
|
|
5119
|
+
updated: [{ kind: "contact", id: contactId }]
|
|
5120
|
+
});
|
|
5121
|
+
});
|
|
5122
|
+
}
|
|
5123
|
+
var STATES2;
|
|
5124
|
+
var init_set_contact_lifecycle = __esm({
|
|
5125
|
+
"src/contacts/set_contact_lifecycle.ts"() {
|
|
5126
|
+
init_mappers4();
|
|
5127
|
+
init_queries();
|
|
5128
|
+
init_receipts4();
|
|
5129
|
+
init_rollups();
|
|
5130
|
+
init_validators4();
|
|
5131
|
+
init_affinity_not_found_error();
|
|
5132
|
+
init_affinity_validation_error();
|
|
5133
|
+
init_resolve_now();
|
|
5134
|
+
init_with_transaction();
|
|
5135
|
+
STATES2 = /* @__PURE__ */ new Set(["active", "dormant", "merged", "lost"]);
|
|
5136
|
+
}
|
|
5137
|
+
});
|
|
5138
|
+
|
|
5139
|
+
// src/contacts/create_contact.ts
|
|
5140
|
+
function createContact(db, input) {
|
|
5141
|
+
return withTransaction(db, () => {
|
|
5142
|
+
const name = input.name.trim();
|
|
5143
|
+
if (name.length === 0) {
|
|
5144
|
+
throw new AffinityValidationError("name must be non-empty");
|
|
5145
|
+
}
|
|
5146
|
+
if (!KINDS.has(input.kind)) {
|
|
5147
|
+
throw new AffinityValidationError("invalid contact kind");
|
|
5148
|
+
}
|
|
5149
|
+
if (input.isOwner !== void 0) {
|
|
5150
|
+
throw new AffinityValidationError(
|
|
5151
|
+
"isOwner is read-only; pass bootstrapOwner: true to create the owner contact"
|
|
5152
|
+
);
|
|
5153
|
+
}
|
|
5154
|
+
const now = resolveNow(input.now);
|
|
5155
|
+
if (input.bootstrapOwner) {
|
|
5156
|
+
const existing = db.prepare(
|
|
5157
|
+
"SELECT id, name FROM contacts WHERE is_owner = 1 AND deleted_at IS NULL"
|
|
5158
|
+
).get();
|
|
5159
|
+
if (existing !== void 0) {
|
|
5160
|
+
throw new AffinityConflictError(
|
|
5161
|
+
`owner already exists: contact ${existing.id} (${existing.name}); bootstrapOwner can only be used once`
|
|
5162
|
+
);
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
const isOwner = input.bootstrapOwner ? 1 : 0;
|
|
5166
|
+
const result = db.prepare(
|
|
5167
|
+
`INSERT INTO contacts (name, kind, lifecycle_state, is_owner, created_at, updated_at)
|
|
5168
|
+
VALUES (?, ?, 'active', ?, ?, ?)`
|
|
5169
|
+
).run(name, input.kind, isOwner, now, now);
|
|
5170
|
+
const id = Number(result.lastInsertRowid);
|
|
5171
|
+
const row = getContactRowById(db, id);
|
|
5172
|
+
if (!row) {
|
|
5173
|
+
throw new AffinityInvariantError("inserted contact row not found");
|
|
5174
|
+
}
|
|
5175
|
+
refreshContactRollup(db, id, now);
|
|
5176
|
+
const primary = mapContactRowToContactListItem(row);
|
|
5177
|
+
return buildContactMutationReceipt(primary, {
|
|
5178
|
+
created: [{ kind: "contact", id }]
|
|
5179
|
+
});
|
|
5180
|
+
});
|
|
5181
|
+
}
|
|
5182
|
+
var KINDS;
|
|
5183
|
+
var init_create_contact = __esm({
|
|
5184
|
+
"src/contacts/create_contact.ts"() {
|
|
5185
|
+
init_mappers4();
|
|
5186
|
+
init_queries();
|
|
5187
|
+
init_receipts4();
|
|
5188
|
+
init_rollups();
|
|
5189
|
+
init_affinity_conflict_error();
|
|
5190
|
+
init_affinity_invariant_error();
|
|
5191
|
+
init_affinity_validation_error();
|
|
5192
|
+
init_resolve_now();
|
|
5193
|
+
init_with_transaction();
|
|
5194
|
+
KINDS = /* @__PURE__ */ new Set([
|
|
5195
|
+
"human",
|
|
5196
|
+
"group",
|
|
5197
|
+
"company",
|
|
5198
|
+
"team",
|
|
5199
|
+
"pet",
|
|
5200
|
+
"service",
|
|
5201
|
+
"other"
|
|
5202
|
+
]);
|
|
5203
|
+
}
|
|
5204
|
+
});
|
|
5205
|
+
|
|
5206
|
+
// src/contacts/fold_search_text.ts
|
|
5207
|
+
function foldSearchText(value) {
|
|
5208
|
+
let folded = value.toLowerCase();
|
|
5209
|
+
if (/[^\u0020-\u007e]/.test(folded))
|
|
5210
|
+
folded = folded.replace(/ß/g, "ss").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
5211
|
+
return folded.replace(/ae/g, "a").replace(/oe/g, "o").replace(/ue/g, "u");
|
|
5212
|
+
}
|
|
5213
|
+
var init_fold_search_text = __esm({
|
|
5214
|
+
"src/contacts/fold_search_text.ts"() {
|
|
5215
|
+
}
|
|
5216
|
+
});
|
|
5217
|
+
|
|
5218
|
+
// src/identities/queries.ts
|
|
5219
|
+
function getIdentityRowById(db, id) {
|
|
5220
|
+
const row = db.prepare(
|
|
5221
|
+
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5222
|
+
created_at, updated_at, removed_at
|
|
5223
|
+
FROM identities
|
|
5224
|
+
WHERE id = ? AND removed_at IS NULL`
|
|
5225
|
+
).get(id);
|
|
5226
|
+
if (row === void 0) {
|
|
5227
|
+
return null;
|
|
5228
|
+
}
|
|
5229
|
+
return row;
|
|
5230
|
+
}
|
|
5231
|
+
var init_queries5 = __esm({
|
|
5232
|
+
"src/identities/queries.ts"() {
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
|
|
5236
|
+
// src/identities/receipts.ts
|
|
5237
|
+
function buildIdentityMutationReceipt(primary, partial) {
|
|
5238
|
+
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5239
|
+
return {
|
|
5240
|
+
primary,
|
|
5241
|
+
created: normalized.created,
|
|
5242
|
+
updated: normalized.updated,
|
|
5243
|
+
archived: normalized.archived,
|
|
5244
|
+
removed: normalized.removed,
|
|
5245
|
+
affectedLinks: normalized.affectedLinks,
|
|
5246
|
+
derivedEffects: normalized.derivedEffects
|
|
5247
|
+
};
|
|
5248
|
+
}
|
|
5249
|
+
var init_receipts5 = __esm({
|
|
5250
|
+
"src/identities/receipts.ts"() {
|
|
5251
|
+
init_normalize_mutation_receipt();
|
|
5252
|
+
}
|
|
5253
|
+
});
|
|
5254
|
+
|
|
5255
|
+
// src/identities/validators.ts
|
|
5256
|
+
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5257
|
+
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5258
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5259
|
+
).get(normalizedKey) : db.prepare(
|
|
5260
|
+
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
5261
|
+
).get(normalizedKey, excludeIdentityId);
|
|
5262
|
+
if (row !== void 0) {
|
|
5263
|
+
const owner = db.prepare(
|
|
5264
|
+
"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"
|
|
5265
|
+
).get(normalizedKey);
|
|
5266
|
+
throw new AffinityConflictError(
|
|
5267
|
+
`${normalizedKey.replace(":", " ")} already belongs to #${owner?.id} ${owner?.name}`
|
|
5268
|
+
);
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
var init_validators5 = __esm({
|
|
5272
|
+
"src/identities/validators.ts"() {
|
|
5273
|
+
init_affinity_conflict_error();
|
|
5274
|
+
}
|
|
5275
|
+
});
|
|
5276
|
+
|
|
5277
|
+
// src/identities/validate_identity_input.ts
|
|
5278
|
+
function validateIdentityInput(type, value) {
|
|
5279
|
+
const kind = type.toLowerCase();
|
|
5280
|
+
if (!identityTypes.includes(kind) && !/^custom:[a-z0-9_-]+$/.test(type))
|
|
5281
|
+
throw new AffinityValidationError(
|
|
5282
|
+
`identity type must be one of ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen)`
|
|
5283
|
+
);
|
|
5284
|
+
if (!value.trim())
|
|
5285
|
+
throw new AffinityValidationError("identity value must be non-empty");
|
|
5286
|
+
if (kind === "email" && !/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value))
|
|
5287
|
+
throw new AffinityValidationError(
|
|
5288
|
+
"email value must match local@domain.tld"
|
|
5289
|
+
);
|
|
5290
|
+
if (kind === "phone" && value.replace(/\D/g, "").length < 5)
|
|
5291
|
+
throw new AffinityValidationError(
|
|
5292
|
+
"phone value must contain at least 5 digits"
|
|
5293
|
+
);
|
|
5294
|
+
if (kind === "url" || kind === "website") {
|
|
5295
|
+
try {
|
|
5296
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
5297
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || /\s/.test(value))
|
|
5298
|
+
throw new Error();
|
|
5299
|
+
} catch {
|
|
5300
|
+
throw new AffinityValidationError(
|
|
5301
|
+
"url/website value must be an http/https URL or bare domain"
|
|
5302
|
+
);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
var identityTypes;
|
|
5307
|
+
var init_validate_identity_input = __esm({
|
|
5308
|
+
"src/identities/validate_identity_input.ts"() {
|
|
5309
|
+
init_affinity_validation_error();
|
|
5310
|
+
identityTypes = [
|
|
5311
|
+
"email",
|
|
5312
|
+
"phone",
|
|
5313
|
+
"url",
|
|
5314
|
+
"website",
|
|
5315
|
+
"handle",
|
|
5316
|
+
"x",
|
|
5317
|
+
"twitter",
|
|
5318
|
+
"linkedin",
|
|
5319
|
+
"github",
|
|
5320
|
+
"mastodon",
|
|
5321
|
+
"bluesky",
|
|
5322
|
+
"telegram",
|
|
5323
|
+
"whatsapp",
|
|
5324
|
+
"signal",
|
|
5325
|
+
"matrix",
|
|
5326
|
+
"discord",
|
|
5327
|
+
"slack",
|
|
5328
|
+
"instagram",
|
|
5329
|
+
"facebook",
|
|
5330
|
+
"youtube",
|
|
5331
|
+
"tiktok",
|
|
5332
|
+
"address",
|
|
5333
|
+
"other",
|
|
5334
|
+
"account_id",
|
|
5335
|
+
"alias"
|
|
5336
|
+
];
|
|
5337
|
+
}
|
|
5338
|
+
});
|
|
5339
|
+
|
|
5340
|
+
// src/identities/add_identity.ts
|
|
5341
|
+
function addIdentity(db, contactId, input) {
|
|
5342
|
+
return withTransaction(db, () => {
|
|
5343
|
+
const contact = getContactRowById(db, contactId);
|
|
5344
|
+
if (!contact) {
|
|
5345
|
+
throw new AffinityNotFoundError("contact not found");
|
|
5346
|
+
}
|
|
5347
|
+
if (contact.lifecycle_state === "merged") {
|
|
5348
|
+
throw new AffinityStateError("merged contact is read-only");
|
|
5349
|
+
}
|
|
5350
|
+
const type = input.type.trim();
|
|
5351
|
+
const value = input.value.trim();
|
|
5352
|
+
if (type.length === 0 || value.length === 0) {
|
|
5353
|
+
throw new AffinityValidationError("type and value must be non-empty");
|
|
5354
|
+
}
|
|
5355
|
+
validateIdentityInput(type, value);
|
|
5356
|
+
const nk = normalizeIdentityKey(type, value);
|
|
5357
|
+
assertNoIdentityCollision(db, nk);
|
|
5358
|
+
const now = resolveNow(input.now);
|
|
5359
|
+
const verified = input.verified ? 1 : 0;
|
|
5360
|
+
const verifiedAt = verified === 1 ? now : null;
|
|
5361
|
+
const result = db.prepare(
|
|
5362
|
+
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
5363
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5364
|
+
).run(
|
|
5365
|
+
contactId,
|
|
5366
|
+
type,
|
|
5367
|
+
value,
|
|
5368
|
+
input.label ?? null,
|
|
5369
|
+
nk,
|
|
5370
|
+
verified,
|
|
5371
|
+
verifiedAt,
|
|
5372
|
+
now,
|
|
5373
|
+
now
|
|
5374
|
+
);
|
|
5375
|
+
const id = Number(result.lastInsertRowid);
|
|
5376
|
+
const row = getIdentityRowById(db, id);
|
|
4445
5377
|
if (!row) {
|
|
4446
|
-
throw new
|
|
4447
|
-
}
|
|
4448
|
-
const from = row.lifecycle_state;
|
|
4449
|
-
assertValidLifecycleTransition(from, lifecycleState);
|
|
4450
|
-
const now = resolveNow(options?.now);
|
|
4451
|
-
if (from === lifecycleState) {
|
|
4452
|
-
return buildContactMutationReceipt(mapContactRowToContactListItem(row), {
|
|
4453
|
-
updated: []
|
|
4454
|
-
});
|
|
5378
|
+
throw new AffinityInvariantError("inserted identity not found");
|
|
4455
5379
|
}
|
|
4456
|
-
db.prepare(
|
|
4457
|
-
"UPDATE contacts SET lifecycle_state = ?, updated_at = ? WHERE id = ?"
|
|
4458
|
-
).run(lifecycleState, now, contactId);
|
|
4459
5380
|
refreshContactRollup(db, contactId, now);
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
updated: [{ kind: "contact", id: contactId }]
|
|
5381
|
+
db.prepare(
|
|
5382
|
+
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
5383
|
+
).run(contactId, contactId);
|
|
5384
|
+
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
5385
|
+
created: [{ kind: "identity", id }]
|
|
4466
5386
|
});
|
|
4467
5387
|
});
|
|
4468
5388
|
}
|
|
4469
|
-
var
|
|
4470
|
-
|
|
4471
|
-
"src/contacts/set_contact_lifecycle.ts"() {
|
|
4472
|
-
init_mappers4();
|
|
5389
|
+
var init_add_identity = __esm({
|
|
5390
|
+
"src/identities/add_identity.ts"() {
|
|
4473
5391
|
init_queries();
|
|
4474
|
-
init_receipts4();
|
|
4475
5392
|
init_rollups();
|
|
4476
|
-
|
|
5393
|
+
init_mappers2();
|
|
5394
|
+
init_normalize();
|
|
5395
|
+
init_queries5();
|
|
5396
|
+
init_receipts5();
|
|
5397
|
+
init_validators5();
|
|
5398
|
+
init_affinity_invariant_error();
|
|
4477
5399
|
init_affinity_not_found_error();
|
|
5400
|
+
init_affinity_state_error();
|
|
4478
5401
|
init_affinity_validation_error();
|
|
4479
5402
|
init_resolve_now();
|
|
4480
5403
|
init_with_transaction();
|
|
4481
|
-
|
|
5404
|
+
init_validate_identity_input();
|
|
5405
|
+
}
|
|
5406
|
+
});
|
|
5407
|
+
|
|
5408
|
+
// src/tools/create_contact_tool_result.ts
|
|
5409
|
+
function createContactToolResult(db, input) {
|
|
5410
|
+
return withTransaction(db, () => {
|
|
5411
|
+
const receipt = createContact(db, input);
|
|
5412
|
+
const identityOwners = /* @__PURE__ */ new Set();
|
|
5413
|
+
for (const identity of input.identities ?? []) {
|
|
5414
|
+
validateIdentityInput(identity.type.trim(), identity.value.trim());
|
|
5415
|
+
const existing = db.prepare(
|
|
5416
|
+
"SELECT contact_id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5417
|
+
).get(normalizeIdentityKey(identity.type, identity.value));
|
|
5418
|
+
if (existing) {
|
|
5419
|
+
if (Number(existing.contact_id) !== receipt.primary.id)
|
|
5420
|
+
identityOwners.add(Number(existing.contact_id));
|
|
5421
|
+
continue;
|
|
5422
|
+
}
|
|
5423
|
+
const added = addIdentity(db, receipt.primary.id, identity);
|
|
5424
|
+
receipt.created.push(...added.created);
|
|
5425
|
+
}
|
|
5426
|
+
const result = mutationToolResult("create", receipt, "contact");
|
|
5427
|
+
const fold = (name) => foldSearchText(name).trim().replace(/\s+/g, " ");
|
|
5428
|
+
const warnings = db.prepare(
|
|
5429
|
+
"SELECT id, name FROM contacts WHERE id != ? AND deleted_at IS NULL AND lifecycle_state IN ('active', 'dormant')"
|
|
5430
|
+
).all(receipt.primary.id).flatMap((row) => {
|
|
5431
|
+
const score = identityOwners.has(row.id) ? 1 : fold(row.name) === fold(input.name) ? 0.92 : void 0;
|
|
5432
|
+
return score === void 0 ? [] : [
|
|
5433
|
+
{
|
|
5434
|
+
code: "possible_duplicate",
|
|
5435
|
+
message: `Possible duplicate of #${row.id} ${row.name} (score ${score})`,
|
|
5436
|
+
contactId: row.id,
|
|
5437
|
+
score
|
|
5438
|
+
}
|
|
5439
|
+
];
|
|
5440
|
+
});
|
|
5441
|
+
if (warnings.length) result.warnings = warnings;
|
|
5442
|
+
return result;
|
|
5443
|
+
});
|
|
5444
|
+
}
|
|
5445
|
+
var init_create_contact_tool_result = __esm({
|
|
5446
|
+
"src/tools/create_contact_tool_result.ts"() {
|
|
5447
|
+
init_create_contact();
|
|
5448
|
+
init_fold_search_text();
|
|
5449
|
+
init_add_identity();
|
|
5450
|
+
init_normalize();
|
|
5451
|
+
init_validate_identity_input();
|
|
5452
|
+
init_with_transaction();
|
|
5453
|
+
init_tool_mutation();
|
|
4482
5454
|
}
|
|
4483
5455
|
});
|
|
4484
5456
|
|
|
@@ -4500,24 +5472,76 @@ function contactLocatorSchema3(description) {
|
|
|
4500
5472
|
description
|
|
4501
5473
|
};
|
|
4502
5474
|
}
|
|
4503
|
-
function manageContactToolHandler(db,
|
|
5475
|
+
function manageContactToolHandler(db, rawInput) {
|
|
5476
|
+
const input = structuredClone(rawInput);
|
|
5477
|
+
const inputError = validateToolInput(manageContactTool.inputSchema, input);
|
|
5478
|
+
if (inputError)
|
|
5479
|
+
return toolFailure(
|
|
5480
|
+
"protocol",
|
|
5481
|
+
"invalid_input",
|
|
5482
|
+
"Invalid input.",
|
|
5483
|
+
inputError
|
|
5484
|
+
);
|
|
4504
5485
|
return withToolHandling(() => {
|
|
4505
|
-
if (input.action === "
|
|
4506
|
-
|
|
4507
|
-
"
|
|
4508
|
-
|
|
4509
|
-
|
|
5486
|
+
if (input.action === "export") {
|
|
5487
|
+
if (!input.contact)
|
|
5488
|
+
return toolSuccess("Exported Affinity.", {
|
|
5489
|
+
action: "export",
|
|
5490
|
+
export: exportAffinity(db)
|
|
5491
|
+
});
|
|
5492
|
+
const target = resolveContactLocator(db, input.contact, "contact", true);
|
|
5493
|
+
if (!target.ok) return target.result;
|
|
5494
|
+
return toolSuccess("Exported contact.", {
|
|
5495
|
+
action: "export",
|
|
5496
|
+
export: exportContact(db, target.value.id)
|
|
5497
|
+
});
|
|
5498
|
+
}
|
|
5499
|
+
if (input.action === "delete" && input.confirm !== true)
|
|
5500
|
+
return toolFailure(
|
|
5501
|
+
"protocol",
|
|
5502
|
+
"invalid_input",
|
|
5503
|
+
"Delete requires confirmation.",
|
|
5504
|
+
"delete is permanent; pass confirm: true (CLI: --confirm)"
|
|
4510
5505
|
);
|
|
5506
|
+
if (input.action === "create") {
|
|
5507
|
+
if (!input.input)
|
|
5508
|
+
return toolFailure(
|
|
5509
|
+
"protocol",
|
|
5510
|
+
"invalid_input",
|
|
5511
|
+
"Missing input.",
|
|
5512
|
+
"input is required for create."
|
|
5513
|
+
);
|
|
5514
|
+
return createContactToolResult(db, input.input);
|
|
4511
5515
|
}
|
|
4512
|
-
const contact = resolveContactLocator(
|
|
5516
|
+
const contact = resolveContactLocator(
|
|
5517
|
+
db,
|
|
5518
|
+
input.contact,
|
|
5519
|
+
"contact",
|
|
5520
|
+
input.action === "delete"
|
|
5521
|
+
);
|
|
4513
5522
|
if (!contact.ok) {
|
|
4514
5523
|
return contact.result;
|
|
4515
5524
|
}
|
|
5525
|
+
if (input.action === "delete") {
|
|
5526
|
+
const deletion = deleteContact(db, contact.value.id, { confirm: true });
|
|
5527
|
+
return toolSuccess("Permanently deleted contact.", {
|
|
5528
|
+
action: "delete",
|
|
5529
|
+
deletion,
|
|
5530
|
+
changes: deletion.removed.flatMap(
|
|
5531
|
+
(item) => item.ids.map((id) => ({
|
|
5532
|
+
entity: item.entity,
|
|
5533
|
+
id,
|
|
5534
|
+
action: "removed"
|
|
5535
|
+
}))
|
|
5536
|
+
)
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
4516
5539
|
if (input.action === "revise") {
|
|
4517
5540
|
return mutationToolResult(
|
|
4518
5541
|
"revise",
|
|
4519
5542
|
reviseContact(db, contact.value.id, input.patch),
|
|
4520
|
-
"contact"
|
|
5543
|
+
"contact",
|
|
5544
|
+
Object.keys(input.patch)
|
|
4521
5545
|
);
|
|
4522
5546
|
}
|
|
4523
5547
|
return mutationToolResult(
|
|
@@ -4535,16 +5559,22 @@ function manageContactToolHandler(db, input) {
|
|
|
4535
5559
|
var manageContactTool;
|
|
4536
5560
|
var init_manage_contact_tool = __esm({
|
|
4537
5561
|
"src/tools/manage_contact_tool.ts"() {
|
|
4538
|
-
|
|
5562
|
+
init_delete_contact();
|
|
5563
|
+
init_export_affinity();
|
|
5564
|
+
init_export_contact();
|
|
4539
5565
|
init_revise_contact();
|
|
4540
5566
|
init_set_contact_lifecycle();
|
|
5567
|
+
init_create_contact_tool_result();
|
|
4541
5568
|
init_tool_metadata();
|
|
4542
5569
|
init_tool_mutation();
|
|
4543
5570
|
init_tool_names();
|
|
4544
5571
|
init_tool_resolvers();
|
|
5572
|
+
init_tool_types();
|
|
5573
|
+
init_tool_types();
|
|
5574
|
+
init_validate_tool_input();
|
|
4545
5575
|
manageContactTool = defineAffinityTool({
|
|
4546
5576
|
name: manageContactToolName,
|
|
4547
|
-
description: "Create a contact
|
|
5577
|
+
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.",
|
|
4548
5578
|
whenToUse: "Use this for contact creation and lifecycle-safe contact maintenance.",
|
|
4549
5579
|
whenNotToUse: "Do not use this for identities, links, attributes, or merges.",
|
|
4550
5580
|
sideEffects: "writes_state",
|
|
@@ -4565,10 +5595,25 @@ var init_manage_contact_tool = __esm({
|
|
|
4565
5595
|
action: enumSchema("Operation to perform.", [
|
|
4566
5596
|
"create",
|
|
4567
5597
|
"revise",
|
|
4568
|
-
"set_lifecycle"
|
|
5598
|
+
"set_lifecycle",
|
|
5599
|
+
"export",
|
|
5600
|
+
"delete"
|
|
4569
5601
|
]),
|
|
4570
5602
|
input: objectSchema(
|
|
4571
5603
|
{
|
|
5604
|
+
identities: arraySchema(
|
|
5605
|
+
objectSchema(
|
|
5606
|
+
{
|
|
5607
|
+
type: stringSchema("Identity type."),
|
|
5608
|
+
value: stringSchema("Identity value."),
|
|
5609
|
+
label: stringSchema("Optional label."),
|
|
5610
|
+
verified: booleanSchema("Verified identity."),
|
|
5611
|
+
now: integerSchema("Optional timestamp.")
|
|
5612
|
+
},
|
|
5613
|
+
["type", "value"]
|
|
5614
|
+
),
|
|
5615
|
+
"Optional identities to attach. Identities already owned by another contact are left with that owner and reported as possible duplicates."
|
|
5616
|
+
),
|
|
4572
5617
|
name: stringSchema("Contact name."),
|
|
4573
5618
|
kind: enumSchema("Contact kind.", [
|
|
4574
5619
|
"human",
|
|
@@ -4587,6 +5632,7 @@ var init_manage_contact_tool = __esm({
|
|
|
4587
5632
|
["name", "kind"],
|
|
4588
5633
|
"New contact payload. Required when action=create."
|
|
4589
5634
|
),
|
|
5635
|
+
confirm: booleanSchema("Required true for permanent deletion."),
|
|
4590
5636
|
contact: contactLocatorSchema3(
|
|
4591
5637
|
"Target contact. Required when action=revise or set_lifecycle. Provide contactId or identity."
|
|
4592
5638
|
),
|
|
@@ -4856,9 +5902,32 @@ function resolveDateAnchorTarget(db, target) {
|
|
|
4856
5902
|
}
|
|
4857
5903
|
return { kind: "ok", target: { kind: "link", linkId: link.value.id } };
|
|
4858
5904
|
}
|
|
4859
|
-
function manageDateAnchorToolHandler(db,
|
|
5905
|
+
function manageDateAnchorToolHandler(db, rawInput) {
|
|
5906
|
+
const input = structuredClone(rawInput);
|
|
5907
|
+
const inputError = validateToolInput(manageDateAnchorTool.inputSchema, input);
|
|
5908
|
+
if (inputError)
|
|
5909
|
+
return toolFailure(
|
|
5910
|
+
"protocol",
|
|
5911
|
+
"invalid_input",
|
|
5912
|
+
"Invalid input.",
|
|
5913
|
+
inputError
|
|
5914
|
+
);
|
|
5915
|
+
if (input.action !== "add" && input.anchorEventId === void 0)
|
|
5916
|
+
return toolFailure(
|
|
5917
|
+
"protocol",
|
|
5918
|
+
"invalid_input",
|
|
5919
|
+
"Missing anchorEventId.",
|
|
5920
|
+
"anchorEventId is required."
|
|
5921
|
+
);
|
|
4860
5922
|
return withToolHandling(() => {
|
|
4861
5923
|
if (input.action === "add") {
|
|
5924
|
+
if (!input.input)
|
|
5925
|
+
return toolFailure(
|
|
5926
|
+
"protocol",
|
|
5927
|
+
"invalid_input",
|
|
5928
|
+
"Missing input.",
|
|
5929
|
+
"input is required for add."
|
|
5930
|
+
);
|
|
4862
5931
|
const resolved = resolveDateAnchorTarget(db, input.input.target);
|
|
4863
5932
|
if (resolved.kind === "error") {
|
|
4864
5933
|
return resolved.result;
|
|
@@ -4873,7 +5942,8 @@ function manageDateAnchorToolHandler(db, input) {
|
|
|
4873
5942
|
return mutationToolResult(
|
|
4874
5943
|
"revise",
|
|
4875
5944
|
reviseDateAnchor(db, input.anchorEventId, input.patch, input.options),
|
|
4876
|
-
"date anchor"
|
|
5945
|
+
"date anchor",
|
|
5946
|
+
Object.keys(input.patch)
|
|
4877
5947
|
);
|
|
4878
5948
|
}
|
|
4879
5949
|
return mutationToolResult(
|
|
@@ -4893,6 +5963,8 @@ var init_manage_date_anchor_tool = __esm({
|
|
|
4893
5963
|
init_tool_mutation();
|
|
4894
5964
|
init_tool_names();
|
|
4895
5965
|
init_tool_resolvers();
|
|
5966
|
+
init_tool_types();
|
|
5967
|
+
init_validate_tool_input();
|
|
4896
5968
|
manageDateAnchorTool = defineAffinityTool({
|
|
4897
5969
|
name: manageDateAnchorToolName,
|
|
4898
5970
|
description: "Add, revise, or remove recurring date anchors on contacts or links. Use this for birthdays, anniversaries, and yearly events \u2014 not manage_attribute.",
|
|
@@ -5006,7 +6078,7 @@ function buildDismissalMutationReceipt(primary, partial) {
|
|
|
5006
6078
|
derivedEffects: normalized.derivedEffects
|
|
5007
6079
|
};
|
|
5008
6080
|
}
|
|
5009
|
-
var
|
|
6081
|
+
var init_receipts6 = __esm({
|
|
5010
6082
|
"src/merges/receipts.ts"() {
|
|
5011
6083
|
init_normalize_mutation_receipt();
|
|
5012
6084
|
}
|
|
@@ -5068,7 +6140,7 @@ var init_dismiss_duplicate = __esm({
|
|
|
5068
6140
|
init_affinity_validation_error();
|
|
5069
6141
|
init_resolve_now();
|
|
5070
6142
|
init_with_transaction();
|
|
5071
|
-
|
|
6143
|
+
init_receipts6();
|
|
5072
6144
|
}
|
|
5073
6145
|
});
|
|
5074
6146
|
|
|
@@ -5107,7 +6179,7 @@ var init_undismiss_duplicate = __esm({
|
|
|
5107
6179
|
init_affinity_not_found_error();
|
|
5108
6180
|
init_affinity_validation_error();
|
|
5109
6181
|
init_with_transaction();
|
|
5110
|
-
|
|
6182
|
+
init_receipts6();
|
|
5111
6183
|
}
|
|
5112
6184
|
});
|
|
5113
6185
|
|
|
@@ -5129,7 +6201,19 @@ function contactLocatorSchema5(description) {
|
|
|
5129
6201
|
description
|
|
5130
6202
|
};
|
|
5131
6203
|
}
|
|
5132
|
-
function manageDuplicateDismissalToolHandler(db,
|
|
6204
|
+
function manageDuplicateDismissalToolHandler(db, rawInput) {
|
|
6205
|
+
const input = structuredClone(rawInput);
|
|
6206
|
+
const inputError = validateToolInput(
|
|
6207
|
+
manageDuplicateDismissalTool.inputSchema,
|
|
6208
|
+
input
|
|
6209
|
+
);
|
|
6210
|
+
if (inputError)
|
|
6211
|
+
return toolFailure(
|
|
6212
|
+
"protocol",
|
|
6213
|
+
"invalid_input",
|
|
6214
|
+
"Invalid input.",
|
|
6215
|
+
inputError
|
|
6216
|
+
);
|
|
5133
6217
|
return withToolHandling(() => {
|
|
5134
6218
|
const left = resolveContactLocator(db, input.left, "left");
|
|
5135
6219
|
if (!left.ok) {
|
|
@@ -5139,6 +6223,11 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
5139
6223
|
if (!right.ok) {
|
|
5140
6224
|
return right.result;
|
|
5141
6225
|
}
|
|
6226
|
+
const leftContactId = Math.min(left.value.id, right.value.id);
|
|
6227
|
+
const rightContactId = Math.max(left.value.id, right.value.id);
|
|
6228
|
+
const existed = db.prepare(
|
|
6229
|
+
"SELECT 1 FROM dismissed_duplicates WHERE left_id = ? AND right_id = ?"
|
|
6230
|
+
).get(leftContactId, rightContactId) !== void 0;
|
|
5142
6231
|
if (input.action === "dismiss") {
|
|
5143
6232
|
const receipt2 = dismissDuplicate(
|
|
5144
6233
|
db,
|
|
@@ -5148,13 +6237,35 @@ function manageDuplicateDismissalToolHandler(db, input) {
|
|
|
5148
6237
|
);
|
|
5149
6238
|
return toolSuccess(
|
|
5150
6239
|
`Dismissed pair #${receipt2.primary.leftContactId} / #${receipt2.primary.rightContactId}.`,
|
|
5151
|
-
{
|
|
6240
|
+
{
|
|
6241
|
+
...receipt2,
|
|
6242
|
+
action: "dismiss",
|
|
6243
|
+
changes: [
|
|
6244
|
+
{
|
|
6245
|
+
entity: "duplicate_dismissal",
|
|
6246
|
+
leftContactId,
|
|
6247
|
+
rightContactId,
|
|
6248
|
+
action: existed ? "updated" : "created"
|
|
6249
|
+
}
|
|
6250
|
+
]
|
|
6251
|
+
}
|
|
5152
6252
|
);
|
|
5153
6253
|
}
|
|
5154
6254
|
const receipt = undismissDuplicate(db, left.value.id, right.value.id);
|
|
5155
6255
|
return toolSuccess(
|
|
5156
6256
|
`Un-dismissed pair #${receipt.primary.leftContactId} / #${receipt.primary.rightContactId}.`,
|
|
5157
|
-
{
|
|
6257
|
+
{
|
|
6258
|
+
...receipt,
|
|
6259
|
+
action: "undismiss",
|
|
6260
|
+
changes: existed ? [
|
|
6261
|
+
{
|
|
6262
|
+
entity: "duplicate_dismissal",
|
|
6263
|
+
leftContactId,
|
|
6264
|
+
rightContactId,
|
|
6265
|
+
action: "removed"
|
|
6266
|
+
}
|
|
6267
|
+
] : []
|
|
6268
|
+
}
|
|
5158
6269
|
);
|
|
5159
6270
|
}, "Duplicate dismissal failed.");
|
|
5160
6271
|
}
|
|
@@ -5167,9 +6278,11 @@ var init_manage_duplicate_dismissal_tool = __esm({
|
|
|
5167
6278
|
init_tool_names();
|
|
5168
6279
|
init_tool_resolvers();
|
|
5169
6280
|
init_tool_types();
|
|
6281
|
+
init_tool_types();
|
|
6282
|
+
init_validate_tool_input();
|
|
5170
6283
|
manageDuplicateDismissalTool = defineAffinityTool({
|
|
5171
6284
|
name: manageDuplicateDismissalToolName,
|
|
5172
|
-
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in
|
|
6285
|
+
description: "Dismiss or un-dismiss a duplicate candidate pair so it does not resurface in review_affinity contacts.duplicates.",
|
|
5173
6286
|
whenToUse: "Use dismiss after confirming two contacts are different people (not duplicates). Use undismiss to re-surface a pair for re-evaluation.",
|
|
5174
6287
|
whenNotToUse: "Do not use this to merge contacts \u2014 use merge_contacts for confirmed duplicates.",
|
|
5175
6288
|
sideEffects: "writes_state",
|
|
@@ -5187,140 +6300,18 @@ var init_manage_duplicate_dismissal_tool = __esm({
|
|
|
5187
6300
|
{
|
|
5188
6301
|
action: enumSchema("Action to perform.", ["dismiss", "undismiss"]),
|
|
5189
6302
|
left: contactLocatorSchema5(
|
|
5190
|
-
"Left contact. Provide contactId or identity."
|
|
5191
|
-
),
|
|
5192
|
-
right: contactLocatorSchema5(
|
|
5193
|
-
"Right contact. Provide contactId or identity."
|
|
5194
|
-
),
|
|
5195
|
-
reason: nullableStringSchema("Optional reason for the dismissal.")
|
|
5196
|
-
},
|
|
5197
|
-
["action", "left", "right"],
|
|
5198
|
-
"Dismiss or un-dismiss a duplicate candidate pair."
|
|
5199
|
-
),
|
|
5200
|
-
handler: manageDuplicateDismissalToolHandler
|
|
5201
|
-
});
|
|
5202
|
-
}
|
|
5203
|
-
});
|
|
5204
|
-
|
|
5205
|
-
// src/identities/queries.ts
|
|
5206
|
-
function getIdentityRowById(db, id) {
|
|
5207
|
-
const row = db.prepare(
|
|
5208
|
-
`SELECT id, contact_id, type, value, label, normalized_key, verified, verified_at,
|
|
5209
|
-
created_at, updated_at, removed_at
|
|
5210
|
-
FROM identities
|
|
5211
|
-
WHERE id = ? AND removed_at IS NULL`
|
|
5212
|
-
).get(id);
|
|
5213
|
-
if (row === void 0) {
|
|
5214
|
-
return null;
|
|
5215
|
-
}
|
|
5216
|
-
return row;
|
|
5217
|
-
}
|
|
5218
|
-
var init_queries5 = __esm({
|
|
5219
|
-
"src/identities/queries.ts"() {
|
|
5220
|
-
}
|
|
5221
|
-
});
|
|
5222
|
-
|
|
5223
|
-
// src/identities/receipts.ts
|
|
5224
|
-
function buildIdentityMutationReceipt(primary, partial) {
|
|
5225
|
-
const normalized = normalizeMutationReceiptPartial(partial);
|
|
5226
|
-
return {
|
|
5227
|
-
primary,
|
|
5228
|
-
created: normalized.created,
|
|
5229
|
-
updated: normalized.updated,
|
|
5230
|
-
archived: normalized.archived,
|
|
5231
|
-
removed: normalized.removed,
|
|
5232
|
-
affectedLinks: normalized.affectedLinks,
|
|
5233
|
-
derivedEffects: normalized.derivedEffects
|
|
5234
|
-
};
|
|
5235
|
-
}
|
|
5236
|
-
var init_receipts6 = __esm({
|
|
5237
|
-
"src/identities/receipts.ts"() {
|
|
5238
|
-
init_normalize_mutation_receipt();
|
|
5239
|
-
}
|
|
5240
|
-
});
|
|
5241
|
-
|
|
5242
|
-
// src/identities/validators.ts
|
|
5243
|
-
function assertNoIdentityCollision(db, normalizedKey, excludeIdentityId) {
|
|
5244
|
-
const row = excludeIdentityId === void 0 ? db.prepare(
|
|
5245
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL"
|
|
5246
|
-
).get(normalizedKey) : db.prepare(
|
|
5247
|
-
"SELECT id FROM identities WHERE normalized_key = ? AND removed_at IS NULL AND id != ?"
|
|
5248
|
-
).get(normalizedKey, excludeIdentityId);
|
|
5249
|
-
if (row !== void 0) {
|
|
5250
|
-
throw new AffinityConflictError(
|
|
5251
|
-
"another contact already owns this normalized identity"
|
|
5252
|
-
);
|
|
5253
|
-
}
|
|
5254
|
-
}
|
|
5255
|
-
var init_validators5 = __esm({
|
|
5256
|
-
"src/identities/validators.ts"() {
|
|
5257
|
-
init_affinity_conflict_error();
|
|
5258
|
-
}
|
|
5259
|
-
});
|
|
5260
|
-
|
|
5261
|
-
// src/identities/add_identity.ts
|
|
5262
|
-
function addIdentity(db, contactId, input) {
|
|
5263
|
-
return withTransaction(db, () => {
|
|
5264
|
-
const contact = getContactRowById(db, contactId);
|
|
5265
|
-
if (!contact) {
|
|
5266
|
-
throw new AffinityNotFoundError("contact not found");
|
|
5267
|
-
}
|
|
5268
|
-
if (contact.lifecycle_state === "merged") {
|
|
5269
|
-
throw new AffinityStateError("merged contact is read-only");
|
|
5270
|
-
}
|
|
5271
|
-
const type = input.type.trim();
|
|
5272
|
-
const value = input.value.trim();
|
|
5273
|
-
if (type.length === 0 || value.length === 0) {
|
|
5274
|
-
throw new AffinityValidationError("type and value must be non-empty");
|
|
5275
|
-
}
|
|
5276
|
-
const nk = normalizeIdentityKey(type, value);
|
|
5277
|
-
assertNoIdentityCollision(db, nk);
|
|
5278
|
-
const now = resolveNow(input.now);
|
|
5279
|
-
const verified = input.verified ? 1 : 0;
|
|
5280
|
-
const verifiedAt = verified === 1 ? now : null;
|
|
5281
|
-
const result = db.prepare(
|
|
5282
|
-
`INSERT INTO identities (contact_id, type, value, label, normalized_key, verified, verified_at, created_at, updated_at)
|
|
5283
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
5284
|
-
).run(
|
|
5285
|
-
contactId,
|
|
5286
|
-
type,
|
|
5287
|
-
value,
|
|
5288
|
-
input.label ?? null,
|
|
5289
|
-
nk,
|
|
5290
|
-
verified,
|
|
5291
|
-
verifiedAt,
|
|
5292
|
-
now,
|
|
5293
|
-
now
|
|
5294
|
-
);
|
|
5295
|
-
const id = Number(result.lastInsertRowid);
|
|
5296
|
-
const row = getIdentityRowById(db, id);
|
|
5297
|
-
if (!row) {
|
|
5298
|
-
throw new AffinityInvariantError("inserted identity not found");
|
|
5299
|
-
}
|
|
5300
|
-
refreshContactRollup(db, contactId, now);
|
|
5301
|
-
db.prepare(
|
|
5302
|
-
"DELETE FROM dismissed_duplicates WHERE left_id = ? OR right_id = ?"
|
|
5303
|
-
).run(contactId, contactId);
|
|
5304
|
-
return buildIdentityMutationReceipt(mapIdentityRowToIdentityRecord(row), {
|
|
5305
|
-
created: [{ kind: "identity", id }]
|
|
6303
|
+
"Left contact. Provide contactId or identity."
|
|
6304
|
+
),
|
|
6305
|
+
right: contactLocatorSchema5(
|
|
6306
|
+
"Right contact. Provide contactId or identity."
|
|
6307
|
+
),
|
|
6308
|
+
reason: nullableStringSchema("Optional reason for the dismissal.")
|
|
6309
|
+
},
|
|
6310
|
+
["action", "left", "right"],
|
|
6311
|
+
"Dismiss or un-dismiss a duplicate candidate pair."
|
|
6312
|
+
),
|
|
6313
|
+
handler: manageDuplicateDismissalToolHandler
|
|
5306
6314
|
});
|
|
5307
|
-
});
|
|
5308
|
-
}
|
|
5309
|
-
var init_add_identity = __esm({
|
|
5310
|
-
"src/identities/add_identity.ts"() {
|
|
5311
|
-
init_queries();
|
|
5312
|
-
init_rollups();
|
|
5313
|
-
init_mappers2();
|
|
5314
|
-
init_normalize();
|
|
5315
|
-
init_queries5();
|
|
5316
|
-
init_receipts6();
|
|
5317
|
-
init_validators5();
|
|
5318
|
-
init_affinity_invariant_error();
|
|
5319
|
-
init_affinity_not_found_error();
|
|
5320
|
-
init_affinity_state_error();
|
|
5321
|
-
init_affinity_validation_error();
|
|
5322
|
-
init_resolve_now();
|
|
5323
|
-
init_with_transaction();
|
|
5324
6315
|
}
|
|
5325
6316
|
});
|
|
5326
6317
|
|
|
@@ -5355,7 +6346,7 @@ var init_remove_identity = __esm({
|
|
|
5355
6346
|
init_rollups();
|
|
5356
6347
|
init_mappers2();
|
|
5357
6348
|
init_queries5();
|
|
5358
|
-
|
|
6349
|
+
init_receipts5();
|
|
5359
6350
|
init_affinity_not_found_error();
|
|
5360
6351
|
init_affinity_state_error();
|
|
5361
6352
|
init_resolve_now();
|
|
@@ -5388,6 +6379,7 @@ function reviseIdentity(db, identityId, patch) {
|
|
|
5388
6379
|
if (nextType.length === 0 || nextValue.length === 0) {
|
|
5389
6380
|
throw new AffinityValidationError("type and value must be non-empty");
|
|
5390
6381
|
}
|
|
6382
|
+
validateIdentityInput(nextType, nextValue);
|
|
5391
6383
|
const nextLabel = patch.label !== void 0 ? patch.label : row.label;
|
|
5392
6384
|
const nkNext = normalizeIdentityKey(nextType, nextValue);
|
|
5393
6385
|
if (nkNext !== row.normalized_key) {
|
|
@@ -5413,13 +6405,14 @@ var init_revise_identity = __esm({
|
|
|
5413
6405
|
init_mappers2();
|
|
5414
6406
|
init_normalize();
|
|
5415
6407
|
init_queries5();
|
|
5416
|
-
|
|
6408
|
+
init_receipts5();
|
|
5417
6409
|
init_validators5();
|
|
5418
6410
|
init_affinity_not_found_error();
|
|
5419
6411
|
init_affinity_state_error();
|
|
5420
6412
|
init_affinity_validation_error();
|
|
5421
6413
|
init_resolve_now();
|
|
5422
6414
|
init_with_transaction();
|
|
6415
|
+
init_validate_identity_input();
|
|
5423
6416
|
}
|
|
5424
6417
|
});
|
|
5425
6418
|
|
|
@@ -5453,7 +6446,7 @@ var init_verify_identity = __esm({
|
|
|
5453
6446
|
init_queries();
|
|
5454
6447
|
init_mappers2();
|
|
5455
6448
|
init_queries5();
|
|
5456
|
-
|
|
6449
|
+
init_receipts5();
|
|
5457
6450
|
init_affinity_not_found_error();
|
|
5458
6451
|
init_affinity_state_error();
|
|
5459
6452
|
init_resolve_now();
|
|
@@ -5469,7 +6462,9 @@ function contactLocatorSchema6(description) {
|
|
|
5469
6462
|
contactId: integerSchema("Exact contact id."),
|
|
5470
6463
|
identity: objectSchema(
|
|
5471
6464
|
{
|
|
5472
|
-
type: stringSchema(
|
|
6465
|
+
type: stringSchema(
|
|
6466
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6467
|
+
),
|
|
5473
6468
|
value: stringSchema("Identity value.")
|
|
5474
6469
|
},
|
|
5475
6470
|
["type", "value"],
|
|
@@ -5479,7 +6474,23 @@ function contactLocatorSchema6(description) {
|
|
|
5479
6474
|
description
|
|
5480
6475
|
};
|
|
5481
6476
|
}
|
|
5482
|
-
function manageIdentityToolHandler(db,
|
|
6477
|
+
function manageIdentityToolHandler(db, rawInput) {
|
|
6478
|
+
const input = structuredClone(rawInput);
|
|
6479
|
+
const inputError = validateToolInput(manageIdentityTool.inputSchema, input);
|
|
6480
|
+
if (inputError)
|
|
6481
|
+
return toolFailure(
|
|
6482
|
+
"protocol",
|
|
6483
|
+
"invalid_input",
|
|
6484
|
+
"Invalid input.",
|
|
6485
|
+
inputError
|
|
6486
|
+
);
|
|
6487
|
+
if (input.action !== "add" && input.identityId === void 0)
|
|
6488
|
+
return toolFailure(
|
|
6489
|
+
"protocol",
|
|
6490
|
+
"invalid_input",
|
|
6491
|
+
"Missing identityId.",
|
|
6492
|
+
"identityId is required."
|
|
6493
|
+
);
|
|
5483
6494
|
return withToolHandling(() => {
|
|
5484
6495
|
switch (input.action) {
|
|
5485
6496
|
case "add": {
|
|
@@ -5497,7 +6508,8 @@ function manageIdentityToolHandler(db, input) {
|
|
|
5497
6508
|
return mutationToolResult(
|
|
5498
6509
|
"revise",
|
|
5499
6510
|
reviseIdentity(db, input.identityId, input.patch),
|
|
5500
|
-
"identity"
|
|
6511
|
+
"identity",
|
|
6512
|
+
Object.keys(input.patch)
|
|
5501
6513
|
);
|
|
5502
6514
|
case "verify":
|
|
5503
6515
|
return mutationToolResult(
|
|
@@ -5520,11 +6532,14 @@ var init_manage_identity_tool = __esm({
|
|
|
5520
6532
|
init_add_identity();
|
|
5521
6533
|
init_remove_identity();
|
|
5522
6534
|
init_revise_identity();
|
|
6535
|
+
init_validate_identity_input();
|
|
5523
6536
|
init_verify_identity();
|
|
5524
6537
|
init_tool_metadata();
|
|
5525
6538
|
init_tool_mutation();
|
|
5526
6539
|
init_tool_names();
|
|
5527
6540
|
init_tool_resolvers();
|
|
6541
|
+
init_tool_types();
|
|
6542
|
+
init_validate_tool_input();
|
|
5528
6543
|
manageIdentityTool = defineAffinityTool({
|
|
5529
6544
|
name: manageIdentityToolName,
|
|
5530
6545
|
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.",
|
|
@@ -5557,7 +6572,9 @@ var init_manage_identity_tool = __esm({
|
|
|
5557
6572
|
),
|
|
5558
6573
|
input: objectSchema(
|
|
5559
6574
|
{
|
|
5560
|
-
type: stringSchema(
|
|
6575
|
+
type: stringSchema(
|
|
6576
|
+
`Identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6577
|
+
),
|
|
5561
6578
|
value: stringSchema("Identity value."),
|
|
5562
6579
|
label: stringSchema("Optional label."),
|
|
5563
6580
|
verified: booleanSchema("Whether the identity starts verified."),
|
|
@@ -5571,7 +6588,9 @@ var init_manage_identity_tool = __esm({
|
|
|
5571
6588
|
),
|
|
5572
6589
|
patch: objectSchema(
|
|
5573
6590
|
{
|
|
5574
|
-
type: stringSchema(
|
|
6591
|
+
type: stringSchema(
|
|
6592
|
+
`Optional identity type: ${identityTypes.join(", ")}, or custom:<name> (lowercase a-z, 0-9, underscore or hyphen).`
|
|
6593
|
+
),
|
|
5575
6594
|
value: stringSchema("Optional identity value."),
|
|
5576
6595
|
label: stringSchema("Optional label.")
|
|
5577
6596
|
},
|
|
@@ -5842,7 +6861,19 @@ function linkLocatorSchema4(description) {
|
|
|
5842
6861
|
description
|
|
5843
6862
|
};
|
|
5844
6863
|
}
|
|
5845
|
-
function manageRelationshipToolHandler(db,
|
|
6864
|
+
function manageRelationshipToolHandler(db, rawInput) {
|
|
6865
|
+
const input = structuredClone(rawInput);
|
|
6866
|
+
const inputError = validateToolInput(
|
|
6867
|
+
manageRelationshipTool.inputSchema,
|
|
6868
|
+
input
|
|
6869
|
+
);
|
|
6870
|
+
if (inputError)
|
|
6871
|
+
return toolFailure(
|
|
6872
|
+
"protocol",
|
|
6873
|
+
"invalid_input",
|
|
6874
|
+
"Invalid input.",
|
|
6875
|
+
inputError
|
|
6876
|
+
);
|
|
5846
6877
|
return withToolHandling(() => {
|
|
5847
6878
|
switch (input.action) {
|
|
5848
6879
|
case "seed_social_link": {
|
|
@@ -5941,6 +6972,8 @@ var init_manage_relationship_tool = __esm({
|
|
|
5941
6972
|
init_tool_mutation();
|
|
5942
6973
|
init_tool_names();
|
|
5943
6974
|
init_tool_resolvers();
|
|
6975
|
+
init_tool_types();
|
|
6976
|
+
init_validate_tool_input();
|
|
5944
6977
|
manageRelationshipTool = defineAffinityTool({
|
|
5945
6978
|
name: manageRelationshipToolName,
|
|
5946
6979
|
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.",
|
|
@@ -6246,7 +7279,16 @@ function contactLocatorSchema8(description) {
|
|
|
6246
7279
|
description
|
|
6247
7280
|
};
|
|
6248
7281
|
}
|
|
6249
|
-
function mergeContactsToolHandler(db,
|
|
7282
|
+
function mergeContactsToolHandler(db, rawInput) {
|
|
7283
|
+
const input = structuredClone(rawInput);
|
|
7284
|
+
const inputError = validateToolInput(mergeContactsTool.inputSchema, input);
|
|
7285
|
+
if (inputError)
|
|
7286
|
+
return toolFailure(
|
|
7287
|
+
"protocol",
|
|
7288
|
+
"invalid_input",
|
|
7289
|
+
"Invalid input.",
|
|
7290
|
+
inputError
|
|
7291
|
+
);
|
|
6250
7292
|
return withToolHandling(() => {
|
|
6251
7293
|
const winner = resolveContactLocator(db, input.winner, "winner");
|
|
6252
7294
|
if (!winner.ok) {
|
|
@@ -6276,6 +7318,8 @@ var init_merge_contacts_tool = __esm({
|
|
|
6276
7318
|
init_tool_mutation();
|
|
6277
7319
|
init_tool_names();
|
|
6278
7320
|
init_tool_resolvers();
|
|
7321
|
+
init_tool_types();
|
|
7322
|
+
init_validate_tool_input();
|
|
6279
7323
|
mergeContactsTool = defineAffinityTool({
|
|
6280
7324
|
name: mergeContactsToolName,
|
|
6281
7325
|
description: "Merge one loser contact into one winner contact using explicit contact locators.",
|
|
@@ -6535,7 +7579,16 @@ function prepareParticipants2(db, raw, injectOwner) {
|
|
|
6535
7579
|
const normalized = raw.map(normalizeParticipant2);
|
|
6536
7580
|
return injectOwner ? ensureOwnerParticipates2(db, normalized) : normalized;
|
|
6537
7581
|
}
|
|
6538
|
-
function recordEventToolHandler(db,
|
|
7582
|
+
function recordEventToolHandler(db, rawInput) {
|
|
7583
|
+
const input = structuredClone(rawInput);
|
|
7584
|
+
const inputError = validateToolInput(recordEventTool.inputSchema, input);
|
|
7585
|
+
if (inputError)
|
|
7586
|
+
return toolFailure(
|
|
7587
|
+
"protocol",
|
|
7588
|
+
"invalid_input",
|
|
7589
|
+
"Invalid input.",
|
|
7590
|
+
inputError
|
|
7591
|
+
);
|
|
6539
7592
|
return withToolHandling(() => {
|
|
6540
7593
|
const patched = {
|
|
6541
7594
|
...input.input,
|
|
@@ -6585,6 +7638,8 @@ var init_record_event_tool = __esm({
|
|
|
6585
7638
|
init_tool_mutation();
|
|
6586
7639
|
init_tool_names();
|
|
6587
7640
|
init_tool_resolvers();
|
|
7641
|
+
init_tool_types();
|
|
7642
|
+
init_validate_tool_input();
|
|
6588
7643
|
recordEventTool = defineAffinityTool({
|
|
6589
7644
|
name: recordEventToolName,
|
|
6590
7645
|
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.",
|
|
@@ -6645,7 +7700,7 @@ var init_record_event_tool = __esm({
|
|
|
6645
7700
|
"observed"
|
|
6646
7701
|
])
|
|
6647
7702
|
},
|
|
6648
|
-
["
|
|
7703
|
+
["role"]
|
|
6649
7704
|
),
|
|
6650
7705
|
"Participant list. Must be non-empty and include the owner contact."
|
|
6651
7706
|
),
|
|
@@ -6663,6 +7718,27 @@ var init_record_event_tool = __esm({
|
|
|
6663
7718
|
}
|
|
6664
7719
|
});
|
|
6665
7720
|
|
|
7721
|
+
// src/contacts/contact_attribute_clauses.ts
|
|
7722
|
+
function contactAttributeClauses(filters) {
|
|
7723
|
+
const clauses = [];
|
|
7724
|
+
const params = [];
|
|
7725
|
+
const attributes = filters?.attribute === void 0 ? [] : [filters.attribute].flat();
|
|
7726
|
+
const tags = filters?.tag === void 0 ? [] : [filters.tag].flat();
|
|
7727
|
+
for (const filter of [...attributes, ...tags.map((tag) => `tag:${tag}`)]) {
|
|
7728
|
+
const separator = filter.indexOf("=");
|
|
7729
|
+
const name = separator < 0 ? filter : filter.slice(0, separator);
|
|
7730
|
+
clauses.push(`EXISTS (SELECT 1 FROM attributes a WHERE a.contact_id = contacts.id
|
|
7731
|
+
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(?)"})`);
|
|
7732
|
+
params.push(name);
|
|
7733
|
+
if (separator >= 0) params.push(filter.slice(separator + 1));
|
|
7734
|
+
}
|
|
7735
|
+
return { clauses, params };
|
|
7736
|
+
}
|
|
7737
|
+
var init_contact_attribute_clauses = __esm({
|
|
7738
|
+
"src/contacts/contact_attribute_clauses.ts"() {
|
|
7739
|
+
}
|
|
7740
|
+
});
|
|
7741
|
+
|
|
6666
7742
|
// src/contacts/list_contacts.ts
|
|
6667
7743
|
function listContacts(db, filters, options) {
|
|
6668
7744
|
assertDefaultOrdering("listContacts", options);
|
|
@@ -6686,6 +7762,9 @@ function listContacts(db, filters, options) {
|
|
|
6686
7762
|
if (filters?.includeDormant === false || options?.includeDormant === false) {
|
|
6687
7763
|
clauses.push("lifecycle_state != 'dormant'");
|
|
6688
7764
|
}
|
|
7765
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
7766
|
+
clauses.push(...attributeFilters.clauses);
|
|
7767
|
+
params.push(...attributeFilters.params);
|
|
6689
7768
|
const where = clauses.join(" AND ");
|
|
6690
7769
|
const rows = db.prepare(
|
|
6691
7770
|
`SELECT id, name, kind, lifecycle_state, is_owner, merged_into_contact_id,
|
|
@@ -6714,6 +7793,7 @@ var init_list_contacts = __esm({
|
|
|
6714
7793
|
"src/contacts/list_contacts.ts"() {
|
|
6715
7794
|
init_assert_default_ordering();
|
|
6716
7795
|
init_resolve_list_limit_offset();
|
|
7796
|
+
init_contact_attribute_clauses();
|
|
6717
7797
|
init_mappers4();
|
|
6718
7798
|
}
|
|
6719
7799
|
});
|
|
@@ -6941,131 +8021,6 @@ var init_list_upcoming_dates = __esm({
|
|
|
6941
8021
|
}
|
|
6942
8022
|
});
|
|
6943
8023
|
|
|
6944
|
-
// src/events/get_contact_journal.ts
|
|
6945
|
-
function getContactJournal(db, contactId, options) {
|
|
6946
|
-
assertDefaultOrdering("getContactJournal", options);
|
|
6947
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
6948
|
-
const clauses = ["e.deleted_at IS NULL", "ep.contact_id = ?"];
|
|
6949
|
-
const params = [contactId];
|
|
6950
|
-
if (options?.since !== void 0) {
|
|
6951
|
-
clauses.push("e.occurred_at >= ?");
|
|
6952
|
-
params.push(options.since);
|
|
6953
|
-
}
|
|
6954
|
-
if (options?.until !== void 0) {
|
|
6955
|
-
clauses.push("e.occurred_at <= ?");
|
|
6956
|
-
params.push(options.until);
|
|
6957
|
-
}
|
|
6958
|
-
if (options?.eventTypes !== void 0 && options.eventTypes.length > 0) {
|
|
6959
|
-
const ph = options.eventTypes.map(() => "?").join(", ");
|
|
6960
|
-
clauses.push(`e.type IN (${ph})`);
|
|
6961
|
-
params.push(...options.eventTypes);
|
|
6962
|
-
}
|
|
6963
|
-
const where = clauses.join(" AND ");
|
|
6964
|
-
const rows = db.prepare(
|
|
6965
|
-
`SELECT e.id FROM events e
|
|
6966
|
-
INNER JOIN event_participants ep ON ep.event_id = e.id
|
|
6967
|
-
WHERE ${where}
|
|
6968
|
-
ORDER BY e.occurred_at DESC, e.id DESC
|
|
6969
|
-
LIMIT ? OFFSET ?`
|
|
6970
|
-
).all(...params, limit, offset);
|
|
6971
|
-
return batchLoadEventRecords(
|
|
6972
|
-
db,
|
|
6973
|
-
rows.map((r) => r.id)
|
|
6974
|
-
);
|
|
6975
|
-
}
|
|
6976
|
-
var init_get_contact_journal = __esm({
|
|
6977
|
-
"src/events/get_contact_journal.ts"() {
|
|
6978
|
-
init_assert_default_ordering();
|
|
6979
|
-
init_resolve_list_limit_offset();
|
|
6980
|
-
init_loaders();
|
|
6981
|
-
}
|
|
6982
|
-
});
|
|
6983
|
-
|
|
6984
|
-
// src/events/list_open_commitments.ts
|
|
6985
|
-
function mapResolution(resolvedAt, resolution) {
|
|
6986
|
-
if (resolvedAt === null) {
|
|
6987
|
-
return "open";
|
|
6988
|
-
}
|
|
6989
|
-
if (resolution === "cancelled") {
|
|
6990
|
-
return "cancelled";
|
|
6991
|
-
}
|
|
6992
|
-
return "resolved";
|
|
6993
|
-
}
|
|
6994
|
-
function listOpenCommitments(db, filters, options) {
|
|
6995
|
-
assertDefaultOrdering("listOpenCommitments", options);
|
|
6996
|
-
const { limit, offset } = resolveListLimitOffset(options);
|
|
6997
|
-
const clauses = ["e.deleted_at IS NULL", "oc.resolved_at IS NULL"];
|
|
6998
|
-
const params = [];
|
|
6999
|
-
if (options?.since !== void 0) {
|
|
7000
|
-
clauses.push("e.occurred_at >= ?");
|
|
7001
|
-
params.push(options.since);
|
|
7002
|
-
}
|
|
7003
|
-
if (options?.until !== void 0) {
|
|
7004
|
-
clauses.push("e.occurred_at <= ?");
|
|
7005
|
-
params.push(options.until);
|
|
7006
|
-
}
|
|
7007
|
-
if (filters?.commitmentType !== void 0) {
|
|
7008
|
-
if (filters.commitmentType !== "promise" && filters.commitmentType !== "agreement") {
|
|
7009
|
-
throw new AffinityValidationError("invalid commitmentType filter");
|
|
7010
|
-
}
|
|
7011
|
-
clauses.push("oc.commitment_type = ?");
|
|
7012
|
-
params.push(filters.commitmentType);
|
|
7013
|
-
}
|
|
7014
|
-
if (filters?.contactId !== void 0) {
|
|
7015
|
-
clauses.push(
|
|
7016
|
-
"EXISTS (SELECT 1 FROM event_participants ep WHERE ep.event_id = oc.event_id AND ep.contact_id = ?)"
|
|
7017
|
-
);
|
|
7018
|
-
params.push(filters.contactId);
|
|
7019
|
-
}
|
|
7020
|
-
if (filters?.linkId !== void 0) {
|
|
7021
|
-
clauses.push(
|
|
7022
|
-
"EXISTS (SELECT 1 FROM link_event_effects le WHERE le.event_id = oc.event_id AND le.link_id = ?)"
|
|
7023
|
-
);
|
|
7024
|
-
params.push(filters.linkId);
|
|
7025
|
-
}
|
|
7026
|
-
if (filters?.horizonDays !== void 0) {
|
|
7027
|
-
if (!Number.isInteger(filters.horizonDays) || filters.horizonDays < 0) {
|
|
7028
|
-
throw new AffinityValidationError(
|
|
7029
|
-
"horizonDays must be a non-negative integer"
|
|
7030
|
-
);
|
|
7031
|
-
}
|
|
7032
|
-
const anchor = options?.since ?? Date.now();
|
|
7033
|
-
clauses.push("oc.due_at IS NOT NULL");
|
|
7034
|
-
clauses.push("oc.due_at <= ?");
|
|
7035
|
-
params.push(anchor + filters.horizonDays * DAY_MS3);
|
|
7036
|
-
}
|
|
7037
|
-
const where = clauses.join(" AND ");
|
|
7038
|
-
const rows = db.prepare(
|
|
7039
|
-
`SELECT oc.event_id, oc.commitment_type, oc.due_at, oc.resolved_at, oc.resolution,
|
|
7040
|
-
e.type, e.summary
|
|
7041
|
-
FROM open_commitments oc
|
|
7042
|
-
INNER JOIN events e ON e.id = oc.event_id
|
|
7043
|
-
WHERE ${where}
|
|
7044
|
-
ORDER BY oc.due_at IS NULL, oc.due_at ASC, oc.created_at ASC
|
|
7045
|
-
LIMIT ? OFFSET ?`
|
|
7046
|
-
).all(...params, limit, offset);
|
|
7047
|
-
const eventIds = rows.map((r) => r.event_id);
|
|
7048
|
-
const partsByEvent = batchLoadEventParticipantViews(db, eventIds);
|
|
7049
|
-
return rows.map((r) => ({
|
|
7050
|
-
eventId: r.event_id,
|
|
7051
|
-
type: r.type,
|
|
7052
|
-
summary: r.summary,
|
|
7053
|
-
participants: partsByEvent.get(r.event_id) ?? [],
|
|
7054
|
-
dueAt: r.due_at,
|
|
7055
|
-
resolutionState: mapResolution(r.resolved_at, r.resolution)
|
|
7056
|
-
}));
|
|
7057
|
-
}
|
|
7058
|
-
var DAY_MS3;
|
|
7059
|
-
var init_list_open_commitments = __esm({
|
|
7060
|
-
"src/events/list_open_commitments.ts"() {
|
|
7061
|
-
init_affinity_validation_error();
|
|
7062
|
-
init_assert_default_ordering();
|
|
7063
|
-
init_resolve_list_limit_offset();
|
|
7064
|
-
init_loaders();
|
|
7065
|
-
DAY_MS3 = 864e5;
|
|
7066
|
-
}
|
|
7067
|
-
});
|
|
7068
|
-
|
|
7069
8024
|
// src/graph/get_affinity_chart.ts
|
|
7070
8025
|
function getAffinityChart(db, options) {
|
|
7071
8026
|
const contactClauses = [
|
|
@@ -7299,13 +8254,15 @@ function listRadar(db, filters, options) {
|
|
|
7299
8254
|
if (ownerId === null) {
|
|
7300
8255
|
return [];
|
|
7301
8256
|
}
|
|
8257
|
+
const now = resolveNow(options?.now);
|
|
8258
|
+
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})`;
|
|
7302
8259
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
7303
8260
|
const clauses = [
|
|
7304
8261
|
"l.removed_at IS NULL",
|
|
7305
8262
|
"l.is_structural = 0",
|
|
7306
|
-
"l.from_contact_id = ?"
|
|
8263
|
+
"(l.from_contact_id = ? OR l.to_contact_id = ?)"
|
|
7307
8264
|
];
|
|
7308
|
-
const params = [ownerId];
|
|
8265
|
+
const params = [ownerId, ownerId];
|
|
7309
8266
|
if (filters?.kind !== void 0) {
|
|
7310
8267
|
clauses.push("l.kind = ?");
|
|
7311
8268
|
params.push(filters.kind);
|
|
@@ -7327,6 +8284,7 @@ function listRadar(db, filters, options) {
|
|
|
7327
8284
|
const rows = db.prepare(
|
|
7328
8285
|
`SELECT
|
|
7329
8286
|
l.id,
|
|
8287
|
+
${overdue} AS overdue_commitment,
|
|
7330
8288
|
l.from_contact_id,
|
|
7331
8289
|
l.to_contact_id,
|
|
7332
8290
|
l.trust,
|
|
@@ -7335,17 +8293,18 @@ function listRadar(db, filters, options) {
|
|
|
7335
8293
|
lr.recency_score,
|
|
7336
8294
|
lr.normalized_rank
|
|
7337
8295
|
FROM links l
|
|
7338
|
-
INNER JOIN contacts c_to ON c_to.id = l.to_contact_id
|
|
8296
|
+
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
|
|
7339
8297
|
AND c_to.deleted_at IS NULL AND c_to.lifecycle_state != 'merged'
|
|
7340
8298
|
LEFT JOIN link_rollups lr ON lr.link_id = l.id
|
|
7341
8299
|
WHERE ${where}
|
|
7342
|
-
ORDER BY COALESCE(lr.radar_score, 0) DESC
|
|
8300
|
+
ORDER BY ${overdue} DESC, COALESCE(lr.radar_score, 0) DESC
|
|
7343
8301
|
LIMIT ? OFFSET ?`
|
|
7344
8302
|
).all(...params, limit, offset);
|
|
7345
8303
|
return rows.map((row) => ({
|
|
7346
8304
|
linkId: row.id,
|
|
7347
8305
|
contactId: row.from_contact_id === ownerId ? row.to_contact_id : row.from_contact_id,
|
|
7348
|
-
|
|
8306
|
+
reasons: row.overdue_commitment ? ["overdue_commitment"] : [],
|
|
8307
|
+
driftPriority: row.overdue_commitment ? Math.max(1, row.drift_priority ?? 0) : row.drift_priority ?? 0,
|
|
7349
8308
|
recencyScore: row.recency_score ?? 0,
|
|
7350
8309
|
normalizedRank: row.normalized_rank ?? 0,
|
|
7351
8310
|
trust: row.trust ?? 0,
|
|
@@ -7361,6 +8320,7 @@ var init_list_radar = __esm({
|
|
|
7361
8320
|
init_queries();
|
|
7362
8321
|
init_assert_default_ordering();
|
|
7363
8322
|
init_resolve_list_limit_offset();
|
|
8323
|
+
init_resolve_now();
|
|
7364
8324
|
}
|
|
7365
8325
|
});
|
|
7366
8326
|
|
|
@@ -7414,6 +8374,60 @@ var init_list_dismissed_duplicates = __esm({
|
|
|
7414
8374
|
}
|
|
7415
8375
|
});
|
|
7416
8376
|
|
|
8377
|
+
// src/tools/enrich_review_item.ts
|
|
8378
|
+
function enrichReviewItem(db, item) {
|
|
8379
|
+
const name = (id) => {
|
|
8380
|
+
if (typeof id !== "number") return "";
|
|
8381
|
+
return db.prepare("SELECT name FROM contacts WHERE id = ?").get(id)?.name ?? `Contact #${id}`;
|
|
8382
|
+
};
|
|
8383
|
+
const result = { ...item };
|
|
8384
|
+
if (item.kind === "link") {
|
|
8385
|
+
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,
|
|
8386
|
+
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);
|
|
8387
|
+
if (link) {
|
|
8388
|
+
result.title = link.from_owner ? String(link.to_name) : link.to_owner ? String(link.from_name) : `${link.from_name} \xB7 ${link.to_name}`;
|
|
8389
|
+
result.subtitle = [link.kind, link.role].filter(Boolean).join(" \xB7 ");
|
|
8390
|
+
for (const key of ["affinity", "trust", "rank"])
|
|
8391
|
+
if (typeof link[key] === "number") result[key] = link[key];
|
|
8392
|
+
result.recencyScore ??= link.recency_score;
|
|
8393
|
+
result.driftPriority ??= link.drift_priority;
|
|
8394
|
+
result.readinessScore = link.readiness_score;
|
|
8395
|
+
result.normalizedRank ??= link.normalized_rank;
|
|
8396
|
+
result.reciprocityScore = link.reciprocity_score;
|
|
8397
|
+
result.radarScore = link.radar_score;
|
|
8398
|
+
}
|
|
8399
|
+
}
|
|
8400
|
+
if (item.kind === "event" && typeof item.linkId === "number" && !Array.isArray(item.participants)) {
|
|
8401
|
+
const link = db.prepare("SELECT from_contact_id, to_contact_id FROM links WHERE id = ?").get(item.linkId);
|
|
8402
|
+
if (link)
|
|
8403
|
+
result.subtitle = `${item.subtitle} \xB7 ${name(link.from_contact_id)} \xB7 ${name(link.to_contact_id)}`;
|
|
8404
|
+
}
|
|
8405
|
+
if (Array.isArray(item.participants)) {
|
|
8406
|
+
result.counterpartyNames = item.participants.map(
|
|
8407
|
+
(p) => name(p.contactId)
|
|
8408
|
+
);
|
|
8409
|
+
result.subtitle = [
|
|
8410
|
+
item.subtitle,
|
|
8411
|
+
result.counterpartyNames.join(", ")
|
|
8412
|
+
].filter(Boolean).join(" \xB7 ");
|
|
8413
|
+
}
|
|
8414
|
+
if (typeof item.leftContactId === "number")
|
|
8415
|
+
result.title = `${name(item.leftContactId)} \xB7 ${name(item.rightContactId)}`;
|
|
8416
|
+
if (typeof item.winnerContactId === "number")
|
|
8417
|
+
result.title = `Merged ${name(item.loserContactId)} into ${name(item.winnerContactId)}`;
|
|
8418
|
+
if (item.occursAt !== void 0 && item.kind === "contact")
|
|
8419
|
+
result.title = `${name(item.id)} \xB7 ${item.title}`;
|
|
8420
|
+
for (const [key, value] of Object.entries(result)) {
|
|
8421
|
+
if (typeof value === "number" && /score|priority|affinity|trust|rank/i.test(key))
|
|
8422
|
+
result[key] = Math.round(value * 1e3) / 1e3;
|
|
8423
|
+
}
|
|
8424
|
+
return result;
|
|
8425
|
+
}
|
|
8426
|
+
var init_enrich_review_item = __esm({
|
|
8427
|
+
"src/tools/enrich_review_item.ts"() {
|
|
8428
|
+
}
|
|
8429
|
+
});
|
|
8430
|
+
|
|
7417
8431
|
// src/tools/tool_summary.ts
|
|
7418
8432
|
function summarizeCount(count, singular) {
|
|
7419
8433
|
if (count === 0) {
|
|
@@ -7432,6 +8446,7 @@ var init_tool_summary = __esm({
|
|
|
7432
8446
|
// src/tools/review_affinity_tool.ts
|
|
7433
8447
|
function buildReadOptions(input) {
|
|
7434
8448
|
return {
|
|
8449
|
+
...input.now === void 0 ? {} : { now: input.now },
|
|
7435
8450
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
7436
8451
|
...input.offset === void 0 ? {} : { offset: input.offset },
|
|
7437
8452
|
...input.includeArchived === void 0 ? {} : { includeArchived: input.includeArchived },
|
|
@@ -7551,6 +8566,7 @@ function validateReviewInput(input) {
|
|
|
7551
8566
|
}
|
|
7552
8567
|
function duplicateCandidateItem(candidate) {
|
|
7553
8568
|
return {
|
|
8569
|
+
...candidate,
|
|
7554
8570
|
kind: "contact",
|
|
7555
8571
|
id: candidate.leftContactId,
|
|
7556
8572
|
title: `Possible duplicate: #${candidate.leftContactId} and #${candidate.rightContactId}`,
|
|
@@ -7560,24 +8576,28 @@ function duplicateCandidateItem(candidate) {
|
|
|
7560
8576
|
}
|
|
7561
8577
|
function momentItem(moment) {
|
|
7562
8578
|
return {
|
|
8579
|
+
...moment,
|
|
8580
|
+
occurredAtIso: new Date(moment.occurredAt).toISOString(),
|
|
7563
8581
|
kind: "event",
|
|
7564
8582
|
id: moment.eventId,
|
|
7565
8583
|
title: moment.summary,
|
|
7566
8584
|
subtitle: moment.momentKind,
|
|
7567
|
-
snippet:
|
|
8585
|
+
snippet: new Date(moment.occurredAt).toISOString().slice(0, 10)
|
|
7568
8586
|
};
|
|
7569
8587
|
}
|
|
7570
8588
|
function commitmentItem(commitment) {
|
|
7571
8589
|
return {
|
|
8590
|
+
...commitment,
|
|
7572
8591
|
kind: "event",
|
|
7573
8592
|
id: commitment.eventId,
|
|
7574
8593
|
title: commitment.summary,
|
|
7575
8594
|
subtitle: commitment.type,
|
|
7576
|
-
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt
|
|
8595
|
+
snippet: `state ${commitment.resolutionState}; participants ${commitment.participants.length}; due ${commitment.dueAt == null ? "none" : new Date(commitment.dueAt).toISOString().slice(0, 10)}`
|
|
7577
8596
|
};
|
|
7578
8597
|
}
|
|
7579
8598
|
function radarItem(radar) {
|
|
7580
8599
|
return {
|
|
8600
|
+
...radar,
|
|
7581
8601
|
kind: "link",
|
|
7582
8602
|
id: radar.linkId,
|
|
7583
8603
|
title: `Link #${radar.linkId}`,
|
|
@@ -7587,29 +8607,34 @@ function radarItem(radar) {
|
|
|
7587
8607
|
}
|
|
7588
8608
|
function upcomingDateItem(date) {
|
|
7589
8609
|
return {
|
|
8610
|
+
...date,
|
|
8611
|
+
occursAt: date.occursOn,
|
|
8612
|
+
occursAtIso: new Date(date.occursOn).toISOString(),
|
|
7590
8613
|
kind: date.targetRef.kind,
|
|
7591
8614
|
id: date.targetRef.id,
|
|
7592
8615
|
title: date.summary,
|
|
7593
8616
|
subtitle: date.recurrenceKind,
|
|
7594
|
-
snippet: `occurs ${date.occursOn}`
|
|
8617
|
+
snippet: `occurs ${new Date(date.occursOn).toISOString().slice(0, 10)}`
|
|
7595
8618
|
};
|
|
7596
8619
|
}
|
|
7597
8620
|
function mergeHistoryItem(record) {
|
|
7598
8621
|
return {
|
|
8622
|
+
...record,
|
|
7599
8623
|
kind: "contact",
|
|
7600
8624
|
id: record.winnerContactId,
|
|
7601
8625
|
title: `Merged #${record.loserContactId} into #${record.winnerContactId}`,
|
|
7602
|
-
subtitle:
|
|
8626
|
+
subtitle: new Date(record.mergedAt).toISOString().slice(0, 10),
|
|
7603
8627
|
snippet: record.reasonSummary ?? void 0
|
|
7604
8628
|
};
|
|
7605
8629
|
}
|
|
7606
8630
|
function dismissedPairItem(record) {
|
|
7607
8631
|
return {
|
|
8632
|
+
...record,
|
|
7608
8633
|
kind: "contact",
|
|
7609
8634
|
id: record.leftContactId,
|
|
7610
8635
|
title: `Dismissed pair: #${record.leftContactId} and #${record.rightContactId}`,
|
|
7611
8636
|
subtitle: record.reason ?? "no reason given",
|
|
7612
|
-
snippet: `dismissed at ${record.dismissedAt}`
|
|
8637
|
+
snippet: `dismissed at ${new Date(record.dismissedAt).toISOString().slice(0, 10)}`
|
|
7613
8638
|
};
|
|
7614
8639
|
}
|
|
7615
8640
|
function compactFilters(input) {
|
|
@@ -7619,7 +8644,16 @@ function compactFilters(input) {
|
|
|
7619
8644
|
)
|
|
7620
8645
|
);
|
|
7621
8646
|
}
|
|
7622
|
-
function reviewAffinityToolHandler(db,
|
|
8647
|
+
function reviewAffinityToolHandler(db, rawInput) {
|
|
8648
|
+
const input = structuredClone(rawInput);
|
|
8649
|
+
const inputError = validateToolInput(reviewAffinityTool.inputSchema, input);
|
|
8650
|
+
if (inputError)
|
|
8651
|
+
return toolFailure(
|
|
8652
|
+
"protocol",
|
|
8653
|
+
"invalid_input",
|
|
8654
|
+
"Invalid input.",
|
|
8655
|
+
inputError
|
|
8656
|
+
);
|
|
7623
8657
|
return withToolHandling(() => {
|
|
7624
8658
|
const invalid = validateReviewInput(input);
|
|
7625
8659
|
if (invalid) {
|
|
@@ -7633,6 +8667,9 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7633
8667
|
items = listContacts(
|
|
7634
8668
|
db,
|
|
7635
8669
|
{
|
|
8670
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
8671
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
8672
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
7636
8673
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
7637
8674
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
7638
8675
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -7663,7 +8700,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7663
8700
|
"Provide `contact` when `view` is `events.contact_journal`."
|
|
7664
8701
|
);
|
|
7665
8702
|
}
|
|
7666
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
8703
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
7667
8704
|
if (!contact.ok) {
|
|
7668
8705
|
return contact.result;
|
|
7669
8706
|
}
|
|
@@ -7698,7 +8735,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7698
8735
|
let contactId;
|
|
7699
8736
|
let linkId;
|
|
7700
8737
|
if (input.contact !== void 0) {
|
|
7701
|
-
const contact = resolveContactLocator(
|
|
8738
|
+
const contact = resolveContactLocator(
|
|
8739
|
+
db,
|
|
8740
|
+
input.contact,
|
|
8741
|
+
"contact",
|
|
8742
|
+
true
|
|
8743
|
+
);
|
|
7702
8744
|
if (!contact.ok) {
|
|
7703
8745
|
return contact.result;
|
|
7704
8746
|
}
|
|
@@ -7727,7 +8769,12 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7727
8769
|
let contactId;
|
|
7728
8770
|
let linkId;
|
|
7729
8771
|
if (input.contact !== void 0) {
|
|
7730
|
-
const contact = resolveContactLocator(
|
|
8772
|
+
const contact = resolveContactLocator(
|
|
8773
|
+
db,
|
|
8774
|
+
input.contact,
|
|
8775
|
+
"contact",
|
|
8776
|
+
true
|
|
8777
|
+
);
|
|
7731
8778
|
if (!contact.ok) {
|
|
7732
8779
|
return contact.result;
|
|
7733
8780
|
}
|
|
@@ -7820,7 +8867,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7820
8867
|
"Provide `contact` when `view` is `merges.history`."
|
|
7821
8868
|
);
|
|
7822
8869
|
}
|
|
7823
|
-
const contact = resolveContactLocator(db, locator, "contact");
|
|
8870
|
+
const contact = resolveContactLocator(db, locator, "contact", true);
|
|
7824
8871
|
if (!contact.ok) {
|
|
7825
8872
|
return contact.result;
|
|
7826
8873
|
}
|
|
@@ -7833,6 +8880,7 @@ function reviewAffinityToolHandler(db, input) {
|
|
|
7833
8880
|
items = listDismissedDuplicates(db, options).map(dismissedPairItem);
|
|
7834
8881
|
break;
|
|
7835
8882
|
}
|
|
8883
|
+
items = items.map((item) => enrichReviewItem(db, item));
|
|
7836
8884
|
return toolSuccess(
|
|
7837
8885
|
summarizeCount(items.length, "item"),
|
|
7838
8886
|
{
|
|
@@ -7871,12 +8919,15 @@ var init_review_affinity_tool = __esm({
|
|
|
7871
8919
|
init_list_radar();
|
|
7872
8920
|
init_get_merge_history();
|
|
7873
8921
|
init_list_dismissed_duplicates();
|
|
8922
|
+
init_enrich_review_item();
|
|
7874
8923
|
init_tool_metadata();
|
|
7875
8924
|
init_tool_names();
|
|
7876
8925
|
init_tool_ref();
|
|
7877
8926
|
init_tool_resolvers();
|
|
7878
8927
|
init_tool_summary();
|
|
7879
8928
|
init_tool_types();
|
|
8929
|
+
init_tool_types();
|
|
8930
|
+
init_validate_tool_input();
|
|
7880
8931
|
reviewAffinityTool = defineAffinityTool({
|
|
7881
8932
|
name: reviewAffinityToolName,
|
|
7882
8933
|
description: "Review one affinity surface such as contacts, journals, link queues, commitments, dates, graph state, or merge history.",
|
|
@@ -7888,12 +8939,16 @@ var init_review_affinity_tool = __esm({
|
|
|
7888
8939
|
targetKinds: ["contact", "link", "event"],
|
|
7889
8940
|
inputDescriptions: {
|
|
7890
8941
|
view: "Which review surface to return.",
|
|
8942
|
+
now: "Optional current timestamp for overdue calculations.",
|
|
7891
8943
|
contact: "Target contact locator for views that require one.",
|
|
7892
8944
|
link: "Target link locator for views that require one.",
|
|
7893
8945
|
contactIds: "Optional contact id subset for duplicate review or graph review.",
|
|
7894
8946
|
kind: "Optional contact kind or link kind filter, depending on the view.",
|
|
7895
8947
|
lifecycleState: "Optional contact lifecycle filter for the contact list view.",
|
|
7896
8948
|
linkState: "Optional link state filter for link review views.",
|
|
8949
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
8950
|
+
tag: "Tag name; arrays require all tags.",
|
|
8951
|
+
includeLost: "Whether lost contacts are included.",
|
|
7897
8952
|
includeOwner: "Whether owner contacts should be included in contact review.",
|
|
7898
8953
|
includeDormant: "Whether dormant links or contacts should be included where supported.",
|
|
7899
8954
|
includeArchived: "Whether archived links should be included where supported.",
|
|
@@ -7930,6 +8985,9 @@ var init_review_affinity_tool = __esm({
|
|
|
7930
8985
|
"merges.history",
|
|
7931
8986
|
"merges.dismissed"
|
|
7932
8987
|
]),
|
|
8988
|
+
now: integerSchema(
|
|
8989
|
+
"Optional current timestamp for overdue calculations."
|
|
8990
|
+
),
|
|
7933
8991
|
contact: contactLocatorSchema9("Optional target contact locator."),
|
|
7934
8992
|
link: linkLocatorSchema5("Optional target link locator."),
|
|
7935
8993
|
contactIds: arraySchema(
|
|
@@ -7952,6 +9010,17 @@ var init_review_affinity_tool = __esm({
|
|
|
7952
9010
|
"broken",
|
|
7953
9011
|
"archived"
|
|
7954
9012
|
]),
|
|
9013
|
+
attribute: oneOfSchema([
|
|
9014
|
+
stringSchema("Attribute filter."),
|
|
9015
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9016
|
+
]),
|
|
9017
|
+
tag: oneOfSchema([
|
|
9018
|
+
stringSchema("Tag filter."),
|
|
9019
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9020
|
+
]),
|
|
9021
|
+
includeLost: booleanSchema(
|
|
9022
|
+
"Include lost contacts (search excludes them by default)."
|
|
9023
|
+
),
|
|
7955
9024
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
7956
9025
|
includeDormant: booleanSchema("Whether dormant rows are included."),
|
|
7957
9026
|
includeArchived: booleanSchema("Whether archived links are included."),
|
|
@@ -7998,99 +9067,110 @@ var init_review_affinity_tool = __esm({
|
|
|
7998
9067
|
});
|
|
7999
9068
|
|
|
8000
9069
|
// src/contacts/search_contacts.ts
|
|
8001
|
-
function relevanceScore(name, queryLower, matchedName, matchedNormalizedIdentity) {
|
|
8002
|
-
const nl = name.toLowerCase();
|
|
8003
|
-
if (matchedNormalizedIdentity) return 1.1;
|
|
8004
|
-
if (nl.startsWith(queryLower)) return 1;
|
|
8005
|
-
if (matchedName) return 0.85;
|
|
8006
|
-
return 0.7;
|
|
8007
|
-
}
|
|
8008
9070
|
function searchContacts(db, query, filters, options) {
|
|
8009
|
-
const q = query.trim();
|
|
8010
|
-
if (q
|
|
8011
|
-
return [];
|
|
8012
|
-
}
|
|
9071
|
+
const q = foldSearchText(query.trim());
|
|
9072
|
+
if (!q) return [];
|
|
8013
9073
|
assertDefaultOrdering("searchContacts", options);
|
|
8014
9074
|
const { limit, offset } = resolveListLimitOffset(options);
|
|
8015
|
-
const
|
|
8016
|
-
const
|
|
8017
|
-
|
|
9075
|
+
const fields = /* @__PURE__ */ new Map();
|
|
9076
|
+
const rows = db.prepare(`
|
|
9077
|
+
SELECT contact_id, value || ' ' || normalized_key AS text, 'identity:' || type AS source FROM identities WHERE removed_at IS NULL
|
|
9078
|
+
UNION ALL SELECT contact_id, name || ' ' || COALESCE(value, ''), 'attribute:' || name FROM attributes WHERE deleted_at IS NULL AND contact_id IS NOT NULL
|
|
9079
|
+
UNION ALL SELECT from_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
9080
|
+
UNION ALL SELECT to_contact_id, COALESCE(role, '') || ' ' || COALESCE(bond, ''), 'link_role' FROM links WHERE removed_at IS NULL
|
|
9081
|
+
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
|
|
9082
|
+
`).all();
|
|
9083
|
+
for (const row of rows) {
|
|
9084
|
+
const values = fields.get(row.contact_id) ?? [];
|
|
9085
|
+
values.push({ text: foldSearchText(row.text), source: row.source });
|
|
9086
|
+
fields.set(row.contact_id, values);
|
|
9087
|
+
}
|
|
9088
|
+
const tokens = q.split(/\s+/);
|
|
9089
|
+
const phone = /^[+\d()\-/.\s]+$/.test(q) && q.replace(/\D/g, "").length >= 3;
|
|
9090
|
+
const digits = q.replace(/\D/g, "").replace(/^00/, "");
|
|
9091
|
+
const attributeFilters = contactAttributeClauses(filters);
|
|
8018
9092
|
const clauses = [
|
|
8019
|
-
"
|
|
8020
|
-
"
|
|
9093
|
+
"deleted_at IS NULL",
|
|
9094
|
+
"lifecycle_state != 'merged'",
|
|
9095
|
+
...attributeFilters.clauses
|
|
8021
9096
|
];
|
|
8022
|
-
const params = [];
|
|
9097
|
+
const params = [...attributeFilters.params];
|
|
8023
9098
|
if (filters?.kind !== void 0) {
|
|
8024
|
-
clauses.push("
|
|
9099
|
+
clauses.push("kind = ?");
|
|
8025
9100
|
params.push(filters.kind);
|
|
8026
9101
|
}
|
|
8027
9102
|
if (filters?.lifecycleState !== void 0) {
|
|
8028
|
-
clauses.push("
|
|
9103
|
+
clauses.push("lifecycle_state = ?");
|
|
8029
9104
|
params.push(filters.lifecycleState);
|
|
8030
9105
|
}
|
|
8031
|
-
if (filters?.includeOwner === false)
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
const
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
Number(
|
|
8070
|
-
r.matched_normalized_identity ?? 0
|
|
8071
|
-
) > 0
|
|
9106
|
+
if (filters?.includeOwner === false) clauses.push("is_owner = 0");
|
|
9107
|
+
const contacts = db.prepare(
|
|
9108
|
+
`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 ")}`
|
|
9109
|
+
).all(...params).map((row) => ({
|
|
9110
|
+
...mapContactRowToContactListItem(row),
|
|
9111
|
+
primaryIdentity: row.primary_identity
|
|
9112
|
+
}));
|
|
9113
|
+
const scored = [];
|
|
9114
|
+
for (const contact of contacts) {
|
|
9115
|
+
if (contact.lifecycleState === "dormant" && !(filters?.includeDormant ?? options?.includeDormant))
|
|
9116
|
+
continue;
|
|
9117
|
+
if (contact.lifecycleState === "lost" && !filters?.includeLost) continue;
|
|
9118
|
+
const name = foldSearchText(contact.name);
|
|
9119
|
+
const values = [
|
|
9120
|
+
{ text: name, source: "name" },
|
|
9121
|
+
...(fields.get(contact.id) ?? []).filter(
|
|
9122
|
+
(field) => !contact.isOwner || field.source !== "event" && field.source !== "link_role"
|
|
9123
|
+
)
|
|
9124
|
+
];
|
|
9125
|
+
const phoneFields = phone ? values.filter(
|
|
9126
|
+
(field) => field.source === "identity:phone" && field.text.split(" ").join("").replace(/[^\d]/g, "").replace(/^00/, "").includes(digits)
|
|
9127
|
+
) : [];
|
|
9128
|
+
if (!phoneFields.length && !tokens.every(
|
|
9129
|
+
(token) => values.some((field) => field.text.includes(token))
|
|
9130
|
+
))
|
|
9131
|
+
continue;
|
|
9132
|
+
const matchedOn = [
|
|
9133
|
+
...new Set(
|
|
9134
|
+
[
|
|
9135
|
+
...values.filter(
|
|
9136
|
+
(field) => tokens.some((token) => field.text.includes(token))
|
|
9137
|
+
),
|
|
9138
|
+
...phoneFields
|
|
9139
|
+
].map((field) => field.source)
|
|
9140
|
+
)
|
|
9141
|
+
];
|
|
9142
|
+
const identities = values.filter(
|
|
9143
|
+
(field) => field.source.startsWith("identity:")
|
|
8072
9144
|
);
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
});
|
|
8081
|
-
scored.sort((a, b) => b.score - a.score);
|
|
8082
|
-
return scored.slice(offset, offset + limit).map((s) => s.item);
|
|
9145
|
+
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(
|
|
9146
|
+
(field) => tokens.every((token) => field.text.includes(token))
|
|
9147
|
+
) ? 2 : 1;
|
|
9148
|
+
scored.push({ item: { ...contact, matchedOn }, score });
|
|
9149
|
+
}
|
|
9150
|
+
scored.sort((a, b) => b.score - a.score || a.item.id - b.item.id);
|
|
9151
|
+
return scored.slice(offset, offset + limit).map(({ item }) => item);
|
|
8083
9152
|
}
|
|
8084
9153
|
var init_search_contacts = __esm({
|
|
8085
9154
|
"src/contacts/search_contacts.ts"() {
|
|
8086
9155
|
init_assert_default_ordering();
|
|
8087
9156
|
init_resolve_list_limit_offset();
|
|
9157
|
+
init_contact_attribute_clauses();
|
|
9158
|
+
init_fold_search_text();
|
|
8088
9159
|
init_mappers4();
|
|
8089
9160
|
}
|
|
8090
9161
|
});
|
|
8091
9162
|
|
|
8092
9163
|
// src/tools/search_affinity_tool.ts
|
|
8093
|
-
function searchAffinityToolHandler(db,
|
|
9164
|
+
function searchAffinityToolHandler(db, rawInput) {
|
|
9165
|
+
const input = structuredClone(rawInput);
|
|
9166
|
+
const inputError = validateToolInput(searchAffinityTool.inputSchema, input);
|
|
9167
|
+
if (inputError)
|
|
9168
|
+
return toolFailure(
|
|
9169
|
+
"protocol",
|
|
9170
|
+
"invalid_input",
|
|
9171
|
+
"Invalid input.",
|
|
9172
|
+
inputError
|
|
9173
|
+
);
|
|
8094
9174
|
return withToolHandling(() => {
|
|
8095
9175
|
const options = {
|
|
8096
9176
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
@@ -8100,6 +9180,9 @@ function searchAffinityToolHandler(db, input) {
|
|
|
8100
9180
|
db,
|
|
8101
9181
|
input.query,
|
|
8102
9182
|
{
|
|
9183
|
+
...input.attribute === void 0 ? {} : { attribute: input.attribute },
|
|
9184
|
+
...input.tag === void 0 ? {} : { tag: input.tag },
|
|
9185
|
+
...input.includeLost === void 0 ? {} : { includeLost: input.includeLost },
|
|
8103
9186
|
...input.kind === void 0 ? {} : { kind: input.kind },
|
|
8104
9187
|
...input.lifecycleState === void 0 ? {} : { lifecycleState: input.lifecycleState },
|
|
8105
9188
|
...input.includeOwner === void 0 ? {} : { includeOwner: input.includeOwner },
|
|
@@ -8131,9 +9214,11 @@ var init_search_affinity_tool = __esm({
|
|
|
8131
9214
|
init_tool_resolvers();
|
|
8132
9215
|
init_tool_summary();
|
|
8133
9216
|
init_tool_types();
|
|
9217
|
+
init_tool_types();
|
|
9218
|
+
init_validate_tool_input();
|
|
8134
9219
|
searchAffinityTool = defineAffinityTool({
|
|
8135
9220
|
name: searchAffinityToolName,
|
|
8136
|
-
description: "Search contacts by name
|
|
9221
|
+
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.",
|
|
8137
9222
|
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.",
|
|
8138
9223
|
whenNotToUse: "Do not use this when you already know the exact contact you want to inspect in depth.",
|
|
8139
9224
|
sideEffects: "none",
|
|
@@ -8144,6 +9229,9 @@ var init_search_affinity_tool = __esm({
|
|
|
8144
9229
|
query: "Free-text or natural-key search text.",
|
|
8145
9230
|
kind: "Optional contact kind filter.",
|
|
8146
9231
|
lifecycleState: "Optional lifecycle state filter.",
|
|
9232
|
+
attribute: "Attribute name or name=value; arrays require all filters.",
|
|
9233
|
+
tag: "Tag name; arrays require all tags.",
|
|
9234
|
+
includeLost: "Whether lost contacts are included in search.",
|
|
8147
9235
|
includeOwner: "Whether owner contacts are eligible for results.",
|
|
8148
9236
|
includeDormant: "Whether dormant contacts should be eligible for results.",
|
|
8149
9237
|
limit: "Optional result limit.",
|
|
@@ -8168,6 +9256,17 @@ var init_search_affinity_tool = __esm({
|
|
|
8168
9256
|
"merged",
|
|
8169
9257
|
"lost"
|
|
8170
9258
|
]),
|
|
9259
|
+
attribute: oneOfSchema([
|
|
9260
|
+
stringSchema("Attribute filter."),
|
|
9261
|
+
arraySchema(stringSchema("Attribute filter."), "AND filters.")
|
|
9262
|
+
]),
|
|
9263
|
+
tag: oneOfSchema([
|
|
9264
|
+
stringSchema("Tag filter."),
|
|
9265
|
+
arraySchema(stringSchema("Tag filter."), "AND tags.")
|
|
9266
|
+
]),
|
|
9267
|
+
includeLost: booleanSchema(
|
|
9268
|
+
"Include lost contacts (search excludes them by default)."
|
|
9269
|
+
),
|
|
8171
9270
|
includeOwner: booleanSchema("Whether owner contacts are included."),
|
|
8172
9271
|
includeDormant: booleanSchema("Whether dormant contacts are included."),
|
|
8173
9272
|
limit: integerSchema("Optional result limit."),
|
|
@@ -8214,11 +9313,112 @@ var init_tool_registry = __esm({
|
|
|
8214
9313
|
}
|
|
8215
9314
|
});
|
|
8216
9315
|
|
|
9316
|
+
// src/surface/plain_dates.ts
|
|
9317
|
+
function parsePlainDue(raw, now = Date.now(), endOfDay = true) {
|
|
9318
|
+
const invalid = () => new CliUsageError(
|
|
9319
|
+
`Invalid timestamp "${raw}". Use YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, yesterday, +3d, -2d or an ISO date-time.`
|
|
9320
|
+
);
|
|
9321
|
+
if (/^-?\d+$/.test(raw)) {
|
|
9322
|
+
const value = Number(raw);
|
|
9323
|
+
if (Number.isSafeInteger(value) && Number.isFinite(new Date(value).getTime()))
|
|
9324
|
+
return value;
|
|
9325
|
+
throw invalid();
|
|
9326
|
+
}
|
|
9327
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
9328
|
+
const european = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw);
|
|
9329
|
+
let date;
|
|
9330
|
+
if (iso || european) {
|
|
9331
|
+
const year = Number(iso?.[1] ?? european?.[3]);
|
|
9332
|
+
const month = Number(iso?.[2] ?? european?.[2]);
|
|
9333
|
+
const day = Number(iso?.[3] ?? european?.[1]);
|
|
9334
|
+
date = /* @__PURE__ */ new Date(0);
|
|
9335
|
+
date.setFullYear(year, month - 1, day);
|
|
9336
|
+
date.setHours(
|
|
9337
|
+
endOfDay ? 23 : 0,
|
|
9338
|
+
endOfDay ? 59 : 0,
|
|
9339
|
+
endOfDay ? 59 : 0,
|
|
9340
|
+
endOfDay ? 999 : 0
|
|
9341
|
+
);
|
|
9342
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day)
|
|
9343
|
+
throw invalid();
|
|
9344
|
+
} else if (raw === "today" || raw === "tomorrow" || raw === "yesterday" || /^[+-]\d+d$/.test(raw)) {
|
|
9345
|
+
date = new Date(now);
|
|
9346
|
+
date.setDate(
|
|
9347
|
+
date.getDate() + (raw === "today" ? 0 : raw === "tomorrow" ? 1 : raw === "yesterday" ? -1 : Number(raw.slice(0, -1)))
|
|
9348
|
+
);
|
|
9349
|
+
date.setHours(
|
|
9350
|
+
endOfDay ? 23 : 0,
|
|
9351
|
+
endOfDay ? 59 : 0,
|
|
9352
|
+
endOfDay ? 59 : 0,
|
|
9353
|
+
endOfDay ? 999 : 0
|
|
9354
|
+
);
|
|
9355
|
+
} else {
|
|
9356
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
9357
|
+
raw
|
|
9358
|
+
))
|
|
9359
|
+
throw invalid();
|
|
9360
|
+
parsePlainDue(raw.slice(0, 10), now);
|
|
9361
|
+
date = new Date(raw);
|
|
9362
|
+
}
|
|
9363
|
+
if (!Number.isFinite(date.getTime())) throw invalid();
|
|
9364
|
+
return date.getTime();
|
|
9365
|
+
}
|
|
9366
|
+
function parsePlainStart(raw, now = Date.now()) {
|
|
9367
|
+
return parsePlainDue(raw, now, false);
|
|
9368
|
+
}
|
|
9369
|
+
function normalizeTimeInputs(value, now = Date.now()) {
|
|
9370
|
+
if (Array.isArray(value))
|
|
9371
|
+
return value.map((item) => normalizeTimeInputs(item, now));
|
|
9372
|
+
if (!value || typeof value !== "object") return value;
|
|
9373
|
+
return Object.fromEntries(
|
|
9374
|
+
Object.entries(value).map(([key, item]) => [
|
|
9375
|
+
key,
|
|
9376
|
+
typeof item === "string" && (key.endsWith("At") || ["asOf", "now"].includes(key)) ? key === "dueAt" ? parsePlainDue(item, now) : parsePlainStart(item, now) : normalizeTimeInputs(item, now)
|
|
9377
|
+
])
|
|
9378
|
+
);
|
|
9379
|
+
}
|
|
9380
|
+
var init_plain_dates = __esm({
|
|
9381
|
+
"src/surface/plain_dates.ts"() {
|
|
9382
|
+
init_cli_args();
|
|
9383
|
+
}
|
|
9384
|
+
});
|
|
9385
|
+
|
|
9386
|
+
// src/surface/timestamp_schema.ts
|
|
9387
|
+
function acceptsString(schema) {
|
|
9388
|
+
return schema.type === "string" || Array.isArray(schema.type) && schema.type.includes("string") || (schema.oneOf?.some(acceptsString) ?? false);
|
|
9389
|
+
}
|
|
9390
|
+
function timestampSchema(schema) {
|
|
9391
|
+
return {
|
|
9392
|
+
...schema,
|
|
9393
|
+
...schema.properties ? {
|
|
9394
|
+
properties: Object.fromEntries(
|
|
9395
|
+
Object.entries(schema.properties).map(([key, value]) => [
|
|
9396
|
+
key,
|
|
9397
|
+
key.endsWith("At") || ["asOf", "now"].includes(key) ? acceptsString(value) ? value : {
|
|
9398
|
+
description: value.description,
|
|
9399
|
+
oneOf: [value, { type: "string" }]
|
|
9400
|
+
} : timestampSchema(value)
|
|
9401
|
+
])
|
|
9402
|
+
)
|
|
9403
|
+
} : {},
|
|
9404
|
+
...schema.items ? { items: timestampSchema(schema.items) } : {},
|
|
9405
|
+
...schema.oneOf ? { oneOf: schema.oneOf.map(timestampSchema) } : {}
|
|
9406
|
+
};
|
|
9407
|
+
}
|
|
9408
|
+
var init_timestamp_schema = __esm({
|
|
9409
|
+
"src/surface/timestamp_schema.ts"() {
|
|
9410
|
+
}
|
|
9411
|
+
});
|
|
9412
|
+
|
|
8217
9413
|
// src/surface/affinity_surface_tools.ts
|
|
8218
9414
|
var affinitySurfaceTools, mapping, affinityCliSurfaceTools;
|
|
8219
9415
|
var init_affinity_surface_tools = __esm({
|
|
8220
9416
|
"src/surface/affinity_surface_tools.ts"() {
|
|
9417
|
+
init_cli_args();
|
|
8221
9418
|
init_tool_registry();
|
|
9419
|
+
init_tool_types();
|
|
9420
|
+
init_plain_dates();
|
|
9421
|
+
init_timestamp_schema();
|
|
8222
9422
|
affinitySurfaceTools = affinityTools.map((tool) => ({
|
|
8223
9423
|
name: tool.name,
|
|
8224
9424
|
description: tool.description,
|
|
@@ -8226,8 +9426,24 @@ var init_affinity_surface_tools = __esm({
|
|
|
8226
9426
|
...tool.whenNotToUse === void 0 ? {} : { whenNotToUse: tool.whenNotToUse },
|
|
8227
9427
|
readOnly: tool.readOnly,
|
|
8228
9428
|
sideEffects: tool.sideEffects,
|
|
8229
|
-
inputSchema: {
|
|
8230
|
-
|
|
9429
|
+
inputSchema: {
|
|
9430
|
+
...timestampSchema({ ...tool.inputSchema, type: "object" }),
|
|
9431
|
+
type: "object"
|
|
9432
|
+
},
|
|
9433
|
+
handler: (db, input) => {
|
|
9434
|
+
try {
|
|
9435
|
+
return tool.handler(db, normalizeTimeInputs(input));
|
|
9436
|
+
} catch (error) {
|
|
9437
|
+
if (error instanceof CliUsageError)
|
|
9438
|
+
return toolFailure(
|
|
9439
|
+
"protocol",
|
|
9440
|
+
"invalid_input",
|
|
9441
|
+
"Invalid input.",
|
|
9442
|
+
error.message
|
|
9443
|
+
);
|
|
9444
|
+
throw error;
|
|
9445
|
+
}
|
|
9446
|
+
}
|
|
8231
9447
|
}));
|
|
8232
9448
|
mapping = {
|
|
8233
9449
|
search_affinity: { verb: "search", positionals: ["query"] },
|
|
@@ -8288,10 +9504,22 @@ var init_affinity_surface_tools = __esm({
|
|
|
8288
9504
|
};
|
|
8289
9505
|
affinityCliSurfaceTools = /* @__PURE__ */ affinitySurfaceTools.map((tool) => ({
|
|
8290
9506
|
...tool,
|
|
9507
|
+
inputSchema: {
|
|
9508
|
+
...affinityTools.find((original) => original.name === tool.name)?.inputSchema,
|
|
9509
|
+
type: "object"
|
|
9510
|
+
},
|
|
8291
9511
|
...mapping[tool.name] ? { cli: mapping[tool.name] } : {}
|
|
8292
9512
|
}));
|
|
8293
9513
|
}
|
|
8294
9514
|
});
|
|
9515
|
+
function resolveAffinityDbPath(options = {}) {
|
|
9516
|
+
const path = options.flag ?? ((options.env ?? process.env).GHOSTPAW_AFFINITY_DB || join(homedir(), ".ghostpaw", "affinity.db"));
|
|
9517
|
+
return path === ":memory:" ? path : resolve(path);
|
|
9518
|
+
}
|
|
9519
|
+
var init_resolve_affinity_db_path = __esm({
|
|
9520
|
+
"src/surface/resolve_affinity_db_path.ts"() {
|
|
9521
|
+
}
|
|
9522
|
+
});
|
|
8295
9523
|
|
|
8296
9524
|
// src/attributes/init_attributes_tables.ts
|
|
8297
9525
|
function initAttributesTables(db) {
|
|
@@ -8584,6 +9812,12 @@ function initLinkEventEffectsTables(db) {
|
|
|
8584
9812
|
)
|
|
8585
9813
|
)
|
|
8586
9814
|
`);
|
|
9815
|
+
const columns = db.prepare("PRAGMA table_info(link_event_effects)").all();
|
|
9816
|
+
if (!columns.some((column) => column.name === "counts_for_recency")) {
|
|
9817
|
+
db.exec(
|
|
9818
|
+
"ALTER TABLE link_event_effects ADD COLUMN counts_for_recency INTEGER NOT NULL DEFAULT 1"
|
|
9819
|
+
);
|
|
9820
|
+
}
|
|
8587
9821
|
db.exec(
|
|
8588
9822
|
"CREATE INDEX IF NOT EXISTS idx_link_event_effects_link_time ON link_event_effects(link_id, occurred_at DESC, event_id DESC)"
|
|
8589
9823
|
);
|
|
@@ -8821,19 +10055,6 @@ var init_open_affinity_db = __esm({
|
|
|
8821
10055
|
}
|
|
8822
10056
|
});
|
|
8823
10057
|
|
|
8824
|
-
// src/surface/resolve_affinity_db_path.ts
|
|
8825
|
-
var resolve_affinity_db_path_exports = {};
|
|
8826
|
-
__export(resolve_affinity_db_path_exports, {
|
|
8827
|
-
resolveAffinityDbPath: () => resolveAffinityDbPath
|
|
8828
|
-
});
|
|
8829
|
-
function resolveAffinityDbPath(options = {}) {
|
|
8830
|
-
return options.flag ?? ((options.env ?? process.env).GHOSTPAW_AFFINITY_DB || join(homedir(), ".ghostpaw", "affinity.db"));
|
|
8831
|
-
}
|
|
8832
|
-
var init_resolve_affinity_db_path = __esm({
|
|
8833
|
-
"src/surface/resolve_affinity_db_path.ts"() {
|
|
8834
|
-
}
|
|
8835
|
-
});
|
|
8836
|
-
|
|
8837
10058
|
// src/surfaces/surface_types.ts
|
|
8838
10059
|
function isErrorResult(result) {
|
|
8839
10060
|
return typeof result === "object" && result !== null && "ok" in result && result.ok === false;
|
|
@@ -9073,10 +10294,10 @@ async function serveMcpStdio(server, options = {}) {
|
|
|
9073
10294
|
const response = await server.handleLine(line.replace(/\r$/, ""));
|
|
9074
10295
|
if (response !== void 0)
|
|
9075
10296
|
await new Promise(
|
|
9076
|
-
(
|
|
10297
|
+
(resolve2, reject) => output.write(
|
|
9077
10298
|
`${response}
|
|
9078
10299
|
`,
|
|
9079
|
-
(error) => error ? reject(error) :
|
|
10300
|
+
(error) => error ? reject(error) : resolve2()
|
|
9080
10301
|
)
|
|
9081
10302
|
);
|
|
9082
10303
|
};
|
|
@@ -9166,11 +10387,165 @@ var init_serve_affinity_mcp = __esm({
|
|
|
9166
10387
|
}
|
|
9167
10388
|
});
|
|
9168
10389
|
|
|
9169
|
-
// src/surfaces/
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
10390
|
+
// src/surfaces/cli_output.ts
|
|
10391
|
+
function enrichPlainJson(value) {
|
|
10392
|
+
if (Array.isArray(value)) return value.map(enrichPlainJson);
|
|
10393
|
+
if (value === null || typeof value !== "object")
|
|
10394
|
+
return typeof value === "number" && !Number.isInteger(value) ? Math.round(value * 1e3) / 1e3 : value;
|
|
10395
|
+
const result = {};
|
|
10396
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
10397
|
+
result[key] = enrichPlainJson(entry);
|
|
10398
|
+
if (key.endsWith("At") && typeof entry === "number" && Number.isFinite(new Date(entry).getTime()))
|
|
10399
|
+
result[`${key}Iso`] = new Date(entry).toISOString();
|
|
10400
|
+
}
|
|
10401
|
+
return result;
|
|
10402
|
+
}
|
|
10403
|
+
function suggestCommand(input, names) {
|
|
10404
|
+
let best;
|
|
10405
|
+
let distance = 3;
|
|
10406
|
+
for (const name of names) {
|
|
10407
|
+
let row = Array.from({ length: name.length + 1 }, (_, i) => i);
|
|
10408
|
+
for (let i = 0; i < input.length; i++) {
|
|
10409
|
+
const next = [i + 1];
|
|
10410
|
+
for (let j = 0; j < name.length; j++)
|
|
10411
|
+
next.push(
|
|
10412
|
+
Math.min(
|
|
10413
|
+
(next[j] ?? 0) + 1,
|
|
10414
|
+
(row[j + 1] ?? 0) + 1,
|
|
10415
|
+
(row[j] ?? 0) + (input[i] === name[j] ? 0 : 1)
|
|
10416
|
+
)
|
|
10417
|
+
);
|
|
10418
|
+
row = next;
|
|
10419
|
+
}
|
|
10420
|
+
const current = row[name.length] ?? 3;
|
|
10421
|
+
if (current < distance) {
|
|
10422
|
+
best = name;
|
|
10423
|
+
distance = current;
|
|
10424
|
+
}
|
|
10425
|
+
}
|
|
10426
|
+
return best;
|
|
10427
|
+
}
|
|
10428
|
+
function cliExitCode(value) {
|
|
10429
|
+
const r = value;
|
|
10430
|
+
const code = r.error?.code ?? "";
|
|
10431
|
+
if (r.outcome === "needs_clarification" || code === "not_found") return 3;
|
|
10432
|
+
if ([
|
|
10433
|
+
"conflict",
|
|
10434
|
+
"version_mismatch",
|
|
10435
|
+
"already_resolved",
|
|
10436
|
+
"already_in_state"
|
|
10437
|
+
].includes(code) || /already resolved/i.test(r.error?.message ?? ""))
|
|
10438
|
+
return 4;
|
|
10439
|
+
if (["invalid_input", "validation_error"].includes(code)) return 2;
|
|
10440
|
+
if (["system_error", "internal_error"].includes(code)) return 70;
|
|
10441
|
+
return r.ok === false ? 1 : 0;
|
|
10442
|
+
}
|
|
10443
|
+
function plainJson(value, compact = false, formatError) {
|
|
10444
|
+
const result = enrichPlainJson(value);
|
|
10445
|
+
if (!result || typeof result !== "object") return result;
|
|
10446
|
+
if (formatError && result.error && typeof result.error === "object") {
|
|
10447
|
+
result.error = Object.fromEntries(
|
|
10448
|
+
Object.entries(result.error).map(([key, value2]) => [
|
|
10449
|
+
key,
|
|
10450
|
+
typeof value2 === "string" && ["message", "recovery"].includes(key) ? formatError(value2) : value2
|
|
10451
|
+
])
|
|
10452
|
+
);
|
|
10453
|
+
}
|
|
10454
|
+
const data = result.data;
|
|
10455
|
+
if (data?.items && JSON.stringify(result.entities) === JSON.stringify(data.items))
|
|
10456
|
+
result.entities = void 0;
|
|
10457
|
+
const trim = (v) => {
|
|
10458
|
+
if (Array.isArray(v)) return v.map(trim);
|
|
10459
|
+
if (!v || typeof v !== "object") return v;
|
|
10460
|
+
return Object.fromEntries(
|
|
10461
|
+
Object.entries(v).filter(
|
|
10462
|
+
([k, x]) => !([
|
|
10463
|
+
"created",
|
|
10464
|
+
"updated",
|
|
10465
|
+
"archived",
|
|
10466
|
+
"removed",
|
|
10467
|
+
"affectedLinks",
|
|
10468
|
+
"derivedEffects"
|
|
10469
|
+
].includes(k) && Array.isArray(x) && !x.length)
|
|
10470
|
+
).map(([k, x]) => [k, trim(x)])
|
|
10471
|
+
);
|
|
10472
|
+
};
|
|
10473
|
+
return compact ? trim(result) : result;
|
|
10474
|
+
}
|
|
10475
|
+
function formatLocalTime(ms) {
|
|
10476
|
+
const d = new Date(ms);
|
|
10477
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
10478
|
+
const day = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
10479
|
+
const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
10480
|
+
return hm === "00:00" || hm === "23:59" ? day : `${day} ${hm}`;
|
|
10481
|
+
}
|
|
10482
|
+
function humanResult(value) {
|
|
10483
|
+
const r = value;
|
|
10484
|
+
const items = r.data?.items;
|
|
10485
|
+
if (Array.isArray(items)) {
|
|
10486
|
+
if (!items.length) return `${r.summary ?? "No results."}
|
|
10487
|
+
`;
|
|
10488
|
+
return `${items.map((item) => {
|
|
10489
|
+
const dates = Object.entries(item).filter(([k, v]) => k in DATE_LABELS && typeof v === "number").map(
|
|
10490
|
+
([k, v]) => `${DATE_LABELS[k]} ${// Recurring dates are stored as UTC calendar days.
|
|
10491
|
+
k === "occursAt" ? new Date(v).toISOString().slice(0, 10) : formatLocalTime(v)}`
|
|
10492
|
+
);
|
|
10493
|
+
if (Array.isArray(item.reasons) && item.reasons.length)
|
|
10494
|
+
dates.push(item.reasons.join(", ").replaceAll("_", " "));
|
|
10495
|
+
const subtitle = item.subtitle ? ` \u2014 ${item.subtitle}` : "";
|
|
10496
|
+
const tail = dates.length ? ` \xB7 ${dates.join(" \xB7 ")}` : "";
|
|
10497
|
+
return `#${item.id ?? item.entityId ?? "?"} ${item.title ?? item.name ?? ""}${subtitle}${tail}${item.overdue ? " \xB7 OVERDUE" : ""}`.trim();
|
|
10498
|
+
}).join("\n")}
|
|
10499
|
+
`;
|
|
10500
|
+
}
|
|
10501
|
+
const primary = r.data?.primary;
|
|
10502
|
+
if (primary)
|
|
10503
|
+
return `${r.summary ?? "Done"}${primary.id === void 0 ? "" : ` (id ${primary.id})`}
|
|
10504
|
+
`;
|
|
10505
|
+
if (r.data) {
|
|
10506
|
+
const fields = [];
|
|
10507
|
+
const display = (entry) => {
|
|
10508
|
+
if (entry === null) return "\u2014";
|
|
10509
|
+
if (Array.isArray(entry)) return entry.map(display).join(", ") || "\u2014";
|
|
10510
|
+
if (typeof entry === "object") {
|
|
10511
|
+
const item = entry;
|
|
10512
|
+
return String(
|
|
10513
|
+
item.title ?? item.name ?? item.summary ?? item.value ?? (item.id === void 0 ? JSON.stringify(item) : `#${item.id}`)
|
|
10514
|
+
);
|
|
10515
|
+
}
|
|
10516
|
+
return String(entry);
|
|
10517
|
+
};
|
|
10518
|
+
const flatten = (value2, prefix = "") => {
|
|
10519
|
+
if (!value2 || typeof value2 !== "object") return;
|
|
10520
|
+
for (const [key, entry] of Object.entries(value2)) {
|
|
10521
|
+
const label = `${prefix}${key}`;
|
|
10522
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry))
|
|
10523
|
+
flatten(entry, `${label}.`);
|
|
10524
|
+
else fields.push([label, display(entry)]);
|
|
10525
|
+
}
|
|
10526
|
+
};
|
|
10527
|
+
flatten(enrichPlainJson(r.data.detail ?? r.data));
|
|
10528
|
+
const width = Math.max(0, ...fields.map(([label]) => label.length));
|
|
10529
|
+
return `${fields.map(([label, value2]) => `${label.padEnd(width)}: ${value2}`).join("\n")}
|
|
10530
|
+
`;
|
|
10531
|
+
}
|
|
10532
|
+
return `${r.summary ?? ""}
|
|
10533
|
+
`;
|
|
10534
|
+
}
|
|
10535
|
+
var DATE_LABELS;
|
|
10536
|
+
var init_cli_output = __esm({
|
|
10537
|
+
"src/surfaces/cli_output.ts"() {
|
|
10538
|
+
DATE_LABELS = {
|
|
10539
|
+
occurredAt: "on",
|
|
10540
|
+
dueAt: "due",
|
|
10541
|
+
occursAt: "next",
|
|
10542
|
+
resolvedAt: "resolved",
|
|
10543
|
+
lastMeaningfulInteractionAt: "last"
|
|
10544
|
+
};
|
|
10545
|
+
}
|
|
9173
10546
|
});
|
|
10547
|
+
|
|
10548
|
+
// src/surfaces/skill_markdown.ts
|
|
9174
10549
|
function renderSkillMarkdown(spec, { referencesBase = "references" } = {}) {
|
|
9175
10550
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(spec.name) || spec.name.length > 64)
|
|
9176
10551
|
throw new Error("Invalid skill name");
|
|
@@ -9229,6 +10604,25 @@ function renderSkillMarkdown(spec, { referencesBase = "references" } = {}) {
|
|
|
9229
10604
|
lines.push(
|
|
9230
10605
|
`| ${name} | ${property.type ?? property.oneOf?.map((branch) => branch.type).join(" / ") ?? "any"} | ${tool.inputSchema.required?.includes(name) ? "yes" : "no"} | ${(property.enum ? `${property.enum.join(", ")}. ` : "") + (property.description ?? "").replaceAll("|", "\\|").replaceAll("\n", " ")} |`
|
|
9231
10606
|
);
|
|
10607
|
+
if (spec.cheatSheet) {
|
|
10608
|
+
for (const [parent, property] of Object.entries(
|
|
10609
|
+
tool.inputSchema.properties ?? {}
|
|
10610
|
+
)) {
|
|
10611
|
+
const nested = property.items ?? property;
|
|
10612
|
+
const branches = [nested, ...nested.oneOf ?? []];
|
|
10613
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10614
|
+
for (const branch of branches)
|
|
10615
|
+
for (const [key, child] of Object.entries(branch.properties ?? {})) {
|
|
10616
|
+
if (seen.has(key)) continue;
|
|
10617
|
+
seen.add(key);
|
|
10618
|
+
lines.push(
|
|
10619
|
+
`| ${parent}.${key} | ${child.type ?? "oneOf"} | ${branch.required?.includes(key) ? "yes" : "no"} | ${(child.description ?? "").replaceAll("|", "\\|").replaceAll("\n", " ")} |`
|
|
10620
|
+
);
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
if (spec.examples?.[tool.name])
|
|
10624
|
+
lines.push("", "```sh", spec.examples[tool.name] ?? "", "```");
|
|
10625
|
+
}
|
|
9232
10626
|
lines.push("");
|
|
9233
10627
|
}
|
|
9234
10628
|
lines.push(
|
|
@@ -9243,13 +10637,38 @@ function renderSkillMarkdown(spec, { referencesBase = "references" } = {}) {
|
|
|
9243
10637
|
...spec.principles.map((principle) => `- ${principle}`),
|
|
9244
10638
|
""
|
|
9245
10639
|
);
|
|
9246
|
-
|
|
10640
|
+
const output = lines.join("\n");
|
|
10641
|
+
if (!spec.cheatSheet) return output;
|
|
10642
|
+
const title = `# ${spec.name}
|
|
10643
|
+
|
|
10644
|
+
`;
|
|
10645
|
+
const start = output.indexOf(title) + title.length;
|
|
10646
|
+
const appendix = output.slice(start).replace(/ \(without a global install:.*?\)/g, "").replace(/^## /gm, "### ");
|
|
10647
|
+
return `${output.slice(0, start)}${spec.cheatSheet}
|
|
10648
|
+
|
|
10649
|
+
## Appendix: MCP and tool calls
|
|
10650
|
+
|
|
10651
|
+
${appendix}
|
|
10652
|
+
From the binary: \`${spec.bin} skill <guide>\`.
|
|
10653
|
+
${spec.packageName ? `
|
|
10654
|
+
## Without the binary
|
|
10655
|
+
|
|
10656
|
+
Use \`npx -y ${spec.packageName} ...\` (Node.js 24 or newer), or import \`${spec.packageName}\` as a library.
|
|
10657
|
+
` : ""}`;
|
|
9247
10658
|
}
|
|
9248
10659
|
var init_skill_markdown = __esm({
|
|
9249
10660
|
"src/surfaces/skill_markdown.ts"() {
|
|
9250
10661
|
}
|
|
9251
10662
|
});
|
|
9252
10663
|
|
|
10664
|
+
// src/surface/affinity_skill_references.ts
|
|
10665
|
+
var affinitySkillReferences;
|
|
10666
|
+
var init_affinity_skill_references = __esm({
|
|
10667
|
+
"src/surface/affinity_skill_references.ts"() {
|
|
10668
|
+
affinitySkillReferences = { "bootstrap-and-cold-start": { "description": "Establish a clean Affinity baseline with one owner, one verified identity, and the minimum structure needed for later evidence and review work.", "content": '# Bootstrap And Cold Start\n\nPrimary tools:\n- `manage_contact`\n- `manage_identity`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Establish one clean owner record, make it reachable, and verify the system is ready for later relationship work.\n\nWhen to use:\n- The database is new or logically empty.\n- A harness needs to initialize its own operator profile before doing anything else.\n\nWhen not to use:\n- The owner already exists and you only need to update profile fields.\n- You are importing historical relationships; use the import-focused skill after bootstrap.\n\nStep-by-step sequence:\n1. Assume the schema is already initialized and the database is live before using any tool.\n2. Use `manage_contact` with `action: "create"` and `bootstrapOwner: true` exactly once for the operator.\n3. Immediately attach at least one routing identity with `manage_identity` so future lookups can use a stable natural key.\n4. Use `inspect_affinity_item` with `kind: "owner_profile"` to confirm that the owner profile exists and reads back cleanly.\n5. Use `review_affinity` with `view: "contacts.list"` to confirm that the portfolio is still minimal and no duplicate owner-like records were created during setup.\n\nValidation checks:\n- The owner profile loads successfully.\n- The owner has at least one live identity.\n- The contact list shows exactly the records you expect at this early stage.\n\nPitfalls:\n- Do not create a non-owner placeholder and plan to convert it later if the harness can bootstrap the real owner immediately.\n- Do not skip identity attachment, or later natural-key lookup will be weaker and duplicate-prone.\n- Do not seed relationships before the owner exists if later evidence is meant to be owner-facing.\n\nTips and tricks:\n- Prefer an identity that is stable across sessions, such as a primary email or canonical profile URL.\n- Keep the first dataset tiny. Bootstrap is for a trustworthy base, not a full import.\n- If setup feels ambiguous, inspect first and add less rather than more.\n\nTool calls to prefer:\n- `manage_contact` for owner creation\n- `manage_identity` for first routing key\n- `inspect_affinity_item` for confirmation\n- `review_affinity` for early portfolio sanity checks\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `import-history-without-faking-evidence`' }, "identify-and-locate-contacts": { "description": "Find the right contact reliably, prefer stable natural keys when possible, and avoid creating duplicates through weak lookup discipline.", "content": "# Identify And Locate Contacts\n\nPrimary tools:\n- `search_affinity`\n- `inspect_affinity_item`\n- `manage_identity`\n- `manage_contact`\n\nGoal:\n- Resolve the right contact before mutating anything, using the strongest available locator.\n\nWhen to use:\n- A harness receives a name, email, phone number, URL, or ambiguous reference to a person or organization.\n- You need to confirm whether a contact already exists before creating a new one.\n\nWhen not to use:\n- You already have one exact internal id from a prior successful tool result.\n- You are reviewing a list surface rather than identifying one exact target.\n\nStep-by-step sequence:\n1. Start with `search_affinity` whenever the target might already exist.\n2. If you have a strong natural key such as email, phone, or URL-like handle, use that query first rather than a vague display name.\n3. If search returns one strong candidate, use `inspect_affinity_item` to confirm the profile before mutating.\n4. If the target does not exist, create it with `manage_contact`.\n5. Immediately add the best routing key with `manage_identity` so future lookup becomes exact rather than fuzzy.\n6. If the system returns ambiguity, stop and clarify instead of guessing.\n\nValidation checks:\n- The inspected profile matches the intended person or organization.\n- The best known routing identity is attached and, when appropriate, verified.\n- Later searches by that identity return the same contact cleanly.\n\nPitfalls:\n- Do not trust names alone when a natural key is available.\n- Do not create a second contact just because search returned multiple plausible matches.\n- Do not remove or revise identities casually; they are your best future locator surface.\n\nTips and tricks:\n- Prefer exact identities over integer ids when the harness needs to survive across sessions or partial memory loss.\n- After creating a contact from a vague mention, upgrade it with a real identity as soon as possible.\n- If multiple weak matches appear, inspect before mutating.\n\nTool calls to prefer:\n- `search_affinity` for candidate discovery\n- `inspect_affinity_item` for exact confirmation\n- `manage_identity` for routing-key upgrades\n- `manage_contact` only after lookup fails honestly\n\nRelated skills:\n- `bootstrap-and-cold-start`\n- `reconcile-duplicates-and-merge-safely`" }, "import-history-without-faking-evidence": { "description": "Import pre-existing relationship state honestly, using seeded links for known history instead of inventing fake journal evidence.", "content": '# Import History Without Faking Evidence\n\nPrimary tools:\n- `manage_contact`\n- `manage_identity`\n- `manage_relationship`\n- `inspect_affinity_item`\n\nGoal:\n- Represent real pre-system relationship state without fabricating a timeline of events that never existed inside Affinity.\n\nWhen to use:\n- You are migrating from another CRM, notes system, or memory set.\n- You already know a relationship exists and roughly how strong or active it is, but you do not want to reconstruct every past interaction.\n\nWhen not to use:\n- A new event just happened and should affect mechanics now.\n- You are recording a specific conversation, milestone, transaction, or observation.\n\nStep-by-step sequence:\n1. Create or locate the relevant contacts first.\n2. Add the strongest identities you know so the imported graph stays addressable later.\n3. Use `manage_relationship` with `action: "seed_social_link"` to establish a relational link that already existed before the system.\n4. Set only the fields you can justify: kind, rough rank, trust, cadence, state, and optional bond.\n5. Inspect the result with `inspect_affinity_item` if you need to verify the exact post-import state.\n6. From that point forward, switch to evidence tools for all new real-world activity.\n\nValidation checks:\n- The imported contact pair now has the expected relational link.\n- The seeded state looks plausible when inspected.\n- Later direct events continue progression from the seeded baseline rather than from zero.\n\nPitfalls:\n- Do not backfill dozens of fake conversations just to reach a desired rank.\n- Do not use import seeding for ordinary day-to-day relationship maintenance.\n- Do not overstate confidence on rank or trust when the historical truth is actually vague.\n\nTips and tricks:\n- Seed conservatively if you are unsure. It is safer to underclaim and let real future evidence move the link.\n- Use bond text sparingly as a narrative reminder of why the imported state is what it is.\n- Prefer one honest seed over many synthetic event reconstructions.\n\nTool calls to prefer:\n- `manage_relationship` with `seed_social_link`\n- `manage_contact` and `manage_identity` before seeding\n- `inspect_affinity_item` for post-import verification\n\nRelated skills:\n- `bootstrap-and-cold-start`\n- `record-direct-evidence-well`' }, "model-structure-orgs-and-households": { "description": "Model factual structure such as employment, membership, hierarchy, and household ties without confusing it with live relational mechanics.", "content": '# Model Structure, Orgs, And Households\n\nPrimary tools:\n- `manage_contact`\n- `manage_relationship`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Capture the stable factual skeleton of the world: who belongs where, who reports to whom, and what entities are structurally connected.\n\nWhen to use:\n- You need to represent employment, family structure, team membership, households, or service affiliation.\n- The fact should exist even if no emotional or operational relationship score is implied.\n\nWhen not to use:\n- You want to express how strong, strained, warm, or active a relationship feels.\n- You are recording something that happened in time.\n\nStep-by-step sequence:\n1. Create or locate the entities involved.\n2. Decide whether the fact is structural rather than relational. If the answer survives even with zero interaction history, it is usually structural.\n3. Use `manage_relationship` with `action: "set_structural_tie"` to record the factual tie.\n4. Include a role when the role clarifies the structure, such as job title or household role.\n5. Inspect relevant profiles or review lists after large structural batches to confirm the world model is clean.\n6. Remove or replace outdated ties when the world changes, such as a job move or team change.\n\nValidation checks:\n- The structural tie is visible and stable.\n- No fake progression fields were introduced just to represent a factual tie.\n- The chart and review surfaces now reflect the world structure you expect.\n\nPitfalls:\n- Do not use structural ties to stand in for closeness, trust, or cadence.\n- Do not seed social links just because a structural tie exists; only do that if a live relational track also matters.\n- Do not leave outdated structural ties hanging after the factual world changes.\n\nTips and tricks:\n- Structural and relational links can coexist. Use both when both truths matter.\n- Groups, households, companies, teams, and services often deserve their own contacts.\n- If you are unsure, ask whether the statement is about belonging or about lived relationship quality.\n\nTool calls to prefer:\n- `manage_relationship` with `set_structural_tie`\n- `inspect_affinity_item` to verify exact targets\n- `review_affinity` for broader structural review\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `record-direct-evidence-well`' }, "record-direct-evidence-well": { "description": "Record direct owner-facing social evidence in a way that produces truthful mechanics, good moments, and useful future review surfaces.", "content": "# Record Direct Evidence Well\n\nPrimary tools:\n- `record_event`\n- `inspect_affinity_item`\n- `review_affinity`\n- `manage_relationship`\n\nGoal:\n- Capture what really happened between the owner and other contacts so Affinity can derive progression, trust, cadence, and moments cleanly.\n\nWhen to use:\n- A conversation, activity, support exchange, conflict, correction, milestone, or direct operational interaction just happened.\n- You want future radar, progression, journal, and chart views to reflect the event truthfully.\n\nWhen not to use:\n- You are importing pre-system history.\n- You only observed third parties and were not directly part of the relationship evidence.\n\nStep-by-step sequence:\n1. Confirm the participants first. Use exact contacts whenever possible.\n2. Use `record_event` with the smallest honest event family: `interaction` for ordinary direct evidence, `milestone` when the event is truly milestone-shaped, or another event family only when it better matches reality.\n3. Set significance conservatively. Use high significance only when the event genuinely changed the relationship.\n4. Let the system auto-create owner-facing links when appropriate instead of manually seeding a link just because a direct event happened.\n5. Review the receipt or inspect the resulting profile when you need to understand the effect.\n6. Use `review_affinity` on journal, timeline, moments, or readiness views when you need context after a sequence of events.\n\nValidation checks:\n- The right contacts were involved.\n- The event family matches what really happened.\n- The resulting mechanics feel plausible when reviewed.\n\nPitfalls:\n- Do not inflate significance to force a rank-up.\n- Do not turn ordinary direct evidence into a milestone just to get a dramatic moment.\n- Do not use bond text as evidence. If the bond changed but no event happened, revise the bond separately.\n- Do not guess through ambiguity; clarify the target first.\n\nTips and tricks:\n- Conflict and correction are both first-class direct evidence. Use them honestly rather than hiding them inside generic summaries.\n- Multi-participant owner events can refresh multiple links at once. That is a feature, not a bug.\n- If you need to label the current narrative reading without changing mechanics, use `manage_relationship` with `revise_bond` after the evidence is recorded.\n\nTool calls to prefer:\n- `record_event` for the evidence itself\n- `inspect_affinity_item` for exact read-back\n- `review_affinity` for journals, timelines, moments, and readiness\n- `manage_relationship` only for bond interpretation after the fact\n\nRelated skills:\n- `record-observations-and-referrals`\n- `review-radar-progression-and-graph`" }, "record-observations-and-referrals": { "description": "Capture third-party and referral truth without overclaiming intimacy, directness, or trust that the operator did not actually earn.", "content": '# Record Observations And Referrals\n\nPrimary tools:\n- `record_event`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Add truthful third-party graph information while keeping observational evidence weaker than direct relationship evidence.\n\nWhen to use:\n- You learned that two people know each other, one person referred another, or you observed a third-party relationship pattern.\n- You want the graph to reflect world truth without pretending you had the direct interaction yourself.\n\nWhen not to use:\n- The owner directly participated in the event and should record direct evidence instead.\n- You merely suspect a relationship with no strong enough fact to justify recording it.\n\nStep-by-step sequence:\n1. Confirm the third-party contacts you are referring to.\n2. Use `record_event` with `kind: "observation"` when the truth is observational rather than direct.\n3. Keep significance modest unless the observed fact is genuinely consequential.\n4. After recording, use `review_affinity` to inspect observed-link or graph views and verify the resulting topology.\n5. If later direct evidence exists, record that separately rather than expecting the observation to do all the work.\n\nValidation checks:\n- The graph now reflects the third-party fact you meant to capture.\n- Observed links stay appropriately weaker than direct links.\n- No accidental owner-facing intimacy was invented.\n\nPitfalls:\n- Do not use observations to fake deep trust or rank.\n- Do not create a clique from vague group knowledge when the system only has justified pairwise facts.\n- Do not promote a rumor to structure or direct evidence without better confirmation.\n\nTips and tricks:\n- Referral chains are excellent observational facts because they help later review and graph interpretation.\n- Use chart and observed-link review views together after batches of observation work.\n- Observations are often better than silence, but they should remain humble.\n\nTool calls to prefer:\n- `record_event` with `observation`\n- `review_affinity` with observed-link and graph views\n- `inspect_affinity_item` if you need exact follow-up on one target\n\nRelated skills:\n- `record-direct-evidence-well`\n- `review-radar-progression-and-graph`' }, "capture-transactions-and-commercial-events": { "description": "Capture commercially meaningful interactions as transactions so operational and service relationships accumulate the right evidence shape.", "content": '# Capture Transactions And Commercial Events\n\nPrimary tools:\n- `record_event`\n- `inspect_affinity_item`\n- `review_affinity`\n\nGoal:\n- Represent deals, placements, closings, sponsorships, and operationally meaningful exchanges as transactions instead of flattening them into generic social notes.\n\nWhen to use:\n- Money, deliverables, placements, closings, or other commercially meaningful exchanges just occurred.\n- The relationship is service-oriented, professional, or business-critical.\n\nWhen not to use:\n- The main truth is a personal conversation or ordinary activity.\n- The important action is really a promise or agreement that still needs later resolution.\n\nStep-by-step sequence:\n1. Confirm the right counterparties first.\n2. Use `record_event` with `kind: "transaction"` for the commercial event itself.\n3. Keep the summary concrete enough that future review explains what was exchanged and why it mattered.\n4. Use significance according to the real business weight, not just enthusiasm.\n5. Review the affected relationship if the commercial event should meaningfully change trust, cadence, or graph visibility.\n\nValidation checks:\n- The transaction now appears in the relevant timeline or journal views.\n- The resulting relationship effect matches the commercial importance of the event.\n- Later maintenance views now have the right recency signal.\n\nPitfalls:\n- Do not bury closings, placements, or sponsorship deliveries inside generic conversations.\n- Do not record a transaction when the truth is still only a pending agreement.\n- Do not expect transactions to create the same kind of affinity movement as rich direct social evidence.\n\nTips and tricks:\n- A relationship can be commercially strong and emotionally shallow. That is normal.\n- Use transactions to preserve crisp operational history for later portfolio review.\n- Follow major transactions with review views to confirm the graph and maintenance surfaces updated as expected.\n\nTool calls to prefer:\n- `record_event` with `transaction`\n- `inspect_affinity_item` for exact relationship read-back\n- `review_affinity` for owner-link portfolio review\n\nRelated skills:\n- `manage-promises-and-agreements`\n- `review-radar-progression-and-graph`' }, "manage-promises-and-agreements": { "description": "Track obligations explicitly from creation to resolution so open commitments remain actionable and trust consequences stay honest.", "content": '# Manage Promises And Agreements\n\nPrimary tools:\n- `manage_commitment`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Keep promises and agreements visible, resolvable, and mechanically meaningful instead of letting them disappear into narrative history.\n\nWhen to use:\n- Someone committed to doing something later.\n- A promise, agreement, or operational obligation needs follow-through.\n\nWhen not to use:\n- The event is complete already and no unresolved obligation exists.\n- A date is recurring and belongs in the date-anchor surface instead.\n\nStep-by-step sequence:\n1. When the commitment is made, use `manage_commitment` with `action: "record"`.\n2. Choose `promise` vs `agreement` honestly rather than treating them as stylistic synonyms.\n3. Include a due time when the obligation has a meaningful time boundary.\n4. Use `review_affinity` with the open-commitment view during regular review cycles.\n5. When the outcome is known, resolve it with `manage_commitment` as `kept`, `cancelled`, or `broken`.\n6. Inspect the relevant profile or review the relevant queue again if you need to confirm closure.\n\nValidation checks:\n- Open commitments appear when they should and disappear when resolved.\n- Resolution state matches real-world truth.\n- Trust effects feel consistent with whether the promise was kept or broken.\n\nPitfalls:\n- Do not hide real obligations inside ordinary event summaries.\n- Do not resolve a commitment optimistically before the outcome is actually known.\n- Do not misuse `cancelled` when the truth is actually `broken`.\n\nTips and tricks:\n- If an obligation matters enough to disappoint someone when missed, record it.\n- Review open commitments regularly; the value of the system drops fast if this queue goes stale.\n- When a commitment resolution also deserves narrative context, pair the resolution with an appropriate event in the surrounding workflow.\n\nTool calls to prefer:\n- `manage_commitment` for record and resolve\n- `review_affinity` for the open-commitment queue\n- `inspect_affinity_item` when you need exact context on the affected relationship\n\nRelated skills:\n- `capture-transactions-and-commercial-events`\n- `manage-recurring-dates-and-reminders`' }, "manage-recurring-dates-and-reminders": { "description": "Use date anchors for recurring salience so important birthdays, anniversaries, renewals, and memorials feed review surfaces cleanly.", "content": '# Manage Recurring Dates And Reminders\n\nPrimary tools:\n- `manage_date_anchor`\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Materialize recurring important dates into a durable reminder surface that later review and relationship maintenance can trust.\n\nWhen to use:\n- A date recurs yearly and matters to a contact or link.\n- You want reminders for birthdays, anniversaries, renewals, memorials, or other yearly anchors.\n\nWhen not to use:\n- The date is a one-off event.\n- The fact is a preference or metadata note rather than a recurring anchor.\n\nStep-by-step sequence:\n1. Confirm the target contact or link first.\n2. Use `manage_date_anchor` with `action: "add"` and the right recurrence kind.\n3. Keep the summary operator-friendly so future reminders are immediately understandable.\n4. Use `review_affinity` with the upcoming-dates view and an explicit horizon during regular review work.\n5. Revise anchors when recurrence details change.\n6. Remove anchors when the reminder surface should no longer treat the date as active.\n\nValidation checks:\n- The anchor appears in upcoming-date review when it falls within the chosen horizon.\n- Duplicate anchors are not silently piling up.\n- The summary and significance are still meaningful for later action.\n\nPitfalls:\n- Do not store important recurring dates only as loose attributes.\n- Do not create duplicate yearly anchors for the same target and meaning.\n- Do not use recurring anchors for commitments; those belong in the commitment flow.\n\nTips and tricks:\n- Tune horizons by workflow: short for weekly review, longer for quarterly planning.\n- Use significance to help order upcoming-date review meaningfully.\n- Link-level anchors are valuable when the date belongs to a relationship rather than one person.\n\nTool calls to prefer:\n- `manage_date_anchor` for add, revise, and remove\n- `review_affinity` with the upcoming-dates view\n- `inspect_affinity_item` when you need exact context on the target\n\nRelated skills:\n- `manage-promises-and-agreements`\n- `review-radar-progression-and-graph`' }, "review-radar-progression-and-graph": { "description": "Use the review surfaces to prioritize maintenance, understand progression readiness, and read the shape of the relationship graph without guessing from raw rows.", "content": "# Review Radar, Progression, And Graph\n\nPrimary tools:\n- `review_affinity`\n- `inspect_affinity_item`\n\nGoal:\n- Turn review surfaces into a disciplined operating loop for outreach, prioritization, and graph understanding.\n\nWhen to use:\n- You need a daily or weekly maintenance pass.\n- You want to know who is drifting, who is ready for deeper investment, or how the network is structured.\n\nWhen not to use:\n- You already know the one exact item you need to inspect in depth.\n- You are mutating state and only need confirmation of a single write result.\n\nStep-by-step sequence:\n1. Start with `review_affinity` on the highest-level view that matches the question: radar, owner links, progression readiness, graph, moments, or a timeline view.\n2. Use radar when the question is who needs attention now.\n3. Use progression readiness when the question is where additional positive investment is most likely to deepen a relationship.\n4. Use graph review when the question is network shape, bridges, clusters, or hidden centrality.\n5. Use journal or timeline views when a score needs narrative context before action.\n6. Escalate to `inspect_affinity_item` only after the review surface identifies the exact target worth opening deeply.\n\nValidation checks:\n- The chosen review surface actually matches the decision you are trying to make.\n- You inspected context before acting on a surprising score.\n- Follow-up work targets the right contact or link rather than the loudest metric.\n\nPitfalls:\n- Do not treat radar as a command to act blindly without context.\n- Do not confuse progression readiness with relationship importance; they answer different questions.\n- Do not rely on one score when a timeline or graph view would explain the situation better.\n\nTips and tricks:\n- Review flows are strongest when used rhythmically rather than sporadically.\n- Use graph review to understand bridges and clusters, then use timeline or profile inspection for the exact story behind them.\n- Empty results can still be informative: they may confirm that maintenance debt is currently low.\n\nTool calls to prefer:\n- `review_affinity` for all dashboard-style surfaces\n- `inspect_affinity_item` for exact deep follow-up\n\nRelated skills:\n- `record-direct-evidence-well`\n- `manage-recurring-dates-and-reminders`\n- `reconcile-duplicates-and-merge-safely`" }, "reconcile-duplicates-and-merge-safely": { "description": "Review duplicate candidates carefully and merge only after the identity, history, and structural consequences are understood.", "content": "# Reconcile Duplicates And Merge Safely\n\nPrimary tools:\n- `review_affinity`\n- `inspect_affinity_item`\n- `merge_contacts`\n\nGoal:\n- Collapse true duplicates without losing lineage, identities, participants, or relationship truth.\n\nWhen to use:\n- Duplicate-candidate review surfaces indicate a likely duplicate.\n- A harness or operator has strong evidence that two contacts are the same real entity.\n\nWhen not to use:\n- The match is still speculative and unresolved.\n- The real need is just better identities or labels rather than an actual merge.\n\nStep-by-step sequence:\n1. Start with `review_affinity` on the duplicate-candidate view.\n2. Inspect both candidate contacts before merging.\n3. Confirm the winner explicitly rather than letting recency or convenience decide by accident.\n4. Use `merge_contacts` only when the identity and history picture is clear.\n5. Inspect the surviving contact after the merge to confirm the merged state looks right.\n6. Review merge history when you need lineage confirmation later.\n\nValidation checks:\n- The surviving contact now contains the expected identities and visible history.\n- The loser is no longer treated as a live normal contact.\n- The merge reason is still understandable when reviewed later.\n\nPitfalls:\n- Do not merge on a weak fuzzy-name match alone.\n- Do not skip inspection of both sides before merging.\n- Do not assume merges are easy to conceptually undo just because the system preserves lineage.\n\nTips and tricks:\n- Prefer a winner that already carries the stronger routing identity and richer history unless another reason dominates.\n- If the system is uncertain, clarify first instead of forcing a merge.\n- Review merge history after sensitive merges to confirm the lineage chain is clean.\n\nTool calls to prefer:\n- `review_affinity` for duplicate-candidate discovery and merge-history review\n- `inspect_affinity_item` for both candidate contacts\n- `merge_contacts` only after confirmation\n\nRelated skills:\n- `identify-and-locate-contacts`\n- `import-history-without-faking-evidence`" } };
|
|
10669
|
+
}
|
|
10670
|
+
});
|
|
10671
|
+
|
|
9253
10672
|
// src/skills/skill_types.ts
|
|
9254
10673
|
function defineAffinitySkill(skill) {
|
|
9255
10674
|
return skill;
|
|
@@ -10000,22 +11419,76 @@ var init_soul = __esm({
|
|
|
10000
11419
|
});
|
|
10001
11420
|
|
|
10002
11421
|
// src/surface/affinity_skill_spec.ts
|
|
10003
|
-
var
|
|
10004
|
-
__export(affinity_skill_spec_exports, {
|
|
10005
|
-
affinitySkillSpec: () => affinitySkillSpec
|
|
10006
|
-
});
|
|
10007
|
-
var affinitySkillSpec;
|
|
11422
|
+
var affinityCliExamples, affinityCheatSheet, affinitySkillSpec;
|
|
10008
11423
|
var init_affinity_skill_spec = __esm({
|
|
10009
11424
|
"src/surface/affinity_skill_spec.ts"() {
|
|
10010
11425
|
init_skill_registry();
|
|
10011
11426
|
init_soul();
|
|
10012
11427
|
init_affinity_mcp_instructions();
|
|
10013
11428
|
init_affinity_surface_tools();
|
|
11429
|
+
affinityCliExamples = {
|
|
11430
|
+
search_affinity: 'ghostpaw-affinity search "Jana"',
|
|
11431
|
+
review_affinity: `ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}'`,
|
|
11432
|
+
inspect_affinity_item: `ghostpaw-affinity show contact_profile --contact '{"contactId":2}'`,
|
|
11433
|
+
manage_contact: `ghostpaw-affinity contact create '{"name":"Fox","kind":"human","bootstrapOwner":true}'`,
|
|
11434
|
+
merge_contacts: `ghostpaw-affinity contact merge --winner '{"contactId":2}' --loser '{"contactId":8}' --reason-summary "same person"`,
|
|
11435
|
+
manage_duplicate_dismissal: `ghostpaw-affinity duplicate dismiss --left '{"contactId":5}' --right '{"contactId":10}' --reason '"different people"'`,
|
|
11436
|
+
manage_identity: `ghostpaw-affinity identity add --contact '{"contactId":2}' --input '{"type":"email","value":"jana@x.de"}'`,
|
|
11437
|
+
manage_relationship: `ghostpaw-affinity relationship tie --from '{"contactId":2}' --to '{"contactId":3}' --kind works_at --role Founder`,
|
|
11438
|
+
record_event: `ghostpaw-affinity event record --kind interaction --input '{"type":"conversation","occurredAt":"today","summary":"Call re pilot","significance":6,"participants":[{"contactId":2,"role":"recipient","directionality":"mutual"}]}'`,
|
|
11439
|
+
manage_commitment: `ghostpaw-affinity commitment record --input '{"commitmentType":"promise","occurredAt":"today","dueAt":"+3d","summary":"Send proposal","significance":6,"participants":[{"contactId":2,"role":"recipient"}]}'`,
|
|
11440
|
+
manage_date_anchor: `ghostpaw-affinity date add --input '{"target":{"kind":"contact","contact":{"contactId":2}},"recurrenceKind":"birthday","anchorMonth":11,"anchorDay":3,"summary":"Jana bday","significance":4}'`,
|
|
11441
|
+
manage_attribute: `ghostpaw-affinity attribute set --target '{"kind":"contact","contact":{"contactId":2}}' --name pipeline_stage --value '"proposal"'`
|
|
11442
|
+
};
|
|
11443
|
+
affinityCheatSheet = `## CLI cheat sheet
|
|
11444
|
+
|
|
11445
|
+
Install: \`curl -fsSL https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.sh | sh\`
|
|
11446
|
+
Windows: \`irm https://raw.githubusercontent.com/GhostPawJS/affinity/main/install.ps1 | iex\`
|
|
11447
|
+
|
|
11448
|
+
\`\`\`sh
|
|
11449
|
+
export GHOSTPAW_AFFINITY_DB=/abs/path/affinity.db
|
|
11450
|
+
# Or --db; default ~/.ghostpaw/affinity.db. Piped output is JSON; --compact omits empty receipt arrays.
|
|
11451
|
+
# Exit: 0 ok/no_op, 1 tool error, 2 usage/invalid input, 3 not found/clarification, 4 conflict, 70 internal.
|
|
11452
|
+
# Dates: epoch ms, ISO with offset/Z, YYYY-MM-DD, DD.MM.YYYY, today, tomorrow, +3d, -2d.
|
|
11453
|
+
# Date-only occurredAt uses local midnight; dueAt uses local end of day.
|
|
11454
|
+
ghostpaw-affinity contact create '{"name":"Fox","kind":"human","bootstrapOwner":true}'
|
|
11455
|
+
ghostpaw-affinity search "Jana" # Search names, identities, tags, attributes, roles and events before creating.
|
|
11456
|
+
ghostpaw-affinity contact create '{"name":"Jana Petersen","kind":"human"}'
|
|
11457
|
+
ghostpaw-affinity identity add --contact '{"contactId":2}' --input '{"type":"email","value":"jana@x.de"}'
|
|
11458
|
+
ghostpaw-affinity relationship tie --from '{"contactId":2}' --to '{"contactId":3}' --kind works_at --role Founder
|
|
11459
|
+
ghostpaw-affinity relationship add --from '{"contactId":1}' --to '{"contactId":2}' --kind professional --role lead
|
|
11460
|
+
ghostpaw-affinity event record --kind interaction --input '{"type":"conversation","occurredAt":"today","summary":"Call re pilot","significance":6,"participants":[{"contactId":2,"role":"recipient","directionality":"mutual"}]}'
|
|
11461
|
+
ghostpaw-affinity commitment record --input '{"commitmentType":"promise","occurredAt":"today","dueAt":"+3d","summary":"Send proposal","significance":6,"participants":[{"contactId":2,"role":"recipient"}]}'
|
|
11462
|
+
ghostpaw-affinity commitment resolve --commitment-event-id 6 --resolution kept
|
|
11463
|
+
ghostpaw-affinity date add --input '{"target":{"kind":"contact","contact":{"contactId":2}},"recurrenceKind":"birthday","anchorMonth":11,"anchorDay":3,"summary":"Jana bday","significance":4}'
|
|
11464
|
+
ghostpaw-affinity attribute set --target '{"kind":"contact","contact":{"contactId":2}}' --name pipeline_stage --value '"proposal"'
|
|
11465
|
+
ghostpaw-affinity attribute set --target '{"kind":"contact","contact":{"contactId":2}}' --name tag:gastro-radar --value null
|
|
11466
|
+
ghostpaw-affinity show contact_profile --contact '{"contactId":2}'
|
|
11467
|
+
ghostpaw-affinity review events.contact_journal --contact '{"contactId":2}'
|
|
11468
|
+
ghostpaw-affinity review links.radar
|
|
11469
|
+
ghostpaw-affinity review commitments.open
|
|
11470
|
+
ghostpaw-affinity review dates.upcoming --horizon-days 14
|
|
11471
|
+
ghostpaw-affinity review contacts.list --tag gastro-radar
|
|
11472
|
+
ghostpaw-affinity review contacts.list --attribute pipeline_stage=proposal
|
|
11473
|
+
ghostpaw-affinity review contacts.duplicates
|
|
11474
|
+
ghostpaw-affinity contact merge --winner '{"contactId":2}' --loser '{"contactId":8}' --reason-summary "same person"
|
|
11475
|
+
ghostpaw-affinity duplicate dismiss --left '{"contactId":5}' --right '{"contactId":10}' --reason '"different people"'
|
|
11476
|
+
ghostpaw-affinity contact revise '{"contactId":3}' --patch '{"name":"New Name"}'
|
|
11477
|
+
ghostpaw-affinity contact lifecycle '{"contactId":9}' dormant
|
|
11478
|
+
ghostpaw-affinity relationship state --link '{"linkId":7}' --state dormant
|
|
11479
|
+
ghostpaw-affinity contact export --contact '{"contactId":2}'
|
|
11480
|
+
ghostpaw-affinity contact delete --contact '{"contactId":2}' --confirm
|
|
11481
|
+
\`\`\`
|
|
11482
|
+
|
|
11483
|
+
For bots and agents: run \`ghostpaw-affinity mcp\` for MCP stdio, or use this CLI with \`--json\`. Read a guide with \`ghostpaw-affinity skill <guide>\`.
|
|
11484
|
+
`;
|
|
10014
11485
|
affinitySkillSpec = {
|
|
11486
|
+
cheatSheet: affinityCheatSheet,
|
|
11487
|
+
examples: affinityCliExamples,
|
|
10015
11488
|
name: "affinity",
|
|
10016
11489
|
description: "Search and maintain local contacts, identities, relationships, events, commitments, and recurring dates. Use when recording relationship evidence or reviewing a social CRM.",
|
|
10017
11490
|
license: "MIT",
|
|
10018
|
-
compatibility: "
|
|
11491
|
+
compatibility: "The release binary needs no runtime. MCP stdio or ghostpaw-affinity CLI.",
|
|
10019
11492
|
bin: "ghostpaw-affinity",
|
|
10020
11493
|
packageName: "@ghostpaw/affinity",
|
|
10021
11494
|
server: "ghostpaw-affinity",
|
|
@@ -10027,22 +11500,98 @@ var init_affinity_skill_spec = __esm({
|
|
|
10027
11500
|
}
|
|
10028
11501
|
});
|
|
10029
11502
|
|
|
11503
|
+
// src/surface/affinity_skill_command.ts
|
|
11504
|
+
var affinity_skill_command_exports = {};
|
|
11505
|
+
__export(affinity_skill_command_exports, {
|
|
11506
|
+
affinitySkillCommand: () => affinitySkillCommand
|
|
11507
|
+
});
|
|
11508
|
+
function affinitySkillCommand(args) {
|
|
11509
|
+
if (args.includes("--help") || args.includes("-h"))
|
|
11510
|
+
return "Usage: ghostpaw-affinity skill [<guide>|--list|--help]\n";
|
|
11511
|
+
if (args.length > 1) throw new CliUsageError("Expected one skill guide");
|
|
11512
|
+
const guide = args[0];
|
|
11513
|
+
if (guide === "--list")
|
|
11514
|
+
return `${Object.entries(affinitySkillReferences).map(([name, entry]) => `${name} ${entry.description}`).join("\n")}
|
|
11515
|
+
`;
|
|
11516
|
+
if (!guide) return renderSkillMarkdown(affinitySkillSpec);
|
|
11517
|
+
const found = affinitySkillReferences[guide];
|
|
11518
|
+
if (found) return found.content;
|
|
11519
|
+
const suggestion = suggestCommand(
|
|
11520
|
+
guide,
|
|
11521
|
+
Object.keys(affinitySkillReferences)
|
|
11522
|
+
);
|
|
11523
|
+
throw new CliUsageError(
|
|
11524
|
+
`Unknown guide: ${guide}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`
|
|
11525
|
+
);
|
|
11526
|
+
}
|
|
11527
|
+
var init_affinity_skill_command = __esm({
|
|
11528
|
+
"src/surface/affinity_skill_command.ts"() {
|
|
11529
|
+
init_cli_args();
|
|
11530
|
+
init_cli_output();
|
|
11531
|
+
init_skill_markdown();
|
|
11532
|
+
init_affinity_skill_references();
|
|
11533
|
+
init_affinity_skill_spec();
|
|
11534
|
+
}
|
|
11535
|
+
});
|
|
11536
|
+
|
|
10030
11537
|
// src/surface/affinity_cli_options.ts
|
|
10031
11538
|
init_version();
|
|
11539
|
+
|
|
11540
|
+
// src/surface/affinity_cli_errors.ts
|
|
11541
|
+
var commands = {
|
|
11542
|
+
mergeContacts: "ghostpaw-affinity contact merge"
|
|
11543
|
+
};
|
|
11544
|
+
function affinityCliError(message) {
|
|
11545
|
+
let result = message;
|
|
11546
|
+
for (const [source, command] of Object.entries(commands))
|
|
11547
|
+
result = result.replaceAll(`use ${source}`, `use: ${command}`);
|
|
11548
|
+
return result;
|
|
11549
|
+
}
|
|
11550
|
+
|
|
11551
|
+
// src/surface/affinity_cli_options.ts
|
|
10032
11552
|
init_affinity_surface_tools();
|
|
11553
|
+
var npmEntry = false;
|
|
11554
|
+
function markNpmEntry() {
|
|
11555
|
+
npmEntry = true;
|
|
11556
|
+
}
|
|
11557
|
+
function cliVersion(version, {
|
|
11558
|
+
sea = isSea,
|
|
11559
|
+
npm = npmEntry,
|
|
11560
|
+
platform = process.platform,
|
|
11561
|
+
arch = process.arch
|
|
11562
|
+
} = {}) {
|
|
11563
|
+
let binary = false;
|
|
11564
|
+
try {
|
|
11565
|
+
binary = sea();
|
|
11566
|
+
} catch {
|
|
11567
|
+
}
|
|
11568
|
+
const target = `${platform === "win32" ? "windows" : platform}-${arch}`;
|
|
11569
|
+
return `affinity ${version} (${binary ? `binary, ${target}` : npm ? "npm" : "library"})`;
|
|
11570
|
+
}
|
|
11571
|
+
|
|
11572
|
+
// src/surface/affinity_cli_options.ts
|
|
11573
|
+
init_plain_dates();
|
|
11574
|
+
init_resolve_affinity_db_path();
|
|
10033
11575
|
var affinityCliOptions = {
|
|
11576
|
+
reportCreation: true,
|
|
11577
|
+
transformInput: normalizeTimeInputs,
|
|
11578
|
+
versionLine: () => cliVersion(affinityVersion),
|
|
11579
|
+
describeDb: (db) => resolveAffinityDbPath(db === void 0 ? {} : { flag: db }),
|
|
11580
|
+
formatError: affinityCliError,
|
|
10034
11581
|
bin: "ghostpaw-affinity",
|
|
10035
11582
|
version: affinityVersion,
|
|
10036
11583
|
description: "Local relationship model and social CRM",
|
|
10037
11584
|
tools: affinityCliSurfaceTools,
|
|
10038
11585
|
openContext: async (options) => {
|
|
10039
11586
|
const { openAffinityDb: openAffinityDb2 } = await Promise.resolve().then(() => (init_open_affinity_db(), open_affinity_db_exports));
|
|
10040
|
-
const
|
|
10041
|
-
|
|
10042
|
-
resolveAffinityDbPath2(
|
|
10043
|
-
options.db === void 0 ? {} : { flag: options.db }
|
|
10044
|
-
)
|
|
11587
|
+
const path = resolveAffinityDbPath(
|
|
11588
|
+
options.db === void 0 ? {} : { flag: options.db }
|
|
10045
11589
|
);
|
|
11590
|
+
const created = path !== ":memory:" && !existsSync(path);
|
|
11591
|
+
const db = await openAffinityDb2(path);
|
|
11592
|
+
if (created) (options.stderr ?? process.stderr).write(`created ${path}
|
|
11593
|
+
`);
|
|
11594
|
+
return db;
|
|
10046
11595
|
},
|
|
10047
11596
|
closeContext: (db) => db.close(),
|
|
10048
11597
|
commands: {
|
|
@@ -10052,125 +11601,19 @@ var affinityCliOptions = {
|
|
|
10052
11601
|
options.db === void 0 ? {} : { dbPath: options.db }
|
|
10053
11602
|
);
|
|
10054
11603
|
},
|
|
10055
|
-
skill: async () => {
|
|
10056
|
-
const {
|
|
10057
|
-
|
|
10058
|
-
return renderSkillMarkdown2(affinitySkillSpec2);
|
|
11604
|
+
skill: async (args = []) => {
|
|
11605
|
+
const { affinitySkillCommand: affinitySkillCommand2 } = await Promise.resolve().then(() => (init_affinity_skill_command(), affinity_skill_command_exports));
|
|
11606
|
+
return affinitySkillCommand2(args);
|
|
10059
11607
|
}
|
|
10060
11608
|
}
|
|
10061
11609
|
};
|
|
10062
|
-
var CliUsageError = class extends Error {
|
|
10063
|
-
exitCode = 2;
|
|
10064
|
-
};
|
|
10065
|
-
var keyOf = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
10066
|
-
function parseCliArgs(argv, spec) {
|
|
10067
|
-
const values = {};
|
|
10068
|
-
const positions = [];
|
|
10069
|
-
const flags = spec.flags ?? [];
|
|
10070
|
-
const convert = (raw, arg) => {
|
|
10071
|
-
let value = raw;
|
|
10072
|
-
if (raw.startsWith("@")) {
|
|
10073
|
-
try {
|
|
10074
|
-
value = readFileSync(raw.slice(1), "utf8");
|
|
10075
|
-
} catch {
|
|
10076
|
-
throw new CliUsageError(`Cannot read value file: ${raw.slice(1)}`);
|
|
10077
|
-
}
|
|
10078
|
-
}
|
|
10079
|
-
let result = value;
|
|
10080
|
-
if (arg.type === "number" || arg.type === "integer") {
|
|
10081
|
-
result = Number(value);
|
|
10082
|
-
if (!value.trim() || !Number.isFinite(result) || arg.type === "integer" && !Number.isInteger(result))
|
|
10083
|
-
throw new CliUsageError(
|
|
10084
|
-
`Expected ${arg.type} for ${arg.name}: ${value}`
|
|
10085
|
-
);
|
|
10086
|
-
} else if (arg.type === "boolean") {
|
|
10087
|
-
if (!["true", "false"].includes(value))
|
|
10088
|
-
throw new CliUsageError(`Expected boolean for ${arg.name}`);
|
|
10089
|
-
result = value === "true";
|
|
10090
|
-
} else if (arg.type === "json") {
|
|
10091
|
-
try {
|
|
10092
|
-
result = JSON.parse(value);
|
|
10093
|
-
} catch {
|
|
10094
|
-
throw new CliUsageError(`Invalid JSON for ${arg.name}`);
|
|
10095
|
-
}
|
|
10096
|
-
}
|
|
10097
|
-
if (arg.enum && !arg.enum.includes(result))
|
|
10098
|
-
throw new CliUsageError(
|
|
10099
|
-
`${arg.name} must be one of: ${arg.enum.join(", ")}`
|
|
10100
|
-
);
|
|
10101
|
-
return result;
|
|
10102
|
-
};
|
|
10103
|
-
let ended = false;
|
|
10104
|
-
for (let i = 0; i < argv.length; i++) {
|
|
10105
|
-
const token = argv[i];
|
|
10106
|
-
if (!ended && token === "--") {
|
|
10107
|
-
ended = true;
|
|
10108
|
-
continue;
|
|
10109
|
-
}
|
|
10110
|
-
if (ended || token === "-" || !token.startsWith("-")) {
|
|
10111
|
-
positions.push(token);
|
|
10112
|
-
continue;
|
|
10113
|
-
}
|
|
10114
|
-
const [flag, inline] = token.split(/=(.*)/s);
|
|
10115
|
-
const negative = flag?.startsWith("--no-");
|
|
10116
|
-
const name = flag?.replace(/^--?/, "").replace(/^no-/, "");
|
|
10117
|
-
const arg = flags.find(
|
|
10118
|
-
(entry) => entry.name === name || keyOf(entry.name) === name || entry.short === name
|
|
10119
|
-
);
|
|
10120
|
-
if (!arg) {
|
|
10121
|
-
const prefix = (name ?? "").slice(0, 3);
|
|
10122
|
-
const suggestion = prefix ? flags.find((entry) => entry.name.startsWith(prefix)) : void 0;
|
|
10123
|
-
throw new CliUsageError(
|
|
10124
|
-
`Unknown flag: ${flag}${suggestion ? `; did you mean --${suggestion.name}?` : ""}`
|
|
10125
|
-
);
|
|
10126
|
-
}
|
|
10127
|
-
if (negative && arg.type !== "boolean")
|
|
10128
|
-
throw new CliUsageError(`--no-${arg.name} requires a boolean flag`);
|
|
10129
|
-
let raw = inline;
|
|
10130
|
-
if (arg.type === "boolean" && raw === void 0)
|
|
10131
|
-
raw = negative ? "false" : "true";
|
|
10132
|
-
if (raw === void 0) {
|
|
10133
|
-
const next = argv[i + 1];
|
|
10134
|
-
if (next === void 0 || /^--[a-z]/i.test(next))
|
|
10135
|
-
throw new CliUsageError(`Missing value: --${arg.name}`);
|
|
10136
|
-
raw = next;
|
|
10137
|
-
i++;
|
|
10138
|
-
}
|
|
10139
|
-
const key = keyOf(arg.name);
|
|
10140
|
-
if (arg.type === "array") {
|
|
10141
|
-
const item = convert(raw, { ...arg, type: arg.itemType ?? "string" });
|
|
10142
|
-
values[key] = [...values[key] ?? [], item];
|
|
10143
|
-
} else values[key] = convert(raw, arg);
|
|
10144
|
-
}
|
|
10145
|
-
let index = 0;
|
|
10146
|
-
for (const arg of spec.positionals ?? []) {
|
|
10147
|
-
if (index >= positions.length) continue;
|
|
10148
|
-
values[keyOf(arg.name)] = arg.variadic ? positions.slice(index).map(
|
|
10149
|
-
(value) => convert(value, { ...arg, type: arg.itemType ?? arg.type })
|
|
10150
|
-
) : convert(positions[index], arg);
|
|
10151
|
-
index = arg.variadic ? positions.length : index + 1;
|
|
10152
|
-
}
|
|
10153
|
-
if (index < positions.length)
|
|
10154
|
-
throw new CliUsageError(`Unexpected positional: ${positions[index]}`);
|
|
10155
|
-
for (const arg of [...flags, ...spec.positionals ?? []]) {
|
|
10156
|
-
if (arg.required && values[keyOf(arg.name)] === void 0)
|
|
10157
|
-
throw new CliUsageError(`Missing required argument: ${arg.name}`);
|
|
10158
|
-
}
|
|
10159
|
-
return values;
|
|
10160
|
-
}
|
|
10161
|
-
function renderCliHelp(spec) {
|
|
10162
|
-
const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""} ${arg.description ?? ""}`;
|
|
10163
|
-
return `Usage: ${spec.command} ${(spec.positionals ?? []).map((arg) => `<${arg.name}${arg.variadic ? "..." : ""}>`).join(" ")} [flags]
|
|
10164
|
-
${spec.description ?? ""}
|
|
10165
11610
|
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
Flags:
|
|
10169
|
-
${(spec.flags ?? []).map((arg) => ` --${arg.name}${arg.short ? `, -${arg.short}` : ""} ${describe(arg)}`).join("\n")}
|
|
10170
|
-
`;
|
|
10171
|
-
}
|
|
11611
|
+
// src/surfaces/cli.ts
|
|
11612
|
+
init_cli_args();
|
|
10172
11613
|
|
|
10173
11614
|
// src/surfaces/cli_commands.ts
|
|
11615
|
+
init_cli_args();
|
|
11616
|
+
init_cli_output();
|
|
10174
11617
|
init_validate_input();
|
|
10175
11618
|
function cliCommands(tools) {
|
|
10176
11619
|
return tools.flatMap((tool) => {
|
|
@@ -10242,14 +11685,16 @@ async function runCliCommand(argv, options) {
|
|
|
10242
11685
|
else if (flag === "--compact") compact = true;
|
|
10243
11686
|
else if (flag === "--help" || flag === "-h") help = true;
|
|
10244
11687
|
else if (["--version", "-V", "-v"].includes(flag ?? "")) {
|
|
10245
|
-
if (!quiet)
|
|
11688
|
+
if (!quiet)
|
|
11689
|
+
stdout.write(`${options.versionLine?.() ?? options.version}
|
|
10246
11690
|
`);
|
|
10247
11691
|
return 0;
|
|
10248
11692
|
} else if (flag !== "--no-color") continue;
|
|
10249
11693
|
args.splice(i--, 1);
|
|
10250
11694
|
}
|
|
10251
11695
|
if (args[0] === "version") {
|
|
10252
|
-
if (!quiet)
|
|
11696
|
+
if (!quiet)
|
|
11697
|
+
stdout.write(`${options.versionLine?.() ?? options.version}
|
|
10253
11698
|
`);
|
|
10254
11699
|
return 0;
|
|
10255
11700
|
}
|
|
@@ -10264,10 +11709,12 @@ async function runCliCommand(argv, options) {
|
|
|
10264
11709
|
const specFor = (command) => {
|
|
10265
11710
|
const argument = (name, schema) => ({
|
|
10266
11711
|
name,
|
|
10267
|
-
type: schema.type === "object" || !schema.type ? "json" : schema.type,
|
|
11712
|
+
type: options.transformInput && /(?:At|-at)$|^(asOf|as-of|now)$/.test(name) ? "string" : schema.type === "object" || !schema.type && !schema.oneOf?.some((branch) => branch.type === "string") ? "json" : schema.type ?? "string",
|
|
10268
11713
|
description: schema.description,
|
|
10269
11714
|
enum: schema.enum,
|
|
10270
|
-
...schema.items?.type ? {
|
|
11715
|
+
...schema.items?.type ? {
|
|
11716
|
+
itemType: schema.items.type === "object" || schema.items.type === "array" ? "json" : schema.items.type
|
|
11717
|
+
} : {}
|
|
10271
11718
|
});
|
|
10272
11719
|
const properties = command.tool.inputSchema.properties ?? {};
|
|
10273
11720
|
return {
|
|
@@ -10298,7 +11745,7 @@ async function runCliCommand(argv, options) {
|
|
|
10298
11745
|
throw new CliUsageError(`Unknown command: ${args.join(" ")}`);
|
|
10299
11746
|
if (!quiet)
|
|
10300
11747
|
stdout.write(
|
|
10301
|
-
entry ? renderCliHelp(specFor(entry)) : `${options.bin} ${options.version}
|
|
11748
|
+
entry ? renderCliHelp(specFor(entry)) : `${options.versionLine?.() ?? `${options.bin} ${options.version}`}
|
|
10302
11749
|
${options.description}
|
|
10303
11750
|
|
|
10304
11751
|
Commands:
|
|
@@ -10307,6 +11754,9 @@ ${matches.map((command) => ` ${command.path}`).join("\n")}
|
|
|
10307
11754
|
Global flags: --db <path>, --json, --quiet, --compact, --help/-h, --version/-V/-v, --no-color
|
|
10308
11755
|
`
|
|
10309
11756
|
);
|
|
11757
|
+
if (options.describeDb && !quiet)
|
|
11758
|
+
stdout.write(`Database: ${options.describeDb(db)}
|
|
11759
|
+
`);
|
|
10310
11760
|
return 0;
|
|
10311
11761
|
}
|
|
10312
11762
|
if (args[0] === "completions") {
|
|
@@ -10341,7 +11791,16 @@ _arguments '*:command:(${words.join(" ")})'
|
|
|
10341
11791
|
throw new CliUsageError("Expected completion shell: bash, zsh or fish");
|
|
10342
11792
|
return 0;
|
|
10343
11793
|
}
|
|
10344
|
-
if (!entry)
|
|
11794
|
+
if (!entry) {
|
|
11795
|
+
const path = args.filter((x) => !x.startsWith("--")).slice(0, 2).join(" ");
|
|
11796
|
+
const suggestion = suggestCommand(
|
|
11797
|
+
path,
|
|
11798
|
+
tree.map((c) => c.path)
|
|
11799
|
+
);
|
|
11800
|
+
throw new CliUsageError(
|
|
11801
|
+
`Unknown command: ${path}.${suggestion ? ` Did you mean ${suggestion}?` : ""}`
|
|
11802
|
+
);
|
|
11803
|
+
}
|
|
10345
11804
|
const rest = args.slice(entry.path.split(" ").length);
|
|
10346
11805
|
let base = {};
|
|
10347
11806
|
const spec = specFor(entry);
|
|
@@ -10371,29 +11830,42 @@ _arguments '*:command:(${words.join(" ")})'
|
|
|
10371
11830
|
if (!base || typeof base !== "object" || Array.isArray(base))
|
|
10372
11831
|
throw new CliUsageError("Expected stdin JSON object");
|
|
10373
11832
|
}
|
|
10374
|
-
const
|
|
11833
|
+
const objectPositions = entry.positionals.filter(
|
|
11834
|
+
(name) => entry.tool.inputSchema.properties?.[name]?.type === "object"
|
|
11835
|
+
);
|
|
11836
|
+
if (objectPositions.length === 1 && base && typeof base === "object" && Object.keys(base).length && !(objectPositions[0] in base))
|
|
11837
|
+
base = { [objectPositions[0]]: base };
|
|
11838
|
+
let input = {
|
|
10375
11839
|
...base,
|
|
10376
11840
|
...parseCliArgs(rest, spec),
|
|
10377
11841
|
...entry.fixed
|
|
10378
11842
|
};
|
|
11843
|
+
for (const [key, schema] of Object.entries(
|
|
11844
|
+
entry.tool.inputSchema.properties ?? {}
|
|
11845
|
+
)) {
|
|
11846
|
+
if (schema.type === "array" && schema.items?.type === "object" && Array.isArray(input[key]))
|
|
11847
|
+
input[key] = input[key].flat();
|
|
11848
|
+
}
|
|
11849
|
+
input = options.transformInput?.(input) ?? input;
|
|
10379
11850
|
const errors = validateInput(entry.tool.inputSchema, input);
|
|
10380
11851
|
if (errors.length) throw new CliUsageError(errors.join("\n"));
|
|
10381
|
-
const context = await options.openContext(
|
|
11852
|
+
const context = await options.openContext({
|
|
11853
|
+
...db === void 0 ? {} : { db },
|
|
11854
|
+
...options.reportCreation ? { stderr } : {}
|
|
11855
|
+
});
|
|
10382
11856
|
try {
|
|
10383
11857
|
const result = await entry.tool.handler(context, input);
|
|
10384
11858
|
const record = result;
|
|
10385
|
-
const code =
|
|
11859
|
+
const code = cliExitCode(result);
|
|
10386
11860
|
if (code)
|
|
10387
11861
|
stderr.write(
|
|
10388
|
-
`${record.error?.message ?? record.summary ?? "Tool failed"}
|
|
11862
|
+
`${code === 70 ? "internal error: " : ""}${options.formatError?.(record.error?.message ?? record.summary ?? "Tool failed") ?? record.error?.message ?? record.summary ?? "Tool failed"}
|
|
10389
11863
|
`
|
|
10390
11864
|
);
|
|
10391
11865
|
if (!quiet)
|
|
10392
11866
|
stdout.write(
|
|
10393
|
-
json || !stdout.isTTY ? `${JSON.stringify(result, null, compact ? void 0 : 2)}
|
|
10394
|
-
` :
|
|
10395
|
-
${JSON.stringify(record.data ?? {}, null, 2)}
|
|
10396
|
-
`
|
|
11867
|
+
json || !stdout.isTTY ? `${JSON.stringify(plainJson(result, compact, options.formatError), null, compact ? void 0 : 2)}
|
|
11868
|
+
` : humanResult(result)
|
|
10397
11869
|
);
|
|
10398
11870
|
return code;
|
|
10399
11871
|
} finally {
|
|
@@ -10401,22 +11873,24 @@ ${JSON.stringify(record.data ?? {}, null, 2)}
|
|
|
10401
11873
|
}
|
|
10402
11874
|
} catch (error) {
|
|
10403
11875
|
return fail(
|
|
10404
|
-
error instanceof Error ? error.message : String(error)
|
|
11876
|
+
`${error instanceof CliUsageError ? "" : "internal error: "}${error instanceof Error ? error.message : String(error)}`,
|
|
10405
11877
|
error instanceof CliUsageError ? 2 : 70
|
|
10406
11878
|
);
|
|
10407
11879
|
}
|
|
10408
11880
|
}
|
|
10409
11881
|
|
|
10410
11882
|
// src/surfaces/cli.ts
|
|
10411
|
-
|
|
11883
|
+
init_cli_output();
|
|
10412
11884
|
init_validate_input();
|
|
10413
|
-
async function
|
|
11885
|
+
async function runToolCliInternal(argv, providedOptions) {
|
|
10414
11886
|
let options = providedOptions;
|
|
10415
11887
|
const end = argv.indexOf("--");
|
|
10416
11888
|
if (argv.slice(0, end < 0 ? argv.length : end).some((arg) => ["--version", "-V", "-v"].includes(arg))) {
|
|
10417
11889
|
if (!argv.includes("--quiet"))
|
|
10418
|
-
(options.stdout ?? process.stdout).write(
|
|
10419
|
-
|
|
11890
|
+
(options.stdout ?? process.stdout).write(
|
|
11891
|
+
`${options.versionLine?.() ?? options.version}
|
|
11892
|
+
`
|
|
11893
|
+
);
|
|
10420
11894
|
return 0;
|
|
10421
11895
|
}
|
|
10422
11896
|
const modern = await runCliCommand(argv, options);
|
|
@@ -10468,7 +11942,7 @@ async function runToolCli(argv, providedOptions) {
|
|
|
10468
11942
|
const normalize = (name2) => name2.replace(/[-_]/g, "").toLowerCase();
|
|
10469
11943
|
if (["help", "--help", "-h"].includes(command)) {
|
|
10470
11944
|
stdout.write(
|
|
10471
|
-
`${options.bin} ${options.version}
|
|
11945
|
+
`${options.versionLine?.() ?? `${options.bin} ${options.version}`}
|
|
10472
11946
|
${options.description}
|
|
10473
11947
|
|
|
10474
11948
|
Usage: ${options.bin} [--db path] <command>
|
|
@@ -10481,7 +11955,7 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
|
|
|
10481
11955
|
return 0;
|
|
10482
11956
|
}
|
|
10483
11957
|
if (["--version", "-V", "-v", "version"].includes(command)) {
|
|
10484
|
-
stdout.write(`${options.version}
|
|
11958
|
+
stdout.write(`${options.versionLine?.() ?? options.version}
|
|
10485
11959
|
`);
|
|
10486
11960
|
return 0;
|
|
10487
11961
|
}
|
|
@@ -10501,7 +11975,14 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
|
|
|
10501
11975
|
}
|
|
10502
11976
|
if (command === "skill") {
|
|
10503
11977
|
if (!options.commands?.skill) return fail("skill command unavailable");
|
|
10504
|
-
|
|
11978
|
+
try {
|
|
11979
|
+
stdout.write(await options.commands.skill(args));
|
|
11980
|
+
} catch (error) {
|
|
11981
|
+
return fail(
|
|
11982
|
+
`${error instanceof CliUsageError ? "" : "internal error: "}${error instanceof Error ? error.message : String(error)}`,
|
|
11983
|
+
error instanceof CliUsageError ? 2 : 70
|
|
11984
|
+
);
|
|
11985
|
+
}
|
|
10505
11986
|
return 0;
|
|
10506
11987
|
}
|
|
10507
11988
|
if (command === "mcp") {
|
|
@@ -10545,7 +12026,7 @@ ${JSON.stringify(tool.inputSchema, null, 2)}
|
|
|
10545
12026
|
if (args.length > 1) return fail("Expected a single JSON argument");
|
|
10546
12027
|
const raw = args[0];
|
|
10547
12028
|
input = JSON.parse(
|
|
10548
|
-
raw === "-" || raw === void 0 && !stdin.isTTY ? await readStdin() : raw ?? "{}"
|
|
12029
|
+
raw === "-" || raw === void 0 && !stdin.isTTY ? await readStdin() : raw?.startsWith("@") ? readFileSync(raw.slice(1), "utf8") : raw ?? "{}"
|
|
10549
12030
|
);
|
|
10550
12031
|
} else {
|
|
10551
12032
|
const values = {};
|
|
@@ -10589,40 +12070,79 @@ ${JSON.stringify(tool.inputSchema, null, 2)}
|
|
|
10589
12070
|
...Array.isArray(prior) ? prior : [],
|
|
10590
12071
|
coerce(raw, schema.items ?? {})
|
|
10591
12072
|
];
|
|
10592
|
-
} else
|
|
12073
|
+
} else
|
|
12074
|
+
values[property] = options.transformInput && /At$|^(asOf|now)$/.test(property) ? raw : coerce(raw, schema);
|
|
10593
12075
|
}
|
|
10594
12076
|
input = values;
|
|
10595
12077
|
}
|
|
10596
12078
|
} catch (cause) {
|
|
10597
12079
|
return fail(
|
|
10598
12080
|
`Invalid input: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
10599
|
-
|
|
12081
|
+
2
|
|
10600
12082
|
);
|
|
10601
12083
|
}
|
|
12084
|
+
try {
|
|
12085
|
+
input = options.transformInput?.(input) ?? input;
|
|
12086
|
+
} catch (cause) {
|
|
12087
|
+
return fail(cause instanceof Error ? cause.message : String(cause), 2);
|
|
12088
|
+
}
|
|
10602
12089
|
const errors = validateInput(tool.inputSchema, input);
|
|
10603
|
-
if (errors.length) return fail(errors.join("\n"),
|
|
12090
|
+
if (errors.length) return fail(errors.join("\n"), 2);
|
|
10604
12091
|
let context;
|
|
10605
12092
|
try {
|
|
10606
|
-
context = await options.openContext(
|
|
12093
|
+
context = await options.openContext(
|
|
12094
|
+
options.reportCreation ? { ...ctxOptions, stderr } : ctxOptions
|
|
12095
|
+
);
|
|
10607
12096
|
} catch (cause) {
|
|
10608
|
-
return fail(
|
|
12097
|
+
return fail(
|
|
12098
|
+
`internal error: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
12099
|
+
70
|
|
12100
|
+
);
|
|
10609
12101
|
}
|
|
10610
12102
|
try {
|
|
10611
12103
|
const result = await tool.handler(
|
|
10612
12104
|
context,
|
|
10613
12105
|
input
|
|
10614
12106
|
);
|
|
10615
|
-
stdout.write(
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
12107
|
+
stdout.write(
|
|
12108
|
+
explicitJson || !stdout.isTTY ? `${JSON.stringify(plainJson(result, compact, options.formatError), null, compact ? void 0 : 2)}
|
|
12109
|
+
` : humanResult(result)
|
|
12110
|
+
);
|
|
12111
|
+
const code = cliExitCode(result);
|
|
12112
|
+
if (code) {
|
|
12113
|
+
const r = result;
|
|
12114
|
+
const message = r.error?.message ?? r.summary ?? "Tool failed";
|
|
12115
|
+
stderr.write(
|
|
12116
|
+
`${code === 70 ? "internal error: " : ""}${options.formatError?.(message) ?? message}
|
|
12117
|
+
`
|
|
12118
|
+
);
|
|
12119
|
+
}
|
|
12120
|
+
return code;
|
|
10620
12121
|
} catch (cause) {
|
|
10621
|
-
return fail(
|
|
12122
|
+
return fail(
|
|
12123
|
+
`internal error: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
12124
|
+
70
|
|
12125
|
+
);
|
|
10622
12126
|
} finally {
|
|
10623
12127
|
await options.closeContext?.(context);
|
|
10624
12128
|
}
|
|
10625
12129
|
}
|
|
12130
|
+
async function runToolCli(argv, options) {
|
|
12131
|
+
try {
|
|
12132
|
+
return await runToolCliInternal(argv, options);
|
|
12133
|
+
} catch (error) {
|
|
12134
|
+
const message = `internal error: ${error instanceof Error ? error.message : String(error)}`;
|
|
12135
|
+
(options.stderr ?? process.stderr).write(`${message}
|
|
12136
|
+
`);
|
|
12137
|
+
if (argv.includes("--json") && !argv.includes("--quiet"))
|
|
12138
|
+
(options.stdout ?? process.stdout).write(
|
|
12139
|
+
`${JSON.stringify({ ok: false, error: { code: "internal_error", message } })}
|
|
12140
|
+
`
|
|
12141
|
+
);
|
|
12142
|
+
return 70;
|
|
12143
|
+
}
|
|
12144
|
+
}
|
|
10626
12145
|
|
|
10627
12146
|
// src/cli.ts
|
|
12147
|
+
markNpmEntry();
|
|
10628
12148
|
process.exitCode = await runToolCli(process.argv.slice(2), affinityCliOptions);
|