things-api 0.15.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 (90) 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/install-skill.js +9 -2
  5. package/dist/cli/commands/install-skill.js.map +1 -1
  6. package/dist/cli/commands/repeat-flags.d.ts +11 -1
  7. package/dist/cli/commands/repeat-flags.js +23 -0
  8. package/dist/cli/commands/repeat-flags.js.map +1 -1
  9. package/dist/cli/commands/todo.js +49 -2
  10. package/dist/cli/commands/todo.js.map +1 -1
  11. package/dist/cli/commands/writes.js +182 -61
  12. package/dist/cli/commands/writes.js.map +1 -1
  13. package/dist/cli/help.js +14 -11
  14. package/dist/cli/help.js.map +1 -1
  15. package/dist/cli/read-driver.d.ts +2 -2
  16. package/dist/cli/skill.d.ts +24 -0
  17. package/dist/cli/skill.js +40 -0
  18. package/dist/cli/skill.js.map +1 -1
  19. package/dist/client.d.ts +53 -12
  20. package/dist/client.js +6 -1
  21. package/dist/client.js.map +1 -1
  22. package/dist/contracts.d.ts +1 -1
  23. package/dist/contracts.js +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp/server.js +132 -31
  27. package/dist/mcp/server.js.map +1 -1
  28. package/dist/model/entities.d.ts +35 -0
  29. package/dist/model/entities.js.map +1 -1
  30. package/dist/read/detail.js +49 -1
  31. package/dist/read/detail.js.map +1 -1
  32. package/dist/read/queries.d.ts +12 -0
  33. package/dist/read/queries.js +14 -0
  34. package/dist/read/queries.js.map +1 -1
  35. package/dist/read/shape.d.ts +25 -22
  36. package/dist/read/shape.js +81 -55
  37. package/dist/read/shape.js.map +1 -1
  38. package/dist/read/stage.d.ts +5 -5
  39. package/dist/read/stage.js +2 -2
  40. package/dist/read/views.d.ts +1 -1
  41. package/dist/read/views.js +4 -4
  42. package/dist/surface-copy.d.ts +2 -2
  43. package/dist/surface-copy.js +2 -2
  44. package/dist/write/batch.d.ts +28 -3
  45. package/dist/write/batch.js +162 -46
  46. package/dist/write/batch.js.map +1 -1
  47. package/dist/write/clone.d.ts +4 -0
  48. package/dist/write/clone.js +531 -0
  49. package/dist/write/clone.js.map +1 -0
  50. package/dist/write/commands.js +90 -4
  51. package/dist/write/commands.js.map +1 -1
  52. package/dist/write/guards.d.ts +1 -1
  53. package/dist/write/guards.js +33 -5
  54. package/dist/write/guards.js.map +1 -1
  55. package/dist/write/make-repeating-project.d.ts +2 -3
  56. package/dist/write/make-repeating-project.js +2 -73
  57. package/dist/write/make-repeating-project.js.map +1 -1
  58. package/dist/write/move.js +8 -1
  59. package/dist/write/move.js.map +1 -1
  60. package/dist/write/operations.d.ts +131 -9
  61. package/dist/write/operations.js +4 -1
  62. package/dist/write/operations.js.map +1 -1
  63. package/dist/write/pipeline.d.ts +38 -0
  64. package/dist/write/pipeline.js +68 -3
  65. package/dist/write/pipeline.js.map +1 -1
  66. package/dist/write/pre-state.js +7 -1
  67. package/dist/write/pre-state.js.map +1 -1
  68. package/dist/write/preserve-modified.d.ts +55 -0
  69. package/dist/write/preserve-modified.js +106 -0
  70. package/dist/write/preserve-modified.js.map +1 -0
  71. package/dist/write/promote-clone.d.ts +32 -0
  72. package/dist/write/promote-clone.js +620 -0
  73. package/dist/write/promote-clone.js.map +1 -0
  74. package/dist/write/resolution-timestamps.js +22 -0
  75. package/dist/write/resolution-timestamps.js.map +1 -1
  76. package/dist/write/reversibility.js +27 -14
  77. package/dist/write/reversibility.js.map +1 -1
  78. package/dist/write/undo.d.ts +11 -1
  79. package/dist/write/undo.js +126 -10
  80. package/dist/write/undo.js.map +1 -1
  81. package/dist/write/vectors/simulator.js +32 -1
  82. package/dist/write/vectors/simulator.js.map +1 -1
  83. package/dist/write/vectors/ui-certification.d.ts +1 -1
  84. package/dist/write/vectors/ui-certification.js +10 -3
  85. package/dist/write/vectors/ui-certification.js.map +1 -1
  86. package/package.json +1 -1
  87. package/skills/things-cli/SKILL.md +3 -2
  88. package/skills/things-cli/references/banner.md +1 -1
  89. package/skills/things-cli/references/contracts.md +17 -9
  90. package/skills/things-cli/references/data-model.md +2 -1
@@ -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
  });
@@ -1137,6 +1149,7 @@ export function createThingsMcpServer(options = {}) {
1137
1149
  ...createTagsShape,
1138
1150
  ...tzShape,
1139
1151
  ...dryRunShape,
1152
+ ...preserveModifiedShape,
1140
1153
  ...opIdShape,
1141
1154
  },
1142
1155
  annotations: NON_DESTRUCTIVE,
@@ -1214,6 +1227,7 @@ export function createThingsMcpServer(options = {}) {
1214
1227
  ...createTagsShape,
1215
1228
  ...tzShape,
1216
1229
  ...dryRunShape,
1230
+ ...preserveModifiedShape,
1217
1231
  ...opIdShape,
1218
1232
  },
1219
1233
  annotations: NON_DESTRUCTIVE,
@@ -1329,6 +1343,7 @@ export function createThingsMcpServer(options = {}) {
1329
1343
  .describe(`status completed or canceled: the completion timestamp (${RESOLUTION_DATE_FORMAT})`),
1330
1344
  ...tzShape,
1331
1345
  ...dryRunShape,
1346
+ ...preserveModifiedShape,
1332
1347
  ...opIdShape,
1333
1348
  },
1334
1349
  annotations: NON_DESTRUCTIVE,
@@ -1423,6 +1438,7 @@ export function createThingsMcpServer(options = {}) {
1423
1438
  .optional()
1424
1439
  .describe("Confirm moving into a completed/canceled project (this reopens it)"),
1425
1440
  ...dryRunShape,
1441
+ ...preserveModifiedShape,
1426
1442
  },
1427
1443
  annotations: NON_DESTRUCTIVE,
1428
1444
  }, async (args) => guard(async () => {
@@ -1468,6 +1484,7 @@ export function createThingsMcpServer(options = {}) {
1468
1484
  mode: z.enum(["replace", "add"]).optional().describe("Default: replace"),
1469
1485
  ...createTagsShape,
1470
1486
  ...dryRunShape,
1487
+ ...preserveModifiedShape,
1471
1488
  ...opIdShape,
1472
1489
  },
1473
1490
  annotations: NON_DESTRUCTIVE,
@@ -1526,6 +1543,7 @@ export function createThingsMcpServer(options = {}) {
1526
1543
  .optional()
1527
1544
  .describe("replace: confirm discarding the existing items and their checked states"),
1528
1545
  ...dryRunShape,
1546
+ ...preserveModifiedShape,
1529
1547
  },
1530
1548
  annotations: NON_DESTRUCTIVE,
1531
1549
  }, async (args) => guard(async () => {
@@ -1601,6 +1619,7 @@ export function createThingsMcpServer(options = {}) {
1601
1619
  .optional()
1602
1620
  .describe("kind tag: confirm permanent deletion of ALL nested child tags too"),
1603
1621
  ...dryRunShape,
1622
+ ...preserveModifiedShape,
1604
1623
  ...opIdShape,
1605
1624
  },
1606
1625
  annotations: DESTRUCTIVE,
@@ -1622,7 +1641,7 @@ export function createThingsMcpServer(options = {}) {
1622
1641
  description: "Restore a trashed to-do or project. A to-do returns to the Inbox without its " +
1623
1642
  "previous schedule or project/area. A project is restored in place: its schedule, " +
1624
1643
  "area, and children come back exactly as they were.",
1625
- inputSchema: { uuid: z.string(), ...dryRunShape, ...opIdShape },
1644
+ inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
1626
1645
  annotations: NON_DESTRUCTIVE,
1627
1646
  }, async (args) => guard(async () => {
1628
1647
  const c = getClient();
@@ -1709,6 +1728,7 @@ export function createThingsMcpServer(options = {}) {
1709
1728
  .describe("add/move: place immediately after this heading (title or uuid)"),
1710
1729
  ...driveGuiShape,
1711
1730
  ...dryRunShape,
1731
+ ...preserveModifiedShape,
1712
1732
  },
1713
1733
  annotations: DESTRUCTIVE,
1714
1734
  }, async (args) => guard(async () => {
@@ -1809,6 +1829,7 @@ export function createThingsMcpServer(options = {}) {
1809
1829
  uuid: z.string().describe("the to-do's uuid"),
1810
1830
  ...driveGuiShape,
1811
1831
  ...dryRunShape,
1832
+ ...preserveModifiedShape,
1812
1833
  ...opIdShape,
1813
1834
  },
1814
1835
  annotations: DESTRUCTIVE,
@@ -1818,7 +1839,7 @@ export function createThingsMcpServer(options = {}) {
1818
1839
  "Things proxy shortcuts when installed (in place, and the only path for a repeating " +
1819
1840
  "to-do); otherwise a non-repeating dated to-do falls back to a URL re-schedule that " +
1820
1841
  "briefly moves it to Today and back. Reversible with the undo tool.",
1821
- inputSchema: { uuid: z.string(), ...dryRunShape },
1842
+ inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape },
1822
1843
  annotations: NON_DESTRUCTIVE,
1823
1844
  }, async (args) => guard(async () => mutationResult(await getClient().write.clearReminder(args.uuid, writeOptions(args)))));
1824
1845
  // -------------------------------------------- GUI-driven (Accessibility)
@@ -1902,16 +1923,17 @@ export function createThingsMcpServer(options = {}) {
1902
1923
  server.registerTool("repeat", {
1903
1924
  description: "Manage recurrence on a to-do or project (scope) by driving the local Things app's " +
1904
1925
  "interface — every action needs dangerously_drive_gui. action start: turn a plain item " +
1905
- "into a repeating one this REPLACES it with a new series and cannot be undone (give " +
1906
- "frequency + interval and optionally the weekday set, monthly/yearly day, end bound, " +
1907
- "reminder, or per-occurrence deadline); returns a repeating block with instanceUuid (the " +
1908
- "visible occurrence), templateUuid (the rule), and replacedUuid. action reschedule: " +
1909
- "change a repeating item's rule in place, keeping the same item (undoable it restores " +
1910
- "the previous rule). action pause/resume: stop or restart its new occurrences, keeping " +
1911
- "the rule. action add (scope project only): create a project and make it repeating in " +
1912
- "one call the project is created first and PERSISTS even if the make-repeating step " +
1913
- "refuses; give an area to place it or omit it to create in Someday (only frequency and " +
1914
- "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.",
1915
1937
  inputSchema: {
1916
1938
  scope: z.enum(["todo", "project"]),
1917
1939
  action: z.enum(["start", "reschedule", "pause", "resume", "add"]),
@@ -1919,13 +1941,13 @@ export function createThingsMcpServer(options = {}) {
1919
1941
  .string()
1920
1942
  .optional()
1921
1943
  .describe("start/reschedule/pause/resume: the item (a project also accepts a unique name)"),
1922
- title: z.string().optional().describe("add (project): the new project's title"),
1923
- 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"),
1924
1946
  area: z.string().optional().describe(`add (project): destination area (${REF_FORMAT})`),
1925
1947
  project_deadline: z
1926
1948
  .string()
1927
1949
  .optional()
1928
- .describe(`add (project): the project's due date — ${DATE_FORMAT}`),
1950
+ .describe(`add: the item's due date — ${DATE_FORMAT}`),
1929
1951
  todos: z.array(z.string()).optional().describe("add (project): initial child to-do titles"),
1930
1952
  frequency: z
1931
1953
  .enum(["daily", "weekly", "monthly", "yearly"])
@@ -1951,6 +1973,7 @@ export function createThingsMcpServer(options = {}) {
1951
1973
  start_days_earlier: repeatRuleShape.start_days_earlier,
1952
1974
  ...driveGuiShape,
1953
1975
  ...dryRunShape,
1976
+ ...preserveModifiedShape,
1954
1977
  },
1955
1978
  annotations: DESTRUCTIVE,
1956
1979
  }, async (args) => guard(async () => {
@@ -1958,13 +1981,25 @@ export function createThingsMcpServer(options = {}) {
1958
1981
  const opts = writeOptions(args);
1959
1982
  const { frequency, interval } = args;
1960
1983
  if (args.action === "add") {
1961
- if (args.scope !== "project")
1962
- return usage('action "add" requires scope "project"');
1963
1984
  if (args.title === undefined)
1964
1985
  return usage('action "add" requires title');
1965
1986
  if (frequency === undefined || interval === undefined) {
1966
1987
  return usage('action "add" requires frequency and interval');
1967
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
+ }
1968
2003
  return mutationResult(await c.write.addRepeatingProject({
1969
2004
  title: args.title,
1970
2005
  ...(args.notes !== undefined && { notes: args.notes }),
@@ -1973,6 +2008,7 @@ export function createThingsMcpServer(options = {}) {
1973
2008
  ...(args.todos !== undefined && { todos: args.todos }),
1974
2009
  frequency,
1975
2010
  interval,
2011
+ ...ruleExtras,
1976
2012
  }, opts));
1977
2013
  }
1978
2014
  if (args.uuid === undefined)
@@ -1993,8 +2029,10 @@ export function createThingsMcpServer(options = {}) {
1993
2029
  }
1994
2030
  const extras = repeatExtras(args, frequency);
1995
2031
  if (args.scope === "todo") {
1996
- const op = args.action === "start" ? "todo.make-repeating" : "todo.reschedule-repeat";
1997
- 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));
1998
2036
  }
1999
2037
  // scope project
2000
2038
  if (args.action === "reschedule") {
@@ -2006,7 +2044,7 @@ export function createThingsMcpServer(options = {}) {
2006
2044
  server.registerTool("duplicate_item", {
2007
2045
  description: "Duplicate a to-do or project and return the copy's uuid; a duplicated project " +
2008
2046
  "includes its children. Not available for repeating items.",
2009
- inputSchema: { uuid: z.string(), ...dryRunShape, ...opIdShape },
2047
+ inputSchema: { uuid: z.string(), ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
2010
2048
  annotations: NON_DESTRUCTIVE,
2011
2049
  }, async (args) => guard(async () => {
2012
2050
  const c = getClient();
@@ -2014,6 +2052,42 @@ export function createThingsMcpServer(options = {}) {
2014
2052
  ? await c.write.duplicateTodo(args.uuid, writeOptions(args))
2015
2053
  : await c.write.duplicateProject(args.uuid, writeOptions(args)));
2016
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
+ }));
2017
2091
  // -------------------------------------------------------------- projects
2018
2092
  server.registerTool("add_project", {
2019
2093
  description: "Create a project and return its uuid. Optionally place it in an area, schedule " +
@@ -2036,6 +2110,7 @@ export function createThingsMcpServer(options = {}) {
2036
2110
  `(${RESOLUTION_DATE_FORMAT}); cannot seed open child to-dos`),
2037
2111
  ...tzShape,
2038
2112
  ...dryRunShape,
2113
+ ...preserveModifiedShape,
2039
2114
  ...opIdShape,
2040
2115
  },
2041
2116
  annotations: NON_DESTRUCTIVE,
@@ -2067,6 +2142,7 @@ export function createThingsMcpServer(options = {}) {
2067
2142
  no_area: z.boolean().optional().describe("Leave the current area (a project's detach)"),
2068
2143
  ...positionShape,
2069
2144
  ...dryRunShape,
2145
+ ...preserveModifiedShape,
2070
2146
  },
2071
2147
  annotations: NON_DESTRUCTIVE,
2072
2148
  }, async (args) => guard(async () => {
@@ -2095,6 +2171,7 @@ export function createThingsMcpServer(options = {}) {
2095
2171
  tags: z.array(z.string()).optional().describe(`Tags — ${TAG_REF_FORMAT}`),
2096
2172
  ...createTagsShape,
2097
2173
  ...dryRunShape,
2174
+ ...preserveModifiedShape,
2098
2175
  ...opIdShape,
2099
2176
  },
2100
2177
  annotations: NON_DESTRUCTIVE,
@@ -2106,6 +2183,7 @@ export function createThingsMcpServer(options = {}) {
2106
2183
  title: z.string(),
2107
2184
  parent: z.string().optional().describe("Existing parent tag name"),
2108
2185
  ...dryRunShape,
2186
+ ...preserveModifiedShape,
2109
2187
  ...opIdShape,
2110
2188
  },
2111
2189
  annotations: NON_DESTRUCTIVE,
@@ -2114,7 +2192,7 @@ export function createThingsMcpServer(options = {}) {
2114
2192
  description: "Move completed items to the Logbook now. The result discloses how many items were moved " +
2115
2193
  "(observed.logged); when none are waiting it logs nothing — a clean no-op, not an error. " +
2116
2194
  "This cannot be undone.",
2117
- inputSchema: { ...dryRunShape, ...opIdShape },
2195
+ inputSchema: { ...dryRunShape, ...preserveModifiedShape, ...opIdShape },
2118
2196
  annotations: DESTRUCTIVE,
2119
2197
  }, async (args) => guard(async () => mutationResult(await getClient().write.logNow(writeOptions(args)))));
2120
2198
  // -------------------------------------------------- generic + discovery
@@ -2142,22 +2220,28 @@ export function createThingsMcpServer(options = {}) {
2142
2220
  .optional()
2143
2221
  .describe("area.delete: delete a non-empty area together with its contents"),
2144
2222
  ...dryRunShape,
2223
+ ...preserveModifiedShape,
2145
2224
  ...opIdShape,
2146
2225
  },
2147
2226
  annotations: DESTRUCTIVE,
2148
2227
  }, async (args) => guard(async () => mutationResult(await getClient().write.run(args.op, args.params, writeOptions(args)))));
2149
2228
  server.registerTool("batch", {
2150
2229
  description: "Run several operations in order, each independently — there are no transactions, " +
2151
- "and a failure does not roll back earlier operations. Per-operation results return " +
2152
- "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. " +
2153
2236
  "CHAINING: an operation that creates something may carry temp_id (a handle); a LATER " +
2154
2237
  'operation references that new uuid as "$handle" in any id/container field (dotted ' +
2155
2238
  '"$handle.instance"/"$handle.replaced" reach a repeating op\'s spawned instance / ' +
2156
2239
  "replaced source). A temp_id is valid only on a creating operation (not tag.add — " +
2157
2240
  "reference a tag by title) and unique per batch. IDEMPOTENCY: op_id makes resubmission " +
2158
2241
  "safe — an operation matching an earlier success is reported already-applied, not " +
2159
- "re-created. The result adds temp_id_mapping (handle uuid) and undo_token, which " +
2160
- "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.",
2161
2245
  inputSchema: {
2162
2246
  ops: z
2163
2247
  .array(z.object({
@@ -2183,8 +2267,12 @@ export function createThingsMcpServer(options = {}) {
2183
2267
  .optional(),
2184
2268
  }))
2185
2269
  .describe("Ops in execution order"),
2186
- 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)"),
2187
2274
  ...dryRunShape,
2275
+ ...preserveModifiedShape,
2188
2276
  },
2189
2277
  annotations: DESTRUCTIVE,
2190
2278
  }, async (args) => guard(async () => {
@@ -2213,7 +2301,7 @@ export function createThingsMcpServer(options = {}) {
2213
2301
  });
2214
2302
  const batchResult = await getClient().write.batch(ops, {
2215
2303
  ...(args.dry_run === true && { dryRun: true }),
2216
- ...(args.fail_fast === true && { failFast: true }),
2304
+ ...(args.continue_on_error === true && { continueOnError: true }),
2217
2305
  actor: mcpActor(),
2218
2306
  });
2219
2307
  // First block: the per-op results, each FLATTENED to the wire shape (a
@@ -2222,7 +2310,9 @@ export function createThingsMcpServer(options = {}) {
2222
2310
  // chaining/undo summary, present only when the batch minted a token or
2223
2311
  // bound temp ids.
2224
2312
  const lines = batchResult.results.map(flattenBatchLine);
2225
- 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;
2226
2316
  if (!hasSummary)
2227
2317
  return jsonResult(lines);
2228
2318
  return {
@@ -2233,6 +2323,10 @@ export function createThingsMcpServer(options = {}) {
2233
2323
  text: JSON.stringify({
2234
2324
  tempIdMapping: batchResult.tempIdMapping,
2235
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
+ }),
2236
2330
  }),
2237
2331
  },
2238
2332
  ],
@@ -2274,6 +2368,7 @@ export function createThingsMcpServer(options = {}) {
2274
2368
  "day-block intermix both kinds."),
2275
2369
  ...driveGuiShape,
2276
2370
  ...dryRunShape,
2371
+ ...preserveModifiedShape,
2277
2372
  },
2278
2373
  annotations: NON_DESTRUCTIVE,
2279
2374
  }, async (args) => guard(async () => {
@@ -2292,9 +2387,14 @@ export function createThingsMcpServer(options = {}) {
2292
2387
  "cannot be undone here). By default this undoes only changes made through THIS " +
2293
2388
  "connection — this client's own writes; it will not touch the user's own edits, or " +
2294
2389
  'another client\'s, unless you pass by="*" (all authors) or a specific author name; ' +
2295
- "pass a txn token to undo one exact change. Some changes cannot be reversed " +
2296
- "permanent deletions, or changes whose prior state is unknown and are reported as " +
2297
- "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 " +
2298
2398
  "without its schedule. Undoing the creation of an area or tag deletes it " +
2299
2399
  "permanently — requires dangerously_permanent. An undo is refused when the item " +
2300
2400
  "changed outside this interface since (its list or project, status, schedule, " +
@@ -2322,6 +2422,7 @@ export function createThingsMcpServer(options = {}) {
2322
2422
  "or by another tool) — overwrites whatever the out-of-band change left, instead of " +
2323
2423
  "refusing."),
2324
2424
  ...dryRunShape,
2425
+ ...preserveModifiedShape,
2325
2426
  },
2326
2427
  annotations: DESTRUCTIVE,
2327
2428
  }, async (args) => guard(async () => {