smashspace 0.3.0 → 0.4.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 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
@@ -4202,15 +4202,64 @@ cardCmd.command("search <query>").description(
4202
4202
  console.log(`
4203
4203
  ${allHits.length} card(s) found`);
4204
4204
  });
4205
- cardCmd.command("comment <cardId> <body>").description("Add a comment to a card").option("--author <name>", "Author name").action(
4205
+ cardCmd.command("comment <cardId> <body>").description("Add a comment to a card").option("--author <name>", "Author name").option(
4206
+ "--to <id>",
4207
+ "Address the comment to an assignee id (repeatable). Agents poll their own with `smash inbox --to <id>`.",
4208
+ collectRepeated,
4209
+ []
4210
+ ).action(
4206
4211
  async (cardId, body, opts) => {
4207
4212
  const comment = await api(`/api/cards/${cardId}/comments`, {
4208
4213
  method: "POST",
4209
- body: JSON.stringify({ body, authorName: opts.author })
4214
+ body: JSON.stringify({
4215
+ body,
4216
+ authorName: opts.author,
4217
+ ...opts.to.length > 0 ? { recipients: opts.to } : {}
4218
+ })
4210
4219
  });
4211
- show(comment, [`\u2713 comment ${comment.id} added`]);
4220
+ show(comment, [
4221
+ `\u2713 comment ${comment.id} added${comment.recipients?.length ? ` \u2192 ${comment.recipients.join(", ")}` : ""}`
4222
+ ]);
4212
4223
  }
4213
4224
  );
4225
+ function collectRepeated(value, previous) {
4226
+ return [...previous, value];
4227
+ }
4228
+ program2.command("inbox").description(
4229
+ `New comments across the resolved board (${CONFIG_FILENAME}). Poll with --since <cursor> to read only what you have not seen.`
4230
+ ).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) => {
4231
+ const ctx = resolveContext(opts);
4232
+ const params = new URLSearchParams();
4233
+ if (opts.since)
4234
+ params.set("since", opts.since);
4235
+ if (opts.to)
4236
+ params.set("to", opts.to);
4237
+ if (opts.limit)
4238
+ params.set("limit", opts.limit);
4239
+ const qs = params.toString();
4240
+ const result = await api(
4241
+ `/api/boards/${ctx.boardId}/comments${qs ? `?${qs}` : ""}`,
4242
+ void 0,
4243
+ ctx.baseUrl
4244
+ );
4245
+ if (getOpts().json) {
4246
+ console.log(JSON.stringify(result, null, 2));
4247
+ return;
4248
+ }
4249
+ if (result.comments.length === 0) {
4250
+ console.log(`(no new comments)${result.cursor ? ` cursor: ${result.cursor}` : ""}`);
4251
+ return;
4252
+ }
4253
+ for (const c of result.comments) {
4254
+ const to = c.recipients?.length ? ` \u2192 ${c.recipients.join(", ")}` : "";
4255
+ console.log(
4256
+ `[${c.createdAt}] ${c.authorName ?? "anon"}${to} (${c.listTitle} / ${c.cardTitle} \xB7 ${c.cardId})`
4257
+ );
4258
+ console.log(` ${c.body.replace(/\n/g, "\n ")}`);
4259
+ }
4260
+ console.log(`
4261
+ cursor: ${result.cursor}`);
4262
+ });
4214
4263
  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
4264
  if (!cardId) {
4216
4265
  archiveCmd.help();
@@ -4278,12 +4327,18 @@ checklistCmd.command("delete <itemId>").description("Delete a checklist item").a
4278
4327
  console.log(`\u2713 checklist item ${itemId} deleted`);
4279
4328
  });
4280
4329
  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) => {
4330
+ 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
4331
  const comment = await api(`/api/cards/${cardId}/comments`, {
4283
4332
  method: "POST",
4284
- body: JSON.stringify({ body, authorName: opts.author })
4333
+ body: JSON.stringify({
4334
+ body,
4335
+ authorName: opts.author,
4336
+ ...opts.to.length > 0 ? { recipients: opts.to } : {}
4337
+ })
4285
4338
  });
4286
- show(comment, [`\u2713 comment ${comment.id} added`]);
4339
+ show(comment, [
4340
+ `\u2713 comment ${comment.id} added${comment.recipients?.length ? ` \u2192 ${comment.recipients.join(", ")}` : ""}`
4341
+ ]);
4287
4342
  });
4288
4343
  commentCmd.command("edit <commentId> <body>").description("Edit (replace body of) an existing comment").action(async (commentId, body) => {
4289
4344
  const comment = await api(`/api/comments/${commentId}`, {
@@ -4396,6 +4451,48 @@ attachmentCmd.command("delete <attachmentId>").description("Delete an attachment
4396
4451
  await api(`/api/attachments/${attachmentId}`, { method: "DELETE" });
4397
4452
  console.log(`\u2713 attachment ${attachmentId} deleted`);
4398
4453
  });
4454
+ var tokenCmd = program2.command("token").description("Board-scoped write tokens for external services (POST /api/ingest)");
4455
+ tokenCmd.command("create <boardId> <name>").description("Issue a write-only token for one board. The token is shown once.").action(async (boardId, name) => {
4456
+ const created = await api(
4457
+ `/api/boards/${boardId}/ingest-tokens`,
4458
+ { method: "POST", body: JSON.stringify({ name }) }
4459
+ );
4460
+ show(created, [
4461
+ `\u2713 token ${created.id} created for ${boardId}`,
4462
+ "",
4463
+ created.token,
4464
+ "",
4465
+ "Store it now \u2014 it is not shown again. It can only create cards and",
4466
+ "comments on this board:",
4467
+ ` curl -X POST ${resolveBaseUrl()}/api/ingest \\`,
4468
+ ` -H "Authorization: Bearer ${created.token}" \\`,
4469
+ ' -H "Content-Type: application/json" \\',
4470
+ ` -d '{"list":"\u3084\u308B\u3053\u3068","title":"\u2026"}'`
4471
+ ]);
4472
+ });
4473
+ tokenCmd.command("list <boardId>").description("List a board's write tokens (prefix, created, last used)").action(async (boardId) => {
4474
+ const items = await api(`/api/boards/${boardId}/ingest-tokens`);
4475
+ if (getOpts().json) {
4476
+ console.log(JSON.stringify(items, null, 2));
4477
+ return;
4478
+ }
4479
+ if (items.length === 0) {
4480
+ console.log("(no tokens)");
4481
+ return;
4482
+ }
4483
+ for (const t of items) {
4484
+ console.log(
4485
+ `${t.id} ${t.name} ${t.tokenPrefix}\u2026 created ${t.createdAt} last used ${t.lastUsedAt ?? "never"}`
4486
+ );
4487
+ }
4488
+ });
4489
+ tokenCmd.command("revoke <boardId> <tokenId>").description("Revoke a write token (takes effect immediately)").action(async (boardId, tokenId) => {
4490
+ const revoked = await api(
4491
+ `/api/boards/${boardId}/ingest-tokens/${tokenId}`,
4492
+ { method: "DELETE" }
4493
+ );
4494
+ show(revoked, [`\u2713 token ${revoked.id} revoked at ${revoked.revokedAt}`]);
4495
+ });
4399
4496
  var labelCmd = program2.command("label").description("Labels");
4400
4497
  labelCmd.command("create <boardId> <name>").description("Create a board label").option(
4401
4498
  "--color <color>",
@@ -15766,21 +15766,55 @@ var tools = [
15766
15766
  },
15767
15767
  {
15768
15768
  name: "smash_add_comment",
15769
- description: "Add a comment to a card. Markdown is supported.",
15769
+ 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
15770
  inputSchema: {
15771
15771
  type: "object",
15772
15772
  properties: {
15773
15773
  cardId: { type: "string" },
15774
15774
  body: { type: "string" },
15775
- authorName: { type: "string" }
15775
+ authorName: { type: "string" },
15776
+ recipients: {
15777
+ type: "array",
15778
+ items: { type: "string" },
15779
+ description: "Assignee ids this comment is addressed to (e.g. ['claude-code'])"
15780
+ }
15776
15781
  },
15777
15782
  required: ["cardId", "body"]
15778
15783
  },
15779
15784
  handler: async (args) => api(`/api/cards/${args.cardId}/comments`, {
15780
15785
  method: "POST",
15781
- body: JSON.stringify({ body: args.body, authorName: args.authorName })
15786
+ body: JSON.stringify({
15787
+ body: args.body,
15788
+ authorName: args.authorName,
15789
+ ...Array.isArray(args.recipients) && args.recipients.length > 0 ? { recipients: args.recipients } : {}
15790
+ })
15782
15791
  })
15783
15792
  },
15793
+ {
15794
+ name: "smash_list_new_comments",
15795
+ 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.",
15796
+ inputSchema: {
15797
+ type: "object",
15798
+ properties: {
15799
+ boardId: { type: "string" },
15800
+ since: { type: "string", description: "ISO timestamp; returns comments created after it" },
15801
+ to: { type: "string", description: "Only comments addressed to this assignee id" },
15802
+ limit: { type: "number", description: "Max comments to read (default 100, max 500)" }
15803
+ },
15804
+ required: ["boardId"]
15805
+ },
15806
+ handler: async (args) => {
15807
+ const params = new URLSearchParams();
15808
+ if (typeof args.since === "string" && args.since)
15809
+ params.set("since", args.since);
15810
+ if (typeof args.to === "string" && args.to)
15811
+ params.set("to", args.to);
15812
+ if (typeof args.limit === "number")
15813
+ params.set("limit", String(args.limit));
15814
+ const qs = params.toString();
15815
+ return api(`/api/boards/${args.boardId}/comments${qs ? `?${qs}` : ""}`);
15816
+ }
15817
+ },
15784
15818
  {
15785
15819
  name: "smash_add_checklist_item",
15786
15820
  description: "Add a checklist item to a card.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smashspace",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SmashSpace CLI + MCP — operate SmashSpace boards from your terminal, Claude Code, Cursor, and Codex.",
5
5
  "type": "module",
6
6
  "bin": {