craterview 0.1.3 → 0.3.8

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.
Files changed (3) hide show
  1. package/README.md +55 -17
  2. package/index.ts +57 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,7 +23,7 @@ client installs without dragging a transitive tree behind it.
23
23
  import { CraterView } from "craterview";
24
24
 
25
25
  const cv = new CraterView({ apiKey: "cv_..." });
26
- const job = await cv.run(file, { style: "photo" });
26
+ const job = await cv.run(file, { scale: 4 });
27
27
  const blob = await job.blob();
28
28
  ```
29
29
 
@@ -31,12 +31,30 @@ const blob = await job.blob();
31
31
  API's shape rather than an accident, but it is not something every caller should have to
32
32
  reimplement.
33
33
 
34
+ Build against `echo` first. It costs no credits, needs no GPU and returns a real result — a
35
+ plain upscale — so the request, the parameters and the response are the ones a paid model
36
+ gives, and your integration needs no change when you switch. When you are ready, swap the
37
+ model name for the one you want.
38
+
39
+ ```ts
40
+ const job = await cv.run(file, {
41
+ // echo is free, for building against. Swap in a paid model when you are ready —
42
+ // cv-enhance-v3 to enlarge, cv-restore-v1 to repair, cv-headshot-v1 for portraits.
43
+ model: "echo",
44
+ scale: 2, wait: 30,
45
+ });
46
+ const blob = await job.blob();
47
+ ```
48
+
49
+ A parameter one model publishes is not one another accepts — `cv.models()` says which — and
50
+ a value a model does not publish is refused at submit rather than ignored.
51
+
34
52
  ## An API key
35
53
 
36
54
  Keys begin with `cv_` and are issued from your dashboard.
37
55
 
38
56
  ```ts
39
- const cv = new CraterView({ apiKey: process.env.CRATERVIEW_API_KEY });
57
+ const cv = new CraterView({ apiKey: process.env.CV_API_KEY });
40
58
  ```
41
59
 
42
60
  A key carries your whole allowance and does not expire. **Do not ship one to a browser** —
@@ -47,10 +65,10 @@ browser a session instead.
47
65
 
48
66
  ```ts
49
67
  const job = await cv.run(image, { // Blob | ArrayBuffer | Uint8Array
50
- model: "cv-restore-v1", // default
68
+ model: "cv-enhance-v3", // default
51
69
  wait: 30, // seconds to hold the connection open
52
70
  timeoutMs: 600_000, // total before giving up
53
- style: "photo", // model parameters pass straight through
71
+ scale: 4, // model parameters pass straight through
54
72
  });
55
73
  ```
56
74
 
@@ -66,7 +84,7 @@ Useful when you want to hold the key, submit later, or fan out.
66
84
 
67
85
  ```ts
68
86
  const inputKey = await cv.upload(file); // → "inputs/..."
69
- let job = await cv.submit(inputKey, { style: "photo" });
87
+ let job = await cv.submit(inputKey, { scale: 4 });
70
88
  job = await cv.waitFor(job, 600_000);
71
89
  const blob = await job.blob();
72
90
  ```
@@ -90,7 +108,7 @@ const key = newIdempotencyKey(); // once, before the first attempt
90
108
  let job;
91
109
  for (let attempt = 0; attempt < 3; attempt++) {
92
110
  try {
93
- job = await cv.submit(inputKey, { idempotencyKey: key, style: "photo" });
111
+ job = await cv.submit(inputKey, { idempotencyKey: key, scale: 4 });
94
112
  break;
95
113
  } catch (e) {
96
114
  if (e instanceof CraterViewError) throw e; // the server answered; do not retry
@@ -102,7 +120,8 @@ for (let attempt = 0; attempt < 3; attempt++) {
102
120
  Generating a fresh key per attempt defeats the point entirely — the server has nothing to
103
121
  match against and every attempt starts its own job. Reusing a key with a *different* body
104
122
  is rejected with 409 rather than quietly handing back the earlier result. Claims are kept
105
- for 24 hours; past that the same key starts new work.
123
+ for as long as the job's record is, which is not deleted; the same key always returns
124
+ the same job.
106
125
 
107
126
  ## Errors
108
127
 
@@ -111,7 +130,7 @@ Everything thrown by this client extends `CraterViewError`, so one `catch` handl
111
130
 
112
131
  | Class | Meaning |
113
132
  |---|---|
114
- | `RateLimited` | 429. `.retryAfter` is seconds until the window rolls over. |
133
+ | `RateLimited` | 429. `.retryAfter` is seconds to wait: until the window rolls over for the request rate, or a short fixed interval to poll on for the in-flight cap. |
115
134
  | `JobFailed` | The job ran and did not succeed. `.message` says what you can do about it; `.errorCode` is the half to branch on. |
116
135
  | `CraterViewError` | Everything else, including 4xx and 5xx from the API. |
117
136
 
@@ -131,8 +150,8 @@ Every field the API publishes on a job is exposed here.
131
150
  | `error` | Set when the job failed. Safe to show a user |
132
151
  | `errorCode` | The same fact, as a stable identifier. Branch on this, show the other |
133
152
  | `credits` | **What you were billed** |
134
- | `etaSeconds` | The estimate made at submit. Absent once the job has settled |
135
- | `community` | True when the job is on the community queue: served after priority work, taking a small share of it |
153
+ | `etaSeconds` | Seconds until the job is expected to finish, recomputed on every read — it counts down while the job runs. Absent once the job has settled |
154
+ | `community` | True when the job is on the community queue: served after priority work, always taking a share of it, so it never stalls behind paid work |
136
155
  | `outputUrl`, `downloadUrl` | The result, presigned. One to display, one to save |
137
156
  | `thumbUrl` | A small JPEG of the result, for listings. Null when none was drawn |
138
157
  | `inputUrl` | The file you sent. Null once it has expired — inputs go after a day |
@@ -155,9 +174,11 @@ accepts exactly that length and nothing else. You do not pass it — it is read
155
174
  in hand, which is what makes it impossible to get wrong.
156
175
 
157
176
  **Running out of credit does not stop you.** A job submitted against a balance of zero is
158
- accepted, charged and run — it simply waits in the community queue for capacity that paid
159
- work is not using, and comes back with `community` set. There is no payment error to
160
- handle: credit buys a place at the front of the queue rather than the right to submit.
177
+ accepted, charged and run — it simply waits in the community queue, which is served after
178
+ paid work and always takes a share of it, so it never stalls behind paid work. It comes back
179
+ with `community` set. There is no payment error to handle:
180
+ paying — with credit, or with a subscription — buys a place at the front of the queue rather
181
+ than the right to submit.
161
182
  `etaSeconds` covers the whole wait, queue time included, so a community job simply reports
162
183
  a longer one.
163
184
 
@@ -204,7 +225,7 @@ Every copy of a delivery states the same thing, so the first one you accept is t
204
225
  answer — later copies of an `id` you have already handled can be dropped rather than
205
226
  reconciled.
206
227
 
207
- **Pass the raw body.** Most frameworks parse JSON for you, and re-serialising it changes the
228
+ **Pass the raw body.** Most frameworks parse JSON for you, and re-serializing it changes the
208
229
  bytes the signature was computed over — hence `express.raw` above.
209
230
 
210
231
  `verifyWebhook` is async because it uses WebCrypto, which is what lets it run unchanged in
@@ -245,7 +266,7 @@ old one. You cannot revoke the key you are calling with.
245
266
  ## Everything else
246
267
 
247
268
  ```ts
248
- await cv.models(); // models, parameter schemas, queue depth
269
+ await cv.models(); // models, parameter schemas, which queue you are on
249
270
  await cv.job("job_..."); // one job by id
250
271
  await cv.usage(); // credit balance, spend and job counts
251
272
 
@@ -254,6 +275,10 @@ for await (const job of cv.jobs({ limit: 50, status: "succeeded" })) {
254
275
  }
255
276
  ```
256
277
 
278
+ `limit` is how many jobs one request fetches, not how many you get: the loop keeps going
279
+ until your history runs out. 200 is the largest page the API serves, and a bigger number is
280
+ fetched as 200.
281
+
257
282
  ## Configuration
258
283
 
259
284
  ```ts
@@ -263,7 +288,20 @@ new CraterView({
263
288
  });
264
289
  ```
265
290
 
266
- `baseUrl` is what you change to point at a local gateway.
291
+ `baseUrl` is what you change to point at a local server.
292
+
293
+ ## From Claude Code
294
+
295
+ This repository is also a Claude Code marketplace. Two plugins: `craterview` connects
296
+ CraterView's hosted tools so the assistant enhances images in the conversation, and
297
+ `craterview-api` teaches an agent to call the API from code with this client. See
298
+ [`plugins/`](plugins/) for what each does and how it finds a key.
299
+
300
+ ```
301
+ /plugin marketplace add craterviewai/craterview-ts
302
+ /plugin install craterview@craterviewai
303
+ /plugin install craterview-api@craterviewai
304
+ ```
267
305
 
268
306
  ## Versioning
269
307
 
@@ -272,7 +310,7 @@ or `ModelInfo`, or changes the type of one, gets a minor bump while this is `0.x
272
310
  bump after `1.0.0`; anything purely additive gets a patch. Pin what you depend on.
273
311
 
274
312
  The API this wraps adds fields to its responses without warning, so treat an unfamiliar key in
275
- `result` or a `status` you do not recognise as something to ignore rather than to fail on.
313
+ `result` or a `status` you do not recognize as something to ignore rather than to fail on.
276
314
 
277
315
  ## License
278
316
 
package/index.ts CHANGED
@@ -9,21 +9,19 @@
9
9
  *
10
10
  * Zero dependencies: fetch and Blob are standard in Node 18+ and every browser, so the
11
11
  * client stays installable anywhere without dragging a transitive tree behind it.
12
- *
13
- * **This file is published.** It goes to GitHub and npm, and `main` points at the source, so
14
- * every comment here ships and a doc-comment on an exported member appears in a consumer's
15
- * editor. Write for someone who can see this package and nothing else: what the client does
16
- * and what a caller has to know, never how the service behind it is built.
17
12
  */
18
13
 
19
14
  // Mirrored from package.json, which is the number a release bumps. It cannot be imported
20
15
  // from there — this ships as TypeScript, so the import would have to resolve in the
21
16
  // consumer's toolchain — so test/version.test.ts asserts the two agree.
22
- export const VERSION = "0.1.3";
17
+ export const VERSION = "0.3.8";
23
18
  const DEFAULT_BASE_URL = "https://api.craterview.ai";
24
- // The server rejects a longer wait outright, so asking for one costs a 422 rather than a
25
- // longer wait. Keep in step with MAX_WAIT_SECONDS in the gateway.
19
+ // The server rejects a longer wait outright, so asking for one costs a 422 rather than the
20
+ // wait you asked for. `run()` clamps to this rather than letting that happen.
26
21
  const MAX_SERVER_WAIT = 30;
22
+ // The largest page the job history serves. Same reasoning: a bigger `limit` is refused, so
23
+ // `jobs()` clamps rather than spending a request to be told no.
24
+ const MAX_PAGE = 200;
27
25
 
28
26
  /**
29
27
  * A random key for safely repeating a submission.
@@ -72,11 +70,11 @@ export class CraterViewError extends Error {
72
70
  * request rate, and the account's cap on jobs queued or running at the same time, which
73
71
  * exists so one caller cannot occupy the whole fleet.
74
72
  *
75
- * `retryAfter` is seconds to wait, and how good a number it is depends on which limit you
76
- * hit: exact for the rate limit, where it is when the window rolls over, and a hint for
77
- * the in-flight cap, where a slot frees when one of your own jobs finishes and the server
78
- * can only quote the model's typical duration. `usage()` reports the cap and what you
79
- * currently hold against it.
73
+ * `retryAfter` is seconds to wait, and what it means depends on which limit you hit: for
74
+ * the rate limit it is when the window rolls over; for the in-flight cap it is a fixed
75
+ * short interval to poll on, since a slot frees when one of your own jobs finishes and
76
+ * nothing here predicts that. `usage()` reports the cap and what you currently hold
77
+ * against it.
80
78
  */
81
79
  export class RateLimited extends CraterViewError {
82
80
  constructor(message: string, readonly retryAfter?: number) {
@@ -132,7 +130,7 @@ export class InvalidSignature extends CraterViewError {}
132
130
  *
133
131
  * Throws {@link InvalidSignature} if it does not check out. **Verify before you parse**, and
134
132
  * pass the raw body exactly as received — most frameworks parse JSON for you, and
135
- * re-serialising it changes the bytes the signature was computed over. In Express that means
133
+ * re-serializing it changes the bytes the signature was computed over. In Express that means
136
134
  * `express.raw({ type: "application/json" })` on this route.
137
135
  *
138
136
  * Async because it uses WebCrypto, which is what makes this work unchanged in Node and in a
@@ -220,8 +218,8 @@ function timingSafeEqual(a: string, b: string): boolean {
220
218
  }
221
219
 
222
220
  /**
223
- * One entry from the model catalogue, exactly as it arrives. Snake case is the wire's, as
224
- * with `JobData` — there is no wrapper class here because there is no behaviour to add.
221
+ * One entry from the model catalog, exactly as it arrives. Snake case is the wire's, as
222
+ * with `JobData` — there is no wrapper class here because there is no behavior to add.
225
223
  */
226
224
  export interface ModelInfo {
227
225
  /** The public name, and what you pass as `model` when submitting. */
@@ -248,6 +246,11 @@ export interface ModelInfo {
248
246
  * look identical in `accepts` alone.
249
247
  */
250
248
  video_coming_soon?: boolean;
249
+ /**
250
+ * How long this model is for. `fixed` is a lasting part of the service; `comet` is a
251
+ * featured model that may be withdrawn at short notice, so build on it knowing that.
252
+ */
253
+ tenure?: "fixed" | "comet";
251
254
  max_input_bytes: number;
252
255
  /**
253
256
  * The ceilings **your key** is held to, not the model's widest. Community work is bounded
@@ -272,22 +275,17 @@ export interface ModelInfo {
272
275
  credits_per_video_second?: number | null;
273
276
  reference_fps: number;
274
277
  params_schema: Record<string, unknown>;
275
- /**
276
- * Work ahead of you **on the queue your key would use** — not a platform-wide total.
277
- * Read it with `community`, which says which queue that is.
278
- */
279
- queue_depth: number;
280
278
  /**
281
279
  * Whether your key's work goes to the community queue, which is served after priority
282
- * work and takes a small share of it. False once the account holds credit.
280
+ * work and always takes a share of it, so it never stalls behind paid work. False while
281
+ * the account is paying — holding credit, or on a subscription.
283
282
  *
284
- * The same field, meaning the same thing, as `Job.community` — these are the two places
285
- * the API describes a wait, and they describe it the same way.
283
+ * The same field, meaning the same thing, as `Job.community`. How long a wait will be
284
+ * is answered on the job, once you have one — `Job.eta_seconds` — and nowhere else.
286
285
  */
287
286
  community: boolean;
288
287
  /** Of `params_schema`, the ones that apply to still images only. */
289
288
  image_only_params: string[];
290
- /** A representative job, for sizing a progress indicator before any eta arrives. */
291
289
  }
292
290
 
293
291
  /** One key on the account. Never the key itself — only the prefix, which identifies it. */
@@ -300,7 +298,11 @@ export interface KeyInfo {
300
298
  /** When it stopped working, or null while it still does. */
301
299
  revoked_at: string | null;
302
300
  rate_limit_per_minute: number;
303
- /** Whether this is the key you are calling with. It cannot be revoked while it is. */
301
+ /**
302
+ * Whether this is the key you are calling with. It cannot be revoked by id while it is;
303
+ * `endCurrentKey()` (`DELETE /v1/keys/current`) ends it on request, unless it is the
304
+ * account's only way back in.
305
+ */
304
306
  current: boolean;
305
307
  }
306
308
 
@@ -314,20 +316,14 @@ export interface NewKey {
314
316
  rate_limit_per_minute: number;
315
317
  }
316
318
 
317
- /** The body of a callback, once {@link verifyWebhook} has checked it came from us. */
318
- export interface WebhookEvent {
319
- id: string;
320
- custom_id: string | null;
321
- model: string | null;
322
- status: JobStatus;
323
- error: string | null;
324
- error_code: string | null;
325
- community: boolean;
326
- credits: number | null;
327
- created_at: string | null;
328
- finished_at: string | null;
329
- result: Record<string, unknown> | null;
330
- }
319
+ /**
320
+ * The body of a callback, once {@link verifyWebhook} has checked it came from us.
321
+ *
322
+ * A job, delivered rather than fetched: the same fields a read of the job states, under the
323
+ * same names, every one present and null where it does not apply — so a receiver parses one
324
+ * shape rather than branching on which keys arrived.
325
+ */
326
+ export interface WebhookEvent extends Required<JobData> {}
331
327
 
332
328
  export type JobStatus = "queued" | "running" | "succeeded" | "failed";
333
329
 
@@ -400,10 +396,12 @@ export interface JobData {
400
396
  * counting time spent waiting for a GPU as well as time spent on one. An estimate and never
401
397
  * a promise — read it as guidance, not a deadline. Absent once a job has settled.
402
398
  *
403
- * `community` says the job was submitted against a balance of zero and is on the queue
404
- * served after priority work, which takes a small share of it rather than only what is left.
405
- * Nothing is refused for want of credit — credit buys a place at the front of the queue, not
406
- * the right to submit — so an empty balance means a longer wait and never an error.
399
+ * `community` says the job is on the queue served after priority work, which always takes
400
+ * a share of it rather than only what is left over — so it waits longer at busy times and
401
+ * never stalls behind paid work. That is where an account goes when it has not
402
+ * paid for priority — by holding credit or by subscribing. Nothing is refused for want of
403
+ * either: paying buys a place at the front of the queue, not the right to submit, so an
404
+ * account that has not paid means a longer wait and never an error.
407
405
  */
408
406
  export class Job {
409
407
  constructor(private readonly data: JobData) {}
@@ -418,7 +416,7 @@ export class Job {
418
416
  get etaSeconds() { return this.data.eta_seconds ?? null; }
419
417
  /** Your own name for this job, or null if you did not send one. */
420
418
  get customId() { return this.data.custom_id ?? null; }
421
- // Defaulted rather than nulled: a gateway too old to send the field is not running a
419
+ // Defaulted rather than nulled: a server too old to send the field is not running a
422
420
  // community queue at all, so its jobs are paid work.
423
421
  get community() { return this.data.community ?? false; }
424
422
  // Nulled rather than defaulted, unlike `community` above: absent means the image was not
@@ -547,17 +545,17 @@ export class CraterView {
547
545
  }
548
546
 
549
547
  /**
550
- * Available models: parameter schemas, prices, limits, and your queue.
548
+ * Available models: parameter schemas, prices, limits, and which queue you are on.
551
549
  *
552
550
  * Prices are published here, so the cost of a job is knowable before submitting it.
553
551
  *
554
- * Needs a key, and not only because the figures are live: `queue_depth` and `community`
555
- * are answered **for the queue your key would use**. The API looks up the account's
556
- * balance and reports the queue a job from this key would land in — so two keys asking
557
- * at the same moment can get different numbers, and buying credit changes yours.
552
+ * `community` and the limits are answered **for the queue your key would use**. The API
553
+ * looks up the account's standing and reports the queue a job from this key would land
554
+ * in — so two keys asking at the same moment can get different answers, and paying —
555
+ * credit or a subscription — changes yours.
558
556
  *
559
557
  * A model that is available is not always listed — a model in trial, or being retired,
560
- * stays usable by name while absent from this catalogue.
558
+ * stays usable by name while absent from this catalog.
561
559
  */
562
560
  async models(): Promise<ModelInfo[]> {
563
561
  return await this.request("GET", "/v1/models") as ModelInfo[];
@@ -594,7 +592,7 @@ export class CraterView {
594
592
  * job and is billed for both.
595
593
  */
596
594
  async submit(inputKey: string, options: SubmitOptions = {}): Promise<Job> {
597
- const { wait = 0, idempotencyKey, webhookUrl, customId, model = "cv-restore-v1",
595
+ const { wait = 0, idempotencyKey, webhookUrl, customId, model = "cv-enhance-v3",
598
596
  ...params } = options;
599
597
  // Annotated, not inferred: without this the two branches unify to a type whose key
600
598
  // may be `undefined`, which is not assignable to Record<string, string>.
@@ -618,11 +616,17 @@ export class CraterView {
618
616
  *
619
617
  * Scoped to the account rather than to this key, so a key sees every job the account has
620
618
  * run and not only the ones it submitted itself.
619
+ *
620
+ * `limit` is how many jobs one request fetches, not how many you get: iteration continues
621
+ * until the history runs out. 200 is the largest page the API serves, and a bigger number
622
+ * is fetched as 200 rather than sent and refused.
621
623
  */
622
624
  async *jobs(options: { limit?: number; status?: JobStatus } = {}): AsyncGenerator<Job> {
623
625
  let before: string | undefined;
624
626
  for (;;) {
625
- const params = new URLSearchParams({ limit: String(options.limit ?? 50) });
627
+ const params = new URLSearchParams({
628
+ limit: String(Math.min(options.limit ?? 50, MAX_PAGE)),
629
+ });
626
630
  if (options.status) params.set("status", options.status);
627
631
  if (before) params.set("before", before);
628
632
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craterview",
3
- "version": "0.1.3",
3
+ "version": "0.3.8",
4
4
  "dependencies": {},
5
5
  "description": "TypeScript client for the CraterView image enhancement API",
6
6
  "type": "module",