@wirecat/cli-core 0.550.0 → 0.551.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 (128) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/THIRD_PARTY_NOTICES +41 -0
  3. package/dist/calendar/ics.d.ts +1 -1
  4. package/dist/calendar/ics.js +5 -2
  5. package/dist/calendar/windows-zones.generated.d.ts +1 -0
  6. package/dist/calendar/windows-zones.generated.js +146 -0
  7. package/dist/cli/compact.js +2 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.js +3 -0
  10. package/dist/cli/meeting-findings-input.js +4 -3
  11. package/dist/cli/messenger/backfill-command.js +28 -1
  12. package/dist/cli/messenger/context.js +1 -0
  13. package/dist/cli/messenger/history-gaps.js +4 -4
  14. package/dist/cli/messenger/mcp-command.js +1 -0
  15. package/dist/cli/messenger/messages-delete-command.js +7 -8
  16. package/dist/cli/messenger/projects-command.js +47 -2
  17. package/dist/cli/payment-query.d.ts +8 -0
  18. package/dist/cli/payment-query.js +27 -0
  19. package/dist/cli/project-status-rows.d.ts +8 -0
  20. package/dist/cli/project-status-rows.js +102 -0
  21. package/dist/cli/project-status.d.ts +132 -0
  22. package/dist/cli/project-status.js +248 -0
  23. package/dist/cli/steps.js +4 -1
  24. package/dist/domain/knowledge.d.ts +1 -1
  25. package/dist/domain/knowledge.js +1 -1
  26. package/dist/domain/status-words.d.ts +3 -0
  27. package/dist/domain/status-words.js +44 -0
  28. package/dist/entities/fact.d.ts +12 -1
  29. package/dist/entities/fact.js +13 -0
  30. package/dist/entities/index.d.ts +18 -2
  31. package/dist/entities/meeting-findings.d.ts +1 -1
  32. package/dist/entities/message-transfer.d.ts +16 -0
  33. package/dist/entities/message-transfer.js +6 -0
  34. package/dist/mcp/app-server.js +1 -0
  35. package/dist/mcp/http/oauth.d.ts +2 -1
  36. package/dist/mcp/http/oauth.js +2 -2
  37. package/dist/mcp/http/serve.d.ts +2 -0
  38. package/dist/mcp/http/serve.js +2 -1
  39. package/dist/mcp/server.js +1 -0
  40. package/dist/mcp/surface.js +1 -1
  41. package/dist/meetings/findings-rules.d.ts +14 -3
  42. package/dist/meetings/findings-rules.js +16 -2
  43. package/dist/meetings/index.d.ts +3 -2
  44. package/dist/meetings/index.js +2 -1
  45. package/dist/meetings/meeting-context.d.ts +2 -3
  46. package/dist/meetings/meeting-context.js +3 -4
  47. package/dist/meetings/payment.d.ts +27 -0
  48. package/dist/meetings/payment.js +81 -0
  49. package/dist/services/archive.d.ts +13 -0
  50. package/dist/services/archive.js +56 -5
  51. package/dist/services/deps.d.ts +3 -0
  52. package/dist/services/history-limits.d.ts +2 -0
  53. package/dist/services/index.d.ts +4 -1
  54. package/dist/services/index.js +4 -1
  55. package/dist/services/job-query.d.ts +5 -2
  56. package/dist/services/job-query.js +5 -2
  57. package/dist/services/link-scorers/alias-hits.d.ts +7 -0
  58. package/dist/services/link-scorers/alias-hits.js +54 -0
  59. package/dist/services/link-scorers/chat-senders.d.ts +6 -0
  60. package/dist/services/link-scorers/chat-senders.js +56 -0
  61. package/dist/services/link-scorers/chat-title.d.ts +6 -0
  62. package/dist/services/link-scorers/chat-title.js +44 -0
  63. package/dist/services/link-scorers/email-domain.d.ts +6 -0
  64. package/dist/services/link-scorers/email-domain.js +64 -0
  65. package/dist/services/link-scorers/index.d.ts +9 -0
  66. package/dist/services/link-scorers/index.js +8 -0
  67. package/dist/services/link-scorers/mail-thread.d.ts +3 -0
  68. package/dist/services/link-scorers/mail-thread.js +74 -0
  69. package/dist/services/link-scorers/member-chats.d.ts +3 -0
  70. package/dist/services/link-scorers/member-chats.js +40 -0
  71. package/dist/services/link-scorers/member-dialog.d.ts +3 -0
  72. package/dist/services/link-scorers/member-dialog.js +34 -0
  73. package/dist/services/link-scorers/scan.d.ts +41 -0
  74. package/dist/services/link-scorers/scan.js +244 -0
  75. package/dist/services/link-scorers/shared.d.ts +26 -0
  76. package/dist/services/link-scorers/shared.js +48 -0
  77. package/dist/services/link-scorers/types.d.ts +77 -0
  78. package/dist/services/link-scorers/types.js +1 -0
  79. package/dist/services/project-status/index.d.ts +71 -0
  80. package/dist/services/project-status/index.js +196 -0
  81. package/dist/services/project-status/items.d.ts +10 -0
  82. package/dist/services/project-status/items.js +23 -0
  83. package/dist/services/project-status/signals/amounts.d.ts +20 -0
  84. package/dist/services/project-status/signals/amounts.js +91 -0
  85. package/dist/services/project-status/signals/children.d.ts +13 -0
  86. package/dist/services/project-status/signals/children.js +28 -0
  87. package/dist/services/project-status/signals/coverage.d.ts +13 -0
  88. package/dist/services/project-status/signals/coverage.js +30 -0
  89. package/dist/services/project-status/signals/index.d.ts +3 -0
  90. package/dist/services/project-status/signals/index.js +21 -0
  91. package/dist/services/project-status/signals/last-message.d.ts +4 -0
  92. package/dist/services/project-status/signals/last-message.js +22 -0
  93. package/dist/services/project-status/signals/payments.d.ts +9 -0
  94. package/dist/services/project-status/signals/payments.js +24 -0
  95. package/dist/services/project-status/signals/promises.d.ts +17 -0
  96. package/dist/services/project-status/signals/promises.js +109 -0
  97. package/dist/services/project-status/signals/questions.d.ts +8 -0
  98. package/dist/services/project-status/signals/questions.js +38 -0
  99. package/dist/services/project-status/signals/reply-next.d.ts +19 -0
  100. package/dist/services/project-status/signals/reply-next.js +75 -0
  101. package/dist/services/project-status/signals/topics.d.ts +10 -0
  102. package/dist/services/project-status/signals/topics.js +50 -0
  103. package/dist/services/project-status/types.d.ts +84 -0
  104. package/dist/services/project-status/types.js +1 -0
  105. package/dist/services/proposals/index.d.ts +1 -0
  106. package/dist/services/proposals/index.js +1 -0
  107. package/dist/services/proposals/kinds/findings.d.ts +3 -0
  108. package/dist/services/proposals/kinds/findings.js +8 -1
  109. package/dist/store/decisions.d.ts +5 -1
  110. package/dist/store/decisions.js +5 -1
  111. package/dist/store/evidence-findings.js +1 -0
  112. package/dist/store/index.d.ts +2 -1
  113. package/dist/store/index.js +1 -0
  114. package/dist/store/project-transfer/request.d.ts +1 -1
  115. package/dist/store/project-transfer/schema.d.ts +2 -2
  116. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  117. package/dist/store/sqlite/evidence-findings.js +5 -1
  118. package/dist/store/sqlite/facts.d.ts +13 -0
  119. package/dist/store/sqlite/facts.js +64 -6
  120. package/dist/store/sqlite/findings-core.js +14 -1
  121. package/dist/store/sqlite/knowledge.d.ts +16 -2
  122. package/dist/store/sqlite/knowledge.js +1 -0
  123. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  124. package/dist/store/sqlite/relation-proposals.d.ts +29 -2
  125. package/dist/store/sqlite/relation-proposals.js +75 -4
  126. package/dist/store/store.d.ts +1 -0
  127. package/package.json +3 -3
  128. package/parity.json +11 -0
@@ -26,14 +26,13 @@ export const deleteCommand = (messenger) => annotate(new Command("delete"), {
26
26
  if (environmentOf(this).agent && (everyone || messenger.deleteForMe === false))
27
27
  throw new CliError("permission_error", `${everyone ? "" : `${messenger.name ?? messenger.provider} deletes only for everyone, and `}an agent never ` +
28
28
  `deletes for everyone — the owner can run \`${lineOf(this, "messages delete --for-everyone")}\``, { permission: "messages.delete", sent: false });
29
+ const ids = messages
30
+ .flatMap((one) => one.split(","))
31
+ .map((one) => one.trim())
32
+ .filter(Boolean);
33
+ if (ids.length === 0)
34
+ throw new CliError("validation_error", "no message id given — name the messages to delete", { sent: false });
29
35
  const context = messengerContext(this, messenger);
30
- const deleted = await context.withServices((services) => services.messages.delete({
31
- chat,
32
- messages: messages
33
- .flatMap((one) => one.split(","))
34
- .map((one) => one.trim())
35
- .filter(Boolean),
36
- forEveryone: everyone,
37
- }));
36
+ const deleted = await context.withServices((services) => services.messages.delete({ chat, messages: ids, forEveryone: everyone }));
38
37
  renderAnswer(this, context, itemOf(snakeKeys(deleted)));
39
38
  });
@@ -3,12 +3,15 @@ import { annotate, annotateOption } from "@wirecat/cli-base/commands";
3
3
  import { Command, Option } from "commander";
4
4
  import { parseDuration } from "../../domain/duration.js";
5
5
  import { FETCHING } from "../../services/archive.js";
6
+ import { projectStatus, STATUS_SIGNALS } from "../../services/project-status/index.js";
6
7
  import { gatherStoredProject } from "../../services/stored-project-gathering.js";
7
- import { FETCH } from "../effects.js";
8
+ import { answerEntity } from "../answer-entity.js";
9
+ import { FETCH, LOCAL_READ } from "../effects.js";
8
10
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
9
11
  import { positiveCount } from "../paging.js";
10
12
  import { projectAnswerParts, projectCutoffTime, projectEvidenceText, projectWindowOf, withSyncCommands, } from "../project-evidence.js";
11
- import { lineOf } from "../steps.js";
13
+ import { projectStatusAnswer, projectStatusRow, projectStatusSchema, projectStatusText } from "../project-status.js";
14
+ import { lineOf, nextStep, rootOf, stepLine } from "../steps.js";
12
15
  import { messengerContext } from "./context.js";
13
16
  const collect = (value, previous) => [...previous, value];
14
17
  const upTo = (most, flag) => (value) => {
@@ -128,8 +131,50 @@ export const projectsCommand = (messenger) => {
128
131
  };
129
132
  renderAnswer(this, context, itemOf(snakeKeys(withSyncCommands(found, hints.sync)), parts), () => projectEvidenceText(found, hints));
130
133
  }));
134
+ projects.addCommand(statusCommand(messenger));
131
135
  return projects;
132
136
  };
137
+ /** Where a project stands from this profile's stored messages: no model call, nothing leaves the machine. */
138
+ const statusCommand = (messenger) => answerEntity(annotate(new Command("status"), {
139
+ examples: [`${messenger.app.command} personal projects status HRB --context-query 1C`],
140
+ effects: LOCAL_READ,
141
+ partial: true,
142
+ })
143
+ .summary("where a project stands in this account's linked chats: whose move, open asks, money")
144
+ .description("per chat confirmed as the project's (and its child projects'), from what the store holds: the last message, " +
145
+ "who replies next, open questions both ways, promises, amounts with their direction, payments recorded, " +
146
+ "how long each --context-query has been quiet on their side, and how much of the chat is held. Amounts, " +
147
+ "directions and promises found by words are marked pattern: read the cited message before trusting one. " +
148
+ "Only this profile's account is read; the other linked chats are named, and memo projects status reads them")
149
+ .argument("<project>", "an existing project's id, key, exact name or alias")
150
+ .option("--since-time <time>", "how far back: days (60d, at most 730d) or a start date (2026-09-01)", "60d")
151
+ .option("--before-time <time>", "fixed exclusive end: ISO 8601 timestamp with UTC offset")
152
+ .option("--context-query <words>", "words that all appear in one text; reports their last mention per side. Repeat for another", collect, [])
153
+ .action(async function (project) {
154
+ const options = this.opts();
155
+ const context = messengerContext(this, messenger);
156
+ const found = await context.withStore((store, account) => projectStatus(store, {
157
+ project,
158
+ accounts: [account],
159
+ ...projectWindowOf(options.sinceTime),
160
+ ...(options.beforeTime === undefined ? {} : { asOf: projectCutoffTime(options.beforeTime) }),
161
+ contextQueries: options.contextQuery,
162
+ }));
163
+ const root = rootOf(this);
164
+ const hints = {
165
+ sync: (chat, since) => nextStep("messages.sync", { args: { chat: chat.chatId, since_time: since } }),
166
+ read: (chat, since) => nextStep("messages.list", { args: { chat: chat.chatId, query: `date>=${since}` } }),
167
+ line: (step) => (step.kind === "command" ? lineOf(this, wordsOf(root, step)) : step.why),
168
+ };
169
+ const { item, ...parts } = projectStatusAnswer(found, hints);
170
+ renderAnswer(this, context, itemOf(item, parts), () => projectStatusText(found, hints));
171
+ }), "project_status", projectStatusSchema, { row: projectStatusRow(STATUS_SIGNALS) });
172
+ /** A step's words after the tool and the profile, so `lineOf` names the profile the run used. */
173
+ const wordsOf = (root, step) => {
174
+ const profile = root.getOptionValue("profile");
175
+ const typed = typeof profile === "string" && profile !== "" ? 2 : 1;
176
+ return stepLine(root, step).split(" ").slice(typed).join(" ");
177
+ };
133
178
  const quote = (text) => (/^[\w:.-]+$/.test(text) ? text : `"${text.replace(/(["\\$`])/g, "\\$1")}"`);
134
179
  /** A line per saved page, counts only; a timed heartbeat with titles is the sync-progress work's (cli-core#2121). */
135
180
  const progressNote = (note) => ({ chat, index, of, fetched }) => note(`chat ${index + 1} of ${of} · ${chat} · ${fetched} messages so far`);
@@ -0,0 +1,8 @@
1
+ import type { PaymentFilter } from "../store/store.js";
2
+ /**
3
+ * The terms a facts list adds for payments. `state` is the payment's, not `status`, which stays the review status
4
+ * (proposed, accepted).
5
+ */
6
+ export declare const PAYMENT_QUERY_FIELDS: import("@wirecat/cli-base/query").FieldSpec[];
7
+ /** The store's payment filter from `queryTerms`' answer; `undefined` when no payment term was given. */
8
+ export declare const paymentFilterOf: (terms: Map<string, string>) => PaymentFilter | undefined;
@@ -0,0 +1,27 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { PAYMENT_DIRECTIONS, PAYMENT_STATES } from "../meetings/index.js";
3
+ import { termField } from "./list-query.js";
4
+ /**
5
+ * The terms a facts list adds for payments. `state` is the payment's, not `status`, which stays the review status
6
+ * (proposed, accepted).
7
+ */
8
+ export const PAYMENT_QUERY_FIELDS = [
9
+ termField("open", "bool", "open:true"),
10
+ termField("direction", "enum", "direction:to_us", { values: PAYMENT_DIRECTIONS }),
11
+ termField("state", "enum", "state:paid", { values: PAYMENT_STATES }),
12
+ ];
13
+ /** The store's payment filter from `queryTerms`' answer; `undefined` when no payment term was given. */
14
+ export const paymentFilterOf = (terms) => {
15
+ const open = terms.get("open");
16
+ const direction = terms.get("direction");
17
+ const state = terms.get("state");
18
+ if (open === undefined && direction === undefined && state === undefined)
19
+ return undefined;
20
+ if (open !== undefined && open !== "true" && open !== "false")
21
+ throw new CliError("validation_error", "open takes true or false");
22
+ return {
23
+ ...(open === undefined ? {} : { open: open === "true" }),
24
+ ...(direction === undefined ? {} : { direction }),
25
+ ...(state === undefined ? {} : { state }),
26
+ };
27
+ };
@@ -0,0 +1,8 @@
1
+ import type { RowShape } from "./compact.js";
2
+ /**
3
+ * The compact row of each status signal's items, by `status.<signal id>`; a signal's file and its row are the two
4
+ * places a new signal touches. They live here because a service may not import the CLI's row type.
5
+ */
6
+ export declare const STATUS_SIGNAL_ROWS: Readonly<Record<string, RowShape>>;
7
+ /** The rest of a `project_status` answer: its chats, open money, chats left out, and failed signals. */
8
+ export declare const STATUS_ROWS: Readonly<Record<string, RowShape>>;
@@ -0,0 +1,102 @@
1
+ const where = [{ field: "ref" }, { field: ["chat_title", "chat"] }];
2
+ const who = { field: ["who.owner", "who.name"], tag: "you" };
3
+ /**
4
+ * The compact row of each status signal's items, by `status.<signal id>`; a signal's file and its row are the two
5
+ * places a new signal touches. They live here because a service may not import the CLI's row type.
6
+ */
7
+ export const STATUS_SIGNAL_ROWS = {
8
+ "status.last-message": { columns: [...where, { field: "at", as: "time" }, who] },
9
+ "status.reply-next": {
10
+ columns: [
11
+ ...where,
12
+ { field: "data.next" },
13
+ { field: "data.addressed.name" },
14
+ { field: "data.waiting_since", as: "day" },
15
+ { field: "data.overdue", tag: "overdue" },
16
+ ],
17
+ },
18
+ "status.questions": {
19
+ columns: [...where, { field: "at", as: "day" }, { field: "data.side" }, who],
20
+ text: ["data.excerpt"],
21
+ },
22
+ "status.promises": {
23
+ columns: [
24
+ ...where,
25
+ { field: "at", as: "day" },
26
+ { field: "data.source" },
27
+ { field: "data.side" },
28
+ { field: "data.due", as: "day" },
29
+ { field: "data.status" },
30
+ ],
31
+ text: ["data.title"],
32
+ },
33
+ "status.amounts": {
34
+ columns: [
35
+ ...where,
36
+ { field: "data.direction" },
37
+ { field: "data.amount" },
38
+ { field: "data.currency" },
39
+ { field: "data.state" },
40
+ { field: "data.since", as: "day" },
41
+ { field: "data.asks", as: "count", tag: "asks" },
42
+ { field: "basis" },
43
+ ],
44
+ },
45
+ "status.payments": {
46
+ columns: [{ field: "ref" }, { field: "at", as: "day" }, { field: "data.status" }],
47
+ text: ["data.title"],
48
+ },
49
+ "status.topics": {
50
+ columns: [
51
+ ...where,
52
+ { field: "data.query" },
53
+ { field: "data.theirs_last", as: "day" },
54
+ { field: "data.yours_last", as: "day" },
55
+ { field: "data.silent_since", as: "day" },
56
+ ],
57
+ },
58
+ "status.coverage": {
59
+ columns: [
60
+ { field: "chat" },
61
+ { field: "chat_title" },
62
+ { field: "data.state" },
63
+ { field: "data.held.messages", as: "count", tag: "held" },
64
+ { field: "data.held.oldest_at", as: "day" },
65
+ { field: "sync_command" },
66
+ ],
67
+ },
68
+ "status.children": {
69
+ columns: [
70
+ { field: "data.child" },
71
+ { field: "data.key" },
72
+ { field: "data.name" },
73
+ { field: "data.chats", as: "count", tag: "chats" },
74
+ { field: "data.cut" },
75
+ ],
76
+ },
77
+ };
78
+ /** The rest of a `project_status` answer: its chats, open money, chats left out, and failed signals. */
79
+ export const STATUS_ROWS = {
80
+ ...STATUS_SIGNAL_ROWS,
81
+ "status.chat": {
82
+ columns: [
83
+ { field: "chat" },
84
+ { field: "title" },
85
+ { field: "kind" },
86
+ { field: "role" },
87
+ { field: "attention", as: "count", tag: "attention" },
88
+ { field: "why" },
89
+ ],
90
+ },
91
+ "status.money": {
92
+ columns: [
93
+ { field: "direction" },
94
+ { field: "amount" },
95
+ { field: "currency" },
96
+ { field: "rows", as: "count", tag: "rows" },
97
+ ],
98
+ },
99
+ "status.signal": { columns: [{ field: "id" }, { field: "version" }] },
100
+ "status.out-of-scope": { columns: [{ field: "chat" }, { field: "reason" }, { field: "purpose" }] },
101
+ "status.failed": { columns: [{ field: "signal" }], text: ["why"] },
102
+ };
@@ -0,0 +1,132 @@
1
+ import type { NextStep } from "@wirecat/cli-base/standards";
2
+ import * as v from "valibot";
3
+ import type { ProjectStatus, StatusChatSummary } from "../services/project-status/index.js";
4
+ import type { RowShape } from "./compact.js";
5
+ export interface ProjectStatusHints {
6
+ /** The step that syncs a chat held in part back to `since` (`YYYY-MM-DD`); null when no command here can. */
7
+ sync: (chat: StatusChatSummary, since: string) => NextStep | null;
8
+ /** The step that reads a chat's stored messages from `since`; null when no command here can. */
9
+ read: (chat: StatusChatSummary, since: string) => NextStep | null;
10
+ /** The command line a step runs, for the text form. */
11
+ line: (step: NextStep) => string;
12
+ }
13
+ /** The `project_status` answer drawn by `--compact`: each signal's list with that signal's row. */
14
+ export declare const projectStatusRow: (signals: readonly {
15
+ id: string;
16
+ }[]) => RowShape;
17
+ /** Where a project stands: per chat signals, open money, and what was left out. Empty lists are left out. */
18
+ export declare const projectStatusSchema: v.LooseObjectSchema<{
19
+ readonly project: v.StringSchema<undefined>;
20
+ readonly key: v.NullableSchema<v.StringSchema<undefined>, undefined>;
21
+ readonly name: v.StringSchema<undefined>;
22
+ readonly window: v.StrictObjectSchema<{
23
+ readonly since: v.StringSchema<undefined>;
24
+ readonly until: v.StringSchema<undefined>;
25
+ }, undefined>;
26
+ readonly signals: v.ArraySchema<v.StrictObjectSchema<{
27
+ readonly id: v.StringSchema<undefined>;
28
+ readonly version: v.NumberSchema<undefined>;
29
+ }, undefined>, undefined>;
30
+ readonly money: v.ArraySchema<v.StrictObjectSchema<{
31
+ readonly direction: v.StringSchema<undefined>;
32
+ readonly currency: v.NullableSchema<v.StringSchema<undefined>, undefined>;
33
+ readonly amount: v.StringSchema<undefined>;
34
+ readonly rows: v.NumberSchema<undefined>;
35
+ }, undefined>, undefined>;
36
+ readonly chats: v.ArraySchema<v.StrictObjectSchema<{
37
+ readonly chat: v.StringSchema<undefined>;
38
+ readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
39
+ readonly kind: v.StringSchema<undefined>;
40
+ readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
41
+ readonly purpose: v.StringSchema<undefined>;
42
+ readonly project: v.StringSchema<undefined>;
43
+ readonly coverage: v.StringSchema<undefined>;
44
+ readonly attention: v.NumberSchema<undefined>;
45
+ readonly why: v.NullableSchema<v.StringSchema<undefined>, undefined>;
46
+ }, undefined>, undefined>;
47
+ readonly out_of_scope: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
48
+ readonly chat: v.StringSchema<undefined>;
49
+ readonly reason: v.StringSchema<undefined>;
50
+ readonly purpose: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
51
+ }, undefined>, undefined>, undefined>;
52
+ readonly failed: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
53
+ readonly signal: v.StringSchema<undefined>;
54
+ readonly why: v.StringSchema<undefined>;
55
+ }, undefined>, undefined>, undefined>;
56
+ }, undefined>;
57
+ export type ProjectStatusItem = v.InferOutput<typeof projectStatusSchema>;
58
+ /** The answer of `projects status`: the item, two steps at most, what is missing, and the window. */
59
+ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectStatusHints) => {
60
+ item: {
61
+ project: string;
62
+ key: string | null;
63
+ name: string;
64
+ window: {
65
+ since: string;
66
+ until: string;
67
+ };
68
+ signals: {
69
+ id: string;
70
+ version: number;
71
+ }[];
72
+ money: {
73
+ direction: string;
74
+ currency: string | null;
75
+ amount: string;
76
+ rows: number;
77
+ }[];
78
+ chats: {
79
+ chat: string;
80
+ title: string | null;
81
+ kind: string;
82
+ role: string | null;
83
+ purpose: string;
84
+ project: string;
85
+ coverage: string;
86
+ attention: number;
87
+ why: string | null;
88
+ }[];
89
+ out_of_scope?: {
90
+ chat: string;
91
+ reason: string;
92
+ purpose?: string | undefined;
93
+ }[] | undefined;
94
+ failed?: {
95
+ signal: string;
96
+ why: string;
97
+ }[] | undefined;
98
+ } & {
99
+ [key: string]: unknown;
100
+ };
101
+ next: ({
102
+ kind: "command";
103
+ step: string;
104
+ command: string;
105
+ args: {
106
+ [x: string]: unknown;
107
+ };
108
+ line?: string | undefined;
109
+ why: string;
110
+ safe_to_auto_execute: boolean;
111
+ } | {
112
+ kind: "user_action";
113
+ step: string;
114
+ why: string;
115
+ safe_to_auto_execute: false;
116
+ })[];
117
+ missing: {
118
+ field: string;
119
+ why: string;
120
+ step?: string | undefined;
121
+ }[];
122
+ meta: {
123
+ window: {
124
+ since: string;
125
+ until: string;
126
+ };
127
+ complete: boolean;
128
+ accounts?: string[] | undefined;
129
+ };
130
+ };
131
+ /** The human form: header, money, a line per chat with what is open, then the steps. */
132
+ export declare const projectStatusText: (found: ProjectStatus, hints: ProjectStatusHints) => string;
@@ -0,0 +1,248 @@
1
+ import * as v from "valibot";
2
+ import { snakeKeys } from "./envelope.js";
3
+ import { heldPhrase } from "./messenger/search-answers.js";
4
+ import { STATUS_SIGNAL_ROWS } from "./project-status-rows.js";
5
+ const day = (at) => new Date(at).toISOString().slice(0, 10);
6
+ const listKey = (signal) => signal.replaceAll("-", "_");
7
+ const SECTIONS = {
8
+ signals: "status.signal",
9
+ money: "status.money",
10
+ chats: "status.chat",
11
+ out_of_scope: "status.out-of-scope",
12
+ failed: "status.failed",
13
+ };
14
+ /** The `project_status` answer drawn by `--compact`: each signal's list with that signal's row. */
15
+ export const projectStatusRow = (signals) => ({
16
+ columns: [],
17
+ sections: {
18
+ ...SECTIONS,
19
+ ...Object.fromEntries(signals.map(({ id }) => [listKey(id), `status.${id}`])),
20
+ },
21
+ });
22
+ const statusItem = v.looseObject({
23
+ signal: v.string(),
24
+ ref: v.nullable(v.string()),
25
+ chat: v.nullable(v.string()),
26
+ chat_title: v.nullable(v.string()),
27
+ at: v.nullable(v.string()),
28
+ who: v.nullable(v.strictObject({ ref: v.nullable(v.string()), name: v.nullable(v.string()), owner: v.boolean() })),
29
+ evidence: v.array(v.strictObject({ ref: v.string(), fingerprint: v.nullable(v.string()) })),
30
+ basis: v.picklist(["store", "fact", "task", "pattern"]),
31
+ data: v.record(v.string(), v.unknown()),
32
+ });
33
+ /** Where a project stands: per chat signals, open money, and what was left out. Empty lists are left out. */
34
+ export const projectStatusSchema = v.looseObject({
35
+ project: v.string(),
36
+ key: v.nullable(v.string()),
37
+ name: v.string(),
38
+ window: v.strictObject({ since: v.string(), until: v.string() }),
39
+ signals: v.array(v.strictObject({ id: v.string(), version: v.number() })),
40
+ money: v.array(v.strictObject({ direction: v.string(), currency: v.nullable(v.string()), amount: v.string(), rows: v.number() })),
41
+ chats: v.array(v.strictObject({
42
+ chat: v.string(),
43
+ title: v.nullable(v.string()),
44
+ kind: v.string(),
45
+ role: v.nullable(v.string()),
46
+ purpose: v.string(),
47
+ project: v.string(),
48
+ coverage: v.string(),
49
+ attention: v.number(),
50
+ why: v.nullable(v.string()),
51
+ })),
52
+ out_of_scope: v.optional(v.array(v.strictObject({ chat: v.string(), reason: v.string(), purpose: v.optional(v.string()) }))),
53
+ failed: v.optional(v.array(v.strictObject({ signal: v.string(), why: v.string() }))),
54
+ ...Object.fromEntries(Object.keys(STATUS_SIGNAL_ROWS).map((name) => [listKey(name.slice(7)), v.optional(v.array(statusItem))])),
55
+ });
56
+ const coverageOf = (found, chat) => found.items.find((one) => one.signal === "coverage" && one.chat === chat)?.data;
57
+ const partial = (found, chat) => {
58
+ const held = coverageOf(found, chat.ref);
59
+ return held ? held.state !== "complete" && held.beforeHeld : chat.coverage.completeness.state !== "complete";
60
+ };
61
+ const readFrom = (found, chat) => {
62
+ const reply = found.items.find((one) => one.signal === "reply-next" && one.chat === chat)?.data;
63
+ const money = found.items
64
+ .filter((one) => one.signal === "amounts" && one.chat === chat && one.data.state === "open")
65
+ .map((one) => one.data.since)
66
+ .sort()[0];
67
+ return day([money, reply?.waitingSince, found.window.since].find((one) => typeof one === "string"));
68
+ };
69
+ /** At most two: the chats `attention` weighs highest, a chat held in part synced first. */
70
+ const nextOf = (found, hints) => {
71
+ const byRef = new Map(found.chats.map((chat) => [chat.ref, chat]));
72
+ const top = found.attention.slice(0, 2).flatMap((one) => {
73
+ const chat = byRef.get(one.chat);
74
+ return chat ? [{ chat, why: one.why }] : [];
75
+ });
76
+ const because = (step, why) => (step ? [{ ...step, why }] : []);
77
+ const syncs = top.flatMap(({ chat, why }) => partial(found, chat) ? because(hints.sync(chat, day(found.window.since)), why) : []);
78
+ const reads = top.flatMap(({ chat, why }) => partial(found, chat) ? [] : because(hints.read(chat, readFrom(found, chat.ref)), why));
79
+ return [...syncs, ...reads].slice(0, 2);
80
+ };
81
+ const missingOf = (found) => {
82
+ const held = found.chats.filter((chat) => partial(found, chat));
83
+ const [first] = held;
84
+ return [
85
+ ...(first
86
+ ? [
87
+ {
88
+ field: "history",
89
+ why: `${held.length} of ${found.chats.length} chats hold only part of the window; ` +
90
+ `the first is ${heldPhrase(first.coverage)}`,
91
+ step: "messages.sync",
92
+ },
93
+ ]
94
+ : []),
95
+ ...(found.outOfScope.length
96
+ ? [
97
+ {
98
+ field: "chats",
99
+ why: `${found.outOfScope.length} linked chats were not read here (${[
100
+ ...new Set(found.outOfScope.map((one) => one.reason)),
101
+ ].join(", ")}); memo projects status reads every account`,
102
+ },
103
+ ]
104
+ : []),
105
+ ...found.failed.map((one) => ({ field: `signal:${one.signal}`, why: one.why })),
106
+ ];
107
+ };
108
+ const withPlace = (found, hints) => {
109
+ const titles = new Map(found.chats.map((chat) => [chat.ref, chat.title]));
110
+ const sync = (one) => {
111
+ const chat = found.chats.find((each) => each.ref === one.chat);
112
+ const step = chat && partial(found, chat) ? hints.sync(chat, day(found.window.since)) : null;
113
+ return step ? hints.line(step) : null;
114
+ };
115
+ return (one) => ({
116
+ ...one,
117
+ ref: one.evidence[0]?.ref ?? one.chat,
118
+ chatTitle: one.chat === null ? null : (titles.get(one.chat) ?? null),
119
+ ...(one.signal === "coverage" ? { syncCommand: sync(one) } : {}),
120
+ });
121
+ };
122
+ /** The answer of `projects status`: the item, two steps at most, what is missing, and the window. */
123
+ export const projectStatusAnswer = (found, hints) => {
124
+ const weights = new Map(found.attention.map((one) => [one.chat, one]));
125
+ const place = withPlace(found, hints);
126
+ const lists = Object.fromEntries(found.signals.flatMap(({ id }) => {
127
+ const items = found.items.filter((one) => one.signal === id).map(place);
128
+ return items.length ? [[listKey(id), items]] : [];
129
+ }));
130
+ const item = snakeKeys({
131
+ project: found.project.ref,
132
+ key: found.project.key,
133
+ name: found.project.name,
134
+ window: found.window,
135
+ signals: found.signals,
136
+ money: found.money.open,
137
+ chats: found.chats.map((chat) => ({
138
+ chat: chat.ref,
139
+ title: chat.title,
140
+ kind: chat.kind,
141
+ role: chat.role,
142
+ purpose: chat.purpose,
143
+ project: chat.project,
144
+ coverage: chat.coverage.completeness.state,
145
+ attention: weights.get(chat.ref)?.weight ?? 0,
146
+ why: weights.get(chat.ref)?.why ?? null,
147
+ })),
148
+ ...(found.outOfScope.length ? { outOfScope: found.outOfScope } : {}),
149
+ ...(found.failed.length ? { failed: found.failed } : {}),
150
+ ...lists,
151
+ });
152
+ const missing = missingOf(found);
153
+ const complete = found.failed.length === 0 && !found.chats.some((chat) => partial(found, chat));
154
+ return {
155
+ item,
156
+ next: nextOf(found, hints),
157
+ missing,
158
+ meta: { window: found.window, complete, ...(found.accounts.length ? { accounts: found.accounts } : {}) },
159
+ };
160
+ };
161
+ const NEXT_WORDS = { owner: "YOU", them: "them", nobody: "nobody" };
162
+ const chatLine = (found, chat) => {
163
+ const of = (signal) => found.items.filter((one) => one.signal === signal && one.chat === chat.ref);
164
+ const [last] = of("last-message");
165
+ const reply = of("reply-next")[0]?.data;
166
+ const head = [
167
+ `${chat.title ?? chat.ref} (${[chat.role ?? "no role", chat.kind].join(", ")})`,
168
+ last
169
+ ? `last ${day(last.at)} ${last.who?.owner ? "you" : (last.who?.name ?? "them")}`
170
+ : "nothing in the window",
171
+ ...(reply
172
+ ? [
173
+ `next: ${reply.next === "named" ? `${reply.addressed?.name ?? "someone"} (named)` : NEXT_WORDS[reply.next]}` +
174
+ (reply.waitingSince ? ` since ${day(reply.waitingSince)}` : ""),
175
+ ]
176
+ : []),
177
+ ...(partial(found, chat) ? [heldPhrase(chat.coverage)] : []),
178
+ ].join(" · ");
179
+ const open = [];
180
+ const asked = of("questions").map((one) => one.data);
181
+ const yours = asked.filter((one) => one.side === "yours").length;
182
+ const theirs = asked.length - yours;
183
+ if (theirs)
184
+ open.push(`${theirs} question${theirs === 1 ? "" : "s"} to you unanswered`);
185
+ if (yours)
186
+ open.push(`${yours} question${yours === 1 ? "" : "s"} of yours unanswered`);
187
+ for (const topic of of("topics")) {
188
+ const data = topic.data;
189
+ if (data.silentSince)
190
+ open.push(`"${data.query}" quiet on their side since ${day(data.silentSince)}`);
191
+ }
192
+ const promised = of("promises").length;
193
+ if (promised)
194
+ open.push(`${promised} promise${promised === 1 ? "" : "s"}`);
195
+ return [` ${head}`, ...(open.length ? [` open: ${open.join(" · ")}`] : [])];
196
+ };
197
+ const moneyLine = (one, titles) => {
198
+ const data = one.data;
199
+ const asks = data.asks > 1 && data.lastAsked ? ` (asked ${data.asks}×, last ${day(data.lastAsked)})` : "";
200
+ const where = one.chat === null ? "" : ` · ${titles.get(one.chat) ?? one.chat}`;
201
+ const words = data.direction.replace("_", " ");
202
+ const sum = `${data.amount} ${data.currency ?? "?"} ${data.state} since ${day(data.since)}`;
203
+ return ` ${words.padEnd(7)} ${sum}${asks}${where} · ${one.evidence[0]?.ref ?? ""} [${one.basis}]`;
204
+ };
205
+ /** The human form: header, money, a line per chat with what is open, then the steps. */
206
+ export const projectStatusText = (found, hints) => {
207
+ const titles = new Map(found.chats.map((chat) => [chat.ref, chat.title]));
208
+ const { next } = projectStatusAnswer(found, hints);
209
+ const money = found.items.filter((one) => one.signal === "amounts" && one.data.state !== "mentioned");
210
+ const payments = found.items.filter((one) => one.signal === "payments");
211
+ const children = found.items.filter((one) => one.signal === "children");
212
+ const key = found.project.key ? `${found.project.key} · ` : "";
213
+ const lines = [
214
+ `${key}${found.project.name} · ${found.chats.length} linked chats in ${found.accounts.length} accounts, ` +
215
+ `${day(found.window.since)} to ${day(found.window.until)}`,
216
+ ];
217
+ if (money.length || payments.length) {
218
+ lines.push("Money");
219
+ for (const one of money)
220
+ lines.push(moneyLine(one, titles));
221
+ for (const one of payments) {
222
+ const data = one.data;
223
+ lines.push(` payment ${data.title} · ${one.evidence[0]?.ref ?? ""} [${data.status}]`);
224
+ }
225
+ }
226
+ if (found.chats.length)
227
+ lines.push("Chats");
228
+ for (const chat of found.chats)
229
+ for (const line of chatLine(found, chat))
230
+ lines.push(line);
231
+ if (children.length)
232
+ lines.push("Children");
233
+ for (const one of children) {
234
+ const data = one.data;
235
+ const cut = one.data.cut;
236
+ lines.push(` ${cut ? `${data.child} not read (${cut})` : `${data.key ?? data.child} · ${data.name} · ${data.chats} chats`}`);
237
+ }
238
+ if (found.outOfScope.length)
239
+ lines.push(`Not read here: ${found.outOfScope.map((one) => `${one.chat} (${one.reason})`).join(", ")}`);
240
+ for (const one of found.failed)
241
+ lines.push(`Signal ${one.signal} failed: ${one.why}`);
242
+ if (next.length)
243
+ lines.push("Next");
244
+ next.forEach((step, index) => {
245
+ lines.push(` ${index + 1}. ${hints.line(step)}`);
246
+ });
247
+ return lines.join("\n");
248
+ };
package/dist/cli/steps.js CHANGED
@@ -48,7 +48,10 @@ export const STEPS = defineSteps([
48
48
  {
49
49
  id: "messages.list",
50
50
  command: "messages list",
51
- args: [{ name: "chat", required: true }],
51
+ args: [
52
+ { name: "chat", required: true },
53
+ { name: "query", required: false },
54
+ ],
52
55
  why: "only the newest were read here; this reads the chat itself",
53
56
  },
54
57
  {
@@ -1,4 +1,4 @@
1
- export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise"];
1
+ export { FACT_TYPES } from "../meetings/index.js";
2
2
  export declare const FACT_STATUSES: readonly ["proposed", "accepted", "superseded", "reversed", "rejected"];
3
3
  export declare const MEMORY_KINDS: readonly ["summary", "digest", "preference"];
4
4
  export declare const MEMORY_STATUSES: readonly ["proposed", "confirmed", "stale", "superseded"];
@@ -1,5 +1,5 @@
1
1
  import { TASK_KINDS } from "../tasks/index.js";
2
- export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise"];
2
+ export { FACT_TYPES } from "../meetings/index.js";
3
3
  export const FACT_STATUSES = ["proposed", "accepted", "superseded", "reversed", "rejected"];
4
4
  export const MEMORY_KINDS = ["summary", "digest", "preference"];
5
5
  export const MEMORY_STATUSES = ["proposed", "confirmed", "stale", "superseded"];
@@ -0,0 +1,3 @@
1
+ export declare const MONEY_DONE_FORMS: RegExp;
2
+ /** First person future: "I'll send", "сделаю", "lo envío". Labelled a guess wherever it is shown. */
3
+ export declare const PROMISE_FORMS: RegExp;