smashspace 0.4.0 → 0.7.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/dist/smash.mjs +110 -10
- package/dist/smashspace-mcp.mjs +71 -8
- package/package.json +1 -1
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."
|
|
@@ -4225,6 +4259,57 @@ cardCmd.command("comment <cardId> <body>").description("Add a comment to a card"
|
|
|
4225
4259
|
function collectRepeated(value, previous) {
|
|
4226
4260
|
return [...previous, value];
|
|
4227
4261
|
}
|
|
4262
|
+
program2.command("done <cardId>").description(
|
|
4263
|
+
"Mark a card done \u2014 moves it to the board's done list (it stays on the board). Not the same as `smash archive`, which takes it off."
|
|
4264
|
+
).action(async (cardId) => {
|
|
4265
|
+
const result = await api(`/api/cards/${cardId}/complete`, {
|
|
4266
|
+
method: "POST"
|
|
4267
|
+
});
|
|
4268
|
+
show(result.card, [`\u2713 card ${result.card.id} \u2192 ${result.listTitle}`]);
|
|
4269
|
+
});
|
|
4270
|
+
program2.command("cards").description(
|
|
4271
|
+
"List cards on the resolved board \u2014 light by default (no description). Filter by list/assignee, pick columns with --fields."
|
|
4272
|
+
).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("--status <status>", "open (skip cards in lists marked done) | done | all (default)").option(
|
|
4273
|
+
"--fields <list>",
|
|
4274
|
+
"comma-separated columns (default: id,title,listId,listTitle,updatedAt). Add description only when you need bodies."
|
|
4275
|
+
).option("--limit <n>", "max cards per page (default 100, max 500)").option("--cursor <cursor>", "continue from a previous response's cursor").action(async (opts) => {
|
|
4276
|
+
if (opts.status && !["open", "done", "all"].includes(opts.status)) {
|
|
4277
|
+
console.error("--status must be one of: open, done, all");
|
|
4278
|
+
process.exit(1);
|
|
4279
|
+
}
|
|
4280
|
+
const ctx = resolveContext(opts);
|
|
4281
|
+
const params = new URLSearchParams();
|
|
4282
|
+
if (opts.list)
|
|
4283
|
+
params.set("list", opts.list);
|
|
4284
|
+
if (opts.assignee)
|
|
4285
|
+
params.set("assignee", opts.assignee);
|
|
4286
|
+
if (opts.status)
|
|
4287
|
+
params.set("status", opts.status);
|
|
4288
|
+
if (opts.fields)
|
|
4289
|
+
params.set("fields", opts.fields);
|
|
4290
|
+
if (opts.limit)
|
|
4291
|
+
params.set("limit", opts.limit);
|
|
4292
|
+
if (opts.cursor)
|
|
4293
|
+
params.set("cursor", opts.cursor);
|
|
4294
|
+
const qs = params.toString();
|
|
4295
|
+
const result = await api(
|
|
4296
|
+
`/api/boards/${ctx.boardId}/cards${qs ? `?${qs}` : ""}`,
|
|
4297
|
+
void 0,
|
|
4298
|
+
ctx.baseUrl
|
|
4299
|
+
);
|
|
4300
|
+
if (getOpts().json) {
|
|
4301
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4302
|
+
return;
|
|
4303
|
+
}
|
|
4304
|
+
for (const card of result.cards) {
|
|
4305
|
+
const listTitle = typeof card.listTitle === "string" ? `[${card.listTitle}] ` : "";
|
|
4306
|
+
console.log(`${listTitle}${String(card.title ?? "")} (${String(card.id ?? "")})`);
|
|
4307
|
+
}
|
|
4308
|
+
console.log(
|
|
4309
|
+
`
|
|
4310
|
+
${result.cards.length} of ${result.total}${result.cursor ? ` next: --cursor ${result.cursor}` : ""}`
|
|
4311
|
+
);
|
|
4312
|
+
});
|
|
4228
4313
|
program2.command("inbox").description(
|
|
4229
4314
|
`New comments across the resolved board (${CONFIG_FILENAME}). Poll with --since <cursor> to read only what you have not seen.`
|
|
4230
4315
|
).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) => {
|
|
@@ -4493,6 +4578,13 @@ tokenCmd.command("revoke <boardId> <tokenId>").description("Revoke a write token
|
|
|
4493
4578
|
);
|
|
4494
4579
|
show(revoked, [`\u2713 token ${revoked.id} revoked at ${revoked.revokedAt}`]);
|
|
4495
4580
|
});
|
|
4581
|
+
listCmd.command("done <listId>").description("Mark a list as done (cards in it are excluded by `smash cards --status open`)").option("--no-done", "unmark it instead").action(async (listId, opts) => {
|
|
4582
|
+
const list = await api(`/api/lists/${listId}`, {
|
|
4583
|
+
method: "PATCH",
|
|
4584
|
+
body: JSON.stringify({ isDone: opts.done })
|
|
4585
|
+
});
|
|
4586
|
+
show(list, [`\u2713 list ${list.id} (${list.title}) isDone=${list.isDone}`]);
|
|
4587
|
+
});
|
|
4496
4588
|
var labelCmd = program2.command("label").description("Labels");
|
|
4497
4589
|
labelCmd.command("create <boardId> <name>").description("Create a board label").option(
|
|
4498
4590
|
"--color <color>",
|
|
@@ -4698,23 +4790,31 @@ assigneesCmd.command("add <cardId>").description("Append a single assignee (read
|
|
|
4698
4790
|
console.error("--name must not be empty");
|
|
4699
4791
|
process.exit(1);
|
|
4700
4792
|
}
|
|
4701
|
-
const id = opts.id ?? `${opts.kind}_${Math.random().toString(36).slice(2, 10)}`;
|
|
4702
4793
|
const detail = await api(`/api/cards/${cardId}`);
|
|
4703
4794
|
const current = detail.card.assignees ?? [];
|
|
4704
|
-
const
|
|
4795
|
+
const board = await api(
|
|
4796
|
+
`/api/boards/${detail.card.boardId}/cards?fields=assignees&limit=500`
|
|
4797
|
+
);
|
|
4798
|
+
const known = [
|
|
4705
4799
|
...current,
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
display_name: opts.name,
|
|
4710
|
-
...opts.emoji ? { avatar_emoji: opts.emoji } : {}
|
|
4711
|
-
}
|
|
4800
|
+
...board.cards.flatMap(
|
|
4801
|
+
(c) => Array.isArray(c.assignees) ? c.assignees : []
|
|
4802
|
+
)
|
|
4712
4803
|
];
|
|
4804
|
+
const entry = resolveAssignee(known, {
|
|
4805
|
+
kind: opts.kind,
|
|
4806
|
+
name: opts.name,
|
|
4807
|
+
explicitId: opts.id,
|
|
4808
|
+
emoji: opts.emoji
|
|
4809
|
+
});
|
|
4810
|
+
const next = [...current, entry];
|
|
4713
4811
|
const card = await api(`/api/cards/${cardId}`, {
|
|
4714
4812
|
method: "PATCH",
|
|
4715
4813
|
body: JSON.stringify({ assignees: next })
|
|
4716
4814
|
});
|
|
4717
|
-
show(card, [
|
|
4815
|
+
show(card, [
|
|
4816
|
+
`\u2713 card ${card.id} assignees now has ${next.length} entries (${entry.display_name} \u2192 ${entry.id})`
|
|
4817
|
+
]);
|
|
4718
4818
|
});
|
|
4719
4819
|
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) => {
|
|
4720
4820
|
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: {
|
|
@@ -15553,18 +15553,25 @@ var tools = [
|
|
|
15553
15553
|
},
|
|
15554
15554
|
{
|
|
15555
15555
|
name: "smash_rename_list",
|
|
15556
|
-
description: "Rename a list.",
|
|
15556
|
+
description: "Rename a list, and/or mark it as a done list. Cards in a list with isDone=true are excluded by smash_list_cards({status: 'open'}), so an agent can ask for open work without knowing the board's list names.",
|
|
15557
15557
|
inputSchema: {
|
|
15558
15558
|
type: "object",
|
|
15559
15559
|
properties: {
|
|
15560
15560
|
listId: { type: "string" },
|
|
15561
|
-
title: { type: "string" }
|
|
15561
|
+
title: { type: "string" },
|
|
15562
|
+
isDone: {
|
|
15563
|
+
type: "boolean",
|
|
15564
|
+
description: "Treat cards in this list as done"
|
|
15565
|
+
}
|
|
15562
15566
|
},
|
|
15563
|
-
required: ["listId"
|
|
15567
|
+
required: ["listId"]
|
|
15564
15568
|
},
|
|
15565
15569
|
handler: async (args) => api(`/api/lists/${args.listId}`, {
|
|
15566
15570
|
method: "PATCH",
|
|
15567
|
-
body: JSON.stringify({
|
|
15571
|
+
body: JSON.stringify({
|
|
15572
|
+
...typeof args.title === "string" ? { title: args.title } : {},
|
|
15573
|
+
...typeof args.isDone === "boolean" ? { isDone: args.isDone } : {}
|
|
15574
|
+
})
|
|
15568
15575
|
})
|
|
15569
15576
|
},
|
|
15570
15577
|
{
|
|
@@ -15653,7 +15660,7 @@ var tools = [
|
|
|
15653
15660
|
},
|
|
15654
15661
|
{
|
|
15655
15662
|
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.",
|
|
15663
|
+
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
15664
|
inputSchema: {
|
|
15658
15665
|
type: "object",
|
|
15659
15666
|
properties: {
|
|
@@ -15701,6 +15708,48 @@ var tools = [
|
|
|
15701
15708
|
body: JSON.stringify({ externalRefs: args.externalRefs ?? null })
|
|
15702
15709
|
})
|
|
15703
15710
|
},
|
|
15711
|
+
{
|
|
15712
|
+
name: "smash_list_cards",
|
|
15713
|
+
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`.",
|
|
15714
|
+
inputSchema: {
|
|
15715
|
+
type: "object",
|
|
15716
|
+
properties: {
|
|
15717
|
+
boardId: { type: "string" },
|
|
15718
|
+
list: { type: "string", description: "List name (case-insensitive) or list id" },
|
|
15719
|
+
assignee: { type: "string", description: "Assignee id or display name" },
|
|
15720
|
+
status: {
|
|
15721
|
+
type: "string",
|
|
15722
|
+
enum: ["open", "done", "all"],
|
|
15723
|
+
description: "open skips cards sitting in a list marked done (a flag on the list, not a name match); done keeps only those. Default all."
|
|
15724
|
+
},
|
|
15725
|
+
fields: {
|
|
15726
|
+
type: "array",
|
|
15727
|
+
items: { type: "string" },
|
|
15728
|
+
description: "Columns to return: id, boardId, title, description, listId, listTitle, position, dueAt, labelIds, checklistTitle, agentMeta, externalRefs, assignees, customData, createdAt, updatedAt"
|
|
15729
|
+
},
|
|
15730
|
+
limit: { type: "number", description: "Max cards per page (default 100, max 500)" },
|
|
15731
|
+
cursor: { type: "string", description: "Cursor from a previous response" }
|
|
15732
|
+
},
|
|
15733
|
+
required: ["boardId"]
|
|
15734
|
+
},
|
|
15735
|
+
handler: async (args) => {
|
|
15736
|
+
const params = new URLSearchParams();
|
|
15737
|
+
if (typeof args.list === "string" && args.list)
|
|
15738
|
+
params.set("list", args.list);
|
|
15739
|
+
if (typeof args.assignee === "string" && args.assignee)
|
|
15740
|
+
params.set("assignee", args.assignee);
|
|
15741
|
+
if (typeof args.status === "string" && args.status)
|
|
15742
|
+
params.set("status", args.status);
|
|
15743
|
+
if (Array.isArray(args.fields) && args.fields.length > 0)
|
|
15744
|
+
params.set("fields", args.fields.join(","));
|
|
15745
|
+
if (typeof args.limit === "number")
|
|
15746
|
+
params.set("limit", String(args.limit));
|
|
15747
|
+
if (typeof args.cursor === "string" && args.cursor)
|
|
15748
|
+
params.set("cursor", args.cursor);
|
|
15749
|
+
const qs = params.toString();
|
|
15750
|
+
return api(`/api/boards/${args.boardId}/cards${qs ? `?${qs}` : ""}`);
|
|
15751
|
+
}
|
|
15752
|
+
},
|
|
15704
15753
|
{
|
|
15705
15754
|
name: "smash_set_assignees",
|
|
15706
15755
|
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.",
|
|
@@ -15751,9 +15800,19 @@ var tools = [
|
|
|
15751
15800
|
})
|
|
15752
15801
|
})
|
|
15753
15802
|
},
|
|
15803
|
+
{
|
|
15804
|
+
name: "smash_complete_card",
|
|
15805
|
+
description: "Mark a card done: it moves to the list flagged as done on its board and STAYS on the board, where people can see it. Use this when work finished. smash_delete_card is different \u2014 that archives the card off the board. Fails with a message when no list is marked done yet (mark one with smash_rename_list({listId, isDone: true})).",
|
|
15806
|
+
inputSchema: {
|
|
15807
|
+
type: "object",
|
|
15808
|
+
properties: { cardId: { type: "string" } },
|
|
15809
|
+
required: ["cardId"]
|
|
15810
|
+
},
|
|
15811
|
+
handler: async (args) => api(`/api/cards/${args.cardId}/complete`, { method: "POST" })
|
|
15812
|
+
},
|
|
15754
15813
|
{
|
|
15755
15814
|
name: "smash_delete_card",
|
|
15756
|
-
description: "
|
|
15815
|
+
description: "Archive a card (soft delete) \u2014 NOT the same as marking it done. 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
15816
|
inputSchema: {
|
|
15758
15817
|
type: "object",
|
|
15759
15818
|
properties: { cardId: { type: "string" } },
|
|
@@ -15761,7 +15820,11 @@ var tools = [
|
|
|
15761
15820
|
},
|
|
15762
15821
|
handler: async (args) => {
|
|
15763
15822
|
await api(`/api/cards/${args.cardId}`, { method: "DELETE" });
|
|
15764
|
-
return {
|
|
15823
|
+
return {
|
|
15824
|
+
ok: true,
|
|
15825
|
+
archived: true,
|
|
15826
|
+
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.`
|
|
15827
|
+
};
|
|
15765
15828
|
}
|
|
15766
15829
|
},
|
|
15767
15830
|
{
|