feedbackbasket-cli 0.12.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +305 -0
- package/README.md +382 -336
- package/dist/src/auth/login.d.ts +4 -3
- package/dist/src/auth/login.js +70 -43
- package/dist/src/capabilities.d.ts +42 -0
- package/dist/src/capabilities.js +6 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/commands/auth.d.ts +9 -2
- package/dist/src/commands/auth.js +105 -93
- package/dist/src/commands/feedback-bulk-update.js +3 -0
- package/dist/src/commands/feedback-delete.js +2 -6
- package/dist/src/commands/feedback-note.js +34 -2
- package/dist/src/commands/feedback-reply.js +3 -0
- package/dist/src/commands/mobile.js +1 -0
- package/dist/src/commands/projects.js +2 -6
- package/dist/src/commands/team.js +4 -8
- package/dist/src/commands/widget.js +1 -0
- package/dist/src/confirmation.d.ts +2 -0
- package/dist/src/confirmation.js +11 -0
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +2 -1
- package/package.json +54 -49
- package/skills/feedbackbasket/SKILL.md +436 -390
package/dist/src/auth/login.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
interface LoginResult {
|
|
2
2
|
token: string;
|
|
3
|
-
scope:
|
|
3
|
+
scope: "read" | "full";
|
|
4
4
|
}
|
|
5
|
+
export declare function validateReturnedScope(value: string | null, maximum: "read" | "full"): "read" | "full";
|
|
5
6
|
export declare function isCliTokenFormat(token: string): boolean;
|
|
6
|
-
export declare function browserLogin(baseUrl: string, scope?:
|
|
7
|
-
export declare function manualLogin(baseUrl: string, scope?:
|
|
7
|
+
export declare function browserLogin(baseUrl: string, scope?: "read" | "full"): Promise<LoginResult>;
|
|
8
|
+
export declare function manualLogin(baseUrl: string, scope?: "read" | "full"): Promise<LoginResult>;
|
|
8
9
|
export {};
|
package/dist/src/auth/login.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { createInterface } from
|
|
2
|
-
import { stdin as input, stdout as output } from
|
|
3
|
-
import { createServer } from
|
|
4
|
-
import { randomUUID } from
|
|
5
|
-
import { URL } from
|
|
6
|
-
import open from
|
|
7
|
-
import { brand, logo } from
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { createServer, } from "node:http";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { URL } from "node:url";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { brand, logo } from "../output/theme.js";
|
|
8
|
+
export function validateReturnedScope(value, maximum) {
|
|
9
|
+
if (value !== "read" && value !== "full") {
|
|
10
|
+
throw new Error("The authorization response did not include a valid access level");
|
|
11
|
+
}
|
|
12
|
+
if (maximum === "read" && value === "full") {
|
|
13
|
+
throw new Error("The authorization response exceeds the requested access maximum");
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
8
17
|
const TIMEOUT_MS = 120_000;
|
|
9
18
|
const CLI_TOKEN_PATTERN = /^fb_cli_[a-f0-9]{64}$/;
|
|
10
19
|
export function isCliTokenFormat(token) {
|
|
@@ -38,30 +47,31 @@ const ERROR_HTML = (message) => `<!DOCTYPE html>
|
|
|
38
47
|
<h1>Authentication Failed</h1>
|
|
39
48
|
<p>${message}</p>
|
|
40
49
|
</div></body></html>`;
|
|
41
|
-
export async function browserLogin(baseUrl, scope =
|
|
50
|
+
export async function browserLogin(baseUrl, scope = "read") {
|
|
42
51
|
const state = randomUUID();
|
|
43
52
|
return new Promise((resolve, reject) => {
|
|
44
53
|
const server = createServer((req, res) => {
|
|
45
|
-
const url = new URL(req.url ??
|
|
46
|
-
if (url.pathname !==
|
|
54
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
55
|
+
if (url.pathname !== "/callback") {
|
|
47
56
|
res.writeHead(404);
|
|
48
|
-
res.end(
|
|
57
|
+
res.end("Not found");
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
|
-
const receivedState = url.searchParams.get(
|
|
52
|
-
const token = url.searchParams.get(
|
|
53
|
-
const
|
|
60
|
+
const receivedState = url.searchParams.get("state");
|
|
61
|
+
const token = url.searchParams.get("token");
|
|
62
|
+
const returnedScope = url.searchParams.get("scope");
|
|
63
|
+
const error = url.searchParams.get("error");
|
|
54
64
|
// CSRF validation
|
|
55
65
|
if (receivedState !== state) {
|
|
56
|
-
res.writeHead(400, {
|
|
57
|
-
res.end(ERROR_HTML(
|
|
66
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
67
|
+
res.end(ERROR_HTML("State mismatch — possible CSRF attack. Please try again."));
|
|
58
68
|
cleanup();
|
|
59
|
-
reject(new Error(
|
|
69
|
+
reject(new Error("State mismatch during authentication"));
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
// User denied
|
|
63
73
|
if (error) {
|
|
64
|
-
res.writeHead(200, {
|
|
74
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
65
75
|
res.end(ERROR_HTML(`Authorization was denied: ${error}`));
|
|
66
76
|
cleanup();
|
|
67
77
|
reject(new Error(`Authorization denied: ${error}`));
|
|
@@ -69,32 +79,44 @@ export async function browserLogin(baseUrl, scope = 'read') {
|
|
|
69
79
|
}
|
|
70
80
|
// Missing token
|
|
71
81
|
if (!token) {
|
|
72
|
-
res.writeHead(400, {
|
|
73
|
-
res.end(ERROR_HTML(
|
|
82
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
83
|
+
res.end(ERROR_HTML("No token received. Please try again."));
|
|
84
|
+
cleanup();
|
|
85
|
+
reject(new Error("No token received in callback"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let selectedScope;
|
|
89
|
+
try {
|
|
90
|
+
selectedScope = validateReturnedScope(returnedScope, scope);
|
|
91
|
+
}
|
|
92
|
+
catch (cause) {
|
|
93
|
+
const message = cause instanceof Error ? cause.message : "Invalid access level";
|
|
94
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
95
|
+
res.end(ERROR_HTML(message));
|
|
74
96
|
cleanup();
|
|
75
|
-
reject(new Error(
|
|
97
|
+
reject(new Error(message));
|
|
76
98
|
return;
|
|
77
99
|
}
|
|
78
100
|
// Success
|
|
79
|
-
res.writeHead(200, {
|
|
101
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
80
102
|
res.end(SUCCESS_HTML);
|
|
81
103
|
cleanup();
|
|
82
|
-
resolve({ token, scope });
|
|
104
|
+
resolve({ token, scope: selectedScope });
|
|
83
105
|
});
|
|
84
106
|
const timeout = setTimeout(() => {
|
|
85
107
|
cleanup();
|
|
86
|
-
reject(new Error(
|
|
108
|
+
reject(new Error("Authentication timed out after 2 minutes"));
|
|
87
109
|
}, TIMEOUT_MS);
|
|
88
110
|
function cleanup() {
|
|
89
111
|
clearTimeout(timeout);
|
|
90
112
|
server.close();
|
|
91
113
|
}
|
|
92
114
|
// Listen on random port on loopback
|
|
93
|
-
server.listen(0,
|
|
115
|
+
server.listen(0, "127.0.0.1", () => {
|
|
94
116
|
const addr = server.address();
|
|
95
|
-
if (!addr || typeof addr ===
|
|
117
|
+
if (!addr || typeof addr === "string") {
|
|
96
118
|
cleanup();
|
|
97
|
-
reject(new Error(
|
|
119
|
+
reject(new Error("Failed to start local auth server"));
|
|
98
120
|
return;
|
|
99
121
|
}
|
|
100
122
|
const port = addr.port;
|
|
@@ -102,14 +124,14 @@ export async function browserLogin(baseUrl, scope = 'read') {
|
|
|
102
124
|
console.log();
|
|
103
125
|
console.log(` ${logo()} CLI`);
|
|
104
126
|
console.log();
|
|
105
|
-
console.log(` ${brand.primary(
|
|
127
|
+
console.log(` ${brand.primary("Opening browser for authentication...")}`);
|
|
106
128
|
console.log();
|
|
107
|
-
console.log(` ${brand.muted(
|
|
129
|
+
console.log(` ${brand.muted("If the browser doesn't open, visit:")}`);
|
|
108
130
|
console.log(` ${brand.primary(authorizeUrl)}`);
|
|
109
131
|
console.log();
|
|
110
|
-
console.log(` ${brand.muted(
|
|
111
|
-
console.log(` ${brand.muted(
|
|
112
|
-
console.log(` ${brand.command(
|
|
132
|
+
console.log(` ${brand.muted("Waiting for authentication...")}`);
|
|
133
|
+
console.log(` ${brand.muted("If this machine cannot receive the localhost browser callback, use:")}`);
|
|
134
|
+
console.log(` ${brand.command("feedbackbasket login --manual")}`);
|
|
113
135
|
console.log();
|
|
114
136
|
open(authorizeUrl).catch(() => {
|
|
115
137
|
// Browser open failed; user will use the URL manually.
|
|
@@ -117,33 +139,38 @@ export async function browserLogin(baseUrl, scope = 'read') {
|
|
|
117
139
|
});
|
|
118
140
|
});
|
|
119
141
|
}
|
|
120
|
-
export async function manualLogin(baseUrl, scope =
|
|
142
|
+
export async function manualLogin(baseUrl, scope = "read") {
|
|
121
143
|
const authorizeUrl = `${baseUrl}/cli/authorize?mode=manual&scope=${scope}`;
|
|
122
144
|
console.log();
|
|
123
145
|
console.log(` ${logo()} CLI`);
|
|
124
146
|
console.log();
|
|
125
|
-
console.log(` ${brand.primary(
|
|
147
|
+
console.log(` ${brand.primary("Open this URL on any machine with a browser:")}`);
|
|
126
148
|
console.log();
|
|
127
149
|
console.log(` ${brand.primary(authorizeUrl)}`);
|
|
128
150
|
console.log();
|
|
129
|
-
console.log(` ${brand.muted(
|
|
130
|
-
console.log(` ${brand.muted(
|
|
151
|
+
console.log(` ${brand.muted("Use this when a remote server cannot receive the localhost browser callback.")}`);
|
|
152
|
+
console.log(` ${brand.muted("The CLI still needs outbound HTTPS access to verify and use the token.")}`);
|
|
131
153
|
console.log();
|
|
132
|
-
console.log(` ${brand.muted(
|
|
154
|
+
console.log(` ${brand.muted("After approving access, paste the token shown in your browser.")}`);
|
|
133
155
|
console.log();
|
|
134
156
|
open(authorizeUrl).catch(() => {
|
|
135
157
|
// Browser open failed; user will use the URL manually.
|
|
136
158
|
});
|
|
137
159
|
const rl = createInterface({ input, output });
|
|
138
160
|
try {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
161
|
+
const rawResult = (await rl.question(" Paste authorization result: ")).trim();
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(rawResult);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new Error("Expected the authorization result copied from the browser");
|
|
142
168
|
}
|
|
143
|
-
if (!isCliTokenFormat(token)) {
|
|
144
|
-
throw new Error(
|
|
169
|
+
if (typeof parsed.token !== "string" || !isCliTokenFormat(parsed.token)) {
|
|
170
|
+
throw new Error("Expected a FeedbackBasket CLI token beginning with fb_cli_");
|
|
145
171
|
}
|
|
146
|
-
|
|
172
|
+
const selectedScope = validateReturnedScope(typeof parsed.scope === "string" ? parsed.scope : null, scope);
|
|
173
|
+
return { token: parsed.token, scope: selectedScope };
|
|
147
174
|
}
|
|
148
175
|
finally {
|
|
149
176
|
rl.close();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ProductOperationId } from 'feedbackbasket-agent-contract';
|
|
2
|
+
export declare const CLI_CAPABILITIES: readonly {
|
|
3
|
+
operationId: ProductOperationId;
|
|
4
|
+
commands: readonly string[];
|
|
5
|
+
}[];
|
|
6
|
+
export declare const CLI_EXEMPTIONS: ({
|
|
7
|
+
readonly id: "browser-oauth";
|
|
8
|
+
readonly surface: "mcp-http";
|
|
9
|
+
readonly reason: "Browser OAuth is a Streamable HTTP transport capability; STDIO uses environment credentials and the CLI keeps its own login flow.";
|
|
10
|
+
} | {
|
|
11
|
+
readonly id: "auth";
|
|
12
|
+
readonly surface: "cli";
|
|
13
|
+
readonly reason: "Authentication is local CLI credential management, not a product operation.";
|
|
14
|
+
} | {
|
|
15
|
+
readonly id: "login";
|
|
16
|
+
readonly surface: "cli";
|
|
17
|
+
readonly reason: "The login shortcut starts the local CLI authentication flow.";
|
|
18
|
+
} | {
|
|
19
|
+
readonly id: "logout";
|
|
20
|
+
readonly surface: "cli";
|
|
21
|
+
readonly reason: "Logout removes a local CLI credential.";
|
|
22
|
+
} | {
|
|
23
|
+
readonly id: "doctor";
|
|
24
|
+
readonly surface: "cli";
|
|
25
|
+
readonly reason: "Doctor checks local CLI configuration and connectivity.";
|
|
26
|
+
} | {
|
|
27
|
+
readonly id: "setup";
|
|
28
|
+
readonly surface: "cli";
|
|
29
|
+
readonly reason: "Setup installs local agent guidance and does not change FeedbackBasket product data.";
|
|
30
|
+
} | {
|
|
31
|
+
readonly id: "output";
|
|
32
|
+
readonly surface: "cli";
|
|
33
|
+
readonly reason: "Output flags change terminal formatting only.";
|
|
34
|
+
} | {
|
|
35
|
+
readonly id: "initialize";
|
|
36
|
+
readonly surface: "mcp";
|
|
37
|
+
readonly reason: "MCP initialization is a transport protocol operation.";
|
|
38
|
+
} | {
|
|
39
|
+
readonly id: "resources";
|
|
40
|
+
readonly surface: "mcp";
|
|
41
|
+
readonly reason: "Public MCP resources describe the service and do not access customer product data.";
|
|
42
|
+
})[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PARITY_EXEMPTIONS, PRODUCT_OPERATIONS, } from 'feedbackbasket-agent-contract';
|
|
2
|
+
export const CLI_CAPABILITIES = PRODUCT_OPERATIONS.map((operation) => ({
|
|
3
|
+
operationId: operation.id,
|
|
4
|
+
commands: operation.cli.commands,
|
|
5
|
+
}));
|
|
6
|
+
export const CLI_EXEMPTIONS = PARITY_EXEMPTIONS.filter(({ surface }) => surface === 'cli');
|
package/dist/src/cli.d.ts
CHANGED
package/dist/src/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ function resolveFormat(opts) {
|
|
|
27
27
|
function getWriter() {
|
|
28
28
|
return writer;
|
|
29
29
|
}
|
|
30
|
-
export function
|
|
30
|
+
export function createProgram() {
|
|
31
31
|
const program = new Command('feedbackbasket')
|
|
32
32
|
.version(VERSION, '-v, --version')
|
|
33
33
|
.description('Command-line interface for FeedbackBasket')
|
|
@@ -71,6 +71,10 @@ export function run() {
|
|
|
71
71
|
program.addCommand(createSetupCommand(getWriter));
|
|
72
72
|
// Global error handler
|
|
73
73
|
program.exitOverride();
|
|
74
|
+
return program;
|
|
75
|
+
}
|
|
76
|
+
export function run() {
|
|
77
|
+
const program = createProgram();
|
|
74
78
|
(async () => {
|
|
75
79
|
try {
|
|
76
80
|
await program.parseAsync(process.argv);
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import type { OutputWriter } from
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { OutputWriter } from "../output/writer.js";
|
|
3
|
+
export declare function authLoginResult(email: string | undefined, scope: "read" | "full", defaultProject: string | undefined): {
|
|
4
|
+
authenticated: boolean;
|
|
5
|
+
email: string | undefined;
|
|
6
|
+
scope: "read" | "full";
|
|
7
|
+
defaultProject: string | undefined;
|
|
8
|
+
};
|
|
3
9
|
export declare function createAuthCommand(getWriter: () => OutputWriter): Command;
|
|
10
|
+
export declare function resolveAuthScope(value: string): "read" | "full";
|
|
4
11
|
export declare function createLoginCommand(getWriter: () => OutputWriter): Command;
|
|
5
12
|
export declare function createLogoutCommand(getWriter: () => OutputWriter): Command;
|