@gopherhole/cli 0.5.0 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.js +849 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +673 -1
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ const brand = {
20
20
  greenDark: chalk_1.default.hex('#16a34a'), // gopher-600 - emphasis
21
21
  };
22
22
  // Version
23
- const VERSION = '0.4.1';
23
+ const VERSION = '0.6.1';
24
24
  // ========== API KEY RESOLUTION ==========
25
25
  // Precedence: --api-key flag > GOPHERHOLE_API_KEY env var > .env file in cwd
26
26
  async function resolveApiKey(flagValue) {
@@ -2577,6 +2577,65 @@ ${chalk_1.default.bold('Examples:')}
2577
2577
  process.exit(1);
2578
2578
  }
2579
2579
  });
2580
+ // ========== REQUEST ACCESS (agent-to-agent via API key) ==========
2581
+ program
2582
+ .command('request-access <agentId>')
2583
+ .description(`Request access to another agent (agent-to-agent, uses API key)
2584
+
2585
+ If the target has auto-approve enabled, access is granted immediately.
2586
+ Otherwise it's queued for manual approval by the target tenant.
2587
+
2588
+ ${chalk_1.default.bold('Examples:')}
2589
+ $ gopherhole request-access agent-abc123
2590
+ $ gopherhole request-access agent-abc123 --reason "Need data feed"
2591
+ $ gopherhole request-access agent-abc123 --scopes "messages:send,memory:read"
2592
+ `)
2593
+ .option('--api-key <key>', 'API key (overrides env / .env)')
2594
+ .option('--scopes <scopes>', 'Comma-separated scopes to request (default: messages:send)')
2595
+ .option('--reason <text>', 'Reason for the request (shown to target tenant)')
2596
+ .action(async (agentId, options) => {
2597
+ const apiKey = await resolveApiKey(options.apiKey);
2598
+ if (!apiKey) {
2599
+ console.error(chalk_1.default.red('No API key found.'));
2600
+ console.error(chalk_1.default.gray('Set GOPHERHOLE_API_KEY, pass --api-key, or run gopherhole init'));
2601
+ process.exit(1);
2602
+ }
2603
+ const scopes = options.scopes
2604
+ ? options.scopes.split(',').map((s) => s.trim())
2605
+ : ['messages:send'];
2606
+ const spinner = (0, ora_1.default)(`Requesting access to ${agentId}...`).start();
2607
+ try {
2608
+ const res = await fetch(`${API_URL}/access/request`, {
2609
+ method: 'POST',
2610
+ headers: {
2611
+ 'Content-Type': 'application/json',
2612
+ Authorization: `Bearer ${apiKey}`,
2613
+ },
2614
+ body: JSON.stringify({
2615
+ targetAgentId: agentId,
2616
+ scopes,
2617
+ reason: options.reason,
2618
+ }),
2619
+ });
2620
+ if (!res.ok) {
2621
+ const err = await res.json().catch(() => ({}));
2622
+ throw new Error(err.error || `HTTP ${res.status}`);
2623
+ }
2624
+ const data = await res.json();
2625
+ if (data.grant.status === 'approved') {
2626
+ spinner.succeed(`Access granted immediately (auto-approved). You can now message ${chalk_1.default.cyan(agentId)}.`);
2627
+ }
2628
+ else {
2629
+ spinner.succeed(`Access request submitted (pending approval).`);
2630
+ }
2631
+ console.log(` Grant ID: ${chalk_1.default.cyan(data.grant.id)}`);
2632
+ console.log(` Status: ${data.grant.status}`);
2633
+ }
2634
+ catch (err) {
2635
+ spinner.fail(chalk_1.default.red(err.message));
2636
+ process.exit(1);
2637
+ }
2638
+ });
2580
2639
  // ========== MEMORY COMMANDS (agent-to-agent via memory agent) ==========
2581
2640
  const MEMORY_AGENT = process.env.GOPHERHOLE_MEMORY_AGENT || 'agent-memory-official';
2582
2641
  const memory = program
@@ -2950,5 +3009,794 @@ wsMembers
2950
3009
  process.exit(1);
2951
3010
  }
2952
3011
  });
3012
+ // ========== KEYS COMMAND ==========
3013
+ const keys = program
3014
+ .command('keys')
3015
+ .description(`Manage API keys
3016
+
3017
+ ${chalk_1.default.bold('Examples:')}
3018
+ $ gopherhole keys list
3019
+ $ gopherhole keys create --name "prod" --agent agent-abc123
3020
+ $ gopherhole keys delete key-abc123
3021
+ `);
3022
+ keys
3023
+ .command('list')
3024
+ .description('List all API keys on the tenant')
3025
+ .option('--json', 'Output as JSON')
3026
+ .action(async (options) => {
3027
+ const sessionId = config.get('sessionId');
3028
+ if (!sessionId) {
3029
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3030
+ process.exit(1);
3031
+ }
3032
+ const spinner = (0, ora_1.default)('Fetching API keys...').start();
3033
+ try {
3034
+ const res = await fetch(`${API_URL}/api-keys`, { headers: { 'X-Session-ID': sessionId } });
3035
+ if (!res.ok)
3036
+ throw new Error('Failed to fetch API keys');
3037
+ const data = await res.json();
3038
+ const keysList = Array.isArray(data) ? data : data.keys || [];
3039
+ spinner.stop();
3040
+ if (options.json) {
3041
+ console.log(JSON.stringify(keysList, null, 2));
3042
+ return;
3043
+ }
3044
+ if (keysList.length === 0) {
3045
+ console.log(chalk_1.default.yellow('\nNo API keys found.\n'));
3046
+ return;
3047
+ }
3048
+ console.log(chalk_1.default.bold('\nAPI Keys:\n'));
3049
+ for (const k of keysList) {
3050
+ console.log(` ${chalk_1.default.cyan(k.id)} — ${k.name || 'unnamed'}`);
3051
+ console.log(` Agent: ${k.agentId || k.agent_id || 'none'} | Prefix: ${chalk_1.default.gray(k.prefix || '???')} | Scopes: ${k.scopes?.join(', ') || 'default'}`);
3052
+ console.log('');
3053
+ }
3054
+ }
3055
+ catch (err) {
3056
+ spinner.fail(chalk_1.default.red(err.message));
3057
+ process.exit(1);
3058
+ }
3059
+ });
3060
+ keys
3061
+ .command('create')
3062
+ .description('Create a new API key')
3063
+ .requiredOption('--name <name>', 'Human-readable label for this key')
3064
+ .requiredOption('--agent <agentId>', 'The agent this key authenticates as')
3065
+ .option('--scopes <scopes>', 'Comma-separated scopes (e.g., "messages:send,memory:read")')
3066
+ .action(async (options) => {
3067
+ const sessionId = config.get('sessionId');
3068
+ if (!sessionId) {
3069
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3070
+ process.exit(1);
3071
+ }
3072
+ const spinner = (0, ora_1.default)('Creating API key...').start();
3073
+ try {
3074
+ const body = { name: options.name, agentId: options.agent };
3075
+ if (options.scopes)
3076
+ body.scopes = options.scopes.split(',').map((s) => s.trim());
3077
+ const res = await fetch(`${API_URL}/api-keys`, {
3078
+ method: 'POST',
3079
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3080
+ body: JSON.stringify(body),
3081
+ });
3082
+ if (!res.ok) {
3083
+ const err = await res.json();
3084
+ throw new Error(err.error || 'Failed to create key');
3085
+ }
3086
+ const data = await res.json();
3087
+ spinner.succeed('API key created!');
3088
+ console.log('');
3089
+ console.log(` ID: ${chalk_1.default.cyan(data.id)}`);
3090
+ console.log(` Key: ${brand.green(data.key || data.secret)}`);
3091
+ console.log('');
3092
+ console.log(chalk_1.default.yellow(' Store this key securely — it will not be shown again.'));
3093
+ console.log('');
3094
+ }
3095
+ catch (err) {
3096
+ spinner.fail(chalk_1.default.red(err.message));
3097
+ process.exit(1);
3098
+ }
3099
+ });
3100
+ keys
3101
+ .command('delete <keyId>')
3102
+ .description('Revoke and delete an API key')
3103
+ .option('-y, --yes', 'Skip confirmation')
3104
+ .action(async (keyId, options) => {
3105
+ const sessionId = config.get('sessionId');
3106
+ if (!sessionId) {
3107
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3108
+ process.exit(1);
3109
+ }
3110
+ if (!options.yes) {
3111
+ const { confirm } = await inquirer_1.default.prompt([{
3112
+ type: 'confirm', name: 'confirm', message: `Permanently revoke key ${keyId}?`, default: false,
3113
+ }]);
3114
+ if (!confirm) {
3115
+ console.log(chalk_1.default.gray('Cancelled.'));
3116
+ return;
3117
+ }
3118
+ }
3119
+ const spinner = (0, ora_1.default)('Revoking key...').start();
3120
+ try {
3121
+ const res = await fetch(`${API_URL}/api-keys/${keyId}`, {
3122
+ method: 'DELETE',
3123
+ headers: { 'X-Session-ID': sessionId },
3124
+ });
3125
+ if (!res.ok) {
3126
+ const err = await res.json();
3127
+ throw new Error(err.error || 'Failed to delete key');
3128
+ }
3129
+ spinner.succeed(`Key ${chalk_1.default.cyan(keyId)} revoked and deleted.`);
3130
+ }
3131
+ catch (err) {
3132
+ spinner.fail(chalk_1.default.red(err.message));
3133
+ process.exit(1);
3134
+ }
3135
+ });
3136
+ // ========== TEAM COMMAND ==========
3137
+ const team = program
3138
+ .command('team')
3139
+ .description(`Manage team members
3140
+
3141
+ ${chalk_1.default.bold('Examples:')}
3142
+ $ gopherhole team list
3143
+ $ gopherhole team invite user@example.com --role admin
3144
+ $ gopherhole team remove member-abc123
3145
+ `);
3146
+ team
3147
+ .command('list')
3148
+ .description('List team members')
3149
+ .option('--json', 'Output as JSON')
3150
+ .action(async (options) => {
3151
+ const sessionId = config.get('sessionId');
3152
+ if (!sessionId) {
3153
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3154
+ process.exit(1);
3155
+ }
3156
+ const spinner = (0, ora_1.default)('Fetching team...').start();
3157
+ try {
3158
+ const res = await fetch(`${API_URL}/team/members`, { headers: { 'X-Session-ID': sessionId } });
3159
+ if (!res.ok)
3160
+ throw new Error('Failed to fetch team');
3161
+ const data = await res.json();
3162
+ const members = Array.isArray(data) ? data : data.members || [];
3163
+ spinner.stop();
3164
+ if (options.json) {
3165
+ console.log(JSON.stringify(members, null, 2));
3166
+ return;
3167
+ }
3168
+ if (members.length === 0) {
3169
+ console.log(chalk_1.default.yellow('\nNo team members.\n'));
3170
+ return;
3171
+ }
3172
+ console.log(chalk_1.default.bold('\nTeam Members:\n'));
3173
+ for (const m of members) {
3174
+ const role = m.role === 'admin' ? chalk_1.default.red(m.role) : chalk_1.default.gray(m.role);
3175
+ console.log(` ${chalk_1.default.bold(m.name || m.email)} (${chalk_1.default.cyan(m.id)})`);
3176
+ console.log(` Role: ${role}${m.joinedAt ? ` | Joined: ${m.joinedAt}` : ''}`);
3177
+ console.log('');
3178
+ }
3179
+ }
3180
+ catch (err) {
3181
+ spinner.fail(chalk_1.default.red(err.message));
3182
+ process.exit(1);
3183
+ }
3184
+ });
3185
+ team
3186
+ .command('invite <email>')
3187
+ .description('Invite someone to your tenant')
3188
+ .option('--role <role>', 'Role: admin, member, viewer (default: member)')
3189
+ .action(async (email, options) => {
3190
+ const sessionId = config.get('sessionId');
3191
+ if (!sessionId) {
3192
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3193
+ process.exit(1);
3194
+ }
3195
+ const spinner = (0, ora_1.default)(`Sending invite to ${email}...`).start();
3196
+ try {
3197
+ const body = { email };
3198
+ if (options.role)
3199
+ body.role = options.role;
3200
+ const res = await fetch(`${API_URL}/team/invites`, {
3201
+ method: 'POST',
3202
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3203
+ body: JSON.stringify(body),
3204
+ });
3205
+ if (!res.ok) {
3206
+ const err = await res.json();
3207
+ throw new Error(err.error || 'Failed to send invite');
3208
+ }
3209
+ spinner.succeed(`Invitation sent to ${chalk_1.default.cyan(email)} with role: ${options.role || 'member'}`);
3210
+ }
3211
+ catch (err) {
3212
+ spinner.fail(chalk_1.default.red(err.message));
3213
+ process.exit(1);
3214
+ }
3215
+ });
3216
+ team
3217
+ .command('remove <memberId>')
3218
+ .description('Remove a team member')
3219
+ .option('-y, --yes', 'Skip confirmation')
3220
+ .action(async (memberId, options) => {
3221
+ const sessionId = config.get('sessionId');
3222
+ if (!sessionId) {
3223
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3224
+ process.exit(1);
3225
+ }
3226
+ if (!options.yes) {
3227
+ const { confirm } = await inquirer_1.default.prompt([{
3228
+ type: 'confirm', name: 'confirm', message: `Remove team member ${memberId}?`, default: false,
3229
+ }]);
3230
+ if (!confirm) {
3231
+ console.log(chalk_1.default.gray('Cancelled.'));
3232
+ return;
3233
+ }
3234
+ }
3235
+ const spinner = (0, ora_1.default)('Removing member...').start();
3236
+ try {
3237
+ const res = await fetch(`${API_URL}/team/members/${memberId}`, {
3238
+ method: 'DELETE',
3239
+ headers: { 'X-Session-ID': sessionId },
3240
+ });
3241
+ if (!res.ok) {
3242
+ const err = await res.json();
3243
+ throw new Error(err.error || 'Failed to remove member');
3244
+ }
3245
+ spinner.succeed(`Team member ${chalk_1.default.cyan(memberId)} removed.`);
3246
+ }
3247
+ catch (err) {
3248
+ spinner.fail(chalk_1.default.red(err.message));
3249
+ process.exit(1);
3250
+ }
3251
+ });
3252
+ // ========== USAGE COMMAND ==========
3253
+ const usage = program
3254
+ .command('usage')
3255
+ .description(`View usage statistics
3256
+
3257
+ ${chalk_1.default.bold('Examples:')}
3258
+ $ gopherhole usage summary
3259
+ $ gopherhole usage summary --period week
3260
+ $ gopherhole usage agents
3261
+ `);
3262
+ usage
3263
+ .command('summary')
3264
+ .description('Get high-level usage overview')
3265
+ .option('--period <period>', 'Time window: day, week, month (default: month)')
3266
+ .option('--json', 'Output as JSON')
3267
+ .action(async (options) => {
3268
+ const sessionId = config.get('sessionId');
3269
+ if (!sessionId) {
3270
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3271
+ process.exit(1);
3272
+ }
3273
+ const spinner = (0, ora_1.default)('Fetching usage...').start();
3274
+ try {
3275
+ const params = new URLSearchParams();
3276
+ if (options.period)
3277
+ params.set('period', options.period);
3278
+ const qs = params.toString() ? `?${params}` : '';
3279
+ const res = await fetch(`${API_URL}/usage/summary${qs}`, { headers: { 'X-Session-ID': sessionId } });
3280
+ if (!res.ok)
3281
+ throw new Error('Failed to fetch usage');
3282
+ const data = await res.json();
3283
+ spinner.stop();
3284
+ if (options.json) {
3285
+ console.log(JSON.stringify(data, null, 2));
3286
+ return;
3287
+ }
3288
+ console.log(chalk_1.default.bold(`\nUsage Summary (${options.period || 'month'}):\n`));
3289
+ const d = data;
3290
+ if (d.messagesSent !== undefined)
3291
+ console.log(` Messages Sent: ${brand.green(d.messagesSent)}`);
3292
+ if (d.messagesReceived !== undefined)
3293
+ console.log(` Messages Received: ${brand.green(d.messagesReceived)}`);
3294
+ if (d.tasksCreated !== undefined)
3295
+ console.log(` Tasks Created: ${brand.green(d.tasksCreated)}`);
3296
+ if (d.connections !== undefined)
3297
+ console.log(` Connections: ${brand.green(d.connections)}`);
3298
+ if (d.creditsUsed !== undefined)
3299
+ console.log(` Credits Used: ${brand.green(d.creditsUsed)}`);
3300
+ console.log('');
3301
+ }
3302
+ catch (err) {
3303
+ spinner.fail(chalk_1.default.red(err.message));
3304
+ process.exit(1);
3305
+ }
3306
+ });
3307
+ usage
3308
+ .command('agents')
3309
+ .description('Per-agent usage breakdown')
3310
+ .option('--period <period>', 'Time window: day, week, month (default: month)')
3311
+ .option('--limit <n>', 'Max agents to show (default: 20)')
3312
+ .option('--json', 'Output as JSON')
3313
+ .action(async (options) => {
3314
+ const sessionId = config.get('sessionId');
3315
+ if (!sessionId) {
3316
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3317
+ process.exit(1);
3318
+ }
3319
+ const spinner = (0, ora_1.default)('Fetching per-agent usage...').start();
3320
+ try {
3321
+ const params = new URLSearchParams();
3322
+ if (options.period)
3323
+ params.set('period', options.period);
3324
+ if (options.limit)
3325
+ params.set('limit', options.limit);
3326
+ const qs = params.toString() ? `?${params}` : '';
3327
+ const res = await fetch(`${API_URL}/usage/agents${qs}`, { headers: { 'X-Session-ID': sessionId } });
3328
+ if (!res.ok)
3329
+ throw new Error('Failed to fetch agent usage');
3330
+ const data = await res.json();
3331
+ spinner.stop();
3332
+ if (options.json) {
3333
+ console.log(JSON.stringify(data, null, 2));
3334
+ return;
3335
+ }
3336
+ const agents = Array.isArray(data) ? data : data.agents || [];
3337
+ if (agents.length === 0) {
3338
+ console.log(chalk_1.default.yellow('\nNo usage data.\n'));
3339
+ return;
3340
+ }
3341
+ console.log(chalk_1.default.bold(`\nPer-Agent Usage (${options.period || 'month'}):\n`));
3342
+ for (const a of agents) {
3343
+ console.log(` ${chalk_1.default.bold(a.name || a.agentId)} (${chalk_1.default.cyan(a.agentId || a.id)})`);
3344
+ if (a.messages !== undefined)
3345
+ console.log(` Messages: ${a.messages}`);
3346
+ if (a.tasks !== undefined)
3347
+ console.log(` Tasks: ${a.tasks}`);
3348
+ if (a.credits !== undefined)
3349
+ console.log(` Credits: ${a.credits}`);
3350
+ console.log('');
3351
+ }
3352
+ }
3353
+ catch (err) {
3354
+ spinner.fail(chalk_1.default.red(err.message));
3355
+ process.exit(1);
3356
+ }
3357
+ });
3358
+ // ========== WEBHOOKS COMMAND ==========
3359
+ const webhooks = program
3360
+ .command('webhooks')
3361
+ .description(`Manage webhooks
3362
+
3363
+ ${chalk_1.default.bold('Examples:')}
3364
+ $ gopherhole webhooks list
3365
+ $ gopherhole webhooks create --url https://example.com/hook --events message.received,task.completed
3366
+ $ gopherhole webhooks delete wh-abc123
3367
+ $ gopherhole webhooks test wh-abc123
3368
+ `);
3369
+ webhooks
3370
+ .command('list')
3371
+ .description('List configured webhooks')
3372
+ .option('--json', 'Output as JSON')
3373
+ .action(async (options) => {
3374
+ const sessionId = config.get('sessionId');
3375
+ if (!sessionId) {
3376
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3377
+ process.exit(1);
3378
+ }
3379
+ const spinner = (0, ora_1.default)('Fetching webhooks...').start();
3380
+ try {
3381
+ const res = await fetch(`${API_URL}/webhooks`, { headers: { 'X-Session-ID': sessionId } });
3382
+ if (!res.ok)
3383
+ throw new Error('Failed to fetch webhooks');
3384
+ const data = await res.json();
3385
+ const hooks = Array.isArray(data) ? data : data.webhooks || [];
3386
+ spinner.stop();
3387
+ if (options.json) {
3388
+ console.log(JSON.stringify(hooks, null, 2));
3389
+ return;
3390
+ }
3391
+ if (hooks.length === 0) {
3392
+ console.log(chalk_1.default.yellow('\nNo webhooks configured.\n'));
3393
+ return;
3394
+ }
3395
+ console.log(chalk_1.default.bold('\nWebhooks:\n'));
3396
+ for (const h of hooks) {
3397
+ console.log(` ${chalk_1.default.cyan(h.id)} — ${h.url}`);
3398
+ console.log(` Events: ${h.events?.join(', ') || 'all'}${h.description ? ` | ${chalk_1.default.gray(h.description)}` : ''}`);
3399
+ console.log('');
3400
+ }
3401
+ }
3402
+ catch (err) {
3403
+ spinner.fail(chalk_1.default.red(err.message));
3404
+ process.exit(1);
3405
+ }
3406
+ });
3407
+ webhooks
3408
+ .command('create')
3409
+ .description('Create a new webhook')
3410
+ .requiredOption('--url <url>', 'HTTPS URL to deliver events to')
3411
+ .requiredOption('--events <events>', 'Comma-separated event types (e.g., "message.received,task.completed")')
3412
+ .option('--description <text>', 'Label for this webhook')
3413
+ .option('--secret <secret>', 'Signing secret for HMAC-SHA256 verification')
3414
+ .action(async (options) => {
3415
+ const sessionId = config.get('sessionId');
3416
+ if (!sessionId) {
3417
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3418
+ process.exit(1);
3419
+ }
3420
+ const spinner = (0, ora_1.default)('Creating webhook...').start();
3421
+ try {
3422
+ const body = {
3423
+ url: options.url,
3424
+ events: options.events.split(',').map((e) => e.trim()),
3425
+ };
3426
+ if (options.description)
3427
+ body.description = options.description;
3428
+ if (options.secret)
3429
+ body.secret = options.secret;
3430
+ const res = await fetch(`${API_URL}/webhooks`, {
3431
+ method: 'POST',
3432
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3433
+ body: JSON.stringify(body),
3434
+ });
3435
+ if (!res.ok) {
3436
+ const err = await res.json();
3437
+ throw new Error(err.error || 'Failed to create webhook');
3438
+ }
3439
+ const data = await res.json();
3440
+ spinner.succeed('Webhook created!');
3441
+ console.log(`\n ID: ${chalk_1.default.cyan(data.id)}`);
3442
+ console.log(` URL: ${data.url || options.url}`);
3443
+ console.log(` Events: ${body.events.join(', ')}\n`);
3444
+ }
3445
+ catch (err) {
3446
+ spinner.fail(chalk_1.default.red(err.message));
3447
+ process.exit(1);
3448
+ }
3449
+ });
3450
+ webhooks
3451
+ .command('delete <webhookId>')
3452
+ .description('Delete a webhook')
3453
+ .option('-y, --yes', 'Skip confirmation')
3454
+ .action(async (webhookId, options) => {
3455
+ const sessionId = config.get('sessionId');
3456
+ if (!sessionId) {
3457
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3458
+ process.exit(1);
3459
+ }
3460
+ if (!options.yes) {
3461
+ const { confirm } = await inquirer_1.default.prompt([{
3462
+ type: 'confirm', name: 'confirm', message: `Delete webhook ${webhookId}?`, default: false,
3463
+ }]);
3464
+ if (!confirm) {
3465
+ console.log(chalk_1.default.gray('Cancelled.'));
3466
+ return;
3467
+ }
3468
+ }
3469
+ const spinner = (0, ora_1.default)('Deleting webhook...').start();
3470
+ try {
3471
+ const res = await fetch(`${API_URL}/webhooks/${webhookId}`, {
3472
+ method: 'DELETE',
3473
+ headers: { 'X-Session-ID': sessionId },
3474
+ });
3475
+ if (!res.ok) {
3476
+ const err = await res.json();
3477
+ throw new Error(err.error || 'Failed to delete webhook');
3478
+ }
3479
+ spinner.succeed(`Webhook ${chalk_1.default.cyan(webhookId)} deleted.`);
3480
+ }
3481
+ catch (err) {
3482
+ spinner.fail(chalk_1.default.red(err.message));
3483
+ process.exit(1);
3484
+ }
3485
+ });
3486
+ webhooks
3487
+ .command('test <webhookId>')
3488
+ .description('Send a test event to a webhook')
3489
+ .action(async (webhookId) => {
3490
+ const sessionId = config.get('sessionId');
3491
+ if (!sessionId) {
3492
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3493
+ process.exit(1);
3494
+ }
3495
+ const spinner = (0, ora_1.default)('Sending test event...').start();
3496
+ try {
3497
+ const res = await fetch(`${API_URL}/webhooks/${webhookId}/test`, {
3498
+ method: 'POST',
3499
+ headers: { 'X-Session-ID': sessionId },
3500
+ });
3501
+ if (!res.ok) {
3502
+ const err = await res.json();
3503
+ throw new Error(err.error || 'Failed to test webhook');
3504
+ }
3505
+ const data = await res.json();
3506
+ spinner.succeed(`Test event sent. Response: ${data.status || data.statusCode || 'delivered'}`);
3507
+ }
3508
+ catch (err) {
3509
+ spinner.fail(chalk_1.default.red(err.message));
3510
+ process.exit(1);
3511
+ }
3512
+ });
3513
+ // ========== TENANT COMMAND ==========
3514
+ const tenant = program
3515
+ .command('tenant')
3516
+ .description(`View and manage tenant settings
3517
+
3518
+ ${chalk_1.default.bold('Examples:')}
3519
+ $ gopherhole tenant settings
3520
+ $ gopherhole tenant update --name "Acme Corp" --slug acme
3521
+ `);
3522
+ tenant
3523
+ .command('settings')
3524
+ .description('View current tenant settings')
3525
+ .option('--json', 'Output as JSON')
3526
+ .action(async (options) => {
3527
+ const sessionId = config.get('sessionId');
3528
+ if (!sessionId) {
3529
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3530
+ process.exit(1);
3531
+ }
3532
+ const spinner = (0, ora_1.default)('Fetching tenant settings...').start();
3533
+ try {
3534
+ const res = await fetch(`${API_URL}/auth/tenant/settings`, { headers: { 'X-Session-ID': sessionId } });
3535
+ if (!res.ok)
3536
+ throw new Error('Failed to fetch tenant settings');
3537
+ const data = await res.json();
3538
+ spinner.stop();
3539
+ if (options.json) {
3540
+ console.log(JSON.stringify(data, null, 2));
3541
+ return;
3542
+ }
3543
+ console.log(chalk_1.default.bold('\nTenant Settings:\n'));
3544
+ if (data.name)
3545
+ console.log(` Name: ${brand.green(data.name)}`);
3546
+ if (data.slug)
3547
+ console.log(` Slug: ${chalk_1.default.cyan(data.slug)}`);
3548
+ if (data.plan)
3549
+ console.log(` Plan: ${data.plan}`);
3550
+ if (data.email)
3551
+ console.log(` Email: ${data.email}`);
3552
+ if (data.id)
3553
+ console.log(` ID: ${chalk_1.default.gray(data.id)}`);
3554
+ console.log('');
3555
+ }
3556
+ catch (err) {
3557
+ spinner.fail(chalk_1.default.red(err.message));
3558
+ process.exit(1);
3559
+ }
3560
+ });
3561
+ tenant
3562
+ .command('update')
3563
+ .description('Update tenant name or slug')
3564
+ .option('--name <name>', 'New display name')
3565
+ .option('--slug <slug>', 'New URL-safe slug')
3566
+ .action(async (options) => {
3567
+ const sessionId = config.get('sessionId');
3568
+ if (!sessionId) {
3569
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3570
+ process.exit(1);
3571
+ }
3572
+ const body = {};
3573
+ if (options.name)
3574
+ body.name = options.name;
3575
+ if (options.slug)
3576
+ body.slug = options.slug;
3577
+ if (Object.keys(body).length === 0) {
3578
+ console.log(chalk_1.default.yellow('No changes specified. Use --name or --slug.'));
3579
+ return;
3580
+ }
3581
+ const spinner = (0, ora_1.default)('Updating tenant...').start();
3582
+ try {
3583
+ const res = await fetch(`${API_URL}/auth/tenant`, {
3584
+ method: 'PATCH',
3585
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3586
+ body: JSON.stringify(body),
3587
+ });
3588
+ if (!res.ok) {
3589
+ const err = await res.json();
3590
+ throw new Error(err.error || 'Failed to update tenant');
3591
+ }
3592
+ spinner.succeed('Tenant updated.');
3593
+ if (body.name)
3594
+ console.log(` Name: ${brand.green(body.name)}`);
3595
+ if (body.slug)
3596
+ console.log(` Slug: ${chalk_1.default.cyan(body.slug)}`);
3597
+ }
3598
+ catch (err) {
3599
+ spinner.fail(chalk_1.default.red(err.message));
3600
+ process.exit(1);
3601
+ }
3602
+ });
3603
+ // ========== PROFILE COMMAND ==========
3604
+ program
3605
+ .command('profile')
3606
+ .description(`Update your user profile
3607
+
3608
+ ${chalk_1.default.bold('Examples:')}
3609
+ $ gopherhole profile --name "Jane Smith"
3610
+ $ gopherhole profile --avatar https://example.com/photo.jpg
3611
+ `)
3612
+ .option('--name <name>', 'Display name')
3613
+ .option('--avatar <url>', 'Avatar image URL')
3614
+ .action(async (options) => {
3615
+ const sessionId = config.get('sessionId');
3616
+ if (!sessionId) {
3617
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3618
+ process.exit(1);
3619
+ }
3620
+ const body = {};
3621
+ if (options.name)
3622
+ body.name = options.name;
3623
+ if (options.avatar)
3624
+ body.avatarUrl = options.avatar;
3625
+ if (Object.keys(body).length === 0) {
3626
+ console.log(chalk_1.default.yellow('No changes specified. Use --name or --avatar.'));
3627
+ return;
3628
+ }
3629
+ const spinner = (0, ora_1.default)('Updating profile...').start();
3630
+ try {
3631
+ const res = await fetch(`${API_URL}/auth/me`, {
3632
+ method: 'PATCH',
3633
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3634
+ body: JSON.stringify(body),
3635
+ });
3636
+ if (!res.ok) {
3637
+ const err = await res.json();
3638
+ throw new Error(err.error || 'Failed to update profile');
3639
+ }
3640
+ spinner.succeed('Profile updated.');
3641
+ if (body.name)
3642
+ console.log(` Name: ${brand.green(body.name)}`);
3643
+ if (body.avatarUrl)
3644
+ console.log(` Avatar: updated`);
3645
+ }
3646
+ catch (err) {
3647
+ spinner.fail(chalk_1.default.red(err.message));
3648
+ process.exit(1);
3649
+ }
3650
+ });
3651
+ // ========== SECRETS COMMAND (workspace secrets) ==========
3652
+ const secrets = program
3653
+ .command('secrets')
3654
+ .description(`Manage workspace secrets
3655
+
3656
+ ${chalk_1.default.bold('Examples:')}
3657
+ $ gopherhole secrets list ws-abc123
3658
+ $ gopherhole secrets set ws-abc123 OPENAI_API_KEY sk-abc...
3659
+ $ gopherhole secrets delete ws-abc123 OPENAI_API_KEY
3660
+ `);
3661
+ secrets
3662
+ .command('list <workspaceId>')
3663
+ .description('List secret keys in a workspace (values are never shown)')
3664
+ .option('--json', 'Output as JSON')
3665
+ .action(async (workspaceId, options) => {
3666
+ const sessionId = config.get('sessionId');
3667
+ if (!sessionId) {
3668
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3669
+ process.exit(1);
3670
+ }
3671
+ const spinner = (0, ora_1.default)('Fetching secrets...').start();
3672
+ try {
3673
+ const res = await fetch(`${API_URL}/workspaces/${workspaceId}/secrets`, { headers: { 'X-Session-ID': sessionId } });
3674
+ if (!res.ok)
3675
+ throw new Error('Failed to fetch secrets');
3676
+ const data = await res.json();
3677
+ const secretsList = Array.isArray(data) ? data : data.secrets || [];
3678
+ spinner.stop();
3679
+ if (options.json) {
3680
+ console.log(JSON.stringify(secretsList, null, 2));
3681
+ return;
3682
+ }
3683
+ if (secretsList.length === 0) {
3684
+ console.log(chalk_1.default.yellow('\nNo secrets in this workspace.\n'));
3685
+ return;
3686
+ }
3687
+ console.log(chalk_1.default.bold('\nWorkspace Secrets:\n'));
3688
+ for (const s of secretsList) {
3689
+ console.log(` ${chalk_1.default.cyan(s.key || s.name)}`);
3690
+ }
3691
+ console.log('');
3692
+ }
3693
+ catch (err) {
3694
+ spinner.fail(chalk_1.default.red(err.message));
3695
+ process.exit(1);
3696
+ }
3697
+ });
3698
+ secrets
3699
+ .command('set <workspaceId> <key> <value>')
3700
+ .description('Create or update a workspace secret')
3701
+ .action(async (workspaceId, key, value) => {
3702
+ const sessionId = config.get('sessionId');
3703
+ if (!sessionId) {
3704
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3705
+ process.exit(1);
3706
+ }
3707
+ const spinner = (0, ora_1.default)(`Setting secret ${key}...`).start();
3708
+ try {
3709
+ const res = await fetch(`${API_URL}/workspaces/${workspaceId}/secrets`, {
3710
+ method: 'POST',
3711
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3712
+ body: JSON.stringify({ key, value }),
3713
+ });
3714
+ if (!res.ok) {
3715
+ const err = await res.json();
3716
+ throw new Error(err.error || 'Failed to set secret');
3717
+ }
3718
+ spinner.succeed(`Secret ${chalk_1.default.cyan(key)} stored.`);
3719
+ }
3720
+ catch (err) {
3721
+ spinner.fail(chalk_1.default.red(err.message));
3722
+ process.exit(1);
3723
+ }
3724
+ });
3725
+ secrets
3726
+ .command('delete <workspaceId> <key>')
3727
+ .description('Delete a workspace secret')
3728
+ .option('-y, --yes', 'Skip confirmation')
3729
+ .action(async (workspaceId, key, options) => {
3730
+ const sessionId = config.get('sessionId');
3731
+ if (!sessionId) {
3732
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3733
+ process.exit(1);
3734
+ }
3735
+ if (!options.yes) {
3736
+ const { confirm } = await inquirer_1.default.prompt([{
3737
+ type: 'confirm', name: 'confirm', message: `Delete secret ${key}?`, default: false,
3738
+ }]);
3739
+ if (!confirm) {
3740
+ console.log(chalk_1.default.gray('Cancelled.'));
3741
+ return;
3742
+ }
3743
+ }
3744
+ const spinner = (0, ora_1.default)(`Deleting secret ${key}...`).start();
3745
+ try {
3746
+ const res = await fetch(`${API_URL}/workspaces/${workspaceId}/secrets/${key}`, {
3747
+ method: 'DELETE',
3748
+ headers: { 'X-Session-ID': sessionId },
3749
+ });
3750
+ if (!res.ok) {
3751
+ const err = await res.json();
3752
+ throw new Error(err.error || 'Failed to delete secret');
3753
+ }
3754
+ spinner.succeed(`Secret ${chalk_1.default.cyan(key)} deleted.`);
3755
+ }
3756
+ catch (err) {
3757
+ spinner.fail(chalk_1.default.red(err.message));
3758
+ process.exit(1);
3759
+ }
3760
+ });
3761
+ // ========== CREDITS COMMAND ==========
3762
+ program
3763
+ .command('credits')
3764
+ .description(`View credit balance
3765
+
3766
+ ${chalk_1.default.bold('Examples:')}
3767
+ $ gopherhole credits
3768
+ `)
3769
+ .option('--json', 'Output as JSON')
3770
+ .action(async (options) => {
3771
+ const sessionId = config.get('sessionId');
3772
+ if (!sessionId) {
3773
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3774
+ process.exit(1);
3775
+ }
3776
+ const spinner = (0, ora_1.default)('Fetching balance...').start();
3777
+ try {
3778
+ const res = await fetch(`${API_URL}/credits/balance`, { headers: { 'X-Session-ID': sessionId } });
3779
+ if (!res.ok)
3780
+ throw new Error('Failed to fetch credits');
3781
+ const data = await res.json();
3782
+ spinner.stop();
3783
+ if (options.json) {
3784
+ console.log(JSON.stringify(data, null, 2));
3785
+ return;
3786
+ }
3787
+ console.log(chalk_1.default.bold('\nCredit Balance:\n'));
3788
+ if (data.remaining !== undefined)
3789
+ console.log(` Remaining: ${brand.green(data.remaining)}`);
3790
+ if (data.total !== undefined)
3791
+ console.log(` Total: ${data.total}`);
3792
+ if (data.used !== undefined)
3793
+ console.log(` Used: ${data.used}`);
3794
+ console.log('');
3795
+ }
3796
+ catch (err) {
3797
+ spinner.fail(chalk_1.default.red(err.message));
3798
+ process.exit(1);
3799
+ }
3800
+ });
2953
3801
  // Parse and run
2954
3802
  program.parse();