trelly 0.3.0 → 0.3.1

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 CHANGED
@@ -211,29 +211,20 @@ CI runs the same via `bun install --frozen-lockfile`. See `AGENTS.md` for conven
211
211
 
212
212
  ## Agent skills & plugins
213
213
 
214
- Shipped in the npm package for **end users** — skills plus IDE plugin manifests. Full
215
- plugin docs: [PLUGIN.md](PLUGIN.md) · Privacy: [PRIVACY.md](PRIVACY.md) ·
216
- [skills/README.md](skills/README.md).
214
+ The npm package ships agent skills plus plugin manifests, so Claude Code, Cursor,
215
+ and pi agents learn the trelly CLI + MCP conventions automatically.
217
216
 
218
- ```bash
219
- npm install -g trelly
220
- trelly auth setup && trelly auth login
217
+ Install trelly and log in first (`npm install -g trelly && trelly auth setup && trelly auth login`), then:
221
218
 
222
- pi install npm:trelly
223
- claude plugin install "$(npm root -g)/trelly"
224
- ./bin/install-cursor-plugin-local.sh # Cursor local test (copy, not symlink)
219
+ ```bash
220
+ claude plugin install "$(npm root -g)/trelly" # Claude Code
221
+ "$(npm root -g)/trelly/bin/install-cursor-plugin-local.sh" # Cursor (copies plugin, then reload window)
222
+ pi install npm:trelly # pi
225
223
  ```
226
224
 
227
- MCP-only (no plugin): add `trelly-mcp` to `~/.cursor/mcp.json` — [mcp.example.json](mcp.example.json).
228
-
229
- ### Marketplace submission
230
-
231
- | Platform | Submit |
232
- |----------|--------|
233
- | **Cursor** | [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish) → `https://github.com/brandonkramer/trelly` |
234
- | **Claude Code** | [clau.de/plugin-directory-submission](https://clau.de/plugin-directory-submission) |
225
+ MCP-only (no plugin): add `trelly-mcp` to `~/.cursor/mcp.json` — see [mcp.example.json](mcp.example.json).
235
226
 
236
- Open source (MIT). After Claude listing: `/plugin install trelly@claude-plugins-official`.
227
+ Details: [PLUGIN.md](PLUGIN.md) · [PRIVACY.md](PRIVACY.md) · [skills/README.md](skills/README.md)
237
228
 
238
229
  ## License
239
230
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trelly",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "trelly — fast Trello CLI with multi-profile auth, MCP server, and kanban TUI",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/skills/README.md CHANGED
@@ -6,8 +6,9 @@ developing this repo.
6
6
 
7
7
  | Skill | Use when |
8
8
  |-------|----------|
9
- | [trelly](trelly/SKILL.md) | Terminal / bash: `trelly boards list`, auth, attachments, GitHub PR/commit links, `--json` |
10
- | [trelly-mcp](trelly-mcp/SKILL.md) | IDE agent with MCP wired: tool names, GitHub links via `trello_api`, envelopes, safety |
9
+ | [trelly](trelly/SKILL.md) | Terminal / bash: human card lists, auth, attachments, GitHub links, `--json` |
10
+ | [trelly-mcp](trelly-mcp/SKILL.md) | IDE + MCP: paste `display` from list tools, GitHub links, safety |
11
+ | [trelly-card-display](trelly-card-display.md) | **Required** when user asks to see/list cards (all platforms) |
11
12
 
12
13
  **One copy of the content:** `skills/trelly/` and `skills/trelly-mcp/` in the installed
13
14
  package. Plugins and Pi load from there — you don't maintain separate copies.
@@ -4,7 +4,8 @@ description: >-
4
4
  Operate the trelly Trello CLI (npm trelly; bins trelly/trello): auth setup/login,
5
5
  human vs --json output, boards/lists/cards, search, attachments, GitHub PR/commit
6
6
  links on cards, trello ui, trello api. Use when the user asks to run trelly/trello
7
- commands, link a GitHub PR or commit to a Trello card, or automate Trello with trelly.
7
+ commands, list or show Trello cards/todos, link a GitHub PR or commit to a card, or
8
+ automate Trello with trelly.
8
9
  ---
9
10
 
10
11
  # trelly
@@ -12,6 +13,10 @@ description: >-
12
13
  Fast Trello CLI (`npm install -g trelly`). **Human Trello-styled output by default**;
13
14
  **`--json` for scripts**. Commands: **`trelly`** or **`trello`** (same binary).
14
15
 
16
+ > **Showing cards to a user?** Read [trelly-card-display.md](trelly-card-display.md).
17
+ > Pi has no MCP — use **human CLI** (`trelly lists cards LIST_ID` without `--json`)
18
+ > or format per that contract. Never reply with titles-only.
19
+
15
20
  ## Prerequisites
16
21
 
17
22
  ```bash
@@ -41,7 +46,9 @@ Credentials: `~/.config/trelly/config.json` (migrates from `~/.config/trello-cli
41
46
 
42
47
  - **Scripts:** always `--json` if parsing stdout.
43
48
  - **Token cost:** raw `--json` returns full Trello objects (a board list ≈ 10k tokens) — trim with `jq` (e.g. `| jq '.data[] | {id,name}'`) or request fewer fields (`--fields`, `--query "fields=id,name"`).
44
- - **Showing cards to a human?** Render one markdown line per card: `[name](shortUrl)` + non-zero badges `💬 n · 📎 n · ✓ x/y · ⏰ due` + labels as colored dots (red 🔴 orange 🟠 yellow 🟡 green 🟢 blue 🔵 purple 🟣). Slim the JSON first: `jq '.data[] | {name, shortUrl, labels: [.labels[] | {name, color}], badges: {comments: .badges.comments, attachments: .badges.attachments, checkItems: .badges.checkItems, checkItemsChecked: .badges.checkItemsChecked}, due, dueComplete}'`.
49
+ - **Showing cards to a human?** See [trelly-card-display.md](trelly-card-display.md).
50
+ Human CLI (`trelly lists cards LIST_ID`) already matches the contract. With `--json`,
51
+ slim first: `jq '.data[] | {name, shortUrl, labels: [.labels[] | {name, color}], badges: {comments: .badges.comments, attachments: .badges.attachments, checkItems: .badges.checkItems, checkItemsChecked: .badges.checkItemsChecked}, due, dueComplete}'`.
45
52
  - **`--pretty` alone** does not emit JSON; it only indents `--json` output.
46
53
  - Errors: red `✗` in human mode; exit code `1`.
47
54
 
@@ -0,0 +1,39 @@
1
+ # Card list display contract (all agents)
2
+
3
+ When the user asks to **see**, **list**, or **show** Trello cards (todo, backlog,
4
+ board, column, "what's on…"), you MUST NOT reply with a plain numbered title list.
5
+
6
+ ## MCP (Cursor, Claude Code, Codex with trelly-mcp)
7
+
8
+ `trello_list_cards` and `trello_board_cards` return:
9
+
10
+ - `display` — markdown-v1, ready to paste (linked titles + badges)
11
+ - `data` — slim JSON for automation
12
+
13
+ **Rule: paste `display` verbatim** (optionally add a heading via `displayHeading`).
14
+ Do not re-summarize `data` into your own format.
15
+
16
+ Example line:
17
+
18
+ ```text
19
+ 1. [Publish module behavior](https://trello.com/c/1jH2UTAu) · 💬 2 · 📎 2
20
+ ```
21
+
22
+ Badge keys (omit when zero): 💬 comments · 📎 attachments · ✓ checked/total · ⏰ due.
23
+ Labels: colored dot + `` `name` `` (🔴 🟠 🟡 🟢 🔵 🟣 ⚫ ⚪).
24
+
25
+ ## CLI / Pi (no MCP)
26
+
27
+ **Preferred:** human output (no `--json`):
28
+
29
+ ```bash
30
+ trelly lists cards LIST_ID
31
+ ```
32
+
33
+ **If using `--json`:** format with the jq recipe in `skills/trelly/SKILL.md` Output
34
+ contract, or call MCP when available.
35
+
36
+ ## Automation only
37
+
38
+ Plain `data` / names-only output is OK when the user wants scripts, counts, or IDs —
39
+ not when they asked to **see** the board.
@@ -3,7 +3,8 @@ name: trelly-mcp
3
3
  description: >-
4
4
  Configure and use the trelly MCP stdio server (trello_boards_list,
5
5
  trello_card_create, trello_search, trello_api, etc.). Use when wiring Cursor/Claude
6
- MCP, linking GitHub PRs/commits to Trello cards from an agent, or choosing MCP vs CLI.
6
+ MCP, listing or showing Trello cards/todos, linking GitHub PRs/commits, or choosing
7
+ MCP vs CLI.
7
8
  ---
8
9
 
9
10
  # trelly-mcp
@@ -12,15 +13,22 @@ MCP server for Trello (npm package **trelly**, bin **`trelly-mcp`**). Returns JS
12
13
  envelope on every tool: `{ ok, profile, data }` /
13
14
  `{ ok: false, error, status?, details? }`. Never uses CLI human/Ink output.
14
15
 
16
+ > **Listing cards for a user?** Read [trelly-card-display.md](trelly-card-display.md).
17
+ > **`trello_list_cards` / `trello_board_cards` include `display` — paste it verbatim.**
18
+
15
19
  List/get tools default to lean `fields` (id, name, url, due, …) to keep responses
16
- token-cheap; pass `fields: "all"` or a comma list when you need more.
17
- `trello_list_cards` and `trello_card_get` also include slim `badges`
18
- (comments/attachments/checkItems counts) and `labels` for rich rendering.
20
+ token-cheap; pass `fields: "all"` when you need more.
21
+ `trello_list_cards` and `trello_board_cards` include slim `badges`, `labels`, and
22
+ pre-rendered **`display`** (markdown-v1).
19
23
 
20
24
  ## Rendering card lists for humans
21
25
 
22
- When the user asks to **see** cards (not automation), render one markdown line per
23
- card title linked to the card, then only the badges that are non-zero:
26
+ **Do not reformat.** When `display` is present, show it to the user unchanged
27
+ (you may prepend context from `displayHeading` or your own board/list title).
28
+
29
+ MCP tool text also leads with `display`, then JSON — prefer the markdown block.
30
+
31
+ Manual format (only if `display` missing — e.g. raw `trello_api`):
24
32
 
25
33
  ```
26
34
  1. [Publish module behavior](https://trello.com/c/1jH2UTAu) 🔴 `Priority: Highest` · 💬 4 · 📎 2 · ✓ 2/5 · ⏰ Jul 5
@@ -29,7 +37,6 @@ card — title linked to the card, then only the badges that are non-zero:
29
37
  - Title → `[name](shortUrl)`. Counts from `badges`: 💬 comments · 📎 attachments · ✓ checkItemsChecked/checkItems.
30
38
  - Due: `⏰ Jul 5`, add `(overdue)` if past and not `dueComplete`; `✓` if complete.
31
39
  - Labels: colored dot + name in backticks. Trello color → emoji: red 🔴 · orange 🟠 · yellow 🟡 · green/lime 🟢 · blue/sky 🔵 · purple/pink 🟣 · black ⚫ · none ⚪.
32
- - Whole board: add `fields: "…,badges,labels"` to `trello_board_cards` (lean by default).
33
40
  - Custom-field chips (e.g. `Priority: Highest`): fetch defs once via `trello_api` GET
34
41
  `/boards/{boardId}/customFields`, request `customFieldItems` on cards, match
35
42
  `idValue` → option text/color. Skip unless the user wants them — it's an extra call.
@@ -100,9 +107,9 @@ Starts stdio MCP manually (IDE normally launches `trelly-mcp` itself).
100
107
  | `trello_board_create` | Create board |
101
108
  | `trello_board_archive` | Close board (reversible) |
102
109
  | `trello_board_lists` | Lists on board |
103
- | `trello_board_cards` | All cards on board |
110
+ | `trello_board_cards` | All cards on board (**returns `display` — paste for users**) |
104
111
  | `trello_list_create` | Create list |
105
- | `trello_list_cards` | Cards in list |
112
+ | `trello_list_cards` | Cards in list (**returns `display` markdown — paste for users**) |
106
113
  | `trello_card_get` | Card (`fields` optional) |
107
114
  | `trello_card_create` | Create card on list |
108
115
  | `trello_card_update` | Update card fields map |
package/src/index.test.ts CHANGED
@@ -7,6 +7,7 @@ import { customFieldChips } from "./cli/ui/custom-fields.ts";
7
7
  import { dueStatus, labelHex, listAccentHex } from "./cli/ui/palette.ts";
8
8
  import { isBoard, isCard, isLabel, isList } from "./cli/ui/shapes.ts";
9
9
  import { attachmentMime } from "./util/attachment.ts";
10
+ import { formatCardLine, formatCardListMarkdown } from "./util/card-display.ts";
10
11
 
11
12
  describe("parseKvPairs", () => {
12
13
  it("parses key=value pairs", () => {
@@ -124,3 +125,30 @@ describe("ui shapes", () => {
124
125
  assert.ok(isBoard(board) && !isBoard(card) && !isBoard(list));
125
126
  });
126
127
  });
128
+
129
+ describe("card display markdown", () => {
130
+ it("formats linked title with labels and non-zero badges only", () => {
131
+ const line = formatCardLine({
132
+ name: "Publish module behavior",
133
+ shortUrl: "https://trello.com/c/1jH2UTAu",
134
+ labels: [{ name: "Backend", color: "blue" }],
135
+ badges: { comments: 2, attachments: 2, checkItems: 0, checkItemsChecked: 0 },
136
+ });
137
+ assert.match(
138
+ line,
139
+ /^\[Publish module behavior\]\(https:\/\/trello\.com\/c\/1jH2UTAu\)/,
140
+ );
141
+ assert.match(line, /🔵 `Backend`/);
142
+ assert.match(line, /💬 2/);
143
+ assert.match(line, /📎 2/);
144
+ assert.doesNotMatch(line, /✓/);
145
+ });
146
+
147
+ it("builds numbered list with optional heading", () => {
148
+ const md = formatCardListMarkdown(
149
+ [{ name: "A", shortUrl: "https://trello.com/c/a" }],
150
+ "**To do**",
151
+ );
152
+ assert.equal(md, "**To do**\n\n1. [A](https://trello.com/c/a)");
153
+ });
154
+ });
@@ -2,11 +2,19 @@ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { z } from "zod";
3
3
  import { type JsonValue, type TrelloClient, TrelloError } from "../api/client.ts";
4
4
  import { getClient } from "../cli/context.ts";
5
+ import {
6
+ CARD_LIST_DISPLAY_FORMAT,
7
+ type CardDisplayInput,
8
+ formatCardListMarkdown,
9
+ } from "../util/card-display.ts";
5
10
 
6
11
  export const toolEnvelopeSchema = z.object({
7
12
  ok: z.boolean(),
8
13
  profile: z.string().optional(),
9
14
  data: z.unknown().optional(),
15
+ /** Pre-rendered markdown card list — agents MUST show this to users when present. */
16
+ display: z.string().optional(),
17
+ displayFormat: z.string().optional(),
10
18
  error: z.string().optional(),
11
19
  status: z.number().optional(),
12
20
  details: z.unknown().optional(),
@@ -17,13 +25,34 @@ export type ToolEnvelope = z.infer<typeof toolEnvelopeSchema>;
17
25
  export const profileField = z.string().optional().describe("Auth profile name");
18
26
 
19
27
  export function toolResult(envelope: ToolEnvelope): CallToolResult {
28
+ const text =
29
+ envelope.display && envelope.ok
30
+ ? `${envelope.display}\n\n---\n${JSON.stringify(envelope)}`
31
+ : JSON.stringify(envelope);
20
32
  return {
21
33
  structuredContent: envelope,
22
- content: [{ type: "text", text: JSON.stringify(envelope) }],
34
+ content: [{ type: "text", text }],
23
35
  isError: envelope.ok === false,
24
36
  };
25
37
  }
26
38
 
39
+ /** Attach markdown-v1 card list for agent display (see skills/trelly-mcp). */
40
+ export function cardListDisplay(
41
+ cards: CardDisplayInput[],
42
+ heading?: string,
43
+ ): Pick<ToolEnvelope, "display" | "displayFormat"> {
44
+ if (cards.length === 0) {
45
+ return {
46
+ display: heading ? `${heading}\n\n(no cards)` : "(no cards)",
47
+ displayFormat: CARD_LIST_DISPLAY_FORMAT,
48
+ };
49
+ }
50
+ return {
51
+ display: formatCardListMarkdown(cards, heading),
52
+ displayFormat: CARD_LIST_DISPLAY_FORMAT,
53
+ };
54
+ }
55
+
27
56
  // Trello's badges object is ~400 chars of mostly unused keys; keep the 4 that matter.
28
57
  const BADGE_KEYS = ["comments", "attachments", "checkItems", "checkItemsChecked"];
29
58
 
@@ -62,6 +91,38 @@ export function slimCards(data: unknown): unknown {
62
91
  return data;
63
92
  }
64
93
 
94
+ export async function withCardListResult(
95
+ profile: string | undefined,
96
+ fn: (client: TrelloClient) => Promise<unknown>,
97
+ heading?: string,
98
+ ): Promise<CallToolResult> {
99
+ try {
100
+ const { profileName, client } = getClient(profile);
101
+ const raw = await fn(client);
102
+ const cards = slimCards(raw);
103
+ const arr = Array.isArray(cards) ? (cards as CardDisplayInput[]) : [];
104
+ return toolResult({
105
+ ok: true,
106
+ profile: profileName,
107
+ data: arr,
108
+ ...cardListDisplay(arr, heading),
109
+ });
110
+ } catch (err) {
111
+ if (err instanceof TrelloError) {
112
+ return toolResult({
113
+ ok: false,
114
+ error: err.message,
115
+ status: err.status,
116
+ details: err.body,
117
+ });
118
+ }
119
+ return toolResult({
120
+ ok: false,
121
+ error: err instanceof Error ? err.message : String(err),
122
+ });
123
+ }
124
+ }
125
+
65
126
  export async function withClient<T>(
66
127
  profile: string | undefined,
67
128
  fn: (client: TrelloClient, profileName: string) => Promise<T>,
@@ -2,8 +2,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  profileField,
5
- slimCards,
6
5
  toolEnvelopeSchema,
6
+ withCardListResult,
7
7
  withClient,
8
8
  } from "../handlers.ts";
9
9
 
@@ -120,23 +120,27 @@ export function registerBoardTools(server: McpServer): void {
120
120
  server.registerTool(
121
121
  "trello_board_cards",
122
122
  {
123
- description: "List all cards on a board.",
123
+ description:
124
+ "List all cards on a board. Default fields omit badges/labels — pass fields including badges,labels for rich `display`. When showing cards to the user, paste response `display` verbatim.",
124
125
  inputSchema: {
125
126
  profile: profileField,
126
127
  boardId: z.string().min(1),
127
128
  fields: z
128
129
  .string()
129
- .default("id,name,idList,due,dueComplete,shortUrl,closed")
130
+ .default("id,name,idList,due,dueComplete,shortUrl,closed,badges,labels")
130
131
  .describe(
131
- 'comma-separated fields, "all" for everything; add "badges,labels" for rich lists',
132
+ 'comma-separated fields, "all" for everything; badges,labels included by default for display',
132
133
  ),
134
+ displayHeading: z.string().optional(),
133
135
  },
134
136
  annotations: { readOnlyHint: true },
135
137
  outputSchema,
136
138
  },
137
- async ({ profile, boardId, fields }) =>
138
- withClient(profile, async (client) =>
139
- slimCards(await client.boardCards(boardId, { fields })),
139
+ async ({ profile, boardId, fields, displayHeading }) =>
140
+ withCardListResult(
141
+ profile,
142
+ (client) => client.boardCards(boardId, { fields }),
143
+ displayHeading,
140
144
  ),
141
145
  );
142
146
  }
@@ -2,8 +2,8 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  profileField,
5
- slimCards,
6
5
  toolEnvelopeSchema,
6
+ withCardListResult,
7
7
  withClient,
8
8
  } from "../handlers.ts";
9
9
 
@@ -31,7 +31,8 @@ export function registerListTools(server: McpServer): void {
31
31
  server.registerTool(
32
32
  "trello_list_cards",
33
33
  {
34
- description: "List cards in a list.",
34
+ description:
35
+ "List cards in a list. Response includes `display` (markdown-v1, linked titles + 💬📎✓⏰ badges) — when the user should SEE cards, paste `display` verbatim; do not reformat as a plain title list.",
35
36
  inputSchema: {
36
37
  profile: profileField,
37
38
  listId: z.string().min(1),
@@ -39,13 +40,21 @@ export function registerListTools(server: McpServer): void {
39
40
  .string()
40
41
  .default("id,name,idList,due,dueComplete,shortUrl,closed,badges,labels")
41
42
  .describe('comma-separated fields, "all" for everything'),
43
+ displayHeading: z
44
+ .string()
45
+ .optional()
46
+ .describe(
47
+ 'Optional markdown heading prepended to `display` (e.g. "**Dogster → To do**")',
48
+ ),
42
49
  },
43
50
  annotations: { readOnlyHint: true },
44
51
  outputSchema,
45
52
  },
46
- async ({ profile, listId, fields }) =>
47
- withClient(profile, async (client) =>
48
- slimCards(await client.listCards(listId, { fields })),
53
+ async ({ profile, listId, fields, displayHeading }) =>
54
+ withCardListResult(
55
+ profile,
56
+ (client) => client.listCards(listId, { fields }),
57
+ displayHeading,
49
58
  ),
50
59
  );
51
60
  }
@@ -0,0 +1,100 @@
1
+ import { dueStatus, formatDue } from "../cli/ui/palette.ts";
2
+
3
+ /** Markdown emoji for Trello label colors (agent-facing card lists). */
4
+ const LABEL_DOT: Record<string, string> = {
5
+ red: "🔴",
6
+ orange: "🟠",
7
+ yellow: "🟡",
8
+ green: "🟢",
9
+ lime: "🟢",
10
+ blue: "🔵",
11
+ sky: "🔵",
12
+ sky_light: "🔵",
13
+ sky_dark: "🔵",
14
+ purple: "🟣",
15
+ pink: "🟣",
16
+ black: "⚫",
17
+ none: "⚪",
18
+ };
19
+
20
+ export type CardDisplayInput = {
21
+ name?: string;
22
+ shortUrl?: string;
23
+ url?: string;
24
+ due?: string | null;
25
+ dueComplete?: boolean;
26
+ badges?: {
27
+ comments?: number;
28
+ attachments?: number;
29
+ checkItems?: number;
30
+ checkItemsChecked?: number;
31
+ };
32
+ labels?: Array<{ name?: string; color?: string | null }>;
33
+ };
34
+
35
+ function labelDot(color: string | null | undefined): string {
36
+ if (!color) return "⚪";
37
+ const base = color.split("_")[0] ?? color;
38
+ return LABEL_DOT[color] ?? LABEL_DOT[base] ?? "⚪";
39
+ }
40
+
41
+ function labelParts(labels: CardDisplayInput["labels"]): string[] {
42
+ if (!labels?.length) return [];
43
+ return labels
44
+ .map((label) => {
45
+ const name = (label.name ?? "").trim();
46
+ const dot = labelDot(label.color);
47
+ return name ? `${dot} \`${name}\`` : dot;
48
+ })
49
+ .filter(Boolean);
50
+ }
51
+
52
+ function badgeParts(badges: CardDisplayInput["badges"]): string[] {
53
+ if (!badges) return [];
54
+ const parts: string[] = [];
55
+ const comments = badges.comments ?? 0;
56
+ const attachments = badges.attachments ?? 0;
57
+ const total = badges.checkItems ?? 0;
58
+ const checked = badges.checkItemsChecked ?? 0;
59
+ if (comments > 0) parts.push(`💬 ${comments}`);
60
+ if (attachments > 0) parts.push(`📎 ${attachments}`);
61
+ if (total > 0) parts.push(`✓ ${checked}/${total}`);
62
+ return parts;
63
+ }
64
+
65
+ function duePart(
66
+ due: string | null | undefined,
67
+ dueComplete: boolean | undefined,
68
+ ): string {
69
+ if (!due) return "";
70
+ const status = dueStatus(due, dueComplete);
71
+ const label = formatDue(due);
72
+ if (status === "complete") return `⏰ ${label} ✓`;
73
+ if (status === "overdue") return `⏰ ${label} (overdue)`;
74
+ return `⏰ ${label}`;
75
+ }
76
+
77
+ /** One markdown line: `[name](shortUrl)` + labels + non-zero badges + due. */
78
+ export function formatCardLine(card: CardDisplayInput, index?: number): string {
79
+ const name = card.name ?? "(untitled)";
80
+ const href = card.shortUrl ?? card.url ?? "";
81
+ const prefix = index === undefined ? "" : `${index}. `;
82
+ const title = href ? `[${name}](${href})` : name;
83
+ const extras = [...labelParts(card.labels), ...badgeParts(card.badges)];
84
+ const due = duePart(card.due, card.dueComplete);
85
+ if (due) extras.push(due);
86
+ if (extras.length === 0) return `${prefix}${title}`;
87
+ return `${prefix}${title} · ${extras.join(" · ")}`;
88
+ }
89
+
90
+ /** Numbered markdown list for agent/user display (`displayFormat: markdown-v1`). */
91
+ export function formatCardListMarkdown(
92
+ cards: CardDisplayInput[],
93
+ heading?: string,
94
+ ): string {
95
+ const lines = cards.map((card, i) => formatCardLine(card, i + 1));
96
+ if (!heading) return lines.join("\n");
97
+ return `${heading}\n\n${lines.join("\n")}`;
98
+ }
99
+
100
+ export const CARD_LIST_DISPLAY_FORMAT = "markdown-v1";