@catalyst-cloud/catalyst-skills 0.5.0 → 0.8.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/README.md +11 -178
- package/bin/catalyst-skills.js +3 -49
- package/bin/catalyst.js +3 -0
- package/package.json +8 -29
- package/CHANGELOG.md +0 -41
- package/LICENSE +0 -21
- package/dist/args.js +0 -261
- package/dist/ask.js +0 -158
- package/dist/browser.js +0 -20
- package/dist/cli.js +0 -356
- package/dist/config.js +0 -148
- package/dist/contract-types.js +0 -4
- package/dist/contract.js +0 -178
- package/dist/detach.js +0 -10
- package/dist/errors.js +0 -27
- package/dist/events.js +0 -106
- package/dist/execution.js +0 -369
- package/dist/oauth.js +0 -300
- package/dist/prompt.js +0 -35
- package/dist/query.js +0 -225
- package/dist/ready.js +0 -163
- package/dist/release.js +0 -142
- package/dist/replica.js +0 -325
- package/dist/sdk.js +0 -26
- package/dist/skill-shape.js +0 -179
- package/dist/skills.js +0 -78
- package/dist/transport.js +0 -205
- package/dist/ts-deps-loader.js +0 -68
- package/dist/watch/consumer.js +0 -141
- package/dist/watch/cursor-file.js +0 -62
- package/dist/watch.js +0 -175
- package/dist/write.js +0 -224
- package/skills/catalyst-github/SKILL.md +0 -35
- package/skills/catalyst-github/agents/openai.yaml +0 -6
- package/skills/catalyst-github/agents/portability.yaml +0 -4
- package/skills/catalyst-github/references/is-it-mergeable.md +0 -57
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +0 -61
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +0 -124
- package/skills/catalyst-github/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-github/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-github/scripts/lib/pull.mjs +0 -82
- package/skills/catalyst-github/scripts/read-pr.mjs +0 -97
- package/skills/catalyst-linear/SKILL.md +0 -43
- package/skills/catalyst-linear/agents/openai.yaml +0 -6
- package/skills/catalyst-linear/agents/portability.yaml +0 -5
- package/skills/catalyst-linear/references/reading-a-ticket.md +0 -50
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +0 -53
- package/skills/catalyst-linear/references/writing-to-linear.md +0 -43
- package/skills/catalyst-linear/scripts/comment.mjs +0 -59
- package/skills/catalyst-linear/scripts/create-ticket.mjs +0 -44
- package/skills/catalyst-linear/scripts/label.mjs +0 -48
- package/skills/catalyst-linear/scripts/lib/cli.mjs +0 -164
- package/skills/catalyst-linear/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-linear/scripts/move.mjs +0 -41
- package/skills/catalyst-linear/scripts/read-ticket.mjs +0 -93
- package/skills/catalyst-linear/scripts/search.mjs +0 -49
- package/skills/catalyst-setup/SKILL.md +0 -32
- package/skills/catalyst-setup/agents/openai.yaml +0 -6
- package/skills/catalyst-setup/agents/portability.yaml +0 -4
- package/skills/catalyst-setup/references/what-each-check-means.md +0 -66
- package/skills/catalyst-setup/scripts/check.mjs +0 -75
- package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
- package/skills/connect-me/SKILL.md +0 -63
- package/skills/connect-me/agents/openai.yaml +0 -6
- package/skills/connect-me/agents/portability.yaml +0 -5
- package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
- package/skills/connect-me/scripts/lib/cli.mjs +0 -185
- package/skills/connect-me/scripts/lib/credential.mjs +0 -29
- package/skills/connect-me/scripts/verify-connection.mjs +0 -68
- package/skills/how-catalyst-works/SKILL.md +0 -42
- package/skills/how-catalyst-works/agents/openai.yaml +0 -6
- package/skills/how-catalyst-works/agents/portability.yaml +0 -4
- package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
- package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -54
- package/skills/how-catalyst-works/references/the-ladder.md +0 -41
- package/skills/how-catalyst-works/references/what-runs-next.md +0 -77
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
- package/skills/how-catalyst-works/scripts/show-my-map.mjs +0 -94
- package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
- package/skills/run-this-project/SKILL.md +0 -45
- package/skills/run-this-project/agents/openai.yaml +0 -6
- package/skills/run-this-project/agents/portability.yaml +0 -5
- package/skills/run-this-project/assets/stall-policy.json +0 -15
- package/skills/run-this-project/references/making-work-ready.md +0 -60
- package/skills/run-this-project/references/reacting-to-events.md +0 -76
- package/skills/run-this-project/references/stalls-and-escalation.md +0 -63
- package/skills/run-this-project/scripts/lib/cli.mjs +0 -185
- package/skills/run-this-project/scripts/lib/credential.mjs +0 -29
- package/skills/run-this-project/scripts/make-ready.mjs +0 -64
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +0 -61
- package/skills/unstick/SKILL.md +0 -41
- package/skills/unstick/agents/openai.yaml +0 -6
- package/skills/unstick/agents/portability.yaml +0 -5
- package/skills/unstick/references/playbook.md +0 -51
- package/skills/unstick/scripts/lib/cli.mjs +0 -135
- package/skills/unstick/scripts/lib/credential.mjs +0 -29
- package/skills/unstick/scripts/unstick.mjs +0 -57
- package/skills/what-needs-me/SKILL.md +0 -41
- package/skills/what-needs-me/agents/openai.yaml +0 -6
- package/skills/what-needs-me/agents/portability.yaml +0 -5
- package/skills/what-needs-me/references/raising-a-decision.md +0 -41
- package/skills/what-needs-me/references/reading-the-inbox.md +0 -38
- package/skills/what-needs-me/references/settling-an-answer.md +0 -37
- package/skills/what-needs-me/scripts/inbox.mjs +0 -56
- package/skills/what-needs-me/scripts/lib/cli.mjs +0 -135
- package/skills/what-needs-me/scripts/lib/credential.mjs +0 -29
- package/skills/what-needs-me/scripts/raise.mjs +0 -53
- package/skills/what-needs-me/scripts/settle.mjs +0 -73
- package/skills/whats-happening/SKILL.md +0 -43
- package/skills/whats-happening/agents/openai.yaml +0 -6
- package/skills/whats-happening/agents/portability.yaml +0 -4
- package/skills/whats-happening/assets/status-reply.json +0 -77
- package/skills/whats-happening/references/reading-the-board.md +0 -43
- package/skills/whats-happening/references/reprioritising.md +0 -37
- package/skills/whats-happening/references/routing-work.md +0 -36
- package/skills/whats-happening/references/status-reply.md +0 -34
- package/skills/whats-happening/references/why-is-it-stuck.md +0 -62
- package/skills/whats-happening/scripts/explain.mjs +0 -28
- package/skills/whats-happening/scripts/lib/cli.mjs +0 -135
- package/skills/whats-happening/scripts/lib/credential.mjs +0 -29
- package/skills/whats-happening/scripts/snapshot.mjs +0 -145
package/dist/replica.js
DELETED
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
// replica.ts — the optional local replica: `replica start|stop|status|sql|schema`.
|
|
2
|
-
//
|
|
3
|
-
// `status` is the check every skill runs first and it needs no network and no SDK: the pidfile, the
|
|
4
|
-
// writer-lock heartbeat, and the `sync_meta.cursor` row (read through node:sqlite read-only). Exit
|
|
5
|
-
// 0 fresh, 1 present but stale, 2 not configured, 3 absent. `--probe` adds the one network call.
|
|
6
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { DatabaseSync } from "node:sqlite";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import { flagBool, flagInt, flagString, positionals } from "./args.js";
|
|
10
|
-
import { apiBase, loadConfig, replicaDbPath } from "./config.js";
|
|
11
|
-
import { detachSelf } from "./detach.js";
|
|
12
|
-
import { CliError, UsageError } from "./errors.js";
|
|
13
|
-
import { apiClient } from "./transport.js";
|
|
14
|
-
import { authStrategyFor } from "./oauth.js";
|
|
15
|
-
import { loadSdk } from "./sdk.js";
|
|
16
|
-
import { createEventSync } from "./events.js";
|
|
17
|
-
export const DEFAULT_STALE_MS = 15_000;
|
|
18
|
-
export function pidfilePath(dbPath) {
|
|
19
|
-
return `${dbPath}.pid`;
|
|
20
|
-
}
|
|
21
|
-
export function lockPath(dbPath) {
|
|
22
|
-
return `${dbPath}.writer.lock`;
|
|
23
|
-
}
|
|
24
|
-
export function pidAlive(pid) {
|
|
25
|
-
if (pid === null || !Number.isInteger(pid) || pid <= 0)
|
|
26
|
-
return false;
|
|
27
|
-
try {
|
|
28
|
-
process.kill(pid, 0);
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
return err.code === "EPERM";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export function readPidfile(dbPath) {
|
|
36
|
-
const p = pidfilePath(dbPath);
|
|
37
|
-
if (!existsSync(p))
|
|
38
|
-
return null;
|
|
39
|
-
const n = Number(readFileSync(p, "utf8").trim());
|
|
40
|
-
return Number.isInteger(n) ? n : null;
|
|
41
|
-
}
|
|
42
|
-
function readLock(dbPath) {
|
|
43
|
-
try {
|
|
44
|
-
const rec = JSON.parse(readFileSync(lockPath(dbPath), "utf8"));
|
|
45
|
-
if (typeof rec.pid === "number" && typeof rec.heartbeat === "number")
|
|
46
|
-
return { pid: rec.pid, heartbeat: rec.heartbeat };
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** Read `sync_meta.cursor` read-only through node:sqlite; null when the table or row is absent. */
|
|
54
|
-
export function readCursor(dbPath) {
|
|
55
|
-
let db = null;
|
|
56
|
-
try {
|
|
57
|
-
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
58
|
-
const table = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sync_meta'").get();
|
|
59
|
-
if (!table)
|
|
60
|
-
return null;
|
|
61
|
-
const row = db.prepare("SELECT value FROM sync_meta WHERE key = 'cursor'").get();
|
|
62
|
-
if (!row || row.value === null || row.value === undefined || row.value === "")
|
|
63
|
-
return null;
|
|
64
|
-
const n = Number(row.value);
|
|
65
|
-
return Number.isInteger(n) ? n : null;
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
db?.close();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/** The in-process status logic `query` and `ready` reuse. */
|
|
75
|
-
export function replicaStatus(ctx, cfg, opts = {}) {
|
|
76
|
-
const base = {
|
|
77
|
-
verdict: "not-configured",
|
|
78
|
-
exitCode: 2,
|
|
79
|
-
dbPath: null,
|
|
80
|
-
cursor: null,
|
|
81
|
-
heartbeatAgeMs: null,
|
|
82
|
-
lockPid: null,
|
|
83
|
-
pidfilePid: null,
|
|
84
|
-
writerAlive: false,
|
|
85
|
-
reasons: [],
|
|
86
|
-
};
|
|
87
|
-
if (!cfg)
|
|
88
|
-
return { ...base, reasons: ["not connected"] };
|
|
89
|
-
const dbPath = opts.dbPath ?? replicaDbPath(cfg, ctx.home);
|
|
90
|
-
if (!existsSync(dbPath))
|
|
91
|
-
return { ...base, verdict: "absent", exitCode: 3, dbPath, reasons: ["no replica file"] };
|
|
92
|
-
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
93
|
-
const nowMs = opts.nowMs ?? ctx.now().getTime();
|
|
94
|
-
const lock = readLock(dbPath);
|
|
95
|
-
const pidfilePid = readPidfile(dbPath);
|
|
96
|
-
const heartbeatAgeMs = lock ? Math.max(0, nowMs - lock.heartbeat) : null;
|
|
97
|
-
const writerAlive = pidAlive(lock?.pid ?? null) || pidAlive(pidfilePid);
|
|
98
|
-
const cursor = readCursor(dbPath);
|
|
99
|
-
const reasons = [];
|
|
100
|
-
if (!lock)
|
|
101
|
-
reasons.push("no writer lock");
|
|
102
|
-
else if (heartbeatAgeMs !== null && heartbeatAgeMs >= staleMs)
|
|
103
|
-
reasons.push(`heartbeat ${heartbeatAgeMs}ms old (stale after ${staleMs}ms)`);
|
|
104
|
-
if (cursor === null)
|
|
105
|
-
reasons.push("no cursor");
|
|
106
|
-
if (!writerAlive)
|
|
107
|
-
reasons.push("no live writer process");
|
|
108
|
-
const fresh = reasons.length === 0;
|
|
109
|
-
return {
|
|
110
|
-
verdict: fresh ? "fresh" : "stale",
|
|
111
|
-
exitCode: fresh ? 0 : 1,
|
|
112
|
-
dbPath,
|
|
113
|
-
cursor,
|
|
114
|
-
heartbeatAgeMs,
|
|
115
|
-
lockPid: lock?.pid ?? null,
|
|
116
|
-
pidfilePid,
|
|
117
|
-
writerAlive,
|
|
118
|
-
reasons,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
export function statusLine(s) {
|
|
122
|
-
switch (s.verdict) {
|
|
123
|
-
case "not-configured":
|
|
124
|
-
return "replica: not configured — run login first";
|
|
125
|
-
case "absent":
|
|
126
|
-
return `replica: absent at ${s.dbPath} — start it with: catalyst-skills replica start --detach`;
|
|
127
|
-
case "fresh":
|
|
128
|
-
return `replica: fresh at ${s.dbPath} (cursor ${s.cursor}, heartbeat ${s.heartbeatAgeMs}ms ago${s.lag !== undefined ? `, ${s.lag} behind head ${s.head}` : ""})`;
|
|
129
|
-
case "stale":
|
|
130
|
-
return `replica: stale at ${s.dbPath} (${s.reasons.join("; ")}${s.cursor !== null ? `; cursor ${s.cursor}` : ""}) — reads fall back to the API`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/** better-sqlite3 when it resolves and constructs, else node:sqlite — with exactly one stderr line
|
|
134
|
-
* on the fallback so the choice is visible. */
|
|
135
|
-
export async function engineFor(sdk, dbPath, ctx, deps = {}) {
|
|
136
|
-
const requireDriver = deps.requireDriver ?? (() => createRequire(import.meta.url)("better-sqlite3"));
|
|
137
|
-
try {
|
|
138
|
-
const mod = requireDriver();
|
|
139
|
-
const driver = (mod && typeof mod === "object" && "default" in mod ? mod.default : mod);
|
|
140
|
-
if (typeof driver !== "function")
|
|
141
|
-
throw new Error("better-sqlite3 resolved to a non-constructor");
|
|
142
|
-
return deps.readonly ? sdk.betterSqlite3ReadonlyEngine(driver, dbPath) : sdk.betterSqlite3Engine(driver, dbPath);
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
const why = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
146
|
-
ctx.stderr(`[catalyst-skills] better-sqlite3 unavailable (${why}); using node:sqlite`);
|
|
147
|
-
return deps.readonly ? sdk.nodeSqliteReadonlyEngine(dbPath) : sdk.nodeSqliteEngine(dbPath);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
export async function cmdReplica(args, ctx, deps = {}) {
|
|
151
|
-
const [sub, ...rest] = positionals(args);
|
|
152
|
-
if (!sub)
|
|
153
|
-
throw new UsageError("replica needs a subcommand: start | stop | status | sql | schema");
|
|
154
|
-
if (sub === "status")
|
|
155
|
-
return cmdStatus(args, ctx);
|
|
156
|
-
const cfg = loadConfig(ctx.home);
|
|
157
|
-
if (!cfg)
|
|
158
|
-
throw new CliError("not connected yet — run login first", "not-configured");
|
|
159
|
-
const dbPath = flagString(args, "db") ?? replicaDbPath(cfg, ctx.home);
|
|
160
|
-
switch (sub) {
|
|
161
|
-
case "start":
|
|
162
|
-
return cmdStart(args, ctx, cfg, dbPath, deps);
|
|
163
|
-
case "stop":
|
|
164
|
-
return cmdStop(ctx, dbPath);
|
|
165
|
-
case "sql":
|
|
166
|
-
return cmdSql(args, ctx, dbPath, rest.join(" "), deps);
|
|
167
|
-
case "schema":
|
|
168
|
-
return cmdSchema(args, ctx, dbPath, rest[0]);
|
|
169
|
-
default:
|
|
170
|
-
throw new UsageError(`unknown replica subcommand: ${sub}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
async function cmdStatus(args, ctx) {
|
|
174
|
-
let cfg;
|
|
175
|
-
try {
|
|
176
|
-
cfg = loadConfig(ctx.home);
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
cfg = null;
|
|
180
|
-
}
|
|
181
|
-
const status = replicaStatus(ctx, cfg, {
|
|
182
|
-
staleMs: flagInt(args, "stale-ms", DEFAULT_STALE_MS),
|
|
183
|
-
dbPath: flagString(args, "db"),
|
|
184
|
-
});
|
|
185
|
-
if (flagBool(args, "probe") && cfg && status.verdict !== "not-configured") {
|
|
186
|
-
const head = await apiClient(cfg, ctx).getJson("/api/v1/snapshot", { query: { head: 1 } });
|
|
187
|
-
if (typeof head.body.cursor === "number") {
|
|
188
|
-
status.head = head.body.cursor;
|
|
189
|
-
status.lag = status.cursor === null ? head.body.cursor : head.body.cursor - status.cursor;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
ctx.stdout(args.json ? JSON.stringify(status) : statusLine(status));
|
|
193
|
-
return status.exitCode;
|
|
194
|
-
}
|
|
195
|
-
async function cmdStart(args, ctx, cfg, dbPath, deps) {
|
|
196
|
-
if (flagBool(args, "detach")) {
|
|
197
|
-
const argv = (deps.argv ?? process.argv.slice(1)).filter((a) => a !== "--detach");
|
|
198
|
-
const { pid } = (deps.detach ?? detachSelf)(argv, ctx.env);
|
|
199
|
-
writeFileSync(pidfilePath(dbPath), `${pid}\n`);
|
|
200
|
-
ctx.stdout(`replica writer started in the background (pid ${pid}, db ${dbPath}); check with: catalyst-skills replica status`);
|
|
201
|
-
return 0;
|
|
202
|
-
}
|
|
203
|
-
const sdk = await loadSdk();
|
|
204
|
-
const engine = await engineFor(sdk, dbPath, ctx, deps.engineDeps);
|
|
205
|
-
const replica = new sdk.CatalystReplica({
|
|
206
|
-
baseUrl: apiBase(cfg),
|
|
207
|
-
account: cfg.account,
|
|
208
|
-
accountSource: "declared",
|
|
209
|
-
auth: authStrategyFor(ctx, cfg),
|
|
210
|
-
dbPath,
|
|
211
|
-
engine,
|
|
212
|
-
fetchImpl: ctx.fetch,
|
|
213
|
-
wsFactory: deps.wsFactory,
|
|
214
|
-
log: (level, msg, extra) => ctx.stderr(`[replica ${level}] ${msg}${extra ? ` ${safeJson(extra)}` : ""}`),
|
|
215
|
-
onStatus: (s) => ctx.stderr(`[replica] status=${s}`),
|
|
216
|
-
});
|
|
217
|
-
await replica.start();
|
|
218
|
-
const eventSync = await createEventSync(ctx, { loadSdk: deps.loadEventsSdk });
|
|
219
|
-
void eventSync.start().catch((error) => {
|
|
220
|
-
ctx.stderr(`[events] sync failed: ${error instanceof Error ? error.message : String(error)}; replica remains live`);
|
|
221
|
-
});
|
|
222
|
-
ctx.stdout(`replica live at ${dbPath} (cursor ${replica.cursor ?? "none"}); event cache active — Ctrl-C to stop`);
|
|
223
|
-
const wait = deps.waitForStop ??
|
|
224
|
-
(() => new Promise((resolve) => {
|
|
225
|
-
const done = () => resolve();
|
|
226
|
-
process.once("SIGINT", done);
|
|
227
|
-
process.once("SIGTERM", done);
|
|
228
|
-
}));
|
|
229
|
-
await wait();
|
|
230
|
-
await eventSync.stop();
|
|
231
|
-
await replica.close();
|
|
232
|
-
try {
|
|
233
|
-
if (readPidfile(dbPath) === process.pid)
|
|
234
|
-
unlinkSync(pidfilePath(dbPath));
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// pidfile already gone
|
|
238
|
-
}
|
|
239
|
-
ctx.stdout("replica stopped");
|
|
240
|
-
return 0;
|
|
241
|
-
}
|
|
242
|
-
function cmdStop(ctx, dbPath) {
|
|
243
|
-
const pid = readPidfile(dbPath);
|
|
244
|
-
if (pid === null) {
|
|
245
|
-
ctx.stdout(`no pidfile at ${pidfilePath(dbPath)} — nothing to stop (a foreground writer is stopped with Ctrl-C)`);
|
|
246
|
-
return 1;
|
|
247
|
-
}
|
|
248
|
-
if (!pidAlive(pid)) {
|
|
249
|
-
unlinkSync(pidfilePath(dbPath));
|
|
250
|
-
ctx.stdout(`pid ${pid} is not running; removed the stale pidfile`);
|
|
251
|
-
return 1;
|
|
252
|
-
}
|
|
253
|
-
process.kill(pid, "SIGTERM");
|
|
254
|
-
try {
|
|
255
|
-
unlinkSync(pidfilePath(dbPath));
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
// already removed by the writer
|
|
259
|
-
}
|
|
260
|
-
ctx.stdout(`sent SIGTERM to replica writer pid ${pid}`);
|
|
261
|
-
return 0;
|
|
262
|
-
}
|
|
263
|
-
const SELECT_ONLY = /^\s*(select|with)\b/i;
|
|
264
|
-
export function assertSingleSelect(sql) {
|
|
265
|
-
const trimmed = sql.trim().replace(/;\s*$/, "");
|
|
266
|
-
if (!trimmed)
|
|
267
|
-
throw new UsageError('replica sql needs a query: replica sql "select ..."');
|
|
268
|
-
if (!SELECT_ONLY.test(trimmed) || trimmed.includes(";")) {
|
|
269
|
-
throw new UsageError("replica sql runs exactly one SELECT (or WITH ... SELECT); nothing else is accepted on a read-only handle");
|
|
270
|
-
}
|
|
271
|
-
return trimmed;
|
|
272
|
-
}
|
|
273
|
-
async function cmdSql(args, ctx, dbPath, sql, deps) {
|
|
274
|
-
const query = assertSingleSelect(sql);
|
|
275
|
-
if (!existsSync(dbPath))
|
|
276
|
-
throw new CliError(`no replica at ${dbPath} — start it with: catalyst-skills replica start --detach`, "replica-absent", 3);
|
|
277
|
-
const sdk = await loadSdk();
|
|
278
|
-
const engine = await engineFor(sdk, dbPath, ctx, { ...deps.engineDeps, readonly: true });
|
|
279
|
-
const replica = await sdk.CatalystReplica.openReadOnly({ dbPath, engine, log: () => { } });
|
|
280
|
-
try {
|
|
281
|
-
const rows = replica.sql.exec(query).toArray();
|
|
282
|
-
ctx.stdout(JSON.stringify(rows, null, args.json ? 0 : 2));
|
|
283
|
-
return 0;
|
|
284
|
-
}
|
|
285
|
-
finally {
|
|
286
|
-
await replica.close();
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
async function cmdSchema(args, ctx, dbPath, table) {
|
|
290
|
-
if (!existsSync(dbPath))
|
|
291
|
-
throw new CliError(`no replica at ${dbPath} — the schema is what the file holds; start it with: catalyst-skills replica start --detach`, "replica-absent", 3);
|
|
292
|
-
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
293
|
-
try {
|
|
294
|
-
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map((r) => r.name);
|
|
295
|
-
const wanted = table ? tables.filter((t) => t === table) : tables;
|
|
296
|
-
if (table && wanted.length === 0)
|
|
297
|
-
throw new CliError(`no table "${table}" in the replica (tables: ${tables.join(", ")})`, "table-unknown");
|
|
298
|
-
const out = {};
|
|
299
|
-
for (const t of wanted) {
|
|
300
|
-
out[t] = db.prepare(`PRAGMA table_info("${t.replace(/"/g, '""')}")`).all().map((c) => ({
|
|
301
|
-
name: c.name,
|
|
302
|
-
type: c.type,
|
|
303
|
-
notnull: c.notnull,
|
|
304
|
-
pk: c.pk,
|
|
305
|
-
}));
|
|
306
|
-
}
|
|
307
|
-
if (args.json)
|
|
308
|
-
ctx.stdout(JSON.stringify(out));
|
|
309
|
-
else
|
|
310
|
-
for (const [t, cols] of Object.entries(out))
|
|
311
|
-
ctx.stdout(`${t}: ${cols.map((c) => `${c.name} ${c.type || "ANY"}${c.pk ? " PK" : ""}`).join(", ")}`);
|
|
312
|
-
return 0;
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
db.close();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
function safeJson(v) {
|
|
319
|
-
try {
|
|
320
|
-
return JSON.stringify(v);
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
return String(v);
|
|
324
|
-
}
|
|
325
|
-
}
|
package/dist/sdk.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { CliError } from "./errors.js";
|
|
2
|
-
import { installTsDepsLoader } from "./ts-deps-loader.js";
|
|
3
|
-
let cached = null;
|
|
4
|
-
const realImport = () => import("@catalyst-cloud/sdk/node");
|
|
5
|
-
/** Import the SDK's node entry, installing the type-stripping loader first. Cached per process. */
|
|
6
|
-
export function loadSdk(importer = realImport) {
|
|
7
|
-
if (!cached) {
|
|
8
|
-
cached = (async () => {
|
|
9
|
-
const verdict = installTsDepsLoader();
|
|
10
|
-
try {
|
|
11
|
-
return await importer();
|
|
12
|
-
}
|
|
13
|
-
catch (err) {
|
|
14
|
-
cached = null;
|
|
15
|
-
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
16
|
-
const why = verdict.installed ? "" : ` (${verdict.reason})`;
|
|
17
|
-
throw new CliError(`the Catalyst Cloud SDK could not be loaded on Node ${process.version}${why}: ${detail} — this verb needs the SDK; run under Node 22.15 or newer, or under bun`, "sdk-unavailable");
|
|
18
|
-
}
|
|
19
|
-
})();
|
|
20
|
-
}
|
|
21
|
-
return cached;
|
|
22
|
-
}
|
|
23
|
-
/** Test seam: forget the cached import. */
|
|
24
|
-
export function resetSdkCache() {
|
|
25
|
-
cached = null;
|
|
26
|
-
}
|
package/dist/skill-shape.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
// skill-shape.ts — `validateSkillDir(dir)`: every problem with one skill directory, one string each.
|
|
2
|
-
// The rules are the packaging pipeline's, the plugin shape gate's, the cloud validator's, and this
|
|
3
|
-
// bundle's own content prohibitions, so a skill that passes here installs and publishes everywhere.
|
|
4
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
5
|
-
import { basename, join, relative } from "node:path";
|
|
6
|
-
export const PROVENANCE_MARKER = "vendored-from: @catalyst-cloud/catalyst-skills";
|
|
7
|
-
export const MAX_SKILL_LINES = 80;
|
|
8
|
-
export const MAX_REFERENCE_LINES = 150;
|
|
9
|
-
export const MAX_DESCRIPTION_CHARS = 1024;
|
|
10
|
-
/** Strings no customer-facing skill file may contain. */
|
|
11
|
-
export const FORBIDDEN_CONTENT = [
|
|
12
|
-
{ name: "the maintainer tenant (tenant-0)", re: /tenant-0/ },
|
|
13
|
-
// The public bundle repository is `coalesce-labs/catalyst-cloud-skills`, whose name CONTAINS the
|
|
14
|
-
// private repository's, so this one has to end at a word boundary or it bans its own README.
|
|
15
|
-
{ name: "the private catalyst repository (coalesce-labs/catalyst)", re: /coalesce-labs\/catalyst(?![-\w])/ },
|
|
16
|
-
{ name: "the fleet thoughts repository (thoughts/)", re: /thoughts\// },
|
|
17
|
-
{ name: "an internal ticket id", re: /\bC[TL]C-\d+\b/ },
|
|
18
|
-
{ name: "a Linear CLI name (linearis)", re: /linearis/i },
|
|
19
|
-
{ name: "the internal replica tool name (catalyst-replica)", re: /catalyst-replica/ },
|
|
20
|
-
];
|
|
21
|
-
function walk(dir) {
|
|
22
|
-
const out = [];
|
|
23
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
-
const p = join(dir, e.name);
|
|
25
|
-
if (e.isDirectory())
|
|
26
|
-
out.push(...walk(p));
|
|
27
|
-
else
|
|
28
|
-
out.push(p);
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
/** A flat `key: value` parser with block-scalar support for one key spanning indented lines. */
|
|
33
|
-
export function parseFrontmatter(lines) {
|
|
34
|
-
if (lines[0] !== "---")
|
|
35
|
-
return null;
|
|
36
|
-
const fields = {};
|
|
37
|
-
let closeIndex = -1;
|
|
38
|
-
let current = null;
|
|
39
|
-
for (let i = 1; i < lines.length; i++) {
|
|
40
|
-
const line = lines[i];
|
|
41
|
-
if (line === "---") {
|
|
42
|
-
closeIndex = i;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
const m = /^([A-Za-z][A-Za-z0-9_-]*):\s?(.*)$/.exec(line);
|
|
46
|
-
if (m && !/^\s/.test(line)) {
|
|
47
|
-
current = m[1];
|
|
48
|
-
const v = m[2].trim();
|
|
49
|
-
fields[current] = v === "|" || v === ">" || v === "|-" || v === ">-" ? "" : v.replace(/^"(.*)"$/, "$1");
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (current && /^\s+\S/.test(line)) {
|
|
53
|
-
fields[current] = `${fields[current]}${fields[current] ? " " : ""}${line.trim()}`;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return closeIndex === -1 ? { fields, closeIndex } : { fields, closeIndex };
|
|
57
|
-
}
|
|
58
|
-
const IMPORT_RE = /(?:from\s+|import\s*\(\s*|require\s*\(\s*)["']([^"']+)["']/g;
|
|
59
|
-
export function validateSkillDir(dir) {
|
|
60
|
-
const problems = [];
|
|
61
|
-
const name = basename(dir);
|
|
62
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
63
|
-
return [`${dir} is not a directory`];
|
|
64
|
-
const skillMd = join(dir, "SKILL.md");
|
|
65
|
-
if (!existsSync(skillMd))
|
|
66
|
-
return [`${name}: SKILL.md is missing`];
|
|
67
|
-
const md = readFileSync(skillMd, "utf8");
|
|
68
|
-
const lines = md.split("\n");
|
|
69
|
-
if (lines.at(-1) === "")
|
|
70
|
-
lines.pop();
|
|
71
|
-
if (lines.length > MAX_SKILL_LINES)
|
|
72
|
-
problems.push(`${name}: SKILL.md is ${lines.length} lines (max ${MAX_SKILL_LINES})`);
|
|
73
|
-
const fm = parseFrontmatter(lines);
|
|
74
|
-
let mutatingFrontmatter = false;
|
|
75
|
-
if (!fm)
|
|
76
|
-
problems.push(`${name}: SKILL.md must start with YAML frontmatter (---)`);
|
|
77
|
-
else {
|
|
78
|
-
if (fm.closeIndex === -1)
|
|
79
|
-
problems.push(`${name}: SKILL.md is missing a closing frontmatter fence`);
|
|
80
|
-
if (!fm.fields.name)
|
|
81
|
-
problems.push(`${name}: frontmatter is missing "name"`);
|
|
82
|
-
else if (fm.fields.name !== name)
|
|
83
|
-
problems.push(`${name}: frontmatter name "${fm.fields.name}" must match the directory`);
|
|
84
|
-
if (fm.fields.description === undefined || fm.fields.description.trim() === "")
|
|
85
|
-
problems.push(`${name}: frontmatter is missing "description"`);
|
|
86
|
-
else if (fm.fields.description.length > MAX_DESCRIPTION_CHARS)
|
|
87
|
-
problems.push(`${name}: description is ${fm.fields.description.length} characters (max ${MAX_DESCRIPTION_CHARS})`);
|
|
88
|
-
mutatingFrontmatter = fm.fields["disable-model-invocation"] === "true";
|
|
89
|
-
if (fm.closeIndex !== -1) {
|
|
90
|
-
const after = lines[fm.closeIndex + 1] ?? "";
|
|
91
|
-
if (!(after.startsWith("<!--") && after.includes(PROVENANCE_MARKER))) {
|
|
92
|
-
problems.push(`${name}: the line after the frontmatter must be the HTML provenance comment containing "${PROVENANCE_MARKER}"`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const refsDir = join(dir, "references");
|
|
97
|
-
if (existsSync(refsDir)) {
|
|
98
|
-
const refs = readdirSync(refsDir).filter((f) => f.endsWith(".md")).sort();
|
|
99
|
-
if (refs.length === 0)
|
|
100
|
-
problems.push(`${name}: references/ is empty`);
|
|
101
|
-
const table = /^## Load on demand\s*$/m.test(md);
|
|
102
|
-
if (refs.length > 0 && !table)
|
|
103
|
-
problems.push(`${name}: SKILL.md has no "## Load on demand" table`);
|
|
104
|
-
for (const f of refs) {
|
|
105
|
-
const body = readFileSync(join(refsDir, f), "utf8").split("\n");
|
|
106
|
-
if (body.at(-1) === "")
|
|
107
|
-
body.pop();
|
|
108
|
-
if (body.length > MAX_REFERENCE_LINES)
|
|
109
|
-
problems.push(`${name}: references/${f} is ${body.length} lines (max ${MAX_REFERENCE_LINES})`);
|
|
110
|
-
if (!md.includes(`references/${f}`))
|
|
111
|
-
problems.push(`${name}: references/${f} is not linked from SKILL.md by its literal path`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const scriptsDir = join(dir, "scripts");
|
|
115
|
-
if (existsSync(scriptsDir)) {
|
|
116
|
-
for (const p of walk(scriptsDir).filter((f) => f.endsWith(".mjs"))) {
|
|
117
|
-
const rel = relative(dir, p);
|
|
118
|
-
const src = readFileSync(p, "utf8");
|
|
119
|
-
if (!src.startsWith("#!/usr/bin/env node"))
|
|
120
|
-
problems.push(`${name}: ${rel} must start with #!/usr/bin/env node`);
|
|
121
|
-
if (!src.includes("--help"))
|
|
122
|
-
problems.push(`${name}: ${rel} has no --help`);
|
|
123
|
-
for (const m of src.matchAll(IMPORT_RE)) {
|
|
124
|
-
const spec = m[1];
|
|
125
|
-
if (!(spec.startsWith("node:") || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))) {
|
|
126
|
-
problems.push(`${name}: ${rel} imports "${spec}"; scripts may import only node: modules and relative files`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const portability = join(dir, "agents", "portability.yaml");
|
|
132
|
-
const openai = join(dir, "agents", "openai.yaml");
|
|
133
|
-
let mutating = false;
|
|
134
|
-
let implicitFalse = false;
|
|
135
|
-
if (!existsSync(portability))
|
|
136
|
-
problems.push(`${name}: agents/portability.yaml is missing`);
|
|
137
|
-
else {
|
|
138
|
-
const y = readFileSync(portability, "utf8");
|
|
139
|
-
if (!/^effects:\s*.*$/m.test(y))
|
|
140
|
-
problems.push(`${name}: agents/portability.yaml does not declare "effects"`);
|
|
141
|
-
if (!/^exposure:\s*\[\s*"?catalog"?\s*\]\s*$/m.test(y))
|
|
142
|
-
problems.push(`${name}: agents/portability.yaml must declare exposure: [catalog]`);
|
|
143
|
-
mutating = /^mutating:\s*true\s*$/m.test(y);
|
|
144
|
-
}
|
|
145
|
-
if (!existsSync(openai))
|
|
146
|
-
problems.push(`${name}: agents/openai.yaml is missing`);
|
|
147
|
-
else {
|
|
148
|
-
const y = readFileSync(openai, "utf8");
|
|
149
|
-
if (!/^policy:\s*$/m.test(y))
|
|
150
|
-
problems.push(`${name}: agents/openai.yaml has no "policy:" block`);
|
|
151
|
-
if (!/allow_implicit_invocation:\s*(true|false)/.test(y))
|
|
152
|
-
problems.push(`${name}: agents/openai.yaml does not set policy.allow_implicit_invocation`);
|
|
153
|
-
implicitFalse = /allow_implicit_invocation:\s*false/.test(y);
|
|
154
|
-
}
|
|
155
|
-
if (mutating || mutatingFrontmatter || implicitFalse) {
|
|
156
|
-
if (!mutating)
|
|
157
|
-
problems.push(`${name}: disable-model-invocation / allow_implicit_invocation: false are set but agents/portability.yaml lacks mutating: true`);
|
|
158
|
-
if (!mutatingFrontmatter)
|
|
159
|
-
problems.push(`${name}: is mutating but SKILL.md does not set disable-model-invocation: true`);
|
|
160
|
-
if (!implicitFalse)
|
|
161
|
-
problems.push(`${name}: is mutating but agents/openai.yaml does not set policy.allow_implicit_invocation: false`);
|
|
162
|
-
}
|
|
163
|
-
for (const p of walk(dir)) {
|
|
164
|
-
const rel = relative(dir, p);
|
|
165
|
-
if (/\.log$/i.test(rel))
|
|
166
|
-
problems.push(`${name}: ${rel} is a log file; no *.log may ship in a skill`);
|
|
167
|
-
let text;
|
|
168
|
-
try {
|
|
169
|
-
text = readFileSync(p, "utf8");
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
for (const f of FORBIDDEN_CONTENT)
|
|
175
|
-
if (f.re.test(text))
|
|
176
|
-
problems.push(`${name}: ${rel} mentions ${f.name}`);
|
|
177
|
-
}
|
|
178
|
-
return problems;
|
|
179
|
-
}
|
package/dist/skills.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// skills.ts — copying the bundled skills into the user's skills directory, and the one-line update
|
|
2
|
-
// notice a new version prints on its next session.
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { PACKAGE_NAME, defaultSkillsDirFor } from "./config.js";
|
|
7
|
-
import { PROVENANCE_MARKER } from "./skill-shape.js";
|
|
8
|
-
export function skillsSourceDir() {
|
|
9
|
-
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Copy every `skills/<name>/` that has a SKILL.md. A target directory whose SKILL.md lacks the
|
|
13
|
-
* provenance marker was not installed by this package and is skipped unless `force`.
|
|
14
|
-
*
|
|
15
|
-
* `onlyExisting` is the update path: it refreshes copies this package already made and creates
|
|
16
|
-
* none. That is what keeps the update notice from planting a second copy of every skill beside a
|
|
17
|
-
* set the customer's agent installed as a plugin.
|
|
18
|
-
*/
|
|
19
|
-
export function installSkills(targetDir, opts, sourceDir = skillsSourceDir()) {
|
|
20
|
-
const result = { installed: [], skipped: [] };
|
|
21
|
-
if (existsSync(targetDir) && !statSync(targetDir).isDirectory()) {
|
|
22
|
-
throw new Error(`${targetDir} is not a directory`);
|
|
23
|
-
}
|
|
24
|
-
const names = readdirSync(sourceDir, { withFileTypes: true })
|
|
25
|
-
.filter((e) => e.isDirectory())
|
|
26
|
-
.map((e) => e.name)
|
|
27
|
-
.sort();
|
|
28
|
-
for (const name of names) {
|
|
29
|
-
const src = join(sourceDir, name);
|
|
30
|
-
if (!existsSync(join(src, "SKILL.md")))
|
|
31
|
-
continue;
|
|
32
|
-
const dst = join(targetDir, name);
|
|
33
|
-
const existingMd = join(dst, "SKILL.md");
|
|
34
|
-
if (opts.onlyExisting && !existsSync(existingMd))
|
|
35
|
-
continue;
|
|
36
|
-
if (existsSync(existingMd) && !opts.force) {
|
|
37
|
-
const existing = readFileSync(existingMd, "utf8");
|
|
38
|
-
if (!existing.includes(PROVENANCE_MARKER)) {
|
|
39
|
-
result.skipped.push({ name, reason: "foreign-skill-dir" });
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
mkdirSync(dst, { recursive: true });
|
|
44
|
-
cpSync(src, dst, { recursive: true });
|
|
45
|
-
result.installed.push(name);
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
export function resolveSkillsDir(args, ctx, cfg) {
|
|
50
|
-
return args.skillsDir ?? ctx.env.CATALYST_SKILLS_CLAUDE_DIR ?? cfg?.skillsDir ?? defaultSkillsDirFor(ctx.home);
|
|
51
|
-
}
|
|
52
|
-
export function parseChangelogEntry(changelog, version) {
|
|
53
|
-
const lines = changelog.split("\n");
|
|
54
|
-
const heading = `## ${version}`;
|
|
55
|
-
const start = lines.findIndex((l) => l.trim() === heading);
|
|
56
|
-
if (start === -1)
|
|
57
|
-
return null;
|
|
58
|
-
for (const line of lines.slice(start + 1)) {
|
|
59
|
-
const t = line.trim();
|
|
60
|
-
if (t === "")
|
|
61
|
-
continue;
|
|
62
|
-
if (t.startsWith("#"))
|
|
63
|
-
break;
|
|
64
|
-
return t;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
export function readChangelog() {
|
|
69
|
-
return readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8");
|
|
70
|
-
}
|
|
71
|
-
export function updateNoticeLine(previous, current, entry) {
|
|
72
|
-
const summary = entry ?? "see CHANGELOG.md";
|
|
73
|
-
// `npm update -g` treats a global install as a caret range, and a caret below 1.0.0 never moves a
|
|
74
|
-
// minor — so it never crosses 0.2.x → 0.3.0. Pin @latest via `npm install`, THEN re-run login: the
|
|
75
|
-
// install alone does not rewrite customer.json.cliPath, so the skill helpers would keep spawning the
|
|
76
|
-
// stale recorded bundle and repeat this notice forever; the new global bin records its own path.
|
|
77
|
-
return `[catalyst-skills] updated ${previous} → ${current}: ${summary} · update with: npm install -g ${PACKAGE_NAME}@latest && catalyst-skills login`;
|
|
78
|
-
}
|