xapi-to 0.1.9 → 0.1.11

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
@@ -25,7 +25,7 @@ xapi register
25
25
  xapi config set apiKey=sk-xxx
26
26
 
27
27
  # 3. Or via env var
28
- export XAPI_API_KEY=sk-xxx
28
+ export XAPI_KEY=sk-xxx
29
29
 
30
30
  # 4. Verify connectivity
31
31
  xapi config health
@@ -76,7 +76,7 @@ xapi oauth providers # list available providers
76
76
 
77
77
  ```bash
78
78
  xapi register # create account, saves apiKey automatically
79
- xapi balance # show xToken balance
79
+ xapi balance # show USD balance
80
80
  xapi topup # generate payment URL
81
81
  xapi topup --method stripe --amount 10 # stripe, $10
82
82
  xapi topup --method x402 # x402 (USDC on Base)
@@ -119,7 +119,7 @@ xapi list --format table # human-readable table
119
119
 
120
120
  | Variable | Description |
121
121
  |---|---|
122
- | `XAPI_API_KEY` | API key (overrides config file) |
122
+ | `XAPI_KEY` | API key (overrides config file) |
123
123
  | `XAPI_ACTION_HOST` | Action service host (default: `action.xapi.to`) |
124
124
  | `XAPI_OUTPUT` | Default output format (`json`\|`pretty`\|`table`) |
125
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xapi-to",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Agent-friendly CLI for xapi - discover and call capabilities and APIs",
5
5
  "type": "module",
6
6
  "bin": {
package/src/codegen.ts ADDED
@@ -0,0 +1,306 @@
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
+ }
17
+
18
+ interface ResolvedTarget {
19
+ lang: string;
20
+ lib: string;
21
+ }
22
+
23
+ // ── Target resolution ────────────────────────────────────────────────────────
24
+
25
+ const TARGET_MAP: Record<string, ResolvedTarget> = {
26
+ 'curl': { lang: 'curl', lib: 'curl' },
27
+ 'python': { lang: 'python', lib: 'requests' },
28
+ 'py': { lang: 'python', lib: 'requests' },
29
+ 'python.requests': { lang: 'python', lib: 'requests' },
30
+ 'python.httpx': { lang: 'python', lib: 'httpx' },
31
+ 'py.requests': { lang: 'python', lib: 'requests' },
32
+ 'py.httpx': { lang: 'python', lib: 'httpx' },
33
+ 'javascript': { lang: 'javascript', lib: 'fetch' },
34
+ 'js': { lang: 'javascript', lib: 'fetch' },
35
+ 'javascript.fetch': { lang: 'javascript', lib: 'fetch' },
36
+ 'javascript.axios': { lang: 'javascript', lib: 'axios' },
37
+ 'js.fetch': { lang: 'javascript', lib: 'fetch' },
38
+ 'js.axios': { lang: 'javascript', lib: 'axios' },
39
+ 'typescript': { lang: 'typescript', lib: 'fetch' },
40
+ 'ts': { lang: 'typescript', lib: 'fetch' },
41
+ 'typescript.fetch': { lang: 'typescript', lib: 'fetch' },
42
+ 'ts.fetch': { lang: 'typescript', lib: 'fetch' },
43
+ 'go': { lang: 'go', lib: 'net/http' },
44
+ };
45
+
46
+ const SUPPORTED_TARGETS = [
47
+ 'curl',
48
+ 'python (py) [.requests, .httpx]',
49
+ 'javascript (js) [.fetch, .axios]',
50
+ 'typescript (ts) [.fetch]',
51
+ 'go [net/http]',
52
+ ];
53
+
54
+ export function resolveTarget(raw: string): ResolvedTarget {
55
+ const target = TARGET_MAP[raw.toLowerCase()];
56
+ if (!target) {
57
+ throw new Error(
58
+ `unknown --code target: "${raw}". Supported: ${SUPPORTED_TARGETS.join(', ')}`,
59
+ );
60
+ }
61
+ return target;
62
+ }
63
+
64
+ // ── Default input builder ────────────────────────────────────────────────────
65
+
66
+ interface InputSchema {
67
+ properties?: Record<string, { type?: string; default?: unknown }>;
68
+ required?: string[];
69
+ }
70
+
71
+ function typeDefault(type: string): unknown {
72
+ switch (type) {
73
+ case 'string': return '';
74
+ case 'number': case 'integer': return 0;
75
+ case 'boolean': return false;
76
+ case 'object': return {};
77
+ case 'array': return [];
78
+ default: return '';
79
+ }
80
+ }
81
+
82
+ export function buildDefaultInput(schema: InputSchema): Record<string, unknown> {
83
+ if (!schema.properties) return {};
84
+ const result: Record<string, unknown> = {};
85
+ for (const [key, prop] of Object.entries(schema.properties)) {
86
+ if (prop.default !== undefined) {
87
+ // Clone reference types to avoid shared mutation
88
+ const val = prop.default;
89
+ result[key] = (typeof val === 'object' && val !== null) ? JSON.parse(JSON.stringify(val)) : val;
90
+ } else {
91
+ result[key] = typeDefault(prop.type ?? 'string');
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
97
+ // ── Code generators ──────────────────────────────────────────────────────────
98
+
99
+ const SAFE_HOST_PATTERN = /^[a-zA-Z0-9._\-]+(:\d{1,5})?$/;
100
+
101
+ function validateHost(host: string): void {
102
+ if (!SAFE_HOST_PATTERN.test(host)) {
103
+ throw new Error(`invalid actionHost: "${host}" — must be a valid hostname with optional port`);
104
+ }
105
+ }
106
+
107
+ function baseUrl(actionHost: string): string {
108
+ validateHost(actionHost);
109
+ return `${scheme(actionHost)}://${actionHost}/v1/actions/execute`;
110
+ }
111
+
112
+ function jsonBody(actionId: string, input: Record<string, unknown>): string {
113
+ return JSON.stringify({ action_id: actionId, input }, null, 2);
114
+ }
115
+
116
+ /** Re-indent a multi-line string so continuation lines are aligned */
117
+ function indent(text: string, spaces: number): string {
118
+ const pad = ' '.repeat(spaces);
119
+ const lines = text.split('\n');
120
+ return lines.map((line, i) => (i === 0 ? line : pad + line)).join('\n');
121
+ }
122
+
123
+ /** Escape single quotes for POSIX shell single-quoted strings */
124
+ function shellEscape(s: string): string {
125
+ return s.replace(/'/g, "'\\''");
126
+ }
127
+
128
+ function genCurl(params: CodegenParams): string {
129
+ const url = baseUrl(params.actionHost);
130
+ const body = jsonBody(params.actionId, params.input);
131
+ return [
132
+ '# Set XAPI_KEY env var or replace with your key',
133
+ `curl -X POST '${shellEscape(url)}' \\`,
134
+ ` -H 'Content-Type: application/json' \\`,
135
+ ` -H "XAPI-Key: \${XAPI_KEY}" \\`,
136
+ ` -d '${shellEscape(body)}'`,
137
+ ].join('\n');
138
+ }
139
+
140
+ function genPython(lib: 'requests' | 'httpx', params: CodegenParams): string {
141
+ const url = baseUrl(params.actionHost);
142
+ const payload = { action_id: params.actionId, input: params.input };
143
+ return [
144
+ `# pip install ${lib}`,
145
+ '# Set XAPI_KEY env var or replace with your key',
146
+ 'import os',
147
+ `import ${lib}`,
148
+ '',
149
+ `resp = ${lib}.post(`,
150
+ ` "${url}",`,
151
+ ` headers={`,
152
+ ` "Content-Type": "application/json",`,
153
+ ` "XAPI-Key": os.environ["XAPI_KEY"],`,
154
+ ` },`,
155
+ ` json=${indent(pythonDict(payload), 4)},`,
156
+ `)`,
157
+ 'print(resp.json())',
158
+ ].join('\n');
159
+ }
160
+
161
+ function genJavaScriptFetch(params: CodegenParams): string {
162
+ const url = baseUrl(params.actionHost);
163
+ const body = jsonBody(params.actionId, params.input);
164
+ return [
165
+ '// Set XAPI_KEY env var or replace with your key',
166
+ `const resp = await fetch("${url}", {`,
167
+ ` method: "POST",`,
168
+ ` headers: {`,
169
+ ` "Content-Type": "application/json",`,
170
+ ` "XAPI-Key": process.env.XAPI_KEY,`,
171
+ ` },`,
172
+ ` body: JSON.stringify(${indent(body, 2)}),`,
173
+ `});`,
174
+ 'console.log(await resp.json());',
175
+ ].join('\n');
176
+ }
177
+
178
+ function genJavaScriptAxios(params: CodegenParams): string {
179
+ const url = baseUrl(params.actionHost);
180
+ const body = jsonBody(params.actionId, params.input);
181
+ return [
182
+ '// npm install axios',
183
+ '// Set XAPI_KEY env var or replace with your key',
184
+ 'import axios from "axios";',
185
+ '',
186
+ `const resp = await axios.post(`,
187
+ ` "${url}",`,
188
+ ` ${indent(body, 2)},`,
189
+ ` {`,
190
+ ` headers: {`,
191
+ ` "Content-Type": "application/json",`,
192
+ ` "XAPI-Key": process.env.XAPI_KEY,`,
193
+ ` },`,
194
+ ` },`,
195
+ `);`,
196
+ 'console.log(resp.data);',
197
+ ].join('\n');
198
+ }
199
+
200
+ function genTypescriptFetch(params: CodegenParams): string {
201
+ const url = baseUrl(params.actionHost);
202
+ const body = jsonBody(params.actionId, params.input);
203
+ return [
204
+ '// Set XAPI_KEY env var or replace with your key',
205
+ `const resp: Response = await fetch("${url}", {`,
206
+ ` method: "POST",`,
207
+ ` headers: {`,
208
+ ` "Content-Type": "application/json",`,
209
+ ` "XAPI-Key": process.env.XAPI_KEY!,`,
210
+ ` },`,
211
+ ` body: JSON.stringify(${indent(body, 2)}),`,
212
+ `});`,
213
+ 'const data: unknown = await resp.json();',
214
+ 'console.log(data);',
215
+ ].join('\n');
216
+ }
217
+
218
+ function genGo(params: CodegenParams): string {
219
+ const url = baseUrl(params.actionHost);
220
+ const body = jsonBody(params.actionId, params.input);
221
+ const escaped = body.replace(/`/g, '` + "`" + `');
222
+ return [
223
+ '// Set XAPI_KEY env var or replace with your key',
224
+ 'package main',
225
+ '',
226
+ 'import (',
227
+ '\t"fmt"',
228
+ '\t"io"',
229
+ '\t"net/http"',
230
+ '\t"os"',
231
+ '\t"strings"',
232
+ ')',
233
+ '',
234
+ 'func main() {',
235
+ `\tbody := \`${escaped}\``,
236
+ `\treq, err := http.NewRequest("POST", "${url}", strings.NewReader(body))`,
237
+ '\tif err != nil {',
238
+ '\t\tpanic(err)',
239
+ '\t}',
240
+ '\treq.Header.Set("Content-Type", "application/json")',
241
+ '\treq.Header.Set("XAPI-Key", os.Getenv("XAPI_KEY"))',
242
+ '',
243
+ '\tresp, err := http.DefaultClient.Do(req)',
244
+ '\tif err != nil {',
245
+ '\t\tpanic(err)',
246
+ '\t}',
247
+ '\tdefer resp.Body.Close()',
248
+ '',
249
+ '\tresult, err := io.ReadAll(resp.Body)',
250
+ '\tif err != nil {',
251
+ '\t\tpanic(err)',
252
+ '\t}',
253
+ '\tfmt.Println(string(result))',
254
+ '}',
255
+ ].join('\n');
256
+ }
257
+
258
+ // ── Python dict formatter ────────────────────────────────────────────────────
259
+
260
+ function pythonDict(obj: unknown, depth = 0): string {
261
+ const pad = ' '.repeat(depth);
262
+ const inner = ' '.repeat(depth + 1);
263
+
264
+ if (obj === null || obj === undefined) return 'None';
265
+ if (typeof obj === 'boolean') return obj ? 'True' : 'False';
266
+ if (typeof obj === 'number') return String(obj);
267
+ if (typeof obj === 'string') return JSON.stringify(obj);
268
+
269
+ if (Array.isArray(obj)) {
270
+ if (obj.length === 0) return '[]';
271
+ const items = obj.map(v => `${inner}${pythonDict(v, depth + 1)}`);
272
+ return `[\n${items.join(',\n')}\n${pad}]`;
273
+ }
274
+
275
+ if (typeof obj === 'object') {
276
+ const entries = Object.entries(obj as Record<string, unknown>);
277
+ if (entries.length === 0) return '{}';
278
+ const items = entries.map(
279
+ ([k, v]) => `${inner}${JSON.stringify(k)}: ${pythonDict(v, depth + 1)}`,
280
+ );
281
+ return `{\n${items.join(',\n')}\n${pad}}`;
282
+ }
283
+
284
+ return String(obj);
285
+ }
286
+
287
+ // ── Main entry point ─────────────────────────────────────────────────────────
288
+
289
+ type Generator = (params: CodegenParams) => string;
290
+
291
+ const GENERATORS: Record<string, Record<string, Generator>> = {
292
+ curl: { curl: genCurl },
293
+ python: { requests: p => genPython('requests', p), httpx: p => genPython('httpx', p) },
294
+ javascript: { fetch: genJavaScriptFetch, axios: genJavaScriptAxios },
295
+ typescript: { fetch: genTypescriptFetch },
296
+ go: { 'net/http': genGo },
297
+ };
298
+
299
+ export function generateCode(target: string, params: CodegenParams): { lang: string; lib: string; code: string } {
300
+ const { lang, lib } = resolveTarget(target);
301
+ const generator = GENERATORS[lang]?.[lib];
302
+ if (!generator) {
303
+ throw new Error(`no generator for ${lang}.${lib}`);
304
+ }
305
+ return { lang, lib, code: generator(params) };
306
+ }
@@ -6,10 +6,144 @@
6
6
 
7
7
  import { getConfig, requireApiKey } from '../config.ts';
8
8
  import * as client from '../client.ts';
9
- import { output, err } from '../format.ts';
9
+ import { output, err, getFormat } from '../format.ts';
10
+ import { generateCode, buildDefaultInput, resolveTarget } from '../codegen.ts';
10
11
 
11
12
  const VALID_SOURCES = ['capability', 'api'];
12
13
 
14
+ // ── Subcommand help texts ────────────────────────────────────────────────────
15
+
16
+ const LIST_HELP = `xapi list - List all actions
17
+
18
+ USAGE
19
+ xapi list [flags]
20
+
21
+ FLAGS
22
+ --source capability|api Filter by source type
23
+ --category <name> Filter by category
24
+ --service-id <id> Filter by service
25
+ --page N Page number (default: 1)
26
+ --page-size N Results per page
27
+ --format json|pretty|table Output format
28
+
29
+ EXAMPLES
30
+ xapi list
31
+ xapi list --source api --format table
32
+ xapi list --category social --page 2
33
+ `;
34
+
35
+ const SEARCH_HELP = `xapi search - Search actions by keyword
36
+
37
+ USAGE
38
+ xapi search <query> [flags]
39
+
40
+ FLAGS
41
+ --source capability|api Filter by source type
42
+ --category <name> Filter by category
43
+ --page N Page number (default: 1)
44
+ --page-size N Results per page
45
+ --format json|pretty|table Output format
46
+
47
+ EXAMPLES
48
+ xapi search twitter
49
+ xapi search "tweet detail" --source api
50
+ xapi search weather --category utility --format table
51
+ `;
52
+
53
+ const GET_HELP = `xapi get - Get action schema
54
+
55
+ USAGE
56
+ xapi get <id> [flags]
57
+
58
+ FLAGS
59
+ --method GET|POST|... Filter by HTTP method
60
+ --code <target> Generate code snippet instead of showing schema
61
+ --format json|pretty|table Output format
62
+
63
+ CODE TARGETS
64
+ curl cURL command
65
+ py, python Python (requests)
66
+ python.requests Python with requests
67
+ py.requests alias for python.requests
68
+ python.httpx Python with httpx
69
+ py.httpx alias for python.httpx
70
+ js, javascript JavaScript (fetch)
71
+ javascript.fetch JavaScript with fetch
72
+ js.fetch alias for javascript.fetch
73
+ javascript.axios JavaScript with axios
74
+ js.axios alias for javascript.axios
75
+ ts, typescript TypeScript (fetch)
76
+ typescript.fetch TypeScript with fetch
77
+ ts.fetch alias for typescript.fetch
78
+ go Go (net/http)
79
+
80
+ EXAMPLES
81
+ xapi get twitter.tweet_detail
82
+ xapi get twitter.tweet_detail --method POST
83
+ xapi get twitter.tweet_detail --code curl
84
+ xapi get twitter.tweet_detail --code python.httpx --format pretty
85
+ `;
86
+
87
+ const CALL_HELP = `xapi call - Execute an action
88
+
89
+ USAGE
90
+ xapi call <id> --input '{"key":"val"}' [flags]
91
+
92
+ FLAGS
93
+ --input <json> Input payload as JSON (required for execution)
94
+ --method GET|POST|... Override HTTP method
95
+ --code <target> Generate code snippet instead of executing
96
+ --format json|pretty|table Output format
97
+
98
+ CODE TARGETS
99
+ curl cURL command
100
+ py, python Python (requests)
101
+ python.requests Python with requests
102
+ py.requests alias for python.requests
103
+ python.httpx Python with httpx
104
+ py.httpx alias for python.httpx
105
+ js, javascript JavaScript (fetch)
106
+ javascript.fetch JavaScript with fetch
107
+ js.fetch alias for javascript.fetch
108
+ javascript.axios JavaScript with axios
109
+ js.axios alias for javascript.axios
110
+ ts, typescript TypeScript (fetch)
111
+ typescript.fetch TypeScript with fetch
112
+ ts.fetch alias for typescript.fetch
113
+ go Go (net/http)
114
+
115
+ EXAMPLES
116
+ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
117
+ xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
118
+ xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
119
+ `;
120
+
121
+ /** Print subcommand help and exit if --help flag is set */
122
+ function showHelpIfRequested(flags: Record<string, string>, helpText: string): void {
123
+ if (flags.help) {
124
+ console.log(helpText);
125
+ process.exit(0);
126
+ }
127
+ }
128
+
129
+ /** Validate --code flag: check for bare flag and unknown target (fail fast before I/O) */
130
+ function validateCodeFlag(flags: Record<string, string>): void {
131
+ if (flags.code === 'true') {
132
+ err('--code requires a target language, e.g. --code curl, --code py, --code js');
133
+ }
134
+ resolveTarget(flags.code);
135
+ }
136
+
137
+ /** Output code snippet respecting --format */
138
+ function outputCode(result: { lang: string; lib: string; code: string }, flags: Record<string, string>) {
139
+ const fmt = flags.format || getFormat();
140
+ if (fmt === 'json') {
141
+ output({ language: result.lang, library: result.lib, code: result.code }, 'json');
142
+ } else {
143
+ console.log(result.code);
144
+ }
145
+ }
146
+
13
147
  /** Validate and return source filter from --source flag */
14
148
  function getSource(flags: Record<string, string>): string | undefined {
15
149
  if (!flags.source) return undefined;
@@ -20,6 +154,7 @@ function getSource(flags: Record<string, string>): string | undefined {
20
154
  }
21
155
 
22
156
  export async function actionList(args: string[], flags: Record<string, string>) {
157
+ showHelpIfRequested(flags, LIST_HELP);
23
158
  const cfg = getConfig();
24
159
  try {
25
160
  const res = await client.actionList(cfg, {
@@ -49,6 +184,7 @@ export async function actionList(args: string[], flags: Record<string, string>)
49
184
  }
50
185
 
51
186
  export async function actionSearch(args: string[], flags: Record<string, string>) {
187
+ showHelpIfRequested(flags, SEARCH_HELP);
52
188
  const query = args[0];
53
189
  if (!query) err('usage: xapi search <query>');
54
190
  const cfg = getConfig();
@@ -119,8 +255,10 @@ export async function actionServices(args: string[], flags: Record<string, strin
119
255
  }
120
256
 
121
257
  export async function actionGet(args: string[], flags: Record<string, string>) {
258
+ showHelpIfRequested(flags, GET_HELP);
122
259
  const id = args[0];
123
260
  if (!id) err('usage: xapi get <id> [--method GET|POST|DELETE|...]');
261
+ if (flags.code) validateCodeFlag(flags);
124
262
  const cfg = getConfig();
125
263
  try {
126
264
  const res = await client.actionGet(id, cfg);
@@ -132,6 +270,20 @@ export async function actionGet(args: string[], flags: Record<string, string>) {
132
270
  if (filtered.length === 0) {
133
271
  err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
134
272
  }
273
+
274
+ if (flags.code) {
275
+ if (filtered.length > 1) {
276
+ process.stderr.write(
277
+ `Warning: action "${id}" has ${filtered.length} endpoints; using method "${(filtered[0] as any).method}". Use --method to select a specific one.\n`,
278
+ );
279
+ }
280
+ const action = filtered[0] as any;
281
+ const input = buildDefaultInput(action.input ?? {});
282
+ const result = generateCode(flags.code, { actionId: id, input, actionHost: cfg.actionHost });
283
+ outputCode(result, flags);
284
+ return;
285
+ }
286
+
135
287
  output(filtered.length === 1 ? filtered[0] : filtered, flags.format as any);
136
288
  } catch (e: any) {
137
289
  err('get failed', e.message);
@@ -139,10 +291,11 @@ export async function actionGet(args: string[], flags: Record<string, string>) {
139
291
  }
140
292
 
141
293
  export async function actionCall(args: string[], flags: Record<string, string>) {
294
+ showHelpIfRequested(flags, CALL_HELP);
142
295
  const id = args[0];
143
296
  if (!id) err('usage: xapi call <id> --input \'{"key":"val"}\'');
297
+ if (flags.code) validateCodeFlag(flags);
144
298
  const cfg = getConfig();
145
- requireApiKey(cfg);
146
299
  let input: Record<string, unknown> = {};
147
300
  if (flags.input) {
148
301
  try {
@@ -154,6 +307,14 @@ export async function actionCall(args: string[], flags: Record<string, string>)
154
307
  if (flags.method) {
155
308
  input = { ...input, method: flags.method.toUpperCase() };
156
309
  }
310
+
311
+ if (flags.code) {
312
+ const result = generateCode(flags.code, { actionId: id, input, actionHost: cfg.actionHost });
313
+ outputCode(result, flags);
314
+ return;
315
+ }
316
+
317
+ requireApiKey(cfg);
157
318
  try {
158
319
  const res = await client.actionCall(id, input, cfg);
159
320
  output(res, flags.format as any);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * balance command
3
- * Fetches xTokenBalance from GET /auth/me
3
+ * Fetches balance from GET /auth/me
4
4
  */
5
5
 
6
6
  import { getConfig, requireApiKey, XAPI_API_HOST, scheme } from '../config.ts';
@@ -20,12 +20,12 @@ export async function balance(args: string[], flags: Record<string, string>) {
20
20
  }
21
21
 
22
22
  try {
23
- const me = await request<{ xTokenBalance: string; accountType: string; tier: string }>(
23
+ const me = await request<{ balance: string; accountType: string; tier: string }>(
24
24
  `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/me`,
25
25
  { method: 'GET', headers: { Authorization: `Bearer ${token!}` } },
26
26
  );
27
27
  output({
28
- balance: me.xTokenBalance,
28
+ balance: me.balance,
29
29
  accountType: me.accountType,
30
30
  tier: me.tier,
31
31
  }, flags.format as any);
@@ -109,6 +109,32 @@ async function findCurrentKeyRecord(
109
109
  return match;
110
110
  }
111
111
 
112
+ // ── Help text ──────────────────────────────────────────────────────────────────
113
+
114
+ export const OAUTH_HELP = `xapi oauth - Manage OAuth bindings
115
+
116
+ USAGE
117
+ xapi oauth <command> [flags]
118
+
119
+ COMMANDS
120
+ bind [--provider <name>] Bind an OAuth account to your API key
121
+ status List current OAuth bindings
122
+ unbind <binding-id> Remove an OAuth binding
123
+ providers List available OAuth providers
124
+
125
+ FLAGS
126
+ --provider <name> OAuth provider (default: twitter)
127
+ --format json|pretty|table Output format
128
+
129
+ EXAMPLES
130
+ xapi oauth bind
131
+ xapi oauth bind --provider twitter
132
+ xapi oauth status
133
+ xapi oauth status --format pretty
134
+ xapi oauth unbind abc123
135
+ xapi oauth providers
136
+ `;
137
+
112
138
  // ── Commands ───────────────────────────────────────────────────────────────────
113
139
 
114
140
  /**
package/src/config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Config management
3
3
  * Only apiKey is user-configurable. Host is built-in.
4
- * Reads from env var XAPI_API_KEY or ~/.xapi/config.json
4
+ * Reads from env var XAPI_KEY or ~/.xapi/config.json
5
5
  */
6
6
 
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
@@ -38,7 +38,7 @@ export function getConfig(): XapiConfig {
38
38
  const file = loadFileConfig();
39
39
  return {
40
40
  actionHost: XAPI_ACTION_HOST,
41
- apiKey: process.env.XAPI_API_KEY || file.apiKey,
41
+ apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey,
42
42
  };
43
43
  }
44
44
 
@@ -62,7 +62,7 @@ export function showConfig(): void {
62
62
  actionHost: cfg.actionHost,
63
63
  apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined,
64
64
  source: {
65
- apiKey: process.env.XAPI_API_KEY ? 'env' : file.apiKey ? 'file' : 'none',
65
+ apiKey: (process.env.XAPI_KEY || process.env.XAPI_API_KEY) ? 'env' : file.apiKey ? 'file' : 'none',
66
66
  },
67
67
  configFile: CONFIG_FILE,
68
68
  }, null, 2));
package/src/index.ts CHANGED
@@ -7,19 +7,19 @@
7
7
  * xapi search <query> [--source capability|api] [--category X] [--page N] [--page-size N]
8
8
  * xapi categories [--source capability|api]
9
9
  * xapi services [--page N] [--page-size N] [--category X]
10
- * xapi get <id>
11
- * xapi call <id> --input '{"k":"v"}'
10
+ * xapi get <id> [--code curl|py|js|ts|go]
11
+ * xapi call <id> --input '{"k":"v"}' [--code curl|py|js|ts|go]
12
12
  *
13
13
  * xapi config show
14
14
  * xapi config set apiKey=<key>
15
- * xapi config health
15
+ * xapi health
16
16
  *
17
17
  * Global flags:
18
18
  * --format json|pretty|table output format (default: json)
19
19
  * --help show help
20
20
  *
21
21
  * Env vars:
22
- * XAPI_API_KEY API key
22
+ * XAPI_KEY API key
23
23
  * XAPI_ACTION_HOST Action service host (default: action.xapi.to)
24
24
  * XAPI_OUTPUT default output format (json|pretty|table)
25
25
  */
@@ -30,6 +30,7 @@ import * as regCmds from './commands/register.ts';
30
30
  import * as topupCmds from './commands/topup.ts';
31
31
  import * as balanceCmds from './commands/balance.ts';
32
32
  import * as oauthCmds from './commands/oauth.ts';
33
+ const { OAUTH_HELP } = oauthCmds;
33
34
 
34
35
  // ── Argument parser ───────────────────────────────────────────────────────────
35
36
 
@@ -85,7 +86,10 @@ COMMANDS
85
86
  --page N --page-size N Pagination
86
87
  --category <name> Filter by category
87
88
  get <id> [--method GET|POST|...] Get action schema (filter by HTTP method)
89
+ --code <target> Generate code snippet (curl, py, js, ts, go)
88
90
  call <id> --input '{"key":"val"}' Execute an action
91
+ --code <target> Generate code snippet instead of executing
92
+ Variants: python.requests, python.httpx, javascript.fetch, javascript.axios
89
93
 
90
94
  oauth bind [--provider twitter] Bind Twitter OAuth to your API key
91
95
  oauth status List current OAuth bindings
@@ -96,16 +100,17 @@ COMMANDS
96
100
  balance Show current account balance
97
101
  topup [--amount <usd>] [--method stripe|x402] Generate payment URL
98
102
 
103
+ health Check backend connectivity
104
+
99
105
  config show Show current config
100
106
  config set apiKey=<key> Save API key to ~/.xapi/config.json
101
- config health Check backend connectivity
102
107
 
103
108
  GLOBAL FLAGS
104
109
  --format json|pretty|table Output format (default: json)
105
- --help Show this help
110
+ --help Show help (use with a command for details, e.g. xapi get --help)
106
111
 
107
112
  ENV VARS
108
- XAPI_API_KEY API key (header: XAPI-Key)
113
+ XAPI_KEY API key (header: XAPI-Key)
109
114
  XAPI_ACTION_HOST Action service host (default: action.xapi.to)
110
115
  XAPI_OUTPUT Default output format
111
116
 
@@ -115,11 +120,14 @@ EXAMPLES
115
120
  xapi list --source capability
116
121
  xapi search twitter --source api
117
122
  xapi get twitter.tweet_detail
123
+ xapi get twitter.tweet_detail --code curl
124
+ xapi get twitter.tweet_detail --code py --format pretty
118
125
  xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
126
+ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' --code python
119
127
  xapi categories
120
128
  xapi services --format table
121
129
  xapi config set apiKey=xapi_abc123
122
- xapi config health
130
+ xapi health
123
131
  `;
124
132
 
125
133
  // ── Router ────────────────────────────────────────────────────────────────────
@@ -127,7 +135,7 @@ EXAMPLES
127
135
  async function main() {
128
136
  const { positional, flags } = parseArgs(process.argv.slice(2));
129
137
 
130
- if (flags.help || positional.length === 0) {
138
+ if (positional.length === 0) {
131
139
  console.log(HELP);
132
140
  process.exit(0);
133
141
  }
@@ -148,6 +156,10 @@ async function main() {
148
156
 
149
157
  // ── OAuth commands ──
150
158
  case 'oauth': {
159
+ if (flags.help || rest.length === 0) {
160
+ console.log(OAUTH_HELP);
161
+ process.exit(0);
162
+ }
151
163
  const [subCmd, ...subRest] = rest;
152
164
  switch (subCmd) {
153
165
  case 'bind': return oauthCmds.oauthBind(subRest, flags);
@@ -165,6 +177,7 @@ async function main() {
165
177
  case 'register': return regCmds.register(rest, flags);
166
178
  case 'balance': return balanceCmds.balance(rest, flags);
167
179
  case 'topup': return topupCmds.topup(rest, flags);
180
+ case 'health': return cfgCmds.configHealth(rest, flags);
168
181
 
169
182
  // ── Config commands ──
170
183
  case 'config': {
@@ -172,7 +185,6 @@ async function main() {
172
185
  switch (subCmd) {
173
186
  case 'show': return cfgCmds.configShow(subRest, flags);
174
187
  case 'set': return cfgCmds.configSet(subRest, flags);
175
- case 'health': return cfgCmds.configHealth(subRest, flags);
176
188
  default:
177
189
  console.error(JSON.stringify({ error: `unknown config command: ${subCmd}` }));
178
190
  process.exit(1);