xapi-to 0.1.7

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 ADDED
@@ -0,0 +1,160 @@
1
+ # xapi-cli
2
+
3
+ Agent-friendly command-line interface for [xAPI](https://xapi.to) — discover and call capabilities and APIs from your terminal or AI agent.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Via npx (no install needed)
9
+ npx @xapi-to/xapi --help
10
+
11
+ # Or install globally with bun
12
+ bun add -g @xapi-to/xapi
13
+
14
+ # Or from source
15
+ cd xapi-cli && bun install
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ # 1. Register a new account (apiKey saved automatically)
22
+ xapi register
23
+
24
+ # 2. Or set an existing key
25
+ xapi config set apiKey=sk-xxx
26
+
27
+ # 3. Or via env var
28
+ export XAPI_API_KEY=sk-xxx
29
+
30
+ # 4. Verify connectivity
31
+ xapi config health
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```
37
+ xapi <command> [args] [flags]
38
+ ```
39
+
40
+ ### Action Commands
41
+
42
+ Unified interface for capabilities (built-in) and APIs (third-party). Use `--source capability|api` to filter.
43
+
44
+ ```bash
45
+ xapi list # list all actions
46
+ xapi list --source capability # only built-in capabilities
47
+ xapi list --source api --category DeFi # filter by source and category
48
+ xapi list --page 2 --page-size 20 # pagination
49
+ xapi list --service-id <id> # filter by service
50
+
51
+ xapi search "twitter" # search by keyword
52
+ xapi search "token price" --source api # search APIs only
53
+
54
+ xapi categories # list all categories
55
+ xapi categories --source capability # categories for capabilities only
56
+
57
+ xapi services # list all services
58
+ xapi services --category Social --page-size 10 # filter and paginate
59
+
60
+ xapi get twitter.tweet_detail # get action schema
61
+ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}' # execute
62
+ ```
63
+
64
+ ### OAuth
65
+
66
+ Bind third-party OAuth accounts (e.g. Twitter) to your API key.
67
+
68
+ ```bash
69
+ xapi oauth bind --provider twitter # bind Twitter account
70
+ xapi oauth status # list current bindings
71
+ xapi oauth unbind <binding-id> # remove a binding
72
+ xapi oauth providers # list available providers
73
+ ```
74
+
75
+ ### Account
76
+
77
+ ```bash
78
+ xapi register # create account, saves apiKey automatically
79
+ xapi balance # show xToken balance
80
+ xapi topup # generate payment URL
81
+ xapi topup --method stripe --amount 10 # stripe, $10
82
+ xapi topup --method x402 # x402 (USDC on Base)
83
+ ```
84
+
85
+ ### Config
86
+
87
+ ```bash
88
+ xapi config show # show current config
89
+ xapi config set apiKey=sk-xxx # save API key
90
+ xapi config health # check backend connectivity
91
+ ```
92
+
93
+ ## Workflow: Always GET before CALL
94
+
95
+ Before calling any action, always read its schema first to understand required parameters:
96
+
97
+ ```bash
98
+ # 1. Find the action
99
+ xapi search "twitter"
100
+
101
+ # 2. Read its schema
102
+ xapi get twitter.tweet_detail
103
+
104
+ # 3. Call with correct parameters
105
+ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
106
+ ```
107
+
108
+ ## Output Formats
109
+
110
+ All output is JSON by default — designed for agent consumption.
111
+
112
+ ```bash
113
+ xapi list --format json # default, machine-readable
114
+ xapi list --format pretty # pretty-printed JSON
115
+ xapi list --format table # human-readable table
116
+ ```
117
+
118
+ ## Environment Variables
119
+
120
+ | Variable | Description |
121
+ |---|---|
122
+ | `XAPI_API_KEY` | API key (overrides config file) |
123
+ | `XAPI_ACTION_HOST` | Action service host (default: `action.xapi.to`) |
124
+ | `XAPI_OUTPUT` | Default output format (`json`\|`pretty`\|`table`) |
125
+
126
+ Config is stored at `~/.xapi/config.json`.
127
+
128
+ ## Built-in Capabilities
129
+
130
+ | ID | Description |
131
+ |---|---|
132
+ | `twitter.tweet_detail` | Get tweet details and replies |
133
+ | `twitter.user_by_screen_name` | Get user profile by username |
134
+ | `twitter.user_by_screen_names` | Batch get user profiles by usernames |
135
+ | `twitter.user_tweets` | Get tweets from a user |
136
+ | `twitter.user_media` | Get media posts from a user |
137
+ | `twitter.following` | Get user following list |
138
+ | `twitter.followers` | Get user followers |
139
+ | `twitter.retweeters` | Get tweet retweeters |
140
+ | `twitter.search_timeline` | Search tweets, users, photos, videos |
141
+ | `ai.text.chat.fast` | Fast AI chat completion |
142
+ | `ai.text.chat.reasoning` | Advanced reasoning chat |
143
+ | `ai.text.summarize` | Summarize long text |
144
+ | `ai.text.rewrite` | Rewrite text with different styles |
145
+ | `ai.embedding.generate` | Generate vector embeddings |
146
+ | `web.search` | Web search |
147
+ | `web.search.realtime` | Realtime web search with time filters |
148
+ | `news.search.latest` | Latest news search |
149
+ | `crypto.token.price` | Crypto token price and changes |
150
+ | `crypto.token.metadata` | Crypto token metadata |
151
+
152
+ ## Security
153
+
154
+ - **NEVER send your API key to any domain other than `*.xapi.to`**
155
+ - The key is stored at `~/.xapi/config.json` — do not expose this file
156
+ - `topup` outputs a payment URL containing the API key — do not share publicly
157
+
158
+ ## License
159
+
160
+ MIT
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "xapi-to",
3
+ "version": "0.1.7",
4
+ "description": "Agent-friendly CLI for xapi - discover and call capabilities and APIs",
5
+ "type": "module",
6
+ "bin": {
7
+ "xapi": "src/index.ts"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "!src/tests",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "start": "bun run src/index.ts",
16
+ "dev": "XAPI_ACTION_HOST=localhost:3003 bun run src/index.ts",
17
+ "test": "bun test src/tests"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest"
21
+ },
22
+ "peerDependencies": {
23
+ "typescript": "^5"
24
+ }
25
+ }
package/src/client.ts ADDED
@@ -0,0 +1,264 @@
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/xapi 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
+ ) {
128
+ return request<unknown>(
129
+ `${baseUrl(opts)}/v1/actions/execute`,
130
+ {
131
+ method: 'POST',
132
+ headers: headers(opts.apiKey),
133
+ body: JSON.stringify({ action_id: actionId, input }),
134
+ },
135
+ EXECUTE_TIMEOUT_MS,
136
+ );
137
+ }
138
+
139
+ export async function actionServices(
140
+ opts: ClientOptions,
141
+ params: { page?: number; page_size?: number; category?: string } = {},
142
+ ) {
143
+ const url = new URL(`${baseUrl(opts)}/v1/actions/services`);
144
+ if (params.page) url.searchParams.set('page', String(params.page));
145
+ if (params.page_size) url.searchParams.set('page_size', String(params.page_size));
146
+ if (params.category) url.searchParams.set('category', params.category);
147
+ return request<{ services: unknown[]; pagination: unknown }>(
148
+ url.toString(),
149
+ { method: 'GET', headers: headers(opts.apiKey) },
150
+ );
151
+ }
152
+
153
+ export async function healthCheck(opts: ClientOptions) {
154
+ return request<unknown>(
155
+ `${baseUrl(opts)}/health`,
156
+ { method: 'GET', headers: headers(opts.apiKey) },
157
+ 5_000,
158
+ );
159
+ }
160
+
161
+ // ── Auth ──────────────────────────────────────────────────────────────────────
162
+
163
+ export async function loginWithApiKey(apiKey: string, apiHost: string) {
164
+ return request<{ accessToken: string; user: unknown }>(
165
+ `${scheme(apiHost)}://${apiHost}/api/auth/login/apikey`,
166
+ {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({ apiKey }),
170
+ },
171
+ );
172
+ }
173
+
174
+ // ── OAuth ──────────────────────────────────────────────────────────────────────
175
+
176
+ function jwtHeaders(jwtToken: string): Record<string, string> {
177
+ return { 'Content-Type': 'application/json', Authorization: `Bearer ${jwtToken}` };
178
+ }
179
+
180
+ export async function listKeys(jwtToken: string, apiHost: string) {
181
+ return request<Array<{
182
+ id: string;
183
+ name: string;
184
+ keyPreview: string;
185
+ oauthEnabled: boolean;
186
+ createdAt: string;
187
+ }>>(
188
+ `${scheme(apiHost)}://${apiHost}/api/keys`,
189
+ { method: 'GET', headers: jwtHeaders(jwtToken) },
190
+ );
191
+ }
192
+
193
+ export async function enableOAuthForKey(
194
+ keyId: string,
195
+ plaintextKey: string,
196
+ jwtToken: string,
197
+ apiHost: string,
198
+ ) {
199
+ return request<{ success: boolean; message: string }>(
200
+ `${scheme(apiHost)}://${apiHost}/api/keys/${keyId}/enable-oauth`,
201
+ {
202
+ method: 'POST',
203
+ headers: jwtHeaders(jwtToken),
204
+ body: JSON.stringify({ plaintextKey }),
205
+ },
206
+ );
207
+ }
208
+
209
+ export async function listOAuthProviders(apiHost: string) {
210
+ return request<Array<{
211
+ id: string;
212
+ name: string;
213
+ type: string;
214
+ grantType: string;
215
+ defaultScopes: string;
216
+ }>>(
217
+ `${scheme(apiHost)}://${apiHost}/api/oauth/providers`,
218
+ { method: 'GET', headers: { 'Content-Type': 'application/json' } },
219
+ );
220
+ }
221
+
222
+ export async function initiateOAuth(
223
+ apiKeyId: string,
224
+ providerId: string,
225
+ jwtToken: string,
226
+ apiHost: string,
227
+ ) {
228
+ return request<{ authorizationUrl: string; state: string }>(
229
+ `${scheme(apiHost)}://${apiHost}/api/oauth/authorize`,
230
+ {
231
+ method: 'POST',
232
+ headers: jwtHeaders(jwtToken),
233
+ body: JSON.stringify({ apiKeyId, providerId }),
234
+ },
235
+ );
236
+ }
237
+
238
+ export async function listOAuthBindings(jwtToken: string, apiHost: string) {
239
+ return request<Array<{
240
+ id: string;
241
+ apiKeyId: string;
242
+ providerId: string;
243
+ providerAccountId: string;
244
+ providerAccountName: string | null;
245
+ scopes: string;
246
+ createdAt: string;
247
+ updatedAt: string;
248
+ provider: { id: string; name: string; type: string };
249
+ }>>(
250
+ `${scheme(apiHost)}://${apiHost}/api/oauth/bindings`,
251
+ { method: 'GET', headers: jwtHeaders(jwtToken) },
252
+ );
253
+ }
254
+
255
+ export async function deleteOAuthBinding(
256
+ bindingId: string,
257
+ jwtToken: string,
258
+ apiHost: string,
259
+ ) {
260
+ return request<{ success: boolean }>(
261
+ `${scheme(apiHost)}://${apiHost}/api/oauth/bindings/${bindingId}`,
262
+ { method: 'DELETE', headers: jwtHeaders(jwtToken) },
263
+ );
264
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Top-level action commands: list, search, categories, services, get, call
3
+ * Unified interface for all actions (capabilities + APIs).
4
+ * Use --source capability|api to filter by source type.
5
+ */
6
+
7
+ import { getConfig, requireApiKey } from '../config.ts';
8
+ import * as client from '../client.ts';
9
+ import { output, err } from '../format.ts';
10
+
11
+ const VALID_SOURCES = ['capability', 'api'];
12
+
13
+ /** Validate and return source filter from --source flag */
14
+ function getSource(flags: Record<string, string>): string | undefined {
15
+ if (!flags.source) return undefined;
16
+ if (!VALID_SOURCES.includes(flags.source)) {
17
+ err(`invalid --source value: "${flags.source}". Must be "capability" or "api".`);
18
+ }
19
+ return flags.source;
20
+ }
21
+
22
+ export async function actionList(args: string[], flags: Record<string, string>) {
23
+ const cfg = getConfig();
24
+ try {
25
+ const res = await client.actionList(cfg, {
26
+ source: getSource(flags),
27
+ page: flags.page ? parseInt(flags.page) : undefined,
28
+ page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
29
+ category: flags.category,
30
+ service_id: flags['service-id'],
31
+ });
32
+ const actions = (res.actions || []) as any[];
33
+ if (flags.format === 'table') {
34
+ output(actions.map((a: any) => ({
35
+ id: a.id,
36
+ method: a.method ?? '',
37
+ displayName: a.displayName ?? '',
38
+ source: a.source ?? '',
39
+ category: a.meta?.category ?? '',
40
+ status: a.status ?? '',
41
+ cost: a.meta?.cost ?? '',
42
+ })), 'table');
43
+ } else {
44
+ output(res, flags.format as any);
45
+ }
46
+ } catch (e: any) {
47
+ err('list failed', e.message);
48
+ }
49
+ }
50
+
51
+ export async function actionSearch(args: string[], flags: Record<string, string>) {
52
+ const query = args[0];
53
+ if (!query) err('usage: xapi search <query>');
54
+ const cfg = getConfig();
55
+ try {
56
+ const res = await client.actionSearch(query, cfg, {
57
+ source: getSource(flags),
58
+ category: flags.category,
59
+ page: flags.page ? parseInt(flags.page) : undefined,
60
+ page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
61
+ });
62
+ const results = (res.results || []) as any[];
63
+ if (flags.format === 'table') {
64
+ output(results.map((a: any) => ({
65
+ id: a.id,
66
+ method: a.method ?? '',
67
+ displayName: a.displayName ?? '',
68
+ source: a.source ?? '',
69
+ category: a.meta?.category ?? '',
70
+ status: a.status ?? '',
71
+ cost: a.meta?.cost ?? '',
72
+ })), 'table');
73
+ } else {
74
+ output(res, flags.format as any);
75
+ }
76
+ } catch (e: any) {
77
+ err('search failed', e.message);
78
+ }
79
+ }
80
+
81
+ export async function actionCategories(args: string[], flags: Record<string, string>) {
82
+ const cfg = getConfig();
83
+ try {
84
+ const res = await client.actionCategories(cfg, { source: getSource(flags) });
85
+ if (flags.format === 'table') {
86
+ output(res.categories.map(c => ({ category: c })), 'table');
87
+ } else {
88
+ output(res, flags.format as any);
89
+ }
90
+ } catch (e: any) {
91
+ err('categories failed', e.message);
92
+ }
93
+ }
94
+
95
+ export async function actionServices(args: string[], flags: Record<string, string>) {
96
+ const cfg = getConfig();
97
+ try {
98
+ const res = await client.actionServices(cfg, {
99
+ page: flags.page ? parseInt(flags.page) : undefined,
100
+ page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
101
+ category: flags.category,
102
+ });
103
+ const services = (res.services || []) as any[];
104
+ if (flags.format === 'table') {
105
+ output(services.map((s: any) => ({
106
+ id: s.id,
107
+ name: s.name ?? '',
108
+ category: s.category ?? '',
109
+ source: s.source ?? '',
110
+ endpoints: s.endpointCount ?? '',
111
+ status: s.status ?? '',
112
+ })), 'table');
113
+ } else {
114
+ output(res, flags.format as any);
115
+ }
116
+ } catch (e: any) {
117
+ err('services failed', e.message);
118
+ }
119
+ }
120
+
121
+ export async function actionGet(args: string[], flags: Record<string, string>) {
122
+ const id = args[0];
123
+ if (!id) err('usage: xapi get <id> [--method GET|POST|DELETE|...]');
124
+ const cfg = getConfig();
125
+ try {
126
+ const res = await client.actionGet(id, cfg);
127
+ const actions = Array.isArray(res) ? res : [res];
128
+ const methodFilter = flags.method?.toUpperCase();
129
+ const filtered = methodFilter
130
+ ? actions.filter((a: any) => a.method?.toUpperCase() === methodFilter)
131
+ : actions;
132
+ if (filtered.length === 0) {
133
+ err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
134
+ }
135
+ output(filtered.length === 1 ? filtered[0] : filtered, flags.format as any);
136
+ } catch (e: any) {
137
+ err('get failed', e.message);
138
+ }
139
+ }
140
+
141
+ export async function actionCall(args: string[], flags: Record<string, string>) {
142
+ const id = args[0];
143
+ if (!id) err('usage: xapi call <id> --input \'{"key":"val"}\'');
144
+ const cfg = getConfig();
145
+ requireApiKey(cfg);
146
+ let input: Record<string, unknown> = {};
147
+ if (flags.input) {
148
+ try {
149
+ input = JSON.parse(flags.input);
150
+ } catch {
151
+ err('--input must be valid JSON');
152
+ }
153
+ }
154
+ if (flags.method) {
155
+ input = { ...input, method: flags.method.toUpperCase() };
156
+ }
157
+ try {
158
+ const res = await client.actionCall(id, input, cfg);
159
+ output(res, flags.format as any);
160
+ } catch (e: any) {
161
+ err('call failed', e.message);
162
+ }
163
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * balance command
3
+ * Fetches xTokenBalance from GET /auth/me
4
+ */
5
+
6
+ import { getConfig, requireApiKey, XAPI_API_HOST, scheme } from '../config.ts';
7
+ import { loginWithApiKey, request } from '../client.ts';
8
+ import { output, err } from '../format.ts';
9
+
10
+ export async function balance(args: string[], flags: Record<string, string>) {
11
+ const cfg = getConfig();
12
+ requireApiKey(cfg);
13
+
14
+ let token: string;
15
+ try {
16
+ const res = await loginWithApiKey(cfg.apiKey!, XAPI_API_HOST);
17
+ token = res.accessToken;
18
+ } catch (e: any) {
19
+ err('login failed', e.message);
20
+ }
21
+
22
+ try {
23
+ const me = await request<{ xTokenBalance: string; accountType: string; tier: string }>(
24
+ `${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/me`,
25
+ { method: 'GET', headers: { Authorization: `Bearer ${token!}` } },
26
+ );
27
+ output({
28
+ balance: me.xTokenBalance,
29
+ accountType: me.accountType,
30
+ tier: me.tier,
31
+ }, flags.format as any);
32
+ } catch (e: any) {
33
+ err('balance fetch failed', e.message);
34
+ }
35
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * config commands: show, set, health
3
+ */
4
+
5
+ import { getConfig, saveConfig, showConfig } from '../config.ts';
6
+ import { healthCheck } from '../client.ts';
7
+ import { output, err } from '../format.ts';
8
+
9
+ export async function configShow(args: string[], flags: Record<string, string>) {
10
+ showConfig();
11
+ }
12
+
13
+ export async function configSet(args: string[], flags: Record<string, string>) {
14
+ // xapi config set apiKey=xapi_xxx
15
+ if (args.length === 0) err('usage: xapi config set apiKey=<key>');
16
+ const updates: { apiKey?: string } = {};
17
+ for (const arg of args) {
18
+ const eq = arg.indexOf('=');
19
+ if (eq < 1) err(`invalid key=value: ${arg}`);
20
+ const key = arg.slice(0, eq);
21
+ if (key === 'host') err('host is built-in and cannot be configured');
22
+ if (key !== 'apiKey') err(`unknown config key: ${key} (only apiKey is configurable)`);
23
+ updates.apiKey = arg.slice(eq + 1);
24
+ }
25
+ saveConfig(updates);
26
+ console.log(JSON.stringify({ ok: true, updated: Object.keys(updates) }));
27
+ }
28
+
29
+ export async function configHealth(args: string[], flags: Record<string, string>) {
30
+ const cfg = getConfig();
31
+ const start = Date.now();
32
+ try {
33
+ await healthCheck(cfg);
34
+ output({ status: 'ok', host: cfg.actionHost, latency_ms: Date.now() - start }, flags.format as any);
35
+ } catch (e: any) {
36
+ output({ status: 'error', host: cfg.actionHost, error: e.message }, flags.format as any);
37
+ process.exit(1);
38
+ }
39
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * oauth commands: bind, status, unbind
3
+ *
4
+ * Flow for `xapi oauth bind [--provider twitter]`:
5
+ * 1. Login with current API key → get JWT
6
+ * 2. List API keys → find the one matching the current key by prefix
7
+ * 3. Enable OAuth on the key if not already (POST /keys/:id/enable-oauth)
8
+ * 4. List OAuth providers → find the requested provider
9
+ * 5. POST /oauth/authorize → get authorizationUrl
10
+ * 6. Open browser (macOS/Linux/Windows) and poll for binding completion
11
+ *
12
+ * `xapi oauth status`: list current OAuth bindings for the API key
13
+ * `xapi oauth unbind <binding-id>`: delete an OAuth binding
14
+ */
15
+
16
+ import { spawnSync } from 'child_process';
17
+ import { XAPI_API_HOST, getConfig, requireApiKey } from '../config.ts';
18
+ import {
19
+ loginWithApiKey,
20
+ listKeys,
21
+ enableOAuthForKey,
22
+ listOAuthProviders,
23
+ initiateOAuth,
24
+ listOAuthBindings,
25
+ deleteOAuthBinding,
26
+ } from '../client.ts';
27
+ import { output, err } from '../format.ts';
28
+
29
+ /** Try to open a URL in the default browser. Silent on failure. */
30
+ function openBrowser(url: string): void {
31
+ const cmd = process.platform === 'win32' ? 'start'
32
+ : process.platform === 'darwin' ? 'open'
33
+ : 'xdg-open';
34
+ try {
35
+ spawnSync(cmd, [url], { stdio: 'ignore' });
36
+ } catch {
37
+ // ignore — user can open manually
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Poll bindings until one for (apiKeyId, providerId) appears.
43
+ * Shows a live countdown in TTY mode.
44
+ * Returns the matched binding or null on timeout.
45
+ */
46
+ async function pollForBinding(
47
+ apiKeyId: string,
48
+ providerId: string,
49
+ jwtToken: string,
50
+ timeoutMs = 5 * 60 * 1000,
51
+ intervalMs = 3000,
52
+ ): Promise<{ providerAccountName: string | null } | null> {
53
+ const deadline = Date.now() + timeoutMs;
54
+ const isTTY = process.stdout.isTTY;
55
+
56
+ while (Date.now() < deadline) {
57
+ await new Promise((r) => setTimeout(r, intervalMs));
58
+
59
+ try {
60
+ const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
61
+ const match = Array.isArray(bindings)
62
+ ? bindings.find((b) => b.apiKeyId === apiKeyId && b.providerId === providerId)
63
+ : null;
64
+ if (match) return match;
65
+ } catch {
66
+ // transient error — keep polling
67
+ }
68
+
69
+ if (isTTY) {
70
+ const remaining = Math.ceil((deadline - Date.now()) / 1000);
71
+ process.stdout.write(`\r Waiting for authorization... (${remaining}s remaining) `);
72
+ }
73
+ }
74
+
75
+ if (process.stdout.isTTY) process.stdout.write('\n');
76
+ return null;
77
+ }
78
+
79
+ // ── Helpers ────────────────────────────────────────────────────────────────────
80
+
81
+ async function loginAndGetJwt(apiKey: string): Promise<string> {
82
+ const result = await loginWithApiKey(apiKey, XAPI_API_HOST) as any;
83
+ if (!result?.accessToken) {
84
+ throw new Error('Login failed: no access token returned');
85
+ }
86
+ return result.accessToken;
87
+ }
88
+
89
+ /**
90
+ * Find the API key record that corresponds to the current plaintext API key.
91
+ * Matches by key prefix (first 7 chars of the plaintext key = keyPrefix).
92
+ */
93
+ async function findCurrentKeyRecord(
94
+ plaintextKey: string,
95
+ jwtToken: string,
96
+ ): Promise<{ id: string; name: string; keyPreview: string; oauthEnabled: boolean }> {
97
+ const keys = await listKeys(jwtToken, XAPI_API_HOST);
98
+ if (!Array.isArray(keys) || keys.length === 0) {
99
+ throw new Error('No API keys found for this account');
100
+ }
101
+ if (keys.length === 1) return keys[0];
102
+ // Match by prefix: keyPreview starts with the key's prefix
103
+ const prefix = plaintextKey.substring(0, 7);
104
+ const match = keys.find((k) => k.keyPreview.startsWith(prefix));
105
+ if (!match) {
106
+ // Fallback: use the first key
107
+ return keys[0];
108
+ }
109
+ return match;
110
+ }
111
+
112
+ // ── Commands ───────────────────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * xapi oauth bind [--provider twitter]
116
+ *
117
+ * Initiates OAuth binding for the current API key.
118
+ * Prints the authorization URL for the user to open in a browser.
119
+ */
120
+ export async function oauthBind(args: string[], flags: Record<string, string>) {
121
+ const cfg = getConfig();
122
+ requireApiKey(cfg);
123
+ const apiKey = cfg.apiKey!;
124
+ const providerName = (flags.provider || 'twitter').toLowerCase();
125
+
126
+ try {
127
+ // 1. Login to get JWT
128
+ const jwtToken = await loginAndGetJwt(apiKey);
129
+
130
+ // 2. Find the API key record
131
+ const keyRecord = await findCurrentKeyRecord(apiKey, jwtToken);
132
+
133
+ // 3. Enable OAuth on the key if needed
134
+ if (!keyRecord.oauthEnabled) {
135
+ await enableOAuthForKey(keyRecord.id, apiKey, jwtToken, XAPI_API_HOST);
136
+ }
137
+
138
+ // 4. Find the requested OAuth provider
139
+ const providers = await listOAuthProviders(XAPI_API_HOST);
140
+ if (!Array.isArray(providers) || providers.length === 0) {
141
+ throw new Error('No OAuth providers available');
142
+ }
143
+ const provider = providers.find(
144
+ (p) =>
145
+ p.type.toLowerCase() === providerName ||
146
+ p.name.toLowerCase().includes(providerName),
147
+ );
148
+ if (!provider) {
149
+ const available = providers.map((p) => p.type).join(', ');
150
+ throw new Error(
151
+ `Provider "${providerName}" not found. Available: ${available}`,
152
+ );
153
+ }
154
+
155
+ // 5. Initiate OAuth authorization
156
+ const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST);
157
+ const { authorizationUrl } = result;
158
+
159
+ const isTTY = process.stdout.isTTY;
160
+
161
+ if (isTTY) {
162
+ // Interactive mode: open browser + poll
163
+ console.error(`\n Provider : ${provider.name}`);
164
+ console.error(` API Key : ${keyRecord.keyPreview}`);
165
+ console.error(`\n Authorization URL:\n ${authorizationUrl}\n`);
166
+ console.error(' Opening browser...');
167
+ openBrowser(authorizationUrl);
168
+ console.error(' Waiting for you to complete authorization in the browser...\n');
169
+
170
+ const binding = await pollForBinding(keyRecord.id, provider.id, jwtToken);
171
+
172
+ if (process.stdout.isTTY) process.stdout.write('\n');
173
+
174
+ if (binding) {
175
+ const account = (binding as any).providerAccountName || 'unknown';
176
+ console.error(`\n Authorization complete! Bound to @${account}\n`);
177
+ output({ status: 'success', provider: provider.name, account }, flags.format as any);
178
+ } else {
179
+ err('oauth bind timed out', 'Authorization was not completed within 5 minutes. Run "xapi oauth bind" again.');
180
+ }
181
+ } else {
182
+ // Non-interactive / agent mode: just output the URL
183
+ output({
184
+ status: 'pending',
185
+ provider: provider.name,
186
+ apiKey: keyRecord.keyPreview,
187
+ authorizationUrl,
188
+ }, flags.format as any);
189
+ }
190
+ } catch (e: any) {
191
+ err('oauth bind failed', e.message);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * xapi oauth status
197
+ *
198
+ * Lists all OAuth bindings for the current account.
199
+ */
200
+ export async function oauthStatus(args: string[], flags: Record<string, string>) {
201
+ const cfg = getConfig();
202
+ requireApiKey(cfg);
203
+ const apiKey = cfg.apiKey!;
204
+
205
+ try {
206
+ const jwtToken = await loginAndGetJwt(apiKey);
207
+ const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
208
+
209
+ if (!Array.isArray(bindings) || bindings.length === 0) {
210
+ output({
211
+ status: 'no_bindings',
212
+ message: 'No OAuth bindings found. Run "xapi oauth bind" to connect an account.',
213
+ }, flags.format as any);
214
+ return;
215
+ }
216
+
217
+ output({
218
+ status: 'ok',
219
+ count: bindings.length,
220
+ bindings: bindings.map((b) => ({
221
+ id: b.id,
222
+ provider: b.provider.name,
223
+ providerType: b.provider.type,
224
+ account: b.providerAccountName || b.providerAccountId,
225
+ apiKeyId: b.apiKeyId,
226
+ scopes: b.scopes,
227
+ boundAt: b.createdAt,
228
+ })),
229
+ }, flags.format as any);
230
+ } catch (e: any) {
231
+ err('oauth status failed', e.message);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * xapi oauth unbind <binding-id>
237
+ *
238
+ * Deletes an OAuth binding. Get the ID from `xapi oauth status`.
239
+ */
240
+ export async function oauthUnbind(args: string[], flags: Record<string, string>) {
241
+ const cfg = getConfig();
242
+ requireApiKey(cfg);
243
+ const apiKey = cfg.apiKey!;
244
+
245
+ const bindingId = args[0];
246
+ if (!bindingId) {
247
+ err('usage: xapi oauth unbind <binding-id>', 'Get the binding ID from "xapi oauth status"');
248
+ }
249
+
250
+ try {
251
+ const jwtToken = await loginAndGetJwt(apiKey);
252
+ const result = await deleteOAuthBinding(bindingId, jwtToken, XAPI_API_HOST);
253
+ output({ success: result.success, message: 'OAuth binding removed' }, flags.format as any);
254
+ } catch (e: any) {
255
+ err('oauth unbind failed', e.message);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * xapi oauth providers
261
+ *
262
+ * Lists available OAuth providers.
263
+ */
264
+ export async function oauthProviders(args: string[], flags: Record<string, string>) {
265
+ try {
266
+ const providers = await listOAuthProviders(XAPI_API_HOST);
267
+ output(providers, flags.format as any);
268
+ } catch (e: any) {
269
+ err('oauth providers failed', e.message);
270
+ }
271
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * register command: create a new user account
3
+ *
4
+ * POST /auth/register — no auth required
5
+ * Returns apiKey (shown once), claimCode, claimUrl, tweetTemplate
6
+ * Automatically saves apiKey to ~/.xapi/config.json
7
+ */
8
+
9
+ import { XAPI_API_HOST, saveConfig, scheme } from '../config.ts';
10
+ import { output, err } from '../format.ts';
11
+
12
+ async function registerAccount() {
13
+ const controller = new AbortController();
14
+ const timer = setTimeout(() => controller.abort(), 15_000);
15
+ try {
16
+ const res = await fetch(`${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/register`, {
17
+ method: 'POST',
18
+ headers: { 'Content-Type': 'application/json' },
19
+ signal: controller.signal,
20
+ });
21
+ if (!res.ok) {
22
+ const text = await res.text();
23
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
24
+ }
25
+ return res.json() as Promise<{
26
+ apiKey: string;
27
+ claimCode: string;
28
+ claimSessionId: string;
29
+ claimUrl: string;
30
+ tweetTemplate: string;
31
+ user: { id: string; accountType: string };
32
+ }>;
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+
38
+ export async function register(args: string[], flags: Record<string, string>) {
39
+ try {
40
+ const res = await registerAccount();
41
+
42
+ // auto-save apiKey
43
+ saveConfig({ apiKey: res.apiKey });
44
+
45
+ output({
46
+ apiKey: res.apiKey,
47
+ user: res.user,
48
+ claim: {
49
+ code: res.claimCode,
50
+ sessionId: res.claimSessionId,
51
+ url: res.claimUrl,
52
+ },
53
+ tweetTemplate: res.tweetTemplate,
54
+ note: 'apiKey saved to ~/.xapi/config.json',
55
+ }, flags.format as any);
56
+ } catch (e: any) {
57
+ err('register failed', e.message);
58
+ }
59
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * topup command
3
+ *
4
+ * Generates a payment URL pointing to the xapi frontend topup page.
5
+ * All params are optional.
6
+ *
7
+ * Usage:
8
+ * xapi topup [--amount <usd>] [--method stripe|x402]
9
+ */
10
+
11
+ import { getConfig } from '../config.ts';
12
+ import { output } from '../format.ts';
13
+
14
+ const TOPUP_BASE_URL = 'https://www.xapi.to/topup/payment';
15
+
16
+ export async function topup(args: string[], flags: Record<string, string>) {
17
+ const cfg = getConfig();
18
+
19
+ const url = new URL(TOPUP_BASE_URL);
20
+
21
+ if (cfg.apiKey) url.searchParams.set('apikey', cfg.apiKey);
22
+ if (flags.method) url.searchParams.set('method', flags.method);
23
+
24
+ const amountStr = flags.amount || args[0];
25
+ if (amountStr) {
26
+ const amountUsd = parseFloat(amountStr);
27
+ if (!isNaN(amountUsd) && amountUsd > 0) {
28
+ url.searchParams.set('amount', String(amountUsd));
29
+ }
30
+ }
31
+
32
+ output({ url: url.toString() }, flags.format as any);
33
+ }
package/src/config.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Config management
3
+ * Only apiKey is user-configurable. Host is built-in.
4
+ * Reads from env var XAPI_API_KEY or ~/.xapi/config.json
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
8
+ import { err } from './format.ts';
9
+ import { homedir } from 'os';
10
+ import { join } from 'path';
11
+
12
+ export const XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || 'action.xapi.to'; // action service (capabilities + APIs)
13
+ export const XAPI_API_HOST = process.env.XAPI_API_HOST || 'api.xapi.to'; // auth + agent API
14
+
15
+ /** Returns https:// for remote hosts, http:// for localhost */
16
+ export function scheme(host: string): string {
17
+ return host.startsWith('localhost') || host.startsWith('127.') ? 'http' : 'https';
18
+ }
19
+
20
+ export interface XapiConfig {
21
+ actionHost: string;
22
+ apiKey?: string;
23
+ }
24
+
25
+ const CONFIG_DIR = join(homedir(), '.xapi');
26
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
27
+
28
+ function loadFileConfig(): { apiKey?: string } {
29
+ if (!existsSync(CONFIG_FILE)) return {};
30
+ try {
31
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
32
+ } catch {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ export function getConfig(): XapiConfig {
38
+ const file = loadFileConfig();
39
+ return {
40
+ actionHost: XAPI_ACTION_HOST,
41
+ apiKey: process.env.XAPI_API_KEY || file.apiKey,
42
+ };
43
+ }
44
+
45
+ export function requireApiKey(cfg: XapiConfig): void {
46
+ if (!cfg.apiKey) {
47
+ err('API key not configured', 'Run "npx @xapi-to/xapi register" to create an account, or "npx @xapi-to/xapi config set apiKey=<key>" to set an existing key.');
48
+ }
49
+ }
50
+
51
+ export function saveConfig(updates: { apiKey?: string }): void {
52
+ const current = loadFileConfig();
53
+ const merged = { ...current, ...updates };
54
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
55
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
56
+ }
57
+
58
+ export function showConfig(): void {
59
+ const cfg = getConfig();
60
+ const file = loadFileConfig();
61
+ console.log(JSON.stringify({
62
+ actionHost: cfg.actionHost,
63
+ apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined,
64
+ source: {
65
+ apiKey: process.env.XAPI_API_KEY ? 'env' : file.apiKey ? 'file' : 'none',
66
+ },
67
+ configFile: CONFIG_FILE,
68
+ }, null, 2));
69
+ }
package/src/format.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Output formatting
3
+ * Supports: json (default, machine-readable), pretty (human-readable), table
4
+ */
5
+
6
+ export type OutputFormat = 'json' | 'pretty' | 'table';
7
+
8
+ export function getFormat(): OutputFormat {
9
+ const f = process.env.XAPI_OUTPUT || 'json';
10
+ if (f === 'pretty' || f === 'table') return f;
11
+ return 'json';
12
+ }
13
+
14
+ export function output(data: unknown, format?: OutputFormat): void {
15
+ const fmt = format || getFormat();
16
+ if (fmt === 'json') {
17
+ console.log(JSON.stringify(data));
18
+ return;
19
+ }
20
+ if (fmt === 'pretty') {
21
+ console.log(JSON.stringify(data, null, 2));
22
+ return;
23
+ }
24
+ // table: try to render arrays of objects as a table
25
+ if (fmt === 'table' && Array.isArray(data)) {
26
+ printTable(data as Record<string, unknown>[]);
27
+ return;
28
+ }
29
+ console.log(JSON.stringify(data, null, 2));
30
+ }
31
+
32
+ function printTable(rows: Record<string, unknown>[]): void {
33
+ if (rows.length === 0) {
34
+ console.log('(empty)');
35
+ return;
36
+ }
37
+ const keys = Object.keys(rows[0]);
38
+ const widths = keys.map(k =>
39
+ Math.min(40, Math.max(k.length, ...rows.map(r => String(r[k] ?? '').length)))
40
+ );
41
+ const sep = widths.map(w => '-'.repeat(w)).join(' ');
42
+ const header = keys.map((k, i) => k.padEnd(widths[i])).join(' ');
43
+ console.log(header);
44
+ console.log(sep);
45
+ for (const row of rows) {
46
+ const line = keys.map((k, i) => String(row[k] ?? '').slice(0, widths[i]).padEnd(widths[i])).join(' ');
47
+ console.log(line);
48
+ }
49
+ }
50
+
51
+ export function err(msg: string, detail?: unknown): never {
52
+ if (process.stderr.isTTY) {
53
+ console.error(`Error: ${msg}`);
54
+ if (detail !== undefined) console.error(` ${detail}`);
55
+ } else {
56
+ const out: Record<string, unknown> = { error: msg };
57
+ if (detail !== undefined) out.detail = detail;
58
+ console.error(JSON.stringify(out));
59
+ }
60
+ process.exit(1);
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * xapi CLI - agent-friendly command-line interface for xapi
4
+ *
5
+ * Usage:
6
+ * xapi list [--source capability|api] [--page N] [--page-size N] [--category X]
7
+ * xapi search <query> [--source capability|api] [--category X] [--page N] [--page-size N]
8
+ * xapi categories [--source capability|api]
9
+ * xapi services [--page N] [--page-size N] [--category X]
10
+ * xapi get <id>
11
+ * xapi call <id> --input '{"k":"v"}'
12
+ *
13
+ * xapi config show
14
+ * xapi config set apiKey=<key>
15
+ * xapi config health
16
+ *
17
+ * Global flags:
18
+ * --format json|pretty|table output format (default: json)
19
+ * --help show help
20
+ *
21
+ * Env vars:
22
+ * XAPI_API_KEY API key
23
+ * XAPI_ACTION_HOST Action service host (default: action.xapi.to)
24
+ * XAPI_OUTPUT default output format (json|pretty|table)
25
+ */
26
+
27
+ import * as actionCmds from './commands/action.ts';
28
+ import * as cfgCmds from './commands/config.ts';
29
+ import * as regCmds from './commands/register.ts';
30
+ import * as topupCmds from './commands/topup.ts';
31
+ import * as balanceCmds from './commands/balance.ts';
32
+ import * as oauthCmds from './commands/oauth.ts';
33
+
34
+ // ── Argument parser ───────────────────────────────────────────────────────────
35
+
36
+ interface ParsedArgs {
37
+ positional: string[];
38
+ flags: Record<string, string>;
39
+ }
40
+
41
+ function parseArgs(argv: string[]): ParsedArgs {
42
+ const positional: string[] = [];
43
+ const flags: Record<string, string> = {};
44
+ let i = 0;
45
+ while (i < argv.length) {
46
+ const arg = argv[i];
47
+ if (arg.startsWith('--')) {
48
+ const key = arg.slice(2);
49
+ const next = argv[i + 1];
50
+ if (next && !next.startsWith('--')) {
51
+ flags[key] = next;
52
+ i += 2;
53
+ } else {
54
+ flags[key] = 'true';
55
+ i++;
56
+ }
57
+ } else {
58
+ positional.push(arg);
59
+ i++;
60
+ }
61
+ }
62
+ return { positional, flags };
63
+ }
64
+
65
+ // ── Help ──────────────────────────────────────────────────────────────────────
66
+
67
+ const HELP = `xapi - agent-friendly CLI for xapi
68
+
69
+ USAGE
70
+ xapi <command> [args] [flags]
71
+
72
+ COMMANDS
73
+ list List all actions
74
+ --source capability|api Filter by source type
75
+ --page N --page-size N Pagination
76
+ --category <name> Filter by category
77
+ --service-id <id> Filter by service
78
+ search <query> Search actions by keyword
79
+ --source capability|api Filter by source type
80
+ --category <name> Filter by category
81
+ --page N --page-size N Pagination
82
+ categories List all action categories
83
+ --source capability|api Filter by source type
84
+ services List all services
85
+ --page N --page-size N Pagination
86
+ --category <name> Filter by category
87
+ get <id> [--method GET|POST|...] Get action schema (filter by HTTP method)
88
+ call <id> --input '{"key":"val"}' Execute an action
89
+
90
+ oauth bind [--provider twitter] Bind Twitter OAuth to your API key
91
+ oauth status List current OAuth bindings
92
+ oauth unbind <binding-id> Remove an OAuth binding
93
+ oauth providers List available OAuth providers
94
+
95
+ register Create a new user account (apiKey saved automatically)
96
+ balance Show current account balance
97
+ topup [--amount <usd>] [--method stripe|x402] Generate payment URL
98
+
99
+ config show Show current config
100
+ config set apiKey=<key> Save API key to ~/.xapi/config.json
101
+ config health Check backend connectivity
102
+
103
+ GLOBAL FLAGS
104
+ --format json|pretty|table Output format (default: json)
105
+ --help Show this help
106
+
107
+ ENV VARS
108
+ XAPI_API_KEY API key (header: XAPI-Key)
109
+ XAPI_ACTION_HOST Action service host (default: action.xapi.to)
110
+ XAPI_OUTPUT Default output format
111
+
112
+ EXAMPLES
113
+ xapi register
114
+ xapi list --format table
115
+ xapi list --source capability
116
+ xapi search twitter --source api
117
+ xapi get twitter.tweet_detail
118
+ xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
119
+ xapi categories
120
+ xapi services --format table
121
+ xapi config set apiKey=xapi_abc123
122
+ xapi config health
123
+ `;
124
+
125
+ // ── Router ────────────────────────────────────────────────────────────────────
126
+
127
+ async function main() {
128
+ const { positional, flags } = parseArgs(process.argv.slice(2));
129
+
130
+ if (flags.help || positional.length === 0) {
131
+ console.log(HELP);
132
+ process.exit(0);
133
+ }
134
+
135
+ // inject format from flag into env so format.ts picks it up
136
+ if (flags.format) process.env.XAPI_OUTPUT = flags.format;
137
+
138
+ const [cmd, ...rest] = positional;
139
+
140
+ switch (cmd) {
141
+ // ── Action commands (top-level) ──
142
+ case 'list': return actionCmds.actionList(rest, flags);
143
+ case 'search': return actionCmds.actionSearch(rest, flags);
144
+ case 'categories': return actionCmds.actionCategories(rest, flags);
145
+ case 'services': return actionCmds.actionServices(rest, flags);
146
+ case 'get': return actionCmds.actionGet(rest, flags);
147
+ case 'call': return actionCmds.actionCall(rest, flags);
148
+
149
+ // ── OAuth commands ──
150
+ case 'oauth': {
151
+ const [subCmd, ...subRest] = rest;
152
+ switch (subCmd) {
153
+ case 'bind': return oauthCmds.oauthBind(subRest, flags);
154
+ case 'status': return oauthCmds.oauthStatus(subRest, flags);
155
+ case 'unbind': return oauthCmds.oauthUnbind(subRest, flags);
156
+ case 'providers': return oauthCmds.oauthProviders(subRest, flags);
157
+ default:
158
+ console.error(JSON.stringify({ error: `unknown oauth command: ${subCmd}`, hint: 'valid commands: bind, status, unbind, providers' }));
159
+ process.exit(1);
160
+ }
161
+ break;
162
+ }
163
+
164
+ // ── Account commands ──
165
+ case 'register': return regCmds.register(rest, flags);
166
+ case 'balance': return balanceCmds.balance(rest, flags);
167
+ case 'topup': return topupCmds.topup(rest, flags);
168
+
169
+ // ── Config commands ──
170
+ case 'config': {
171
+ const [subCmd, ...subRest] = rest;
172
+ switch (subCmd) {
173
+ case 'show': return cfgCmds.configShow(subRest, flags);
174
+ case 'set': return cfgCmds.configSet(subRest, flags);
175
+ case 'health': return cfgCmds.configHealth(subRest, flags);
176
+ default:
177
+ console.error(JSON.stringify({ error: `unknown config command: ${subCmd}` }));
178
+ process.exit(1);
179
+ }
180
+ break;
181
+ }
182
+
183
+ default:
184
+ console.error(JSON.stringify({ error: `unknown command: ${cmd}`, hint: 'run xapi --help' }));
185
+ process.exit(1);
186
+ }
187
+ }
188
+
189
+ main().catch(e => {
190
+ console.error(JSON.stringify({ error: 'fatal', message: e.message }));
191
+ process.exit(1);
192
+ });