@catalyst-cloud/cli 0.9.2 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.3
4
+
5
+ A machine the Catalyst installer moved from `@catalyst-cloud/catalyst-skills` to `@catalyst-cloud/cli` works without logging in again. The recorded launcher pointed into the removed package, so `ready` failed its CLI-path check and skill scripts could not start the CLI; any command now repoints it at the launcher that is running. The CLI also leaves the installer's skills alone: it never writes through a symlinked skill folder, and when the installer owns skill placement it does not refresh skills at all. When a refresh does fail, it tells you to re-run the install command.
6
+
7
+ Onboarding no longer tells you to re-credential an ended or cancelled coding account: it says to retire it on the AI accounts page, and such an account no longer leaves setup unfinished. Before you mint a replacement token, it has you check that your terminal is logged into the account the slot belongs to. `catalyst accounts` names each account by its label, else its email, with the slot id second, adds `displayName` to `--json`, and marks a cancelled subscription "subscription canceled — retire on the AI accounts page".
8
+
3
9
  ## 0.9.2
4
10
 
5
11
  The CLI works against a tenant serving contract 2.x. Catalyst Cloud's tenant contract moved to 2.0.0 when it removed `merge.cloudRemediateRequiredChecks`, and every earlier bundle accepted only 1.x, so `ready`, onboarding and most commands stopped with "the tenant serves contract version 2.2.0 but this bundle accepts 1.x". This bundle accepts `1.x || 2.x`. The one place that read the removed field already treated it as absent.
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // their own modules; this file keeps every export the 0.1 tests and the bin import.
3
3
  import { existsSync } from "node:fs";
4
4
  import { parseArgs, positionals, verbHelp } from "./args.js";
5
- import { CONFIG_MODE, DEFAULT_BASE_URL, LEGACY_PACKAGE_NAME, PACKAGE_NAME, cliPath, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, formatMode, loadConfig, modernCliPath, normalizeBaseUrl, readManifest, requireConfig, saveConfig, writeConfig, } from "./config.js";
5
+ import { CONFIG_MODE, DEFAULT_BASE_URL, LEGACY_PACKAGE_NAME, PACKAGE_NAME, cliPath, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, formatMode, installerOwnsSkills, INSTALL_COMMAND, loadConfig, modernCliPath, 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
8
  import { fetchMe } from "./transport.js";
@@ -100,8 +100,14 @@ function migrateLegacyCliPath(ctx) {
100
100
  }
101
101
  if (!cfg)
102
102
  return;
103
- const next = modernCliPath(cfg.cliPath);
104
- if (next === null)
103
+ let next = modernCliPath(cfg.cliPath);
104
+ // An upgrade from @catalyst-cloud/catalyst-skills to @catalyst-cloud/cli removes the old package
105
+ // folder, so the recorded launcher no longer exists and has no sibling. Point it at the launcher
106
+ // that is running now, so `ready` and every skill script work without another login.
107
+ if (next === null && typeof cfg.cliPath === "string" && cfg.cliPath !== "" && !existsSync(cfg.cliPath)) {
108
+ next = cliPath();
109
+ }
110
+ if (next === null || next === cfg.cliPath)
105
111
  return;
106
112
  cfg.cliPath = next;
107
113
  try {
@@ -134,13 +140,23 @@ function maybePrintUpdateNotice(args, ctx) {
134
140
  const entry = parseChangelogEntry(readChangelog(), manifest.version);
135
141
  say(updateNoticeLine(previous, manifest.version, entry));
136
142
  }
143
+ if (installerOwnsSkills(ctx.home, ctx.env)) {
144
+ cfg.lastSkillBundleVersion = manifest.version;
145
+ try {
146
+ saveConfig(ctx.home, cfg);
147
+ }
148
+ catch {
149
+ // Best effort: the notice prints again next time, and nothing else depends on it.
150
+ }
151
+ return;
152
+ }
137
153
  const skillsDir = resolveSkillsDir(args, ctx, cfg);
138
154
  let refreshed;
139
155
  try {
140
156
  refreshed = installSkills(skillsDir, { force: false, onlyExisting: true });
141
157
  }
142
158
  catch (err) {
143
- ctx.stderr(`[catalyst-skills] could not refresh the skills at ${skillsDir} (${err instanceof Error ? err.message : String(err)}) — the ${manifest.version} skills are not installed yet; run: catalyst-skills install`);
159
+ ctx.stderr(`[catalyst-skills] could not refresh the skills at ${skillsDir} (${err instanceof Error ? err.message : String(err)}) — the ${manifest.version} skills are not installed yet; re-run the install command: ${INSTALL_COMMAND}`);
144
160
  return;
145
161
  }
146
162
  if (refreshed.installed.length > 0)
package/dist/config.js CHANGED
@@ -91,6 +91,8 @@ export function updatePackageName(root = packageRoot()) {
91
91
  return LEGACY_PACKAGE_NAME;
92
92
  return PACKAGE_NAME;
93
93
  }
94
+ /** The Catalyst installer, which owns skill placement and switches a machine between packages. */
95
+ export const INSTALL_COMMAND = "curl -fsSL https://staging.catalystcloud.dev/install.sh | sh";
94
96
  /** The one upgrade command every hint prints: update the installed package, then log in again so
95
97
  * customer.json records the new launcher path. */
96
98
  export function upgradeCommand(root) {
@@ -115,6 +117,20 @@ export function normalizeBaseUrl(url) {
115
117
  export function apiBase(cfg) {
116
118
  return `${normalizeBaseUrl(cfg.baseUrl)}/api/v1`;
117
119
  }
120
+ /** True when the Catalyst installer owns skill placement on this machine: it exports
121
+ * CATALYST_SKILLS_DIR, or it wrote the machine paths file (every v1 record carries a skills role).
122
+ * The CLI then never refreshes skill folders itself; the installer's own refresh does. */
123
+ export function installerOwnsSkills(home, env = {}) {
124
+ if (env.CATALYST_SKILLS_DIR !== undefined && env.CATALYST_SKILLS_DIR !== "")
125
+ return true;
126
+ try {
127
+ const file = machinePathsFile({ env: { ...env, HOME: home } });
128
+ return Boolean(file && machineFilePresent(file));
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
118
134
  function machineFilePresent(file) {
119
135
  try {
120
136
  lstatSync(file);
package/dist/execution.js CHANGED
@@ -318,12 +318,23 @@ export const ACCOUNT_STATUS = {
318
318
  attested: "attested — healthy at last check, no work observed since",
319
319
  unobserved: "unobserved — enrolled, but nothing has been seen from it yet",
320
320
  };
321
+ /** The name a person or agent calls an account by: its label, else its email, else its slot id. */
322
+ export function accountDisplayName(a) {
323
+ for (const v of [a.label, a.email])
324
+ if (typeof v === "string" && v.trim() !== "")
325
+ return v.trim();
326
+ return a.accountSlot ?? "?";
327
+ }
321
328
  /** One slot as a line a human reads: who it is, what state it is in, and what it is spending on. */
322
329
  export function renderAccount(a) {
323
- const name = a.label ? `${a.accountSlot ?? "?"} (${a.label})` : (a.accountSlot ?? "?");
330
+ const display = accountDisplayName(a);
331
+ const name = display === (a.accountSlot ?? "?") ? display : `${display} (${a.accountSlot ?? "?"})`;
324
332
  const harness = a.harness ? `/${a.harness}` : "";
325
333
  const status = a.status ? (ACCOUNT_STATUS[a.status] ?? a.status) : "status unknown";
326
334
  const bits = [`${name} ${a.provider ?? "?"}${harness} ${status}`];
335
+ // CTC-4199: a cancelled subscription is retired, never re-credentialed; say so in the text view too.
336
+ if (a.renewalStatus === "canceled")
337
+ bits.push("subscription canceled — retire on the AI accounts page");
327
338
  if (typeof a.bindingUsedPercent === "number") {
328
339
  const resets = typeof a.bindingResetsAtMs === "number" ? `, resets ${new Date(a.bindingResetsAtMs).toISOString()}` : "";
329
340
  bits.push(`usage ${a.bindingUsedPercent}% of the ${a.bindingWindow ?? "binding"} window${resets}`);
@@ -356,7 +367,8 @@ export async function cmdAccounts(args, ctx) {
356
367
  throw needsNewerCloud("coding-account status", cfg);
357
368
  const accounts = Array.isArray(res.body) ? res.body : (res.body?.accounts ?? []);
358
369
  if (args.json) {
359
- ctx.stdout(JSON.stringify(res.body));
370
+ const named = accounts.map((a) => ({ ...a, displayName: accountDisplayName(a) }));
371
+ ctx.stdout(JSON.stringify(Array.isArray(res.body) ? named : { ...res.body, accounts: named }));
360
372
  return 0;
361
373
  }
362
374
  if (accounts.length === 0) {
package/dist/skills.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // skills.ts — copying the bundled skills into the user's skills directory, and the one-line update
2
2
  // notice a new version prints on its next session.
3
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { defaultSkillsDirFor, upgradeCommand } from "./config.js";
@@ -31,6 +31,10 @@ export function installSkills(targetDir, opts, sourceDir = skillsSourceDir()) {
31
31
  if (!existsSync(join(src, "SKILL.md")))
32
32
  continue;
33
33
  const dst = join(targetDir, name);
34
+ // A symlinked entry belongs to whoever linked it (the Catalyst installer links its skills in);
35
+ // this package never writes through one, and says nothing about it.
36
+ if (isSymlink(dst))
37
+ continue;
34
38
  const existingMd = join(dst, "SKILL.md");
35
39
  if (opts.onlyExisting && !existsSync(existingMd))
36
40
  continue;
@@ -47,6 +51,14 @@ export function installSkills(targetDir, opts, sourceDir = skillsSourceDir()) {
47
51
  }
48
52
  return result;
49
53
  }
54
+ function isSymlink(path) {
55
+ try {
56
+ return lstatSync(path).isSymbolicLink();
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
50
62
  export function resolveSkillsDir(args, ctx, cfg) {
51
63
  return args.skillsDir ?? ctx.env.CATALYST_SKILLS_CLAUDE_DIR ?? cfg?.skillsDir ?? defaultSkillsDirFor(ctx.home);
52
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat — get set up, see what is happening, see what needs you, run a project, unstick what is stuck — plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your tenant with one command. The skills directory is the roster; this line never counts it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
8
8
 
9
9
  # Catalyst's GitHub
10
10
 
@@ -5,7 +5,7 @@ description: >-
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  disable-model-invocation: true
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
9
9
 
10
10
  # Catalyst Linear
11
11
 
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
9
9
 
10
10
  # Onboard me
11
11
 
@@ -34,7 +34,11 @@ Say these two things plainly:
34
34
 
35
35
  ## Claude
36
36
 
37
- The same page takes a Claude account's new credential. Its field is "Replacement setup token". The person runs `claude setup-token` in their terminal, pastes the token it prints into that field, and presses Replace credential. Then re-run the script.
37
+ The same page takes a Claude account's new credential. Its field is "Replacement setup token". `claude setup-token` mints a token for whichever Claude account the terminal is logged into, not for the slot. So first match them: read the slot's account on the AI accounts page (its label or email), then have the person run `claude` and `/status` to see the email the terminal is logged into. If they differ, stop, and log in to the slot's account first. Only then run `claude setup-token`, paste the token into that field, and press Replace credential. Then re-run the script.
38
+
39
+ ## An ended or cancelled account
40
+
41
+ An account whose subscription is cancelled, or which has ended, is not a credential problem, and no token revives it. Retire it on the AI accounts page. Never replace its credential with a token from another account: that puts a working account's login onto a dead slot.
38
42
 
39
43
  ## Any other provider
40
44
 
@@ -288,7 +288,11 @@ let accountsNext = "enrol a coding account a phase can run on";
288
288
  // credential itself. The poll fields are read only when the cloud sends them.
289
289
  const CREDENTIAL_ERROR_CODES = new Set(["no_access_token", "no_credential", "http_401", "http_403", "usage_unauthorized", "usage_forbidden"]);
290
290
  const CREDENTIAL_FAILURE_STREAK = 3;
291
+ // A cancelled subscription or an ended account is not a credential problem: no token brings it
292
+ // back. It is retired, never re-credentialed, and it does not make setup unfinished.
293
+ const isRetirable = (a) => a?.renewalStatus === "canceled" || a?.status === "ended" || a?.declaredState === "ended";
291
294
  const credentialProblem = (a) => {
295
+ if (isRetirable(a)) return null;
292
296
  if (a?.quarantined === true) return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
293
297
  if (a?.status === "expired-or-revoked") return "expired or revoked";
294
298
  if (CREDENTIAL_ERROR_CODES.has(a?.lastPollErrorCode) && typeof a?.pollFailureCount === "number" && a.pollFailureCount >= CREDENTIAL_FAILURE_STREAK) {
@@ -318,18 +322,21 @@ if (!connected) {
318
322
  const head = [line, `${ca.activeCount ?? "?"} active`];
319
323
  const { rows, error } = readAccountRows();
320
324
  const dead = rows === null ? [] : rows.filter((a) => credentialProblem(a) !== null);
325
+ const retirable = rows === null ? [] : rows.filter(isRetirable);
326
+ const who = (a) => [a.label, a.email].find((v) => typeof v === "string" && v !== "");
327
+ const retireLine = retirable.length === 0 ? [] : [`${retirable.length} account(s) ended or cancelled (${retirable.map((a) => `${a.provider ?? "unknown provider"} ${a.accountSlot ?? "?"}`).join(", ")}): retire them on the AI accounts page. Do not replace their credential; no token revives a cancelled subscription.`];
321
328
  if (rows === null) {
322
329
  accountsNext = "read the coding accounts again; each account's credential could not be checked";
323
330
  add("coding accounts", DETAIL, "unreadable", [...head, `coding accounts could not be checked in detail (${error}). Whether each one still has a working credential is unknown. Do not enroll one on this reading.`], null, null);
324
331
  } else if (dead.length > 0) {
325
- const name = (a) => `${a.provider ?? "unknown provider"} account ${a.accountSlot ?? "?"}`;
332
+ const name = (a) => `${a.provider ?? "unknown provider"} account ${a.accountSlot ?? "?"}${who(a) ? ` (${who(a)})` : ""}`;
326
333
  const more = dead.length > 1 ? ` (and ${dead.length - 1} more)` : "";
327
334
  accountsNext = `${name(dead[0])} needs a new credential${more}. Replace it on the AI accounts page (Settings → AI accounts → the account → Replace credential). Do not enroll another account.`;
328
335
  add(
329
336
  "coding accounts",
330
337
  DETAIL,
331
338
  "unfinished",
332
- [...head, ...dead.map((a) => `${name(a)} needs a new credential: ${credentialProblem(a)}`), "Replace its credential on its own page. Do not enroll another account. The steps are in references/replacing-a-credential.md."],
339
+ [...head, ...dead.map((a) => `${name(a)} needs a new credential: ${credentialProblem(a)}`), "Replace its credential on its own page, with a credential minted from THAT account: check the login you mint from matches the account named here first. Do not enroll another account. The steps are in references/replacing-a-credential.md.", ...retireLine],
333
340
  owner,
334
341
  where,
335
342
  );
@@ -337,7 +344,7 @@ if (!connected) {
337
344
  accountsNext = "replace the credential of the coding account the page marks; do not enroll another account";
338
345
  add("coding accounts", DETAIL, "unfinished", [...head, "The contract says an account needs a new credential, and the account list does not say which. Open the page and look for it."], owner, where);
339
346
  } else {
340
- add("coding accounts", DETAIL, "ok", [...head, `${rows.length} checked, none needs a new credential`]);
347
+ add("coding accounts", DETAIL, "ok", [...head, `${rows.length} checked, none needs a new credential`, ...retireLine]);
341
348
  }
342
349
  } else if (ca.state === "inactive") {
343
350
  accountsNext = "reactivate a coding account that is out of rotation; do not enroll another one";
@@ -4,7 +4,7 @@ description: >-
4
4
  Am I set up? Machine readiness (Node, the tenant connection, the cached contract, the CLI path, the skills, the SDK, the optional replica) plus tenant readiness from the contract's per-team checks, in one verdict: what passes, what is blocked, what is merely waiting, and who can click what. Use when someone asks "am I set up", "what is missing", "why does nothing happen", "is the replica running", or right after connecting a new machine. Reports; never repairs.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
8
8
 
9
9
  # Am I set up?
10
10
 
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
9
9
 
10
10
  # Connect me
11
11
 
@@ -4,7 +4,7 @@ description: >-
4
4
  How Catalyst Cloud runs a ticket on the customer's own tenant, as facts an agent loads on demand: the eight-phase ladder and what each phase produces, the eleven board slots and this team's live stage map, what happens when a phase fails (retry, backoff, Remediate, park), how the queue is ordered and routed and every reason a ticket is excluded, and the coding-account model. Use when a person asks "how does this work?", "why did it do that?", "why is this stuck?", "what runs next?" or "how does it prioritise?". Read-only; its scripts explain one ticket's eligibility in plain English, show what is running and queued, and print the tenant's stage map and thresholds straight from the contract.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
8
8
 
9
9
  # How Catalyst works
10
10
 
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
9
9
 
10
10
  # Run this project
11
11
 
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
9
9
 
10
10
  # Unstick
11
11
 
@@ -5,7 +5,7 @@ description: >-
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
7
7
  ---
8
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
8
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
9
9
 
10
10
  # What needs me
11
11
 
@@ -4,7 +4,7 @@ description: >-
4
4
  The desk for a Catalyst Cloud tenant. Use when the person asks "what's happening?", "where are we?", "why is that stuck?", "what closed?", "what's next?", or asks for something to be done rather than known. Reads the tenant contract, what is running and queued, the eligibility explainer and the open asks through the catalyst-skills CLI, and answers in one reply with ticket ids. Routes work to a project owner and decisions to what-needs-me. Never composes a URL, never polls, never answers as the human.
5
5
  allowed-tools: Bash(catalyst-skills:*) Bash(npx @catalyst-cloud/catalyst-skills:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.2 — written in this repository for customer tenants -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.9.3 — written in this repository for customer tenants -->
8
8
 
9
9
  # What's happening
10
10