@catalyst-cloud/catalyst-skills 0.1.1 → 0.2.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 (111) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +109 -53
  3. package/dist/args.js +237 -0
  4. package/dist/ask.js +137 -0
  5. package/dist/cli.js +165 -279
  6. package/dist/config.js +106 -0
  7. package/dist/contract-types.js +4 -0
  8. package/dist/contract.js +178 -0
  9. package/dist/detach.js +10 -0
  10. package/dist/errors.js +27 -0
  11. package/dist/execution.js +156 -0
  12. package/dist/http.js +142 -0
  13. package/dist/prompt.js +35 -0
  14. package/dist/query.js +187 -0
  15. package/dist/ready.js +123 -0
  16. package/dist/replica.js +318 -0
  17. package/dist/sdk.js +26 -0
  18. package/dist/skill-shape.js +179 -0
  19. package/dist/skills.js +74 -0
  20. package/dist/ts-deps-loader.js +68 -0
  21. package/dist/watch/consumer.js +141 -0
  22. package/dist/watch/cursor-file.js +62 -0
  23. package/dist/watch.js +173 -0
  24. package/dist/write.js +224 -0
  25. package/package.json +9 -5
  26. package/skills/catalyst-github/SKILL.md +35 -0
  27. package/skills/catalyst-github/agents/openai.yaml +6 -0
  28. package/skills/catalyst-github/agents/portability.yaml +4 -0
  29. package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
  30. package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
  31. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
  32. package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
  33. package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
  34. package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
  35. package/skills/catalyst-linear/SKILL.md +43 -0
  36. package/skills/catalyst-linear/agents/openai.yaml +6 -0
  37. package/skills/catalyst-linear/agents/portability.yaml +5 -0
  38. package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
  39. package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
  40. package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
  41. package/skills/catalyst-linear/scripts/comment.mjs +59 -0
  42. package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
  43. package/skills/catalyst-linear/scripts/label.mjs +48 -0
  44. package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
  45. package/skills/catalyst-linear/scripts/move.mjs +41 -0
  46. package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
  47. package/skills/catalyst-linear/scripts/search.mjs +49 -0
  48. package/skills/catalyst-setup/SKILL.md +32 -0
  49. package/skills/catalyst-setup/agents/openai.yaml +6 -0
  50. package/skills/catalyst-setup/agents/portability.yaml +4 -0
  51. package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
  52. package/skills/catalyst-setup/scripts/check.mjs +75 -0
  53. package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
  54. package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
  55. package/skills/connect-me/SKILL.md +55 -0
  56. package/skills/connect-me/agents/openai.yaml +6 -0
  57. package/skills/connect-me/agents/portability.yaml +5 -0
  58. package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
  59. package/skills/connect-me/scripts/lib/cli.mjs +184 -0
  60. package/skills/connect-me/scripts/verify-connection.mjs +68 -0
  61. package/skills/how-catalyst-works/SKILL.md +42 -0
  62. package/skills/how-catalyst-works/agents/openai.yaml +6 -0
  63. package/skills/how-catalyst-works/agents/portability.yaml +4 -0
  64. package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
  65. package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
  66. package/skills/how-catalyst-works/references/the-ladder.md +41 -0
  67. package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
  68. package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
  69. package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
  70. package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
  71. package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
  72. package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
  73. package/skills/run-this-project/SKILL.md +45 -0
  74. package/skills/run-this-project/agents/openai.yaml +6 -0
  75. package/skills/run-this-project/agents/portability.yaml +5 -0
  76. package/skills/run-this-project/assets/stall-policy.json +15 -0
  77. package/skills/run-this-project/references/making-work-ready.md +60 -0
  78. package/skills/run-this-project/references/reacting-to-events.md +76 -0
  79. package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
  80. package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
  81. package/skills/run-this-project/scripts/make-ready.mjs +64 -0
  82. package/skills/run-this-project/scripts/scope-status.mjs +0 -0
  83. package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
  84. package/skills/what-needs-me/SKILL.md +41 -0
  85. package/skills/what-needs-me/agents/openai.yaml +6 -0
  86. package/skills/what-needs-me/agents/portability.yaml +5 -0
  87. package/skills/what-needs-me/references/raising-a-decision.md +41 -0
  88. package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
  89. package/skills/what-needs-me/references/settling-an-answer.md +37 -0
  90. package/skills/what-needs-me/scripts/inbox.mjs +47 -0
  91. package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
  92. package/skills/what-needs-me/scripts/raise.mjs +53 -0
  93. package/skills/what-needs-me/scripts/settle.mjs +73 -0
  94. package/skills/whats-happening/SKILL.md +43 -0
  95. package/skills/whats-happening/agents/openai.yaml +6 -0
  96. package/skills/whats-happening/agents/portability.yaml +4 -0
  97. package/skills/whats-happening/assets/status-reply.json +77 -0
  98. package/skills/whats-happening/references/reading-the-board.md +43 -0
  99. package/skills/whats-happening/references/reprioritising.md +37 -0
  100. package/skills/whats-happening/references/routing-work.md +36 -0
  101. package/skills/whats-happening/references/status-reply.md +34 -0
  102. package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
  103. package/skills/whats-happening/scripts/explain.mjs +28 -0
  104. package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
  105. package/skills/whats-happening/scripts/snapshot.mjs +145 -0
  106. package/skills/ask/SKILL.md +0 -37
  107. package/skills/concierge/SKILL.md +0 -41
  108. package/skills/join/SKILL.md +0 -30
  109. package/skills/linearis/SKILL.md +0 -40
  110. package/skills/setup/SKILL.md +0 -27
  111. package/skills/steward/SKILL.md +0 -30
package/dist/cli.js CHANGED
@@ -1,268 +1,72 @@
1
- // cli.ts — CTC-1926: the `catalyst-skills` customer installer/joiner (npm: @catalyst-cloud/catalyst-skills).
2
- import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
6
- export const DEFAULT_BASE_URL = "https://staging.catalystcloud.dev";
7
- export const PROVENANCE_MARKER = "vendored-from: @catalyst-cloud/catalyst-skills";
1
+ // cli.ts — the `catalyst-skills` dispatcher (npm: @catalyst-cloud/catalyst-skills). Verbs live in
2
+ // their own modules; this file keeps every export the 0.1 tests and the bin import.
3
+ import { existsSync } from "node:fs";
4
+ import { parseArgs, positionals, verbHelp } from "./args.js";
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
+ import { loadContract, pickPath } from "./contract.js";
7
+ import { CliError, MeError, UsageError } from "./errors.js";
8
+ import { fetchMe } from "./http.js";
9
+ import { cmdAccounts, cmdExplain, cmdQueue, cmdRunning } from "./execution.js";
10
+ import { cmdQuery } from "./query.js";
11
+ import { cmdReady } from "./ready.js";
12
+ import { cmdReplica } from "./replica.js";
13
+ import { promptSecret, stdinIsTty } from "./prompt.js";
14
+ import { PROVENANCE_MARKER } from "./skill-shape.js";
15
+ import { installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine } from "./skills.js";
16
+ import { cmdWatch } from "./watch.js";
17
+ import { cmdWrite } from "./write.js";
18
+ import { cmdAsk } from "./ask.js";
19
+ 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, };
8
20
  export const CUSTOMER_SKILLS = [
9
- "ask",
10
- "concierge",
11
- "join",
12
- "linearis",
13
- "setup",
14
- "steward",
21
+ "catalyst-github",
22
+ "catalyst-linear",
23
+ "catalyst-setup",
24
+ "connect-me",
25
+ "how-catalyst-works",
26
+ "run-this-project",
27
+ "what-needs-me",
28
+ "whats-happening",
15
29
  ];
16
- export class UsageError extends Error {
17
- }
18
- export class CliError extends Error {
19
- code;
20
- constructor(message, code) {
21
- super(message);
22
- this.code = code;
23
- }
24
- }
25
- export class MeError extends Error {
26
- kind;
27
- status;
28
- constructor(message, kind, status) {
29
- super(message);
30
- this.kind = kind;
31
- this.status = status;
32
- }
33
- }
34
- export function parseArgs(argv) {
35
- const out = { command: null, force: false, help: false, version: false };
36
- for (let i = 0; i < argv.length; i++) {
37
- const a = argv[i];
38
- if (a === "-h" || a === "--help")
39
- out.help = true;
40
- else if (a === "-V" || a === "--version")
41
- out.version = true;
42
- else if (a === "--key")
43
- out.key = requireValue(argv, ++i, "--key");
44
- else if (a === "--base-url")
45
- out.baseUrl = requireValue(argv, ++i, "--base-url");
46
- else if (a === "--skills-dir")
47
- out.skillsDir = requireValue(argv, ++i, "--skills-dir");
48
- else if (a === "--force")
49
- out.force = true;
50
- else if (a.startsWith("--"))
51
- throw new UsageError(`unknown option: ${a}`);
52
- else if (out.command === null)
53
- out.command = a;
54
- else
55
- throw new UsageError(`unexpected argument: ${a}`);
56
- }
57
- return out;
58
- }
59
- function requireValue(argv, i, flag) {
60
- const v = argv[i];
61
- if (v === undefined || v === "")
62
- throw new UsageError(`${flag} requires a value`);
63
- return v;
64
- }
65
- let manifestCache = null;
66
- export function readManifest() {
67
- if (manifestCache)
68
- return manifestCache;
69
- const raw = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
70
- manifestCache = {
71
- version: raw.version,
72
- tenantContractRange: raw.catalystCloud?.tenantContractRange ?? "unpinned",
73
- };
74
- return manifestCache;
75
- }
76
- export function configPathFor(home) {
77
- return join(home, ".config", "catalyst-cloud", "customer.json");
78
- }
79
- export function defaultSkillsDirFor(home) {
80
- return join(home, ".claude", "skills");
81
- }
82
- export function loadConfig(home) {
83
- const path = configPathFor(home);
84
- if (!existsSync(path))
85
- return null;
86
- let parsed;
87
- try {
88
- parsed = JSON.parse(readFileSync(path, "utf8"));
89
- }
90
- catch {
91
- throw new CliError(`config at ${path} is not valid JSON — re-run join to rewrite it`, "config-corrupt");
92
- }
93
- const cfg = parsed;
94
- if (typeof cfg.account !== "string" ||
95
- typeof cfg.key !== "string" ||
96
- typeof cfg.baseUrl !== "string") {
97
- throw new CliError(`config at ${path} is missing required fields — re-run join to rewrite it`, "config-corrupt");
98
- }
99
- return cfg;
100
- }
101
- export const CONFIG_MODE = 0o600;
102
- /**
103
- * Write the config and return its path plus the mode the file ACTUALLY carries afterwards.
104
- *
105
- * Codex round 1 (P1): Node honours `mode` only when it creates the file, so a rewrite of an existing
106
- * `customer.json` (key rotation, the corrupt-config repair, the update stamp) left a pre-existing
107
- * 0644 file world-readable while the CLI claimed 0600. The chmod runs on every write, and the
108
- * reported mode is read back from the file rather than asserted.
109
- */
110
- export function saveConfig(home, cfg) {
111
- return writeConfig(home, cfg).path;
112
- }
113
- export function writeConfig(home, cfg) {
114
- const path = configPathFor(home);
115
- mkdirSync(dirname(path), { recursive: true });
116
- writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: CONFIG_MODE });
117
- chmodSync(path, CONFIG_MODE);
118
- return { path, mode: statSync(path).mode & 0o777 };
119
- }
120
- export function formatMode(mode) {
121
- return "0" + mode.toString(8);
122
- }
123
- export function normalizeBaseUrl(url) {
124
- return url.replace(/\/+$/, "");
125
- }
126
- export async function fetchMe(baseUrl, key, fetchImpl, timeoutMs = 15_000) {
127
- const url = `${normalizeBaseUrl(baseUrl)}/api/v1/me`;
128
- let res;
129
- try {
130
- res = await fetchImpl(url, {
131
- headers: { authorization: `Bearer ${key}`, accept: "application/json" },
132
- signal: AbortSignal.timeout(timeoutMs),
133
- });
134
- }
135
- catch (err) {
136
- throw new MeError(`could not reach ${url}: ${err instanceof Error ? err.message : String(err)}`, "network");
137
- }
138
- if (!res.ok) {
139
- let reason;
140
- try {
141
- const body = (await res.json());
142
- reason = body.reason ?? body.error ?? "";
143
- }
144
- catch {
145
- reason = "";
146
- }
147
- const detail = res.status === 401
148
- ? "credential not accepted — ask your tenant admin for a valid account key"
149
- : reason || `HTTP ${res.status}`;
150
- throw new MeError(`GET /me failed (${res.status}): ${detail}`, "http", res.status);
151
- }
152
- let body;
153
- try {
154
- body = (await res.json());
155
- }
156
- catch {
157
- throw new MeError("GET /me returned a non-JSON body", "shape");
158
- }
159
- if (typeof body.account !== "string" ||
160
- typeof body.slug !== "string" ||
161
- typeof body.name !== "string" ||
162
- (body.permissions !== null && !Array.isArray(body.permissions)) ||
163
- (body.principal !== "service" && body.principal !== "session")) {
164
- throw new MeError("GET /me returned an unexpected shape", "shape");
165
- }
166
- return {
167
- account: body.account,
168
- slug: body.slug,
169
- name: body.name,
170
- permissions: body.permissions,
171
- principal: body.principal,
172
- };
173
- }
174
- export function parseChangelogEntry(changelog, version) {
175
- const lines = changelog.split("\n");
176
- const heading = `## ${version}`;
177
- const start = lines.findIndex((l) => l.trim() === heading);
178
- if (start === -1)
179
- return null;
180
- for (const line of lines.slice(start + 1)) {
181
- const t = line.trim();
182
- if (t === "")
183
- continue;
184
- if (t.startsWith("#"))
185
- break;
186
- return t;
187
- }
188
- return null;
189
- }
190
- export function readChangelog() {
191
- return readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8");
192
- }
193
- export function updateNoticeLine(previous, current, entry) {
194
- const summary = entry ?? "see CHANGELOG.md";
195
- return `[catalyst-skills] updated ${previous} → ${current}: ${summary} · update with: npm update -g ${PACKAGE_NAME} (or: npx ${PACKAGE_NAME}@latest join)`;
196
- }
197
- export function skillsSourceDir() {
198
- return fileURLToPath(new URL("../skills", import.meta.url));
199
- }
200
- export function installSkills(targetDir, opts, sourceDir = skillsSourceDir()) {
201
- const result = { installed: [], skipped: [] };
202
- const names = readdirSync(sourceDir, { withFileTypes: true })
203
- .filter((e) => e.isDirectory())
204
- .map((e) => e.name)
205
- .sort();
206
- for (const name of names) {
207
- const src = join(sourceDir, name);
208
- if (!existsSync(join(src, "SKILL.md")))
209
- continue;
210
- const dst = join(targetDir, name);
211
- const existingMd = join(dst, "SKILL.md");
212
- if (existsSync(existingMd) && !opts.force) {
213
- const existing = readFileSync(existingMd, "utf8");
214
- if (!existing.includes(PROVENANCE_MARKER)) {
215
- result.skipped.push({ name, reason: "foreign-skill-dir" });
216
- continue;
217
- }
218
- }
219
- mkdirSync(dst, { recursive: true });
220
- cpSync(src, dst, { recursive: true });
221
- result.installed.push(name);
222
- }
223
- return result;
224
- }
30
+ /** `login` is the verb; `join` is the 0.1 name, kept as a deprecated alias for one minor version. */
31
+ const VERB_ALIASES = { join: "login" };
32
+ /** Verbs whose stdout is for a human, so the update notice may share it. Every other verb's stdout
33
+ * is machine-read by a skill script and the notice goes to stderr. */
34
+ const HUMAN_VERBS = new Set(["login", "install", "status", "notice"]);
225
35
  export function usageText() {
226
36
  return [
227
- `${PACKAGE_NAME} — install the customer skill bundle and join your catalyst-cloud tenant`,
37
+ `${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your tenant, read through the SDK, write through the agent proxy`,
228
38
  "",
229
39
  "Usage:",
230
- " catalyst-skills join --key <account-key> [--base-url <url>] [--skills-dir <dir>]",
231
- " catalyst-skills install [--skills-dir <dir>] [--force]",
232
- " catalyst-skills notice",
233
- " catalyst-skills status",
40
+ " catalyst-skills login [--key <account-key>] [--base-url <url>] [--start-replica]",
41
+ " catalyst-skills join ... (deprecated alias of login; removed in the next minor version)",
42
+ " catalyst-skills install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
43
+ " catalyst-skills status | notice | me | ready | accounts",
44
+ " catalyst-skills contract [--refresh] [--path <a.b.c>]",
45
+ " catalyst-skills query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <n>>",
46
+ " catalyst-skills replica <start [--detach]|stop|status [--probe]|sql \"<select>\"|schema [table]>",
47
+ " catalyst-skills explain <ticket> | running | queue [--team K]",
48
+ " catalyst-skills watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
49
+ " catalyst-skills write <comment|state|label|create|reaction|attachment|session> ...",
50
+ " catalyst-skills ask <raise|accept|list> ...",
51
+ "",
52
+ "Every verb takes --help. --json makes the output machine-readable.",
234
53
  "",
235
54
  "The key may also come from CATALYST_CLOUD_TOKEN; the base URL defaults to",
236
55
  `CATALYST_CLOUD_BASE_URL or ${DEFAULT_BASE_URL}.`,
237
56
  "",
238
- "join installs the skills (default ~/.claude/skills), calls GET /api/v1/me to discover",
239
- "your tenant from the key alone, and writes ~/.config/catalyst-cloud/customer.json (0600).",
57
+ "login calls GET /api/v1/me to discover your tenant from the key alone, writes",
58
+ "~/.config/catalyst-cloud/customer.json (0600) with the key and this CLI's path, and caches the",
59
+ "tenant contract beside it. With no key in the environment and a terminal attached it prompts",
60
+ "for the key without echoing it. It does not install skills: your agent's own install command",
61
+ "does that, and `install` is only here to repair a copy this package made.",
240
62
  ].join("\n");
241
63
  }
242
- export function defaultCtx() {
243
- return {
244
- env: process.env,
245
- home: process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/",
246
- stdout: (line) => console.log(line),
247
- stderr: (line) => console.error(line),
248
- fetch: fetch,
249
- now: () => new Date(),
250
- };
251
- }
252
- export function resolveSkillsDir(args, ctx, cfg) {
253
- return (args.skillsDir ??
254
- ctx.env.CATALYST_SKILLS_CLAUDE_DIR ??
255
- cfg?.skillsDir ??
256
- defaultSkillsDirFor(ctx.home));
257
- }
258
64
  /**
259
- * The Tier-2 update path. When the bundle on disk is newer than the version the config last
260
- * recorded, the copies under the skills dir are still the OLD bundle's — `npm update -g` and a newer
261
- * `npx` replace the package, not the copies join made (Codex round 1, P2). So the copies are
262
- * refreshed FIRST and the new version is recorded only once that succeeded; a failed refresh leaves
263
- * the old stamp in place so the next command tries again instead of silencing the notice forever.
65
+ * The Tier-2 update path: when the bundle on disk is newer than the version the config last
66
+ * recorded, refresh the copied skills FIRST and record the new version only once that succeeded.
264
67
  */
265
68
  function maybePrintUpdateNotice(args, ctx) {
69
+ const say = HUMAN_VERBS.has(args.command ?? "") ? ctx.stdout : ctx.stderr;
266
70
  let cfg;
267
71
  try {
268
72
  cfg = loadConfig(ctx.home);
@@ -278,31 +82,32 @@ function maybePrintUpdateNotice(args, ctx) {
278
82
  const previous = cfg.lastSkillBundleVersion;
279
83
  if (previous) {
280
84
  const entry = parseChangelogEntry(readChangelog(), manifest.version);
281
- ctx.stdout(updateNoticeLine(previous, manifest.version, entry));
85
+ say(updateNoticeLine(previous, manifest.version, entry));
282
86
  }
283
87
  const skillsDir = resolveSkillsDir(args, ctx, cfg);
284
88
  let refreshed;
285
89
  try {
286
- refreshed = installSkills(skillsDir, { force: false });
90
+ refreshed = installSkills(skillsDir, { force: false, onlyExisting: true });
287
91
  }
288
92
  catch (err) {
289
93
  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`);
290
94
  return;
291
95
  }
292
- if (refreshed.installed.length > 0) {
293
- ctx.stdout(`[catalyst-skills] refreshed ${refreshed.installed.join(", ")} at ${skillsDir} to ${manifest.version}`);
294
- }
96
+ if (refreshed.installed.length > 0)
97
+ say(`[catalyst-skills] refreshed ${refreshed.installed.join(", ")} at ${skillsDir} to ${manifest.version}`);
295
98
  for (const s of refreshed.skipped) {
296
- ctx.stdout(`[catalyst-skills] left "${s.name}" alone: ${skillsDir}/${s.name} was not installed by this package (catalyst-skills install --force to replace)`);
99
+ say(`[catalyst-skills] left "${s.name}" alone: ${skillsDir}/${s.name} was not installed by this package (catalyst-skills install --force to replace)`);
297
100
  }
298
101
  cfg.lastSkillBundleVersion = manifest.version;
299
102
  cfg.skillsDir = skillsDir;
300
103
  saveConfig(ctx.home, cfg);
301
104
  }
302
- export async function main(argv, ctx = defaultCtx()) {
105
+ export async function main(argv, ctx = defaultCtx(), deps = {}) {
303
106
  let args;
304
107
  try {
305
- args = parseArgs(argv);
108
+ args = parseArgs(argv.map((a, i) => (i === 0 && VERB_ALIASES[a] ? VERB_ALIASES[a] : a)));
109
+ if (args.command && VERB_ALIASES[args.command])
110
+ args.command = VERB_ALIASES[args.command];
306
111
  }
307
112
  catch (err) {
308
113
  ctx.stderr(err instanceof Error ? err.message : String(err));
@@ -314,21 +119,49 @@ export async function main(argv, ctx = defaultCtx()) {
314
119
  ctx.stdout(`${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
315
120
  return 0;
316
121
  }
317
- if (args.help || args.command === null) {
122
+ if (args.command === null) {
318
123
  ctx.stdout(usageText());
319
124
  return 0;
320
125
  }
126
+ if (args.help) {
127
+ ctx.stdout(verbHelp(args.command));
128
+ return 0;
129
+ }
321
130
  try {
322
131
  maybePrintUpdateNotice(args, ctx);
323
132
  switch (args.command) {
324
- case "join":
325
- return await cmdJoin(args, ctx, manifest);
133
+ case "login":
134
+ return await cmdLogin(args, ctx, deps);
326
135
  case "install":
327
136
  return cmdInstall(args, ctx);
328
137
  case "notice":
329
138
  return 0;
330
139
  case "status":
331
- return cmdStatus(ctx, manifest);
140
+ return cmdStatus(ctx);
141
+ case "me":
142
+ return await cmdMe(args, ctx);
143
+ case "contract":
144
+ return await cmdContract(args, ctx);
145
+ case "query":
146
+ return await cmdQuery(args, ctx, { engineDeps: deps.replica?.engineDeps });
147
+ case "replica":
148
+ return await cmdReplica(args, ctx, deps.replica);
149
+ case "explain":
150
+ return await cmdExplain(args, ctx);
151
+ case "running":
152
+ return await cmdRunning(args, ctx);
153
+ case "queue":
154
+ return await cmdQueue(args, ctx);
155
+ case "watch":
156
+ return await cmdWatch(args, ctx, deps.watch);
157
+ case "write":
158
+ return await cmdWrite(args, ctx, deps.write);
159
+ case "ask":
160
+ return await cmdAsk(args, ctx);
161
+ case "ready":
162
+ return await cmdReady(args, ctx, { skillNames: CUSTOMER_SKILLS, loadSdk: deps.loadSdk });
163
+ case "accounts":
164
+ return cmdAccounts(ctx);
332
165
  default:
333
166
  ctx.stderr(`unknown command: ${args.command}`);
334
167
  ctx.stderr(usageText());
@@ -338,21 +171,29 @@ export async function main(argv, ctx = defaultCtx()) {
338
171
  catch (err) {
339
172
  if (err instanceof UsageError) {
340
173
  ctx.stderr(err.message);
341
- ctx.stderr(usageText());
174
+ ctx.stderr(args.command && args.command in VERB_HELP_KNOWN ? verbHelp(args.command) : usageText());
342
175
  return 1;
343
176
  }
344
- if (err instanceof MeError || err instanceof CliError) {
177
+ if (err instanceof CliError) {
178
+ ctx.stderr(`catalyst-skills: ${err.message}`);
179
+ return err.exitCode;
180
+ }
181
+ if (err instanceof MeError) {
345
182
  ctx.stderr(`catalyst-skills: ${err.message}`);
346
183
  return 2;
347
184
  }
348
185
  throw err;
349
186
  }
350
187
  }
351
- async function cmdJoin(args, ctx, manifest) {
352
- const key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
353
- if (!key) {
354
- throw new UsageError("join needs an account key: pass --key <account-key> or set CATALYST_CLOUD_TOKEN");
188
+ 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]));
189
+ async function cmdLogin(args, ctx, deps) {
190
+ const manifest = readManifest();
191
+ let key = (args.key ?? ctx.env.CATALYST_CLOUD_TOKEN ?? "").trim();
192
+ if (!key && (deps.isTty ?? stdinIsTty)()) {
193
+ key = (await (deps.promptSecret ?? promptSecret)("Account key (not echoed): ")).trim();
355
194
  }
195
+ if (!key)
196
+ throw new UsageError("login needs an account key: pass --key <account-key> or set CATALYST_CLOUD_TOKEN");
356
197
  const baseUrl = normalizeBaseUrl(args.baseUrl ?? ctx.env.CATALYST_CLOUD_BASE_URL ?? DEFAULT_BASE_URL);
357
198
  const me = await fetchMe(baseUrl, key, ctx.fetch);
358
199
  let existing;
@@ -364,7 +205,6 @@ async function cmdJoin(args, ctx, manifest) {
364
205
  }
365
206
  const previous = existing?.lastSkillBundleVersion ?? null;
366
207
  const skillsDir = resolveSkillsDir(args, ctx, existing);
367
- const result = installSkills(skillsDir, { force: args.force });
368
208
  const config = {
369
209
  baseUrl,
370
210
  key,
@@ -376,21 +216,37 @@ async function cmdJoin(args, ctx, manifest) {
376
216
  joinedAt: ctx.now().toISOString(),
377
217
  lastSkillBundleVersion: manifest.version,
378
218
  skillsDir,
219
+ cliPath: cliPath(),
220
+ replicaDb: existing?.replicaDb ?? defaultReplicaDbFor(ctx.home),
379
221
  };
380
222
  const written = writeConfig(ctx.home, config);
381
- ctx.stdout(`Joined ${me.name} (${me.slug}) — account ${me.account}`);
382
- ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your key)${written.mode === CONFIG_MODE ? "" : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
383
- ctx.stdout(result.installed.length > 0
384
- ? `Skills installed to ${skillsDir}: ${result.installed.join(", ")}`
385
- : `No skills installed — ${skillsDir} already had them`);
386
- for (const s of result.skipped) {
387
- ctx.stdout(`Skipped "${s.name}": ${skillsDir}/${s.name} exists and was not installed by this package (use --force to replace)`);
388
- }
223
+ ctx.stdout(`Connected to ${me.name} (${me.slug}) — account ${me.account}`);
224
+ 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`}`);
389
225
  ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
226
+ try {
227
+ const loaded = await loadContract(ctx, config, { refresh: true });
228
+ ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
229
+ }
230
+ catch (err) {
231
+ if (err instanceof CliError && (err.code === "contract-forbidden" || err.code === "contract-version")) {
232
+ ctx.stderr(`[catalyst-skills] ${err.message}`);
233
+ }
234
+ else {
235
+ throw err;
236
+ }
237
+ }
390
238
  if (previous && previous !== manifest.version) {
391
239
  const entry = parseChangelogEntry(readChangelog(), manifest.version);
392
240
  ctx.stdout(updateNoticeLine(previous, manifest.version, entry));
393
241
  }
242
+ if (args.flags["start-replica"] === true) {
243
+ const code = await cmdReplica(parseArgs(["replica", "start", "--detach"]), ctx, {
244
+ ...deps.replica,
245
+ argv: deps.replica?.argv ?? [cliPath(), "replica", "start"],
246
+ });
247
+ if (code !== 0)
248
+ return code;
249
+ }
394
250
  return 0;
395
251
  }
396
252
  function cmdInstall(args, ctx) {
@@ -411,14 +267,44 @@ function cmdInstall(args, ctx) {
411
267
  }
412
268
  return 0;
413
269
  }
414
- function cmdStatus(ctx, manifest) {
270
+ function cmdStatus(ctx) {
271
+ const manifest = readManifest();
415
272
  const cfg = loadConfig(ctx.home);
416
273
  if (!cfg) {
417
- ctx.stdout(`Not joined yet — run: npx ${PACKAGE_NAME} join --key <your account key>`);
274
+ ctx.stdout(`Not connected yet — run: CATALYST_CLOUD_TOKEN=<your account key> npx ${PACKAGE_NAME} login`);
418
275
  return 0;
419
276
  }
420
277
  ctx.stdout(`Tenant: ${cfg.name} (${cfg.slug}) — account ${cfg.account}`);
421
278
  ctx.stdout(`API: ${cfg.baseUrl} (principal: ${cfg.principal})`);
422
279
  ctx.stdout(`Bundle: ${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
280
+ if (cfg.cliPath)
281
+ ctx.stdout(`CLI: ${cfg.cliPath}${existsSync(cfg.cliPath) ? "" : " (missing — re-run login)"}`);
282
+ ctx.stdout(`Contract: ${existsSync(contractPathFor(ctx.home)) ? contractPathFor(ctx.home) : "not cached (run: catalyst-skills contract --refresh)"}`);
283
+ return 0;
284
+ }
285
+ async function cmdMe(args, ctx) {
286
+ const cfg = requireConfig(ctx);
287
+ const me = await fetchMe(cfg.baseUrl, cfg.key, ctx.fetch);
288
+ if (args.json)
289
+ ctx.stdout(JSON.stringify(me));
290
+ else {
291
+ ctx.stdout(`${me.name} (${me.slug}) — account ${me.account}`);
292
+ ctx.stdout(`principal: ${me.principal}; permissions: ${me.permissions ? me.permissions.join(", ") : "unrestricted"}`);
293
+ }
294
+ return 0;
295
+ }
296
+ async function cmdContract(args, ctx) {
297
+ const cfg = requireConfig(ctx);
298
+ const [sub] = positionals(args);
299
+ if (sub)
300
+ throw new UsageError(`contract takes no positional argument (got "${sub}"); use --path <a.b.c>`);
301
+ const refresh = args.flags.refresh === true;
302
+ const loaded = await loadContract(ctx, cfg, { refresh });
303
+ ctx.stderr(`contract: ${loaded.doc.contractVersion} from ${loaded.source}${loaded.source === "cache" ? ` (${loaded.ageSeconds}s old)` : ""}`);
304
+ const path = typeof args.flags.path === "string" ? args.flags.path : undefined;
305
+ const value = path ? pickPath(loaded.doc, path) : loaded.doc;
306
+ if (path && value === undefined)
307
+ throw new CliError(`the contract has nothing at "${path}"`, "contract-path");
308
+ ctx.stdout(typeof value === "string" ? value : JSON.stringify(value, null, args.json ? 0 : 2));
423
309
  return 0;
424
310
  }
package/dist/config.js ADDED
@@ -0,0 +1,106 @@
1
+ // config.ts — ~/.config/catalyst-cloud/customer.json and its siblings. The ONLY place the base URL
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";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CliError } from "./errors.js";
7
+ export const PACKAGE_NAME = "@catalyst-cloud/catalyst-skills";
8
+ export const DEFAULT_BASE_URL = "https://staging.catalystcloud.dev";
9
+ export const CONFIG_MODE = 0o600;
10
+ export function defaultCtx() {
11
+ return {
12
+ env: process.env,
13
+ home: process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/",
14
+ stdout: (line) => console.log(line),
15
+ stderr: (line) => console.error(line),
16
+ fetch: fetch,
17
+ now: () => new Date(),
18
+ };
19
+ }
20
+ export function configDirFor(home) {
21
+ return join(home, ".config", "catalyst-cloud");
22
+ }
23
+ export function configPathFor(home) {
24
+ return join(configDirFor(home), "customer.json");
25
+ }
26
+ export function contractPathFor(home) {
27
+ return join(configDirFor(home), "contract.json");
28
+ }
29
+ export function watchCursorPathFor(home) {
30
+ return join(configDirFor(home), "watch-cursor.json");
31
+ }
32
+ export function defaultReplicaDbFor(home) {
33
+ return join(configDirFor(home), "replica.db");
34
+ }
35
+ export function defaultSkillsDirFor(home) {
36
+ return join(home, ".claude", "skills");
37
+ }
38
+ /** The CLI launcher this very package ships — recorded by login so skill scripts can spawn it. */
39
+ export function cliPath() {
40
+ return fileURLToPath(new URL("../bin/catalyst-skills.js", import.meta.url));
41
+ }
42
+ export function normalizeBaseUrl(url) {
43
+ return url.replace(/\/+$/, "");
44
+ }
45
+ /** The origin WITH the versioned prefix — what the SDK and every API read/write take. */
46
+ export function apiBase(cfg) {
47
+ return `${normalizeBaseUrl(cfg.baseUrl)}/api/v1`;
48
+ }
49
+ export function replicaDbPath(cfg, home) {
50
+ return cfg.replicaDb ?? defaultReplicaDbFor(home);
51
+ }
52
+ export function loadConfig(home) {
53
+ const path = configPathFor(home);
54
+ if (!existsSync(path))
55
+ return null;
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(readFileSync(path, "utf8"));
59
+ }
60
+ catch {
61
+ throw new CliError(`config at ${path} is not valid JSON — re-run login to rewrite it`, "config-corrupt");
62
+ }
63
+ const cfg = parsed;
64
+ if (typeof cfg.account !== "string" || typeof cfg.key !== "string" || typeof cfg.baseUrl !== "string") {
65
+ throw new CliError(`config at ${path} is missing required fields — re-run login to rewrite it`, "config-corrupt");
66
+ }
67
+ return cfg;
68
+ }
69
+ /** Load the config or refuse with the one line every verb prints when the machine is not connected. */
70
+ export function requireConfig(ctx) {
71
+ const cfg = loadConfig(ctx.home);
72
+ if (!cfg) {
73
+ throw new CliError(`not connected yet — run: CATALYST_CLOUD_TOKEN=<account key> npx ${PACKAGE_NAME} login`, "not-configured");
74
+ }
75
+ return cfg;
76
+ }
77
+ export function saveConfig(home, cfg) {
78
+ return writeConfig(home, cfg).path;
79
+ }
80
+ /** Write the config and return the mode the file ACTUALLY carries afterwards (chmod runs on every
81
+ * write: Node honours `mode` only when it creates the file). */
82
+ export function writeConfig(home, cfg) {
83
+ const path = configPathFor(home);
84
+ mkdirSync(dirname(path), { recursive: true });
85
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", { mode: CONFIG_MODE });
86
+ chmodSync(path, CONFIG_MODE);
87
+ return { path, mode: statSync(path).mode & 0o777 };
88
+ }
89
+ export function formatMode(mode) {
90
+ return "0" + mode.toString(8);
91
+ }
92
+ let manifestCache = null;
93
+ export function readManifest() {
94
+ if (manifestCache)
95
+ return manifestCache;
96
+ const raw = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
97
+ manifestCache = {
98
+ version: raw.version,
99
+ tenantContractRange: raw.catalystCloud?.tenantContractRange ?? "unpinned",
100
+ };
101
+ return manifestCache;
102
+ }
103
+ /** Test seam: forget the cached manifest. */
104
+ export function resetManifestCache() {
105
+ manifestCache = null;
106
+ }
@@ -0,0 +1,4 @@
1
+ // contract-types.ts — the SHAPE of GET /api/v1/agent/contract as this bundle reads it. Typed here
2
+ // (the cloud's own declaration lives in another repository) and filled only from the wire. No field
3
+ // below carries a default: a tenant value the contract does not serve is refused, never guessed.
4
+ export {};