@cavuno/board 1.2.0 → 1.3.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 ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/skills.ts
4
+ import { readFileSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+ import { fileURLToPath } from "url";
7
+ function packageRoot() {
8
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+ }
10
+ function resolveFromPackageRoot(relativePath) {
11
+ return resolve(packageRoot(), relativePath);
12
+ }
13
+ function loadSkillManifest() {
14
+ const manifestPath = resolveFromPackageRoot("skills/manifest.json");
15
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
16
+ }
17
+
18
+ // src/setup/run.ts
19
+ import { cpSync, existsSync, mkdirSync, readFileSync as readFileSync2 } from "fs";
20
+ import { dirname as dirname2, resolve as resolve2 } from "path";
21
+ function detectFramework(cwd) {
22
+ const pkgPath = resolve2(cwd, "package.json");
23
+ if (!existsSync(pkgPath)) return null;
24
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
25
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
26
+ if (deps["@tanstack/react-start"]) return "tanstack-start";
27
+ return null;
28
+ }
29
+ function runSetup(cwd = process.cwd()) {
30
+ const manifest = loadSkillManifest();
31
+ const framework = detectFramework(cwd);
32
+ const chosen = manifest.skills.filter(
33
+ (skill) => skill.category === "core" || skill.framework === framework
34
+ );
35
+ const targetDir = resolve2(cwd, ".claude", "skills");
36
+ mkdirSync(targetDir, { recursive: true });
37
+ const copied = [];
38
+ for (const skill of chosen) {
39
+ const sourceDir = dirname2(resolveFromPackageRoot(skill.path));
40
+ const destDir = resolve2(targetDir, skill.name);
41
+ mkdirSync(destDir, { recursive: true });
42
+ cpSync(sourceDir, destDir, { recursive: true });
43
+ copied.push(skill.name);
44
+ }
45
+ return { version: manifest.version, framework, targetDir, copied };
46
+ }
47
+
48
+ // src/bin.ts
49
+ function main() {
50
+ if (process.argv[2] !== "setup") {
51
+ console.error("Usage: cavuno-board setup");
52
+ process.exit(1);
53
+ }
54
+ const result = runSetup();
55
+ console.log(`
56
+ @cavuno/board setup \u2014 v${result.version}`);
57
+ console.log(
58
+ `Detected framework: ${result.framework ?? "none (core skills only)"}`
59
+ );
60
+ console.log(`Copied ${result.copied.length} skills \u2192 ${result.targetDir}`);
61
+ for (const name of result.copied) console.log(` - ${name}`);
62
+ console.log("\nNext steps:");
63
+ console.log(" 1. Set your environment variables:");
64
+ console.log(" PUBLIC_CAVUNO_API_URL=https://api.cavuno.com");
65
+ console.log(
66
+ " PUBLIC_CAVUNO_BOARD=pk_... # your board publishable key"
67
+ );
68
+ console.log(' 2. Ask your coding agent: "set up my Cavuno board".');
69
+ console.log(" It reads the cavuno-board-setup skill first.");
70
+ console.log(
71
+ "\n Re-run `npx @cavuno/board setup` after upgrading to refresh skills."
72
+ );
73
+ }
74
+ main();
package/dist/index.d.mts CHANGED
@@ -252,6 +252,8 @@ interface CustomStorage {
252
252
  type StorageMode = 'memory' | 'nostore' | CustomStorage;
253
253
  declare const ACCESS_TOKEN_KEY = "cavuno_board_access_token";
254
254
  declare const REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
255
+ /** Board-password access grant — sent as `X-Board-Access` to pass the wall. */
256
+ declare const BOARD_ACCESS_GRANT_KEY = "cavuno_board_access_grant";
255
257
 
256
258
  /**
257
259
  * The request handed to `onRequest` and `fetch`. `onRequest` may
@@ -340,6 +342,8 @@ interface PublicBoard {
340
342
  language: string;
341
343
  logoUrl: string | null;
342
344
  primaryDomain: string | null;
345
+ /** Whitelabel toggle (default `true`) — render the "Powered by Cavuno" badge unless `false`. */
346
+ showCavunoBranding: boolean;
343
347
  features: PublicBoardFeatures;
344
348
  analytics: PublicBoardAnalytics;
345
349
  theme: PublicBoardTheme | null;
@@ -352,9 +356,9 @@ interface PublicBoard {
352
356
  */
353
357
  interface BoardSeo {
354
358
  object: 'board_seo';
355
- /** Verbatim `ads.txt` content, or `null` when not configured (the handler 404s). */
359
+ /** Verbatim `ads.txt` content, or `null` when not configured. */
356
360
  adsTxt: string | null;
357
- /** IndexNow key-file content, or `null` when not configured (the handler 404s). */
361
+ /** IndexNow key-file content, or `null` when not configured. */
358
362
  indexNowKey: string | null;
359
363
  /** Google site-verification token for the `<meta>` tag, or `null`. */
360
364
  googleSiteVerification: string | null;
@@ -402,6 +406,12 @@ declare class BoardApiError extends Error {
402
406
  declare function isBoardApiError(e: unknown): e is BoardApiError;
403
407
  declare function isNotFound(e: unknown): e is BoardApiError;
404
408
  declare function isUnauthorized(e: unknown): e is BoardApiError;
409
+ /**
410
+ * The board is password-protected and the read carried no valid `X-Board-Access`
411
+ * grant. Distinct from an expired board-USER token (also 401): this code means
412
+ * "call `password.verify()` again to mint a fresh grant", not "re-login".
413
+ */
414
+ declare function isBoardPasswordRequired(e: unknown): e is BoardApiError;
405
415
  declare function isForbidden(e: unknown): e is BoardApiError;
406
416
  declare function isValidationError(e: unknown): e is BoardApiError;
407
417
  declare function isRateLimited(e: unknown): e is BoardApiError;
@@ -413,7 +423,7 @@ declare function isConflict(e: unknown): e is BoardApiError;
413
423
  * constant because the package is platform-neutral and cannot read
414
424
  * package.json at runtime.
415
425
  */
416
- declare const SDK_VERSION = "1.2.0";
426
+ declare const SDK_VERSION = "1.3.0";
417
427
 
418
428
  interface BoardUser {
419
429
  id: string;
@@ -464,6 +474,41 @@ interface ResetPasswordBody {
464
474
  password: string;
465
475
  }
466
476
 
477
+ /**
478
+ * Query for `board.embed.jobs()` — the embeddable, UNGATED jobs widget. Mirrors
479
+ * the browse list's facets + geo, plus a free-text `q` keyword (the widget is
480
+ * searchable). Deliberately has NO `category`/`skill` programmatic seeding and
481
+ * NO `fields` sparse fieldset — the embed widget exposes none of those.
482
+ */
483
+ type EmbedJobsQuery = {
484
+ /** Free-text search query, up to 200 characters. */
485
+ q?: string;
486
+ cursor?: string;
487
+ /** Default 8; values above the embed ceiling of 50 are clamped to 50. */
488
+ limit?: number;
489
+ /** Storefront page offset; takes precedence over `cursor`. `offset + limit` ≤ 10,000. */
490
+ offset?: number;
491
+ /** Single or repeated (up to 10) — repeated params are OR-matched. */
492
+ companyId?: string | string[];
493
+ remoteOption?: RemoteOption | RemoteOption[];
494
+ employmentType?: EmploymentType | EmploymentType[];
495
+ seniority?: Seniority | Seniority[];
496
+ /** Place slug for a geo radius search; unresolvable slugs are ignored. */
497
+ location?: string;
498
+ /** Radius in km around `location` (10–250; default 50). */
499
+ radius?: number;
500
+ };
501
+
502
+ /**
503
+ * The board-access grant returned by `password.verify()`. Send `token` as the
504
+ * `X-Board-Access` header on content reads to pass a board's password wall
505
+ * (the SDK does this automatically once the grant is stored).
506
+ */
507
+ interface BoardAccessGrant {
508
+ object: 'board_access_grant';
509
+ token: string;
510
+ }
511
+
467
512
  /**
468
513
  * The result of resolving a path against the board's configured redirects
469
514
  * (`board.redirects.resolve()`). A headless frontend 308s to `target`, or 404s
@@ -509,9 +554,13 @@ interface PublicBlogPostSummary {
509
554
  slug: string;
510
555
  featured: boolean;
511
556
  coverUrl: string | null;
557
+ /** Alt text for `coverUrl` (the cover/feature image). */
558
+ featureImageAlt: string | null;
512
559
  customExcerpt: string | null;
513
560
  readingTimeMin: number | null;
514
561
  publishedAt: string | null;
562
+ /** A custom canonical URL override for this post, if set. */
563
+ canonicalUrl: string | null;
515
564
  createdAt: string;
516
565
  authors: BlogAuthorEmbed[];
517
566
  tags: BlogTagEmbed[];
@@ -520,11 +569,22 @@ interface PublicBlogPostSummary {
520
569
  interface PublicBlogPost extends PublicBlogPostSummary {
521
570
  html: string | null;
522
571
  ogImageUrl: string | null;
523
- featureImageAlt: string | null;
524
572
  featureImageCaption: string | null;
525
573
  seoTitle: string | null;
526
574
  seoDescription: string | null;
527
- canonicalUrl: string | null;
575
+ /**
576
+ * Slug-history resolution. When the requested slug is a renamed post's OLD
577
+ * slug, `redirected` is `true` and `newSlug` is the post's current slug —
578
+ * issue a 308 to it. Otherwise `redirected` is `false` and `newSlug` is null.
579
+ */
580
+ redirected: boolean;
581
+ newSlug: string | null;
582
+ }
583
+ /** Previous (older) + next (newer) posts for the detail prev/next nav. */
584
+ interface PublicBlogAdjacentPosts {
585
+ object: 'blog_adjacent_posts';
586
+ previous: PublicBlogPostSummary | null;
587
+ next: PublicBlogPostSummary | null;
528
588
  }
529
589
  type BlogPostsListQuery = {
530
590
  cursor?: string;
@@ -535,6 +595,10 @@ type BlogPostsListQuery = {
535
595
  /** Opt-in only: pass `'true'` to restrict to featured posts. */
536
596
  featured?: 'true';
537
597
  };
598
+ type BlogSimilarQuery = {
599
+ /** 1–20; default 6. */
600
+ limit?: number;
601
+ };
538
602
  interface BlogSearchBody {
539
603
  /** Free-text query, 1–200 characters. Required. */
540
604
  query: string;
@@ -596,6 +660,109 @@ interface SaveJobBody {
596
660
  jobId: string;
597
661
  }
598
662
 
663
+ type JobAlertFrequency = 'daily' | 'weekly';
664
+ type JobAlertRemoteOption = 'on_site' | 'hybrid' | 'remote';
665
+ /**
666
+ * Alert filters a consumer can capture. Only `jobFunctions` (→ job categories),
667
+ * `placeSlugs` (→ place IDs) and `remoteOptions` scope the digest server-side;
668
+ * `seniorityLevels`/`salary*` are stored but do not filter delivery.
669
+ */
670
+ type JobAlertFiltersInput = {
671
+ jobFunctions?: string[];
672
+ seniorityLevels?: string[];
673
+ remoteOptions?: JobAlertRemoteOption[];
674
+ placeSlugs?: string[];
675
+ salaryMin?: number;
676
+ salaryMax?: number;
677
+ salaryCurrency?: string;
678
+ };
679
+ /** Body for `jobAlerts.subscribe`. `consent` must be `true` (server-enforced). */
680
+ type JobAlertSubscribeInput = {
681
+ email: string;
682
+ consent: true;
683
+ frequency?: JobAlertFrequency;
684
+ filters?: JobAlertFiltersInput;
685
+ /** Scopes the alert to the originating job/search (for the digest + analytics). */
686
+ context?: {
687
+ source?: string;
688
+ jobId?: string;
689
+ jobSlug?: string;
690
+ };
691
+ };
692
+ interface JobAlertSubscription {
693
+ object: 'job_alert_subscription';
694
+ status: 'created' | 'duplicate';
695
+ requiresConfirmation: boolean;
696
+ confirmed: boolean;
697
+ }
698
+ interface JobAlertConfirmation {
699
+ object: 'job_alert_confirmation';
700
+ status: 'confirmed' | 'already_confirmed' | 'expired' | 'not_found';
701
+ }
702
+ interface JobAlertResendResult {
703
+ object: 'job_alert_confirmation_resend';
704
+ status: 'sent' | 'not_found' | 'already_confirmed' | 'throttled' | 'send_failed';
705
+ }
706
+ interface JobAlertManageResult {
707
+ object: 'job_alert_manage_result';
708
+ success: boolean;
709
+ }
710
+ /**
711
+ * The stored filter criteria surfaced on the manage page (a serializable subset
712
+ * of the internal filter object — the fields a consumer renders). `jobFunctions`,
713
+ * `placeIds` and `remoteOptions` are the dimensions the digest actually matches on.
714
+ */
715
+ interface JobAlertStoredFilters {
716
+ jobFunctions?: string[];
717
+ seniorityLevels?: string[];
718
+ remoteOptions?: string[];
719
+ placeIds?: string[];
720
+ salaryMin?: number | null;
721
+ salaryMax?: number | null;
722
+ salaryCurrency?: string | null;
723
+ frequency?: string;
724
+ }
725
+ interface JobAlertPreference {
726
+ id: string;
727
+ label: string | null;
728
+ frequency: string;
729
+ isActive: boolean;
730
+ filters: JobAlertStoredFilters;
731
+ /** Per-preference HMAC token for `updatePreference`/`deletePreference`. */
732
+ manageToken: string;
733
+ }
734
+ interface JobAlertManageState {
735
+ object: 'job_alert_manage_state';
736
+ email: string;
737
+ confirmed: boolean;
738
+ unsubscribed: boolean;
739
+ preferences: JobAlertPreference[];
740
+ }
741
+ /** Query for `jobAlerts.manage` — the HMAC manage token from the digest email. */
742
+ type JobAlertManageQuery = {
743
+ subscription: string;
744
+ token: string;
745
+ };
746
+ /** Body for `jobAlerts.unsubscribe` / `resubscribe` (HMAC manage token). */
747
+ type JobAlertManageTokenInput = {
748
+ subscriptionId: string;
749
+ token: string;
750
+ preferenceId?: string;
751
+ };
752
+ type JobAlertUpdatePreferenceInput = {
753
+ subscriptionId: string;
754
+ preferenceId: string;
755
+ token: string;
756
+ /** Required — the update is a full replace; restate it to avoid resetting cadence. */
757
+ frequency: JobAlertFrequency;
758
+ filters?: JobAlertFiltersInput;
759
+ };
760
+ type JobAlertDeletePreferenceInput = {
761
+ subscriptionId: string;
762
+ preferenceId: string;
763
+ token: string;
764
+ };
765
+
599
766
  interface TaxonomyGeo {
600
767
  lat: number | null;
601
768
  lng: number | null;
@@ -622,6 +789,17 @@ interface TaxonomyResolution {
622
789
  redirectTo: string | null;
623
790
  geo: TaxonomyGeo | null;
624
791
  }
792
+ /**
793
+ * Query for `taxonomy.places.list()`. Omit it (or `q`) for the full locations
794
+ * directory; pass `q` (≥2 chars) for location autocomplete — the top name
795
+ * matches ranked, what a location search field renders.
796
+ */
797
+ type PlacesListQuery = {
798
+ /** Location autocomplete query; ≥2 chars → top name matches, under 2 → empty. */
799
+ q?: string;
800
+ /** Max autocomplete results when `q` is given (1–50; default 10). */
801
+ limit?: number;
802
+ };
625
803
  /**
626
804
  * A place in the board's locations directory (`taxonomy.places.list()` →
627
805
  * `GET /places`), the data the `/jobs/locations/` index renders. `jobCount` is
@@ -701,6 +879,9 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
701
879
  search(body: JobsSearchBody, query?: Record<string, never>, options?: FetchOptions): Promise<JobCardSearchEnvelope>;
702
880
  similar(jobSlug: string, query?: JobsSimilarQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJobCard>>;
703
881
  };
882
+ embed: {
883
+ jobs(query?: EmbedJobsQuery, options?: FetchOptions): Promise<ListEnvelope<PublicJobCard>>;
884
+ };
704
885
  companies: {
705
886
  list(query?: CompaniesListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicCompany>>;
706
887
  retrieve(companySlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicCompany>;
@@ -711,6 +892,8 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
711
892
  posts: {
712
893
  list(query?: BlogPostsListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicBlogPostSummary>>;
713
894
  retrieve(postSlug: string, query?: Record<string, never>, options?: FetchOptions): Promise<PublicBlogPost>;
895
+ adjacent(postSlug: string, options?: FetchOptions): Promise<PublicBlogAdjacentPosts>;
896
+ similar(postSlug: string, query?: BlogSimilarQuery, options?: FetchOptions): Promise<ListEnvelope<PublicBlogPostSummary>>;
714
897
  };
715
898
  tags: {
716
899
  list(query?: Record<string, never>, options?: FetchOptions): Promise<ListEnvelope<PublicBlogTag>>;
@@ -739,6 +922,9 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
739
922
  unsave(jobId: string, query?: Record<string, never>, options?: FetchOptions): Promise<void>;
740
923
  };
741
924
  };
925
+ password: {
926
+ verify(password: string, options?: FetchOptions): Promise<BoardAccessGrant>;
927
+ };
742
928
  taxonomy: {
743
929
  categories: {
744
930
  resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
@@ -747,14 +933,28 @@ declare function createBoardClient(options: CreateBoardClientOptions): {
747
933
  resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
748
934
  };
749
935
  places: {
750
- list(options?: FetchOptions): Promise<ListEnvelope<PublicPlace>>;
936
+ list(query?: PlacesListQuery, options?: FetchOptions): Promise<ListEnvelope<PublicPlace>>;
751
937
  resolve(slug: string, options?: FetchOptions): Promise<TaxonomyResolution>;
752
938
  };
753
939
  };
754
940
  redirects: {
755
941
  resolve(path: string, options?: FetchOptions): Promise<RedirectResolution>;
756
942
  };
943
+ jobAlerts: {
944
+ subscribe(input: JobAlertSubscribeInput, options?: FetchOptions): Promise<JobAlertSubscription>;
945
+ confirm(input: {
946
+ token: string;
947
+ }, options?: FetchOptions): Promise<JobAlertConfirmation>;
948
+ resendConfirmation(input: {
949
+ email: string;
950
+ }, options?: FetchOptions): Promise<JobAlertResendResult>;
951
+ manage(query: JobAlertManageQuery, options?: FetchOptions): Promise<JobAlertManageState>;
952
+ unsubscribe(input: JobAlertManageTokenInput, options?: FetchOptions): Promise<JobAlertManageResult>;
953
+ resubscribe(input: JobAlertManageTokenInput, options?: FetchOptions): Promise<JobAlertManageResult>;
954
+ updatePreference(input: JobAlertUpdatePreferenceInput, options?: FetchOptions): Promise<JobAlertManageResult>;
955
+ deletePreference(input: JobAlertDeletePreferenceInput, options?: FetchOptions): Promise<JobAlertManageResult>;
956
+ };
757
957
  };
758
958
  type BoardSdk = ReturnType<typeof createBoardClient>;
759
959
 
760
- export { ACCESS_TOKEN_KEY, type Awaitable, type BlogAuthorEmbed, type BlogPostsListQuery, type BlogSearchBody, type BlogTagEmbed, BoardApiError, type BoardAuthSession, BoardClient, type BoardRequest, type BoardSdk, type BoardSeo, type BoardUser, type CompaniesListQuery, type CompaniesSearchBody, type CompanyJobsListQuery, type CreateBoardClientOptions, type CustomStorage, type EducationRequirement, type EmploymentType, type FetchOptions, type ForgotPasswordBody, type JobCardListEnvelope, type JobCardSearchEnvelope, type JobCompany, type JobsListQuery, type JobsSearchBody, type ListEnvelope, type Logger, type LoginBody, type LogoutBody, type OfficeLocation, type PublicBlogAuthor, type PublicBlogPost, type PublicBlogPostSummary, type PublicBlogTag, type PublicBoard, type PublicBoardAnalytics, type PublicBoardFeatures, type PublicBoardTheme, type PublicCompany, type PublicJob, type PublicJobCard, type PublicPlace, REFRESH_TOKEN_KEY, type RedirectResolution, type RefreshBody, type RegisterBody, type RelatedSearch, type RemoteOption, type RemotePermit, type RemoteTimezone, type ResetPasswordBody, SDK_VERSION, type SaveJobBody, type SavedJob, type SavedJobsListQuery, type SearchEnvelope, type Seniority, type StorageMode, type StorefrontPagination, type TaxonomyGeo, type TaxonomyResolution, type VerifyEmailBody, createBoardClient, isBoardApiError, isConflict, isForbidden, isNotFound, isRateLimited, isUnauthorized, isValidationError };
960
+ export { ACCESS_TOKEN_KEY, type Awaitable, BOARD_ACCESS_GRANT_KEY, type BlogAuthorEmbed, type BlogPostsListQuery, type BlogSearchBody, type BlogSimilarQuery, type BlogTagEmbed, type BoardAccessGrant, BoardApiError, type BoardAuthSession, BoardClient, type BoardRequest, type BoardSdk, type BoardSeo, type BoardUser, type CompaniesListQuery, type CompaniesSearchBody, type CompanyJobsListQuery, type CreateBoardClientOptions, type CustomStorage, type EducationRequirement, type EmbedJobsQuery, type EmploymentType, type FetchOptions, type ForgotPasswordBody, type JobAlertConfirmation, type JobAlertDeletePreferenceInput, type JobAlertFiltersInput, type JobAlertFrequency, type JobAlertManageQuery, type JobAlertManageResult, type JobAlertManageState, type JobAlertManageTokenInput, type JobAlertPreference, type JobAlertRemoteOption, type JobAlertResendResult, type JobAlertStoredFilters, type JobAlertSubscribeInput, type JobAlertSubscription, type JobAlertUpdatePreferenceInput, type JobCardListEnvelope, type JobCardSearchEnvelope, type JobCompany, type JobsListQuery, type JobsSearchBody, type ListEnvelope, type Logger, type LoginBody, type LogoutBody, type OfficeLocation, type PlacesListQuery, type PublicBlogAdjacentPosts, type PublicBlogAuthor, type PublicBlogPost, type PublicBlogPostSummary, type PublicBlogTag, type PublicBoard, type PublicBoardAnalytics, type PublicBoardFeatures, type PublicBoardTheme, type PublicCompany, type PublicJob, type PublicJobCard, type PublicPlace, REFRESH_TOKEN_KEY, type RedirectResolution, type RefreshBody, type RegisterBody, type RelatedSearch, type RemoteOption, type RemotePermit, type RemoteTimezone, type ResetPasswordBody, SDK_VERSION, type SaveJobBody, type SavedJob, type SavedJobsListQuery, type SearchEnvelope, type Seniority, type StorageMode, type StorefrontPagination, type TaxonomyGeo, type TaxonomyResolution, type VerifyEmailBody, createBoardClient, isBoardApiError, isBoardPasswordRequired, isConflict, isForbidden, isNotFound, isRateLimited, isUnauthorized, isValidationError };