@estruyf/worklog-cli 0.0.0-stage → 0.2.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 (3) hide show
  1. package/README.md +114 -2
  2. package/dist/worklog.js +2909 -0
  3. package/package.json +27 -4
@@ -0,0 +1,2909 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cli/worklog.ts
4
+ import { execFileSync } from "node:child_process";
5
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
6
+ import { dirname, join, relative, sep } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+
9
+ // src/db/memoryDb.ts
10
+ var MemoryDb = class {
11
+ clients = [];
12
+ tasks = [];
13
+ worklog = [];
14
+ dayNotes = [];
15
+ checklists = [];
16
+ meetings = [];
17
+ tasksById = /* @__PURE__ */ new Map();
18
+ notesByDate = /* @__PURE__ */ new Map();
19
+ /** Replace all cached records (called by the indexer after a full parse). */
20
+ load(data) {
21
+ this.clients = [...data.clients].sort((a, b) => a.name.localeCompare(b.name));
22
+ this.tasks = data.tasks.filter((t) => t.id);
23
+ this.worklog = data.worklog;
24
+ this.dayNotes = data.dayNotes;
25
+ this.checklists = data.checklists;
26
+ this.meetings = data.meetings;
27
+ this.tasksById = new Map(this.tasks.map((t) => [t.id, t]));
28
+ this.notesByDate = new Map(this.dayNotes.map((n) => [n.date, n]));
29
+ }
30
+ reset() {
31
+ this.load({ clients: [], tasks: [], worklog: [], dayNotes: [], checklists: [], meetings: [] });
32
+ }
33
+ getClients() {
34
+ return this.clients;
35
+ }
36
+ getTask(id) {
37
+ return this.tasksById.get(id);
38
+ }
39
+ /** Direct children of a task. */
40
+ getChildren(parentId) {
41
+ return this.tasks.filter((t) => t.parentId === parentId).sort(byCreatedThenTitle);
42
+ }
43
+ /** Active (not-yet-closed) tasks for a client — closed once it has a completed date. */
44
+ openTasksForClient(clientId) {
45
+ return this.tasks.filter((t) => t.clientIds.includes(clientId) && !t.completed).sort(byCreatedThenTitle);
46
+ }
47
+ /** Archived occurrences of a recurring task, most recently completed first. */
48
+ occurrencesOf(templateId) {
49
+ return this.tasks.filter((t) => t.repeatOf === templateId).sort((a, b) => (b.completed ?? "").localeCompare(a.completed ?? ""));
50
+ }
51
+ /** Every task (open + done) — used to build the app state. */
52
+ getAllTasks() {
53
+ return [...this.tasks].sort(byCreatedThenTitle);
54
+ }
55
+ /** Every worklog entry — used to build the app state. */
56
+ getAllWorklog() {
57
+ return [...this.worklog].sort((a, b) => a.date.localeCompare(b.date) || a.clientId.localeCompare(b.clientId));
58
+ }
59
+ /** The note for one day, if it has one. */
60
+ getDayNote(date) {
61
+ return this.notesByDate.get(date);
62
+ }
63
+ /** Every day note, oldest first — used to build the app state. */
64
+ getAllDayNotes() {
65
+ return [...this.dayNotes].sort((a, b) => a.date.localeCompare(b.date));
66
+ }
67
+ /** Every checklist, by name — the order they are offered in. */
68
+ getAllChecklists() {
69
+ return [...this.checklists].sort((a, b) => a.name.localeCompare(b.name));
70
+ }
71
+ getMeeting(id) {
72
+ return this.meetings.find((m) => m.id === id);
73
+ }
74
+ /** Every meeting, most recent first — the order every list of them is read in. */
75
+ getAllMeetings() {
76
+ return [...this.meetings].sort(
77
+ (a, b) => b.date.localeCompare(a.date) || (b.time ?? "").localeCompare(a.time ?? "") || a.title.localeCompare(b.title)
78
+ );
79
+ }
80
+ };
81
+ function byCreatedThenTitle(a, b) {
82
+ return (a.created ?? "").localeCompare(b.created ?? "") || a.title.localeCompare(b.title);
83
+ }
84
+
85
+ // src/parser/taskHeading.ts
86
+ var H2 = /^##\s+/;
87
+ var META = /^-\s+[A-Za-z][\w-]*\s*:/;
88
+ var ID_META = /^-\s+id\s*:\s*(.+?)\s*$/;
89
+ function isTaskHeading(lines, index) {
90
+ if (!H2.test(lines[index])) {
91
+ return false;
92
+ }
93
+ for (let i = index + 1; i < lines.length; i++) {
94
+ if (ID_META.test(lines[i])) {
95
+ return true;
96
+ }
97
+ if (!META.test(lines[i])) {
98
+ return false;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+
104
+ // src/util/date.ts
105
+ function today() {
106
+ return toISODate(/* @__PURE__ */ new Date());
107
+ }
108
+ function toISODate(d) {
109
+ const y = d.getFullYear();
110
+ const m = String(d.getMonth() + 1).padStart(2, "0");
111
+ const day = String(d.getDate()).padStart(2, "0");
112
+ return `${y}-${m}-${day}`;
113
+ }
114
+ function monthOf(isoDate) {
115
+ return isoDate.slice(0, 7);
116
+ }
117
+ function nowStamp() {
118
+ const d = /* @__PURE__ */ new Date();
119
+ const hh = String(d.getHours()).padStart(2, "0");
120
+ const mm = String(d.getMinutes()).padStart(2, "0");
121
+ return `${toISODate(d)} ${hh}:${mm}`;
122
+ }
123
+ function parseISODate(iso) {
124
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso.trim());
125
+ if (!match) {
126
+ return void 0;
127
+ }
128
+ const y = Number(match[1]);
129
+ const m = Number(match[2]);
130
+ const d = Number(match[3]);
131
+ if (m < 1 || m > 12 || d < 1 || d > daysInMonth(y, m)) {
132
+ return void 0;
133
+ }
134
+ return { y, m, d };
135
+ }
136
+ function isValidISODate(iso) {
137
+ return parseISODate(iso) !== void 0;
138
+ }
139
+ function formatParts(p) {
140
+ return `${String(p.y).padStart(4, "0")}-${String(p.m).padStart(2, "0")}-${String(p.d).padStart(2, "0")}`;
141
+ }
142
+ function daysInMonth(y, m) {
143
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
144
+ }
145
+ function weekdayOf(iso) {
146
+ const p = parseISODate(iso);
147
+ if (!p) {
148
+ return 0;
149
+ }
150
+ return new Date(Date.UTC(p.y, p.m - 1, p.d)).getUTCDay();
151
+ }
152
+ function daysSinceEpoch(iso) {
153
+ const p = parseISODate(iso);
154
+ if (!p) {
155
+ return 0;
156
+ }
157
+ return Math.round(Date.UTC(p.y, p.m - 1, p.d) / 864e5);
158
+ }
159
+ function addDays(iso, n) {
160
+ const p = parseISODate(iso);
161
+ if (!p) {
162
+ return iso;
163
+ }
164
+ const d = new Date(Date.UTC(p.y, p.m - 1, p.d + n));
165
+ return formatParts({ y: d.getUTCFullYear(), m: d.getUTCMonth() + 1, d: d.getUTCDate() });
166
+ }
167
+ function addMonths(iso, n) {
168
+ const p = parseISODate(iso);
169
+ if (!p) {
170
+ return iso;
171
+ }
172
+ const total = p.y * 12 + (p.m - 1) + n;
173
+ const y = Math.floor(total / 12);
174
+ const m = total % 12 + 1;
175
+ return formatParts({ y, m, d: Math.min(p.d, daysInMonth(y, m)) });
176
+ }
177
+ function addYears(iso, n) {
178
+ return addMonths(iso, n * 12);
179
+ }
180
+ function withDayOfMonth(iso, day) {
181
+ const p = parseISODate(iso);
182
+ if (!p) {
183
+ return iso;
184
+ }
185
+ const max = daysInMonth(p.y, p.m);
186
+ return formatParts({ y: p.y, m: p.m, d: day === "last" ? max : Math.min(day, max) });
187
+ }
188
+ function weekIndexOf(iso) {
189
+ return Math.floor((daysSinceEpoch(iso) + 4) / 7);
190
+ }
191
+
192
+ // src/model/recurrence.ts
193
+ var WEEKDAY_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
194
+ var WEEKDAY_ALIASES = {
195
+ sun: 0,
196
+ sunday: 0,
197
+ mon: 1,
198
+ monday: 1,
199
+ tue: 2,
200
+ tues: 2,
201
+ tuesday: 2,
202
+ wed: 3,
203
+ weds: 3,
204
+ wednesday: 3,
205
+ thu: 4,
206
+ thur: 4,
207
+ thurs: 4,
208
+ thursday: 4,
209
+ fri: 5,
210
+ friday: 5,
211
+ sat: 6,
212
+ saturday: 6
213
+ };
214
+ var BUSINESS_DAYS = [1, 2, 3, 4, 5];
215
+ var MAX_STEPS = 800;
216
+ var HEAD_SUGAR = {
217
+ daily: { unit: "day", interval: 1 },
218
+ weekly: { unit: "week", interval: 1 },
219
+ monthly: { unit: "month", interval: 1 },
220
+ yearly: { unit: "year", interval: 1 },
221
+ annually: { unit: "year", interval: 1 },
222
+ weekdays: { unit: "week", interval: 1, weekdays: BUSINESS_DAYS }
223
+ };
224
+ var UNIT_WORDS = {
225
+ day: "day",
226
+ days: "day",
227
+ week: "week",
228
+ weeks: "week",
229
+ month: "month",
230
+ months: "month",
231
+ year: "year",
232
+ years: "year"
233
+ };
234
+ function parseRecurrence(input) {
235
+ const text = input.trim().toLowerCase();
236
+ if (!text) {
237
+ return void 0;
238
+ }
239
+ const [headText, specText] = splitOn(text);
240
+ const head = parseHead(headText);
241
+ if (!head) {
242
+ return void 0;
243
+ }
244
+ const rec = {
245
+ unit: head.unit,
246
+ interval: head.interval,
247
+ weekdays: head.weekdays,
248
+ anchor: "schedule"
249
+ };
250
+ if (specText !== void 0) {
251
+ if (!applySpec(rec, specText)) {
252
+ return void 0;
253
+ }
254
+ }
255
+ return rec;
256
+ }
257
+ function splitOn(text) {
258
+ const m = /^(.*?)\s+on\s+(.+)$/.exec(text);
259
+ return m ? [m[1].trim(), m[2].trim()] : [text, void 0];
260
+ }
261
+ function parseHead(text) {
262
+ const sugar = HEAD_SUGAR[text];
263
+ if (sugar) {
264
+ return { ...sugar };
265
+ }
266
+ const m = /^every\s+(?:(\d+)\s+)?([a-z]+)$/.exec(text);
267
+ if (!m) {
268
+ return void 0;
269
+ }
270
+ const unit = UNIT_WORDS[m[2]];
271
+ if (!unit) {
272
+ return void 0;
273
+ }
274
+ const interval = m[1] === void 0 ? 1 : Number(m[1]);
275
+ if (!Number.isInteger(interval) || interval < 1 || interval > 1e3) {
276
+ return void 0;
277
+ }
278
+ return { unit, interval };
279
+ }
280
+ function applySpec(rec, spec) {
281
+ switch (rec.unit) {
282
+ case "week": {
283
+ const days = spec.split(/[\s,]+/).filter(Boolean).map((name) => WEEKDAY_ALIASES[name]);
284
+ if (!days.length || days.some((d) => d === void 0)) {
285
+ return false;
286
+ }
287
+ rec.weekdays = [...new Set(days)].sort((a, b) => a - b);
288
+ return true;
289
+ }
290
+ case "month": {
291
+ const day = parseMonthDay(spec);
292
+ if (day === void 0) {
293
+ return false;
294
+ }
295
+ rec.monthDay = day;
296
+ return true;
297
+ }
298
+ case "year": {
299
+ const m = /^(\d{1,2})-(\d{1,2}|last)$/.exec(spec);
300
+ if (!m) {
301
+ return false;
302
+ }
303
+ const month = Number(m[1]);
304
+ const day = parseMonthDay(m[2]);
305
+ if (month < 1 || month > 12 || day === void 0) {
306
+ return false;
307
+ }
308
+ rec.month = month;
309
+ rec.monthDay = day;
310
+ return true;
311
+ }
312
+ default:
313
+ return false;
314
+ }
315
+ }
316
+ function parseMonthDay(spec) {
317
+ if (spec === "last") {
318
+ return "last";
319
+ }
320
+ if (!/^\d{1,2}$/.test(spec)) {
321
+ return void 0;
322
+ }
323
+ const n = Number(spec);
324
+ return n >= 1 && n <= 31 ? n : void 0;
325
+ }
326
+ function formatRecurrence(rec) {
327
+ const head = formatHead(rec);
328
+ const spec = formatSpec(rec);
329
+ return spec ? `${head} on ${spec}` : head;
330
+ }
331
+ function formatHead(rec) {
332
+ if (rec.interval === 1) {
333
+ if (rec.unit === "week" && isBusinessWeek(rec.weekdays)) {
334
+ return "weekdays";
335
+ }
336
+ return { day: "daily", week: "weekly", month: "monthly", year: "yearly" }[rec.unit];
337
+ }
338
+ return `every ${rec.interval} ${rec.unit}s`;
339
+ }
340
+ function formatSpec(rec) {
341
+ switch (rec.unit) {
342
+ case "week":
343
+ if (!rec.weekdays?.length || rec.interval === 1 && isBusinessWeek(rec.weekdays)) {
344
+ return void 0;
345
+ }
346
+ return rec.weekdays.map((d) => WEEKDAY_NAMES[d]).join(",");
347
+ case "month":
348
+ return rec.monthDay === void 0 ? void 0 : String(rec.monthDay);
349
+ case "year":
350
+ if (rec.month === void 0 || rec.monthDay === void 0) {
351
+ return void 0;
352
+ }
353
+ return `${String(rec.month).padStart(2, "0")}-${rec.monthDay === "last" ? "last" : String(rec.monthDay).padStart(2, "0")}`;
354
+ default:
355
+ return void 0;
356
+ }
357
+ }
358
+ function isBusinessWeek(weekdays) {
359
+ return !!weekdays && weekdays.length === 5 && BUSINESS_DAYS.every((d) => weekdays.includes(d));
360
+ }
361
+ function nextOccurrence(rec, base) {
362
+ if (!isValidISODate(base)) {
363
+ return void 0;
364
+ }
365
+ const next = rec.anchor === "completion" ? stepFromCompletion(rec, base) : stepInSeries(rec, base);
366
+ if (!next) {
367
+ return void 0;
368
+ }
369
+ if (rec.until && next > rec.until) {
370
+ return void 0;
371
+ }
372
+ return next;
373
+ }
374
+ function firstOccurrence(rec, from) {
375
+ if (!isValidISODate(from)) {
376
+ return void 0;
377
+ }
378
+ const namesDays = !!rec.weekdays?.length || rec.monthDay !== void 0 || rec.month !== void 0;
379
+ const start = namesDays ? nextOccurrence({ ...rec, interval: 1, anchor: "schedule", until: void 0 }, addDays(from, -1)) : from;
380
+ if (!start || rec.until && start > rec.until) {
381
+ return void 0;
382
+ }
383
+ return start;
384
+ }
385
+ function nextDueAfterCompletion(rec, currentDue, completedDate) {
386
+ const base = rec.anchor === "completion" ? completedDate : currentDue ?? completedDate;
387
+ return nextOccurrence(rec, base);
388
+ }
389
+ function stepFromCompletion(rec, base) {
390
+ let next;
391
+ switch (rec.unit) {
392
+ case "day":
393
+ next = addDays(base, rec.interval);
394
+ break;
395
+ case "week":
396
+ next = addDays(base, 7 * rec.interval);
397
+ break;
398
+ case "month":
399
+ next = addMonths(base, rec.interval);
400
+ break;
401
+ case "year":
402
+ next = addYears(base, rec.interval);
403
+ break;
404
+ }
405
+ if (rec.monthDay !== void 0) {
406
+ next = withDayOfMonth(next, rec.monthDay);
407
+ }
408
+ if (rec.weekdays?.length) {
409
+ next = rollToWeekday(next, rec.weekdays);
410
+ }
411
+ return next > base ? next : void 0;
412
+ }
413
+ function stepInSeries(rec, base) {
414
+ switch (rec.unit) {
415
+ case "day":
416
+ return addDays(base, rec.interval);
417
+ case "week":
418
+ return nextWeekly(rec, base);
419
+ case "month":
420
+ return nextMonthly(rec, base);
421
+ case "year":
422
+ return nextYearly(rec, base);
423
+ }
424
+ }
425
+ function nextWeekly(rec, base) {
426
+ if (!rec.weekdays?.length) {
427
+ return addDays(base, 7 * rec.interval);
428
+ }
429
+ const baseWeek = weekIndexOf(base);
430
+ for (let i = 1; i <= MAX_STEPS; i++) {
431
+ const cand = addDays(base, i);
432
+ if (!rec.weekdays.includes(weekdayOf(cand))) {
433
+ continue;
434
+ }
435
+ if ((weekIndexOf(cand) - baseWeek) % rec.interval === 0) {
436
+ return cand;
437
+ }
438
+ }
439
+ return void 0;
440
+ }
441
+ function nextMonthly(rec, base) {
442
+ const parts = parseISODate(base);
443
+ if (!parts) {
444
+ return void 0;
445
+ }
446
+ const day = rec.monthDay ?? parts.d;
447
+ const firstOfMonth = withDayOfMonth(base, 1);
448
+ for (let k = 0; k <= MAX_STEPS; k++) {
449
+ const cand = withDayOfMonth(addMonths(firstOfMonth, k * rec.interval), day);
450
+ if (cand > base) {
451
+ return cand;
452
+ }
453
+ }
454
+ return void 0;
455
+ }
456
+ function nextYearly(rec, base) {
457
+ const parts = parseISODate(base);
458
+ if (!parts) {
459
+ return void 0;
460
+ }
461
+ const month = rec.month ?? parts.m;
462
+ const day = rec.monthDay ?? parts.d;
463
+ for (let k = 0; k <= MAX_STEPS; k++) {
464
+ const year = parts.y + k * rec.interval;
465
+ const cand = withDayOfMonth(`${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-01`, day);
466
+ if (cand > base) {
467
+ return cand;
468
+ }
469
+ }
470
+ return void 0;
471
+ }
472
+ function rollToWeekday(iso, weekdays) {
473
+ for (let i = 0; i < 7; i++) {
474
+ const cand = addDays(iso, i);
475
+ if (weekdays.includes(weekdayOf(cand))) {
476
+ return cand;
477
+ }
478
+ }
479
+ return iso;
480
+ }
481
+
482
+ // src/parser/taskParser.ts
483
+ var H1 = /^#\s+(.*)$/;
484
+ var H22 = /^##\s+(.*)$/;
485
+ var META2 = /^-\s+([A-Za-z][\w-]*)\s*:\s*(.*)$/;
486
+ var NOTES_HEADING = /^###\s+Notes\s*$/;
487
+ var NOTE_ENTRY = /^-\s+(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2})?)\s+—\s+(.*)$/;
488
+ var PROMPTS_HEADING = /^###\s+Prompts\s*$/;
489
+ var PROMPT_ENTRY = /^-\s+\[([ xX])\]\s*(.*)$/;
490
+ var PROMPT_RAN = /^(\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2})?)\s+—\s+(.*)$/;
491
+ var CHECKLIST_HEADING = /^###\s+Checklist\s*$/;
492
+ var CHECKLIST_ENTRY = /^-\s+\[([ xX])\]\s*(.*)$/;
493
+ function parseTaskFile(content, sourceFile, clientId) {
494
+ const lines = content.split(/\r?\n/);
495
+ const result = { tasks: [] };
496
+ let current;
497
+ let inMeta = false;
498
+ const flush = () => {
499
+ if (!current) {
500
+ return;
501
+ }
502
+ const t = current.task;
503
+ const { description, checklist, prompts, notes } = splitBody(current.descLines);
504
+ result.tasks.push({
505
+ id: t.id ?? "",
506
+ title: (t.title ?? "").trim(),
507
+ description,
508
+ status: t.status ?? "open",
509
+ priority: t.priority,
510
+ parentId: t.parentId,
511
+ clientIds: t.clientIds && t.clientIds.length ? t.clientIds : [clientId],
512
+ links: t.links,
513
+ attachments: t.attachments,
514
+ created: t.created,
515
+ due: t.due,
516
+ completed: t.completed,
517
+ repeat: buildRecurrence(t.repeatMeta),
518
+ lastDone: t.lastDone,
519
+ repeatOf: t.repeatOf,
520
+ workedOn: t.workedOn,
521
+ tags: t.tags,
522
+ checklist,
523
+ prompts,
524
+ notes,
525
+ sourceFile,
526
+ sourceLine: current.line
527
+ });
528
+ current = void 0;
529
+ };
530
+ for (let i = 0; i < lines.length; i++) {
531
+ const line = lines[i];
532
+ const h2 = isTaskHeading(lines, i) ? H22.exec(line) : null;
533
+ if (h2) {
534
+ flush();
535
+ current = {
536
+ task: { title: h2[1], links: [], clientIds: [] },
537
+ descLines: [],
538
+ line: i
539
+ };
540
+ inMeta = true;
541
+ continue;
542
+ }
543
+ if (!current) {
544
+ const h1 = H1.exec(line);
545
+ if (h1 && result.displayName === void 0) {
546
+ result.displayName = h1[1].trim();
547
+ }
548
+ continue;
549
+ }
550
+ if (inMeta) {
551
+ const meta = META2.exec(line);
552
+ if (meta) {
553
+ applyMeta(current.task, meta[1].toLowerCase(), meta[2].trim());
554
+ continue;
555
+ }
556
+ inMeta = false;
557
+ if (line.trim() === "") {
558
+ continue;
559
+ }
560
+ }
561
+ current.descLines.push(line);
562
+ }
563
+ flush();
564
+ return result;
565
+ }
566
+ function buildRecurrence(meta) {
567
+ if (!meta?.expr) {
568
+ return void 0;
569
+ }
570
+ const rec = parseRecurrence(meta.expr);
571
+ if (!rec) {
572
+ return void 0;
573
+ }
574
+ const anchor = parseAnchor(meta.from);
575
+ if (anchor) {
576
+ rec.anchor = anchor;
577
+ }
578
+ if (meta.until) {
579
+ rec.until = meta.until;
580
+ }
581
+ return rec;
582
+ }
583
+ function parseAnchor(value) {
584
+ switch (value?.trim().toLowerCase()) {
585
+ case "completion":
586
+ case "completed":
587
+ case "done":
588
+ return "completion";
589
+ case "schedule":
590
+ case "due":
591
+ return "schedule";
592
+ default:
593
+ return void 0;
594
+ }
595
+ }
596
+ function repeatMeta(task) {
597
+ return task.repeatMeta ??= {};
598
+ }
599
+ function applyMeta(task, key, value) {
600
+ switch (key) {
601
+ case "id":
602
+ task.id = value;
603
+ break;
604
+ case "status":
605
+ task.status = value || "open";
606
+ break;
607
+ case "priority":
608
+ task.priority = value || void 0;
609
+ break;
610
+ case "parent":
611
+ task.parentId = value || void 0;
612
+ break;
613
+ case "link": {
614
+ const link = parseLink(value);
615
+ if (link) {
616
+ task.links.push(link);
617
+ }
618
+ break;
619
+ }
620
+ case "attachment":
621
+ if (value) {
622
+ (task.attachments ??= []).push(value);
623
+ }
624
+ break;
625
+ case "created":
626
+ task.created = value || void 0;
627
+ break;
628
+ case "due":
629
+ task.due = value || void 0;
630
+ break;
631
+ case "completed":
632
+ task.completed = value || void 0;
633
+ break;
634
+ case "repeat":
635
+ repeatMeta(task).expr = value || void 0;
636
+ break;
637
+ case "repeatfrom":
638
+ repeatMeta(task).from = value || void 0;
639
+ break;
640
+ case "repeatuntil":
641
+ repeatMeta(task).until = value || void 0;
642
+ break;
643
+ case "lastdone":
644
+ task.lastDone = value || void 0;
645
+ break;
646
+ case "repeatof":
647
+ task.repeatOf = value || void 0;
648
+ break;
649
+ case "tags": {
650
+ const tags = value.split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
651
+ task.tags = tags.length ? tags : void 0;
652
+ break;
653
+ }
654
+ case "worked": {
655
+ if (!value) {
656
+ break;
657
+ }
658
+ const dates = value.split(/[\s,]+/).map((d) => d.trim()).filter(Boolean);
659
+ if (dates.length === 0) {
660
+ break;
661
+ }
662
+ const all = /* @__PURE__ */ new Set([...task.workedOn ?? [], ...dates]);
663
+ task.workedOn = [...all].sort();
664
+ break;
665
+ }
666
+ case "client":
667
+ task.clientIds = value.split(/[,\s]+/).filter(Boolean);
668
+ break;
669
+ default:
670
+ break;
671
+ }
672
+ }
673
+ function parseLink(value) {
674
+ const trimmed = value.trim();
675
+ if (!trimmed) {
676
+ return void 0;
677
+ }
678
+ const spaceIdx = trimmed.search(/\s/);
679
+ if (spaceIdx === -1) {
680
+ return { url: trimmed };
681
+ }
682
+ return {
683
+ url: trimmed.slice(0, spaceIdx),
684
+ label: trimmed.slice(spaceIdx + 1).trim() || void 0
685
+ };
686
+ }
687
+ function splitBody(lines) {
688
+ const marks = [];
689
+ lines.forEach((line, at) => {
690
+ if (PROMPTS_HEADING.test(line)) {
691
+ marks.push({ at, kind: "prompts" });
692
+ } else if (NOTES_HEADING.test(line)) {
693
+ marks.push({ at, kind: "notes" });
694
+ } else if (CHECKLIST_HEADING.test(line)) {
695
+ marks.push({ at, kind: "checklist" });
696
+ }
697
+ });
698
+ if (marks.length === 0) {
699
+ const description2 = lines.join("\n").trim();
700
+ return { description: description2 || void 0 };
701
+ }
702
+ const description = lines.slice(0, marks[0].at).join("\n").trim();
703
+ const checklist = [];
704
+ const prompts = [];
705
+ const notes = [];
706
+ marks.forEach((mark, i) => {
707
+ const body = lines.slice(mark.at + 1, marks[i + 1]?.at ?? lines.length);
708
+ if (mark.kind === "prompts") {
709
+ prompts.push(...parsePrompts(body));
710
+ } else if (mark.kind === "checklist") {
711
+ checklist.push(...parseChecklist(body));
712
+ } else {
713
+ notes.push(...parseNotes(body));
714
+ }
715
+ });
716
+ return {
717
+ description: description || void 0,
718
+ checklist: checklist.length ? checklist : void 0,
719
+ prompts: prompts.length ? prompts : void 0,
720
+ notes: notes.length ? notes : void 0
721
+ };
722
+ }
723
+ function parseChecklist(lines) {
724
+ const items = [];
725
+ for (const line of lines) {
726
+ const m = CHECKLIST_ENTRY.exec(line);
727
+ const text = m?.[2].trim();
728
+ if (m && text) {
729
+ items.push({ text, done: m[1].toLowerCase() === "x" });
730
+ }
731
+ }
732
+ return items;
733
+ }
734
+ function parsePrompts(lines) {
735
+ const prompts = [];
736
+ let current;
737
+ const flush = () => {
738
+ if (current) {
739
+ prompts.push({
740
+ title: current.title.trim(),
741
+ text: current.text.join("\n").trim(),
742
+ ran: current.ran || void 0,
743
+ ranAt: current.ranAt
744
+ });
745
+ current = void 0;
746
+ }
747
+ };
748
+ for (const line of lines) {
749
+ const m = PROMPT_ENTRY.exec(line);
750
+ if (m) {
751
+ flush();
752
+ const ran = m[1].toLowerCase() === "x";
753
+ const stamped = ran ? PROMPT_RAN.exec(m[2]) : null;
754
+ current = {
755
+ title: stamped ? stamped[2] : m[2],
756
+ ran,
757
+ ranAt: stamped ? stamped[1].replace("T", " ") : void 0,
758
+ text: []
759
+ };
760
+ } else if (current) {
761
+ current.text.push(line.replace(/^ {1,2}/, ""));
762
+ }
763
+ }
764
+ flush();
765
+ return prompts;
766
+ }
767
+ function parseNotes(lines) {
768
+ const notes = [];
769
+ let current;
770
+ const flush = () => {
771
+ if (current) {
772
+ notes.push({
773
+ timestamp: current.timestamp,
774
+ text: current.text.join("\n").trim()
775
+ });
776
+ current = void 0;
777
+ }
778
+ };
779
+ for (const line of lines) {
780
+ const m = NOTE_ENTRY.exec(line);
781
+ if (m) {
782
+ flush();
783
+ current = { timestamp: m[1].replace("T", " "), text: [m[2]] };
784
+ } else if (current) {
785
+ current.text.push(line.replace(/^ {1,2}/, ""));
786
+ }
787
+ }
788
+ flush();
789
+ return notes;
790
+ }
791
+ function serializePrompt(prompt) {
792
+ const stamp = prompt.ran && prompt.ranAt ? `${prompt.ranAt} \u2014 ` : "";
793
+ const head = `- [${prompt.ran ? "x" : " "}] ${stamp}${prompt.title}`;
794
+ if (!prompt.text.trim()) {
795
+ return head;
796
+ }
797
+ const body = prompt.text.split(/\r?\n/).map((l) => l.trim() ? ` ${l}` : "");
798
+ return [head, ...body].join("\n");
799
+ }
800
+ function serializeChecklistItem(item) {
801
+ return `- [${item.done ? "x" : " "}] ${item.text}`;
802
+ }
803
+ function serializeNote(note) {
804
+ const [first = "", ...rest] = note.text.split(/\r?\n/);
805
+ const head = `- ${note.timestamp} \u2014 ${first}`;
806
+ const cont = rest.map((l) => l.trim() ? ` ${l}` : "");
807
+ return [head, ...cont].join("\n");
808
+ }
809
+ function serializeTask(task, clientId) {
810
+ const out = [];
811
+ out.push(`## ${task.title}`);
812
+ out.push(`- id: ${task.id}`);
813
+ out.push(`- status: ${task.status}`);
814
+ if (task.priority) {
815
+ out.push(`- priority: ${task.priority}`);
816
+ }
817
+ if (task.parentId) {
818
+ out.push(`- parent: ${task.parentId}`);
819
+ }
820
+ if (clientId && (task.clientIds.length > 1 || task.clientIds.length === 1 && task.clientIds[0] !== clientId)) {
821
+ out.push(`- client: ${task.clientIds.join(" ")}`);
822
+ }
823
+ for (const link of task.links) {
824
+ out.push(`- link: ${link.label ? `${link.url} ${link.label}` : link.url}`);
825
+ }
826
+ for (const ref of task.attachments ?? []) {
827
+ out.push(`- attachment: ${ref}`);
828
+ }
829
+ if (task.created) {
830
+ out.push(`- created: ${task.created}`);
831
+ }
832
+ if (task.due) {
833
+ out.push(`- due: ${task.due}`);
834
+ }
835
+ if (task.completed) {
836
+ out.push(`- completed: ${task.completed}`);
837
+ }
838
+ if (task.repeat) {
839
+ out.push(`- repeat: ${formatRecurrence(task.repeat)}`);
840
+ if (task.repeat.anchor === "completion") {
841
+ out.push(`- repeatFrom: completion`);
842
+ }
843
+ if (task.repeat.until) {
844
+ out.push(`- repeatUntil: ${task.repeat.until}`);
845
+ }
846
+ }
847
+ if (task.lastDone) {
848
+ out.push(`- lastDone: ${task.lastDone}`);
849
+ }
850
+ if (task.repeatOf) {
851
+ out.push(`- repeatOf: ${task.repeatOf}`);
852
+ }
853
+ for (const worked of task.workedOn ?? []) {
854
+ out.push(`- worked: ${worked}`);
855
+ }
856
+ if (task.tags && task.tags.length) {
857
+ out.push(`- tags: ${task.tags.join(", ")}`);
858
+ }
859
+ const body = [];
860
+ if (task.description && task.description.trim()) {
861
+ body.push(task.description.trim());
862
+ }
863
+ if (task.checklist && task.checklist.length) {
864
+ body.push(["### Checklist", ...task.checklist.map(serializeChecklistItem)].join("\n"));
865
+ }
866
+ if (task.prompts && task.prompts.length) {
867
+ body.push(`### Prompts
868
+ ${task.prompts.map(serializePrompt).join("\n\n")}`);
869
+ }
870
+ if (task.notes && task.notes.length) {
871
+ body.push(["### Notes", ...task.notes.map(serializeNote)].join("\n"));
872
+ }
873
+ if (body.length) {
874
+ out.push("");
875
+ out.push(body.join("\n\n"));
876
+ }
877
+ return out.join("\n");
878
+ }
879
+
880
+ // src/parser/worklogParser.ts
881
+ var LINE = /^-\s+(\d{4}-\d{2}-\d{2})\s+(\S+)\s+([\d.]+)\s*(?:[—–]|-{1,2}|:)?\s*(.*)$/;
882
+ function parseWorklogFile(content, sourceFile) {
883
+ const lines = content.split(/\r?\n/);
884
+ const entries = [];
885
+ for (let i = 0; i < lines.length; i++) {
886
+ const m = LINE.exec(lines[i].trim());
887
+ if (!m) {
888
+ continue;
889
+ }
890
+ const hours = Number(m[3]);
891
+ if (!isFinite(hours)) {
892
+ continue;
893
+ }
894
+ const note = m[4]?.trim();
895
+ entries.push({
896
+ date: m[1],
897
+ clientId: m[2],
898
+ hours,
899
+ note: note ? note : void 0,
900
+ sourceFile,
901
+ sourceLine: i
902
+ });
903
+ }
904
+ return entries;
905
+ }
906
+ function serializeWorklogEntry(entry) {
907
+ const base = `- ${entry.date} ${entry.clientId} ${formatHours(entry.hours)}`;
908
+ return entry.note ? `${base} \u2014 ${entry.note}` : base;
909
+ }
910
+ function formatHours(hours) {
911
+ return Number.isInteger(hours) ? String(hours) : String(hours);
912
+ }
913
+
914
+ // src/parser/dayNotes.ts
915
+ var DAY_HEADING = /^##[ \t]+(\d{4}-\d{2}-\d{2})[ \t]*$/;
916
+ function splitDayNoteBlocks(content) {
917
+ const lines = content.split(/\r?\n/);
918
+ const starts = [];
919
+ for (let i = 0; i < lines.length; i++) {
920
+ if (DAY_HEADING.test(lines[i])) {
921
+ starts.push(i);
922
+ }
923
+ }
924
+ const header = lines.slice(0, starts.length ? starts[0] : lines.length).join("\n").replace(/\s+$/, "");
925
+ const blocks = [];
926
+ for (let b = 0; b < starts.length; b++) {
927
+ const end = b + 1 < starts.length ? starts[b + 1] : lines.length;
928
+ const date = DAY_HEADING.exec(lines[starts[b]])[1];
929
+ blocks.push({ date, text: bodyOf(lines.slice(starts[b] + 1, end)), line: starts[b] });
930
+ }
931
+ return { header, blocks };
932
+ }
933
+ function parseDayNotesFile(content, sourceFile) {
934
+ const byDate = /* @__PURE__ */ new Map();
935
+ for (const b of splitDayNoteBlocks(content).blocks) {
936
+ if (b.text !== "") {
937
+ byDate.set(b.date, { date: b.date, body: b.text, sourceFile, sourceLine: b.line });
938
+ }
939
+ }
940
+ return [...byDate.values()];
941
+ }
942
+ function bodyOf(lines) {
943
+ let start = 0;
944
+ while (start < lines.length && lines[start].trim() === "") {
945
+ start++;
946
+ }
947
+ return lines.slice(start).join("\n").replace(/\s+$/, "");
948
+ }
949
+
950
+ // src/parser/checklistParser.ts
951
+ var ITEM = /^(\s*)([-*+])\s+\[([ xX])\]\s?(.*)$/;
952
+ var H12 = /^#[ \t]+(.*)$/;
953
+ var H23 = /^##[ \t]+(.*)$/;
954
+ var LAST_RUN = /^-[ \t]+last run:[ \t]*(\d{4}-\d{2}-\d{2})[ \t]*$/;
955
+ function parseChecklistFile(content, sourceFile, id) {
956
+ const lines = content.split(/\r?\n/);
957
+ let name = "";
958
+ let lastRun;
959
+ const sections = [{ items: [] }];
960
+ for (let i = 0; i < lines.length; i++) {
961
+ const line = lines[i];
962
+ const item = ITEM.exec(line);
963
+ if (item) {
964
+ sections[sections.length - 1].items.push({
965
+ text: item[4].trim(),
966
+ done: item[3] !== " ",
967
+ line: i
968
+ });
969
+ continue;
970
+ }
971
+ const h2 = H23.exec(line);
972
+ if (h2) {
973
+ sections.push({ title: h2[1].trim(), line: i, items: [] });
974
+ continue;
975
+ }
976
+ const h1 = H12.exec(line);
977
+ if (h1 && !name) {
978
+ name = h1[1].trim();
979
+ continue;
980
+ }
981
+ const run2 = LAST_RUN.exec(line);
982
+ if (run2 && !lastRun) {
983
+ lastRun = run2[1];
984
+ }
985
+ }
986
+ const kept = sections.filter((s, i) => i > 0 || s.items.length > 0);
987
+ return {
988
+ id,
989
+ name: name || id,
990
+ lastRun,
991
+ sections: kept.length ? kept : [sections[0]],
992
+ sourceFile
993
+ };
994
+ }
995
+
996
+ // src/parser/blocks.ts
997
+ var ID_META2 = /^-\s+id\s*:\s*(.+?)\s*$/;
998
+ function findBlockRange(content, taskId) {
999
+ const lines = content.split(/\r?\n/);
1000
+ let blockStart = -1;
1001
+ let matched = false;
1002
+ const ranges = [];
1003
+ for (let i = 0; i < lines.length; i++) {
1004
+ if (isTaskHeading(lines, i)) {
1005
+ if (blockStart !== -1) {
1006
+ ranges.push({ start: blockStart, end: i, matched });
1007
+ }
1008
+ blockStart = i;
1009
+ matched = false;
1010
+ } else if (blockStart !== -1) {
1011
+ const m = ID_META2.exec(lines[i]);
1012
+ if (m && m[1] === taskId) {
1013
+ matched = true;
1014
+ }
1015
+ }
1016
+ }
1017
+ if (blockStart !== -1) {
1018
+ ranges.push({ start: blockStart, end: lines.length, matched });
1019
+ }
1020
+ const hit = ranges.find((r) => r.matched);
1021
+ return hit ? { start: hit.start, end: hit.end } : void 0;
1022
+ }
1023
+ function replaceBlock(content, taskId, newBlock) {
1024
+ const range = findBlockRange(content, taskId);
1025
+ if (!range) {
1026
+ return void 0;
1027
+ }
1028
+ const lines = content.split(/\r?\n/);
1029
+ const original = lines.slice(range.start, range.end);
1030
+ const hadTrailingBlank = original.length > 0 && original[original.length - 1].trim() === "";
1031
+ const replacement = newBlock.split(/\r?\n/);
1032
+ if (hadTrailingBlank) {
1033
+ replacement.push("");
1034
+ }
1035
+ const next = [...lines.slice(0, range.start), ...replacement, ...lines.slice(range.end)];
1036
+ return next.join("\n").replace(/\n{3,}/g, "\n\n");
1037
+ }
1038
+ function extractBlock(content, taskId) {
1039
+ const range = findBlockRange(content, taskId);
1040
+ if (!range) {
1041
+ return void 0;
1042
+ }
1043
+ const lines = content.split(/\r?\n/);
1044
+ const blockLines = lines.slice(range.start, range.end);
1045
+ while (blockLines.length && blockLines[blockLines.length - 1].trim() === "") {
1046
+ blockLines.pop();
1047
+ }
1048
+ const before = lines.slice(0, range.start);
1049
+ const after = lines.slice(range.end);
1050
+ while (before.length && before[before.length - 1].trim() === "") {
1051
+ before.pop();
1052
+ }
1053
+ const remainderLines = [...before];
1054
+ if (before.length && after.some((l) => l.trim() !== "")) {
1055
+ remainderLines.push("");
1056
+ }
1057
+ remainderLines.push(...after);
1058
+ let remainder = remainderLines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, "");
1059
+ if (remainder) {
1060
+ remainder += "\n";
1061
+ }
1062
+ return { block: blockLines.join("\n"), remainder, startLine: range.start };
1063
+ }
1064
+
1065
+ // src/parser/meetingParser.ts
1066
+ var H24 = /^##\s+(.*)$/;
1067
+ var META3 = /^-\s+([A-Za-z][\w-]*)\s*:\s*(.*)$/;
1068
+ var ACTIONS_HEADING = /^###\s+Action items\s*$/i;
1069
+ var ACTION_ENTRY = /^-\s+\[([ xX])\]\s*(.*)$/;
1070
+ var ACTION_TASK = /^(.*?)\s+→\s+(t_[A-Za-z0-9]+)$/;
1071
+ var TIME = /^(\d{1,2}):(\d{2})$/;
1072
+ var DURATION = /^(?:(\d+(?:\.\d+)?)\s*h)?\s*(?:(\d+)\s*m(?:in)?)?$/i;
1073
+ function parseDuration(value) {
1074
+ const text = value.trim();
1075
+ if (/^\d+$/.test(text)) {
1076
+ const n = Number(text);
1077
+ return n > 0 ? n : void 0;
1078
+ }
1079
+ const m = DURATION.exec(text);
1080
+ if (!m || !m[1] && !m[2]) {
1081
+ return void 0;
1082
+ }
1083
+ const minutes = Math.round(Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0));
1084
+ return minutes > 0 ? minutes : void 0;
1085
+ }
1086
+ function normalizeTime(value) {
1087
+ const m = TIME.exec(value.trim());
1088
+ if (!m || Number(m[1]) > 23 || Number(m[2]) > 59) {
1089
+ return void 0;
1090
+ }
1091
+ return `${m[1].padStart(2, "0")}:${m[2]}`;
1092
+ }
1093
+ function parseMeetingFile(content, sourceFile) {
1094
+ const lines = content.split(/\r?\n/);
1095
+ const starts = [];
1096
+ for (let i = 0; i < lines.length; i++) {
1097
+ if (isTaskHeading(lines, i)) {
1098
+ starts.push(i);
1099
+ }
1100
+ }
1101
+ const meetings = [];
1102
+ for (let b = 0; b < starts.length; b++) {
1103
+ const end = b + 1 < starts.length ? starts[b + 1] : lines.length;
1104
+ const meeting = parseBlock(lines.slice(starts[b], end), sourceFile, starts[b]);
1105
+ if (meeting.id) {
1106
+ meetings.push(meeting);
1107
+ }
1108
+ }
1109
+ return meetings;
1110
+ }
1111
+ function parseBlock(lines, sourceFile, line) {
1112
+ const meeting = {
1113
+ id: "",
1114
+ title: (H24.exec(lines[0])?.[1] ?? "").trim(),
1115
+ date: "",
1116
+ people: [],
1117
+ notes: "",
1118
+ actions: [],
1119
+ sourceFile,
1120
+ sourceLine: line
1121
+ };
1122
+ let i = 1;
1123
+ for (; i < lines.length; i++) {
1124
+ const meta = META3.exec(lines[i]);
1125
+ if (!meta) {
1126
+ break;
1127
+ }
1128
+ const value = meta[2].trim();
1129
+ switch (meta[1].toLowerCase()) {
1130
+ case "id":
1131
+ meeting.id = value;
1132
+ break;
1133
+ case "date":
1134
+ meeting.date = value;
1135
+ break;
1136
+ case "time":
1137
+ meeting.time = normalizeTime(value) ?? meeting.time;
1138
+ break;
1139
+ case "duration":
1140
+ meeting.duration = parseDuration(value) ?? meeting.duration;
1141
+ break;
1142
+ case "client":
1143
+ meeting.clientId = value || void 0;
1144
+ break;
1145
+ case "people":
1146
+ meeting.people = splitPeople(value);
1147
+ break;
1148
+ }
1149
+ }
1150
+ const body = trimBlank(lines.slice(i));
1151
+ const at = actionsStart(body);
1152
+ if (at < 0) {
1153
+ meeting.notes = body.join("\n");
1154
+ } else {
1155
+ meeting.notes = trimBlank(body.slice(0, at)).join("\n");
1156
+ meeting.actions = body.slice(at + 1).filter((l) => l.trim() !== "").map(parseAction);
1157
+ }
1158
+ return meeting;
1159
+ }
1160
+ function actionsStart(body) {
1161
+ for (let i = body.length - 1; i >= 0; i--) {
1162
+ if (ACTIONS_HEADING.test(body[i])) {
1163
+ return body.slice(i + 1).every((l) => l.trim() === "" || ACTION_ENTRY.test(l)) ? i : -1;
1164
+ }
1165
+ }
1166
+ return -1;
1167
+ }
1168
+ function parseAction(line) {
1169
+ const m = ACTION_ENTRY.exec(line);
1170
+ const done = m[1] !== " ";
1171
+ const linked = ACTION_TASK.exec(m[2].trim());
1172
+ return linked ? { text: linked[1].trim(), done, taskId: linked[2] } : { text: m[2].trim(), done };
1173
+ }
1174
+ function splitPeople(value) {
1175
+ const seen = /* @__PURE__ */ new Set();
1176
+ const out = [];
1177
+ for (const raw of value.split(",")) {
1178
+ const name = raw.trim();
1179
+ if (name && !seen.has(name.toLowerCase())) {
1180
+ seen.add(name.toLowerCase());
1181
+ out.push(name);
1182
+ }
1183
+ }
1184
+ return out;
1185
+ }
1186
+ function trimBlank(lines) {
1187
+ let start = 0;
1188
+ let end = lines.length;
1189
+ while (start < end && lines[start].trim() === "") {
1190
+ start++;
1191
+ }
1192
+ while (end > start && lines[end - 1].trim() === "") {
1193
+ end--;
1194
+ }
1195
+ return lines.slice(start, end);
1196
+ }
1197
+
1198
+ // src/model/status.ts
1199
+ var DEFAULT_STATUSES = [
1200
+ { id: "open", label: "Open" },
1201
+ { id: "in-progress", label: "In progress" },
1202
+ { id: "done", label: "Closed", terminal: true }
1203
+ ];
1204
+ var MAX_STATUS_LABEL = 20;
1205
+ var HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
1206
+ function parseStatusColor(color) {
1207
+ const value = typeof color === "string" ? color.trim() : "";
1208
+ return HEX_COLOR.test(value) ? value : void 0;
1209
+ }
1210
+ function normalizeStatuses(input) {
1211
+ const defs = [];
1212
+ const seen = /* @__PURE__ */ new Set();
1213
+ for (const raw of Array.isArray(input) ? input : []) {
1214
+ if (!raw || typeof raw !== "object") {
1215
+ continue;
1216
+ }
1217
+ const candidate = raw;
1218
+ const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
1219
+ const label2 = typeof candidate.label === "string" ? candidate.label.trim() : "";
1220
+ if (!id || !label2 || seen.has(id)) {
1221
+ continue;
1222
+ }
1223
+ seen.add(id);
1224
+ defs.push({
1225
+ id,
1226
+ label: label2.slice(0, MAX_STATUS_LABEL),
1227
+ terminal: candidate.terminal === true || void 0,
1228
+ color: parseStatusColor(candidate.color)
1229
+ });
1230
+ }
1231
+ if (defs.length === 0) {
1232
+ return DEFAULT_STATUSES.map((s) => ({ ...s }));
1233
+ }
1234
+ const flagged = defs.findIndex((s) => s.terminal);
1235
+ const terminalIndex = flagged >= 0 ? flagged : defs.length - 1;
1236
+ const terminal = defs[terminalIndex];
1237
+ const working = defs.filter((_, i) => i !== terminalIndex);
1238
+ if (working.length === 0) {
1239
+ const fallback = DEFAULT_STATUSES.find((s) => !s.terminal && s.id !== terminal.id) ?? { id: "open", label: "Open" };
1240
+ working.push({ id: fallback.id, label: fallback.label });
1241
+ }
1242
+ return [
1243
+ ...working.map((s) => ({ ...s, terminal: void 0 })),
1244
+ { ...terminal, terminal: true }
1245
+ ];
1246
+ }
1247
+ function terminalStatusId(statuses) {
1248
+ return (statuses.find((s) => s.terminal) ?? statuses[statuses.length - 1])?.id ?? "done";
1249
+ }
1250
+ function isTerminalStatus(statuses, id) {
1251
+ return statuses.find((s) => s.id === id)?.terminal === true;
1252
+ }
1253
+ function openStatusId(statuses) {
1254
+ return (statuses.find((s) => !s.terminal) ?? statuses[0])?.id ?? "open";
1255
+ }
1256
+ function inProgressStatusId(statuses) {
1257
+ const explicit = statuses.find((s) => s.id === "in-progress");
1258
+ if (explicit) {
1259
+ return explicit.id;
1260
+ }
1261
+ return statuses.filter((s) => !s.terminal)[1]?.id;
1262
+ }
1263
+
1264
+ // src/model/aiAgents.ts
1265
+ var COMMAND_EXECUTOR_ID = "eliostruyf.execcommand";
1266
+ var COMMAND_EXECUTOR_URL = `https://marketplace.visualstudio.com/items?itemName=${COMMAND_EXECUTOR_ID}`;
1267
+ var AI_AGENTS = [
1268
+ {
1269
+ id: "copilot",
1270
+ label: "GitHub Copilot",
1271
+ description: "Opens Copilot Chat in VS Code with the task in the input box.",
1272
+ command: "workbench.action.chat.open",
1273
+ // `isPartialQuery` is what leaves the prompt sitting in the input rather than
1274
+ // sending it. Without it the link decides what the agent works on; with it,
1275
+ // you do.
1276
+ args: (prompt) => [{ query: prompt, isPartialQuery: true }]
1277
+ },
1278
+ // Appended rather than slotted in, for the same reason as AUTO_SYNC_EVENTS:
1279
+ // `parseAiAgents` orders a saved array by this list, so inserting in the middle
1280
+ // rewrites every existing config on its next save for no reason.
1281
+ {
1282
+ id: "claude",
1283
+ label: "Claude Code",
1284
+ description: "Opens a Claude Code tab in VS Code with the task in its prompt box.",
1285
+ command: "claude-vscode.editor.open",
1286
+ // `(sessionId, prompt, viewColumn)`. The first parameter is left out on
1287
+ // purpose: it resumes an existing session, and passing anything there would
1288
+ // have the prompt read as a session key. The third picks a column, which is a
1289
+ // choice the editor makes better than we can.
1290
+ //
1291
+ // The prompt lands in the tab's input rather than being sent — the webview
1292
+ // reads it once and calls `setInputText`. That is the same bargain as
1293
+ // Copilot's `isPartialQuery`, and the reason this beats the terminal command,
1294
+ // which would run what it was handed.
1295
+ args: (prompt) => [void 0, prompt]
1296
+ }
1297
+ ];
1298
+ function parseAiAgents(value) {
1299
+ if (!Array.isArray(value)) {
1300
+ return [];
1301
+ }
1302
+ const wanted = new Set(value);
1303
+ return AI_AGENTS.filter((a) => wanted.has(a.id)).map((a) => a.id);
1304
+ }
1305
+
1306
+ // src/model/syncEvents.ts
1307
+ var AUTO_SYNC_EVENTS = [
1308
+ {
1309
+ id: "taskCreated",
1310
+ label: "A task or to-do is created",
1311
+ description: "A new task, subtask or general to-do."
1312
+ },
1313
+ {
1314
+ id: "taskStatus",
1315
+ label: "A task changes state",
1316
+ description: "Status changed, marked done or worked on, reopened, completion date changed."
1317
+ },
1318
+ {
1319
+ id: "taskEdited",
1320
+ label: "A task is edited or deleted",
1321
+ description: "Title, due date, links, tags, description, task notes, checklist, attachments, recurrence, parent \u2014 or a deletion."
1322
+ },
1323
+ {
1324
+ id: "timeLogged",
1325
+ label: "Time is logged",
1326
+ description: "An entry in the day view\u2019s log form is added, changed or removed."
1327
+ },
1328
+ {
1329
+ id: "settings",
1330
+ label: "Clients, statuses or settings change",
1331
+ description: "A client or task status is added, edited, reordered or removed, or these settings are saved."
1332
+ },
1333
+ // Appended, not slotted in beside the other edit events: `parseAutoSyncEvents`
1334
+ // orders a saved array by this list, so an insertion in the middle rewrites
1335
+ // every existing config's `events` on its next save for no reason.
1336
+ {
1337
+ id: "dayNote",
1338
+ label: "A day note is saved",
1339
+ description: "The freeform Markdown written on a day, saved or cleared from the day view."
1340
+ },
1341
+ // Its own event rather than part of `taskEdited`: a prompt queue is an optional
1342
+ // block (`features.prompts`), and someone who writes prompts for an agent
1343
+ // running elsewhere wants them on the branch sooner than the rest of the task.
1344
+ {
1345
+ id: "prompt",
1346
+ label: "A prompt is written or run",
1347
+ description: "A prompt in a task\u2019s queue is added, edited, ticked off as run, or removed."
1348
+ },
1349
+ // Appended for the reason above. Its own event rather than part of any of the
1350
+ // task ones: a list is not a task, and the thing you want on the branch fast is
1351
+ // a list being ticked through on a phone that is about to lose signal.
1352
+ {
1353
+ id: "list",
1354
+ label: "A list changes",
1355
+ description: "An item on a reusable checklist is ticked, added, edited, moved or removed \u2014 or a list is created, renamed, duplicated, deleted or started again."
1356
+ },
1357
+ // Appended for the reason above. Its own event: the notes of a meeting that
1358
+ // just ended are what you want on the branch before the laptop lid closes.
1359
+ {
1360
+ id: "meeting",
1361
+ label: "Meeting notes change",
1362
+ description: "A meeting is started, edited or deleted \u2014 its notes, people, action items or times."
1363
+ }
1364
+ ];
1365
+ function parseAutoSyncEvents(value) {
1366
+ if (!Array.isArray(value)) {
1367
+ return [];
1368
+ }
1369
+ const wanted = new Set(value);
1370
+ return AUTO_SYNC_EVENTS.filter((e) => wanted.has(e.id)).map((e) => e.id);
1371
+ }
1372
+
1373
+ // src/model/taskSort.ts
1374
+ var TASK_SORT_KEYS = ["created", "due", "priority", "title", "status"];
1375
+ var DEFAULT_TASK_SORT = { key: "created", dir: "asc" };
1376
+ function normalizeTaskSort(value) {
1377
+ if (!value || typeof value !== "object") {
1378
+ return { ...DEFAULT_TASK_SORT };
1379
+ }
1380
+ const candidate = value;
1381
+ return {
1382
+ key: TASK_SORT_KEYS.includes(candidate.key) ? candidate.key : DEFAULT_TASK_SORT.key,
1383
+ dir: candidate.dir === "desc" ? "desc" : "asc"
1384
+ };
1385
+ }
1386
+
1387
+ // src/model/codeTheme.ts
1388
+ var CODE_THEMES = ["system", "light", "dark"];
1389
+ var DEFAULT_CODE_THEME = "system";
1390
+ function normalizeCodeTheme(value) {
1391
+ return CODE_THEMES.includes(value) ? value : DEFAULT_CODE_THEME;
1392
+ }
1393
+
1394
+ // src/workspace/paths.ts
1395
+ var DEFAULT_HOURS_PER_DAY = 8;
1396
+ var DEFAULT_WEEK_START = 0;
1397
+ var DEFAULT_TODOS_PER_PAGE = 5;
1398
+ var DEFAULT_AUTO_SYNC = { enabled: false, delayMinutes: 5, events: [] };
1399
+ var DEFAULT_FEATURES = { attachments: true, prompts: true, checklist: true, lists: true, meetings: true };
1400
+ function parseFeatures(value) {
1401
+ const raw = value ?? {};
1402
+ return {
1403
+ attachments: raw.attachments !== false,
1404
+ prompts: raw.prompts !== false,
1405
+ checklist: raw.checklist !== false,
1406
+ lists: raw.lists !== false,
1407
+ meetings: raw.meetings !== false
1408
+ };
1409
+ }
1410
+ function parseTodosPerPage(value) {
1411
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : DEFAULT_TODOS_PER_PAGE;
1412
+ }
1413
+ function parseAutoSync(value) {
1414
+ const raw = value ?? {};
1415
+ const delay = typeof raw.delayMinutes === "number" && Number.isFinite(raw.delayMinutes) && raw.delayMinutes >= 1 ? raw.delayMinutes : DEFAULT_AUTO_SYNC.delayMinutes;
1416
+ return { enabled: raw.enabled === true, delayMinutes: delay, events: parseAutoSyncEvents(raw.events) };
1417
+ }
1418
+ function parseLinks(value) {
1419
+ if (!Array.isArray(value)) {
1420
+ return [];
1421
+ }
1422
+ return value.map((l) => typeof l === "string" ? { url: l } : l).filter((l) => !!l && typeof l.url === "string" && !!l.url.trim()).map((l) => {
1423
+ const label2 = typeof l.label === "string" ? l.label.trim() : "";
1424
+ return { url: l.url.trim(), ...label2 ? { label: label2 } : {} };
1425
+ });
1426
+ }
1427
+ function parseClientLinks(value) {
1428
+ const links = parseLinks(value);
1429
+ return links.length ? links : void 0;
1430
+ }
1431
+ var DAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
1432
+ function parseWeekStart(value) {
1433
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 6) {
1434
+ return value;
1435
+ }
1436
+ if (typeof value === "string") {
1437
+ const idx = DAY_NAMES.indexOf(value.trim().toLowerCase());
1438
+ if (idx >= 0) {
1439
+ return idx;
1440
+ }
1441
+ }
1442
+ return DEFAULT_WEEK_START;
1443
+ }
1444
+ var FileMap = class {
1445
+ /** repo-relative path -> UTF-8 text (markdown, json). */
1446
+ text = /* @__PURE__ */ new Map();
1447
+ /** repo-relative path -> bytes (images under assets/). */
1448
+ binary = /* @__PURE__ */ new Map();
1449
+ /** Paths written since load; the host commits exactly these. */
1450
+ dirty = /* @__PURE__ */ new Set();
1451
+ /** Dirty paths that were removed rather than written; they commit as tree
1452
+ * deletions instead of blobs. */
1453
+ deleted = /* @__PURE__ */ new Set();
1454
+ /** Paths the branch is known to hold — everything loaded, plus everything a
1455
+ * successful sync wrote. A delete only has to reach GitHub for those. */
1456
+ remote = /* @__PURE__ */ new Set();
1457
+ /** Blob SHA per path as loaded from GitHub, to skip unchanged files on reload. */
1458
+ baseSha = /* @__PURE__ */ new Map();
1459
+ /** Text of each path as the branch last had it — the version the local edits
1460
+ * started from. It's the common ancestor the sync merge needs to tell "I
1461
+ * changed this" from "the other instance changed this" (see data/merge). Set
1462
+ * on load and updated on every successful push. */
1463
+ baseText = /* @__PURE__ */ new Map();
1464
+ markDirty(path) {
1465
+ this.dirty.add(path);
1466
+ }
1467
+ clearDirty() {
1468
+ this.dirty.clear();
1469
+ }
1470
+ };
1471
+ var mounted;
1472
+ function mountFileMap(map) {
1473
+ mounted = map;
1474
+ }
1475
+ function fileMap() {
1476
+ if (!mounted) {
1477
+ throw new Error("No repository is mounted.");
1478
+ }
1479
+ return mounted;
1480
+ }
1481
+ async function readText(path) {
1482
+ return fileMap().text.get(path);
1483
+ }
1484
+ async function writeText(path, text) {
1485
+ const fm = fileMap();
1486
+ fm.deleted.delete(path);
1487
+ fm.text.set(path, text);
1488
+ fm.markDirty(path);
1489
+ }
1490
+ async function ensureDir(_path) {
1491
+ }
1492
+ function defaultConfig() {
1493
+ return {
1494
+ hoursPerDay: DEFAULT_HOURS_PER_DAY,
1495
+ weekStart: DEFAULT_WEEK_START,
1496
+ todosPerPage: DEFAULT_TODOS_PER_PAGE,
1497
+ defaultTaskSort: { ...DEFAULT_TASK_SORT },
1498
+ codeTheme: DEFAULT_CODE_THEME,
1499
+ clients: [],
1500
+ // Copied, not shared: the caller may edit the result, and the status editor
1501
+ // splices this very array when config.json is missing.
1502
+ statuses: DEFAULT_STATUSES.map((s) => ({ ...s })),
1503
+ autoSync: parseAutoSync(void 0),
1504
+ features: { ...DEFAULT_FEATURES },
1505
+ aiAgents: []
1506
+ };
1507
+ }
1508
+ var Workspace = class {
1509
+ get configDir() {
1510
+ return ".worklog";
1511
+ }
1512
+ get configFile() {
1513
+ return ".worklog/config.json";
1514
+ }
1515
+ get clientsDir() {
1516
+ return "clients";
1517
+ }
1518
+ get archiveDir() {
1519
+ return "archive";
1520
+ }
1521
+ get worklogDir() {
1522
+ return "worklog";
1523
+ }
1524
+ get notesDir() {
1525
+ return "notes";
1526
+ }
1527
+ get listsDir() {
1528
+ return "lists";
1529
+ }
1530
+ get meetingsDir() {
1531
+ return "meetings";
1532
+ }
1533
+ get assetsDir() {
1534
+ return "assets";
1535
+ }
1536
+ get appFile() {
1537
+ return "worklog.worklog";
1538
+ }
1539
+ clientFile(clientId) {
1540
+ return `clients/${clientId}.md`;
1541
+ }
1542
+ archiveFile(clientId, month) {
1543
+ return `archive/${clientId}/${month}.md`;
1544
+ }
1545
+ worklogFile(month) {
1546
+ return `worklog/${month}.md`;
1547
+ }
1548
+ notesFile(month) {
1549
+ return `notes/${month}.md`;
1550
+ }
1551
+ listFile(listId) {
1552
+ return `lists/${listId}.md`;
1553
+ }
1554
+ meetingsFile(month) {
1555
+ return `meetings/${month}.md`;
1556
+ }
1557
+ async loadConfig() {
1558
+ const raw = await readText(this.configFile);
1559
+ if (raw === void 0) {
1560
+ return defaultConfig();
1561
+ }
1562
+ try {
1563
+ const parsed = JSON.parse(raw);
1564
+ return {
1565
+ hoursPerDay: parsed.hoursPerDay && parsed.hoursPerDay > 0 ? parsed.hoursPerDay : DEFAULT_HOURS_PER_DAY,
1566
+ weekStart: parseWeekStart(parsed.weekStart),
1567
+ todosPerPage: parseTodosPerPage(parsed.todosPerPage),
1568
+ // Absent in every repo written before this setting existed, so the
1569
+ // fallback is the order those repos have always been shown in.
1570
+ defaultTaskSort: normalizeTaskSort(parsed.defaultTaskSort),
1571
+ // Absent in every repo written before this setting existed, and absent
1572
+ // reads as "follow the OS" — see normalizeCodeTheme.
1573
+ codeTheme: normalizeCodeTheme(parsed.codeTheme),
1574
+ // `archived`, `description` and `links` are normalized to value-or-absent
1575
+ // so they never round-trip a stray value into config.json (JSON.stringify
1576
+ // drops the undefined).
1577
+ clients: Array.isArray(parsed.clients) ? parsed.clients.filter((c) => !!c && !!c.id).map((c) => ({
1578
+ ...c,
1579
+ description: typeof c.description === "string" && c.description.trim() ? c.description : void 0,
1580
+ links: parseClientLinks(c.links),
1581
+ archived: c.archived === true ? true : void 0
1582
+ })) : [],
1583
+ // Normalized rather than trusted: a hand-edited list with no closing
1584
+ // status, two of them, or duplicate ids has to read as *something*, and
1585
+ // every caller assumes exactly one terminal status sitting last.
1586
+ statuses: normalizeStatuses(parsed.statuses),
1587
+ autoSync: parseAutoSync(parsed.autoSync),
1588
+ // Absent means both on, which is what every repo written before these
1589
+ // switches existed expects.
1590
+ features: parseFeatures(parsed.features),
1591
+ // Absent in every repo written before this setting existed, and absent is
1592
+ // the same answer as "none switched on" — no agent is offered by default.
1593
+ aiAgents: parseAiAgents(parsed.aiAgents)
1594
+ };
1595
+ } catch {
1596
+ return defaultConfig();
1597
+ }
1598
+ }
1599
+ async saveConfig(config) {
1600
+ await writeText(this.configFile, JSON.stringify(config, null, 2) + "\n");
1601
+ }
1602
+ };
1603
+ function stem(path) {
1604
+ const base = path.split("/").pop() ?? path;
1605
+ return base.replace(/\.[^.]+$/, "");
1606
+ }
1607
+ function dirName(path) {
1608
+ const parts = path.split("/");
1609
+ parts.pop();
1610
+ return parts.pop() ?? "";
1611
+ }
1612
+
1613
+ // src/model/todos.ts
1614
+ var GENERAL_TODO_CLIENT_ID = "todos";
1615
+ var GENERAL_TODO_LABEL = "To-dos";
1616
+ var GENERAL_TODO_COLOR = "#64748b";
1617
+ function isGeneralTodoClientId(id) {
1618
+ return id === GENERAL_TODO_CLIENT_ID;
1619
+ }
1620
+ function generalTodoClient() {
1621
+ return { id: GENERAL_TODO_CLIENT_ID, name: GENERAL_TODO_LABEL, color: GENERAL_TODO_COLOR };
1622
+ }
1623
+
1624
+ // src/model/worklog.ts
1625
+ var EVENT_WORKLOG_PREFIX = "event:";
1626
+ function eventWorklogClientId(eventType) {
1627
+ const normalized = eventType.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1628
+ if (!normalized) {
1629
+ throw new Error("Event type is required.");
1630
+ }
1631
+ return `${EVENT_WORKLOG_PREFIX}${normalized}`;
1632
+ }
1633
+ function isEventWorklogClientId(clientId) {
1634
+ return clientId.startsWith(EVENT_WORKLOG_PREFIX);
1635
+ }
1636
+ function eventTypeFromClientId(clientId) {
1637
+ return isEventWorklogClientId(clientId) ? clientId.slice(EVENT_WORKLOG_PREFIX.length) : "";
1638
+ }
1639
+ function formatEventTypeLabel(eventType) {
1640
+ return eventType.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1641
+ }
1642
+ var EVENT_TYPES = [
1643
+ { id: "vacation", label: "Vacation" },
1644
+ { id: "public-holiday", label: "Public holiday" },
1645
+ { id: "out-of-office", label: "Out of office" },
1646
+ { id: "conference", label: "Conference" },
1647
+ { id: "sick", label: "Sick day" },
1648
+ { id: "other", label: "Other" }
1649
+ ];
1650
+ function eventLabel(eventType) {
1651
+ return EVENT_TYPES.find((e) => e.id === eventType)?.label ?? formatEventTypeLabel(eventType);
1652
+ }
1653
+ function roundHours(n) {
1654
+ return Math.round(n * 100) / 100;
1655
+ }
1656
+ function resolveHours(amount, hoursPerDay) {
1657
+ if (amount === "full") {
1658
+ return hoursPerDay;
1659
+ }
1660
+ if (amount === "half") {
1661
+ return hoursPerDay / 2;
1662
+ }
1663
+ const hours = typeof amount === "number" ? amount : Number(amount.trim().replace(/h$/i, ""));
1664
+ return isFinite(hours) && hours >= 0 && String(amount).trim() !== "" ? roundHours(hours) : void 0;
1665
+ }
1666
+ function unloggableReason(clients, clientId) {
1667
+ if (isGeneralTodoClientId(clientId)) {
1668
+ return "The general to-do list belongs to no client, so it has no hours.";
1669
+ }
1670
+ const client = clients.find((c) => c.id === clientId);
1671
+ if (!client) {
1672
+ return `Unknown client "${clientId}".`;
1673
+ }
1674
+ if (client.archived) {
1675
+ return `"${client.name}" is archived; restore it in Settings to log time on it.`;
1676
+ }
1677
+ return void 0;
1678
+ }
1679
+
1680
+ // src/model/recurringTask.ts
1681
+ function withSeededDue(task, from = today()) {
1682
+ if (!task.repeat || task.due || task.completed) {
1683
+ return task;
1684
+ }
1685
+ const start = firstOccurrence(task.repeat, from);
1686
+ return start ? { ...task, due: start } : task;
1687
+ }
1688
+
1689
+ // src/workspace/indexer.ts
1690
+ async function rebuild(db, ws) {
1691
+ const config = await ws.loadConfig();
1692
+ const fm = fileMap();
1693
+ const tasks = [];
1694
+ const worklog = [];
1695
+ const dayNotes = [];
1696
+ const checklists = [];
1697
+ const meetings = [];
1698
+ for (const [path, text] of fm.text) {
1699
+ if (/^clients\/[^/]+\.md$/.test(path)) {
1700
+ tasks.push(...parseTaskFile(text, path, stem(path)).tasks);
1701
+ continue;
1702
+ }
1703
+ if (/^archive\/[^/]+\/[^/]+\.md$/.test(path)) {
1704
+ tasks.push(...parseTaskFile(text, path, dirName(path)).tasks);
1705
+ continue;
1706
+ }
1707
+ if (/^worklog\/[^/]+\.md$/.test(path)) {
1708
+ worklog.push(...parseWorklogFile(text, path));
1709
+ continue;
1710
+ }
1711
+ if (/^notes\/[^/]+\.md$/.test(path)) {
1712
+ dayNotes.push(...parseDayNotesFile(text, path));
1713
+ continue;
1714
+ }
1715
+ if (/^lists\/[^/]+\.md$/.test(path)) {
1716
+ checklists.push(parseChecklistFile(text, path, stem(path)));
1717
+ continue;
1718
+ }
1719
+ if (/^meetings\/[^/]+\.md$/.test(path)) {
1720
+ meetings.push(...parseMeetingFile(text, path));
1721
+ continue;
1722
+ }
1723
+ }
1724
+ const seeded = tasks.map((t) => withSeededDue(t));
1725
+ const clients = mergeClients(config.clients, seeded, worklog);
1726
+ db.reset();
1727
+ db.load({ clients, tasks: seeded, worklog, dayNotes, checklists, meetings });
1728
+ return {
1729
+ clients: clients.length,
1730
+ tasks: seeded.length,
1731
+ worklog: worklog.length,
1732
+ dayNotes: dayNotes.length,
1733
+ checklists: checklists.length,
1734
+ meetings: meetings.length
1735
+ };
1736
+ }
1737
+ function mergeClients(configured, tasks, worklog) {
1738
+ const byId = /* @__PURE__ */ new Map();
1739
+ for (const c of configured) {
1740
+ byId.set(c.id, c);
1741
+ }
1742
+ const ensure = (id) => {
1743
+ if (id && !byId.has(id)) {
1744
+ byId.set(id, { id, name: id });
1745
+ }
1746
+ };
1747
+ for (const t of tasks) {
1748
+ t.clientIds.forEach(ensure);
1749
+ }
1750
+ for (const w of worklog) {
1751
+ if (!isEventWorklogClientId(w.clientId)) {
1752
+ ensure(w.clientId);
1753
+ }
1754
+ }
1755
+ return [...byId.values()];
1756
+ }
1757
+
1758
+ // src/store.ts
1759
+ var Store = class {
1760
+ db = new MemoryDb();
1761
+ ws = new Workspace();
1762
+ config;
1763
+ listeners = /* @__PURE__ */ new Set();
1764
+ /** Rebuild from the mounted file map (called once after load, then after each edit). */
1765
+ async rebuild(reason) {
1766
+ this.config = await this.ws.loadConfig();
1767
+ await rebuild(this.db, this.ws);
1768
+ this.emitChange(reason);
1769
+ }
1770
+ getConfig() {
1771
+ return this.config;
1772
+ }
1773
+ onDidChange(listener) {
1774
+ this.listeners.add(listener);
1775
+ return () => this.listeners.delete(listener);
1776
+ }
1777
+ emitChange(reason) {
1778
+ for (const l of this.listeners) {
1779
+ l(reason);
1780
+ }
1781
+ }
1782
+ };
1783
+
1784
+ // src/server/github.ts
1785
+ function isWorklogPath(path) {
1786
+ return path === ".worklog/config.json" || path === "worklog.worklog" || /^clients\/[^/]+\.md$/.test(path) || /^archive\/[^/]+\/[^/]+\.md$/.test(path) || /^worklog\/[^/]+\.md$/.test(path) || /^notes\/[^/]+\.md$/.test(path) || /^lists\/[^/]+\.md$/.test(path) || /^meetings\/[^/]+\.md$/.test(path) || /^assets\/[^/]+$/.test(path);
1787
+ }
1788
+
1789
+ // src/model/priority.ts
1790
+ var NORMAL_PRIORITY_ID = "normal";
1791
+ var NORMAL = { id: NORMAL_PRIORITY_ID, label: "Normal", rank: 2 };
1792
+ var PRIORITIES = [
1793
+ { id: "urgent", label: "Urgent", rank: 0 },
1794
+ { id: "high", label: "High", rank: 1 },
1795
+ NORMAL,
1796
+ { id: "low", label: "Low", rank: 3 }
1797
+ ];
1798
+ var BY_ID = new Map(PRIORITIES.map((p) => [p.id, p]));
1799
+ function priorityDef(raw) {
1800
+ return raw ? BY_ID.get(raw.trim().toLowerCase()) : void 0;
1801
+ }
1802
+ function priorityRank(raw) {
1803
+ return (priorityDef(raw) ?? NORMAL).rank;
1804
+ }
1805
+ function writablePriority(raw) {
1806
+ const def = priorityDef(raw);
1807
+ return def && def.id !== NORMAL_PRIORITY_ID ? def.id : void 0;
1808
+ }
1809
+
1810
+ // src/parser/ids.ts
1811
+ var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
1812
+ function newTaskId(size = 6) {
1813
+ return "t_" + randomToken(size);
1814
+ }
1815
+ function randomToken(size) {
1816
+ const bytes = new Uint8Array(size);
1817
+ crypto.getRandomValues(bytes);
1818
+ let out = "";
1819
+ for (let i = 0; i < size; i++) {
1820
+ out += ALPHABET[bytes[i] % ALPHABET.length];
1821
+ }
1822
+ return out;
1823
+ }
1824
+
1825
+ // src/services/assets.ts
1826
+ var MAX_BYTES = 10 * 1024 * 1024;
1827
+
1828
+ // src/commands/shared.ts
1829
+ async function appendTaskBlock(ws, client, block) {
1830
+ await ensureDir(ws.clientsDir);
1831
+ const path = ws.clientFile(client.id);
1832
+ const existing = await readText(path);
1833
+ let content;
1834
+ if (existing === void 0 || existing.trim() === "") {
1835
+ content = `# ${client.name}
1836
+
1837
+ ${block}
1838
+ `;
1839
+ } else {
1840
+ content = existing.replace(/\s+$/, "") + "\n\n" + block + "\n";
1841
+ }
1842
+ await writeText(path, content);
1843
+ }
1844
+ async function appendArchiveBlock(ws, clientId, clientName, month, block) {
1845
+ await ensureDir(`${ws.archiveDir}/${clientId}`);
1846
+ const path = ws.archiveFile(clientId, month);
1847
+ const existing = await readText(path);
1848
+ let content;
1849
+ if (existing === void 0 || existing.trim() === "") {
1850
+ content = `# ${clientName} \u2014 ${month}
1851
+
1852
+ ${block}
1853
+ `;
1854
+ } else {
1855
+ content = existing.replace(/\s+$/, "") + "\n\n" + block + "\n";
1856
+ }
1857
+ await writeText(path, content);
1858
+ }
1859
+
1860
+ // src/services/taskOps.ts
1861
+ function collectDescendants(store, taskId) {
1862
+ const out = [];
1863
+ const visit = (id) => {
1864
+ for (const child of store.db.getChildren(id)) {
1865
+ out.push(child);
1866
+ visit(child.id);
1867
+ }
1868
+ };
1869
+ visit(taskId);
1870
+ return out;
1871
+ }
1872
+ function isClosed(task) {
1873
+ return !!task.completed;
1874
+ }
1875
+ async function setTaskStatus(store, taskId, statusId) {
1876
+ const task = store.db.getTask(taskId);
1877
+ if (!task) {
1878
+ throw new Error(`Task ${taskId} not found.`);
1879
+ }
1880
+ const statuses = store.getConfig().statuses;
1881
+ const goingTerminal = isTerminalStatus(statuses, statusId);
1882
+ if (goingTerminal && !isClosed(task)) {
1883
+ return closeCascade(store, taskId, statusId, today());
1884
+ }
1885
+ if (!goingTerminal && isClosed(task)) {
1886
+ return reopenTask(store, taskId, statusId);
1887
+ }
1888
+ await updateInPlace(store, task, (t) => ({ ...t, status: statusId }));
1889
+ await store.rebuild("setStatus");
1890
+ return { ...task, status: statusId };
1891
+ }
1892
+ async function toggleTaskWorkedOn(store, taskId, date) {
1893
+ const task = store.db.getTask(taskId);
1894
+ if (!task) {
1895
+ throw new Error(`Task ${taskId} not found.`);
1896
+ }
1897
+ if (isGeneralTodoClientId(task.clientIds[0])) {
1898
+ throw new Error("To-dos don't track worked-on days \u2014 they're open or closed.");
1899
+ }
1900
+ const existing = new Set(task.workedOn ?? []);
1901
+ const marking = !existing.has(date);
1902
+ if (marking) {
1903
+ existing.add(date);
1904
+ } else {
1905
+ existing.delete(date);
1906
+ }
1907
+ const workedOn = [...existing].sort();
1908
+ const wip = marking && !isClosed(task) ? inProgressStatusId(store.getConfig().statuses) : void 0;
1909
+ await updateInPlace(store, task, (t) => ({
1910
+ ...t,
1911
+ workedOn,
1912
+ status: wip ?? t.status
1913
+ }));
1914
+ await store.rebuild("toggleWorked");
1915
+ return { ...task, workedOn, status: wip ?? task.status };
1916
+ }
1917
+ async function addTaskNote(store, taskId, text) {
1918
+ const task = store.db.getTask(taskId);
1919
+ if (!task) {
1920
+ throw new Error(`Task ${taskId} not found.`);
1921
+ }
1922
+ const trimmed = text.trim();
1923
+ if (!trimmed) {
1924
+ throw new Error("Cannot add an empty note.");
1925
+ }
1926
+ const note = { timestamp: nowStamp(), text: trimmed };
1927
+ await updateInPlace(store, task, (t) => ({
1928
+ ...t,
1929
+ notes: [...t.notes ?? [], note]
1930
+ }));
1931
+ await store.rebuild("addNote");
1932
+ return store.db.getTask(taskId) ?? task;
1933
+ }
1934
+ async function closeTaskById(store, taskId, completedDate = today()) {
1935
+ const terminal = terminalStatusId(store.getConfig().statuses);
1936
+ return closeCascade(store, taskId, terminal, completedDate);
1937
+ }
1938
+ async function closeCascade(store, taskId, statusId, completedDate, endSeries = false) {
1939
+ const root = store.db.getTask(taskId);
1940
+ if (!root) {
1941
+ throw new Error(`Task ${taskId} not found.`);
1942
+ }
1943
+ const openDescendants = collectDescendants(store, taskId).filter(
1944
+ (t) => !isClosed(t)
1945
+ );
1946
+ for (const child of openDescendants) {
1947
+ await closeOne(store, child, statusId, completedDate, endSeries);
1948
+ }
1949
+ const closed = await closeOne(store, root, statusId, completedDate, endSeries);
1950
+ await store.rebuild("closeTask");
1951
+ return closed;
1952
+ }
1953
+ async function closeOne(store, task, statusId, completedDate, endSeries = false) {
1954
+ const clientId = task.clientIds[0];
1955
+ const clientName = store.db.getClients().find((c) => c.id === clientId)?.name ?? clientId;
1956
+ const clientUri = store.ws.clientFile(clientId);
1957
+ const content = await readText(clientUri);
1958
+ if (content === void 0) {
1959
+ throw new Error(`Could not read ${clientId}.md to close "${task.title}".`);
1960
+ }
1961
+ const extracted = extractBlock(content, task.id);
1962
+ if (!extracted) {
1963
+ throw new Error(`Could not locate task ${task.id} in ${clientId}.md.`);
1964
+ }
1965
+ const source = withSeededDue(
1966
+ parseTaskFile(extracted.block, clientUri, clientId).tasks[0] ?? task
1967
+ );
1968
+ if (source.repeat && !endSeries) {
1969
+ const nextDue = nextDueAfterCompletion(
1970
+ source.repeat,
1971
+ source.due,
1972
+ completedDate
1973
+ );
1974
+ if (nextDue) {
1975
+ return rollOccurrence(store, {
1976
+ source,
1977
+ clientId,
1978
+ clientName,
1979
+ clientUri,
1980
+ content,
1981
+ statusId,
1982
+ completedDate,
1983
+ nextDue
1984
+ });
1985
+ }
1986
+ }
1987
+ const closed = {
1988
+ ...source,
1989
+ status: statusId,
1990
+ completed: completedDate
1991
+ };
1992
+ await writeText(clientUri, extracted.remainder);
1993
+ await appendArchiveBlock(
1994
+ store.ws,
1995
+ clientId,
1996
+ clientName,
1997
+ monthOf(closed.completed),
1998
+ serializeTask(closed, clientId)
1999
+ );
2000
+ return closed;
2001
+ }
2002
+ async function rollOccurrence(store, args) {
2003
+ const { source, clientId, clientName, clientUri, content } = args;
2004
+ const snapshot = {
2005
+ ...source,
2006
+ id: newTaskId(),
2007
+ status: args.statusId,
2008
+ completed: args.completedDate,
2009
+ repeat: void 0,
2010
+ lastDone: void 0,
2011
+ repeatOf: source.id
2012
+ };
2013
+ const live = {
2014
+ ...source,
2015
+ status: openStatusId(store.getConfig().statuses),
2016
+ due: args.nextDue,
2017
+ lastDone: args.completedDate,
2018
+ completed: void 0,
2019
+ notes: void 0,
2020
+ // The queue is the plan and comes along; the prompts that ran belong to the
2021
+ // occurrence that ran them and stay on its snapshot, or a weekly task would
2022
+ // carry every prompt it ever ran into every future week.
2023
+ prompts: queuedPrompts(source.prompts),
2024
+ // Same reasoning one step further: the steps are the plan for the series and
2025
+ // come along, but unticked — the ticks belong to the occurrence that earned
2026
+ // them and stay on its snapshot.
2027
+ checklist: untickedChecklist(source.checklist),
2028
+ workedOn: void 0
2029
+ };
2030
+ const replaced = replaceBlock(
2031
+ content,
2032
+ source.id,
2033
+ serializeTask(live, clientId)
2034
+ );
2035
+ if (replaced === void 0) {
2036
+ throw new Error(`Could not locate task ${source.id} to roll forward.`);
2037
+ }
2038
+ await writeText(clientUri, replaced.replace(/\s*$/, "") + "\n");
2039
+ await appendArchiveBlock(
2040
+ store.ws,
2041
+ clientId,
2042
+ clientName,
2043
+ monthOf(args.completedDate),
2044
+ serializeTask(snapshot, clientId)
2045
+ );
2046
+ return live;
2047
+ }
2048
+ function queuedPrompts(prompts) {
2049
+ const queued = (prompts ?? []).filter((p) => !p.ran);
2050
+ return queued.length ? queued : void 0;
2051
+ }
2052
+ function untickedChecklist(checklist) {
2053
+ return checklist?.length ? checklist.map((i) => ({ ...i, done: false })) : void 0;
2054
+ }
2055
+ async function reopenTask(store, taskId, statusId) {
2056
+ const task = store.db.getTask(taskId);
2057
+ if (!task) {
2058
+ throw new Error(`Task ${taskId} not found.`);
2059
+ }
2060
+ if (task.repeatOf) {
2061
+ const template = store.db.getTask(task.repeatOf);
2062
+ if (template?.repeat) {
2063
+ return undoOccurrence(store, task, template, statusId);
2064
+ }
2065
+ }
2066
+ const clientId = task.clientIds[0];
2067
+ const client = store.db.getClients().find((c) => c.id === clientId);
2068
+ const archiveUri = task.sourceFile;
2069
+ const content = await readText(archiveUri);
2070
+ if (content === void 0) {
2071
+ throw new Error(`Could not read the archive file for "${task.title}".`);
2072
+ }
2073
+ const extracted = extractBlock(content, task.id);
2074
+ if (!extracted) {
2075
+ throw new Error(`Could not locate task ${task.id} in the archive.`);
2076
+ }
2077
+ const parsed = parseTaskFile(extracted.block, archiveUri, clientId).tasks[0];
2078
+ const reopened = {
2079
+ ...parsed ?? task,
2080
+ status: statusId,
2081
+ completed: void 0
2082
+ };
2083
+ await writeText(archiveUri, extracted.remainder);
2084
+ await appendTaskBlock(
2085
+ store.ws,
2086
+ client ?? { id: clientId, name: clientId },
2087
+ serializeTask(reopened, clientId)
2088
+ );
2089
+ await store.rebuild("reopenTask");
2090
+ return reopened;
2091
+ }
2092
+ async function undoOccurrence(store, snapshot, template, statusId) {
2093
+ const archiveUri = snapshot.sourceFile;
2094
+ const archiveContent = await readText(archiveUri);
2095
+ if (archiveContent === void 0) {
2096
+ throw new Error(`Could not read the archive file for "${snapshot.title}".`);
2097
+ }
2098
+ const extracted = extractBlock(archiveContent, snapshot.id);
2099
+ if (!extracted) {
2100
+ throw new Error(`Could not locate occurrence ${snapshot.id} in the archive.`);
2101
+ }
2102
+ const previous = store.db.occurrencesOf(template.id).find((o) => o.id !== snapshot.id && o.completed);
2103
+ await writeText(archiveUri, extracted.remainder);
2104
+ await updateInPlace(store, template, (t) => ({
2105
+ ...t,
2106
+ status: statusId,
2107
+ due: snapshot.due,
2108
+ lastDone: previous?.completed,
2109
+ notes: snapshot.notes,
2110
+ prompts: snapshot.prompts,
2111
+ checklist: snapshot.checklist,
2112
+ workedOn: snapshot.workedOn
2113
+ }));
2114
+ await store.rebuild("undoOccurrence");
2115
+ return store.db.getTask(template.id) ?? template;
2116
+ }
2117
+ async function updateTask(store, taskId, fields) {
2118
+ const task = store.db.getTask(taskId);
2119
+ if (!task) {
2120
+ throw new Error(`Task ${taskId} not found.`);
2121
+ }
2122
+ const current = task.clientIds[0];
2123
+ const target = fields.clientId && fields.clientId.trim() ? fields.clientId : current;
2124
+ const apply = (t) => withSeededDue({
2125
+ ...t,
2126
+ title: fields.title !== void 0 ? fields.title.trim() || t.title : t.title,
2127
+ // Passing the field at all is a decision about priority, so an unrecognised
2128
+ // value clears it rather than leaving the old one standing.
2129
+ priority: fields.priority !== void 0 ? writablePriority(fields.priority) : t.priority,
2130
+ parentId: fields.parentId !== void 0 ? fields.parentId || void 0 : t.parentId,
2131
+ links: fields.links !== void 0 ? parseLinks(fields.links) : t.links,
2132
+ description: fields.description !== void 0 ? fields.description.trim() || void 0 : t.description,
2133
+ due: fields.due !== void 0 ? fields.due.trim() || void 0 : t.due,
2134
+ tags: fields.tags !== void 0 ? fields.tags.map((tag) => tag.trim()).filter(Boolean) : t.tags,
2135
+ repeat: fields.repeat !== void 0 ? fields.repeat ?? void 0 : t.repeat,
2136
+ // Dropping the rule drops the series' bookkeeping with it.
2137
+ lastDone: fields.repeat === null ? void 0 : t.lastDone
2138
+ });
2139
+ if (target === current) {
2140
+ await updateInPlace(store, task, apply);
2141
+ } else {
2142
+ const uri = task.sourceFile;
2143
+ const content = await readText(uri);
2144
+ if (content === void 0) {
2145
+ throw new Error(`Could not read ${task.sourceFile}.`);
2146
+ }
2147
+ const extracted = extractBlock(content, task.id);
2148
+ if (!extracted) {
2149
+ throw new Error(`Could not locate task ${task.id}.`);
2150
+ }
2151
+ const parsed = parseTaskFile(extracted.block, uri, current).tasks[0] ?? task;
2152
+ const moved = apply({ ...parsed, clientIds: [target] });
2153
+ await writeText(uri, extracted.remainder);
2154
+ if (moved.completed) {
2155
+ const name = store.db.getClients().find((c) => c.id === target)?.name ?? target;
2156
+ await appendArchiveBlock(
2157
+ store.ws,
2158
+ target,
2159
+ name,
2160
+ monthOf(moved.completed),
2161
+ serializeTask(moved, target)
2162
+ );
2163
+ } else {
2164
+ const client = store.db.getClients().find((c) => c.id === target) ?? {
2165
+ id: target,
2166
+ name: target
2167
+ };
2168
+ await appendTaskBlock(store.ws, client, serializeTask(moved, target));
2169
+ }
2170
+ }
2171
+ await store.rebuild("updateTask");
2172
+ return store.db.getTask(taskId) ?? task;
2173
+ }
2174
+ async function updateInPlace(store, task, mutate) {
2175
+ const uri = task.sourceFile;
2176
+ const content = await readText(uri);
2177
+ if (content === void 0) {
2178
+ throw new Error(`Could not read ${task.sourceFile}.`);
2179
+ }
2180
+ const parsed = parseTaskFile(
2181
+ extractBlockText(content, task.id),
2182
+ uri,
2183
+ task.clientIds[0]
2184
+ ).tasks[0] ?? task;
2185
+ const next = mutate({ ...parsed, clientIds: task.clientIds });
2186
+ const replaced = replaceBlock(
2187
+ content,
2188
+ task.id,
2189
+ serializeTask(next, task.clientIds[0])
2190
+ );
2191
+ if (replaced === void 0) {
2192
+ throw new Error(`Could not locate task ${task.id} to update.`);
2193
+ }
2194
+ await writeText(uri, replaced.replace(/\s*$/, "") + "\n");
2195
+ }
2196
+ function extractBlockText(content, taskId) {
2197
+ return extractBlock(content, taskId)?.block ?? "";
2198
+ }
2199
+
2200
+ // src/services/tasks.ts
2201
+ async function createTask(store, input) {
2202
+ const title = input.title.trim();
2203
+ if (!title) {
2204
+ throw new Error("A task title is required.");
2205
+ }
2206
+ const client = store.db.getClients().find((c) => c.id === input.clientId) ?? (isGeneralTodoClientId(input.clientId) ? generalTodoClient() : void 0);
2207
+ if (!client) {
2208
+ throw new Error(`Unknown client "${input.clientId}".`);
2209
+ }
2210
+ const task = {
2211
+ id: newTaskId(),
2212
+ title,
2213
+ description: input.description?.trim() || void 0,
2214
+ // The configured first working status, not the literal 'open': the list is
2215
+ // the user's, and a renamed or removed first status would otherwise mint
2216
+ // tasks in a status nothing knows about.
2217
+ status: openStatusId(store.getConfig().statuses),
2218
+ priority: writablePriority(input.priority),
2219
+ parentId: input.parentId || void 0,
2220
+ clientIds: [client.id],
2221
+ links: parseLinks(input.links),
2222
+ created: today(),
2223
+ due: input.due?.trim() || void 0,
2224
+ repeat: input.repeat,
2225
+ tags: (input.tags ?? []).map((t) => t.trim()).filter(Boolean),
2226
+ sourceFile: "",
2227
+ sourceLine: 0
2228
+ };
2229
+ const seeded = withSeededDue(task);
2230
+ await appendTaskBlock(store.ws, client, serializeTask(seeded, client.id));
2231
+ await store.rebuild("addTask");
2232
+ return seeded;
2233
+ }
2234
+
2235
+ // src/services/logWork.ts
2236
+ async function logWork(store, input) {
2237
+ const date = input.date ?? today();
2238
+ let task = findTask(store, input);
2239
+ const created = !task;
2240
+ if (!task) {
2241
+ task = await createTask(store, {
2242
+ clientId: input.clientId,
2243
+ title: input.title,
2244
+ links: input.link ? [input.link] : void 0
2245
+ });
2246
+ }
2247
+ const markedWorked = !(task.workedOn ?? []).includes(date);
2248
+ if (markedWorked) {
2249
+ task = await toggleTaskWorkedOn(store, task.id, date);
2250
+ }
2251
+ const note = input.note?.trim();
2252
+ const noteAdded = !!note && !(task.notes ?? []).some((n) => n.timestamp.startsWith(today()) && n.text === note);
2253
+ if (noteAdded) {
2254
+ task = await addTaskNote(store, task.id, note);
2255
+ }
2256
+ return { task: store.db.getTask(task.id) ?? task, created, markedWorked, noteAdded };
2257
+ }
2258
+ function findTask(store, input) {
2259
+ if (input.taskId) {
2260
+ const byId = store.db.getTask(input.taskId);
2261
+ if (!byId) {
2262
+ throw new Error(`Task ${input.taskId} not found.`);
2263
+ }
2264
+ return byId;
2265
+ }
2266
+ const all = store.db.getAllTasks().filter((t) => t.clientIds.includes(input.clientId));
2267
+ const link = input.link ? normalizeUrl(input.link) : void 0;
2268
+ const byLink = link ? all.find((t) => t.links.some((l) => normalizeUrl(l.url) === link)) : void 0;
2269
+ const title = input.title.trim().toLowerCase();
2270
+ return byLink ?? all.find((t) => !t.completed && t.title.trim().toLowerCase() === title);
2271
+ }
2272
+ function normalizeUrl(url) {
2273
+ return url.trim().replace(/\/+$/, "");
2274
+ }
2275
+
2276
+ // src/services/editTask.ts
2277
+ function requireTask(store, ref, { includeClosed = false } = {}) {
2278
+ if (ref.taskId) {
2279
+ const byId = store.db.getTask(ref.taskId);
2280
+ if (!byId) {
2281
+ throw new Error(`Task ${ref.taskId} not found.`);
2282
+ }
2283
+ return byId;
2284
+ }
2285
+ if (!ref.link && !ref.title) {
2286
+ throw new Error("Name the task with an id, a link or a title.");
2287
+ }
2288
+ const scoped = store.db.getAllTasks().filter((t) => !ref.clientId || t.clientIds.includes(ref.clientId));
2289
+ const link = ref.link ? normalizeUrl(ref.link) : void 0;
2290
+ const title = ref.title?.trim().toLowerCase();
2291
+ const matches = scoped.filter(
2292
+ (t) => link ? t.links.some((l) => normalizeUrl(l.url) === link) : t.title.trim().toLowerCase() === title
2293
+ );
2294
+ const named = link ? `linked to ${ref.link}` : `called "${ref.title}"`;
2295
+ const open = matches.filter((t) => !t.completed);
2296
+ if (open.length > 1) {
2297
+ const list2 = open.map((t) => `${t.id} (${t.sourceFile})`).join(", ");
2298
+ throw new Error(`Several open tasks are ${named}: ${list2}. Pass --id, or --client to narrow it down.`);
2299
+ }
2300
+ if (open.length === 1) {
2301
+ return open[0];
2302
+ }
2303
+ const closed = matches.filter((t) => t.completed).sort((a, b) => (b.completed ?? "").localeCompare(a.completed ?? ""))[0];
2304
+ if (includeClosed && closed) {
2305
+ return closed;
2306
+ }
2307
+ throw new Error(`No open task is ${named}${ref.clientId ? ` for client "${ref.clientId}"` : ""}.`);
2308
+ }
2309
+ async function editTask(store, ref, edit) {
2310
+ let task = requireTask(store, ref);
2311
+ const fields = {};
2312
+ const changed = [];
2313
+ const title = edit.title?.trim();
2314
+ if (title && title !== task.title) {
2315
+ fields.title = title;
2316
+ changed.push("title");
2317
+ }
2318
+ if (edit.due !== void 0 && (edit.due ?? void 0) !== task.due) {
2319
+ fields.due = edit.due ?? "";
2320
+ changed.push("due");
2321
+ }
2322
+ if (edit.priority !== void 0 && writablePriority(edit.priority) !== writablePriority(task.priority)) {
2323
+ fields.priority = edit.priority;
2324
+ changed.push("priority");
2325
+ }
2326
+ if (edit.description !== void 0 && (edit.description.trim() || void 0) !== task.description) {
2327
+ fields.description = edit.description;
2328
+ changed.push("description");
2329
+ }
2330
+ if (edit.addTags?.length || edit.removeTags?.length) {
2331
+ const current = task.tags ?? [];
2332
+ const remove = new Set((edit.removeTags ?? []).map((t) => t.trim().toLowerCase()));
2333
+ const kept = current.filter((t) => !remove.has(t.toLowerCase()));
2334
+ const added = (edit.addTags ?? []).map((t) => t.trim()).filter((t) => t && !kept.some((k) => k.toLowerCase() === t.toLowerCase()));
2335
+ const tags = [...kept, ...added];
2336
+ if (tags.join("\n") !== current.join("\n")) {
2337
+ fields.tags = tags;
2338
+ changed.push("tags");
2339
+ }
2340
+ }
2341
+ if (edit.parentId !== void 0 && (edit.parentId ?? void 0) !== task.parentId) {
2342
+ if (edit.parentId) {
2343
+ requireParent(store, edit.parentId, task.id);
2344
+ }
2345
+ fields.parentId = edit.parentId ?? "";
2346
+ changed.push("parent");
2347
+ }
2348
+ if (Object.keys(fields).length > 0) {
2349
+ task = await updateTask(store, task.id, fields);
2350
+ }
2351
+ if (edit.status !== void 0 && edit.status !== task.status) {
2352
+ const statuses = store.getConfig().statuses;
2353
+ if (!statuses.some((s) => s.id === edit.status)) {
2354
+ throw new Error(`Unknown status "${edit.status}". Known: ${statuses.map((s) => s.id).join(", ")}.`);
2355
+ }
2356
+ task = await setTaskStatus(store, task.id, edit.status);
2357
+ changed.push("status");
2358
+ }
2359
+ return { task: store.db.getTask(task.id) ?? task, changed };
2360
+ }
2361
+ async function closeTask(store, ref) {
2362
+ const task = requireTask(store, ref, { includeClosed: true });
2363
+ if (task.completed || task.repeat && task.lastDone === today()) {
2364
+ return { task, closed: false };
2365
+ }
2366
+ const closed = await closeTaskById(store, task.id);
2367
+ return { task: store.db.getTask(task.id) ?? closed, closed: true };
2368
+ }
2369
+ function requireParent(store, parentId, childId) {
2370
+ const parent = store.db.getTask(parentId);
2371
+ if (!parent) {
2372
+ throw new Error(`Parent task ${parentId} not found.`);
2373
+ }
2374
+ for (let p = parent; p; p = p.parentId ? store.db.getTask(p.parentId) : void 0) {
2375
+ if (p.id === childId) {
2376
+ throw new Error(`Task ${childId} can't go under ${parentId}: that is the task itself or one of its subtasks.`);
2377
+ }
2378
+ }
2379
+ return parent;
2380
+ }
2381
+
2382
+ // src/services/addTask.ts
2383
+ async function addTask(store, input) {
2384
+ const existing = findTask(store, input);
2385
+ if (existing) {
2386
+ return { task: existing, created: false };
2387
+ }
2388
+ if (input.parentId) {
2389
+ requireParent(store, input.parentId);
2390
+ }
2391
+ const task = await createTask(store, {
2392
+ clientId: input.clientId,
2393
+ title: input.title,
2394
+ links: input.link ? [input.link] : void 0,
2395
+ due: input.due,
2396
+ priority: input.priority,
2397
+ description: input.description,
2398
+ tags: input.tags,
2399
+ parentId: input.parentId
2400
+ });
2401
+ return { task: store.db.getTask(task.id) ?? task, created: true };
2402
+ }
2403
+
2404
+ // src/services/listTasks.ts
2405
+ function listTasks(store, filter = {}) {
2406
+ const tag = filter.tag?.trim().toLowerCase();
2407
+ return store.db.getAllTasks().filter((t) => !t.completed).filter((t) => !filter.clientId || t.clientIds.includes(filter.clientId)).filter((t) => !filter.dueBy || !!t.due && t.due <= filter.dueBy).filter((t) => !tag || (t.tags ?? []).some((x) => x.toLowerCase() === tag)).sort(
2408
+ (a, b) => (a.due ?? "\uFFFF").localeCompare(b.due ?? "\uFFFF") || priorityRank(a.priority) - priorityRank(b.priority) || (a.created ?? "").localeCompare(b.created ?? "")
2409
+ );
2410
+ }
2411
+
2412
+ // src/services/worklog.ts
2413
+ function withEntry(existing, month, date, clientId, hours, note) {
2414
+ const line = serializeWorklogEntry({ date, clientId, hours, note });
2415
+ if (existing === void 0 || existing.trim() === "") {
2416
+ return `# Worklog ${month}
2417
+
2418
+ ${line}
2419
+ `;
2420
+ }
2421
+ const lines = existing.split(/\r?\n/);
2422
+ const prefix = `- ${date} ${clientId} `;
2423
+ const idx = lines.findIndex((l) => l.startsWith(prefix));
2424
+ if (idx >= 0) {
2425
+ lines[idx] = line;
2426
+ } else {
2427
+ let insertAt = lines.length;
2428
+ while (insertAt > 0 && lines[insertAt - 1].trim() === "") {
2429
+ insertAt--;
2430
+ }
2431
+ lines.splice(insertAt, 0, line);
2432
+ }
2433
+ return lines.join("\n").replace(/\s+$/, "") + "\n";
2434
+ }
2435
+ async function setWorklog(store, date, clientId, hours, note) {
2436
+ if (!(hours > 0)) {
2437
+ return removeWorklog(store, date, clientId);
2438
+ }
2439
+ const month = monthOf(date);
2440
+ const uri = store.ws.worklogFile(month);
2441
+ await ensureDir(store.ws.worklogDir);
2442
+ const existing = await readText(uri);
2443
+ await writeText(uri, withEntry(existing, month, date, clientId, hours, note));
2444
+ await store.rebuild("setWorklog");
2445
+ }
2446
+ async function removeWorklog(store, date, clientId) {
2447
+ const uri = store.ws.worklogFile(monthOf(date));
2448
+ const existing = await readText(uri);
2449
+ if (existing === void 0) {
2450
+ return;
2451
+ }
2452
+ const prefix = `- ${date} ${clientId} `;
2453
+ const lines = existing.split(/\r?\n/).filter((l) => !l.startsWith(prefix));
2454
+ await writeText(uri, lines.join("\n").replace(/\s+$/, "") + "\n");
2455
+ await store.rebuild("removeWorklog");
2456
+ }
2457
+
2458
+ // src/services/logHours.ts
2459
+ async function logHours(store, input) {
2460
+ const config = store.getConfig();
2461
+ const date = input.date ?? today();
2462
+ if (!!input.clientId === !!input.event) {
2463
+ throw new Error("Name either a client or a day event.");
2464
+ }
2465
+ let target;
2466
+ if (input.event) {
2467
+ target = eventWorklogClientId(input.event);
2468
+ if (!EVENT_TYPES.some((e) => e.id === eventTypeFromClientId(target))) {
2469
+ throw new Error(`Unknown day event "${input.event}". Known: ${EVENT_TYPES.map((e) => e.id).join(", ")}.`);
2470
+ }
2471
+ } else {
2472
+ target = input.clientId;
2473
+ const reason = unloggableReason(store.db.getClients(), target);
2474
+ if (reason) {
2475
+ throw new Error(reason);
2476
+ }
2477
+ }
2478
+ const hours = resolveHours(input.hours, config.hoursPerDay);
2479
+ if (hours === void 0) {
2480
+ throw new Error(`Hours must be full, half or a number, got "${input.hours}".`);
2481
+ }
2482
+ const previous = dayEntries(store, date).find((e) => e.clientId === target);
2483
+ const note = input.note !== void 0 ? input.note.trim() || void 0 : previous?.note;
2484
+ const changed = hours > 0 ? previous?.hours !== hours || previous?.note !== note : !!previous;
2485
+ if (changed) {
2486
+ await setWorklog(store, date, target, hours, note);
2487
+ }
2488
+ return {
2489
+ target,
2490
+ date,
2491
+ hours,
2492
+ previous,
2493
+ changed,
2494
+ dayTotal: roundHours(dayEntries(store, date).reduce((sum, e) => sum + e.hours, 0)),
2495
+ hoursPerDay: config.hoursPerDay
2496
+ };
2497
+ }
2498
+ function dayEntries(store, date) {
2499
+ return store.db.getAllWorklog().filter((e) => e.date === date);
2500
+ }
2501
+ function targetLabel(store, target) {
2502
+ if (isEventWorklogClientId(target)) {
2503
+ return eventLabel(eventTypeFromClientId(target));
2504
+ }
2505
+ return store.db.getClients().find((c) => c.id === target)?.name ?? target;
2506
+ }
2507
+
2508
+ // cli/worklog.ts
2509
+ var TEXT_PATH = /\.(md|json)$|\.worklog$/;
2510
+ var PUSH_ATTEMPTS = 3;
2511
+ var NONE = "none";
2512
+ var USAGE = `Usage: worklog <command> [options]
2513
+
2514
+ log Work that was done: marks the day worked on a task (creating the task
2515
+ when it's new) and adds the note.
2516
+ add Something still to do: creates an open task. No day is marked worked.
2517
+ Without --client it goes on the general to-do list.
2518
+ update Changes an existing task: due date, priority, description, ...
2519
+ done Closes a task. A recurring one rolls on to its next occurrence.
2520
+ list Prints the open tasks. Read-only.
2521
+ hours Sets the hours worked for a client (or a day event) on a day. Without
2522
+ --hours it prints what's logged that day.
2523
+
2524
+ All but list, and hours without --hours, commit and push to your timesheet repo.
2525
+
2526
+ Every command:
2527
+ --repo <path> timesheet clone (default: $WORKLOG_REPO)
2528
+ --client <id> client id from .worklog/config.json; "todos" is the
2529
+ general to-do list
2530
+
2531
+ Naming a task (log, update, done):
2532
+ --id <t_xxxxxx> this exact task
2533
+ --link <url> the task linked to this issue/ticket url
2534
+ --title <title> the open task with this title
2535
+
2536
+ log --client <id> --title <title>:
2537
+ --note <text> progress note added to the task
2538
+ --date <date> day worked, YYYY-MM-DD (default: today)
2539
+
2540
+ add --title <title>:
2541
+ --link <url> issue/ticket url
2542
+ --due <date> day the task is due, YYYY-MM-DD
2543
+ --priority <level> ${PRIORITIES.map((p) => p.id).join(", ")}
2544
+ --description <text> what the task is about
2545
+ --tag <tag> repeat for several tags
2546
+ --parent <t_xxxxxx> make it a subtask of this task
2547
+
2548
+ update:
2549
+ --rename <title> new title
2550
+ --due <date|none> new due date, or none to clear it
2551
+ --priority <level> new priority; normal clears it
2552
+ --description <text> new description; "" clears it
2553
+ --status <id> a status from .worklog/config.json
2554
+ --tag <tag> add a tag (repeatable)
2555
+ --untag <tag> remove a tag (repeatable)
2556
+ --parent <id|none> move it under this task, or none to make it top-level
2557
+
2558
+ hours --client <id> | --event <type>:
2559
+ --hours <amount> full, half, or a number of hours; 0 removes the entry.
2560
+ Sets the day's total for that client, it doesn't add
2561
+ --date <date> the day, YYYY-MM-DD (default: today)
2562
+ --note <text> what the time went to (default: keep the note there)
2563
+ --event <type> ${EVENT_TYPES.map((e) => e.id).join(", ")}
2564
+
2565
+ list:
2566
+ --due-by <date> only tasks due on or before this day
2567
+ --tag <tag> only tasks with this tag
2568
+ --json print JSON instead of one line per task
2569
+
2570
+ log, add, update, done, hours:
2571
+ --dry-run show the change, commit nothing`;
2572
+ var OPTIONS = {
2573
+ log: ["client", "id", "link", "title", "note", "date", "dry-run"],
2574
+ add: ["client", "link", "title", "due", "priority", "description", "tag", "parent", "dry-run"],
2575
+ update: [
2576
+ "client",
2577
+ "id",
2578
+ "link",
2579
+ "title",
2580
+ "rename",
2581
+ "due",
2582
+ "priority",
2583
+ "description",
2584
+ "status",
2585
+ "tag",
2586
+ "untag",
2587
+ "parent",
2588
+ "dry-run"
2589
+ ],
2590
+ done: ["client", "id", "link", "title", "dry-run"],
2591
+ list: ["client", "due-by", "tag", "json"],
2592
+ hours: ["client", "event", "hours", "date", "note", "dry-run"]
2593
+ };
2594
+ var HINTS = {
2595
+ date: `A to-do's date is --due; --date is the day worked, for "worklog log" and "worklog hours".`,
2596
+ due: "The day worked is --date; --due is when a to-do is due."
2597
+ };
2598
+ async function main() {
2599
+ const { values, positionals } = parseArgs({
2600
+ allowPositionals: true,
2601
+ options: {
2602
+ client: { type: "string" },
2603
+ title: { type: "string" },
2604
+ link: { type: "string" },
2605
+ id: { type: "string" },
2606
+ note: { type: "string" },
2607
+ date: { type: "string" },
2608
+ due: { type: "string" },
2609
+ "due-by": { type: "string" },
2610
+ priority: { type: "string" },
2611
+ description: { type: "string" },
2612
+ rename: { type: "string" },
2613
+ status: { type: "string" },
2614
+ tag: { type: "string", multiple: true },
2615
+ untag: { type: "string", multiple: true },
2616
+ parent: { type: "string" },
2617
+ event: { type: "string" },
2618
+ hours: { type: "string" },
2619
+ json: { type: "boolean" },
2620
+ repo: { type: "string" },
2621
+ "dry-run": { type: "boolean" },
2622
+ help: { type: "boolean", short: "h" }
2623
+ }
2624
+ });
2625
+ if (values.help || positionals.length === 0) {
2626
+ console.log(USAGE);
2627
+ return;
2628
+ }
2629
+ const command = positionals[0];
2630
+ if (!(command in OPTIONS) || positionals.length > 1) {
2631
+ throw new Error(`Unknown command "${positionals.join(" ")}".
2632
+
2633
+ ${USAGE}`);
2634
+ }
2635
+ const allowed = /* @__PURE__ */ new Set([...OPTIONS[command], "repo", "help"]);
2636
+ const misplaced = Object.keys(values).find((o) => !allowed.has(o));
2637
+ if (misplaced) {
2638
+ const hint = HINTS[misplaced] ? ` ${HINTS[misplaced]}` : "";
2639
+ throw new Error(`"worklog ${command}" has no --${misplaced} option.${hint}
2640
+
2641
+ ${USAGE}`);
2642
+ }
2643
+ validate(values);
2644
+ const repo = values.repo ?? process.env.WORKLOG_REPO;
2645
+ if (!repo) {
2646
+ throw new Error("Point --repo or $WORKLOG_REPO at a clone of your timesheet repo.");
2647
+ }
2648
+ if (command === "list") {
2649
+ return list(repo, values);
2650
+ }
2651
+ if (command === "hours" && values.hours === void 0) {
2652
+ return showDay(repo, values.date ?? today());
2653
+ }
2654
+ return run(repo, toEdit(command, values), !!values["dry-run"]);
2655
+ }
2656
+ function validate(values) {
2657
+ for (const option of ["date", "due", "due-by"]) {
2658
+ const value = values[option];
2659
+ if (typeof value === "string" && !(option === "due" && value === NONE) && !isValidISODate(value)) {
2660
+ throw new Error(`--${option} must be YYYY-MM-DD, got "${value}".`);
2661
+ }
2662
+ }
2663
+ const priority = values.priority;
2664
+ if (typeof priority === "string" && !PRIORITIES.some((p) => p.id === priority.trim().toLowerCase())) {
2665
+ throw new Error(`--priority must be one of ${PRIORITIES.map((p) => p.id).join(", ")}, got "${priority}".`);
2666
+ }
2667
+ }
2668
+ function toEdit(command, values) {
2669
+ const str = (key) => values[key];
2670
+ const list2 = (key) => values[key];
2671
+ const ref = { taskId: str("id"), link: str("link"), title: str("title"), clientId: str("client") };
2672
+ const orNull = (value) => value === NONE ? null : value;
2673
+ switch (command) {
2674
+ case "log": {
2675
+ const clientId = str("client");
2676
+ const title = str("title");
2677
+ if (!clientId || !title) {
2678
+ throw new Error(`--client and --title are required.
2679
+
2680
+ ${USAGE}`);
2681
+ }
2682
+ const input = { clientId, title, taskId: ref.taskId, link: ref.link, note: str("note"), date: str("date") };
2683
+ return async (store) => describeLog(await logWork(store, input), input);
2684
+ }
2685
+ case "add": {
2686
+ const title = str("title");
2687
+ if (!title) {
2688
+ throw new Error(`--title is required.
2689
+
2690
+ ${USAGE}`);
2691
+ }
2692
+ const input = {
2693
+ clientId: str("client") ?? GENERAL_TODO_CLIENT_ID,
2694
+ title,
2695
+ link: ref.link,
2696
+ due: str("due"),
2697
+ priority: str("priority"),
2698
+ description: str("description"),
2699
+ tags: list2("tag"),
2700
+ parentId: str("parent")
2701
+ };
2702
+ return async (store) => describeAdd(await addTask(store, input));
2703
+ }
2704
+ case "update": {
2705
+ requireRef(ref);
2706
+ const edit = {
2707
+ title: str("rename"),
2708
+ due: orNull(str("due")),
2709
+ priority: str("priority"),
2710
+ description: str("description"),
2711
+ status: str("status"),
2712
+ addTags: list2("tag"),
2713
+ removeTags: list2("untag"),
2714
+ parentId: orNull(str("parent"))
2715
+ };
2716
+ if (Object.values(edit).every((v) => v === void 0)) {
2717
+ throw new Error(`Nothing to change: pass at least one of --rename, --due, --priority, --description, --status, --tag, --untag, --parent.`);
2718
+ }
2719
+ return async (store) => describeUpdate(await editTask(store, ref, edit));
2720
+ }
2721
+ case "hours": {
2722
+ const input = { clientId: str("client"), event: str("event"), hours: str("hours") ?? "", note: str("note"), date: str("date") };
2723
+ return async (store) => describeHours(store, await logHours(store, input));
2724
+ }
2725
+ default: {
2726
+ requireRef(ref);
2727
+ return async (store) => describeDone(await closeTask(store, ref));
2728
+ }
2729
+ }
2730
+ }
2731
+ function requireRef(ref) {
2732
+ if (!ref.taskId && !ref.link && !ref.title) {
2733
+ throw new Error(`Name the task with --id, --link or --title.
2734
+
2735
+ ${USAGE}`);
2736
+ }
2737
+ }
2738
+ async function run(repo, edit, dryRun) {
2739
+ const git = gitIn(repo);
2740
+ if (git("status", "--porcelain", "--untracked-files=no")) {
2741
+ throw new Error(`${repo} has uncommitted changes; commit or discard them first.`);
2742
+ }
2743
+ git("fetch", "--quiet");
2744
+ if (git("rev-list", "--count", "@{u}..HEAD") !== "0") {
2745
+ throw new Error(`${repo} has unpushed commits; push them first.`);
2746
+ }
2747
+ git("merge", "--ff-only", "--quiet", "@{u}");
2748
+ for (let attempt = 1; ; attempt++) {
2749
+ const { line, changed } = await applyToClone(repo, edit);
2750
+ if (changed.length === 0) {
2751
+ console.log(`${line} \u2014 nothing new to write`);
2752
+ return;
2753
+ }
2754
+ if (dryRun) {
2755
+ git("add", "--intent-to-add", "--", ...changed);
2756
+ console.log(git("--no-pager", "diff", "--", ...changed));
2757
+ git("reset", "--quiet", "--hard", "HEAD");
2758
+ console.log(`${line} \u2014 dry run, nothing committed`);
2759
+ return;
2760
+ }
2761
+ git("add", "--", ...changed);
2762
+ git("commit", "--quiet", "-m", `chore: worklog sync ${today()}`);
2763
+ try {
2764
+ git("push", "--quiet");
2765
+ console.log(`${line} \u2014 pushed ${git("rev-parse", "--short", "HEAD")}`);
2766
+ return;
2767
+ } catch (err) {
2768
+ git("reset", "--quiet", "--hard", "@{u}");
2769
+ const raced = /rejected|fetch first|non-fast-forward/.test(String(err.stderr));
2770
+ if (!raced) {
2771
+ throw new Error("Push failed; nothing was written.", { cause: err });
2772
+ }
2773
+ if (attempt >= PUSH_ATTEMPTS) {
2774
+ throw new Error(`Push rejected ${PUSH_ATTEMPTS} times; nothing was written.`, { cause: err });
2775
+ }
2776
+ git("fetch", "--quiet");
2777
+ git("reset", "--quiet", "--hard", "@{u}");
2778
+ }
2779
+ }
2780
+ }
2781
+ function catchUp(repo) {
2782
+ const git = gitIn(repo);
2783
+ try {
2784
+ git("fetch", "--quiet");
2785
+ if (git("status", "--porcelain", "--untracked-files=no") || git("rev-list", "--count", "@{u}..HEAD") !== "0") {
2786
+ console.error(`${repo} has local changes; reading it as it is, without the latest from the remote.`);
2787
+ } else {
2788
+ git("merge", "--ff-only", "--quiet", "@{u}");
2789
+ }
2790
+ } catch {
2791
+ console.error("Could not fetch the remote; reading the clone as it is.");
2792
+ }
2793
+ }
2794
+ async function list(repo, values) {
2795
+ catchUp(repo);
2796
+ const { store } = await loadClone(repo);
2797
+ const tasks = listTasks(store, {
2798
+ clientId: values.client,
2799
+ dueBy: values["due-by"],
2800
+ tag: values.tag?.[0]
2801
+ });
2802
+ if (values.json) {
2803
+ console.log(JSON.stringify(tasks.map(toJson), null, 2));
2804
+ return;
2805
+ }
2806
+ if (tasks.length === 0) {
2807
+ console.log("No open tasks match.");
2808
+ return;
2809
+ }
2810
+ for (const t of tasks) {
2811
+ console.log(
2812
+ [t.id, t.due ?? "-", t.priority ?? "-", t.clientIds[0], t.status, t.title].join(" ")
2813
+ );
2814
+ }
2815
+ }
2816
+ async function showDay(repo, date) {
2817
+ catchUp(repo);
2818
+ const { store } = await loadClone(repo);
2819
+ const entries = dayEntries(store, date);
2820
+ const total = entries.reduce((sum, e) => sum + e.hours, 0);
2821
+ console.log(`${date}: ${total}h logged of ${store.getConfig().hoursPerDay}h`);
2822
+ for (const e of entries) {
2823
+ console.log(["", e.clientId, `${e.hours}h`, targetLabel(store, e.clientId), e.note ?? ""].join(" ").trimEnd());
2824
+ }
2825
+ }
2826
+ function toJson(t) {
2827
+ return {
2828
+ id: t.id,
2829
+ title: t.title,
2830
+ client: t.clientIds[0],
2831
+ status: t.status,
2832
+ priority: t.priority ?? "normal",
2833
+ due: t.due ?? null,
2834
+ tags: t.tags ?? [],
2835
+ links: t.links.map((l) => l.url),
2836
+ parent: t.parentId ?? null,
2837
+ repeats: !!t.repeat,
2838
+ created: t.created ?? null,
2839
+ description: t.description ?? null,
2840
+ file: t.sourceFile
2841
+ };
2842
+ }
2843
+ function gitIn(repo) {
2844
+ return (...args) => execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2845
+ }
2846
+ async function loadClone(repo) {
2847
+ const fm = new FileMap();
2848
+ for (const path of listFiles(repo)) {
2849
+ if (isWorklogPath(path) && TEXT_PATH.test(path)) {
2850
+ fm.text.set(path, readFileSync(join(repo, path), "utf8"));
2851
+ fm.remote.add(path);
2852
+ }
2853
+ }
2854
+ mountFileMap(fm);
2855
+ const store = new Store();
2856
+ await store.rebuild("load");
2857
+ return { store, fm };
2858
+ }
2859
+ async function applyToClone(repo, edit) {
2860
+ const { store, fm } = await loadClone(repo);
2861
+ const before = new Map(fm.text);
2862
+ const line = await edit(store);
2863
+ const changed = [...fm.dirty].filter((path) => fm.text.has(path) && fm.text.get(path) !== before.get(path));
2864
+ for (const path of changed) {
2865
+ mkdirSync(dirname(join(repo, path)), { recursive: true });
2866
+ writeFileSync(join(repo, path), fm.text.get(path) ?? "");
2867
+ }
2868
+ return { line, changed };
2869
+ }
2870
+ function listFiles(repo) {
2871
+ return readdirSync(repo, { recursive: true, withFileTypes: true }).filter((e) => e.isFile()).map((e) => relative(repo, join(e.parentPath, e.name)).split(sep).join("/")).filter((path) => !path.startsWith(".git/"));
2872
+ }
2873
+ function label(task) {
2874
+ return `${task.id} "${task.title}" (${task.sourceFile})`;
2875
+ }
2876
+ function describeLog({ task, created, markedWorked, noteAdded }, input) {
2877
+ const what = [
2878
+ created ? "created" : "found",
2879
+ markedWorked ? `marked worked ${input.date ?? today()}` : void 0,
2880
+ noteAdded ? "note added" : void 0
2881
+ ].filter(Boolean);
2882
+ return `${label(task)}: ${what.join(", ")}`;
2883
+ }
2884
+ function describeAdd({ task, created }) {
2885
+ const what = [
2886
+ created ? "created" : "already exists, left as is",
2887
+ task.due ? `due ${task.due}` : void 0,
2888
+ task.priority ? `priority ${task.priority}` : void 0
2889
+ ].filter(Boolean);
2890
+ return `${label(task)}: ${what.join(", ")}`;
2891
+ }
2892
+ function describeUpdate({ task, changed }) {
2893
+ return `${label(task)}: ${changed.length ? `changed ${changed.join(", ")}` : "already up to date"}`;
2894
+ }
2895
+ function describeHours(store, r) {
2896
+ const was = !r.previous ? "new" : r.previous.hours === r.hours && r.changed ? "note updated" : `was ${r.previous.hours}h`;
2897
+ const what = r.hours > 0 ? `${r.hours}h (${was})` : r.previous ? `removed (was ${r.previous.hours}h)` : "nothing logged";
2898
+ return `${targetLabel(store, r.target)} on ${r.date}: ${what}; day total ${r.dayTotal}h of ${r.hoursPerDay}h`;
2899
+ }
2900
+ function describeDone({ task, closed }) {
2901
+ if (!closed) {
2902
+ return `${label(task)}: ${task.completed ? `already closed on ${task.completed}` : `already done today, next due ${task.due}`}`;
2903
+ }
2904
+ return `${label(task)}: ${task.completed ? "closed" : `done, next due ${task.due}`}`;
2905
+ }
2906
+ main().catch((err) => {
2907
+ console.error(err instanceof Error ? err.message : String(err));
2908
+ process.exit(1);
2909
+ });