feedbackbasket-cli 0.9.1 → 0.10.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 +28 -3
- package/dist/src/cli.js +2 -0
- package/dist/src/client.d.ts +10 -3
- package/dist/src/client.js +60 -35
- package/dist/src/commands/projects.js +35 -0
- package/dist/src/commands/waitlist.d.ts +3 -0
- package/dist/src/commands/waitlist.js +99 -0
- package/dist/src/commands/widget.js +59 -7
- package/dist/src/help.js +6 -2
- package/dist/src/types.d.ts +22 -0
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +3 -4
- package/skills/feedbackbasket/SKILL.md +91 -20
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ feedbackbasket feedback update <id> --status PLANNED --agent
|
|
|
41
41
|
feedbackbasket widget script myproject --agent
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
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.
|
|
45
|
+
|
|
44
46
|
### Install Claude Code Skill
|
|
45
47
|
|
|
46
48
|
```bash
|
|
@@ -70,11 +72,14 @@ All project commands accept **name or ID** (e.g. `feedbackbasket` or `cmn3c7sgv.
|
|
|
70
72
|
feedbackbasket projects list # List all projects with stats
|
|
71
73
|
feedbackbasket projects show <name-or-id> # Project details
|
|
72
74
|
feedbackbasket projects create "My App" --url https://... # Create project
|
|
75
|
+
feedbackbasket projects create "Local Test" --url http://localhost:3000 --allow-local-url
|
|
73
76
|
feedbackbasket projects update myapp --name "New Name" # Update project
|
|
74
77
|
feedbackbasket projects update myapp --reply-to vlad@example.com # Set default reply-to email
|
|
75
78
|
feedbackbasket projects delete myapp # Delete (with confirmation)
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
Use the production, staging, preview, or intended public website URL for projects. The CLI blocks accidental `localhost`/loopback URLs in agent and non-interactive mode unless you pass `--allow-local-url` for an explicitly local-only test project.
|
|
82
|
+
|
|
78
83
|
### Feedback
|
|
79
84
|
|
|
80
85
|
```bash
|
|
@@ -124,23 +129,39 @@ feedbackbasket bugs stats --project myapp # Per-project stats
|
|
|
124
129
|
feedbackbasket widget settings myapp
|
|
125
130
|
|
|
126
131
|
# Update widget configuration
|
|
132
|
+
feedbackbasket widget settings myapp --capture-mode waitlist
|
|
133
|
+
feedbackbasket widget settings myapp --capture-mode feedback
|
|
127
134
|
feedbackbasket widget settings myapp --color "#22c55e" --label "Send Feedback"
|
|
128
135
|
feedbackbasket widget settings myapp --position bottom-left --display modal
|
|
129
136
|
feedbackbasket widget settings myapp --email-required --intro "How can we improve?"
|
|
130
137
|
feedbackbasket widget settings myapp --button-radius 10 --button-size regular
|
|
131
|
-
feedbackbasket widget settings myapp --show-email --allow-attachments
|
|
138
|
+
feedbackbasket widget settings myapp --show-email --allow-attachments
|
|
132
139
|
feedbackbasket widget settings myapp --email-read-only --hide-email-when-prefilled
|
|
140
|
+
feedbackbasket widget settings myapp --error-tracking --allow-console-errors
|
|
133
141
|
|
|
134
142
|
# Configure guided feedback types and follow-up questions
|
|
135
143
|
feedbackbasket widget flow myapp
|
|
136
|
-
feedbackbasket widget flow myapp --enable
|
|
137
|
-
feedbackbasket widget flow myapp --reset-default --enable
|
|
144
|
+
feedbackbasket widget flow myapp --enable # guided only when requested
|
|
145
|
+
feedbackbasket widget flow myapp --reset-default --enable # guided only when requested
|
|
138
146
|
feedbackbasket widget flow myapp --config ./feedback-flow.json
|
|
139
147
|
|
|
140
148
|
# Get embed code (ready to paste into your HTML)
|
|
141
149
|
feedbackbasket widget script myapp
|
|
142
150
|
```
|
|
143
151
|
|
|
152
|
+
Waitlist mode uses the same project script. Add `data-feedbackbasket-waitlist` to your own form, with a required `email` field and optional `name` field. The CLI's `widget script` output shows a starter form when waitlist mode is active.
|
|
153
|
+
|
|
154
|
+
### Waitlist
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
feedbackbasket waitlist list myapp
|
|
158
|
+
feedbackbasket waitlist list myapp --search "@example.com" --limit 50 --offset 0
|
|
159
|
+
feedbackbasket waitlist list myapp --agent
|
|
160
|
+
feedbackbasket waitlist export myapp
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Waitlist listing returns emails, optional names, source pages, total counts, the active capture mode, and pagination. Export prints the dashboard-compatible CSV to stdout.
|
|
164
|
+
|
|
144
165
|
For inline trigger mode, load the widget once and call the public API from your own button:
|
|
145
166
|
|
|
146
167
|
```html
|
|
@@ -151,8 +172,12 @@ For inline trigger mode, load the widget once and call the public API from your
|
|
|
151
172
|
|
|
152
173
|
Passing the trigger element lets popup mode open beside your custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
|
|
153
174
|
|
|
175
|
+
Use only the public `openFeedbackForm()` API from the snippet. Do not call internal or undocumented methods such as `open()` or `openModal()`.
|
|
176
|
+
|
|
154
177
|
Use `--email-read-only` and `--hide-email-when-prefilled` with runtime `userEmail` values from your app. These settings do not store visitor emails in FeedbackBasket widget settings.
|
|
155
178
|
|
|
179
|
+
The default widget experience is a basic modal. Only switch to popup mode or enable guided feedback when you intentionally want that flow.
|
|
180
|
+
|
|
156
181
|
`widget flow --config` accepts either a `feedbackFlow` object or a JSON object with a `feedbackFlow` key. V1 supports guided mode with `text`, `textarea`, and `single_choice` follow-up questions.
|
|
157
182
|
|
|
158
183
|
```json
|
package/dist/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createDoctorCommand } from './commands/doctor.js';
|
|
|
11
11
|
import { createSetupCommand } from './commands/setup.js';
|
|
12
12
|
import { createWidgetCommand } from './commands/widget.js';
|
|
13
13
|
import { createTeamCommand } from './commands/team.js';
|
|
14
|
+
import { createWaitlistCommand } from './commands/waitlist.js';
|
|
14
15
|
import { renderRootHelp } from './help.js';
|
|
15
16
|
let writer;
|
|
16
17
|
function resolveFormat(opts) {
|
|
@@ -62,6 +63,7 @@ export function run() {
|
|
|
62
63
|
program.addCommand(createFeedbackCommand(getWriter));
|
|
63
64
|
program.addCommand(createBugsCommand(getWriter));
|
|
64
65
|
program.addCommand(createWidgetCommand(getWriter));
|
|
66
|
+
program.addCommand(createWaitlistCommand(getWriter));
|
|
65
67
|
program.addCommand(createTeamCommand(getWriter));
|
|
66
68
|
program.addCommand(createDoctorCommand(getWriter));
|
|
67
69
|
program.addCommand(createSetupCommand(getWriter));
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
|
|
1
|
+
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings, WaitlistResponse } from './types.js';
|
|
2
2
|
export declare class FeedbackBasketClient {
|
|
3
|
-
private
|
|
3
|
+
private readonly apiBaseUrl;
|
|
4
|
+
private readonly token;
|
|
4
5
|
constructor(token: string, baseUrl: string);
|
|
5
6
|
me(): Promise<UserProfile>;
|
|
6
7
|
listProjects(): Promise<ProjectsResponse>;
|
|
@@ -48,9 +49,16 @@ export declare class FeedbackBasketClient {
|
|
|
48
49
|
getWidgetScript(projectId: string): Promise<{
|
|
49
50
|
projectId: string;
|
|
50
51
|
projectName: string;
|
|
52
|
+
captureMode: 'feedback' | 'waitlist';
|
|
51
53
|
embedCode: string;
|
|
52
54
|
scriptUrl: string;
|
|
53
55
|
}>;
|
|
56
|
+
getWaitlist(projectId: string, params?: {
|
|
57
|
+
search?: string;
|
|
58
|
+
limit?: number;
|
|
59
|
+
offset?: number;
|
|
60
|
+
}): Promise<WaitlistResponse>;
|
|
61
|
+
exportWaitlist(projectId: string): Promise<string>;
|
|
54
62
|
updateFeedback(id: string, data: {
|
|
55
63
|
status?: string;
|
|
56
64
|
category?: string;
|
|
@@ -118,5 +126,4 @@ export declare class FeedbackBasketClient {
|
|
|
118
126
|
email: string;
|
|
119
127
|
}>;
|
|
120
128
|
private request;
|
|
121
|
-
private handleError;
|
|
122
129
|
}
|
package/dist/src/client.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import axios, { AxiosError } from 'axios';
|
|
2
1
|
import { USER_AGENT } from './version.js';
|
|
3
|
-
import { errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
|
|
2
|
+
import { CLIError, errAuth, errForbidden, errRateLimit, errNetwork, errAPI } from './output/errors.js';
|
|
4
3
|
export class FeedbackBasketClient {
|
|
5
|
-
|
|
4
|
+
apiBaseUrl;
|
|
5
|
+
token;
|
|
6
6
|
constructor(token, baseUrl) {
|
|
7
|
-
this.
|
|
8
|
-
|
|
9
|
-
timeout: 30_000,
|
|
10
|
-
headers: {
|
|
11
|
-
'Authorization': `Bearer ${token}`,
|
|
12
|
-
'Content-Type': 'application/json',
|
|
13
|
-
'User-Agent': USER_AGENT,
|
|
14
|
-
},
|
|
15
|
-
});
|
|
7
|
+
this.apiBaseUrl = `${baseUrl.replace(/\/$/, '')}/api/v1`;
|
|
8
|
+
this.token = token;
|
|
16
9
|
}
|
|
17
10
|
async me() {
|
|
18
11
|
return this.request('GET', '/auth/me');
|
|
@@ -63,6 +56,14 @@ export class FeedbackBasketClient {
|
|
|
63
56
|
async getWidgetScript(projectId) {
|
|
64
57
|
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget-script`);
|
|
65
58
|
}
|
|
59
|
+
async getWaitlist(projectId, params = {}) {
|
|
60
|
+
const query = buildQuery(params);
|
|
61
|
+
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist${query}`);
|
|
62
|
+
}
|
|
63
|
+
async exportWaitlist(projectId) {
|
|
64
|
+
const data = await this.request('GET', `/projects/${encodeURIComponent(projectId)}/waitlist/export`);
|
|
65
|
+
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
66
|
+
}
|
|
66
67
|
// Write operations
|
|
67
68
|
async updateFeedback(id, data) {
|
|
68
69
|
return this.request('PATCH', `/feedback/${encodeURIComponent(id)}`, data);
|
|
@@ -95,8 +96,8 @@ export class FeedbackBasketClient {
|
|
|
95
96
|
return this.request('DELETE', `/feedback/${encodeURIComponent(feedbackId)}/notes/${encodeURIComponent(noteId)}`);
|
|
96
97
|
}
|
|
97
98
|
async exportFeedback(projectId, format = 'csv') {
|
|
98
|
-
const
|
|
99
|
-
return typeof
|
|
99
|
+
const data = await this.request('GET', `/projects/${encodeURIComponent(projectId)}/export?format=${format}`);
|
|
100
|
+
return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
100
101
|
}
|
|
101
102
|
// Team
|
|
102
103
|
async listTeam() {
|
|
@@ -109,34 +110,58 @@ export class FeedbackBasketClient {
|
|
|
109
110
|
return this.request('DELETE', `/team/${encodeURIComponent(memberId)}`);
|
|
110
111
|
}
|
|
111
112
|
async request(method, path, data) {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const timeout = setTimeout(() => controller.abort(), 30_000);
|
|
112
115
|
try {
|
|
113
|
-
const response = await this.
|
|
114
|
-
|
|
116
|
+
const response = await fetch(`${this.apiBaseUrl}${path}`, {
|
|
117
|
+
method,
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
headers: {
|
|
120
|
+
'Authorization': `Bearer ${this.token}`,
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'User-Agent': USER_AGENT,
|
|
123
|
+
},
|
|
124
|
+
body: data === undefined ? undefined : JSON.stringify(data),
|
|
125
|
+
});
|
|
126
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
127
|
+
const payload = contentType.includes('application/json')
|
|
128
|
+
? await response.json().catch(() => null)
|
|
129
|
+
: await response.text();
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const message = getErrorMessage(payload, response.statusText);
|
|
132
|
+
switch (response.status) {
|
|
133
|
+
case 401: throw errAuth(message);
|
|
134
|
+
case 403: throw errForbidden(message);
|
|
135
|
+
case 404: throw errAPI(404, message);
|
|
136
|
+
case 429: throw errRateLimit();
|
|
137
|
+
default: throw errAPI(response.status, message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return payload;
|
|
115
141
|
}
|
|
116
142
|
catch (error) {
|
|
117
|
-
|
|
143
|
+
if (error instanceof CLIError)
|
|
144
|
+
throw error;
|
|
145
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
146
|
+
throw errNetwork(cause);
|
|
118
147
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (error instanceof AxiosError) {
|
|
122
|
-
const status = error.response?.status;
|
|
123
|
-
const message = error.response?.data?.error
|
|
124
|
-
?? error.response?.data?.message
|
|
125
|
-
?? error.message;
|
|
126
|
-
if (!error.response) {
|
|
127
|
-
return errNetwork(error);
|
|
128
|
-
}
|
|
129
|
-
switch (status) {
|
|
130
|
-
case 401: return errAuth(message);
|
|
131
|
-
case 403: return errForbidden(message);
|
|
132
|
-
case 404: return errAPI(404, message);
|
|
133
|
-
case 429: return errRateLimit();
|
|
134
|
-
default: return errAPI(status ?? 500, message);
|
|
135
|
-
}
|
|
148
|
+
finally {
|
|
149
|
+
clearTimeout(timeout);
|
|
136
150
|
}
|
|
137
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
138
151
|
}
|
|
139
152
|
}
|
|
153
|
+
function getErrorMessage(payload, fallback) {
|
|
154
|
+
if (payload && typeof payload === 'object') {
|
|
155
|
+
const value = payload;
|
|
156
|
+
if (typeof value.error === 'string')
|
|
157
|
+
return value.error;
|
|
158
|
+
if (typeof value.message === 'string')
|
|
159
|
+
return value.message;
|
|
160
|
+
}
|
|
161
|
+
if (typeof payload === 'string' && payload.trim())
|
|
162
|
+
return payload;
|
|
163
|
+
return fallback || 'Request failed';
|
|
164
|
+
}
|
|
140
165
|
function buildQuery(params) {
|
|
141
166
|
const parts = [];
|
|
142
167
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -59,9 +59,11 @@ export function createProjectsCommand(getWriter) {
|
|
|
59
59
|
.description('Create a new project')
|
|
60
60
|
.requiredOption('--url <url>', 'Project URL')
|
|
61
61
|
.option('--description <text>', 'Project description')
|
|
62
|
+
.option('--allow-local-url', 'Allow localhost URLs for local-only test projects')
|
|
62
63
|
.action(async (name, opts) => {
|
|
63
64
|
const writer = getWriter();
|
|
64
65
|
const client = requireClient();
|
|
66
|
+
await ensureProjectUrlIsIntentional(opts.url, writer, Boolean(opts.allowLocalUrl));
|
|
65
67
|
const result = await client.createProject({
|
|
66
68
|
name,
|
|
67
69
|
url: opts.url,
|
|
@@ -89,6 +91,7 @@ export function createProjectsCommand(getWriter) {
|
|
|
89
91
|
.option('--url <url>', 'New project URL')
|
|
90
92
|
.option('--description <text>', 'New project description')
|
|
91
93
|
.option('--reply-to <email>', 'Default reply-to email for feedback replies (empty string to clear)')
|
|
94
|
+
.option('--allow-local-url', 'Allow localhost URLs for local-only test projects')
|
|
92
95
|
.action(async (idOrName, opts) => {
|
|
93
96
|
const writer = getWriter();
|
|
94
97
|
if (!opts.name &&
|
|
@@ -100,6 +103,9 @@ export function createProjectsCommand(getWriter) {
|
|
|
100
103
|
const client = requireClient();
|
|
101
104
|
const resolved = await resolveProject(client, idOrName);
|
|
102
105
|
const id = resolved.id;
|
|
106
|
+
if (opts.url) {
|
|
107
|
+
await ensureProjectUrlIsIntentional(opts.url, writer, Boolean(opts.allowLocalUrl));
|
|
108
|
+
}
|
|
103
109
|
const data = {};
|
|
104
110
|
if (opts.name)
|
|
105
111
|
data['name'] = opts.name;
|
|
@@ -225,3 +231,32 @@ function renderProjectDetail(project) {
|
|
|
225
231
|
}
|
|
226
232
|
console.log();
|
|
227
233
|
}
|
|
234
|
+
async function ensureProjectUrlIsIntentional(url, writer, allowLocalUrl) {
|
|
235
|
+
if (!isLocalProjectUrl(url) || allowLocalUrl)
|
|
236
|
+
return;
|
|
237
|
+
const message = 'Project URL looks like a local development address. Use the production, staging, preview, or intended public website URL, or pass --allow-local-url for a local-only test project.';
|
|
238
|
+
if (writer.isMachineOutput() || !process.stdin.isTTY) {
|
|
239
|
+
throw errUsage(message, 'Example: feedbackbasket projects create "My App" --url https://myapp.com');
|
|
240
|
+
}
|
|
241
|
+
console.log(` ${brand.warning('Warning:')} ${message}`);
|
|
242
|
+
console.log();
|
|
243
|
+
const confirmed = await confirm(' Use this as a local-only test project URL?', false);
|
|
244
|
+
if (!confirmed) {
|
|
245
|
+
throw errUsage('Project URL change cancelled. Re-run with the real website URL.', 'Example: feedbackbasket projects create "My App" --url https://myapp.com');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function isLocalProjectUrl(input) {
|
|
249
|
+
try {
|
|
250
|
+
const url = new URL(input);
|
|
251
|
+
const hostname = url.hostname.toLowerCase();
|
|
252
|
+
return (hostname === 'localhost' ||
|
|
253
|
+
hostname === '127.0.0.1' ||
|
|
254
|
+
hostname === '0.0.0.0' ||
|
|
255
|
+
hostname === '::1' ||
|
|
256
|
+
hostname === '[::1]' ||
|
|
257
|
+
hostname.endsWith('.localhost'));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 { Format } from '../output/writer.js';
|
|
7
|
+
import { brand, divider } from '../output/theme.js';
|
|
8
|
+
import { resolveProject } from '../resolve.js';
|
|
9
|
+
export function createWaitlistCommand(getWriter) {
|
|
10
|
+
const waitlist = new Command('waitlist')
|
|
11
|
+
.description('View and export waitlist signups');
|
|
12
|
+
waitlist
|
|
13
|
+
.command('list [project]')
|
|
14
|
+
.description('List waitlist signups with optional search')
|
|
15
|
+
.option('--search <query>', 'Search email addresses and names')
|
|
16
|
+
.option('--limit <n>', 'Max results (1-100)', '50')
|
|
17
|
+
.option('--offset <n>', 'Offset for pagination', '0')
|
|
18
|
+
.action(async (projectArg, opts) => {
|
|
19
|
+
const writer = getWriter();
|
|
20
|
+
const client = requireClient();
|
|
21
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
22
|
+
const limit = parseInteger(opts.limit, '--limit', 1, 100);
|
|
23
|
+
const offset = parseInteger(opts.offset, '--offset', 0);
|
|
24
|
+
const result = await client.getWaitlist(projectId, {
|
|
25
|
+
search: opts.search,
|
|
26
|
+
limit,
|
|
27
|
+
offset,
|
|
28
|
+
});
|
|
29
|
+
if (writer.effectiveFormat() === Format.Styled) {
|
|
30
|
+
renderWaitlist(result.project.name, result.project.captureMode, result.entries, result.totalSignups, result.pagination.totalCount);
|
|
31
|
+
if (result.pagination.hasMore) {
|
|
32
|
+
console.log(brand.muted(` Next page: feedbackbasket waitlist list ${projectId} --offset ${offset + limit} --limit ${limit}`));
|
|
33
|
+
console.log();
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
writer.ok(result, {
|
|
38
|
+
summary: `Showing ${result.entries.length} of ${result.pagination.totalCount} matching waitlist signups`,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
waitlist
|
|
42
|
+
.command('export [project]')
|
|
43
|
+
.description('Export all waitlist signups as CSV')
|
|
44
|
+
.action(async (projectArg) => {
|
|
45
|
+
const client = requireClient();
|
|
46
|
+
const projectId = await resolveProjectId(client, projectArg);
|
|
47
|
+
console.log(await client.exportWaitlist(projectId));
|
|
48
|
+
});
|
|
49
|
+
return waitlist;
|
|
50
|
+
}
|
|
51
|
+
function requireClient() {
|
|
52
|
+
const manager = new AuthManager();
|
|
53
|
+
const token = manager.resolveToken();
|
|
54
|
+
if (!token)
|
|
55
|
+
throw errAuth();
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
return new FeedbackBasketClient(token, config.baseUrl);
|
|
58
|
+
}
|
|
59
|
+
async function resolveProjectId(client, projectArg) {
|
|
60
|
+
if (projectArg)
|
|
61
|
+
return (await resolveProject(client, projectArg)).id;
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
if (config.defaultProject)
|
|
64
|
+
return config.defaultProject;
|
|
65
|
+
throw errUsage('Project is required. Pass a project name/ID or set a default.', 'feedbackbasket waitlist list <project>');
|
|
66
|
+
}
|
|
67
|
+
function parseInteger(value, flag, minimum, maximum) {
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
if (!Number.isInteger(parsed) ||
|
|
70
|
+
parsed < minimum ||
|
|
71
|
+
(maximum !== undefined && parsed > maximum)) {
|
|
72
|
+
const range = maximum === undefined ? `${minimum} or greater` : `${minimum}-${maximum}`;
|
|
73
|
+
throw errUsage(`${flag} must be an integer in the range ${range}`);
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
function renderWaitlist(projectName, captureMode, entries, totalSignups, matchingSignups) {
|
|
78
|
+
console.log(brand.bold(`Waitlist — ${projectName}`));
|
|
79
|
+
console.log(divider(50));
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(` ${brand.label('Capture mode')} ${captureMode}`);
|
|
82
|
+
console.log(` ${brand.label('Total signups')} ${totalSignups}`);
|
|
83
|
+
if (matchingSignups !== totalSignups) {
|
|
84
|
+
console.log(` ${brand.label('Matches')} ${matchingSignups}`);
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
if (entries.length === 0) {
|
|
88
|
+
console.log(` ${brand.muted('No waitlist signups found')}`);
|
|
89
|
+
console.log();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
console.log(` ${brand.bold(entry.email)}${entry.name ? ` — ${entry.name}` : ''}`);
|
|
94
|
+
console.log(` ${brand.muted(new Date(entry.updatedAt).toLocaleString())}`);
|
|
95
|
+
if (entry.pageUrl)
|
|
96
|
+
console.log(` ${brand.muted(entry.pageUrl)}`);
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -51,6 +51,7 @@ export function createWidgetCommand(getWriter) {
|
|
|
51
51
|
widget
|
|
52
52
|
.command('settings [project]')
|
|
53
53
|
.description('View or update widget settings')
|
|
54
|
+
.option('--capture-mode <mode>', 'Capture mode (feedback, waitlist)')
|
|
54
55
|
.option('--color <hex>', 'Button color (e.g. #22c55e)')
|
|
55
56
|
.option('--label <text>', 'Button label')
|
|
56
57
|
.option('--position <pos>', 'Widget position (bottom-right, bottom-left, middle-right-edge, middle-left-edge, bottom-right-edge, bottom-left-edge)')
|
|
@@ -77,6 +78,10 @@ export function createWidgetCommand(getWriter) {
|
|
|
77
78
|
.option('--no-show-icon', 'Hide the icon')
|
|
78
79
|
.option('--show-branding', 'Show FeedbackBasket branding')
|
|
79
80
|
.option('--no-show-branding', 'Hide FeedbackBasket branding when plan allows it')
|
|
81
|
+
.option('--allow-console-errors', 'Let visitors include captured console errors with feedback')
|
|
82
|
+
.option('--no-allow-console-errors', 'Hide the console error sharing option')
|
|
83
|
+
.option('--error-tracking', 'Enable automatic browser error tracking')
|
|
84
|
+
.option('--no-error-tracking', 'Disable automatic browser error tracking')
|
|
80
85
|
.option('--z-index <value>', 'Widget z-index')
|
|
81
86
|
.option('--guided', 'Enable guided feedback types')
|
|
82
87
|
.option('--disable-guided', 'Disable guided feedback types')
|
|
@@ -84,20 +89,26 @@ export function createWidgetCommand(getWriter) {
|
|
|
84
89
|
const writer = getWriter();
|
|
85
90
|
const client = requireClient();
|
|
86
91
|
const projectId = await resolveProjectId(client, projectArg);
|
|
87
|
-
const hasUpdates = opts.color || opts.label || opts.position || opts.intro ||
|
|
92
|
+
const hasUpdates = opts.captureMode || opts.color || opts.label || opts.position || opts.intro ||
|
|
88
93
|
opts.success || opts.trigger || opts.display ||
|
|
89
94
|
opts.buttonRadius || opts.buttonSize || opts.icon ||
|
|
90
95
|
opts.emailRequired !== undefined || opts.showEmail !== undefined ||
|
|
91
96
|
opts.emailReadOnly !== undefined || opts.hideEmailWhenPrefilled !== undefined ||
|
|
92
97
|
opts.allowAttachments !== undefined || opts.iconOnly !== undefined ||
|
|
93
98
|
opts.showIcon !== undefined || opts.showBranding !== undefined ||
|
|
99
|
+
opts.allowConsoleErrors !== undefined || opts.errorTracking !== undefined ||
|
|
94
100
|
opts.zIndex || opts.guided || opts.disableGuided;
|
|
95
101
|
if (hasUpdates) {
|
|
96
102
|
if (opts.guided && opts.disableGuided) {
|
|
97
103
|
throw errUsage('Choose either --guided or --disable-guided, not both');
|
|
98
104
|
}
|
|
105
|
+
if (opts.captureMode && !['feedback', 'waitlist'].includes(opts.captureMode)) {
|
|
106
|
+
throw errUsage('Capture mode must be feedback or waitlist');
|
|
107
|
+
}
|
|
99
108
|
// Update mode
|
|
100
109
|
const settings = {};
|
|
110
|
+
if (opts.captureMode)
|
|
111
|
+
settings.captureMode = opts.captureMode;
|
|
101
112
|
if (opts.color)
|
|
102
113
|
settings.buttonColor = opts.color;
|
|
103
114
|
if (opts.label)
|
|
@@ -134,6 +145,10 @@ export function createWidgetCommand(getWriter) {
|
|
|
134
145
|
settings.showIcon = opts.showIcon;
|
|
135
146
|
if (opts.showBranding !== undefined)
|
|
136
147
|
settings.showBranding = opts.showBranding;
|
|
148
|
+
if (opts.allowConsoleErrors !== undefined)
|
|
149
|
+
settings.allowConsoleErrors = opts.allowConsoleErrors;
|
|
150
|
+
if (opts.errorTracking !== undefined)
|
|
151
|
+
settings.errorTrackingEnabled = opts.errorTracking;
|
|
137
152
|
if (opts.zIndex)
|
|
138
153
|
settings.zIndex = parseInt(opts.zIndex, 10);
|
|
139
154
|
if (opts.guided || opts.disableGuided) {
|
|
@@ -153,7 +168,7 @@ export function createWidgetCommand(getWriter) {
|
|
|
153
168
|
breadcrumbs: [
|
|
154
169
|
{ action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
|
|
155
170
|
{ action: 'View settings', cmd: `feedbackbasket widget settings ${projectId}` },
|
|
156
|
-
{ action: 'Configure guided questions', cmd: `feedbackbasket widget flow ${projectId}
|
|
171
|
+
{ action: 'Configure guided questions', cmd: `feedbackbasket widget flow ${projectId}` },
|
|
157
172
|
],
|
|
158
173
|
});
|
|
159
174
|
}
|
|
@@ -167,7 +182,7 @@ export function createWidgetCommand(getWriter) {
|
|
|
167
182
|
summary: `Widget settings for "${result.projectName}"`,
|
|
168
183
|
breadcrumbs: [
|
|
169
184
|
{ action: 'Update color', cmd: `feedbackbasket widget settings ${projectId} --color "#22c55e"` },
|
|
170
|
-
{ action: '
|
|
185
|
+
{ action: 'View guided flow', cmd: `feedbackbasket widget flow ${projectId}` },
|
|
171
186
|
{ action: 'Get embed code', cmd: `feedbackbasket widget script ${projectId}` },
|
|
172
187
|
],
|
|
173
188
|
});
|
|
@@ -227,8 +242,8 @@ export function createWidgetCommand(getWriter) {
|
|
|
227
242
|
writer.ok(result.settings.feedbackFlow ?? DEFAULT_FEEDBACK_FLOW, {
|
|
228
243
|
summary: `Guided feedback flow for "${result.projectName}"`,
|
|
229
244
|
breadcrumbs: [
|
|
230
|
-
{ action: 'Enable guided flow', cmd: `feedbackbasket widget flow ${projectId} --enable` },
|
|
231
|
-
{ action: 'Reset templates', cmd: `feedbackbasket widget flow ${projectId} --reset-default --enable` },
|
|
245
|
+
{ action: 'Enable guided flow if requested', cmd: `feedbackbasket widget flow ${projectId} --enable` },
|
|
246
|
+
{ action: 'Reset templates if requested', cmd: `feedbackbasket widget flow ${projectId} --reset-default --enable` },
|
|
232
247
|
{ action: 'Apply JSON config', cmd: `feedbackbasket widget flow ${projectId} --config ./feedback-flow.json` },
|
|
233
248
|
],
|
|
234
249
|
});
|
|
@@ -253,9 +268,26 @@ export function createWidgetCommand(getWriter) {
|
|
|
253
268
|
console.log();
|
|
254
269
|
console.log(brand.muted(` Script URL: ${result.scriptUrl}`));
|
|
255
270
|
console.log();
|
|
256
|
-
|
|
271
|
+
if (settingsResult.settings.captureMode === 'waitlist') {
|
|
272
|
+
renderWaitlistHelp();
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
renderInlineTriggerHelp(settingsResult.settings);
|
|
276
|
+
}
|
|
257
277
|
}
|
|
258
|
-
|
|
278
|
+
const output = result.captureMode === 'waitlist'
|
|
279
|
+
? {
|
|
280
|
+
...result,
|
|
281
|
+
waitlist: {
|
|
282
|
+
formAttribute: 'data-feedbackbasket-waitlist',
|
|
283
|
+
requiredField: 'email',
|
|
284
|
+
optionalField: 'name',
|
|
285
|
+
stateAttribute: 'data-feedbackbasket-state',
|
|
286
|
+
events: ['feedbackbasket:waitlist:success', 'feedbackbasket:waitlist:error'],
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
: result;
|
|
290
|
+
writer.ok(output, {
|
|
259
291
|
summary: `Embed code for "${result.projectName}"`,
|
|
260
292
|
breadcrumbs: [
|
|
261
293
|
{ action: 'Customize widget', cmd: `feedbackbasket widget settings ${projectId}` },
|
|
@@ -362,6 +394,23 @@ function renderInlineTriggerHelp(settings) {
|
|
|
362
394
|
console.log(brand.muted(' Modal mode stays centered; passing the trigger is safe for future popup changes.'));
|
|
363
395
|
}
|
|
364
396
|
console.log(brand.muted(' Existing calls to window.FeedbackWidget.openFeedbackForm() still work.'));
|
|
397
|
+
console.log(brand.muted(' Do not call internal methods such as open() or openModal().'));
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
function renderWaitlistHelp() {
|
|
401
|
+
console.log(brand.bold('Waitlist form setup'));
|
|
402
|
+
console.log(divider(50));
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(brand.muted(' Keep the project script installed and annotate your own form:'));
|
|
405
|
+
console.log();
|
|
406
|
+
console.log(` ${brand.primary('<form data-feedbackbasket-waitlist>')}`);
|
|
407
|
+
console.log(` ${brand.primary(' <input name="name" autocomplete="name">')}`);
|
|
408
|
+
console.log(` ${brand.primary(' <input name="email" type="email" autocomplete="email" required>')}`);
|
|
409
|
+
console.log(` ${brand.primary(' <button type="submit">Join the waitlist</button>')}`);
|
|
410
|
+
console.log(` ${brand.primary('</form>')}`);
|
|
411
|
+
console.log();
|
|
412
|
+
console.log(brand.muted(' Email is required; name is optional. The script handles submission without replacing your styling.'));
|
|
413
|
+
console.log(brand.muted(' Read data-feedbackbasket-state for loading, success, and error UI.'));
|
|
365
414
|
console.log();
|
|
366
415
|
}
|
|
367
416
|
function renderWidgetSettings(projectName, settings) {
|
|
@@ -369,6 +418,7 @@ function renderWidgetSettings(projectName, settings) {
|
|
|
369
418
|
console.log(divider(40));
|
|
370
419
|
console.log();
|
|
371
420
|
const display = [
|
|
421
|
+
['Capture Mode', String(settings.captureMode ?? 'feedback')],
|
|
372
422
|
['Button Color', String(settings.buttonColor ?? '')],
|
|
373
423
|
['Button Label', String(settings.buttonLabel ?? '')],
|
|
374
424
|
['Button Radius', String(settings.buttonRadius ?? '')],
|
|
@@ -389,6 +439,8 @@ function renderWidgetSettings(projectName, settings) {
|
|
|
389
439
|
['Success Message', String(settings.successMessage ?? '')],
|
|
390
440
|
['Z-Index', String(settings.zIndex ?? '')],
|
|
391
441
|
['Show Branding', String(settings.showBranding ?? true)],
|
|
442
|
+
['Share Console Errors', String(settings.allowConsoleErrors ?? false)],
|
|
443
|
+
['Error Tracking', String(settings.errorTrackingEnabled ?? false)],
|
|
392
444
|
];
|
|
393
445
|
for (const [label, value] of display) {
|
|
394
446
|
console.log(` ${brand.label(label.padEnd(18))} ${value}`);
|
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, widgets, and team from your terminal.')}`);
|
|
20
|
+
lines.push(` ${brand.muted('Manage projects, feedback, waitlists, widgets, and team from your terminal.')}`);
|
|
21
21
|
lines.push('');
|
|
22
22
|
// Core Commands
|
|
23
23
|
lines.push(section(' CORE COMMANDS'));
|
|
@@ -25,6 +25,7 @@ export function renderRootHelp() {
|
|
|
25
25
|
lines.push(cmd('feedback', 'View and manage feedback'));
|
|
26
26
|
lines.push(cmd('bugs', 'View bug reports with severity'));
|
|
27
27
|
lines.push(cmd('widget', 'Manage feedback widget & get embed code'));
|
|
28
|
+
lines.push(cmd('waitlist', 'View and export waitlist signups'));
|
|
28
29
|
lines.push(cmd('team', 'Manage organization members'));
|
|
29
30
|
lines.push('');
|
|
30
31
|
// Shortcuts
|
|
@@ -36,6 +37,7 @@ export function renderRootHelp() {
|
|
|
36
37
|
lines.push(section(' SEARCH & EXPORT'));
|
|
37
38
|
lines.push(cmd('feedback search', 'Search feedback across projects'));
|
|
38
39
|
lines.push(cmd('feedback export', 'Export feedback to CSV, Markdown, or JSON'));
|
|
40
|
+
lines.push(cmd('waitlist export', 'Export waitlist signups to CSV'));
|
|
39
41
|
lines.push(cmd('bugs stats', 'Bug statistics summary'));
|
|
40
42
|
lines.push('');
|
|
41
43
|
// Auth & Config
|
|
@@ -61,7 +63,9 @@ export function renderRootHelp() {
|
|
|
61
63
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket feedback list --category BUG --status OPEN`);
|
|
62
64
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket bugs list --severity high`);
|
|
63
65
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget script myapp`);
|
|
64
|
-
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget
|
|
66
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --display modal`);
|
|
67
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget settings myapp --capture-mode waitlist`);
|
|
68
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket waitlist list myapp --search "@example.com"`);
|
|
65
69
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects create "My App" --url https://myapp.com`);
|
|
66
70
|
lines.push('');
|
|
67
71
|
// Learn More
|
package/dist/src/types.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface FeedbackFlowSettings {
|
|
|
24
24
|
types: FeedbackFlowType[];
|
|
25
25
|
}
|
|
26
26
|
export interface WidgetSettings {
|
|
27
|
+
captureMode?: 'feedback' | 'waitlist';
|
|
27
28
|
widgetType?: string;
|
|
28
29
|
triggerMode?: 'floating' | 'inline';
|
|
29
30
|
buttonColor?: string;
|
|
@@ -44,6 +45,8 @@ export interface WidgetSettings {
|
|
|
44
45
|
displayMode?: 'modal' | 'popup';
|
|
45
46
|
zIndex?: number;
|
|
46
47
|
showBranding?: boolean;
|
|
48
|
+
allowConsoleErrors?: boolean;
|
|
49
|
+
errorTrackingEnabled?: boolean;
|
|
47
50
|
feedbackFlow?: FeedbackFlowSettings;
|
|
48
51
|
}
|
|
49
52
|
export interface Project {
|
|
@@ -124,6 +127,25 @@ export interface Pagination {
|
|
|
124
127
|
offset: number;
|
|
125
128
|
hasMore: boolean;
|
|
126
129
|
}
|
|
130
|
+
export interface WaitlistEntry {
|
|
131
|
+
id: string;
|
|
132
|
+
email: string;
|
|
133
|
+
name?: string | null;
|
|
134
|
+
pageUrl?: string | null;
|
|
135
|
+
referrerUrl?: string | null;
|
|
136
|
+
createdAt: string;
|
|
137
|
+
updatedAt: string;
|
|
138
|
+
}
|
|
139
|
+
export interface WaitlistResponse {
|
|
140
|
+
project: {
|
|
141
|
+
id: string;
|
|
142
|
+
name: string;
|
|
143
|
+
captureMode: 'feedback' | 'waitlist';
|
|
144
|
+
};
|
|
145
|
+
entries: WaitlistEntry[];
|
|
146
|
+
totalSignups: number;
|
|
147
|
+
pagination: Pagination;
|
|
148
|
+
}
|
|
127
149
|
export interface FeedbackResponse {
|
|
128
150
|
feedback: Feedback[];
|
|
129
151
|
pagination: Pagination;
|
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.10.0";
|
|
2
|
+
export declare const USER_AGENT = "FeedbackBasket-CLI/0.10.0";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.10.0';
|
|
2
2
|
export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feedbackbasket-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Command-line interface for FeedbackBasket — manage feedback from your terminal",
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -28,14 +28,13 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://feedbackbasket.com",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"axios": "1.7.0",
|
|
32
31
|
"chalk": "^5.3.0",
|
|
33
32
|
"commander": "^13.1.0",
|
|
34
33
|
"open": "^10.1.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
36
|
"@types/node": "^22.0.0",
|
|
38
|
-
"tsx": "^4.
|
|
37
|
+
"tsx": "^4.23.0",
|
|
39
38
|
"typescript": "^5.7.0"
|
|
40
39
|
},
|
|
41
40
|
"engines": {
|
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Manage FeedbackBasket projects, feedback, bugs, widgets, and
|
|
4
|
-
triggers:
|
|
5
|
-
- feedbackbasket
|
|
6
|
-
- feedback
|
|
7
|
-
- bugs
|
|
8
|
-
- bug reports
|
|
9
|
-
- user feedback
|
|
10
|
-
- widget
|
|
11
|
-
- feedback widget
|
|
12
|
-
invocable: true
|
|
13
|
-
argument-hint: "<command> [options]"
|
|
2
|
+
name: feedbackbasket
|
|
3
|
+
description: Manage FeedbackBasket projects, feedback, bugs, feedback widgets, waitlist capture, and teams from the command line. Use when an agent needs to configure FeedbackBasket, collect feedback or waitlist signups, query feedback, or manage a FeedbackBasket project.
|
|
14
4
|
---
|
|
15
5
|
|
|
16
6
|
# FeedbackBasket CLI
|
|
17
7
|
|
|
18
|
-
Full command-line interface for managing feedback, bug reports, projects, widgets, and
|
|
8
|
+
Full command-line interface for managing feedback, waitlist signups, bug reports, projects, widgets, and teams in FeedbackBasket. Works with any AI agent that can run shell commands.
|
|
19
9
|
|
|
20
10
|
## Authentication
|
|
21
11
|
|
|
@@ -53,6 +43,18 @@ feedbackbasket projects delete <name-or-id> --yes
|
|
|
53
43
|
|
|
54
44
|
All project commands accept **name or ID**. Names are matched case-insensitively with fuzzy suggestions on typos.
|
|
55
45
|
|
|
46
|
+
**Project selection rule for widget installs:** when the user asks to add a FeedbackBasket widget, bubble, popup, modal, or feedback button to the current app, first resolve the FeedbackBasket project for this app. Do not use the CLI default project just because one is configured.
|
|
47
|
+
|
|
48
|
+
1. Identify the current app's real website URL or intended public URL from the user, app config, docs, or existing FeedbackBasket embed code.
|
|
49
|
+
2. Run `feedbackbasket projects list --agent` and look for an existing project whose `url` matches that site or whose name clearly matches the current app.
|
|
50
|
+
3. If exactly one project matches, use that project ID/name for `widget settings`, `widget script`, and feedback commands.
|
|
51
|
+
4. If multiple projects could match, ask the user which one to use.
|
|
52
|
+
5. If no project matches, ask whether to create a new project for this app, then create it with the confirmed real URL. Do not create a project from a localhost URL unless the user explicitly wants a local-only test project.
|
|
53
|
+
|
|
54
|
+
**Project URL rule for agents:** confirm the real website URL before creating or updating a project. Never use `localhost`, `127.0.0.1`, `0.0.0.0`, `::1`, or a local dev server URL unless the user explicitly says the project is only for local testing. If the repo only exposes a local URL, ask for the production, staging, preview, or intended public URL. Do not guess a public domain from package names, git remotes, or environment variables. For an explicitly local-only test project, pass `--allow-local-url`.
|
|
55
|
+
|
|
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
|
+
|
|
56
58
|
### Feedback
|
|
57
59
|
```bash
|
|
58
60
|
# Read
|
|
@@ -94,21 +96,47 @@ feedbackbasket widget script <project>
|
|
|
94
96
|
|
|
95
97
|
# View settings
|
|
96
98
|
feedbackbasket widget settings <project>
|
|
99
|
+
feedbackbasket widget settings <project> --capture-mode waitlist
|
|
100
|
+
feedbackbasket widget settings <project> --capture-mode feedback
|
|
97
101
|
|
|
98
102
|
# Customize
|
|
99
103
|
feedbackbasket widget settings <project> --color "#22c55e" --label "Send Feedback"
|
|
100
104
|
feedbackbasket widget settings <project> --position bottom-left --display modal
|
|
101
105
|
feedbackbasket widget settings <project> --email-required --intro "How can we improve?"
|
|
102
|
-
feedbackbasket widget settings <project> --show-email --allow-attachments
|
|
106
|
+
feedbackbasket widget settings <project> --show-email --allow-attachments
|
|
103
107
|
feedbackbasket widget settings <project> --email-read-only --hide-email-when-prefilled
|
|
108
|
+
feedbackbasket widget settings <project> --error-tracking --allow-console-errors
|
|
104
109
|
|
|
105
110
|
# Guided feedback types and follow-up questions
|
|
106
111
|
feedbackbasket widget flow <project>
|
|
107
|
-
feedbackbasket widget flow <project> --enable
|
|
108
|
-
feedbackbasket widget flow <project> --reset-default --enable
|
|
112
|
+
feedbackbasket widget flow <project> --enable # only when the user chooses guided feedback
|
|
113
|
+
feedbackbasket widget flow <project> --reset-default --enable # only when the user chooses guided feedback
|
|
109
114
|
feedbackbasket widget flow <project> --config ./feedback-flow.json
|
|
110
115
|
```
|
|
111
116
|
|
|
117
|
+
Waitlist mode keeps the same project script and binds to the host app's own annotated form:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<form data-feedbackbasket-waitlist>
|
|
121
|
+
<input name="name" autocomplete="name">
|
|
122
|
+
<input name="email" type="email" autocomplete="email" required>
|
|
123
|
+
<button type="submit">Join the waitlist</button>
|
|
124
|
+
</form>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Email is required and name is optional. Use `data-feedbackbasket-state="loading|success|error"` for custom UI, or listen for the bubbling `feedbackbasket:waitlist:success` and `feedbackbasket:waitlist:error` events. Do not add a competing submit handler.
|
|
128
|
+
|
|
129
|
+
### Waitlist Signups
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
feedbackbasket waitlist list <project>
|
|
133
|
+
feedbackbasket waitlist list <project> --search "@example.com" --limit 50 --offset 0
|
|
134
|
+
feedbackbasket waitlist list <project> --agent
|
|
135
|
+
feedbackbasket waitlist export <project>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Agent output includes signup emails, optional names, captured/referrer pages, total counts, active capture mode, and pagination. Use `waitlist export` for the same CSV export available in the dashboard.
|
|
139
|
+
|
|
112
140
|
For inline trigger mode, load the widget once and call the public API from the host app's custom button:
|
|
113
141
|
|
|
114
142
|
```html
|
|
@@ -127,9 +155,13 @@ In React:
|
|
|
127
155
|
|
|
128
156
|
Passing the trigger element lets popup mode open beside the custom button. Calling `window.FeedbackWidget.openFeedbackForm()` with no arguments still uses the configured widget position.
|
|
129
157
|
|
|
158
|
+
Use only the public `openFeedbackForm()` API from the snippet. Do not call internal or undocumented methods such as `open()`, `openModal()`, or direct modal element manipulation; those can exist in the widget bundle but are not stable integration points.
|
|
159
|
+
|
|
130
160
|
`email-read-only` and `hide-email-when-prefilled` control behavior only when the host app passes a runtime `userEmail` value. Do not store visitor emails in widget settings.
|
|
131
161
|
|
|
132
|
-
|
|
162
|
+
Use the basic widget experience by default: `displayMode` stays `modal`, and guided feedback stays disabled. Ask the user before switching to `popup` or enabling guided feedback. If the user does not care, keep modal + basic feedback.
|
|
163
|
+
|
|
164
|
+
`widget flow --config` accepts either a `feedbackFlow` object or a JSON object with a `feedbackFlow` key. Use it only when the user wants to customize visitor choices such as Bug report, Feature request, and General feedback. Supported v1 question types are `text`, `textarea`, and `single_choice`.
|
|
133
165
|
|
|
134
166
|
### Team
|
|
135
167
|
```bash
|
|
@@ -146,14 +178,20 @@ feedbackbasket setup claude # Install this skill for Claude Code
|
|
|
146
178
|
|
|
147
179
|
## Common Agent Workflows
|
|
148
180
|
|
|
149
|
-
###
|
|
181
|
+
### Add a widget to the current app
|
|
150
182
|
```bash
|
|
183
|
+
# First resolve the project for this app. Do not rely on the CLI default project.
|
|
184
|
+
feedbackbasket projects list --agent
|
|
185
|
+
|
|
186
|
+
# If no existing project matches the current app's real URL/name, create one after confirming the URL.
|
|
151
187
|
feedbackbasket projects create "My App" --url https://myapp.com --agent
|
|
152
188
|
feedbackbasket widget script "My App" --agent
|
|
153
189
|
# Agent gets the embed code, adds it to the HTML
|
|
154
190
|
feedbackbasket widget settings "My App" --color "#22c55e" --label "Feedback" --agent
|
|
155
|
-
# Optional
|
|
156
|
-
feedbackbasket widget
|
|
191
|
+
# Optional, when the user wants a waitlist instead of feedback capture
|
|
192
|
+
# feedbackbasket widget settings "My App" --capture-mode waitlist --agent
|
|
193
|
+
# Optional, only when requested: enable the guided wizard with Bug, Feature, and General templates
|
|
194
|
+
# feedbackbasket widget flow "My App" --reset-default --enable --agent
|
|
157
195
|
```
|
|
158
196
|
|
|
159
197
|
### Triage new feedback
|
|
@@ -176,6 +214,39 @@ feedbackbasket feedback create "Login button is broken" \
|
|
|
176
214
|
```
|
|
177
215
|
Agent mode returns the created feedback ID, dashboard URL, and feedback object. Created feedback is analyzed by AI and follows the project's notification settings.
|
|
178
216
|
|
|
217
|
+
### File agent-found issues in FeedbackBasket
|
|
218
|
+
|
|
219
|
+
When the user says "file this in FeedbackBasket", "log this bug", "create feedback for this issue", "add this to FeedbackBasket", or similar, create a concise feedback item for the issue the agent found.
|
|
220
|
+
|
|
221
|
+
Before creating the item, resolve the target project:
|
|
222
|
+
|
|
223
|
+
1. If the user explicitly names a FeedbackBasket project, use that project.
|
|
224
|
+
2. If the current repo/app clearly matches exactly one FeedbackBasket project name or project URL from `feedbackbasket projects list --agent`, use that project.
|
|
225
|
+
3. If the CLI default project clearly matches the current repo/app, use it.
|
|
226
|
+
4. If multiple projects are plausible, ask the user which FeedbackBasket project to file it under.
|
|
227
|
+
5. Do not silently guess the project when it is ambiguous.
|
|
228
|
+
|
|
229
|
+
Keep agent-filed feedback short and dashboard-friendly:
|
|
230
|
+
|
|
231
|
+
- Title: under 80 characters, action-oriented, no stack traces.
|
|
232
|
+
- Content: 1 to 3 short paragraphs, ideally under 600 characters, focused on the user-visible problem, expected behavior, and actual behavior.
|
|
233
|
+
- Do not paste long logs, full reasoning chains, or broad investigation notes into the body.
|
|
234
|
+
- Put structured context in metadata: `source=agent`, `found_by=<agent>`, `repo=<name>`, `branch=<branch>`, `route=<path>`, `file=<path>`, `severity=<low|medium|high>`, `test=<command>`.
|
|
235
|
+
|
|
236
|
+
Use:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
feedbackbasket feedback create "<short title>" \
|
|
240
|
+
--content "<brief user-visible issue description>" \
|
|
241
|
+
--project <project-name-or-id> \
|
|
242
|
+
--type bug \
|
|
243
|
+
--metadata source=agent \
|
|
244
|
+
--metadata found_by=codex \
|
|
245
|
+
--agent
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
After creation, report the feedback ID and dashboard URL to the user.
|
|
249
|
+
|
|
179
250
|
### Investigate high-priority bugs
|
|
180
251
|
```bash
|
|
181
252
|
feedbackbasket bugs list --severity high --agent
|