@wirecat/cli-core 0.393.0 → 0.394.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 +16 -0
- package/dist/cli/messenger/archive-commands.js +28 -0
- package/dist/cli/messenger/contacts-command.js +29 -0
- package/dist/cli/messenger/stats-command.js +26 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/sends/permissions.js +2 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/people.d.ts +5 -0
- package/dist/services/people.js +2 -0
- package/dist/services/person-context.d.ts +12 -2
- package/dist/services/person-context.js +2 -2
- package/dist/services/person-preparation.d.ts +66 -0
- package/dist/services/person-preparation.js +126 -0
- package/dist/store/exports.d.ts +1 -0
- package/dist/store/people.d.ts +6 -0
- package/dist/store/people.js +2 -0
- package/dist/store/sqlite/person-preparation.d.ts +48 -0
- package/dist/store/sqlite/person-preparation.js +72 -0
- package/package.json +1 -1
- package/parity.json +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## 0.394.0 — 23.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Statistics commands include examples in help and command discovery, with guidance for reusing an exact evidence selection.
|
|
13
|
+
|
|
14
|
+
- Store commands include concrete examples in help and command discovery.
|
|
15
|
+
|
|
16
|
+
- **`contacts prepare <person>`: what to know before talking to someone.** What `contacts context` gives, plus the
|
|
17
|
+
one-to-one chats where their message is the newest the store holds, open tasks assigned to them, accepted facts
|
|
18
|
+
about them and their projects, your notes about them (withheld when the profile denies `contacts.notes.list`), and
|
|
19
|
+
their recent meetings — each with its reference, under one byte budget, with `prepare.coverage` saying what each
|
|
20
|
+
section left out and why. Tasks of another account of the same messenger, and facts on a project bound to one, stay
|
|
21
|
+
out. It reads messages, so `messages: deny` refuses it, as it does `contacts context`. `personPreparation` in
|
|
22
|
+
services and `people.preparation` on the store.
|
|
23
|
+
|
|
8
24
|
## 0.393.0 — 23.10.2026
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -12,6 +12,7 @@ import { kindsOf } from "../../services/inbox.js";
|
|
|
12
12
|
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
14
14
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
15
|
+
import { commandAt } from "../steps.js";
|
|
15
16
|
import { jobsCommand } from "./backfill-command.js";
|
|
16
17
|
import { messengerContext } from "./context.js";
|
|
17
18
|
import { admitPassword, linesOf, openFolder, runFileFor, saveManifest, sealRun, writeChanges } from "./export-folder.js";
|
|
@@ -30,6 +31,33 @@ export const storeCommand = (messenger) => {
|
|
|
30
31
|
.addCommand(clearCommand(messenger));
|
|
31
32
|
for (const command of storeMaintenanceCommands(messenger))
|
|
32
33
|
store.addCommand(command);
|
|
34
|
+
const examples = {
|
|
35
|
+
backup: "./backup.db",
|
|
36
|
+
check: "",
|
|
37
|
+
clear: "--left --dry-run",
|
|
38
|
+
"copies delete": "messages_copy_1",
|
|
39
|
+
decrypt: "./backup.enc --output ./restored.db",
|
|
40
|
+
export: '"Book club" --output ./messages.jsonl',
|
|
41
|
+
"gaps plan": '"Book club"',
|
|
42
|
+
"gaps repair": '"Book club" --limit 100',
|
|
43
|
+
info: "",
|
|
44
|
+
"jobs cancel": "job-101",
|
|
45
|
+
"jobs clear": "",
|
|
46
|
+
"jobs list": "",
|
|
47
|
+
"jobs retry": "--failed",
|
|
48
|
+
"jobs show": "",
|
|
49
|
+
migrate: "",
|
|
50
|
+
reindex: "",
|
|
51
|
+
repair: "--dry-run",
|
|
52
|
+
reset: "",
|
|
53
|
+
restore: "./backup.db",
|
|
54
|
+
status: '"Book club"',
|
|
55
|
+
};
|
|
56
|
+
for (const [path, args] of Object.entries(examples)) {
|
|
57
|
+
const command = commandAt(store, path);
|
|
58
|
+
if (command)
|
|
59
|
+
annotate(command, { examples: [`${messenger.app.command} personal store ${path} ${args}`.trimEnd()] });
|
|
60
|
+
}
|
|
33
61
|
return store;
|
|
34
62
|
};
|
|
35
63
|
/**
|
|
@@ -137,6 +137,35 @@ export const contactsCommand = (messenger) => {
|
|
|
137
137
|
if (hasMore)
|
|
138
138
|
context.renderer.note("cut at --limit; a larger one shows more");
|
|
139
139
|
});
|
|
140
|
+
annotate(contacts.command("prepare"), {
|
|
141
|
+
effects: REMOTE_READ,
|
|
142
|
+
examples: [`${messenger.app.command} contacts prepare Ana`],
|
|
143
|
+
})
|
|
144
|
+
.summary("what to know before you talk to someone, each item with its reference")
|
|
145
|
+
.description("before you talk to someone: what contacts context gives, plus the one-to-one chats where they wrote last, " +
|
|
146
|
+
"open tasks, accepted facts, your notes and their recent meetings, each with its reference; never connects")
|
|
147
|
+
.argument("<person>", "their id, @username, or part of their name")
|
|
148
|
+
.option("--limit <n>", `at most this many messages, and items in each section; ${CONTEXT_MESSAGES} if not given`, positiveCount("--limit"))
|
|
149
|
+
.option("--since-time <time>", "no message or meeting older than this ISO 8601 time, or 2h / 1d ago")
|
|
150
|
+
.action(async function (person) {
|
|
151
|
+
const { limit, sinceTime } = this.opts();
|
|
152
|
+
const context = messengerContext(this, messenger);
|
|
153
|
+
const window = dateWindow(sinceTime === undefined ? {} : { sinceTime });
|
|
154
|
+
const found = await context.withServices((services) => services.people.prepare(person, {
|
|
155
|
+
messages: limit ?? CONTEXT_MESSAGES,
|
|
156
|
+
notes: levelFor(context.settings.permissions, "contacts.notes.list").level !== "deny",
|
|
157
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
158
|
+
}));
|
|
159
|
+
const { hasMore, ...rest } = found;
|
|
160
|
+
renderAnswer(this, context, itemOf(snakeKeys(rest), {
|
|
161
|
+
next: syncsFirst(messenger)
|
|
162
|
+
? found.notRead
|
|
163
|
+
.filter((chat) => chat.provider === messenger.provider)
|
|
164
|
+
.map((chat) => nextStep("messages.sync", { args: { chat: chat.chatId } }))
|
|
165
|
+
: [],
|
|
166
|
+
meta: { ...window.meta, ...(hasMore ? { truncated: true } : {}) },
|
|
167
|
+
}));
|
|
168
|
+
});
|
|
140
169
|
annotate(contacts.command("timeline"), { effects: REMOTE_READ })
|
|
141
170
|
.description("everything one person took part in, in every messenger linked to them — messages they wrote or were " +
|
|
142
171
|
"mentioned in, chats, mail, meetings, tasks — newest first, from the store; never connects")
|
|
@@ -12,6 +12,7 @@ import { EVENTS_DAYS } from "../../services/chats.js";
|
|
|
12
12
|
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { destroysNothing, LOCAL_WRITE } from "../effects.js";
|
|
14
14
|
import { renderAnswer } from "../envelope.js";
|
|
15
|
+
import { commandAt } from "../steps.js";
|
|
15
16
|
import { adminStatisticsCommand } from "./admin-statistics-command.js";
|
|
16
17
|
import { statsCommand as chatStatsCommand } from "./chats-stats-command.js";
|
|
17
18
|
import { messengerContext } from "./context.js";
|
|
@@ -84,5 +85,30 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
|
|
|
84
85
|
context.renderer.note("partial data — the chart shows lower bounds");
|
|
85
86
|
renderAnswer(this, context, chartAnswer(chart, chartFile, window.meta));
|
|
86
87
|
});
|
|
88
|
+
const examples = {
|
|
89
|
+
charts: '"Book club" --output ./activity.svg',
|
|
90
|
+
"chats newcomers": '"Book club"',
|
|
91
|
+
"chats retention": '"Book club"',
|
|
92
|
+
"chats show": '"Book club"',
|
|
93
|
+
"contacts evidence": '101 --selection "$selection" --component messages',
|
|
94
|
+
"contacts responses": '--chat "Project room"',
|
|
95
|
+
"contacts top": '--chat "Book club" --limit 10',
|
|
96
|
+
"messages counters refresh": '--chat "Book club" --dry-run',
|
|
97
|
+
"messages counters show": '--chat "Book club" --limit 10',
|
|
98
|
+
"messages discussion": '--chat "Book club"',
|
|
99
|
+
"messages evidence": '"$message" --selection "$selection" --component reactions',
|
|
100
|
+
"messages show": '--chat "Book club" --by day',
|
|
101
|
+
"messages top": '--chat "Book club" --limit 10',
|
|
102
|
+
"messages unanswered": '--chat "Project room"',
|
|
103
|
+
"tasks show": '--chat "Project room"',
|
|
104
|
+
};
|
|
105
|
+
for (const [path, args] of Object.entries(examples)) {
|
|
106
|
+
const command = commandAt(stats, path);
|
|
107
|
+
if (command)
|
|
108
|
+
annotate(command, { examples: [`${messenger.app.command} personal stats ${path} ${args}`] });
|
|
109
|
+
}
|
|
110
|
+
for (const target of ["messages", "contacts"]) {
|
|
111
|
+
commandAt(stats, `${target} evidence`)?.addHelpText("after", `\nSet selection to drilldown.selection from stats ${target} top; copy the matching row's ${target === "messages" ? "message locator into message" : "person id"}.\n`);
|
|
112
|
+
}
|
|
87
113
|
return stats;
|
|
88
114
|
};
|
|
@@ -38,6 +38,7 @@ export declare const meetingTools: ({
|
|
|
38
38
|
max_read_bytes?: undefined;
|
|
39
39
|
cursor?: undefined;
|
|
40
40
|
} | {
|
|
41
|
+
limit?: undefined;
|
|
41
42
|
event_id?: undefined;
|
|
42
43
|
series_id?: undefined;
|
|
43
44
|
page?: undefined;
|
|
@@ -53,11 +54,11 @@ export declare const meetingTools: ({
|
|
|
53
54
|
format?: undefined;
|
|
54
55
|
transcript_id?: undefined;
|
|
55
56
|
query?: undefined;
|
|
56
|
-
limit?: undefined;
|
|
57
57
|
scan_limit?: undefined;
|
|
58
58
|
max_read_bytes?: undefined;
|
|
59
59
|
cursor?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
limit?: undefined;
|
|
61
62
|
event_id?: undefined;
|
|
62
63
|
series_id?: undefined;
|
|
63
64
|
page?: undefined;
|
|
@@ -75,11 +76,11 @@ export declare const meetingTools: ({
|
|
|
75
76
|
format?: undefined;
|
|
76
77
|
transcript_id?: undefined;
|
|
77
78
|
query?: undefined;
|
|
78
|
-
limit?: undefined;
|
|
79
79
|
scan_limit?: undefined;
|
|
80
80
|
max_read_bytes?: undefined;
|
|
81
81
|
cursor?: undefined;
|
|
82
82
|
} | {
|
|
83
|
+
limit?: undefined;
|
|
83
84
|
event_id?: undefined;
|
|
84
85
|
series_id?: undefined;
|
|
85
86
|
page?: undefined;
|
|
@@ -101,7 +102,6 @@ export declare const meetingTools: ({
|
|
|
101
102
|
minimum: number;
|
|
102
103
|
};
|
|
103
104
|
query?: undefined;
|
|
104
|
-
limit?: undefined;
|
|
105
105
|
scan_limit?: undefined;
|
|
106
106
|
max_read_bytes?: undefined;
|
|
107
107
|
cursor?: undefined;
|
|
@@ -188,6 +188,7 @@ export declare const meetingTools: ({
|
|
|
188
188
|
max_read_bytes?: undefined;
|
|
189
189
|
cursor?: undefined;
|
|
190
190
|
} | {
|
|
191
|
+
limit?: undefined;
|
|
191
192
|
event_id?: undefined;
|
|
192
193
|
series_id?: undefined;
|
|
193
194
|
page?: undefined;
|
|
@@ -203,11 +204,11 @@ export declare const meetingTools: ({
|
|
|
203
204
|
format?: undefined;
|
|
204
205
|
transcript_id?: undefined;
|
|
205
206
|
query?: undefined;
|
|
206
|
-
limit?: undefined;
|
|
207
207
|
scan_limit?: undefined;
|
|
208
208
|
max_read_bytes?: undefined;
|
|
209
209
|
cursor?: undefined;
|
|
210
210
|
} | {
|
|
211
|
+
limit?: undefined;
|
|
211
212
|
event_id?: undefined;
|
|
212
213
|
series_id?: undefined;
|
|
213
214
|
page?: undefined;
|
|
@@ -225,11 +226,11 @@ export declare const meetingTools: ({
|
|
|
225
226
|
format?: undefined;
|
|
226
227
|
transcript_id?: undefined;
|
|
227
228
|
query?: undefined;
|
|
228
|
-
limit?: undefined;
|
|
229
229
|
scan_limit?: undefined;
|
|
230
230
|
max_read_bytes?: undefined;
|
|
231
231
|
cursor?: undefined;
|
|
232
232
|
} | {
|
|
233
|
+
limit?: undefined;
|
|
233
234
|
event_id?: undefined;
|
|
234
235
|
series_id?: undefined;
|
|
235
236
|
page?: undefined;
|
|
@@ -251,7 +252,6 @@ export declare const meetingTools: ({
|
|
|
251
252
|
minimum: number;
|
|
252
253
|
};
|
|
253
254
|
query?: undefined;
|
|
254
|
-
limit?: undefined;
|
|
255
255
|
scan_limit?: undefined;
|
|
256
256
|
max_read_bytes?: undefined;
|
|
257
257
|
cursor?: undefined;
|
|
@@ -338,6 +338,7 @@ export declare const meetingTools: ({
|
|
|
338
338
|
max_read_bytes?: undefined;
|
|
339
339
|
cursor?: undefined;
|
|
340
340
|
} | {
|
|
341
|
+
limit?: undefined;
|
|
341
342
|
event_id?: undefined;
|
|
342
343
|
series_id?: undefined;
|
|
343
344
|
page?: undefined;
|
|
@@ -353,11 +354,11 @@ export declare const meetingTools: ({
|
|
|
353
354
|
format?: undefined;
|
|
354
355
|
transcript_id?: undefined;
|
|
355
356
|
query?: undefined;
|
|
356
|
-
limit?: undefined;
|
|
357
357
|
scan_limit?: undefined;
|
|
358
358
|
max_read_bytes?: undefined;
|
|
359
359
|
cursor?: undefined;
|
|
360
360
|
} | {
|
|
361
|
+
limit?: undefined;
|
|
361
362
|
event_id?: undefined;
|
|
362
363
|
series_id?: undefined;
|
|
363
364
|
page?: undefined;
|
|
@@ -375,11 +376,11 @@ export declare const meetingTools: ({
|
|
|
375
376
|
format?: undefined;
|
|
376
377
|
transcript_id?: undefined;
|
|
377
378
|
query?: undefined;
|
|
378
|
-
limit?: undefined;
|
|
379
379
|
scan_limit?: undefined;
|
|
380
380
|
max_read_bytes?: undefined;
|
|
381
381
|
cursor?: undefined;
|
|
382
382
|
} | {
|
|
383
|
+
limit?: undefined;
|
|
383
384
|
event_id?: undefined;
|
|
384
385
|
series_id?: undefined;
|
|
385
386
|
page?: undefined;
|
|
@@ -401,7 +402,6 @@ export declare const meetingTools: ({
|
|
|
401
402
|
minimum: number;
|
|
402
403
|
};
|
|
403
404
|
query?: undefined;
|
|
404
|
-
limit?: undefined;
|
|
405
405
|
scan_limit?: undefined;
|
|
406
406
|
max_read_bytes?: undefined;
|
|
407
407
|
cursor?: undefined;
|
|
@@ -488,6 +488,7 @@ export declare const meetingTools: ({
|
|
|
488
488
|
max_read_bytes?: undefined;
|
|
489
489
|
cursor?: undefined;
|
|
490
490
|
} | {
|
|
491
|
+
limit?: undefined;
|
|
491
492
|
event_id?: undefined;
|
|
492
493
|
series_id?: undefined;
|
|
493
494
|
page?: undefined;
|
|
@@ -503,11 +504,11 @@ export declare const meetingTools: ({
|
|
|
503
504
|
format?: undefined;
|
|
504
505
|
transcript_id?: undefined;
|
|
505
506
|
query?: undefined;
|
|
506
|
-
limit?: undefined;
|
|
507
507
|
scan_limit?: undefined;
|
|
508
508
|
max_read_bytes?: undefined;
|
|
509
509
|
cursor?: undefined;
|
|
510
510
|
} | {
|
|
511
|
+
limit?: undefined;
|
|
511
512
|
event_id?: undefined;
|
|
512
513
|
series_id?: undefined;
|
|
513
514
|
page?: undefined;
|
|
@@ -525,11 +526,11 @@ export declare const meetingTools: ({
|
|
|
525
526
|
format?: undefined;
|
|
526
527
|
transcript_id?: undefined;
|
|
527
528
|
query?: undefined;
|
|
528
|
-
limit?: undefined;
|
|
529
529
|
scan_limit?: undefined;
|
|
530
530
|
max_read_bytes?: undefined;
|
|
531
531
|
cursor?: undefined;
|
|
532
532
|
} | {
|
|
533
|
+
limit?: undefined;
|
|
533
534
|
event_id?: undefined;
|
|
534
535
|
series_id?: undefined;
|
|
535
536
|
page?: undefined;
|
|
@@ -551,7 +552,6 @@ export declare const meetingTools: ({
|
|
|
551
552
|
minimum: number;
|
|
552
553
|
};
|
|
553
554
|
query?: undefined;
|
|
554
|
-
limit?: undefined;
|
|
555
555
|
scan_limit?: undefined;
|
|
556
556
|
max_read_bytes?: undefined;
|
|
557
557
|
cursor?: undefined;
|
|
@@ -638,6 +638,7 @@ export declare const meetingTools: ({
|
|
|
638
638
|
max_read_bytes?: undefined;
|
|
639
639
|
cursor?: undefined;
|
|
640
640
|
} | {
|
|
641
|
+
limit?: undefined;
|
|
641
642
|
event_id?: undefined;
|
|
642
643
|
series_id?: undefined;
|
|
643
644
|
page?: undefined;
|
|
@@ -653,11 +654,11 @@ export declare const meetingTools: ({
|
|
|
653
654
|
format?: undefined;
|
|
654
655
|
transcript_id?: undefined;
|
|
655
656
|
query?: undefined;
|
|
656
|
-
limit?: undefined;
|
|
657
657
|
scan_limit?: undefined;
|
|
658
658
|
max_read_bytes?: undefined;
|
|
659
659
|
cursor?: undefined;
|
|
660
660
|
} | {
|
|
661
|
+
limit?: undefined;
|
|
661
662
|
event_id?: undefined;
|
|
662
663
|
series_id?: undefined;
|
|
663
664
|
page?: undefined;
|
|
@@ -675,11 +676,11 @@ export declare const meetingTools: ({
|
|
|
675
676
|
format?: undefined;
|
|
676
677
|
transcript_id?: undefined;
|
|
677
678
|
query?: undefined;
|
|
678
|
-
limit?: undefined;
|
|
679
679
|
scan_limit?: undefined;
|
|
680
680
|
max_read_bytes?: undefined;
|
|
681
681
|
cursor?: undefined;
|
|
682
682
|
} | {
|
|
683
|
+
limit?: undefined;
|
|
683
684
|
event_id?: undefined;
|
|
684
685
|
series_id?: undefined;
|
|
685
686
|
page?: undefined;
|
|
@@ -701,7 +702,6 @@ export declare const meetingTools: ({
|
|
|
701
702
|
minimum: number;
|
|
702
703
|
};
|
|
703
704
|
query?: undefined;
|
|
704
|
-
limit?: undefined;
|
|
705
705
|
scan_limit?: undefined;
|
|
706
706
|
max_read_bytes?: undefined;
|
|
707
707
|
cursor?: undefined;
|
|
@@ -450,7 +450,8 @@ export const keyForCommand = (path) => {
|
|
|
450
450
|
return path.join(".");
|
|
451
451
|
if (top === "messages" && next === "sync")
|
|
452
452
|
return "messages";
|
|
453
|
-
if (SHOW_MESSAGES.has(top) ||
|
|
453
|
+
if (SHOW_MESSAGES.has(top) ||
|
|
454
|
+
(top === "contacts" && (next === "context" || next === "prepare" || next === "timeline")))
|
|
454
455
|
return "messages";
|
|
455
456
|
if (RESOURCES.includes(top))
|
|
456
457
|
return path.join(".");
|
package/dist/services/index.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export type { ContactSync, PeopleService } from "./people.js";
|
|
|
80
80
|
export { peopleService, phoneOf } from "./people.js";
|
|
81
81
|
export type { ContextMessage, ContextOptions, NotReadReason, PersonContext, SharedChat, } from "./person-context.js";
|
|
82
82
|
export { CONTEXT_BYTES, CONTEXT_MESSAGES, identityIn, personContext } from "./person-context.js";
|
|
83
|
+
export type { PersonPreparation, WaitingDialog } from "./person-preparation.js";
|
|
84
|
+
export { PREPARE_BYTES, personPreparation } from "./person-preparation.js";
|
|
83
85
|
export type { PersonTimeline, Scope, TimelineItem, TimelineOptions } from "./person-timeline.js";
|
|
84
86
|
export { personTimeline, SCOPES, TIMELINE_ITEMS, TIMELINE_MAX } from "./person-timeline.js";
|
|
85
87
|
export type { RankedRow, RankingFound, RankingQuery, RankingsService } from "./rankings.js";
|
package/dist/services/index.js
CHANGED
|
@@ -57,6 +57,7 @@ export { searchStore } from "./messages-archive-search.js";
|
|
|
57
57
|
export { moderationService } from "./moderation.js";
|
|
58
58
|
export { peopleService, phoneOf } from "./people.js";
|
|
59
59
|
export { CONTEXT_BYTES, CONTEXT_MESSAGES, identityIn, personContext } from "./person-context.js";
|
|
60
|
+
export { PREPARE_BYTES, personPreparation } from "./person-preparation.js";
|
|
60
61
|
export { personTimeline, SCOPES, TIMELINE_ITEMS, TIMELINE_MAX } from "./person-timeline.js";
|
|
61
62
|
export { rankingsService } from "./rankings.js";
|
|
62
63
|
export { searchRecordOf } from "./search-record.js";
|
|
@@ -5,6 +5,7 @@ import type { PersonRecord } from "../store/store.js";
|
|
|
5
5
|
import type { PageWindow } from "./chats.js";
|
|
6
6
|
import { type ServiceDeps } from "./deps.js";
|
|
7
7
|
import { type ContextOptions, type Detail, type PersonContext, type PersonMessages } from "./person-context.js";
|
|
8
|
+
import { type PersonPreparation } from "./person-preparation.js";
|
|
8
9
|
import { type PersonTimeline, type TimelineOptions } from "./person-timeline.js";
|
|
9
10
|
export interface ContactSync {
|
|
10
11
|
added: number;
|
|
@@ -52,6 +53,10 @@ export interface PeopleService {
|
|
|
52
53
|
}>>;
|
|
53
54
|
/** What the store holds about them, across every messenger linked to them; never connects. */
|
|
54
55
|
context(person: string, options?: ContextOptions): Promise<PersonContext>;
|
|
56
|
+
/** `context` and what to know before talking to them: chats waiting on you, tasks, facts, notes, meetings. */
|
|
57
|
+
prepare(person: string, options?: ContextOptions & {
|
|
58
|
+
notes?: boolean;
|
|
59
|
+
}): Promise<PersonPreparation>;
|
|
55
60
|
/** Everything they took part in, from the store's involvement index; never connects. */
|
|
56
61
|
timeline(person: string, options?: TimelineOptions): Promise<PersonTimeline>;
|
|
57
62
|
/**
|
package/dist/services/people.js
CHANGED
|
@@ -8,6 +8,7 @@ import { newOperationId } from "../sends/send-id.js";
|
|
|
8
8
|
import { fromStore, storeIfOpen } from "./deps.js";
|
|
9
9
|
import { storedChatId } from "./message-resolution.js";
|
|
10
10
|
import { CHAT_MESSAGES, identityIn, personContext, personMessages, } from "./person-context.js";
|
|
11
|
+
import { personPreparation } from "./person-preparation.js";
|
|
11
12
|
import { personProfile } from "./person-profile.js";
|
|
12
13
|
import { personTimeline } from "./person-timeline.js";
|
|
13
14
|
export const peopleService = (deps) => {
|
|
@@ -75,6 +76,7 @@ export const peopleService = (deps) => {
|
|
|
75
76
|
},
|
|
76
77
|
profile: (person) => personProfile(deps, person),
|
|
77
78
|
context: async (person, options) => personContext(await deps.store(), await deps.account(), person, options),
|
|
79
|
+
prepare: async (person, options) => personPreparation(await deps.store(), await deps.account(), person, options),
|
|
78
80
|
timeline: async (person, options) => personTimeline(await deps.store(), await deps.account(), person, options),
|
|
79
81
|
messagesIn: async (person, { chats, limit, detail, fetch = false }) => {
|
|
80
82
|
const store = await deps.store();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ChatKind, Id, Provider } from "../domain/models.js";
|
|
2
|
-
import type { AccountKey, IdentityRef, MessageStore, PersonRecord } from "../store/store.js";
|
|
1
|
+
import type { ChatKind, Id, Message, Provider } from "../domain/models.js";
|
|
2
|
+
import type { AccountKey, IdentityRef, LinkedIdentity, MessageStore, PersonRecord } from "../store/store.js";
|
|
3
3
|
export declare const CONTEXT_MESSAGES = 10;
|
|
4
4
|
export declare const CONTEXT_BYTES: number;
|
|
5
5
|
export interface ContextMessage {
|
|
@@ -60,6 +60,16 @@ export interface ContextOptions {
|
|
|
60
60
|
/** ms: nothing older. */
|
|
61
61
|
since?: number;
|
|
62
62
|
}
|
|
63
|
+
export declare const toContext: (key: AccountKey, message: Message, chatTitle: string | null) => ContextMessage;
|
|
64
|
+
/**
|
|
65
|
+
* The accounts each identity is read in: for the messenger asked, only the account asked about — a
|
|
66
|
+
* second account of the same messenger is somebody else's view; for a linked messenger, every account
|
|
67
|
+
* of this store that saw that identity.
|
|
68
|
+
*/
|
|
69
|
+
export declare const readsOf: (record: PersonRecord, asked: AccountKey) => {
|
|
70
|
+
key: AccountKey;
|
|
71
|
+
identity: LinkedIdentity;
|
|
72
|
+
}[];
|
|
63
73
|
/**
|
|
64
74
|
* What the store knows about one person, across every identity linked to them. Reads the store
|
|
65
75
|
* only: nothing is fetched, nothing marked read. Identity is by id, never by name.
|
|
@@ -4,7 +4,7 @@ import { pickPerson } from "../resolve.js";
|
|
|
4
4
|
import { messageOfEmail } from "../store/store.js";
|
|
5
5
|
export const CONTEXT_MESSAGES = 10;
|
|
6
6
|
export const CONTEXT_BYTES = 64 * 1024;
|
|
7
|
-
const toContext = (key, message, chatTitle) => ({
|
|
7
|
+
export const toContext = (key, message, chatTitle) => ({
|
|
8
8
|
locator: formatLocator({ ...key, chat: message.chatId, message: message.id }),
|
|
9
9
|
provider: key.provider,
|
|
10
10
|
account: key.account,
|
|
@@ -49,7 +49,7 @@ const newestFirst = (a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp);
|
|
|
49
49
|
* second account of the same messenger is somebody else's view; for a linked messenger, every account
|
|
50
50
|
* of this store that saw that identity.
|
|
51
51
|
*/
|
|
52
|
-
const readsOf = (record, asked) => record.identities.flatMap((identity) => (identity.provider === asked.provider ? [asked.account] : identity.accounts)
|
|
52
|
+
export const readsOf = (record, asked) => record.identities.flatMap((identity) => (identity.provider === asked.provider ? [asked.account] : identity.accounts)
|
|
53
53
|
.filter((account) => identity.accounts.includes(account))
|
|
54
54
|
.map((account) => ({ key: { provider: identity.provider, account }, identity })));
|
|
55
55
|
/**
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Id, Provider } from "../domain/models.js";
|
|
2
|
+
import type { AccountKey, MessageStore, PreparationFact, PreparationNote, PreparationTask } from "../store/store.js";
|
|
3
|
+
import { type ContextMessage, type ContextOptions, type PersonContext } from "./person-context.js";
|
|
4
|
+
import { type PersonMeetingContextItem } from "./person-meeting-context.js";
|
|
5
|
+
export declare const PREPARE_BYTES: number;
|
|
6
|
+
export interface WaitingDialog {
|
|
7
|
+
provider: Provider;
|
|
8
|
+
account: Id;
|
|
9
|
+
chatId: Id;
|
|
10
|
+
title: string | null;
|
|
11
|
+
/** Their message, the newest the store holds in this one-to-one chat. */
|
|
12
|
+
message: ContextMessage;
|
|
13
|
+
/** The store does not hold the whole chat, so a later answer may be missing from it. */
|
|
14
|
+
partial: boolean;
|
|
15
|
+
}
|
|
16
|
+
type Cut = "limit" | "bytes" | null;
|
|
17
|
+
interface SectionCoverage {
|
|
18
|
+
included: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
cutBy: Cut;
|
|
21
|
+
}
|
|
22
|
+
export interface PersonPreparation extends PersonContext {
|
|
23
|
+
prepare: {
|
|
24
|
+
waiting: WaitingDialog[];
|
|
25
|
+
tasks: PreparationTask[];
|
|
26
|
+
facts: PreparationFact[];
|
|
27
|
+
notes: PreparationNote[];
|
|
28
|
+
meetings: PersonMeetingContextItem[];
|
|
29
|
+
limits: {
|
|
30
|
+
items: number;
|
|
31
|
+
bytes: number;
|
|
32
|
+
};
|
|
33
|
+
coverage: {
|
|
34
|
+
waiting: SectionCoverage & {
|
|
35
|
+
rule: "their message is the newest the store holds in a one-to-one chat";
|
|
36
|
+
};
|
|
37
|
+
tasks: SectionCoverage & {
|
|
38
|
+
selection: "open, assigned to them in any role";
|
|
39
|
+
accounts: string[];
|
|
40
|
+
};
|
|
41
|
+
facts: SectionCoverage & {
|
|
42
|
+
selection: "accepted, about them or a project they are a member of";
|
|
43
|
+
};
|
|
44
|
+
/** `withheld` when the profile denies `contacts.notes.list`. */
|
|
45
|
+
notes: SectionCoverage & {
|
|
46
|
+
selection: "written by you, about them";
|
|
47
|
+
withheld?: "contacts.notes.list";
|
|
48
|
+
};
|
|
49
|
+
/** `accounts`: the ones their meetings came from. */
|
|
50
|
+
meetings: SectionCoverage & {
|
|
51
|
+
read: "every account of the store that saw them";
|
|
52
|
+
accounts: string[];
|
|
53
|
+
pending: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `contacts context` and what to know before talking to them: the one-to-one chats where they wrote last, open tasks,
|
|
60
|
+
* accepted facts, your notes and their recent meetings, each item with its reference. The messages keep their own
|
|
61
|
+
* budget; the sections share what is left of `PREPARE_BYTES`, in that order, so evidence comes first.
|
|
62
|
+
*/
|
|
63
|
+
export declare const personPreparation: (store: MessageStore, asked: AccountKey, reference: string, options?: ContextOptions & {
|
|
64
|
+
notes?: boolean;
|
|
65
|
+
}) => Promise<PersonPreparation>;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { parseMeetingReference } from "../meetings/index.js";
|
|
3
|
+
import { CONTEXT_BYTES, CONTEXT_MESSAGES, personContext, readsOf, toContext, } from "./person-context.js";
|
|
4
|
+
import { personMeetingContext } from "./person-meeting-context.js";
|
|
5
|
+
export const PREPARE_BYTES = 96 * 1024;
|
|
6
|
+
const MEETINGS = 5;
|
|
7
|
+
const CUES = 3;
|
|
8
|
+
const size = (value) => Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
9
|
+
const accountName = ({ provider, account }) => `${provider}:${account}`;
|
|
10
|
+
/**
|
|
11
|
+
* `contacts context` and what to know before talking to them: the one-to-one chats where they wrote last, open tasks,
|
|
12
|
+
* accepted facts, your notes and their recent meetings, each item with its reference. The messages keep their own
|
|
13
|
+
* budget; the sections share what is left of `PREPARE_BYTES`, in that order, so evidence comes first.
|
|
14
|
+
*/
|
|
15
|
+
export const personPreparation = async (store, asked, reference, options = {}) => {
|
|
16
|
+
const context = await personContext(store, asked, reference, { ...options, bytes: CONTEXT_BYTES });
|
|
17
|
+
const limit = options.messages ?? CONTEXT_MESSAGES;
|
|
18
|
+
let left = PREPARE_BYTES - size({ recent: context.recent, mentions: context.mentions });
|
|
19
|
+
const budgeted = (page) => {
|
|
20
|
+
const items = [];
|
|
21
|
+
let cut = false;
|
|
22
|
+
for (const item of page.items.slice(0, limit)) {
|
|
23
|
+
const bytes = size(item);
|
|
24
|
+
if (cut || bytes > left) {
|
|
25
|
+
cut = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
left -= bytes;
|
|
29
|
+
items.push(item);
|
|
30
|
+
}
|
|
31
|
+
const over = page.hasMore || page.items.length > limit;
|
|
32
|
+
return {
|
|
33
|
+
items,
|
|
34
|
+
coverage: { included: items.length, hasMore: over || cut, cutBy: cut ? "bytes" : over ? "limit" : null },
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const reads = readsOf(context.person, asked);
|
|
38
|
+
const partial = new Set(context.notRead.map((chat) => `${chat.provider}/${chat.account}/${chat.chatId}`));
|
|
39
|
+
const waiting = [];
|
|
40
|
+
for (const chat of context.shared.filter((one) => one.kind === "dialog")) {
|
|
41
|
+
const key = { provider: chat.provider, account: chat.account };
|
|
42
|
+
const [newest] = (await store.messages.messages(key, chat.chatId, { limit: 1 })).items;
|
|
43
|
+
if (newest?.outgoing !== false)
|
|
44
|
+
continue;
|
|
45
|
+
waiting.push({
|
|
46
|
+
...key,
|
|
47
|
+
chatId: chat.chatId,
|
|
48
|
+
title: chat.title,
|
|
49
|
+
message: toContext(key, newest, chat.title),
|
|
50
|
+
partial: partial.has(`${chat.provider}/${chat.account}/${chat.chatId}`),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
waiting.sort((a, b) => Date.parse(b.message.timestamp) - Date.parse(a.message.timestamp));
|
|
54
|
+
const accounts = [...new Map(reads.map(({ key }) => [accountName(key), key])).values()];
|
|
55
|
+
const held = await store.people.preparation(context.person.uid, { accounts, limit });
|
|
56
|
+
const stored = await store.accounts.storedAccounts();
|
|
57
|
+
const meetings = await meetingsOf(store, context.person.uid, stored, options.since, () => left);
|
|
58
|
+
const sections = {
|
|
59
|
+
waiting: budgeted({ items: waiting, hasMore: false }),
|
|
60
|
+
tasks: budgeted(held.tasks),
|
|
61
|
+
facts: budgeted(held.facts),
|
|
62
|
+
notes: budgeted(options.notes === false ? { items: [], hasMore: false } : held.notes),
|
|
63
|
+
meetings: budgeted({ items: meetings.items, hasMore: meetings.hasMore }),
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
...context,
|
|
67
|
+
prepare: {
|
|
68
|
+
waiting: sections.waiting.items,
|
|
69
|
+
tasks: sections.tasks.items,
|
|
70
|
+
facts: sections.facts.items,
|
|
71
|
+
notes: sections.notes.items,
|
|
72
|
+
meetings: sections.meetings.items,
|
|
73
|
+
limits: { items: limit, bytes: PREPARE_BYTES },
|
|
74
|
+
coverage: {
|
|
75
|
+
waiting: {
|
|
76
|
+
...sections.waiting.coverage,
|
|
77
|
+
rule: "their message is the newest the store holds in a one-to-one chat",
|
|
78
|
+
},
|
|
79
|
+
tasks: {
|
|
80
|
+
...sections.tasks.coverage,
|
|
81
|
+
selection: "open, assigned to them in any role",
|
|
82
|
+
accounts: accounts.map(accountName),
|
|
83
|
+
},
|
|
84
|
+
facts: { ...sections.facts.coverage, selection: "accepted, about them or a project they are a member of" },
|
|
85
|
+
notes: {
|
|
86
|
+
...sections.notes.coverage,
|
|
87
|
+
selection: "written by you, about them",
|
|
88
|
+
...(options.notes === false ? { withheld: "contacts.notes.list" } : {}),
|
|
89
|
+
},
|
|
90
|
+
meetings: {
|
|
91
|
+
...sections.meetings.coverage,
|
|
92
|
+
read: "every account of the store that saw them",
|
|
93
|
+
accounts: meetings.accounts,
|
|
94
|
+
pending: meetings.pending,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
/** Their recent meetings in every account of the store that saw them; none is not an error. */
|
|
101
|
+
const meetingsOf = async (store, uid, stored, since, left) => {
|
|
102
|
+
const none = { items: [], hasMore: false, accounts: [], pending: 0 };
|
|
103
|
+
if (!stored.length || left() < 2)
|
|
104
|
+
return none;
|
|
105
|
+
try {
|
|
106
|
+
const found = await personMeetingContext(store, `person:${uid}`, {
|
|
107
|
+
accountIds: stored.slice(0, 1000).map(({ id }) => id),
|
|
108
|
+
meetings: MEETINGS,
|
|
109
|
+
cues: CUES,
|
|
110
|
+
bytes: Math.min(left(), 4 * 1024 * 1024),
|
|
111
|
+
...(since === undefined ? {} : { since }),
|
|
112
|
+
});
|
|
113
|
+
const read = new Set(found.items.map((item) => parseMeetingReference(item.reference).accountId));
|
|
114
|
+
return {
|
|
115
|
+
items: found.items,
|
|
116
|
+
hasMore: found.coverage.truncatedBy !== null || found.coverage.scanLimitReached,
|
|
117
|
+
accounts: stored.filter(({ id }) => read.has(id)).map(accountName),
|
|
118
|
+
pending: found.coverage.pending,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error instanceof CliError && error.code === "not_found")
|
|
123
|
+
return none;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
};
|
package/dist/store/exports.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { OTHER_SOURCES, type OtherSource, type SourceMention } from "./sqlite/me
|
|
|
31
31
|
export { MENTION_QUERY, MENTION_STATUSES, type MentionDecision, type MentionStatus, type MentionsRebuild, type MentionsState, type StoredMention, type StoredPlace, } from "./sqlite/mentions.js";
|
|
32
32
|
export { drainNoteIndex, noteIndexState, resetNoteIndex } from "./sqlite/note-index.js";
|
|
33
33
|
export type { Link as ThingLink } from "./sqlite/notes.js";
|
|
34
|
+
export type { PersonPreparationRead, PreparationFact, PreparationNote, PreparationTask, } from "./sqlite/person-preparation.js";
|
|
34
35
|
export { savedPhoneCountries, saveStorePhoneCountries } from "./sqlite/phone-countries.js";
|
|
35
36
|
export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
|
|
36
37
|
export type { ProposedAction } from "./sqlite/proposed-actions.js";
|
package/dist/store/people.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Contact, Id, Page, PersonAlias, Provider } from "../domain/models.
|
|
|
2
2
|
import type { PeopleLookup } from "../resolve.js";
|
|
3
3
|
import type { ContactFilter } from "./sqlite/identities.js";
|
|
4
4
|
import type { StoreContext } from "./sqlite/open.js";
|
|
5
|
+
import { type PersonPreparationRead } from "./sqlite/person-preparation.js";
|
|
5
6
|
import type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
|
|
6
7
|
import type { AccountKey, IdentityRef, LinkOptions, PersonFacts, PersonRecord } from "./types.js";
|
|
7
8
|
export interface AccountPeople {
|
|
@@ -44,6 +45,11 @@ export interface PeopleStore extends AccountPeople {
|
|
|
44
45
|
/** The person a `person:<uid>` reference names. */
|
|
45
46
|
personSeenInAccounts(uid: string, accountIds: readonly number[]): Promise<boolean>;
|
|
46
47
|
personByUid(uid: string): Promise<PersonRecord | undefined>;
|
|
48
|
+
/** Accepted facts, the owner's notes and open tasks about them; tasks of an account only if `accounts` holds it. */
|
|
49
|
+
preparation(uid: string, options: {
|
|
50
|
+
accounts: readonly AccountKey[];
|
|
51
|
+
limit: number;
|
|
52
|
+
}): Promise<PersonPreparationRead>;
|
|
47
53
|
/** Records the decision in `identity_link_events`; `unlinkIdentity` undoes it. */
|
|
48
54
|
linkIdentities(person: IdentityRef, other: IdentityRef, options: LinkOptions): Promise<PersonRecord>;
|
|
49
55
|
unlinkIdentity(identity: IdentityRef, options: LinkOptions): Promise<PersonRecord>;
|
package/dist/store/people.js
CHANGED
|
@@ -2,6 +2,7 @@ import { keysOver } from "./keys.js";
|
|
|
2
2
|
import * as identities from "./sqlite/identities.js";
|
|
3
3
|
import { linkToOwner } from "./sqlite/own-identities.js";
|
|
4
4
|
import * as personLinks from "./sqlite/person-links.js";
|
|
5
|
+
import { personPreparation } from "./sqlite/person-preparation.js";
|
|
5
6
|
import { personSeenInAccounts } from "./sqlite/person-scope.js";
|
|
6
7
|
import * as privatePeople from "./sqlite/private-people.js";
|
|
7
8
|
import { write } from "./sqlite/transaction.js";
|
|
@@ -55,6 +56,7 @@ export const peopleStoreOver = (context, keyed) => {
|
|
|
55
56
|
personOf: async (identity) => personLinks.personOf(context, identity),
|
|
56
57
|
personSeenInAccounts: async (uid, accountIds) => personSeenInAccounts(context, uid, accountIds),
|
|
57
58
|
personByUid: async (uid) => personLinks.personByUid(context, uid),
|
|
59
|
+
preparation: async (uid, options) => personPreparation(context, uid, options),
|
|
58
60
|
linkIdentities: async (person, other, options) => {
|
|
59
61
|
return write(database, () => personLinks.linkIdentities(context, person, other, options));
|
|
60
62
|
},
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AccountKey } from "../types.js";
|
|
2
|
+
import type { StoreContext } from "./open.js";
|
|
3
|
+
export interface PreparationFact {
|
|
4
|
+
ref: string;
|
|
5
|
+
type: string;
|
|
6
|
+
statement: string;
|
|
7
|
+
subject: string | null;
|
|
8
|
+
project: string | null;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PreparationNote {
|
|
12
|
+
ref: string;
|
|
13
|
+
title: string | null;
|
|
14
|
+
text: string;
|
|
15
|
+
revision: number;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PreparationTask {
|
|
19
|
+
ref: string;
|
|
20
|
+
title: string;
|
|
21
|
+
role: string;
|
|
22
|
+
dueAt: string | null;
|
|
23
|
+
project: string;
|
|
24
|
+
source: string | null;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PersonPreparationRead {
|
|
28
|
+
facts: {
|
|
29
|
+
items: PreparationFact[];
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
};
|
|
32
|
+
notes: {
|
|
33
|
+
items: PreparationNote[];
|
|
34
|
+
hasMore: boolean;
|
|
35
|
+
};
|
|
36
|
+
tasks: {
|
|
37
|
+
items: PreparationTask[];
|
|
38
|
+
hasMore: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The accepted facts, the owner's notes and the open tasks about one person. A project bound to an account is that
|
|
43
|
+
* account's: its facts never come along, and its tasks only when `accounts` holds it.
|
|
44
|
+
*/
|
|
45
|
+
export declare const personPreparation: ({ database }: Pick<StoreContext, "database">, uid: string, { accounts, limit }: {
|
|
46
|
+
accounts: readonly AccountKey[];
|
|
47
|
+
limit: number;
|
|
48
|
+
}) => PersonPreparationRead;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { linkKind } from "./link-kinds.js";
|
|
3
|
+
import { taskIdOf } from "./things.js";
|
|
4
|
+
import { read } from "./transaction.js";
|
|
5
|
+
const GLOBAL_PROJECT = "SELECT id FROM main.projects WHERE account_id IS NULL AND deleted_at IS NULL";
|
|
6
|
+
const iso = (value) => (value == null ? null : new Date(Number(value)).toISOString());
|
|
7
|
+
/**
|
|
8
|
+
* The accepted facts, the owner's notes and the open tasks about one person. A project bound to an account is that
|
|
9
|
+
* account's: its facts never come along, and its tasks only when `accounts` holds it.
|
|
10
|
+
*/
|
|
11
|
+
export const personPreparation = ({ database }, uid, { accounts, limit }) => {
|
|
12
|
+
const person = Number(uid);
|
|
13
|
+
if (!/^[1-9]\d*$/.test(uid) || !Number.isSafeInteger(person) || !Number.isSafeInteger(limit) || limit < 1)
|
|
14
|
+
throw new CliError("validation_error", "person preparation takes a person id and a positive limit");
|
|
15
|
+
const page = (rows) => ({ items: rows.slice(0, limit), hasMore: rows.length > limit });
|
|
16
|
+
return read(database, () => {
|
|
17
|
+
const projects = `SELECT l.to_id FROM links l WHERE l.from_type='person' AND l.from_id=? AND l.to_type='project'
|
|
18
|
+
AND l.kind=${linkKind("member-of")} AND l.confirmed=1 AND l.to_id IN (${GLOBAL_PROJECT})`;
|
|
19
|
+
const facts = database
|
|
20
|
+
.prepare(`SELECT f.id,f.type,f.statement,f.subject_type,f.subject_id,f.project_id,f.updated_at FROM facts f
|
|
21
|
+
WHERE f.status='accepted' AND f.deleted_at IS NULL
|
|
22
|
+
AND ((f.subject_type='person' AND f.subject_id=?) OR (f.subject_type='project' AND f.subject_id IN (${projects}))
|
|
23
|
+
OR f.project_id IN (${projects}))
|
|
24
|
+
AND (f.subject_type IS NOT 'project' OR f.subject_id IN (${GLOBAL_PROJECT}))
|
|
25
|
+
AND (f.project_id IS NULL OR f.project_id IN (${GLOBAL_PROJECT}))
|
|
26
|
+
ORDER BY f.updated_at DESC, f.id DESC LIMIT ?`)
|
|
27
|
+
.all(person, person, person, limit + 1)
|
|
28
|
+
.map((row) => ({
|
|
29
|
+
ref: `fact:${row.id}`,
|
|
30
|
+
type: String(row.type),
|
|
31
|
+
statement: String(row.statement),
|
|
32
|
+
subject: row.subject_type == null ? null : `${row.subject_type}:${row.subject_id}`,
|
|
33
|
+
project: row.project_id == null ? null : `project:${row.project_id}`,
|
|
34
|
+
updatedAt: iso(row.updated_at),
|
|
35
|
+
}));
|
|
36
|
+
const notes = database
|
|
37
|
+
.prepare(`SELECT n.id,n.title,n.body,n.revision,n.updated_at FROM notes n
|
|
38
|
+
JOIN persons owner ON n.author_type='person' AND n.author_id=owner.id AND owner.owner=1
|
|
39
|
+
WHERE n.deleted_at IS NULL
|
|
40
|
+
AND ((n.notable_type='person' AND n.notable_id=?) OR EXISTS(SELECT 1 FROM links l WHERE l.from_type='note'
|
|
41
|
+
AND l.from_id=n.id AND l.to_type='person' AND l.to_id=? AND l.kind=${linkKind("about")} AND l.confirmed=1))
|
|
42
|
+
AND (n.notable_type='person' OR (n.notable_type='project' AND n.notable_id IN (${GLOBAL_PROJECT})))
|
|
43
|
+
ORDER BY n.updated_at DESC, n.id DESC LIMIT ?`)
|
|
44
|
+
.all(person, person, limit + 1)
|
|
45
|
+
.map((row) => ({
|
|
46
|
+
ref: `note:${row.id}`,
|
|
47
|
+
title: row.title == null ? null : String(row.title),
|
|
48
|
+
text: String(row.body),
|
|
49
|
+
revision: Number(row.revision),
|
|
50
|
+
updatedAt: iso(row.updated_at),
|
|
51
|
+
}));
|
|
52
|
+
const tasks = database
|
|
53
|
+
.prepare(`SELECT t.id,t.title,t.due_at,t.project_id,t.source_locator,t.created_at,group_concat(DISTINCT ta.role) AS role
|
|
54
|
+
FROM task_assignments ta JOIN tasks t ON t.id=ta.task_id JOIN main.projects p ON p.id=t.project_id
|
|
55
|
+
WHERE ta.assignee_type='person' AND ta.assignee_id=? AND t.deleted_at IS NULL AND t.status='open'
|
|
56
|
+
AND p.deleted_at IS NULL AND (p.account_id IS NULL OR p.account_id IN (SELECT a.id FROM accounts a
|
|
57
|
+
JOIN json_each(?) k ON a.provider=json_extract(k.value,'$.provider')
|
|
58
|
+
AND a.external_id=json_extract(k.value,'$.account')))
|
|
59
|
+
GROUP BY t.id ORDER BY t.due_at IS NULL, t.due_at, t.created_at, t.id LIMIT ?`)
|
|
60
|
+
.all(person, JSON.stringify(accounts.map(({ provider, account }) => ({ provider, account }))), limit + 1)
|
|
61
|
+
.map((row) => ({
|
|
62
|
+
ref: `task:${taskIdOf(database, Number(row.id))}`,
|
|
63
|
+
title: String(row.title),
|
|
64
|
+
role: String(row.role),
|
|
65
|
+
dueAt: iso(row.due_at),
|
|
66
|
+
project: `project:${row.project_id}`,
|
|
67
|
+
source: row.source_locator == null ? null : String(row.source_locator),
|
|
68
|
+
createdAt: iso(row.created_at),
|
|
69
|
+
}));
|
|
70
|
+
return { facts: page(facts), notes: page(notes), tasks: page(tasks) };
|
|
71
|
+
});
|
|
72
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.394.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -2816,6 +2816,13 @@
|
|
|
2816
2816
|
"contacts notes show": {
|
|
2817
2817
|
"in": "all"
|
|
2818
2818
|
},
|
|
2819
|
+
"contacts prepare": {
|
|
2820
|
+
"in": "all",
|
|
2821
|
+
"options": {
|
|
2822
|
+
"--limit": "all",
|
|
2823
|
+
"--since-time": "all"
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2819
2826
|
"contacts profile": {
|
|
2820
2827
|
"in": "all",
|
|
2821
2828
|
"options": {
|