@skillcdn/console 0.1.21 → 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.
- package/README.md +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +61 -6
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +15 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +133 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts +8 -4
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +63 -13
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +13 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +12 -1
- package/dist/client.js.map +1 -1
- package/dist/rules.js +1 -1
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +145 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +89 -0
- package/dist/schemas.js.map +1 -1
- package/docs/specs/cli.md +3 -3
- package/docs/specs/rest.md +7 -1
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/src/api.ts +8 -0
- package/src/cli/cli.ts +71 -9
- package/src/cli/format.ts +156 -7
- package/src/cli/help.ts +89 -19
- package/src/client.ts +48 -0
- package/src/rules.ts +1 -1
- package/src/schemas.ts +110 -0
package/docs/specs/cli.md
CHANGED
|
@@ -52,8 +52,8 @@ The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-
|
|
|
52
52
|
| `members` | `GET .../members` | one line per member: login, role, since when |
|
|
53
53
|
| `member add <login> [--role owner\|member] [--project <key>[,<key>...]]`, `member role <login> <owner\|member>`, `member remove <login>` | `GET /api/v1/people` for the id, then `POST .../members` (once per project when `--project` names several with commas), `PATCH .../members/<id>`, `DELETE .../members/<id>` | the member as they are now, or that they are no longer listed; with several projects, one line per project, a person listed already said so; for an owner |
|
|
54
54
|
| `category new <key> <name> [--color <name>]`, `category edit <key> [--name <name>] [--color <name>]`, `category remove <key>` | `POST /api/v1/categories`, `PATCH .../categories/<key>`, `DELETE .../categories/<key>` | the category as it is now, or that it is gone; for an administrator |
|
|
55
|
-
| `skills` | `GET .../skills` | the project's pack, or the organization's: each skill with its description, the path `skill` reads it by and the URI an agent loads it by through its own SkillCDN connection; or why there are none to show; where a person connects an agent that has no connection to the address, and where the console's own skills are on this machine, when the command knows |
|
|
56
|
-
| `skill <path
|
|
55
|
+
| `skills`, `skills <folder>`, `skills --search <words> [<folder>] [--limit <n>]` | `GET .../skills`; `GET .../skills/browse?path=&cursor=`, page after page to the end; `GET .../skills/find?query=&path=&limit=`, one page | the project's pack, or the organization's: the pack by name with how many skills and documents it has, and each skill with its description, the path `skill` reads it by and the URI an agent loads it by through its own SkillCDN connection; or why there are none to show; where a person connects an agent that has no connection to the address, and where the console's own skills are on this machine, when the command knows. With a folder (`/` for the root), what it holds as `browse_repo` lays it out: its skills, its folders with what each holds and the README that introduces it, its files. With `--search`, what the words find as `search_repo` does, in relevance order, a skill with the files of it that matched, within the folder when one is named, ten unless `--limit` says otherwise (up to 25), and a last line when more matched |
|
|
56
|
+
| `skill <path>`, `skill --file <path>` | `GET .../skills/one?path=&cursor=`, page after page until `complete`; with `--file`, `GET .../skills/file?path=&offset=` until the end | the skill whole, as `load_skill` lays it out: the rules of the repository from its root to the nearest manifest, the body and the included files, each under a line that names its path, then the files it has and what it refers to; or the file whole, or what a folder holds; a private pack through the pack token the project holds ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)). `--file` takes no skill's path beside it: the two given together are not understood |
|
|
57
57
|
| `rules` | `GET .../rules`, or `GET /api/v1/rules` without a project named, and `GET /api/v1/me` | the rules that hold, as they read for the person (`{language}` is the language they chose, or the task's) ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)): the workspace's, the console's own until an administrator writes the organization's, with its version and who wrote it when written; then this project's own, or that it adds none |
|
|
58
58
|
| `rules write --body <markdown>\|--file <path>\|-\|none [--workspace] [--base <version>]` | `PUT .../docs/rules` or `POST .../docs/rules/archive`; with `--workspace`, `PUT /api/v1/rules` or `POST /api/v1/rules/archive` | the version written, that the console's own rules sent as they are wrote nothing, or that the rules are put away; for an owner of the project, or an administrator for the workspace's |
|
|
59
59
|
| `take <number\|id> [--agent <name>]` | `GET .../tasks/<ref>`, then `POST .../runs`, then `GET .../rules`, `GET /api/v1/me`, `GET .../tasks/<ref>` again and `GET .../tasks/<id>/comments` | the run that began, the task in full as it stands taken with the discussion on it, the rules that hold as they read for the person, and what to do next; a run open on the task is taken over, and that is said when it happened ([ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)) |
|
|
@@ -116,7 +116,7 @@ Plain text on standard output: one line per thing in a list, a few lines for one
|
|
|
116
116
|
|
|
117
117
|
## What an agent is told
|
|
118
118
|
|
|
119
|
-
`console help`
|
|
119
|
+
The command's help has two layers. `console help` is the map, read once at the start: what the board is, that the agent acts as the person whose token it holds and that everything it sends is shown to people as text, which project the commands work in and how it is named; then every command in a line, by section (signing in, the board, working, the documents, configuring), each with what it does in a sentence, a many-formed command (`task`, `goal`, `project`) listing its forms without their flags; then what holds everywhere, the exit codes and the refusals any command may meet. `console help <command>` is the reference, read when the command is about to be used: the command in full, every form and flag, what each does, the limits the console holds it to (how long a report, a summary, a question or an option may be; how many options, reports and links) and the refusals it may meet, by code, so that an agent learns them before it runs into them. The help says what the command does; how the work goes and what the organization asks are the skill's and the rules', and are not repeated in it. The skill `working-the-board` ([skills/working-the-board/SKILL.md](../../skills/working-the-board/SKILL.md), [ADR-0015](../adr/0015-the-consoles-skills-live-in-this-repository-in-the-skillcdn-format-and-the-package-carries-them.md)) says how the work goes: reading, taking, reporting at the milestones, asking and waiting, handing in, writing down, finishing. The rules of the board say what the organization and the project ask of agents ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)): which tasks to take, which skills to load first, what to report, when to ask; `take` prints them with the task, so that they are read once, there, and `console rules` reads them any time, as when a task is still to be chosen.
|
|
120
120
|
|
|
121
121
|
## Running an agent on a task
|
|
122
122
|
|
package/docs/specs/rest.md
CHANGED
|
@@ -245,7 +245,7 @@ Takes `status` (`finished`, `failed` or `abandoned`) and optionally a `summary`
|
|
|
245
245
|
|
|
246
246
|
### `GET /api/v1/projects/<key>/skills`
|
|
247
247
|
|
|
248
|
-
The project's pack, as the SkillCDN deployment serves it at the project's address, or at the organization's (`SKILLS_ADDRESS`) when the project names none, read through `SKILLCDN_URL` ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)): `{ "address", "source", "page", "status", "items" }`. `address` is canonical, or `null` with `status: "none"` when neither names one; `source` is the deployment's origin and `page` where a person browses the address. `status` is `ready`, or why `items` is empty: `indexing` and `failed` as the deployment says of the repository, `not_found` when it does not serve the address to the console (a private pack is read with the project's pack token; without one the console asks as nobody, so a private repository is not read), `token_invalid` when the deployment refuses the pack token the project holds (expired, removed, or its maker lost the repository), `unavailable` when it could not be reached or read. An item is one skill: `name`, `description`, `directory` and `path` (of its `SKILL.md`, from the repository's root, which the routes below take), `page` (the skill at the deployment, for a person), `uri` (what an agent loads it by through its own SkillCDN connection, `skill://gh/<owner>/<repo>/<path>`, or `null`), and `translations` (the title and the description by language tag, as the repository gives them). The console keeps nothing of the skills: a public answer per address stands for a minute, then the deployment is asked again; what is read with a pack token is asked every time, and held for nobody.
|
|
248
|
+
The project's pack, as the SkillCDN deployment serves it at the project's address, or at the organization's (`SKILLS_ADDRESS`) when the project names none, read through `SKILLCDN_URL` ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)): `{ "address", "source", "page", "status", "items" }`. `address` is canonical, or `null` with `status: "none"` when neither names one; `source` is the deployment's origin and `page` where a person browses the address. When `ready`, `pack` names the pack as its manifest does, else its README, else the host (`name`, `description` and `language`, each or `null`), and `counts` says how many `skills` and `documents` the deployment indexed at the address; neither is there otherwise. `status` is `ready`, or why `items` is empty: `indexing` and `failed` as the deployment says of the repository, `not_found` when it does not serve the address to the console (a private pack is read with the project's pack token; without one the console asks as nobody, so a private repository is not read), `token_invalid` when the deployment refuses the pack token the project holds (expired, removed, or its maker lost the repository), `unavailable` when it could not be reached or read. An item is one skill: `name`, `description`, `directory` and `path` (of its `SKILL.md`, from the repository's root, which the routes below take), `page` (the skill at the deployment, for a person), `uri` (what an agent loads it by through its own SkillCDN connection, `skill://gh/<owner>/<repo>/<path>`, or `null`), and `translations` (the title and the description by language tag, as the repository gives them). The console keeps nothing of the skills: a public answer per address stands for a minute, then the deployment is asked again; what is read with a pack token is asked every time, and held for nobody.
|
|
249
249
|
|
|
250
250
|
### `GET /api/v1/projects/<key>/skills/one?path=&cursor=` and `GET .../skills/file?path=&offset=&limit=`
|
|
251
251
|
|
|
@@ -253,6 +253,12 @@ One skill of the pack, and a file of it, read by the console from the deployment
|
|
|
253
253
|
|
|
254
254
|
`skills/one` is the deployment's `load_skill`, in the console's shape: `{ "address", "source", "page", "status", "skill" }`, `skill` being `null` unless `status` is `ready`, else `path`, `directory`, `name`, `description`, `rules` (the manifests that hold for the skill, from the repository's root to the nearest, each `path`, `body` and `truncated`), `body`, `included` (the files the skill includes, the same way), `files` (the paths of the skill's own files), `references` (each `path` and `status`: `available`, `blocked`, `missing` or `outside_mount`), `complete` and `nextCursor` (a page may end inside a rule or a file; the next page continues it, and the skill is whole only when `complete`), `serving` (`full`, whether the skill is served whole or described only under its license; `license`; `sourceUrl`, where it is read at its host) and `warnings`. `skills/file` is `read_repo_file`: `{ "address", "source", "status", "file", "folder" }`, `file` being the page (`path`, `content`, `offset`, `nextOffset` or `null` at the end, `totalLength`, in characters; `limit` 1 to 100,000) or `folder` what a folder holds (`path`, `entries` of `path`, `kind` and `size`, `truncated`), both `null` unless `ready`; `failed` here is a file the deployment will not carry, too large or not text.
|
|
255
255
|
|
|
256
|
+
### `GET /api/v1/projects/<key>/skills/browse?path=&cursor=&limit=` and `GET .../skills/find?query=&path=&cursor=&limit=`
|
|
257
|
+
|
|
258
|
+
What a folder of the pack holds, and what words find in it, read the same way: the deployment's `browse_repo` and `search_repo` in the console's shape, for whoever may see the project, with the pack token where the project holds one ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)); each read is logged with who read which folder, never with the words searched for. `path` is a folder's path from the repository's root, up to 500 characters, the mounted folder when left out; `cursor` continues one listing; a query out of bounds is `request.invalid`.
|
|
259
|
+
|
|
260
|
+
`skills/browse` answers `{ "address", "source", "page", "status", "folder" }`, `folder` being `null` unless `ready`, else the folder's `path`, its `overview` (the README that introduces it: `path`, `title` and `description`, or `null`), its `entries`, each with `kind` (`skill`, `directory` or `file`), `path` (of the `SKILL.md` for a skill), `name` and `description` (or `null`), `skillCount` and `documentCount` (what a folder holds), `size` (a file's, in bytes, or `null`), `overviewPath` (a folder's README, or `null`), `license` and `describedOnly` (a skill's: whether the address only describes it under its license), and `nextCursor` (or `null` at the end; `limit` 1 to 200, 50 unless given). `skills/find` answers `{ "address", "source", "page", "status", "found" }`, `found` being `null` unless `ready`, else the `query` (1 to 500 characters, required), the `path` the search was kept to (or `null`), `items` in relevance order, a `skill` as the listing's item with the `files` of it that matched (each `path`, `title` and `summary`), `moreFiles`, `license` and `describedOnly`, or a `document` with its `path`, `title`, `summary` and `skillDirectory` (or `null`), and `nextCursor` (`limit` 1 to 25, 10 unless given). Words are matched, not meaning, in the language the pack is written in; the deployment's listing of a mount is bounded, and these two are how the rest of a large pack is reached.
|
|
261
|
+
|
|
256
262
|
### `PUT /api/v1/projects/<key>/pack-token` and `DELETE .../pack-token`
|
|
257
263
|
|
|
258
264
|
The pack token ([ADR-0028](../adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md)), for an owner, signed in from the console's own pages or through their agent's token. `PUT` takes `{ "token" }`, a token a person made at the deployment for the pack's repository (8 to 1024 characters, no whitespace), tries it against the deployment once (`project.pack_token_refused` when the pack is not served to it or the project names no pack; `skills.unavailable` when the deployment could not be reached), seals it with the console's secret (`project.pack_token_unsupported` where the console has none) and keeps it, answering the project with `packToken: { setAt }` and writing a `project.updated` event naming the field `packToken`. The token is answered nowhere and logged nowhere. `DELETE` takes it away the same way; taking away what is not there writes nothing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillcdn/console",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "What a custom SkillCDN console is built from: the API client and schemas, the components, the composition of the default console, and the console command agents work the board with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/skillcdn/console#readme",
|
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: Needs a shell with the console command on the PATH (npm install -g @skillcdn/console, Node.js 24 or newer), signed in by a person. Nothing else; the command needs no key of its own and the agent never sees a token.
|
|
6
6
|
metadata:
|
|
7
7
|
author: skillcdn
|
|
8
|
-
version: "1.
|
|
8
|
+
version: "1.16"
|
|
9
9
|
skillcdn:
|
|
10
10
|
translations:
|
|
11
11
|
ko:
|
|
@@ -32,7 +32,7 @@ A task of the board goes in, named by its number; a run comes out: the task take
|
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| The `console` command, signed in | `console whoami` says as whom, at which console, and in which project this directory works; and whether the command and the console go together. `console logins` lists the consoles this machine is signed in to; a directory bound to another console (`.skillcdn-console.json`) needs a login there, and a token never goes to a console it was not made at ([ADR-0027](/docs/adr/0027-the-command-keeps-a-login-per-console-and-a-token-goes-only-to-the-console-that-issued-it.md)). | Stop and tell the person what to do: `npm install -g @skillcdn/console`; `console login --url <the console's address>`, which shows an address and a code the person approves on the console's own pages; `console use <key>` once in the directory, to name the project. Never look for a token. |
|
|
34
34
|
| The command at a version the console takes | A command the console refuses as outdated, or as too new, exits with `4` and prints the command that updates it where it is installed ([ADR-0025](/docs/adr/0025-the-command-names-its-version-on-every-request-and-the-console-holds-it-to-the-range-it-takes.md)). | Run the update command it printed, then go on; where the machine refuses the update, stop and tell the person. A line on stderr that a newer command exists, while the console still answers, is read and left for later. |
|
|
35
|
-
| The command's own words | `console help
|
|
35
|
+
| The command's own words | `console help` lists every command in a line, read once; `console help <command>` says one in full, every form and flag, the limits the console holds it to, and the refusals it may meet, by code, read when the command is about to be used. | They are the reference at run time. This skill says how the work goes, not what each flag means. |
|
|
36
36
|
|
|
37
37
|
The full contract is the [specification of the command line](/docs/specs/cli.md); the command is a thin client of the [REST API](/docs/specs/rest.md), which a script may use directly.
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ The full contract is the [specification of the command line](/docs/specs/cli.md)
|
|
|
41
41
|
| Input | Source |
|
|
42
42
|
|---|---|
|
|
43
43
|
| The task | Its number (`#7`) or its id, from what the person said. Told to pick one, `console work` lists what is on the person's plate across their projects, the sooner due first (`--person <login>` another person's, for an administrator or an owner of a project they are in), and `console tasks --state ready --mine` what is ready and theirs in this one (`--due overdue`, `--due week`, `--category <key>` and `--goal <id>` narrow it further); a task is never taken without being told which, or that any ready one may be. |
|
|
44
|
-
| The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask; the comments on it, which are the discussion so far, printed with the task, their pictures saved with `console file <id>`; with its plan, the category, the goal it works towards (`console goal <id>` says what reaching it means, its category and the days it starts and is meant to be reached by) and the days it starts and is due, which the reports and the summary keep to. The pages it links to (`console doc <path>`) and the project's pack (`console skills` lists its skills; `console skill <path>` reads one whole through the console, its rules first, a private pack through the pack token the project holds, [ADR-0028](/docs/adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md); `console skill
|
|
44
|
+
| The work | The task's body, read with `console task <number>`: what to make, where, the milestones, what to ask; the comments on it, which are the discussion so far, printed with the task, their pictures saved with `console file <id>`; with its plan, the category, the goal it works towards (`console goal <id>` says what reaching it means, its category and the days it starts and is meant to be reached by) and the days it starts and is due, which the reports and the summary keep to. The pages it links to (`console doc <path>`) and the project's pack (`console skills` lists its skills and says how many skills and documents it has; `console skills <folder>` shows what a folder of it holds, `console skills --search <words>` what the words find in it; `console skill <path>` reads one whole through the console, its rules first, a private pack through the pack token the project holds, [ADR-0028](/docs/adr/0028-the-console-reads-a-projects-pack-through-the-deployments-rest-api-with-a-pack-token-for-a-private-one.md); `console skill --file <file>` a file of it; a host that lists skills through a connection of its own loads them by their URI) say the rest. |
|
|
45
45
|
| The rules | The workspace's, the console's own until an administrator writes the organization's, and this project's own when it has them: which skills to load first, where the work lives, the pages, what may be decided alone, how to report ([ADR-0023](/docs/adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)). `console take` prints them with the task, so they are read once, there; `console rules` reads them without taking, as when a task is still to be chosen. They hold over this skill's defaults where they differ. |
|
|
46
46
|
|
|
47
47
|
## Workflow
|
package/src/api.ts
CHANGED
|
@@ -209,7 +209,11 @@ export {
|
|
|
209
209
|
type RestRunInput,
|
|
210
210
|
type RestRuns,
|
|
211
211
|
type RestSkill,
|
|
212
|
+
type RestSkillBrowse,
|
|
213
|
+
type RestSkillBrowseEntry,
|
|
212
214
|
type RestSkillFile,
|
|
215
|
+
type RestSkillFind,
|
|
216
|
+
type RestSkillFindItem,
|
|
213
217
|
type RestSkillLoad,
|
|
214
218
|
type RestSkillPage,
|
|
215
219
|
type RestSkills,
|
|
@@ -296,7 +300,11 @@ export {
|
|
|
296
300
|
restRunInputSchema,
|
|
297
301
|
restRunSchema,
|
|
298
302
|
restRunsSchema,
|
|
303
|
+
restSkillBrowseEntrySchema,
|
|
304
|
+
restSkillBrowseSchema,
|
|
299
305
|
restSkillFileSchema,
|
|
306
|
+
restSkillFindItemSchema,
|
|
307
|
+
restSkillFindSchema,
|
|
300
308
|
restSkillLoadSchema,
|
|
301
309
|
restSkillPageSchema,
|
|
302
310
|
restSkillSchema,
|
package/src/cli/cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type RestDecision,
|
|
18
18
|
type RestMember,
|
|
19
19
|
type RestPerson,
|
|
20
|
+
type RestSkillBrowse,
|
|
20
21
|
type RestSkillFile,
|
|
21
22
|
type RestSkillLoad,
|
|
22
23
|
type RestSkillPage,
|
|
@@ -74,7 +75,9 @@ import {
|
|
|
74
75
|
formatRules,
|
|
75
76
|
formatRun,
|
|
76
77
|
formatRunLine,
|
|
78
|
+
formatSkillBrowse,
|
|
77
79
|
formatSkillFile,
|
|
80
|
+
formatSkillFind,
|
|
78
81
|
formatSkillLoad,
|
|
79
82
|
formatSkills,
|
|
80
83
|
formatTask,
|
|
@@ -995,27 +998,82 @@ const categories: Command = async (args, io) => {
|
|
|
995
998
|
return EXIT.ok;
|
|
996
999
|
};
|
|
997
1000
|
|
|
1001
|
+
/** How many pages of a skill, of a file or of a folder the command follows before it stops: far above what one holds. */
|
|
1002
|
+
const MAX_SKILL_PAGES = 64;
|
|
1003
|
+
/** How many results a search answers at most: what the deployment allows on one page. */
|
|
1004
|
+
const MAX_FIND_LIMIT = 25;
|
|
1005
|
+
|
|
998
1006
|
const skills: Command = async (args, io) => {
|
|
999
|
-
const { values } = parse(args, {
|
|
1007
|
+
const { values, positionals } = parse(args, {
|
|
1008
|
+
search: { type: "string" },
|
|
1009
|
+
limit: { type: "string" },
|
|
1010
|
+
});
|
|
1011
|
+
if (positionals.length > 1) {
|
|
1012
|
+
throw misuse("Say one folder: console skills [<folder>] [--search <words>]");
|
|
1013
|
+
}
|
|
1014
|
+
// The root of the pack is the mounted folder, which the deployment reads as no path at all.
|
|
1015
|
+
const given = positionals[0];
|
|
1016
|
+
const folder = given === undefined || given === "/" || given === "." ? undefined : given;
|
|
1017
|
+
const words = text(values, "search");
|
|
1018
|
+
const limit = text(values, "limit");
|
|
1019
|
+
if (words === undefined && limit !== undefined) {
|
|
1020
|
+
throw misuse("--limit goes with --search: console skills --search <words> [--limit <n>]");
|
|
1021
|
+
}
|
|
1022
|
+
const atMost = limit === undefined ? undefined : Number(limit);
|
|
1023
|
+
if (
|
|
1024
|
+
atMost !== undefined &&
|
|
1025
|
+
(!Number.isInteger(atMost) || atMost < 1 || atMost > MAX_FIND_LIMIT)
|
|
1026
|
+
) {
|
|
1027
|
+
throw misuse(`Not a number of results, 1 to ${MAX_FIND_LIMIT}: ${limit}`);
|
|
1028
|
+
}
|
|
1000
1029
|
const session = await open(io, values);
|
|
1001
1030
|
const project = await session.project();
|
|
1031
|
+
if (words !== undefined) {
|
|
1032
|
+
// One page of what the words find; more is said, and narrowed by the agent, not fetched.
|
|
1033
|
+
const found = await project.skillFind(words, { path: folder, limit: atMost });
|
|
1034
|
+
answer(io, session, found, () => formatSkillFind(found));
|
|
1035
|
+
return EXIT.ok;
|
|
1036
|
+
}
|
|
1037
|
+
if (given !== undefined) {
|
|
1038
|
+
// The folder whole: its pages until the end, as the deployment lists them.
|
|
1039
|
+
const folders: NonNullable<RestSkillBrowse["folder"]>[] = [];
|
|
1040
|
+
let cursor: string | undefined;
|
|
1041
|
+
let last: RestSkillBrowse | undefined;
|
|
1042
|
+
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
1043
|
+
last = await project.skillBrowse({ path: folder, cursor });
|
|
1044
|
+
if (last.status !== "ready" || last.folder === null) {
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
folders.push(last.folder);
|
|
1048
|
+
if (last.folder.nextCursor === null) {
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
cursor = last.folder.nextCursor;
|
|
1052
|
+
}
|
|
1053
|
+
if (last === undefined) {
|
|
1054
|
+
throw failed("The folder could not be read.");
|
|
1055
|
+
}
|
|
1056
|
+
const browsed = last;
|
|
1057
|
+
answer(io, session, { ...browsed, folders }, () => formatSkillBrowse(browsed, folders));
|
|
1058
|
+
return EXIT.ok;
|
|
1059
|
+
}
|
|
1002
1060
|
const found = await project.skills();
|
|
1003
1061
|
answer(io, session, found, () => formatSkills(found, io.skillsDir));
|
|
1004
1062
|
return EXIT.ok;
|
|
1005
1063
|
};
|
|
1006
1064
|
|
|
1007
|
-
/** How many pages of a skill, or of a file, the command follows before it stops: far above what one holds. */
|
|
1008
|
-
const MAX_SKILL_PAGES = 64;
|
|
1009
|
-
|
|
1010
1065
|
const skill: Command = async (args, io) => {
|
|
1011
1066
|
const { values, positionals } = parse(args, { file: { type: "string" } });
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
)
|
|
1067
|
+
const file = text(values, "file");
|
|
1068
|
+
// A file is read by its own path from the repository's root; a skill's path beside it would be
|
|
1069
|
+
// ignored, which reads as if it mattered, so the two together are not understood.
|
|
1070
|
+
if (file !== undefined && positionals.length > 0) {
|
|
1071
|
+
throw misuse(
|
|
1072
|
+
"Say one: console skill <path of its SKILL.md> for a skill, or console skill --file <path> for a file of the pack.",
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1016
1075
|
const session = await open(io, values);
|
|
1017
1076
|
const project = await session.project();
|
|
1018
|
-
const file = text(values, "file");
|
|
1019
1077
|
if (file !== undefined) {
|
|
1020
1078
|
// The file whole, page after page, as the console reads it from the deployment.
|
|
1021
1079
|
const parts: string[] = [];
|
|
@@ -1040,6 +1098,10 @@ const skill: Command = async (args, io) => {
|
|
|
1040
1098
|
answer(io, session, { ...read, content }, () => formatSkillFile(read, content));
|
|
1041
1099
|
return EXIT.ok;
|
|
1042
1100
|
}
|
|
1101
|
+
const path = theOne(
|
|
1102
|
+
positionals,
|
|
1103
|
+
"Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack",
|
|
1104
|
+
);
|
|
1043
1105
|
// The skill whole: its pages until complete, the rules first, as load_skill lays them out.
|
|
1044
1106
|
const pages: RestSkillPage[] = [];
|
|
1045
1107
|
let cursor: string | undefined;
|
package/src/cli/format.ts
CHANGED
|
@@ -15,7 +15,10 @@ import type {
|
|
|
15
15
|
RestProjectRules,
|
|
16
16
|
RestRules,
|
|
17
17
|
RestRun,
|
|
18
|
+
RestSkillBrowse,
|
|
19
|
+
RestSkillBrowseEntry,
|
|
18
20
|
RestSkillFile,
|
|
21
|
+
RestSkillFind,
|
|
19
22
|
RestSkillLoad,
|
|
20
23
|
RestSkillPage,
|
|
21
24
|
RestSkills,
|
|
@@ -254,10 +257,13 @@ const SKILLS_WORDS: Readonly<Record<Exclude<RestSkills["status"], "none" | "read
|
|
|
254
257
|
unavailable: "SkillCDN could not be reached; ask again later.",
|
|
255
258
|
};
|
|
256
259
|
|
|
260
|
+
/** A count with its noun: `1 skill`, `2 skills`. */
|
|
261
|
+
const counted = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
262
|
+
|
|
257
263
|
/**
|
|
258
|
-
* The organization's skills: where they are,
|
|
259
|
-
* person connects an agent that has no connection to the
|
|
260
|
-
* skills are on this machine, when the command knows.
|
|
264
|
+
* The organization's skills: where they are, the pack by name and by count, each skill with
|
|
265
|
+
* what an agent loads it by, where a person connects an agent that has no connection to the
|
|
266
|
+
* address, and where the console's own skills are on this machine, when the command knows.
|
|
261
267
|
*/
|
|
262
268
|
export function formatSkills(skills: RestSkills, ownSkills?: string): string {
|
|
263
269
|
const own =
|
|
@@ -273,16 +279,29 @@ export function formatSkills(skills: RestSkills, ownSkills?: string): string {
|
|
|
273
279
|
if (skills.status !== "ready") {
|
|
274
280
|
return [head, SKILLS_WORDS[skills.status], ...own].join("\n");
|
|
275
281
|
}
|
|
282
|
+
const lines = [head];
|
|
283
|
+
if (skills.pack !== undefined) {
|
|
284
|
+
const named = [skills.pack.name, skills.pack.description]
|
|
285
|
+
.filter((text): text is string => text !== null)
|
|
286
|
+
.join(" ");
|
|
287
|
+
if (named !== "") {
|
|
288
|
+
lines.push(named);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (skills.counts !== undefined) {
|
|
292
|
+
lines.push(
|
|
293
|
+
`${counted(skills.counts.skills, "skill")}, ${counted(skills.counts.documents, "document")}: console skills <folder> shows what a folder holds (/ for the root), console skills --search <words> finds skills and documents by words.`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
276
296
|
if (skills.items.length === 0) {
|
|
277
|
-
|
|
297
|
+
lines.push("No skills at this address.");
|
|
278
298
|
}
|
|
279
|
-
const lines = [head];
|
|
280
299
|
for (const skill of skills.items) {
|
|
281
300
|
lines.push(`${skill.name} ${skill.description}`, ` ${skill.uri ?? skill.path}`);
|
|
282
301
|
}
|
|
283
302
|
lines.push(
|
|
284
303
|
"",
|
|
285
|
-
`Read a skill whole with console skill <path>, its rules first, and a file of the pack with console skill
|
|
304
|
+
`Read a skill whole with console skill <path>, its rules first, and a file of the pack with console skill --file <path>; or load it by its URI through your own SkillCDN connection to this address, which a person makes at ${skills.page}. --json adds the page of each, for a person.`,
|
|
286
305
|
...own,
|
|
287
306
|
);
|
|
288
307
|
return lines.join("\n");
|
|
@@ -576,7 +595,7 @@ export function formatSkillLoad(loaded: RestSkillLoad, pages: readonly RestSkill
|
|
|
576
595
|
if (last.files.length > 0) {
|
|
577
596
|
lines.push(
|
|
578
597
|
"",
|
|
579
|
-
`Files of the skill, read with console skill
|
|
598
|
+
`Files of the skill, read with console skill --file <path>: ${last.files.join(", ")}`,
|
|
580
599
|
);
|
|
581
600
|
}
|
|
582
601
|
if (last.references.length > 0) {
|
|
@@ -621,6 +640,8 @@ export function formatSkillFile(read: RestSkillFile, content: string): string {
|
|
|
621
640
|
(entry) =>
|
|
622
641
|
`${entry.kind === "directory" ? "d" : "f"} ${entry.path}${entry.size === null ? "" : ` ${entry.size}`}`,
|
|
623
642
|
),
|
|
643
|
+
"",
|
|
644
|
+
"console skills <folder> shows a folder with its skills marked, what each folder holds and the README that introduces it.",
|
|
624
645
|
].join("\n");
|
|
625
646
|
}
|
|
626
647
|
if (read.file === null) {
|
|
@@ -631,3 +652,131 @@ export function formatSkillFile(read: RestSkillFile, content: string): string {
|
|
|
631
652
|
content.trimEnd(),
|
|
632
653
|
].join("\n");
|
|
633
654
|
}
|
|
655
|
+
|
|
656
|
+
/** One entry of a browsed folder: what it is, its path, its words, and the facts of its kind. */
|
|
657
|
+
function formatBrowseEntry(entry: RestSkillBrowseEntry): string {
|
|
658
|
+
const facts: string[] = [];
|
|
659
|
+
if (entry.kind === "directory") {
|
|
660
|
+
if (entry.skillCount > 0) {
|
|
661
|
+
facts.push(counted(entry.skillCount, "skill"));
|
|
662
|
+
}
|
|
663
|
+
if (entry.documentCount > 0) {
|
|
664
|
+
facts.push(counted(entry.documentCount, "document"));
|
|
665
|
+
}
|
|
666
|
+
if (entry.overviewPath !== null) {
|
|
667
|
+
facts.push(`README ${entry.overviewPath}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (entry.kind === "skill") {
|
|
671
|
+
if (entry.license !== null) {
|
|
672
|
+
facts.push(entry.license);
|
|
673
|
+
}
|
|
674
|
+
if (entry.describedOnly) {
|
|
675
|
+
facts.push("described only");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (entry.kind === "file" && entry.size !== null) {
|
|
679
|
+
facts.push(`${entry.size} bytes`);
|
|
680
|
+
}
|
|
681
|
+
const words = [entry.name, entry.description]
|
|
682
|
+
.filter((text): text is string => text !== null && text !== "")
|
|
683
|
+
.join(" ");
|
|
684
|
+
const mark = entry.kind === "skill" ? "skill" : entry.kind === "directory" ? "d" : "f";
|
|
685
|
+
return `${mark} ${entry.path}${words === "" ? "" : ` ${words}`}${facts.length === 0 ? "" : ` (${facts.join("; ")})`}`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* What a folder of the pack holds, as browse_repo lays it out: the README that introduces the
|
|
690
|
+
* folder, then its skills, its folders with what each holds, and its files, across the pages the
|
|
691
|
+
* listing came in.
|
|
692
|
+
*/
|
|
693
|
+
export function formatSkillBrowse(
|
|
694
|
+
browsed: RestSkillBrowse,
|
|
695
|
+
folders: readonly NonNullable<RestSkillBrowse["folder"]>[],
|
|
696
|
+
): string {
|
|
697
|
+
if (browsed.status === "none" || browsed.address === null) {
|
|
698
|
+
return NO_ADDRESS;
|
|
699
|
+
}
|
|
700
|
+
const head = `folder at ${browsed.address}, served by ${browsed.source}: ${browsed.status}`;
|
|
701
|
+
if (browsed.status !== "ready") {
|
|
702
|
+
return [head, SKILLS_WORDS[browsed.status]].join("\n");
|
|
703
|
+
}
|
|
704
|
+
const [first] = folders;
|
|
705
|
+
if (first === undefined) {
|
|
706
|
+
return [head, "Nothing was read."].join("\n");
|
|
707
|
+
}
|
|
708
|
+
const last = folders[folders.length - 1] ?? first;
|
|
709
|
+
const lines = [`folder ${first.path === "" ? "/" : first.path} at ${browsed.address}`];
|
|
710
|
+
if (first.overview !== null) {
|
|
711
|
+
const words = [first.overview.title, first.overview.description]
|
|
712
|
+
.filter((text): text is string => text !== null && text !== "")
|
|
713
|
+
.join(" ");
|
|
714
|
+
lines.push(` ${words === "" ? "" : `${words} `}(README: ${first.overview.path})`);
|
|
715
|
+
}
|
|
716
|
+
const entries = folders.flatMap((folder) => folder.entries);
|
|
717
|
+
if (entries.length === 0) {
|
|
718
|
+
lines.push("Nothing in this folder.");
|
|
719
|
+
}
|
|
720
|
+
for (const entry of entries) {
|
|
721
|
+
lines.push(formatBrowseEntry(entry));
|
|
722
|
+
}
|
|
723
|
+
if (last.nextCursor !== null) {
|
|
724
|
+
lines.push("More entries: the listing stopped before the end of the folder.");
|
|
725
|
+
}
|
|
726
|
+
lines.push(
|
|
727
|
+
"",
|
|
728
|
+
"Read a skill with console skill <path>; a file, or a README named above, with console skill --file <path>; a folder with console skills <folder>.",
|
|
729
|
+
);
|
|
730
|
+
return lines.join("\n");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* What words found in the pack, as search_repo lays it out: skills with the files of theirs that
|
|
735
|
+
* matched, documents on their own, in relevance order; words, not meaning.
|
|
736
|
+
*/
|
|
737
|
+
export function formatSkillFind(found: RestSkillFind): string {
|
|
738
|
+
if (found.status === "none" || found.address === null) {
|
|
739
|
+
return NO_ADDRESS;
|
|
740
|
+
}
|
|
741
|
+
const head = `search at ${found.address}, served by ${found.source}: ${found.status}`;
|
|
742
|
+
if (found.status !== "ready") {
|
|
743
|
+
return [head, SKILLS_WORDS[found.status]].join("\n");
|
|
744
|
+
}
|
|
745
|
+
if (found.found === null) {
|
|
746
|
+
return [head, "Nothing was read."].join("\n");
|
|
747
|
+
}
|
|
748
|
+
const { query, path, items, nextCursor } = found.found;
|
|
749
|
+
const where = path === null || path === "" ? "" : ` within ${path}`;
|
|
750
|
+
const lines = [
|
|
751
|
+
`found for "${query}"${where} at ${found.address}: ${counted(items.length, "result")}`,
|
|
752
|
+
];
|
|
753
|
+
for (const item of items) {
|
|
754
|
+
if (item.kind === "skill") {
|
|
755
|
+
lines.push(
|
|
756
|
+
`skill ${item.path} ${item.name} ${item.description}${item.describedOnly ? " (described only)" : ""}`,
|
|
757
|
+
);
|
|
758
|
+
for (const file of item.files) {
|
|
759
|
+
lines.push(` file ${file.path}${file.title === null ? "" : ` ${file.title}`}`);
|
|
760
|
+
}
|
|
761
|
+
if (item.moreFiles > 0) {
|
|
762
|
+
lines.push(` and ${counted(item.moreFiles, "more file")}`);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
lines.push(
|
|
766
|
+
`document ${item.path}${item.title === null ? "" : ` ${item.title}`}${item.summary === null ? "" : ` ${item.summary}`}`,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (nextCursor !== null) {
|
|
771
|
+
lines.push(
|
|
772
|
+
"More matched: narrow the words, or search within a folder (console skills <folder> --search <words>).",
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
if (items.length > 0) {
|
|
776
|
+
lines.push(
|
|
777
|
+
"",
|
|
778
|
+
"Read a skill with console skill <path>, a file or a document with console skill --file <path>.",
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return lines.join("\n");
|
|
782
|
+
}
|