feedbackbasket-cli 0.7.0 → 0.9.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 +7 -1
- package/dist/src/client.d.ts +7 -11
- package/dist/src/client.js +7 -1
- package/dist/src/commands/feedback-create.d.ts +3 -0
- package/dist/src/commands/feedback-create.js +120 -0
- package/dist/src/commands/feedback-reply.js +73 -55
- package/dist/src/commands/feedback.js +3 -0
- package/dist/src/help.js +1 -0
- package/dist/src/types.d.ts +36 -0
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/feedbackbasket/SKILL.md +31 -12
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ feedbackbasket login --manual
|
|
|
22
22
|
# Start exploring
|
|
23
23
|
feedbackbasket projects list
|
|
24
24
|
feedbackbasket feedback list
|
|
25
|
+
feedbackbasket feedback create "Login button is broken" --project myapp --type bug
|
|
25
26
|
feedbackbasket bugs list --severity high
|
|
26
27
|
```
|
|
27
28
|
|
|
@@ -35,6 +36,7 @@ Any AI agent with shell access (Claude Code, Codex, Cursor, OpenCode) can use th
|
|
|
35
36
|
# Agents should use --agent flag for raw JSON output
|
|
36
37
|
feedbackbasket projects list --agent
|
|
37
38
|
feedbackbasket feedback list --category BUG --agent
|
|
39
|
+
feedbackbasket feedback create "Login button is broken" --content "Clicking Log in does nothing in Safari." --project myapp --type bug --agent
|
|
38
40
|
feedbackbasket feedback update <id> --status PLANNED --agent
|
|
39
41
|
feedbackbasket widget script myproject --agent
|
|
40
42
|
```
|
|
@@ -87,9 +89,13 @@ feedbackbasket feedback show <id> # View detail, includ
|
|
|
87
89
|
feedbackbasket feedback search "crash on mobile" # Search shortcut
|
|
88
90
|
|
|
89
91
|
# Write
|
|
92
|
+
feedbackbasket feedback create "Title" --content "Body" --project myapp
|
|
93
|
+
feedbackbasket feedback create "Login bug" --content "Clicking Log in does nothing" --project myapp --type bug --page-url https://example.com/login
|
|
90
94
|
feedbackbasket feedback update <id> --status PLANNED # Update status
|
|
91
95
|
feedbackbasket feedback update <id> --category BUG # Update category
|
|
92
|
-
feedbackbasket feedback reply <id> "Thanks
|
|
96
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery email --reply-to support@example.com
|
|
97
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery widget
|
|
98
|
+
feedbackbasket feedback reply <id> "Thanks!" --delivery both --reply-to support@example.com
|
|
93
99
|
feedbackbasket feedback replies <id> # List sent replies
|
|
94
100
|
feedbackbasket feedback note <id> "Investigating this..." # Add internal note
|
|
95
101
|
feedbackbasket feedback delete <id> # Delete feedback
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
|
|
1
|
+
import type { ProjectsResponse, FeedbackResponse, BugReportsResponse, FeedbackParams, FeedbackCreateInput, FeedbackCreateResponse, FeedbackReplyResponse, BugReportParams, UserProfile, Project, Feedback, WidgetSettings } from './types.js';
|
|
2
2
|
export declare class FeedbackBasketClient {
|
|
3
3
|
private http;
|
|
4
4
|
constructor(token: string, baseUrl: string);
|
|
@@ -34,6 +34,7 @@ export declare class FeedbackBasketClient {
|
|
|
34
34
|
category?: string;
|
|
35
35
|
limit?: number;
|
|
36
36
|
}): Promise<FeedbackResponse>;
|
|
37
|
+
createFeedback(data: FeedbackCreateInput): Promise<FeedbackCreateResponse>;
|
|
37
38
|
getWidgetSettings(projectId: string): Promise<{
|
|
38
39
|
projectId: string;
|
|
39
40
|
projectName: string;
|
|
@@ -60,16 +61,10 @@ export declare class FeedbackBasketClient {
|
|
|
60
61
|
content: string;
|
|
61
62
|
createdAt: string;
|
|
62
63
|
}>;
|
|
63
|
-
sendReply(feedbackId: string, content: string,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
replyToEmail: string;
|
|
68
|
-
sentBy: string;
|
|
69
|
-
createdAt: string;
|
|
70
|
-
};
|
|
71
|
-
sentTo: string;
|
|
72
|
-
}>;
|
|
64
|
+
sendReply(feedbackId: string, content: string, opts?: {
|
|
65
|
+
replyToEmail?: string;
|
|
66
|
+
destinations?: Array<'email' | 'widget'>;
|
|
67
|
+
}): Promise<FeedbackReplyResponse>;
|
|
73
68
|
listReplies(feedbackId: string): Promise<{
|
|
74
69
|
replies: Array<{
|
|
75
70
|
id: string;
|
|
@@ -78,6 +73,7 @@ export declare class FeedbackBasketClient {
|
|
|
78
73
|
sentBy: string;
|
|
79
74
|
createdAt: string;
|
|
80
75
|
}>;
|
|
76
|
+
messages?: NonNullable<FeedbackReplyResponse['message']>[];
|
|
81
77
|
total: number;
|
|
82
78
|
}>;
|
|
83
79
|
deleteFeedback(id: string): Promise<{
|
package/dist/src/client.js
CHANGED
|
@@ -50,6 +50,9 @@ export class FeedbackBasketClient {
|
|
|
50
50
|
async searchFeedback(query, opts = {}) {
|
|
51
51
|
return this.getFeedback({ search: query, ...opts });
|
|
52
52
|
}
|
|
53
|
+
async createFeedback(data) {
|
|
54
|
+
return this.request('POST', '/feedback', data);
|
|
55
|
+
}
|
|
53
56
|
// Widget
|
|
54
57
|
async getWidgetSettings(projectId) {
|
|
55
58
|
return this.request('GET', `/projects/${encodeURIComponent(projectId)}/widget`);
|
|
@@ -67,8 +70,11 @@ export class FeedbackBasketClient {
|
|
|
67
70
|
async createNote(feedbackId, content) {
|
|
68
71
|
return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/notes`, { content });
|
|
69
72
|
}
|
|
70
|
-
async sendReply(feedbackId, content,
|
|
73
|
+
async sendReply(feedbackId, content, opts = {}) {
|
|
71
74
|
const body = { content };
|
|
75
|
+
if (opts.destinations)
|
|
76
|
+
body.destinations = opts.destinations;
|
|
77
|
+
const replyToEmail = opts.replyToEmail;
|
|
72
78
|
if (replyToEmail)
|
|
73
79
|
body['replyToEmail'] = replyToEmail;
|
|
74
80
|
return this.request('POST', `/feedback/${encodeURIComponent(feedbackId)}/replies`, body);
|
|
@@ -0,0 +1,120 @@
|
|
|
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 } from '../output/theme.js';
|
|
7
|
+
import { resolveProject } from '../resolve.js';
|
|
8
|
+
const validTypes = new Set(['bug', 'feature', 'general']);
|
|
9
|
+
const validCategories = new Set(['BUG', 'FEATURE_REQUEST', 'IMPROVEMENT', 'QUESTION']);
|
|
10
|
+
const validStatuses = new Set(['OPEN', 'UNDER_REVIEW', 'PLANNED', 'IN_PROGRESS', 'COMPLETE', 'CLOSED']);
|
|
11
|
+
export function createFeedbackCreateCommand(getWriter) {
|
|
12
|
+
return new Command('create')
|
|
13
|
+
.argument('<title>', 'Short title or summary for the feedback')
|
|
14
|
+
.description('Create a new feedback item')
|
|
15
|
+
.requiredOption('--project <id-or-name>', 'Project ID or name')
|
|
16
|
+
.option('--content <body>', 'Longer feedback body')
|
|
17
|
+
.option('--type <type>', 'Feedback type (bug, feature, general)')
|
|
18
|
+
.option('--category <category>', 'Category (BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION)')
|
|
19
|
+
.option('--status <status>', 'Initial status (OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED)')
|
|
20
|
+
.option('--email <email>', 'Submitter email')
|
|
21
|
+
.option('--page-url <url>', 'Page URL where the feedback applies')
|
|
22
|
+
.option('--metadata <key=value>', 'Metadata key/value pair (repeatable)', collectMetadata, [])
|
|
23
|
+
.action(async (title, opts) => {
|
|
24
|
+
const writer = getWriter();
|
|
25
|
+
const client = requireClient();
|
|
26
|
+
if (opts.type && !validTypes.has(opts.type)) {
|
|
27
|
+
throw errUsage('Invalid type. Must be one of: bug, feature, general', 'Example: feedbackbasket feedback create "Login is broken" --project myapp --type bug');
|
|
28
|
+
}
|
|
29
|
+
if (opts.category && !validCategories.has(opts.category)) {
|
|
30
|
+
throw errUsage('Invalid category. Must be one of: BUG, FEATURE_REQUEST, IMPROVEMENT, QUESTION');
|
|
31
|
+
}
|
|
32
|
+
if (opts.status && !validStatuses.has(opts.status)) {
|
|
33
|
+
throw errUsage('Invalid status. Must be one of: OPEN, UNDER_REVIEW, PLANNED, IN_PROGRESS, COMPLETE, CLOSED');
|
|
34
|
+
}
|
|
35
|
+
const project = await resolveProject(client, opts.project);
|
|
36
|
+
const content = composeContent(title, opts.content);
|
|
37
|
+
const metadata = parseMetadata(opts.metadata ?? []);
|
|
38
|
+
const result = await client.createFeedback({
|
|
39
|
+
projectId: project.id,
|
|
40
|
+
content,
|
|
41
|
+
type: opts.type,
|
|
42
|
+
category: opts.category,
|
|
43
|
+
status: opts.status,
|
|
44
|
+
email: opts.email,
|
|
45
|
+
pageUrl: opts.pageUrl,
|
|
46
|
+
metadata,
|
|
47
|
+
});
|
|
48
|
+
if (!writer.isMachineOutput()) {
|
|
49
|
+
console.log(` ${brand.success('[OK]')} Feedback created: ${brand.bold(result.id)}`);
|
|
50
|
+
console.log(` ${brand.muted('Project:')} ${project.name}`);
|
|
51
|
+
console.log(` ${brand.muted('URL:')} ${result.url}`);
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
writer.ok(result, {
|
|
55
|
+
summary: `Created feedback ${result.id}`,
|
|
56
|
+
breadcrumbs: [
|
|
57
|
+
{ action: 'View feedback', cmd: `feedbackbasket feedback show ${result.id}` },
|
|
58
|
+
{ action: 'Update status', cmd: `feedbackbasket feedback update ${result.id} --status UNDER_REVIEW` },
|
|
59
|
+
{ action: 'List project feedback', cmd: `feedbackbasket feedback list --project ${project.id}` },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function requireClient() {
|
|
65
|
+
const manager = new AuthManager();
|
|
66
|
+
const token = manager.resolveToken();
|
|
67
|
+
if (!token)
|
|
68
|
+
throw errAuth();
|
|
69
|
+
const config = loadConfig();
|
|
70
|
+
return new FeedbackBasketClient(token, config.baseUrl);
|
|
71
|
+
}
|
|
72
|
+
function composeContent(title, body) {
|
|
73
|
+
const cleanTitle = title.trim();
|
|
74
|
+
const cleanBody = body?.trim();
|
|
75
|
+
const content = cleanBody ? `${cleanTitle}\n\n${cleanBody}` : cleanTitle;
|
|
76
|
+
if (!content) {
|
|
77
|
+
throw errUsage('Feedback title is required');
|
|
78
|
+
}
|
|
79
|
+
if (content.length > 2000) {
|
|
80
|
+
throw errUsage('Feedback content must be 2000 characters or less');
|
|
81
|
+
}
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
84
|
+
function collectMetadata(value, previous) {
|
|
85
|
+
return previous.concat(value);
|
|
86
|
+
}
|
|
87
|
+
function parseMetadata(entries) {
|
|
88
|
+
if (entries.length === 0)
|
|
89
|
+
return undefined;
|
|
90
|
+
const metadata = {};
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const separator = entry.indexOf('=');
|
|
93
|
+
if (separator <= 0) {
|
|
94
|
+
throw errUsage(`Invalid metadata "${entry}"`, 'Use --metadata key=value, for example --metadata source=codex');
|
|
95
|
+
}
|
|
96
|
+
const key = entry.slice(0, separator).trim();
|
|
97
|
+
const rawValue = entry.slice(separator + 1).trim();
|
|
98
|
+
if (!key) {
|
|
99
|
+
throw errUsage('Metadata keys cannot be empty');
|
|
100
|
+
}
|
|
101
|
+
metadata[key] = parseMetadataValue(rawValue);
|
|
102
|
+
}
|
|
103
|
+
return metadata;
|
|
104
|
+
}
|
|
105
|
+
function parseMetadataValue(value) {
|
|
106
|
+
if (value === 'true')
|
|
107
|
+
return true;
|
|
108
|
+
if (value === 'false')
|
|
109
|
+
return false;
|
|
110
|
+
if (value === 'null')
|
|
111
|
+
return null;
|
|
112
|
+
if (value !== '' && Number.isFinite(Number(value)))
|
|
113
|
+
return Number(value);
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(value);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -4,79 +4,84 @@ import { AuthManager } from '../auth/manager.js';
|
|
|
4
4
|
import { loadConfig } from '../config/config.js';
|
|
5
5
|
import { errAuth, errUsage } from '../output/errors.js';
|
|
6
6
|
import { brand } from '../output/theme.js';
|
|
7
|
-
import { ask
|
|
7
|
+
import { ask } from '../prompt.js';
|
|
8
|
+
const deliveryOptions = new Set(['email', 'widget', 'both']);
|
|
8
9
|
export function createFeedbackReplyCommand(getWriter) {
|
|
9
10
|
return new Command('reply')
|
|
10
11
|
.argument('<id>', 'Feedback ID to reply to')
|
|
11
12
|
.argument('[content]', 'Reply content (or use --content)')
|
|
12
|
-
.description('
|
|
13
|
+
.description('Reply to feedback by email, widget thread, or both')
|
|
13
14
|
.option('--content <text>', 'Reply content (alternative to positional argument)')
|
|
14
|
-
.option('--
|
|
15
|
+
.option('--delivery <delivery>', 'Reply delivery (email, widget, both)', 'email')
|
|
16
|
+
.option('--reply-to <email>', 'Reply-to email for email delivery')
|
|
15
17
|
.action(async (id, contentArg, opts) => {
|
|
16
18
|
const writer = getWriter();
|
|
17
19
|
const content = contentArg ?? opts.content;
|
|
20
|
+
const delivery = opts.delivery;
|
|
18
21
|
if (!content) {
|
|
19
|
-
throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!"');
|
|
22
|
+
throw errUsage('Reply content is required', 'Example: feedbackbasket feedback reply <id> "Thanks for reporting this!" --delivery widget');
|
|
23
|
+
}
|
|
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');
|
|
20
26
|
}
|
|
21
27
|
const client = requireClient();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
const feedback = await client.getFeedbackById(id);
|
|
29
|
+
const destinations = delivery === 'both'
|
|
30
|
+
? ['email', 'widget']
|
|
31
|
+
: [delivery];
|
|
32
|
+
const sendsEmail = destinations.includes('email');
|
|
33
|
+
const sendsWidget = destinations.includes('widget');
|
|
34
|
+
let replyTo = opts.replyTo ?? feedback.project.replyToEmail ?? undefined;
|
|
35
|
+
if (sendsEmail) {
|
|
26
36
|
if (!feedback.email) {
|
|
27
|
-
throw errUsage('This feedback has no email address
|
|
37
|
+
throw errUsage('This feedback has no email address; use --delivery widget if it has a widget thread.');
|
|
28
38
|
}
|
|
29
|
-
|
|
30
|
-
if (!projectReplyTo) {
|
|
39
|
+
if (!replyTo) {
|
|
31
40
|
const isInteractive = !writer.isMachineOutput() && process.stdin.isTTY;
|
|
32
|
-
if (isInteractive) {
|
|
33
|
-
|
|
34
|
-
const manager = new AuthManager();
|
|
35
|
-
const creds = manager.getCredentials();
|
|
36
|
-
const accountEmail = creds?.email;
|
|
37
|
-
console.log();
|
|
38
|
-
console.log(brand.warning(' No reply-to email configured for this project.'));
|
|
39
|
-
console.log(brand.muted(' The recipient will see this as the sender address.'));
|
|
40
|
-
console.log();
|
|
41
|
-
if (accountEmail) {
|
|
42
|
-
const useAccount = await confirm(` Use ${brand.bold(accountEmail)}?`);
|
|
43
|
-
if (useAccount) {
|
|
44
|
-
replyTo = accountEmail;
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
replyTo = await ask(' Enter reply-to email: ');
|
|
48
|
-
if (!replyTo || !replyTo.includes('@')) {
|
|
49
|
-
console.log(brand.muted(' Cancelled.'));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
replyTo = await ask(' Enter reply-to email: ');
|
|
56
|
-
if (!replyTo || !replyTo.includes('@')) {
|
|
57
|
-
console.log(brand.muted(' Cancelled.'));
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
console.log();
|
|
62
|
-
console.log(brand.muted(` Tip: Set a default with: feedbackbasket projects update ${feedback.project.name} --reply-to ${replyTo}`));
|
|
63
|
-
console.log();
|
|
41
|
+
if (!isInteractive) {
|
|
42
|
+
throw errUsage('No reply-to email configured for this project.', 'Ask the human which reply-to email to use, then pass --reply-to <email> or set a project default.');
|
|
64
43
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(brand.warning(' No reply-to email configured for this project.'));
|
|
46
|
+
console.log(brand.muted(' The recipient will see this as the sender address.'));
|
|
47
|
+
console.log();
|
|
48
|
+
replyTo = await ask(' Enter reply-to email: ');
|
|
49
|
+
if (!replyTo || !replyTo.includes('@')) {
|
|
50
|
+
console.log(brand.muted(' Cancelled.'));
|
|
51
|
+
return;
|
|
68
52
|
}
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(brand.muted(` Tip: Set a default with: feedbackbasket projects update ${feedback.project.name} --reply-to ${replyTo}`));
|
|
55
|
+
console.log();
|
|
69
56
|
}
|
|
70
57
|
}
|
|
71
|
-
|
|
58
|
+
if (sendsWidget && !feedback.hasWidgetAccess) {
|
|
59
|
+
throw errUsage('This feedback is not connected to a widget thread.', 'Use --delivery email for feedback with an email address, or ask the human how they want to respond.');
|
|
60
|
+
}
|
|
61
|
+
const result = await client.sendReply(id, content, {
|
|
62
|
+
replyToEmail: replyTo,
|
|
63
|
+
destinations,
|
|
64
|
+
});
|
|
72
65
|
if (!writer.isMachineOutput()) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
if (result.sentTo) {
|
|
67
|
+
console.log(` ${brand.success('[OK]')} Email reply sent to ${brand.bold(result.sentTo)}`);
|
|
68
|
+
if (result.reply) {
|
|
69
|
+
console.log(` ${brand.muted('From:')} ${result.reply.replyToEmail}`);
|
|
70
|
+
console.log(` ${brand.muted('By:')} ${result.reply.sentBy}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (result.message) {
|
|
74
|
+
console.log(` ${brand.success('[OK]')} Widget reply posted`);
|
|
75
|
+
console.log(` ${brand.muted('By:')} ${result.message.sentByName ?? 'CLI'}`);
|
|
76
|
+
}
|
|
76
77
|
console.log();
|
|
77
78
|
}
|
|
78
79
|
writer.ok(result, {
|
|
79
|
-
summary:
|
|
80
|
+
summary: delivery === 'both'
|
|
81
|
+
? 'Reply sent by email and widget'
|
|
82
|
+
: delivery === 'widget'
|
|
83
|
+
? 'Widget reply posted'
|
|
84
|
+
: `Reply sent to ${result.sentTo}`,
|
|
80
85
|
breadcrumbs: [
|
|
81
86
|
{ action: 'View replies', cmd: `feedbackbasket feedback replies ${id}` },
|
|
82
87
|
{ action: 'Update status', cmd: `feedbackbasket feedback update ${id} --status COMPLETE` },
|
|
@@ -93,8 +98,10 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
93
98
|
const writer = getWriter();
|
|
94
99
|
const client = requireClient();
|
|
95
100
|
const result = await client.listReplies(id);
|
|
101
|
+
const messages = result.messages ?? [];
|
|
102
|
+
const visibleWidgetMessages = messages.filter((item) => !item.replyId);
|
|
96
103
|
if (!writer.isMachineOutput()) {
|
|
97
|
-
if (result.
|
|
104
|
+
if (result.total === 0) {
|
|
98
105
|
console.log(brand.muted(' No replies sent yet.'));
|
|
99
106
|
console.log();
|
|
100
107
|
}
|
|
@@ -102,7 +109,8 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
102
109
|
console.log(brand.bold(`${result.total} repl${result.total === 1 ? 'y' : 'ies'} for feedback ${id}`));
|
|
103
110
|
console.log();
|
|
104
111
|
for (const r of result.replies) {
|
|
105
|
-
console.log(` ${brand.success('
|
|
112
|
+
console.log(` ${brand.success('->')} ${brand.bold(r.sentBy)} ${brand.muted(r.createdAt)}`);
|
|
113
|
+
console.log(` ${brand.muted('Delivery:')} email`);
|
|
106
114
|
console.log(` ${brand.muted('Reply-to:')} ${r.replyToEmail}`);
|
|
107
115
|
console.log();
|
|
108
116
|
for (const line of r.content.split('\n')) {
|
|
@@ -110,12 +118,22 @@ export function createFeedbackRepliesCommand(getWriter) {
|
|
|
110
118
|
}
|
|
111
119
|
console.log();
|
|
112
120
|
}
|
|
121
|
+
for (const message of visibleWidgetMessages) {
|
|
122
|
+
console.log(` ${brand.success('->')} ${brand.bold(message.sentByName ?? 'CLI')} ${brand.muted(message.createdAt)}`);
|
|
123
|
+
console.log(` ${brand.muted('Delivery:')} widget`);
|
|
124
|
+
console.log();
|
|
125
|
+
for (const line of message.content.split('\n')) {
|
|
126
|
+
console.log(` ${line}`);
|
|
127
|
+
}
|
|
128
|
+
console.log();
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
|
-
writer.ok(result.replies, {
|
|
132
|
+
writer.ok({ replies: result.replies, messages }, {
|
|
116
133
|
summary: `${result.total} repl${result.total === 1 ? 'y' : 'ies'}`,
|
|
117
134
|
breadcrumbs: [
|
|
118
|
-
{ action: 'Send
|
|
135
|
+
{ action: 'Send an email reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery email` },
|
|
136
|
+
{ action: 'Post a widget reply', cmd: `feedbackbasket feedback reply ${id} "<content>" --delivery widget` },
|
|
119
137
|
{ action: 'View feedback', cmd: `feedbackbasket feedback show ${id}` },
|
|
120
138
|
],
|
|
121
139
|
});
|
|
@@ -17,6 +17,7 @@ async function resolveProjectId(client, optProject, all) {
|
|
|
17
17
|
}
|
|
18
18
|
import { createFeedbackUpdateCommand } from './feedback-update.js';
|
|
19
19
|
import { createFeedbackNoteCommand } from './feedback-note.js';
|
|
20
|
+
import { createFeedbackCreateCommand } from './feedback-create.js';
|
|
20
21
|
import { createFeedbackDeleteCommand } from './feedback-delete.js';
|
|
21
22
|
import { createFeedbackBulkUpdateCommand } from './feedback-bulk-update.js';
|
|
22
23
|
import { createFeedbackExportCommand } from './feedback-export.js';
|
|
@@ -25,6 +26,7 @@ export function createFeedbackCommand(getWriter) {
|
|
|
25
26
|
const feedback = new Command('feedback')
|
|
26
27
|
.description('View and manage feedback');
|
|
27
28
|
// Write subcommands
|
|
29
|
+
feedback.addCommand(createFeedbackCreateCommand(getWriter));
|
|
28
30
|
feedback.addCommand(createFeedbackUpdateCommand(getWriter));
|
|
29
31
|
feedback.addCommand(createFeedbackNoteCommand(getWriter));
|
|
30
32
|
feedback.addCommand(createFeedbackReplyCommand(getWriter));
|
|
@@ -184,6 +186,7 @@ function renderFeedbackDetail(item) {
|
|
|
184
186
|
['OS', item.os],
|
|
185
187
|
['Device', item.device],
|
|
186
188
|
['Language', item.language],
|
|
189
|
+
['Widget Thread', item.hasWidgetAccess ? 'Yes' : null],
|
|
187
190
|
['Created', item.createdAt],
|
|
188
191
|
];
|
|
189
192
|
for (const [label, value] of fields) {
|
package/dist/src/help.js
CHANGED
|
@@ -57,6 +57,7 @@ export function renderRootHelp() {
|
|
|
57
57
|
// Examples
|
|
58
58
|
lines.push(section(' EXAMPLES'));
|
|
59
59
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket projects list`);
|
|
60
|
+
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket feedback create "Login bug" --project myapp --type bug`);
|
|
60
61
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket feedback list --category BUG --status OPEN`);
|
|
61
62
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket bugs list --severity high`);
|
|
62
63
|
lines.push(`${INDENT}${brand.muted('$')} feedbackbasket widget script myapp`);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -2,6 +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
6
|
export type FeedbackFlowQuestionType = 'text' | 'textarea' | 'single_choice';
|
|
6
7
|
export interface FeedbackFlowQuestion {
|
|
7
8
|
id: string;
|
|
@@ -84,6 +85,7 @@ export interface Feedback {
|
|
|
84
85
|
os?: string | null;
|
|
85
86
|
device?: string | null;
|
|
86
87
|
language?: string | null;
|
|
88
|
+
hasWidgetAccess?: boolean;
|
|
87
89
|
attachments?: FeedbackAttachment[];
|
|
88
90
|
project: {
|
|
89
91
|
id: string;
|
|
@@ -126,6 +128,40 @@ export interface FeedbackResponse {
|
|
|
126
128
|
feedback: Feedback[];
|
|
127
129
|
pagination: Pagination;
|
|
128
130
|
}
|
|
131
|
+
export interface FeedbackCreateInput {
|
|
132
|
+
projectId: string;
|
|
133
|
+
content: string;
|
|
134
|
+
type?: 'bug' | 'feature' | 'general';
|
|
135
|
+
category?: FeedbackCategory;
|
|
136
|
+
status?: FeedbackStatus;
|
|
137
|
+
email?: string;
|
|
138
|
+
pageUrl?: string;
|
|
139
|
+
metadata?: Record<string, unknown>;
|
|
140
|
+
}
|
|
141
|
+
export interface FeedbackCreateResponse {
|
|
142
|
+
id: string;
|
|
143
|
+
url: string;
|
|
144
|
+
feedback: Feedback;
|
|
145
|
+
}
|
|
146
|
+
export interface FeedbackReplyResponse {
|
|
147
|
+
reply: {
|
|
148
|
+
id: string;
|
|
149
|
+
content: string;
|
|
150
|
+
replyToEmail: string;
|
|
151
|
+
sentBy: string;
|
|
152
|
+
createdAt: string;
|
|
153
|
+
} | null;
|
|
154
|
+
message: {
|
|
155
|
+
id: string;
|
|
156
|
+
content: string;
|
|
157
|
+
delivery: 'WIDGET' | 'EMAIL' | 'BOTH';
|
|
158
|
+
sentByName: string | null;
|
|
159
|
+
replyId: string | null;
|
|
160
|
+
createdAt: string;
|
|
161
|
+
} | null;
|
|
162
|
+
sentTo: string | null;
|
|
163
|
+
destinations: Array<'email' | 'widget'>;
|
|
164
|
+
}
|
|
129
165
|
export interface BugReportsResponse {
|
|
130
166
|
bugReports: BugReport[];
|
|
131
167
|
stats: {
|
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.9.0";
|
|
2
|
+
export declare const USER_AGENT = "FeedbackBasket-CLI/0.9.0";
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.9.0';
|
|
2
2
|
export const USER_AGENT = `FeedbackBasket-CLI/${VERSION}`;
|
package/package.json
CHANGED
|
@@ -62,14 +62,17 @@ feedbackbasket feedback show <id>
|
|
|
62
62
|
feedbackbasket feedback search "crash on mobile" --project <id> --limit 10
|
|
63
63
|
|
|
64
64
|
# Write
|
|
65
|
+
feedbackbasket feedback create "Login button is broken" --content "Clicking Log in does nothing in Safari." --project <id> --type bug
|
|
66
|
+
feedbackbasket feedback create "Feature idea" --content "Let users export saved views." --project <id> --type feature --metadata source=agent
|
|
65
67
|
feedbackbasket feedback update <id> --status PLANNED --category BUG --sentiment NEGATIVE
|
|
66
68
|
feedbackbasket feedback note <id> "Investigating — appears related to auth flow"
|
|
67
69
|
feedbackbasket feedback delete <id> --yes
|
|
68
70
|
feedbackbasket feedback bulk-update --status CLOSED --ids id1,id2,id3
|
|
69
71
|
|
|
70
|
-
# Reply to submitter
|
|
71
|
-
feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!"
|
|
72
|
-
feedbackbasket feedback reply <id> "<content>" --
|
|
72
|
+
# Reply to submitter by email, widget thread, or both
|
|
73
|
+
feedbackbasket feedback reply <id> "Thanks for reporting — we pushed a fix!" --delivery email --reply-to support@example.com
|
|
74
|
+
feedbackbasket feedback reply <id> "<content>" --delivery widget
|
|
75
|
+
feedbackbasket feedback reply <id> "<content>" --delivery both --reply-to support@example.com
|
|
73
76
|
feedbackbasket feedback replies <id> # list past replies
|
|
74
77
|
|
|
75
78
|
# Export
|
|
@@ -143,6 +146,18 @@ feedbackbasket feedback update <id> --status UNDER_REVIEW --agent
|
|
|
143
146
|
feedbackbasket feedback note <id> "Reviewing — appears related to auth flow" --agent
|
|
144
147
|
```
|
|
145
148
|
|
|
149
|
+
### Capture new feedback without leaving the terminal
|
|
150
|
+
```bash
|
|
151
|
+
feedbackbasket feedback create "Login button is broken" \
|
|
152
|
+
--content "Clicking Log in does nothing in Safari." \
|
|
153
|
+
--project myapp \
|
|
154
|
+
--type bug \
|
|
155
|
+
--page-url https://example.com/login \
|
|
156
|
+
--metadata source=agent \
|
|
157
|
+
--agent
|
|
158
|
+
```
|
|
159
|
+
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.
|
|
160
|
+
|
|
146
161
|
### Investigate high-priority bugs
|
|
147
162
|
```bash
|
|
148
163
|
feedbackbasket bugs list --severity high --agent
|
|
@@ -152,18 +167,22 @@ feedbackbasket feedback show <id> --agent
|
|
|
152
167
|
|
|
153
168
|
### Close the loop — reply to the submitter
|
|
154
169
|
```bash
|
|
155
|
-
# Agent reads context,
|
|
156
|
-
feedbackbasket feedback show <id> --agent # read
|
|
157
|
-
feedbackbasket feedback reply <id> "<drafted response>" --agent
|
|
170
|
+
# Agent reads context, asks which delivery method to use, then sends it
|
|
171
|
+
feedbackbasket feedback show <id> --agent # read email, hasWidgetAccess, project.replyToEmail
|
|
172
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery widget --agent
|
|
173
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery email --reply-to support@example.com --agent
|
|
174
|
+
feedbackbasket feedback reply <id> "<drafted response>" --delivery both --reply-to support@example.com --agent
|
|
158
175
|
feedbackbasket feedback update <id> --status COMPLETE --agent
|
|
159
176
|
feedbackbasket feedback note <id> "Replied via CLI" --agent
|
|
160
177
|
```
|
|
161
|
-
**Important
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
**Important reply safety rules:**
|
|
179
|
+
- Before replying, the agent MUST inspect `feedback show --agent`, then ask the human which delivery method to use: `email`, `widget`, or `both`, unless the human already specified it in the current conversation.
|
|
180
|
+
- If `feedback show` returns `email: null`, do not use `--delivery email` or `--delivery both`. If `hasWidgetAccess: true`, use `--delivery widget`; otherwise ask the human how they want to respond.
|
|
181
|
+
- If `hasWidgetAccess: false`, do not use `--delivery widget` or `--delivery both`.
|
|
182
|
+
- 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.
|
|
183
|
+
- 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>`.
|
|
184
|
+
|
|
185
|
+
Never silently guess a reply-to address. It becomes the "From" address the customer sees.
|
|
167
186
|
|
|
168
187
|
### Export for analysis
|
|
169
188
|
```bash
|