@ddtcorex/dsh-maestro-memory 1.0.1

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 (109) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +448 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +756 -0
  6. package/lib/index.d.ts +10 -0
  7. package/lib/index.d.ts.map +1 -0
  8. package/lib/index.js +627 -0
  9. package/lib/index.js.map +1 -0
  10. package/lib/memory/batch.d.ts +44 -0
  11. package/lib/memory/batch.d.ts.map +1 -0
  12. package/lib/memory/batch.js +75 -0
  13. package/lib/memory/batch.js.map +1 -0
  14. package/lib/memory/feedback.d.ts +18 -0
  15. package/lib/memory/feedback.d.ts.map +1 -0
  16. package/lib/memory/feedback.js +29 -0
  17. package/lib/memory/feedback.js.map +1 -0
  18. package/lib/memory/store.d.ts +154 -0
  19. package/lib/memory/store.d.ts.map +1 -0
  20. package/lib/memory/store.js +606 -0
  21. package/lib/memory/store.js.map +1 -0
  22. package/lib/migration/cli.d.ts +18 -0
  23. package/lib/migration/cli.d.ts.map +1 -0
  24. package/lib/migration/cli.js +143 -0
  25. package/lib/migration/cli.js.map +1 -0
  26. package/lib/migration/fixture.d.ts +45 -0
  27. package/lib/migration/fixture.d.ts.map +1 -0
  28. package/lib/migration/fixture.js +177 -0
  29. package/lib/migration/fixture.js.map +1 -0
  30. package/lib/migration/service.d.ts +75 -0
  31. package/lib/migration/service.d.ts.map +1 -0
  32. package/lib/migration/service.js +713 -0
  33. package/lib/migration/service.js.map +1 -0
  34. package/lib/prompt/snapshot.d.ts +26 -0
  35. package/lib/prompt/snapshot.d.ts.map +1 -0
  36. package/lib/prompt/snapshot.js +74 -0
  37. package/lib/prompt/snapshot.js.map +1 -0
  38. package/lib/review/queue.d.ts +72 -0
  39. package/lib/review/queue.d.ts.map +1 -0
  40. package/lib/review/queue.js +306 -0
  41. package/lib/review/queue.js.map +1 -0
  42. package/lib/skills-browser.d.ts +67 -0
  43. package/lib/skills-browser.d.ts.map +1 -0
  44. package/lib/skills-browser.js +185 -0
  45. package/lib/skills-browser.js.map +1 -0
  46. package/lib/storage/atomic-store.d.ts +211 -0
  47. package/lib/storage/atomic-store.d.ts.map +1 -0
  48. package/lib/storage/atomic-store.js +582 -0
  49. package/lib/storage/atomic-store.js.map +1 -0
  50. package/lib/storage/layout.d.ts +81 -0
  51. package/lib/storage/layout.d.ts.map +1 -0
  52. package/lib/storage/layout.js +162 -0
  53. package/lib/storage/layout.js.map +1 -0
  54. package/lib/storage/legacy-format.d.ts +104 -0
  55. package/lib/storage/legacy-format.d.ts.map +1 -0
  56. package/lib/storage/legacy-format.js +262 -0
  57. package/lib/storage/legacy-format.js.map +1 -0
  58. package/lib/sync/config.d.ts +22 -0
  59. package/lib/sync/config.d.ts.map +1 -0
  60. package/lib/sync/config.js +57 -0
  61. package/lib/sync/config.js.map +1 -0
  62. package/lib/sync/git.d.ts +97 -0
  63. package/lib/sync/git.d.ts.map +1 -0
  64. package/lib/sync/git.js +282 -0
  65. package/lib/sync/git.js.map +1 -0
  66. package/lib/sync/index.d.ts +6 -0
  67. package/lib/sync/index.d.ts.map +1 -0
  68. package/lib/sync/index.js +6 -0
  69. package/lib/sync/index.js.map +1 -0
  70. package/lib/sync/layout.d.ts +15 -0
  71. package/lib/sync/layout.d.ts.map +1 -0
  72. package/lib/sync/layout.js +33 -0
  73. package/lib/sync/layout.js.map +1 -0
  74. package/lib/sync/merge.d.ts +42 -0
  75. package/lib/sync/merge.d.ts.map +1 -0
  76. package/lib/sync/merge.js +159 -0
  77. package/lib/sync/merge.js.map +1 -0
  78. package/lib/sync/service.d.ts +70 -0
  79. package/lib/sync/service.d.ts.map +1 -0
  80. package/lib/sync/service.js +508 -0
  81. package/lib/sync/service.js.map +1 -0
  82. package/lib/todo/store.d.ts +142 -0
  83. package/lib/todo/store.d.ts.map +1 -0
  84. package/lib/todo/store.js +452 -0
  85. package/lib/todo/store.js.map +1 -0
  86. package/lib/types/client/index.d.ts +7 -0
  87. package/lib/types/client/index.d.ts.map +1 -0
  88. package/package.json +63 -0
  89. package/src/client/index.tsx +994 -0
  90. package/src/host/index.ts +591 -0
  91. package/src/host/memory/batch.ts +107 -0
  92. package/src/host/memory/feedback.ts +36 -0
  93. package/src/host/memory/store.ts +650 -0
  94. package/src/host/migration/cli.ts +139 -0
  95. package/src/host/migration/fixture.ts +218 -0
  96. package/src/host/migration/service.ts +786 -0
  97. package/src/host/prompt/snapshot.ts +95 -0
  98. package/src/host/review/queue.ts +325 -0
  99. package/src/host/skills-browser.ts +191 -0
  100. package/src/host/storage/atomic-store.ts +605 -0
  101. package/src/host/storage/layout.ts +191 -0
  102. package/src/host/storage/legacy-format.ts +308 -0
  103. package/src/host/sync/config.ts +67 -0
  104. package/src/host/sync/git.ts +280 -0
  105. package/src/host/sync/index.ts +5 -0
  106. package/src/host/sync/layout.ts +39 -0
  107. package/src/host/sync/merge.ts +188 -0
  108. package/src/host/sync/service.ts +494 -0
  109. package/src/host/todo/store.ts +480 -0
@@ -0,0 +1,480 @@
1
+ /**
2
+ * todo/store.ts — TodoStore for M3-PR-A
3
+ * Four tracks (life/work/project/daily), IDs, status/due/quadrant updates,
4
+ * overdue/today/current-project smart view (limit 8), done timestamp,
5
+ * historical daily lookup (past + expired).
6
+ *
7
+ * File format: HTML comment header + §-delimited entries, each entry:
8
+ * [YYYY-MM-DD HH:MM] [id:xxxxxxxx] [q1-q4] [due:YYYY-MM-DD] [status:...] [cat:...] [done:...]\ncontent
9
+ * Tracks:
10
+ * life TODOS-life.md
11
+ * work TODOS-work.md
12
+ * project projects/<hash>/TODOS.md (cwd-isolated)
13
+ * daily daily/YYYY-MM-DD.todo.md (separate from daily log)
14
+ */
15
+
16
+ import { randomBytes } from 'node:crypto'
17
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs'
18
+ import { dirname, join } from 'node:path'
19
+ import { resolveMemoryRoot, lifeTodoPath, workTodoPath, projectTodoPath, dailyTodoPath, maestroMetaDir } from '../storage/layout.ts'
20
+ import { parseTodoEntry, stampTodoLine, ENTRY_DELIMITER, TODO_HEADER, TODO_TARGETS, TODO_STATUSES } from '../storage/legacy-format.ts'
21
+ import { withLockSync } from '../storage/atomic-store.ts'
22
+
23
+ function isWriteBlockedSync(root: string): boolean {
24
+ try {
25
+ const p = join(maestroMetaDir(root), 'write-block.json')
26
+ if (!existsSync(p)) return false
27
+ const data = JSON.parse(readFileSync(p, 'utf8'))
28
+ return data.blocked === true
29
+ } catch {
30
+ return false
31
+ }
32
+ }
33
+
34
+ export type TodoTarget = (typeof TODO_TARGETS)[number]
35
+ export type TodoStatus = (typeof TODO_STATUSES)[number]
36
+ export type TodoQuadrant = 'q1' | 'q2' | 'q3' | 'q4'
37
+
38
+ export const DEFAULT_VIEW_LIMIT = 8
39
+
40
+ /** Local date YYYY-MM-DD (local time, not UTC) */
41
+ export function todayStamp(): string {
42
+ const d = new Date()
43
+ const mm = String(d.getMonth() + 1).padStart(2, '0')
44
+ const dd = String(d.getDate()).padStart(2, '0')
45
+ return `${d.getFullYear()}-${mm}-${dd}`
46
+ }
47
+ function nowStamp(): string {
48
+ const d = new Date()
49
+ const hh = String(d.getHours()).padStart(2, '0')
50
+ const mm = String(d.getMinutes()).padStart(2, '0')
51
+ return `${todayStamp()} ${hh}:${mm}`
52
+ }
53
+ function newId(): string {
54
+ return randomBytes(4).toString('hex')
55
+ }
56
+
57
+ export interface TodoItem {
58
+ id: string | null
59
+ time: string
60
+ quadrant: string | null
61
+ due: string | null
62
+ status: string
63
+ doneAt: string | null
64
+ cat: string | null
65
+ text: string
66
+ raw: string
67
+ target?: TodoTarget
68
+ day?: string
69
+ past?: boolean
70
+ }
71
+
72
+ export interface TodoInput {
73
+ content: string
74
+ due?: string | null
75
+ quadrant?: string | null
76
+ cat?: string | null
77
+ status?: string
78
+ }
79
+
80
+ export class TodoStore {
81
+ constructor(private readonly memoryDir: string | null = null) {}
82
+
83
+ private root(): string {
84
+ return resolveMemoryRoot(this.memoryDir)
85
+ }
86
+
87
+ /** Resolve one track's file path; project requires cwd, daily honors date */
88
+ private pathFor(target: TodoTarget, cwd?: string, date?: string): string {
89
+ if (target === 'life') return lifeTodoPath(this.root())
90
+ if (target === 'work') return workTodoPath(this.root())
91
+ if (target === 'project') {
92
+ if (!cwd) throw new Error('project todo requires cwd')
93
+ return projectTodoPath(this.root(), cwd)
94
+ }
95
+ if (target === 'daily') {
96
+ const d = date ?? todayStamp()
97
+ return dailyTodoPath(this.root(), d)
98
+ }
99
+ throw new Error(`unknown target ${target}`)
100
+ }
101
+
102
+ /** Read raw text; missing -> '' */
103
+ private readText(target: TodoTarget, cwd?: string, date?: string): string {
104
+ try {
105
+ return readFileSync(this.pathFor(target, cwd, date), 'utf8')
106
+ } catch (e: any) {
107
+ if (e?.code === 'ENOENT') return ''
108
+ throw e
109
+ }
110
+ }
111
+
112
+ /** Parse raw text into items (strip header comment) */
113
+ private parseAll(text: string): TodoItem[] {
114
+ const body = text
115
+ .replace(/^<!--[\s\S]*?-->\s*/, '')
116
+ .replace(/^\s*§\s*\n?/, '')
117
+ .trim()
118
+ if (body === '') return []
119
+ return body
120
+ .split(ENTRY_DELIMITER)
121
+ .map((entry) => entry.trim())
122
+ .filter((entry) => entry.length > 0)
123
+ .map((entry) => parseTodoEntry(entry))
124
+ .filter((item): item is NonNullable<ReturnType<typeof parseTodoEntry>> => item !== null)
125
+ .map((item) => ({ ...item } as TodoItem))
126
+ }
127
+
128
+ /** All items of one track (daily: honors date, default today) */
129
+ itemsOf(target: TodoTarget, cwd?: string, date?: string): TodoItem[] {
130
+ const items = this.parseAll(this.readText(target, cwd, date))
131
+ if (target === 'daily') {
132
+ const day = date ?? todayStamp()
133
+ for (const it of items) it.day = day
134
+ }
135
+ return items
136
+ }
137
+
138
+ /** All past daily items: every daily file before today, newest day first, each tagged day+past */
139
+ pastItemsOf(today: string = todayStamp()): TodoItem[] {
140
+ let names: string[] = []
141
+ try {
142
+ names = readdirSync(join(this.root(), 'daily'))
143
+ } catch {
144
+ return []
145
+ }
146
+ const days = names
147
+ .filter((n) => /^\d{4}-\d{2}-\d{2}\.todo\.md$/.test(n))
148
+ .map((n) => n.slice(0, 10))
149
+ .filter((day) => day < today)
150
+ .sort()
151
+ .reverse()
152
+ const all: TodoItem[] = []
153
+ for (const day of days) {
154
+ all.push(...this.itemsOf('daily', undefined, day))
155
+ }
156
+ // tag past
157
+ for (const it of all) it.past = true
158
+ return all
159
+ }
160
+
161
+ private assertNotBlocked(): void {
162
+ if (isWriteBlockedSync(this.root())) {
163
+ throw new Error(`write blocked: migration verify mismatch (see ${join(maestroMetaDir(this.root()), 'write-block.json')})`)
164
+ }
165
+ }
166
+
167
+ /** Atomically write one track's items (header + entries) under directory lock */
168
+ private write(target: TodoTarget, cwd: string | undefined, items: { raw: string }[], date?: string): void {
169
+ this.assertNotBlocked()
170
+ const p = this.pathFor(target, cwd, date)
171
+ const dir = dirname(p)
172
+ mkdirSync(dir, { recursive: true })
173
+ withLockSync(dir, () => {
174
+ const body = items.map((it) => it.raw).join(ENTRY_DELIMITER)
175
+ const text = `${TODO_HEADER}${body.length > 0 ? `\n§\n${body}\n` : ''}`
176
+ const tmp = `${p}.tmp.${process.pid}.${Date.now()}`
177
+ writeFileSync(tmp, text, 'utf8')
178
+ renameSync(tmp, p)
179
+ })
180
+ }
181
+
182
+ // -------------------------------------------------------------------------
183
+ // Back-compat minimal API (M1)
184
+ // -------------------------------------------------------------------------
185
+
186
+ /** M1 compat: list parsed entries for one track */
187
+ list(target: TodoTarget, cwd?: string, date?: string): TodoItem[] {
188
+ return this.itemsOf(target, cwd, date) as any
189
+ }
190
+
191
+ /** M1 compat: add via {content,due,quadrant,cat} */
192
+ add(target: TodoTarget, input: TodoInput, cwd?: string): { ok: true; id: string } | { ok: false; error: string } {
193
+ const res = this.addTodo(target, input.content, { quadrant: input.quadrant ?? null, due: input.due ?? null, cat: input.cat ?? null }, cwd)
194
+ if (!res.ok) return { ok: false, error: res.message }
195
+ return { ok: true, id: res.id! }
196
+ }
197
+
198
+ /** M1 smartView compat: delegates to listTodos default view for single track */
199
+ smartView(target: TodoTarget, cwd?: string): TodoItem[] {
200
+ const result = this.listTodos([target], {}, cwd)
201
+ return result.items as any
202
+ }
203
+
204
+ // -------------------------------------------------------------------------
205
+ // Full API (M3)
206
+ // -------------------------------------------------------------------------
207
+
208
+ addTodo(
209
+ target: TodoTarget,
210
+ content: string,
211
+ meta: { quadrant?: string | null; due?: string | null; cat?: string | null } = {},
212
+ cwd?: string,
213
+ ): { ok: true; message: string; id: string; target: string } | { ok: false; message: string; target: string } {
214
+ if (!TODO_TARGETS.includes(target as any)) {
215
+ return { ok: false, message: `invalid target "${target}"`, target }
216
+ }
217
+ const text = String(content ?? '').trim()
218
+ if (!text) return { ok: false, message: 'empty content', target }
219
+ const id = newId()
220
+ const raw = stampTodoLine(
221
+ {
222
+ time: nowStamp(),
223
+ id,
224
+ quadrant: meta.quadrant ?? null,
225
+ due: meta.due ?? null,
226
+ status: 'pending',
227
+ cat: meta.cat ?? null,
228
+ doneAt: null,
229
+ },
230
+ text,
231
+ )
232
+ const items = this.itemsOf(target, cwd)
233
+ items.push({ raw } as any)
234
+ try {
235
+ this.write(target, cwd, items as any)
236
+ } catch (e: any) {
237
+ return { ok: false, message: e?.message ?? String(e), target }
238
+ }
239
+ return { ok: true, message: `added to ${target} (${items.length})`, id, target }
240
+ }
241
+
242
+ findById(
243
+ target: TodoTarget | undefined,
244
+ id: string,
245
+ cwd?: string,
246
+ date?: string,
247
+ ): { target: TodoTarget; item: TodoItem; items: TodoItem[]; day?: string } | null {
248
+ const targets: TodoTarget[] = target !== undefined ? [target] : ([...TODO_TARGETS] as TodoTarget[])
249
+ for (const t of targets) {
250
+ let items: TodoItem[]
251
+ try {
252
+ if (t === 'daily' && date === undefined) {
253
+ items = [...this.itemsOf('daily', cwd, undefined), ...this.pastItemsOf()]
254
+ } else {
255
+ items = this.itemsOf(t, cwd, date)
256
+ }
257
+ } catch {
258
+ continue
259
+ }
260
+ const item = items.find((e) => e.id === id)
261
+ if (item) return { target: t, item, items, day: (item as any).day }
262
+ }
263
+ return null
264
+ }
265
+
266
+ updateTodo(
267
+ target: TodoTarget | undefined,
268
+ id: string,
269
+ patch: { status?: string; quadrant?: string | null; due?: string | null; cat?: string | null; content?: string },
270
+ cwd?: string,
271
+ date?: string,
272
+ ): { ok: boolean; message: string; target: string } {
273
+ const found = this.findById(target as any, id, cwd, date)
274
+ if (!found) {
275
+ return { ok: false, message: target ? `not found: ${id} in ${target}` : `not found: ${id}`, target: target ?? '?' }
276
+ }
277
+ const { target: t, item, day } = found
278
+ const meta = parseTodoEntry(item.raw)!
279
+ const nextStatus = patch.status ?? meta.status
280
+ if (patch.status && !TODO_STATUSES.includes(patch.status as any)) {
281
+ return { ok: false, message: `invalid status "${patch.status}"`, target: t }
282
+ }
283
+ if (patch.quadrant !== undefined && patch.quadrant !== null && !/^q[1-4]$/.test(patch.quadrant)) {
284
+ return { ok: false, message: `invalid quadrant "${patch.quadrant}"`, target: t }
285
+ }
286
+ if (patch.due !== undefined && patch.due !== null && !/^\d{4}-\d{2}-\d{2}$/.test(patch.due)) {
287
+ return { ok: false, message: `invalid due "${patch.due}"`, target: t }
288
+ }
289
+ const doneAt = nextStatus === 'done' ? (meta.doneAt ?? nowStamp()) : null
290
+ const raw = stampTodoLine(
291
+ {
292
+ time: meta.time,
293
+ id: meta.id!,
294
+ quadrant: patch.quadrant !== undefined ? (patch.quadrant as any) : meta.quadrant,
295
+ due: patch.due !== undefined ? (patch.due as any) : meta.due,
296
+ status: nextStatus,
297
+ cat: patch.cat !== undefined ? (patch.cat as any) : meta.cat,
298
+ doneAt,
299
+ },
300
+ patch.content !== undefined ? patch.content : meta.text,
301
+ )
302
+ // write back to correct file (daily may be past day)
303
+ const current = this.itemsOf(t, cwd, day)
304
+ const idx = current.findIndex((e) => e.id === id)
305
+ if (idx === -1) return { ok: false, message: 'gone', target: t }
306
+ const next = [...current]
307
+ next[idx] = { raw } as any
308
+ try {
309
+ this.write(t, cwd, next as any, day)
310
+ } catch (e: any) {
311
+ return { ok: false, message: e?.message ?? String(e), target: t }
312
+ }
313
+ return { ok: true, message: `updated ${t}`, target: t }
314
+ }
315
+
316
+ doneTodo(target: TodoTarget | undefined, id: string, cwd?: string, date?: string): { ok: boolean; message: string; target: string } {
317
+ return this.updateTodo(target as any, id, { status: 'done' }, cwd, date)
318
+ }
319
+
320
+ removeTodo(target: TodoTarget | undefined, id: string, cwd?: string, date?: string): { ok: boolean; message: string; target: string } {
321
+ const found = this.findById(target as any, id, cwd, date)
322
+ if (!found) return { ok: false, message: `not found: ${id}`, target: target ?? '?' }
323
+ const { target: t, day } = found
324
+ const current = this.itemsOf(t, cwd, day)
325
+ const next = current.filter((e) => e.id !== id)
326
+ if (next.length === current.length) return { ok: false, message: 'gone', target: t }
327
+ try {
328
+ this.write(t, cwd, next as any, day)
329
+ } catch (e: any) {
330
+ return { ok: false, message: e?.message ?? String(e), target: t }
331
+ }
332
+ return { ok: true, message: `deleted ${t}`, target: t }
333
+ }
334
+
335
+ listTodos(
336
+ targets: TodoTarget[],
337
+ options: { status?: string; quadrant?: string; due?: string; cat?: string; date?: string; all?: boolean; past?: boolean; expired?: boolean } = {},
338
+ cwd?: string,
339
+ today: string = todayStamp(),
340
+ ): { items: TodoItem[]; total: number; truncated: boolean; defaultView: boolean; hint: string | null } {
341
+ const done = (it: TodoItem) => it.status === 'done' || it.status === 'cancelled'
342
+ const isOverdue = (it: TodoItem) => it.due !== null && it.due < today && !done(it)
343
+ const isToday = (it: TodoItem) => it.due === today && !done(it)
344
+ const isPast = (it: TodoItem) => it.past === true
345
+ const isPastExpired = (it: TodoItem) => {
346
+ if (!isPast(it) || done(it)) return false
347
+ if (it.due === null) return true
348
+ return it.due < today
349
+ }
350
+ const wantStatus = (it: TodoItem) => {
351
+ if (options.status === undefined || options.status === 'all') return true
352
+ return it.status === options.status
353
+ }
354
+ const wantQuadrant = (it: TodoItem) => {
355
+ if (options.quadrant === undefined || options.quadrant === 'all') return true
356
+ return it.quadrant === options.quadrant
357
+ }
358
+ const wantDue = (it: TodoItem) => {
359
+ if (options.due === undefined || options.due === 'all') return true
360
+ if (options.due === 'overdue') return isOverdue(it)
361
+ if (options.due === 'today') return isToday(it) || isOverdue(it)
362
+ return true
363
+ }
364
+ const wantCat = (it: TodoItem) => {
365
+ if (options.cat === undefined) return true
366
+ return it.cat !== null && it.cat.toLowerCase().includes(String(options.cat).toLowerCase())
367
+ }
368
+
369
+ const all: TodoItem[] = []
370
+ const seen = new Set<string>()
371
+ for (const target of targets) {
372
+ let items: TodoItem[]
373
+ try {
374
+ items = this.itemsOf(target, cwd, options.date)
375
+ } catch {
376
+ continue
377
+ }
378
+ for (const it of items) {
379
+ if (!wantStatus(it) || !wantQuadrant(it) || !wantDue(it) || !wantCat(it)) continue
380
+ if (it.id && seen.has(it.id)) continue
381
+ if (it.id) seen.add(it.id)
382
+ all.push({ ...it, target })
383
+ }
384
+ }
385
+ if (options.past === true && targets.includes('daily')) {
386
+ for (const it of this.pastItemsOf(today)) {
387
+ if (!wantStatus(it) || !wantQuadrant(it) || !wantDue(it) || !wantCat(it)) continue
388
+ if (it.id && seen.has(it.id)) continue
389
+ if (it.id) seen.add(it.id)
390
+ const marked = { ...it, target: 'daily' as const, past: true }
391
+ if (options.expired !== true && isPastExpired(marked as any)) continue
392
+ all.push(marked as any)
393
+ }
394
+ }
395
+
396
+ const defaultView =
397
+ options.all !== true &&
398
+ options.past !== true &&
399
+ options.status === undefined &&
400
+ options.quadrant === undefined &&
401
+ options.due === undefined &&
402
+ options.cat === undefined
403
+ let selected = all
404
+ if (defaultView) {
405
+ selected = all.filter((it) => {
406
+ if (done(it)) return false
407
+ if (isOverdue(it) || isToday(it)) return true
408
+ if (it.target === 'project' && cwd) return true
409
+ if ((it.target === 'life' || it.target === 'work') && (it.quadrant === 'q1' || it.quadrant === 'q2')) return true
410
+ if (it.target === 'daily') return true
411
+ return false
412
+ })
413
+ }
414
+ const rank = (it: TodoItem) => (isPast(it) ? 9 : isOverdue(it) ? 0 : isToday(it) ? 1 : it.quadrant === 'q1' ? 2 : it.quadrant === 'q2' ? 3 : it.quadrant === 'q3' ? 4 : it.quadrant === 'q4' ? 5 : 6)
415
+ selected.sort((a, b) => {
416
+ const ra = rank(a)
417
+ const rb = rank(b)
418
+ if (ra !== rb) return ra - rb
419
+ if (isPast(a) && isPast(b)) {
420
+ const d = String(b.day ?? '').localeCompare(String(a.day ?? ''))
421
+ if (d !== 0) return d
422
+ return String(b.time).localeCompare(String(a.time))
423
+ }
424
+ const d = String(a.due ?? '').localeCompare(String(b.due ?? ''))
425
+ if (d !== 0) return d
426
+ return String(a.time).localeCompare(String(b.time))
427
+ })
428
+ const explicit = options.all === true || options.past === true
429
+ const truncated = !explicit && selected.length > DEFAULT_VIEW_LIMIT
430
+ const hint =
431
+ options.past === true && options.expired !== true
432
+ ? 'Hint: unfinished expired leftovers for past daily todos are hidden by default; to view them, call list again with expired=true'
433
+ : null
434
+ return {
435
+ items: explicit ? selected : selected.slice(0, DEFAULT_VIEW_LIMIT),
436
+ total: selected.length,
437
+ truncated,
438
+ defaultView,
439
+ hint,
440
+ }
441
+ }
442
+
443
+ formatList(
444
+ result: { items: TodoItem[]; total: number; truncated: boolean; defaultView: boolean; hint: string | null },
445
+ today: string = todayStamp(),
446
+ ): string {
447
+ const { items, total, truncated, defaultView, hint } = result
448
+ if (items.length === 0) {
449
+ const h = hint ? `\n${hint}` : ''
450
+ return defaultView
451
+ ? `Todos (default view): no unfinished todos needing attention (overdue / due today / current project / important & urgent) — all clear 🎉${h}`
452
+ : `Todos: no matching entries${h}`
453
+ }
454
+ const head = defaultView
455
+ ? `Todos (default view: overdue / due today / current project unfinished / important & urgent, up to ${DEFAULT_VIEW_LIMIT})`
456
+ : `Todos (${total}${truncated ? `, showing first ${DEFAULT_VIEW_LIMIT}` : ''})`
457
+ const lines = items.map((it) => {
458
+ const tags: string[] = [`[${it.target}]`]
459
+ if (it.past) tags.push(`[past ${it.day}]`)
460
+ if (it.quadrant) tags.push(`[${it.quadrant}]`)
461
+ if (it.due !== null) tags.push(it.due < today ? `[overdue ${it.due}]` : `[${it.due}]`)
462
+ if (it.status !== 'pending') tags.push(`[${it.status}]`)
463
+ if (it.cat) tags.push(`[${it.cat}]`)
464
+ const text = it.text.split('\n')[0]
465
+ return `- ${tags.join(' ')} ${text} (id: ${it.id})`
466
+ })
467
+ const h = hint ? `\n${hint}` : ''
468
+ return `${head}\n${lines.join('\n')}\ntag semantics: q1-q4 = quadrants (important x urgent); due = deadline; status = state; operate by id (dtodo done/update/remove <id>, may include target=).${h}`
469
+ }
470
+ }
471
+
472
+ export function resolveQuadrant(args: { quadrant?: string; important?: boolean; urgent?: boolean }): string | null {
473
+ if (typeof args.quadrant === 'string' && /^q[1-4]$/.test(args.quadrant)) return args.quadrant
474
+ const important = args.important === true
475
+ const urgent = args.urgent === true
476
+ if (important && urgent) return 'q1'
477
+ if (important && !urgent) return 'q2'
478
+ if (!important && urgent) return 'q3'
479
+ return null
480
+ }