feedbackbasket-cli 0.7.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
@@ -22,6 +22,7 @@ feedbackbasket login --manual
22
22
  # Start exploring
23
23
  feedbackbasket projects list
24
24
  feedbackbasket feedback list
25
+ feedbackbasket feedback create "Login button is broken" --project myapp --type bug
25
26
  feedbackbasket bugs list --severity high
26
27
  ```
27
28
 
@@ -35,6 +36,7 @@ Any AI agent with shell access (Claude Code, Codex, Cursor, OpenCode) can use th
35
36
  # Agents should use --agent flag for raw JSON output
36
37
  feedbackbasket projects list --agent
37
38
  feedbackbasket feedback list --category BUG --agent
39
+ feedbackbasket feedback create "Login button is broken" --content "Clicking Log in does nothing in Safari." --project myapp --type bug --agent
38
40
  feedbackbasket feedback update <id> --status PLANNED --agent
39
41
  feedbackbasket widget script myproject --agent
40
42
  ```
@@ -87,9 +89,13 @@ feedbackbasket feedback show <id> # View detail, includ
87
89
  feedbackbasket feedback search "crash on mobile" # Search shortcut
88
90
 
89
91
  # Write
92
+ feedbackbasket feedback create "Title" --content "Body" --project myapp
93
+ feedbackbasket feedback create "Login bug" --content "Clicking Log in does nothing" --project myapp --type bug --page-url https://example.com/login
90
94
  feedbackbasket feedback update <id> --status PLANNED # Update status
91
95
  feedbackbasket feedback update <id> --category BUG # Update category
92
- 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
93
99
  feedbackbasket feedback replies <id> # List sent replies
94
100
  feedbackbasket feedback note <id> "Investigating this..." # Add internal note
95
101
  feedbackbasket feedback delete <id> # Delete feedback
@@ -1,4 +1,4 @@
1
- import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, 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);
@@ -34,6 +34,7 @@ export declare class FeedbackBasketClient {
34
34
  category?: string;
35
35
  limit?: number;
36
36
  }): Promise<FeedbackResponse>;
37
+ createFeedback(data: FeedbackCreateInput): Promise<FeedbackCreateResponse>;
37
38
  getWidgetSettings(projectId: string): Promise<{
38
39
  projectId: string;
39
40
  projectName: string;
@@ -60,16 +61,10 @@ export declare class FeedbackBasketClient {
60
61
  content: string;
61
62
  createdAt: string;
62
63
  }>;
63
- sendReply(feedbackId: string, content: string, replyToEmail?: string): Promise<{
64
- reply: {
65
- id: string;
66
- content: string;
67
- replyToEmail: string;
68
- sentBy: string;
69
- createdAt: string;
70
- };
71
- sentTo: string;
72
- }>;
64
+ sendReply(feedbackId: string, content: string, opts?: {
65
+ replyToEmail?: string;
66
+ destinations?: Array<'email' | 'widget'>;
67
+ }): Promise<FeedbackReplyResponse>;
73
68
  listReplies(feedbackId: string): Promise<{
74
69
  replies: Array<{
75
70
  id: string;
@@ -78,6 +73,7 @@ export declare class FeedbackBasketClient {
78
73
  sentBy: string;
79
74
  createdAt: string;
80
75
  }>;
76
+ messages?: NonNullable<FeedbackReplyResponse['message']>[];
81
77
  total: number;
82
78
  }>;
83
79
  deleteFeedback(id: string): Promise<{
@@ -50,6 +50,9 @@ export class FeedbackBasketClient {
50
50
  async searchFeedback(query, opts = {}) {
51
51
  return this.getFeedback({ search: query, ...opts });
52
52
  }
53
+ async createFeedback(data) {
54
+ return this.request('POST', '/feedback', data);
55
+ }
53
56
  // Widget
54
57
  async getWidgetSettings(projectId) {
55
58
  return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget`);
@@ -67,8 +70,11 @@ export class FeedbackBasketClient {
67
70
  async createNote(feedbackId, content) {
68
71
  return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/notes`, { content });
69
72
  }
70
- async sendReply(feedbackId, content, replyToEmail) {
73
+ async sendReply(feedbackId, content, opts = {}) {
71
74
  const body = { content };
75
+ if (opts.destinations)
76
+ body.destinations = opts.destinations;
77
+ const replyToEmail = opts.replyToEmail;
72
78
  if (replyToEmail)
73
79
  body['replyToEmail'] = replyToEmail;
74
80
  return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/replies`, body);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import type { OutputWriter } from '../output/writer.js';
3
+ export declare function createFeedbackCreateCommand(getWriter: () => OutputWriter): Command;
@@ -0,0 +1,120 @@
1
+ import { Command } from 'commander';
2
+ import { FeedbackBasketClient } from '../client.js';
3
+ import { AuthManager } from '../auth/manager.js';
4
+ import { loadConfig } from '../config/config.js';
5
+ import { errAuth, errUsage } from '../output/errors.js';
6
+ import { brand } from '../output/theme.js';
7
+ import { resolveProject } from '../resolve.js';
8
+ const validTypes = new Set(['bug', 'feature', 'general']);
9
+ const validCategories = new Set(['BUG', 'FEATURE_REQUEST', 'IMPROVEMENT', 'QUESTION']);
10
+ const validStatuses = new Set(['OPEN', 'UNDER_REVIEW', 'PLANNED', 'IN_PROGRESS', 'COMPLETE', 'CLOSED']);
11
+ export function createFeedbackCreateCommand(getWriter) {
12
+ return new Command('create')
13
+ .argument('<title>', 'Short title or summary for the feedback')
14
+ .description('Create a new feedback item')
15
+ .requiredOption('--project <id-or-name>', 'Project ID or name')
16
+ .option('--content <body>', 'Longer feedback body')
17
+ .option('--type <type>', 'Feedback type (bug, feature, general)')
18
+ .option('--category <category>', 'Category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
19
+ .option('--status <status>', 'Initial status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
20
+ .option('--email <email>', 'Submitter email')
21
+ .option('--page-url <url>', 'Page URL where the feedback applies')
22
+ .option('--metadata <key=value>', 'Metadata key/value pair (repeatable)', collectMetadata, [])
23
+ .action(async (title, opts) => {
24
+ const writer = getWriter();
25
+ const client = requireClient();
26
+ if (opts.type && !validTypes.has(opts.type)) {
27
+ throw errUsage('Invalid type. Must be one of: bug, feature, general', 'Example: feedbackbasket feedback create "Login is broken" --project myapp --type bug');
28
+ }
29
+ if (opts.category && !validCategories.has(opts.category)) {
30
+ throw errUsage('Invalid category. Must be one of: BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION');
31
+ }
32
+ if (opts.status && !validStatuses.has(opts.status)) {
33
+ throw errUsage('Invalid status. Must be one of: OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED');
34
+ }
35
+ const project = await resolveProject(client, opts.project);
36
+ const content = composeContent(title, opts.content);
37
+ const metadata = parseMetadata(opts.metadata ?? []);
38
+ const result = await client.createFeedback({
39
+ projectId: project.id,
40
+ content,
41
+ type: opts.type,
42
+ category: opts.category,
43
+ status: opts.status,
44
+ email: opts.email,
45
+ pageUrl: opts.pageUrl,
46
+ metadata,
47
+ });
48
+ if (!writer.isMachineOutput()) {
49
+ console.log(` ${brand.success('[OK]')} Feedback created: ${brand.bold(result.id)}`);
50
+ console.log(` ${brand.muted('Project:')} ${project.name}`);
51
+ console.log(` ${brand.muted('URL:')} ${result.url}`);
52
+ console.log();
53
+ }
54
+ writer.ok(result, {
55
+ summary: `Created feedback ${result.id}`,
56
+ breadcrumbs: [
57
+ { action: 'View feedback', cmd: `feedbackbasket feedback show ${result.id}` },
58
+ { action: 'Update status', cmd: `feedbackbasket feedback update ${result.id} --status UNDER_REVIEW` },
59
+ { action: 'List project feedback', cmd: `feedbackbasket feedback list --project ${project.id}` },
60
+ ],
61
+ });
62
+ });
63
+ }
64
+ function requireClient() {
65
+ const manager = new AuthManager();
66
+ const token = manager.resolveToken();
67
+ if (!token)
68
+ throw errAuth();
69
+ const config = loadConfig();
70
+ return new FeedbackBasketClient(token, config.baseUrl);
71
+ }
72
+ function composeContent(title, body) {
73
+ const cleanTitle = title.trim();
74
+ const cleanBody = body?.trim();
75
+ const content = cleanBody ? `${cleanTitle}\n\n${cleanBody}` : cleanTitle;
76
+ if (!content) {
77
+ throw errUsage('Feedback title is required');
78
+ }
79
+ if (content.length > 2000) {
80
+ throw errUsage('Feedback content must be 2000 characters or less');
81
+ }
82
+ return content;
83
+ }
84
+ function collectMetadata(value, previous) {
85
+ return previous.concat(value);
86
+ }
87
+ function parseMetadata(entries) {
88
+ if (entries.length === 0)
89
+ return undefined;
90
+ const metadata = {};
91
+ for (const entry of entries) {
92
+ const separator = entry.indexOf('=');
93
+ if (separator <= 0) {
94
+ throw errUsage(`Invalid metadata "${entry}"`, 'Use --metadata key=value, for example --metadata source=codex');
95
+ }
96
+ const key = entry.slice(0, separator).trim();
97
+ const rawValue = entry.slice(separator + 1).trim();
98
+ if (!key) {
99
+ throw errUsage('Metadata keys cannot be empty');
100
+ }
101
+ metadata[key] = parseMetadataValue(rawValue);
102
+ }
103
+ return metadata;
104
+ }
105
+ function parseMetadataValue(value) {
106
+ if (value === 'true')
107
+ return true;
108
+ if (value === 'false')
109
+ return false;
110
+ if (value === 'null')
111
+ return null;
112
+ if (value !== '' && Number.isFinite(Number(value)))
113
+ return Number(value);
114
+ try {
115
+ return JSON.parse(value);
116
+ }
117
+ catch {
118
+ return value;
119
+ }
120
+ }
@@ -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
  });
@@ -17,6 +17,7 @@ async function resolveProjectId(client, optProject, all) {
17
17
  }
18
18
  import { createFeedbackUpdateCommand } from './feedback-update.js';
19
19
  import { createFeedbackNoteCommand } from './feedback-note.js';
20
+ import { createFeedbackCreateCommand } from './feedback-create.js';
20
21
  import { createFeedbackDeleteCommand } from './feedback-delete.js';
21
22
  import { createFeedbackBulkUpdateCommand } from './feedback-bulk-update.js';
22
23
  import { createFeedbackExportCommand } from './feedback-export.js';
@@ -25,6 +26,7 @@ export function createFeedbackCommand(getWriter) {
25
26
  const feedback = new Command('feedback')
26
27
  .description('View and manage feedback');
27
28
  // Write subcommands
29
+ feedback.addCommand(createFeedbackCreateCommand(getWriter));
28
30
  feedback.addCommand(createFeedbackUpdateCommand(getWriter));
29
31
  feedback.addCommand(createFeedbackNoteCommand(getWriter));
30
32
  feedback.addCommand(createFeedbackReplyCommand(getWriter));
@@ -184,6 +186,7 @@ function renderFeedbackDetail(item) {
184
186
  ['OS', item.os],
185
187
  ['Device', item.device],
186
188
  ['Language', item.language],
189
+ ['Widget Thread', item.hasWidgetAccess ? 'Yes' : null],
187
190
  ['Created', item.createdAt],
188
191
  ];
189
192
  for (const [label, value] of fields) {
package/dist/src/help.js CHANGED
@@ -57,6 +57,7 @@ export function renderRootHelp() {
57
57
  // Examples
58
58
  lines.push(section(' EXAMPLES'));
59
59
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects list`);
60
+ lines.push(`${INDENT}${brand.muted('$')} feedbackbasket feedback create "Login bug" --project myapp --type bug`);
60
61
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket feedback list --category BUG --status OPEN`);
61
62
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket bugs list --severity high`);
62
63
  lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget script myapp`);
@@ -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;
@@ -126,6 +128,40 @@ export interface FeedbackResponse {
126
128
  feedback: Feedback[];
127
129
  pagination: Pagination;
128
130
  }
131
+ export interface FeedbackCreateInput {
132
+ projectId: string;
133
+ content: string;
134
+ type?: 'bug' | 'feature' | 'general';
135
+ category?: FeedbackCategory;
136
+ status?: FeedbackStatus;
137
+ email?: string;
138
+ pageUrl?: string;
139
+ metadata?: Record<string, unknown>;
140
+ }
141
+ export interface FeedbackCreateResponse {
142
+ id: string;
143
+ url: string;
144
+ feedback: Feedback;
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
+ }
129
165
  export interface BugReportsResponse {
130
166
  bugReports: BugReport[];
131
167
  stats: {
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.7.0";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.7.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.7.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.7.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",
@@ -62,14 +62,17 @@ feedbackbasket feedback show <id>
62
62
  feedbackbasket feedback search "crash on mobile" --project <id> --limit 10
63
63
 
64
64
  # Write
65
+ feedbackbasket feedback create "Login button is broken" --content "Clicking Log in does nothing in Safari." --project <id> --type bug
66
+ feedbackbasket feedback create "Feature idea" --content "Let users export saved views." --project <id> --type feature --metadata source=agent
65
67
  feedbackbasket feedback update <id> --status PLANNED --category BUG --sentiment NEGATIVE
66
68
  feedbackbasket feedback note <id> "Investigating — appears related to auth flow"
67
69
  feedbackbasket feedback delete <id> --yes
68
70
  feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3
69
71
 
70
- # Reply to submitter via email
71
- feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!"
72
- 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
73
76
  feedbackbasket feedback replies <id> # list past replies
74
77
 
75
78
  # Export
@@ -143,6 +146,18 @@ feedbackbasket feedback update <id> --status UNDER_REVIEW --agent
143
146
  feedbackbasket feedback note <id> "Reviewing — appears related to auth flow" --agent
144
147
  ```
145
148
 
149
+ ### Capture new feedback without leaving the terminal
150
+ ```bash
151
+ feedbackbasket feedback create "Login button is broken" \
152
+ --content "Clicking Log in does nothing in Safari." \
153
+ --project myapp \
154
+ --type bug \
155
+ --page-url https://example.com/login \
156
+ --metadata source=agent \
157
+ --agent
158
+ ```
159
+ Agent mode returns the created feedback ID, dashboard URL, and feedback object. Created feedback is analyzed by AI and follows the project's notification settings.
160
+
146
161
  ### Investigate high-priority bugs
147
162
  ```bash
148
163
  feedbackbasket bugs list --severity high --agent
@@ -152,18 +167,22 @@ feedbackbasket feedback show <id> --agent
152
167
 
153
168
  ### Close the loop — reply to the submitter
154
169
  ```bash
155
- # Agent reads context, drafts its own reply, sends it
156
- feedbackbasket feedback show <id> --agent # read context + project.replyToEmail
157
- 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
158
175
  feedbackbasket feedback update <id> --status COMPLETE --agent
159
176
  feedbackbasket feedback note <id> "Replied via CLI" --agent
160
177
  ```
161
- **Important:** If `feedback show` returns `project.replyToEmail: null`, the agent MUST either:
162
- 1. Pass `--reply-to <email>` with an explicit address, OR
163
- 2. Ask the human which reply-to email to use (the account owner's email is a reasonable default, but requires user confirmation), OR
164
- 3. Set a project default first: `feedbackbasket projects update <project> --reply-to <email>`
165
-
166
- Never silently guess a reply-to address it becomes the "From" address the customer sees.
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>`.
184
+
185
+ Never silently guess a reply-to address. It becomes the "From" address the customer sees.
167
186
 
168
187
  ### Export for analysis
169
188
  ```bash