@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
package/dist/services/deps.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { MessengerTraits } from "../ports/traits.js";
|
|
|
5
5
|
import type { SendGuard } from "../sends/guard.js";
|
|
6
6
|
import type { AccountStore } from "../store/account-store.js";
|
|
7
7
|
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
8
|
+
import type { HistoryLimits } from "./history-limits.js";
|
|
8
9
|
import type { Override } from "./index.js";
|
|
9
10
|
export interface ServiceMessenger extends MessengerTraits {
|
|
10
11
|
/** Replaces shared use cases for this messenger; its commands and MCP tools both get the replacement. */
|
|
@@ -39,6 +40,8 @@ export interface ServiceDeps {
|
|
|
39
40
|
/** `false` keeps no search history: recording was turned off by name. Kept when unset. */
|
|
40
41
|
history?: boolean;
|
|
41
42
|
searchCatchUp?: boolean;
|
|
43
|
+
/** How far back `messages sync` reads each kind of chat, and which groups it does not read at all. */
|
|
44
|
+
historyLimits?: HistoryLimits;
|
|
42
45
|
/** This account has picture search on (`pictureSearch`): its images are embedded and searched by their pixels. */
|
|
43
46
|
pictures?: boolean;
|
|
44
47
|
/** Which reranker orders `--discover`'s first hits (`searchReranker`), or `off`; the default when unset. */
|
|
@@ -22,6 +22,8 @@ export interface HistorySkip {
|
|
|
22
22
|
members?: number;
|
|
23
23
|
/** For `older_than_window`: the window's start when the push was saved. */
|
|
24
24
|
keptFrom?: string;
|
|
25
|
+
/** `messages sync` stopped there, so how many messages were left out is not known (`messages` is 0). */
|
|
26
|
+
unread?: true;
|
|
25
27
|
}
|
|
26
28
|
/** The kind and size of a chat, as the batch names it or the store holds it. */
|
|
27
29
|
export interface ChatShape {
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type BotCheckService } from "../botcheck/service.js";
|
|
2
|
+
export { paymentTitle } from "../meetings/index.js";
|
|
2
3
|
export { amountOf, amountsOf, type TaskAmount } from "../search/task-signature.js";
|
|
3
4
|
export { AGENDA_DEFAULTS } from "./agenda.js";
|
|
4
5
|
export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
|
|
5
6
|
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
|
|
6
7
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
7
8
|
export { excerpt } from "./excerpt.js";
|
|
9
|
+
export { aliasHits, BUILT_IN_PURPOSES, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
|
|
8
10
|
export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
|
|
9
11
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
|
10
12
|
export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.js";
|
|
@@ -19,6 +21,7 @@ export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./pr
|
|
|
19
21
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
20
22
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
21
23
|
export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
|
|
24
|
+
export { DEFAULT_PURPOSES, type MoneyTotal, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
|
|
22
25
|
export { questions } from "./questions.js";
|
|
23
26
|
export { canonicalProjectRef, gatherStoredProject, type LinkedChat, linkedChatsOf, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
|
|
24
27
|
import { type AccountService } from "./account.js";
|
|
@@ -181,7 +184,7 @@ export { type PackageUpgradeOutcome, type PackageUpgradePorts, type PackageUpgra
|
|
|
181
184
|
export { type PersonMeetingContext, type PersonMeetingContextItem, type PersonMeetingContextOptions, type PersonMeetingReadStore, personMeetingContext, } from "./person-meeting-context.js";
|
|
182
185
|
export { privatePeopleService } from "./private-people.js";
|
|
183
186
|
export { approveAsTask, type ProposalTaskStore, proposalSource } from "./proposal-tasks.js";
|
|
184
|
-
export { type Decided, type Decision, emptyProposalRegistry, type ProposalFilter, type ProposalKind, type ProposalRegistry, type ProposalRow, parseProposalIds, proposalRegistry, } from "./proposals/index.js";
|
|
187
|
+
export { type Decided, type Decision, emptyProposalRegistry, FACT_SUMMARIES, type ProposalFilter, type ProposalKind, type ProposalRegistry, type ProposalRow, parseProposalIds, proposalRegistry, } from "./proposals/index.js";
|
|
185
188
|
export type { RetentionQuery, RetentionService } from "./retention.js";
|
|
186
189
|
export { retentionService } from "./retention.js";
|
|
187
190
|
export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
|
package/dist/services/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { botCheckService } from "../botcheck/service.js";
|
|
2
|
+
export { paymentTitle } from "../meetings/index.js";
|
|
2
3
|
export { amountOf, amountsOf } from "../search/task-signature.js";
|
|
3
4
|
export { AGENDA_DEFAULTS } from "./agenda.js";
|
|
4
5
|
export { chatCoverage } from "./chat-coverage.js";
|
|
5
6
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
6
7
|
export { excerpt } from "./excerpt.js";
|
|
8
|
+
export { aliasHits, BUILT_IN_PURPOSES, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
|
|
7
9
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
|
8
10
|
export { moveMediaArchive } from "./media-archive-move.js";
|
|
9
11
|
export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
|
|
@@ -11,6 +13,7 @@ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from
|
|
|
11
13
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
12
14
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
13
15
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
16
|
+
export { DEFAULT_PURPOSES, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
|
|
14
17
|
export { questions } from "./questions.js";
|
|
15
18
|
export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, } from "./stored-project-gathering.js";
|
|
16
19
|
import { accountService } from "./account.js";
|
|
@@ -139,7 +142,7 @@ export { upgradePackage, } from "./package-upgrade.js";
|
|
|
139
142
|
export { personMeetingContext, } from "./person-meeting-context.js";
|
|
140
143
|
export { privatePeopleService } from "./private-people.js";
|
|
141
144
|
export { approveAsTask, proposalSource } from "./proposal-tasks.js";
|
|
142
|
-
export { emptyProposalRegistry, parseProposalIds, proposalRegistry, } from "./proposals/index.js";
|
|
145
|
+
export { emptyProposalRegistry, FACT_SUMMARIES, parseProposalIds, proposalRegistry, } from "./proposals/index.js";
|
|
143
146
|
export { retentionService } from "./retention.js";
|
|
144
147
|
export { RESOURCES_SEARCHED, searchAll, } from "./search-all.js";
|
|
145
148
|
export { RESOURCES_SEARCHED_WITH_MEETINGS, searchAllIncludingMeetings, searchAllWithMeetings, } from "./search-all-meetings.js";
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The job folder is the list's own source: every file is read whole, so it answers each field. `sql`, not `native`:
|
|
4
|
+
* `evaluate` takes the whole language, OR included, and the help and the MCP description say so.
|
|
5
|
+
*/
|
|
3
6
|
export declare const JOBS_QUERY: {
|
|
4
7
|
readonly entity: "job";
|
|
5
|
-
readonly backend: "
|
|
8
|
+
readonly backend: "sql";
|
|
6
9
|
readonly fields: import("@wirecat/cli-base/query").FieldSpec[];
|
|
7
10
|
};
|
|
8
11
|
/** One profile's jobs as `store jobs list` answers them, newest first; the query is checked as each file is read. */
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { evaluate, queryFieldsOf } from "@wirecat/cli-base/query";
|
|
2
2
|
import { job } from "../entities/job.js";
|
|
3
3
|
import { jobItem, listJobs } from "./backfill-jobs.js";
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The job folder is the list's own source: every file is read whole, so it answers each field. `sql`, not `native`:
|
|
6
|
+
* `evaluate` takes the whole language, OR included, and the help and the MCP description say so.
|
|
7
|
+
*/
|
|
5
8
|
export const JOBS_QUERY = {
|
|
6
9
|
entity: "job",
|
|
7
|
-
backend: "
|
|
10
|
+
backend: "sql",
|
|
8
11
|
fields: queryFieldsOf(job, {
|
|
9
12
|
native: Object.keys(job.entries),
|
|
10
13
|
examples: { state: "state:failed", started_at: "started_at>=7d", chat: "chat:7", fetched: "fetched>0" },
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LinkScorer } from "./types.js";
|
|
2
|
+
export declare const aliasWeight: (hits: number) => number;
|
|
3
|
+
/**
|
|
4
|
+
* Messages in the window that name the project as whole words, grouped by chat: 3–9 of them, or 10 and more. A
|
|
5
|
+
* text search finds a word inside another, so each hit is checked again by the whole-word rule.
|
|
6
|
+
*/
|
|
7
|
+
export declare const aliasHits: LinkScorer;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { formatReference } from "../../domain/references.js";
|
|
2
|
+
import { chatRef, projectTerms, wholeWords } from "./shared.js";
|
|
3
|
+
const SEARCH_LIMIT = 500;
|
|
4
|
+
export const aliasWeight = (hits) => (hits >= 10 ? 0.55 : hits >= 3 ? 0.35 : 0);
|
|
5
|
+
/**
|
|
6
|
+
* Messages in the window that name the project as whole words, grouped by chat: 3–9 of them, or 10 and more. A
|
|
7
|
+
* text search finds a word inside another, so each hit is checked again by the whole-word rule.
|
|
8
|
+
*/
|
|
9
|
+
export const aliasHits = {
|
|
10
|
+
id: "alias-hits",
|
|
11
|
+
version: 1,
|
|
12
|
+
candidates: async ({ store, project, sources, since, signal }) => {
|
|
13
|
+
if (!project)
|
|
14
|
+
return [];
|
|
15
|
+
// The trigram index answers a shorter query with nothing.
|
|
16
|
+
const terms = projectTerms(project).filter((term) => [...term.trim()].length >= 3);
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const key of sources) {
|
|
19
|
+
const chats = new Map();
|
|
20
|
+
for (const term of terms) {
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
const pattern = wholeWords(term);
|
|
23
|
+
const hits = await store.messages.find({ account: key, text: term, limit: SEARCH_LIMIT });
|
|
24
|
+
for (const hit of hits.items) {
|
|
25
|
+
if (hit.timestamp < since || !pattern.test(hit.text.normalize("NFKC").toLowerCase()))
|
|
26
|
+
continue;
|
|
27
|
+
const messages = chats.get(String(hit.chatId)) ?? new Map();
|
|
28
|
+
messages.set(String(hit.id), formatReference({
|
|
29
|
+
type: "message",
|
|
30
|
+
provider: key.provider,
|
|
31
|
+
account: String(key.account),
|
|
32
|
+
chat: String(hit.chatId),
|
|
33
|
+
message: String(hit.id),
|
|
34
|
+
}));
|
|
35
|
+
chats.set(String(hit.chatId), messages);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const [chat, messages] of chats) {
|
|
39
|
+
const weight = aliasWeight(messages.size);
|
|
40
|
+
if (!weight)
|
|
41
|
+
continue;
|
|
42
|
+
found.push({
|
|
43
|
+
from: chatRef(key, chat),
|
|
44
|
+
to: project.ref,
|
|
45
|
+
kind: "member-of",
|
|
46
|
+
weight,
|
|
47
|
+
why: `${messages.size} messages since ${since.slice(0, 10)} name the project`,
|
|
48
|
+
evidence: [...messages.values()].slice(0, 3),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return found;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LinkScorer } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A person who wrote 5 or more messages in the window in one of the project's confirmed chats, found by the sender's
|
|
4
|
+
* identity, never by the name a message shows. In a group of more than 30 people writing there says less.
|
|
5
|
+
*/
|
|
6
|
+
export declare const chatSenders: LinkScorer;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { formatReference, parseReference } from "../../domain/references.js";
|
|
2
|
+
import { contactRef, personRefOf, sameAccount } from "./shared.js";
|
|
3
|
+
const WEIGHT = 0.5;
|
|
4
|
+
const LARGE_GROUP_WEIGHT = 0.2;
|
|
5
|
+
const LARGE_GROUP = 30;
|
|
6
|
+
const MIN_MESSAGES = 5;
|
|
7
|
+
const READ_LIMIT = 2000;
|
|
8
|
+
/**
|
|
9
|
+
* A person who wrote 5 or more messages in the window in one of the project's confirmed chats, found by the sender's
|
|
10
|
+
* identity, never by the name a message shows. In a group of more than 30 people writing there says less.
|
|
11
|
+
*/
|
|
12
|
+
export const chatSenders = {
|
|
13
|
+
id: "chat-senders",
|
|
14
|
+
version: 1,
|
|
15
|
+
candidates: async ({ store, project, confirmed, sources, since, owner, signal }) => {
|
|
16
|
+
if (!project)
|
|
17
|
+
return [];
|
|
18
|
+
const found = [];
|
|
19
|
+
for (const chat of confirmed.chats) {
|
|
20
|
+
const reference = parseReference(chat);
|
|
21
|
+
if (reference.type !== "chat")
|
|
22
|
+
continue;
|
|
23
|
+
const key = sources.find((source) => sameAccount(source, reference));
|
|
24
|
+
if (!key)
|
|
25
|
+
continue;
|
|
26
|
+
signal?.throwIfAborted();
|
|
27
|
+
const page = await store.messages.messages(key, reference.chat, { limit: READ_LIMIT, since, outgoing: false });
|
|
28
|
+
const senders = new Map();
|
|
29
|
+
for (const message of page.items) {
|
|
30
|
+
if (message.senderId === null || message.senderIsChat)
|
|
31
|
+
continue;
|
|
32
|
+
const id = String(message.senderId);
|
|
33
|
+
senders.set(id, { count: (senders.get(id)?.count ?? 0) + 1, last: String(message.id) });
|
|
34
|
+
}
|
|
35
|
+
const size = Math.max((await store.roster.members(key, reference.chat)).length, senders.size);
|
|
36
|
+
const weight = size > LARGE_GROUP ? LARGE_GROUP_WEIGHT : WEIGHT;
|
|
37
|
+
for (const [id, { count, last }] of senders) {
|
|
38
|
+
if (count < MIN_MESSAGES)
|
|
39
|
+
continue;
|
|
40
|
+
const person = await personRefOf(store, key.provider, id);
|
|
41
|
+
if (!person || person === owner)
|
|
42
|
+
continue;
|
|
43
|
+
found.push({
|
|
44
|
+
from: person,
|
|
45
|
+
to: project.ref,
|
|
46
|
+
kind: "member-of",
|
|
47
|
+
weight,
|
|
48
|
+
why: `${count} messages since ${since.slice(0, 10)} in a project chat (${chat})${size > LARGE_GROUP ? `, a group of ${size}` : ""}`,
|
|
49
|
+
evidence: [formatReference({ ...reference, type: "message", message: last })],
|
|
50
|
+
identity: contactRef(key.provider, id),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LinkScorer } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A chat whose title names the project, its key or an alias as whole words. A purpose the owner configured that the
|
|
4
|
+
* title also names («HRB sales») is proposed with it. Mail threads are the `mail-thread` scorer's.
|
|
5
|
+
*/
|
|
6
|
+
export declare const chatTitle: LinkScorer;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { chatRef, namesProject, wholeWords } from "./shared.js";
|
|
2
|
+
const WEIGHT = 0.7;
|
|
3
|
+
/**
|
|
4
|
+
* A chat whose title names the project, its key or an alias as whole words. A purpose the owner configured that the
|
|
5
|
+
* title also names («HRB sales») is proposed with it. Mail threads are the `mail-thread` scorer's.
|
|
6
|
+
*/
|
|
7
|
+
export const chatTitle = {
|
|
8
|
+
id: "chat-title",
|
|
9
|
+
version: 1,
|
|
10
|
+
candidates: async ({ store, project, sources, purposes, signal }) => {
|
|
11
|
+
if (!project)
|
|
12
|
+
return [];
|
|
13
|
+
const names = namesProject(project);
|
|
14
|
+
const purposeOf = purposes
|
|
15
|
+
.filter((purpose) => purpose !== "work")
|
|
16
|
+
.map((purpose) => ({ purpose, pattern: wholeWords(purpose) }));
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const key of sources) {
|
|
19
|
+
if (key.provider === "email")
|
|
20
|
+
continue;
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
const chats = await store.chats.chats(key, {});
|
|
23
|
+
for (const chat of chats.items) {
|
|
24
|
+
if (chat.kind === "saved")
|
|
25
|
+
continue;
|
|
26
|
+
const term = names(chat.title);
|
|
27
|
+
if (!term)
|
|
28
|
+
continue;
|
|
29
|
+
const title = (chat.title ?? "").normalize("NFKC").toLowerCase();
|
|
30
|
+
const purpose = purposeOf.find(({ pattern }) => pattern.test(title))?.purpose;
|
|
31
|
+
found.push({
|
|
32
|
+
from: chatRef(key, chat.id),
|
|
33
|
+
to: project.ref,
|
|
34
|
+
kind: "member-of",
|
|
35
|
+
weight: WEIGHT,
|
|
36
|
+
why: `the chat title names the project${term === project.name ? "" : ` (${term === project.key ? "key" : "alias"})`}`,
|
|
37
|
+
evidence: [],
|
|
38
|
+
...(purpose ? { purpose } : {}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LinkScorer } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* People whose mail addresses share a company domain, related to each other, weakly: a shared domain does not prove
|
|
4
|
+
* they work together. Needs no project; the rule `relationships scan` ran before scorers existed.
|
|
5
|
+
*/
|
|
6
|
+
export declare const emailDomain: LinkScorer;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { contactRef } from "./shared.js";
|
|
2
|
+
const WEIGHT = 0.3;
|
|
3
|
+
const CONTACTS = 500;
|
|
4
|
+
const MOST_PAIRS = 200;
|
|
5
|
+
const FREE = new Set([
|
|
6
|
+
"gmail.com",
|
|
7
|
+
"outlook.com",
|
|
8
|
+
"hotmail.com",
|
|
9
|
+
"yahoo.com",
|
|
10
|
+
"icloud.com",
|
|
11
|
+
"proton.me",
|
|
12
|
+
"protonmail.com",
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* People whose mail addresses share a company domain, related to each other, weakly: a shared domain does not prove
|
|
16
|
+
* they work together. Needs no project; the rule `relationships scan` ran before scorers existed.
|
|
17
|
+
*/
|
|
18
|
+
export const emailDomain = {
|
|
19
|
+
id: "email-domain",
|
|
20
|
+
version: 1,
|
|
21
|
+
candidates: async ({ store, sources, owner, signal }) => {
|
|
22
|
+
const domains = new Map();
|
|
23
|
+
for (const key of sources) {
|
|
24
|
+
if (key.provider !== "email")
|
|
25
|
+
continue;
|
|
26
|
+
signal?.throwIfAborted();
|
|
27
|
+
const contacts = await store.people.contacts(key, { order: "name", limit: CONTACTS, offset: 0 });
|
|
28
|
+
for (const contact of contacts.items) {
|
|
29
|
+
const address = String(contact.id).toLowerCase();
|
|
30
|
+
const domain = address.split("@")[1];
|
|
31
|
+
if (!domain || FREE.has(domain))
|
|
32
|
+
continue;
|
|
33
|
+
const person = await store.people.personOf({ provider: key.provider, id: contact.id });
|
|
34
|
+
if (!person || `person:${person.uid}` === owner)
|
|
35
|
+
continue;
|
|
36
|
+
const people = domains.get(domain) ?? new Map();
|
|
37
|
+
if (!people.has(`person:${person.uid}`))
|
|
38
|
+
people.set(`person:${person.uid}`, contactRef(key.provider, contact.id));
|
|
39
|
+
domains.set(domain, people);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const found = [];
|
|
43
|
+
for (const [domain, people] of domains) {
|
|
44
|
+
const sorted = [...people].sort(([a], [b]) => a.localeCompare(b));
|
|
45
|
+
for (let left = 0; left < sorted.length; left++)
|
|
46
|
+
for (let right = left + 1; right < sorted.length; right++) {
|
|
47
|
+
if (found.length >= MOST_PAIRS)
|
|
48
|
+
return found;
|
|
49
|
+
const [from, identity] = sorted[left];
|
|
50
|
+
const [to] = sorted[right];
|
|
51
|
+
found.push({
|
|
52
|
+
from,
|
|
53
|
+
to,
|
|
54
|
+
kind: "related-to",
|
|
55
|
+
weight: WEIGHT,
|
|
56
|
+
why: `addresses share the domain ${domain}; this does not prove they work together`,
|
|
57
|
+
evidence: [],
|
|
58
|
+
identity,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { aliasHits } from "./alias-hits.js";
|
|
2
|
+
export { chatSenders } from "./chat-senders.js";
|
|
3
|
+
export { chatTitle } from "./chat-title.js";
|
|
4
|
+
export { emailDomain } from "./email-domain.js";
|
|
5
|
+
export { mailThread } from "./mail-thread.js";
|
|
6
|
+
export { memberChats } from "./member-chats.js";
|
|
7
|
+
export { memberDialog } from "./member-dialog.js";
|
|
8
|
+
export { BUILT_IN_PURPOSES, combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
|
|
9
|
+
export type { LinkCandidate, LinkScorer, ScannedProject, ScannedProposal, ScanResult, ScorerInput, Signal, } from "./types.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { aliasHits } from "./alias-hits.js";
|
|
2
|
+
export { chatSenders } from "./chat-senders.js";
|
|
3
|
+
export { chatTitle } from "./chat-title.js";
|
|
4
|
+
export { emailDomain } from "./email-domain.js";
|
|
5
|
+
export { mailThread } from "./mail-thread.js";
|
|
6
|
+
export { memberChats } from "./member-chats.js";
|
|
7
|
+
export { memberDialog } from "./member-dialog.js";
|
|
8
|
+
export { BUILT_IN_PURPOSES, combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { formatReference } from "../../domain/references.js";
|
|
2
|
+
import { identitiesIn, namesProject } from "./shared.js";
|
|
3
|
+
const NAMED = 0.7;
|
|
4
|
+
const MEMBER_ONLY = 0.3;
|
|
5
|
+
const READ_LIMIT = 500;
|
|
6
|
+
/** A mail thread a confirmed member writes in or is written to: strong when its subject names the project. */
|
|
7
|
+
export const mailThread = {
|
|
8
|
+
id: "mail-thread",
|
|
9
|
+
version: 1,
|
|
10
|
+
candidates: async ({ store, project, confirmed, sources, since, owner, signal }) => {
|
|
11
|
+
if (!project)
|
|
12
|
+
return [];
|
|
13
|
+
const names = namesProject(project);
|
|
14
|
+
const people = confirmed.people.filter((person) => person !== owner);
|
|
15
|
+
const found = [];
|
|
16
|
+
for (const key of sources) {
|
|
17
|
+
if (key.provider !== "email")
|
|
18
|
+
continue;
|
|
19
|
+
const account = await store.accounts.storedAccount(key).catch(() => undefined);
|
|
20
|
+
if (!account)
|
|
21
|
+
continue;
|
|
22
|
+
const threads = new Map();
|
|
23
|
+
for (const { person, id } of await identitiesIn(store, people, "email")) {
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
const emails = await store.mail.emails({
|
|
26
|
+
accountId: account.id,
|
|
27
|
+
participant: id,
|
|
28
|
+
since: Date.parse(since),
|
|
29
|
+
limit: READ_LIMIT,
|
|
30
|
+
});
|
|
31
|
+
for (const email of emails) {
|
|
32
|
+
const held = threads.get(email.emailThreadId) ?? {
|
|
33
|
+
members: new Set(),
|
|
34
|
+
subject: email.subject,
|
|
35
|
+
email: email.externalId,
|
|
36
|
+
};
|
|
37
|
+
held.members.add(person);
|
|
38
|
+
threads.set(email.emailThreadId, held);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const [id, { members, subject, email }] of threads) {
|
|
42
|
+
const thread = (await store.mail.thread(id))?.thread;
|
|
43
|
+
if (!thread)
|
|
44
|
+
continue;
|
|
45
|
+
const named = names(thread.subject ?? subject) !== undefined;
|
|
46
|
+
const chat = formatReference({
|
|
47
|
+
type: "chat",
|
|
48
|
+
provider: "email",
|
|
49
|
+
account: String(key.account),
|
|
50
|
+
chat: thread.externalId,
|
|
51
|
+
});
|
|
52
|
+
const who = [...members].sort();
|
|
53
|
+
found.push({
|
|
54
|
+
from: chat,
|
|
55
|
+
to: project.ref,
|
|
56
|
+
kind: "member-of",
|
|
57
|
+
weight: named ? NAMED : MEMBER_ONLY,
|
|
58
|
+
why: `a thread with ${who.length > 1 ? `${who.length} members` : `a member (${who[0]})`}${named ? "; its subject names the project" : ""}`,
|
|
59
|
+
evidence: [
|
|
60
|
+
formatReference({
|
|
61
|
+
type: "message",
|
|
62
|
+
provider: "email",
|
|
63
|
+
account: String(key.account),
|
|
64
|
+
chat: thread.externalId,
|
|
65
|
+
message: email,
|
|
66
|
+
}),
|
|
67
|
+
...who,
|
|
68
|
+
].slice(0, 5),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return found;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { chatRef, identitiesIn } from "./shared.js";
|
|
2
|
+
const WEIGHT = 0.5;
|
|
3
|
+
/** A group or channel where two or more of the project's confirmed person members are members. */
|
|
4
|
+
export const memberChats = {
|
|
5
|
+
id: "member-chats",
|
|
6
|
+
version: 1,
|
|
7
|
+
candidates: async ({ store, project, confirmed, sources, owner, signal }) => {
|
|
8
|
+
if (!project)
|
|
9
|
+
return [];
|
|
10
|
+
const people = confirmed.people.filter((person) => person !== owner);
|
|
11
|
+
const found = [];
|
|
12
|
+
for (const key of sources) {
|
|
13
|
+
const members = new Map();
|
|
14
|
+
for (const { person, id } of await identitiesIn(store, people, key.provider)) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
for (const chat of await store.chats.chatsWith(key, id)) {
|
|
17
|
+
if (chat.kind !== "group" && chat.kind !== "channel")
|
|
18
|
+
continue;
|
|
19
|
+
const held = members.get(String(chat.id)) ?? new Set();
|
|
20
|
+
held.add(person);
|
|
21
|
+
members.set(String(chat.id), held);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const [chat, held] of members) {
|
|
25
|
+
if (held.size < 2)
|
|
26
|
+
continue;
|
|
27
|
+
const named = [...held].sort();
|
|
28
|
+
found.push({
|
|
29
|
+
from: chatRef(key, chat),
|
|
30
|
+
to: project.ref,
|
|
31
|
+
kind: "member-of",
|
|
32
|
+
weight: WEIGHT,
|
|
33
|
+
why: `a group with ${held.size} confirmed members (${named.slice(0, 3).join(", ")}${held.size > 3 ? ", …" : ""})`,
|
|
34
|
+
evidence: named.slice(0, 5),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return found;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { chatRef, identitiesIn, projectsOf } from "./shared.js";
|
|
2
|
+
const WEIGHT = 0.6;
|
|
3
|
+
/** A dialog with a confirmed person member, divided by how many projects that person is a member of. */
|
|
4
|
+
export const memberDialog = {
|
|
5
|
+
id: "member-dialog",
|
|
6
|
+
version: 1,
|
|
7
|
+
candidates: async ({ store, project, confirmed, sources, owner, signal }) => {
|
|
8
|
+
if (!project)
|
|
9
|
+
return [];
|
|
10
|
+
const people = confirmed.people.filter((person) => person !== owner);
|
|
11
|
+
const projects = new Map();
|
|
12
|
+
const found = [];
|
|
13
|
+
for (const key of sources)
|
|
14
|
+
for (const { person, id } of await identitiesIn(store, people, key.provider)) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
for (const chat of await store.chats.chatsWith(key, id)) {
|
|
17
|
+
if (chat.kind !== "dialog")
|
|
18
|
+
continue;
|
|
19
|
+
if (!projects.has(person))
|
|
20
|
+
projects.set(person, Math.max(1, await projectsOf(store, person)));
|
|
21
|
+
const shared = projects.get(person) ?? 1;
|
|
22
|
+
found.push({
|
|
23
|
+
from: chatRef(key, chat.id),
|
|
24
|
+
to: project.ref,
|
|
25
|
+
kind: "member-of",
|
|
26
|
+
weight: WEIGHT / shared,
|
|
27
|
+
why: `a dialog with a member (${person})${shared > 1 ? `, who is in ${shared} projects` : ""}`,
|
|
28
|
+
evidence: [person],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return found;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MessageStore } from "../../store/store.js";
|
|
2
|
+
import type { AccountKey } from "../../store/types.js";
|
|
3
|
+
import type { LinkScorer, ScanResult } from "./types.js";
|
|
4
|
+
/** A new scorer is one file and one line here. */
|
|
5
|
+
export declare const SCORERS: readonly LinkScorer[];
|
|
6
|
+
/** What a scan with no project runs: person ↔ person rules. */
|
|
7
|
+
export declare const PERSON_SCORERS: readonly LinkScorer[];
|
|
8
|
+
/** The rule bot every scan proposal is written by: only the owner decides its proposals. */
|
|
9
|
+
export declare const SCAN_BOT: {
|
|
10
|
+
readonly bot: "memo-scan";
|
|
11
|
+
readonly kind: "rule";
|
|
12
|
+
};
|
|
13
|
+
export declare const BUILT_IN_PURPOSES: readonly ["work", "sales", "support", "other"];
|
|
14
|
+
export declare const DEFAULT_THRESHOLD = 0.5;
|
|
15
|
+
/** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
|
|
16
|
+
export declare const PERSON_THRESHOLD = 0.3;
|
|
17
|
+
export declare const DEFAULT_LIMIT = 20;
|
|
18
|
+
export interface ScanInput {
|
|
19
|
+
/** A project's key, name, alias or `project:<id>`; `null` runs the person ↔ person rules. */
|
|
20
|
+
project: string | null;
|
|
21
|
+
sources: AccountKey[];
|
|
22
|
+
since: string;
|
|
23
|
+
/** `false` reads only: nothing is written, and each proposal says `dry-run`. */
|
|
24
|
+
write: boolean;
|
|
25
|
+
scorers?: readonly LinkScorer[];
|
|
26
|
+
threshold?: number;
|
|
27
|
+
/** Most new proposals in one run, 1–50. */
|
|
28
|
+
limit?: number;
|
|
29
|
+
/** The host's configured purposes; built-in when left out. */
|
|
30
|
+
purposes?: readonly string[];
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/** 1 − Π(1 − wᵢ), over each scorer's strongest signal for the pair. */
|
|
34
|
+
export declare const combinedScore: (weights: readonly number[]) => number;
|
|
35
|
+
/**
|
|
36
|
+
* Runs the scorers for one project over the sources named, combines their signals per pair, and proposes what scores
|
|
37
|
+
* `threshold` or more, highest first, at most `limit` new ones. A pair a link joins, or that was rejected once, is
|
|
38
|
+
* never proposed; a pending one is updated in place. People are matched by identity only. With `write: false`
|
|
39
|
+
* nothing is written.
|
|
40
|
+
*/
|
|
41
|
+
export declare const scanProjectLinks: (store: MessageStore, input: ScanInput) => Promise<ScanResult>;
|