@skillcdn/console 0.1.8 → 0.1.10

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 (147) hide show
  1. package/README.md +5 -5
  2. package/dist/api.d.ts +5 -5
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +255 -10
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +9 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +80 -0
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +62 -7
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +31 -4
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +19 -3
  19. package/dist/client.js.map +1 -1
  20. package/dist/components/board.d.ts +27 -1
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +75 -7
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/calendar.d.ts +34 -0
  25. package/dist/components/calendar.d.ts.map +1 -0
  26. package/dist/components/calendar.js +96 -0
  27. package/dist/components/calendar.js.map +1 -0
  28. package/dist/components/categories.d.ts +13 -0
  29. package/dist/components/categories.d.ts.map +1 -0
  30. package/dist/components/categories.js +52 -0
  31. package/dist/components/categories.js.map +1 -0
  32. package/dist/components/decision-list.d.ts +10 -0
  33. package/dist/components/decision-list.d.ts.map +1 -1
  34. package/dist/components/decision-list.js +14 -1
  35. package/dist/components/decision-list.js.map +1 -1
  36. package/dist/components/event-feed.d.ts.map +1 -1
  37. package/dist/components/event-feed.js +12 -0
  38. package/dist/components/event-feed.js.map +1 -1
  39. package/dist/components/goals.d.ts +35 -0
  40. package/dist/components/goals.d.ts.map +1 -0
  41. package/dist/components/goals.js +45 -0
  42. package/dist/components/goals.js.map +1 -0
  43. package/dist/components/task-form.d.ts +5 -1
  44. package/dist/components/task-form.d.ts.map +1 -1
  45. package/dist/components/task-form.js +13 -1
  46. package/dist/components/task-form.js.map +1 -1
  47. package/dist/components/task-list.d.ts +16 -0
  48. package/dist/components/task-list.d.ts.map +1 -0
  49. package/dist/components/task-list.js +14 -0
  50. package/dist/components/task-list.js.map +1 -0
  51. package/dist/components/task-view.d.ts +9 -1
  52. package/dist/components/task-view.d.ts.map +1 -1
  53. package/dist/components/task-view.js +10 -4
  54. package/dist/components/task-view.js.map +1 -1
  55. package/dist/components/ui.d.ts +30 -1
  56. package/dist/components/ui.d.ts.map +1 -1
  57. package/dist/components/ui.js +60 -0
  58. package/dist/components/ui.js.map +1 -1
  59. package/dist/components/work.d.ts +10 -0
  60. package/dist/components/work.d.ts.map +1 -0
  61. package/dist/components/work.js +29 -0
  62. package/dist/components/work.js.map +1 -0
  63. package/dist/console.css +511 -0
  64. package/dist/console.d.ts +8 -0
  65. package/dist/console.d.ts.map +1 -1
  66. package/dist/console.js +124 -8
  67. package/dist/console.js.map +1 -1
  68. package/dist/data.d.ts +14 -1
  69. package/dist/data.d.ts.map +1 -1
  70. package/dist/data.js +36 -2
  71. package/dist/data.js.map +1 -1
  72. package/dist/dates.d.ts +39 -0
  73. package/dist/dates.d.ts.map +1 -0
  74. package/dist/dates.js +120 -0
  75. package/dist/dates.js.map +1 -0
  76. package/dist/i18n/en.d.ts +162 -1
  77. package/dist/i18n/en.d.ts.map +1 -1
  78. package/dist/i18n/en.js +174 -1
  79. package/dist/i18n/en.js.map +1 -1
  80. package/dist/i18n/ko.d.ts.map +1 -1
  81. package/dist/i18n/ko.js +169 -1
  82. package/dist/i18n/ko.js.map +1 -1
  83. package/dist/index.d.ts +10 -4
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +10 -4
  86. package/dist/index.js.map +1 -1
  87. package/dist/limits.d.ts +5 -0
  88. package/dist/limits.d.ts.map +1 -1
  89. package/dist/limits.js +5 -0
  90. package/dist/limits.js.map +1 -1
  91. package/dist/router.d.ts +50 -1
  92. package/dist/router.d.ts.map +1 -1
  93. package/dist/router.js +74 -1
  94. package/dist/router.js.map +1 -1
  95. package/dist/routes.d.ts +7 -1
  96. package/dist/routes.d.ts.map +1 -1
  97. package/dist/routes.js +9 -0
  98. package/dist/routes.js.map +1 -1
  99. package/dist/schemas.d.ts +366 -0
  100. package/dist/schemas.d.ts.map +1 -1
  101. package/dist/schemas.js +112 -3
  102. package/dist/schemas.js.map +1 -1
  103. package/dist/vocabulary.d.ts +15 -1
  104. package/dist/vocabulary.d.ts.map +1 -1
  105. package/dist/vocabulary.js +26 -0
  106. package/dist/vocabulary.js.map +1 -1
  107. package/docs/specs/cli.md +11 -4
  108. package/docs/specs/rest.md +33 -8
  109. package/package.json +1 -1
  110. package/skills/building-a-console/SKILL.md +4 -4
  111. package/skills/building-a-console/references/the-default-console.md +1 -1
  112. package/skills/deploying-a-console/SKILL.md +11 -10
  113. package/skills/deploying-a-console/assets/deployer-policy.json +13 -12
  114. package/skills/deploying-a-console/assets/secrets.example.json +4 -4
  115. package/skills/deploying-a-console/references/access.md +10 -9
  116. package/skills/deploying-a-console/references/resources.md +107 -50
  117. package/skills/deploying-a-console/references/sizing.md +1 -1
  118. package/skills/deploying-a-console/scripts/store-secrets.mjs +1 -15
  119. package/skills/working-the-board/SKILL.md +7 -6
  120. package/src/api.ts +30 -0
  121. package/src/cli/cli.ts +311 -10
  122. package/src/cli/format.ts +88 -0
  123. package/src/cli/help.ts +69 -7
  124. package/src/client.ts +70 -2
  125. package/src/components/board.tsx +205 -12
  126. package/src/components/calendar.tsx +255 -0
  127. package/src/components/categories.tsx +209 -0
  128. package/src/components/decision-list.tsx +62 -5
  129. package/src/components/event-feed.tsx +12 -0
  130. package/src/components/goals.tsx +264 -0
  131. package/src/components/task-form.tsx +78 -1
  132. package/src/components/task-list.tsx +61 -0
  133. package/src/components/task-view.tsx +74 -3
  134. package/src/components/ui.tsx +116 -1
  135. package/src/components/work.tsx +83 -0
  136. package/src/console.tsx +337 -4
  137. package/src/data.ts +63 -8
  138. package/src/dates.ts +143 -0
  139. package/src/i18n/en.ts +184 -1
  140. package/src/i18n/ko.ts +175 -1
  141. package/src/index.ts +58 -1
  142. package/src/limits.ts +5 -0
  143. package/src/router.ts +119 -6
  144. package/src/routes.ts +10 -0
  145. package/src/schemas.ts +141 -1
  146. package/src/styles/console.css +511 -0
  147. package/src/vocabulary.ts +33 -0
@@ -78,7 +78,7 @@ Part of the plan, in plain words, one line per item, the chosen size first:
78
78
  ## Options the plan offers
79
79
 
80
80
  - **The database as serverless capacity.** Aurora Serverless v2 for PostgreSQL scales its capacity with load, down to nothing when idle (0 ACU with automatic pause, since 2024-11). The console holds a listening connection open and sweeps every fifteen minutes, so it never idles and the database never pauses: the floor is 0.5 ACU, about $44 a month at $0.12 per ACU-hour (read 2026-10), more than the `small` and `medium` instances. Offer it when the organization wants capacity that follows load without resizing, and say the floor.
81
- - **The bucket on Cloudflare R2** instead of S3: the same S3 API, keys made in the Cloudflare dashboard, `S3_ENDPOINT=https://<account id>.r2.cloudflarestorage.com`, `S3_REGION=auto`, path style; no egress charges. Offer it when the organization keeps its files at Cloudflare already; S3 stays the default because the keys, the budget and the audit are then in one account.
81
+ - **The bucket on Cloudflare R2** instead of S3: the same S3 API, `S3_ENDPOINT=https://<account id>.r2.cloudflarestorage.com`, `S3_REGION=auto`, path style; no egress charges. Its keys are made in the Cloudflare dashboard (R2, Manage API tokens, Object Read & Write on the one bucket), saved in the secrets file as `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`, stored as parameters and carried by the `api` task as secrets, with no `S3_CREDENTIALS` line: the task's role cannot reach R2. Offer it when the organization keeps its files at Cloudflare already; S3 stays the default because the task's own role reaches it with no key to keep, and the budget and the audit are then in one account.
82
82
  - **A load balancer instead of the tunnel.** ECS Express Mode creates an Application Load Balancer with HTTPS on an AWS domain in one command, at about $16 to $23 a month for the balancer (read 2026-10); the custom domain is a manual step on the balancer, and Cloudflare then proxies to it. Offer it only when the organization does not want its traffic through the tunnel; the tunnel is the default because it costs nothing, exposes nothing, and brings the client's address to the console cleanly.
83
83
  - **ARM tasks** when the organization has ARM runners for its builds (building for ARM on an x86 runner takes many times longer): a fifth off the task line.
84
84
 
@@ -5,7 +5,6 @@
5
5
  // node store-secrets.mjs set <file> <NAME> the value is standard input
6
6
  // node store-secrets.mjs derive <file> <NAME> <template> {{OTHER}} in the template is replaced
7
7
  // node store-secrets.mjs render <file> <in> <out> a JSON file with {{NAME}} placeholders filled
8
- // node store-secrets.mjs key <file> <iam user> creates an access key for the user and stores both halves
9
8
  // node store-secrets.mjs store <file> <prefix> every value as a SecureString parameter <prefix>/<NAME>
10
9
  import { execFileSync } from "node:child_process";
11
10
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -15,7 +14,7 @@ import process from "node:process";
15
14
 
16
15
  const [mode, file, ...rest] = process.argv.slice(2);
17
16
  const usage = () => {
18
- process.stderr.write("usage: node store-secrets.mjs set|derive|render|key|store <file> ...\n");
17
+ process.stderr.write("usage: node store-secrets.mjs set|derive|render|store <file> ...\n");
19
18
  process.exit(2);
20
19
  };
21
20
  if (mode === undefined || file === undefined) {
@@ -73,19 +72,6 @@ switch (mode) {
73
72
  writeFileSync(output, fill(read(), readFileSync(input, "utf8")), { mode: 0o600 });
74
73
  break;
75
74
  }
76
- case "key": {
77
- const [user] = rest;
78
- if (user === undefined) {
79
- usage();
80
- }
81
- const answer = JSON.parse(aws(["iam", "create-access-key"], { UserName: user }));
82
- write({
83
- ...read(),
84
- S3_ACCESS_KEY_ID: answer.AccessKey.AccessKeyId,
85
- S3_SECRET_ACCESS_KEY: answer.AccessKey.SecretAccessKey,
86
- });
87
- break;
88
- }
89
75
  case "store": {
90
76
  const [prefix] = rest;
91
77
  if (prefix === undefined) {
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
6
6
  metadata:
7
7
  author: skillcdn
8
- version: "1.1"
8
+ version: "1.2"
9
9
  skillcdn:
10
10
  translations:
11
11
  ko:
@@ -37,8 +37,8 @@ The full contract is the [specification of the command line](/docs/specs/cli.md)
37
37
 
38
38
  | Input | Source |
39
39
  |---|---|
40
- | The task | Its number (`#7`) or its id, from what the person said. Told to pick one, `console tasks --state ready` lists what is ready; a task is never taken without being told which, or that any ready one may be. |
41
- | The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask. The pages it links to (`console doc <path>`) and the project's skills (`console skills`, each loaded through the agent's own SkillCDN connection) say the rest. |
40
+ | The task | Its number (`#7`) or its id, from what the person said. Told to pick one, `console work` lists what is on the person's plate across their projects, the sooner due first, and `console tasks --state ready --mine` what is ready and theirs in this one (`--due overdue`, `--due week`, `--category <key>` and `--goal <id>` narrow it further); a task is never taken without being told which, or that any ready one may be. |
41
+ | The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask; with its plan, the category, the goal it works towards (`console goal <id>` says what reaching it means) and the days it starts and is due, which the reports and the summary keep to. The pages it links to (`console doc <path>`) and the project's skills (`console skills`, each loaded through the agent's own SkillCDN connection) say the rest. |
42
42
  | The organization's way | Its own skill for its console, when it has one, adds which tasks to take, what to report and when to ask; it comes through the same kind of address as its other skills. |
43
43
 
44
44
  ## Workflow
@@ -47,7 +47,7 @@ Each phase ends with something on the board. Only phase 4 waits for a person.
47
47
 
48
48
  ### Phase 1: Read
49
49
 
50
- `console task <number>` shows the task in full, the decisions about it and the runs on it. A run open on it by another agent means the task is taken: say so and stop. Read what the body links to, with `console doc <path>` for the project's pages and `console skills` for the organization's skills, before anything is made.
50
+ `console task <number>` shows the task in full, with its plan, the decisions about it and the runs on it. A run open on it by another agent means the task is taken: say so and stop. A task that is past its due day is still the task; the first report says so, and what the day was for is kept in mind. Read what the body links to, with `console doc <path>` for the project's pages and `console skills` for the organization's skills, before anything is made.
51
51
 
52
52
  ### Phase 2: Take
53
53
 
@@ -77,10 +77,11 @@ After the last report, `console finish --summary "<what was done, and what is le
77
77
 
78
78
  - Never look for, read, print or send a token. The rules of the repository say the rest.
79
79
  - One run at a time: a command on a run acts on the one open run of this token in the project; working two tasks at once, say which with `--run <id>`.
80
- - The board's words are the vocabulary's own: states `idea`, `ready`, `in_progress`, `in_review`, `done` and `dropped`; priorities `low`, `normal`, `high` and `urgent`. Taking a task moves it, finishing moves it; it is not moved by hand around a run.
80
+ - The board's words are the vocabulary's own: states `idea`, `ready`, `in_progress`, `in_review`, `done` and `dropped`; priorities `low`, `normal`, `high` and `urgent`; a day is `2026-10-24`. Taking a task moves it, finishing moves it; it is not moved by hand around a run. When a task is started and when it is done, the board writes down itself: never typed.
81
+ - The plan is the person's: a task's days, its category and its goal are changed (`console task edit`) only when the task says so or a person asks, and a goal is set or moved (`console goal new`, `console goal edit`) only when the person is an owner and said to.
81
82
  - Exit codes: `0` done; `1` refused or unreachable, the reason on standard error as a code and words, which `console help <command>` names; `2` not understood; `3` a decision still waits.
82
83
  - Everything sent is held to limits the console states (`console help <command>`); a refusal `request.invalid` names the field.
83
84
 
84
85
  ## Terms
85
86
 
86
- - **Task**: what is to be done, numbered per project. **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for. **Decision**: a question with options, which a person answers on the board. **Page**: a document of the project, Markdown at a path that does not change. **Project**: a board of its own, with its members and its skills address.
87
+ - **Task**: what is to be done, numbered per project, with its plan (a category, a goal, the days it starts and is due) and its moments (when it was started and done, which the board keeps). **Goal**: what the project works towards, on its roadmap, with the day it is meant to be reached by; its progress is its tasks. **Category**: a kind of work, the workspace's word for it, by key. **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for. **Decision**: a question with options, which a person answers on the board. **Page**: a document of the project, Markdown at a path that does not change. **Project**: a board of its own, with its members and its skills address.
package/src/api.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  type FetchLike,
14
14
  type FileUpload,
15
15
  type ProjectClient,
16
+ type TaskFilter,
16
17
  } from "./client.js";
17
18
  export {
18
19
  CONNECT_CODE_ALPHABET,
@@ -36,6 +37,9 @@ export {
36
37
  MAX_AGENT_LENGTH,
37
38
  MAX_ARTIFACTS_PER_RUN,
38
39
  MAX_BODY_LENGTH,
40
+ MAX_CATEGORIES,
41
+ MAX_CATEGORY_KEY_LENGTH,
42
+ MAX_CATEGORY_NAME_LENGTH,
39
43
  MAX_DOCUMENT_LENGTH,
40
44
  MAX_DOCUMENT_PATH_LENGTH,
41
45
  MAX_DOCUMENT_SEGMENT_LENGTH,
@@ -68,6 +72,7 @@ export {
68
72
  export {
69
73
  AUTH_ROUTES,
70
74
  CLAIM_PATH,
75
+ categoryPath,
71
76
  connectPath,
72
77
  isSignInFailure,
73
78
  loginPath,
@@ -88,6 +93,10 @@ export {
88
93
  type RestArtifact,
89
94
  type RestArtifactInput,
90
95
  type RestBacklink,
96
+ type RestCategories,
97
+ type RestCategory,
98
+ type RestCategoryInput,
99
+ type RestCategoryPatch,
91
100
  type RestClaim,
92
101
  type RestClaimInput,
93
102
  type RestConnectInput,
@@ -110,6 +119,10 @@ export {
110
119
  type RestEvents,
111
120
  type RestFile,
112
121
  type RestFileInput,
122
+ type RestGoal,
123
+ type RestGoalInput,
124
+ type RestGoalPatch,
125
+ type RestGoals,
113
126
  type RestMe,
114
127
  type RestMember,
115
128
  type RestMemberInput,
@@ -133,6 +146,7 @@ export {
133
146
  type RestSkill,
134
147
  type RestSkills,
135
148
  type RestTask,
149
+ type RestTaskCounts,
136
150
  type RestTaskInput,
137
151
  type RestTaskLink,
138
152
  type RestTaskPatch,
@@ -144,10 +158,15 @@ export {
144
158
  type RestVersion,
145
159
  type RestVersionSummary,
146
160
  type RestVersions,
161
+ type RestWork,
147
162
  restAnswerInputSchema,
148
163
  restArtifactInputSchema,
149
164
  restArtifactSchema,
150
165
  restBacklinkSchema,
166
+ restCategoriesSchema,
167
+ restCategoryInputSchema,
168
+ restCategoryPatchSchema,
169
+ restCategorySchema,
151
170
  restClaimInputSchema,
152
171
  restClaimSchema,
153
172
  restConnectInputSchema,
@@ -170,6 +189,10 @@ export {
170
189
  restEventsSchema,
171
190
  restFileInputSchema,
172
191
  restFileSchema,
192
+ restGoalInputSchema,
193
+ restGoalPatchSchema,
194
+ restGoalSchema,
195
+ restGoalsSchema,
173
196
  restMemberInputSchema,
174
197
  restMemberPatchSchema,
175
198
  restMemberSchema,
@@ -192,6 +215,7 @@ export {
192
215
  restRunsSchema,
193
216
  restSkillSchema,
194
217
  restSkillsSchema,
218
+ restTaskCountsSchema,
195
219
  restTaskInputSchema,
196
220
  restTaskLinkSchema,
197
221
  restTaskPatchSchema,
@@ -204,16 +228,22 @@ export {
204
228
  restVersionSchema,
205
229
  restVersionSummarySchema,
206
230
  restVersionsSchema,
231
+ restWorkSchema,
207
232
  } from "./schemas.js";
208
233
  export { hasForbiddenCodePoint } from "./text.js";
209
234
  export {
210
235
  ARTIFACT_KINDS,
211
236
  type ArtifactKind,
237
+ CATEGORY_COLORS,
238
+ type CategoryColor,
212
239
  EVENT_KINDS,
213
240
  type EventKind,
241
+ GOAL_STATES,
242
+ type GoalState,
214
243
  isProviderKey,
215
244
  LINK_SOURCES,
216
245
  type LinkSource,
246
+ OPEN_TASK_STATES,
217
247
  PERSON_ROLES,
218
248
  type PersonRole,
219
249
  PROJECT_ROLES,
package/src/cli/cli.ts CHANGED
@@ -7,18 +7,29 @@ import {
7
7
  type FetchLike,
8
8
  type ProjectClient,
9
9
  } from "../client.js";
10
+ import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
10
11
  import { MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
11
- import type { RestClaim, RestDecision } from "../schemas.js";
12
- import { TASK_PRIORITIES, TASK_STATES, type TaskPriority, type TaskState } from "../vocabulary.js";
12
+ import type { RestClaim, RestDecision, RestTaskPatch } from "../schemas.js";
13
+ import {
14
+ GOAL_STATES,
15
+ type GoalState,
16
+ TASK_PRIORITIES,
17
+ TASK_STATES,
18
+ type TaskPriority,
19
+ type TaskState,
20
+ } from "../vocabulary.js";
13
21
  import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
14
22
  import type { CredentialStore } from "./credentials.js";
15
23
  import type { DirectoryStore } from "./directory.js";
16
24
  import {
17
25
  formatAnswer,
26
+ formatCategoryLine,
18
27
  formatDecision,
19
28
  formatDecisionLine,
20
29
  formatDocument,
21
30
  formatDocuments,
31
+ formatGoal,
32
+ formatGoalLine,
22
33
  formatOptions,
23
34
  formatProjectLine,
24
35
  formatRun,
@@ -28,6 +39,7 @@ import {
28
39
  formatTaskLine,
29
40
  formatVersion,
30
41
  formatVersionLine,
42
+ formatWork,
31
43
  } from "./format.js";
32
44
  import { COMMAND_HELP, helpFor, usage } from "./help.js";
33
45
  import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
@@ -255,6 +267,79 @@ function priorityOf(value: string | undefined): TaskPriority | undefined {
255
267
  return found;
256
268
  }
257
269
 
270
+ function goalStateOf(value: string | undefined): GoalState | undefined {
271
+ if (value === undefined) {
272
+ return undefined;
273
+ }
274
+ const found = GOAL_STATES.find((state) => state === value);
275
+ if (found === undefined) {
276
+ throw misuse(`Not a state of a goal: ${value}. One of ${GOAL_STATES.join(", ")}`);
277
+ }
278
+ return found;
279
+ }
280
+
281
+ /** Today by this machine's clock, as a day. */
282
+ const todayOf = (io: CliIo): string => dayOfDate(new Date(io.now()));
283
+
284
+ /**
285
+ * A day as a flag takes one: `2026-10-24`, `today`, `tomorrow`, or `+<n>` days from today; and
286
+ * `none` where a field may be cleared, which is `null` on the wire.
287
+ */
288
+ function dayOf(io: CliIo, value: string | undefined, clearable = false): string | null | undefined {
289
+ if (value === undefined) {
290
+ return undefined;
291
+ }
292
+ if (clearable && value === "none") {
293
+ return null;
294
+ }
295
+ if (value === "today") {
296
+ return todayOf(io);
297
+ }
298
+ if (value === "tomorrow") {
299
+ return addDays(todayOf(io), 1);
300
+ }
301
+ const ahead = /^\+(\d{1,3})$/.exec(value);
302
+ if (ahead !== null) {
303
+ return addDays(todayOf(io), Number(ahead[1]));
304
+ }
305
+ if (!isDay(value)) {
306
+ throw misuse(`Not a day: ${value}. Say it as 2026-10-24, today, tomorrow, or +7`);
307
+ }
308
+ return value;
309
+ }
310
+
311
+ /** A value a field is set to, or `none` to clear it, which is `null` on the wire. */
312
+ const orNone = (value: string | undefined): string | null | undefined =>
313
+ value === "none" ? null : value;
314
+
315
+ /** The id of the person a flag names: `me`, or a login of the workspace. */
316
+ async function personIdOf(
317
+ session: Session,
318
+ value: string | undefined,
319
+ ): Promise<string | null | undefined> {
320
+ if (value === undefined) {
321
+ return undefined;
322
+ }
323
+ if (value === "none") {
324
+ return null;
325
+ }
326
+ if (value === "me") {
327
+ const me = await session.client.me();
328
+ if (me.person === null) {
329
+ throw failed("The console does not know this token. Sign in again: console login");
330
+ }
331
+ return me.person.id;
332
+ }
333
+ const { items } = await session.client.people();
334
+ const found = items.find((person) => person.login.toLowerCase() === value.toLowerCase());
335
+ if (found === undefined) {
336
+ throw misuse(
337
+ `Nobody of the workspace is called ${value}. console people is not a command; the logins are as people sign in.`,
338
+ );
339
+ }
340
+ return found.id;
341
+ }
342
+
258
343
  function secondsOf(value: string | undefined, fallback: number): number {
259
344
  if (value === undefined) {
260
345
  return fallback;
@@ -452,17 +537,67 @@ const use: Command = async (args, io) => {
452
537
  };
453
538
 
454
539
  const tasks: Command = async (args, io) => {
455
- const { values } = parse(args, { state: { type: "string" } }, false);
540
+ const { values } = parse(
541
+ args,
542
+ {
543
+ state: { type: "string" },
544
+ open: { type: "boolean" },
545
+ mine: { type: "boolean" },
546
+ category: { type: "string" },
547
+ goal: { type: "string" },
548
+ due: { type: "string" },
549
+ },
550
+ false,
551
+ );
456
552
  const state = stateOf(text(values, "state"));
457
553
  const session = await open(io, values);
458
554
  const project = await session.project();
459
- const found = await project.tasks(state === undefined ? {} : { state });
555
+ // What is due: a day, or the words for the usual windows; overdue keeps only the open ones.
556
+ const due = text(values, "due");
557
+ const today = todayOf(io);
558
+ const dueBy =
559
+ due === undefined
560
+ ? undefined
561
+ : due === "overdue"
562
+ ? addDays(today, -1)
563
+ : due === "week"
564
+ ? addDays(today, 6)
565
+ : (dayOf(io, due) ?? undefined);
566
+ const assignee = on(values, "mine")
567
+ ? ((await personIdOf(session, "me")) ?? undefined)
568
+ : undefined;
569
+ const found = await project.tasks({
570
+ ...(state === undefined ? {} : { state }),
571
+ ...(on(values, "open") || due === "overdue" ? { open: true } : {}),
572
+ ...(assignee === undefined ? {} : { assignee }),
573
+ ...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
574
+ ...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
575
+ ...(dueBy === undefined ? {} : { dueBy }),
576
+ });
460
577
  answer(io, session, found, () =>
461
578
  found.items.length === 0 ? "No tasks." : found.items.map(formatTaskLine).join("\n"),
462
579
  );
463
580
  return EXIT.ok;
464
581
  };
465
582
 
583
+ const work: Command = async (args, io) => {
584
+ const { values } = parse(args, {}, false);
585
+ const session = await open(io, values);
586
+ const found = await session.client.work();
587
+ answer(io, session, found, () => formatWork(found));
588
+ return EXIT.ok;
589
+ };
590
+
591
+ const categories: Command = async (args, io) => {
592
+ const { values } = parse(args, {}, false);
593
+ const session = await open(io, values);
594
+ const found = await session.client.categories();
595
+ answer(io, session, found, () =>
596
+ found.items.length === 0 ? "No categories." : found.items.map(formatCategoryLine).join("\n"),
597
+ );
598
+ return EXIT.ok;
599
+ };
600
+
466
601
  const skills: Command = async (args, io) => {
467
602
  const { values } = parse(args, {}, false);
468
603
  const session = await open(io, values);
@@ -472,33 +607,105 @@ const skills: Command = async (args, io) => {
472
607
  return EXIT.ok;
473
608
  };
474
609
 
610
+ /** The flags that set a task's fields, shared by writing one and changing one. */
611
+ const TASK_FIELDS: OptionSpec = {
612
+ state: { type: "string" },
613
+ priority: { type: "string" },
614
+ category: { type: "string" },
615
+ goal: { type: "string" },
616
+ starts: { type: "string" },
617
+ due: { type: "string" },
618
+ assignee: { type: "string" },
619
+ parent: { type: "string" },
620
+ };
621
+
622
+ /** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
623
+ async function taskFieldsOf(
624
+ io: CliIo,
625
+ session: Session,
626
+ project: ProjectClient,
627
+ values: Values,
628
+ ): Promise<RestTaskPatch> {
629
+ const state = stateOf(text(values, "state"));
630
+ const priority = priorityOf(text(values, "priority"));
631
+ const category = orNone(text(values, "category"));
632
+ const goalId = orNone(text(values, "goal"));
633
+ const startsOn = dayOf(io, text(values, "starts"), true);
634
+ const dueOn = dayOf(io, text(values, "due"), true);
635
+ const assigneeId = await personIdOf(session, text(values, "assignee"));
636
+ const parent = text(values, "parent");
637
+ const parentId =
638
+ parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
639
+ return {
640
+ ...(state === undefined ? {} : { state }),
641
+ ...(priority === undefined ? {} : { priority }),
642
+ ...(category === undefined ? {} : { category }),
643
+ ...(goalId === undefined ? {} : { goalId }),
644
+ ...(startsOn === undefined ? {} : { startsOn }),
645
+ ...(dueOn === undefined ? {} : { dueOn }),
646
+ ...(assigneeId === undefined ? {} : { assigneeId }),
647
+ ...(parentId === undefined ? {} : { parentId }),
648
+ };
649
+ }
650
+
475
651
  const newTask: Command = async (args, io) => {
476
652
  const { values, positionals } = parse(args, {
653
+ ...TASK_FIELDS,
477
654
  body: { type: "string" },
478
655
  file: { type: "string" },
479
- state: { type: "string" },
480
- priority: { type: "string" },
481
656
  });
482
657
  const title = theOne(positionals, 'Say the title: console task new "<title>"');
483
- const state = stateOf(text(values, "state"));
484
- const priority = priorityOf(text(values, "priority"));
485
658
  const body = await bodyOf(io, values, text(values, "body"));
486
659
  const session = await open(io, values);
487
660
  const project = await session.project();
661
+ const fields = await taskFieldsOf(io, session, project, values);
488
662
  const made = await project.createTask({
489
663
  title,
490
664
  ...(body === undefined ? {} : { body }),
491
- ...(state === undefined ? {} : { state }),
492
- ...(priority === undefined ? {} : { priority }),
665
+ ...fields,
493
666
  });
494
667
  answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
495
668
  return EXIT.ok;
496
669
  };
497
670
 
671
+ const editTask: Command = async (args, io) => {
672
+ const { values, positionals } = parse(args, {
673
+ ...TASK_FIELDS,
674
+ title: { type: "string" },
675
+ body: { type: "string" },
676
+ file: { type: "string" },
677
+ });
678
+ const ref = theOne(
679
+ positionals,
680
+ "Say which task: console task edit <number|id> --due <day> [--category <key>] ...",
681
+ );
682
+ const body = await bodyOf(io, values, text(values, "body"));
683
+ const session = await open(io, values);
684
+ const project = await session.project();
685
+ const title = text(values, "title");
686
+ const patch: RestTaskPatch = {
687
+ ...(title === undefined ? {} : { title }),
688
+ ...(body === undefined ? {} : { body }),
689
+ ...(await taskFieldsOf(io, session, project, values)),
690
+ };
691
+ if (Object.keys(patch).length === 0) {
692
+ throw misuse(
693
+ "Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.",
694
+ );
695
+ }
696
+ const found = await project.task(ref);
697
+ const changed = await project.updateTask(found.id, patch);
698
+ answer(io, session, changed, () => formatTask(changed));
699
+ return EXIT.ok;
700
+ };
701
+
498
702
  const task: Command = async (args, io) => {
499
703
  if (args[0] === "new") {
500
704
  return newTask(args.slice(1), io);
501
705
  }
706
+ if (args[0] === "edit") {
707
+ return editTask(args.slice(1), io);
708
+ }
502
709
  const { values, positionals } = parse(args, {});
503
710
  const ref = theOne(positionals, "Say which task: console task <number|id>");
504
711
  const session = await open(io, values);
@@ -864,6 +1071,88 @@ const restore: Command = async (args, io) => {
864
1071
  return EXIT.ok;
865
1072
  };
866
1073
 
1074
+ const goals: Command = async (args, io) => {
1075
+ const { values } = parse(args, {}, false);
1076
+ const session = await open(io, values);
1077
+ const project = await session.project();
1078
+ const found = await project.goals();
1079
+ answer(io, session, found, () =>
1080
+ found.items.length === 0
1081
+ ? "No goals. An owner sets one: console goal new <title> --due <day>"
1082
+ : found.items.map(formatGoalLine).join("\n"),
1083
+ );
1084
+ return EXIT.ok;
1085
+ };
1086
+
1087
+ const GOAL_FIELDS: OptionSpec = {
1088
+ state: { type: "string" },
1089
+ due: { type: "string" },
1090
+ body: { type: "string" },
1091
+ file: { type: "string" },
1092
+ };
1093
+
1094
+ const newGoal: Command = async (args, io) => {
1095
+ const { values, positionals } = parse(args, GOAL_FIELDS);
1096
+ const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
1097
+ const state = goalStateOf(text(values, "state"));
1098
+ const dueOn = dayOf(io, text(values, "due"));
1099
+ const body = await bodyOf(io, values, text(values, "body"));
1100
+ const session = await open(io, values);
1101
+ const project = await session.project();
1102
+ const made = await project.createGoal({
1103
+ title,
1104
+ ...(body === undefined ? {} : { body }),
1105
+ ...(state === undefined ? {} : { state }),
1106
+ ...(dueOn === undefined ? {} : { dueOn }),
1107
+ });
1108
+ answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
1109
+ return EXIT.ok;
1110
+ };
1111
+
1112
+ const editGoal: Command = async (args, io) => {
1113
+ const { values, positionals } = parse(args, { ...GOAL_FIELDS, title: { type: "string" } });
1114
+ const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
1115
+ const title = text(values, "title");
1116
+ const state = goalStateOf(text(values, "state"));
1117
+ const dueOn = dayOf(io, text(values, "due"), true);
1118
+ const body = await bodyOf(io, values, text(values, "body"));
1119
+ const patch = {
1120
+ ...(title === undefined ? {} : { title }),
1121
+ ...(body === undefined ? {} : { body }),
1122
+ ...(state === undefined ? {} : { state }),
1123
+ ...(dueOn === undefined ? {} : { dueOn }),
1124
+ };
1125
+ if (Object.keys(patch).length === 0) {
1126
+ throw misuse(
1127
+ "Say what to change: --title, --body, --state or --due; --due none clears the day.",
1128
+ );
1129
+ }
1130
+ const session = await open(io, values);
1131
+ const project = await session.project();
1132
+ const changed = await project.updateGoal(id, patch);
1133
+ answer(io, session, changed, () => formatGoal(changed));
1134
+ return EXIT.ok;
1135
+ };
1136
+
1137
+ const goal: Command = async (args, io) => {
1138
+ if (args[0] === "new") {
1139
+ return newGoal(args.slice(1), io);
1140
+ }
1141
+ if (args[0] === "edit") {
1142
+ return editGoal(args.slice(1), io);
1143
+ }
1144
+ const { values, positionals } = parse(args, {});
1145
+ const id = theOne(positionals, "Say which goal: console goal <id>");
1146
+ const session = await open(io, values);
1147
+ const project = await session.project();
1148
+ const found = await project.goal(id);
1149
+ const towards = await project.tasks({ goal: found.id });
1150
+ answer(io, session, { goal: found, tasks: towards.items }, () =>
1151
+ formatGoal(found, towards.items),
1152
+ );
1153
+ return EXIT.ok;
1154
+ };
1155
+
867
1156
  const decisions: Command = async (args, io) => {
868
1157
  const { values } = parse(args, { open: { type: "boolean" } }, false);
869
1158
  const session = await open(io, values);
@@ -1024,8 +1313,12 @@ const COMMANDS: Readonly<Record<string, Command>> = {
1024
1313
  serve,
1025
1314
  projects,
1026
1315
  use,
1316
+ work,
1027
1317
  tasks,
1028
1318
  task,
1319
+ goals,
1320
+ goal,
1321
+ categories,
1029
1322
  skills,
1030
1323
  take,
1031
1324
  report,
@@ -1072,6 +1365,14 @@ function hintFor(error: ApiError): string {
1072
1365
  return "The page has moved on since the version you started from. Read it again, and write from there.\n";
1073
1366
  case "document.archived":
1074
1367
  return "The page is archived. console restore <path> brings it back first.\n";
1368
+ case "task.invalid_category":
1369
+ return "No category has that key. console categories lists them.\n";
1370
+ case "task.invalid_goal":
1371
+ return "No goal of this project has that id. console goals lists them.\n";
1372
+ case "task.invalid_dates":
1373
+ return "The due day is before the start. Say a due day on or after it.\n";
1374
+ case "goal.not_found":
1375
+ return "No goal of this project has that id. console goals lists them.\n";
1075
1376
  case "connect.too_many":
1076
1377
  return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
1077
1378
  case "network":