@mulmoclaude/todo-plugin 0.1.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.
- package/dist/Preview.vue.d.ts +8 -0
- package/dist/View.vue.d.ts +12 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/useTodos.d.ts +26 -0
- package/dist/composables.js +101 -0
- package/dist/composables.js.map +1 -0
- package/dist/definition-mymex4HE.js +55 -0
- package/dist/definition-mymex4HE.js.map +1 -0
- package/dist/definition.d.ts +43 -0
- package/dist/handlers/columns.d.ts +31 -0
- package/dist/handlers/items.d.ts +36 -0
- package/dist/handlers/llm.d.ts +30 -0
- package/dist/handlers/priority-notifier.d.ts +55 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +1223 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/utils.d.ts +4 -0
- package/dist/io.d.ts +6 -0
- package/dist/labels-C4z7FMoE.js +85 -0
- package/dist/labels-C4z7FMoE.js.map +1 -0
- package/dist/labels.d.ts +15 -0
- package/dist/lang/de.d.ts +25 -0
- package/dist/lang/en.d.ts +25 -0
- package/dist/lang/es.d.ts +25 -0
- package/dist/lang/fr.d.ts +25 -0
- package/dist/lang/index.d.ts +28 -0
- package/dist/lang/ja.d.ts +25 -0
- package/dist/lang/ko.d.ts +25 -0
- package/dist/lang/pt-BR.d.ts +25 -0
- package/dist/lang/zh.d.ts +25 -0
- package/dist/lang-D72AIF9U.js +215 -0
- package/dist/lang-D72AIF9U.js.map +1 -0
- package/dist/priority.d.ts +10 -0
- package/dist/shared.d.ts +13 -0
- package/dist/shared.js +89 -0
- package/dist/shared.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/viewModes.d.ts +11 -0
- package/dist/vue.d.ts +59 -0
- package/dist/vue.js +422 -0
- package/dist/vue.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
import { t as TOOL_DEFINITION } from "./definition-mymex4HE.js";
|
|
2
|
+
import { a as listLabelsWithCount, c as subtractLabels, o as mergeLabels, r as filterByLabels } from "./labels-C4z7FMoE.js";
|
|
3
|
+
//#region ../../../node_modules/gui-chat-protocol/dist/index.js
|
|
4
|
+
function definePlugin(setup) {
|
|
5
|
+
return setup;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/internal/utils.ts
|
|
9
|
+
function makeId(prefix) {
|
|
10
|
+
return `${prefix}_${globalThis.crypto.randomUUID()}`;
|
|
11
|
+
}
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
var MAX_SLUG_LEN = 60;
|
|
16
|
+
function slugify$1(label, fallback) {
|
|
17
|
+
const normalised = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_SLUG_LEN);
|
|
18
|
+
return normalised.length > 0 ? normalised : fallback;
|
|
19
|
+
}
|
|
20
|
+
function disambiguateSlug(base, existingIds) {
|
|
21
|
+
if (!existingIds.has(base)) return base;
|
|
22
|
+
let n = 2;
|
|
23
|
+
while (existingIds.has(`${base}-${n}`)) n += 1;
|
|
24
|
+
return `${base}-${n}`;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/handlers/columns.ts
|
|
28
|
+
var DEFAULT_COLUMNS = [
|
|
29
|
+
{
|
|
30
|
+
id: "backlog",
|
|
31
|
+
label: "Backlog"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "todo",
|
|
35
|
+
label: "Todo"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "in-progress",
|
|
39
|
+
label: "In Progress"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "done",
|
|
43
|
+
label: "Done",
|
|
44
|
+
isDone: true
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
function slugify(label) {
|
|
48
|
+
return slugify$1(label, "column");
|
|
49
|
+
}
|
|
50
|
+
function uniqueId(base, existingIds) {
|
|
51
|
+
return disambiguateSlug(base, existingIds);
|
|
52
|
+
}
|
|
53
|
+
function findColumn(columns, columnId) {
|
|
54
|
+
return columns.find((column) => column.id === columnId);
|
|
55
|
+
}
|
|
56
|
+
function ensureColumnsValid(columns) {
|
|
57
|
+
if (columns.length === 0) return [...DEFAULT_COLUMNS];
|
|
58
|
+
const seen = /* @__PURE__ */ new Set();
|
|
59
|
+
for (const column of columns) {
|
|
60
|
+
if (seen.has(column.id)) return [...DEFAULT_COLUMNS];
|
|
61
|
+
seen.add(column.id);
|
|
62
|
+
}
|
|
63
|
+
const doneCount = columns.filter((column) => column.isDone).length;
|
|
64
|
+
if (doneCount === 0) return columns.map((column, i) => i === columns.length - 1 ? {
|
|
65
|
+
...column,
|
|
66
|
+
isDone: true
|
|
67
|
+
} : column);
|
|
68
|
+
if (doneCount > 1) {
|
|
69
|
+
let kept = false;
|
|
70
|
+
return columns.map((column) => {
|
|
71
|
+
if (!column.isDone) return column;
|
|
72
|
+
if (kept) return {
|
|
73
|
+
id: column.id,
|
|
74
|
+
label: column.label
|
|
75
|
+
};
|
|
76
|
+
kept = true;
|
|
77
|
+
return column;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return columns;
|
|
81
|
+
}
|
|
82
|
+
function normalizeColumns(raw) {
|
|
83
|
+
if (!Array.isArray(raw)) return [...DEFAULT_COLUMNS];
|
|
84
|
+
const cleaned = [];
|
|
85
|
+
for (const entry of raw) {
|
|
86
|
+
if (!isRecord(entry)) continue;
|
|
87
|
+
if (typeof entry["id"] !== "string" || typeof entry["label"] !== "string") continue;
|
|
88
|
+
const col = {
|
|
89
|
+
id: entry["id"],
|
|
90
|
+
label: entry["label"]
|
|
91
|
+
};
|
|
92
|
+
if (entry["isDone"] === true) col.isDone = true;
|
|
93
|
+
cleaned.push(col);
|
|
94
|
+
}
|
|
95
|
+
return ensureColumnsValid(cleaned);
|
|
96
|
+
}
|
|
97
|
+
function doneColumnId(columns) {
|
|
98
|
+
const done = columns.find((column) => column.isDone);
|
|
99
|
+
if (done) return done.id;
|
|
100
|
+
const last = columns[columns.length - 1];
|
|
101
|
+
if (!last) throw new Error("doneColumnId: empty columns array (normalizeColumns invariant violated)");
|
|
102
|
+
return last.id;
|
|
103
|
+
}
|
|
104
|
+
function defaultStatusId(columns) {
|
|
105
|
+
const open = columns.find((column) => !column.isDone);
|
|
106
|
+
return open ? open.id : doneColumnId(columns);
|
|
107
|
+
}
|
|
108
|
+
function resyncDoneMembership(items, newDoneId) {
|
|
109
|
+
let changed = false;
|
|
110
|
+
return {
|
|
111
|
+
items: items.map((item) => {
|
|
112
|
+
const shouldBeDone = item.status === newDoneId;
|
|
113
|
+
if (item.completed === shouldBeDone) return item;
|
|
114
|
+
changed = true;
|
|
115
|
+
return {
|
|
116
|
+
...item,
|
|
117
|
+
completed: shouldBeDone
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
changed
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
var ORDER_STEP$1 = 1e3;
|
|
124
|
+
function rebuildColumnOrder(items, columnId) {
|
|
125
|
+
const inColumn = items.filter((item) => item.status === columnId).sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
|
|
126
|
+
const newOrders = /* @__PURE__ */ new Map();
|
|
127
|
+
inColumn.forEach((item, index) => newOrders.set(item.id, (index + 1) * ORDER_STEP$1));
|
|
128
|
+
return items.map((item) => {
|
|
129
|
+
const newOrder = newOrders.get(item.id);
|
|
130
|
+
if (newOrder === void 0) return item;
|
|
131
|
+
return {
|
|
132
|
+
...item,
|
|
133
|
+
order: newOrder
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function handleAddColumn(columns, items, input) {
|
|
138
|
+
if (!input.label || input.label.trim().length === 0) return {
|
|
139
|
+
kind: "error",
|
|
140
|
+
status: 400,
|
|
141
|
+
error: "label required"
|
|
142
|
+
};
|
|
143
|
+
const columnId = uniqueId(slugify(input.label), new Set(columns.map((column) => column.id)));
|
|
144
|
+
const columnToAdd = {
|
|
145
|
+
id: columnId,
|
|
146
|
+
label: input.label.trim()
|
|
147
|
+
};
|
|
148
|
+
if (input.isDone === true) columnToAdd.isDone = true;
|
|
149
|
+
if (input.isDone === true) {
|
|
150
|
+
const nextColumns = [...columns.map((column) => ({
|
|
151
|
+
...column,
|
|
152
|
+
isDone: false
|
|
153
|
+
})), columnToAdd];
|
|
154
|
+
const { items: nextItems, changed } = resyncDoneMembership(items, columnId);
|
|
155
|
+
return {
|
|
156
|
+
kind: "success",
|
|
157
|
+
columns: nextColumns,
|
|
158
|
+
...changed ? { items: nextItems } : {}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
kind: "success",
|
|
163
|
+
columns: [...columns, columnToAdd]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function handlePatchColumn(columns, columnId, input, items) {
|
|
167
|
+
const target = findColumn(columns, columnId);
|
|
168
|
+
if (!target) return {
|
|
169
|
+
kind: "error",
|
|
170
|
+
status: 404,
|
|
171
|
+
error: `column not found: ${columnId}`
|
|
172
|
+
};
|
|
173
|
+
const patched = {
|
|
174
|
+
id: target.id,
|
|
175
|
+
label: target.label
|
|
176
|
+
};
|
|
177
|
+
if (target.isDone) patched.isDone = true;
|
|
178
|
+
if (typeof input.label === "string" && input.label.trim().length > 0) patched.label = input.label.trim();
|
|
179
|
+
let nextColumns = columns.map((column) => column.id === columnId ? patched : column);
|
|
180
|
+
let itemsChanged = false;
|
|
181
|
+
let nextItems = items;
|
|
182
|
+
if (input.isDone === true && !target.isDone) {
|
|
183
|
+
nextColumns = nextColumns.map((column) => column.id === columnId ? {
|
|
184
|
+
...column,
|
|
185
|
+
isDone: true
|
|
186
|
+
} : {
|
|
187
|
+
id: column.id,
|
|
188
|
+
label: column.label
|
|
189
|
+
});
|
|
190
|
+
const synced = resyncDoneMembership(items, columnId);
|
|
191
|
+
nextItems = synced.items;
|
|
192
|
+
itemsChanged = synced.changed;
|
|
193
|
+
} else if (input.isDone === false && target.isDone) return {
|
|
194
|
+
kind: "error",
|
|
195
|
+
status: 400,
|
|
196
|
+
error: "at least one column must be marked as done"
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
kind: "success",
|
|
200
|
+
columns: nextColumns,
|
|
201
|
+
...itemsChanged ? { items: nextItems } : {}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function handleDeleteColumn(columns, columnId, items) {
|
|
205
|
+
if (columns.length <= 1) return {
|
|
206
|
+
kind: "error",
|
|
207
|
+
status: 400,
|
|
208
|
+
error: "cannot delete the last remaining column"
|
|
209
|
+
};
|
|
210
|
+
const target = findColumn(columns, columnId);
|
|
211
|
+
if (!target) return {
|
|
212
|
+
kind: "error",
|
|
213
|
+
status: 404,
|
|
214
|
+
error: `column not found: ${columnId}`
|
|
215
|
+
};
|
|
216
|
+
const remaining = columns.filter((column) => column.id !== columnId);
|
|
217
|
+
let nextColumns = remaining;
|
|
218
|
+
if (target.isDone) nextColumns = remaining.map((column, i) => i === remaining.length - 1 ? {
|
|
219
|
+
...column,
|
|
220
|
+
isDone: true
|
|
221
|
+
} : column);
|
|
222
|
+
const newDoneId = doneColumnId(nextColumns);
|
|
223
|
+
const refugeId = target.isDone ? newDoneId : defaultStatusId(nextColumns);
|
|
224
|
+
let itemsChanged = false;
|
|
225
|
+
let nextItems = items.map((item) => {
|
|
226
|
+
if (item.status !== columnId) return item;
|
|
227
|
+
itemsChanged = true;
|
|
228
|
+
return {
|
|
229
|
+
...item,
|
|
230
|
+
status: refugeId
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
if (itemsChanged) nextItems = rebuildColumnOrder(nextItems, refugeId);
|
|
234
|
+
if (target.isDone) {
|
|
235
|
+
const synced = resyncDoneMembership(nextItems, newDoneId);
|
|
236
|
+
nextItems = synced.items;
|
|
237
|
+
itemsChanged = itemsChanged || synced.changed;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
kind: "success",
|
|
241
|
+
columns: nextColumns,
|
|
242
|
+
...itemsChanged ? { items: nextItems } : {}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function handleReorderColumns(columns, ids) {
|
|
246
|
+
if (!Array.isArray(ids)) return {
|
|
247
|
+
kind: "error",
|
|
248
|
+
status: 400,
|
|
249
|
+
error: "ids array required"
|
|
250
|
+
};
|
|
251
|
+
if (ids.length !== columns.length) return {
|
|
252
|
+
kind: "error",
|
|
253
|
+
status: 400,
|
|
254
|
+
error: "ids must contain every existing column id exactly once"
|
|
255
|
+
};
|
|
256
|
+
const known = new Set(columns.map((column) => column.id));
|
|
257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
258
|
+
for (const columnId of ids) {
|
|
259
|
+
if (!known.has(columnId) || seen.has(columnId)) return {
|
|
260
|
+
kind: "error",
|
|
261
|
+
status: 400,
|
|
262
|
+
error: "ids must contain every existing column id exactly once"
|
|
263
|
+
};
|
|
264
|
+
seen.add(columnId);
|
|
265
|
+
}
|
|
266
|
+
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
267
|
+
return {
|
|
268
|
+
kind: "success",
|
|
269
|
+
columns: ids.map((columnId) => {
|
|
270
|
+
const column = byId.get(columnId);
|
|
271
|
+
if (!column) throw new Error(`reorderColumns: missing column for id "${columnId}" (set-equality check above missed it)`);
|
|
272
|
+
return column;
|
|
273
|
+
})
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/handlers/items.ts
|
|
278
|
+
var ORDER_STEP = 1e3;
|
|
279
|
+
var PRIORITIES = [
|
|
280
|
+
"low",
|
|
281
|
+
"medium",
|
|
282
|
+
"high",
|
|
283
|
+
"urgent"
|
|
284
|
+
];
|
|
285
|
+
function migrateItems(rawItems, columns) {
|
|
286
|
+
const doneId = doneColumnId(columns);
|
|
287
|
+
const openId = defaultStatusId(columns);
|
|
288
|
+
const validStatusIds = new Set(columns.map((column) => column.id));
|
|
289
|
+
const withStatus = rawItems.map((item) => {
|
|
290
|
+
if (typeof item.status === "string" && validStatusIds.has(item.status)) return item;
|
|
291
|
+
const status = item.completed ? doneId : openId;
|
|
292
|
+
return {
|
|
293
|
+
...item,
|
|
294
|
+
status
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
298
|
+
for (const item of withStatus) {
|
|
299
|
+
const key = item.status ?? openId;
|
|
300
|
+
let bucket = byStatus.get(key);
|
|
301
|
+
if (!bucket) {
|
|
302
|
+
bucket = [];
|
|
303
|
+
byStatus.set(key, bucket);
|
|
304
|
+
}
|
|
305
|
+
bucket.push(item);
|
|
306
|
+
}
|
|
307
|
+
const orderById = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const [, group] of byStatus) {
|
|
309
|
+
const missing = group.filter((item) => typeof item.order !== "number");
|
|
310
|
+
if (missing.length === 0) continue;
|
|
311
|
+
const existingMax = group.filter((item) => typeof item.order === "number").reduce((acc, item) => Math.max(acc, item.order ?? 0), 0);
|
|
312
|
+
[...missing].sort((left, right) => left.createdAt - right.createdAt).forEach((item, i) => {
|
|
313
|
+
orderById.set(item.id, existingMax + (i + 1) * ORDER_STEP);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return withStatus.map((item) => {
|
|
317
|
+
const next = orderById.get(item.id);
|
|
318
|
+
if (next === void 0) return item;
|
|
319
|
+
return {
|
|
320
|
+
...item,
|
|
321
|
+
order: next
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function isPriority(value) {
|
|
326
|
+
return typeof value === "string" && PRIORITIES.includes(value);
|
|
327
|
+
}
|
|
328
|
+
function isDueDate(value) {
|
|
329
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
330
|
+
}
|
|
331
|
+
function nextOrder(items, statusId) {
|
|
332
|
+
const inColumn = items.filter((item) => item.status === statusId).map((item) => item.order ?? 0);
|
|
333
|
+
if (inColumn.length === 0) return ORDER_STEP;
|
|
334
|
+
return Math.max(...inColumn) + ORDER_STEP;
|
|
335
|
+
}
|
|
336
|
+
function resolveStatus(input, columns) {
|
|
337
|
+
if (input.status === void 0 || input.status === "") return {
|
|
338
|
+
kind: "ok",
|
|
339
|
+
status: defaultStatusId(columns)
|
|
340
|
+
};
|
|
341
|
+
if (new Set(columns.map((column) => column.id)).has(input.status)) return {
|
|
342
|
+
kind: "ok",
|
|
343
|
+
status: input.status
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
kind: "error",
|
|
347
|
+
status: 400,
|
|
348
|
+
error: `unknown status: ${input.status}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function applyOptionalFields(item, input) {
|
|
352
|
+
if (input.priority !== void 0 && input.priority !== "") {
|
|
353
|
+
if (!isPriority(input.priority)) return {
|
|
354
|
+
kind: "error",
|
|
355
|
+
status: 400,
|
|
356
|
+
error: "invalid priority"
|
|
357
|
+
};
|
|
358
|
+
item.priority = input.priority;
|
|
359
|
+
}
|
|
360
|
+
if (input.dueDate !== void 0 && input.dueDate !== "") {
|
|
361
|
+
if (!isDueDate(input.dueDate)) return {
|
|
362
|
+
kind: "error",
|
|
363
|
+
status: 400,
|
|
364
|
+
error: "dueDate must be YYYY-MM-DD"
|
|
365
|
+
};
|
|
366
|
+
item.dueDate = input.dueDate;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function handleCreate(items, columns, input) {
|
|
371
|
+
if (!input.text || input.text.trim().length === 0) return {
|
|
372
|
+
kind: "error",
|
|
373
|
+
status: 400,
|
|
374
|
+
error: "text required"
|
|
375
|
+
};
|
|
376
|
+
const resolved = resolveStatus(input, columns);
|
|
377
|
+
if (resolved.kind === "error") return resolved;
|
|
378
|
+
const { status } = resolved;
|
|
379
|
+
const item = {
|
|
380
|
+
id: makeId("todo"),
|
|
381
|
+
text: input.text.trim(),
|
|
382
|
+
completed: status === doneColumnId(columns),
|
|
383
|
+
createdAt: Date.now(),
|
|
384
|
+
status,
|
|
385
|
+
order: nextOrder(items, status)
|
|
386
|
+
};
|
|
387
|
+
if (input.note !== void 0 && input.note !== "") item.note = input.note;
|
|
388
|
+
const normalizedLabels = mergeLabels([], input.labels ?? []);
|
|
389
|
+
if (normalizedLabels.length > 0) item.labels = normalizedLabels;
|
|
390
|
+
const fieldError = applyOptionalFields(item, input);
|
|
391
|
+
if (fieldError) return fieldError;
|
|
392
|
+
return {
|
|
393
|
+
kind: "success",
|
|
394
|
+
items: [...items, item],
|
|
395
|
+
item
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function applyTextPatch(updated, input) {
|
|
399
|
+
if (typeof input.text !== "string") return null;
|
|
400
|
+
if (input.text.trim().length === 0) return {
|
|
401
|
+
kind: "error",
|
|
402
|
+
status: 400,
|
|
403
|
+
error: "text cannot be empty"
|
|
404
|
+
};
|
|
405
|
+
updated.text = input.text.trim();
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function applyNotePatch(updated, input) {
|
|
409
|
+
if (input.note === null || input.note === "") {
|
|
410
|
+
delete updated.note;
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
if (typeof input.note === "string") updated.note = input.note;
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function applyLabelsPatch(updated, input) {
|
|
417
|
+
if (!Array.isArray(input.labels)) return null;
|
|
418
|
+
const merged = mergeLabels([], input.labels);
|
|
419
|
+
if (merged.length > 0) updated.labels = merged;
|
|
420
|
+
else delete updated.labels;
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
function applyPriorityPatch(updated, input) {
|
|
424
|
+
if (input.priority === null || input.priority === "") {
|
|
425
|
+
delete updated.priority;
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
if (input.priority === void 0) return null;
|
|
429
|
+
if (!isPriority(input.priority)) return {
|
|
430
|
+
kind: "error",
|
|
431
|
+
status: 400,
|
|
432
|
+
error: "invalid priority"
|
|
433
|
+
};
|
|
434
|
+
updated.priority = input.priority;
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function applyDueDatePatch(updated, input) {
|
|
438
|
+
if (input.dueDate === null || input.dueDate === "") {
|
|
439
|
+
delete updated.dueDate;
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
if (input.dueDate === void 0) return null;
|
|
443
|
+
if (!isDueDate(input.dueDate)) return {
|
|
444
|
+
kind: "error",
|
|
445
|
+
status: 400,
|
|
446
|
+
error: "dueDate must be YYYY-MM-DD"
|
|
447
|
+
};
|
|
448
|
+
updated.dueDate = input.dueDate;
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
function applyStatusPatch(updated, target, items, columns, input) {
|
|
452
|
+
if (typeof input.status !== "string" || input.status === target.status) return null;
|
|
453
|
+
if (!new Set(columns.map((column) => column.id)).has(input.status)) return {
|
|
454
|
+
kind: "error",
|
|
455
|
+
status: 400,
|
|
456
|
+
error: `unknown status: ${input.status}`
|
|
457
|
+
};
|
|
458
|
+
updated.status = input.status;
|
|
459
|
+
updated.order = nextOrder(items, input.status);
|
|
460
|
+
updated.completed = input.status === doneColumnId(columns);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
function applyCompletedPatch(updated, items, columns, input) {
|
|
464
|
+
if (typeof input.completed !== "boolean") return null;
|
|
465
|
+
if (input.completed === updated.completed) return null;
|
|
466
|
+
updated.completed = input.completed;
|
|
467
|
+
const targetStatus = input.completed ? doneColumnId(columns) : defaultStatusId(columns);
|
|
468
|
+
if (targetStatus !== updated.status) {
|
|
469
|
+
updated.status = targetStatus;
|
|
470
|
+
updated.order = nextOrder(items, targetStatus);
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function handlePatch(items, columns, itemId, input) {
|
|
475
|
+
const target = items.find((item) => item.id === itemId);
|
|
476
|
+
if (!target) return {
|
|
477
|
+
kind: "error",
|
|
478
|
+
status: 404,
|
|
479
|
+
error: `item not found: ${itemId}`
|
|
480
|
+
};
|
|
481
|
+
const updated = { ...target };
|
|
482
|
+
const steps = [
|
|
483
|
+
() => applyTextPatch(updated, input),
|
|
484
|
+
() => applyNotePatch(updated, input),
|
|
485
|
+
() => applyLabelsPatch(updated, input),
|
|
486
|
+
() => applyPriorityPatch(updated, input),
|
|
487
|
+
() => applyDueDatePatch(updated, input),
|
|
488
|
+
() => applyStatusPatch(updated, target, items, columns, input),
|
|
489
|
+
() => applyCompletedPatch(updated, items, columns, input)
|
|
490
|
+
];
|
|
491
|
+
for (const step of steps) {
|
|
492
|
+
const err = step();
|
|
493
|
+
if (err) return err;
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
kind: "success",
|
|
497
|
+
items: items.map((item) => item.id === itemId ? updated : item),
|
|
498
|
+
item: updated
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function handleMove(items, columns, itemId, input) {
|
|
502
|
+
const target = items.find((item) => item.id === itemId);
|
|
503
|
+
if (!target) return {
|
|
504
|
+
kind: "error",
|
|
505
|
+
status: 404,
|
|
506
|
+
error: `item not found: ${itemId}`
|
|
507
|
+
};
|
|
508
|
+
const validStatusIds = new Set(columns.map((column) => column.id));
|
|
509
|
+
const newStatus = input.status ?? target.status ?? defaultStatusId(columns);
|
|
510
|
+
if (!validStatusIds.has(newStatus)) return {
|
|
511
|
+
kind: "error",
|
|
512
|
+
status: 400,
|
|
513
|
+
error: `unknown status: ${newStatus}`
|
|
514
|
+
};
|
|
515
|
+
const isDone = newStatus === doneColumnId(columns);
|
|
516
|
+
const updatedSelf = {
|
|
517
|
+
...target,
|
|
518
|
+
status: newStatus,
|
|
519
|
+
completed: isDone
|
|
520
|
+
};
|
|
521
|
+
const others = items.filter((item) => item.id !== itemId && item.status === newStatus).sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
|
|
522
|
+
const insertAt = clampPosition(input.position, others.length);
|
|
523
|
+
const reordered = [...others];
|
|
524
|
+
reordered.splice(insertAt, 0, updatedSelf);
|
|
525
|
+
const reorderedById = /* @__PURE__ */ new Map();
|
|
526
|
+
reordered.forEach((item, i) => reorderedById.set(item.id, (i + 1) * ORDER_STEP));
|
|
527
|
+
const nextItems = items.map((item) => {
|
|
528
|
+
const newOrder = reorderedById.get(item.id);
|
|
529
|
+
if (item.id === itemId) return {
|
|
530
|
+
...updatedSelf,
|
|
531
|
+
order: newOrder ?? updatedSelf.order ?? ORDER_STEP
|
|
532
|
+
};
|
|
533
|
+
if (newOrder !== void 0) return {
|
|
534
|
+
...item,
|
|
535
|
+
order: newOrder
|
|
536
|
+
};
|
|
537
|
+
return item;
|
|
538
|
+
});
|
|
539
|
+
const finalSelf = nextItems.find((item) => item.id === itemId);
|
|
540
|
+
if (!finalSelf) throw new Error(`reorder result missing item ${itemId}`);
|
|
541
|
+
return {
|
|
542
|
+
kind: "success",
|
|
543
|
+
items: nextItems,
|
|
544
|
+
item: finalSelf
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function clampPosition(raw, max) {
|
|
548
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return max;
|
|
549
|
+
if (raw < 0) return 0;
|
|
550
|
+
if (raw > max) return max;
|
|
551
|
+
return Math.floor(raw);
|
|
552
|
+
}
|
|
553
|
+
function handleDeleteItem(items, itemId) {
|
|
554
|
+
if (!items.find((item) => item.id === itemId)) return {
|
|
555
|
+
kind: "error",
|
|
556
|
+
status: 404,
|
|
557
|
+
error: `item not found: ${itemId}`
|
|
558
|
+
};
|
|
559
|
+
return {
|
|
560
|
+
kind: "success",
|
|
561
|
+
items: items.filter((item) => item.id !== itemId)
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
//#endregion
|
|
565
|
+
//#region src/io.ts
|
|
566
|
+
var TODOS_FILE = "todos.json";
|
|
567
|
+
var COLUMNS_FILE = "columns.json";
|
|
568
|
+
async function readJson(files, rel) {
|
|
569
|
+
if (!await files.exists(rel)) return void 0;
|
|
570
|
+
try {
|
|
571
|
+
return JSON.parse(await files.read(rel));
|
|
572
|
+
} catch {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function isTodoItem(value) {
|
|
577
|
+
if (!value || typeof value !== "object") return false;
|
|
578
|
+
const obj = value;
|
|
579
|
+
return typeof obj.id === "string" && typeof obj.text === "string" && typeof obj.completed === "boolean" && typeof obj.createdAt === "number";
|
|
580
|
+
}
|
|
581
|
+
async function loadColumns(files) {
|
|
582
|
+
return normalizeColumns(await readJson(files, COLUMNS_FILE) ?? DEFAULT_COLUMNS);
|
|
583
|
+
}
|
|
584
|
+
async function saveColumns(files, columns) {
|
|
585
|
+
await files.write(COLUMNS_FILE, JSON.stringify(columns, null, 2));
|
|
586
|
+
}
|
|
587
|
+
async function loadTodos(files) {
|
|
588
|
+
const raw = await readJson(files, TODOS_FILE);
|
|
589
|
+
return migrateItems(Array.isArray(raw) ? raw.filter(isTodoItem) : [], await loadColumns(files));
|
|
590
|
+
}
|
|
591
|
+
async function saveTodos(files, items) {
|
|
592
|
+
await files.write(TODOS_FILE, JSON.stringify(items, null, 2));
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/handlers/llm.ts
|
|
596
|
+
function findTodoByText(items, text) {
|
|
597
|
+
const needle = text.toLowerCase();
|
|
598
|
+
return items.find((i) => i.text.toLowerCase().includes(needle));
|
|
599
|
+
}
|
|
600
|
+
function handleShow(items, input) {
|
|
601
|
+
const filterLabels = input.filterLabels ?? [];
|
|
602
|
+
const filtered = filterByLabels(items, filterLabels);
|
|
603
|
+
return {
|
|
604
|
+
kind: "success",
|
|
605
|
+
items: filtered,
|
|
606
|
+
message: filterLabels.length > 0 ? `Showing ${filtered.length} of ${items.length} todo item(s) filtered by: ${filterLabels.join(", ")}` : `Showing ${items.length} todo item(s)`,
|
|
607
|
+
jsonData: { items: filtered.map((i) => ({
|
|
608
|
+
text: i.text,
|
|
609
|
+
completed: i.completed,
|
|
610
|
+
...i.labels && i.labels.length > 0 && { labels: i.labels }
|
|
611
|
+
})) }
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function handleAdd(items, input) {
|
|
615
|
+
if (!input.text) return {
|
|
616
|
+
kind: "error",
|
|
617
|
+
status: 400,
|
|
618
|
+
error: "text required"
|
|
619
|
+
};
|
|
620
|
+
const normalizedLabels = mergeLabels([], input.labels ?? []);
|
|
621
|
+
const item = {
|
|
622
|
+
id: makeId("todo"),
|
|
623
|
+
text: input.text,
|
|
624
|
+
...input.note !== void 0 && { note: input.note },
|
|
625
|
+
...normalizedLabels.length > 0 && { labels: normalizedLabels },
|
|
626
|
+
completed: false,
|
|
627
|
+
createdAt: Date.now()
|
|
628
|
+
};
|
|
629
|
+
return {
|
|
630
|
+
kind: "success",
|
|
631
|
+
items: [...items, item],
|
|
632
|
+
message: normalizedLabels.length > 0 ? `Added: "${input.text}" [${normalizedLabels.join(", ")}]` : `Added: "${input.text}"`,
|
|
633
|
+
jsonData: {
|
|
634
|
+
added: input.text,
|
|
635
|
+
labels: normalizedLabels
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function handleDelete(items, input) {
|
|
640
|
+
if (!input.text) return {
|
|
641
|
+
kind: "error",
|
|
642
|
+
status: 400,
|
|
643
|
+
error: "text required"
|
|
644
|
+
};
|
|
645
|
+
const needle = input.text.toLowerCase();
|
|
646
|
+
const next = items.filter((i) => !i.text.toLowerCase().includes(needle));
|
|
647
|
+
return {
|
|
648
|
+
kind: "success",
|
|
649
|
+
items: next,
|
|
650
|
+
message: next.length < items.length ? `Deleted: "${input.text}"` : `Item not found: "${input.text}"`,
|
|
651
|
+
jsonData: { deleted: input.text }
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
function handleUpdate(items, input) {
|
|
655
|
+
if (!input.text || !input.newText) return {
|
|
656
|
+
kind: "error",
|
|
657
|
+
status: 400,
|
|
658
|
+
error: "text and newText required"
|
|
659
|
+
};
|
|
660
|
+
const target = findTodoByText(items, input.text);
|
|
661
|
+
if (!target) return {
|
|
662
|
+
kind: "success",
|
|
663
|
+
items,
|
|
664
|
+
message: `Item not found: "${input.text}"`,
|
|
665
|
+
jsonData: {}
|
|
666
|
+
};
|
|
667
|
+
const oldText = target.text;
|
|
668
|
+
const updated = {
|
|
669
|
+
...target,
|
|
670
|
+
text: input.newText,
|
|
671
|
+
note: input.note !== void 0 ? input.note || void 0 : target.note
|
|
672
|
+
};
|
|
673
|
+
return {
|
|
674
|
+
kind: "success",
|
|
675
|
+
items: items.map((i) => i.id === target.id ? updated : i),
|
|
676
|
+
message: `Updated: "${oldText}" → "${input.newText}"`,
|
|
677
|
+
jsonData: {
|
|
678
|
+
oldText,
|
|
679
|
+
newText: input.newText
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function setCompleted(items, input, completed, verb, jsonKey) {
|
|
684
|
+
if (!input.text) return {
|
|
685
|
+
kind: "error",
|
|
686
|
+
status: 400,
|
|
687
|
+
error: "text required"
|
|
688
|
+
};
|
|
689
|
+
const target = findTodoByText(items, input.text);
|
|
690
|
+
if (!target) return {
|
|
691
|
+
kind: "success",
|
|
692
|
+
items,
|
|
693
|
+
message: `Item not found: "${input.text}"`,
|
|
694
|
+
jsonData: {}
|
|
695
|
+
};
|
|
696
|
+
const updated = {
|
|
697
|
+
...target,
|
|
698
|
+
completed
|
|
699
|
+
};
|
|
700
|
+
return {
|
|
701
|
+
kind: "success",
|
|
702
|
+
items: items.map((i) => i.id === target.id ? updated : i),
|
|
703
|
+
message: `${verb}: "${target.text}"`,
|
|
704
|
+
jsonData: { [jsonKey]: target.text }
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function handleCheck(items, input) {
|
|
708
|
+
return setCompleted(items, input, true, "Checked", "checkedItem");
|
|
709
|
+
}
|
|
710
|
+
function handleUncheck(items, input) {
|
|
711
|
+
return setCompleted(items, input, false, "Unchecked", "uncheckedItem");
|
|
712
|
+
}
|
|
713
|
+
function handleClearCompleted(items) {
|
|
714
|
+
const count = items.filter((i) => i.completed).length;
|
|
715
|
+
return {
|
|
716
|
+
kind: "success",
|
|
717
|
+
items: items.filter((i) => !i.completed),
|
|
718
|
+
message: `Cleared ${count} completed item(s)`,
|
|
719
|
+
jsonData: { clearedCount: count }
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function handleAddLabel(items, input) {
|
|
723
|
+
if (!input.text || !input.labels || input.labels.length === 0) return {
|
|
724
|
+
kind: "error",
|
|
725
|
+
status: 400,
|
|
726
|
+
error: "text and a non-empty labels array required"
|
|
727
|
+
};
|
|
728
|
+
const target = findTodoByText(items, input.text);
|
|
729
|
+
if (!target) return {
|
|
730
|
+
kind: "success",
|
|
731
|
+
items,
|
|
732
|
+
message: `Item not found: "${input.text}"`,
|
|
733
|
+
jsonData: { notFound: input.text }
|
|
734
|
+
};
|
|
735
|
+
const merged = mergeLabels(target.labels ?? [], input.labels);
|
|
736
|
+
const updated = {
|
|
737
|
+
...target,
|
|
738
|
+
labels: merged
|
|
739
|
+
};
|
|
740
|
+
return {
|
|
741
|
+
kind: "success",
|
|
742
|
+
items: items.map((i) => i.id === target.id ? updated : i),
|
|
743
|
+
message: `Labels on "${target.text}": ${merged.join(", ")}`,
|
|
744
|
+
jsonData: {
|
|
745
|
+
item: target.text,
|
|
746
|
+
labels: merged
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function handleRemoveLabel(items, input) {
|
|
751
|
+
if (!input.text || !input.labels || input.labels.length === 0) return {
|
|
752
|
+
kind: "error",
|
|
753
|
+
status: 400,
|
|
754
|
+
error: "text and a non-empty labels array required"
|
|
755
|
+
};
|
|
756
|
+
const target = findTodoByText(items, input.text);
|
|
757
|
+
if (!target) return {
|
|
758
|
+
kind: "success",
|
|
759
|
+
items,
|
|
760
|
+
message: `Item not found: "${input.text}"`,
|
|
761
|
+
jsonData: { notFound: input.text }
|
|
762
|
+
};
|
|
763
|
+
const remaining = subtractLabels(target.labels ?? [], input.labels);
|
|
764
|
+
const updated = { ...target };
|
|
765
|
+
if (remaining.length > 0) updated.labels = remaining;
|
|
766
|
+
else delete updated.labels;
|
|
767
|
+
return {
|
|
768
|
+
kind: "success",
|
|
769
|
+
items: items.map((i) => i.id === target.id ? updated : i),
|
|
770
|
+
message: remaining.length > 0 ? `Labels on "${target.text}": ${remaining.join(", ")}` : `"${target.text}" now has no labels`,
|
|
771
|
+
jsonData: {
|
|
772
|
+
item: target.text,
|
|
773
|
+
labels: remaining
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function handleListLabels(items) {
|
|
778
|
+
const inventory = listLabelsWithCount(items);
|
|
779
|
+
const summary = inventory.map((entry) => `${entry.label} (${entry.count})`).join(", ");
|
|
780
|
+
return {
|
|
781
|
+
kind: "success",
|
|
782
|
+
items,
|
|
783
|
+
message: inventory.length === 0 ? "No labels in use" : `${inventory.length} label(s) in use: ${summary}`,
|
|
784
|
+
jsonData: { labels: inventory }
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
var HANDLERS = {
|
|
788
|
+
show: handleShow,
|
|
789
|
+
add: handleAdd,
|
|
790
|
+
delete: handleDelete,
|
|
791
|
+
update: handleUpdate,
|
|
792
|
+
check: handleCheck,
|
|
793
|
+
uncheck: handleUncheck,
|
|
794
|
+
clear_completed: handleClearCompleted,
|
|
795
|
+
add_label: handleAddLabel,
|
|
796
|
+
remove_label: handleRemoveLabel,
|
|
797
|
+
list_labels: handleListLabels
|
|
798
|
+
};
|
|
799
|
+
function dispatchTodos(action, items, input) {
|
|
800
|
+
const handler = HANDLERS[action];
|
|
801
|
+
if (!handler) return {
|
|
802
|
+
kind: "error",
|
|
803
|
+
status: 400,
|
|
804
|
+
error: `Unknown action: ${action}`
|
|
805
|
+
};
|
|
806
|
+
return handler(items, input);
|
|
807
|
+
}
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/handlers/priority-notifier.ts
|
|
810
|
+
var PLUGIN_DATA_KIND = "todo-priority";
|
|
811
|
+
var NAVIGATE_TARGET = "/todos";
|
|
812
|
+
var TITLE_MAX = 60;
|
|
813
|
+
var BODY_MAX = 4e3;
|
|
814
|
+
var TICKETS_FILE = "urgent-tickets.json";
|
|
815
|
+
function isTicket(value) {
|
|
816
|
+
if (!value || typeof value !== "object") return false;
|
|
817
|
+
const t = value;
|
|
818
|
+
if (typeof t["todoId"] !== "string") return false;
|
|
819
|
+
if (typeof t["notificationId"] !== "string") return false;
|
|
820
|
+
if (t["priority"] !== "urgent" && t["priority"] !== "high") return false;
|
|
821
|
+
if (t["title"] !== void 0 && typeof t["title"] !== "string") return false;
|
|
822
|
+
if (t["body"] !== void 0 && typeof t["body"] !== "string") return false;
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
async function loadTickets(files, log) {
|
|
826
|
+
if (!await files.exists(TICKETS_FILE)) return { tickets: {} };
|
|
827
|
+
let raw;
|
|
828
|
+
try {
|
|
829
|
+
raw = JSON.parse(await files.read(TICKETS_FILE));
|
|
830
|
+
} catch (err) {
|
|
831
|
+
log?.warn("priority reconcile: tickets file unparseable; treating as empty", {
|
|
832
|
+
file: TICKETS_FILE,
|
|
833
|
+
error: String(err)
|
|
834
|
+
});
|
|
835
|
+
return { tickets: {} };
|
|
836
|
+
}
|
|
837
|
+
if (!raw || typeof raw !== "object" || !("tickets" in raw) || typeof raw.tickets !== "object" || raw.tickets === null) {
|
|
838
|
+
log?.warn("priority reconcile: tickets file has unexpected shape; treating as empty", { file: TICKETS_FILE });
|
|
839
|
+
return { tickets: {} };
|
|
840
|
+
}
|
|
841
|
+
const rawTickets = raw.tickets;
|
|
842
|
+
const out = {};
|
|
843
|
+
for (const [key, value] of Object.entries(rawTickets)) {
|
|
844
|
+
if (!isTicket(value)) continue;
|
|
845
|
+
if (value.todoId !== key) continue;
|
|
846
|
+
out[key] = value;
|
|
847
|
+
}
|
|
848
|
+
return { tickets: out };
|
|
849
|
+
}
|
|
850
|
+
async function saveTickets(files, file) {
|
|
851
|
+
await files.write(TICKETS_FILE, JSON.stringify(file, null, 2));
|
|
852
|
+
}
|
|
853
|
+
function isNotifiablePriority(priority) {
|
|
854
|
+
return priority === "urgent" || priority === "high";
|
|
855
|
+
}
|
|
856
|
+
function severityFor(priority) {
|
|
857
|
+
return priority === "urgent" ? "urgent" : "nudge";
|
|
858
|
+
}
|
|
859
|
+
function truncate(text, max) {
|
|
860
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
861
|
+
}
|
|
862
|
+
function buildTitle(item) {
|
|
863
|
+
return truncate(item.text, TITLE_MAX);
|
|
864
|
+
}
|
|
865
|
+
function buildBody(item) {
|
|
866
|
+
const note = item.note?.trim();
|
|
867
|
+
let body = "";
|
|
868
|
+
if (note) body = note;
|
|
869
|
+
else if (item.dueDate) body = `Due ${item.dueDate}`;
|
|
870
|
+
return body.length <= BODY_MAX ? body : `${body.slice(0, BODY_MAX - 1)}…`;
|
|
871
|
+
}
|
|
872
|
+
async function safeClear(notifier, notificationId, todoId, log) {
|
|
873
|
+
try {
|
|
874
|
+
await notifier.clear(notificationId);
|
|
875
|
+
return true;
|
|
876
|
+
} catch (err) {
|
|
877
|
+
log?.warn("priority reconcile: clear failed", {
|
|
878
|
+
notificationId,
|
|
879
|
+
todoId,
|
|
880
|
+
error: String(err)
|
|
881
|
+
});
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function safeUpdate(notifier, notificationId, patch, todoId, log) {
|
|
886
|
+
try {
|
|
887
|
+
await notifier.update(notificationId, patch);
|
|
888
|
+
return true;
|
|
889
|
+
} catch (err) {
|
|
890
|
+
log?.warn("priority reconcile: update failed", {
|
|
891
|
+
notificationId,
|
|
892
|
+
todoId,
|
|
893
|
+
error: String(err)
|
|
894
|
+
});
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async function safePublish(notifier, item, priority, log) {
|
|
899
|
+
try {
|
|
900
|
+
const { id } = await notifier.publish({
|
|
901
|
+
severity: severityFor(priority),
|
|
902
|
+
lifecycle: "action",
|
|
903
|
+
title: buildTitle(item),
|
|
904
|
+
body: buildBody(item),
|
|
905
|
+
navigateTarget: NAVIGATE_TARGET,
|
|
906
|
+
pluginData: {
|
|
907
|
+
kind: PLUGIN_DATA_KIND,
|
|
908
|
+
todoId: item.id,
|
|
909
|
+
priority
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
return id;
|
|
913
|
+
} catch (err) {
|
|
914
|
+
log?.warn("priority reconcile: publish failed", {
|
|
915
|
+
todoId: item.id,
|
|
916
|
+
error: String(err)
|
|
917
|
+
});
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/** Reconcile the plugin's tickets and the host's bell entries with
|
|
922
|
+
* the current item list. After this resolves:
|
|
923
|
+
*
|
|
924
|
+
* - every notifiable item has a ticket and a live bell, with the
|
|
925
|
+
* bell's severity / title / body matching the item's current
|
|
926
|
+
* state;
|
|
927
|
+
* - no ticket references a non-notifiable item.
|
|
928
|
+
*
|
|
929
|
+
* Idempotent and tolerant of partial state. Drift is detected per-
|
|
930
|
+
* ticket against the title / body / priority stored at last publish
|
|
931
|
+
* or update; an item rename flows through `notifier.update` rather
|
|
932
|
+
* than clear-then-publish, preserving the notificationId. */
|
|
933
|
+
async function reconcilePriorityNotifications(items, notifier, files, log) {
|
|
934
|
+
const ticketsFile = await loadTickets(files, log);
|
|
935
|
+
const itemsById = new Map(items.map((item) => [item.id, item]));
|
|
936
|
+
let dirty = false;
|
|
937
|
+
for (const [todoId, ticket] of Object.entries(ticketsFile.tickets)) {
|
|
938
|
+
const item = itemsById.get(todoId);
|
|
939
|
+
if (!(item !== void 0 && !item.completed && isNotifiablePriority(item.priority))) {
|
|
940
|
+
if (await safeClear(notifier, ticket.notificationId, todoId, log)) {
|
|
941
|
+
delete ticketsFile.tickets[todoId];
|
|
942
|
+
dirty = true;
|
|
943
|
+
}
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (!(await notifier.get(ticket.notificationId) !== void 0)) {
|
|
947
|
+
log?.warn("priority reconcile: ghost ticket dropped; Phase 2 will re-publish", {
|
|
948
|
+
notificationId: ticket.notificationId,
|
|
949
|
+
todoId
|
|
950
|
+
});
|
|
951
|
+
delete ticketsFile.tickets[todoId];
|
|
952
|
+
dirty = true;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
const currentPriority = item.priority;
|
|
956
|
+
const desiredTitle = buildTitle(item);
|
|
957
|
+
const desiredBody = buildBody(item);
|
|
958
|
+
const priorityDrift = currentPriority !== ticket.priority;
|
|
959
|
+
const titleDrift = ticket.title !== desiredTitle;
|
|
960
|
+
const bodyDrift = ticket.body !== desiredBody;
|
|
961
|
+
if (!priorityDrift && !titleDrift && !bodyDrift) continue;
|
|
962
|
+
if (await safeUpdate(notifier, ticket.notificationId, {
|
|
963
|
+
...priorityDrift ? { severity: severityFor(currentPriority) } : {},
|
|
964
|
+
...titleDrift ? { title: desiredTitle } : {},
|
|
965
|
+
...bodyDrift ? { body: desiredBody } : {},
|
|
966
|
+
...priorityDrift ? { pluginData: {
|
|
967
|
+
kind: PLUGIN_DATA_KIND,
|
|
968
|
+
todoId,
|
|
969
|
+
priority: currentPriority
|
|
970
|
+
} } : {}
|
|
971
|
+
}, todoId, log)) {
|
|
972
|
+
ticketsFile.tickets[todoId] = {
|
|
973
|
+
todoId,
|
|
974
|
+
notificationId: ticket.notificationId,
|
|
975
|
+
priority: currentPriority,
|
|
976
|
+
title: desiredTitle,
|
|
977
|
+
body: desiredBody
|
|
978
|
+
};
|
|
979
|
+
dirty = true;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
for (const item of items) {
|
|
983
|
+
if (item.completed || !isNotifiablePriority(item.priority)) continue;
|
|
984
|
+
if (ticketsFile.tickets[item.id]) continue;
|
|
985
|
+
const newId = await safePublish(notifier, item, item.priority, log);
|
|
986
|
+
if (newId === null) continue;
|
|
987
|
+
ticketsFile.tickets[item.id] = {
|
|
988
|
+
todoId: item.id,
|
|
989
|
+
notificationId: newId,
|
|
990
|
+
priority: item.priority,
|
|
991
|
+
title: buildTitle(item),
|
|
992
|
+
body: buildBody(item)
|
|
993
|
+
};
|
|
994
|
+
dirty = true;
|
|
995
|
+
}
|
|
996
|
+
if (dirty) try {
|
|
997
|
+
await saveTickets(files, ticketsFile);
|
|
998
|
+
} catch (err) {
|
|
999
|
+
log?.warn("priority reconcile: tickets save failed", { error: String(err) });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region src/index.ts
|
|
1004
|
+
var READ_ONLY_ACTIONS = new Set(["show", "list_labels"]);
|
|
1005
|
+
function isLlmArgs(value) {
|
|
1006
|
+
return typeof value === "object" && value !== null && "action" in value && typeof value.action === "string";
|
|
1007
|
+
}
|
|
1008
|
+
function isUiArgs(value) {
|
|
1009
|
+
return typeof value === "object" && value !== null && "kind" in value && typeof value.kind === "string";
|
|
1010
|
+
}
|
|
1011
|
+
var src_default = definePlugin((runtime) => {
|
|
1012
|
+
const { pubsub, files, log } = runtime;
|
|
1013
|
+
const { notifier } = runtime;
|
|
1014
|
+
async function reconcileNotifications(items) {
|
|
1015
|
+
await reconcilePriorityNotifications(items, notifier, files.data, log);
|
|
1016
|
+
}
|
|
1017
|
+
const bootReconcile = (async () => {
|
|
1018
|
+
try {
|
|
1019
|
+
await reconcileNotifications(await loadTodos(files.data));
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
log.warn("boot reconcile failed", { error: String(err) });
|
|
1022
|
+
}
|
|
1023
|
+
})();
|
|
1024
|
+
async function handleLlm(args) {
|
|
1025
|
+
await bootReconcile;
|
|
1026
|
+
const { action, ...input } = args;
|
|
1027
|
+
log.info("dispatch llm", { action });
|
|
1028
|
+
const result = dispatchTodos(action, await loadTodos(files.data), input);
|
|
1029
|
+
if (result.kind === "error") {
|
|
1030
|
+
log.warn("dispatch llm error", {
|
|
1031
|
+
action,
|
|
1032
|
+
error: result.error
|
|
1033
|
+
});
|
|
1034
|
+
return {
|
|
1035
|
+
error: result.error,
|
|
1036
|
+
status: result.status
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
if (!READ_ONLY_ACTIONS.has(action)) {
|
|
1040
|
+
await saveTodos(files.data, result.items);
|
|
1041
|
+
await reconcileNotifications(result.items);
|
|
1042
|
+
pubsub.publish("changed", {
|
|
1043
|
+
reason: "llm-action",
|
|
1044
|
+
action
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
data: { items: result.items },
|
|
1049
|
+
message: result.message,
|
|
1050
|
+
jsonData: result.jsonData,
|
|
1051
|
+
instructions: "Display the updated todo list to the user."
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
async function handleUi(args) {
|
|
1055
|
+
await bootReconcile;
|
|
1056
|
+
log.info("dispatch ui", { kind: args.kind });
|
|
1057
|
+
if (args.kind === "listAll") {
|
|
1058
|
+
const [items, columns] = await Promise.all([loadTodos(files.data), loadColumns(files.data)]);
|
|
1059
|
+
return { data: {
|
|
1060
|
+
items,
|
|
1061
|
+
columns
|
|
1062
|
+
} };
|
|
1063
|
+
}
|
|
1064
|
+
const [items, columns] = await Promise.all([loadTodos(files.data), loadColumns(files.data)]);
|
|
1065
|
+
if (args.kind === "itemCreate") {
|
|
1066
|
+
const result = handleCreate(items, columns, args);
|
|
1067
|
+
if (result.kind === "error") return {
|
|
1068
|
+
error: result.error,
|
|
1069
|
+
status: result.status
|
|
1070
|
+
};
|
|
1071
|
+
await saveTodos(files.data, result.items);
|
|
1072
|
+
await reconcileNotifications(result.items);
|
|
1073
|
+
pubsub.publish("changed", { reason: "item-create" });
|
|
1074
|
+
return {
|
|
1075
|
+
data: {
|
|
1076
|
+
items: result.items,
|
|
1077
|
+
columns
|
|
1078
|
+
},
|
|
1079
|
+
...result.item && { item: result.item }
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
if (args.kind === "itemPatch") {
|
|
1083
|
+
const result = handlePatch(items, columns, args.id, args);
|
|
1084
|
+
if (result.kind === "error") return {
|
|
1085
|
+
error: result.error,
|
|
1086
|
+
status: result.status
|
|
1087
|
+
};
|
|
1088
|
+
await saveTodos(files.data, result.items);
|
|
1089
|
+
await reconcileNotifications(result.items);
|
|
1090
|
+
pubsub.publish("changed", {
|
|
1091
|
+
reason: "item-patch",
|
|
1092
|
+
id: args.id
|
|
1093
|
+
});
|
|
1094
|
+
return {
|
|
1095
|
+
data: {
|
|
1096
|
+
items: result.items,
|
|
1097
|
+
columns
|
|
1098
|
+
},
|
|
1099
|
+
...result.item && { item: result.item }
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
if (args.kind === "itemMove") {
|
|
1103
|
+
const result = handleMove(items, columns, args.id, args);
|
|
1104
|
+
if (result.kind === "error") return {
|
|
1105
|
+
error: result.error,
|
|
1106
|
+
status: result.status
|
|
1107
|
+
};
|
|
1108
|
+
await saveTodos(files.data, result.items);
|
|
1109
|
+
await reconcileNotifications(result.items);
|
|
1110
|
+
pubsub.publish("changed", {
|
|
1111
|
+
reason: "item-move",
|
|
1112
|
+
id: args.id
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
data: {
|
|
1116
|
+
items: result.items,
|
|
1117
|
+
columns
|
|
1118
|
+
},
|
|
1119
|
+
...result.item && { item: result.item }
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (args.kind === "itemDelete") {
|
|
1123
|
+
const result = handleDeleteItem(items, args.id);
|
|
1124
|
+
if (result.kind === "error") return {
|
|
1125
|
+
error: result.error,
|
|
1126
|
+
status: result.status
|
|
1127
|
+
};
|
|
1128
|
+
await saveTodos(files.data, result.items);
|
|
1129
|
+
await reconcileNotifications(result.items);
|
|
1130
|
+
pubsub.publish("changed", {
|
|
1131
|
+
reason: "item-delete",
|
|
1132
|
+
id: args.id
|
|
1133
|
+
});
|
|
1134
|
+
return { data: {
|
|
1135
|
+
items: result.items,
|
|
1136
|
+
columns
|
|
1137
|
+
} };
|
|
1138
|
+
}
|
|
1139
|
+
if (args.kind === "columnsAdd") {
|
|
1140
|
+
const result = handleAddColumn(columns, items, args);
|
|
1141
|
+
if (result.kind === "error") return {
|
|
1142
|
+
error: result.error,
|
|
1143
|
+
status: result.status
|
|
1144
|
+
};
|
|
1145
|
+
await saveColumns(files.data, result.columns);
|
|
1146
|
+
if (result.items) await saveTodos(files.data, result.items);
|
|
1147
|
+
if (result.items) await reconcileNotifications(result.items);
|
|
1148
|
+
pubsub.publish("changed", { reason: "column-add" });
|
|
1149
|
+
return { data: {
|
|
1150
|
+
items: result.items ?? items,
|
|
1151
|
+
columns: result.columns
|
|
1152
|
+
} };
|
|
1153
|
+
}
|
|
1154
|
+
if (args.kind === "columnPatch") {
|
|
1155
|
+
const result = handlePatchColumn(columns, args.id, args, items);
|
|
1156
|
+
if (result.kind === "error") return {
|
|
1157
|
+
error: result.error,
|
|
1158
|
+
status: result.status
|
|
1159
|
+
};
|
|
1160
|
+
await saveColumns(files.data, result.columns);
|
|
1161
|
+
if (result.items) await saveTodos(files.data, result.items);
|
|
1162
|
+
if (result.items) await reconcileNotifications(result.items);
|
|
1163
|
+
pubsub.publish("changed", {
|
|
1164
|
+
reason: "column-patch",
|
|
1165
|
+
id: args.id
|
|
1166
|
+
});
|
|
1167
|
+
return { data: {
|
|
1168
|
+
items: result.items ?? items,
|
|
1169
|
+
columns: result.columns
|
|
1170
|
+
} };
|
|
1171
|
+
}
|
|
1172
|
+
if (args.kind === "columnDelete") {
|
|
1173
|
+
const result = handleDeleteColumn(columns, args.id, items);
|
|
1174
|
+
if (result.kind === "error") return {
|
|
1175
|
+
error: result.error,
|
|
1176
|
+
status: result.status
|
|
1177
|
+
};
|
|
1178
|
+
await saveColumns(files.data, result.columns);
|
|
1179
|
+
if (result.items) await saveTodos(files.data, result.items);
|
|
1180
|
+
if (result.items) await reconcileNotifications(result.items);
|
|
1181
|
+
pubsub.publish("changed", {
|
|
1182
|
+
reason: "column-delete",
|
|
1183
|
+
id: args.id
|
|
1184
|
+
});
|
|
1185
|
+
return { data: {
|
|
1186
|
+
items: result.items ?? items,
|
|
1187
|
+
columns: result.columns
|
|
1188
|
+
} };
|
|
1189
|
+
}
|
|
1190
|
+
if (args.kind === "columnsOrder") {
|
|
1191
|
+
const result = handleReorderColumns(columns, args.ids);
|
|
1192
|
+
if (result.kind === "error") return {
|
|
1193
|
+
error: result.error,
|
|
1194
|
+
status: result.status
|
|
1195
|
+
};
|
|
1196
|
+
await saveColumns(files.data, result.columns);
|
|
1197
|
+
pubsub.publish("changed", { reason: "columns-order" });
|
|
1198
|
+
return { data: {
|
|
1199
|
+
items,
|
|
1200
|
+
columns: result.columns
|
|
1201
|
+
} };
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
error: `unknown kind: ${JSON.stringify(args)}`,
|
|
1205
|
+
status: 400
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
TOOL_DEFINITION,
|
|
1210
|
+
async manageTodoList(rawArgs) {
|
|
1211
|
+
if (isLlmArgs(rawArgs)) return handleLlm(rawArgs);
|
|
1212
|
+
if (isUiArgs(rawArgs)) return handleUi(rawArgs);
|
|
1213
|
+
return {
|
|
1214
|
+
error: "unknown args shape — expected { action: ... } or { kind: ... }",
|
|
1215
|
+
status: 400
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
});
|
|
1220
|
+
//#endregion
|
|
1221
|
+
export { TOOL_DEFINITION, src_default as default };
|
|
1222
|
+
|
|
1223
|
+
//# sourceMappingURL=index.js.map
|