@catalyst-cloud/catalyst-skills 0.2.0 → 0.3.0
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/CHANGELOG.md +8 -0
- package/README.md +39 -29
- package/bin/catalyst-skills.js +42 -2
- package/dist/args.js +14 -8
- package/dist/ask.js +26 -5
- package/dist/cli.js +20 -8
- package/dist/config.js +1 -1
- package/dist/contract.js +2 -2
- package/dist/execution.js +165 -13
- package/dist/http.js +55 -3
- package/dist/prompt.js +1 -1
- package/dist/query.js +42 -4
- package/dist/ready.js +2 -2
- package/package.json +4 -3
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/lib/cli.mjs +2 -2
- package/skills/catalyst-github/scripts/read-pr.mjs +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-linear/references/reading-a-ticket.md +2 -2
- package/skills/catalyst-linear/references/writing-to-linear.md +1 -1
- package/skills/catalyst-linear/scripts/lib/cli.mjs +1 -1
- package/skills/catalyst-setup/SKILL.md +3 -3
- package/skills/catalyst-setup/references/what-each-check-means.md +6 -5
- package/skills/catalyst-setup/scripts/lib/cli.mjs +2 -2
- package/skills/catalyst-setup/scripts/replica-status.mjs +1 -1
- package/skills/connect-me/SKILL.md +9 -9
- package/skills/connect-me/agents/openai.yaml +1 -1
- package/skills/connect-me/scripts/lib/cli.mjs +2 -2
- package/skills/how-catalyst-works/SKILL.md +2 -2
- package/skills/how-catalyst-works/references/coding-accounts.md +2 -2
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +2 -2
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +1 -1
- package/skills/how-catalyst-works/scripts/whats-running.mjs +1 -2
- package/skills/run-this-project/SKILL.md +2 -2
- package/skills/run-this-project/references/reacting-to-events.md +1 -1
- package/skills/run-this-project/references/stalls-and-escalation.md +1 -1
- package/skills/run-this-project/scripts/lib/cli.mjs +2 -2
- package/skills/what-needs-me/SKILL.md +2 -2
- package/skills/what-needs-me/references/reading-the-inbox.md +1 -1
- package/skills/what-needs-me/scripts/inbox.mjs +15 -6
- package/skills/what-needs-me/scripts/lib/cli.mjs +2 -2
- package/skills/whats-happening/SKILL.md +3 -3
- package/skills/whats-happening/assets/status-reply.json +1 -1
- package/skills/whats-happening/references/reading-the-board.md +1 -1
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/references/why-is-it-stuck.md +1 -1
- package/skills/whats-happening/scripts/lib/cli.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
You connect with your own personal key now, not the tenant's shared account key: mint it yourself at Settings → API keys in the Catalyst Cloud app (every member can; no admin needed), and `login` prints who you connected as beside which tenant. Your agent acts as you — an ask it raises names you, and "what needs me" means you: `ask list` (and the what-needs-me skill) keeps only the asks assigned to your Linear user by default, `--anyone` lists the whole tenant, and when your Linear identity is not matched yet it says so and shows everything rather than an empty list. `customer.json` gains a `user` block (id, label, role, your Linear user id); an older config is still read unchanged. Connecting with the account key still works — it is the right credential for a host or daemon — but `login` tells you it names no person. Every skill, reference and help line that said "account key", or said execution history and coding-account status were not readable, has been rewritten: `explain --history` and `accounts` read them, and the two things your key genuinely cannot do (release a park, read PR labels or reactions) are named instead. The catalyst-setup reference now lists all eleven team checks, including `environment_declared`. Needs a cloud that admits personal keys on the agent routes; on an older cloud the contract read says so by name, and the account key still works until it is updated.
|
|
6
|
+
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
Fixes five defects found by running 0.2.0's verbs against a live tenant. Three skills (catalyst-github, catalyst-setup, how-catalyst-works) carried a colon-and-space inside an unquoted description, which YAML reads as a nested map rather than text, so `npx skills add` skipped them and still reported success — every Codex, Cursor and OpenCode install of 0.2.0 quietly got five of the eight skills, and reinstalling with this version gives you all eight. `running` no longer asks for lease attributions it has no coordinates for, so the headline "what's happening?" verb works instead of failing for every tenant; the lease read is still available as `running --ticket T --phase P`. The CLI no longer truncates its own output at 64KB when a skill reads it through a pipe, which made `query issues --json` on a tenant past roughly eighty tickets return invalid JSON with a success code. `accounts` now actually reads your coding-account slots and renders them, `history <ticket>` (also `explain --history`) shows a ticket's execution history, and both say plainly when your cloud is older than this bundle rather than printing an empty success. `queue` with no `--team` reads every team on your tenant instead of being refused, as its help always promised. `query changes` works at all now: it reads the feed's NDJSON, where before it called any successful response a non-JSON body — nothing noticed because the documented `--since 0` is refused on any tenant whose feed has rotated, so a success was never reached. That refusal now names a cursor you can actually use, and `--since head` starts from now.
|
|
10
|
+
|
|
3
11
|
## 0.2.0
|
|
4
12
|
|
|
5
13
|
Eight skills named from the executive's seat (whats-happening, what-needs-me, run-this-project, catalyst-setup, catalyst-linear, catalyst-github, how-catalyst-works, connect-me) replace the six; reads and the live event stream go through the Catalyst Cloud SDK and writes through the tenant's agent proxy, all behind catalyst-skills verbs (me, contract, query, replica, explain, running, queue, watch, write, ask, ready, accounts), with every stage id, label id, threshold and route read from the tenant contract; Node 22 is now required; installing the skills has moved to your agent's own command (the Claude Code plugin marketplace in this repository, or npx skills add for Codex, Cursor, OpenCode and the rest) and the CLI no longer copies them, leaving install only as a repair path; catalyst-skills login replaces join, which stays as a deprecated alias for one minor version, and login now prompts for the key without echoing it when no CATALYST_CLOUD_TOKEN is set; every SKILL.md declares allowed-tools scoped to this package's binary, so no skill needs a blanket shell grant; your existing customer.json is still read unchanged and gains the CLI path the next time you run catalyst-skills login, which also caches the tenant contract; run catalyst-skills ready to confirm.
|
package/README.md
CHANGED
|
@@ -4,9 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Start by pasting one sentence into the coding agent you already use:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```
|
|
10
|
+
Help me understand and set up Catalyst Cloud. Read https://staging.catalystcloud.dev/agent-guide.md first, then walk me through it step by step.
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Your agent reads the guide, explains Catalyst Cloud in terms of your own repositories and tickets, finds out where it is running and what is already installed, and does the setup below itself — asking once before it writes to your machine. Everything else on this page is the reference it follows.
|
|
14
|
+
|
|
15
|
+
Or by hand. One command, for every coding agent on the machine:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx skills@latest add coalesce-labs/catalyst-cloud-skills --all
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It installs all eight skills for each agent it detects (Claude Code, Codex, Cursor, OpenCode and the rest). Add `-g` to install into your home directory instead of the project. Skills installed this way do not auto-update; run `npx skills update -y` to refresh them.
|
|
22
|
+
|
|
23
|
+
<details><summary><strong>Alternative for Claude Code: the plugin marketplace</strong></summary>
|
|
24
|
+
|
|
25
|
+
The plugin installs the set as a managed bundle that updates when we ship. It needs a GitHub SSH key, and it does not load into the session you are already in — run `/reload-plugins` or restart afterwards. Pick one rail; installing both leaves you with every skill twice.
|
|
10
26
|
|
|
11
27
|
```
|
|
12
28
|
/plugin marketplace add coalesce-labs/catalyst-cloud-skills
|
|
@@ -14,65 +30,59 @@ Two ways in. The plugin installs the set as a managed bundle that updates when w
|
|
|
14
30
|
```
|
|
15
31
|
</details>
|
|
16
32
|
|
|
17
|
-
<details><summary><strong>
|
|
33
|
+
<details><summary><strong>One agent at a time</strong></summary>
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
Codex:
|
|
20
36
|
|
|
21
37
|
```sh
|
|
22
38
|
npx skills@latest add coalesce-labs/catalyst-cloud-skills -a codex
|
|
23
39
|
```
|
|
24
|
-
</details>
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
Run this command in your project:
|
|
41
|
+
Cursor:
|
|
29
42
|
|
|
30
43
|
```sh
|
|
31
44
|
npx skills@latest add coalesce-labs/catalyst-cloud-skills -a cursor
|
|
32
45
|
```
|
|
33
|
-
</details>
|
|
34
|
-
|
|
35
|
-
<details><summary><strong>OpenCode, Amp, Windsurf and the rest</strong></summary>
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
OpenCode, Amp, Windsurf and the rest:
|
|
38
48
|
|
|
39
49
|
```sh
|
|
40
50
|
npx skills@latest add coalesce-labs/catalyst-cloud-skills
|
|
41
51
|
```
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
Without `--all` the installer asks which skills to take and which agents to install them on.
|
|
44
54
|
</details>
|
|
45
55
|
|
|
46
56
|
### Then connect to your tenant
|
|
47
57
|
|
|
48
|
-
The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine with your
|
|
58
|
+
The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine with your own **personal key** — mint it at Settings → API keys in the Catalyst Cloud app (every member can; no admin needed):
|
|
49
59
|
|
|
50
60
|
```sh
|
|
51
61
|
npm install -g @catalyst-cloud/catalyst-skills
|
|
52
|
-
CATALYST_CLOUD_TOKEN=<your-
|
|
62
|
+
CATALYST_CLOUD_TOKEN=<your-personal-key> catalyst-skills login
|
|
53
63
|
catalyst-skills ready
|
|
54
64
|
```
|
|
55
65
|
|
|
56
|
-
Passing the key as an environment variable is the recommended form, because a key typed into a command line lands in your shell history. With no key in the environment and a terminal attached, `catalyst-skills login` prompts for it without echoing it; `--key <your-
|
|
66
|
+
Passing the key as an environment variable is the recommended form, because a key typed into a command line lands in your shell history. With no key in the environment and a terminal attached, `catalyst-skills login` prompts for it without echoing it; `--key <your-personal-key>` is the third form, for a script. `npx @catalyst-cloud/catalyst-skills login` works without the global install, and `bunx` works in place of `npx`. A non-default cloud is set with `CATALYST_CLOUD_BASE_URL` or `--base-url <url>`.
|
|
57
67
|
|
|
58
68
|
That is the whole setup. Everything below explains what you just installed.
|
|
59
69
|
|
|
60
70
|
## What this is
|
|
61
71
|
|
|
62
|
-
Eight skills that let your coding agent run your own Catalyst Cloud tenant (https://catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your tenant through the Catalyst Cloud SDK, write to it through the tenant's agent proxy, and never compose a URL or run a tool of their own; every read, write and subscription is a `catalyst-skills` verb with `--help`. Every skill is plain Markdown under `skills/<name>/SKILL.md` in this repository, and
|
|
72
|
+
Eight skills that let your coding agent run your own Catalyst Cloud tenant (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your tenant through the Catalyst Cloud SDK, write to it through the tenant's agent proxy, and never compose a URL or run a tool of their own; every read, write and subscription is a `catalyst-skills` verb with `--help`. Every skill is plain Markdown under `skills/<name>/SKILL.md` in this repository, and your own personal key is the only credential. Your agent acts as you: the asks it raises and the comments it posts carry your name, and "what needs me" means you.
|
|
63
73
|
|
|
64
74
|
## What connecting does
|
|
65
75
|
|
|
66
|
-
`login` does three things: it calls `GET /api/v1/me` with your key, which discovers your tenant from the key alone; it writes `~/.config/catalyst-cloud/customer.json` with file mode `0600`, holding your key and the absolute path of the CLI so every skill script can spawn the same binary; and it fetches the tenant contract (`GET /api/v1/agent/contract`) and caches it at `~/.config/catalyst-cloud/contract.json` with its ETag, so stage names and ids, label ids, the ask template, thresholds and the route table come from your tenant, live, and no skill restates them.
|
|
76
|
+
`login` does three things: it calls `GET /api/v1/me` with your key, which discovers your tenant and who you are from the key alone; it writes `~/.config/catalyst-cloud/customer.json` with file mode `0600`, holding your key, who you are (id, label, role, your Linear user id) and the absolute path of the CLI so every skill script can spawn the same binary; and it fetches the tenant contract (`GET /api/v1/agent/contract`) and caches it at `~/.config/catalyst-cloud/contract.json` with its ETag, so stage names and ids, label ids, the ask template, thresholds and the route table come from your tenant, live, and no skill restates them.
|
|
67
77
|
|
|
68
|
-
`login` does not install skills; the install command above did that. Re-running `login` after a key rotation rewrites the config. `catalyst-skills status` prints which tenant this machine is connected to, `catalyst-skills ready` prints one READY or NOT READY verdict with the fix for each failure and who can apply it, and `catalyst-skills --version` prints the package version and its pinned tenant contract range.
|
|
78
|
+
`login` does not install skills; the install command above did that. Re-running `login` after a key rotation rewrites the config. `catalyst-skills status` prints which tenant this machine is connected to and as whom, `catalyst-skills ready` prints one READY or NOT READY verdict with the fix for each failure and who can apply it, and `catalyst-skills --version` prints the package version and its pinned tenant contract range.
|
|
69
79
|
|
|
70
|
-
The setup skill Catalyst seeds into your repository ends by pointing at this same command. That served skill lives in the Catalyst Cloud application, not here; this is the only connect step a customer runs,
|
|
80
|
+
The setup skill Catalyst seeds into your repository ends by pointing at this same command. That served skill lives in the Catalyst Cloud application, not here; this is the only connect step a customer runs. The tenant's **account key** (Settings → Account keys, admin-minted) is a different credential — for a host or daemon that runs unattended — and is not what a person connects their own agent with: it would strip your name from everything your agent writes, and `login` says so if you use one.
|
|
71
81
|
|
|
72
82
|
## Requirements
|
|
73
83
|
|
|
74
84
|
- Node 22 or newer. The bundle uses Node's built-in SQLite module for the optional local replica, so there is no native dependency to build; if `better-sqlite3` resolves on the machine it is used instead. `catalyst-skills ready` names the exact reason when an older Node is found.
|
|
75
|
-
-
|
|
85
|
+
- Your personal key, minted by you at Settings → API keys. The key is the only tenant selector: you never type a tenant or account id. If your Linear identity is not matched yet, `login` says so; an admin matches it in Settings → Members, and until then "what needs me" shows everyone's asks.
|
|
76
86
|
- An agent that discovers skills. Claude Code loads the plugin; Codex, Cursor, OpenCode and the rest read the `skills/<name>/SKILL.md` files the `npx skills` installer writes.
|
|
77
87
|
- Bun is optional, only if you prefer `bunx` over `npx`.
|
|
78
88
|
|
|
@@ -110,16 +120,16 @@ Nothing rotates. The replica is upserts and deletes into one file, the cursor is
|
|
|
110
120
|
| `catalyst-linear` | "Show me the ticket, the history, what Catalyst wrote on it." | Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the replica when fresh and the API otherwise, always naming the source; writes comments, card moves, labels and new tickets as the app actor; knows what a ticket accumulates as Catalyst works it. | [`SKILL.md`](skills/catalyst-linear/SKILL.md) |
|
|
111
121
|
| `catalyst-github` | "Show me the PR, the checks, the review, the queue." | A ticket's pull request with its checks, reviews and review threads; whether it is mergeable under the repository's policy; what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). | [`SKILL.md`](skills/catalyst-github/SKILL.md) |
|
|
112
122
|
| `how-catalyst-works` | "How does this work? Why did it do that? How does it prioritise?" | The execution model as references loaded on demand: the eight-phase ladder, the eleven board slots and this team's live stage map, what happens when a phase fails, how the queue is ordered and routed, every exclusion reason, and the coding-account model. Scripts explain one ticket's eligibility in plain English. | [`SKILL.md`](skills/how-catalyst-works/SKILL.md) |
|
|
113
|
-
| `connect-me` | "Connect this machine to my tenant." | Connects the machine to the tenant with
|
|
123
|
+
| `connect-me` | "Connect this machine to my tenant." | Connects the machine to the tenant with your own personal key, caches the tenant contract, verifies, and offers to start the replica. | [`SKILL.md`](skills/connect-me/SKILL.md) |
|
|
114
124
|
|
|
115
125
|
Four skills only read (`whats-happening`, `catalyst-setup`, `catalyst-github`, `how-catalyst-works`). The four that write anything (`catalyst-linear`, `what-needs-me`, `run-this-project`, `connect-me`) are marked so an agent cannot invoke them on its own; the person asks for them. Every skill declares `allowed-tools` scoped to this package's own binary, so none of them needs a blanket shell grant.
|
|
116
126
|
|
|
117
127
|
## What a key cannot see yet
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst-skills explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst-skills accounts`: provider, declared and observed state, window usage, walls, quarantine — never a credential; enrolling or pausing one is `<your cloud>/settings/coding-accounts`). Two things it cannot do, and the skills say so by name rather than guess:
|
|
120
130
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
131
|
+
- Release a park. When a ticket is parked after repeated failures, an operator releases it; the skill names the park and points at `<your cloud>/settings`.
|
|
132
|
+
- Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
|
|
123
133
|
|
|
124
134
|
## Versions and origins
|
|
125
135
|
|
|
@@ -127,11 +137,11 @@ The package pins the tenant contract range `1.x`, recorded in `package.json` und
|
|
|
127
137
|
|
|
128
138
|
## What it writes on your machine
|
|
129
139
|
|
|
130
|
-
- `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your
|
|
140
|
+
- `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your personal key, who you are, and the CLI path.
|
|
131
141
|
- `~/.config/catalyst-cloud/contract.json`, the cached tenant contract.
|
|
132
142
|
- Only if you start them: `~/.config/catalyst-cloud/replica.db` with its `.pid` and `.writer.lock` sidecars, and `~/.config/catalyst-cloud/watch-cursor.json`.
|
|
133
143
|
|
|
134
|
-
The skill files themselves are written by whichever install command you ran, in that tool's own location. Your
|
|
144
|
+
The skill files themselves are written by whichever install command you ran, in that tool's own location. Your personal key goes into that one config file and nowhere else.
|
|
135
145
|
|
|
136
146
|
## Updating
|
|
137
147
|
|
|
@@ -156,11 +166,11 @@ npm uninstall -g @catalyst-cloud/catalyst-skills
|
|
|
156
166
|
|
|
157
167
|
## If login fails
|
|
158
168
|
|
|
159
|
-
- `catalyst-skills: GET /me failed (401): credential not accepted —
|
|
169
|
+
- `catalyst-skills: GET /me failed (401): credential not accepted — mint a personal key at Settings → API keys and log in again` — the key is stale, mistyped or revoked. Mint a new one, then run `login` again.
|
|
160
170
|
- `catalyst-skills: GET /me failed (403): account-not-operational` — the tenant is suspended. This is an admin conversation on the tenant, not a local fix.
|
|
161
171
|
- `catalyst-skills: could not reach <url>: <detail>` — the machine cannot reach the cloud. The URL is named in the message; check `CATALYST_CLOUD_BASE_URL` or `--base-url`.
|
|
162
172
|
- A line naming two contract versions after `Connected to` — the tenant serves a contract outside this bundle's `1.x` range. The config is written; update the bundle before using the other skills.
|
|
163
|
-
- `[catalyst-skills] GET /api/v1/agent/contract refused (403):
|
|
173
|
+
- `[catalyst-skills] GET /api/v1/agent/contract refused (403): this cloud is older than the bundle …` on stderr — the cloud has not yet deployed personal-key access to the contract. Update the cloud, or connect with the tenant's account key until it has.
|
|
164
174
|
|
|
165
175
|
## License
|
|
166
176
|
|
package/bin/catalyst-skills.js
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Thin ESM launcher for @catalyst-cloud/catalyst-skills (CTC-1926). tsc does not carry a shebang
|
|
3
3
|
// through emit, so the bin is this two-line wrapper and the program lives in dist/cli.js.
|
|
4
|
+
//
|
|
5
|
+
// ⛔ NEVER `process.exit(code)` STRAIGHT AFTER A WRITE. When stdout is a PIPE — which is how every
|
|
6
|
+
// skill script spawns this CLI — writes are asynchronous, so `process.exit` severs whatever is
|
|
7
|
+
// still in flight AND STILL REPORTS THE ORIGINAL EXIT CODE. The caller then reads a truncated body
|
|
8
|
+
// with a success code and has no way to tell it from a complete one: `query issues --json` on a
|
|
9
|
+
// tenant with ~80+ tickets came back as 65,528 bytes of invalid JSON, exit 0 (0.2.0). Interactive
|
|
10
|
+
// use hid it, because a tty gives stdout a synchronous write path.
|
|
11
|
+
//
|
|
12
|
+
// So: park the code on `process.exitCode`, wait for both streams to drain, and only then exit. The
|
|
13
|
+
// explicit exit stays — `fetch`'s keep-alive sockets can hold the event loop open for seconds after
|
|
14
|
+
// the work is done, and a CLI that lingers is its own defect — but by then nothing is buffered.
|
|
4
15
|
import("../dist/cli.js")
|
|
5
16
|
.then((m) => m.main(process.argv.slice(2)))
|
|
6
|
-
.then((code) =>
|
|
17
|
+
.then((code) => finish(code))
|
|
7
18
|
.catch((err) => {
|
|
8
19
|
console.error(`catalyst-skills: failed to load: ${err instanceof Error ? err.message : String(err)}`);
|
|
9
|
-
|
|
20
|
+
finish(1);
|
|
10
21
|
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve once every byte already handed to `stream` has reached the other end. The empty write is
|
|
25
|
+
* ordered behind the real ones, so its callback is the drain signal; a destroyed or errored stream
|
|
26
|
+
* (a reader that hung up) resolves immediately rather than hanging the process forever.
|
|
27
|
+
*/
|
|
28
|
+
function drained(stream) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
if (!stream || stream.destroyed || stream.writableEnded) return resolve();
|
|
31
|
+
let settled = false;
|
|
32
|
+
const done = () => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
resolve();
|
|
36
|
+
};
|
|
37
|
+
stream.once("error", done);
|
|
38
|
+
try {
|
|
39
|
+
stream.write("", done);
|
|
40
|
+
} catch {
|
|
41
|
+
done();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function finish(code) {
|
|
47
|
+
process.exitCode = code;
|
|
48
|
+
await Promise.all([drained(process.stdout), drained(process.stderr)]);
|
|
49
|
+
process.exit(code);
|
|
50
|
+
}
|
package/dist/args.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// error naming the verb, and every verb's --help is generated from its own table.
|
|
4
4
|
import { UsageError } from "./errors.js";
|
|
5
5
|
const GLOBAL_FLAGS = {
|
|
6
|
-
key: { value: true, help: "
|
|
6
|
+
key: { value: true, help: "your personal key (or CATALYST_CLOUD_TOKEN)" },
|
|
7
7
|
"base-url": { value: true, help: "Catalyst Cloud origin (or CATALYST_CLOUD_BASE_URL)" },
|
|
8
8
|
"skills-dir": { value: true, help: "where the skills are copied (default ~/.claude/skills)" },
|
|
9
9
|
force: { value: false, help: "replace a skill directory this package did not install" },
|
|
@@ -38,11 +38,15 @@ export const FLAG_TABLES = {
|
|
|
38
38
|
"stale-ms": { value: true, help: "status: heartbeat age that counts as stale (default 15000)" },
|
|
39
39
|
},
|
|
40
40
|
explain: {
|
|
41
|
-
history: { value: false, help: "per-ticket execution history
|
|
41
|
+
history: { value: false, help: "per-ticket execution history instead of the eligibility reason" },
|
|
42
|
+
},
|
|
43
|
+
history: {},
|
|
44
|
+
running: {
|
|
45
|
+
ticket: { value: true, help: "with --phase: also the lease attributions for this (ticket, phase)" },
|
|
46
|
+
phase: { value: true, help: "with --ticket: the phase whose lease attributions to read" },
|
|
42
47
|
},
|
|
43
|
-
running: {},
|
|
44
48
|
queue: {
|
|
45
|
-
team: { value: true, help: "team key" },
|
|
49
|
+
team: { value: true, help: "team key (default: every team the tenant contract names)" },
|
|
46
50
|
},
|
|
47
51
|
watch: {
|
|
48
52
|
team: { value: true, help: "scope: a team key" },
|
|
@@ -81,6 +85,7 @@ export const FLAG_TABLES = {
|
|
|
81
85
|
default: { value: true, help: "raise: the default if silent" },
|
|
82
86
|
blocks: { value: true, repeat: true, help: "raise: a ticket this decision blocks (repeatable)" },
|
|
83
87
|
"nothing-to-block": { value: false, help: "raise: declare that nothing is blocked" },
|
|
88
|
+
anyone: { value: false, help: "list: every open ask in the tenant, not only the ones assigned to you" },
|
|
84
89
|
"ask-key": { value: true, help: "raise: idempotency key" },
|
|
85
90
|
answer: { value: true, help: "accept: the answering comment id" },
|
|
86
91
|
role: { value: true, help: "accept: the role recording the answer" },
|
|
@@ -95,16 +100,17 @@ export const VERB_USAGE = {
|
|
|
95
100
|
notice: "notice",
|
|
96
101
|
me: "me [--json]",
|
|
97
102
|
contract: "contract [--refresh] [--path <a.b.c>] [--json]",
|
|
98
|
-
query: "query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <
|
|
103
|
+
query: "query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>> [--team K] [--project P] [--state S] [--limit N] [--source replica|api] [--json]",
|
|
99
104
|
replica: "replica <start [--detach]|stop|status [--probe] [--json]|sql \"<select>\"|schema [table]> [--db <path>]",
|
|
100
105
|
explain: "explain <ticket> [--history] [--json]",
|
|
101
|
-
|
|
106
|
+
history: "history <ticket> [--json]",
|
|
107
|
+
running: "running [--ticket T --phase P] [--json]",
|
|
102
108
|
queue: "queue [--team K] [--json]",
|
|
103
109
|
watch: "watch [--team K] [--ticket T]... [--project P] [--exec CMD] [--cursor-file <path>] [--from cursor|head]",
|
|
104
110
|
write: "write <comment <ticket> --body|--stdin [--parent] [--bookkeeping] [--as-user] | state <ticket> --slot|--state-id|--state-type | label <ticket> --add... --remove... | create --team --title [--label] [--priority] | reaction <ticket>|--comment <id> --emoji <e> | attachment <ticket> --title --url | session <ticket> [--title] [--plan-file] [--activity]>",
|
|
105
|
-
ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--json]>",
|
|
111
|
+
ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--anyone] [--json]>",
|
|
106
112
|
ready: "ready [--json]",
|
|
107
|
-
accounts: "accounts",
|
|
113
|
+
accounts: "accounts [--json]",
|
|
108
114
|
};
|
|
109
115
|
export function parseArgs(argv) {
|
|
110
116
|
const out = {
|
package/dist/ask.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function cmdAsk(args, ctx) {
|
|
|
24
24
|
case "accept":
|
|
25
25
|
return accept(args, ctx, doc, api, rest);
|
|
26
26
|
case "list":
|
|
27
|
-
return list(args, ctx, doc, api);
|
|
27
|
+
return list(args, ctx, doc, api, cfg);
|
|
28
28
|
default:
|
|
29
29
|
throw new UsageError(`unknown ask subcommand: ${sub}`);
|
|
30
30
|
}
|
|
@@ -74,6 +74,18 @@ async function accept(args, ctx, doc, api, rest) {
|
|
|
74
74
|
ctx.stdout(args.json ? JSON.stringify(result) : `ask ${ask.identifier}: answer ${answer} recorded by ${role}`);
|
|
75
75
|
return 0;
|
|
76
76
|
}
|
|
77
|
+
/** Decide the inbox scope from the connected identity and the `--anyone` flag. A personal key with a
|
|
78
|
+
* matched Linear identity reads "mine" by default; `--anyone` widens; an unmatched identity or a
|
|
79
|
+
* host key cannot mean "me" and says so rather than filtering to nothing. */
|
|
80
|
+
export function inboxScope(cfg, anyone) {
|
|
81
|
+
if (anyone)
|
|
82
|
+
return { kind: "anyone" };
|
|
83
|
+
if (!cfg.user)
|
|
84
|
+
return { kind: "no-person" };
|
|
85
|
+
if (!cfg.user.linearUserId)
|
|
86
|
+
return { kind: "unmatched", label: cfg.user.label };
|
|
87
|
+
return { kind: "mine", linearUserId: cfg.user.linearUserId, label: cfg.user.label };
|
|
88
|
+
}
|
|
77
89
|
/** Rank open asks by the priority-weighted count of open tickets each one blocks. Priority 1 (urgent)
|
|
78
90
|
* weighs 4, 4 (low) weighs 1, 0 (none) weighs 1. */
|
|
79
91
|
export function rankAsks(issues, doc, openState) {
|
|
@@ -113,22 +125,31 @@ export function rankAsks(issues, doc, openState) {
|
|
|
113
125
|
state: String(issue.state ?? ""),
|
|
114
126
|
blocks: blocked,
|
|
115
127
|
score: blocked.reduce((sum, id) => sum + weight(byIdentifier.get(id)), 0),
|
|
128
|
+
assigneeId: typeof issue.assignee_id === "string" && issue.assignee_id !== "" ? issue.assignee_id : null,
|
|
116
129
|
});
|
|
117
130
|
}
|
|
118
131
|
return out.sort((a, b) => b.score - a.score || a.identifier.localeCompare(b.identifier));
|
|
119
132
|
}
|
|
120
|
-
async function list(args, ctx, doc, api) {
|
|
133
|
+
async function list(args, ctx, doc, api, cfg) {
|
|
121
134
|
const [issuesRes, states] = await Promise.all([api.getJson("/api/v1/issues", { query: { limit: 500 } }), fetchWorkflowStates(api)]);
|
|
122
135
|
const issues = rowsOf(issuesRes.body);
|
|
123
136
|
const terminal = new Set(states.filter((s) => ["completed", "canceled", "cancelled"].includes(s.type.toLowerCase())).map((s) => s.name.toLowerCase()));
|
|
124
137
|
const openState = (issue) => !terminal.has(String(issue.state ?? "").toLowerCase());
|
|
125
|
-
const
|
|
138
|
+
const all = rankAsks(issues, doc, openState);
|
|
139
|
+
const scope = inboxScope(cfg, flagBool(args, "anyone"));
|
|
140
|
+
const ranked = scope.kind === "mine" ? all.filter((a) => a.assigneeId === scope.linearUserId) : all;
|
|
126
141
|
if (args.json) {
|
|
127
|
-
ctx.stdout(JSON.stringify(ranked));
|
|
142
|
+
ctx.stdout(JSON.stringify({ scope, asks: ranked }));
|
|
128
143
|
return 0;
|
|
129
144
|
}
|
|
145
|
+
if (scope.kind === "unmatched") {
|
|
146
|
+
ctx.stderr(`[catalyst-skills] your Linear identity is not matched yet (an admin matches it in Settings → Members), so this is every open ask, not only yours`);
|
|
147
|
+
}
|
|
148
|
+
else if (scope.kind === "no-person") {
|
|
149
|
+
ctx.stderr(`[catalyst-skills] connected with the tenant's account key, which names no person — this is every open ask; log in with your personal key to see only yours`);
|
|
150
|
+
}
|
|
130
151
|
if (ranked.length === 0) {
|
|
131
|
-
ctx.stdout("no open asks");
|
|
152
|
+
ctx.stdout(scope.kind === "mine" ? `no open asks assigned to ${scope.label} (${all.length} open in the tenant — add --anyone to see them)` : "no open asks");
|
|
132
153
|
return 0;
|
|
133
154
|
}
|
|
134
155
|
for (const a of ranked)
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CONFIG_MODE, DEFAULT_BASE_URL, PACKAGE_NAME, cliPath, configPathFor, co
|
|
|
6
6
|
import { loadContract, pickPath } from "./contract.js";
|
|
7
7
|
import { CliError, MeError, UsageError } from "./errors.js";
|
|
8
8
|
import { fetchMe } from "./http.js";
|
|
9
|
-
import { cmdAccounts, cmdExplain, cmdQueue, cmdRunning } from "./execution.js";
|
|
9
|
+
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning } from "./execution.js";
|
|
10
10
|
import { cmdQuery } from "./query.js";
|
|
11
11
|
import { cmdReady } from "./ready.js";
|
|
12
12
|
import { cmdReplica } from "./replica.js";
|
|
@@ -37,14 +37,14 @@ export function usageText() {
|
|
|
37
37
|
`${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your tenant, read through the SDK, write through the agent proxy`,
|
|
38
38
|
"",
|
|
39
39
|
"Usage:",
|
|
40
|
-
" catalyst-skills login [--key <
|
|
40
|
+
" catalyst-skills login [--key <personal-key>] [--base-url <url>] [--start-replica]",
|
|
41
41
|
" catalyst-skills join ... (deprecated alias of login; removed in the next minor version)",
|
|
42
42
|
" catalyst-skills install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
|
|
43
43
|
" catalyst-skills status | notice | me | ready | accounts",
|
|
44
44
|
" catalyst-skills contract [--refresh] [--path <a.b.c>]",
|
|
45
|
-
" catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <
|
|
45
|
+
" catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
|
|
46
46
|
" catalyst-skills replica <start [--detach]|stop|status [--probe]|sql \"<select>\"|schema [table]>",
|
|
47
|
-
" catalyst-skills explain <ticket> | running | queue [--team K]",
|
|
47
|
+
" catalyst-skills explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
|
|
48
48
|
" catalyst-skills watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
|
|
49
49
|
" catalyst-skills write <comment|state|label|create|reaction|attachment|session> ...",
|
|
50
50
|
" catalyst-skills ask <raise|accept|list> ...",
|
|
@@ -148,6 +148,8 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
148
148
|
return await cmdReplica(args, ctx, deps.replica);
|
|
149
149
|
case "explain":
|
|
150
150
|
return await cmdExplain(args, ctx);
|
|
151
|
+
case "history":
|
|
152
|
+
return await cmdHistory(args, ctx);
|
|
151
153
|
case "running":
|
|
152
154
|
return await cmdRunning(args, ctx);
|
|
153
155
|
case "queue":
|
|
@@ -161,7 +163,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
161
163
|
case "ready":
|
|
162
164
|
return await cmdReady(args, ctx, { skillNames: CUSTOMER_SKILLS, loadSdk: deps.loadSdk });
|
|
163
165
|
case "accounts":
|
|
164
|
-
return cmdAccounts(ctx);
|
|
166
|
+
return await cmdAccounts(args, ctx);
|
|
165
167
|
default:
|
|
166
168
|
ctx.stderr(`unknown command: ${args.command}`);
|
|
167
169
|
ctx.stderr(usageText());
|
|
@@ -190,10 +192,10 @@ async function cmdLogin(args, ctx, deps) {
|
|
|
190
192
|
const manifest = readManifest();
|
|
191
193
|
let key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
|
|
192
194
|
if (!key && (deps.isTty ?? stdinIsTty)()) {
|
|
193
|
-
key = (await (deps.promptSecret ?? promptSecret)("
|
|
195
|
+
key = (await (deps.promptSecret ?? promptSecret)("Personal key (not echoed): ")).trim();
|
|
194
196
|
}
|
|
195
197
|
if (!key)
|
|
196
|
-
throw new UsageError("login needs
|
|
198
|
+
throw new UsageError("login needs your personal key: pass --key <personal-key> or set CATALYST_CLOUD_TOKEN");
|
|
197
199
|
const baseUrl = normalizeBaseUrl(args.baseUrl ?? ctx.env.CATALYST_CLOUD_BASE_URL ?? DEFAULT_BASE_URL);
|
|
198
200
|
const me = await fetchMe(baseUrl, key, ctx.fetch);
|
|
199
201
|
let existing;
|
|
@@ -213,6 +215,7 @@ async function cmdLogin(args, ctx, deps) {
|
|
|
213
215
|
name: me.name,
|
|
214
216
|
permissions: me.permissions,
|
|
215
217
|
principal: me.principal,
|
|
218
|
+
...(me.user ? { user: me.user } : {}),
|
|
216
219
|
joinedAt: ctx.now().toISOString(),
|
|
217
220
|
lastSkillBundleVersion: manifest.version,
|
|
218
221
|
skillsDir,
|
|
@@ -221,6 +224,12 @@ async function cmdLogin(args, ctx, deps) {
|
|
|
221
224
|
};
|
|
222
225
|
const written = writeConfig(ctx.home, config);
|
|
223
226
|
ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
|
|
227
|
+
if (me.user) {
|
|
228
|
+
ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId ? "" : " — your Linear identity is not matched yet, so \"what needs me\" will show everyone's asks until an admin matches it in Settings"}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
ctx.stderr(`[catalyst-skills] this is the tenant's account key (a host credential), not your own — the skills work, but nothing your agent writes will carry your name and "what needs me" cannot mean you. Mint a personal key at Settings → API keys and log in with that.`);
|
|
232
|
+
}
|
|
224
233
|
ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your key and the CLI path)${written.mode === CONFIG_MODE ? "" : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
|
|
225
234
|
ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
|
|
226
235
|
try {
|
|
@@ -271,11 +280,12 @@ function cmdStatus(ctx) {
|
|
|
271
280
|
const manifest = readManifest();
|
|
272
281
|
const cfg = loadConfig(ctx.home);
|
|
273
282
|
if (!cfg) {
|
|
274
|
-
ctx.stdout(`Not connected yet — run: CATALYST_CLOUD_TOKEN=<your
|
|
283
|
+
ctx.stdout(`Not connected yet — run: CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE_NAME} login`);
|
|
275
284
|
return 0;
|
|
276
285
|
}
|
|
277
286
|
ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
|
|
278
287
|
ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
|
|
288
|
+
ctx.stdout(cfg.user ? `As: ${cfg.user.label} (${cfg.user.role})` : "As: the tenant's account key (a host credential — no person)");
|
|
279
289
|
ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
280
290
|
if (cfg.cliPath)
|
|
281
291
|
ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
|
|
@@ -290,6 +300,8 @@ async function cmdMe(args, ctx) {
|
|
|
290
300
|
else {
|
|
291
301
|
ctx.stdout(`${me.name} (${me.slug}) — account ${me.account}`);
|
|
292
302
|
ctx.stdout(`principal: ${me.principal}; permissions: ${me.permissions ? me.permissions.join(", ") : "unrestricted"}`);
|
|
303
|
+
if (me.user)
|
|
304
|
+
ctx.stdout(`user: ${me.user.label} (${me.user.role}); linear: ${me.user.linearUserId ?? "unmatched"}`);
|
|
293
305
|
}
|
|
294
306
|
return 0;
|
|
295
307
|
}
|
package/dist/config.js
CHANGED
|
@@ -70,7 +70,7 @@ export function loadConfig(home) {
|
|
|
70
70
|
export function requireConfig(ctx) {
|
|
71
71
|
const cfg = loadConfig(ctx.home);
|
|
72
72
|
if (!cfg) {
|
|
73
|
-
throw new CliError(`not connected yet — run: CATALYST_CLOUD_TOKEN=<
|
|
73
|
+
throw new CliError(`not connected yet — run: CATALYST_CLOUD_TOKEN=<your personal key> npx ${PACKAGE_NAME} login`, "not-configured");
|
|
74
74
|
}
|
|
75
75
|
return cfg;
|
|
76
76
|
}
|
package/dist/contract.js
CHANGED
|
@@ -52,7 +52,7 @@ export function assertContractRange(version, range) {
|
|
|
52
52
|
* Load the contract per the cloud's own cache policy: a cache younger than `doc.cache.maxAgeSeconds`
|
|
53
53
|
* is used as-is; otherwise a conditional GET revalidates it (304 refreshes `fetchedAt`, 200 replaces
|
|
54
54
|
* the document). A network failure on a cache older than `staleRefusalSeconds` refuses; a 403 (a
|
|
55
|
-
*
|
|
55
|
+
* personal key on an older cloud) refuses naming the cloud version; a major version outside the bundle's range refuses
|
|
56
56
|
* naming both versions.
|
|
57
57
|
*/
|
|
58
58
|
export async function loadContract(ctx, cfg, opts = {}) {
|
|
@@ -79,7 +79,7 @@ export async function loadContract(ctx, cfg, opts = {}) {
|
|
|
79
79
|
}
|
|
80
80
|
catch (err) {
|
|
81
81
|
if (err instanceof CliError && err.status === 403) {
|
|
82
|
-
throw new CliError(`GET ${CONTRACT_ROUTE} refused (403):
|
|
82
|
+
throw new CliError(`GET ${CONTRACT_ROUTE} refused (403): this cloud is older than the bundle and does not yet admit a personal key on the contract — update the cloud, or log in with the tenant's account key until it is`, "contract-forbidden", 2, 403);
|
|
83
83
|
}
|
|
84
84
|
if (err instanceof MeError && err.kind === "network" && cached) {
|
|
85
85
|
if (ageSeconds >= cached.doc.cache.staleRefusalSeconds) {
|