@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
package/dist/cli/cli.js CHANGED
@@ -6,12 +6,14 @@ import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
6
6
  import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
7
7
  import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
8
8
  import { hostOfLogin, } from "../schemas.js";
9
- import { COLOR_NAMES, GOAL_STATES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
9
+ import { COLOR_NAMES, GOAL_STATES, PEOPLE_VISIBILITIES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, WEEK_STARTS, } from "../vocabulary.js";
10
10
  import { loadWebRoot, WebRootError } from "../web.js";
11
11
  import { CredentialsError, EMPTY_CREDENTIALS, loginsAt, resolveLogin, withLogin, withoutLogin, } from "./credentials.js";
12
- import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
12
+ import { THE_SENTENCE } from "./directory.js";
13
+ import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, formatWorkspace, moment, setMomentZone, } from "./format.js";
13
14
  import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
14
15
  import { describeInstall, updateCommandFor } from "./install.js";
16
+ import { bodyOfFile, bodyOfFolder, bodyOfSkill, formatSkillBrowse, formatSkillFile, formatSkillFind, formatSkillLoad, formatSkills, joinFile, joinFolder, joinSkill, skillPathIn, summaryOf, } from "./pack.js";
15
17
  import { createServeHandler, DEFAULT_SERVE_PORT } from "./serve.js";
16
18
  // The `console` command (docs/specs/cli.md): the agent's side of the console. A thin client of
17
19
  // the REST API with no logic of its own: every command is one or two requests and the words to
@@ -148,6 +150,12 @@ async function open(io, values) {
148
150
  throw failed(wordsOf(resolved));
149
151
  }
150
152
  const { url, token } = resolved;
153
+ // Moments are printed in the workspace's time zone as the login kept it (ADR-0035); the
154
+ // environment's pair keeps nothing, and they stay as the API gives them.
155
+ setMomentZone(resolved.via === "login"
156
+ ? credentials?.logins.find((kept) => kept.url === url && kept.account === resolved.account)
157
+ ?.timeZone
158
+ : undefined);
151
159
  const client = clientOf(io, url, token);
152
160
  const projectKey = async () => text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
153
161
  return {
@@ -290,15 +298,20 @@ async function personIdOf(session, value) {
290
298
  }
291
299
  return (await personOf(session, value)).id;
292
300
  }
293
- /** The person a login names, among the workspace's; the logins are as people sign in. */
301
+ /**
302
+ * The person a login names, among the workspace's, found by their exact login as people sign in
303
+ * (ADR-0035): whom the console shows the person, and, for an owner of a project, anyone of the
304
+ * workspace, so that someone the People page does not show can still be listed.
305
+ */
294
306
  async function personOf(session, login) {
295
- const { items } = await session.client.people();
296
- const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
307
+ const found = await session.client.findPerson(login);
297
308
  if (found === undefined) {
298
- throw misuse(`Nobody of the workspace is called ${login}. console people lists everyone, by login.`);
309
+ throw misuse(`Nobody of the workspace is called ${login}, or they are not yours to see. console people lists whom you see, by login.`);
299
310
  }
300
311
  return found;
301
312
  }
313
+ /** Who sees whom, as the command takes it: with a hyphen or an underscore. */
314
+ const peopleVisibilityOf = (value) => wordOf(value.replaceAll("-", "_"), PEOPLE_VISIBILITIES, "who sees whom");
302
315
  /** One word of a vocabulary, or the refusal to understand. */
303
316
  function wordOf(value, words, what) {
304
317
  const found = words.find((word) => word === value);
@@ -412,7 +425,14 @@ async function keep(io, url, token, agent, asDefault) {
412
425
  .giveUpToken()
413
426
  .catch(() => undefined);
414
427
  }
415
- const next = withLogin(credentials, { url, account: person.login, token, agent, since: new Date(io.now()).toISOString() }, { default: asDefault });
428
+ const next = withLogin(credentials, {
429
+ url,
430
+ account: person.login,
431
+ token,
432
+ agent,
433
+ since: new Date(io.now()).toISOString(),
434
+ timeZone: me.workspace.timeZone ?? undefined,
435
+ }, { default: asDefault });
416
436
  const kept = await io.store.save(next);
417
437
  const standing = next.default === url ? ", the default console here" : "";
418
438
  io.stdout(`${agent === undefined ? "Signed in" : `Connected as "${agent}"`} to ${me.workspace.name} at ${url} as ${person.login} (${person.role})${standing}. The token is kept in ${kept}.\nNext, say which project this directory works in: console use <key> (console projects lists them).\n`);
@@ -556,6 +576,21 @@ function agentOf(env) {
556
576
  }
557
577
  return undefined;
558
578
  }
579
+ /**
580
+ * Keeps the workspace's time zone on the login the command used (ADR-0035), so that moments are
581
+ * printed in it from now on without asking; the environment's pair keeps nothing.
582
+ */
583
+ async function rememberZone(io, session, zone) {
584
+ setMomentZone(zone);
585
+ if (session.via !== "login" || session.credentials === undefined) {
586
+ return;
587
+ }
588
+ const kept = session.credentials.logins.find((login) => login.url === session.url && login.account === session.account);
589
+ if (kept === undefined || kept.timeZone === zone) {
590
+ return;
591
+ }
592
+ await io.store.save(withLogin(session.credentials, { ...kept, timeZone: zone }));
593
+ }
559
594
  const whoami = async (args, io) => {
560
595
  const { values } = parse(args, {}, false);
561
596
  const session = await open(io, values);
@@ -564,6 +599,7 @@ const whoami = async (args, io) => {
564
599
  if (person === null) {
565
600
  throw failed("The console does not know this token. Sign in again: console login");
566
601
  }
602
+ await rememberZone(io, session, me.workspace.timeZone ?? undefined);
567
603
  const key = await session.projectKey();
568
604
  const command = parseVersion(io.version);
569
605
  const server = parseVersion(me.console?.version);
@@ -627,7 +663,14 @@ const use = async (args, io) => {
627
663
  const bound = pinUrl
628
664
  ? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
629
665
  : "";
630
- answer(io, session, project, () => `Working in ${project.name} (${project.key})${bound} from this directory on: kept in ${kept}, which is meant to be committed in a private repository and left out of a public one. What its people ask of agents: console rules.`);
666
+ answer(io, session, project, () => [
667
+ `Working in ${project.name} (${project.key})${bound} from this directory on: kept in ${kept}, which is meant to be committed in a private repository and left out of a public one. What its people ask of agents: console rules.`,
668
+ // Nothing sends an agent to the console until the checkout says so; the sentence is the
669
+ // skill's (references/the-sentence.md), printed here as `skillcdn use` prints its own.
670
+ "Then put this sentence into the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), as it is, so that every agent working here is sent to the console:",
671
+ "",
672
+ THE_SENTENCE,
673
+ ].join("\n"));
631
674
  return EXIT.ok;
632
675
  };
633
676
  const tasks = async (args, io) => {
@@ -705,10 +748,16 @@ const categories = async (args, io) => {
705
748
  const MAX_SKILL_PAGES = 64;
706
749
  /** How many results a search answers at most: what the deployment allows on one page. */
707
750
  const MAX_FIND_LIMIT = 25;
751
+ /** The pack of the project as the console lists it, and what its reads are rendered at; none when the pack did not answer. */
752
+ async function packOf(project) {
753
+ const listing = await project.skills();
754
+ return { listing, summary: summaryOf(listing) };
755
+ }
708
756
  const skills = async (args, io) => {
709
757
  const { values, positionals } = parse(args, {
710
758
  search: { type: "string" },
711
759
  limit: { type: "string" },
760
+ cursor: { type: "string" },
712
761
  });
713
762
  if (positionals.length > 1) {
714
763
  throw misuse("Say one folder: console skills [<folder>] [--search <words>]");
@@ -718,8 +767,9 @@ const skills = async (args, io) => {
718
767
  const folder = given === undefined || given === "/" || given === "." ? undefined : given;
719
768
  const words = text(values, "search");
720
769
  const limit = text(values, "limit");
721
- if (words === undefined && limit !== undefined) {
722
- throw misuse("--limit goes with --search: console skills --search <words> [--limit <n>]");
770
+ const cursor = text(values, "cursor");
771
+ if (words === undefined && (limit !== undefined || cursor !== undefined)) {
772
+ throw misuse("--limit and --cursor go with --search: console skills --search <words> [--limit <n>] [--cursor <cursor>]");
723
773
  }
724
774
  const atMost = limit === undefined ? undefined : Number(limit);
725
775
  if (atMost !== undefined &&
@@ -728,37 +778,39 @@ const skills = async (args, io) => {
728
778
  }
729
779
  const session = await open(io, values);
730
780
  const project = await session.project();
781
+ const { listing, summary } = await packOf(project);
782
+ // The pack as it stands; or why it does not answer, which a folder or a search would meet too.
783
+ if (summary === undefined || (words === undefined && given === undefined)) {
784
+ answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
785
+ return EXIT.ok;
786
+ }
731
787
  if (words !== undefined) {
732
- // One page of what the words find; more is said, and narrowed by the agent, not fetched.
733
- const found = await project.skillFind(words, { path: folder, limit: atMost });
734
- answer(io, session, found, () => formatSkillFind(found));
788
+ // One page of what the words find; more is said, and asked for with --cursor.
789
+ const found = await project.skillFind(words, { path: folder, limit: atMost, cursor });
790
+ answer(io, session, found, () => formatSkillFind(found, summary));
735
791
  return EXIT.ok;
736
792
  }
737
- if (given !== undefined) {
738
- // The folder whole: its pages until the end, as the deployment lists them.
739
- const folders = [];
740
- let cursor;
741
- let last;
742
- for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
743
- last = await project.skillBrowse({ path: folder, cursor });
744
- if (last.status !== "ready" || last.folder === null) {
745
- break;
746
- }
747
- folders.push(last.folder);
748
- if (last.folder.nextCursor === null) {
749
- break;
750
- }
751
- cursor = last.folder.nextCursor;
793
+ // The folder whole: its pages until the end, as the pack lists them.
794
+ const pages = [];
795
+ let next;
796
+ let last;
797
+ for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
798
+ last = await project.skillBrowse({ path: folder, cursor: next });
799
+ if (last.read?.status !== "ready") {
800
+ break;
752
801
  }
753
- if (last === undefined) {
754
- throw failed("The folder could not be read.");
802
+ pages.push(last);
803
+ next = last.read.nextCursor ?? undefined;
804
+ if (next === undefined) {
805
+ break;
755
806
  }
756
- const browsed = last;
757
- answer(io, session, { ...browsed, folders }, () => formatSkillBrowse(browsed, folders));
758
- return EXIT.ok;
759
807
  }
760
- const found = await project.skills();
761
- answer(io, session, found, () => formatSkills(found, io.skillsDir));
808
+ if (last === undefined) {
809
+ throw failed("The folder could not be read.");
810
+ }
811
+ const browsed = last;
812
+ const whole = joinFolder(pages, summary);
813
+ answer(io, session, { ...browsed, read: whole === undefined ? null : bodyOfFolder(whole) }, () => formatSkillBrowse(browsed, whole));
762
814
  return EXIT.ok;
763
815
  };
764
816
  const skill = async (args, io) => {
@@ -771,73 +823,62 @@ const skill = async (args, io) => {
771
823
  }
772
824
  const session = await open(io, values);
773
825
  const project = await session.project();
826
+ const { listing, summary } = await packOf(project);
827
+ if (summary === undefined) {
828
+ answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
829
+ return EXIT.ok;
830
+ }
774
831
  if (file !== undefined) {
775
- // The file whole, page after page, as the console reads it from the deployment.
776
- const parts = [];
832
+ // The file whole, page after page, as the console reads it from the pack.
833
+ const pages = [];
777
834
  let offset;
778
835
  let last;
779
- for (let pages = 0; pages < MAX_SKILL_PAGES; pages += 1) {
836
+ for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
780
837
  last = await project.skillFile(file, offset === undefined ? {} : { offset });
781
- if (last.status !== "ready" || last.file === null) {
838
+ if (last.read === null) {
782
839
  break;
783
840
  }
784
- parts.push(last.file.content);
785
- if (last.file.nextOffset === null) {
841
+ pages.push(last);
842
+ if (last.read.kind !== "file" || last.read.nextOffset === null) {
786
843
  break;
787
844
  }
788
- offset = last.file.nextOffset;
845
+ offset = last.read.nextOffset;
789
846
  }
790
847
  if (last === undefined) {
791
848
  throw failed("The file could not be read.");
792
849
  }
793
- const read = last;
794
- const content = parts.join("");
795
- answer(io, session, { ...read, content }, () => formatSkillFile(read, content));
850
+ const fileRead = last;
851
+ const whole = joinFile(pages, summary);
852
+ answer(io, session, { ...fileRead, read: whole === undefined ? null : bodyOfFile(whole) }, () => formatSkillFile(fileRead, whole));
796
853
  return EXIT.ok;
797
854
  }
798
- const { path, pack } = skillPathOf(theOne(positionals, "Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack"));
855
+ const wanted = skillPathIn(theOne(positionals, "Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack"), listing.address);
856
+ if ("error" in wanted) {
857
+ throw misuse(wanted.error);
858
+ }
799
859
  // The skill whole: its pages until complete, the rules first, as load_skill lays them out.
800
860
  const pages = [];
801
861
  let cursor;
802
862
  let last;
803
863
  for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
804
- last = await project.skill(path, cursor);
805
- if (last.status !== "ready" || last.skill === null) {
864
+ last = await project.skill(wanted.path, cursor);
865
+ if (last.read?.status !== "ready") {
806
866
  break;
807
867
  }
808
- pages.push(last.skill);
809
- if (last.skill.complete || last.skill.nextCursor === null) {
868
+ pages.push(last);
869
+ cursor = last.read.skill.nextCursor ?? undefined;
870
+ if (last.read.skill.complete === true || cursor === undefined) {
810
871
  break;
811
872
  }
812
- cursor = last.skill.nextCursor;
813
873
  }
814
874
  if (last === undefined) {
815
875
  throw failed("The skill could not be read.");
816
876
  }
817
- // A URI of another pack would read this pack's skill at the same path, or nothing: said instead.
818
- if (pack !== undefined && last.address !== null && last.address !== pack) {
819
- throw misuse(`That URI is of another pack, ${pack}; this project's pack is at ${last.address}. Say the path of the SKILL.md, or a URI console skills lists.`);
820
- }
821
877
  const loaded = last;
822
- answer(io, session, { ...loaded, pages }, () => formatSkillLoad(loaded, pages));
878
+ const whole = joinSkill(pages, summary);
879
+ answer(io, session, { ...loaded, read: whole === undefined ? null : bodyOfSkill(whole) }, () => formatSkillLoad(loaded, whole));
823
880
  return EXIT.ok;
824
881
  };
825
- /**
826
- * A skill's path as given, or taken out of the URI `console skills` lists beside it
827
- * (`skill://gh/<owner>/<repo>/<path>`), with the pack the URI names, for the read to check.
828
- */
829
- function skillPathOf(given) {
830
- const uri = /^skill:\/\/(gh\/[^/]+\/[^/]+)\/(.+)$/.exec(given);
831
- const named = uri?.[1];
832
- const within = uri?.[2];
833
- if (named !== undefined && within !== undefined) {
834
- return { path: within, pack: `/${named}` };
835
- }
836
- if (given.startsWith("skill://")) {
837
- throw misuse(`Not a skill's URI the console knows: ${given}. Say the path of its SKILL.md from the repository's root, or the URI console skills lists.`);
838
- }
839
- return { path: given };
840
- }
841
882
  /** A version number given as a flag, or nothing; anything else is not understood. */
842
883
  function versionOf(given) {
843
884
  if (given === undefined) {
@@ -980,7 +1021,7 @@ const newTask = async (args, io) => {
980
1021
  ...(body === undefined ? {} : { body }),
981
1022
  ...fields,
982
1023
  });
983
- answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
1024
+ answer(io, session, made, () => `Task #${made.number} written: ${made.title}\ntask id: ${made.id}`);
984
1025
  return EXIT.ok;
985
1026
  };
986
1027
  const editTask = async (args, io) => {
@@ -1170,6 +1211,9 @@ const take = async (args, io) => {
1170
1211
  };
1171
1212
  answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
1172
1213
  `Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}${elsewhere}`,
1214
+ // The ids on a line of their own, each named: a run's and a task's begin alike, and a
1215
+ // task has a number for people as well.
1216
+ `run id: ${run.id} task id: ${found.id} (task #${found.number})`,
1173
1217
  "",
1174
1218
  formatTask(taken, undefined, undefined, comments.items),
1175
1219
  "",
@@ -1284,7 +1328,7 @@ const ask = async (args, io) => {
1284
1328
  // The id is printed before any waiting, so that a wait cut short loses nothing.
1285
1329
  io.stdout(session.json
1286
1330
  ? `${JSON.stringify(raised, null, 2)}\n`
1287
- : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\n${formatOptions(raised)}\n`);
1331
+ : `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`);
1288
1332
  return settle(io, session, project, raised, seconds, true);
1289
1333
  };
1290
1334
  const decision = async (args, io) => {
@@ -1489,7 +1533,7 @@ const newGoal = async (args, io) => {
1489
1533
  ...(startsOn === undefined ? {} : { startsOn }),
1490
1534
  ...(dueOn === undefined ? {} : { dueOn }),
1491
1535
  });
1492
- answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
1536
+ answer(io, session, made, () => `Goal set: ${made.title}\ngoal id: ${made.id}`);
1493
1537
  return EXIT.ok;
1494
1538
  };
1495
1539
  const editGoal = async (args, io) => {
@@ -1684,6 +1728,66 @@ const people = async (args, io) => {
1684
1728
  answer(io, session, found, () => [...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"));
1685
1729
  return EXIT.ok;
1686
1730
  };
1731
+ // The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
1732
+ // on the Settings page or through their agent here (ADR-0021).
1733
+ const WORKSPACE_FLAGS = "--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
1734
+ const WORKSPACE_USAGE = `Say what to do: console workspace | console workspace edit [${WORKSPACE_FLAGS.replaceAll(", ", "] [")}] | console workspace pack-token --connect|--stdin|--remove`;
1735
+ const workspace = async (args, io) => {
1736
+ const [verb, ...rest] = args;
1737
+ if (verb === undefined || verb.startsWith("--")) {
1738
+ const { values } = parse(args, {}, false);
1739
+ const session = await open(io, values);
1740
+ const me = await session.client.me();
1741
+ await rememberZone(io, session, me.workspace.timeZone ?? undefined);
1742
+ answer(io, session, me.workspace, () => formatWorkspace(me.workspace));
1743
+ return EXIT.ok;
1744
+ }
1745
+ if (verb === "edit") {
1746
+ const { values } = parse(rest, {
1747
+ "people-visibility": { type: "string" },
1748
+ "week-start": { type: "string" },
1749
+ "time-zone": { type: "string" },
1750
+ }, false);
1751
+ const visibility = text(values, "people-visibility");
1752
+ const weekStart = text(values, "week-start");
1753
+ const zone = text(values, "time-zone");
1754
+ if (visibility === undefined && weekStart === undefined && zone === undefined) {
1755
+ throw misuse(`Say what to change: ${WORKSPACE_FLAGS}`);
1756
+ }
1757
+ const session = await open(io, values);
1758
+ const changed = await session.client.updateWorkspace({
1759
+ ...(visibility === undefined ? {} : { peopleVisibility: peopleVisibilityOf(visibility) }),
1760
+ ...(weekStart === undefined
1761
+ ? {}
1762
+ : { weekStart: wordOf(weekStart, WEEK_STARTS, "a day the week begins on") }),
1763
+ ...(zone === undefined ? {} : { timeZone: zone === "none" ? null : zone }),
1764
+ });
1765
+ await rememberZone(io, session, changed.timeZone ?? undefined);
1766
+ answer(io, session, changed, () => formatWorkspace(changed));
1767
+ return EXIT.ok;
1768
+ }
1769
+ if (verb === "pack-token") {
1770
+ const { values, positionals } = parse(rest, PACK_TOKEN_FLAGS);
1771
+ if (positionals.length > 0) {
1772
+ throw misuse("The workspace's token names no project: console workspace pack-token --connect, --stdin or --remove");
1773
+ }
1774
+ const way = wayOf(values);
1775
+ const session = await open(io, values);
1776
+ const { client } = session;
1777
+ return packToken(io, session, way, {
1778
+ set: (token) => client.setWorkspacePackToken({ token }),
1779
+ remove: () => client.removeWorkspacePackToken(),
1780
+ connect: () => client.connectWorkspacePackToken(),
1781
+ async standing() {
1782
+ const state = await client.workspacePackTokenConnection();
1783
+ return state.status === "connected"
1784
+ ? { status: "connected", held: state.workspace }
1785
+ : state;
1786
+ },
1787
+ }, formatWorkspace);
1788
+ }
1789
+ throw misuse(WORKSPACE_USAGE);
1790
+ };
1687
1791
  const PERSON_USAGE = "Say what to do: console person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
1688
1792
  const person = async (args, io) => {
1689
1793
  const what = args[0];
@@ -1781,30 +1885,79 @@ const editProject = async (args, io) => {
1781
1885
  answer(io, session, changed, () => formatProject(changed));
1782
1886
  return EXIT.ok;
1783
1887
  };
1784
- /** The pack token (ADR-0028): read from standard input and never from an argument, so that no shell keeps it. */
1785
- const projectPackToken = async (args, io) => {
1786
- const { values, positionals } = parse(args, {
1787
- stdin: { type: "boolean" },
1788
- remove: { type: "boolean" },
1789
- });
1790
- const key = theOne(positionals, "Say which project: console project pack-token <key> --stdin, or console project pack-token <key> --remove");
1791
- if (on(values, "stdin") === on(values, "remove")) {
1792
- throw misuse("Say one of --stdin, which reads the token from standard input, and --remove, which takes it away.");
1793
- }
1794
- const session = await open(io, values);
1795
- const project = session.client.project(key);
1796
- if (on(values, "remove")) {
1797
- const changed = await project.removePackToken();
1798
- answer(io, session, changed, () => formatProject(changed));
1888
+ /** How a pack's connection ended short of a token (ADR-0036), in words for the person. */
1889
+ const PACK_CONNECT_ENDED = {
1890
+ expired: "The code expired before anyone approved it. Run the command again for a new one.",
1891
+ declined: "The request was declined at SkillCDN, or the code was spent.",
1892
+ refused: "SkillCDN refused the request: the repository is not one the person may open, the pack is public and needs no token, or they hold as many tokens as one may.",
1893
+ };
1894
+ /** How a SkillCDN token is to change, one of the three the flags name. */
1895
+ const wayOf = (values) => {
1896
+ const ways = ["connect", "stdin", "remove"].filter((way) => on(values, way));
1897
+ const [way] = ways;
1898
+ if (way === undefined || ways.length !== 1) {
1899
+ throw misuse("Say one of --connect, which asks SkillCDN for a code a person approves; --stdin, which reads a token made by hand from standard input; and --remove, which takes the token away.");
1900
+ }
1901
+ return way;
1902
+ };
1903
+ /**
1904
+ * The SkillCDN token (ADR-0028): connected with a code a person approves at SkillCDN (ADR-0036),
1905
+ * or read from standard input and never from an argument, so that no shell keeps it; or taken
1906
+ * away. The same for a project's and for the workspace's.
1907
+ */
1908
+ async function packToken(io, session, way, holder, format) {
1909
+ if (way === "remove") {
1910
+ const changed = await holder.remove();
1911
+ answer(io, session, changed, () => format(changed));
1799
1912
  return EXIT.ok;
1800
1913
  }
1914
+ if (way === "connect") {
1915
+ const started = await holder.connect();
1916
+ io.stdout(`To connect the pack, a person who may open its repository opens ${started.url} signed in at SkillCDN, checks that the page shows the code ${started.code}, and approves; the console receives the SkillCDN token and keeps it sealed, shown to nobody.\nWaiting for that, until ${moment(started.expiresAt)}.\n`);
1917
+ for (;;) {
1918
+ await io.sleep(started.intervalSeconds * 1000);
1919
+ const state = await holder.standing();
1920
+ if (state.status === "pending") {
1921
+ continue;
1922
+ }
1923
+ if (state.status === "connected") {
1924
+ answer(io, session, state.held, () => `Connected.\n${format(state.held)}`);
1925
+ return EXIT.ok;
1926
+ }
1927
+ if (state.status === "ended") {
1928
+ throw failed(PACK_CONNECT_ENDED[state.reason]);
1929
+ }
1930
+ throw failed("The console no longer waits for the connection; run the command again for a new code.");
1931
+ }
1932
+ }
1801
1933
  const token = (await io.readStdin()).trim();
1802
1934
  if (token.length === 0) {
1803
1935
  throw failed("No token was given on standard input.");
1804
1936
  }
1805
- const changed = await project.setPackToken({ token });
1806
- answer(io, session, changed, () => formatProject(changed));
1937
+ const changed = await holder.set(token);
1938
+ answer(io, session, changed, () => format(changed));
1807
1939
  return EXIT.ok;
1940
+ }
1941
+ const PACK_TOKEN_FLAGS = {
1942
+ connect: { type: "boolean" },
1943
+ stdin: { type: "boolean" },
1944
+ remove: { type: "boolean" },
1945
+ };
1946
+ const projectPackToken = async (args, io) => {
1947
+ const { values, positionals } = parse(args, PACK_TOKEN_FLAGS);
1948
+ const key = theOne(positionals, "Say which project: console project pack-token <key> --connect, --stdin or --remove");
1949
+ const way = wayOf(values);
1950
+ const session = await open(io, values);
1951
+ const project = session.client.project(key);
1952
+ return packToken(io, session, way, {
1953
+ set: (token) => project.setPackToken({ token }),
1954
+ remove: () => project.removePackToken(),
1955
+ connect: () => project.connectPackToken(),
1956
+ async standing() {
1957
+ const state = await project.packTokenConnection();
1958
+ return state.status === "connected" ? { status: "connected", held: state.project } : state;
1959
+ },
1960
+ }, formatProject);
1808
1961
  };
1809
1962
  const projectIcon = async (args, io) => {
1810
1963
  const { values, positionals } = parse(args, { remove: { type: "boolean" } });
@@ -1991,6 +2144,7 @@ const COMMANDS = {
1991
2144
  rules,
1992
2145
  people,
1993
2146
  person,
2147
+ workspace,
1994
2148
  project,
1995
2149
  members,
1996
2150
  member,
@@ -2125,6 +2279,8 @@ function hintFor(error, io, seen) {
2125
2279
  return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
2126
2280
  case "member.not_found":
2127
2281
  return "The person is not listed in the project. console members lists those who are.\n";
2282
+ case "member.open_project":
2283
+ return "The project is open to the workspace, so everyone of it is a member already. console member add <login> --role owner lists an owner; console member role <login> member takes one back to member.\n";
2128
2284
  case "category.key_taken":
2129
2285
  return "A category has that key already. console categories lists them.\n";
2130
2286
  case "category.not_found":