@wirecat/cli-core 0.548.0 → 0.550.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/CHANGELOG.md +232 -0
- package/dist/calendar/cli/index.d.ts +10 -1
- package/dist/calendar/cli/index.js +9 -3
- package/dist/calendar/cli/search-all.d.ts +17 -0
- package/dist/calendar/cli/search-all.js +72 -0
- package/dist/cli/answer-entity.d.ts +4 -0
- package/dist/cli/answer-entity.js +5 -0
- package/dist/cli/bot/context.d.ts +2 -0
- package/dist/cli/compact.d.ts +41 -0
- package/dist/cli/compact.js +313 -0
- package/dist/cli/context.d.ts +11 -2
- package/dist/cli/context.js +15 -6
- package/dist/cli/envelope.d.ts +5 -1
- package/dist/cli/envelope.js +15 -4
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/messenger/search-command.d.ts +10 -0
- package/dist/cli/messenger/search-command.js +19 -13
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +7 -1
- package/dist/domain/providers.d.ts +3 -0
- package/dist/domain/providers.js +20 -4
- package/dist/domain/ref-arguments.js +3 -7
- package/dist/domain/ref-summaries.d.ts +56 -0
- package/dist/domain/ref-summaries.js +63 -0
- package/dist/entities/index.d.ts +11 -0
- package/dist/entities/relationship.d.ts +2 -0
- package/dist/entities/relationship.js +2 -0
- package/dist/entities/task-results.d.ts +10 -0
- package/dist/entities/task-results.js +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/mail/partial.d.ts +9 -3
- package/dist/mail/partial.js +35 -10
- package/dist/mail/source.d.ts +7 -1
- package/dist/mail/sync.js +1 -1
- package/dist/output-detail.js +9 -5
- package/dist/output.d.ts +5 -1
- package/dist/output.js +29 -4
- package/dist/services/proposals/kinds/relationship-update.d.ts +4 -0
- package/dist/services/proposals/kinds/relationship-update.js +53 -0
- package/dist/services/proposals/registry.js +2 -0
- package/dist/store/decisions.d.ts +3 -1
- package/dist/store/decisions.js +7 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/sqlite/email-locations.d.ts +22 -1
- package/dist/store/sqlite/email-locations.js +29 -2
- package/dist/store/sqlite/emails.d.ts +3 -7
- package/dist/store/sqlite/emails.js +4 -2
- package/dist/store/sqlite/evidence-sources.d.ts +2 -2
- package/dist/store/sqlite/evidence-sources.js +41 -77
- package/dist/store/sqlite/knowledge.d.ts +17 -23
- package/dist/store/sqlite/knowledge.js +13 -13
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +12 -0
- package/dist/store/sqlite/notes.d.ts +5 -0
- package/dist/store/sqlite/notes.js +6 -2
- package/dist/store/sqlite/open-store.js +2 -0
- package/dist/store/sqlite/proposed-actions.d.ts +1 -1
- package/dist/store/sqlite/proposed-actions.js +10 -2
- package/dist/store/sqlite/ref-summaries.d.ts +17 -0
- package/dist/store/sqlite/ref-summaries.js +378 -0
- package/dist/store/sqlite/relation-proposals.d.ts +40 -0
- package/dist/store/sqlite/relation-proposals.js +88 -1
- package/dist/store/sqlite/schema.d.ts +16 -0
- package/dist/store/sqlite/schema.js +1 -0
- package/dist/store/sqlite/suggested-links.d.ts +10 -0
- package/dist/store/sqlite/suggested-links.js +68 -0
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +5 -0
- package/package.json +3 -3
- package/parity.json +4 -0
|
@@ -15,7 +15,7 @@ import { meetingProjectsStoreOver } from "./meeting-projects.js";
|
|
|
15
15
|
import { holdingText } from "./note-index.js";
|
|
16
16
|
import { insertLink, linkOf } from "./notes.js";
|
|
17
17
|
import { queryFilter } from "./query-filter.js";
|
|
18
|
-
import { relationProposalsOver } from "./relation-proposals.js";
|
|
18
|
+
import { relationOf, relationProposalsOver, } from "./relation-proposals.js";
|
|
19
19
|
import { addTags as addThingTags, canonicalTagNamed, createTag, labelsOf, liveTagging, removeTags as removeThingTags, setMainTopic, } from "./tags.js";
|
|
20
20
|
import { INBOX_KEY_LENGTH, TASK_KEY_PREFIXES } from "./task-keys.js";
|
|
21
21
|
import { inboxKey, taskAccountOf, taskIdOf, taskRowOf } from "./tasks.js";
|
|
@@ -221,18 +221,6 @@ export const knowledgeStoreOver = (context) => {
|
|
|
221
221
|
scope: row.scope,
|
|
222
222
|
createdAt: iso(row.created_at),
|
|
223
223
|
});
|
|
224
|
-
const relationOf = (link) => ({
|
|
225
|
-
id: link.id,
|
|
226
|
-
from: link.from,
|
|
227
|
-
to: String(link.to),
|
|
228
|
-
kind: link.kind,
|
|
229
|
-
role: link.role,
|
|
230
|
-
evidence: link.evidence,
|
|
231
|
-
confirmed: link.confirmed,
|
|
232
|
-
provenance: link.provenance,
|
|
233
|
-
confidenceCategory: link.confirmed ? "owner-confirmed" : "weak",
|
|
234
|
-
createdAt: link.createdAt,
|
|
235
|
-
});
|
|
236
224
|
const proposals = relationProposalsOver(context);
|
|
237
225
|
const RELATIONS = `SELECT * FROM links WHERE kind IN (${RELATION_KINDS.map(linkKind).join(",")}) AND to_id IS NOT NULL`;
|
|
238
226
|
const relation = (id) => {
|
|
@@ -561,6 +549,7 @@ export const knowledgeStoreOver = (context) => {
|
|
|
561
549
|
origin: confirmed ? "owner" : "suggested",
|
|
562
550
|
confirmed,
|
|
563
551
|
...(input.role === undefined ? {} : { role: input.role }),
|
|
552
|
+
...(input.purpose === undefined ? {} : { purpose: input.purpose }),
|
|
564
553
|
...(input.evidence === undefined ? {} : { evidence: input.evidence }),
|
|
565
554
|
...(input.provenance === undefined ? {} : { provenance: input.provenance }),
|
|
566
555
|
})));
|
|
@@ -573,6 +562,17 @@ export const knowledgeStoreOver = (context) => {
|
|
|
573
562
|
});
|
|
574
563
|
return proposals.propose(from, input, by);
|
|
575
564
|
},
|
|
565
|
+
updateRelation: (async (key, id, input) => {
|
|
566
|
+
checked(key);
|
|
567
|
+
const link = linkOf(database, relation(id));
|
|
568
|
+
if (input.by === "owner")
|
|
569
|
+
return relationOf(proposals.update(link, input));
|
|
570
|
+
return proposals.proposeUpdate(link, input, input.by);
|
|
571
|
+
}),
|
|
572
|
+
decideRelationUpdate: async (id, decision) => {
|
|
573
|
+
const { proposal, link } = proposals.decideUpdate(id, decision);
|
|
574
|
+
return { proposal, relation: link && relationOf(link) };
|
|
575
|
+
},
|
|
576
576
|
decideLinkProposal: async (id, decision) => proposals.decideProposal(id, decision),
|
|
577
577
|
decideSuggestedLink: async (id, decision) => proposals.decideLink(id, decision),
|
|
578
578
|
relations: async (key, reference) => {
|
|
@@ -57,6 +57,8 @@ export const MANIFEST = [
|
|
|
57
57
|
{ name: "20261031230945_version-41-task-fields", version: 41, minCompatible: 34 },
|
|
58
58
|
{ name: "20261101002523_version-42-task-revision", version: 42, minCompatible: 34 },
|
|
59
59
|
{ name: "20261101011859_version-43-memory-project", version: 43, minCompatible: 34 },
|
|
60
|
+
{ name: "20261102050533_version-44-relationship-purpose", version: 44, minCompatible: 34 },
|
|
61
|
+
{ name: "20261102050534_version-44-suggested-links-move", version: 44, minCompatible: 34 },
|
|
60
62
|
];
|
|
61
63
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
62
64
|
const migrations = [];
|
|
@@ -877,5 +877,17 @@ export const GENERATED = [
|
|
|
877
877
|
"ALTER TABLE `memories` ADD `project_id` integer REFERENCES projects(id);",
|
|
878
878
|
"CREATE INDEX `memories_by_project_id` ON `memories` (`project_id`);"
|
|
879
879
|
]
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
"name": "20261102050533_version-44-relationship-purpose",
|
|
883
|
+
"statements": [
|
|
884
|
+
"ALTER TABLE `links` ADD `purpose` text;"
|
|
885
|
+
]
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"name": "20261102050534_version-44-suggested-links-move",
|
|
889
|
+
"statements": [
|
|
890
|
+
"-- A suggestion's references (chat:, msg:) need the store's own encoding, which SQL cannot write, so the next writable\n-- open moves the unconfirmed agent and rule links into `proposed_actions` and deletes this key (moveSuggestedLinks).\nINSERT OR REPLACE INTO store_settings (key, value, at) VALUES ('suggested_links_to_move', '1', unixepoch() * 1000);"
|
|
891
|
+
]
|
|
880
892
|
}
|
|
881
893
|
];
|
|
@@ -56,6 +56,8 @@ export interface Link {
|
|
|
56
56
|
origin: (typeof LINK_SOURCES)[number];
|
|
57
57
|
targetText: string | null;
|
|
58
58
|
role: string | null;
|
|
59
|
+
/** What a chat is for in a project, from the purposes memo's config lists; `null` means `work`. */
|
|
60
|
+
purpose: string | null;
|
|
59
61
|
evidence: string | null;
|
|
60
62
|
provenance: string | null;
|
|
61
63
|
confirmed: boolean;
|
|
@@ -79,6 +81,7 @@ export interface LinkInput {
|
|
|
79
81
|
anchor?: string;
|
|
80
82
|
origin?: (typeof LINK_ORIGINS)[number];
|
|
81
83
|
role?: string;
|
|
84
|
+
purpose?: string;
|
|
82
85
|
evidence?: string;
|
|
83
86
|
provenance?: string;
|
|
84
87
|
confirmed?: boolean;
|
|
@@ -194,6 +197,8 @@ export declare const requiredThing: (database: CacheDatabase, reference: string,
|
|
|
194
197
|
* and only a name some note is waiting for costs more.
|
|
195
198
|
*/
|
|
196
199
|
export declare const resolvePersonLinks: (database: CacheDatabase, names: (string | null | undefined)[]) => void;
|
|
200
|
+
/** The store keeps no list of purposes: memo's config holds it, and checks a purpose before it gets here. */
|
|
201
|
+
export declare const purposeOf: (purpose: string) => string;
|
|
197
202
|
/** Writes one link; an identical resolved link is answered instead of doubled. */
|
|
198
203
|
export declare const insertLink: (context: StoreContext, input: LinkInput) => Link;
|
|
199
204
|
export declare const notesStoreOver: (context: StoreContext) => Omit<NotesStore, keyof NoteSearch>;
|
|
@@ -87,6 +87,7 @@ export const linkOf = (database, row) => {
|
|
|
87
87
|
origin: row.source,
|
|
88
88
|
targetText: row.target_text == null ? null : String(row.target_text),
|
|
89
89
|
role: row.role == null ? null : String(row.role),
|
|
90
|
+
purpose: row.purpose == null ? null : String(row.purpose),
|
|
90
91
|
evidence: row.evidence == null ? null : String(row.evidence),
|
|
91
92
|
provenance: metadata.provenance ?? null,
|
|
92
93
|
confirmed: Number(row.confirmed) === 1,
|
|
@@ -164,6 +165,8 @@ const ownerLink = (input) => {
|
|
|
164
165
|
});
|
|
165
166
|
return input;
|
|
166
167
|
};
|
|
168
|
+
/** The store keeps no list of purposes: memo's config holds it, and checks a purpose before it gets here. */
|
|
169
|
+
export const purposeOf = (purpose) => textOf(purpose, "a purpose", 100);
|
|
167
170
|
/** Writes one link; an identical resolved link is answered instead of doubled. */
|
|
168
171
|
export const insertLink = (context, input) => {
|
|
169
172
|
const { database, now } = context;
|
|
@@ -187,8 +190,9 @@ export const insertLink = (context, input) => {
|
|
|
187
190
|
const provenance = input.provenance === undefined ? undefined : textOf(input.provenance, "provenance", 2000);
|
|
188
191
|
const row = database
|
|
189
192
|
.prepare("INSERT INTO links (from_type, from_id, to_type, to_id, kind, anchor, source, target_text, target_folded, role, evidence, " +
|
|
190
|
-
"metadata, confirmed, created_at, author, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
191
|
-
|
|
193
|
+
"metadata, confirmed, created_at, author, updated_at, purpose) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
|
|
194
|
+
"RETURNING id")
|
|
195
|
+
.get(from.type, from.id, to?.type ?? null, to?.id ?? null, kind, input.anchor ?? null, origin, targetText, targetText === null ? null : targetKey(targetText), input.role === undefined ? null : textOf(input.role, "a role", 200), input.evidence === undefined ? null : textOf(input.evidence, "evidence", 2000), provenance === undefined ? null : JSON.stringify({ provenance }), input.confirmed === false ? 0 : 1, at, origin === "owner" ? "owner" : null, at, input.purpose === undefined ? null : purposeOf(input.purpose));
|
|
192
196
|
if (to === null && targetText !== null)
|
|
193
197
|
resolveKey(database, targetKey(targetText));
|
|
194
198
|
return linkOf(database, database.prepare("SELECT * FROM links WHERE id = ?").get(Number(row?.id)));
|
|
@@ -16,6 +16,7 @@ import { loadProviders } from "./providers.js";
|
|
|
16
16
|
import { assertReadCompatible } from "./read-compatibility.js";
|
|
17
17
|
import { stemsState } from "./stems.js";
|
|
18
18
|
import { fillStems, followDefaultStemmers } from "./stems-fill.js";
|
|
19
|
+
import { moveSuggestedLinks } from "./suggested-links.js";
|
|
19
20
|
import { fillVectorIndexes } from "./vector-index.js";
|
|
20
21
|
export const BACKFILL_ON_OPEN = 5_000;
|
|
21
22
|
const NOT_WRITABLE = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
@@ -55,6 +56,7 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, ver
|
|
|
55
56
|
fillFiles(database, now);
|
|
56
57
|
fillFolded(database);
|
|
57
58
|
fillJoinKeys(database);
|
|
59
|
+
moveSuggestedLinks(database, now);
|
|
58
60
|
const pending = pendingNormalization(database);
|
|
59
61
|
if (pending > 0 && pending <= BACKFILL_ON_OPEN)
|
|
60
62
|
backfillNormalized(database);
|
|
@@ -81,7 +81,7 @@ type ProposalOperations = Omit<{
|
|
|
81
81
|
proposer(id: string): Proposer;
|
|
82
82
|
};
|
|
83
83
|
/** Who proposed an action, for `mayDecide`. */
|
|
84
|
-
export declare const proposerOfAction: ({ proposedBy }: ProposedAction) => Proposer;
|
|
84
|
+
export declare const proposerOfAction: ({ proposedBy, payload }: ProposedAction) => Proposer;
|
|
85
85
|
export declare const proposalOperationsOver: (context: StoreContext) => ProposalOperations;
|
|
86
86
|
export declare const proposedActionsStoreOver: (context: StoreContext) => ProposedActionsStore;
|
|
87
87
|
export {};
|
|
@@ -20,11 +20,19 @@ const STATUS = `CASE WHEN p.status = 'approved' AND p.kind IN ('${MEETING_FINDIN
|
|
|
20
20
|
const NAMED = `SELECT p.*, ${STATUS} AS shown_status, b.name AS proposer_name, b.kind AS proposer_kind FROM proposed_actions p ` +
|
|
21
21
|
"LEFT JOIN bots b ON p.proposed_by_type = 'bot' AND b.id = p.proposed_by_id";
|
|
22
22
|
/** Who proposed an action, for `mayDecide`. */
|
|
23
|
-
export const proposerOfAction = ({ proposedBy }) => proposedBy.type === "person"
|
|
23
|
+
export const proposerOfAction = ({ proposedBy, payload }) => proposedBy.type === "person"
|
|
24
24
|
? { type: "person", id: proposedBy.id }
|
|
25
25
|
: proposedBy.name === null
|
|
26
26
|
? { type: "unknown" }
|
|
27
|
-
: {
|
|
27
|
+
: {
|
|
28
|
+
type: "bot",
|
|
29
|
+
id: proposedBy.id,
|
|
30
|
+
name: proposedBy.name,
|
|
31
|
+
kind: proposedBy.kind ?? "agent",
|
|
32
|
+
...(movedSuggestion(payload) ? { verified: false } : {}),
|
|
33
|
+
};
|
|
34
|
+
/** A suggestion moved from `links` names its bot only as the link's provenance text said it (store version 44). */
|
|
35
|
+
const movedSuggestion = (payload) => typeof payload === "object" && payload !== null && "suggestedLink" in payload;
|
|
28
36
|
export const proposalOperationsOver = (context) => {
|
|
29
37
|
const { database, now } = context;
|
|
30
38
|
const actionOf = (row) => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RefSummary } from "../../domain/ref-summaries.js";
|
|
2
|
+
import type { CacheDatabase } from "../driver.js";
|
|
3
|
+
/** A summary plus what an evidence reader shows beside it; `summaries` drops the extra fields. */
|
|
4
|
+
export interface RefRead extends RefSummary {
|
|
5
|
+
sender: string | null;
|
|
6
|
+
excerpt: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Every ref read back in one statement per kind present, keyed by the text passed and in its order. A bad ref is
|
|
10
|
+
* `unknown` and a ref the store lacks is `unavailable`: one bad row must not fail a page of them.
|
|
11
|
+
*/
|
|
12
|
+
export declare const refReadsOf: (database: CacheDatabase, texts: readonly string[], { excerpt }?: {
|
|
13
|
+
excerpt?: boolean;
|
|
14
|
+
}) => Map<string, RefRead>;
|
|
15
|
+
export declare const refSummariesOf: (database: CacheDatabase, texts: readonly string[], options?: {
|
|
16
|
+
excerpt?: boolean;
|
|
17
|
+
}) => Map<string, RefSummary>;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { filesAccountSql, providerLabel, providerNamed } from "../../domain/providers.js";
|
|
2
|
+
import { oneLine } from "../../domain/ref-summaries.js";
|
|
3
|
+
import { formatReference, parseReference } from "../../domain/references.js";
|
|
4
|
+
const EXCERPT_CHARS = 280;
|
|
5
|
+
const TITLE_CHARS = 120;
|
|
6
|
+
const MEMORY_TITLE_CHARS = 80;
|
|
7
|
+
/** SQLite's parameter limit is per statement; a long page is read in slices, still one statement per slice. */
|
|
8
|
+
const SLICE = 500;
|
|
9
|
+
const flat = (value) => {
|
|
10
|
+
if (value == null)
|
|
11
|
+
return null;
|
|
12
|
+
const text = oneLine(String(value));
|
|
13
|
+
return text === "" ? null : text;
|
|
14
|
+
};
|
|
15
|
+
const cut = (text, chars) => text === null || text.length <= chars ? text : `${text.slice(0, chars - 1)}…`;
|
|
16
|
+
const iso = (at) => (at === null ? null : new Date(at).toISOString());
|
|
17
|
+
const minute = (at) => iso(at)?.slice(0, 16).replace("T", " ") ?? null;
|
|
18
|
+
const storeId = (text) => (/^[1-9]\d{0,15}$/.test(text) ? Number(text) : undefined);
|
|
19
|
+
const bodyOr = (body, column) => (body ? column : "NULL");
|
|
20
|
+
const PERSON_NAME = "coalesce(p.name, (SELECT a.name FROM aliases a WHERE a.aliasable_type = 'person' AND a.aliasable_id = p.id " +
|
|
21
|
+
"ORDER BY a.display DESC, a.id LIMIT 1), (SELECT i.name FROM identity_links l JOIN identities i " +
|
|
22
|
+
"ON i.id = l.identity_id WHERE l.person_id = p.id AND i.name IS NOT NULL ORDER BY i.id LIMIT 1))";
|
|
23
|
+
/** What a row is about, by name; the owner is left out, or every owner's note would say whose it is. */
|
|
24
|
+
const subjectName = (type, id) => `CASE ${type} WHEN 'person' THEN (SELECT ${PERSON_NAME} FROM persons p WHERE p.id = ${id} AND p.owner = 0) ` +
|
|
25
|
+
`WHEN 'project' THEN (SELECT name FROM projects WHERE id = ${id}) ` +
|
|
26
|
+
`WHEN 'organization' THEN (SELECT name FROM organizations WHERE id = ${id}) ` +
|
|
27
|
+
`WHEN 'account' THEN (SELECT name FROM accounts WHERE id = ${id}) END`;
|
|
28
|
+
const select = (columns) => `SELECT w.n, ${["title", "holder", "sender", "at", "provider", "account", "deleted", "body"]
|
|
29
|
+
.map((column) => `${columns[column] ?? "NULL"} AS ${column}`)
|
|
30
|
+
.join(", ")}`;
|
|
31
|
+
const byId = (kind, table, columns) => ({
|
|
32
|
+
kind,
|
|
33
|
+
sql: (body) => `${select(columns(body))} FROM w JOIN ${table} x ON x.id = w.k1`,
|
|
34
|
+
});
|
|
35
|
+
const READERS = {
|
|
36
|
+
message: {
|
|
37
|
+
kind: "message",
|
|
38
|
+
sql: (body) => `${select({
|
|
39
|
+
holder: "c.title",
|
|
40
|
+
sender: "coalesce(m.sender_name, i.name)",
|
|
41
|
+
at: "m.sent_at",
|
|
42
|
+
provider: "a.provider",
|
|
43
|
+
account: "a.external_id",
|
|
44
|
+
deleted: "m.deleted_at",
|
|
45
|
+
body: bodyOr(body, "m.text"),
|
|
46
|
+
})} FROM w JOIN accounts a ON a.provider = w.k1 AND a.external_id = w.k2 ` +
|
|
47
|
+
"JOIN chats c ON c.account_id = a.id AND c.external_id = w.k3 " +
|
|
48
|
+
"JOIN messages m ON m.chat_id = c.id AND m.external_id = w.k4 LEFT JOIN identities i ON i.id = m.sender_identity_id",
|
|
49
|
+
},
|
|
50
|
+
email: {
|
|
51
|
+
kind: "email",
|
|
52
|
+
sql: (body) => `${select({
|
|
53
|
+
title: "e.subject",
|
|
54
|
+
holder: "coalesce(e.from_name, e.from_address)",
|
|
55
|
+
sender: "coalesce(e.from_name, e.from_address)",
|
|
56
|
+
at: "e.sent_at",
|
|
57
|
+
provider: "'email'",
|
|
58
|
+
account: "a.external_id",
|
|
59
|
+
deleted: "e.deleted_at",
|
|
60
|
+
body: bodyOr(body, "coalesce(e.body_text, e.snippet)"),
|
|
61
|
+
})} FROM w JOIN accounts a ON a.provider = 'email' AND a.external_id = w.k1 ` +
|
|
62
|
+
"JOIN email_threads t ON t.account_id = a.id AND t.external_id = w.k2 " +
|
|
63
|
+
"JOIN emails e ON e.email_thread_id = t.id AND e.external_id = w.k3",
|
|
64
|
+
},
|
|
65
|
+
chat: {
|
|
66
|
+
kind: "chat",
|
|
67
|
+
sql: () => `${select({
|
|
68
|
+
title: "c.title",
|
|
69
|
+
at: "(SELECT max(sent_at) FROM messages WHERE chat_id = c.id)",
|
|
70
|
+
provider: "a.provider",
|
|
71
|
+
account: "a.external_id",
|
|
72
|
+
})} FROM w JOIN accounts a ON a.provider = w.k1 AND a.external_id = w.k2 ` +
|
|
73
|
+
"JOIN chats c ON c.account_id = a.id AND c.external_id = w.k3",
|
|
74
|
+
},
|
|
75
|
+
email_thread: {
|
|
76
|
+
kind: "email_thread",
|
|
77
|
+
sql: () => `${select({
|
|
78
|
+
title: "t.subject",
|
|
79
|
+
holder: "a.external_id",
|
|
80
|
+
at: "t.last_email_at",
|
|
81
|
+
provider: "'email'",
|
|
82
|
+
account: "a.external_id",
|
|
83
|
+
deleted: "t.deleted_at",
|
|
84
|
+
})} FROM w JOIN accounts a ON a.provider = 'email' AND a.external_id = w.k1 ` +
|
|
85
|
+
"JOIN email_threads t ON t.account_id = a.id AND t.external_id = w.k2",
|
|
86
|
+
},
|
|
87
|
+
contact: {
|
|
88
|
+
kind: "contact",
|
|
89
|
+
sql: () => `${select({ title: "coalesce(i.name, '@' || i.username)", provider: "i.provider" })} ` +
|
|
90
|
+
"FROM w JOIN identities i ON i.provider = w.k1 AND i.external_id = w.k2",
|
|
91
|
+
},
|
|
92
|
+
folder: {
|
|
93
|
+
kind: "folder",
|
|
94
|
+
sql: () => `${select({ title: "a.name", provider: "a.provider", account: "a.external_id" })} ` +
|
|
95
|
+
`FROM w JOIN accounts a ON a.id = w.k1 AND ${filesAccountSql("a.provider")}`,
|
|
96
|
+
},
|
|
97
|
+
task: {
|
|
98
|
+
kind: "task",
|
|
99
|
+
// A task moved to another project took a new key there; its old key still names it, as in `taskRowOf`.
|
|
100
|
+
sql: (body) => `${select({
|
|
101
|
+
title: "CASE WHEN x.key IS NULL THEN x.title ELSE x.key || ' · ' || x.title END",
|
|
102
|
+
holder: "p.name",
|
|
103
|
+
at: "x.due_at",
|
|
104
|
+
deleted: "x.deleted_at",
|
|
105
|
+
body: bodyOr(body, "x.description"),
|
|
106
|
+
})} FROM w JOIN tasks x ON x.id = coalesce(` +
|
|
107
|
+
"(SELECT id FROM tasks WHERE package_id = w.k1 OR key = w.k1 ORDER BY id LIMIT 1), " +
|
|
108
|
+
"(SELECT task_id FROM task_events WHERE kind = 'project' AND json_extract(changes, '$.key[0]') = w.k1 " +
|
|
109
|
+
"ORDER BY id DESC LIMIT 1)) LEFT JOIN projects p ON p.id = x.project_id",
|
|
110
|
+
},
|
|
111
|
+
meeting: {
|
|
112
|
+
kind: "meeting",
|
|
113
|
+
sql: (body) => `${select({
|
|
114
|
+
title: "m.title",
|
|
115
|
+
holder: "coalesce(a.name, a.external_id)",
|
|
116
|
+
sender: "r.speaker_name",
|
|
117
|
+
at: "m.started_at + coalesce(r.start_ms, 0)",
|
|
118
|
+
provider: "a.provider",
|
|
119
|
+
account: "a.external_id",
|
|
120
|
+
deleted: "coalesce(m.deleted_at, t.deleted_at)",
|
|
121
|
+
body: bodyOr(body, "CASE WHEN w.k4 IS NOT NULL THEN r.text WHEN w.k3 IS NULL THEN m.description END"),
|
|
122
|
+
})} FROM w JOIN meetings m ON m.id = w.k2 AND m.account_id = w.k1 LEFT JOIN accounts a ON a.id = m.account_id ` +
|
|
123
|
+
"LEFT JOIN meeting_transcripts t ON t.id = w.k3 AND t.meeting_id = m.id " +
|
|
124
|
+
"LEFT JOIN meeting_transcript_rows r ON r.meeting_transcript_id = t.id AND r.position = w.k4 " +
|
|
125
|
+
"WHERE (w.k3 IS NULL OR t.id IS NOT NULL) AND (w.k4 IS NULL OR r.id IS NOT NULL)",
|
|
126
|
+
},
|
|
127
|
+
event: {
|
|
128
|
+
kind: "event",
|
|
129
|
+
sql: (body) => `${select({
|
|
130
|
+
title: "x.title",
|
|
131
|
+
holder: "c.title",
|
|
132
|
+
at: "x.starts_at",
|
|
133
|
+
provider: "a.provider",
|
|
134
|
+
account: "a.external_id",
|
|
135
|
+
deleted: "x.deleted_at",
|
|
136
|
+
body: bodyOr(body, "x.description"),
|
|
137
|
+
})} FROM w JOIN events x ON x.id = w.k1 ` +
|
|
138
|
+
"LEFT JOIN calendar_entries ce ON ce.id = (SELECT min(id) FROM calendar_entries WHERE event_id = x.id) " +
|
|
139
|
+
"LEFT JOIN calendars c ON c.id = ce.calendar_id LEFT JOIN accounts a ON a.id = c.account_id",
|
|
140
|
+
},
|
|
141
|
+
document: {
|
|
142
|
+
kind: "document",
|
|
143
|
+
sql: (body) => `${select({
|
|
144
|
+
title: "coalesce(x.title, x.file_name)",
|
|
145
|
+
at: "coalesce(x.external_updated_at, x.updated_at)",
|
|
146
|
+
provider: "a.provider",
|
|
147
|
+
account: "a.external_id",
|
|
148
|
+
deleted: "x.deleted_at",
|
|
149
|
+
body: bodyOr(body, "x.body"),
|
|
150
|
+
})} FROM w JOIN documents x ON x.id = w.k1 LEFT JOIN accounts a ON a.id = x.account_id`,
|
|
151
|
+
},
|
|
152
|
+
note: byId("note", "notes", (body) => ({
|
|
153
|
+
title: "x.title",
|
|
154
|
+
holder: subjectName("x.notable_type", "x.notable_id"),
|
|
155
|
+
at: "x.updated_at",
|
|
156
|
+
provider: "'notes'",
|
|
157
|
+
deleted: "x.deleted_at",
|
|
158
|
+
body: bodyOr(body, "x.body"),
|
|
159
|
+
})),
|
|
160
|
+
person: {
|
|
161
|
+
kind: "person",
|
|
162
|
+
sql: () => `${select({ title: PERSON_NAME })} FROM w JOIN persons p ON p.id = w.k1`,
|
|
163
|
+
},
|
|
164
|
+
organization: byId("organization", "organizations", () => ({ title: "x.name", deleted: "x.deleted_at" })),
|
|
165
|
+
project: byId("project", "projects", () => ({
|
|
166
|
+
title: "CASE WHEN x.key IS NULL THEN x.name ELSE x.key || ' · ' || x.name END",
|
|
167
|
+
deleted: "x.deleted_at",
|
|
168
|
+
})),
|
|
169
|
+
memory: byId("memory", "memories", (body) => ({
|
|
170
|
+
title: "substr(x.body, 1, 400)",
|
|
171
|
+
holder: subjectName("x.subject_type", "x.subject_id"),
|
|
172
|
+
at: "x.created_at",
|
|
173
|
+
deleted: "x.deleted_at",
|
|
174
|
+
body: bodyOr(body, "x.body"),
|
|
175
|
+
})),
|
|
176
|
+
fact: byId("fact", "facts", (body) => ({
|
|
177
|
+
title: "substr(x.statement, 1, 400)",
|
|
178
|
+
holder: subjectName("x.subject_type", "x.subject_id"),
|
|
179
|
+
at: "x.created_at",
|
|
180
|
+
deleted: "x.deleted_at",
|
|
181
|
+
body: bodyOr(body, "x.statement"),
|
|
182
|
+
})),
|
|
183
|
+
bot: byId("bot", "bots", () => ({ title: "x.name" })),
|
|
184
|
+
};
|
|
185
|
+
/** Mail saved before the mail tables existed is still messages, so a miss there is read again as a message. */
|
|
186
|
+
const FALLBACK = { email: "message", email_thread: "chat" };
|
|
187
|
+
const route = (reference) => {
|
|
188
|
+
switch (reference.type) {
|
|
189
|
+
case "message":
|
|
190
|
+
return reference.provider === "email"
|
|
191
|
+
? { reader: "email", keys: [reference.account, reference.chat, reference.message] }
|
|
192
|
+
: { reader: "message", keys: [reference.provider, reference.account, reference.chat, reference.message] };
|
|
193
|
+
case "chat":
|
|
194
|
+
return reference.provider === "email"
|
|
195
|
+
? { reader: "email_thread", keys: [reference.account, reference.chat] }
|
|
196
|
+
: { reader: "chat", keys: [reference.provider, reference.account, reference.chat] };
|
|
197
|
+
case "contact":
|
|
198
|
+
return { reader: "contact", keys: [reference.provider, reference.id] };
|
|
199
|
+
case "meeting":
|
|
200
|
+
return {
|
|
201
|
+
reader: "meeting",
|
|
202
|
+
keys: [reference.accountId, reference.meetingId, reference.transcriptId ?? null, reference.cuePosition ?? null],
|
|
203
|
+
};
|
|
204
|
+
case "task":
|
|
205
|
+
return { reader: "task", keys: [reference.id] };
|
|
206
|
+
case "entity":
|
|
207
|
+
return undefined;
|
|
208
|
+
case "folder": {
|
|
209
|
+
const id = storeId(reference.id);
|
|
210
|
+
return id === undefined ? undefined : { reader: "folder", keys: [id] };
|
|
211
|
+
}
|
|
212
|
+
default: {
|
|
213
|
+
const id = storeId(reference.id);
|
|
214
|
+
return id === undefined ? undefined : { reader: reference.type, keys: [id] };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const keysOf = (reader, reference) => {
|
|
219
|
+
if (reader === "message" && reference.type === "message")
|
|
220
|
+
return [reference.provider, reference.account, reference.chat, reference.message];
|
|
221
|
+
if (reader === "chat" && reference.type === "chat")
|
|
222
|
+
return [reference.provider, reference.account, reference.chat];
|
|
223
|
+
return [];
|
|
224
|
+
};
|
|
225
|
+
const kindOfReference = (reference) => reference.type === "entity"
|
|
226
|
+
? "unknown"
|
|
227
|
+
: reference.type === "message" && reference.provider === "email"
|
|
228
|
+
? "email"
|
|
229
|
+
: reference.type === "chat" && reference.provider === "email"
|
|
230
|
+
? "email_thread"
|
|
231
|
+
: reference.type;
|
|
232
|
+
const runReader = (database, reader, wanted, body) => {
|
|
233
|
+
const found = new Map();
|
|
234
|
+
const width = wanted[0]?.keys.length ?? 0;
|
|
235
|
+
const names = Array.from({ length: width }, (_, at) => `k${at + 1}`);
|
|
236
|
+
for (let start = 0; start < wanted.length; start += SLICE) {
|
|
237
|
+
const slice = wanted.slice(start, start + SLICE);
|
|
238
|
+
const values = slice.map(() => `(${["?", ...names.map(() => "?")].join(", ")})`).join(", ");
|
|
239
|
+
const sql = `WITH w(n, ${names.join(", ")}) AS (VALUES ${values}) ${reader.sql(body)}`;
|
|
240
|
+
const parameters = slice.flatMap((one, at) => [start + at, ...one.keys]);
|
|
241
|
+
for (const row of database.prepare(sql).all(...parameters)) {
|
|
242
|
+
const n = Number(row.n);
|
|
243
|
+
if (found.has(n))
|
|
244
|
+
continue;
|
|
245
|
+
found.set(n, {
|
|
246
|
+
title: flat(row.title),
|
|
247
|
+
holder: flat(row.holder),
|
|
248
|
+
sender: flat(row.sender),
|
|
249
|
+
at: row.at == null ? null : Number(row.at),
|
|
250
|
+
provider: row.provider == null ? null : String(row.provider),
|
|
251
|
+
account: row.account == null ? null : String(row.account),
|
|
252
|
+
deleted: row.deleted != null,
|
|
253
|
+
body: row.body == null ? null : String(row.body),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return found;
|
|
258
|
+
};
|
|
259
|
+
const toolOf = (provider) => (provider === null ? null : (providerNamed(provider)?.command ?? null));
|
|
260
|
+
const refOfAccount = (reference) => reference.type === "message" || reference.type === "chat"
|
|
261
|
+
? { provider: reference.provider, account: reference.account }
|
|
262
|
+
: reference.type === "contact"
|
|
263
|
+
? { provider: reference.provider, account: null }
|
|
264
|
+
: { provider: null, account: null };
|
|
265
|
+
const titleOf = (kind, reference, found) => {
|
|
266
|
+
switch (kind) {
|
|
267
|
+
case "message": {
|
|
268
|
+
const words = [found.sender, minute(found.at)].filter((one) => one !== null);
|
|
269
|
+
return words.length === 0 ? null : words.join(" · ");
|
|
270
|
+
}
|
|
271
|
+
case "memory":
|
|
272
|
+
case "fact":
|
|
273
|
+
return cut(found.title, MEMORY_TITLE_CHARS);
|
|
274
|
+
case "folder":
|
|
275
|
+
return reference.type === "folder" && reference.path !== null && found.title !== null
|
|
276
|
+
? `${found.title}/${reference.path}`
|
|
277
|
+
: found.title;
|
|
278
|
+
default:
|
|
279
|
+
return found.title;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const inOf = (kind, found) => found.holder ??
|
|
283
|
+
((kind === "chat" || kind === "contact" || kind === "document") && found.provider !== null
|
|
284
|
+
? providerLabel(found.provider)
|
|
285
|
+
: null);
|
|
286
|
+
const readOf = (wanted, found, reader, excerpt) => {
|
|
287
|
+
const ref = formatReference(wanted.reference);
|
|
288
|
+
if (found === undefined || reader === undefined) {
|
|
289
|
+
const { provider, account } = refOfAccount(wanted.reference);
|
|
290
|
+
return {
|
|
291
|
+
ref,
|
|
292
|
+
kind: kindOfReference(wanted.reference),
|
|
293
|
+
title: null,
|
|
294
|
+
in: null,
|
|
295
|
+
at: null,
|
|
296
|
+
provider,
|
|
297
|
+
account,
|
|
298
|
+
tool: toolOf(provider),
|
|
299
|
+
state: "unavailable",
|
|
300
|
+
sender: null,
|
|
301
|
+
excerpt: null,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const state = found.deleted ? "deleted" : "available";
|
|
305
|
+
return {
|
|
306
|
+
ref,
|
|
307
|
+
kind: reader.kind,
|
|
308
|
+
title: cut(titleOf(reader.kind, wanted.reference, found), TITLE_CHARS),
|
|
309
|
+
in: cut(inOf(reader.kind, found), TITLE_CHARS),
|
|
310
|
+
at: iso(found.at),
|
|
311
|
+
provider: found.provider,
|
|
312
|
+
account: found.account,
|
|
313
|
+
tool: toolOf(found.provider),
|
|
314
|
+
state,
|
|
315
|
+
sender: found.sender,
|
|
316
|
+
excerpt: excerpt && state === "available" ? cut(flat(found.body), EXCERPT_CHARS) : null,
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
const unknown = (text) => ({
|
|
320
|
+
ref: text.trim(),
|
|
321
|
+
kind: "unknown",
|
|
322
|
+
title: null,
|
|
323
|
+
in: null,
|
|
324
|
+
at: null,
|
|
325
|
+
provider: null,
|
|
326
|
+
account: null,
|
|
327
|
+
tool: null,
|
|
328
|
+
state: "unavailable",
|
|
329
|
+
sender: null,
|
|
330
|
+
excerpt: null,
|
|
331
|
+
});
|
|
332
|
+
const parsed = (text) => {
|
|
333
|
+
try {
|
|
334
|
+
return parseReference(text);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Every ref read back in one statement per kind present, keyed by the text passed and in its order. A bad ref is
|
|
342
|
+
* `unknown` and a ref the store lacks is `unavailable`: one bad row must not fail a page of them.
|
|
343
|
+
*/
|
|
344
|
+
export const refReadsOf = (database, texts, { excerpt = false } = {}) => {
|
|
345
|
+
const reads = new Map();
|
|
346
|
+
const groups = new Map();
|
|
347
|
+
for (const text of new Set(texts)) {
|
|
348
|
+
const reference = parsed(text);
|
|
349
|
+
const routed = reference === undefined ? undefined : route(reference);
|
|
350
|
+
if (reference === undefined || routed === undefined) {
|
|
351
|
+
reads.set(text, reference === undefined ? unknown(text) : readOf({ text, reference, keys: [] }, undefined, undefined, false));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
reads.set(text, unknown(text));
|
|
355
|
+
const group = groups.get(routed.reader) ?? [];
|
|
356
|
+
group.push({ text, reference, keys: routed.keys });
|
|
357
|
+
groups.set(routed.reader, group);
|
|
358
|
+
}
|
|
359
|
+
const pending = [...groups.entries()];
|
|
360
|
+
while (pending.length > 0) {
|
|
361
|
+
const [name, wanted] = pending.shift();
|
|
362
|
+
const reader = READERS[name];
|
|
363
|
+
const found = runReader(database, reader, wanted, excerpt);
|
|
364
|
+
const fallback = FALLBACK[name];
|
|
365
|
+
const missed = [];
|
|
366
|
+
for (const [n, one] of wanted.entries()) {
|
|
367
|
+
const row = found.get(n);
|
|
368
|
+
if (row === undefined && fallback !== undefined)
|
|
369
|
+
missed.push(one);
|
|
370
|
+
reads.set(one.text, readOf(one, row, reader, excerpt));
|
|
371
|
+
}
|
|
372
|
+
if (fallback !== undefined && missed.length > 0)
|
|
373
|
+
pending.push([fallback, missed.map((one) => ({ ...one, keys: keysOf(fallback, one.reference) }))]);
|
|
374
|
+
}
|
|
375
|
+
return reads;
|
|
376
|
+
};
|
|
377
|
+
const summaryOf = ({ sender, excerpt, ...summary }, withExcerpt) => withExcerpt ? { ...summary, excerpt } : summary;
|
|
378
|
+
export const refSummariesOf = (database, texts, options = {}) => new Map([...refReadsOf(database, texts, options)].map(([text, read]) => [text, summaryOf(read, options.excerpt === true)]));
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import { type Decider } from "../decisions.js";
|
|
2
|
+
import type { RELATION_KINDS } from "./link-kinds.js";
|
|
2
3
|
import type { Author } from "./memories.js";
|
|
3
4
|
import { type Link } from "./notes.js";
|
|
4
5
|
import type { StoreContext } from "./open.js";
|
|
5
6
|
import { type ProposedAction } from "./proposed-actions.js";
|
|
6
7
|
import { type Thing } from "./things.js";
|
|
8
|
+
export interface KnowledgeRelation {
|
|
9
|
+
id: string;
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
kind: (typeof RELATION_KINDS)[number];
|
|
13
|
+
role: string | null;
|
|
14
|
+
/** What a chat is for in the project, from the purposes memo's config lists; `null` means `work`. */
|
|
15
|
+
purpose: string | null;
|
|
16
|
+
evidence: string | null;
|
|
17
|
+
confirmed: boolean;
|
|
18
|
+
provenance: string | null;
|
|
19
|
+
confidenceCategory: "owner-confirmed" | "weak";
|
|
20
|
+
createdAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RelateInput {
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
kind: KnowledgeRelation["kind"];
|
|
26
|
+
role?: string;
|
|
27
|
+
purpose?: string;
|
|
28
|
+
evidence?: string;
|
|
29
|
+
confirmed?: boolean;
|
|
30
|
+
provenance?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const relationOf: (link: Link) => KnowledgeRelation;
|
|
7
33
|
/** `relationship` proposals in `proposed_actions`: an agent's or a rule's, and the rejected ones a scan skips. */
|
|
8
34
|
export declare const relationProposalsOver: (context: StoreContext) => {
|
|
9
35
|
/** The same pending pair returns the proposal already held. */
|
|
@@ -12,10 +38,19 @@ export declare const relationProposalsOver: (context: StoreContext) => {
|
|
|
12
38
|
to: string;
|
|
13
39
|
kind: string;
|
|
14
40
|
role?: string;
|
|
41
|
+
purpose?: string;
|
|
15
42
|
evidence?: string[];
|
|
16
43
|
reason?: string;
|
|
17
44
|
}, by: Exclude<Author, "owner">) => ProposedAction;
|
|
18
45
|
rejected: (link: Link, by: Decider, reason?: string) => void;
|
|
46
|
+
update: (link: Link, input: RelationUpdate) => Link;
|
|
47
|
+
proposeUpdate: (link: Link, input: RelationUpdate & {
|
|
48
|
+
reason?: string;
|
|
49
|
+
}, by: Exclude<Author, "owner">) => ProposedAction;
|
|
50
|
+
decideUpdate: (id: string, decision: LinkDecision) => {
|
|
51
|
+
proposal: ProposedAction;
|
|
52
|
+
link: Link | null;
|
|
53
|
+
};
|
|
19
54
|
/**
|
|
20
55
|
* Decides a stored `relationship` or `key_document` proposal in one transaction: the confirmed link and the
|
|
21
56
|
* decision land together or not at all.
|
|
@@ -27,6 +62,11 @@ export declare const relationProposalsOver: (context: StoreContext) => {
|
|
|
27
62
|
/** Decides an unconfirmed link written before proposals were stored; only the owner may, as its author is unknown. */
|
|
28
63
|
decideLink: (id: string, decision: LinkDecision) => Link | null;
|
|
29
64
|
};
|
|
65
|
+
/** A relationship's role and purpose to set; `null` clears one. */
|
|
66
|
+
export interface RelationUpdate {
|
|
67
|
+
role?: string | null;
|
|
68
|
+
purpose?: string | null;
|
|
69
|
+
}
|
|
30
70
|
export interface LinkDecision {
|
|
31
71
|
confirm: boolean;
|
|
32
72
|
by: Decider;
|