@ghostpaw/affinity 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/dist/cli.js +99 -35
- package/dist/index.cjs +69 -26
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +69 -26
- package/dist/mcp-server.js +86 -27
- package/dist/mcp.cjs +83 -27
- package/dist/mcp.js +83 -27
- package/package.json +1 -1
- package/skills/affinity/references/import-history-without-faking-evidence.md +1 -1
- package/skills/affinity/references/record-direct-evidence-well.md +1 -1
- package/skills/affinity/references/record-observations-and-referrals.md +2 -1
- package/skills/affinity/references/tools.md +7 -6
package/dist/index.cjs
CHANGED
|
@@ -1399,6 +1399,8 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
1399
1399
|
momentKind: row.moment_kind,
|
|
1400
1400
|
participants
|
|
1401
1401
|
};
|
|
1402
|
+
if (row.provenance_json != null)
|
|
1403
|
+
base.provenance = JSON.parse(row.provenance_json);
|
|
1402
1404
|
if (row.content != null) base.content = decodeEventText(row.content);
|
|
1403
1405
|
if (row.channel != null) base.channel = row.channel;
|
|
1404
1406
|
if (row.external_source != null && row.external_id != null)
|
|
@@ -1689,14 +1691,9 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
|
1689
1691
|
text = text.replace(" ", "T");
|
|
1690
1692
|
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1691
1693
|
}
|
|
1692
|
-
let rounding = 0;
|
|
1693
1694
|
text = text.replace(
|
|
1694
1695
|
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1695
|
-
(_, digits) => {
|
|
1696
|
-
const ms = Math.round(Number(`0.${digits}`) * 1e3);
|
|
1697
|
-
rounding = ms === 1e3 ? 1 : 0;
|
|
1698
|
-
return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
|
|
1699
|
-
}
|
|
1696
|
+
(_, digits) => `.${digits.slice(0, 3)}`
|
|
1700
1697
|
);
|
|
1701
1698
|
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1702
1699
|
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
@@ -1716,7 +1713,7 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
|
1716
1713
|
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1717
1714
|
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1718
1715
|
return fail();
|
|
1719
|
-
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text)
|
|
1716
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
|
|
1720
1717
|
if (!Number.isFinite(timestamp)) return fail();
|
|
1721
1718
|
return { timestamp, day };
|
|
1722
1719
|
}
|
|
@@ -2437,7 +2434,9 @@ function insertJournalEvent(db, params) {
|
|
|
2437
2434
|
);
|
|
2438
2435
|
const eventId = Number(result.lastInsertRowid);
|
|
2439
2436
|
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
2440
|
-
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2
|
|
2437
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
2438
|
+
summary
|
|
2439
|
+
) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
|
|
2441
2440
|
eventId
|
|
2442
2441
|
);
|
|
2443
2442
|
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
@@ -5634,7 +5633,7 @@ function exportContact(db, id) {
|
|
|
5634
5633
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
5635
5634
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
5636
5635
|
const events = query(
|
|
5637
|
-
"SELECT DISTINCT e.*, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
5636
|
+
"SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
5638
5637
|
id
|
|
5639
5638
|
).map((event) => ({
|
|
5640
5639
|
...event,
|
|
@@ -5762,12 +5761,14 @@ function getEventThread(db, source, id, options) {
|
|
|
5762
5761
|
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
5763
5762
|
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
5764
5763
|
WHERE e.deleted_at IS NULL AND (e.parent_event_id=p.id OR (e.parent_source=p.external_source AND e.parent_ref_id=p.external_id) OR (e.thread_source=? AND e.thread_id=?))
|
|
5765
|
-
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
5764
|
+
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
5766
5765
|
root.id,
|
|
5767
5766
|
root.externalRef?.source ?? null,
|
|
5768
5767
|
root.externalRef?.id ?? null,
|
|
5769
5768
|
options?.channel ?? null,
|
|
5770
5769
|
options?.channel?.trim().toLowerCase() ?? null,
|
|
5770
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5771
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5771
5772
|
limit,
|
|
5772
5773
|
offset
|
|
5773
5774
|
);
|
|
@@ -6062,7 +6063,7 @@ Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
|
|
|
6062
6063
|
Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
|
|
6063
6064
|
Participant contact locators may use identities, including did, instead of ids.
|
|
6064
6065
|
Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
|
|
6065
|
-
SQLite date-times without offsets mean UTC; microseconds
|
|
6066
|
+
SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
|
|
6066
6067
|
Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
|
|
6067
6068
|
The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
|
|
6068
6069
|
show the mapping and verify an idempotent retry and byte-identical text.
|
|
@@ -6324,7 +6325,7 @@ arrive. Commitments and resolutions never reset contact recency or cadence.
|
|
|
6324
6325
|
Imported text and event maintenance:
|
|
6325
6326
|
- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
|
|
6326
6327
|
- Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.
|
|
6327
|
-
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
|
|
6328
|
+
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.
|
|
6328
6329
|
- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
|
|
6329
6330
|
- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
|
|
6330
6331
|
- Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.
|
|
@@ -6390,7 +6391,8 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
|
|
|
6390
6391
|
content: `# Record Observations And Referrals
|
|
6391
6392
|
|
|
6392
6393
|
Likes, reposts and follows are ambient observations, not milestones. Record them
|
|
6393
|
-
with affectsBond:false
|
|
6394
|
+
with affectsBond:false. Like/follow/repost labels default to false across families;
|
|
6395
|
+
explicit true opts in. Observations with significance <=2 also default to false.
|
|
6394
6396
|
They stay in journals but do not change affinity or meaningful-contact recency.
|
|
6395
6397
|
provenance.ambient:true also suppresses effects. Use explicit owner and other
|
|
6396
6398
|
participants, actor/recipient roles, and the correct directionality.
|
|
@@ -6769,7 +6771,7 @@ var externalRefSchema = objectSchema(
|
|
|
6769
6771
|
);
|
|
6770
6772
|
var eventMetadataSchema = {
|
|
6771
6773
|
affectsBond: booleanSchema(
|
|
6772
|
-
"Whether evidence affects the relationship; defaults false for observations with significance <=2."
|
|
6774
|
+
"Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
|
|
6773
6775
|
),
|
|
6774
6776
|
content: stringSchema(
|
|
6775
6777
|
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
@@ -7308,6 +7310,13 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
7308
7310
|
`${path}.${key}`,
|
|
7309
7311
|
referenceNow
|
|
7310
7312
|
);
|
|
7313
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
|
|
7314
|
+
const provenance = object.provenance;
|
|
7315
|
+
object.provenance = {
|
|
7316
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
7317
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
|
|
7318
|
+
};
|
|
7319
|
+
}
|
|
7311
7320
|
object[key] = parsed.timestamp;
|
|
7312
7321
|
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
7313
7322
|
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
@@ -7325,7 +7334,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
7325
7334
|
const error = validateToolInput(
|
|
7326
7335
|
child,
|
|
7327
7336
|
object[key],
|
|
7328
|
-
`${path}.${key}`,
|
|
7337
|
+
key === "input" && path === "input" ? path : `${path}.${key}`,
|
|
7329
7338
|
referenceNow,
|
|
7330
7339
|
strict
|
|
7331
7340
|
);
|
|
@@ -7719,6 +7728,11 @@ function setAttribute(db, target, name, value) {
|
|
|
7719
7728
|
const stored = normalizeAttributeValue(value);
|
|
7720
7729
|
const now = resolveNow();
|
|
7721
7730
|
const existing = getLiveAttributeRow(db, target, key);
|
|
7731
|
+
if (existing?.value === stored)
|
|
7732
|
+
return buildAttributeMutationReceipt(
|
|
7733
|
+
mapAttributeRowToAttributeRecord(existing),
|
|
7734
|
+
{}
|
|
7735
|
+
);
|
|
7722
7736
|
if (existing) {
|
|
7723
7737
|
db.prepare(
|
|
7724
7738
|
"UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
|
|
@@ -7858,7 +7872,30 @@ function linkLocatorSchema2(description) {
|
|
|
7858
7872
|
};
|
|
7859
7873
|
}
|
|
7860
7874
|
function resolveAttributeTarget(db, target) {
|
|
7861
|
-
if (target.kind === "event")
|
|
7875
|
+
if (target.kind === "event") {
|
|
7876
|
+
if (target.eventId === void 0 === (target.ref === void 0))
|
|
7877
|
+
return {
|
|
7878
|
+
kind: "error",
|
|
7879
|
+
result: toolFailure(
|
|
7880
|
+
"protocol",
|
|
7881
|
+
"invalid_input",
|
|
7882
|
+
"Invalid event target.",
|
|
7883
|
+
"Provide exactly one of target.eventId or target.ref."
|
|
7884
|
+
)
|
|
7885
|
+
};
|
|
7886
|
+
const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
|
|
7887
|
+
if (eventId === void 0)
|
|
7888
|
+
return {
|
|
7889
|
+
kind: "error",
|
|
7890
|
+
result: toolFailure(
|
|
7891
|
+
"domain",
|
|
7892
|
+
"not_found",
|
|
7893
|
+
"Event not found.",
|
|
7894
|
+
"No event matches target.ref."
|
|
7895
|
+
)
|
|
7896
|
+
};
|
|
7897
|
+
return { kind: "ok", target: { kind: "event", eventId } };
|
|
7898
|
+
}
|
|
7862
7899
|
if (target.kind === "contact") {
|
|
7863
7900
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
7864
7901
|
if (!contact.ok) {
|
|
@@ -7940,7 +7977,8 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
7940
7977
|
target: objectSchema(
|
|
7941
7978
|
{
|
|
7942
7979
|
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
7943
|
-
eventId: integerSchema("Target event id."),
|
|
7980
|
+
eventId: integerSchema("Target event id; provide eventId or ref."),
|
|
7981
|
+
ref: externalRefSchema,
|
|
7944
7982
|
contact: contactLocatorSchema2(
|
|
7945
7983
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
7946
7984
|
),
|
|
@@ -9816,12 +9854,12 @@ function reviewPage(db, rawInput, pageOnly) {
|
|
|
9816
9854
|
if (!pageOnly && input.view !== "graph.chart") {
|
|
9817
9855
|
const limit = input.limit ?? 50;
|
|
9818
9856
|
const offset = input.offset ?? 0;
|
|
9819
|
-
if (!Number.isInteger(limit) || limit < 1 || limit >
|
|
9857
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
|
|
9820
9858
|
return toolFailure(
|
|
9821
9859
|
"protocol",
|
|
9822
9860
|
"invalid_input",
|
|
9823
9861
|
"Invalid pagination.",
|
|
9824
|
-
"limit must be 1..
|
|
9862
|
+
"limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
|
|
9825
9863
|
);
|
|
9826
9864
|
const all = [];
|
|
9827
9865
|
let page;
|
|
@@ -9873,7 +9911,7 @@ function reviewPage(db, rawInput, pageOnly) {
|
|
|
9873
9911
|
{
|
|
9874
9912
|
...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
|
|
9875
9913
|
...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
|
|
9876
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
9914
|
+
...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
|
|
9877
9915
|
...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
|
|
9878
9916
|
},
|
|
9879
9917
|
options
|
|
@@ -9887,9 +9925,10 @@ function reviewPage(db, rawInput, pageOnly) {
|
|
|
9887
9925
|
"Missing thread reference.",
|
|
9888
9926
|
"ref is required for events.thread."
|
|
9889
9927
|
);
|
|
9890
|
-
items = getEventThread(db, input.ref.source, input.ref.id,
|
|
9891
|
-
|
|
9892
|
-
|
|
9928
|
+
items = getEventThread(db, input.ref.source, input.ref.id, {
|
|
9929
|
+
...options,
|
|
9930
|
+
...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
|
|
9931
|
+
}).map(toEventListToolItem);
|
|
9893
9932
|
break;
|
|
9894
9933
|
}
|
|
9895
9934
|
case "events.contact_journal": {
|
|
@@ -10148,7 +10187,7 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
10148
10187
|
includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
|
|
10149
10188
|
since: "Optional lower timestamp bound for time-based views.",
|
|
10150
10189
|
until: "Optional upper timestamp bound for time-based views.",
|
|
10151
|
-
limit: "Optional result limit.",
|
|
10190
|
+
limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
|
|
10152
10191
|
offset: "Optional result offset."
|
|
10153
10192
|
},
|
|
10154
10193
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
@@ -10253,13 +10292,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
10253
10292
|
]),
|
|
10254
10293
|
horizonDays: integerSchema("Optional horizon-days filter."),
|
|
10255
10294
|
exactOnly: booleanSchema("Whether duplicate review is exact-only."),
|
|
10256
|
-
minScore: numberSchema(
|
|
10295
|
+
minScore: numberSchema(
|
|
10296
|
+
"Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
|
|
10297
|
+
),
|
|
10257
10298
|
includeDismissed: booleanSchema(
|
|
10258
10299
|
"Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
|
|
10259
10300
|
),
|
|
10260
10301
|
since: integerSchema("Optional lower timestamp bound."),
|
|
10261
10302
|
until: integerSchema("Optional upper timestamp bound."),
|
|
10262
|
-
limit: integerSchema(
|
|
10303
|
+
limit: integerSchema(
|
|
10304
|
+
"Optional result limit, 1..10000 (default 50); use offset for paging."
|
|
10305
|
+
),
|
|
10263
10306
|
offset: integerSchema("Optional result offset.")
|
|
10264
10307
|
},
|
|
10265
10308
|
["view"],
|
package/dist/index.d.cts
CHANGED
|
@@ -273,6 +273,7 @@ interface ExternalRef {
|
|
|
273
273
|
/** Journal entry with explicit participants — CONCEPT.md EventRecord. */
|
|
274
274
|
interface EventRecord {
|
|
275
275
|
content?: string;
|
|
276
|
+
provenance?: unknown;
|
|
276
277
|
externalRef?: ExternalRef;
|
|
277
278
|
channel?: string;
|
|
278
279
|
parentRef?: ExternalRef;
|
|
@@ -846,6 +847,7 @@ declare function searchEvents(db: AffinityDb, query: string, options?: {
|
|
|
846
847
|
offset?: number;
|
|
847
848
|
}): EventSearchItem[];
|
|
848
849
|
declare function getEventThread(db: AffinityDb, source: string, id: string, options?: {
|
|
850
|
+
eventTypes?: readonly EventType[];
|
|
849
851
|
includeContent?: boolean;
|
|
850
852
|
channel?: string;
|
|
851
853
|
limit?: number;
|
|
@@ -1491,6 +1493,11 @@ type AttributeTargetInput = {
|
|
|
1491
1493
|
} | {
|
|
1492
1494
|
kind: "event";
|
|
1493
1495
|
eventId: number;
|
|
1496
|
+
ref?: never;
|
|
1497
|
+
} | {
|
|
1498
|
+
kind: "event";
|
|
1499
|
+
ref: ExternalRef;
|
|
1500
|
+
eventId?: never;
|
|
1494
1501
|
};
|
|
1495
1502
|
type ManageAttributeToolInput = {
|
|
1496
1503
|
action: "set";
|
package/dist/index.d.ts
CHANGED
|
@@ -273,6 +273,7 @@ interface ExternalRef {
|
|
|
273
273
|
/** Journal entry with explicit participants — CONCEPT.md EventRecord. */
|
|
274
274
|
interface EventRecord {
|
|
275
275
|
content?: string;
|
|
276
|
+
provenance?: unknown;
|
|
276
277
|
externalRef?: ExternalRef;
|
|
277
278
|
channel?: string;
|
|
278
279
|
parentRef?: ExternalRef;
|
|
@@ -846,6 +847,7 @@ declare function searchEvents(db: AffinityDb, query: string, options?: {
|
|
|
846
847
|
offset?: number;
|
|
847
848
|
}): EventSearchItem[];
|
|
848
849
|
declare function getEventThread(db: AffinityDb, source: string, id: string, options?: {
|
|
850
|
+
eventTypes?: readonly EventType[];
|
|
849
851
|
includeContent?: boolean;
|
|
850
852
|
channel?: string;
|
|
851
853
|
limit?: number;
|
|
@@ -1491,6 +1493,11 @@ type AttributeTargetInput = {
|
|
|
1491
1493
|
} | {
|
|
1492
1494
|
kind: "event";
|
|
1493
1495
|
eventId: number;
|
|
1496
|
+
ref?: never;
|
|
1497
|
+
} | {
|
|
1498
|
+
kind: "event";
|
|
1499
|
+
ref: ExternalRef;
|
|
1500
|
+
eventId?: never;
|
|
1494
1501
|
};
|
|
1495
1502
|
type ManageAttributeToolInput = {
|
|
1496
1503
|
action: "set";
|
package/dist/index.js
CHANGED
|
@@ -1397,6 +1397,8 @@ function mapEventRowToEventRecord(row, participants) {
|
|
|
1397
1397
|
momentKind: row.moment_kind,
|
|
1398
1398
|
participants
|
|
1399
1399
|
};
|
|
1400
|
+
if (row.provenance_json != null)
|
|
1401
|
+
base.provenance = JSON.parse(row.provenance_json);
|
|
1400
1402
|
if (row.content != null) base.content = decodeEventText(row.content);
|
|
1401
1403
|
if (row.channel != null) base.channel = row.channel;
|
|
1402
1404
|
if (row.external_source != null && row.external_id != null)
|
|
@@ -1687,14 +1689,9 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
|
1687
1689
|
text = text.replace(" ", "T");
|
|
1688
1690
|
if (!/(?:Z|[+-]\d{2}:\d{2})$/.test(text)) text += "Z";
|
|
1689
1691
|
}
|
|
1690
|
-
let rounding = 0;
|
|
1691
1692
|
text = text.replace(
|
|
1692
1693
|
/\.(\d{4,})(?=Z|[+-]\d{2}:\d{2}$)/,
|
|
1693
|
-
(_, digits) => {
|
|
1694
|
-
const ms = Math.round(Number(`0.${digits}`) * 1e3);
|
|
1695
|
-
rounding = ms === 1e3 ? 1 : 0;
|
|
1696
|
-
return `.${String(ms === 1e3 ? 999 : ms).padStart(3, "0")}`;
|
|
1697
|
-
}
|
|
1694
|
+
(_, digits) => `.${digits.slice(0, 3)}`
|
|
1698
1695
|
);
|
|
1699
1696
|
if (/^(today|yesterday|tomorrow|[+-]\d+d)$/.test(text)) {
|
|
1700
1697
|
const offset = text === "today" ? 0 : text === "tomorrow" ? 1 : text === "yesterday" ? -1 : Number(text.slice(0, -1));
|
|
@@ -1714,7 +1711,7 @@ function parseCalendarTime(value, field = "time", now = Date.now()) {
|
|
|
1714
1711
|
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
1715
1712
|
if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== day)
|
|
1716
1713
|
return fail();
|
|
1717
|
-
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text)
|
|
1714
|
+
const timestamp = dateOnly ? midnight + (field.split(".").at(-1) === "dueAt" ? 86399999 : 0) : Date.parse(text);
|
|
1718
1715
|
if (!Number.isFinite(timestamp)) return fail();
|
|
1719
1716
|
return { timestamp, day };
|
|
1720
1717
|
}
|
|
@@ -2435,7 +2432,9 @@ function insertJournalEvent(db, params) {
|
|
|
2435
2432
|
);
|
|
2436
2433
|
const eventId = Number(result.lastInsertRowid);
|
|
2437
2434
|
db.prepare("UPDATE events SET affects_bond=? WHERE id=?").run(
|
|
2438
|
-
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2
|
|
2435
|
+
params.affectsBond === void 0 ? params.type === "observation" && params.significance <= 2 || ["activity", "milestone", "observation"].includes(params.type) && /^(?:follow(?:ed|ing)?|like(?:d)?|repost(?:ed)?)(?:\b|[-_])/i.test(
|
|
2436
|
+
summary
|
|
2437
|
+
) && !/^follow[ -]up\b/i.test(summary) ? 0 : 1 : Number(params.affectsBond),
|
|
2439
2438
|
eventId
|
|
2440
2439
|
);
|
|
2441
2440
|
db.prepare("UPDATE events SET declared_moment_kind=? WHERE id=?").run(
|
|
@@ -5632,7 +5631,7 @@ function exportContact(db, id) {
|
|
|
5632
5631
|
const contact = query("SELECT * FROM contacts WHERE id = ?", id)[0];
|
|
5633
5632
|
if (!contact) throw new AffinityNotFoundError("contact not found");
|
|
5634
5633
|
const events = query(
|
|
5635
|
-
"SELECT DISTINCT e.*, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
5634
|
+
"SELECT DISTINCT e.*, COALESCE(e.parent_event_id, (SELECT parent.id FROM events parent WHERE parent.external_source=e.parent_source AND parent.external_id=e.parent_ref_id AND parent.deleted_at IS NULL)) AS parent_event_id, CAST(e.content AS BLOB) AS content, CAST(e.summary AS BLOB) AS summary FROM events e JOIN event_participants p ON p.event_id = e.id WHERE p.contact_id = ? AND e.type != 'date_anchor' ORDER BY e.id",
|
|
5636
5635
|
id
|
|
5637
5636
|
).map((event) => ({
|
|
5638
5637
|
...event,
|
|
@@ -5760,12 +5759,14 @@ function getEventThread(db, source, id, options) {
|
|
|
5760
5759
|
const rows = db.prepare(`WITH RECURSIVE thread(id) AS (
|
|
5761
5760
|
SELECT ? UNION SELECT e.id FROM events e JOIN thread t JOIN events p ON p.id=t.id
|
|
5762
5761
|
WHERE e.deleted_at IS NULL AND (e.parent_event_id=p.id OR (e.parent_source=p.external_source AND e.parent_ref_id=p.external_id) OR (e.thread_source=? AND e.thread_id=?))
|
|
5763
|
-
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
5762
|
+
) SELECT e.id FROM events e JOIN thread t ON e.id=t.id WHERE e.deleted_at IS NULL AND (? IS NULL OR e.channel=?) AND (? IS NULL OR e.type IN (SELECT value FROM json_each(?))) ORDER BY e.occurred_at,e.id LIMIT ? OFFSET ?`).all(
|
|
5764
5763
|
root.id,
|
|
5765
5764
|
root.externalRef?.source ?? null,
|
|
5766
5765
|
root.externalRef?.id ?? null,
|
|
5767
5766
|
options?.channel ?? null,
|
|
5768
5767
|
options?.channel?.trim().toLowerCase() ?? null,
|
|
5768
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5769
|
+
options?.eventTypes === void 0 ? null : JSON.stringify(options.eventTypes),
|
|
5769
5770
|
limit,
|
|
5770
5771
|
offset
|
|
5771
5772
|
);
|
|
@@ -6060,7 +6061,7 @@ Use ghostpaw-affinity batch [--chunk 500] with JSONL {tool,args}, or
|
|
|
6060
6061
|
Use externalRef {source:"bluesky",id:recordUri}, verbatim content and channel.
|
|
6061
6062
|
Participant contact locators may use identities, including did, instead of ids.
|
|
6062
6063
|
Read JSON flags from stdin with --input - or from @file for large payloads (E2BIG).
|
|
6063
|
-
SQLite date-times without offsets mean UTC; microseconds
|
|
6064
|
+
SQLite date-times without offsets mean UTC; microseconds truncate to milliseconds; raw timestamps remain in provenance.
|
|
6064
6065
|
Missing inbound-follow timestamps should use a known first_seen or be counted as skipped.
|
|
6065
6066
|
The synthetic scripts/examples/import_social_jsonl.mjs and import_dry_run.mjs
|
|
6066
6067
|
show the mapping and verify an idempotent retry and byte-identical text.
|
|
@@ -6322,7 +6323,7 @@ arrive. Commitments and resolutions never reset contact recency or cadence.
|
|
|
6322
6323
|
Imported text and event maintenance:
|
|
6323
6324
|
- Use externalRef: {source, id} for idempotent intake; retries return no_op and the existing id.
|
|
6324
6325
|
- Store verbatim text in content (up to 1,048,576 UTF-8 bytes, empty allowed). Summary is optional when content exists; its derived label is at most 140 characters.
|
|
6325
|
-
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId}.
|
|
6326
|
+
- Use channel for the medium, parentRef or parentEventId for replies, and threadRef for the root. Event attributes use target {kind: "event", eventId} or {kind: "event", ref: {source, id}}. Unchanged values return no_op.
|
|
6326
6327
|
- Inspect kind "event" by eventId or ref. Search scope "events" or "all" discovers event ids; default scope remains contacts.
|
|
6327
6328
|
- Review journals/timelines with includeContent: true only when needed; CLI uses --with-content. Filter with channel. Review events.thread by ref for chronological context.
|
|
6328
6329
|
- Use manage_event revise with patch: summary, content, occurredAt, significance, channel, type within the same kind, or externalRef only when absent. Unknown keys fail validation.
|
|
@@ -6388,7 +6389,8 @@ var recordObservationsAndReferralsSkill = defineAffinitySkill({
|
|
|
6388
6389
|
content: `# Record Observations And Referrals
|
|
6389
6390
|
|
|
6390
6391
|
Likes, reposts and follows are ambient observations, not milestones. Record them
|
|
6391
|
-
with affectsBond:false
|
|
6392
|
+
with affectsBond:false. Like/follow/repost labels default to false across families;
|
|
6393
|
+
explicit true opts in. Observations with significance <=2 also default to false.
|
|
6392
6394
|
They stay in journals but do not change affinity or meaningful-contact recency.
|
|
6393
6395
|
provenance.ambient:true also suppresses effects. Use explicit owner and other
|
|
6394
6396
|
participants, actor/recipient roles, and the correct directionality.
|
|
@@ -6767,7 +6769,7 @@ var externalRefSchema = objectSchema(
|
|
|
6767
6769
|
);
|
|
6768
6770
|
var eventMetadataSchema = {
|
|
6769
6771
|
affectsBond: booleanSchema(
|
|
6770
|
-
"Whether evidence affects the relationship; defaults false for observations with significance <=2."
|
|
6772
|
+
"Whether evidence affects the relationship; defaults false for like/follow/repost labels and observations with significance <=2; explicit true opts in."
|
|
6771
6773
|
),
|
|
6772
6774
|
content: stringSchema(
|
|
6773
6775
|
"Verbatim text, including empty text; at most 1 MB UTF-8. Summary may be omitted when content is supplied."
|
|
@@ -7306,6 +7308,13 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
7306
7308
|
`${path}.${key}`,
|
|
7307
7309
|
referenceNow
|
|
7308
7310
|
);
|
|
7311
|
+
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key) && typeof object[key] === "string" && /\.\d{4,}/.test(object[key])) {
|
|
7312
|
+
const provenance = object.provenance;
|
|
7313
|
+
object.provenance = {
|
|
7314
|
+
...provenance && typeof provenance === "object" ? provenance : {},
|
|
7315
|
+
[`raw${key[0]?.toUpperCase()}${key.slice(1)}`]: object[key]
|
|
7316
|
+
};
|
|
7317
|
+
}
|
|
7309
7318
|
object[key] = parsed.timestamp;
|
|
7310
7319
|
if (!path.endsWith(".patch") && ["occurredAt", "dueAt"].includes(key))
|
|
7311
7320
|
object[key.replace(/At$/, "On")] ??= parsed.day;
|
|
@@ -7323,7 +7332,7 @@ function validateToolInput(schema, value, path = "input", injectedNow = Date.now
|
|
|
7323
7332
|
const error = validateToolInput(
|
|
7324
7333
|
child,
|
|
7325
7334
|
object[key],
|
|
7326
|
-
`${path}.${key}`,
|
|
7335
|
+
key === "input" && path === "input" ? path : `${path}.${key}`,
|
|
7327
7336
|
referenceNow,
|
|
7328
7337
|
strict
|
|
7329
7338
|
);
|
|
@@ -7717,6 +7726,11 @@ function setAttribute(db, target, name, value) {
|
|
|
7717
7726
|
const stored = normalizeAttributeValue(value);
|
|
7718
7727
|
const now = resolveNow();
|
|
7719
7728
|
const existing = getLiveAttributeRow(db, target, key);
|
|
7729
|
+
if (existing?.value === stored)
|
|
7730
|
+
return buildAttributeMutationReceipt(
|
|
7731
|
+
mapAttributeRowToAttributeRecord(existing),
|
|
7732
|
+
{}
|
|
7733
|
+
);
|
|
7720
7734
|
if (existing) {
|
|
7721
7735
|
db.prepare(
|
|
7722
7736
|
"UPDATE attributes SET value = ?, updated_at = ? WHERE id = ?"
|
|
@@ -7856,7 +7870,30 @@ function linkLocatorSchema2(description) {
|
|
|
7856
7870
|
};
|
|
7857
7871
|
}
|
|
7858
7872
|
function resolveAttributeTarget(db, target) {
|
|
7859
|
-
if (target.kind === "event")
|
|
7873
|
+
if (target.kind === "event") {
|
|
7874
|
+
if (target.eventId === void 0 === (target.ref === void 0))
|
|
7875
|
+
return {
|
|
7876
|
+
kind: "error",
|
|
7877
|
+
result: toolFailure(
|
|
7878
|
+
"protocol",
|
|
7879
|
+
"invalid_input",
|
|
7880
|
+
"Invalid event target.",
|
|
7881
|
+
"Provide exactly one of target.eventId or target.ref."
|
|
7882
|
+
)
|
|
7883
|
+
};
|
|
7884
|
+
const eventId = target.eventId ?? (target.ref && getEventByExternalRef(db, target.ref.source, target.ref.id)?.id);
|
|
7885
|
+
if (eventId === void 0)
|
|
7886
|
+
return {
|
|
7887
|
+
kind: "error",
|
|
7888
|
+
result: toolFailure(
|
|
7889
|
+
"domain",
|
|
7890
|
+
"not_found",
|
|
7891
|
+
"Event not found.",
|
|
7892
|
+
"No event matches target.ref."
|
|
7893
|
+
)
|
|
7894
|
+
};
|
|
7895
|
+
return { kind: "ok", target: { kind: "event", eventId } };
|
|
7896
|
+
}
|
|
7860
7897
|
if (target.kind === "contact") {
|
|
7861
7898
|
const contact = resolveContactLocator(db, target.contact, "target.contact");
|
|
7862
7899
|
if (!contact.ok) {
|
|
@@ -7938,7 +7975,8 @@ var manageAttributeTool = defineAffinityTool({
|
|
|
7938
7975
|
target: objectSchema(
|
|
7939
7976
|
{
|
|
7940
7977
|
kind: enumSchema("Target kind.", ["contact", "link", "event"]),
|
|
7941
|
-
eventId: integerSchema("Target event id."),
|
|
7978
|
+
eventId: integerSchema("Target event id; provide eventId or ref."),
|
|
7979
|
+
ref: externalRefSchema,
|
|
7942
7980
|
contact: contactLocatorSchema2(
|
|
7943
7981
|
"Contact locator. Required when kind=contact. Provide contactId or identity."
|
|
7944
7982
|
),
|
|
@@ -9814,12 +9852,12 @@ function reviewPage(db, rawInput, pageOnly) {
|
|
|
9814
9852
|
if (!pageOnly && input.view !== "graph.chart") {
|
|
9815
9853
|
const limit = input.limit ?? 50;
|
|
9816
9854
|
const offset = input.offset ?? 0;
|
|
9817
|
-
if (!Number.isInteger(limit) || limit < 1 || limit >
|
|
9855
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1e4 || !Number.isInteger(offset) || offset < 0)
|
|
9818
9856
|
return toolFailure(
|
|
9819
9857
|
"protocol",
|
|
9820
9858
|
"invalid_input",
|
|
9821
9859
|
"Invalid pagination.",
|
|
9822
|
-
"limit must be 1..
|
|
9860
|
+
"limit must be 1..10000; use offset to page through larger results; offset must be non-negative"
|
|
9823
9861
|
);
|
|
9824
9862
|
const all = [];
|
|
9825
9863
|
let page;
|
|
@@ -9871,7 +9909,7 @@ function reviewPage(db, rawInput, pageOnly) {
|
|
|
9871
9909
|
{
|
|
9872
9910
|
...input.contactIds === void 0 ? {} : { contactIds: input.contactIds },
|
|
9873
9911
|
...input.exactOnly === void 0 ? {} : { exactOnly: input.exactOnly },
|
|
9874
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
9912
|
+
...input.minScore === void 0 ? { minScore: 0.5 } : { minScore: input.minScore },
|
|
9875
9913
|
...input.includeDismissed === void 0 ? {} : { includeDismissed: input.includeDismissed }
|
|
9876
9914
|
},
|
|
9877
9915
|
options
|
|
@@ -9885,9 +9923,10 @@ function reviewPage(db, rawInput, pageOnly) {
|
|
|
9885
9923
|
"Missing thread reference.",
|
|
9886
9924
|
"ref is required for events.thread."
|
|
9887
9925
|
);
|
|
9888
|
-
items = getEventThread(db, input.ref.source, input.ref.id,
|
|
9889
|
-
|
|
9890
|
-
|
|
9926
|
+
items = getEventThread(db, input.ref.source, input.ref.id, {
|
|
9927
|
+
...options,
|
|
9928
|
+
...input.eventTypes === void 0 ? {} : { eventTypes: input.eventTypes }
|
|
9929
|
+
}).map(toEventListToolItem);
|
|
9891
9930
|
break;
|
|
9892
9931
|
}
|
|
9893
9932
|
case "events.contact_journal": {
|
|
@@ -10146,7 +10185,7 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
10146
10185
|
includeDismissed: "Whether dismissed pairs should be included in duplicate review (marked with dismissed flag).",
|
|
10147
10186
|
since: "Optional lower timestamp bound for time-based views.",
|
|
10148
10187
|
until: "Optional upper timestamp bound for time-based views.",
|
|
10149
|
-
limit: "Optional result limit.",
|
|
10188
|
+
limit: "Optional result limit, 1..10000 (default 50); use offset for paging.",
|
|
10150
10189
|
offset: "Optional result offset."
|
|
10151
10190
|
},
|
|
10152
10191
|
outputDescription: "Returns compact review items, a count, the applied filters, and the chart payload for graph review.",
|
|
@@ -10251,13 +10290,17 @@ var reviewAffinityTool = defineAffinityTool({
|
|
|
10251
10290
|
]),
|
|
10252
10291
|
horizonDays: integerSchema("Optional horizon-days filter."),
|
|
10253
10292
|
exactOnly: booleanSchema("Whether duplicate review is exact-only."),
|
|
10254
|
-
minScore: numberSchema(
|
|
10293
|
+
minScore: numberSchema(
|
|
10294
|
+
"Optional duplicate minimum score (default 0.5; use 0 for all candidates)."
|
|
10295
|
+
),
|
|
10255
10296
|
includeDismissed: booleanSchema(
|
|
10256
10297
|
"Whether dismissed pairs are included in duplicate review (marked with dismissed flag)."
|
|
10257
10298
|
),
|
|
10258
10299
|
since: integerSchema("Optional lower timestamp bound."),
|
|
10259
10300
|
until: integerSchema("Optional upper timestamp bound."),
|
|
10260
|
-
limit: integerSchema(
|
|
10301
|
+
limit: integerSchema(
|
|
10302
|
+
"Optional result limit, 1..10000 (default 50); use offset for paging."
|
|
10303
|
+
),
|
|
10261
10304
|
offset: integerSchema("Optional result offset.")
|
|
10262
10305
|
},
|
|
10263
10306
|
["view"],
|