feedbackbasket-cli 0.4.0 → 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.
package/README.md CHANGED
@@ -16,7 +16,7 @@ feedbackbasket login
16
16
  # Or use a token directly (for CI/headless)
17
17
  feedbackbasket login --token fb_cli_your_token_here
18
18
 
19
- # Remote server/headless browser flow (paste token back into terminal)
19
+ # Remote server flow: use when localhost browser callbacks cannot reach the CLI
20
20
  feedbackbasket login --manual
21
21
 
22
22
  # Start exploring
@@ -51,7 +51,7 @@ feedbackbasket setup claude
51
51
 
52
52
  ```bash
53
53
  feedbackbasket login # Browser OAuth flow (alias for auth login)
54
- feedbackbasket login --manual # Remote/headless browser token paste
54
+ feedbackbasket login --manual # No localhost browser callback (remote servers)
55
55
  feedbackbasket login --token <token> # Use an existing CLI token (CI / scripts)
56
56
  feedbackbasket logout # Clear credentials (alias for auth logout)
57
57
  feedbackbasket auth status # Show auth state, scope, default project
@@ -121,11 +121,43 @@ feedbackbasket widget settings myapp
121
121
  feedbackbasket widget settings myapp --color "#22c55e" --label "Send Feedback"
122
122
  feedbackbasket widget settings myapp --position bottom-left --display modal
123
123
  feedbackbasket widget settings myapp --email-required --intro "How can we improve?"
124
+ feedbackbasket widget settings myapp --button-radius 10 --button-size regular
125
+ feedbackbasket widget settings myapp --show-email --allow-attachments --guided
126
+
127
+ # Configure guided feedback types and follow-up questions
128
+ feedbackbasket widget flow myapp
129
+ feedbackbasket widget flow myapp --enable
130
+ feedbackbasket widget flow myapp --reset-default --enable
131
+ feedbackbasket widget flow myapp --config ./feedback-flow.json
124
132
 
125
133
  # Get embed code (ready to paste into your HTML)
126
134
  feedbackbasket widget script myapp
127
135
  ```
128
136
 
137
+ `widget flow --config` accepts either a `feedbackFlow` object or a JSON object with a `feedbackFlow` key. V1 supports guided mode with `text`, `textarea`, and `single_choice` follow-up questions.
138
+
139
+ ```json
140
+ {
141
+ "enabled": true,
142
+ "mode": "guided",
143
+ "types": [
144
+ {
145
+ "id": "bug",
146
+ "emoji": "🐞",
147
+ "label": "Bug report",
148
+ "description": "Something is broken or not working",
149
+ "questions": [
150
+ {
151
+ "id": "steps",
152
+ "label": "What steps can reproduce it?",
153
+ "type": "textarea"
154
+ }
155
+ ]
156
+ }
157
+ ]
158
+ }
159
+ ```
160
+
129
161
  ### Team
130
162
 
131
163
  ```bash
@@ -108,7 +108,8 @@ export async function browserLogin(baseUrl, scope = 'read') {
108
108
  console.log(` ${brand.primary(authorizeUrl)}`);
109
109
  console.log();
110
110
  console.log(` ${brand.muted('Waiting for authentication...')}`);
111
- console.log(` ${brand.muted('On remote machines, use: feedbackbasket login --manual')}`);
111
+ console.log(` ${brand.muted('If this machine cannot receive the localhost browser callback, use:')}`);
112
+ console.log(` ${brand.command('feedbackbasket login --manual')}`);
112
113
  console.log();
113
114
  open(authorizeUrl).catch(() => {
114
115
  // Browser open failed; user will use the URL manually.
@@ -121,10 +122,13 @@ export async function manualLogin(baseUrl, scope = 'read') {
121
122
  console.log();
122
123
  console.log(` ${logo()} CLI`);
123
124
  console.log();
124
- console.log(` ${brand.primary('Open this URL to create a CLI token:')}`);
125
+ console.log(` ${brand.primary('Open this URL on any machine with a browser:')}`);
125
126
  console.log();
126
127
  console.log(` ${brand.primary(authorizeUrl)}`);
127
128
  console.log();
129
+ console.log(` ${brand.muted('Use this when a remote server cannot receive the localhost browser callback.')}`);
130
+ console.log(` ${brand.muted('The CLI still needs outbound HTTPS access to verify and use the token.')}`);
131
+ console.log();
128
132
  console.log(` ${brand.muted('After approving access, paste the token shown in your browser.')}`);
129
133
  console.log();
130
134
  open(authorizeUrl).catch(() => {
@@ -1,4 +1,4 @@
1
- import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, BugReportParams, UserProfile, Project, Feedback } from './types.js';
1
+ import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
2
2
  export declare class FeedbackBasketClient {
3
3
  private http;
4
4
  constructor(token: string, baseUrl: string);
@@ -37,12 +37,12 @@ export declare class FeedbackBasketClient {
37
37
  getWidgetSettings(projectId: string): Promise<{
38
38
  projectId: string;
39
39
  projectName: string;
40
- settings: Record<string, unknown>;
40
+ settings: WidgetSettings;
41
41
  }>;
42
- updateWidgetSettings(projectId: string, settings: Record<string, unknown>): Promise<{
42
+ updateWidgetSettings(projectId: string, settings: Partial<WidgetSettings>): Promise<{
43
43
  projectId: string;
44
44
  projectName: string;
45
- settings: Record<string, unknown>;
45
+ settings: WidgetSettings;
46
46
  }>;
47
47
  getWidgetScript(projectId: string): Promise<{
48
48
  projectId: string;
@@ -18,7 +18,7 @@ export function createAuthCommand(getWriter) {
18
18
  .command('login')
19
19
  .description('Authenticate with FeedbackBasket')
20
20
  .option('--token <token>', 'Use an API token directly (for CI/headless)')
21
- .option('--manual', 'Show a browser token to paste back into the terminal')
21
+ .option('--manual', 'Authenticate without a localhost browser callback')
22
22
  .option('--scope <scope>', 'Access scope: read or full', 'full')
23
23
  .action(async (opts) => {
24
24
  const writer = getWriter();
@@ -299,7 +299,7 @@ export function createLoginCommand(getWriter) {
299
299
  return new Command('login')
300
300
  .description('Authenticate with FeedbackBasket (alias for auth login)')
301
301
  .option('--token <token>', 'Use an API token directly (for CI/headless)')
302
- .option('--manual', 'Show a browser token to paste back into the terminal')
302
+ .option('--manual', 'Authenticate without a localhost browser callback')
303
303
  .option('--scope <scope>', 'Access scope: read or full', 'full')
304
304
  .action(async (opts) => {
305
305
  // Delegate to auth login by re-parsing
@@ -171,6 +171,7 @@ function renderFeedbackDetail(item) {
171
171
  const fields = [
172
172
  ['Status', item.status],
173
173
  ['Category', item.category],
174
+ ['Feedback Type', formatFeedbackType(item)],
174
175
  ['Sentiment', item.sentiment],
175
176
  ['Priority', item.aiPriorityScore != null ? String(item.aiPriorityScore) : null],
176
177
  ['Email', item.email],
@@ -190,6 +191,15 @@ function renderFeedbackDetail(item) {
190
191
  console.log();
191
192
  console.log(brand.bold('Content:'));
192
193
  console.log(` ${item.content}`);
194
+ if (item.followUpAnswers && item.followUpAnswers.length > 0) {
195
+ console.log();
196
+ console.log(brand.bold('Follow-up Answers:'));
197
+ for (const answer of item.followUpAnswers) {
198
+ const value = answer.value?.trim() || brand.muted('(skipped)');
199
+ console.log(` ${brand.bold(answer.label)}`);
200
+ console.log(` ${value}`);
201
+ }
202
+ }
193
203
  if (item.aiSummary) {
194
204
  console.log();
195
205
  console.log(brand.bold('AI Summary:'));
@@ -209,3 +219,11 @@ function renderFeedbackDetail(item) {
209
219
  }
210
220
  }
211
221
  }
222
+ function formatFeedbackType(item) {
223
+ if (!item.feedbackType)
224
+ return undefined;
225
+ const label = item.feedbackType.label || item.feedbackType.id;
226
+ if (!label)
227
+ return undefined;
228
+ return item.feedbackType.emoji ? `${item.feedbackType.emoji} ${label}` : label;
229
+ }
@@ -1,10 +1,49 @@
1
1
  import { Command } from 'commander';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { FeedbackBasketClient } from '../client.js';
3
4
  import { AuthManager } from '../auth/manager.js';
4
5
  import { loadConfig } from '../config/config.js';
5
6
  import { errAuth, errUsage } from '../output/errors.js';
6
7
  import { brand, divider } from '../output/theme.js';
7
8
  import { resolveProject } from '../resolve.js';
9
+ const DEFAULT_FEEDBACK_FLOW = {
10
+ enabled: false,
11
+ mode: 'guided',
12
+ types: [
13
+ {
14
+ id: 'bug',
15
+ emoji: '🐞',
16
+ label: 'Bug report',
17
+ description: 'Something is broken or not working',
18
+ questions: [
19
+ { id: 'steps', label: 'What steps can reproduce it?', type: 'textarea', placeholder: 'Tell us what you did before the issue happened.' },
20
+ { id: 'expected', label: 'What did you expect to happen?', type: 'textarea', placeholder: 'Describe the result you expected.' },
21
+ { id: 'urgency', label: 'How urgent is this?', type: 'single_choice', options: ['Low', 'Medium', 'High'] },
22
+ ],
23
+ },
24
+ {
25
+ id: 'feature',
26
+ emoji: '💡',
27
+ label: 'Feature request',
28
+ description: 'Suggest an idea or improvement',
29
+ questions: [
30
+ { id: 'problem', label: 'What problem are you trying to solve?', type: 'textarea', placeholder: 'Share the job this feature would help with.' },
31
+ { id: 'benefit', label: 'Who would benefit from this?', type: 'text', placeholder: 'For example: admins, customers, teammates.' },
32
+ { id: 'importance', label: 'How important is this?', type: 'single_choice', options: ['Nice to have', 'Important', 'Critical'] },
33
+ ],
34
+ },
35
+ {
36
+ id: 'general',
37
+ emoji: '💬',
38
+ label: 'General feedback',
39
+ description: 'Share thoughts, praise, or anything else',
40
+ questions: [
41
+ { id: 'context', label: 'What made you want to share this?', type: 'textarea', placeholder: 'Add any helpful context.' },
42
+ { id: 'sentiment', label: 'How are you feeling about it?', type: 'single_choice', options: ['Happy', 'Neutral', 'Frustrated'] },
43
+ ],
44
+ },
45
+ ],
46
+ };
8
47
  export function createWidgetCommand(getWriter) {
9
48
  const widget = new Command('widget')
10
49
  .description('Manage feedback widget');
@@ -14,23 +53,44 @@ export function createWidgetCommand(getWriter) {
14
53
  .description('View or update widget settings')
15
54
  .option('--color <hex>', 'Button color (e.g. #22c55e)')
16
55
  .option('--label <text>', 'Button label')
17
- .option('--position <pos>', 'Widget position (bottom-right, bottom-left, top-right, top-left)')
56
+ .option('--position <pos>', 'Widget position (bottom-right, bottom-left, middle-right-edge, middle-left-edge, bottom-right-edge, bottom-left-edge)')
18
57
  .option('--intro <text>', 'Intro message shown in the widget')
19
58
  .option('--success <text>', 'Success message after submission')
20
- .option('--trigger <mode>', 'Trigger mode (floating, manual)')
21
- .option('--display <mode>', 'Display mode (modal, popover)')
59
+ .option('--trigger <mode>', 'Trigger mode (floating, inline)')
60
+ .option('--display <mode>', 'Display mode (modal, popup)')
61
+ .option('--button-radius <px>', 'Button radius in pixels')
62
+ .option('--button-size <size>', 'Button size (mini, regular)')
63
+ .option('--icon <value>', 'Icon value (emoji or svg:chat-bubble-bottom-center-text)')
22
64
  .option('--email-required', 'Require email from submitters')
23
65
  .option('--no-email-required', 'Make email optional')
66
+ .option('--show-email', 'Show the email field')
67
+ .option('--no-show-email', 'Hide the email field')
68
+ .option('--allow-attachments', 'Allow image attachments')
69
+ .option('--no-allow-attachments', 'Disable image attachments')
24
70
  .option('--icon-only', 'Show only the icon, no label')
25
71
  .option('--no-icon-only', 'Show both icon and label')
72
+ .option('--show-icon', 'Show an icon next to the label')
73
+ .option('--no-show-icon', 'Hide the icon')
74
+ .option('--show-branding', 'Show FeedbackBasket branding')
75
+ .option('--no-show-branding', 'Hide FeedbackBasket branding when plan allows it')
76
+ .option('--z-index <value>', 'Widget z-index')
77
+ .option('--guided', 'Enable guided feedback types')
78
+ .option('--disable-guided', 'Disable guided feedback types')
26
79
  .action(async (projectArg, opts) => {
27
80
  const writer = getWriter();
28
81
  const client = requireClient();
29
82
  const projectId = await resolveProjectId(client, projectArg);
30
83
  const hasUpdates = opts.color || opts.label || opts.position || opts.intro ||
31
84
  opts.success || opts.trigger || opts.display ||
32
- opts.emailRequired !== undefined || opts.iconOnly !== undefined;
85
+ opts.buttonRadius || opts.buttonSize || opts.icon ||
86
+ opts.emailRequired !== undefined || opts.showEmail !== undefined ||
87
+ opts.allowAttachments !== undefined || opts.iconOnly !== undefined ||
88
+ opts.showIcon !== undefined || opts.showBranding !== undefined ||
89
+ opts.zIndex || opts.guided || opts.disableGuided;
33
90
  if (hasUpdates) {
91
+ if (opts.guided && opts.disableGuided) {
92
+ throw errUsage('Choose either --guided or --disable-guided, not both');
93
+ }
34
94
  // Update mode
35
95
  const settings = {};
36
96
  if (opts.color)
@@ -47,10 +107,33 @@ export function createWidgetCommand(getWriter) {
47
107
  settings.triggerMode = opts.trigger;
48
108
  if (opts.display)
49
109
  settings.displayMode = opts.display;
110
+ if (opts.buttonRadius)
111
+ settings.buttonRadius = parseInt(opts.buttonRadius, 10);
112
+ if (opts.buttonSize)
113
+ settings.buttonSize = opts.buttonSize;
114
+ if (opts.icon)
115
+ settings.icon = opts.icon;
50
116
  if (opts.emailRequired !== undefined)
51
117
  settings.emailRequired = opts.emailRequired;
118
+ if (opts.showEmail !== undefined)
119
+ settings.showEmailField = opts.showEmail;
120
+ if (opts.allowAttachments !== undefined)
121
+ settings.allowAttachments = opts.allowAttachments;
52
122
  if (opts.iconOnly !== undefined)
53
123
  settings.iconOnly = opts.iconOnly;
124
+ if (opts.showIcon !== undefined)
125
+ settings.showIcon = opts.showIcon;
126
+ if (opts.showBranding !== undefined)
127
+ settings.showBranding = opts.showBranding;
128
+ if (opts.zIndex)
129
+ settings.zIndex = parseInt(opts.zIndex, 10);
130
+ if (opts.guided || opts.disableGuided) {
131
+ const current = await client.getWidgetSettings(projectId);
132
+ settings.feedbackFlow = {
133
+ ...(current.settings.feedbackFlow ?? DEFAULT_FEEDBACK_FLOW),
134
+ enabled: Boolean(opts.guided),
135
+ };
136
+ }
54
137
  const result = await client.updateWidgetSettings(projectId, settings);
55
138
  if (!writer.isMachineOutput()) {
56
139
  console.log(` ${brand.success('✓')} Widget settings updated for ${brand.bold(result.projectName)}`);
@@ -61,6 +144,7 @@ export function createWidgetCommand(getWriter) {
61
144
  breadcrumbs: [
62
145
  { action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
63
146
  { action: 'View settings', cmd: `feedbackbasket widget settings ${projectId}` },
147
+ { action: 'Configure guided questions', cmd: `feedbackbasket widget flow ${projectId} --enable` },
64
148
  ],
65
149
  });
66
150
  }
@@ -74,10 +158,71 @@ export function createWidgetCommand(getWriter) {
74
158
  summary: `Widget settings for "${result.projectName}"`,
75
159
  breadcrumbs: [
76
160
  { action: 'Update color', cmd: `feedbackbasket widget settings ${projectId} --color "#22c55e"` },
161
+ { action: 'Enable guided flow', cmd: `feedbackbasket widget flow ${projectId} --enable` },
162
+ { action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
163
+ ],
164
+ });
165
+ }
166
+ });
167
+ // --- widget flow ---
168
+ widget
169
+ .command('flow [project]')
170
+ .description('View or update guided feedback types and follow-up questions')
171
+ .option('--enable', 'Enable guided feedback flow')
172
+ .option('--disable', 'Disable guided feedback flow')
173
+ .option('--reset-default', 'Reset guided flow to the default Bug, Feature, and General templates')
174
+ .option('--config <path>', 'Path to a JSON file containing a feedbackFlow object')
175
+ .action(async (projectArg, opts) => {
176
+ const writer = getWriter();
177
+ const client = requireClient();
178
+ const projectId = await resolveProjectId(client, projectArg);
179
+ const hasUpdates = opts.enable || opts.disable || opts.resetDefault || opts.config;
180
+ if (hasUpdates) {
181
+ if (opts.enable && opts.disable) {
182
+ throw errUsage('Choose either --enable or --disable, not both');
183
+ }
184
+ const current = await client.getWidgetSettings(projectId);
185
+ let feedbackFlow = current.settings.feedbackFlow ?? DEFAULT_FEEDBACK_FLOW;
186
+ if (opts.resetDefault) {
187
+ feedbackFlow = cloneDefaultFeedbackFlow();
188
+ }
189
+ if (opts.config) {
190
+ feedbackFlow = parseFeedbackFlowConfig(opts.config);
191
+ }
192
+ if (opts.enable)
193
+ feedbackFlow = { ...feedbackFlow, enabled: true };
194
+ if (opts.disable)
195
+ feedbackFlow = { ...feedbackFlow, enabled: false };
196
+ const result = await client.updateWidgetSettings(projectId, { feedbackFlow });
197
+ if (!writer.isMachineOutput()) {
198
+ console.log(` ${brand.success('✓')} Guided feedback flow updated for ${brand.bold(result.projectName)}`);
199
+ console.log();
200
+ renderFeedbackFlow(result.settings.feedbackFlow);
201
+ }
202
+ writer.ok(result.settings.feedbackFlow, {
203
+ summary: `Updated guided feedback flow for "${result.projectName}"`,
204
+ breadcrumbs: [
205
+ { action: 'View widget settings', cmd: `feedbackbasket widget settings ${projectId}` },
77
206
  { action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
78
207
  ],
79
208
  });
209
+ return;
210
+ }
211
+ const result = await client.getWidgetSettings(projectId);
212
+ if (!writer.isMachineOutput()) {
213
+ console.log(brand.bold(`Guided feedback flow — ${result.projectName}`));
214
+ console.log(divider(40));
215
+ console.log();
216
+ renderFeedbackFlow(result.settings.feedbackFlow);
80
217
  }
218
+ writer.ok(result.settings.feedbackFlow ?? DEFAULT_FEEDBACK_FLOW, {
219
+ summary: `Guided feedback flow for "${result.projectName}"`,
220
+ breadcrumbs: [
221
+ { action: 'Enable guided flow', cmd: `feedbackbasket widget flow ${projectId} --enable` },
222
+ { action: 'Reset templates', cmd: `feedbackbasket widget flow ${projectId} --reset-default --enable` },
223
+ { action: 'Apply JSON config', cmd: `feedbackbasket widget flow ${projectId} --config ./feedback-flow.json` },
224
+ ],
225
+ });
81
226
  });
82
227
  // --- widget script ---
83
228
  widget
@@ -127,6 +272,66 @@ async function resolveProjectId(client, projectArg) {
127
272
  return config.defaultProject;
128
273
  throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket widget settings <project> or feedbackbasket config set defaultProject <id>');
129
274
  }
275
+ function cloneDefaultFeedbackFlow() {
276
+ return JSON.parse(JSON.stringify(DEFAULT_FEEDBACK_FLOW));
277
+ }
278
+ function parseFeedbackFlowConfig(path) {
279
+ const raw = readFileSync(path, 'utf8');
280
+ const parsed = JSON.parse(raw);
281
+ const candidate = isRecord(parsed) && isRecord(parsed.feedbackFlow)
282
+ ? parsed.feedbackFlow
283
+ : parsed;
284
+ if (!isRecord(candidate) || !Array.isArray(candidate.types)) {
285
+ throw errUsage('Feedback flow config must be a feedbackFlow object with a types array');
286
+ }
287
+ return {
288
+ enabled: typeof candidate.enabled === 'boolean' ? candidate.enabled : true,
289
+ mode: 'guided',
290
+ types: candidate.types.map((type, index) => {
291
+ if (!isRecord(type))
292
+ throw errUsage(`Feedback type at index ${index} must be an object`);
293
+ const id = stringValue(type.id) || `type-${index + 1}`;
294
+ const label = stringValue(type.label);
295
+ if (!label)
296
+ throw errUsage(`Feedback type "${id}" is missing label`);
297
+ return {
298
+ id,
299
+ emoji: stringValue(type.emoji) || '💬',
300
+ label,
301
+ description: stringValue(type.description),
302
+ questions: Array.isArray(type.questions)
303
+ ? type.questions.map((question, qIndex) => {
304
+ if (!isRecord(question))
305
+ throw errUsage(`Question ${qIndex + 1} in "${id}" must be an object`);
306
+ const questionId = stringValue(question.id) || `question-${qIndex + 1}`;
307
+ const questionLabel = stringValue(question.label);
308
+ const questionType = stringValue(question.type);
309
+ if (!questionLabel)
310
+ throw errUsage(`Question "${questionId}" in "${id}" is missing label`);
311
+ if (!['text', 'textarea', 'single_choice'].includes(questionType)) {
312
+ throw errUsage(`Question "${questionId}" type must be text, textarea, or single_choice`);
313
+ }
314
+ return {
315
+ id: questionId,
316
+ label: questionLabel,
317
+ type: questionType,
318
+ placeholder: stringValue(question.placeholder) || undefined,
319
+ options: Array.isArray(question.options)
320
+ ? question.options.map(String).filter(Boolean)
321
+ : undefined,
322
+ };
323
+ })
324
+ : [],
325
+ };
326
+ }),
327
+ };
328
+ }
329
+ function isRecord(value) {
330
+ return !!value && typeof value === 'object' && !Array.isArray(value);
331
+ }
332
+ function stringValue(value) {
333
+ return typeof value === 'string' ? value.trim() : '';
334
+ }
130
335
  function renderWidgetSettings(projectName, settings) {
131
336
  console.log(brand.bold(`Widget settings — ${projectName}`));
132
337
  console.log(divider(40));
@@ -135,12 +340,17 @@ function renderWidgetSettings(projectName, settings) {
135
340
  ['Button Color', String(settings.buttonColor ?? '')],
136
341
  ['Button Label', String(settings.buttonLabel ?? '')],
137
342
  ['Button Radius', String(settings.buttonRadius ?? '')],
343
+ ['Button Size', String(settings.buttonSize ?? '')],
344
+ ['Icon', String(settings.icon ?? '')],
138
345
  ['Icon Only', String(settings.iconOnly ?? false)],
139
346
  ['Show Icon', String(settings.showIcon ?? true)],
140
347
  ['Position', String(settings.position ?? '')],
141
348
  ['Trigger Mode', String(settings.triggerMode ?? '')],
142
349
  ['Display Mode', String(settings.displayMode ?? '')],
350
+ ['Show Email', String(settings.showEmailField ?? true)],
143
351
  ['Email Required', String(settings.emailRequired ?? false)],
352
+ ['Attachments', String(settings.allowAttachments ?? true)],
353
+ ['Guided Flow', String(settings.feedbackFlow?.enabled ?? false)],
144
354
  ['Intro Message', String(settings.introMessage ?? '')],
145
355
  ['Success Message', String(settings.successMessage ?? '')],
146
356
  ['Z-Index', String(settings.zIndex ?? '')],
@@ -150,4 +360,35 @@ function renderWidgetSettings(projectName, settings) {
150
360
  console.log(` ${brand.label(label.padEnd(18))} ${value}`);
151
361
  }
152
362
  console.log();
363
+ if (settings.feedbackFlow?.enabled) {
364
+ renderFeedbackFlow(settings.feedbackFlow);
365
+ }
366
+ }
367
+ function renderFeedbackFlow(flow) {
368
+ const feedbackFlow = flow ?? DEFAULT_FEEDBACK_FLOW;
369
+ console.log(` ${brand.label('Enabled'.padEnd(18))} ${feedbackFlow.enabled}`);
370
+ console.log(` ${brand.label('Mode'.padEnd(18))} ${feedbackFlow.mode}`);
371
+ console.log();
372
+ if (feedbackFlow.types.length === 0) {
373
+ console.log(` ${brand.muted('No feedback types configured')}`);
374
+ console.log();
375
+ return;
376
+ }
377
+ for (const type of feedbackFlow.types) {
378
+ const heading = `${type.emoji ? `${type.emoji} ` : ''}${type.label} (${type.id})`;
379
+ console.log(` ${brand.bold(heading)}`);
380
+ if (type.description) {
381
+ console.log(` ${brand.muted(type.description)}`);
382
+ }
383
+ if (type.questions.length === 0) {
384
+ console.log(` ${brand.muted('No follow-up questions')}`);
385
+ }
386
+ else {
387
+ for (const question of type.questions) {
388
+ const options = question.options?.length ? ` [${question.options.join(', ')}]` : '';
389
+ console.log(` - ${question.label} ${brand.muted(`(${question.type})${options}`)}`);
390
+ }
391
+ }
392
+ console.log();
393
+ }
153
394
  }
package/dist/src/help.js CHANGED
@@ -60,6 +60,7 @@ export function renderRootHelp() {
60
60
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket feedback list --category BUG --status OPEN`);
61
61
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket bugs list --severity high`);
62
62
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget script myapp`);
63
+ lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget flow myapp --reset-default --enable`);
63
64
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects create "My App" --url https://myapp.com`);
64
65
  lines.push('');
65
66
  // Learn More
@@ -2,6 +2,47 @@ export type FeedbackStatus = 'OPEN' | 'UNDER_REVIEW' | 'PLANNED' | 'IN_PROGRESS'
2
2
  export type FeedbackCategory = 'BUG' | 'FEATURE_REQUEST' | 'IMPROVEMENT' | 'QUESTION';
3
3
  export type Sentiment = 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
4
4
  export type Severity = 'high' | 'medium' | 'low';
5
+ export type FeedbackFlowQuestionType = 'text' | 'textarea' | 'single_choice';
6
+ export interface FeedbackFlowQuestion {
7
+ id: string;
8
+ label: string;
9
+ type: FeedbackFlowQuestionType;
10
+ placeholder?: string;
11
+ options?: string[];
12
+ }
13
+ export interface FeedbackFlowType {
14
+ id: string;
15
+ emoji: string;
16
+ label: string;
17
+ description: string;
18
+ questions: FeedbackFlowQuestion[];
19
+ }
20
+ export interface FeedbackFlowSettings {
21
+ enabled: boolean;
22
+ mode: 'guided';
23
+ types: FeedbackFlowType[];
24
+ }
25
+ export interface WidgetSettings {
26
+ widgetType?: string;
27
+ triggerMode?: 'floating' | 'inline';
28
+ buttonColor?: string;
29
+ buttonRadius?: number;
30
+ buttonLabel?: string;
31
+ buttonSize?: 'mini' | 'regular';
32
+ iconOnly?: boolean;
33
+ showIcon?: boolean;
34
+ icon?: string;
35
+ introMessage?: string;
36
+ successMessage?: string;
37
+ position?: string;
38
+ showEmailField?: boolean;
39
+ emailRequired?: boolean;
40
+ allowAttachments?: boolean;
41
+ displayMode?: 'modal' | 'popup';
42
+ zIndex?: number;
43
+ showBranding?: boolean;
44
+ feedbackFlow?: FeedbackFlowSettings;
45
+ }
5
46
  export interface Project {
6
47
  id: string;
7
48
  name: string;
@@ -23,6 +64,18 @@ export interface Feedback {
23
64
  aiSummary?: string | null;
24
65
  aiPriorityScore?: number | null;
25
66
  reasoning?: string | null;
67
+ feedbackType?: {
68
+ id?: string;
69
+ emoji?: string;
70
+ label?: string;
71
+ description?: string;
72
+ } | null;
73
+ followUpAnswers?: Array<{
74
+ questionId: string;
75
+ label: string;
76
+ type: FeedbackFlowQuestionType;
77
+ value: string;
78
+ }> | null;
26
79
  pageUrl?: string | null;
27
80
  browser?: string | null;
28
81
  os?: string | null;
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.4.0";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.4.0";
1
+ export declare const VERSION = "0.6.0";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.6.0";
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.4.0';
1
+ export const VERSION = '0.6.0';
2
2
  export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedbackbasket-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Command-line interface for FeedbackBasket — manage feedback from your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/src/cli.js",
@@ -21,7 +21,7 @@ Full command-line interface for managing feedback, bug reports, projects, widget
21
21
 
22
22
  ```bash
23
23
  feedbackbasket login # Opens browser — one click, full access
24
- feedbackbasket login --manual # Remote/headless browser token paste
24
+ feedbackbasket login --manual # No localhost browser callback (remote servers)
25
25
  feedbackbasket login --token <TOKEN> # Manual token (CI/headless)
26
26
  feedbackbasket auth status # Check auth state
27
27
  feedbackbasket doctor # Full diagnostics
@@ -96,8 +96,17 @@ feedbackbasket widget settings <project>
96
96
  feedbackbasket widget settings <project> --color "#22c55e" --label "Send Feedback"
97
97
  feedbackbasket widget settings <project> --position bottom-left --display modal
98
98
  feedbackbasket widget settings <project> --email-required --intro "How can we improve?"
99
+ feedbackbasket widget settings <project> --show-email --allow-attachments --guided
100
+
101
+ # Guided feedback types and follow-up questions
102
+ feedbackbasket widget flow <project>
103
+ feedbackbasket widget flow <project> --enable
104
+ feedbackbasket widget flow <project> --reset-default --enable
105
+ feedbackbasket widget flow <project> --config ./feedback-flow.json
99
106
  ```
100
107
 
108
+ `widget flow --config` accepts either a `feedbackFlow` object or a JSON object with a `feedbackFlow` key. Use it when an agent needs to customize visitor choices such as Bug report, Feature request, and General feedback. Supported v1 question types are `text`, `textarea`, and `single_choice`.
109
+
101
110
  ### Team
102
111
  ```bash
103
112
  feedbackbasket team list
@@ -119,6 +128,8 @@ feedbackbasket projects create "My App" --url https://myapp.com --agent
119
128
  feedbackbasket widget script "My App" --agent
120
129
  # Agent gets the embed code, adds it to the HTML
121
130
  feedbackbasket widget settings "My App" --color "#22c55e" --label "Feedback" --agent
131
+ # Optional: enable the guided wizard with Bug, Feature, and General templates
132
+ feedbackbasket widget flow "My App" --reset-default --enable --agent
122
133
  ```
123
134
 
124
135
  ### Triage new feedback
@@ -133,7 +144,7 @@ feedbackbasket feedback note <id> "Reviewing — appears related to auth flow" -
133
144
  ```bash
134
145
  feedbackbasket bugs list --severity high --agent
135
146
  feedbackbasket feedback show <id> --agent
136
- # Response includes browser, OS, page URL, AI analysis, priority score
147
+ # Response includes browser, OS, page URL, submitted feedback type, follow-up answers, AI analysis, priority score
137
148
  ```
138
149
 
139
150
  ### Close the loop — reply to the submitter