things-api 0.11.0 → 0.12.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 +16 -5
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/area.d.ts +18 -11
- package/dist/cli/commands/area.js +55 -67
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -2
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +19 -4
- package/dist/cli/commands/install-skill.js +101 -52
- package/dist/cli/commands/install-skill.js.map +1 -1
- package/dist/cli/commands/mcp.js +9 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +1 -0
- package/dist/cli/commands/project.js +20 -28
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +100 -28
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.js +32 -8
- 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 +769 -143
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +14 -3
- package/dist/cli/did-you-mean.js +26 -3
- package/dist/cli/did-you-mean.js.map +1 -1
- package/dist/cli/glyphs.d.ts +16 -2
- package/dist/cli/glyphs.js +60 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +67 -12
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +3 -2
- package/dist/cli/move-hint.js +2 -2
- package/dist/cli/move-hint.js.map +1 -1
- package/dist/cli/read-driver.d.ts +43 -5
- package/dist/cli/read-driver.js +81 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +19 -8
- package/dist/cli/render.js +59 -25
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +2 -2
- package/dist/cli/resolve-invocation.js +3 -3
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/skill-check.d.ts +12 -6
- package/dist/cli/skill-check.js +31 -14
- package/dist/cli/skill-check.js.map +1 -1
- package/dist/cli/skill.d.ts +8 -0
- package/dist/cli/skill.js +10 -0
- package/dist/cli/skill.js.map +1 -1
- package/dist/cli/verb-hint.js +1 -1
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/client.d.ts +140 -19
- package/dist/client.js +253 -38
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +66 -2
- package/dist/config.js +120 -12
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +176 -28
- package/dist/contracts.js +22 -1
- package/dist/contracts.js.map +1 -1
- package/dist/diagnose.d.ts +34 -2
- package/dist/diagnose.js +36 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +19 -8
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +733 -304
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +46 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +2 -0
- package/dist/model/mappers.js +41 -2
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +8 -1
- package/dist/model/recurrence.js.map +1 -1
- package/dist/read/area-filter.d.ts +76 -0
- package/dist/read/area-filter.js +59 -0
- package/dist/read/area-filter.js.map +1 -0
- package/dist/read/area-view.d.ts +11 -11
- package/dist/read/area-view.js +50 -28
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +15 -8
- package/dist/read/detail.js.map +1 -1
- package/dist/read/predicates.d.ts +18 -0
- package/dist/read/predicates.js +19 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +31 -15
- package/dist/read/project-view.js +72 -39
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/pseudo-area.d.ts +29 -0
- package/dist/read/pseudo-area.js +27 -0
- package/dist/read/pseudo-area.js.map +1 -0
- package/dist/read/queries.d.ts +105 -18
- package/dist/read/queries.js +185 -30
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +126 -0
- package/dist/read/scope.js +162 -0
- package/dist/read/scope.js.map +1 -0
- package/dist/read/search-rank.d.ts +2 -5
- package/dist/read/shape.d.ts +162 -0
- package/dist/read/shape.js +686 -0
- package/dist/read/shape.js.map +1 -0
- package/dist/read/show-target.d.ts +9 -1
- package/dist/read/show-target.js +45 -4
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/stage.d.ts +199 -0
- package/dist/read/stage.js +125 -0
- package/dist/read/stage.js.map +1 -0
- package/dist/read/truncation.d.ts +7 -5
- package/dist/read/truncation.js +33 -8
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +49 -9
- package/dist/read/views.js +193 -40
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +9 -0
- package/dist/surface-copy.js +9 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -71
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.js +266 -55
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +71 -10
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.d.ts +10 -1
- package/dist/write/heading.js +35 -5
- package/dist/write/heading.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +2 -2
- package/dist/write/make-repeating-project.js +9 -9
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.d.ts +130 -0
- package/dist/write/move.js +1533 -0
- package/dist/write/move.js.map +1 -0
- package/dist/write/operations.d.ts +105 -19
- package/dist/write/operations.js +56 -7
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +31 -0
- package/dist/write/opid.js +30 -0
- package/dist/write/opid.js.map +1 -0
- package/dist/write/pipeline.d.ts +70 -3
- package/dist/write/pipeline.js +198 -33
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +162 -8
- package/dist/write/pre-state.js +385 -20
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.d.ts +23 -3
- package/dist/write/reorder.js +1871 -85
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/reversibility.js +19 -7
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.d.ts +31 -0
- package/dist/write/scope-guard.js +162 -0
- package/dist/write/scope-guard.js.map +1 -0
- package/dist/write/undo.js +120 -13
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +16 -7
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +1 -1
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +83 -27
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +11 -0
- package/dist/write/vectors/ui-certification.d.ts +3 -3
- package/dist/write/vectors/ui-certification.js +41 -5
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-drag.d.ts +21 -1
- package/dist/write/vectors/ui-drag.js +138 -27
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +21 -1
- package/dist/write/vectors/ui-recipes.js +166 -7
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +25 -1
- package/dist/write/vectors/ui.js +142 -33
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -1
- package/dist/write/verify/delta.js +106 -29
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +7 -1
- package/schema/envelope.schema.json +383 -0
- package/skills/things-cli/SKILL.md +34 -17
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +30 -9
- package/skills/things-cli/references/errors.md +49 -0
- package/skills/things-cli/references/gui.md +1 -1
- package/skills/things-cli/references/model.md +15 -6
- package/skills/things-cli/references/ordering.md +71 -0
package/dist/mcp/server.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP surface over ThingsClient — the third thin layer (CLI, library, MCP),
|
|
3
|
-
* all consuming the same client.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* all consuming the same client. Read tools return the library's JSON objects
|
|
4
|
+
* verbatim; mutation/move tools frame their result the same way the CLI's wire
|
|
5
|
+
* envelope does (docs/contract.md) — a success drops the internal discriminator
|
|
6
|
+
* (via mutationWireData), a dry-run emits the bare plan, and batch lines flatten
|
|
7
|
+
* to the string-`outcome` JSONL shape. Mutation failures surface as MCP tool
|
|
8
|
+
* errors carrying the machine-readable code + the remediation text the guards
|
|
9
|
+
* produce. Nothing here contains Things logic.
|
|
7
10
|
*
|
|
8
11
|
* Tool descriptions and the server instructions follow the consumer-voice
|
|
9
12
|
* contract in docs/design/surface-copy.md: behavior and side effects only,
|
|
@@ -13,7 +16,7 @@
|
|
|
13
16
|
*/
|
|
14
17
|
import { z } from "zod";
|
|
15
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
-
import { ALL_DESC, AREA_LIMIT_DESC, AREA_PREVIEW_LIMIT, blockedCode,
|
|
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, ReferenceResolutionError, REMINDER_FORMAT, schemaWarnings, shapeReadPayload, splitWhenSugar, tagFilterFields, tagFlagConflict, validateViewArgs, verifyFailedCode, WHEN_VALUES, } from "../index.js";
|
|
17
20
|
function jsonResult(data) {
|
|
18
21
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
19
22
|
}
|
|
@@ -44,17 +47,18 @@ function truncatedResult(data, truncation) {
|
|
|
44
47
|
}
|
|
45
48
|
/**
|
|
46
49
|
* Grouped read result (anytime/someday): the per-block-truncated sections plus
|
|
47
|
-
* a second block carrying the {@link
|
|
48
|
-
* anything was hidden, a one-line note the
|
|
50
|
+
* a second block carrying the unified {@link Truncation} counts (its `blocks`
|
|
51
|
+
* hold the per-block nesting) and, when anything was hidden, a one-line note the
|
|
52
|
+
* agent can read.
|
|
49
53
|
*/
|
|
50
|
-
function groupedResult(data,
|
|
51
|
-
const note =
|
|
54
|
+
function groupedResult(data, truncation) {
|
|
55
|
+
const note = truncation.truncated
|
|
52
56
|
? "some blocks are previews — raise area_limit/project_limit for more per block, or all: true for every item"
|
|
53
57
|
: undefined;
|
|
54
58
|
return {
|
|
55
59
|
content: [
|
|
56
60
|
{ type: "text", text: JSON.stringify(omitEmpty(data)) },
|
|
57
|
-
{ type: "text", text: JSON.stringify({
|
|
61
|
+
{ type: "text", text: JSON.stringify({ truncation, ...(note !== undefined && { note }) }) },
|
|
58
62
|
],
|
|
59
63
|
};
|
|
60
64
|
}
|
|
@@ -112,12 +116,20 @@ function errorResult(error) {
|
|
|
112
116
|
function usage(message) {
|
|
113
117
|
return errorResult({ code: "usage", message });
|
|
114
118
|
}
|
|
115
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Map a mutation outcome to an MCP result (errors carry remediation). Framing
|
|
121
|
+
* mirrors the CLI wire (docs/contract.md, phase-2 alignment): a SUCCESS drops
|
|
122
|
+
* the internal success discriminator via {@link mutationWireData} (call success
|
|
123
|
+
* is the tool result's own not-an-error signal — the payload carries no `kind`),
|
|
124
|
+
* and a dry-run returns the bare plan (the CLI's `mutation-plan` data). Failures
|
|
125
|
+
* stay tool errors carrying the machine-readable code.
|
|
126
|
+
*/
|
|
116
127
|
function mutationResult(result) {
|
|
117
128
|
switch (result.kind) {
|
|
118
129
|
case "ok":
|
|
130
|
+
return jsonResult(mutationWireData(result));
|
|
119
131
|
case "dry-run":
|
|
120
|
-
return jsonResult(result);
|
|
132
|
+
return jsonResult(result.plan);
|
|
121
133
|
case "blocked":
|
|
122
134
|
return errorResult({
|
|
123
135
|
code: blockedCode(result),
|
|
@@ -146,6 +158,89 @@ function mutationResult(result) {
|
|
|
146
158
|
});
|
|
147
159
|
}
|
|
148
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Map a move/reorder orchestrator outcome to an MCP result (spec §4/§7). Framing
|
|
163
|
+
* mirrors {@link mutationResult}: a `move-ok` drops the internal discriminator
|
|
164
|
+
* (the placement-honesty `note` and every `membership`/`placement` field — with
|
|
165
|
+
* their own `touched`/`warnings` disclosures — pass through), and a dry-run
|
|
166
|
+
* returns the bare plan.
|
|
167
|
+
*/
|
|
168
|
+
function moveResult(result) {
|
|
169
|
+
switch (result.kind) {
|
|
170
|
+
case "move-ok":
|
|
171
|
+
return jsonResult(mutationWireData(result));
|
|
172
|
+
case "move-dry-run":
|
|
173
|
+
return jsonResult(result.plan);
|
|
174
|
+
case "move-refused":
|
|
175
|
+
return errorResult({
|
|
176
|
+
code: result.refusal,
|
|
177
|
+
message: result.detail,
|
|
178
|
+
...(result.remediation !== undefined && { remediation: result.remediation }),
|
|
179
|
+
...(result.candidates !== undefined && { details: { candidates: result.candidates } }),
|
|
180
|
+
});
|
|
181
|
+
case "move-leg-failed":
|
|
182
|
+
return errorResult({
|
|
183
|
+
code: "verify-failed",
|
|
184
|
+
message: result.detail,
|
|
185
|
+
remediation: `completed ${result.completed.length} leg(s) before the failure`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Flatten one batch line to the wire shape (docs/contract.md — the JSONL grammar
|
|
191
|
+
* shared with the CLI): the internal `outcome` union object collapses to one
|
|
192
|
+
* level. `outcome` becomes its tag as a plain string (`"ok"`, `"blocked"`,
|
|
193
|
+
* `"dry-run"`, `"already-applied"`, …), and every variant field (uuid, detail,
|
|
194
|
+
* plan, expected, observed, …) sits as a sibling of the line-level keys. The
|
|
195
|
+
* outcome's own `op` duplicates the line-level `op`, so it is dropped — no
|
|
196
|
+
* variant field name collides with a line-level key.
|
|
197
|
+
*/
|
|
198
|
+
function flattenBatchLine(r) {
|
|
199
|
+
const { index, op, outcome, tempId, boundUuid, opId } = r;
|
|
200
|
+
const { kind, op: _outcomeOp, ...variant } = outcome;
|
|
201
|
+
return {
|
|
202
|
+
index,
|
|
203
|
+
op,
|
|
204
|
+
outcome: kind,
|
|
205
|
+
...variant,
|
|
206
|
+
...(tempId !== undefined && { tempId }),
|
|
207
|
+
...(boundUuid !== undefined && { boundUuid }),
|
|
208
|
+
...(opId !== undefined && { opId }),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Build a MovePosition from the shared MCP position args (null when none). */
|
|
212
|
+
function movePositionArgs(args) {
|
|
213
|
+
const chosen = [
|
|
214
|
+
args.first === true,
|
|
215
|
+
args.last === true,
|
|
216
|
+
args.before !== undefined,
|
|
217
|
+
args.after !== undefined,
|
|
218
|
+
].filter(Boolean).length;
|
|
219
|
+
if (chosen > 1)
|
|
220
|
+
return "conflict";
|
|
221
|
+
if (args.first === true)
|
|
222
|
+
return { at: "first" };
|
|
223
|
+
if (args.last === true)
|
|
224
|
+
return { at: "last" };
|
|
225
|
+
if (args.before !== undefined)
|
|
226
|
+
return { before: args.before };
|
|
227
|
+
if (args.after !== undefined)
|
|
228
|
+
return { after: args.after };
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
/** The shared position input schema for the move tools. */
|
|
232
|
+
const positionShape = {
|
|
233
|
+
first: z.boolean().optional().describe("place the block at the top of its bucket"),
|
|
234
|
+
last: z.boolean().optional().describe("place the block at the bottom of its bucket"),
|
|
235
|
+
before: z
|
|
236
|
+
.string()
|
|
237
|
+
.optional()
|
|
238
|
+
.describe("place the block immediately before this item (same bucket)"),
|
|
239
|
+
after: z
|
|
240
|
+
.string()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("place the block immediately after this item (same bucket)"),
|
|
243
|
+
};
|
|
149
244
|
const READ_ONLY = { readOnlyHint: true };
|
|
150
245
|
const NON_DESTRUCTIVE = { destructiveHint: false };
|
|
151
246
|
const DESTRUCTIVE = { destructiveHint: true };
|
|
@@ -201,10 +296,25 @@ const MCP_UNTAGGED_CONFLICT = "untagged does not combine with tag/exact_tag";
|
|
|
201
296
|
const dryRunShape = {
|
|
202
297
|
dry_run: z.boolean().optional().describe("Preview the planned change without applying anything"),
|
|
203
298
|
};
|
|
299
|
+
/**
|
|
300
|
+
* The per-call idempotency key for a single write tool — the analogue of a batch
|
|
301
|
+
* line's op_id. A resubmission carrying the same key is recognized as already
|
|
302
|
+
* applied (a prior verified change with that key) and is not re-run. Spread into
|
|
303
|
+
* the single-mutation write tools (not the variadic move/reorder tools, whose
|
|
304
|
+
* idempotency is the batch-shaped per-line op_id).
|
|
305
|
+
*/
|
|
306
|
+
const opIdShape = {
|
|
307
|
+
op_id: z
|
|
308
|
+
.string()
|
|
309
|
+
.optional()
|
|
310
|
+
.describe("Idempotency key: a resubmission with the same key is recognized as already applied " +
|
|
311
|
+
"and not re-run (matches [A-Za-z0-9_-], 1-64 chars)"),
|
|
312
|
+
};
|
|
204
313
|
/**
|
|
205
314
|
* The per-call opt-in for the tools that reach a change only by driving the
|
|
206
315
|
* local Things app's accessibility interface. Shared by `repeat`, the
|
|
207
|
-
* `convert_to_project`
|
|
316
|
+
* `convert_to_project` tool, the heading tool's `promote_heading` action, and
|
|
317
|
+
* the `areas` reorder scope.
|
|
208
318
|
*/
|
|
209
319
|
const driveGuiShape = {
|
|
210
320
|
dangerously_drive_gui: z
|
|
@@ -226,6 +336,20 @@ const createTagsShape = {
|
|
|
226
336
|
"instead of stopping on an unknown tag"),
|
|
227
337
|
};
|
|
228
338
|
const containerRef = (ref) => ({ uuid: ref, title: ref });
|
|
339
|
+
/**
|
|
340
|
+
* The `loose` reserved-word disclosure for the area-filtered read tools
|
|
341
|
+
* (read_view, search — the same advisory the CLI surfaces, #333/#346). `loose`
|
|
342
|
+
* ALWAYS addresses the null area; when a real area named "Loose" shadows the
|
|
343
|
+
* reserved word this names it (by uuid) so it stays targetable. Returns
|
|
344
|
+
* undefined when the ref is not the reserved word, or nothing shadows it.
|
|
345
|
+
* (get_area reads its disclosure from the area-view result's own `notice`.)
|
|
346
|
+
*/
|
|
347
|
+
function looseAreaWarnings(c, areaRef) {
|
|
348
|
+
if (areaRef === undefined || !isLooseRef(areaRef))
|
|
349
|
+
return undefined;
|
|
350
|
+
const shadow = c.read.areas().find((a) => isLooseRef(a.title));
|
|
351
|
+
return shadow !== undefined ? [looseShadowNotice(shadow.uuid)] : undefined;
|
|
352
|
+
}
|
|
229
353
|
/** Cap on project titles inlined into the server instructions. */
|
|
230
354
|
const INSTRUCTIONS_MAX_PROJECTS = 100;
|
|
231
355
|
/** A tag's display label: nested tags show `parent > child`. */
|
|
@@ -258,14 +382,22 @@ function buildInstructions(getClient) {
|
|
|
258
382
|
"confirmation parameter named in their description; refused calls return an error saying " +
|
|
259
383
|
"what to pass.",
|
|
260
384
|
"- Read-result semantics: an item's tags are its direct tags; its effective tags also include " +
|
|
261
|
-
"tags inherited from its containing project and area.
|
|
262
|
-
"
|
|
263
|
-
"in Anytime and
|
|
385
|
+
"tags inherited from its containing project and area. The today and evening markers are " +
|
|
386
|
+
"present only for an item in Today (evening implies today); an unscheduled start=active item " +
|
|
387
|
+
"is in Anytime and carries neither. Completing an item makes it findable in Logbook.",
|
|
264
388
|
"- For capped reads, pass limit to cap rows or all: true for everything; if both are set, all wins.",
|
|
265
389
|
`- Read results are compact: ${OMIT_EMPTY_NOTE}`,
|
|
266
390
|
];
|
|
267
391
|
try {
|
|
268
392
|
const c = getClient();
|
|
393
|
+
// Under a container scope the inventory below is already limited to in-scope
|
|
394
|
+
// containers (the client filters areas/projects), so out-of-scope names are
|
|
395
|
+
// never embedded here. State the scope plainly so the agent knows its bounds.
|
|
396
|
+
if (c.scope !== undefined) {
|
|
397
|
+
lines.push("", `Scope: this server is limited to the ${c.scope.kind} "${c.scope.title}". Only items ` +
|
|
398
|
+
"within it are readable, and every change is confined to it; a reference naming anything " +
|
|
399
|
+
"outside it returns not-found, exactly as a nonexistent reference does.");
|
|
400
|
+
}
|
|
269
401
|
const areas = c.read.areas();
|
|
270
402
|
const tags = c.read.tags();
|
|
271
403
|
const projects = c.read.projects();
|
|
@@ -306,6 +438,13 @@ function deriveMcpActor(clientName) {
|
|
|
306
438
|
.replaceAll(/-+$/g, "");
|
|
307
439
|
return slug === "" ? MCP_ACTOR_PREFIX : `${MCP_ACTOR_PREFIX}:${slug}`;
|
|
308
440
|
}
|
|
441
|
+
/** Validate an op_id (throwing RangeError → mapped to a `usage` tool error by `guard`). */
|
|
442
|
+
function assertOpId(opId) {
|
|
443
|
+
if (!OP_ID_RE.test(opId)) {
|
|
444
|
+
throw new RangeError("op_id must match [A-Za-z0-9_-] and be 1-64 characters");
|
|
445
|
+
}
|
|
446
|
+
return opId;
|
|
447
|
+
}
|
|
309
448
|
export function createThingsMcpServer(options = {}) {
|
|
310
449
|
// One lazily-opened client for the server's lifetime; SQLite read
|
|
311
450
|
// snapshots are per-statement, so fresh reads see external commits.
|
|
@@ -313,6 +452,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
313
452
|
const getClient = () => {
|
|
314
453
|
client ??= openThings({
|
|
315
454
|
...(options.dbPath !== undefined && { dbPath: options.dbPath }),
|
|
455
|
+
...(options.scope !== undefined && { scope: options.scope }),
|
|
316
456
|
...options.openOptions,
|
|
317
457
|
});
|
|
318
458
|
return client;
|
|
@@ -333,9 +473,11 @@ export function createThingsMcpServer(options = {}) {
|
|
|
333
473
|
...(args.acknowledge_project_reopen === true && { acknowledgeProjectReopen: true }),
|
|
334
474
|
...(args.dangerously_permanent === true && { dangerouslyPermanent: true }),
|
|
335
475
|
...(args.acknowledge_tag_subtree === true && { acknowledgeTagSubtree: true }),
|
|
476
|
+
...(args.allow_non_empty_area === true && { allowNonEmptyArea: true }),
|
|
336
477
|
...(args.dangerously_drive_gui === true && { dangerouslyDriveGui: true }),
|
|
337
478
|
...(args.create_tags === true && { createTags: true }),
|
|
338
479
|
...(args.tz !== undefined && { zone: args.tz }),
|
|
480
|
+
...(args.op_id !== undefined && { opId: assertOpId(args.op_id) }),
|
|
339
481
|
});
|
|
340
482
|
/** Run a handler, mapping environment/usage throws to tool errors. */
|
|
341
483
|
const guard = async (fn) => {
|
|
@@ -364,8 +506,13 @@ export function createThingsMcpServer(options = {}) {
|
|
|
364
506
|
* same note the CLI prints), so a consumer sees when the Things database no
|
|
365
507
|
* longer matches the validated schema and its data may be incomplete. No
|
|
366
508
|
* block is added when the schema checks out or the read itself errored.
|
|
509
|
+
*
|
|
510
|
+
* `extraWarnings` folds in the read's own advisories (the `loose` reserved-word
|
|
511
|
+
* shadow disclosure, an area-view's placement `notice`) so they reach the
|
|
512
|
+
* consumer in the SAME `meta.warnings` array the CLI surfaces — schema warnings
|
|
513
|
+
* first, then the read-specific notices.
|
|
367
514
|
*/
|
|
368
|
-
const readGuard = async (fn, tz) => {
|
|
515
|
+
const readGuard = async (fn, tz, extraMeta, extraWarnings) => {
|
|
369
516
|
const result = await guard(fn);
|
|
370
517
|
if (result.isError === true)
|
|
371
518
|
return result;
|
|
@@ -382,9 +529,23 @@ export function createThingsMcpServer(options = {}) {
|
|
|
382
529
|
catch {
|
|
383
530
|
warnings = [];
|
|
384
531
|
}
|
|
532
|
+
// Read-specific advisories the handler resolved (loose-shadow / area notice)
|
|
533
|
+
// — merged AFTER the schema warnings, defensive against a DB read throwing.
|
|
534
|
+
let readNotices = [];
|
|
535
|
+
try {
|
|
536
|
+
readNotices = extraWarnings?.() ?? [];
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
readNotices = [];
|
|
540
|
+
}
|
|
541
|
+
const allWarnings = [...warnings, ...readNotices];
|
|
542
|
+
// Extra additive meta the handler resolved during the read (e.g. the active
|
|
543
|
+
// `area` filter) — evaluated AFTER fn ran, so a value it populated is seen.
|
|
544
|
+
const extra = extraMeta?.() ?? {};
|
|
385
545
|
const meta = {
|
|
386
|
-
...(
|
|
546
|
+
...(allWarnings.length > 0 && { warnings: allWarnings }),
|
|
387
547
|
...(clock !== undefined && { clock }),
|
|
548
|
+
...extra,
|
|
388
549
|
};
|
|
389
550
|
if (Object.keys(meta).length === 0)
|
|
390
551
|
return result;
|
|
@@ -422,6 +583,10 @@ export function createThingsMcpServer(options = {}) {
|
|
|
422
583
|
view: z.enum(["today", "inbox", "anytime", "upcoming", "someday", "logbook", "trash"]),
|
|
423
584
|
...tagFilterShape,
|
|
424
585
|
...tzShape,
|
|
586
|
+
area: z
|
|
587
|
+
.string()
|
|
588
|
+
.optional()
|
|
589
|
+
.describe(`today/anytime/someday/upcoming/logbook only: ${AREA_FILTER_DESC}`),
|
|
425
590
|
evening: z.boolean().optional().describe("today only: show only the This Evening section"),
|
|
426
591
|
show_active_project_items: z
|
|
427
592
|
.union([z.boolean(), z.number().int().min(1)])
|
|
@@ -462,111 +627,141 @@ export function createThingsMcpServer(options = {}) {
|
|
|
462
627
|
.optional()
|
|
463
628
|
.describe("show everything (flat views: no row limit; anytime/someday: no per-block caps); " +
|
|
464
629
|
ALL_WINS_NOTE),
|
|
630
|
+
full: z.boolean().optional().describe(FULL_DESC),
|
|
465
631
|
},
|
|
466
632
|
annotations: READ_ONLY,
|
|
467
|
-
}, async (args) =>
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return usage("show_active_project_items applies only to someday");
|
|
494
|
-
}
|
|
495
|
-
if (args.view !== "today" && args.evening === true) {
|
|
496
|
-
return usage(`evening applies only to today, not ${args.view}`);
|
|
497
|
-
}
|
|
498
|
-
if (args.view === "someday" && args.project_limit !== undefined) {
|
|
499
|
-
return usage("project_limit does not apply to someday — pass a number as show_active_project_items " +
|
|
500
|
-
"to cap that section's project lists");
|
|
501
|
-
}
|
|
502
|
-
const limit = resolveLimit(args);
|
|
503
|
-
const areaLimit = resolveCap(args.area_limit, args.all, AREA_PREVIEW_LIMIT);
|
|
504
|
-
const projectLimit = resolveCap(args.project_limit, args.all, PROJECT_PREVIEW_LIMIT);
|
|
505
|
-
if (areaLimit === "conflict" || projectLimit === "conflict") {
|
|
506
|
-
return usage("pass at most one of area_limit/project_limit / all");
|
|
507
|
-
}
|
|
508
|
-
const c = getClient();
|
|
509
|
-
const filter = validated.filter;
|
|
510
|
-
const zone = args.tz !== undefined ? { zone: args.tz } : {};
|
|
511
|
-
switch (args.view) {
|
|
512
|
-
case "today": {
|
|
513
|
-
const { view, truncation } = c.read.today({
|
|
514
|
-
...filter,
|
|
515
|
-
...zone,
|
|
516
|
-
...(args.evening === true && { eveningOnly: true }),
|
|
517
|
-
limit,
|
|
518
|
-
});
|
|
519
|
-
return truncatedResult(view, truncation);
|
|
633
|
+
}, async (args) => {
|
|
634
|
+
// The active `area` scope, resolved during the read and surfaced as the
|
|
635
|
+
// additive `meta.filter` (the readGuard extra-meta thunk reads it after fn).
|
|
636
|
+
let filterMeta;
|
|
637
|
+
const full = args.full === true;
|
|
638
|
+
return readGuard(() => {
|
|
639
|
+
const badZone = badTz(args.tz);
|
|
640
|
+
if (badZone !== null)
|
|
641
|
+
return badZone;
|
|
642
|
+
// Tag-conflict AND overdue-applicability both derive from the shared
|
|
643
|
+
// contract: read_view honors overdue only on today/inbox/anytime/someday
|
|
644
|
+
// (the current-work views), matching FILTER_CONTRACT.
|
|
645
|
+
const validated = validateViewArgs(args.view, { ...tagPresence(args), overdue: args.overdue }, {
|
|
646
|
+
untaggedConflict: MCP_UNTAGGED_CONFLICT,
|
|
647
|
+
overdueRejected: `overdue applies to today/inbox/anytime/someday, not ${args.view}`,
|
|
648
|
+
overdueStatusWiden: "",
|
|
649
|
+
});
|
|
650
|
+
if (!validated.ok)
|
|
651
|
+
return usage(validated.message);
|
|
652
|
+
// show_active_project_items is the preferred name; active_project_items
|
|
653
|
+
// stays accepted as a compatibility alias.
|
|
654
|
+
const showActiveProjectItems = args.show_active_project_items ?? args.active_project_items;
|
|
655
|
+
const isGrouped = args.view === "anytime" || args.view === "someday";
|
|
656
|
+
if (isGrouped && args.limit !== undefined) {
|
|
657
|
+
return usage(`limit does not apply to ${args.view} — cap blocks with area_limit` +
|
|
658
|
+
`${args.view === "anytime" ? "/project_limit" : ""}, or pass all: true`);
|
|
520
659
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return truncatedResult(items, truncation);
|
|
660
|
+
if (!isGrouped && (args.area_limit !== undefined || args.project_limit !== undefined)) {
|
|
661
|
+
return usage(`area_limit/project_limit apply only to anytime/someday, not ${args.view}`);
|
|
524
662
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
...filter,
|
|
528
|
-
...zone,
|
|
529
|
-
areaLimit,
|
|
530
|
-
projectLimit,
|
|
531
|
-
});
|
|
532
|
-
return groupedResult(view, grouped);
|
|
663
|
+
if (args.view !== "someday" && showActiveProjectItems !== undefined) {
|
|
664
|
+
return usage("show_active_project_items applies only to someday");
|
|
533
665
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
...filter,
|
|
537
|
-
...zone,
|
|
538
|
-
...(args.horizon !== undefined && { horizon: args.horizon }),
|
|
539
|
-
limit,
|
|
540
|
-
});
|
|
541
|
-
return truncatedResult(items, truncation);
|
|
666
|
+
if (args.view !== "today" && args.evening === true) {
|
|
667
|
+
return usage(`evening applies only to today, not ${args.view}`);
|
|
542
668
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
const { view, grouped } = c.read.someday({
|
|
549
|
-
...filter,
|
|
550
|
-
...zone,
|
|
551
|
-
...((active === true || typeof active === "number") && {
|
|
552
|
-
activeProjectItems: true,
|
|
553
|
-
}),
|
|
554
|
-
areaLimit,
|
|
555
|
-
// true = every item per project; a number caps each list.
|
|
556
|
-
projectLimit: typeof active === "number" ? active : null,
|
|
557
|
-
});
|
|
558
|
-
return groupedResult(view, grouped);
|
|
669
|
+
// The `area` filter applies to the area-carrying views only; inbox
|
|
670
|
+
// (area-less captures) and trash have no area to scope by.
|
|
671
|
+
if ((args.view === "inbox" || args.view === "trash") && args.area !== undefined) {
|
|
672
|
+
return usage(`area applies to today/anytime/someday/upcoming/logbook, not ${args.view}`);
|
|
559
673
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
674
|
+
if (args.view === "someday" && args.project_limit !== undefined) {
|
|
675
|
+
return usage("project_limit does not apply to someday — pass a number as show_active_project_items " +
|
|
676
|
+
"to cap that section's project lists");
|
|
563
677
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
678
|
+
const limit = resolveLimit(args);
|
|
679
|
+
const areaLimit = resolveCap(args.area_limit, args.all, AREA_PREVIEW_LIMIT);
|
|
680
|
+
const projectLimit = resolveCap(args.project_limit, args.all, PROJECT_PREVIEW_LIMIT);
|
|
681
|
+
if (areaLimit === "conflict" || projectLimit === "conflict") {
|
|
682
|
+
return usage("pass at most one of area_limit/project_limit / all");
|
|
567
683
|
}
|
|
568
|
-
|
|
569
|
-
|
|
684
|
+
const c = getClient();
|
|
685
|
+
const filter = validated.filter;
|
|
686
|
+
const zone = args.tz !== undefined ? { zone: args.tz } : {};
|
|
687
|
+
// The `area` scope, threaded into the area-carrying views; the client
|
|
688
|
+
// resolves it, applies the post-filter, and hands back the target for
|
|
689
|
+
// the `meta.filter` annotation (captured into `filterMeta`).
|
|
690
|
+
const area = args.area !== undefined ? { area: args.area } : {};
|
|
691
|
+
switch (args.view) {
|
|
692
|
+
case "today": {
|
|
693
|
+
const { view, truncation, filter: fm, } = c.read.today({
|
|
694
|
+
...filter,
|
|
695
|
+
...zone,
|
|
696
|
+
...area,
|
|
697
|
+
...(args.evening === true && { eveningOnly: true }),
|
|
698
|
+
limit,
|
|
699
|
+
});
|
|
700
|
+
filterMeta = fm;
|
|
701
|
+
return truncatedResult(shapeReadPayload("today", view, full), truncation);
|
|
702
|
+
}
|
|
703
|
+
case "inbox": {
|
|
704
|
+
const { items, truncation } = c.read.inbox({ ...filter, ...zone, limit });
|
|
705
|
+
return truncatedResult(shapeReadPayload("inbox", items, full), truncation);
|
|
706
|
+
}
|
|
707
|
+
case "anytime": {
|
|
708
|
+
const { view, truncation, filter: fm, } = c.read.anytime({
|
|
709
|
+
...filter,
|
|
710
|
+
...zone,
|
|
711
|
+
...area,
|
|
712
|
+
areaLimit,
|
|
713
|
+
projectLimit,
|
|
714
|
+
});
|
|
715
|
+
filterMeta = fm;
|
|
716
|
+
return groupedResult(shapeReadPayload("anytime", view, full), truncation);
|
|
717
|
+
}
|
|
718
|
+
case "upcoming": {
|
|
719
|
+
const { items, truncation, filter: fm, } = c.read.upcoming({
|
|
720
|
+
...filter,
|
|
721
|
+
...zone,
|
|
722
|
+
...area,
|
|
723
|
+
...(args.horizon !== undefined && { horizon: args.horizon }),
|
|
724
|
+
limit,
|
|
725
|
+
});
|
|
726
|
+
filterMeta = fm;
|
|
727
|
+
return truncatedResult(shapeReadPayload("upcoming", items, full), truncation);
|
|
728
|
+
}
|
|
729
|
+
case "someday": {
|
|
730
|
+
const active = showActiveProjectItems;
|
|
731
|
+
if (typeof active === "number" && args.all === true) {
|
|
732
|
+
return usage("pass at most one of a numeric show_active_project_items / all");
|
|
733
|
+
}
|
|
734
|
+
const { view, truncation, filter: fm, } = c.read.someday({
|
|
735
|
+
...filter,
|
|
736
|
+
...zone,
|
|
737
|
+
...area,
|
|
738
|
+
...((active === true || typeof active === "number") && {
|
|
739
|
+
activeProjectItems: true,
|
|
740
|
+
}),
|
|
741
|
+
areaLimit,
|
|
742
|
+
// true = every item per project; a number caps each list.
|
|
743
|
+
projectLimit: typeof active === "number" ? active : null,
|
|
744
|
+
});
|
|
745
|
+
filterMeta = fm;
|
|
746
|
+
return groupedResult(shapeReadPayload("someday", view, full), truncation);
|
|
747
|
+
}
|
|
748
|
+
case "logbook": {
|
|
749
|
+
const { items, truncation, filter: fm, } = c.read.logbook({
|
|
750
|
+
...filter,
|
|
751
|
+
...zone,
|
|
752
|
+
...area,
|
|
753
|
+
limit,
|
|
754
|
+
});
|
|
755
|
+
filterMeta = fm;
|
|
756
|
+
return truncatedResult(shapeReadPayload("logbook", items, full), truncation);
|
|
757
|
+
}
|
|
758
|
+
case "trash": {
|
|
759
|
+
const { items, truncation } = c.read.trash({ ...zone, limit });
|
|
760
|
+
return truncatedResult(shapeReadPayload("trash", items, full), truncation);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}, args.tz, () => (filterMeta !== undefined ? { filter: filterMeta } : undefined), () => looseAreaWarnings(getClient(), args.area));
|
|
764
|
+
});
|
|
570
765
|
server.registerTool("search", {
|
|
571
766
|
description: "Find items by title/notes substring. Returns open, untrashed items by default; " +
|
|
572
767
|
"include more with logged/trashed/all. Scope with project/area/tag — scope " +
|
|
@@ -583,7 +778,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
583
778
|
area: z
|
|
584
779
|
.string()
|
|
585
780
|
.optional()
|
|
586
|
-
.describe(`Restrict to one area's direct
|
|
781
|
+
.describe(`Restrict to one area's subtree — its direct items plus its projects' children (${REF_FORMAT}, or \`loose\` for area-less)`),
|
|
587
782
|
type: z.enum(["to-do", "project"]).optional(),
|
|
588
783
|
logged: z.boolean().optional().describe("Also include completed/canceled items"),
|
|
589
784
|
trashed: z.boolean().optional().describe("Also include trashed items"),
|
|
@@ -592,6 +787,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
592
787
|
.optional()
|
|
593
788
|
.describe(`Everything, unbounded: open + logged + trashed, no row limit; ${ALL_WINS_NOTE}`),
|
|
594
789
|
limit: z.number().int().min(1).optional().describe(`${LIMIT_DESC}; ${LIMIT_IGNORED_NOTE}`),
|
|
790
|
+
full: z.boolean().optional().describe(FULL_DESC),
|
|
595
791
|
},
|
|
596
792
|
annotations: READ_ONLY,
|
|
597
793
|
}, async (args) => readGuard(() => {
|
|
@@ -625,8 +821,8 @@ export function createThingsMcpServer(options = {}) {
|
|
|
625
821
|
...(args.trashed === true && { trashed: true }),
|
|
626
822
|
...(args.all === true && { all: true }),
|
|
627
823
|
});
|
|
628
|
-
return truncatedResult(items, truncation);
|
|
629
|
-
}, args.tz));
|
|
824
|
+
return truncatedResult(shapeReadPayload("search", items, args.full === true), truncation);
|
|
825
|
+
}, args.tz, undefined, () => looseAreaWarnings(getClient(), args.area)));
|
|
630
826
|
server.registerTool("changes_since", {
|
|
631
827
|
description: "List items created or modified since a moment — including trashed, logged, and " +
|
|
632
828
|
"repeating items (inspect each item's fields to tell them apart). Edits to tags, " +
|
|
@@ -636,6 +832,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
636
832
|
since: z.string().describe("ISO date-time, e.g. 2026-07-06T08:00:00"),
|
|
637
833
|
...limitShape,
|
|
638
834
|
...tzShape,
|
|
835
|
+
full: z.boolean().optional().describe(FULL_DESC),
|
|
639
836
|
},
|
|
640
837
|
annotations: READ_ONLY,
|
|
641
838
|
}, async (args) => readGuard(() => {
|
|
@@ -652,7 +849,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
652
849
|
limit,
|
|
653
850
|
...(args.tz !== undefined && { zone: args.tz }),
|
|
654
851
|
});
|
|
655
|
-
return truncatedResult(items, truncation);
|
|
852
|
+
return truncatedResult(shapeReadPayload("changes", items, args.full === true), truncation);
|
|
656
853
|
}, args.tz));
|
|
657
854
|
server.registerTool("get_item", {
|
|
658
855
|
description: "Full detail for one item by uuid: notes, schedule, reminder, deadline, tags " +
|
|
@@ -665,7 +862,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
665
862
|
const item = getClient().read.byUuid(args.uuid);
|
|
666
863
|
return item === null
|
|
667
864
|
? errorResult({ code: "not-found", message: noUuidMatch("item", args.uuid) })
|
|
668
|
-
: readResult(item);
|
|
865
|
+
: readResult(shapeReadPayload("detail", item, false));
|
|
669
866
|
}));
|
|
670
867
|
server.registerTool("get_project", {
|
|
671
868
|
description: "One project's full contents: metadata plus its to-dos grouped under their headings. " +
|
|
@@ -680,6 +877,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
680
877
|
.boolean()
|
|
681
878
|
.optional()
|
|
682
879
|
.describe("Keep only child to-dos past their deadline (due today is not overdue); headings left empty are dropped"),
|
|
880
|
+
full: z.boolean().optional().describe(FULL_DESC),
|
|
683
881
|
},
|
|
684
882
|
annotations: READ_ONLY,
|
|
685
883
|
}, async (args) => readGuard(() => {
|
|
@@ -688,15 +886,17 @@ export function createThingsMcpServer(options = {}) {
|
|
|
688
886
|
return badZone;
|
|
689
887
|
if (tagFlagConflict(tagPresence(args)))
|
|
690
888
|
return usage(MCP_UNTAGGED_CONFLICT);
|
|
691
|
-
return readResult(getClient().read.projectView(args.uuid, {
|
|
889
|
+
return readResult(shapeReadPayload("project-view", getClient().read.projectView(args.uuid, {
|
|
692
890
|
overdue: args.overdue === true,
|
|
693
891
|
...tagFilterFields(tagPresence(args)),
|
|
694
892
|
...(args.tz !== undefined && { zone: args.tz }),
|
|
695
|
-
}));
|
|
893
|
+
}), args.full === true));
|
|
696
894
|
}, args.tz));
|
|
697
895
|
server.registerTool("get_area", {
|
|
698
896
|
description: "One area's contents: metadata plus its direct to-dos (active first), its " +
|
|
699
|
-
"projects in canonical order, later (scheduled/repeating/someday)
|
|
897
|
+
"projects in canonical order, and later (scheduled/repeating/someday). The area " +
|
|
898
|
+
"logbook is not returned here — read it with read_view logbook + area; trashed " +
|
|
899
|
+
"rows live in read_view trash. " +
|
|
700
900
|
`The project-rows and direct-to-dos sections are capped at ${AREA_PREVIEW_LIMIT} each ` +
|
|
701
901
|
"by default (project_limit / area_limit adjust them; all: true lifts both); the " +
|
|
702
902
|
"second result block reports the counts. " +
|
|
@@ -722,28 +922,36 @@ export function createThingsMcpServer(options = {}) {
|
|
|
722
922
|
.optional()
|
|
723
923
|
.describe("Keep only rows (loose to-dos AND child projects) whose own deadline is past (due today is not overdue); no descent into project contents"),
|
|
724
924
|
all: z.boolean().optional().describe("return both sections in full (no caps)"),
|
|
925
|
+
full: z.boolean().optional().describe(FULL_DESC),
|
|
725
926
|
},
|
|
726
927
|
annotations: READ_ONLY,
|
|
727
|
-
}, async (args) =>
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
928
|
+
}, async (args) => {
|
|
929
|
+
// The `loose` pseudo-area shadow disclosure the area-view read resolves —
|
|
930
|
+
// captured after fn runs and folded into meta.warnings (parity with the
|
|
931
|
+
// CLI's `area show loose` notice).
|
|
932
|
+
let areaNotice;
|
|
933
|
+
return readGuard(() => {
|
|
934
|
+
const badZone = badTz(args.tz);
|
|
935
|
+
if (badZone !== null)
|
|
936
|
+
return badZone;
|
|
937
|
+
if (tagFlagConflict(tagPresence(args)))
|
|
938
|
+
return usage(MCP_UNTAGGED_CONFLICT);
|
|
939
|
+
const areaLimit = resolveCap(args.area_limit, args.all, AREA_PREVIEW_LIMIT);
|
|
940
|
+
const projectLimit = resolveCap(args.project_limit, args.all, AREA_PREVIEW_LIMIT);
|
|
941
|
+
if (areaLimit === "conflict" || projectLimit === "conflict") {
|
|
942
|
+
return usage("pass at most one of area_limit/project_limit / all");
|
|
943
|
+
}
|
|
944
|
+
const { view, truncation, notice } = getClient().read.areaView(args.ref, {
|
|
945
|
+
overdue: args.overdue === true,
|
|
946
|
+
...tagFilterFields(tagPresence(args)),
|
|
947
|
+
...(args.tz !== undefined && { zone: args.tz }),
|
|
948
|
+
areaLimit,
|
|
949
|
+
projectLimit,
|
|
950
|
+
});
|
|
951
|
+
areaNotice = notice;
|
|
952
|
+
return groupedResult(shapeReadPayload("area-view", view, args.full === true), truncation);
|
|
953
|
+
}, args.tz, undefined, () => (areaNotice !== undefined ? [areaNotice] : undefined));
|
|
954
|
+
});
|
|
747
955
|
server.registerTool("list_collections", {
|
|
748
956
|
description: "List every project, area, or tag (tags include their parent-tag nesting). Use to " +
|
|
749
957
|
"refresh the inventory summarized in the server instructions. The tag filters scope " +
|
|
@@ -757,6 +965,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
757
965
|
.boolean()
|
|
758
966
|
.optional()
|
|
759
967
|
.describe("projects only: keep only projects past their deadline (due today is not overdue); areas/tags carry no deadline and reject it"),
|
|
968
|
+
full: z.boolean().optional().describe(`projects only: ${FULL_DESC}`),
|
|
760
969
|
},
|
|
761
970
|
annotations: READ_ONLY,
|
|
762
971
|
}, async (args) => readGuard(() => {
|
|
@@ -784,7 +993,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
784
993
|
}
|
|
785
994
|
if (tagFlagConflict(tagPresence(args)))
|
|
786
995
|
return usage(MCP_UNTAGGED_CONFLICT);
|
|
787
|
-
return readResult(args.kind === "projects"
|
|
996
|
+
return readResult(shapeReadPayload(args.kind, args.kind === "projects"
|
|
788
997
|
? c.read.projects({
|
|
789
998
|
overdue: args.overdue === true,
|
|
790
999
|
...tagFilterFields(tagPresence(args)),
|
|
@@ -792,7 +1001,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
792
1001
|
})
|
|
793
1002
|
: args.kind === "areas"
|
|
794
1003
|
? c.read.areas()
|
|
795
|
-
: c.read.tags());
|
|
1004
|
+
: c.read.tags(), args.full === true));
|
|
796
1005
|
}, args.tz));
|
|
797
1006
|
// ---------------------------------------------------------------- to-dos
|
|
798
1007
|
const whenSchema = z.string().optional().describe(WHEN_VALUES);
|
|
@@ -820,6 +1029,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
820
1029
|
...createTagsShape,
|
|
821
1030
|
...tzShape,
|
|
822
1031
|
...dryRunShape,
|
|
1032
|
+
...opIdShape,
|
|
823
1033
|
},
|
|
824
1034
|
annotations: NON_DESTRUCTIVE,
|
|
825
1035
|
}, async (args) => guard(async () => {
|
|
@@ -881,6 +1091,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
881
1091
|
...createTagsShape,
|
|
882
1092
|
...tzShape,
|
|
883
1093
|
...dryRunShape,
|
|
1094
|
+
...opIdShape,
|
|
884
1095
|
},
|
|
885
1096
|
annotations: NON_DESTRUCTIVE,
|
|
886
1097
|
}, async (args) => guard(async () => {
|
|
@@ -983,6 +1194,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
983
1194
|
.optional()
|
|
984
1195
|
.describe("scope project, open only: also reopen the to-dos resolved with the project"),
|
|
985
1196
|
...dryRunShape,
|
|
1197
|
+
...opIdShape,
|
|
986
1198
|
},
|
|
987
1199
|
annotations: NON_DESTRUCTIVE,
|
|
988
1200
|
}, async (args) => guard(async () => {
|
|
@@ -1017,6 +1229,13 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1017
1229
|
if (args.children !== undefined) {
|
|
1018
1230
|
return usage("children applies only to status 'completed' or 'canceled'");
|
|
1019
1231
|
}
|
|
1232
|
+
// Reopening a project is a multi-leg compound — single-op idempotency
|
|
1233
|
+
// (op_id) does not apply to it in phase 1 (every other set_status path
|
|
1234
|
+
// is a single mutation). Refuse rather than silently drop the key.
|
|
1235
|
+
if (args.op_id !== undefined) {
|
|
1236
|
+
return usage("op_id is not available when reopening a project (a multi-leg compound in phase 1) — " +
|
|
1237
|
+
"use the batch tool with a per-line op_id");
|
|
1238
|
+
}
|
|
1020
1239
|
const outcome = await c.write.reopenProject(args.uuid, {
|
|
1021
1240
|
...opts,
|
|
1022
1241
|
...(args.restore_children === true && { restoreChildren: true }),
|
|
@@ -1026,21 +1245,37 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1026
1245
|
: mutationResult(outcome.project);
|
|
1027
1246
|
}));
|
|
1028
1247
|
server.registerTool("move_todo", {
|
|
1029
|
-
description: "Move
|
|
1030
|
-
"
|
|
1031
|
-
"
|
|
1032
|
-
"
|
|
1033
|
-
"
|
|
1248
|
+
description: "Move one or more to-dos as an ordered block (spec §4). MOVE changes WHAT a to-do " +
|
|
1249
|
+
"belongs to (membership somewhere); to REARRANGE to-dos that already share a container " +
|
|
1250
|
+
"without changing membership, call this with a position (first/last/before/after) and " +
|
|
1251
|
+
"NO destination — that is an in-place reorder, anchored at the earliest movee's slot, " +
|
|
1252
|
+
"and unmentioned siblings keep their order. The uuids order is the order they land " +
|
|
1253
|
+
"(name them backwards to reverse). Pass at most one destination: to_project, to_heading " +
|
|
1254
|
+
"(within to_project, or the movees' shared project), to_area, no_heading (leave the " +
|
|
1255
|
+
"heading, stay in the project), loose (leave heading, project, AND area), or to_inbox. " +
|
|
1256
|
+
"An anchor (before/after) positions but never migrates — an anchor-only move that would " +
|
|
1257
|
+
"cross containers is refused, as is a --before/--after whose movees span buckets. " +
|
|
1258
|
+
"Membership always succeeds; top-of-bucket placement is guaranteed only where a reorder " +
|
|
1259
|
+
"protocol exists (the result's placementClass states which). Moving into a " +
|
|
1260
|
+
"completed/canceled project reopens it — pass acknowledge_project_reopen.",
|
|
1034
1261
|
inputSchema: {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1262
|
+
uuids: z.array(z.string()).describe("The to-dos to move, in the order they should land"),
|
|
1263
|
+
to_project: z.string().optional().describe(`Destination project (${REF_FORMAT})`),
|
|
1264
|
+
to_heading: z
|
|
1265
|
+
.string()
|
|
1266
|
+
.optional()
|
|
1267
|
+
.describe("Destination heading (exact title or uuid; within to_project or the shared project)"),
|
|
1268
|
+
to_area: z.string().optional().describe(`Destination area (${REF_FORMAT})`),
|
|
1269
|
+
no_heading: z
|
|
1041
1270
|
.boolean()
|
|
1042
1271
|
.optional()
|
|
1043
|
-
.describe("
|
|
1272
|
+
.describe("Leave the heading but stay in the current project (unheaded block)"),
|
|
1273
|
+
loose: z
|
|
1274
|
+
.boolean()
|
|
1275
|
+
.optional()
|
|
1276
|
+
.describe("Detach from heading, project, AND area, keeping the schedule"),
|
|
1277
|
+
to_inbox: z.boolean().optional().describe("Move back to the Inbox (removes any schedule)"),
|
|
1278
|
+
...positionShape,
|
|
1044
1279
|
acknowledge_project_reopen: z
|
|
1045
1280
|
.boolean()
|
|
1046
1281
|
.optional()
|
|
@@ -1049,21 +1284,37 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1049
1284
|
},
|
|
1050
1285
|
annotations: NON_DESTRUCTIVE,
|
|
1051
1286
|
}, async (args) => guard(async () => {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1287
|
+
const dests = [];
|
|
1288
|
+
if (args.to_heading !== undefined) {
|
|
1289
|
+
dests.push({
|
|
1290
|
+
kind: "heading",
|
|
1291
|
+
sel: args.to_heading,
|
|
1292
|
+
...(args.to_project !== undefined && { project: containerRef(args.to_project) }),
|
|
1293
|
+
});
|
|
1056
1294
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1295
|
+
else if (args.to_project !== undefined) {
|
|
1296
|
+
dests.push({ kind: "project", ref: containerRef(args.to_project) });
|
|
1297
|
+
}
|
|
1298
|
+
if (args.to_area !== undefined)
|
|
1299
|
+
dests.push({ kind: "area", ref: containerRef(args.to_area) });
|
|
1300
|
+
if (args.no_heading === true)
|
|
1301
|
+
dests.push({ kind: "no-heading" });
|
|
1302
|
+
if (args.loose === true)
|
|
1303
|
+
dests.push({ kind: "loose" });
|
|
1304
|
+
if (args.to_inbox === true)
|
|
1305
|
+
dests.push({ kind: "inbox" });
|
|
1306
|
+
if (dests.length > 1) {
|
|
1307
|
+
return usage("pass at most one destination (to_project/to_heading/to_area/no_heading/loose/to_inbox)");
|
|
1308
|
+
}
|
|
1309
|
+
const position = movePositionArgs(args);
|
|
1310
|
+
if (position === "conflict")
|
|
1311
|
+
return usage("pass at most one of first/last/before/after");
|
|
1312
|
+
const request = {
|
|
1313
|
+
uuids: args.uuids,
|
|
1314
|
+
...(dests[0] !== undefined && { destination: dests[0] }),
|
|
1315
|
+
...(position !== undefined && { position }),
|
|
1316
|
+
};
|
|
1317
|
+
return moveResult(await getClient().write.moveTodos(request, writeOptions(args)));
|
|
1067
1318
|
}));
|
|
1068
1319
|
server.registerTool("set_tags", {
|
|
1069
1320
|
description: "Replace or extend a to-do's or project's tags. mode 'replace' (default) sets exactly " +
|
|
@@ -1075,6 +1326,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1075
1326
|
mode: z.enum(["replace", "add"]).optional().describe("Default: replace"),
|
|
1076
1327
|
...createTagsShape,
|
|
1077
1328
|
...dryRunShape,
|
|
1329
|
+
...opIdShape,
|
|
1078
1330
|
},
|
|
1079
1331
|
annotations: NON_DESTRUCTIVE,
|
|
1080
1332
|
}, async (args) => guard(async () => {
|
|
@@ -1184,8 +1436,9 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1184
1436
|
"kind item: moves a to-do or project to the Trash (recoverable via restore_item until " +
|
|
1185
1437
|
"the Trash is emptied; a deleted project takes its to-dos with it; not available for " +
|
|
1186
1438
|
"repeating to-dos). kind area: PERMANENT — areas do not go to the Trash, so this cannot " +
|
|
1187
|
-
"be undone and requires dangerously_permanent;
|
|
1188
|
-
"
|
|
1439
|
+
"be undone and requires dangerously_permanent; deleting an area moves its to-dos and " +
|
|
1440
|
+
"projects to the Trash, so a non-empty area is refused unless you pass " +
|
|
1441
|
+
"allow_non_empty_area (empty it first to keep its contents). kind tag: PERMANENT — requires " +
|
|
1189
1442
|
"dangerously_permanent; the tag is removed from every item, and if it has nested child " +
|
|
1190
1443
|
"tags they are ALL permanently deleted with it — pass acknowledge_tag_subtree to confirm.",
|
|
1191
1444
|
inputSchema: {
|
|
@@ -1197,11 +1450,16 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1197
1450
|
.boolean()
|
|
1198
1451
|
.optional()
|
|
1199
1452
|
.describe("kind area/tag: confirm permanent, unrecoverable deletion"),
|
|
1453
|
+
allow_non_empty_area: z
|
|
1454
|
+
.boolean()
|
|
1455
|
+
.optional()
|
|
1456
|
+
.describe("kind area: delete a non-empty area together with its contents (its to-dos and projects move to the Trash)"),
|
|
1200
1457
|
acknowledge_tag_subtree: z
|
|
1201
1458
|
.boolean()
|
|
1202
1459
|
.optional()
|
|
1203
1460
|
.describe("kind tag: confirm permanent deletion of ALL nested child tags too"),
|
|
1204
1461
|
...dryRunShape,
|
|
1462
|
+
...opIdShape,
|
|
1205
1463
|
},
|
|
1206
1464
|
annotations: DESTRUCTIVE,
|
|
1207
1465
|
}, async (args) => guard(async () => {
|
|
@@ -1222,7 +1480,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1222
1480
|
description: "Restore a trashed to-do or project. A to-do returns to the Inbox without its " +
|
|
1223
1481
|
"previous schedule or project/area. A project is restored in place: its schedule, " +
|
|
1224
1482
|
"area, and children come back exactly as they were.",
|
|
1225
|
-
inputSchema: { uuid: z.string(), ...dryRunShape },
|
|
1483
|
+
inputSchema: { uuid: z.string(), ...dryRunShape, ...opIdShape },
|
|
1226
1484
|
annotations: NON_DESTRUCTIVE,
|
|
1227
1485
|
}, async (args) => guard(async () => {
|
|
1228
1486
|
const c = getClient();
|
|
@@ -1239,6 +1497,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1239
1497
|
completion_date: z.string().optional().describe(DATE_FORMAT),
|
|
1240
1498
|
creation_date: z.string().optional().describe(DATE_FORMAT),
|
|
1241
1499
|
...dryRunShape,
|
|
1500
|
+
...opIdShape,
|
|
1242
1501
|
},
|
|
1243
1502
|
annotations: NON_DESTRUCTIVE,
|
|
1244
1503
|
}, async (args) => guard(async () => mutationResult(await getClient().write.backdateTodo(args.uuid, {
|
|
@@ -1255,6 +1514,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1255
1514
|
creation_date: z.string().optional().describe(`${DATE_FORMAT}; <= completion_date`),
|
|
1256
1515
|
notes: z.string().optional(),
|
|
1257
1516
|
...dryRunShape,
|
|
1517
|
+
...opIdShape,
|
|
1258
1518
|
},
|
|
1259
1519
|
annotations: NON_DESTRUCTIVE,
|
|
1260
1520
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addLoggedTodo({
|
|
@@ -1264,33 +1524,76 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1264
1524
|
...(args.notes !== undefined && { notes: args.notes }),
|
|
1265
1525
|
}, writeOptions(args)))));
|
|
1266
1526
|
server.registerTool("heading", {
|
|
1267
|
-
description: "Manage a project
|
|
1268
|
-
"
|
|
1269
|
-
"
|
|
1270
|
-
"
|
|
1271
|
-
"
|
|
1272
|
-
"
|
|
1273
|
-
"
|
|
1274
|
-
"
|
|
1275
|
-
"
|
|
1276
|
-
"
|
|
1277
|
-
"
|
|
1527
|
+
description: "Manage a project's headings — action selects which; project is always required, and a " +
|
|
1528
|
+
"heading is selected by its exact title or its uuid (never an ordinal). add_heading: a " +
|
|
1529
|
+
"new heading in the project (project + title; returns its uuid; uses the Things proxy " +
|
|
1530
|
+
"shortcuts, set up once with `things setup shortcuts`); a placement flag positions it, " +
|
|
1531
|
+
"else it appends. rename_heading: rename in place (project + heading + title; works on " +
|
|
1532
|
+
"archived headings). archive_heading: retire a heading so it leaves the active project " +
|
|
1533
|
+
"view (reversible with unarchive_heading); with open children pass children — complete " +
|
|
1534
|
+
"or cancel resolve them with the heading in one cascade, reparent moves them to the " +
|
|
1535
|
+
"project root keeping them open. unarchive_heading: bring an archived heading back; " +
|
|
1536
|
+
"restore_children also reopens the children the archive resolved with it. " +
|
|
1537
|
+
"promote_heading: promote a heading into a new project — this REPLACES the heading and " +
|
|
1538
|
+
"cannot be undone (its to-dos move under the new project), and requires " +
|
|
1539
|
+
"dangerously_drive_gui. move_heading: reposition headings as an ordered block (children " +
|
|
1540
|
+
"follow); pass exactly one placement flag. Reordering headings needs allow-experimental. " +
|
|
1541
|
+
"move_heading_to_project: relocate ONE heading (with its to-dos) to a DIFFERENT project " +
|
|
1542
|
+
"(project + heading + to_project) — the cross-project move, distinct from move_heading's " +
|
|
1543
|
+
"within-project reorder; GUI-only (requires dangerously_drive_gui), fails closed on a " +
|
|
1544
|
+
"source-heading or destination-project title collision, and has no undo (move it back). " +
|
|
1545
|
+
"dissolve_heading: remove a heading but KEEP its to-dos as direct project children (NOT " +
|
|
1546
|
+
"trashed — the opposite of a delete cascade); GUI-only (requires dangerously_drive_gui), " +
|
|
1547
|
+
"fails closed on a title collision, no undo.",
|
|
1278
1548
|
inputSchema: {
|
|
1279
|
-
action: z.enum([
|
|
1280
|
-
|
|
1281
|
-
|
|
1549
|
+
action: z.enum([
|
|
1550
|
+
"add_heading",
|
|
1551
|
+
"rename_heading",
|
|
1552
|
+
"archive_heading",
|
|
1553
|
+
"unarchive_heading",
|
|
1554
|
+
"promote_heading",
|
|
1555
|
+
"move_heading",
|
|
1556
|
+
"move_heading_to_project",
|
|
1557
|
+
"dissolve_heading",
|
|
1558
|
+
]),
|
|
1559
|
+
project: z.string().describe(`the heading's project (${REF_FORMAT})`),
|
|
1560
|
+
heading: z
|
|
1561
|
+
.string()
|
|
1562
|
+
.optional()
|
|
1563
|
+
.describe("rename/archive/unarchive/promote: the heading selector (exact title or uuid)"),
|
|
1564
|
+
headings: z
|
|
1565
|
+
.array(z.string())
|
|
1566
|
+
.optional()
|
|
1567
|
+
.describe("move_heading: heading selectors in the order they should land"),
|
|
1568
|
+
to_project: z
|
|
1569
|
+
.string()
|
|
1570
|
+
.optional()
|
|
1571
|
+
.describe(`move_heading_to_project: the destination project (${REF_FORMAT})`),
|
|
1572
|
+
title: z
|
|
1282
1573
|
.string()
|
|
1283
1574
|
.optional()
|
|
1284
|
-
.describe("
|
|
1285
|
-
title: z.string().optional().describe("create: the new heading; rename: the new title"),
|
|
1575
|
+
.describe("add_heading: the new heading; rename_heading: the new title"),
|
|
1286
1576
|
children: z
|
|
1287
1577
|
.enum(["complete", "cancel", "reparent"])
|
|
1288
1578
|
.optional()
|
|
1289
|
-
.describe("
|
|
1579
|
+
.describe("archive_heading: required when the heading has open children"),
|
|
1290
1580
|
restore_children: z
|
|
1291
1581
|
.boolean()
|
|
1292
1582
|
.optional()
|
|
1293
|
-
.describe("
|
|
1583
|
+
.describe("unarchive_heading: also reopen the children archived with the heading"),
|
|
1584
|
+
first: z
|
|
1585
|
+
.boolean()
|
|
1586
|
+
.optional()
|
|
1587
|
+
.describe("add/move: place first among the project's headings"),
|
|
1588
|
+
last: z.boolean().optional().describe("add/move: place last among the project's headings"),
|
|
1589
|
+
before_heading: z
|
|
1590
|
+
.string()
|
|
1591
|
+
.optional()
|
|
1592
|
+
.describe("add/move: place immediately before this heading (title or uuid)"),
|
|
1593
|
+
after_heading: z
|
|
1594
|
+
.string()
|
|
1595
|
+
.optional()
|
|
1596
|
+
.describe("add/move: place immediately after this heading (title or uuid)"),
|
|
1294
1597
|
...driveGuiShape,
|
|
1295
1598
|
...dryRunShape,
|
|
1296
1599
|
},
|
|
@@ -1298,45 +1601,105 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1298
1601
|
}, async (args) => guard(async () => {
|
|
1299
1602
|
const c = getClient();
|
|
1300
1603
|
const opts = writeOptions(args);
|
|
1604
|
+
const proj = c.resolve.project(args.project);
|
|
1605
|
+
const placementCount = [
|
|
1606
|
+
args.first === true,
|
|
1607
|
+
args.last === true,
|
|
1608
|
+
args.before_heading !== undefined,
|
|
1609
|
+
args.after_heading !== undefined,
|
|
1610
|
+
].filter(Boolean).length;
|
|
1611
|
+
const placement = () => {
|
|
1612
|
+
if (args.first === true)
|
|
1613
|
+
return { position: "first" };
|
|
1614
|
+
if (args.last === true)
|
|
1615
|
+
return { position: "last" };
|
|
1616
|
+
if (args.before_heading !== undefined) {
|
|
1617
|
+
return { before: c.resolve.heading(proj.uuid, args.before_heading).uuid };
|
|
1618
|
+
}
|
|
1619
|
+
if (args.after_heading !== undefined) {
|
|
1620
|
+
return { after: c.resolve.heading(proj.uuid, args.after_heading).uuid };
|
|
1621
|
+
}
|
|
1622
|
+
return undefined;
|
|
1623
|
+
};
|
|
1301
1624
|
switch (args.action) {
|
|
1302
|
-
case "
|
|
1303
|
-
if (args.
|
|
1304
|
-
return usage('action "
|
|
1625
|
+
case "add_heading": {
|
|
1626
|
+
if (args.title === undefined)
|
|
1627
|
+
return usage('action "add_heading" requires title');
|
|
1628
|
+
if (placementCount > 1) {
|
|
1629
|
+
return usage("pass at most one of first/last/before_heading/after_heading");
|
|
1305
1630
|
}
|
|
1306
|
-
return mutationResult(await c.write.
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1631
|
+
return mutationResult(await c.write.addHeading({ uuid: proj.uuid }, args.title, placement(), opts));
|
|
1632
|
+
}
|
|
1633
|
+
case "rename_heading": {
|
|
1634
|
+
if (args.heading === undefined || args.title === undefined) {
|
|
1635
|
+
return usage('action "rename_heading" requires heading and title');
|
|
1310
1636
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1637
|
+
const h = c.resolve.heading(proj.uuid, args.heading);
|
|
1638
|
+
return mutationResult(await c.write.renameHeading(h.uuid, args.title, opts));
|
|
1639
|
+
}
|
|
1640
|
+
case "archive_heading": {
|
|
1641
|
+
if (args.heading === undefined)
|
|
1642
|
+
return usage('action "archive_heading" requires heading');
|
|
1643
|
+
const h = c.resolve.heading(proj.uuid, args.heading);
|
|
1644
|
+
const r = await c.write.archiveHeading(h.uuid, args.children !== undefined ? { children: args.children } : {}, opts);
|
|
1316
1645
|
return r.heading.kind === "ok" || r.heading.kind === "dry-run"
|
|
1317
1646
|
? jsonResult(r)
|
|
1318
1647
|
: mutationResult(r.heading);
|
|
1319
1648
|
}
|
|
1320
|
-
case "
|
|
1321
|
-
if (args.
|
|
1322
|
-
return usage('action "
|
|
1323
|
-
|
|
1649
|
+
case "unarchive_heading": {
|
|
1650
|
+
if (args.heading === undefined) {
|
|
1651
|
+
return usage('action "unarchive_heading" requires heading');
|
|
1652
|
+
}
|
|
1653
|
+
const h = c.resolve.heading(proj.uuid, args.heading);
|
|
1654
|
+
const r = await c.write.unarchiveHeading(h.uuid, args.restore_children === true ? { restoreChildren: true } : {}, opts);
|
|
1324
1655
|
return r.heading.kind === "ok" || r.heading.kind === "dry-run"
|
|
1325
1656
|
? jsonResult(r)
|
|
1326
1657
|
: mutationResult(r.heading);
|
|
1327
1658
|
}
|
|
1328
|
-
case "
|
|
1329
|
-
if (args.
|
|
1330
|
-
return usage('action "
|
|
1659
|
+
case "promote_heading": {
|
|
1660
|
+
if (args.heading === undefined)
|
|
1661
|
+
return usage('action "promote_heading" requires heading');
|
|
1662
|
+
const h = c.resolve.heading(proj.uuid, args.heading);
|
|
1663
|
+
return mutationResult(await c.write.run("project.promote-heading", { uuid: h.uuid }, opts));
|
|
1664
|
+
}
|
|
1665
|
+
case "move_heading": {
|
|
1666
|
+
if (args.headings === undefined || args.headings.length === 0) {
|
|
1667
|
+
return usage('action "move_heading" requires headings');
|
|
1668
|
+
}
|
|
1669
|
+
if (placementCount !== 1) {
|
|
1670
|
+
return usage("move_heading requires exactly one of first/last/before_heading/after_heading");
|
|
1671
|
+
}
|
|
1672
|
+
const headings = args.headings.map((s) => c.resolve.heading(proj.uuid, s).uuid);
|
|
1673
|
+
return mutationResult(await c.write.moveHeading({ uuid: proj.uuid }, headings, placement(), opts));
|
|
1674
|
+
}
|
|
1675
|
+
case "move_heading_to_project": {
|
|
1676
|
+
if (args.heading === undefined || args.to_project === undefined) {
|
|
1677
|
+
return usage('action "move_heading_to_project" requires heading and to_project');
|
|
1331
1678
|
}
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1679
|
+
const dest = c.resolve.project(args.to_project);
|
|
1680
|
+
return mutationResult(await c.write.moveHeadingToProject({ uuid: proj.uuid }, args.heading, { uuid: dest.uuid }, opts));
|
|
1681
|
+
}
|
|
1682
|
+
case "dissolve_heading": {
|
|
1683
|
+
if (args.heading === undefined)
|
|
1684
|
+
return usage('action "dissolve_heading" requires heading');
|
|
1685
|
+
const h = c.resolve.heading(proj.uuid, args.heading);
|
|
1686
|
+
return mutationResult(await c.write.dissolveHeading(h.uuid, opts));
|
|
1337
1687
|
}
|
|
1338
1688
|
}
|
|
1339
1689
|
}));
|
|
1690
|
+
server.registerTool("convert_to_project", {
|
|
1691
|
+
description: "Promote a to-do into a project. This REPLACES the to-do with a new project (its notes " +
|
|
1692
|
+
"are kept); the to-do's identity is gone and it cannot be undone. Requires " +
|
|
1693
|
+
"dangerously_drive_gui. The new project's uuid is on the result. (To promote a HEADING, " +
|
|
1694
|
+
"use the heading tool's promote_heading action.)",
|
|
1695
|
+
inputSchema: {
|
|
1696
|
+
uuid: z.string().describe("the to-do's uuid"),
|
|
1697
|
+
...driveGuiShape,
|
|
1698
|
+
...dryRunShape,
|
|
1699
|
+
...opIdShape,
|
|
1700
|
+
},
|
|
1701
|
+
annotations: DESTRUCTIVE,
|
|
1702
|
+
}, async (args) => guard(async () => mutationResult(await getClient().write.run("todo.convert-to-project", { uuid: args.uuid }, writeOptions(args)))));
|
|
1340
1703
|
server.registerTool("clear_reminder", {
|
|
1341
1704
|
description: "Clear a to-do's time-of-day reminder while keeping its scheduled date. Uses the " +
|
|
1342
1705
|
"Things proxy shortcuts when installed (in place, and the only path for a repeating " +
|
|
@@ -1355,7 +1718,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1355
1718
|
"friday",
|
|
1356
1719
|
"saturday",
|
|
1357
1720
|
]);
|
|
1358
|
-
// The base rule (also used by
|
|
1721
|
+
// The base rule (also used by the repeat tool's create action, which stays minimal —
|
|
1359
1722
|
// its own `deadline` is the project's due DATE, not the repeat's Add-deadlines).
|
|
1360
1723
|
const baseRepeatShape = {
|
|
1361
1724
|
frequency: z.enum(["daily", "weekly", "monthly", "yearly"]).describe("How often it repeats"),
|
|
@@ -1432,39 +1795,36 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1432
1795
|
"visible occurrence), templateUuid (the rule), and replacedUuid. action reschedule: " +
|
|
1433
1796
|
"change a repeating item's rule in place, keeping the same item (undoable — it restores " +
|
|
1434
1797
|
"the previous rule). action pause/resume: stop or restart its new occurrences, keeping " +
|
|
1435
|
-
"the rule. action
|
|
1798
|
+
"the rule. action add (scope project only): create a project and make it repeating in " +
|
|
1436
1799
|
"one call — the project is created first and PERSISTS even if the make-repeating step " +
|
|
1437
1800
|
"refuses; give an area to place it or omit it to create in Someday (only frequency and " +
|
|
1438
1801
|
"interval are supported); returns the new project's uuid.",
|
|
1439
1802
|
inputSchema: {
|
|
1440
1803
|
scope: z.enum(["todo", "project"]),
|
|
1441
|
-
action: z.enum(["start", "reschedule", "pause", "resume", "
|
|
1804
|
+
action: z.enum(["start", "reschedule", "pause", "resume", "add"]),
|
|
1442
1805
|
uuid: z
|
|
1443
1806
|
.string()
|
|
1444
1807
|
.optional()
|
|
1445
1808
|
.describe("start/reschedule/pause/resume: the item (a project also accepts a unique name)"),
|
|
1446
|
-
title: z.string().optional().describe("
|
|
1447
|
-
notes: z.string().optional().describe("
|
|
1448
|
-
area: z.string().optional().describe(`
|
|
1809
|
+
title: z.string().optional().describe("add (project): the new project's title"),
|
|
1810
|
+
notes: z.string().optional().describe("add (project): notes"),
|
|
1811
|
+
area: z.string().optional().describe(`add (project): destination area (${REF_FORMAT})`),
|
|
1449
1812
|
project_deadline: z
|
|
1450
1813
|
.string()
|
|
1451
1814
|
.optional()
|
|
1452
|
-
.describe(`
|
|
1453
|
-
todos: z
|
|
1454
|
-
.array(z.string())
|
|
1455
|
-
.optional()
|
|
1456
|
-
.describe("create (project): initial child to-do titles"),
|
|
1815
|
+
.describe(`add (project): the project's due date — ${DATE_FORMAT}`),
|
|
1816
|
+
todos: z.array(z.string()).optional().describe("add (project): initial child to-do titles"),
|
|
1457
1817
|
frequency: z
|
|
1458
1818
|
.enum(["daily", "weekly", "monthly", "yearly"])
|
|
1459
1819
|
.optional()
|
|
1460
|
-
.describe("start/reschedule/
|
|
1820
|
+
.describe("start/reschedule/add: how often it repeats"),
|
|
1461
1821
|
interval: z
|
|
1462
1822
|
.number()
|
|
1463
1823
|
.int()
|
|
1464
1824
|
.min(1)
|
|
1465
1825
|
.max(99)
|
|
1466
1826
|
.optional()
|
|
1467
|
-
.describe("start/reschedule/
|
|
1827
|
+
.describe("start/reschedule/add: every N units (1–99)"),
|
|
1468
1828
|
after_completion: repeatRuleShape.after_completion,
|
|
1469
1829
|
weekdays: repeatRuleShape.weekdays,
|
|
1470
1830
|
monthly_day: repeatRuleShape.monthly_day,
|
|
@@ -1484,15 +1844,15 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1484
1844
|
const c = getClient();
|
|
1485
1845
|
const opts = writeOptions(args);
|
|
1486
1846
|
const { frequency, interval } = args;
|
|
1487
|
-
if (args.action === "
|
|
1847
|
+
if (args.action === "add") {
|
|
1488
1848
|
if (args.scope !== "project")
|
|
1489
|
-
return usage('action "
|
|
1849
|
+
return usage('action "add" requires scope "project"');
|
|
1490
1850
|
if (args.title === undefined)
|
|
1491
|
-
return usage('action "
|
|
1851
|
+
return usage('action "add" requires title');
|
|
1492
1852
|
if (frequency === undefined || interval === undefined) {
|
|
1493
|
-
return usage('action "
|
|
1853
|
+
return usage('action "add" requires frequency and interval');
|
|
1494
1854
|
}
|
|
1495
|
-
return mutationResult(await c.write.
|
|
1855
|
+
return mutationResult(await c.write.addRepeatingProject({
|
|
1496
1856
|
title: args.title,
|
|
1497
1857
|
...(args.notes !== undefined && { notes: args.notes }),
|
|
1498
1858
|
...(args.area !== undefined && { area: containerRef(args.area) }),
|
|
@@ -1533,7 +1893,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1533
1893
|
server.registerTool("duplicate_item", {
|
|
1534
1894
|
description: "Duplicate a to-do or project and return the copy's uuid; a duplicated project " +
|
|
1535
1895
|
"includes its children. Not available for repeating items.",
|
|
1536
|
-
inputSchema: { uuid: z.string(), ...dryRunShape },
|
|
1896
|
+
inputSchema: { uuid: z.string(), ...dryRunShape, ...opIdShape },
|
|
1537
1897
|
annotations: NON_DESTRUCTIVE,
|
|
1538
1898
|
}, async (args) => guard(async () => {
|
|
1539
1899
|
const c = getClient();
|
|
@@ -1554,6 +1914,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1554
1914
|
todos: z.array(z.string()).optional().describe("Initial child to-do titles"),
|
|
1555
1915
|
...tzShape,
|
|
1556
1916
|
...dryRunShape,
|
|
1917
|
+
...opIdShape,
|
|
1557
1918
|
},
|
|
1558
1919
|
annotations: NON_DESTRUCTIVE,
|
|
1559
1920
|
}, async (args) => guard(async () => {
|
|
@@ -1570,23 +1931,37 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1570
1931
|
}, writeOptions(args)));
|
|
1571
1932
|
}));
|
|
1572
1933
|
server.registerTool("move_project", {
|
|
1573
|
-
description: "Move
|
|
1574
|
-
"
|
|
1934
|
+
description: "Move one or more projects as an ordered block (spec §4/§5). Pass at most one " +
|
|
1935
|
+
"destination: to_area, or no_area (leave the area — a project's complete detach). To " +
|
|
1936
|
+
"REORDER projects among their siblings without changing area, pass a position " +
|
|
1937
|
+
"(first/last/before/after) and NO destination. An anchor positions but never migrates. " +
|
|
1938
|
+
"The project's status and schedule are unaffected; the result's placementClass states " +
|
|
1939
|
+
"whether top-of-bucket placement was guaranteed.",
|
|
1575
1940
|
inputSchema: {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1941
|
+
uuids: z.array(z.string()).describe(`The projects to move (${REF_FORMAT}), in order`),
|
|
1942
|
+
to_area: z.string().optional().describe(`Destination area (${REF_FORMAT})`),
|
|
1943
|
+
no_area: z.boolean().optional().describe("Leave the current area (a project's detach)"),
|
|
1944
|
+
...positionShape,
|
|
1579
1945
|
...dryRunShape,
|
|
1580
1946
|
},
|
|
1581
1947
|
annotations: NON_DESTRUCTIVE,
|
|
1582
1948
|
}, async (args) => guard(async () => {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1949
|
+
const dests = [];
|
|
1950
|
+
if (args.to_area !== undefined)
|
|
1951
|
+
dests.push({ kind: "area", ref: containerRef(args.to_area) });
|
|
1952
|
+
if (args.no_area === true)
|
|
1953
|
+
dests.push({ kind: "no-area" });
|
|
1954
|
+
if (dests.length > 1)
|
|
1955
|
+
return usage("pass at most one of to_area / no_area");
|
|
1956
|
+
const position = movePositionArgs(args);
|
|
1957
|
+
if (position === "conflict")
|
|
1958
|
+
return usage("pass at most one of first/last/before/after");
|
|
1959
|
+
const request = {
|
|
1960
|
+
uuids: args.uuids,
|
|
1961
|
+
...(dests[0] !== undefined && { destination: dests[0] }),
|
|
1962
|
+
...(position !== undefined && { position }),
|
|
1963
|
+
};
|
|
1964
|
+
return moveResult(await getClient().write.moveProjects(request, writeOptions(args)));
|
|
1590
1965
|
}));
|
|
1591
1966
|
// ----------------------------------------------------------------- areas
|
|
1592
1967
|
server.registerTool("add_area", {
|
|
@@ -1596,6 +1971,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1596
1971
|
tags: z.array(z.string()).optional().describe(`Tags — ${TAG_REF_FORMAT}`),
|
|
1597
1972
|
...createTagsShape,
|
|
1598
1973
|
...dryRunShape,
|
|
1974
|
+
...opIdShape,
|
|
1599
1975
|
},
|
|
1600
1976
|
annotations: NON_DESTRUCTIVE,
|
|
1601
1977
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addArea({ title: args.title, ...(args.tags !== undefined && { tags: args.tags }) }, writeOptions(args)))));
|
|
@@ -1606,6 +1982,7 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1606
1982
|
title: z.string(),
|
|
1607
1983
|
parent: z.string().optional().describe("Existing parent tag name"),
|
|
1608
1984
|
...dryRunShape,
|
|
1985
|
+
...opIdShape,
|
|
1609
1986
|
},
|
|
1610
1987
|
annotations: NON_DESTRUCTIVE,
|
|
1611
1988
|
}, async (args) => guard(async () => mutationResult(await getClient().write.addTag({ title: args.title, ...(args.parent !== undefined && { parent: args.parent }) }, writeOptions(args)))));
|
|
@@ -1629,25 +2006,48 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1629
2006
|
.boolean()
|
|
1630
2007
|
.optional()
|
|
1631
2008
|
.describe("Required for area/tag delete and trash.empty (PERMANENT, no Trash)"),
|
|
2009
|
+
allow_non_empty_area: z
|
|
2010
|
+
.boolean()
|
|
2011
|
+
.optional()
|
|
2012
|
+
.describe("area.delete: delete a non-empty area together with its contents"),
|
|
1632
2013
|
...dryRunShape,
|
|
2014
|
+
...opIdShape,
|
|
1633
2015
|
},
|
|
1634
2016
|
annotations: DESTRUCTIVE,
|
|
1635
2017
|
}, async (args) => guard(async () => mutationResult(await getClient().write.run(args.op, args.params, writeOptions(args)))));
|
|
1636
2018
|
server.registerTool("batch", {
|
|
1637
2019
|
description: "Run several operations in order, each independently — there are no transactions, " +
|
|
1638
2020
|
"and a failure does not roll back earlier operations. Per-operation results return " +
|
|
1639
|
-
"in order; fail_fast skips the remainder after the first failure."
|
|
2021
|
+
"in order; fail_fast skips the remainder after the first failure. " +
|
|
2022
|
+
"CHAINING: an operation that creates something may carry temp_id (a handle); a LATER " +
|
|
2023
|
+
'operation references that new uuid as "$handle" in any id/container field (dotted ' +
|
|
2024
|
+
'"$handle.instance"/"$handle.replaced" reach a repeating op\'s spawned instance / ' +
|
|
2025
|
+
"replaced source). A temp_id is valid only on a creating operation (not tag.add — " +
|
|
2026
|
+
"reference a tag by title) and unique per batch. IDEMPOTENCY: op_id makes resubmission " +
|
|
2027
|
+
"safe — an operation matching an earlier success is reported already-applied, not " +
|
|
2028
|
+
"re-created. The result adds temp_id_mapping (handle → uuid) and undo_token, which " +
|
|
2029
|
+
"reverses the whole batch as one unit via the undo tool.",
|
|
1640
2030
|
inputSchema: {
|
|
1641
2031
|
ops: z
|
|
1642
2032
|
.array(z.object({
|
|
1643
2033
|
op: z.enum(OPERATION_KINDS),
|
|
1644
2034
|
params: z.record(z.string(), z.unknown()),
|
|
2035
|
+
temp_id: z
|
|
2036
|
+
.string()
|
|
2037
|
+
.optional()
|
|
2038
|
+
.describe('Handle for this op\'s new uuid, referenced later as "$handle"'),
|
|
2039
|
+
op_id: z
|
|
2040
|
+
.string()
|
|
2041
|
+
.optional()
|
|
2042
|
+
.describe("Idempotency id — a resubmitted op matching an earlier success is skipped"),
|
|
1645
2043
|
options: z
|
|
1646
2044
|
.object({
|
|
1647
2045
|
acknowledge_checklist_reset: z.boolean().optional(),
|
|
1648
2046
|
acknowledge_project_reopen: z.boolean().optional(),
|
|
1649
2047
|
dangerously_permanent: z.boolean().optional(),
|
|
1650
2048
|
acknowledge_tag_subtree: z.boolean().optional(),
|
|
2049
|
+
allow_non_empty_area: z.boolean().optional(),
|
|
2050
|
+
dangerously_drive_gui: z.boolean().optional(),
|
|
1651
2051
|
})
|
|
1652
2052
|
.optional(),
|
|
1653
2053
|
}))
|
|
@@ -1668,91 +2068,120 @@ export function createThingsMcpServer(options = {}) {
|
|
|
1668
2068
|
...(o?.acknowledge_project_reopen === true && { acknowledgeProjectReopen: true }),
|
|
1669
2069
|
...(o?.dangerously_permanent === true && { dangerouslyPermanent: true }),
|
|
1670
2070
|
...(o?.acknowledge_tag_subtree === true && { acknowledgeTagSubtree: true }),
|
|
2071
|
+
...(o?.allow_non_empty_area === true && { allowNonEmptyArea: true }),
|
|
2072
|
+
...(o?.dangerously_drive_gui === true && { dangerouslyDriveGui: true }),
|
|
1671
2073
|
...(ceiling !== undefined && { maxDisruption: ceiling }),
|
|
1672
2074
|
};
|
|
1673
2075
|
return {
|
|
1674
2076
|
op: op.op,
|
|
1675
2077
|
params: op.params,
|
|
2078
|
+
...(op.temp_id !== undefined && { tempId: op.temp_id }),
|
|
2079
|
+
...(op.op_id !== undefined && { opId: op.op_id }),
|
|
1676
2080
|
...(Object.keys(opts).length > 0 && { options: opts }),
|
|
1677
2081
|
};
|
|
1678
2082
|
});
|
|
1679
|
-
const
|
|
2083
|
+
const batchResult = await getClient().write.batch(ops, {
|
|
1680
2084
|
...(args.dry_run === true && { dryRun: true }),
|
|
1681
2085
|
...(args.fail_fast === true && { failFast: true }),
|
|
1682
2086
|
actor: mcpActor(),
|
|
1683
2087
|
});
|
|
1684
|
-
|
|
2088
|
+
// First block: the per-op results, each FLATTENED to the wire shape (a
|
|
2089
|
+
// string `outcome` with the variant fields hoisted — parity with the CLI
|
|
2090
|
+
// JSONL stream, docs/contract.md). Second block (additive): the batch
|
|
2091
|
+
// chaining/undo summary, present only when the batch minted a token or
|
|
2092
|
+
// bound temp ids.
|
|
2093
|
+
const lines = batchResult.results.map(flattenBatchLine);
|
|
2094
|
+
const hasSummary = batchResult.undoToken !== undefined || Object.keys(batchResult.tempIdMapping).length > 0;
|
|
2095
|
+
if (!hasSummary)
|
|
2096
|
+
return jsonResult(lines);
|
|
2097
|
+
return {
|
|
2098
|
+
content: [
|
|
2099
|
+
{ type: "text", text: JSON.stringify(lines) },
|
|
2100
|
+
{
|
|
2101
|
+
type: "text",
|
|
2102
|
+
text: JSON.stringify({
|
|
2103
|
+
tempIdMapping: batchResult.tempIdMapping,
|
|
2104
|
+
...(batchResult.undoToken !== undefined && { undoToken: batchResult.undoToken }),
|
|
2105
|
+
}),
|
|
2106
|
+
},
|
|
2107
|
+
],
|
|
2108
|
+
};
|
|
1685
2109
|
}));
|
|
1686
2110
|
server.registerTool("reorder", {
|
|
1687
|
-
description: "
|
|
1688
|
-
"
|
|
1689
|
-
"
|
|
1690
|
-
"
|
|
1691
|
-
"
|
|
1692
|
-
"
|
|
1693
|
-
"Today
|
|
1694
|
-
"
|
|
1695
|
-
|
|
1696
|
-
"
|
|
1697
|
-
"
|
|
1698
|
-
"
|
|
1699
|
-
"
|
|
2111
|
+
description: "Rearrange to-dos IN PLACE within the list or container and the bucket they already " +
|
|
2112
|
+
"share — this REARRANGES, never changes what an item belongs to (to change membership " +
|
|
2113
|
+
"use move_todo / move_project). The refs order is the resulting order; unmentioned " +
|
|
2114
|
+
"siblings keep theirs. Bare (no position) assembles the named items as a block at the " +
|
|
2115
|
+
"EARLIEST one's current slot (partial-selection friendly); first/last/before/after " +
|
|
2116
|
+
"position the block instead. Refs that span different containers or buckets are refused. " +
|
|
2117
|
+
"A Today or This Evening member also holds a slot in its own container, so a set that is " +
|
|
2118
|
+
"coherent on BOTH axes is ambiguous — pass `in` to name the axis (the refusal names both " +
|
|
2119
|
+
"choices). The project rows the Today/Evening/day lists intermix with to-dos may be " +
|
|
2120
|
+
"reordered alongside them. Ordering the Today, Inbox, or Someday lists, a project's " +
|
|
2121
|
+
"to-dos, or an area must first be enabled once via `things config set allow-experimental " +
|
|
2122
|
+
"true`. To reorder a project's HEADINGS (children follow) use the heading tool's " +
|
|
2123
|
+
"move_heading action; to reorder sidebar AREAS use reorder_areas.",
|
|
1700
2124
|
inputSchema: {
|
|
1701
|
-
|
|
1702
|
-
"today",
|
|
1703
|
-
"evening",
|
|
1704
|
-
"inbox",
|
|
1705
|
-
"someday",
|
|
1706
|
-
"project",
|
|
1707
|
-
"headings",
|
|
1708
|
-
"area",
|
|
1709
|
-
"projects",
|
|
1710
|
-
"areas",
|
|
1711
|
-
]),
|
|
1712
|
-
container: z
|
|
1713
|
-
.string()
|
|
1714
|
-
.optional()
|
|
1715
|
-
.describe(`Project/area (${REF_FORMAT}) — required for those scopes`),
|
|
1716
|
-
uuids: z
|
|
2125
|
+
refs: z
|
|
1717
2126
|
.array(z.string())
|
|
2127
|
+
.describe("The items to rearrange, in the order they should land (may be a subset)"),
|
|
2128
|
+
...positionShape,
|
|
2129
|
+
in: z
|
|
2130
|
+
.string()
|
|
1718
2131
|
.optional()
|
|
1719
|
-
.describe("
|
|
1720
|
-
|
|
1721
|
-
|
|
2132
|
+
.describe("Disambiguate the axis of a Today/This Evening set: today | evening | anytime | " +
|
|
2133
|
+
"someday | inbox, or a project/area/heading ref (uuid or unique title)"),
|
|
2134
|
+
...dryRunShape,
|
|
2135
|
+
},
|
|
2136
|
+
annotations: NON_DESTRUCTIVE,
|
|
2137
|
+
}, async (args) => guard(async () => {
|
|
2138
|
+
const position = movePositionArgs(args);
|
|
2139
|
+
if (position === "conflict")
|
|
2140
|
+
return usage("pass at most one of first/last/before/after");
|
|
2141
|
+
const request = {
|
|
2142
|
+
uuids: args.refs,
|
|
2143
|
+
...(position !== undefined && { position }),
|
|
2144
|
+
...(args.in !== undefined && { in: args.in }),
|
|
2145
|
+
};
|
|
2146
|
+
return moveResult(await getClient().write.reorderTodos(request, writeOptions(args)));
|
|
2147
|
+
}));
|
|
2148
|
+
server.registerTool("reorder_areas", {
|
|
2149
|
+
description: "Move a sidebar area to a new position in the area order (target by uuid or unique " +
|
|
2150
|
+
"name). Pass exactly one destination: before/after another area, or first/last. This " +
|
|
2151
|
+
"visibly drives the local Things app (the window comes forward and the sidebar may " +
|
|
2152
|
+
"scroll) and must be turned on first with `things config set ui-enabled true`; the " +
|
|
2153
|
+
"area's projects and to-dos are untouched.",
|
|
2154
|
+
inputSchema: {
|
|
2155
|
+
target: z.string().describe(`the area to move (${REF_FORMAT})`),
|
|
1722
2156
|
before: z
|
|
1723
2157
|
.string()
|
|
1724
2158
|
.optional()
|
|
1725
|
-
.describe(`
|
|
2159
|
+
.describe(`place it immediately above this area (${REF_FORMAT})`),
|
|
1726
2160
|
after: z
|
|
1727
2161
|
.string()
|
|
1728
2162
|
.optional()
|
|
1729
|
-
.describe(`
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
.optional()
|
|
1733
|
-
.describe("scope areas: move it to the top or bottom of the area list"),
|
|
2163
|
+
.describe(`place it immediately below this area (${REF_FORMAT})`),
|
|
2164
|
+
first: z.boolean().optional().describe("move it to the top of the area list"),
|
|
2165
|
+
last: z.boolean().optional().describe("move it to the bottom of the area list"),
|
|
1734
2166
|
...driveGuiShape,
|
|
1735
2167
|
...dryRunShape,
|
|
1736
2168
|
},
|
|
1737
2169
|
annotations: NON_DESTRUCTIVE,
|
|
1738
2170
|
}, async (args) => guard(async () => {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
uuids: args.uuids,
|
|
1754
|
-
...(args.container !== undefined && { container: containerRef(args.container) }),
|
|
1755
|
-
...(args.strategy !== undefined && { strategy: args.strategy }),
|
|
2171
|
+
const chosen = [
|
|
2172
|
+
args.before !== undefined,
|
|
2173
|
+
args.after !== undefined,
|
|
2174
|
+
args.first === true,
|
|
2175
|
+
args.last === true,
|
|
2176
|
+
].filter(Boolean).length;
|
|
2177
|
+
if (chosen !== 1)
|
|
2178
|
+
return usage("pass exactly one of before / after / first / last");
|
|
2179
|
+
return mutationResult(await getClient().write.run("area.reorder", {
|
|
2180
|
+
target: args.target,
|
|
2181
|
+
...(args.before !== undefined && { before: args.before }),
|
|
2182
|
+
...(args.after !== undefined && { after: args.after }),
|
|
2183
|
+
...(args.first === true && { position: "first" }),
|
|
2184
|
+
...(args.last === true && { position: "last" }),
|
|
1756
2185
|
}, writeOptions(args)));
|
|
1757
2186
|
}));
|
|
1758
2187
|
server.registerTool("undo", {
|