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 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 for reporting!" # Email the submitter
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
@@ -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, replyToEmail?: string): Promise<{
65
- reply: {
66
- id: string;
67
- content: string;
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<{
@@ -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, replyToEmail) {
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, confirm } from '../prompt.js';
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('Send an email reply to the feedback submitter')
13
+ .description('Reply to feedback by email, widget thread, or both')
13
14
  .option('--content <text>', 'Reply content (alternative to positional argument)')
14
- .option('--reply-to <email>', 'Reply-to email (overrides project default)')
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
- let replyTo = opts.replyTo;
23
- // If no --reply-to flag, check if we need to resolve one interactively
24
- if (!replyTo) {
25
- const feedback = await client.getFeedbackById(id);
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 cannot send a reply.');
37
+ throw errUsage('This feedback has no email address; use --delivery widget if it has a widget thread.');
28
38
  }
29
- const projectReplyTo = feedback.project.replyToEmail;
30
- if (!projectReplyTo) {
39
+ if (!replyTo) {
31
40
  const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
32
- if (isInteractive) {
33
- // Get user's email as suggestion
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
- else {
66
- // Agent mode — must pass --reply-to or set project default
67
- throw errUsage('No reply-to email configured for this project.', `Pass --reply-to <email> or set a default: feedbackbasket projects update <project> --reply-to <email>`);
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
- const result = await client.sendReply(id, content, replyTo);
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
- console.log(` ${brand.success('✓')} Reply sent to ${brand.bold(result.sentTo)}`);
74
- console.log(` ${brand.muted('From:')} ${result.reply.replyToEmail}`);
75
- console.log(` ${brand.muted('By:')} ${result.reply.sentBy}`);
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: `Reply sent to ${result.sentTo}`,
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.replies.length === 0) {
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('')} ${brand.bold(r.sentBy)} ${brand.muted(r.createdAt)}`);
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 a reply', cmd: `feedbackbasket feedback reply ${id} "<content>"` },
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) {
@@ -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: {
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.8.0";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.8.0";
1
+ export declare const VERSION = "0.9.0";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.0";
@@ -1,2 +1,2 @@
1
- export const VERSION = '0.8.0';
1
+ export const VERSION = '0.9.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.8.0",
3
+ "version": "0.9.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",
@@ -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 via email
73
- feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!"
74
- feedbackbasket feedback reply <id> "<content>" --reply-to vlad@example.com # override reply-to
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, drafts its own reply, sends it
170
- feedbackbasket feedback show <id> --agent # read context + project.replyToEmail
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:** If `feedback show` returns `project.replyToEmail: null`, the agent MUST either:
176
- 1. Pass `--reply-to <email>` with an explicit address, OR
177
- 2. Ask the human which reply-to email to use (the account owner's email is a reasonable default, but requires user confirmation), OR
178
- 3. Set a project default first: `feedbackbasket projects update <project> --reply-to <email>`
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 it becomes the "From" address the customer sees.
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