things-api 0.7.0 → 0.9.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 (154) hide show
  1. package/README.md +6 -3
  2. package/dist/cli/commands/area.d.ts +23 -0
  3. package/dist/cli/commands/area.js +164 -33
  4. package/dist/cli/commands/area.js.map +1 -1
  5. package/dist/cli/commands/project.d.ts +19 -0
  6. package/dist/cli/commands/project.js +142 -29
  7. package/dist/cli/commands/project.js.map +1 -1
  8. package/dist/cli/commands/reads.d.ts +10 -51
  9. package/dist/cli/commands/reads.js +454 -268
  10. package/dist/cli/commands/reads.js.map +1 -1
  11. package/dist/cli/commands/show.d.ts +9 -0
  12. package/dist/cli/commands/show.js +169 -0
  13. package/dist/cli/commands/show.js.map +1 -0
  14. package/dist/cli/commands/snapshot.js +1 -1
  15. package/dist/cli/commands/snapshot.js.map +1 -1
  16. package/dist/cli/commands/todo.d.ts +9 -0
  17. package/dist/cli/commands/todo.js +101 -16
  18. package/dist/cli/commands/todo.js.map +1 -1
  19. package/dist/cli/commands/writes.js +55 -1
  20. package/dist/cli/commands/writes.js.map +1 -1
  21. package/dist/cli/did-you-mean.d.ts +29 -0
  22. package/dist/cli/did-you-mean.js +53 -0
  23. package/dist/cli/did-you-mean.js.map +1 -0
  24. package/dist/cli/glyphs.d.ts +95 -0
  25. package/dist/cli/glyphs.js +282 -0
  26. package/dist/cli/glyphs.js.map +1 -0
  27. package/dist/cli/main.js +11 -1
  28. package/dist/cli/main.js.map +1 -1
  29. package/dist/cli/period.d.ts +32 -0
  30. package/dist/cli/period.js +129 -0
  31. package/dist/cli/period.js.map +1 -0
  32. package/dist/cli/read-driver.d.ts +76 -0
  33. package/dist/cli/read-driver.js +181 -0
  34. package/dist/cli/read-driver.js.map +1 -0
  35. package/dist/cli/render.d.ts +182 -0
  36. package/dist/cli/render.js +585 -0
  37. package/dist/cli/render.js.map +1 -0
  38. package/dist/cli/resolve-invocation.d.ts +78 -0
  39. package/dist/cli/resolve-invocation.js +178 -0
  40. package/dist/cli/resolve-invocation.js.map +1 -0
  41. package/dist/cli/shell-quote.d.ts +9 -0
  42. package/dist/cli/shell-quote.js +12 -0
  43. package/dist/cli/shell-quote.js.map +1 -0
  44. package/dist/cli/style.d.ts +5 -0
  45. package/dist/cli/style.js +5 -0
  46. package/dist/cli/style.js.map +1 -1
  47. package/dist/client.d.ts +45 -39
  48. package/dist/client.js +11 -74
  49. package/dist/client.js.map +1 -1
  50. package/dist/contracts.d.ts +66 -1
  51. package/dist/contracts.js +1 -1
  52. package/dist/contracts.js.map +1 -1
  53. package/dist/db/baselines/db-v26.js +7 -5
  54. package/dist/db/baselines/db-v26.js.map +1 -1
  55. package/dist/db/schema.d.ts +1 -1
  56. package/dist/db/schema.js +7 -1
  57. package/dist/db/schema.js.map +1 -1
  58. package/dist/mcp/server.js +227 -44
  59. package/dist/mcp/server.js.map +1 -1
  60. package/dist/model/entities.d.ts +21 -0
  61. package/dist/model/entities.js.map +1 -1
  62. package/dist/model/mappers.js +17 -1
  63. package/dist/model/mappers.js.map +1 -1
  64. package/dist/model/occurrences.d.ts +6 -1
  65. package/dist/model/occurrences.js +9 -2
  66. package/dist/model/occurrences.js.map +1 -1
  67. package/dist/model/recurrence.d.ts +30 -2
  68. package/dist/model/recurrence.js +20 -0
  69. package/dist/model/recurrence.js.map +1 -1
  70. package/dist/read/area-view.js +21 -5
  71. package/dist/read/area-view.js.map +1 -1
  72. package/dist/read/caps.d.ts +14 -0
  73. package/dist/read/caps.js +21 -0
  74. package/dist/read/caps.js.map +1 -0
  75. package/dist/read/detail.js +12 -2
  76. package/dist/read/detail.js.map +1 -1
  77. package/dist/read/log-boundary.d.ts +29 -0
  78. package/dist/read/log-boundary.js +38 -0
  79. package/dist/read/log-boundary.js.map +1 -0
  80. package/dist/read/pagination.d.ts +104 -0
  81. package/dist/read/pagination.js +208 -0
  82. package/dist/read/pagination.js.map +1 -0
  83. package/dist/read/predicates.d.ts +31 -0
  84. package/dist/read/predicates.js +48 -0
  85. package/dist/read/predicates.js.map +1 -0
  86. package/dist/read/project-view.js +28 -3
  87. package/dist/read/project-view.js.map +1 -1
  88. package/dist/read/queries.d.ts +21 -3
  89. package/dist/read/queries.js +35 -8
  90. package/dist/read/queries.js.map +1 -1
  91. package/dist/read/search-rank.d.ts +36 -0
  92. package/dist/read/search-rank.js +31 -0
  93. package/dist/read/search-rank.js.map +1 -0
  94. package/dist/read/show-target.d.ts +25 -0
  95. package/dist/read/show-target.js +41 -0
  96. package/dist/read/show-target.js.map +1 -0
  97. package/dist/read/sidebar-order.d.ts +19 -0
  98. package/dist/read/sidebar-order.js +92 -0
  99. package/dist/read/sidebar-order.js.map +1 -0
  100. package/dist/read/snapshot.js +2 -0
  101. package/dist/read/snapshot.js.map +1 -1
  102. package/dist/read/views.d.ts +64 -8
  103. package/dist/read/views.js +290 -155
  104. package/dist/read/views.js.map +1 -1
  105. package/dist/surface-copy.d.ts +23 -0
  106. package/dist/surface-copy.js +23 -0
  107. package/dist/surface-copy.js.map +1 -1
  108. package/dist/write/automation-probe.js +14 -6
  109. package/dist/write/automation-probe.js.map +1 -1
  110. package/dist/write/capabilities.d.ts +3 -0
  111. package/dist/write/capabilities.js +3 -0
  112. package/dist/write/capabilities.js.map +1 -1
  113. package/dist/write/checklist.d.ts +50 -0
  114. package/dist/write/checklist.js +61 -0
  115. package/dist/write/checklist.js.map +1 -0
  116. package/dist/write/clear-reminder.d.ts +3 -0
  117. package/dist/write/clear-reminder.js +205 -0
  118. package/dist/write/clear-reminder.js.map +1 -0
  119. package/dist/write/commands.js +122 -8
  120. package/dist/write/commands.js.map +1 -1
  121. package/dist/write/edit-checklist.d.ts +3 -0
  122. package/dist/write/edit-checklist.js +184 -0
  123. package/dist/write/edit-checklist.js.map +1 -0
  124. package/dist/write/failure-hints.d.ts +7 -0
  125. package/dist/write/failure-hints.js +25 -2
  126. package/dist/write/failure-hints.js.map +1 -1
  127. package/dist/write/guards.d.ts +1 -1
  128. package/dist/write/guards.js +16 -0
  129. package/dist/write/guards.js.map +1 -1
  130. package/dist/write/operations.d.ts +32 -1
  131. package/dist/write/operations.js +3 -0
  132. package/dist/write/operations.js.map +1 -1
  133. package/dist/write/pipeline.d.ts +3 -0
  134. package/dist/write/pipeline.js +23 -1
  135. package/dist/write/pipeline.js.map +1 -1
  136. package/dist/write/reversibility.d.ts +46 -0
  137. package/dist/write/reversibility.js +164 -0
  138. package/dist/write/reversibility.js.map +1 -0
  139. package/dist/write/undo.d.ts +29 -2
  140. package/dist/write/undo.js +357 -9
  141. package/dist/write/undo.js.map +1 -1
  142. package/dist/write/vectors/registry.js +2 -1
  143. package/dist/write/vectors/registry.js.map +1 -1
  144. package/dist/write/vectors/shortcuts.d.ts +9 -0
  145. package/dist/write/vectors/shortcuts.js +90 -0
  146. package/dist/write/vectors/shortcuts.js.map +1 -0
  147. package/dist/write/vectors/types.d.ts +11 -3
  148. package/dist/write/vectors/url-scheme.js +16 -0
  149. package/dist/write/vectors/url-scheme.js.map +1 -1
  150. package/dist/write/verify/delta.d.ts +1 -1
  151. package/dist/write/verify/delta.js +1 -1
  152. package/dist/write/verify/delta.js.map +1 -1
  153. package/package.json +1 -1
  154. package/shortcuts/things-proxy-find-items.shortcut +0 -0
@@ -1,299 +1,441 @@
1
- import { openThings } from "../../client.js";
2
- import { ThingsDbNotFoundError } from "../../db/locate.js";
3
- import { ThingsDbOpenError } from "../../db/connection.js";
4
- import { isTodayMember } from "../../read/views.js";
5
- import { blue, bold, dim, magenta, red, underline, yellow } from "../style.js";
6
- import { errorEnvelope, ExitCode, okEnvelope } from "../../contracts.js";
7
- export function withClient(opts, kind, fn, render) {
8
- const started = Date.now();
9
- let client = null;
10
- try {
11
- client = openThings(opts.db ? { dbPath: opts.db } : {});
12
- const fp = client.fingerprint();
13
- const data = fn(client);
14
- const meta = {
15
- dbVersion: fp.observation.databaseVersion,
16
- fingerprint: fp.kind === "ok" ? "ok" : fp.kind === "drift" ? "drift" : "unknown",
17
- elapsedMs: Date.now() - started,
18
- };
19
- if (fp.kind !== "ok") {
20
- process.stderr.write(`warning: schema fingerprint ${meta.fingerprint} — reads best-effort, writes disabled (run \`things doctor\`)\n`);
21
- }
22
- if (opts.json) {
23
- process.stdout.write(`${JSON.stringify(okEnvelope(kind, data, meta))}\n`);
24
- }
25
- else {
26
- process.stdout.write(`${render(data).join("\n")}\n`);
27
- }
28
- process.exitCode = ExitCode.Ok;
29
- }
30
- catch (err) {
31
- const meta = {
32
- dbVersion: null,
33
- fingerprint: "unknown",
34
- elapsedMs: Date.now() - started,
35
- };
36
- const isEnv = err instanceof ThingsDbNotFoundError || err instanceof ThingsDbOpenError;
37
- const message = err instanceof Error ? err.message : String(err);
38
- if (opts.json) {
39
- process.stdout.write(`${JSON.stringify(errorEnvelope({ code: isEnv ? "environment" : "unexpected", message }, meta))}\n`);
40
- }
41
- else {
42
- process.stderr.write(`error: ${message}\n`);
43
- }
44
- process.exitCode = isEnv ? ExitCode.Environment : ExitCode.Unexpected;
45
- }
46
- finally {
47
- client?.close();
48
- }
49
- }
50
1
  /**
51
- * One item line: `<uuid-prefix> <marker> [meta …] <title> #tags (container)`.
52
- * Human output shows a SHORTENED uuid prefix (every command accepts unique
53
- * prefixes >= 6 chars); `uuidWidth` is the display length from
54
- * uuidDisplayWidth never below 8 so a copied prefix stays unique across
55
- * the whole database, not just the rendered list. Tags follow the title
56
- * (`#`-prefixed, space-separated), mirroring the GUI. Exactly one space
57
- * separates every following token; meta tokens colorize on a TTY only
58
- * (../style.ts) — piped output stays plain. `--json` always carries full
59
- * uuids.
2
+ * Read-only list commands: option/description wiring per view, delegating to
3
+ * the read driver (../read-driver.ts) for envelope/output and to the pure
4
+ * renderers (../render.ts) for human lines. Each command renders a compact
5
+ * human table (UUIDs always shown agents and humans both need stable
6
+ * references) or a --json envelope.
60
7
  */
61
- export function formatItem(item, uuidWidth = 0, opts = {}) {
62
- const asTitle = opts.projectTitle === true && item.type === "project";
63
- const marker = asTitle
64
- ? null
65
- : (item.type === "project" ? "P" : "-") + (item.repeating.isTemplate ? "↻" : "");
66
- const meta = [
67
- item.deadline ? red(`!${item.deadline}`) : null,
68
- item.startDate ? yellow(`@${item.startDate}`) : null,
69
- item.status !== "open" ? magenta(`[${item.status}]`) : null,
70
- ].filter((s) => s !== null);
71
- const tags = item.tags.length > 0 ? ` ${blue(`#${item.tags.map((t) => t.title).join(" #")}`)}` : "";
72
- const context = item.type === "to-do" && item.project
73
- ? item.project.uuid === opts.suppressProject
74
- ? ""
75
- : ` (${item.project.title})`
76
- : item.area
77
- ? item.area.uuid === opts.suppressArea
78
- ? ""
79
- : ` (${item.area.title})`
80
- : "";
81
- const shownUuid = uuidWidth > 0 && item.uuid.length > uuidWidth
82
- ? item.uuid.slice(0, uuidWidth)
83
- : item.uuid.padEnd(uuidWidth);
84
- const title = asTitle ? bold(underline(item.title)) : item.title;
85
- return [
86
- `${dim(shownUuid)} `,
87
- ...(marker === null ? [] : [marker]),
88
- ...meta,
89
- `${title}${context === "" ? "" : dim(context)}${tags}`,
90
- ].join(" ");
91
- }
92
- /** Minimum displayed-prefix length: shorter prefixes collide across the DB. */
93
- const UUID_DISPLAY_MIN = 8;
8
+ import { Option } from "commander";
9
+ import { execFileSync } from "node:child_process";
10
+ import { localToday } from "../../model/dates.js";
11
+ import { dim } from "../style.js";
12
+ import { areaMark, LEGEND, shortDate } from "../glyphs.js";
13
+ import { formatItem, renderAnytimePreview, renderLegend, renderList, renderLogbook, renderProjectsSidebar, renderSearch, renderSections, renderSomedayPreview, renderToday, renderUpcoming, stripAnsi, uuidCol, uuidDisplayWidth, } from "../render.js";
14
+ import { invocation, parseCap, parseLimit, runRead, shellQuote, withClient, } from "../read-driver.js";
15
+ import { parsePeriodEnd, parsePeriodStart } from "../period.js";
16
+ import { ExitCode, okEnvelope } from "../../contracts.js";
17
+ import { AREA_PREVIEW_LIMIT, PROJECT_PREVIEW_LIMIT, paginateList, paginateToday, partitionSomedaySection, previewSections, previewSomedaySections, } from "../../read/pagination.js";
18
+ import { ALL_DESC, AREA_LIMIT_DESC, GROUPED_ALL_DESC, LIMIT_DESC, PERIOD_SINCE, PERIOD_UNTIL, PROJECT_LIMIT_DESC, } from "../../surface-copy.js";
94
19
  /**
95
- * Display width for a list's uuid column: the shortest prefix that is
96
- * unique WITHIN the list, floored at UUID_DISPLAY_MIN (list-local
97
- * uniqueness at 2–3 chars would still collide database-wide).
20
+ * Foreground the Things app on a resource via its share URI. A GUI action
21
+ * on this Mac — NOT headless; the shared implementation behind every
22
+ * `open` command. Returns the URI it launched.
98
23
  */
99
- export function uuidDisplayWidth(items) {
100
- if (items.length === 0)
101
- return UUID_DISPLAY_MIN;
102
- const sorted = items.map((i) => i.uuid).toSorted();
103
- let needed = 1;
104
- for (let i = 1; i < sorted.length; i++) {
105
- const a = sorted[i - 1] ?? "";
106
- const b = sorted[i] ?? "";
107
- let common = 0;
108
- while (common < a.length && common < b.length && a[common] === b[common])
109
- common++;
110
- needed = Math.max(needed, common + 1);
111
- }
112
- return Math.max(UUID_DISPLAY_MIN, needed);
24
+ export function openInThings(uuid) {
25
+ const uri = `things:///show?id=${uuid}`;
26
+ execFileSync("/usr/bin/open", [uri]);
27
+ return uri;
113
28
  }
114
- function renderList(items) {
115
- const w = uuidDisplayWidth(items);
116
- return items.length === 0 ? ["(empty)"] : items.map((i) => formatItem(i, w));
29
+ function registerListView(program, spec) {
30
+ program
31
+ .command(spec.name)
32
+ .description(spec.description)
33
+ .option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
34
+ .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
35
+ .option("--limit <n>", LIMIT_DESC)
36
+ .option("--all", ALL_DESC)
37
+ .option("--json", "emit versioned JSON envelope on stdout")
38
+ .option("--db <path>", "explicit database path")
39
+ .action((opts) => {
40
+ const lim = parseLimit(opts);
41
+ if (!lim.ok)
42
+ return;
43
+ const base = invocation(spec.name, [
44
+ opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
45
+ opts.exactTag === true && "--exact-tag",
46
+ ]);
47
+ runRead(opts, spec.name, (c) => spec.paginate(spec.fetch(c, opts.tag, opts.exactTag), lim.limit), spec.render, base, spec.name);
48
+ });
117
49
  }
118
- /**
119
- * Sidebar-grouped views (anytime/someday), rendered the way the GUI reads:
120
- * the area-less block headerless first, then one `── <area> ──` header per
121
- * area; inside a section, loose to-dos first, then each project GROUP — a
122
- * blank line, the project's bold+underlined title row, then its members.
123
- * Container names implied by the grouping are not repeated on member rows
124
- * (an area header covers its rows; a project title row covers the to-dos
125
- * beneath it — a clustered child whose project row is absent, e.g. under a
126
- * tag filter, keeps its `(project)` suffix). `star` prefixes each item line
127
- * with the Today-membership star.
128
- */
129
- export function renderSections(sections, star = false) {
130
- const all = sections.flatMap((s) => s.items);
131
- if (all.length === 0)
132
- return ["(empty)"];
133
- const w = uuidDisplayWidth(all);
134
- const lines = [];
135
- const blank = () => {
136
- if (lines.length > 0 && lines.at(-1) !== "")
137
- lines.push("");
138
- };
139
- for (const section of sections) {
140
- if (section.area !== null) {
141
- blank();
142
- lines.push(bold(`── ${section.area.title} ──`));
50
+ export function registerReadCommands(program) {
51
+ program
52
+ .command("legend")
53
+ .description("The symbols and colors the list views use, grouped and explained: to-do boxes and " +
54
+ "project circles, markers and chips, colors and styles, section dividers and hints. " +
55
+ "Human-readable by default; --json emits {glyph, meaning, group} rows.")
56
+ .option("--json", "emit versioned JSON envelope on stdout")
57
+ .action((opts) => {
58
+ const started = Date.now();
59
+ if (opts.json) {
60
+ const entries = LEGEND.map((e) => ({
61
+ glyph: stripAnsi(e.glyph),
62
+ meaning: e.meaning,
63
+ group: e.group,
64
+ }));
65
+ const meta = {
66
+ dbVersion: null,
67
+ fingerprint: "unknown",
68
+ elapsedMs: Date.now() - started,
69
+ };
70
+ process.stdout.write(`${JSON.stringify(okEnvelope("legend", entries, meta))}\n`);
143
71
  }
144
- // The uuid of the project whose title row is directly above (its member
145
- // rows drop their redundant `(project)` suffix).
146
- let openProject = null;
147
- for (const item of section.items) {
148
- const prefix = star ? `${isTodayMember(item) ? yellow("★") : " "} ` : "";
149
- if (item.type === "project") {
150
- openProject = item.uuid;
151
- blank();
152
- lines.push(`${prefix}${formatItem(item, w, { projectTitle: true, suppressArea: section.area?.uuid ?? null })}`);
153
- }
154
- else {
155
- lines.push(`${prefix}${formatItem(item, w, {
156
- suppressProject: openProject,
157
- suppressArea: section.area?.uuid ?? null,
158
- })}`);
159
- }
72
+ else {
73
+ process.stdout.write(`${renderLegend().join("\n")}\n`);
160
74
  }
161
- }
162
- return lines;
163
- }
164
- export function registerReadCommands(program) {
165
- const listCommands = [
166
- {
167
- name: "today",
168
- description: "The Today list, split into Today and This Evening (evening expires daily), with the sidebar badge split (red = deadline due/overdue)",
169
- fetch: (c, tag, exactTag) => c.read.today(tag === undefined ? undefined : { tag, ...(exactTag === true && { exactTag }) }),
170
- render: (data) => [
171
- `── Today (badge: ${data.badge.dueOrOverdue} due/overdue · ${data.badge.other} other) ──`,
172
- ...renderList(data.today),
173
- "── This Evening ──",
174
- ...renderList(data.evening),
175
- ],
176
- },
177
- {
178
- name: "inbox",
179
- description: "Unprocessed captures (Inbox)",
180
- fetch: (c, tag, exactTag) => c.read.inbox(tag === undefined ? undefined : { tag, ...(exactTag === true && { exactTag }) }),
181
- },
182
- {
183
- name: "anytime",
184
- description: "All active items in the UI's sidebar-mirroring order (area-less first, then per " +
185
- "area: direct to-dos, then each project with its members). Today members are " +
186
- "starred (★). Children of someday/future-scheduled projects are excluded the " +
187
- "project row represents them",
188
- fetch: (c, tag, exactTag) => c.read.anytime(tag === undefined ? undefined : { tag, ...(exactTag === true && { exactTag }) }),
189
- render: (sections) => renderSections(sections, true),
190
- },
191
- {
192
- name: "someday",
193
- description: "Someday items (incubated, undated) in sidebar order. Project children are " +
194
- "represented by their project row; --active-project-items also lists someday " +
195
- "to-dos inside active projects (the UI's 'Show items from active projects' toggle)",
196
- fetch: (c, tag, exactTag, activeProjectItems) => c.read.someday({
197
- ...(tag !== undefined && { tag }),
198
- ...(exactTag === true && { exactTag }),
199
- ...(activeProjectItems === true && { activeProjectItems }),
200
- }),
201
- render: (sections) => renderSections(sections),
202
- extraOption: [
203
- "--active-project-items",
204
- "also list someday to-dos inside active projects",
205
- "activeProjectItems",
206
- ],
207
- },
208
- ];
209
- for (const cmd of listCommands) {
210
- const command = program
211
- .command(cmd.name)
212
- .description(cmd.description)
213
- .option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
214
- .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
215
- .option("--json", "emit versioned JSON envelope on stdout")
216
- .option("--db <path>", "explicit database path");
217
- if (cmd.extraOption)
218
- command.option(cmd.extraOption[0], cmd.extraOption[1]);
219
- command.action((opts) => {
220
- const extra = cmd.extraOption ? opts[cmd.extraOption[2]] === true : undefined;
221
- withClient(opts, cmd.name, (c) => cmd.fetch(c, opts.tag, opts.exactTag, extra), (cmd.render ?? renderList));
222
- });
223
- }
75
+ process.exitCode = ExitCode.Ok;
76
+ });
77
+ program
78
+ .command("today")
79
+ .description("The Today list, split into Today and This Evening (evening expires daily), with the sidebar badge split (red = deadline due/overdue)")
80
+ .option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
81
+ .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
82
+ .option("--limit <n>", LIMIT_DESC)
83
+ .option("--all", ALL_DESC)
84
+ .option("--json", "emit versioned JSON envelope on stdout")
85
+ .option("--db <path>", "explicit database path")
86
+ .action((opts) => {
87
+ const lim = parseLimit(opts);
88
+ if (!lim.ok)
89
+ return;
90
+ const base = invocation("today", [
91
+ opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
92
+ opts.exactTag === true && "--exact-tag",
93
+ ]);
94
+ runRead(opts, "today", (c) => {
95
+ const full = c.read.today(opts.tag === undefined
96
+ ? undefined
97
+ : { tag: opts.tag, ...(opts.exactTag === true && { exactTag: true }) });
98
+ const { data, pagination } = paginateToday(full, lim.limit);
99
+ // The renderer needs the PRE-cap view to keep This Evening honest
100
+ // under truncation, so the lines are precomputed here; the global
101
+ // footer (whole-view remainder) is still appended by the driver.
102
+ return { data, pagination, lines: renderToday(full, data, base) };
103
+ },
104
+ // Type-correct fallback for the TodayView payload; never reached
105
+ // because `lines` is always precomputed above.
106
+ (data) => renderToday(data, data, base), base, "today");
107
+ });
108
+ registerListView(program, {
109
+ name: "inbox",
110
+ description: "Unprocessed captures (Inbox)",
111
+ fetch: (c, tag, exactTag) => c.read.inbox(tag === undefined ? undefined : { tag, ...(exactTag === true && { exactTag }) }),
112
+ render: renderList,
113
+ paginate: paginateList,
114
+ });
115
+ program
116
+ .command("anytime")
117
+ .description("All active items in the UI's sidebar-mirroring order (area-less first, then per " +
118
+ "area: direct to-dos, then each project with its members). Today members are " +
119
+ "starred (★). Children of someday/future-scheduled projects are excluded — the " +
120
+ "project row represents them. Every group and project row is always shown; " +
121
+ "--area-limit caps each area's direct list, --project-limit each project's list, " +
122
+ "--all shows everything")
123
+ .option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
124
+ .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
125
+ .option("--area-limit <n>", AREA_LIMIT_DESC)
126
+ .option("--project-limit <n>", PROJECT_LIMIT_DESC)
127
+ .option("--all", GROUPED_ALL_DESC)
128
+ .addOption(new Option("--limit <n>").hideHelp())
129
+ .option("--json", "emit versioned JSON envelope on stdout")
130
+ .option("--db <path>", "explicit database path")
131
+ .action((opts) => {
132
+ if (opts.limit !== undefined) {
133
+ process.stderr.write("error: --limit is not available on anytime — cap blocks with --area-limit / --project-limit, or pass --all\n");
134
+ process.exitCode = ExitCode.Usage;
135
+ return;
136
+ }
137
+ const area = parseCap("--area-limit", opts.areaLimit, AREA_PREVIEW_LIMIT, opts.all === true);
138
+ if (!area.ok)
139
+ return;
140
+ const project = parseCap("--project-limit", opts.projectLimit, PROJECT_PREVIEW_LIMIT, opts.all === true);
141
+ if (!project.ok)
142
+ return;
143
+ const limits = { area: area.limit, project: project.limit };
144
+ const base = invocation("anytime", [
145
+ opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
146
+ opts.exactTag === true && "--exact-tag",
147
+ ]);
148
+ runRead(opts, "anytime", (c) => {
149
+ const full = c.read.anytime(opts.tag === undefined
150
+ ? undefined
151
+ : { tag: opts.tag, ...(opts.exactTag === true && { exactTag: true }) });
152
+ const { data, grouped } = previewSections(full, limits);
153
+ return { data, grouped, lines: renderAnytimePreview(full, limits, base) };
154
+ },
155
+ // Grouped views hand back precomputed `lines`; renderSections is the
156
+ // type-correct fallback for the SidebarSection[] payload but is never
157
+ // reached here.
158
+ renderSections, undefined, "anytime");
159
+ });
160
+ program
161
+ .command("someday")
162
+ .description("Someday items (incubated, undated) in sidebar order — inside each group the " +
163
+ "someday projects list first, then the to-dos; project children are represented " +
164
+ "by their project row. --show-active-project-items [n] appends a trailing section " +
165
+ "of someday to-dos inside active projects, grouped under their project (the UI's " +
166
+ "'Show items from active projects' toggle; n caps each project's list). " +
167
+ "--area-limit caps each group, --all shows everything")
168
+ .option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
169
+ .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
170
+ .option("--area-limit <n>", AREA_LIMIT_DESC)
171
+ .option("--show-active-project-items [n]", "append someday to-dos inside active projects, grouped under their project; " +
172
+ "n caps each project's list (bare flag: every item)")
173
+ .option("--all", GROUPED_ALL_DESC)
174
+ .addOption(new Option("--limit <n>").hideHelp())
175
+ .option("--json", "emit versioned JSON envelope on stdout")
176
+ .option("--db <path>", "explicit database path")
177
+ .action((opts) => {
178
+ if (opts.limit !== undefined) {
179
+ process.stderr.write("error: --limit is not available on someday — cap groups with --area-limit, or pass --all\n");
180
+ process.exitCode = ExitCode.Usage;
181
+ return;
182
+ }
183
+ const showActive = opts.showActiveProjectItems !== undefined;
184
+ let projectCap = null;
185
+ if (typeof opts.showActiveProjectItems === "string") {
186
+ const capped = parseCap("--show-active-project-items", opts.showActiveProjectItems, 0, opts.all === true);
187
+ if (!capped.ok)
188
+ return;
189
+ projectCap = capped.limit;
190
+ }
191
+ const area = parseCap("--area-limit", opts.areaLimit, AREA_PREVIEW_LIMIT, opts.all === true);
192
+ if (!area.ok)
193
+ return;
194
+ const limits = { area: area.limit, project: projectCap };
195
+ const filter = {
196
+ ...(opts.tag !== undefined && { tag: opts.tag }),
197
+ ...(opts.exactTag === true && { exactTag: true }),
198
+ };
199
+ const base = invocation("someday", [
200
+ opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
201
+ opts.exactTag === true && "--exact-tag",
202
+ ]);
203
+ runRead(opts, "someday", (c) => {
204
+ const full = c.read.someday({
205
+ ...filter,
206
+ ...(showActive && { activeProjectItems: true }),
207
+ });
208
+ // Hidden-items-never-silent: when the toggle is off, one extra
209
+ // query counts what it would reveal so the hint can say so.
210
+ const hiddenActiveItems = showActive
211
+ ? 0
212
+ : c.read
213
+ .someday({ ...filter, activeProjectItems: true })
214
+ .reduce((n, s) => n +
215
+ partitionSomedaySection(s).children.reduce((m, g) => m + g.items.length, 0), 0);
216
+ const { data, grouped } = previewSomedaySections(full, limits);
217
+ return {
218
+ data,
219
+ grouped,
220
+ lines: renderSomedayPreview(full, limits, base, showActive, hiddenActiveItems),
221
+ };
222
+ },
223
+ // Precomputed lines above; renderSections is the type-correct
224
+ // SidebarSection[] fallback, never reached here.
225
+ renderSections, undefined, "someday");
226
+ });
224
227
  program
225
228
  .command("upcoming")
226
229
  .description("Future-scheduled items in date order, INCLUDING each repeating item's next " +
227
- "occurrence (↻ marker; deadline derived from the repeat rule). --horizon <n> also " +
230
+ "occurrence (↻ marker; deadline derived from the repeat rule). Shows the next " +
231
+ "month by default — widen with --until (relative `2w`/`3m`/`1y` or absolute " +
232
+ "`2026-09`/`2026`) or --all for the full horizon. --horizon <n> also " +
228
233
  "PROJECTS the following n-1 occurrences per repeating item from its decoded rule " +
229
234
  "(fixed rules only, max 10) — projections are host math the app has not " +
230
235
  "materialized yet.")
236
+ .option("--until <period>", `only items scheduled through this bound: ${PERIOD_UNTIL} (whole periods)`, "1m")
237
+ .option("--since <period>", `skip items scheduled before this bound: ${PERIOD_SINCE}`)
238
+ .option("--all", "no date bound and no row limit — the app's full Upcoming")
239
+ .option("--limit <n>", LIMIT_DESC)
231
240
  .option("--tag <ref>", "filter by tag (uuid or unique name): direct, inherited, or descendant-tagged")
232
241
  .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
233
242
  .option("--horizon <n>", "occurrences per repeating item (default 1 = UI parity)")
234
243
  .option("--json", "emit versioned JSON envelope on stdout")
235
244
  .option("--db <path>", "explicit database path")
236
- .action((opts) => {
237
- withClient(opts, "upcoming", (c) => c.read.upcoming({
245
+ .action((opts, command) => {
246
+ const untilGiven = command.getOptionValueSource("until") !== "default";
247
+ const sinceGiven = opts.since !== undefined;
248
+ if (opts.all === true && (untilGiven || sinceGiven)) {
249
+ process.stderr.write("error: --all does not combine with --until/--since\n");
250
+ process.exitCode = ExitCode.Usage;
251
+ return;
252
+ }
253
+ const lim = parseLimit(opts);
254
+ if (!lim.ok)
255
+ return;
256
+ const untilDate = opts.all === true ? undefined : parsePeriodEnd(opts.until);
257
+ if (untilDate !== undefined && Number.isNaN(untilDate.getTime())) {
258
+ process.stderr.write(`error: --until is not a parseable period: ${opts.until}\n`);
259
+ process.exitCode = ExitCode.Usage;
260
+ return;
261
+ }
262
+ const sinceDate = sinceGiven ? parsePeriodStart(opts.since) : undefined;
263
+ if (sinceDate !== undefined && Number.isNaN(sinceDate.getTime())) {
264
+ process.stderr.write(`error: --since is not a parseable period: ${opts.since}\n`);
265
+ process.exitCode = ExitCode.Usage;
266
+ return;
267
+ }
268
+ const until = untilDate === undefined ? undefined : localToday(untilDate);
269
+ const since = sinceDate === undefined ? undefined : localToday(sinceDate);
270
+ const base = invocation("upcoming", [
271
+ untilGiven && `--until ${shellQuote(opts.until)}`,
272
+ sinceGiven && `--since ${shellQuote(opts.since)}`,
273
+ opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
274
+ opts.exactTag === true && "--exact-tag",
275
+ opts.horizon !== undefined && `--horizon ${shellQuote(opts.horizon)}`,
276
+ ]);
277
+ runRead(opts, "upcoming", (c) => paginateList(c.read.upcoming({
278
+ ...(until !== undefined && { until }),
279
+ ...(since !== undefined && { since }),
238
280
  ...(opts.tag !== undefined && { tag: opts.tag }),
239
281
  ...(opts.exactTag === true && { exactTag: true }),
240
282
  ...(opts.horizon !== undefined && { horizon: Number(opts.horizon) }),
241
- }), renderList);
283
+ }), lim.limit), (items) => {
284
+ const lines = renderUpcoming(items);
285
+ if (until !== undefined) {
286
+ lines.push("", dim(`(through ${shortDate(until, localToday())} — --all for the full horizon)`));
287
+ }
288
+ return lines;
289
+ }, base, "upcoming");
290
+ });
291
+ program
292
+ .command("logbook")
293
+ .description("Completed and canceled items, most recent first, grouped under month headings " +
294
+ "(year appended beyond the current year). Scope with --area (direct items + its " +
295
+ "projects' children, heading-nested included) / --project (all children, " +
296
+ "heading-nested included) / --tag; bound the logged date with --since/--until.")
297
+ .option("--limit <n>", LIMIT_DESC)
298
+ .option("--all", ALL_DESC)
299
+ .option("--area <ref>", "restrict to an area: direct items plus its projects' children")
300
+ .option("--project <ref>", "restrict to one project's children (uuid or unique name)")
301
+ .option("--since <when>", `only entries logged on/after this bound: ${PERIOD_SINCE}`)
302
+ .option("--until <when>", `only entries logged on/before this bound: ${PERIOD_UNTIL}`)
303
+ .option("--tag <ref>", "filter by tag (uuid or unique name), direct OR inherited")
304
+ .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
305
+ .option("--json", "emit versioned JSON envelope on stdout")
306
+ .option("--db <path>", "explicit database path")
307
+ .action((opts) => {
308
+ const lim = parseLimit(opts);
309
+ if (!lim.ok)
310
+ return;
311
+ const since = opts.since !== undefined ? parsePeriodStart(opts.since) : undefined;
312
+ const until = opts.until !== undefined ? parsePeriodEnd(opts.until) : undefined;
313
+ for (const [flag, value] of [
314
+ ["--since", since],
315
+ ["--until", until],
316
+ ]) {
317
+ if (value !== undefined && Number.isNaN(value.getTime())) {
318
+ process.stderr.write(`error: ${flag} is not a parseable date\n`);
319
+ process.exitCode = ExitCode.Usage;
320
+ return;
321
+ }
322
+ }
323
+ const base = invocation("logbook", [
324
+ opts.area !== undefined && `--area ${shellQuote(opts.area)}`,
325
+ opts.project !== undefined && `--project ${shellQuote(opts.project)}`,
326
+ opts.since !== undefined && `--since ${shellQuote(opts.since)}`,
327
+ opts.until !== undefined && `--until ${shellQuote(opts.until)}`,
328
+ opts.tag !== undefined && `--tag ${shellQuote(opts.tag)}`,
329
+ opts.exactTag === true && "--exact-tag",
330
+ ]);
331
+ runRead(opts, "logbook", (c) => paginateList(c.read.logbook({
332
+ limit: null,
333
+ ...(opts.area !== undefined && { area: opts.area }),
334
+ ...(opts.project !== undefined && { project: opts.project }),
335
+ ...(since !== undefined && { since }),
336
+ ...(until !== undefined && { until }),
337
+ ...(opts.tag !== undefined && { tag: opts.tag }),
338
+ ...(opts.exactTag === true && { exactTag: true }),
339
+ }), lim.limit), (items) => renderLogbook(items), base, "logbook");
340
+ });
341
+ program
342
+ .command("trash")
343
+ .description("Trashed items (trashed=1 flag, any status), most recently modified first")
344
+ .option("--limit <n>", LIMIT_DESC)
345
+ .option("--all", ALL_DESC)
346
+ .option("--json", "emit versioned JSON envelope on stdout")
347
+ .option("--db <path>", "explicit database path")
348
+ .action((opts) => {
349
+ const lim = parseLimit(opts);
350
+ if (!lim.ok)
351
+ return;
352
+ runRead(opts, "trash", (c) => paginateList(c.read.trash({ limit: null }), lim.limit), renderList, invocation("trash", []), "trash");
242
353
  });
243
- for (const cmd of [
244
- {
245
- name: "logbook",
246
- description: "Completed and canceled items, most recent first",
247
- fetch: (c, limit, tag, exactTag) => c.read.logbook({
248
- limit,
249
- ...(tag !== undefined && { tag }),
250
- ...(exactTag === true && { exactTag }),
251
- }),
252
- defaultLimit: 100,
253
- },
254
- {
255
- name: "trash",
256
- description: "Trashed items (trashed=1 flag, any status), most recently modified first",
257
- fetch: (c, limit, _tag, _exactTag) => c.read.trash({ limit }),
258
- defaultLimit: 200,
259
- },
260
- ]) {
261
- program
262
- .command(cmd.name)
263
- .description(cmd.description)
264
- .option("--limit <n>", "maximum items to return", String(cmd.defaultLimit))
265
- .option("--tag <ref>", "filter by tag (uuid or unique name), direct OR inherited — logbook only")
266
- .option("--exact-tag", "match the named tag only — exclude hierarchy descendants")
267
- .option("--json", "emit versioned JSON envelope on stdout")
268
- .option("--db <path>", "explicit database path")
269
- .action((opts) => {
270
- withClient(opts, cmd.name, (c) => cmd.fetch(c, Number(opts.limit), opts.tag, opts.exactTag), renderList);
271
- });
272
- }
273
354
  program
274
355
  .command("projects")
275
- .description("Active projects (optionally scoped to --area <uuid>)")
356
+ .description("Active projects in sidebar order: loose projects first, then grouped under " +
357
+ "their area (optionally scoped to --area <ref>). Someday and future-scheduled " +
358
+ "projects are hidden — --show-later appends them after each group's active " +
359
+ "block (state carried by the (~) mark and ‹date› chip)")
276
360
  .option("--area <ref>", "filter by area (uuid or unique name)")
361
+ .option("--show-later", "include someday/future-scheduled projects after each active block")
362
+ .option("--all", "include someday/future-scheduled projects (same as --show-later)")
277
363
  .option("--json", "emit versioned JSON envelope on stdout")
278
364
  .option("--db <path>", "explicit database path")
279
365
  .action((opts) => {
280
- withClient(opts, "projects", (c) => c.read.projects(opts.area ? { areaUuid: opts.area } : {}), renderList);
366
+ // --all lifts the sole default restriction here the hidden later block
367
+ // — so it is exactly --show-later (the charter: --all removes every
368
+ // default restriction on the view's own content).
369
+ const showLater = opts.showLater === true || opts.all === true;
370
+ let hints;
371
+ withClient(opts, "projects", (c) => {
372
+ const scope = opts.area !== undefined ? { areaUuid: opts.area } : {};
373
+ const visible = c.read.projects({
374
+ ...scope,
375
+ ...(showLater && { later: true }),
376
+ });
377
+ if (opts.area === undefined) {
378
+ // Sidebar scaffold: every VISIBLE area renders (project-less
379
+ // ones say so), in sidebar order; the loose block leads. One
380
+ // extra projects query buys the hidden-later counts.
381
+ const full = showLater ? visible : c.read.projects({ later: true });
382
+ const shown = new Set(visible.map((i) => i.uuid));
383
+ const groups = [
384
+ { area: null, hidden: 0 },
385
+ ...c.read
386
+ .areas()
387
+ .filter((a) => a.visible)
388
+ .map((a) => ({ area: { uuid: a.uuid, title: a.title }, hidden: 0 })),
389
+ ];
390
+ const at = new Map(groups.map((g, i) => [g.area?.uuid ?? null, i]));
391
+ for (const item of full) {
392
+ if (shown.has(item.uuid))
393
+ continue;
394
+ const g = groups[at.get(item.area?.uuid ?? null) ?? 0];
395
+ if (g !== undefined)
396
+ g.hidden += 1;
397
+ }
398
+ hints = { groups };
399
+ }
400
+ else if (!showLater) {
401
+ // --area scoped: only the bottom hint needs a count.
402
+ const full = c.read.projects({ ...scope, later: true });
403
+ hints = { groups: [{ area: null, hidden: full.length - visible.length }] };
404
+ }
405
+ return visible;
406
+ },
407
+ // Scoped to one area the list is flat (the scope names the group);
408
+ // unscoped it mirrors the sidebar with ⬡ area headers.
409
+ (items) => {
410
+ if (opts.area === undefined)
411
+ return renderProjectsSidebar(items, hints);
412
+ const lines = renderList(items);
413
+ const hidden = hints?.groups.reduce((n, g) => n + g.hidden, 0) ?? 0;
414
+ if (hidden > 0)
415
+ lines.push("", dim(`(${hidden} later project${hidden === 1 ? "" : "s"} — visible with \`--show-later\`)`));
416
+ return lines;
417
+ });
281
418
  });
282
419
  program
283
420
  .command("areas")
284
421
  .description("All areas with their direct tags")
422
+ .option("--all", "show every area (no default restriction applies)")
285
423
  .option("--json", "emit versioned JSON envelope on stdout")
286
424
  .option("--db <path>", "explicit database path")
287
425
  .action((opts) => {
288
- withClient(opts, "areas", (c) => c.read.areas(), ((data) => data.map((a) => `${a.uuid} ${a.title}${a.tags.length ? ` #${a.tags.map((t) => t.title).join(" #")}` : ""}`)));
426
+ withClient(opts, "areas", (c) => c.read.areas(), (data) => {
427
+ const w = uuidDisplayWidth(data);
428
+ return data.map((a) => `${dim(uuidCol(a.uuid, w))} ${areaMark()} ${a.title}${a.tags.length ? ` ${dim(`#${a.tags.map((t) => t.title).join(" #")}`)}` : ""}`);
429
+ });
289
430
  });
290
431
  program
291
432
  .command("tags")
292
433
  .description("Tag taxonomy (parent → child hierarchy flattened with refs)")
434
+ .option("--all", "show every tag (no default restriction applies)")
293
435
  .option("--json", "emit versioned JSON envelope on stdout")
294
436
  .option("--db <path>", "explicit database path")
295
437
  .action((opts) => {
296
- withClient(opts, "tags", (c) => c.read.tags(), ((data) => data.map((t) => `${t.uuid} ${t.parent ? `${t.parent.title}/` : ""}${t.title}`)));
438
+ withClient(opts, "tags", (c) => c.read.tags(), (data) => data.map((t) => `${t.uuid} ${t.parent ? `${t.parent.title}/` : ""}${t.title}`));
297
439
  });
298
440
  program
299
441
  .command("changes")
@@ -301,26 +443,34 @@ export function registerReadCommands(program) {
301
443
  "trashed, logged, and repeating-template rows so agents can sync state; check " +
302
444
  "trashed/status/repeating on each item. Caveats: tag/area edits and checklist-item " +
303
445
  "edits don't bump tasks and are invisible here.")
304
- .requiredOption("--since <when>", "ISO date/datetime (e.g. 2026-07-05T14:30:00)")
305
- .option("--limit <n>", "maximum items", "200")
446
+ .requiredOption("--since <when>", `ISO date/datetime (e.g. 2026-07-05T14:30:00), or ${PERIOD_SINCE}`)
447
+ .option("--limit <n>", LIMIT_DESC)
448
+ .option("--all", ALL_DESC)
306
449
  .option("--json", "emit versioned JSON envelope on stdout")
307
450
  .option("--db <path>", "explicit database path")
308
451
  .action((opts) => {
309
- const since = new Date(opts.since);
452
+ const lim = parseLimit(opts);
453
+ if (!lim.ok)
454
+ return;
455
+ const since = parsePeriodStart(opts.since);
310
456
  if (Number.isNaN(since.getTime())) {
311
457
  process.stderr.write(`error: --since is not a parseable date: ${opts.since}\n`);
312
458
  process.exitCode = ExitCode.Usage;
313
459
  return;
314
460
  }
315
- withClient(opts, "changes", (c) => c.read.changes({ since, limit: Number(opts.limit) }), ((items) => items.length === 0
461
+ const base = invocation("changes", [`--since ${shellQuote(opts.since)}`]);
462
+ runRead(opts, "changes", (c) => paginateList(c.read.changes({ since, limit: null }), lim.limit), (items) => items.length === 0
316
463
  ? ["(no changes)"]
317
- : items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`)));
464
+ : items.map((i) => `${i.changeKind === "created" ? "+" : "~"} ${formatItem(i)}${i.trashed ? " [trashed]" : ""}`), base);
318
465
  });
319
466
  program
320
467
  .command("search <query>")
321
- .description("Title/notes substring search, most recently modified first. Default scope: OPEN + " +
322
- "untrashed items only widen with --logged / --trashed / --all. Scope with " +
323
- "--project / --area / --tag (tag matches include hierarchy descendants) / --type.")
468
+ .description("Title/notes substring search, ranked: title matches first, then notes, then a " +
469
+ "project surfaced by a matching heading title (shown as its parent project, " +
470
+ "`via heading`); projects rank above to-dos, active above someday, ties broken by " +
471
+ "most-recently-modified. Default scope: OPEN + untrashed items only — widen with " +
472
+ "--logged / --trashed / --all. Scope with --project / --area / --tag (tag matches " +
473
+ "include hierarchy descendants) / --type.")
324
474
  .option("--project <ref>", "restrict to one project's children (uuid or unique name)")
325
475
  .option("--area <ref>", "restrict to one area's direct members (uuid or unique name)")
326
476
  .option("--tag <ref>", "restrict by tag: direct, inherited, or descendant-tagged")
@@ -328,19 +478,36 @@ export function registerReadCommands(program) {
328
478
  .option("--type <kind>", "todo | project")
329
479
  .option("--logged", "include completed/canceled items")
330
480
  .option("--trashed", "include trashed items")
331
- .option("--all", "legacy behavior: everything (open + logged + trashed)")
332
- .option("--limit <n>", "maximum results", "50")
481
+ .option("--all", "everything, unbounded: open + logged + trashed, with no row limit (excludes --limit)")
482
+ .option("--limit <n>", LIMIT_DESC)
333
483
  .option("--json", "emit versioned JSON envelope on stdout")
334
484
  .option("--db <path>", "explicit database path")
335
485
  .action((query, opts) => {
336
486
  const type = opts["type"];
337
487
  if (type !== undefined && type !== "todo" && type !== "project") {
338
488
  process.stderr.write("error: --type must be todo or project\n");
339
- process.exitCode = 2;
489
+ process.exitCode = ExitCode.Usage;
340
490
  return;
341
491
  }
342
- withClient(opts, "search", (c) => c.read.search(query, {
343
- limit: Number(opts["limit"] ?? 50),
492
+ const all = opts["all"] === true;
493
+ const limitOpt = opts["limit"];
494
+ // --all widens the scope AND lifts the row limit — combining it with an
495
+ // explicit --limit is contradictory (like every other view).
496
+ const lim = parseLimit({ all, ...(limitOpt !== undefined && { limit: limitOpt }) });
497
+ if (!lim.ok)
498
+ return;
499
+ const base = invocation("search", [
500
+ shellQuote(query),
501
+ opts["project"] !== undefined && `--project ${shellQuote(opts["project"])}`,
502
+ opts["area"] !== undefined && `--area ${shellQuote(opts["area"])}`,
503
+ opts["tag"] !== undefined && `--tag ${shellQuote(opts["tag"])}`,
504
+ opts["exactTag"] === true && "--exact-tag",
505
+ type !== undefined && `--type ${type}`,
506
+ opts["logged"] === true && "--logged",
507
+ opts["trashed"] === true && "--trashed",
508
+ ]);
509
+ runRead(opts, "search", (c) => paginateList(c.read.search(query, {
510
+ limit: null,
344
511
  ...(opts["project"] !== undefined && { project: opts["project"] }),
345
512
  ...(opts["area"] !== undefined && { area: opts["area"] }),
346
513
  ...(opts["tag"] !== undefined && { tag: opts["tag"] }),
@@ -348,8 +515,27 @@ export function registerReadCommands(program) {
348
515
  ...(type !== undefined && { type: type === "todo" ? "to-do" : "project" }),
349
516
  ...(opts["logged"] === true && { logged: true }),
350
517
  ...(opts["trashed"] === true && { trashed: true }),
351
- ...(opts["all"] === true && { all: true }),
352
- }), renderList);
518
+ ...(all && { all: true }),
519
+ }), lim.limit), renderSearch, base);
353
520
  });
521
+ // Every row-rendering view points at `things legend` for its glyph language.
522
+ const GLYPH_VIEWS = new Set([
523
+ "today",
524
+ "inbox",
525
+ "anytime",
526
+ "someday",
527
+ "upcoming",
528
+ "logbook",
529
+ "trash",
530
+ "projects",
531
+ "areas",
532
+ "changes",
533
+ "search",
534
+ ]);
535
+ for (const c of program.commands) {
536
+ if (GLYPH_VIEWS.has(c.name())) {
537
+ c.addHelpText("after", "\nsymbols & colors: run `things legend`");
538
+ }
539
+ }
354
540
  }
355
541
  //# sourceMappingURL=reads.js.map