@skillcdn/console 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +50 -2
  2. package/SKILLCDN.md +35 -0
  3. package/dist/cli/cli.d.ts +3 -0
  4. package/dist/cli/cli.d.ts.map +1 -1
  5. package/dist/cli/cli.js +61 -0
  6. package/dist/cli/cli.js.map +1 -1
  7. package/dist/cli/help.d.ts.map +1 -1
  8. package/dist/cli/help.js +5 -0
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/main.js +2 -0
  11. package/dist/cli/main.js.map +1 -1
  12. package/dist/cli/serve.d.ts +37 -0
  13. package/dist/cli/serve.d.ts.map +1 -0
  14. package/dist/cli/serve.js +218 -0
  15. package/dist/cli/serve.js.map +1 -0
  16. package/dist/console.d.ts +6 -0
  17. package/dist/console.d.ts.map +1 -1
  18. package/dist/console.js +78 -13
  19. package/dist/console.js.map +1 -1
  20. package/dist/data.d.ts.map +1 -1
  21. package/dist/data.js +5 -1
  22. package/dist/data.js.map +1 -1
  23. package/dist/i18n/en.d.ts +4 -0
  24. package/dist/i18n/en.d.ts.map +1 -1
  25. package/dist/i18n/en.js +4 -0
  26. package/dist/i18n/en.js.map +1 -1
  27. package/dist/i18n/ko.d.ts.map +1 -1
  28. package/dist/i18n/ko.js +4 -0
  29. package/dist/i18n/ko.js.map +1 -1
  30. package/dist/schemas.d.ts +1 -0
  31. package/dist/schemas.d.ts.map +1 -1
  32. package/dist/schemas.js +5 -0
  33. package/dist/schemas.js.map +1 -1
  34. package/dist/web.d.ts +16 -0
  35. package/dist/web.d.ts.map +1 -0
  36. package/dist/web.js +149 -0
  37. package/dist/web.js.map +1 -0
  38. package/docs/specs/cli.md +92 -0
  39. package/docs/specs/rest.md +221 -0
  40. package/package.json +14 -3
  41. package/skills/building-a-console/SKILL.md +102 -0
  42. package/skills/building-a-console/references/the-default-console.md +140 -0
  43. package/skills/deploying-a-console/SKILL.md +130 -0
  44. package/skills/deploying-a-console/assets/deployer-policy.json +118 -0
  45. package/skills/deploying-a-console/assets/secrets.example.json +11 -0
  46. package/skills/deploying-a-console/references/access.md +78 -0
  47. package/skills/deploying-a-console/references/resources.md +516 -0
  48. package/skills/deploying-a-console/references/sizing.md +90 -0
  49. package/skills/deploying-a-console/scripts/import-aws-key.mjs +35 -0
  50. package/skills/deploying-a-console/scripts/store-secrets.mjs +114 -0
  51. package/skills/working-the-board/SKILL.md +86 -0
  52. package/src/api.ts +235 -0
  53. package/src/cli/cli.ts +1114 -0
  54. package/src/cli/credentials.ts +68 -0
  55. package/src/cli/directory.ts +59 -0
  56. package/src/cli/format.ts +284 -0
  57. package/src/cli/help.ts +326 -0
  58. package/src/cli/main.ts +39 -0
  59. package/src/cli/serve.ts +276 -0
  60. package/src/cli/terminal.ts +28 -0
  61. package/src/client.ts +533 -0
  62. package/src/components/board.tsx +112 -0
  63. package/src/components/connect.tsx +126 -0
  64. package/src/components/decision-form.tsx +136 -0
  65. package/src/components/decision-list.tsx +278 -0
  66. package/src/components/documents.tsx +569 -0
  67. package/src/components/event-feed.tsx +129 -0
  68. package/src/components/markdown.tsx +72 -0
  69. package/src/components/people.tsx +59 -0
  70. package/src/components/projects.tsx +364 -0
  71. package/src/components/runs.tsx +175 -0
  72. package/src/components/shell.tsx +284 -0
  73. package/src/components/sign-in.tsx +61 -0
  74. package/src/components/skills.tsx +78 -0
  75. package/src/components/task-form.tsx +229 -0
  76. package/src/components/task-view.tsx +272 -0
  77. package/src/components/tokens.tsx +223 -0
  78. package/src/components/ui.tsx +219 -0
  79. package/src/connect-page.tsx +148 -0
  80. package/src/connect.ts +27 -0
  81. package/src/console.tsx +1166 -0
  82. package/src/data.ts +493 -0
  83. package/src/docs-page.tsx +334 -0
  84. package/src/documents.ts +76 -0
  85. package/src/i18n/en.ts +537 -0
  86. package/src/i18n/index.ts +76 -0
  87. package/src/i18n/ko.ts +511 -0
  88. package/src/index.ts +142 -0
  89. package/src/limits.ts +61 -0
  90. package/src/router.ts +295 -0
  91. package/src/routes.ts +106 -0
  92. package/src/schemas.ts +776 -0
  93. package/src/styles/console.css +2432 -0
  94. package/src/text.ts +33 -0
  95. package/src/use-action.ts +37 -0
  96. package/src/vocabulary.ts +109 -0
  97. 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,221 @@
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
+ | 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. |
56
+
57
+ ## The workspace
58
+
59
+ ### `GET /api/v1/me`
60
+
61
+ `{ "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.
62
+
63
+ ### `GET /api/v1/people`
64
+
65
+ `{ "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.
66
+
67
+ ### `PATCH /api/v1/people/<id>`
68
+
69
+ 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.
70
+
71
+ ### `GET /api/v1/projects`
72
+
73
+ `{ "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.
74
+
75
+ ### `POST /api/v1/projects`
76
+
77
+ 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`.
78
+
79
+ ### `GET /api/v1/events?after=&limit=`
80
+
81
+ 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.
82
+
83
+ ### `PATCH /api/v1/me`
84
+
85
+ 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.
86
+
87
+ ### `GET /api/v1/tokens` and `POST /api/v1/tokens`
88
+
89
+ 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.
90
+
91
+ ### `DELETE /api/v1/tokens/<id>`
92
+
93
+ 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`.
94
+
95
+ ### `GET /api/v1/people/<id>/tokens` and `DELETE /api/v1/people/<id>/tokens/<token id>`
96
+
97
+ 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.
98
+
99
+ ### Connecting an agent
100
+
101
+ `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)).
102
+
103
+ `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`).
104
+
105
+ `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`.
106
+
107
+ `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.
108
+
109
+ ## A project
110
+
111
+ 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.
112
+
113
+ ### `GET /api/v1/projects/<key>` and `PATCH /api/v1/projects/<key>`
114
+
115
+ 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.
116
+
117
+ ### `GET /api/v1/projects/<key>/members` and `POST /api/v1/projects/<key>/members`
118
+
119
+ `{ "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`.
120
+
121
+ ### `PATCH /api/v1/projects/<key>/members/<person id>` and `DELETE .../members/<person id>`
122
+
123
+ 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.
124
+
125
+ ### `GET /api/v1/projects/<key>/tasks?state=`
126
+
127
+ `{ "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`.
128
+
129
+ ### `POST /api/v1/projects/<key>/tasks`
130
+
131
+ 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.
132
+
133
+ ### `GET /api/v1/projects/<key>/tasks/<id or number>` and `PATCH .../tasks/<id>`
134
+
135
+ 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.
136
+
137
+ ### `GET /api/v1/projects/<key>/decisions?open=&task=`
138
+
139
+ `{ "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.
140
+
141
+ ### `POST /api/v1/projects/<key>/decisions`
142
+
143
+ 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.
144
+
145
+ ### `GET .../decisions/<id>?wait=` and `POST .../decisions/<id>/answer`
146
+
147
+ 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.
148
+
149
+ 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.
150
+
151
+ ### `PATCH /api/v1/projects/<key>/decisions/<id>`
152
+
153
+ 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.
154
+
155
+ ### `GET /api/v1/projects/<key>/runs?task=&open=&mine=` and `GET .../runs/<id>`
156
+
157
+ `{ "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`.
158
+
159
+ ### `POST /api/v1/projects/<key>/runs`
160
+
161
+ 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.
162
+
163
+ ### `POST .../runs/<id>/reports` and `POST .../runs/<id>/artifacts`
164
+
165
+ 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).
166
+
167
+ ### `POST .../runs/<id>/files` and `GET /api/v1/projects/<key>/files/<id>`
168
+
169
+ 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.
170
+
171
+ 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`.
172
+
173
+ ### `POST .../runs/<id>/end`
174
+
175
+ 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`.
176
+
177
+ ### `GET /api/v1/projects/<key>/skills`
178
+
179
+ 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.
180
+
181
+ ### The documents
182
+
183
+ 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.
184
+
185
+ #### `GET /api/v1/projects/<key>/docs?folder=&q=&archived=`
186
+
187
+ `{ "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`.
188
+
189
+ #### `GET /api/v1/projects/<key>/docs/<path>`
190
+
191
+ 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.
192
+
193
+ #### `PUT /api/v1/projects/<key>/docs/<path>`
194
+
195
+ 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`.
196
+
197
+ #### `POST .../docs/<path>/archive` and `POST .../docs/<path>/restore`
198
+
199
+ 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.
200
+
201
+ #### `GET .../docs/<path>/versions` and `GET .../docs/<path>/versions/<number>`
202
+
203
+ `{ "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`.
204
+
205
+ #### `POST .../docs/<path>/files` and `GET .../docs/<path>/files/<id>`
206
+
207
+ 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.
208
+
209
+ ### `GET /api/v1/projects/<key>/events?after=&limit=&task=&run=&decision=&document=`
210
+
211
+ `{ "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`.
212
+
213
+ ### `GET /api/v1/projects/<key>/events/stream?after=`
214
+
215
+ 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.
216
+
217
+ 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.
218
+
219
+ ## Signing in
220
+
221
+ 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.6",
3
+ "version": "0.1.8",
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,102 @@
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; to deploy a console for an organization, use deploying-a-console."
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 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
+ metadata:
7
+ author: skillcdn
8
+ version: "1.1"
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를, 조직의 콘솔을 배포할 때는 deploying-a-console을 쓰세요."
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, for the organization, built into the image: as the source in `console-ui/` of its deployment repository, or as a build beside the Dockerfile that `deploy/README.md` shows; 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
+ ## How the person is involved
20
+
21
+ - **Questions:** one message at the start, the ones under "Inputs" that the request leaves open, each for the half it leaves open. Everything else is derived and shown with the plan.
22
+ - **Checkpoints:** the plan (the kind, where the build goes, what is configured and what is replaced, the colours and the marks as they will be used), before a file of the console is written; the look, with the pages in front of them, before more pages are built on it. One short message each, with the recommendation; "OK", in the person's language, continues.
23
+ - **Go-ahead:** when the person says to go ahead alone, the look is reported instead of approved; the plan holds in every mode. Nothing is pushed or published here.
24
+ - **Changes mid-run:** applied from that point; a change after the build rewrites the parts it touches, not the whole console. A phase with nothing to do for the request is skipped, and the line says so.
25
+
26
+ ## Requirements
27
+
28
+ | Need | How | When missing |
29
+ |---|---|---|
30
+ | Node.js 24 or newer, with npm or pnpm | `node --version` | Stop and say so. |
31
+ | The package | `npm install @skillcdn/console react react-dom`, for pages `npm install --save-dev vite @vitejs/plugin-react typescript`, and for Korean `npm install pretendard`, the face the default console loads | The package is on npm; nothing else is needed. |
32
+ | For looking at the pages against a board, one's own or the organization's: 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. |
33
+ | Optional: a browser the agent can drive, or a screenshot tool | Looking at the pages and their console at the look checkpoint and for the verdicts. | The person opens the local address themselves, and the layout, console and policy verdicts are reported as not run. |
34
+
35
+ ## Where to read, in this order
36
+
37
+ Nothing here restates them: they are the contract, current at the version installed.
38
+
39
+ 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 two of the four files to write.
40
+ 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. `brand` takes a `symbol`, a `wordmark`, or both.
41
+ 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). The repository's tests (`packages/console/src/components/components.test.tsx`, `src/console.test.tsx`), not shipped in the package, show what each piece renders, for tests of one's own.
42
+ 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).
43
+ 5. **The default console itself**, `apps/console/web/` in the console's repository: the page, the script, the build and the compiler's settings, with the brand, the fonts and the icons wired in. Its four files are quoted in [the-default-console.md](references/the-default-console.md), kept identical to them by the repository's check, for an agent that has the skill and not the repository; a console of one's own copies their shape.
44
+ 6. **The architecture**: [docs/architecture.md](/docs/architecture.md) for how the parts fit, and the [decisions](/docs/adr/README.md), served through the repository's connection and not in the package; ADR-0014 says how a person's own console reaches the organization's.
45
+
46
+ ## Inputs
47
+
48
+ | Input | Source |
49
+ |---|---|
50
+ | Which kind | Asked when the request does not say: "Is this console for you alone, served from your machine with your token; for the whole organization, in place of the default pages in the image; or a script against the API?" A member usually wants their own. |
51
+ | Where the organization's build goes | Asked for the organization's: "How is the console run: with the deployment repository that deploying-a-console made, with an image you build yourselves, or from a directory mounted into it?" The answer places the result: the source in `console-ui/` of that repository, which its workflow builds on every push; a repository of its own whose build goes beside the Dockerfile of [deploy/README.md](/deploy/README.md) "A custom console in the image"; or the build in the directory. |
52
+ | What it must show or do | From the request: a page, a component, a language, a brand, a column, a filter, a report. Asked for its missing half: "What should it show or do that the default console does not?" Everything else is the default console's and stays. |
53
+ | The marks | 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 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. 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: the board's name alone and no marks, which is the honest console. |
54
+ | The colour | 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. |
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`. |
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
+
59
+ ## Workflow
60
+
61
+ Each phase produces something the person can look at and ends with one line to them.
62
+
63
+ ### Phase 1: Start from the default console
64
+
65
+ Produces the **plan** and the first build. Install first, in the new folder, so that the README, the types and the sources are at hand for the plan: the install writes only `package.json`, the lockfile and `node_modules`, and the plan is shown before any file of the console is written. A console is four files and a configuration: `index.html` with a `#root`, `src/main.tsx` that calls `createConsole({ ... }).mount(root)` and imports `@skillcdn/console/console.css`, `vite.config.ts` with the React plugin and, for building against the real board, `/api` sent to `console serve` with `changeOrigin`, and `tsconfig.json` with `jsx`, `moduleResolution: "Bundler"` and the `vite/client` types that make an imported picture a module. The README's "A console of your own" shows two of them, the-default-console.md all four with the marks, the icons and a face wired in. Write the plan (the kind, where the build goes, the files, what is configured, what is replaced, the colours and the marks) and stop for the checkpoint; then write the files and build: `npx vite build` makes `dist/` with its `index.html`. A script instead imports `createClient` from `@skillcdn/console/api`, which brings no React.
66
+
67
+ ### Phase 2: Configure before replacing
68
+
69
+ Produces the **configuration**. `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. A console with marks of its own imports their files in `main.tsx`, so that the build takes them as assets (a small picture is inlined as a `data:` address, which the policy allows), and passes their addresses through `brand`; sets `index.html`'s `<title>`, its `lang` and its icons, the mark as `favicon.svg` linked directly, with an `.ico` and a touch icon when the person has them (the default console links three from its brand package through a small build plugin); and loads the faces its languages need. 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, with the mapping of the plan. 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. With the look in place, show the pages and stop for the look checkpoint.
70
+
71
+ ### Phase 3: Compose pages of one's own
72
+
73
+ Produces the **pages**. 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. A request without a page of its own skips this phase.
74
+
75
+ ### Phase 4: Run it against the board
76
+
77
+ Produces the **running console**. `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 console is looked at the same way, on the builder's token: it shows every page a token may see, and the pages a token may not (the agents, the settings) are the package's own and unchanged, so the look checkpoint covers everything the console changed. Where the organization's build then goes is the plan's answer: `console-ui/` of the deployment repository, whose workflow builds it into the image on every push, handed over to deploying-a-console, the skill beside this one at the same address (in the package under `node_modules/@skillcdn/console/skills/deploying-a-console/`), with the source and the repository; the build beside the Dockerfile of [deploy/README.md](/deploy/README.md) "A custom console in the image", for an image the organization builds itself; or the mounted directory. A script: `createClient({ baseUrl, token })`, every answer parsed with the package's schemas.
78
+
79
+ ### Phase 5: Check and show
80
+
81
+ Produces the **checked build** and the delivery. `npx tsc --noEmit` and `npx vite build` pass; the pages are looked at, with a browser the agent can drive (the board, a task, a decision, the documents, a narrow window, and the browser's console), else by the person at the local address, with the layout, console and policy verdicts reported as not run; `dist/` holds no token and loads no script, style or font from elsewhere. `git init` and a commit of what was made; the CI job is the organization's own: with the reference deployment, the deployment repository's workflow builds `console-ui/`, else a job that runs the two checks and the build. What was made is shown to the person as a diff or a repository, with the checks that ran and what was not run, before anything is pushed or published.
82
+
83
+ ### Verdicts
84
+
85
+ | Result | Verdict |
86
+ |---|---|
87
+ | `tsc --noEmit` and `vite build` pass; the build, served by `console serve dist` or the image, shows the board, a task, a decision and the documents, with nothing in the browser's console where a browser could look; every word the pages show is in every pack; a narrow window scrolls no sideways | Accepted |
88
+ | A layout, console or policy verdict could not run without a browser; a pack of one's own is still a copy of English, marked so; the `.ico` and the touch icon wait for files the person does not have yet | Accepted, with the gap named in the delivery |
89
+ | A token in the bundle or in a file of the repository; a script, a style or a font loaded from elsewhere; what people or agents wrote rendered as HTML; the policy refusing something the pages need | Redone |
90
+
91
+ ## Hard rules
92
+
93
+ - 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.
94
+ - 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.
95
+ - 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.
96
+ - 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.
97
+ - 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.
98
+ - 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.
99
+
100
+ ## Terms
101
+
102
+ - **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`. **The look**: the tokens, the marks, the faces and the icons a console shows itself with. **A mark**: a small picture (the symbol) or the name as lettering (the wordmark). **A face**: a typeface the pages load for a language's script.