@skillcdn/console 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +105 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +9 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +13 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +13 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +13 -7
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +36 -24
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -5
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +11 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +5 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +163 -11
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +24 -4
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +11 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +22 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +11 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +17 -9
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/schemas.d.ts +12 -11
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +11 -8
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -0
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +2 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +9 -9
- package/docs/specs/rest.md +14 -14
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/skills/working-the-board/references/terms.md +2 -2
- package/src/api.ts +5 -0
- package/src/cli/cli.ts +131 -18
- package/src/cli/format.ts +11 -7
- package/src/cli/help.ts +15 -11
- package/src/cli/main.ts +1 -0
- package/src/client.ts +23 -10
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +10 -4
- package/src/components/calendar.tsx +68 -32
- package/src/components/picker.tsx +297 -0
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +2 -2
- package/src/components/task-view.tsx +23 -9
- package/src/components/ui.tsx +25 -0
- package/src/components/work.tsx +6 -4
- package/src/console.tsx +29 -3
- package/src/data.ts +44 -1
- package/src/i18n/en.ts +16 -8
- package/src/i18n/ko.ts +17 -9
- package/src/index.ts +16 -1
- package/src/limits.ts +2 -0
- package/src/schemas.ts +11 -7
- package/src/styles/console.css +163 -11
- package/src/vocabulary.ts +3 -0
package/docs/specs/rest.md
CHANGED
|
@@ -13,7 +13,7 @@ The API is the board as its members see it: the people, the projects, and in eac
|
|
|
13
13
|
- A token ([`POST /api/v1/tokens`](#get-apiv1tokens-and-post-apiv1tokens)) is presented as `Authorization: Bearer <secret>` on any route, by an agent, a script or a console of a person's own. It is then the credential: a cookie beside it is not looked at, and no origin is needed, since nothing attaches a token but whoever holds it. A token that is nothing is refused, whatever cookie travels with it. What a token does is attributed to its person, and the event names the agent: the token's name, or what the agent called itself on its run.
|
|
14
14
|
- A token configures as its person does ([ADR-0021](../adr/0021-a-token-configures-as-its-person-does.md)): every request a session may make, a token may make, within the same roles, except the tokens themselves and the approval of a connection, which stay a person's own doing, signed in on the console's own pages; the one thing a token does about the tokens is give itself up (`DELETE /api/v1/me/token`).
|
|
15
15
|
- An agent connects without anyone copying a token ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)): [`POST /api/v1/connect`](#connecting-an-agent) is nobody's, the approval is a person's own doing on the console's own pages, and the claim is the command's, with the secret it was given.
|
|
16
|
-
- The command names its version on every request, as the product token of its `user-agent` (`skillcdn-console/0.1.18 (cli; node 24.11.0; linux)`), and every answer under `/api` says the console's version (`console-version`) and the range of command versions it takes (`console-client-range`, as npm spells one: `>=0.1.18 <0.2.0`) ([ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)). Before 1.0 the range is the console's
|
|
16
|
+
- The command names its version on every request, as the product token of its `user-agent` (`skillcdn-console/0.1.18 (cli; node 24.11.0; linux)`), and every answer under `/api` says the console's version (`console-version`) and the range of command versions it takes (`console-client-range`, as npm spells one: `>=0.1.18 <0.2.0`) ([ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)). Before 1.0 the range is the console's line, every patch of its major.minor, from the floor the console names where a release raised one: the oldest command whose requests and answers still fit it, raised only by a release that changed what passes between them ([ADR-0038](../adr/0038-the-command-and-the-console-go-together-within-a-line-and-a-release-that-changes-what-passes-between-them-raises-the-floors.md)); from 1.0, the console's major and a minor within one, either way. A command outside the range is refused with `426` before anything else is asked of its request, `client.outdated` or `client.too_new`, the message naming both versions and the range. The command holds the console to a floor of its own the same way, and refuses a console below it itself, as `console.outdated`, in this same form. A request that names no command, a browser's, a script's, is held to nothing.
|
|
17
17
|
- Request bodies are JSON, at most 256 KiB, and parsed once with the package's input schema. Text is trimmed where it is a line, bounded everywhere (`MAX_TITLE_LENGTH` and the rest of `@skillcdn/console/api`), and refused when it carries control or invisible characters. Links are `https` URLs only.
|
|
18
18
|
- Errors have `{ "error": { "code": "...", "message": "..." } }`. The code is stable; the message is for a person, in English.
|
|
19
19
|
|
|
@@ -28,7 +28,7 @@ The API is the board as its members see it: the people, the projects, and in eac
|
|
|
28
28
|
| 404 | `project.no_icon` | The project has no image for its mark. |
|
|
29
29
|
| 400 | `member.invalid_person` | The person to list is not one of the workspace, or is deactivated. |
|
|
30
30
|
| 400 | `listing.unknown_host` | The login to let in names a provider nobody signs in through here. |
|
|
31
|
-
| 400 | `task.
|
|
31
|
+
| 400 | `task.invalid_worker` | A person named as a worker is not one who may work in the project. |
|
|
32
32
|
| 400 | `task.invalid_parent` | The parent is not a task of the project, is the task itself, or would make a loop. |
|
|
33
33
|
| 400 | `task.invalid_category` | No category of the workspace has that key. |
|
|
34
34
|
| 400 | `task.invalid_goal` | The goal is not one of the project's. |
|
|
@@ -77,18 +77,18 @@ The API is the board as its members see it: the people, the projects, and in eac
|
|
|
77
77
|
| 409 | `listing.exists` | The login is let in already, or is one an active person signs in as. |
|
|
78
78
|
| 409 | `run.over`, `run.task_closed` | The run has ended already; the task is done or dropped. A run open on a task is no refusal: a new run takes it over ([ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)). |
|
|
79
79
|
| 413 | `request.too_large` | The body is over the limit; for a file handed in or attached, the file is over `MAX_FILE_BYTES`. |
|
|
80
|
-
| 426 | `client.outdated`, `client.too_new` | The command named a version outside the range the console takes ([ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)):
|
|
80
|
+
| 426 | `client.outdated`, `client.too_new` | The command named a version outside the range the console takes ([ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md), [ADR-0038](../adr/0038-the-command-and-the-console-go-together-within-a-line-and-a-release-that-changes-what-passes-between-them-raises-the-floors.md)): below the floor the console names, of an older line, or of a line it does not take yet. The message names both versions and the range; `console-version` and `console-client-range` say them too. `console.outdated` is the command's own refusal of a console older than it works with, in this form. |
|
|
81
81
|
| 503 | `file.store_unavailable` | The bytes of files are kept in a bucket, and it did not answer or refused the console; `retry-after` says when to try again. Nothing was written on the run or the page. |
|
|
82
82
|
|
|
83
83
|
## The workspace
|
|
84
84
|
|
|
85
85
|
### `GET /api/v1/me`
|
|
86
86
|
|
|
87
|
-
`{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label" }, ...], "console": { "version", "clientRange" } }`: what the board is called, the most days a token may be good for here (or `null` where a person chooses), where the organization's skills come from (`source`, the origin of the SkillCDN deployment they are read through, and `address`, the organization's, canonical, which a project without one of its own reads, or `null`; and `packToken`, when the SkillCDN token the organization's pack is read with was set, `{ setAt }`, or `null`), who sees whom on the People page (`peopleVisibility`: `everyone`, or `shared_projects`, where a member sees only the people they share a project with and an administrator everyone; [ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md); read as optional, for a console from before the setting), the day the calendars begin a week on (`weekStart`: `auto`, as the language of the pages has it, or `monday`, `sunday
|
|
87
|
+
`{ "workspace": { "name", "tokenDaysAtMost", "skills": { "source", "address", "packToken" }, "peopleVisibility" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label" }, ...], "console": { "version", "clientRange" } }`: what the board is called, the most days a token may be good for here (or `null` where a person chooses), where the organization's skills come from (`source`, the origin of the SkillCDN deployment they are read through, and `address`, the organization's, canonical, which a project without one of its own reads, or `null`; and `packToken`, when the SkillCDN token the organization's pack is read with was set, `{ setAt }`, or `null`), who sees whom on the People page (`peopleVisibility`: `everyone`, or `shared_projects`, where a member sees only the people they share a project with and an administrator everyone; [ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md); read as optional, for a console from before the setting), the day the calendars begin a week on and this week is counted by (`weekStart`: `auto`, as the language of the pages has it, or `monday`, `sunday`; [ADR-0039](../adr/0039-this-week-is-the-plans-unit-and-the-week-begins-as-the-workspace-says.md)) and the workspace's time zone (`timeZone`: an IANA name, in which the command prints moments, or `null` for none; both optional the same way), who the session cookie or the token says is asking (`id`, `login`, `name` or `null`, `avatar` or `null`, `role`), with what (`agent`: the name of the token when a token asks, as its person called it, so that a console of a person's own knows it holds one and offers nothing a token cannot do; `null` on a session), the language the person chose for the pages (`language`, a tag, or `null` for the browser's), and the identity providers people sign in through (`gh`, `google`, each with the label of its button), none where nobody can; and the console's own version with the range of command versions it takes (`console`, [ADR-0025](../adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)), for a console built on the package to compare itself with. Nobody is an answer, not an error; the pages ask this once to decide what to show, and the command once, at `login`, to see that the console knows the token.
|
|
88
88
|
|
|
89
89
|
### `GET /api/v1/people?login=`
|
|
90
90
|
|
|
91
|
-
`{ "items": [person, ...], "listed": [listing, ...] }`: the people the asker may see, those in first and by login, and the logins an administrator let in that nobody has signed in with yet ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-record-and-an-administrator-keeps-it.md)). Who sees whom is the workspace's setting, decided by the one policy of the server on every request ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)): with `everyone`, everyone who has signed in; with `shared_projects`, a member sees the active people who are in a project they are in, themselves, and the administrators, while an administrator sees everyone whatever the setting; a project open to the workspace has everyone in it, so a member of one sees everyone. The logins let in (`listed`) and the organization's reading (`organization`) go only to whom the policy shows everyone, `[]` and `null` otherwise. With `login`, one person by their exact login, compared without regard to case, or none: whom the asker sees, and, for an owner of a project, anyone active of the workspace, so that an owner lists someone the list does not show, learning that the login is one of the workspace and nothing more; `listed` is `[]` and `organization` `null` then; an empty or overlong login is `request.invalid`. A person carries their `role` in the workspace (`admin`, who configures it, or `member`, who works in the projects they are in), their `status` (`active`, or `deactivated`: kept with everything they did, refused on every request, and offered to no picker), their `source` (what let them in at their last sign-in: `organization`, the git host's organization the deployment names; `domain`, the Workspace domain it names; or `listed`, an administrator's listing) and `deactivatedAt` (or `null`). A listing carries its `id`, the `host` (`gh`, `google`), the `login` as the administrator spelled it, and `createdAt`. `organization` is the organization at the git host whose members are in, where the deployment names one (`host`, `name`, and `syncedAt`, when the worker last read it whole, or `null` until it has), or `null`. What a picker of
|
|
91
|
+
`{ "items": [person, ...], "listed": [listing, ...] }`: the people the asker may see, those in first and by login, and the logins an administrator let in that nobody has signed in with yet ([ADR-0022](../adr/0022-who-is-in-is-the-consoles-own-record-and-an-administrator-keeps-it.md)). Who sees whom is the workspace's setting, decided by the one policy of the server on every request ([ADR-0035](../adr/0035-who-may-see-whom-is-decided-by-one-policy-and-the-workspace-keeps-its-settings.md)): with `everyone`, everyone who has signed in; with `shared_projects`, a member sees the active people who are in a project they are in, themselves, and the administrators, while an administrator sees everyone whatever the setting; a project open to the workspace has everyone in it, so a member of one sees everyone. The logins let in (`listed`) and the organization's reading (`organization`) go only to whom the policy shows everyone, `[]` and `null` otherwise. With `login`, one person by their exact login, compared without regard to case, or none: whom the asker sees, and, for an owner of a project, anyone active of the workspace, so that an owner lists someone the list does not show, learning that the login is one of the workspace and nothing more; `listed` is `[]` and `organization` `null` then; an empty or overlong login is `request.invalid`. A person carries their `role` in the workspace (`admin`, who configures it, or `member`, who works in the projects they are in), their `status` (`active`, or `deactivated`: kept with everything they did, refused on every request, and offered to no picker), their `source` (what let them in at their last sign-in: `organization`, the git host's organization the deployment names; `domain`, the Workspace domain it names; or `listed`, an administrator's listing) and `deactivatedAt` (or `null`). A listing carries its `id`, the `host` (`gh`, `google`), the `login` as the administrator spelled it, and `createdAt`. `organization` is the organization at the git host whose members are in, where the deployment names one (`host`, `name`, and `syncedAt`, when the worker last read it whole, or `null` until it has), or `null`. What a picker of workers and of members is made of, the active people. A token reads it as its person does: what the command's `--worker`, `member` and `person` take a login from, through `login`.
|
|
92
92
|
|
|
93
93
|
### `PATCH /api/v1/people/<id>`
|
|
94
94
|
|
|
@@ -140,7 +140,7 @@ The rules of the board ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-th
|
|
|
140
140
|
|
|
141
141
|
### `GET /api/v1/work`
|
|
142
142
|
|
|
143
|
-
`{ "tasks": [{ "project": { "key", "name", "color", "symbol", "icon" }, "task" }, ...], "closed": [{ "project", "task" }, ...], "decisions": [{ "project", "decision" }, ...] }`: what is on the asker's plate across the projects they may see ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)): the open tasks
|
|
143
|
+
`{ "tasks": [{ "project": { "key", "name", "color", "symbol", "icon" }, "task" }, ...], "closed": [{ "project", "task" }, ...], "decisions": [{ "project", "decision" }, ...] }`: what is on the asker's plate across the projects they may see ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)): the open tasks they are at work on, the sooner due first and those with no day last; `closed`, the tasks they are at work on that are done or dropped and not yet put away, the last closed first, so that the week on the plate's page shows them on their days as a project's calendar does; and the decisions that wait for a person, the longest waiting first; each with the project it belongs to, so that a page says and links it. A task put away is in neither list ([ADR-0030](../adr/0030-a-task-is-put-away-from-any-state.md)). At most `LIST_LIMIT` of each.
|
|
144
144
|
|
|
145
145
|
### `GET /api/v1/tokens` and `POST /api/v1/tokens`
|
|
146
146
|
|
|
@@ -160,7 +160,7 @@ An administrator's sight of everyone's agents ([ADR-0011](../adr/0011-an-agent-c
|
|
|
160
160
|
|
|
161
161
|
### `GET /api/v1/people/<id>/work`
|
|
162
162
|
|
|
163
|
-
`{ "tasks": [{ "project", "task" }, ...], "closed": [{ "project", "task" }, ...] }`: what is on another person's plate, for whoever oversees them: an administrator, or an owner of a project the person is in (everyone of the workspace is in a project open to it; a private one holds those listed), signed in or through their agent. The person
|
|
163
|
+
`{ "tasks": [{ "project", "task" }, ...], "closed": [{ "project", "task" }, ...] }`: what is on another person's plate, for whoever oversees them: an administrator, or an owner of a project the person is in (everyone of the workspace is in a project open to it; a private one holds those listed), signed in or through their agent. The open tasks the person is at work on, the sooner due first and those with no day last, and as `closed` those of them done or dropped and not yet put away, both within the projects the asker may see and no other, each with its project; the decisions are left out, since they wait for anyone. `auth.forbidden` for anyone else; `person.not_found` for an id that is nobody's.
|
|
164
164
|
|
|
165
165
|
### Connecting an agent
|
|
166
166
|
|
|
@@ -192,17 +192,17 @@ The project's own image for its mark, in place of the symbol on the colour. `POS
|
|
|
192
192
|
|
|
193
193
|
For an owner, signed in or with a token. The patch takes `role` and writes `project.member_changed`; in a private project the person must be listed (`member.not_found`). In a project open to the workspace anyone active of it may be made an `owner`, which lists them, and an owner made a `member` again loses the listing and stays the member everyone is, answered with `listed: false`; an administrator owns every project whatever is asked, and nothing is written for them. The removal takes a listing away, answers `204` and writes `project.member_removed`; a person not listed is `member.not_found`, and in a project open to the workspace the person stays a member. A project may be left without an owner listed; the administrators own it then.
|
|
194
194
|
|
|
195
|
-
### `GET /api/v1/projects/<key>/tasks?state=&open=&archived=&category=&goal=&
|
|
195
|
+
### `GET /api/v1/projects/<key>/tasks?state=&open=&archived=&category=&goal=&worker=&unassigned=&dueBy=&q=&limit=&offset=`
|
|
196
196
|
|
|
197
|
-
`{ "items": [task, ...], "more": boolean }`: the project's board, newest first, a page at a time: `limit` (1 to `LIST_LIMIT`, the default) and `offset` (from 0) say which page, and `more` whether there is one after it; a `limit` or an `offset` that is not a number in range is `request.invalid`. `state` keeps one state of it; `open=true` the tasks not done or dropped, `open=false` the rest; `category=<key>` those sorted into a category; `goal=<id>` those that work towards a goal; `
|
|
197
|
+
`{ "items": [task, ...], "more": boolean }`: the project's board, newest first, a page at a time: `limit` (1 to `LIST_LIMIT`, the default) and `offset` (from 0) say which page, and `more` whether there is one after it; a `limit` or an `offset` that is not a number in range is `request.invalid`. `state` keeps one state of it; `open=true` the tasks not done or dropped, `open=false` the rest; `category=<key>` those sorted into a category; `goal=<id>` those that work towards a goal; `worker=<person id>` those one person is at work on, `unassigned=true` those nobody is at work on (with `worker`, nothing); `dueBy=<day>` (`2026-10-24`) those due on that day or before it; `q=<words>` (1 to `MAX_SEARCH_LENGTH` characters) those whose title holds every word, whatever the case and in any order, and the task of that number for a number (`12`, `#12`), which is how a picker or a command finds a task by its title ([ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)). The tasks put away ([ADR-0029](../adr/0029-a-task-done-or-dropped-is-put-away-out-of-the-board-and-open-by-its-number.md)) are left out; `archived=true` answers them alone, newest first: the archive and the board are two listings, never one. A task carries its `number` (the one people say out loud, numbered per project), `title`, `body` (Markdown, to be shown as text or rendered to elements, never as HTML), `state`, `priority`, `owner` (one person, whoever wrote it), `workers` (the people at work on it, in the order they came to it, `[]` while nobody is; [ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)), `parentId`, `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), `goalId` (or `null`), its plan ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md)): `startsOn` and `dueOn`, each a day (`2026-10-24`) or `null`; its moments, which the board keeps itself: `startedAt`, when it was first moved past `ready`, and `doneAt`, when it was moved to `done`, each an instant or `null`; `archivedAt`, when it was put away, an instant or `null` while it is current; `links` (`url` and `label`), `openDecisions` (how many decisions about it wait for a person), `openRuns` (how many runs on it are open: begun and not ended, which says nothing of whether an agent is still at it, [ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)), `createdAt` and `updatedAt`.
|
|
198
198
|
|
|
199
199
|
### `POST /api/v1/projects/<key>/tasks`
|
|
200
200
|
|
|
201
|
-
Takes `title`, and optionally `body`, `state` (default `idea`), `priority` (default `normal`), `
|
|
201
|
+
Takes `title`, and optionally `body`, `state` (default `idea`), `priority` (default `normal`), `workerIds` (the people to be at work on it, each one who may work in the project: everyone of the workspace for a project open to it, those listed for a private one, an administrator anywhere; each once however often named, up to `MAX_WORKERS`; `task.invalid_worker`), `parentId` (a task of the project), `category` (the key of a category of the workspace, or `null`; `task.invalid_category`), `goalId` (a goal of the project, or `null`; `task.invalid_goal`), `startsOn` and `dueOn` (days; the due day not before the start, `task.invalid_dates`) and `links`. The owner is whoever writes it. Answers `201` with the task, numbered after the project's last, and writes a `task.created` event. A task written past `ready` has its `startedAt`; one written `done` has its `doneAt` too.
|
|
202
202
|
|
|
203
203
|
### `GET /api/v1/projects/<key>/tasks/<id or number>` and `PATCH .../tasks/<id>`
|
|
204
204
|
|
|
205
|
-
One task, by its id or by its number in the project (`/tasks/7`): the pages hold ids, people and their agents say numbers. A patch takes the id and names only what changes, with the same fields as a creation; a field set to `null` clears it. A change of `state` is a `task.moved` event with `from` and `to`; any other change is one `task.updated` event naming the `fields` (`category`, `goalId`, `startsOn` and `dueOn` among them, as the input names them). A patch that changes nothing writes nothing and answers the task as it is. The moments follow the moves: the first move past `ready` sets `startedAt`, a move to `done` sets `doneAt`, and a move back out of `done` clears it.
|
|
205
|
+
One task, by its id or by its number in the project (`/tasks/7`): the pages hold ids, people and their agents say numbers. A patch takes the id and names only what changes, with the same fields as a creation; a field set to `null` clears it, and `workerIds` names the workers whole, in place of those there were, only the newcomers being checked, `[]` leaving nobody on it. A change of `state` is a `task.moved` event with `from` and `to`; any other change is one `task.updated` event naming the `fields` (`workers`, `category`, `goalId`, `startsOn` and `dueOn` among them, as the input names them). A patch that changes nothing writes nothing and answers the task as it is. The moments follow the moves: the first move past `ready` sets `startedAt`, a move to `done` sets `doneAt`, and a move back out of `done` clears it.
|
|
206
206
|
|
|
207
207
|
### `POST .../tasks/<id>/archive`, `POST .../tasks/<id>/restore` and `POST /api/v1/projects/<key>/tasks/archive`
|
|
208
208
|
|
|
@@ -232,9 +232,9 @@ The answer takes `option` (the id of one of its options) or `own` (the person's
|
|
|
232
232
|
|
|
233
233
|
Grows the record: takes any of `body` (the context) and `outcome` (what followed; empty clears it), for anyone who may work in the project, with a session from the console's own pages or a token, answered or not. Writes a `decision.updated` event naming the `fields`; a patch that changes nothing writes nothing.
|
|
234
234
|
|
|
235
|
-
### `GET /api/v1/projects/<key>/goals
|
|
235
|
+
### `GET /api/v1/projects/<key>/goals?state=&open=&category=&q=&limit=&offset=` and `POST /api/v1/projects/<key>/goals`
|
|
236
236
|
|
|
237
|
-
The roadmap ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md), [ADR-0026](../adr/0026-a-goal-is-planned-in-days-and-sorted-into-a-category-and-the-roadmap-is-read-as-months.md)). `GET`, for whoever may see the project, answers `{ "items": [goal, ...] }`: the active goals first, then the planned, the reached and the dropped, the sooner due first within each and the older first among those with no day;
|
|
237
|
+
The roadmap ([ADR-0019](../adr/0019-a-task-is-planned-in-days-the-board-keeps-its-moments-and-goals-and-categories-give-the-work-its-shape.md), [ADR-0026](../adr/0026-a-goal-is-planned-in-days-and-sorted-into-a-category-and-the-roadmap-is-read-as-months.md)). `GET`, for whoever may see the project, answers `{ "items": [goal, ...], "more": boolean }`: a page of the roadmap, the active goals first, then the planned, the reached and the dropped, the sooner due first within each and the older first among those with no day; `limit` (1 to `LIST_LIMIT`, the default) and `offset` say which page and `more` whether there is one after it; `?category=<key>` keeps those sorted into a category, `?state=<state>` those in one state, `?open=true` the planned and the active and `?open=false` the reached and the dropped, and `?q=<words>` those whose title holds every word, whatever the case, which is how a picker or a command finds a goal ([ADR-0037](../adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)). A goal carries its `title`, `body` (Markdown: what reaching it means), `state` (`planned`, `active`, `done`, `dropped`), `category` (its `key`, `name` and `color` as the workspace keeps it, or `null`), its plan, `startsOn` (the day the work towards it is meant to begin, or `null`) and `dueOn` (the day it is meant to be reached by, or `null`), `createdBy`, `taskCounts` (the tasks that work towards it, counted by state: `idea`, `ready`, `in_progress`, `in_review`, `done`, `dropped`; read from the board as it stands, never written down), `createdAt` and `updatedAt`. `POST`, for an owner of the project, from the console's own pages or with a token, takes `title` and optionally `body`, `state` (default `planned`), `category` (the key of a category of the workspace, or `null`; `goal.invalid_category`), `startsOn` and `dueOn` (days; the target not before the start, `goal.invalid_dates`), answers `201` with the goal and writes a `goal.created` event with its `title`; a member is `auth.forbidden`.
|
|
238
238
|
|
|
239
239
|
### `GET .../goals/<id>` and `PATCH .../goals/<id>`
|
|
240
240
|
|
|
@@ -276,7 +276,7 @@ The pack token ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through
|
|
|
276
276
|
|
|
277
277
|
### `POST /api/v1/projects/<key>/pack-token/connect`, `GET .../pack-token/connect` and `DELETE .../pack-token/connect`
|
|
278
278
|
|
|
279
|
-
A private pack connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an owner. `POST` asks the deployment to connect the project's address (the project's own, or the organization's), as nobody, the console calling itself
|
|
279
|
+
A private pack connected with a code a person approves ([ADR-0036](../adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)), for an owner. `POST` asks the deployment to connect the project's address (the project's own, or the organization's), as nobody, the console calling itself by the workspace's name with Console after it, once (`Acme Console`, whether the name ends with it or not), and answers `201` with what the person is shown: `{ "code", "url", "expiresAt", "intervalSeconds" }`, the code, the page at the deployment to approve it on, until when, and how often to ask again; the secret the console claims with is kept sealed on a row of the project's and answered never. One request per project at a time: a new one replaces the one under way. `project.pack_connect_unsupported` (`409`) where the console reaches no deployment or keeps no secret to seal with; `project.pack_connect_refused` (`400`) when the project names no pack, the pack is on the console's machine, or the deployment refused to begin, in its words (an address with a ref or a path, a public pack); `skills.unavailable` (`503`) when the deployment could not be reached. `GET` asks how it stands and answers one of `{ "status": "none" }`, nothing asked; `{ "status": "pending", "code", "url", "expiresAt", "intervalSeconds" }`, while the person has not approved; `{ "status": "connected", "project" }`, once, when the token was claimed, sealed and kept as `PUT .../pack-token` keeps one, writing `project.updated` with `packToken`, the project answered as it stands; or `{ "status": "ended", "reason": "expired" | "declined" | "refused" }`, when the code's time ran out, the person declined or the code was spent, or the deployment refused (a repository the person may not open, as many tokens as one may hold), the request gone each time. A deployment that could not be reached is `skills.unavailable`, and the request stands. `DELETE` gives the request up and answers `204`.
|
|
280
280
|
|
|
281
281
|
### `GET /api/v1/projects/<key>/rules`
|
|
282
282
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillcdn/console",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "What a custom SkillCDN console is built from: the API client and schemas, the components, the composition of the default console, and the console command agents work the board with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/skillcdn/console#readme",
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: A shell with Node.js 24 or newer and npm or pnpm, and an agent that reads and writes files. For looking at the pages against a board, the console command signed in by a person. A browser the agent can drive is optional. No key of the agent's own is needed, and it never sees a token.
|
|
6
6
|
metadata:
|
|
7
7
|
author: skillcdn
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.5"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
@@ -53,7 +53,7 @@ Nothing here restates them: they are the contract, current at the version instal
|
|
|
53
53
|
| The marks | From the organization's website when the request, or deploying-a-console, names one: its symbol and wordmark read from the site as [the-look-from-a-website.md](references/the-look-from-a-website.md) says, the light version or, with the person's word, the one-colour mark recoloured, shown at the plan. Else asked for a console that shows marks of its own, for the half the request leaves open: "Which picture should the pages show beside the board's name: a small square mark, the name as lettering, or both? Where is the file? The pages are dark, so it should be a light one." `brand` takes either or both. The symbol's slot is a square of one line's height (`.sc-brand-symbol`, 1rem): a wide mark, a lettermark of three or four times its height, is a wordmark and goes in as one, alone or beside a square symbol. A mark drawn in a dark colour vanishes on the dark pages: ask for a light version, or, with the person's word, recolour it when it is one colour. Default without a site: the board's name alone and no marks, which is the honest console. |
|
|
54
54
|
| The colour | From the website's own colours when one is named, as the reference says, its primary colour read from the theme colour, the stylesheet's properties or the primary button. Else asked when the request names none and marks are wanted: "Which colour, or which mood?" A colour comes as words or a value; the value is the mark's own fill when it has one, else chosen and shown. A light colour becomes the accent (`--sc-color-accent` and its family); a dark one tints the field and its light (`--sc-color-field`, `--sc-color-light-*`), with a lighter shade of it as the accent, since an accent must read on the dark field. The mapping is shown at the plan, with the contrast checked. |
|
|
55
55
|
| The languages | Asked when the organization's language is not among the packs: "Which languages should the pages speak?" Default: the packs the package ships, chosen by the browser; a Korean organization gets Pretendard loaded, as the default console does. |
|
|
56
|
-
| The name | Not asked: the server names the board (`WORKSPACE_NAME`), and `title` is only what the page says until the server answers. `index.html` gets the organization's `<title>` and `lang`. |
|
|
56
|
+
| The name | Not asked: the server names the board (`WORKSPACE_NAME`, conventionally the organization or the person it is for, then Console: `Acme Console`), and `title` is only what the page says until the server answers. `index.html` gets the organization's `<title>` and `lang`. |
|
|
57
57
|
| The console's address | For running one's own, what `console whoami` says. A script takes `CONSOLE_URL` and `CONSOLE_TOKEN` from the environment, never from a file in the repository. |
|
|
58
58
|
|
|
59
59
|
## Workflow
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Needs a shell with Node.js 24 or newer, git, curl, jq and the AWS CLI v2, the gh command signed in or an empty repository the person made, network access to AWS, Cloudflare, GitHub and npm, an AWS account and a domain on Cloudflare the person can create a key and a token for, and a browser on any device for the few steps only a signed-in person can take. Works in any agent that can run a shell and write files; on Windows, in Git Bash with path conversion off.
|
|
6
6
|
metadata:
|
|
7
7
|
author: skillcdn
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.4"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
@@ -43,7 +43,7 @@ Check these before the first message. Without the AWS CLI, offer to install it (
|
|
|
43
43
|
| Input | Source |
|
|
44
44
|
|---|---|
|
|
45
45
|
| How many people | Asked: "How many people will use the console within a year: just you, up to 10, up to 30, or up to 100?" The answer picks the size ([sizing.md](references/sizing.md)). |
|
|
46
|
-
| The board's name |
|
|
46
|
+
| The board's name | Suggested from the convention and confirmed, never invented: the organization or the person the console is for, then Console, as `SkillCDN Console` is named. With the organization's name in the request or on its website: "The board will be called `Acme Console`, as consoles are named: the organization's name, then Console. Keep it, or say another?" Without one: "Whose console is this, your organization's or yours? Its name, then Console, is what the board is called." It goes to `WORKSPACE_NAME`, one line of up to 100 characters: what the pages say after the SkillCDN mark, and what the console calls itself at SkillCDN when it connects a pack. |
|
|
47
47
|
| The web address | Asked: "Which web address should the console have? A domain you already own, and where it is registered (I set it up on Cloudflare; you change its nameservers there once, and I give you the steps), or should I find one to buy (I show the price before buying)? I suggest `console.<domain>`." A domain is needed: the tunnel has no free hostname. A domain that already serves the organization's site is fine: only the console's hostname is added to its zone, and nothing of the site is touched (resources.md "A zone that serves other hosts"). |
|
|
48
48
|
| How people sign in, and who | Asked: "How should people sign in: with GitHub, with Google (Workspace), or both? Who configures the console (you, at least): their GitHub login or Google address. With GitHub, everyone of your organization can be in, as the organization has them: say which organization. With a Google Workspace, everyone of the domain is in. Anyone else, an administrator lets in on the People page once the console runs." |
|
|
49
49
|
| The organization's skills | Asked: "Where are your organization's skills on SkillCDN, if anywhere yet? An address like `/gh/<owner>/<repo>`. Is that repository private?" Default: none; a project names one later on its Settings page. A private one is connected in phase 6, with a code the person approves at SkillCDN ([ADR-0036](/docs/adr/0036-a-private-pack-is-connected-with-a-code-a-person-approves-from-the-projects-settings.md)); nothing of it is needed before. |
|
|
@@ -251,7 +251,7 @@ The task definition of the `api` role with the tunnel's connector beside it, in
|
|
|
251
251
|
"portMappings": [{ "containerPort": 11199, "protocol": "tcp" }],
|
|
252
252
|
"environment": [
|
|
253
253
|
{ "name": "PUBLIC_URL", "value": "https://<hostname>" },
|
|
254
|
-
{ "name": "WORKSPACE_NAME", "value": "<the board's name>" },
|
|
254
|
+
{ "name": "WORKSPACE_NAME", "value": "<the board's name, such as Acme Console>" },
|
|
255
255
|
{ "name": "WORKER_IN_PROCESS", "value": "true" },
|
|
256
256
|
{ "name": "TRUSTED_PROXIES", "value": "127.0.0.1,::1" },
|
|
257
257
|
{ "name": "CLIENT_IP_HEADER", "value": "cf-connecting-ip" },
|
|
@@ -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.
|
|
8
|
+
version: "1.21"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
@@ -31,7 +31,7 @@ A task of the board goes in, named by its number; a run comes out: the task take
|
|
|
31
31
|
| Need | How | When missing |
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| The `console` command, signed in | `console whoami` says as whom, at which console, and in which project this directory works; and whether the command and the console go together. `console logins` lists the consoles this machine is signed in to; a directory bound to another console (`.skillcdn-console.json`) needs a login there, and a token never goes to a console it was not made at ([ADR-0027](/docs/adr/0027-the-command-keeps-a-login-per-console-and-a-token-goes-only-to-the-console-that-issued-it.md)). | Stop and tell the person what to do: `npm install -g @skillcdn/console`; `console login --url <the console's address>`, which shows an address and a code the person approves on the console's own pages; `console use <key>` once in the directory, to name the project. Never look for a token. |
|
|
34
|
-
| The command at a version the console takes | A command the console refuses as outdated, or as too new, exits with `4` and prints the command that updates it where it is installed ([ADR-0025](/docs/adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)). | Run the update command it printed, then go on; where the machine refuses the update, stop and tell the person. A line on stderr that a newer command exists, while the console still answers, is read and left for later. |
|
|
34
|
+
| The command at a version the console takes | A command the console refuses as outdated, or as too new, exits with `4` and prints the command that updates it where it is installed; so does a command that refuses the console as older than it works with, printing the command that installs the console's version ([ADR-0025](/docs/adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md), [ADR-0038](/docs/adr/0038-the-command-and-the-console-go-together-within-a-line-and-a-release-that-changes-what-passes-between-them-raises-the-floors.md)). Within a line of SkillCDN the two go together whatever the patch, until a release says otherwise. | Run the update command it printed, then go on; where the machine refuses the update, stop and tell the person. A line on stderr that a newer command exists, while the console still answers, is read and left for later. |
|
|
35
35
|
| The command's own words | `console help` lists every command in a line, read once; `console help <command>` says one in full, every form and flag, the limits the console holds it to, and the refusals it may meet, by code, read when the command is about to be used. | They are the reference at run time. This skill says how the work goes, not what each flag means. |
|
|
36
36
|
|
|
37
37
|
The full contract is the [specification of the command line](/docs/specs/cli.md); the command is a thin client of the [REST API](/docs/specs/rest.md), which a script may use directly.
|
|
@@ -40,7 +40,7 @@ The full contract is the [specification of the command line](/docs/specs/cli.md)
|
|
|
40
40
|
|
|
41
41
|
| Input | Source |
|
|
42
42
|
|---|---|
|
|
43
|
-
| 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 (`--person <login>` another person's, for an administrator or an owner of a project they are in), 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. |
|
|
43
|
+
| 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 (`--person <login>` another person's, for an administrator or an owner of a project they are in), 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; `--search <words>` finds a task by the words of its title, or by its number); a task is never taken without being told which, or that any ready one may be. |
|
|
44
44
|
| The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask; the comments on it, which are the discussion so far, printed with the task, their pictures saved with `console file <id>`; with its plan, the category, the goal it works towards (`console goal <id>` says what reaching it means, its category and the days it starts and is meant to be reached by) 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 pack (`console skills` says where the pack is, what a client is told of it on connect, and every skill and document; `console skills <folder>` shows what a folder of it holds, `console skills --search <words>` what the words find in it; `console skill <path>` reads one whole through the console, its rules first, as an MCP client's model is given it, a private pack through the pack token the project holds, [ADR-0028](/docs/adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md), and a pack on the console's own machine from its directory, [ADR-0031](/docs/adr/0031-the-console-reads-a-pack-as-the-skillcdn-command-does-and-a-local-one-from-a-directory.md); `console skill --file <file>` a file of it; a host that lists skills through a connection of its own loads them by their URI, which means nothing elsewhere for a pack on the console's machine) say the rest. |
|
|
45
45
|
| The rules | The workspace's, the console's own until an administrator writes the organization's, and this project's own when it has them: which skills to load first, where the work lives, the pages, what may be decided alone, how to report ([ADR-0023](/docs/adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)). `console take` prints them with the task, so they are read once, there; `console rules` reads them without taking, as when a task is still to be chosen. They hold over this skill's defaults where they differ. |
|
|
46
46
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Terms
|
|
2
2
|
|
|
3
|
-
- **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).
|
|
3
|
+
- **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); its owner is the one person who wrote it, and its workers are the people at work on it, any number, among whom taking a task puts the agent's person ([ADR-0037](/docs/adr/0037-a-task-has-one-owner-and-any-number-of-workers-and-a-parent-or-a-goal-is-found-by-search.md)).
|
|
4
4
|
- **Goal**: what the project works towards, on its roadmap, with the day it is meant to be reached by; its progress is its tasks.
|
|
5
5
|
- **Category**: a kind of work, the workspace's word for it, by key.
|
|
6
6
|
- **Run**: one agent at work on one task for one person, with its reports, what it handed in, and what it waits for.
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
- **Page**: a document of the project, Markdown at a path that does not change.
|
|
9
9
|
- **Project**: a board of its own, with its members and its skills address.
|
|
10
10
|
- **SkillCDN token**: what the console reads a private pack with, `pack-token` in the API and the command: a project's own, or the workspace's for the organization's pack, which a project reads when it names no address of its own; connected with a code a person approves at SkillCDN, or made there by hand, and shown to nobody.
|
|
11
|
-
- **Number and id**: a task has a number (`#7`), what people say and the commands take, and an id, what the API takes and `--parent` and `--goal` may; a run, a decision, a goal and a comment have ids alone. Every command that makes one prints it on a line of its own, named (`task id:`, `run id:`, `decision id:`, `goal id:`, `comment <id>:`), and `--json` prints the whole answer: an id or a number is read from there, never assumed, since several agents write on one board.
|
|
11
|
+
- **Number and id**: a task has a number (`#7`), what people say and the commands take, and an id, what the API takes and `--parent` and `--goal` may; a run, a decision, a goal and a comment have ids alone. A task whose number is not known is found by the words of its title, `console tasks --search <words>`, and a goal's id by `console goals --search <words>`. Every command that makes one prints it on a line of its own, named (`task id:`, `run id:`, `decision id:`, `goal id:`, `comment <id>:`), and `--json` prints the whole answer: an id or a number is read from there, never assumed, since several agents write on one board.
|
package/src/api.ts
CHANGED
|
@@ -27,14 +27,17 @@ export {
|
|
|
27
27
|
type ClientJudgement,
|
|
28
28
|
type ClientPolicy,
|
|
29
29
|
type ClientVerdict,
|
|
30
|
+
CONSOLE_AT_LEAST,
|
|
30
31
|
clientRange,
|
|
31
32
|
clientVersionOf,
|
|
32
33
|
commonRange,
|
|
33
34
|
compareVersions,
|
|
35
|
+
consoleRange,
|
|
34
36
|
formatRange,
|
|
35
37
|
formatVersion,
|
|
36
38
|
inRange,
|
|
37
39
|
judgeClient,
|
|
40
|
+
judgeConsole,
|
|
38
41
|
parseRange,
|
|
39
42
|
parseVersion,
|
|
40
43
|
userAgentOf,
|
|
@@ -101,6 +104,7 @@ export {
|
|
|
101
104
|
MAX_TOKENS_PER_PERSON,
|
|
102
105
|
MAX_URL_LENGTH,
|
|
103
106
|
MAX_VERSIONS_PER_DOCUMENT,
|
|
107
|
+
MAX_WORKERS,
|
|
104
108
|
MIN_OPTIONS,
|
|
105
109
|
} from "./limits.js";
|
|
106
110
|
export { projectColorOf, projectSymbolOf } from "./marks.js";
|
|
@@ -347,6 +351,7 @@ export {
|
|
|
347
351
|
type LinkSource,
|
|
348
352
|
MEMBERSHIP_SOURCES,
|
|
349
353
|
type MembershipSource,
|
|
354
|
+
OPEN_GOAL_STATES,
|
|
350
355
|
OPEN_TASK_STATES,
|
|
351
356
|
PACK_CONNECT_ENDINGS,
|
|
352
357
|
type PackConnectEnding,
|
package/src/cli/cli.ts
CHANGED
|
@@ -7,8 +7,18 @@ import {
|
|
|
7
7
|
type FetchLike,
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
CLIENT_RANGE_HEADER,
|
|
12
|
+
CONSOLE_AT_LEAST,
|
|
13
|
+
consoleRange,
|
|
14
|
+
formatRange,
|
|
15
|
+
judgeClient,
|
|
16
|
+
judgeConsole,
|
|
17
|
+
parseVersion,
|
|
18
|
+
VERSION_HEADER,
|
|
19
|
+
formatVersion as versionText,
|
|
20
|
+
} from "../compat.js";
|
|
21
|
+
import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
|
|
12
22
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
13
23
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
14
24
|
import {
|
|
@@ -168,6 +178,10 @@ export interface CliIo {
|
|
|
168
178
|
readonly install?: Install | undefined;
|
|
169
179
|
/** What the command last said of the versions; left out, it says it every time. */
|
|
170
180
|
readonly notices?: NoticeStore | undefined;
|
|
181
|
+
/** The oldest console this command works with (ADR-0038); left out, the package's own floor. */
|
|
182
|
+
readonly consoleAtLeast?: string | undefined;
|
|
183
|
+
/** The language and region of this machine (`ko-KR`), for what follows a language where the person chose none; left out, English. */
|
|
184
|
+
readonly locale?: string | undefined;
|
|
171
185
|
/** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
|
|
172
186
|
readonly skillsDir?: string | undefined;
|
|
173
187
|
/** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
|
|
@@ -889,11 +903,15 @@ const whoami: Command = async (args, io) => {
|
|
|
889
903
|
command === undefined || server === undefined
|
|
890
904
|
? undefined
|
|
891
905
|
: judgeClient(command, server).verdict;
|
|
906
|
+
// The oldest console this command works with (ADR-0038), as it stands within the command's line.
|
|
907
|
+
const atLeast = io.consoleAtLeast ?? CONSOLE_AT_LEAST;
|
|
908
|
+
const floor =
|
|
909
|
+
command === undefined ? atLeast : versionText(consoleRange(command, atLeast).atLeast);
|
|
892
910
|
// A console from before the rule says nothing of itself; the command says so instead.
|
|
893
911
|
const consoleWords =
|
|
894
912
|
me.console === undefined
|
|
895
913
|
? "the console does not say its version, so it is older than 0.1.18"
|
|
896
|
-
: `the console is ${me.console.version} and takes ${me.console.clientRange}`;
|
|
914
|
+
: `the console is ${me.console.version} and takes ${me.console.clientRange}; this command works with a console from ${floor} on`;
|
|
897
915
|
answer(
|
|
898
916
|
io,
|
|
899
917
|
session,
|
|
@@ -975,6 +993,16 @@ const use: Command = async (args, io) => {
|
|
|
975
993
|
return EXIT.ok;
|
|
976
994
|
};
|
|
977
995
|
|
|
996
|
+
/**
|
|
997
|
+
* The last day of this week (ADR-0039): the week begins as the workspace says, or, left to the
|
|
998
|
+
* language, as the person's language has it, else this machine's.
|
|
999
|
+
*/
|
|
1000
|
+
async function endOfWeek(io: CliIo, session: Session, today: string): Promise<string> {
|
|
1001
|
+
const me = await session.client.me();
|
|
1002
|
+
const language = me.language ?? io.locale ?? "en";
|
|
1003
|
+
return weekOf(today, firstDayOfWeek(language, me.workspace.weekStart ?? "auto")).at(-1) ?? today;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
978
1006
|
const tasks: Command = async (args, io) => {
|
|
979
1007
|
const { values } = parse(
|
|
980
1008
|
args,
|
|
@@ -986,6 +1014,7 @@ const tasks: Command = async (args, io) => {
|
|
|
986
1014
|
category: { type: "string" },
|
|
987
1015
|
goal: { type: "string" },
|
|
988
1016
|
due: { type: "string" },
|
|
1017
|
+
search: { type: "string" },
|
|
989
1018
|
archived: { type: "boolean" },
|
|
990
1019
|
...PAGE_OPTIONS,
|
|
991
1020
|
},
|
|
@@ -994,7 +1023,7 @@ const tasks: Command = async (args, io) => {
|
|
|
994
1023
|
const state = stateOf(text(values, "state"));
|
|
995
1024
|
if (on(values, "mine") && on(values, "unassigned")) {
|
|
996
1025
|
throw misuse(
|
|
997
|
-
"Say one: --mine for the tasks
|
|
1026
|
+
"Say one: --mine for the tasks you work on, or --unassigned for those nobody works on",
|
|
998
1027
|
);
|
|
999
1028
|
}
|
|
1000
1029
|
const page = pageOf(values);
|
|
@@ -1009,19 +1038,22 @@ const tasks: Command = async (args, io) => {
|
|
|
1009
1038
|
: due === "overdue"
|
|
1010
1039
|
? addDays(today, -1)
|
|
1011
1040
|
: due === "week"
|
|
1012
|
-
?
|
|
1041
|
+
? await endOfWeek(io, session, today)
|
|
1013
1042
|
: (dayOf(io, due) ?? undefined);
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1043
|
+
const worker = on(values, "mine") ? ((await personIdOf(session, "me")) ?? undefined) : undefined;
|
|
1044
|
+
const search = text(values, "search")?.trim();
|
|
1045
|
+
if (search === "") {
|
|
1046
|
+
throw misuse("Say what to search for: console tasks --search <words>");
|
|
1047
|
+
}
|
|
1017
1048
|
const found = await project.tasks({
|
|
1018
1049
|
...(state === undefined ? {} : { state }),
|
|
1019
1050
|
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
1020
|
-
...(
|
|
1051
|
+
...(worker === undefined ? {} : { worker }),
|
|
1021
1052
|
...(on(values, "unassigned") ? { unassigned: true } : {}),
|
|
1022
1053
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
1023
1054
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
1024
1055
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
1056
|
+
...(search === undefined ? {} : { q: search }),
|
|
1025
1057
|
...(on(values, "archived") ? { archived: true } : {}),
|
|
1026
1058
|
...page,
|
|
1027
1059
|
});
|
|
@@ -1335,10 +1367,42 @@ const TASK_FIELDS: OptionSpec = {
|
|
|
1335
1367
|
goal: { type: "string" },
|
|
1336
1368
|
starts: { type: "string" },
|
|
1337
1369
|
due: { type: "string" },
|
|
1338
|
-
|
|
1370
|
+
worker: { type: "string", multiple: true },
|
|
1339
1371
|
parent: { type: "string" },
|
|
1340
1372
|
};
|
|
1341
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* The people `--worker` names, each a login or `me`, given again or with commas between for
|
|
1376
|
+
* several, in place of those there were; `none` alone for nobody; nothing when the flag was
|
|
1377
|
+
* not given.
|
|
1378
|
+
*/
|
|
1379
|
+
async function workerIdsOf(
|
|
1380
|
+
session: Session,
|
|
1381
|
+
given: readonly string[],
|
|
1382
|
+
): Promise<string[] | undefined> {
|
|
1383
|
+
if (given.length === 0) {
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1386
|
+
const names = given
|
|
1387
|
+
.flatMap((value) => value.split(","))
|
|
1388
|
+
.map((name) => name.trim())
|
|
1389
|
+
.filter((name) => name.length > 0);
|
|
1390
|
+
if (names.includes("none")) {
|
|
1391
|
+
if (names.length > 1) {
|
|
1392
|
+
throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
|
|
1393
|
+
}
|
|
1394
|
+
return [];
|
|
1395
|
+
}
|
|
1396
|
+
const ids: string[] = [];
|
|
1397
|
+
for (const name of names) {
|
|
1398
|
+
const id = await personIdOf(session, name);
|
|
1399
|
+
if (typeof id === "string" && !ids.includes(id)) {
|
|
1400
|
+
ids.push(id);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return ids;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1342
1406
|
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
1343
1407
|
async function taskFieldsOf(
|
|
1344
1408
|
io: CliIo,
|
|
@@ -1352,7 +1416,7 @@ async function taskFieldsOf(
|
|
|
1352
1416
|
const goalId = orNone(text(values, "goal"));
|
|
1353
1417
|
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
1354
1418
|
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1355
|
-
const
|
|
1419
|
+
const workerIds = await workerIdsOf(session, texts(values, "worker"));
|
|
1356
1420
|
const parent = text(values, "parent");
|
|
1357
1421
|
const parentId =
|
|
1358
1422
|
parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
@@ -1363,7 +1427,7 @@ async function taskFieldsOf(
|
|
|
1363
1427
|
...(goalId === undefined ? {} : { goalId }),
|
|
1364
1428
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
1365
1429
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1366
|
-
...(
|
|
1430
|
+
...(workerIds === undefined ? {} : { workerIds }),
|
|
1367
1431
|
...(parentId === undefined ? {} : { parentId }),
|
|
1368
1432
|
};
|
|
1369
1433
|
}
|
|
@@ -1415,7 +1479,7 @@ const editTask: Command = async (args, io) => {
|
|
|
1415
1479
|
};
|
|
1416
1480
|
if (Object.keys(patch).length === 0) {
|
|
1417
1481
|
throw misuse(
|
|
1418
|
-
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --
|
|
1482
|
+
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.",
|
|
1419
1483
|
);
|
|
1420
1484
|
}
|
|
1421
1485
|
const found = await project.task(ref);
|
|
@@ -1956,23 +2020,36 @@ const restore: Command = async (args, io) => {
|
|
|
1956
2020
|
const goals: Command = async (args, io) => {
|
|
1957
2021
|
const { values } = parse(
|
|
1958
2022
|
args,
|
|
1959
|
-
{
|
|
2023
|
+
{
|
|
2024
|
+
category: { type: "string" },
|
|
2025
|
+
state: { type: "string" },
|
|
2026
|
+
search: { type: "string" },
|
|
2027
|
+
...PAGE_OPTIONS,
|
|
2028
|
+
},
|
|
1960
2029
|
false,
|
|
1961
2030
|
);
|
|
1962
2031
|
const state = goalStateOf(text(values, "state"));
|
|
1963
2032
|
const category = text(values, "category");
|
|
2033
|
+
const search = text(values, "search")?.trim();
|
|
2034
|
+
if (search === "") {
|
|
2035
|
+
throw misuse("Say what to search for: console goals --search <words>");
|
|
2036
|
+
}
|
|
2037
|
+
const page = pageOf(values);
|
|
1964
2038
|
const session = await open(io, values);
|
|
1965
2039
|
const project = await session.project();
|
|
1966
2040
|
const found = await project.goals({
|
|
1967
2041
|
...(state === undefined ? {} : { state }),
|
|
1968
2042
|
...(category === undefined ? {} : { category }),
|
|
2043
|
+
...(search === undefined ? {} : { q: search }),
|
|
2044
|
+
...page,
|
|
1969
2045
|
});
|
|
1970
2046
|
answer(io, session, found, () =>
|
|
1971
2047
|
found.items.length === 0
|
|
1972
|
-
? state === undefined && category === undefined
|
|
2048
|
+
? state === undefined && category === undefined && search === undefined
|
|
1973
2049
|
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1974
2050
|
: "No goal matches."
|
|
1975
|
-
: found.items.map(formatGoalLine).join("\n")
|
|
2051
|
+
: found.items.map(formatGoalLine).join("\n") +
|
|
2052
|
+
formatMore(found.more, page.limit, page.offset),
|
|
1976
2053
|
);
|
|
1977
2054
|
return EXIT.ok;
|
|
1978
2055
|
};
|
|
@@ -2892,11 +2969,39 @@ async function noticeVersions(io: CliIo, seen: ConsoleSeen): Promise<void> {
|
|
|
2892
2969
|
});
|
|
2893
2970
|
}
|
|
2894
2971
|
|
|
2972
|
+
/**
|
|
2973
|
+
* The refusal of a console older than this command works with (ADR-0038), in a console's own
|
|
2974
|
+
* form: a `426` the command reads as it reads the console's, before it acts on anything the
|
|
2975
|
+
* console answered.
|
|
2976
|
+
*/
|
|
2977
|
+
function consoleRefusal(io: CliIo, seen: ConsoleSeen): Response | undefined {
|
|
2978
|
+
const command = parseVersion(io.version);
|
|
2979
|
+
const server = parseVersion(seen.version);
|
|
2980
|
+
if (command === undefined || server === undefined) {
|
|
2981
|
+
return undefined;
|
|
2982
|
+
}
|
|
2983
|
+
const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
|
|
2984
|
+
if (verdict !== "outdated") {
|
|
2985
|
+
return undefined;
|
|
2986
|
+
}
|
|
2987
|
+
return Response.json(
|
|
2988
|
+
{
|
|
2989
|
+
error: {
|
|
2990
|
+
code: "console.outdated",
|
|
2991
|
+
message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
|
|
2992
|
+
},
|
|
2993
|
+
},
|
|
2994
|
+
{ status: 426 },
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2895
2998
|
/** What a refusal of the console is followed by, when there is something to do about it. */
|
|
2896
2999
|
function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
|
|
2897
3000
|
switch (error.code) {
|
|
2898
3001
|
case "client.outdated":
|
|
2899
3002
|
return `Update the command: ${updateWords(io, seen.version)}\n`;
|
|
3003
|
+
case "console.outdated":
|
|
3004
|
+
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2900
3005
|
case "client.too_new":
|
|
2901
3006
|
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2902
3007
|
case "auth.required":
|
|
@@ -2972,7 +3077,13 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
2972
3077
|
const response = await io.fetch(input, init);
|
|
2973
3078
|
seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
|
|
2974
3079
|
seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
|
|
2975
|
-
|
|
3080
|
+
// A console older than this command works with is refused here (ADR-0038); `serve`
|
|
3081
|
+
// carries the pages' own requests, which are not the command's to judge.
|
|
3082
|
+
// A refusal of the console's own is passed on as it is.
|
|
3083
|
+
return (
|
|
3084
|
+
(name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
|
|
3085
|
+
response
|
|
3086
|
+
);
|
|
2976
3087
|
},
|
|
2977
3088
|
};
|
|
2978
3089
|
try {
|
|
@@ -3004,7 +3115,9 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
3004
3115
|
}
|
|
3005
3116
|
if (error instanceof ApiError) {
|
|
3006
3117
|
io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
|
|
3007
|
-
return error.code === "client.outdated" ||
|
|
3118
|
+
return error.code === "client.outdated" ||
|
|
3119
|
+
error.code === "client.too_new" ||
|
|
3120
|
+
error.code === "console.outdated"
|
|
3008
3121
|
? EXIT.incompatible
|
|
3009
3122
|
: EXIT.failed;
|
|
3010
3123
|
}
|