@wirecat/cli-core 0.554.0 → 0.556.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 +58 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/config-command.js +64 -2
- package/dist/cli/config-issues.d.ts +2 -0
- package/dist/cli/config-issues.js +11 -3
- package/dist/cli/config-repair.d.ts +26 -0
- package/dist/cli/config-repair.js +252 -0
- package/dist/cli/messenger/catch-up-answers.d.ts +50 -1
- package/dist/cli/messenger/catch-up-answers.js +2 -1
- package/dist/cli/messenger/catch-up.d.ts +6 -2
- package/dist/cli/messenger/catch-up.js +11 -2
- package/dist/cli/messenger/conversation-answers.d.ts +3 -0
- package/dist/cli/messenger/inbox.js +1 -1
- package/dist/cli/messenger/message-answers.d.ts +3 -0
- package/dist/cli/messenger/message-writes.d.ts +6 -0
- package/dist/cli/messenger/points.d.ts +9 -4
- package/dist/cli/messenger/points.js +10 -4
- package/dist/cli/messenger/review.js +1 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/tasks-analysis-command.d.ts +8 -0
- package/dist/cli/messenger/tasks-analysis-command.js +184 -0
- package/dist/cli/messenger/tasks-command.js +11 -2
- package/dist/cli/steps.js +10 -0
- package/dist/domain/models.d.ts +4 -0
- package/dist/entities/contact-context.d.ts +6 -0
- package/dist/entities/conversation-results.d.ts +6 -0
- package/dist/entities/evidence.d.ts +24 -0
- package/dist/entities/index.d.ts +327 -5
- package/dist/entities/index.js +6 -2
- package/dist/entities/listener.d.ts +12 -0
- package/dist/entities/message-context.d.ts +9 -0
- package/dist/entities/message-write.d.ts +12 -0
- package/dist/entities/message.d.ts +6 -0
- package/dist/entities/message.js +6 -0
- package/dist/entities/store-records.d.ts +3 -0
- package/dist/entities/task-results.d.ts +72 -0
- package/dist/entities/task-results.js +40 -1
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +9 -0
- package/dist/mail/cli/index.d.ts +3 -1
- package/dist/mail/cli/index.js +7 -1
- package/dist/mail/cli/marks.d.ts +26 -0
- package/dist/mail/cli/marks.js +51 -0
- package/dist/mail/cli/review.d.ts +24 -0
- package/dist/mail/cli/review.js +274 -0
- package/dist/mail/index.d.ts +3 -0
- package/dist/mail/index.js +3 -0
- package/dist/mail/marks.d.ts +56 -0
- package/dist/mail/marks.js +178 -0
- package/dist/mail/owed.d.ts +11 -0
- package/dist/mail/owed.js +14 -0
- package/dist/mail/review.d.ts +37 -0
- package/dist/mail/review.js +118 -0
- package/dist/sends/permissions.js +1 -0
- package/dist/services/agenda-mail.d.ts +15 -0
- package/dist/services/agenda-mail.js +66 -0
- package/dist/services/agenda.d.ts +4 -0
- package/dist/services/agenda.js +17 -7
- package/dist/services/task-analysis.d.ts +58 -0
- package/dist/services/task-analysis.js +125 -0
- package/dist/store/exports.d.ts +2 -1
- package/dist/store/exports.js +1 -0
- package/dist/store/index.d.ts +1 -1
- package/dist/store/sqlite/email-activity.d.ts +93 -0
- package/dist/store/sqlite/email-activity.js +109 -0
- package/dist/store/sqlite/emails.d.ts +9 -3
- package/dist/store/sqlite/emails.js +5 -8
- package/dist/store/sqlite/task-analysis-marks.d.ts +7 -0
- package/dist/store/sqlite/task-analysis-marks.js +27 -0
- package/dist/store/sync.d.ts +5 -1
- package/dist/store/sync.js +4 -0
- package/dist/store/types.d.ts +25 -0
- package/package.json +1 -1
- package/parity.json +29 -0
- package/skills/follow-ups/SKILL.md +22 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { int, str } from "./events.js";
|
|
2
|
+
/** `tasks agenda` fields over `threadActivity`'s threads: an email thread counts as a dialog. */
|
|
3
|
+
export const MAIL_AGENDA_COLUMNS = { chat_kind: { type: "enum", sql: "'dialog'" } };
|
|
4
|
+
// The thread's own time is the Date header where sync stored one, so a thread is picked by it; the newest email is
|
|
5
|
+
// picked by when it arrived, which is what an answer follows.
|
|
6
|
+
const ACTIVITY = `
|
|
7
|
+
SELECT t.external_id AS thread, t.subject, t.last_email_at, t.emails_count,
|
|
8
|
+
e.external_id, coalesce(e.received_at, e.sent_at) AS at,
|
|
9
|
+
coalesce(e.outgoing, lower(e.from_address) = lower(a.external_id)) AS outgoing,
|
|
10
|
+
EXISTS (SELECT 1 FROM email_recipients r WHERE r.email_id = e.id AND r.role IN ('to', 'cc')
|
|
11
|
+
AND lower(r.address) = lower(a.external_id)) AS addressed,
|
|
12
|
+
EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
13
|
+
WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk,
|
|
14
|
+
coalesce(json_extract(e.headers, '$."list-id"'), json_extract(e.metadata, '$.listId')) IS NOT NULL AS list,
|
|
15
|
+
EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
16
|
+
WHERE m.email_id = e.id AND b.kind = 'envelope') AS envelope,
|
|
17
|
+
EXISTS (SELECT 1 FROM account_identities ai WHERE ai.account_id = t.account_id
|
|
18
|
+
AND ai.identity_id = e.from_identity_id AND ai.mail_demoted = 1) AS demoted
|
|
19
|
+
FROM email_threads t JOIN accounts a ON a.id = t.account_id
|
|
20
|
+
LEFT JOIN emails e ON e.id = (SELECT f.id FROM emails f WHERE f.email_thread_id = t.id AND f.deleted_at IS NULL
|
|
21
|
+
AND coalesce(f.draft, 0) = 0 ORDER BY coalesce(f.received_at, f.sent_at) DESC, f.id DESC LIMIT 1)
|
|
22
|
+
WHERE t.account_id = ? AND t.deleted_at IS NULL AND t.last_email_at > ?`;
|
|
23
|
+
export const threadActivity = (database, accountId, since, where) => database
|
|
24
|
+
.prepare(`${ACTIVITY}${where ? ` AND (${where.sql})` : ""} ORDER BY t.last_email_at DESC, t.id DESC`)
|
|
25
|
+
.all(accountId, since, ...(where?.params ?? []))
|
|
26
|
+
.map((row) => ({
|
|
27
|
+
threadId: String(row.thread),
|
|
28
|
+
subject: str(row.subject),
|
|
29
|
+
lastEmailAt: Number(row.last_email_at),
|
|
30
|
+
emails: Number(row.emails_count),
|
|
31
|
+
newest: row.external_id == null
|
|
32
|
+
? null
|
|
33
|
+
: {
|
|
34
|
+
externalId: String(row.external_id),
|
|
35
|
+
at: int(row.at) ?? Number(row.last_email_at),
|
|
36
|
+
outgoing: Boolean(row.outgoing),
|
|
37
|
+
addressed: Boolean(row.addressed),
|
|
38
|
+
bulk: Boolean(row.bulk),
|
|
39
|
+
list: Boolean(row.list),
|
|
40
|
+
envelope: Boolean(row.envelope),
|
|
41
|
+
demoted: Boolean(row.demoted),
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
/** A thread's live emails, oldest first; `window` keeps those received in (after, until], in ms. */
|
|
45
|
+
export const threadEmailIds = (database, accountId, thread, { after, until } = {}) => database
|
|
46
|
+
.prepare(`SELECT e.id FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
|
|
47
|
+
WHERE t.account_id = ? AND t.external_id = ? AND e.deleted_at IS NULL
|
|
48
|
+
AND coalesce(e.received_at, e.sent_at, 0) > coalesce(?, -1)
|
|
49
|
+
AND coalesce(e.received_at, e.sent_at, 0) <= coalesce(?, 9e15)
|
|
50
|
+
ORDER BY coalesce(e.sent_at, e.received_at), e.id`)
|
|
51
|
+
.all(accountId, thread, after ?? null, until ?? null)
|
|
52
|
+
.map(({ id }) => Number(id));
|
|
53
|
+
const MARKS = `
|
|
54
|
+
SELECT e.mail_rule_id, coalesce(json_extract(e.headers, '$."list-id"'), json_extract(e.metadata, '$.listId')) AS list_id,
|
|
55
|
+
EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
56
|
+
WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk
|
|
57
|
+
FROM emails e WHERE e.account_id = ? AND e.external_id = ? AND e.deleted_at IS NULL`;
|
|
58
|
+
export const emailMarks = (database, accountId, externalIds) => {
|
|
59
|
+
const one = database.prepare(MARKS);
|
|
60
|
+
const marks = new Map();
|
|
61
|
+
for (const id of externalIds) {
|
|
62
|
+
const row = one.get(accountId, id);
|
|
63
|
+
if (row)
|
|
64
|
+
marks.set(id, { bulk: Boolean(row.bulk), listId: str(row.list_id), ruleId: int(row.mail_rule_id) });
|
|
65
|
+
}
|
|
66
|
+
return marks;
|
|
67
|
+
};
|
|
68
|
+
const MARKED = `
|
|
69
|
+
SELECT e.id, e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.created_at,
|
|
70
|
+
e.updated_at, coalesce(e.draft, 0) AS draft, json_extract(e.metadata, '$.partial') IS NOT NULL AS partial,
|
|
71
|
+
coalesce(json_extract(e.headers, '$."list-id"'), json_extract(e.metadata, '$.listId')) IS NOT NULL
|
|
72
|
+
OR EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
73
|
+
WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk,
|
|
74
|
+
EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
|
|
75
|
+
WHERE m.email_id = e.id AND b.kind = 'envelope') AS envelope
|
|
76
|
+
FROM emails e JOIN email_threads t ON t.id = e.email_thread_id`;
|
|
77
|
+
const markedOf = (row) => ({
|
|
78
|
+
id: Number(row.id),
|
|
79
|
+
externalId: String(row.external_id),
|
|
80
|
+
thread: String(row.thread),
|
|
81
|
+
at: int(row.at),
|
|
82
|
+
storedAt: Number(row.created_at),
|
|
83
|
+
changedAt: Number(row.updated_at),
|
|
84
|
+
bulk: Boolean(row.bulk),
|
|
85
|
+
envelope: Boolean(row.envelope),
|
|
86
|
+
partial: Boolean(row.partial),
|
|
87
|
+
draft: Boolean(row.draft),
|
|
88
|
+
});
|
|
89
|
+
export const emailsAfter = (database, accountId, afterId, limit, ids = []) => {
|
|
90
|
+
const next = database
|
|
91
|
+
.prepare(`${MARKED} WHERE e.id > ? AND e.account_id = ? AND e.deleted_at IS NULL ORDER BY e.id LIMIT ?`)
|
|
92
|
+
.all(afterId, accountId, limit);
|
|
93
|
+
const one = database.prepare(`${MARKED} WHERE e.id = ? AND e.account_id = ? AND e.deleted_at IS NULL`);
|
|
94
|
+
const named = ids.flatMap((id) => {
|
|
95
|
+
const row = one.get(id, accountId);
|
|
96
|
+
return row ? [row] : [];
|
|
97
|
+
});
|
|
98
|
+
return [...named, ...next].map(markedOf);
|
|
99
|
+
};
|
|
100
|
+
export const mailActivity = (database) => ({
|
|
101
|
+
emailsAfter: async (accountId, afterId, limit, ids) => emailsAfter(database, accountId, afterId, limit, ids),
|
|
102
|
+
newestEmailId: async (accountId) => Number(database.prepare("SELECT max(id) AS id FROM emails WHERE account_id = ?").get(accountId)?.id ?? 0),
|
|
103
|
+
statesLike: async (accountId, prefix) => database
|
|
104
|
+
.prepare("SELECT key, value, updated_at FROM sync_cursors WHERE account_id = ? AND substr(key, 1, ?) = ?")
|
|
105
|
+
.all(accountId, prefix.length, prefix)
|
|
106
|
+
.map((row) => ({ key: String(row.key), value: String(row.value), at: Number(row.updated_at) })),
|
|
107
|
+
emailMarks: async (accountId, externalIds) => emailMarks(database, accountId, externalIds),
|
|
108
|
+
threadActivity: async (accountId, since, where) => threadActivity(database, accountId, since, where),
|
|
109
|
+
});
|
|
@@ -3,6 +3,7 @@ import type { Attachment, Metadata } from "../../meetings/index.js";
|
|
|
3
3
|
import type { Stemmer, Stemmers } from "../../search/stem.js";
|
|
4
4
|
import type { CacheDatabase } from "../driver.js";
|
|
5
5
|
import type { ConversationSummary, StoredHit } from "../types.js";
|
|
6
|
+
import { type MailActivityStore } from "./email-activity.js";
|
|
6
7
|
import { type EmailLocationInput, type FolderEmail, type PartialMailStore } from "./email-locations.js";
|
|
7
8
|
import { type MailRulesStore } from "./mail-rules.js";
|
|
8
9
|
import { type AttachmentInput } from "./meetings.js";
|
|
@@ -205,7 +206,7 @@ export interface SenderScore {
|
|
|
205
206
|
demoted?: boolean;
|
|
206
207
|
}
|
|
207
208
|
/** Mail in tables of its own: threads, emails, recipients and mailboxes, keyed by account and Message-ID. */
|
|
208
|
-
export interface MailStore extends PartialMailStore {
|
|
209
|
+
export interface MailStore extends PartialMailStore, MailActivityStore {
|
|
209
210
|
saveThread(input: ThreadSave): Promise<ThreadDetails>;
|
|
210
211
|
/** Several threads and sync state in one transaction: a slice of mail commits with its resume point, or not at all. */
|
|
211
212
|
saveThreads(inputs: ThreadSave[], states?: SyncStateSave[]): Promise<ThreadDetails[]>;
|
|
@@ -268,8 +269,13 @@ export interface MailStore extends PartialMailStore {
|
|
|
268
269
|
}[]>;
|
|
269
270
|
/** Live emails of one account by Message-ID, each placed in its thread; an unknown one is left out. */
|
|
270
271
|
inThreads(accountId: number, externalIds: string[]): Promise<EmailInThread[]>;
|
|
271
|
-
/**
|
|
272
|
-
|
|
272
|
+
/** Emails by store id, as search hits in the order given; one the store does not hold is left out. */
|
|
273
|
+
emailHits(ids: readonly number[]): Promise<StoredHit[]>;
|
|
274
|
+
/** A thread's live emails, oldest first, as search hits; `window` keeps those received in (after, until], in ms. */
|
|
275
|
+
threadHits(accountId: number, thread: string, window?: {
|
|
276
|
+
after?: number;
|
|
277
|
+
until?: number;
|
|
278
|
+
}): Promise<StoredHit[]>;
|
|
273
279
|
/** The emails of one account nearest in meaning to `query`, each by its best chunk, best first. */
|
|
274
280
|
nearest(model: string, query: Float32Array, options: {
|
|
275
281
|
accountId: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatLocator } from "../../domain/locator.js";
|
|
2
2
|
import { formatReference } from "../../domain/references.js";
|
|
3
3
|
import { fold, normalize } from "../normalize.js";
|
|
4
|
+
import { mailActivity, threadEmailIds } from "./email-activity.js";
|
|
4
5
|
import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
|
|
5
6
|
import { backfillSentAt, folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
|
|
6
7
|
import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
|
|
@@ -472,6 +473,7 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
|
|
|
472
473
|
}));
|
|
473
474
|
},
|
|
474
475
|
...partialMail(database),
|
|
476
|
+
...mailActivity(database),
|
|
475
477
|
async emailFacts(filter = {}) {
|
|
476
478
|
const [where, params] = emailWhere(filter);
|
|
477
479
|
return database
|
|
@@ -631,14 +633,9 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
|
|
|
631
633
|
return place ? [{ ...place, score }] : [];
|
|
632
634
|
});
|
|
633
635
|
},
|
|
634
|
-
async
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
WHERE t.account_id = ? AND t.external_id = ? AND e.deleted_at IS NULL
|
|
638
|
-
ORDER BY coalesce(e.sent_at, e.received_at), e.id`)
|
|
639
|
-
.all(accountId, threadExternalId)
|
|
640
|
-
.map(({ id }) => Number(id));
|
|
641
|
-
return emailHitsByPk(database, ids);
|
|
636
|
+
emailHits: async (ids) => emailHitsByPk(database, [...ids]),
|
|
637
|
+
async threadHits(accountId, thread, window) {
|
|
638
|
+
return emailHitsByPk(database, threadEmailIds(database, accountId, thread, window));
|
|
642
639
|
},
|
|
643
640
|
async inThreads(accountId, externalIds) {
|
|
644
641
|
const id = database.prepare("SELECT id FROM emails WHERE account_id = ? AND external_id = ? AND deleted_at IS NULL");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Id } from "../../domain/models.js";
|
|
2
|
+
import type { CacheDatabase } from "../driver.js";
|
|
3
|
+
import type { AccountKey, TaskAnalysisMark, TaskAnalysisMarkWrite } from "../types.js";
|
|
4
|
+
export declare const taskAnalysisMarks: (database: CacheDatabase, account: AccountKey) => TaskAnalysisMark[];
|
|
5
|
+
export declare const taskAnalysisMark: (database: CacheDatabase, account: AccountKey, chatId: Id) => TaskAnalysisMark | undefined;
|
|
6
|
+
/** `checked`: the whole stretch through `checkedThrough` was read just now, by this store's clock. */
|
|
7
|
+
export declare const setTaskAnalysisMark: (database: CacheDatabase, account: AccountKey, { chatId, checked, ...saved }: TaskAnalysisMarkWrite, now: number) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// In store_settings beside task_rules_until, so backup and restore carry it and it needs no migration.
|
|
2
|
+
const prefixOf = ({ provider, account }) => `task_analysis:${provider}:${account}:`;
|
|
3
|
+
const markOf = (prefix, row) => ({
|
|
4
|
+
chatId: String(row.key).slice(prefix.length),
|
|
5
|
+
...JSON.parse(String(row.value)),
|
|
6
|
+
at: new Date(Number(row.at)).toISOString(),
|
|
7
|
+
});
|
|
8
|
+
export const taskAnalysisMarks = (database, account) => {
|
|
9
|
+
const prefix = prefixOf(account);
|
|
10
|
+
return database
|
|
11
|
+
.prepare("SELECT key, value, at FROM store_settings WHERE substr(key, 1, ?) = ? ORDER BY at DESC")
|
|
12
|
+
.all(prefix.length, prefix)
|
|
13
|
+
.map((row) => markOf(prefix, row));
|
|
14
|
+
};
|
|
15
|
+
export const taskAnalysisMark = (database, account, chatId) => {
|
|
16
|
+
const prefix = prefixOf(account);
|
|
17
|
+
const row = database.prepare("SELECT key, value, at FROM store_settings WHERE key = ?").get(prefix + chatId);
|
|
18
|
+
return row ? markOf(prefix, row) : undefined;
|
|
19
|
+
};
|
|
20
|
+
/** `checked`: the whole stretch through `checkedThrough` was read just now, by this store's clock. */
|
|
21
|
+
export const setTaskAnalysisMark = (database, account, { chatId, checked, ...saved }, now) => {
|
|
22
|
+
const checkedAt = checked ? new Date(now).toISOString() : taskAnalysisMark(database, account, chatId)?.checkedAt;
|
|
23
|
+
database
|
|
24
|
+
.prepare("INSERT INTO store_settings (key, value, at) VALUES (?, ?, ?) " +
|
|
25
|
+
"ON CONFLICT (key) DO UPDATE SET value = excluded.value, at = excluded.at")
|
|
26
|
+
.run(prefixOf(account) + chatId, JSON.stringify({ ...saved, checkedAt: checkedAt ?? new Date(now).toISOString() }), now);
|
|
27
|
+
};
|
package/dist/store/sync.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Id } from "../domain/models.js";
|
|
2
2
|
import type { StoreContext } from "./sqlite/open.js";
|
|
3
|
-
import type { AccountKey, Delta, Range } from "./types.js";
|
|
3
|
+
import type { AccountKey, Delta, Range, TaskAnalysisMark, TaskAnalysisMarkWrite } from "./types.js";
|
|
4
4
|
export interface SyncStore {
|
|
5
5
|
/** What a sync remembered under `name` for this account, and when; a caller encodes a number itself. */
|
|
6
6
|
syncState(key: AccountKey, name: string): Promise<{
|
|
@@ -31,5 +31,9 @@ export interface SyncStore {
|
|
|
31
31
|
/** Where the account's last task-rule pass stopped: the newest store time it read. */
|
|
32
32
|
taskRulesStop(key: AccountKey): Promise<string | null>;
|
|
33
33
|
setTaskRulesStop(key: AccountKey, until: string): Promise<void>;
|
|
34
|
+
/** Every chat of the account read for tasks, the latest moved first. */
|
|
35
|
+
taskAnalysisMarks(key: AccountKey): Promise<TaskAnalysisMark[]>;
|
|
36
|
+
taskAnalysisMark(key: AccountKey, chatId: Id): Promise<TaskAnalysisMark | undefined>;
|
|
37
|
+
setTaskAnalysisMark(key: AccountKey, mark: TaskAnalysisMarkWrite): Promise<void>;
|
|
34
38
|
}
|
|
35
39
|
export declare const syncStoreOver: (context: StoreContext) => SyncStore;
|
package/dist/store/sync.js
CHANGED
|
@@ -2,6 +2,7 @@ import { keysOver } from "./keys.js";
|
|
|
2
2
|
import * as chatQueries from "./sqlite/chats.js";
|
|
3
3
|
import * as ranges from "./sqlite/ranges.js";
|
|
4
4
|
import * as sync from "./sqlite/sync.js";
|
|
5
|
+
import { setTaskAnalysisMark, taskAnalysisMark, taskAnalysisMarks } from "./sqlite/task-analysis-marks.js";
|
|
5
6
|
import { setTaskRulesStop, taskRulesStop } from "./sqlite/task-rule-stops.js";
|
|
6
7
|
import { write } from "./sqlite/transaction.js";
|
|
7
8
|
export const syncStoreOver = (context) => {
|
|
@@ -56,5 +57,8 @@ export const syncStoreOver = (context) => {
|
|
|
56
57
|
},
|
|
57
58
|
taskRulesStop: async (key) => taskRulesStop(database, key),
|
|
58
59
|
setTaskRulesStop: async (key, until) => setTaskRulesStop(database, key, until, context.now()),
|
|
60
|
+
taskAnalysisMarks: async (key) => taskAnalysisMarks(database, key),
|
|
61
|
+
taskAnalysisMark: async (key, chatId) => taskAnalysisMark(database, key, chatId),
|
|
62
|
+
setTaskAnalysisMark: async (key, mark) => setTaskAnalysisMark(database, key, mark, context.now()),
|
|
59
63
|
};
|
|
60
64
|
};
|
package/dist/store/types.d.ts
CHANGED
|
@@ -251,3 +251,28 @@ export interface Range {
|
|
|
251
251
|
from: number;
|
|
252
252
|
to: number;
|
|
253
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* How far a chat's messages were read for tasks: every stored message sent from `from` through `through`, the one
|
|
256
|
+
* with id `throughId` last. Its own point, so reading a chat, marking it read or another reader's `--new` never
|
|
257
|
+
* moves it.
|
|
258
|
+
*/
|
|
259
|
+
export interface TaskAnalysisMark {
|
|
260
|
+
chatId: Id;
|
|
261
|
+
from: string;
|
|
262
|
+
through: string;
|
|
263
|
+
throughId: Id;
|
|
264
|
+
/** Who moved it last: an agent's name or the owner. */
|
|
265
|
+
by: string;
|
|
266
|
+
/**
|
|
267
|
+
* At `checkedAt` every stored message sent from `from` through `checkedThrough` had been read; one saved or edited
|
|
268
|
+
* later inside that stretch (history fetched afterwards) was not.
|
|
269
|
+
*/
|
|
270
|
+
checkedAt: string;
|
|
271
|
+
checkedThrough: string;
|
|
272
|
+
/** When it last moved. */
|
|
273
|
+
at: string;
|
|
274
|
+
}
|
|
275
|
+
/** `checked` stamps `checkedAt` with the store's clock; otherwise the mark keeps its earlier one. */
|
|
276
|
+
export type TaskAnalysisMarkWrite = Omit<TaskAnalysisMark, "at" | "checkedAt"> & {
|
|
277
|
+
checked: boolean;
|
|
278
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.556.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
|
@@ -735,6 +735,9 @@
|
|
|
735
735
|
"value": "<on|off>",
|
|
736
736
|
"meaning": "only the owner may change the name and photo"
|
|
737
737
|
},
|
|
738
|
+
"--open-new": {
|
|
739
|
+
"meaning": "open a task from a message the chat's task point already passed; without it a read again only updates and closes tasks"
|
|
740
|
+
},
|
|
738
741
|
"--operation-id": {
|
|
739
742
|
"value": "<id>",
|
|
740
743
|
"meaning": "identify a send or creation attempt; message/poll retries reuse it, while an unknown topic creation must never be repeated"
|
|
@@ -2763,6 +2766,12 @@
|
|
|
2763
2766
|
"config": {
|
|
2764
2767
|
"in": "all"
|
|
2765
2768
|
},
|
|
2769
|
+
"config repair": {
|
|
2770
|
+
"in": "all",
|
|
2771
|
+
"options": {
|
|
2772
|
+
"--dry-run": "all"
|
|
2773
|
+
}
|
|
2774
|
+
},
|
|
2766
2775
|
"config set": {
|
|
2767
2776
|
"in": "all",
|
|
2768
2777
|
"options": {
|
|
@@ -4816,6 +4825,12 @@
|
|
|
4816
4825
|
"tasks": {
|
|
4817
4826
|
"in": "all"
|
|
4818
4827
|
},
|
|
4828
|
+
"tasks acknowledge": {
|
|
4829
|
+
"in": "all",
|
|
4830
|
+
"options": {
|
|
4831
|
+
"--since-time": "all"
|
|
4832
|
+
}
|
|
4833
|
+
},
|
|
4819
4834
|
"tasks add": {
|
|
4820
4835
|
"in": "all",
|
|
4821
4836
|
"options": {
|
|
@@ -4825,6 +4840,7 @@
|
|
|
4825
4840
|
"--due": "all",
|
|
4826
4841
|
"--into": "all",
|
|
4827
4842
|
"--new": "all",
|
|
4843
|
+
"--open-new": "all",
|
|
4828
4844
|
"--remind-time": "all",
|
|
4829
4845
|
"--title": "all",
|
|
4830
4846
|
"--type": {
|
|
@@ -4931,6 +4947,13 @@
|
|
|
4931
4947
|
"--holder": "all"
|
|
4932
4948
|
}
|
|
4933
4949
|
},
|
|
4950
|
+
"tasks review": {
|
|
4951
|
+
"in": "all",
|
|
4952
|
+
"options": {
|
|
4953
|
+
"--limit": "all",
|
|
4954
|
+
"--since-time": "all"
|
|
4955
|
+
}
|
|
4956
|
+
},
|
|
4934
4957
|
"tasks scan": {
|
|
4935
4958
|
"in": "all",
|
|
4936
4959
|
"options": {
|
|
@@ -4940,6 +4963,12 @@
|
|
|
4940
4963
|
"tasks show": {
|
|
4941
4964
|
"in": "all"
|
|
4942
4965
|
},
|
|
4966
|
+
"tasks status": {
|
|
4967
|
+
"in": "all",
|
|
4968
|
+
"options": {
|
|
4969
|
+
"--limit": "all"
|
|
4970
|
+
}
|
|
4971
|
+
},
|
|
4943
4972
|
"tasks unmerge": {
|
|
4944
4973
|
"in": "all"
|
|
4945
4974
|
},
|
|
@@ -38,6 +38,26 @@ no message text — read all of it:
|
|
|
38
38
|
|
|
39
39
|
Anything not on the agenda waits for its own date. Do not open it.
|
|
40
40
|
|
|
41
|
+
**Every chat keeps its own point of reading for tasks.** `review --new`, `inbox`, the owner reading on the phone
|
|
42
|
+
and other agents all move their own points, so a message they showed is not yet read for tasks. Work from the
|
|
43
|
+
task point instead:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
{{command}} tasks status --json # per chat: read for tasks through when, how many wait, how many came late
|
|
47
|
+
{{command}} tasks review "<chat>" --json # the messages after the point, oldest first, and the chat's open tasks
|
|
48
|
+
{{command}} tasks acknowledge "<chat>" <message id> # after its tasks are opened, updated or closed: the next review starts after it
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For each chat with `waiting` above 0: `tasks review`, then open new tasks, update or close the listed `open_tasks`
|
|
52
|
+
the new messages settle, then run the `next` step the answer gives (`tasks acknowledge`). Acknowledge only what you
|
|
53
|
+
finished; a page with `has_more: true` is followed by the next one. A chat with `late` above 0 got older history
|
|
54
|
+
after it was read: `tasks review "<chat>" --since-time <mark.from>` reads the stretch again (up to 1000 messages),
|
|
55
|
+
and acknowledging that read clears it. A read again only updates and closes tasks: `tasks add` refuses a message the
|
|
56
|
+
point already passed (`task.already-read`) unless it is given `--open-new`, which you pass only when the owner asked
|
|
57
|
+
for tasks from that history. When that answer has `has_more: true` it gives no acknowledge step: the stretch
|
|
58
|
+
is too long to read again in one page, so tell the owner and move on. A chat whose history is not stored yet is fetched by `messages sync` first; `tasks review` reads
|
|
59
|
+
only the store.
|
|
60
|
+
|
|
41
61
|
**History is not work.** A first sync opens no tasks from old messages (`task_first_pass_per_chat`, 0 by
|
|
42
62
|
default) and says how many it held. Do not read old chats looking for tasks. Ask the owner which chats to build,
|
|
43
63
|
then for each: `{{command}} tasks scan "<chat>" --json` lists what would open, and `--yes` opens it. Dig deeper
|
|
@@ -103,7 +123,7 @@ stop. The owner sends what they choose.
|
|
|
103
123
|
|
|
104
124
|
`{{command}} schedule agent follow-ups claude "<prompt>" --at 08:30` runs this every morning. Then you run
|
|
105
125
|
with the CLI's read-only variable set (`MAX_READ_ONLY=1` for max). The task work above still runs: `tasks scan --yes`,
|
|
106
|
-
`add`, `plan`, `link`, `merge`, `unmerge`, `duplicates reject`, `duplicates defer`, `claim`, `release` and `close`
|
|
126
|
+
`acknowledge`, `add`, `plan`, `link`, `merge`, `unmerge`, `duplicates reject`, `duplicates defer`, `claim`, `release` and `close`
|
|
107
127
|
— but every `tasks close` then waits on the owner as a suggestion, whatever your certainty, and `--as-owner` is
|
|
108
128
|
refused. Everything else that writes is refused, so do not try: every send, reaction and edit; changes to schedules,
|
|
109
129
|
settings, the recipient list or auto-replies; tags, contact notes and aliases, saved searches, `tasks reject`. What you print is the report the owner opens; its first line shows in the desktop
|
|
@@ -115,7 +135,7 @@ A recipe is a set of flags, not a new command:
|
|
|
115
135
|
|
|
116
136
|
| Recipe | Command |
|
|
117
137
|
|---|---|
|
|
118
|
-
| Morning sweep | `review --new --json`, then `tasks agenda --json` |
|
|
138
|
+
| Morning sweep | `review --new --json`, then `tasks agenda --json`, then `tasks status --json` and a `tasks review` per waiting chat |
|
|
119
139
|
| Weekly quiet-chat pass | `tasks agenda --waiting 3d --within 90d --json` |
|
|
120
140
|
| Groups too, not only people | `tasks agenda "chat_kind:(dialog OR group OR channel)" --json` |
|
|
121
141
|
| Before a call with someone | `tasks list 'chat:"<chat>" state:open' --json`, then `tasks show` each |
|