feedbackbasket-cli 0.3.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.
Files changed (63) hide show
  1. package/README.md +215 -0
  2. package/dist/bin/feedbackbasket.d.ts +2 -0
  3. package/dist/bin/feedbackbasket.js +3 -0
  4. package/dist/src/auth/login.d.ts +6 -0
  5. package/dist/src/auth/login.js +111 -0
  6. package/dist/src/auth/manager.d.ts +11 -0
  7. package/dist/src/auth/manager.js +38 -0
  8. package/dist/src/cli.d.ts +1 -0
  9. package/dist/src/cli.js +79 -0
  10. package/dist/src/client.d.ts +105 -0
  11. package/dist/src/client.js +133 -0
  12. package/dist/src/commands/auth.d.ts +5 -0
  13. package/dist/src/commands/auth.js +313 -0
  14. package/dist/src/commands/bugs.d.ts +3 -0
  15. package/dist/src/commands/bugs.js +120 -0
  16. package/dist/src/commands/doctor.d.ts +3 -0
  17. package/dist/src/commands/doctor.js +130 -0
  18. package/dist/src/commands/feedback-bulk-update.d.ts +3 -0
  19. package/dist/src/commands/feedback-bulk-update.js +39 -0
  20. package/dist/src/commands/feedback-delete.d.ts +3 -0
  21. package/dist/src/commands/feedback-delete.js +45 -0
  22. package/dist/src/commands/feedback-export.d.ts +3 -0
  23. package/dist/src/commands/feedback-export.js +43 -0
  24. package/dist/src/commands/feedback-note.d.ts +3 -0
  25. package/dist/src/commands/feedback-note.js +41 -0
  26. package/dist/src/commands/feedback-update.d.ts +3 -0
  27. package/dist/src/commands/feedback-update.js +54 -0
  28. package/dist/src/commands/feedback.d.ts +3 -0
  29. package/dist/src/commands/feedback.js +201 -0
  30. package/dist/src/commands/projects.d.ts +3 -0
  31. package/dist/src/commands/projects.js +218 -0
  32. package/dist/src/commands/setup.d.ts +3 -0
  33. package/dist/src/commands/setup.js +90 -0
  34. package/dist/src/commands/team.d.ts +3 -0
  35. package/dist/src/commands/team.js +112 -0
  36. package/dist/src/commands/widget.d.ts +3 -0
  37. package/dist/src/commands/widget.js +153 -0
  38. package/dist/src/config/config.d.ts +10 -0
  39. package/dist/src/config/config.js +41 -0
  40. package/dist/src/config/credentials.d.ts +12 -0
  41. package/dist/src/config/credentials.js +39 -0
  42. package/dist/src/output/codes.d.ts +16 -0
  43. package/dist/src/output/codes.js +29 -0
  44. package/dist/src/output/envelope.d.ts +24 -0
  45. package/dist/src/output/envelope.js +2 -0
  46. package/dist/src/output/errors.d.ts +13 -0
  47. package/dist/src/output/errors.js +37 -0
  48. package/dist/src/output/styled.d.ts +4 -0
  49. package/dist/src/output/styled.js +78 -0
  50. package/dist/src/output/theme.d.ts +25 -0
  51. package/dist/src/output/theme.js +43 -0
  52. package/dist/src/output/writer.d.ts +23 -0
  53. package/dist/src/output/writer.js +109 -0
  54. package/dist/src/prompt.d.ts +3 -0
  55. package/dist/src/prompt.js +32 -0
  56. package/dist/src/resolve.d.ts +9 -0
  57. package/dist/src/resolve.js +70 -0
  58. package/dist/src/types.d.ts +100 -0
  59. package/dist/src/types.js +2 -0
  60. package/dist/src/version.d.ts +2 -0
  61. package/dist/src/version.js +2 -0
  62. package/package.json +49 -0
  63. package/skills/feedbackbasket/SKILL.md +132 -0
@@ -0,0 +1,39 @@
1
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { configDir, ensureConfigDir } from './config.js';
4
+ function credentialsPath() {
5
+ return join(configDir(), 'credentials.json');
6
+ }
7
+ export function loadCredentials() {
8
+ // Environment variable takes precedence (like Basecamp's BASECAMP_TOKEN)
9
+ const envToken = process.env['FEEDBACKBASKET_TOKEN'];
10
+ if (envToken) {
11
+ return {
12
+ token: envToken,
13
+ scope: 'full',
14
+ createdAt: new Date().toISOString(),
15
+ };
16
+ }
17
+ try {
18
+ const raw = readFileSync(credentialsPath(), 'utf-8');
19
+ return JSON.parse(raw);
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function saveCredentials(creds) {
26
+ ensureConfigDir();
27
+ writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
28
+ }
29
+ export function clearCredentials() {
30
+ const path = credentialsPath();
31
+ if (existsSync(path)) {
32
+ unlinkSync(path);
33
+ }
34
+ }
35
+ export function maskToken(token) {
36
+ if (token.length <= 20)
37
+ return token.slice(0, 8) + '...';
38
+ return token.slice(0, 14) + '...' + token.slice(-4);
39
+ }
@@ -0,0 +1,16 @@
1
+ export declare const ExitOK = 0;
2
+ export declare const ExitUsage = 1;
3
+ export declare const ExitNotFound = 2;
4
+ export declare const ExitAuth = 3;
5
+ export declare const ExitForbidden = 4;
6
+ export declare const ExitRateLimit = 5;
7
+ export declare const ExitNetwork = 6;
8
+ export declare const ExitAPI = 7;
9
+ export declare const CodeUsage = "usage_error";
10
+ export declare const CodeNotFound = "not_found";
11
+ export declare const CodeAuth = "auth_error";
12
+ export declare const CodeForbidden = "forbidden";
13
+ export declare const CodeRateLimit = "rate_limit";
14
+ export declare const CodeNetwork = "network_error";
15
+ export declare const CodeAPI = "api_error";
16
+ export declare function exitCodeFor(code: string): number;
@@ -0,0 +1,29 @@
1
+ // Exit codes — matching Basecamp CLI conventions
2
+ export const ExitOK = 0;
3
+ export const ExitUsage = 1;
4
+ export const ExitNotFound = 2;
5
+ export const ExitAuth = 3;
6
+ export const ExitForbidden = 4;
7
+ export const ExitRateLimit = 5;
8
+ export const ExitNetwork = 6;
9
+ export const ExitAPI = 7;
10
+ // Error codes (string identifiers for JSON envelope)
11
+ export const CodeUsage = 'usage_error';
12
+ export const CodeNotFound = 'not_found';
13
+ export const CodeAuth = 'auth_error';
14
+ export const CodeForbidden = 'forbidden';
15
+ export const CodeRateLimit = 'rate_limit';
16
+ export const CodeNetwork = 'network_error';
17
+ export const CodeAPI = 'api_error';
18
+ const codeToExit = {
19
+ [CodeUsage]: ExitUsage,
20
+ [CodeNotFound]: ExitNotFound,
21
+ [CodeAuth]: ExitAuth,
22
+ [CodeForbidden]: ExitForbidden,
23
+ [CodeRateLimit]: ExitRateLimit,
24
+ [CodeNetwork]: ExitNetwork,
25
+ [CodeAPI]: ExitAPI,
26
+ };
27
+ export function exitCodeFor(code) {
28
+ return codeToExit[code] ?? ExitAPI;
29
+ }
@@ -0,0 +1,24 @@
1
+ export interface Breadcrumb {
2
+ action: string;
3
+ cmd: string;
4
+ description?: string;
5
+ }
6
+ export interface SuccessResponse<T = unknown> {
7
+ ok: true;
8
+ data: T;
9
+ summary?: string;
10
+ notice?: string;
11
+ breadcrumbs?: Breadcrumb[];
12
+ }
13
+ export interface ErrorResponse {
14
+ ok: false;
15
+ error: string;
16
+ code: string;
17
+ hint?: string;
18
+ }
19
+ export type Response<T = unknown> = SuccessResponse<T> | ErrorResponse;
20
+ export interface ResponseOptions {
21
+ summary?: string;
22
+ notice?: string;
23
+ breadcrumbs?: Breadcrumb[];
24
+ }
@@ -0,0 +1,2 @@
1
+ // JSON envelope types — adapted from Basecamp CLI's Response struct
2
+ export {};
@@ -0,0 +1,13 @@
1
+ export declare class CLIError extends Error {
2
+ readonly code: string;
3
+ readonly hint?: string;
4
+ readonly exitCode: number;
5
+ constructor(code: string, message: string, hint?: string);
6
+ }
7
+ export declare function errAuth(message?: string): CLIError;
8
+ export declare function errNotFound(resource: string, id: string): CLIError;
9
+ export declare function errForbidden(message?: string): CLIError;
10
+ export declare function errRateLimit(): CLIError;
11
+ export declare function errNetwork(cause?: Error): CLIError;
12
+ export declare function errAPI(status: number, message: string): CLIError;
13
+ export declare function errUsage(message: string, hint?: string): CLIError;
@@ -0,0 +1,37 @@
1
+ import { CodeAuth, CodeNotFound, CodeForbidden, CodeRateLimit, CodeNetwork, CodeAPI, CodeUsage, exitCodeFor, } from './codes.js';
2
+ export class CLIError extends Error {
3
+ code;
4
+ hint;
5
+ exitCode;
6
+ constructor(code, message, hint) {
7
+ super(message);
8
+ this.name = 'CLIError';
9
+ this.code = code;
10
+ this.hint = hint;
11
+ this.exitCode = exitCodeFor(code);
12
+ }
13
+ }
14
+ export function errAuth(message = 'Not authenticated') {
15
+ return new CLIError(CodeAuth, message, 'Run: feedbackbasket auth login');
16
+ }
17
+ export function errNotFound(resource, id) {
18
+ return new CLIError(CodeNotFound, `${resource} "${id}" not found`);
19
+ }
20
+ export function errForbidden(message = 'Access denied') {
21
+ return new CLIError(CodeForbidden, message, 'This action requires --scope full');
22
+ }
23
+ export function errRateLimit() {
24
+ return new CLIError(CodeRateLimit, 'Rate limit exceeded', 'Wait a moment and try again');
25
+ }
26
+ export function errNetwork(cause) {
27
+ const message = cause?.message
28
+ ? `Network error: ${cause.message}`
29
+ : 'Could not connect to FeedbackBasket';
30
+ return new CLIError(CodeNetwork, message, 'Check your internet connection and try again');
31
+ }
32
+ export function errAPI(status, message) {
33
+ return new CLIError(CodeAPI, `API error (${status}): ${message}`);
34
+ }
35
+ export function errUsage(message, hint) {
36
+ return new CLIError(CodeUsage, message, hint);
37
+ }
@@ -0,0 +1,4 @@
1
+ import type { ResponseOptions } from './envelope.js';
2
+ import type { CLIError } from './errors.js';
3
+ export declare function renderStyledResponse(data: unknown, opts: ResponseOptions): void;
4
+ export declare function renderStyledError(error: CLIError): void;
@@ -0,0 +1,78 @@
1
+ import { brand, divider } from './theme.js';
2
+ export function renderStyledResponse(data, opts) {
3
+ if (opts.summary) {
4
+ console.log(brand.primaryBold(opts.summary));
5
+ console.log();
6
+ }
7
+ if (Array.isArray(data)) {
8
+ renderArray(data);
9
+ }
10
+ else if (data && typeof data === 'object') {
11
+ renderObject(data);
12
+ }
13
+ else {
14
+ console.log(String(data));
15
+ }
16
+ if (opts.notice) {
17
+ console.log();
18
+ console.log(brand.warning(opts.notice));
19
+ }
20
+ if (opts.breadcrumbs && opts.breadcrumbs.length > 0) {
21
+ renderBreadcrumbs(opts.breadcrumbs);
22
+ }
23
+ }
24
+ export function renderStyledError(error) {
25
+ console.error(brand.error.bold(`Error: ${error.message}`));
26
+ if (error.hint) {
27
+ console.error(brand.muted(` Hint: ${error.hint}`));
28
+ }
29
+ }
30
+ function renderBreadcrumbs(breadcrumbs) {
31
+ console.log();
32
+ console.log(divider());
33
+ console.log(brand.bold('Hints:'));
34
+ for (const bc of breadcrumbs) {
35
+ const desc = bc.description ? brand.muted(` — ${bc.description}`) : '';
36
+ console.log(` ${brand.command(bc.cmd)}${desc}`);
37
+ }
38
+ }
39
+ function renderArray(items) {
40
+ if (items.length === 0) {
41
+ console.log(brand.muted(' No results'));
42
+ return;
43
+ }
44
+ for (const item of items) {
45
+ if (item && typeof item === 'object') {
46
+ renderObject(item);
47
+ console.log();
48
+ }
49
+ else {
50
+ console.log(` ${String(item)}`);
51
+ }
52
+ }
53
+ }
54
+ function renderObject(obj) {
55
+ const maxKeyLen = Math.max(...Object.keys(obj).map(k => k.length));
56
+ for (const [key, value] of Object.entries(obj)) {
57
+ if (value === null || value === undefined)
58
+ continue;
59
+ const label = brand.label(key.padEnd(maxKeyLen));
60
+ if (Array.isArray(value)) {
61
+ if (value.length === 0) {
62
+ console.log(` ${label} ${brand.muted('(none)')}`);
63
+ }
64
+ else if (typeof value[0] === 'object') {
65
+ console.log(` ${label} ${brand.muted(`(${value.length} items)`)}`);
66
+ }
67
+ else {
68
+ console.log(` ${label} ${value.join(', ')}`);
69
+ }
70
+ }
71
+ else if (typeof value === 'object') {
72
+ console.log(` ${label} ${JSON.stringify(value)}`);
73
+ }
74
+ else {
75
+ console.log(` ${label} ${String(value)}`);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,25 @@
1
+ export declare const brand: {
2
+ primary: import("chalk").ChalkInstance;
3
+ primaryBold: import("chalk").ChalkInstance;
4
+ bold: import("chalk").ChalkInstance;
5
+ muted: import("chalk").ChalkInstance;
6
+ dim: import("chalk").ChalkInstance;
7
+ success: import("chalk").ChalkInstance;
8
+ error: import("chalk").ChalkInstance;
9
+ warning: import("chalk").ChalkInstance;
10
+ info: import("chalk").ChalkInstance;
11
+ label: import("chalk").ChalkInstance;
12
+ value: import("chalk").ChalkInstance;
13
+ hint: import("chalk").ChalkInstance;
14
+ divider: import("chalk").ChalkInstance;
15
+ command: import("chalk").ChalkInstance;
16
+ bug: import("chalk").ChalkInstance;
17
+ feature: import("chalk").ChalkInstance;
18
+ improvement: import("chalk").ChalkInstance;
19
+ question: import("chalk").ChalkInstance;
20
+ high: import("chalk").ChalkInstance;
21
+ medium: import("chalk").ChalkInstance;
22
+ low: import("chalk").ChalkInstance;
23
+ };
24
+ export declare function divider(width?: number): string;
25
+ export declare function logo(): string;
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+ // FeedbackBasket brand colors
3
+ // Primary: green (#22c55e) — used for accents, commands, success
4
+ // Foreground: white/gray — standard text
5
+ // Error: red — errors, bugs, high severity
6
+ // Warning: amber/yellow — notices, medium severity
7
+ // Muted: gray — secondary text, dividers
8
+ export const brand = {
9
+ // Primary accent — use instead of cyan
10
+ primary: chalk.hex('#22c55e'),
11
+ primaryBold: chalk.hex('#22c55e').bold,
12
+ // Text
13
+ bold: chalk.bold,
14
+ muted: chalk.gray,
15
+ dim: chalk.dim,
16
+ // Status
17
+ success: chalk.hex('#22c55e'),
18
+ error: chalk.red,
19
+ warning: chalk.yellow,
20
+ info: chalk.blue,
21
+ // Semantic
22
+ label: chalk.bold,
23
+ value: chalk.white,
24
+ hint: chalk.gray.italic,
25
+ divider: chalk.gray,
26
+ command: chalk.hex('#22c55e'),
27
+ // Category badges
28
+ bug: chalk.red,
29
+ feature: chalk.hex('#22c55e'),
30
+ improvement: chalk.hex('#06b6d4'),
31
+ question: chalk.yellow,
32
+ // Severity
33
+ high: chalk.red,
34
+ medium: chalk.yellow,
35
+ low: chalk.hex('#22c55e'),
36
+ };
37
+ // Box drawing
38
+ export function divider(width = 50) {
39
+ return brand.muted('─'.repeat(width));
40
+ }
41
+ export function logo() {
42
+ return `${brand.primaryBold('Feedback')}${brand.bold('Basket')}`;
43
+ }
@@ -0,0 +1,23 @@
1
+ import type { ResponseOptions } from './envelope.js';
2
+ import { CLIError } from './errors.js';
3
+ export declare enum Format {
4
+ Auto = "auto",
5
+ JSON = "json",
6
+ Quiet = "quiet",
7
+ Styled = "styled",
8
+ Markdown = "markdown"
9
+ }
10
+ export interface WriterOptions {
11
+ format: Format;
12
+ }
13
+ export declare class OutputWriter {
14
+ private format;
15
+ constructor(opts: WriterOptions);
16
+ effectiveFormat(): Format;
17
+ isMachineOutput(): boolean;
18
+ ok<T>(data: T, opts?: ResponseOptions): void;
19
+ err(error: CLIError): void;
20
+ private renderJSON;
21
+ private renderQuiet;
22
+ private renderMarkdown;
23
+ }
@@ -0,0 +1,109 @@
1
+ import { renderStyledResponse, renderStyledError } from './styled.js';
2
+ export var Format;
3
+ (function (Format) {
4
+ Format["Auto"] = "auto";
5
+ Format["JSON"] = "json";
6
+ Format["Quiet"] = "quiet";
7
+ Format["Styled"] = "styled";
8
+ Format["Markdown"] = "markdown";
9
+ })(Format || (Format = {}));
10
+ export class OutputWriter {
11
+ format;
12
+ constructor(opts) {
13
+ this.format = opts.format;
14
+ }
15
+ effectiveFormat() {
16
+ if (this.format !== Format.Auto)
17
+ return this.format;
18
+ return process.stdout.isTTY ? Format.Styled : Format.JSON;
19
+ }
20
+ isMachineOutput() {
21
+ const f = this.effectiveFormat();
22
+ return f === Format.JSON || f === Format.Quiet;
23
+ }
24
+ ok(data, opts = {}) {
25
+ const format = this.effectiveFormat();
26
+ switch (format) {
27
+ case Format.JSON:
28
+ this.renderJSON(data, opts);
29
+ break;
30
+ case Format.Quiet:
31
+ this.renderQuiet(data);
32
+ break;
33
+ case Format.Markdown:
34
+ this.renderMarkdown(data, opts);
35
+ break;
36
+ case Format.Styled:
37
+ default:
38
+ renderStyledResponse(data, opts);
39
+ break;
40
+ }
41
+ }
42
+ err(error) {
43
+ const format = this.effectiveFormat();
44
+ if (format === Format.JSON || format === Format.Quiet) {
45
+ const envelope = {
46
+ ok: false,
47
+ error: error.message,
48
+ code: error.code,
49
+ hint: error.hint,
50
+ };
51
+ console.error(JSON.stringify(envelope));
52
+ }
53
+ else {
54
+ renderStyledError(error);
55
+ }
56
+ }
57
+ renderJSON(data, opts) {
58
+ const envelope = {
59
+ ok: true,
60
+ data,
61
+ summary: opts.summary,
62
+ notice: opts.notice,
63
+ breadcrumbs: opts.breadcrumbs,
64
+ };
65
+ console.log(JSON.stringify(envelope, null, 2));
66
+ }
67
+ renderQuiet(data) {
68
+ console.log(JSON.stringify(data, null, 2));
69
+ }
70
+ renderMarkdown(data, opts) {
71
+ if (opts.summary) {
72
+ console.log(`## ${opts.summary}\n`);
73
+ }
74
+ if (Array.isArray(data)) {
75
+ for (const item of data) {
76
+ if (item && typeof item === 'object') {
77
+ for (const [key, value] of Object.entries(item)) {
78
+ if (value !== null && value !== undefined) {
79
+ console.log(`- **${key}**: ${String(value)}`);
80
+ }
81
+ }
82
+ console.log();
83
+ }
84
+ else {
85
+ console.log(`- ${String(item)}`);
86
+ }
87
+ }
88
+ }
89
+ else if (data && typeof data === 'object') {
90
+ for (const [key, value] of Object.entries(data)) {
91
+ if (value !== null && value !== undefined) {
92
+ console.log(`- **${key}**: ${String(value)}`);
93
+ }
94
+ }
95
+ }
96
+ else {
97
+ console.log(String(data));
98
+ }
99
+ if (opts.notice) {
100
+ console.log(`\n> ${opts.notice}`);
101
+ }
102
+ if (opts.breadcrumbs && opts.breadcrumbs.length > 0) {
103
+ console.log('\n### Hints\n');
104
+ for (const bc of opts.breadcrumbs) {
105
+ console.log(`- \`${bc.cmd}\` — ${bc.action}`);
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,3 @@
1
+ export declare function ask(question: string): Promise<string>;
2
+ export declare function confirm(question: string, defaultYes?: boolean): Promise<boolean>;
3
+ export declare function select(question: string, options: string[]): Promise<number | null>;
@@ -0,0 +1,32 @@
1
+ import { createInterface } from 'node:readline';
2
+ const rl = () => createInterface({ input: process.stdin, output: process.stdout });
3
+ export function ask(question) {
4
+ return new Promise((resolve) => {
5
+ const iface = rl();
6
+ iface.question(question, (answer) => {
7
+ iface.close();
8
+ resolve(answer.trim());
9
+ });
10
+ });
11
+ }
12
+ export async function confirm(question, defaultYes = true) {
13
+ const hint = defaultYes ? 'Y/n' : 'y/N';
14
+ const answer = await ask(`${question} (${hint}): `);
15
+ if (answer === '')
16
+ return defaultYes;
17
+ return answer.toLowerCase().startsWith('y');
18
+ }
19
+ export async function select(question, options) {
20
+ for (let i = 0; i < options.length; i++) {
21
+ console.log(` ${i + 1}. ${options[i]}`);
22
+ }
23
+ console.log();
24
+ const answer = await ask(` ${question} (1-${options.length}, or skip): `);
25
+ if (answer === '' || answer.toLowerCase() === 'skip' || answer.toLowerCase() === 's') {
26
+ return null;
27
+ }
28
+ const num = parseInt(answer, 10);
29
+ if (isNaN(num) || num < 1 || num > options.length)
30
+ return null;
31
+ return num - 1;
32
+ }
@@ -0,0 +1,9 @@
1
+ import { FeedbackBasketClient } from './client.js';
2
+ import type { Project } from './types.js';
3
+ /**
4
+ * Resolve a project by ID or name.
5
+ * - If input looks like a cuid, treat as ID
6
+ * - Otherwise, fetch all projects and match by name (case-insensitive)
7
+ * - Supports partial matching (e.g. "feedback" matches "feedbackbasket")
8
+ */
9
+ export declare function resolveProject(client: FeedbackBasketClient, input: string): Promise<Project>;
@@ -0,0 +1,70 @@
1
+ import { errNotFound, errUsage } from './output/errors.js';
2
+ // CUIDs look like: cmn3c7sgv000004jx16lhl07o (25 chars, starts with c, alphanumeric)
3
+ function looksLikeId(input) {
4
+ return /^c[a-z0-9]{20,}$/.test(input);
5
+ }
6
+ /**
7
+ * Resolve a project by ID or name.
8
+ * - If input looks like a cuid, treat as ID
9
+ * - Otherwise, fetch all projects and match by name (case-insensitive)
10
+ * - Supports partial matching (e.g. "feedback" matches "feedbackbasket")
11
+ */
12
+ export async function resolveProject(client, input) {
13
+ // Direct ID lookup
14
+ if (looksLikeId(input)) {
15
+ return client.getProject(input);
16
+ }
17
+ // Name-based lookup — fetch all projects and match
18
+ const result = await client.listProjects();
19
+ const projects = result.projects;
20
+ if (projects.length === 0) {
21
+ throw errNotFound('project', input);
22
+ }
23
+ const lower = input.toLowerCase();
24
+ // 1. Exact match (case-insensitive)
25
+ const exact = projects.filter(p => p.name.toLowerCase() === lower);
26
+ if (exact.length === 1)
27
+ return exact[0];
28
+ // 2. Starts-with match
29
+ const startsWith = projects.filter(p => p.name.toLowerCase().startsWith(lower));
30
+ if (startsWith.length === 1)
31
+ return startsWith[0];
32
+ // 3. Contains match
33
+ const contains = projects.filter(p => p.name.toLowerCase().includes(lower));
34
+ if (contains.length === 1)
35
+ return contains[0];
36
+ // Ambiguous — multiple matches
37
+ if (contains.length > 1) {
38
+ const names = contains.map(p => ` - ${p.name} (${p.id})`).join('\n');
39
+ throw errUsage(`Ambiguous project name "${input}" — matches ${contains.length} projects:\n${names}`, `Use the full project ID or a more specific name`);
40
+ }
41
+ // Not found — suggest similar names
42
+ const suggestions = projects
43
+ .map(p => ({ name: p.name, dist: levenshtein(lower, p.name.toLowerCase()) }))
44
+ .sort((a, b) => a.dist - b.dist)
45
+ .slice(0, 3)
46
+ .filter(s => s.dist <= Math.max(input.length * 0.6, 3))
47
+ .map(s => s.name);
48
+ if (suggestions.length > 0) {
49
+ throw errUsage(`Project "${input}" not found. Did you mean: ${suggestions.join(', ')}?`, `Use feedbackbasket projects list to see all projects`);
50
+ }
51
+ throw errNotFound('project', input);
52
+ }
53
+ // Simple Levenshtein distance for typo suggestions
54
+ function levenshtein(a, b) {
55
+ const m = a.length;
56
+ const n = b.length;
57
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
58
+ for (let i = 0; i <= m; i++)
59
+ dp[i][0] = i;
60
+ for (let j = 0; j <= n; j++)
61
+ dp[0][j] = j;
62
+ for (let i = 1; i <= m; i++) {
63
+ for (let j = 1; j <= n; j++) {
64
+ dp[i][j] = a[i - 1] === b[j - 1]
65
+ ? dp[i - 1][j - 1]
66
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
67
+ }
68
+ }
69
+ return dp[m][n];
70
+ }
@@ -0,0 +1,100 @@
1
+ export type FeedbackStatus = 'OPEN' | 'UNDER_REVIEW' | 'PLANNED' | 'IN_PROGRESS' | 'COMPLETE' | 'CLOSED';
2
+ export type FeedbackCategory = 'BUG' | 'FEATURE_REQUEST' | 'IMPROVEMENT' | 'QUESTION';
3
+ export type Sentiment = 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
4
+ export type Severity = 'high' | 'medium' | 'low';
5
+ export interface Project {
6
+ id: string;
7
+ name: string;
8
+ url: string;
9
+ description?: string;
10
+ createdAt: string;
11
+ totalFeedback: number;
12
+ byStatus: Record<string, number>;
13
+ byCategory: Record<string, number>;
14
+ }
15
+ export interface Feedback {
16
+ id: string;
17
+ content: string;
18
+ email?: string | null;
19
+ status: FeedbackStatus;
20
+ category?: FeedbackCategory | null;
21
+ sentiment?: Sentiment | null;
22
+ aiSummary?: string | null;
23
+ aiPriorityScore?: number | null;
24
+ reasoning?: string | null;
25
+ pageUrl?: string | null;
26
+ browser?: string | null;
27
+ os?: string | null;
28
+ device?: string | null;
29
+ language?: string | null;
30
+ project: {
31
+ id: string;
32
+ name: string;
33
+ };
34
+ notes?: FeedbackNote[];
35
+ createdAt: string;
36
+ }
37
+ export interface FeedbackNote {
38
+ id: string;
39
+ content: string;
40
+ createdAt: string;
41
+ author: {
42
+ name: string;
43
+ };
44
+ }
45
+ export interface BugReport extends Feedback {
46
+ severity: Severity;
47
+ }
48
+ export interface ProjectsResponse {
49
+ projects: Project[];
50
+ totalProjects: number;
51
+ }
52
+ export interface Pagination {
53
+ totalCount: number;
54
+ limit: number;
55
+ offset: number;
56
+ hasMore: boolean;
57
+ }
58
+ export interface FeedbackResponse {
59
+ feedback: Feedback[];
60
+ pagination: Pagination;
61
+ }
62
+ export interface BugReportsResponse {
63
+ bugReports: BugReport[];
64
+ stats: {
65
+ total: number;
66
+ bySeverity: {
67
+ high: number;
68
+ medium: number;
69
+ low: number;
70
+ };
71
+ byStatus: Record<string, number>;
72
+ };
73
+ pagination: Pagination;
74
+ }
75
+ export interface FeedbackParams {
76
+ projectId?: string;
77
+ category?: FeedbackCategory;
78
+ status?: FeedbackStatus;
79
+ sentiment?: Sentiment;
80
+ search?: string;
81
+ limit?: number;
82
+ offset?: number;
83
+ includeNotes?: boolean;
84
+ }
85
+ export interface BugReportParams {
86
+ projectId?: string;
87
+ status?: FeedbackStatus;
88
+ severity?: Severity;
89
+ search?: string;
90
+ limit?: number;
91
+ offset?: number;
92
+ includeNotes?: boolean;
93
+ }
94
+ export interface UserProfile {
95
+ id: string;
96
+ name: string;
97
+ email: string;
98
+ organizationId: string;
99
+ organizationName: string;
100
+ }
@@ -0,0 +1,2 @@
1
+ // API response types — aligned with FeedbackBasket v3 REST API
2
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const VERSION = "0.3.0";
2
+ export declare const USER_AGENT = "FeedbackBasket-CLI/0.3.0";