@catalyst-cloud/cli 0.13.0 → 0.14.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +27 -30
  3. package/dist/args.js +299 -61
  4. package/dist/ask.js +30 -18
  5. package/dist/capabilities.js +7 -0
  6. package/dist/cli.js +125 -20
  7. package/dist/config.js +3 -1
  8. package/dist/env/draft.js +134 -0
  9. package/dist/env/legacy.js +54 -0
  10. package/dist/env/parse-env.js +50 -0
  11. package/dist/env/settings-toml.js +66 -0
  12. package/dist/env.js +126 -26
  13. package/dist/environment.js +1 -1
  14. package/dist/legacy.js +224 -0
  15. package/dist/onboard-consent.js +65 -0
  16. package/dist/onboard.js +836 -0
  17. package/dist/project.js +191 -0
  18. package/dist/ready.js +11 -1
  19. package/dist/release.js +1 -1
  20. package/dist/repo.js +297 -0
  21. package/dist/runtime-verb.js +1 -1
  22. package/dist/secret.js +25 -4
  23. package/dist/skill-shape.js +21 -13
  24. package/dist/skills.js +44 -4
  25. package/dist/transport.js +26 -1
  26. package/dist/var.js +118 -0
  27. package/package.json +6 -4
  28. package/skills/catalyst-github/SKILL.md +7 -7
  29. package/skills/catalyst-github/references/is-it-mergeable.md +8 -2
  30. package/skills/catalyst-github/references/what-a-pr-accumulates.md +3 -3
  31. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +3 -3
  32. package/skills/catalyst-github/scripts/lib/cli.mjs +9 -7
  33. package/skills/catalyst-github/scripts/lib/credential.mjs +2 -2
  34. package/skills/catalyst-github/scripts/read-pr.mjs +2 -2
  35. package/skills/catalyst-linear/SKILL.md +17 -19
  36. package/skills/catalyst-linear/agents/openai.yaml +1 -1
  37. package/skills/catalyst-linear/agents/portability.yaml +1 -1
  38. package/skills/catalyst-linear/references/reading-a-ticket.md +9 -26
  39. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +2 -2
  40. package/skills/catalyst-linear/references/writing-to-linear.md +7 -9
  41. package/skills/catalyst-linear/scripts/comment.mjs +1 -1
  42. package/skills/catalyst-linear/scripts/create-ticket.mjs +1 -1
  43. package/skills/catalyst-linear/scripts/label.mjs +1 -1
  44. package/skills/catalyst-linear/scripts/lib/cli.mjs +9 -7
  45. package/skills/catalyst-linear/scripts/lib/credential.mjs +2 -2
  46. package/skills/catalyst-linear/scripts/move.mjs +1 -1
  47. package/skills/catalyst-linear/scripts/read-ticket.mjs +1 -1
  48. package/skills/catalyst-linear/scripts/search.mjs +1 -1
  49. package/skills/catalyst-onboard/SKILL.md +37 -57
  50. package/skills/catalyst-onboard/agents/openai.yaml +2 -2
  51. package/skills/catalyst-onboard/agents/portability.yaml +1 -1
  52. package/skills/catalyst-onboard/references/connecting-this-machine.md +35 -0
  53. package/skills/catalyst-onboard/references/declaring-a-repository.md +18 -21
  54. package/skills/catalyst-onboard/references/local-sync.md +19 -7
  55. package/skills/catalyst-onboard/references/reading-ready.md +79 -0
  56. package/skills/catalyst-onboard/references/replacing-a-credential.md +8 -33
  57. package/skills/catalyst-onboard/references/repository-agent-setup.md +23 -0
  58. package/skills/catalyst-onboard/references/skill-sources.md +13 -19
  59. package/skills/catalyst-onboard/references/the-one-path.md +18 -74
  60. package/skills/catalyst-onboard/references/what-a-phase-needs.md +21 -28
  61. package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -37
  62. package/skills/{catalyst-setup → catalyst-onboard}/scripts/check.mjs +10 -9
  63. package/skills/catalyst-onboard/scripts/lib/cli.mjs +6 -4
  64. package/skills/catalyst-onboard/scripts/lib/credential.mjs +2 -2
  65. package/skills/catalyst-onboard/scripts/lib/project-coverage.mjs +53 -0
  66. package/skills/catalyst-onboard/scripts/where-am-i.mjs +929 -184
  67. package/skills/run-this-project/SKILL.md +23 -25
  68. package/skills/run-this-project/agents/openai.yaml +1 -1
  69. package/skills/run-this-project/agents/portability.yaml +1 -1
  70. package/skills/run-this-project/references/making-work-ready.md +20 -37
  71. package/skills/run-this-project/references/reacting-to-events.md +25 -48
  72. package/skills/run-this-project/references/stalls-and-escalation.md +20 -42
  73. package/skills/run-this-project/scripts/lib/cli.mjs +7 -5
  74. package/skills/run-this-project/scripts/lib/credential.mjs +2 -2
  75. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  76. package/skills/run-this-project/scripts/watch-scope.mjs +1 -1
  77. package/skills/unstick/SKILL.md +12 -12
  78. package/skills/unstick/agents/openai.yaml +1 -1
  79. package/skills/unstick/agents/portability.yaml +1 -1
  80. package/skills/unstick/references/playbook.md +13 -13
  81. package/skills/unstick/scripts/lib/cli.mjs +6 -4
  82. package/skills/unstick/scripts/lib/credential.mjs +2 -2
  83. package/skills/unstick/scripts/unstick.mjs +1 -1
  84. package/skills/what-needs-me/SKILL.md +12 -14
  85. package/skills/what-needs-me/agents/openai.yaml +2 -2
  86. package/skills/what-needs-me/agents/portability.yaml +1 -1
  87. package/skills/what-needs-me/references/raising-a-decision.md +8 -16
  88. package/skills/what-needs-me/references/reading-the-inbox.md +6 -13
  89. package/skills/what-needs-me/references/settling-an-answer.md +6 -12
  90. package/skills/what-needs-me/scripts/inbox.mjs +14 -13
  91. package/skills/what-needs-me/scripts/lib/cli.mjs +6 -4
  92. package/skills/what-needs-me/scripts/lib/credential.mjs +2 -2
  93. package/skills/what-needs-me/scripts/raise.mjs +1 -1
  94. package/skills/what-needs-me/scripts/settle.mjs +1 -1
  95. package/skills/what-this-repo-needs/SKILL.md +10 -19
  96. package/skills/what-this-repo-needs/references/what-each-group-means.md +14 -44
  97. package/skills/what-this-repo-needs/scripts/check.mjs +13 -14
  98. package/skills/what-this-repo-needs/scripts/inventory.mjs +2 -2
  99. package/skills/what-this-repo-needs/scripts/lib/cli.mjs +11 -9
  100. package/skills/what-this-repo-needs/scripts/lib/credential.mjs +2 -2
  101. package/skills/whats-happening/SKILL.md +20 -30
  102. package/skills/whats-happening/agents/openai.yaml +1 -1
  103. package/skills/whats-happening/references/coding-accounts.md +11 -0
  104. package/skills/whats-happening/references/routing-work.md +4 -33
  105. package/skills/whats-happening/references/settings-and-where-they-live.md +17 -0
  106. package/skills/whats-happening/references/stages-and-mapping.md +19 -0
  107. package/skills/whats-happening/references/status-reply.md +11 -31
  108. package/skills/whats-happening/references/the-ladder.md +20 -0
  109. package/skills/whats-happening/references/what-runs-next.md +13 -0
  110. package/skills/whats-happening/references/when-a-phase-fails.md +21 -0
  111. package/skills/whats-happening/references/why-is-it-stuck.md +38 -62
  112. package/skills/whats-happening/scripts/explain.mjs +1 -1
  113. package/skills/whats-happening/scripts/lib/cli.mjs +7 -5
  114. package/skills/whats-happening/scripts/lib/credential.mjs +2 -2
  115. package/skills/{how-catalyst-works → whats-happening}/scripts/show-my-map.mjs +18 -32
  116. package/skills/whats-happening/scripts/snapshot.mjs +10 -6
  117. package/dist/env/declaration-rules.js +0 -163
  118. package/skills/catalyst-onboard/references/choosing-a-coding-account.md +0 -52
  119. package/skills/catalyst-onboard/references/who-fixes-what.md +0 -64
  120. package/skills/catalyst-setup/SKILL.md +0 -36
  121. package/skills/catalyst-setup/agents/openai.yaml +0 -6
  122. package/skills/catalyst-setup/agents/portability.yaml +0 -4
  123. package/skills/catalyst-setup/references/what-each-check-means.md +0 -88
  124. package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
  125. package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
  126. package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
  127. package/skills/connect-me/SKILL.md +0 -63
  128. package/skills/connect-me/agents/openai.yaml +0 -6
  129. package/skills/connect-me/agents/portability.yaml +0 -5
  130. package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
  131. package/skills/connect-me/scripts/lib/cli.mjs +0 -185
  132. package/skills/connect-me/scripts/lib/credential.mjs +0 -29
  133. package/skills/connect-me/scripts/verify-connection.mjs +0 -68
  134. package/skills/how-catalyst-works/SKILL.md +0 -45
  135. package/skills/how-catalyst-works/agents/openai.yaml +0 -6
  136. package/skills/how-catalyst-works/agents/portability.yaml +0 -4
  137. package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
  138. package/skills/how-catalyst-works/references/settings-and-where-they-live.md +0 -20
  139. package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -56
  140. package/skills/how-catalyst-works/references/the-ladder.md +0 -41
  141. package/skills/how-catalyst-works/references/what-catalyst-is.md +0 -30
  142. package/skills/how-catalyst-works/references/what-runs-next.md +0 -95
  143. package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
  144. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
  145. package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
  146. package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
  147. package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
  148. package/skills/whats-happening/references/reading-the-board.md +0 -43
  149. package/skills/whats-happening/references/reprioritising.md +0 -37
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- // cli.ts — the `catalyst-skills` dispatcher (npm: @catalyst-cloud/catalyst-skills). Verbs live in
1
+ // cli.ts — the `catalyst` dispatcher (npm: @catalyst-cloud/cli). Verbs live in
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";
@@ -8,14 +8,14 @@ import { CliError, MeError, UsageError } from "./errors.js";
8
8
  import { fetchMe } from "./transport.js";
9
9
  import { bearerFor, deviceFlowLogin } from "./oauth.js";
10
10
  import { openBrowser as defaultOpenBrowser } from "./browser.js";
11
- import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning } from "./execution.js";
11
+ import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning, } from "./execution.js";
12
12
  import { cmdQuery } from "./query.js";
13
13
  import { cmdReady } from "./ready.js";
14
14
  import { cmdReplica } from "./replica.js";
15
15
  import { cmdRuntime } from "./runtime-verb.js";
16
16
  import { cmdEvents } from "./events.js";
17
17
  import { stdinIsTty } from "./prompt.js";
18
- import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion } from "./skill-shape.js";
18
+ import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion, } from "./skill-shape.js";
19
19
  import { installedBundleVersion, installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine, } from "./skills.js";
20
20
  import { cmdWatch } from "./watch.js";
21
21
  import { cmdWrite } from "./write.js";
@@ -25,18 +25,20 @@ import { cmdRelease } from "./release.js";
25
25
  import { cmdEnvironment } from "./environment.js";
26
26
  import { cmdEnv } from "./env.js";
27
27
  import { cmdSecret } from "./secret.js";
28
+ import { cmdVar } from "./var.js";
28
29
  import { cmdTeam } from "./team.js";
29
30
  import { cmdCapabilities } from "./capabilities.js";
31
+ import { cmdProject } from "./project.js";
32
+ import { cmdRepo } from "./repo.js";
33
+ import { cmdLegacy, findLegacy } from "./legacy.js";
30
34
  import { cmdIdentity } from "./identity.js";
31
35
  import { cmdConnections } from "./connections.js";
36
+ import { cmdOnboard } from "./onboard.js";
32
37
  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, };
33
38
  export const CUSTOMER_SKILLS = [
34
39
  "catalyst-github",
35
40
  "catalyst-linear",
36
41
  "catalyst-onboard",
37
- "catalyst-setup",
38
- "connect-me",
39
- "how-catalyst-works",
40
42
  "run-this-project",
41
43
  "unstick",
42
44
  "what-needs-me",
@@ -57,11 +59,12 @@ export function usageText() {
57
59
  " catalyst login --key <personal-key> [--base-url <url>] (or CATALYST_CLOUD_TOKEN, for a key)",
58
60
  " catalyst join ... (deprecated alias of login; removed in the next minor version)",
59
61
  " catalyst install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
62
+ " catalyst onboard [--resume-from <step>] [--only <step>] [--yes] [--dry-run] [--json]",
60
63
  " catalyst status | notice | me | ready | accounts",
61
64
  " catalyst mcp add|list|remove (vault references only)",
62
65
  " catalyst contract [--refresh] [--path <a.b.c>]",
63
66
  " catalyst query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
64
- " catalyst replica <start [--detach]|stop|status [--probe]|sql \"<select>\"|schema [table]>",
67
+ ' catalyst replica <start [--detach]|stop|status [--probe]|sql "<select>"|schema [table]>',
65
68
  " catalyst runtime <status [--json]|install|path|uninstall> (a pinned Node this CLI manages itself)",
66
69
  " catalyst events <tail|wait-for|query|status [--probe]> [--type NAME] [--ticket CTC-N] [--after SEQUENCE]",
67
70
  " catalyst explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
@@ -70,10 +73,15 @@ export function usageText() {
70
73
  " catalyst ask <raise|accept|list> ...",
71
74
  " catalyst release <ticket> --because <what changed> [--retry-unchanged] [--dry-run] | release --class <c> --team <K> ...",
72
75
  " catalyst environment [read] | environment propose --file <path>|--stdin [--approve] | environment approve (your account; needs login)",
73
- " catalyst env <inventory [path] | check <file>> (this repository, offline — no login, no network)",
74
- " catalyst secret set <NAME> --repo <owner/name> [--command '<cmd>'] | secret import <file> --repo <owner/name>",
76
+ " catalyst env <inventory [path] | check <file> | migrate [catalyst.env.json] | draft [--root DIR] [--diff] [--write]> [--json] (this repository, offline — no login, no network)",
77
+ " catalyst secret set <NAME> --repo <owner/name> [--command '<cmd>'] | secret import <file> --repo <owner/name> [--names NAME[,NAME...]]",
78
+ " catalyst var set <NAME> | var import <file> [--repo <owner/name>] [--names NAME[,NAME...]] (plain environment variables)",
75
79
  " catalyst team <list|check|map|adopt|migrate|checklist> ...",
76
80
  " catalyst capabilities [--json] (what this CLI can do, the role each verb needs, and whether this cloud serves it)",
81
+ " catalyst project list [--json] (all tenant projects, including those with no stage mapping)",
82
+ " catalyst project wip-limit <get|set <n>|set default> [--team K] (a project's new-start WIP limit; members read, owners and admins set)",
83
+ " catalyst repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> (a checkout's AGENTS.md block and portable agent layout; working tree only)",
84
+ " catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
77
85
  " catalyst identity linear <status|options|set> [<linearUserId>] [--json]",
78
86
  " catalyst connections personal <linear|github> <start|status> [--wait <seconds>] [--json]",
79
87
  "",
@@ -111,7 +119,10 @@ function migrateLegacyCliPath(ctx) {
111
119
  // An upgrade from @catalyst-cloud/catalyst-skills to @catalyst-cloud/cli removes the old package
112
120
  // folder, so the recorded launcher no longer exists and has no sibling. Point it at the launcher
113
121
  // that is running now, so `ready` and every skill script work without another login.
114
- if (next === null && typeof cfg.cliPath === "string" && cfg.cliPath !== "" && !existsSync(cfg.cliPath)) {
122
+ if (next === null &&
123
+ typeof cfg.cliPath === "string" &&
124
+ cfg.cliPath !== "" &&
125
+ !existsSync(cfg.cliPath)) {
115
126
  next = cliPath();
116
127
  }
117
128
  if (next === null || next === cfg.cliPath)
@@ -168,6 +179,8 @@ function maybePrintUpdateNotice(args, ctx) {
168
179
  }
169
180
  if (refreshed.installed.length > 0)
170
181
  say(`[catalyst] refreshed ${refreshed.installed.join(", ")} at ${skillsDir} to ${manifest.version}`);
182
+ if (refreshed.removed.length > 0)
183
+ say(`[catalyst] removed retired skills ${refreshed.removed.join(", ")} from ${skillsDir}; their work moved into the skills this bundle ships`);
171
184
  for (const s of refreshed.skipped) {
172
185
  say(`[catalyst] left "${s.name}" alone: ${skillsDir}/${s.name} was not installed by this package (catalyst install --force to replace)`);
173
186
  }
@@ -208,6 +221,36 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
208
221
  return await cmdLogin(args, ctx, deps);
209
222
  case "install":
210
223
  return cmdInstall(args, ctx);
224
+ case "onboard":
225
+ return await cmdOnboard(args, ctx, {
226
+ runStep: async (id, stepCtx) => {
227
+ if (id !== "legacy")
228
+ return { state: "skipped" };
229
+ const before = findLegacy(stepCtx.home, process.platform).filter((item) => !item.data).length;
230
+ const safeCtx = args.json
231
+ ? { ...stepCtx, stdout: stepCtx.stderr }
232
+ : stepCtx;
233
+ const legacyArgs = {
234
+ ...args,
235
+ command: "legacy",
236
+ subcommand: null,
237
+ rest: [],
238
+ flags: { remove: true, yes: true },
239
+ json: false,
240
+ help: false,
241
+ version: false,
242
+ };
243
+ const code = await cmdLegacy(legacyArgs, safeCtx, deps.legacy ?? {});
244
+ if (code !== 0)
245
+ throw new CliError("legacy cleanup did not finish", "onboard-legacy-failed", 10);
246
+ const after = findLegacy(stepCtx.home, process.platform).filter((item) => !item.data).length;
247
+ return {
248
+ state: after === 0 ? "done" : "skipped",
249
+ evidence: { found: before, remaining: after },
250
+ };
251
+ },
252
+ isTty: deps.isTty ?? stdinIsTty,
253
+ }, readManifest().version);
211
254
  case "notice":
212
255
  return 0;
213
256
  case "status":
@@ -217,7 +260,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
217
260
  case "contract":
218
261
  return await cmdContract(args, ctx);
219
262
  case "query":
220
- return await cmdQuery(args, ctx, { engineDeps: deps.replica?.engineDeps });
263
+ return await cmdQuery(args, ctx, {
264
+ engineDeps: deps.replica?.engineDeps,
265
+ });
221
266
  case "replica":
222
267
  return await cmdReplica(args, ctx, deps.replica);
223
268
  case "runtime":
@@ -239,7 +284,11 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
239
284
  case "ask":
240
285
  return await cmdAsk(args, ctx);
241
286
  case "ready":
242
- return await cmdReady(args, ctx, { skillNames: CUSTOMER_SKILLS, loadSdk: deps.loadSdk, offline: args.flags.offline === true });
287
+ return await cmdReady(args, ctx, {
288
+ skillNames: CUSTOMER_SKILLS,
289
+ loadSdk: deps.loadSdk,
290
+ offline: args.flags.offline === true,
291
+ });
243
292
  case "mcp":
244
293
  return await cmdMcp(args, ctx);
245
294
  case "accounts":
@@ -252,10 +301,18 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
252
301
  return await cmdEnv(args, ctx, deps.env ?? {});
253
302
  case "secret":
254
303
  return await cmdSecret(args, ctx, deps.secret ?? {});
304
+ case "var":
305
+ return await cmdVar(args, ctx, deps.var ?? {});
255
306
  case "team":
256
307
  return await cmdTeam(args, ctx, deps.team);
257
308
  case "capabilities":
258
309
  return await cmdCapabilities(args, ctx);
310
+ case "project":
311
+ return await cmdProject(args, ctx);
312
+ case "repo":
313
+ return await cmdRepo(args, ctx);
314
+ case "legacy":
315
+ return await cmdLegacy(args, ctx, deps.legacy ?? {});
259
316
  case "identity":
260
317
  return await cmdIdentity(args, ctx, deps.identity ?? {});
261
318
  case "connections":
@@ -269,7 +326,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
269
326
  catch (err) {
270
327
  if (err instanceof UsageError) {
271
328
  ctx.stderr(err.message);
272
- ctx.stderr(args.command && args.command in VERB_HELP_KNOWN ? verbHelp(args.command) : usageText());
329
+ ctx.stderr(args.command && args.command in VERB_HELP_KNOWN
330
+ ? verbHelp(args.command)
331
+ : usageText());
273
332
  return 1;
274
333
  }
275
334
  if (err instanceof CliError) {
@@ -283,7 +342,40 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
283
342
  throw err;
284
343
  }
285
344
  }
286
- const VERB_HELP_KNOWN = Object.fromEntries(["login", "join", "install", "status", "notice", "me", "contract", "query", "replica", "runtime", "events", "explain", "running", "queue", "watch", "write", "ask", "ready", "accounts", "release", "secret", "env", "environment", "connections", "identity", "mcp", "team", "capabilities"].map((v) => [v, true]));
345
+ const VERB_HELP_KNOWN = Object.fromEntries([
346
+ "login",
347
+ "join",
348
+ "install",
349
+ "status",
350
+ "notice",
351
+ "me",
352
+ "contract",
353
+ "query",
354
+ "replica",
355
+ "runtime",
356
+ "events",
357
+ "explain",
358
+ "running",
359
+ "queue",
360
+ "watch",
361
+ "write",
362
+ "ask",
363
+ "ready",
364
+ "accounts",
365
+ "release",
366
+ "secret",
367
+ "env",
368
+ "environment",
369
+ "connections",
370
+ "identity",
371
+ "mcp",
372
+ "team",
373
+ "capabilities",
374
+ "project",
375
+ "repo",
376
+ "legacy",
377
+ "onboard",
378
+ ].map((v) => [v, true]));
287
379
  async function cmdLogin(args, ctx, deps) {
288
380
  const manifest = readManifest();
289
381
  const key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
@@ -327,19 +419,24 @@ async function cmdLogin(args, ctx, deps) {
327
419
  const written = writeConfig(ctx.home, config);
328
420
  ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
329
421
  if (me.user) {
330
- ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId ? "" : " — your Linear identity is not matched yet, so \"what needs me\" will show everyone's asks until an admin matches it in Settings"}`);
422
+ ctx.stdout(`Connected as ${me.user.label} (${me.user.role})${me.user.linearUserId
423
+ ? ""
424
+ : ' — your Linear identity is not matched yet, so "what needs me" will show everyone\'s asks until an admin matches it in Settings'}`);
331
425
  }
332
426
  else {
333
427
  ctx.stderr(`[catalyst] 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.`);
334
428
  }
335
- 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`}`);
429
+ 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
430
+ ? ""
431
+ : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
336
432
  ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
337
433
  try {
338
434
  const loaded = await loadContract(ctx, config, { refresh: true });
339
435
  ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
340
436
  }
341
437
  catch (err) {
342
- if (err instanceof CliError && (err.code === "contract-forbidden" || err.code === "contract-version")) {
438
+ if (err instanceof CliError &&
439
+ (err.code === "contract-forbidden" || err.code === "contract-version")) {
343
440
  ctx.stderr(`[catalyst] ${err.message}`);
344
441
  }
345
442
  else {
@@ -376,6 +473,8 @@ function cmdInstall(args, ctx) {
376
473
  for (const s of result.skipped) {
377
474
  ctx.stdout(`Skipped "${s.name}": ${skillsDir}/${s.name} exists and was not installed by this package (use --force to replace)`);
378
475
  }
476
+ if (result.removed.length > 0)
477
+ ctx.stdout(`Removed retired skills from ${skillsDir}: ${result.removed.join(", ")}`);
379
478
  return 0;
380
479
  }
381
480
  function cmdStatus(ctx) {
@@ -387,8 +486,12 @@ function cmdStatus(ctx) {
387
486
  }
388
487
  ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
389
488
  ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
390
- ctx.stdout(cfg.user ? `As: ${cfg.user.label} (${cfg.user.role})` : "As: the tenant's account key (a host credential — no person)");
391
- ctx.stdout(cfg.auth ? `Credential: your login (expires ${relativeExpiry(cfg.auth.expiresAt, ctx.now())})` : "Credential: personal key");
489
+ ctx.stdout(cfg.user
490
+ ? `As: ${cfg.user.label} (${cfg.user.role})`
491
+ : "As: the tenant's account key (a host credential — no person)");
492
+ ctx.stdout(cfg.auth
493
+ ? `Credential: your login (expires ${relativeExpiry(cfg.auth.expiresAt, ctx.now())})`
494
+ : "Credential: personal key");
392
495
  ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
393
496
  if (cfg.cliPath)
394
497
  ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
@@ -433,6 +536,8 @@ async function cmdContract(args, ctx) {
433
536
  const value = path ? pickPath(loaded.doc, path) : loaded.doc;
434
537
  if (path && value === undefined)
435
538
  throw new CliError(`the contract has nothing at "${path}"`, "contract-path");
436
- ctx.stdout(typeof value === "string" ? value : JSON.stringify(value, null, args.json ? 0 : 2));
539
+ ctx.stdout(typeof value === "string"
540
+ ? value
541
+ : JSON.stringify(value, null, args.json ? 0 : 2));
437
542
  return 0;
438
543
  }
package/dist/config.js CHANGED
@@ -111,7 +111,9 @@ export function modernCliPath(recorded) {
111
111
  return existsSync(sibling) ? sibling : null;
112
112
  }
113
113
  export function normalizeBaseUrl(url) {
114
- return url.replace(/\/+$/, "");
114
+ while (url.endsWith("/"))
115
+ url = url.slice(0, -1);
116
+ return url;
115
117
  }
116
118
  /** The origin WITH the versioned prefix — what the SDK and every API read/write take. */
117
119
  export function apiBase(cfg) {
@@ -0,0 +1,134 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { convertLegacyEnvironmentJson } from "./legacy.js";
4
+ import { inventoryRepo } from "./inventory.js";
5
+ import { walkRepo } from "./walk.js";
6
+ const SECRET_NAME_RE = /(?:^|_)(?:API_?KEY|AUTH|CREDENTIALS?|PASSWORD|PRIVATE_?KEY|SECRET|TOKEN)(?:_|$)/i;
7
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
8
+ const MAX_VARIABLES = 128;
9
+ /** Build a stable, value-free environment table from repository evidence. `.env` is never read:
10
+ * inventoryRepo's walker only opens supported examples and source/config files. The legacy
11
+ * declaration is parsed for names and explicit secret flags, and no legacy value escapes. */
12
+ export function draftEnvironment(root, deps = {}) {
13
+ const inventory = inventoryRepo(root, deps);
14
+ const files = (deps.listFiles ?? walkRepo)(root);
15
+ const names = new Set();
16
+ const secrets = new Set();
17
+ const sources = new Set();
18
+ for (const entry of inventory.entries) {
19
+ if (entry.group === "bindings" || !entry.needsDeclaration || !ENV_NAME_RE.test(entry.name))
20
+ continue;
21
+ names.add(entry.name);
22
+ if (SECRET_NAME_RE.test(entry.name))
23
+ secrets.add(entry.name);
24
+ for (const finding of entry.found)
25
+ sources.add(finding.file);
26
+ }
27
+ const legacyPath = join(root, "catalyst.env.json");
28
+ const legacy = deps.readFile !== undefined ? deps.readFile(legacyPath) : readOptional(legacyPath);
29
+ if (legacy !== undefined) {
30
+ const converted = convertLegacyEnvironmentJson(legacy);
31
+ if (converted.state === "invalid")
32
+ return converted;
33
+ for (const name of converted.variableNames)
34
+ names.add(name);
35
+ for (const name of converted.secretNames)
36
+ secrets.add(name);
37
+ if (converted.variableNames.length > 0)
38
+ sources.add("catalyst.env.json");
39
+ }
40
+ const sortedNames = [...names].sort((a, b) => a.localeCompare(b, "en"));
41
+ if (sortedNames.length > MAX_VARIABLES) {
42
+ return { state: "invalid", errors: [`environment has more than ${MAX_VARIABLES} names`] };
43
+ }
44
+ const secretNames = [...secrets].filter((name) => names.has(name)).sort((a, b) => a.localeCompare(b, "en"));
45
+ const standard = standardSteps(root, files, deps);
46
+ if (standard.error !== undefined)
47
+ return { state: "invalid", errors: [standard.error] };
48
+ for (const source of standard.sources)
49
+ sources.add(source);
50
+ for (const path of files)
51
+ if (path.startsWith(".github/workflows/"))
52
+ sources.add(path);
53
+ const chunks = ["[environment]"];
54
+ for (const name of sortedNames)
55
+ chunks.push([
56
+ "[[environment.variables]]",
57
+ `name = ${JSON.stringify(name)}`,
58
+ ...(secrets.has(name) ? ["secret = true"] : []),
59
+ "required = false",
60
+ "",
61
+ ].join("\n"));
62
+ chunks.push(...standard.setup, ...standard.verify);
63
+ const toml = `${chunks.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
64
+ return {
65
+ state: "valid",
66
+ names: sortedNames,
67
+ secretNames,
68
+ setup: standard.setup,
69
+ verify: standard.verify,
70
+ toml,
71
+ sources: [...sources].sort((a, b) => a.localeCompare(b, "en")),
72
+ };
73
+ }
74
+ /** Conservative package.json/lockfile inference. No repository script is executed. */
75
+ function standardSteps(root, files, deps) {
76
+ if (!files.includes("package.json"))
77
+ return { setup: [], verify: [], sources: [] };
78
+ const raw = deps.readFile !== undefined ? deps.readFile(join(root, "package.json")) : readOptional(join(root, "package.json"));
79
+ if (raw === undefined)
80
+ return { setup: [], verify: [], sources: [] };
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(raw);
84
+ }
85
+ catch {
86
+ return { setup: [], verify: [], sources: [], error: "package.json is not valid JSON; refusing to guess setup commands" };
87
+ }
88
+ if (!isRecord(parsed))
89
+ return { setup: [], verify: [], sources: [], error: "package.json must contain a JSON object; refusing to guess setup commands" };
90
+ const scripts = isRecord(parsed.scripts) ? parsed.scripts : {};
91
+ const lock = ["bun.lock", "bun.lockb", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"].find((name) => files.includes(name));
92
+ const manager = lock?.startsWith("bun.") ? "bun"
93
+ : lock === "pnpm-lock.yaml" ? "pnpm"
94
+ : lock === "yarn.lock" ? "yarn"
95
+ : lock === "package-lock.json" ? "npm" : undefined;
96
+ const setup = [];
97
+ const verify = [];
98
+ const sources = ["package.json"];
99
+ if (manager !== undefined && lock !== undefined) {
100
+ const install = manager === "npm" ? ["npm", "ci"]
101
+ : manager === "yarn" ? ["yarn", "install", "--immutable"]
102
+ : [manager, "install", "--frozen-lockfile"];
103
+ setup.push(stepToml("setup", "install-dependencies", install, [lock]));
104
+ sources.push(lock);
105
+ }
106
+ const selected = typeof scripts["check"] === "string"
107
+ ? ["check"]
108
+ : ["typecheck", "test"].filter((name) => typeof scripts[name] === "string");
109
+ if (manager !== undefined) {
110
+ for (const name of selected)
111
+ verify.push(stepToml("verify", name, [manager, "run", name]));
112
+ }
113
+ return { setup, verify, sources };
114
+ }
115
+ function stepToml(section, name, run, cacheKeyFiles = []) {
116
+ return [
117
+ `[[environment.${section}]]`,
118
+ `name = ${JSON.stringify(name)}`,
119
+ `run = [${run.map((arg) => JSON.stringify(arg)).join(", ")}]`,
120
+ ...(cacheKeyFiles.length ? [`cache_key_files = [${cacheKeyFiles.map((path) => JSON.stringify(path)).join(", ")}]`] : []),
121
+ "",
122
+ ].join("\n");
123
+ }
124
+ function readOptional(path) {
125
+ try {
126
+ return readFileSync(path, "utf8");
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ }
132
+ function isRecord(value) {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
@@ -0,0 +1,54 @@
1
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2
+ const SECRET_NAME_RE = /(?:^|_)(?:API_?KEY|AUTH|CREDENTIALS?|PASSWORD|PRIVATE_?KEY|SECRET|TOKEN)(?:_|$)/i;
3
+ /** Convert legacy catalyst.env.json names only. Values and provenance are never copied. */
4
+ export function convertLegacyEnvironmentJson(text) {
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(text);
8
+ }
9
+ catch {
10
+ return { state: "invalid", errors: ["legacy declaration is not valid JSON"] };
11
+ }
12
+ if (!isRecord(parsed) || !Array.isArray(parsed.environment)) {
13
+ return {
14
+ state: "invalid",
15
+ errors: ["legacy declaration must contain an environment array"],
16
+ };
17
+ }
18
+ const errors = [];
19
+ const names = new Set();
20
+ const secretNames = [];
21
+ for (const [index, entry] of parsed.environment.entries()) {
22
+ if (!isRecord(entry) ||
23
+ typeof entry.name !== "string" ||
24
+ !ENV_NAME_RE.test(entry.name)) {
25
+ errors.push(`environment[${index}].name is not a valid environment variable name`);
26
+ continue;
27
+ }
28
+ names.add(entry.name);
29
+ const isSecret = entry.secret === true || SECRET_NAME_RE.test(entry.name);
30
+ if (isSecret && !secretNames.includes(entry.name)) {
31
+ secretNames.push(entry.name);
32
+ }
33
+ }
34
+ const variableNames = [...names].sort();
35
+ if (variableNames.length > 128)
36
+ errors.push("environment has more than 128 names");
37
+ if (errors.length)
38
+ return { state: "invalid", errors };
39
+ const toml = variableNames.length === 0
40
+ ? "[environment]\n"
41
+ : variableNames
42
+ .map((name) => [
43
+ "[[environment.variables]]",
44
+ `name = ${JSON.stringify(name)}`,
45
+ ...(secretNames.includes(name) ? ["secret = true"] : []),
46
+ "required = false",
47
+ "",
48
+ ].join("\n"))
49
+ .join("\n");
50
+ return { state: "valid", variableNames, secretNames, toml };
51
+ }
52
+ function isRecord(value) {
53
+ return typeof value === "object" && value !== null && !Array.isArray(value);
54
+ }
@@ -0,0 +1,50 @@
1
+ /** Select dotenv assignment lines by name while keeping their original bytes for the cloud's parser.
2
+ * Unselected values never cross the network boundary. This mirrors the cloud parser's line grammar. */
3
+ export function filterEnvFileNames(text, selected) {
4
+ const lines = [];
5
+ const found = new Set();
6
+ for (const line of text.split(/\r?\n/)) {
7
+ const trimmed = line.trim();
8
+ if (trimmed === "" || trimmed.startsWith("#"))
9
+ continue;
10
+ const assignment = trimmed.startsWith("export ") ? trimmed.slice("export ".length) : trimmed;
11
+ const equals = assignment.indexOf("=");
12
+ if (equals < 0)
13
+ continue;
14
+ const name = assignment.slice(0, equals).trim();
15
+ if (!selected.has(name))
16
+ continue;
17
+ lines.push(line);
18
+ found.add(name);
19
+ }
20
+ return { text: lines.length === 0 ? "" : `${lines.join("\n")}\n`, found: [...found].sort((a, b) => a.localeCompare(b, "en")) };
21
+ }
22
+ /** Parse dotenv assignments using the mirror import grammar, after the caller has locally selected
23
+ * names. Values stay in this process and are posted one at a time to the documented write route. */
24
+ export function parseEnvAssignments(text, selected) {
25
+ const entries = [];
26
+ for (const rawLine of text.split("\n")) {
27
+ const trimmed = rawLine.trim();
28
+ if (trimmed === "" || trimmed.startsWith("#"))
29
+ continue;
30
+ const assignment = trimmed.startsWith("export ") ? trimmed.slice("export ".length) : trimmed;
31
+ const equals = assignment.indexOf("=");
32
+ if (equals < 0)
33
+ continue;
34
+ const name = assignment.slice(0, equals).trim();
35
+ if (name === "" || (selected !== undefined && !selected.has(name)))
36
+ continue;
37
+ const rawValue = assignment.slice(equals + 1);
38
+ let value = rawValue.trim();
39
+ if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
40
+ value = value.slice(1, -1);
41
+ }
42
+ else {
43
+ const comment = rawValue.search(/\s#/);
44
+ if (comment >= 0)
45
+ value = rawValue.slice(0, comment).trim();
46
+ }
47
+ entries.push({ name, value });
48
+ }
49
+ return entries;
50
+ }
@@ -0,0 +1,66 @@
1
+ import { parse, TomlError } from "smol-toml";
2
+ const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
3
+ /** Validate TOML syntax and the repo declaration's environment variable table without printing values. */
4
+ export function validateSettingsToml(text) {
5
+ let parsed;
6
+ try {
7
+ parsed = parse(text);
8
+ }
9
+ catch (error) {
10
+ const line = error instanceof TomlError && typeof error.line === "number"
11
+ ? ` at line ${error.line}`
12
+ : "";
13
+ return { state: "invalid", errors: [`TOML could not be parsed${line}`] };
14
+ }
15
+ if (!isRecord(parsed))
16
+ return {
17
+ state: "invalid",
18
+ errors: ["the settings file must have a TOML table at its root"],
19
+ };
20
+ if (parsed.environment === undefined)
21
+ return { state: "no-source", errors: [] };
22
+ if (!isRecord(parsed.environment))
23
+ return { state: "invalid", errors: ["environment must be a table"] };
24
+ const section = parsed.environment;
25
+ if (section.variables === undefined)
26
+ return { state: "valid", errors: [], variableNames: [] };
27
+ if (!Array.isArray(section.variables))
28
+ return {
29
+ state: "invalid",
30
+ errors: ["environment.variables must be an array of tables"],
31
+ };
32
+ const errors = [];
33
+ const names = new Set();
34
+ const variableNames = [];
35
+ if (section.variables.length > 128)
36
+ errors.push("environment.variables has more than 128 entries");
37
+ for (const [index, raw] of section.variables.entries()) {
38
+ const prefix = `environment.variables[${index}]`;
39
+ if (!isRecord(raw)) {
40
+ errors.push(`${prefix} must be a table`);
41
+ continue;
42
+ }
43
+ const name = raw.name;
44
+ if (typeof name !== "string" || !ENV_NAME_RE.test(name)) {
45
+ errors.push(`${prefix}.name is not a valid environment variable name`);
46
+ }
47
+ else {
48
+ variableNames.push(name);
49
+ if (names.has(name))
50
+ errors.push(`${prefix}.name duplicates ${name}`);
51
+ names.add(name);
52
+ }
53
+ if (raw.required !== undefined && typeof raw.required !== "boolean")
54
+ errors.push(`${prefix}.required must be a boolean`);
55
+ if (raw.secret !== undefined && typeof raw.secret !== "boolean")
56
+ errors.push(`${prefix}.secret must be a boolean`);
57
+ if ("value" in raw)
58
+ errors.push(`${prefix}.value is not allowed; store values in the cloud`);
59
+ }
60
+ return errors.length > 0
61
+ ? { state: "invalid", errors }
62
+ : { state: "valid", errors: [], variableNames };
63
+ }
64
+ function isRecord(value) {
65
+ return typeof value === "object" && value !== null && !Array.isArray(value);
66
+ }