feedbackbasket-cli 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,7 +83,7 @@ feedbackbasket feedback list --category BUG # Filter by category
83
83
  feedbackbasket feedback list --status OPEN # Filter by status
84
84
  feedbackbasket feedback list --sentiment NEGATIVE # Filter by sentiment
85
85
  feedbackbasket feedback list --search "login issue" # Text search
86
- feedbackbasket feedback show <id> # View single item detail
86
+ feedbackbasket feedback show <id> # View detail, including attachment links
87
87
  feedbackbasket feedback search "crash on mobile" # Search shortcut
88
88
 
89
89
  # Write
@@ -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
@@ -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;
@@ -123,6 +123,9 @@ function renderBugList(bugs) {
123
123
  if (bug.aiSummary) {
124
124
  console.log(` ${brand.hint(bug.aiSummary)}`);
125
125
  }
126
+ if (bug.attachments && bug.attachments.length > 0) {
127
+ console.log(` ${brand.muted(`${bug.attachments.length} attachment${bug.attachments.length === 1 ? '' : 's'}`)}`);
128
+ }
126
129
  console.log();
127
130
  }
128
131
  }
@@ -161,6 +161,9 @@ function renderFeedbackList(items) {
161
161
  if (item.aiSummary) {
162
162
  console.log(` ${brand.hint(item.aiSummary)}`);
163
163
  }
164
+ if (item.attachments && item.attachments.length > 0) {
165
+ console.log(` ${brand.muted(`${item.attachments.length} attachment${item.attachments.length === 1 ? '' : 's'}`)}`);
166
+ }
164
167
  console.log();
165
168
  }
166
169
  }
@@ -171,6 +174,7 @@ function renderFeedbackDetail(item) {
171
174
  const fields = [
172
175
  ['Status', item.status],
173
176
  ['Category', item.category],
177
+ ['Feedback Type', formatFeedbackType(item)],
174
178
  ['Sentiment', item.sentiment],
175
179
  ['Priority', item.aiPriorityScore != null ? String(item.aiPriorityScore) : null],
176
180
  ['Email', item.email],
@@ -190,6 +194,25 @@ function renderFeedbackDetail(item) {
190
194
  console.log();
191
195
  console.log(brand.bold('Content:'));
192
196
  console.log(` ${item.content}`);
197
+ if (item.followUpAnswers && item.followUpAnswers.length > 0) {
198
+ console.log();
199
+ console.log(brand.bold('Follow-up Answers:'));
200
+ for (const answer of item.followUpAnswers) {
201
+ const value = answer.value?.trim() || brand.muted('(skipped)');
202
+ console.log(` ${brand.bold(answer.label)}`);
203
+ console.log(` ${value}`);
204
+ }
205
+ }
206
+ if (item.attachments && item.attachments.length > 0) {
207
+ console.log();
208
+ console.log(brand.bold(`Attachments (${item.attachments.length}):`));
209
+ for (const attachment of item.attachments) {
210
+ const size = typeof attachment.size === 'number' ? ` ${brand.muted(formatBytes(attachment.size))}` : '';
211
+ const type = attachment.mimeType ? ` ${brand.muted(attachment.mimeType)}` : '';
212
+ console.log(` ${brand.bold(attachment.filename)}${size}${type}`);
213
+ console.log(` ${attachment.url}`);
214
+ }
215
+ }
193
216
  if (item.aiSummary) {
194
217
  console.log();
195
218
  console.log(brand.bold('AI Summary:'));
@@ -209,3 +232,22 @@ function renderFeedbackDetail(item) {
209
232
  }
210
233
  }
211
234
  }
235
+ function formatFeedbackType(item) {
236
+ if (!item.feedbackType)
237
+ return undefined;
238
+ const label = item.feedbackType.label || item.feedbackType.id;
239
+ if (!label)
240
+ return undefined;
241
+ return item.feedbackType.emoji ? `${item.feedbackType.emoji} ${label}` : label;
242
+ }
243
+ function formatBytes(bytes) {
244
+ if (!Number.isFinite(bytes) || bytes < 0)
245
+ return '';
246
+ if (bytes < 1024)
247
+ return `${bytes} B`;
248
+ const kb = bytes / 1024;
249
+ if (kb < 1024)
250
+ return `${kb.toFixed(kb >= 10 ? 0 : 1)} KB`;
251
+ const mb = kb / 1024;
252
+ return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
253
+ }
@@ -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,11 +64,24 @@ 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;
29
82
  device?: string | null;
30
83
  language?: string | null;
84
+ attachments?: FeedbackAttachment[];
31
85
  project: {
32
86
  id: string;
33
87
  name: string;
@@ -36,6 +90,14 @@ export interface Feedback {
36
90
  notes?: FeedbackNote[];
37
91
  createdAt: string;
38
92
  }
93
+ export interface FeedbackAttachment {
94
+ id: string;
95
+ url: string;
96
+ filename: string;
97
+ size?: number;
98
+ mimeType?: string;
99
+ createdAt?: string;
100
+ }
39
101
  export interface FeedbackNote {
40
102
  id: string;
41
103
  content: string;
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.5.0";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.5.0";
1
+ export declare const VERSION = "0.6.1";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.6.1";
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.5.0';
1
+ export const VERSION = '0.6.1';
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.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Command-line interface for FeedbackBasket — manage feedback from your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/src/cli.js",
@@ -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, attachment URLs, AI analysis, priority score
137
148
  ```
138
149
 
139
150
  ### Close the loop — reply to the submitter