@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 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
+ };
@@ -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
+ };