@hotstack/catalogue-client 0.1.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 +79 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +145 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +32 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +5 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @hotstack/catalogue-client
|
|
2
|
+
|
|
3
|
+
A thin, typed, dependency-free client for the [HotStack Catalogue](https://github.com/hotstacklabs/hotstack-mcp-catalog)
|
|
4
|
+
read API — search MCP servers and plugins, fetch full entries, and read the
|
|
5
|
+
curated registry projection. Global `fetch` only; no runtime dependencies.
|
|
6
|
+
|
|
7
|
+
This is the **read/discovery** SDK. It never asserts that a listed server is
|
|
8
|
+
safe (the catalogue records provenance, not endorsements), and it has no auth
|
|
9
|
+
or tool-execution surface — that is a separate product.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @hotstack/catalogue-client
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node ≥ 20, or any runtime with a global `fetch` (pass `fetch` explicitly for
|
|
18
|
+
older ones).
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { CatalogueClient } from '@hotstack/catalogue-client';
|
|
24
|
+
|
|
25
|
+
const client = new CatalogueClient({ baseUrl: 'https://api.hotstack.dev' });
|
|
26
|
+
|
|
27
|
+
// Search (filters + sort mirror GET /v1/search).
|
|
28
|
+
const page = await client.search({ q: 'gmail', category: 'communication', sort: 'newest', limit: 10 });
|
|
29
|
+
console.log(page.total, page.results[0]?.id);
|
|
30
|
+
|
|
31
|
+
// Walk every match without hand-rolling pagination.
|
|
32
|
+
for await (const hit of client.searchAll({ category: 'data' })) {
|
|
33
|
+
console.log(hit.id, hit.tier);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fetch one entry (its inline `mcp` config is on `entry`, ready to paste).
|
|
37
|
+
const detail = await client.entry('stripe/stripe-mcp');
|
|
38
|
+
console.log(detail.entry);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Honesty semantics
|
|
42
|
+
|
|
43
|
+
The catalogue never fabricates data to look healthy, and neither does this
|
|
44
|
+
client:
|
|
45
|
+
|
|
46
|
+
- **Degraded vs empty.** When no source has ever ingested successfully the API
|
|
47
|
+
returns `503`; the client throws `CatalogueUnavailableError` (never an empty
|
|
48
|
+
result). A legitimate zero-hit search is a normal `SearchPage` with
|
|
49
|
+
`total: 0`.
|
|
50
|
+
- **Stale data.** `client.registry()` surfaces the `stale` flag
|
|
51
|
+
(`x-hotstack-stale`) so you can tell current data from last-good data.
|
|
52
|
+
- **ETag round-trip.** Pass the `etag` from a prior `registry()` call to
|
|
53
|
+
revalidate; an unchanged index returns `{ status: 'not_modified' }`.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { CatalogueUnavailableError, EntryNotFoundError } from '@hotstack/catalogue-client';
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await client.entry('nope/missing');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof EntryNotFoundError) { /* 404 */ }
|
|
62
|
+
if (error instanceof CatalogueUnavailableError) { /* index down, not empty */ }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
- `new CatalogueClient({ baseUrl, fetch?, userAgent? })`
|
|
69
|
+
- `search(params): Promise<SearchPage>` — `GET /v1/search`
|
|
70
|
+
- `searchAll(params): AsyncIterableIterator<SearchHit>` — pages until done
|
|
71
|
+
- `entry(id): Promise<EntryDetail>` — `GET /v1/entries/:id` (404 → `EntryNotFoundError`)
|
|
72
|
+
- `registry({ etag? }): Promise<RegistryResult>` — `GET /v1/registry`
|
|
73
|
+
- `sources(): Promise<SourceStatus[]>` — `GET /v1/sources`
|
|
74
|
+
|
|
75
|
+
The full parameter and response reference is [`docs/api.md`](https://github.com/hotstacklabs/hotstack-mcp-catalog/blob/main/docs/api.md).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EntryDetail, RegistryResult, SearchHit, SearchPage, SearchParams, SourceStatus } from './types.js';
|
|
2
|
+
export interface CatalogueClientOptions {
|
|
3
|
+
/** Catalogue service origin, e.g. "https://api.hotstack.dev". Trailing slashes are trimmed. */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
/** Fetch implementation; defaults to the global `fetch`. */
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
/** Optional User-Agent sent with every request (ignored by browsers). */
|
|
8
|
+
userAgent?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class CatalogueClient {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(options: CatalogueClientOptions);
|
|
13
|
+
/** GET /v1/search. Throws CatalogueUnavailableError on the degraded 503. */
|
|
14
|
+
search(params?: SearchParams): Promise<SearchPage>;
|
|
15
|
+
/**
|
|
16
|
+
* Walk every page of a search, yielding each hit once. Pages at the server's
|
|
17
|
+
* max limit and stops on `hasMore: false`. Honors an explicit `offset` start.
|
|
18
|
+
*/
|
|
19
|
+
searchAll(params?: SearchParams): AsyncIterableIterator<SearchHit>;
|
|
20
|
+
/** GET /v1/entries/:id. Throws EntryNotFoundError on 404, CatalogueUnavailableError on 503. */
|
|
21
|
+
entry(id: string): Promise<EntryDetail>;
|
|
22
|
+
/**
|
|
23
|
+
* GET /v1/registry. Pass `etag` from a prior call for a conditional request:
|
|
24
|
+
* an unchanged index returns `{status: 'not_modified'}`. Surfaces the strong
|
|
25
|
+
* ETag and the honest `stale` flag; throws CatalogueUnavailableError on 503.
|
|
26
|
+
*/
|
|
27
|
+
registry(opts?: {
|
|
28
|
+
etag?: string;
|
|
29
|
+
}): Promise<RegistryResult>;
|
|
30
|
+
/** GET /v1/sources — per-source ingest status. Always 200 when the service is up. */
|
|
31
|
+
sources(): Promise<SourceStatus[]>;
|
|
32
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The catalogue read client: a thin, typed wrapper over the three stable read
|
|
2
|
+
// endpoints plus /v1/sources. Zero runtime dependencies — global `fetch` only
|
|
3
|
+
// (inject one for older runtimes or tests). Read/discovery only: this SDK never
|
|
4
|
+
// grows auth or tool-execution features (that is a separate product).
|
|
5
|
+
import { CatalogueApiError, CatalogueUnavailableError, EntryNotFoundError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* The server's `limit` clamp (parseSearchParams, MAX_LIMIT in
|
|
8
|
+
* src/api/search.ts). searchAll pages at this size so it never over-requests.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_LIMIT = 100;
|
|
11
|
+
function buildSearchQuery(params) {
|
|
12
|
+
const query = new URLSearchParams();
|
|
13
|
+
if (params.q !== undefined)
|
|
14
|
+
query.set('q', params.q);
|
|
15
|
+
if (params.category !== undefined)
|
|
16
|
+
query.set('category', params.category);
|
|
17
|
+
if (params.kind !== undefined)
|
|
18
|
+
query.set('kind', params.kind);
|
|
19
|
+
if (params.tier !== undefined)
|
|
20
|
+
query.set('tier', params.tier);
|
|
21
|
+
if (params.publisher !== undefined)
|
|
22
|
+
query.set('publisher', params.publisher);
|
|
23
|
+
if (params.sort !== undefined)
|
|
24
|
+
query.set('sort', params.sort);
|
|
25
|
+
if (params.limit !== undefined)
|
|
26
|
+
query.set('limit', String(params.limit));
|
|
27
|
+
if (params.offset !== undefined)
|
|
28
|
+
query.set('offset', String(params.offset));
|
|
29
|
+
const encoded = query.toString();
|
|
30
|
+
return encoded.length > 0 ? `?${encoded}` : '';
|
|
31
|
+
}
|
|
32
|
+
async function readBody(response) {
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class CatalogueClient {
|
|
42
|
+
#baseUrl;
|
|
43
|
+
#fetch;
|
|
44
|
+
#userAgent;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
if (typeof options.baseUrl !== 'string' || options.baseUrl.length === 0) {
|
|
47
|
+
throw new TypeError('CatalogueClient requires a non-empty baseUrl');
|
|
48
|
+
}
|
|
49
|
+
this.#baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
50
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
51
|
+
if (typeof fetchImpl !== 'function') {
|
|
52
|
+
throw new TypeError('global fetch is unavailable in this runtime; pass options.fetch');
|
|
53
|
+
}
|
|
54
|
+
this.#fetch = fetchImpl;
|
|
55
|
+
this.#userAgent = options.userAgent;
|
|
56
|
+
}
|
|
57
|
+
async #request(path, init) {
|
|
58
|
+
const headers = new Headers(init?.headers);
|
|
59
|
+
headers.set('accept', 'application/json');
|
|
60
|
+
if (this.#userAgent !== undefined)
|
|
61
|
+
headers.set('user-agent', this.#userAgent);
|
|
62
|
+
// Call through a local so `fetch` keeps its own `this` (native fetch is not
|
|
63
|
+
// bound to the client instance).
|
|
64
|
+
const doFetch = this.#fetch;
|
|
65
|
+
return doFetch(`${this.#baseUrl}${path}`, { ...init, headers });
|
|
66
|
+
}
|
|
67
|
+
/** GET /v1/search. Throws CatalogueUnavailableError on the degraded 503. */
|
|
68
|
+
async search(params = {}) {
|
|
69
|
+
const path = `/v1/search${buildSearchQuery(params)}`;
|
|
70
|
+
const response = await this.#request(path);
|
|
71
|
+
if (response.status === 503)
|
|
72
|
+
throw new CatalogueUnavailableError({ status: 503, url: this.#baseUrl + path, body: await readBody(response) });
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new CatalogueApiError(`search failed with ${response.status}`, { status: response.status, url: this.#baseUrl + path, body: await readBody(response) });
|
|
75
|
+
}
|
|
76
|
+
return (await response.json());
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Walk every page of a search, yielding each hit once. Pages at the server's
|
|
80
|
+
* max limit and stops on `hasMore: false`. Honors an explicit `offset` start.
|
|
81
|
+
*/
|
|
82
|
+
async *searchAll(params = {}) {
|
|
83
|
+
const pageSize = Math.min(params.limit ?? MAX_LIMIT, MAX_LIMIT);
|
|
84
|
+
let offset = params.offset ?? 0;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const page = await this.search({ ...params, limit: pageSize, offset });
|
|
87
|
+
for (const hit of page.results)
|
|
88
|
+
yield hit;
|
|
89
|
+
if (!page.hasMore || page.results.length === 0)
|
|
90
|
+
break;
|
|
91
|
+
offset += page.results.length;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** GET /v1/entries/:id. Throws EntryNotFoundError on 404, CatalogueUnavailableError on 503. */
|
|
95
|
+
async entry(id) {
|
|
96
|
+
// Ids are publisher-namespaced ("publisher/name"); the literal "/" stays in
|
|
97
|
+
// the path, each segment percent-encoded.
|
|
98
|
+
const path = `/v1/entries/${id.split('/').map(encodeURIComponent).join('/')}`;
|
|
99
|
+
const response = await this.#request(path);
|
|
100
|
+
if (response.status === 404)
|
|
101
|
+
throw new EntryNotFoundError(id, { status: 404, url: this.#baseUrl + path, body: await readBody(response) });
|
|
102
|
+
if (response.status === 503)
|
|
103
|
+
throw new CatalogueUnavailableError({ status: 503, url: this.#baseUrl + path, body: await readBody(response) });
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new CatalogueApiError(`entry lookup failed with ${response.status}`, { status: response.status, url: this.#baseUrl + path, body: await readBody(response) });
|
|
106
|
+
}
|
|
107
|
+
return (await response.json());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* GET /v1/registry. Pass `etag` from a prior call for a conditional request:
|
|
111
|
+
* an unchanged index returns `{status: 'not_modified'}`. Surfaces the strong
|
|
112
|
+
* ETag and the honest `stale` flag; throws CatalogueUnavailableError on 503.
|
|
113
|
+
*/
|
|
114
|
+
async registry(opts = {}) {
|
|
115
|
+
const path = '/v1/registry';
|
|
116
|
+
const headers = new Headers();
|
|
117
|
+
if (opts.etag !== undefined)
|
|
118
|
+
headers.set('if-none-match', opts.etag);
|
|
119
|
+
const response = await this.#request(path, { headers });
|
|
120
|
+
if (response.status === 304)
|
|
121
|
+
return { status: 'not_modified' };
|
|
122
|
+
if (response.status === 503)
|
|
123
|
+
throw new CatalogueUnavailableError({ status: 503, url: this.#baseUrl + path, body: await readBody(response) });
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new CatalogueApiError(`registry fetch failed with ${response.status}`, { status: response.status, url: this.#baseUrl + path, body: await readBody(response) });
|
|
126
|
+
}
|
|
127
|
+
const index = (await response.json());
|
|
128
|
+
return {
|
|
129
|
+
status: 'ok',
|
|
130
|
+
index,
|
|
131
|
+
etag: response.headers.get('etag') ?? '',
|
|
132
|
+
stale: response.headers.get('x-hotstack-stale') === 'true',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** GET /v1/sources — per-source ingest status. Always 200 when the service is up. */
|
|
136
|
+
async sources() {
|
|
137
|
+
const path = '/v1/sources';
|
|
138
|
+
const response = await this.#request(path);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new CatalogueApiError(`sources fetch failed with ${response.status}`, { status: response.status, url: this.#baseUrl + path, body: await readBody(response) });
|
|
141
|
+
}
|
|
142
|
+
const body = (await response.json());
|
|
143
|
+
return body.sources;
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CatalogueApiErrorInit {
|
|
2
|
+
status: number;
|
|
3
|
+
url: string;
|
|
4
|
+
/** Parsed JSON body when available, else the raw text. */
|
|
5
|
+
body: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare class CatalogueApiError extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly body: unknown;
|
|
11
|
+
constructor(message: string, init: CatalogueApiErrorInit);
|
|
12
|
+
}
|
|
13
|
+
/** GET /v1/entries/:id → 404: the id is not in the index. */
|
|
14
|
+
export declare class EntryNotFoundError extends CatalogueApiError {
|
|
15
|
+
constructor(id: string, init: CatalogueApiErrorInit);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The API's explicit 503 degraded response — no configured source has ever
|
|
19
|
+
* ingested successfully. Distinct from a 200 empty/zero-hit result.
|
|
20
|
+
*/
|
|
21
|
+
export declare class CatalogueUnavailableError extends CatalogueApiError {
|
|
22
|
+
constructor(init: CatalogueApiErrorInit);
|
|
23
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// One error hierarchy over the catalogue API. The degraded (503) signal is
|
|
2
|
+
// never swallowed: it becomes CatalogueUnavailableError, distinct from a
|
|
3
|
+
// legitimate empty result, so a consumer can tell "down" from "empty".
|
|
4
|
+
export class CatalogueApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
url;
|
|
7
|
+
body;
|
|
8
|
+
constructor(message, init) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'CatalogueApiError';
|
|
11
|
+
this.status = init.status;
|
|
12
|
+
this.url = init.url;
|
|
13
|
+
this.body = init.body;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** GET /v1/entries/:id → 404: the id is not in the index. */
|
|
17
|
+
export class EntryNotFoundError extends CatalogueApiError {
|
|
18
|
+
constructor(id, init) {
|
|
19
|
+
super(`no catalogue entry with id "${id}"`, init);
|
|
20
|
+
this.name = 'EntryNotFoundError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The API's explicit 503 degraded response — no configured source has ever
|
|
25
|
+
* ingested successfully. Distinct from a 200 empty/zero-hit result.
|
|
26
|
+
*/
|
|
27
|
+
export class CatalogueUnavailableError extends CatalogueApiError {
|
|
28
|
+
constructor(init) {
|
|
29
|
+
super('catalogue unavailable: no source has been ingested successfully yet', init);
|
|
30
|
+
this.name = 'CatalogueUnavailableError';
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { CatalogueClient, type CatalogueClientOptions } from './client.js';
|
|
2
|
+
export { CatalogueApiError, CatalogueUnavailableError, EntryNotFoundError, type CatalogueApiErrorInit } from './errors.js';
|
|
3
|
+
export type { ComponentKind, EntryDetail, HotstackCategory, MarketplaceIndex, RegistryResult, SearchHit, SearchPage, SearchParams, SortOrder, SourceStatus, SourceTier, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// @hotstack/catalogue-client — typed, dependency-free read client for the
|
|
2
|
+
// HotStack Catalogue API. See README.md for a quickstart.
|
|
3
|
+
export { CatalogueClient } from './client.js';
|
|
4
|
+
export { CatalogueApiError, CatalogueUnavailableError, EntryNotFoundError } from './errors.js';
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type SourceTier = 'curated' | 'community' | 'indexed';
|
|
2
|
+
export type ComponentKind = 'mcp' | 'skills' | 'module' | 'cli';
|
|
3
|
+
export type HotstackCategory = 'code' | 'communication' | 'data' | 'design' | 'development' | 'productivity' | 'sales-marketing';
|
|
4
|
+
export type SortOrder = 'relevance' | 'newest' | 'name';
|
|
5
|
+
/** GET /v1/search query parameters. All optional. */
|
|
6
|
+
export interface SearchParams {
|
|
7
|
+
q?: string;
|
|
8
|
+
category?: HotstackCategory;
|
|
9
|
+
kind?: ComponentKind;
|
|
10
|
+
tier?: SourceTier;
|
|
11
|
+
publisher?: string;
|
|
12
|
+
sort?: SortOrder;
|
|
13
|
+
/** Page size, 1–100 (server default 20). */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** Page start, ≥ 0 (server default 0). */
|
|
16
|
+
offset?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface SearchHit {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Entry kind: `inline-mcp` | `bundle` | `plugin`. Kept as string; new kinds must not break clients. */
|
|
21
|
+
kind: string;
|
|
22
|
+
tier: SourceTier;
|
|
23
|
+
/** Blended ordering value in [0, 1). */
|
|
24
|
+
rank: number;
|
|
25
|
+
/** Ingest-time ranking score. */
|
|
26
|
+
score: number;
|
|
27
|
+
provenance: {
|
|
28
|
+
sourceId: string;
|
|
29
|
+
ingestedAt: string;
|
|
30
|
+
};
|
|
31
|
+
/** The full native entry (including any inline `mcp` config). Shape varies by kind. */
|
|
32
|
+
entry: unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface SearchPage {
|
|
35
|
+
total: number;
|
|
36
|
+
limit: number;
|
|
37
|
+
offset: number;
|
|
38
|
+
hasMore: boolean;
|
|
39
|
+
results: SearchHit[];
|
|
40
|
+
}
|
|
41
|
+
export interface EntryDetail {
|
|
42
|
+
id: string;
|
|
43
|
+
kind: string;
|
|
44
|
+
tier: SourceTier;
|
|
45
|
+
provenance: {
|
|
46
|
+
sourceId: string;
|
|
47
|
+
ingestedAt: string;
|
|
48
|
+
/** Present only when the id collided across sources. */
|
|
49
|
+
collision?: Record<string, unknown>;
|
|
50
|
+
};
|
|
51
|
+
ranking: {
|
|
52
|
+
score: number;
|
|
53
|
+
signals: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
entry: unknown;
|
|
56
|
+
}
|
|
57
|
+
/** The GET /v1/registry compat projection (byte-compatible MarketplaceIndex). */
|
|
58
|
+
export interface MarketplaceIndex {
|
|
59
|
+
schemaVersion: number;
|
|
60
|
+
plugins: unknown[];
|
|
61
|
+
}
|
|
62
|
+
export interface SourceStatus {
|
|
63
|
+
id: string;
|
|
64
|
+
tier: SourceTier;
|
|
65
|
+
adapter: string;
|
|
66
|
+
state: 'ok' | 'degraded' | 'never_ingested';
|
|
67
|
+
denylisted: boolean;
|
|
68
|
+
lastAttemptAt: string | null;
|
|
69
|
+
lastSuccessAt: string | null;
|
|
70
|
+
lastError: string | null;
|
|
71
|
+
lastErrorAt: string | null;
|
|
72
|
+
entryCount: number;
|
|
73
|
+
issues: Array<{
|
|
74
|
+
path: string;
|
|
75
|
+
message: string;
|
|
76
|
+
}>;
|
|
77
|
+
collisions: Array<{
|
|
78
|
+
entryId: string;
|
|
79
|
+
role: string;
|
|
80
|
+
winnerSourceId?: string;
|
|
81
|
+
sourceIds: string[];
|
|
82
|
+
}>;
|
|
83
|
+
removals: Array<{
|
|
84
|
+
entryId: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
detail: string;
|
|
87
|
+
removedAt: string;
|
|
88
|
+
}>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* GET /v1/registry outcome. `not_modified` surfaces a real 304 from an ETag
|
|
92
|
+
* round-trip; `ok` carries the strong `etag` and the honest `stale` flag
|
|
93
|
+
* (`x-hotstack-stale`) so a consumer can tell current from last-good data.
|
|
94
|
+
*/
|
|
95
|
+
export type RegistryResult = {
|
|
96
|
+
status: 'ok';
|
|
97
|
+
index: MarketplaceIndex;
|
|
98
|
+
etag: string;
|
|
99
|
+
stale: boolean;
|
|
100
|
+
} | {
|
|
101
|
+
status: 'not_modified';
|
|
102
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Types hand-authored from docs/api.md (the API reference is the contract),
|
|
2
|
+
// named to match the service's exported shapes but deliberately NOT imported
|
|
3
|
+
// from service code — the package stands alone. The live contract test keeps
|
|
4
|
+
// them honest against the real /v1 responses.
|
|
5
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hotstack/catalogue-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed, dependency-free fetch client for the HotStack Catalogue read API (search, entries, registry).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"hotstack",
|
|
27
|
+
"mcp",
|
|
28
|
+
"catalogue",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"sdk"
|
|
31
|
+
]
|
|
32
|
+
}
|