@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/config-command.js +64 -2
  4. package/dist/cli/config-issues.d.ts +2 -0
  5. package/dist/cli/config-issues.js +11 -3
  6. package/dist/cli/config-repair.d.ts +26 -0
  7. package/dist/cli/config-repair.js +252 -0
  8. package/dist/cli/messenger/catch-up-answers.d.ts +50 -1
  9. package/dist/cli/messenger/catch-up-answers.js +2 -1
  10. package/dist/cli/messenger/catch-up.d.ts +6 -2
  11. package/dist/cli/messenger/catch-up.js +11 -2
  12. package/dist/cli/messenger/conversation-answers.d.ts +3 -0
  13. package/dist/cli/messenger/inbox.js +1 -1
  14. package/dist/cli/messenger/message-answers.d.ts +3 -0
  15. package/dist/cli/messenger/message-writes.d.ts +6 -0
  16. package/dist/cli/messenger/points.d.ts +9 -4
  17. package/dist/cli/messenger/points.js +10 -4
  18. package/dist/cli/messenger/review.js +1 -1
  19. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  20. package/dist/cli/messenger/tasks-analysis-command.d.ts +8 -0
  21. package/dist/cli/messenger/tasks-analysis-command.js +184 -0
  22. package/dist/cli/messenger/tasks-command.js +11 -2
  23. package/dist/cli/steps.js +10 -0
  24. package/dist/domain/models.d.ts +4 -0
  25. package/dist/entities/contact-context.d.ts +6 -0
  26. package/dist/entities/conversation-results.d.ts +6 -0
  27. package/dist/entities/evidence.d.ts +24 -0
  28. package/dist/entities/index.d.ts +327 -5
  29. package/dist/entities/index.js +6 -2
  30. package/dist/entities/listener.d.ts +12 -0
  31. package/dist/entities/message-context.d.ts +9 -0
  32. package/dist/entities/message-write.d.ts +12 -0
  33. package/dist/entities/message.d.ts +6 -0
  34. package/dist/entities/message.js +6 -0
  35. package/dist/entities/store-records.d.ts +3 -0
  36. package/dist/entities/task-results.d.ts +72 -0
  37. package/dist/entities/task-results.js +40 -1
  38. package/dist/errors/reasons.d.ts +1 -1
  39. package/dist/errors/reasons.js +9 -0
  40. package/dist/mail/cli/index.d.ts +3 -1
  41. package/dist/mail/cli/index.js +7 -1
  42. package/dist/mail/cli/marks.d.ts +26 -0
  43. package/dist/mail/cli/marks.js +51 -0
  44. package/dist/mail/cli/review.d.ts +24 -0
  45. package/dist/mail/cli/review.js +274 -0
  46. package/dist/mail/index.d.ts +3 -0
  47. package/dist/mail/index.js +3 -0
  48. package/dist/mail/marks.d.ts +56 -0
  49. package/dist/mail/marks.js +178 -0
  50. package/dist/mail/owed.d.ts +11 -0
  51. package/dist/mail/owed.js +14 -0
  52. package/dist/mail/review.d.ts +37 -0
  53. package/dist/mail/review.js +118 -0
  54. package/dist/sends/permissions.js +1 -0
  55. package/dist/services/agenda-mail.d.ts +15 -0
  56. package/dist/services/agenda-mail.js +66 -0
  57. package/dist/services/agenda.d.ts +4 -0
  58. package/dist/services/agenda.js +17 -7
  59. package/dist/services/task-analysis.d.ts +58 -0
  60. package/dist/services/task-analysis.js +125 -0
  61. package/dist/store/exports.d.ts +2 -1
  62. package/dist/store/exports.js +1 -0
  63. package/dist/store/index.d.ts +1 -1
  64. package/dist/store/sqlite/email-activity.d.ts +93 -0
  65. package/dist/store/sqlite/email-activity.js +109 -0
  66. package/dist/store/sqlite/emails.d.ts +9 -3
  67. package/dist/store/sqlite/emails.js +5 -8
  68. package/dist/store/sqlite/task-analysis-marks.d.ts +7 -0
  69. package/dist/store/sqlite/task-analysis-marks.js +27 -0
  70. package/dist/store/sync.d.ts +5 -1
  71. package/dist/store/sync.js +4 -0
  72. package/dist/store/types.d.ts +25 -0
  73. package/package.json +1 -1
  74. package/parity.json +29 -0
  75. package/skills/follow-ups/SKILL.md +22 -2
@@ -1,5 +1,5 @@
1
1
  import type { Meta } from "@wirecat/cli-base/standards";
2
- import type { ChatKind, Inbox, Review } from "../../domain/models.js";
2
+ import type { ChatKind, Inbox, Message, Review } from "../../domain/models.js";
3
3
  import { type Hearing } from "../../speech/hearing.js";
4
4
  import type { AccountKey } from "../../store/store.js";
5
5
  import { type QueryWords } from "../list-query.js";
@@ -14,6 +14,54 @@ export declare const catchUpQuery: (words: QueryWords, registry: typeof INBOX_QU
14
14
  kinds?: ChatKind[];
15
15
  meta?: Partial<Meta>;
16
16
  };
17
+ type Read = Message & {
18
+ chatTitle?: string | null;
19
+ };
20
+ /** One message of a catch-up answer: the fields `inbox` and `review` show, for every messenger and for mail. */
21
+ export declare const summaryOf: (message: Read, reader: AccountKey) => {
22
+ ref: string;
23
+ chat_id: string;
24
+ chat_title: string | null | undefined;
25
+ timestamp: string;
26
+ sender_name: string | null;
27
+ sender_is_me: boolean;
28
+ outgoing: boolean | null;
29
+ text: string;
30
+ transcript?: string | undefined;
31
+ attachments: {
32
+ message?: string | undefined;
33
+ kind: string;
34
+ position?: number | undefined;
35
+ url?: string | undefined;
36
+ name?: string | undefined;
37
+ title?: string | undefined;
38
+ mime?: string | undefined;
39
+ size?: number | undefined;
40
+ width?: number | undefined;
41
+ height?: number | undefined;
42
+ duration?: number | undefined;
43
+ text?: string | undefined;
44
+ extraction?: {
45
+ origin: string;
46
+ extractor: string;
47
+ chars: number;
48
+ error?: string | undefined;
49
+ } | undefined;
50
+ local_path?: string | undefined;
51
+ contact?: {
52
+ name: string;
53
+ first_name?: string | undefined;
54
+ last_name?: string | undefined;
55
+ phone?: string | undefined;
56
+ user_id?: string | undefined;
57
+ } | undefined;
58
+ buttons?: {
59
+ kind: "app" | "callback" | "chat" | "clipboard" | "contact" | "link" | "location" | "message" | "other";
60
+ text: string;
61
+ url?: string | undefined;
62
+ }[][] | undefined;
63
+ }[];
64
+ };
17
65
  export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean, meta?: Partial<Meta>) => import("../envelope.js").ListBody<{
18
66
  ref: string;
19
67
  chat_id: string;
@@ -113,3 +161,4 @@ export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined,
113
161
  }[][] | undefined;
114
162
  }[];
115
163
  }>;
164
+ export {};
@@ -33,7 +33,8 @@ export const catchUpQuery = (words, registry, command) => {
33
33
  ...(meta === undefined ? {} : { meta }),
34
34
  };
35
35
  };
36
- const summaryOf = (message, reader) => {
36
+ /** One message of a catch-up answer: the fields `inbox` and `review` show, for every messenger and for mail. */
37
+ export const summaryOf = (message, reader) => {
37
38
  const { ref, chat_id, chat_title, timestamp, sender_name, sender_is_me, outgoing, text, transcript, attachments } = messageItem(message, reader);
38
39
  return {
39
40
  ref,
@@ -1,3 +1,7 @@
1
1
  import type { Command } from "commander";
2
- /** Whose `--new` points a run moves: an agent's are its own, so the owner's `inbox --new` still shows everything. */
3
- export declare const pointsOf: <N extends "inbox" | "review">(command: Command, name: N) => N | `mcp-${N}`;
2
+ import type { AppIdentity } from "../../ports/app.js";
3
+ /**
4
+ * Whose `--new` points a run moves. A scheduled run (`<PREFIX>_SCHEDULED`) keeps one per schedule, an agent over MCP
5
+ * one of its own, and the owner the plain one: no reader moves another's point past what it never showed.
6
+ */
7
+ export declare const pointsOf: (command: Command, name: string, app: Pick<AppIdentity, "envPrefix">) => string;
@@ -1,3 +1,12 @@
1
1
  import { environmentOf } from "../context.js";
2
- /** Whose `--new` points a run moves: an agent's are its own, so the owner's `inbox --new` still shows everything. */
3
- export const pointsOf = (command, name) => environmentOf(command).agent ? `mcp-${name}` : name;
2
+ /**
3
+ * Whose `--new` points a run moves. A scheduled run (`<PREFIX>_SCHEDULED`) keeps one per schedule, an agent over MCP
4
+ * one of its own, and the owner the plain one: no reader moves another's point past what it never showed.
5
+ */
6
+ export const pointsOf = (command, name, app) => {
7
+ const { agent, env } = environmentOf(command);
8
+ const scheduled = (env ?? process.env)[`${app.envPrefix}_SCHEDULED`]?.trim();
9
+ if (scheduled)
10
+ return `schedule-${scheduled.replace(/[^a-z0-9-]/gi, "_")}-${name}`;
11
+ return agent ? `mcp-${name}` : name;
12
+ };
@@ -248,6 +248,9 @@ export declare const conversationShown: (account: AccountKey, { summary, message
248
248
  signals: string[];
249
249
  demoted?: true | undefined;
250
250
  } | undefined;
251
+ bulk?: boolean | undefined;
252
+ list_id?: string | null | undefined;
253
+ rule_id?: number | null | undefined;
251
254
  } | undefined;
252
255
  provider_metadata?: {
253
256
  [x: string]: unknown;
@@ -53,7 +53,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
53
53
  throw new CliError("validation_error", "--new keeps its own point — not with a date in the query");
54
54
  const saved = fresh && since === undefined
55
55
  ? checkPoints(app, {
56
- command: pointsOf(this, "inbox"),
56
+ command: pointsOf(this, "inbox", app),
57
57
  profile: settings.profile,
58
58
  env,
59
59
  firstLookMs: FIRST_LOOK_MS,
@@ -225,6 +225,9 @@ export declare const threadAnswer: ({ items, ...thread }: ThreadContext, reader:
225
225
  signals: string[];
226
226
  demoted?: true | undefined;
227
227
  } | undefined;
228
+ bulk?: boolean | undefined;
229
+ list_id?: string | null | undefined;
230
+ rule_id?: number | null | undefined;
228
231
  } | undefined;
229
232
  provider_metadata?: {
230
233
  [x: string]: unknown;
@@ -229,6 +229,9 @@ export declare const writtenAnswer: ({ operationId, sendId, message, messages, s
229
229
  signals: string[];
230
230
  demoted?: true | undefined;
231
231
  } | undefined;
232
+ bulk?: boolean | undefined;
233
+ list_id?: string | null | undefined;
234
+ rule_id?: number | null | undefined;
232
235
  } | undefined;
233
236
  provider_metadata?: {
234
237
  [x: string]: unknown;
@@ -448,6 +451,9 @@ export declare const writtenAnswer: ({ operationId, sendId, message, messages, s
448
451
  signals: string[];
449
452
  demoted?: true | undefined;
450
453
  } | undefined;
454
+ bulk?: boolean | undefined;
455
+ list_id?: string | null | undefined;
456
+ rule_id?: number | null | undefined;
451
457
  } | undefined;
452
458
  provider_metadata?: {
453
459
  [x: string]: unknown;
@@ -5,16 +5,21 @@ export interface CheckPoints {
5
5
  first: number;
6
6
  /** ms, per chat: where its next check starts. */
7
7
  chats: ReadonlyMap<Id, number>;
8
- /** Moves the chats checked; the rest keep their point. */
9
- save(checked: Record<Id, string>): void;
8
+ /**
9
+ * Moves the chats checked; the rest keep their point. With `through`, everything up to it was read: the start for
10
+ * a chat never checked moves there, and the points it passes are dropped, so the file stays small.
11
+ */
12
+ save(checked: Record<Id, string>, options?: {
13
+ through?: string;
14
+ }): void;
10
15
  }
11
16
  /**
12
17
  * Where `<command> --new` stopped, per chat and profile, beside the remembered account. `inbox` and
13
18
  * `review` keep a file each: sharing one, an `inbox` run would move `review` past what it never showed.
14
- * The MCP tools keep their own too, so an agent's run never moves the owner's `--new`.
19
+ * Each reader keeps its own too (`pointsOf`), so an agent's or a schedule's run never moves the owner's `--new`.
15
20
  */
16
21
  export declare const checkPoints: (app: AppIdentity, { command, profile, env, firstLookMs, }: {
17
- command: "inbox" | "review" | "mcp-inbox" | "mcp-review";
22
+ command: string;
18
23
  profile: string;
19
24
  env: NodeJS.ProcessEnv;
20
25
  firstLookMs: number;
@@ -4,7 +4,7 @@ import { resolvePaths, writeSecurely } from "@wirecat/cli-base";
4
4
  /**
5
5
  * Where `<command> --new` stopped, per chat and profile, beside the remembered account. `inbox` and
6
6
  * `review` keep a file each: sharing one, an `inbox` run would move `review` past what it never showed.
7
- * The MCP tools keep their own too, so an agent's run never moves the owner's `--new`.
7
+ * Each reader keeps its own too (`pointsOf`), so an agent's or a schedule's run never moves the owner's `--new`.
8
8
  */
9
9
  export const checkPoints = (app, { command, profile, env, firstLookMs, }) => {
10
10
  const file = join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, command, `${profile}.json`);
@@ -13,10 +13,16 @@ export const checkPoints = (app, { command, profile, env, firstLookMs, }) => {
13
13
  return {
14
14
  first: Date.parse(first),
15
15
  chats: new Map(Object.entries(saved.chats ?? {}).map(([chat, at]) => [chat, Date.parse(at)])),
16
- save: (checked) => {
17
- if (Object.keys(checked).length === 0)
16
+ save: (checked, { through } = {}) => {
17
+ if (Object.keys(checked).length === 0 && through === undefined)
18
18
  return;
19
- const kept = { lastCheckAt: first, chats: { ...saved.chats, ...checked } };
19
+ const chats = { ...saved.chats, ...checked };
20
+ const kept = through === undefined
21
+ ? { lastCheckAt: first, chats }
22
+ : {
23
+ lastCheckAt: through,
24
+ chats: Object.fromEntries(Object.entries(chats).filter(([, at]) => Date.parse(at) > Date.parse(through))),
25
+ };
20
26
  writeSecurely(file, `${JSON.stringify(kept)}\n`, 0o600);
21
27
  },
22
28
  };
@@ -52,7 +52,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
52
52
  }
53
53
  const saved = options.new
54
54
  ? checkPoints(messenger.app, {
55
- command: pointsOf(this, "review"),
55
+ command: pointsOf(this, "review", messenger.app),
56
56
  profile: settings.profile,
57
57
  env,
58
58
  firstLookMs: REVIEW_DAYS * 86_400_000,
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
3
3
  import type { Messenger } from "./context.js";
4
4
  /** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
5
5
  export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
6
+ error?: undefined;
6
7
  path: string;
7
8
  exists: boolean;
8
- error?: undefined;
9
9
  } | {
10
10
  path: string;
11
11
  exists: boolean;
@@ -0,0 +1,8 @@
1
+ import type { Command } from "commander";
2
+ import { type Messenger } from "./context.js";
3
+ /**
4
+ * Each chat's own point of reading for tasks (`../../services/task-analysis.ts`): how far it was read, what waits, and
5
+ * the step that moves it. Apart from `inbox --new`, `review --new` and the messenger's read state, which other
6
+ * readers move.
7
+ */
8
+ export declare const analysisCommands: (tasks: Command, messenger: Messenger) => void;
@@ -0,0 +1,184 @@
1
+ import { annotate } from "@wirecat/cli-base/commands";
2
+ import { formatLocator } from "../../domain/locator.js";
3
+ import { storedChatId } from "../../services/message-resolution.js";
4
+ import { momentOf } from "../../services/moment.js";
5
+ import { ANALYSIS_PAGE, ANALYSIS_PAGE_MAX, acknowledgeTaskAnalysis, taskAnalysisList, unanalysedMessages, } from "../../services/task-analysis.js";
6
+ import { TASK_QUERY } from "../../services/tasks.js";
7
+ import { actorOf } from "../author.js";
8
+ import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
9
+ import { itemOf, renderAnswer } from "../envelope.js";
10
+ import { listQuery, pagedList, pageOf, typedQuery } from "../list-query.js";
11
+ import { positiveCount } from "../paging.js";
12
+ import { nextStep } from "../steps.js";
13
+ import { messengerContext, refuseLocalWrite } from "./context.js";
14
+ const when = (at) => (at === null ? "—" : at.slice(0, 16).replace("T", " "));
15
+ const sinceOf = (typed) => typed === undefined ? undefined : new Date(momentOf(typed, "--since-time")).toISOString();
16
+ const markItem = (mark) => ({
17
+ chat: mark.chatId,
18
+ from: mark.from,
19
+ through: mark.through,
20
+ through_id: mark.throughId,
21
+ by: mark.by,
22
+ checked_at: mark.checkedAt,
23
+ checked_through: mark.checkedThrough,
24
+ at: mark.at,
25
+ });
26
+ const analysisItem = (chat) => ({
27
+ chat: chat.chatId,
28
+ title: chat.title,
29
+ kind: chat.kind,
30
+ newest_at: chat.newestAt,
31
+ mark: chat.mark === null ? null : markItem(chat.mark),
32
+ waiting: chat.waiting,
33
+ late: chat.late,
34
+ untrusted: ["title"],
35
+ });
36
+ /**
37
+ * Each chat's own point of reading for tasks (`../../services/task-analysis.ts`): how far it was read, what waits, and
38
+ * the step that moves it. Apart from `inbox --new`, `review --new` and the messenger's read state, which other
39
+ * readers move.
40
+ */
41
+ export const analysisCommands = (tasks, messenger) => {
42
+ const tool = messenger.app.command;
43
+ annotate(tasks.command("status"), {
44
+ output: { entity: "task_analysis" },
45
+ effects: LOCAL_READ,
46
+ examples: [`${tool} personal tasks status`],
47
+ })
48
+ .summary("how far each chat was read for tasks, and how much waits")
49
+ .description("every chat that can hold your tasks — not a channel or Saved Messages — with the point its messages were read " +
50
+ "for tasks through, who read them, how many stored messages wait after it, and how many arrived late inside " +
51
+ "the stretch already read (history fetched afterwards). Its own point: reading a chat, marking it read, " +
52
+ "inbox --new and review --new never move it; tasks acknowledge does. Reads only the local store")
53
+ .option("--limit <n>", "how many chats", positiveCount("--limit"))
54
+ .action(async function () {
55
+ const context = messengerContext(this, messenger);
56
+ const { limit, offset } = pageOf(context.settings);
57
+ const chats = await context.withStore((store, account) => taskAnalysisList(store, account), {
58
+ name: "tasks status",
59
+ });
60
+ const shown = chats.slice(offset, offset + limit);
61
+ renderAnswer(this, context, pagedList("tasks status", context.settings, {
62
+ items: shown.map(analysisItem),
63
+ has_more: chats.length > offset + limit,
64
+ }), () => shown.length === 0
65
+ ? ""
66
+ : `${shown
67
+ .map((one) => `${(one.title ?? one.chatId).slice(0, 30).padEnd(30)} read through ${when(one.mark?.through ?? null)}` +
68
+ ` ${one.waiting} waiting${one.late > 0 ? `, ${one.late} late` : ""}`)
69
+ .join("\n")}\n`);
70
+ });
71
+ annotate(tasks.command("review"), {
72
+ output: { entity: "task_review" },
73
+ effects: LOCAL_READ,
74
+ examples: [`${tool} personal tasks review <chat>`, `${tool} personal tasks review <chat> --since-time 30d`],
75
+ })
76
+ .summary("a chat's stored messages not yet read for tasks, with its open tasks")
77
+ .description("the stored messages after the chat's task point, oldest first, and the chat's open tasks, so one read can open " +
78
+ "new tasks and update or close the old ones. A chat never read starts 14 days back. Moves nothing: when done, " +
79
+ "tasks acknowledge <chat> <through> records it. Reads only the local store; messages sync or backfill fetch " +
80
+ "what it lacks")
81
+ .argument("<chat>", messenger.chatArgument)
82
+ .option("--limit <n>", `how many messages (default ${ANALYSIS_PAGE}, ${ANALYSIS_PAGE_MAX} with --since-time; at most ${ANALYSIS_PAGE_MAX})`, positiveCount("--limit"))
83
+ .option("--since-time <time>", "read again from this ISO 8601 time, or 30d ago, whatever the point says; what the point already passed only " +
84
+ "updates and closes tasks, unless tasks add is given --open-new")
85
+ .action(async function (chat) {
86
+ const context = messengerContext(this, messenger);
87
+ const { sinceTime, ...options } = this.opts();
88
+ const since = sinceOf(sinceTime);
89
+ // A read again clears late messages only when one acknowledge covers the whole stretch, so it takes one page.
90
+ const limit = options.limit ?? (since === undefined ? ANALYSIS_PAGE : ANALYSIS_PAGE_MAX);
91
+ const { id, page, sourceOf } = await context.withStore(async (store, account) => {
92
+ const id = await storedChatId(messenger, chat, store, account, { includeLeft: true });
93
+ return {
94
+ id,
95
+ page: await unanalysedMessages(store, account, id, {
96
+ limit: Math.min(limit, ANALYSIS_PAGE_MAX),
97
+ ...(since === undefined ? {} : { since }),
98
+ }),
99
+ sourceOf: (message) => formatLocator({ ...account, chat: id, message }),
100
+ };
101
+ }, { name: "tasks review" });
102
+ const open = await context.withServices((services) => services.tasks.list({
103
+ query: listQuery(typedQuery([`chat:${JSON.stringify(id)}`, "state:open"]), TASK_QUERY, "tasks review"),
104
+ limit: 100,
105
+ offset: 0,
106
+ }));
107
+ const last = page.messages.at(-1);
108
+ renderAnswer(this, context, itemOf({
109
+ chat: id,
110
+ from: page.from,
111
+ mark: page.mark === null ? null : markItem(page.mark),
112
+ late: page.late,
113
+ has_more: page.more,
114
+ through: last ? sourceOf(last.id) : null,
115
+ messages: page.messages.map((one) => ({
116
+ ref: sourceOf(one.id),
117
+ at: one.timestamp,
118
+ sender: one.senderName,
119
+ outgoing: one.outgoing === true,
120
+ reply_to: one.replyTo?.id ?? one.replyToId ?? null,
121
+ text: one.text,
122
+ })),
123
+ open_tasks: open.items.map(({ key = null, kind, title, source_message }) => ({
124
+ key,
125
+ kind,
126
+ title,
127
+ source: source_message,
128
+ })),
129
+ untrusted: ["messages.text", "messages.sender"],
130
+ }, {
131
+ next: last && !(since !== undefined && page.more)
132
+ ? [
133
+ nextStep("tasks.acknowledge", {
134
+ args: { chat: id, message: last.id, since_time: since },
135
+ values: { shown: page.messages.length },
136
+ }),
137
+ ]
138
+ : [],
139
+ }), () => [
140
+ `from ${when(page.from)}: ${page.messages.length} message(s)${page.more ? ", more after them" : ""}` +
141
+ (page.late > 0
142
+ ? `; ${page.late} arrived late inside the stretch read: --since-time ${page.mark?.from} reads it again`
143
+ : ""),
144
+ ...page.messages.map((one) => ` ${when(one.timestamp)} ${one.outgoing ? "you" : (one.senderName ?? "?")}: ${one.text.replace(/\s+/g, " ").slice(0, 100)}`),
145
+ `open tasks (${open.items.length})`,
146
+ ...open.items.map((task) => ` ${task.key} ${task.kind} ${task.title ?? ""}`),
147
+ ...(since !== undefined && page.more
148
+ ? [
149
+ `this read again does not reach the point in one page: acknowledging it moves nothing and late ` +
150
+ "messages stay until a later version pages a read again (tell the owner)",
151
+ ]
152
+ : last
153
+ ? [`done reading: ${tool} tasks acknowledge ${id} ${last.id}`]
154
+ : []),
155
+ "",
156
+ ].join("\n"));
157
+ });
158
+ destroysNothing(annotate(tasks.command("acknowledge"), {
159
+ output: { entity: "task_analysis_mark" },
160
+ effects: LOCAL_WRITE,
161
+ examples: [`${tool} personal tasks acknowledge <chat> <message>`],
162
+ }))
163
+ .summary("record that a chat was read for tasks through a message")
164
+ .description("moves the chat's task point to this message once its tasks are opened, updated or closed; it never moves " +
165
+ "back. With --since-time, the read started there: a read again from the start of the stretch clears what " +
166
+ "arrived late inside it")
167
+ .argument("<chat>", messenger.chatArgument)
168
+ .argument("<message>", "the id of the last message read, as tasks review shows it")
169
+ .option("--since-time <time>", "where the read started, when tasks review was given --since-time")
170
+ .action(async function (chat, message) {
171
+ const context = messengerContext(this, messenger);
172
+ refuseLocalWrite(context, messenger.app.command, "tasks.acknowledge");
173
+ const { sinceTime } = this.opts();
174
+ const since = sinceOf(sinceTime);
175
+ const by = actorOf(this);
176
+ const { mark, moved } = await context.withStore(async (store, account) => {
177
+ const id = await storedChatId(messenger, chat, store, account, { includeLeft: true });
178
+ return acknowledgeTaskAnalysis(store, account, id, message, { by, ...(since === undefined ? {} : { since }) });
179
+ }, { name: "tasks acknowledge" });
180
+ renderAnswer(this, context, itemOf({ ...markItem(mark), moved }), () => moved
181
+ ? `read for tasks from ${when(mark.from)} through ${when(mark.through)}\n`
182
+ : `already read through ${when(mark.through)}; the point stays\n`);
183
+ });
184
+ };
@@ -5,6 +5,7 @@ import { readOnlyLockOf, readOnlyRefusal } from "../../ports/app.js";
5
5
  import { AGENDA_QUERY, agendaItem } from "../../services/agenda.js";
6
6
  import { storedChatId } from "../../services/message-resolution.js";
7
7
  import { aheadOf, momentOf } from "../../services/moment.js";
8
+ import { refuseAlreadyRead } from "../../services/task-analysis.js";
8
9
  import { unlistedNote } from "../../services/task-categories.js";
9
10
  import { ARRIVAL_WINDOW, applyTaskRulesToChats } from "../../services/task-rules.js";
10
11
  import { CLAIM_LEASE_MS, closedStateOf, TASK_PAIR_QUERY, TASK_QUERY, taskLinkKindOf, taskTypeOf, } from "../../services/tasks.js";
@@ -18,6 +19,7 @@ import { positiveCount } from "../paging.js";
18
19
  import { messengerContext, refuseLocalWrite } from "./context.js";
19
20
  import { coverageGaps } from "./search-answers.js";
20
21
  import { refuseJsonl, taskStatsAnswer } from "./stats-answers.js";
22
+ import { analysisCommands } from "./tasks-analysis-command.js";
21
23
  const line = ({ id, key, state, kind, group, createdAt, message, note }) => [
22
24
  key ?? id.slice(0, 8),
23
25
  state.padEnd(9),
@@ -175,11 +177,17 @@ export const tasksCommand = (messenger) => {
175
177
  .requiredOption("--type <type>", "the task's type: question, request, mention or promise", taskTypeOf)
176
178
  .requiredOption("--category <name>", categoryHelp)
177
179
  .option("--into <id>", "when tasks add listed tasks that may be the same job: link the message to this one instead of opening a task")
178
- .option("--new", "when tasks add listed tasks that may be the same job: open the task anyway"), "send Alex the API token").action(async function (message) {
180
+ .option("--new", "when tasks add listed tasks that may be the same job: open the task anyway")
181
+ .option("--open-new", "open a task from a message the chat's task point already passed (a read again with tasks review " +
182
+ "--since-time); without it such a read only updates and closes tasks"), "send Alex the API token").action(async function (message) {
179
183
  const context = messengerContext(this, messenger);
180
184
  refuseLocalWrite(context, messenger.app.command, "tasks.add");
181
185
  const options = this.opts();
182
- const { into, new: anyway, ...rest } = options;
186
+ const { into, new: anyway, openNew, ...rest } = options;
187
+ if (!openNew)
188
+ await context.withStore((store, account) => refuseAlreadyRead(store, account, message), {
189
+ name: "tasks add",
190
+ });
183
191
  const plan = planFrom(rest);
184
192
  const { task, created, attached, review, followUpOf, card: planned, } = await context.withServices((services) => services.tasks.add(message, options.type, writerOf(this), plan, {
185
193
  ...(into === undefined ? {} : { attach: into }),
@@ -440,6 +448,7 @@ export const tasksCommand = (messenger) => {
440
448
  }, { name: "tasks scan" });
441
449
  renderAnswer(this, context, listOf("tasks scan", { items: passes.map(snakeKeys), limit: passes.length, has_more: false }), () => `${passes.map((pass) => applyText(pass, yes, tool)).join("\n")}\n`);
442
450
  });
451
+ analysisCommands(tasks, messenger);
443
452
  destroysNothing(annotate(tasks.command("claim"), {
444
453
  output: { entity: "task_card" },
445
454
  effects: LOCAL_WRITE,
package/dist/cli/steps.js CHANGED
@@ -209,6 +209,16 @@ export const STEPS = defineSteps([
209
209
  args: [],
210
210
  why: "serve still runs an older version; a restart runs the one installed now",
211
211
  },
212
+ {
213
+ id: "tasks.acknowledge",
214
+ command: "tasks acknowledge",
215
+ args: [
216
+ { name: "chat", required: true },
217
+ { name: "message", required: true },
218
+ { name: "since_time", required: false },
219
+ ],
220
+ why: "once the tasks these {shown} messages hold are opened, updated or closed, so the next review starts after them",
221
+ },
212
222
  {
213
223
  id: "search.meetings",
214
224
  command: "*",
@@ -519,6 +519,10 @@ export interface Review {
519
519
  unanswered?: {
520
520
  olderThanHours: number;
521
521
  };
522
+ /** `--waiting` (mail): only threads where the owner wrote last and nobody answered for this many hours. */
523
+ waiting?: {
524
+ olderThanHours: number;
525
+ };
522
526
  /** With saved points: each chat read whole, and the time its next check starts from. */
523
527
  checked?: Record<Id, string>;
524
528
  /** Tasks the review opened and closed in the store, by the rules; absent when the store was not open. */
@@ -346,6 +346,9 @@ export declare const contactMessages: v.StrictObjectSchema<{
346
346
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
347
347
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
348
348
  }, undefined>, undefined>;
349
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
350
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
351
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
349
352
  readonly reply_to: v.ArraySchema<v.StrictObjectSchema<{
350
353
  readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
351
354
  readonly address: v.StringSchema<undefined>;
@@ -1118,6 +1121,9 @@ export declare const contactContext: v.UnionSchema<[v.StrictObjectSchema<{
1118
1121
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
1119
1122
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1120
1123
  }, undefined>, undefined>;
1124
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1125
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
1126
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
1121
1127
  readonly reply_to: v.ArraySchema<v.StrictObjectSchema<{
1122
1128
  readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
1123
1129
  readonly address: v.StringSchema<undefined>;
@@ -224,6 +224,9 @@ export declare const conversationDetails: v.UnionSchema<[v.StrictObjectSchema<{
224
224
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
225
225
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
226
226
  }, undefined>, undefined>;
227
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
228
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
229
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
227
230
  }, undefined>, undefined>;
228
231
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
229
232
  }, undefined>, undefined>;
@@ -441,6 +444,9 @@ export declare const conversationDetails: v.UnionSchema<[v.StrictObjectSchema<{
441
444
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
442
445
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
443
446
  }, undefined>, undefined>;
447
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
448
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
449
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
444
450
  }, undefined>, undefined>;
445
451
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
446
452
  }, undefined>], undefined>;
@@ -215,6 +215,9 @@ export declare const contribution: v.StrictObjectSchema<{
215
215
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
216
216
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
217
217
  }, undefined>, undefined>;
218
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
219
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
220
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
218
221
  }, undefined>, undefined>;
219
222
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
220
223
  }, undefined>, "entries" | "~run" | "~standard" | "~types"> & {
@@ -432,6 +435,9 @@ export declare const contribution: v.StrictObjectSchema<{
432
435
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
433
436
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
434
437
  }, undefined>, undefined>;
438
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
439
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
440
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
435
441
  }, undefined>, undefined>;
436
442
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
437
443
  }, "chat_title" | "ref" | "sender_name" | "text" | "timestamp">;
@@ -687,6 +693,9 @@ export declare const contribution: v.StrictObjectSchema<{
687
693
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
688
694
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
689
695
  }, undefined>, undefined>;
696
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
697
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
698
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
690
699
  }, undefined>, undefined>;
691
700
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
692
701
  }, undefined>, "entries" | "~run" | "~standard" | "~types"> & {
@@ -904,6 +913,9 @@ export declare const contribution: v.StrictObjectSchema<{
904
913
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
905
914
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
906
915
  }, undefined>, undefined>;
916
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
917
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
918
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
907
919
  }, undefined>, undefined>;
908
920
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
909
921
  }, "chat_title" | "ref" | "sender_name" | "text" | "timestamp">;
@@ -1195,6 +1207,9 @@ export declare const evidence: v.UnionSchema<[v.StrictObjectSchema<{
1195
1207
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
1196
1208
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1197
1209
  }, undefined>, undefined>;
1210
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1211
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
1212
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
1198
1213
  }, undefined>, undefined>;
1199
1214
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
1200
1215
  }, undefined>, "entries" | "~run" | "~standard" | "~types"> & {
@@ -1412,6 +1427,9 @@ export declare const evidence: v.UnionSchema<[v.StrictObjectSchema<{
1412
1427
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
1413
1428
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1414
1429
  }, undefined>, undefined>;
1430
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1431
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
1432
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
1415
1433
  }, undefined>, undefined>;
1416
1434
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
1417
1435
  }, "chat_title" | "ref" | "sender_name" | "text" | "timestamp">;
@@ -1667,6 +1685,9 @@ export declare const evidence: v.UnionSchema<[v.StrictObjectSchema<{
1667
1685
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
1668
1686
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1669
1687
  }, undefined>, undefined>;
1688
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1689
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
1690
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
1670
1691
  }, undefined>, undefined>;
1671
1692
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
1672
1693
  }, undefined>, "entries" | "~run" | "~standard" | "~types"> & {
@@ -1884,6 +1905,9 @@ export declare const evidence: v.UnionSchema<[v.StrictObjectSchema<{
1884
1905
  readonly signals: v.ArraySchema<v.StringSchema<undefined>, undefined>;
1885
1906
  readonly demoted: v.OptionalSchema<v.LiteralSchema<true, undefined>, undefined>;
1886
1907
  }, undefined>, undefined>;
1908
+ readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
1909
+ readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
1910
+ readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
1887
1911
  }, undefined>, undefined>;
1888
1912
  readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
1889
1913
  }, "chat_title" | "ref" | "sender_name" | "text" | "timestamp">;