@j0hanz/todokit-mcp 1.2.0 → 1.2.2
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/README.md +4 -0
- package/dist/storage.js +77 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ npm start
|
|
|
66
66
|
|
|
67
67
|
By default, todos are stored in `todos.json` in the current working directory. To control where data is written, set the `TODOKIT_TODO_FILE` environment variable to an absolute or relative path ending with `.json`. Relative paths resolve from the current working directory. The directory is created as needed; if the file does not exist, the server starts with an empty list.
|
|
68
68
|
|
|
69
|
+
When all todos are completed, the server removes the storage file so it does not linger on disk.
|
|
70
|
+
|
|
69
71
|
Examples:
|
|
70
72
|
|
|
71
73
|
```bash
|
|
@@ -231,6 +233,8 @@ Result fields:
|
|
|
231
233
|
|
|
232
234
|
Delete all todos from the list.
|
|
233
235
|
|
|
236
|
+
This also removes the configured todo storage file (defaults to `todos.json`), so the next read starts from an empty list.
|
|
237
|
+
|
|
234
238
|
| Parameter | Type | Required | Default | Description |
|
|
235
239
|
| :-------- | :--- | :------- | :------ | :---------- |
|
|
236
240
|
| (none) | - | - | - | - |
|
package/dist/storage.js
CHANGED
|
@@ -278,31 +278,89 @@ export async function readTodos() {
|
|
|
278
278
|
return todos;
|
|
279
279
|
}
|
|
280
280
|
export async function withTodos(mutate) {
|
|
281
|
+
return withTodoFileUpdate((todos) => {
|
|
282
|
+
const { todos: nextTodos, result } = mutate(todos);
|
|
283
|
+
if (nextTodos === todos) {
|
|
284
|
+
return { kind: 'no_change', result };
|
|
285
|
+
}
|
|
286
|
+
if (areAllTodosCompleted(nextTodos)) {
|
|
287
|
+
return { kind: 'delete_file', result };
|
|
288
|
+
}
|
|
289
|
+
return { kind: 'save', todos: nextTodos, result };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function areAllTodosCompleted(todos) {
|
|
293
|
+
return todos.length > 0 && todos.every((todo) => todo.completed);
|
|
294
|
+
}
|
|
295
|
+
function normalizeTodoFileUpdate(outcome, current, hasPersistedFile) {
|
|
296
|
+
if (outcome.kind === 'save' && areAllTodosCompleted(outcome.todos)) {
|
|
297
|
+
return {
|
|
298
|
+
kind: 'proceed',
|
|
299
|
+
outcome: { kind: 'delete_file', result: outcome.result },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (outcome.kind !== 'no_change') {
|
|
303
|
+
return { kind: 'proceed', outcome };
|
|
304
|
+
}
|
|
305
|
+
if (hasPersistedFile && areAllTodosCompleted(current)) {
|
|
306
|
+
return {
|
|
307
|
+
kind: 'proceed',
|
|
308
|
+
outcome: { kind: 'delete_file', result: outcome.result },
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return { kind: 'return', result: outcome.result };
|
|
312
|
+
}
|
|
313
|
+
async function deleteTodoFile(path) {
|
|
314
|
+
const start = nowMs();
|
|
315
|
+
await withTimeout(rm(path, { force: true }), WRITE_TIMEOUT_MS, 'File remove timed out').catch((error) => {
|
|
316
|
+
if (isNotFoundError(error))
|
|
317
|
+
return;
|
|
318
|
+
throw error;
|
|
319
|
+
});
|
|
320
|
+
cache = { todos: [], mtimeMs: null };
|
|
321
|
+
publishStorageEvent({
|
|
322
|
+
v: 1,
|
|
323
|
+
kind: 'storage',
|
|
324
|
+
op: 'write',
|
|
325
|
+
at: new Date().toISOString(),
|
|
326
|
+
durationMs: Math.max(0, nowMs() - start),
|
|
327
|
+
todoCount: 0,
|
|
328
|
+
renameRetries: 0,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async function withTodoFileUpdate(work) {
|
|
281
332
|
return enqueueWrite(async () => {
|
|
282
333
|
const path = getTodoFilePath();
|
|
283
334
|
for (let attempt = 0; attempt <= MAX_CONFLICT_RETRIES; attempt += 1) {
|
|
284
335
|
const mtimeMs = await getFileMtime(path, IO_TIMEOUT_MS);
|
|
285
336
|
const current = cache?.mtimeMs === mtimeMs ? cache.todos : await loadTodos(path);
|
|
286
337
|
cache = { todos: current, mtimeMs };
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
338
|
+
const normalized = normalizeTodoFileUpdate(work(current), current, mtimeMs !== null);
|
|
339
|
+
if (normalized.kind === 'return') {
|
|
340
|
+
return normalized.result;
|
|
341
|
+
}
|
|
342
|
+
const { outcome } = normalized;
|
|
343
|
+
const release = await acquireWriteLock(path, getLockTimeoutMs());
|
|
344
|
+
try {
|
|
345
|
+
const latestMtime = await getFileMtime(path, IO_TIMEOUT_MS);
|
|
346
|
+
if (latestMtime !== mtimeMs) {
|
|
347
|
+
if (attempt >= MAX_CONFLICT_RETRIES) {
|
|
348
|
+
throw createCodedError('E_STORAGE_CONFLICT', 'Todo storage changed during update; please retry.');
|
|
298
349
|
}
|
|
299
|
-
await
|
|
350
|
+
await delay(25 * (attempt + 1));
|
|
351
|
+
continue;
|
|
300
352
|
}
|
|
301
|
-
|
|
302
|
-
await
|
|
353
|
+
if (outcome.kind === 'save') {
|
|
354
|
+
await saveTodos(path, outcome.todos);
|
|
303
355
|
}
|
|
356
|
+
else {
|
|
357
|
+
await deleteTodoFile(path);
|
|
358
|
+
}
|
|
359
|
+
return outcome.result;
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
await release();
|
|
304
363
|
}
|
|
305
|
-
return result;
|
|
306
364
|
}
|
|
307
365
|
throw createCodedError('E_STORAGE_CONFLICT', 'Todo storage update failed due to concurrent modifications');
|
|
308
366
|
});
|
|
@@ -565,8 +623,8 @@ export function deleteTodosByIds(ids) {
|
|
|
565
623
|
});
|
|
566
624
|
}
|
|
567
625
|
export function deleteAllTodos() {
|
|
568
|
-
return
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
});
|
|
626
|
+
return withTodoFileUpdate((todos) => ({
|
|
627
|
+
kind: 'delete_file',
|
|
628
|
+
result: todos.map((todo) => todo.id),
|
|
629
|
+
}));
|
|
572
630
|
}
|
package/package.json
CHANGED