@wirecat/cli-core 0.544.0 → 0.546.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 +88 -0
- package/dist/calendar/busy.js +4 -1
- package/dist/calendar/cli/answers.d.ts +31 -0
- package/dist/calendar/cli/answers.js +8 -0
- package/dist/calendar/cli/index.d.ts +6 -0
- package/dist/calendar/cli/index.js +43 -4
- package/dist/calendar/cli/remind.js +2 -1
- package/dist/calendar/expand.d.ts +17 -0
- package/dist/calendar/expand.js +340 -0
- package/dist/calendar/feeds.d.ts +96 -0
- package/dist/calendar/feeds.js +324 -0
- package/dist/calendar/ics.d.ts +15 -4
- package/dist/calendar/ics.js +18 -7
- package/dist/calendar/index.d.ts +3 -1
- package/dist/calendar/index.js +2 -0
- package/dist/calendar/server-search.js +3 -1
- package/dist/calendar/writes.js +15 -4
- package/dist/cli/after-dashes.d.ts +16 -0
- package/dist/cli/after-dashes.js +41 -0
- package/dist/cli/bot/context.d.ts +1 -1
- package/dist/cli/bot/context.js +9 -1
- package/dist/cli/bot/messages.js +2 -0
- package/dist/cli/bot/setup.js +1 -0
- package/dist/cli/contract-templates.js +2 -2
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +3 -2
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/admin-commands.js +5 -1
- package/dist/cli/messenger/admin-contacts-command.js +5 -1
- package/dist/cli/messenger/admin-folders-command.js +5 -1
- package/dist/cli/messenger/archive-commands.js +20 -2
- package/dist/cli/messenger/backfill-command.js +13 -2
- package/dist/cli/messenger/complete-command.js +1 -0
- package/dist/cli/messenger/context.js +3 -0
- package/dist/cli/messenger/history-gaps.d.ts +12 -0
- package/dist/cli/messenger/history-gaps.js +41 -0
- package/dist/cli/messenger/messages-command.js +9 -22
- package/dist/cli/messenger/messages-edit-command.js +1 -0
- package/dist/cli/messenger/messages-press-command.js +1 -0
- package/dist/cli/messenger/messages-search-command.js +5 -1
- package/dist/cli/messenger/messages-send-command.js +1 -0
- package/dist/cli/messenger/observed.js +2 -2
- package/dist/cli/messenger/polls-command.js +1 -0
- package/dist/cli/messenger/private-people-command.js +5 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -2
- package/dist/cli/messenger/search-answers.js +12 -5
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/stored.d.ts +6 -1
- package/dist/cli/messenger/stored.js +94 -5
- package/dist/cli/messenger/topics-command.js +1 -1
- package/dist/cli/program.js +17 -6
- package/dist/cli/schedule/schedule-command.js +1 -0
- package/dist/cli/settings.d.ts +9 -0
- package/dist/cli/settings.js +20 -0
- package/dist/cli/shared-command-exports.d.ts +24 -0
- package/dist/cli/steps.js +1 -0
- package/dist/domain/message-link.d.ts +3 -0
- package/dist/domain/message-link.js +9 -14
- package/dist/domain/money-words.d.ts +11 -0
- package/dist/domain/money-words.js +38 -0
- package/dist/domain/providers.d.ts +2 -0
- package/dist/domain/providers.js +15 -3
- package/dist/domain/ref-arguments.d.ts +31 -0
- package/dist/domain/ref-arguments.js +79 -0
- package/dist/domain/references.d.ts +20 -2
- package/dist/domain/references.js +132 -47
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/message-transfer.d.ts +4 -0
- package/dist/entities/message-transfer.js +2 -0
- package/dist/errors/reasons.js +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/oauth/client-file.d.ts +14 -0
- package/dist/oauth/client-file.js +28 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/ports/messenger.d.ts +3 -2
- package/dist/resolve.d.ts +3 -1
- package/dist/resolve.js +8 -0
- package/dist/search/task-signature.d.ts +5 -1
- package/dist/search/task-signature.js +27 -9
- package/dist/services/agenda.d.ts +5 -0
- package/dist/services/agenda.js +3 -1
- package/dist/services/history-limits.d.ts +45 -0
- package/dist/services/history-limits.js +50 -0
- package/dist/services/index.d.ts +5 -1
- package/dist/services/index.js +5 -1
- package/dist/services/message-resolution.d.ts +4 -1
- package/dist/services/message-resolution.js +19 -3
- package/dist/services/messages.js +15 -13
- package/dist/services/money-words.d.ts +3 -0
- package/dist/services/money-words.js +5 -0
- package/dist/services/questions.d.ts +7 -2
- package/dist/services/questions.js +13 -4
- package/dist/services/stored-project-gathering.d.ts +9 -0
- package/dist/services/stored-project-gathering.js +9 -4
- package/dist/services/task-categories.d.ts +2 -0
- package/dist/services/task-categories.js +19 -8
- package/dist/store/chats.d.ts +10 -0
- package/dist/store/chats.js +25 -0
- package/dist/store/sqlite/chats.js +2 -1
- package/dist/store/sqlite/evidence-sources.js +2 -2
- package/dist/store/sqlite/history-skips.d.ts +24 -0
- package/dist/store/sqlite/history-skips.js +95 -0
- package/dist/store/sqlite/identities.js +1 -1
- package/dist/store/sqlite/knowledge.js +7 -2
- package/dist/store/sqlite/notes.js +4 -4
- package/dist/store/sqlite/things.d.ts +0 -1
- package/dist/store/sqlite/things.js +25 -24
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +4 -0
|
@@ -3,6 +3,7 @@ import { validateFormattedText, visibleFormattedText } from "../domain/formattin
|
|
|
3
3
|
import { formatLocator } from "../domain/locator.js";
|
|
4
4
|
import { fileLinks, messageLinkTarget, validatePermalink } from "../domain/message-link.js";
|
|
5
5
|
import { pinDurationsText } from "../domain/pin-duration.js";
|
|
6
|
+
import { ensureAccount } from "../domain/ref-arguments.js";
|
|
6
7
|
import { threadIdOf } from "../domain/thread.js";
|
|
7
8
|
import { capability } from "../ports/messenger.js";
|
|
8
9
|
import { codeOf, guardedWrite, sentIds } from "../sends/guarded.js";
|
|
@@ -31,6 +32,14 @@ const filesOf = ({ attachments }) => {
|
|
|
31
32
|
export const messagesService = (deps) => {
|
|
32
33
|
const { guard } = deps;
|
|
33
34
|
const inStore = async (read) => read(await deps.store(), await deps.account());
|
|
35
|
+
/** A message named by a reference is read only in the account the reference names; the store is not opened to say so. */
|
|
36
|
+
const ownTarget = async (target, account) => {
|
|
37
|
+
if (account.provider !== deps.messenger.provider)
|
|
38
|
+
throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
|
|
39
|
+
if (target.account === undefined)
|
|
40
|
+
return;
|
|
41
|
+
await ensureAccount({ text: target.text, account: target.account }, { account: account.account, ...(deps.profile === undefined ? {} : { profile: deps.profile }) });
|
|
42
|
+
};
|
|
34
43
|
const serverThenStore = async (store, account, query) => {
|
|
35
44
|
const server = await searchServer(deps, query);
|
|
36
45
|
const only = query.backend === "server" && server
|
|
@@ -151,11 +160,8 @@ export const messagesService = (deps) => {
|
|
|
151
160
|
},
|
|
152
161
|
around: async (reference, message, window) => {
|
|
153
162
|
const target = messageLinkTarget(reference, message, deps.messenger.provider);
|
|
154
|
-
if (target.account !== undefined)
|
|
155
|
-
|
|
156
|
-
if (account.provider !== deps.messenger.provider || target.account !== account.account)
|
|
157
|
-
throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
|
|
158
|
-
}
|
|
163
|
+
if (target.account !== undefined)
|
|
164
|
+
await ownTarget(target, await deps.account());
|
|
159
165
|
if (fromStore(deps)) {
|
|
160
166
|
return inStore(async (store, account) => store.messages.around(account, await readChatId(deps, target.chat, store, account), target.message, window));
|
|
161
167
|
}
|
|
@@ -163,9 +169,7 @@ export const messagesService = (deps) => {
|
|
|
163
169
|
},
|
|
164
170
|
thread: (reference, message, options) => inStore(async (store, account) => {
|
|
165
171
|
const target = messageLinkTarget(reference, message, deps.messenger.provider);
|
|
166
|
-
|
|
167
|
-
(target.account !== undefined && target.account !== account.account))
|
|
168
|
-
throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
|
|
172
|
+
await ownTarget(target, account);
|
|
169
173
|
return readThreadContext(store, account, await storedChatId(deps.messenger, target.chat, store, account, { includeLeft: true }), target.message, options);
|
|
170
174
|
}),
|
|
171
175
|
comments: async (chat, typedPost, window) => {
|
|
@@ -184,9 +188,7 @@ export const messagesService = (deps) => {
|
|
|
184
188
|
link: async (reference, message) => {
|
|
185
189
|
const target = messageLinkTarget(reference, message, deps.messenger.provider);
|
|
186
190
|
const account = await deps.account();
|
|
187
|
-
|
|
188
|
-
(target.account !== undefined && target.account !== account.account))
|
|
189
|
-
throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
|
|
191
|
+
await ownTarget(target, account);
|
|
190
192
|
if (fromStore(deps)) {
|
|
191
193
|
const store = await deps.store();
|
|
192
194
|
const chat = await readChatId(deps, target.chat, store, account);
|
|
@@ -227,8 +229,8 @@ export const messagesService = (deps) => {
|
|
|
227
229
|
},
|
|
228
230
|
download: async (reference, message) => {
|
|
229
231
|
const target = messageLinkTarget(reference, message, deps.messenger.provider);
|
|
230
|
-
if (target.account !== undefined
|
|
231
|
-
|
|
232
|
+
if (target.account !== undefined)
|
|
233
|
+
await ownTarget(target, await deps.account());
|
|
232
234
|
const connection = await deps.connection();
|
|
233
235
|
const download = capability(connection, "download", "download attachments");
|
|
234
236
|
return download((await connection.resolve(target.chat)).id, target.message);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { MONEY_WORDS } from "../domain/money-words.js";
|
|
2
|
+
import { listsHit } from "./task-categories.js";
|
|
3
|
+
export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, PROMISE } from "../domain/money-words.js";
|
|
4
|
+
/** The money classes the text's words belong to; none for a text with no money verb. */
|
|
5
|
+
export const moneyClassesOf = (text) => listsHit(text, MONEY_WORDS);
|
|
@@ -9,9 +9,14 @@ export declare const unanswered: (messages: Message[], { answerers, before }: {
|
|
|
9
9
|
answerers: ReadonlySet<Id>;
|
|
10
10
|
before: number;
|
|
11
11
|
}) => Message[];
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Every question from others, as `unanswered` reads them, with the message that answered it, if one did.
|
|
14
|
+
* With `asker: "owner"` it is the owner's own questions instead, and anyone but the owner answers: by replying to
|
|
15
|
+
* it, or by speaking next. `answerers` is not read then.
|
|
16
|
+
*/
|
|
17
|
+
export declare const questions: (messages: Message[], { answerers, asker }: {
|
|
14
18
|
answerers: ReadonlySet<Id>;
|
|
19
|
+
asker?: "others" | "owner";
|
|
15
20
|
}) => {
|
|
16
21
|
question: Message;
|
|
17
22
|
answer?: Message;
|
|
@@ -9,12 +9,21 @@ const LINKS = /https?:\/\/\S+/g;
|
|
|
9
9
|
export const unanswered = (messages, { answerers, before }) => questions(messages, { answerers })
|
|
10
10
|
.filter(({ question, answer }) => answer === undefined && Date.parse(question.timestamp) < before)
|
|
11
11
|
.map(({ question }) => question);
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Every question from others, as `unanswered` reads them, with the message that answered it, if one did.
|
|
14
|
+
* With `asker: "owner"` it is the owner's own questions instead, and anyone but the owner answers: by replying to
|
|
15
|
+
* it, or by speaking next. `answerers` is not read then.
|
|
16
|
+
*/
|
|
17
|
+
export const questions = (messages, { answerers, asker = "others" }) => {
|
|
18
|
+
const fromOwner = (message) => message.outgoing === true;
|
|
19
|
+
const answers = asker === "owner"
|
|
20
|
+
? (message) => !fromOwner(message)
|
|
21
|
+
: (message) => fromOwner(message) || (message.senderId !== null && answerers.has(message.senderId));
|
|
15
22
|
const byId = new Map(messages.map((message) => [message.id, message]));
|
|
16
23
|
// Telegram names only the id a reply answers; the message itself is found in the window, or not at all.
|
|
17
24
|
const repliesToAnswerer = (message) => {
|
|
25
|
+
if (asker === "owner")
|
|
26
|
+
return false;
|
|
18
27
|
const to = message.replyTo?.id ?? message.replyToId;
|
|
19
28
|
const quoted = to === undefined ? undefined : byId.get(to);
|
|
20
29
|
if (quoted)
|
|
@@ -22,7 +31,7 @@ export const questions = (messages, { answerers }) => {
|
|
|
22
31
|
return (message.replyTo !== null && (message.replyTo.outgoing === true || answerers.has(message.replyTo.senderId ?? "")));
|
|
23
32
|
};
|
|
24
33
|
return messages.flatMap((message, index) => {
|
|
25
|
-
if (answers(message))
|
|
34
|
+
if (asker === "owner" ? !fromOwner(message) : answers(message))
|
|
26
35
|
return [];
|
|
27
36
|
const transcript = "transcript" in message && typeof message.transcript === "string" ? message.transcript : "";
|
|
28
37
|
if (![message.text, transcript].join("\n").replace(LINKS, "").includes("?") && !repliesToAnswerer(message))
|
|
@@ -34,6 +34,15 @@ export interface StoredProjectInput {
|
|
|
34
34
|
}
|
|
35
35
|
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
36
36
|
export declare const canonicalProjectRef: (store: MessageStore, reference: string) => Promise<string>;
|
|
37
|
+
export interface LinkedChat {
|
|
38
|
+
provider: string;
|
|
39
|
+
account: string;
|
|
40
|
+
chat: string;
|
|
41
|
+
/** The link's free role (`source report`, `what worked`), if the owner set one. */
|
|
42
|
+
role: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** Only links the owner confirmed: a suggested one would put a whole chat into the evidence on a guess. */
|
|
45
|
+
export declare const linkedChatsOf: (store: MessageStore, project: string | null) => Promise<LinkedChat[]>;
|
|
37
46
|
export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvidence>> & {
|
|
38
47
|
projectRef: string | null;
|
|
39
48
|
unreadSources: {
|
|
@@ -118,7 +118,7 @@ const memberLanesOf = async (store, project, given, room) => {
|
|
|
118
118
|
return { lanes, left };
|
|
119
119
|
};
|
|
120
120
|
/** Only links the owner confirmed: a suggested one would put a whole chat into the evidence on a guess. */
|
|
121
|
-
const linkedChatsOf = async (store, project) => {
|
|
121
|
+
export const linkedChatsOf = async (store, project) => {
|
|
122
122
|
if (!project)
|
|
123
123
|
return [];
|
|
124
124
|
const chats = [];
|
|
@@ -129,9 +129,14 @@ const linkedChatsOf = async (store, project) => {
|
|
|
129
129
|
continue;
|
|
130
130
|
const reference = parseReference(relation.from);
|
|
131
131
|
if (reference.type === "chat")
|
|
132
|
-
chats.push(
|
|
132
|
+
chats.push({
|
|
133
|
+
provider: reference.provider,
|
|
134
|
+
account: reference.account,
|
|
135
|
+
chat: reference.chat,
|
|
136
|
+
role: relation.role,
|
|
137
|
+
});
|
|
133
138
|
}
|
|
134
|
-
return chats
|
|
139
|
+
return chats;
|
|
135
140
|
};
|
|
136
141
|
/**
|
|
137
142
|
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
@@ -158,7 +163,7 @@ export const gatherStoredProject = async (store, input) => {
|
|
|
158
163
|
const members = await memberLanesOf(store, project.ref, new Set(contacts.flatMap((one) => one.reference ?? [])), room);
|
|
159
164
|
const people = [...contacts, ...members.lanes];
|
|
160
165
|
const linked = await linkedChatsOf(store, project.ref);
|
|
161
|
-
const linkedChats = linked.slice(0, LINKED_CHATS);
|
|
166
|
+
const linkedChats = linked.slice(0, LINKED_CHATS).map(({ provider, account, chat }) => ({ provider, account, chat }));
|
|
162
167
|
const budgets = {
|
|
163
168
|
...(input.maxReads === undefined ? {} : { maxReads: input.maxReads }),
|
|
164
169
|
...(input.limit === undefined ? {} : { maxItems: input.limit }),
|
|
@@ -2,6 +2,8 @@ import { type TaskCategory } from "../domain/task-categories.js";
|
|
|
2
2
|
export { DEFAULT_TASK_CATEGORIES, FALLBACK_CATEGORY, parseTaskCategory, type TaskCategory, type UnlistedCategory, unlistedNote, withTaskCategory, } from "../domain/task-categories.js";
|
|
3
3
|
/** A category the list holds, or a refusal that names the list. */
|
|
4
4
|
export declare const taskCategoryOf: (categories?: readonly TaskCategory[], flag?: string) => (typed: string) => string;
|
|
5
|
+
/** The names of every list the text hits, so a message can be a request and a promise at once. */
|
|
6
|
+
export declare const listsHit: (text: string, categories: readonly TaskCategory[]) => string[];
|
|
5
7
|
/**
|
|
6
8
|
* The category with the most distinct trigger stems in the text; none is `other`, a tie goes to the earlier one.
|
|
7
9
|
* A word hits by its English stem first, so a cognate (`hospital`) counts once, not twice.
|
|
@@ -38,15 +38,12 @@ const triggerStems = (categories) => {
|
|
|
38
38
|
stemsByList.set(categories, built);
|
|
39
39
|
return built;
|
|
40
40
|
};
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
* A word hits by its English stem first, so a cognate (`hospital`) counts once, not twice.
|
|
44
|
-
*/
|
|
45
|
-
export const categorize = (text, categories = DEFAULT_TASK_CATEGORIES) => {
|
|
41
|
+
/** For each list the text hits, the number of distinct trigger stems it hit, in list order. */
|
|
42
|
+
const hitsByList = (text, categories) => {
|
|
46
43
|
const stem = stemmersOf();
|
|
47
44
|
const english = stem.english.stemTokens(text);
|
|
48
45
|
const spanish = readsSpanish(text) ? stem.spanish.stemTokens(text) : [];
|
|
49
|
-
|
|
46
|
+
const counted = [];
|
|
50
47
|
for (const triggers of triggerStems(categories)) {
|
|
51
48
|
const hit = new Set();
|
|
52
49
|
english.forEach((word, at) => {
|
|
@@ -56,8 +53,22 @@ export const categorize = (text, categories = DEFAULT_TASK_CATEGORIES) => {
|
|
|
56
53
|
else if (other !== undefined && triggers.spanish.has(other))
|
|
57
54
|
hit.add(other);
|
|
58
55
|
});
|
|
59
|
-
|
|
60
|
-
best = { name: triggers.name, hits: hit.size };
|
|
56
|
+
counted.push({ name: triggers.name, hits: hit.size });
|
|
61
57
|
}
|
|
58
|
+
return counted;
|
|
59
|
+
};
|
|
60
|
+
/** The names of every list the text hits, so a message can be a request and a promise at once. */
|
|
61
|
+
export const listsHit = (text, categories) => hitsByList(text, categories)
|
|
62
|
+
.filter(({ hits }) => hits > 0)
|
|
63
|
+
.map(({ name }) => name);
|
|
64
|
+
/**
|
|
65
|
+
* The category with the most distinct trigger stems in the text; none is `other`, a tie goes to the earlier one.
|
|
66
|
+
* A word hits by its English stem first, so a cognate (`hospital`) counts once, not twice.
|
|
67
|
+
*/
|
|
68
|
+
export const categorize = (text, categories = DEFAULT_TASK_CATEGORIES) => {
|
|
69
|
+
let best = { name: FALLBACK_CATEGORY, hits: 0 };
|
|
70
|
+
for (const counted of hitsByList(text, categories))
|
|
71
|
+
if (counted.hits > best.hits)
|
|
72
|
+
best = counted;
|
|
62
73
|
return best.name;
|
|
63
74
|
};
|
package/dist/store/chats.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ChannelTagMatch } from "../domain/channel-tags.js";
|
|
2
2
|
import type { Chat, ChatKind, ChatUpdate, Id, Page } from "../domain/models.js";
|
|
3
|
+
import type { HistorySkip } from "../services/history-limits.js";
|
|
3
4
|
import { type ChatRekey, type ChatRekeys } from "./sqlite/chat-rekey.js";
|
|
4
5
|
import type { ChatMetadata, LeftChat } from "./sqlite/chats.js";
|
|
5
6
|
import type { ChatCompleteness } from "./sqlite/completeness.js";
|
|
7
|
+
import type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
|
|
6
8
|
import type { StoreContext } from "./sqlite/open.js";
|
|
7
9
|
import type { Where } from "./sqlite/query-filter.js";
|
|
8
10
|
import type { AccountKey, ChatActivity, ChatStats, StoredChatFilter } from "./types.js";
|
|
@@ -64,6 +66,14 @@ export interface ChatsStore {
|
|
|
64
66
|
* `chatId` itself. Only for finding the chat: the old id and its history are never shown or searched.
|
|
65
67
|
*/
|
|
66
68
|
renamedChatId(key: AccountKey, chatId: Id): Promise<Id | undefined>;
|
|
69
|
+
/** Per stored chat: its kind, how many people, and whether the owner lifted its history limits. */
|
|
70
|
+
historyShapes(key: AccountKey, chatIds: Id[]): Promise<Map<Id, ChatHistoryShape>>;
|
|
71
|
+
/** Adds what the history limits left out of pushed history, per chat. */
|
|
72
|
+
recordHistorySkips(key: AccountKey, skips: Map<Id, HistorySkip>): Promise<void>;
|
|
73
|
+
/** Lifts the history limits for one chat, or puts them back; `false` when the store does not know the chat. */
|
|
74
|
+
setHistoryIncluded(key: AccountKey, chatId: Id, included: boolean): Promise<boolean>;
|
|
75
|
+
/** Every chat whose pushed history the limits left out, most messages first. */
|
|
76
|
+
historySkips(key: AccountKey): Promise<StoredHistorySkip[]>;
|
|
67
77
|
/** A stored chat's kind, with or without messages; `undefined` when the store does not know the chat. */
|
|
68
78
|
chatKind(key: AccountKey, chatId: Id): Promise<ChatKind | undefined>;
|
|
69
79
|
/** A stored chat's title, with or without messages; `null` when it has none or the store does not know the chat. */
|
package/dist/store/chats.js
CHANGED
|
@@ -3,6 +3,7 @@ import { keysOver } from "./keys.js";
|
|
|
3
3
|
import { rekeyChat, rekeyChats } from "./sqlite/chat-rekey.js";
|
|
4
4
|
import * as chatQueries from "./sqlite/chats.js";
|
|
5
5
|
import * as completeness from "./sqlite/completeness.js";
|
|
6
|
+
import * as history from "./sqlite/history-skips.js";
|
|
6
7
|
import * as reads from "./sqlite/reads.js";
|
|
7
8
|
import { write } from "./sqlite/transaction.js";
|
|
8
9
|
export const chatsStoreOver = (context) => {
|
|
@@ -85,6 +86,30 @@ export const chatsStoreOver = (context) => {
|
|
|
85
86
|
const accountKey = findAccountPk(key);
|
|
86
87
|
return accountKey === undefined ? undefined : chatQueries.renamedChatId(context, accountKey, chatId);
|
|
87
88
|
},
|
|
89
|
+
historyShapes: async (key, chatIds) => {
|
|
90
|
+
const accountKey = findAccountPk(key);
|
|
91
|
+
return accountKey === undefined ? new Map() : history.historyShapes(context, accountKey, chatIds);
|
|
92
|
+
},
|
|
93
|
+
recordHistorySkips: async (key, skips) => {
|
|
94
|
+
const accountKey = findAccountPk(key);
|
|
95
|
+
if (accountKey === undefined || skips.size === 0)
|
|
96
|
+
return;
|
|
97
|
+
write(database, () => history.recordHistorySkips(context, accountKey, skips));
|
|
98
|
+
},
|
|
99
|
+
setHistoryIncluded: async (key, chatId, included) => {
|
|
100
|
+
const accountKey = findAccountPk(key);
|
|
101
|
+
if (accountKey === undefined)
|
|
102
|
+
return false;
|
|
103
|
+
let done = false;
|
|
104
|
+
write(database, () => {
|
|
105
|
+
done = history.setHistoryIncluded(context, accountKey, chatId, included);
|
|
106
|
+
});
|
|
107
|
+
return done;
|
|
108
|
+
},
|
|
109
|
+
historySkips: async (key) => {
|
|
110
|
+
const accountKey = findAccountPk(key);
|
|
111
|
+
return accountKey === undefined ? [] : history.historySkips(context, accountKey);
|
|
112
|
+
},
|
|
88
113
|
chatKind: async (key, chatId) => {
|
|
89
114
|
const accountKey = findAccountPk(key);
|
|
90
115
|
if (accountKey === undefined)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { and, eq, inArray, or, sql } from "./drizzle/core.js";
|
|
3
3
|
import { folded } from "./folded.js";
|
|
4
|
+
import { withoutHistory } from "./history-skips.js";
|
|
4
5
|
import { identityPk } from "./identities.js";
|
|
5
6
|
import { bound } from "./query-filter.js";
|
|
6
7
|
import { chatEventPeople, chatEvents, chatMembers, chats, chunks, fetchLeases, identities, involvements, memberCounts, memberObservationMembers, memberObservations, memberStays, messageReactions, messageRevisions, messages, messageTranscripts, pollVotes, syncRanges, } from "./schema.js";
|
|
@@ -395,7 +396,7 @@ const toChat = (row, at, parentId) => ({
|
|
|
395
396
|
scope: row.scope,
|
|
396
397
|
parentChatId: parentId ?? null,
|
|
397
398
|
membershipState: row.membershipState,
|
|
398
|
-
providerMetadata: parsed(row.metadata),
|
|
399
|
+
providerMetadata: withoutHistory(parsed(row.metadata)),
|
|
399
400
|
}),
|
|
400
401
|
...muteOf(row, at),
|
|
401
402
|
...present({ archived: flag(row.archived), markedUnread: flag(row.markedUnread) }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { linkKind } from "./link-kinds.js";
|
|
2
|
-
import {
|
|
2
|
+
import { referenceOfThing, stateOfThing, storedThing, thingOf } from "./things.js";
|
|
3
3
|
const EXCERPT_CHARS = 280;
|
|
4
4
|
const excerptOf = (text) => {
|
|
5
5
|
if (text == null)
|
|
@@ -59,7 +59,7 @@ export const evidenceSourcesOf = (database, from) => database
|
|
|
59
59
|
.map((link) => sourceOf(database, storedThing(link.to_type, link.to_id)));
|
|
60
60
|
const thingNamed = (database, reference) => {
|
|
61
61
|
try {
|
|
62
|
-
return thingOf(database,
|
|
62
|
+
return thingOf(database, reference);
|
|
63
63
|
}
|
|
64
64
|
catch {
|
|
65
65
|
return undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Id } from "../../domain/models.js";
|
|
2
|
+
import type { ChatShape, HistorySkip } from "../../services/history-limits.js";
|
|
3
|
+
import type { StoreContext } from "./open.js";
|
|
4
|
+
/**
|
|
5
|
+
* The history limits' record of a chat, kept in the chat row's `metadata` under this key so no store version is
|
|
6
|
+
* needed; `Chat.providerMetadata` leaves it out.
|
|
7
|
+
*/
|
|
8
|
+
export declare const HISTORY_KEY = "$history";
|
|
9
|
+
export interface ChatHistoryShape extends ChatShape {
|
|
10
|
+
included: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface StoredHistorySkip extends HistorySkip {
|
|
13
|
+
chatId: Id;
|
|
14
|
+
title: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare const historyShapes: (context: StoreContext, accountKey: number, chatIds: Id[]) => Map<Id, ChatHistoryShape>;
|
|
17
|
+
/** Adds what one push left out to what the chat's record already holds. A chat the store lacks is left as it is. */
|
|
18
|
+
export declare const recordHistorySkips: (context: StoreContext, accountKey: number, skips: Map<Id, HistorySkip>) => void;
|
|
19
|
+
/** Lifts the limits for one chat from now on, or puts them back. Answers `false` for a chat the store lacks. */
|
|
20
|
+
export declare const setHistoryIncluded: (context: StoreContext, accountKey: number, chatId: Id, included: boolean) => boolean;
|
|
21
|
+
/** Every chat of the account whose pushed history the limits left out, most messages first. */
|
|
22
|
+
export declare const historySkips: ({ database }: StoreContext, accountKey: number) => StoredHistorySkip[];
|
|
23
|
+
/** `metadata` as the messenger gave it: the history record is the store's own. */
|
|
24
|
+
export declare const withoutHistory: <T extends Record<string, unknown> | undefined>(metadata: T) => T;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The history limits' record of a chat, kept in the chat row's `metadata` under this key so no store version is
|
|
3
|
+
* needed; `Chat.providerMetadata` leaves it out.
|
|
4
|
+
*/
|
|
5
|
+
export const HISTORY_KEY = "$history";
|
|
6
|
+
const objectOf = (text) => {
|
|
7
|
+
if (typeof text !== "string")
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(text);
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const recordOf = (metadata) => {
|
|
20
|
+
const value = objectOf(metadata)[HISTORY_KEY];
|
|
21
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
22
|
+
};
|
|
23
|
+
const rowOf = ({ database }, accountKey, chatId) => database
|
|
24
|
+
.prepare("SELECT id, kind, participants_count, metadata FROM chats WHERE account_id = ? AND external_id = ?")
|
|
25
|
+
.get(accountKey, chatId);
|
|
26
|
+
const update = (context, id, metadata, change) => {
|
|
27
|
+
const all = objectOf(metadata);
|
|
28
|
+
const next = change(recordOf(metadata));
|
|
29
|
+
if (next.skipped === undefined && !next.included)
|
|
30
|
+
delete all[HISTORY_KEY];
|
|
31
|
+
else
|
|
32
|
+
all[HISTORY_KEY] = next;
|
|
33
|
+
const text = Object.keys(all).length === 0 ? null : JSON.stringify(all);
|
|
34
|
+
context.database.prepare("UPDATE chats SET metadata = ?, updated_at = ? WHERE id = ?").run(text, context.now(), id);
|
|
35
|
+
};
|
|
36
|
+
export const historyShapes = (context, accountKey, chatIds) => {
|
|
37
|
+
const shapes = new Map();
|
|
38
|
+
for (const chatId of new Set(chatIds)) {
|
|
39
|
+
const row = rowOf(context, accountKey, chatId);
|
|
40
|
+
if (!row)
|
|
41
|
+
continue;
|
|
42
|
+
shapes.set(chatId, {
|
|
43
|
+
kind: String(row.kind),
|
|
44
|
+
participantsCount: row.participants_count == null ? null : Number(row.participants_count),
|
|
45
|
+
included: recordOf(row.metadata).included === true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return shapes;
|
|
49
|
+
};
|
|
50
|
+
/** Adds what one push left out to what the chat's record already holds. A chat the store lacks is left as it is. */
|
|
51
|
+
export const recordHistorySkips = (context, accountKey, skips) => {
|
|
52
|
+
for (const [chatId, skip] of skips) {
|
|
53
|
+
const row = rowOf(context, accountKey, chatId);
|
|
54
|
+
if (!row)
|
|
55
|
+
continue;
|
|
56
|
+
update(context, Number(row.id), row.metadata, (held) => {
|
|
57
|
+
const before = held.skipped;
|
|
58
|
+
const skipped = before
|
|
59
|
+
? {
|
|
60
|
+
...skip,
|
|
61
|
+
messages: before.messages + skip.messages,
|
|
62
|
+
oldestAt: before.oldestAt < skip.oldestAt ? before.oldestAt : skip.oldestAt,
|
|
63
|
+
newestAt: before.newestAt > skip.newestAt ? before.newestAt : skip.newestAt,
|
|
64
|
+
}
|
|
65
|
+
: skip;
|
|
66
|
+
return { ...held, skipped };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** Lifts the limits for one chat from now on, or puts them back. Answers `false` for a chat the store lacks. */
|
|
71
|
+
export const setHistoryIncluded = (context, accountKey, chatId, included) => {
|
|
72
|
+
const row = rowOf(context, accountKey, chatId);
|
|
73
|
+
if (!row)
|
|
74
|
+
return false;
|
|
75
|
+
update(context, Number(row.id), row.metadata, (held) => ({ ...held, included }));
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
/** Every chat of the account whose pushed history the limits left out, most messages first. */
|
|
79
|
+
export const historySkips = ({ database }, accountKey) => database
|
|
80
|
+
.prepare(`SELECT external_id, title, metadata FROM chats WHERE account_id = ? AND metadata LIKE '%"${HISTORY_KEY}"%'`)
|
|
81
|
+
.all(accountKey)
|
|
82
|
+
.flatMap((row) => {
|
|
83
|
+
const { skipped } = recordOf(row.metadata);
|
|
84
|
+
return skipped
|
|
85
|
+
? [{ chatId: String(row.external_id), title: row.title == null ? null : String(row.title), ...skipped }]
|
|
86
|
+
: [];
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => b.messages - a.messages);
|
|
89
|
+
/** `metadata` as the messenger gave it: the history record is the store's own. */
|
|
90
|
+
export const withoutHistory = (metadata) => {
|
|
91
|
+
if (metadata === undefined || !(HISTORY_KEY in metadata))
|
|
92
|
+
return metadata;
|
|
93
|
+
const { [HISTORY_KEY]: _, ...rest } = metadata;
|
|
94
|
+
return rest;
|
|
95
|
+
};
|
|
@@ -205,7 +205,7 @@ export const people = ({ orm, database }, provider, { account, accounts: native
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
const byId = new Map(everyone.map((person) => [person.id, person]));
|
|
208
|
-
return { get: (id) => byId.get(id), all: () => everyone };
|
|
208
|
+
return { provider, get: (id) => byId.get(id), all: () => everyone };
|
|
209
209
|
};
|
|
210
210
|
const contactsWhere = (accountKey, key, { query, notes }) => {
|
|
211
211
|
if (query !== undefined && query.trim().length < 3) {
|
|
@@ -127,7 +127,7 @@ export const knowledgeStoreOver = (context) => {
|
|
|
127
127
|
if (thing.type === "meeting")
|
|
128
128
|
return { type: "meeting", id: String(thing.id) };
|
|
129
129
|
const reference = referenceOfThing(database, thing);
|
|
130
|
-
if (reference === undefined
|
|
130
|
+
if (reference === undefined)
|
|
131
131
|
return undefined;
|
|
132
132
|
const parsed = parseReference(reference);
|
|
133
133
|
if (parsed.type === "message")
|
|
@@ -136,7 +136,12 @@ export const knowledgeStoreOver = (context) => {
|
|
|
136
136
|
return { type: "chat", id: parsed.chat };
|
|
137
137
|
if (parsed.type === "folder")
|
|
138
138
|
return { type: "folder", id: parsed.id, path: parsed.path };
|
|
139
|
-
if (parsed.type === "entity" ||
|
|
139
|
+
if (parsed.type === "entity" ||
|
|
140
|
+
parsed.type === "memory" ||
|
|
141
|
+
parsed.type === "fact" ||
|
|
142
|
+
parsed.type === "bot" ||
|
|
143
|
+
parsed.type === "event" ||
|
|
144
|
+
parsed.type === "meeting")
|
|
140
145
|
return undefined;
|
|
141
146
|
return { type: parsed.type, id: parsed.id };
|
|
142
147
|
};
|
|
@@ -14,7 +14,7 @@ import { LINK_KINDS, LINK_ORIGINS, linkKind } from "./link-kinds.js";
|
|
|
14
14
|
import { reviewLink } from "./link-reviews.js";
|
|
15
15
|
import { holdingText } from "./note-index.js";
|
|
16
16
|
import { addTags, ensureTag, labelsOf } from "./tags.js";
|
|
17
|
-
import {
|
|
17
|
+
import { referenceOfThing, storedThing, thingOf } from "./things.js";
|
|
18
18
|
import { write } from "./transaction.js";
|
|
19
19
|
export const NOTE_FORMATS = ["obsidian", "markdown"];
|
|
20
20
|
export { LINK_ORIGINS } from "./link-kinds.js";
|
|
@@ -94,7 +94,7 @@ export const linkOf = (database, row) => {
|
|
|
94
94
|
};
|
|
95
95
|
/** The row a reference names, refused when the store holds none. */
|
|
96
96
|
export const requiredThing = (database, reference, what = "the target") => {
|
|
97
|
-
const thing = thingOf(database,
|
|
97
|
+
const thing = thingOf(database, reference);
|
|
98
98
|
if (!thing)
|
|
99
99
|
throw new CliError("not_found", `${what} ${reference} is not in the local store`);
|
|
100
100
|
return thing;
|
|
@@ -522,7 +522,7 @@ export const notesStoreOver = (context) => {
|
|
|
522
522
|
const { limit, offset } = bounded(options.limit, options.offset);
|
|
523
523
|
if (options.source !== undefined)
|
|
524
524
|
oneOf(options.source, ["file", "internal"], "a note's source");
|
|
525
|
-
const about = options.about === undefined ? undefined : thingOf(database,
|
|
525
|
+
const about = options.about === undefined ? undefined : thingOf(database, options.about);
|
|
526
526
|
if (options.about !== undefined && !about)
|
|
527
527
|
return { items: [], hasMore: false };
|
|
528
528
|
const folder = options.folderId === undefined ? null : Number(folderRow(options.folderId).id);
|
|
@@ -597,7 +597,7 @@ export const notesStoreOver = (context) => {
|
|
|
597
597
|
const pair = (reference) => {
|
|
598
598
|
if (reference === undefined)
|
|
599
599
|
return { known: true, type: null, id: null };
|
|
600
|
-
const thing = thingOf(database,
|
|
600
|
+
const thing = thingOf(database, reference);
|
|
601
601
|
return thing ? { known: true, type: thing.type, id: thing.id } : { known: false, type: null, id: null };
|
|
602
602
|
};
|
|
603
603
|
const from = pair(options.from);
|
|
@@ -24,4 +24,3 @@ export declare const stateOfThing: (database: CacheDatabase, thing: Thing | unde
|
|
|
24
24
|
export declare const taskRowOf: (database: CacheDatabase, id: string) => number | undefined;
|
|
25
25
|
/** The package's id for a task row: the reverse of `taskRowOf`. */
|
|
26
26
|
export declare const taskIdOf: (database: CacheDatabase, rowId: number) => string | undefined;
|
|
27
|
-
export declare const canonicalThingReference: (reference: string) => string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatLocator } from "../../domain/locator.js";
|
|
2
2
|
import { filesAccountSql } from "../../domain/providers.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { formatReference, parseReference } from "../../domain/references.js";
|
|
4
|
+
import { formatMeetingReference } from "../../meetings/index.js";
|
|
5
5
|
import { ENTITY_TABLES, isEntityType } from "./entity-types.js";
|
|
6
6
|
/** A pointer as a row holds it. An unknown type is not refused on read: `store check` reports it. */
|
|
7
7
|
export const storedThing = (type, id) => ({ type: String(type), id: Number(id) });
|
|
@@ -16,6 +16,7 @@ const REFERABLE = {
|
|
|
16
16
|
memory: { tombstone: true },
|
|
17
17
|
fact: { tombstone: true },
|
|
18
18
|
bot: { tombstone: false },
|
|
19
|
+
event: { tombstone: true },
|
|
19
20
|
message: { tombstone: true },
|
|
20
21
|
email: { tombstone: true },
|
|
21
22
|
email_thread: { tombstone: true },
|
|
@@ -30,30 +31,11 @@ const storeId = (text) => (/^[1-9]\d{0,15}$/.test(text) ? Number(text) : undefin
|
|
|
30
31
|
* `messages.db` (a ULID, an `entity:`) is simply not found: it is never guessed at.
|
|
31
32
|
*/
|
|
32
33
|
export const thingOf = (database, reference) => {
|
|
33
|
-
if (typeof reference === "string" && reference.trim().startsWith("meeting:")) {
|
|
34
|
-
const parsed = parseMeetingReference(reference);
|
|
35
|
-
const meeting = database
|
|
36
|
-
.prepare("SELECT id FROM meetings WHERE id=? AND account_id=? AND deleted_at IS NULL")
|
|
37
|
-
.get(parsed.meetingId, parsed.accountId);
|
|
38
|
-
if (!meeting)
|
|
39
|
-
return undefined;
|
|
40
|
-
if (parsed.transcriptId === undefined)
|
|
41
|
-
return { type: "meeting", id: parsed.meetingId };
|
|
42
|
-
const transcript = database
|
|
43
|
-
.prepare("SELECT id FROM meeting_transcripts WHERE id=? AND meeting_id=? AND deleted_at IS NULL")
|
|
44
|
-
.get(parsed.transcriptId, parsed.meetingId);
|
|
45
|
-
if (!transcript)
|
|
46
|
-
return undefined;
|
|
47
|
-
if (parsed.cuePosition === undefined)
|
|
48
|
-
return { type: "meeting_transcript", id: parsed.transcriptId };
|
|
49
|
-
const row = database
|
|
50
|
-
.prepare("SELECT id FROM meeting_transcript_rows WHERE meeting_transcript_id=? AND position=?")
|
|
51
|
-
.get(parsed.transcriptId, parsed.cuePosition);
|
|
52
|
-
return row ? { type: "meeting_transcript_row", id: Number(row.id) } : undefined;
|
|
53
|
-
}
|
|
54
34
|
const parsed = typeof reference === "string" ? parseReference(reference) : reference;
|
|
55
35
|
const found = (type, row) => row ? { type, id: Number(row.id) } : undefined;
|
|
56
36
|
switch (parsed.type) {
|
|
37
|
+
case "meeting":
|
|
38
|
+
return meetingThingOf(database, parsed);
|
|
57
39
|
case "message":
|
|
58
40
|
return ((parsed.provider === "email" ? emailOf(database, parsed.account, parsed.chat, parsed.message) : undefined) ??
|
|
59
41
|
found("message", database
|
|
@@ -90,6 +72,26 @@ export const thingOf = (database, reference) => {
|
|
|
90
72
|
}
|
|
91
73
|
}
|
|
92
74
|
};
|
|
75
|
+
const meetingThingOf = (database, parsed) => {
|
|
76
|
+
const meeting = database
|
|
77
|
+
.prepare("SELECT id FROM meetings WHERE id=? AND account_id=? AND deleted_at IS NULL")
|
|
78
|
+
.get(parsed.meetingId, parsed.accountId);
|
|
79
|
+
if (!meeting)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (parsed.transcriptId === undefined)
|
|
82
|
+
return { type: "meeting", id: parsed.meetingId };
|
|
83
|
+
const transcript = database
|
|
84
|
+
.prepare("SELECT id FROM meeting_transcripts WHERE id=? AND meeting_id=? AND deleted_at IS NULL")
|
|
85
|
+
.get(parsed.transcriptId, parsed.meetingId);
|
|
86
|
+
if (!transcript)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (parsed.cuePosition === undefined)
|
|
89
|
+
return { type: "meeting_transcript", id: parsed.transcriptId };
|
|
90
|
+
const row = database
|
|
91
|
+
.prepare("SELECT id FROM meeting_transcript_rows WHERE meeting_transcript_id=? AND position=?")
|
|
92
|
+
.get(parsed.transcriptId, parsed.cuePosition);
|
|
93
|
+
return row ? { type: "meeting_transcript_row", id: Number(row.id) } : undefined;
|
|
94
|
+
};
|
|
93
95
|
/** Mail that memo saved before it wrote the mail tables is still messages, so the caller falls back to them. */
|
|
94
96
|
export const emailOf = (database, account, thread, email) => {
|
|
95
97
|
const row = database
|
|
@@ -237,4 +239,3 @@ export const taskIdOf = (database, rowId) => {
|
|
|
237
239
|
const row = database.prepare("SELECT key, package_id AS id FROM tasks WHERE id = ?").get(rowId);
|
|
238
240
|
return row ? String(row.id ?? row.key) : undefined;
|
|
239
241
|
};
|
|
240
|
-
export const canonicalThingReference = (reference) => reference.trim().startsWith("meeting:") ? canonicalMeetingReference(reference) : canonicalReference(reference);
|
package/dist/store/store.d.ts
CHANGED
|
@@ -106,6 +106,7 @@ export interface MessageStore {
|
|
|
106
106
|
forAccount(key: AccountKey): AccountStore;
|
|
107
107
|
close(): Promise<void>;
|
|
108
108
|
}
|
|
109
|
+
export type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
|
|
109
110
|
export { BACKFILL_ON_OPEN, type StoreOptions } from "./sqlite/open-store.js";
|
|
110
111
|
export { fileFacts, readStoreTables, type TableKind } from "./sqlite/store-tables.js";
|
|
111
112
|
export declare const openStore: (options?: StoreOptions) => Promise<MessageStore>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.546.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",
|