@skillcdn/console 0.1.9 → 0.1.10

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 (138) hide show
  1. package/README.md +5 -5
  2. package/dist/api.d.ts +5 -5
  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.map +1 -1
  7. package/dist/cli/cli.js +255 -10
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +9 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +80 -0
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +62 -7
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +31 -4
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +19 -3
  19. package/dist/client.js.map +1 -1
  20. package/dist/components/board.d.ts +27 -1
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +75 -7
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/calendar.d.ts +34 -0
  25. package/dist/components/calendar.d.ts.map +1 -0
  26. package/dist/components/calendar.js +96 -0
  27. package/dist/components/calendar.js.map +1 -0
  28. package/dist/components/categories.d.ts +13 -0
  29. package/dist/components/categories.d.ts.map +1 -0
  30. package/dist/components/categories.js +52 -0
  31. package/dist/components/categories.js.map +1 -0
  32. package/dist/components/decision-list.d.ts +10 -0
  33. package/dist/components/decision-list.d.ts.map +1 -1
  34. package/dist/components/decision-list.js +14 -1
  35. package/dist/components/decision-list.js.map +1 -1
  36. package/dist/components/event-feed.d.ts.map +1 -1
  37. package/dist/components/event-feed.js +12 -0
  38. package/dist/components/event-feed.js.map +1 -1
  39. package/dist/components/goals.d.ts +35 -0
  40. package/dist/components/goals.d.ts.map +1 -0
  41. package/dist/components/goals.js +45 -0
  42. package/dist/components/goals.js.map +1 -0
  43. package/dist/components/task-form.d.ts +5 -1
  44. package/dist/components/task-form.d.ts.map +1 -1
  45. package/dist/components/task-form.js +13 -1
  46. package/dist/components/task-form.js.map +1 -1
  47. package/dist/components/task-list.d.ts +16 -0
  48. package/dist/components/task-list.d.ts.map +1 -0
  49. package/dist/components/task-list.js +14 -0
  50. package/dist/components/task-list.js.map +1 -0
  51. package/dist/components/task-view.d.ts +9 -1
  52. package/dist/components/task-view.d.ts.map +1 -1
  53. package/dist/components/task-view.js +10 -4
  54. package/dist/components/task-view.js.map +1 -1
  55. package/dist/components/ui.d.ts +30 -1
  56. package/dist/components/ui.d.ts.map +1 -1
  57. package/dist/components/ui.js +60 -0
  58. package/dist/components/ui.js.map +1 -1
  59. package/dist/components/work.d.ts +10 -0
  60. package/dist/components/work.d.ts.map +1 -0
  61. package/dist/components/work.js +29 -0
  62. package/dist/components/work.js.map +1 -0
  63. package/dist/console.css +511 -0
  64. package/dist/console.d.ts +8 -0
  65. package/dist/console.d.ts.map +1 -1
  66. package/dist/console.js +124 -8
  67. package/dist/console.js.map +1 -1
  68. package/dist/data.d.ts +14 -1
  69. package/dist/data.d.ts.map +1 -1
  70. package/dist/data.js +36 -2
  71. package/dist/data.js.map +1 -1
  72. package/dist/dates.d.ts +39 -0
  73. package/dist/dates.d.ts.map +1 -0
  74. package/dist/dates.js +120 -0
  75. package/dist/dates.js.map +1 -0
  76. package/dist/i18n/en.d.ts +162 -1
  77. package/dist/i18n/en.d.ts.map +1 -1
  78. package/dist/i18n/en.js +174 -1
  79. package/dist/i18n/en.js.map +1 -1
  80. package/dist/i18n/ko.d.ts.map +1 -1
  81. package/dist/i18n/ko.js +169 -1
  82. package/dist/i18n/ko.js.map +1 -1
  83. package/dist/index.d.ts +10 -4
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +10 -4
  86. package/dist/index.js.map +1 -1
  87. package/dist/limits.d.ts +5 -0
  88. package/dist/limits.d.ts.map +1 -1
  89. package/dist/limits.js +5 -0
  90. package/dist/limits.js.map +1 -1
  91. package/dist/router.d.ts +50 -1
  92. package/dist/router.d.ts.map +1 -1
  93. package/dist/router.js +74 -1
  94. package/dist/router.js.map +1 -1
  95. package/dist/routes.d.ts +7 -1
  96. package/dist/routes.d.ts.map +1 -1
  97. package/dist/routes.js +9 -0
  98. package/dist/routes.js.map +1 -1
  99. package/dist/schemas.d.ts +366 -0
  100. package/dist/schemas.d.ts.map +1 -1
  101. package/dist/schemas.js +112 -3
  102. package/dist/schemas.js.map +1 -1
  103. package/dist/vocabulary.d.ts +15 -1
  104. package/dist/vocabulary.d.ts.map +1 -1
  105. package/dist/vocabulary.js +26 -0
  106. package/dist/vocabulary.js.map +1 -1
  107. package/docs/specs/cli.md +11 -4
  108. package/docs/specs/rest.md +33 -8
  109. package/package.json +1 -1
  110. package/skills/working-the-board/SKILL.md +7 -6
  111. package/src/api.ts +30 -0
  112. package/src/cli/cli.ts +311 -10
  113. package/src/cli/format.ts +88 -0
  114. package/src/cli/help.ts +69 -7
  115. package/src/client.ts +70 -2
  116. package/src/components/board.tsx +205 -12
  117. package/src/components/calendar.tsx +255 -0
  118. package/src/components/categories.tsx +209 -0
  119. package/src/components/decision-list.tsx +62 -5
  120. package/src/components/event-feed.tsx +12 -0
  121. package/src/components/goals.tsx +264 -0
  122. package/src/components/task-form.tsx +78 -1
  123. package/src/components/task-list.tsx +61 -0
  124. package/src/components/task-view.tsx +74 -3
  125. package/src/components/ui.tsx +116 -1
  126. package/src/components/work.tsx +83 -0
  127. package/src/console.tsx +337 -4
  128. package/src/data.ts +63 -8
  129. package/src/dates.ts +143 -0
  130. package/src/i18n/en.ts +184 -1
  131. package/src/i18n/ko.ts +175 -1
  132. package/src/index.ts +58 -1
  133. package/src/limits.ts +5 -0
  134. package/src/router.ts +119 -6
  135. package/src/routes.ts +10 -0
  136. package/src/schemas.ts +141 -1
  137. package/src/styles/console.css +511 -0
  138. package/src/vocabulary.ts +33 -0
package/src/cli/cli.ts CHANGED
@@ -7,18 +7,29 @@ import {
7
7
  type FetchLike,
8
8
  type ProjectClient,
9
9
  } from "../client.js";
10
+ import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
10
11
  import { MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
11
- import type { RestClaim, RestDecision } from "../schemas.js";
12
- import { TASK_PRIORITIES, TASK_STATES, type TaskPriority, type TaskState } from "../vocabulary.js";
12
+ import type { RestClaim, RestDecision, RestTaskPatch } from "../schemas.js";
13
+ import {
14
+ GOAL_STATES,
15
+ type GoalState,
16
+ TASK_PRIORITIES,
17
+ TASK_STATES,
18
+ type TaskPriority,
19
+ type TaskState,
20
+ } from "../vocabulary.js";
13
21
  import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
14
22
  import type { CredentialStore } from "./credentials.js";
15
23
  import type { DirectoryStore } from "./directory.js";
16
24
  import {
17
25
  formatAnswer,
26
+ formatCategoryLine,
18
27
  formatDecision,
19
28
  formatDecisionLine,
20
29
  formatDocument,
21
30
  formatDocuments,
31
+ formatGoal,
32
+ formatGoalLine,
22
33
  formatOptions,
23
34
  formatProjectLine,
24
35
  formatRun,
@@ -28,6 +39,7 @@ import {
28
39
  formatTaskLine,
29
40
  formatVersion,
30
41
  formatVersionLine,
42
+ formatWork,
31
43
  } from "./format.js";
32
44
  import { COMMAND_HELP, helpFor, usage } from "./help.js";
33
45
  import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
@@ -255,6 +267,79 @@ function priorityOf(value: string | undefined): TaskPriority | undefined {
255
267
  return found;
256
268
  }
257
269
 
270
+ function goalStateOf(value: string | undefined): GoalState | undefined {
271
+ if (value === undefined) {
272
+ return undefined;
273
+ }
274
+ const found = GOAL_STATES.find((state) => state === value);
275
+ if (found === undefined) {
276
+ throw misuse(`Not a state of a goal: ${value}. One of ${GOAL_STATES.join(", ")}`);
277
+ }
278
+ return found;
279
+ }
280
+
281
+ /** Today by this machine's clock, as a day. */
282
+ const todayOf = (io: CliIo): string => dayOfDate(new Date(io.now()));
283
+
284
+ /**
285
+ * A day as a flag takes one: `2026-10-24`, `today`, `tomorrow`, or `+<n>` days from today; and
286
+ * `none` where a field may be cleared, which is `null` on the wire.
287
+ */
288
+ function dayOf(io: CliIo, value: string | undefined, clearable = false): string | null | undefined {
289
+ if (value === undefined) {
290
+ return undefined;
291
+ }
292
+ if (clearable && value === "none") {
293
+ return null;
294
+ }
295
+ if (value === "today") {
296
+ return todayOf(io);
297
+ }
298
+ if (value === "tomorrow") {
299
+ return addDays(todayOf(io), 1);
300
+ }
301
+ const ahead = /^\+(\d{1,3})$/.exec(value);
302
+ if (ahead !== null) {
303
+ return addDays(todayOf(io), Number(ahead[1]));
304
+ }
305
+ if (!isDay(value)) {
306
+ throw misuse(`Not a day: ${value}. Say it as 2026-10-24, today, tomorrow, or +7`);
307
+ }
308
+ return value;
309
+ }
310
+
311
+ /** A value a field is set to, or `none` to clear it, which is `null` on the wire. */
312
+ const orNone = (value: string | undefined): string | null | undefined =>
313
+ value === "none" ? null : value;
314
+
315
+ /** The id of the person a flag names: `me`, or a login of the workspace. */
316
+ async function personIdOf(
317
+ session: Session,
318
+ value: string | undefined,
319
+ ): Promise<string | null | undefined> {
320
+ if (value === undefined) {
321
+ return undefined;
322
+ }
323
+ if (value === "none") {
324
+ return null;
325
+ }
326
+ if (value === "me") {
327
+ const me = await session.client.me();
328
+ if (me.person === null) {
329
+ throw failed("The console does not know this token. Sign in again: console login");
330
+ }
331
+ return me.person.id;
332
+ }
333
+ const { items } = await session.client.people();
334
+ const found = items.find((person) => person.login.toLowerCase() === value.toLowerCase());
335
+ if (found === undefined) {
336
+ throw misuse(
337
+ `Nobody of the workspace is called ${value}. console people is not a command; the logins are as people sign in.`,
338
+ );
339
+ }
340
+ return found.id;
341
+ }
342
+
258
343
  function secondsOf(value: string | undefined, fallback: number): number {
259
344
  if (value === undefined) {
260
345
  return fallback;
@@ -452,17 +537,67 @@ const use: Command = async (args, io) => {
452
537
  };
453
538
 
454
539
  const tasks: Command = async (args, io) => {
455
- const { values } = parse(args, { state: { type: "string" } }, false);
540
+ const { values } = parse(
541
+ args,
542
+ {
543
+ state: { type: "string" },
544
+ open: { type: "boolean" },
545
+ mine: { type: "boolean" },
546
+ category: { type: "string" },
547
+ goal: { type: "string" },
548
+ due: { type: "string" },
549
+ },
550
+ false,
551
+ );
456
552
  const state = stateOf(text(values, "state"));
457
553
  const session = await open(io, values);
458
554
  const project = await session.project();
459
- const found = await project.tasks(state === undefined ? {} : { state });
555
+ // What is due: a day, or the words for the usual windows; overdue keeps only the open ones.
556
+ const due = text(values, "due");
557
+ const today = todayOf(io);
558
+ const dueBy =
559
+ due === undefined
560
+ ? undefined
561
+ : due === "overdue"
562
+ ? addDays(today, -1)
563
+ : due === "week"
564
+ ? addDays(today, 6)
565
+ : (dayOf(io, due) ?? undefined);
566
+ const assignee = on(values, "mine")
567
+ ? ((await personIdOf(session, "me")) ?? undefined)
568
+ : undefined;
569
+ const found = await project.tasks({
570
+ ...(state === undefined ? {} : { state }),
571
+ ...(on(values, "open") || due === "overdue" ? { open: true } : {}),
572
+ ...(assignee === undefined ? {} : { assignee }),
573
+ ...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
574
+ ...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
575
+ ...(dueBy === undefined ? {} : { dueBy }),
576
+ });
460
577
  answer(io, session, found, () =>
461
578
  found.items.length === 0 ? "No tasks." : found.items.map(formatTaskLine).join("\n"),
462
579
  );
463
580
  return EXIT.ok;
464
581
  };
465
582
 
583
+ const work: Command = async (args, io) => {
584
+ const { values } = parse(args, {}, false);
585
+ const session = await open(io, values);
586
+ const found = await session.client.work();
587
+ answer(io, session, found, () => formatWork(found));
588
+ return EXIT.ok;
589
+ };
590
+
591
+ const categories: Command = async (args, io) => {
592
+ const { values } = parse(args, {}, false);
593
+ const session = await open(io, values);
594
+ const found = await session.client.categories();
595
+ answer(io, session, found, () =>
596
+ found.items.length === 0 ? "No categories." : found.items.map(formatCategoryLine).join("\n"),
597
+ );
598
+ return EXIT.ok;
599
+ };
600
+
466
601
  const skills: Command = async (args, io) => {
467
602
  const { values } = parse(args, {}, false);
468
603
  const session = await open(io, values);
@@ -472,33 +607,105 @@ const skills: Command = async (args, io) => {
472
607
  return EXIT.ok;
473
608
  };
474
609
 
610
+ /** The flags that set a task's fields, shared by writing one and changing one. */
611
+ const TASK_FIELDS: OptionSpec = {
612
+ state: { type: "string" },
613
+ priority: { type: "string" },
614
+ category: { type: "string" },
615
+ goal: { type: "string" },
616
+ starts: { type: "string" },
617
+ due: { type: "string" },
618
+ assignee: { type: "string" },
619
+ parent: { type: "string" },
620
+ };
621
+
622
+ /** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
623
+ async function taskFieldsOf(
624
+ io: CliIo,
625
+ session: Session,
626
+ project: ProjectClient,
627
+ values: Values,
628
+ ): Promise<RestTaskPatch> {
629
+ const state = stateOf(text(values, "state"));
630
+ const priority = priorityOf(text(values, "priority"));
631
+ const category = orNone(text(values, "category"));
632
+ const goalId = orNone(text(values, "goal"));
633
+ const startsOn = dayOf(io, text(values, "starts"), true);
634
+ const dueOn = dayOf(io, text(values, "due"), true);
635
+ const assigneeId = await personIdOf(session, text(values, "assignee"));
636
+ const parent = text(values, "parent");
637
+ const parentId =
638
+ parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
639
+ return {
640
+ ...(state === undefined ? {} : { state }),
641
+ ...(priority === undefined ? {} : { priority }),
642
+ ...(category === undefined ? {} : { category }),
643
+ ...(goalId === undefined ? {} : { goalId }),
644
+ ...(startsOn === undefined ? {} : { startsOn }),
645
+ ...(dueOn === undefined ? {} : { dueOn }),
646
+ ...(assigneeId === undefined ? {} : { assigneeId }),
647
+ ...(parentId === undefined ? {} : { parentId }),
648
+ };
649
+ }
650
+
475
651
  const newTask: Command = async (args, io) => {
476
652
  const { values, positionals } = parse(args, {
653
+ ...TASK_FIELDS,
477
654
  body: { type: "string" },
478
655
  file: { type: "string" },
479
- state: { type: "string" },
480
- priority: { type: "string" },
481
656
  });
482
657
  const title = theOne(positionals, 'Say the title: console task new "<title>"');
483
- const state = stateOf(text(values, "state"));
484
- const priority = priorityOf(text(values, "priority"));
485
658
  const body = await bodyOf(io, values, text(values, "body"));
486
659
  const session = await open(io, values);
487
660
  const project = await session.project();
661
+ const fields = await taskFieldsOf(io, session, project, values);
488
662
  const made = await project.createTask({
489
663
  title,
490
664
  ...(body === undefined ? {} : { body }),
491
- ...(state === undefined ? {} : { state }),
492
- ...(priority === undefined ? {} : { priority }),
665
+ ...fields,
493
666
  });
494
667
  answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
495
668
  return EXIT.ok;
496
669
  };
497
670
 
671
+ const editTask: Command = async (args, io) => {
672
+ const { values, positionals } = parse(args, {
673
+ ...TASK_FIELDS,
674
+ title: { type: "string" },
675
+ body: { type: "string" },
676
+ file: { type: "string" },
677
+ });
678
+ const ref = theOne(
679
+ positionals,
680
+ "Say which task: console task edit <number|id> --due <day> [--category <key>] ...",
681
+ );
682
+ const body = await bodyOf(io, values, text(values, "body"));
683
+ const session = await open(io, values);
684
+ const project = await session.project();
685
+ const title = text(values, "title");
686
+ const patch: RestTaskPatch = {
687
+ ...(title === undefined ? {} : { title }),
688
+ ...(body === undefined ? {} : { body }),
689
+ ...(await taskFieldsOf(io, session, project, values)),
690
+ };
691
+ if (Object.keys(patch).length === 0) {
692
+ throw misuse(
693
+ "Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.",
694
+ );
695
+ }
696
+ const found = await project.task(ref);
697
+ const changed = await project.updateTask(found.id, patch);
698
+ answer(io, session, changed, () => formatTask(changed));
699
+ return EXIT.ok;
700
+ };
701
+
498
702
  const task: Command = async (args, io) => {
499
703
  if (args[0] === "new") {
500
704
  return newTask(args.slice(1), io);
501
705
  }
706
+ if (args[0] === "edit") {
707
+ return editTask(args.slice(1), io);
708
+ }
502
709
  const { values, positionals } = parse(args, {});
503
710
  const ref = theOne(positionals, "Say which task: console task <number|id>");
504
711
  const session = await open(io, values);
@@ -864,6 +1071,88 @@ const restore: Command = async (args, io) => {
864
1071
  return EXIT.ok;
865
1072
  };
866
1073
 
1074
+ const goals: Command = async (args, io) => {
1075
+ const { values } = parse(args, {}, false);
1076
+ const session = await open(io, values);
1077
+ const project = await session.project();
1078
+ const found = await project.goals();
1079
+ answer(io, session, found, () =>
1080
+ found.items.length === 0
1081
+ ? "No goals. An owner sets one: console goal new <title> --due <day>"
1082
+ : found.items.map(formatGoalLine).join("\n"),
1083
+ );
1084
+ return EXIT.ok;
1085
+ };
1086
+
1087
+ const GOAL_FIELDS: OptionSpec = {
1088
+ state: { type: "string" },
1089
+ due: { type: "string" },
1090
+ body: { type: "string" },
1091
+ file: { type: "string" },
1092
+ };
1093
+
1094
+ const newGoal: Command = async (args, io) => {
1095
+ const { values, positionals } = parse(args, GOAL_FIELDS);
1096
+ const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
1097
+ const state = goalStateOf(text(values, "state"));
1098
+ const dueOn = dayOf(io, text(values, "due"));
1099
+ const body = await bodyOf(io, values, text(values, "body"));
1100
+ const session = await open(io, values);
1101
+ const project = await session.project();
1102
+ const made = await project.createGoal({
1103
+ title,
1104
+ ...(body === undefined ? {} : { body }),
1105
+ ...(state === undefined ? {} : { state }),
1106
+ ...(dueOn === undefined ? {} : { dueOn }),
1107
+ });
1108
+ answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
1109
+ return EXIT.ok;
1110
+ };
1111
+
1112
+ const editGoal: Command = async (args, io) => {
1113
+ const { values, positionals } = parse(args, { ...GOAL_FIELDS, title: { type: "string" } });
1114
+ const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
1115
+ const title = text(values, "title");
1116
+ const state = goalStateOf(text(values, "state"));
1117
+ const dueOn = dayOf(io, text(values, "due"), true);
1118
+ const body = await bodyOf(io, values, text(values, "body"));
1119
+ const patch = {
1120
+ ...(title === undefined ? {} : { title }),
1121
+ ...(body === undefined ? {} : { body }),
1122
+ ...(state === undefined ? {} : { state }),
1123
+ ...(dueOn === undefined ? {} : { dueOn }),
1124
+ };
1125
+ if (Object.keys(patch).length === 0) {
1126
+ throw misuse(
1127
+ "Say what to change: --title, --body, --state or --due; --due none clears the day.",
1128
+ );
1129
+ }
1130
+ const session = await open(io, values);
1131
+ const project = await session.project();
1132
+ const changed = await project.updateGoal(id, patch);
1133
+ answer(io, session, changed, () => formatGoal(changed));
1134
+ return EXIT.ok;
1135
+ };
1136
+
1137
+ const goal: Command = async (args, io) => {
1138
+ if (args[0] === "new") {
1139
+ return newGoal(args.slice(1), io);
1140
+ }
1141
+ if (args[0] === "edit") {
1142
+ return editGoal(args.slice(1), io);
1143
+ }
1144
+ const { values, positionals } = parse(args, {});
1145
+ const id = theOne(positionals, "Say which goal: console goal <id>");
1146
+ const session = await open(io, values);
1147
+ const project = await session.project();
1148
+ const found = await project.goal(id);
1149
+ const towards = await project.tasks({ goal: found.id });
1150
+ answer(io, session, { goal: found, tasks: towards.items }, () =>
1151
+ formatGoal(found, towards.items),
1152
+ );
1153
+ return EXIT.ok;
1154
+ };
1155
+
867
1156
  const decisions: Command = async (args, io) => {
868
1157
  const { values } = parse(args, { open: { type: "boolean" } }, false);
869
1158
  const session = await open(io, values);
@@ -1024,8 +1313,12 @@ const COMMANDS: Readonly<Record<string, Command>> = {
1024
1313
  serve,
1025
1314
  projects,
1026
1315
  use,
1316
+ work,
1027
1317
  tasks,
1028
1318
  task,
1319
+ goals,
1320
+ goal,
1321
+ categories,
1029
1322
  skills,
1030
1323
  take,
1031
1324
  report,
@@ -1072,6 +1365,14 @@ function hintFor(error: ApiError): string {
1072
1365
  return "The page has moved on since the version you started from. Read it again, and write from there.\n";
1073
1366
  case "document.archived":
1074
1367
  return "The page is archived. console restore <path> brings it back first.\n";
1368
+ case "task.invalid_category":
1369
+ return "No category has that key. console categories lists them.\n";
1370
+ case "task.invalid_goal":
1371
+ return "No goal of this project has that id. console goals lists them.\n";
1372
+ case "task.invalid_dates":
1373
+ return "The due day is before the start. Say a due day on or after it.\n";
1374
+ case "goal.not_found":
1375
+ return "No goal of this project has that id. console goals lists them.\n";
1075
1376
  case "connect.too_many":
1076
1377
  return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
1077
1378
  case "network":
package/src/cli/format.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  import type {
2
2
  RestArtifact,
3
+ RestCategory,
3
4
  RestDecision,
4
5
  RestDocument,
5
6
  RestDocumentSummary,
6
7
  RestDocuments,
8
+ RestGoal,
7
9
  RestProject,
8
10
  RestRun,
9
11
  RestSkills,
10
12
  RestTask,
11
13
  RestVersion,
12
14
  RestVersionSummary,
15
+ RestWork,
13
16
  } from "../schemas.js";
14
17
 
15
18
  // What the command prints: one line per thing in a list, a few lines for one thing in full.
@@ -47,6 +50,12 @@ export function formatTaskLine(task: RestTask): string {
47
50
  if (task.assignee !== null) {
48
51
  notes.push(`assigned to ${task.assignee.login}`);
49
52
  }
53
+ if (task.category !== null) {
54
+ notes.push(task.category.key);
55
+ }
56
+ if (task.dueOn !== null) {
57
+ notes.push(`due ${task.dueOn}`);
58
+ }
50
59
  if (task.openDecisions > 0) {
51
60
  notes.push(`${plural(task.openDecisions, "decision")} waiting`);
52
61
  }
@@ -56,6 +65,28 @@ export function formatTaskLine(task: RestTask): string {
56
65
  return `#${task.number} ${task.state} ${task.priority} ${task.title} (${notes.join("; ")})`;
57
66
  }
58
67
 
68
+ /** The plan and what happened, as a line each, only where there is something to say. */
69
+ function scheduleLines(task: RestTask): string[] {
70
+ const lines: string[] = [];
71
+ const plan = [
72
+ task.category === null ? undefined : `category: ${task.category.key}`,
73
+ task.goalId === null ? undefined : `goal: ${task.goalId}`,
74
+ task.startsOn === null ? undefined : `starts: ${task.startsOn}`,
75
+ task.dueOn === null ? undefined : `due: ${task.dueOn}`,
76
+ ].filter((part) => part !== undefined);
77
+ if (plan.length > 0) {
78
+ lines.push(plan.join(" "));
79
+ }
80
+ const happened = [
81
+ task.startedAt === null ? undefined : `started: ${task.startedAt}`,
82
+ task.doneAt === null ? undefined : `done: ${task.doneAt}`,
83
+ ].filter((part) => part !== undefined);
84
+ if (happened.length > 0) {
85
+ lines.push(happened.join(" "));
86
+ }
87
+ return lines;
88
+ }
89
+
59
90
  /** One task in full; with the decisions about it and the runs on it, when given. */
60
91
  export function formatTask(
61
92
  task: RestTask,
@@ -66,6 +97,7 @@ export function formatTask(
66
97
  `#${task.number} ${task.title}`,
67
98
  `state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} assignee: ${task.assignee?.login ?? "nobody"}`,
68
99
  `id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
100
+ ...scheduleLines(task),
69
101
  ];
70
102
  if (task.links.length > 0) {
71
103
  lines.push("links:");
@@ -165,6 +197,62 @@ export function formatSkills(skills: RestSkills): string {
165
197
  return lines.join("\n");
166
198
  }
167
199
 
200
+ /** How far a goal is, from its tasks: the done, the at work, and the rest, in one phrase. */
201
+ function progressOf(goal: RestGoal): string {
202
+ const counts = goal.taskCounts;
203
+ const total = counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
204
+ const working = counts.in_progress + counts.in_review;
205
+ return `${counts.done}/${total} done${working === 0 ? "" : `, ${working} in progress`}${counts.dropped === 0 ? "" : `, ${counts.dropped} dropped`}`;
206
+ }
207
+
208
+ /** One goal: its id, its state, its title, when it is due, and how far it is. */
209
+ export function formatGoalLine(goal: RestGoal): string {
210
+ const notes = [goal.dueOn === null ? "no target day" : `due ${goal.dueOn}`, progressOf(goal)];
211
+ return `${goal.id} ${goal.state} ${goal.title} (${notes.join("; ")})`;
212
+ }
213
+
214
+ /** One goal in full; with the tasks that work towards it, when given. */
215
+ export function formatGoal(goal: RestGoal, tasks?: readonly RestTask[]): string {
216
+ const lines = [
217
+ `goal ${goal.id}: ${goal.title}`,
218
+ `state: ${goal.state} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
219
+ `progress: ${progressOf(goal)}`,
220
+ ];
221
+ if (goal.body.trim().length > 0) {
222
+ lines.push("", goal.body.trimEnd());
223
+ }
224
+ if (tasks !== undefined) {
225
+ lines.push("", tasks.length === 0 ? "tasks: none" : "tasks:");
226
+ for (const task of tasks) {
227
+ lines.push(` ${formatTaskLine(task)}`);
228
+ }
229
+ }
230
+ return lines.join("\n");
231
+ }
232
+
233
+ /** One category: its key, what people call it, and its colour. */
234
+ export function formatCategoryLine(category: RestCategory): string {
235
+ return `${category.key} ${category.name} (${category.color})`;
236
+ }
237
+
238
+ /** What is on a person's plate: their open tasks by project, and the decisions waiting. */
239
+ export function formatWork(work: RestWork): string {
240
+ const lines: string[] = [];
241
+ if (work.tasks.length === 0) {
242
+ lines.push("No open task is assigned to you.");
243
+ } else {
244
+ lines.push("tasks assigned to you:");
245
+ for (const { project, task } of work.tasks) {
246
+ lines.push(` ${project.key} ${formatTaskLine(task)}`);
247
+ }
248
+ }
249
+ lines.push("", work.decisions.length === 0 ? "decisions waiting: none" : "decisions waiting:");
250
+ for (const { project, decision } of work.decisions) {
251
+ lines.push(` ${project.key} ${formatDecisionLine(decision)}`);
252
+ }
253
+ return lines.join("\n");
254
+ }
255
+
168
256
  export function formatDecisionLine(decision: RestDecision): string {
169
257
  const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
170
258
  return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
package/src/cli/help.ts CHANGED
@@ -51,6 +51,22 @@ const MARKDOWN = `Markdown, up to ${MAX_BODY_LENGTH} characters`;
51
51
  const DOCUMENT_PATH = `a path of lowercase letters, digits and hyphens, with slashes between its folders (guides/onboarding), up to ${MAX_DOCUMENT_PATH_LENGTH} characters; it does not change once written`;
52
52
  const DOCUMENT_REFUSALS = ["document.not_found: no page has that path in this project"];
53
53
 
54
+ const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from today";
55
+ const TASK_FIELD_FLAGS =
56
+ "[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login|me>] [--parent <number|id>]";
57
+ const TASK_FIELD_LIMITS = [
58
+ `starts, due: ${DAY}; the due day is not before the start`,
59
+ "category: the key of one of the workspace's categories (console categories); goal: the id of one of the project's goals (console goals)",
60
+ "assignee: a login of the workspace, or me; parent: a task of the project",
61
+ ];
62
+ const TASK_FIELD_REFUSALS = [
63
+ "task.invalid_category: no category has that key",
64
+ "task.invalid_goal: the goal is not one of this project",
65
+ "task.invalid_dates: the due day is before the start",
66
+ "task.invalid_assignee: the person may not work in the project",
67
+ "task.invalid_parent: the parent is not a task of the project, or would make a loop",
68
+ ];
69
+
54
70
  export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
55
71
  login: {
56
72
  usage: "login --url <origin> [--agent <name>] [--token-stdin]",
@@ -82,18 +98,52 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
82
98
  "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. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over it.",
83
99
  refusals: ["project.not_found: no project has the key, or it is not yours to see"],
84
100
  },
101
+ work: {
102
+ usage: "work",
103
+ about:
104
+ "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.",
105
+ },
85
106
  tasks: {
86
- usage: "tasks [--state <state>]",
107
+ usage:
108
+ "tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>]",
87
109
  about:
88
- "The tasks on the board, newest first: number, state, priority, title, who owns and works on each, and what waits on it. States: idea, ready, in_progress, in_review, done, dropped.",
110
+ "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, --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). States: idea, ready, in_progress, in_review, done, dropped.",
89
111
  },
90
112
  task: {
113
+ 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}`,
114
+ about:
115
+ "One task in full, with its plan (category, goal, start and due days), what happened to it (when it was started and done, which the board keeps itself), the decisions about it and the runs on it; or a new task written down; or a task changed, only in what the flags say, where none clears a field (priorities: low, normal, high, urgent).",
116
+ limits: [
117
+ `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
118
+ `body: ${MARKDOWN}`,
119
+ ...TASK_FIELD_LIMITS,
120
+ ],
121
+ refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
122
+ },
123
+ goals: {
124
+ usage: "goals",
125
+ about:
126
+ "The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its target day, and how far it is, counted from the tasks that work towards it.",
127
+ },
128
+ goal: {
91
129
  usage:
92
- "task <number|id> | task new <title> [--body <markdown>|--file <path>] [--state <state>] [--priority <priority>]",
130
+ 'goal <id> | goal new "<title>" [--body <markdown>|--file <path>] [--state <state>] [--due <day>] | goal edit <id> [--title <title>] [--body <markdown>|--file <path>] [--state <state>] [--due <day|none>]',
93
131
  about:
94
- "One task in full, with the decisions about it and the runs on it; or a new task written down (priorities: low, normal, high, urgent).",
95
- limits: [`title: one line, up to ${MAX_TITLE_LENGTH} characters`, `body: ${MARKDOWN}`],
96
- refusals: ["task.not_found: no task has that number or id"],
132
+ "One goal in full, with the tasks that work towards it; or a goal set on the roadmap, for an owner of the project; or one changed, only in what the flags say. A task works towards a goal through its --goal.",
133
+ limits: [
134
+ `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
135
+ `body: ${MARKDOWN}`,
136
+ `due: ${DAY}`,
137
+ ],
138
+ refusals: [
139
+ "goal.not_found: no goal of this project has that id",
140
+ "auth.forbidden: only an owner of the project sets its roadmap",
141
+ ],
142
+ },
143
+ categories: {
144
+ usage: "categories",
145
+ about:
146
+ "The categories tasks are sorted into, the same in every project: each with its key, which --category takes, its name and its colour. An administrator keeps them on the console's Categories page.",
97
147
  },
98
148
  skills: {
99
149
  usage: "skills",
@@ -283,7 +333,19 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
283
333
  },
284
334
  {
285
335
  title: "The board",
286
- commands: ["tasks", "task", "decisions", "decision", "runs", "run", "skills"],
336
+ commands: [
337
+ "work",
338
+ "tasks",
339
+ "task",
340
+ "goals",
341
+ "goal",
342
+ "categories",
343
+ "decisions",
344
+ "decision",
345
+ "runs",
346
+ "run",
347
+ "skills",
348
+ ],
287
349
  },
288
350
  { title: "Working", commands: ["take", "report", "hand-in", "ask", "finish", "fail", "abandon"] },
289
351
  {