feedbackbasket-cli 0.11.0 → 3.0.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.
@@ -0,0 +1,38 @@
1
+ import { type ProductOperationId } from 'feedbackbasket-agent-contract';
2
+ export declare const CLI_CAPABILITIES: readonly {
3
+ operationId: ProductOperationId;
4
+ commands: readonly string[];
5
+ }[];
6
+ export declare const CLI_EXEMPTIONS: ({
7
+ readonly id: "auth";
8
+ readonly surface: "cli";
9
+ readonly reason: "Authentication is local CLI credential management, not a product operation.";
10
+ } | {
11
+ readonly id: "login";
12
+ readonly surface: "cli";
13
+ readonly reason: "The login shortcut starts the local CLI authentication flow.";
14
+ } | {
15
+ readonly id: "logout";
16
+ readonly surface: "cli";
17
+ readonly reason: "Logout removes a local CLI credential.";
18
+ } | {
19
+ readonly id: "doctor";
20
+ readonly surface: "cli";
21
+ readonly reason: "Doctor checks local CLI configuration and connectivity.";
22
+ } | {
23
+ readonly id: "setup";
24
+ readonly surface: "cli";
25
+ readonly reason: "Setup installs local agent guidance and does not change FeedbackBasket product data.";
26
+ } | {
27
+ readonly id: "output";
28
+ readonly surface: "cli";
29
+ readonly reason: "Output flags change terminal formatting only.";
30
+ } | {
31
+ readonly id: "initialize";
32
+ readonly surface: "mcp";
33
+ readonly reason: "MCP initialization is a transport protocol operation.";
34
+ } | {
35
+ readonly id: "resources";
36
+ readonly surface: "mcp";
37
+ readonly reason: "Public MCP resources describe the service and do not access customer product data.";
38
+ })[];
@@ -0,0 +1,6 @@
1
+ import { PARITY_EXEMPTIONS, PRODUCT_OPERATIONS, } from 'feedbackbasket-agent-contract';
2
+ export const CLI_CAPABILITIES = PRODUCT_OPERATIONS.map((operation) => ({
3
+ operationId: operation.id,
4
+ commands: operation.cli.commands,
5
+ }));
6
+ export const CLI_EXEMPTIONS = PARITY_EXEMPTIONS.filter(({ surface }) => surface === 'cli');
package/dist/src/cli.d.ts CHANGED
@@ -1 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createProgram(): Command;
1
3
  export declare function run(): void;
package/dist/src/cli.js CHANGED
@@ -27,7 +27,7 @@ function resolveFormat(opts) {
27
27
  function getWriter() {
28
28
  return writer;
29
29
  }
30
- export function run() {
30
+ export function createProgram() {
31
31
  const program = new Command('feedbackbasket')
32
32
  .version(VERSION, '-v, --version')
33
33
  .description('Command-line interface for FeedbackBasket')
@@ -71,6 +71,10 @@ export function run() {
71
71
  program.addCommand(createSetupCommand(getWriter));
72
72
  // Global error handler
73
73
  program.exitOverride();
74
+ return program;
75
+ }
76
+ export function run() {
77
+ const program = createProgram();
74
78
  (async () => {
75
79
  try {
76
80
  await program.parseAsync(process.argv);
@@ -56,6 +56,7 @@ export declare class FeedbackBasketClient {
56
56
  getMobileIntegration(projectId: string, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
57
57
  updateMobileIntegration(projectId: string, data: {
58
58
  enabled?: boolean;
59
+ allowVisitorReplies?: boolean;
59
60
  addBundleIds?: string[];
60
61
  removeBundleIds?: string[];
61
62
  }, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import type { OutputWriter } from '../output/writer.js';
3
3
  export declare function createAuthCommand(getWriter: () => OutputWriter): Command;
4
+ export declare function resolveAuthScope(value: string): 'read' | 'full';
4
5
  export declare function createLoginCommand(getWriter: () => OutputWriter): Command;
5
6
  export declare function createLogoutCommand(getWriter: () => OutputWriter): Command;
@@ -23,7 +23,7 @@ export function createAuthCommand(getWriter) {
23
23
  .action(async (opts) => {
24
24
  const writer = getWriter();
25
25
  const config = loadConfig();
26
- const scope = opts.scope === 'read' ? 'read' : 'full';
26
+ const scope = resolveAuthScope(opts.scope);
27
27
  const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
28
28
  // ── Step 1: Authentication ──
29
29
  if (isInteractive) {
@@ -294,6 +294,11 @@ export function createAuthCommand(getWriter) {
294
294
  });
295
295
  return auth;
296
296
  }
297
+ export function resolveAuthScope(value) {
298
+ if (value === 'read' || value === 'full')
299
+ return value;
300
+ throw errUsage('Scope must be "read" or "full"');
301
+ }
297
302
  // Top-level aliases: `feedbackbasket login` and `feedbackbasket logout`
298
303
  export function createLoginCommand(getWriter) {
299
304
  return new Command('login')
@@ -4,11 +4,13 @@ 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 { requireHighImpactConfirmation } from '../confirmation.js';
7
8
  export function createFeedbackBulkUpdateCommand(getWriter) {
8
9
  return new Command('bulk-update')
9
10
  .description('Update status for multiple feedback items at once')
10
11
  .requiredOption('--status <status>', 'New status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
11
12
  .requiredOption('--ids <ids>', 'Comma-separated feedback IDs')
13
+ .option('--yes', 'Confirm the bulk update')
12
14
  .action(async (opts) => {
13
15
  const writer = getWriter();
14
16
  const client = requireClient();
@@ -16,6 +18,7 @@ export function createFeedbackBulkUpdateCommand(getWriter) {
16
18
  if (ids.length === 0) {
17
19
  throw errUsage('At least one ID is required', 'Example: --ids id1,id2,id3');
18
20
  }
21
+ 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.');
19
22
  const result = await client.bulkUpdateStatus(ids, opts.status);
20
23
  if (!writer.isMachineOutput()) {
21
24
  console.log(` ${brand.success('✓')} Updated ${result.updated} feedback items to ${brand.bold(result.status)}`);
@@ -4,7 +4,7 @@ import { AuthManager } from '../auth/manager.js';
4
4
  import { loadConfig } from '../config/config.js';
5
5
  import { errAuth } from '../output/errors.js';
6
6
  import { brand } from '../output/theme.js';
7
- import { confirm } from '../prompt.js';
7
+ import { requireHighImpactConfirmation } from '../confirmation.js';
8
8
  export function createFeedbackDeleteCommand(getWriter) {
9
9
  return new Command('delete')
10
10
  .argument('<id>', 'Feedback ID to delete')
@@ -16,12 +16,8 @@ export function createFeedbackDeleteCommand(getWriter) {
16
16
  if (!opts.yes && !writer.isMachineOutput() && process.stdin.isTTY) {
17
17
  console.log(` ${brand.warning('Warning:')} This will permanently delete feedback ${brand.bold(id)}`);
18
18
  console.log();
19
- const confirmed = await confirm(' Delete this feedback?', false);
20
- if (!confirmed) {
21
- console.log(brand.muted(' Cancelled.'));
22
- return;
23
- }
24
19
  }
20
+ await requireHighImpactConfirmation(writer, Boolean(opts.yes), 'Delete this feedback?', '--yes is required to delete feedback in machine mode.');
25
21
  const result = await client.deleteFeedback(id);
26
22
  if (!writer.isMachineOutput()) {
27
23
  console.log(` ${brand.success('✓')} Deleted feedback ${id}`);
@@ -4,15 +4,19 @@ 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 { requireHighImpactConfirmation } from '../confirmation.js';
7
8
  export function createFeedbackNoteCommand(getWriter) {
8
- return new Command('note')
9
- .argument('<id>', 'Feedback ID to add a note to')
9
+ const note = new Command('note')
10
+ .argument('[id]', 'Feedback ID to add a note to')
10
11
  .argument('[content]', 'Note content (or use --content)')
11
12
  .description('Add an internal note to a feedback item')
12
13
  .option('--content <text>', 'Note content (alternative to positional argument)')
13
14
  .action(async (id, contentArg, opts) => {
14
15
  const writer = getWriter();
15
16
  const content = contentArg ?? opts.content;
17
+ if (!id) {
18
+ throw errUsage('Feedback ID is required', 'Example: feedbackbasket feedback note <id> "Your note here"');
19
+ }
16
20
  if (!content) {
17
21
  throw errUsage('Note content is required', 'Example: feedbackbasket feedback note <id> "Your note here"');
18
22
  }
@@ -30,6 +34,34 @@ export function createFeedbackNoteCommand(getWriter) {
30
34
  ],
31
35
  });
32
36
  });
37
+ note
38
+ .command('update <feedbackId> <noteId>')
39
+ .description('Update an internal feedback note')
40
+ .requiredOption('--content <text>', 'New note content')
41
+ .action(async (feedbackId, noteId, opts) => {
42
+ const writer = getWriter();
43
+ const client = requireClient();
44
+ const result = await client.updateNote(feedbackId, noteId, opts.content);
45
+ writer.ok(result, {
46
+ summary: `Updated note ${noteId}`,
47
+ breadcrumbs: [{ action: 'View feedback', cmd: `feedbackbasket feedback show ${feedbackId}` }],
48
+ });
49
+ });
50
+ note
51
+ .command('delete <feedbackId> <noteId>')
52
+ .description('Delete an internal feedback note')
53
+ .option('--yes', 'Confirm note deletion')
54
+ .action(async (feedbackId, noteId, opts) => {
55
+ const writer = getWriter();
56
+ const client = requireClient();
57
+ await requireHighImpactConfirmation(writer, Boolean(opts.yes), `Delete note ${noteId}?`, '--yes is required to delete a note in machine mode.');
58
+ const result = await client.deleteNote(feedbackId, noteId);
59
+ writer.ok(result, {
60
+ summary: `Deleted note ${noteId}`,
61
+ breadcrumbs: [{ action: 'View feedback', cmd: `feedbackbasket feedback show ${feedbackId}` }],
62
+ });
63
+ });
64
+ return note;
33
65
  }
34
66
  function requireClient() {
35
67
  const manager = new AuthManager();
@@ -5,6 +5,7 @@ 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 { ask } from '../prompt.js';
8
+ import { requireHighImpactConfirmation } from '../confirmation.js';
8
9
  const deliveryOptions = new Set(['email', 'widget', 'in-app', 'both']);
9
10
  export function createFeedbackReplyCommand(getWriter) {
10
11
  return new Command('reply')
@@ -14,6 +15,7 @@ export function createFeedbackReplyCommand(getWriter) {
14
15
  .option('--content <text>', 'Reply content (alternative to positional argument)')
15
16
  .option('--delivery <delivery>', 'Reply delivery (email, widget, in-app, both)', 'email')
16
17
  .option('--reply-to <email>', 'Reply-to email for email delivery')
18
+ .option('--yes', 'Confirm that the reply can be sent')
17
19
  .action(async (id, contentArg, opts) => {
18
20
  const writer = getWriter();
19
21
  const content = contentArg ?? opts.content;
@@ -61,6 +63,7 @@ export function createFeedbackReplyCommand(getWriter) {
61
63
  if (sendsWidget && !feedback.hasWidgetAccess) {
62
64
  throw errUsage('This feedback has no in-app or widget reply thread.', 'Use --delivery email for feedback with an email address, or ask the human how they want to respond.');
63
65
  }
66
+ await requireHighImpactConfirmation(writer, Boolean(opts.yes), `Send this reply by ${delivery}?`, '--yes is required to send a reply in machine mode.');
64
67
  const result = await client.sendReply(id, content, {
65
68
  replyToEmail: replyTo,
66
69
  destinations,
@@ -96,44 +99,53 @@ export function createFeedbackReplyCommand(getWriter) {
96
99
  export function createFeedbackRepliesCommand(getWriter) {
97
100
  return new Command('replies')
98
101
  .argument('<id>', 'Feedback ID')
99
- .description('List all replies sent for a feedback item')
102
+ .description('List the complete feedback conversation')
100
103
  .action(async (id) => {
101
104
  const writer = getWriter();
102
105
  const client = requireClient();
103
106
  const result = await client.listReplies(id);
104
107
  const messages = result.messages ?? [];
105
- const visibleWidgetMessages = messages.filter((item) => !item.replyId);
108
+ const linkedReplyIds = new Set(messages.map((item) => item.replyId).filter((replyId) => replyId !== null));
109
+ const visibleEmailReplies = result.replies.filter((item) => !linkedReplyIds.has(item.id));
110
+ const conversation = [
111
+ ...visibleEmailReplies.map((item) => ({ kind: 'email', item })),
112
+ ...messages.map((item) => ({ kind: 'thread', item })),
113
+ ].sort((left, right) => new Date(left.item.createdAt).getTime() - new Date(right.item.createdAt).getTime());
106
114
  if (!writer.isMachineOutput()) {
107
115
  if (result.total === 0) {
108
116
  console.log(brand.muted(' No replies sent yet.'));
109
117
  console.log();
110
118
  }
111
119
  else {
112
- console.log(brand.bold(`${result.total} repl${result.total === 1 ? 'y' : 'ies'} for feedback ${id}`));
120
+ console.log(brand.bold(`${result.total} conversation message${result.total === 1 ? '' : 's'} for feedback ${id}`));
113
121
  console.log();
114
- for (const r of result.replies) {
115
- console.log(` ${brand.success('->')} ${brand.bold(r.sentBy)} ${brand.muted(r.createdAt)}`);
116
- console.log(` ${brand.muted('Delivery:')} email`);
117
- console.log(` ${brand.muted('Reply-to:')} ${r.replyToEmail}`);
118
- console.log();
119
- for (const line of r.content.split('\n')) {
120
- console.log(` ${line}`);
122
+ for (const entry of conversation) {
123
+ if (entry.kind === 'email') {
124
+ const emailReply = entry.item;
125
+ console.log(` ${brand.success('->')} ${brand.bold(emailReply.sentBy)} ${brand.muted(emailReply.createdAt)}`);
126
+ console.log(` ${brand.muted('Delivery:')} email`);
127
+ console.log(` ${brand.muted('Reply-to:')} ${emailReply.replyToEmail}`);
128
+ console.log();
129
+ for (const line of emailReply.content.split('\n')) {
130
+ console.log(` ${line}`);
131
+ }
121
132
  }
122
- console.log();
123
- }
124
- for (const message of visibleWidgetMessages) {
125
- console.log(` ${brand.success('->')} ${brand.bold(message.sentByName ?? 'CLI')} ${brand.muted(message.createdAt)}`);
126
- console.log(` ${brand.muted('Delivery:')} widget`);
127
- console.log();
128
- for (const line of message.content.split('\n')) {
129
- console.log(` ${line}`);
133
+ else {
134
+ const message = entry.item;
135
+ const visitor = message.senderType === 'VISITOR';
136
+ console.log(` ${visitor ? brand.primary('<-') : brand.success('->')} ${brand.bold(visitor ? 'User' : (message.sentByName ?? 'CLI'))} ${brand.muted(message.createdAt)}`);
137
+ console.log(` ${brand.muted('Delivery:')} ${visitor ? 'visitor follow-up' : message.delivery === 'BOTH' ? 'email + widget/in-app' : 'widget/in-app'}`);
138
+ console.log();
139
+ for (const line of message.content.split('\n')) {
140
+ console.log(` ${line}`);
141
+ }
130
142
  }
131
143
  console.log();
132
144
  }
133
145
  }
134
146
  }
135
147
  writer.ok({ replies: result.replies, messages }, {
136
- summary: `${result.total} repl${result.total === 1 ? 'y' : 'ies'}`,
148
+ summary: `${result.total} conversation message${result.total === 1 ? '' : 's'}`,
137
149
  breadcrumbs: [
138
150
  { action: 'Send an email reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery email` },
139
151
  { action: 'Post a widget reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery widget` },
@@ -187,6 +187,7 @@ function renderFeedbackDetail(item) {
187
187
  ['Device', item.device],
188
188
  ['Language', item.language],
189
189
  ['Reply Channel', item.replyChannel === 'in_app' ? 'In-app' : item.hasWidgetAccess ? 'Widget' : null],
190
+ ['Conversation', item.awaitingOwnerReply ? 'User replied — team response needed' : null],
190
191
  ['Created', item.createdAt],
191
192
  ];
192
193
  for (const [label, value] of fields) {
@@ -31,6 +31,7 @@ export function createMobileCommand(getWriter) {
31
31
  ? [
32
32
  { action: 'Verify SDK connection', cmd: `feedbackbasket mobile verify ${ref}` },
33
33
  { action: 'Add a bundle ID', cmd: `feedbackbasket mobile bundle-ids ${ref} --add com.example.app` },
34
+ { action: 'Configure conversations', cmd: `feedbackbasket mobile conversations ${ref} --enable` },
34
35
  ]
35
36
  : [
36
37
  { action: 'Enable mobile feedback', cmd: `feedbackbasket mobile setup ${ref}` },
@@ -66,6 +67,7 @@ export function createMobileCommand(getWriter) {
66
67
  });
67
68
  mobile
68
69
  .command('bundle-ids [project]')
70
+ .alias('bundle')
69
71
  .description('Add or remove allowed iOS bundle IDs')
70
72
  .option('--add <bundle-id>', 'Bundle ID to add (repeatable)', collect, [])
71
73
  .option('--remove <bundle-id>', 'Bundle ID to remove (repeatable)', collect, [])
@@ -97,6 +99,31 @@ export function createMobileCommand(getWriter) {
97
99
  ],
98
100
  });
99
101
  });
102
+ mobile
103
+ .command('conversations [project]')
104
+ .description('Enable or disable in-app follow-up replies')
105
+ .option('--enable', 'Let users continue feedback conversations in the app')
106
+ .option('--disable', 'Keep in-app team replies read-only')
107
+ .action(async (projectArg, opts) => {
108
+ if (Boolean(opts.enable) === Boolean(opts.disable)) {
109
+ throw errUsage('Choose either --enable or --disable');
110
+ }
111
+ const writer = getWriter();
112
+ const client = requireClient();
113
+ const projectId = await resolveProjectId(client, projectArg);
114
+ const allowVisitorReplies = Boolean(opts.enable);
115
+ const result = await client.updateMobileIntegration(projectId, { allowVisitorReplies });
116
+ if (!writer.isMachineOutput()) {
117
+ console.log(` ${brand.success('✓')} In-app conversations ${allowVisitorReplies ? 'enabled' : 'disabled'} for ${brand.bold(result.project.name)}`);
118
+ console.log();
119
+ }
120
+ writer.ok(result, {
121
+ summary: `${allowVisitorReplies ? 'Enabled' : 'Disabled'} in-app follow-up replies for "${result.project.name}"`,
122
+ breadcrumbs: [
123
+ { action: 'View mobile status', cmd: `feedbackbasket mobile status ${projectRef(projectArg, projectId)}` },
124
+ ],
125
+ });
126
+ });
100
127
  mobile
101
128
  .command('verify [project]')
102
129
  .description('Verify that the SDK has connected from the expected app')
@@ -269,6 +296,7 @@ function renderMobileStatus(result) {
269
296
  }
270
297
  const integration = result.integration;
271
298
  console.log(` ${brand.label('Status'.padEnd(18))} ${integration.enabled ? 'Enabled' : 'Disabled'}`);
299
+ console.log(` ${brand.label('Conversations'.padEnd(18))} ${integration.allowVisitorReplies ? 'Enabled' : 'Read-only'}`);
272
300
  console.log(` ${brand.label('Publishable key'.padEnd(18))} ${integration.publishableKey}`);
273
301
  console.log(` ${brand.label('Bundle IDs'.padEnd(18))} ${integration.bundleIds.join(', ') || 'Any bundle ID'}`);
274
302
  console.log(` ${brand.label('Connection'.padEnd(18))} ${integration.connection.connected ? 'Connected' : 'Waiting for first heartbeat'}`);
@@ -5,6 +5,7 @@ import { loadConfig } from '../config/config.js';
5
5
  import { errAuth, errUsage } from '../output/errors.js';
6
6
  import { brand, divider } from '../output/theme.js';
7
7
  import { confirm } from '../prompt.js';
8
+ import { requireHighImpactConfirmation } from '../confirmation.js';
8
9
  import { resolveProject } from '../resolve.js';
9
10
  export function createProjectsCommand(getWriter) {
10
11
  const projects = new Command('projects')
@@ -139,17 +140,12 @@ export function createProjectsCommand(getWriter) {
139
140
  const resolved = await resolveProject(client, idOrName);
140
141
  const id = resolved.id;
141
142
  const projectName = resolved.name;
142
- // Confirmation (skip in agent mode or --yes)
143
143
  if (!opts.yes && !writer.isMachineOutput() && process.stdin.isTTY) {
144
144
  console.log(` ${brand.warning('Warning:')} This will permanently delete project "${brand.bold(projectName)}"`);
145
145
  console.log(` ${brand.muted('All feedback, notes, and settings will be lost.')}`);
146
146
  console.log();
147
- const confirmed = await confirm(` Delete "${projectName}"?`, false);
148
- if (!confirmed) {
149
- console.log(brand.muted(' Cancelled.'));
150
- return;
151
- }
152
147
  }
148
+ await requireHighImpactConfirmation(writer, Boolean(opts.yes), `Delete "${projectName}"?`, '--yes is required to delete a project in machine mode.');
153
149
  const result = await client.deleteProject(id);
154
150
  if (!writer.isMachineOutput()) {
155
151
  console.log(` ${brand.success('✓')} Deleted project "${brand.bold(result.name)}"`);
@@ -4,7 +4,7 @@ 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, divider } from '../output/theme.js';
7
- import { confirm } from '../prompt.js';
7
+ import { requireHighImpactConfirmation } from '../confirmation.js';
8
8
  export function createTeamCommand(getWriter) {
9
9
  const team = new Command('team')
10
10
  .description('Manage organization members');
@@ -31,12 +31,14 @@ export function createTeamCommand(getWriter) {
31
31
  .command('role <memberId>')
32
32
  .description('Update a member\'s role')
33
33
  .requiredOption('--role <role>', 'New role: admin or member')
34
+ .option('--yes', 'Confirm the role change')
34
35
  .action(async (memberId, opts) => {
35
36
  const writer = getWriter();
36
37
  const client = requireClient();
37
38
  if (!['admin', 'member'].includes(opts.role)) {
38
39
  throw errUsage('Role must be "admin" or "member"');
39
40
  }
41
+ await requireHighImpactConfirmation(writer, Boolean(opts.yes), `Change member ${memberId} to ${opts.role}?`, '--yes is required to change a team role in machine mode.');
40
42
  const result = await client.updateMemberRole(memberId, opts.role);
41
43
  if (!writer.isMachineOutput()) {
42
44
  console.log(` ${brand.success('✓')} Updated ${brand.bold(result.name)} to ${brand.bold(result.role)}`);
@@ -57,13 +59,7 @@ export function createTeamCommand(getWriter) {
57
59
  .action(async (memberId, opts) => {
58
60
  const writer = getWriter();
59
61
  const client = requireClient();
60
- if (!opts.yes && !writer.isMachineOutput() && process.stdin.isTTY) {
61
- const confirmed = await confirm(` Remove member ${memberId}?`, false);
62
- if (!confirmed) {
63
- console.log(brand.muted(' Cancelled.'));
64
- return;
65
- }
66
- }
62
+ await requireHighImpactConfirmation(writer, Boolean(opts.yes), `Remove member ${memberId}?`, '--yes is required to remove a team member in machine mode.');
67
63
  const result = await client.removeMember(memberId);
68
64
  if (!writer.isMachineOutput()) {
69
65
  console.log(` ${brand.success('✓')} Removed ${brand.bold(result.name)} (${result.email})`);
@@ -50,6 +50,7 @@ export function createWidgetCommand(getWriter) {
50
50
  // --- widget settings ---
51
51
  widget
52
52
  .command('settings [project]')
53
+ .alias('update')
53
54
  .description('View or update widget settings')
54
55
  .option('--capture-mode <mode>', 'Capture mode (feedback, waitlist)')
55
56
  .option('--color <hex>', 'Button color (e.g. #22c55e)')
@@ -72,6 +73,8 @@ export function createWidgetCommand(getWriter) {
72
73
  .option('--no-hide-email-when-prefilled', 'Show the email field even when userEmail is prefilled')
73
74
  .option('--allow-attachments', 'Allow image attachments')
74
75
  .option('--no-allow-attachments', 'Disable image attachments')
76
+ .option('--allow-visitor-replies', 'Let visitors continue feedback conversations')
77
+ .option('--no-allow-visitor-replies', 'Keep widget replies read-only for visitors')
75
78
  .option('--icon-only', 'Show only the icon, no label')
76
79
  .option('--no-icon-only', 'Show both icon and label')
77
80
  .option('--show-icon', 'Show an icon next to the label')
@@ -94,7 +97,7 @@ export function createWidgetCommand(getWriter) {
94
97
  opts.buttonRadius || opts.buttonSize || opts.icon ||
95
98
  opts.emailRequired !== undefined || opts.showEmail !== undefined ||
96
99
  opts.emailReadOnly !== undefined || opts.hideEmailWhenPrefilled !== undefined ||
97
- opts.allowAttachments !== undefined || opts.iconOnly !== undefined ||
100
+ opts.allowAttachments !== undefined || opts.allowVisitorReplies !== undefined || opts.iconOnly !== undefined ||
98
101
  opts.showIcon !== undefined || opts.showBranding !== undefined ||
99
102
  opts.allowConsoleErrors !== undefined || opts.errorTracking !== undefined ||
100
103
  opts.zIndex || opts.guided || opts.disableGuided;
@@ -139,6 +142,8 @@ export function createWidgetCommand(getWriter) {
139
142
  settings.hideEmailFieldWhenPrefilled = opts.hideEmailWhenPrefilled;
140
143
  if (opts.allowAttachments !== undefined)
141
144
  settings.allowAttachments = opts.allowAttachments;
145
+ if (opts.allowVisitorReplies !== undefined)
146
+ settings.allowVisitorReplies = opts.allowVisitorReplies;
142
147
  if (opts.iconOnly !== undefined)
143
148
  settings.iconOnly = opts.iconOnly;
144
149
  if (opts.showIcon !== undefined)
@@ -434,6 +439,7 @@ function renderWidgetSettings(projectName, settings) {
434
439
  ['Email Read Only', String(settings.emailReadOnly ?? false)],
435
440
  ['Hide Prefilled Email', String(settings.hideEmailFieldWhenPrefilled ?? false)],
436
441
  ['Attachments', String(settings.allowAttachments ?? true)],
442
+ ['Follow-up Replies', String(settings.allowVisitorReplies ?? false)],
437
443
  ['Guided Flow', String(settings.feedbackFlow?.enabled ?? false)],
438
444
  ['Intro Message', String(settings.introMessage ?? '')],
439
445
  ['Success Message', String(settings.successMessage ?? '')],
@@ -0,0 +1,2 @@
1
+ import type { OutputWriter } from './output/writer.js';
2
+ export declare function requireHighImpactConfirmation(writer: OutputWriter, confirmedByFlag: boolean, question: string, hint: string): Promise<void>;
@@ -0,0 +1,11 @@
1
+ import { errUsage } from './output/errors.js';
2
+ import { confirm } from './prompt.js';
3
+ export async function requireHighImpactConfirmation(writer, confirmedByFlag, question, hint) {
4
+ if (confirmedByFlag)
5
+ return;
6
+ if (writer.isMachineOutput() || !process.stdin.isTTY) {
7
+ throw errUsage(hint, `${hint} Re-run the command with --yes.`);
8
+ }
9
+ if (!(await confirm(` ${question}`, false)))
10
+ throw errUsage('Action cancelled');
11
+ }
@@ -42,6 +42,7 @@ export interface WidgetSettings {
42
42
  emailReadOnly?: boolean;
43
43
  hideEmailFieldWhenPrefilled?: boolean;
44
44
  allowAttachments?: boolean;
45
+ allowVisitorReplies?: boolean;
45
46
  displayMode?: 'modal' | 'popup';
46
47
  zIndex?: number;
47
48
  showBranding?: boolean;
@@ -68,6 +69,7 @@ export interface MobileIntegrationResponse {
68
69
  };
69
70
  integration: {
70
71
  enabled: boolean;
72
+ allowVisitorReplies: boolean;
71
73
  publishableKey: string;
72
74
  publishableKeyIncluded: boolean;
73
75
  hostedFormUrl: string | null;
@@ -95,6 +97,7 @@ export interface Feedback {
95
97
  sentiment?: Sentiment | null;
96
98
  aiSummary?: string | null;
97
99
  aiPriorityScore?: number | null;
100
+ awaitingOwnerReply?: boolean;
98
101
  reasoning?: string | null;
99
102
  feedbackType?: {
100
103
  id?: string;
@@ -202,6 +205,7 @@ export interface FeedbackReplyResponse {
202
205
  } | null;
203
206
  message: {
204
207
  id: string;
208
+ senderType: 'OWNER' | 'VISITOR';
205
209
  content: string;
206
210
  delivery: 'WIDGET' | 'EMAIL' | 'BOTH';
207
211
  sentByName: string | null;
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.11.0";
2
- export declare const USER_AGENT = "FeedbackBasket-CLI/0.11.0";
1
+ export declare const VERSION: "3.0.0";
2
+ export declare const USER_AGENT: string;
@@ -1,2 +1,3 @@
1
- export const VERSION = '0.11.0';
1
+ import { AGENT_SURFACE_VERSION } from 'feedbackbasket-agent-contract';
2
+ export const VERSION = AGENT_SURFACE_VERSION;
2
3
  export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;