@skillcdn/console 0.1.24 → 0.3.1

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 (156) hide show
  1. package/README.md +8 -7
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +248 -92
  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 +13 -28
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +74 -274
  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 +36 -19
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/pack.d.ts +46 -0
  26. package/dist/cli/pack.d.ts.map +1 -0
  27. package/dist/cli/pack.js +250 -0
  28. package/dist/cli/pack.js.map +1 -0
  29. package/dist/client.d.ts +26 -1
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +14 -1
  32. package/dist/client.js.map +1 -1
  33. package/dist/compat.d.ts +1 -1
  34. package/dist/compat.js +1 -1
  35. package/dist/components/calendar.d.ts +5 -1
  36. package/dist/components/calendar.d.ts.map +1 -1
  37. package/dist/components/calendar.js +3 -3
  38. package/dist/components/calendar.js.map +1 -1
  39. package/dist/components/decision-list.d.ts +13 -1
  40. package/dist/components/decision-list.d.ts.map +1 -1
  41. package/dist/components/decision-list.js +74 -29
  42. package/dist/components/decision-list.js.map +1 -1
  43. package/dist/components/event-feed.d.ts.map +1 -1
  44. package/dist/components/event-feed.js +2 -0
  45. package/dist/components/event-feed.js.map +1 -1
  46. package/dist/components/pack-connect.d.ts +23 -0
  47. package/dist/components/pack-connect.d.ts.map +1 -0
  48. package/dist/components/pack-connect.js +86 -0
  49. package/dist/components/pack-connect.js.map +1 -0
  50. package/dist/components/pack-token.d.ts +27 -0
  51. package/dist/components/pack-token.d.ts.map +1 -0
  52. package/dist/components/pack-token.js +11 -0
  53. package/dist/components/pack-token.js.map +1 -0
  54. package/dist/components/paged.d.ts +24 -0
  55. package/dist/components/paged.d.ts.map +1 -1
  56. package/dist/components/paged.js +40 -4
  57. package/dist/components/paged.js.map +1 -1
  58. package/dist/components/projects.d.ts +13 -0
  59. package/dist/components/projects.d.ts.map +1 -1
  60. package/dist/components/projects.js +37 -5
  61. package/dist/components/projects.js.map +1 -1
  62. package/dist/components/skills.d.ts.map +1 -1
  63. package/dist/components/skills.js +8 -4
  64. package/dist/components/skills.js.map +1 -1
  65. package/dist/components/ui.d.ts +2 -0
  66. package/dist/components/ui.d.ts.map +1 -1
  67. package/dist/components/ui.js +7 -2
  68. package/dist/components/ui.js.map +1 -1
  69. package/dist/components/work.d.ts +3 -1
  70. package/dist/components/work.d.ts.map +1 -1
  71. package/dist/components/work.js +1 -1
  72. package/dist/components/work.js.map +1 -1
  73. package/dist/components/workspace.d.ts +46 -0
  74. package/dist/components/workspace.d.ts.map +1 -0
  75. package/dist/components/workspace.js +124 -0
  76. package/dist/components/workspace.js.map +1 -0
  77. package/dist/console.css +268 -5
  78. package/dist/console.d.ts.map +1 -1
  79. package/dist/console.js +62 -23
  80. package/dist/console.js.map +1 -1
  81. package/dist/data.d.ts +16 -1
  82. package/dist/data.d.ts.map +1 -1
  83. package/dist/data.js +64 -2
  84. package/dist/data.js.map +1 -1
  85. package/dist/dates.d.ts +12 -2
  86. package/dist/dates.d.ts.map +1 -1
  87. package/dist/dates.js +20 -2
  88. package/dist/dates.js.map +1 -1
  89. package/dist/i18n/en.d.ts +66 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +80 -8
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +75 -7
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +5 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/router.d.ts +10 -5
  101. package/dist/router.d.ts.map +1 -1
  102. package/dist/router.js +24 -9
  103. package/dist/router.js.map +1 -1
  104. package/dist/routes.d.ts +4 -0
  105. package/dist/routes.d.ts.map +1 -1
  106. package/dist/routes.js +4 -0
  107. package/dist/routes.js.map +1 -1
  108. package/dist/schemas.d.ts +500 -158
  109. package/dist/schemas.d.ts.map +1 -1
  110. package/dist/schemas.js +169 -165
  111. package/dist/schemas.js.map +1 -1
  112. package/dist/vocabulary.d.ts +19 -1
  113. package/dist/vocabulary.d.ts.map +1 -1
  114. package/dist/vocabulary.js +16 -0
  115. package/dist/vocabulary.js.map +1 -1
  116. package/docs/specs/cli.md +19 -14
  117. package/docs/specs/rest.md +34 -19
  118. package/package.json +2 -1
  119. package/skills/building-a-console/SKILL.md +5 -5
  120. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  121. package/skills/deploying-a-console/SKILL.md +9 -8
  122. package/skills/working-the-board/SKILL.md +5 -5
  123. package/skills/working-the-board/references/setting-up.md +1 -1
  124. package/skills/working-the-board/references/terms.md +2 -0
  125. package/skills/working-the-board/references/the-sentence.md +1 -1
  126. package/src/api.ts +18 -7
  127. package/src/cli/cli.ts +345 -109
  128. package/src/cli/credentials.ts +7 -0
  129. package/src/cli/directory.ts +9 -0
  130. package/src/cli/format.ts +82 -341
  131. package/src/cli/help.ts +43 -19
  132. package/src/cli/pack.ts +356 -0
  133. package/src/client.ts +84 -0
  134. package/src/compat.ts +1 -1
  135. package/src/components/calendar.tsx +8 -2
  136. package/src/components/decision-list.tsx +227 -94
  137. package/src/components/event-feed.tsx +2 -0
  138. package/src/components/pack-connect.tsx +179 -0
  139. package/src/components/pack-token.tsx +74 -0
  140. package/src/components/paged.tsx +72 -3
  141. package/src/components/projects.tsx +108 -9
  142. package/src/components/skills.tsx +51 -33
  143. package/src/components/ui.tsx +16 -3
  144. package/src/components/work.tsx +4 -1
  145. package/src/components/workspace.tsx +298 -0
  146. package/src/console.tsx +176 -29
  147. package/src/data.ts +89 -2
  148. package/src/dates.ts +23 -2
  149. package/src/i18n/en.ts +94 -8
  150. package/src/i18n/ko.ts +84 -7
  151. package/src/index.ts +25 -1
  152. package/src/router.ts +37 -14
  153. package/src/routes.ts +4 -0
  154. package/src/schemas.ts +202 -189
  155. package/src/styles/console.css +268 -5
  156. package/src/vocabulary.ts +25 -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
@@ -15,17 +15,11 @@ import type {
15
15
  RestProjectRules,
16
16
  RestRules,
17
17
  RestRun,
18
- RestSkillBrowse,
19
- RestSkillBrowseEntry,
20
- RestSkillFile,
21
- RestSkillFind,
22
- RestSkillLoad,
23
- RestSkillPage,
24
- RestSkills,
25
18
  RestTask,
26
19
  RestVersion,
27
20
  RestVersionSummary,
28
21
  RestWork,
22
+ RestWorkspace,
29
23
  } from "../schemas.js";
30
24
 
31
25
  // What the command prints: one line per thing in a list, a few lines for one thing in full.
@@ -34,6 +28,48 @@ import type {
34
28
 
35
29
  const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? "" : "s"}`;
36
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
+
37
73
  const indent = (text: string): string =>
38
74
  text
39
75
  .trimEnd()
@@ -61,7 +97,7 @@ export function formatProject(project: RestProject): string {
61
97
  `project ${project.key}: ${project.name}`,
62
98
  `visibility: ${project.visibility} role: ${project.role} skills: ${project.skillsAddress ?? "the organization's"}`,
63
99
  `color: ${project.color ?? "none"} symbol: ${project.symbol ?? "none"} image: ${project.icon === null ? "none" : project.icon.contentType}`,
64
- `pack token: ${project.packToken === null ? "none" : `set ${project.packToken.setAt}`}`,
100
+ `pack token: ${project.packToken === null ? "none" : `set ${moment(project.packToken.setAt)}`}`,
65
101
  ];
66
102
  if (project.description.trim().length > 0) {
67
103
  lines.push("", project.description.trimEnd());
@@ -85,9 +121,25 @@ export function formatListingLine(listing: RestListing): string {
85
121
  return `${listing.login} (let in at ${HOST_NAMES[listing.host]}, not signed in yet)`;
86
122
  }
87
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
+
88
140
  /** One member of a project: their login, what they are in it, and since when. */
89
141
  export function formatMemberLine(member: RestMember): string {
90
- 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)})`;
91
143
  }
92
144
 
93
145
  /** The line under a page of a listing that has more after it: how to ask for the next page. */
@@ -128,9 +180,9 @@ function scheduleLines(task: RestTask): string[] {
128
180
  lines.push(plan.join(" "));
129
181
  }
130
182
  const happened = [
131
- task.startedAt === null ? undefined : `started: ${task.startedAt}`,
132
- task.doneAt === null ? undefined : `done: ${task.doneAt}`,
133
- task.archivedAt === null ? undefined : `archived: ${task.archivedAt}`,
183
+ task.startedAt === null ? undefined : `started: ${moment(task.startedAt)}`,
184
+ task.doneAt === null ? undefined : `done: ${moment(task.doneAt)}`,
185
+ task.archivedAt === null ? undefined : `archived: ${moment(task.archivedAt)}`,
134
186
  ].filter((part) => part !== undefined);
135
187
  if (happened.length > 0) {
136
188
  lines.push(happened.join(" "));
@@ -188,7 +240,7 @@ export function formatTask(
188
240
  export function formatComment(comment: RestComment): string {
189
241
  const who =
190
242
  comment.agent === null ? comment.author.login : `${comment.author.login} as ${comment.agent}`;
191
- const lines = [` ${who}, ${comment.createdAt}, comment ${comment.id}:`];
243
+ const lines = [` ${who}, ${moment(comment.createdAt)}, comment ${comment.id}:`];
192
244
  if (comment.body.trim().length > 0) {
193
245
  lines.push(indent(comment.body));
194
246
  }
@@ -206,7 +258,7 @@ export function formatRunLine(run: RestRun): string {
206
258
  notes.push(`waiting for decision ${run.waitingFor}`);
207
259
  }
208
260
  if (run.endedAt === null) {
209
- notes.push(`last heard ${run.lastHeardAt}`);
261
+ notes.push(`last heard ${moment(run.lastHeardAt)}`);
210
262
  }
211
263
  return `${run.id} ${run.status} ${run.agent} for ${run.person.login} task #${run.taskNumber} (${notes.join(", ")})`;
212
264
  }
@@ -215,7 +267,7 @@ export function formatRunLine(run: RestRun): string {
215
267
  export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string): string {
216
268
  const lines = [
217
269
  formatRunLine(run),
218
- `started: ${run.startedAt}${run.endedAt === null ? "" : ` ended: ${run.endedAt}`}`,
270
+ `started: ${moment(run.startedAt)}${run.endedAt === null ? "" : ` ended: ${moment(run.endedAt)}`}`,
219
271
  ];
220
272
  if (run.summary !== null) {
221
273
  lines.push("", "summary:", indent(run.summary));
@@ -223,7 +275,7 @@ export function formatRun(run: RestRun, fileUrl?: (artifactId: string) => string
223
275
  if (run.reports.length > 0) {
224
276
  lines.push("", "reports:");
225
277
  for (const report of run.reports) {
226
- lines.push(` [${report.createdAt}]`, indent(report.body));
278
+ lines.push(` [${moment(report.createdAt)}]`, indent(report.body));
227
279
  }
228
280
  }
229
281
  if (run.artifacts.length > 0) {
@@ -247,77 +299,13 @@ export function formatArtifact(
247
299
  return artifact.label === null ? what : `${artifact.label}: ${what}`;
248
300
  }
249
301
 
250
- const SKILLS_WORDS: Readonly<Record<Exclude<RestSkills["status"], "none" | "ready">, string>> = {
251
- indexing: "SkillCDN is still indexing the repository; ask again in a moment.",
252
- failed: "SkillCDN could not index the repository.",
253
- not_found:
254
- "SkillCDN does not serve this address to the console: nothing is there, or the pack is private and the project holds no pack token for it (console project pack-token <key> --stdin, for an owner).",
255
- token_invalid:
256
- "The pack token the project holds no longer works at SkillCDN: an owner sets a new one (console project pack-token <key> --stdin).",
257
- unavailable: "SkillCDN could not be reached; ask again later.",
258
- };
259
-
260
- /** A count with its noun: `1 skill`, `2 skills`. */
261
- const counted = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? "" : "s"}`;
262
-
263
- /**
264
- * The organization's skills: where they are, the pack by name and by count, each skill with
265
- * what an agent loads it by, where a person connects an agent that has no connection to the
266
- * address, and where the console's own skills are on this machine, when the command knows.
267
- */
268
- export function formatSkills(skills: RestSkills, ownSkills?: string): string {
269
- const own =
270
- ownSkills === undefined
271
- ? []
272
- : ["", `The console's own skills, for working the board: ${ownSkills}`];
273
- if (skills.status === "none" || skills.address === null || skills.page === null) {
274
- return ["No skills address: neither this project nor the console names one.", ...own].join(
275
- "\n",
276
- );
277
- }
278
- const head = `skills at ${skills.address}, served by ${skills.source}: ${skills.status}`;
279
- if (skills.status !== "ready") {
280
- return [head, SKILLS_WORDS[skills.status], ...own].join("\n");
281
- }
282
- const lines = [head];
283
- if (skills.pack !== undefined) {
284
- const named = [skills.pack.name, skills.pack.description]
285
- .filter((text): text is string => text !== null)
286
- .join(" ");
287
- if (named !== "") {
288
- lines.push(named);
289
- }
290
- }
291
- if (skills.counts !== undefined) {
292
- lines.push(
293
- `${counted(skills.counts.skills, "skill")}, ${counted(skills.counts.documents, "document")}: console skills <folder> shows what a folder holds (. for the root), console skills --search <words> finds skills and documents by words.`,
294
- );
295
- }
296
- if (skills.items.length === 0) {
297
- lines.push("No skills at this address.");
298
- }
299
- // The path, which console skill takes, and the URI beside it, which a host's own connection takes.
300
- for (const skill of skills.items) {
301
- lines.push(
302
- `${skill.name} ${skill.description}`,
303
- ` ${skill.path}${skill.uri === null ? "" : ` ${skill.uri}`}`,
304
- );
305
- }
306
- lines.push(
307
- "",
308
- `Read a skill whole with console skill <path>, its rules first, and a file of the pack with console skill --file <path>; or load it by its URI through your own SkillCDN connection to this address, which a person makes at ${skills.page}. --json adds the page of each, for a person.`,
309
- ...own,
310
- );
311
- return lines.join("\n");
312
- }
313
-
314
302
  /** One half of the rules: where it comes from, and the text as it stands. */
315
303
  function formatRulesHalf(where: "the workspace" | "this project", rules: RestRules): string {
316
304
  const page = rules.page;
317
305
  const facts =
318
306
  page === null
319
307
  ? undefined
320
- : `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${page.updatedAt}`;
308
+ : `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${moment(page.updatedAt)}`;
321
309
  if (rules.source === "none" || rules.body === null) {
322
310
  return `Rules of ${where}: none of its own; the workspace's hold.`;
323
311
  }
@@ -417,19 +405,25 @@ export function formatDecisionLine(decision: RestDecision): string {
417
405
  return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
418
406
  }
419
407
 
408
+ /** The options, one a line; the person's own answer, when they wrote one, is the last and says so. */
420
409
  export function formatOptions(decision: RestDecision): string {
421
- return decision.options.map((option) => ` ${option.id}) ${option.label}`).join("\n");
410
+ return decision.options
411
+ .map(
412
+ (option) =>
413
+ ` ${option.id}) ${option.label}${option.own === true ? " (the person's own answer, not one given)" : ""}`,
414
+ )
415
+ .join("\n");
422
416
  }
423
417
 
424
- /** The answer, with who gave it; or that there is none yet. */
418
+ /** The answer, with who gave it, and that it is in their own words when it is; or that there is none yet. */
425
419
  export function formatAnswer(decision: RestDecision): string {
426
420
  const { answer } = decision;
427
421
  if (answer === null) {
428
422
  return "waits for a person.";
429
423
  }
430
- const label = decision.options.find((option) => option.id === answer.option)?.label;
424
+ const chosen = decision.options.find((option) => option.id === answer.option);
431
425
  const lines = [
432
- `Answered: ${answer.option}) ${label ?? answer.option}, by ${answer.by.login} at ${answer.at}`,
426
+ `Answered: ${answer.option}) ${chosen?.label ?? answer.option}${chosen?.own === true ? ", in their own words" : ""}, by ${answer.by.login} at ${answer.at}`,
433
427
  ];
434
428
  if (answer.note !== null && answer.note.length > 0) {
435
429
  lines.push(`Note: ${answer.note}`);
@@ -462,7 +456,7 @@ export function formatDocumentLine(document: RestDocumentSummary): string {
462
456
  if (document.archivedAt !== null) {
463
457
  notes.push("archived");
464
458
  }
465
- return `${document.path} ${document.title} (${notes.join(", ")}; ${document.updatedAt})`;
459
+ return `${document.path} ${document.title} (${notes.join(", ")}; ${moment(document.updatedAt)})`;
466
460
  }
467
461
 
468
462
  /** A folder's folders and pages, or the pages a search found. */
@@ -484,7 +478,7 @@ export function formatDocument(
484
478
  ): string {
485
479
  const lines = [
486
480
  `${document.path}: ${document.title}`,
487
- `version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${document.updatedAt}${document.archivedAt === null ? "" : `; archived ${document.archivedAt}`}`,
481
+ `version ${document.version}, by ${writer(document.updatedBy, document.agent)} at ${moment(document.updatedAt)}${document.archivedAt === null ? "" : `; archived ${moment(document.archivedAt)}`}`,
488
482
  `id: ${document.id}`,
489
483
  ];
490
484
  if (document.body.trim().length > 0) {
@@ -518,7 +512,7 @@ export function formatDocument(
518
512
  }
519
513
 
520
514
  export function formatVersionLine(version: RestVersionSummary): string {
521
- return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${version.createdAt})`;
515
+ return `v${version.number} ${version.title} (by ${writer(version.author, version.agent)}; ${moment(version.createdAt)})`;
522
516
  }
523
517
 
524
518
  /** One version of a document, with its body as it was. */
@@ -531,256 +525,3 @@ export function formatVersion(path: string, version: RestVersion): string {
531
525
  }
532
526
 
533
527
  /** The text of one path across the pages a skill came in: the fragments joined in order. */
534
- function joinTexts(
535
- pages: readonly RestSkillPage[],
536
- pick: (page: RestSkillPage) => readonly { readonly path: string; readonly body: string }[],
537
- ): { readonly path: string; readonly body: string }[] {
538
- const joined = new Map<string, string>();
539
- for (const page of pages) {
540
- for (const text of pick(page)) {
541
- joined.set(text.path, (joined.get(text.path) ?? "") + text.body);
542
- }
543
- }
544
- return [...joined.entries()].map(([path, body]) => ({ path, body }));
545
- }
546
-
547
- const NO_ADDRESS = "No skills address: neither this project nor the console names one.";
548
-
549
- /**
550
- * One skill, as load_skill lays it out for a model (ADR-0028): the rules from the repository's
551
- * root to the nearest manifest, the body, the included files, each under a line that names its
552
- * path; then the files the skill has and what it refers to. A skill described only is named and
553
- * pointed at, and nothing of its body is shown.
554
- */
555
- export function formatSkillLoad(loaded: RestSkillLoad, pages: readonly RestSkillPage[]): string {
556
- if (loaded.status === "none" || loaded.address === null) {
557
- return NO_ADDRESS;
558
- }
559
- const head = `skill at ${loaded.address}, served by ${loaded.source}: ${loaded.status}`;
560
- if (loaded.status !== "ready") {
561
- return [head, SKILLS_WORDS[loaded.status]].join("\n");
562
- }
563
- const [first] = pages;
564
- if (first === undefined) {
565
- return [head, "Nothing was read."].join("\n");
566
- }
567
- const last = pages[pages.length - 1] ?? first;
568
- const lines = [
569
- `${first.name} ${first.description}`,
570
- `skill ${first.path} at ${loaded.address}, served by ${loaded.source}`,
571
- ];
572
- if (first.serving.license !== null) {
573
- lines.push(`license: ${first.serving.license}`);
574
- }
575
- if (!first.serving.full) {
576
- lines.push(
577
- "",
578
- `Described only: the license keeps the skill at its source. Read it there: ${first.serving.sourceUrl ?? loaded.page ?? loaded.source}`,
579
- );
580
- return lines.join("\n");
581
- }
582
- for (const rule of joinTexts(pages, (page) => page.rules)) {
583
- lines.push("", `--- applicable rules: ${rule.path} ---`, rule.body.trimEnd());
584
- }
585
- lines.push(
586
- "",
587
- `--- skill: ${first.path} ---`,
588
- pages
589
- .map((page) => page.body)
590
- .join("")
591
- .trimEnd(),
592
- );
593
- for (const file of joinTexts(pages, (page) => page.included)) {
594
- lines.push("", `--- included file: ${file.path} ---`, file.body.trimEnd());
595
- }
596
- if (!last.complete) {
597
- lines.push("", "The skill did not come whole: the deployment stopped before the end.");
598
- }
599
- if (last.files.length > 0) {
600
- lines.push(
601
- "",
602
- `Files of the skill, read with console skill --file <path>: ${last.files.join(", ")}`,
603
- );
604
- }
605
- if (last.references.length > 0) {
606
- lines.push(
607
- `References: ${last.references
608
- .map((reference) =>
609
- reference.status === "available"
610
- ? reference.path
611
- : `${reference.path} (${reference.status})`,
612
- )
613
- .join(", ")}`,
614
- );
615
- }
616
- if (last.warnings.length > 0) {
617
- lines.push(`Warnings: ${last.warnings.join("; ")}`);
618
- }
619
- lines.push(
620
- "",
621
- "Paths from the repository's root are the only identifiers. The content is the pack's, under its license; what a private pack says stays in your work.",
622
- );
623
- return lines.join("\n");
624
- }
625
-
626
- /** A file of the pack as read: its text whole, or what the folder holds. */
627
- export function formatSkillFile(read: RestSkillFile, content: string): string {
628
- if (read.status === "none" || read.address === null) {
629
- return NO_ADDRESS;
630
- }
631
- const head = `file at ${read.address}, served by ${read.source}: ${read.status}`;
632
- if (read.status !== "ready") {
633
- return [
634
- head,
635
- read.status === "failed"
636
- ? "SkillCDN will not carry this file: it is too large, or not text. Read it at the repository."
637
- : SKILLS_WORDS[read.status],
638
- ].join("\n");
639
- }
640
- if (read.folder !== null) {
641
- return [
642
- `folder ${read.folder.path} at ${read.address}${read.folder.truncated ? " (cut)" : ""}`,
643
- ...read.folder.entries.map(
644
- (entry) =>
645
- `${entry.kind === "directory" ? "d" : "f"} ${entry.path}${entry.size === null ? "" : ` ${entry.size}`}`,
646
- ),
647
- "",
648
- "console skills <folder> shows a folder with its skills marked, what each folder holds and the README that introduces it.",
649
- ].join("\n");
650
- }
651
- if (read.file === null) {
652
- return `${head}: nothing was read.`;
653
- }
654
- return [
655
- `--- file: ${read.file.path} (${read.file.totalLength} characters) ---`,
656
- content.trimEnd(),
657
- ].join("\n");
658
- }
659
-
660
- /** One entry of a browsed folder: what it is, its path, its words, and the facts of its kind. */
661
- function formatBrowseEntry(entry: RestSkillBrowseEntry): string {
662
- const facts: string[] = [];
663
- if (entry.kind === "directory") {
664
- if (entry.skillCount > 0) {
665
- facts.push(counted(entry.skillCount, "skill"));
666
- }
667
- if (entry.documentCount > 0) {
668
- facts.push(counted(entry.documentCount, "document"));
669
- }
670
- if (entry.overviewPath !== null) {
671
- facts.push(`README ${entry.overviewPath}`);
672
- }
673
- }
674
- if (entry.kind === "skill") {
675
- if (entry.license !== null) {
676
- facts.push(entry.license);
677
- }
678
- if (entry.describedOnly) {
679
- facts.push("described only");
680
- }
681
- }
682
- if (entry.kind === "file" && entry.size !== null) {
683
- facts.push(`${entry.size} bytes`);
684
- }
685
- const words = [entry.name, entry.description]
686
- .filter((text): text is string => text !== null && text !== "")
687
- .join(" ");
688
- const mark = entry.kind === "skill" ? "skill" : entry.kind === "directory" ? "d" : "f";
689
- return `${mark} ${entry.path}${words === "" ? "" : ` ${words}`}${facts.length === 0 ? "" : ` (${facts.join("; ")})`}`;
690
- }
691
-
692
- /**
693
- * What a folder of the pack holds, as browse_repo lays it out: the README that introduces the
694
- * folder, then its skills, its folders with what each holds, and its files, across the pages the
695
- * listing came in.
696
- */
697
- export function formatSkillBrowse(
698
- browsed: RestSkillBrowse,
699
- folders: readonly NonNullable<RestSkillBrowse["folder"]>[],
700
- ): string {
701
- if (browsed.status === "none" || browsed.address === null) {
702
- return NO_ADDRESS;
703
- }
704
- const head = `folder at ${browsed.address}, served by ${browsed.source}: ${browsed.status}`;
705
- if (browsed.status !== "ready") {
706
- return [head, SKILLS_WORDS[browsed.status]].join("\n");
707
- }
708
- const [first] = folders;
709
- if (first === undefined) {
710
- return [head, "Nothing was read."].join("\n");
711
- }
712
- const last = folders[folders.length - 1] ?? first;
713
- const lines = [`folder ${first.path === "" ? "/" : first.path} at ${browsed.address}`];
714
- if (first.overview !== null) {
715
- const words = [first.overview.title, first.overview.description]
716
- .filter((text): text is string => text !== null && text !== "")
717
- .join(" ");
718
- lines.push(` ${words === "" ? "" : `${words} `}(README: ${first.overview.path})`);
719
- }
720
- const entries = folders.flatMap((folder) => folder.entries);
721
- if (entries.length === 0) {
722
- lines.push("Nothing in this folder.");
723
- }
724
- for (const entry of entries) {
725
- lines.push(formatBrowseEntry(entry));
726
- }
727
- if (last.nextCursor !== null) {
728
- lines.push("More entries: the listing stopped before the end of the folder.");
729
- }
730
- lines.push(
731
- "",
732
- "Read a skill with console skill <path>; a file, or a README named above, with console skill --file <path>; a folder with console skills <folder>.",
733
- );
734
- return lines.join("\n");
735
- }
736
-
737
- /**
738
- * What words found in the pack, as search_repo lays it out: skills with the files of theirs that
739
- * matched, documents on their own, in relevance order; words, not meaning.
740
- */
741
- export function formatSkillFind(found: RestSkillFind): string {
742
- if (found.status === "none" || found.address === null) {
743
- return NO_ADDRESS;
744
- }
745
- const head = `search at ${found.address}, served by ${found.source}: ${found.status}`;
746
- if (found.status !== "ready") {
747
- return [head, SKILLS_WORDS[found.status]].join("\n");
748
- }
749
- if (found.found === null) {
750
- return [head, "Nothing was read."].join("\n");
751
- }
752
- const { query, path, items, nextCursor } = found.found;
753
- const where = path === null || path === "" ? "" : ` within ${path}`;
754
- const lines = [
755
- `found for "${query}"${where} at ${found.address}: ${counted(items.length, "result")}`,
756
- ];
757
- for (const item of items) {
758
- if (item.kind === "skill") {
759
- lines.push(
760
- `skill ${item.path} ${item.name} ${item.description}${item.describedOnly ? " (described only)" : ""}`,
761
- );
762
- for (const file of item.files) {
763
- lines.push(` file ${file.path}${file.title === null ? "" : ` ${file.title}`}`);
764
- }
765
- if (item.moreFiles > 0) {
766
- lines.push(` and ${counted(item.moreFiles, "more file")}`);
767
- }
768
- } else {
769
- lines.push(
770
- `document ${item.path}${item.title === null ? "" : ` ${item.title}`}${item.summary === null ? "" : ` ${item.summary}`}`,
771
- );
772
- }
773
- }
774
- if (nextCursor !== null) {
775
- lines.push(
776
- "More matched: narrow the words, or search within a folder (console skills <folder> --search <words>).",
777
- );
778
- }
779
- if (items.length > 0) {
780
- lines.push(
781
- "",
782
- "Read a skill with console skill <path>, a file or a document with console skill --file <path>.",
783
- );
784
- }
785
- return lines.join("\n");
786
- }