@skillcdn/console 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -2
- package/SKILLCDN.md +29 -0
- package/dist/cli/cli.d.ts +3 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +61 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +5 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +2 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/serve.d.ts +37 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +218 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/console.d.ts +6 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +78 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +5 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +4 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +4 -0
- package/dist/i18n/ko.js.map +1 -1
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -0
- package/dist/schemas.js.map +1 -1
- package/dist/web.d.ts +16 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +149 -0
- package/dist/web.js.map +1 -0
- package/docs/specs/cli.md +92 -0
- package/docs/specs/rest.md +220 -0
- package/package.json +14 -3
- package/skills/building-a-console/SKILL.md +79 -0
- package/skills/working-the-board/SKILL.md +78 -0
- package/src/api.ts +235 -0
- package/src/cli/cli.ts +1114 -0
- package/src/cli/credentials.ts +68 -0
- package/src/cli/directory.ts +59 -0
- package/src/cli/format.ts +284 -0
- package/src/cli/help.ts +326 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/serve.ts +276 -0
- package/src/cli/terminal.ts +28 -0
- package/src/client.ts +533 -0
- package/src/components/board.tsx +112 -0
- package/src/components/connect.tsx +126 -0
- package/src/components/decision-form.tsx +136 -0
- package/src/components/decision-list.tsx +278 -0
- package/src/components/documents.tsx +569 -0
- package/src/components/event-feed.tsx +129 -0
- package/src/components/markdown.tsx +72 -0
- package/src/components/people.tsx +59 -0
- package/src/components/projects.tsx +364 -0
- package/src/components/runs.tsx +175 -0
- package/src/components/shell.tsx +284 -0
- package/src/components/sign-in.tsx +61 -0
- package/src/components/skills.tsx +78 -0
- package/src/components/task-form.tsx +229 -0
- package/src/components/task-view.tsx +272 -0
- package/src/components/tokens.tsx +223 -0
- package/src/components/ui.tsx +219 -0
- package/src/connect-page.tsx +148 -0
- package/src/connect.ts +27 -0
- package/src/console.tsx +1166 -0
- package/src/data.ts +493 -0
- package/src/docs-page.tsx +334 -0
- package/src/documents.ts +76 -0
- package/src/i18n/en.ts +537 -0
- package/src/i18n/index.ts +76 -0
- package/src/i18n/ko.ts +511 -0
- package/src/index.ts +142 -0
- package/src/limits.ts +61 -0
- package/src/router.ts +295 -0
- package/src/routes.ts +106 -0
- package/src/schemas.ts +776 -0
- package/src/styles/console.css +2432 -0
- package/src/text.ts +33 -0
- package/src/use-action.ts +37 -0
- package/src/vocabulary.ts +109 -0
- package/src/web.ts +170 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Spec: the command line
|
|
2
|
+
|
|
3
|
+
- Status: **Draft.** Version 1 is what an agent works the board with (Claude Code, Codex, any agent with a shell), what a person's own scripts and hooks use, and what serves a console of a person's own ([ADR-0014](../adr/0014-a-persons-own-console-is-served-from-their-machine-by-the-command.md)).
|
|
4
|
+
- The command is `console`, the `bin` of `@skillcdn/console` (`packages/console/src/cli/`): a thin client of the [REST API](rest.md) with no logic of its own ([ADR-0006](../adr/0006-agents-work-the-board-through-the-rest-api-and-the-command-line-not-an-mcp-server.md)). `@skillcdn/console/cli` exports `runCli(argv, io)`, the command as a function; the integration tests run it against the app with no socket.
|
|
5
|
+
|
|
6
|
+
## Signing in
|
|
7
|
+
|
|
8
|
+
A person installs the package where the agent runs, or has the agent install it, and the command connects once ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)): `console login --url <origin>` asks the console to connect, prints the address of the console's own page and a code of eight characters, and waits; the person opens the address signed in, checks that the page shows the same code, names the agent and approves; and the command receives a token of its own, which it never prints. An agent that can run commands does this itself and relays the address and the code to its person; `--agent <name>` says what it calls itself, shown to the person with the machine's name. A token made by hand on the Agents page is read from standard input with `--token-stdin` instead, for a script or a job, and checked against the console (`GET /api/v1/me` must know the person). Either way the command keeps the token with the address in `$XDG_CONFIG_HOME/skillcdn-console/credentials.json` (`~/.config/skillcdn-console/credentials.json` when `XDG_CONFIG_HOME` is unset), readable by the person alone where the system can say so. `CONSOLE_URL` and `CONSOLE_TOKEN` in the environment win over the file, for a hook or a job; `--url <origin>` names another console for one command. `logout` forgets the file; the token itself is removed on the Agents page, by its person or by an administrator. A token is never taken on the command line, never typed, and never printed.
|
|
9
|
+
|
|
10
|
+
From then on the agent is that person on the board ([ADR-0004](../adr/0004-people-and-agents-reach-the-board-only-through-the-api-with-a-credential-of-their-own.md)): what it does is attributed to the person and shown as done by the agent, called what the person called the token unless `take --agent` says otherwise.
|
|
11
|
+
|
|
12
|
+
## The project
|
|
13
|
+
|
|
14
|
+
The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-what-a-person-may-see-and-change-is-decided-per-project.md)), and every command on it works in one: the one `--project <key>` names, else `CONSOLE_PROJECT` in the environment, else the one `.skillcdn-console.json` names in the working directory or the nearest directory above it. `console use <key>` writes that file, after checking that the console knows the project to the person; it is meant to be committed, so that everyone who works in the checkout, and every agent they run in it, is in the same project without saying so. With none of the three, a command on the board says so and exits with `1`. `console projects` lists the projects the person may work in, with what they are in each; a project the person may not see is not found, as if it were not there.
|
|
15
|
+
|
|
16
|
+
## The commands
|
|
17
|
+
|
|
18
|
+
| Command | Asks the console | Says |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `login --url <origin> [--agent <name>] [--token-stdin]` | `POST /api/v1/connect`, then `POST /api/v1/connect/claim` until a person approved, then `GET /api/v1/me` with the token | the address and the code for a person; then who the token is; keeps the credentials |
|
|
21
|
+
| `logout` | nothing | that they are forgotten |
|
|
22
|
+
| `whoami` | `GET /api/v1/me` | the login, the role, the workspace, the address, and the project this directory works in |
|
|
23
|
+
| `projects` | `GET /api/v1/projects` | one line per project: key, name, what the person is in it, what waits in it |
|
|
24
|
+
| `use <key>` | `GET /api/v1/projects/<key>` | that the directory works in the project from now on; writes `.skillcdn-console.json` |
|
|
25
|
+
| `tasks [--state <state>]` | `GET .../tasks` | one line per task: number, state, priority, title, owner, assignee, what waits on it |
|
|
26
|
+
| `task <number\|id>` | `GET .../tasks/<ref>`, the decisions about it, the runs on it | the task in full |
|
|
27
|
+
| `task new <title> [--body <markdown>\|--file <path>] [--state <state>] [--priority <priority>]` | `POST .../tasks` | the number and the id |
|
|
28
|
+
| `skills` | `GET .../skills` | the project's skills, or the organization's: each with its description and the URI an agent loads it by through its own SkillCDN connection; or why there are none to show |
|
|
29
|
+
| `take <number\|id> [--agent <name>]` | `GET .../tasks/<ref>`, then `POST .../runs` | the run that began, the task in full, and what to do next |
|
|
30
|
+
| `report <markdown>`, `report --file <path>`, `report -` | `POST .../runs/<id>/reports` | how many reports the run carries |
|
|
31
|
+
| `hand-in <https url \| file path> [--label <words>]` | `POST .../runs/<id>/artifacts` for a link; `POST .../runs/<id>/files` for a file, sent as a form with its name and its media type by extension | how many artifacts |
|
|
32
|
+
| `ask "<question>" --option "<label>" ... [--body <markdown>\|--file <path>] [--wait <seconds>]` | `POST .../decisions` with `runId`, then `GET .../decisions/<id>?wait=` | the decision raised, its options; then the answer, or that it still waits |
|
|
33
|
+
| `decision <id> [--wait <seconds>]` | `GET .../decisions/<id>`, with `?wait=` while waiting | the decision; with a wait, the answer or that it still waits |
|
|
34
|
+
| `decisions [--open]` | `GET .../decisions` | one line per decision |
|
|
35
|
+
| `finish [--summary <markdown>\|--file <path>]`, `fail [...]` | `POST .../runs/<id>/end` | how the run ended |
|
|
36
|
+
| `abandon [<run id>] [--summary <markdown>]` | `POST .../runs/<id>/end`, as `abandoned` | the same |
|
|
37
|
+
| `runs [--task <number\|id>] [--open] [--mine]` | `GET .../runs` | one line per run |
|
|
38
|
+
| `run <id>` | `GET .../runs/<id>` | the run in full, with its reports, what it handed in and where each file is read |
|
|
39
|
+
| `decision <id> --outcome <markdown>\|--file <path>` | `PATCH .../decisions/<id>` | the decision as a record, with what followed written down |
|
|
40
|
+
| `docs [<folder>] [--search <words>] [--archived]` | `GET .../docs?folder=` or `?q=` | the folders and the pages of a folder, or the pages found by words: one line per page |
|
|
41
|
+
| `doc <path> [--version <n>\|--versions]` | `GET .../docs/<path>`, `.../versions`, `.../versions/<n>` | the page in full: its body, what it refers to, what refers to it, its files; or its versions; or one as it was |
|
|
42
|
+
| `write <path> [--title <title>] --body <markdown>\|--file <path> [--base <version>]` | `GET .../docs/<path>` for the title when none is said, then `PUT .../docs/<path>` | the path, the title and the version written, and what the page refers to |
|
|
43
|
+
| `attach <path> <file> [--label <words>]` | `POST .../docs/<path>/files`, as a form | how many files the page carries |
|
|
44
|
+
| `archive <path>`, `restore <path>` | `POST .../docs/<path>/archive`, `.../restore` | that the page is put away, or back |
|
|
45
|
+
| `serve [<dir>] [--port <n>]` | `GET /api/v1/me` once; then every request of the pages under `/api/`, carried with the token | where it serves and as whom; then nothing until stopped ([a console of one's own](#a-console-of-ones-own)) |
|
|
46
|
+
| `help [<command>]` | nothing | the usage, written for an agent that meets the command for the first time |
|
|
47
|
+
| `--version` (also `version`, `-v`) | nothing | which version of the command this is |
|
|
48
|
+
|
|
49
|
+
`...` is `/api/v1/projects/<key>`, the project the command works in. Everywhere: `--json` prints the console's own answer as JSON, in the shapes of `@skillcdn/console/api`; `--url <origin>` names another console for this one command; `--project <key>` the project. A Markdown body given as `-` is read from standard input. Every argument is bounded and checked by the console, as the REST API's schemas say; the command sends it as it was given.
|
|
50
|
+
|
|
51
|
+
## The documents
|
|
52
|
+
|
|
53
|
+
A project's pages of Markdown ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)), by path (`guides/onboarding`): `console docs` lists a folder, `console doc` reads a page, `console write` writes one, the first version or the next, keeping the title when none is said, `console attach` adds a file from the machine, `console archive` and `console restore` put a page away and bring it back. A link in a body whose destination is a page's path refers to that page, and `console doc` says what refers to a page in return. An agent writes down what it learned where the next agent will read it, and refers to it from its reports and in what it asks.
|
|
54
|
+
|
|
55
|
+
## The run a command means
|
|
56
|
+
|
|
57
|
+
`report`, `hand-in`, `ask`, `finish` and `fail` act on a run. Named with `--run <id>`, that one; else the one open run begun with this token in the project (`GET .../runs?open=true&mine=true`). None open, or several: the command exits with `1` and says so, listing the open ones, so that an agent working two tasks at once says which.
|
|
58
|
+
|
|
59
|
+
## Waiting for a decision
|
|
60
|
+
|
|
61
|
+
`ask` waits 60 seconds for the answer unless `--wait` says otherwise, within what an agent's shell gives one command; `decision <id> --wait <seconds>` waits as long as it is told, a day at most. The command asks the console to hold each request for up to 50 seconds (`?wait=`), the server's own bound, and repeats while the time lasts, with a breath between requests; the answer arrives as soon as a person gives it, since the console is woken through the database's own channel. A decision that still waits when the time is up is exit code `3`: the run waits with it, so the agent keeps waiting with `console decision <id> --wait 100`, within what its shell gives one command, and goes on meanwhile only with work that does not depend on the answer. `ask` prints the decision's id as soon as it is raised, before any waiting, so that a wait cut short loses nothing; with `--json` it prints the decision then, and again with its answer when that comes.
|
|
62
|
+
|
|
63
|
+
## A console of one's own
|
|
64
|
+
|
|
65
|
+
`console serve [<dir>] [--port <n>]` serves a console a person built from the package, for themselves ([ADR-0014](../adr/0014-a-persons-own-console-is-served-from-their-machine-by-the-command.md)): the build in `<dir>` (its `index.html` and files) at `http://127.0.0.1:11197/`, or the port given (`0` for one the system picks), and everything under `/api/` carried to the console the command is signed in to, with its token as `Authorization: Bearer`, so that the pages never hold the token and the organization's console allows no other origin. The command listens on the loopback only and answers no request whose host is not it; a request to the API that the browser says comes from another site's page (`Sec-Fetch-Site`) is refused, as is one from a browser that does not say and names another origin. What is the browser's does not travel: cookies, the page's origin and referrer, the browser's own metadata; what comes back travels decoded, without a cookie. The feed's stream passes through as it streams, and a file handed in as a form. Without a directory only the API is served, for a development server to send its `/api/` requests to with the loopback's host, so that a console is built with its reloading against the real board. The command checks the token against the console first (`GET /api/v1/me`), says where it serves and as whom, and runs until stopped. The pages learn from the same answer that they hold a token (`agent`) and offer nothing a token cannot do. How a console is built from the package is in the [package's README](../../packages/console/README.md#a-console-of-your-own).
|
|
66
|
+
|
|
67
|
+
## Output and exit codes
|
|
68
|
+
|
|
69
|
+
Plain text on standard output: one line per thing in a list, a few lines for one thing in full, with ids given, since the commands take them, and the vocabulary's own words (`in_progress`, `waiting`), since the options take them. What went wrong goes to standard error as the console's stable code and its words (`run.task_taken: an agent is at work on the task already`), with a hint when there is something to do about it.
|
|
70
|
+
|
|
71
|
+
| Exit code | Meaning |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `0` | Done. |
|
|
74
|
+
| `1` | The console refused, or could not be reached; the reason is on standard error. |
|
|
75
|
+
| `2` | The command was not understood. |
|
|
76
|
+
| `3` | The decision still waits for a person. |
|
|
77
|
+
|
|
78
|
+
## What an agent is told
|
|
79
|
+
|
|
80
|
+
`console help` says what the board is, that the agent acts as the person whose token it holds and that everything it sends is shown to people as text, which project the commands work in and how it is named, what each command does (take a task, report at the milestones of the work, hand in what was made, ask when a person must decide, finish when done, write and read the project's pages), and the refusals any command may meet. `console help <command>` adds what the command takes, the limits the console holds it to (how long a report, a summary, a question or an option may be; how many options, reports and links) and the refusals it may meet, by code, so that an agent learns them before it runs into them. The skill `working-the-board` ([skills/working-the-board/SKILL.md](../../skills/working-the-board/SKILL.md), [ADR-0015](../adr/0015-the-consoles-skills-live-in-this-repository-in-the-skillcdn-format-and-the-package-carries-them.md)) says how the work goes: reading, taking, reporting at the milestones, asking and waiting, handing in, writing down, finishing. An organization's own skill for its console adds the rest: which tasks to take, what to report, when to ask.
|
|
81
|
+
|
|
82
|
+
## Running an agent on a task
|
|
83
|
+
|
|
84
|
+
What a person does, once the command is signed in where the agent runs; tried with Claude Code on 2026-10-09, with an agent that took a task, reported at each milestone, raised a decision, acted on the answer, handed in and finished, and a person who answered on the board.
|
|
85
|
+
|
|
86
|
+
1. Write the task on the board of the project, with its body as the work order: what to make, where, the milestones, what to ask.
|
|
87
|
+
2. Start the agent in the directory it is to work in, where `console use <key>` has named the project (once, committed), and tell it, in its own words, what this amounts to: run `console help`; take the task with `console take <number>` and follow its body; report at the milestones with `console report`; ask with `console ask` when a person must decide, and keep waiting with `console decision <id> --wait 100` until the answer comes; hand in with `console hand-in`; finish with `console finish --summary`. Say that the command is signed in already and that it must never look for or print a token.
|
|
88
|
+
3. Follow the run on the board, and answer the decisions it raises. The agent's reports, what it handed in and its summary are the run's record on the task's page.
|
|
89
|
+
|
|
90
|
+
## Not yet
|
|
91
|
+
|
|
92
|
+
Hooks that report an agent's events without being asked ([roadmap](../roadmap.md)).
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Spec: the REST API of the board
|
|
2
|
+
|
|
3
|
+
- Status: **Draft.** Version 1 serves the default UI, the `console` command ([cli.md](cli.md)), and a custom console built from `@skillcdn/console`. It changes until 1.0; the package's changelog says when.
|
|
4
|
+
- The schemas live in `packages/console` (`@skillcdn/console/api`); the handlers in `apps/console/src/http/rest.ts`. The integration tests parse every answer with the package's schemas, which is what the pages and the command parse with.
|
|
5
|
+
|
|
6
|
+
The API is the board as its members see it: the people, the projects, and in each project the tasks, the runs, the decisions, the documents, and everything that happened. It is the one surface of the console ([ADR-0006](../adr/0006-agents-work-the-board-through-the-rest-api-and-the-command-line-not-an-mcp-server.md)): the pages, the command and a custom console are all its clients. Every route but `GET /api/v1/me` is for whoever is signed in, or for whoever presents a token a person made; nobody is answered with a refusal, never with less. The one workspace of the deployment is implied: there is no workspace in any path until a second one exists. The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-what-a-person-may-see-and-change-is-decided-per-project.md)): what belongs to one is under `/api/v1/projects/<key>`, and a project the asker may not see is not found, whatever is asked under it.
|
|
7
|
+
|
|
8
|
+
## Conventions
|
|
9
|
+
|
|
10
|
+
- Base path `/api/v1`; UTF-8 JSON. Absent values are `null`, never missing keys. Instants are ISO 8601 strings.
|
|
11
|
+
- Every answer is `cache-control: no-store`: it is one person's, or it changes.
|
|
12
|
+
- A request that changes something (`POST`, `PATCH`, `DELETE`) on a session must carry the session cookie **and** come from the console's own pages: the browser's `origin` header must be `PUBLIC_URL`. The cookie is `SameSite=Lax`, so another site's request does not carry it in the first place; the origin is the second fence.
|
|
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
|
+
- Configuring is a person's own doing, signed in on the console's own pages, never with a token: the tokens themselves, roles, projects and their members.
|
|
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
|
+
- 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.
|
|
17
|
+
- Errors have `{ "error": { "code": "...", "message": "..." } }`. The code is stable; the message is for a person, in English.
|
|
18
|
+
|
|
19
|
+
| Status | `code` | When |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 400 | `request.invalid` | A query parameter or the body does not parse; the message names the first field at fault. |
|
|
22
|
+
| 400 | `project.invalid_address` | The skills address is not one, as the standard spells them. |
|
|
23
|
+
| 400 | `member.invalid_person` | The person to list is not one of the workspace. |
|
|
24
|
+
| 400 | `task.invalid_assignee` | The assignee is not a person who may work in the project. |
|
|
25
|
+
| 400 | `task.invalid_parent` | The parent is not a task of the project, is the task itself, or would make a loop. |
|
|
26
|
+
| 400 | `decision.invalid_task` | The decision is about a task that is not one of the project's. |
|
|
27
|
+
| 400 | `decision.no_such_option` | The answer names an option the decision does not have. |
|
|
28
|
+
| 400 | `decision.invalid_run` | A decision raised from a run that is not the asker's, or is over. |
|
|
29
|
+
| 400 | `token.expiry_at_most` | A token that would not expire, or would outlast `TOKEN_DAYS_AT_MOST`, where the organization requires an expiry. |
|
|
30
|
+
| 400 | `document.invalid_path` | The path a page is written at is not one: segments of lowercase letters, digits and hyphens, separated by slashes. |
|
|
31
|
+
| 400 | `run.too_many_reports`, `run.too_many_artifacts` | The run carries as many as one may (`MAX_REPORTS_PER_RUN`, `MAX_ARTIFACTS_PER_RUN`). |
|
|
32
|
+
| 401 | `auth.required` | Nobody is signed in, or the session has ended, or the token is nothing. The cookie is taken away when the operator no longer lists the login. |
|
|
33
|
+
| 403 | `auth.forbidden_origin` | A request that changes something on a session did not come from the console's own pages. |
|
|
34
|
+
| 403 | `auth.forbidden` | A member asked for what only an administrator, or only an owner of the project, may do. |
|
|
35
|
+
| 403 | `auth.session_required` | A token asked for what only a person signed in may do: the tokens themselves, and configuring. |
|
|
36
|
+
| 403 | `run.not_yours` | The run is another person's. |
|
|
37
|
+
| 404 | `project.not_found` | No project has that key, or it is not the asker's to see: the two are one answer. The key is checked before anything is asked. |
|
|
38
|
+
| 404 | `task.not_found`, `decision.not_found`, `token.not_found`, `person.not_found`, `run.not_found`, `run.task_not_found`, `member.not_found` | No such id in the project (or in the workspace, for a token, a person); for a token, none of the asker's. An id that is not a UUID is not found either, except a task's number. |
|
|
39
|
+
| 404 | `file.not_found` | No file handed in, or attached to the page, has that id in the project; a link handed in is not a file. |
|
|
40
|
+
| 404 | `document.not_found`, `document.version_not_found` | No page has that path in the project, or the page has no such version. A path that is not one is not found either. |
|
|
41
|
+
| 404 | `connect.not_found` | No agent asks to connect under this code or secret: it was never asked, it expired, the person said no, or the token was handed over already. |
|
|
42
|
+
| 404 | `not_found` | Nothing at this path. |
|
|
43
|
+
| 409 | `project.key_taken` | A project of the workspace has that key already. |
|
|
44
|
+
| 409 | `member.exists` | The person is listed in the project already. |
|
|
45
|
+
| 409 | `decision.answered` | The decision has an answer already. |
|
|
46
|
+
| 409 | `document.conflict` | The page has moved on since the version the write started from (`baseVersion`), or was written at that path by someone else first. |
|
|
47
|
+
| 409 | `document.archived` | The page is archived: restore it before writing to it or attaching to it. |
|
|
48
|
+
| 409 | `document.too_many_versions`, `document.too_many_files` | The page carries as many versions, or files, as one may (`MAX_VERSIONS_PER_DOCUMENT`, `MAX_FILES_PER_DOCUMENT`). |
|
|
49
|
+
| 409 | `token.too_many` | The person holds as many live tokens as one may (`MAX_TOKENS_PER_PERSON`). |
|
|
50
|
+
| 409 | `connect.approved` | The connection was approved already; the command claims its token. |
|
|
51
|
+
| 429 | `connect.too_many` | As many agents ask to connect as the console holds at once; try again in a few minutes. |
|
|
52
|
+
| 409 | `person.last_admin` | The change would leave the board without an administrator. |
|
|
53
|
+
| 409 | `run.over`, `run.task_taken`, `run.task_closed` | The run has ended already; an agent is at work on the task already; the task is done or dropped. |
|
|
54
|
+
| 413 | `request.too_large` | The body is over the limit; for a file handed in or attached, the file is over `MAX_FILE_BYTES`. |
|
|
55
|
+
|
|
56
|
+
## The workspace
|
|
57
|
+
|
|
58
|
+
### `GET /api/v1/me`
|
|
59
|
+
|
|
60
|
+
`{ "workspace": { "name", "tokenDaysAtMost" }, "person": ... | null, "agent": "..." | null, "language": "..." | null, "signIn": [{ "key", "label" }, ...] }`: what the board is called and the most days a token may be good for here (or `null` where a person chooses), 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. 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.
|
|
61
|
+
|
|
62
|
+
### `GET /api/v1/people`
|
|
63
|
+
|
|
64
|
+
`{ "items": [person, ...] }`: everyone who has signed in and is still listed, by login. What a picker of assignees and of members is made of. A person carries their `role` in the workspace: `admin`, who configures it, or `member`, who works in the projects they are in.
|
|
65
|
+
|
|
66
|
+
### `PATCH /api/v1/people/<id>`
|
|
67
|
+
|
|
68
|
+
Takes `role` and answers the person. For an administrator signed in on the console's own pages, never with a token: an agent works as its person does, and configuring is a person's own doing. The board keeps at least one administrator (`person.last_admin`). A change writes a `person.role_changed` event with `login` and `role`; a change to what is already so writes nothing.
|
|
69
|
+
|
|
70
|
+
### `GET /api/v1/projects`
|
|
71
|
+
|
|
72
|
+
`{ "items": [project, ...] }`: the projects the asker may see, by name. A project carries its `key` (what paths and the command say; lowercase letters, digits and hyphens; immutable), `name`, `description`, `visibility` (`workspace`: everyone of the workspace is a member; `private`: only those listed), `skillsAddress` (canonical, or `null` for the organization's), `role` (what the asker is in it: `owner`, who configures it, or `member`, who works in it), `openDecisions` and `openRuns` (what waits in it), `createdAt` and `updatedAt`. A workspace administrator sees every project, as its owner; a member sees those open to the workspace, as a member, and those they are listed in, as what they are listed.
|
|
73
|
+
|
|
74
|
+
### `POST /api/v1/projects`
|
|
75
|
+
|
|
76
|
+
Takes `key` and `name`, and optionally `description`, `visibility` (default `private`) and `skillsAddress` (checked as the standard spells one, kept canonical; `project.invalid_address`). For whoever is signed in on the console's own pages, never with a token; they become the project's owner. Answers `201` with the project and writes a `project.created` event in it, with `key` and `name`. A key taken is `project.key_taken`.
|
|
77
|
+
|
|
78
|
+
### `GET /api/v1/events?after=&limit=`
|
|
79
|
+
|
|
80
|
+
The workspace's own events, the ones about no project: who joined (`person.joined`), who was made what (`person.role_changed`). The same shape and parameters as a project's feed below, without the filters.
|
|
81
|
+
|
|
82
|
+
### `PATCH /api/v1/me`
|
|
83
|
+
|
|
84
|
+
Keeps the person's own choice of language for the pages ([ADR-0012](../adr/0012-the-pages-speak-the-persons-language-from-packs-that-ship-with-the-package.md)): `{ "language": "ko" }`, a language tag as a browser sends one, or `null` to choose none and let the browser's languages decide; answers what `GET` answers. A person's own doing, from the console's own pages: never with a token.
|
|
85
|
+
|
|
86
|
+
### `GET /api/v1/tokens` and `POST /api/v1/tokens`
|
|
87
|
+
|
|
88
|
+
The tokens of whoever is signed in, for their agents, scripts and consoles of their own: managed by a person signed in on the console's own pages, never with a token, so that a token which leaks cannot outlive its removal through tokens of its own. `GET` answers `{ "items": [token, ...] }`, the live ones newest first: `id`, `name`, `createdAt`, `expiresAt` (or `null` for a token that does not expire), `lastUsedAt` (or `null`; noted at most hourly), and never a secret. `POST` takes `name` (what the person calls it: the agent it is for, where it runs; what the board shows at work unless the agent says otherwise, and what an event names as the agent) and optionally `expiresInDays` (`1` to `MAX_TOKEN_DAYS`; `DEFAULT_TOKEN_DAYS` when left out; `null` for a token that does not expire, where the organization allows one; `token.expiry_at_most` otherwise) and answers `201` with `{ "token": token, "secret": "cns_t_..." }`: the secret this once, which the server keeps only as a hash.
|
|
89
|
+
|
|
90
|
+
### `DELETE /api/v1/tokens/<id>`
|
|
91
|
+
|
|
92
|
+
Takes one of the asker's tokens away, whoever holds it, and answers `204`. A token that is not the asker's is `token.not_found`.
|
|
93
|
+
|
|
94
|
+
### `GET /api/v1/people/<id>/tokens` and `DELETE /api/v1/people/<id>/tokens/<token id>`
|
|
95
|
+
|
|
96
|
+
An administrator's sight of everyone's agents ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)): the live tokens of any person, as `GET /api/v1/tokens` lists one's own, and the way to take one away, from the console's own pages; `auth.forbidden` for a member, `person.not_found` for an id that is nobody's, `token.not_found` for a token that is not that person's.
|
|
97
|
+
|
|
98
|
+
### Connecting an agent
|
|
99
|
+
|
|
100
|
+
`POST /api/v1/connect`, `GET /api/v1/connect/<code>`, `POST /api/v1/connect/<code>/approve`, `POST /api/v1/connect/<code>/deny` and `POST /api/v1/connect/claim`: how an agent gets its token without anyone copying one ([ADR-0011](../adr/0011-an-agent-connects-with-a-short-code-a-person-approves-and-the-token-is-never-shown.md)).
|
|
101
|
+
|
|
102
|
+
`POST /api/v1/connect` is nobody's: the command sends `{ "agent" }` (what the agent calls itself and where it runs, a line up to `MAX_TOKEN_NAME_LENGTH`) and is answered `201` with `{ "code", "url", "secret", "expiresAt", "interval" }`: a code of eight characters in two groups (`ABCD-EFGH`, from an alphabet without look-alikes) for a person to approve, the console's own page for it, a secret (`cns_c_...`) the command claims with and shows nowhere, when the request expires (ten minutes after it began), and how many seconds the command waits between claims. A deployment holds a bounded number of requests at once (`connect.too_many`).
|
|
103
|
+
|
|
104
|
+
`GET /api/v1/connect/<code>`, for a person signed in, answers what asks: `{ "code", "agent", "createdAt", "expiresAt", "approved" }`. A code is read as a person types it: any case, with or without the hyphen. `POST .../approve`, from the console's own pages, takes what `POST /api/v1/tokens` takes (`name`, `expiresInDays`) and answers the request, approved; `POST .../deny` takes the request away and answers `204`.
|
|
105
|
+
|
|
106
|
+
`POST /api/v1/connect/claim` takes `{ "secret" }` and answers `202` with `{ "status": "pending", "expiresAt" }` while nobody approved, or `200` with `{ "status": "connected", "token", "secret" }` once: the token is made then, for the approver, as any token of theirs is, and the request goes with it, so that a second claim is `connect.not_found`. Nothing secret rests in the database meanwhile: the hash of the command's secret, and never a token.
|
|
107
|
+
|
|
108
|
+
## A project
|
|
109
|
+
|
|
110
|
+
Everything below is under `/api/v1/projects/<key>`, for whoever may see the project; what changes it needs a member (with a session from the console's own pages, or a token), what configures it an owner signed in.
|
|
111
|
+
|
|
112
|
+
### `GET /api/v1/projects/<key>` and `PATCH /api/v1/projects/<key>`
|
|
113
|
+
|
|
114
|
+
The project, as the asker sees it. A patch, for an owner signed in, takes any of `name`, `description`, `visibility` and `skillsAddress` (`null` for the organization's), and writes a `project.updated` event naming the `fields`; what is already so writes nothing. The key does not change.
|
|
115
|
+
|
|
116
|
+
### `GET /api/v1/projects/<key>/members` and `POST /api/v1/projects/<key>/members`
|
|
117
|
+
|
|
118
|
+
`{ "items": [member, ...] }`: those listed in the project, by login, each a `person`, their `role` in it (`owner`, `member`) and `addedAt`. A project open to the workspace has everyone of it as a member besides; an administrator is an owner of every project, listed or not. `POST`, for an owner signed in, takes `personId` (a person of the workspace; `member.invalid_person`) and optionally `role` (default `member`), answers `201` with the member and writes `project.member_added` with `login` and `role`. A person listed already is `member.exists`.
|
|
119
|
+
|
|
120
|
+
### `PATCH /api/v1/projects/<key>/members/<person id>` and `DELETE .../members/<person id>`
|
|
121
|
+
|
|
122
|
+
For an owner signed in. The patch takes `role` and writes `project.member_changed`; the removal answers `204` and writes `project.member_removed`. A person not listed is `member.not_found`. A project may be left without an owner listed; the administrators own it then.
|
|
123
|
+
|
|
124
|
+
### `GET /api/v1/projects/<key>/tasks?state=`
|
|
125
|
+
|
|
126
|
+
`{ "items": [task, ...] }`: the project's board, newest first, at most `LIST_LIMIT` tasks; `state` keeps one state of it. 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`, `assignee` (a person, or `null`), `parentId`, `links` (`url` and `label`), `openDecisions` (how many decisions about it wait for a person), `openRuns` (how many agents are at work on it, or waiting), `createdAt` and `updatedAt`.
|
|
127
|
+
|
|
128
|
+
### `POST /api/v1/projects/<key>/tasks`
|
|
129
|
+
|
|
130
|
+
Takes `title`, and optionally `body`, `state` (default `idea`), `priority` (default `normal`), `assigneeId` (a person 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; `task.invalid_assignee`), `parentId` (a task of the project) 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.
|
|
131
|
+
|
|
132
|
+
### `GET /api/v1/projects/<key>/tasks/<id or number>` and `PATCH .../tasks/<id>`
|
|
133
|
+
|
|
134
|
+
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`. A patch that changes nothing writes nothing and answers the task as it is.
|
|
135
|
+
|
|
136
|
+
### `GET /api/v1/projects/<key>/decisions?open=&task=`
|
|
137
|
+
|
|
138
|
+
`{ "items": [decision, ...] }`: the project's, the ones that wait first, newest first within each; `open=true` keeps only those that wait, `task=<id>` only those about one task. A decision carries its `question`, `body` (Markdown: the context it rests on), `options` (each an `id` and a `label`), `taskId` and `taskNumber` (or `null`), `raisedBy`, `run` (the `id` and `agent` of the run that raised it, when an agent asked, or `null`), `answer`: `null` while it waits, else the chosen `option`, a `note` or `null` (the rationale given with it), `by` and `at`; and `outcome`, what followed, in Markdown, or `null` until someone writes it. Context, rationale and outcome make the decision a record ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)); a link in any of them to a document's path is kept both ways, so that the document says the decision refers to it.
|
|
139
|
+
|
|
140
|
+
### `POST /api/v1/projects/<key>/decisions`
|
|
141
|
+
|
|
142
|
+
Takes `question`, `options` (two to `MAX_OPTIONS` labels), and optionally `body`, and either `taskId` or `runId`. Options are numbered from `1`. From a run, which must be the asker's and open (`decision.invalid_run`), the decision is about the run's task, `taskId` is not looked at, and the run waits (`status: waiting`) until the answer. Answers `201` with the decision and writes a `decision.raised` event, with the `agent` when a run asked.
|
|
143
|
+
|
|
144
|
+
### `GET .../decisions/<id>?wait=` and `POST .../decisions/<id>/answer`
|
|
145
|
+
|
|
146
|
+
One decision. With `wait=<seconds>` (`0` to `600`) the request is held while the decision waits, up to the server's own bound (50 seconds), and answers the decision as it stands then: how an agent learns the answer at once, woken through the database's own channel, without asking again and again. A decision answered already, or a wait of `0`, answers at once.
|
|
147
|
+
|
|
148
|
+
The answer takes `option` (the id of one of its options) and optionally a `note`. A decision is answered once; the answer records who gave it and when, writes a `decision.answered` event with the label of the option, and lets a run that waited go on (`status: running`), unless another decision of its still waits.
|
|
149
|
+
|
|
150
|
+
### `PATCH /api/v1/projects/<key>/decisions/<id>`
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
### `GET /api/v1/projects/<key>/runs?task=&open=&mine=` and `GET .../runs/<id>`
|
|
155
|
+
|
|
156
|
+
`{ "items": [run, ...] }`: the project's runs, newest first, at most `LIST_LIMIT`; `task=<id>` keeps those on one task, `open=true` those not over (`running` or `waiting`) and `open=false` those over, `mine=true` the asker's own: with a token, the runs begun with that token, which is one agent's; on a session, the runs for the person. A run is one agent at work on one task for one person: `taskId` and `taskNumber` (the one people say), `person` (whom the agent acts for), `agent` (what it calls itself), `status` (`running`, `waiting` for a decision, `finished`, `failed`, `abandoned`), `startedAt`, `endedAt` or `null`, `summary` (Markdown, what the agent said at the end, or `null`), `reports` (each `id`, `body` in Markdown, `createdAt`; oldest first), `artifacts` (each `id`, `kind`, `url` for a link or `null`, `label` or `null`, `file` for a file or `null`, and `createdAt`; a file is its `name`, `size` in bytes, `contentType` and `sha256`), and `waitingFor`, the id of the decision the run waits for, or `null`.
|
|
157
|
+
|
|
158
|
+
### `POST /api/v1/projects/<key>/runs`
|
|
159
|
+
|
|
160
|
+
Takes `taskId` (a task of the project; `run.task_not_found` otherwise), and optionally `agent` (what people see at work, `MAX_AGENT_LENGTH`; left out, the name of the token presented, or the person's login on a session). The run begins: the task becomes the asker's and `in_progress`, and the board is told (`run.started`). One agent at a time per task (`run.task_taken`); a task that is `done` or `dropped` cannot be taken (`run.task_closed`). Answers `201` with the run, whose `id` the rest take.
|
|
161
|
+
|
|
162
|
+
### `POST .../runs/<id>/reports` and `POST .../runs/<id>/artifacts`
|
|
163
|
+
|
|
164
|
+
How the work goes, and what was made. A report takes `body` (Markdown, `MAX_BODY_LENGTH`, not blank) and tells the board (`run.reported`, with the first words as `excerpt`); a link takes an https `url` and optionally a `label` (`run.handed_in`, with the label or the link's first words). Both answer `201` with the run. Only the run's person adds to it (`run.not_yours`), only while it is open (`run.over`), and only so many (`run.too_many_reports`, `run.too_many_artifacts`, counting links and files together).
|
|
165
|
+
|
|
166
|
+
### `POST .../runs/<id>/files` and `GET /api/v1/projects/<key>/files/<id>`
|
|
167
|
+
|
|
168
|
+
A file handed in, as the parts of a form (`multipart/form-data`): the part `file` carries the bytes, with the file's name and its media type; the part `label` is optional. The name is one line without a path (`MAX_FILE_NAME_LENGTH`), the file is not empty and at most `MAX_FILE_BYTES` (`request.too_large`), and a media type that is not one, or none, is `application/octet-stream`. The bytes are kept under their SHA-256, so the same file handed in twice is kept once; the artifact on the run is `kind: "file"` with `file` filled in and `url` null. Answers `201` with the run and tells the board (`run.handed_in`, with the label or the file's name). The same rules as for a link: the run's person only, while the run is open, and only so many.
|
|
169
|
+
|
|
170
|
+
The bytes are read back at `/api/v1/projects/<key>/files/<artifact id>` by whoever may see the project, with a session or a token: `content-length` and `content-disposition` name the file, the few kinds a browser may show in place (pictures, plain text, Markdown, CSV, JSON) are answered with their type and `inline`, and every other kind as `application/octet-stream` and `attachment`, never sniffed (`x-content-type-options`) and under a policy that runs nothing (`sandbox`). An id that is a link's, or nothing, or another project's, is `file.not_found`.
|
|
171
|
+
|
|
172
|
+
### `POST .../runs/<id>/end`
|
|
173
|
+
|
|
174
|
+
Takes `status` (`finished`, `failed` or `abandoned`) and optionally a `summary` (Markdown, `MAX_SUMMARY_LENGTH`: what was done and what is left), ends the run (`run.ended`), and answers it. A finished run puts a task that was `in_progress` up for review (`in_review`); the rest leave the task as it is. The run's person ends it, as `finished` or `failed` from their agent; `abandoned` is a person giving up on a run that will not come back: their own, or anyone's as an owner of the project signed in on the console's own pages, never with a token (`run.not_yours`). A run that is over is `run.over`.
|
|
175
|
+
|
|
176
|
+
### `GET /api/v1/projects/<key>/skills`
|
|
177
|
+
|
|
178
|
+
The project's skills, as the SkillCDN deployment serves them at the project's address, or at the organization's (`SKILLS_ADDRESS`) when the project names none, read through `SKILLCDN_URL`: `{ "address", "source", "page", "status", "items" }`. `address` is canonical, or `null` with `status: "none"` when neither names one; `source` is the deployment's origin and `page` where a person browses the address. `status` is `ready`, or why `items` is empty: `indexing` and `failed` as the deployment says of the repository, `not_found` when it does not serve the address to the console (which asks as nobody, so a private repository is not read), `unavailable` when it could not be reached or read. An item is one skill: `name`, `description`, `directory` and `path` (of its `SKILL.md`, from the repository's root), `page` (the skill at the deployment, for a person), `uri` (what an agent loads it by through its own SkillCDN connection, `skill://gh/<owner>/<repo>/<path>`, or `null`), and `translations` (the title and the description by language tag, as the repository gives them). The console keeps nothing of the skills: one answer per address stands for a minute, then the deployment is asked again.
|
|
179
|
+
|
|
180
|
+
### The documents
|
|
181
|
+
|
|
182
|
+
The project's pages of Markdown ([ADR-0009](../adr/0009-documents-are-pages-of-markdown-in-a-project-addressed-by-path-versioned-and-linked-both-ways.md)): each under a path of lowercase segments separated by slashes (`guides/onboarding`), which is its address and does not change; the folders are what the paths say. In every path below, the document's path is **one segment of the URL, its slashes encoded** (`/docs/guides%2Fonboarding`), which is what `projectPath(key, "docs", path)` builds. Whoever may see the project reads them; whoever may work in it writes them, with a session or a token.
|
|
183
|
+
|
|
184
|
+
#### `GET /api/v1/projects/<key>/docs?folder=&q=&archived=`
|
|
185
|
+
|
|
186
|
+
`{ "folder", "folders": [path, ...], "items": [document, ...] }`: the pages of a folder (`folder=guides`; the root when left out or empty), by title, and the folders in it, as paths, by name; at most `LIST_LIMIT` of each. With `q=<words>` (`MAX_SEARCH_LENGTH`), the pages found by their words in the title or the body, the best first, through the database's own text search and the title and the path by substring; `folder` is not looked at then, and `folders` is empty. Archived pages are left out unless `archived=true`. A page here is everything but its body: `id`, `path`, `title`, `version` (the number of the latest version, from 1), `updatedBy` and `agent` (who wrote the latest version, and the agent they wrote it through, or `null`), `archivedAt` (or `null`), `createdAt` and `updatedAt`.
|
|
187
|
+
|
|
188
|
+
#### `GET /api/v1/projects/<key>/docs/<path>`
|
|
189
|
+
|
|
190
|
+
The page in full: the fields above, `body` (Markdown, rendered to elements, never as HTML), `createdBy`, `links` (the documents it links to, each a `path` and the `title` of the page there, or `null` while none is written yet), `backlinks` (what links to it: each a `kind`, `document`, `task` or `decision`, its `id`, the document's `path` or the task's `number` or `null`, and a `title`: the document's, the task's, or the decision's question), and `files` (each an `id`, a `label` or `null`, the `file` as a run hands one in, `addedBy`, `agent` or `null`, and `createdAt`). A path that is not one, or has no page, or is in a project the asker may not see, is not found.
|
|
191
|
+
|
|
192
|
+
#### `PUT /api/v1/projects/<key>/docs/<path>`
|
|
193
|
+
|
|
194
|
+
Writes the page: takes `title`, `body` (Markdown, `MAX_DOCUMENT_LENGTH`) and optionally `baseVersion`, the version the writer started from. The first write at a path answers `201` with the page at version 1; the next ones answer `200` with the next version, each kept with who wrote it, through which agent, and when. A write that changes nothing is no version and no event. The write is refused when the page has moved on since `baseVersion` (`document.conflict`), when the page is archived (`document.archived`), and when it carries as many versions as one may (`document.too_many_versions`). Every link in the body whose destination is a document path (`guides/onboarding`, from the project's root, with or without a `#fragment`) is recorded, and the page it names, written or not yet, says so. Writes a `document.written` event with `path`, `title` and `version`.
|
|
195
|
+
|
|
196
|
+
#### `POST .../docs/<path>/archive` and `POST .../docs/<path>/restore`
|
|
197
|
+
|
|
198
|
+
Put the page away, and bring it back: archived, it is kept out of the folders and the search, readable at its path, and not written to; nothing is deleted. Each answers the page and writes `document.archived` or `document.restored`; what is already so writes nothing.
|
|
199
|
+
|
|
200
|
+
#### `GET .../docs/<path>/versions` and `GET .../docs/<path>/versions/<number>`
|
|
201
|
+
|
|
202
|
+
`{ "items": [version, ...] }`, newest first: each a `number`, the `title` as it was, the `author`, the `agent` or `null`, and `createdAt`. One version adds its `body` as it was. A page without that version is `document.version_not_found`.
|
|
203
|
+
|
|
204
|
+
#### `POST .../docs/<path>/files` and `GET .../docs/<path>/files/<id>`
|
|
205
|
+
|
|
206
|
+
A file attached to the page, sent and read back exactly as a run hands one in ([above](#post-runsidfiles-and-get-apiv1projectskeyfilesid)): the parts `file` and `label` of a form, kept under the SHA-256 of the bytes, at most `MAX_FILE_BYTES`, at most `MAX_FILES_PER_DOCUMENT` on one page (`document.too_many_files`), and none on an archived page. Answers `201` with the page and writes `document.file_attached` with the label or the file's name. The bytes are read back at the file's `id`, with the same headers and the same policy as a run's file.
|
|
207
|
+
|
|
208
|
+
### `GET /api/v1/projects/<key>/events?after=&limit=&task=&run=&decision=&document=`
|
|
209
|
+
|
|
210
|
+
`{ "items": [event, ...], "more": boolean }`: what happened in the project after event number `after` (`0` for the beginning), oldest first, at most `limit` (default and maximum `EVENTS_PAGE_LIMIT`); `more` says whether there is more after the last item. `task=<id>`, `run=<id>`, `decision=<id>` or `document=<id>` keeps the events about one of them: how a page shows everything that happened to a task, a run, a decision or a document. An event carries its `id` (the number), `kind` (one of `EVENT_KINDS`), the `actor` (a person, or, for what an agent did, the person it acts for), `agent` (what the person acted through: the run's agent for what a run did, else the name of the token presented; `null` when a person acted themselves), `projectId`, `taskId`, `decisionId`, `runId` and `documentId` (or `null`), `data` (what a feed shows without asking for the subject: `number`, `title`, `fields`, `from`, `to`, `question`, `option`, `login`, `role`, `key`, `name`, `agent`, `status`, `label`, `excerpt`, `path`, `version`, each only when the kind has it), and `createdAt`.
|
|
211
|
+
|
|
212
|
+
### `GET /api/v1/projects/<key>/events/stream?after=`
|
|
213
|
+
|
|
214
|
+
The same events as they happen, as server-sent events: one message per event, with `id` the event's number, `event` its kind and `data` the event as JSON; a comment (`: ping`) every 25 seconds of silence. A browser that reconnects sends `Last-Event-ID`, which wins over `after`, so that nothing is missed. The stream ends when the server shuts down; an `EventSource` reconnects on its own.
|
|
215
|
+
|
|
216
|
+
The feed is written in the same transaction as the change it records, so what a stream carries happened, and what happened is carried; every process of the deployment learns of a change through the database's own channel, so a board with several `api` replicas is one board.
|
|
217
|
+
|
|
218
|
+
## Signing in
|
|
219
|
+
|
|
220
|
+
Not part of the REST API, and described in [`deploy/README.md`](../../deploy/README.md#signing-in): `GET /auth/<provider>/login?return_to=` and `GET /auth/<provider>/callback` for each provider the deployment has (`gh`, `google`; any other is nothing), and `POST /auth/logout` from the console's own pages. A page learns why a sign-in did not complete from `?sign_in=denied|expired|failed|refused` on the page it was for.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillcdn/console",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"skillcdn",
|
|
15
15
|
"console",
|
|
16
16
|
"agents",
|
|
17
|
-
"cli"
|
|
17
|
+
"cli",
|
|
18
|
+
"skills"
|
|
18
19
|
],
|
|
19
20
|
"type": "module",
|
|
20
21
|
"sideEffects": false,
|
|
@@ -37,11 +38,21 @@
|
|
|
37
38
|
"types": "./dist/cli/cli.d.ts",
|
|
38
39
|
"default": "./dist/cli/cli.js"
|
|
39
40
|
},
|
|
41
|
+
"./web": {
|
|
42
|
+
"types": "./dist/web.d.ts",
|
|
43
|
+
"default": "./dist/web.js"
|
|
44
|
+
},
|
|
40
45
|
"./console.css": "./dist/console.css"
|
|
41
46
|
},
|
|
42
47
|
"files": [
|
|
43
48
|
"dist",
|
|
44
|
-
"!dist/**/*.tsbuildinfo"
|
|
49
|
+
"!dist/**/*.tsbuildinfo",
|
|
50
|
+
"src",
|
|
51
|
+
"!src/**/*.test.ts",
|
|
52
|
+
"!src/**/*.test.tsx",
|
|
53
|
+
"skills",
|
|
54
|
+
"docs",
|
|
55
|
+
"SKILLCDN.md"
|
|
45
56
|
],
|
|
46
57
|
"publishConfig": {
|
|
47
58
|
"access": "public"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: building-a-console
|
|
3
|
+
description: "Builds a console from the @skillcdn/console package: a console of a person's own, served from their machine with their token by console serve; an organization's console, which takes the default UI's place in the image; or a script against the console's REST API. Says where the package's layers are (the API client and schemas, the components, the composition), how the default console is put together, what to configure and what to replace (a component, the language packs, the brand, the styles' tokens), how a build is run against the board and checked, and the rules that keep untrusted content harmless. Use when an agent is asked to make, change or extend a SkillCDN Console UI or a client of its API. To work tasks on the board, use working-the-board."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: A shell with Node.js 24 or newer and npm or pnpm, and an agent that reads and writes files. For running a console of one's own, the console command signed in by a person. No key of the agent's own is needed, and it never sees a token.
|
|
6
|
+
metadata:
|
|
7
|
+
author: skillcdn
|
|
8
|
+
version: "1.0"
|
|
9
|
+
skillcdn:
|
|
10
|
+
translations:
|
|
11
|
+
ko:
|
|
12
|
+
title: 패키지로 콘솔 만들기
|
|
13
|
+
description: "@skillcdn/console 패키지로 콘솔을 만듭니다. console serve로 내 PC에서 내 토큰으로 돌리는 개인 콘솔, 이미지에서 기본 UI 자리를 대신하는 조직 콘솔, 또는 콘솔 REST API를 쓰는 스크립트. 패키지의 세 계층(API 클라이언트와 스키마, 컴포넌트, 구성)이 어디 있는지, 기본 콘솔이 어떻게 조립되는지, 무엇을 설정하고 무엇을 바꾸는지(컴포넌트, 언어 팩, 브랜드, 스타일 토큰), 빌드를 보드에 붙여 돌리고 확인하는 법, 신뢰할 수 없는 내용을 안전하게 다루는 규칙을 말합니다. SkillCDN 콘솔 UI나 API 클라이언트를 만들거나 바꾸라고 할 때 쓰세요. 보드의 작업을 할 때는 working-the-board를 쓰세요."
|
|
14
|
+
---
|
|
15
|
+
# Building a console from the package
|
|
16
|
+
|
|
17
|
+
What goes in: what the console is for, which is one person's own, the organization's, or a script against the API, and what it must show or do. What comes out: a small repository that depends on `@skillcdn/console` and builds to static files, served by `console serve` on one person's machine or by the image in the default UI's place; or a script that talks to the REST API with a token. Nothing of the server is built: a console shows an organization's board through its API and holds nothing of its own.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
| Need | How | When missing |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Node.js 24 or newer, with npm or pnpm | `node --version` | Stop and say so. |
|
|
24
|
+
| The package | `npm install @skillcdn/console react react-dom`, and for pages `npm install --save-dev vite @vitejs/plugin-react typescript` | The package is on npm; nothing else is needed. |
|
|
25
|
+
| For running one's own: the `console` command, signed in | `console whoami` says as whom and at which console. | `npm install -g @skillcdn/console`, then `console login --url <the console's address>`: a person approves the code it shows. Never look for a token. |
|
|
26
|
+
|
|
27
|
+
## Where to read, in this order
|
|
28
|
+
|
|
29
|
+
Nothing here restates them: they are the contract, current at the version installed.
|
|
30
|
+
|
|
31
|
+
1. **The package's README**: `node_modules/@skillcdn/console/README.md` once installed, [`packages/console/README.md`](/packages/console/README.md) in the console's repository. Every export by layer and what each takes, the styles, the languages, and "A console of your own" with the three files to write.
|
|
32
|
+
2. **The types**: `node_modules/@skillcdn/console/dist/index.d.ts`, `api.d.ts`, `cli/cli.d.ts` and `web.d.ts`: the exact props of every component and the shape of every answer, with the comment that says what each is for.
|
|
33
|
+
3. **The sources**, shipped with the package under `src/`: `console.tsx` (how the default console composes the pages from the components, routes, and loads its data), `components/*.tsx` (each component and what it renders), `data.ts` (how the workspace and a project are loaded and kept current by the stream), `styles/console.css` (the tokens, `--sc-*`, and every class, `sc-`-prefixed), `i18n/en.ts` (every word the pages show, keyed as a pack is).
|
|
34
|
+
4. **The REST API**: [docs/specs/rest.md](/docs/specs/rest.md), in the package at `docs/specs/rest.md`: every route, what it takes and answers, who may ask, and the codes of its refusals. **The command line**: [docs/specs/cli.md](/docs/specs/cli.md).
|
|
35
|
+
5. **The default console itself**, in the console's repository under `apps/console/web/`: `index.html`, `src/main.tsx` and `vite.config.ts`, with the brand, the fonts and the icons wired in; a console of one's own copies their shape.
|
|
36
|
+
6. **The architecture**: [docs/architecture.md](/docs/architecture.md) for how the parts fit, and the [decisions](/docs/adr/README.md); ADR-0014 says how a person's own console reaches the organization's.
|
|
37
|
+
|
|
38
|
+
## Inputs
|
|
39
|
+
|
|
40
|
+
| Input | Source |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Which kind | One person's own, served by `console serve` with their token; the organization's, built into the image at `WEB_ROOT`; or a script, from the API client alone. Asked once when the request does not say; a member usually wants their own. |
|
|
43
|
+
| What it must show or do | From the request: a page, a component, a language, a brand, a column, a filter, a report. Everything else is the default console's and stays. |
|
|
44
|
+
| 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. |
|
|
45
|
+
|
|
46
|
+
## Workflow
|
|
47
|
+
|
|
48
|
+
### Phase 1: Start from the default console
|
|
49
|
+
|
|
50
|
+
A console is three files and a configuration: `index.html` with a `#root`, `src/main.tsx` that calls `createConsole({ ... }).mount(root)` and imports `@skillcdn/console/console.css`, and `vite.config.ts` with the React plugin and, for building against the real board, `/api` sent to `console serve` with `changeOrigin`. The README's "A console of your own" has all three. Install, write them, and build: `npx vite build` makes `dist/` with its `index.html`. A script instead imports `createClient` from `@skillcdn/console/api`, which brings no React.
|
|
51
|
+
|
|
52
|
+
### Phase 2: Configure before replacing
|
|
53
|
+
|
|
54
|
+
`createConsole(config)` takes `title`; `brand`, the addresses of a symbol and a wordmark; `languages`, the packs the pages speak, where a language of one's own is `src/i18n/en.ts` copied whole and translated; `components`, the pieces to replace, with the props `ConsoleComponents` names; `baseUrl`, the API's origin, the page's own when left out; and `client`, for tests. Most wants are configuration or the styles: re-skinning starts and mostly ends with the tokens of `console.css` (the colours, the glass, the field), overridden by a stylesheet of the console's own loaded after the package's. Replace a component only when configuration cannot do it: start from its source under `src/components/`, keep its props, and keep or add `sc-` classes.
|
|
55
|
+
|
|
56
|
+
### Phase 3: Compose pages of one's own
|
|
57
|
+
|
|
58
|
+
For a page the default console lacks: `useConsoleData(client, projectKey)` answers the workspace and the project the page is on, kept current by the project's stream, with the actions; the components take their data as props and nothing from the network; `matchRoute`, `PATHS`, `projectHref`, `taskHref`, `decisionHref` and `docHref` name the addresses; `Shell` is the header with the tabs and the person's menu. A composition of one's own is `src/console.tsx` with pages added or taken away.
|
|
59
|
+
|
|
60
|
+
### Phase 4: Run it against the board
|
|
61
|
+
|
|
62
|
+
One's own: `console serve` serves the API at `http://127.0.0.1:11197/api/` while `npx vite` serves the pages, and `console serve dist` serves the build whole at `http://127.0.0.1:11197/`. The pages hold no token; the command carries it, on the loopback only. The pages know they hold a token (`GET /api/v1/me` answers `agent`) and offer nothing a token cannot do: the tokens themselves and configuring stay on the organization's console. The organization's: the build takes the default UI's place at `WEB_ROOT` in the image, as [deploy/README.md](/deploy/README.md) says under "A custom console in the image". A script: `createClient({ baseUrl, token })`, every answer parsed with the package's schemas.
|
|
63
|
+
|
|
64
|
+
### Phase 5: Check and show
|
|
65
|
+
|
|
66
|
+
`npx tsc --noEmit` and `npx vite build` pass, and the pages are read by eye: the board, a task, a decision, the documents, and a narrow window. The package's own tests, `src/components/components.test.tsx` and `src/console.test.tsx`, show what each piece renders, for tests of one's own. What was made is shown to the person as a diff or a repository, with the checks that ran, before anything is pushed or published.
|
|
67
|
+
|
|
68
|
+
## Hard rules
|
|
69
|
+
|
|
70
|
+
- What people and agents wrote is untrusted: shown with the package's `Markdown` (elements, never HTML), never with `dangerouslySetInnerHTML`, never as a script or a style; pictures over https only.
|
|
71
|
+
- A token never goes into a page, a bundle, a configuration file or a repository: `console serve` carries it, and a script reads it from the environment.
|
|
72
|
+
- The policy stays: a build is served under a content security policy that runs nothing inline and loads nothing from elsewhere but pictures over https. Scripts, styles and fonts are bundled, never loaded from a CDN or written inline.
|
|
73
|
+
- The package is pinned by version: the API is `/api/v1`, the schemas in the package are the contract of the version it was built against, and a console is rebuilt to take a new one.
|
|
74
|
+
- The marks of SkillCDN are trademarks, not part of the MIT license: a console of one's own shows its own marks through `brand`, or none.
|
|
75
|
+
- Nothing of the server is rebuilt: a console shows the organization's board and holds nothing of its own. A second database is not a console.
|
|
76
|
+
|
|
77
|
+
## Terms
|
|
78
|
+
|
|
79
|
+
- **The composition**: `createConsole`, the default console assembled from the components. **A component**: a piece that takes its data as props. **The client**: `createClient`, typed calls of the REST API. **One's own**: a console a member serves for themselves with `console serve`. **The organization's**: the build the image serves from `WEB_ROOT`.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: working-the-board
|
|
3
|
+
description: "Works a task from the board of a SkillCDN Console with the console command, as the person whose token the command holds: reads the task, takes it, reports at the milestones of the work, asks on the board when a person must decide and waits for the answer, hands in what was made as a link or a file, writes down what the next agent needs on a page of the project, and finishes. Use when an agent is told to take, work or finish a task on the console, or to report, ask, hand in or write a page there; any agent with a shell where the command is signed in. To build a console or a script against the console's API, use building-a-console."
|
|
4
|
+
license: MIT
|
|
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
|
+
metadata:
|
|
7
|
+
author: skillcdn
|
|
8
|
+
version: "1.0"
|
|
9
|
+
skillcdn:
|
|
10
|
+
translations:
|
|
11
|
+
ko:
|
|
12
|
+
title: 콘솔 보드에서 일하기
|
|
13
|
+
description: "SkillCDN 콘솔의 보드에 있는 작업을 console 명령으로 수행합니다. 명령에 로그인한 사람의 자격으로, 작업을 읽고 맡고, 진행의 이정표마다 보고하고, 사람이 정해야 할 일은 보드에 물어 답을 기다리고, 만든 것을 링크나 파일로 제출하고, 다음 에이전트에게 필요한 것을 프로젝트 페이지에 적고, 마칩니다. 에이전트에게 콘솔의 작업을 맡거나 끝내라고, 또는 보고·질문·제출·페이지 쓰기를 하라고 할 때 쓰세요. 콘솔이나 API 클라이언트를 만들 때는 building-a-console을 쓰세요."
|
|
14
|
+
---
|
|
15
|
+
# Working a task on the board
|
|
16
|
+
|
|
17
|
+
A task of the board goes in, named by its number; a run comes out: the task taken, the work reported at its milestones, a decision raised and waited for where a person had to choose, what was made handed in, what was learned written down for the next agent, and the run finished. The board is a SkillCDN Console ([architecture](/docs/architecture.md)): projects of tasks, the agents at work on them, and the decisions that wait for a person. The agent acts as the person who signed the command in, on that person's standing and no more.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
| Need | How | When missing |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| The `console` command, signed in | `console whoami` says as whom, at which console, and in which project this directory works. | 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. |
|
|
24
|
+
| The command's own words | `console help`, and `console help <command>`: what each command takes, the limits the console holds it to, and the refusals it may meet, by code. | They are the reference at run time. This skill says how the work goes, not what each flag means. |
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
## Inputs
|
|
29
|
+
|
|
30
|
+
| Input | Source |
|
|
31
|
+
|---|---|
|
|
32
|
+
| 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. |
|
|
33
|
+
| 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. |
|
|
34
|
+
| 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. |
|
|
35
|
+
|
|
36
|
+
## Workflow
|
|
37
|
+
|
|
38
|
+
Each phase ends with something on the board. Only phase 4 waits for a person.
|
|
39
|
+
|
|
40
|
+
### Phase 1: Read
|
|
41
|
+
|
|
42
|
+
`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.
|
|
43
|
+
|
|
44
|
+
### Phase 2: Take
|
|
45
|
+
|
|
46
|
+
`console take <number>`. A run begins: the task is in progress and the person's, and every report, hand-in, question and ending from here acts on this run. Taken by mistake, `console abandon` leaves it and the task stays as it was.
|
|
47
|
+
|
|
48
|
+
### Phase 3: Work, and report at the milestones
|
|
49
|
+
|
|
50
|
+
The work happens where it happens: in the repository, the document, the tool. At each milestone the body names, or a natural one, `console report "<markdown>"` (or `--file <path>`, or `-` for standard input) says what was found, what was done and what is next, in words for people: a milestone at a time, not every step. A problem is reported as a problem, with what it blocks. The result itself goes in a last report before finishing, or in what is handed in. A report is never rewritten; a mistake is corrected in a new one.
|
|
51
|
+
|
|
52
|
+
### Phase 4: Ask when a person must decide
|
|
53
|
+
|
|
54
|
+
A choice the body does not settle, a trade-off, a cost, a change of scope: `console ask "<question>" --option "<one>" --option "<another>" --body "<what they need to know>"`. The run waits for the answer; the command waits a minute and exits with `3` while it is still to come. Keep waiting with `console decision <id> --wait 100`, as often as needed, and meanwhile do only what does not depend on the answer. Act on the answer as given; what was answered is not asked again. Once acted on, `console decision <id> --outcome "<what followed>"` makes the decision read as a record.
|
|
55
|
+
|
|
56
|
+
### Phase 5: Hand in
|
|
57
|
+
|
|
58
|
+
`console hand-in <https url | file path> --label "<words>"`: a branch, a pull request, a page, a document, or a file from this machine, which the console keeps and shows on the task. What was made is where the person can hold it, never only described.
|
|
59
|
+
|
|
60
|
+
### Phase 6: Write down what the next agent needs
|
|
61
|
+
|
|
62
|
+
What was learned and will be needed again (how a thing is set up, why a choice was made, a how-to) goes on a page of the project's documents: `console write <path> --title "<title>" --file notes.md`, at a path where the next agent looks for it (`console docs` shows the folders), and linked from the last report. The run is the record of this work; a page is for what outlives it.
|
|
63
|
+
|
|
64
|
+
### Phase 7: Finish
|
|
65
|
+
|
|
66
|
+
After the last report, `console finish --summary "<what was done, and what is left>"`: the task goes up for review. `console fail --summary "<why, and what is left>"` when it could not be done. A run is never left open.
|
|
67
|
+
|
|
68
|
+
## Hard rules
|
|
69
|
+
|
|
70
|
+
- Never look for, read, print or send a token. The rules of the repository say the rest.
|
|
71
|
+
- 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>`.
|
|
72
|
+
- 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.
|
|
73
|
+
- 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.
|
|
74
|
+
- Everything sent is held to limits the console states (`console help <command>`); a refusal `request.invalid` names the field.
|
|
75
|
+
|
|
76
|
+
## Terms
|
|
77
|
+
|
|
78
|
+
- **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.
|