xapi-to 0.1.13 → 0.1.14

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/package.json CHANGED
@@ -1,25 +1,30 @@
1
1
  {
2
2
  "name": "xapi-to",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Agent-friendly CLI for xapi - discover and call capabilities and APIs",
5
5
  "type": "module",
6
6
  "bin": {
7
- "xapi": "src/index.ts"
7
+ "xapi": "dist/index.js",
8
+ "xapi-to": "dist/index.js"
8
9
  },
9
10
  "files": [
10
- "src",
11
- "!src/tests",
11
+ "dist",
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
- "start": "bun run src/index.ts",
15
+ "build": "tsup src/index.ts --format esm --target node18 --clean --out-dir dist --tsconfig tsconfig.build.json",
16
+ "start": "node dist/index.js",
16
17
  "dev": "XAPI_ACTION_HOST=localhost:3003 bun run src/index.ts",
18
+ "prepublishOnly": "npm run build",
17
19
  "test": "bun test src/tests"
18
20
  },
19
- "devDependencies": {
20
- "@types/bun": "latest"
21
+ "engines": {
22
+ "node": ">=18"
21
23
  },
22
- "peerDependencies": {
23
- "typescript": "^5"
24
+ "devDependencies": {
25
+ "@types/bun": "^1.3.9",
26
+ "@types/node": "^18",
27
+ "tsup": "^8.5.1",
28
+ "typescript": "^6.0.3"
24
29
  }
25
30
  }
package/src/client.ts DELETED
@@ -1,265 +0,0 @@
1
- /**
2
- * HTTP client - thin wrapper around fetch with timeout/retry
3
- */
4
-
5
- import { scheme } from './config.ts';
6
-
7
- const DEFAULT_TIMEOUT_MS = 30_000;
8
- const EXECUTE_TIMEOUT_MS = 60_000;
9
-
10
- export interface ClientOptions {
11
- actionHost: string;
12
- apiKey?: string;
13
- }
14
-
15
- export async function request<T>(
16
- url: string,
17
- options: RequestInit,
18
- timeoutMs = DEFAULT_TIMEOUT_MS,
19
- ): Promise<T> {
20
- const controller = new AbortController();
21
- const timer = setTimeout(() => controller.abort(), timeoutMs);
22
- try {
23
- const res = await fetch(url, { ...options, signal: controller.signal });
24
- if (!res.ok) {
25
- const text = await res.text();
26
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
27
- }
28
- const body = await res.json() as T;
29
- // Detect business-level auth errors (HTTP 200 but unauthorized)
30
- if (body && typeof body === 'object' && 'success' in body && (body as any).success === false) {
31
- const data = (body as any).data;
32
- if (data?.statusCode === 401 || data?.error === 'Unauthorized') {
33
- throw new Error(
34
- 'Authentication failed: ' + (data.message || 'Invalid or missing API key')
35
- + '. Run "npx xapi-to config set apiKey=<key>" to update your key.',
36
- );
37
- }
38
- if (data?.error === 'OAuth Required' || (data?.statusCode === 403 && data?.message?.includes('OAuth'))) {
39
- throw new Error(
40
- (data.message || 'OAuth authorization required')
41
- + '. Run "xapi oauth bind" to connect your account.',
42
- );
43
- }
44
- }
45
- return body;
46
- } finally {
47
- clearTimeout(timer);
48
- }
49
- }
50
-
51
- function headers(apiKey?: string): Record<string, string> {
52
- const h: Record<string, string> = { 'Content-Type': 'application/json' };
53
- if (apiKey) h['XAPI-Key'] = apiKey;
54
- return h;
55
- }
56
-
57
- function baseUrl(opts: ClientOptions): string {
58
- return `${scheme(opts.actionHost)}://${opts.actionHost}`;
59
- }
60
-
61
- // ── Actions (unified: capabilities + APIs) ───────────────────────────────────
62
-
63
- export async function actionList(
64
- opts: ClientOptions,
65
- params: { page?: number; page_size?: number; category?: string; source?: string; service_id?: string } = {},
66
- ) {
67
- const url = new URL(`${baseUrl(opts)}/v1/actions`);
68
- if (params.page) url.searchParams.set('page', String(params.page));
69
- if (params.page_size) url.searchParams.set('page_size', String(params.page_size));
70
- if (params.category) url.searchParams.set('category', params.category);
71
- if (params.source) url.searchParams.set('source', params.source);
72
- if (params.service_id) url.searchParams.set('service_id', params.service_id);
73
- return request<{ actions: unknown[]; pagination: unknown }>(
74
- url.toString(),
75
- { method: 'GET', headers: headers(opts.apiKey) },
76
- );
77
- }
78
-
79
- export async function actionSearch(
80
- query: string,
81
- opts: ClientOptions,
82
- params: { category?: string; source?: string; page?: number; page_size?: number } = {},
83
- ) {
84
- const url = new URL(`${baseUrl(opts)}/v1/actions/search`);
85
- url.searchParams.set('q', query);
86
- if (params.category) url.searchParams.set('category', params.category);
87
- if (params.source) url.searchParams.set('source', params.source);
88
- if (params.page) url.searchParams.set('page', String(params.page));
89
- if (params.page_size) url.searchParams.set('page_size', String(params.page_size));
90
- return request<{ results: unknown[]; query: string; pagination: unknown }>(
91
- url.toString(),
92
- { method: 'GET', headers: headers(opts.apiKey) },
93
- );
94
- }
95
-
96
- export async function actionCategories(opts: ClientOptions, params: { source?: string } = {}) {
97
- const url = new URL(`${baseUrl(opts)}/v1/actions/categories`);
98
- if (params.source) url.searchParams.set('source', params.source);
99
- return request<{ categories: string[]; total: number }>(
100
- url.toString(),
101
- { method: 'GET', headers: headers(opts.apiKey) },
102
- );
103
- }
104
-
105
- export async function actionGet(id: string, opts: ClientOptions) {
106
- return request<unknown[]>(
107
- `${baseUrl(opts)}/v1/actions/${encodeURIComponent(id)}`,
108
- { method: 'GET', headers: headers(opts.apiKey) },
109
- );
110
- }
111
-
112
- export async function actionBatch(ids: string[], opts: ClientOptions) {
113
- return request<{ actions: unknown[]; missing_ids: string[] }>(
114
- `${baseUrl(opts)}/v1/actions/batch`,
115
- {
116
- method: 'POST',
117
- headers: headers(opts.apiKey),
118
- body: JSON.stringify({ ids }),
119
- },
120
- );
121
- }
122
-
123
- export async function actionCall(
124
- actionId: string,
125
- input: Record<string, unknown>,
126
- opts: ClientOptions,
127
- httpMethod?: string,
128
- ) {
129
- return request<unknown>(
130
- `${baseUrl(opts)}/v1/actions/execute`,
131
- {
132
- method: 'POST',
133
- headers: headers(opts.apiKey),
134
- body: JSON.stringify({ action_id: actionId, ...(httpMethod ? { method: httpMethod } : {}), input }),
135
- },
136
- EXECUTE_TIMEOUT_MS,
137
- );
138
- }
139
-
140
- export async function actionServices(
141
- opts: ClientOptions,
142
- params: { page?: number; page_size?: number; category?: string } = {},
143
- ) {
144
- const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
145
- if (params.page) url.searchParams.set('page', String(params.page));
146
- if (params.page_size) url.searchParams.set('page_size', String(params.page_size));
147
- if (params.category) url.searchParams.set('category', params.category);
148
- return request<{ services: unknown[]; pagination: unknown }>(
149
- url.toString(),
150
- { method: 'GET', headers: headers(opts.apiKey) },
151
- );
152
- }
153
-
154
- export async function healthCheck(opts: ClientOptions) {
155
- return request<unknown>(
156
- `${baseUrl(opts)}/health`,
157
- { method: 'GET', headers: headers(opts.apiKey) },
158
- 5_000,
159
- );
160
- }
161
-
162
- // ── Auth ──────────────────────────────────────────────────────────────────────
163
-
164
- export async function loginWithApiKey(apiKey: string, apiHost: string) {
165
- return request<{ accessToken: string; user: unknown }>(
166
- `${scheme(apiHost)}://${apiHost}/api/auth/login/apikey`,
167
- {
168
- method: 'POST',
169
- headers: { 'Content-Type': 'application/json' },
170
- body: JSON.stringify({ apiKey }),
171
- },
172
- );
173
- }
174
-
175
- // ── OAuth ──────────────────────────────────────────────────────────────────────
176
-
177
- function jwtHeaders(jwtToken: string): Record<string, string> {
178
- return { 'Content-Type': 'application/json', Authorization: `Bearer ${jwtToken}` };
179
- }
180
-
181
- export async function listKeys(jwtToken: string, apiHost: string) {
182
- return request<Array<{
183
- id: string;
184
- name: string;
185
- keyPreview: string;
186
- oauthEnabled: boolean;
187
- createdAt: string;
188
- }>>(
189
- `${scheme(apiHost)}://${apiHost}/api/keys`,
190
- { method: 'GET', headers: jwtHeaders(jwtToken) },
191
- );
192
- }
193
-
194
- export async function enableOAuthForKey(
195
- keyId: string,
196
- plaintextKey: string,
197
- jwtToken: string,
198
- apiHost: string,
199
- ) {
200
- return request<{ success: boolean; message: string }>(
201
- `${scheme(apiHost)}://${apiHost}/api/keys/${keyId}/enable-oauth`,
202
- {
203
- method: 'POST',
204
- headers: jwtHeaders(jwtToken),
205
- body: JSON.stringify({ plaintextKey }),
206
- },
207
- );
208
- }
209
-
210
- export async function listOAuthProviders(apiHost: string) {
211
- return request<Array<{
212
- id: string;
213
- name: string;
214
- type: string;
215
- grantType: string;
216
- defaultScopes: string;
217
- }>>(
218
- `${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
219
- { method: 'GET', headers: { 'Content-Type': 'application/json' } },
220
- );
221
- }
222
-
223
- export async function initiateOAuth(
224
- apiKeyId: string,
225
- providerId: string,
226
- jwtToken: string,
227
- apiHost: string,
228
- ) {
229
- return request<{ authorizationUrl: string; state: string }>(
230
- `${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
231
- {
232
- method: 'POST',
233
- headers: jwtHeaders(jwtToken),
234
- body: JSON.stringify({ apiKeyId, providerId }),
235
- },
236
- );
237
- }
238
-
239
- export async function listOAuthBindings(jwtToken: string, apiHost: string) {
240
- return request<Array<{
241
- id: string;
242
- apiKeyId: string;
243
- providerId: string;
244
- providerAccountId: string;
245
- providerAccountName: string | null;
246
- scopes: string;
247
- createdAt: string;
248
- updatedAt: string;
249
- provider: { id: string; name: string; type: string };
250
- }>>(
251
- `${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
252
- { method: 'GET', headers: jwtHeaders(jwtToken) },
253
- );
254
- }
255
-
256
- export async function deleteOAuthBinding(
257
- bindingId: string,
258
- jwtToken: string,
259
- apiHost: string,
260
- ) {
261
- return request<{ success: boolean }>(
262
- `${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
263
- { method: 'DELETE', headers: jwtHeaders(jwtToken) },
264
- );
265
- }
package/src/codegen.ts DELETED
@@ -1,307 +0,0 @@
1
- /**
2
- * Code snippet generation for API actions.
3
- *
4
- * Generates executable code in curl, Python, JavaScript, TypeScript, and Go
5
- * that calls the xapi action execute endpoint.
6
- */
7
-
8
- import { scheme } from './config.ts';
9
-
10
- // ── Types ────────────────────────────────────────────────────────────────────
11
-
12
- export interface CodegenParams {
13
- actionId: string;
14
- input: Record<string, unknown>;
15
- actionHost: string;
16
- method?: string;
17
- }
18
-
19
- interface ResolvedTarget {
20
- lang: string;
21
- lib: string;
22
- }
23
-
24
- // ── Target resolution ────────────────────────────────────────────────────────
25
-
26
- const TARGET_MAP: Record<string, ResolvedTarget> = {
27
- 'curl': { lang: 'curl', lib: 'curl' },
28
- 'python': { lang: 'python', lib: 'requests' },
29
- 'py': { lang: 'python', lib: 'requests' },
30
- 'python.requests': { lang: 'python', lib: 'requests' },
31
- 'python.httpx': { lang: 'python', lib: 'httpx' },
32
- 'py.requests': { lang: 'python', lib: 'requests' },
33
- 'py.httpx': { lang: 'python', lib: 'httpx' },
34
- 'javascript': { lang: 'javascript', lib: 'fetch' },
35
- 'js': { lang: 'javascript', lib: 'fetch' },
36
- 'javascript.fetch': { lang: 'javascript', lib: 'fetch' },
37
- 'javascript.axios': { lang: 'javascript', lib: 'axios' },
38
- 'js.fetch': { lang: 'javascript', lib: 'fetch' },
39
- 'js.axios': { lang: 'javascript', lib: 'axios' },
40
- 'typescript': { lang: 'typescript', lib: 'fetch' },
41
- 'ts': { lang: 'typescript', lib: 'fetch' },
42
- 'typescript.fetch': { lang: 'typescript', lib: 'fetch' },
43
- 'ts.fetch': { lang: 'typescript', lib: 'fetch' },
44
- 'go': { lang: 'go', lib: 'net/http' },
45
- };
46
-
47
- const SUPPORTED_TARGETS = [
48
- 'curl',
49
- 'python (py) [.requests, .httpx]',
50
- 'javascript (js) [.fetch, .axios]',
51
- 'typescript (ts) [.fetch]',
52
- 'go [net/http]',
53
- ];
54
-
55
- export function resolveTarget(raw: string): ResolvedTarget {
56
- const target = TARGET_MAP[raw.toLowerCase()];
57
- if (!target) {
58
- throw new Error(
59
- `unknown --code target: "${raw}". Supported: ${SUPPORTED_TARGETS.join(', ')}`,
60
- );
61
- }
62
- return target;
63
- }
64
-
65
- // ── Default input builder ────────────────────────────────────────────────────
66
-
67
- interface InputSchema {
68
- properties?: Record<string, { type?: string; default?: unknown }>;
69
- required?: string[];
70
- }
71
-
72
- function typeDefault(type: string): unknown {
73
- switch (type) {
74
- case 'string': return '';
75
- case 'number': case 'integer': return 0;
76
- case 'boolean': return false;
77
- case 'object': return {};
78
- case 'array': return [];
79
- default: return '';
80
- }
81
- }
82
-
83
- export function buildDefaultInput(schema: InputSchema): Record<string, unknown> {
84
- if (!schema.properties) return {};
85
- const result: Record<string, unknown> = {};
86
- for (const [key, prop] of Object.entries(schema.properties)) {
87
- if (prop.default !== undefined) {
88
- // Clone reference types to avoid shared mutation
89
- const val = prop.default;
90
- result[key] = (typeof val === 'object' && val !== null) ? JSON.parse(JSON.stringify(val)) : val;
91
- } else {
92
- result[key] = typeDefault(prop.type ?? 'string');
93
- }
94
- }
95
- return result;
96
- }
97
-
98
- // ── Code generators ──────────────────────────────────────────────────────────
99
-
100
- const SAFE_HOST_PATTERN = /^[a-zA-Z0-9._\-]+(:\d{1,5})?$/;
101
-
102
- function validateHost(host: string): void {
103
- if (!SAFE_HOST_PATTERN.test(host)) {
104
- throw new Error(`invalid actionHost: "${host}" — must be a valid hostname with optional port`);
105
- }
106
- }
107
-
108
- function baseUrl(actionHost: string): string {
109
- validateHost(actionHost);
110
- return `${scheme(actionHost)}://${actionHost}/v1/actions/execute`;
111
- }
112
-
113
- function jsonBody(actionId: string, input: Record<string, unknown>, method?: string): string {
114
- return JSON.stringify({ action_id: actionId, ...(method ? { method } : {}), input }, null, 2);
115
- }
116
-
117
- /** Re-indent a multi-line string so continuation lines are aligned */
118
- function indent(text: string, spaces: number): string {
119
- const pad = ' '.repeat(spaces);
120
- const lines = text.split('\n');
121
- return lines.map((line, i) => (i === 0 ? line : pad + line)).join('\n');
122
- }
123
-
124
- /** Escape single quotes for POSIX shell single-quoted strings */
125
- function shellEscape(s: string): string {
126
- return s.replace(/'/g, "'\\''");
127
- }
128
-
129
- function genCurl(params: CodegenParams): string {
130
- const url = baseUrl(params.actionHost);
131
- const body = jsonBody(params.actionId, params.input, params.method);
132
- return [
133
- '# Set XAPI_KEY env var or replace with your key',
134
- `curl -X POST '${shellEscape(url)}' \\`,
135
- ` -H 'Content-Type: application/json' \\`,
136
- ` -H "XAPI-Key: \${XAPI_KEY}" \\`,
137
- ` -d '${shellEscape(body)}'`,
138
- ].join('\n');
139
- }
140
-
141
- function genPython(lib: 'requests' | 'httpx', params: CodegenParams): string {
142
- const url = baseUrl(params.actionHost);
143
- const payload = { action_id: params.actionId, ...(params.method ? { method: params.method } : {}), input: params.input };
144
- return [
145
- `# pip install ${lib}`,
146
- '# Set XAPI_KEY env var or replace with your key',
147
- 'import os',
148
- `import ${lib}`,
149
- '',
150
- `resp = ${lib}.post(`,
151
- ` "${url}",`,
152
- ` headers={`,
153
- ` "Content-Type": "application/json",`,
154
- ` "XAPI-Key": os.environ["XAPI_KEY"],`,
155
- ` },`,
156
- ` json=${indent(pythonDict(payload), 4)},`,
157
- `)`,
158
- 'print(resp.json())',
159
- ].join('\n');
160
- }
161
-
162
- function genJavaScriptFetch(params: CodegenParams): string {
163
- const url = baseUrl(params.actionHost);
164
- const body = jsonBody(params.actionId, params.input, params.method);
165
- return [
166
- '// Set XAPI_KEY env var or replace with your key',
167
- `const resp = await fetch("${url}", {`,
168
- ` method: "POST",`,
169
- ` headers: {`,
170
- ` "Content-Type": "application/json",`,
171
- ` "XAPI-Key": process.env.XAPI_KEY,`,
172
- ` },`,
173
- ` body: JSON.stringify(${indent(body, 2)}),`,
174
- `});`,
175
- 'console.log(await resp.json());',
176
- ].join('\n');
177
- }
178
-
179
- function genJavaScriptAxios(params: CodegenParams): string {
180
- const url = baseUrl(params.actionHost);
181
- const body = jsonBody(params.actionId, params.input, params.method);
182
- return [
183
- '// npm install axios',
184
- '// Set XAPI_KEY env var or replace with your key',
185
- 'import axios from "axios";',
186
- '',
187
- `const resp = await axios.post(`,
188
- ` "${url}",`,
189
- ` ${indent(body, 2)},`,
190
- ` {`,
191
- ` headers: {`,
192
- ` "Content-Type": "application/json",`,
193
- ` "XAPI-Key": process.env.XAPI_KEY,`,
194
- ` },`,
195
- ` },`,
196
- `);`,
197
- 'console.log(resp.data);',
198
- ].join('\n');
199
- }
200
-
201
- function genTypescriptFetch(params: CodegenParams): string {
202
- const url = baseUrl(params.actionHost);
203
- const body = jsonBody(params.actionId, params.input, params.method);
204
- return [
205
- '// Set XAPI_KEY env var or replace with your key',
206
- `const resp: Response = await fetch("${url}", {`,
207
- ` method: "POST",`,
208
- ` headers: {`,
209
- ` "Content-Type": "application/json",`,
210
- ` "XAPI-Key": process.env.XAPI_KEY!,`,
211
- ` },`,
212
- ` body: JSON.stringify(${indent(body, 2)}),`,
213
- `});`,
214
- 'const data: unknown = await resp.json();',
215
- 'console.log(data);',
216
- ].join('\n');
217
- }
218
-
219
- function genGo(params: CodegenParams): string {
220
- const url = baseUrl(params.actionHost);
221
- const body = jsonBody(params.actionId, params.input, params.method);
222
- const escaped = body.replace(/`/g, '` + "`" + `');
223
- return [
224
- '// Set XAPI_KEY env var or replace with your key',
225
- 'package main',
226
- '',
227
- 'import (',
228
- '\t"fmt"',
229
- '\t"io"',
230
- '\t"net/http"',
231
- '\t"os"',
232
- '\t"strings"',
233
- ')',
234
- '',
235
- 'func main() {',
236
- `\tbody := \`${escaped}\``,
237
- `\treq, err := http.NewRequest("POST", "${url}", strings.NewReader(body))`,
238
- '\tif err != nil {',
239
- '\t\tpanic(err)',
240
- '\t}',
241
- '\treq.Header.Set("Content-Type", "application/json")',
242
- '\treq.Header.Set("XAPI-Key", os.Getenv("XAPI_KEY"))',
243
- '',
244
- '\tresp, err := http.DefaultClient.Do(req)',
245
- '\tif err != nil {',
246
- '\t\tpanic(err)',
247
- '\t}',
248
- '\tdefer resp.Body.Close()',
249
- '',
250
- '\tresult, err := io.ReadAll(resp.Body)',
251
- '\tif err != nil {',
252
- '\t\tpanic(err)',
253
- '\t}',
254
- '\tfmt.Println(string(result))',
255
- '}',
256
- ].join('\n');
257
- }
258
-
259
- // ── Python dict formatter ────────────────────────────────────────────────────
260
-
261
- function pythonDict(obj: unknown, depth = 0): string {
262
- const pad = ' '.repeat(depth);
263
- const inner = ' '.repeat(depth + 1);
264
-
265
- if (obj === null || obj === undefined) return 'None';
266
- if (typeof obj === 'boolean') return obj ? 'True' : 'False';
267
- if (typeof obj === 'number') return String(obj);
268
- if (typeof obj === 'string') return JSON.stringify(obj);
269
-
270
- if (Array.isArray(obj)) {
271
- if (obj.length === 0) return '[]';
272
- const items = obj.map(v => `${inner}${pythonDict(v, depth + 1)}`);
273
- return `[\n${items.join(',\n')}\n${pad}]`;
274
- }
275
-
276
- if (typeof obj === 'object') {
277
- const entries = Object.entries(obj as Record<string, unknown>);
278
- if (entries.length === 0) return '{}';
279
- const items = entries.map(
280
- ([k, v]) => `${inner}${JSON.stringify(k)}: ${pythonDict(v, depth + 1)}`,
281
- );
282
- return `{\n${items.join(',\n')}\n${pad}}`;
283
- }
284
-
285
- return String(obj);
286
- }
287
-
288
- // ── Main entry point ─────────────────────────────────────────────────────────
289
-
290
- type Generator = (params: CodegenParams) => string;
291
-
292
- const GENERATORS: Record<string, Record<string, Generator>> = {
293
- curl: { curl: genCurl },
294
- python: { requests: p => genPython('requests', p), httpx: p => genPython('httpx', p) },
295
- javascript: { fetch: genJavaScriptFetch, axios: genJavaScriptAxios },
296
- typescript: { fetch: genTypescriptFetch },
297
- go: { 'net/http': genGo },
298
- };
299
-
300
- export function generateCode(target: string, params: CodegenParams): { lang: string; lib: string; code: string } {
301
- const { lang, lib } = resolveTarget(target);
302
- const generator = GENERATORS[lang]?.[lib];
303
- if (!generator) {
304
- throw new Error(`no generator for ${lang}.${lib}`);
305
- }
306
- return { lang, lib, code: generator(params) };
307
- }