@wirecat/cli-core 0.549.0 → 0.551.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 +223 -22
- package/THIRD_PARTY_NOTICES +41 -0
- package/dist/calendar/ics.d.ts +1 -1
- package/dist/calendar/ics.js +5 -2
- package/dist/calendar/windows-zones.generated.d.ts +1 -0
- package/dist/calendar/windows-zones.generated.js +146 -0
- package/dist/cli/compact.js +2 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/meeting-findings-input.js +4 -3
- package/dist/cli/messenger/backfill-command.js +28 -1
- package/dist/cli/messenger/context.js +1 -0
- package/dist/cli/messenger/history-gaps.js +4 -4
- package/dist/cli/messenger/mcp-command.js +1 -0
- package/dist/cli/messenger/messages-delete-command.js +7 -8
- package/dist/cli/messenger/projects-command.js +47 -2
- package/dist/cli/payment-query.d.ts +8 -0
- package/dist/cli/payment-query.js +27 -0
- package/dist/cli/project-status-rows.d.ts +8 -0
- package/dist/cli/project-status-rows.js +102 -0
- package/dist/cli/project-status.d.ts +132 -0
- package/dist/cli/project-status.js +248 -0
- package/dist/cli/steps.js +4 -1
- package/dist/domain/knowledge.d.ts +1 -1
- package/dist/domain/knowledge.js +1 -1
- package/dist/domain/status-words.d.ts +3 -0
- package/dist/domain/status-words.js +44 -0
- package/dist/entities/fact.d.ts +12 -1
- package/dist/entities/fact.js +13 -0
- package/dist/entities/index.d.ts +18 -2
- package/dist/entities/meeting-findings.d.ts +1 -1
- package/dist/entities/message-transfer.d.ts +16 -0
- package/dist/entities/message-transfer.js +6 -0
- package/dist/errors/reasons.js +1 -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/mcp/app-server.js +1 -0
- package/dist/mcp/http/oauth.d.ts +2 -1
- package/dist/mcp/http/oauth.js +2 -2
- package/dist/mcp/http/serve.d.ts +2 -0
- package/dist/mcp/http/serve.js +2 -1
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/surface.js +1 -1
- package/dist/meetings/findings-rules.d.ts +14 -3
- package/dist/meetings/findings-rules.js +16 -2
- package/dist/meetings/index.d.ts +3 -2
- package/dist/meetings/index.js +2 -1
- package/dist/meetings/meeting-context.d.ts +2 -3
- package/dist/meetings/meeting-context.js +3 -4
- package/dist/meetings/payment.d.ts +27 -0
- package/dist/meetings/payment.js +81 -0
- package/dist/services/archive.d.ts +13 -0
- package/dist/services/archive.js +56 -5
- package/dist/services/deps.d.ts +3 -0
- package/dist/services/history-limits.d.ts +2 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +4 -1
- package/dist/services/job-query.d.ts +5 -2
- package/dist/services/job-query.js +5 -2
- package/dist/services/link-scorers/alias-hits.d.ts +7 -0
- package/dist/services/link-scorers/alias-hits.js +54 -0
- package/dist/services/link-scorers/chat-senders.d.ts +6 -0
- package/dist/services/link-scorers/chat-senders.js +56 -0
- package/dist/services/link-scorers/chat-title.d.ts +6 -0
- package/dist/services/link-scorers/chat-title.js +44 -0
- package/dist/services/link-scorers/email-domain.d.ts +6 -0
- package/dist/services/link-scorers/email-domain.js +64 -0
- package/dist/services/link-scorers/index.d.ts +9 -0
- package/dist/services/link-scorers/index.js +8 -0
- package/dist/services/link-scorers/mail-thread.d.ts +3 -0
- package/dist/services/link-scorers/mail-thread.js +74 -0
- package/dist/services/link-scorers/member-chats.d.ts +3 -0
- package/dist/services/link-scorers/member-chats.js +40 -0
- package/dist/services/link-scorers/member-dialog.d.ts +3 -0
- package/dist/services/link-scorers/member-dialog.js +34 -0
- package/dist/services/link-scorers/scan.d.ts +41 -0
- package/dist/services/link-scorers/scan.js +244 -0
- package/dist/services/link-scorers/shared.d.ts +26 -0
- package/dist/services/link-scorers/shared.js +48 -0
- package/dist/services/link-scorers/types.d.ts +77 -0
- package/dist/services/link-scorers/types.js +1 -0
- package/dist/services/project-status/index.d.ts +71 -0
- package/dist/services/project-status/index.js +196 -0
- package/dist/services/project-status/items.d.ts +10 -0
- package/dist/services/project-status/items.js +23 -0
- package/dist/services/project-status/signals/amounts.d.ts +20 -0
- package/dist/services/project-status/signals/amounts.js +91 -0
- package/dist/services/project-status/signals/children.d.ts +13 -0
- package/dist/services/project-status/signals/children.js +28 -0
- package/dist/services/project-status/signals/coverage.d.ts +13 -0
- package/dist/services/project-status/signals/coverage.js +30 -0
- package/dist/services/project-status/signals/index.d.ts +3 -0
- package/dist/services/project-status/signals/index.js +21 -0
- package/dist/services/project-status/signals/last-message.d.ts +4 -0
- package/dist/services/project-status/signals/last-message.js +22 -0
- package/dist/services/project-status/signals/payments.d.ts +9 -0
- package/dist/services/project-status/signals/payments.js +24 -0
- package/dist/services/project-status/signals/promises.d.ts +17 -0
- package/dist/services/project-status/signals/promises.js +109 -0
- package/dist/services/project-status/signals/questions.d.ts +8 -0
- package/dist/services/project-status/signals/questions.js +38 -0
- package/dist/services/project-status/signals/reply-next.d.ts +19 -0
- package/dist/services/project-status/signals/reply-next.js +75 -0
- package/dist/services/project-status/signals/topics.d.ts +10 -0
- package/dist/services/project-status/signals/topics.js +50 -0
- package/dist/services/project-status/types.d.ts +84 -0
- package/dist/services/project-status/types.js +1 -0
- package/dist/services/proposals/index.d.ts +1 -0
- package/dist/services/proposals/index.js +1 -0
- package/dist/services/proposals/kinds/findings.d.ts +3 -0
- package/dist/services/proposals/kinds/findings.js +8 -1
- package/dist/store/decisions.d.ts +5 -1
- package/dist/store/decisions.js +5 -1
- package/dist/store/evidence-findings.js +1 -0
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +1 -0
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- 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-findings.js +5 -1
- package/dist/store/sqlite/facts.d.ts +13 -0
- package/dist/store/sqlite/facts.js +64 -6
- package/dist/store/sqlite/findings-core.js +14 -1
- package/dist/store/sqlite/knowledge.d.ts +16 -2
- package/dist/store/sqlite/knowledge.js +1 -0
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/dist/store/sqlite/relation-proposals.d.ts +29 -2
- package/dist/store/sqlite/relation-proposals.js +75 -4
- package/dist/store/store.d.ts +1 -0
- package/package.json +3 -3
- package/parity.json +11 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Message } from "../../domain/models.js";
|
|
2
|
+
import type { StatusChat, StatusItem, StatusWho } from "./types.js";
|
|
3
|
+
/** The most of a message's text any status item carries. */
|
|
4
|
+
export declare const EXCERPT_CHARS = 80;
|
|
5
|
+
export declare const excerpt: (text: string) => string;
|
|
6
|
+
export declare const messageRef: (chat: StatusChat, message: Pick<Message, "id">) => string;
|
|
7
|
+
export declare const evidenceOf: (chat: StatusChat, ...messages: Pick<Message, "id">[]) => StatusItem["evidence"];
|
|
8
|
+
export declare const whoOf: (message: Pick<Message, "outgoing" | "senderName">) => StatusWho;
|
|
9
|
+
export declare const fromOwner: (message: Pick<Message, "outgoing">) => boolean;
|
|
10
|
+
export declare const day: (at: number | string) => string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { formatLocator } from "../../domain/locator.js";
|
|
2
|
+
/** The most of a message's text any status item carries. */
|
|
3
|
+
export const EXCERPT_CHARS = 80;
|
|
4
|
+
export const excerpt = (text) => {
|
|
5
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
6
|
+
const letters = Array.from(flat);
|
|
7
|
+
return letters.length <= EXCERPT_CHARS ? flat : `${letters.slice(0, EXCERPT_CHARS - 1).join("")}…`;
|
|
8
|
+
};
|
|
9
|
+
export const messageRef = (chat, message) => formatLocator({
|
|
10
|
+
provider: chat.account.provider,
|
|
11
|
+
account: chat.account.account,
|
|
12
|
+
chat: chat.chatId,
|
|
13
|
+
message: message.id,
|
|
14
|
+
});
|
|
15
|
+
export const evidenceOf = (chat, ...messages) => messages.slice(0, 8).map((message) => ({ ref: messageRef(chat, message), fingerprint: null }));
|
|
16
|
+
// Person refs wait for the shared ref summaries; until then the sender's stored name is the label.
|
|
17
|
+
export const whoOf = (message) => ({
|
|
18
|
+
ref: null,
|
|
19
|
+
name: message.outgoing === true ? null : message.senderName,
|
|
20
|
+
owner: message.outgoing === true,
|
|
21
|
+
});
|
|
22
|
+
export const fromOwner = (message) => message.outgoing === true;
|
|
23
|
+
export const day = (at) => new Date(at).toISOString().slice(0, 10);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export type MoneyDirection = "to_us" | "from_us" | "unknown";
|
|
3
|
+
export interface AmountsData {
|
|
4
|
+
/** Decimal as text, two places: money is never a float. */
|
|
5
|
+
amount: string;
|
|
6
|
+
currency: string | null;
|
|
7
|
+
direction: MoneyDirection;
|
|
8
|
+
/** `paid-mentioned` is a message saying so, never a payment the owner recorded. */
|
|
9
|
+
state: "open" | "paid-mentioned" | "mentioned";
|
|
10
|
+
/** The first ask, or the first mention when nobody asked. */
|
|
11
|
+
since: string;
|
|
12
|
+
lastAsked: string | null;
|
|
13
|
+
asks: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const centsText: (cents: number) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Every message with an amount and a money word, folded by amount and currency per chat: the first ask is `since`, a
|
|
18
|
+
* later "paid" message naming the same amount makes it `paid-mentioned`. Instalments are not summed into a total.
|
|
19
|
+
*/
|
|
20
|
+
export declare const amounts: StatusSignal<AmountsData>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { MONEY_DONE, MONEY_REQUEST } from "../../../domain/money-words.js";
|
|
2
|
+
import { MONEY_DONE_FORMS } from "../../../domain/status-words.js";
|
|
3
|
+
import { DEFAULT_TASK_CATEGORIES } from "../../../domain/task-categories.js";
|
|
4
|
+
import { amountsOf } from "../../../search/task-signature.js";
|
|
5
|
+
import { moneyClassesOf } from "../../money-words.js";
|
|
6
|
+
import { listsHit } from "../../task-categories.js";
|
|
7
|
+
import { evidenceOf, fromOwner, whoOf } from "../items.js";
|
|
8
|
+
const MONEY = DEFAULT_TASK_CATEGORIES.filter((category) => category.name === "money");
|
|
9
|
+
// Past forms first: their stem is also a request's (`оплатил` and `оплати` → `оплат`).
|
|
10
|
+
const verbOf = (text) => {
|
|
11
|
+
if (MONEY_DONE_FORMS.test(text))
|
|
12
|
+
return "done";
|
|
13
|
+
const classes = moneyClassesOf(text);
|
|
14
|
+
if (classes.includes(MONEY_DONE))
|
|
15
|
+
return "done";
|
|
16
|
+
return classes.includes(MONEY_REQUEST) ? "request" : "none";
|
|
17
|
+
};
|
|
18
|
+
const roleDirection = (role) => {
|
|
19
|
+
const named = role?.trim().toLowerCase();
|
|
20
|
+
return named === "client" ? "to_us" : named === "contractor" ? "from_us" : "unknown";
|
|
21
|
+
};
|
|
22
|
+
/** The sender's side decides: a stem cannot tell an ask from a report, so who wrote it is the evidence. */
|
|
23
|
+
const directionOf = (verb, owner, role) => {
|
|
24
|
+
if (verb === "request")
|
|
25
|
+
return owner ? "to_us" : "from_us";
|
|
26
|
+
if (verb === "done")
|
|
27
|
+
return owner ? "from_us" : "to_us";
|
|
28
|
+
return roleDirection(role);
|
|
29
|
+
};
|
|
30
|
+
const rowsOf = (chat) => chat.messages.flatMap((message) => {
|
|
31
|
+
const found = amountsOf(message.text);
|
|
32
|
+
if (!found.length)
|
|
33
|
+
return [];
|
|
34
|
+
const verb = verbOf(message.text);
|
|
35
|
+
if (verb === "none" && !listsHit(message.text, MONEY).length)
|
|
36
|
+
return [];
|
|
37
|
+
const direction = directionOf(verb, fromOwner(message), chat.role);
|
|
38
|
+
return found.map(({ value, currency }) => ({
|
|
39
|
+
message,
|
|
40
|
+
cents: Math.round(value * 100),
|
|
41
|
+
currency,
|
|
42
|
+
verb,
|
|
43
|
+
direction,
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
export const centsText = (cents) => `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, "0")}`;
|
|
47
|
+
/**
|
|
48
|
+
* Every message with an amount and a money word, folded by amount and currency per chat: the first ask is `since`, a
|
|
49
|
+
* later "paid" message naming the same amount makes it `paid-mentioned`. Instalments are not summed into a total.
|
|
50
|
+
*/
|
|
51
|
+
export const amounts = {
|
|
52
|
+
id: "amounts",
|
|
53
|
+
version: 1,
|
|
54
|
+
scope: "chat",
|
|
55
|
+
read: async (_context, chat) => {
|
|
56
|
+
if (!chat)
|
|
57
|
+
return [];
|
|
58
|
+
const groups = new Map();
|
|
59
|
+
for (const row of rowsOf(chat)) {
|
|
60
|
+
const key = `${row.cents}|${row.currency ?? ""}`;
|
|
61
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
62
|
+
}
|
|
63
|
+
return [...groups.values()].map((rows) => {
|
|
64
|
+
const asks = rows.filter((row) => row.verb === "request");
|
|
65
|
+
const done = rows.filter((row) => row.verb === "done");
|
|
66
|
+
const lastAsk = asks.at(-1);
|
|
67
|
+
const first = (asks[0] ?? done[0] ?? rows[0]);
|
|
68
|
+
const paidAfter = done.some((row) => !lastAsk || Date.parse(row.message.timestamp) >= Date.parse(lastAsk.message.timestamp));
|
|
69
|
+
const state = asks.length ? (paidAfter ? "paid-mentioned" : "open") : done.length ? "paid-mentioned" : "mentioned";
|
|
70
|
+
const cited = asks.length ? [...asks, ...(paidAfter ? done.slice(-1) : [])] : rows;
|
|
71
|
+
return {
|
|
72
|
+
signal: "amounts",
|
|
73
|
+
chat: chat.ref,
|
|
74
|
+
at: first.message.timestamp,
|
|
75
|
+
who: whoOf(first.message),
|
|
76
|
+
evidence: evidenceOf(chat, ...new Set(cited.map((row) => row.message))),
|
|
77
|
+
basis: "pattern",
|
|
78
|
+
data: {
|
|
79
|
+
amount: centsText(first.cents),
|
|
80
|
+
currency: first.currency,
|
|
81
|
+
direction: first.direction,
|
|
82
|
+
state,
|
|
83
|
+
since: first.message.timestamp,
|
|
84
|
+
lastAsked: lastAsk?.message.timestamp ?? null,
|
|
85
|
+
asks: asks.length,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
attention: (items) => items.flatMap((item) => item.chat !== null && item.data.state === "open" ? [{ chat: item.chat, weight: 2, why: "money open" }] : []),
|
|
91
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface ChildrenData {
|
|
3
|
+
child: string;
|
|
4
|
+
key: string | null;
|
|
5
|
+
name: string | null;
|
|
6
|
+
depth: number | null;
|
|
7
|
+
/** Linked chats read for it; a chat linked to two projects counts once, under the first. */
|
|
8
|
+
chats: number | null;
|
|
9
|
+
/** Why it was not read: deeper than two levels, seen twice, or past eight children. */
|
|
10
|
+
cut: "depth" | "cycle" | "limit" | null;
|
|
11
|
+
}
|
|
12
|
+
/** Child projects (a confirmed `member-of` link to this one) that were read, and those cut. */
|
|
13
|
+
export declare const children: StatusSignal<ChildrenData>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Child projects (a confirmed `member-of` link to this one) that were read, and those cut. */
|
|
2
|
+
export const children = {
|
|
3
|
+
id: "children",
|
|
4
|
+
version: 1,
|
|
5
|
+
scope: "project",
|
|
6
|
+
read: async (context) => {
|
|
7
|
+
const item = (data) => ({
|
|
8
|
+
signal: "children",
|
|
9
|
+
chat: null,
|
|
10
|
+
at: null,
|
|
11
|
+
who: null,
|
|
12
|
+
evidence: [{ ref: data.child, fingerprint: null }],
|
|
13
|
+
basis: "store",
|
|
14
|
+
data,
|
|
15
|
+
});
|
|
16
|
+
return [
|
|
17
|
+
...context.project.children.map((child) => item({
|
|
18
|
+
child: child.ref,
|
|
19
|
+
key: child.key,
|
|
20
|
+
name: child.name,
|
|
21
|
+
depth: child.depth,
|
|
22
|
+
chats: context.chats.filter((chat) => chat.project === child.ref).length,
|
|
23
|
+
cut: null,
|
|
24
|
+
})),
|
|
25
|
+
...context.project.cut.map(({ ref, reason }) => item({ child: ref, key: null, name: null, depth: null, chats: null, cut: reason })),
|
|
26
|
+
];
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface CoverageData {
|
|
3
|
+
state: "complete" | "partial" | "unknown";
|
|
4
|
+
held: {
|
|
5
|
+
messages: number;
|
|
6
|
+
oldestAt: string | null;
|
|
7
|
+
newestAt: string | null;
|
|
8
|
+
};
|
|
9
|
+
/** The window starts before the oldest message the store holds, so the window is not read whole. */
|
|
10
|
+
beforeHeld: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** What the store holds of a chat it does not hold whole; the answer adds the line that syncs it. */
|
|
13
|
+
export declare const coverage: StatusSignal<CoverageData>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** What the store holds of a chat it does not hold whole; the answer adds the line that syncs it. */
|
|
2
|
+
export const coverage = {
|
|
3
|
+
id: "coverage",
|
|
4
|
+
version: 1,
|
|
5
|
+
scope: "chat",
|
|
6
|
+
read: async (context, chat) => {
|
|
7
|
+
if (!chat || chat.coverage.completeness.state === "complete")
|
|
8
|
+
return [];
|
|
9
|
+
const { completeness, held } = chat.coverage;
|
|
10
|
+
const oldest = held.oldestAt === null ? null : Date.parse(held.oldestAt);
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
signal: "coverage",
|
|
14
|
+
chat: chat.ref,
|
|
15
|
+
at: completeness.fetchedAt,
|
|
16
|
+
who: null,
|
|
17
|
+
evidence: [],
|
|
18
|
+
basis: "store",
|
|
19
|
+
data: {
|
|
20
|
+
state: completeness.state,
|
|
21
|
+
held,
|
|
22
|
+
beforeHeld: completeness.state !== "complete" && (oldest === null || context.since < oldest),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
},
|
|
27
|
+
attention: (items) => items.flatMap((item) => item.chat !== null && item.data.state !== "complete" && item.data.beforeHeld
|
|
28
|
+
? [{ chat: item.chat, weight: 3, why: "held in part" }]
|
|
29
|
+
: []),
|
|
30
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { amounts } from "./amounts.js";
|
|
2
|
+
import { children } from "./children.js";
|
|
3
|
+
import { coverage } from "./coverage.js";
|
|
4
|
+
import { lastMessage } from "./last-message.js";
|
|
5
|
+
import { payments } from "./payments.js";
|
|
6
|
+
import { promises } from "./promises.js";
|
|
7
|
+
import { questions } from "./questions.js";
|
|
8
|
+
import { replyNext } from "./reply-next.js";
|
|
9
|
+
import { topics } from "./topics.js";
|
|
10
|
+
/** The registry; its order is the answer's order. A new signal is one file here and its row in the CLI. */
|
|
11
|
+
export const STATUS_SIGNALS = [
|
|
12
|
+
lastMessage,
|
|
13
|
+
replyNext,
|
|
14
|
+
questions,
|
|
15
|
+
promises,
|
|
16
|
+
amounts,
|
|
17
|
+
payments,
|
|
18
|
+
topics,
|
|
19
|
+
coverage,
|
|
20
|
+
children,
|
|
21
|
+
];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export type LastMessageData = Record<string, never>;
|
|
3
|
+
/** The newest stored message in the window: when, and who wrote it. A chat held in part says so through `coverage`. */
|
|
4
|
+
export declare const lastMessage: StatusSignal<LastMessageData>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { evidenceOf, whoOf } from "../items.js";
|
|
2
|
+
/** The newest stored message in the window: when, and who wrote it. A chat held in part says so through `coverage`. */
|
|
3
|
+
export const lastMessage = {
|
|
4
|
+
id: "last-message",
|
|
5
|
+
version: 1,
|
|
6
|
+
scope: "chat",
|
|
7
|
+
read: async (_context, chat) => {
|
|
8
|
+
const last = chat?.messages.at(-1);
|
|
9
|
+
if (!chat || !last)
|
|
10
|
+
return [];
|
|
11
|
+
const item = {
|
|
12
|
+
signal: "last-message",
|
|
13
|
+
chat: chat.ref,
|
|
14
|
+
at: last.timestamp,
|
|
15
|
+
who: whoOf(last),
|
|
16
|
+
evidence: evidenceOf(chat, last),
|
|
17
|
+
basis: "store",
|
|
18
|
+
data: {},
|
|
19
|
+
};
|
|
20
|
+
return [item];
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Payment } from "../../../meetings/index.js";
|
|
2
|
+
import type { StatusSignal } from "../types.js";
|
|
3
|
+
export interface PaymentsData extends Payment {
|
|
4
|
+
title: string;
|
|
5
|
+
/** The fact's review status: proposed or accepted. */
|
|
6
|
+
status: string;
|
|
7
|
+
}
|
|
8
|
+
/** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
|
|
9
|
+
export declare const payments: StatusSignal<PaymentsData>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { paymentTitle } from "../../../meetings/index.js";
|
|
2
|
+
const HOLDING = new Set(["proposed", "accepted"]);
|
|
3
|
+
/** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
|
|
4
|
+
export const payments = {
|
|
5
|
+
id: "payments",
|
|
6
|
+
version: 2,
|
|
7
|
+
scope: "project",
|
|
8
|
+
read: async (context) => {
|
|
9
|
+
const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
|
|
10
|
+
const facts = (await Promise.all(projects.map((project) => context.store.facts.list({ project, type: "payment", limit: 500 })))).flat();
|
|
11
|
+
return facts
|
|
12
|
+
.filter((fact) => fact.payment !== null && HOLDING.has(fact.status))
|
|
13
|
+
.map((fact) => ({
|
|
14
|
+
signal: "payments",
|
|
15
|
+
chat: null,
|
|
16
|
+
at: fact.occurredAt ?? fact.createdAt,
|
|
17
|
+
who: null,
|
|
18
|
+
// The fact first, then what it cites, so an amount read from a cited message is not counted twice.
|
|
19
|
+
evidence: [fact.ref, ...fact.evidence].map((ref) => ({ ref, fingerprint: null })),
|
|
20
|
+
basis: "fact",
|
|
21
|
+
data: { ...fact.payment, title: paymentTitle(fact), status: fact.status },
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface PromisesData {
|
|
3
|
+
/** Where it came from: a stored task, a recorded fact, or a word match in a message (a guess). */
|
|
4
|
+
source: "task" | "fact" | "pattern";
|
|
5
|
+
/** `promise` or `request` for a task; a fact's and a pattern's are promises. */
|
|
6
|
+
kind: string;
|
|
7
|
+
title: string;
|
|
8
|
+
/** Who promised, for a pattern: the owner (`yours`) or the other side. */
|
|
9
|
+
side: "yours" | "theirs" | null;
|
|
10
|
+
due: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Open promise and request tasks of the project or from its chats, promise facts proposed or accepted, and up to
|
|
15
|
+
* three promises per chat found by first person future words ("сделаю", "I'll send"), marked as patterns.
|
|
16
|
+
*/
|
|
17
|
+
export declare const promises: StatusSignal<PromisesData>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { isLocator, parseLocator } from "../../../domain/locator.js";
|
|
3
|
+
import { PROMISE_FORMS } from "../../../domain/status-words.js";
|
|
4
|
+
import { dueDayOf } from "../../../search/task-signature.js";
|
|
5
|
+
import { normalize } from "../../../store/normalize.js";
|
|
6
|
+
import { inboxKey } from "../../../store/store.js";
|
|
7
|
+
import { taskAccount } from "../../task-rules.js";
|
|
8
|
+
import { day, evidenceOf, excerpt, fromOwner, whoOf } from "../items.js";
|
|
9
|
+
const PATTERNS_PER_CHAT = 3;
|
|
10
|
+
const DAY = 86_400_000;
|
|
11
|
+
const KINDS = new Set(["promise", "request"]);
|
|
12
|
+
const OPEN = new Set(["open", "todo", "in_progress"]);
|
|
13
|
+
const chatOfSource = (context, source) => {
|
|
14
|
+
if (source === null || !isLocator(source))
|
|
15
|
+
return undefined;
|
|
16
|
+
const { provider, account, chat } = parseLocator(source);
|
|
17
|
+
return context.chats.find((one) => one.account.provider === provider && one.account.account === account && one.chatId === chat);
|
|
18
|
+
};
|
|
19
|
+
// An account that never had a task has no inbox project yet.
|
|
20
|
+
const orNone = (error) => {
|
|
21
|
+
if (error instanceof CliError && error.code === "not_found")
|
|
22
|
+
return [];
|
|
23
|
+
throw error;
|
|
24
|
+
};
|
|
25
|
+
const tasksOf = async (context) => {
|
|
26
|
+
const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
|
|
27
|
+
const accounts = new Map(context.chats.map((chat) => [taskAccount(chat.account), chat.account]));
|
|
28
|
+
const inboxes = [...accounts.keys()].map(inboxKey);
|
|
29
|
+
const found = await Promise.all([...projects, ...inboxes].map((project) => context.store.tasks.find({ project, limit: 500 }).catch(orNone)));
|
|
30
|
+
const projectRefs = new Set(projects);
|
|
31
|
+
return found
|
|
32
|
+
.flat()
|
|
33
|
+
.filter((task) => KINDS.has(task.type) && OPEN.has(task.status))
|
|
34
|
+
.filter((task) => projectRefs.has(task.project) || chatOfSource(context, task.source) !== undefined);
|
|
35
|
+
};
|
|
36
|
+
const taskItem = (context, task) => ({
|
|
37
|
+
signal: "promises",
|
|
38
|
+
chat: chatOfSource(context, task.source)?.ref ?? null,
|
|
39
|
+
at: task.createdAt,
|
|
40
|
+
who: null,
|
|
41
|
+
evidence: [{ ref: task.ref, fingerprint: null }, ...(task.source ? [{ ref: task.source, fingerprint: null }] : [])],
|
|
42
|
+
basis: "task",
|
|
43
|
+
data: {
|
|
44
|
+
source: "task",
|
|
45
|
+
kind: task.type,
|
|
46
|
+
title: excerpt(task.title),
|
|
47
|
+
side: null,
|
|
48
|
+
due: task.dueAt === null ? null : day(task.dueAt),
|
|
49
|
+
status: task.status,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const patternItems = (chat) => chat.messages
|
|
53
|
+
.filter((message) => !message.text.includes("?") && PROMISE_FORMS.test(message.text))
|
|
54
|
+
.slice(-PATTERNS_PER_CHAT)
|
|
55
|
+
.reverse()
|
|
56
|
+
.map((message) => {
|
|
57
|
+
const due = dueDayOf(normalize(message.text), Date.parse(message.timestamp));
|
|
58
|
+
return {
|
|
59
|
+
signal: "promises",
|
|
60
|
+
chat: chat.ref,
|
|
61
|
+
at: message.timestamp,
|
|
62
|
+
who: whoOf(message),
|
|
63
|
+
evidence: evidenceOf(chat, message),
|
|
64
|
+
basis: "pattern",
|
|
65
|
+
data: {
|
|
66
|
+
source: "pattern",
|
|
67
|
+
kind: "promise",
|
|
68
|
+
title: excerpt(message.text),
|
|
69
|
+
side: fromOwner(message) ? "yours" : "theirs",
|
|
70
|
+
due: due === null ? null : day(due * DAY),
|
|
71
|
+
status: "open",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* Open promise and request tasks of the project or from its chats, promise facts proposed or accepted, and up to
|
|
77
|
+
* three promises per chat found by first person future words ("сделаю", "I'll send"), marked as patterns.
|
|
78
|
+
*/
|
|
79
|
+
export const promises = {
|
|
80
|
+
id: "promises",
|
|
81
|
+
version: 1,
|
|
82
|
+
scope: "project",
|
|
83
|
+
read: async (context) => {
|
|
84
|
+
const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
|
|
85
|
+
const facts = (await Promise.all(projects.map((project) => context.store.facts.list({ type: "promise", project }))))
|
|
86
|
+
.flat()
|
|
87
|
+
.filter((fact) => fact.status === "proposed" || fact.status === "accepted");
|
|
88
|
+
return [
|
|
89
|
+
...(await tasksOf(context)).map((task) => taskItem(context, task)),
|
|
90
|
+
...facts.map((fact) => ({
|
|
91
|
+
signal: "promises",
|
|
92
|
+
chat: null,
|
|
93
|
+
at: fact.occurredAt ?? fact.createdAt,
|
|
94
|
+
who: null,
|
|
95
|
+
evidence: [{ ref: fact.ref, fingerprint: null }],
|
|
96
|
+
basis: "fact",
|
|
97
|
+
data: {
|
|
98
|
+
source: "fact",
|
|
99
|
+
kind: "promise",
|
|
100
|
+
title: excerpt(fact.statement),
|
|
101
|
+
side: null,
|
|
102
|
+
due: null,
|
|
103
|
+
status: fact.status,
|
|
104
|
+
},
|
|
105
|
+
})),
|
|
106
|
+
...context.chats.flatMap(patternItems),
|
|
107
|
+
];
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface QuestionsData {
|
|
3
|
+
/** `theirs`: someone asked and the owner has not answered; `yours`: the owner asked and nobody answered. */
|
|
4
|
+
side: "theirs" | "yours";
|
|
5
|
+
excerpt: string;
|
|
6
|
+
}
|
|
7
|
+
/** Open questions both ways, newest first, at most five a side. A question answered in another chat stays open. */
|
|
8
|
+
export declare const questions: StatusSignal<QuestionsData>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { questions as questionsOf } from "../../questions.js";
|
|
2
|
+
import { evidenceOf, excerpt, whoOf } from "../items.js";
|
|
3
|
+
const PER_SIDE = 5;
|
|
4
|
+
const WEEK = 7 * 86_400_000;
|
|
5
|
+
const NOBODY = new Set();
|
|
6
|
+
/** Open questions both ways, newest first, at most five a side. A question answered in another chat stays open. */
|
|
7
|
+
export const questions = {
|
|
8
|
+
id: "questions",
|
|
9
|
+
version: 1,
|
|
10
|
+
scope: "chat",
|
|
11
|
+
read: async (_context, chat) => {
|
|
12
|
+
if (!chat)
|
|
13
|
+
return [];
|
|
14
|
+
const open = (asker) => questionsOf(chat.messages, { answerers: NOBODY, asker })
|
|
15
|
+
.filter(({ answer }) => answer === undefined)
|
|
16
|
+
.map(({ question }) => question)
|
|
17
|
+
.reverse()
|
|
18
|
+
.slice(0, PER_SIDE);
|
|
19
|
+
return [
|
|
20
|
+
["theirs", open("others")],
|
|
21
|
+
["yours", open("owner")],
|
|
22
|
+
].flatMap(([side, asked]) => asked.map((question) => ({
|
|
23
|
+
signal: "questions",
|
|
24
|
+
chat: chat.ref,
|
|
25
|
+
at: question.timestamp,
|
|
26
|
+
who: whoOf(question),
|
|
27
|
+
evidence: evidenceOf(chat, question),
|
|
28
|
+
basis: "store",
|
|
29
|
+
data: { side, excerpt: excerpt(question.text) },
|
|
30
|
+
})));
|
|
31
|
+
},
|
|
32
|
+
attention: (items, context) => {
|
|
33
|
+
const stale = new Set(items.flatMap((item) => item.chat !== null && item.data.side === "yours" && item.at && context.now - Date.parse(item.at) > WEEK
|
|
34
|
+
? [item.chat]
|
|
35
|
+
: []));
|
|
36
|
+
return [...stale].map((chat) => ({ chat, weight: 1, why: "your question unanswered for a week" }));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface ReplyNextData {
|
|
3
|
+
next: "owner" | "them" | "named" | "nobody";
|
|
4
|
+
/** Since when the one named has been the one to answer. */
|
|
5
|
+
waitingSince: string | null;
|
|
6
|
+
/** In a group: the person the newest message replies to or mentions. */
|
|
7
|
+
addressed: {
|
|
8
|
+
ref: string | null;
|
|
9
|
+
name: string | null;
|
|
10
|
+
} | null;
|
|
11
|
+
/** Waited longer than `tasks agenda` does before it lists the chat (a day for you, a week for them). */
|
|
12
|
+
overdue: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whose move it is, by five rules in order: the owner wrote last → them; a dialog → the owner; in a group a message
|
|
16
|
+
* that replies to or mentions the owner, or a question right after the owner spoke → the owner; one that names
|
|
17
|
+
* another member → that member; else nobody. A name typed without a mention is not seen.
|
|
18
|
+
*/
|
|
19
|
+
export declare const replyNext: StatusSignal<ReplyNextData>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AGENDA_DEFAULTS } from "../../agenda.js";
|
|
2
|
+
import { evidenceOf, fromOwner, whoOf } from "../items.js";
|
|
3
|
+
/** Whom a message answers or names: the quoted message's sender first, then the first mention. */
|
|
4
|
+
const targetOf = (message, chat, owners) => {
|
|
5
|
+
const byId = (id) => chat.messages.find((one) => one.id === id);
|
|
6
|
+
const quoted = message.replyTo ?? (message.replyToId === undefined ? undefined : byId(message.replyToId));
|
|
7
|
+
if (quoted)
|
|
8
|
+
return {
|
|
9
|
+
owner: quoted.outgoing === true || (quoted.senderId !== null && owners.has(quoted.senderId)),
|
|
10
|
+
id: quoted.senderId,
|
|
11
|
+
name: quoted.senderName,
|
|
12
|
+
};
|
|
13
|
+
const [mentioned] = message.mentions ?? [];
|
|
14
|
+
if (mentioned === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (owners.has(mentioned))
|
|
17
|
+
return { owner: true, id: mentioned, name: null };
|
|
18
|
+
const named = chat.messages.find((one) => one.senderId === mentioned);
|
|
19
|
+
return { owner: false, id: mentioned, name: named?.senderName ?? null };
|
|
20
|
+
};
|
|
21
|
+
const asksSomething = (message) => message.text.includes("?");
|
|
22
|
+
/**
|
|
23
|
+
* Whose move it is, by five rules in order: the owner wrote last → them; a dialog → the owner; in a group a message
|
|
24
|
+
* that replies to or mentions the owner, or a question right after the owner spoke → the owner; one that names
|
|
25
|
+
* another member → that member; else nobody. A name typed without a mention is not seen.
|
|
26
|
+
*/
|
|
27
|
+
export const replyNext = {
|
|
28
|
+
id: "reply-next",
|
|
29
|
+
version: 1,
|
|
30
|
+
scope: "chat",
|
|
31
|
+
read: async (context, chat) => {
|
|
32
|
+
const last = chat?.messages.at(-1);
|
|
33
|
+
if (!chat || !last)
|
|
34
|
+
return [];
|
|
35
|
+
const { messages } = chat;
|
|
36
|
+
const owners = new Set(messages.flatMap((one) => (fromOwner(one) && one.senderId !== null ? [one.senderId] : [])));
|
|
37
|
+
const lastOwn = messages.findLastIndex(fromOwner);
|
|
38
|
+
const firstAfterOwn = messages[lastOwn + 1] ?? messages[0];
|
|
39
|
+
const target = targetOf(last, chat, owners);
|
|
40
|
+
const decide = () => {
|
|
41
|
+
if (last.outgoing === null)
|
|
42
|
+
return { next: "nobody", addressed: null, since: null };
|
|
43
|
+
if (fromOwner(last))
|
|
44
|
+
return {
|
|
45
|
+
next: "them",
|
|
46
|
+
addressed: chat.kind !== "dialog" && target && !target.owner ? { ref: null, name: target.name } : null,
|
|
47
|
+
since: last.timestamp,
|
|
48
|
+
};
|
|
49
|
+
const since = firstAfterOwn?.timestamp ?? last.timestamp;
|
|
50
|
+
if (chat.kind === "dialog")
|
|
51
|
+
return { next: "owner", addressed: null, since };
|
|
52
|
+
if (target?.owner || (asksSomething(last) && lastOwn === messages.length - 2))
|
|
53
|
+
return { next: "owner", addressed: null, since };
|
|
54
|
+
if (target)
|
|
55
|
+
return { next: "named", addressed: { ref: null, name: target.name }, since };
|
|
56
|
+
return { next: "nobody", addressed: null, since };
|
|
57
|
+
};
|
|
58
|
+
const { next, addressed, since } = decide();
|
|
59
|
+
const waited = since === null ? 0 : context.now - Date.parse(since);
|
|
60
|
+
const overdue = (next === "owner" && waited >= AGENDA_DEFAULTS.unanswered) ||
|
|
61
|
+
(next === "them" && waited >= AGENDA_DEFAULTS.waiting);
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
signal: "reply-next",
|
|
65
|
+
chat: chat.ref,
|
|
66
|
+
at: last.timestamp,
|
|
67
|
+
who: whoOf(last),
|
|
68
|
+
evidence: evidenceOf(chat, last),
|
|
69
|
+
basis: "store",
|
|
70
|
+
data: { next, waitingSince: since, addressed, overdue },
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
},
|
|
74
|
+
attention: (items) => items.flatMap((item) => item.chat !== null && item.data.next === "owner" ? [{ chat: item.chat, weight: 2, why: "your move" }] : []),
|
|
75
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface TopicsData {
|
|
3
|
+
query: string;
|
|
4
|
+
yoursLast: string | null;
|
|
5
|
+
theirsLast: string | null;
|
|
6
|
+
/** Their newest mention, when the owner spoke of it later: the other side went quiet on it. */
|
|
7
|
+
silentSince: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** Per `--context-query`, the newest message of each side that holds every one of its words, in the window. */
|
|
10
|
+
export declare const topics: StatusSignal<TopicsData>;
|