@invizi/cli 0.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/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # invizi-cli
2
+
3
+ Thin CLI for Invizi with local auth/setup commands and remote command execution.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @invizi/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ invizi auth login
15
+ invizi auth status
16
+ invizi --help
17
+ invizi connect 0x1234...abcd --label main
18
+ invizi setup
19
+ ```
20
+
21
+ ## Development
22
+
23
+ ### Scripts
24
+
25
+ ```bash
26
+ npm run build # Compile TypeScript to dist/
27
+ npm run typecheck # TypeScript checks (no emit)
28
+ npm test # Jest tests
29
+ ```
30
+
31
+ ### Project Layout
32
+
33
+ - `src/` TypeScript source code
34
+ - `dist/` compiled runtime (generated)
35
+ - `bin/invizi.js` published executable wrapper
36
+
37
+ ### Packaging
38
+
39
+ - `npm pack` and `npm publish` run `prepack`, which builds `dist/` automatically.
package/bin/invizi.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/invizi.js';
3
+
4
+ main(process.argv.slice(2))
5
+ .then((code) => process.exit(code))
6
+ .catch((err) => {
7
+ console.error(err?.message || String(err));
8
+ process.exit(1);
9
+ });
package/dist/auth.js ADDED
@@ -0,0 +1,232 @@
1
+ import { getApiUrl, loadConfig, saveConfig } from './config.js';
2
+ const DEFAULT_SCOPE = 'openid profile email offline_access';
3
+ function sleep(ms) {
4
+ return new Promise(resolve => setTimeout(resolve, ms));
5
+ }
6
+ function getAuth0Config(config = loadConfig()) {
7
+ const auth = config.auth;
8
+ const domain = process.env.AUTH0_DOMAIN || config.auth0Domain || auth?.domain;
9
+ const audience = process.env.AUTH0_AUDIENCE || config.auth0Audience || auth?.audience;
10
+ const clientId = process.env.AUTH0_CLIENT_ID || config.auth0ClientId || auth?.clientId;
11
+ if (!domain || !audience || !clientId) {
12
+ throw new Error('Missing AUTH0_DOMAIN, AUTH0_AUDIENCE, or AUTH0_CLIENT_ID. Set env vars or save them in config.');
13
+ }
14
+ return { domain, audience, clientId };
15
+ }
16
+ function getStoredAuth() {
17
+ const cfg = loadConfig();
18
+ return cfg.auth || null;
19
+ }
20
+ function setStoredAuth(auth) {
21
+ const cfg = loadConfig();
22
+ cfg.auth = auth;
23
+ saveConfig(cfg);
24
+ }
25
+ function clearStoredAuth() {
26
+ const cfg = loadConfig();
27
+ delete cfg.auth;
28
+ saveConfig(cfg);
29
+ }
30
+ function isTokenValid(auth, skewMs = 30_000) {
31
+ if (!auth || !auth.accessToken || !auth.expiresAt)
32
+ return false;
33
+ return auth.expiresAt > Date.now() + skewMs;
34
+ }
35
+ export function inferEmail(profile) {
36
+ if (!profile || typeof profile !== 'object')
37
+ return null;
38
+ const p = profile;
39
+ if (typeof p.email === 'string' && p.email.includes('@'))
40
+ return p.email;
41
+ if (typeof p.name === 'string' && p.name.includes('@'))
42
+ return p.name;
43
+ if (typeof p.nickname === 'string' && p.nickname.includes('@'))
44
+ return p.nickname;
45
+ return null;
46
+ }
47
+ async function fetchAuth0UserProfile(accessToken, domain) {
48
+ if (!accessToken || !domain)
49
+ return null;
50
+ const res = await fetch(`https://${domain}/userinfo`, {
51
+ headers: { Authorization: `Bearer ${accessToken}` },
52
+ });
53
+ if (!res.ok)
54
+ return null;
55
+ return await res.json();
56
+ }
57
+ async function refreshAccessToken(auth) {
58
+ if (!auth.refreshToken)
59
+ return null;
60
+ const { domain, clientId } = getAuth0Config();
61
+ const res = await fetch(`https://${domain}/oauth/token`, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
64
+ body: new URLSearchParams({
65
+ grant_type: 'refresh_token',
66
+ client_id: clientId,
67
+ refresh_token: auth.refreshToken,
68
+ }),
69
+ });
70
+ if (!res.ok)
71
+ return null;
72
+ const data = await res.json();
73
+ if (!data.access_token || !data.expires_in)
74
+ return null;
75
+ return {
76
+ ...auth,
77
+ accessToken: data.access_token,
78
+ refreshToken: data.refresh_token || auth.refreshToken,
79
+ expiresAt: Date.now() + (data.expires_in * 1000),
80
+ tokenType: data.token_type || auth.tokenType,
81
+ scope: data.scope || auth.scope,
82
+ };
83
+ }
84
+ export async function getAccessToken() {
85
+ const auth = getStoredAuth();
86
+ if (!auth)
87
+ return null;
88
+ if (isTokenValid(auth))
89
+ return auth.accessToken;
90
+ const refreshed = await refreshAccessToken(auth);
91
+ if (!refreshed)
92
+ return null;
93
+ setStoredAuth(refreshed);
94
+ return refreshed.accessToken;
95
+ }
96
+ export async function getAuthorizationHeader() {
97
+ const accessToken = await getAccessToken();
98
+ if (accessToken)
99
+ return `Bearer ${accessToken}`;
100
+ if (process.env.INVIZI_API_KEY) {
101
+ return `Bearer ${process.env.INVIZI_API_KEY}`;
102
+ }
103
+ return null;
104
+ }
105
+ async function login() {
106
+ const { domain, audience, clientId } = getAuth0Config();
107
+ const deviceRes = await fetch(`https://${domain}/oauth/device/code`, {
108
+ method: 'POST',
109
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
110
+ body: new URLSearchParams({
111
+ client_id: clientId,
112
+ audience,
113
+ scope: DEFAULT_SCOPE,
114
+ }),
115
+ });
116
+ if (!deviceRes.ok) {
117
+ const body = await deviceRes.text();
118
+ throw new Error(`Auth0 device flow init failed: ${body}`);
119
+ }
120
+ const device = await deviceRes.json();
121
+ console.log('\nAuth0 login required');
122
+ console.log(`1) Open: ${device.verification_uri_complete || device.verification_uri}`);
123
+ if (!device.verification_uri_complete) {
124
+ console.log(`2) Enter code: ${device.user_code}`);
125
+ }
126
+ const deadline = Date.now() + (device.expires_in * 1000);
127
+ let intervalMs = (device.interval || 5) * 1000;
128
+ while (Date.now() < deadline) {
129
+ await sleep(intervalMs);
130
+ const tokenRes = await fetch(`https://${domain}/oauth/token`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
133
+ body: new URLSearchParams({
134
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
135
+ device_code: device.device_code,
136
+ client_id: clientId,
137
+ }),
138
+ });
139
+ const token = await tokenRes.json();
140
+ if (tokenRes.ok && token.access_token && token.expires_in) {
141
+ setStoredAuth({
142
+ accessToken: token.access_token,
143
+ refreshToken: token.refresh_token,
144
+ expiresAt: Date.now() + (token.expires_in * 1000),
145
+ tokenType: token.token_type,
146
+ scope: token.scope,
147
+ domain,
148
+ audience,
149
+ clientId,
150
+ });
151
+ console.log('\nLogin successful.');
152
+ return 0;
153
+ }
154
+ if (token.error === 'authorization_pending')
155
+ continue;
156
+ if (token.error === 'slow_down') {
157
+ intervalMs += 5000;
158
+ continue;
159
+ }
160
+ if (token.error === 'expired_token') {
161
+ throw new Error('Device code expired. Run `invizi auth login` again.');
162
+ }
163
+ throw new Error(`Login failed: ${token.error || 'unknown error'}`);
164
+ }
165
+ throw new Error('Login timed out before authorization completed.');
166
+ }
167
+ async function status() {
168
+ const auth = getStoredAuth();
169
+ if (!auth) {
170
+ if (process.env.INVIZI_API_KEY) {
171
+ console.log('No Auth0 token stored. Using INVIZI_API_KEY fallback.');
172
+ }
173
+ else {
174
+ console.log('Not logged in. Run: invizi auth login');
175
+ return 1;
176
+ }
177
+ }
178
+ else {
179
+ const ttlSec = Math.max(0, Math.floor((auth.expiresAt - Date.now()) / 1000));
180
+ console.log(`Token: present (expires in ~${ttlSec}s)`);
181
+ }
182
+ const header = await getAuthorizationHeader();
183
+ if (!header) {
184
+ console.log('No credentials available.');
185
+ return 1;
186
+ }
187
+ // Use v1 protocol (auth.me) instead of /api/me (not exposed via nginx)
188
+ const res = await fetch(`${getApiUrl()}/cli/v1/execute`, {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json', Authorization: header },
191
+ body: JSON.stringify({ commandId: 'auth.me' }),
192
+ });
193
+ if (!res.ok) {
194
+ console.log(`Unable to query identity (${res.status})`);
195
+ return 1;
196
+ }
197
+ const response = await res.json();
198
+ if (!response.ok || !response.result) {
199
+ console.log(`Unable to query identity: ${response.error?.message || 'unknown error'}`);
200
+ return 1;
201
+ }
202
+ const me = response.result;
203
+ console.log(`User: ${me.id} (${me.role}) via ${me.authProvider}`);
204
+ let email = me.email || null;
205
+ if (!email && auth?.accessToken && auth?.domain) {
206
+ const profile = await fetchAuth0UserProfile(auth.accessToken, auth.domain);
207
+ email = inferEmail(profile);
208
+ }
209
+ if (email) {
210
+ console.log(`Email: ${email}`);
211
+ }
212
+ if (Array.isArray(me.permissions) && me.permissions.length > 0) {
213
+ console.log(`Permissions: ${me.permissions.join(', ')}`);
214
+ }
215
+ return 0;
216
+ }
217
+ function logout() {
218
+ clearStoredAuth();
219
+ console.log('Logged out.');
220
+ return 0;
221
+ }
222
+ export async function auth(args) {
223
+ const sub = args[0] || 'status';
224
+ if (sub === 'login')
225
+ return login();
226
+ if (sub === 'logout')
227
+ return logout();
228
+ if (sub === 'status')
229
+ return status();
230
+ console.error('Usage: invizi auth <login|logout|status>');
231
+ return 1;
232
+ }
package/dist/config.js ADDED
@@ -0,0 +1,46 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export const DEFAULT_API_URL = 'https://api.invizi.co';
5
+ export function getConfigDir(home = homedir()) {
6
+ const xdg = join(home, '.config', 'invizi');
7
+ const fallback = join(home, '.invizi');
8
+ if (existsSync(join(home, '.config'))) {
9
+ return xdg;
10
+ }
11
+ return fallback;
12
+ }
13
+ export function getConfigPath(home = homedir()) {
14
+ return join(getConfigDir(home), 'config.json');
15
+ }
16
+ export function loadConfig(configPath = getConfigPath()) {
17
+ try {
18
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ export function saveConfig(config, configPath = getConfigPath()) {
25
+ mkdirSync(dirname(configPath), { recursive: true });
26
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
27
+ }
28
+ export function resolveApiUrl(config, env = process.env) {
29
+ return config.apiUrl || env.INVIZI_API_URL || env.INVIZI_SERVER || DEFAULT_API_URL;
30
+ }
31
+ export function getApiUrl() {
32
+ return resolveApiUrl(loadConfig());
33
+ }
34
+ export function redactConfig(config) {
35
+ const auth = config.auth
36
+ ? {
37
+ ...config.auth,
38
+ accessToken: '***',
39
+ refreshToken: config.auth.refreshToken ? '***' : undefined,
40
+ }
41
+ : undefined;
42
+ return {
43
+ ...config,
44
+ auth,
45
+ };
46
+ }
@@ -0,0 +1,131 @@
1
+ import { getApiUrl, loadConfig, saveConfig } from './config.js';
2
+ import { getAuthorizationHeader } from './auth.js';
3
+ const HLP_API = 'https://api.hyperliquid.xyz/info';
4
+ function detectChain(address) {
5
+ if (/^0x[0-9a-fA-F]{40}$/.test(address))
6
+ return 'evm';
7
+ return null;
8
+ }
9
+ const EXCHANGE_CHECKERS = {
10
+ evm: [{ name: 'hyperliquid', check: validateHlpAddress }],
11
+ };
12
+ async function validateHlpAddress(address) {
13
+ const [stateRes, fillsRes] = await Promise.all([
14
+ fetch(HLP_API, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({ type: 'clearinghouseState', user: address }),
18
+ }),
19
+ fetch(HLP_API, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json' },
22
+ body: JSON.stringify({ type: 'userFills', user: address }),
23
+ }),
24
+ ]);
25
+ if (!stateRes.ok || !fillsRes.ok) {
26
+ return null;
27
+ }
28
+ const state = (await stateRes.json());
29
+ const fills = (await fillsRes.json());
30
+ return {
31
+ positions: state.assetPositions?.length || 0,
32
+ accountValue: Number.parseFloat(state.marginSummary?.accountValue || '0'),
33
+ trades: fills.length || 0,
34
+ };
35
+ }
36
+ async function registerWithServer(exchange, address, label) {
37
+ const apiUrl = getApiUrl();
38
+ const authHeader = await getAuthorizationHeader();
39
+ if (!authHeader) {
40
+ throw new Error('Not authenticated. Run: invizi auth login');
41
+ }
42
+ const res = await fetch(`${apiUrl}/api/connect`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ Authorization: authHeader,
47
+ },
48
+ body: JSON.stringify({ exchange, address, label }),
49
+ });
50
+ if (!res.ok) {
51
+ const bodyText = await res.text();
52
+ let parsed = null;
53
+ try {
54
+ parsed = JSON.parse(bodyText);
55
+ }
56
+ catch {
57
+ parsed = null;
58
+ }
59
+ if (res.status === 403 && parsed?.error?.includes('Tracking limit reached')) {
60
+ const tracked = parsed.trackedAddress;
61
+ const details = tracked
62
+ ? `existing ${tracked.exchange}:${tracked.address}${tracked.label ? ` (${tracked.label})` : ''}`
63
+ : 'already at limit';
64
+ throw new Error(`Tracking limit reached (${details}).`);
65
+ }
66
+ throw new Error(`Server error: ${res.status} ${parsed?.error || bodyText}`);
67
+ }
68
+ return (await res.json());
69
+ }
70
+ export async function connect(args) {
71
+ const address = args[0];
72
+ const labelIdx = args.indexOf('--label');
73
+ const label = labelIdx !== -1 ? args[labelIdx + 1] || null : null;
74
+ if (!address) {
75
+ console.error('Usage: invizi connect <wallet-address> [--label name]');
76
+ return 1;
77
+ }
78
+ const chain = detectChain(address);
79
+ if (!chain) {
80
+ console.error(`Unrecognized address format: ${address}`);
81
+ console.error('Supported: EVM addresses (0x...)');
82
+ return 1;
83
+ }
84
+ const checkers = EXCHANGE_CHECKERS[chain];
85
+ if (!checkers || checkers.length === 0) {
86
+ console.error(`No supported exchanges for chain: ${chain}`);
87
+ return 1;
88
+ }
89
+ console.log(`Scanning exchanges for ${address}...`);
90
+ const found = [];
91
+ for (const { name, check } of checkers) {
92
+ const info = await check(address);
93
+ if (info && (info.positions > 0 || info.trades > 0 || info.accountValue > 0)) {
94
+ found.push({ exchange: name, info });
95
+ console.log(` [ok] ${name}: ${info.positions} positions, ${info.trades} trades, $${info.accountValue.toFixed(2)}`);
96
+ }
97
+ else {
98
+ console.log(` [x] ${name}: no activity`);
99
+ }
100
+ }
101
+ if (found.length === 0) {
102
+ console.error('No activity found on any supported exchange for this address.');
103
+ return 1;
104
+ }
105
+ const selected = found[0];
106
+ try {
107
+ const result = await registerWithServer(selected.exchange, address, label);
108
+ const config = loadConfig();
109
+ config.userId = result.userId;
110
+ config.trackedAddressId = result.trackedAddressId;
111
+ delete config.accountId;
112
+ config.address = address;
113
+ config.exchange = selected.exchange;
114
+ if (label)
115
+ config.label = label;
116
+ saveConfig(config);
117
+ console.log(`Connected watch-only tracking: ${selected.exchange}`);
118
+ if (result.alreadyTracked) {
119
+ console.log(' Already tracked previously.');
120
+ }
121
+ console.log(` Positions: ${selected.info.positions}`);
122
+ console.log(` Trades: ${selected.info.trades}`);
123
+ console.log(` Account value: $${selected.info.accountValue.toFixed(2)}`);
124
+ return 0;
125
+ }
126
+ catch (error) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ console.error(`Failed to register: ${message}`);
129
+ return 1;
130
+ }
131
+ }
package/dist/invizi.js ADDED
@@ -0,0 +1,145 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { auth, getAuthorizationHeader } from './auth.js';
3
+ import { connect } from './connect.js';
4
+ import { getApiUrl, getConfigPath, loadConfig, redactConfig } from './config.js';
5
+ import { setup } from './setup.js';
6
+ function showLocalHelp() {
7
+ console.log(`
8
+ Invizi CLI
9
+
10
+ Commands:
11
+ invizi auth login Login via Auth0 device flow
12
+ invizi auth status Show current auth identity
13
+ invizi auth logout Clear local Auth0 session
14
+ invizi connect <address> Connect a wallet address (auto-detects exchange)
15
+ invizi setup Install skills into your AI tool
16
+ invizi config Show current config
17
+ invizi version Show CLI version
18
+
19
+ Run invizi --help after login for the remote command list.
20
+ `);
21
+ }
22
+ async function executeProtocol(args, authHeader) {
23
+ const apiUrl = getApiUrl();
24
+ const res = await fetch(`${apiUrl}/cli/v1/execute`, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ Authorization: authHeader,
29
+ },
30
+ body: JSON.stringify({ argv: args }),
31
+ });
32
+ if (res.status === 404) {
33
+ return { handled: false, exitCode: 0 };
34
+ }
35
+ if (res.status === 401 || res.status === 403) {
36
+ console.error('Auth error. Run: invizi auth login');
37
+ return { handled: true, exitCode: 1 };
38
+ }
39
+ const output = (await res.json().catch(() => null));
40
+ if (!output || typeof output !== 'object') {
41
+ console.error('Invalid protocol response from server');
42
+ return { handled: true, exitCode: 1 };
43
+ }
44
+ if (typeof output.text === 'string' && output.text.length > 0) {
45
+ process.stdout.write(`${output.text}${output.text.endsWith('\n') ? '' : '\n'}`);
46
+ }
47
+ if (!output.text && output.result !== undefined) {
48
+ process.stdout.write(`${JSON.stringify(output.result, null, 2)}\n`);
49
+ }
50
+ if (output.error?.message) {
51
+ process.stderr.write(`${output.error.message}\n`);
52
+ return { handled: true, exitCode: 1 };
53
+ }
54
+ return { handled: true, exitCode: output.ok === false ? 1 : 0 };
55
+ }
56
+ async function showRemoteHelp(authHeader) {
57
+ const apiUrl = getApiUrl();
58
+ const res = await fetch(`${apiUrl}/cli/v1/commands`, {
59
+ headers: { Authorization: authHeader },
60
+ });
61
+ if (!res.ok) {
62
+ throw new Error(`Unable to fetch command catalog (${res.status})`);
63
+ }
64
+ const data = (await res.json());
65
+ const commands = Array.isArray(data.commands) ? data.commands : [];
66
+ if (commands.length === 0) {
67
+ console.log('No commands available for this user.');
68
+ return;
69
+ }
70
+ console.log('Invizi Remote Commands\n');
71
+ for (const cmd of commands) {
72
+ const name = Array.isArray(cmd.tokens) ? cmd.tokens.join(' ') : cmd.id || '(unknown)';
73
+ const desc = cmd.description || '';
74
+ console.log(` ${name.padEnd(20)} ${desc}`);
75
+ }
76
+ console.log('\nUse: invizi <command> --help (if supported)');
77
+ }
78
+ async function executeRemote(args, options = {}) {
79
+ const authHeader = await getAuthorizationHeader();
80
+ if (!authHeader) {
81
+ console.error('Not authenticated. Run: invizi auth login');
82
+ return 1;
83
+ }
84
+ try {
85
+ const protocol = await executeProtocol(args, authHeader);
86
+ return protocol.exitCode;
87
+ }
88
+ catch (error) {
89
+ if (!options.quietNetworkError) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ console.error(message);
92
+ }
93
+ return 1;
94
+ }
95
+ }
96
+ export async function main(rawArgs = process.argv.slice(2)) {
97
+ const args = rawArgs;
98
+ const command = args[0];
99
+ if (!command) {
100
+ showLocalHelp();
101
+ return 0;
102
+ }
103
+ if (command === '--help' || command === '-h') {
104
+ const header = await getAuthorizationHeader();
105
+ if (!header) {
106
+ showLocalHelp();
107
+ return 0;
108
+ }
109
+ try {
110
+ await showRemoteHelp(header);
111
+ return 0;
112
+ }
113
+ catch {
114
+ try {
115
+ const code = await executeRemote(['--help'], { quietNetworkError: true });
116
+ if (code !== 0) {
117
+ showLocalHelp();
118
+ return 0;
119
+ }
120
+ return code;
121
+ }
122
+ catch {
123
+ showLocalHelp();
124
+ return 0;
125
+ }
126
+ }
127
+ }
128
+ if (command === 'auth')
129
+ return auth(args.slice(1));
130
+ if (command === 'connect')
131
+ return connect(args.slice(1));
132
+ if (command === 'setup')
133
+ return setup(args.slice(1));
134
+ if (command === 'config') {
135
+ console.log(JSON.stringify(redactConfig(loadConfig()), null, 2));
136
+ console.log(`\nConfig path: ${getConfigPath()}`);
137
+ return 0;
138
+ }
139
+ if (command === 'version') {
140
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
141
+ console.log(`invizi-cli v${pkg.version || 'unknown'}`);
142
+ return 0;
143
+ }
144
+ return executeRemote(args);
145
+ }
package/dist/setup.js ADDED
@@ -0,0 +1,114 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { loadConfig, saveConfig, getConfigPath } from './config.js';
5
+ const SKILLS_URL = 'https://pub-6e61fc0656544ad3bf01416b1d0a6065.r2.dev';
6
+ function detectAiTool() {
7
+ const checks = [
8
+ { name: 'Claude Code', dir: join(homedir(), '.claude', 'skills'), marker: join(homedir(), '.claude') },
9
+ { name: 'OpenCode', dir: join(homedir(), '.opencode', 'skills'), marker: join(homedir(), '.opencode') },
10
+ ];
11
+ for (const { name, dir, marker } of checks) {
12
+ if (existsSync(marker)) {
13
+ return { name, skillsDir: dir };
14
+ }
15
+ }
16
+ return null;
17
+ }
18
+ // Public Auth0 config (client-side values, not secrets)
19
+ const AUTH0_CONFIG = {
20
+ domain: 'dev-u1fgwppsg73adwpc.us.auth0.com',
21
+ audience: 'https://api-dev.invizi.co',
22
+ clientId: 'RK5mtxCZygkrBkQr9n2CkgSkjFFsYSsI',
23
+ };
24
+ export async function setup(args) {
25
+ const outputIdx = args.indexOf('--output');
26
+ const customOutput = outputIdx !== -1 ? args[outputIdx + 1] : null;
27
+ const dryRun = args.includes('--dry-run');
28
+ const isDev = args.includes('--dev');
29
+ const apiUrl = isDev ? 'http://localhost:3000' : 'https://api.invizi.co';
30
+ // Step 0: Configure API URL + Auth0
31
+ console.log(`Configuring Invizi CLI (${isDev ? 'dev' : 'prod'})...`);
32
+ if (!dryRun) {
33
+ const config = loadConfig();
34
+ let changed = false;
35
+ // Always update apiUrl if --dev flag changes the target
36
+ if (!config.apiUrl || config.apiUrl !== apiUrl) {
37
+ config.apiUrl = apiUrl;
38
+ changed = true;
39
+ }
40
+ if (!config.auth0Domain) {
41
+ config.auth0Domain = AUTH0_CONFIG.domain;
42
+ changed = true;
43
+ }
44
+ if (!config.auth0Audience) {
45
+ config.auth0Audience = AUTH0_CONFIG.audience;
46
+ changed = true;
47
+ }
48
+ if (!config.auth0ClientId) {
49
+ config.auth0ClientId = AUTH0_CONFIG.clientId;
50
+ changed = true;
51
+ }
52
+ if (changed) {
53
+ saveConfig(config);
54
+ console.log(` Config saved to ${getConfigPath()}`);
55
+ console.log(` API: ${config.apiUrl}`);
56
+ }
57
+ else {
58
+ console.log(' Config already set up.');
59
+ }
60
+ }
61
+ console.log('');
62
+ let skillsDir;
63
+ let toolName;
64
+ if (customOutput) {
65
+ skillsDir = customOutput;
66
+ toolName = 'custom path';
67
+ }
68
+ else {
69
+ const tool = detectAiTool();
70
+ if (!tool) {
71
+ console.error('No supported AI tool detected.');
72
+ console.error('Supported: Claude Code (~/.claude/), OpenCode (~/.opencode/)');
73
+ console.error('Use --output <path> to specify a custom install directory.');
74
+ return 1;
75
+ }
76
+ skillsDir = tool.skillsDir;
77
+ toolName = tool.name;
78
+ }
79
+ console.log(`Detected: ${toolName}`);
80
+ console.log(`Skills directory: ${skillsDir}`);
81
+ if (dryRun) {
82
+ console.log('(dry run - no files will be written)');
83
+ }
84
+ console.log('Fetching skills...');
85
+ const listRes = await fetch(`${SKILLS_URL}/list.json`);
86
+ if (!listRes.ok) {
87
+ console.error('Failed to fetch skill list from Invizi.');
88
+ return 1;
89
+ }
90
+ const skills = (await listRes.json());
91
+ for (const name of skills) {
92
+ const res = await fetch(`${SKILLS_URL}/${name}/SKILL.md`);
93
+ if (!res.ok) {
94
+ console.error(` [x] ${name} (download failed)`);
95
+ continue;
96
+ }
97
+ const content = await res.text();
98
+ if (!dryRun) {
99
+ const dir = join(skillsDir, name);
100
+ mkdirSync(dir, { recursive: true });
101
+ writeFileSync(join(dir, 'SKILL.md'), content);
102
+ }
103
+ console.log(` [ok] ${name}`);
104
+ }
105
+ console.log('');
106
+ if (dryRun) {
107
+ console.log('Dry run complete. No files written.');
108
+ }
109
+ else {
110
+ console.log(`Installed ${skills.length} skills to ${skillsDir}`);
111
+ console.log('Try /invizi-trade in your AI tool.');
112
+ }
113
+ return 0;
114
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@invizi/cli",
3
+ "version": "0.1.1",
4
+ "description": "Invizi CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "invizi": "./bin/invizi.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "dist/"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json",
15
+ "typecheck": "tsc --noEmit -p tsconfig.json",
16
+ "test": "jest",
17
+ "prepack": "npm run build"
18
+ },
19
+ "devDependencies": {
20
+ "@types/jest": "^30.0.0",
21
+ "@types/node": "^24.6.0",
22
+ "jest": "^30.2.0",
23
+ "ts-jest": "^29.4.4",
24
+ "typescript": "^5.9.3"
25
+ },
26
+ "keywords": [
27
+ "invizi",
28
+ "cli"
29
+ ],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/invizi/cli"
34
+ },
35
+ "homepage": "https://invizi.co"
36
+ }