@wirecat/cli-core 0.558.0 → 0.560.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 +60 -0
- package/dist/background/schedules.d.ts +2 -0
- package/dist/cli/effects.d.ts +13 -2
- package/dist/cli/effects.js +23 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/meeting-findings-input.d.ts +3 -0
- package/dist/cli/meeting-findings-input.js +4 -0
- package/dist/cli/messenger/conversation-answers.d.ts +3 -0
- package/dist/cli/messenger/message-answers.d.ts +3 -0
- package/dist/cli/messenger/message-writes.d.ts +6 -0
- package/dist/cli/schedule/schedule-command.d.ts +6 -1
- package/dist/cli/schedule/schedule-command.js +24 -2
- package/dist/domain/records.d.ts +161 -0
- package/dist/domain/records.js +234 -0
- package/dist/entities/contact-context.d.ts +6 -0
- package/dist/entities/conversation-results.d.ts +6 -0
- package/dist/entities/evidence.d.ts +24 -0
- package/dist/entities/index.d.ts +190 -0
- package/dist/entities/index.js +3 -0
- package/dist/entities/listener.d.ts +12 -0
- package/dist/entities/mail-findings.d.ts +116 -0
- package/dist/entities/mail-findings.js +68 -0
- package/dist/entities/message-context.d.ts +9 -0
- package/dist/entities/message-write.d.ts +12 -0
- package/dist/entities/message.d.ts +6 -0
- package/dist/entities/message.js +6 -0
- package/dist/entities/store-records.d.ts +3 -0
- package/dist/errors/reasons.js +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mail/cli/marks.d.ts +1 -0
- package/dist/mail/cli/marks.js +11 -4
- package/dist/mail/cli/review.js +10 -2
- package/dist/mail/findings.d.ts +55 -0
- package/dist/mail/findings.js +140 -0
- package/dist/mail/index.d.ts +2 -1
- package/dist/mail/index.js +2 -1
- package/dist/mail/marks.d.ts +10 -3
- package/dist/mail/marks.js +34 -5
- package/dist/meetings/findings-rules.d.ts +24 -0
- package/dist/meetings/findings-rules.js +136 -116
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +2 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/runs/secrets.d.ts +2 -0
- package/dist/runs/secrets.js +2 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/ingest.d.ts +45 -0
- package/dist/services/ingest.js +80 -0
- package/dist/services/proposals/kinds/mail-findings.d.ts +8 -0
- package/dist/services/proposals/kinds/mail-findings.js +87 -0
- package/dist/services/proposals/registry.js +3 -1
- package/dist/services/proposals/types.d.ts +2 -0
- package/dist/store/decisions.d.ts +1 -1
- package/dist/store/decisions.js +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +1 -0
- package/dist/store/mail-findings.d.ts +51 -0
- package/dist/store/mail-findings.js +1 -0
- package/dist/store/path.d.ts +2 -0
- package/dist/store/path.js +21 -1
- package/dist/store/sqlite/accounts.js +4 -0
- package/dist/store/sqlite/email-activity.d.ts +8 -0
- package/dist/store/sqlite/email-activity.js +31 -5
- package/dist/store/sqlite/findings-core.d.ts +1 -0
- package/dist/store/sqlite/findings-core.js +1 -1
- package/dist/store/sqlite/mail-findings.d.ts +3 -0
- package/dist/store/sqlite/mail-findings.js +333 -0
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/memories.d.ts +4 -1
- package/dist/store/sqlite/memories.js +48 -43
- package/dist/store/sqlite/migrations.generated.js +16 -0
- package/dist/store/sqlite/open-store.d.ts +1 -1
- package/dist/store/sqlite/open-store.js +4 -1
- package/dist/store/sqlite/open.js +2 -1
- package/dist/store/sqlite/proposed-actions.js +2 -1
- package/dist/store/sqlite/records.d.ts +81 -0
- package/dist/store/sqlite/records.js +372 -0
- package/dist/store/sqlite/schema.d.ts +353 -0
- package/dist/store/sqlite/schema.js +37 -0
- package/dist/store/sqlite/tasks.d.ts +2 -0
- package/dist/store/sqlite/tasks.js +26 -24
- package/dist/store/sqlite/test-accounts.d.ts +8 -0
- package/dist/store/sqlite/test-accounts.js +31 -0
- package/dist/store/sqlite/work-tasks.d.ts +1 -0
- package/dist/store/sqlite/work-tasks.js +6 -0
- package/dist/store/store.d.ts +6 -0
- package/dist/store/store.js +10 -0
- package/package.json +4 -2
- package/schemas/records-envelope.v1.schema.json +108 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type IngestError } from "../domain/records.js";
|
|
2
|
+
import type { MessageStore } from "../store/store.js";
|
|
3
|
+
export interface IngestOptions {
|
|
4
|
+
/** The account the caller means; the envelope's `source.account` must be this one. */
|
|
5
|
+
account: string;
|
|
6
|
+
/** Checks and plans everything, then writes nothing. */
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
/** Records per transaction, 1000 by default. */
|
|
9
|
+
chunk?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface IngestReceipt {
|
|
12
|
+
dryRun: boolean;
|
|
13
|
+
source: {
|
|
14
|
+
provider: string;
|
|
15
|
+
account: string;
|
|
16
|
+
batchId: string;
|
|
17
|
+
} | null;
|
|
18
|
+
created: number;
|
|
19
|
+
updated: number;
|
|
20
|
+
unchanged: number;
|
|
21
|
+
deleted: number;
|
|
22
|
+
refused: number;
|
|
23
|
+
/** `invoice:` and `payment:` proposals this import made; the owner confirms them in review. */
|
|
24
|
+
proposed: string[];
|
|
25
|
+
/** Kept, with no entity yet: each named role points at a record not imported, or one with no entity. */
|
|
26
|
+
waiting: {
|
|
27
|
+
key: string;
|
|
28
|
+
pointer: string;
|
|
29
|
+
missing: string[];
|
|
30
|
+
}[];
|
|
31
|
+
/** Changed or deleted at the source after the entity was made from it: the entity was not touched. */
|
|
32
|
+
flagged: {
|
|
33
|
+
key: string;
|
|
34
|
+
pointer: string;
|
|
35
|
+
entity: string;
|
|
36
|
+
}[];
|
|
37
|
+
errors: IngestError[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Takes a v1 records envelope (or `envelopeOfJsonl` of a JSONL file) into the store: checks it, then writes per
|
|
41
|
+
* chunk in one transaction. The same import twice writes nothing new. A record with a lower revision than the stored
|
|
42
|
+
* one is refused, a delete is soft, a reference to a record not yet imported waits for it, and nothing is linked by
|
|
43
|
+
* name. An invoice or a payment becomes a proposal: an import never writes confirmed money.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ingest: (store: Pick<MessageStore, "records">, envelope: unknown, { account, dryRun, chunk }: IngestOptions) => Promise<IngestReceipt>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { envelopeErrors, pointerOf, recordErrors, } from "../domain/records.js";
|
|
2
|
+
const emptyReceipt = (dryRun) => ({
|
|
3
|
+
dryRun,
|
|
4
|
+
source: null,
|
|
5
|
+
created: 0,
|
|
6
|
+
updated: 0,
|
|
7
|
+
unchanged: 0,
|
|
8
|
+
deleted: 0,
|
|
9
|
+
refused: 0,
|
|
10
|
+
proposed: [],
|
|
11
|
+
waiting: [],
|
|
12
|
+
flagged: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Takes a v1 records envelope (or `envelopeOfJsonl` of a JSONL file) into the store: checks it, then writes per
|
|
17
|
+
* chunk in one transaction. The same import twice writes nothing new. A record with a lower revision than the stored
|
|
18
|
+
* one is refused, a delete is soft, a reference to a record not yet imported waits for it, and nothing is linked by
|
|
19
|
+
* name. An invoice or a payment becomes a proposal: an import never writes confirmed money.
|
|
20
|
+
*/
|
|
21
|
+
export const ingest = async (store, envelope, { account, dryRun = false, chunk = 1000 }) => {
|
|
22
|
+
const receipt = emptyReceipt(dryRun);
|
|
23
|
+
const broken = envelopeErrors(envelope, account);
|
|
24
|
+
if (broken.length > 0)
|
|
25
|
+
return { ...receipt, errors: broken };
|
|
26
|
+
const { source, records } = envelope;
|
|
27
|
+
receipt.source = { provider: source.provider, account: source.account, batchId: source.batch_id };
|
|
28
|
+
const taken = [];
|
|
29
|
+
const keys = new Set();
|
|
30
|
+
const identities = new Set();
|
|
31
|
+
for (const [index, record] of records.entries()) {
|
|
32
|
+
const errors = recordErrors(record, index);
|
|
33
|
+
const valid = record;
|
|
34
|
+
if (errors.length === 0 && keys.has(valid.key))
|
|
35
|
+
errors.push({
|
|
36
|
+
pointer: pointerOf("records", index, "key"),
|
|
37
|
+
code: "record.duplicate-key",
|
|
38
|
+
message: "another record of this envelope has this key",
|
|
39
|
+
key: valid.key,
|
|
40
|
+
});
|
|
41
|
+
const identity = errors.length === 0 ? `${valid.type}\0${valid.external_id}` : "";
|
|
42
|
+
if (errors.length === 0 && identities.has(identity))
|
|
43
|
+
errors.push({
|
|
44
|
+
pointer: pointerOf("records", index, "external_id"),
|
|
45
|
+
code: "record.duplicate",
|
|
46
|
+
message: `another record of this envelope is ${valid.type} ${valid.external_id}`,
|
|
47
|
+
key: valid.key,
|
|
48
|
+
});
|
|
49
|
+
if (errors.length > 0) {
|
|
50
|
+
receipt.refused += 1;
|
|
51
|
+
for (const error of errors)
|
|
52
|
+
receipt.errors.push(error);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
keys.add(valid.key);
|
|
56
|
+
identities.add(identity);
|
|
57
|
+
taken.push({ index, record: valid });
|
|
58
|
+
}
|
|
59
|
+
const written = await store.records.write(source, taken, { dryRun, chunk });
|
|
60
|
+
for (const outcome of written.outcomes) {
|
|
61
|
+
receipt[outcome.status] += 1;
|
|
62
|
+
if (outcome.error)
|
|
63
|
+
receipt.errors.push(outcome.error);
|
|
64
|
+
}
|
|
65
|
+
for (const error of written.laterErrors)
|
|
66
|
+
receipt.errors.push(error);
|
|
67
|
+
receipt.errors.sort((a, b) => a.pointer.localeCompare(b.pointer, "en", { numeric: true }));
|
|
68
|
+
receipt.proposed = written.proposed;
|
|
69
|
+
receipt.waiting = written.waiting.map(({ index, key, missing }) => ({
|
|
70
|
+
key,
|
|
71
|
+
pointer: pointerOf("records", index, "relations"),
|
|
72
|
+
missing,
|
|
73
|
+
}));
|
|
74
|
+
receipt.flagged = written.flagged.map(({ index, key, entity }) => ({
|
|
75
|
+
key,
|
|
76
|
+
pointer: pointerOf("records", index),
|
|
77
|
+
entity,
|
|
78
|
+
}));
|
|
79
|
+
return receipt;
|
|
80
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type MessageStore } from "../../../store/index.js";
|
|
2
|
+
import type { ProposalKind } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* What an agent read in new mail, one batch per mailbox and run. Only the owner decides it (NEED-2187): an agent's
|
|
5
|
+
* confirmation is refused whatever `agent_decisions` says. Confirming accepts the parts named, or every finding not
|
|
6
|
+
* rejected before; the rest are rejected.
|
|
7
|
+
*/
|
|
8
|
+
export declare const mailFindingsKind: (store: Pick<MessageStore, "mailFindings" | "proposedActions">) => ProposalKind;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { MAIL_FINDINGS_KIND } from "../../../store/index.js";
|
|
2
|
+
import { kept, proposerOfAuthor } from "./shared.js";
|
|
3
|
+
/** A finding's one line in the inbox; never the email's text, only what the agent wrote. */
|
|
4
|
+
const summaryOf = (finding) => {
|
|
5
|
+
switch (finding.kind) {
|
|
6
|
+
case "task":
|
|
7
|
+
return finding.title;
|
|
8
|
+
case "fact":
|
|
9
|
+
return finding.statement;
|
|
10
|
+
case "task_update":
|
|
11
|
+
return `${finding.change} ${finding.task}: ${finding.reason}`;
|
|
12
|
+
case "memory":
|
|
13
|
+
return finding.body.slice(0, 200);
|
|
14
|
+
case "link":
|
|
15
|
+
return `${finding.from} → ${finding.to}`;
|
|
16
|
+
case "new_contact":
|
|
17
|
+
return `${finding.name} <${finding.address}>`;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const rowOf = (batch, createdAt) => {
|
|
21
|
+
const findings = batch.input.findings;
|
|
22
|
+
const outcome = (id) => batch.receipt?.items.find((item) => item.id === id)?.status ?? null;
|
|
23
|
+
const project = findings.flatMap((finding) => ("project" in finding && finding.project ? [finding.project] : []))[0] ?? null;
|
|
24
|
+
return {
|
|
25
|
+
id: batch.id,
|
|
26
|
+
kind: "mail_findings",
|
|
27
|
+
status: batch.status === "proposed" ? "proposed" : batch.receipt?.accepted.length ? "confirmed" : "rejected",
|
|
28
|
+
project,
|
|
29
|
+
subject: `findings:${batch.id}`,
|
|
30
|
+
object: null,
|
|
31
|
+
summary: `${findings.length} mail finding${findings.length === 1 ? "" : "s"} for ${batch.account.account}`,
|
|
32
|
+
proposedBy: proposerOfAuthor(batch.author),
|
|
33
|
+
reason: null,
|
|
34
|
+
evidence: [...new Set(findings.flatMap((finding) => finding.evidence.map((one) => one.reference)))].slice(0, 5),
|
|
35
|
+
score: null,
|
|
36
|
+
signals: null,
|
|
37
|
+
parts: findings.map((finding) => ({
|
|
38
|
+
id: finding.id,
|
|
39
|
+
kind: finding.kind,
|
|
40
|
+
summary: summaryOf(finding),
|
|
41
|
+
outcome: outcome(finding.id),
|
|
42
|
+
})),
|
|
43
|
+
createdAt,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* What an agent read in new mail, one batch per mailbox and run. Only the owner decides it (NEED-2187): an agent's
|
|
48
|
+
* confirmation is refused whatever `agent_decisions` says. Confirming accepts the parts named, or every finding not
|
|
49
|
+
* rejected before; the rest are rejected.
|
|
50
|
+
*/
|
|
51
|
+
export const mailFindingsKind = (store) => {
|
|
52
|
+
const created = async (batch) => rowOf(batch, (await store.proposedActions.get(batch.id)).createdAt);
|
|
53
|
+
return {
|
|
54
|
+
kind: "mail_findings",
|
|
55
|
+
stored: [MAIL_FINDINGS_KIND],
|
|
56
|
+
parts: true,
|
|
57
|
+
ownerOnly: true,
|
|
58
|
+
list: async (filter) => {
|
|
59
|
+
const status = filter.status ?? "proposed";
|
|
60
|
+
const rows = [];
|
|
61
|
+
for (let offset = 0; rows.length < filter.limit; offset += 50) {
|
|
62
|
+
const page = await store.mailFindings.list({
|
|
63
|
+
...(status === "proposed" ? { status: "proposed" } : {}),
|
|
64
|
+
...(status === "confirmed" || status === "rejected" ? { status: "reviewed" } : {}),
|
|
65
|
+
limit: 50,
|
|
66
|
+
offset,
|
|
67
|
+
});
|
|
68
|
+
for (const row of kept(await Promise.all(page.items.map(created)), { ...filter, limit: 50 }))
|
|
69
|
+
rows.push(row);
|
|
70
|
+
if (!page.hasMore)
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return kept(rows, filter);
|
|
74
|
+
},
|
|
75
|
+
get: async (id) => created(await store.mailFindings.get(id)),
|
|
76
|
+
decide: async (id, { confirm, by, parts }) => {
|
|
77
|
+
const batch = await store.mailFindings.get(id);
|
|
78
|
+
const turnedDown = new Set(batch.previous.filter((one) => one.status === "rejected").map((one) => one.id));
|
|
79
|
+
const accept = !confirm
|
|
80
|
+
? []
|
|
81
|
+
: (parts ?? batch.input.findings.map((finding) => finding.id).filter((one) => !turnedDown.has(one)));
|
|
82
|
+
const reviewed = await store.mailFindings.review(id, { fingerprint: batch.fingerprint, accept, by });
|
|
83
|
+
const made = (reviewed.receipt?.items ?? []).flatMap((item) => item.status === "accepted" && item.reference && !item.reused ? [item.reference] : []);
|
|
84
|
+
return { id, kind: "mail_findings", status: accept.length ? "confirmed" : "rejected", made };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -5,6 +5,7 @@ import { factKind } from "./kinds/fact.js";
|
|
|
5
5
|
import { invoiceKind, paymentKind } from "./kinds/finance.js";
|
|
6
6
|
import { findingsKind } from "./kinds/findings.js";
|
|
7
7
|
import { keyDocumentKind } from "./kinds/key-document.js";
|
|
8
|
+
import { mailFindingsKind } from "./kinds/mail-findings.js";
|
|
8
9
|
import { memoryKind } from "./kinds/memory.js";
|
|
9
10
|
import { relationshipKind } from "./kinds/relationship.js";
|
|
10
11
|
import { relationshipUpdateKind } from "./kinds/relationship-update.js";
|
|
@@ -98,7 +99,7 @@ export const emptyProposalRegistry = (store) => {
|
|
|
98
99
|
});
|
|
99
100
|
continue;
|
|
100
101
|
}
|
|
101
|
-
const refusal = mayDecide(row, decision.by, policy);
|
|
102
|
+
const refusal = mayDecide(kind.ownerOnly ? { ...row, ownerOnly: true } : row, decision.by, policy);
|
|
102
103
|
if (refusal) {
|
|
103
104
|
refused.push(refusal);
|
|
104
105
|
continue;
|
|
@@ -127,6 +128,7 @@ export const proposalRegistry = (store) => {
|
|
|
127
128
|
paymentKind(store),
|
|
128
129
|
memoryKind(store),
|
|
129
130
|
findingsKind(store),
|
|
131
|
+
mailFindingsKind(store),
|
|
130
132
|
])
|
|
131
133
|
registry.register(kind);
|
|
132
134
|
registry.register(actionKind(store, registry.claimed));
|
|
@@ -64,6 +64,8 @@ export interface ProposalKind {
|
|
|
64
64
|
readonly stored?: readonly string[];
|
|
65
65
|
/** Whether its proposals have parts decided on their own, as a findings batch has findings. */
|
|
66
66
|
readonly parts?: boolean;
|
|
67
|
+
/** Only the owner decides it, whatever the host's `agent_decisions` says. */
|
|
68
|
+
readonly ownerOnly?: boolean;
|
|
67
69
|
/** Bounded: at most `offset + limit` rows, newest first. */
|
|
68
70
|
list(filter: ProposalFilter): Promise<ProposalRow[]>;
|
|
69
71
|
get(id: string): Promise<ProposalRow>;
|
|
@@ -22,7 +22,7 @@ export type Decider = "owner" | {
|
|
|
22
22
|
reason: string;
|
|
23
23
|
};
|
|
24
24
|
export interface DecisionPolicy {
|
|
25
|
-
/** The host's `agent_decisions`: `
|
|
25
|
+
/** The host's `agent_decisions`: off unless the owner sets it to `true`; off keeps every decision with the owner. */
|
|
26
26
|
agentDecisions: boolean;
|
|
27
27
|
/** A rule scorer's proposal is the owner's alone to decide. */
|
|
28
28
|
ruleProposalsOwnerOnly: boolean;
|
package/dist/store/decisions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
export const DEFAULT_DECISION_POLICY = { agentDecisions:
|
|
2
|
+
export const DEFAULT_DECISION_POLICY = { agentDecisions: false, ruleProposalsOwnerOnly: true };
|
|
3
3
|
/**
|
|
4
4
|
* The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
|
|
5
5
|
* Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
|
package/dist/store/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type { CacheDatabase, CacheStatement, OpenDatabase, SqlValue } from "./dr
|
|
|
16
16
|
export { PRAGMAS } from "./driver.js";
|
|
17
17
|
export { EVIDENCE_FINDINGS_KIND, type EvidenceFindingsBatch, type EvidenceFindingsDecider, type EvidenceFindingsReceipt, type EvidenceFindingsStore, validateEvidenceFindings, } from "./evidence-findings.js";
|
|
18
18
|
export { ORGANIZATIONS_QUERY, organization, PROJECTS_QUERY, project, REMINDERS_QUERY, reminder, } from "./knowledge-query.js";
|
|
19
|
+
export { MAIL_FINDINGS_KIND, type MailFindingsBatch, type MailFindingsStore } from "./mail-findings.js";
|
|
19
20
|
export type { MeetingFindingsBatch, MeetingFindingsStore } from "./meeting-findings.js";
|
|
20
21
|
export { MIGRATIONS, type Migration, migrate } from "./migrations.js";
|
|
21
22
|
export { openCache } from "./open.js";
|
|
@@ -54,6 +55,7 @@ export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, typ
|
|
|
54
55
|
export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
|
|
55
56
|
export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
|
|
56
57
|
export { PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
|
|
58
|
+
export type { RecordKey, RecordOutcome, RecordStore, RecordsWritten, SourceRecord, } from "./sqlite/records.js";
|
|
57
59
|
export type { LinkDecision, RelationUpdate } from "./sqlite/relation-proposals.js";
|
|
58
60
|
export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchIndexState, searchIndexState, } from "./sqlite/search-index.js";
|
|
59
61
|
export type { SyncStateSave } from "./sqlite/sync.js";
|
package/dist/store/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { applyDocumentTransfer, previewDocumentTransfer } from "./document-trans
|
|
|
8
8
|
export { PRAGMAS } from "./driver.js";
|
|
9
9
|
export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
|
|
10
10
|
export { ORGANIZATIONS_QUERY, organization, PROJECTS_QUERY, project, REMINDERS_QUERY, reminder, } from "./knowledge-query.js";
|
|
11
|
+
export { MAIL_FINDINGS_KIND } from "./mail-findings.js";
|
|
11
12
|
export { MIGRATIONS, migrate } from "./migrations.js";
|
|
12
13
|
export { openCache } from "./open.js";
|
|
13
14
|
export { storePath } from "./path.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MailFindingsInput } from "../mail/findings.js";
|
|
2
|
+
import type { FindingOutcome, FindingsReceipt } from "../meetings/index.js";
|
|
3
|
+
import type { Decider } from "./decisions.js";
|
|
4
|
+
import type { AccountKey } from "./types.js";
|
|
5
|
+
export declare const MAIL_FINDINGS_KIND = "mail_findings";
|
|
6
|
+
export interface MailFindingsBatch {
|
|
7
|
+
id: string;
|
|
8
|
+
account: AccountKey;
|
|
9
|
+
input: MailFindingsInput;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
status: "proposed" | "reviewed";
|
|
12
|
+
author: {
|
|
13
|
+
type: "person" | "bot";
|
|
14
|
+
id: string;
|
|
15
|
+
name: string | null;
|
|
16
|
+
kind: "agent" | "rule" | null;
|
|
17
|
+
};
|
|
18
|
+
decidedBy: {
|
|
19
|
+
type: "person" | "bot";
|
|
20
|
+
id: string;
|
|
21
|
+
} | null;
|
|
22
|
+
decidedAt: string | null;
|
|
23
|
+
previous: FindingOutcome[];
|
|
24
|
+
receipt: FindingsReceipt | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What an agent read in new mail — tasks, task updates, facts, memories, links, new contacts — cited and staged for
|
|
28
|
+
* the owner, one batch per mailbox and run. Only the owner decides one: an agent's mail proposal never becomes the
|
|
29
|
+
* owner's word without him.
|
|
30
|
+
*/
|
|
31
|
+
export interface MailFindingsStore {
|
|
32
|
+
stage(account: AccountKey, input: MailFindingsInput, agent: {
|
|
33
|
+
bot: string;
|
|
34
|
+
model?: string;
|
|
35
|
+
}): Promise<MailFindingsBatch>;
|
|
36
|
+
get(id: string): Promise<MailFindingsBatch>;
|
|
37
|
+
list(options?: {
|
|
38
|
+
account?: AccountKey;
|
|
39
|
+
status?: "proposed" | "reviewed";
|
|
40
|
+
limit?: number;
|
|
41
|
+
offset?: number;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
items: MailFindingsBatch[];
|
|
44
|
+
hasMore: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
review(id: string, input: {
|
|
47
|
+
fingerprint: string;
|
|
48
|
+
accept: string[];
|
|
49
|
+
by: Decider;
|
|
50
|
+
}): Promise<MailFindingsBatch>;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAIL_FINDINGS_KIND = "mail_findings";
|
package/dist/store/path.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export declare const wirecatDirectory: (env?: NodeJS.ProcessEnv, platform?: Node
|
|
|
6
6
|
* before this one: a build still installed would open that name and run its own migrations over these tables.
|
|
7
7
|
*/
|
|
8
8
|
export declare const storePath: (env?: NodeJS.ProcessEnv) => string;
|
|
9
|
+
/** Throws when a test is about to open a file in the owner's real WireCat folder. */
|
|
10
|
+
export declare const refuseOwnerStoreInTests: (file: string, env?: NodeJS.ProcessEnv, home?: string | undefined) => void;
|
package/dist/store/path.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { userInfo } from "node:os";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
3
|
import { resolvePaths } from "@wirecat/cli-base";
|
|
3
4
|
/** The folder the CLIs share: the store, its backups and the models. `WIRECAT_STATE_DIR` moves all of it. */
|
|
4
5
|
export const wirecatDirectory = (env = process.env, platform) => resolvePaths({ appName: "WireCat", prefix: "WIRECAT", env, ...(platform ? { platform } : {}) }).state;
|
|
@@ -8,3 +9,22 @@ export const wirecatDirectory = (env = process.env, platform) => resolvePaths({
|
|
|
8
9
|
* before this one: a build still installed would open that name and run its own migrations over these tables.
|
|
9
10
|
*/
|
|
10
11
|
export const storePath = (env = process.env) => env.MESSAGING_STORE ?? join(wirecatDirectory(env), "wirecat.db");
|
|
12
|
+
// The account's home from the user database, not HOME: the test sandbox moves HOME, and a worker thread's
|
|
13
|
+
// `os.homedir()` still answers the real one (the 2026-10-18 leak).
|
|
14
|
+
const accountHome = () => {
|
|
15
|
+
try {
|
|
16
|
+
return userInfo().homedir;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** Throws when a test is about to open a file in the owner's real WireCat folder. */
|
|
23
|
+
export const refuseOwnerStoreInTests = (file, env = process.env, home = accountHome()) => {
|
|
24
|
+
if (!env.VITEST || !home || file === ":memory:")
|
|
25
|
+
return;
|
|
26
|
+
const inside = relative(wirecatDirectory({ HOME: home, USERPROFILE: home }), resolve(file));
|
|
27
|
+
if (inside.startsWith("..") || isAbsolute(inside))
|
|
28
|
+
return;
|
|
29
|
+
throw new Error(`a test tried to open ${file}, in the owner's real WireCat folder; give it a MESSAGING_STORE or HOME of its own`);
|
|
30
|
+
};
|
|
@@ -114,6 +114,10 @@ export const purgeAccount = ({ orm, database }, accountKey) => {
|
|
|
114
114
|
// The inbox project and its tasks are the owner's; only the tie to the account goes.
|
|
115
115
|
orm.update(projects).set({ accountId: null }).where(eq(projects.accountId, accountKey)).run();
|
|
116
116
|
orm.run(sql `DELETE FROM files WHERE account_id = ${accountKey}`);
|
|
117
|
+
// An entity made from a record is the owner's and stays; only what the source said goes.
|
|
118
|
+
const recordsOf = sql `(SELECT id FROM records WHERE account_id = ${accountKey})`;
|
|
119
|
+
orm.run(sql `DELETE FROM record_relations WHERE record_id IN ${recordsOf} OR target_id IN ${recordsOf}`);
|
|
120
|
+
orm.run(sql `DELETE FROM records WHERE account_id = ${accountKey}`);
|
|
117
121
|
orm.delete(accounts).where(eq(accounts.id, accountKey)).run();
|
|
118
122
|
dropOrphanPeople(database, unseenBefore);
|
|
119
123
|
dropUnusedVectors(database, hashes);
|
|
@@ -35,6 +35,12 @@ export interface EmailMarks {
|
|
|
35
35
|
listId: string | null;
|
|
36
36
|
/** The mail rule that decided how it is stored, by its id in `rules list`. */
|
|
37
37
|
ruleId: number | null;
|
|
38
|
+
/** What a finding citing this email gives as its evidence fingerprint: a hash of its stored subject and text. */
|
|
39
|
+
fingerprint: string;
|
|
40
|
+
/** No earlier stored email, in any account, came from this address. */
|
|
41
|
+
newSender: boolean;
|
|
42
|
+
/** How many identities the sender's person has; 1 is this address alone. 0 when the sender is unknown. */
|
|
43
|
+
senderIdentities: number;
|
|
38
44
|
}
|
|
39
45
|
/** One email as a processing mark sees it, by its store id; never its text. */
|
|
40
46
|
export interface MarkedEmail {
|
|
@@ -52,6 +58,8 @@ export interface MarkedEmail {
|
|
|
52
58
|
/** Stored from its first bytes, not whole yet. */
|
|
53
59
|
partial: boolean;
|
|
54
60
|
draft: boolean;
|
|
61
|
+
/** Its sender's mail looks like spam, a scam or a robot (the sender score). */
|
|
62
|
+
demoted: boolean;
|
|
55
63
|
}
|
|
56
64
|
export interface MailActivityStore {
|
|
57
65
|
/**
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { contextEvidenceFingerprint } from "../../context/evidence.js";
|
|
2
|
+
import { formatLocator } from "../../domain/locator.js";
|
|
1
3
|
import { int, str } from "./events.js";
|
|
2
4
|
/** `tasks agenda` fields over `threadActivity`'s threads: an email thread counts as a dialog. */
|
|
3
5
|
export const MAIL_AGENDA_COLUMNS = { chat_kind: { type: "enum", sql: "'dialog'" } };
|
|
@@ -50,18 +52,39 @@ export const threadEmailIds = (database, accountId, thread, { after, until } = {
|
|
|
50
52
|
ORDER BY coalesce(e.sent_at, e.received_at), e.id`)
|
|
51
53
|
.all(accountId, thread, after ?? null, until ?? null)
|
|
52
54
|
.map(({ id }) => Number(id));
|
|
55
|
+
// The subject and text are read only to hash them as `contextRecordOf` does, so a cited fingerprint matches.
|
|
53
56
|
const MARKS = `
|
|
54
57
|
SELECT e.mail_rule_id, coalesce(json_extract(e.headers, '$."list-id"'), json_extract(e.metadata, '$.listId')) AS list_id,
|
|
55
58
|
EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
56
|
-
WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk
|
|
57
|
-
|
|
59
|
+
WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk,
|
|
60
|
+
a.provider, a.external_id AS account, t.external_id AS thread, e.subject, coalesce(e.body_text, '') AS body,
|
|
61
|
+
e.from_identity_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM emails f
|
|
62
|
+
WHERE f.from_identity_id = e.from_identity_id AND f.id < e.id AND f.deleted_at IS NULL) AS new_sender,
|
|
63
|
+
(SELECT count(*) FROM identity_links o WHERE o.person_id =
|
|
64
|
+
(SELECT l.person_id FROM identity_links l WHERE l.identity_id = e.from_identity_id)) AS identities
|
|
65
|
+
FROM emails e JOIN accounts a ON a.id = e.account_id JOIN email_threads t ON t.id = e.email_thread_id
|
|
66
|
+
WHERE e.account_id = ? AND e.external_id = ? AND e.deleted_at IS NULL`;
|
|
58
67
|
export const emailMarks = (database, accountId, externalIds) => {
|
|
59
68
|
const one = database.prepare(MARKS);
|
|
60
69
|
const marks = new Map();
|
|
61
70
|
for (const id of externalIds) {
|
|
62
71
|
const row = one.get(accountId, id);
|
|
63
|
-
if (row)
|
|
64
|
-
|
|
72
|
+
if (!row)
|
|
73
|
+
continue;
|
|
74
|
+
const ref = formatLocator({
|
|
75
|
+
provider: String(row.provider),
|
|
76
|
+
account: String(row.account),
|
|
77
|
+
chat: String(row.thread),
|
|
78
|
+
message: id,
|
|
79
|
+
});
|
|
80
|
+
marks.set(id, {
|
|
81
|
+
bulk: Boolean(row.bulk),
|
|
82
|
+
listId: str(row.list_id),
|
|
83
|
+
ruleId: int(row.mail_rule_id),
|
|
84
|
+
fingerprint: contextEvidenceFingerprint({ ref, title: str(row.subject), text: String(row.body), revision: null }),
|
|
85
|
+
newSender: Boolean(row.new_sender),
|
|
86
|
+
senderIdentities: Number(row.identities ?? 0),
|
|
87
|
+
});
|
|
65
88
|
}
|
|
66
89
|
return marks;
|
|
67
90
|
};
|
|
@@ -72,7 +95,9 @@ const MARKED = `
|
|
|
72
95
|
OR EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
73
96
|
WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk,
|
|
74
97
|
EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
75
|
-
WHERE m.email_id = e.id AND b.kind = 'envelope') AS envelope
|
|
98
|
+
WHERE m.email_id = e.id AND b.kind = 'envelope') AS envelope,
|
|
99
|
+
EXISTS (SELECT 1 FROM account_identities ai WHERE ai.account_id = e.account_id
|
|
100
|
+
AND ai.identity_id = e.from_identity_id AND ai.mail_demoted = 1) AS demoted
|
|
76
101
|
FROM emails e JOIN email_threads t ON t.id = e.email_thread_id`;
|
|
77
102
|
const markedOf = (row) => ({
|
|
78
103
|
id: Number(row.id),
|
|
@@ -85,6 +110,7 @@ const markedOf = (row) => ({
|
|
|
85
110
|
envelope: Boolean(row.envelope),
|
|
86
111
|
partial: Boolean(row.partial),
|
|
87
112
|
draft: Boolean(row.draft),
|
|
113
|
+
demoted: Boolean(row.demoted),
|
|
88
114
|
});
|
|
89
115
|
export const emailsAfter = (database, accountId, afterId, limit, ids = []) => {
|
|
90
116
|
const next = database
|
|
@@ -16,4 +16,5 @@ export declare const findingsCoreOver: (context: StoreContext, kind: string) =>
|
|
|
16
16
|
snapshotParts: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string | null, seen: Map<string, unknown>) => void) => Record<string, string>;
|
|
17
17
|
previous: (account: number | null, fingerprint: string) => FindingOutcome | undefined;
|
|
18
18
|
outcomes: (account: number | null, input: FindingsInput, accepted: string[], agent: FindingsAgent, decidedBy: Decider) => FindingOutcome[];
|
|
19
|
+
create: (finding: Finding, agent: FindingsAgent, decidedBy: Decider) => string;
|
|
19
20
|
};
|