feedbackbasket-cli 0.10.0 → 0.11.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 +30 -0
- package/dist/src/cli.js +2 -0
- package/dist/src/client.d.ts +8 -1
- package/dist/src/client.js +13 -0
- package/dist/src/commands/feedback-reply.d.ts +2 -0
- package/dist/src/commands/feedback-reply.js +23 -13
- package/dist/src/commands/feedback.js +1 -1
- package/dist/src/commands/mobile.d.ts +7 -0
- package/dist/src/commands/mobile.js +292 -0
- package/dist/src/help.js +4 -1
- package/dist/src/types.d.ts +28 -1
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +2 -1
- package/skills/feedbackbasket/SKILL.md +61 -6
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ feedbackbasket feedback list --category BUG --agent
|
|
|
39
39
|
feedbackbasket feedback create "Login button is broken" --content "Clicking Log in does nothing in Safari." --project myapp --type bug --agent
|
|
40
40
|
feedbackbasket feedback update <id> --status PLANNED --agent
|
|
41
41
|
feedbackbasket widget script myproject --agent
|
|
42
|
+
feedbackbasket mobile setup myproject --bundle-id com.example.app --include-publishable-key --agent
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
When installing or configuring a widget for the current app, agents should not rely on the CLI default project. First run `feedbackbasket projects list --agent`, match the current app by its real website URL or clearly matching project name, and only create a new project after confirming no existing project belongs to this app. If the only known URL is `localhost`, ask for the production, staging, preview, or intended public URL before creating the project.
|
|
@@ -100,6 +101,7 @@ feedbackbasket feedback update <id> --status PLANNED # Update status
|
|
|
100
101
|
feedbackbasket feedback update <id> --category BUG # Update category
|
|
101
102
|
feedbackbasket feedback reply <id> "Thanks!" --delivery email --reply-to support@example.com
|
|
102
103
|
feedbackbasket feedback reply <id> "Thanks!" --delivery widget
|
|
104
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery in-app
|
|
103
105
|
feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com
|
|
104
106
|
feedbackbasket feedback replies <id> # List sent replies
|
|
105
107
|
feedbackbasket feedback note <id> "Investigating this..." # Add internal note
|
|
@@ -202,6 +204,34 @@ The default widget experience is a basic modal. Only switch to popup mode or ena
|
|
|
202
204
|
}
|
|
203
205
|
```
|
|
204
206
|
|
|
207
|
+
### Mobile Apps
|
|
208
|
+
|
|
209
|
+
Mobile setup is additive and does not change the website widget. The `fb_mobile_` value is a publishable, write-only project identifier designed to ship in an app; it is not a CLI token or private API key. Mobile commands mask it unless `--include-publishable-key` is explicitly supplied.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Enable mobile feedback and add allowed iOS bundle IDs
|
|
213
|
+
feedbackbasket mobile setup myapp --bundle-id com.example.app
|
|
214
|
+
|
|
215
|
+
# Return the publishable key and hosted form URL for an authorized app setup
|
|
216
|
+
feedbackbasket mobile setup myapp --bundle-id com.example.app --include-publishable-key --agent
|
|
217
|
+
|
|
218
|
+
# Inspect and verify the SDK heartbeat
|
|
219
|
+
feedbackbasket mobile status myapp
|
|
220
|
+
feedbackbasket mobile verify myapp --bundle-id com.example.app --wait 120
|
|
221
|
+
|
|
222
|
+
# Add or remove bundle IDs without replacing the others
|
|
223
|
+
feedbackbasket mobile bundle-ids myapp --add com.example.app.beta
|
|
224
|
+
feedbackbasket mobile bundle-ids myapp --remove com.example.app.beta
|
|
225
|
+
|
|
226
|
+
# Actions that can interrupt installed apps require explicit confirmation
|
|
227
|
+
feedbackbasket mobile disable myapp --yes
|
|
228
|
+
feedbackbasket mobile rotate-key myapp --yes --include-publishable-key
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Agents should never repeat the full publishable key in their final response. They must never place `fb_cli_` or `fb_key_` credentials in a mobile app. Key rotation invalidates the previous key and therefore requires explicit user authorization.
|
|
232
|
+
|
|
233
|
+
The native Swift SDK securely stores reply-thread credentials in the app Keychain and shows team replies in the same feedback sheet. Host apps do not need to build an inbox or manage reply tokens. Hosted-form integrations remain email-only.
|
|
234
|
+
|
|
205
235
|
### Team
|
|
206
236
|
|
|
207
237
|
```bash
|
package/dist/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createSetupCommand } from './commands/setup.js';
|
|
|
12
12
|
import { createWidgetCommand } from './commands/widget.js';
|
|
13
13
|
import { createTeamCommand } from './commands/team.js';
|
|
14
14
|
import { createWaitlistCommand } from './commands/waitlist.js';
|
|
15
|
+
import { createMobileCommand } from './commands/mobile.js';
|
|
15
16
|
import { renderRootHelp } from './help.js';
|
|
16
17
|
let writer;
|
|
17
18
|
function resolveFormat(opts) {
|
|
@@ -64,6 +65,7 @@ export function run() {
|
|
|
64
65
|
program.addCommand(createBugsCommand(getWriter));
|
|
65
66
|
program.addCommand(createWidgetCommand(getWriter));
|
|
66
67
|
program.addCommand(createWaitlistCommand(getWriter));
|
|
68
|
+
program.addCommand(createMobileCommand(getWriter));
|
|
67
69
|
program.addCommand(createTeamCommand(getWriter));
|
|
68
70
|
program.addCommand(createDoctorCommand(getWriter));
|
|
69
71
|
program.addCommand(createSetupCommand(getWriter));
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings, WaitlistResponse } from './types.js';
|
|
1
|
+
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings, WaitlistResponse, MobileIntegrationResponse } from './types.js';
|
|
2
2
|
export declare class FeedbackBasketClient {
|
|
3
3
|
private readonly apiBaseUrl;
|
|
4
4
|
private readonly token;
|
|
@@ -53,6 +53,13 @@ export declare class FeedbackBasketClient {
|
|
|
53
53
|
embedCode: string;
|
|
54
54
|
scriptUrl: string;
|
|
55
55
|
}>;
|
|
56
|
+
getMobileIntegration(projectId: string, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
|
|
57
|
+
updateMobileIntegration(projectId: string, data: {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
addBundleIds?: string[];
|
|
60
|
+
removeBundleIds?: string[];
|
|
61
|
+
}, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
|
|
62
|
+
rotateMobileProjectKey(projectId: string, includePublishableKey?: boolean): Promise<MobileIntegrationResponse>;
|
|
56
63
|
getWaitlist(projectId: string, params?: {
|
|
57
64
|
search?: string;
|
|
58
65
|
limit?: number;
|
package/dist/src/client.js
CHANGED
|
@@ -56,6 +56,19 @@ export class FeedbackBasketClient {
|
|
|
56
56
|
async getWidgetScript(projectId) {
|
|
57
57
|
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget-script`);
|
|
58
58
|
}
|
|
59
|
+
// Mobile feedback
|
|
60
|
+
async getMobileIntegration(projectId, includePublishableKey = false) {
|
|
61
|
+
const query = includePublishableKey ? '?includePublishableKey=true' : '';
|
|
62
|
+
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/mobile${query}`);
|
|
63
|
+
}
|
|
64
|
+
async updateMobileIntegration(projectId, data, includePublishableKey = false) {
|
|
65
|
+
const query = includePublishableKey ? '?includePublishableKey=true' : '';
|
|
66
|
+
return this.request('PATCH', `/projects/${encodeURIComponent(projectId)}/mobile${query}`, data);
|
|
67
|
+
}
|
|
68
|
+
async rotateMobileProjectKey(projectId, includePublishableKey = false) {
|
|
69
|
+
const query = includePublishableKey ? '?includePublishableKey=true' : '';
|
|
70
|
+
return this.request('POST', `/projects/${encodeURIComponent(projectId)}/mobile/rotate-key${query}`);
|
|
71
|
+
}
|
|
59
72
|
async getWaitlist(projectId, params = {}) {
|
|
60
73
|
const query = buildQuery(params);
|
|
61
74
|
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist${query}`);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { OutputWriter } from '../output/writer.js';
|
|
3
|
+
import type { ReplyDelivery } from '../types.js';
|
|
3
4
|
export declare function createFeedbackReplyCommand(getWriter: () => OutputWriter): Command;
|
|
4
5
|
export declare function createFeedbackRepliesCommand(getWriter: () => OutputWriter): Command;
|
|
6
|
+
export declare function replyDestinations(delivery: ReplyDelivery): Array<'email' | 'widget'>;
|
|
@@ -5,14 +5,14 @@ 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
|
-
const deliveryOptions = new Set(['email', 'widget', 'both']);
|
|
8
|
+
const deliveryOptions = new Set(['email', 'widget', 'in-app', 'both']);
|
|
9
9
|
export function createFeedbackReplyCommand(getWriter) {
|
|
10
10
|
return new Command('reply')
|
|
11
11
|
.argument('<id>', 'Feedback ID to reply to')
|
|
12
12
|
.argument('[content]', 'Reply content (or use --content)')
|
|
13
|
-
.description('Reply to feedback by email, widget thread, or both')
|
|
13
|
+
.description('Reply to feedback by email, widget/in-app thread, or both')
|
|
14
14
|
.option('--content <text>', 'Reply content (alternative to positional argument)')
|
|
15
|
-
.option('--delivery <delivery>', 'Reply delivery (email, widget, both)', 'email')
|
|
15
|
+
.option('--delivery <delivery>', 'Reply delivery (email, widget, in-app, both)', 'email')
|
|
16
16
|
.option('--reply-to <email>', 'Reply-to email for email delivery')
|
|
17
17
|
.action(async (id, contentArg, opts) => {
|
|
18
18
|
const writer = getWriter();
|
|
@@ -22,19 +22,22 @@ export function createFeedbackReplyCommand(getWriter) {
|
|
|
22
22
|
throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!" --delivery widget');
|
|
23
23
|
}
|
|
24
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');
|
|
25
|
+
throw errUsage('Delivery must be email, widget, in-app, or both', 'Example: feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com');
|
|
26
26
|
}
|
|
27
27
|
const client = requireClient();
|
|
28
28
|
const feedback = await client.getFeedbackById(id);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (delivery === 'in-app' && feedback.replyChannel !== 'in_app') {
|
|
30
|
+
throw errUsage('This feedback does not have an in-app reply thread.', 'Use --delivery widget for website-widget feedback or --delivery email when an email address is available.');
|
|
31
|
+
}
|
|
32
|
+
const destinations = replyDestinations(delivery);
|
|
32
33
|
const sendsEmail = destinations.includes('email');
|
|
33
34
|
const sendsWidget = destinations.includes('widget');
|
|
34
35
|
let replyTo = opts.replyTo ?? feedback.project.replyToEmail ?? undefined;
|
|
35
36
|
if (sendsEmail) {
|
|
36
37
|
if (!feedback.email) {
|
|
37
|
-
throw errUsage(
|
|
38
|
+
throw errUsage(feedback.replyChannel === 'in_app'
|
|
39
|
+
? 'This feedback has no email address; use --delivery in-app.'
|
|
40
|
+
: 'This feedback has no email address; use --delivery widget if it has a widget thread.');
|
|
38
41
|
}
|
|
39
42
|
if (!replyTo) {
|
|
40
43
|
const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
|
|
@@ -56,7 +59,7 @@ export function createFeedbackReplyCommand(getWriter) {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
if (sendsWidget && !feedback.hasWidgetAccess) {
|
|
59
|
-
throw errUsage('This feedback
|
|
62
|
+
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.');
|
|
60
63
|
}
|
|
61
64
|
const result = await client.sendReply(id, content, {
|
|
62
65
|
replyToEmail: replyTo,
|
|
@@ -71,16 +74,16 @@ export function createFeedbackReplyCommand(getWriter) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
if (result.message) {
|
|
74
|
-
console.log(` ${brand.success('[OK]')} Widget reply posted`);
|
|
77
|
+
console.log(` ${brand.success('[OK]')} ${feedback.replyChannel === 'in_app' ? 'In-app' : 'Widget'} reply posted`);
|
|
75
78
|
console.log(` ${brand.muted('By:')} ${result.message.sentByName ?? 'CLI'}`);
|
|
76
79
|
}
|
|
77
80
|
console.log();
|
|
78
81
|
}
|
|
79
82
|
writer.ok(result, {
|
|
80
83
|
summary: delivery === 'both'
|
|
81
|
-
?
|
|
82
|
-
:
|
|
83
|
-
? 'Widget reply posted
|
|
84
|
+
? `Reply sent by email and ${feedback.replyChannel === 'in_app' ? 'in-app' : 'widget'}`
|
|
85
|
+
: sendsWidget
|
|
86
|
+
? `${feedback.replyChannel === 'in_app' ? 'In-app' : 'Widget'} reply posted`
|
|
84
87
|
: `Reply sent to ${result.sentTo}`,
|
|
85
88
|
breadcrumbs: [
|
|
86
89
|
{ action: 'View replies', cmd: `feedbackbasket feedback replies ${id}` },
|
|
@@ -139,6 +142,13 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
139
142
|
});
|
|
140
143
|
});
|
|
141
144
|
}
|
|
145
|
+
export function replyDestinations(delivery) {
|
|
146
|
+
if (delivery === 'both')
|
|
147
|
+
return ['email', 'widget'];
|
|
148
|
+
if (delivery === 'in-app')
|
|
149
|
+
return ['widget'];
|
|
150
|
+
return [delivery];
|
|
151
|
+
}
|
|
142
152
|
function requireClient() {
|
|
143
153
|
const manager = new AuthManager();
|
|
144
154
|
const token = manager.resolveToken();
|
|
@@ -186,7 +186,7 @@ function renderFeedbackDetail(item) {
|
|
|
186
186
|
['OS', item.os],
|
|
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
190
|
['Created', item.createdAt],
|
|
191
191
|
];
|
|
192
192
|
for (const [label, value] of fields) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { MobileIntegrationResponse } from '../types.js';
|
|
3
|
+
import type { OutputWriter } from '../output/writer.js';
|
|
4
|
+
export declare function createMobileCommand(getWriter: () => OutputWriter): Command;
|
|
5
|
+
export declare function projectRef(projectArg: string | undefined, projectId: string): string;
|
|
6
|
+
export declare function validateBundleIds(bundleIds: string[]): string[];
|
|
7
|
+
export declare function isMobileConnectionVerified(result: MobileIntegrationResponse, expectedBundleId?: string): boolean;
|
|
@@ -0,0 +1,292 @@
|
|
|
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, divider } from '../output/theme.js';
|
|
7
|
+
import { confirm } from '../prompt.js';
|
|
8
|
+
import { resolveProject } from '../resolve.js';
|
|
9
|
+
const BUNDLE_ID_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;
|
|
10
|
+
const SAFE_PROJECT_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*(?: [A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
11
|
+
const VERIFY_POLL_INTERVAL_MS = 5_000;
|
|
12
|
+
const MAX_VERIFY_WAIT_SECONDS = 300;
|
|
13
|
+
export function createMobileCommand(getWriter) {
|
|
14
|
+
const mobile = new Command('mobile')
|
|
15
|
+
.description('Set up and verify mobile app feedback');
|
|
16
|
+
mobile
|
|
17
|
+
.command('status [project]')
|
|
18
|
+
.description('Show mobile feedback configuration and connection status')
|
|
19
|
+
.option('--include-publishable-key', 'Include the full publishable mobile project key')
|
|
20
|
+
.action(async (projectArg, opts) => {
|
|
21
|
+
const writer = getWriter();
|
|
22
|
+
const client = requireClient();
|
|
23
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
24
|
+
const ref = projectRef(projectArg, projectId);
|
|
25
|
+
const result = await client.getMobileIntegration(projectId, Boolean(opts.includePublishableKey));
|
|
26
|
+
if (!writer.isMachineOutput())
|
|
27
|
+
renderMobileStatus(result);
|
|
28
|
+
writer.ok(result, {
|
|
29
|
+
summary: `Mobile feedback for "${result.project.name}"`,
|
|
30
|
+
breadcrumbs: result.integration
|
|
31
|
+
? [
|
|
32
|
+
{ action: 'Verify SDK connection', cmd: `feedbackbasket mobile verify ${ref}` },
|
|
33
|
+
{ action: 'Add a bundle ID', cmd: `feedbackbasket mobile bundle-ids ${ref} --add com.example.app` },
|
|
34
|
+
]
|
|
35
|
+
: [
|
|
36
|
+
{ action: 'Enable mobile feedback', cmd: `feedbackbasket mobile setup ${ref}` },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
mobile
|
|
41
|
+
.command('setup [project]')
|
|
42
|
+
.description('Enable mobile feedback and add allowed bundle IDs')
|
|
43
|
+
.option('--bundle-id <bundle-id>', 'Allowed iOS bundle ID (repeatable)', collect, [])
|
|
44
|
+
.option('--include-publishable-key', 'Include the full publishable key required to configure the app')
|
|
45
|
+
.action(async (projectArg, opts) => {
|
|
46
|
+
const writer = getWriter();
|
|
47
|
+
const client = requireClient();
|
|
48
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
49
|
+
const bundleIds = validateBundleIds(opts.bundleId);
|
|
50
|
+
const result = await client.updateMobileIntegration(projectId, { enabled: true, addBundleIds: bundleIds }, Boolean(opts.includePublishableKey));
|
|
51
|
+
if (!writer.isMachineOutput()) {
|
|
52
|
+
console.log(` ${brand.success('✓')} Mobile feedback enabled for ${brand.bold(result.project.name)}`);
|
|
53
|
+
if (!opts.includePublishableKey) {
|
|
54
|
+
console.log(` ${brand.muted('The publishable key is masked. Re-run with --include-publishable-key when configuring the app.')}`);
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
}
|
|
58
|
+
const ref = projectRef(projectArg, projectId);
|
|
59
|
+
writer.ok(withSetupGuidance(result), {
|
|
60
|
+
summary: `Mobile feedback ready for "${result.project.name}"`,
|
|
61
|
+
breadcrumbs: [
|
|
62
|
+
{ action: 'Check connection', cmd: `feedbackbasket mobile verify ${ref}` },
|
|
63
|
+
{ action: 'View mobile status', cmd: `feedbackbasket mobile status ${ref}` },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
mobile
|
|
68
|
+
.command('bundle-ids [project]')
|
|
69
|
+
.description('Add or remove allowed iOS bundle IDs')
|
|
70
|
+
.option('--add <bundle-id>', 'Bundle ID to add (repeatable)', collect, [])
|
|
71
|
+
.option('--remove <bundle-id>', 'Bundle ID to remove (repeatable)', collect, [])
|
|
72
|
+
.action(async (projectArg, opts) => {
|
|
73
|
+
const writer = getWriter();
|
|
74
|
+
const client = requireClient();
|
|
75
|
+
const additions = validateBundleIds(opts.add);
|
|
76
|
+
const removals = validateBundleIds(opts.remove);
|
|
77
|
+
if (additions.length === 0 && removals.length === 0) {
|
|
78
|
+
throw errUsage('Pass at least one --add or --remove bundle ID', 'Example: feedbackbasket mobile bundle-ids myapp --add com.example.app');
|
|
79
|
+
}
|
|
80
|
+
const overlap = additions.filter((bundleId) => removals.includes(bundleId));
|
|
81
|
+
if (overlap.length > 0) {
|
|
82
|
+
throw errUsage(`The same bundle ID cannot be added and removed: ${overlap.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
85
|
+
const result = await client.updateMobileIntegration(projectId, {
|
|
86
|
+
addBundleIds: additions,
|
|
87
|
+
removeBundleIds: removals,
|
|
88
|
+
});
|
|
89
|
+
if (!writer.isMachineOutput()) {
|
|
90
|
+
console.log(` ${brand.success('✓')} Allowed bundle IDs updated for ${brand.bold(result.project.name)}`);
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
writer.ok(result, {
|
|
94
|
+
summary: `Updated mobile bundle IDs for "${result.project.name}"`,
|
|
95
|
+
breadcrumbs: [
|
|
96
|
+
{ action: 'View mobile status', cmd: `feedbackbasket mobile status ${projectRef(projectArg, projectId)}` },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
mobile
|
|
101
|
+
.command('verify [project]')
|
|
102
|
+
.description('Verify that the SDK has connected from the expected app')
|
|
103
|
+
.option('--bundle-id <bundle-id>', 'Expected bundle ID')
|
|
104
|
+
.option('--wait <seconds>', 'Wait for the first matching heartbeat (maximum 300 seconds)', '0')
|
|
105
|
+
.action(async (projectArg, opts) => {
|
|
106
|
+
const writer = getWriter();
|
|
107
|
+
const client = requireClient();
|
|
108
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
109
|
+
const expectedBundleId = opts.bundleId
|
|
110
|
+
? validateBundleIds([opts.bundleId])[0]
|
|
111
|
+
: undefined;
|
|
112
|
+
const waitSeconds = parseWaitSeconds(opts.wait);
|
|
113
|
+
const deadline = Date.now() + waitSeconds * 1_000;
|
|
114
|
+
let result = await client.getMobileIntegration(projectId);
|
|
115
|
+
while (!isMobileConnectionVerified(result, expectedBundleId) && Date.now() < deadline) {
|
|
116
|
+
await sleep(Math.min(VERIFY_POLL_INTERVAL_MS, deadline - Date.now()));
|
|
117
|
+
result = await client.getMobileIntegration(projectId);
|
|
118
|
+
}
|
|
119
|
+
const verified = isMobileConnectionVerified(result, expectedBundleId);
|
|
120
|
+
const verification = {
|
|
121
|
+
verified,
|
|
122
|
+
expectedBundleId: expectedBundleId ?? null,
|
|
123
|
+
project: result.project,
|
|
124
|
+
integration: result.integration,
|
|
125
|
+
};
|
|
126
|
+
if (!writer.isMachineOutput())
|
|
127
|
+
renderVerification(verification);
|
|
128
|
+
writer.ok(verification, {
|
|
129
|
+
summary: verified
|
|
130
|
+
? `Verified mobile connection for "${result.project.name}"`
|
|
131
|
+
: `Mobile connection not yet verified for "${result.project.name}"`,
|
|
132
|
+
notice: verified
|
|
133
|
+
? undefined
|
|
134
|
+
: 'Build and launch the app, then run this command again with --wait 120.',
|
|
135
|
+
});
|
|
136
|
+
if (!verified)
|
|
137
|
+
process.exitCode = 2;
|
|
138
|
+
});
|
|
139
|
+
mobile
|
|
140
|
+
.command('disable [project]')
|
|
141
|
+
.description('Disable mobile feedback without changing the website widget')
|
|
142
|
+
.option('--yes', 'Confirm disabling mobile feedback')
|
|
143
|
+
.action(async (projectArg, opts) => {
|
|
144
|
+
const writer = getWriter();
|
|
145
|
+
const client = requireClient();
|
|
146
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
147
|
+
await requireDestructiveConfirmation(writer, Boolean(opts.yes), 'Disable mobile feedback? Installed apps will stop submitting feedback.', '--yes is required to disable mobile feedback in agent mode');
|
|
148
|
+
const result = await client.updateMobileIntegration(projectId, { enabled: false });
|
|
149
|
+
if (!writer.isMachineOutput()) {
|
|
150
|
+
console.log(` ${brand.success('✓')} Mobile feedback disabled for ${brand.bold(result.project.name)}`);
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
writer.ok(result, {
|
|
154
|
+
summary: `Disabled mobile feedback for "${result.project.name}"`,
|
|
155
|
+
breadcrumbs: [
|
|
156
|
+
{ action: 'Re-enable mobile feedback', cmd: `feedbackbasket mobile setup ${projectRef(projectArg, projectId)}` },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
mobile
|
|
161
|
+
.command('rotate-key [project]')
|
|
162
|
+
.description('Rotate the publishable mobile key (existing app builds will stop working)')
|
|
163
|
+
.option('--yes', 'Confirm key rotation')
|
|
164
|
+
.option('--include-publishable-key', 'Include the newly generated publishable key')
|
|
165
|
+
.action(async (projectArg, opts) => {
|
|
166
|
+
const writer = getWriter();
|
|
167
|
+
const client = requireClient();
|
|
168
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
169
|
+
await requireDestructiveConfirmation(writer, Boolean(opts.yes), 'Rotate the mobile project key? Existing app builds will stop submitting feedback.', '--yes is required to rotate a mobile project key in agent mode');
|
|
170
|
+
const result = await client.rotateMobileProjectKey(projectId, Boolean(opts.includePublishableKey));
|
|
171
|
+
if (!writer.isMachineOutput()) {
|
|
172
|
+
console.log(` ${brand.warning('!')} Mobile project key rotated for ${brand.bold(result.project.name)}`);
|
|
173
|
+
console.log(' Update and release every installed app that used the previous key.');
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
176
|
+
writer.ok(result, {
|
|
177
|
+
summary: `Rotated mobile project key for "${result.project.name}"`,
|
|
178
|
+
notice: 'Existing app builds using the previous key can no longer submit feedback.',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
return mobile;
|
|
182
|
+
}
|
|
183
|
+
function requireClient() {
|
|
184
|
+
const manager = new AuthManager();
|
|
185
|
+
const token = manager.resolveToken();
|
|
186
|
+
if (!token)
|
|
187
|
+
throw errAuth();
|
|
188
|
+
return new FeedbackBasketClient(token, loadConfig().baseUrl);
|
|
189
|
+
}
|
|
190
|
+
async function resolveProjectId(client, projectArg) {
|
|
191
|
+
if (projectArg)
|
|
192
|
+
return (await resolveProject(client, projectArg)).id;
|
|
193
|
+
const config = loadConfig();
|
|
194
|
+
if (config.defaultProject)
|
|
195
|
+
return config.defaultProject;
|
|
196
|
+
throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket mobile status <project>');
|
|
197
|
+
}
|
|
198
|
+
function collect(value, previous) {
|
|
199
|
+
return [...previous, value];
|
|
200
|
+
}
|
|
201
|
+
export function projectRef(projectArg, projectId) {
|
|
202
|
+
const ref = projectArg?.trim();
|
|
203
|
+
if (!ref || !SAFE_PROJECT_REF_PATTERN.test(ref))
|
|
204
|
+
return projectId;
|
|
205
|
+
return ref.includes(' ') ? `"${ref}"` : ref;
|
|
206
|
+
}
|
|
207
|
+
export function validateBundleIds(bundleIds) {
|
|
208
|
+
const normalized = Array.from(new Set(bundleIds.map((value) => value.trim()).filter(Boolean)));
|
|
209
|
+
const invalid = normalized.find((bundleId) => !BUNDLE_ID_PATTERN.test(bundleId));
|
|
210
|
+
if (invalid) {
|
|
211
|
+
throw errUsage(`Invalid bundle ID "${invalid}"`, 'Bundle IDs must look like com.example.app');
|
|
212
|
+
}
|
|
213
|
+
if (normalized.length > 20)
|
|
214
|
+
throw errUsage('A project can have at most 20 bundle IDs');
|
|
215
|
+
return normalized;
|
|
216
|
+
}
|
|
217
|
+
export function isMobileConnectionVerified(result, expectedBundleId) {
|
|
218
|
+
const integration = result.integration;
|
|
219
|
+
if (!integration?.enabled || !integration.connection.connected)
|
|
220
|
+
return false;
|
|
221
|
+
return !expectedBundleId || integration.connection.bundleId === expectedBundleId;
|
|
222
|
+
}
|
|
223
|
+
function parseWaitSeconds(value) {
|
|
224
|
+
const seconds = Number(value);
|
|
225
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > MAX_VERIFY_WAIT_SECONDS) {
|
|
226
|
+
throw errUsage(`--wait must be a whole number between 0 and ${MAX_VERIFY_WAIT_SECONDS}`);
|
|
227
|
+
}
|
|
228
|
+
return seconds;
|
|
229
|
+
}
|
|
230
|
+
function sleep(milliseconds) {
|
|
231
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds)));
|
|
232
|
+
}
|
|
233
|
+
async function requireDestructiveConfirmation(writer, confirmedByFlag, question, agentHint) {
|
|
234
|
+
if (confirmedByFlag)
|
|
235
|
+
return;
|
|
236
|
+
if (writer.isMachineOutput() || !process.stdin.isTTY) {
|
|
237
|
+
throw errUsage(agentHint);
|
|
238
|
+
}
|
|
239
|
+
if (!(await confirm(` ${question}`, false))) {
|
|
240
|
+
throw errUsage('Action cancelled');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function withSetupGuidance(result) {
|
|
244
|
+
return {
|
|
245
|
+
...result,
|
|
246
|
+
setup: {
|
|
247
|
+
publishableKeyIncluded: result.integration?.publishableKeyIncluded ?? false,
|
|
248
|
+
supportedIntegrations: ['swiftui', 'uikit', 'react-native', 'flutter', 'hosted-form'],
|
|
249
|
+
nextSteps: result.integration?.publishableKeyIncluded
|
|
250
|
+
? [
|
|
251
|
+
'Detect the mobile framework and minimum supported platform version.',
|
|
252
|
+
'Configure the app with the returned publishable mobile project key.',
|
|
253
|
+
'Add an accessible Send feedback action to an existing Settings, Help, or Support screen.',
|
|
254
|
+
'Build and launch the app, then verify its SDK heartbeat.',
|
|
255
|
+
]
|
|
256
|
+
: [
|
|
257
|
+
'Re-run setup with --include-publishable-key when you are ready to configure the app.',
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function renderMobileStatus(result) {
|
|
263
|
+
console.log(brand.bold(`Mobile feedback — ${result.project.name}`));
|
|
264
|
+
console.log(divider(54));
|
|
265
|
+
if (!result.integration) {
|
|
266
|
+
console.log(brand.muted(' Mobile feedback is not enabled.'));
|
|
267
|
+
console.log();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const integration = result.integration;
|
|
271
|
+
console.log(` ${brand.label('Status'.padEnd(18))} ${integration.enabled ? 'Enabled' : 'Disabled'}`);
|
|
272
|
+
console.log(` ${brand.label('Publishable key'.padEnd(18))} ${integration.publishableKey}`);
|
|
273
|
+
console.log(` ${brand.label('Bundle IDs'.padEnd(18))} ${integration.bundleIds.join(', ') || 'Any bundle ID'}`);
|
|
274
|
+
console.log(` ${brand.label('Connection'.padEnd(18))} ${integration.connection.connected ? 'Connected' : 'Waiting for first heartbeat'}`);
|
|
275
|
+
if (integration.connection.lastSeenAt) {
|
|
276
|
+
console.log(` ${brand.label('Last seen'.padEnd(18))} ${integration.connection.lastSeenAt}`);
|
|
277
|
+
}
|
|
278
|
+
if (integration.connection.bundleId) {
|
|
279
|
+
console.log(` ${brand.label('Last bundle'.padEnd(18))} ${integration.connection.bundleId}`);
|
|
280
|
+
}
|
|
281
|
+
console.log();
|
|
282
|
+
}
|
|
283
|
+
function renderVerification(result) {
|
|
284
|
+
const icon = result.verified ? brand.success('✓') : brand.warning('!');
|
|
285
|
+
console.log(` ${icon} ${result.verified ? 'Mobile SDK connection verified' : 'Mobile SDK connection not yet verified'}`);
|
|
286
|
+
if (result.expectedBundleId)
|
|
287
|
+
console.log(` ${brand.muted(`Expected bundle: ${result.expectedBundleId}`)}`);
|
|
288
|
+
if (result.integration?.connection.lastSeenAt) {
|
|
289
|
+
console.log(` ${brand.muted(`Last heartbeat: ${result.integration.connection.lastSeenAt}`)}`);
|
|
290
|
+
}
|
|
291
|
+
console.log();
|
|
292
|
+
}
|
package/dist/src/help.js
CHANGED
|
@@ -17,7 +17,7 @@ export function renderRootHelp() {
|
|
|
17
17
|
lines.push(` ${logo()} CLI ${brand.muted(`v${VERSION}`)}`);
|
|
18
18
|
lines.push('');
|
|
19
19
|
lines.push(` ${brand.muted('The command-line interface for FeedbackBasket.')}`);
|
|
20
|
-
lines.push(` ${brand.muted('Manage projects, feedback, waitlists, widgets, and team from your terminal.')}`);
|
|
20
|
+
lines.push(` ${brand.muted('Manage projects, feedback, mobile apps, waitlists, widgets, and team from your terminal.')}`);
|
|
21
21
|
lines.push('');
|
|
22
22
|
// Core Commands
|
|
23
23
|
lines.push(section(' CORE COMMANDS'));
|
|
@@ -26,6 +26,7 @@ export function renderRootHelp() {
|
|
|
26
26
|
lines.push(cmd('bugs', 'View bug reports with severity'));
|
|
27
27
|
lines.push(cmd('widget', 'Manage feedback widget & get embed code'));
|
|
28
28
|
lines.push(cmd('waitlist', 'View and export waitlist signups'));
|
|
29
|
+
lines.push(cmd('mobile', 'Set up and verify mobile app feedback'));
|
|
29
30
|
lines.push(cmd('team', 'Manage organization members'));
|
|
30
31
|
lines.push('');
|
|
31
32
|
// Shortcuts
|
|
@@ -66,6 +67,8 @@ export function renderRootHelp() {
|
|
|
66
67
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --display modal`);
|
|
67
68
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --capture-mode waitlist`);
|
|
68
69
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket waitlist list myapp --search "@example.com"`);
|
|
70
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket mobile setup myapp --bundle-id com.example.app`);
|
|
71
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket mobile verify myapp --bundle-id com.example.app --wait 120`);
|
|
69
72
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects create "My App" --url https://myapp.com`);
|
|
70
73
|
lines.push('');
|
|
71
74
|
// Learn More
|
package/dist/src/types.d.ts
CHANGED
|
@@ -2,7 +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
|
+
export type ReplyDelivery = 'email' | 'widget' | 'in-app' | 'both';
|
|
6
6
|
export type FeedbackFlowQuestionType = 'text' | 'textarea' | 'single_choice';
|
|
7
7
|
export interface FeedbackFlowQuestion {
|
|
8
8
|
id: string;
|
|
@@ -60,6 +60,32 @@ export interface Project {
|
|
|
60
60
|
byStatus: Record<string, number>;
|
|
61
61
|
byCategory: Record<string, number>;
|
|
62
62
|
}
|
|
63
|
+
export interface MobileIntegrationResponse {
|
|
64
|
+
project: {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
url: string;
|
|
68
|
+
};
|
|
69
|
+
integration: {
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
publishableKey: string;
|
|
72
|
+
publishableKeyIncluded: boolean;
|
|
73
|
+
hostedFormUrl: string | null;
|
|
74
|
+
bundleIds: string[];
|
|
75
|
+
connection: {
|
|
76
|
+
connected: boolean;
|
|
77
|
+
lastSeenAt: string | null;
|
|
78
|
+
platform: string | null;
|
|
79
|
+
sdkVersion: string | null;
|
|
80
|
+
bundleId: string | null;
|
|
81
|
+
};
|
|
82
|
+
} | null;
|
|
83
|
+
sdk: {
|
|
84
|
+
swiftPackageUrl: string;
|
|
85
|
+
minimumIOSVersion: string;
|
|
86
|
+
productionBaseUrl: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
63
89
|
export interface Feedback {
|
|
64
90
|
id: string;
|
|
65
91
|
content: string;
|
|
@@ -89,6 +115,7 @@ export interface Feedback {
|
|
|
89
115
|
device?: string | null;
|
|
90
116
|
language?: string | null;
|
|
91
117
|
hasWidgetAccess?: boolean;
|
|
118
|
+
replyChannel?: 'widget' | 'in_app' | null;
|
|
92
119
|
attachments?: FeedbackAttachment[];
|
|
93
120
|
project: {
|
|
94
121
|
id: string;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
2
|
-
export declare const USER_AGENT = "FeedbackBasket-CLI/0.
|
|
1
|
+
export declare const VERSION = "0.11.0";
|
|
2
|
+
export declare const USER_AGENT = "FeedbackBasket-CLI/0.11.0";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.11.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.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
|
+
"test": "tsx --test tests/*.test.ts",
|
|
12
13
|
"dev": "tsx bin/feedbackbasket.ts",
|
|
13
14
|
"start": "node dist/bin/feedbackbasket.js",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feedbackbasket
|
|
3
|
-
description: Manage FeedbackBasket projects, feedback, bugs,
|
|
3
|
+
description: Manage FeedbackBasket projects, feedback, bugs, website widgets, mobile app feedback, waitlist capture, and teams from the command line. Use whenever an agent needs to configure FeedbackBasket in a web or mobile app, install its Swift SDK or hosted mobile form, collect feedback or waitlist signups, query feedback, or manage a FeedbackBasket project.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# FeedbackBasket CLI
|
|
@@ -55,6 +55,59 @@ All project commands accept **name or ID**. Names are matched case-insensitively
|
|
|
55
55
|
|
|
56
56
|
**Capture-mode decision:** if the user asks for feedback, a feedback bubble, bug reports, or feature requests, use `--capture-mode feedback`. If they ask for a waitlist, launch list, early access, or email capture, use `--capture-mode waitlist`. If they ask to set up FeedbackBasket without choosing, explain both options and ask which they want. Do not switch an existing project without confirmation because only one capture mode is active at a time.
|
|
57
57
|
|
|
58
|
+
**Mobile project selection rule:** resolve the FeedbackBasket project for the current app before running mobile commands. Prefer a clearly matching existing project name or product URL. If multiple projects are plausible, ask the user. If none exists, confirm a real product, support, marketing, or App Store URL before creating one; do not invent a URL or use a local development address.
|
|
59
|
+
|
|
60
|
+
### Mobile App Feedback
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
feedbackbasket mobile status <project> --agent
|
|
64
|
+
feedbackbasket mobile setup <project> --bundle-id com.example.app --agent
|
|
65
|
+
feedbackbasket mobile setup <project> --bundle-id com.example.app --include-publishable-key --agent
|
|
66
|
+
feedbackbasket mobile bundle-ids <project> --add com.example.app.beta --agent
|
|
67
|
+
feedbackbasket mobile bundle-ids <project> --remove com.example.app.beta --agent
|
|
68
|
+
feedbackbasket mobile verify <project> --bundle-id com.example.app --wait 120 --agent
|
|
69
|
+
feedbackbasket mobile disable <project> --yes --agent
|
|
70
|
+
feedbackbasket mobile rotate-key <project> --yes --include-publishable-key --agent
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `fb_mobile_` project key is a publishable, write-only identifier designed to ship in the app. It cannot read feedback or administer the project. It is still masked by default to reduce accidental disclosure in logs and transcripts. Use `--include-publishable-key` only while performing a mobile setup the user authorized, and never repeat the full value in the final response.
|
|
74
|
+
|
|
75
|
+
Never put an `fb_cli_` CLI token or `fb_key_` MCP/API key in application source, build settings, prompts, logs, or generated configuration. Those are private credentials and are not interchangeable with the publishable mobile key.
|
|
76
|
+
|
|
77
|
+
For SwiftUI apps targeting iOS 16 or later, use the Swift package returned by `mobile setup` and its native feedback sheet. For UIKit, use the package API or host the SwiftUI sheet. For React Native, Flutter, or unsupported stacks, use the returned hosted form URL in the app's existing in-app browser when available.
|
|
78
|
+
|
|
79
|
+
Configure the Swift package once at app startup with the returned publishable key:
|
|
80
|
+
|
|
81
|
+
```swift
|
|
82
|
+
import FeedbackBasket
|
|
83
|
+
|
|
84
|
+
FeedbackBasket.configure(
|
|
85
|
+
projectKey: "fb_mobile_returned_by_mobile_setup"
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Present its standard SwiftUI sheet from the selected Settings, Help, or Support view:
|
|
90
|
+
|
|
91
|
+
```swift
|
|
92
|
+
@State private var showingFeedback = false
|
|
93
|
+
|
|
94
|
+
Button("Send feedback") {
|
|
95
|
+
showingFeedback = true
|
|
96
|
+
}
|
|
97
|
+
.feedbackBasketSheet(
|
|
98
|
+
isPresented: $showingFeedback,
|
|
99
|
+
context: ["screen": "Settings"]
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The native SDK stores each submission's reply-thread credential in the app Keychain and shows team replies in the same feedback sheet when it is opened again. Do not build a separate inbox, polling client, or token store in the host app. Hosted-form integrations remain email-only.
|
|
104
|
+
|
|
105
|
+
Add an accessible Send feedback action to an appropriate existing Settings, Help, or Support screen. Attach only useful non-sensitive context. Do not send passwords, authentication tokens, payment information, private form contents, crash reports, analytics, session recordings, or automatic logs.
|
|
106
|
+
|
|
107
|
+
Treat a supplied project key as production unless the user explicitly confirms a staging key and base URL. Build and launch the app so the SDK can send its heartbeat, then use `mobile verify`; do not submit test feedback to production. A prior matching heartbeat is a valid connection result because the SDK throttles successful heartbeat attempts.
|
|
108
|
+
|
|
109
|
+
`mobile setup` is idempotent and adds bundle IDs without replacing existing entries. Do not rotate a key or disable mobile feedback unless the user explicitly requested that disruptive action. Rotation stops every released app using the previous key.
|
|
110
|
+
|
|
58
111
|
### Feedback
|
|
59
112
|
```bash
|
|
60
113
|
# Read
|
|
@@ -71,9 +124,10 @@ feedbackbasket feedback note <id> "Investigating — appears related to auth flo
|
|
|
71
124
|
feedbackbasket feedback delete <id> --yes
|
|
72
125
|
feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3
|
|
73
126
|
|
|
74
|
-
# Reply to submitter by email, widget thread, or both
|
|
127
|
+
# Reply to submitter by email, widget/in-app thread, or both
|
|
75
128
|
feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!" --delivery email --reply-to support@example.com
|
|
76
129
|
feedbackbasket feedback reply <id> "<content>" --delivery widget
|
|
130
|
+
feedbackbasket feedback reply <id> "<content>" --delivery in-app
|
|
77
131
|
feedbackbasket feedback reply <id> "<content>" --delivery both --reply-to support@example.com
|
|
78
132
|
feedbackbasket feedback replies <id> # list past replies
|
|
79
133
|
|
|
@@ -257,17 +311,18 @@ feedbackbasket feedback show <id> --agent
|
|
|
257
311
|
### Close the loop — reply to the submitter
|
|
258
312
|
```bash
|
|
259
313
|
# Agent reads context, asks which delivery method to use, then sends it
|
|
260
|
-
feedbackbasket feedback show <id> --agent # read email,
|
|
314
|
+
feedbackbasket feedback show <id> --agent # read email, replyChannel, project.replyToEmail
|
|
261
315
|
feedbackbasket feedback reply <id> "<drafted response>" --delivery widget --agent
|
|
316
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery in-app --agent
|
|
262
317
|
feedbackbasket feedback reply <id> "<drafted response>" --delivery email --reply-to support@example.com --agent
|
|
263
318
|
feedbackbasket feedback reply <id> "<drafted response>" --delivery both --reply-to support@example.com --agent
|
|
264
319
|
feedbackbasket feedback update <id> --status COMPLETE --agent
|
|
265
320
|
feedbackbasket feedback note <id> "Replied via CLI" --agent
|
|
266
321
|
```
|
|
267
322
|
**Important reply safety rules:**
|
|
268
|
-
- Before replying, the agent MUST inspect `feedback show --agent`, then ask the human which delivery method to use
|
|
269
|
-
- If `
|
|
270
|
-
- If `
|
|
323
|
+
- Before replying, the agent MUST inspect `feedback show --agent`, including `replyChannel`, then ask the human which available delivery method to use unless the human already specified it in the current conversation.
|
|
324
|
+
- If `replyChannel: "in_app"`, use `--delivery in-app`. If `replyChannel: "widget"`, use `--delivery widget`. Use `--delivery both` only when an email address and a reply channel are both available.
|
|
325
|
+
- If `feedback show` returns `email: null`, do not use `--delivery email` or `--delivery both`. If `replyChannel: null`, do not use thread delivery.
|
|
271
326
|
- 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.
|
|
272
327
|
- 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>`.
|
|
273
328
|
|