@skillcdn/console 0.1.5 → 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.
Files changed (89) hide show
  1. package/README.md +50 -2
  2. package/SKILLCDN.md +29 -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.css +33 -10
  17. package/dist/console.d.ts +6 -0
  18. package/dist/console.d.ts.map +1 -1
  19. package/dist/console.js +78 -13
  20. package/dist/console.js.map +1 -1
  21. package/dist/data.d.ts.map +1 -1
  22. package/dist/data.js +5 -1
  23. package/dist/data.js.map +1 -1
  24. package/dist/i18n/en.d.ts +4 -0
  25. package/dist/i18n/en.d.ts.map +1 -1
  26. package/dist/i18n/en.js +4 -0
  27. package/dist/i18n/en.js.map +1 -1
  28. package/dist/i18n/ko.d.ts.map +1 -1
  29. package/dist/i18n/ko.js +4 -0
  30. package/dist/i18n/ko.js.map +1 -1
  31. package/dist/schemas.d.ts +1 -0
  32. package/dist/schemas.d.ts.map +1 -1
  33. package/dist/schemas.js +5 -0
  34. package/dist/schemas.js.map +1 -1
  35. package/dist/web.d.ts +16 -0
  36. package/dist/web.d.ts.map +1 -0
  37. package/dist/web.js +149 -0
  38. package/dist/web.js.map +1 -0
  39. package/docs/specs/cli.md +92 -0
  40. package/docs/specs/rest.md +220 -0
  41. package/package.json +14 -3
  42. package/skills/building-a-console/SKILL.md +79 -0
  43. package/skills/working-the-board/SKILL.md +78 -0
  44. package/src/api.ts +235 -0
  45. package/src/cli/cli.ts +1114 -0
  46. package/src/cli/credentials.ts +68 -0
  47. package/src/cli/directory.ts +59 -0
  48. package/src/cli/format.ts +284 -0
  49. package/src/cli/help.ts +326 -0
  50. package/src/cli/main.ts +39 -0
  51. package/src/cli/serve.ts +276 -0
  52. package/src/cli/terminal.ts +28 -0
  53. package/src/client.ts +533 -0
  54. package/src/components/board.tsx +112 -0
  55. package/src/components/connect.tsx +126 -0
  56. package/src/components/decision-form.tsx +136 -0
  57. package/src/components/decision-list.tsx +278 -0
  58. package/src/components/documents.tsx +569 -0
  59. package/src/components/event-feed.tsx +129 -0
  60. package/src/components/markdown.tsx +72 -0
  61. package/src/components/people.tsx +59 -0
  62. package/src/components/projects.tsx +364 -0
  63. package/src/components/runs.tsx +175 -0
  64. package/src/components/shell.tsx +284 -0
  65. package/src/components/sign-in.tsx +61 -0
  66. package/src/components/skills.tsx +78 -0
  67. package/src/components/task-form.tsx +229 -0
  68. package/src/components/task-view.tsx +272 -0
  69. package/src/components/tokens.tsx +223 -0
  70. package/src/components/ui.tsx +219 -0
  71. package/src/connect-page.tsx +148 -0
  72. package/src/connect.ts +27 -0
  73. package/src/console.tsx +1166 -0
  74. package/src/data.ts +493 -0
  75. package/src/docs-page.tsx +334 -0
  76. package/src/documents.ts +76 -0
  77. package/src/i18n/en.ts +537 -0
  78. package/src/i18n/index.ts +76 -0
  79. package/src/i18n/ko.ts +511 -0
  80. package/src/index.ts +142 -0
  81. package/src/limits.ts +61 -0
  82. package/src/router.ts +295 -0
  83. package/src/routes.ts +106 -0
  84. package/src/schemas.ts +776 -0
  85. package/src/styles/console.css +2432 -0
  86. package/src/text.ts +33 -0
  87. package/src/use-action.ts +37 -0
  88. package/src/vocabulary.ts +109 -0
  89. package/src/web.ts +170 -0
package/src/schemas.ts ADDED
@@ -0,0 +1,776 @@
1
+ // The mini build of zod: these schemas also run in the browser, where the full build would be
2
+ // most of what a page downloads. The server validates what it is sent with the input schemas
3
+ // and builds the output shapes; the pages and a custom console parse answers with the rest.
4
+ // Absent values are `null` on the wire, never missing keys. Changes within a version are additive.
5
+ import * as z from "zod/mini";
6
+ import { isDocumentPath } from "./documents.js";
7
+ import {
8
+ MAX_AGENT_LENGTH,
9
+ MAX_BODY_LENGTH,
10
+ MAX_DOCUMENT_LENGTH,
11
+ MAX_DOCUMENT_PATH_LENGTH,
12
+ MAX_FILE_NAME_LENGTH,
13
+ MAX_LANGUAGE_TAG_LENGTH,
14
+ MAX_LINK_LABEL_LENGTH,
15
+ MAX_LINKS,
16
+ MAX_NOTE_LENGTH,
17
+ MAX_OPTION_LABEL_LENGTH,
18
+ MAX_OPTIONS,
19
+ MAX_PROJECT_DESCRIPTION_LENGTH,
20
+ MAX_PROJECT_KEY_LENGTH,
21
+ MAX_PROJECT_NAME_LENGTH,
22
+ MAX_QUESTION_LENGTH,
23
+ MAX_SECRET_LENGTH,
24
+ MAX_SKILLS_ADDRESS_LENGTH,
25
+ MAX_SUMMARY_LENGTH,
26
+ MAX_TITLE_LENGTH,
27
+ MAX_TOKEN_DAYS,
28
+ MAX_TOKEN_NAME_LENGTH,
29
+ MAX_URL_LENGTH,
30
+ MIN_OPTIONS,
31
+ } from "./limits.js";
32
+ import { hasForbiddenCodePoint } from "./text.js";
33
+ import {
34
+ ARTIFACT_KINDS,
35
+ EVENT_KINDS,
36
+ LINK_SOURCES,
37
+ PERSON_ROLES,
38
+ PROJECT_ROLES,
39
+ PROJECT_VISIBILITIES,
40
+ PROVIDER_KEYS,
41
+ RUN_ENDINGS,
42
+ RUN_STATUSES,
43
+ SKILLS_STATUSES,
44
+ TASK_PRIORITIES,
45
+ TASK_STATES,
46
+ } from "./vocabulary.js";
47
+
48
+ /** A line of text a person wrote: trimmed, bounded, and free of characters that hide. */
49
+ const line = (maxLength: number) =>
50
+ z.string().check(
51
+ z.trim(),
52
+ z.minLength(1),
53
+ z.maxLength(maxLength),
54
+ z.refine((value) => !hasForbiddenCodePoint(value), "must not contain control characters"),
55
+ );
56
+
57
+ /** A text of a few lines: bounded, with its line breaks, and otherwise the same rule. */
58
+ const paragraphs = (maxLength: number) =>
59
+ z.string().check(
60
+ z.maxLength(maxLength),
61
+ z.refine((value) => !hasForbiddenCodePoint(value, true), "must not contain control characters"),
62
+ );
63
+
64
+ /** A body in Markdown. */
65
+ const body = paragraphs(MAX_BODY_LENGTH);
66
+
67
+ /** A link a person adds to a task: the web, over https, and nothing that runs. */
68
+ const url = z.string().check(z.trim(), z.maxLength(MAX_URL_LENGTH), z.url({ protocol: /^https$/ }));
69
+
70
+ const uuid = z.uuid();
71
+ /** An ISO 8601 instant, as the server writes it. */
72
+ const instant = z.iso.datetime();
73
+ const count = z.int().check(z.nonnegative());
74
+
75
+ /** A person of the workspace, as the pages show them. Nothing here is a secret. */
76
+ export const restPersonSchema = z.object({
77
+ id: uuid,
78
+ /** What the account is called at its provider: a login at the git host, an address at Google. */
79
+ login: z.string(),
80
+ /** The name the account goes by at the host, or `null`. */
81
+ name: z.nullable(z.string()),
82
+ /** The account's picture as the host serves it: an https URL loaded by the browser, or `null`. */
83
+ avatar: z.nullable(z.string()),
84
+ /** An administrator configures the board; a member works on it. */
85
+ role: z.enum(PERSON_ROLES),
86
+ });
87
+ export type RestPerson = z.infer<typeof restPersonSchema>;
88
+
89
+ /** An identity provider people may sign in through, as the pages offer it. */
90
+ export const restProviderSchema = z.object({
91
+ key: z.enum(PROVIDER_KEYS),
92
+ /** What people read on the button. */
93
+ label: z.string(),
94
+ });
95
+ export type RestProvider = z.infer<typeof restProviderSchema>;
96
+
97
+ /** A language tag as a browser sends one (`en`, `ko-KR`): what the pages speak, by the person's choice. */
98
+ const languageTag = z
99
+ .string()
100
+ .check(
101
+ z.maxLength(MAX_LANGUAGE_TAG_LENGTH),
102
+ z.regex(/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/, "must be a language tag"),
103
+ );
104
+
105
+ /** What `PATCH /api/v1/me` is sent: the language the person chose, or `null` to choose none. */
106
+ export const restMePatchSchema = z.object({ language: z.nullable(languageTag) });
107
+ export type RestMePatch = z.infer<typeof restMePatchSchema>;
108
+
109
+ /**
110
+ * `GET /api/v1/me`: the workspace, who the session cookie says is signed in (or `null`), the
111
+ * language they chose (or `null`), and the identity providers people sign in through, none
112
+ * where nobody can.
113
+ */
114
+ export const restMeSchema = z.object({
115
+ workspace: z.object({
116
+ name: z.string(),
117
+ /** The most days a token may be good for here, or `null` when a person chooses. */
118
+ tokenDaysAtMost: z.nullable(z.int().check(z.positive())),
119
+ }),
120
+ person: z.nullable(restPersonSchema),
121
+ /**
122
+ * What the token asking is called by its person, when a token asks: how a console of a
123
+ * person's own knows it holds one. `null` on a session, and for nobody.
124
+ */
125
+ agent: z.nullable(z.string()),
126
+ /** The language the person chose for the pages (ADR-0012), or `null` for the browser's. */
127
+ language: z.nullable(languageTag),
128
+ signIn: z.array(restProviderSchema),
129
+ });
130
+ export type RestMe = z.infer<typeof restMeSchema>;
131
+
132
+ /** `GET /api/v1/people`: everyone who has signed in, by login. */
133
+ export const restPeopleSchema = z.object({ items: z.array(restPersonSchema) });
134
+ export type RestPeople = z.infer<typeof restPeopleSchema>;
135
+
136
+ /** What `PATCH /api/v1/people/<id>` is sent, by an administrator: what the person is to be. */
137
+ export const restPersonPatchSchema = z.object({ role: z.enum(PERSON_ROLES) });
138
+ export type RestPersonPatch = z.infer<typeof restPersonPatchSchema>;
139
+
140
+ /** The words under `/projects/` that are the pages' own, which no project may be called. */
141
+ export const RESERVED_PROJECT_KEYS: readonly string[] = ["new"];
142
+
143
+ /**
144
+ * A project's key: what its addresses and the command say. Lowercase letters, digits and
145
+ * hyphens, beginning and ending with a letter or a digit, not a word the pages reserve;
146
+ * immutable once the project is made.
147
+ */
148
+ const projectKey = z.string().check(
149
+ z.maxLength(MAX_PROJECT_KEY_LENGTH),
150
+ z.regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, "must be lowercase letters, digits and hyphens"),
151
+ z.refine((key) => !RESERVED_PROJECT_KEYS.includes(key), "is a word the pages reserve"),
152
+ );
153
+
154
+ /** The address of a project's skills, as the standard spells one; the console checks it as one. */
155
+ const skillsAddress = line(MAX_SKILLS_ADDRESS_LENGTH);
156
+
157
+ /** A project of the workspace, as the asker sees it: with what they are in it. */
158
+ export const restProjectSchema = z.object({
159
+ id: uuid,
160
+ key: projectKey,
161
+ name: z.string(),
162
+ description: z.string(),
163
+ /** Who is a member: every member of the workspace, or only those listed. */
164
+ visibility: z.enum(PROJECT_VISIBILITIES),
165
+ /** The address of the project's skills at the SkillCDN deployment, canonical, or `null` for the organization's. */
166
+ skillsAddress: z.nullable(z.string()),
167
+ /** What the asker is in it: an owner configures the project, a member works on it. */
168
+ role: z.enum(PROJECT_ROLES),
169
+ /** How many decisions in it wait for a person. */
170
+ openDecisions: count,
171
+ /** How many agents are at work in it, or waiting. */
172
+ openRuns: count,
173
+ createdAt: instant,
174
+ updatedAt: instant,
175
+ });
176
+ export type RestProject = z.infer<typeof restProjectSchema>;
177
+
178
+ /** `GET /api/v1/projects`: the projects the asker may see, by name. */
179
+ export const restProjectsSchema = z.object({ items: z.array(restProjectSchema) });
180
+ export type RestProjects = z.infer<typeof restProjectsSchema>;
181
+
182
+ /** What `POST /api/v1/projects` is sent: the key and the name; the rest has a default. */
183
+ export const restProjectInputSchema = z.object({
184
+ key: projectKey,
185
+ name: line(MAX_PROJECT_NAME_LENGTH),
186
+ description: z.optional(paragraphs(MAX_PROJECT_DESCRIPTION_LENGTH)),
187
+ /** Left out: `private`. */
188
+ visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
189
+ /** Left out or `null`: the organization's skills. */
190
+ skillsAddress: z.optional(z.nullable(skillsAddress)),
191
+ });
192
+ export type RestProjectInput = z.infer<typeof restProjectInputSchema>;
193
+
194
+ /** What `PATCH /api/v1/projects/<key>` is sent: only what changes; the key does not. */
195
+ export const restProjectPatchSchema = z.object({
196
+ name: z.optional(line(MAX_PROJECT_NAME_LENGTH)),
197
+ description: z.optional(paragraphs(MAX_PROJECT_DESCRIPTION_LENGTH)),
198
+ visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
199
+ skillsAddress: z.optional(z.nullable(skillsAddress)),
200
+ });
201
+ export type RestProjectPatch = z.infer<typeof restProjectPatchSchema>;
202
+
203
+ /** A person listed in a project, with what they are in it. */
204
+ export const restMemberSchema = z.object({
205
+ person: restPersonSchema,
206
+ role: z.enum(PROJECT_ROLES),
207
+ addedAt: instant,
208
+ });
209
+ export type RestMember = z.infer<typeof restMemberSchema>;
210
+
211
+ /** `GET /api/v1/projects/<key>/members`: those listed, by login. */
212
+ export const restMembersSchema = z.object({ items: z.array(restMemberSchema) });
213
+ export type RestMembers = z.infer<typeof restMembersSchema>;
214
+
215
+ /** What `POST /api/v1/projects/<key>/members` is sent: a person of the workspace, and what they are to be. */
216
+ export const restMemberInputSchema = z.object({
217
+ personId: uuid,
218
+ /** Left out: `member`. */
219
+ role: z.optional(z.enum(PROJECT_ROLES)),
220
+ });
221
+ export type RestMemberInput = z.infer<typeof restMemberInputSchema>;
222
+
223
+ /** What `PATCH /api/v1/projects/<key>/members/<person id>` is sent. */
224
+ export const restMemberPatchSchema = z.object({ role: z.enum(PROJECT_ROLES) });
225
+ export type RestMemberPatch = z.infer<typeof restMemberPatchSchema>;
226
+
227
+ export const restTaskLinkSchema = z.object({
228
+ url: z.string(),
229
+ label: z.nullable(z.string()),
230
+ });
231
+ export type RestTaskLink = z.infer<typeof restTaskLinkSchema>;
232
+
233
+ export const restTaskSchema = z.object({
234
+ id: uuid,
235
+ /** The task's number in its project, the one people say out loud. */
236
+ number: z.int().check(z.positive()),
237
+ title: z.string(),
238
+ /** Markdown. Shown as text or rendered to elements, never as HTML. */
239
+ body: z.string(),
240
+ state: z.enum(TASK_STATES),
241
+ priority: z.enum(TASK_PRIORITIES),
242
+ owner: restPersonSchema,
243
+ assignee: z.nullable(restPersonSchema),
244
+ /** The task this one is part of, or `null`. */
245
+ parentId: z.nullable(uuid),
246
+ links: z.array(restTaskLinkSchema),
247
+ /** How many decisions about this task wait for a person. */
248
+ openDecisions: count,
249
+ /** How many runs are at work on it, or waiting. */
250
+ openRuns: count,
251
+ createdAt: instant,
252
+ updatedAt: instant,
253
+ });
254
+ export type RestTask = z.infer<typeof restTaskSchema>;
255
+
256
+ /** `GET /api/v1/projects/<key>/tasks`: the board, newest first. */
257
+ export const restTasksSchema = z.object({ items: z.array(restTaskSchema) });
258
+ export type RestTasks = z.infer<typeof restTasksSchema>;
259
+
260
+ const taskLinkInput = z.object({
261
+ url,
262
+ label: z.optional(z.nullable(line(MAX_LINK_LABEL_LENGTH))),
263
+ });
264
+
265
+ /** What `POST /api/v1/projects/<key>/tasks` is sent. Everything but the title has a default. */
266
+ export const restTaskInputSchema = z.object({
267
+ title: line(MAX_TITLE_LENGTH),
268
+ body: z.optional(body),
269
+ state: z.optional(z.enum(TASK_STATES)),
270
+ priority: z.optional(z.enum(TASK_PRIORITIES)),
271
+ assigneeId: z.optional(z.nullable(uuid)),
272
+ parentId: z.optional(z.nullable(uuid)),
273
+ links: z.optional(z.array(taskLinkInput).check(z.maxLength(MAX_LINKS))),
274
+ });
275
+ export type RestTaskInput = z.infer<typeof restTaskInputSchema>;
276
+
277
+ /** What `PATCH /api/v1/projects/<key>/tasks/<id>` is sent: only what changes. */
278
+ export const restTaskPatchSchema = z.object({
279
+ title: z.optional(line(MAX_TITLE_LENGTH)),
280
+ body: z.optional(body),
281
+ state: z.optional(z.enum(TASK_STATES)),
282
+ priority: z.optional(z.enum(TASK_PRIORITIES)),
283
+ assigneeId: z.optional(z.nullable(uuid)),
284
+ parentId: z.optional(z.nullable(uuid)),
285
+ links: z.optional(z.array(taskLinkInput).check(z.maxLength(MAX_LINKS))),
286
+ });
287
+ export type RestTaskPatch = z.infer<typeof restTaskPatchSchema>;
288
+
289
+ export const restDecisionOptionSchema = z.object({
290
+ /** What an answer names: stable for the life of the decision. */
291
+ id: z.string(),
292
+ label: z.string(),
293
+ });
294
+ export type RestDecisionOption = z.infer<typeof restDecisionOptionSchema>;
295
+
296
+ export const restDecisionSchema = z.object({
297
+ id: uuid,
298
+ question: z.string(),
299
+ /** Markdown: what a person needs to know to answer. */
300
+ body: z.string(),
301
+ options: z.array(restDecisionOptionSchema),
302
+ /** The task the decision is about, or `null`. */
303
+ taskId: z.nullable(uuid),
304
+ /** The task's number, when there is a task, or `null`. */
305
+ taskNumber: z.nullable(z.int()),
306
+ raisedBy: restPersonSchema,
307
+ /** The run that raised it, as the agent asked, or `null` when a person did. */
308
+ run: z.nullable(z.object({ id: uuid, agent: z.string() })),
309
+ /** The answer, with who gave it and when; `null` while the decision waits. */
310
+ answer: z.nullable(
311
+ z.object({
312
+ /** The id of the chosen option. */
313
+ option: z.string(),
314
+ /** The rationale given with the answer, or `null`. */
315
+ note: z.nullable(z.string()),
316
+ by: restPersonSchema,
317
+ at: instant,
318
+ }),
319
+ ),
320
+ /** What followed the decision, in Markdown, written afterwards; `null` until someone does. */
321
+ outcome: z.nullable(z.string()),
322
+ createdAt: instant,
323
+ updatedAt: instant,
324
+ });
325
+ export type RestDecision = z.infer<typeof restDecisionSchema>;
326
+
327
+ /** `GET /api/v1/projects/<key>/decisions`: the ones that wait first, newest first within each. */
328
+ export const restDecisionsSchema = z.object({ items: z.array(restDecisionSchema) });
329
+ export type RestDecisions = z.infer<typeof restDecisionsSchema>;
330
+
331
+ /** What `POST /api/v1/projects/<key>/decisions` is sent: the question, its context, and the options. */
332
+ export const restDecisionInputSchema = z.object({
333
+ question: line(MAX_QUESTION_LENGTH),
334
+ body: z.optional(body),
335
+ options: z
336
+ .array(z.object({ label: line(MAX_OPTION_LABEL_LENGTH) }))
337
+ .check(z.minLength(MIN_OPTIONS), z.maxLength(MAX_OPTIONS)),
338
+ taskId: z.optional(z.nullable(uuid)),
339
+ /** The run that asks, when an agent does: the decision is about its task, and it waits for the answer. */
340
+ runId: z.optional(z.nullable(uuid)),
341
+ });
342
+ export type RestDecisionInput = z.infer<typeof restDecisionInputSchema>;
343
+
344
+ /** What `PATCH .../decisions/<id>` is sent: the record growing, its context or what followed; only what changes. */
345
+ export const restDecisionPatchSchema = z.object({
346
+ body: z.optional(body),
347
+ /** What followed the decision; empty clears it. */
348
+ outcome: z.optional(body),
349
+ });
350
+ export type RestDecisionPatch = z.infer<typeof restDecisionPatchSchema>;
351
+
352
+ /** What `POST .../decisions/<id>/answer` is sent: an option, and a word about it. */
353
+ export const restAnswerInputSchema = z.object({
354
+ option: z.string().check(z.minLength(1), z.maxLength(64)),
355
+ note: z.optional(paragraphs(MAX_NOTE_LENGTH)),
356
+ });
357
+ export type RestAnswerInput = z.infer<typeof restAnswerInputSchema>;
358
+
359
+ /**
360
+ * What an event says about its subject, so that a feed can show a line without asking for the
361
+ * task or the decision: every field is there only when the kind has it.
362
+ */
363
+ export const restEventDataSchema = z.object({
364
+ number: z.optional(z.int()),
365
+ title: z.optional(z.string()),
366
+ /** For `task.updated`, `project.updated` and `decision.updated`: which fields changed. */
367
+ fields: z.optional(z.array(z.string())),
368
+ /** For the events about a document: its path, and the version written. */
369
+ path: z.optional(z.string()),
370
+ version: z.optional(z.int()),
371
+ /** For `task.moved`. */
372
+ from: z.optional(z.enum(TASK_STATES)),
373
+ to: z.optional(z.enum(TASK_STATES)),
374
+ question: z.optional(z.string()),
375
+ /** For `decision.answered`: the label of the chosen option. */
376
+ option: z.optional(z.string()),
377
+ /** For `person.role_changed` and the events about a project's members: whose role, and what it became. */
378
+ login: z.optional(z.string()),
379
+ role: z.optional(z.string()),
380
+ /** For the events about a project: its key and its name. */
381
+ key: z.optional(z.string()),
382
+ name: z.optional(z.string()),
383
+ /** For what an agent did: what it calls itself. */
384
+ agent: z.optional(z.string()),
385
+ /** For `run.ended`: how. */
386
+ status: z.optional(z.enum(RUN_STATUSES)),
387
+ /** For `run.handed_in`: what the artifact is called. */
388
+ label: z.optional(z.string()),
389
+ /** For `run.reported`: the first words of the report. */
390
+ excerpt: z.optional(z.string()),
391
+ });
392
+ export type RestEventData = z.infer<typeof restEventDataSchema>;
393
+
394
+ export const restEventSchema = z.object({
395
+ /** Events are numbered in the order they were written; the feed is read from a number on. */
396
+ id: z.int().check(z.positive()),
397
+ kind: z.enum(EVENT_KINDS),
398
+ /** Who did it; `null` for the console itself. */
399
+ actor: z.nullable(restPersonSchema),
400
+ /** The agent the actor acted as, when they acted with a token; `null` when a person did it themselves. */
401
+ agent: z.nullable(z.string()),
402
+ /** The project it happened in, or `null` for what happened to the workspace itself. */
403
+ projectId: z.nullable(uuid),
404
+ taskId: z.nullable(uuid),
405
+ decisionId: z.nullable(uuid),
406
+ runId: z.nullable(uuid),
407
+ /** The document it is about, or `null`. */
408
+ documentId: z.nullable(uuid),
409
+ data: restEventDataSchema,
410
+ createdAt: instant,
411
+ });
412
+ export type RestEvent = z.infer<typeof restEventSchema>;
413
+
414
+ /** What an agent reported while at work: Markdown, shown as text or rendered to elements. */
415
+ export const restReportSchema = z.object({
416
+ id: uuid,
417
+ body: z.string(),
418
+ createdAt: instant,
419
+ });
420
+ export type RestReport = z.infer<typeof restReportSchema>;
421
+
422
+ /** A file a run handed in, as the console keeps it; its bytes are at `projectPath(key, "files", <artifact id>)`. */
423
+ export const restFileSchema = z.object({
424
+ /** What the agent called it: one line, without a path. */
425
+ name: z.string(),
426
+ /** In bytes. */
427
+ size: count,
428
+ /** The media type the agent said, or `application/octet-stream`. */
429
+ contentType: z.string(),
430
+ /** The SHA-256 of the bytes, in hex: what the console keeps them under. */
431
+ sha256: z.string(),
432
+ });
433
+ export type RestFile = z.infer<typeof restFileSchema>;
434
+
435
+ /** What a run handed in: a link to a branch, a pull request, a page; or a file the console keeps. */
436
+ export const restArtifactSchema = z.object({
437
+ id: uuid,
438
+ kind: z.enum(ARTIFACT_KINDS),
439
+ /** The link, for a link; `null` for a file. */
440
+ url: z.nullable(z.string()),
441
+ label: z.nullable(z.string()),
442
+ /** The file, for a file; `null` for a link. */
443
+ file: z.nullable(restFileSchema),
444
+ createdAt: instant,
445
+ });
446
+ export type RestArtifact = z.infer<typeof restArtifactSchema>;
447
+
448
+ /** One agent at work on one task for one person: what it did, and what it waits for. */
449
+ export const restRunSchema = z.object({
450
+ id: uuid,
451
+ taskId: uuid,
452
+ /** The task's number, as people say it. */
453
+ taskNumber: z.int().check(z.positive()),
454
+ /** The person the agent acts for. */
455
+ person: restPersonSchema,
456
+ /** What the agent calls itself. */
457
+ agent: z.string(),
458
+ status: z.enum(RUN_STATUSES),
459
+ startedAt: instant,
460
+ endedAt: z.nullable(instant),
461
+ /** What the agent said when the run ended, in Markdown, or `null`. */
462
+ summary: z.nullable(z.string()),
463
+ /** Oldest first. */
464
+ reports: z.array(restReportSchema),
465
+ artifacts: z.array(restArtifactSchema),
466
+ /** The id of the decision the run waits for, or `null`. */
467
+ waitingFor: z.nullable(uuid),
468
+ });
469
+ export type RestRun = z.infer<typeof restRunSchema>;
470
+
471
+ /** `GET /api/v1/projects/<key>/runs`: the runs, newest first. */
472
+ export const restRunsSchema = z.object({ items: z.array(restRunSchema) });
473
+ export type RestRuns = z.infer<typeof restRunsSchema>;
474
+
475
+ /** `GET .../events?after=`: what happened after that number, oldest first. */
476
+ export const restEventsSchema = z.object({
477
+ items: z.array(restEventSchema),
478
+ /** True when there is more after the last item than one page carries. */
479
+ more: z.boolean(),
480
+ });
481
+ export type RestEvents = z.infer<typeof restEventsSchema>;
482
+
483
+ /**
484
+ * A token a person made for an agent, a script or a console of their own, as their own page
485
+ * lists it. The secret itself is answered once, when the token is made, and never kept.
486
+ */
487
+ export const restTokenSchema = z.object({
488
+ id: uuid,
489
+ /** What the person calls it: the agent it is for, where it runs. */
490
+ name: z.string(),
491
+ createdAt: instant,
492
+ /** When it stops being good, or `null` for a token that does not expire. */
493
+ expiresAt: z.nullable(instant),
494
+ /** When it was last presented, or `null` if never. */
495
+ lastUsedAt: z.nullable(instant),
496
+ });
497
+ export type RestToken = z.infer<typeof restTokenSchema>;
498
+
499
+ /** `GET /api/v1/tokens`: the tokens of whoever asks, newest first. */
500
+ export const restTokensSchema = z.object({ items: z.array(restTokenSchema) });
501
+ export type RestTokens = z.infer<typeof restTokensSchema>;
502
+
503
+ /** What `POST /api/v1/tokens` is sent: a name, and for how many days it is good. */
504
+ export const restTokenInputSchema = z.object({
505
+ name: line(MAX_TOKEN_NAME_LENGTH),
506
+ /** Left out: `DEFAULT_TOKEN_DAYS`. `null`: the token does not expire. */
507
+ expiresInDays: z.optional(z.nullable(z.int().check(z.gte(1), z.lte(MAX_TOKEN_DAYS)))),
508
+ });
509
+ export type RestTokenInput = z.infer<typeof restTokenInputSchema>;
510
+
511
+ /** What `POST /api/v1/tokens` answers: the token, and its secret, this once. */
512
+ export const restTokenCreatedSchema = z.object({
513
+ token: restTokenSchema,
514
+ /** Presented as `Authorization: Bearer <secret>`. The server keeps its hash, and never shows it again. */
515
+ secret: z.string(),
516
+ });
517
+ export type RestTokenCreated = z.infer<typeof restTokenCreatedSchema>;
518
+
519
+ /** What `POST /api/v1/connect` is sent by the command: what the agent calls itself, and where it runs. */
520
+ export const restConnectInputSchema = z.object({ agent: line(MAX_TOKEN_NAME_LENGTH) });
521
+ export type RestConnectInput = z.infer<typeof restConnectInputSchema>;
522
+
523
+ /**
524
+ * What `POST /api/v1/connect` answers (ADR-0011): the code a person approves, the address of the
525
+ * console's own page for it, and the secret the command claims the token with, shown nowhere.
526
+ */
527
+ export const restConnectionSchema = z.object({
528
+ code: z.string(),
529
+ url: z.string(),
530
+ secret: z.string(),
531
+ expiresAt: instant,
532
+ /** How many seconds the command waits between claims. */
533
+ interval: z.int().check(z.positive()),
534
+ });
535
+ export type RestConnection = z.infer<typeof restConnectionSchema>;
536
+
537
+ /** `GET /api/v1/connect/<code>`: what asks to connect, for the person who approves it. */
538
+ export const restConnectRequestSchema = z.object({
539
+ code: z.string(),
540
+ /** What the agent calls itself, and where it runs. */
541
+ agent: z.string(),
542
+ createdAt: instant,
543
+ expiresAt: instant,
544
+ approved: z.boolean(),
545
+ });
546
+ export type RestConnectRequest = z.infer<typeof restConnectRequestSchema>;
547
+
548
+ /** What `POST /api/v1/connect/claim` is sent: the secret the connection began with. */
549
+ export const restClaimInputSchema = z.object({
550
+ secret: z.string().check(z.minLength(1), z.maxLength(MAX_SECRET_LENGTH)),
551
+ });
552
+ export type RestClaimInput = z.infer<typeof restClaimInputSchema>;
553
+
554
+ /** What the claim answers: wait, or the token this once, with its secret. */
555
+ export const restClaimSchema = z.union([
556
+ z.object({ status: z.literal("pending"), expiresAt: instant }),
557
+ z.object({ status: z.literal("connected"), token: restTokenSchema, secret: z.string() }),
558
+ ]);
559
+ export type RestClaim = z.infer<typeof restClaimSchema>;
560
+
561
+ /** One of the project's skills, as the SkillCDN deployment lists it at the project's address. */
562
+ export const restSkillSchema = z.object({
563
+ name: z.string(),
564
+ description: z.string(),
565
+ /** The skill's folder in the repository (`""` for the root), and the path of its `SKILL.md`. */
566
+ directory: z.string(),
567
+ path: z.string(),
568
+ /** Where a person reads the skill: its page at the deployment. */
569
+ page: z.string(),
570
+ /** What an agent loads the skill by, through its own SkillCDN connection; `null` when the path is not one. */
571
+ uri: z.nullable(z.string()),
572
+ /** The title and the description in other languages, by language tag, as the repository gives them. */
573
+ translations: z.record(
574
+ z.string(),
575
+ z.object({ title: z.nullable(z.string()), description: z.nullable(z.string()) }),
576
+ ),
577
+ });
578
+ export type RestSkill = z.infer<typeof restSkillSchema>;
579
+
580
+ /** `GET /api/v1/projects/<key>/skills`: the project's skills, read by address through SkillCDN. */
581
+ export const restSkillsSchema = z.object({
582
+ /** The address, canonical, or `null` when neither the project nor the deployment names one. */
583
+ address: z.nullable(z.string()),
584
+ /** The origin of the SkillCDN deployment the skills are read through. */
585
+ source: z.string(),
586
+ /** Where a person browses the address, or `null` without one. */
587
+ page: z.nullable(z.string()),
588
+ status: z.enum(SKILLS_STATUSES),
589
+ /** The skills, when `ready`; empty otherwise. */
590
+ items: z.array(restSkillSchema),
591
+ });
592
+ export type RestSkills = z.infer<typeof restSkillsSchema>;
593
+
594
+ /**
595
+ * A document's path (ADR-0009): segments of lowercase letters, digits and hyphens, separated
596
+ * by slashes, as a repository keeps files; the document's address, which does not change.
597
+ */
598
+ const documentPath = z
599
+ .string()
600
+ .check(
601
+ z.maxLength(MAX_DOCUMENT_PATH_LENGTH),
602
+ z.refine(
603
+ isDocumentPath,
604
+ "must be segments of lowercase letters, digits and hyphens, separated by slashes",
605
+ ),
606
+ );
607
+
608
+ /** A document as a folder or a search lists it: everything but the body. */
609
+ export const restDocumentSummarySchema = z.object({
610
+ id: uuid,
611
+ path: documentPath,
612
+ title: z.string(),
613
+ /** The number of the latest version, from 1. */
614
+ version: z.int().check(z.positive()),
615
+ /** Who wrote the latest version, and the agent they wrote it through, or `null`. */
616
+ updatedBy: restPersonSchema,
617
+ agent: z.nullable(z.string()),
618
+ /** When the document was archived, or `null` while it is current. */
619
+ archivedAt: z.nullable(instant),
620
+ createdAt: instant,
621
+ updatedAt: instant,
622
+ });
623
+ export type RestDocumentSummary = z.infer<typeof restDocumentSummarySchema>;
624
+
625
+ /** `GET /api/v1/projects/<key>/docs?folder=`: a folder's pages and folders; with `q=`, the pages found. */
626
+ export const restDocumentsSchema = z.object({
627
+ /** The folder listed, `""` for the root; `""` for a search. */
628
+ folder: z.string(),
629
+ /** The folders in it, as paths, by name; none for a search. */
630
+ folders: z.array(z.string()),
631
+ /** The pages in it, by title; or the pages found, the best first. */
632
+ items: z.array(restDocumentSummarySchema),
633
+ });
634
+ export type RestDocuments = z.infer<typeof restDocumentsSchema>;
635
+
636
+ /** A document the document links to: its path, and its title when there is a document there yet. */
637
+ export const restDocumentLinkSchema = z.object({
638
+ path: z.string(),
639
+ title: z.nullable(z.string()),
640
+ });
641
+ export type RestDocumentLink = z.infer<typeof restDocumentLinkSchema>;
642
+
643
+ /** What refers to the document: another document, a task, or a decision, each by what people call it. */
644
+ export const restBacklinkSchema = z.object({
645
+ kind: z.enum(LINK_SOURCES),
646
+ id: uuid,
647
+ /** The document's path, for a document. */
648
+ path: z.nullable(z.string()),
649
+ /** The task's number, for a task. */
650
+ number: z.nullable(z.int()),
651
+ /** The document's title, the task's title, or the decision's question. */
652
+ title: z.string(),
653
+ });
654
+ export type RestBacklink = z.infer<typeof restBacklinkSchema>;
655
+
656
+ /** A file attached to a document; its bytes are at `projectPath(key, "docs", path)/files/<id>`. */
657
+ export const restDocumentFileSchema = z.object({
658
+ id: uuid,
659
+ label: z.nullable(z.string()),
660
+ file: restFileSchema,
661
+ addedBy: restPersonSchema,
662
+ agent: z.nullable(z.string()),
663
+ createdAt: instant,
664
+ });
665
+ export type RestDocumentFile = z.infer<typeof restDocumentFileSchema>;
666
+
667
+ /** A document in full: its latest version, what it links to, what refers to it, and its files. */
668
+ export const restDocumentSchema = z.object({
669
+ ...restDocumentSummarySchema.shape,
670
+ /** Markdown. Shown as text or rendered to elements, never as HTML. */
671
+ body: z.string(),
672
+ createdBy: restPersonSchema,
673
+ /** The documents it links to, by path. */
674
+ links: z.array(restDocumentLinkSchema),
675
+ /** What links to it. */
676
+ backlinks: z.array(restBacklinkSchema),
677
+ files: z.array(restDocumentFileSchema),
678
+ });
679
+ export type RestDocument = z.infer<typeof restDocumentSchema>;
680
+
681
+ /** What `PUT /api/v1/projects/<key>/docs/<path>` is sent: the page as it is to be. */
682
+ export const restDocumentInputSchema = z.object({
683
+ title: line(MAX_TITLE_LENGTH),
684
+ body: paragraphs(MAX_DOCUMENT_LENGTH),
685
+ /** The version the writer started from; the write is refused when the document has moved on. */
686
+ baseVersion: z.optional(z.int().check(z.positive())),
687
+ });
688
+ export type RestDocumentInput = z.infer<typeof restDocumentInputSchema>;
689
+
690
+ /** One version of a document, as the list of them says it: who wrote it, through which agent, when. */
691
+ export const restVersionSummarySchema = z.object({
692
+ number: z.int().check(z.positive()),
693
+ title: z.string(),
694
+ author: restPersonSchema,
695
+ agent: z.nullable(z.string()),
696
+ createdAt: instant,
697
+ });
698
+ export type RestVersionSummary = z.infer<typeof restVersionSummarySchema>;
699
+
700
+ /** `GET .../docs/<path>/versions`: newest first. */
701
+ export const restVersionsSchema = z.object({ items: z.array(restVersionSummarySchema) });
702
+ export type RestVersions = z.infer<typeof restVersionsSchema>;
703
+
704
+ /** `GET .../docs/<path>/versions/<number>`: the version with its body. */
705
+ export const restVersionSchema = z.object({
706
+ ...restVersionSummarySchema.shape,
707
+ body: z.string(),
708
+ });
709
+ export type RestVersion = z.infer<typeof restVersionSchema>;
710
+
711
+ export const restErrorSchema = z.object({
712
+ error: z.object({
713
+ /** Stable; what a client branches on. */
714
+ code: z.string(),
715
+ /** For a person, in English. */
716
+ message: z.string(),
717
+ }),
718
+ });
719
+ export type RestError = z.infer<typeof restErrorSchema>;
720
+
721
+ /** What an agent says when it ends a run: Markdown, bounded. */
722
+ const summary = paragraphs(MAX_SUMMARY_LENGTH);
723
+
724
+ /** What `POST /api/v1/projects/<key>/runs` is sent: the task to take, and what the agent calls itself. */
725
+ export const restRunInputSchema = z.object({
726
+ taskId: uuid,
727
+ /** Left out, the agent is called what its person called the token, or by the person's login. */
728
+ agent: z.optional(line(MAX_AGENT_LENGTH)),
729
+ });
730
+ export type RestRunInput = z.infer<typeof restRunInputSchema>;
731
+
732
+ /** What `POST .../runs/<id>/reports` is sent: how the work goes, in Markdown. */
733
+ export const restReportInputSchema = z.object({
734
+ body: body.check(z.refine((value) => value.trim().length > 0, "must say something")),
735
+ });
736
+ export type RestReportInput = z.infer<typeof restReportInputSchema>;
737
+
738
+ /** What `POST .../runs/<id>/artifacts` is sent: a link to what was made. */
739
+ export const restArtifactInputSchema = z.object({
740
+ url,
741
+ label: z.optional(line(MAX_LINK_LABEL_LENGTH)),
742
+ });
743
+ export type RestArtifactInput = z.infer<typeof restArtifactInputSchema>;
744
+
745
+ /** A media type as a client says one: `type/subtype`, lowercase, without parameters. */
746
+ const mediaType = z
747
+ .string()
748
+ .check(
749
+ z.trim(),
750
+ z.toLowerCase(),
751
+ z.maxLength(120),
752
+ z.regex(/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/, "must be a media type"),
753
+ );
754
+
755
+ /**
756
+ * What `POST .../runs/<id>/files` is sent beside the bytes, as the parts of a form: the
757
+ * file's name and media type come with the `file` part, the `label` is a part of its own.
758
+ */
759
+ export const restFileInputSchema = z.object({
760
+ name: line(MAX_FILE_NAME_LENGTH).check(
761
+ z.refine(
762
+ (value) => !/[\\/]/.test(value) && value !== "." && value !== "..",
763
+ "must be a file name, not a path",
764
+ ),
765
+ ),
766
+ contentType: z.optional(mediaType),
767
+ label: z.optional(line(MAX_LINK_LABEL_LENGTH)),
768
+ });
769
+ export type RestFileInput = z.infer<typeof restFileInputSchema>;
770
+
771
+ /** What `POST .../runs/<id>/end` is sent: how the run ended, and what was done and left. */
772
+ export const restRunEndInputSchema = z.object({
773
+ status: z.enum(RUN_ENDINGS),
774
+ summary: z.optional(summary),
775
+ });
776
+ export type RestRunEndInput = z.infer<typeof restRunEndInputSchema>;