feedbackbasket-cli 0.8.0 → 0.9.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 +13 -1
- package/dist/src/client.d.ts +6 -11
- package/dist/src/client.js +4 -1
- package/dist/src/commands/feedback-reply.js +73 -55
- package/dist/src/commands/feedback.js +1 -0
- package/dist/src/commands/widget.js +23 -0
- package/dist/src/types.d.ts +21 -0
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/feedbackbasket/SKILL.md +35 -12
package/README.md
CHANGED
|
@@ -93,7 +93,9 @@ feedbackbasket feedback create "Title" --content "Body" --project myapp
|
|
|
93
93
|
feedbackbasket feedback create "Login bug" --content "Clicking Log in does nothing" --project myapp --type bug --page-url https://example.com/login
|
|
94
94
|
feedbackbasket feedback update <id> --status PLANNED # Update status
|
|
95
95
|
feedbackbasket feedback update <id> --category BUG # Update category
|
|
96
|
-
feedbackbasket feedback reply <id> "Thanks
|
|
96
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery email --reply-to support@example.com
|
|
97
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery widget
|
|
98
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com
|
|
97
99
|
feedbackbasket feedback replies <id> # List sent replies
|
|
98
100
|
feedbackbasket feedback note <id> "Investigating this..." # Add internal note
|
|
99
101
|
feedbackbasket feedback delete <id> # Delete feedback
|
|
@@ -139,6 +141,16 @@ feedbackbasket widget flow myapp --config ./feedback-flow.json
|
|
|
139
141
|
feedbackbasket widget script myapp
|
|
140
142
|
```
|
|
141
143
|
|
|
144
|
+
For inline trigger mode, load the widget once and call the public API from your own button:
|
|
145
|
+
|
|
146
|
+
```html
|
|
147
|
+
<button onclick="window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })">
|
|
148
|
+
Feedback
|
|
149
|
+
</button>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
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.
|
|
153
|
+
|
|
142
154
|
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.
|
|
143
155
|
|
|
144
156
|
`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.
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
|
|
1
|
+
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, 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);
|
|
@@ -61,16 +61,10 @@ export declare class FeedbackBasketClient {
|
|
|
61
61
|
content: string;
|
|
62
62
|
createdAt: string;
|
|
63
63
|
}>;
|
|
64
|
-
sendReply(feedbackId: string, content: string,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
replyToEmail: string;
|
|
69
|
-
sentBy: string;
|
|
70
|
-
createdAt: string;
|
|
71
|
-
};
|
|
72
|
-
sentTo: string;
|
|
73
|
-
}>;
|
|
64
|
+
sendReply(feedbackId: string, content: string, opts?: {
|
|
65
|
+
replyToEmail?: string;
|
|
66
|
+
destinations?: Array<'email' | 'widget'>;
|
|
67
|
+
}): Promise<FeedbackReplyResponse>;
|
|
74
68
|
listReplies(feedbackId: string): Promise<{
|
|
75
69
|
replies: Array<{
|
|
76
70
|
id: string;
|
|
@@ -79,6 +73,7 @@ export declare class FeedbackBasketClient {
|
|
|
79
73
|
sentBy: string;
|
|
80
74
|
createdAt: string;
|
|
81
75
|
}>;
|
|
76
|
+
messages?: NonNullable<FeedbackReplyResponse['message']>[];
|
|
82
77
|
total: number;
|
|
83
78
|
}>;
|
|
84
79
|
deleteFeedback(id: string): Promise<{
|
package/dist/src/client.js
CHANGED
|
@@ -70,8 +70,11 @@ export class FeedbackBasketClient {
|
|
|
70
70
|
async createNote(feedbackId, content) {
|
|
71
71
|
return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/notes`, { content });
|
|
72
72
|
}
|
|
73
|
-
async sendReply(feedbackId, content,
|
|
73
|
+
async sendReply(feedbackId, content, opts = {}) {
|
|
74
74
|
const body = { content };
|
|
75
|
+
if (opts.destinations)
|
|
76
|
+
body.destinations = opts.destinations;
|
|
77
|
+
const replyToEmail = opts.replyToEmail;
|
|
75
78
|
if (replyToEmail)
|
|
76
79
|
body['replyToEmail'] = replyToEmail;
|
|
77
80
|
return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/replies`, body);
|
|
@@ -4,79 +4,84 @@ import { AuthManager } from '../auth/manager.js';
|
|
|
4
4
|
import { loadConfig } from '../config/config.js';
|
|
5
5
|
import { errAuth, errUsage } from '../output/errors.js';
|
|
6
6
|
import { brand } from '../output/theme.js';
|
|
7
|
-
import { ask
|
|
7
|
+
import { ask } from '../prompt.js';
|
|
8
|
+
const deliveryOptions = new Set(['email', 'widget', 'both']);
|
|
8
9
|
export function createFeedbackReplyCommand(getWriter) {
|
|
9
10
|
return new Command('reply')
|
|
10
11
|
.argument('<id>', 'Feedback ID to reply to')
|
|
11
12
|
.argument('[content]', 'Reply content (or use --content)')
|
|
12
|
-
.description('
|
|
13
|
+
.description('Reply to feedback by email, widget thread, or both')
|
|
13
14
|
.option('--content <text>', 'Reply content (alternative to positional argument)')
|
|
14
|
-
.option('--
|
|
15
|
+
.option('--delivery <delivery>', 'Reply delivery (email, widget, both)', 'email')
|
|
16
|
+
.option('--reply-to <email>', 'Reply-to email for email delivery')
|
|
15
17
|
.action(async (id, contentArg, opts) => {
|
|
16
18
|
const writer = getWriter();
|
|
17
19
|
const content = contentArg ?? opts.content;
|
|
20
|
+
const delivery = opts.delivery;
|
|
18
21
|
if (!content) {
|
|
19
|
-
throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!"');
|
|
22
|
+
throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!" --delivery widget');
|
|
23
|
+
}
|
|
24
|
+
if (!deliveryOptions.has(delivery)) {
|
|
25
|
+
throw errUsage('Delivery must be email, widget, or both', 'Example: feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com');
|
|
20
26
|
}
|
|
21
27
|
const client = requireClient();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
const feedback = await client.getFeedbackById(id);
|
|
29
|
+
const destinations = delivery === 'both'
|
|
30
|
+
? ['email', 'widget']
|
|
31
|
+
: [delivery];
|
|
32
|
+
const sendsEmail = destinations.includes('email');
|
|
33
|
+
const sendsWidget = destinations.includes('widget');
|
|
34
|
+
let replyTo = opts.replyTo ?? feedback.project.replyToEmail ?? undefined;
|
|
35
|
+
if (sendsEmail) {
|
|
26
36
|
if (!feedback.email) {
|
|
27
|
-
throw errUsage('This feedback has no email address
|
|
37
|
+
throw errUsage('This feedback has no email address; use --delivery widget if it has a widget thread.');
|
|
28
38
|
}
|
|
29
|
-
|
|
30
|
-
if (!projectReplyTo) {
|
|
39
|
+
if (!replyTo) {
|
|
31
40
|
const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
|
|
32
|
-
if (isInteractive) {
|
|
33
|
-
|
|
34
|
-
const manager = new AuthManager();
|
|
35
|
-
const creds = manager.getCredentials();
|
|
36
|
-
const accountEmail = creds?.email;
|
|
37
|
-
console.log();
|
|
38
|
-
console.log(brand.warning(' No reply-to email configured for this project.'));
|
|
39
|
-
console.log(brand.muted(' The recipient will see this as the sender address.'));
|
|
40
|
-
console.log();
|
|
41
|
-
if (accountEmail) {
|
|
42
|
-
const useAccount = await confirm(` Use ${brand.bold(accountEmail)}?`);
|
|
43
|
-
if (useAccount) {
|
|
44
|
-
replyTo = accountEmail;
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
replyTo = await ask(' Enter reply-to email: ');
|
|
48
|
-
if (!replyTo || !replyTo.includes('@')) {
|
|
49
|
-
console.log(brand.muted(' Cancelled.'));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
replyTo = await ask(' Enter reply-to email: ');
|
|
56
|
-
if (!replyTo || !replyTo.includes('@')) {
|
|
57
|
-
console.log(brand.muted(' Cancelled.'));
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
console.log();
|
|
62
|
-
console.log(brand.muted(` Tip: Set a default with: feedbackbasket projects update ${feedback.project.name} --reply-to ${replyTo}`));
|
|
63
|
-
console.log();
|
|
41
|
+
if (!isInteractive) {
|
|
42
|
+
throw errUsage('No reply-to email configured for this project.', 'Ask the human which reply-to email to use, then pass --reply-to <email> or set a project default.');
|
|
64
43
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(brand.warning(' No reply-to email configured for this project.'));
|
|
46
|
+
console.log(brand.muted(' The recipient will see this as the sender address.'));
|
|
47
|
+
console.log();
|
|
48
|
+
replyTo = await ask(' Enter reply-to email: ');
|
|
49
|
+
if (!replyTo || !replyTo.includes('@')) {
|
|
50
|
+
console.log(brand.muted(' Cancelled.'));
|
|
51
|
+
return;
|
|
68
52
|
}
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(brand.muted(` Tip: Set a default with: feedbackbasket projects update ${feedback.project.name} --reply-to ${replyTo}`));
|
|
55
|
+
console.log();
|
|
69
56
|
}
|
|
70
57
|
}
|
|
71
|
-
|
|
58
|
+
if (sendsWidget && !feedback.hasWidgetAccess) {
|
|
59
|
+
throw errUsage('This feedback is not connected to a widget thread.', 'Use --delivery email for feedback with an email address, or ask the human how they want to respond.');
|
|
60
|
+
}
|
|
61
|
+
const result = await client.sendReply(id, content, {
|
|
62
|
+
replyToEmail: replyTo,
|
|
63
|
+
destinations,
|
|
64
|
+
});
|
|
72
65
|
if (!writer.isMachineOutput()) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
if (result.sentTo) {
|
|
67
|
+
console.log(` ${brand.success('[OK]')} Email reply sent to ${brand.bold(result.sentTo)}`);
|
|
68
|
+
if (result.reply) {
|
|
69
|
+
console.log(` ${brand.muted('From:')} ${result.reply.replyToEmail}`);
|
|
70
|
+
console.log(` ${brand.muted('By:')} ${result.reply.sentBy}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (result.message) {
|
|
74
|
+
console.log(` ${brand.success('[OK]')} Widget reply posted`);
|
|
75
|
+
console.log(` ${brand.muted('By:')} ${result.message.sentByName ?? 'CLI'}`);
|
|
76
|
+
}
|
|
76
77
|
console.log();
|
|
77
78
|
}
|
|
78
79
|
writer.ok(result, {
|
|
79
|
-
summary:
|
|
80
|
+
summary: delivery === 'both'
|
|
81
|
+
? 'Reply sent by email and widget'
|
|
82
|
+
: delivery === 'widget'
|
|
83
|
+
? 'Widget reply posted'
|
|
84
|
+
: `Reply sent to ${result.sentTo}`,
|
|
80
85
|
breadcrumbs: [
|
|
81
86
|
{ action: 'View replies', cmd: `feedbackbasket feedback replies ${id}` },
|
|
82
87
|
{ action: 'Update status', cmd: `feedbackbasket feedback update ${id} --status COMPLETE` },
|
|
@@ -93,8 +98,10 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
93
98
|
const writer = getWriter();
|
|
94
99
|
const client = requireClient();
|
|
95
100
|
const result = await client.listReplies(id);
|
|
101
|
+
const messages = result.messages ?? [];
|
|
102
|
+
const visibleWidgetMessages = messages.filter((item) => !item.replyId);
|
|
96
103
|
if (!writer.isMachineOutput()) {
|
|
97
|
-
if (result.
|
|
104
|
+
if (result.total === 0) {
|
|
98
105
|
console.log(brand.muted(' No replies sent yet.'));
|
|
99
106
|
console.log();
|
|
100
107
|
}
|
|
@@ -102,7 +109,8 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
102
109
|
console.log(brand.bold(`${result.total} repl${result.total === 1 ? 'y' : 'ies'} for feedback ${id}`));
|
|
103
110
|
console.log();
|
|
104
111
|
for (const r of result.replies) {
|
|
105
|
-
console.log(` ${brand.success('
|
|
112
|
+
console.log(` ${brand.success('->')} ${brand.bold(r.sentBy)} ${brand.muted(r.createdAt)}`);
|
|
113
|
+
console.log(` ${brand.muted('Delivery:')} email`);
|
|
106
114
|
console.log(` ${brand.muted('Reply-to:')} ${r.replyToEmail}`);
|
|
107
115
|
console.log();
|
|
108
116
|
for (const line of r.content.split('\n')) {
|
|
@@ -110,12 +118,22 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
110
118
|
}
|
|
111
119
|
console.log();
|
|
112
120
|
}
|
|
121
|
+
for (const message of visibleWidgetMessages) {
|
|
122
|
+
console.log(` ${brand.success('->')} ${brand.bold(message.sentByName ?? 'CLI')} ${brand.muted(message.createdAt)}`);
|
|
123
|
+
console.log(` ${brand.muted('Delivery:')} widget`);
|
|
124
|
+
console.log();
|
|
125
|
+
for (const line of message.content.split('\n')) {
|
|
126
|
+
console.log(` ${line}`);
|
|
127
|
+
}
|
|
128
|
+
console.log();
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
|
-
writer.ok(result.replies, {
|
|
132
|
+
writer.ok({ replies: result.replies, messages }, {
|
|
116
133
|
summary: `${result.total} repl${result.total === 1 ? 'y' : 'ies'}`,
|
|
117
134
|
breadcrumbs: [
|
|
118
|
-
{ action: 'Send
|
|
135
|
+
{ action: 'Send an email reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery email` },
|
|
136
|
+
{ action: 'Post a widget reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery widget` },
|
|
119
137
|
{ action: 'View feedback', cmd: `feedbackbasket feedback show ${id}` },
|
|
120
138
|
],
|
|
121
139
|
});
|
|
@@ -186,6 +186,7 @@ function renderFeedbackDetail(item) {
|
|
|
186
186
|
['OS', item.os],
|
|
187
187
|
['Device', item.device],
|
|
188
188
|
['Language', item.language],
|
|
189
|
+
['Widget Thread', item.hasWidgetAccess ? 'Yes' : null],
|
|
189
190
|
['Created', item.createdAt],
|
|
190
191
|
];
|
|
191
192
|
for (const [label, value] of fields) {
|
|
@@ -243,6 +243,7 @@ export function createWidgetCommand(getWriter) {
|
|
|
243
243
|
const projectId = await resolveProjectId(client, projectArg);
|
|
244
244
|
const result = await client.getWidgetScript(projectId);
|
|
245
245
|
if (!writer.isMachineOutput()) {
|
|
246
|
+
const settingsResult = await client.getWidgetSettings(projectId);
|
|
246
247
|
console.log(brand.bold(`Widget embed code for ${result.projectName}`));
|
|
247
248
|
console.log(divider(50));
|
|
248
249
|
console.log();
|
|
@@ -252,6 +253,7 @@ export function createWidgetCommand(getWriter) {
|
|
|
252
253
|
console.log();
|
|
253
254
|
console.log(brand.muted(` Script URL: ${result.scriptUrl}`));
|
|
254
255
|
console.log();
|
|
256
|
+
renderInlineTriggerHelp(settingsResult.settings);
|
|
255
257
|
}
|
|
256
258
|
writer.ok(result, {
|
|
257
259
|
summary: `Embed code for "${result.projectName}"`,
|
|
@@ -341,6 +343,27 @@ function isRecord(value) {
|
|
|
341
343
|
function stringValue(value) {
|
|
342
344
|
return typeof value === 'string' ? value.trim() : '';
|
|
343
345
|
}
|
|
346
|
+
function renderInlineTriggerHelp(settings) {
|
|
347
|
+
if (settings.triggerMode !== 'inline')
|
|
348
|
+
return;
|
|
349
|
+
console.log(brand.bold('Custom inline trigger'));
|
|
350
|
+
console.log(divider(50));
|
|
351
|
+
console.log();
|
|
352
|
+
console.log(brand.muted(' Call this from your own button:'));
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(` ${brand.primary('<button onclick="window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })">')}`);
|
|
355
|
+
console.log(` ${brand.primary(' Feedback')}`);
|
|
356
|
+
console.log(` ${brand.primary('</button>')}`);
|
|
357
|
+
console.log();
|
|
358
|
+
if (settings.displayMode === 'popup') {
|
|
359
|
+
console.log(brand.muted(' Passing the trigger keeps popup mode anchored beside your custom button.'));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
console.log(brand.muted(' Modal mode stays centered; passing the trigger is safe for future popup changes.'));
|
|
363
|
+
}
|
|
364
|
+
console.log(brand.muted(' Existing calls to window.FeedbackWidget.openFeedbackForm() still work.'));
|
|
365
|
+
console.log();
|
|
366
|
+
}
|
|
344
367
|
function renderWidgetSettings(projectName, settings) {
|
|
345
368
|
console.log(brand.bold(`Widget settings — ${projectName}`));
|
|
346
369
|
console.log(divider(40));
|
package/dist/src/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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 ReplyDelivery = 'email' | 'widget' | 'both';
|
|
5
6
|
export type FeedbackFlowQuestionType = 'text' | 'textarea' | 'single_choice';
|
|
6
7
|
export interface FeedbackFlowQuestion {
|
|
7
8
|
id: string;
|
|
@@ -84,6 +85,7 @@ export interface Feedback {
|
|
|
84
85
|
os?: string | null;
|
|
85
86
|
device?: string | null;
|
|
86
87
|
language?: string | null;
|
|
88
|
+
hasWidgetAccess?: boolean;
|
|
87
89
|
attachments?: FeedbackAttachment[];
|
|
88
90
|
project: {
|
|
89
91
|
id: string;
|
|
@@ -141,6 +143,25 @@ export interface FeedbackCreateResponse {
|
|
|
141
143
|
url: string;
|
|
142
144
|
feedback: Feedback;
|
|
143
145
|
}
|
|
146
|
+
export interface FeedbackReplyResponse {
|
|
147
|
+
reply: {
|
|
148
|
+
id: string;
|
|
149
|
+
content: string;
|
|
150
|
+
replyToEmail: string;
|
|
151
|
+
sentBy: string;
|
|
152
|
+
createdAt: string;
|
|
153
|
+
} | null;
|
|
154
|
+
message: {
|
|
155
|
+
id: string;
|
|
156
|
+
content: string;
|
|
157
|
+
delivery: 'WIDGET' | 'EMAIL' | 'BOTH';
|
|
158
|
+
sentByName: string | null;
|
|
159
|
+
replyId: string | null;
|
|
160
|
+
createdAt: string;
|
|
161
|
+
} | null;
|
|
162
|
+
sentTo: string | null;
|
|
163
|
+
destinations: Array<'email' | 'widget'>;
|
|
164
|
+
}
|
|
144
165
|
export interface BugReportsResponse {
|
|
145
166
|
bugReports: BugReport[];
|
|
146
167
|
stats: {
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
2
|
-
export declare const USER_AGENT = "FeedbackBasket-CLI/0.
|
|
1
|
+
export declare const VERSION = "0.9.1";
|
|
2
|
+
export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.1";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.9.1';
|
|
2
2
|
export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
|
package/package.json
CHANGED
|
@@ -69,9 +69,10 @@ feedbackbasket feedback note <id> "Investigating — appears related to auth flo
|
|
|
69
69
|
feedbackbasket feedback delete <id> --yes
|
|
70
70
|
feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3
|
|
71
71
|
|
|
72
|
-
# Reply to submitter
|
|
73
|
-
feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!"
|
|
74
|
-
feedbackbasket feedback reply <id> "<content>" --
|
|
72
|
+
# Reply to submitter by email, widget thread, or both
|
|
73
|
+
feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!" --delivery email --reply-to support@example.com
|
|
74
|
+
feedbackbasket feedback reply <id> "<content>" --delivery widget
|
|
75
|
+
feedbackbasket feedback reply <id> "<content>" --delivery both --reply-to support@example.com
|
|
75
76
|
feedbackbasket feedback replies <id> # list past replies
|
|
76
77
|
|
|
77
78
|
# Export
|
|
@@ -108,6 +109,24 @@ feedbackbasket widget flow <project> --reset-default --enable
|
|
|
108
109
|
feedbackbasket widget flow <project> --config ./feedback-flow.json
|
|
109
110
|
```
|
|
110
111
|
|
|
112
|
+
For inline trigger mode, load the widget once and call the public API from the host app's custom button:
|
|
113
|
+
|
|
114
|
+
```html
|
|
115
|
+
<button onclick="window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })">
|
|
116
|
+
Feedback
|
|
117
|
+
</button>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
In React:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
<button onClick={(event) => window.FeedbackWidget.openFeedbackForm({ trigger: event.currentTarget })}>
|
|
124
|
+
Feedback
|
|
125
|
+
</button>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
111
130
|
`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.
|
|
112
131
|
|
|
113
132
|
`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`.
|
|
@@ -166,18 +185,22 @@ feedbackbasket feedback show <id> --agent
|
|
|
166
185
|
|
|
167
186
|
### Close the loop — reply to the submitter
|
|
168
187
|
```bash
|
|
169
|
-
# Agent reads context,
|
|
170
|
-
feedbackbasket feedback show <id> --agent # read
|
|
171
|
-
feedbackbasket feedback reply <id> "<drafted response>" --agent
|
|
188
|
+
# Agent reads context, asks which delivery method to use, then sends it
|
|
189
|
+
feedbackbasket feedback show <id> --agent # read email, hasWidgetAccess, project.replyToEmail
|
|
190
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery widget --agent
|
|
191
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery email --reply-to support@example.com --agent
|
|
192
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery both --reply-to support@example.com --agent
|
|
172
193
|
feedbackbasket feedback update <id> --status COMPLETE --agent
|
|
173
194
|
feedbackbasket feedback note <id> "Replied via CLI" --agent
|
|
174
195
|
```
|
|
175
|
-
**Important
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
**Important reply safety rules:**
|
|
197
|
+
- Before replying, the agent MUST inspect `feedback show --agent`, then ask the human which delivery method to use: `email`, `widget`, or `both`, unless the human already specified it in the current conversation.
|
|
198
|
+
- If `feedback show` returns `email: null`, do not use `--delivery email` or `--delivery both`. If `hasWidgetAccess: true`, use `--delivery widget`; otherwise ask the human how they want to respond.
|
|
199
|
+
- If `hasWidgetAccess: false`, do not use `--delivery widget` or `--delivery both`.
|
|
200
|
+
- If the delivery includes email and `project.replyToEmail: null`, the agent MUST ask the human which reply-to email to use before sending. Do not use the account owner's email, token owner's email, or any remembered address without explicit confirmation in the current conversation.
|
|
201
|
+
- After the human confirms a reply-to address, pass it explicitly with `--reply-to <email>`, or set a project default first with `feedbackbasket projects update <project> --reply-to <email>`.
|
|
202
|
+
|
|
203
|
+
Never silently guess a reply-to address. It becomes the "From" address the customer sees.
|
|
181
204
|
|
|
182
205
|
### Export for analysis
|
|
183
206
|
```bash
|