@xuda.ai/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/bin/xuda.mjs +59 -0
- package/package.json +41 -0
- package/src/auth.mjs +91 -0
- package/src/config.mjs +11 -0
- package/src/index.mjs +10 -0
- package/src/session.mjs +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @xuda.ai/cli
|
|
2
|
+
|
|
3
|
+
Run published Xuda AI agents from your terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i -g @xuda.ai/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Sign in
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
xuda login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Opens xuda.ai in your browser and asks you to approve a one-time code. The
|
|
18
|
+
access token is stored at `~/.xuda/credentials.json`.
|
|
19
|
+
|
|
20
|
+
## Run an agent
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
xuda run <agent-id>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install a specific agent's published shim — the package runs it for you:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
npm i -g @xuda.ai/<agent-slug>
|
|
30
|
+
<agent-slug>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|-------------------|---------------------------------------|
|
|
37
|
+
| `xuda login` | Device-code sign-in to Xuda |
|
|
38
|
+
| `xuda logout` | Forget stored credentials |
|
|
39
|
+
| `xuda whoami` | Print the signed-in account email |
|
|
40
|
+
| `xuda run <id>` | Open a chat session with an agent |
|
|
41
|
+
|
|
42
|
+
## Environment
|
|
43
|
+
|
|
44
|
+
| Variable | Default |
|
|
45
|
+
|------------------|--------------------------|
|
|
46
|
+
| `XUDA_API_URL` | `https://api.xuda.io` |
|
|
47
|
+
| `XUDA_WEB_URL` | `https://xuda.ai` |
|
|
48
|
+
| `XUDA_WS_URL` | derived from `XUDA_API_URL` |
|
|
49
|
+
|
|
50
|
+
## Notes
|
|
51
|
+
|
|
52
|
+
Only free agents are published to npm. Paid agents stay accessible through the
|
|
53
|
+
Xuda dashboard.
|
package/bin/xuda.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { login, clearCredentials, loadCredentials, runAgent } from '../src/index.mjs';
|
|
3
|
+
|
|
4
|
+
const [, , cmd, ...rest] = process.argv;
|
|
5
|
+
|
|
6
|
+
const help = () => {
|
|
7
|
+
console.log(`xuda — Xuda AI agent CLI
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
xuda login Sign in to Xuda
|
|
11
|
+
xuda logout Forget stored credentials
|
|
12
|
+
xuda whoami Show signed-in account
|
|
13
|
+
xuda run <agent-id> Run a published AI agent
|
|
14
|
+
xuda help Show this help
|
|
15
|
+
|
|
16
|
+
Per-agent CLIs (installed as @xuda.ai/<agent>) call xuda under the hood.
|
|
17
|
+
`);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const main = async () => {
|
|
21
|
+
try {
|
|
22
|
+
switch (cmd) {
|
|
23
|
+
case 'login':
|
|
24
|
+
await login();
|
|
25
|
+
break;
|
|
26
|
+
case 'logout':
|
|
27
|
+
await clearCredentials();
|
|
28
|
+
console.log('Signed out.');
|
|
29
|
+
break;
|
|
30
|
+
case 'whoami': {
|
|
31
|
+
const c = await loadCredentials();
|
|
32
|
+
if (!c) { console.log('Not signed in.'); process.exit(1); }
|
|
33
|
+
console.log(c.user_email || c.uid || '(unknown)');
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'run': {
|
|
37
|
+
const agentId = rest[0];
|
|
38
|
+
if (!agentId) { console.error('xuda run: agent-id required'); process.exit(2); }
|
|
39
|
+
await runAgent({ agentId });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'help':
|
|
43
|
+
case '--help':
|
|
44
|
+
case '-h':
|
|
45
|
+
case undefined:
|
|
46
|
+
help();
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
console.error(`unknown command: ${cmd}`);
|
|
50
|
+
help();
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(`error: ${err.message || err}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xuda.ai/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Xuda CLI — sign in to Xuda and run published AI agents from your terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xuda": "./bin/xuda.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.mjs",
|
|
11
|
+
"./run": "./src/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"socket.io-client": "^4.7.5"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"pub": "npm version patch --force && npm publish --access public"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"xuda",
|
|
29
|
+
"ai",
|
|
30
|
+
"agent",
|
|
31
|
+
"cli",
|
|
32
|
+
"chat"
|
|
33
|
+
],
|
|
34
|
+
"homepage": "https://xuda.ai",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/xudaio/xuda"
|
|
38
|
+
},
|
|
39
|
+
"author": "Xuda",
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
package/src/auth.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { API_URL, WEB_URL, CRED_DIR, CRED_FILE } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
|
|
7
|
+
const post = async (route, body) => {
|
|
8
|
+
const res = await fetch(`${API_URL}${route}`, {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: { 'content-type': 'application/json' },
|
|
11
|
+
body: JSON.stringify(body || {}),
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
const txt = await res.text().catch(() => '');
|
|
15
|
+
throw new Error(`POST ${route} ${res.status} ${txt}`);
|
|
16
|
+
}
|
|
17
|
+
return res.json();
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const openBrowser = (url) => {
|
|
21
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
22
|
+
try {
|
|
23
|
+
spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref();
|
|
24
|
+
} catch {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const loadCredentials = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(CRED_FILE, 'utf8');
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const saveCredentials = async (creds) => {
|
|
37
|
+
await fs.mkdir(CRED_DIR, { recursive: true, mode: 0o700 });
|
|
38
|
+
await fs.writeFile(CRED_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const clearCredentials = async () => {
|
|
42
|
+
try {
|
|
43
|
+
await fs.unlink(CRED_FILE);
|
|
44
|
+
} catch {}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const login = async () => {
|
|
48
|
+
const start = await post('/cpi/start_cli_auth', { client_name: 'xuda-cli' });
|
|
49
|
+
if (start.code < 0) throw new Error(`auth start failed: ${start.data}`);
|
|
50
|
+
const { device_code, user_code, verification_uri, interval = 3, expires_in = 600 } = start.data;
|
|
51
|
+
|
|
52
|
+
const fullUri = `${verification_uri}?code=${encodeURIComponent(user_code)}`;
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(' Sign in to Xuda to continue.');
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(` Open: ${fullUri}`);
|
|
57
|
+
console.log(` Code: ${user_code}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(' (waiting for approval...)');
|
|
60
|
+
openBrowser(fullUri);
|
|
61
|
+
|
|
62
|
+
const deadline = Date.now() + expires_in * 1000;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
await sleep(interval * 1000);
|
|
65
|
+
const poll = await post('/cpi/poll_cli_auth', { device_code });
|
|
66
|
+
if (poll.code === 1 && poll.data?.access_token) {
|
|
67
|
+
const creds = {
|
|
68
|
+
access_token: poll.data.access_token,
|
|
69
|
+
uid: poll.data.uid,
|
|
70
|
+
user_email: poll.data.user_email,
|
|
71
|
+
api_url: API_URL,
|
|
72
|
+
web_url: WEB_URL,
|
|
73
|
+
created_ts: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
await saveCredentials(creds);
|
|
76
|
+
console.log(` Signed in as ${creds.user_email}`);
|
|
77
|
+
return creds;
|
|
78
|
+
}
|
|
79
|
+
if (poll.code === -1 || poll.data === 'denied') {
|
|
80
|
+
throw new Error('access denied');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw new Error('auth code expired');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const requireCredentials = async () => {
|
|
87
|
+
const creds = await loadCredentials();
|
|
88
|
+
if (creds?.access_token) return creds;
|
|
89
|
+
console.log('Not signed in. Starting login...');
|
|
90
|
+
return await login();
|
|
91
|
+
};
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const API_URL = process.env.XUDA_API_URL || 'https://api.xuda.io';
|
|
5
|
+
export const WEB_URL = process.env.XUDA_WEB_URL || 'https://xuda.ai';
|
|
6
|
+
export const WS_URL = (process.env.XUDA_WS_URL || API_URL).replace(/^http/, 'ws') + '/ws';
|
|
7
|
+
|
|
8
|
+
export const CRED_DIR = path.join(os.homedir(), '.xuda');
|
|
9
|
+
export const CRED_FILE = path.join(CRED_DIR, 'credentials.json');
|
|
10
|
+
|
|
11
|
+
export const CLI_NAME = '@xuda.ai/cli';
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { requireCredentials } from './auth.mjs';
|
|
2
|
+
|
|
3
|
+
export { login, loadCredentials, saveCredentials, clearCredentials, requireCredentials } from './auth.mjs';
|
|
4
|
+
|
|
5
|
+
export const runAgent = async ({ agentId, agentName, agentVersion } = {}) => {
|
|
6
|
+
if (!agentId) throw new Error('runAgent: agentId is required');
|
|
7
|
+
const credentials = await requireCredentials();
|
|
8
|
+
const { runAgentSession } = await import('./session.mjs');
|
|
9
|
+
await runAgentSession({ agentId, agentName, agentVersion, credentials });
|
|
10
|
+
};
|
package/src/session.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { io } from 'socket.io-client';
|
|
3
|
+
import { API_URL } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
const CLI_VERSION = '0.1.0';
|
|
6
|
+
|
|
7
|
+
export const runAgentSession = async ({ agentId, agentName, agentVersion, credentials }) => {
|
|
8
|
+
const socket = io(API_URL, {
|
|
9
|
+
path: '/ws',
|
|
10
|
+
transports: ['websocket'],
|
|
11
|
+
reconnection: false,
|
|
12
|
+
query: {
|
|
13
|
+
source: 'cli',
|
|
14
|
+
gtp_token: credentials.access_token,
|
|
15
|
+
account_id: credentials.uid,
|
|
16
|
+
cli_version: CLI_VERSION,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
let sessionId = null;
|
|
21
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY });
|
|
22
|
+
const prompt = () => {
|
|
23
|
+
if (!process.stdin.isTTY) return;
|
|
24
|
+
rl.setPrompt('› ');
|
|
25
|
+
rl.prompt();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
const t = setTimeout(() => reject(new Error('handshake timeout')), 15000);
|
|
30
|
+
|
|
31
|
+
socket.on('connect', () => {
|
|
32
|
+
socket.emit('agent_handshake', { agent_id: agentId, agent_version: agentVersion, cli_version: CLI_VERSION });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
socket.on('handshake_ok', (msg) => {
|
|
36
|
+
clearTimeout(t);
|
|
37
|
+
sessionId = msg.session_id;
|
|
38
|
+
const name = msg.agent_name || agentName || 'agent';
|
|
39
|
+
console.log(`Connected to ${name}. (ctrl-c to exit)`);
|
|
40
|
+
if (msg.welcome) console.log(`\n${msg.welcome}\n`);
|
|
41
|
+
resolve();
|
|
42
|
+
prompt();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
socket.on('handshake_err', (msg) => {
|
|
46
|
+
clearTimeout(t);
|
|
47
|
+
reject(new Error(msg?.reason || 'handshake refused'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
socket.on('connect_error', (err) => {
|
|
51
|
+
clearTimeout(t);
|
|
52
|
+
reject(new Error(`connection error: ${err.message}`));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
socket.on('stream_start', () => process.stdout.write('\n'));
|
|
57
|
+
socket.on('stream_delta', (msg) => { if (msg?.text) process.stdout.write(msg.text); });
|
|
58
|
+
socket.on('stream_end', () => { process.stdout.write('\n\n'); prompt(); });
|
|
59
|
+
socket.on('error', (msg) => { console.error(`\nerror: ${msg?.message || 'unknown'}`); prompt(); });
|
|
60
|
+
|
|
61
|
+
socket.on('disconnect', () => {
|
|
62
|
+
rl.close();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
rl.on('line', (line) => {
|
|
67
|
+
const text = line.trim();
|
|
68
|
+
if (!text) { prompt(); return; }
|
|
69
|
+
socket.emit('prompt', { session_id: sessionId, text });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
rl.on('SIGINT', () => {
|
|
73
|
+
try { socket.emit('close', { session_id: sessionId }); } catch {}
|
|
74
|
+
socket.disconnect();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
};
|