@catalyst-cloud/catalyst-skills 0.3.1 → 0.5.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 +16 -0
- package/README.md +28 -15
- package/dist/args.js +19 -1
- package/dist/ask.js +1 -1
- package/dist/browser.js +20 -0
- package/dist/cli.js +56 -22
- package/dist/config.js +48 -6
- package/dist/contract.js +1 -1
- package/dist/events.js +106 -0
- package/dist/execution.js +55 -9
- package/dist/oauth.js +300 -0
- package/dist/query.js +1 -1
- package/dist/ready.js +1 -1
- package/dist/release.js +142 -0
- package/dist/replica.js +10 -3
- package/dist/{http.js → transport.js} +21 -10
- package/dist/watch/consumer.js +2 -2
- package/dist/watch.js +4 -2
- package/dist/write.js +1 -1
- package/package.json +5 -4
- package/skills/catalyst-github/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-github/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +4 -3
- package/skills/catalyst-linear/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/SKILL.md +1 -1
- package/skills/catalyst-setup/references/what-each-check-means.md +5 -1
- package/skills/catalyst-setup/scripts/lib/cli.mjs +3 -2
- package/skills/catalyst-setup/scripts/lib/credential.mjs +29 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +1 -1
- package/skills/connect-me/SKILL.md +16 -8
- package/skills/connect-me/scripts/lib/cli.mjs +3 -2
- package/skills/connect-me/scripts/lib/credential.mjs +29 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +1 -1
- package/skills/how-catalyst-works/references/what-runs-next.md +1 -1
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +4 -4
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +4 -3
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +29 -0
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/run-this-project/references/making-work-ready.md +1 -1
- package/skills/run-this-project/references/stalls-and-escalation.md +2 -2
- package/skills/run-this-project/scripts/lib/cli.mjs +3 -2
- package/skills/run-this-project/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/SKILL.md +41 -0
- package/skills/unstick/agents/openai.yaml +6 -0
- package/skills/unstick/agents/portability.yaml +5 -0
- package/skills/unstick/references/playbook.md +51 -0
- package/skills/unstick/scripts/lib/cli.mjs +135 -0
- package/skills/unstick/scripts/lib/credential.mjs +29 -0
- package/skills/unstick/scripts/unstick.mjs +57 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +3 -2
- package/skills/what-needs-me/scripts/lib/credential.mjs +29 -0
- package/skills/whats-happening/SKILL.md +1 -1
- package/skills/whats-happening/references/why-is-it-stuck.md +9 -4
- package/skills/whats-happening/scripts/lib/cli.mjs +3 -2
- package/skills/whats-happening/scripts/lib/credential.mjs +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
The local replica can now keep the durable tenant event backbone beside its entity database. `catalyst-skills replica start` owns both synchronizers in one process, while `catalyst-skills events tail`, `events wait-for`, and `events query` read the bounded local cache with exact event-type and ticket filters. A failed event synchronizer is reported without stopping a healthy entity replica. This requires `@catalyst-cloud/sdk` 0.10.0 or newer.
|
|
6
|
+
|
|
7
|
+
You can release a stuck ticket yourself now. `catalyst-skills release <ticket> --because "<what changed>"` asks your tenant to release every park or hold on the ticket the right way for each one — after repeated failures, a spent repair-round cap, a repair that changed nothing, a validate failure that recurred — or to release nothing and name, for each thing it will not release, what a person has to do instead (close their own pull request, read a review that will not converge, answer the ask for a spent repair budget). `--dry-run` previews it and changes nothing; `--retry-unchanged` says something changed that the cloud cannot see, such as an outage that ended; `release --class <failure-class> --team <key>` does the same for every ticket on one team parked for the same reason. A refusal exits 1. Every release is recorded against your login, and `explain --history` now prints what holds the ticket and the ticket's past releases. The new `unstick` skill runs the whole loop — read why nothing runs, read the history, judge whether the cause is fixed, preview, release — and raises an ask only for what needs a person; the desk, the project owner and the failure references no longer say a park needs an operator. The eligibility explanation knows seven more reasons, including a parked phase and a person's own pull request. Needs a cloud that serves the release route (tenant contract 1.6.0); an older cloud says so.
|
|
8
|
+
|
|
9
|
+
Every skill's scripts now work for a machine connected with the keyless login. Since 0.4.0 a keyless `login` stores a login session and no key, and every skill script except the new `unstick` one read that as "not connected" and stopped before doing anything; only a personal key worked. The scripts now accept either, spawn the CLI, and let it authenticate and refresh the session as it always has. The check lives in one file vendored into every skill, so the next kind of credential lands once, and the not-connected line now names the keyless login first.
|
|
10
|
+
|
|
11
|
+
## 0.4.1
|
|
12
|
+
|
|
13
|
+
`explain` now tells you when a whole team cannot start work. A team whose stages are not saved (no stage chosen for dispatch, PR, done and canceled, or a chosen stage since deleted in Linear) gets no dispatch queue at all, and `explain` used to answer a ticket in that team's Todo with "state Todo is not a dispatch state" — which sent people looking at the wrong setting. It now says the ticket cannot start because the team has no saved stage mapping, names the missing stages, and names the fix: a tenant owner or admin opens Settings → Linear teams, picks the team and presses Map my stages (or Adopt the Catalyst workflow). Needs a cloud that sends the team's dispatch gate; an older cloud prints what it did before. The reason table's wording for `workflow_mapping_unknown` and `ordering_never_published` no longer reads as a passing hiccup, since neither clears on its own for an unmapped team. The references now say plainly that `teams[].gitAutomation` in the contract is a stored consent for a feature that is not built — nothing reads it, so it never starts or stops work — where they used to say enabling it could delete a team's review automation. The catalyst-setup reference gains a section on setting up one team at a time as a pilot: what starts, what stays where it is, and that no other team's stages or tickets change.
|
|
14
|
+
|
|
15
|
+
## 0.4.0
|
|
16
|
+
|
|
17
|
+
You can connect keyless now: run `catalyst-skills login` with no key and it opens a browser device-code login — it prints a short code and a URL, you approve in your browser (or from your phone on a machine with no browser), and this machine is connected as you, with nothing to mint or paste. The short-lived session token refreshes silently on every request and the config is rewritten atomically each time, so you stay connected for months and log in again only if the session is revoked or you have been away long enough to lapse — then one clear line tells you to run `login`. `customer.json` now holds exactly one of a login session (`auth`) or a personal key; `status` names which ("Credential: your login (expires …)" vs "Credential: personal key"). The personal key rail is unchanged and remains the fallback for scripts and unattended shells — `--key` or `CATALYST_CLOUD_TOKEN` — and keyless is the preferred rail everywhere the docs and the connect-me skill lead with it. Under the hood the auth layer is reworked around one credential provider (SDK 0.9): the replica and the live watch use the SDK's bearer auth strategy and resolve a fresh token per connect, so an OAuth session rotates underneath a long-running watch without a reconnect, and every HTTP read and write refreshes on the same path.
|
|
18
|
+
|
|
3
19
|
## 0.3.1
|
|
4
20
|
|
|
5
21
|
`ready` now names the person you connected as: when your key is a personal key, the config line reads "joined <tenant> as <you> (<role>)" instead of the generic "as service" that every api key showed, and an account (host) key still names the tenant with no person. `ready` also learns the bundle version your tenant expects, when the cloud publishes one: if this installed bundle is older than that minimum you get a one-line warning that names both versions and the upgrade command, but it stays a note — `ready` never refuses over a stale bundle, and a cloud that does not publish a minimum changes nothing. `explain` no longer calls every ticket with no dispatch row "unknown": a ticket that merely sits in Backlog (or any non-dispatch state) is now named as known to the mirror with the state it is in, and only a ticket the mirror has never seen keeps the unknown wording.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Or by hand. One command, for every coding agent on the machine:
|
|
|
18
18
|
npx skills@latest add coalesce-labs/catalyst-cloud-skills --all
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
It installs all
|
|
21
|
+
It installs all nine 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
22
|
|
|
23
23
|
<details><summary><strong>Alternative for Claude Code: the plugin marketplace</strong></summary>
|
|
24
24
|
|
|
@@ -55,27 +55,37 @@ Without `--all` the installer asks which skills to take and which agents to inst
|
|
|
55
55
|
|
|
56
56
|
### Then connect to your tenant
|
|
57
57
|
|
|
58
|
-
The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine
|
|
58
|
+
The skills call one CLI, and the CLI holds your credential. Install it once and connect this machine — the keyless way logs you in as yourself, with nothing to mint or paste:
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
npm install -g @catalyst-cloud/catalyst-skills
|
|
62
|
-
|
|
62
|
+
catalyst-skills login
|
|
63
63
|
catalyst-skills ready
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
`catalyst-skills login` with no key opens a device-code login: it prints a short code and a URL, you approve it in your browser, and this machine is connected as you. On a machine with no browser (a remote box, a container) the code and URL still work — approve them from your phone. The short-lived session refreshes silently afterwards, so you log in about once a year. `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>`.
|
|
67
|
+
|
|
68
|
+
Prefer a key? Pass one instead — mint a **personal key** at Settings → API keys in the Catalyst Cloud app (every member can; no admin needed). The environment form keeps it out of your shell history:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
CATALYST_CLOUD_TOKEN=<your-personal-key> catalyst-skills login
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--key <your-personal-key>` is the third form, for a script.
|
|
67
75
|
|
|
68
76
|
That is the whole setup. Everything below explains what you just installed.
|
|
69
77
|
|
|
70
78
|
## What this is
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
Nine 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 login — a keyless device-code session, or a 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.
|
|
73
81
|
|
|
74
82
|
## What connecting does
|
|
75
83
|
|
|
76
|
-
`login` does three things: it calls `GET /api/v1/me` with your
|
|
84
|
+
`login` does three things: it calls `GET /api/v1/me` with your credential, which discovers your tenant and who you are from the credential alone; it writes `~/.config/catalyst-cloud/customer.json` with file mode `0600`, holding your credential (a keyless login session, or a personal 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.
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
A keyless session's access token is short-lived and rotates on its own: every request refreshes it within a minute of expiry and rewrites the config atomically, so you stay connected for months without logging in again. You only log in again if the session is revoked or you have been away long enough to lapse — then one clear line tells you to run `login`.
|
|
87
|
+
|
|
88
|
+
`login` does not install skills; the install command above did that. Re-running `login` after a key rotation, or to switch rails, 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.
|
|
79
89
|
|
|
80
90
|
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.
|
|
81
91
|
|
|
@@ -102,12 +112,14 @@ Nothing, by default. After `login`, every read, write, ask and explanation goes
|
|
|
102
112
|
| process | needed for | what it holds on disk | lifetime |
|
|
103
113
|
| --- | --- | --- | --- |
|
|
104
114
|
| none | every read, write, ask and explain | `customer.json` and `contract.json` | the default after login |
|
|
105
|
-
| `catalyst-skills replica start` | local SQL, cheap repeated reads, `
|
|
115
|
+
| `catalyst-skills replica start` | local SQL, cheap repeated reads, and local event `tail`, `wait-for`, and `query` | one SQLite file plus a bounded event cache under `$XDG_STATE_HOME/catalyst/events/` (fallback `~/.local/state/catalyst/events/`) | one long-running process; foreground by default, `--detach` writes a pidfile beside the database and returns; `login --start-replica` does the same at the end of login |
|
|
106
116
|
| `catalyst-skills watch` | a project owner reacting to its scope | one cursor file (`~/.config/catalyst-cloud/watch-cursor.json`) stamped with the tenant | lives inside the session that armed it; exits with it |
|
|
107
117
|
|
|
108
118
|
The check every skill runs first is `catalyst-skills replica status`, which needs no network: is the pidfile's process alive, is the writer-lock heartbeat younger than the staleness threshold, and is the cursor non-empty. It exits `0` for fresh, `1` for present but stale, `2` for not connected, `3` for absent, and prints one line either way (`--json` for scripts). A fresh replica is used; anything else falls back to the API and the skill says so in its answer. A skill never refuses to work because the replica is down and never silently reads a stale one. `replica status --probe` compares the local cursor against the cloud's head for the honest "how far behind" number; that is the only form that touches the network. `catalyst-skills replica stop` stops a detached writer.
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
`catalyst-skills events tail` follows new cached events, `events wait-for --type ... --ticket ... --timeout ...` performs a bounded wait, and `events query` reads retained history. These commands never write the cache or contact the cloud. The replica process is the single writer and reports an event-sync failure without terminating a healthy entity replica. The event cache keeps closed daily segments for at most seven days or 256 MiB per tenant and reports an explicit gap when a requested sequence has retired.
|
|
121
|
+
|
|
122
|
+
The replica is a Node process, not a service: the supported path is the plain command, and `skills/connect-me/references/keeping-the-replica-running.md` gives launchd and systemd examples for people who want the writer to survive a reboot.
|
|
111
123
|
|
|
112
124
|
## The skills
|
|
113
125
|
|
|
@@ -120,26 +132,26 @@ Nothing rotates. The replica is upserts and deletes into one file, the cursor is
|
|
|
120
132
|
| `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) |
|
|
121
133
|
| `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) |
|
|
122
134
|
| `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) |
|
|
135
|
+
| `unstick` | "Why is this parked? Unpark it. Get things flowing again." | Reads why a ticket is not running and every park or hold on it, decides whether the recorded cause is fixed, previews the release and releases it the right way from your own login — or one failure class across a team — and raises an ask only for what a person has to do. | [`SKILL.md`](skills/unstick/SKILL.md) |
|
|
123
136
|
| `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) |
|
|
124
137
|
|
|
125
|
-
Four skills only read (`whats-happening`, `catalyst-setup`, `catalyst-github`, `how-catalyst-works`). The
|
|
138
|
+
Four skills only read (`whats-happening`, `catalyst-setup`, `catalyst-github`, `how-catalyst-works`). The five that write anything (`catalyst-linear`, `what-needs-me`, `run-this-project`, `connect-me`, `unstick`) 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.
|
|
126
139
|
|
|
127
140
|
## What a key cannot see yet
|
|
128
141
|
|
|
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`).
|
|
142
|
+
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`). It also releases a parked or held ticket once its cause is fixed: `catalyst-skills release <ticket> --because <what changed>` (the `unstick` skill runs it), recorded against your name and shown in the ticket's history. One thing it cannot do, and the skills say so by name rather than guess:
|
|
130
143
|
|
|
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
144
|
- Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
|
|
133
145
|
|
|
134
146
|
## Versions and origins
|
|
135
147
|
|
|
136
|
-
The package pins the tenant contract range `1.x`, recorded in `package.json` under `catalystCloud.tenantContractRange`, and reports it in `--version`, `status` and `login`. A tenant whose contract version falls outside that range is refused with one line naming both versions; update the bundle. Every skill carries a `vendored-from:` line naming this package as its origin; all
|
|
148
|
+
The package pins the tenant contract range `1.x`, recorded in `package.json` under `catalystCloud.tenantContractRange`, and reports it in `--version`, `status` and `login`. A tenant whose contract version falls outside that range is refused with one line naming both versions; update the bundle. Every skill carries a `vendored-from:` line naming this package as its origin; all nine are written in this repository for customer tenants.
|
|
137
149
|
|
|
138
150
|
## What it writes on your machine
|
|
139
151
|
|
|
140
152
|
- `~/.config/catalyst-cloud/customer.json`, written with mode `0600`, holding your personal key, who you are, and the CLI path.
|
|
141
153
|
- `~/.config/catalyst-cloud/contract.json`, the cached tenant contract.
|
|
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`.
|
|
154
|
+
- Only if you start them: `~/.config/catalyst-cloud/replica.db` with its `.pid` and `.writer.lock` sidecars, `$XDG_STATE_HOME/catalyst/events/<tenant>/backbone/` (or the home-directory fallback) with bounded daily event segments, and `~/.config/catalyst-cloud/watch-cursor.json`.
|
|
143
155
|
|
|
144
156
|
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.
|
|
145
157
|
|
|
@@ -155,12 +167,13 @@ A `customer.json` written by an older bundle is still read unchanged; it gains t
|
|
|
155
167
|
|
|
156
168
|
## Uninstalling
|
|
157
169
|
|
|
158
|
-
Remove the skills the way you installed them: `/plugin uninstall catalyst@catalyst-cloud` in Claude Code, or delete the
|
|
170
|
+
Remove the skills the way you installed them: `/plugin uninstall catalyst@catalyst-cloud` in Claude Code, or delete the nine directories (`catalyst-github`, `catalyst-linear`, `catalyst-setup`, `connect-me`, `how-catalyst-works`, `run-this-project`, `unstick`, `what-needs-me`, `whats-happening`) from wherever `npx skills add` wrote them. Then remove what the CLI wrote:
|
|
159
171
|
|
|
160
172
|
```sh
|
|
161
173
|
catalyst-skills replica stop
|
|
162
174
|
rm -f ~/.config/catalyst-cloud/customer.json ~/.config/catalyst-cloud/contract.json ~/.config/catalyst-cloud/watch-cursor.json
|
|
163
175
|
rm -f ~/.config/catalyst-cloud/replica.db ~/.config/catalyst-cloud/replica.db.pid ~/.config/catalyst-cloud/replica.db.writer.lock
|
|
176
|
+
rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/catalyst/events"
|
|
164
177
|
npm uninstall -g @catalyst-cloud/catalyst-skills
|
|
165
178
|
```
|
|
166
179
|
|
package/dist/args.js
CHANGED
|
@@ -37,6 +37,14 @@ export const FLAG_TABLES = {
|
|
|
37
37
|
probe: { value: false, help: "status: also fetch the cloud head and print the lag" },
|
|
38
38
|
"stale-ms": { value: true, help: "status: heartbeat age that counts as stale (default 15000)" },
|
|
39
39
|
},
|
|
40
|
+
events: {
|
|
41
|
+
type: { value: true, help: "exact event type" },
|
|
42
|
+
ticket: { value: true, help: "ticket identifier found in the event payload" },
|
|
43
|
+
after: { value: true, help: "event sequence to read after (tail/wait default to local head)" },
|
|
44
|
+
limit: { value: true, help: "query: maximum matching events (default 50)" },
|
|
45
|
+
timeout: { value: true, help: "wait-for: bounded wait in seconds (default 300)" },
|
|
46
|
+
directory: { value: true, help: "event cache directory (default: SDK XDG path)" },
|
|
47
|
+
},
|
|
40
48
|
explain: {
|
|
41
49
|
history: { value: false, help: "per-ticket execution history instead of the eligibility reason" },
|
|
42
50
|
},
|
|
@@ -92,9 +100,17 @@ export const FLAG_TABLES = {
|
|
|
92
100
|
},
|
|
93
101
|
ready: {},
|
|
94
102
|
accounts: {},
|
|
103
|
+
release: {
|
|
104
|
+
because: { value: true, help: "what changed since the ticket was held (required unless --dry-run)" },
|
|
105
|
+
"retry-unchanged": { value: false, help: "release even though nothing the mirror can see changed (say what did in --because)" },
|
|
106
|
+
"dry-run": { value: false, help: "show what a release would clear and refuse, and change nothing" },
|
|
107
|
+
class: { value: true, help: "release every ticket on --team parked under this failure class" },
|
|
108
|
+
team: { value: true, help: "with --class: the team key" },
|
|
109
|
+
limit: { value: true, help: "with --class: at most this many tickets (the cloud caps it at 25)" },
|
|
110
|
+
},
|
|
95
111
|
};
|
|
96
112
|
export const VERB_USAGE = {
|
|
97
|
-
login: "login [--
|
|
113
|
+
login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
|
|
98
114
|
install: "install [--skills-dir <dir>] [--force]",
|
|
99
115
|
status: "status",
|
|
100
116
|
notice: "notice",
|
|
@@ -102,6 +118,7 @@ export const VERB_USAGE = {
|
|
|
102
118
|
contract: "contract [--refresh] [--path <a.b.c>] [--json]",
|
|
103
119
|
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]",
|
|
104
120
|
replica: "replica <start [--detach]|stop|status [--probe] [--json]|sql \"<select>\"|schema [table]> [--db <path>]",
|
|
121
|
+
events: "events <tail|wait-for|query> [--type NAME] [--ticket CTC-N] [--after SEQUENCE] [--limit N] [--timeout SECONDS] [--directory PATH]",
|
|
105
122
|
explain: "explain <ticket> [--history] [--json]",
|
|
106
123
|
history: "history <ticket> [--json]",
|
|
107
124
|
running: "running [--ticket T --phase P] [--json]",
|
|
@@ -111,6 +128,7 @@ export const VERB_USAGE = {
|
|
|
111
128
|
ask: "ask <raise --team --title [--context] [--option]... [--default] --blocks <ticket>...|--nothing-to-block [--ask-key] | accept <askTicket> --answer <commentId> --role <role> | list [--anyone] [--json]>",
|
|
112
129
|
ready: "ready [--json]",
|
|
113
130
|
accounts: "accounts [--json]",
|
|
131
|
+
release: "release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] [--json] | release --class <failure-class> --team <K> --because <what changed> [--retry-unchanged] [--dry-run] [--limit N] [--json]",
|
|
114
132
|
};
|
|
115
133
|
export function parseArgs(argv) {
|
|
116
134
|
const out = {
|
package/dist/ask.js
CHANGED
|
@@ -5,7 +5,7 @@ import { flagBool, flagList, flagString, positionals } from "./args.js";
|
|
|
5
5
|
import { requireConfig } from "./config.js";
|
|
6
6
|
import { loadContract, teamByKey } from "./contract.js";
|
|
7
7
|
import { UsageError } from "./errors.js";
|
|
8
|
-
import { apiClient } from "./
|
|
8
|
+
import { apiClient } from "./transport.js";
|
|
9
9
|
import { rowsOf } from "./query.js";
|
|
10
10
|
import { fetchWorkflowStates, postAgent, resolveIssue } from "./write.js";
|
|
11
11
|
export async function cmdAsk(args, ctx) {
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// browser.ts — open the device-flow verification page in the person's default browser. Best-effort:
|
|
2
|
+
// a machine with no browser (a headless mini, a container) simply shows the code and URL instead, so
|
|
3
|
+
// this never throws into the login flow — the caller wraps it, and a spawn failure is swallowed here.
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
/** The platform's "open this URL" command. `open` on macOS, `start` via cmd on Windows, else xdg-open. */
|
|
6
|
+
export function openBrowser(url) {
|
|
7
|
+
const [cmd, args] = process.platform === "darwin"
|
|
8
|
+
? ["open", [url]]
|
|
9
|
+
: process.platform === "win32"
|
|
10
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
11
|
+
: ["xdg-open", [url]];
|
|
12
|
+
try {
|
|
13
|
+
const child = spawn(cmd, [...args], { stdio: "ignore", detached: true });
|
|
14
|
+
child.on("error", () => { }); // no opener installed — the printed URL is the fallback
|
|
15
|
+
child.unref();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// spawn itself threw (unusual) — the code and URL on screen are the fallback
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,17 +5,21 @@ import { parseArgs, positionals, verbHelp } from "./args.js";
|
|
|
5
5
|
import { CONFIG_MODE, DEFAULT_BASE_URL, PACKAGE_NAME, cliPath, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, formatMode, loadConfig, normalizeBaseUrl, readManifest, requireConfig, saveConfig, writeConfig, } from "./config.js";
|
|
6
6
|
import { loadContract, pickPath } from "./contract.js";
|
|
7
7
|
import { CliError, MeError, UsageError } from "./errors.js";
|
|
8
|
-
import { fetchMe } from "./
|
|
8
|
+
import { fetchMe } from "./transport.js";
|
|
9
|
+
import { bearerFor, deviceFlowLogin } from "./oauth.js";
|
|
10
|
+
import { openBrowser as defaultOpenBrowser } from "./browser.js";
|
|
9
11
|
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning } from "./execution.js";
|
|
10
12
|
import { cmdQuery } from "./query.js";
|
|
11
13
|
import { cmdReady } from "./ready.js";
|
|
12
14
|
import { cmdReplica } from "./replica.js";
|
|
13
|
-
import {
|
|
15
|
+
import { cmdEvents } from "./events.js";
|
|
16
|
+
import { stdinIsTty } from "./prompt.js";
|
|
14
17
|
import { PROVENANCE_MARKER } from "./skill-shape.js";
|
|
15
18
|
import { installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine } from "./skills.js";
|
|
16
19
|
import { cmdWatch } from "./watch.js";
|
|
17
20
|
import { cmdWrite } from "./write.js";
|
|
18
21
|
import { cmdAsk } from "./ask.js";
|
|
22
|
+
import { cmdRelease } from "./release.js";
|
|
19
23
|
export { CONFIG_MODE, DEFAULT_BASE_URL, PACKAGE_NAME, PROVENANCE_MARKER, CliError, MeError, UsageError, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, fetchMe, formatMode, installSkills, loadConfig, normalizeBaseUrl, parseArgs, parseChangelogEntry, readChangelog, readManifest, resolveSkillsDir, saveConfig, skillsSourceDir, updateNoticeLine, writeConfig, };
|
|
20
24
|
export const CUSTOMER_SKILLS = [
|
|
21
25
|
"catalyst-github",
|
|
@@ -24,6 +28,7 @@ export const CUSTOMER_SKILLS = [
|
|
|
24
28
|
"connect-me",
|
|
25
29
|
"how-catalyst-works",
|
|
26
30
|
"run-this-project",
|
|
31
|
+
"unstick",
|
|
27
32
|
"what-needs-me",
|
|
28
33
|
"whats-happening",
|
|
29
34
|
];
|
|
@@ -37,28 +42,33 @@ export function usageText() {
|
|
|
37
42
|
`${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your tenant, read through the SDK, write through the agent proxy`,
|
|
38
43
|
"",
|
|
39
44
|
"Usage:",
|
|
40
|
-
" catalyst-skills login [--
|
|
45
|
+
" catalyst-skills login [--base-url <url>] [--start-replica] (keyless: logs you in as yourself)",
|
|
46
|
+
" catalyst-skills login --key <personal-key> [--base-url <url>] (or CATALYST_CLOUD_TOKEN, for a key)",
|
|
41
47
|
" catalyst-skills join ... (deprecated alias of login; removed in the next minor version)",
|
|
42
48
|
" catalyst-skills install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
|
|
43
49
|
" catalyst-skills status | notice | me | ready | accounts",
|
|
44
50
|
" catalyst-skills contract [--refresh] [--path <a.b.c>]",
|
|
45
51
|
" catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
|
|
46
52
|
" catalyst-skills replica <start [--detach]|stop|status [--probe]|sql \"<select>\"|schema [table]>",
|
|
53
|
+
" catalyst-skills events <tail|wait-for|query> [--type NAME] [--ticket CTC-N] [--after SEQUENCE]",
|
|
47
54
|
" catalyst-skills explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
|
|
48
55
|
" catalyst-skills watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
|
|
49
56
|
" catalyst-skills write <comment|state|label|create|reaction|attachment|session> ...",
|
|
50
57
|
" catalyst-skills ask <raise|accept|list> ...",
|
|
58
|
+
" catalyst-skills release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] | release --class <c> --team <K> ...",
|
|
51
59
|
"",
|
|
52
60
|
"Every verb takes --help. --json makes the output machine-readable.",
|
|
53
61
|
"",
|
|
54
|
-
"
|
|
55
|
-
|
|
62
|
+
"With no key, login is keyless: it opens a WorkOS device-code flow, prints a short code and a URL,",
|
|
63
|
+
"and connects you as yourself once you approve in the browser — nothing to mint or paste. Pass",
|
|
64
|
+
"--key (or set CATALYST_CLOUD_TOKEN) to use a personal or account key instead. The base URL defaults",
|
|
65
|
+
`to CATALYST_CLOUD_BASE_URL or ${DEFAULT_BASE_URL}.`,
|
|
56
66
|
"",
|
|
57
|
-
"login
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
67
|
+
"login discovers your tenant from GET /api/v1/me, writes ~/.config/catalyst-cloud/customer.json",
|
|
68
|
+
"(0600) holding your login session (or key) and this CLI's path, and caches the tenant contract",
|
|
69
|
+
"beside it. A keyless session's short-lived token refreshes silently on every request. login does",
|
|
70
|
+
"not install skills: your agent's own install command does that, and `install` is only here to",
|
|
71
|
+
"repair a copy this package made.",
|
|
62
72
|
].join("\n");
|
|
63
73
|
}
|
|
64
74
|
/**
|
|
@@ -146,6 +156,8 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
146
156
|
return await cmdQuery(args, ctx, { engineDeps: deps.replica?.engineDeps });
|
|
147
157
|
case "replica":
|
|
148
158
|
return await cmdReplica(args, ctx, deps.replica);
|
|
159
|
+
case "events":
|
|
160
|
+
return await cmdEvents(args, ctx, deps.events);
|
|
149
161
|
case "explain":
|
|
150
162
|
return await cmdExplain(args, ctx);
|
|
151
163
|
case "history":
|
|
@@ -164,6 +176,8 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
164
176
|
return await cmdReady(args, ctx, { skillNames: CUSTOMER_SKILLS, loadSdk: deps.loadSdk });
|
|
165
177
|
case "accounts":
|
|
166
178
|
return await cmdAccounts(args, ctx);
|
|
179
|
+
case "release":
|
|
180
|
+
return await cmdRelease(args, ctx);
|
|
167
181
|
default:
|
|
168
182
|
ctx.stderr(`unknown command: ${args.command}`);
|
|
169
183
|
ctx.stderr(usageText());
|
|
@@ -187,17 +201,22 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
187
201
|
throw err;
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
|
-
const VERB_HELP_KNOWN = Object.fromEntries(["login", "join", "install", "status", "notice", "me", "contract", "query", "replica", "explain", "running", "queue", "watch", "write", "ask", "ready", "accounts"].map((v) => [v, true]));
|
|
204
|
+
const VERB_HELP_KNOWN = Object.fromEntries(["login", "join", "install", "status", "notice", "me", "contract", "query", "replica", "events", "explain", "running", "queue", "watch", "write", "ask", "ready", "accounts", "release"].map((v) => [v, true]));
|
|
191
205
|
async function cmdLogin(args, ctx, deps) {
|
|
192
206
|
const manifest = readManifest();
|
|
193
|
-
|
|
194
|
-
if (!key && (deps.isTty ?? stdinIsTty)()) {
|
|
195
|
-
key = (await (deps.promptSecret ?? promptSecret)("Personal key (not echoed): ")).trim();
|
|
196
|
-
}
|
|
197
|
-
if (!key)
|
|
198
|
-
throw new UsageError("login needs your personal key: pass --key <personal-key> or set CATALYST_CLOUD_TOKEN");
|
|
207
|
+
const key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
|
|
199
208
|
const baseUrl = normalizeBaseUrl(args.baseUrl ?? ctx.env.CATALYST_CLOUD_BASE_URL ?? DEFAULT_BASE_URL);
|
|
200
|
-
|
|
209
|
+
// Keyless is the preferred rail: with no key in flag or env, run the WorkOS device flow and log in
|
|
210
|
+
// as the person. A key (flag or CATALYST_CLOUD_TOKEN) still takes the token rail unchanged.
|
|
211
|
+
let auth;
|
|
212
|
+
const bearer = key
|
|
213
|
+
? key
|
|
214
|
+
: (auth = await deviceFlowLogin(ctx, baseUrl, {
|
|
215
|
+
isTty: deps.isTty ?? stdinIsTty,
|
|
216
|
+
openBrowser: deps.openBrowser ?? defaultOpenBrowser,
|
|
217
|
+
sleep: deps.sleep,
|
|
218
|
+
})).accessToken;
|
|
219
|
+
const me = await fetchMe(baseUrl, bearer, ctx.fetch);
|
|
201
220
|
let existing;
|
|
202
221
|
try {
|
|
203
222
|
existing = loadConfig(ctx.home);
|
|
@@ -209,7 +228,8 @@ async function cmdLogin(args, ctx, deps) {
|
|
|
209
228
|
const skillsDir = resolveSkillsDir(args, ctx, existing);
|
|
210
229
|
const config = {
|
|
211
230
|
baseUrl,
|
|
212
|
-
key,
|
|
231
|
+
...(key ? { key } : {}),
|
|
232
|
+
...(auth ? { auth } : {}),
|
|
213
233
|
account: me.account,
|
|
214
234
|
slug: me.slug,
|
|
215
235
|
name: me.name,
|
|
@@ -230,7 +250,7 @@ async function cmdLogin(args, ctx, deps) {
|
|
|
230
250
|
else {
|
|
231
251
|
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
252
|
}
|
|
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`}`);
|
|
253
|
+
ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your ${auth ? "login session" : "key"} and the CLI path)${written.mode === CONFIG_MODE ? "" : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
|
|
234
254
|
ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
|
|
235
255
|
try {
|
|
236
256
|
const loaded = await loadContract(ctx, config, { refresh: true });
|
|
@@ -280,21 +300,35 @@ function cmdStatus(ctx) {
|
|
|
280
300
|
const manifest = readManifest();
|
|
281
301
|
const cfg = loadConfig(ctx.home);
|
|
282
302
|
if (!cfg) {
|
|
283
|
-
ctx.stdout(`Not connected yet — run:
|
|
303
|
+
ctx.stdout(`Not connected yet — run: npx ${PACKAGE_NAME} login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN)`);
|
|
284
304
|
return 0;
|
|
285
305
|
}
|
|
286
306
|
ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
|
|
287
307
|
ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
|
|
288
308
|
ctx.stdout(cfg.user ? `As: ${cfg.user.label} (${cfg.user.role})` : "As: the tenant's account key (a host credential — no person)");
|
|
309
|
+
ctx.stdout(cfg.auth ? `Credential: your login (expires ${relativeExpiry(cfg.auth.expiresAt, ctx.now())})` : "Credential: personal key");
|
|
289
310
|
ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
290
311
|
if (cfg.cliPath)
|
|
291
312
|
ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
|
|
292
313
|
ctx.stdout(`Contract: ${existsSync(contractPathFor(ctx.home)) ? contractPathFor(ctx.home) : "not cached (run: catalyst-skills contract --refresh)"}`);
|
|
293
314
|
return 0;
|
|
294
315
|
}
|
|
316
|
+
/** A short human relative time for an ISO expiry, e.g. "in 14m", "in 2h", "in 6d", or "now" once past. */
|
|
317
|
+
function relativeExpiry(iso, now) {
|
|
318
|
+
const ms = Date.parse(iso) - now.getTime();
|
|
319
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
320
|
+
return "now";
|
|
321
|
+
const mins = Math.round(ms / 60_000);
|
|
322
|
+
if (mins < 60)
|
|
323
|
+
return `in ${mins}m`;
|
|
324
|
+
const hours = Math.round(mins / 60);
|
|
325
|
+
if (hours < 48)
|
|
326
|
+
return `in ${hours}h`;
|
|
327
|
+
return `in ${Math.round(hours / 24)}d`;
|
|
328
|
+
}
|
|
295
329
|
async function cmdMe(args, ctx) {
|
|
296
330
|
const cfg = requireConfig(ctx);
|
|
297
|
-
const me = await fetchMe(cfg.baseUrl, cfg
|
|
331
|
+
const me = await fetchMe(cfg.baseUrl, await bearerFor(ctx, cfg), ctx.fetch);
|
|
298
332
|
if (args.json)
|
|
299
333
|
ctx.stdout(JSON.stringify(me));
|
|
300
334
|
else {
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// config.ts — ~/.config/catalyst-cloud/customer.json and its siblings. The ONLY place the base URL
|
|
2
2
|
// and the `/api/v1` prefix are joined: the SDK wants the base with the prefix, GET /me without.
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { CliError } from "./errors.js";
|
|
@@ -29,6 +30,10 @@ export function contractPathFor(home) {
|
|
|
29
30
|
export function watchCursorPathFor(home) {
|
|
30
31
|
return join(configDirFor(home), "watch-cursor.json");
|
|
31
32
|
}
|
|
33
|
+
/** CTC-2112 — where the cached CLI-auth discovery document lives (the `/api/v1/auth/cli` answer). */
|
|
34
|
+
export function discoveryCachePathFor(home) {
|
|
35
|
+
return join(configDirFor(home), "auth-discovery.json");
|
|
36
|
+
}
|
|
32
37
|
export function defaultReplicaDbFor(home) {
|
|
33
38
|
return join(configDirFor(home), "replica.db");
|
|
34
39
|
}
|
|
@@ -61,28 +66,65 @@ export function loadConfig(home) {
|
|
|
61
66
|
throw new CliError(`config at ${path} is not valid JSON — re-run login to rewrite it`, "config-corrupt");
|
|
62
67
|
}
|
|
63
68
|
const cfg = parsed;
|
|
64
|
-
if (typeof cfg.account !== "string" || typeof cfg.
|
|
69
|
+
if (typeof cfg.account !== "string" || typeof cfg.baseUrl !== "string") {
|
|
65
70
|
throw new CliError(`config at ${path} is missing required fields — re-run login to rewrite it`, "config-corrupt");
|
|
66
71
|
}
|
|
72
|
+
const hasKey = typeof cfg.key === "string" && cfg.key !== "";
|
|
73
|
+
const hasOauth = isOauthAuth(cfg.auth);
|
|
74
|
+
if (hasKey === hasOauth) {
|
|
75
|
+
throw new CliError(`config at ${path} must hold exactly one of a personal key or an OAuth session — re-run login to rewrite it`, "config-corrupt");
|
|
76
|
+
}
|
|
67
77
|
return cfg;
|
|
68
78
|
}
|
|
69
79
|
/** Load the config or refuse with the one line every verb prints when the machine is not connected. */
|
|
70
80
|
export function requireConfig(ctx) {
|
|
71
81
|
const cfg = loadConfig(ctx.home);
|
|
72
82
|
if (!cfg) {
|
|
73
|
-
throw new CliError(`not connected yet — run:
|
|
83
|
+
throw new CliError(`not connected yet — run: npx ${PACKAGE_NAME} login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN for a key)`, "not-configured");
|
|
74
84
|
}
|
|
75
85
|
return cfg;
|
|
76
86
|
}
|
|
77
87
|
export function saveConfig(home, cfg) {
|
|
78
88
|
return writeConfig(home, cfg).path;
|
|
79
89
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
90
|
+
/** True when `v` is a well-formed OAuth session block. */
|
|
91
|
+
export function isOauthAuth(v) {
|
|
92
|
+
if (typeof v !== "object" || v === null)
|
|
93
|
+
return false;
|
|
94
|
+
const a = v;
|
|
95
|
+
return (a.kind === "oauth" &&
|
|
96
|
+
typeof a.accessToken === "string" &&
|
|
97
|
+
typeof a.refreshToken === "string" &&
|
|
98
|
+
typeof a.expiresAt === "string" &&
|
|
99
|
+
typeof a.sessionId === "string");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Write the config atomically (a rotated token must never be half-written under a crash) and return
|
|
103
|
+
* the mode the file ACTUALLY carries afterwards. Write to a sibling `.tmp` at 0600, then rename over
|
|
104
|
+
* the target — rename is atomic within a directory — and chmod once more (a pre-existing target keeps
|
|
105
|
+
* its own mode through the rename on some platforms).
|
|
106
|
+
*/
|
|
82
107
|
export function writeConfig(home, cfg) {
|
|
83
108
|
const path = configPathFor(home);
|
|
84
109
|
mkdirSync(dirname(path), { recursive: true });
|
|
85
|
-
|
|
110
|
+
// A UNIQUE sibling temp per write (pid + randomness): two concurrent CLI processes reaching the
|
|
111
|
+
// OAuth refresh window must not share `customer.json.tmp`, or one renames/removes it out from under
|
|
112
|
+
// the other and the second write fails with ENOENT or lands the wrong contents (Codex P2).
|
|
113
|
+
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
114
|
+
try {
|
|
115
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: CONFIG_MODE });
|
|
116
|
+
chmodSync(tmp, CONFIG_MODE);
|
|
117
|
+
renameSync(tmp, path);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
try {
|
|
121
|
+
rmSync(tmp, { force: true });
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// best-effort cleanup of the temp file
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
86
128
|
chmodSync(path, CONFIG_MODE);
|
|
87
129
|
return { path, mode: statSync(path).mode & 0o777 };
|
|
88
130
|
}
|
package/dist/contract.js
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "
|
|
|
5
5
|
import { dirname } from "node:path";
|
|
6
6
|
import { contractPathFor, readManifest } from "./config.js";
|
|
7
7
|
import { CliError, MeError } from "./errors.js";
|
|
8
|
-
import { apiClient } from "./
|
|
8
|
+
import { apiClient } from "./transport.js";
|
|
9
9
|
export const CONTRACT_ROUTE = "/api/v1/agent/contract";
|
|
10
10
|
export function readContractCache(home) {
|
|
11
11
|
const path = contractPathFor(home);
|