@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 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 { todos, result } = mutate(current);
288
- if (todos !== current) {
289
- const release = await acquireWriteLock(path, getLockTimeoutMs());
290
- try {
291
- const latestMtime = await getFileMtime(path, IO_TIMEOUT_MS);
292
- if (latestMtime !== mtimeMs) {
293
- if (attempt >= MAX_CONFLICT_RETRIES) {
294
- throw createCodedError('E_STORAGE_CONFLICT', 'Todo storage changed during update; please retry.');
295
- }
296
- await delay(25 * (attempt + 1));
297
- continue;
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 saveTodos(path, todos);
350
+ await delay(25 * (attempt + 1));
351
+ continue;
300
352
  }
301
- finally {
302
- await release();
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 withTodos((todos) => {
569
- const deletedIds = todos.map((todo) => todo.id);
570
- return { todos: [], result: deletedIds };
571
- });
626
+ return withTodoFileUpdate((todos) => ({
627
+ kind: 'delete_file',
628
+ result: todos.map((todo) => todo.id),
629
+ }));
572
630
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/todokit-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "mcpName": "io.github.j0hanz/todokit",
5
5
  "description": "A MCP server for Todokit, a task management and productivity tool with JSON storage.",
6
6
  "type": "module",