feedbackbasket-cli 0.8.0 → 0.9.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 +3 -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/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 +16 -11
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
|
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) {
|
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.0";
|
|
2
|
+
export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.0";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.9.0';
|
|
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
|
|
@@ -166,18 +167,22 @@ feedbackbasket feedback show <id> --agent
|
|
|
166
167
|
|
|
167
168
|
### Close the loop — reply to the submitter
|
|
168
169
|
```bash
|
|
169
|
-
# Agent reads context,
|
|
170
|
-
feedbackbasket feedback show <id> --agent # read
|
|
171
|
-
feedbackbasket feedback reply <id> "<drafted response>" --agent
|
|
170
|
+
# Agent reads context, asks which delivery method to use, then sends it
|
|
171
|
+
feedbackbasket feedback show <id> --agent # read email, hasWidgetAccess, project.replyToEmail
|
|
172
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery widget --agent
|
|
173
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery email --reply-to support@example.com --agent
|
|
174
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery both --reply-to support@example.com --agent
|
|
172
175
|
feedbackbasket feedback update <id> --status COMPLETE --agent
|
|
173
176
|
feedbackbasket feedback note <id> "Replied via CLI" --agent
|
|
174
177
|
```
|
|
175
|
-
**Important
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
**Important reply safety rules:**
|
|
179
|
+
- 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.
|
|
180
|
+
- 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.
|
|
181
|
+
- If `hasWidgetAccess: false`, do not use `--delivery widget` or `--delivery both`.
|
|
182
|
+
- 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.
|
|
183
|
+
- 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>`.
|
|
179
184
|
|
|
180
|
-
Never silently guess a reply-to address
|
|
185
|
+
Never silently guess a reply-to address. It becomes the "From" address the customer sees.
|
|
181
186
|
|
|
182
187
|
### Export for analysis
|
|
183
188
|
```bash
|