feedbackbasket-cli 3.1.0 → 3.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## [3.2.0] - 2026-09-04
6
+
7
+ ### Added
8
+
9
+ - Added structured close reasons and optional internal close notes to feedback create, update, bulk update, list, and show commands.
10
+
11
+ ### Changed
12
+
13
+ - Closing feedback now requires `--close-reason`. The `OTHER` reason also requires `--close-note`.
14
+ - The CLI and both MCP transports now use agent surface version `3.2.0`.
15
+
5
16
  ## [3.1.0] - 2026-08-22
6
17
 
7
18
  ### Added
package/README.md CHANGED
@@ -30,7 +30,7 @@ The browser selects the organization, Read or Full access, and Selected projects
30
30
 
31
31
  The first time you log in, a setup wizard walks you through selecting a default project and installing the Claude Code skill.
32
32
 
33
- The CLI uses agent surface version `3.1.0`. CLI login accepts only private CLI credentials. It does not accept MCP keys or OAuth tokens. Use `--yes` for high-impact commands in agent or machine mode. Interactive use can show a confirmation prompt. Never put an access token, refresh token, CLI token, or MCP key in source, prompts, logs, generated configuration, or final output.
33
+ The CLI uses agent surface version `3.2.0`. CLI login accepts only private CLI credentials. It does not accept MCP keys or OAuth tokens. Use `--yes` for high-impact commands in agent or machine mode. Interactive use can show a confirmation prompt. Never put an access token, refresh token, CLI token, or MCP key in source, prompts, logs, generated configuration, or final output.
34
34
 
35
35
  ## Agent Usage
36
36
 
@@ -57,7 +57,7 @@ feedbackbasket setup claude
57
57
  <!-- BEGIN GENERATED AGENT CAPABILITIES -->
58
58
  ## Agent capability contract
59
59
 
60
- Agent surface version: `3.1.0`. The CLI and both MCP transports implement the same 31 product operations.
60
+ Agent surface version: `3.2.0`. The CLI and both MCP transports implement the same 31 product operations.
61
61
 
62
62
  | Product operation | CLI command | MCP tool | Required access | Confirm |
63
63
  | --- | --- | --- | --- | --- |
@@ -135,6 +135,7 @@ feedbackbasket feedback list --category BUG # Filter by category
135
135
  feedbackbasket feedback list --status OPEN # Filter by status
136
136
  feedbackbasket feedback list --sentiment NEGATIVE # Filter by sentiment
137
137
  feedbackbasket feedback list --search "login issue" # Text search
138
+ feedbackbasket feedback list --status CLOSED --close-reason NOT_PLANNED
138
139
  feedbackbasket feedback show <id> # View detail, including attachment links
139
140
  feedbackbasket feedback search "crash on mobile" # Search shortcut
140
141
 
@@ -142,6 +143,7 @@ feedbackbasket feedback search "crash on mobile" # Search shortcut
142
143
  feedbackbasket feedback create "Title" --content "Body" --project myapp
143
144
  feedbackbasket feedback create "Login bug" --content "Clicking Log in does nothing" --project myapp --type bug --page-url https://example.com/login
144
145
  feedbackbasket feedback update <id> --status PLANNED # Update status
146
+ feedbackbasket feedback update <id> --status CLOSED --close-reason NOT_PLANNED
145
147
  feedbackbasket feedback update <id> --category BUG # Update category
146
148
  feedbackbasket feedback reply <id> "Thanks!" --delivery email --reply-to support@example.com
147
149
  feedbackbasket feedback reply <id> "Thanks!" --delivery widget
@@ -150,7 +152,7 @@ feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@
150
152
  feedbackbasket feedback replies <id> # Show the complete conversation
151
153
  feedbackbasket feedback note <id> "Investigating this..." # Add internal note
152
154
  feedbackbasket feedback delete <id> # Delete feedback
153
- feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3
155
+ feedbackbasket feedback bulk-update --status CLOSED --close-reason NOT_ACTIONABLE --ids id1,id2,id3
154
156
 
155
157
  # Export
156
158
  feedbackbasket feedback export myapp --format csv # Export to CSV
@@ -71,6 +71,8 @@ export declare class FeedbackBasketClient {
71
71
  status?: string;
72
72
  category?: string;
73
73
  sentiment?: string;
74
+ closeReason?: string;
75
+ closeNote?: string;
74
76
  }): Promise<Feedback>;
75
77
  createNote(feedbackId: string, content: string): Promise<{
76
78
  id: string;
@@ -96,9 +98,13 @@ export declare class FeedbackBasketClient {
96
98
  deleted: boolean;
97
99
  id: string;
98
100
  }>;
99
- bulkUpdateStatus(ids: string[], status: string): Promise<{
101
+ bulkUpdateStatus(ids: string[], status: string, closure?: {
102
+ closeReason?: string;
103
+ closeNote?: string;
104
+ }): Promise<{
100
105
  updated: number;
101
106
  status: string;
107
+ closeReason?: string | null;
102
108
  }>;
103
109
  updateNote(feedbackId: string, noteId: string, content: string): Promise<{
104
110
  id: string;
@@ -99,8 +99,12 @@ export class FeedbackBasketClient {
99
99
  async deleteFeedback(id) {
100
100
  return this.request('DELETE', `/feedback/${encodeURIComponent(id)}`);
101
101
  }
102
- async bulkUpdateStatus(ids, status) {
103
- return this.request('POST', '/feedback/bulk-update', { ids, status });
102
+ async bulkUpdateStatus(ids, status, closure = {}) {
103
+ return this.request('POST', '/feedback/bulk-update', {
104
+ ids,
105
+ status,
106
+ ...closure,
107
+ });
104
108
  }
105
109
  async updateNote(feedbackId, noteId, content) {
106
110
  return this.request('PATCH', `/feedback/${encodeURIComponent(feedbackId)}/notes/${encodeURIComponent(noteId)}`, { content });
@@ -117,7 +121,9 @@ export class FeedbackBasketClient {
117
121
  return this.request('GET', '/team');
118
122
  }
119
123
  async updateMemberRole(memberId, role) {
120
- return this.request('PATCH', `/team/${encodeURIComponent(memberId)}`, { role });
124
+ return this.request('PATCH', `/team/${encodeURIComponent(memberId)}`, {
125
+ role,
126
+ });
121
127
  }
122
128
  async removeMember(memberId) {
123
129
  return this.request('DELETE', `/team/${encodeURIComponent(memberId)}`);
@@ -130,24 +136,27 @@ export class FeedbackBasketClient {
130
136
  method,
131
137
  signal: controller.signal,
132
138
  headers: {
133
- 'Authorization': `Bearer ${this.token}`,
139
+ Authorization: `Bearer ${this.token}`,
134
140
  'Content-Type': 'application/json',
135
141
  'User-Agent': USER_AGENT,
136
142
  },
137
143
  body: data === undefined ? undefined : JSON.stringify(data),
138
144
  });
139
145
  const contentType = response.headers.get('content-type') ?? '';
140
- const payload = contentType.includes('application/json')
141
- ? await response.json().catch(() => null)
142
- : await response.text();
146
+ const payload = contentType.includes('application/json') ? await response.json().catch(() => null) : await response.text();
143
147
  if (!response.ok) {
144
148
  const message = getErrorMessage(payload, response.statusText);
145
149
  switch (response.status) {
146
- case 401: throw errAuth(message);
147
- case 403: throw errForbidden(message);
148
- case 404: throw errAPI(404, message);
149
- case 429: throw errRateLimit();
150
- default: throw errAPI(response.status, message);
150
+ case 401:
151
+ throw errAuth(message);
152
+ case 403:
153
+ throw errForbidden(message);
154
+ case 404:
155
+ throw errAPI(404, message);
156
+ case 429:
157
+ throw errRateLimit();
158
+ default:
159
+ throw errAPI(response.status, message);
151
160
  }
152
161
  }
153
162
  return payload;
@@ -5,30 +5,46 @@ import { loadConfig } from '../config/config.js';
5
5
  import { errAuth, errUsage } from '../output/errors.js';
6
6
  import { brand } from '../output/theme.js';
7
7
  import { requireHighImpactConfirmation } from '../confirmation.js';
8
+ const validCloseReasons = new Set(['DUPLICATE', 'NOT_PLANNED', 'COULD_NOT_REPRODUCE', 'NOT_ACTIONABLE', 'NO_LONGER_RELEVANT', 'SPAM', 'OTHER']);
8
9
  export function createFeedbackBulkUpdateCommand(getWriter) {
9
10
  return new Command('bulk-update')
10
11
  .description('Update status for multiple feedback items at once')
11
12
  .requiredOption('--status <status>', 'New status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
13
+ .option('--close-reason <reason>', 'Reason for closing (DUPLICATE, NOT_PLANNED, COULD_NOT_REPRODUCE, NOT_ACTIONABLE, NO_LONGER_RELEVANT, SPAM, OTHER)')
14
+ .option('--close-note <note>', 'Internal closure note; required when the reason is OTHER')
12
15
  .requiredOption('--ids <ids>', 'Comma-separated feedback IDs')
13
16
  .option('--yes', 'Confirm the bulk update')
14
17
  .action(async (opts) => {
15
18
  const writer = getWriter();
16
19
  const client = requireClient();
17
- const ids = opts.ids.split(',').map((id) => id.trim()).filter(Boolean);
20
+ const ids = opts.ids
21
+ .split(',')
22
+ .map((id) => id.trim())
23
+ .filter(Boolean);
18
24
  if (ids.length === 0) {
19
25
  throw errUsage('At least one ID is required', 'Example: --ids id1,id2,id3');
20
26
  }
27
+ if (opts.status === 'CLOSED' && !opts.closeReason) {
28
+ throw errUsage('--close-reason is required when status is CLOSED');
29
+ }
30
+ if (opts.closeReason && !validCloseReasons.has(opts.closeReason)) {
31
+ throw errUsage('Invalid close reason. Must be one of: DUPLICATE, NOT_PLANNED, COULD_NOT_REPRODUCE, NOT_ACTIONABLE, NO_LONGER_RELEVANT, SPAM, OTHER');
32
+ }
33
+ if (opts.closeReason === 'OTHER' && !opts.closeNote?.trim()) {
34
+ throw errUsage('--close-note is required when --close-reason is OTHER');
35
+ }
21
36
  await requireHighImpactConfirmation(writer, Boolean(opts.yes), `Update ${ids.length} feedback item${ids.length === 1 ? '' : 's'}?`, '--yes is required for a bulk update in machine mode.');
22
- const result = await client.bulkUpdateStatus(ids, opts.status);
37
+ const result = await client.bulkUpdateStatus(ids, opts.status, {
38
+ closeReason: opts.closeReason,
39
+ closeNote: opts.closeNote,
40
+ });
23
41
  if (!writer.isMachineOutput()) {
24
42
  console.log(` ${brand.success('✓')} Updated ${result.updated} feedback items to ${brand.bold(result.status)}`);
25
43
  console.log();
26
44
  }
27
45
  writer.ok(result, {
28
46
  summary: `Updated ${result.updated} items to ${result.status}`,
29
- breadcrumbs: [
30
- { action: 'List feedback', cmd: 'feedbackbasket feedback list' },
31
- ],
47
+ breadcrumbs: [{ action: 'List feedback', cmd: 'feedbackbasket feedback list' }],
32
48
  });
33
49
  });
34
50
  }
@@ -8,6 +8,7 @@ import { resolveProject } from '../resolve.js';
8
8
  const validTypes = new Set(['bug', 'feature', 'general']);
9
9
  const validCategories = new Set(['BUG', 'FEATURE_REQUEST', 'IMPROVEMENT', 'QUESTION']);
10
10
  const validStatuses = new Set(['OPEN', 'UNDER_REVIEW', 'PLANNED', 'IN_PROGRESS', 'COMPLETE', 'CLOSED']);
11
+ const validCloseReasons = new Set(['DUPLICATE', 'NOT_PLANNED', 'COULD_NOT_REPRODUCE', 'NOT_ACTIONABLE', 'NO_LONGER_RELEVANT', 'SPAM', 'OTHER']);
11
12
  export function createFeedbackCreateCommand(getWriter) {
12
13
  return new Command('create')
13
14
  .argument('<title>', 'Short title or summary for the feedback')
@@ -17,6 +18,8 @@ export function createFeedbackCreateCommand(getWriter) {
17
18
  .option('--type <type>', 'Feedback type (bug, feature, general)')
18
19
  .option('--category <category>', 'Category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
19
20
  .option('--status <status>', 'Initial status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
21
+ .option('--close-reason <reason>', 'Reason when the initial status is CLOSED')
22
+ .option('--close-note <note>', 'Internal closure note; required when the reason is OTHER')
20
23
  .option('--email <email>', 'Submitter email')
21
24
  .option('--page-url <url>', 'Page URL where the feedback applies')
22
25
  .option('--metadata <key=value>', 'Metadata key/value pair (repeatable)', collectMetadata, [])
@@ -32,6 +35,15 @@ export function createFeedbackCreateCommand(getWriter) {
32
35
  if (opts.status && !validStatuses.has(opts.status)) {
33
36
  throw errUsage('Invalid status. Must be one of: OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED');
34
37
  }
38
+ if (opts.status === 'CLOSED' && !opts.closeReason) {
39
+ throw errUsage('--close-reason is required when status is CLOSED');
40
+ }
41
+ if (opts.closeReason && !validCloseReasons.has(opts.closeReason)) {
42
+ throw errUsage('Invalid close reason. Must be one of: DUPLICATE, NOT_PLANNED, COULD_NOT_REPRODUCE, NOT_ACTIONABLE, NO_LONGER_RELEVANT, SPAM, OTHER');
43
+ }
44
+ if (opts.closeReason === 'OTHER' && !opts.closeNote?.trim()) {
45
+ throw errUsage('--close-note is required when --close-reason is OTHER');
46
+ }
35
47
  const project = await resolveProject(client, opts.project);
36
48
  const content = composeContent(title, opts.content);
37
49
  const metadata = parseMetadata(opts.metadata ?? []);
@@ -41,6 +53,8 @@ export function createFeedbackCreateCommand(getWriter) {
41
53
  type: opts.type,
42
54
  category: opts.category,
43
55
  status: opts.status,
56
+ closeReason: opts.closeReason,
57
+ closeNote: opts.closeNote,
44
58
  email: opts.email,
45
59
  pageUrl: opts.pageUrl,
46
60
  metadata,
@@ -54,9 +68,18 @@ export function createFeedbackCreateCommand(getWriter) {
54
68
  writer.ok(result, {
55
69
  summary: `Created feedback ${result.id}`,
56
70
  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}` },
71
+ {
72
+ action: 'View feedback',
73
+ cmd: `feedbackbasket feedback show ${result.id}`,
74
+ },
75
+ {
76
+ action: 'Update status',
77
+ cmd: `feedbackbasket feedback update ${result.id} --status UNDER_REVIEW`,
78
+ },
79
+ {
80
+ action: 'List project feedback',
81
+ cmd: `feedbackbasket feedback list --project ${project.id}`,
82
+ },
60
83
  ],
61
84
  });
62
85
  });
@@ -4,11 +4,14 @@ 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
+ const validCloseReasons = new Set(['DUPLICATE', 'NOT_PLANNED', 'COULD_NOT_REPRODUCE', 'NOT_ACTIONABLE', 'NO_LONGER_RELEVANT', 'SPAM', 'OTHER']);
7
8
  export function createFeedbackUpdateCommand(getWriter) {
8
9
  return new Command('update')
9
10
  .argument('<id>', 'Feedback ID to update')
10
11
  .description('Update a feedback item')
11
12
  .option('--status <status>', 'Set status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
13
+ .option('--close-reason <reason>', 'Reason for closing (DUPLICATE, NOT_PLANNED, COULD_NOT_REPRODUCE, NOT_ACTIONABLE, NO_LONGER_RELEVANT, SPAM, OTHER)')
14
+ .option('--close-note <note>', 'Internal closure note; required when the reason is OTHER')
12
15
  .option('--category <category>', 'Set category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
13
16
  .option('--sentiment <sentiment>', 'Set sentiment (POSITIVE, NEGATIVE, NEUTRAL)')
14
17
  .action(async (id, opts) => {
@@ -16,6 +19,15 @@ export function createFeedbackUpdateCommand(getWriter) {
16
19
  if (!opts.status && !opts.category && !opts.sentiment) {
17
20
  throw errUsage('At least one of --status, --category, or --sentiment is required', 'Example: feedbackbasket feedback update <id> --status PLANNED');
18
21
  }
22
+ if (opts.status === 'CLOSED' && !opts.closeReason) {
23
+ throw errUsage('--close-reason is required when status is CLOSED');
24
+ }
25
+ if (opts.closeReason && !validCloseReasons.has(opts.closeReason)) {
26
+ throw errUsage('Invalid close reason. Must be one of: DUPLICATE, NOT_PLANNED, COULD_NOT_REPRODUCE, NOT_ACTIONABLE, NO_LONGER_RELEVANT, SPAM, OTHER');
27
+ }
28
+ if (opts.closeReason === 'OTHER' && !opts.closeNote?.trim()) {
29
+ throw errUsage('--close-note is required when --close-reason is OTHER');
30
+ }
19
31
  const client = requireClient();
20
32
  const data = {};
21
33
  if (opts.status)
@@ -24,6 +36,10 @@ export function createFeedbackUpdateCommand(getWriter) {
24
36
  data['category'] = opts.category;
25
37
  if (opts.sentiment)
26
38
  data['sentiment'] = opts.sentiment;
39
+ if (opts.closeReason)
40
+ data['closeReason'] = opts.closeReason;
41
+ if (opts.closeNote)
42
+ data['closeNote'] = opts.closeNote;
27
43
  const updated = await client.updateFeedback(id, data);
28
44
  if (!writer.isMachineOutput()) {
29
45
  console.log(brand.success(`Updated feedback ${id}`));
@@ -33,12 +49,20 @@ export function createFeedbackUpdateCommand(getWriter) {
33
49
  console.log(` Category: ${opts.category}`);
34
50
  if (opts.sentiment)
35
51
  console.log(` Sentiment: ${opts.sentiment}`);
52
+ if (opts.closeReason)
53
+ console.log(` Reason: ${opts.closeReason}`);
36
54
  }
37
55
  writer.ok(updated, {
38
56
  summary: `Updated feedback ${id}`,
39
57
  breadcrumbs: [
40
- { action: 'View updated item', cmd: `feedbackbasket feedback show ${id}` },
41
- { action: 'Add a note', cmd: `feedbackbasket feedback note ${id} "<note>"` },
58
+ {
59
+ action: 'View updated item',
60
+ cmd: `feedbackbasket feedback show ${id}`,
61
+ },
62
+ {
63
+ action: 'Add a note',
64
+ cmd: `feedbackbasket feedback note ${id} "<note>"`,
65
+ },
42
66
  { action: 'Back to list', cmd: 'feedbackbasket feedback list' },
43
67
  ],
44
68
  });
@@ -23,8 +23,7 @@ import { createFeedbackBulkUpdateCommand } from './feedback-bulk-update.js';
23
23
  import { createFeedbackExportCommand } from './feedback-export.js';
24
24
  import { createFeedbackReplyCommand, createFeedbackRepliesCommand } from './feedback-reply.js';
25
25
  export function createFeedbackCommand(getWriter) {
26
- const feedback = new Command('feedback')
27
- .description('View and manage feedback');
26
+ const feedback = new Command('feedback').description('View and manage feedback');
28
27
  // Write subcommands
29
28
  feedback.addCommand(createFeedbackCreateCommand(getWriter));
30
29
  feedback.addCommand(createFeedbackUpdateCommand(getWriter));
@@ -42,6 +41,7 @@ export function createFeedbackCommand(getWriter) {
42
41
  .option('--all', 'Show feedback across all projects (ignore default project)')
43
42
  .option('--category <category>', 'Filter by category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
44
43
  .option('--status <status>', 'Filter by status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
44
+ .option('--close-reason <reason>', 'Filter by close reason')
45
45
  .option('--sentiment <sentiment>', 'Filter by sentiment (POSITIVE, NEGATIVE, NEUTRAL)')
46
46
  .option('--search <query>', 'Search feedback content')
47
47
  .option('--limit <n>', 'Max results (1-100)', '20')
@@ -54,6 +54,7 @@ export function createFeedbackCommand(getWriter) {
54
54
  projectId: await resolveProjectId(client, opts.project, opts.all),
55
55
  category: opts.category,
56
56
  status: opts.status,
57
+ closeReason: opts.closeReason,
57
58
  sentiment: opts.sentiment,
58
59
  search: opts.search,
59
60
  limit: parseInt(opts.limit, 10),
@@ -65,13 +66,22 @@ export function createFeedbackCommand(getWriter) {
65
66
  }
66
67
  const breadcrumbs = [];
67
68
  if (opts.project) {
68
- breadcrumbs.push({ action: 'View bugs for this project', cmd: `feedbackbasket bugs list --project ${opts.project}` });
69
+ breadcrumbs.push({
70
+ action: 'View bugs for this project',
71
+ cmd: `feedbackbasket bugs list --project ${opts.project}`,
72
+ });
69
73
  }
70
74
  if (result.pagination.hasMore) {
71
75
  const nextOffset = parseInt(opts.offset, 10) + parseInt(opts.limit, 10);
72
- breadcrumbs.push({ action: 'Next page', cmd: `feedbackbasket feedback list --offset ${nextOffset} --limit ${opts.limit}${opts.project ? ` --project ${opts.project}` : ''}` });
76
+ breadcrumbs.push({
77
+ action: 'Next page',
78
+ cmd: `feedbackbasket feedback list --offset ${nextOffset} --limit ${opts.limit}${opts.project ? ` --project ${opts.project}` : ''}`,
79
+ });
73
80
  }
74
- breadcrumbs.push({ action: 'Search', cmd: 'feedbackbasket feedback search "<query>"' });
81
+ breadcrumbs.push({
82
+ action: 'Search',
83
+ cmd: 'feedbackbasket feedback search "<query>"',
84
+ });
75
85
  writer.ok(result.feedback, {
76
86
  summary: `Showing ${result.feedback.length} of ${result.pagination.totalCount} feedback items`,
77
87
  notice: result.pagination.hasMore ? `Use --offset ${parseInt(opts.offset, 10) + parseInt(opts.limit, 10)} to see more` : undefined,
@@ -92,8 +102,14 @@ export function createFeedbackCommand(getWriter) {
92
102
  writer.ok(item, {
93
103
  summary: `Feedback ${item.id}`,
94
104
  breadcrumbs: [
95
- { action: 'Update status', cmd: `feedbackbasket feedback update ${item.id} --status <STATUS>` },
96
- { action: 'Add note', cmd: `feedbackbasket feedback note ${item.id} "<note>"` },
105
+ {
106
+ action: 'Update status',
107
+ cmd: `feedbackbasket feedback update ${item.id} --status <STATUS>`,
108
+ },
109
+ {
110
+ action: 'Add note',
111
+ cmd: `feedbackbasket feedback note ${item.id} "<note>"`,
112
+ },
97
113
  { action: 'Back to list', cmd: 'feedbackbasket feedback list' },
98
114
  ],
99
115
  });
@@ -120,7 +136,10 @@ export function createFeedbackCommand(getWriter) {
120
136
  summary: `${result.pagination.totalCount} result${result.pagination.totalCount === 1 ? '' : 's'} for "${query}"`,
121
137
  breadcrumbs: [
122
138
  { action: 'List all feedback', cmd: 'feedbackbasket feedback list' },
123
- { action: 'Search bugs', cmd: `feedbackbasket bugs list --search "${query}"` },
139
+ {
140
+ action: 'Search bugs',
141
+ cmd: `feedbackbasket bugs list --search "${query}"`,
142
+ },
124
143
  ],
125
144
  });
126
145
  });
@@ -156,7 +175,7 @@ function renderFeedbackList(items) {
156
175
  const cat = categoryEmoji[item.category ?? ''] ?? brand.muted('[?]');
157
176
  const priority = priorityLabel(item.aiPriorityScore);
158
177
  const content = item.content.length > 80 ? item.content.slice(0, 77) + '...' : item.content;
159
- const status = brand.muted(`[${item.status}]`);
178
+ const status = brand.muted(`[${item.status}${item.closeReason ? ` · ${item.closeReason}` : ''}]`);
160
179
  const proj = item.project ? brand.primary(item.project.name) : '';
161
180
  console.log(`${cat} ${priority} ${status} ${proj} ${brand.muted(item.id)}`);
162
181
  console.log(` ${content}`);
@@ -175,6 +194,8 @@ function renderFeedbackDetail(item) {
175
194
  console.log();
176
195
  const fields = [
177
196
  ['Status', item.status],
197
+ ['Close Reason', item.closeReason],
198
+ ['Close Note', item.closeNote],
178
199
  ['Category', item.category],
179
200
  ['Feedback Type', formatFeedbackType(item)],
180
201
  ['Sentiment', item.sentiment],
@@ -1,4 +1,5 @@
1
1
  export type FeedbackStatus = 'OPEN' | 'UNDER_REVIEW' | 'PLANNED' | 'IN_PROGRESS' | 'COMPLETE' | 'CLOSED';
2
+ export type FeedbackCloseReason = 'DUPLICATE' | 'NOT_PLANNED' | 'COULD_NOT_REPRODUCE' | 'NOT_ACTIONABLE' | 'NO_LONGER_RELEVANT' | 'SPAM' | 'OTHER';
2
3
  export type FeedbackCategory = 'BUG' | 'FEATURE_REQUEST' | 'IMPROVEMENT' | 'QUESTION';
3
4
  export type Sentiment = 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
4
5
  export type Severity = 'high' | 'medium' | 'low';
@@ -93,6 +94,9 @@ export interface Feedback {
93
94
  content: string;
94
95
  email?: string | null;
95
96
  status: FeedbackStatus;
97
+ closeReason?: FeedbackCloseReason | null;
98
+ closeNote?: string | null;
99
+ closedAt?: string | null;
96
100
  category?: FeedbackCategory | null;
97
101
  sentiment?: Sentiment | null;
98
102
  aiSummary?: string | null;
@@ -186,6 +190,8 @@ export interface FeedbackCreateInput {
186
190
  type?: 'bug' | 'feature' | 'general';
187
191
  category?: FeedbackCategory;
188
192
  status?: FeedbackStatus;
193
+ closeReason?: FeedbackCloseReason;
194
+ closeNote?: string;
189
195
  email?: string;
190
196
  pageUrl?: string;
191
197
  metadata?: Record<string, unknown>;
@@ -232,6 +238,7 @@ export interface FeedbackParams {
232
238
  projectId?: string;
233
239
  category?: FeedbackCategory;
234
240
  status?: FeedbackStatus;
241
+ closeReason?: FeedbackCloseReason;
235
242
  sentiment?: Sentiment;
236
243
  search?: string;
237
244
  limit?: number;
@@ -1,2 +1,2 @@
1
- export declare const VERSION: "3.1.0";
1
+ export declare const VERSION: "3.2.0";
2
2
  export declare const USER_AGENT: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feedbackbasket-cli",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Command-line interface for FeedbackBasket — manage feedback and waitlists from your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/src/cli.js",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "chalk": "^5.3.0",
36
36
  "commander": "^13.1.0",
37
- "feedbackbasket-agent-contract": "3.1.0",
37
+ "feedbackbasket-agent-contract": "3.2.0",
38
38
  "open": "^10.1.0"
39
39
  },
40
40
  "devDependencies": {
@@ -7,7 +7,7 @@ description: Manage FeedbackBasket projects, feedback, bugs, website widgets, mo
7
7
 
8
8
  Full command-line interface for managing feedback, waitlist signups, bug reports, projects, widgets, and teams in FeedbackBasket. Works with any AI agent that can run shell commands.
9
9
 
10
- The unified agent surface version is `3.1.0`. It has 31 product operations. The CLI, stdio MCP package, and live Streamable HTTP MCP server implement the same contract.
10
+ The unified agent surface version is `3.2.0`. It has 31 product operations. The CLI, stdio MCP package, and live Streamable HTTP MCP server implement the same contract.
11
11
 
12
12
  ## Authentication
13
13
 
@@ -39,7 +39,7 @@ Use the CLI when the agent has shell access and an existing CLI login. Use MCP w
39
39
 
40
40
  For remote MCP, add `https://feedbackbasket.com/.well-known/mcp` to the host. Save it, select **Authenticate**, sign in, select an organization, select Read or Full access, select Selected projects or All projects, and select **Allow**. Browser OAuth is the recommended remote setup. Do not ask the user to paste an OAuth token.
41
41
 
42
- For local STDIO MCP, CI, servers, or unattended automation, use `feedbackbasket-mcp-server@3.1.0` with an `fb_key_` credential from the host credential store or an environment variable. Browser OAuth is only for Streamable HTTP. STDIO still uses an environment credential. The CLI keeps `feedbackbasket login` and its private `fb_cli_` token flow in this release.
42
+ For local STDIO MCP, CI, servers, or unattended automation, use `feedbackbasket-mcp-server@3.2.0` with an `fb_key_` credential from the host credential store or an environment variable. Browser OAuth is only for Streamable HTTP. STDIO still uses an environment credential. The CLI keeps `feedbackbasket login` and its private `fb_cli_` token flow in this release.
43
43
 
44
44
  Access tokens, refresh tokens, CLI tokens, and MCP keys are private and are not interchangeable. Never put a credential in source, command arguments, logs, prompts, snapshots, generated files, or final responses. Use browser OAuth, the host credential store, or an environment variable as applicable.
45
45
 
@@ -135,6 +135,7 @@ Treat a supplied project key as production unless the user explicitly confirms a
135
135
  # Read
136
136
  feedbackbasket feedback list --project <id> --category BUG --status OPEN --sentiment NEGATIVE
137
137
  feedbackbasket feedback list --search "login" --limit 50 --offset 0 --notes
138
+ feedbackbasket feedback list --status CLOSED --close-reason NOT_PLANNED
138
139
  feedbackbasket feedback show <id>
139
140
  feedbackbasket feedback search "crash on mobile" --project <id> --limit 10
140
141
 
@@ -146,7 +147,9 @@ feedbackbasket feedback note <id> "Investigating — appears related to auth flo
146
147
  feedbackbasket feedback note update <id> <note-id> --content "Updated internal note"
147
148
  feedbackbasket feedback note delete <id> <note-id> --yes
148
149
  feedbackbasket feedback delete <id> --yes
149
- feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3 --yes
150
+ feedbackbasket feedback update <id> --status CLOSED --close-reason NOT_PLANNED
151
+ feedbackbasket feedback update <id> --status CLOSED --close-reason OTHER --close-note "Reason for closing"
152
+ feedbackbasket feedback bulk-update --status CLOSED --close-reason NOT_ACTIONABLE --ids id1,id2,id3 --yes
150
153
 
151
154
  # Reply to submitter by email, widget/in-app thread, or both
152
155
  feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!" --delivery email --reply-to support@example.com --yes
@@ -391,12 +394,13 @@ feedbackbasket feedback search "crash" --category BUG --agent
391
394
 
392
395
  ## Filtering Options
393
396
 
394
- | Type | Values |
395
- | ------------ | ---------------------------------------------------------------------- |
396
- | Categories | `BUG`, `FEATURE_REQUEST`, `IMPROVEMENT`, `QUESTION` |
397
- | Statuses | `OPEN`, `UNDER_REVIEW`, `PLANNED`, `IN_PROGRESS`, `COMPLETE`, `CLOSED` |
398
- | Sentiments | `POSITIVE`, `NEGATIVE`, `NEUTRAL` |
399
- | Bug Severity | `high`, `medium`, `low` |
397
+ | Type | Values |
398
+ | ------------- | ---------------------------------------------------------------------------------------------------------- |
399
+ | Categories | `BUG`, `FEATURE_REQUEST`, `IMPROVEMENT`, `QUESTION` |
400
+ | Statuses | `OPEN`, `UNDER_REVIEW`, `PLANNED`, `IN_PROGRESS`, `COMPLETE`, `CLOSED` |
401
+ | Close reasons | `DUPLICATE`, `NOT_PLANNED`, `COULD_NOT_REPRODUCE`, `NOT_ACTIONABLE`, `NO_LONGER_RELEVANT`, `SPAM`, `OTHER` |
402
+ | Sentiments | `POSITIVE`, `NEGATIVE`, `NEUTRAL` |
403
+ | Bug Severity | `high`, `medium`, `low` |
400
404
 
401
405
  ## JSON Envelope
402
406
 
@@ -432,5 +436,6 @@ Errors include hints:
432
436
  - Project names resolve case-insensitively with fuzzy matching
433
437
  - Write operations use full scope (granted by default during login)
434
438
  - Feedback IDs are stable CUIDs — safe to reference across commands
439
+ - Closing feedback requires `--close-reason`. The `OTHER` reason also requires `--close-note`.
435
440
  - All timestamps are ISO 8601
436
441
  - `--yes` confirms all high-impact CLI operations in agent or machine mode.