dayhand 0.0.0-stage → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +100 -2
  3. package/backend/convex/_generated/api.d.ts +181 -0
  4. package/backend/convex/_generated/api.js +23 -0
  5. package/backend/convex/_generated/dataModel.d.ts +60 -0
  6. package/backend/convex/_generated/server.d.ts +162 -0
  7. package/backend/convex/_generated/server.js +101 -0
  8. package/backend/convex/apiKeys.ts +171 -0
  9. package/backend/convex/app.ts +197 -0
  10. package/backend/convex/auth.config.ts +28 -0
  11. package/backend/convex/calendar.ts +61 -0
  12. package/backend/convex/calendarFeeds.ts +105 -0
  13. package/backend/convex/callLimits.ts +67 -0
  14. package/backend/convex/connections.ts +28 -0
  15. package/backend/convex/crons.ts +18 -0
  16. package/backend/convex/devices.ts +224 -0
  17. package/backend/convex/http.ts +56 -0
  18. package/backend/convex/lib/activity.ts +155 -0
  19. package/backend/convex/lib/agentControls.ts +146 -0
  20. package/backend/convex/lib/agentWork.ts +250 -0
  21. package/backend/convex/lib/appToken.ts +68 -0
  22. package/backend/convex/lib/approvals.ts +311 -0
  23. package/backend/convex/lib/args.ts +175 -0
  24. package/backend/convex/lib/backlogMd.ts +309 -0
  25. package/backend/convex/lib/backlogOps.ts +361 -0
  26. package/backend/convex/lib/brief.ts +132 -0
  27. package/backend/convex/lib/cimd.ts +57 -0
  28. package/backend/convex/lib/core.ts +380 -0
  29. package/backend/convex/lib/dates.ts +114 -0
  30. package/backend/convex/lib/env.ts +19 -0
  31. package/backend/convex/lib/exportPlan.ts +98 -0
  32. package/backend/convex/lib/findTasks.ts +119 -0
  33. package/backend/convex/lib/frontmatter.ts +165 -0
  34. package/backend/convex/lib/guide.ts +286 -0
  35. package/backend/convex/lib/health.ts +115 -0
  36. package/backend/convex/lib/http.ts +27 -0
  37. package/backend/convex/lib/ical.ts +243 -0
  38. package/backend/convex/lib/importFormats.ts +429 -0
  39. package/backend/convex/lib/importOps.ts +145 -0
  40. package/backend/convex/lib/jsonSchema.ts +50 -0
  41. package/backend/convex/lib/limits.ts +42 -0
  42. package/backend/convex/lib/mcpContent.ts +220 -0
  43. package/backend/convex/lib/mcpProtocol.ts +128 -0
  44. package/backend/convex/lib/mcpTools.ts +505 -0
  45. package/backend/convex/lib/oauthConfig.ts +59 -0
  46. package/backend/convex/lib/oauthPage.ts +125 -0
  47. package/backend/convex/lib/oauthRules.ts +159 -0
  48. package/backend/convex/lib/openapi.ts +230 -0
  49. package/backend/convex/lib/outputSchemas.ts +2901 -0
  50. package/backend/convex/lib/problem.ts +69 -0
  51. package/backend/convex/lib/proposals.ts +172 -0
  52. package/backend/convex/lib/repeat.ts +185 -0
  53. package/backend/convex/lib/restRoutes.ts +294 -0
  54. package/backend/convex/lib/rrule.ts +171 -0
  55. package/backend/convex/lib/secrets.ts +135 -0
  56. package/backend/convex/lib/settingsOps.ts +47 -0
  57. package/backend/convex/lib/taskOps.ts +304 -0
  58. package/backend/convex/lib/toolResults.ts +50 -0
  59. package/backend/convex/lib/validators.ts +143 -0
  60. package/backend/convex/lib/views.ts +145 -0
  61. package/backend/convex/lib/vtimezone.ts +151 -0
  62. package/backend/convex/lib/webhookEvents.ts +160 -0
  63. package/backend/convex/lib/webhookSign.ts +30 -0
  64. package/backend/convex/lib/webhookTarget.ts +75 -0
  65. package/backend/convex/mcp.ts +405 -0
  66. package/backend/convex/migrations.ts +91 -0
  67. package/backend/convex/oauth.ts +388 -0
  68. package/backend/convex/oauthHttp.ts +381 -0
  69. package/backend/convex/oauthOwner.ts +173 -0
  70. package/backend/convex/ops.ts +205 -0
  71. package/backend/convex/pairing.ts +178 -0
  72. package/backend/convex/rest.ts +254 -0
  73. package/backend/convex/restAccount.ts +56 -0
  74. package/backend/convex/restImport.ts +73 -0
  75. package/backend/convex/schema.ts +435 -0
  76. package/backend/convex/tsconfig.json +18 -0
  77. package/backend/convex/webhooks.ts +339 -0
  78. package/backend/package-lock.json +3068 -0
  79. package/backend/package.json +33 -0
  80. package/backend/scripts/app-token.mjs +79 -0
  81. package/backend/scripts/fetch-local-backend.mjs +72 -0
  82. package/backend/scripts/gen-output-schemas.mjs +60 -0
  83. package/backend/scripts/jwt.mjs +67 -0
  84. package/backend/scripts/local-backend-lib.mjs +123 -0
  85. package/backend/scripts/local-backend.mjs +81 -0
  86. package/backend/scripts/setup-convex.mjs +158 -0
  87. package/backend/scripts/setup-lib.mjs +338 -0
  88. package/backend/scripts/setup-output.mjs +108 -0
  89. package/backend/scripts/setup.mjs +344 -0
  90. package/backend/scripts/signing-key.mjs +61 -0
  91. package/backend/tsconfig.json +16 -0
  92. package/dist/backlog/folder.js +114 -0
  93. package/dist/bin/dayhand.js +10 -0
  94. package/dist/bin/hlm.js +12 -0
  95. package/dist/completion/cache.js +54 -0
  96. package/dist/completion/engine.js +187 -0
  97. package/dist/completion/scripts.js +82 -0
  98. package/dist/config/aliases.js +25 -0
  99. package/dist/config/env.js +68 -0
  100. package/dist/config/file.js +146 -0
  101. package/dist/config/paths.js +58 -0
  102. package/dist/config/resolve.js +53 -0
  103. package/dist/config/url.js +26 -0
  104. package/dist/core/editor.js +37 -0
  105. package/dist/core/exit.js +59 -0
  106. package/dist/core/io.js +14 -0
  107. package/dist/core/output.js +39 -0
  108. package/dist/core/prompt.js +55 -0
  109. package/dist/core/version.js +15 -0
  110. package/dist/core/words.js +36 -0
  111. package/dist/credentials/file.js +65 -0
  112. package/dist/credentials/index.js +29 -0
  113. package/dist/credentials/keychain.js +44 -0
  114. package/dist/credentials/store.js +39 -0
  115. package/dist/docs.js +116 -0
  116. package/dist/fields.js +33 -0
  117. package/dist/globals.js +16 -0
  118. package/dist/guide.js +36 -0
  119. package/dist/help.js +56 -0
  120. package/dist/i18n/dates.js +84 -0
  121. package/dist/i18n/de.js +240 -0
  122. package/dist/i18n/en.js +243 -0
  123. package/dist/i18n/es.js +240 -0
  124. package/dist/i18n/fr.js +240 -0
  125. package/dist/i18n/index.js +67 -0
  126. package/dist/i18n/ja.js +240 -0
  127. package/dist/i18n/pl.js +245 -0
  128. package/dist/i18n/pt-BR.js +240 -0
  129. package/dist/i18n/zh-Hans.js +240 -0
  130. package/dist/main.js +180 -0
  131. package/dist/man.js +56 -0
  132. package/dist/registry/agents.js +67 -0
  133. package/dist/registry/auth.js +168 -0
  134. package/dist/registry/backlog.js +106 -0
  135. package/dist/registry/calendar.js +103 -0
  136. package/dist/registry/dates.js +33 -0
  137. package/dist/registry/filters.js +98 -0
  138. package/dist/registry/index.js +24 -0
  139. package/dist/registry/integrations.js +245 -0
  140. package/dist/registry/local.js +89 -0
  141. package/dist/registry/model.js +104 -0
  142. package/dist/registry/ops.js +47 -0
  143. package/dist/registry/owner.js +349 -0
  144. package/dist/registry/packaged-backend.js +59 -0
  145. package/dist/registry/parse.js +129 -0
  146. package/dist/registry/server.js +252 -0
  147. package/dist/registry/system.js +220 -0
  148. package/dist/registry/tasks.js +512 -0
  149. package/dist/registry/types.js +1 -0
  150. package/dist/registry/ui.js +51 -0
  151. package/dist/registry/work.js +207 -0
  152. package/dist/render/format.js +146 -0
  153. package/dist/render/model.js +29 -0
  154. package/dist/render/style.js +24 -0
  155. package/dist/render/table.js +46 -0
  156. package/dist/render/views.js +256 -0
  157. package/dist/run.js +133 -0
  158. package/dist/setup.js +26 -0
  159. package/dist/skill.js +83 -0
  160. package/dist/topics.js +121 -0
  161. package/dist/transport/client.js +196 -0
  162. package/dist/ui/agents.js +12 -0
  163. package/dist/ui/api.js +99 -0
  164. package/dist/ui/app.js +62 -0
  165. package/dist/ui/budget.js +42 -0
  166. package/dist/ui/controller.js +368 -0
  167. package/dist/ui/effects.js +166 -0
  168. package/dist/ui/frame.js +206 -0
  169. package/dist/ui/handle.js +271 -0
  170. package/dist/ui/health.js +22 -0
  171. package/dist/ui/keys.js +120 -0
  172. package/dist/ui/line.js +56 -0
  173. package/dist/ui/lists.js +58 -0
  174. package/dist/ui/model.js +25 -0
  175. package/dist/ui/quickadd.js +66 -0
  176. package/dist/ui/rows.js +68 -0
  177. package/dist/ui/run.js +54 -0
  178. package/dist/ui/screens/agents.js +65 -0
  179. package/dist/ui/screens/board.js +41 -0
  180. package/dist/ui/screens/detail.js +142 -0
  181. package/dist/ui/screens/overlay.js +58 -0
  182. package/dist/ui/screens/screen.js +3 -0
  183. package/dist/ui/screens/today.js +60 -0
  184. package/dist/ui/screens/week.js +76 -0
  185. package/dist/ui/state.js +128 -0
  186. package/dist/ui/text.js +70 -0
  187. package/dist/when/calendar.js +95 -0
  188. package/dist/when/parse.js +314 -0
  189. package/man/dayhand.1 +1163 -0
  190. package/man/dh.1 +1 -0
  191. package/package.json +56 -4
@@ -0,0 +1,512 @@
1
+ // Task commands: verbs at the top level, one MCP tool each (see docs/cli.md).
2
+ import { usageError } from "../core/exit.js";
3
+ import { applyDue } from "./dates.js";
4
+ import { FILTER_OPTIONS, hasFilters, listTasks, parseStatuses } from "./filters.js";
5
+ import { eachRef, oneOrMany, tool, userTimezone } from "./ops.js";
6
+ import { checklistOf, editChecklist, REMINDER_OPTIONS, remindersInput, REPEAT_OPTIONS, repeatInput } from "./model.js";
7
+ import { PROPOSAL_OPTIONS, proposalArgs } from "./owner.js";
8
+ import { parseLink, resolveRef } from "./parse.js";
9
+ import { criteriaOf, PROOF_OPTIONS, proofInput } from "./work.js";
10
+ const REF = { name: "ref", description: "Task: #42 or 42 (its number), now, or its id", required: true };
11
+ const REFS = { name: "refs", description: "Tasks: #42 or 42, now, or ids", required: true, variadic: true };
12
+ const SCHEDULE_OPTIONS = [
13
+ { flags: "--due <when>", description: "Due day, optionally with a time: tomorrow, fri 14:00, in 3 days, 2026-10-23 (dayhand help dates)" },
14
+ { flags: "--time <HH:MM>", description: "Due time, 24h, in your time zone" },
15
+ ...REPEAT_OPTIONS,
16
+ ];
17
+ // Agent work fields (backend 0.10.0): what done means, how, and what has to happen first.
18
+ const WORK_OPTIONS = [
19
+ { flags: "--accept <text>", description: "Acceptance criterion: what done means (repeatable, in order)", kind: "list" },
20
+ { flags: "--plan <text>", description: "How the work will be done" },
21
+ { flags: "--depends-on <ref>", description: "A task that must be done first (repeatable)", kind: "list" },
22
+ { flags: "--blocked <reason>", description: "Why the task cannot go on; a blocked task is not ready" },
23
+ ];
24
+ /** --accept, --plan, --depends-on and --blocked as tool arguments. */
25
+ async function workArgs(ctx, opts, input) {
26
+ if (opts.accept.length)
27
+ input.acceptance = criteriaOf(opts.accept);
28
+ if (opts.plan !== undefined)
29
+ input.plan = opts.plan;
30
+ if (opts.blocked !== undefined)
31
+ input.blockedReason = opts.blocked;
32
+ const depends = opts.dependsOn;
33
+ if (depends.length)
34
+ input.dependsOn = await Promise.all(depends.map((ref) => resolveRef(ctx, ref)));
35
+ }
36
+ const asList = (value) => (value.result ?? []);
37
+ /** A command that applies one tool to each of several tasks. */
38
+ function perTask(spec, call, tools) {
39
+ return {
40
+ name: spec.name,
41
+ aliases: spec.aliases,
42
+ summary: spec.summary,
43
+ args: [REFS],
44
+ tools,
45
+ fields: "task",
46
+ schema: "task.schema.json",
47
+ examples: spec.examples,
48
+ run: async (ctx, args) => {
49
+ const results = await eachRef(ctx, args.refs, spec.verb, (taskId, clientId) => call(ctx, taskId, clientId));
50
+ return { value: oneOrMany(results), view: { kind: "tasks", verb: spec.done, tasks: results } };
51
+ },
52
+ };
53
+ }
54
+ const setStatus = (status) => (ctx, taskId, clientId) => tool(ctx, "set_status", { taskId, status, clientId });
55
+ export const taskCommands = [
56
+ {
57
+ name: "today",
58
+ schema: "today.schema.json",
59
+ summary: "The Today screen: now task, overdue, today and done today (also plain `dayhand`)",
60
+ tools: ["list_today"],
61
+ fields: "task",
62
+ examples: ["dayhand", "dayhand today --json", "dayhand today --plain --fields ref,title"],
63
+ run: async (ctx) => {
64
+ const view = await tool(ctx, "list_today");
65
+ return { value: view, view: { kind: "today", view } };
66
+ },
67
+ },
68
+ {
69
+ name: "add",
70
+ schema: "task.schema.json",
71
+ summary: "Add a task (due today unless --due or --no-due)",
72
+ args: [{ name: "title", description: "Task title", required: true, variadic: true }],
73
+ options: [
74
+ ...SCHEDULE_OPTIONS,
75
+ { flags: "--no-due", description: "No due day: the task shows only in list and board" },
76
+ { flags: "-l, --label <label>", description: "Label (repeatable)", kind: "list" },
77
+ { flags: "--note <text>", description: "Note" },
78
+ { flags: "--status <status>", description: "Initial status", choices: ["todo", "doing"] },
79
+ { flags: "--resume-note <text>", description: "Where you stopped / next step" },
80
+ { flags: "--link <url>", description: 'Link, optionally followed by a title: "https://… Title" (repeatable)', kind: "list" },
81
+ { flags: "--item <text>", description: "Checklist item (repeatable, in order)", kind: "list" },
82
+ ...REMINDER_OPTIONS,
83
+ ...WORK_OPTIONS,
84
+ ...PROPOSAL_OPTIONS,
85
+ ],
86
+ tools: ["add_task"],
87
+ fields: "task",
88
+ examples: [
89
+ 'dayhand add Call the accountant --due "fri 2pm" -l home',
90
+ 'dayhand add Pay rent --due "1 nov" --dry-run',
91
+ 'dayhand add "Review the plan" --time 09:00 --every week --on mon,thu',
92
+ 'dayhand add Payroll --due 2026-10-30 --month-weekday "last fri" --remind "09:00 -1d"',
93
+ "dayhand add Water plants --every day --interval 3 --from-completion",
94
+ "dayhand add Pack --due fri --item passport --item charger",
95
+ "dayhand add Buy a present --no-due --json",
96
+ 'dayhand add "Ship the export" --no-due --accept "CSV opens in a spreadsheet" --plan "write, then test" --depends-on 41',
97
+ 'dayhand add "Checkout times out" --propose --source "sentry:CHECKOUT-42 https://sentry.example.com/issues/42"',
98
+ ],
99
+ run: async (ctx, args, opts) => {
100
+ const input = { title: args.title.join(" "), clientId: ctx.clientId() };
101
+ copy(opts, input, { time: "dueTime", note: "note", status: "status", resumeNote: "resumeNote" });
102
+ if (opts.due !== false)
103
+ await applyDue(ctx, input, opts.due ?? "today", opts);
104
+ if (opts.label.length)
105
+ input.labels = opts.label;
106
+ if (opts.link.length)
107
+ input.links = opts.link.map(parseLink);
108
+ const repeat = await repeatInput(ctx, opts);
109
+ if (repeat)
110
+ input.repeat = repeat;
111
+ if (opts.item.length)
112
+ input.checklist = checklistOf(opts.item);
113
+ const reminders = remindersInput(opts);
114
+ if (reminders)
115
+ input.reminders = reminders;
116
+ await workArgs(ctx, opts, input);
117
+ proposalArgs(opts, input);
118
+ const task = await tool(ctx, "add_task", input);
119
+ return { value: task, view: { kind: "task", verb: "Added", task } };
120
+ },
121
+ },
122
+ {
123
+ name: "list",
124
+ schema: "task-list.schema.json",
125
+ aliases: ["ls"],
126
+ summary: "Open tasks, oldest due first (with --done also done ones); filters narrow the list",
127
+ options: [
128
+ { flags: "-l, --label <label>", description: "Only tasks with this label (repeatable: all must match)", kind: "list" },
129
+ { flags: "--done", description: "Include done tasks" },
130
+ { flags: "--archived", description: "Include archived tasks" },
131
+ ...FILTER_OPTIONS,
132
+ ],
133
+ tools: ["list_all"],
134
+ fields: "task",
135
+ examples: [
136
+ "dayhand list -l home --status todo,doing",
137
+ "dayhand list --overdue",
138
+ 'dayhand list --due-before eow --search accountant --limit 20',
139
+ "dayhand list --limit 50 --cursor <nextCursor> --json",
140
+ "dayhand list --done --json --fields ref,title,status",
141
+ "dayhand list --ready --json",
142
+ ],
143
+ run: async (ctx, _args, opts) => {
144
+ const statuses = parseStatuses(opts.status);
145
+ const includeDone = Boolean(opts.done) || Boolean(statuses?.includes("done"));
146
+ const input = { includeDone, includeArchived: Boolean(opts.archived) };
147
+ if (opts.label.length)
148
+ input.labels = opts.label;
149
+ const value = await listTasks(ctx, input, opts);
150
+ return { value, view: { kind: "tasks", tasks: asList(value), empty: hasFilters(opts) ? "no tasks match" : undefined } };
151
+ },
152
+ },
153
+ {
154
+ name: "board",
155
+ schema: "board.schema.json",
156
+ summary: "Kanban board: todo, doing and done columns",
157
+ options: [{ flags: "--done-limit <n>", description: "Maximum tasks in the done column (default 50)", kind: "int" }],
158
+ tools: ["board"],
159
+ fields: "task",
160
+ examples: ["dayhand board", "dayhand board --done-limit 5 --json"],
161
+ run: async (ctx, _args, opts) => {
162
+ const value = await tool(ctx, "board", opts.doneLimit === undefined ? {} : { doneLimit: opts.doneLimit });
163
+ const section = (name) => ({ name, label: name.toUpperCase(), tasks: value[name], count: true, empty: "empty" });
164
+ return { value, view: { kind: "sections", columns: true, sections: [section("todo"), section("doing"), section("done")] } };
165
+ },
166
+ },
167
+ {
168
+ name: "show",
169
+ schema: "task-detail.schema.json",
170
+ summary: "One task with its change history (who changed what, when)",
171
+ args: [REF],
172
+ tools: ["get_task"],
173
+ fields: "task",
174
+ examples: ["dayhand show 42", "dayhand show now --json"],
175
+ run: async (ctx, args) => {
176
+ const taskId = await resolveRef(ctx, args.ref);
177
+ const [value, timezone] = await Promise.all([tool(ctx, "get_task", { taskId }), userTimezone(ctx)]);
178
+ return { value, view: { kind: "detail", task: value.task, history: value.history, timezone } };
179
+ },
180
+ },
181
+ {
182
+ name: "edit",
183
+ schema: "task.schema.json",
184
+ summary: "Change a task's fields; --clear-* removes one; on a terminal without options, edit title and note in $EDITOR",
185
+ args: [REF],
186
+ options: [
187
+ { flags: "--title <text>", description: "New title" },
188
+ { flags: "--note <text>", description: "Replace the note" },
189
+ { flags: "--clear-note", description: "Remove the note" },
190
+ ...SCHEDULE_OPTIONS,
191
+ { flags: "--clear-due", description: "Remove the due day (and time)" },
192
+ { flags: "--clear-time", description: "Remove the due time" },
193
+ { flags: "--clear-repeat", description: "Stop repeating" },
194
+ { flags: "--resume-note <text>", description: "Where you stopped / next step" },
195
+ { flags: "--clear-resume-note", description: "Remove the resume note" },
196
+ { flags: "-l, --label <label>", description: "Replace all labels (repeatable)", kind: "list" },
197
+ { flags: "--add-label <label>", description: "Add a label (repeatable)", kind: "list" },
198
+ { flags: "--rm-label <label>", description: "Remove a label (repeatable)", kind: "list" },
199
+ { flags: "--clear-labels", description: "Remove all labels" },
200
+ { flags: "--link <url>", description: 'Replace all links: "https://… Title" (repeatable)', kind: "list" },
201
+ { flags: "--clear-links", description: "Remove all links" },
202
+ { flags: "--item <text>", description: "Replace the checklist (repeatable; dayhand checklist edits single items)", kind: "list" },
203
+ { flags: "--clear-checklist", description: "Remove the checklist" },
204
+ ...REMINDER_OPTIONS,
205
+ { flags: "--default-reminders", description: "Back to the default reminders from settings" },
206
+ { ...WORK_OPTIONS[0], description: "Replace the acceptance criteria (repeatable; dayhand criteria edits single ones)" },
207
+ { flags: "--clear-acceptance", description: "Remove the acceptance criteria" },
208
+ WORK_OPTIONS[1],
209
+ { flags: "--clear-plan", description: "Remove the plan" },
210
+ { ...WORK_OPTIONS[2], description: "Replace the dependencies: a task that must be done first (repeatable)" },
211
+ { flags: "--clear-depends", description: "Remove all dependencies" },
212
+ WORK_OPTIONS[3],
213
+ { flags: "--unblock", description: "Remove the blocked reason" },
214
+ ],
215
+ tools: ["update_task"],
216
+ fields: "task",
217
+ examples: [
218
+ 'dayhand edit 42 --title "Call the bank" --due "tomorrow 10:00"',
219
+ "dayhand edit 42 --add-label work --rm-label home",
220
+ "dayhand edit 42 --every month --month-day last --until 2027-06-30",
221
+ 'dayhand edit 42 --rrule "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO"',
222
+ "dayhand edit 42 --remind 15m --remind 1d",
223
+ "dayhand edit 42 --clear-repeat",
224
+ 'dayhand edit 42 --blocked "waiting for the client\'s brief"',
225
+ "dayhand edit 42 --depends-on 40 --depends-on 41",
226
+ "dayhand edit 42",
227
+ ],
228
+ run: async (ctx, args, opts) => {
229
+ const taskId = await resolveRef(ctx, args.ref);
230
+ const input = await editArgs(ctx, taskId, opts);
231
+ if (Object.keys(input).length === 0 && ctx.interactive && ctx.format === "human")
232
+ return editInEditor(ctx, taskId);
233
+ if (Object.keys(input).length === 0)
234
+ throw usageError("Nothing to change: give at least one option", { hint: "dayhand edit --help" });
235
+ const task = await tool(ctx, "update_task", { taskId, ...input, clientId: ctx.clientId() });
236
+ return { value: task, view: { kind: "task", verb: "Updated", task } };
237
+ },
238
+ },
239
+ {
240
+ name: "note",
241
+ schema: ["task-detail.schema.json", "task.schema.json"],
242
+ summary: "Print a task's note (on a terminal: edit it in $VISUAL/$EDITOR), or append text to it",
243
+ args: [REF, { name: "text", description: "Text to append as a new line", variadic: true }],
244
+ options: [{ flags: "--clear", description: "Remove the note" }],
245
+ tools: ["get_task", "update_task"],
246
+ fields: "task",
247
+ examples: ["dayhand note 42", 'dayhand note 42 "Account number is in the email"', "dayhand note 42 --clear"],
248
+ run: async (ctx, args, opts) => {
249
+ const taskId = await resolveRef(ctx, args.ref);
250
+ const text = (args.text ?? []).join(" ").trim();
251
+ if (!text && !opts.clear) {
252
+ const value = await tool(ctx, "get_task", { taskId });
253
+ if (ctx.interactive && ctx.format === "human")
254
+ return editNote(ctx, taskId, value.task);
255
+ return { value, view: { kind: "text", text: value.task.note ?? "(no note)" } };
256
+ }
257
+ if (text && opts.clear)
258
+ throw usageError("Give text to append or --clear, not both");
259
+ let note = null;
260
+ if (text) {
261
+ const current = (await tool(ctx, "get_task", { taskId })).task.note;
262
+ note = current ? `${current}\n${text}` : text;
263
+ }
264
+ const task = await tool(ctx, "update_task", { taskId, note, clientId: ctx.clientId() });
265
+ return { value: task, view: { kind: "task", verb: "Note updated", task } };
266
+ },
267
+ },
268
+ {
269
+ name: "checklist",
270
+ schema: ["task-detail.schema.json", "task.schema.json"],
271
+ summary: "Print a task's checklist, or tick, untick, move, remove and add items",
272
+ description: "Items are numbered from 1 as printed. Changes apply in this order: --check, --uncheck, --move, --rm, then --add.",
273
+ args: [REF],
274
+ options: [
275
+ { flags: "--add <text>", description: "Add an item at the end (repeatable)", kind: "list" },
276
+ { flags: "--check <n>", description: "Tick items: 2, 1,3 or all (repeatable)", kind: "list" },
277
+ { flags: "--uncheck <n>", description: "Untick items (repeatable)", kind: "list" },
278
+ { flags: "--move <from:to>", description: "Move an item, e.g. 3:1 (repeatable)", kind: "list" },
279
+ { flags: "--rm <n>", description: "Remove items (repeatable)", kind: "list" },
280
+ { flags: "--clear", description: "Remove the whole checklist" },
281
+ ],
282
+ tools: ["get_task", "update_task"],
283
+ fields: "task",
284
+ examples: ["dayhand checklist 42", 'dayhand checklist 42 --add "Book the hotel" --add "Pack"', "dayhand checklist 42 --check 1,2", "dayhand checklist 42 --move 3:1 --rm 4"],
285
+ run: async (ctx, args, opts) => {
286
+ const taskId = await resolveRef(ctx, args.ref);
287
+ const detail = await tool(ctx, "get_task", { taskId });
288
+ const edits = ["add", "check", "uncheck", "move", "rm"].some((name) => opts[name].length > 0);
289
+ if (!edits && !opts.clear)
290
+ return { value: detail, view: { kind: "text", text: checklistText(detail.task) } };
291
+ if (edits && opts.clear)
292
+ throw usageError("--clear cannot be combined with other changes");
293
+ const checklist = opts.clear ? null : editChecklist(detail.task.checklist ?? [], opts);
294
+ const task = await tool(ctx, "update_task", { taskId, checklist, clientId: ctx.clientId() });
295
+ return { value: task, view: { kind: "text", text: checklistText(task) } };
296
+ },
297
+ },
298
+ {
299
+ name: "move",
300
+ schema: "task.schema.json",
301
+ summary: "Move a task to another day, optionally with a time (none removes the date)",
302
+ args: [REF, { name: "date", description: "When: tomorrow, fri 14:00, next mon, 2026-10-23 … or none (dayhand help dates)", required: true }],
303
+ options: [
304
+ { flags: "--time <HH:MM>", description: "New due time" },
305
+ { flags: "--clear-time", description: "Remove the due time" },
306
+ ],
307
+ tools: ["reschedule_task"],
308
+ fields: "task",
309
+ examples: ["dayhand move 42 tomorrow --time 08:00", 'dayhand move 42 "next mon 9am"', "dayhand move 42 none"],
310
+ run: async (ctx, args, opts) => {
311
+ const taskId = await resolveRef(ctx, args.ref);
312
+ const date = args.date.trim();
313
+ const input = { taskId };
314
+ if (date.toLowerCase() === "none")
315
+ input.dueDate = null;
316
+ else
317
+ await applyDue(ctx, input, date, opts);
318
+ input.clientId = ctx.clientId();
319
+ if (opts.time !== undefined)
320
+ input.dueTime = opts.time;
321
+ if (opts.clearTime)
322
+ input.dueTime = null;
323
+ const task = await tool(ctx, "reschedule_task", input);
324
+ return { value: task, view: { kind: "task", verb: "Moved", task } };
325
+ },
326
+ },
327
+ {
328
+ name: "done",
329
+ schema: "complete.schema.json",
330
+ summary: "Mark tasks done, optionally with proof; a repeating task gets its next occurrence",
331
+ description: "An agent completing a task with acceptance criteria needs every criterion met (dayhand criteria) and --summary; otherwise it fails with precondition_failed (exit 6).",
332
+ args: [REFS],
333
+ options: PROOF_OPTIONS,
334
+ tools: ["complete_task"],
335
+ fields: "task",
336
+ examples: [
337
+ "dayhand done 42",
338
+ "dayhand done 42 43 now",
339
+ "dayhand done 1 2 3 --yes --client-id 7f3c",
340
+ 'dayhand done 42 --summary "Export shipped" --check "unit tests=pass https://ci.example/run/7" --proof-link "https://example.com/pr/7 PR"',
341
+ ],
342
+ run: async (ctx, args, opts) => {
343
+ const proof = proofInput(opts);
344
+ const results = await eachRef(ctx, args.refs, "complete", (taskId, clientId) => tool(ctx, "complete_task", proof ? { taskId, proof, clientId } : { taskId, clientId }));
345
+ if (results.length === 1)
346
+ return { value: results[0], view: { kind: "task", verb: "Done", ...results[0] } };
347
+ const tasks = results.flatMap((r) => (r.next ? [r.task, r.next] : [r.task]));
348
+ return { value: { results }, view: { kind: "tasks", verb: "Done (with next occurrences)", tasks } };
349
+ },
350
+ },
351
+ perTask({ name: "start", summary: "Set tasks to doing", verb: "start", done: "Started", examples: ["dayhand start 42"] }, setStatus("doing"), ["set_status"]),
352
+ perTask({ name: "stop", summary: "Set tasks back to todo (from doing)", verb: "stop", done: "Stopped", examples: ["dayhand stop 42"] }, setStatus("todo"), ["set_status"]),
353
+ perTask({ name: "reopen", summary: "Set done tasks back to todo", verb: "reopen", done: "Reopened", examples: ["dayhand reopen 42"] }, setStatus("todo"), ["set_status"]),
354
+ {
355
+ name: "now",
356
+ schema: ["set-now.schema.json", "today.schema.json"],
357
+ summary: "Show the now task, make a task the now task (--note: where you stopped), or --clear",
358
+ args: [{ name: "ref", description: "Task to make the now task" }],
359
+ options: [
360
+ { flags: "--note <text>", description: "Resume note shown on Today" },
361
+ { flags: "--clear", description: "Clear the now task" },
362
+ ],
363
+ tools: ["set_now", "list_today"],
364
+ fields: "task",
365
+ examples: ["dayhand now", 'dayhand now 42 --note "have the account number"', "dayhand now --clear"],
366
+ run: async (ctx, args, opts) => {
367
+ const ref = args.ref;
368
+ if (opts.clear) {
369
+ if (ref || opts.note !== undefined)
370
+ throw usageError("--clear takes no task and no --note");
371
+ const value = await tool(ctx, "set_now", { taskId: null, clientId: ctx.clientId() });
372
+ return { value, view: { kind: "text", text: "Now task cleared" } };
373
+ }
374
+ if (!ref) {
375
+ if (opts.note !== undefined)
376
+ throw usageError("--note needs a task: dayhand now <ref> --note <text>");
377
+ const value = await tool(ctx, "list_today");
378
+ return { value, view: value.now ? { kind: "task", verb: "Now", task: value.now } : { kind: "text", text: "(no now task)" } };
379
+ }
380
+ const input = { taskId: await resolveRef(ctx, ref), clientId: ctx.clientId() };
381
+ if (opts.note !== undefined)
382
+ input.resumeNote = opts.note;
383
+ const task = await tool(ctx, "set_now", input);
384
+ return { value: task, view: { kind: "task", verb: "Now", task } };
385
+ },
386
+ },
387
+ perTask({ name: "archive", summary: "Archive tasks: off Today and the board, kept in exports", verb: "archive", done: "Archived", examples: ["dayhand archive 42"] }, (ctx, taskId, clientId) => tool(ctx, "archive_task", { taskId, clientId }), ["archive_task"]),
388
+ perTask({ name: "rm", aliases: ["delete"], summary: "Move tasks to the trash (purged after 30 days)", verb: "delete", done: "Moved to the trash", examples: ["dayhand rm 42", "dayhand rm 4 5 6 --yes"] }, (ctx, taskId, clientId) => tool(ctx, "delete_task", { taskId, clientId }), ["delete_task"]),
389
+ perTask({ name: "restore", summary: "Restore tasks from the trash (or the archive)", verb: "restore", done: "Restored", examples: ["dayhand restore 42"] }, (ctx, taskId, clientId) => tool(ctx, "restore_task", { taskId, clientId }), ["restore_task"]),
390
+ {
391
+ name: "trash",
392
+ schema: "task-list.schema.json",
393
+ summary: "Tasks in the trash, newest first",
394
+ tools: ["list_trash"],
395
+ fields: "task",
396
+ examples: ["dayhand trash", "dayhand trash --json"],
397
+ run: async (ctx) => {
398
+ const value = await tool(ctx, "list_trash");
399
+ return { value, view: { kind: "tasks", tasks: asList(value), empty: "trash is empty" } };
400
+ },
401
+ },
402
+ ];
403
+ /** "#42 Pack (1/2)" and the numbered items, as `dayhand checklist` prints them. */
404
+ function checklistText(task) {
405
+ const items = task.checklist ?? [];
406
+ const ref = task.ref !== undefined ? `#${task.ref}` : task.id;
407
+ if (items.length === 0)
408
+ return `${ref} ${task.title}: (no checklist)`;
409
+ const done = items.filter((item) => item.done).length;
410
+ return [`${ref} ${task.title} (${done}/${items.length})`, ...items.map((item, i) => ` ${i + 1}. ${item.done ? "[x]" : "[ ]"} ${item.title}`)].join("\n");
411
+ }
412
+ /** Copies defined options to tool argument names. */
413
+ function copy(opts, input, names) {
414
+ for (const [from, to] of Object.entries(names))
415
+ if (opts[from] !== undefined)
416
+ input[to] = opts[from];
417
+ }
418
+ async function editArgs(ctx, taskId, opts) {
419
+ const input = {};
420
+ copy(opts, input, { title: "title", note: "note", time: "dueTime", resumeNote: "resumeNote" });
421
+ if (opts.due !== undefined)
422
+ await applyDue(ctx, input, opts.due, opts);
423
+ await workArgs(ctx, opts, input);
424
+ const clears = {
425
+ clearNote: "note",
426
+ clearDue: "dueDate",
427
+ clearTime: "dueTime",
428
+ clearRepeat: "repeat",
429
+ clearResumeNote: "resumeNote",
430
+ clearAcceptance: "acceptance",
431
+ clearPlan: "plan",
432
+ unblock: "blockedReason",
433
+ };
434
+ for (const [flag, field] of Object.entries(clears)) {
435
+ if (!opts[flag])
436
+ continue;
437
+ if (input[field] !== undefined)
438
+ throw usageError(`--${kebab(flag)} cannot be combined with a new value for it`);
439
+ input[field] = null;
440
+ }
441
+ const repeat = await repeatInput(ctx, opts);
442
+ if (repeat && opts.clearRepeat)
443
+ throw usageError("--clear-repeat cannot be combined with the repeat options");
444
+ if (repeat)
445
+ input.repeat = repeat;
446
+ const items = opts.item;
447
+ if (items.length && opts.clearChecklist)
448
+ throw usageError("--item cannot be combined with --clear-checklist");
449
+ if (items.length)
450
+ input.checklist = checklistOf(items);
451
+ if (opts.clearChecklist)
452
+ input.checklist = null;
453
+ const reminders = remindersInput(opts);
454
+ if (reminders !== undefined)
455
+ input.reminders = reminders;
456
+ const set = opts.label;
457
+ const added = opts.addLabel;
458
+ const removed = opts.rmLabel;
459
+ if ([set.length > 0, added.length + removed.length > 0, Boolean(opts.clearLabels)].filter(Boolean).length > 1) {
460
+ throw usageError("Use one of --label, --add-label/--rm-label or --clear-labels");
461
+ }
462
+ if (set.length)
463
+ input.labels = set;
464
+ if (opts.clearLabels)
465
+ input.labels = [];
466
+ if (added.length || removed.length) {
467
+ const current = (await tool(ctx, "get_task", { taskId })).task.labels;
468
+ input.labels = [...new Set([...current, ...added])].filter((l) => !removed.includes(l));
469
+ }
470
+ if (opts.clearDepends) {
471
+ if (input.dependsOn !== undefined)
472
+ throw usageError("--depends-on cannot be combined with --clear-depends");
473
+ input.dependsOn = [];
474
+ }
475
+ const links = opts.link;
476
+ if (links.length && opts.clearLinks)
477
+ throw usageError("--link cannot be combined with --clear-links");
478
+ if (links.length)
479
+ input.links = links.map(parseLink);
480
+ if (opts.clearLinks)
481
+ input.links = [];
482
+ return input;
483
+ }
484
+ const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
485
+ /** `dayhand note <ref>` on a terminal: the note in the editor; an unchanged note sends nothing. */
486
+ async function editNote(ctx, taskId, task) {
487
+ const before = task.note ?? "";
488
+ const after = ctx.edit(before ? `${before}\n` : "", `dayhand-note-${task.ref ?? "task"}.md`).replace(/\s+$/, "");
489
+ if (after === before)
490
+ return { value: task, view: { kind: "text", text: "Note unchanged" } };
491
+ const updated = await tool(ctx, "update_task", { taskId, note: after === "" ? null : after, clientId: ctx.clientId() });
492
+ return { value: updated, view: { kind: "task", verb: "Note updated", task: updated } };
493
+ }
494
+ /** `dayhand edit <ref>` on a terminal without options: first line the title, after a blank line the note. */
495
+ async function editInEditor(ctx, taskId) {
496
+ const { task } = await tool(ctx, "get_task", { taskId });
497
+ const text = ctx.edit(`${task.title}\n\n${task.note ? `${task.note}\n` : ""}`, `dayhand-task-${task.ref ?? "task"}.md`);
498
+ const [first = "", ...rest] = text.split(/\r?\n/);
499
+ const title = first.trim();
500
+ if (!title)
501
+ throw usageError("The first line (the title) is empty; nothing was changed");
502
+ const note = rest.join("\n").replace(/^(\s*\n)+/, "").replace(/\s+$/, "");
503
+ const changes = {};
504
+ if (title !== task.title)
505
+ changes.title = title;
506
+ if (note !== (task.note ?? ""))
507
+ changes.note = note === "" ? null : note;
508
+ if (Object.keys(changes).length === 0)
509
+ return { value: task, view: { kind: "text", text: "No changes" } };
510
+ const updated = await tool(ctx, "update_task", { taskId, ...changes, clientId: ctx.clientId() });
511
+ return { value: updated, view: { kind: "task", verb: "Updated", task: updated } };
512
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ // dayhand ui: the full-screen terminal app (src/ui/). Ink and React load only
2
+ // here, so every other command starts as fast as before.
3
+ import { usageError } from "../core/exit.js";
4
+ import { LANGUAGES, matchLanguage, resolveLanguage } from "../i18n/index.js";
5
+ const VIEWS = ["today", "board", "week", "trash"];
6
+ const INTERVAL = { min: 10, max: 300, default: 12 };
7
+ /** A hung server must not keep a change waiting for the default 30 s. */
8
+ const TIMEOUT_MS = 10_000;
9
+ export const uiCommands = [
10
+ {
11
+ name: "ui",
12
+ summary: "Dayhand full screen in the terminal: Today, board, week and trash, with keys like vim and lazygit (dayhand help ui)",
13
+ description: "Needs a terminal. Refreshes every 12 s by default, within the key's limits of 120 reads and 60 changes a minute. " +
14
+ "q quits, ? lists the keys; dayhand help ui prints them. The screens speak the terminal's language (LC_ALL, LC_MESSAGES " +
15
+ `or LANG, overridden by DAYHAND_LANG or --lang) when it is one of ${LANGUAGES.join(", ")}, else English; ` +
16
+ "date phrases stay English.",
17
+ options: [
18
+ { flags: "--view <view>", description: "The view to open with", choices: VIEWS },
19
+ { flags: "--interval <seconds>", description: `Seconds between refreshes, ${INTERVAL.min} to ${INTERVAL.max} (default ${INTERVAL.default})`, kind: "int" },
20
+ { flags: "--no-mouse", description: "Leave the mouse to the terminal (for selecting text)" },
21
+ { flags: "--lang <language>", description: `The screens' language: ${LANGUAGES.join(", ")} (default: DAYHAND_LANG, then the locale)` },
22
+ ],
23
+ tools: ["list_today", "board", "list_all", "list_trash", "get_task", "get_settings", "add_task", "update_task", "set_status", "set_now", "complete_task", "reschedule_task", "delete_task", "restore_task"],
24
+ dryRun: false,
25
+ examples: ["dayhand ui", "dayhand ui --view week", "dayhand ui --interval 30 --no-mouse", "dayhand ui --lang pl"],
26
+ run: async (ctx, _args, opts) => {
27
+ if (ctx.format !== "human")
28
+ throw usageError("dayhand ui is for a terminal; scripts use dayhand today --json, dayhand board --json or dayhand list --json");
29
+ if (!ctx.interactive)
30
+ throw usageError("dayhand ui needs a terminal (stdin and stdout) and input allowed (no --no-input or DAYHAND_NO_INPUT)", { hint: "dayhand today" });
31
+ const interval = opts.interval ?? INTERVAL.default;
32
+ if (interval < INTERVAL.min || interval > INTERVAL.max)
33
+ throw usageError(`--interval must be ${INTERVAL.min} to ${INTERVAL.max} seconds`);
34
+ const lang = opts.lang;
35
+ if (lang !== undefined && !matchLanguage(lang))
36
+ throw usageError(`--lang must be one of ${LANGUAGES.join(", ")}`, { valid: [...LANGUAGES] });
37
+ const { connection } = await ctx.client();
38
+ const [{ DayhandClient }, { makeApi }, { runUi }] = await Promise.all([import("../transport/client.js"), import("../ui/api.js"), import("../ui/run.js")]);
39
+ await runUi({
40
+ api: makeApi(new DayhandClient(connection, ctx.io.fetch, undefined, TIMEOUT_MS)),
41
+ view: opts.view ?? "today",
42
+ interval,
43
+ mouse: opts.mouse !== false,
44
+ language: resolveLanguage(lang, ctx.env),
45
+ color: ctx.color(),
46
+ editText: (text, fileName) => ctx.edit(text, fileName),
47
+ });
48
+ return { value: null, view: { kind: "text", text: "" } };
49
+ },
50
+ },
51
+ ];