@gopherhole/cli 0.4.1 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.js +855 -15
  2. package/package.json +1 -1
  3. package/src/index.ts +677 -15
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.0';
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) {
@@ -871,6 +871,9 @@ ${chalk_1.default.bold('Examples:')}
871
871
  $ gopherhole agents config agent-abc123 --no-auto-approve
872
872
  $ gopherhole agents config agent-abc123 --price 0.01 --price-unit request
873
873
  $ gopherhole agents config agent-abc123 --visibility public
874
+ $ gopherhole agents config agent-abc123 --alias support
875
+ $ gopherhole agents config agent-abc123 --email-enabled
876
+ $ gopherhole agents config agent-abc123 --no-email-enabled
874
877
  `)
875
878
  .option('--auto-approve', 'Enable auto-approve (instant access for marketplace)')
876
879
  .option('--no-auto-approve', 'Disable auto-approve (require manual approval)')
@@ -881,6 +884,9 @@ ${chalk_1.default.bold('Examples:')}
881
884
  .option('--category <category>', 'Set category')
882
885
  .option('--tags <tags>', 'Set tags (comma-separated)')
883
886
  .option('--description <text>', 'Update description')
887
+ .option('--alias <alias>', 'Set email alias (<alias>[.<tenant-slug>]@gopherhole.io). Triggers 30-day grace redirect from the previous alias if email is enabled')
888
+ .option('--email-enabled', 'Enable inbound + outbound email for this agent (requires an alias)')
889
+ .option('--no-email-enabled', 'Disable email for this agent (inbound rejects with 550, Postie refuses to send)')
884
890
  .action(async (agentId, options) => {
885
891
  const sessionId = config.get('sessionId');
886
892
  if (!sessionId) {
@@ -921,26 +927,65 @@ ${chalk_1.default.bold('Examples:')}
921
927
  if (options.description) {
922
928
  body.description = options.description;
923
929
  }
924
- if (Object.keys(body).length === 0) {
930
+ const aliasChange = options.alias;
931
+ const emailEnabledChange = options.emailEnabled;
932
+ if (Object.keys(body).length === 0 && aliasChange === undefined && emailEnabledChange === undefined) {
925
933
  console.log(chalk_1.default.yellow('No changes specified.'));
926
934
  console.log(chalk_1.default.gray('Use --help to see available options.'));
927
935
  return;
928
936
  }
929
937
  const spinner = (0, ora_1.default)('Updating agent config...').start();
930
- log('PATCH /agents/' + agentId, body);
931
938
  try {
932
- const res = await fetch(`${API_URL}/agents/${agentId}`, {
933
- method: 'PATCH',
934
- headers: {
935
- 'Content-Type': 'application/json',
936
- 'X-Session-ID': sessionId,
937
- },
938
- body: JSON.stringify(body),
939
- });
940
- if (!res.ok) {
941
- const err = await res.json();
942
- logError('config', err);
943
- throw new Error(err.error || 'Failed to update agent');
939
+ if (Object.keys(body).length > 0) {
940
+ log('PATCH /agents/' + agentId, body);
941
+ const res = await fetch(`${API_URL}/agents/${agentId}`, {
942
+ method: 'PATCH',
943
+ headers: {
944
+ 'Content-Type': 'application/json',
945
+ 'X-Session-ID': sessionId,
946
+ },
947
+ body: JSON.stringify(body),
948
+ });
949
+ if (!res.ok) {
950
+ const err = await res.json();
951
+ logError('config', err);
952
+ throw new Error(err.error || 'Failed to update agent');
953
+ }
954
+ }
955
+ // Alias rename has its own endpoint because it also writes the
956
+ // 30-day grace redirect into agent_alias_history.
957
+ if (aliasChange !== undefined) {
958
+ log('PATCH /agents/' + agentId + '/alias', { alias: aliasChange });
959
+ const res = await fetch(`${API_URL}/agents/${agentId}/alias`, {
960
+ method: 'PATCH',
961
+ headers: {
962
+ 'Content-Type': 'application/json',
963
+ 'X-Session-ID': sessionId,
964
+ },
965
+ body: JSON.stringify({ alias: aliasChange }),
966
+ });
967
+ if (!res.ok) {
968
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
969
+ logError('alias', err);
970
+ throw new Error(err.error || 'Failed to set alias');
971
+ }
972
+ }
973
+ // Email toggle is a dedicated endpoint that enforces alias-first.
974
+ if (emailEnabledChange !== undefined) {
975
+ log('PATCH /agents/' + agentId + '/email-enabled', { enabled: emailEnabledChange });
976
+ const res = await fetch(`${API_URL}/agents/${agentId}/email-enabled`, {
977
+ method: 'PATCH',
978
+ headers: {
979
+ 'Content-Type': 'application/json',
980
+ 'X-Session-ID': sessionId,
981
+ },
982
+ body: JSON.stringify({ enabled: emailEnabledChange }),
983
+ });
984
+ if (!res.ok) {
985
+ const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
986
+ logError('email-enabled', err);
987
+ throw new Error(err.error || 'Failed to toggle email');
988
+ }
944
989
  }
945
990
  spinner.succeed('Agent config updated');
946
991
  // Show what was changed
@@ -969,6 +1014,12 @@ ${chalk_1.default.bold('Examples:')}
969
1014
  const desc = body.description;
970
1015
  changes.push(` Description: ${chalk_1.default.gray(desc.slice(0, 50))}${desc.length > 50 ? '...' : ''}`);
971
1016
  }
1017
+ if (aliasChange !== undefined) {
1018
+ changes.push(` Alias: ${brand.green(aliasChange)}`);
1019
+ }
1020
+ if (emailEnabledChange !== undefined) {
1021
+ changes.push(` Email: ${emailEnabledChange ? brand.green('enabled 📬') : chalk_1.default.gray('disabled')}`);
1022
+ }
972
1023
  if (changes.length > 0) {
973
1024
  console.log('');
974
1025
  console.log(chalk_1.default.bold('Changes:'));
@@ -2899,5 +2950,794 @@ wsMembers
2899
2950
  process.exit(1);
2900
2951
  }
2901
2952
  });
2953
+ // ========== KEYS COMMAND ==========
2954
+ const keys = program
2955
+ .command('keys')
2956
+ .description(`Manage API keys
2957
+
2958
+ ${chalk_1.default.bold('Examples:')}
2959
+ $ gopherhole keys list
2960
+ $ gopherhole keys create --name "prod" --agent agent-abc123
2961
+ $ gopherhole keys delete key-abc123
2962
+ `);
2963
+ keys
2964
+ .command('list')
2965
+ .description('List all API keys on the tenant')
2966
+ .option('--json', 'Output as JSON')
2967
+ .action(async (options) => {
2968
+ const sessionId = config.get('sessionId');
2969
+ if (!sessionId) {
2970
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
2971
+ process.exit(1);
2972
+ }
2973
+ const spinner = (0, ora_1.default)('Fetching API keys...').start();
2974
+ try {
2975
+ const res = await fetch(`${API_URL}/api-keys`, { headers: { 'X-Session-ID': sessionId } });
2976
+ if (!res.ok)
2977
+ throw new Error('Failed to fetch API keys');
2978
+ const data = await res.json();
2979
+ const keysList = Array.isArray(data) ? data : data.keys || [];
2980
+ spinner.stop();
2981
+ if (options.json) {
2982
+ console.log(JSON.stringify(keysList, null, 2));
2983
+ return;
2984
+ }
2985
+ if (keysList.length === 0) {
2986
+ console.log(chalk_1.default.yellow('\nNo API keys found.\n'));
2987
+ return;
2988
+ }
2989
+ console.log(chalk_1.default.bold('\nAPI Keys:\n'));
2990
+ for (const k of keysList) {
2991
+ console.log(` ${chalk_1.default.cyan(k.id)} — ${k.name || 'unnamed'}`);
2992
+ console.log(` Agent: ${k.agentId || k.agent_id || 'none'} | Prefix: ${chalk_1.default.gray(k.prefix || '???')} | Scopes: ${k.scopes?.join(', ') || 'default'}`);
2993
+ console.log('');
2994
+ }
2995
+ }
2996
+ catch (err) {
2997
+ spinner.fail(chalk_1.default.red(err.message));
2998
+ process.exit(1);
2999
+ }
3000
+ });
3001
+ keys
3002
+ .command('create')
3003
+ .description('Create a new API key')
3004
+ .requiredOption('--name <name>', 'Human-readable label for this key')
3005
+ .requiredOption('--agent <agentId>', 'The agent this key authenticates as')
3006
+ .option('--scopes <scopes>', 'Comma-separated scopes (e.g., "messages:send,memory:read")')
3007
+ .action(async (options) => {
3008
+ const sessionId = config.get('sessionId');
3009
+ if (!sessionId) {
3010
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3011
+ process.exit(1);
3012
+ }
3013
+ const spinner = (0, ora_1.default)('Creating API key...').start();
3014
+ try {
3015
+ const body = { name: options.name, agentId: options.agent };
3016
+ if (options.scopes)
3017
+ body.scopes = options.scopes.split(',').map((s) => s.trim());
3018
+ const res = await fetch(`${API_URL}/api-keys`, {
3019
+ method: 'POST',
3020
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3021
+ body: JSON.stringify(body),
3022
+ });
3023
+ if (!res.ok) {
3024
+ const err = await res.json();
3025
+ throw new Error(err.error || 'Failed to create key');
3026
+ }
3027
+ const data = await res.json();
3028
+ spinner.succeed('API key created!');
3029
+ console.log('');
3030
+ console.log(` ID: ${chalk_1.default.cyan(data.id)}`);
3031
+ console.log(` Key: ${brand.green(data.key || data.secret)}`);
3032
+ console.log('');
3033
+ console.log(chalk_1.default.yellow(' Store this key securely — it will not be shown again.'));
3034
+ console.log('');
3035
+ }
3036
+ catch (err) {
3037
+ spinner.fail(chalk_1.default.red(err.message));
3038
+ process.exit(1);
3039
+ }
3040
+ });
3041
+ keys
3042
+ .command('delete <keyId>')
3043
+ .description('Revoke and delete an API key')
3044
+ .option('-y, --yes', 'Skip confirmation')
3045
+ .action(async (keyId, options) => {
3046
+ const sessionId = config.get('sessionId');
3047
+ if (!sessionId) {
3048
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3049
+ process.exit(1);
3050
+ }
3051
+ if (!options.yes) {
3052
+ const { confirm } = await inquirer_1.default.prompt([{
3053
+ type: 'confirm', name: 'confirm', message: `Permanently revoke key ${keyId}?`, default: false,
3054
+ }]);
3055
+ if (!confirm) {
3056
+ console.log(chalk_1.default.gray('Cancelled.'));
3057
+ return;
3058
+ }
3059
+ }
3060
+ const spinner = (0, ora_1.default)('Revoking key...').start();
3061
+ try {
3062
+ const res = await fetch(`${API_URL}/api-keys/${keyId}`, {
3063
+ method: 'DELETE',
3064
+ headers: { 'X-Session-ID': sessionId },
3065
+ });
3066
+ if (!res.ok) {
3067
+ const err = await res.json();
3068
+ throw new Error(err.error || 'Failed to delete key');
3069
+ }
3070
+ spinner.succeed(`Key ${chalk_1.default.cyan(keyId)} revoked and deleted.`);
3071
+ }
3072
+ catch (err) {
3073
+ spinner.fail(chalk_1.default.red(err.message));
3074
+ process.exit(1);
3075
+ }
3076
+ });
3077
+ // ========== TEAM COMMAND ==========
3078
+ const team = program
3079
+ .command('team')
3080
+ .description(`Manage team members
3081
+
3082
+ ${chalk_1.default.bold('Examples:')}
3083
+ $ gopherhole team list
3084
+ $ gopherhole team invite user@example.com --role admin
3085
+ $ gopherhole team remove member-abc123
3086
+ `);
3087
+ team
3088
+ .command('list')
3089
+ .description('List team members')
3090
+ .option('--json', 'Output as JSON')
3091
+ .action(async (options) => {
3092
+ const sessionId = config.get('sessionId');
3093
+ if (!sessionId) {
3094
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3095
+ process.exit(1);
3096
+ }
3097
+ const spinner = (0, ora_1.default)('Fetching team...').start();
3098
+ try {
3099
+ const res = await fetch(`${API_URL}/team/members`, { headers: { 'X-Session-ID': sessionId } });
3100
+ if (!res.ok)
3101
+ throw new Error('Failed to fetch team');
3102
+ const data = await res.json();
3103
+ const members = Array.isArray(data) ? data : data.members || [];
3104
+ spinner.stop();
3105
+ if (options.json) {
3106
+ console.log(JSON.stringify(members, null, 2));
3107
+ return;
3108
+ }
3109
+ if (members.length === 0) {
3110
+ console.log(chalk_1.default.yellow('\nNo team members.\n'));
3111
+ return;
3112
+ }
3113
+ console.log(chalk_1.default.bold('\nTeam Members:\n'));
3114
+ for (const m of members) {
3115
+ const role = m.role === 'admin' ? chalk_1.default.red(m.role) : chalk_1.default.gray(m.role);
3116
+ console.log(` ${chalk_1.default.bold(m.name || m.email)} (${chalk_1.default.cyan(m.id)})`);
3117
+ console.log(` Role: ${role}${m.joinedAt ? ` | Joined: ${m.joinedAt}` : ''}`);
3118
+ console.log('');
3119
+ }
3120
+ }
3121
+ catch (err) {
3122
+ spinner.fail(chalk_1.default.red(err.message));
3123
+ process.exit(1);
3124
+ }
3125
+ });
3126
+ team
3127
+ .command('invite <email>')
3128
+ .description('Invite someone to your tenant')
3129
+ .option('--role <role>', 'Role: admin, member, viewer (default: member)')
3130
+ .action(async (email, options) => {
3131
+ const sessionId = config.get('sessionId');
3132
+ if (!sessionId) {
3133
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3134
+ process.exit(1);
3135
+ }
3136
+ const spinner = (0, ora_1.default)(`Sending invite to ${email}...`).start();
3137
+ try {
3138
+ const body = { email };
3139
+ if (options.role)
3140
+ body.role = options.role;
3141
+ const res = await fetch(`${API_URL}/team/invites`, {
3142
+ method: 'POST',
3143
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3144
+ body: JSON.stringify(body),
3145
+ });
3146
+ if (!res.ok) {
3147
+ const err = await res.json();
3148
+ throw new Error(err.error || 'Failed to send invite');
3149
+ }
3150
+ spinner.succeed(`Invitation sent to ${chalk_1.default.cyan(email)} with role: ${options.role || 'member'}`);
3151
+ }
3152
+ catch (err) {
3153
+ spinner.fail(chalk_1.default.red(err.message));
3154
+ process.exit(1);
3155
+ }
3156
+ });
3157
+ team
3158
+ .command('remove <memberId>')
3159
+ .description('Remove a team member')
3160
+ .option('-y, --yes', 'Skip confirmation')
3161
+ .action(async (memberId, options) => {
3162
+ const sessionId = config.get('sessionId');
3163
+ if (!sessionId) {
3164
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3165
+ process.exit(1);
3166
+ }
3167
+ if (!options.yes) {
3168
+ const { confirm } = await inquirer_1.default.prompt([{
3169
+ type: 'confirm', name: 'confirm', message: `Remove team member ${memberId}?`, default: false,
3170
+ }]);
3171
+ if (!confirm) {
3172
+ console.log(chalk_1.default.gray('Cancelled.'));
3173
+ return;
3174
+ }
3175
+ }
3176
+ const spinner = (0, ora_1.default)('Removing member...').start();
3177
+ try {
3178
+ const res = await fetch(`${API_URL}/team/members/${memberId}`, {
3179
+ method: 'DELETE',
3180
+ headers: { 'X-Session-ID': sessionId },
3181
+ });
3182
+ if (!res.ok) {
3183
+ const err = await res.json();
3184
+ throw new Error(err.error || 'Failed to remove member');
3185
+ }
3186
+ spinner.succeed(`Team member ${chalk_1.default.cyan(memberId)} removed.`);
3187
+ }
3188
+ catch (err) {
3189
+ spinner.fail(chalk_1.default.red(err.message));
3190
+ process.exit(1);
3191
+ }
3192
+ });
3193
+ // ========== USAGE COMMAND ==========
3194
+ const usage = program
3195
+ .command('usage')
3196
+ .description(`View usage statistics
3197
+
3198
+ ${chalk_1.default.bold('Examples:')}
3199
+ $ gopherhole usage summary
3200
+ $ gopherhole usage summary --period week
3201
+ $ gopherhole usage agents
3202
+ `);
3203
+ usage
3204
+ .command('summary')
3205
+ .description('Get high-level usage overview')
3206
+ .option('--period <period>', 'Time window: day, week, month (default: month)')
3207
+ .option('--json', 'Output as JSON')
3208
+ .action(async (options) => {
3209
+ const sessionId = config.get('sessionId');
3210
+ if (!sessionId) {
3211
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3212
+ process.exit(1);
3213
+ }
3214
+ const spinner = (0, ora_1.default)('Fetching usage...').start();
3215
+ try {
3216
+ const params = new URLSearchParams();
3217
+ if (options.period)
3218
+ params.set('period', options.period);
3219
+ const qs = params.toString() ? `?${params}` : '';
3220
+ const res = await fetch(`${API_URL}/usage/summary${qs}`, { headers: { 'X-Session-ID': sessionId } });
3221
+ if (!res.ok)
3222
+ throw new Error('Failed to fetch usage');
3223
+ const data = await res.json();
3224
+ spinner.stop();
3225
+ if (options.json) {
3226
+ console.log(JSON.stringify(data, null, 2));
3227
+ return;
3228
+ }
3229
+ console.log(chalk_1.default.bold(`\nUsage Summary (${options.period || 'month'}):\n`));
3230
+ const d = data;
3231
+ if (d.messagesSent !== undefined)
3232
+ console.log(` Messages Sent: ${brand.green(d.messagesSent)}`);
3233
+ if (d.messagesReceived !== undefined)
3234
+ console.log(` Messages Received: ${brand.green(d.messagesReceived)}`);
3235
+ if (d.tasksCreated !== undefined)
3236
+ console.log(` Tasks Created: ${brand.green(d.tasksCreated)}`);
3237
+ if (d.connections !== undefined)
3238
+ console.log(` Connections: ${brand.green(d.connections)}`);
3239
+ if (d.creditsUsed !== undefined)
3240
+ console.log(` Credits Used: ${brand.green(d.creditsUsed)}`);
3241
+ console.log('');
3242
+ }
3243
+ catch (err) {
3244
+ spinner.fail(chalk_1.default.red(err.message));
3245
+ process.exit(1);
3246
+ }
3247
+ });
3248
+ usage
3249
+ .command('agents')
3250
+ .description('Per-agent usage breakdown')
3251
+ .option('--period <period>', 'Time window: day, week, month (default: month)')
3252
+ .option('--limit <n>', 'Max agents to show (default: 20)')
3253
+ .option('--json', 'Output as JSON')
3254
+ .action(async (options) => {
3255
+ const sessionId = config.get('sessionId');
3256
+ if (!sessionId) {
3257
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3258
+ process.exit(1);
3259
+ }
3260
+ const spinner = (0, ora_1.default)('Fetching per-agent usage...').start();
3261
+ try {
3262
+ const params = new URLSearchParams();
3263
+ if (options.period)
3264
+ params.set('period', options.period);
3265
+ if (options.limit)
3266
+ params.set('limit', options.limit);
3267
+ const qs = params.toString() ? `?${params}` : '';
3268
+ const res = await fetch(`${API_URL}/usage/agents${qs}`, { headers: { 'X-Session-ID': sessionId } });
3269
+ if (!res.ok)
3270
+ throw new Error('Failed to fetch agent usage');
3271
+ const data = await res.json();
3272
+ spinner.stop();
3273
+ if (options.json) {
3274
+ console.log(JSON.stringify(data, null, 2));
3275
+ return;
3276
+ }
3277
+ const agents = Array.isArray(data) ? data : data.agents || [];
3278
+ if (agents.length === 0) {
3279
+ console.log(chalk_1.default.yellow('\nNo usage data.\n'));
3280
+ return;
3281
+ }
3282
+ console.log(chalk_1.default.bold(`\nPer-Agent Usage (${options.period || 'month'}):\n`));
3283
+ for (const a of agents) {
3284
+ console.log(` ${chalk_1.default.bold(a.name || a.agentId)} (${chalk_1.default.cyan(a.agentId || a.id)})`);
3285
+ if (a.messages !== undefined)
3286
+ console.log(` Messages: ${a.messages}`);
3287
+ if (a.tasks !== undefined)
3288
+ console.log(` Tasks: ${a.tasks}`);
3289
+ if (a.credits !== undefined)
3290
+ console.log(` Credits: ${a.credits}`);
3291
+ console.log('');
3292
+ }
3293
+ }
3294
+ catch (err) {
3295
+ spinner.fail(chalk_1.default.red(err.message));
3296
+ process.exit(1);
3297
+ }
3298
+ });
3299
+ // ========== WEBHOOKS COMMAND ==========
3300
+ const webhooks = program
3301
+ .command('webhooks')
3302
+ .description(`Manage webhooks
3303
+
3304
+ ${chalk_1.default.bold('Examples:')}
3305
+ $ gopherhole webhooks list
3306
+ $ gopherhole webhooks create --url https://example.com/hook --events message.received,task.completed
3307
+ $ gopherhole webhooks delete wh-abc123
3308
+ $ gopherhole webhooks test wh-abc123
3309
+ `);
3310
+ webhooks
3311
+ .command('list')
3312
+ .description('List configured webhooks')
3313
+ .option('--json', 'Output as JSON')
3314
+ .action(async (options) => {
3315
+ const sessionId = config.get('sessionId');
3316
+ if (!sessionId) {
3317
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3318
+ process.exit(1);
3319
+ }
3320
+ const spinner = (0, ora_1.default)('Fetching webhooks...').start();
3321
+ try {
3322
+ const res = await fetch(`${API_URL}/webhooks`, { headers: { 'X-Session-ID': sessionId } });
3323
+ if (!res.ok)
3324
+ throw new Error('Failed to fetch webhooks');
3325
+ const data = await res.json();
3326
+ const hooks = Array.isArray(data) ? data : data.webhooks || [];
3327
+ spinner.stop();
3328
+ if (options.json) {
3329
+ console.log(JSON.stringify(hooks, null, 2));
3330
+ return;
3331
+ }
3332
+ if (hooks.length === 0) {
3333
+ console.log(chalk_1.default.yellow('\nNo webhooks configured.\n'));
3334
+ return;
3335
+ }
3336
+ console.log(chalk_1.default.bold('\nWebhooks:\n'));
3337
+ for (const h of hooks) {
3338
+ console.log(` ${chalk_1.default.cyan(h.id)} — ${h.url}`);
3339
+ console.log(` Events: ${h.events?.join(', ') || 'all'}${h.description ? ` | ${chalk_1.default.gray(h.description)}` : ''}`);
3340
+ console.log('');
3341
+ }
3342
+ }
3343
+ catch (err) {
3344
+ spinner.fail(chalk_1.default.red(err.message));
3345
+ process.exit(1);
3346
+ }
3347
+ });
3348
+ webhooks
3349
+ .command('create')
3350
+ .description('Create a new webhook')
3351
+ .requiredOption('--url <url>', 'HTTPS URL to deliver events to')
3352
+ .requiredOption('--events <events>', 'Comma-separated event types (e.g., "message.received,task.completed")')
3353
+ .option('--description <text>', 'Label for this webhook')
3354
+ .option('--secret <secret>', 'Signing secret for HMAC-SHA256 verification')
3355
+ .action(async (options) => {
3356
+ const sessionId = config.get('sessionId');
3357
+ if (!sessionId) {
3358
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3359
+ process.exit(1);
3360
+ }
3361
+ const spinner = (0, ora_1.default)('Creating webhook...').start();
3362
+ try {
3363
+ const body = {
3364
+ url: options.url,
3365
+ events: options.events.split(',').map((e) => e.trim()),
3366
+ };
3367
+ if (options.description)
3368
+ body.description = options.description;
3369
+ if (options.secret)
3370
+ body.secret = options.secret;
3371
+ const res = await fetch(`${API_URL}/webhooks`, {
3372
+ method: 'POST',
3373
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3374
+ body: JSON.stringify(body),
3375
+ });
3376
+ if (!res.ok) {
3377
+ const err = await res.json();
3378
+ throw new Error(err.error || 'Failed to create webhook');
3379
+ }
3380
+ const data = await res.json();
3381
+ spinner.succeed('Webhook created!');
3382
+ console.log(`\n ID: ${chalk_1.default.cyan(data.id)}`);
3383
+ console.log(` URL: ${data.url || options.url}`);
3384
+ console.log(` Events: ${body.events.join(', ')}\n`);
3385
+ }
3386
+ catch (err) {
3387
+ spinner.fail(chalk_1.default.red(err.message));
3388
+ process.exit(1);
3389
+ }
3390
+ });
3391
+ webhooks
3392
+ .command('delete <webhookId>')
3393
+ .description('Delete a webhook')
3394
+ .option('-y, --yes', 'Skip confirmation')
3395
+ .action(async (webhookId, options) => {
3396
+ const sessionId = config.get('sessionId');
3397
+ if (!sessionId) {
3398
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3399
+ process.exit(1);
3400
+ }
3401
+ if (!options.yes) {
3402
+ const { confirm } = await inquirer_1.default.prompt([{
3403
+ type: 'confirm', name: 'confirm', message: `Delete webhook ${webhookId}?`, default: false,
3404
+ }]);
3405
+ if (!confirm) {
3406
+ console.log(chalk_1.default.gray('Cancelled.'));
3407
+ return;
3408
+ }
3409
+ }
3410
+ const spinner = (0, ora_1.default)('Deleting webhook...').start();
3411
+ try {
3412
+ const res = await fetch(`${API_URL}/webhooks/${webhookId}`, {
3413
+ method: 'DELETE',
3414
+ headers: { 'X-Session-ID': sessionId },
3415
+ });
3416
+ if (!res.ok) {
3417
+ const err = await res.json();
3418
+ throw new Error(err.error || 'Failed to delete webhook');
3419
+ }
3420
+ spinner.succeed(`Webhook ${chalk_1.default.cyan(webhookId)} deleted.`);
3421
+ }
3422
+ catch (err) {
3423
+ spinner.fail(chalk_1.default.red(err.message));
3424
+ process.exit(1);
3425
+ }
3426
+ });
3427
+ webhooks
3428
+ .command('test <webhookId>')
3429
+ .description('Send a test event to a webhook')
3430
+ .action(async (webhookId) => {
3431
+ const sessionId = config.get('sessionId');
3432
+ if (!sessionId) {
3433
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3434
+ process.exit(1);
3435
+ }
3436
+ const spinner = (0, ora_1.default)('Sending test event...').start();
3437
+ try {
3438
+ const res = await fetch(`${API_URL}/webhooks/${webhookId}/test`, {
3439
+ method: 'POST',
3440
+ headers: { 'X-Session-ID': sessionId },
3441
+ });
3442
+ if (!res.ok) {
3443
+ const err = await res.json();
3444
+ throw new Error(err.error || 'Failed to test webhook');
3445
+ }
3446
+ const data = await res.json();
3447
+ spinner.succeed(`Test event sent. Response: ${data.status || data.statusCode || 'delivered'}`);
3448
+ }
3449
+ catch (err) {
3450
+ spinner.fail(chalk_1.default.red(err.message));
3451
+ process.exit(1);
3452
+ }
3453
+ });
3454
+ // ========== TENANT COMMAND ==========
3455
+ const tenant = program
3456
+ .command('tenant')
3457
+ .description(`View and manage tenant settings
3458
+
3459
+ ${chalk_1.default.bold('Examples:')}
3460
+ $ gopherhole tenant settings
3461
+ $ gopherhole tenant update --name "Acme Corp" --slug acme
3462
+ `);
3463
+ tenant
3464
+ .command('settings')
3465
+ .description('View current tenant settings')
3466
+ .option('--json', 'Output as JSON')
3467
+ .action(async (options) => {
3468
+ const sessionId = config.get('sessionId');
3469
+ if (!sessionId) {
3470
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3471
+ process.exit(1);
3472
+ }
3473
+ const spinner = (0, ora_1.default)('Fetching tenant settings...').start();
3474
+ try {
3475
+ const res = await fetch(`${API_URL}/auth/tenant/settings`, { headers: { 'X-Session-ID': sessionId } });
3476
+ if (!res.ok)
3477
+ throw new Error('Failed to fetch tenant settings');
3478
+ const data = await res.json();
3479
+ spinner.stop();
3480
+ if (options.json) {
3481
+ console.log(JSON.stringify(data, null, 2));
3482
+ return;
3483
+ }
3484
+ console.log(chalk_1.default.bold('\nTenant Settings:\n'));
3485
+ if (data.name)
3486
+ console.log(` Name: ${brand.green(data.name)}`);
3487
+ if (data.slug)
3488
+ console.log(` Slug: ${chalk_1.default.cyan(data.slug)}`);
3489
+ if (data.plan)
3490
+ console.log(` Plan: ${data.plan}`);
3491
+ if (data.email)
3492
+ console.log(` Email: ${data.email}`);
3493
+ if (data.id)
3494
+ console.log(` ID: ${chalk_1.default.gray(data.id)}`);
3495
+ console.log('');
3496
+ }
3497
+ catch (err) {
3498
+ spinner.fail(chalk_1.default.red(err.message));
3499
+ process.exit(1);
3500
+ }
3501
+ });
3502
+ tenant
3503
+ .command('update')
3504
+ .description('Update tenant name or slug')
3505
+ .option('--name <name>', 'New display name')
3506
+ .option('--slug <slug>', 'New URL-safe slug')
3507
+ .action(async (options) => {
3508
+ const sessionId = config.get('sessionId');
3509
+ if (!sessionId) {
3510
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3511
+ process.exit(1);
3512
+ }
3513
+ const body = {};
3514
+ if (options.name)
3515
+ body.name = options.name;
3516
+ if (options.slug)
3517
+ body.slug = options.slug;
3518
+ if (Object.keys(body).length === 0) {
3519
+ console.log(chalk_1.default.yellow('No changes specified. Use --name or --slug.'));
3520
+ return;
3521
+ }
3522
+ const spinner = (0, ora_1.default)('Updating tenant...').start();
3523
+ try {
3524
+ const res = await fetch(`${API_URL}/auth/tenant`, {
3525
+ method: 'PATCH',
3526
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3527
+ body: JSON.stringify(body),
3528
+ });
3529
+ if (!res.ok) {
3530
+ const err = await res.json();
3531
+ throw new Error(err.error || 'Failed to update tenant');
3532
+ }
3533
+ spinner.succeed('Tenant updated.');
3534
+ if (body.name)
3535
+ console.log(` Name: ${brand.green(body.name)}`);
3536
+ if (body.slug)
3537
+ console.log(` Slug: ${chalk_1.default.cyan(body.slug)}`);
3538
+ }
3539
+ catch (err) {
3540
+ spinner.fail(chalk_1.default.red(err.message));
3541
+ process.exit(1);
3542
+ }
3543
+ });
3544
+ // ========== PROFILE COMMAND ==========
3545
+ program
3546
+ .command('profile')
3547
+ .description(`Update your user profile
3548
+
3549
+ ${chalk_1.default.bold('Examples:')}
3550
+ $ gopherhole profile --name "Jane Smith"
3551
+ $ gopherhole profile --avatar https://example.com/photo.jpg
3552
+ `)
3553
+ .option('--name <name>', 'Display name')
3554
+ .option('--avatar <url>', 'Avatar image URL')
3555
+ .action(async (options) => {
3556
+ const sessionId = config.get('sessionId');
3557
+ if (!sessionId) {
3558
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3559
+ process.exit(1);
3560
+ }
3561
+ const body = {};
3562
+ if (options.name)
3563
+ body.name = options.name;
3564
+ if (options.avatar)
3565
+ body.avatarUrl = options.avatar;
3566
+ if (Object.keys(body).length === 0) {
3567
+ console.log(chalk_1.default.yellow('No changes specified. Use --name or --avatar.'));
3568
+ return;
3569
+ }
3570
+ const spinner = (0, ora_1.default)('Updating profile...').start();
3571
+ try {
3572
+ const res = await fetch(`${API_URL}/auth/me`, {
3573
+ method: 'PATCH',
3574
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3575
+ body: JSON.stringify(body),
3576
+ });
3577
+ if (!res.ok) {
3578
+ const err = await res.json();
3579
+ throw new Error(err.error || 'Failed to update profile');
3580
+ }
3581
+ spinner.succeed('Profile updated.');
3582
+ if (body.name)
3583
+ console.log(` Name: ${brand.green(body.name)}`);
3584
+ if (body.avatarUrl)
3585
+ console.log(` Avatar: updated`);
3586
+ }
3587
+ catch (err) {
3588
+ spinner.fail(chalk_1.default.red(err.message));
3589
+ process.exit(1);
3590
+ }
3591
+ });
3592
+ // ========== SECRETS COMMAND (workspace secrets) ==========
3593
+ const secrets = program
3594
+ .command('secrets')
3595
+ .description(`Manage workspace secrets
3596
+
3597
+ ${chalk_1.default.bold('Examples:')}
3598
+ $ gopherhole secrets list ws-abc123
3599
+ $ gopherhole secrets set ws-abc123 OPENAI_API_KEY sk-abc...
3600
+ $ gopherhole secrets delete ws-abc123 OPENAI_API_KEY
3601
+ `);
3602
+ secrets
3603
+ .command('list <workspaceId>')
3604
+ .description('List secret keys in a workspace (values are never shown)')
3605
+ .option('--json', 'Output as JSON')
3606
+ .action(async (workspaceId, options) => {
3607
+ const sessionId = config.get('sessionId');
3608
+ if (!sessionId) {
3609
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3610
+ process.exit(1);
3611
+ }
3612
+ const spinner = (0, ora_1.default)('Fetching secrets...').start();
3613
+ try {
3614
+ const res = await fetch(`${API_URL}/workspaces/${workspaceId}/secrets`, { headers: { 'X-Session-ID': sessionId } });
3615
+ if (!res.ok)
3616
+ throw new Error('Failed to fetch secrets');
3617
+ const data = await res.json();
3618
+ const secretsList = Array.isArray(data) ? data : data.secrets || [];
3619
+ spinner.stop();
3620
+ if (options.json) {
3621
+ console.log(JSON.stringify(secretsList, null, 2));
3622
+ return;
3623
+ }
3624
+ if (secretsList.length === 0) {
3625
+ console.log(chalk_1.default.yellow('\nNo secrets in this workspace.\n'));
3626
+ return;
3627
+ }
3628
+ console.log(chalk_1.default.bold('\nWorkspace Secrets:\n'));
3629
+ for (const s of secretsList) {
3630
+ console.log(` ${chalk_1.default.cyan(s.key || s.name)}`);
3631
+ }
3632
+ console.log('');
3633
+ }
3634
+ catch (err) {
3635
+ spinner.fail(chalk_1.default.red(err.message));
3636
+ process.exit(1);
3637
+ }
3638
+ });
3639
+ secrets
3640
+ .command('set <workspaceId> <key> <value>')
3641
+ .description('Create or update a workspace secret')
3642
+ .action(async (workspaceId, key, value) => {
3643
+ const sessionId = config.get('sessionId');
3644
+ if (!sessionId) {
3645
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3646
+ process.exit(1);
3647
+ }
3648
+ const spinner = (0, ora_1.default)(`Setting secret ${key}...`).start();
3649
+ try {
3650
+ const res = await fetch(`${API_URL}/workspaces/${workspaceId}/secrets`, {
3651
+ method: 'POST',
3652
+ headers: { 'Content-Type': 'application/json', 'X-Session-ID': sessionId },
3653
+ body: JSON.stringify({ key, value }),
3654
+ });
3655
+ if (!res.ok) {
3656
+ const err = await res.json();
3657
+ throw new Error(err.error || 'Failed to set secret');
3658
+ }
3659
+ spinner.succeed(`Secret ${chalk_1.default.cyan(key)} stored.`);
3660
+ }
3661
+ catch (err) {
3662
+ spinner.fail(chalk_1.default.red(err.message));
3663
+ process.exit(1);
3664
+ }
3665
+ });
3666
+ secrets
3667
+ .command('delete <workspaceId> <key>')
3668
+ .description('Delete a workspace secret')
3669
+ .option('-y, --yes', 'Skip confirmation')
3670
+ .action(async (workspaceId, key, options) => {
3671
+ const sessionId = config.get('sessionId');
3672
+ if (!sessionId) {
3673
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3674
+ process.exit(1);
3675
+ }
3676
+ if (!options.yes) {
3677
+ const { confirm } = await inquirer_1.default.prompt([{
3678
+ type: 'confirm', name: 'confirm', message: `Delete secret ${key}?`, default: false,
3679
+ }]);
3680
+ if (!confirm) {
3681
+ console.log(chalk_1.default.gray('Cancelled.'));
3682
+ return;
3683
+ }
3684
+ }
3685
+ const spinner = (0, ora_1.default)(`Deleting secret ${key}...`).start();
3686
+ try {
3687
+ const res = await fetch(`${API_URL}/workspaces/${workspaceId}/secrets/${key}`, {
3688
+ method: 'DELETE',
3689
+ headers: { 'X-Session-ID': sessionId },
3690
+ });
3691
+ if (!res.ok) {
3692
+ const err = await res.json();
3693
+ throw new Error(err.error || 'Failed to delete secret');
3694
+ }
3695
+ spinner.succeed(`Secret ${chalk_1.default.cyan(key)} deleted.`);
3696
+ }
3697
+ catch (err) {
3698
+ spinner.fail(chalk_1.default.red(err.message));
3699
+ process.exit(1);
3700
+ }
3701
+ });
3702
+ // ========== CREDITS COMMAND ==========
3703
+ program
3704
+ .command('credits')
3705
+ .description(`View credit balance
3706
+
3707
+ ${chalk_1.default.bold('Examples:')}
3708
+ $ gopherhole credits
3709
+ `)
3710
+ .option('--json', 'Output as JSON')
3711
+ .action(async (options) => {
3712
+ const sessionId = config.get('sessionId');
3713
+ if (!sessionId) {
3714
+ console.log(chalk_1.default.yellow('Not logged in. Run: gopherhole login'));
3715
+ process.exit(1);
3716
+ }
3717
+ const spinner = (0, ora_1.default)('Fetching balance...').start();
3718
+ try {
3719
+ const res = await fetch(`${API_URL}/credits/balance`, { headers: { 'X-Session-ID': sessionId } });
3720
+ if (!res.ok)
3721
+ throw new Error('Failed to fetch credits');
3722
+ const data = await res.json();
3723
+ spinner.stop();
3724
+ if (options.json) {
3725
+ console.log(JSON.stringify(data, null, 2));
3726
+ return;
3727
+ }
3728
+ console.log(chalk_1.default.bold('\nCredit Balance:\n'));
3729
+ if (data.remaining !== undefined)
3730
+ console.log(` Remaining: ${brand.green(data.remaining)}`);
3731
+ if (data.total !== undefined)
3732
+ console.log(` Total: ${data.total}`);
3733
+ if (data.used !== undefined)
3734
+ console.log(` Used: ${data.used}`);
3735
+ console.log('');
3736
+ }
3737
+ catch (err) {
3738
+ spinner.fail(chalk_1.default.red(err.message));
3739
+ process.exit(1);
3740
+ }
3741
+ });
2902
3742
  // Parse and run
2903
3743
  program.parse();