@wirecat/cli-core 0.548.0 → 0.549.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 (64) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/calendar/cli/index.d.ts +10 -1
  3. package/dist/calendar/cli/index.js +9 -3
  4. package/dist/calendar/cli/search-all.d.ts +17 -0
  5. package/dist/calendar/cli/search-all.js +72 -0
  6. package/dist/cli/answer-entity.d.ts +4 -0
  7. package/dist/cli/answer-entity.js +5 -0
  8. package/dist/cli/bot/context.d.ts +2 -0
  9. package/dist/cli/compact.d.ts +41 -0
  10. package/dist/cli/compact.js +313 -0
  11. package/dist/cli/context.d.ts +11 -2
  12. package/dist/cli/context.js +15 -6
  13. package/dist/cli/envelope.d.ts +5 -1
  14. package/dist/cli/envelope.js +15 -4
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +2 -0
  17. package/dist/cli/messenger/search-command.d.ts +10 -0
  18. package/dist/cli/messenger/search-command.js +19 -13
  19. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  20. package/dist/cli/program.d.ts +3 -0
  21. package/dist/cli/program.js +7 -1
  22. package/dist/domain/providers.d.ts +3 -0
  23. package/dist/domain/providers.js +20 -4
  24. package/dist/domain/ref-arguments.js +3 -7
  25. package/dist/domain/ref-summaries.d.ts +56 -0
  26. package/dist/domain/ref-summaries.js +63 -0
  27. package/dist/entities/index.d.ts +11 -0
  28. package/dist/entities/relationship.d.ts +2 -0
  29. package/dist/entities/relationship.js +2 -0
  30. package/dist/entities/task-results.d.ts +10 -0
  31. package/dist/entities/task-results.js +1 -0
  32. package/dist/errors/reasons.js +1 -0
  33. package/dist/output-detail.js +9 -5
  34. package/dist/output.d.ts +5 -1
  35. package/dist/output.js +29 -4
  36. package/dist/services/proposals/kinds/relationship-update.d.ts +4 -0
  37. package/dist/services/proposals/kinds/relationship-update.js +53 -0
  38. package/dist/services/proposals/registry.js +2 -0
  39. package/dist/store/decisions.d.ts +3 -1
  40. package/dist/store/decisions.js +7 -1
  41. package/dist/store/index.d.ts +2 -1
  42. package/dist/store/sqlite/evidence-sources.d.ts +2 -2
  43. package/dist/store/sqlite/evidence-sources.js +41 -77
  44. package/dist/store/sqlite/knowledge.d.ts +17 -23
  45. package/dist/store/sqlite/knowledge.js +13 -13
  46. package/dist/store/sqlite/manifest.js +2 -0
  47. package/dist/store/sqlite/migrations.generated.js +12 -0
  48. package/dist/store/sqlite/notes.d.ts +5 -0
  49. package/dist/store/sqlite/notes.js +6 -2
  50. package/dist/store/sqlite/open-store.js +2 -0
  51. package/dist/store/sqlite/proposed-actions.d.ts +1 -1
  52. package/dist/store/sqlite/proposed-actions.js +10 -2
  53. package/dist/store/sqlite/ref-summaries.d.ts +17 -0
  54. package/dist/store/sqlite/ref-summaries.js +378 -0
  55. package/dist/store/sqlite/relation-proposals.d.ts +40 -0
  56. package/dist/store/sqlite/relation-proposals.js +88 -1
  57. package/dist/store/sqlite/schema.d.ts +16 -0
  58. package/dist/store/sqlite/schema.js +1 -0
  59. package/dist/store/sqlite/suggested-links.d.ts +10 -0
  60. package/dist/store/sqlite/suggested-links.js +68 -0
  61. package/dist/store/store.d.ts +3 -0
  62. package/dist/store/store.js +5 -0
  63. package/package.json +3 -3
  64. package/parity.json +4 -0
@@ -0,0 +1,313 @@
1
+ import { visibleControls } from "@wirecat/cli-base";
2
+ import { metaOf } from "@wirecat/cli-base/commands";
3
+ import { ENTITIES } from "../entities/index.js";
4
+ import { answerRow } from "./answer-entity.js";
5
+ export const TEXT_CHARS = 400;
6
+ const sender = { field: ["sender_is_me", "sender_name"], tag: "me" };
7
+ const messageRow = {
8
+ columns: [
9
+ { field: "ref" },
10
+ { field: "timestamp", as: "time" },
11
+ { field: "chat_title" },
12
+ sender,
13
+ { field: ["reply_to_id", "reply_to.id"], as: "reply" },
14
+ { field: "forwarded_from", as: "forward" },
15
+ { field: "attachments", as: "files" },
16
+ ],
17
+ text: ["text", "transcript", "excerpt"],
18
+ };
19
+ /** Rows for cli-core's shared entities. */
20
+ export const SHARED_ROWS = {
21
+ message: messageRow,
22
+ "message.summary": messageRow,
23
+ "message.hit": {
24
+ columns: [
25
+ { field: "ref" },
26
+ { field: "timestamp", as: "time" },
27
+ { field: "chat_title" },
28
+ sender,
29
+ { field: "attachments", as: "files" },
30
+ ],
31
+ text: ["excerpt", "text"],
32
+ },
33
+ chat: {
34
+ columns: [
35
+ { field: "ref" },
36
+ { field: "title" },
37
+ { field: "kind" },
38
+ { field: "unread_count", as: "count", tag: "unread" },
39
+ { field: "last_message_at", as: "time" },
40
+ ],
41
+ },
42
+ };
43
+ /** Any record: what names it, what it is called, what state it is in, then what it says. */
44
+ export const GENERIC_ROW = {
45
+ columns: [
46
+ { field: ["key", "ref", "id"] },
47
+ { field: ["title", "name", "path"] },
48
+ { field: ["state", "status", "due_at"] },
49
+ ],
50
+ text: ["text", "excerpt"],
51
+ };
52
+ /** The row an entity named in a section is drawn with: the app's, the shared one, else the generic one. */
53
+ export const rowsOfEntity = (table = {}) => (entity) => (Object.hasOwn(table, entity) ? table[entity] : undefined) ?? SHARED_ROWS[entity] ?? GENERIC_ROW;
54
+ const declaredNames = (command) => {
55
+ const output = command ? metaOf(command).output : undefined;
56
+ if (!output)
57
+ return [];
58
+ return output.view ? [`${output.entity}.${output.view}`, output.entity] : [output.entity];
59
+ };
60
+ /** An app's own rows by entity name, as `entityFields` takes its fields; a shared name is refused. */
61
+ export const entityRows = (table) => {
62
+ const shared = Object.keys(table).filter((name) => name in ENTITIES || name in SHARED_ROWS);
63
+ if (shared.length > 0)
64
+ throw new Error(`entityRows: ${shared.join(", ")} is cli-core's entity; name yours apart`);
65
+ return (command) => {
66
+ const name = declaredNames(command).find((one) => Object.hasOwn(table, one));
67
+ return name === undefined ? undefined : table[name];
68
+ };
69
+ };
70
+ /** The row a command's answer is drawn with: its own, its app's, the shared one, else the generic one. */
71
+ export const rowShapeOf = (command, resolve) => {
72
+ const own = answerRow(command) ?? resolve?.(command);
73
+ if (own)
74
+ return own;
75
+ const shared = declaredNames(command).find((name) => Object.hasOwn(SHARED_ROWS, name));
76
+ return shared === undefined ? GENERIC_ROW : SHARED_ROWS[shared];
77
+ };
78
+ const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
79
+ const present = (value) => value !== null && value !== undefined && value !== "" && value !== false && !(Array.isArray(value) && !value.length);
80
+ const at = (item, path) => path.split(".").reduce((value, key) => (isFields(value) ? value[key] : undefined), item);
81
+ const fieldsOfColumn = (field) => (typeof field === "string" ? [field] : field);
82
+ // A record or a list is not one value; a file list is the only column that reads one, by its own path.
83
+ const printable = (value, column) => present(value) &&
84
+ (column?.as === "files" || column?.as === "forward" || column?.as === "reply" || !isListOrRecord(value));
85
+ const isListOrRecord = (value) => typeof value === "object" && value !== null;
86
+ const firstSet = (item, field, column) => fieldsOfColumn(field)
87
+ .map((path) => at(item, path))
88
+ .find((value) => printable(value, column));
89
+ const sourceOf = (item, column) => fieldsOfColumn(column.field).find((path) => printable(at(item, path), column));
90
+ // Whitespace runs become one space and control or direction characters show, so a text cannot start a row of its own.
91
+ const clean = (text) => visibleControls(text.replace(/\s+/g, " ").trim());
92
+ const cut = (text, chars) => {
93
+ if (chars <= 0)
94
+ return text;
95
+ const letters = Array.from(text);
96
+ return letters.length <= chars ? text : `${letters.slice(0, chars).join("")}…(+${letters.length - chars})`;
97
+ };
98
+ const clockFor = (zone) => {
99
+ const format = new Intl.DateTimeFormat("en-CA", {
100
+ timeZone: zone,
101
+ year: "numeric",
102
+ month: "2-digit",
103
+ day: "2-digit",
104
+ hour: "2-digit",
105
+ minute: "2-digit",
106
+ hourCycle: "h23",
107
+ });
108
+ return {
109
+ zone,
110
+ parts: (iso) => {
111
+ const ms = Date.parse(iso);
112
+ if (Number.isNaN(ms))
113
+ return undefined;
114
+ const part = Object.fromEntries(format.formatToParts(ms).map(({ type, value }) => [type, value]));
115
+ return { date: `${part.year}-${part.month}-${part.day}`, time: `${part.hour}:${part.minute}` };
116
+ },
117
+ };
118
+ };
119
+ const duration = (seconds) => {
120
+ const whole = Math.round(seconds);
121
+ return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
122
+ };
123
+ const fileTag = (file) => {
124
+ if (!isFields(file))
125
+ return undefined;
126
+ const kind = typeof file.kind === "string" && file.kind !== "" ? file.kind : "file";
127
+ const extra = typeof file.duration === "number" ? duration(file.duration) : typeof file.name === "string" ? file.name : "";
128
+ return `[${clean(extra ? `${kind} ${extra}` : kind)}]`;
129
+ };
130
+ const cellOf = (item, column, clock) => {
131
+ const value = firstSet(item, column.field, column);
132
+ if (!present(value))
133
+ return undefined;
134
+ switch (column.as) {
135
+ case "time":
136
+ case "day": {
137
+ if (typeof value !== "string")
138
+ return undefined;
139
+ const parts = clock.parts(value);
140
+ if (!parts)
141
+ return { text: clean(value) };
142
+ const day = parts.date.slice(5);
143
+ return { text: column.as === "day" ? day : `${day} ${parts.time}`, date: parts.date };
144
+ }
145
+ case "count":
146
+ return typeof value === "number" && value !== 0
147
+ ? { text: column.tag ? `${value} ${column.tag}` : String(value) }
148
+ : undefined;
149
+ case "files": {
150
+ const tags = (Array.isArray(value) ? value : [value]).map(fileTag).filter((tag) => tag !== undefined);
151
+ return tags.length ? { text: tags.join(" ") } : undefined;
152
+ }
153
+ case "reply": {
154
+ const id = isFields(value) ? value.id : value;
155
+ return typeof id === "string" || typeof id === "number" ? { text: `↳#${clean(String(id))}` } : undefined;
156
+ }
157
+ case "forward": {
158
+ const name = isFields(value) ? value.sender_name : value;
159
+ return { text: typeof name === "string" && name !== "" ? `↪ ${clean(name)}` : "↪" };
160
+ }
161
+ default:
162
+ if (value === true)
163
+ return column.tag ? { text: column.tag } : undefined;
164
+ if (typeof value === "string" || typeof value === "number")
165
+ return { text: clean(String(value)) };
166
+ return undefined;
167
+ }
168
+ };
169
+ const textOf = (item, shape, chars) => {
170
+ const value = (shape.text ?? [])
171
+ .map((field) => at(item, field))
172
+ .find((one) => typeof one === "string" && present(one));
173
+ return typeof value === "string" ? cut(clean(value), chars) : undefined;
174
+ };
175
+ // A line that starts with "# " is the answer's own; a row is shifted so that no item can pass for one.
176
+ const guarded = (line) => (line.startsWith("#") && !/^#[^\s#]/.test(line) ? `\\${line}` : line);
177
+ const REF = /^[a-z]+:\S*\/[^/\s]+$/;
178
+ /** The shared part of the refs up to their last `/`, when every row has one and there is more than one row. */
179
+ const sharedPrefix = (refs) => {
180
+ if (refs.length < 2 || !refs.every((ref) => ref !== undefined && REF.test(ref)))
181
+ return undefined;
182
+ const prefixes = new Set(refs.map((ref) => ref.slice(0, ref.lastIndexOf("/") + 1)));
183
+ return prefixes.size === 1 ? [...prefixes][0] : undefined;
184
+ };
185
+ const span = (dates) => {
186
+ if (!dates.length)
187
+ return undefined;
188
+ const sorted = [...dates].sort();
189
+ const first = sorted[0];
190
+ const last = sorted[sorted.length - 1];
191
+ return first === last ? first : `${first}…${last}`;
192
+ };
193
+ // A column every row prints the same, a chat's title in a list of its messages, is said once in the header.
194
+ const sameEverywhere = (rows, index) => {
195
+ const first = rows[0]?.cells[index]?.text;
196
+ return first !== undefined && rows.every(({ cells }) => cells[index]?.text === first);
197
+ };
198
+ const rowsOfItems = (items, shape, clock, chars) => {
199
+ const dates = [];
200
+ const rows = items.map((item) => {
201
+ if (!isFields(item))
202
+ return { cells: [], text: clean(String(item)) };
203
+ const cells = shape.columns.map((column) => cellOf(item, column, clock));
204
+ for (const cell of cells)
205
+ if (cell?.date)
206
+ dates.push(cell.date);
207
+ return { cells, text: textOf(item, shape, chars), item };
208
+ });
209
+ const firstColumn = shape.columns[0];
210
+ const refs = rows.map(({ item }) => item && firstColumn ? firstSet(item, firstColumn.field) : undefined);
211
+ const prefix = sharedPrefix(refs.map((ref) => (typeof ref === "string" ? ref : undefined)));
212
+ const hoisted = rows.length < 2 ? [] : shape.columns.map((_, index) => index > 0 && sameEverywhere(rows, index));
213
+ const named = hoisted.flatMap((hoist, index) => {
214
+ const column = shape.columns[index];
215
+ const item = rows[0]?.item;
216
+ if (!hoist || !column || !item)
217
+ return [];
218
+ return [`${sourceOf(item, column) ?? fieldsOfColumn(column.field)[0]}: ${rows[0]?.cells[index]?.text}`];
219
+ });
220
+ const lines = rows.map(({ cells, text, item }) => {
221
+ const parts = cells.map((cell, index) => (hoisted[index] ? undefined : cell?.text));
222
+ const shortened = prefix ? clean(prefix) : undefined;
223
+ if (shortened && parts[0]?.startsWith(shortened))
224
+ parts[0] = `#${parts[0].slice(shortened.length)}`;
225
+ const shown = [...parts, text].filter((part) => part !== undefined && part !== "");
226
+ return guarded(shown.length ? shown.join(" · ") : item ? blockLine(item, chars) : "");
227
+ });
228
+ const when = span(dates);
229
+ const header = [...(prefix ? [`ref: ${clean(prefix)}<id>`] : []), ...named, ...(when ? [when, clock.zone] : [])];
230
+ return { header, lines };
231
+ };
232
+ const blockLine = (fields, chars) => blockOf(fields, chars).join(" ");
233
+ /** A record as `field: value` lines; a nested record is indented under its field, a list of records counted. */
234
+ const blockOf = (fields, chars, indent = "") => Object.entries(fields)
235
+ .filter(([name]) => !(name === "priority" && "priority_label" in fields))
236
+ .filter(([, value]) => present(value) || value === false)
237
+ .flatMap(([name, value]) => {
238
+ const label = `${indent}${clean(name)}:`;
239
+ if (isFields(value))
240
+ return [label, ...blockOf(value, chars, `${indent} `)];
241
+ if (Array.isArray(value))
242
+ return value.every((one) => !isFields(one) && !Array.isArray(one))
243
+ ? [`${label} ${cut(clean(value.join(", ")), chars)}`]
244
+ : [`${label} ${value.length} — --json shows them`];
245
+ return [`${label} ${cut(clean(String(value)), chars)}`];
246
+ });
247
+ const footerOf = (body) => {
248
+ const lines = [];
249
+ const steps = body.next ?? [];
250
+ const lineOf = (step) => (step.kind === "command" ? (step.line ?? step.command) : undefined);
251
+ if ("items" in body && body.page.has_more) {
252
+ const more = steps.find((step) => step.step === "page.next");
253
+ const line = more?.kind === "command" ? more.line : undefined;
254
+ const page = body.page;
255
+ lines.push(`# more: ${clean(line ??
256
+ (page.mode === "cursor" && page.next_cursor
257
+ ? `--cursor ${page.next_cursor}`
258
+ : page.mode === "page" && page.next_page
259
+ ? `--page ${page.next_page}`
260
+ : "raise --limit"))}`);
261
+ }
262
+ for (const missing of body.missing ?? [])
263
+ lines.push(`# missing: ${clean(missing.field)}: ${clean(missing.why)}`);
264
+ for (const step of steps) {
265
+ if (step.step === "page.next" && "items" in body)
266
+ continue;
267
+ const line = lineOf(step);
268
+ lines.push(`# next: ${clean(step.why)}${line ? `: ${clean(line)}` : ""}`);
269
+ }
270
+ if (body.meta?.complete === false) {
271
+ const issues = (body.meta.issues ?? []).map((issue) => `${clean(issue.source)}: ${clean(String(issue.code))}`);
272
+ lines.push(`# complete: false${issues.length ? ` (${issues.join(", ")})` : ""}`);
273
+ }
274
+ if (body.meta?.truncated)
275
+ lines.push("# truncated: true");
276
+ return lines;
277
+ };
278
+ const usableZone = (zone) => {
279
+ if (!zone)
280
+ return undefined;
281
+ try {
282
+ new Intl.DateTimeFormat("en", { timeZone: zone });
283
+ return zone;
284
+ }
285
+ catch {
286
+ return undefined;
287
+ }
288
+ };
289
+ /** The whole answer as compact text: header, rows, footer. Pure: no store, no clock. */
290
+ export const compactText = (body, shape, options, rowsOf = rowsOfEntity()) => {
291
+ const clock = clockFor(usableZone(body.meta?.timezone) ?? usableZone(options.timezone) ?? "UTC");
292
+ const chars = options.textChars ?? TEXT_CHARS;
293
+ let lines = [];
294
+ if ("items" in body) {
295
+ const { header, lines: rows } = rowsOfItems(body.items, shape, clock, chars);
296
+ lines = (header.length ? [`# ${header.join(" · ")}`] : []).concat(rows.length ? rows : ["# nothing found"]);
297
+ }
298
+ else if (isFields(body.item)) {
299
+ const sections = shape.sections ?? {};
300
+ const rest = Object.fromEntries(Object.entries(body.item).filter(([name]) => !Object.hasOwn(sections, name)));
301
+ lines = blockOf(rest, chars).map(guarded);
302
+ for (const [name, entity] of Object.entries(sections)) {
303
+ const list = body.item[name];
304
+ if (!Array.isArray(list))
305
+ continue;
306
+ const { header, lines: rows } = rowsOfItems(list, rowsOf(entity), clock, chars);
307
+ lines = lines.concat([`# ${[`${clean(name)}: ${list.length}`, ...header].join(" · ")}`], rows);
308
+ }
309
+ }
310
+ else if (present(body.item))
311
+ lines = [guarded(cut(clean(String(body.item)), chars))];
312
+ return lines.concat(footerOf(body)).join("\n");
313
+ };
@@ -1,6 +1,6 @@
1
1
  import { type KeyringStore, type Renderer, type RenderFormat, type Streams } from "@wirecat/cli-base";
2
- import type { ErrorObject } from "@wirecat/cli-base/standards";
3
- import type { Command } from "commander";
2
+ import { type ErrorObject } from "@wirecat/cli-base/standards";
3
+ import { type Command } from "commander";
4
4
  import { type Deadline, type RunBudget } from "../background/budget.js";
5
5
  import type { MessageEvent } from "../domain/models.js";
6
6
  import type { AppIdentity } from "../ports/app.js";
@@ -13,6 +13,8 @@ import { type Closeable } from "./deadline.js";
13
13
  import { type GlobalFlags, type ResolveOptions, type Settings } from "./settings.js";
14
14
  /** What every CLI's commands write to, when it is not the real terminal. A CLI adds its own fields. */
15
15
  export interface BaseEnvironment {
16
+ /** The app's own compact rows (`ProgramDefinition.rows`), for `renderAnswer`. */
17
+ rows?: import("./compact.js").RowTable;
16
18
  discoveryAvailability?: (command: import("./command-availability.js").AvailabilityCommand) => import("./command-availability.js").CommandAvailability | undefined;
17
19
  streams?: Streams;
18
20
  /** Whether a person is looking. Defaults to whether stdout is a terminal. */
@@ -121,12 +123,16 @@ export declare const environmentOf: <E extends BaseEnvironment = BaseEnvironment
121
123
  * output goes and a test that captures it sees it. `readSecret(prompt, { ...secretInputOf(this), echo: true })`.
122
124
  */
123
125
  export declare const secretInputOf: (command: Command) => Pick<SecretInput, "input" | "output">;
126
+ /** Whether the run asked for one line per item (WC-OUT-24). */
127
+ export declare const compactOf: (command: Command) => boolean;
124
128
  /** For the commands that print but never need settings or a connection. */
125
129
  export declare const outputFor: (command: Command) => {
126
130
  format: "json" | "jsonl" | "pretty";
127
131
  color: boolean;
128
132
  streams: Streams;
129
133
  renderer: Renderer;
134
+ compact?: boolean | undefined;
135
+ full?: boolean | undefined;
130
136
  };
131
137
  export interface BaseContext {
132
138
  settings: Settings;
@@ -135,6 +141,9 @@ export interface BaseContext {
135
141
  /** Whether the human view may use colour — for views that print their own text. */
136
142
  color: boolean;
137
143
  streams: Streams;
144
+ /** One line per item; `renderAnswer` draws it, `full` keeps each text whole. */
145
+ compact?: boolean;
146
+ full?: boolean;
138
147
  env: NodeJS.ProcessEnv;
139
148
  keyring?: KeyringStore;
140
149
  /**
@@ -1,5 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { metaOf } from "@wirecat/cli-base/commands";
3
+ import { COMPACT_FLAG } from "@wirecat/cli-base/standards";
4
+ import { Option } from "commander";
3
5
  import { runBudget } from "../background/budget.js";
4
6
  import { resolveOutput } from "../output.js";
5
7
  import { recorded } from "../runs/recording.js";
@@ -43,22 +45,27 @@ export const secretInputOf = (command) => {
43
45
  const { stdin, stderr } = environmentOf(command);
44
46
  return { ...(stdin ? { input: stdin } : {}), ...(stderr ? { output: stderr } : {}) };
45
47
  };
48
+ const detailOf = ({ brief, full }) => {
49
+ if (brief && full)
50
+ throw new CliError("validation_error", "--brief and --full ask for opposite things — pick one");
51
+ return brief ? { hitDetail: "brief" } : full ? { hitDetail: "full" } : {};
52
+ };
53
+ const COMPACT_KEY = new Option(COMPACT_FLAG).attributeName();
54
+ /** Whether the run asked for one line per item (WC-OUT-24). */
55
+ export const compactOf = (command) => command.optsWithGlobals()[COMPACT_KEY] === true;
46
56
  /** For the commands that print but never need settings or a connection. */
47
57
  export const outputFor = (command) => {
48
58
  const { streams, tty, env, agent } = environmentOf(command);
49
59
  return resolveOutput({
50
60
  ...command.optsWithGlobals(),
61
+ ...detailOf(command.optsWithGlobals()),
62
+ compact: compactOf(command),
51
63
  ...(agent ? { capture: agent.result } : {}),
52
64
  ...(streams ? { streams } : {}),
53
65
  ...(env ? { env } : {}),
54
66
  ...(tty === undefined ? {} : { tty }),
55
67
  });
56
68
  };
57
- const detailOf = ({ brief, full }) => {
58
- if (brief && full)
59
- throw new CliError("validation_error", "--brief and --full ask for opposite things — pick one");
60
- return brief ? { hitDetail: "brief" } : full ? { hitDetail: "full" } : {};
61
- };
62
69
  /** Everything a command needs that no messenger decides, resolved once. */
63
70
  export const baseContext = (command, resolveSettings) => {
64
71
  const environment = environmentOf(command);
@@ -66,9 +73,10 @@ export const baseContext = (command, resolveSettings) => {
66
73
  const flags = command.optsWithGlobals();
67
74
  const overrides = environment.agent?.overrides ?? [];
68
75
  const settings = resolveSettings(overrides.length > 0 ? { ...flags, permission: [...overrides, ...(flags.permission ?? [])] } : flags, { env });
69
- const { renderer, format, color, streams } = resolveOutput({
76
+ const { renderer, format, color, streams, compact, full } = resolveOutput({
70
77
  ...settings,
71
78
  env,
79
+ compact: compactOf(command),
72
80
  ...detailOf(command.optsWithGlobals()),
73
81
  ...(command.optsWithGlobals().fields
74
82
  ? { fields: command.optsWithGlobals().fields }
@@ -85,6 +93,7 @@ export const baseContext = (command, resolveSettings) => {
85
93
  format,
86
94
  color,
87
95
  streams,
96
+ ...(compact ? { compact, full: full === true } : {}),
88
97
  env,
89
98
  ...(environment.keyring ? { keyring: environment.keyring } : {}),
90
99
  // The deadline inside the record, so a timeout finishes the run as failed rather than leaving it running.
@@ -193,6 +193,10 @@ export interface AnswerOutput {
193
193
  renderer: Renderer;
194
194
  format: RenderFormat;
195
195
  streams: Streams;
196
+ /** One line per item from the same answer (WC-OUT-24); an agent's run keeps JSON. */
197
+ compact?: boolean;
198
+ /** With `compact`: each text whole, not cut. */
199
+ full?: boolean;
196
200
  }
197
201
  /** An answer as JSON-lines records, for a writer that streams objects: one per item, then the end record. */
198
202
  export declare const jsonlRecords: <T>(body: Body<T>) => object[];
@@ -201,7 +205,7 @@ export declare const jsonlRecords: <T>(body: Body<T>) => object[];
201
205
  * the items — `view` draws them — and each next step this CLI has, as a line on stderr. A partial answer the
202
206
  * command does not declare `partial` exits with its first issue's code, after printing what was read.
203
207
  */
204
- export declare const renderAnswer: <T>(command: Command, { renderer, format, streams }: AnswerOutput, body: Body<T>, view?: (body: Body<T>) => string) => void;
208
+ export declare const renderAnswer: <T>(command: Command, { renderer, format, streams, compact, full }: AnswerOutput, body: Body<T>, view?: (body: Body<T>) => string) => void;
205
209
  /**
206
210
  * A document that is not an answer about the owner's data — the command registry, a `--dry-run` preview of what a
207
211
  * command would do — printed whole, with no envelope.
@@ -1,8 +1,9 @@
1
1
  import { exitCodeOfAnswer } from "@wirecat/cli-base";
2
2
  import { metaOf } from "@wirecat/cli-base/commands";
3
- import { itemEnvelopeSchema, jsonlOf, listEnvelopeSchema, MAX_NEXT_STEPS, } from "@wirecat/cli-base/standards";
3
+ import { COMPACT_FLAG, itemEnvelopeSchema, jsonlOf, listEnvelopeSchema, MAX_NEXT_STEPS, } from "@wirecat/cli-base/standards";
4
4
  import * as v from "valibot";
5
5
  import { currentRun } from "../runs/scope.js";
6
+ import { compactText, entityRows, rowShapeOf, rowsOfEntity, TEXT_CHARS } from "./compact.js";
6
7
  import { environmentOf, setExitCode } from "./context.js";
7
8
  import { withoutProviderRefs } from "./provider-refs.js";
8
9
  import { nextStep, rootOf, runnable, stepLine, withLines, withSafety } from "./steps.js";
@@ -128,6 +129,8 @@ export const sealed = (value) => {
128
129
  const { meta, ...rest } = withoutProviderRefs(value);
129
130
  return { ...rest, meta: metaNow(meta) };
130
131
  };
132
+ /** The steps a compact answer prints, each rerunning in the compact form the caller chose. */
133
+ const compactSteps = (root, steps) => steps.map((step) => step.kind === "command" ? { ...step, line: `${step.line ?? stepLine(root, step)} ${COMPACT_FLAG}` } : step);
131
134
  /** An answer as JSON lines: one line per item, then the end line. One thing is a list of one that has no more. */
132
135
  const linesOf = (body) => jsonlOf("items" in body ? body : { items: [body.item], page: { mode: "page", limit: 1, has_more: false }, meta: body.meta });
133
136
  /** An answer as JSON-lines records, for a writer that streams objects: one per item, then the end record. */
@@ -139,7 +142,7 @@ export const jsonlRecords = (body) => linesOf(sealed(body))
139
142
  * the items — `view` draws them — and each next step this CLI has, as a line on stderr. A partial answer the
140
143
  * command does not declare `partial` exits with its first issue's code, after printing what was read.
141
144
  */
142
- export const renderAnswer = (command, { renderer, format, streams }, body, view) => {
145
+ export const renderAnswer = (command, { renderer, format, streams, compact, full }, body, view) => {
143
146
  if (!isBody(body))
144
147
  refuse(body);
145
148
  const root = rootOf(command);
@@ -149,7 +152,15 @@ export const renderAnswer = (command, { renderer, format, streams }, body, view)
149
152
  if (next.length === 0)
150
153
  delete kept.next;
151
154
  const whole = sealed(kept);
152
- if (format === "json")
155
+ const environment = environmentOf(command);
156
+ const drawn = compact && !environment.agent;
157
+ if (drawn) {
158
+ const lined = whole.next ? { ...whole, next: compactSteps(root, whole.next) } : whole;
159
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
160
+ const { rows } = environment;
161
+ streams.data(compactText(lined, rowShapeOf(command, rows && entityRows(rows)), { timezone, textChars: full ? 0 : TEXT_CHARS }, rowsOfEntity(rows)));
162
+ }
163
+ else if (format === "json")
153
164
  renderer.result(whole);
154
165
  else if (format === "jsonl")
155
166
  for (const line of linesOf(whole).split("\n"))
@@ -161,7 +172,7 @@ export const renderAnswer = (command, { renderer, format, streams }, body, view)
161
172
  const code = exitCodeOfAnswer(whole.meta, metaOf(command).partial === true);
162
173
  if (code !== 0)
163
174
  setExitCode(command, code);
164
- if (format === "json")
175
+ if (drawn || format === "json")
165
176
  return;
166
177
  for (const step of next)
167
178
  renderer.note(step.kind === "command" ? `→ ${step.why}: ${stepLine(root, step)}` : `→ ${step.why}`);
@@ -1,5 +1,6 @@
1
1
  export { AI_ENTRIES, AI_SETTING_KEYS, type AISettings, resolveAISettings } from "../analysis/settings.js";
2
2
  export { parseDuration } from "../domain/duration.js";
3
+ export { openOf, PROFILE_PLACEHOLDER, type ProfilesFor, type RefOpen, type RefSummary, refLabel, } from "../domain/ref-summaries.js";
3
4
  export { botTemplate } from "../entities/list-results.js";
4
5
  export { syncProgress } from "../entities/store-records.js";
5
6
  export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
@@ -58,6 +59,7 @@ export type { CommandAvailability, DiscoveredCommand } from "./command-availabil
58
59
  export { commandContract, type JsonSchema, SCHEMA_DIALECT } from "./command-contract.js";
59
60
  export { type CommandRunOptions, type CommandRuns, commandRuns, type ProviderAnswer, type ProviderCall, providerEvent, providerRequest, type RunSettings, recordRuns, runSettings, type SavedRunSettings, } from "./command-runs.js";
60
61
  export { CONTRACT } from "./commands-command.js";
62
+ export { type CompactOptions, compactText, entityRows, GENERIC_ROW, type RowColumn, type RowShape, type RowsResolver, type RowTable, type RowValue, rowShapeOf, rowsOfEntity, SHARED_ROWS, } from "./compact.js";
61
63
  export { refuseUnknownKey } from "./config-command.js";
62
64
  export { type ConfigIssue, type ConfigIssueKind, checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
63
65
  export { loadSnakeCaseConfig } from "./config-keys.js";
package/dist/cli/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AI_ENTRIES, AI_SETTING_KEYS, resolveAISettings } from "../analysis/settings.js";
2
2
  export { parseDuration } from "../domain/duration.js";
3
+ export { openOf, PROFILE_PLACEHOLDER, refLabel, } from "../domain/ref-summaries.js";
3
4
  export { botTemplate } from "../entities/list-results.js";
4
5
  export { syncProgress } from "../entities/store-records.js";
5
6
  export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
@@ -50,6 +51,7 @@ export { PressLog, UpdatesCursor } from "./bot/updates.js";
50
51
  export { commandContract, SCHEMA_DIALECT } from "./command-contract.js";
51
52
  export { commandRuns, providerEvent, providerRequest, recordRuns, runSettings, } from "./command-runs.js";
52
53
  export { CONTRACT } from "./commands-command.js";
54
+ export { compactText, entityRows, GENERIC_ROW, rowShapeOf, rowsOfEntity, SHARED_ROWS, } from "./compact.js";
53
55
  export { refuseUnknownKey } from "./config-command.js";
54
56
  export { checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
55
57
  export { loadSnakeCaseConfig } from "./config-keys.js";
@@ -1,5 +1,15 @@
1
1
  import { Command } from "commander";
2
+ import type { SearchAllIncludingMeetingsFound } from "../../services/search-all-meetings.js";
2
3
  import { type Messenger } from "./context.js";
4
+ import { type SourceSearched } from "./search-answers.js";
5
+ export declare const searchAllOnlyOf: (value: string) => SourceSearched[];
6
+ /** One `search all` hit as a person reads it. */
7
+ export declare const searchAllLine: (item: SearchAllIncludingMeetingsFound["items"][number]) => string;
8
+ /**
9
+ * `search all`'s scope, paging and matching options, the same in every CLI that mounts it (WC-ACCT-8): a messenger's
10
+ * and a calendar's.
11
+ */
12
+ export declare const searchAllOptions: (command: Command) => Command;
3
13
  /**
4
14
  * `search <resource>`: every search of the tool, one leaf per resource (STANDARD.md, "Search
5
15
  * hierarchy"). `topics` only where the CLI has forum topics.
@@ -15,7 +15,7 @@ import { messagesSearchCommand } from "./messages-search-command.js";
15
15
  import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, shownOf, } from "./search-answers.js";
16
16
  import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
17
17
  import { topicsSearchCommand } from "./topics-command.js";
18
- const onlyOf = (value) => {
18
+ export const searchAllOnlyOf = (value) => {
19
19
  const chosen = value.split(",").map((one) => one.trim());
20
20
  if (chosen.includes("mail"))
21
21
  throw new CliError("validation_error", "mail is searched by the separate memo CLI: install @wirecat/cli-memo, then `memo search mail …`");
@@ -24,7 +24,8 @@ const onlyOf = (value) => {
24
24
  throw new CliError("validation_error", `--only takes ${SOURCE_SEARCHED.join(", ")}, separated by commas; meetings when --source names a meeting account`);
25
25
  return [...new Set(chosen)];
26
26
  };
27
- const lineOf = (item) => item.kind === "meeting"
27
+ /** One `search all` hit as a person reads it. */
28
+ export const searchAllLine = (item) => item.kind === "meeting"
28
29
  ? `meeting · ${item.provider} · ${singleLine(item.title ?? "")} ${item.ref}\n ${item.timestamp ?? "time unknown"} ${singleLine(item.text).slice(0, 200)}\n`
29
30
  : `${item.kind} · ${item.provider} · ${singleLine(item.title ?? "")} ${item.ref}${item.url ? `\n ${singleLine(item.url)}` : ""}\n ${item.timestamp}${item.from?.name ? ` ${singleLine(item.from.name)}` : ""} ${singleLine(item.text)}\n`;
30
31
  const maxMeetingsOf = (value) => {
@@ -34,6 +35,19 @@ const maxMeetingsOf = (value) => {
34
35
  throw new CliError("validation_error", "--max-meetings takes a whole number from 1, or all");
35
36
  return Number(value);
36
37
  };
38
+ /**
39
+ * `search all`'s scope, paging and matching options, the same in every CLI that mounts it (WC-ACCT-8): a messenger's
40
+ * and a calendar's.
41
+ */
42
+ export const searchAllOptions = (command) => command
43
+ .option("--only <resources>", "only these resources, separated by commas: messages, notes, events; meetings when --source names a meeting account", searchAllOnlyOf)
44
+ .option("--source <accounts>", "only these accounts, separated by commas: provider:account as a hit shows it, a provider, a kind or " +
45
+ "group:<name>; each resource reads the ones of its kind, and meetings only when it names one")
46
+ .option("--max-meetings <n>", "how many of the meeting account's meetings to look through, newest first, or all for every one (default 100)", maxMeetingsOf)
47
+ .option("--limit <n>", "how many", positiveCount("--limit"))
48
+ .option("--page <n>", "which page, starting at 1", positiveCount("--page"))
49
+ .option("--exact", "bare words and quotes match their exact form only, as exact:word does")
50
+ .option("--timezone <zone>", "the IANA timezone for calendar date boundaries");
37
51
  const allCommand = (messenger) => {
38
52
  const command = annotate(new Command("all"), { effects: REMOTE_READ, partial: true })
39
53
  .summary("search stored messages, notes, calendar events and selected meeting transcripts")
@@ -48,16 +62,8 @@ const allCommand = (messenger) => {
48
62
  .argument("<query...>", 'strict Lucene query: words, "phrases", AND/OR/NOT, field groups and date ranges')
49
63
  .addHelpText("after", SAVE_HELP)
50
64
  .addHelpText("after", `transcript: narrows a message search to voice transcripts, content: to the text of files, filename: to their names — \`${messenger.app.command} search all 'content:invoice'\`.
51
- --json: the list is items; each result's reference is ref, with its kind.`)
52
- .option("--only <resources>", "only these resources, separated by commas: messages, notes, events; meetings when --source names a meeting account", onlyOf)
53
- .option("--source <accounts>", "only these accounts, separated by commas: provider:account as a hit shows it, a provider, a kind or " +
54
- "group:<name>; each resource reads the ones of its kind, and meetings only when it names one")
55
- .option("--max-meetings <n>", "how many of the meeting account's meetings to look through, newest first, or all for every one (default 100)", maxMeetingsOf)
56
- .option("--limit <n>", "how many", positiveCount("--limit"))
57
- .option("--page <n>", "which page, starting at 1", positiveCount("--page"))
58
- .option("--exact", "bare words and quotes match their exact form only, as exact:word does")
59
- .option("--timezone <zone>", "the IANA timezone for calendar date boundaries");
60
- return backendOptions(annotate(command, { output: { entity: "hit" } }), messenger).action(async function (words) {
65
+ --json: the list is items; each result's reference is ref, with its kind.`);
66
+ return backendOptions(annotate(searchAllOptions(command), { output: { entity: "hit" } }), messenger).action(async function (words) {
61
67
  const context = messengerContext(this, messenger);
62
68
  const { only, source, maxMeetings, limit: typed, page, exact, timezone, } = this.opts();
63
69
  const limit = typed ?? context.settings.limit;
@@ -99,7 +105,7 @@ const allCommand = (messenger) => {
99
105
  context.renderer.note(found.server && !found.server.skipped
100
106
  ? "nothing found in the local store or on the messenger's server"
101
107
  : "nothing found in what the local store holds");
102
- return found.items.map(lineOf).join("");
108
+ return found.items.map(searchAllLine).join("");
103
109
  });
104
110
  });
105
111
  };
@@ -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;
7
6
  path: string;
8
7
  exists: boolean;
8
+ error?: undefined;
9
9
  } | {
10
10
  path: string;
11
11
  exists: boolean;
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { type AppIdentity } from "../ports/app.js";
3
+ import { type RowTable } from "./compact.js";
3
4
  import { type BaseEnvironment } from "./context.js";
4
5
  import { type FirstScreen } from "./help/first-screen.js";
5
6
  import type { CommandPermissions } from "./permission-keys.js";
@@ -16,6 +17,8 @@ export interface ProgramDefinition {
16
17
  resolveSettings: (flags?: GlobalFlags, options?: ResolveOptions) => Pick<Settings, "profile" | "keepFailedRuns" | "keepRunsForDays" | "skillHint"> & Partial<Pick<Settings, "recordInput" | "keepSuccessfulRunsForDays" | "permissions" | "permissionSources" | "configPath" | "savedPermissions" | "sources">>;
17
18
  };
18
19
  configure?: (program: Command) => void;
20
+ /** The app's own rows for `--compact`, by entity name; a shared entity's name is refused. */
21
+ rows?: RowTable;
19
22
  /** The tool's pitch and "what you can do" lines on `--help` and a bare run. */
20
23
  firstScreen?: FirstScreen;
21
24
  prepare?: (program: Command, environment: RunOptions) => void | Promise<void>;