things-api 0.9.0 → 0.11.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 (261) hide show
  1. package/README.md +57 -12
  2. package/dist/audit/log.js +56 -2
  3. package/dist/audit/log.js.map +1 -1
  4. package/dist/audit/schema.d.ts +26 -8
  5. package/dist/audit/schema.js +42 -1
  6. package/dist/audit/schema.js.map +1 -1
  7. package/dist/cli/clock.d.ts +8 -0
  8. package/dist/cli/clock.js +24 -0
  9. package/dist/cli/clock.js.map +1 -0
  10. package/dist/cli/commands/area.d.ts +26 -3
  11. package/dist/cli/commands/area.js +129 -75
  12. package/dist/cli/commands/area.js.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts +1 -1
  14. package/dist/cli/commands/doctor.js +44 -5
  15. package/dist/cli/commands/doctor.js.map +1 -1
  16. package/dist/cli/commands/install-skill.d.ts +45 -0
  17. package/dist/cli/commands/install-skill.js +207 -0
  18. package/dist/cli/commands/install-skill.js.map +1 -0
  19. package/dist/cli/commands/mcp.js +23 -4
  20. package/dist/cli/commands/mcp.js.map +1 -1
  21. package/dist/cli/commands/project.d.ts +18 -1
  22. package/dist/cli/commands/project.js +74 -35
  23. package/dist/cli/commands/project.js.map +1 -1
  24. package/dist/cli/commands/reads.d.ts +15 -2
  25. package/dist/cli/commands/reads.js +459 -165
  26. package/dist/cli/commands/reads.js.map +1 -1
  27. package/dist/cli/commands/repeat-flags.d.ts +20 -0
  28. package/dist/cli/commands/repeat-flags.js +76 -0
  29. package/dist/cli/commands/repeat-flags.js.map +1 -0
  30. package/dist/cli/commands/setup.js +8 -4
  31. package/dist/cli/commands/setup.js.map +1 -1
  32. package/dist/cli/commands/show.js +53 -30
  33. package/dist/cli/commands/show.js.map +1 -1
  34. package/dist/cli/commands/todo.d.ts +1 -1
  35. package/dist/cli/commands/todo.js +20 -13
  36. package/dist/cli/commands/todo.js.map +1 -1
  37. package/dist/cli/commands/writes.js +388 -161
  38. package/dist/cli/commands/writes.js.map +1 -1
  39. package/dist/cli/did-you-mean.d.ts +1 -1
  40. package/dist/cli/excess-args.d.ts +15 -0
  41. package/dist/cli/excess-args.js +51 -0
  42. package/dist/cli/excess-args.js.map +1 -0
  43. package/dist/cli/glyphs.d.ts +55 -2
  44. package/dist/cli/glyphs.js +114 -21
  45. package/dist/cli/glyphs.js.map +1 -1
  46. package/dist/cli/help.d.ts +54 -0
  47. package/dist/cli/help.js +405 -0
  48. package/dist/cli/help.js.map +1 -0
  49. package/dist/cli/main.js +64 -26
  50. package/dist/cli/main.js.map +1 -1
  51. package/dist/cli/move-hint.d.ts +45 -0
  52. package/dist/cli/move-hint.js +196 -0
  53. package/dist/cli/move-hint.js.map +1 -0
  54. package/dist/cli/period.d.ts +9 -8
  55. package/dist/cli/period.js +56 -30
  56. package/dist/cli/period.js.map +1 -1
  57. package/dist/cli/read-driver.d.ts +26 -19
  58. package/dist/cli/read-driver.js +77 -33
  59. package/dist/cli/read-driver.js.map +1 -1
  60. package/dist/cli/render.d.ts +92 -28
  61. package/dist/cli/render.js +333 -103
  62. package/dist/cli/render.js.map +1 -1
  63. package/dist/cli/resolve-invocation.d.ts +42 -4
  64. package/dist/cli/resolve-invocation.js +97 -14
  65. package/dist/cli/resolve-invocation.js.map +1 -1
  66. package/dist/cli/skill-check.d.ts +21 -0
  67. package/dist/cli/skill-check.js +82 -0
  68. package/dist/cli/skill-check.js.map +1 -0
  69. package/dist/cli/skill.d.ts +62 -0
  70. package/dist/cli/skill.js +142 -0
  71. package/dist/cli/skill.js.map +1 -0
  72. package/dist/cli/tag-filters.d.ts +63 -0
  73. package/dist/cli/tag-filters.js +59 -0
  74. package/dist/cli/tag-filters.js.map +1 -0
  75. package/dist/cli/verb-hint.d.ts +25 -0
  76. package/dist/cli/verb-hint.js +163 -0
  77. package/dist/cli/verb-hint.js.map +1 -0
  78. package/dist/cli/version.d.ts +4 -0
  79. package/dist/cli/version.js +18 -0
  80. package/dist/cli/version.js.map +1 -0
  81. package/dist/cli/width.d.ts +135 -0
  82. package/dist/cli/width.js +313 -0
  83. package/dist/cli/width.js.map +1 -0
  84. package/dist/client.d.ts +160 -24
  85. package/dist/client.js +132 -19
  86. package/dist/client.js.map +1 -1
  87. package/dist/config.d.ts +11 -0
  88. package/dist/config.js +3 -0
  89. package/dist/config.js.map +1 -1
  90. package/dist/contracts.d.ts +97 -17
  91. package/dist/contracts.js +44 -1
  92. package/dist/contracts.js.map +1 -1
  93. package/dist/db/fingerprint.d.ts +12 -0
  94. package/dist/db/fingerprint.js +15 -1
  95. package/dist/db/fingerprint.js.map +1 -1
  96. package/dist/db/locate.js +1 -1
  97. package/dist/db/locate.js.map +1 -1
  98. package/dist/diagnose.d.ts +63 -0
  99. package/dist/diagnose.js +38 -1
  100. package/dist/diagnose.js.map +1 -1
  101. package/dist/index.d.ts +36 -5
  102. package/dist/index.js +38 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp/server.d.ts +9 -1
  105. package/dist/mcp/server.js +1038 -436
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/model/clock.d.ts +61 -0
  108. package/dist/model/clock.js +71 -0
  109. package/dist/model/clock.js.map +1 -0
  110. package/dist/model/dates.d.ts +26 -3
  111. package/dist/model/dates.js +84 -3
  112. package/dist/model/dates.js.map +1 -1
  113. package/dist/model/entities.d.ts +48 -10
  114. package/dist/model/entities.js.map +1 -1
  115. package/dist/model/mappers.d.ts +9 -2
  116. package/dist/model/mappers.js +32 -9
  117. package/dist/model/mappers.js.map +1 -1
  118. package/dist/model/serialize.d.ts +28 -0
  119. package/dist/model/serialize.js +80 -0
  120. package/dist/model/serialize.js.map +1 -0
  121. package/dist/model/when-sugar.d.ts +47 -0
  122. package/dist/model/when-sugar.js +45 -0
  123. package/dist/model/when-sugar.js.map +1 -0
  124. package/dist/read/area-view.d.ts +8 -1
  125. package/dist/read/area-view.js +55 -17
  126. package/dist/read/area-view.js.map +1 -1
  127. package/dist/read/detail.d.ts +1 -1
  128. package/dist/read/detail.js +11 -4
  129. package/dist/read/detail.js.map +1 -1
  130. package/dist/read/filter-contract.d.ts +117 -0
  131. package/dist/read/filter-contract.js +78 -0
  132. package/dist/read/filter-contract.js.map +1 -0
  133. package/dist/read/log-boundary.d.ts +1 -1
  134. package/dist/read/log-boundary.js +13 -3
  135. package/dist/read/log-boundary.js.map +1 -1
  136. package/dist/read/predicates.d.ts +30 -0
  137. package/dist/read/predicates.js +30 -0
  138. package/dist/read/predicates.js.map +1 -1
  139. package/dist/read/project-view.d.ts +12 -1
  140. package/dist/read/project-view.js +45 -12
  141. package/dist/read/project-view.js.map +1 -1
  142. package/dist/read/queries.d.ts +152 -7
  143. package/dist/read/queries.js +277 -36
  144. package/dist/read/queries.js.map +1 -1
  145. package/dist/read/sections.d.ts +51 -0
  146. package/dist/read/sections.js +37 -0
  147. package/dist/read/sections.js.map +1 -0
  148. package/dist/read/sidebar-order.js +2 -1
  149. package/dist/read/sidebar-order.js.map +1 -1
  150. package/dist/read/snapshot.d.ts +1 -1
  151. package/dist/read/snapshot.js +9 -2
  152. package/dist/read/snapshot.js.map +1 -1
  153. package/dist/read/tags.d.ts +27 -3
  154. package/dist/read/tags.js +83 -12
  155. package/dist/read/tags.js.map +1 -1
  156. package/dist/read/truncation.d.ts +71 -0
  157. package/dist/read/{pagination.js → truncation.js} +103 -94
  158. package/dist/read/truncation.js.map +1 -0
  159. package/dist/read/views.d.ts +147 -22
  160. package/dist/read/views.js +282 -80
  161. package/dist/read/views.js.map +1 -1
  162. package/dist/surface-copy.d.ts +19 -0
  163. package/dist/surface-copy.js +32 -0
  164. package/dist/surface-copy.js.map +1 -1
  165. package/dist/sync-health.d.ts +78 -0
  166. package/dist/sync-health.js +312 -0
  167. package/dist/sync-health.js.map +1 -0
  168. package/dist/write/accessibility-probe.d.ts +12 -0
  169. package/dist/write/accessibility-probe.js +71 -0
  170. package/dist/write/accessibility-probe.js.map +1 -0
  171. package/dist/write/automation-probe.d.ts +8 -1
  172. package/dist/write/automation-probe.js +16 -1
  173. package/dist/write/automation-probe.js.map +1 -1
  174. package/dist/write/batch.js +5 -2
  175. package/dist/write/batch.js.map +1 -1
  176. package/dist/write/capabilities.d.ts +8 -0
  177. package/dist/write/capabilities.js +13 -6
  178. package/dist/write/capabilities.js.map +1 -1
  179. package/dist/write/commands.d.ts +3 -1
  180. package/dist/write/commands.js +602 -49
  181. package/dist/write/commands.js.map +1 -1
  182. package/dist/write/edit-checklist.js +3 -2
  183. package/dist/write/edit-checklist.js.map +1 -1
  184. package/dist/write/guards.d.ts +2 -7
  185. package/dist/write/guards.js +87 -6
  186. package/dist/write/guards.js.map +1 -1
  187. package/dist/write/heading.js +2 -0
  188. package/dist/write/heading.js.map +1 -1
  189. package/dist/write/lock.d.ts +40 -2
  190. package/dist/write/lock.js +91 -14
  191. package/dist/write/lock.js.map +1 -1
  192. package/dist/write/make-repeating-project.d.ts +4 -0
  193. package/dist/write/make-repeating-project.js +253 -0
  194. package/dist/write/make-repeating-project.js.map +1 -0
  195. package/dist/write/operations.d.ts +145 -1
  196. package/dist/write/operations.js +48 -0
  197. package/dist/write/operations.js.map +1 -1
  198. package/dist/write/pipeline.d.ts +73 -2
  199. package/dist/write/pipeline.js +218 -43
  200. package/dist/write/pipeline.js.map +1 -1
  201. package/dist/write/planner.js +8 -1
  202. package/dist/write/planner.js.map +1 -1
  203. package/dist/write/pre-state.d.ts +105 -3
  204. package/dist/write/pre-state.js +144 -3
  205. package/dist/write/pre-state.js.map +1 -1
  206. package/dist/write/recurrence-rule-blob.d.ts +65 -0
  207. package/dist/write/recurrence-rule-blob.js +150 -0
  208. package/dist/write/recurrence-rule-blob.js.map +1 -0
  209. package/dist/write/reopen.js +3 -2
  210. package/dist/write/reopen.js.map +1 -1
  211. package/dist/write/reorder.js +27 -17
  212. package/dist/write/reorder.js.map +1 -1
  213. package/dist/write/repeat-rule.d.ts +22 -0
  214. package/dist/write/repeat-rule.js +258 -0
  215. package/dist/write/repeat-rule.js.map +1 -0
  216. package/dist/write/reversibility.js +49 -0
  217. package/dist/write/reversibility.js.map +1 -1
  218. package/dist/write/tag-refs.d.ts +47 -0
  219. package/dist/write/tag-refs.js +126 -0
  220. package/dist/write/tag-refs.js.map +1 -0
  221. package/dist/write/undo.d.ts +74 -5
  222. package/dist/write/undo.js +493 -80
  223. package/dist/write/undo.js.map +1 -1
  224. package/dist/write/vectors/registry.d.ts +15 -1
  225. package/dist/write/vectors/registry.js +65 -2
  226. package/dist/write/vectors/registry.js.map +1 -1
  227. package/dist/write/vectors/simulator.d.ts +44 -0
  228. package/dist/write/vectors/simulator.js +1136 -0
  229. package/dist/write/vectors/simulator.js.map +1 -0
  230. package/dist/write/vectors/types.d.ts +154 -3
  231. package/dist/write/vectors/ui-certification.d.ts +54 -0
  232. package/dist/write/vectors/ui-certification.js +43 -0
  233. package/dist/write/vectors/ui-certification.js.map +1 -0
  234. package/dist/write/vectors/ui-drag.d.ts +188 -0
  235. package/dist/write/vectors/ui-drag.js +1095 -0
  236. package/dist/write/vectors/ui-drag.js.map +1 -0
  237. package/dist/write/vectors/ui-recipes.d.ts +83 -0
  238. package/dist/write/vectors/ui-recipes.js +610 -0
  239. package/dist/write/vectors/ui-recipes.js.map +1 -0
  240. package/dist/write/vectors/ui.d.ts +152 -0
  241. package/dist/write/vectors/ui.js +704 -0
  242. package/dist/write/vectors/ui.js.map +1 -0
  243. package/dist/write/verify/delta.d.ts +141 -4
  244. package/dist/write/verify/delta.js +199 -21
  245. package/dist/write/verify/delta.js.map +1 -1
  246. package/dist/write/verify/poller.d.ts +7 -1
  247. package/dist/write/verify/poller.js +21 -1
  248. package/dist/write/verify/poller.js.map +1 -1
  249. package/package.json +18 -3
  250. package/skills/things-cli/SKILL.md +53 -0
  251. package/skills/things-cli/references/contracts.md +41 -0
  252. package/skills/things-cli/references/gui.md +11 -0
  253. package/skills/things-cli/references/model.md +51 -0
  254. package/dist/cli/exit-codes.d.ts +0 -26
  255. package/dist/cli/exit-codes.js +0 -26
  256. package/dist/cli/exit-codes.js.map +0 -1
  257. package/dist/cli/output.d.ts +0 -42
  258. package/dist/cli/output.js +0 -16
  259. package/dist/cli/output.js.map +0 -1
  260. package/dist/read/pagination.d.ts +0 -104
  261. package/dist/read/pagination.js.map +0 -1
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Width-aware TTY row fitting (docs/design/width-aware-tty.md). Terminal list
3
+ * rows never wrap: like the Things GUI, an over-long row truncates its title
4
+ * with a trailing `…` and folds its tags, keeping the always-present metadata
5
+ * (uuid, box, chips, tail markers, the full deadline token) intact. Everything
6
+ * here is pure and takes the target width explicitly, so it is unit-testable
7
+ * without a TTY and byte-stable off one: fitting engages ONLY when the driver
8
+ * resolves a positive width (setFitWidth) — the default is null (no fitting),
9
+ * so pipes, grep, and `--json` are untouched by construction.
10
+ *
11
+ * No runtime dependency (guest e2e bundles ship node + dist + commander only),
12
+ * so the display-width math is a small vendored wcwidth: SGR escapes strip to
13
+ * zero, East-Asian-Wide + emoji-presentation codepoints count two cells, the
14
+ * rest count one. ZWJ-sequence emoji can be off by a cell (accepted — worst
15
+ * case a row runs a hair short); the glyph vocabulary (★ ⏾ ⚑ ↻ ⍾ ≡ ◷) is all
16
+ * ambiguous/narrow class and counts one, so the layout math matches the render.
17
+ */
18
+ const SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
19
+ /** Drop every SGR (color/dim/bold/…) escape, leaving the visible characters. */
20
+ export function stripSgr(s) {
21
+ return s.replace(SGR, "");
22
+ }
23
+ // Codepoint ranges that occupy TWO terminal cells: the East-Asian Wide/Fullwidth
24
+ // core plus the default-emoji-presentation set (so "⭐️" counts 2 while the
25
+ // ambiguous-class "★" counts 1 — the property that keeps our glyph set narrow).
26
+ const WIDE_RANGES = [
27
+ [0x1100, 0x115f], // Hangul Jamo
28
+ [0x2e80, 0x303e], // CJK radicals … Kangxi
29
+ [0x3041, 0x33ff], // Hiragana … CJK compat
30
+ [0x3400, 0x4dbf], // CJK Ext A
31
+ [0x4e00, 0x9fff], // CJK Unified
32
+ [0xa000, 0xa4cf], // Yi
33
+ [0xac00, 0xd7a3], // Hangul Syllables
34
+ [0xf900, 0xfaff], // CJK Compat Ideographs
35
+ [0xfe30, 0xfe4f], // CJK Compat Forms
36
+ [0xff00, 0xff60], // Fullwidth Forms
37
+ [0xffe0, 0xffe6], // Fullwidth signs
38
+ // Emoji-presentation (default-wide) — the codepoints Unicode marks
39
+ // Emoji_Presentation=Yes outside the SMP blocks below.
40
+ [0x231a, 0x231b],
41
+ [0x23e9, 0x23ec],
42
+ [0x23f0, 0x23f0],
43
+ [0x23f3, 0x23f3],
44
+ [0x25fd, 0x25fe],
45
+ [0x2614, 0x2615],
46
+ [0x2648, 0x2653],
47
+ [0x267f, 0x267f],
48
+ [0x2693, 0x2693],
49
+ [0x26a1, 0x26a1],
50
+ [0x26aa, 0x26ab],
51
+ [0x26bd, 0x26be],
52
+ [0x26c4, 0x26c5],
53
+ [0x26ce, 0x26ce],
54
+ [0x26d4, 0x26d4],
55
+ [0x26ea, 0x26ea],
56
+ [0x26f2, 0x26f3],
57
+ [0x26f5, 0x26f5],
58
+ [0x26fa, 0x26fa],
59
+ [0x26fd, 0x26fd],
60
+ [0x2705, 0x2705],
61
+ [0x270a, 0x270b],
62
+ [0x2728, 0x2728],
63
+ [0x274c, 0x274c],
64
+ [0x274e, 0x274e],
65
+ [0x2753, 0x2755],
66
+ [0x2757, 0x2757],
67
+ [0x2795, 0x2797],
68
+ [0x27b0, 0x27b0],
69
+ [0x27bf, 0x27bf],
70
+ [0x2b1b, 0x2b1c],
71
+ [0x2b50, 0x2b50], // ⭐ WHITE MEDIUM STAR (Emoji_Presentation — 2 cells)
72
+ [0x2b55, 0x2b55],
73
+ [0x1f000, 0x1faff], // SMP emoji blocks (mahjong … symbols & pictographs ext)
74
+ [0x20000, 0x3fffd], // CJK Ext B+ (SIP/TIP)
75
+ ];
76
+ // Zero-width: combining marks, ZW(SP/NJ/J), and variation selectors. Treating
77
+ // VS16 (FE0F) as zero-width means an emoji-presentation base already counted 2
78
+ // stays 2 (rather than 3) — the "⭐️" case.
79
+ const ZERO_RANGES = [
80
+ [0x0300, 0x036f], // combining diacritics
81
+ [0x200b, 0x200f], // ZWSP … RLM (incl. ZWJ 200d)
82
+ [0xfe00, 0xfe0f], // variation selectors
83
+ ];
84
+ function inRanges(cp, ranges) {
85
+ for (const [lo, hi] of ranges)
86
+ if (cp >= lo && cp <= hi)
87
+ return true;
88
+ return false;
89
+ }
90
+ /** Terminal cells a single codepoint occupies: 0 (combining/zero-width), 2 (wide), else 1. */
91
+ export function charWidth(cp) {
92
+ if (cp === 0)
93
+ return 0;
94
+ if (inRanges(cp, ZERO_RANGES))
95
+ return 0;
96
+ if (inRanges(cp, WIDE_RANGES))
97
+ return 2;
98
+ return 1;
99
+ }
100
+ /** Visible terminal width of a string: SGR-stripped, summed per codepoint. */
101
+ export function visibleWidth(s) {
102
+ let w = 0;
103
+ for (const ch of stripSgr(s))
104
+ w += charWidth(ch.codePointAt(0) ?? 0);
105
+ return w;
106
+ }
107
+ /**
108
+ * Truncate PLAIN text (no SGR) to at most `cols` terminal cells, never splitting
109
+ * a wide codepoint across the boundary. Used to clip a raw title before its
110
+ * styling wrappers are re-applied, so the ellipsis/clip boundary lands OUTSIDE
111
+ * the SGR runs (the fitter never cuts an escape sequence).
112
+ */
113
+ export function clipPlain(s, cols) {
114
+ if (cols <= 0)
115
+ return "";
116
+ let w = 0;
117
+ let out = "";
118
+ for (const ch of s) {
119
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
120
+ if (w + cw > cols)
121
+ break;
122
+ out += ch;
123
+ w += cw;
124
+ }
125
+ return out;
126
+ }
127
+ // ── Row fitting ─────────────────────────────────────────────────────────────
128
+ /**
129
+ * TITLE : TAGS relative max-width ratio (tunable — ONE place). It arbitrates the
130
+ * collapsible budget ONLY under contention — when BOTH segments' natural widths
131
+ * exceed their shares. Otherwise the fit is lazy: the side within its share keeps
132
+ * its natural width and the other takes everything left (see {@link fitRow}). 4:1
133
+ * keeps the title dominant when they do genuinely contend.
134
+ */
135
+ const TITLE_RATIO = 4;
136
+ const TAGS_RATIO = 1;
137
+ /**
138
+ * The title's protected minimum, in columns. NOT a per-row clamp: it is the
139
+ * input to the derived floors (render.ts FULL_FIT_FLOOR / COMPACT_FIT_FLOOR =
140
+ * worst-case furniture + TITLE_MIN) and the threshold the sacrifice order
141
+ * protects the title down to. Because the driver fits every row to at least the
142
+ * compact floor (max(width, COMPACT_FIT_FLOOR)),
143
+ * the worst-furniture row's title lands at exactly TITLE_MIN and every lighter
144
+ * row's title is automatically wider — so this floor is always satisfiable and
145
+ * there is no sub-floor clip (below the floor the terminal wraps, losing nothing).
146
+ */
147
+ export const TITLE_MIN = 16;
148
+ /** Overflow marker for folded tags (dim styling applied by the row's styleTags). */
149
+ const TAG_OVERFLOW = "#…";
150
+ /**
151
+ * Plain form (no styling, no leading space) of the tag run showing the first `k`
152
+ * real tags; when `k` is below the total, the dropped tags fold into the dim
153
+ * `#…` marker. `k === 0` is the bare marker.
154
+ */
155
+ function tagForm(names, k) {
156
+ const shown = names.slice(0, k).map((n) => `#${n}`);
157
+ if (k < names.length)
158
+ shown.push(TAG_OVERFLOW);
159
+ return shown.join(" ");
160
+ }
161
+ /** Visible width of the rendered tag run at level `k` (its leading space + form). */
162
+ function tagWidth(names, k) {
163
+ if (names.length === 0)
164
+ return 0;
165
+ return 1 + visibleWidth(tagForm(names, k));
166
+ }
167
+ /** Widest tag level (most real tags) whose render fits `cap`, floored at `floorK`. */
168
+ function pickTagLevel(names, cap, floorK) {
169
+ for (let k = names.length; k >= floorK; k--) {
170
+ if (tagWidth(names, k) <= cap)
171
+ return k;
172
+ }
173
+ return floorK;
174
+ }
175
+ /**
176
+ * EXPERIMENTAL — the right-aligned deadline gutter (GUI flag-column parity). When
177
+ * fitting is active, every fitted row's `⚑` token is pushed flush to the effective
178
+ * width so all deadlines in a view line up in one gutter, exactly like the app's
179
+ * right-pinned flag column. `body` is the composed row WITHOUT its deadline and
180
+ * `deadline` the pre-styled ` ⚑ …` segment (its leading space + token) or "".
181
+ * The padding is plain spaces OUTSIDE the styled token, and there is always ≥ 1
182
+ * space — so a row that already fills the width is byte-identical to the inline
183
+ * (unguttered) form, and a row with no deadline is returned untouched (ragged
184
+ * right, as the GUI leaves flag-less rows). This is only ever reached from
185
+ * {@link fitRow} (fitting active), so the byte-stable null path never sees it.
186
+ * Reverting the experiment is deleting this function and its two `fitRow` calls.
187
+ */
188
+ function alignDeadline(body, deadline, width) {
189
+ if (deadline === "")
190
+ return body;
191
+ // deadline = " " + token; swap that single inline space for the gutter run.
192
+ const pad = width - visibleWidth(body) - visibleWidth(deadline) + 1;
193
+ return `${body}${" ".repeat(Math.max(1, pad))}${deadline.slice(1)}`;
194
+ }
195
+ /**
196
+ * Fit one row into `width` terminal cells per the ratified collapse order. The
197
+ * always-present parts (uuid, box, meta chips, tail markers, full deadline)
198
+ * never shrink; title and tags divvy up what remains LAZILY — tags fold to the
199
+ * widest progressive level that fits and the 4:1 ratio arbitrates only when both
200
+ * genuinely contend (slack an under-share side leaves flows to the other). Then
201
+ * the CLI-only container is sacrificed WHOLE (before the last tag folds), then
202
+ * the last tag folds to bare `#…`, and the title bottoms at TITLE_MIN. `width` is
203
+ * the caller's effective width (already max'd with COMPACT_FIT_FLOOR), so the final
204
+ * stage always satisfies the floor; a sub-floor width would simply let the
205
+ * terminal wrap (the title is kept whole rather than clipped). A row that already
206
+ * fits keeps its full content; the only always-applied transform is the
207
+ * experimental deadline gutter ({@link alignDeadline}), which right-pins the `⚑`.
208
+ */
209
+ export function fitRow(seg, width) {
210
+ const names = seg.tagNames;
211
+ const wLeft = visibleWidth(seg.left) + 1; // + the space before the title
212
+ const wTail = visibleWidth(seg.tail);
213
+ const wCtx = visibleWidth(seg.context);
214
+ const wDead = visibleWidth(seg.deadline);
215
+ const wTitle = visibleWidth(seg.rawTitle);
216
+ // A naturally short title never floors at TITLE_MIN — it simply fits whole.
217
+ const titleNeed = Math.min(TITLE_MIN, wTitle);
218
+ const fixed = (ctx) => wLeft + wTail + wDead + (ctx ? wCtx : 0);
219
+ const fitTitle = (budget) => wTitle <= budget
220
+ ? seg.styleTitle(seg.rawTitle)
221
+ : seg.styleTitle(`${clipPlain(seg.rawTitle, budget - 1)}…`);
222
+ const renderRow = (title, k, ctx) => {
223
+ const tags = names.length > 0 ? seg.styleTags(tagForm(names, k)) : "";
224
+ const body = `${seg.left} ${title}${seg.tail}${tags}${ctx ? seg.context : ""}`;
225
+ return alignDeadline(body, seg.deadline, width);
226
+ };
227
+ // Stage "full": everything fits with tags whole and the container present.
228
+ // (The deadline is still pushed to the gutter — GUI flags pin right even on a
229
+ // short row — but the null path returns seg.full before reaching the fitter.)
230
+ const budgetFull = width - fixed(true);
231
+ if (wTitle + tagWidth(names, names.length) <= budgetFull) {
232
+ const body = seg.full.slice(0, seg.full.length - seg.deadline.length);
233
+ return alignDeadline(body, seg.deadline, width);
234
+ }
235
+ // Stage "both-shrink": tags fold LAZILY — to the WIDEST progressive level that
236
+ // fits, not an eager 1/5 cap. The 4:1 ratio arbitrates ONLY under contention
237
+ // (both natural widths exceed their shares of the collapsible budget). When the
238
+ // title is within its 4/5 share it keeps its natural width and the tags get
239
+ // everything left (their widest fitting level); when instead the tags are
240
+ // within their 1/5 share they stay whole and the title gets the rest. Either
241
+ // way, slack a discrete tag level does not consume flows BACK to the title, and
242
+ // neither side is shrunk below what the budget actually forces. Container kept.
243
+ const tagShare = Math.floor((budgetFull * TAGS_RATIO) / (TITLE_RATIO + TAGS_RATIO));
244
+ const titleShare = budgetFull - tagShare;
245
+ const tagCap = wTitle <= titleShare ? budgetFull - wTitle : tagShare;
246
+ const k = pickTagLevel(names, tagCap, 1);
247
+ const titleBudgetB = budgetFull - tagWidth(names, k);
248
+ if (titleBudgetB >= titleNeed)
249
+ return renderRow(fitTitle(titleBudgetB), k, true);
250
+ // Stage "container-drop": the CLI-only container yields WHOLE (a truncated
251
+ // parenthetical is worse than none) BEFORE the last tag folds — tags hold at
252
+ // `#first #…`, the reclaimed width goes to the title.
253
+ const budgetNoCtx = width - fixed(false);
254
+ const titleBudgetC = budgetNoCtx - tagWidth(names, 1);
255
+ if (titleBudgetC >= titleNeed)
256
+ return renderRow(fitTitle(titleBudgetC), 1, false);
257
+ // Stage "tags-bare": the last tag folds to the bare `#…` marker (container
258
+ // already gone), the title floors at TITLE_MIN. No end-clip: at an effective
259
+ // width this budget clears the floor; a sub-floor width keeps the title whole
260
+ // and lets the terminal wrap (never dropping the deadline).
261
+ const titleBudgetD = budgetNoCtx - tagWidth(names, 0);
262
+ const dTitle = titleBudgetD >= 1 ? fitTitle(titleBudgetD) : seg.styleTitle(seg.rawTitle);
263
+ return renderRow(dTitle, 0, false);
264
+ }
265
+ /**
266
+ * Resolve the effective fit from the once-resolved fit width and the two
267
+ * glyph-derived floors (`render.ts`: FULL_FIT_FLOOR / COMPACT_FIT_FLOOR). This
268
+ * is the SINGLE decision point for the full-vs-compact deadline form:
269
+ * - `width ≥ full` → full forms, fit to width (today's behavior);
270
+ * - `compact ≤ width < full` → all deadlines compact, fit to width;
271
+ * - `width < compact` → clamp to the compact floor and wrap (sub-floor
272
+ * rule at the lower floor), still compact.
273
+ * The floors live in render.ts because their derivation needs the glyph
274
+ * inventory; the branching lives here so it is pure and unit-testable.
275
+ */
276
+ export function resolveFit(fitWidth, fullFloor, compactFloor) {
277
+ const width = Math.max(fitWidth, compactFloor);
278
+ return { width, compact: width < fullFloor };
279
+ }
280
+ // ── Width resolution + the module-level fit width ────────────────────────────
281
+ let fitWidth = null;
282
+ /**
283
+ * Set the process-wide fit width (columns), resolved ONCE by the CLI driver at
284
+ * startup. `null` disables fitting (the default — every non-TTY path stays
285
+ * byte-stable). Tests set this explicitly and reset it afterward.
286
+ */
287
+ export function setFitWidth(width) {
288
+ fitWidth = width;
289
+ }
290
+ /** The current fit width, or null when fitting is disabled. */
291
+ export function getFitWidth() {
292
+ return fitWidth;
293
+ }
294
+ /**
295
+ * Resolve the fit width from the environment and stdout: `THINGS_WIDTH` wins
296
+ * when set (a positive integer forces that width; `0` disables fitting), else
297
+ * the terminal's column count when stdout is a TTY, else null (pipes, grep,
298
+ * captured output — no fitting, byte-stable). A malformed `THINGS_WIDTH` is
299
+ * ignored (falls through to the TTY/null resolution).
300
+ */
301
+ export function resolveWidth(opts) {
302
+ const raw = opts.env["THINGS_WIDTH"];
303
+ if (raw !== undefined) {
304
+ const n = Number(raw);
305
+ if (Number.isInteger(n) && n >= 0)
306
+ return n === 0 ? null : n;
307
+ // malformed — fall through to the TTY resolution
308
+ }
309
+ if (opts.isTTY && typeof opts.columns === "number" && opts.columns > 0)
310
+ return opts.columns;
311
+ return null;
312
+ }
313
+ //# sourceMappingURL=width.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"width.js","sourceRoot":"","sources":["../../src/cli/width.ts"],"names":[],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -4,14 +4,16 @@
4
4
  */
5
5
  import type { AuditWriter } from "./audit/log.ts";
6
6
  import { type ThingsApiConfig } from "./config.ts";
7
- import { type FingerprintStatus } from "./db/fingerprint.ts";
7
+ import { type ClockMeta } from "./model/clock.ts";
8
+ import { type GroupedTruncation, type Truncation } from "./contracts.ts";
9
+ import { type FingerprintStatus, type SchemaStatus } from "./db/fingerprint.ts";
8
10
  import type { AnyTask, Area, Project, Tag } from "./model/entities.ts";
9
11
  import { type AreaView } from "./read/area-view.ts";
10
12
  import { type ProjectView } from "./read/project-view.ts";
11
13
  import { type Snapshot } from "./read/snapshot.ts";
12
14
  import { type ShowTarget } from "./read/show-target.ts";
13
- import { type ChangedItem, type LiteSearchResult, type ListItem, type LogbookFilter, type SearchOptions, type SearchResultItem, type SidebarSection, type SomedayFilter, type TodayView, type UpcomingFilter, type ViewFilter } from "./read/views.ts";
14
- import type { AreaAddParams, AreaUpdateParams, ContainerRef, ProjectCancelParams, OperationKind, OperationParamsMap, ProjectAddParams, ProjectCompleteParams, ProjectUpdateParams, ReorderParams, TagAddParams, TagUpdateParams, HeadingArchiveParams, HeadingUnarchiveParams, TodoAddLoggedParams, TodoAddParams, TodoBackdateParams, TodoMoveParams, TodoUpdateParams } from "./write/operations.ts";
15
+ import { type ChangedItem, type InboxFilter, type LiteSearchResult, type ListItem, type LogbookFilter, type SearchOptions, type SearchResultItem, type SidebarSection, type SomedayFilter, type TodayFilter, type TodayView, type UpcomingFilter, type ViewFilter } from "./read/views.ts";
16
+ import type { AreaAddParams, AreaUpdateParams, ContainerRef, ProjectCancelParams, OperationKind, OperationParamsMap, ProjectAddParams, ProjectCompleteParams, ProjectCreateRepeatingParams, ProjectUpdateParams, RepeatRuleParams, ReorderParams, TagAddParams, TagUpdateParams, HeadingArchiveParams, HeadingUnarchiveParams, TodoAddLoggedParams, TodoAddParams, TodoBackdateParams, TodoMoveParams, TodoUpdateParams } from "./write/operations.ts";
15
17
  import { type MutationResult, type WriteOptions } from "./write/pipeline.ts";
16
18
  import { type BatchItemResult, type BatchOp, type BatchOptions } from "./write/batch.ts";
17
19
  import { type EnvironmentTracker } from "./write/environment.ts";
@@ -26,6 +28,12 @@ export interface OpenOptions {
26
28
  dbPath?: string;
27
29
  /** Injectable clock (tests, pinned-clock lab runs). */
28
30
  now?: () => Date;
31
+ /**
32
+ * Default consumer IANA zone for every date boundary (tests / explicit
33
+ * embedding). Overrides `THINGS_TZ` from the environment; a per-read `zone`
34
+ * still overrides this. Absent uses `THINGS_TZ`, else the host zone.
35
+ */
36
+ zone?: string;
29
37
  /** Injectable write vectors (tests: FakeVector; lab: probe vectors). */
30
38
  vectors?: WriteVector[];
31
39
  /** Env for config/state-dir resolution (tests). */
@@ -40,20 +48,109 @@ export interface OpenOptions {
40
48
  environment?: EnvironmentTracker;
41
49
  };
42
50
  }
51
+ /**
52
+ * Row cap for a bounded FLAT view (inbox/today/upcoming/logbook/trash/search/
53
+ * changes). Resolution follows resolveCap: omitted → the 50-row default,
54
+ * an explicit number caps at it, `null` or `all: true` returns every row.
55
+ */
56
+ export interface ListBound {
57
+ limit?: number | null;
58
+ all?: boolean;
59
+ }
60
+ /**
61
+ * Per-read consumer-zone override (the MCP `tz` argument). Overrides the
62
+ * process default (`THINGS_TZ` / the `OpenOptions.zone` embedding / the host)
63
+ * for THIS read only. Absent uses that default; an invalid zone is rejected by
64
+ * the calling surface before it reaches here.
65
+ */
66
+ export interface ClockScopedRead {
67
+ zone?: string;
68
+ }
69
+ /**
70
+ * Per-block caps for a bounded GROUPED view (anytime/someday) or the composite
71
+ * area card. Each omitted cap falls back to the view's own default (anytime:
72
+ * 30 per area, 3 per project; someday: 30 per area, every active-project item;
73
+ * area card: 30 per section); `null` on a cap, or `all: true`, lifts it.
74
+ */
75
+ export interface GroupedBound {
76
+ areaLimit?: number | null;
77
+ projectLimit?: number | null;
78
+ all?: boolean;
79
+ }
80
+ /** A bounded flat view: the shown rows plus the exact truncation counts. */
81
+ export interface BoundedList<T> {
82
+ items: T[];
83
+ truncation: Truncation;
84
+ }
85
+ /**
86
+ * A bounded Today view: `view` is the shown split (capped in render order —
87
+ * Today, then This Evening) and `truncation` the exact counts, including the
88
+ * per-section (`today`/`evening`) breakdown a renderer needs to stay honest.
89
+ */
90
+ export interface BoundedTodayView {
91
+ view: TodayView;
92
+ truncation: Truncation;
93
+ }
94
+ /**
95
+ * A bounded sidebar catalogue (anytime/someday): `view` is the
96
+ * per-block-capped sections and `grouped` the per-block counts (identity-
97
+ * carrying, project blocks nested under their area/loose block).
98
+ */
99
+ export interface BoundedSectionsView {
100
+ view: SidebarSection[];
101
+ grouped: GroupedTruncation;
102
+ }
103
+ /** A bounded composite area card: the per-section-capped view and the per-block counts. */
104
+ export interface BoundedAreaView {
105
+ view: AreaView;
106
+ grouped: GroupedTruncation;
107
+ }
43
108
  export interface ThingsClient {
44
109
  dbPath: string;
45
110
  config: ThingsApiConfig;
46
111
  fingerprint(): FingerprintStatus;
112
+ /**
113
+ * The read-path schema check: the cached fingerprint comparison reduced to a
114
+ * warn-or-not verdict (ok / drift / unknown-version) with detail. Reuses the
115
+ * SAME lazily-built fingerprint the write path gates on — computed at most
116
+ * once per client, so it costs nothing after the first read.
117
+ */
118
+ schemaStatus(): SchemaStatus;
119
+ /**
120
+ * The additive `meta.clock` honesty field for this client's effective clock,
121
+ * or undefined when the host clock is in force (no `THINGS_TZ`/`THINGS_NOW`
122
+ * and no per-read override). `zoneOverride` reflects a per-read zone (the MCP
123
+ * `tz` argument) so the reported `today` matches what that read computed.
124
+ */
125
+ clockMeta(zoneOverride?: string): ClockMeta | undefined;
47
126
  read: {
48
- today(filter?: ViewFilter): TodayView;
49
- inbox(filter?: ViewFilter): ListItem[];
50
- anytime(filter?: ViewFilter): SidebarSection[];
51
- upcoming(filter?: UpcomingFilter): ListItem[];
52
- someday(filter?: SomedayFilter): SidebarSection[];
53
- logbook(options?: LogbookFilter): ListItem[];
54
- trash(options?: {
55
- limit?: number | null;
56
- }): ListItem[];
127
+ /**
128
+ * The Today list (Today + This Evening split) with the sidebar badge,
129
+ * bounded to `limit` rows (default 50) counted in render order — Today
130
+ * first, then This Evening. `all`/`limit: null` returns every row; the
131
+ * `truncation` metadata carries the per-section (`today`/`evening`) counts.
132
+ */
133
+ today(options?: TodayFilter & ListBound & ClockScopedRead): BoundedTodayView;
134
+ /** Inbox captures, bounded (default 50). */
135
+ inbox(options?: InboxFilter & ListBound & ClockScopedRead): BoundedList<ListItem>;
136
+ /**
137
+ * Anytime catalogue: every area header and project row is always present;
138
+ * `areaLimit` (default 30) caps each area/loose block, `projectLimit`
139
+ * (default 3) each project block. `all` lifts both.
140
+ */
141
+ anytime(options?: ViewFilter & GroupedBound & ClockScopedRead): BoundedSectionsView;
142
+ /** Future-scheduled items in date order, bounded (default 50). */
143
+ upcoming(options?: UpcomingFilter & ListBound & ClockScopedRead): BoundedList<ListItem>;
144
+ /**
145
+ * Someday catalogue: `areaLimit` (default 30) caps each group; with
146
+ * `activeProjectItems`, `projectLimit` (default: every item) caps each
147
+ * active project's trailing child list. `all` lifts both.
148
+ */
149
+ someday(options?: SomedayFilter & GroupedBound & ClockScopedRead): BoundedSectionsView;
150
+ /** Logbook entries (most recent first), bounded (default 50). */
151
+ logbook(options?: Omit<LogbookFilter, "limit"> & ListBound & ClockScopedRead): BoundedList<ListItem>;
152
+ /** Trashed items (most recently modified first), bounded (default 50). */
153
+ trash(options?: ListBound & ClockScopedRead): BoundedList<ListItem>;
57
154
  /**
58
155
  * Projects in sidebar order. LATER (someday + future-scheduled) projects
59
156
  * are excluded by default — `later: true` appends them after the active
@@ -62,14 +159,32 @@ export interface ThingsClient {
62
159
  projects(options?: {
63
160
  areaUuid?: string;
64
161
  later?: boolean;
65
- }): Project[];
66
- /** Composite project view. Targets by uuid, unique name, or uuid prefix. */
67
- projectView(ref: string): ProjectView;
68
- /** Composite area view: direct to-dos, projects in sidebar order, later, logged. */
69
- areaView(ref: string): AreaView;
162
+ overdue?: boolean;
163
+ } & ViewFilter & ClockScopedRead): Project[];
164
+ /**
165
+ * Composite project view. Targets by uuid, unique name, or uuid prefix.
166
+ * `overdue: true` keeps only child to-dos whose own deadline is overdue
167
+ * (open, before today); the tag filters (`tags`/`untagged`) keep only the
168
+ * child to-dos carrying the tag DIRECTLY (the container semantics — tags
169
+ * inherited from this project are ignored). Any content scope collapses
170
+ * headings left with no surviving child.
171
+ */
172
+ projectView(ref: string, options?: ViewFilter & ClockScopedRead): ProjectView;
173
+ /**
174
+ * Composite area view: direct to-dos, projects in sidebar order, later,
175
+ * logged. `overdue: true` keeps only the loose to-dos AND child projects
176
+ * whose OWN deadline is overdue; the tag filters keep only the rows
177
+ * matching by their own tags — no descent into project contents. Bounded
178
+ * per section: `projectLimit`/`areaLimit` (default 30 each) cap the ACTIVE
179
+ * project-rows and direct-to-dos sections (scheduled/someday project rows
180
+ * always survive, routed to the card's later sections); `all` lifts both.
181
+ * The `grouped` metadata carries the per-section counts.
182
+ */
183
+ areaView(ref: string, options?: ViewFilter & GroupedBound & ClockScopedRead): BoundedAreaView;
70
184
  areas(): Area[];
71
185
  tags(): Tag[];
72
- search(query: string, options?: SearchOptions): SearchResultItem[];
186
+ /** Title/notes substring search, ranked, bounded (default 50). */
187
+ search(query: string, options?: SearchOptions & ClockScopedRead): BoundedList<SearchResultItem>;
73
188
  /**
74
189
  * Did-you-mean fallback: case-insensitive title-only substring match over
75
190
  * areas/projects/to-dos (open + untrashed), ordered and capped. `type`
@@ -79,11 +194,10 @@ export interface ThingsClient {
79
194
  type?: "to-do" | "project" | "area";
80
195
  limit?: number;
81
196
  }): LiteSearchResult;
82
- /** Rows created/modified since a moment — incl. trashed/logged/templates. */
197
+ /** Rows created/modified since a moment — incl. trashed/logged/templates — bounded (default 50). */
83
198
  changes(options: {
84
199
  since: Date;
85
- limit?: number | null;
86
- }): ChangedItem[];
200
+ } & ListBound & ClockScopedRead): BoundedList<ChangedItem>;
87
201
  byUuid(uuid: string): AnyTask | null;
88
202
  /**
89
203
  * Classify a loose reference (uuid, >=6-char prefix, share link, or
@@ -176,6 +290,24 @@ export interface ThingsClient {
176
290
  /** Duplicate a project INCLUDING its children; the copy's uuid is on the result. */
177
291
  duplicateProject(uuid: string, options?: WriteOptions): Promise<MutationResult>;
178
292
  deleteProject(uuid: string, options?: WriteOptions): Promise<MutationResult>;
293
+ /**
294
+ * Turn an existing project into a repeating series. Drives the local Things
295
+ * app through the Accessibility API (two-key gated: `ui.enabled` config +
296
+ * `dangerouslyDriveGui`). This REPLACES the project with a new repeating
297
+ * template (its area is kept, its schedule is normalized to Someday); the
298
+ * original's identity is gone and it cannot be undone. The new template's
299
+ * uuid is on the result. An area-less Anytime project is moved to Someday
300
+ * first — a cleanup-free intermediate step surfaced in the plan.
301
+ */
302
+ makeRepeatingProject(uuid: string, rule: Omit<RepeatRuleParams, "uuid">, options?: WriteOptions): Promise<MutationResult>;
303
+ /**
304
+ * Create a project and, in the same call, turn it into a repeating series.
305
+ * TWO operations: the project is created first (and persists even if the
306
+ * make-repeating step refuses); then it is promoted (which drives the GUI —
307
+ * two-key gated, same as makeRepeatingProject). Give an `area` to place it,
308
+ * or omit it to create in Someday. The new template's uuid is on the result.
309
+ */
310
+ createRepeatingProject(params: ProjectCreateRepeatingParams, options?: WriteOptions): Promise<MutationResult>;
179
311
  /** Replace a project's full tag set (an empty list clears all tags). */
180
312
  setProjectTags(uuid: string, tags: string[], options?: WriteOptions): Promise<MutationResult>;
181
313
  /** Merge: current project tags + new ones, then replace. */
@@ -198,9 +330,13 @@ export interface ThingsClient {
198
330
  */
199
331
  batch(ops: BatchOp[], options?: BatchOptions, onResult?: (result: BatchItemResult) => void): Promise<BatchItemResult[]>;
200
332
  /**
201
- * Undo the last N changes made through this client, newest first, by
202
- * applying the inverse change. Irreversible changes are reported as
203
- * such, never guessed at.
333
+ * Undo changes made through this client, newest first, by applying the
334
+ * inverse change. Selection: `last` trailing changes (default 1), narrowed
335
+ * to author `by` (exact actor, or `*`/undefined for all), or `txn` for one
336
+ * exact change by its `undoToken`. Irreversible changes are reported as
337
+ * such, never guessed at. An inverse is refused when the item changed
338
+ * outside things-api since (its container, status, schedule, trashed state,
339
+ * or a content field moved); `acknowledgeOutOfBandChanges` overwrites anyway.
204
340
  */
205
341
  undo(options?: UndoOptions, onItem?: (item: UndoItemResult) => void): Promise<UndoItemResult[]>;
206
342
  };