@skillcdn/console 0.1.17 → 0.1.19

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 (154) hide show
  1. package/README.md +4 -4
  2. package/SKILLCDN.md +1 -1
  3. package/dist/api.d.ts +3 -2
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +2 -1
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts +11 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +496 -51
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/cli/credentials.d.ts +102 -3
  12. package/dist/cli/credentials.d.ts.map +1 -1
  13. package/dist/cli/credentials.js +177 -8
  14. package/dist/cli/credentials.js.map +1 -1
  15. package/dist/cli/directory.d.ts +6 -0
  16. package/dist/cli/directory.d.ts.map +1 -1
  17. package/dist/cli/directory.js +37 -6
  18. package/dist/cli/directory.js.map +1 -1
  19. package/dist/cli/format.d.ts +11 -2
  20. package/dist/cli/format.d.ts.map +1 -1
  21. package/dist/cli/format.js +114 -5
  22. package/dist/cli/format.js.map +1 -1
  23. package/dist/cli/help.d.ts.map +1 -1
  24. package/dist/cli/help.js +49 -25
  25. package/dist/cli/help.js.map +1 -1
  26. package/dist/cli/install.d.ts +21 -0
  27. package/dist/cli/install.d.ts.map +1 -0
  28. package/dist/cli/install.js +87 -0
  29. package/dist/cli/install.js.map +1 -0
  30. package/dist/cli/main.js +22 -3
  31. package/dist/cli/main.js.map +1 -1
  32. package/dist/cli/notices.d.ts +13 -0
  33. package/dist/cli/notices.d.ts.map +1 -0
  34. package/dist/cli/notices.js +31 -0
  35. package/dist/cli/notices.js.map +1 -0
  36. package/dist/client.d.ts +36 -3
  37. package/dist/client.d.ts.map +1 -1
  38. package/dist/client.js +22 -4
  39. package/dist/client.js.map +1 -1
  40. package/dist/compat.d.ts +66 -0
  41. package/dist/compat.d.ts.map +1 -0
  42. package/dist/compat.js +115 -0
  43. package/dist/compat.js.map +1 -0
  44. package/dist/components/board.d.ts +9 -1
  45. package/dist/components/board.d.ts.map +1 -1
  46. package/dist/components/board.js +82 -5
  47. package/dist/components/board.js.map +1 -1
  48. package/dist/components/calendar.d.ts +3 -1
  49. package/dist/components/calendar.d.ts.map +1 -1
  50. package/dist/components/calendar.js +21 -2
  51. package/dist/components/calendar.js.map +1 -1
  52. package/dist/components/comments.d.ts.map +1 -1
  53. package/dist/components/comments.js +12 -4
  54. package/dist/components/comments.js.map +1 -1
  55. package/dist/components/event-feed.d.ts.map +1 -1
  56. package/dist/components/event-feed.js +4 -0
  57. package/dist/components/event-feed.js.map +1 -1
  58. package/dist/components/goals.d.ts +20 -2
  59. package/dist/components/goals.d.ts.map +1 -1
  60. package/dist/components/goals.js +50 -13
  61. package/dist/components/goals.js.map +1 -1
  62. package/dist/components/markdown.d.ts +6 -0
  63. package/dist/components/markdown.d.ts.map +1 -1
  64. package/dist/components/markdown.js +7 -0
  65. package/dist/components/markdown.js.map +1 -1
  66. package/dist/components/pictures.d.ts +68 -0
  67. package/dist/components/pictures.d.ts.map +1 -0
  68. package/dist/components/pictures.js +213 -0
  69. package/dist/components/pictures.js.map +1 -0
  70. package/dist/components/projects.d.ts +12 -0
  71. package/dist/components/projects.d.ts.map +1 -1
  72. package/dist/components/projects.js +15 -0
  73. package/dist/components/projects.js.map +1 -1
  74. package/dist/components/roadmap.d.ts +37 -0
  75. package/dist/components/roadmap.d.ts.map +1 -0
  76. package/dist/components/roadmap.js +76 -0
  77. package/dist/components/roadmap.js.map +1 -0
  78. package/dist/components/task-view.d.ts +3 -0
  79. package/dist/components/task-view.d.ts.map +1 -1
  80. package/dist/components/task-view.js +6 -4
  81. package/dist/components/task-view.js.map +1 -1
  82. package/dist/components/tokens.d.ts +2 -0
  83. package/dist/components/tokens.d.ts.map +1 -1
  84. package/dist/components/tokens.js +13 -3
  85. package/dist/components/tokens.js.map +1 -1
  86. package/dist/console.css +486 -3
  87. package/dist/console.d.ts.map +1 -1
  88. package/dist/console.js +132 -17
  89. package/dist/console.js.map +1 -1
  90. package/dist/data.d.ts +16 -1
  91. package/dist/data.d.ts.map +1 -1
  92. package/dist/data.js +48 -3
  93. package/dist/data.js.map +1 -1
  94. package/dist/i18n/en.d.ts +76 -0
  95. package/dist/i18n/en.d.ts.map +1 -1
  96. package/dist/i18n/en.js +74 -2
  97. package/dist/i18n/en.js.map +1 -1
  98. package/dist/i18n/ko.d.ts.map +1 -1
  99. package/dist/i18n/ko.js +74 -2
  100. package/dist/i18n/ko.js.map +1 -1
  101. package/dist/index.d.ts +6 -4
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +6 -4
  104. package/dist/index.js.map +1 -1
  105. package/dist/router.d.ts +31 -7
  106. package/dist/router.d.ts.map +1 -1
  107. package/dist/router.js +48 -8
  108. package/dist/router.js.map +1 -1
  109. package/dist/rules.js +5 -5
  110. package/dist/rules.js.map +1 -1
  111. package/dist/schemas.d.ts +200 -0
  112. package/dist/schemas.d.ts.map +1 -1
  113. package/dist/schemas.js +96 -0
  114. package/dist/schemas.js.map +1 -1
  115. package/dist/vocabulary.d.ts +2 -2
  116. package/dist/vocabulary.d.ts.map +1 -1
  117. package/dist/vocabulary.js +3 -0
  118. package/dist/vocabulary.js.map +1 -1
  119. package/docs/specs/cli.md +26 -15
  120. package/docs/specs/rest.md +31 -9
  121. package/package.json +3 -2
  122. package/skills/working-the-board/SKILL.md +11 -4
  123. package/src/api.ts +36 -0
  124. package/src/cli/cli.ts +616 -54
  125. package/src/cli/credentials.ts +276 -14
  126. package/src/cli/directory.ts +43 -6
  127. package/src/cli/format.ts +146 -5
  128. package/src/cli/help.ts +51 -25
  129. package/src/cli/install.ts +111 -0
  130. package/src/cli/main.ts +23 -3
  131. package/src/cli/notices.ts +48 -0
  132. package/src/client.ts +91 -6
  133. package/src/compat.ts +173 -0
  134. package/src/components/board.tsx +154 -8
  135. package/src/components/calendar.tsx +41 -1
  136. package/src/components/comments.tsx +20 -10
  137. package/src/components/event-feed.tsx +4 -0
  138. package/src/components/goals.tsx +234 -88
  139. package/src/components/markdown.tsx +19 -0
  140. package/src/components/pictures.tsx +389 -0
  141. package/src/components/projects.tsx +68 -0
  142. package/src/components/roadmap.tsx +242 -0
  143. package/src/components/task-view.tsx +43 -2
  144. package/src/components/tokens.tsx +38 -1
  145. package/src/console.tsx +278 -4
  146. package/src/data.ts +73 -2
  147. package/src/i18n/en.ts +77 -2
  148. package/src/i18n/ko.ts +77 -2
  149. package/src/index.ts +35 -0
  150. package/src/router.ts +70 -11
  151. package/src/rules.ts +5 -5
  152. package/src/schemas.ts +117 -0
  153. package/src/styles/console.css +486 -3
  154. package/src/vocabulary.ts +3 -0
package/src/cli/help.ts CHANGED
@@ -84,23 +84,28 @@ const TASK_FIELD_REFUSALS = [
84
84
 
85
85
  export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
86
86
  login: {
87
- usage: "login --url <origin> [--agent <name>] [--token-stdin]",
87
+ usage: "login --url <origin> [--agent <name>] [--token-stdin] [--default]",
88
88
  about:
89
- "Connects this agent to the console: shows an address and a code, which a person opens signed in, names the agent and approves; the command then receives a token of its own and keeps it, with the console's address, in your home directory, never showing it. An agent running the command relays the address and the code to its person and waits. --agent says what the agent calls itself; left out, the command names the agent it runs under where its environment says so (Claude Code, Codex), else An agent; the machine's name follows, and the pages show the name before 'on' with the whole on hover. --token-stdin reads a token made by hand from standard input instead, for a script or a job.",
89
+ "Connects this agent to the console: shows an address and a code, which a person opens signed in, names the agent and approves; the command then receives a token of its own and keeps it, with the console's address and the account, in your home directory, never showing it. One login per console and account: a second console adds a login beside the first, the first is the default console a directory that names none works at, and --default makes this one that. A login repeated for the same console and account replaces its token, which is given up at the console. An agent running the command relays the address and the code to its person and waits. --agent says what the agent calls itself; left out, the command names the agent it runs under where its environment says so (Claude Code, Codex), else An agent; the machine's name follows, and the pages show the name before 'on' with the whole on hover. --token-stdin reads a token made by hand from standard input instead, for a script or a job.",
90
90
  refusals: [
91
91
  "connect.not_found: the code was not approved in time, or the person said it was not theirs; run login again",
92
92
  "the console does not know the token given: make one on the Agents page and try again",
93
93
  ],
94
94
  },
95
+ logins: {
96
+ usage: "logins",
97
+ about:
98
+ "The consoles this machine is signed in to, each with the account, what the token is called there, since when, and which is the default; never a token.",
99
+ },
95
100
  logout: {
96
- usage: "logout",
101
+ usage: "logout [--url <origin>] [--account <login>]",
97
102
  about:
98
- "Signs this agent out: gives the token up at the console, so that it is good for nothing from now on, and forgets it with the address kept here. A console out of reach leaves the token to the Agents page.",
103
+ "Signs this agent out of one console: gives the token up there, so that it is good for nothing from now on, and forgets it here. Which console: --url, else the one this directory is bound to, else the default, else the one console signed in to; --account says which account where the console has several. A console out of reach leaves the token to the Agents page.",
99
104
  },
100
105
  whoami: {
101
106
  usage: "whoami",
102
107
  about:
103
- "Who the token acts as, which console it is for, and which project this directory works in.",
108
+ "Who the token acts as, which console the command talks to from here and as which account, and which project this directory works in.",
104
109
  },
105
110
  projects: {
106
111
  usage: "projects",
@@ -108,9 +113,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
108
113
  "The projects you may work in: each with its key, its name, what you are in it, and what waits in it. Every command on the board works in one project.",
109
114
  },
110
115
  use: {
111
- usage: "use <key>",
116
+ usage: "use <key> [--url <origin>] [--account <login>]",
112
117
  about:
113
- "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.",
118
+ "Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names.",
114
119
  refusals: ["project.not_found: no project has the key, or it is not yours to see"],
115
120
  },
116
121
  work: {
@@ -120,37 +125,43 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
120
125
  },
121
126
  tasks: {
122
127
  usage:
123
- "tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--limit <n>] [--offset <n>]",
124
- about: `The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --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). A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
128
+ "tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>] [--unassigned] [--archived] [--limit <n>] [--offset <n>]",
129
+ about: `The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --unassigned those assigned to nobody (not with --mine), --category and --goal those sorted into one, --due those due by a day: today, within the week, or overdue (the open ones past their day). The tasks put away (ADR-0029) are left out unless --archived asks for them alone. A page at a time: ${DEFAULT_PAGE_LIMIT} unless --limit says otherwise, from --offset on; a last line says when there is more. States: idea, ready, in_progress, in_review, done, dropped.`,
125
130
  },
126
131
  task: {
127
- 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}`,
132
+ usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task archive <number|id> | task archive --goal <id> | task restore <number|id>`,
128
133
  about:
129
- "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, the runs on it and the discussion on it, each comment whole with its files; 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).",
134
+ "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, and when it was put away), the decisions about it, the runs on it and the discussion on it, each comment whole with its files; 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). archive puts a done or dropped task away (ADR-0029): out of the listings, the board and the calendar, open by its number still; with --goal, every done or dropped task of the goal at once, for an owner. restore brings one back.",
130
135
  limits: [
131
136
  `title: one line, up to ${MAX_TITLE_LENGTH} characters, saying what is to be done on its own; what it refers to goes in the body`,
132
137
  `body: ${MARKDOWN}`,
133
138
  ...TASK_FIELD_LIMITS,
134
139
  ],
135
- refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
140
+ refusals: [
141
+ "task.not_found: no task has that number or id",
142
+ "task.open: an open task is not put away; one done or dropped is",
143
+ ...TASK_FIELD_REFUSALS,
144
+ ],
136
145
  },
137
146
  goals: {
138
- usage: "goals",
147
+ usage: "goals [--category <key>] [--state <state>]",
139
148
  about:
140
- "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.",
149
+ "The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its category, the days it starts and is meant to be reached by, and how far it is, counted from the tasks that work towards it. --category keeps those sorted into one, --state those in one state.",
141
150
  },
142
151
  goal: {
143
152
  usage:
144
- '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>]',
153
+ 'goal <id> | goal new "<title>" [--body <markdown>|--file <path>] [--state <state>] [--category <key>] [--starts <day>] [--due <day>] | goal edit <id> [--title <title>] [--body <markdown>|--file <path>] [--state <state>] [--category <key|none>] [--starts <day|none>] [--due <day|none>]',
145
154
  about:
146
- "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.",
155
+ "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, where none clears a category or a day. A goal is planned in days as a task is, a start and a target, and sorted into a category of the workspace (console categories). A task works towards a goal through its --goal.",
147
156
  limits: [
148
157
  `title: one line, up to ${MAX_TITLE_LENGTH} characters`,
149
158
  `body: ${MARKDOWN}`,
150
- `due: ${DAY}`,
159
+ `starts, due: ${DAY}`,
151
160
  ],
152
161
  refusals: [
153
162
  "goal.not_found: no goal of this project has that id",
163
+ "goal.invalid_category: no category of the workspace has that key",
164
+ "goal.invalid_dates: the target day is before the start",
154
165
  "auth.forbidden: only an owner of the project sets its roadmap",
155
166
  ],
156
167
  },
@@ -179,9 +190,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
179
190
  },
180
191
  project: {
181
192
  usage:
182
- 'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove',
193
+ 'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --stdin | project pack-token <key> --remove',
183
194
  about:
184
- "One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away. The organization's skills are the project's unless --skills names an address of its own.",
195
+ "One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its pack token (ADR-0028), the token a person made at SkillCDN for a private pack, read from standard input and never an argument, which the console keeps sealed to read the pack with, or taken away. The organization's skills are the project's unless --skills names an address of its own.",
185
196
  limits: [
186
197
  `key: ${KEY}, up to ${MAX_PROJECT_KEY_LENGTH} characters; it does not change once made`,
187
198
  `name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
@@ -204,9 +215,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
204
215
  },
205
216
  member: {
206
217
  usage:
207
- "member add <login> [--role owner|member] | member role <login> <owner|member> | member remove <login>",
218
+ "member add <login> [--role owner|member] [--project <key>[,<key>...]] | member role <login> <owner|member> | member remove <login>",
208
219
  about:
209
- "Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project.",
220
+ "Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
210
221
  refusals: [
211
222
  "auth.forbidden: only an owner of the project lists its members",
212
223
  "member.invalid_person: the person is not of the workspace",
@@ -235,7 +246,17 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
235
246
  skills: {
236
247
  usage: "skills",
237
248
  about:
238
- "The organization's skills, as SkillCDN serves them at the project's address: each with its description and the URI you load it by through your own SkillCDN connection, which a person makes at the address's page when you have none; and where the console's own skills are on this machine. --json adds the page of each, for a person.",
249
+ "The skills of the project's pack, as SkillCDN serves them at the project's address: each with its description, the path of its SKILL.md, which console skill reads it by, and the URI you load it by through your own SkillCDN connection where your host has one; and where the console's own skills are on this machine. A private pack is read with the pack token the project holds (ADR-0028). --json adds the page of each, for a person.",
250
+ },
251
+ skill: {
252
+ usage: "skill <path> [--file <path>]",
253
+ about:
254
+ "One skill of the project's pack, whole, as load_skill lays it out for you: the rules of the repository from its root to the nearest manifest, the skill's body, the files it includes, each under a line that names its path; then the files it has and what it refers to. Read by the console from the SkillCDN deployment, with the pack token the project holds where the pack is private, so you need no connection of your own. <path> is the path of the SKILL.md from the repository's root, as console skills lists it. --file <path> reads a file of the pack instead, whole, or what a folder holds. What you read is the pack's, under its license: what a private pack says stays in your work and is not copied into reports or pages beyond what the work needs.",
255
+ refusals: [
256
+ "status not_found: the pack is not served to the console; private, with no pack token on the project",
257
+ "status token_invalid: the pack token the project holds no longer works; an owner sets a new one",
258
+ "status failed, for a file: SkillCDN will not carry it, too large or not text",
259
+ ],
239
260
  },
240
261
  rules: {
241
262
  usage:
@@ -447,14 +468,18 @@ Exit codes: 0 done; 1 the console refused, or could not be reached (the reason i
447
468
  What the console refuses is printed as its code and its words. Everywhere:
448
469
  ${COMMON_REFUSALS.map((refusal) => ` ${refusal}`).join("\n")}
449
470
 
450
- Credentials: CONSOLE_URL and CONSOLE_TOKEN in the environment, else what login kept in
451
- $XDG_CONFIG_HOME/skillcdn-console/credentials.json (~/.config when XDG_CONFIG_HOME is unset).
471
+ Which console a command talks to: --url, else CONSOLE_URL in the environment, else the console
472
+ .skillcdn-console.json binds this directory to, else the default login. The token is then the one
473
+ kept for that console alone (CONSOLE_TOKEN only together with CONSOLE_URL, for a hook or a job), in
474
+ $XDG_CONFIG_HOME/skillcdn-console/credentials.json (~/.config when XDG_CONFIG_HOME is unset);
475
+ a console nobody is signed in to here gets no request. --account <login> says which account where
476
+ a console has several. console logins lists them.
452
477
  `;
453
478
 
454
479
  const SECTIONS: readonly { readonly title: string; readonly commands: readonly string[] }[] = [
455
480
  {
456
481
  title: "Signing in, once, as a person, and the project to work in",
457
- commands: ["login", "logout", "whoami", "projects", "use"],
482
+ commands: ["login", "logins", "logout", "whoami", "projects", "use"],
458
483
  },
459
484
  {
460
485
  title: "The board",
@@ -470,6 +495,7 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
470
495
  "runs",
471
496
  "run",
472
497
  "skills",
498
+ "skill",
473
499
  "rules",
474
500
  ],
475
501
  },
@@ -0,0 +1,111 @@
1
+ // Where the command runs from, so that the update it is told to make is the one that takes
2
+ // effect: installed for the machine, in a project, run once from a cache, or a checkout of the
3
+ // repository. Read from the package's own path and the working directory; nothing is asked of
4
+ // the system, so that a notice about versions costs a command nothing.
5
+
6
+ export type InstallKind = "global" | "project" | "temporary" | "checkout";
7
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
8
+
9
+ export interface Install {
10
+ readonly kind: InstallKind;
11
+ readonly manager: PackageManager;
12
+ /** The directory the package is installed in: a project's root, the machine's global one, a cache's; the checkout itself. */
13
+ readonly root: string;
14
+ }
15
+
16
+ export interface InstallClues {
17
+ /** The package's own directory, where its manifest is. */
18
+ readonly packageDir: string;
19
+ readonly cwd: string;
20
+ readonly env: Readonly<Record<string, string | undefined>>;
21
+ readonly exists: (path: string) => boolean;
22
+ }
23
+
24
+ const PACKAGE = "@skillcdn/console";
25
+
26
+ const slashes = (path: string): string => path.replaceAll("\\", "/").replace(/\/+$/, "");
27
+
28
+ /** The package manager that ran this command, when one did: `pnpm exec`, `npx` and their kind say so. */
29
+ function managerRunning(env: InstallClues["env"]): PackageManager | undefined {
30
+ const agent = env.npm_config_user_agent ?? "";
31
+ for (const manager of ["pnpm", "yarn", "bun", "npm"] as const) {
32
+ if (agent.startsWith(`${manager}/`)) {
33
+ return manager;
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ export function detectInstall(clues: InstallClues): Install {
40
+ const dir = slashes(clues.packageDir);
41
+ const cwd = slashes(clues.cwd);
42
+ const at = dir.indexOf("/node_modules/");
43
+ if (at < 0) {
44
+ return { kind: "checkout", manager: "pnpm", root: dir };
45
+ }
46
+ const root = dir.slice(0, at);
47
+ const lower = dir.toLowerCase();
48
+ const running = managerRunning(clues.env);
49
+ // What `npx`, `pnpm dlx`, `yarn dlx` and `bunx` fetch into a cache lives only for the run.
50
+ if (/\/(_npx|dlx|dlx-[^/]*)\//.test(lower) || lower.includes("/.bun/install/cache/")) {
51
+ const manager =
52
+ running ??
53
+ (lower.includes("/_npx/") ? "npm" : lower.includes("/.bun/install/cache/") ? "bun" : "pnpm");
54
+ return { kind: "temporary", manager, root };
55
+ }
56
+ if (cwd === root || cwd.startsWith(`${root}/`)) {
57
+ const manager: PackageManager = clues.exists(`${root}/pnpm-lock.yaml`)
58
+ ? "pnpm"
59
+ : clues.exists(`${root}/yarn.lock`)
60
+ ? "yarn"
61
+ : clues.exists(`${root}/bun.lock`) || clues.exists(`${root}/bun.lockb`)
62
+ ? "bun"
63
+ : (running ?? "npm");
64
+ return { kind: "project", manager, root };
65
+ }
66
+ const manager: PackageManager = /\/pnpm\/global\/|\/\.pnpm-global\//.test(lower)
67
+ ? "pnpm"
68
+ : lower.includes("/yarn/global/")
69
+ ? "yarn"
70
+ : lower.includes("/.bun/install/global/")
71
+ ? "bun"
72
+ : "npm";
73
+ return { kind: "global", manager, root };
74
+ }
75
+
76
+ /** The command that puts the version named where this command is installed. */
77
+ export function updateCommandFor(install: Install, version: string): string {
78
+ const spec = `${PACKAGE}@${version}`;
79
+ switch (install.kind) {
80
+ case "global":
81
+ return install.manager === "npm"
82
+ ? `npm install -g ${spec}`
83
+ : install.manager === "yarn"
84
+ ? `yarn global add ${spec}`
85
+ : `${install.manager} add -g ${spec}`;
86
+ case "project":
87
+ return `${install.manager === "npm" ? "npm install" : `${install.manager} add`} ${spec}`;
88
+ case "temporary":
89
+ return install.manager === "npm"
90
+ ? `npx ${spec}`
91
+ : install.manager === "bun"
92
+ ? `bunx ${spec}`
93
+ : `${install.manager} dlx ${spec}`;
94
+ case "checkout":
95
+ return "git pull && pnpm install && pnpm build";
96
+ }
97
+ }
98
+
99
+ /** Where the command is installed, in words: what `whoami` and `--version` say. */
100
+ export function describeInstall(install: Install): string {
101
+ switch (install.kind) {
102
+ case "global":
103
+ return `installed for the machine with ${install.manager} in ${install.root}`;
104
+ case "project":
105
+ return `installed in the project at ${install.root} with ${install.manager}`;
106
+ case "temporary":
107
+ return `fetched for this run by ${install.manager}`;
108
+ case "checkout":
109
+ return `built from the checkout at ${install.root}`;
110
+ }
111
+ }
package/src/cli/main.ts CHANGED
@@ -5,21 +5,28 @@ import { hostname } from "node:os";
5
5
  import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import * as z from "zod/mini";
8
+ import { userAgentOf } from "../compat.js";
8
9
  import { runCli } from "./cli.js";
9
10
  import { createFileStore, defaultConfigDir } from "./credentials.js";
10
11
  import { createDirectoryStore } from "./directory.js";
12
+ import { detectInstall } from "./install.js";
13
+ import { createNoticeStore } from "./notices.js";
11
14
  import { listenOn } from "./serve.js";
12
15
  import { readBytesAt, readFileAt, readStdin, writeBytesAt } from "./terminal.js";
13
16
 
14
17
  // The `console` command: the only place the command reads the process. Everything it does is in
15
18
  // cli.ts, which takes what it needs and is what the tests run.
16
19
 
17
- /** The version of the package this file runs from: its own manifest, two levels above dist/cli. */
20
+ /** The package's own directory, two levels above dist/cli: where its manifest and its skills are. */
21
+ const packageDir = fileURLToPath(new URL("../../", import.meta.url));
22
+ /** The version of the package this file runs from: its own manifest. */
18
23
  const manifest = z
19
24
  .object({ version: z.string() })
20
25
  .safeParse(createRequire(import.meta.url)("../../package.json"));
26
+ const version = manifest.success ? manifest.data.version : undefined;
21
27
  /** The console's own skills, shipped with the package beside dist/ (ADR-0015); not there in a checkout before packing. */
22
28
  const skillsDir = fileURLToPath(new URL("../../skills/", import.meta.url));
29
+ const configDir = defaultConfigDir(process.env);
23
30
 
24
31
  process.exitCode = await runCli(process.argv.slice(2), {
25
32
  env: process.env,
@@ -35,11 +42,24 @@ process.exitCode = await runCli(process.argv.slice(2), {
35
42
  readFile: readFileAt,
36
43
  readBytes: readBytesAt,
37
44
  writeBytes: writeBytesAt,
38
- store: createFileStore(defaultConfigDir(process.env)),
45
+ store: createFileStore(configDir),
39
46
  directory: createDirectoryStore(process.cwd()),
40
47
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
41
48
  now: () => Date.now(),
42
- version: manifest.success ? manifest.data.version : undefined,
49
+ version,
50
+ // The command names itself on every request, and where it runs, so that the console holds it
51
+ // to the versions it takes and the access log says which commands are about (ADR-0025).
52
+ userAgent:
53
+ version === undefined
54
+ ? undefined
55
+ : userAgentOf(version, `cli; node ${process.versions.node}; ${process.platform}`),
56
+ install: detectInstall({
57
+ packageDir,
58
+ cwd: process.cwd(),
59
+ env: process.env,
60
+ exists: existsSync,
61
+ }),
62
+ notices: createNoticeStore(configDir),
43
63
  skillsDir: existsSync(skillsDir) ? skillsDir : undefined,
44
64
  listen: listenOn,
45
65
  });
@@ -0,0 +1,48 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import * as z from "zod/mini";
4
+
5
+ // What the command last said about the versions, kept beside the credentials, so that it says
6
+ // it once a day for one pair of versions and not on every command an agent runs. A notice is
7
+ // not worth failing a command for: a file that cannot be read or written is as good as none.
8
+
9
+ export interface VersionNotice {
10
+ /** The version of the command that noticed. */
11
+ readonly command: string;
12
+ /** The version of the console it noticed. */
13
+ readonly console: string;
14
+ readonly shownAt: string;
15
+ }
16
+
17
+ export interface NoticeStore {
18
+ load(): Promise<VersionNotice | undefined>;
19
+ save(notice: VersionNotice): Promise<void>;
20
+ }
21
+
22
+ const noticesSchema = z.object({
23
+ versions: z.optional(z.object({ command: z.string(), console: z.string(), shownAt: z.string() })),
24
+ });
25
+
26
+ const FILE_NAME = "notices.json";
27
+
28
+ export function createNoticeStore(directory: string): NoticeStore {
29
+ const path = join(directory, FILE_NAME);
30
+ return {
31
+ async load() {
32
+ try {
33
+ const parsed = noticesSchema.safeParse(JSON.parse(await readFile(path, "utf8")));
34
+ return parsed.success ? parsed.data.versions : undefined;
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ },
39
+ async save(notice) {
40
+ try {
41
+ await mkdir(directory, { recursive: true, mode: 0o700 });
42
+ await writeFile(path, `${JSON.stringify({ versions: notice }, null, 2)}\n`);
43
+ } catch {
44
+ // Said this time; said again next time.
45
+ }
46
+ },
47
+ };
48
+ }
package/src/client.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from "./routes.js";
13
13
  import {
14
14
  type RestAnswerInput,
15
+ type RestArchivedTasks,
15
16
  type RestArtifactInput,
16
17
  type RestCategories,
17
18
  type RestCategory,
@@ -43,6 +44,7 @@ import {
43
44
  type RestMemberPatch,
44
45
  type RestMembers,
45
46
  type RestMePatch,
47
+ type RestPackTokenInput,
46
48
  type RestPeople,
47
49
  type RestPerson,
48
50
  type RestPersonPatch,
@@ -59,6 +61,8 @@ import {
59
61
  type RestRunEndInput,
60
62
  type RestRunInput,
61
63
  type RestRuns,
64
+ type RestSkillFile,
65
+ type RestSkillLoad,
62
66
  type RestSkills,
63
67
  type RestTask,
64
68
  type RestTaskInput,
@@ -70,6 +74,7 @@ import {
70
74
  type RestVersion,
71
75
  type RestVersions,
72
76
  type RestWork,
77
+ restArchivedTasksSchema,
73
78
  restCategoriesSchema,
74
79
  restCategorySchema,
75
80
  restClaimSchema,
@@ -98,6 +103,8 @@ import {
98
103
  restRulesSchema,
99
104
  restRunSchema,
100
105
  restRunsSchema,
106
+ restSkillFileSchema,
107
+ restSkillLoadSchema,
101
108
  restSkillsSchema,
102
109
  restTaskSchema,
103
110
  restTasksSchema,
@@ -107,7 +114,7 @@ import {
107
114
  restVersionsSchema,
108
115
  restWorkSchema,
109
116
  } from "./schemas.js";
110
- import type { TaskState } from "./vocabulary.js";
117
+ import type { GoalState, TaskState } from "./vocabulary.js";
111
118
 
112
119
  // The client of the console's REST API: what the default console and a custom one talk to the
113
120
  // server with, and what the command line, a script or a console of a person's own talks to it
@@ -154,6 +161,12 @@ export interface ClientOptions {
154
161
  * the page's own origin is what the console answers to.
155
162
  */
156
163
  readonly token?: string;
164
+ /**
165
+ * What the client calls itself, sent as `user-agent` where the platform lets a client say
166
+ * (Node; a browser keeps its own): the command sends its product token, by which the console
167
+ * holds it to the versions it takes (ADR-0025).
168
+ */
169
+ readonly userAgent?: string;
157
170
  }
158
171
 
159
172
  export class ApiError extends Error {
@@ -189,6 +202,13 @@ export interface Page {
189
202
  }
190
203
 
191
204
  /** What the board is narrowed to: one state, the open ones, a category, a goal, an assignee, or what is due by a day. */
205
+ /** What the roadmap is narrowed by: the goals in one state, or sorted into one category (ADR-0026). */
206
+ export interface GoalFilter {
207
+ readonly state?: GoalState | undefined;
208
+ /** The key of a category of the workspace. */
209
+ readonly category?: string | undefined;
210
+ }
211
+
192
212
  export interface TaskFilter extends Page {
193
213
  readonly state?: TaskState | undefined;
194
214
  /** True for the tasks not done or dropped; false for the rest. */
@@ -199,8 +219,12 @@ export interface TaskFilter extends Page {
199
219
  readonly goal?: string | undefined;
200
220
  /** The id of the person the tasks are assigned to. */
201
221
  readonly assignee?: string | undefined;
222
+ /** True for the tasks assigned to nobody. */
223
+ readonly unassigned?: boolean | undefined;
202
224
  /** A day (`2026-10-20`): the tasks due on it or before it. */
203
225
  readonly dueBy?: string | undefined;
226
+ /** True for the tasks put away alone (ADR-0029); false, or left out, for the current ones. */
227
+ readonly archived?: boolean | undefined;
204
228
  }
205
229
 
206
230
  /** What a folder's listing, or a search, of the documents is narrowed to. */
@@ -236,6 +260,11 @@ export interface ProjectClient {
236
260
  task(ref: string, signal?: AbortSignal): Promise<RestTask>;
237
261
  createTask(input: RestTaskInput): Promise<RestTask>;
238
262
  updateTask(id: string, patch: RestTaskPatch): Promise<RestTask>;
263
+ /** Puts a done or dropped task away (ADR-0029), or brings one back. */
264
+ archiveTask(id: string): Promise<RestTask>;
265
+ restoreTask(id: string): Promise<RestTask>;
266
+ /** Puts every done or dropped task of a goal away at once; for an owner. Answers how many. */
267
+ archiveTasksOfGoal(goalId: string): Promise<RestArchivedTasks>;
239
268
  /** The decisions, or only those that wait, or only those about one task; a page of them. */
240
269
  decisions(
241
270
  filter?: { readonly open?: boolean; readonly task?: string } & Page,
@@ -281,6 +310,18 @@ export interface ProjectClient {
281
310
  eventStreamUrl(after: number): string;
282
311
  /** The project's skills, as SkillCDN serves them at its address, or the organization's. */
283
312
  skills(signal?: AbortSignal): Promise<RestSkills>;
313
+ /** One skill of the pack, a page of it (`load_skill`), read by the console with the pack token the project holds (ADR-0028). */
314
+ skill(path: string, cursor?: string, signal?: AbortSignal): Promise<RestSkillLoad>;
315
+ /** A file of the pack, a page of it, or what a folder holds (`read_repo_file`). */
316
+ skillFile(
317
+ path: string,
318
+ page?: { readonly offset?: number | undefined; readonly limit?: number | undefined },
319
+ signal?: AbortSignal,
320
+ ): Promise<RestSkillFile>;
321
+ /** Sets the pack token the project reads a private pack with; for an owner. The token travels once and is answered never. */
322
+ setPackToken(input: RestPackTokenInput): Promise<RestProject>;
323
+ /** Takes the pack token away; for an owner. */
324
+ removePackToken(): Promise<RestProject>;
284
325
  /** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
285
326
  rules(signal?: AbortSignal): Promise<RestProjectRules>;
286
327
  /** A folder's pages and folders, or the pages a search finds. */
@@ -302,7 +343,8 @@ export interface ProjectClient {
302
343
  /** Where the bytes of a file attached to a document are read, by the file's id. */
303
344
  documentFileUrl(path: string, fileId: string): string;
304
345
  /** The roadmap (ADR-0019): the goals, the active first, each with its tasks counted by state. */
305
- goals(signal?: AbortSignal): Promise<RestGoals>;
346
+ /** The roadmap, or what the filter keeps of it: the active goals first. */
347
+ goals(filter?: GoalFilter, signal?: AbortSignal): Promise<RestGoals>;
306
348
  goal(id: string, signal?: AbortSignal): Promise<RestGoal>;
307
349
  /** Sets a goal; for an owner, with a session or a token. */
308
350
  createGoal(input: RestGoalInput): Promise<RestGoal>;
@@ -381,7 +423,8 @@ const INVALID = "The server answered unexpectedly.";
381
423
  export function createClient(options: ClientOptions = {}): ConsoleClient {
382
424
  const base = (options.baseUrl ?? "").replace(/\/+$/, "");
383
425
  const send = options.fetch ?? ((input, init) => fetch(input, init));
384
- const { token } = options;
426
+ const { token, userAgent } = options;
427
+ const named: Record<string, string> = userAgent === undefined ? {} : { "user-agent": userAgent };
385
428
 
386
429
  const withQuery = (path: string, params: Record<string, string | undefined>): string => {
387
430
  const query = new URLSearchParams();
@@ -423,6 +466,7 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
423
466
  // A form names its own type, boundary included; everything else is JSON.
424
467
  ...(body === undefined || form ? {} : { "content-type": "application/json" }),
425
468
  ...(token === undefined ? {} : { authorization: `Bearer ${token}` }),
469
+ ...named,
426
470
  },
427
471
  ...(body === undefined ? {} : { body: form ? body : JSON.stringify(body) }),
428
472
  });
@@ -457,7 +501,10 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
457
501
  method: "GET",
458
502
  ...(signal === undefined ? {} : { signal }),
459
503
  credentials: token === undefined ? "same-origin" : "omit",
460
- headers: token === undefined ? {} : { authorization: `Bearer ${token}` },
504
+ headers: {
505
+ ...(token === undefined ? {} : { authorization: `Bearer ${token}` }),
506
+ ...named,
507
+ },
461
508
  });
462
509
  } catch (error) {
463
510
  if (signal?.aborted === true) {
@@ -534,10 +581,12 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
534
581
  "GET",
535
582
  withQuery(at("tasks"), {
536
583
  state: filter.state,
584
+ archived: flag(filter.archived),
537
585
  open: flag(filter.open),
538
586
  category: filter.category,
539
587
  goal: filter.goal,
540
588
  assignee: filter.assignee,
589
+ unassigned: flag(filter.unassigned),
541
590
  dueBy: filter.dueBy,
542
591
  limit: number(filter.limit),
543
592
  offset: number(filter.offset),
@@ -551,6 +600,12 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
551
600
  createTask: (input) => request("POST", `${base}${at("tasks")}`, input, restTaskSchema),
552
601
  updateTask: (id, patch) =>
553
602
  request("PATCH", `${base}${at("tasks", id)}`, patch, restTaskSchema),
603
+ archiveTask: (id) =>
604
+ request("POST", `${base}${at("tasks", id)}/archive`, undefined, restTaskSchema),
605
+ restoreTask: (id) =>
606
+ request("POST", `${base}${at("tasks", id)}/restore`, undefined, restTaskSchema),
607
+ archiveTasksOfGoal: (goalId) =>
608
+ request("POST", `${base}${at("tasks")}/archive`, { goalId }, restArchivedTasksSchema),
554
609
  decisions: (filter = {}, signal) =>
555
610
  request(
556
611
  "GET",
@@ -639,6 +694,30 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
639
694
  eventStreamUrl: (after) => withQuery(`${at("events")}/stream`, { after: String(after) }),
640
695
  skills: (signal) =>
641
696
  request("GET", `${base}${at("skills")}`, undefined, restSkillsSchema, signal),
697
+ skill: (path, cursor, signal) =>
698
+ request(
699
+ "GET",
700
+ withQuery(`${at("skills")}/one`, { path, cursor }),
701
+ undefined,
702
+ restSkillLoadSchema,
703
+ signal,
704
+ ),
705
+ skillFile: (path, page = {}, signal) =>
706
+ request(
707
+ "GET",
708
+ withQuery(`${at("skills")}/file`, {
709
+ path,
710
+ offset: page.offset === undefined ? undefined : String(page.offset),
711
+ limit: page.limit === undefined ? undefined : String(page.limit),
712
+ }),
713
+ undefined,
714
+ restSkillFileSchema,
715
+ signal,
716
+ ),
717
+ setPackToken: (input) =>
718
+ request("PUT", `${base}${at()}/pack-token`, input, restProjectSchema),
719
+ removePackToken: () =>
720
+ request("DELETE", `${base}${at()}/pack-token`, undefined, restProjectSchema),
642
721
  rules: (signal) =>
643
722
  request("GET", `${base}${at("rules")}`, undefined, restProjectRulesSchema, signal),
644
723
  documents: (filter = {}, signal) =>
@@ -681,8 +760,14 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
681
760
  request("POST", `${base}${at("docs", path)}/files`, formOf(input), restDocumentSchema),
682
761
  documentFileUrl: (path, fileId) =>
683
762
  `${base}${at("docs", path)}/files/${encodeURIComponent(fileId)}`,
684
- goals: (signal) =>
685
- request("GET", `${base}${at("goals")}`, undefined, restGoalsSchema, signal),
763
+ goals: (filter = {}, signal) =>
764
+ request(
765
+ "GET",
766
+ withQuery(at("goals"), { state: filter.state, category: filter.category }),
767
+ undefined,
768
+ restGoalsSchema,
769
+ signal,
770
+ ),
686
771
  goal: (id, signal) =>
687
772
  request("GET", `${base}${at("goals", id)}`, undefined, restGoalSchema, signal),
688
773
  createGoal: (input) => request("POST", `${base}${at("goals")}`, input, restGoalSchema),