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 +160 -0
- package/package.json +25 -0
- package/src/client.ts +264 -0
- package/src/commands/action.ts +163 -0
- package/src/commands/balance.ts +35 -0
- package/src/commands/config.ts +39 -0
- package/src/commands/oauth.ts +271 -0
- package/src/commands/register.ts +59 -0
- package/src/commands/topup.ts +33 -0
- package/src/config.ts +69 -0
- package/src/format.ts +61 -0
- package/src/index.ts +192 -0
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
|
+
});
|