@cloudverse/aix-cli 1.0.0 → 1.1.1

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/dist/auth.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ interface StoredTokens {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_at: number;
5
+ org_id: string;
6
+ api_url: string;
7
+ }
8
+ export declare function saveTokens(data: {
9
+ access_token: string;
10
+ refresh_token: string;
11
+ expires_in: number;
12
+ org_id: string;
13
+ api_url: string;
14
+ }): void;
15
+ export declare function loadTokens(): StoredTokens | null;
16
+ export declare function clearTokens(): void;
17
+ export declare function refreshIfNeeded(): Promise<void>;
18
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,75 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { request, setTokenOverride } from './http.js';
5
+ function getConfigDir() {
6
+ const dir = path.join(os.homedir(), '.aix');
7
+ if (!fs.existsSync(dir)) {
8
+ fs.mkdirSync(dir, { mode: 0o700, recursive: true });
9
+ }
10
+ return dir;
11
+ }
12
+ function getTokenPath() {
13
+ return path.join(getConfigDir(), 'tokens.json');
14
+ }
15
+ export function saveTokens(data) {
16
+ const tokens = {
17
+ access_token: data.access_token,
18
+ refresh_token: data.refresh_token,
19
+ expires_at: Date.now() + data.expires_in * 1000,
20
+ org_id: data.org_id,
21
+ api_url: data.api_url,
22
+ };
23
+ const tokenPath = getTokenPath();
24
+ fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
25
+ }
26
+ export function loadTokens() {
27
+ const tokenPath = getTokenPath();
28
+ if (!fs.existsSync(tokenPath))
29
+ return null;
30
+ try {
31
+ const raw = fs.readFileSync(tokenPath, 'utf-8');
32
+ const tokens = JSON.parse(raw);
33
+ if (!tokens.access_token || !tokens.refresh_token)
34
+ return null;
35
+ setTokenOverride(tokens.access_token);
36
+ return tokens;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ export function clearTokens() {
43
+ const tokenPath = getTokenPath();
44
+ if (fs.existsSync(tokenPath)) {
45
+ fs.unlinkSync(tokenPath);
46
+ }
47
+ setTokenOverride(null);
48
+ }
49
+ export async function refreshIfNeeded() {
50
+ const tokens = loadTokens();
51
+ if (!tokens)
52
+ return;
53
+ const bufferMs = 60 * 1000;
54
+ if (tokens.expires_at > Date.now() + bufferMs) {
55
+ return;
56
+ }
57
+ try {
58
+ const res = await request('POST', '/api/auth/device/refresh', {
59
+ refresh_token: tokens.refresh_token,
60
+ });
61
+ if (res.ok && res.data.access_token) {
62
+ saveTokens({
63
+ access_token: res.data.access_token,
64
+ refresh_token: res.data.refresh_token,
65
+ expires_in: res.data.expires_in,
66
+ org_id: res.data.org_id || tokens.org_id,
67
+ api_url: tokens.api_url,
68
+ });
69
+ setTokenOverride(res.data.access_token);
70
+ }
71
+ }
72
+ catch {
73
+ // Refresh failed silently - existing token may still work
74
+ }
75
+ }
package/dist/http.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export declare function setTokenOverride(token: string | null): void;
2
+ export declare function setBaseUrl(url: string | null): void;
1
3
  export interface HttpConfig {
2
4
  baseUrl: string;
3
5
  token: string | null;
package/dist/http.js CHANGED
@@ -1,8 +1,16 @@
1
- const DEFAULT_BASE_URL = 'http://localhost:5000';
1
+ const DEFAULT_BASE_URL = 'https://aix.cloudverse.ai';
2
+ let tokenOverride = null;
3
+ let baseUrlOverride = null;
4
+ export function setTokenOverride(token) {
5
+ tokenOverride = token;
6
+ }
7
+ export function setBaseUrl(url) {
8
+ baseUrlOverride = url ? url.replace(/\/$/, '') : null;
9
+ }
2
10
  export function getConfig() {
3
11
  return {
4
- baseUrl: (process.env.AIX_API_URL || DEFAULT_BASE_URL).replace(/\/$/, ''),
5
- token: process.env.AIX_TOKEN || null,
12
+ baseUrl: baseUrlOverride || (process.env.AIX_API_URL || DEFAULT_BASE_URL).replace(/\/$/, ''),
13
+ token: tokenOverride || process.env.AIX_TOKEN || null,
6
14
  };
7
15
  }
8
16
  export async function request(method, path, body) {
@@ -15,17 +23,31 @@ export async function request(method, path, body) {
15
23
  if (config.token) {
16
24
  headers['Authorization'] = `Bearer ${config.token}`;
17
25
  }
18
- const res = await fetch(url, {
19
- method,
20
- headers,
21
- body: body ? JSON.stringify(body) : undefined,
22
- });
23
- let data;
26
+ let res;
24
27
  try {
25
- data = await res.json();
28
+ res = await fetch(url, {
29
+ method,
30
+ headers,
31
+ body: body ? JSON.stringify(body) : undefined,
32
+ });
33
+ }
34
+ catch (err) {
35
+ throw new Error(`Cannot connect to ${config.baseUrl}: ${err.message || err}`);
36
+ }
37
+ let data;
38
+ const contentType = res.headers.get('content-type') || '';
39
+ if (contentType.includes('application/json')) {
40
+ try {
41
+ data = await res.json();
42
+ }
43
+ catch {
44
+ data = { error: `Failed to parse JSON response (HTTP ${res.status})` };
45
+ }
26
46
  }
27
- catch {
28
- data = { error: `HTTP ${res.status}: ${res.statusText}` };
47
+ else {
48
+ const text = await res.text().catch(() => '');
49
+ data = { error: `Server returned non-JSON response (HTTP ${res.status})`, body: text.slice(0, 200) };
50
+ return { ok: false, status: res.status, data };
29
51
  }
30
52
  return { ok: res.ok, status: res.status, data };
31
53
  }
package/dist/index.js CHANGED
@@ -1,12 +1,135 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { request, exitWithError } from './http.js';
3
+ import { request, exitWithError, getConfig, setBaseUrl } from './http.js';
4
4
  import { formatDecision, formatExplanation, formatHealth, formatPolicy, formatCatalogStatus } from './formatters.js';
5
+ import { loadTokens, saveTokens, clearTokens, refreshIfNeeded } from './auth.js';
5
6
  const program = new Command();
6
7
  program
7
8
  .name('aix')
8
9
  .description('AIX Brain Decision Engine CLI — decision-only surface')
9
10
  .version('1.0.0');
11
+ program
12
+ .command('login')
13
+ .description('Authenticate with AIX via browser-based device login')
14
+ .option('--api-url <url>', 'AIX server URL (default: https://aix.cloudverse.ai)')
15
+ .action(async (opts) => {
16
+ if (opts.apiUrl) {
17
+ setBaseUrl(opts.apiUrl);
18
+ }
19
+ const config = getConfig();
20
+ const baseUrl = config.baseUrl;
21
+ console.log('\n\x1b[1mAIX Device Login\x1b[0m');
22
+ console.log('─'.repeat(50));
23
+ console.log(` Server: ${baseUrl}\n`);
24
+ let startRes;
25
+ try {
26
+ startRes = await request('POST', '/api/auth/device/start', {
27
+ cli_version: '1.1.0',
28
+ });
29
+ }
30
+ catch (err) {
31
+ exitWithError(`Cannot connect to AIX server at ${baseUrl}`, `Network error: ${err.message}\n Try: aix login --api-url https://your-server-url`);
32
+ }
33
+ if (!startRes.ok) {
34
+ exitWithError('Failed to start device login', JSON.stringify(startRes.data, null, 2));
35
+ }
36
+ const { device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in } = startRes.data;
37
+ if (!user_code || !verification_uri_complete || !device_code) {
38
+ exitWithError('Unexpected response from server (missing device login fields)', `Server may not support device login, or the URL is incorrect.\n Server: ${baseUrl}\n Response: ${JSON.stringify(startRes.data, null, 2)}\n Try: aix login --api-url https://your-server-url`);
39
+ }
40
+ console.log(`\n Your verification code:\n`);
41
+ console.log(` \x1b[1m\x1b[36m${user_code}\x1b[0m\n`);
42
+ console.log(` Opening browser to:\n`);
43
+ console.log(` \x1b[4m${verification_uri_complete}\x1b[0m\n`);
44
+ const openBrowser = async (url) => {
45
+ const { exec } = await import('child_process');
46
+ const platform = process.platform;
47
+ const cmd = platform === 'darwin' ? 'open'
48
+ : platform === 'win32' ? 'start'
49
+ : 'xdg-open';
50
+ exec(`${cmd} "${url}"`, (err) => {
51
+ if (err) {
52
+ console.log(` \x1b[33mCould not open browser automatically.\x1b[0m`);
53
+ console.log(` Please open the URL above manually.\n`);
54
+ }
55
+ });
56
+ };
57
+ await openBrowser(verification_uri_complete);
58
+ console.log(` Waiting for approval (expires in ${Math.floor(expires_in / 60)} minutes)...\n`);
59
+ const deadline = Date.now() + expires_in * 1000;
60
+ let pollInterval = interval * 1000;
61
+ while (Date.now() < deadline) {
62
+ await new Promise(r => setTimeout(r, pollInterval));
63
+ const pollRes = await request('POST', '/api/auth/device/poll', { device_code });
64
+ if (pollRes.data.status === 'authorization_pending') {
65
+ process.stdout.write('.');
66
+ continue;
67
+ }
68
+ if (pollRes.data.status === 'slow_down') {
69
+ pollInterval = pollRes.data.interval * 1000;
70
+ continue;
71
+ }
72
+ if (pollRes.data.status === 'ok') {
73
+ console.log('\n');
74
+ saveTokens({
75
+ access_token: pollRes.data.access_token,
76
+ refresh_token: pollRes.data.refresh_token,
77
+ expires_in: pollRes.data.expires_in,
78
+ org_id: pollRes.data.org_id,
79
+ api_url: baseUrl,
80
+ });
81
+ console.log(' \x1b[32m✓ Login successful!\x1b[0m');
82
+ console.log(` Org: ${pollRes.data.org_id}`);
83
+ console.log(` Server: ${baseUrl}`);
84
+ console.log('\n Token saved. You can now use \x1b[1maix decide\x1b[0m and other commands.\n');
85
+ return;
86
+ }
87
+ if (pollRes.data.status === 'expired') {
88
+ console.log('\n');
89
+ exitWithError('Login expired. Please try again.');
90
+ }
91
+ if (pollRes.data.status === 'access_denied') {
92
+ console.log('\n');
93
+ exitWithError('Login was denied.');
94
+ }
95
+ exitWithError('Unexpected response', JSON.stringify(pollRes.data, null, 2));
96
+ }
97
+ console.log('\n');
98
+ exitWithError('Login timed out. Please try again.');
99
+ });
100
+ program
101
+ .command('logout')
102
+ .description('Clear saved authentication tokens')
103
+ .action(() => {
104
+ clearTokens();
105
+ console.log('\n \x1b[32m✓ Logged out.\x1b[0m Tokens cleared.\n');
106
+ });
107
+ program
108
+ .command('whoami')
109
+ .description('Show current authentication status')
110
+ .action(async () => {
111
+ const tokens = loadTokens();
112
+ if (!tokens) {
113
+ console.log('\n Not logged in. Run \x1b[1maix login\x1b[0m to authenticate.\n');
114
+ return;
115
+ }
116
+ console.log('\n\x1b[1mAIX Auth Status\x1b[0m');
117
+ console.log('─'.repeat(50));
118
+ console.log(` Org: ${tokens.org_id}`);
119
+ console.log(` Server: ${tokens.api_url}`);
120
+ const expiresAt = tokens.expires_at ? new Date(tokens.expires_at) : null;
121
+ if (expiresAt) {
122
+ const remaining = expiresAt.getTime() - Date.now();
123
+ if (remaining > 0) {
124
+ const mins = Math.floor(remaining / 60000);
125
+ console.log(` Token: valid (${mins}m remaining)`);
126
+ }
127
+ else {
128
+ console.log(' Token: expired (will auto-refresh on next command)');
129
+ }
130
+ }
131
+ console.log('');
132
+ });
10
133
  program
11
134
  .command('decide')
12
135
  .description('Request a routing decision from the AIX Brain')
@@ -22,6 +145,7 @@ program
22
145
  .option('--intent-json <path>', 'Path to JSON file with full request payload')
23
146
  .option('--format <string>', 'Output format: pretty | json', 'pretty')
24
147
  .action(async (opts) => {
148
+ await refreshIfNeeded();
25
149
  let payload;
26
150
  if (opts.intentJson) {
27
151
  const fs = await import('fs');
@@ -76,6 +200,7 @@ program
76
200
  .description('Get explanation and trace for a past decision')
77
201
  .option('--format <string>', 'Output format: pretty | json', 'pretty')
78
202
  .action(async (decisionId, opts) => {
203
+ await refreshIfNeeded();
79
204
  const res = await request('GET', `/api/brain/decisions/${decisionId}`);
80
205
  if (!res.ok) {
81
206
  exitWithError(`Failed to fetch decision (HTTP ${res.status})`, JSON.stringify(res.data, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudverse/aix-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "AIX Brain Decision Engine CLI - decision-only surface for AI workload routing",
5
5
  "type": "module",
6
6
  "bin": {