things-api 0.14.0 → 0.16.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 (140) hide show
  1. package/README.md +4 -4
  2. package/dist/audit/schema.d.ts +8 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/commands/area.js +1 -1
  5. package/dist/cli/commands/install-skill.js +9 -2
  6. package/dist/cli/commands/install-skill.js.map +1 -1
  7. package/dist/cli/commands/project.js +1 -1
  8. package/dist/cli/commands/project.js.map +1 -1
  9. package/dist/cli/commands/reads.js +73 -4
  10. package/dist/cli/commands/reads.js.map +1 -1
  11. package/dist/cli/commands/repeat-flags.d.ts +11 -1
  12. package/dist/cli/commands/repeat-flags.js +23 -0
  13. package/dist/cli/commands/repeat-flags.js.map +1 -1
  14. package/dist/cli/commands/todo.js +50 -3
  15. package/dist/cli/commands/todo.js.map +1 -1
  16. package/dist/cli/commands/writes.js +202 -61
  17. package/dist/cli/commands/writes.js.map +1 -1
  18. package/dist/cli/glyphs.js +9 -10
  19. package/dist/cli/glyphs.js.map +1 -1
  20. package/dist/cli/help.js +18 -13
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/read-driver.d.ts +9 -3
  23. package/dist/cli/read-driver.js +3 -1
  24. package/dist/cli/read-driver.js.map +1 -1
  25. package/dist/cli/render.d.ts +33 -2
  26. package/dist/cli/render.js +60 -9
  27. package/dist/cli/render.js.map +1 -1
  28. package/dist/cli/skill.d.ts +24 -0
  29. package/dist/cli/skill.js +40 -0
  30. package/dist/cli/skill.js.map +1 -1
  31. package/dist/client.d.ts +88 -15
  32. package/dist/client.js +31 -5
  33. package/dist/client.js.map +1 -1
  34. package/dist/contracts.d.ts +13 -2
  35. package/dist/contracts.js +1 -1
  36. package/dist/contracts.js.map +1 -1
  37. package/dist/index.d.ts +5 -4
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/mcp/server.js +187 -36
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/model/entities.d.ts +97 -40
  43. package/dist/model/entities.js +0 -4
  44. package/dist/model/entities.js.map +1 -1
  45. package/dist/model/mappers.js +24 -43
  46. package/dist/model/mappers.js.map +1 -1
  47. package/dist/read/area-view.js +6 -6
  48. package/dist/read/area-view.js.map +1 -1
  49. package/dist/read/detail.js +51 -3
  50. package/dist/read/detail.js.map +1 -1
  51. package/dist/read/filter-contract.d.ts +1 -1
  52. package/dist/read/filter-contract.js +3 -0
  53. package/dist/read/filter-contract.js.map +1 -1
  54. package/dist/read/log-boundary.d.ts +46 -2
  55. package/dist/read/log-boundary.js +43 -2
  56. package/dist/read/log-boundary.js.map +1 -1
  57. package/dist/read/predicates.d.ts +13 -0
  58. package/dist/read/predicates.js +13 -0
  59. package/dist/read/predicates.js.map +1 -1
  60. package/dist/read/project-view.js +5 -5
  61. package/dist/read/project-view.js.map +1 -1
  62. package/dist/read/queries.d.ts +12 -0
  63. package/dist/read/queries.js +14 -0
  64. package/dist/read/queries.js.map +1 -1
  65. package/dist/read/search-rank.js +2 -2
  66. package/dist/read/search-rank.js.map +1 -1
  67. package/dist/read/shape.d.ts +30 -26
  68. package/dist/read/shape.js +130 -101
  69. package/dist/read/shape.js.map +1 -1
  70. package/dist/read/snapshot.js +2 -2
  71. package/dist/read/snapshot.js.map +1 -1
  72. package/dist/read/stage.d.ts +36 -18
  73. package/dist/read/stage.js +30 -13
  74. package/dist/read/stage.js.map +1 -1
  75. package/dist/read/views.d.ts +62 -0
  76. package/dist/read/views.js +134 -14
  77. package/dist/read/views.js.map +1 -1
  78. package/dist/surface-copy.d.ts +2 -2
  79. package/dist/surface-copy.js +2 -2
  80. package/dist/write/batch.d.ts +28 -3
  81. package/dist/write/batch.js +162 -46
  82. package/dist/write/batch.js.map +1 -1
  83. package/dist/write/clear-reminder.js +3 -1
  84. package/dist/write/clear-reminder.js.map +1 -1
  85. package/dist/write/clone.d.ts +4 -0
  86. package/dist/write/clone.js +531 -0
  87. package/dist/write/clone.js.map +1 -0
  88. package/dist/write/commands.js +196 -29
  89. package/dist/write/commands.js.map +1 -1
  90. package/dist/write/guards.d.ts +1 -1
  91. package/dist/write/guards.js +44 -12
  92. package/dist/write/guards.js.map +1 -1
  93. package/dist/write/make-repeating-project.d.ts +2 -3
  94. package/dist/write/make-repeating-project.js +2 -73
  95. package/dist/write/make-repeating-project.js.map +1 -1
  96. package/dist/write/move.js +53 -1
  97. package/dist/write/move.js.map +1 -1
  98. package/dist/write/operations.d.ts +134 -11
  99. package/dist/write/operations.js +5 -1
  100. package/dist/write/operations.js.map +1 -1
  101. package/dist/write/pipeline.d.ts +38 -0
  102. package/dist/write/pipeline.js +71 -6
  103. package/dist/write/pipeline.js.map +1 -1
  104. package/dist/write/pre-state.d.ts +41 -0
  105. package/dist/write/pre-state.js +71 -8
  106. package/dist/write/pre-state.js.map +1 -1
  107. package/dist/write/preserve-modified.d.ts +55 -0
  108. package/dist/write/preserve-modified.js +106 -0
  109. package/dist/write/preserve-modified.js.map +1 -0
  110. package/dist/write/promote-clone.d.ts +32 -0
  111. package/dist/write/promote-clone.js +620 -0
  112. package/dist/write/promote-clone.js.map +1 -0
  113. package/dist/write/reorder.js +49 -2
  114. package/dist/write/reorder.js.map +1 -1
  115. package/dist/write/resolution-timestamps.js +29 -7
  116. package/dist/write/resolution-timestamps.js.map +1 -1
  117. package/dist/write/reversibility.js +32 -14
  118. package/dist/write/reversibility.js.map +1 -1
  119. package/dist/write/scope-guard.js +2 -0
  120. package/dist/write/scope-guard.js.map +1 -1
  121. package/dist/write/undo.d.ts +12 -2
  122. package/dist/write/undo.js +145 -22
  123. package/dist/write/undo.js.map +1 -1
  124. package/dist/write/vectors/applescript.js +9 -0
  125. package/dist/write/vectors/applescript.js.map +1 -1
  126. package/dist/write/vectors/simulator.js +242 -10
  127. package/dist/write/vectors/simulator.js.map +1 -1
  128. package/dist/write/vectors/ui-certification.d.ts +1 -1
  129. package/dist/write/vectors/ui-certification.js +10 -3
  130. package/dist/write/vectors/ui-certification.js.map +1 -1
  131. package/dist/write/verify/delta.d.ts +55 -10
  132. package/dist/write/verify/delta.js +73 -10
  133. package/dist/write/verify/delta.js.map +1 -1
  134. package/package.json +1 -1
  135. package/schema/envelope.schema.json +25 -0
  136. package/skills/things-cli/SKILL.md +10 -7
  137. package/skills/things-cli/references/banner.md +1 -1
  138. package/skills/things-cli/references/contracts.md +17 -9
  139. package/skills/things-cli/references/{model.md → data-model.md} +5 -2
  140. package/skills/things-cli/references/ordering.md +2 -0
@@ -327,6 +327,17 @@ const MCP_UNTAGGED_CONFLICT = "untagged does not combine with tag/exact_tag";
327
327
  const dryRunShape = {
328
328
  dry_run: z.boolean().optional().describe("Preview the planned change without applying anything"),
329
329
  };
330
+ const preserveModifiedShape = {
331
+ preserve_modified: z
332
+ .boolean()
333
+ .optional()
334
+ .describe("Keep this change off the modification-date timeline: capture each pre-existing edited " +
335
+ "item's modification date and restore it (to the whole second) after the change, so a " +
336
+ "changes/watch query keyed on it does not surface the edit. A no-op on a pure create. " +
337
+ "Safe with Things Cloud: the restored date syncs to your other devices and stays put, so " +
338
+ "the item stays off the timeline everywhere — unless another device edits the same item " +
339
+ "at nearly the same time, which re-dates it."),
340
+ };
330
341
  /**
331
342
  * The per-call idempotency key for a single write tool — the analogue of a batch
332
343
  * line's op_id. A resubmission carrying the same key is recognized as already
@@ -507,6 +518,7 @@ export function createThingsMcpServer(options = {}) {
507
518
  ...(args.allow_non_empty_area === true && { allowNonEmptyArea: true }),
508
519
  ...(args.dangerously_drive_gui === true && { dangerouslyDriveGui: true }),
509
520
  ...(args.create_tags === true && { createTags: true }),
521
+ ...(args.preserve_modified === true && { preserveModified: true }),
510
522
  ...(args.tz !== undefined && { zone: args.tz }),
511
523
  ...(args.op_id !== undefined && { opId: assertOpId(args.op_id) }),
512
524
  });
@@ -600,7 +612,10 @@ export function createThingsMcpServer(options = {}) {
600
612
  description: "Read a Things list as the app presents it: today (two children buckets — " +
601
613
  "children.today and children.evening, evening expires daily; the whole-view " +
602
614
  "count due/overdue vs. other rides the result's second block), inbox, anytime, " +
603
- "upcoming, someday, logbook, or trash. For upcoming, " +
615
+ "upcoming, someday, logbook, trash, or deadlines (a flat items list of everything " +
616
+ "carrying a deadline — to-dos and projects — in deadline order, most-overdue first; " +
617
+ "repeating items appear at their next occurrence's projected deadline; scope with " +
618
+ "today/overdue/project/area/tag). For upcoming, " +
604
619
  "horizon > 1 also includes future occurrences of repeating items (up to 10 each). " +
605
620
  "anytime/someday return sections in canonical order (area + items; null area = the " +
606
621
  "top-level block); children of someday/future-scheduled projects are excluded " +
@@ -615,13 +630,30 @@ export function createThingsMcpServer(options = {}) {
615
630
  " " +
616
631
  REF_RULE_NOTE,
617
632
  inputSchema: {
618
- view: z.enum(["today", "inbox", "anytime", "upcoming", "someday", "logbook", "trash"]),
633
+ view: z.enum([
634
+ "today",
635
+ "inbox",
636
+ "anytime",
637
+ "upcoming",
638
+ "someday",
639
+ "logbook",
640
+ "trash",
641
+ "deadlines",
642
+ ]),
619
643
  ...tagFilterShape,
620
644
  ...tzShape,
621
645
  area: z
622
646
  .string()
623
647
  .optional()
624
- .describe(`today/anytime/someday/upcoming/logbook only: ${AREA_FILTER_DESC}`),
648
+ .describe(`today/anytime/someday/upcoming/logbook/deadlines only: ${AREA_FILTER_DESC}`),
649
+ today: z
650
+ .boolean()
651
+ .optional()
652
+ .describe("deadlines only: keep only current Today members (This Evening included)"),
653
+ project: z
654
+ .string()
655
+ .optional()
656
+ .describe("deadlines only: restrict to one project's children (uuid or unique name)"),
625
657
  evening: z.boolean().optional().describe("today only: show only the This Evening section"),
626
658
  show_active_project_items: z
627
659
  .union([z.boolean(), z.number().int().min(1)])
@@ -679,7 +711,7 @@ export function createThingsMcpServer(options = {}) {
679
711
  // (the current-work views), matching FILTER_CONTRACT.
680
712
  const validated = validateViewArgs(args.view, { ...tagPresence(args), overdue: args.overdue }, {
681
713
  untaggedConflict: MCP_UNTAGGED_CONFLICT,
682
- overdueRejected: `overdue applies to today/inbox/anytime/someday, not ${args.view}`,
714
+ overdueRejected: `overdue applies to today/inbox/anytime/someday/deadlines, not ${args.view}`,
683
715
  overdueStatusWiden: "",
684
716
  });
685
717
  if (!validated.ok)
@@ -701,6 +733,14 @@ export function createThingsMcpServer(options = {}) {
701
733
  if (args.view !== "today" && args.evening === true) {
702
734
  return usage(`evening applies only to today, not ${args.view}`);
703
735
  }
736
+ // The two deadlines-only scopes. `area`/`tag`/`overdue`/`limit` are
737
+ // shared with the other flat views; `today`/`project` are not.
738
+ if (args.view !== "deadlines" && args.today === true) {
739
+ return usage(`today applies only to deadlines, not ${args.view}`);
740
+ }
741
+ if (args.view !== "deadlines" && args.project !== undefined) {
742
+ return usage(`project applies only to deadlines, not ${args.view}`);
743
+ }
704
744
  // The `area` filter applies to the area-carrying views only; inbox
705
745
  // (area-less captures) and trash have no area to scope by.
706
746
  if ((args.view === "inbox" || args.view === "trash") && args.area !== undefined) {
@@ -786,19 +826,34 @@ export function createThingsMcpServer(options = {}) {
786
826
  return groupedResult(withSectionTotals(shapeReadPayload("someday", view, full, c.refPromoter()), sectionTotals), truncation);
787
827
  }
788
828
  case "logbook": {
789
- const { items, truncation, filter: fm, } = c.read.logbook({
829
+ const { items, truncation, logging, filter: fm, } = c.read.logbook({
790
830
  ...filter,
791
831
  ...zone,
792
832
  ...area,
793
833
  limit,
794
834
  });
795
835
  filterMeta = fm;
796
- return truncatedResult(shapeReadPayload("logbook", items, full, c.refPromoter()), truncation);
836
+ // The log-move cadence fact rides the metadata block (the CLI
837
+ // meta.logging analog, exactly as today's counts do).
838
+ return truncatedResult(shapeReadPayload("logbook", items, full, c.refPromoter()), truncation, { logging });
797
839
  }
798
840
  case "trash": {
799
841
  const { items, truncation } = c.read.trash({ ...zone, limit });
800
842
  return truncatedResult(shapeReadPayload("trash", items, full, c.refPromoter()), truncation);
801
843
  }
844
+ case "deadlines": {
845
+ // `project`/`area` scope inside the view (SQL), not the shared
846
+ // area post-filter — so no `meta.filter` here (parity with search).
847
+ const { items, truncation } = c.read.deadlines({
848
+ ...filter,
849
+ ...zone,
850
+ ...(args.today === true && { todayOnly: true }),
851
+ ...(args.project !== undefined && { project: args.project }),
852
+ ...(args.area !== undefined && { area: args.area }),
853
+ limit,
854
+ });
855
+ return truncatedResult(shapeReadPayload("deadlines", items, full, c.refPromoter()), truncation);
856
+ }
802
857
  }
803
858
  }, args.tz, () => (filterMeta !== undefined ? { filter: filterMeta } : undefined), () => looseAreaWarnings(getClient(), args.area));
804
859
  });
@@ -1094,6 +1149,7 @@ export function createThingsMcpServer(options = {}) {
1094
1149
  ...createTagsShape,
1095
1150
  ...tzShape,
1096
1151
  ...dryRunShape,
1152
+ ...preserveModifiedShape,
1097
1153
  ...opIdShape,
1098
1154
  },
1099
1155
  annotations: NON_DESTRUCTIVE,
@@ -1171,6 +1227,7 @@ export function createThingsMcpServer(options = {}) {
1171
1227
  ...createTagsShape,
1172
1228
  ...tzShape,
1173
1229
  ...dryRunShape,
1230
+ ...preserveModifiedShape,
1174
1231
  ...opIdShape,
1175
1232
  },
1176
1233
  annotations: NON_DESTRUCTIVE,
@@ -1286,6 +1343,7 @@ export function createThingsMcpServer(options = {}) {
1286
1343
  .describe(`status completed or canceled: the completion timestamp (${RESOLUTION_DATE_FORMAT})`),
1287
1344
  ...tzShape,
1288
1345
  ...dryRunShape,
1346
+ ...preserveModifiedShape,
1289
1347
  ...opIdShape,
1290
1348
  },
1291
1349
  annotations: NON_DESTRUCTIVE,
@@ -1380,6 +1438,7 @@ export function createThingsMcpServer(options = {}) {
1380
1438
  .optional()
1381
1439
  .describe("Confirm moving into a completed/canceled project (this reopens it)"),
1382
1440
  ...dryRunShape,
1441
+ ...preserveModifiedShape,
1383
1442
  },
1384
1443
  annotations: NON_DESTRUCTIVE,
1385
1444
  }, async (args) => guard(async () => {
@@ -1425,6 +1484,7 @@ export function createThingsMcpServer(options = {}) {
1425
1484
  mode: z.enum(["replace", "add"]).optional().describe("Default: replace"),
1426
1485
  ...createTagsShape,
1427
1486
  ...dryRunShape,
1487
+ ...preserveModifiedShape,
1428
1488
  ...opIdShape,
1429
1489
  },
1430
1490
  annotations: NON_DESTRUCTIVE,
@@ -1483,6 +1543,7 @@ export function createThingsMcpServer(options = {}) {
1483
1543
  .optional()
1484
1544
  .describe("replace: confirm discarding the existing items and their checked states"),
1485
1545
  ...dryRunShape,
1546
+ ...preserveModifiedShape,
1486
1547
  },
1487
1548
  annotations: NON_DESTRUCTIVE,
1488
1549
  }, async (args) => guard(async () => {
@@ -1558,6 +1619,7 @@ export function createThingsMcpServer(options = {}) {
1558
1619
  .optional()
1559
1620
  .describe("kind tag: confirm permanent deletion of ALL nested child tags too"),
1560
1621
  ...dryRunShape,
1622
+ ...preserveModifiedShape,
1561
1623
  ...opIdShape,
1562
1624
  },
1563
1625
  annotations: DESTRUCTIVE,
@@ -1579,7 +1641,7 @@ export function createThingsMcpServer(options = {}) {
1579
1641
  description: "Restore a trashed to-do or project. A to-do returns to the Inbox without its " +
1580
1642
  "previous schedule or project/area. A project is restored in place: its schedule, " +
1581
1643
  "area, and children come back exactly as they were.",
1582
- inputSchema: { uuid: z.string(), ...dryRunShape, ...opIdShape },
1644
+ inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
1583
1645
  annotations: NON_DESTRUCTIVE,
1584
1646
  }, async (args) => guard(async () => {
1585
1647
  const c = getClient();
@@ -1666,6 +1728,7 @@ export function createThingsMcpServer(options = {}) {
1666
1728
  .describe("add/move: place immediately after this heading (title or uuid)"),
1667
1729
  ...driveGuiShape,
1668
1730
  ...dryRunShape,
1731
+ ...preserveModifiedShape,
1669
1732
  },
1670
1733
  annotations: DESTRUCTIVE,
1671
1734
  }, async (args) => guard(async () => {
@@ -1766,6 +1829,7 @@ export function createThingsMcpServer(options = {}) {
1766
1829
  uuid: z.string().describe("the to-do's uuid"),
1767
1830
  ...driveGuiShape,
1768
1831
  ...dryRunShape,
1832
+ ...preserveModifiedShape,
1769
1833
  ...opIdShape,
1770
1834
  },
1771
1835
  annotations: DESTRUCTIVE,
@@ -1775,7 +1839,7 @@ export function createThingsMcpServer(options = {}) {
1775
1839
  "Things proxy shortcuts when installed (in place, and the only path for a repeating " +
1776
1840
  "to-do); otherwise a non-repeating dated to-do falls back to a URL re-schedule that " +
1777
1841
  "briefly moves it to Today and back. Reversible with the undo tool.",
1778
- inputSchema: { uuid: z.string(), ...dryRunShape },
1842
+ inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape },
1779
1843
  annotations: NON_DESTRUCTIVE,
1780
1844
  }, async (args) => guard(async () => mutationResult(await getClient().write.clearReminder(args.uuid, writeOptions(args)))));
1781
1845
  // -------------------------------------------- GUI-driven (Accessibility)
@@ -1859,16 +1923,17 @@ export function createThingsMcpServer(options = {}) {
1859
1923
  server.registerTool("repeat", {
1860
1924
  description: "Manage recurrence on a to-do or project (scope) by driving the local Things app's " +
1861
1925
  "interface — every action needs dangerously_drive_gui. action start: turn a plain item " +
1862
- "into a repeating one this REPLACES it with a new series and cannot be undone (give " +
1863
- "frequency + interval and optionally the weekday set, monthly/yearly day, end bound, " +
1864
- "reminder, or per-occurrence deadline); returns a repeating block with instanceUuid (the " +
1865
- "visible occurrence), templateUuid (the rule), and replacedUuid. action reschedule: " +
1866
- "change a repeating item's rule in place, keeping the same item (undoable it restores " +
1867
- "the previous rule). action pause/resume: stop or restart its new occurrences, keeping " +
1868
- "the rule. action add (scope project only): create a project and make it repeating in " +
1869
- "one call the project is created first and PERSISTS even if the make-repeating step " +
1870
- "refuses; give an area to place it or omit it to create in Someday (only frequency and " +
1871
- "interval are supported); returns the new project's uuid.",
1926
+ "into a repeating one (promote-via-clone: a disposable copy is promoted and the ORIGINAL " +
1927
+ "is moved to the Trash, so undo reverses it — it removes the new series and restores the " +
1928
+ "original); give frequency + interval and optionally the weekday set, monthly/yearly day, " +
1929
+ "end bound, or per-occurrence deadline; returns a repeating block with instanceUuid (the " +
1930
+ "visible occurrence), templateUuid (the rule), and replacedUuid. A project holding a " +
1931
+ "nested repeating template is refused. action reschedule: change a repeating item's rule " +
1932
+ "in place, keeping the same item (undoable it restores the previous rule). action " +
1933
+ "pause/resume: stop or restart its new occurrences, keeping the rule. action add: create " +
1934
+ "an item and make it repeating in one call it is created first and PERSISTS even if the " +
1935
+ "promote refuses (give a title; for a project give an area to place it or omit it to " +
1936
+ "create in Someday); undo removes the created series. Returns the new template's uuid.",
1872
1937
  inputSchema: {
1873
1938
  scope: z.enum(["todo", "project"]),
1874
1939
  action: z.enum(["start", "reschedule", "pause", "resume", "add"]),
@@ -1876,13 +1941,13 @@ export function createThingsMcpServer(options = {}) {
1876
1941
  .string()
1877
1942
  .optional()
1878
1943
  .describe("start/reschedule/pause/resume: the item (a project also accepts a unique name)"),
1879
- title: z.string().optional().describe("add (project): the new project's title"),
1880
- notes: z.string().optional().describe("add (project): notes"),
1944
+ title: z.string().optional().describe("add: the new item's title"),
1945
+ notes: z.string().optional().describe("add: notes"),
1881
1946
  area: z.string().optional().describe(`add (project): destination area (${REF_FORMAT})`),
1882
1947
  project_deadline: z
1883
1948
  .string()
1884
1949
  .optional()
1885
- .describe(`add (project): the project's due date — ${DATE_FORMAT}`),
1950
+ .describe(`add: the item's due date — ${DATE_FORMAT}`),
1886
1951
  todos: z.array(z.string()).optional().describe("add (project): initial child to-do titles"),
1887
1952
  frequency: z
1888
1953
  .enum(["daily", "weekly", "monthly", "yearly"])
@@ -1908,6 +1973,7 @@ export function createThingsMcpServer(options = {}) {
1908
1973
  start_days_earlier: repeatRuleShape.start_days_earlier,
1909
1974
  ...driveGuiShape,
1910
1975
  ...dryRunShape,
1976
+ ...preserveModifiedShape,
1911
1977
  },
1912
1978
  annotations: DESTRUCTIVE,
1913
1979
  }, async (args) => guard(async () => {
@@ -1915,13 +1981,25 @@ export function createThingsMcpServer(options = {}) {
1915
1981
  const opts = writeOptions(args);
1916
1982
  const { frequency, interval } = args;
1917
1983
  if (args.action === "add") {
1918
- if (args.scope !== "project")
1919
- return usage('action "add" requires scope "project"');
1920
1984
  if (args.title === undefined)
1921
1985
  return usage('action "add" requires title');
1922
1986
  if (frequency === undefined || interval === undefined) {
1923
1987
  return usage('action "add" requires frequency and interval');
1924
1988
  }
1989
+ // add-repeating carries only the calendar-anchor rule fields; the base
1990
+ // add owns the item's own deadline, and the rule reminder/start-offset
1991
+ // are set with a follow-up reschedule.
1992
+ const { reminder: _reminder, deadline: _deadline, startDaysEarlier: _sde, ...ruleExtras } = repeatExtras(args, frequency);
1993
+ if (args.scope === "todo") {
1994
+ return mutationResult(await c.write.addRepeatingTodo({
1995
+ title: args.title,
1996
+ ...(args.notes !== undefined && { notes: args.notes }),
1997
+ ...(args.project_deadline !== undefined && { deadline: args.project_deadline }),
1998
+ frequency,
1999
+ interval,
2000
+ ...ruleExtras,
2001
+ }, opts));
2002
+ }
1925
2003
  return mutationResult(await c.write.addRepeatingProject({
1926
2004
  title: args.title,
1927
2005
  ...(args.notes !== undefined && { notes: args.notes }),
@@ -1930,6 +2008,7 @@ export function createThingsMcpServer(options = {}) {
1930
2008
  ...(args.todos !== undefined && { todos: args.todos }),
1931
2009
  frequency,
1932
2010
  interval,
2011
+ ...ruleExtras,
1933
2012
  }, opts));
1934
2013
  }
1935
2014
  if (args.uuid === undefined)
@@ -1950,8 +2029,10 @@ export function createThingsMcpServer(options = {}) {
1950
2029
  }
1951
2030
  const extras = repeatExtras(args, frequency);
1952
2031
  if (args.scope === "todo") {
1953
- const op = args.action === "start" ? "todo.make-repeating" : "todo.reschedule-repeat";
1954
- return mutationResult(await c.write.run(op, { uuid: args.uuid, frequency, interval, ...extras }, opts));
2032
+ if (args.action === "start") {
2033
+ return mutationResult(await c.write.makeRepeatingTodo(args.uuid, { frequency, interval, ...extras }, opts));
2034
+ }
2035
+ return mutationResult(await c.write.run("todo.reschedule-repeat", { uuid: args.uuid, frequency, interval, ...extras }, opts));
1955
2036
  }
1956
2037
  // scope project
1957
2038
  if (args.action === "reschedule") {
@@ -1963,7 +2044,7 @@ export function createThingsMcpServer(options = {}) {
1963
2044
  server.registerTool("duplicate_item", {
1964
2045
  description: "Duplicate a to-do or project and return the copy's uuid; a duplicated project " +
1965
2046
  "includes its children. Not available for repeating items.",
1966
- inputSchema: { uuid: z.string(), ...dryRunShape, ...opIdShape },
2047
+ inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
1967
2048
  annotations: NON_DESTRUCTIVE,
1968
2049
  }, async (args) => guard(async () => {
1969
2050
  const c = getClient();
@@ -1971,6 +2052,42 @@ export function createThingsMcpServer(options = {}) {
1971
2052
  ? await c.write.duplicateTodo(args.uuid, writeOptions(args))
1972
2053
  : await c.write.duplicateProject(args.uuid, writeOptions(args)));
1973
2054
  }));
2055
+ server.registerTool("clone_item", {
2056
+ description: "Clone a to-do or project — a faithful content copy through official write surfaces and " +
2057
+ "return the clone's uuid. Copies title, notes, tags, when, reminder, deadline, checklist " +
2058
+ "items and their checked state, container, and completed/canceled state with the exact " +
2059
+ "original timestamp; a project also copies its area, headings, and children. The clone is " +
2060
+ "a new capture (a new uuid, created now, at its container's usual position). Refuses a " +
2061
+ "trashed source and a project holding a nested repeating template. A repeating template is " +
2062
+ "cloned as a NEW repeating series (its content plus the source's rule, minted fresh — no " +
2063
+ "instances copied); this drives the app (requires dangerously_drive_gui) and refuses a " +
2064
+ "rule the Repeat dialog cannot express. Undo trashes the clone (a template clone trashes " +
2065
+ "the new series). Not the same as duplicate_item (Things' own native copy).",
2066
+ inputSchema: {
2067
+ uuid: z.string(),
2068
+ title: z
2069
+ .string()
2070
+ .optional()
2071
+ .describe("Give the clone a different title (default: the source's title)"),
2072
+ preserve_created: z
2073
+ .boolean()
2074
+ .optional()
2075
+ .describe("Copy the source's creation date onto the clone (minute resolution)"),
2076
+ ...driveGuiShape,
2077
+ ...dryRunShape,
2078
+ ...opIdShape,
2079
+ },
2080
+ annotations: NON_DESTRUCTIVE,
2081
+ }, async (args) => guard(async () => {
2082
+ const c = getClient();
2083
+ const clone = {
2084
+ ...(args.title !== undefined && { title: args.title }),
2085
+ ...(args.preserve_created === true && { preserveCreated: true }),
2086
+ };
2087
+ return mutationResult(itemType(args.uuid) === "to-do"
2088
+ ? await c.write.cloneTodo(args.uuid, clone, writeOptions(args))
2089
+ : await c.write.cloneProject(args.uuid, clone, writeOptions(args)));
2090
+ }));
1974
2091
  // -------------------------------------------------------------- projects
1975
2092
  server.registerTool("add_project", {
1976
2093
  description: "Create a project and return its uuid. Optionally place it in an area, schedule " +
@@ -1993,6 +2110,7 @@ export function createThingsMcpServer(options = {}) {
1993
2110
  `(${RESOLUTION_DATE_FORMAT}); cannot seed open child to-dos`),
1994
2111
  ...tzShape,
1995
2112
  ...dryRunShape,
2113
+ ...preserveModifiedShape,
1996
2114
  ...opIdShape,
1997
2115
  },
1998
2116
  annotations: NON_DESTRUCTIVE,
@@ -2024,6 +2142,7 @@ export function createThingsMcpServer(options = {}) {
2024
2142
  no_area: z.boolean().optional().describe("Leave the current area (a project's detach)"),
2025
2143
  ...positionShape,
2026
2144
  ...dryRunShape,
2145
+ ...preserveModifiedShape,
2027
2146
  },
2028
2147
  annotations: NON_DESTRUCTIVE,
2029
2148
  }, async (args) => guard(async () => {
@@ -2052,6 +2171,7 @@ export function createThingsMcpServer(options = {}) {
2052
2171
  tags: z.array(z.string()).optional().describe(`Tags — ${TAG_REF_FORMAT}`),
2053
2172
  ...createTagsShape,
2054
2173
  ...dryRunShape,
2174
+ ...preserveModifiedShape,
2055
2175
  ...opIdShape,
2056
2176
  },
2057
2177
  annotations: NON_DESTRUCTIVE,
@@ -2063,10 +2183,18 @@ export function createThingsMcpServer(options = {}) {
2063
2183
  title: z.string(),
2064
2184
  parent: z.string().optional().describe("Existing parent tag name"),
2065
2185
  ...dryRunShape,
2186
+ ...preserveModifiedShape,
2066
2187
  ...opIdShape,
2067
2188
  },
2068
2189
  annotations: NON_DESTRUCTIVE,
2069
2190
  }, async (args) => guard(async () => mutationResult(await getClient().write.addTag({ title: args.title, ...(args.parent !== undefined && { parent: args.parent }) }, writeOptions(args)))));
2191
+ server.registerTool("log_now", {
2192
+ description: "Move completed items to the Logbook now. The result discloses how many items were moved " +
2193
+ "(observed.logged); when none are waiting it logs nothing — a clean no-op, not an error. " +
2194
+ "This cannot be undone.",
2195
+ inputSchema: { ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
2196
+ annotations: DESTRUCTIVE,
2197
+ }, async (args) => guard(async () => mutationResult(await getClient().write.logNow(writeOptions(args)))));
2070
2198
  // -------------------------------------------------- generic + discovery
2071
2199
  server.registerTool("run_operation", {
2072
2200
  description: "Run any cataloged operation by kind — the generic entry for operations without a " +
@@ -2092,22 +2220,28 @@ export function createThingsMcpServer(options = {}) {
2092
2220
  .optional()
2093
2221
  .describe("area.delete: delete a non-empty area together with its contents"),
2094
2222
  ...dryRunShape,
2223
+ ...preserveModifiedShape,
2095
2224
  ...opIdShape,
2096
2225
  },
2097
2226
  annotations: DESTRUCTIVE,
2098
2227
  }, async (args) => guard(async () => mutationResult(await getClient().write.run(args.op, args.params, writeOptions(args)))));
2099
2228
  server.registerTool("batch", {
2100
2229
  description: "Run several operations in order, each independently — there are no transactions, " +
2101
- "and a failure does not roll back earlier operations. Per-operation results return " +
2102
- "in order; fail_fast skips the remainder after the first failure. " +
2230
+ "and a failure does not roll back earlier operations. A statically-invalid operation " +
2231
+ "(bad shape, unknown op, a $ref to an undeclared/forward temp_id, a duplicate temp_id) " +
2232
+ "refuses the WHOLE batch before anything runs, naming every bad operation. Otherwise " +
2233
+ "per-operation results return in order. By DEFAULT a runtime failure STOPS the batch " +
2234
+ "(later operations reported not-run, with resume guidance in the summary); " +
2235
+ "continue_on_error runs past failures. " +
2103
2236
  "CHAINING: an operation that creates something may carry temp_id (a handle); a LATER " +
2104
2237
  'operation references that new uuid as "$handle" in any id/container field (dotted ' +
2105
2238
  '"$handle.instance"/"$handle.replaced" reach a repeating op\'s spawned instance / ' +
2106
2239
  "replaced source). A temp_id is valid only on a creating operation (not tag.add — " +
2107
2240
  "reference a tag by title) and unique per batch. IDEMPOTENCY: op_id makes resubmission " +
2108
2241
  "safe — an operation matching an earlier success is reported already-applied, not " +
2109
- "re-created. The result adds temp_id_mapping (handle uuid) and undo_token, which " +
2110
- "reverses the whole batch as one unit via the undo tool.",
2242
+ "re-created (put an op_id on EVERY operation so a stopped batch can be resubmitted " +
2243
+ "verbatim to resume). The result adds temp_id_mapping (handle uuid) and undo_token, " +
2244
+ "which reverses the whole batch as one unit via the undo tool.",
2111
2245
  inputSchema: {
2112
2246
  ops: z
2113
2247
  .array(z.object({
@@ -2133,8 +2267,12 @@ export function createThingsMcpServer(options = {}) {
2133
2267
  .optional(),
2134
2268
  }))
2135
2269
  .describe("Ops in execution order"),
2136
- fail_fast: z.boolean().optional().describe("Skip remaining ops after the first failure"),
2270
+ continue_on_error: z
2271
+ .boolean()
2272
+ .optional()
2273
+ .describe("Run past a failed op instead of stopping (default: stop on failure)"),
2137
2274
  ...dryRunShape,
2275
+ ...preserveModifiedShape,
2138
2276
  },
2139
2277
  annotations: DESTRUCTIVE,
2140
2278
  }, async (args) => guard(async () => {
@@ -2163,7 +2301,7 @@ export function createThingsMcpServer(options = {}) {
2163
2301
  });
2164
2302
  const batchResult = await getClient().write.batch(ops, {
2165
2303
  ...(args.dry_run === true && { dryRun: true }),
2166
- ...(args.fail_fast === true && { failFast: true }),
2304
+ ...(args.continue_on_error === true && { continueOnError: true }),
2167
2305
  actor: mcpActor(),
2168
2306
  });
2169
2307
  // First block: the per-op results, each FLATTENED to the wire shape (a
@@ -2172,7 +2310,9 @@ export function createThingsMcpServer(options = {}) {
2172
2310
  // chaining/undo summary, present only when the batch minted a token or
2173
2311
  // bound temp ids.
2174
2312
  const lines = batchResult.results.map(flattenBatchLine);
2175
- const hasSummary = batchResult.undoToken !== undefined || Object.keys(batchResult.tempIdMapping).length > 0;
2313
+ const hasSummary = batchResult.undoToken !== undefined ||
2314
+ batchResult.resumption !== undefined ||
2315
+ Object.keys(batchResult.tempIdMapping).length > 0;
2176
2316
  if (!hasSummary)
2177
2317
  return jsonResult(lines);
2178
2318
  return {
@@ -2183,6 +2323,10 @@ export function createThingsMcpServer(options = {}) {
2183
2323
  text: JSON.stringify({
2184
2324
  tempIdMapping: batchResult.tempIdMapping,
2185
2325
  ...(batchResult.undoToken !== undefined && { undoToken: batchResult.undoToken }),
2326
+ // Resume guidance when a runtime failure stopped the batch (Change 2).
2327
+ ...(batchResult.resumption !== undefined && {
2328
+ resumption: batchResult.resumption,
2329
+ }),
2186
2330
  }),
2187
2331
  },
2188
2332
  ],
@@ -2224,6 +2368,7 @@ export function createThingsMcpServer(options = {}) {
2224
2368
  "day-block intermix both kinds."),
2225
2369
  ...driveGuiShape,
2226
2370
  ...dryRunShape,
2371
+ ...preserveModifiedShape,
2227
2372
  },
2228
2373
  annotations: NON_DESTRUCTIVE,
2229
2374
  }, async (args) => guard(async () => {
@@ -2242,9 +2387,14 @@ export function createThingsMcpServer(options = {}) {
2242
2387
  "cannot be undone here). By default this undoes only changes made through THIS " +
2243
2388
  "connection — this client's own writes; it will not touch the user's own edits, or " +
2244
2389
  'another client\'s, unless you pass by="*" (all authors) or a specific author name; ' +
2245
- "pass a txn token to undo one exact change. Some changes cannot be reversed " +
2246
- "permanent deletions, or changes whose prior state is unknown and are reported as " +
2247
- "irreversible; a to-do brought back from an undone delete returns to the Inbox " +
2390
+ "pass a txn token to undo one exact change. last/by select the N newest NOT-YET-undone " +
2391
+ "changes: a change already undone is SKIPPED, so repeating undo with last:N walks " +
2392
+ "progressively deeper into history instead of re-selecting the same changes. An " +
2393
+ "irreversible change is NOT skipped — it still counts toward N and is reported every " +
2394
+ "pass, so you see it rather than silently reaching past it. Some changes cannot be " +
2395
+ "reversed — permanent deletions, or changes whose prior state is unknown — and are " +
2396
+ "reported as irreversible; a to-do brought back from an undone delete returns to the " +
2397
+ "Inbox " +
2248
2398
  "without its schedule. Undoing the creation of an area or tag deletes it " +
2249
2399
  "permanently — requires dangerously_permanent. An undo is refused when the item " +
2250
2400
  "changed outside this interface since (its list or project, status, schedule, " +
@@ -2272,6 +2422,7 @@ export function createThingsMcpServer(options = {}) {
2272
2422
  "or by another tool) — overwrites whatever the out-of-band change left, instead of " +
2273
2423
  "refusing."),
2274
2424
  ...dryRunShape,
2425
+ ...preserveModifiedShape,
2275
2426
  },
2276
2427
  annotations: DESTRUCTIVE,
2277
2428
  }, async (args) => guard(async () => {