@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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +756 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +627 -0
- package/lib/index.js.map +1 -0
- package/lib/memory/batch.d.ts +44 -0
- package/lib/memory/batch.d.ts.map +1 -0
- package/lib/memory/batch.js +75 -0
- package/lib/memory/batch.js.map +1 -0
- package/lib/memory/feedback.d.ts +18 -0
- package/lib/memory/feedback.d.ts.map +1 -0
- package/lib/memory/feedback.js +29 -0
- package/lib/memory/feedback.js.map +1 -0
- package/lib/memory/store.d.ts +154 -0
- package/lib/memory/store.d.ts.map +1 -0
- package/lib/memory/store.js +606 -0
- package/lib/memory/store.js.map +1 -0
- package/lib/migration/cli.d.ts +18 -0
- package/lib/migration/cli.d.ts.map +1 -0
- package/lib/migration/cli.js +143 -0
- package/lib/migration/cli.js.map +1 -0
- package/lib/migration/fixture.d.ts +45 -0
- package/lib/migration/fixture.d.ts.map +1 -0
- package/lib/migration/fixture.js +177 -0
- package/lib/migration/fixture.js.map +1 -0
- package/lib/migration/service.d.ts +75 -0
- package/lib/migration/service.d.ts.map +1 -0
- package/lib/migration/service.js +713 -0
- package/lib/migration/service.js.map +1 -0
- package/lib/prompt/snapshot.d.ts +26 -0
- package/lib/prompt/snapshot.d.ts.map +1 -0
- package/lib/prompt/snapshot.js +74 -0
- package/lib/prompt/snapshot.js.map +1 -0
- package/lib/review/queue.d.ts +72 -0
- package/lib/review/queue.d.ts.map +1 -0
- package/lib/review/queue.js +306 -0
- package/lib/review/queue.js.map +1 -0
- package/lib/skills-browser.d.ts +67 -0
- package/lib/skills-browser.d.ts.map +1 -0
- package/lib/skills-browser.js +185 -0
- package/lib/skills-browser.js.map +1 -0
- package/lib/storage/atomic-store.d.ts +211 -0
- package/lib/storage/atomic-store.d.ts.map +1 -0
- package/lib/storage/atomic-store.js +582 -0
- package/lib/storage/atomic-store.js.map +1 -0
- package/lib/storage/layout.d.ts +81 -0
- package/lib/storage/layout.d.ts.map +1 -0
- package/lib/storage/layout.js +162 -0
- package/lib/storage/layout.js.map +1 -0
- package/lib/storage/legacy-format.d.ts +104 -0
- package/lib/storage/legacy-format.d.ts.map +1 -0
- package/lib/storage/legacy-format.js +262 -0
- package/lib/storage/legacy-format.js.map +1 -0
- package/lib/sync/config.d.ts +22 -0
- package/lib/sync/config.d.ts.map +1 -0
- package/lib/sync/config.js +57 -0
- package/lib/sync/config.js.map +1 -0
- package/lib/sync/git.d.ts +97 -0
- package/lib/sync/git.d.ts.map +1 -0
- package/lib/sync/git.js +282 -0
- package/lib/sync/git.js.map +1 -0
- package/lib/sync/index.d.ts +6 -0
- package/lib/sync/index.d.ts.map +1 -0
- package/lib/sync/index.js +6 -0
- package/lib/sync/index.js.map +1 -0
- package/lib/sync/layout.d.ts +15 -0
- package/lib/sync/layout.d.ts.map +1 -0
- package/lib/sync/layout.js +33 -0
- package/lib/sync/layout.js.map +1 -0
- package/lib/sync/merge.d.ts +42 -0
- package/lib/sync/merge.d.ts.map +1 -0
- package/lib/sync/merge.js +159 -0
- package/lib/sync/merge.js.map +1 -0
- package/lib/sync/service.d.ts +70 -0
- package/lib/sync/service.d.ts.map +1 -0
- package/lib/sync/service.js +508 -0
- package/lib/sync/service.js.map +1 -0
- package/lib/todo/store.d.ts +142 -0
- package/lib/todo/store.d.ts.map +1 -0
- package/lib/todo/store.js +452 -0
- package/lib/todo/store.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/client/index.tsx +994 -0
- package/src/host/index.ts +591 -0
- package/src/host/memory/batch.ts +107 -0
- package/src/host/memory/feedback.ts +36 -0
- package/src/host/memory/store.ts +650 -0
- package/src/host/migration/cli.ts +139 -0
- package/src/host/migration/fixture.ts +218 -0
- package/src/host/migration/service.ts +786 -0
- package/src/host/prompt/snapshot.ts +95 -0
- package/src/host/review/queue.ts +325 -0
- package/src/host/skills-browser.ts +191 -0
- package/src/host/storage/atomic-store.ts +605 -0
- package/src/host/storage/layout.ts +191 -0
- package/src/host/storage/legacy-format.ts +308 -0
- package/src/host/sync/config.ts +67 -0
- package/src/host/sync/git.ts +280 -0
- package/src/host/sync/index.ts +5 -0
- package/src/host/sync/layout.ts +39 -0
- package/src/host/sync/merge.ts +188 -0
- package/src/host/sync/service.ts +494 -0
- 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
|
+
}
|