@cavuno/board 1.28.1 → 1.30.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/dist/bin.mjs +636 -58
- package/dist/filters.d.mts +1 -1
- package/dist/filters.d.ts +1 -1
- package/dist/format.d.mts +1 -1
- package/dist/format.d.ts +1 -1
- package/dist/index.d.mts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{jobs-DK5mPBgq.d.mts → jobs-lzeoVGY6.d.mts} +2 -0
- package/dist/{jobs-DK5mPBgq.d.ts → jobs-lzeoVGY6.d.ts} +2 -0
- package/dist/{salaries-CXt6Vkrp.d.ts → salaries-Cy3MhZov.d.ts} +1 -1
- package/dist/{salaries-CrJsaZe6.d.mts → salaries-DpH5CxdM.d.mts} +1 -1
- package/dist/seo.d.mts +2 -2
- package/dist/seo.d.ts +2 -2
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/sitemap.d.mts +2 -2
- package/dist/sitemap.d.ts +2 -2
- package/dist/skills-types.d.mts +20 -0
- package/dist/skills-types.d.ts +20 -0
- package/dist/skills-types.js +18 -0
- package/dist/skills-types.mjs +0 -0
- package/dist/skills.d.mts +15 -8
- package/dist/skills.d.ts +15 -8
- package/dist/skills.js +10 -7
- package/dist/skills.mjs +10 -7
- package/package.json +11 -1
- package/skills/cavuno-board-smoke-test/SKILL.md +28 -0
- package/skills/manifest.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res) => function __init() {
|
|
4
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
-
};
|
|
6
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
7
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
8
|
-
};
|
|
9
2
|
|
|
10
3
|
// src/skills.ts
|
|
11
4
|
import { readFileSync } from "fs";
|
|
@@ -14,26 +7,580 @@ import { fileURLToPath } from "url";
|
|
|
14
7
|
function packageRoot() {
|
|
15
8
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
9
|
}
|
|
17
|
-
function resolveFromPackageRoot(relativePath) {
|
|
18
|
-
return resolve(packageRoot(), relativePath);
|
|
10
|
+
function resolveFromPackageRoot(relativePath, baseDir) {
|
|
11
|
+
return resolve(baseDir ?? packageRoot(), relativePath);
|
|
19
12
|
}
|
|
20
|
-
function loadSkillManifest() {
|
|
21
|
-
const manifestPath = resolveFromPackageRoot("skills/manifest.json");
|
|
13
|
+
function loadSkillManifest(baseDir) {
|
|
14
|
+
const manifestPath = resolveFromPackageRoot("skills/manifest.json", baseDir);
|
|
22
15
|
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
23
16
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
function loadSkillCorpus(baseDir) {
|
|
18
|
+
const manifest = loadSkillManifest(baseDir);
|
|
19
|
+
return {
|
|
20
|
+
version: manifest.version,
|
|
21
|
+
skills: manifest.skills.map((skill) => ({
|
|
22
|
+
...skill,
|
|
23
|
+
content: readFileSync(
|
|
24
|
+
resolveFromPackageRoot(skill.path, baseDir),
|
|
25
|
+
"utf8"
|
|
26
|
+
)
|
|
27
|
+
}))
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/doctor/checks.ts
|
|
32
|
+
function record(id, tier) {
|
|
33
|
+
return (status, detail) => ({
|
|
34
|
+
id,
|
|
35
|
+
tier,
|
|
36
|
+
status,
|
|
37
|
+
detail
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
var ENV_API_URL = record("env.api-url", 1);
|
|
41
|
+
var ENV_BOARD_KEY = record("env.board-key", 1);
|
|
42
|
+
var PK_RE = /^pk_[0-9a-f]{32}$/;
|
|
43
|
+
function checkEnv(env) {
|
|
44
|
+
const results = [];
|
|
45
|
+
if (!env.apiUrl) {
|
|
46
|
+
results.push(ENV_API_URL("fail", "PUBLIC_CAVUNO_API_URL is not set"));
|
|
47
|
+
} else {
|
|
48
|
+
let origin = null;
|
|
49
|
+
try {
|
|
50
|
+
origin = new URL(env.apiUrl).origin;
|
|
51
|
+
} catch {
|
|
52
|
+
origin = null;
|
|
53
|
+
}
|
|
54
|
+
results.push(
|
|
55
|
+
origin ? ENV_API_URL("pass", origin) : ENV_API_URL(
|
|
56
|
+
"fail",
|
|
57
|
+
`PUBLIC_CAVUNO_API_URL is not a valid URL: ${env.apiUrl}`
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!env.boardKey) {
|
|
62
|
+
results.push(ENV_BOARD_KEY("fail", "PUBLIC_CAVUNO_BOARD is not set"));
|
|
63
|
+
} else {
|
|
64
|
+
results.push(
|
|
65
|
+
PK_RE.test(env.boardKey) ? ENV_BOARD_KEY("pass", "pk_ key") : ENV_BOARD_KEY(
|
|
66
|
+
"fail",
|
|
67
|
+
"PUBLIC_CAVUNO_BOARD must be a publishable key (pk_ + 32 hex chars)"
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
var JOB_DETAIL_LINK_RE = /href="(\/companies\/[a-z0-9-]+\/jobs\/[a-z0-9-]+)"/i;
|
|
74
|
+
function extractJobDetailLink(html) {
|
|
75
|
+
return html.match(JOB_DETAIL_LINK_RE)?.[1] ?? null;
|
|
76
|
+
}
|
|
77
|
+
var JSON_LD_RE = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
|
|
78
|
+
function extractJobPostingJsonLd(html) {
|
|
79
|
+
for (const match of html.matchAll(JSON_LD_RE)) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(match[1].trim());
|
|
82
|
+
if (parsed["@type"] === "JobPosting") return parsed;
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
var LOC_RE = /<loc>([^<]+)<\/loc>/gi;
|
|
89
|
+
function parseSitemap(xml) {
|
|
90
|
+
const kind = /<urlset[\s>]/i.test(xml) ? "urlset" : /<sitemapindex[\s>]/i.test(xml) ? "index" : null;
|
|
91
|
+
if (!kind) return null;
|
|
92
|
+
return { kind, urls: [...xml.matchAll(LOC_RE)].map((m) => m[1].trim()) };
|
|
93
|
+
}
|
|
94
|
+
function summarize(results) {
|
|
95
|
+
const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
|
|
96
|
+
const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
|
|
97
|
+
const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
|
|
98
|
+
return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/doctor/probe.ts
|
|
102
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
103
|
+
async function probe(fetchImpl, url) {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetchImpl(url, {
|
|
106
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
ok: response.ok,
|
|
110
|
+
status: response.status,
|
|
111
|
+
body: await response.text()
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
status: 0,
|
|
117
|
+
body: error instanceof Error ? error.message : String(error)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/doctor/read.ts
|
|
123
|
+
var READ = {
|
|
124
|
+
home: record("read.home", 2),
|
|
125
|
+
jobs: record("read.jobs", 2),
|
|
126
|
+
jsonld: record("read.jsonld", 2),
|
|
127
|
+
sitemap: record("read.sitemap", 2),
|
|
128
|
+
robots: record("read.robots", 2)
|
|
129
|
+
};
|
|
130
|
+
function skipReadProbes(reason) {
|
|
131
|
+
return Object.values(READ).map((make) => make("skip", reason));
|
|
132
|
+
}
|
|
133
|
+
async function probeJobsAndJsonLd(fetchImpl, base) {
|
|
134
|
+
const jobs = await probe(fetchImpl, `${base}/jobs`);
|
|
135
|
+
const jobLink = jobs.ok ? extractJobDetailLink(jobs.body) : null;
|
|
136
|
+
const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
|
|
137
|
+
"fail",
|
|
138
|
+
jobs.ok ? "listing renders but contains no /companies/{c}/jobs/{s} detail links" : `listing HTTP ${jobs.status}`
|
|
139
|
+
);
|
|
140
|
+
if (!jobLink) {
|
|
141
|
+
return [
|
|
142
|
+
jobsResult,
|
|
143
|
+
READ.jsonld(
|
|
144
|
+
"skip",
|
|
145
|
+
jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" : "not probed \u2014 the /jobs listing itself failed (see read.jobs failure)"
|
|
146
|
+
)
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
const detail = await probe(fetchImpl, `${base}${jobLink}`);
|
|
150
|
+
const posting = detail.ok ? extractJobPostingJsonLd(detail.body) : null;
|
|
151
|
+
return [
|
|
152
|
+
jobsResult,
|
|
153
|
+
posting ? READ.jsonld(
|
|
154
|
+
"pass",
|
|
155
|
+
`JobPosting JSON-LD present (${String(posting.title ?? "")})`
|
|
156
|
+
) : READ.jsonld(
|
|
157
|
+
"fail",
|
|
158
|
+
detail.ok ? "job detail page has no parseable JobPosting JSON-LD" : `job detail HTTP ${detail.status}`
|
|
159
|
+
)
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
function onFrontend(base, locUrl) {
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(locUrl);
|
|
165
|
+
return `${base}${parsed.pathname}${parsed.search}`;
|
|
166
|
+
} catch {
|
|
167
|
+
return locUrl;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function probeSitemap(fetchImpl, base) {
|
|
171
|
+
const sitemap = await probe(fetchImpl, `${base}/sitemap.xml`);
|
|
172
|
+
if (!sitemap.ok) {
|
|
173
|
+
return READ.sitemap("fail", `sitemap.xml HTTP ${sitemap.status}`);
|
|
174
|
+
}
|
|
175
|
+
const doc = parseSitemap(sitemap.body);
|
|
176
|
+
if (!doc || doc.urls.length === 0) {
|
|
177
|
+
return READ.sitemap(
|
|
178
|
+
"fail",
|
|
179
|
+
doc ? "sitemap.xml has no <loc> entries" : "sitemap.xml is not a sitemap"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
let urls = doc.urls;
|
|
183
|
+
if (doc.kind === "index") {
|
|
184
|
+
const child = await probe(fetchImpl, onFrontend(base, urls[0]));
|
|
185
|
+
const childDoc = child.ok ? parseSitemap(child.body) : null;
|
|
186
|
+
if (!childDoc || childDoc.urls.length === 0) {
|
|
187
|
+
return READ.sitemap(
|
|
188
|
+
"fail",
|
|
189
|
+
`child sitemap ${urls[0]} ${child.ok ? "has no <loc> entries" : `HTTP ${child.status}`}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
urls = childDoc.urls;
|
|
193
|
+
}
|
|
194
|
+
const sampleUrl = onFrontend(base, urls[0]);
|
|
195
|
+
const sample = await probe(fetchImpl, sampleUrl);
|
|
196
|
+
return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
|
|
197
|
+
}
|
|
198
|
+
async function runReadProbes(fetchImpl, frontendUrl) {
|
|
199
|
+
const base = frontendUrl.replace(/\/$/, "");
|
|
200
|
+
const [home, jobsAndJsonLd, sitemap, robots] = await Promise.all([
|
|
201
|
+
probe(fetchImpl, base),
|
|
202
|
+
probeJobsAndJsonLd(fetchImpl, base),
|
|
203
|
+
probeSitemap(fetchImpl, base),
|
|
204
|
+
probe(fetchImpl, `${base}/robots.txt`)
|
|
205
|
+
]);
|
|
206
|
+
return [
|
|
207
|
+
home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
|
|
208
|
+
"fail",
|
|
209
|
+
home.ok ? "home returned 200 but no HTML document \u2014 captive portal or empty shell?" : `home HTTP ${home.status}`
|
|
210
|
+
),
|
|
211
|
+
...jobsAndJsonLd,
|
|
212
|
+
sitemap,
|
|
213
|
+
robots.ok && /user-agent\s*:/i.test(robots.body) ? READ.robots("pass", "present") : READ.robots(
|
|
214
|
+
"fail",
|
|
215
|
+
robots.ok ? "robots.txt returned 200 but has no User-agent directive" : `robots.txt HTTP ${robots.status}`
|
|
216
|
+
)
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/doctor/writes.ts
|
|
221
|
+
var WRITE = {
|
|
222
|
+
board: record("write.board", 3),
|
|
223
|
+
register: record("write.register", 3),
|
|
224
|
+
login: record("write.login", 3),
|
|
225
|
+
email: record("write.email", 3),
|
|
226
|
+
apply: record("write.apply", 3),
|
|
227
|
+
alert: record("write.alert", 3)
|
|
228
|
+
};
|
|
229
|
+
var DEPENDENT_CHECKS = [
|
|
230
|
+
WRITE.register,
|
|
231
|
+
WRITE.login,
|
|
232
|
+
WRITE.email,
|
|
233
|
+
WRITE.apply,
|
|
234
|
+
WRITE.alert
|
|
235
|
+
];
|
|
236
|
+
function skipWriteProbes(reason) {
|
|
237
|
+
return Object.values(WRITE).map((make) => make("skip", reason));
|
|
238
|
+
}
|
|
239
|
+
var FETCH_TIMEOUT_MS2 = 1e4;
|
|
240
|
+
var RESEND_API_BASE = "https://api.resend.com";
|
|
241
|
+
async function request(fetchImpl, url, init) {
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetchImpl(url, {
|
|
244
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2),
|
|
245
|
+
...init
|
|
246
|
+
});
|
|
247
|
+
let json = null;
|
|
248
|
+
try {
|
|
249
|
+
json = await response.json();
|
|
250
|
+
} catch {
|
|
251
|
+
json = null;
|
|
252
|
+
}
|
|
253
|
+
return { ok: response.ok, status: response.status, json };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
status: 0,
|
|
258
|
+
json: { error: error instanceof Error ? error.message : String(error) }
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function post(fetchImpl, url, body, bearer) {
|
|
263
|
+
return request(fetchImpl, url, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: {
|
|
266
|
+
"content-type": "application/json",
|
|
267
|
+
accept: "application/json",
|
|
268
|
+
...bearer ? { authorization: `Bearer ${bearer}` } : {}
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify(body)
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
274
|
+
async function observeVerificationEmail(fetchImpl, apiKey, recipient, pollMs, timeoutMs) {
|
|
275
|
+
const deadline = Date.now() + timeoutMs;
|
|
276
|
+
const subjectMarker = `[To: ${recipient}]`;
|
|
277
|
+
do {
|
|
278
|
+
const list = await request(
|
|
279
|
+
fetchImpl,
|
|
280
|
+
`${RESEND_API_BASE}/emails?limit=20`,
|
|
281
|
+
{
|
|
282
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
if (!list.ok) {
|
|
286
|
+
return WRITE.email(
|
|
287
|
+
"fail",
|
|
288
|
+
`Resend list returned HTTP ${list.status} \u2014 wrong RESEND_API_KEY?`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const items = list.json?.data ?? [];
|
|
292
|
+
const match = items.find(
|
|
293
|
+
(item) => item.subject?.includes(subjectMarker) || item.to?.includes(recipient)
|
|
294
|
+
);
|
|
295
|
+
if (match) {
|
|
296
|
+
return WRITE.email(
|
|
297
|
+
"pass",
|
|
298
|
+
`verification send observed in Resend for ${recipient}`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
await sleep(pollMs);
|
|
302
|
+
} while (Date.now() < deadline);
|
|
303
|
+
return WRITE.email(
|
|
304
|
+
"fail",
|
|
305
|
+
`no verification send for ${recipient} appeared in Resend within ${timeoutMs}ms`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
async function runWriteProbes(options) {
|
|
309
|
+
const { fetchImpl } = options;
|
|
310
|
+
const origin = new URL(options.env.apiUrl).origin;
|
|
311
|
+
const base = `${origin}/v1/boards/${encodeURIComponent(options.env.boardKey)}`;
|
|
312
|
+
const nonce = options.nonce ?? `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
313
|
+
const email = `doctor+${nonce}@cavuno.com`;
|
|
314
|
+
const password = `doctor-probe-${nonce}-A1!`;
|
|
315
|
+
let context = options.boardContext ?? null;
|
|
316
|
+
if (context === null) {
|
|
317
|
+
const r = await request(fetchImpl, base);
|
|
318
|
+
if (!r.ok) {
|
|
319
|
+
return [
|
|
320
|
+
WRITE.board("fail", `board context did not resolve (HTTP ${r.status})`),
|
|
321
|
+
...DEPENDENT_CHECKS.map(
|
|
322
|
+
(make) => make("skip", "not run \u2014 the write gate failed (see write.board)")
|
|
323
|
+
)
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
context = r.json ?? {};
|
|
327
|
+
}
|
|
328
|
+
if (context.sandbox !== true) {
|
|
329
|
+
return [
|
|
330
|
+
WRITE.board(
|
|
331
|
+
"fail",
|
|
332
|
+
"refusing write probes: this board is not the platform sandbox (`sandbox` is not true) \u2014 point PUBLIC_CAVUNO_BOARD at the sandbox pk_"
|
|
333
|
+
),
|
|
334
|
+
...DEPENDENT_CHECKS.map(
|
|
335
|
+
(make) => make("skip", "not run \u2014 the write gate failed (see write.board)")
|
|
336
|
+
)
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
const results = [
|
|
340
|
+
WRITE.board("pass", "board is the platform sandbox")
|
|
341
|
+
];
|
|
342
|
+
const registered = await post(fetchImpl, `${base}/auth/register`, {
|
|
343
|
+
role: "candidate",
|
|
344
|
+
method: "emailpass",
|
|
345
|
+
email,
|
|
346
|
+
password,
|
|
347
|
+
displayName: "Doctor Probe"
|
|
348
|
+
});
|
|
349
|
+
const sessionOk = (r) => r.ok && typeof r.json?.accessToken === "string";
|
|
350
|
+
results.push(
|
|
351
|
+
sessionOk(registered) ? WRITE.register("pass", `candidate registered (${email})`) : WRITE.register(
|
|
352
|
+
"fail",
|
|
353
|
+
`register returned HTTP ${registered.status}` + (registered.status === 429 ? " \u2014 rate-limited: register allows 5 signups / 15 min per IP; wait before rerunning" : "")
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
let accessToken = null;
|
|
357
|
+
if (sessionOk(registered)) {
|
|
358
|
+
const login = await post(fetchImpl, `${base}/auth/login`, {
|
|
359
|
+
email,
|
|
360
|
+
password
|
|
361
|
+
});
|
|
362
|
+
if (sessionOk(login)) {
|
|
363
|
+
accessToken = login.json.accessToken;
|
|
364
|
+
results.push(WRITE.login("pass", "emailpass login returns a session"));
|
|
365
|
+
} else {
|
|
366
|
+
results.push(WRITE.login("fail", `login returned HTTP ${login.status}`));
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
results.push(WRITE.login("skip", "not run \u2014 register failed"));
|
|
370
|
+
}
|
|
371
|
+
if (!sessionOk(registered)) {
|
|
372
|
+
results.push(WRITE.email("skip", "not run \u2014 register failed"));
|
|
373
|
+
} else if (!options.resendApiKey) {
|
|
374
|
+
results.push(
|
|
375
|
+
WRITE.email(
|
|
376
|
+
"skip",
|
|
377
|
+
"RESEND_API_KEY not set \u2014 the verification send was NOT verified (platform-operator check)"
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
results.push(
|
|
382
|
+
await observeVerificationEmail(
|
|
383
|
+
fetchImpl,
|
|
384
|
+
options.resendApiKey,
|
|
385
|
+
email,
|
|
386
|
+
options.emailPollMs ?? 2e3,
|
|
387
|
+
options.emailTimeoutMs ?? 3e4
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (!accessToken) {
|
|
392
|
+
results.push(WRITE.apply("skip", "not run \u2014 no session (see above)"));
|
|
393
|
+
} else {
|
|
394
|
+
const listing = await request(fetchImpl, `${base}/jobs?limit=1`);
|
|
395
|
+
const card = listing.json?.data?.[0];
|
|
396
|
+
if (!listing.ok || !card?.slug) {
|
|
397
|
+
results.push(
|
|
398
|
+
WRITE.apply(
|
|
399
|
+
"fail",
|
|
400
|
+
listing.ok ? "the board has no jobs to apply to \u2014 is the sandbox seeded?" : `jobs listing returned HTTP ${listing.status}`
|
|
401
|
+
)
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
const applied = await post(
|
|
405
|
+
fetchImpl,
|
|
406
|
+
`${base}/jobs/${encodeURIComponent(card.slug)}/apply`,
|
|
407
|
+
{},
|
|
408
|
+
accessToken
|
|
409
|
+
);
|
|
410
|
+
results.push(
|
|
411
|
+
applied.ok && typeof applied.json?.id === "string" ? WRITE.apply("pass", `applied to ${card.slug}`) : WRITE.apply("fail", `apply returned HTTP ${applied.status}`)
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const alert = await post(fetchImpl, `${base}/job-alerts`, {
|
|
416
|
+
email,
|
|
417
|
+
consent: true
|
|
418
|
+
});
|
|
419
|
+
results.push(
|
|
420
|
+
alert.ok ? WRITE.alert("pass", "job-alert subscription accepted") : WRITE.alert("fail", `job-alerts returned HTTP ${alert.status}`)
|
|
421
|
+
);
|
|
422
|
+
return results;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/doctor/run.ts
|
|
426
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
427
|
+
import { join } from "path";
|
|
428
|
+
var STATIC_API = record("static.api", 1);
|
|
429
|
+
var STATIC_BOARD = record("static.board", 1);
|
|
430
|
+
var STATIC_SKILLS = record("static.skills", 1);
|
|
431
|
+
async function checkApiReachable(fetchImpl, apiUrl) {
|
|
432
|
+
let origin;
|
|
433
|
+
try {
|
|
434
|
+
origin = new URL(apiUrl).origin;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const spec = await probe(fetchImpl, `${origin}/api/v1/openapi.json`);
|
|
439
|
+
const result = spec.ok ? spec : await probe(fetchImpl, `${origin}/v1/openapi.json`);
|
|
440
|
+
if (!result.ok) {
|
|
441
|
+
return STATIC_API(
|
|
442
|
+
"fail",
|
|
443
|
+
`OpenAPI spec unreachable (HTTP ${result.status})`
|
|
444
|
+
);
|
|
27
445
|
}
|
|
28
|
-
|
|
446
|
+
try {
|
|
447
|
+
const parsed = JSON.parse(result.body);
|
|
448
|
+
if (typeof parsed.openapi !== "string") throw new Error("not a spec");
|
|
449
|
+
} catch {
|
|
450
|
+
return STATIC_API(
|
|
451
|
+
"fail",
|
|
452
|
+
"endpoint returned 200 but not an OpenAPI document \u2014 proxy or captive portal in the way?"
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return STATIC_API("pass", "OpenAPI spec reachable");
|
|
456
|
+
}
|
|
457
|
+
async function checkBoardResolves(fetchImpl, env) {
|
|
458
|
+
const origin = new URL(env.apiUrl).origin;
|
|
459
|
+
const result = await probe(
|
|
460
|
+
fetchImpl,
|
|
461
|
+
`${origin}/v1/boards/${encodeURIComponent(env.boardKey)}`
|
|
462
|
+
);
|
|
463
|
+
if (!result.ok) {
|
|
464
|
+
return {
|
|
465
|
+
result: STATIC_BOARD(
|
|
466
|
+
"fail",
|
|
467
|
+
`board did not resolve for this key (HTTP ${result.status}) \u2014 revoked or wrong key?`
|
|
468
|
+
),
|
|
469
|
+
context: null
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
let parsed = null;
|
|
473
|
+
try {
|
|
474
|
+
parsed = JSON.parse(result.body);
|
|
475
|
+
if (typeof parsed !== "object" || parsed === null) throw new Error("shape");
|
|
476
|
+
} catch {
|
|
477
|
+
return {
|
|
478
|
+
result: STATIC_BOARD(
|
|
479
|
+
"fail",
|
|
480
|
+
"board endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
|
|
481
|
+
),
|
|
482
|
+
context: null
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
result: STATIC_BOARD("pass", "publishable key resolves the board"),
|
|
487
|
+
context: parsed
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
var SKILL_ROOTS = [".claude/skills", ".agents/skills"];
|
|
491
|
+
function checkSkillsFreshness(projectRoot) {
|
|
492
|
+
const roots = SKILL_ROOTS.map((root) => join(projectRoot, root)).filter(
|
|
493
|
+
(root) => existsSync(root)
|
|
494
|
+
);
|
|
495
|
+
if (roots.length === 0) {
|
|
496
|
+
return STATIC_SKILLS(
|
|
497
|
+
"skip",
|
|
498
|
+
"no .claude/skills or .agents/skills directory \u2014 run `npx @cavuno/board setup` to install agent skills"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const corpus = loadSkillCorpus();
|
|
502
|
+
const stale = /* @__PURE__ */ new Set();
|
|
503
|
+
const seen = /* @__PURE__ */ new Set();
|
|
504
|
+
for (const root of roots) {
|
|
505
|
+
for (const skill of corpus.skills) {
|
|
506
|
+
const copied = join(root, skill.name, "SKILL.md");
|
|
507
|
+
if (!existsSync(copied)) continue;
|
|
508
|
+
seen.add(skill.name);
|
|
509
|
+
if (readFileSync2(copied, "utf8") !== skill.content) {
|
|
510
|
+
stale.add(skill.name);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const found = seen.size;
|
|
515
|
+
if (found === 0) {
|
|
516
|
+
return STATIC_SKILLS(
|
|
517
|
+
"skip",
|
|
518
|
+
"no cavuno-board-* skills installed \u2014 run `npx @cavuno/board setup`"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return stale.size === 0 ? STATIC_SKILLS(
|
|
522
|
+
"pass",
|
|
523
|
+
`${found} installed skills match v${corpus.version}`
|
|
524
|
+
) : STATIC_SKILLS(
|
|
525
|
+
"fail",
|
|
526
|
+
`stale skills (re-run \`npx @cavuno/board setup\`): ${[...stale].join(", ")}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
async function runDoctor(options) {
|
|
530
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
531
|
+
const results = [];
|
|
532
|
+
const envResults = checkEnv(options.env);
|
|
533
|
+
results.push(...envResults);
|
|
534
|
+
const envOk = envResults.every((r) => r.status === "pass");
|
|
535
|
+
if (options.env.apiUrl) {
|
|
536
|
+
const api = await checkApiReachable(fetchImpl, options.env.apiUrl);
|
|
537
|
+
results.push(
|
|
538
|
+
api ?? STATIC_API("skip", "API URL malformed \u2014 fix env.api-url first")
|
|
539
|
+
);
|
|
540
|
+
} else {
|
|
541
|
+
results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
|
|
542
|
+
}
|
|
543
|
+
let boardContext = null;
|
|
544
|
+
if (envOk) {
|
|
545
|
+
const resolved = await checkBoardResolves(fetchImpl, options.env);
|
|
546
|
+
boardContext = resolved.context;
|
|
547
|
+
results.push(resolved.result);
|
|
548
|
+
} else {
|
|
549
|
+
results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
|
|
550
|
+
}
|
|
551
|
+
results.push(checkSkillsFreshness(options.projectRoot ?? process.cwd()));
|
|
552
|
+
results.push(
|
|
553
|
+
...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
|
|
554
|
+
);
|
|
555
|
+
if (!options.sandbox) {
|
|
556
|
+
results.push(
|
|
557
|
+
...skipWriteProbes(
|
|
558
|
+
"write probes not enabled \u2014 run with --sandbox against the platform sandbox"
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
} else if (!envOk) {
|
|
562
|
+
results.push(...skipWriteProbes("env checks failed \u2014 fix them first"));
|
|
563
|
+
} else {
|
|
564
|
+
results.push(
|
|
565
|
+
...await runWriteProbes({
|
|
566
|
+
env: options.env,
|
|
567
|
+
fetchImpl,
|
|
568
|
+
resendApiKey: options.resendApiKey,
|
|
569
|
+
nonce: options.writeProbeNonce,
|
|
570
|
+
boardContext
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return { results, summary: summarize(results) };
|
|
575
|
+
}
|
|
29
576
|
|
|
30
577
|
// src/setup/run.ts
|
|
31
|
-
import { cpSync, existsSync, mkdirSync, readFileSync as
|
|
578
|
+
import { cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3 } from "fs";
|
|
32
579
|
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
33
580
|
function detectFramework(cwd) {
|
|
34
581
|
const pkgPath = resolve2(cwd, "package.json");
|
|
35
|
-
if (!
|
|
36
|
-
const pkg = JSON.parse(
|
|
582
|
+
if (!existsSync2(pkgPath)) return null;
|
|
583
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
|
|
37
584
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
38
585
|
if (deps["@tanstack/react-start"]) return "tanstack-start";
|
|
39
586
|
return null;
|
|
@@ -48,7 +595,7 @@ function runSetup(cwd = process.cwd()) {
|
|
|
48
595
|
resolve2(cwd, ".claude", "skills"),
|
|
49
596
|
resolve2(cwd, ".agents", "skills")
|
|
50
597
|
];
|
|
51
|
-
const existing = roots.filter((root) =>
|
|
598
|
+
const existing = roots.filter((root) => existsSync2(root));
|
|
52
599
|
const targetDirs = existing.length > 0 ? existing : [roots[0]];
|
|
53
600
|
const copied = [];
|
|
54
601
|
for (const targetDir of targetDirs) {
|
|
@@ -63,45 +610,76 @@ function runSetup(cwd = process.cwd()) {
|
|
|
63
610
|
}
|
|
64
611
|
return { version: manifest.version, framework, targetDirs, copied };
|
|
65
612
|
}
|
|
66
|
-
var init_run = __esm({
|
|
67
|
-
"src/setup/run.ts"() {
|
|
68
|
-
"use strict";
|
|
69
|
-
init_skills();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
613
|
|
|
73
614
|
// src/bin.ts
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
615
|
+
async function doctor(argv) {
|
|
616
|
+
const frontendFlag = argv.indexOf("--frontend");
|
|
617
|
+
const frontendUrl = frontendFlag >= 0 ? argv[frontendFlag + 1] : void 0;
|
|
618
|
+
const { results, summary } = await runDoctor({
|
|
619
|
+
env: {
|
|
620
|
+
apiUrl: process.env.PUBLIC_CAVUNO_API_URL,
|
|
621
|
+
boardKey: process.env.PUBLIC_CAVUNO_BOARD
|
|
622
|
+
},
|
|
623
|
+
frontendUrl,
|
|
624
|
+
// Tier-3 write probes: opt-in via --sandbox, and refused server-side
|
|
625
|
+
// unless the resolved board is platform-marked test (writes.ts gate).
|
|
626
|
+
sandbox: argv.includes("--sandbox"),
|
|
627
|
+
resendApiKey: process.env.RESEND_API_KEY
|
|
628
|
+
});
|
|
629
|
+
console.log("\n@cavuno/board doctor");
|
|
630
|
+
for (const result of results) {
|
|
631
|
+
const mark = result.status === "pass" ? "\u2713" : result.status === "fail" ? "\u2717" : "\u2212";
|
|
632
|
+
console.log(
|
|
633
|
+
` ${mark} [tier ${result.tier}] ${result.id} \u2014 ${result.detail}`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
console.log(
|
|
637
|
+
`
|
|
638
|
+
${summary.passed.length} passed, ${summary.failed.length} failed, ${summary.skipped.length} skipped`
|
|
639
|
+
);
|
|
640
|
+
if (summary.skipped.length > 0) {
|
|
641
|
+
console.log(` Skipped (NOT verified): ${summary.skipped.join(", ")}`);
|
|
642
|
+
}
|
|
643
|
+
if (!frontendUrl) {
|
|
644
|
+
console.log(
|
|
645
|
+
" Pass --frontend <url> to run the read probes against your board frontend."
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
process.exit(summary.exitCode);
|
|
649
|
+
}
|
|
650
|
+
function main() {
|
|
651
|
+
const verb = process.argv[2];
|
|
652
|
+
if (verb === "doctor") {
|
|
653
|
+
doctor(process.argv.slice(3)).catch((error) => {
|
|
654
|
+
console.error("doctor crashed:", error);
|
|
655
|
+
process.exit(1);
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (verb !== "setup") {
|
|
660
|
+
console.error("Usage: cavuno-board <setup|doctor>");
|
|
661
|
+
process.exit(1);
|
|
105
662
|
}
|
|
106
|
-
|
|
107
|
-
|
|
663
|
+
const result = runSetup();
|
|
664
|
+
console.log(`
|
|
665
|
+
@cavuno/board setup \u2014 v${result.version}`);
|
|
666
|
+
console.log(
|
|
667
|
+
`Detected framework: ${result.framework ?? "none (core skills only)"}`
|
|
668
|
+
);
|
|
669
|
+
console.log(
|
|
670
|
+
`Copied ${result.copied.length} skills \u2192 ${result.targetDirs.join(", ")}`
|
|
671
|
+
);
|
|
672
|
+
for (const name of result.copied) console.log(` - ${name}`);
|
|
673
|
+
console.log("\nNext steps:");
|
|
674
|
+
console.log(" 1. Set your environment variables:");
|
|
675
|
+
console.log(" PUBLIC_CAVUNO_API_URL=https://api.cavuno.com");
|
|
676
|
+
console.log(
|
|
677
|
+
" PUBLIC_CAVUNO_BOARD=pk_... # your board publishable key"
|
|
678
|
+
);
|
|
679
|
+
console.log(' 2. Ask your coding agent: "set up my Cavuno board".');
|
|
680
|
+
console.log(" It reads the cavuno-board-setup skill first.");
|
|
681
|
+
console.log(
|
|
682
|
+
"\n Re-run `npx @cavuno/board setup` after upgrading to refresh skills."
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
main();
|
package/dist/filters.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-
|
|
1
|
+
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-lzeoVGY6.mjs';
|
|
2
2
|
|
|
3
3
|
declare const REMOTE_OPTIONS: readonly RemoteOption[];
|
|
4
4
|
/**
|
package/dist/filters.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-
|
|
1
|
+
import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-lzeoVGY6.js';
|
|
2
2
|
|
|
3
3
|
declare const REMOTE_OPTIONS: readonly RemoteOption[];
|
|
4
4
|
/**
|
package/dist/format.d.mts
CHANGED
package/dist/format.d.ts
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-
|
|
2
|
-
export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-
|
|
3
|
-
import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-
|
|
4
|
-
export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-
|
|
1
|
+
import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-lzeoVGY6.mjs';
|
|
2
|
+
export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-lzeoVGY6.mjs';
|
|
3
|
+
import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-DpH5CxdM.mjs';
|
|
4
|
+
export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-DpH5CxdM.mjs';
|
|
5
5
|
|
|
6
6
|
type Awaitable<T> = T | Promise<T>;
|
|
7
7
|
/**
|
|
@@ -203,7 +203,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
|
|
|
203
203
|
* constant because the package is platform-neutral and cannot read
|
|
204
204
|
* package.json at runtime.
|
|
205
205
|
*/
|
|
206
|
-
declare const SDK_VERSION = "1.
|
|
206
|
+
declare const SDK_VERSION = "1.30.0";
|
|
207
207
|
|
|
208
208
|
type SavedJob = Schemas['SavedJob'];
|
|
209
209
|
type SavedJobsListQuery = {
|
|
@@ -654,6 +654,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
|
|
|
654
654
|
logoUrl: string | null;
|
|
655
655
|
primaryDomain: string | null;
|
|
656
656
|
showCavunoBranding: boolean;
|
|
657
|
+
sandbox: boolean;
|
|
657
658
|
features: {
|
|
658
659
|
jobAlerts: boolean;
|
|
659
660
|
candidates: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-
|
|
2
|
-
export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-
|
|
3
|
-
import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-
|
|
4
|
-
export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-
|
|
1
|
+
import { b as Schemas, R as RemoteOption, E as EmploymentType, S as Seniority, L as ListEnvelope, c as components, d as JobsListQuery, e as JobCardListEnvelope, f as JobsSearchBody, g as JobCardSearchEnvelope, h as JobsSimilarQuery, i as SearchEnvelope } from './jobs-lzeoVGY6.js';
|
|
2
|
+
export { C as CustomFieldValue, j as CustomFieldValues, k as EducationRequirement, l as JobCompany, J as JobSort, O as OfficeLocation, P as PublicJob, a as PublicJobCard, m as RelatedSearch, n as RemotePermit, o as RemoteTimezone, p as StorefrontPagination } from './jobs-lzeoVGY6.js';
|
|
3
|
+
import { c as CompaniesListQuery, d as CompanyListEnvelope, e as CompaniesSearchBody, f as CompanyJobsListQuery, g as CompanySimilarQuery, h as CompanyMarketsListQuery, i as SalaryDetailQuery, j as BlogPostsListQuery, k as BlogSimilarQuery, l as BlogSearchBody } from './salaries-Cy3MhZov.js';
|
|
4
|
+
export { B as BlogAuthorEmbed, m as BlogTagEmbed, C as CompanyCategorySalary, n as CompanyMarket, o as CompanyMarketRef, b as CompanySalary, p as CustomFieldDefinition, q as CustomFieldOption, r as CustomFieldType, L as LocationSalaryDetail, s as LocationSkillsIndex, t as LocationTitlesIndex, u as PublicBlogAdjacentPosts, v as PublicBlogAuthor, w as PublicBlogPost, a as PublicBlogPostSummary, x as PublicBlogTag, P as PublicBoard, y as PublicBoardAnalytics, z as PublicBoardFeatures, A as PublicBoardTheme, D as PublicCompany, E as PublicCompanyDetail, F as SalaryCompany, G as SalaryLocation, H as SalarySkill, I as SalaryTitle, J as SkillLocationSalary, K as SkillLocationsIndex, S as SkillSalaryDetail, M as TitleLocationSalary, N as TitleLocationsIndex, T as TitleSalaryDetail } from './salaries-Cy3MhZov.js';
|
|
5
5
|
|
|
6
6
|
type Awaitable<T> = T | Promise<T>;
|
|
7
7
|
/**
|
|
@@ -203,7 +203,7 @@ declare function paginate<Q extends Record<string, unknown>, P extends PageShape
|
|
|
203
203
|
* constant because the package is platform-neutral and cannot read
|
|
204
204
|
* package.json at runtime.
|
|
205
205
|
*/
|
|
206
|
-
declare const SDK_VERSION = "1.
|
|
206
|
+
declare const SDK_VERSION = "1.30.0";
|
|
207
207
|
|
|
208
208
|
type SavedJob = Schemas['SavedJob'];
|
|
209
209
|
type SavedJobsListQuery = {
|
|
@@ -654,6 +654,7 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
|
|
|
654
654
|
logoUrl: string | null;
|
|
655
655
|
primaryDomain: string | null;
|
|
656
656
|
showCavunoBranding: boolean;
|
|
657
|
+
sandbox: boolean;
|
|
657
658
|
features: {
|
|
658
659
|
jobAlerts: boolean;
|
|
659
660
|
candidates: boolean;
|
package/dist/index.js
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -2382,6 +2382,8 @@ interface components {
|
|
|
2382
2382
|
primaryDomain: string | null;
|
|
2383
2383
|
/** @description Whitelabel toggle (default `true`). Render the "Powered by Cavuno" badge unless `false`. */
|
|
2384
2384
|
showCavunoBranding: boolean;
|
|
2385
|
+
/** @description True ONLY for the platform sandbox fixture tenant — the safe target for write probes (doctor tier 3); data is reset nightly. Every real tenant board is `false`. */
|
|
2386
|
+
sandbox: boolean;
|
|
2385
2387
|
features: {
|
|
2386
2388
|
jobAlerts: boolean;
|
|
2387
2389
|
candidates: boolean;
|
|
@@ -2382,6 +2382,8 @@ interface components {
|
|
|
2382
2382
|
primaryDomain: string | null;
|
|
2383
2383
|
/** @description Whitelabel toggle (default `true`). Render the "Powered by Cavuno" badge unless `false`. */
|
|
2384
2384
|
showCavunoBranding: boolean;
|
|
2385
|
+
/** @description True ONLY for the platform sandbox fixture tenant — the safe target for write probes (doctor tier 3); data is reset nightly. Every real tenant board is `false`. */
|
|
2386
|
+
sandbox: boolean;
|
|
2385
2387
|
features: {
|
|
2386
2388
|
jobAlerts: boolean;
|
|
2387
2389
|
candidates: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-
|
|
1
|
+
import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-lzeoVGY6.js';
|
|
2
2
|
|
|
3
3
|
type PublicBoard = Schemas['PublicBoardContext'];
|
|
4
4
|
type PublicBoardFeatures = PublicBoard['features'];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-
|
|
1
|
+
import { b as Schemas, L as ListEnvelope, m as RelatedSearch } from './jobs-lzeoVGY6.mjs';
|
|
2
2
|
|
|
3
3
|
type PublicBoard = Schemas['PublicBoardContext'];
|
|
4
4
|
type PublicBoardFeatures = PublicBoard['features'];
|
package/dist/seo.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-
|
|
2
|
-
import { P as PublicJob } from './jobs-
|
|
1
|
+
import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-DpH5CxdM.mjs';
|
|
2
|
+
import { P as PublicJob } from './jobs-lzeoVGY6.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
|
package/dist/seo.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-
|
|
2
|
-
import { P as PublicJob } from './jobs-
|
|
1
|
+
import { P as PublicBoard, a as PublicBlogPostSummary, B as BlogAuthorEmbed, C as CompanyCategorySalary, b as CompanySalary, L as LocationSalaryDetail, S as SkillSalaryDetail, T as TitleSalaryDetail } from './salaries-Cy3MhZov.js';
|
|
2
|
+
import { P as PublicJob } from './jobs-lzeoVGY6.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Google for Jobs `JobPosting` structured data on the `@cavuno/board` wire
|
package/dist/server.d.mts
CHANGED
package/dist/server.d.ts
CHANGED
package/dist/sitemap.d.mts
CHANGED
package/dist/sitemap.d.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-manifest types, split from `skills.ts` so non-Node consumers (the
|
|
3
|
+
* remote-mcp worker types its `/dev` toolset against the wire shape) can
|
|
4
|
+
* import them without dragging the Node fs loader into their program.
|
|
5
|
+
*/
|
|
6
|
+
interface SkillManifestEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
/** Path to the SKILL.md, relative to the package root. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Framework slug for flavor skills; `null` for framework-agnostic core skills. */
|
|
12
|
+
framework: string | null;
|
|
13
|
+
category: 'core' | 'flavor';
|
|
14
|
+
}
|
|
15
|
+
interface SkillManifest {
|
|
16
|
+
version: string;
|
|
17
|
+
skills: SkillManifestEntry[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type { SkillManifest, SkillManifestEntry };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill-manifest types, split from `skills.ts` so non-Node consumers (the
|
|
3
|
+
* remote-mcp worker types its `/dev` toolset against the wire shape) can
|
|
4
|
+
* import them without dragging the Node fs loader into their program.
|
|
5
|
+
*/
|
|
6
|
+
interface SkillManifestEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
/** Path to the SKILL.md, relative to the package root. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Framework slug for flavor skills; `null` for framework-agnostic core skills. */
|
|
12
|
+
framework: string | null;
|
|
13
|
+
category: 'core' | 'flavor';
|
|
14
|
+
}
|
|
15
|
+
interface SkillManifest {
|
|
16
|
+
version: string;
|
|
17
|
+
skills: SkillManifestEntry[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type { SkillManifest, SkillManifestEntry };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// src/skills/types.ts
|
|
17
|
+
var types_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(types_exports);
|
|
File without changes
|
package/dist/skills.d.mts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* in-admin sidekick (ADR-0033) read the corpus through here, so the two doors
|
|
6
|
-
* stay fed from one source.
|
|
2
|
+
* Skill-manifest types, split from `skills.ts` so non-Node consumers (the
|
|
3
|
+
* remote-mcp worker types its `/dev` toolset against the wire shape) can
|
|
4
|
+
* import them without dragging the Node fs loader into their program.
|
|
7
5
|
*/
|
|
8
6
|
interface SkillManifestEntry {
|
|
9
7
|
name: string;
|
|
@@ -18,6 +16,15 @@ interface SkillManifest {
|
|
|
18
16
|
version: string;
|
|
19
17
|
skills: SkillManifestEntry[];
|
|
20
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Skill-corpus loader. Node-only (reads the shipped `skills/` directory from
|
|
22
|
+
* the installed package) — kept out of the isomorphic core and exposed via the
|
|
23
|
+
* `@cavuno/board/skills` subpath export. Both `npx @cavuno/board setup` and the
|
|
24
|
+
* in-admin sidekick (ADR-0033) read the corpus through here, so the two doors
|
|
25
|
+
* stay fed from one source.
|
|
26
|
+
*/
|
|
27
|
+
|
|
21
28
|
interface LoadedSkill extends SkillManifestEntry {
|
|
22
29
|
content: string;
|
|
23
30
|
}
|
|
@@ -26,13 +33,13 @@ interface SkillCorpus {
|
|
|
26
33
|
skills: LoadedSkill[];
|
|
27
34
|
}
|
|
28
35
|
/** Resolve a package-root-relative path (e.g. a manifest `path`) to an absolute path. */
|
|
29
|
-
declare function resolveFromPackageRoot(relativePath: string): string;
|
|
30
|
-
declare function loadSkillManifest(): SkillManifest;
|
|
36
|
+
declare function resolveFromPackageRoot(relativePath: string, baseDir?: string): string;
|
|
37
|
+
declare function loadSkillManifest(baseDir?: string): SkillManifest;
|
|
31
38
|
/**
|
|
32
39
|
* Load the full skill corpus — manifest metadata plus each skill's markdown
|
|
33
40
|
* `content`. The sidekick injects `content` into its agent context; an external
|
|
34
41
|
* Claude Code instead receives the files copied by the setup command.
|
|
35
42
|
*/
|
|
36
|
-
declare function loadSkillCorpus(): SkillCorpus;
|
|
43
|
+
declare function loadSkillCorpus(baseDir?: string): SkillCorpus;
|
|
37
44
|
|
|
38
45
|
export { type LoadedSkill, type SkillCorpus, type SkillManifest, type SkillManifestEntry, loadSkillCorpus, loadSkillManifest, resolveFromPackageRoot };
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* in-admin sidekick (ADR-0033) read the corpus through here, so the two doors
|
|
6
|
-
* stay fed from one source.
|
|
2
|
+
* Skill-manifest types, split from `skills.ts` so non-Node consumers (the
|
|
3
|
+
* remote-mcp worker types its `/dev` toolset against the wire shape) can
|
|
4
|
+
* import them without dragging the Node fs loader into their program.
|
|
7
5
|
*/
|
|
8
6
|
interface SkillManifestEntry {
|
|
9
7
|
name: string;
|
|
@@ -18,6 +16,15 @@ interface SkillManifest {
|
|
|
18
16
|
version: string;
|
|
19
17
|
skills: SkillManifestEntry[];
|
|
20
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Skill-corpus loader. Node-only (reads the shipped `skills/` directory from
|
|
22
|
+
* the installed package) — kept out of the isomorphic core and exposed via the
|
|
23
|
+
* `@cavuno/board/skills` subpath export. Both `npx @cavuno/board setup` and the
|
|
24
|
+
* in-admin sidekick (ADR-0033) read the corpus through here, so the two doors
|
|
25
|
+
* stay fed from one source.
|
|
26
|
+
*/
|
|
27
|
+
|
|
21
28
|
interface LoadedSkill extends SkillManifestEntry {
|
|
22
29
|
content: string;
|
|
23
30
|
}
|
|
@@ -26,13 +33,13 @@ interface SkillCorpus {
|
|
|
26
33
|
skills: LoadedSkill[];
|
|
27
34
|
}
|
|
28
35
|
/** Resolve a package-root-relative path (e.g. a manifest `path`) to an absolute path. */
|
|
29
|
-
declare function resolveFromPackageRoot(relativePath: string): string;
|
|
30
|
-
declare function loadSkillManifest(): SkillManifest;
|
|
36
|
+
declare function resolveFromPackageRoot(relativePath: string, baseDir?: string): string;
|
|
37
|
+
declare function loadSkillManifest(baseDir?: string): SkillManifest;
|
|
31
38
|
/**
|
|
32
39
|
* Load the full skill corpus — manifest metadata plus each skill's markdown
|
|
33
40
|
* `content`. The sidekick injects `content` into its agent context; an external
|
|
34
41
|
* Claude Code instead receives the files copied by the setup command.
|
|
35
42
|
*/
|
|
36
|
-
declare function loadSkillCorpus(): SkillCorpus;
|
|
43
|
+
declare function loadSkillCorpus(baseDir?: string): SkillCorpus;
|
|
37
44
|
|
|
38
45
|
export { type LoadedSkill, type SkillCorpus, type SkillManifest, type SkillManifestEntry, loadSkillCorpus, loadSkillManifest, resolveFromPackageRoot };
|
package/dist/skills.js
CHANGED
|
@@ -37,20 +37,23 @@ var import_node_url = require("url");
|
|
|
37
37
|
function packageRoot() {
|
|
38
38
|
return (0, import_node_path.resolve)((0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "..");
|
|
39
39
|
}
|
|
40
|
-
function resolveFromPackageRoot(relativePath) {
|
|
41
|
-
return (0, import_node_path.resolve)(packageRoot(), relativePath);
|
|
40
|
+
function resolveFromPackageRoot(relativePath, baseDir) {
|
|
41
|
+
return (0, import_node_path.resolve)(baseDir ?? packageRoot(), relativePath);
|
|
42
42
|
}
|
|
43
|
-
function loadSkillManifest() {
|
|
44
|
-
const manifestPath = resolveFromPackageRoot("skills/manifest.json");
|
|
43
|
+
function loadSkillManifest(baseDir) {
|
|
44
|
+
const manifestPath = resolveFromPackageRoot("skills/manifest.json", baseDir);
|
|
45
45
|
return JSON.parse((0, import_node_fs.readFileSync)(manifestPath, "utf8"));
|
|
46
46
|
}
|
|
47
|
-
function loadSkillCorpus() {
|
|
48
|
-
const manifest = loadSkillManifest();
|
|
47
|
+
function loadSkillCorpus(baseDir) {
|
|
48
|
+
const manifest = loadSkillManifest(baseDir);
|
|
49
49
|
return {
|
|
50
50
|
version: manifest.version,
|
|
51
51
|
skills: manifest.skills.map((skill) => ({
|
|
52
52
|
...skill,
|
|
53
|
-
content: (0, import_node_fs.readFileSync)(
|
|
53
|
+
content: (0, import_node_fs.readFileSync)(
|
|
54
|
+
resolveFromPackageRoot(skill.path, baseDir),
|
|
55
|
+
"utf8"
|
|
56
|
+
)
|
|
54
57
|
}))
|
|
55
58
|
};
|
|
56
59
|
}
|
package/dist/skills.mjs
CHANGED
|
@@ -5,20 +5,23 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
function packageRoot() {
|
|
6
6
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
7
|
}
|
|
8
|
-
function resolveFromPackageRoot(relativePath) {
|
|
9
|
-
return resolve(packageRoot(), relativePath);
|
|
8
|
+
function resolveFromPackageRoot(relativePath, baseDir) {
|
|
9
|
+
return resolve(baseDir ?? packageRoot(), relativePath);
|
|
10
10
|
}
|
|
11
|
-
function loadSkillManifest() {
|
|
12
|
-
const manifestPath = resolveFromPackageRoot("skills/manifest.json");
|
|
11
|
+
function loadSkillManifest(baseDir) {
|
|
12
|
+
const manifestPath = resolveFromPackageRoot("skills/manifest.json", baseDir);
|
|
13
13
|
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
14
14
|
}
|
|
15
|
-
function loadSkillCorpus() {
|
|
16
|
-
const manifest = loadSkillManifest();
|
|
15
|
+
function loadSkillCorpus(baseDir) {
|
|
16
|
+
const manifest = loadSkillManifest(baseDir);
|
|
17
17
|
return {
|
|
18
18
|
version: manifest.version,
|
|
19
19
|
skills: manifest.skills.map((skill) => ({
|
|
20
20
|
...skill,
|
|
21
|
-
content: readFileSync(
|
|
21
|
+
content: readFileSync(
|
|
22
|
+
resolveFromPackageRoot(skill.path, baseDir),
|
|
23
|
+
"utf8"
|
|
24
|
+
)
|
|
22
25
|
}))
|
|
23
26
|
};
|
|
24
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavuno/board",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Typed isomorphic client for the Cavuno Board API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -100,6 +100,16 @@
|
|
|
100
100
|
"default": "./dist/skills.js"
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
|
+
"./skills/types": {
|
|
104
|
+
"import": {
|
|
105
|
+
"types": "./dist/skills-types.d.mts",
|
|
106
|
+
"default": "./dist/skills-types.mjs"
|
|
107
|
+
},
|
|
108
|
+
"require": {
|
|
109
|
+
"types": "./dist/skills-types.d.ts",
|
|
110
|
+
"default": "./dist/skills-types.js"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
103
113
|
"./skills/*": "./skills/*"
|
|
104
114
|
},
|
|
105
115
|
"files": [
|
|
@@ -15,6 +15,34 @@ auth, and SEO surfaces behave. Verify at runtime, in this order.
|
|
|
15
15
|
- After upgrading `@cavuno/board` or rotating the `pk_…` key.
|
|
16
16
|
- When any surface renders empty and you don't know which layer is wrong.
|
|
17
17
|
|
|
18
|
+
## 0 — Run `doctor` first (deterministic pass)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
PUBLIC_CAVUNO_API_URL=... PUBLIC_CAVUNO_BOARD=pk_... \
|
|
22
|
+
npx @cavuno/board doctor --frontend http://localhost:3000
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Doctor codifies the static checks (env shape, API reachability) and the
|
|
26
|
+
read probes (home/jobs render, JobPosting JSON-LD, sitemap, robots) as
|
|
27
|
+
pass/fail/skip with a non-zero exit on failure — anything it SKIPS is
|
|
28
|
+
named in its summary and still needs the manual checks below. Use the
|
|
29
|
+
rest of this skill for the behavioral checks doctor does not automate
|
|
30
|
+
(auth flows, gating semantics, production build).
|
|
31
|
+
|
|
32
|
+
Add `--sandbox` to also run the write probes (register → login → apply →
|
|
33
|
+
alert-signup). They are refused unless the board the `pk_` resolves is
|
|
34
|
+
THE platform sandbox (`sandbox: true` in the board context — set only by
|
|
35
|
+
the platform's sandbox seed and stripped from any client settings write)
|
|
36
|
+
— never point them at a real tenant. The
|
|
37
|
+
email-delivery check additionally needs `RESEND_API_KEY` (a platform
|
|
38
|
+
operator credential) and SKIPs loudly without it.
|
|
39
|
+
|
|
40
|
+
Rate limits are real on the sandbox: the auth endpoints share a
|
|
41
|
+
10-requests/min bucket and register allows 5 signups per 15 minutes per
|
|
42
|
+
IP, and one probe run consumes 4 requests + 1 signup. Rapid reruns will
|
|
43
|
+
start failing with HTTP 429 — that's the limiter working, not a broken
|
|
44
|
+
board; wait a few minutes.
|
|
45
|
+
|
|
18
46
|
## 1 — Probe the API directly (before blaming app code)
|
|
19
47
|
|
|
20
48
|
Use the real env values the app reads (`PUBLIC_CAVUNO_API_URL`,
|
package/skills/manifest.json
CHANGED