feedbackbasket-cli 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,12 +129,15 @@ feedbackbasket bugs stats --project myapp # Per-project stats
129
129
  feedbackbasket widget settings myapp
130
130
 
131
131
  # Update widget configuration
132
+ feedbackbasket widget settings myapp --capture-mode waitlist
133
+ feedbackbasket widget settings myapp --capture-mode feedback
132
134
  feedbackbasket widget settings myapp --color "#22c55e" --label "Send Feedback"
133
135
  feedbackbasket widget settings myapp --position bottom-left --display modal
134
136
  feedbackbasket widget settings myapp --email-required --intro "How can we improve?"
135
137
  feedbackbasket widget settings myapp --button-radius 10 --button-size regular
136
138
  feedbackbasket widget settings myapp --show-email --allow-attachments
137
139
  feedbackbasket widget settings myapp --email-read-only --hide-email-when-prefilled
140
+ feedbackbasket widget settings myapp --error-tracking --allow-console-errors
138
141
 
139
142
  # Configure guided feedback types and follow-up questions
140
143
  feedbackbasket widget flow myapp
@@ -146,6 +149,19 @@ feedbackbasket widget flow myapp --config ./feedback-flow.json
146
149
  feedbackbasket widget script myapp
147
150
  ```
148
151
 
152
+ Waitlist mode uses the same project script. Add `data-feedbackbasket-waitlist` to your own form, with a required `email` field and optional `name` field. The CLI's `widget script` output shows a starter form when waitlist mode is active.
153
+
154
+ ### Waitlist
155
+
156
+ ```bash
157
+ feedbackbasket waitlist list myapp
158
+ feedbackbasket waitlist list myapp --search "@example.com" --limit 50 --offset 0
159
+ feedbackbasket waitlist list myapp --agent
160
+ feedbackbasket waitlist export myapp
161
+ ```
162
+
163
+ Waitlist listing returns emails, optional names, source pages, total counts, the active capture mode, and pagination. Export prints the dashboard-compatible CSV to stdout.
164
+
149
165
  For inline trigger mode, load the widget once and call the public API from your own button:
150
166
 
151
167
  ```html
@@ -156,6 +172,8 @@ For inline trigger mode, load the widget once and call the public API from your
156
172
 
157
173
  Passing the trigger element lets popup mode open beside your custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
158
174
 
175
+ Use only the public `openFeedbackForm()` API from the snippet. Do not call internal or undocumented methods such as `open()` or `openModal()`.
176
+
159
177
  Use `--email-read-only` and `--hide-email-when-prefilled` with runtime `userEmail` values from your app. These settings do not store visitor emails in FeedbackBasket widget settings.
160
178
 
161
179
  The default widget experience is a basic modal. Only switch to popup mode or enable guided feedback when you intentionally want that flow.
package/dist/src/cli.js CHANGED
@@ -11,6 +11,7 @@ import { createDoctorCommand } from './commands/doctor.js';
11
11
  import { createSetupCommand } from './commands/setup.js';
12
12
  import { createWidgetCommand } from './commands/widget.js';
13
13
  import { createTeamCommand } from './commands/team.js';
14
+ import { createWaitlistCommand } from './commands/waitlist.js';
14
15
  import { renderRootHelp } from './help.js';
15
16
  let writer;
16
17
  function resolveFormat(opts) {
@@ -62,6 +63,7 @@ export function run() {
62
63
  program.addCommand(createFeedbackCommand(getWriter));
63
64
  program.addCommand(createBugsCommand(getWriter));
64
65
  program.addCommand(createWidgetCommand(getWriter));
66
+ program.addCommand(createWaitlistCommand(getWriter));
65
67
  program.addCommand(createTeamCommand(getWriter));
66
68
  program.addCommand(createDoctorCommand(getWriter));
67
69
  program.addCommand(createSetupCommand(getWriter));
@@ -1,6 +1,7 @@
1
- import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
1
+ import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings, WaitlistResponse } from './types.js';
2
2
  export declare class FeedbackBasketClient {
3
- private http;
3
+ private readonly apiBaseUrl;
4
+ private readonly token;
4
5
  constructor(token: string, baseUrl: string);
5
6
  me(): Promise<UserProfile>;
6
7
  listProjects(): Promise<ProjectsResponse>;
@@ -48,9 +49,16 @@ export declare class FeedbackBasketClient {
48
49
  getWidgetScript(projectId: string): Promise<{
49
50
  projectId: string;
50
51
  projectName: string;
52
+ captureMode: 'feedback' | 'waitlist';
51
53
  embedCode: string;
52
54
  scriptUrl: string;
53
55
  }>;
56
+ getWaitlist(projectId: string, params?: {
57
+ search?: string;
58
+ limit?: number;
59
+ offset?: number;
60
+ }): Promise<WaitlistResponse>;
61
+ exportWaitlist(projectId: string): Promise<string>;
54
62
  updateFeedback(id: string, data: {
55
63
  status?: string;
56
64
  category?: string;
@@ -118,5 +126,4 @@ export declare class FeedbackBasketClient {
118
126
  email: string;
119
127
  }>;
120
128
  private request;
121
- private handleError;
122
129
  }
@@ -1,18 +1,11 @@
1
- import axios, { AxiosError } from 'axios';
2
1
  import { USER_AGENT } from './version.js';
3
- import { errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
2
+ import { CLIError, errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
4
3
  export class FeedbackBasketClient {
5
- http;
4
+ apiBaseUrl;
5
+ token;
6
6
  constructor(token, baseUrl) {
7
- this.http = axios.create({
8
- baseURL: `${baseUrl}/api/v1`,
9
- timeout: 30_000,
10
- headers: {
11
- 'Authorization': `Bearer ${token}`,
12
- 'Content-Type': 'application/json',
13
- 'User-Agent': USER_AGENT,
14
- },
15
- });
7
+ this.apiBaseUrl = `${baseUrl.replace(/\/$/, '')}/api/v1`;
8
+ this.token = token;
16
9
  }
17
10
  async me() {
18
11
  return this.request('GET', '/auth/me');
@@ -63,6 +56,14 @@ export class FeedbackBasketClient {
63
56
  async getWidgetScript(projectId) {
64
57
  return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget-script`);
65
58
  }
59
+ async getWaitlist(projectId, params = {}) {
60
+ const query = buildQuery(params);
61
+ return this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist${query}`);
62
+ }
63
+ async exportWaitlist(projectId) {
64
+ const data = await this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist/export`);
65
+ return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
66
+ }
66
67
  // Write operations
67
68
  async updateFeedback(id, data) {
68
69
  return this.request('PATCH', `/feedback/${encodeURIComponent(id)}`, data);
@@ -95,8 +96,8 @@ export class FeedbackBasketClient {
95
96
  return this.request('DELETE', `/feedback/${encodeURIComponent(feedbackId)}/notes/${encodeURIComponent(noteId)}`);
96
97
  }
97
98
  async exportFeedback(projectId, format = 'csv') {
98
- const response = await this.http.get(`/projects/${encodeURIComponent(projectId)}/export?format=${format}`);
99
- return typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2);
99
+ const data = await this.request('GET', `/projects/${encodeURIComponent(projectId)}/export?format=${format}`);
100
+ return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
100
101
  }
101
102
  // Team
102
103
  async listTeam() {
@@ -109,34 +110,58 @@ export class FeedbackBasketClient {
109
110
  return this.request('DELETE', `/team/${encodeURIComponent(memberId)}`);
110
111
  }
111
112
  async request(method, path, data) {
113
+ const controller = new AbortController();
114
+ const timeout = setTimeout(() => controller.abort(), 30_000);
112
115
  try {
113
- const response = await this.http.request({ method, url: path, data });
114
- return response.data;
116
+ const response = await fetch(`${this.apiBaseUrl}${path}`, {
117
+ method,
118
+ signal: controller.signal,
119
+ headers: {
120
+ 'Authorization': `Bearer ${this.token}`,
121
+ 'Content-Type': 'application/json',
122
+ 'User-Agent': USER_AGENT,
123
+ },
124
+ body: data === undefined ? undefined : JSON.stringify(data),
125
+ });
126
+ const contentType = response.headers.get('content-type') ?? '';
127
+ const payload = contentType.includes('application/json')
128
+ ? await response.json().catch(() => null)
129
+ : await response.text();
130
+ if (!response.ok) {
131
+ const message = getErrorMessage(payload, response.statusText);
132
+ switch (response.status) {
133
+ case 401: throw errAuth(message);
134
+ case 403: throw errForbidden(message);
135
+ case 404: throw errAPI(404, message);
136
+ case 429: throw errRateLimit();
137
+ default: throw errAPI(response.status, message);
138
+ }
139
+ }
140
+ return payload;
115
141
  }
116
142
  catch (error) {
117
- throw this.handleError(error);
143
+ if (error instanceof CLIError)
144
+ throw error;
145
+ const cause = error instanceof Error ? error : new Error(String(error));
146
+ throw errNetwork(cause);
118
147
  }
119
- }
120
- handleError(error) {
121
- if (error instanceof AxiosError) {
122
- const status = error.response?.status;
123
- const message = error.response?.data?.error
124
- ?? error.response?.data?.message
125
- ?? error.message;
126
- if (!error.response) {
127
- return errNetwork(error);
128
- }
129
- switch (status) {
130
- case 401: return errAuth(message);
131
- case 403: return errForbidden(message);
132
- case 404: return errAPI(404, message);
133
- case 429: return errRateLimit();
134
- default: return errAPI(status ?? 500, message);
135
- }
148
+ finally {
149
+ clearTimeout(timeout);
136
150
  }
137
- return error instanceof Error ? error : new Error(String(error));
138
151
  }
139
152
  }
153
+ function getErrorMessage(payload, fallback) {
154
+ if (payload && typeof payload === 'object') {
155
+ const value = payload;
156
+ if (typeof value.error === 'string')
157
+ return value.error;
158
+ if (typeof value.message === 'string')
159
+ return value.message;
160
+ }
161
+ if (typeof payload === 'string' && payload.trim())
162
+ return payload;
163
+ return fallback || 'Request failed';
164
+ }
140
165
  function buildQuery(params) {
141
166
  const parts = [];
142
167
  for (const [key, value] of Object.entries(params)) {
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type OutputWriter } from '../output/writer.js';
3
+ export declare function createWaitlistCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,99 @@
1
+ import { Command } from 'commander';
2
+ import { FeedbackBasketClient } from '../client.js';
3
+ import { AuthManager } from '../auth/manager.js';
4
+ import { loadConfig } from '../config/config.js';
5
+ import { errAuth, errUsage } from '../output/errors.js';
6
+ import { Format } from '../output/writer.js';
7
+ import { brand, divider } from '../output/theme.js';
8
+ import { resolveProject } from '../resolve.js';
9
+ export function createWaitlistCommand(getWriter) {
10
+ const waitlist = new Command('waitlist')
11
+ .description('View and export waitlist signups');
12
+ waitlist
13
+ .command('list [project]')
14
+ .description('List waitlist signups with optional search')
15
+ .option('--search <query>', 'Search email addresses and names')
16
+ .option('--limit <n>', 'Max results (1-100)', '50')
17
+ .option('--offset <n>', 'Offset for pagination', '0')
18
+ .action(async (projectArg, opts) => {
19
+ const writer = getWriter();
20
+ const client = requireClient();
21
+ const projectId = await resolveProjectId(client, projectArg);
22
+ const limit = parseInteger(opts.limit, '--limit', 1, 100);
23
+ const offset = parseInteger(opts.offset, '--offset', 0);
24
+ const result = await client.getWaitlist(projectId, {
25
+ search: opts.search,
26
+ limit,
27
+ offset,
28
+ });
29
+ if (writer.effectiveFormat() === Format.Styled) {
30
+ renderWaitlist(result.project.name, result.project.captureMode, result.entries, result.totalSignups, result.pagination.totalCount);
31
+ if (result.pagination.hasMore) {
32
+ console.log(brand.muted(` Next page: feedbackbasket waitlist list ${projectId} --offset ${offset + limit} --limit ${limit}`));
33
+ console.log();
34
+ }
35
+ return;
36
+ }
37
+ writer.ok(result, {
38
+ summary: `Showing ${result.entries.length} of ${result.pagination.totalCount} matching waitlist signups`,
39
+ });
40
+ });
41
+ waitlist
42
+ .command('export [project]')
43
+ .description('Export all waitlist signups as CSV')
44
+ .action(async (projectArg) => {
45
+ const client = requireClient();
46
+ const projectId = await resolveProjectId(client, projectArg);
47
+ console.log(await client.exportWaitlist(projectId));
48
+ });
49
+ return waitlist;
50
+ }
51
+ function requireClient() {
52
+ const manager = new AuthManager();
53
+ const token = manager.resolveToken();
54
+ if (!token)
55
+ throw errAuth();
56
+ const config = loadConfig();
57
+ return new FeedbackBasketClient(token, config.baseUrl);
58
+ }
59
+ async function resolveProjectId(client, projectArg) {
60
+ if (projectArg)
61
+ return (await resolveProject(client, projectArg)).id;
62
+ const config = loadConfig();
63
+ if (config.defaultProject)
64
+ return config.defaultProject;
65
+ throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket waitlist list <project>');
66
+ }
67
+ function parseInteger(value, flag, minimum, maximum) {
68
+ const parsed = Number(value);
69
+ if (!Number.isInteger(parsed) ||
70
+ parsed < minimum ||
71
+ (maximum !== undefined && parsed > maximum)) {
72
+ const range = maximum === undefined ? `${minimum} or greater` : `${minimum}-${maximum}`;
73
+ throw errUsage(`${flag} must be an integer in the range ${range}`);
74
+ }
75
+ return parsed;
76
+ }
77
+ function renderWaitlist(projectName, captureMode, entries, totalSignups, matchingSignups) {
78
+ console.log(brand.bold(`Waitlist — ${projectName}`));
79
+ console.log(divider(50));
80
+ console.log();
81
+ console.log(` ${brand.label('Capture mode')} ${captureMode}`);
82
+ console.log(` ${brand.label('Total signups')} ${totalSignups}`);
83
+ if (matchingSignups !== totalSignups) {
84
+ console.log(` ${brand.label('Matches')} ${matchingSignups}`);
85
+ }
86
+ console.log();
87
+ if (entries.length === 0) {
88
+ console.log(` ${brand.muted('No waitlist signups found')}`);
89
+ console.log();
90
+ return;
91
+ }
92
+ for (const entry of entries) {
93
+ console.log(` ${brand.bold(entry.email)}${entry.name ? ` — ${entry.name}` : ''}`);
94
+ console.log(` ${brand.muted(new Date(entry.updatedAt).toLocaleString())}`);
95
+ if (entry.pageUrl)
96
+ console.log(` ${brand.muted(entry.pageUrl)}`);
97
+ console.log();
98
+ }
99
+ }
@@ -51,6 +51,7 @@ export function createWidgetCommand(getWriter) {
51
51
  widget
52
52
  .command('settings [project]')
53
53
  .description('View or update widget settings')
54
+ .option('--capture-mode <mode>', 'Capture mode (feedback, waitlist)')
54
55
  .option('--color <hex>', 'Button color (e.g. #22c55e)')
55
56
  .option('--label <text>', 'Button label')
56
57
  .option('--position <pos>', 'Widget position (bottom-right, bottom-left, middle-right-edge, middle-left-edge, bottom-right-edge, bottom-left-edge)')
@@ -77,6 +78,10 @@ export function createWidgetCommand(getWriter) {
77
78
  .option('--no-show-icon', 'Hide the icon')
78
79
  .option('--show-branding', 'Show FeedbackBasket branding')
79
80
  .option('--no-show-branding', 'Hide FeedbackBasket branding when plan allows it')
81
+ .option('--allow-console-errors', 'Let visitors include captured console errors with feedback')
82
+ .option('--no-allow-console-errors', 'Hide the console error sharing option')
83
+ .option('--error-tracking', 'Enable automatic browser error tracking')
84
+ .option('--no-error-tracking', 'Disable automatic browser error tracking')
80
85
  .option('--z-index <value>', 'Widget z-index')
81
86
  .option('--guided', 'Enable guided feedback types')
82
87
  .option('--disable-guided', 'Disable guided feedback types')
@@ -84,20 +89,26 @@ export function createWidgetCommand(getWriter) {
84
89
  const writer = getWriter();
85
90
  const client = requireClient();
86
91
  const projectId = await resolveProjectId(client, projectArg);
87
- const hasUpdates = opts.color || opts.label || opts.position || opts.intro ||
92
+ const hasUpdates = opts.captureMode || opts.color || opts.label || opts.position || opts.intro ||
88
93
  opts.success || opts.trigger || opts.display ||
89
94
  opts.buttonRadius || opts.buttonSize || opts.icon ||
90
95
  opts.emailRequired !== undefined || opts.showEmail !== undefined ||
91
96
  opts.emailReadOnly !== undefined || opts.hideEmailWhenPrefilled !== undefined ||
92
97
  opts.allowAttachments !== undefined || opts.iconOnly !== undefined ||
93
98
  opts.showIcon !== undefined || opts.showBranding !== undefined ||
99
+ opts.allowConsoleErrors !== undefined || opts.errorTracking !== undefined ||
94
100
  opts.zIndex || opts.guided || opts.disableGuided;
95
101
  if (hasUpdates) {
96
102
  if (opts.guided && opts.disableGuided) {
97
103
  throw errUsage('Choose either --guided or --disable-guided, not both');
98
104
  }
105
+ if (opts.captureMode && !['feedback', 'waitlist'].includes(opts.captureMode)) {
106
+ throw errUsage('Capture mode must be feedback or waitlist');
107
+ }
99
108
  // Update mode
100
109
  const settings = {};
110
+ if (opts.captureMode)
111
+ settings.captureMode = opts.captureMode;
101
112
  if (opts.color)
102
113
  settings.buttonColor = opts.color;
103
114
  if (opts.label)
@@ -134,6 +145,10 @@ export function createWidgetCommand(getWriter) {
134
145
  settings.showIcon = opts.showIcon;
135
146
  if (opts.showBranding !== undefined)
136
147
  settings.showBranding = opts.showBranding;
148
+ if (opts.allowConsoleErrors !== undefined)
149
+ settings.allowConsoleErrors = opts.allowConsoleErrors;
150
+ if (opts.errorTracking !== undefined)
151
+ settings.errorTrackingEnabled = opts.errorTracking;
137
152
  if (opts.zIndex)
138
153
  settings.zIndex = parseInt(opts.zIndex, 10);
139
154
  if (opts.guided || opts.disableGuided) {
@@ -253,9 +268,26 @@ export function createWidgetCommand(getWriter) {
253
268
  console.log();
254
269
  console.log(brand.muted(` Script URL: ${result.scriptUrl}`));
255
270
  console.log();
256
- renderInlineTriggerHelp(settingsResult.settings);
271
+ if (settingsResult.settings.captureMode === 'waitlist') {
272
+ renderWaitlistHelp();
273
+ }
274
+ else {
275
+ renderInlineTriggerHelp(settingsResult.settings);
276
+ }
257
277
  }
258
- writer.ok(result, {
278
+ const output = result.captureMode === 'waitlist'
279
+ ? {
280
+ ...result,
281
+ waitlist: {
282
+ formAttribute: 'data-feedbackbasket-waitlist',
283
+ requiredField: 'email',
284
+ optionalField: 'name',
285
+ stateAttribute: 'data-feedbackbasket-state',
286
+ events: ['feedbackbasket:waitlist:success', 'feedbackbasket:waitlist:error'],
287
+ },
288
+ }
289
+ : result;
290
+ writer.ok(output, {
259
291
  summary: `Embed code for "${result.projectName}"`,
260
292
  breadcrumbs: [
261
293
  { action: 'Customize widget', cmd: `feedbackbasket widget settings ${projectId}` },
@@ -362,6 +394,23 @@ function renderInlineTriggerHelp(settings) {
362
394
  console.log(brand.muted(' Modal mode stays centered; passing the trigger is safe for future popup changes.'));
363
395
  }
364
396
  console.log(brand.muted(' Existing calls to window.FeedbackWidget.openFeedbackForm() still work.'));
397
+ console.log(brand.muted(' Do not call internal methods such as open() or openModal().'));
398
+ console.log();
399
+ }
400
+ function renderWaitlistHelp() {
401
+ console.log(brand.bold('Waitlist form setup'));
402
+ console.log(divider(50));
403
+ console.log();
404
+ console.log(brand.muted(' Keep the project script installed and annotate your own form:'));
405
+ console.log();
406
+ console.log(` ${brand.primary('<form data-feedbackbasket-waitlist>')}`);
407
+ console.log(` ${brand.primary(' <input name="name" autocomplete="name">')}`);
408
+ console.log(` ${brand.primary(' <input name="email" type="email" autocomplete="email" required>')}`);
409
+ console.log(` ${brand.primary(' <button type="submit">Join the waitlist</button>')}`);
410
+ console.log(` ${brand.primary('</form>')}`);
411
+ console.log();
412
+ console.log(brand.muted(' Email is required; name is optional. The script handles submission without replacing your styling.'));
413
+ console.log(brand.muted(' Read data-feedbackbasket-state for loading, success, and error UI.'));
365
414
  console.log();
366
415
  }
367
416
  function renderWidgetSettings(projectName, settings) {
@@ -369,6 +418,7 @@ function renderWidgetSettings(projectName, settings) {
369
418
  console.log(divider(40));
370
419
  console.log();
371
420
  const display = [
421
+ ['Capture Mode', String(settings.captureMode ?? 'feedback')],
372
422
  ['Button Color', String(settings.buttonColor ?? '')],
373
423
  ['Button Label', String(settings.buttonLabel ?? '')],
374
424
  ['Button Radius', String(settings.buttonRadius ?? '')],
@@ -389,6 +439,8 @@ function renderWidgetSettings(projectName, settings) {
389
439
  ['Success Message', String(settings.successMessage ?? '')],
390
440
  ['Z-Index', String(settings.zIndex ?? '')],
391
441
  ['Show Branding', String(settings.showBranding ?? true)],
442
+ ['Share Console Errors', String(settings.allowConsoleErrors ?? false)],
443
+ ['Error Tracking', String(settings.errorTrackingEnabled ?? false)],
392
444
  ];
393
445
  for (const [label, value] of display) {
394
446
  console.log(` ${brand.label(label.padEnd(18))} ${value}`);
package/dist/src/help.js CHANGED
@@ -17,7 +17,7 @@ export function renderRootHelp() {
17
17
  lines.push(` ${logo()} CLI ${brand.muted(`v${VERSION}`)}`);
18
18
  lines.push('');
19
19
  lines.push(` ${brand.muted('The command-line interface for FeedbackBasket.')}`);
20
- lines.push(` ${brand.muted('Manage projects, feedback, widgets, and team from your terminal.')}`);
20
+ lines.push(` ${brand.muted('Manage projects, feedback, waitlists, widgets, and team from your terminal.')}`);
21
21
  lines.push('');
22
22
  // Core Commands
23
23
  lines.push(section(' CORE COMMANDS'));
@@ -25,6 +25,7 @@ export function renderRootHelp() {
25
25
  lines.push(cmd('feedback', 'View and manage feedback'));
26
26
  lines.push(cmd('bugs', 'View bug reports with severity'));
27
27
  lines.push(cmd('widget', 'Manage feedback widget & get embed code'));
28
+ lines.push(cmd('waitlist', 'View and export waitlist signups'));
28
29
  lines.push(cmd('team', 'Manage organization members'));
29
30
  lines.push('');
30
31
  // Shortcuts
@@ -36,6 +37,7 @@ export function renderRootHelp() {
36
37
  lines.push(section(' SEARCH & EXPORT'));
37
38
  lines.push(cmd('feedback search', 'Search feedback across projects'));
38
39
  lines.push(cmd('feedback export', 'Export feedback to CSV, Markdown, or JSON'));
40
+ lines.push(cmd('waitlist export', 'Export waitlist signups to CSV'));
39
41
  lines.push(cmd('bugs stats', 'Bug statistics summary'));
40
42
  lines.push('');
41
43
  // Auth & Config
@@ -62,6 +64,8 @@ export function renderRootHelp() {
62
64
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket bugs list --severity high`);
63
65
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget script myapp`);
64
66
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --display modal`);
67
+ lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --capture-mode waitlist`);
68
+ lines.push(`${INDENT}${brand.muted('$')} feedbackbasket waitlist list myapp --search "@example.com"`);
65
69
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects create "My App" --url https://myapp.com`);
66
70
  lines.push('');
67
71
  // Learn More
@@ -24,6 +24,7 @@ export interface FeedbackFlowSettings {
24
24
  types: FeedbackFlowType[];
25
25
  }
26
26
  export interface WidgetSettings {
27
+ captureMode?: 'feedback' | 'waitlist';
27
28
  widgetType?: string;
28
29
  triggerMode?: 'floating' | 'inline';
29
30
  buttonColor?: string;
@@ -44,6 +45,8 @@ export interface WidgetSettings {
44
45
  displayMode?: 'modal' | 'popup';
45
46
  zIndex?: number;
46
47
  showBranding?: boolean;
48
+ allowConsoleErrors?: boolean;
49
+ errorTrackingEnabled?: boolean;
47
50
  feedbackFlow?: FeedbackFlowSettings;
48
51
  }
49
52
  export interface Project {
@@ -124,6 +127,25 @@ export interface Pagination {
124
127
  offset: number;
125
128
  hasMore: boolean;
126
129
  }
130
+ export interface WaitlistEntry {
131
+ id: string;
132
+ email: string;
133
+ name?: string | null;
134
+ pageUrl?: string | null;
135
+ referrerUrl?: string | null;
136
+ createdAt: string;
137
+ updatedAt: string;
138
+ }
139
+ export interface WaitlistResponse {
140
+ project: {
141
+ id: string;
142
+ name: string;
143
+ captureMode: 'feedback' | 'waitlist';
144
+ };
145
+ entries: WaitlistEntry[];
146
+ totalSignups: number;
147
+ pagination: Pagination;
148
+ }
127
149
  export interface FeedbackResponse {
128
150
  feedback: Feedback[];
129
151
  pagination: Pagination;
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.9.3";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.3";
1
+ export declare const VERSION = "0.10.0";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.10.0";
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.9.3';
1
+ export const VERSION = '0.10.0';
2
2
  export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "feedbackbasket-cli",
3
- "version": "0.9.3",
4
- "description": "Command-line interface for FeedbackBasket — manage feedback from your terminal",
3
+ "version": "0.10.0",
4
+ "description": "Command-line interface for FeedbackBasket — manage feedback and waitlists from your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/src/cli.js",
7
7
  "bin": {
@@ -28,14 +28,13 @@
28
28
  },
29
29
  "homepage": "https://feedbackbasket.com",
30
30
  "dependencies": {
31
- "axios": "1.7.0",
32
31
  "chalk": "^5.3.0",
33
32
  "commander": "^13.1.0",
34
33
  "open": "^10.1.0"
35
34
  },
36
35
  "devDependencies": {
37
36
  "@types/node": "^22.0.0",
38
- "tsx": "^4.0.0",
37
+ "tsx": "^4.23.0",
39
38
  "typescript": "^5.7.0"
40
39
  },
41
40
  "engines": {
@@ -1,21 +1,11 @@
1
1
  ---
2
- name: FeedbackBasket
3
- description: Manage FeedbackBasket projects, feedback, bugs, widgets, and team from the command line
4
- triggers:
5
- - feedbackbasket
6
- - feedback
7
- - bugs
8
- - bug reports
9
- - user feedback
10
- - widget
11
- - feedback widget
12
- invocable: true
13
- argument-hint: "<command> [options]"
2
+ name: feedbackbasket
3
+ description: Manage FeedbackBasket projects, feedback, bugs, feedback widgets, waitlist capture, and teams from the command line. Use when an agent needs to configure FeedbackBasket, collect feedback or waitlist signups, query feedback, or manage a FeedbackBasket project.
14
4
  ---
15
5
 
16
6
  # FeedbackBasket CLI
17
7
 
18
- Full command-line interface for managing feedback, bug reports, projects, widgets, and team in FeedbackBasket. Works with any AI agent that can run shell commands.
8
+ Full command-line interface for managing feedback, waitlist signups, bug reports, projects, widgets, and teams in FeedbackBasket. Works with any AI agent that can run shell commands.
19
9
 
20
10
  ## Authentication
21
11
 
@@ -63,6 +53,8 @@ All project commands accept **name or ID**. Names are matched case-insensitively
63
53
 
64
54
  **Project URL rule for agents:** confirm the real website URL before creating or updating a project. Never use `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, or a local dev server URL unless the user explicitly says the project is only for local testing. If the repo only exposes a local URL, ask for the production, staging, preview, or intended public URL. Do not guess a public domain from package names, git remotes, or environment variables. For an explicitly local-only test project, pass `--allow-local-url`.
65
55
 
56
+ **Capture-mode decision:** if the user asks for feedback, a feedback bubble, bug reports, or feature requests, use `--capture-mode feedback`. If they ask for a waitlist, launch list, early access, or email capture, use `--capture-mode waitlist`. If they ask to set up FeedbackBasket without choosing, explain both options and ask which they want. Do not switch an existing project without confirmation because only one capture mode is active at a time.
57
+
66
58
  ### Feedback
67
59
  ```bash
68
60
  # Read
@@ -104,6 +96,8 @@ feedbackbasket widget script <project>
104
96
 
105
97
  # View settings
106
98
  feedbackbasket widget settings <project>
99
+ feedbackbasket widget settings <project> --capture-mode waitlist
100
+ feedbackbasket widget settings <project> --capture-mode feedback
107
101
 
108
102
  # Customize
109
103
  feedbackbasket widget settings <project> --color "#22c55e" --label "Send Feedback"
@@ -111,6 +105,7 @@ feedbackbasket widget settings <project> --position bottom-left --display modal
111
105
  feedbackbasket widget settings <project> --email-required --intro "How can we improve?"
112
106
  feedbackbasket widget settings <project> --show-email --allow-attachments
113
107
  feedbackbasket widget settings <project> --email-read-only --hide-email-when-prefilled
108
+ feedbackbasket widget settings <project> --error-tracking --allow-console-errors
114
109
 
115
110
  # Guided feedback types and follow-up questions
116
111
  feedbackbasket widget flow <project>
@@ -119,6 +114,29 @@ feedbackbasket widget flow <project> --reset-default --enable # only when the u
119
114
  feedbackbasket widget flow <project> --config ./feedback-flow.json
120
115
  ```
121
116
 
117
+ Waitlist mode keeps the same project script and binds to the host app's own annotated form:
118
+
119
+ ```html
120
+ <form data-feedbackbasket-waitlist>
121
+ <input name="name" autocomplete="name">
122
+ <input name="email" type="email" autocomplete="email" required>
123
+ <button type="submit">Join the waitlist</button>
124
+ </form>
125
+ ```
126
+
127
+ Email is required and name is optional. Use `data-feedbackbasket-state="loading|success|error"` for custom UI, or listen for the bubbling `feedbackbasket:waitlist:success` and `feedbackbasket:waitlist:error` events. Do not add a competing submit handler.
128
+
129
+ ### Waitlist Signups
130
+
131
+ ```bash
132
+ feedbackbasket waitlist list <project>
133
+ feedbackbasket waitlist list <project> --search "@example.com" --limit 50 --offset 0
134
+ feedbackbasket waitlist list <project> --agent
135
+ feedbackbasket waitlist export <project>
136
+ ```
137
+
138
+ Agent output includes signup emails, optional names, captured/referrer pages, total counts, active capture mode, and pagination. Use `waitlist export` for the same CSV export available in the dashboard.
139
+
122
140
  For inline trigger mode, load the widget once and call the public API from the host app's custom button:
123
141
 
124
142
  ```html
@@ -137,6 +155,8 @@ In React:
137
155
 
138
156
  Passing the trigger element lets popup mode open beside the custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
139
157
 
158
+ Use only the public `openFeedbackForm()` API from the snippet. Do not call internal or undocumented methods such as `open()`, `openModal()`, or direct modal element manipulation; those can exist in the widget bundle but are not stable integration points.
159
+
140
160
  `email-read-only` and `hide-email-when-prefilled` control behavior only when the host app passes a runtime `userEmail` value. Do not store visitor emails in widget settings.
141
161
 
142
162
  Use the basic widget experience by default: `displayMode` stays `modal`, and guided feedback stays disabled. Ask the user before switching to `popup` or enabling guided feedback. If the user does not care, keep modal + basic feedback.
@@ -168,6 +188,8 @@ feedbackbasket projects create "My App" --url https://myapp.com --agent
168
188
  feedbackbasket widget script "My App" --agent
169
189
  # Agent gets the embed code, adds it to the HTML
170
190
  feedbackbasket widget settings "My App" --color "#22c55e" --label "Feedback" --agent
191
+ # Optional, when the user wants a waitlist instead of feedback capture
192
+ # feedbackbasket widget settings "My App" --capture-mode waitlist --agent
171
193
  # Optional, only when requested: enable the guided wizard with Bug, Feature, and General templates
172
194
  # feedbackbasket widget flow "My App" --reset-default --enable --agent
173
195
  ```