@cloudverse/aix-cli 1.0.0 → 1.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/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,4 @@
1
+ export declare function setTokenOverride(token: string | null): void;
1
2
  export interface HttpConfig {
2
3
  baseUrl: string;
3
4
  token: string | null;
package/dist/http.js CHANGED
@@ -1,8 +1,12 @@
1
- const DEFAULT_BASE_URL = 'http://localhost:5000';
1
+ const DEFAULT_BASE_URL = 'https://aix.cloudverse.ai';
2
+ let tokenOverride = null;
3
+ export function setTokenOverride(token) {
4
+ tokenOverride = token;
5
+ }
2
6
  export function getConfig() {
3
7
  return {
4
8
  baseUrl: (process.env.AIX_API_URL || DEFAULT_BASE_URL).replace(/\/$/, ''),
5
- token: process.env.AIX_TOKEN || null,
9
+ token: tokenOverride || process.env.AIX_TOKEN || null,
6
10
  };
7
11
  }
8
12
  export async function request(method, path, body) {
package/dist/index.js CHANGED
@@ -1,12 +1,122 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { request, exitWithError } from './http.js';
3
+ import { request, exitWithError, getConfig } 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
+ const config = getConfig();
17
+ const baseUrl = opts.apiUrl || config.baseUrl;
18
+ console.log('\n\x1b[1mAIX Device Login\x1b[0m');
19
+ console.log('─'.repeat(50));
20
+ const startRes = await request('POST', '/api/auth/device/start', {
21
+ cli_version: '1.0.0',
22
+ });
23
+ if (!startRes.ok) {
24
+ exitWithError('Failed to start device login', JSON.stringify(startRes.data, null, 2));
25
+ }
26
+ const { device_code, user_code, verification_uri, verification_uri_complete, interval, expires_in } = startRes.data;
27
+ console.log(`\n Your verification code:\n`);
28
+ console.log(` \x1b[1m\x1b[36m${user_code}\x1b[0m\n`);
29
+ console.log(` Opening browser to:\n`);
30
+ console.log(` \x1b[4m${verification_uri_complete}\x1b[0m\n`);
31
+ const openBrowser = async (url) => {
32
+ const { exec } = await import('child_process');
33
+ const platform = process.platform;
34
+ const cmd = platform === 'darwin' ? 'open'
35
+ : platform === 'win32' ? 'start'
36
+ : 'xdg-open';
37
+ exec(`${cmd} "${url}"`, (err) => {
38
+ if (err) {
39
+ console.log(` \x1b[33mCould not open browser automatically.\x1b[0m`);
40
+ console.log(` Please open the URL above manually.\n`);
41
+ }
42
+ });
43
+ };
44
+ await openBrowser(verification_uri_complete);
45
+ console.log(` Waiting for approval (expires in ${Math.floor(expires_in / 60)} minutes)...\n`);
46
+ const deadline = Date.now() + expires_in * 1000;
47
+ let pollInterval = interval * 1000;
48
+ while (Date.now() < deadline) {
49
+ await new Promise(r => setTimeout(r, pollInterval));
50
+ const pollRes = await request('POST', '/api/auth/device/poll', { device_code });
51
+ if (pollRes.data.status === 'authorization_pending') {
52
+ process.stdout.write('.');
53
+ continue;
54
+ }
55
+ if (pollRes.data.status === 'slow_down') {
56
+ pollInterval = pollRes.data.interval * 1000;
57
+ continue;
58
+ }
59
+ if (pollRes.data.status === 'ok') {
60
+ console.log('\n');
61
+ saveTokens({
62
+ access_token: pollRes.data.access_token,
63
+ refresh_token: pollRes.data.refresh_token,
64
+ expires_in: pollRes.data.expires_in,
65
+ org_id: pollRes.data.org_id,
66
+ api_url: baseUrl,
67
+ });
68
+ console.log(' \x1b[32m✓ Login successful!\x1b[0m');
69
+ console.log(` Org: ${pollRes.data.org_id}`);
70
+ console.log(` Server: ${baseUrl}`);
71
+ console.log('\n Token saved. You can now use \x1b[1maix decide\x1b[0m and other commands.\n');
72
+ return;
73
+ }
74
+ if (pollRes.data.status === 'expired') {
75
+ console.log('\n');
76
+ exitWithError('Login expired. Please try again.');
77
+ }
78
+ if (pollRes.data.status === 'access_denied') {
79
+ console.log('\n');
80
+ exitWithError('Login was denied.');
81
+ }
82
+ exitWithError('Unexpected response', JSON.stringify(pollRes.data, null, 2));
83
+ }
84
+ console.log('\n');
85
+ exitWithError('Login timed out. Please try again.');
86
+ });
87
+ program
88
+ .command('logout')
89
+ .description('Clear saved authentication tokens')
90
+ .action(() => {
91
+ clearTokens();
92
+ console.log('\n \x1b[32m✓ Logged out.\x1b[0m Tokens cleared.\n');
93
+ });
94
+ program
95
+ .command('whoami')
96
+ .description('Show current authentication status')
97
+ .action(async () => {
98
+ const tokens = loadTokens();
99
+ if (!tokens) {
100
+ console.log('\n Not logged in. Run \x1b[1maix login\x1b[0m to authenticate.\n');
101
+ return;
102
+ }
103
+ console.log('\n\x1b[1mAIX Auth Status\x1b[0m');
104
+ console.log('─'.repeat(50));
105
+ console.log(` Org: ${tokens.org_id}`);
106
+ console.log(` Server: ${tokens.api_url}`);
107
+ const expiresAt = tokens.expires_at ? new Date(tokens.expires_at) : null;
108
+ if (expiresAt) {
109
+ const remaining = expiresAt.getTime() - Date.now();
110
+ if (remaining > 0) {
111
+ const mins = Math.floor(remaining / 60000);
112
+ console.log(` Token: valid (${mins}m remaining)`);
113
+ }
114
+ else {
115
+ console.log(' Token: expired (will auto-refresh on next command)');
116
+ }
117
+ }
118
+ console.log('');
119
+ });
10
120
  program
11
121
  .command('decide')
12
122
  .description('Request a routing decision from the AIX Brain')
@@ -22,6 +132,7 @@ program
22
132
  .option('--intent-json <path>', 'Path to JSON file with full request payload')
23
133
  .option('--format <string>', 'Output format: pretty | json', 'pretty')
24
134
  .action(async (opts) => {
135
+ await refreshIfNeeded();
25
136
  let payload;
26
137
  if (opts.intentJson) {
27
138
  const fs = await import('fs');
@@ -76,6 +187,7 @@ program
76
187
  .description('Get explanation and trace for a past decision')
77
188
  .option('--format <string>', 'Output format: pretty | json', 'pretty')
78
189
  .action(async (decisionId, opts) => {
190
+ await refreshIfNeeded();
79
191
  const res = await request('GET', `/api/brain/decisions/${decisionId}`);
80
192
  if (!res.ok) {
81
193
  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.0",
4
4
  "description": "AIX Brain Decision Engine CLI - decision-only surface for AI workload routing",
5
5
  "type": "module",
6
6
  "bin": {