trelly 0.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.
Files changed (46) hide show
  1. package/README.md +191 -0
  2. package/bin/run-ts +31 -0
  3. package/bin/trelly +2 -0
  4. package/bin/trelly-mcp +2 -0
  5. package/package.json +64 -0
  6. package/src/api/client.ts +332 -0
  7. package/src/api/http.ts +86 -0
  8. package/src/auth/browser-flow.ts +217 -0
  9. package/src/auth/profiles.ts +163 -0
  10. package/src/cli/commands/actions.ts +17 -0
  11. package/src/cli/commands/api.ts +46 -0
  12. package/src/cli/commands/auth.ts +248 -0
  13. package/src/cli/commands/boards.ts +152 -0
  14. package/src/cli/commands/cards.ts +194 -0
  15. package/src/cli/commands/checklists.ts +79 -0
  16. package/src/cli/commands/custom-fields.ts +75 -0
  17. package/src/cli/commands/labels.ts +47 -0
  18. package/src/cli/commands/lists.ts +54 -0
  19. package/src/cli/commands/members.ts +14 -0
  20. package/src/cli/commands/orgs.ts +23 -0
  21. package/src/cli/commands/run.ts +32 -0
  22. package/src/cli/commands/search.ts +23 -0
  23. package/src/cli/commands/ui.ts +48 -0
  24. package/src/cli/commands/webhooks.ts +44 -0
  25. package/src/cli/context.ts +70 -0
  26. package/src/cli/index.ts +47 -0
  27. package/src/cli/ui/app.tsx +753 -0
  28. package/src/cli/ui/custom-fields.ts +75 -0
  29. package/src/cli/ui/palette.ts +69 -0
  30. package/src/cli/ui/shapes.ts +68 -0
  31. package/src/cli/ui/static.tsx +382 -0
  32. package/src/index.test.ts +117 -0
  33. package/src/mcp/handlers.ts +61 -0
  34. package/src/mcp/server.ts +17 -0
  35. package/src/mcp/tools/api.ts +27 -0
  36. package/src/mcp/tools/boards.ts +116 -0
  37. package/src/mcp/tools/cards.ts +138 -0
  38. package/src/mcp/tools/checklists.ts +40 -0
  39. package/src/mcp/tools/index.ts +22 -0
  40. package/src/mcp/tools/labels.ts +40 -0
  41. package/src/mcp/tools/lists.ts +37 -0
  42. package/src/mcp/tools/profiles.ts +40 -0
  43. package/src/mcp/tools/search.ts +31 -0
  44. package/src/mcp/tools/webhooks.ts +52 -0
  45. package/src/util/runtime.ts +39 -0
  46. package/src/version.ts +15 -0
@@ -0,0 +1,75 @@
1
+ import type { Command } from "commander";
2
+ import { getClient, parseKvPairs } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerCustomFieldCommands(program: Command): void {
6
+ const fields = program
7
+ .command("custom-fields")
8
+ .description("Custom field operations");
9
+
10
+ fields.command("get <id>").action((id, _opts, cmd) =>
11
+ run(cmd, async () => {
12
+ const { client } = getClient(rootOpts(cmd).profile);
13
+ return client.customFieldGet(id);
14
+ }),
15
+ );
16
+
17
+ fields
18
+ .command("create")
19
+ .requiredOption("--board <boardId>", "Board id")
20
+ .requiredOption("--name <name>", "Field name")
21
+ .requiredOption("--type <type>", "text|number|date|checkbox|list")
22
+ .action((opts, cmd) =>
23
+ run(cmd, async () => {
24
+ const { client } = getClient(rootOpts(cmd).profile);
25
+ return client.customFieldCreate({
26
+ idModel: opts.board,
27
+ modelType: "board",
28
+ name: opts.name,
29
+ type: opts.type,
30
+ });
31
+ }),
32
+ );
33
+
34
+ fields
35
+ .command("update <id>")
36
+ .option("--params <kv...>", "key=value params")
37
+ .action((id, opts, cmd) =>
38
+ run(cmd, async () => {
39
+ const { client } = getClient(rootOpts(cmd).profile);
40
+ return client.customFieldUpdate(id, parseKvPairs(opts.params));
41
+ }),
42
+ );
43
+
44
+ fields.command("delete <id>").action((id, _opts, cmd) =>
45
+ run(cmd, async () => {
46
+ const { client } = getClient(rootOpts(cmd).profile);
47
+ return client.customFieldDelete(id);
48
+ }),
49
+ );
50
+
51
+ fields
52
+ .command("set-item <fieldId>")
53
+ .requiredOption("--card <cardId>", "Card id")
54
+ .option("--text <text>", "Text value")
55
+ .option("--number <number>", "Number value")
56
+ .option("--date <iso>", "Date value")
57
+ .option("--checked <bool>", "Checkbox value")
58
+ .action((fieldId, opts, cmd) =>
59
+ run(cmd, async () => {
60
+ const { client } = getClient(rootOpts(cmd).profile);
61
+ return client.customFieldUpdateItem(
62
+ fieldId,
63
+ { idCard: opts.card },
64
+ {
65
+ value: {
66
+ text: opts.text,
67
+ number: opts.number,
68
+ date: opts.date,
69
+ checked: opts.checked,
70
+ },
71
+ },
72
+ );
73
+ }),
74
+ );
75
+ }
@@ -0,0 +1,47 @@
1
+ import type { Command } from "commander";
2
+ import { getClient, parseKvPairs } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerLabelCommands(program: Command): void {
6
+ const labels = program.command("labels").description("Label operations");
7
+
8
+ labels.command("get <id>").action((id, _opts, cmd) =>
9
+ run(cmd, async () => {
10
+ const { client } = getClient(rootOpts(cmd).profile);
11
+ return client.labelGet(id);
12
+ }),
13
+ );
14
+
15
+ labels
16
+ .command("create")
17
+ .requiredOption("--board <boardId>", "Board id")
18
+ .requiredOption("--name <name>", "Label name")
19
+ .requiredOption("--color <color>", "Label color")
20
+ .action((opts, cmd) =>
21
+ run(cmd, async () => {
22
+ const { client } = getClient(rootOpts(cmd).profile);
23
+ return client.labelCreate({
24
+ idBoard: opts.board,
25
+ name: opts.name,
26
+ color: opts.color,
27
+ });
28
+ }),
29
+ );
30
+
31
+ labels
32
+ .command("update <id>")
33
+ .option("--params <kv...>", "key=value params")
34
+ .action((id, opts, cmd) =>
35
+ run(cmd, async () => {
36
+ const { client } = getClient(rootOpts(cmd).profile);
37
+ return client.labelUpdate(id, parseKvPairs(opts.params));
38
+ }),
39
+ );
40
+
41
+ labels.command("delete <id>").action((id, _opts, cmd) =>
42
+ run(cmd, async () => {
43
+ const { client } = getClient(rootOpts(cmd).profile);
44
+ return client.labelDelete(id);
45
+ }),
46
+ );
47
+ }
@@ -0,0 +1,54 @@
1
+ import type { Command } from "commander";
2
+ import { getClient, parseKvPairs } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerListCommands(program: Command): void {
6
+ const lists = program.command("lists").description("List (column) operations");
7
+
8
+ lists.command("get <id>").action((id, _opts, cmd) =>
9
+ run(cmd, async () => {
10
+ const { client } = getClient(rootOpts(cmd).profile);
11
+ return client.listGet(id);
12
+ }),
13
+ );
14
+
15
+ lists
16
+ .command("create")
17
+ .requiredOption("--board <boardId>", "Board id")
18
+ .requiredOption("--name <name>", "List name")
19
+ .option("--pos <pos>", "Position")
20
+ .action((opts, cmd) =>
21
+ run(cmd, async () => {
22
+ const { client } = getClient(rootOpts(cmd).profile);
23
+ return client.listCreate({
24
+ idBoard: opts.board,
25
+ name: opts.name,
26
+ pos: opts.pos,
27
+ });
28
+ }),
29
+ );
30
+
31
+ lists
32
+ .command("update <id>")
33
+ .option("--params <kv...>", "key=value params")
34
+ .action((id, opts, cmd) =>
35
+ run(cmd, async () => {
36
+ const { client } = getClient(rootOpts(cmd).profile);
37
+ return client.listUpdate(id, parseKvPairs(opts.params));
38
+ }),
39
+ );
40
+
41
+ lists.command("archive <id>").action((id, _opts, cmd) =>
42
+ run(cmd, async () => {
43
+ const { client } = getClient(rootOpts(cmd).profile);
44
+ return client.listArchive(id);
45
+ }),
46
+ );
47
+
48
+ lists.command("cards <id>").action((id, _opts, cmd) =>
49
+ run(cmd, async () => {
50
+ const { client } = getClient(rootOpts(cmd).profile);
51
+ return client.listCards(id);
52
+ }),
53
+ );
54
+ }
@@ -0,0 +1,14 @@
1
+ import type { Command } from "commander";
2
+ import { getClient } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerMemberCommands(program: Command): void {
6
+ const members = program.command("members").description("Member operations");
7
+
8
+ members.command("me").action((_opts, cmd) =>
9
+ run(cmd, async () => {
10
+ const { client } = getClient(rootOpts(cmd).profile);
11
+ return client.memberMe();
12
+ }),
13
+ );
14
+ }
@@ -0,0 +1,23 @@
1
+ import type { Command } from "commander";
2
+ import { getClient } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerOrgCommands(program: Command): void {
6
+ const orgs = program
7
+ .command("orgs")
8
+ .description("Workspace (organization) operations");
9
+
10
+ orgs.command("get <id>").action((id, _opts, cmd) =>
11
+ run(cmd, async () => {
12
+ const { client } = getClient(rootOpts(cmd).profile);
13
+ return client.orgGet(id);
14
+ }),
15
+ );
16
+
17
+ orgs.command("boards <id>").action((id, _opts, cmd) =>
18
+ run(cmd, async () => {
19
+ const { client } = getClient(rootOpts(cmd).profile);
20
+ return client.orgBoards(id);
21
+ }),
22
+ );
23
+ }
@@ -0,0 +1,32 @@
1
+ import type { Command } from "commander";
2
+ import { TrelloError } from "../../api/client.ts";
3
+ import { failure, getClient, printResult, success } from "../context.ts";
4
+
5
+ export function rootOpts(cmd: Command): {
6
+ profile?: string;
7
+ pretty: boolean;
8
+ json: boolean;
9
+ } {
10
+ let node: Command | null = cmd;
11
+ while (node?.parent) node = node.parent;
12
+ return node?.opts() ?? {};
13
+ }
14
+
15
+ export async function run<T>(cmd: Command, fn: () => Promise<T>): Promise<void> {
16
+ const opts = rootOpts(cmd);
17
+ try {
18
+ const { profileName } = getClient(opts.profile);
19
+ const data = await fn();
20
+ printResult(success(profileName, data), opts);
21
+ } catch (err) {
22
+ if (err instanceof TrelloError) {
23
+ printResult(
24
+ failure(err.message, { status: err.status, details: err.body }),
25
+ opts,
26
+ );
27
+ } else {
28
+ printResult(failure(err instanceof Error ? err.message : String(err)), opts);
29
+ }
30
+ process.exitCode = 1;
31
+ }
32
+ }
@@ -0,0 +1,23 @@
1
+ import type { Command } from "commander";
2
+ import { getClient } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerSearchCommands(program: Command): void {
6
+ program
7
+ .command("search")
8
+ .description("Search boards, cards, members, and actions")
9
+ .argument("<query>", "Search query")
10
+ .option("--model-types <types>", "Comma-separated: cards,boards,members,...")
11
+ .option("--cards-limit <n>", "Max cards")
12
+ .option("--boards-limit <n>", "Max boards")
13
+ .action((query, opts, cmd) =>
14
+ run(cmd, async () => {
15
+ const { client } = getClient(rootOpts(cmd).profile);
16
+ return client.search(query, {
17
+ modelTypes: opts.modelTypes,
18
+ cards_limit: opts.cardsLimit,
19
+ boards_limit: opts.boardsLimit,
20
+ });
21
+ }),
22
+ );
23
+ }
@@ -0,0 +1,48 @@
1
+ import type { Command } from "commander";
2
+ import { failure, getClient, printResult } from "../context.ts";
3
+ import { rootOpts } from "./run.ts";
4
+
5
+ const ENTER_ALT_SCREEN = "\u001b[?1049h\u001b[H";
6
+ const LEAVE_ALT_SCREEN = "\u001b[?1049l";
7
+
8
+ export async function launchUi(
9
+ boardId: string | undefined,
10
+ cmd: Command,
11
+ ): Promise<void> {
12
+ const root = rootOpts(cmd);
13
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
14
+ printResult(
15
+ failure("interactive mode requires a terminal — run trelly --help for commands"),
16
+ root,
17
+ );
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ try {
22
+ const { profileName, client } = getClient(root.profile);
23
+ const [{ render }, { createElement }, { App }] = await Promise.all([
24
+ import("ink"),
25
+ import("react"),
26
+ import("../ui/app.tsx"),
27
+ ]);
28
+ process.stdout.write(ENTER_ALT_SCREEN);
29
+ try {
30
+ const app = render(createElement(App, { client, boardId, profileName }));
31
+ await app.waitUntilExit();
32
+ } finally {
33
+ process.stdout.write(LEAVE_ALT_SCREEN);
34
+ }
35
+ } catch (err) {
36
+ printResult(failure(err instanceof Error ? err.message : String(err)), root);
37
+ process.exitCode = 1;
38
+ }
39
+ }
40
+
41
+ export function registerUiCommand(program: Command): void {
42
+ program
43
+ .command("ui [boardId]")
44
+ .description(
45
+ "Interactive kanban — picks a board when no id is given (arrows/hjkl, ⏎, r, q)",
46
+ )
47
+ .action((boardId, _opts, cmd) => launchUi(boardId, cmd));
48
+ }
@@ -0,0 +1,44 @@
1
+ import type { Command } from "commander";
2
+ import { getClient } from "../context.ts";
3
+ import { rootOpts, run } from "./run.ts";
4
+
5
+ export function registerWebhookCommands(program: Command): void {
6
+ const webhooks = program.command("webhooks").description("Webhook operations");
7
+
8
+ webhooks.command("list").action((_opts, cmd) =>
9
+ run(cmd, async () => {
10
+ const { client } = getClient(rootOpts(cmd).profile);
11
+ return client.webhooksForToken();
12
+ }),
13
+ );
14
+
15
+ webhooks
16
+ .command("create")
17
+ .requiredOption("--callback <url>", "Callback URL")
18
+ .requiredOption("--id-model <id>", "Board or card id to watch")
19
+ .option("--desc <desc>", "Description")
20
+ .action((opts, cmd) =>
21
+ run(cmd, async () => {
22
+ const { client } = getClient(rootOpts(cmd).profile);
23
+ return client.webhookCreate({
24
+ callbackURL: opts.callback,
25
+ idModel: opts.idModel,
26
+ description: opts.desc,
27
+ });
28
+ }),
29
+ );
30
+
31
+ webhooks.command("get <id>").action((id, _opts, cmd) =>
32
+ run(cmd, async () => {
33
+ const { client } = getClient(rootOpts(cmd).profile);
34
+ return client.webhookGet(id);
35
+ }),
36
+ );
37
+
38
+ webhooks.command("delete <id>").action((id, _opts, cmd) =>
39
+ run(cmd, async () => {
40
+ const { client } = getClient(rootOpts(cmd).profile);
41
+ return client.webhookDelete(id);
42
+ }),
43
+ );
44
+ }
@@ -0,0 +1,70 @@
1
+ import { createClient } from "../api/client.ts";
2
+ import { resolveProfile } from "../auth/profiles.ts";
3
+
4
+ export function getClient(profile?: string) {
5
+ const { name, profile: creds } = resolveProfile(profile);
6
+ return {
7
+ profileName: name,
8
+ client: createClient(creds.apiKey, creds.token),
9
+ };
10
+ }
11
+
12
+ export type CliResult<T = unknown> = {
13
+ ok: true;
14
+ profile: string;
15
+ data: T;
16
+ };
17
+
18
+ export type CliError = {
19
+ ok: false;
20
+ profile?: string;
21
+ error: string;
22
+ status?: number;
23
+ details?: unknown;
24
+ };
25
+
26
+ export function success<T>(profile: string, data: T): CliResult<T> {
27
+ return { ok: true, profile, data };
28
+ }
29
+
30
+ export function failure(
31
+ error: string,
32
+ extra: Omit<CliError, "ok" | "error"> = {},
33
+ ): CliError {
34
+ return { ok: false, error, ...extra };
35
+ }
36
+
37
+ export type OutputOptions = { json?: boolean; pretty?: boolean };
38
+
39
+ export function printResult(
40
+ result: CliResult | CliError,
41
+ opts: OutputOptions = {},
42
+ ): void {
43
+ if (opts.json) {
44
+ const text = opts.pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result);
45
+ console.log(text);
46
+ return;
47
+ }
48
+ void import("./ui/static.tsx").then(({ renderResult }) => renderResult(result));
49
+ }
50
+
51
+ export function parseKvPairs(pairs: string[] | undefined): Record<string, string> {
52
+ const out: Record<string, string> = {};
53
+ for (const pair of pairs ?? []) {
54
+ const idx = pair.indexOf("=");
55
+ if (idx <= 0) {
56
+ throw new Error(`Invalid key=value pair: ${pair}`);
57
+ }
58
+ out[pair.slice(0, idx)] = pair.slice(idx + 1);
59
+ }
60
+ return out;
61
+ }
62
+
63
+ export function parseJsonFlag(raw: string | undefined, label: string): unknown {
64
+ if (!raw) return undefined;
65
+ try {
66
+ return JSON.parse(raw);
67
+ } catch {
68
+ throw new Error(`${label} must be valid JSON`);
69
+ }
70
+ }
@@ -0,0 +1,47 @@
1
+ import { Command } from "commander";
2
+ import { packageVersion } from "../version.ts";
3
+ import { registerActionCommands } from "./commands/actions.ts";
4
+ import { registerApiCommand } from "./commands/api.ts";
5
+ import { registerAuthCommands } from "./commands/auth.ts";
6
+ import { registerBoardCommands } from "./commands/boards.ts";
7
+ import { registerCardCommands } from "./commands/cards.ts";
8
+ import { registerChecklistCommands } from "./commands/checklists.ts";
9
+ import { registerCustomFieldCommands } from "./commands/custom-fields.ts";
10
+ import { registerLabelCommands } from "./commands/labels.ts";
11
+ import { registerListCommands } from "./commands/lists.ts";
12
+ import { registerMemberCommands } from "./commands/members.ts";
13
+ import { registerOrgCommands } from "./commands/orgs.ts";
14
+ import { registerSearchCommands } from "./commands/search.ts";
15
+ import { launchUi, registerUiCommand } from "./commands/ui.ts";
16
+ import { registerWebhookCommands } from "./commands/webhooks.ts";
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name("trelly")
22
+ .description("trelly — Trello CLI; bare `trelly` opens the interactive board UI")
23
+ .version(packageVersion())
24
+ .option("-p, --profile <name>", "Auth profile (or TRELLO_PROFILE env)")
25
+ .option("--json", "Output the raw JSON envelope", false)
26
+ .option("--pretty", "Indent --json output", false)
27
+ .action((_opts, cmd) => launchUi(undefined, cmd));
28
+
29
+ registerAuthCommands(program);
30
+ registerBoardCommands(program);
31
+ registerListCommands(program);
32
+ registerCardCommands(program);
33
+ registerChecklistCommands(program);
34
+ registerLabelCommands(program);
35
+ registerCustomFieldCommands(program);
36
+ registerSearchCommands(program);
37
+ registerWebhookCommands(program);
38
+ registerMemberCommands(program);
39
+ registerOrgCommands(program);
40
+ registerActionCommands(program);
41
+ registerApiCommand(program);
42
+ registerUiCommand(program);
43
+
44
+ program.parseAsync(process.argv).catch((err) => {
45
+ console.error(err instanceof Error ? err.message : err);
46
+ process.exit(1);
47
+ });