things-api 0.13.0 → 0.14.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.
- package/README.md +2 -2
- package/dist/cli/commands/area.d.ts +5 -5
- package/dist/cli/commands/area.js +12 -7
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/project.js +1 -1
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +21 -12
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.js +6 -2
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +116 -70
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/glyphs.d.ts +9 -2
- package/dist/cli/glyphs.js +11 -5
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/read-driver.d.ts +39 -9
- package/dist/cli/read-driver.js +37 -16
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +25 -26
- package/dist/cli/render.js +57 -56
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +92 -35
- package/dist/client.js +61 -18
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +23 -33
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +158 -144
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/dates.d.ts +26 -0
- package/dist/model/dates.js +46 -4
- package/dist/model/dates.js.map +1 -1
- package/dist/read/area-filter.d.ts +1 -1
- package/dist/read/area-filter.js +5 -4
- package/dist/read/area-filter.js.map +1 -1
- package/dist/read/area-view.d.ts +0 -11
- package/dist/read/area-view.js +1 -5
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/project-view.d.ts +34 -19
- package/dist/read/project-view.js +26 -20
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/scope.d.ts +1 -1
- package/dist/read/scope.js +5 -4
- package/dist/read/scope.js.map +1 -1
- package/dist/read/shape.d.ts +73 -14
- package/dist/read/shape.js +459 -151
- package/dist/read/shape.js.map +1 -1
- package/dist/read/truncation.d.ts +75 -2
- package/dist/read/truncation.js +69 -46
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +19 -18
- package/dist/read/views.js +5 -6
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +6 -0
- package/dist/surface-copy.js +6 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.js +0 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +25 -1
- package/dist/write/commands.js +256 -121
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.js +27 -11
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.d.ts +26 -5
- package/dist/write/move.js +313 -2
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +49 -19
- package/dist/write/operations.js +2 -2
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.js +25 -4
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +48 -28
- package/dist/write/pre-state.js +134 -13
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +3 -3
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +34 -0
- package/dist/write/resolution-timestamps.js +286 -0
- package/dist/write/resolution-timestamps.js.map +1 -0
- package/dist/write/reversibility.js +5 -5
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.js +0 -5
- package/dist/write/scope-guard.js.map +1 -1
- package/dist/write/undo.d.ts +0 -6
- package/dist/write/undo.js +44 -4
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +14 -4
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +0 -8
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +16 -2
- package/dist/write/verify/delta.js +21 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +2 -78
- package/skills/things-cli/SKILL.md +7 -5
- package/skills/things-cli/references/contracts.md +3 -4
- package/skills/things-cli/references/model.md +8 -8
- package/skills/things-cli/references/ordering.md +16 -7
package/dist/mcp/server.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
-
import { ALL_DESC, AREA_FILTER_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode, capabilitiesTable, DATE_FORMAT, DEFAULT_LIST_LIMIT, diagnose, FILTER_CONTRACT, FULL_DESC, hasTagPresence, isLooseRef, isValidTimeZone, LIMIT_DESC, looseShadowNotice, MCP_WHEN_LABELS, mutationWireData, noUuidMatch, omitEmpty, OMIT_EMPTY_NOTE, OP_ID_RE, OPERATION_KINDS, openThings, PKG_VERSION, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, REF_FORMAT, REF_RULE_NOTE, ReferenceResolutionError, REMINDER_FORMAT, schemaWarnings, shapeReadPayload, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
19
|
+
import { ALL_DESC, AREA_FILTER_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode, capabilitiesTable, DATE_FORMAT, DEFAULT_LIST_LIMIT, diagnose, FILTER_CONTRACT, FULL_DESC, hasTagPresence, isLooseRef, isValidTimeZone, LIMIT_DESC, looseShadowNotice, MCP_WHEN_LABELS, mutationWireData, noUuidMatch, omitEmpty, OMIT_EMPTY_NOTE, OP_ID_RE, OPERATION_KINDS, openThings, PKG_VERSION, PROJECT_LIMIT_DESC, PROJECT_PREVIEW_LIMIT, REF_FORMAT, REF_RULE_NOTE, ReferenceResolutionError, REMINDER_FORMAT, RESOLUTION_DATE_FORMAT, schemaWarnings, shapeReadPayload, withTodayBucketTotals, withAreaBucketTotals, withSectionTotals, withUpcomingBlockTotals, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
20
20
|
function jsonResult(data) {
|
|
21
21
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
22
22
|
}
|
|
@@ -52,10 +52,11 @@ function truncatedResult(data, truncation, meta) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
* Grouped read result (anytime/someday): the per-block-truncated sections
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* agent can read.
|
|
55
|
+
* Grouped read result (anytime/someday): the per-block-truncated sections (each
|
|
56
|
+
* capped section carrying its inline `total`, R1) plus a second block carrying
|
|
57
|
+
* the WHOLE-VIEW {@link Truncation} rollup and, when anything was hidden, a
|
|
58
|
+
* one-line note the agent can read. The pre-v2 `blocks[]` sidecar is retired —
|
|
59
|
+
* completeness rides each section's inline `total` (doctrine v2 PR 5).
|
|
59
60
|
*/
|
|
60
61
|
function groupedResult(data, truncation) {
|
|
61
62
|
const note = truncation.truncated
|
|
@@ -238,6 +239,26 @@ function movePositionArgs(args) {
|
|
|
238
239
|
return { after: args.after };
|
|
239
240
|
return undefined;
|
|
240
241
|
}
|
|
242
|
+
/** The universal-reorder anchor grammar (start/end/before/after → MovePosition). */
|
|
243
|
+
function reorderPositionArgs(args) {
|
|
244
|
+
const chosen = [
|
|
245
|
+
args.start === true,
|
|
246
|
+
args.end === true,
|
|
247
|
+
args.before !== undefined,
|
|
248
|
+
args.after !== undefined,
|
|
249
|
+
].filter(Boolean).length;
|
|
250
|
+
if (chosen > 1)
|
|
251
|
+
return "conflict";
|
|
252
|
+
if (args.start === true)
|
|
253
|
+
return { at: "first" };
|
|
254
|
+
if (args.end === true)
|
|
255
|
+
return { at: "last" };
|
|
256
|
+
if (args.before !== undefined)
|
|
257
|
+
return { before: args.before };
|
|
258
|
+
if (args.after !== undefined)
|
|
259
|
+
return { after: args.after };
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
241
262
|
/** The shared position input schema for the move tools. */
|
|
242
263
|
const positionShape = {
|
|
243
264
|
first: z.boolean().optional().describe("place the block at the top of its bucket"),
|
|
@@ -392,10 +413,9 @@ function buildInstructions(getClient) {
|
|
|
392
413
|
"confirmation parameter named in their description; refused calls return an error saying " +
|
|
393
414
|
"what to pass.",
|
|
394
415
|
"- Read-result semantics: an item's tags are its direct tags; its effective tags also include " +
|
|
395
|
-
"tags inherited from its containing project and area.
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"findable in Logbook.",
|
|
416
|
+
"tags inherited from its containing project and area. The today and evening markers are " +
|
|
417
|
+
"present only for an item in Today (evening implies today); an unscheduled start=active item " +
|
|
418
|
+
"is in Anytime and carries neither. Completing an item makes it findable in Logbook.",
|
|
399
419
|
"- For capped reads, pass limit to cap rows or all: true for everything; if both are set, all wins.",
|
|
400
420
|
`- Read results are compact: ${OMIT_EMPTY_NOTE}`,
|
|
401
421
|
];
|
|
@@ -577,14 +597,15 @@ export function createThingsMcpServer(options = {}) {
|
|
|
577
597
|
};
|
|
578
598
|
// ------------------------------------------------------------------ reads
|
|
579
599
|
server.registerTool("read_view", {
|
|
580
|
-
description: "Read a Things list as the app presents it: today (
|
|
581
|
-
"
|
|
600
|
+
description: "Read a Things list as the app presents it: today (two children buckets — " +
|
|
601
|
+
"children.today and children.evening, evening expires daily; the whole-view " +
|
|
602
|
+
"count due/overdue vs. other rides the result's second block), inbox, anytime, " +
|
|
582
603
|
"upcoming, someday, logbook, or trash. For upcoming, " +
|
|
583
604
|
"horizon > 1 also includes future occurrences of repeating items (up to 10 each). " +
|
|
584
605
|
"anytime/someday return sections in canonical order (area + items; null area = the " +
|
|
585
606
|
"top-level block); children of someday/future-scheduled projects are excluded " +
|
|
586
607
|
"from anytime — the project row represents them; someday lists each group's " +
|
|
587
|
-
"project rows before its to-dos.
|
|
608
|
+
"project rows before its to-dos. Row-capped views (today/inbox/upcoming/logbook/trash) " +
|
|
588
609
|
`return at most ${DEFAULT_LIST_LIMIT} items by default (raise with limit); ` +
|
|
589
610
|
"anytime/someday always return every group and cap per block instead — " +
|
|
590
611
|
`area_limit (default ${AREA_PREVIEW_LIMIT}) per area block, and on anytime ` +
|
|
@@ -601,10 +622,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
601
622
|
.string()
|
|
602
623
|
.optional()
|
|
603
624
|
.describe(`today/anytime/someday/upcoming/logbook only: ${AREA_FILTER_DESC}`),
|
|
604
|
-
evening: z
|
|
605
|
-
.boolean()
|
|
606
|
-
.optional()
|
|
607
|
-
.describe("today only: show only This-Evening members (when = evening)"),
|
|
625
|
+
evening: z.boolean().optional().describe("today only: show only the This Evening section"),
|
|
608
626
|
show_active_project_items: z
|
|
609
627
|
.union([z.boolean(), z.number().int().min(1)])
|
|
610
628
|
.optional()
|
|
@@ -707,7 +725,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
707
725
|
const area = args.area !== undefined ? { area: args.area } : {};
|
|
708
726
|
switch (args.view) {
|
|
709
727
|
case "today": {
|
|
710
|
-
const { view, truncation, filter: fm, } = c.read.today({
|
|
728
|
+
const { view, truncation, totals, filter: fm, } = c.read.today({
|
|
711
729
|
...filter,
|
|
712
730
|
...zone,
|
|
713
731
|
...area,
|
|
@@ -715,14 +733,17 @@ export function createThingsMcpServer(options = {}) {
|
|
|
715
733
|
limit,
|
|
716
734
|
});
|
|
717
735
|
filterMeta = fm;
|
|
718
|
-
|
|
736
|
+
// The data block is the two `children` bucket records (each with its
|
|
737
|
+
// inline `total` when capped); the whole-view `counts` aggregate
|
|
738
|
+
// rides the metadata block (the CLI meta.counts analog).
|
|
739
|
+
return truncatedResult(withTodayBucketTotals(shapeReadPayload("today", view, full, c.refPromoter()), totals), truncation, { counts: view.counts });
|
|
719
740
|
}
|
|
720
741
|
case "inbox": {
|
|
721
742
|
const { items, truncation } = c.read.inbox({ ...filter, ...zone, limit });
|
|
722
743
|
return truncatedResult(shapeReadPayload("inbox", items, full, c.refPromoter()), truncation);
|
|
723
744
|
}
|
|
724
745
|
case "anytime": {
|
|
725
|
-
const { view, truncation, filter: fm, } = c.read.anytime({
|
|
746
|
+
const { view, truncation, sectionTotals, filter: fm, } = c.read.anytime({
|
|
726
747
|
...filter,
|
|
727
748
|
...zone,
|
|
728
749
|
...area,
|
|
@@ -730,10 +751,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
730
751
|
projectLimit,
|
|
731
752
|
});
|
|
732
753
|
filterMeta = fm;
|
|
733
|
-
return groupedResult(shapeReadPayload("anytime", view, full, c.refPromoter()), truncation);
|
|
754
|
+
return groupedResult(withSectionTotals(shapeReadPayload("anytime", view, full, c.refPromoter()), sectionTotals), truncation);
|
|
734
755
|
}
|
|
735
756
|
case "upcoming": {
|
|
736
|
-
const { items, truncation, filter: fm, } = c.read.upcoming({
|
|
757
|
+
const { items, truncation, upcomingTotals, filter: fm, } = c.read.upcoming({
|
|
737
758
|
...filter,
|
|
738
759
|
...zone,
|
|
739
760
|
...area,
|
|
@@ -741,14 +762,16 @@ export function createThingsMcpServer(options = {}) {
|
|
|
741
762
|
limit,
|
|
742
763
|
});
|
|
743
764
|
filterMeta = fm;
|
|
744
|
-
|
|
765
|
+
// The data block is the day-block `sections` array, each capped block
|
|
766
|
+
// carrying its inline `total` (R1); the flat rollup rides truncation.
|
|
767
|
+
return truncatedResult(withUpcomingBlockTotals(shapeReadPayload("upcoming", items, full, c.refPromoter()), upcomingTotals), truncation);
|
|
745
768
|
}
|
|
746
769
|
case "someday": {
|
|
747
770
|
const active = showActiveProjectItems;
|
|
748
771
|
if (typeof active === "number" && args.all === true) {
|
|
749
772
|
return usage("pass at most one of a numeric show_active_project_items / all");
|
|
750
773
|
}
|
|
751
|
-
const { view, truncation, filter: fm, } = c.read.someday({
|
|
774
|
+
const { view, truncation, sectionTotals, filter: fm, } = c.read.someday({
|
|
752
775
|
...filter,
|
|
753
776
|
...zone,
|
|
754
777
|
...area,
|
|
@@ -760,7 +783,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
760
783
|
projectLimit: typeof active === "number" ? active : null,
|
|
761
784
|
});
|
|
762
785
|
filterMeta = fm;
|
|
763
|
-
return groupedResult(shapeReadPayload("someday", view, full, c.refPromoter()), truncation);
|
|
786
|
+
return groupedResult(withSectionTotals(shapeReadPayload("someday", view, full, c.refPromoter()), sectionTotals), truncation);
|
|
764
787
|
}
|
|
765
788
|
case "logbook": {
|
|
766
789
|
const { items, truncation, filter: fm, } = c.read.logbook({
|
|
@@ -888,13 +911,11 @@ export function createThingsMcpServer(options = {}) {
|
|
|
888
911
|
: readResult(shapeReadPayload("detail", item, false, getClient().refPromoter()));
|
|
889
912
|
}));
|
|
890
913
|
server.registerTool("get_project", {
|
|
891
|
-
description: "One project's full contents: metadata
|
|
892
|
-
"
|
|
893
|
-
"
|
|
894
|
-
"
|
|
895
|
-
"
|
|
896
|
-
"items (or logbook) on the heading ref. The tag filters keep only the child to-dos " +
|
|
897
|
-
"matching by their own tags (a heading left with none is dropped). " +
|
|
914
|
+
description: "One project's full contents: metadata plus its to-dos grouped under their headings. " +
|
|
915
|
+
"Open headings (and archived ones not yet swept to the logbook) stay live, carrying " +
|
|
916
|
+
"archived once archived; a swept archived heading moves into the logged region as a " +
|
|
917
|
+
"group with its children. The tag filters keep only the child to-dos matching by their " +
|
|
918
|
+
"own tags (a heading left with none is dropped). " +
|
|
898
919
|
OMIT_EMPTY_NOTE +
|
|
899
920
|
" " +
|
|
900
921
|
REF_RULE_NOTE,
|
|
@@ -922,11 +943,11 @@ export function createThingsMcpServer(options = {}) {
|
|
|
922
943
|
}), args.full === true, getClient().refPromoter()));
|
|
923
944
|
}, args.tz));
|
|
924
945
|
server.registerTool("get_area", {
|
|
925
|
-
description: "One area's contents: metadata plus its direct to-dos
|
|
926
|
-
"
|
|
927
|
-
"
|
|
928
|
-
"
|
|
929
|
-
`The project
|
|
946
|
+
description: "One area's contents: metadata plus its direct to-dos (active first), its " +
|
|
947
|
+
"projects in canonical order, and later (scheduled/repeating/someday). The area " +
|
|
948
|
+
"logbook is not returned here — read it with read_view logbook + area; trashed " +
|
|
949
|
+
"rows live in read_view trash. " +
|
|
950
|
+
`The project-rows and direct-to-dos sections are capped at ${AREA_PREVIEW_LIMIT} each ` +
|
|
930
951
|
"by default (project_limit / area_limit adjust them; all: true lifts both); the " +
|
|
931
952
|
"second result block reports the counts. " +
|
|
932
953
|
OMIT_EMPTY_NOTE +
|
|
@@ -972,7 +993,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
972
993
|
if (areaLimit === "conflict" || projectLimit === "conflict") {
|
|
973
994
|
return usage("pass at most one of area_limit/project_limit / all");
|
|
974
995
|
}
|
|
975
|
-
const { view, truncation, notice } = getClient().read.areaView(args.ref, {
|
|
996
|
+
const { view, truncation, totals, notice } = getClient().read.areaView(args.ref, {
|
|
976
997
|
overdue: args.overdue === true,
|
|
977
998
|
...tagFilterFields(tagPresence(args)),
|
|
978
999
|
...(args.tz !== undefined && { zone: args.tz }),
|
|
@@ -980,7 +1001,9 @@ export function createThingsMcpServer(options = {}) {
|
|
|
980
1001
|
projectLimit,
|
|
981
1002
|
});
|
|
982
1003
|
areaNotice = notice;
|
|
983
|
-
|
|
1004
|
+
// Each capped scope's completeness rides its inline `total` (R1, PR 3);
|
|
1005
|
+
// the whole-view rollup rides `truncation` (no `blocks[]` sidecar).
|
|
1006
|
+
return groupedResult(withAreaBucketTotals(shapeReadPayload("area-view", view, args.full === true, getClient().refPromoter()), totals), truncation);
|
|
984
1007
|
}, args.tz, undefined, () => (areaNotice !== undefined ? [areaNotice] : undefined));
|
|
985
1008
|
});
|
|
986
1009
|
server.registerTool("list_collections", {
|
|
@@ -1059,6 +1082,15 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1059
1082
|
.boolean()
|
|
1060
1083
|
.optional()
|
|
1061
1084
|
.describe("Confirm adding into a completed/canceled project (this reopens it)"),
|
|
1085
|
+
created_at: z
|
|
1086
|
+
.string()
|
|
1087
|
+
.optional()
|
|
1088
|
+
.describe(`Born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`),
|
|
1089
|
+
completed_at: z
|
|
1090
|
+
.string()
|
|
1091
|
+
.optional()
|
|
1092
|
+
.describe(`Born completed, in the Logbook, with this completion timestamp ` +
|
|
1093
|
+
`(${RESOLUTION_DATE_FORMAT}); drop when/reminder`),
|
|
1062
1094
|
...createTagsShape,
|
|
1063
1095
|
...tzShape,
|
|
1064
1096
|
...dryRunShape,
|
|
@@ -1085,6 +1117,8 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1085
1117
|
...(args.project !== undefined && { project: containerRef(args.project) }),
|
|
1086
1118
|
...(args.area !== undefined && { area: containerRef(args.area) }),
|
|
1087
1119
|
...(args.heading !== undefined && { heading: args.heading }),
|
|
1120
|
+
...(args.created_at !== undefined && { createdAt: args.created_at }),
|
|
1121
|
+
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1088
1122
|
}, writeOptions(args)));
|
|
1089
1123
|
}));
|
|
1090
1124
|
server.registerTool("update", {
|
|
@@ -1094,7 +1128,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1094
1128
|
"deadline/clear_deadline; changing the schedule keeps an existing reminder unless a new " +
|
|
1095
1129
|
"one is set, schedule and deadline changes are unavailable for repeating items, and " +
|
|
1096
1130
|
"clear_reminder needs the item scheduled for today or this evening (a reminder on a " +
|
|
1097
|
-
"future date can only be changed, not cleared).
|
|
1131
|
+
"future date can only be changed, not cleared). created_at/completed_at rewrite the " +
|
|
1132
|
+
"resolution timestamps of an already-resolved item; created_at is status-safe, but " +
|
|
1133
|
+
"completed_at on an open item is refused — resolving it is set_status's boundary, not " +
|
|
1134
|
+
"update's. kind area: title and/or tags (the full " +
|
|
1098
1135
|
"replacement set). kind tag: title, parent (nest under it) or unnest (to the top level; " +
|
|
1099
1136
|
"exclusive), and shortcut or clear_shortcut (exclusive). Tags must exist unless " +
|
|
1100
1137
|
"create_tags is set.",
|
|
@@ -1113,6 +1150,16 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1113
1150
|
clear_reminder: z.boolean().optional().describe("todo/project: remove the reminder"),
|
|
1114
1151
|
deadline: z.string().optional().describe(`todo/project: ${DATE_FORMAT}`),
|
|
1115
1152
|
clear_deadline: z.boolean().optional().describe("todo/project: remove the deadline"),
|
|
1153
|
+
created_at: z
|
|
1154
|
+
.string()
|
|
1155
|
+
.optional()
|
|
1156
|
+
.describe(`todo/project: rewrite the creation timestamp (${RESOLUTION_DATE_FORMAT}); status-safe`),
|
|
1157
|
+
completed_at: z
|
|
1158
|
+
.string()
|
|
1159
|
+
.optional()
|
|
1160
|
+
.describe("todo/project: rewrite the completion timestamp of an already-resolved item " +
|
|
1161
|
+
`(${RESOLUTION_DATE_FORMAT}; a canceled one stays canceled); an open item is ` +
|
|
1162
|
+
"refused — use set_status to resolve it"),
|
|
1116
1163
|
tags: z
|
|
1117
1164
|
.array(z.string())
|
|
1118
1165
|
.optional()
|
|
@@ -1160,6 +1207,8 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1160
1207
|
...(args.clear_reminder === true && { reminder: null }),
|
|
1161
1208
|
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1162
1209
|
...(args.clear_deadline === true && { deadline: null }),
|
|
1210
|
+
...(args.created_at !== undefined && { createdAt: args.created_at }),
|
|
1211
|
+
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1163
1212
|
}, opts));
|
|
1164
1213
|
}
|
|
1165
1214
|
return mutationResult(await c.write.updateProject(args.uuid, {
|
|
@@ -1172,6 +1221,8 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1172
1221
|
...(args.clear_reminder === true && { reminder: null }),
|
|
1173
1222
|
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1174
1223
|
...(args.clear_deadline === true && { deadline: null }),
|
|
1224
|
+
...(args.created_at !== undefined && { createdAt: args.created_at }),
|
|
1225
|
+
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1175
1226
|
}, opts));
|
|
1176
1227
|
}
|
|
1177
1228
|
if (args.kind === "area") {
|
|
@@ -1211,7 +1262,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1211
1262
|
"scope project, completing or canceling requires a children policy: 'require-resolved' " +
|
|
1212
1263
|
"errors if open to-dos remain; 'auto-complete'/'auto-cancel' resolves them together with " +
|
|
1213
1264
|
"the project (canceling never alters already-completed children). scope project, status " +
|
|
1214
|
-
"open, restore_children also reopens the to-dos that were resolved with the project."
|
|
1265
|
+
"open, restore_children also reopens the to-dos that were resolved with the project. " +
|
|
1266
|
+
"completed_at (status completed or canceled) sets the completion timestamp — also the " +
|
|
1267
|
+
'"Completed on" stamp for a canceled item — backdating it; reaching a backdated canceled ' +
|
|
1268
|
+
"item is a multi-leg, non-atomic sequence, disclosed in the result and in dry_run.",
|
|
1215
1269
|
inputSchema: {
|
|
1216
1270
|
scope: z.enum(["todo", "project"]),
|
|
1217
1271
|
uuid: z
|
|
@@ -1226,21 +1280,33 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1226
1280
|
.boolean()
|
|
1227
1281
|
.optional()
|
|
1228
1282
|
.describe("scope project, open only: also reopen the to-dos resolved with the project"),
|
|
1283
|
+
completed_at: z
|
|
1284
|
+
.string()
|
|
1285
|
+
.optional()
|
|
1286
|
+
.describe(`status completed or canceled: the completion timestamp (${RESOLUTION_DATE_FORMAT})`),
|
|
1287
|
+
...tzShape,
|
|
1229
1288
|
...dryRunShape,
|
|
1230
1289
|
...opIdShape,
|
|
1231
1290
|
},
|
|
1232
1291
|
annotations: NON_DESTRUCTIVE,
|
|
1233
1292
|
}, async (args) => guard(async () => {
|
|
1293
|
+
const badZone = badTz(args.tz);
|
|
1294
|
+
if (badZone !== null)
|
|
1295
|
+
return badZone;
|
|
1234
1296
|
const c = getClient();
|
|
1235
1297
|
const opts = writeOptions(args);
|
|
1298
|
+
if (args.completed_at !== undefined && args.status === "open") {
|
|
1299
|
+
return usage("completed_at applies only to status 'completed' or 'canceled'");
|
|
1300
|
+
}
|
|
1301
|
+
const resolution = args.completed_at !== undefined ? { completedAt: args.completed_at } : {};
|
|
1236
1302
|
if (args.scope === "todo") {
|
|
1237
1303
|
if (args.children !== undefined || args.restore_children !== undefined) {
|
|
1238
1304
|
return usage("children/restore_children apply only to scope project");
|
|
1239
1305
|
}
|
|
1240
1306
|
return mutationResult(args.status === "completed"
|
|
1241
|
-
? await c.write.completeTodo(args.uuid, opts)
|
|
1307
|
+
? await c.write.completeTodo(args.uuid, resolution, opts)
|
|
1242
1308
|
: args.status === "canceled"
|
|
1243
|
-
? await c.write.cancelTodo(args.uuid, opts)
|
|
1309
|
+
? await c.write.cancelTodo(args.uuid, resolution, opts)
|
|
1244
1310
|
: await c.write.reopenTodo(args.uuid, opts));
|
|
1245
1311
|
}
|
|
1246
1312
|
// scope project
|
|
@@ -1251,13 +1317,13 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1251
1317
|
if (args.children !== "require-resolved" && args.children !== "auto-complete") {
|
|
1252
1318
|
return usage("status 'completed' requires children: 'require-resolved' or 'auto-complete'");
|
|
1253
1319
|
}
|
|
1254
|
-
return mutationResult(await c.write.completeProject(args.uuid, { children: args.children }, opts));
|
|
1320
|
+
return mutationResult(await c.write.completeProject(args.uuid, { children: args.children, ...resolution }, opts));
|
|
1255
1321
|
}
|
|
1256
1322
|
if (args.status === "canceled") {
|
|
1257
1323
|
if (args.children !== "require-resolved" && args.children !== "auto-cancel") {
|
|
1258
1324
|
return usage("status 'canceled' requires children: 'require-resolved' or 'auto-cancel'");
|
|
1259
1325
|
}
|
|
1260
|
-
return mutationResult(await c.write.cancelProject(args.uuid, { children: args.children }, opts));
|
|
1326
|
+
return mutationResult(await c.write.cancelProject(args.uuid, { children: args.children, ...resolution }, opts));
|
|
1261
1327
|
}
|
|
1262
1328
|
if (args.children !== undefined) {
|
|
1263
1329
|
return usage("children applies only to status 'completed' or 'canceled'");
|
|
@@ -1521,41 +1587,12 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1521
1587
|
? await c.write.restoreTodo(args.uuid, writeOptions(args))
|
|
1522
1588
|
: await c.write.restoreProject(args.uuid, writeOptions(args)));
|
|
1523
1589
|
}));
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
completion_date: z.string().optional().describe(DATE_FORMAT),
|
|
1531
|
-
creation_date: z.string().optional().describe(DATE_FORMAT),
|
|
1532
|
-
...dryRunShape,
|
|
1533
|
-
...opIdShape,
|
|
1534
|
-
},
|
|
1535
|
-
annotations: NON_DESTRUCTIVE,
|
|
1536
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.backdateTodo(args.uuid, {
|
|
1537
|
-
...(args.completion_date !== undefined && { completionDate: args.completion_date }),
|
|
1538
|
-
...(args.creation_date !== undefined && { creationDate: args.creation_date }),
|
|
1539
|
-
}, writeOptions(args)))));
|
|
1540
|
-
server.registerTool("add_logged_todo", {
|
|
1541
|
-
description: "Create a to-do directly in the Logbook: completed, with the given past " +
|
|
1542
|
-
"completion date (and optionally a past creation date). For importing history " +
|
|
1543
|
-
"from another system.",
|
|
1544
|
-
inputSchema: {
|
|
1545
|
-
title: z.string(),
|
|
1546
|
-
completion_date: z.string().describe(DATE_FORMAT),
|
|
1547
|
-
creation_date: z.string().optional().describe(`${DATE_FORMAT}; <= completion_date`),
|
|
1548
|
-
notes: z.string().optional(),
|
|
1549
|
-
...dryRunShape,
|
|
1550
|
-
...opIdShape,
|
|
1551
|
-
},
|
|
1552
|
-
annotations: NON_DESTRUCTIVE,
|
|
1553
|
-
}, async (args) => guard(async () => mutationResult(await getClient().write.addLoggedTodo({
|
|
1554
|
-
title: args.title,
|
|
1555
|
-
completionDate: args.completion_date,
|
|
1556
|
-
...(args.creation_date !== undefined && { creationDate: args.creation_date }),
|
|
1557
|
-
...(args.notes !== undefined && { notes: args.notes }),
|
|
1558
|
-
}, writeOptions(args)))));
|
|
1590
|
+
// NB: the bespoke backdate_todo / add_logged_todo tools were removed with the
|
|
1591
|
+
// engine ops (plan PR A). MCP parity for the resolution-timestamp surface is
|
|
1592
|
+
// restored (PR B) as created_at/completed_at params folded onto the existing
|
|
1593
|
+
// write tools: add_todo / add_project (Logbook import), update (rewrite an
|
|
1594
|
+
// already-resolved item's timestamps), and set_status (backdate on
|
|
1595
|
+
// complete/cancel) — matching the CLI's --created-at/--completed-at flags.
|
|
1559
1596
|
server.registerTool("heading", {
|
|
1560
1597
|
description: "Manage a project's headings — action selects which; project is always required, and a " +
|
|
1561
1598
|
"heading is selected by its exact title or its uuid (never an ordinal). add_heading: a " +
|
|
@@ -1945,6 +1982,15 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1945
1982
|
when: whenSchema,
|
|
1946
1983
|
deadline: z.string().optional().describe(DATE_FORMAT),
|
|
1947
1984
|
todos: z.array(z.string()).optional().describe("Initial child to-do titles"),
|
|
1985
|
+
created_at: z
|
|
1986
|
+
.string()
|
|
1987
|
+
.optional()
|
|
1988
|
+
.describe(`Born with this creation timestamp (${RESOLUTION_DATE_FORMAT})`),
|
|
1989
|
+
completed_at: z
|
|
1990
|
+
.string()
|
|
1991
|
+
.optional()
|
|
1992
|
+
.describe(`Born completed, in the Logbook, with this completion timestamp ` +
|
|
1993
|
+
`(${RESOLUTION_DATE_FORMAT}); cannot seed open child to-dos`),
|
|
1948
1994
|
...tzShape,
|
|
1949
1995
|
...dryRunShape,
|
|
1950
1996
|
...opIdShape,
|
|
@@ -1961,6 +2007,8 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1961
2007
|
...(args.when !== undefined && { when: args.when }),
|
|
1962
2008
|
...(args.deadline !== undefined && { deadline: args.deadline }),
|
|
1963
2009
|
...(args.todos !== undefined && { todos: args.todos }),
|
|
2010
|
+
...(args.created_at !== undefined && { createdAt: args.created_at }),
|
|
2011
|
+
...(args.completed_at !== undefined && { completedAt: args.completed_at }),
|
|
1964
2012
|
}, writeOptions(args)));
|
|
1965
2013
|
}));
|
|
1966
2014
|
server.registerTool("move_project", {
|
|
@@ -2141,87 +2189,53 @@ export function createThingsMcpServer(options = {}) {
|
|
|
2141
2189
|
};
|
|
2142
2190
|
}));
|
|
2143
2191
|
server.registerTool("reorder", {
|
|
2144
|
-
description: "
|
|
2145
|
-
"
|
|
2146
|
-
"use move_todo / move_project).
|
|
2147
|
-
"
|
|
2148
|
-
"
|
|
2149
|
-
"
|
|
2150
|
-
"
|
|
2151
|
-
"
|
|
2152
|
-
"
|
|
2153
|
-
"
|
|
2154
|
-
"
|
|
2155
|
-
"
|
|
2156
|
-
"
|
|
2157
|
-
"
|
|
2158
|
-
"
|
|
2192
|
+
description: "The ONE reorder tool — rearrange a single-KIND set IN PLACE: to-dos, projects, headings, " +
|
|
2193
|
+
"OR sidebar areas. This REARRANGES, never changes what an item belongs to (to change " +
|
|
2194
|
+
"membership use move_todo / move_project). All refs must be one kind; only to-dos and " +
|
|
2195
|
+
"projects may intermix, and only on the shared Today/Evening/day axes. A mixed-kind set, " +
|
|
2196
|
+
"a cross-container set, and a non-member anchor each get one precise refusal. The refs " +
|
|
2197
|
+
"order is the resulting order; unmentioned siblings keep theirs. Bare (no position) " +
|
|
2198
|
+
"assembles the named items as a block at the EARLIEST one's current slot; start/end/" +
|
|
2199
|
+
"before/after position the block. A Today or This Evening member also holds a slot in its " +
|
|
2200
|
+
"container, so a set coherent on BOTH axes is ambiguous — pass `in` to name the axis. " +
|
|
2201
|
+
"HEADINGS: same-project heading re-ranking runs the native heading-block wire (children " +
|
|
2202
|
+
"follow); an archived heading is reorderable but repositioning it brings it back to open " +
|
|
2203
|
+
"(disclosed in the result). AREAS: this drives the local Things app (sidebar drag) and " +
|
|
2204
|
+
"must be turned on with `things config set ui-enabled true` plus dangerously_drive_gui; " +
|
|
2205
|
+
"the areas' projects and to-dos are untouched. Ordering the Today, Inbox, or Someday " +
|
|
2206
|
+
"lists, or a project's to-dos, must first be enabled once via `things config set " +
|
|
2207
|
+
"allow-experimental true`.",
|
|
2159
2208
|
inputSchema: {
|
|
2160
2209
|
refs: z
|
|
2161
2210
|
.array(z.string())
|
|
2162
|
-
.describe("The items to rearrange
|
|
2163
|
-
|
|
2211
|
+
.describe("The items to rearrange (one kind: to-dos, projects, headings, or areas), in the " +
|
|
2212
|
+
"order they should land (may be a subset)"),
|
|
2213
|
+
start: z.boolean().optional().describe("place the block at the start of its scope"),
|
|
2214
|
+
end: z.boolean().optional().describe("place the block at the end of its scope"),
|
|
2215
|
+
before: z.string().optional().describe("place the block immediately before this item"),
|
|
2216
|
+
after: z.string().optional().describe("place the block immediately after this item"),
|
|
2164
2217
|
in: z
|
|
2165
2218
|
.string()
|
|
2166
2219
|
.optional()
|
|
2167
|
-
.describe("
|
|
2168
|
-
"area/heading ref (uuid or unique title), upcoming (the
|
|
2169
|
-
"shares), or a YYYY-MM-DD day-block. A stage-list or
|
|
2170
|
-
"at a time
|
|
2171
|
-
"
|
|
2172
|
-
|
|
2220
|
+
.describe("to-dos/projects only — name the axis to reorder on: today | evening | anytime | " +
|
|
2221
|
+
"someday | inbox, a project/area/heading ref (uuid or unique title), upcoming (the " +
|
|
2222
|
+
"one future day the set shares), or a YYYY-MM-DD day-block. A stage-list or " +
|
|
2223
|
+
"container axis sorts one KIND at a time; only today | evening | upcoming | a " +
|
|
2224
|
+
"day-block intermix both kinds."),
|
|
2225
|
+
...driveGuiShape,
|
|
2173
2226
|
...dryRunShape,
|
|
2174
2227
|
},
|
|
2175
2228
|
annotations: NON_DESTRUCTIVE,
|
|
2176
2229
|
}, async (args) => guard(async () => {
|
|
2177
|
-
const position =
|
|
2230
|
+
const position = reorderPositionArgs(args);
|
|
2178
2231
|
if (position === "conflict")
|
|
2179
|
-
return usage("pass at most one of
|
|
2232
|
+
return usage("pass at most one of start/end/before/after");
|
|
2180
2233
|
const request = {
|
|
2181
2234
|
uuids: args.refs,
|
|
2182
2235
|
...(position !== undefined && { position }),
|
|
2183
2236
|
...(args.in !== undefined && { in: args.in }),
|
|
2184
2237
|
};
|
|
2185
|
-
return moveResult(await getClient().write.
|
|
2186
|
-
}));
|
|
2187
|
-
server.registerTool("reorder_areas", {
|
|
2188
|
-
description: "Move a sidebar area to a new position in the area order (target by uuid or unique " +
|
|
2189
|
-
"name). Pass exactly one destination: before/after another area, or first/last. This " +
|
|
2190
|
-
"visibly drives the local Things app (the window comes forward and the sidebar may " +
|
|
2191
|
-
"scroll) and must be turned on first with `things config set ui-enabled true`; the " +
|
|
2192
|
-
"area's projects and to-dos are untouched.",
|
|
2193
|
-
inputSchema: {
|
|
2194
|
-
target: z.string().describe(`the area to move (${REF_FORMAT})`),
|
|
2195
|
-
before: z
|
|
2196
|
-
.string()
|
|
2197
|
-
.optional()
|
|
2198
|
-
.describe(`place it immediately above this area (${REF_FORMAT})`),
|
|
2199
|
-
after: z
|
|
2200
|
-
.string()
|
|
2201
|
-
.optional()
|
|
2202
|
-
.describe(`place it immediately below this area (${REF_FORMAT})`),
|
|
2203
|
-
first: z.boolean().optional().describe("move it to the top of the area list"),
|
|
2204
|
-
last: z.boolean().optional().describe("move it to the bottom of the area list"),
|
|
2205
|
-
...driveGuiShape,
|
|
2206
|
-
...dryRunShape,
|
|
2207
|
-
},
|
|
2208
|
-
annotations: NON_DESTRUCTIVE,
|
|
2209
|
-
}, async (args) => guard(async () => {
|
|
2210
|
-
const chosen = [
|
|
2211
|
-
args.before !== undefined,
|
|
2212
|
-
args.after !== undefined,
|
|
2213
|
-
args.first === true,
|
|
2214
|
-
args.last === true,
|
|
2215
|
-
].filter(Boolean).length;
|
|
2216
|
-
if (chosen !== 1)
|
|
2217
|
-
return usage("pass exactly one of before / after / first / last");
|
|
2218
|
-
return mutationResult(await getClient().write.run("area.reorder", {
|
|
2219
|
-
target: args.target,
|
|
2220
|
-
...(args.before !== undefined && { before: args.before }),
|
|
2221
|
-
...(args.after !== undefined && { after: args.after }),
|
|
2222
|
-
...(args.first === true && { position: "first" }),
|
|
2223
|
-
...(args.last === true && { position: "last" }),
|
|
2224
|
-
}, writeOptions(args)));
|
|
2238
|
+
return moveResult(await getClient().write.reorderAny(request, writeOptions(args)));
|
|
2225
2239
|
}));
|
|
2226
2240
|
server.registerTool("undo", {
|
|
2227
2241
|
description: "Undo the last N changes, newest first (changes made directly in the Things app " +
|