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.
- package/CHANGELOG.md +289 -0
- package/README.md +374 -326
- package/dist/src/capabilities.d.ts +38 -0
- package/dist/src/capabilities.js +6 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/client.d.ts +1 -0
- package/dist/src/commands/auth.d.ts +1 -0
- package/dist/src/commands/auth.js +6 -1
- package/dist/src/commands/feedback-bulk-update.js +3 -0
- package/dist/src/commands/feedback-delete.js +2 -6
- package/dist/src/commands/feedback-note.js +34 -2
- package/dist/src/commands/feedback-reply.js +31 -19
- package/dist/src/commands/feedback.js +1 -0
- package/dist/src/commands/mobile.js +28 -0
- package/dist/src/commands/projects.js +2 -6
- package/dist/src/commands/team.js +4 -8
- package/dist/src/commands/widget.js +7 -1
- package/dist/src/confirmation.d.ts +2 -0
- package/dist/src/confirmation.js +11 -0
- package/dist/src/types.d.ts +4 -0
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +2 -1
- package/package.json +54 -49
- package/skills/feedbackbasket/SKILL.md +399 -378
|
@@ -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
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
|
|
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);
|
package/dist/src/client.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
9
|
-
.argument('
|
|
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
|
|
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
|
|
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}
|
|
120
|
+
console.log(brand.bold(`${result.total} conversation message${result.total === 1 ? '' : 's'} for feedback ${id}`));
|
|
113
121
|
console.log();
|
|
114
|
-
for (const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
console.log(
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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}
|
|
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 {
|
|
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
|
-
|
|
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,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
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION
|
|
2
|
-
export declare const USER_AGENT
|
|
1
|
+
export declare const VERSION: "3.0.0";
|
|
2
|
+
export declare const USER_AGENT: string;
|
package/dist/src/version.js
CHANGED