@shiplens/cli 1.2.7

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/lib/auth.js ADDED
@@ -0,0 +1,107 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getGlobalConfig, saveGlobalConfig } = require('./config');
4
+
5
+ function maskSecret(secret) {
6
+ if (!secret || secret.length <= 8) {
7
+ return '******';
8
+ }
9
+ const prefixLen = secret.length < 12 ? 3 : 7;
10
+ const suffixLen = 4;
11
+ if (secret.length < prefixLen + suffixLen) {
12
+ return secret.slice(0, 3) + '...' + secret.slice(-2);
13
+ }
14
+ return secret.slice(0, prefixLen) + '...' + secret.slice(-suffixLen);
15
+ }
16
+
17
+ function readDotEnvSecret(dir = process.cwd()) {
18
+ const envFiles = [path.join(dir, '.shiplens.env'), path.join(dir, '.env')];
19
+ for (const f of envFiles) {
20
+ if (fs.existsSync(f)) {
21
+ try {
22
+ const content = fs.readFileSync(f, 'utf8');
23
+ const lines = content.split('\n');
24
+ for (const line of lines) {
25
+ const trimmed = line.trim();
26
+ if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
27
+ const [k, ...v] = trimmed.split('=');
28
+ const key = k.trim();
29
+ const val = v.join('=').trim().replace(/^["']|["']$/g, '');
30
+ if (key === 'SHIPLENS_ACCESS_SECRET' || key === 'SHIPLENS_MCP_TOKEN') {
31
+ return val;
32
+ }
33
+ }
34
+ } catch (e) {}
35
+ }
36
+ }
37
+ return '';
38
+ }
39
+
40
+ function resolveSecret(explicitSecret) {
41
+ if (explicitSecret) {
42
+ return {
43
+ secret: explicitSecret,
44
+ masked_secret: maskSecret(explicitSecret),
45
+ source: 'flag',
46
+ is_present: true,
47
+ };
48
+ }
49
+
50
+ const envSec = process.env.SHIPLENS_ACCESS_SECRET || process.env.SHIPLENS_MCP_TOKEN;
51
+ if (envSec) {
52
+ return {
53
+ secret: envSec,
54
+ masked_secret: maskSecret(envSec),
55
+ source: 'environment',
56
+ is_present: true,
57
+ };
58
+ }
59
+
60
+ const dotEnvSec = readDotEnvSecret();
61
+ if (dotEnvSec) {
62
+ return {
63
+ secret: dotEnvSec,
64
+ masked_secret: maskSecret(dotEnvSec),
65
+ source: 'dotenv',
66
+ is_present: true,
67
+ };
68
+ }
69
+
70
+ const gCfg = getGlobalConfig();
71
+ if (gCfg && gCfg.secret) {
72
+ return {
73
+ secret: gCfg.secret,
74
+ masked_secret: maskSecret(gCfg.secret),
75
+ source: 'config',
76
+ is_present: true,
77
+ };
78
+ }
79
+
80
+ return {
81
+ secret: '',
82
+ masked_secret: '',
83
+ source: 'none',
84
+ is_present: false,
85
+ };
86
+ }
87
+
88
+ function saveSecretToGlobal(secret, email = '', userId = '') {
89
+ const gCfg = getGlobalConfig();
90
+ gCfg.secret = secret;
91
+ if (email) gCfg.email = email;
92
+ if (userId) gCfg.user_id = userId;
93
+ saveGlobalConfig(gCfg);
94
+ }
95
+
96
+ function clearGlobalSecret() {
97
+ const gCfg = getGlobalConfig();
98
+ gCfg.secret = '';
99
+ saveGlobalConfig(gCfg);
100
+ }
101
+
102
+ module.exports = {
103
+ maskSecret,
104
+ resolveSecret,
105
+ saveSecretToGlobal,
106
+ clearGlobalSecret,
107
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,258 @@
1
+ const { APIClient } = require('./api');
2
+ const { resolveSecret } = require('./auth');
3
+ const { getLocalConfig } = require('./config');
4
+
5
+ const { handleInit } = require('./commands/init');
6
+ const { handleAuth } = require('./commands/auth');
7
+ const { handleProjects } = require('./commands/projects');
8
+ const { handleQuery } = require('./commands/query');
9
+ const { handleSQL } = require('./commands/sql');
10
+ const { handleSummary } = require('./commands/summary');
11
+ const { handlePages, handlePaths, handleCanvas } = require('./commands/pages');
12
+ const { handleHeatmap } = require('./commands/heatmap');
13
+ const { handleDashboards } = require('./commands/dashboards');
14
+ const { handleDoctor } = require('./commands/doctor');
15
+ const { handleContext } = require('./commands/context');
16
+ const { handleMcp } = require('./commands/mcp');
17
+
18
+ const VERSION = '1.2.7';
19
+
20
+ function parseArgs(argv) {
21
+ const flags = {};
22
+ const args = [];
23
+ let i = 0;
24
+
25
+ while (i < argv.length) {
26
+ const arg = argv[i];
27
+ if (arg.startsWith('--')) {
28
+ const equalIndex = arg.indexOf('=');
29
+ if (equalIndex !== -1) {
30
+ const key = arg.slice(2, equalIndex);
31
+ const val = arg.slice(equalIndex + 1);
32
+ if (flags[key]) {
33
+ if (Array.isArray(flags[key])) flags[key].push(val);
34
+ else flags[key] = [flags[key], val];
35
+ } else {
36
+ flags[key] = val;
37
+ }
38
+ } else {
39
+ const key = arg.slice(2);
40
+ const next = argv[i + 1];
41
+ if (next && !next.startsWith('-')) {
42
+ if (flags[key]) {
43
+ if (Array.isArray(flags[key])) flags[key].push(next);
44
+ else flags[key] = [flags[key], next];
45
+ } else {
46
+ flags[key] = next;
47
+ }
48
+ i++;
49
+ } else {
50
+ flags[key] = true;
51
+ }
52
+ }
53
+ } else if (arg.startsWith('-')) {
54
+ const key = arg.slice(1);
55
+ if (key === 'v') flags.version = true;
56
+ else if (key === 'h') flags.help = true;
57
+ else flags[key] = true;
58
+ } else {
59
+ args.push(arg);
60
+ }
61
+ i++;
62
+ }
63
+
64
+ return { args, flags };
65
+ }
66
+
67
+ function printHelp() {
68
+ console.log(`Shiplens CLI - High-Speed User Telemetry & SDK Management Tool (v${VERSION})
69
+
70
+ Usage:
71
+ shiplens <command> [subcommand] [options]
72
+ npx.cmd --yes shiplens-cli <command> [subcommand] [options] (Windows)
73
+ npx --yes shiplens-cli <command> [subcommand] [options] (macOS/Linux)
74
+
75
+ Core Commands:
76
+ init 15-second zero-config analytics initialization & dashboard setup
77
+ auth Authentication & credentials (status, set, secret, whoami, logout, bind, mcp-config, configure)
78
+ projects Project management (list, bind, delete)
79
+ query Multi-dimensional metrics and conversion funnel queries
80
+ sql Execute secure read-only SQL queries on ClickHouse
81
+ summary Retrieve product overview (traffic, bounce rates, geos, devices)
82
+ pages Inspect page-level visits and average dwell times
83
+ paths Analyze user flow journeys and Sankey transition paths
84
+ canvas Retrieve global user behavior canvas topology
85
+ heatmap Retrieve click heatmaps and skeleton wireframes
86
+ dashboards AI dashboard management and one-click creation (list, create [--ai])
87
+ doctor Run full end-to-end diagnostics on SDK, credentials, and network
88
+ context Manage business context dictionary (push, pull, show)
89
+ mcp serve Run local MCP proxy server with device credentials
90
+
91
+ auth Subcommands:
92
+ auth status Check credential validity
93
+ auth set [--secret-stdin] Save Access Secret
94
+ auth secret list List Access Secrets (masked)
95
+ auth secret create [--scopes ...] Create scoped offline Access Secret
96
+ auth secret revoke --key-id <id> Revoke offline Access Secret
97
+ auth whoami Display current authenticated user
98
+ auth logout Clear local credentials
99
+ auth bind --email <email> [--json] Send Magic Link email for account binding
100
+ auth mcp-config [--client <client>] Print MCP configuration (cursor/codex/claude/antigravity/manual)
101
+ auth configure --client <client> Write MCP configuration into target client
102
+ auth configure --client manual Output standard MCP JSON configuration
103
+
104
+ init Dedicated Options:
105
+ --name <string> Project name (default: auto-read from package.json)
106
+ --description <string> Project functional description & positioning
107
+ --genre <id> Level 1 category ID (e.g. utilities, finance, casual_games)
108
+ --subgenre <id> Level 2 subcategory ID (e.g. developer_tools, financial_tools)
109
+ --tags <ids> Level 4 feature tags (comma-separated tag_ids)
110
+ --industry <string> Compatibility industry category field
111
+ --email <email|auto> Auto or manual binding email
112
+
113
+ Global Options:
114
+ --json Output in standard JSON format (required for AI Agents)
115
+ --force Force overwrite existing local configuration and project ID
116
+ --no-commit Skip automatic Git commit after initialization
117
+ --app-id <id> Explicitly specify target project app_id
118
+ --env <env> Target environment (production / staging, default: production)
119
+ --secret <key> Pass Access Secret explicitly
120
+ --api-url <url> Custom backend API base URL (default: http://120.26.230.33)
121
+ -v, --version Display CLI version
122
+ -h, --help Display help information
123
+ `);
124
+ }
125
+
126
+ async function runCLI(argv = process.argv.slice(2)) {
127
+ const { args, flags } = parseArgs(argv);
128
+
129
+ if (flags.version || (args.length === 0 && flags.v)) {
130
+ if (flags.json) {
131
+ console.log(JSON.stringify({ ok: true, version: VERSION }));
132
+ } else {
133
+ console.log(`shiplens v${VERSION}`);
134
+ }
135
+ return;
136
+ }
137
+
138
+ if (flags.help || args.length === 0 || args[0] === 'help') {
139
+ printHelp();
140
+ return;
141
+ }
142
+
143
+ const isJSON = Boolean(flags.json);
144
+
145
+ const output = (data, textFormatter) => {
146
+ if (isJSON) {
147
+ console.log(JSON.stringify(data, null, 2));
148
+ } else if (textFormatter) {
149
+ textFormatter();
150
+ } else {
151
+ console.log(JSON.stringify(data, null, 2));
152
+ }
153
+ };
154
+
155
+ const outputError = (err) => {
156
+ const code = err.code || 'INTERNAL_ERROR';
157
+ if (isJSON) {
158
+ console.log(JSON.stringify({
159
+ ok: false,
160
+ code,
161
+ message: err.message,
162
+ status: err.status,
163
+ }, null, 2));
164
+ } else {
165
+ console.error(`❌ Error [${code}]: ${err.message}`);
166
+ }
167
+ process.exitCode = 1;
168
+ };
169
+
170
+ // Resolve credentials and API client
171
+ const resolvedAuth = resolveSecret(flags.secret);
172
+ let apiUrl = flags['api-url'] || process.env.SHIPLENS_API_URL;
173
+ if (!apiUrl) {
174
+ const localCfg = getLocalConfig();
175
+ if (localCfg && localCfg.api_url) apiUrl = localCfg.api_url;
176
+ }
177
+ if (!apiUrl) apiUrl = 'http://120.26.230.33';
178
+
179
+ const client = new APIClient(apiUrl, resolvedAuth.secret);
180
+
181
+ const resolveAppId = () => {
182
+ if (flags['app-id']) return flags['app-id'];
183
+ const localCfg = getLocalConfig();
184
+ if (localCfg && localCfg.app_id) return localCfg.app_id;
185
+ const err = new Error('Target app_id not found. Provide --app-id or run inside an initialized project directory.');
186
+ err.code = 'APP_NOT_FOUND';
187
+ throw err;
188
+ };
189
+
190
+ const ctx = {
191
+ client,
192
+ resolvedAuth,
193
+ resolveAppId,
194
+ output,
195
+ isJSON,
196
+ };
197
+
198
+ const command = args[0];
199
+ const subcommand = args[1];
200
+ const cmdArgs = args.slice(1);
201
+
202
+ try {
203
+ switch (command) {
204
+ case 'init':
205
+ await handleInit(cmdArgs, flags, ctx);
206
+ break;
207
+ case 'auth':
208
+ await handleAuth(subcommand || 'status', args.slice(2), flags, ctx);
209
+ break;
210
+ case 'projects':
211
+ await handleProjects(subcommand || 'list', args.slice(2), flags, ctx);
212
+ break;
213
+ case 'query':
214
+ await handleQuery(cmdArgs, flags, ctx);
215
+ break;
216
+ case 'sql':
217
+ await handleSQL(cmdArgs, flags, ctx);
218
+ break;
219
+ case 'summary':
220
+ await handleSummary(cmdArgs, flags, ctx);
221
+ break;
222
+ case 'pages':
223
+ await handlePages(cmdArgs, flags, ctx);
224
+ break;
225
+ case 'paths':
226
+ await handlePaths(cmdArgs, flags, ctx);
227
+ break;
228
+ case 'canvas':
229
+ await handleCanvas(cmdArgs, flags, ctx);
230
+ break;
231
+ case 'heatmap':
232
+ await handleHeatmap(cmdArgs, flags, ctx);
233
+ break;
234
+ case 'dashboards':
235
+ await handleDashboards(subcommand || 'list', args.slice(2), flags, ctx);
236
+ break;
237
+ case 'doctor':
238
+ await handleDoctor(cmdArgs, flags, ctx);
239
+ break;
240
+ case 'context':
241
+ await handleContext(subcommand || 'show', args.slice(2), flags, ctx);
242
+ break;
243
+ case 'mcp':
244
+ await handleMcp(subcommand || 'serve');
245
+ break;
246
+ default:
247
+ throw new Error(`Unknown command: ${command}. Use shiplens --help to view supported commands.`);
248
+ }
249
+ } catch (err) {
250
+ outputError(err);
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ runCLI,
256
+ parseArgs,
257
+ VERSION,
258
+ };
@@ -0,0 +1,242 @@
1
+ const readline = require('readline');
2
+ const { getDeviceEnv } = require('../config');
3
+ const { saveDeviceEnv } = require('../device-env');
4
+ const { CLIENTS, getMcpConfig, configureMcpClient } = require('../mcp-config');
5
+ const { saveSecretToGlobal, clearGlobalSecret, maskSecret } = require('../auth');
6
+ const { APIClient } = require('../api');
7
+
8
+ async function handleAuth(subcommand, args, flags, ctx) {
9
+ switch (subcommand) {
10
+ case 'status': {
11
+ const deviceEnv = getDeviceEnv();
12
+ if (deviceEnv) {
13
+ return ctx.output({ ok: true, authenticated: false, device_env: 'present', mcp_url: deviceEnv.SHIPLENS_MCP_URL, next: 'Use shiplens auth mcp-config, then complete OAuth in your Agent.' }, () => {
14
+ console.log(`✅ shiplens.env detected; MCP: ${deviceEnv.SHIPLENS_MCP_URL}`);
15
+ console.log('Complete OAuth login in your Agent before using MCP.');
16
+ });
17
+ }
18
+ if (!ctx.resolvedAuth.is_present) {
19
+ return ctx.output({
20
+ ok: false,
21
+ authenticated: false,
22
+ message: 'No credentials detected. Configure via shiplens auth set.',
23
+ }, () => {
24
+ console.log('⚠️ No valid credentials configured. Run: shiplens auth set');
25
+ });
26
+ }
27
+
28
+ try {
29
+ const meResp = await ctx.client.me();
30
+ ctx.output({
31
+ ok: true,
32
+ authenticated: true,
33
+ source: ctx.resolvedAuth.source,
34
+ masked_secret: ctx.resolvedAuth.masked_secret,
35
+ user_id: meResp.user_id,
36
+ email: meResp.email,
37
+ last_verified_at: new Date().toISOString(),
38
+ }, () => {
39
+ console.log('✅ Authentication valid');
40
+ console.log(`👤 User ID: ${meResp.user_id} | Email: ${meResp.email}`);
41
+ console.log(`🔑 Secret Source: ${ctx.resolvedAuth.source} (${ctx.resolvedAuth.masked_secret})`);
42
+ });
43
+ } catch (err) {
44
+ ctx.output({
45
+ ok: false,
46
+ authenticated: false,
47
+ source: ctx.resolvedAuth.source,
48
+ masked_secret: ctx.resolvedAuth.masked_secret,
49
+ error: err.message,
50
+ }, () => {
51
+ console.log(`❌ Credential verification failed: ${err.message}`);
52
+ });
53
+ }
54
+ break;
55
+ }
56
+
57
+ case 'mcp-config': {
58
+ const deviceEnv = getDeviceEnv();
59
+ if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
60
+ const client = flags.client || 'manual';
61
+ if (!CLIENTS.includes(client)) throw new Error(`Unsupported MCP client: ${client}. Supported: ${CLIENTS.join(', ')}`);
62
+ const config = getMcpConfig(client, deviceEnv.SHIPLENS_MCP_URL);
63
+ ctx.output({ ok: true, mcp_config: config, oauth_required: true }, () => console.log(JSON.stringify(config, null, 2)));
64
+ break;
65
+ }
66
+
67
+ case 'configure': {
68
+ const deviceEnv = getDeviceEnv();
69
+ if (!deviceEnv) throw new Error('shiplens.env not found. Run shiplens auth bind and complete email activation first.');
70
+ const client = flags.client || args[0];
71
+ if (!CLIENTS.includes(client)) throw new Error(`Specify target MCP client via --client: ${CLIENTS.join(', ')}`);
72
+ const result = configureMcpClient(client, deviceEnv.SHIPLENS_MCP_URL);
73
+ ctx.output({ ok: true, client, oauth_required: true, ...result }, () => {
74
+ if (client === 'manual') {
75
+ console.log(JSON.stringify(result.config, null, 2));
76
+ return;
77
+ }
78
+ console.log(result.written ? `✅ Shiplens MCP configuration written to ${client}.` : 'ℹ️ Shiplens MCP config already exists, unchanged.');
79
+ console.log('Complete OAuth login in your Agent MCP panel.');
80
+ });
81
+ break;
82
+ }
83
+
84
+ case 'set': {
85
+ let secret = '';
86
+ if (flags['secret-stdin']) {
87
+ const chunks = [];
88
+ for await (const chunk of process.stdin) {
89
+ chunks.push(chunk);
90
+ }
91
+ secret = Buffer.concat(chunks).toString('utf8').trim();
92
+ } else if (args.length > 0) {
93
+ secret = args[0].trim();
94
+ } else if (flags.secret) {
95
+ secret = flags.secret.trim();
96
+ } else {
97
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
98
+ secret = await new Promise((resolve) => {
99
+ rl.question('Enter Shiplens Access Secret: ', (ans) => {
100
+ rl.close();
101
+ resolve(ans.trim());
102
+ });
103
+ });
104
+ }
105
+
106
+ if (!secret) {
107
+ throw new Error('Access Secret cannot be empty.');
108
+ }
109
+
110
+ const tempClient = new APIClient(ctx.client.baseURL, secret);
111
+ let email = '';
112
+ let userId = '';
113
+ try {
114
+ const meResp = await tempClient.me();
115
+ email = meResp.email || '';
116
+ userId = meResp.user_id || '';
117
+ } catch (e) {}
118
+
119
+ saveSecretToGlobal(secret, email, userId);
120
+
121
+ ctx.output({
122
+ ok: true,
123
+ masked_secret: maskSecret(secret),
124
+ email: email || undefined,
125
+ user_id: userId || undefined,
126
+ message: 'Access Secret saved securely to ~/.shiplens/config.json',
127
+ }, () => {
128
+ console.log(`✅ Access Secret saved: ${maskSecret(secret)}`);
129
+ if (email) console.log(`👤 Bound User: ${email} (${userId})`);
130
+ });
131
+ break;
132
+ }
133
+
134
+ case 'secret': {
135
+ const action = args[0] || 'list';
136
+ if (!ctx.resolvedAuth.is_present) throw new Error('Run auth set first to configure an Access Secret with administrative rights.');
137
+ if (action === 'list') {
138
+ const response = await ctx.client.listAccessSecrets();
139
+ return ctx.output({ ok: true, access_secrets: response.api_keys || [] }, () => console.log(JSON.stringify(response.api_keys || [], null, 2)));
140
+ }
141
+ if (action === 'create') {
142
+ const scopes = flags.scopes ? String(flags.scopes).split(',').map((scope) => scope.trim()).filter(Boolean) : undefined;
143
+ const response = await ctx.client.createAccessSecret({ app_id: flags['app-id'], scopes, expires_at: flags.expires });
144
+ saveSecretToGlobal(response.api_key);
145
+ return ctx.output({ ok: true, api_key_id: response.api_key_id, api_key_preview: response.api_key_preview, app_id: response.app_id, scopes: response.scopes, expires_at: response.expires_at, saved_to_global_config: true }, () => {
146
+ console.log(`✅ Offline Access Secret created and saved (${response.api_key_preview}).`);
147
+ });
148
+ }
149
+ if (action === 'revoke') {
150
+ const apiKeyId = flags['key-id'] || args[1];
151
+ if (!apiKeyId) throw new Error('Provide --key-id <api_key_id>.');
152
+ const response = await ctx.client.revokeAccessSecret(apiKeyId);
153
+ return ctx.output({ ok: true, ...response }, () => console.log(`✅ Revoked Access Secret: ${response.api_key_id}`));
154
+ }
155
+ throw new Error('Supported auth secret subcommands: list, create, revoke');
156
+ }
157
+
158
+ case 'whoami': {
159
+ const meResp = await ctx.client.me();
160
+ ctx.output(meResp, () => {
161
+ console.log(`👤 User ID: ${meResp.user_id}`);
162
+ console.log(`📧 Email: ${meResp.email}`);
163
+ if (meResp.project_count) {
164
+ console.log(`📦 Projects Count: ${meResp.project_count}`);
165
+ }
166
+ });
167
+ break;
168
+ }
169
+
170
+ case 'logout': {
171
+ clearGlobalSecret();
172
+ ctx.output({
173
+ ok: true,
174
+ message: 'Logged out successfully. Local credentials cleared.',
175
+ }, () => {
176
+ console.log('🚪 Logged out successfully. Local credentials cleared.');
177
+ });
178
+ break;
179
+ }
180
+
181
+ case 'bind': {
182
+ const email = flags.email || args[0];
183
+ if (!email || !email.includes('@')) {
184
+ const err = new Error('Provide a valid email address, e.g.: shiplens auth bind --email you@company.com');
185
+ err.code = 'INVALID_EMAIL';
186
+ throw err;
187
+ }
188
+
189
+ const { getLocalConfig } = require('../config');
190
+ const localCfg = getLocalConfig() || {};
191
+ const appId = flags['app-id'] || localCfg.app_id || '';
192
+ const projectName = localCfg.project_name || '';
193
+
194
+ let resp;
195
+ try {
196
+ resp = await ctx.client.startEmailWithRetry({
197
+ email,
198
+ project_id: appId || undefined,
199
+ project_name: projectName || undefined,
200
+ client_name: 'Shiplens CLI',
201
+ device_os: process.platform,
202
+ device_name: require('os').hostname(),
203
+ });
204
+ } catch (err) {
205
+ ctx.output({
206
+ ok: false,
207
+ magic_link_sent: false,
208
+ email,
209
+ app_id: appId || undefined,
210
+ code: err.code || 'NETWORK_FAILED',
211
+ message: err.message,
212
+ }, () => {
213
+ console.error(`❌ Failed to send email [${err.code || 'NETWORK_FAILED'}]: ${err.message}`);
214
+ });
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+
219
+ const envPath = saveDeviceEnv(resp.shiplens_env);
220
+
221
+ ctx.output({
222
+ ok: true,
223
+ magic_link_sent: true,
224
+ email,
225
+ app_id: appId || undefined,
226
+ project_name: projectName || undefined,
227
+ shiplens_env_written: Boolean(envPath),
228
+ message: `Activation email sent to ${email}. Click the link to complete registration.`,
229
+ }, () => {
230
+ console.log(`📧 Activation email sent to: ${email}`);
231
+ if (appId) console.log(`📦 Bound App ID: ${appId}`);
232
+ console.log('👉 Click the link in your email to activate your 50,000 monthly free events quota.');
233
+ });
234
+ break;
235
+ }
236
+
237
+ default:
238
+ throw new Error(`Unknown auth subcommand: ${subcommand}. Supported: status, set, secret, whoami, logout, bind, mcp-config, configure`);
239
+ }
240
+ }
241
+
242
+ module.exports = { handleAuth };