smashspace 0.3.0 → 0.5.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 +39 -0
- package/dist/smash.mjs +192 -16
- package/dist/smashspace-mcp.mjs +80 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,45 @@ smash card update <cardId> --desc-file spec.md # multi-line body from a file /
|
|
|
112
112
|
Pick a board with `-b <label>` (defaults to `main`), or bypass config with
|
|
113
113
|
`--board-id <id>`.
|
|
114
114
|
|
|
115
|
+
## Agent inbox: who said what to whom
|
|
116
|
+
|
|
117
|
+
Comments carry an optional **recipient**, so several agents can hand work to
|
|
118
|
+
each other on the card it is about — no separate channel, and a human reads the
|
|
119
|
+
same thread in the UI.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
smash comment add <cardId> "PR is up, please review" --author codex --to claude-code
|
|
123
|
+
smash inbox --to claude-code # only what is addressed to me
|
|
124
|
+
smash inbox --since <cursor> # only what I have not read yet
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`smash inbox` prints a `cursor` at the end; pass it as the next `--since` and
|
|
128
|
+
you read each message once. `--json` gives `{ comments, cursor }`.
|
|
129
|
+
|
|
130
|
+
## Write from CI or a cloud job (no CLI, no MCP)
|
|
131
|
+
|
|
132
|
+
For environments where you cannot install anything — a scheduled cloud run, a
|
|
133
|
+
webhook handler, a GitHub Action — issue a **board-scoped write-only token** and
|
|
134
|
+
post with `curl`:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
smash token create <boardId> "GitHub Actions" # shown once; store it now
|
|
138
|
+
smash token list <boardId> # prefix / created / last used
|
|
139
|
+
smash token revoke <boardId> <tokenId> # immediate
|
|
140
|
+
|
|
141
|
+
curl -X POST https://smashspace.app/api/ingest \
|
|
142
|
+
-H "Authorization: Bearer smash_ing_xxxxxxxx" \
|
|
143
|
+
-H "Content-Type: application/json" \
|
|
144
|
+
-H "Idempotency-Key: my-event-123" \
|
|
145
|
+
-d '{"list":"やること","title":"支払いエラー","labels":["緊急"]}'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The token can only create cards and comments on that one board — it cannot read
|
|
149
|
+
or delete. Lists and labels are matched by name, so the caller needs no internal
|
|
150
|
+
ids, and a repeated `Idempotency-Key` returns the first card instead of making
|
|
151
|
+
another (keys never expire). Tokens can also be issued from the space's
|
|
152
|
+
**Settings → Connect** tab.
|
|
153
|
+
|
|
115
154
|
## Multiple repos on one machine
|
|
116
155
|
|
|
117
156
|
`smash` is **directory-aware**: it walks up from your current directory to the
|
package/dist/smash.mjs
CHANGED
|
@@ -3398,6 +3398,40 @@ import { createInterface } from "node:readline/promises";
|
|
|
3398
3398
|
import { relative, join as join3, dirname as dirname3, basename } from "node:path";
|
|
3399
3399
|
import { stdin, stdout } from "node:process";
|
|
3400
3400
|
|
|
3401
|
+
// ../../shared/assignee-id.ts
|
|
3402
|
+
function normalizeAssigneeName(name) {
|
|
3403
|
+
return name.trim().toLowerCase();
|
|
3404
|
+
}
|
|
3405
|
+
function findAssigneeByName(known, name) {
|
|
3406
|
+
const needle = normalizeAssigneeName(name);
|
|
3407
|
+
return known.find((a) => normalizeAssigneeName(a.display_name) === needle);
|
|
3408
|
+
}
|
|
3409
|
+
function hash32(input) {
|
|
3410
|
+
let h = 2166136261;
|
|
3411
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
3412
|
+
h ^= input.charCodeAt(i);
|
|
3413
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
3414
|
+
}
|
|
3415
|
+
return h >>> 0;
|
|
3416
|
+
}
|
|
3417
|
+
function stableAssigneeId(kind, name) {
|
|
3418
|
+
const normalized = normalizeAssigneeName(name);
|
|
3419
|
+
const ascii = normalized.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
3420
|
+
const suffix = ascii.length >= 2 ? ascii : hash32(normalized).toString(36).padStart(7, "0");
|
|
3421
|
+
return `${kind}_${suffix}`;
|
|
3422
|
+
}
|
|
3423
|
+
function resolveAssignee(known, input) {
|
|
3424
|
+
const existing = input.explicitId ? known.find((a) => a.id === input.explicitId) : findAssigneeByName(known, input.name);
|
|
3425
|
+
const id = input.explicitId ?? existing?.id ?? stableAssigneeId(input.kind, input.name);
|
|
3426
|
+
const emoji = input.emoji ?? existing?.avatar_emoji;
|
|
3427
|
+
return {
|
|
3428
|
+
kind: input.kind,
|
|
3429
|
+
id,
|
|
3430
|
+
display_name: input.name.trim(),
|
|
3431
|
+
...emoji ? { avatar_emoji: emoji } : {}
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3401
3435
|
// ../../shared/board-registry.ts
|
|
3402
3436
|
var BOARD_ID_PATTERN = /^(?:board_|b_|s_)[a-z0-9]+$/i;
|
|
3403
3437
|
var URL_BOARD_PATTERN = /^(https?:\/\/[^/]+)\/(?:board|space)\/((?:board_|b_|s_)[a-z0-9]+)/i;
|
|
@@ -4135,7 +4169,7 @@ cardCmd.command("move <cardId>").description("Move a card to a list (and optiona
|
|
|
4135
4169
|
);
|
|
4136
4170
|
cardCmd.command("delete <cardId>").description("Delete a card").action(async (cardId) => {
|
|
4137
4171
|
await api(`/api/cards/${cardId}`, { method: "DELETE" });
|
|
4138
|
-
console.log(`\u2713 card ${cardId}
|
|
4172
|
+
console.log(`\u2713 card ${cardId} archived (restore with: smash archive restore ${cardId})`);
|
|
4139
4173
|
});
|
|
4140
4174
|
cardCmd.command("search <query>").description(
|
|
4141
4175
|
"Search cards by case-insensitive substring match (title + description). Use --board to search one board, or set SMASH_BOARDS env var to search all registered boards."
|
|
@@ -4202,15 +4236,101 @@ cardCmd.command("search <query>").description(
|
|
|
4202
4236
|
console.log(`
|
|
4203
4237
|
${allHits.length} card(s) found`);
|
|
4204
4238
|
});
|
|
4205
|
-
cardCmd.command("comment <cardId> <body>").description("Add a comment to a card").option("--author <name>", "Author name").
|
|
4239
|
+
cardCmd.command("comment <cardId> <body>").description("Add a comment to a card").option("--author <name>", "Author name").option(
|
|
4240
|
+
"--to <id>",
|
|
4241
|
+
"Address the comment to an assignee id (repeatable). Agents poll their own with `smash inbox --to <id>`.",
|
|
4242
|
+
collectRepeated,
|
|
4243
|
+
[]
|
|
4244
|
+
).action(
|
|
4206
4245
|
async (cardId, body, opts) => {
|
|
4207
4246
|
const comment = await api(`/api/cards/${cardId}/comments`, {
|
|
4208
4247
|
method: "POST",
|
|
4209
|
-
body: JSON.stringify({
|
|
4248
|
+
body: JSON.stringify({
|
|
4249
|
+
body,
|
|
4250
|
+
authorName: opts.author,
|
|
4251
|
+
...opts.to.length > 0 ? { recipients: opts.to } : {}
|
|
4252
|
+
})
|
|
4210
4253
|
});
|
|
4211
|
-
show(comment, [
|
|
4254
|
+
show(comment, [
|
|
4255
|
+
`\u2713 comment ${comment.id} added${comment.recipients?.length ? ` \u2192 ${comment.recipients.join(", ")}` : ""}`
|
|
4256
|
+
]);
|
|
4212
4257
|
}
|
|
4213
4258
|
);
|
|
4259
|
+
function collectRepeated(value, previous) {
|
|
4260
|
+
return [...previous, value];
|
|
4261
|
+
}
|
|
4262
|
+
program2.command("cards").description(
|
|
4263
|
+
"List cards on the resolved board \u2014 light by default (no description). Filter by list/assignee, pick columns with --fields."
|
|
4264
|
+
).option("-b, --board <label>", "board label from config").option("--board-id <id>", "explicit board id (bypass config)").option("-l, --list <name>", "only cards in this list (name or id)").option("--assignee <idOrName>", "only cards assigned to this id or display name").option(
|
|
4265
|
+
"--fields <list>",
|
|
4266
|
+
"comma-separated columns (default: id,title,listId,listTitle,updatedAt). Add description only when you need bodies."
|
|
4267
|
+
).option("--limit <n>", "max cards per page (default 100, max 500)").option("--cursor <cursor>", "continue from a previous response's cursor").action(async (opts) => {
|
|
4268
|
+
const ctx = resolveContext(opts);
|
|
4269
|
+
const params = new URLSearchParams();
|
|
4270
|
+
if (opts.list)
|
|
4271
|
+
params.set("list", opts.list);
|
|
4272
|
+
if (opts.assignee)
|
|
4273
|
+
params.set("assignee", opts.assignee);
|
|
4274
|
+
if (opts.fields)
|
|
4275
|
+
params.set("fields", opts.fields);
|
|
4276
|
+
if (opts.limit)
|
|
4277
|
+
params.set("limit", opts.limit);
|
|
4278
|
+
if (opts.cursor)
|
|
4279
|
+
params.set("cursor", opts.cursor);
|
|
4280
|
+
const qs = params.toString();
|
|
4281
|
+
const result = await api(
|
|
4282
|
+
`/api/boards/${ctx.boardId}/cards${qs ? `?${qs}` : ""}`,
|
|
4283
|
+
void 0,
|
|
4284
|
+
ctx.baseUrl
|
|
4285
|
+
);
|
|
4286
|
+
if (getOpts().json) {
|
|
4287
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
for (const card of result.cards) {
|
|
4291
|
+
const listTitle = typeof card.listTitle === "string" ? `[${card.listTitle}] ` : "";
|
|
4292
|
+
console.log(`${listTitle}${String(card.title ?? "")} (${String(card.id ?? "")})`);
|
|
4293
|
+
}
|
|
4294
|
+
console.log(
|
|
4295
|
+
`
|
|
4296
|
+
${result.cards.length} of ${result.total}${result.cursor ? ` next: --cursor ${result.cursor}` : ""}`
|
|
4297
|
+
);
|
|
4298
|
+
});
|
|
4299
|
+
program2.command("inbox").description(
|
|
4300
|
+
`New comments across the resolved board (${CONFIG_FILENAME}). Poll with --since <cursor> to read only what you have not seen.`
|
|
4301
|
+
).option("-b, --board <label>", "board label from config").option("--board-id <id>", "explicit board id (bypass config)").option("--since <iso>", "only comments created after this ISO timestamp (exclusive)").option("--to <id>", "only comments addressed to this assignee id").option("--limit <n>", "max comments to read (default 100, max 500)").action(async (opts) => {
|
|
4302
|
+
const ctx = resolveContext(opts);
|
|
4303
|
+
const params = new URLSearchParams();
|
|
4304
|
+
if (opts.since)
|
|
4305
|
+
params.set("since", opts.since);
|
|
4306
|
+
if (opts.to)
|
|
4307
|
+
params.set("to", opts.to);
|
|
4308
|
+
if (opts.limit)
|
|
4309
|
+
params.set("limit", opts.limit);
|
|
4310
|
+
const qs = params.toString();
|
|
4311
|
+
const result = await api(
|
|
4312
|
+
`/api/boards/${ctx.boardId}/comments${qs ? `?${qs}` : ""}`,
|
|
4313
|
+
void 0,
|
|
4314
|
+
ctx.baseUrl
|
|
4315
|
+
);
|
|
4316
|
+
if (getOpts().json) {
|
|
4317
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4318
|
+
return;
|
|
4319
|
+
}
|
|
4320
|
+
if (result.comments.length === 0) {
|
|
4321
|
+
console.log(`(no new comments)${result.cursor ? ` cursor: ${result.cursor}` : ""}`);
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
for (const c of result.comments) {
|
|
4325
|
+
const to = c.recipients?.length ? ` \u2192 ${c.recipients.join(", ")}` : "";
|
|
4326
|
+
console.log(
|
|
4327
|
+
`[${c.createdAt}] ${c.authorName ?? "anon"}${to} (${c.listTitle} / ${c.cardTitle} \xB7 ${c.cardId})`
|
|
4328
|
+
);
|
|
4329
|
+
console.log(` ${c.body.replace(/\n/g, "\n ")}`);
|
|
4330
|
+
}
|
|
4331
|
+
console.log(`
|
|
4332
|
+
cursor: ${result.cursor}`);
|
|
4333
|
+
});
|
|
4214
4334
|
var archiveCmd = program2.command("archive [cardId]").description("Archive a card (soft-delete). Without cardId: manage archived cards (list/restore/purge)").option("-b, --board <label>", `board label from ${CONFIG_FILENAME}`).option("--board-id <id>", "explicit board id (bypass config)").action(async (cardId, opts) => {
|
|
4215
4335
|
if (!cardId) {
|
|
4216
4336
|
archiveCmd.help();
|
|
@@ -4278,12 +4398,18 @@ checklistCmd.command("delete <itemId>").description("Delete a checklist item").a
|
|
|
4278
4398
|
console.log(`\u2713 checklist item ${itemId} deleted`);
|
|
4279
4399
|
});
|
|
4280
4400
|
var commentCmd = program2.command("comment").description("Card comments (add / edit / delete)");
|
|
4281
|
-
commentCmd.command("add <cardId> <body>").description("Add a comment to a card").option("--author <name>", "Author name").action(async (cardId, body, opts) => {
|
|
4401
|
+
commentCmd.command("add <cardId> <body>").description("Add a comment to a card").option("--author <name>", "Author name").option("--to <id>", "Address the comment to an assignee id (repeatable)", collectRepeated, []).action(async (cardId, body, opts) => {
|
|
4282
4402
|
const comment = await api(`/api/cards/${cardId}/comments`, {
|
|
4283
4403
|
method: "POST",
|
|
4284
|
-
body: JSON.stringify({
|
|
4404
|
+
body: JSON.stringify({
|
|
4405
|
+
body,
|
|
4406
|
+
authorName: opts.author,
|
|
4407
|
+
...opts.to.length > 0 ? { recipients: opts.to } : {}
|
|
4408
|
+
})
|
|
4285
4409
|
});
|
|
4286
|
-
show(comment, [
|
|
4410
|
+
show(comment, [
|
|
4411
|
+
`\u2713 comment ${comment.id} added${comment.recipients?.length ? ` \u2192 ${comment.recipients.join(", ")}` : ""}`
|
|
4412
|
+
]);
|
|
4287
4413
|
});
|
|
4288
4414
|
commentCmd.command("edit <commentId> <body>").description("Edit (replace body of) an existing comment").action(async (commentId, body) => {
|
|
4289
4415
|
const comment = await api(`/api/comments/${commentId}`, {
|
|
@@ -4396,6 +4522,48 @@ attachmentCmd.command("delete <attachmentId>").description("Delete an attachment
|
|
|
4396
4522
|
await api(`/api/attachments/${attachmentId}`, { method: "DELETE" });
|
|
4397
4523
|
console.log(`\u2713 attachment ${attachmentId} deleted`);
|
|
4398
4524
|
});
|
|
4525
|
+
var tokenCmd = program2.command("token").description("Board-scoped write tokens for external services (POST /api/ingest)");
|
|
4526
|
+
tokenCmd.command("create <boardId> <name>").description("Issue a write-only token for one board. The token is shown once.").action(async (boardId, name) => {
|
|
4527
|
+
const created = await api(
|
|
4528
|
+
`/api/boards/${boardId}/ingest-tokens`,
|
|
4529
|
+
{ method: "POST", body: JSON.stringify({ name }) }
|
|
4530
|
+
);
|
|
4531
|
+
show(created, [
|
|
4532
|
+
`\u2713 token ${created.id} created for ${boardId}`,
|
|
4533
|
+
"",
|
|
4534
|
+
created.token,
|
|
4535
|
+
"",
|
|
4536
|
+
"Store it now \u2014 it is not shown again. It can only create cards and",
|
|
4537
|
+
"comments on this board:",
|
|
4538
|
+
` curl -X POST ${resolveBaseUrl()}/api/ingest \\`,
|
|
4539
|
+
` -H "Authorization: Bearer ${created.token}" \\`,
|
|
4540
|
+
' -H "Content-Type: application/json" \\',
|
|
4541
|
+
` -d '{"list":"\u3084\u308B\u3053\u3068","title":"\u2026"}'`
|
|
4542
|
+
]);
|
|
4543
|
+
});
|
|
4544
|
+
tokenCmd.command("list <boardId>").description("List a board's write tokens (prefix, created, last used)").action(async (boardId) => {
|
|
4545
|
+
const items = await api(`/api/boards/${boardId}/ingest-tokens`);
|
|
4546
|
+
if (getOpts().json) {
|
|
4547
|
+
console.log(JSON.stringify(items, null, 2));
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
if (items.length === 0) {
|
|
4551
|
+
console.log("(no tokens)");
|
|
4552
|
+
return;
|
|
4553
|
+
}
|
|
4554
|
+
for (const t of items) {
|
|
4555
|
+
console.log(
|
|
4556
|
+
`${t.id} ${t.name} ${t.tokenPrefix}\u2026 created ${t.createdAt} last used ${t.lastUsedAt ?? "never"}`
|
|
4557
|
+
);
|
|
4558
|
+
}
|
|
4559
|
+
});
|
|
4560
|
+
tokenCmd.command("revoke <boardId> <tokenId>").description("Revoke a write token (takes effect immediately)").action(async (boardId, tokenId) => {
|
|
4561
|
+
const revoked = await api(
|
|
4562
|
+
`/api/boards/${boardId}/ingest-tokens/${tokenId}`,
|
|
4563
|
+
{ method: "DELETE" }
|
|
4564
|
+
);
|
|
4565
|
+
show(revoked, [`\u2713 token ${revoked.id} revoked at ${revoked.revokedAt}`]);
|
|
4566
|
+
});
|
|
4399
4567
|
var labelCmd = program2.command("label").description("Labels");
|
|
4400
4568
|
labelCmd.command("create <boardId> <name>").description("Create a board label").option(
|
|
4401
4569
|
"--color <color>",
|
|
@@ -4601,23 +4769,31 @@ assigneesCmd.command("add <cardId>").description("Append a single assignee (read
|
|
|
4601
4769
|
console.error("--name must not be empty");
|
|
4602
4770
|
process.exit(1);
|
|
4603
4771
|
}
|
|
4604
|
-
const id = opts.id ?? `${opts.kind}_${Math.random().toString(36).slice(2, 10)}`;
|
|
4605
4772
|
const detail = await api(`/api/cards/${cardId}`);
|
|
4606
4773
|
const current = detail.card.assignees ?? [];
|
|
4607
|
-
const
|
|
4774
|
+
const board = await api(
|
|
4775
|
+
`/api/boards/${detail.card.boardId}/cards?fields=assignees&limit=500`
|
|
4776
|
+
);
|
|
4777
|
+
const known = [
|
|
4608
4778
|
...current,
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
display_name: opts.name,
|
|
4613
|
-
...opts.emoji ? { avatar_emoji: opts.emoji } : {}
|
|
4614
|
-
}
|
|
4779
|
+
...board.cards.flatMap(
|
|
4780
|
+
(c) => Array.isArray(c.assignees) ? c.assignees : []
|
|
4781
|
+
)
|
|
4615
4782
|
];
|
|
4783
|
+
const entry = resolveAssignee(known, {
|
|
4784
|
+
kind: opts.kind,
|
|
4785
|
+
name: opts.name,
|
|
4786
|
+
explicitId: opts.id,
|
|
4787
|
+
emoji: opts.emoji
|
|
4788
|
+
});
|
|
4789
|
+
const next = [...current, entry];
|
|
4616
4790
|
const card = await api(`/api/cards/${cardId}`, {
|
|
4617
4791
|
method: "PATCH",
|
|
4618
4792
|
body: JSON.stringify({ assignees: next })
|
|
4619
4793
|
});
|
|
4620
|
-
show(card, [
|
|
4794
|
+
show(card, [
|
|
4795
|
+
`\u2713 card ${card.id} assignees now has ${next.length} entries (${entry.display_name} \u2192 ${entry.id})`
|
|
4796
|
+
]);
|
|
4621
4797
|
});
|
|
4622
4798
|
assigneesCmd.command("update <cardId> <assigneeId>").description("Change an assignee's icon (emoji) and/or display name").option("--emoji <emoji>", "New avatar_emoji").option("--no-emoji", "Remove the avatar_emoji (fall back to the default icon)").option("--name <name>", "New display_name").action(async (cardId, assigneeId, opts) => {
|
|
4623
4799
|
if (opts.emoji === void 0 && opts.name === void 0) {
|
package/dist/smashspace-mcp.mjs
CHANGED
|
@@ -15509,7 +15509,7 @@ var tools = [
|
|
|
15509
15509
|
},
|
|
15510
15510
|
{
|
|
15511
15511
|
name: "smash_get_board",
|
|
15512
|
-
description: "Get full board detail including lists, cards (with labelIds), and labels.",
|
|
15512
|
+
description: "Get full board detail including lists, cards (with labelIds), and labels. This returns every card's full description and can run to six figures of characters on a busy board \u2014 prefer smash_list_cards when you only need to find cards, and come back here for the whole structure.",
|
|
15513
15513
|
inputSchema: {
|
|
15514
15514
|
type: "object",
|
|
15515
15515
|
properties: {
|
|
@@ -15653,7 +15653,7 @@ var tools = [
|
|
|
15653
15653
|
},
|
|
15654
15654
|
{
|
|
15655
15655
|
name: "smash_set_agent_meta",
|
|
15656
|
-
description: "Set or clear a card's agent_meta (structured field for external agents like Claude Code). Pass agentMeta=null to clear. Common shape: {primary_agent, status: 'idle'|'working'|'waiting_review'|'blocked'|'done', turn: 'human'|'agent'|'blocked', started_at, updated_at, progress (0..1), current_step}. avatar_emoji is shown on the board's cards, so give each agent a distinct emoji (and reuse the same id) to make ownership visible at a glance. Extra keys are accepted for forward-compat.",
|
|
15656
|
+
description: "Set or clear a card's agent_meta (structured field for external agents like Claude Code). Pass agentMeta=null to clear. Common shape: {primary_agent, status: 'idle'|'working'|'waiting_review'|'blocked'|'done', turn: 'human'|'agent'|'blocked', started_at, updated_at, progress (0..1), current_step}. REUSE THE SAME id for the same role across cards (e.g. always 'platform', not a fresh random id): ids are how anyone filters cards later, and a role that gets a new id per card cannot be filtered at all. Check smash_list_cards with fields=['assignees'] to see which ids the board already uses. avatar_emoji is shown on the board's cards, so give each agent a distinct emoji (and reuse the same id) to make ownership visible at a glance. Extra keys are accepted for forward-compat.",
|
|
15657
15657
|
inputSchema: {
|
|
15658
15658
|
type: "object",
|
|
15659
15659
|
properties: {
|
|
@@ -15701,6 +15701,41 @@ var tools = [
|
|
|
15701
15701
|
body: JSON.stringify({ externalRefs: args.externalRefs ?? null })
|
|
15702
15702
|
})
|
|
15703
15703
|
},
|
|
15704
|
+
{
|
|
15705
|
+
name: "smash_list_cards",
|
|
15706
|
+
description: "List a board's cards WITHOUT their descriptions \u2014 use this instead of smash_get_board when you only need to find cards. smash_get_board returns every card body and can exceed a tool-output limit on a busy board. Filter with `list` and `assignee` (matches an assignee id or display name), and pick columns with `fields` (default: id, title, listId, listTitle, updatedAt). Pass fields including 'description' only when you truly need bodies; otherwise fetch one card with smash_get_card. Paginate with the returned `cursor`.",
|
|
15707
|
+
inputSchema: {
|
|
15708
|
+
type: "object",
|
|
15709
|
+
properties: {
|
|
15710
|
+
boardId: { type: "string" },
|
|
15711
|
+
list: { type: "string", description: "List name (case-insensitive) or list id" },
|
|
15712
|
+
assignee: { type: "string", description: "Assignee id or display name" },
|
|
15713
|
+
fields: {
|
|
15714
|
+
type: "array",
|
|
15715
|
+
items: { type: "string" },
|
|
15716
|
+
description: "Columns to return: id, boardId, title, description, listId, listTitle, position, dueAt, labelIds, checklistTitle, agentMeta, externalRefs, assignees, customData, createdAt, updatedAt"
|
|
15717
|
+
},
|
|
15718
|
+
limit: { type: "number", description: "Max cards per page (default 100, max 500)" },
|
|
15719
|
+
cursor: { type: "string", description: "Cursor from a previous response" }
|
|
15720
|
+
},
|
|
15721
|
+
required: ["boardId"]
|
|
15722
|
+
},
|
|
15723
|
+
handler: async (args) => {
|
|
15724
|
+
const params = new URLSearchParams();
|
|
15725
|
+
if (typeof args.list === "string" && args.list)
|
|
15726
|
+
params.set("list", args.list);
|
|
15727
|
+
if (typeof args.assignee === "string" && args.assignee)
|
|
15728
|
+
params.set("assignee", args.assignee);
|
|
15729
|
+
if (Array.isArray(args.fields) && args.fields.length > 0)
|
|
15730
|
+
params.set("fields", args.fields.join(","));
|
|
15731
|
+
if (typeof args.limit === "number")
|
|
15732
|
+
params.set("limit", String(args.limit));
|
|
15733
|
+
if (typeof args.cursor === "string" && args.cursor)
|
|
15734
|
+
params.set("cursor", args.cursor);
|
|
15735
|
+
const qs = params.toString();
|
|
15736
|
+
return api(`/api/boards/${args.boardId}/cards${qs ? `?${qs}` : ""}`);
|
|
15737
|
+
}
|
|
15738
|
+
},
|
|
15704
15739
|
{
|
|
15705
15740
|
name: "smash_set_assignees",
|
|
15706
15741
|
description: "Set or clear a card's assignees (humans + external agents listed together). Pass assignees=null to clear. Each entry: {kind: 'human'|'agent', id: string, display_name: string, avatar_emoji?: string}. Extra keys are accepted for forward-compat.",
|
|
@@ -15753,7 +15788,7 @@ var tools = [
|
|
|
15753
15788
|
},
|
|
15754
15789
|
{
|
|
15755
15790
|
name: "smash_delete_card",
|
|
15756
|
-
description: "
|
|
15791
|
+
description: "Archive a card (soft delete). It leaves the board but is kept: it shows up in the space's archived column and smash_restore_card puts it back. Nothing is destroyed here \u2014 smash_permanently_delete_card is the irreversible one.",
|
|
15757
15792
|
inputSchema: {
|
|
15758
15793
|
type: "object",
|
|
15759
15794
|
properties: { cardId: { type: "string" } },
|
|
@@ -15761,26 +15796,64 @@ var tools = [
|
|
|
15761
15796
|
},
|
|
15762
15797
|
handler: async (args) => {
|
|
15763
15798
|
await api(`/api/cards/${args.cardId}`, { method: "DELETE" });
|
|
15764
|
-
return {
|
|
15799
|
+
return {
|
|
15800
|
+
ok: true,
|
|
15801
|
+
archived: true,
|
|
15802
|
+
note: `Archived, not destroyed. Restore with smash_restore_card({cardId: "${args.cardId}"}), or find it in the space's \u5B8C\u4E86\u6E08\u307F / Archive column.`
|
|
15803
|
+
};
|
|
15765
15804
|
}
|
|
15766
15805
|
},
|
|
15767
15806
|
{
|
|
15768
15807
|
name: "smash_add_comment",
|
|
15769
|
-
description: "Add a comment to a card. Markdown is supported.",
|
|
15808
|
+
description: "Add a comment to a card. Markdown is supported. Use `recipients` to address the comment to specific assignee ids (the same ids as smash_set_assignees), so the addressee can poll just its own messages with smash_list_new_comments. Agent-to-agent hand-offs belong on the card they are about \u2014 there is no separate channel, which keeps the exchange attached to the work.",
|
|
15770
15809
|
inputSchema: {
|
|
15771
15810
|
type: "object",
|
|
15772
15811
|
properties: {
|
|
15773
15812
|
cardId: { type: "string" },
|
|
15774
15813
|
body: { type: "string" },
|
|
15775
|
-
authorName: { type: "string" }
|
|
15814
|
+
authorName: { type: "string" },
|
|
15815
|
+
recipients: {
|
|
15816
|
+
type: "array",
|
|
15817
|
+
items: { type: "string" },
|
|
15818
|
+
description: "Assignee ids this comment is addressed to (e.g. ['claude-code'])"
|
|
15819
|
+
}
|
|
15776
15820
|
},
|
|
15777
15821
|
required: ["cardId", "body"]
|
|
15778
15822
|
},
|
|
15779
15823
|
handler: async (args) => api(`/api/cards/${args.cardId}/comments`, {
|
|
15780
15824
|
method: "POST",
|
|
15781
|
-
body: JSON.stringify({
|
|
15825
|
+
body: JSON.stringify({
|
|
15826
|
+
body: args.body,
|
|
15827
|
+
authorName: args.authorName,
|
|
15828
|
+
...Array.isArray(args.recipients) && args.recipients.length > 0 ? { recipients: args.recipients } : {}
|
|
15829
|
+
})
|
|
15782
15830
|
})
|
|
15783
15831
|
},
|
|
15832
|
+
{
|
|
15833
|
+
name: "smash_list_new_comments",
|
|
15834
|
+
description: "Read comments across a whole board in created order \u2014 an agent inbox. Pass `since` (an ISO timestamp, exclusive) to get only what you have not seen, and `to` to keep just the comments addressed to you. The result carries a `cursor`; store it and pass it as the next `since`. Prefer this over re-reading the board: it is one request and returns only new messages.",
|
|
15835
|
+
inputSchema: {
|
|
15836
|
+
type: "object",
|
|
15837
|
+
properties: {
|
|
15838
|
+
boardId: { type: "string" },
|
|
15839
|
+
since: { type: "string", description: "ISO timestamp; returns comments created after it" },
|
|
15840
|
+
to: { type: "string", description: "Only comments addressed to this assignee id" },
|
|
15841
|
+
limit: { type: "number", description: "Max comments to read (default 100, max 500)" }
|
|
15842
|
+
},
|
|
15843
|
+
required: ["boardId"]
|
|
15844
|
+
},
|
|
15845
|
+
handler: async (args) => {
|
|
15846
|
+
const params = new URLSearchParams();
|
|
15847
|
+
if (typeof args.since === "string" && args.since)
|
|
15848
|
+
params.set("since", args.since);
|
|
15849
|
+
if (typeof args.to === "string" && args.to)
|
|
15850
|
+
params.set("to", args.to);
|
|
15851
|
+
if (typeof args.limit === "number")
|
|
15852
|
+
params.set("limit", String(args.limit));
|
|
15853
|
+
const qs = params.toString();
|
|
15854
|
+
return api(`/api/boards/${args.boardId}/comments${qs ? `?${qs}` : ""}`);
|
|
15855
|
+
}
|
|
15856
|
+
},
|
|
15784
15857
|
{
|
|
15785
15858
|
name: "smash_add_checklist_item",
|
|
15786
15859
|
description: "Add a checklist item to a card.",
|