@catalyst-cloud/cli 0.15.2 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +5 -17
  4. package/dist/oauth.js +1 -1
  5. package/dist/onboard-accounts.js +67 -171
  6. package/dist/onboard-checklist.js +309 -0
  7. package/dist/onboard-existing.js +1 -1
  8. package/dist/onboard-first-ticket.js +199 -0
  9. package/dist/onboard-headless.js +2 -2
  10. package/dist/onboard-housekeeping.js +93 -0
  11. package/dist/onboard-next.js +108 -9
  12. package/dist/onboard-ready.js +61 -0
  13. package/dist/onboard-runtime.js +60 -27
  14. package/dist/onboard-standalone-copy.js +73 -66
  15. package/dist/onboard-ui.js +162 -89
  16. package/dist/onboard-values.js +47 -0
  17. package/dist/onboard.js +49 -13
  18. package/dist/ready-copy.js +87 -0
  19. package/dist/ready.js +1 -57
  20. package/dist/setup-onboard-copy.js +237 -41
  21. package/dist/setup-render.js +73 -16
  22. package/dist/setup.js +6 -3
  23. package/package.json +1 -1
  24. package/skills/catalyst-github/SKILL.md +1 -1
  25. package/skills/catalyst-linear/SKILL.md +1 -1
  26. package/skills/catalyst-onboard/SKILL.md +3 -3
  27. package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
  28. package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
  29. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  30. package/skills/run-this-project/SKILL.md +2 -2
  31. package/skills/unstick/SKILL.md +1 -1
  32. package/skills/what-needs-me/SKILL.md +1 -1
  33. package/skills/what-this-repo-needs/SKILL.md +1 -1
  34. package/skills/whats-happening/SKILL.md +2 -2
  35. package/skills/whats-happening/references/coding-accounts.md +1 -1
  36. package/skills/whats-happening/scripts/snapshot.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.3
4
+
5
+ - Setup can finish once the steps needed to run work are ready. It lists optional next steps and keeps your first-ticket choice when you resume.
6
+ - Setup shows one progress display with numbered steps and the reason a step needs attention.
7
+ - When the Connect accounts page is available, setup uses it to finish account connections. Any usable AI account counts without a test request.
8
+ - Setup offers API keys by default. Subscription account options appear only when your workspace enables them.
9
+
3
10
  ## 0.15.2
4
11
 
5
12
  - `catalyst setup` is the command to set up Catalyst. It runs the same steps as `catalyst onboard`, which still works.
package/README.md CHANGED
@@ -111,7 +111,7 @@ The command is `catalyst`. `catalyst-skills` still works as a deprecated alias,
111
111
 
112
112
  ## What this is
113
113
 
114
- A set of skills that let your coding agent run your own Catalyst Cloud account (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your account through the Catalyst Cloud SDK, write to it through the account's agent proxy, and never compose a URL or run a tool of their own; every read, write and subscription is a `catalyst` 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.
114
+ A set of skills that let your coding agent run your own Catalyst Cloud account (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your account through the Catalyst Cloud SDK, write to it through the account's agent proxy, and never compose a URL or run a tool of their own; every read, write and live watch is a `catalyst` 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.
115
115
 
116
116
  ## What connecting does
117
117
 
@@ -231,7 +231,7 @@ Some of these only read, and some write: a comment, a card move, an ask, a relea
231
231
 
232
232
  ## What a key cannot see yet
233
233
 
234
- Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst 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 release <ticket> --because <what changed>` (the `unstick` skill runs it), recorded against your name and shown in the ticket's history. And it declares what your containers need: `catalyst environment` reads the account-wide declaration, `environment propose --file <path> --approve` proposes and approves it in one compare-and-set, and the values behind the names stay in the cloud — reading needs any active seat, proposing and approving need an admin or owner one. An admin or owner can put a repository's values in from the terminal. `catalyst secret import .env --repo owner/name` stores every name in the file and lists the declared names that still have no value. `catalyst secret set NAME --repo owner/name --command 'op read op://Vault/item/field'` runs the command on your machine and stores its output; with no `--command` it reads the value from stdin, or asks for it without echoing. No value is ever printed, and the cloud's audit records the command, not its output. That is the account's own declaration — a repository's own `.catalyst/catalyst.toml`, which `catalyst-onboard` already reports on, is a separate thing. `catalyst env inventory` lists the environment variable names a repository needs — never a value; `env check` checks the TOML syntax and environment variable table in `.catalyst/catalyst.toml` offline; `env migrate [catalyst.env.json]` prints a TOML environment table from a legacy declaration, with all names optional and values omitted. These commands need no login (the `what-this-repo-needs` skill walks through them). Two things it cannot do, and the skills say so by name rather than guess:
234
+ Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst accounts`: provider, declared and observed state, usage limits, 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 release <ticket> --because <what changed>` (the `unstick` skill runs it), recorded against your name and shown in the ticket's history. And it declares what your containers need: `catalyst environment` reads the account-wide declaration, `environment propose --file <path> --approve` proposes and approves it in one compare-and-set, and the values behind the names stay in the cloud — reading needs any active seat, proposing and approving need an admin or owner one. An admin or owner can put a repository's values in from the terminal. `catalyst secret import .env --repo owner/name` stores every name in the file and lists the declared names that still have no value. `catalyst secret set NAME --repo owner/name --command 'op read op://Vault/item/field'` runs the command on your machine and stores its output; with no `--command` it reads the value from stdin, or asks for it without echoing. No value is ever printed, and the cloud's audit records the command, not its output. That is the account's own declaration — a repository's own `.catalyst/catalyst.toml`, which `catalyst-onboard` already reports on, is a separate thing. `catalyst env inventory` lists the environment variable names a repository needs — never a value; `env check` checks the TOML syntax and environment variable table in `.catalyst/catalyst.toml` offline; `env migrate [catalyst.env.json]` prints a TOML environment table from a legacy declaration, with all names optional and values omitted. These commands need no login (the `what-this-repo-needs` skill walks through them). Two things it cannot do, and the skills say so by name rather than guess:
235
235
 
236
236
  - Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
237
237
  - Compute a flow number — cycle time, throughput, or how long pull requests have been open. Nothing serves those yet, so say they are not computed rather than counting something else and calling it that.
package/dist/cli.js CHANGED
@@ -44,12 +44,12 @@ import { cmdRepo } from "./repo.js";
44
44
  import { cmdLegacy } from "./legacy.js";
45
45
  import { cmdIdentity } from "./identity.js";
46
46
  import { cmdConnections } from "./connections.js";
47
- import { cmdOnboard, onboardErrorJournal, onboardRequiredStepsWaiting, } from "./onboard.js";
47
+ import { cmdOnboard, onboardErrorJournal, } from "./onboard.js";
48
48
  import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
49
49
  import { createOnboardRuntime } from "./onboard-runtime.js";
50
50
  import { argvRequestsHeadless, headlessOnboardDeps, headlessUsageRefusal, onboardHeadlessRequested, runOnboardHeadless, } from "./onboard-headless.js";
51
51
  import { savedOnboardBaseUrl } from "./onboard-next.js";
52
- import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
52
+ import { onboardingReadyReport, onboardReadyStep, observeCloudOnboarding, } from "./onboard-ready.js";
53
53
  import { selectedOnboardTeam } from "./onboard-existing.js";
54
54
  export { CONFIG_MODE, DEFAULT_BASE_URL, FIRST_STAMPED_VERSION, LEGACY_PACKAGE_NAME, PACKAGE_NAME, PROVENANCE_MARKER, CliError, MeError, UsageError, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, fetchMe, formatMode, installedBundleVersion, installSkills, loadConfig, modernCliPath, normalizeBaseUrl, parseArgs, parseChangelogEntry, parseProvenanceVersion, readChangelog, readManifest, resolveSkillsDir, saveConfig, skillsSourceDir, updateNoticeLine, writeConfig, };
55
55
  export const CUSTOMER_SKILLS = [
@@ -306,6 +306,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
306
306
  renderer: createSetupRenderer(process.stdout, ctx.env),
307
307
  disposeRenderer: true,
308
308
  signinTimeoutMs: deps.onboardSigninTimeoutMs,
309
+ version: manifest.version,
309
310
  });
310
311
  }
311
312
  catch (error) {
@@ -328,6 +329,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
328
329
  renderer: createSetupRenderer(output, ctx.env),
329
330
  disposeRenderer: true,
330
331
  signinTimeoutMs: deps.onboardSigninTimeoutMs,
332
+ version: manifest.version,
331
333
  interactive: false,
332
334
  verbose: args.flags.verbose === true,
333
335
  });
@@ -395,21 +397,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
395
397
  localSync: journal?.localSync ?? args.flags["local-sync"] === true,
396
398
  observe: observeCloudOnboarding,
397
399
  });
398
- const failed = report.checks.some((check) => check.required && check.state === "fail") && !onboardRequiredStepsWaiting(journal);
399
- return {
400
- state: report.state === "complete"
401
- ? "done"
402
- : failed
403
- ? "failed"
404
- : "waiting",
405
- reason: report.state === "complete"
406
- ? undefined
407
- : "onboarding_checks_pending",
408
- evidence: {
409
- checks: report.checks.length,
410
- passed: report.checks.filter((check) => check.state === "pass").length,
411
- },
412
- };
400
+ return onboardReadyStep(report, journal);
413
401
  },
414
402
  legacy: deps.legacy,
415
403
  openBrowser: deps.openBrowser,
package/dist/oauth.js CHANGED
@@ -156,7 +156,7 @@ export async function deviceFlowLogin(ctx, baseUrl, deps = {}) {
156
156
  throw new Error("browser-unavailable");
157
157
  await deps.openBrowser(auth.verification_uri_complete ?? auth.verification_uri);
158
158
  browserOpened = true;
159
- ctx.stdout("Opened your browser to that page — approve there, or use the code above.");
159
+ ctx.stdout("Opened your browser to that page. Approve there, or use the code above.");
160
160
  }
161
161
  catch {
162
162
  // a browser that will not open is not a failure; the code and URL still work
@@ -3,7 +3,6 @@ import { readExistingOnboardJson } from "./onboard-existing.js";
3
3
  import { pollConsent } from "./onboard-consent.js";
4
4
  import { verifyOnboardRoutes } from "./onboard-capabilities.js";
5
5
  const listPath = "/api/v1/coding-accounts";
6
- const validatePath = "/api/v1/coding-accounts/:slot/validate";
7
6
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
8
7
  ? value
9
8
  : null;
@@ -94,195 +93,92 @@ async function inventory(ctx, journal, signal) {
94
93
  }
95
94
  return { slots, identity };
96
95
  }
97
- /** Verify one existing Claude slot per run. A stored login, cached poll or Codex shape is not
98
- * fresh provider access. Enrollment and a phase's actual lease eligibility remain separate. */
96
+ /** Why one account cannot take work, or null when it can. The Connect page's AI-account rule
97
+ * (the server's isUsableCodingAccount): any provider, any member's account, and a canceled
98
+ * subscription keeps working until its paid access ends. */
99
+ function unusableCause(row, now) {
100
+ if (row.declaredState !== "active")
101
+ return "coding_account_inactive";
102
+ if (row.revokedAtMs !== null ||
103
+ (row.accessEndsAtMs !== null && row.accessEndsAtMs <= now))
104
+ return "coding_account_ended";
105
+ if (row.needsCredential)
106
+ return "coding_account_needs_login";
107
+ if (row.quarantined)
108
+ return "coding_account_quarantined";
109
+ if (row.walled)
110
+ return "coding_account_walled";
111
+ return null;
112
+ }
113
+ export function usableAiAccount(row, now) {
114
+ return unusableCause(row, now) === null;
115
+ }
116
+ /** CTC-4680: the step is done when the workspace has at least one usable AI account, from any
117
+ * provider. With --coding-account, that named account is the one that must be usable. */
99
118
  export function onboardAccountsAdapter(input = {}) {
100
- let proof = null;
101
- let attempted = false;
102
- const candidate = (slots, now) => slots
119
+ const usable = (slots, now) => slots
103
120
  .filter((row) => (input.slot === undefined || row.accountSlot === input.slot) &&
104
- row.provider === "claude" &&
105
- row.declaredState === "active" &&
106
- row.ownedByMe &&
107
- !row.needsCredential &&
108
- !row.walled &&
109
- !row.quarantined &&
110
- row.renewalStatus !== "canceled" &&
111
- row.revokedAtMs === null &&
112
- (row.accessEndsAtMs === null || row.accessEndsAtMs > now))
121
+ usableAiAccount(row, now))
113
122
  .sort((a, b) => a.accountSlot.localeCompare(b.accountSlot))[0];
114
123
  const check = async (ctx, journal, signal) => {
115
124
  const live = await inventory(ctx, journal, signal);
116
125
  if ("reason" in live)
117
126
  return waiting(live.reason);
118
- const selected = candidate(live.slots, ctx.now().getTime());
119
- if (proof &&
120
- equal(live.identity, proof) &&
121
- live.slots.some((row) => row.accountSlot === proof.slot &&
122
- row.provider === proof.provider &&
123
- row.declaredState === "active" &&
124
- row.ownedByMe &&
125
- !row.needsCredential &&
126
- !row.walled &&
127
- !row.quarantined &&
128
- row.renewalStatus !== "canceled" &&
129
- row.revokedAtMs === null &&
130
- (row.accessEndsAtMs === null ||
131
- row.accessEndsAtMs > ctx.now().getTime())) &&
132
- proof.checkedAtMs <= ctx.now().getTime() + 5000 &&
133
- proof.checkedAtMs >= ctx.now().getTime() - 30000)
127
+ const selected = usable(live.slots, ctx.now().getTime());
128
+ if (selected)
134
129
  return {
135
130
  state: "done",
136
131
  evidence: {
137
- accountSlot: proof.slot,
138
- provider: proof.provider,
139
- checkedAt: new Date(proof.checkedAtMs).toISOString(),
132
+ provider: selected.provider,
133
+ checkedAt: ctx.now().toISOString(),
140
134
  },
141
135
  };
142
- proof = null;
143
- if (!live.slots.length)
144
- return input.waitForAccount &&
145
- ["owner", "admin"].includes(live.identity.role)
146
- ? { state: "pending" }
147
- : waiting("account_enrollment_required");
148
- if (input.slot !== undefined &&
149
- !live.slots.some((row) => row.accountSlot === input.slot))
150
- return waiting("coding_account_not_found");
151
- if (!selected)
152
- return waiting(live.slots.some((row) => row.provider === "codex")
153
- ? "codex_provider_access_unverified"
154
- : "account_provider_access_unverified");
155
- if (attempted)
156
- return waiting("account_provider_access_unverified");
157
- const cfg = loadConfig(ctx.home);
158
- if (!cfg?.user || !["owner", "admin"].includes(cfg.user.role))
159
- return waiting("account_validation_admin_required");
160
- return { state: "pending" };
136
+ if (input.slot !== undefined) {
137
+ const named = live.slots.find((row) => row.accountSlot === input.slot);
138
+ // A named account speaks for itself: other accounts in the workspace may be fine.
139
+ return waiting(named
140
+ ? (unusableCause(named, ctx.now().getTime()) ?? "coding_account_not_found")
141
+ : "coding_account_not_found");
142
+ }
143
+ if (input.waitForAccount &&
144
+ ["owner", "admin"].includes(live.identity.role))
145
+ return { state: "pending" };
146
+ return waiting(live.slots.length ? "ai_account_not_usable" : "account_enrollment_required");
161
147
  };
162
148
  return {
163
149
  check,
164
150
  act: async (ctx, journal, external) => {
165
- if (attempted)
166
- return waiting("account_provider_access_unverified");
167
151
  const initial = await inventory(ctx, journal, external);
168
152
  if ("reason" in initial)
169
153
  return waiting(initial.reason);
170
- let live = initial;
171
- if (!live.slots.length &&
172
- input.waitForAccount &&
173
- ["owner", "admin"].includes(live.identity.role)) {
174
- const result = await input.waitForAccount(() => pollConsent({
175
- timeoutMs: input.accountWaitMs,
176
- signal: external,
177
- sleep: input.sleep,
178
- readStatus: async (signal) => {
179
- const current = await inventory(ctx, journal, signal);
180
- if ("reason" in current)
181
- return { outcome: "waiting", reason: current.reason };
182
- live = current;
183
- return { outcome: current.slots.length ? "connected" : "absent" };
184
- },
185
- }));
186
- if (result.state !== "done")
187
- return waiting(result.reason === "consent_timeout"
188
- ? "account_enrollment_required"
189
- : (result.reason ?? "account_enrollment_required"));
190
- }
191
- const selected = candidate(live.slots, ctx.now().getTime());
192
- if (!selected)
193
- return waiting("account_provider_access_unverified");
194
- const support = await verifyOnboardRoutes(ctx, journal, [{ method: "POST", path: validatePath }], external);
195
- if ("reason" in support)
196
- return waiting(support.reason);
197
- const cfg = loadConfig(ctx.home);
198
- if (!equal(binding(ctx, journal), live.identity) ||
199
- !cfg?.user ||
200
- !["owner", "admin"].includes(cfg.user.role))
201
- return waiting("account_identity_unverified");
202
- const expiry = cfg.auth
203
- ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
204
- : 0;
205
- const bearer = cfg.key ||
206
- (cfg.auth && Number.isFinite(expiry) && expiry > 30000
207
- ? cfg.auth.accessToken
208
- : undefined);
209
- if (!bearer)
210
- return waiting("account_login_refresh_required");
211
- input.message?.("Checking one stored Claude account with a one-token provider request. This may use Claude quota. Codex credentials are not refreshed. Phase eligibility is checked separately.");
212
- // A display callback must not be able to switch the identity used for this request.
213
- if (!equal(binding(ctx, journal), live.identity))
214
- return waiting("account_identity_unverified");
215
- const deadline = new AbortController();
216
- const signal = external
217
- ? AbortSignal.any([external, deadline.signal])
218
- : deadline.signal;
219
- const timer = setTimeout(() => deadline.abort(), 15000);
220
- let remove = () => { };
221
- try {
222
- return await new Promise((resolve) => {
223
- const stopped = () => resolve(waiting(external?.aborted
224
- ? "interrupted"
225
- : "account_validation_unavailable"));
226
- if (signal.aborted) {
227
- stopped();
228
- return;
229
- }
230
- signal.addEventListener("abort", stopped, { once: true });
231
- remove = () => signal.removeEventListener("abort", stopped);
232
- Promise.resolve()
233
- .then(async () => {
234
- if (signal.aborted)
235
- return waiting("interrupted");
236
- // UI callbacks can queue a config change before this deferred send executes.
237
- if (!equal(binding(ctx, journal), live.identity))
238
- return waiting("account_identity_unverified");
239
- attempted = true;
240
- const response = await ctx.fetch(`${support.origin}/api/v1/coding-accounts/${encodeURIComponent(selected.accountSlot)}/validate`, {
241
- method: "POST",
242
- redirect: "error",
243
- signal,
244
- headers: {
245
- authorization: `Bearer ${bearer}`,
246
- accept: "application/json",
247
- },
248
- });
249
- if (response.status !== 200)
250
- return waiting("account_validation_unavailable");
251
- const body = object(await response.json());
252
- if (signal.aborted ||
253
- !equal(binding(ctx, journal), live.identity))
254
- return waiting("account_identity_unverified");
255
- const now = ctx.now().getTime();
256
- if (!body ||
257
- body.provider !== selected.provider ||
258
- typeof body.checkedAtMs !== "number" ||
259
- !Number.isSafeInteger(body.checkedAtMs) ||
260
- body.checkedAtMs > now + 5000 ||
261
- body.checkedAtMs < now - 30000 ||
262
- !["working", "walled", "rejected", "inconclusive"].includes(String(body.result)))
263
- return waiting("account_validation_unverified");
264
- if (body.result !== "working")
265
- return waiting(body.result === "walled"
266
- ? "account_provider_walled"
267
- : body.result === "rejected"
268
- ? "account_provider_rejected"
269
- : "account_provider_access_unverified");
270
- proof = {
271
- ...live.identity,
272
- slot: selected.accountSlot,
273
- provider: selected.provider,
274
- checkedAtMs: body.checkedAtMs,
275
- };
276
- input.message?.("Claude provider access was verified. This does not reserve a runner or prove that a phase can start.");
277
- return check(ctx, journal, signal);
278
- })
279
- .then((result) => (signal.aborted ? stopped() : resolve(result)), () => stopped());
280
- });
281
- }
282
- finally {
283
- clearTimeout(timer);
284
- remove();
285
- }
154
+ if (!input.waitForAccount ||
155
+ !["owner", "admin"].includes(initial.identity.role))
156
+ return check(ctx, journal, external);
157
+ let enrolled = initial.slots.length > 0;
158
+ const result = await input.waitForAccount(() => pollConsent({
159
+ timeoutMs: input.accountWaitMs,
160
+ signal: external,
161
+ sleep: input.sleep,
162
+ readStatus: async (signal) => {
163
+ const current = await inventory(ctx, journal, signal);
164
+ if ("reason" in current)
165
+ return { outcome: "waiting", reason: current.reason };
166
+ enrolled = current.slots.length > 0;
167
+ return {
168
+ outcome: usable(current.slots, ctx.now().getTime())
169
+ ? "connected"
170
+ : "absent",
171
+ };
172
+ },
173
+ }));
174
+ if (result.state === "done")
175
+ return check(ctx, journal, external);
176
+ // A wait that ran out holds the step and offers another try (round 5).
177
+ return waiting(result.reason !== "consent_timeout"
178
+ ? (result.reason ?? "account_enrollment_required")
179
+ : enrolled
180
+ ? "ai_account_not_usable"
181
+ : "account_enrollment_required");
286
182
  },
287
183
  };
288
184
  }