@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.
- package/CHANGELOG.md +4 -0
- package/README.md +109 -53
- package/dist/args.js +237 -0
- package/dist/ask.js +137 -0
- package/dist/cli.js +165 -279
- package/dist/config.js +106 -0
- package/dist/contract-types.js +4 -0
- package/dist/contract.js +178 -0
- package/dist/detach.js +10 -0
- package/dist/errors.js +27 -0
- package/dist/execution.js +156 -0
- package/dist/http.js +142 -0
- package/dist/prompt.js +35 -0
- package/dist/query.js +187 -0
- package/dist/ready.js +123 -0
- package/dist/replica.js +318 -0
- package/dist/sdk.js +26 -0
- package/dist/skill-shape.js +179 -0
- package/dist/skills.js +74 -0
- package/dist/ts-deps-loader.js +68 -0
- package/dist/watch/consumer.js +141 -0
- package/dist/watch/cursor-file.js +62 -0
- package/dist/watch.js +173 -0
- package/dist/write.js +224 -0
- package/package.json +9 -5
- package/skills/catalyst-github/SKILL.md +35 -0
- package/skills/catalyst-github/agents/openai.yaml +6 -0
- package/skills/catalyst-github/agents/portability.yaml +4 -0
- package/skills/catalyst-github/references/is-it-mergeable.md +57 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +61 -0
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +124 -0
- package/skills/catalyst-github/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-github/scripts/lib/pull.mjs +82 -0
- package/skills/catalyst-github/scripts/read-pr.mjs +97 -0
- package/skills/catalyst-linear/SKILL.md +43 -0
- package/skills/catalyst-linear/agents/openai.yaml +6 -0
- package/skills/catalyst-linear/agents/portability.yaml +5 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +50 -0
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +53 -0
- package/skills/catalyst-linear/references/writing-to-linear.md +43 -0
- package/skills/catalyst-linear/scripts/comment.mjs +59 -0
- package/skills/catalyst-linear/scripts/create-ticket.mjs +44 -0
- package/skills/catalyst-linear/scripts/label.mjs +48 -0
- package/skills/catalyst-linear/scripts/lib/cli.mjs +163 -0
- package/skills/catalyst-linear/scripts/move.mjs +41 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +93 -0
- package/skills/catalyst-linear/scripts/search.mjs +49 -0
- package/skills/catalyst-setup/SKILL.md +32 -0
- package/skills/catalyst-setup/agents/openai.yaml +6 -0
- package/skills/catalyst-setup/agents/portability.yaml +4 -0
- package/skills/catalyst-setup/references/what-each-check-means.md +61 -0
- package/skills/catalyst-setup/scripts/check.mjs +75 -0
- package/skills/catalyst-setup/scripts/lib/cli.mjs +102 -0
- package/skills/catalyst-setup/scripts/replica-status.mjs +46 -0
- package/skills/connect-me/SKILL.md +55 -0
- package/skills/connect-me/agents/openai.yaml +6 -0
- package/skills/connect-me/agents/portability.yaml +5 -0
- package/skills/connect-me/references/keeping-the-replica-running.md +88 -0
- package/skills/connect-me/scripts/lib/cli.mjs +184 -0
- package/skills/connect-me/scripts/verify-connection.mjs +68 -0
- package/skills/how-catalyst-works/SKILL.md +42 -0
- package/skills/how-catalyst-works/agents/openai.yaml +6 -0
- package/skills/how-catalyst-works/agents/portability.yaml +4 -0
- package/skills/how-catalyst-works/references/coding-accounts.md +51 -0
- package/skills/how-catalyst-works/references/stages-and-mapping.md +54 -0
- package/skills/how-catalyst-works/references/the-ladder.md +41 -0
- package/skills/how-catalyst-works/references/what-runs-next.md +77 -0
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +57 -0
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +41 -0
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +163 -0
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +94 -0
- package/skills/how-catalyst-works/scripts/whats-running.mjs +66 -0
- package/skills/run-this-project/SKILL.md +45 -0
- package/skills/run-this-project/agents/openai.yaml +6 -0
- package/skills/run-this-project/agents/portability.yaml +5 -0
- package/skills/run-this-project/assets/stall-policy.json +15 -0
- package/skills/run-this-project/references/making-work-ready.md +60 -0
- package/skills/run-this-project/references/reacting-to-events.md +76 -0
- package/skills/run-this-project/references/stalls-and-escalation.md +63 -0
- package/skills/run-this-project/scripts/lib/cli.mjs +184 -0
- package/skills/run-this-project/scripts/make-ready.mjs +64 -0
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +61 -0
- package/skills/what-needs-me/SKILL.md +41 -0
- package/skills/what-needs-me/agents/openai.yaml +6 -0
- package/skills/what-needs-me/agents/portability.yaml +5 -0
- package/skills/what-needs-me/references/raising-a-decision.md +41 -0
- package/skills/what-needs-me/references/reading-the-inbox.md +38 -0
- package/skills/what-needs-me/references/settling-an-answer.md +37 -0
- package/skills/what-needs-me/scripts/inbox.mjs +47 -0
- package/skills/what-needs-me/scripts/lib/cli.mjs +134 -0
- package/skills/what-needs-me/scripts/raise.mjs +53 -0
- package/skills/what-needs-me/scripts/settle.mjs +73 -0
- package/skills/whats-happening/SKILL.md +43 -0
- package/skills/whats-happening/agents/openai.yaml +6 -0
- package/skills/whats-happening/agents/portability.yaml +4 -0
- package/skills/whats-happening/assets/status-reply.json +77 -0
- package/skills/whats-happening/references/reading-the-board.md +43 -0
- package/skills/whats-happening/references/reprioritising.md +37 -0
- package/skills/whats-happening/references/routing-work.md +36 -0
- package/skills/whats-happening/references/status-reply.md +34 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +57 -0
- package/skills/whats-happening/scripts/explain.mjs +28 -0
- package/skills/whats-happening/scripts/lib/cli.mjs +134 -0
- package/skills/whats-happening/scripts/snapshot.mjs +145 -0
- package/skills/ask/SKILL.md +0 -37
- package/skills/concierge/SKILL.md +0 -41
- package/skills/join/SKILL.md +0 -30
- package/skills/linearis/SKILL.md +0 -40
- package/skills/setup/SKILL.md +0 -27
- package/skills/steward/SKILL.md +0 -30
package/dist/cli.js
CHANGED
|
@@ -1,268 +1,72 @@
|
|
|
1
|
-
// cli.ts —
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
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
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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} —
|
|
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
|
|
231
|
-
" catalyst-skills
|
|
232
|
-
" catalyst-skills
|
|
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
|
-
"
|
|
239
|
-
"
|
|
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
|
|
260
|
-
* recorded, the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
325
|
-
return await
|
|
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
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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(`
|
|
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
|
|
270
|
+
function cmdStatus(ctx) {
|
|
271
|
+
const manifest = readManifest();
|
|
415
272
|
const cfg = loadConfig(ctx.home);
|
|
416
273
|
if (!cfg) {
|
|
417
|
-
ctx.stdout(`Not
|
|
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 {};
|