@sandstormapp/sdk 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sandstormapp/sdk
2
2
 
3
- Sandstorm's public API as a typed client for Node and edge runtimes: one method per tool in the catalogue, typed from the tool's own JSON Schema, and the tasks those calls start. It is generated from the API's OpenAPI document and depends on nothing at runtime but `fetch`.
3
+ Sandstorm's public API as a typed client for Node and edge runtimes: one method per tool in the catalogue, typed from the tool's own JSON Schema. A tool that only reads answers its JSON at once; every other tool starts a task the client follows. It is generated from the API's OpenAPI document and depends on nothing at runtime but `fetch`.
4
4
 
5
5
  ```bash
6
6
  yarn add @sandstormapp/sdk
@@ -36,27 +36,43 @@ new Sandstorm({ apiKey, brand: '2f7fa2e7-c931-47b2-a66a-f1393233f2b3' })
36
36
 
37
37
  `brand` is sent as `x-sandstorm-brand` on every request. `baseUrl` points the client somewhere other than `https://agent.sandstormapp.com`, such as an API running locally.
38
38
 
39
+ ## Reading
40
+
41
+ A tool that only reads, such as `search_assets`, `read_offer` or `get_brand_identity`, is a `GET` with its arguments as query parameters, a list as its name repeated, and answers the tool's own JSON at once; the method takes the same arguments as an object:
42
+
43
+ ```ts
44
+ const { assets } = await sandstorm.tools.search_assets({ types: ['image'], limit: 10 })
45
+ const identity = await sandstorm.tools.get_brand_identity()
46
+ ```
47
+
48
+ ```bash
49
+ curl 'https://agent.sandstormapp.com/v1/tools/search_assets?types=image&limit=10' \
50
+ -H "Authorization: Bearer $SANDSTORM_API_KEY"
51
+ ```
52
+
53
+ A read the tool refuses, such as a ref it cannot find, throws with status 422 and the tool's reason.
54
+
39
55
  ## Calling a tool
40
56
 
41
- Every tool is a method on `sandstorm.tools`, named as the catalogue names it, taking the body the API holds it to. A call starts a task and answers its id at once:
57
+ Every other tool is a method on `sandstorm.tools`, named as the catalogue names it, taking the body the API holds it to. A call starts a task and answers its id at once:
42
58
 
43
59
  ```ts
44
- const started = await sandstorm.tools.search_assets({ types: ['image'], limit: 10 })
60
+ const started = await sandstorm.tools.generate_image_nano_banana_pro({ prompt, asset_name: 'Sunlit Mug' })
45
61
  // { task_id: '…', status: 'running' }
46
62
  ```
47
63
 
48
64
  ```bash
49
- curl -X POST https://agent.sandstormapp.com/v1/tools/search_assets \
65
+ curl -X POST https://agent.sandstormapp.com/v1/tools/generate_image_nano_banana_pro \
50
66
  -H "Authorization: Bearer $SANDSTORM_API_KEY" -H 'Content-Type: application/json' \
51
- -d '{"types":["image"],"limit":10}'
67
+ -d '{"prompt":"…","asset_name":"Sunlit Mug"}'
52
68
  ```
53
69
 
54
70
  With `wait`, the API holds the request up to that many seconds (at most 200) and answers the finished task, or the started shape when the time runs out:
55
71
 
56
72
  ```ts
57
- const answer = await sandstorm.tools.search_assets({ types: ['image'], limit: 10 }, { wait: 30 })
73
+ const answer = await sandstorm.tools.generate_image_nano_banana_pro({ prompt, asset_name: 'Sunlit Mug' }, { wait: 30 })
58
74
  if (answer.status === 'running') console.log('still running', answer.task_id)
59
- else console.log(answer.result)
75
+ else console.log(answer.files)
60
76
  ```
61
77
 
62
78
  A tool that asks the person before it runs in the app takes `confirm: true` here, since nobody is there to ask; its input type requires it, and the API refuses the call with 403 without it:
@@ -71,7 +87,7 @@ curl -X POST https://agent.sandstormapp.com/v1/tools/revoke_asset_share \
71
87
  -d '{"slug":"summer-drop","confirm":true}'
72
88
  ```
73
89
 
74
- The catalogue itself, every tool this key can call with its schema, hints and whether it asks first:
90
+ The catalogue itself, every tool this key can call with its schema, hints and whether it asks first; `readOnlyHint` marks the tools that read, and `toolMethods`, exported beside the client, says which method each tool takes:
75
91
 
76
92
  ```ts
77
93
  const { tools } = await sandstorm.catalogue()
@@ -136,12 +152,14 @@ try {
136
152
 
137
153
  | status | code | when |
138
154
  | --- | --- | --- |
139
- | 400 | `bad_request` | the body does not match the tool's schema; the message names the mismatch |
155
+ | 400 | `bad_request` | the query or body does not match the tool's schema; the message names the mismatch |
140
156
  | 401 | `unauthorized` | the bearer is not a live key |
141
157
  | 402 | `billing` | the organization cannot run Sandstorm right now |
142
158
  | 403 | `forbidden` | the tool asks first and the body did not carry `confirm: true`, or the brand named is not one of the organization's |
143
159
  | 404 | `tool_not_found`, `task_not_found` | no such tool for this brand, or no such task |
160
+ | 405 | `method_not_allowed` | a read was posted, or a task tool was read; the message names the method that works |
144
161
  | 409 | `task_not_running` | the task to stop has already ended |
162
+ | 422 | `tool_failed` | a read the tool refused, with the tool's reason |
145
163
  | 429 | `rate_limited` | more than 300 requests in a minute from this key |
146
164
 
147
165
  An answer that is not the API's, such as one from the edge, throws with its `status`, no `code`, and the text of the answer.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,17 @@
1
- import { toolNames, type components, type paths } from './openapi.ts';
2
- export { toolNames };
1
+ import { toolMethods, type components, type paths } from './openapi.ts';
2
+ export { toolMethods };
3
3
  export type Task = components['schemas']['Task'];
4
4
  export type Started = components['schemas']['Started'];
5
5
  export type Tool = components['schemas']['Tool'];
6
6
  export type Finished = Task & {
7
7
  readonly status: Exclude<Task['status'], 'running'>;
8
8
  };
9
- export type ToolName = (typeof toolNames)[number];
10
- export type ToolInput<Name extends ToolName> = paths[`/v1/tools/${Name}`]['post']['requestBody']['content']['application/json'];
9
+ export type ToolName = keyof typeof toolMethods;
10
+ type Method<Name extends ToolName> = (typeof toolMethods)[Name];
11
+ type Operation<Name extends ToolName> = NonNullable<paths[`/v1/tools/${Name}`][Method<Name>]>;
12
+ type Query<Name extends ToolName> = Operation<Name>['parameters']['query'];
13
+ export type ToolInput<Name extends ToolName> = Method<Name> extends 'get' ? NonNullable<Query<Name>> : NonNullable<Operation<Name>['requestBody']>['content']['application/json'];
14
+ export type ReadResult<Name extends ToolName> = Operation<Name>['responses'][200]['content']['application/json'];
11
15
  export type Catalogue = paths['/v1/tools']['get']['responses'][200]['content']['application/json'];
12
16
  export type TaskList = paths['/v1/tasks']['get']['responses'][200]['content']['application/json'];
13
17
  export type Stopped = paths['/v1/tasks/{taskId}/stop']['post']['responses'][202]['content']['application/json'];
@@ -18,8 +22,9 @@ export interface ToolCall<Name extends ToolName> {
18
22
  (input: ToolInput<Name>): Promise<Started>;
19
23
  (input: ToolInput<Name>, options: Wait): Promise<Task | Started>;
20
24
  }
25
+ export type ToolRead<Name extends ToolName> = undefined extends Query<Name> ? (query?: ToolInput<Name>) => Promise<ReadResult<Name>> : (query: ToolInput<Name>) => Promise<ReadResult<Name>>;
21
26
  export type Tools = {
22
- readonly [Name in ToolName]: ToolCall<Name>;
27
+ readonly [Name in ToolName]: Method<Name> extends 'get' ? ToolRead<Name> : ToolCall<Name>;
23
28
  };
24
29
  export interface Tasks {
25
30
  list(): Promise<TaskList>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { toolNames } from "./openapi.js";
2
- export { toolNames };
1
+ import { toolMethods } from "./openapi.js";
2
+ export { toolMethods };
3
3
  export class SandstormError extends Error {
4
4
  name = 'SandstormError';
5
5
  status;
@@ -38,7 +38,12 @@ export class Sandstorm {
38
38
  constructor({ apiKey, brand, baseUrl = BASE_URL }) {
39
39
  this.#baseUrl = baseUrl.replace(/\/$/, '');
40
40
  this.#headers = { authorization: `Bearer ${apiKey}`, ...(brand === undefined ? {} : { 'x-sandstorm-brand': brand }) };
41
- this.tools = Object.fromEntries(toolNames.map((name) => [name, (input, options) => this.#call(name, input, options)]));
41
+ this.tools = Object.fromEntries(Object.entries(toolMethods).map(([name, method]) => [
42
+ name,
43
+ method === 'get'
44
+ ? (query) => this.#read(name, query)
45
+ : (input, options) => this.#call(name, input, options),
46
+ ]));
42
47
  this.tasks = {
43
48
  list: () => this.#send('GET', '/v1/tasks'),
44
49
  get: (taskId) => this.#send('GET', `/v1/tasks/${taskId}`),
@@ -56,6 +61,16 @@ export class Sandstorm {
56
61
  catalogue() {
57
62
  return this.#send('GET', '/v1/tools');
58
63
  }
64
+ #read(name, query = {}) {
65
+ const search = new URLSearchParams();
66
+ for (const [key, value] of Object.entries(query)) {
67
+ for (const each of Array.isArray(value) ? value : [value])
68
+ if (each !== undefined)
69
+ search.append(key, String(each));
70
+ }
71
+ const asked = search.toString();
72
+ return this.#send('GET', `/v1/tools/${name}${asked === '' ? '' : `?${asked}`}`);
73
+ }
59
74
  #call(name, input, options) {
60
75
  const headers = options === undefined ? JSON_BODY : { ...JSON_BODY, prefer: `wait=${options.wait}` };
61
76
  return this.#send('POST', `/v1/tools/${name}`, headers, JSON.stringify(input));