@skillcdn/console 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +5 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +290 -46
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +5 -0
  11. package/dist/cli/credentials.d.ts.map +1 -1
  12. package/dist/cli/credentials.js +2 -0
  13. package/dist/cli/credentials.js.map +1 -1
  14. package/dist/cli/directory.d.ts +7 -0
  15. package/dist/cli/directory.d.ts.map +1 -1
  16. package/dist/cli/directory.js +7 -0
  17. package/dist/cli/directory.js.map +1 -1
  18. package/dist/cli/format.d.ts +10 -1
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +76 -20
  21. package/dist/cli/format.js.map +1 -1
  22. package/dist/cli/help.d.ts.map +1 -1
  23. package/dist/cli/help.js +42 -23
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/main.js +1 -0
  26. package/dist/cli/main.js.map +1 -1
  27. package/dist/client.d.ts +39 -9
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +24 -3
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.d.ts +28 -6
  32. package/dist/compat.d.ts.map +1 -1
  33. package/dist/compat.js +55 -14
  34. package/dist/compat.js.map +1 -1
  35. package/dist/components/board.d.ts.map +1 -1
  36. package/dist/components/board.js +6 -4
  37. package/dist/components/board.js.map +1 -1
  38. package/dist/components/calendar.d.ts +18 -8
  39. package/dist/components/calendar.d.ts.map +1 -1
  40. package/dist/components/calendar.js +39 -27
  41. package/dist/components/calendar.js.map +1 -1
  42. package/dist/components/event-feed.d.ts.map +1 -1
  43. package/dist/components/event-feed.js +2 -0
  44. package/dist/components/event-feed.js.map +1 -1
  45. package/dist/components/pack-connect.d.ts +23 -0
  46. package/dist/components/pack-connect.d.ts.map +1 -0
  47. package/dist/components/pack-connect.js +86 -0
  48. package/dist/components/pack-connect.js.map +1 -0
  49. package/dist/components/pack-token.d.ts +27 -0
  50. package/dist/components/pack-token.d.ts.map +1 -0
  51. package/dist/components/pack-token.js +11 -0
  52. package/dist/components/pack-token.js.map +1 -0
  53. package/dist/components/picker.d.ts +39 -0
  54. package/dist/components/picker.d.ts.map +1 -0
  55. package/dist/components/picker.js +155 -0
  56. package/dist/components/picker.js.map +1 -0
  57. package/dist/components/projects.d.ts +13 -0
  58. package/dist/components/projects.d.ts.map +1 -1
  59. package/dist/components/projects.js +37 -5
  60. package/dist/components/projects.js.map +1 -1
  61. package/dist/components/task-form.d.ts +18 -4
  62. package/dist/components/task-form.d.ts.map +1 -1
  63. package/dist/components/task-form.js +56 -13
  64. package/dist/components/task-form.js.map +1 -1
  65. package/dist/components/task-list.js +2 -2
  66. package/dist/components/task-list.js.map +1 -1
  67. package/dist/components/task-view.d.ts +7 -1
  68. package/dist/components/task-view.d.ts.map +1 -1
  69. package/dist/components/task-view.js +6 -5
  70. package/dist/components/task-view.js.map +1 -1
  71. package/dist/components/ui.d.ts +7 -0
  72. package/dist/components/ui.d.ts.map +1 -1
  73. package/dist/components/ui.js +11 -0
  74. package/dist/components/ui.js.map +1 -1
  75. package/dist/components/work.d.ts +3 -1
  76. package/dist/components/work.d.ts.map +1 -1
  77. package/dist/components/work.js +6 -4
  78. package/dist/components/work.js.map +1 -1
  79. package/dist/components/workspace.d.ts +46 -0
  80. package/dist/components/workspace.d.ts.map +1 -0
  81. package/dist/components/workspace.js +124 -0
  82. package/dist/components/workspace.js.map +1 -0
  83. package/dist/console.css +319 -11
  84. package/dist/console.d.ts.map +1 -1
  85. package/dist/console.js +83 -24
  86. package/dist/console.js.map +1 -1
  87. package/dist/data.d.ts +26 -1
  88. package/dist/data.d.ts.map +1 -1
  89. package/dist/data.js +59 -1
  90. package/dist/data.js.map +1 -1
  91. package/dist/dates.d.ts +12 -2
  92. package/dist/dates.d.ts.map +1 -1
  93. package/dist/dates.js +20 -2
  94. package/dist/dates.js.map +1 -1
  95. package/dist/i18n/en.d.ts +64 -3
  96. package/dist/i18n/en.d.ts.map +1 -1
  97. package/dist/i18n/en.js +81 -16
  98. package/dist/i18n/en.js.map +1 -1
  99. package/dist/i18n/ko.d.ts.map +1 -1
  100. package/dist/i18n/ko.js +77 -16
  101. package/dist/i18n/ko.js.map +1 -1
  102. package/dist/index.d.ts +6 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +6 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/limits.d.ts +2 -0
  107. package/dist/limits.d.ts.map +1 -1
  108. package/dist/limits.js +2 -0
  109. package/dist/limits.js.map +1 -1
  110. package/dist/router.d.ts +4 -4
  111. package/dist/router.d.ts.map +1 -1
  112. package/dist/router.js +11 -2
  113. package/dist/router.js.map +1 -1
  114. package/dist/routes.d.ts +4 -0
  115. package/dist/routes.d.ts.map +1 -1
  116. package/dist/routes.js +4 -0
  117. package/dist/routes.js.map +1 -1
  118. package/dist/schemas.d.ts +194 -13
  119. package/dist/schemas.d.ts.map +1 -1
  120. package/dist/schemas.js +121 -21
  121. package/dist/schemas.js.map +1 -1
  122. package/dist/vocabulary.d.ts +21 -1
  123. package/dist/vocabulary.d.ts.map +1 -1
  124. package/dist/vocabulary.js +18 -0
  125. package/dist/vocabulary.js.map +1 -1
  126. package/docs/specs/cli.md +25 -20
  127. package/docs/specs/rest.md +42 -19
  128. package/package.json +2 -2
  129. package/skills/building-a-console/SKILL.md +6 -6
  130. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  131. package/skills/deploying-a-console/SKILL.md +9 -8
  132. package/skills/deploying-a-console/references/resources.md +1 -1
  133. package/skills/working-the-board/SKILL.md +5 -5
  134. package/skills/working-the-board/references/setting-up.md +1 -1
  135. package/skills/working-the-board/references/terms.md +3 -1
  136. package/skills/working-the-board/references/the-sentence.md +1 -1
  137. package/src/api.ts +21 -0
  138. package/src/cli/cli.ts +384 -51
  139. package/src/cli/credentials.ts +7 -0
  140. package/src/cli/directory.ts +9 -0
  141. package/src/cli/format.ts +83 -20
  142. package/src/cli/help.ts +50 -23
  143. package/src/cli/main.ts +1 -0
  144. package/src/client.ts +107 -10
  145. package/src/compat.ts +63 -14
  146. package/src/components/board.tsx +10 -4
  147. package/src/components/calendar.tsx +76 -34
  148. package/src/components/event-feed.tsx +2 -0
  149. package/src/components/pack-connect.tsx +179 -0
  150. package/src/components/pack-token.tsx +74 -0
  151. package/src/components/picker.tsx +297 -0
  152. package/src/components/projects.tsx +108 -9
  153. package/src/components/task-form.tsx +149 -63
  154. package/src/components/task-list.tsx +2 -2
  155. package/src/components/task-view.tsx +23 -9
  156. package/src/components/ui.tsx +25 -0
  157. package/src/components/work.tsx +10 -5
  158. package/src/components/workspace.tsx +298 -0
  159. package/src/console.tsx +202 -32
  160. package/src/data.ts +101 -1
  161. package/src/dates.ts +23 -2
  162. package/src/i18n/en.ts +92 -16
  163. package/src/i18n/ko.ts +84 -16
  164. package/src/index.ts +32 -1
  165. package/src/limits.ts +2 -0
  166. package/src/router.ts +17 -5
  167. package/src/routes.ts +4 -0
  168. package/src/schemas.ts +140 -19
  169. package/src/styles/console.css +319 -11
  170. package/src/vocabulary.ts +28 -0
@@ -22,6 +22,11 @@ export interface Login {
22
22
  readonly agent: string | undefined;
23
23
  /** When the command was signed in, when known. */
24
24
  readonly since: string | undefined;
25
+ /**
26
+ * The workspace's time zone as the console last said it, at login or whoami (ADR-0035): what
27
+ * the command prints moments in, without asking again; none when the workspace names none.
28
+ */
29
+ readonly timeZone?: string | undefined;
25
30
  }
26
31
 
27
32
  /** Everything the command keeps: the logins, and the console a directory that names none works at. */
@@ -71,6 +76,7 @@ const loginSchema = z.object({
71
76
  token: z.string().check(z.minLength(1), z.maxLength(1024)),
72
77
  agent: z.optional(z.string().check(z.maxLength(200))),
73
78
  since: z.optional(z.string().check(z.maxLength(40))),
79
+ timeZone: z.optional(z.string().check(z.minLength(1), z.maxLength(64))),
74
80
  });
75
81
  const fileSchema = z.object({
76
82
  version: z.literal(2),
@@ -95,6 +101,7 @@ export function parseCredentials(json: unknown): Credentials | undefined {
95
101
  token: login.token,
96
102
  agent: login.agent,
97
103
  since: login.since,
104
+ ...(login.timeZone === undefined ? {} : { timeZone: login.timeZone }),
98
105
  })),
99
106
  };
100
107
  }
@@ -15,6 +15,15 @@ import * as z from "zod/mini";
15
15
  /** The file's name, in the working directory or one above it. */
16
16
  export const DIRECTORY_FILE = ".skillcdn-console.json";
17
17
 
18
+ /**
19
+ * The sentence a checkout's agreement carries to send the agents working there to the console:
20
+ * the one the skill working-the-board keeps (references/the-sentence.md) and this repository's
21
+ * own agreement carries. `console use` prints it, since nothing tells an agent to look until the
22
+ * checkout says so, as `skillcdn use` prints the sentence for a pack.
23
+ */
24
+ export const THE_SENTENCE =
25
+ "**Bound to a console?** When `.skillcdn-console.json` is in this checkout, or `console whoami` names a project, work the task through that SkillCDN console (`console help` says how).";
26
+
18
27
  /** How large the file may be before it is not read: a few names, never more. */
19
28
  const MAX_FILE_CHARS = 16 * 1024;
20
29
 
package/src/cli/format.ts CHANGED
@@ -19,6 +19,7 @@ import type {
19
19
  RestVersion,
20
20
  RestVersionSummary,
21
21
  RestWork,
22
+ RestWorkspace,
22
23
  } from "../schemas.js";
23
24
 
24
25
  // What the command prints: one line per thing in a list, a few lines for one thing in full.
@@ -27,6 +28,48 @@ import type {
27
28
 
28
29
  const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`;
29
30
 
31
+ /** The time zone moments are printed in: the workspace's, as the command last learned it (ADR-0035); none leaves them as the API gives them. */
32
+ let momentZone: string | undefined;
33
+
34
+ /** Says which time zone the moments printed from now on are in, or none. */
35
+ export function setMomentZone(zone: string | undefined): void {
36
+ momentZone = zone;
37
+ }
38
+
39
+ /**
40
+ * An instant as the command prints it: in the workspace's time zone, as ISO 8601 with its
41
+ * offset (`2026-10-18T15:04:17+09:00`), when a zone is known; else as the API gave it, in UTC.
42
+ */
43
+ export function moment(iso: string): string {
44
+ if (momentZone === undefined) {
45
+ return iso;
46
+ }
47
+ const date = new Date(iso);
48
+ if (Number.isNaN(date.getTime())) {
49
+ return iso;
50
+ }
51
+ let parts: Intl.DateTimeFormatPart[];
52
+ try {
53
+ parts = new Intl.DateTimeFormat("en-US", {
54
+ timeZone: momentZone,
55
+ hourCycle: "h23",
56
+ year: "numeric",
57
+ month: "2-digit",
58
+ day: "2-digit",
59
+ hour: "2-digit",
60
+ minute: "2-digit",
61
+ second: "2-digit",
62
+ timeZoneName: "longOffset",
63
+ }).formatToParts(date);
64
+ } catch {
65
+ return iso;
66
+ }
67
+ const of = (type: Intl.DateTimeFormatPartTypes): string =>
68
+ parts.find((part) => part.type === type)?.value ?? "";
69
+ const offset = of("timeZoneName").replace("GMT", "");
70
+ return `${of("year")}-${of("month")}-${of("day")}T${of("hour")}:${of("minute")}:${of("second")}${offset === "" ? "Z" : offset}`;
71
+ }
72
+
30
73
  const indent = (text: string): string =>
31
74
  text
32
75
  .trimEnd()
@@ -54,7 +97,7 @@ export function formatProject(project: RestProject): string {
54
97
  `project ${project.key}: ${project.name}`,
55
98
  `visibility: ${project.visibility} role: ${project.role} skills: ${project.skillsAddress ?? "the organization's"}`,
56
99
  `color: ${project.color ?? "none"} symbol: ${project.symbol ?? "none"} image: ${project.icon === null ? "none" : project.icon.contentType}`,
57
- `pack token: ${project.packToken === null ? "none" : `set ${project.packToken.setAt}`}`,
100
+ `pack token: ${project.packToken === null ? "none" : `set ${moment(project.packToken.setAt)}`}`,
58
101
  ];
59
102
  if (project.description.trim().length > 0) {
60
103
  lines.push("", project.description.trimEnd());
@@ -78,9 +121,25 @@ export function formatListingLine(listing: RestListing): string {
78
121
  return `${listing.login} (let in at ${HOST_NAMES[listing.host]}, not signed in yet)`;
79
122
  }
80
123
 
124
+ /** The workspace as its Settings page shows it: what it is called, who sees whom, the week, the time zone, where the skills come from, and what tokens may last. */
125
+ export function formatWorkspace(workspace: RestWorkspace): string {
126
+ const visibility = workspace.peopleVisibility ?? "everyone";
127
+ const zone = workspace.timeZone ?? null;
128
+ const token = workspace.skills.packToken ?? null;
129
+ return [
130
+ `workspace ${workspace.name}`,
131
+ `people visibility: ${visibility} (${visibility === "everyone" ? "everyone sees everyone" : "a member sees only the people they share a project with; an administrator sees everyone"})`,
132
+ `week begins on: ${workspace.weekStart ?? "auto"} (auto: the system default, as each device has it)`,
133
+ `time zone: ${zone ?? "none"} (${zone === null ? "moments are printed as the API gives them, in UTC" : "moments are printed in it, as ISO 8601 with its offset"})`,
134
+ `skills: ${workspace.skills.address ?? "no address of the organization's"} at ${workspace.skills.source}`,
135
+ `SkillCDN token: ${token === null ? "none" : `set ${moment(token.setAt)}`} (${token === null ? "the organization's pack is read with none" : "the organization's pack is read with it by a project with no address and no token of its own"})`,
136
+ `token days at most: ${workspace.tokenDaysAtMost ?? "a person chooses"}`,
137
+ ].join("\n");
138
+ }
139
+
81
140
  /** One member of a project: their login, what they are in it, and since when. */
82
141
  export function formatMemberLine(member: RestMember): string {
83
- return `${member.person.login} ${member.role} (since ${member.addedAt.slice(0, 10)})`;
142
+ return `${member.person.login} ${member.role} (since ${moment(member.addedAt).slice(0, 10)})`;
84
143
  }
85
144
 
86
145
  /** The line under a page of a listing that has more after it: how to ask for the next page. */
@@ -88,10 +147,14 @@ export function formatMore(more: boolean, limit: number, offset: number): string
88
147
  return more ? `\n... more: --offset ${offset + limit}` : "";
89
148
  }
90
149
 
150
+ /** The logins of the people at work on a task, in the order they came to it. */
151
+ const logins = (people: readonly RestPerson[]): string =>
152
+ people.map((person) => person.login).join(", ");
153
+
91
154
  export function formatTaskLine(task: RestTask): string {
92
155
  const notes = [`owner ${task.owner.login}`];
93
- if (task.assignee !== null) {
94
- notes.push(`assigned to ${task.assignee.login}`);
156
+ if (task.workers.length > 0) {
157
+ notes.push(`workers ${logins(task.workers)}`);
95
158
  }
96
159
  if (task.category !== null) {
97
160
  notes.push(task.category.key);
@@ -121,9 +184,9 @@ function scheduleLines(task: RestTask): string[] {
121
184
  lines.push(plan.join(" "));
122
185
  }
123
186
  const happened = [
124
- task.startedAt === null ? undefined : `started: ${task.startedAt}`,
125
- task.doneAt === null ? undefined : `done: ${task.doneAt}`,
126
- task.archivedAt === null ? undefined : `archived: ${task.archivedAt}`,
187
+ task.startedAt === null ? undefined : `started: ${moment(task.startedAt)}`,
188
+ task.doneAt === null ? undefined : `done: ${moment(task.doneAt)}`,
189
+ task.archivedAt === null ? undefined : `archived: ${moment(task.archivedAt)}`,
127
190
  ].filter((part) => part !== undefined);
128
191
  if (happened.length > 0) {
129
192
  lines.push(happened.join(" "));
@@ -140,7 +203,7 @@ export function formatTask(
140
203
  ): string {
141
204
  const lines = [
142
205
  `#${task.number} ${task.title}`,
143
- `state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} assignee: ${task.assignee?.login ?? "nobody"}`,
206
+ `state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} workers: ${task.workers.length === 0 ? "nobody" : logins(task.workers)}`,
144
207
  `id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
145
208
  ...scheduleLines(task),
146
209
  ];
@@ -181,7 +244,7 @@ export function formatTask(
181
244
  export function formatComment(comment: RestComment): string {
182
245
  const who =
183
246
  comment.agent === null ? comment.author.login : `${comment.author.login} as ${comment.agent}`;
184
- const lines = [` ${who}, ${comment.createdAt}, comment ${comment.id}:`];
247
+ const lines = [` ${who}, ${moment(comment.createdAt)}, comment ${comment.id}:`];
185
248
  if (comment.body.trim().length > 0) {
186
249
  lines.push(indent(comment.body));
187
250
  }
@@ -199,7 +262,7 @@ export function formatRunLine(run: RestRun): string {
199
262
  notes.push(`waiting for decision ${run.waitingFor}`);
200
263
  }
201
264
  if (run.endedAt === null) {
202
- notes.push(`last heard ${run.lastHeardAt}`);
265
+ notes.push(`last heard ${moment(run.lastHeardAt)}`);
203
266
  }
204
267
  return `${run.id} ${run.status} ${run.agent} for ${run.person.login} task #${run.taskNumber} (${notes.join(", ")})`;
205
268
  }
@@ -208,7 +271,7 @@ export function formatRunLine(run: RestRun): string {
208
271
  export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string): string {
209
272
  const lines = [
210
273
  formatRunLine(run),
211
- `started: ${run.startedAt}${run.endedAt === null ? "" : ` ended: ${run.endedAt}`}`,
274
+ `started: ${moment(run.startedAt)}${run.endedAt === null ? "" : ` ended: ${moment(run.endedAt)}`}`,
212
275
  ];
213
276
  if (run.summary !== null) {
214
277
  lines.push("", "summary:", indent(run.summary));
@@ -216,7 +279,7 @@ export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string
216
279
  if (run.reports.length > 0) {
217
280
  lines.push("", "reports:");
218
281
  for (const report of run.reports) {
219
- lines.push(` [${report.createdAt}]`, indent(report.body));
282
+ lines.push(` [${moment(report.createdAt)}]`, indent(report.body));
220
283
  }
221
284
  }
222
285
  if (run.artifacts.length > 0) {
@@ -246,7 +309,7 @@ function formatRulesHalf(where: "the workspace" | "this project", rules: RestRul
246
309
  const facts =
247
310
  page === null
248
311
  ? undefined
249
- : `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${page.updatedAt}`;
312
+ : `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${moment(page.updatedAt)}`;
250
313
  if (rules.source === "none" || rules.body === null) {
251
314
  return `Rules of ${where}: none of its own; the workspace's hold.`;
252
315
  }
@@ -316,9 +379,9 @@ export function formatCategoryLine(category: RestCategory): string {
316
379
  export function formatWork(work: RestWork): string {
317
380
  const lines: string[] = [];
318
381
  if (work.tasks.length === 0) {
319
- lines.push("No open task is assigned to you.");
382
+ lines.push("No open task is on your plate.");
320
383
  } else {
321
- lines.push("tasks assigned to you:");
384
+ lines.push("tasks you work on:");
322
385
  for (const { project, task } of work.tasks) {
323
386
  lines.push(` ${project.key} ${formatTaskLine(task)}`);
324
387
  }
@@ -333,10 +396,10 @@ export function formatWork(work: RestWork): string {
333
396
  /** Another person's plate: their open tasks, each with its project; no decisions, which wait for anyone. */
334
397
  export function formatPersonWork(person: RestPerson, work: RestPersonWork): string {
335
398
  if (work.tasks.length === 0) {
336
- return `No open task is assigned to ${person.login}.`;
399
+ return `No open task is on ${person.login}'s plate.`;
337
400
  }
338
401
  return [
339
- `tasks assigned to ${person.login}:`,
402
+ `tasks ${person.login} works on:`,
340
403
  ...work.tasks.map(({ project, task }) => ` ${project.key} ${formatTaskLine(task)}`),
341
404
  ].join("\n");
342
405
  }
@@ -397,7 +460,7 @@ export function formatDocumentLine(document: RestDocumentSummary): string {
397
460
  if (document.archivedAt !== null) {
398
461
  notes.push("archived");
399
462
  }
400
- return `${document.path} ${document.title} (${notes.join(", ")}; ${document.updatedAt})`;
463
+ return `${document.path} ${document.title} (${notes.join(", ")}; ${moment(document.updatedAt)})`;
401
464
  }
402
465
 
403
466
  /** A folder's folders and pages, or the pages a search found. */
@@ -419,7 +482,7 @@ export function formatDocument(
419
482
  ): string {
420
483
  const lines = [
421
484
  `${document.path}: ${document.title}`,
422
- `version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${document.updatedAt}${document.archivedAt === null ? "" : `; archived ${document.archivedAt}`}`,
485
+ `version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${moment(document.updatedAt)}${document.archivedAt === null ? "" : `; archived ${moment(document.archivedAt)}`}`,
423
486
  `id: ${document.id}`,
424
487
  ];
425
488
  if (document.body.trim().length > 0) {
@@ -453,7 +516,7 @@ export function formatDocument(
453
516
  }
454
517
 
455
518
  export function formatVersionLine(version: RestVersionSummary): string {
456
- return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${version.createdAt})`;
519
+ return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${moment(version.createdAt)})`;
457
520
  }
458
521
 
459
522
  /** One version of a document, with its body as it was. */
package/src/cli/help.ts CHANGED
@@ -23,9 +23,11 @@ import {
23
23
  MAX_QUESTION_LENGTH,
24
24
  MAX_REPORTS_PER_RUN,
25
25
  MAX_RULES_LENGTH,
26
+ MAX_SEARCH_LENGTH,
26
27
  MAX_SUMMARY_LENGTH,
27
28
  MAX_TITLE_LENGTH,
28
29
  MAX_VERSIONS_PER_DOCUMENT,
30
+ MAX_WORKERS,
29
31
  MIN_OPTIONS,
30
32
  } from "../limits.js";
31
33
  import { COLOR_NAMES } from "../vocabulary.js";
@@ -84,17 +86,17 @@ const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from tod
84
86
  const COLORS = `one of ${COLOR_NAMES.join(", ")}`;
85
87
  const KEY = "lowercase letters, digits and hyphens";
86
88
  const TASK_FIELD_FLAGS =
87
- "[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login|me>] [--parent <number|id>]";
89
+ "[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--worker <login|me|none>]... [--parent <number|id>]";
88
90
  const TASK_FIELD_LIMITS = [
89
91
  `starts, due: ${DAY}; the due day is not before the start`,
90
92
  "category: the key of one of the workspace's categories (console categories); goal: the id of one of the project's goals (console goals)",
91
- "assignee: a login of the workspace, or me; parent: a task of the project",
93
+ `worker: a login of the workspace, or me; given again, or with commas between, for several, up to ${MAX_WORKERS}, in place of the workers there were; none alone for nobody. parent: a task of the project, by its number or its id; console tasks --search <words> finds one by its title`,
92
94
  ];
93
95
  const TASK_FIELD_REFUSALS = [
94
96
  "task.invalid_category: no category has that key",
95
97
  "task.invalid_goal: the goal is not one of this project",
96
98
  "task.invalid_dates: the due day is before the start",
97
- "task.invalid_assignee: the person may not work in the project",
99
+ "task.invalid_worker: a person named as a worker may not work in the project",
98
100
  "task.invalid_parent: the parent is not a task of the project, or would make a loop",
99
101
  ];
100
102
 
@@ -136,9 +138,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
136
138
  },
137
139
  use: {
138
140
  usage: "use <key> [--url <origin>] [--account <login>]",
139
- synopsis: "Binds this directory to a project: writes .skillcdn-console.json here.",
141
+ synopsis:
142
+ "Binds this directory to a project: writes .skillcdn-console.json here, and prints the sentence for the checkout's agreement.",
140
143
  about:
141
- "Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names.",
144
+ "Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names. Then prints the sentence for the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), to be copied as it is, since nothing sends an agent to the console until the checkout says so.",
142
145
  refusals: ["project.not_found: no project has the key, or it is not yours to see"],
143
146
  },
144
147
  work: {
@@ -146,14 +149,15 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
146
149
  synopsis:
147
150
  "What is on your plate across your projects: your open tasks, and the decisions waiting.",
148
151
  about:
149
- "What is on your plate across the projects you may see: the open tasks assigned to you, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: their open tasks within the projects you may see, without the decisions, which wait for anyone.",
152
+ "What is on your plate across the projects you may see: the open tasks you work on, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: the open tasks they work on within the projects you may see, without the decisions, which wait for anyone.",
150
153
  },
151
154
  tasks: {
152
155
  usage:
153
- "tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--archived] [--limit <n>] [--offset <n>]",
156
+ "tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--search <words>] [--archived] [--limit <n>] [--offset <n>]",
154
157
  synopsis:
155
- "The tasks on the board, newest first, narrowed by state, owner, category, goal or due day.",
156
- about: `The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --unassigned those assigned to nobody (not with --mine), --category and --goal those sorted into one, --due those due by a day: today, within the week, or overdue (the open ones past their day). The tasks put away (ADR-0029) are left out unless --archived asks for them alone. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
158
+ "The tasks on the board, newest first, narrowed by state, worker, category, goal, due day or words of the title.",
159
+ about: `The tasks on the board, newest first: number, state, priority, title, who owns and who works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those you work on, --unassigned those nobody works on (not with --mine), --category and --goal those sorted into one, --due those due by a day: today, this week (through its last day, the week beginning as the workspace says, console workspace, or as your language has it), or overdue (the open ones past their day), --search those whose title has every word given, whatever the case, and the task of that number for a number (12 or #12): how a task is found by its title, as a parent or otherwise. The tasks put away (ADR-0029) are left out unless --archived asks for them alone. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
160
+ limits: [`search: 1 to ${MAX_SEARCH_LENGTH} characters`],
157
161
  },
158
162
  task: {
159
163
  usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task archive <number|id> | task archive --goal <id> | task restore <number|id>`,
@@ -170,10 +174,11 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
170
174
  refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
171
175
  },
172
176
  goals: {
173
- usage: "goals [--category <key>] [--state <state>]",
177
+ usage:
178
+ "goals [--category <key>] [--state <state>] [--search <words>] [--limit <n>] [--offset <n>]",
174
179
  synopsis: "The roadmap: the project's goals, with their state, their days and how far each is.",
175
- about:
176
- "The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its category, the days it starts and is meant to be reached by, and how far it is, counted from the tasks that work towards it. --category keeps those sorted into one, --state those in one state.",
180
+ about: `The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its category, the days it starts and is meant to be reached by, and how far it is, counted from the tasks that work towards it. --category keeps those sorted into one, --state those in one state, --search those whose title has every word given, whatever the case: how a goal's id is found for --goal. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more.`,
181
+ limits: [`search: 1 to ${MAX_SEARCH_LENGTH} characters`],
177
182
  },
178
183
  goal: {
179
184
  usage:
@@ -203,9 +208,26 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
203
208
  },
204
209
  people: {
205
210
  usage: "people",
206
- synopsis: "Everyone of the workspace, by login, with what each is in it.",
211
+ synopsis: "The people you see, by login, with what each is in the workspace.",
207
212
  about:
208
- "Everyone of the workspace, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Then the logins an administrator let in that nobody has signed in with yet. The logins are what --assignee, member and person take.",
213
+ "The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --worker, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
214
+ },
215
+ workspace: {
216
+ usage:
217
+ "workspace | workspace edit [--people-visibility <everyone|shared-projects>] [--week-start <auto|monday|sunday>] [--time-zone <zone|none>] | workspace pack-token --connect | workspace pack-token --stdin | workspace pack-token --remove",
218
+ overview:
219
+ "workspace | workspace edit [flags] | workspace pack-token --connect|--stdin|--remove",
220
+ synopsis:
221
+ "The workspace's settings; or some changed, or its SkillCDN token set, for an administrator.",
222
+ about:
223
+ "What the workspace is called; who sees whom on the People page and in the pickers (everyone; or shared-projects: a member sees only the people they share a project with, an administrator everyone; a project open to the workspace has everyone in it, so a member of one sees everyone); the day the calendars begin a week on (auto: the system default, as each device has it); the workspace's time zone, an IANA name such as Asia/Seoul, in which this command prints moments as ISO 8601 with its offset, as the login kept it at login or whoami, or none, which leaves them as the API gives them, in UTC; where the organization's skills come from; and the most days a token may last. edit changes any of the first three, for an administrator; --json leaves moments as the API gives them. pack-token is the SkillCDN token the organization's pack is read with, by every project that names no address of its own and holds no token of its own (ADR-0036), for an administrator: with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody; with --stdin, a token made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away.",
224
+ refusals: [
225
+ "auth.forbidden: only an administrator changes the workspace's settings or its token",
226
+ "request.invalid: the time zone is not a name the console knows",
227
+ "workspace.pack_token_refused: SkillCDN does not serve the organization's pack to the token, or the console names no address for the organization's skills",
228
+ "workspace.pack_connect_refused: the console names no address for the organization's skills, the pack is on the console's machine, or SkillCDN refused to begin",
229
+ "workspace.pack_token_unsupported, workspace.pack_connect_unsupported: the console keeps no secret to seal a token with (AUTH_SECRET), or reaches no deployment",
230
+ ],
209
231
  },
210
232
  person: {
211
233
  usage:
@@ -224,12 +246,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
224
246
  },
225
247
  project: {
226
248
  usage:
227
- 'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --stdin | project pack-token <key> --remove',
249
+ 'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --connect | project pack-token <key> --stdin | project pack-token <key> --remove',
228
250
  overview:
229
- "project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --stdin|--remove",
230
- synopsis: "One project in full; or a project made, changed, given its mark or its pack token.",
251
+ "project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --connect|--stdin|--remove",
252
+ synopsis:
253
+ "One project in full; or a project made, changed, given its mark or its SkillCDN token.",
231
254
  about:
232
- "One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its pack token (ADR-0028), the token a person made at SkillCDN for a private pack, read from standard input and never an argument, which the console keeps sealed to read the pack with, or taken away. The organization's skills are the project's unless --skills names an address of its own.",
255
+ "One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its SkillCDN token (pack-token, ADR-0028), which the console keeps sealed to read a private pack with, and which a project that reads the organization's pack may leave to the workspace's (console workspace pack-token): with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody (ADR-0036); with --stdin, a token a person made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away. The organization's skills are the project's unless --skills names an address of its own.",
233
256
  limits: [
234
257
  `key: ${KEY}, up to ${MAX_PROJECT_KEY_LENGTH} characters; it does not change once made`,
235
258
  `name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
@@ -243,13 +266,16 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
243
266
  "project.invalid_address: the skills address is not one as SkillCDN spells it",
244
267
  "project.invalid_icon: the file is not an image of those kinds, by its bytes",
245
268
  "request.too_large: the image is over the limit",
269
+ "project.pack_connect_unsupported: the console reaches no deployment, or keeps no secret to seal a token with",
270
+ "project.pack_connect_refused: the project names no pack, the pack is on the console's machine, or SkillCDN refused to begin",
271
+ "skills.unavailable: SkillCDN could not be reached; try again later",
246
272
  ],
247
273
  },
248
274
  members: {
249
275
  usage: "members",
250
- synopsis: "Those listed in the project, with what each is in it.",
276
+ synopsis: "The members of the project, with what each is in it.",
251
277
  about:
252
- "Those listed in the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. A project open to the workspace has everyone of it as a member besides, and an administrator owns every project.",
278
+ "The members of the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. In a private project, those listed; in one open to the workspace, everyone of it, an owner where listed as one or an administrator, a member otherwise. An administrator owns every project.",
253
279
  },
254
280
  member: {
255
281
  usage:
@@ -257,11 +283,12 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
257
283
  synopsis:
258
284
  "Lists a person in the project, changes what they are in it, or takes them off; for an owner.",
259
285
  about:
260
- "Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
286
+ "Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. In a project open to the workspace everyone is a member already: only an owner is listed (member add --role owner, or member role <login> owner), and an owner made a member again stays the member everyone is. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
261
287
  refusals: [
262
288
  "auth.forbidden: only an owner of the project lists its members",
263
289
  "member.invalid_person: the person is not of the workspace",
264
290
  "member.exists: the person is listed already; member role changes what they are",
291
+ "member.open_project: the project is open to the workspace, so everyone is a member already; list an owner with --role owner",
265
292
  "member.not_found: the person is not listed in the project",
266
293
  ],
267
294
  },
@@ -328,7 +355,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
328
355
  usage: "take <number|id> [--agent <name>]",
329
356
  synopsis: "Takes a task: a run begins, and the task is in progress and the person's.",
330
357
  about:
331
- "Takes a task: a run begins, and the task is in progress and the person's; the rules of the board are printed with the task. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
358
+ "Takes a task: a run begins, and the task is in progress and the person's; the run's id and the task's id are printed on a line of their own, then the task, then the rules of the board. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
332
359
  limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
333
360
  refusals: [
334
361
  "task.not_found: no task has that number or id",
@@ -604,7 +631,7 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
604
631
  {
605
632
  title:
606
633
  "Configuring, as the person: what an administrator or an owner does on the pages, their agent does here, named on it",
607
- commands: ["people", "person", "project", "members", "member", "category"],
634
+ commands: ["workspace", "people", "person", "project", "members", "member", "category"],
608
635
  },
609
636
  ];
610
637
 
package/src/cli/main.ts CHANGED
@@ -60,6 +60,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
60
60
  exists: existsSync,
61
61
  }),
62
62
  notices: createNoticeStore(configDir),
63
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
63
64
  skillsDir: existsSync(skillsDir) ? skillsDir : undefined,
64
65
  listen: listenOn,
65
66
  });