things-api 0.10.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 +50 -13
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/clock.d.ts +8 -0
- package/dist/cli/clock.js +24 -0
- package/dist/cli/clock.js.map +1 -0
- package/dist/cli/commands/area.d.ts +18 -11
- package/dist/cli/commands/area.js +61 -74
- 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 +60 -0
- package/dist/cli/commands/install-skill.js +256 -0
- package/dist/cli/commands/install-skill.js.map +1 -0
- 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 +27 -34
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +15 -2
- package/dist/cli/commands/reads.js +144 -44
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +7 -2
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +38 -14
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +15 -9
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +797 -174
- 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 +32 -2
- package/dist/cli/glyphs.js +83 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +121 -16
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +33 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +46 -0
- package/dist/cli/move-hint.js +196 -0
- package/dist/cli/move-hint.js.map +1 -0
- package/dist/cli/period.d.ts +2 -8
- package/dist/cli/period.js +44 -30
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +43 -5
- package/dist/cli/read-driver.js +91 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +19 -8
- package/dist/cli/render.js +70 -27
- 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 +27 -0
- package/dist/cli/skill-check.js +99 -0
- package/dist/cli/skill-check.js.map +1 -0
- package/dist/cli/skill.d.ts +70 -0
- package/dist/cli/skill.js +152 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +6 -2
- package/dist/cli/tag-filters.js +8 -6
- package/dist/cli/tag-filters.js.map +1 -1
- package/dist/cli/verb-hint.js +28 -3
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/cli/version.d.ts +4 -0
- package/dist/cli/version.js +18 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/client.d.ts +171 -27
- package/dist/client.js +279 -40
- 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 +186 -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 +25 -9
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +1157 -684
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/clock.d.ts +61 -0
- package/dist/model/clock.js +71 -0
- package/dist/model/clock.js.map +1 -0
- package/dist/model/dates.d.ts +26 -3
- package/dist/model/dates.js +84 -3
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +60 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +4 -2
- package/dist/model/mappers.js +66 -9
- 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 +12 -12
- package/dist/read/area-view.js +58 -34
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.d.ts +1 -1
- package/dist/read/detail.js +26 -12
- package/dist/read/detail.js.map +1 -1
- package/dist/read/log-boundary.d.ts +1 -1
- package/dist/read/log-boundary.js +13 -3
- package/dist/read/log-boundary.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 +32 -16
- package/dist/read/project-view.js +80 -45
- 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 +108 -19
- package/dist/read/queries.js +212 -35
- 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/snapshot.d.ts +1 -1
- package/dist/read/snapshot.js +9 -2
- package/dist/read/snapshot.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 +8 -6
- package/dist/read/truncation.js +35 -10
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +61 -20
- package/dist/read/views.js +240 -82
- 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/accessibility-probe.d.ts +1 -1
- package/dist/write/accessibility-probe.js +8 -0
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.d.ts +1 -1
- package/dist/write/automation-probe.js +8 -0
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -70
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +1 -1
- package/dist/write/commands.js +316 -66
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +94 -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 +122 -3
- package/dist/write/pipeline.js +309 -52
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.js +8 -1
- package/dist/write/planner.js.map +1 -1
- package/dist/write/pre-state.d.ts +197 -6
- package/dist/write/pre-state.js +427 -20
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/recurrence-rule-blob.d.ts +65 -0
- package/dist/write/recurrence-rule-blob.js +150 -0
- package/dist/write/recurrence-rule-blob.js.map +1 -0
- 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/registry.d.ts +5 -7
- package/dist/write/vectors/registry.js +49 -2
- package/dist/write/vectors/registry.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.d.ts +44 -0
- package/dist/write/vectors/simulator.js +1192 -0
- package/dist/write/vectors/simulator.js.map +1 -0
- package/dist/write/vectors/types.d.ts +46 -0
- package/dist/write/vectors/ui-certification.d.ts +10 -4
- package/dist/write/vectors/ui-certification.js +43 -10
- 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 +34 -1
- package/dist/write/vectors/ui-recipes.js +217 -6
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +40 -1
- package/dist/write/vectors/ui.js +196 -39
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +127 -2
- package/dist/write/verify/delta.js +255 -16
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +7 -1
- package/dist/write/verify/poller.js +20 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +24 -3
- package/schema/envelope.schema.json +383 -0
- package/skills/things-cli/SKILL.md +70 -0
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +62 -0
- package/skills/things-cli/references/errors.md +49 -0
- package/skills/things-cli/references/gui.md +11 -0
- package/skills/things-cli/references/model.md +60 -0
- package/skills/things-cli/references/ordering.md +71 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://github.com/mikegreiling/things-api/schema/envelope.schema.json",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"$ref": "#/definitions/WireEnvelope",
|
|
5
|
+
"definitions": {
|
|
6
|
+
"WireEnvelope": {
|
|
7
|
+
"anyOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "#/definitions/WireOkEnvelope"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "#/definitions/ErrorEnvelope"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"description": "The machine-readable ROOT the envelope JSON Schema is generated from (`schema/envelope.schema.json`, produced by `npm run schema:gen`): the discriminated union — on the boolean `ok` — of a successful envelope and an {@link ErrorEnvelope } , exactly as written to stdout by the CLI `--json` surface and inherited by the MCP server. This is the same grammar docs/contract.md describes in prose; the schema is its generated, testable rendering. NB: the streaming commands (`batch`, `undo`) emit JSON Lines, a documented exception NOT covered by this envelope type."
|
|
16
|
+
},
|
|
17
|
+
"WireOkEnvelope": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"apiVersion": {
|
|
21
|
+
"type": "number",
|
|
22
|
+
"const": 1
|
|
23
|
+
},
|
|
24
|
+
"ok": {
|
|
25
|
+
"type": "boolean",
|
|
26
|
+
"const": true
|
|
27
|
+
},
|
|
28
|
+
"kind": {
|
|
29
|
+
"$ref": "#/definitions/WireOkKind",
|
|
30
|
+
"description": "The payload class — see {@link WireOkKind } ."
|
|
31
|
+
},
|
|
32
|
+
"data": {
|
|
33
|
+
"$ref": "#/definitions/WireData",
|
|
34
|
+
"description": "Command-specific payload; open at the schema layer (see {@link WireData } )."
|
|
35
|
+
},
|
|
36
|
+
"meta": {
|
|
37
|
+
"$ref": "#/definitions/EnvelopeMeta"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": ["apiVersion", "ok", "kind", "data", "meta"],
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"description": "A successful `--json` envelope, exactly as emitted on stdout."
|
|
43
|
+
},
|
|
44
|
+
"WireOkKind": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": [
|
|
47
|
+
"inbox",
|
|
48
|
+
"upcoming",
|
|
49
|
+
"logbook",
|
|
50
|
+
"trash",
|
|
51
|
+
"changes",
|
|
52
|
+
"search",
|
|
53
|
+
"projects",
|
|
54
|
+
"areas",
|
|
55
|
+
"tags",
|
|
56
|
+
"today",
|
|
57
|
+
"anytime",
|
|
58
|
+
"someday",
|
|
59
|
+
"area-view",
|
|
60
|
+
"project-view",
|
|
61
|
+
"detail",
|
|
62
|
+
"mutation-result",
|
|
63
|
+
"move-result",
|
|
64
|
+
"mutation-plan",
|
|
65
|
+
"move-plan",
|
|
66
|
+
"project-reopen",
|
|
67
|
+
"doctor",
|
|
68
|
+
"capabilities",
|
|
69
|
+
"config",
|
|
70
|
+
"legend",
|
|
71
|
+
"setup-shortcuts",
|
|
72
|
+
"install-skill"
|
|
73
|
+
],
|
|
74
|
+
"description": "The envelope `kind` discriminators a SUCCESSFUL response carries today (the error envelope's `kind` is always the literal `\"error\"`). This union is the schema's record of the known payload classes — the flat-list reads (`data.items`), the sectioned reads (`data.sections`), the composite cards (`data.view`), the single-entity detail (`data.item`), the mutations/plans (flat `data` fields), and the diagnostic payloads.\n\nPer the compatibility covenant (docs/contract.md) a consumer MUST tolerate an UNKNOWN `kind`: adding a new kind is additive / non-breaking. So this union is \"the kinds known to THIS build\", not a closed set a generic reader may assume complete — the JSON Schema pins it as an enum so drift is caught, but a forward-compatible consumer routes on the wrapper it finds in `data`."
|
|
75
|
+
},
|
|
76
|
+
"WireData": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": {},
|
|
79
|
+
"description": "The `data` payload of a successful envelope. Its CONCRETE shape is command-specific — one of the R1/R2 read wrappers (`item` | `view` | `items` | `sections`) or a mutation's flat result fields — and, crucially, entity payloads are omit-empty pruned on the wire (docs/design/contracts.md).\n\nCOVERAGE BOUNDARY: this schema does NOT model the per-kind payload shapes. It pins the ENVELOPE layer exactly (`apiVersion`, `ok`, `kind`, `meta`, and the whole `error` object) and treats `data` as an open JSON object. Fully typing every kind's payload against the omit-empty wire shape is a separate, larger effort; until then `src/contracts.ts` and `docs/contract.md` remain the authoritative description of what each `kind`'s `data` contains."
|
|
80
|
+
},
|
|
81
|
+
"EnvelopeMeta": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"dbVersion": {
|
|
85
|
+
"type": ["number", "null"],
|
|
86
|
+
"description": "Things database schema version (`Meta.databaseVersion`), null when no DB was opened."
|
|
87
|
+
},
|
|
88
|
+
"fingerprint": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"enum": ["ok", "drift", "user-accepted", "unknown"],
|
|
91
|
+
"description": "Schema fingerprint status at the time of the command."
|
|
92
|
+
},
|
|
93
|
+
"elapsedMs": {
|
|
94
|
+
"type": "number",
|
|
95
|
+
"description": "Wall-clock duration of the command in milliseconds."
|
|
96
|
+
},
|
|
97
|
+
"truncation": {
|
|
98
|
+
"$ref": "#/definitions/Truncation",
|
|
99
|
+
"description": "The read's completeness metadata (the single truncation shape). Present on any read that could drop rows — flat views (row `limit`), the Today split (`sections`), and grouped views (anytime/someday/`area show`, `blocks`). `meta.truncation.truncated` is the universal \"did I see everything\" check."
|
|
100
|
+
},
|
|
101
|
+
"resolvedCommand": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "The canonical `things …` command a sugar invocation normalized to (bare noun, keyword-in-show, uuid/share-link routing). Present only on routed reads reached via a sugar form; absent for canonical invocations."
|
|
104
|
+
},
|
|
105
|
+
"warnings": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
},
|
|
110
|
+
"description": "Non-blocking advisories about this read (ADDITIVE). Present only when there is something to flag — currently a one-line note when the Things database schema no longer matches the version this build was validated against (reads stay best-effort; run `things doctor`). Absent means none."
|
|
111
|
+
},
|
|
112
|
+
"clock": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"timezone": {
|
|
116
|
+
"type": "string"
|
|
117
|
+
},
|
|
118
|
+
"today": {
|
|
119
|
+
"type": "string"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"required": ["timezone", "today"],
|
|
123
|
+
"additionalProperties": false,
|
|
124
|
+
"description": "The effective clock this response's date boundaries were computed for (ADDITIVE honesty field). Present ONLY when a consumer zone (`THINGS_TZ` / the MCP `tz` argument) or a pinned `THINGS_NOW` is in effect; ABSENT on the host clock, so the wire shape is unchanged for ordinary consumers."
|
|
125
|
+
},
|
|
126
|
+
"filter": {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"properties": {
|
|
129
|
+
"area": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"properties": {
|
|
132
|
+
"uuid": {
|
|
133
|
+
"type": "string"
|
|
134
|
+
},
|
|
135
|
+
"title": {
|
|
136
|
+
"type": "string"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"required": ["uuid", "title"],
|
|
140
|
+
"additionalProperties": false
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"required": ["area"],
|
|
144
|
+
"additionalProperties": false,
|
|
145
|
+
"description": "The active content filter this response was scoped to (ADDITIVE). Present ONLY when a scope was applied — currently `area` (the `--area` view filter), carrying the resolved area's uuid + title; ABSENT otherwise, so the wire shape is unchanged for unscoped reads (the `meta.clock` precedent)."
|
|
146
|
+
},
|
|
147
|
+
"scope": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"kind": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"enum": ["area", "project"]
|
|
153
|
+
},
|
|
154
|
+
"uuid": {
|
|
155
|
+
"type": "string"
|
|
156
|
+
},
|
|
157
|
+
"title": {
|
|
158
|
+
"type": "string"
|
|
159
|
+
},
|
|
160
|
+
"source": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"enum": ["flag", "env", "config"]
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"required": ["kind", "uuid", "title", "source"],
|
|
166
|
+
"additionalProperties": false,
|
|
167
|
+
"description": "The active container scope this response was jailed to (ADDITIVE). Present ONLY when a scope is in force (the MCP `--scope` flag / `THINGS_API_SCOPE` / a stored `scope`), naming the container + where the scope came from; ABSENT otherwise, so the wire shape is unchanged for unscoped reads. Lets an agent know its own jail — which is not an oracle for what lies outside it (the `meta.clock` / `meta.filter` additive precedent)."
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"required": ["dbVersion", "fingerprint", "elapsedMs"],
|
|
171
|
+
"additionalProperties": false
|
|
172
|
+
},
|
|
173
|
+
"Truncation": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": {
|
|
176
|
+
"shown": {
|
|
177
|
+
"type": "number"
|
|
178
|
+
},
|
|
179
|
+
"total": {
|
|
180
|
+
"type": "number"
|
|
181
|
+
},
|
|
182
|
+
"limit": {
|
|
183
|
+
"type": ["number", "null"]
|
|
184
|
+
},
|
|
185
|
+
"truncated": {
|
|
186
|
+
"type": "boolean"
|
|
187
|
+
},
|
|
188
|
+
"sections": {
|
|
189
|
+
"type": "array",
|
|
190
|
+
"items": {
|
|
191
|
+
"$ref": "#/definitions/SectionCount"
|
|
192
|
+
},
|
|
193
|
+
"description": "Per-section shown/total breakdown for a split flat view (Today); absent otherwise."
|
|
194
|
+
},
|
|
195
|
+
"blocks": {
|
|
196
|
+
"type": "array",
|
|
197
|
+
"items": {
|
|
198
|
+
"$ref": "#/definitions/GroupBlock"
|
|
199
|
+
},
|
|
200
|
+
"description": "Per-block nesting for a grouped view (anytime/someday/area card); absent otherwise."
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"required": ["shown", "total", "limit", "truncated"],
|
|
204
|
+
"additionalProperties": false,
|
|
205
|
+
"description": "The single truncation-metadata shape for every read (the `meta.truncation` field). `shown` items were returned of `total` that matched after all filters; `limit` is the effective cap (null when the caller asked for all rows, and always null on a grouped view whose caps are per-block); `truncated` is the UNIVERSAL completeness check — true exactly when anything was dropped (`shown < total`, or any block hid rows). The dropped remainder is `total - shown`.\n\nTwo optional per-shape breakdowns hang off it: `sections` for a split flat view (the Today split — per render-section shown/total), and `blocks` for a grouped view (anytime/someday/`area show` — the identity-carrying per-block nesting). Exactly one of them is present on the shapes that have it; both are absent on a plain flat view."
|
|
206
|
+
},
|
|
207
|
+
"SectionCount": {
|
|
208
|
+
"type": "object",
|
|
209
|
+
"properties": {
|
|
210
|
+
"key": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"enum": ["today", "evening"]
|
|
213
|
+
},
|
|
214
|
+
"shown": {
|
|
215
|
+
"type": "number"
|
|
216
|
+
},
|
|
217
|
+
"total": {
|
|
218
|
+
"type": "number"
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
"required": ["key", "shown", "total"],
|
|
222
|
+
"additionalProperties": false,
|
|
223
|
+
"description": "Per-section counts for a SPLIT flat view — currently only `today`'s Today / This-Evening split. One entry per section IN RENDER ORDER, each reporting the rows `shown` of the `total` that matched after all filters. Lets a renderer keep a downstream section (This Evening) honest under a single global cap without a pre-truncation copy of the view."
|
|
224
|
+
},
|
|
225
|
+
"GroupBlock": {
|
|
226
|
+
"type": "object",
|
|
227
|
+
"properties": {
|
|
228
|
+
"kind": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"enum": ["loose", "area", "project", "projects"]
|
|
231
|
+
},
|
|
232
|
+
"ref": {
|
|
233
|
+
"type": ["string", "null"],
|
|
234
|
+
"description": "Container reference (area or project uuid); null for the loose block."
|
|
235
|
+
},
|
|
236
|
+
"title": {
|
|
237
|
+
"type": ["string", "null"],
|
|
238
|
+
"description": "Container title; null for the loose block."
|
|
239
|
+
},
|
|
240
|
+
"shown": {
|
|
241
|
+
"type": "number"
|
|
242
|
+
},
|
|
243
|
+
"total": {
|
|
244
|
+
"type": "number"
|
|
245
|
+
},
|
|
246
|
+
"limit": {
|
|
247
|
+
"type": ["number", "null"],
|
|
248
|
+
"description": "The cap that applied to THIS block (null = uncapped)."
|
|
249
|
+
},
|
|
250
|
+
"totalProjects": {
|
|
251
|
+
"type": "number",
|
|
252
|
+
"description": "Type split for blocks that mix project rows and to-dos (someday's loose/area blocks; projects always list first, so the hidden split is `totalProjects - min(shown, totalProjects)` projects, remainder to-dos). Absent on single-type blocks."
|
|
253
|
+
},
|
|
254
|
+
"totalTodos": {
|
|
255
|
+
"type": "number"
|
|
256
|
+
},
|
|
257
|
+
"children": {
|
|
258
|
+
"type": "array",
|
|
259
|
+
"items": {
|
|
260
|
+
"$ref": "#/definitions/GroupBlock"
|
|
261
|
+
},
|
|
262
|
+
"description": "Nested project blocks (anytime item-lists / someday active-project groups). Absent when none."
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"required": ["kind", "ref", "title", "shown", "total", "limit"],
|
|
266
|
+
"additionalProperties": false,
|
|
267
|
+
"description": "One identity-carrying block of a grouped catalogue (anytime/someday) or a sectioned detail view (`area show`). Every header/section is always rendered; only the innermost item lists are capped. Emitted for every block that has rows to cap (`total > 0`) — including a block whose rows were ALL dropped (`shown: 0`), so no truncated header is untraceable. A block with no cappable rows of its own (`total: 0`) is omitted UNLESS it wraps truncated `children` — an area whose only capped content is its project item-lists still appears as their container.\n\nBlocks are NESTED: an area/loose block carries its project blocks in `children` — in anytime the project item-lists inside the area, in someday the active-project child groups found in that section. The `area show` `projects`/`area` blocks are siblings of one area and stay top-level."
|
|
268
|
+
},
|
|
269
|
+
"ErrorEnvelope": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"apiVersion": {
|
|
273
|
+
"type": "number",
|
|
274
|
+
"const": 1
|
|
275
|
+
},
|
|
276
|
+
"ok": {
|
|
277
|
+
"type": "boolean",
|
|
278
|
+
"const": false
|
|
279
|
+
},
|
|
280
|
+
"kind": {
|
|
281
|
+
"type": "string",
|
|
282
|
+
"const": "error"
|
|
283
|
+
},
|
|
284
|
+
"error": {
|
|
285
|
+
"type": "object",
|
|
286
|
+
"properties": {
|
|
287
|
+
"code": {
|
|
288
|
+
"$ref": "#/definitions/ErrorCode",
|
|
289
|
+
"description": "Stable machine-readable code from the {@link ErrorCode } registry (mirrors the exit-code family, e.g. \"verify-failed\", \"blocked:H-UNKNOWN-TAG\")."
|
|
290
|
+
},
|
|
291
|
+
"message": {
|
|
292
|
+
"type": "string"
|
|
293
|
+
},
|
|
294
|
+
"likelyCause": {
|
|
295
|
+
"type": "string",
|
|
296
|
+
"description": "Advisory attribution when failure signals point somewhere: e.g. \"permission-denied\", \"permission-pending\", \"feature-disabled\", \"app-updated\", \"schema-drift\", \"app-behavior-change\"."
|
|
297
|
+
},
|
|
298
|
+
"remediation": {
|
|
299
|
+
"type": "string",
|
|
300
|
+
"description": "Actionable next step for the caller, when one exists."
|
|
301
|
+
},
|
|
302
|
+
"detail": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"properties": {
|
|
305
|
+
"expected": {},
|
|
306
|
+
"observed": {
|
|
307
|
+
"anyOf": [
|
|
308
|
+
{
|
|
309
|
+
"type": "object",
|
|
310
|
+
"additionalProperties": {}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
"type": "null"
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
"candidates": {
|
|
318
|
+
"type": "array",
|
|
319
|
+
"items": {}
|
|
320
|
+
},
|
|
321
|
+
"suggestions": {
|
|
322
|
+
"type": "array",
|
|
323
|
+
"items": {
|
|
324
|
+
"type": "string"
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
"considered": {
|
|
328
|
+
"type": "array",
|
|
329
|
+
"items": {
|
|
330
|
+
"type": "object",
|
|
331
|
+
"properties": {
|
|
332
|
+
"vector": {
|
|
333
|
+
"type": "string"
|
|
334
|
+
},
|
|
335
|
+
"why": {
|
|
336
|
+
"type": "string"
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
"required": ["vector", "why"],
|
|
340
|
+
"additionalProperties": false
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
"placed": {
|
|
344
|
+
"type": "array",
|
|
345
|
+
"items": {
|
|
346
|
+
"type": "string"
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
"remaining": {
|
|
350
|
+
"type": "array",
|
|
351
|
+
"items": {
|
|
352
|
+
"type": "string"
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
"cause": {},
|
|
356
|
+
"failed": {},
|
|
357
|
+
"completed": {
|
|
358
|
+
"type": "array",
|
|
359
|
+
"items": {}
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
"additionalProperties": false,
|
|
363
|
+
"description": "The SINGLE structured, machine-readable failure-context object (there is no separate `details` field — the two were reconciled into this one). Each key is present only for the failures that produce it: - `expected` / `observed` — a verify-failed mutation's expected delta and the observed post-write state. - `candidates` — the disambiguation list for a not-found / ambiguous resolution (or a show/bare-noun did-you-mean): the ONE fixed, flag- invariant candidate shape (`CandidateRef`; see docs/contract.md), a LIVE-scoped pool capped at 8, so an agent can self-correct without another round-trip. - `suggestions` — for a bare mutation verb (`things update <ref>`), the concrete namespaced command(s) to run instead. - `considered` — the vectors weighed (and why each was rejected) for an unsupported operation. - `placed` / `remaining` / `cause` — a bounce reorder that aborted part-way: the items already placed, those not yet placed, and the cause. - `failed` / `completed` — a multi-leg move that failed mid-way: the leg that failed and the legs already completed before it."
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
"required": ["code", "message"],
|
|
367
|
+
"additionalProperties": false
|
|
368
|
+
},
|
|
369
|
+
"meta": {
|
|
370
|
+
"$ref": "#/definitions/EnvelopeMeta"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
"required": ["apiVersion", "ok", "kind", "error", "meta"],
|
|
374
|
+
"additionalProperties": false
|
|
375
|
+
},
|
|
376
|
+
"ErrorCode": {
|
|
377
|
+
"type": "string",
|
|
378
|
+
"description": "The registry of stable machine-readable error codes an error envelope can carry — the compiler IS the registry. Every `error.code` value the surfaces emit is a member of this union, and the human-readable meaning of each is frozen at v1.0 (new codes may still be ADDED after v1.0 — that is non-breaking; a documented code's MEANING never changes). The canonical per-code table (meaning + the `detail` keys each may carry) lives in `docs/contract.md` (The error-code registry).\n\nTwo members are template-literal families rather than fixed strings, because their suffix is minted in the write layer and the core deliberately never depends on it (see {@link blockedCode } / {@link verifyFailedCode } ): - `verify-failed:${reason}` — a single mutation executed but read-after-write verification failed; the suffix is the reason (`timeout` | `mismatch` | `silent-noop`). The bare `verify-failed` (no suffix) is the multi-leg move/reorder failure. - `blocked:${suffix}` — a mutation refused before touching the app; the suffix is the specific hazard id (`H-…`) when one is named, else the block reason (`drift` | `disruption-tier` | `lock` | `environment` | `clock` | `scope`). The bare `blocked` is a policy refusal from the move planner.\n\n`blocked:drift` maps to exit code 5 (DriftBlocked); every other `blocked:*` maps to exit code 4 (Blocked)."
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
"title": "things-api envelope",
|
|
382
|
+
"description": "JSON Schema for the things-api --json response envelope (the discriminated union of a success envelope and an error envelope), generated from the WireEnvelope type in src/contracts.ts. See docs/contract.md (The machine-readable schema). The envelope layer is modeled exactly; the command-specific `data` payload is an open object (coverage boundary)."
|
|
383
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: things-cli
|
|
3
|
+
description: Read and manage a user's tasks in the Things 3 app (macOS) through the `things` CLI — list views like Today/Inbox/Upcoming, search, and create, edit, schedule, complete, move, or organize to-dos, projects, areas, headings, and tags. Use whenever the user asks about their tasks, to-dos, projects, or anything in Things.
|
|
4
|
+
version: 0.12.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Things CLI
|
|
8
|
+
|
|
9
|
+
`things` is a command-line interface to the user's Things 3 task database. Reads are instant SQL queries; writes go through the app itself and are checked after they land. Use `things` when it is on your PATH; otherwise — or when `things --version` reports below **0.12.0** — substitute `npx -y things-api@latest` in every command (identical subcommands and flags), which always pairs the current commands with their current help.
|
|
10
|
+
|
|
11
|
+
`things --help` is the one-screen index; `things <group> --help` lists a group's verbs and flags (always current for the binary you invoke); `things help <topic>` opens a contract guide — topics: `agent`, `filters`, `ids`, `move`, `output`, `repeating`, `writes`.
|
|
12
|
+
|
|
13
|
+
## Data model (read this first)
|
|
14
|
+
|
|
15
|
+
- **To-do** — the basic item: title, notes, an optional **checklist** (sub-steps), tags, schedule, deadline, reminder. Has at most ONE container — loose in an **area**, directly in a **project**, or under a **heading** inside a project — or **none at all** (standalone to-dos are normal, like standalone projects).
|
|
16
|
+
- **Inbox** — not a container but the *untriaged state*: an inbox to-do has no container and no schedule. Filing it into a container or scheduling it moves it out of the Inbox; moving something back TO the Inbox un-files AND un-schedules it.
|
|
17
|
+
- **Project** — a goal-sized container of to-dos, optionally divided by **headings** (section labels; a heading belongs to one project and cannot hold projects). Projects live in an area or stand alone. Projects can also have their own notes, tags, schedule, and deadline.
|
|
18
|
+
- **Area** — a top-level bucket (e.g. a sphere of responsibility) holding projects and loose to-dos. Areas have tags but no dates, and never go to the Trash — deleting one is permanent.
|
|
19
|
+
- **Tags** — form a hierarchy, and are **inherited downward**: a to-do effectively carries its own tags plus those of its project and area. Headings carry no tags, but inheritance flows through them from project to to-do.
|
|
20
|
+
- **Views** are queries over this model, matching the app's sidebar: `inbox` (unsorted), `today` (scheduled for today, incl. This Evening), `upcoming` (future-dated), `anytime` (all active), `someday` (kept without a date), `logbook` (completed/canceled), `trash`.
|
|
21
|
+
- **Scheduling vocabulary**: an item's *when* is `today | evening | anytime | someday | YYYY-MM-DD`. A **deadline** is a separate due date; a **reminder** is a separate time-of-day alert — never write `date@time` into *when*.
|
|
22
|
+
|
|
23
|
+
## Reading position from JSON: `stage` and `when`
|
|
24
|
+
|
|
25
|
+
Reads decompose an item's position onto two derived, presence-keyed words (they REPLACED the old `start`/`logged`/`trashed`/`todaySection` fields):
|
|
26
|
+
|
|
27
|
+
- **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly (a completed row is `logbook`, a trashed row `trash`, regardless of any other hint). Dropped inside a section that already states it; present in mixed lists, `search`, `changes`, and `detail`.
|
|
28
|
+
- **`when`** — the TIME POSITION: `today | evening | a future ISO date`, or absent. `evening` implies today; someday is a bucket (→ `stage`), never a `when`. A due deadline pulls an undated row into Today (`when: "today"`); the app re-files it into Anytime, so it derives `stage: "anytime"` (it leaves the Inbox/Someday list).
|
|
29
|
+
- **`provisional: true`** marks a Today member the app has not yet materialized (the "N new to-dos" banner / `•` pip); see [references/banner.md](references/banner.md).
|
|
30
|
+
|
|
31
|
+
**Absence is meaningful.** In the compact list tier a field at its default is omitted (no `status` = open, no `when` = not in Today), and inside a single-container node an item omits ancestry the node already states (absent `project`/`area` there = inherited). Full rules and the compact/full tiers: [references/model.md](references/model.md).
|
|
32
|
+
|
|
33
|
+
## Referring to items
|
|
34
|
+
|
|
35
|
+
Commands take a `<ref>`: a UUID, a unique UUID prefix (≥ 6 chars), a Things share link, or a (unique) title. Ambiguous refs fail with the candidates listed — pick one and retry ([references/errors.md](references/errors.md) has the candidate/dead-row/hazard contract). Discover UUIDs via any read command; add `--json` for stable machine output (UUIDs are in `.uuid`; emitted UUIDs are always full).
|
|
36
|
+
|
|
37
|
+
## Stable contracts
|
|
38
|
+
|
|
39
|
+
These hold regardless of the binary version; see [references/contracts.md](references/contracts.md) for the full text.
|
|
40
|
+
|
|
41
|
+
- **JSON envelope**: every `--json` response is `{ apiVersion, ok, kind, data, meta }`. Read results from `.data` (`.data.items`/`.data.sections`/`.data.item`/`.data.view` per `kind`), never `.items`; UUIDs are `.uuid`, not `.id`. Check `meta.truncation.truncated` before concluding "no match" or "that's everything". List/search rows are compact summaries whose `tags` may be incomplete — use `things show <ref> --json` for effective tags, checklist, notes, and placement.
|
|
42
|
+
- **Exit codes**: `0` landed and checked · `2` usage · `3` verify-failed · `4` blocked · `5` drift-blocked · `6` unsupported · `7` environment (`1` is an internal bug). Nonzero means the change did NOT stick; the message names the fix.
|
|
43
|
+
- **Previews, undo & idempotency**: `--dry-run` shows any write's exact plan without executing; `things undo` reverses recent changes made through this tool (each reversible write returns an `undoToken`); a single mutation may carry `--op-id <key>` so an ambiguous resubmission is recognized as already applied (the variadic `move`/`reorder` refuse it — use `things batch` with a per-line `opId`).
|
|
44
|
+
- **Preconditions**: referenced containers and tags must already exist — create nested structures outside-in and reuse each returned UUID.
|
|
45
|
+
- If the user requests a JSON reply schema, return exactly that object after the read or checked write.
|
|
46
|
+
|
|
47
|
+
## Reading
|
|
48
|
+
|
|
49
|
+
Views and lookups — pass `--json` whenever you will act on the output: `things today | inbox | upcoming | anytime | someday | logbook | trash`, `things show <ref>` (full detail incl. notes + checklist + effective tags), `things projects [ref]`, `things areas [ref]`, `things tags`, `things search <words>`, `things changes --since <moment>`. Compact rows carry `hasNotes`, `checklist:{open,total}` (to-dos), and `todos:{open,total}` (projects); `--full` (or `show`) adds the full `notes`, `startDate`, and checklist `items`. A `search` hit carries `match:{field,text}` provenance when it matched something other than the title. Filters (`--tag`, `--untagged`, `--overdue`, `--limit N`, `--all`, …) compose with AND — see `things help filters`. The reserved read-only ref `loose` addresses the area-less items as a pseudo-area (`areas loose`, `area show loose`, `--area loose`) and wins over any real area named "Loose"; every write verb refuses it.
|
|
50
|
+
|
|
51
|
+
## Writing
|
|
52
|
+
|
|
53
|
+
Namespaced verb families — run `things <group> --help` for the verbs and `things <group> <verb> --help` for exact flags: `things todo …` (add/update/complete/cancel/reopen/move/delete/restore/tags/checklist/make-repeating), `things project …` (add/update/move/complete/… plus the heading verbs), `things area …`, `things tag …`, plus `things batch` (JSONL — chain created uuids across lines with `tempId`/`$ref`, retry safely with `opId`, undo the whole run with its `undoToken`), `things undo`, and `things reorder`.
|
|
54
|
+
|
|
55
|
+
**Scheduling is an update, not a move**: `things todo update <ref> --when today|evening|anytime|someday|YYYY-MM-DD` schedules or parks an item; `move` changes its CONTAINER only.
|
|
56
|
+
|
|
57
|
+
**Move vs reorder — keep them apart** (`things help move`). MOVE changes WHAT an item belongs to: `things todo move <refs…> --to-project|--to-heading|--to-area <ref>`, or the detach family `--no-heading` / `--loose` / `--inbox`; `things project move <refs…> --to-area <ref>|--no-area`. REORDER changes only ARRANGEMENT in place, never membership: `things reorder <refs…> [--first|--last|--before <ref>|--after <ref>] [--in <target>]`. Both are variadic (selection order = landing order). An anchor POSITIONS but never MIGRATES — a cross-container/bucket anchor fails closed. A Today/Evening member sits on two axes (its Today slot and its container slot); a set coherent on both is REFUSED until you pass `--in` (`today|evening|anytime|someday|inbox`, or a project/area/heading ref). Every non-template item is sortable; the axes, gates, and automatic fallbacks are in [references/ordering.md](references/ordering.md). (MCP exposes these as the `reorder` and `reorder_areas` tools.)
|
|
58
|
+
|
|
59
|
+
**Guarded writes** surface their consequence and require an explicit flag: deleting a NON-EMPTY area needs `--allow-non-empty`, a permanent delete needs `--dangerously-permanent`, and a UI-driving op (e.g. `area reorder`) needs `--dangerously-drive-gui` plus `things config set ui-enabled true`. The full acknowledgment contract is in [references/errors.md](references/errors.md).
|
|
60
|
+
|
|
61
|
+
**Quick skeletons**: `things todo add "T1" "T2" "T3" [shared flags]` creates several to-dos in one call (every shared flag — `--project`/`--area`/`--when`/`--tags`/… — applies to each; `--id-only` prints the new uuids one per line for chaining; one `undoToken` removes the set). To stand up a new project with children, `things project add "<title>" --todo "T1" --todo "T2" …` (repeatable). For richer per-item metadata or cross-item references, use `things batch`.
|
|
62
|
+
|
|
63
|
+
## Going deeper
|
|
64
|
+
|
|
65
|
+
- [references/model.md](references/model.md) — the full data model, `stage`/`when` derivation, the compact/full tiers, view membership, and filters.
|
|
66
|
+
- [references/contracts.md](references/contracts.md) — the JSON envelope, exit codes, safety/undo/idempotency, batch chaining, and recurrence.
|
|
67
|
+
- [references/ordering.md](references/ordering.md) — move vs reorder in depth: axes, gates, caps, automatic fallbacks, placement guarantees, and the one dead class (templates).
|
|
68
|
+
- [references/errors.md](references/errors.md) — the error contract: the candidate shape, dead-row hints, hazard acknowledgments, and the error-code registry.
|
|
69
|
+
- [references/banner.md](references/banner.md) — the Today "new to-dos" banner, the provisional `•` pip, reminder/evening liveness, and what a watcher sees.
|
|
70
|
+
- [references/gui.md](references/gui.md) — how the user sees Things in the app (where results appear, what list rows show).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# The Today banner, provisional pips, and liveness (deep reference)
|
|
2
|
+
|
|
3
|
+
Things renders three presentation-layer facts that are NOT stored as their own bytes — the "new to-dos" banner, the evening section, and the reminder bell. The reader mirrors the app faithfully rather than reporting the raw columns. This page is the model behind the `provisional` field, the `•` pip, and reminder/evening liveness.
|
|
4
|
+
|
|
5
|
+
## The provisional `•` pip and the "N new to-dos" banner
|
|
6
|
+
|
|
7
|
+
When items arrive in Today on their own, Things shows a yellow "You have N new to-dos" banner and a yellow `•` pip on each such row. Those rows are **provisional** — Today members the app has not yet materialized.
|
|
8
|
+
|
|
9
|
+
- The wire emits presence-keyed **`provisional: true`** on exactly those rows, on every tier, and never drops it (the banner is not a section). The banner's `N` equals the count of `provisional` Today members, exactly — **the banner as data.**
|
|
10
|
+
- **What "new" means:** the flagged rows are the AUTONOMOUS entrants — a scheduled arrival (lands on its startDate), a repeat-instance spawn, or a due-deadline pull (no startDate yet). A USER-placed entrant (`add`/`update --when today`, which lands materialized) is never flagged. The wire hands you `provisional` directly, so you never compute it; the underlying law (what a raw-DB watcher would evaluate) is `start` is not `1` (active) OR `startDate IS NULL`.
|
|
11
|
+
- `provisional` reuses the same Today markers `when` does, so it can never disagree with the ★ the row would render.
|
|
12
|
+
- **The pip is Today-view-only.** In the app's Anytime list the same provisional row shows the ordinary Today ★ and no pip. So the TTY renders `•` only in `things today` (Today and This Evening sections); the wire `provisional` field is present on every surface, but the `•` glyph is not.
|
|
13
|
+
|
|
14
|
+
**A provisional row is already a full Today member** — visible, sortable, and mutable through every ordinary write. Model it as `stage: "anytime"` + `when: "today"` + `provisional: true`: at pull time the app re-files a deadline-pulled Inbox/Someday row into Anytime (R13/BANNER1b), so it is EXCLUDED from the Inbox/Someday lists and INCLUDED in Anytime even though the user never chose to leave those buckets — the derived stage reflects where the app actually shows it.
|
|
15
|
+
|
|
16
|
+
## There is no `today ok` — dismissing the banner is a data mutation
|
|
17
|
+
|
|
18
|
+
`things today ok` (acknowledge the banner / clear the pips) is a DECIDED not-to-implement (⛔). Reasons worth knowing:
|
|
19
|
+
|
|
20
|
+
- Clicking **OK** in the app does exactly one thing: it MATERIALIZES the provisional rows — `start := 1` and `startDate := ` the cohort/deadline date where it was NULL — and writes nothing else, in no other table. There is no separate "reviewed" marker anywhere; acknowledgment IS the row mutation.
|
|
21
|
+
- That mutation is **not byte-reproducible** through a supported write (`--when today` no-ops on a provisional row; the OK'd `startDate` is a past date every URL write clamps; `--when anytime` is the only leg that flips the bucket but it ejects the row from Today), and it silently **de-inboxes / de-somedays** an untriaged pulled item (`start 0→1` / `2→1`). Approximating it would diverge silently from the app's own bytes, so it is not offered.
|
|
22
|
+
|
|
23
|
+
**The per-item dismiss you CAN drive (deadline-pulled rows):** rescheduling an overdue-deadline row out of Today to a no-startDate bucket — `things todo update <ref> --when someday` (or `anytime`) — auto-stamps the app's own suppression so the past deadline stops re-pulling it; or `things todo update <ref> --clear-deadline` removes the deadline outright. There is no dedicated "dismiss deadline" affordance in the app either — this reschedule side effect is it.
|
|
24
|
+
|
|
25
|
+
## What a watcher sees
|
|
26
|
+
|
|
27
|
+
A process polling the database must tolerate the app rewriting provisional rows' schedule bytes out from under it: when a human clicks OK (on this or any synced device), every listed row's `start`/`startDate` change (`start:=1`, `startDate:=` the cohort date). Nothing else moves. So a watcher should key "new/unreviewed" off the same derivation the reader uses (`start != 1 OR startDate IS NULL`), NOT off a stored marker (there is none), and expect those bytes to settle once the user acknowledges.
|
|
28
|
+
|
|
29
|
+
## Reminder and evening liveness (§9n)
|
|
30
|
+
|
|
31
|
+
The app shows a reminder bell and a "This Evening" placement only while the row's `startDate` is TODAY (or future, for a reminder). Once `startDate` goes strictly past, the app discards both at the presentation layer while leaving the raw columns in the database forever — nothing clears them, not a day-rollover, not the banner OK. The reader mirrors this:
|
|
32
|
+
|
|
33
|
+
- **Reminders.** A today- or future-dated reminder is reported (`reminder` present); a strictly-past one is OMITTED on every surface (list rows, detail, JSON, search, and the TTY `◷` chip). The stored byte is never touched. On the WRITE side a bare `--when` re-schedule auto-preserves a LIVE reminder but never RESURRECTS a stale one (so `--when evening` on a weeks-past row no longer re-arms an 18:00 bell the user believed gone).
|
|
34
|
+
- **Evening.** An arrived evening row whose `startDate` has gone past collapses to a plain Today member — `when` reads `"today"`, not `"evening"`. The evening section expires daily.
|
|
35
|
+
- **Reminder format:** the JSON wire carries `HH:MM` (24-hour); the human TTY chip renders 12-hour. Same fact, two renderings.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Stable contracts: envelope, exit codes, safety, recurrence
|
|
2
|
+
|
|
3
|
+
The apiVersion-stable contracts that hold regardless of which binary version you invoke. Mechanics — verbs, flags, per-operation preconditions — live in `things <group> --help` and `things help <topic>` and version with the binary; the contracts here do not.
|
|
4
|
+
|
|
5
|
+
## JSON envelope
|
|
6
|
+
|
|
7
|
+
Every `--json` response is an envelope `{ apiVersion, ok, kind, data, meta }`:
|
|
8
|
+
|
|
9
|
+
- Results are in `.data`, in exactly one of four wrappers named by `kind`: `.data.item` (one entity), `.data.view` (an area/project card), `.data.items` (a flat list), or `.data.sections` (a list split into named sections). Never `.items` at the top level. Item UUIDs are in `.uuid`, not `.id`; emitted UUIDs are always full.
|
|
10
|
+
- Check `meta.truncation.truncated` before concluding "no match" or "that's everything": it is `true` exactly when any row was hidden (`shown < total`, or any grouped block capped). Raise `--limit`/`--all` or narrow the filter rather than assuming you saw everything.
|
|
11
|
+
- List/search rows are compact **summaries**: their `tags` field is not necessarily the complete effective set, and placement can be partial. Use `things show <ref> --json` when notes, checklist, placement, or inherited/effective tags matter.
|
|
12
|
+
|
|
13
|
+
## Exit codes (writes are verified after they land)
|
|
14
|
+
|
|
15
|
+
A value once assigned keeps its meaning forever — the codes are never renumbered.
|
|
16
|
+
|
|
17
|
+
- `0` — the change landed and was verified.
|
|
18
|
+
- `1` — unexpected (an internal error / bug): stop and report.
|
|
19
|
+
- `2` — usage error: fix your invocation and retry.
|
|
20
|
+
- `3` — verify-failed: the change did NOT stick; the message carries the reason and usually the remediation.
|
|
21
|
+
- `4` — blocked: refused before it touched the app (a guard/hazard, scope, lock, …); the message names the flag or fix.
|
|
22
|
+
- `5` — drift-blocked: the database schema fingerprint no longer matches; writes are held until re-certified.
|
|
23
|
+
- `6` — unsupported: no available write surface performs this operation.
|
|
24
|
+
- `7` — environment: database not found, Things not installed, or a permission problem.
|
|
25
|
+
|
|
26
|
+
A nonzero exit is informative, not a dead end — it means the write did not silently half-apply, so you are never left guessing whether it took. Read the message; it usually names the fix. The error `code`, the candidate/dead-row contract, and the hazard acknowledgments are in [errors.md](errors.md).
|
|
27
|
+
|
|
28
|
+
## Safety & recovery
|
|
29
|
+
|
|
30
|
+
- `--dry-run` previews the exact plan (operation, target, expected change) for ANY write without executing — use it for anything destructive, bulk, or unfamiliar.
|
|
31
|
+
- `things undo` reverses recent changes made through this tool (its own audit trail, not arbitrary app history). Prefer a targeted fix when you know it; undo is the safety net.
|
|
32
|
+
- To-do/project deletes are TRASHES: `todo delete` moves to Trash and is restorable (`todo restore`). Emptying the trash is permanent and requires explicit user intent — don't do it unless asked. AREAS are the exception — an area does not go to the Trash, so deleting one is permanent (`--dangerously-permanent`), and a non-empty area also needs `--allow-non-empty` (see [errors.md](errors.md)).
|
|
33
|
+
- Ambiguous refs FAIL with the candidates listed — retry with a UUID or a unique prefix. Never guess between candidates for a destructive action; inspect details or ask. The full candidate shape and dead-row hints are in [errors.md](errors.md).
|
|
34
|
+
- Referenced containers and tags must already exist. Create nested structures **outside-in** (area → project → heading → to-do), and prefer each newly returned UUID as the next reference so duplicate titles cannot redirect placement.
|
|
35
|
+
- Some operations are disruptive and require an explicit flag, **including their dry runs**: `--allow-disruptive` permits an op that briefly steals window focus; an op that visibly DRIVES the Things UI needs both `--allow-disruptive` and `--allow-very-disruptive` (the two-key gate). `things capabilities` lists each operation's support and any preconditions.
|
|
36
|
+
- If a request needs a capability the tool reports as unsupported, say so plainly rather than improvising through unrelated commands.
|
|
37
|
+
|
|
38
|
+
## Bulk creation (contract summary)
|
|
39
|
+
|
|
40
|
+
- **Several to-dos at once**: `things todo add "T1" "T2" "T3" [shared flags]`. Every shared flag (`--project`/`--area`/`--heading`/`--when`/`--tags`/`--deadline`/…) applies to each title; titles land in argument order. `--stdin` reads newline-delimited titles from stdin (blank lines skipped) instead of positional args (the two are mutually exclusive). `--id-only` prints exactly one uuid per line in creation order — pipe it to chain follow-up commands (mutually exclusive with `--json`).
|
|
41
|
+
- **One undo for the whole set**: a multi-title add runs as one unit. `--json` streams a per-line result plus a trailing `summary` line carrying a single `undoToken`; `things undo --txn <undoToken>` removes the whole skeleton at once. A single-title `add` is unchanged — it still returns the ordinary single mutation-result envelope.
|
|
42
|
+
- **New project with children**: `things project add "<title>" --todo "T1" --todo "T2" …` (`--todo` repeatable) seeds a project skeleton in one call.
|
|
43
|
+
- **Richer batches**: for per-item metadata that differs, or cross-item references (`tempId`/`$ref`), use `things batch` (JSONL) — same one-`undoToken` undo semantics.
|
|
44
|
+
|
|
45
|
+
## Batch (many changes at once)
|
|
46
|
+
|
|
47
|
+
`things batch` runs a JSONL script (one `{"op","params",…}` per line) sequentially and independently — no transactions; a failure does not roll back earlier lines. Three fields make multi-step work reliable:
|
|
48
|
+
|
|
49
|
+
- **`tempId` (chaining):** a line that CREATES something (a to-do, project, area, heading, repeater — never `tag.add`) can carry `"tempId":"proj1"`; a LATER line references that new uuid as `"$proj1"` in any id/container field. Dotted forms reach a repeater's parts: `"$proj1.instance"` (the visible occurrence), `"$proj1.replaced"` (the original). This is how you "create a project, then file to-dos into it" in one submission without knowing the uuid up front. Handles are `[A-Za-z0-9_-]{1,32}` and unique per batch; an unknown or forward `$ref` fails just that line.
|
|
50
|
+
- **`opId` (safe retry):** carry a stable `"opId"` per line so resubmitting a batch after an ambiguous failure does not double-create — a line matching an earlier success is reported `already-applied`, not re-run. The single-op analogue is `--op-id <key>` (MCP `op_id`) on ONE mutation: a resubmission with a matched key returns the original success (`alreadyApplied: true`, the original `uuid`/`undoToken`) instead of running again. The variadic `move`/`reorder` are multi-leg compounds and REFUSE `--op-id` — express their idempotency as `things batch` with a per-line `opId`.
|
|
51
|
+
- **Undo the whole batch:** the trailing summary line returns `tempIdMapping` (handle → uuid) and `undoToken`; `things undo --txn <undoToken>` reverses the entire submission as one unit.
|
|
52
|
+
|
|
53
|
+
## Recurrence (contract summary)
|
|
54
|
+
|
|
55
|
+
Full rule vocabulary and worked examples: **`things help repeating`**. The stable contract:
|
|
56
|
+
|
|
57
|
+
- Turning an item into a **fixed** repeater REPLACES it: the original becomes a hidden template plus a fresh first occurrence, so its UUID changes. The response returns a `repeating` block — `instanceUuid` (the visible current occurrence; use it to reach the item), `templateUuid` (the recurring rule; use it for `reschedule-repeat`), and `replacedUuid` (the original). Use these rather than re-finding the item by title.
|
|
58
|
+
- `--after-completion` schedules the next occurrence N units after you complete the current one, and KEEPS the item's UUID.
|
|
59
|
+
- Repeating operations require `--allow-disruptive` (including dry runs) and a required `--interval <n>` alongside `--frequency` (`--interval 1` for every unit).
|
|
60
|
+
- New repeater: add the item first (`things todo add "<title>" --json`), then `things todo make-repeating <returned-uuid> --frequency <f> --interval 1 [--weekdays …] --allow-disruptive`.
|
|
61
|
+
- Multiple weekdays go in ONE rule (`--weekdays monday,thursday,friday`) — never create two repeaters for "every Thursday and Friday".
|
|
62
|
+
- `things show <ref> --json` on an occurrence exposes `repeating.templateUuid` — use it as the `<ref>` for `reschedule-repeat`.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# The error contract (deep reference)
|
|
2
|
+
|
|
3
|
+
A failed `--json` call carries `ok: false`, `kind: "error"`, and an `error` object with a machine-readable `code`, a human `message`, an optional `likelyCause` and `remediation`, and at most one structured `detail`. The exit code mirrors the family (see [contracts.md](contracts.md)). A nonzero exit is informative, not a dead end — the change did not silently half-apply, and the message usually names the fix.
|
|
4
|
+
|
|
5
|
+
## Candidates — self-correcting from an ambiguous or missing ref
|
|
6
|
+
|
|
7
|
+
When a name or partial-uuid resolves to more than one row, or to none, `error.detail.candidates` offers a disambiguation list so you self-correct without another round-trip. Each entry is ONE fixed, slim shape:
|
|
8
|
+
|
|
9
|
+
- `uuid`, `title`, and `type` (`"to-do" | "project" | "heading" | "area" | "tag"`) — always.
|
|
10
|
+
- A container hint `area` and/or `project` as a TITLE string — only when set.
|
|
11
|
+
- `stage` / `when` (the same derivations as the item wire) — only for a to-do/project candidate whose row carries them. A trashed/logged candidate needs no boolean: `stage` already reads `"trash"` / `"logbook"`.
|
|
12
|
+
- NOTHING else — no notes, counts, dates, or raw fields.
|
|
13
|
+
|
|
14
|
+
The shape is **flag-invariant**: `--full` / `--all` never widen it (an error payload is the most determinism-critical surface). The list is **capped at 8**; when more matched, the `message` states the total (e.g. "matches 12 projects … 4 more"). `ambiguous` carries the candidate list; `not-found` carries `candidates: []`.
|
|
15
|
+
|
|
16
|
+
**Live-scoped pool + dead-row hints.** By default candidates are LIVE rows only — a trashed or logged row never appears as a "did you mean". A trash/logbook-domain op (e.g. `project restore`) widens its own pool to that domain. When a name matches ZERO live rows but a DEAD row exists, the `message` gains an honest tail ("1 trashed item matches this name — see `things trash`", or the logbook equivalent) and `candidates` stays `[]` — no dangling-ref invitation. A write whose only name-match is a completed/logged project resolves to nothing by name (target it by uuid if intended); this prevents stranding an open child inside an invisible done project.
|
|
17
|
+
|
|
18
|
+
Never guess between candidates for a destructive action — inspect details or ask.
|
|
19
|
+
|
|
20
|
+
## Guarded writes and their acknowledgments
|
|
21
|
+
|
|
22
|
+
Some writes have a cascading or permanent effect. Each is refused BEFORE touching the app until you pass the flag that names the consequence; a `--dry-run` shows the same refusal. The CLI describes each by its consequence, but the JSON error's `blocked:<hazard>` code is the machine-readable handle:
|
|
23
|
+
|
|
24
|
+
- **Delete a NON-EMPTY area** → `--allow-non-empty`. Deleting an area sends its to-dos AND its projects (with their children) to the Trash and destroys the area row permanently. A delete pre-counts live members and refuses with the counts ("the area is not empty — it contains 3 projects and 12 to-dos"), naming both remediations: empty the area first, or pass `--allow-non-empty`. An empty area is unaffected (still just `--dangerously-permanent`). Code: `blocked:H-AREA-NOT-EMPTY`.
|
|
25
|
+
- **Permanent delete** (an area; emptying the Trash) → `--dangerously-permanent`. There is no inverse; `undo` names the manual one rather than pretending. Code: `blocked:H-PERMANENT-DELETE`.
|
|
26
|
+
- **Drive the live UI** (the ops the app offers nowhere else — e.g. `area reorder`, and some heading/repeat ops) → the two-key gate: the `--dangerously-drive-gui` flag AND `things config set ui-enabled true` (plus Accessibility granted to the process). These visibly drive the Things window. Code: `blocked:H-UI-DRIVE`. (Separately, the `--allow-disruptive` / `--allow-very-disruptive` flags raise the process's disruption ceiling for any op that steals focus or drives the UI.)
|
|
27
|
+
- **Reopen or reuse a resolved project** — moving or adding an OPEN child into a completed/canceled project reopens it via the app; that is acknowledged (`blocked:H-REOPEN-RESOLVED-PROJECT`) so it is never a silent side effect.
|
|
28
|
+
- Other guards name their own consequence the same way (heading cascades, backdating an open item, checklist replacement, repeat scheduling, tag-subtree delete, …).
|
|
29
|
+
|
|
30
|
+
`things capabilities` lists every operation's support and preconditions; `--dry-run` previews any plan. If a request needs a capability the tool reports as unsupported, say so plainly rather than improvising through unrelated commands.
|
|
31
|
+
|
|
32
|
+
## The error-code families
|
|
33
|
+
|
|
34
|
+
Every `error.code` is drawn from a frozen registry. Route on the code, and for the two template families route on the prefix:
|
|
35
|
+
|
|
36
|
+
| Family | Meaning | Exit |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `usage` | Bad flags/arguments, mutually-exclusive flags, an unparseable date, or a move that meant to schedule. | 2 |
|
|
39
|
+
| `not-found` | A ref or subject resolved to nothing (`candidates: []`). | 2 |
|
|
40
|
+
| `ambiguous` | A name/partial-uuid matched several rows (carries `candidates`). | 2 |
|
|
41
|
+
| `unsupported` | No available write vector supports the operation (`detail.considered`). | 6 |
|
|
42
|
+
| `environment` | Database not found, Things not installed, or a permission problem. | 7 |
|
|
43
|
+
| `unexpected` | An internal error (a bug) — stop and report. | 1 |
|
|
44
|
+
| `verify-failed:<reason>` | A single mutation executed but the read-back check failed. `<reason>` ∈ `timeout | mismatch | silent-noop`. | 3 |
|
|
45
|
+
| `blocked:<suffix>` | Refused before dispatch — a hazard id (above) or a reason (`disruption-tier`, `lock`, `scope`, `clock`, `environment`, `drift`). `blocked:drift` is exit 5; all other `blocked:` are exit 4. | 4 / 5 |
|
|
46
|
+
| `bounce-aborted` | A reorder bounce aborted part-way (`detail.placed`/`remaining`/`cause`). | 3 |
|
|
47
|
+
| `verify-failed` | A multi-leg move/reorder failed mid-way (`detail.failed`/`completed`). | 3 |
|
|
48
|
+
|
|
49
|
+
A consumer that does not recognize a specific suffix routes on the prefix (`blocked:` / `verify-failed:`) and the exit code.
|