@wirecat/cli-core 0.530.0 → 0.532.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 (67) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli/index.d.ts +2 -2
  3. package/dist/cli/index.js +1 -1
  4. package/dist/cli/list-query.d.ts +0 -4
  5. package/dist/cli/list-query.js +19 -1
  6. package/dist/cli/messenger/attachments-command.d.ts +1 -0
  7. package/dist/cli/messenger/attachments-command.js +1 -1
  8. package/dist/cli/messenger/download-command.js +37 -10
  9. package/dist/cli/messenger/schedule-command.d.ts +2 -19
  10. package/dist/cli/messenger/schedule-command.js +2 -231
  11. package/dist/cli/messenger/search-answers.js +7 -2
  12. package/dist/cli/messenger/topics-command.js +22 -5
  13. package/dist/cli/schedule/schedule-command.d.ts +19 -0
  14. package/dist/cli/schedule/schedule-command.js +261 -0
  15. package/dist/cli/shared-command-exports.d.ts +1 -1
  16. package/dist/cli/shared-command-exports.js +2 -1
  17. package/dist/domain/attachments.d.ts +9 -1
  18. package/dist/domain/attachments.js +12 -0
  19. package/dist/domain/message-link.d.ts +14 -0
  20. package/dist/domain/message-link.js +8 -0
  21. package/dist/domain/models.d.ts +2 -0
  22. package/dist/entities/attachment.d.ts +7 -0
  23. package/dist/entities/attachment.js +2 -0
  24. package/dist/entities/hit.d.ts +7 -0
  25. package/dist/entities/hit.js +3 -0
  26. package/dist/entities/index.d.ts +61 -2
  27. package/dist/entities/index.js +6 -3
  28. package/dist/entities/list-results.d.ts +3 -0
  29. package/dist/entities/list-results.js +1 -0
  30. package/dist/entities/message-link.d.ts +8 -0
  31. package/dist/entities/message-link.js +9 -1
  32. package/dist/entities/task-results.d.ts +30 -0
  33. package/dist/entities/task-results.js +1 -0
  34. package/dist/entities/task.d.ts +7 -0
  35. package/dist/entities/task.js +2 -0
  36. package/dist/errors/reasons.js +1 -0
  37. package/dist/mail/account-key.d.ts +2 -0
  38. package/dist/mail/account-key.js +1 -0
  39. package/dist/mail/cli/index.js +10 -10
  40. package/dist/mail/index.d.ts +1 -0
  41. package/dist/mail/index.js +1 -0
  42. package/dist/mail/retry.js +2 -1
  43. package/dist/mail/rule-stored.js +2 -1
  44. package/dist/mail/sender-scores.js +2 -1
  45. package/dist/mail/sync.js +3 -2
  46. package/dist/ports/messenger.d.ts +3 -0
  47. package/dist/services/attachments.d.ts +11 -0
  48. package/dist/services/attachments.js +45 -2
  49. package/dist/services/deps.d.ts +1 -1
  50. package/dist/services/deps.js +1 -1
  51. package/dist/services/messages.d.ts +2 -3
  52. package/dist/services/messages.js +27 -8
  53. package/dist/services/search-all.d.ts +3 -0
  54. package/dist/services/search-all.js +2 -0
  55. package/dist/services/tasks.d.ts +4 -2
  56. package/dist/services/tasks.js +2 -2
  57. package/dist/services/topics.d.ts +3 -0
  58. package/dist/services/topics.js +20 -6
  59. package/dist/store/index.d.ts +1 -0
  60. package/dist/store/sqlite/task-board.js +3 -1
  61. package/dist/store/sqlite/task-comments.d.ts +23 -0
  62. package/dist/store/sqlite/task-comments.js +41 -0
  63. package/dist/store/sqlite/things.js +8 -1
  64. package/dist/store/sqlite/work-tasks.d.ts +13 -2
  65. package/dist/store/sqlite/work-tasks.js +52 -1
  66. package/package.json +1 -1
  67. package/parity.json +3 -0
@@ -8,7 +8,7 @@ import { accountSession } from "./account-session.js";
8
8
  import { adminReport } from "./admin-report.js";
9
9
  import { agenda } from "./agenda.js";
10
10
  import { analysisConsent, analysisConsentRevocation } from "./analysis-consent.js";
11
- import { attachment } from "./attachment.js";
11
+ import { attachment, hitFile } from "./attachment.js";
12
12
  import { attachmentBytes, attachmentExtraction, attachmentTextChange } from "./attachment-results.js";
13
13
  import { chart } from "./chart.js";
14
14
  import { chat } from "./chat.js";
@@ -42,7 +42,7 @@ import { inviteLink } from "./invite-link.js";
42
42
  import { job } from "./job.js";
43
43
  import { joinRequest } from "./join-request.js";
44
44
  import { organization, project, reminder } from "./knowledge-lists.js";
45
- import { botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, callRecord, scheduledJob, stickerInventory, } from "./list-results.js";
45
+ import { botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, callRecord, scheduledJob, stickerInventory, topicIcon, } from "./list-results.js";
46
46
  import { messageEvent, serveSummary, watchedMessage, watchItem } from "./listener.js";
47
47
  import { mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer } from "./media-archive.js";
48
48
  import { event, meeting, meetingDetails, meetingHit, meetingLink, meetingParticipants, meetingPassage, meetingProjectEvidence, meetingReview, meetingSpeaker, meetingStats, meetingSummary, meetingTranscript, } from "./meeting.js";
@@ -94,7 +94,7 @@ import { topic } from "./topic.js";
94
94
  * subset of its fields, never a renamed one. Commands move onto these one by one; until then, a schema
95
95
  * is the target, not a description of today's output.
96
96
  */
97
- export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reaction, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, storeTable, tagChange, tagging, task, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
97
+ export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reaction, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, storeTable, tagChange, tagging, task, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, topicIcon, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
98
98
  export const ENTITIES = {
99
99
  sync_progress: syncProgress,
100
100
  ...MEETING_RESULT_ENTITIES,
@@ -120,6 +120,8 @@ export const ENTITIES = {
120
120
  "follow_ups",
121
121
  "email",
122
122
  ]).entries,
123
+ /** The files the message carries; absent when it has none. */
124
+ attachments: v.optional(v.array(hitFile)),
123
125
  untrusted,
124
126
  }),
125
127
  "message.summary": v.pick(message, [
@@ -183,6 +185,7 @@ export const ENTITIES = {
183
185
  bot_seen_chat: botSeenChat,
184
186
  bot_admin: botAdmin,
185
187
  bot_menu_entry: botMenuEntry,
188
+ topic_icon: topicIcon,
186
189
  bot_webhook: botWebhook,
187
190
  scheduled_job: scheduledJob,
188
191
  call_record: callRecord,
@@ -37,6 +37,9 @@ export declare const botMenuEntry: v.StrictObjectSchema<{
37
37
  readonly name: v.StringSchema<undefined>;
38
38
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
39
39
  }, undefined>;
40
+ export declare const topicIcon: v.StrictObjectSchema<{
41
+ readonly emoji: v.StringSchema<undefined>;
42
+ }, undefined>;
40
43
  export declare const botWebhook: v.StrictObjectSchema<{
41
44
  readonly url: v.StringSchema<undefined>;
42
45
  readonly types: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
@@ -26,6 +26,7 @@ export const botAdmin = v.strictObject({
26
26
  title: v.nullable(text),
27
27
  });
28
28
  export const botMenuEntry = v.strictObject({ name: text, description: v.nullable(text) });
29
+ export const topicIcon = v.strictObject({ emoji: text });
29
30
  export const botWebhook = v.strictObject({ url: text, types: v.optional(v.array(text)) });
30
31
  export const scheduledJob = v.strictObject({
31
32
  name: text,
@@ -6,4 +6,12 @@ export declare const messageLink: v.StrictObjectSchema<{
6
6
  readonly access: v.PicklistSchema<["public", "restricted", "unknown", "unavailable"], undefined>;
7
7
  /** Why there is no link. */
8
8
  readonly reason: v.NullableSchema<v.PicklistSchema<["unsupported_chat", "unsupported_provider", "offline"], undefined>, undefined>;
9
+ /** The message's files, each with its own link where the messenger gives one; absent when it has none. */
10
+ readonly files: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
11
+ readonly position: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
12
+ readonly kind: v.StringSchema<undefined>;
13
+ readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
14
+ readonly url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
15
+ readonly reason: v.NullableSchema<v.PicklistSchema<["unsupported_provider"], undefined>, undefined>;
16
+ }, undefined>, undefined>, undefined>;
9
17
  }, undefined>;
@@ -1,5 +1,5 @@
1
1
  import * as v from "valibot";
2
- import { reference, text } from "./fields.js";
2
+ import { count, reference, text } from "./fields.js";
3
3
  /** Where a message can be opened: its locator always, a provider link when there is one and who that link reaches. */
4
4
  export const messageLink = v.strictObject({
5
5
  ref: reference,
@@ -7,4 +7,12 @@ export const messageLink = v.strictObject({
7
7
  access: v.picklist(["public", "restricted", "unknown", "unavailable"]),
8
8
  /** Why there is no link. */
9
9
  reason: v.nullable(v.picklist(["unsupported_chat", "unsupported_provider", "offline"])),
10
+ /** The message's files, each with its own link where the messenger gives one; absent when it has none. */
11
+ files: v.optional(v.array(v.strictObject({
12
+ position: count,
13
+ kind: text,
14
+ name: v.optional(text),
15
+ url: v.nullable(text),
16
+ reason: v.nullable(v.picklist(["unsupported_provider"])),
17
+ }))),
10
18
  });
@@ -6,6 +6,12 @@ export declare const taskCard: v.StrictObjectSchema<{
6
6
  readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
7
7
  readonly project_key: v.StringSchema<undefined>;
8
8
  readonly project_name: v.StringSchema<undefined>;
9
+ readonly created_by: v.NullableSchema<v.StrictObjectSchema<{
10
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
11
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
12
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
13
+ readonly owner: v.BooleanSchema<undefined>;
14
+ }, undefined>, undefined>;
9
15
  readonly title: v.StringSchema<undefined>;
10
16
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
11
17
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
@@ -112,6 +118,12 @@ export declare const taskCreated: v.StrictObjectSchema<{
112
118
  readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
113
119
  readonly project_key: v.StringSchema<undefined>;
114
120
  readonly project_name: v.StringSchema<undefined>;
121
+ readonly created_by: v.NullableSchema<v.StrictObjectSchema<{
122
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
123
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
124
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
125
+ readonly owner: v.BooleanSchema<undefined>;
126
+ }, undefined>, undefined>;
115
127
  readonly title: v.StringSchema<undefined>;
116
128
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
117
129
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
@@ -219,6 +231,12 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
219
231
  readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
220
232
  readonly project_key: v.StringSchema<undefined>;
221
233
  readonly project_name: v.StringSchema<undefined>;
234
+ readonly created_by: v.NullableSchema<v.StrictObjectSchema<{
235
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
236
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
237
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
238
+ readonly owner: v.BooleanSchema<undefined>;
239
+ }, undefined>, undefined>;
222
240
  readonly title: v.StringSchema<undefined>;
223
241
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
224
242
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
@@ -354,6 +372,12 @@ export declare const taskMerge: v.StrictObjectSchema<{
354
372
  readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
355
373
  readonly project_key: v.StringSchema<undefined>;
356
374
  readonly project_name: v.StringSchema<undefined>;
375
+ readonly created_by: v.NullableSchema<v.StrictObjectSchema<{
376
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
377
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
378
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
379
+ readonly owner: v.BooleanSchema<undefined>;
380
+ }, undefined>, undefined>;
357
381
  readonly title: v.StringSchema<undefined>;
358
382
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
359
383
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
@@ -454,6 +478,12 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
454
478
  readonly project: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
455
479
  readonly project_key: v.StringSchema<undefined>;
456
480
  readonly project_name: v.StringSchema<undefined>;
481
+ readonly created_by: v.NullableSchema<v.StrictObjectSchema<{
482
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
483
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
484
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
485
+ readonly owner: v.BooleanSchema<undefined>;
486
+ }, undefined>, undefined>;
457
487
  readonly title: v.StringSchema<undefined>;
458
488
  readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
459
489
  readonly type: v.PicklistSchema<readonly ["bug", "feature", "chore", "question", "request", "mention", "promise"], undefined>;
@@ -27,6 +27,7 @@ export const taskCard = v.strictObject({
27
27
  project: reference,
28
28
  project_key: text,
29
29
  project_name: text,
30
+ created_by: v.nullable(v.strictObject({ type: v.picklist(["person", "bot"]), id, name: v.nullable(text), owner: v.boolean() })),
30
31
  title: text,
31
32
  description: v.nullable(text),
32
33
  type: v.picklist(TASK_TYPES),
@@ -22,6 +22,13 @@ export declare const task: v.StrictObjectSchema<{
22
22
  /** The project's key and name, as `tasks counts` names it. */
23
23
  readonly project_key: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
24
24
  readonly project_name: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
25
+ /** Who added it: the owner, or an agent by its bot name. */
26
+ readonly created_by: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
27
+ readonly type: v.PicklistSchema<["person", "bot"], undefined>;
28
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
29
+ readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
30
+ readonly owner: v.BooleanSchema<undefined>;
31
+ }, undefined>, undefined>, undefined>;
25
32
  readonly account: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
26
33
  /** The chat it came from, as a `chat:` reference. */
27
34
  readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
@@ -25,6 +25,8 @@ export const task = v.strictObject({
25
25
  /** The project's key and name, as `tasks counts` names it. */
26
26
  project_key: v.optional(v.nullable(text)),
27
27
  project_name: v.optional(v.nullable(text)),
28
+ /** Who added it: the owner, or an agent by its bot name. */
29
+ created_by: v.optional(v.nullable(v.strictObject({ type: v.picklist(["person", "bot"]), id, name: v.nullable(text), owner: v.boolean() }))),
28
30
  account: v.optional(text),
29
31
  /** The chat it came from, as a `chat:` reference. */
30
32
  chat: v.optional(v.nullable(reference)),
@@ -133,6 +133,7 @@ const OTHERS = [
133
133
  ["task.stale", "configuration_error", "the task changed since it was read; read it again"],
134
134
  validation("task.column", "the task beside which it moves is in another column than the status asked for"),
135
135
  validation("task.dependency-cycle", "the task would depend on itself"),
136
+ validation("task.has-subtasks", "the task has subtasks; move or detach them before it moves to another project"),
136
137
  validation("task.duplicate-candidates", "open tasks may be the same job: attach to one, or add it as new"),
137
138
  validation("task.certainty-required", "an agent closes a task with its certainty, 0 to 1"),
138
139
  ["task.pair-decided", "permission_error", "the owner already decided the pair; an agent does not change it"],
@@ -0,0 +1,2 @@
1
+ import type { AccountKey } from "../store/index.js";
2
+ export declare const mailAccountKey: (address: string) => AccountKey;
@@ -0,0 +1 @@
1
+ export const mailAccountKey = (address) => ({ provider: "email", account: address.toLowerCase() });
@@ -84,9 +84,9 @@ const searchCommand = (mail) => {
84
84
  partial: true,
85
85
  output: { entity: "message", view: "hit" },
86
86
  examples: [
87
- `${mail.app.command} ya search messages invoice`,
88
- `${mail.app.command} ya search messages 'from:ann@example.com has:attachment date>30d'`,
89
- `${mail.app.command} ya search messages 'list:github.com is:unread' --newest`,
87
+ `${mail.app.command} personal search messages invoice`,
88
+ `${mail.app.command} personal search messages 'from:ann@example.com has:attachment date>30d'`,
89
+ `${mail.app.command} personal search messages 'list:github.com is:unread' --newest`,
90
90
  ],
91
91
  })
92
92
  .summary(mail.server ? "find emails by words and fields" : "find stored emails by words and fields")
@@ -103,7 +103,7 @@ const searchCommand = (mail) => {
103
103
  .option("--timezone <zone>", "the IANA timezone for calendar date boundaries")
104
104
  .addHelpText("after", `
105
105
  Fields: from: to: cc: bcc: subject: body: mailbox: (or label:) list: (a List-Id or its domain) is:unread|read|starred
106
- has:attachment, filename:, content: (the text of attachments), size>, date: — \`${mail.app.command} ya search messages 'content:invoice'\`.
106
+ has:attachment, filename:, content: (the text of attachments), size>, date: — \`${mail.app.command} personal search messages 'content:invoice'\`.
107
107
  chat: names a thread by its subject. Mail sent to many is in the mailbox bulk; NOT mailbox:bulk leaves it out.
108
108
  Mail from senders with signs of fraud comes after the rest of the page; --plain-order keeps it in place.
109
109
  --json: the list is items; each hit's reference is ref (msg:email/…), which messages show reads.${mail.server
@@ -194,8 +194,8 @@ const messagesCommand = (mail) => {
194
194
  annotate(messages.command("show"), {
195
195
  effects: LOCAL_READ,
196
196
  examples: [
197
- `${mail.app.command} ya messages show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E'`,
198
- `${mail.app.command} ya messages show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E' --before-n 2`,
197
+ `${mail.app.command} personal messages show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E'`,
198
+ `${mail.app.command} personal messages show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E' --before-n 2`,
199
199
  ],
200
200
  })
201
201
  .summary("one stored email whole, with the text of its attachments")
@@ -243,8 +243,8 @@ const conversationsCommand = (mail) => {
243
243
  const list = withQuery(annotate(conversations.command("list"), {
244
244
  effects: LOCAL_READ,
245
245
  examples: [
246
- `${mail.app.command} ya conversations list`,
247
- `${mail.app.command} ya conversations list subject:invoice emails_count>1`,
246
+ `${mail.app.command} personal conversations list`,
247
+ `${mail.app.command} personal conversations list subject:invoice emails_count>1`,
248
248
  ],
249
249
  })
250
250
  .summary("stored email threads, newest first")
@@ -291,8 +291,8 @@ const conversationsCommand = (mail) => {
291
291
  annotate(conversations.command("show"), {
292
292
  effects: LOCAL_READ,
293
293
  examples: [
294
- `${mail.app.command} ya conversations show 'chat:email/me%40example.com/thread-1'`,
295
- `${mail.app.command} ya conversations show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E'`,
294
+ `${mail.app.command} personal conversations show 'chat:email/me%40example.com/thread-1'`,
295
+ `${mail.app.command} personal conversations show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E'`,
296
296
  ],
297
297
  })
298
298
  .summary("one email thread, every email oldest first")
@@ -1,3 +1,4 @@
1
+ export { mailAccountKey } from "./account-key.js";
1
2
  export { BULK, bulkBox } from "./bulk.js";
2
3
  export { factsOf } from "./facts.js";
3
4
  export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
@@ -1,3 +1,4 @@
1
+ export { mailAccountKey } from "./account-key.js";
1
2
  export { BULK, bulkBox } from "./bulk.js";
2
3
  export { factsOf } from "./facts.js";
3
4
  export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { mailAccountKey } from "./account-key.js";
2
3
  import { addressOf, FETCH_CHUNK, readAttachments } from "./attachments.js";
3
4
  import { mailFolders } from "./folders.js";
4
5
  import { mailHeadersOf } from "./headers.js";
@@ -12,7 +13,7 @@ export const RETRYABLE_ATTACHMENTS = ["engine-missing"];
12
13
  */
13
14
  export const retryUnread = async ({ store, source, account, since, max, maxBodyChars, maxAttachmentChars, now, loadEngine, index, }) => {
14
15
  const startedAt = now();
15
- const key = { provider: "email", account: account.address.toLowerCase() };
16
+ const key = mailAccountKey(account.address);
16
17
  const find = source.find;
17
18
  if (find === undefined)
18
19
  throw new CliError("validation_error", "--retry-attachments reads mail again by its Gmail id; an mbox file or a plain IMAP account has none");
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { mailAccountKey } from "./account-key.js";
2
3
  import { PRESET_NAMES } from "./presets.js";
3
4
  const ACTION_OF = { full: "keep", envelope: "envelope", skip: "skip" };
4
5
  const KINDS = {
@@ -56,7 +57,7 @@ export const storedMailRule = (stored) => {
56
57
  export const storedMailRuleSet = async (store, account, query) => {
57
58
  let stored;
58
59
  try {
59
- const { id } = await store.accounts.storedAccount({ provider: "email", account: account.address.toLowerCase() });
60
+ const { id } = await store.accounts.storedAccount(mailAccountKey(account.address));
60
61
  stored = await store.mail.rules.list(id, query);
61
62
  }
62
63
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { mailAccountKey } from "./account-key.js";
2
3
  import { domainsToAge, groupKey, probeMail } from "./probe.js";
3
4
  import { decide } from "./rules.js";
4
5
  import { FRAUD_SIGNALS, FREE_MAIL, registrableOf, WEIGHTS } from "./score.js";
@@ -18,7 +19,7 @@ const RECENT = 2 * 365 * DAY;
18
19
  * looked up first, for senders heard from in the last two years.
19
20
  */
20
21
  export const scoreSenders = async (store, address, ages, now = Date.now(), rules = []) => {
21
- const key = { provider: "email", account: address.toLowerCase() };
22
+ const key = mailAccountKey(address);
22
23
  const last = (await store.sync.syncState(key, SCORED))?.value;
23
24
  const done = last === undefined ? undefined : JSON.parse(last);
24
25
  if (done !== undefined && done.version === SCORE_VERSION && now - done.at < DAY)
package/dist/mail/sync.js CHANGED
@@ -2,6 +2,7 @@ import { setTimeout } from "node:timers/promises";
2
2
  import { CliError } from "@wirecat/cli-base";
3
3
  import { importEngine } from "../attachments/extract.js";
4
4
  import { isBusy } from "../store/sqlite/transaction.js";
5
+ import { mailAccountKey } from "./account-key.js";
5
6
  import { FETCH_CHUNK, MAX_ATTACHMENT_CHARS, readAttachments } from "./attachments.js";
6
7
  import { BULK, bulkBox } from "./bulk.js";
7
8
  import { parsedEmail } from "./email-input.js";
@@ -108,7 +109,7 @@ export const syncMail = async (source, store, options) => {
108
109
  })),
109
110
  ];
110
111
  const complete = last.complete && plan.stopped === null && unreadable.length === 0 && remaining === 0;
111
- const account = { provider: "email", account: options.account.address.toLowerCase() };
112
+ const account = mailAccountKey(options.account.address);
112
113
  const indexed = plan.stopped === null ? await index?.(store, account) : undefined;
113
114
  const deletionsSkipped = rounds.map((round) => round.deletionsSkipped).findLast((text) => text !== undefined);
114
115
  const { unreadable: _unreadable, ...rest } = last;
@@ -155,7 +156,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
155
156
  index,
156
157
  });
157
158
  const startedAt = now();
158
- const key = { provider: "email", account: account.address.toLowerCase() };
159
+ const key = mailAccountKey(account.address);
159
160
  const accountId = await store.accounts.saveAccount(key, { name: account.address });
160
161
  const folders = await mailFolders(account, source);
161
162
  if (folders.length < 1 || folders.length > 50)
@@ -272,6 +272,7 @@ export interface ForumControl {
272
272
  enableForum(chatId: Id): Promise<ForumState>;
273
273
  createTopic(chatId: Id, title: string, options: {
274
274
  sendId: string;
275
+ icon?: string;
275
276
  }): Promise<Topic>;
276
277
  }
277
278
  export interface TopicEditing {
@@ -281,6 +282,8 @@ export interface TopicEditing {
281
282
  orderPinnedTopics(chatId: Id, topicIds: Id[]): Promise<void>;
282
283
  /** Deletes the topic and every message in it, for everyone; it cannot be undone. A topic that is gone is `not_found`. */
283
284
  deleteTopic?(chatId: Id, topicId: Id): Promise<void>;
285
+ /** The emojis any account may give a topic as its icon. */
286
+ topicIcons?(): Promise<string[]>;
284
287
  }
285
288
  export interface ThreadAddressing {
286
289
  validateThread(chatId: Id, threadId: Id, options: {
@@ -9,6 +9,7 @@ import { attachment as attachmentEntity } from "../entities/attachment.js";
9
9
  import { type TextOrigin } from "../store/store.js";
10
10
  import { batchProgress } from "./batch.js";
11
11
  import { type ServiceDeps } from "./deps.js";
12
+ import { type Saved } from "./file-download.js";
12
13
  export type ExtractStatus = "extracted" | "needs-agent" | "unreadable" | "engine-missing" | "too-large" | "missing";
13
14
  /** One file looked at. Never its text: the text stays in the local store. */
14
15
  export interface ExtractItem {
@@ -114,6 +115,16 @@ export interface AttachmentsService {
114
115
  items: AttachmentItem[];
115
116
  has_more: boolean;
116
117
  }>;
118
+ /**
119
+ * Copies files the store already holds to `output`, never over a file there, and never asks the messenger: one
120
+ * not held locally is an error before anything is written. The store keeps pointing at its own copy.
121
+ */
122
+ saveHeld(target: {
123
+ chat: string;
124
+ message?: string;
125
+ attachment?: number;
126
+ output: string;
127
+ }): Promise<Saved[]>;
117
128
  /** Keeps text an agent read from a file — a scan, a photo — replacing whatever was there. Sends nothing. */
118
129
  setText(target: {
119
130
  chat: string;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { constants } from "node:fs";
3
- import { open, readFile, stat } from "node:fs/promises";
2
+ import { constants, mkdirSync } from "node:fs";
3
+ import { lstat, open, readFile, stat } from "node:fs/promises";
4
4
  import { CliError } from "@wirecat/cli-base";
5
5
  import { queryFieldsOf, walkQuery } from "@wirecat/cli-base/query";
6
6
  import { directoryPaths } from "../attachments/directory.js";
@@ -18,6 +18,7 @@ import { refusedPlace } from "../sends/upload.js";
18
18
  import { attachmentColumns, queryFilter, } from "../store/store.js";
19
19
  import { batchProgress } from "./batch.js";
20
20
  import { accountStore } from "./deps.js";
21
+ import { save } from "./file-download.js";
21
22
  import { chatIdIn } from "./message-resolution.js";
22
23
  export const ATTACHMENT_QUERY = {
23
24
  entity: "attachment",
@@ -407,6 +408,48 @@ export const attachmentsService = (deps) => ({
407
408
  ...preview,
408
409
  };
409
410
  },
411
+ saveHeld: async ({ chat, message, attachment, output }) => {
412
+ if (attachment !== undefined && (!Number.isSafeInteger(attachment) || attachment < 1))
413
+ throw new CliError("validation_error", "attachment must be a position from 1");
414
+ const store = await accountStore(deps);
415
+ const { account } = store;
416
+ const { chatId, messageId } = await messageOf(deps, store, chat, message);
417
+ const ref = formatLocator({ ...account, chat: chatId, message: messageId });
418
+ const files = (await store.attachments.attachments({ chatId, messageId, limit: 1000 })).filter(({ kind }) => !NOT_FILES.has(kind));
419
+ if (files.length === 0)
420
+ throw new CliError("not_found", `the local store holds no file of ${ref}`);
421
+ const chosen = attachment === undefined ? files : files.filter(({ position }) => position === attachment - 1);
422
+ if (chosen.length === 0)
423
+ throw new CliError("not_found", `${ref} has no file at position ${attachment}; its files are at ${files.map(({ position }) => position + 1).join(", ")}`);
424
+ const env = deps.env ?? process.env;
425
+ const held = await Promise.all(chosen.map(async (file) => {
426
+ const missing = new CliError("not_found", `file ${file.position + 1} of ${ref} is not saved on this computer; run messages download without --offline`);
427
+ if (file.localPath === null)
428
+ throw missing;
429
+ if (refusedPlace(file.localPath, deps.messenger.app, env))
430
+ throw new CliError("validation_error", "cannot copy known credential files, the WireCat apps' own files or the message store");
431
+ const found = await lstat(file.localPath).catch(() => undefined);
432
+ if (!found)
433
+ throw missing;
434
+ if (!found.isFile())
435
+ throw new CliError("validation_error", "the saved attachment must be a regular file");
436
+ return { file, path: file.localPath };
437
+ }));
438
+ mkdirSync(output, { recursive: true });
439
+ const saved = [];
440
+ for (const { file, path } of held)
441
+ saved.push(await save({
442
+ kind: file.kind,
443
+ ...(file.name === null ? {} : { name: file.name }),
444
+ ...(file.mime === null ? {} : { mime: file.mime }),
445
+ position: file.position,
446
+ bytes: async function* () {
447
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
448
+ yield* handle.createReadStream();
449
+ },
450
+ }, output, `${messageId}-${file.position + 1}`));
451
+ return saved;
452
+ },
410
453
  setText: async ({ chat, message, attachment, text }) => {
411
454
  if (text.trim() === "")
412
455
  throw new CliError("validation_error", "the text is empty — nothing to keep");
@@ -72,7 +72,7 @@ export declare const fromStore: (deps: Pick<ServiceDeps, "offline" | "reads">) =
72
72
  export declare const nothingStored: (messenger: MessengerTraits) => string;
73
73
  /** Where a store-mode messenger's history comes from, for what it cannot do. */
74
74
  export declare const PUSHED = "this messenger's history is read from the local store";
75
- export declare const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context`, `contacts list|show` and `review`";
75
+ export declare const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context|download`, `contacts list|show` and `review`";
76
76
  /** Over a connection that is already open — an MCP session holds one for minutes. */
77
77
  export declare const onlineDeps: (messenger: ServiceMessenger, adapter: MessengerAdapter, guard: SendGuard, where?: {
78
78
  profile?: string;
@@ -55,7 +55,7 @@ export const fromStore = (deps) => deps.offline || deps.reads === "store";
55
55
  export const nothingStored = (messenger) => `nothing stored for this chat yet — keep \`${typedCommand(messenger.app.command, "serve")}\` running`;
56
56
  /** Where a store-mode messenger's history comes from, for what it cannot do. */
57
57
  export const PUSHED = "this messenger's history is read from the local store";
58
- export const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context`, `contacts list|show` and `review`";
58
+ export const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context|download`, `contacts list|show` and `review`";
59
59
  /** Over a connection that is already open — an MCP session holds one for minutes. */
60
60
  export const onlineDeps = (messenger, adapter, guard, where = {}) => ({
61
61
  messenger,
@@ -197,7 +197,6 @@ export interface Reacted {
197
197
  }
198
198
  /** max-cli's: many at once is what a ban for automation looks like. */
199
199
  export declare const DELETE_AT_ONCE = 10;
200
- /** Every write goes through the guard: asked before it goes, told after, on every outcome. */
201
200
  export interface MessagesService {
202
201
  /** The account these messages are read as, which names each one's locator. */
203
202
  reader(): Promise<AccountKey>;
@@ -212,8 +211,8 @@ export interface MessagesService {
212
211
  }): Promise<Page<Message> & {
213
212
  discussion: Discussion;
214
213
  }>;
215
- /** The files of one message. Always from the messenger, whatever its history is read from. */
216
- download(chat: string, message: Id): Promise<Download>;
214
+ /** The files of one message, `chat` and its id or a `msg:` locator alone. Always from the messenger. */
215
+ download(chat: string, message?: Id): Promise<Download>;
217
216
  /**
218
217
  * Records in the local store where the files went, so their text can be read later. Only a
219
218
  * message the store holds; answers how many attachments it recorded.
@@ -1,7 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { validateFormattedText, visibleFormattedText } from "../domain/formatting.js";
3
3
  import { formatLocator } from "../domain/locator.js";
4
- import { messageLinkTarget, validatePermalink } from "../domain/message-link.js";
4
+ import { fileLinks, messageLinkTarget, validatePermalink } from "../domain/message-link.js";
5
5
  import { pinDurationsText } from "../domain/pin-duration.js";
6
6
  import { threadIdOf } from "../domain/thread.js";
7
7
  import { capability } from "../ports/messenger.js";
@@ -22,6 +22,12 @@ import { searchServer, sourceKey, } from "./server-search.js";
22
22
  import { readThreadContext, threadBounds } from "./thread-context.js";
23
23
  /** max-cli's: many at once is what a ban for automation looks like. */
24
24
  export const DELETE_AT_ONCE = 10;
25
+ /** Every write goes through the guard: asked before it goes, told after, on every outcome. */
26
+ /** Only a message with files says what can be linked of them. */
27
+ const filesOf = ({ attachments }) => {
28
+ const files = fileLinks(attachments);
29
+ return files.length > 0 ? { files } : {};
30
+ };
25
31
  export const messagesService = (deps) => {
26
32
  const { guard } = deps;
27
33
  const inStore = async (read) => read(await deps.store(), await deps.account());
@@ -185,13 +191,15 @@ export const messagesService = (deps) => {
185
191
  const store = await deps.store();
186
192
  const chat = await readChatId(deps, target.chat, store, account);
187
193
  const found = await store.messages.around(account, chat, target.message, { before: 0, after: 0 });
188
- if (!found.some((item) => item.id === target.message && item.chatId === chat))
194
+ const held = found.find((item) => item.id === target.message && item.chatId === chat);
195
+ if (!held)
189
196
  throw new CliError("not_found", "no such message in this account's stored chat");
190
197
  return {
191
198
  locator: formatLocator({ ...account, chat, message: target.message }),
192
199
  url: null,
193
200
  access: "unavailable",
194
201
  reason: deps.offline ? "offline" : "unsupported_provider",
202
+ ...filesOf(held),
195
203
  };
196
204
  }
197
205
  const connection = await deps.connection();
@@ -199,20 +207,31 @@ export const messagesService = (deps) => {
199
207
  throw new CliError("authentication_error", "the connected account differs from the recorded profile; refresh the session before linking");
200
208
  const { id: chat } = await connection.resolve(target.chat);
201
209
  const locator = formatLocator({ ...account, chat, message: target.message });
202
- if (connection.permalink)
203
- return { locator, ...validatePermalink(await connection.permalink(chat, target.message)) };
210
+ if (connection.permalink) {
211
+ const permalink = validatePermalink(await connection.permalink(chat, target.message));
212
+ const held = await deps
213
+ .store()
214
+ .then((store) => store.messages.around(account, chat, target.message, { before: 0, after: 0 }))
215
+ .then((found) => found.find((item) => item.id === target.message && item.chatId === chat))
216
+ .catch(() => undefined);
217
+ return { locator, ...permalink, ...(held ? filesOf(held) : {}) };
218
+ }
204
219
  const found = await capability(connection, "around", "read a message")(chat, target.message, {
205
220
  before: 0,
206
221
  after: 0,
207
222
  });
208
- if (!found.some((item) => item.id === target.message && item.chatId === chat))
223
+ const held = found.find((item) => item.id === target.message && item.chatId === chat);
224
+ if (!held)
209
225
  throw new CliError("not_found", "no such message in this chat");
210
- return { locator, url: null, access: "unavailable", reason: "unsupported_provider" };
226
+ return { locator, url: null, access: "unavailable", reason: "unsupported_provider", ...filesOf(held) };
211
227
  },
212
- download: async (chat, message) => {
228
+ download: async (reference, message) => {
229
+ const target = messageLinkTarget(reference, message, deps.messenger.provider);
230
+ if (target.account !== undefined && target.account !== (await deps.account()).account)
231
+ throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
213
232
  const connection = await deps.connection();
214
233
  const download = capability(connection, "download", "download attachments");
215
- return download((await connection.resolve(chat)).id, message);
234
+ return download((await connection.resolve(target.chat)).id, target.message);
216
235
  },
217
236
  keepDownloaded: (chat, message, files) => inStore(async (store, account) => {
218
237
  const chatId = (await storedChatId(deps.messenger, chat, store, account).catch(() => undefined)) ??
@@ -1,4 +1,5 @@
1
1
  import { type ErrorCode } from "@wirecat/cli-base";
2
+ import { type HitFile } from "../domain/attachments.js";
2
3
  import { type MailFields } from "../domain/models.js";
3
4
  import type { MessengerTraits } from "../ports/traits.js";
4
5
  import type { AccountKey, MessageStore } from "../store/store.js";
@@ -30,6 +31,8 @@ export interface SearchAllItem {
30
31
  senderIsMe?: boolean;
31
32
  /** A mail's people, labels and thread, as `search mail` gives them. */
32
33
  email?: MailFields;
34
+ /** A message's or a mail's files, as `search messages` names them. */
35
+ attachments?: HitFile[];
33
36
  /** Which search found it: words, meaning or both. */
34
37
  foundBy: ("title" | "words" | "meaning")[];
35
38
  }