@skillcdn/console 0.1.20 → 0.1.22

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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/api.d.ts +1 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +1 -1
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +67 -6
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +15 -4
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +133 -7
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts +8 -4
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +67 -19
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/client.d.ts +13 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +12 -1
  20. package/dist/client.js.map +1 -1
  21. package/dist/components/board.d.ts +4 -4
  22. package/dist/components/board.d.ts.map +1 -1
  23. package/dist/components/board.js +48 -19
  24. package/dist/components/board.js.map +1 -1
  25. package/dist/components/task-view.d.ts.map +1 -1
  26. package/dist/components/task-view.js +2 -4
  27. package/dist/components/task-view.js.map +1 -1
  28. package/dist/console.css +59 -17
  29. package/dist/console.d.ts.map +1 -1
  30. package/dist/console.js +1 -1
  31. package/dist/console.js.map +1 -1
  32. package/dist/i18n/en.js +1 -1
  33. package/dist/i18n/en.js.map +1 -1
  34. package/dist/i18n/ko.js +1 -1
  35. package/dist/i18n/ko.js.map +1 -1
  36. package/dist/rules.d.ts.map +1 -1
  37. package/dist/rules.js +3 -2
  38. package/dist/rules.js.map +1 -1
  39. package/dist/schemas.d.ts +145 -0
  40. package/dist/schemas.d.ts.map +1 -1
  41. package/dist/schemas.js +89 -0
  42. package/dist/schemas.js.map +1 -1
  43. package/docs/specs/cli.md +5 -5
  44. package/docs/specs/rest.md +8 -3
  45. package/package.json +1 -1
  46. package/skills/working-the-board/SKILL.md +4 -4
  47. package/src/api.ts +8 -0
  48. package/src/cli/cli.ts +77 -9
  49. package/src/cli/format.ts +156 -7
  50. package/src/cli/help.ts +93 -25
  51. package/src/client.ts +48 -0
  52. package/src/components/board.tsx +86 -33
  53. package/src/components/task-view.tsx +10 -12
  54. package/src/console.tsx +8 -2
  55. package/src/i18n/en.ts +1 -1
  56. package/src/i18n/ko.ts +1 -1
  57. package/src/rules.ts +3 -2
  58. package/src/schemas.ts +110 -0
  59. package/src/styles/console.css +59 -17
package/src/client.ts CHANGED
@@ -61,7 +61,9 @@ import {
61
61
  type RestRunEndInput,
62
62
  type RestRunInput,
63
63
  type RestRuns,
64
+ type RestSkillBrowse,
64
65
  type RestSkillFile,
66
+ type RestSkillFind,
65
67
  type RestSkillLoad,
66
68
  type RestSkills,
67
69
  type RestTask,
@@ -103,7 +105,9 @@ import {
103
105
  restRulesSchema,
104
106
  restRunSchema,
105
107
  restRunsSchema,
108
+ restSkillBrowseSchema,
106
109
  restSkillFileSchema,
110
+ restSkillFindSchema,
107
111
  restSkillLoadSchema,
108
112
  restSkillsSchema,
109
113
  restTaskSchema,
@@ -318,6 +322,25 @@ export interface ProjectClient {
318
322
  page?: { readonly offset?: number | undefined; readonly limit?: number | undefined },
319
323
  signal?: AbortSignal,
320
324
  ): Promise<RestSkillFile>;
325
+ /** What a folder of the pack holds, a page of it (`browse_repo`): the mounted folder when `path` is left out. */
326
+ skillBrowse(
327
+ page?: {
328
+ readonly path?: string | undefined;
329
+ readonly cursor?: string | undefined;
330
+ readonly limit?: number | undefined;
331
+ },
332
+ signal?: AbortSignal,
333
+ ): Promise<RestSkillBrowse>;
334
+ /** The skills and the documents of the pack that words find, a page of them in relevance order (`search_repo`), within `path` when given. */
335
+ skillFind(
336
+ query: string,
337
+ options?: {
338
+ readonly path?: string | undefined;
339
+ readonly cursor?: string | undefined;
340
+ readonly limit?: number | undefined;
341
+ },
342
+ signal?: AbortSignal,
343
+ ): Promise<RestSkillFind>;
321
344
  /** Sets the pack token the project reads a private pack with; for an owner. The token travels once and is answered never. */
322
345
  setPackToken(input: RestPackTokenInput): Promise<RestProject>;
323
346
  /** Takes the pack token away; for an owner. */
@@ -714,6 +737,31 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
714
737
  restSkillFileSchema,
715
738
  signal,
716
739
  ),
740
+ skillBrowse: (page = {}, signal) =>
741
+ request(
742
+ "GET",
743
+ withQuery(`${at("skills")}/browse`, {
744
+ path: page.path,
745
+ cursor: page.cursor,
746
+ limit: page.limit === undefined ? undefined : String(page.limit),
747
+ }),
748
+ undefined,
749
+ restSkillBrowseSchema,
750
+ signal,
751
+ ),
752
+ skillFind: (query, options = {}, signal) =>
753
+ request(
754
+ "GET",
755
+ withQuery(`${at("skills")}/find`, {
756
+ query,
757
+ path: options.path,
758
+ cursor: options.cursor,
759
+ limit: options.limit === undefined ? undefined : String(options.limit),
760
+ }),
761
+ undefined,
762
+ restSkillFindSchema,
763
+ signal,
764
+ ),
717
765
  setPackToken: (input) =>
718
766
  request("PUT", `${base}${at()}/pack-token`, input, restProjectSchema),
719
767
  removePackToken: () =>
@@ -18,7 +18,6 @@ import {
18
18
  } from "../api.js";
19
19
  import { useWords } from "../i18n/index.js";
20
20
  import type { BoardFilter } from "../router.js";
21
- import { OPEN_TASK_STATES } from "../vocabulary.js";
22
21
  import { isPlainClick } from "./shell.js";
23
22
  import {
24
23
  Avatar,
@@ -35,12 +34,13 @@ import {
35
34
  // A card says what a person needs at a glance and nothing more: the number and the title, the
36
35
  // category, what is due, who is on it, and what blocks it. Whether an agent is at it is not
37
36
  // said, since the board cannot know (ADR-0020); the task's page shows its runs. A card is moved
38
- // to another column by hand: pressed anywhere but on its title and its menu, then dragged, with
39
- // a mouse at once and under a finger once it has rested a moment, so that a finger that moves
40
- // at once scrolls as it always did. The card's menu holds the rest: the way to the task's form,
41
- // and the way to put the task away once it is done or dropped (ADR-0029). The keyboard changes a
42
- // state on the task's page. The board takes its data as props and nothing from the network;
43
- // what it is told on a click goes back up.
37
+ // to another column by hand: pressed anywhere but on its menu, its title included, then dragged
38
+ // whole, with a mouse at once and under a finger once it has rested a moment, so that a finger
39
+ // that moves at once scrolls as it always did; a click on the title opens the task as long as the
40
+ // card was not dragged. The card's menu holds the rest: the way to the task's form, and the way to
41
+ // put the task away (ADR-0029, ADR-0030). The keyboard changes a state on the task's page. The
42
+ // board takes its data as props and nothing from the network; what it is told on a click goes
43
+ // back up.
44
44
 
45
45
  export interface BoardProps {
46
46
  readonly tasks: readonly RestTask[];
@@ -57,7 +57,7 @@ export interface BoardProps {
57
57
  readonly editHref?: ((task: RestTask) => string) | undefined;
58
58
  /** Called when the menu's edit is chosen, to show the form without leaving the page; left out, the link is followed. */
59
59
  readonly onEdit?: ((task: RestTask) => void) | undefined;
60
- /** Called when a task done or dropped is put away from its card's menu (ADR-0029). Left out, the menu offers nothing of it. */
60
+ /** Called when a task is put away from its card's menu (ADR-0029, ADR-0030). Left out, the menu offers nothing of it. */
61
61
  readonly onArchive?: ((task: RestTask) => void) | undefined;
62
62
  }
63
63
 
@@ -78,12 +78,21 @@ export function columnStateOf(value: string | null | undefined): TaskState | und
78
78
  return TASK_STATES.find((state) => state === value);
79
79
  }
80
80
 
81
+ interface Point {
82
+ readonly x: number;
83
+ readonly y: number;
84
+ }
85
+
81
86
  /** A card on its way to another column. */
82
87
  interface Drag {
83
88
  readonly task: RestTask;
84
- readonly from: { readonly x: number; readonly y: number };
89
+ readonly from: Point;
85
90
  readonly x: number;
86
91
  readonly y: number;
92
+ /** Where on the card it was taken hold of, so that the card moves whole under the pointer. */
93
+ readonly grip: Point;
94
+ /** How wide the card is where it stands, so that the one in hand is the same. */
95
+ readonly width: number;
87
96
  /** Whether the card has come loose: the mouse has travelled, or the finger has rested. */
88
97
  readonly moving: boolean;
89
98
  /** Whether the press is a finger's, which must rest before the card comes loose. */
@@ -97,23 +106,21 @@ export interface TaskCardProps {
97
106
  readonly href: string;
98
107
  readonly onOpen: (task: RestTask) => void;
99
108
  readonly today?: string | undefined;
100
- /** Called when the card is pressed anywhere but on its controls, to drag it to another column; left out, the card stays where it is. */
109
+ /** Called when the card is pressed anywhere but on its menu, to drag it to another column; left out, the card stays where it is. */
101
110
  readonly onPress?: ((task: RestTask, event: ReactPointerEvent<HTMLElement>) => void) | undefined;
102
- /** Whether the card is on its way to another column. */
111
+ /** Whether the card is on its way to another column: the one left in its place is faded. */
103
112
  readonly dragging?: boolean | undefined;
104
113
  /** Where the task is edited; given, the card's menu leads there. */
105
114
  readonly editHref?: string | undefined;
106
115
  /** Called when the menu's edit is chosen, to show the form without leaving the page; left out, the link is followed. */
107
116
  readonly onEdit?: ((task: RestTask) => void) | undefined;
108
- /** Called when the task is put away from the card's menu; offered once the task is done or dropped (ADR-0029). */
117
+ /** Called when the task is put away from the card's menu, whatever its state (ADR-0030). */
109
118
  readonly onArchive?: ((task: RestTask) => void) | undefined;
110
119
  }
111
120
 
112
121
  export function TaskCard(props: TaskCardProps) {
113
122
  const words = useWords();
114
123
  const { task, onPress } = props;
115
- const closed = !OPEN_TASK_STATES.includes(task.state);
116
- const archive = closed ? props.onArchive : undefined;
117
124
  return (
118
125
  <article
119
126
  className={cx(
@@ -124,12 +131,19 @@ export function TaskCard(props: TaskCardProps) {
124
131
  aria-label={`#${task.number} ${task.title}`}
125
132
  onPointerDown={onPress === undefined ? undefined : (event) => onPress(task, event)}
126
133
  >
127
- {(props.editHref !== undefined || archive !== undefined) && (
128
- <CardMenu task={task} editHref={props.editHref} onEdit={props.onEdit} onArchive={archive} />
134
+ {(props.editHref !== undefined || props.onArchive !== undefined) && (
135
+ <CardMenu
136
+ task={task}
137
+ editHref={props.editHref}
138
+ onEdit={props.onEdit}
139
+ onArchive={props.onArchive}
140
+ />
129
141
  )}
130
142
  <a
131
143
  className="sc-card-title"
132
144
  href={props.href}
145
+ // The card is dragged by our own hand, the title included; the browser's drag of a link is not it.
146
+ draggable={false}
133
147
  onClick={(event) => {
134
148
  if (isPlainClick(event)) {
135
149
  event.preventDefault();
@@ -169,7 +183,8 @@ const MENU_MARGIN = 8;
169
183
  /**
170
184
  * The card's menu: three dots in its corner, which open the ways a card has beyond being moved,
171
185
  * each given by the composition. The menu is rendered over the document, so that no column
172
- * clips it; a press elsewhere, the escape key, a scroll or a change of the window closes it.
186
+ * clips it; a press elsewhere, the escape key, a scroll or a change of the window closes it, and
187
+ * so does the button that opened it.
173
188
  */
174
189
  function CardMenu(props: {
175
190
  readonly task: RestTask;
@@ -194,6 +209,13 @@ function CardMenu(props: {
194
209
  right: Math.max(MENU_MARGIN, window.innerWidth - rect.right),
195
210
  });
196
211
  };
212
+ /** Whether the focus, or a press, is on the menu or on its button: the menu's own. */
213
+ const own = useCallback(
214
+ (target: EventTarget | null): boolean =>
215
+ target instanceof Node &&
216
+ (panel.current?.contains(target) === true || button.current?.contains(target) === true),
217
+ [],
218
+ );
197
219
  useEffect(() => {
198
220
  if (!open) {
199
221
  return;
@@ -214,14 +236,9 @@ function CardMenu(props: {
214
236
  }
215
237
  panel.current?.querySelector<HTMLElement>("[role='menuitem']")?.focus({ preventScroll: true });
216
238
  const away = (event: PointerEvent) => {
217
- const target = event.target;
218
- if (
219
- target instanceof Node &&
220
- (panel.current?.contains(target) === true || button.current?.contains(target) === true)
221
- ) {
222
- return;
239
+ if (!own(event.target)) {
240
+ close();
223
241
  }
224
- close();
225
242
  };
226
243
  const key = (event: KeyboardEvent) => {
227
244
  if (event.key === "Escape") {
@@ -239,7 +256,7 @@ function CardMenu(props: {
239
256
  window.removeEventListener("scroll", close, true);
240
257
  window.removeEventListener("resize", close);
241
258
  };
242
- }, [open, close]);
259
+ }, [open, close, own]);
243
260
  // The arrow keys walk the items, around the ends.
244
261
  const walk = (event: ReactKeyboardEvent<HTMLDivElement>) => {
245
262
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") {
@@ -283,9 +300,9 @@ function CardMenu(props: {
283
300
  aria-label={`#${task.number} ${task.title}`}
284
301
  style={{ top: place.top, right: place.right }}
285
302
  onKeyDown={walk}
303
+ // The focus leaving for anywhere but the menu's own button closes it; the button closes it itself.
286
304
  onBlur={(event) => {
287
- const next = event.relatedTarget;
288
- if (!(next instanceof Node && panel.current?.contains(next) === true)) {
305
+ if (!own(event.relatedTarget)) {
289
306
  close();
290
307
  }
291
308
  }}
@@ -340,6 +357,8 @@ export function byDue(a: RestTask, b: RestTask): number {
340
357
  return b.number - a.number;
341
358
  }
342
359
 
360
+ const nothing = () => undefined;
361
+
343
362
  export function Board(props: BoardProps) {
344
363
  const words = useWords();
345
364
  const { onMove } = props;
@@ -353,10 +372,10 @@ export function Board(props: BoardProps) {
353
372
  const loose = useRef(false);
354
373
 
355
374
  const press = useCallback((task: RestTask, event: ReactPointerEvent<HTMLElement>) => {
356
- // The card's own controls, its title and its menu, are pressed for themselves.
375
+ // The card's menu is pressed for itself; everything else on the card takes hold of it.
357
376
  if (
358
377
  event.button !== 0 ||
359
- (event.target instanceof Element && event.target.closest("a, button") !== null)
378
+ (event.target instanceof Element && event.target.closest("button") !== null)
360
379
  ) {
361
380
  return;
362
381
  }
@@ -365,14 +384,26 @@ export function Board(props: BoardProps) {
365
384
  // The press is the drag's beginning and nothing else: no text is selected on the way.
366
385
  event.preventDefault();
367
386
  }
387
+ const rect = event.currentTarget.getBoundingClientRect();
368
388
  const from = { x: event.clientX, y: event.clientY };
369
- setDrag({ task, from, x: from.x, y: from.y, moving: false, hold, over: undefined });
389
+ setDrag({
390
+ task,
391
+ from,
392
+ x: from.x,
393
+ y: from.y,
394
+ grip: { x: from.x - rect.left, y: from.y - rect.top },
395
+ width: rect.width,
396
+ moving: false,
397
+ hold,
398
+ over: undefined,
399
+ });
370
400
  }, []);
371
401
 
372
402
  // While a card is held, the pointer is followed over the whole document: the column under it
373
403
  // is marked, the board scrolls when the pointer nears its edge, and the release moves the task.
374
404
  // A finger's hold is timed here, and once the card has come loose the page no longer scrolls
375
- // under the finger, nor asks for the menu a long press would otherwise bring up.
405
+ // under the finger, nor asks for the menu a long press would otherwise bring up; the click that
406
+ // follows a drag opens nothing.
376
407
  const held = drag !== null;
377
408
  useEffect(() => {
378
409
  if (!held) {
@@ -421,8 +452,18 @@ export function Board(props: BoardProps) {
421
452
  return { ...before, moving: true, over: columnUnder(before.x, before.y) };
422
453
  });
423
454
  };
455
+ const swallow = (event: MouseEvent) => {
456
+ event.preventDefault();
457
+ event.stopPropagation();
458
+ };
424
459
  const release = (event: PointerEvent) => {
425
460
  const last = current.current;
461
+ if (loose.current) {
462
+ // The click the browser sends after the pointer is let go would open the title's task:
463
+ // it is swallowed, and the ear for it taken down once that moment has passed.
464
+ window.addEventListener("click", swallow, true);
465
+ window.setTimeout(() => window.removeEventListener("click", swallow, true), 0);
466
+ }
426
467
  if (last?.moving === true && event.type === "pointerup") {
427
468
  const over = columnUnder(event.clientX, event.clientY);
428
469
  if (over !== undefined && over !== last.task.state) {
@@ -506,12 +547,24 @@ export function Board(props: BoardProps) {
506
547
  );
507
548
  })}
508
549
  {dragging !== null && (
550
+ // The card in hand: the whole of it, as wide as where it stands, under the pointer where it was taken hold of.
509
551
  <div
510
552
  className="sc-drag-ghost"
511
553
  aria-hidden="true"
512
- style={{ transform: `translate(${dragging.x + 12}px, ${dragging.y + 12}px)` }}
554
+ style={{
555
+ width: dragging.width,
556
+ transform: `translate(${dragging.x - dragging.grip.x}px, ${dragging.y - dragging.grip.y}px)`,
557
+ }}
513
558
  >
514
- <span className="sc-card-number">#{dragging.task.number}</span> {dragging.task.title}
559
+ <TaskCard
560
+ task={dragging.task}
561
+ href={props.taskHref(dragging.task)}
562
+ onOpen={nothing}
563
+ today={props.today}
564
+ // The menu too, inert under the pointer, so that the card in hand keeps the shape of the one in its place.
565
+ editHref={props.editHref?.(dragging.task)}
566
+ onArchive={props.onArchive}
567
+ />
515
568
  </div>
516
569
  )}
517
570
  </div>
@@ -16,7 +16,7 @@ import type {
16
16
  import { formatDay } from "../dates.js";
17
17
  import { useLanguage, useWords } from "../i18n/index.js";
18
18
  import type { TaskFormKind } from "../router.js";
19
- import { OPEN_TASK_STATES, TASK_STATES } from "../vocabulary.js";
19
+ import { TASK_STATES } from "../vocabulary.js";
20
20
  import { type CommentDraft, TaskThread } from "./comments.js";
21
21
  import { DecisionForm } from "./decision-form.js";
22
22
  import { EventFeed } from "./event-feed.js";
@@ -354,17 +354,15 @@ export function TaskView(props: TaskViewProps) {
354
354
  <a className="sc-button sc-button-secondary" href={props.formHref("ask")}>
355
355
  {words.task.raiseDecision}
356
356
  </a>
357
- {props.onArchive !== undefined &&
358
- task.archivedAt === null &&
359
- !OPEN_TASK_STATES.includes(task.state) && (
360
- <Button
361
- variant="secondary"
362
- disabled={props.busy === true}
363
- onClick={() => props.onArchive?.(task)}
364
- >
365
- {words.task.archive}
366
- </Button>
367
- )}
357
+ {props.onArchive !== undefined && task.archivedAt === null && (
358
+ <Button
359
+ variant="secondary"
360
+ disabled={props.busy === true}
361
+ onClick={() => props.onArchive?.(task)}
362
+ >
363
+ {words.task.archive}
364
+ </Button>
365
+ )}
368
366
  {props.onRestore !== undefined && task.archivedAt !== null && (
369
367
  <Button
370
368
  variant="secondary"
package/src/console.tsx CHANGED
@@ -738,7 +738,14 @@ function ProjectPage(props: {
738
738
  return (
739
739
  <>
740
740
  <div className="sc-page-head">
741
- <h1 className="sc-page-title">{project.name}</h1>
741
+ <div className="sc-page-heading">
742
+ <h1 className="sc-page-title">{project.name}</h1>
743
+ {project.description.length > 0 && (
744
+ <p className="sc-page-lead" title={project.description}>
745
+ {project.description}
746
+ </p>
747
+ )}
748
+ </div>
742
749
  <div className="sc-page-actions">
743
750
  <LinkButton href={projectHref(key, "archive")}>{words.board.archive}</LinkButton>
744
751
  <LinkButton href={newTaskHref(key)} variant="primary">
@@ -746,7 +753,6 @@ function ProjectPage(props: {
746
753
  </LinkButton>
747
754
  </div>
748
755
  </div>
749
- {project.description.length > 0 && <p className="sc-lead">{project.description}</p>}
750
756
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
751
757
  {data.tasks.length > 0 && (
752
758
  <BoardFilters
package/src/i18n/en.ts CHANGED
@@ -605,7 +605,7 @@ export const en = {
605
605
  loading: "Loading the archive",
606
606
  empty: {
607
607
  title: "Nothing is put away",
608
- body: "A task done or dropped is put away from its page, with its goal, or by the console after a while.",
608
+ body: "A task is put away from its page or its card, with its goal once the goal is closed, or by the console a while after it was done or dropped.",
609
609
  },
610
610
  since: "put away",
611
611
  restore: "Bring back",
package/src/i18n/ko.ts CHANGED
@@ -569,7 +569,7 @@ export const ko: Messages = {
569
569
  loading: "보관함을 불러오는 중",
570
570
  empty: {
571
571
  title: "보관된 작업이 없습니다",
572
- body: "완료·폐기된 작업은 작업 페이지에서, 목표를 닫을 때 함께, 또는 일정 기간 뒤 콘솔이 보관합니다.",
572
+ body: "작업은 작업 페이지나 카드 메뉴에서, 목표를 닫을 때 함께, 또는 완료·폐기된 뒤 일정 기간이 지나면 콘솔이 보관합니다.",
573
573
  },
574
574
  since: "보관",
575
575
  restore: "되살리기",
package/src/rules.ts CHANGED
@@ -23,12 +23,13 @@ export const REFERENCE_RULES = [
23
23
  "",
24
24
  "## Start here, every time",
25
25
  "",
26
- "1. These rules come with the task: `console take` prints them under it, and `console rules` reads them any time. Read them before anything is made. The task says where the work is and what it links to; `console task <number>` shows a task in full, with its plan, its decisions, the runs on it and the discussion on it, without taking it. A task put away (archived) once done or dropped is out of the listings and the board, and opens by its number still. How the board is worked is the skill working-the-board, shipped with the command (`console skills` says where it is); these rules say what this organization asks, not how the command works.",
26
+ "1. These rules come with the task: `console take` prints them under it, and `console rules` reads them any time. Read them before anything is made. The task says where the work is and what it links to; `console task <number>` shows a task in full, with its plan, its decisions, the runs on it and the discussion on it, without taking it. A task put away (archived) is out of the listings and the board, and opens by its number still. How the board is worked is the skill working-the-board, shipped with the command: read it before the first command of a run (`console skills` prints where it is); these rules say what this organization asks, not how the command works.",
27
27
  "2. Act as the person whose token the command holds, and no further. What they may not do, the console refuses; that step goes back to them in a report, never around them.",
28
+ '3. Work given in chat, with no task on the board yet, goes on the board before it is done: write it (`console task new "<title>" --body <markdown>`), tell the person its number, take it (`console take <number>`) and work it as any task, without asking whether to. Until the task is written, the board is only read.',
28
29
  "",
29
30
  "## The organization's skills",
30
31
  "",
31
- "`console skills` lists the skills of this project's pack: each with its name, what it does, and the path of its SKILL.md. `console skill <path>` reads one whole, its rules first, as the console reads it from the SkillCDN deployment for you, a private pack with the pack token the project holds; `console skill <path> --file <file>` reads a file of the pack. Where your host lists skills through a connection of your own to the address, the URI `skill://gh/<owner>/<repo>/<path>` is the skill there, followed until its context is complete. Either way, never fetch a skill another way. Load the skill the task names before anything is made; with none named, the one whose description fits the work. The rules those skills share arrive with each of them, from their repository's SKILLCDN.md, and hold within that work. What you read of a private pack is the project's: it stays in your work, and is not copied into a report, a page or a comment beyond what the work needs.",
32
+ "`console skills` lists the skills of this project's pack: each with its name, what it does, and the path of its SKILL.md; `console skills <folder>` shows what a folder of the pack holds, and `console skills --search <words>` finds its skills and documents by words. `console skill <path>` reads one whole, its rules first, as the console reads it from the SkillCDN deployment for you, a private pack with the pack token the project holds; `console skill --file <file>` reads a file of the pack. Where your host lists skills through a connection of your own to the address, the URI `skill://gh/<owner>/<repo>/<path>` is the skill there, followed until its context is complete. Either way, never fetch a skill another way. Load the skill the task names before anything is made; with none named, the one whose description fits the work. The rules those skills share arrive with each of them, from their repository's SKILLCDN.md, and hold within that work. What you read of a private pack is the project's: it stays in your work, and is not copied into a report, a page or a comment beyond what the work needs.",
32
33
  "",
33
34
  "## Where the work lives",
34
35
  "",
package/src/schemas.ts CHANGED
@@ -915,6 +915,16 @@ export const restSkillsSchema = z.object({
915
915
  /** Where a person browses the address, or `null` without one. */
916
916
  page: z.nullable(z.string()),
917
917
  status: z.enum(SKILLS_STATUSES),
918
+ /** The pack as its manifest or its README names it, when `ready`: what it is called, what it is for, the language it is written in. */
919
+ pack: z.optional(
920
+ z.object({
921
+ name: z.nullable(z.string()),
922
+ description: z.nullable(z.string()),
923
+ language: z.nullable(z.string()),
924
+ }),
925
+ ),
926
+ /** How many skills and documents the deployment indexed at the address, when `ready`. */
927
+ counts: z.optional(z.object({ skills: count, documents: count })),
918
928
  /** The skills, when `ready`; empty otherwise. */
919
929
  items: z.array(restSkillSchema),
920
930
  });
@@ -1006,6 +1016,106 @@ export const restSkillFileSchema = z.object({
1006
1016
  });
1007
1017
  export type RestSkillFile = z.infer<typeof restSkillFileSchema>;
1008
1018
 
1019
+ /** An entry of a folder of the pack, as the deployment browses it: a skill, a folder with what it holds, or a file. */
1020
+ export const restSkillBrowseEntrySchema = z.object({
1021
+ kind: z.enum(["skill", "directory", "file"]),
1022
+ /** The path from the repository's root: of the `SKILL.md` for a skill, of the folder, of the file. */
1023
+ path: z.string(),
1024
+ name: z.nullable(z.string()),
1025
+ description: z.nullable(z.string()),
1026
+ /** For a folder: the skills and the documents under it. */
1027
+ skillCount: count,
1028
+ documentCount: count,
1029
+ /** For a file: its size in bytes. */
1030
+ size: z.nullable(count),
1031
+ /** For a folder: the README that introduces it, read with the file route. */
1032
+ overviewPath: z.nullable(z.string()),
1033
+ /** For a skill: its license, and whether the address only describes it. */
1034
+ license: z.nullable(z.string()),
1035
+ describedOnly: z.boolean(),
1036
+ });
1037
+ export type RestSkillBrowseEntry = z.infer<typeof restSkillBrowseEntrySchema>;
1038
+
1039
+ /** `GET .../skills/browse?path=&cursor=&limit=`: what a folder of the pack holds, a page of it (`browse_repo`); `folder` is `null` unless `ready`. */
1040
+ export const restSkillBrowseSchema = z.object({
1041
+ address: z.nullable(z.string()),
1042
+ source: z.string(),
1043
+ page: z.nullable(z.string()),
1044
+ status: z.enum(SKILLS_STATUSES),
1045
+ folder: z.nullable(
1046
+ z.object({
1047
+ path: z.string(),
1048
+ /** The README that introduces the folder, or `null`. */
1049
+ overview: z.nullable(
1050
+ z.object({
1051
+ path: z.string(),
1052
+ title: z.nullable(z.string()),
1053
+ description: z.nullable(z.string()),
1054
+ }),
1055
+ ),
1056
+ entries: z.array(restSkillBrowseEntrySchema),
1057
+ /** Where the listing goes on, or `null` at its end. */
1058
+ nextCursor: z.nullable(z.string()),
1059
+ }),
1060
+ ),
1061
+ });
1062
+ export type RestSkillBrowse = z.infer<typeof restSkillBrowseSchema>;
1063
+
1064
+ /** What a search of the pack found: a skill, as the listing has it, with the files of it that matched; or a document on its own. */
1065
+ export const restSkillFindItemSchema = z.discriminatedUnion("kind", [
1066
+ z.object({
1067
+ kind: z.literal("skill"),
1068
+ name: z.string(),
1069
+ description: z.string(),
1070
+ directory: z.string(),
1071
+ path: z.string(),
1072
+ page: z.string(),
1073
+ uri: z.nullable(z.string()),
1074
+ translations: z.record(
1075
+ z.string(),
1076
+ z.object({ title: z.nullable(z.string()), description: z.nullable(z.string()) }),
1077
+ ),
1078
+ /** The files of the skill that matched, a few, and how many more did. */
1079
+ files: z.array(
1080
+ z.object({
1081
+ path: z.string(),
1082
+ title: z.nullable(z.string()),
1083
+ summary: z.nullable(z.string()),
1084
+ }),
1085
+ ),
1086
+ moreFiles: count,
1087
+ license: z.nullable(z.string()),
1088
+ describedOnly: z.boolean(),
1089
+ }),
1090
+ z.object({
1091
+ kind: z.literal("document"),
1092
+ path: z.string(),
1093
+ title: z.nullable(z.string()),
1094
+ summary: z.nullable(z.string()),
1095
+ /** The folder of the skill the document belongs to, or `null` for one on its own. */
1096
+ skillDirectory: z.nullable(z.string()),
1097
+ }),
1098
+ ]);
1099
+ export type RestSkillFindItem = z.infer<typeof restSkillFindItemSchema>;
1100
+
1101
+ /** `GET .../skills/find?query=&path=&cursor=&limit=`: what words find in the pack, a page of it in relevance order (`search_repo`); `found` is `null` unless `ready`. */
1102
+ export const restSkillFindSchema = z.object({
1103
+ address: z.nullable(z.string()),
1104
+ source: z.string(),
1105
+ page: z.nullable(z.string()),
1106
+ status: z.enum(SKILLS_STATUSES),
1107
+ found: z.nullable(
1108
+ z.object({
1109
+ query: z.string(),
1110
+ /** The folder the search was kept to, or `null` for the whole pack. */
1111
+ path: z.nullable(z.string()),
1112
+ items: z.array(restSkillFindItemSchema),
1113
+ nextCursor: z.nullable(z.string()),
1114
+ }),
1115
+ ),
1116
+ });
1117
+ export type RestSkillFind = z.infer<typeof restSkillFindSchema>;
1118
+
1009
1119
  /**
1010
1120
  * A document's path (ADR-0009): segments of lowercase letters, digits and hyphens, separated
1011
1121
  * by slashes, as a repository keeps files; the document's address, which does not change.