things-api 0.19.0 → 0.19.1

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 (131) hide show
  1. package/README.md +3 -1
  2. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  3. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  5. package/dist/audit/schema.d.ts +19 -2
  6. package/dist/audit/schema.js.map +1 -1
  7. package/dist/capability.d.ts +166 -4
  8. package/dist/capability.js +303 -13
  9. package/dist/capability.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +13 -1
  11. package/dist/cli/commands/doctor.js +24 -14
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/helpers.js +14 -3
  14. package/dist/cli/commands/helpers.js.map +1 -1
  15. package/dist/cli/commands/mcp.js +8 -0
  16. package/dist/cli/commands/mcp.js.map +1 -1
  17. package/dist/cli/commands/repeat-flags.js +34 -2
  18. package/dist/cli/commands/repeat-flags.js.map +1 -1
  19. package/dist/cli/commands/setup.js +16 -3
  20. package/dist/cli/commands/setup.js.map +1 -1
  21. package/dist/cli/commands/writes.js +102 -67
  22. package/dist/cli/commands/writes.js.map +1 -1
  23. package/dist/cli/interrupt.d.ts +54 -6
  24. package/dist/cli/interrupt.js +78 -12
  25. package/dist/cli/interrupt.js.map +1 -1
  26. package/dist/cli/main.js +8 -5
  27. package/dist/cli/main.js.map +1 -1
  28. package/dist/client.d.ts +1 -1
  29. package/dist/contracts.d.ts +3 -3
  30. package/dist/contracts.js +1 -1
  31. package/dist/deputy/install.d.ts +15 -1
  32. package/dist/deputy/install.js +46 -6
  33. package/dist/deputy/install.js.map +1 -1
  34. package/dist/deputy/routing.d.ts +16 -0
  35. package/dist/deputy/routing.js +70 -0
  36. package/dist/deputy/routing.js.map +1 -1
  37. package/dist/deputy/wake.d.ts +61 -0
  38. package/dist/deputy/wake.js +149 -0
  39. package/dist/deputy/wake.js.map +1 -0
  40. package/dist/diagnose.d.ts +6 -4
  41. package/dist/diagnose.js +20 -4
  42. package/dist/diagnose.js.map +1 -1
  43. package/dist/direct-setup.d.ts +31 -5
  44. package/dist/direct-setup.js +200 -26
  45. package/dist/direct-setup.js.map +1 -1
  46. package/dist/host-access.d.ts +12 -0
  47. package/dist/host-access.js +12 -0
  48. package/dist/host-access.js.map +1 -1
  49. package/dist/index.d.ts +3 -3
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/mcp/server.js +23 -9
  53. package/dist/mcp/server.js.map +1 -1
  54. package/dist/op-result.js +8 -1
  55. package/dist/op-result.js.map +1 -1
  56. package/dist/session-grant.js +16 -1
  57. package/dist/session-grant.js.map +1 -1
  58. package/dist/surface-copy.d.ts +11 -3
  59. package/dist/surface-copy.js +11 -3
  60. package/dist/surface-copy.js.map +1 -1
  61. package/dist/wizard.js +8 -1
  62. package/dist/wizard.js.map +1 -1
  63. package/dist/write/availability.d.ts +0 -15
  64. package/dist/write/availability.js +7 -62
  65. package/dist/write/availability.js.map +1 -1
  66. package/dist/write/batch.js +17 -6
  67. package/dist/write/batch.js.map +1 -1
  68. package/dist/write/commands.d.ts +10 -6
  69. package/dist/write/commands.js +120 -31
  70. package/dist/write/commands.js.map +1 -1
  71. package/dist/write/failure-hints.d.ts +29 -6
  72. package/dist/write/failure-hints.js +9 -14
  73. package/dist/write/failure-hints.js.map +1 -1
  74. package/dist/write/guards.js +3 -2
  75. package/dist/write/guards.js.map +1 -1
  76. package/dist/write/move.js +9 -12
  77. package/dist/write/move.js.map +1 -1
  78. package/dist/write/operations.d.ts +15 -8
  79. package/dist/write/operations.js +9 -0
  80. package/dist/write/operations.js.map +1 -1
  81. package/dist/write/opid.d.ts +67 -23
  82. package/dist/write/opid.js +141 -19
  83. package/dist/write/opid.js.map +1 -1
  84. package/dist/write/param-schema.d.ts +0 -44
  85. package/dist/write/param-schema.js +10 -3
  86. package/dist/write/param-schema.js.map +1 -1
  87. package/dist/write/pipeline.d.ts +38 -7
  88. package/dist/write/pipeline.js +104 -18
  89. package/dist/write/pipeline.js.map +1 -1
  90. package/dist/write/pre-state.d.ts +30 -16
  91. package/dist/write/pre-state.js +88 -20
  92. package/dist/write/pre-state.js.map +1 -1
  93. package/dist/write/promote-clone.js +132 -18
  94. package/dist/write/promote-clone.js.map +1 -1
  95. package/dist/write/repeat-collateral.d.ts +31 -0
  96. package/dist/write/repeat-collateral.js +153 -0
  97. package/dist/write/repeat-collateral.js.map +1 -0
  98. package/dist/write/resolution-timestamps.js +25 -2
  99. package/dist/write/resolution-timestamps.js.map +1 -1
  100. package/dist/write/template-mutation.js +163 -16
  101. package/dist/write/template-mutation.js.map +1 -1
  102. package/dist/write/vectors/applescript.js +3 -10
  103. package/dist/write/vectors/applescript.js.map +1 -1
  104. package/dist/write/vectors/simulator.js +8 -2
  105. package/dist/write/vectors/simulator.js.map +1 -1
  106. package/dist/write/vectors/types.d.ts +211 -1
  107. package/dist/write/vectors/ui-certification.js +63 -13
  108. package/dist/write/vectors/ui-certification.js.map +1 -1
  109. package/dist/write/vectors/ui-chord.d.ts +144 -0
  110. package/dist/write/vectors/ui-chord.js +428 -0
  111. package/dist/write/vectors/ui-chord.js.map +1 -0
  112. package/dist/write/vectors/ui-drag.d.ts +8 -4
  113. package/dist/write/vectors/ui-drag.js +2 -0
  114. package/dist/write/vectors/ui-drag.js.map +1 -1
  115. package/dist/write/vectors/ui-recipes.d.ts +51 -3
  116. package/dist/write/vectors/ui-recipes.js +452 -44
  117. package/dist/write/vectors/ui-recipes.js.map +1 -1
  118. package/dist/write/vectors/ui.d.ts +267 -10
  119. package/dist/write/vectors/ui.js +1006 -64
  120. package/dist/write/vectors/ui.js.map +1 -1
  121. package/dist/write/vectors/url-scheme.js +3 -0
  122. package/dist/write/vectors/url-scheme.js.map +1 -1
  123. package/dist/write/verify/delta.d.ts +41 -0
  124. package/dist/write/verify/delta.js +36 -2
  125. package/dist/write/verify/delta.js.map +1 -1
  126. package/dist/write/verify/poller.d.ts +4 -1
  127. package/dist/write/verify/poller.js +14 -0
  128. package/dist/write/verify/poller.js.map +1 -1
  129. package/package.json +1 -1
  130. package/schema/envelope.schema.json +1 -1
  131. package/skills/things-cli/SKILL.md +1 -1
@@ -11,7 +11,7 @@ import { armInterrupt, disarmInterrupt } from "../interrupt.js";
11
11
  import { isDevVersion } from "../skill.js";
12
12
  import { CLI_VERSION } from "../version.js";
13
13
  import { addRepeatCalendarFlags, addRepeatingRuleFieldsFromOpts, addRepeatRuleFlags, repeatRuleFlagsFromOpts, } from "./repeat-flags.js";
14
- import { aggregateExitCode, blockedCode, buildUpdatePatch, capabilitiesTable, CLI_UPDATE_LABELS, ClockError, closeCliTrace, describeConfig, errorEnvelope, ExitCode, HELPERS_MODES, installCliTrace, getConfigKey, parseHelpersMode, mutationWireData, NOTES_FORMAT, okEnvelope, openThings, OP_ID_RE, outcomeFailed, ParamSchemaError, ReferenceResolutionError, saveConfigKey, splitWhenSugar, trace, ThingsDbNotFoundError, ThingsDbOpenError, verifyFailedCode, } from "../../index.js";
14
+ import { aggregateExitCode, blockedCode, buildUpdatePatch, capabilitiesTable, CLI_UPDATE_LABELS, ClockError, closeCliTrace, describeConfig, errorEnvelope, ExitCode, HELPERS_MODES, installCliTrace, getConfigKey, parseHelpersMode, mutationWireData, NOTES_FORMAT, okEnvelope, openThings, OP_ID_RE, outcomeFailed, ParamSchemaError, ReferenceResolutionError, RESOLUTION_DATE_FORMAT, saveConfigKey, splitWhenSugar, trace, ThingsDbNotFoundError, ThingsDbOpenError, verifyFailedCode, } from "../../index.js";
15
15
  import { usageError } from "../read-driver.js";
16
16
  import { dim } from "../style.js";
17
17
  /**
@@ -43,11 +43,12 @@ function addWriteFlags(cmd, capability = {}) {
43
43
  .addOption(opId);
44
44
  }
45
45
  /**
46
- * The write flags for a multi-leg COMPOUND command: the same set, with `--op-id`
47
- * kept out of `--help` because that command's execution refuses it (see
48
- * `opIdCompoundRefused` / `runMoveCmd`). Every command wrapped in this MUST
46
+ * The write flags for a multi-result COMPOUND command: the same set, with
47
+ * `--op-id` kept out of `--help` because that command's execution refuses it
48
+ * (see `opIdCompoundRefused` / `runMoveCmd`). Every command wrapped in this MUST
49
49
  * refuse `--op-id` at runtime, and every command that refuses it MUST be wrapped
50
- * in this — `test/cli/help-contract.test.ts` holds the two halves together.
50
+ * in this — `test/cli/help-contract.test.ts` holds the two halves together. A
51
+ * compound that records ONE summary the key rides uses plain `addWriteFlags`.
51
52
  */
52
53
  const addCompoundWriteFlags = (cmd) => addWriteFlags(cmd, { opId: "unsupported" });
53
54
  /** Validate an `--op-id`; on a malformed value emit the usage error and return false. */
@@ -60,11 +61,16 @@ function opIdOk(opts) {
60
61
  return false;
61
62
  }
62
63
  /**
63
- * Refuse `--op-id` on a multi-leg COMPOUND command. Single-op idempotency (phase
64
- * 1) replays exactly ONE recorded ok result; a compound records several (or a
65
- * summary) and its idempotency is the batch-shaped per-line `op_id`. Refusing is
66
- * honest dropping the flag silently would leave a resubmission un-deduped.
67
- * Returns true when refused (the caller returns).
64
+ * Refuse `--op-id` on a multi-leg COMPOUND command. The rule is ONE RECORDED
65
+ * RESULT PER KEY: an idempotency key replays exactly one recorded outcome, so a
66
+ * command that records SEVERAL the variadic move/reorder, the granular
67
+ * checklist edit, clear-reminder, the heading archive pair, project reopen
68
+ * cannot honor one, and its idempotency is the batch-shaped per-line `op_id`.
69
+ * (A compound that records ONE SUMMARY the key rides — the template-target
70
+ * verbs, the `--completed-at` flip-dance, and the promote verbs — accepts the
71
+ * flag instead; ruling 2026-08-25.) Refusing is honest — dropping the flag
72
+ * silently would leave a resubmission un-deduped. Returns true when refused
73
+ * (the caller returns).
68
74
  */
69
75
  function opIdCompoundRefused(opts, what) {
70
76
  if (opts.opId === undefined)
@@ -216,10 +222,15 @@ function splitCsv(value) {
216
222
  .filter((s) => s !== "");
217
223
  }
218
224
  /**
219
- * Open a CLI write invocation: install the dev-mode step-timeline trace and arm
220
- * the signal-safe interrupt guard (TRACE1, #487). Returns the teardown for the
221
- * driver's `finally` — it closes the trace with the final exit code, then
222
- * disarms the guard so a signal during teardown/reads emits nothing.
225
+ * Open a CLI write invocation: install the dev-mode step-timeline trace, and
226
+ * hand back the two things every write driver needs (TRACE1, #487)
227
+ *
228
+ * - `openClient(dbPath)`: opens the Things client AND arms the signal-safe
229
+ * interrupt guard once that open has returned. This is the ONLY arming seam
230
+ * for the CLI; see below for why it sits past the open.
231
+ * - `endInvocation()`: the driver's `finally` — closes the trace with the
232
+ * final exit code, then disarms the guard so a signal during teardown or a
233
+ * trailing read emits nothing.
223
234
  *
224
235
  * EVERY write driver calls this, not just the single-mutation one: the library
225
236
  * half (the in-flight marker, the watchdog, the plain-stderr interrupt line)
@@ -228,6 +239,15 @@ function splitCsv(value) {
228
239
  * invocation's stdout is machine-readable — drivers that emit JSONL regardless
229
240
  * of `--json` (batch, undo) pass true.
230
241
  *
242
+ * ## Why the arm sits PAST the client open
243
+ *
244
+ * `openThings` is the read gate, and its `open(2)` on the Things container is
245
+ * synchronous and can block indefinitely behind a TCC dialog. A listener armed
246
+ * across that span cannot dispatch (see src/cli/interrupt.ts's module note), so
247
+ * arming from the top of the driver made a write stalled there swallow SIGTERM
248
+ * — measured exit 137. Nothing can be in flight before the client exists, so
249
+ * the window this gives up is one where the guard had nothing to report anyway.
250
+ *
231
251
  * The sink is a no-op unless tracing is on (a `-dev` build, or config/env
232
252
  * forced), so this costs a config read on every write and nothing else.
233
253
  */
@@ -237,22 +257,28 @@ function beginWriteInvocation(json, startedAt) {
237
257
  version: CLI_VERSION,
238
258
  isDev: isDevVersion(CLI_VERSION),
239
259
  });
240
- armInterrupt(json);
241
- return () => {
242
- trace(() => ({
243
- phase: "invocation-end",
244
- exitCode: process.exitCode ?? 0,
245
- elapsedMs: Date.now() - startedAt,
246
- }));
247
- disarmInterrupt();
248
- closeCliTrace();
260
+ return {
261
+ openClient: (dbPath) => {
262
+ const client = openThings(dbPath ? { dbPath } : {});
263
+ armInterrupt(json);
264
+ return client;
265
+ },
266
+ endInvocation: () => {
267
+ trace(() => ({
268
+ phase: "invocation-end",
269
+ exitCode: process.exitCode ?? 0,
270
+ elapsedMs: Date.now() - startedAt,
271
+ }));
272
+ disarmInterrupt();
273
+ closeCliTrace();
274
+ },
249
275
  };
250
276
  }
251
277
  async function runWrite(opts, fn, emitFn = emitResult) {
252
278
  if (!opIdOk(opts))
253
279
  return;
254
280
  const started = Date.now();
255
- const endInvocation = beginWriteInvocation(opts.json === true, started);
281
+ const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, started);
256
282
  let client = null;
257
283
  const meta = (client_) => {
258
284
  let dbVersion = null;
@@ -267,7 +293,7 @@ async function runWrite(opts, fn, emitFn = emitResult) {
267
293
  return { dbVersion, fingerprint, elapsedMs: Date.now() - started, ...(clock && { clock }) };
268
294
  };
269
295
  try {
270
- client = openThings(opts.db ? { dbPath: opts.db } : {});
296
+ client = openClient(opts.db);
271
297
  const result = await fn(client);
272
298
  emitFn(result, opts, meta(client));
273
299
  }
@@ -497,7 +523,7 @@ async function runMoveCmd(opts, fn) {
497
523
  return;
498
524
  }
499
525
  const started = Date.now();
500
- const endInvocation = beginWriteInvocation(opts.json === true, started);
526
+ const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, started);
501
527
  let client = null;
502
528
  const meta = () => {
503
529
  let dbVersion = null;
@@ -510,7 +536,7 @@ async function runMoveCmd(opts, fn) {
510
536
  return { dbVersion, fingerprint, elapsedMs: Date.now() - started };
511
537
  };
512
538
  try {
513
- client = openThings(opts.db ? { dbPath: opts.db } : {});
539
+ client = openClient(opts.db);
514
540
  emitMoveResult(await fn(client), opts, meta());
515
541
  }
516
542
  catch (err) {
@@ -757,10 +783,10 @@ function addResultLine(r) {
757
783
  * in creation order, and nothing else. Exit code is the worst leg's failure.
758
784
  */
759
785
  async function runBulkAdd(opts, ops, idOnly) {
760
- const endInvocation = beginWriteInvocation(opts.json === true, Date.now());
786
+ const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, Date.now());
761
787
  let client = null;
762
788
  try {
763
- client = openThings(opts.db ? { dbPath: opts.db } : {});
789
+ client = openClient(opts.db);
764
790
  const batchResult = await client.write.batch(ops, {
765
791
  ...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
766
792
  ...(opts.actor !== undefined && { actor: opts.actor }),
@@ -889,8 +915,8 @@ export function registerWriteCommands(program) {
889
915
  .option("--project <ref>", "destination project (uuid or unique name)")
890
916
  .option("--area <ref>", "destination area (uuid or unique name)")
891
917
  .option("--heading <name>", "existing heading in the destination project")
892
- .option("--created-at <iso>", "born with this creation timestamp (ISO date or datetime; a date is noon in the effective zone)")
893
- .option("--completed-at <iso>", "born completed (in the Logbook) with this completion timestamp (ISO date or datetime); drop --when/--reminder")
918
+ .option("--created-at <iso>", `born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`)
919
+ .option("--completed-at <iso>", `born completed (in the Logbook) with this completion timestamp (${RESOLUTION_DATE_FORMAT}); drop --when/--reminder`)
894
920
  .option("--acknowledge-project-reopen", "allow adding into a completed/canceled project")
895
921
  .option("--stdin", "read newline-delimited titles from stdin (blank lines skipped); exclusive with title arguments")
896
922
  .option("--id-only", "print only the new uuid(s), one per line in creation order, and nothing else (exclusive with --json)"))).action(async (titles, opts) => {
@@ -1012,7 +1038,7 @@ export function registerWriteCommands(program) {
1012
1038
  .option("--clear-reminder", "clear the reminder (works while scheduled today|evening)")
1013
1039
  .option("--deadline <date>", "YYYY-MM-DD")
1014
1040
  .option("--clear-deadline", "remove the deadline")
1015
- .option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
1041
+ .option("--created-at <iso>", `rewrite the creation timestamp (${RESOLUTION_DATE_FORMAT}); status-safe`)
1016
1042
  .option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved to-do (a canceled one stays canceled); open to-dos are refused — use complete/cancel")
1017
1043
  .option("--exception", "repeating to-dos only: change just the NEXT occurrence and leave the series alone " +
1018
1044
  "(creates that occurrence if it has not appeared yet); refused when the series already " +
@@ -1059,7 +1085,7 @@ export function registerWriteCommands(program) {
1059
1085
  "creating a second occurrence. " +
1060
1086
  '--completed-at sets the completion timestamp (also the "Completed on" stamp for a canceled ' +
1061
1087
  `item), ${stampNote} — a multi-leg sequence, disclosed in the result and --dry-run.`)
1062
- .option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
1088
+ .option("--completed-at <iso>", `completion timestamp (${RESOLUTION_DATE_FORMAT})`)).action(async (uuid, opts) => {
1063
1089
  const resolution = opts["completedAt"] !== undefined ? { completedAt: opts["completedAt"] } : {};
1064
1090
  await runWrite(opts, (c) => c.write[method](uuid, resolution, writeOptionsFrom(opts)));
1065
1091
  });
@@ -1269,7 +1295,9 @@ export function registerWriteCommands(program) {
1269
1295
  .description("Turn a plain to-do into a repeating one. A disposable copy is promoted and the " +
1270
1296
  "ORIGINAL is moved to the Trash — so `things undo` reverses it (it removes the new " +
1271
1297
  "series and restores the original). The new template's uuid is printed. Set the rule " +
1272
- "with the flags below; see `things help repeating`.")
1298
+ "with the flags below; see `things help repeating`. Each re-run makes ANOTHER series, " +
1299
+ "so pass --op-id on a retry: a resubmission with the same key replays the first " +
1300
+ "result instead of making a second series.")
1273
1301
  .requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
1274
1302
  .requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
1275
1303
  const frequency = opts["frequency"];
@@ -1408,14 +1436,18 @@ export function registerWriteCommands(program) {
1408
1436
  return c.write.run("project.promote-heading", { uuid: h.uuid }, writeOptionsFrom(opts));
1409
1437
  });
1410
1438
  });
1411
- addPlacementFlags(addWriteFlags(project
1439
+ addPlacementFlags(addDriveGuiFlag(addWriteFlags(project
1412
1440
  .command("move-heading <project> <headings...>")
1413
1441
  .description("Reposition one or more of a project's headings as an ordered block — the selection " +
1414
- "order is the resulting order, and each heading's to-dos follow it. Pass exactly one " +
1415
- "placement: --first, --last, --before-heading <sel>, or --after-heading <sel>. Each " +
1416
- `<heading> is a ${HEADING_SEL_HELP}. Reordering headings rides the same experimental ` +
1417
- "surface as `things reorder` enable it once with `things config set " +
1418
- "allow-experimental true`."))).action(async (projectRef, sels, opts) => {
1442
+ "order is the resulting order, and each heading's to-dos follow it. Pass exactly " +
1443
+ "one placement: --first, --last, --before-heading <sel>, or --after-heading <sel>. " +
1444
+ `Each <heading> is a ${HEADING_SEL_HELP}. GUI-ONLY: Things offers heading order as ` +
1445
+ "keyboard shortcuts on a selected heading row and nowhere else, so this drives the " +
1446
+ "app — enable it once with `things config set ui-enabled true` and pass " +
1447
+ "--dangerously-drive-gui. It moves one heading one slot at a time, re-reading the " +
1448
+ "order after each step. Completed and canceled headings are not shown in the " +
1449
+ "project view, so they are skipped over and keep their place in the list; naming " +
1450
+ "one as a heading to move or as an anchor is refused.")))).action(async (projectRef, sels, opts) => {
1419
1451
  if (countPlacementFlags(opts) !== 1) {
1420
1452
  usageError(opts, "pass exactly one of --first / --last / --before-heading / --after-heading");
1421
1453
  return;
@@ -1436,8 +1468,9 @@ export function registerWriteCommands(program) {
1436
1468
  "destination. GUI-ONLY: it drives the heading row's ellipsis Move… menu, so enable it " +
1437
1469
  "once with `things config set ui.enabled true` and pass --dangerously-drive-gui. Fails " +
1438
1470
  "closed when the heading title is shared by another heading in the project, or the " +
1439
- "destination title by another project (the drive addresses both by title). No undo " +
1440
- "move it back to reverse."))).action(async (projectRef, sel, opts) => {
1471
+ "destination title by another project (both are addressed by title), and when the " +
1472
+ "destination project is completed or canceled — reopen it first. No undo move it " +
1473
+ "back to reverse."))).action(async (projectRef, sel, opts) => {
1441
1474
  await runWrite(opts, (c) => {
1442
1475
  const proj = c.resolve.project(projectRef);
1443
1476
  const dest = c.resolve.project(String(opts["to"]));
@@ -1492,31 +1525,34 @@ export function registerWriteCommands(program) {
1492
1525
  await runWrite(opts, (c) => c.write.run(op, { uuid }, writeOptionsFrom(opts)));
1493
1526
  });
1494
1527
  }
1495
- addDriveGuiFlag(addRepeatRuleFlags(addCompoundWriteFlags(project
1528
+ addDriveGuiFlag(addRepeatRuleFlags(addWriteFlags(project
1496
1529
  .command("make-repeating <ref>")
1497
1530
  .description("Turn a project into a repeating one. A disposable copy is promoted and the ORIGINAL is " +
1498
1531
  "moved to the Trash — so `things undo` reverses it (it removes the new series and " +
1499
1532
  "restores the original). Refuses a project that holds a nested repeating template. An " +
1500
1533
  "Anytime project with no area is moved to Someday first (a cleanup-free intermediate " +
1501
- "step, shown in --dry-run). Set the rule with the flags below; see `things help repeating`.")
1534
+ "step, shown in --dry-run). Set the rule with the flags below; see `things help " +
1535
+ "repeating`. Each re-run makes ANOTHER series, so pass --op-id on a retry: a " +
1536
+ "resubmission with the same key replays the first result instead of making a second " +
1537
+ "series.")
1502
1538
  .requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
1503
1539
  .requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (uuid, opts) => {
1504
1540
  const frequency = opts["frequency"];
1505
- if (opIdCompoundRefused(opts, "project make-repeating"))
1506
- return;
1507
1541
  await runWrite(opts, (c) => c.write.makeRepeatingProject(uuid, {
1508
1542
  frequency,
1509
1543
  interval: Number(opts["interval"]),
1510
1544
  ...repeatRuleFlagsFromOpts(opts, frequency),
1511
1545
  }, writeOptionsFrom(opts)));
1512
1546
  });
1513
- addDriveGuiFlag(addRepeatCalendarFlags(addCompoundWriteFlags(project
1547
+ addDriveGuiFlag(addRepeatCalendarFlags(addWriteFlags(project
1514
1548
  .command("add-repeating <title>")
1515
1549
  .description("Create a project and turn it into a repeating series in ONE call. Two legs: the project " +
1516
1550
  "is created (notes/area/when/deadline/child to-dos) and PERSISTS even if the promote " +
1517
1551
  "refuses; then it is promoted (which drives the GUI). Give --area to place it, or omit " +
1518
1552
  "it to create in Someday. The new repeating project's uuid is printed; `things undo` " +
1519
- "removes the created series (trash-both).")
1553
+ "removes the created series (trash-both). Each re-run makes ANOTHER series, so pass " +
1554
+ "--op-id on a retry: a resubmission with the same key replays the first result " +
1555
+ "instead of making a second series.")
1520
1556
  .option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
1521
1557
  .option("--area <ref>", "destination area (uuid or unique name)")
1522
1558
  .option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
@@ -1529,8 +1565,6 @@ export function registerWriteCommands(program) {
1529
1565
  const todos = opts["todo"];
1530
1566
  const area = containerRef(opts["area"]);
1531
1567
  const frequency = opts["frequency"];
1532
- if (opIdCompoundRefused(opts, "project add-repeating"))
1533
- return;
1534
1568
  if (!(await resolveNotesStdin(opts)))
1535
1569
  return;
1536
1570
  await runWrite(opts, (c) => c.write.addRepeatingProject({
@@ -1543,12 +1577,14 @@ export function registerWriteCommands(program) {
1543
1577
  ...addRepeatingRuleFieldsFromOpts(opts, frequency, Number(opts["interval"])),
1544
1578
  }, writeOptionsFrom(opts)));
1545
1579
  });
1546
- addDriveGuiFlag(addRepeatCalendarFlags(addCompoundWriteFlags(todo
1580
+ addDriveGuiFlag(addRepeatCalendarFlags(addWriteFlags(todo
1547
1581
  .command("add-repeating <title>")
1548
1582
  .description("Create a to-do and turn it into a repeating series in ONE call. Two legs: the to-do is " +
1549
1583
  "created (notes/tags/when/deadline/reminder/checklist/--created-at) and PERSISTS even " +
1550
1584
  "if the promote refuses; then it is promoted (which drives the GUI). The new repeating " +
1551
- "template's uuid is printed; `things undo` removes the created series (trash-both).")
1585
+ "template's uuid is printed; `things undo` removes the created series (trash-both). " +
1586
+ "Each re-run makes ANOTHER series, so pass --op-id on a retry: a resubmission with " +
1587
+ "the same key replays the first result instead of making a second series.")
1552
1588
  .option("--notes <text>", `notes body — ${NOTES_FORMAT} (${NOTES_INPUT})`)
1553
1589
  .option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
1554
1590
  .option("--reminder <time>", "HH:mm reminder (needs a schedulable --when)")
@@ -1563,7 +1599,7 @@ export function registerWriteCommands(program) {
1563
1599
  .option("--project <ref>", "destination project (uuid or unique name)")
1564
1600
  .option("--area <ref>", "destination area (uuid or unique name)")
1565
1601
  .option("--heading <title>", "existing heading inside the target project")
1566
- .option("--created-at <iso>", "born with this creation timestamp (ISO date or datetime; a date is noon in the effective zone)")
1602
+ .option("--created-at <iso>", `born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`)
1567
1603
  .requiredOption("--frequency <freq>", REPEAT_FREQ_HELP)
1568
1604
  .requiredOption("--interval <n>", REPEAT_INTERVAL_HELP)))).action(async (title, opts) => {
1569
1605
  const tags = opts["tag"];
@@ -1571,8 +1607,6 @@ export function registerWriteCommands(program) {
1571
1607
  const projectRef = containerRef(opts["project"]);
1572
1608
  const area = containerRef(opts["area"]);
1573
1609
  const frequency = opts["frequency"];
1574
- if (opIdCompoundRefused(opts, "todo add-repeating"))
1575
- return;
1576
1610
  if (!(await resolveNotesStdin(opts)))
1577
1611
  return;
1578
1612
  await runWrite(opts, (c) => c.write.addRepeatingTodo({
@@ -1603,7 +1637,7 @@ export function registerWriteCommands(program) {
1603
1637
  .option("--when <value>", "today | evening | anytime | someday | YYYY-MM-DD")
1604
1638
  .option("--deadline <date>", "YYYY-MM-DD")
1605
1639
  .option("--todo <title>", "initial child to-do, repeatable (seeds the new project)", collect, [])
1606
- .option("--created-at <iso>", "born with this creation timestamp (ISO date or datetime; a date is noon in the effective zone)")
1640
+ .option("--created-at <iso>", `born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`)
1607
1641
  .option("--completed-at <iso>", "born completed (in the Logbook) with this completion timestamp; cannot seed open child to-dos (§5b)")).action(async (title, opts) => {
1608
1642
  const todos = opts["todo"];
1609
1643
  const area = containerRef(opts["area"]);
@@ -1636,7 +1670,7 @@ export function registerWriteCommands(program) {
1636
1670
  .option("--clear-reminder", "clear the reminder (works while scheduled today|evening)")
1637
1671
  .option("--deadline <date>", "YYYY-MM-DD")
1638
1672
  .option("--clear-deadline", "remove the deadline")
1639
- .option("--created-at <iso>", "rewrite the creation timestamp (ISO date or datetime; a date is noon in the effective zone); status-safe")
1673
+ .option("--created-at <iso>", `rewrite the creation timestamp (${RESOLUTION_DATE_FORMAT}); status-safe`)
1640
1674
  .option("--completed-at <iso>", "rewrite the completion timestamp of an already-resolved project (a canceled one stays canceled); open projects are refused — use complete/cancel")
1641
1675
  .action(async (uuid, opts) => {
1642
1676
  if (!(await resolveNotesStdin(opts)))
@@ -1704,7 +1738,7 @@ export function registerWriteCommands(program) {
1704
1738
  "canceled via the certified flip legs (multi-leg, disclosed); it is refused while the " +
1705
1739
  "project still has open children.")
1706
1740
  .requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-cancel (cancel them too)")
1707
- .option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
1741
+ .option("--completed-at <iso>", `completion timestamp (${RESOLUTION_DATE_FORMAT})`)).action(async (uuid, opts) => {
1708
1742
  await runWrite(opts, (c) => c.write.cancelProject(uuid, {
1709
1743
  children: opts.children,
1710
1744
  ...(opts["completedAt"] !== undefined && {
@@ -1723,10 +1757,10 @@ export function registerWriteCommands(program) {
1723
1757
  if (opIdCompoundRefused(opts, "project reopen"))
1724
1758
  return;
1725
1759
  const started = Date.now();
1726
- const endInvocation = beginWriteInvocation(opts.json === true, started);
1760
+ const { openClient, endInvocation } = beginWriteInvocation(opts.json === true, started);
1727
1761
  let client = null;
1728
1762
  try {
1729
- client = openThings(opts.db ? { dbPath: opts.db } : {});
1763
+ client = openClient(opts.db);
1730
1764
  const outcome = await client.write.reopenProject(uuid, {
1731
1765
  ...writeOptionsFrom(opts),
1732
1766
  ...(opts.restoreChildren === true && { restoreChildren: true }),
@@ -1798,7 +1832,7 @@ export function registerWriteCommands(program) {
1798
1832
  "open to-dos, so an explicit --children policy is required. --completed-at sets the " +
1799
1833
  "completion timestamp and backdates it (multi-leg, disclosed).")
1800
1834
  .requiredOption("--children <policy>", "require-resolved (error if open to-dos remain) | auto-complete (complete them too)")
1801
- .option("--completed-at <iso>", "completion timestamp (ISO date or datetime; a date is noon in the effective zone)")).action(async (uuid, opts) => {
1835
+ .option("--completed-at <iso>", `completion timestamp (${RESOLUTION_DATE_FORMAT})`)).action(async (uuid, opts) => {
1802
1836
  await runWrite(opts, (c) => c.write.completeProject(uuid, {
1803
1837
  children: opts.children,
1804
1838
  ...(opts["completedAt"] !== undefined && {
@@ -2048,10 +2082,10 @@ export function registerWriteCommands(program) {
2048
2082
  }
2049
2083
  // batch streams JSONL regardless of --json, so the interrupt guard is
2050
2084
  // armed machine-readable.
2051
- const endInvocation = beginWriteInvocation(true, Date.now());
2085
+ const { openClient, endInvocation } = beginWriteInvocation(true, Date.now());
2052
2086
  let client = null;
2053
2087
  try {
2054
- client = openThings(opts.db ? { dbPath: opts.db } : {});
2088
+ client = openClient(opts.db);
2055
2089
  const batchResult = await client.write.batch(ops, {
2056
2090
  ...(opts.dryRun !== undefined && { dryRun: opts.dryRun }),
2057
2091
  ...(opts["continueOnError"] === true && { continueOnError: true }),
@@ -2139,10 +2173,10 @@ export function registerWriteCommands(program) {
2139
2173
  }
2140
2174
  // undo streams JSONL regardless of --json, so the interrupt guard is armed
2141
2175
  // machine-readable (each inverse is a real write, GUI drives included).
2142
- const endInvocation = beginWriteInvocation(true, Date.now());
2176
+ const { openClient, endInvocation } = beginWriteInvocation(true, Date.now());
2143
2177
  let client = null;
2144
2178
  try {
2145
- client = openThings(opts.db ? { dbPath: opts.db } : {});
2179
+ client = openClient(opts.db);
2146
2180
  const items = await client.write.undo({
2147
2181
  ...(opts["last"] !== undefined && { last: Number(opts["last"]) }),
2148
2182
  ...(opts["by"] !== undefined && { by: String(opts["by"]) }),
@@ -2211,8 +2245,9 @@ export function registerWriteCommands(program) {
2211
2245
  "EARLIEST one's current slot; --start/--end/--before/--after position the block. A " +
2212
2246
  "Today/Evening member also has an index slot in its container, so a set sharing BOTH " +
2213
2247
  "axes is ambiguous — pass --in to say which. HEADINGS: same-project heading " +
2214
- "re-ranking runs the native heading-block wire; an archived heading is reorderable " +
2215
- "but repositioning it brings it back to open (disclosed). AREAS: this drives the " +
2248
+ "re-ranking drives the local Things app (keyboard shortcuts on the heading row) " +
2249
+ "and covers the open headings only a completed or canceled heading is not shown " +
2250
+ "in the project view, so it is skipped over and cannot be moved. AREAS: this drives the " +
2216
2251
  "local Things app (sidebar drag). `things area reorder` and `things project " +
2217
2252
  "move-heading` remain as kind-specific spellings.")
2218
2253
  .option("--in <target>", "to-dos/projects only — name the axis to reorder on: today | evening | anytime | " +