smashspace 0.1.6 → 0.1.8

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 (3) hide show
  1. package/README.md +17 -7
  2. package/dist/smash.mjs +80 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,11 +34,17 @@ server below. (Headless? use `smash login --token <PAT>`.)
34
34
 
35
35
  ## Onboard a project for agents: `smash init`
36
36
 
37
- Run once per repo to scaffold everything agents (Claude Code / Codex) need to
38
- operate SmashSpace as the task backbone — no manual copying:
37
+ **One project = one `smash init` = one space.** Run it once inside each repo, so
38
+ that project's agents (Claude Code / Codex) operate their own space:
39
39
 
40
40
  ```bash
41
- smash init --board-id b_xxxxxxxx
41
+ cd your/dir/project
42
+
43
+ # create a new space and pin it to this project
44
+ smash init --new-space "Your Project"
45
+
46
+ # …or pin an existing space
47
+ smash init --space-id s_xxxxxxxx
42
48
  ```
43
49
 
44
50
  It writes (and is safe to re-run — existing config is kept unless `--force`):
@@ -46,12 +52,16 @@ It writes (and is safe to re-run — existing config is kept unless `--force`):
46
52
  - `.claude/skills/smash/SKILL.md` + `.codex/skills/smash/SKILL.md` — the
47
53
  SmashSpace Operator playbook (CLI/MCP usage + Inbox→Todo→Doing→Review→Done
48
54
  workflow + safety rules). Always refreshed to the installed CLI version.
49
- - `.smashspace.json` — pins the instance + board.
55
+ - `.smashspace.json` — pins the instance + space (which space this dir talks to).
50
56
  - `.mcp.json` — wires the `smashspace` MCP server (merged, other servers kept).
51
57
 
52
- Flags: `--skills-only` (just the operator skill), `--no-mcp`, `--force`,
53
- `--board-id <id>`. Then `smash login` once on the machine and the agents are
54
- ready.
58
+ Plain `smash init` (no space flag) is safe: it installs the skill and writes a
59
+ `.smashspace.json` with a **placeholder** space id, then prints how to set a real
60
+ one (`--new-space` / `--space-id`). It never guesses or deletes anything.
61
+
62
+ Flags: `--new-space <name>`, `--space-id <id>` (`--board-id` is a legacy alias),
63
+ `--skills-only` (just the operator skill), `--no-mcp`, `--force`. Sign in once
64
+ with `smash login` first (`--new-space` needs it).
55
65
 
56
66
  ## Project config: `.smashspace.json`
57
67
 
package/dist/smash.mjs CHANGED
@@ -3599,7 +3599,11 @@ the target board for you.
3599
3599
  shim that fails with "Volta error: Could not execute command".
3600
3600
  - MCP server: \`smashspace-mcp\` (exposes \`smash_*\` tools, same token as the CLI)
3601
3601
  - Sign in once per machine: \`smash login\` (browser approval; token saved to
3602
- \`~/.config/smash/credentials.json\` and reused by MCP)
3602
+ \`~/.config/smash/credentials.json\` and reused by MCP). It is idempotent \u2014 if a
3603
+ valid token is already saved it does nothing, so don't re-run it in a loop.
3604
+ - If \`smash\` is on PATH, **call it directly** \u2014 do not prefix
3605
+ \`PATH=\u2026/node/\u2026/bin\`. A "Volta error: Could not execute command" means a stale
3606
+ install; fix it with \`volta install smashspace\` rather than PATH-hacking.
3603
3607
 
3604
3608
  ### Which board am I on?
3605
3609
 
@@ -3671,7 +3675,20 @@ Keep the card's agent state current while you work:
3671
3675
  }
3672
3676
  \`\`\`
3673
3677
 
3674
- Set \`status: "done"\` only after verification passes.
3678
+ Set \`status: "done"\` only after verification passes. Keep it current on
3679
+ meaningful moves/edits too (not just during todo-dev) so a human can see whose
3680
+ turn it is at a glance.
3681
+
3682
+ ## Batching (stay inside the CLI)
3683
+
3684
+ Prefer the CLI's own batch flags over dropping to a shell/node loop:
3685
+
3686
+ \`\`\`bash
3687
+ smash create "Title" --desc-file spec.md # multi-line description, no quoting pain
3688
+ smash card update <cardId> --desc-file - # description from stdin
3689
+ smash checklist add <cardId> --file steps.txt # one item per line (or many args)
3690
+ smash checklist add <cardId> "Step 1" "Step 2" "Step 3"
3691
+ \`\`\`
3675
3692
 
3676
3693
  ## Command: /smash list
3677
3694
 
@@ -3829,6 +3846,15 @@ function show(data, headerLines = []) {
3829
3846
  if (data !== void 0)
3830
3847
  console.log(JSON.stringify(data, null, 2));
3831
3848
  }
3849
+ async function readTextArg(pathOrDash) {
3850
+ if (pathOrDash === "-") {
3851
+ const chunks = [];
3852
+ for await (const chunk of stdin)
3853
+ chunks.push(chunk);
3854
+ return Buffer.concat(chunks).toString("utf8");
3855
+ }
3856
+ return readFile2(pathOrDash, "utf8");
3857
+ }
3832
3858
  function resolveBaseUrl(loaded = loadProjectConfig()) {
3833
3859
  if (program2.getOptionValueSource("baseUrl") === "cli")
3834
3860
  return getOpts().baseUrl;
@@ -4058,12 +4084,14 @@ ${detail.card.description}`);
4058
4084
  }
4059
4085
  }
4060
4086
  });
4061
- cardCmd.command("update <cardId>").description("Update card fields (title, description, due date, cover, checklist title)").option("--title <title>", "New title").option("--description <description>", "New description (markdown)").option("--due <iso>", "Due date as ISO string, or empty string to clear").option("--cover <attachmentId>", "Cover attachment ID, or empty string to clear").option("--checklist-title <title>", "Custom checklist section title, or empty string to clear").action(
4087
+ cardCmd.command("update <cardId>").description("Update card fields (title, description, due date, cover, checklist title)").option("--title <title>", "New title").option("--description <description>", "New description (markdown)").option("--desc-file <path>", "read description from a file (or - for stdin)").option("--due <iso>", "Due date as ISO string, or empty string to clear").option("--cover <attachmentId>", "Cover attachment ID, or empty string to clear").option("--checklist-title <title>", "Custom checklist section title, or empty string to clear").action(
4062
4088
  async (cardId, opts) => {
4063
4089
  const patch = {};
4064
4090
  if (opts.title !== void 0)
4065
4091
  patch.title = opts.title;
4066
- if (opts.description !== void 0)
4092
+ if (opts.descFile !== void 0)
4093
+ patch.description = await readTextArg(opts.descFile);
4094
+ else if (opts.description !== void 0)
4067
4095
  patch.description = opts.description;
4068
4096
  if (opts.due !== void 0)
4069
4097
  patch.dueAt = opts.due === "" ? null : opts.due;
@@ -4072,7 +4100,7 @@ cardCmd.command("update <cardId>").description("Update card fields (title, descr
4072
4100
  if (opts.checklistTitle !== void 0)
4073
4101
  patch.checklistTitle = opts.checklistTitle === "" ? null : opts.checklistTitle;
4074
4102
  if (Object.keys(patch).length === 0) {
4075
- console.error("Provide at least one of: --title, --description, --due, --cover, --checklist-title");
4103
+ console.error("Provide at least one of: --title, --description, --desc-file, --due, --cover, --checklist-title");
4076
4104
  process.exit(1);
4077
4105
  }
4078
4106
  const card = await api(`/api/cards/${cardId}`, {
@@ -4198,15 +4226,28 @@ archiveCmd.command("purge <cardId>").description("Permanently delete an archived
4198
4226
  console.log(`\u2713 card ${cardId} permanently deleted`);
4199
4227
  });
4200
4228
  var checklistCmd = program2.command("checklist").description("Card checklist items");
4201
- checklistCmd.command("add <cardId> <title>").description("Add a checklist item to a card").action(async (cardId, title) => {
4202
- const item = await api(
4203
- `/api/cards/${cardId}/checklist-items`,
4204
- {
4205
- method: "POST",
4206
- body: JSON.stringify({ title })
4207
- }
4208
- );
4209
- show(item, [`\u2713 checklist item ${item.id} added`]);
4229
+ checklistCmd.command("add <cardId> [titles...]").description("Add one or more checklist items (repeat titles, or --file with one per line)").option("--file <path>", "read items from a file, one per line (or - for stdin)").action(async (cardId, titles, opts) => {
4230
+ let items = titles ?? [];
4231
+ if (opts.file) {
4232
+ const text = await readTextArg(opts.file);
4233
+ items = items.concat(
4234
+ text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
4235
+ );
4236
+ }
4237
+ if (items.length === 0) {
4238
+ console.error("Provide at least one item title, or --file <path>");
4239
+ process.exit(1);
4240
+ }
4241
+ const created = [];
4242
+ for (const title of items) {
4243
+ created.push(
4244
+ await api(`/api/cards/${cardId}/checklist-items`, {
4245
+ method: "POST",
4246
+ body: JSON.stringify({ title })
4247
+ })
4248
+ );
4249
+ }
4250
+ show(created, [`\u2713 ${created.length} checklist item(s) added`]);
4210
4251
  });
4211
4252
  checklistCmd.command("toggle <itemId>").description("Toggle a checklist item to checked/unchecked").option("--check", "Mark as checked", false).option("--uncheck", "Mark as unchecked", false).action(
4212
4253
  async (itemId, opts) => {
@@ -4722,7 +4763,7 @@ program2.command("status").aliases(["context", "where"]).description(
4722
4763
  });
4723
4764
  program2.command("init").description(
4724
4765
  "Scaffold this project for agents: operator skill + .smashspace.json + .mcp.json"
4725
- ).option("--board-id <id>", "Board/space id to pin in .smashspace.json / .mcp.json").option("--skills-only", "Only (re)install the operator skill files").option("--no-mcp", "Skip writing .mcp.json").option("-f, --force", "Overwrite .smashspace.json / .mcp.json if they exist").action(
4766
+ ).option("--new-space <name>", "create a new space with this name, then pin it (needs login)").option("--space-id <id>", "existing space id to pin, e.g. s_xxxxxxxx").option("--board-id <id>", "alias for --space-id (legacy)").option("--skills-only", "Only (re)install the operator skill files").option("--no-mcp", "Skip writing .mcp.json").option("-f, --force", "Overwrite .smashspace.json / .mcp.json if they exist").action(
4726
4767
  async (opts) => {
4727
4768
  const cwd = process.cwd();
4728
4769
  const actions = [];
@@ -4747,15 +4788,26 @@ program2.command("init").description(
4747
4788
  "\u2713 operator skill \u2192 .claude/skills/smash/ + .codex/skills/smash/"
4748
4789
  );
4749
4790
  const baseUrl = resolveBaseUrl();
4750
- const boardId = opts.boardId ?? "b_xxxxxxxx";
4751
- const placeholder = boardId === "b_xxxxxxxx";
4791
+ let boardId;
4792
+ if (opts.newSpace) {
4793
+ const space = await api(
4794
+ "/api/boards",
4795
+ { method: "POST", body: JSON.stringify({ title: opts.newSpace }) },
4796
+ baseUrl
4797
+ );
4798
+ boardId = space.id;
4799
+ actions.push(`\u2713 created space "${space.title || opts.newSpace}" (${space.id})`);
4800
+ } else {
4801
+ boardId = opts.spaceId ?? opts.boardId ?? "s_xxxxxxxx";
4802
+ }
4803
+ const placeholder = boardId === "s_xxxxxxxx";
4752
4804
  if (!opts.skillsOnly) {
4753
4805
  if (await exists(CONFIG_FILENAME) && !opts.force) {
4754
4806
  actions.push(`\u2022 ${CONFIG_FILENAME} exists \u2014 kept (use --force)`);
4755
4807
  } else {
4756
4808
  await put(CONFIG_FILENAME, smashspaceJson(baseUrl, boardId));
4757
4809
  actions.push(
4758
- `\u2713 ${CONFIG_FILENAME}${placeholder ? " (placeholder space_id \u2014 edit it)" : ` (${boardId})`}`
4810
+ `\u2713 ${CONFIG_FILENAME}${placeholder ? " (placeholder space_id \u2014 set a real one below)" : ` \u2192 ${boardId}`}`
4759
4811
  );
4760
4812
  }
4761
4813
  if (opts.mcp !== false) {
@@ -4790,9 +4842,14 @@ program2.command("init").description(
4790
4842
  }
4791
4843
  }
4792
4844
  }
4793
- const next = ["", "Next:", " 1. smash login # sign in (browser approval)"];
4845
+ const next = ["", "Next:"];
4794
4846
  if (!opts.skillsOnly && placeholder) {
4795
- next.push(" 2. edit .smashspace.json # set your real space_id (`smash board list` to find it)");
4847
+ next.push(" This project isn't pinned to a space yet. Pick one:");
4848
+ next.push(' smash init --new-space "My Project" --force # create + pin a new space');
4849
+ next.push(" smash init --space-id s_xxxxxxxx --force # pin an existing space");
4850
+ next.push(" (find ids with: smash space list)");
4851
+ } else if (!opts.skillsOnly) {
4852
+ next.push(" smash login # if you haven't yet \u2014 then you're ready.");
4796
4853
  }
4797
4854
  next.push(
4798
4855
  " \u2192 Claude Code / Codex pick up .claude|.codex/skills/smash automatically"
@@ -4804,24 +4861,18 @@ program2.command("init").description(
4804
4861
  show(void 0, [...actions, ...next]);
4805
4862
  }
4806
4863
  );
4807
- program2.command("create <title>").description(`Create a card on the resolved board (${CONFIG_FILENAME}, default "main")`).option("-b, --board <label>", "board label from config").option("--board-id <id>", "explicit board id (bypass config)").option("-l, --list <name>", "target list name (default: first list)").option("-d, --desc <text>", "card description (Markdown)").action(async (title, opts) => {
4864
+ program2.command("create <title>").description(`Create a card on the resolved board (${CONFIG_FILENAME}, default "main")`).option("-b, --board <label>", "board label from config").option("--board-id <id>", "explicit board id (bypass config)").option("-l, --list <name>", "target list name (default: first list)").option("-d, --desc <text>", "card description (Markdown)").option("--desc-file <path>", "read description from a file (or - for stdin)").action(async (title, opts) => {
4808
4865
  const ctx = resolveContext(opts);
4809
4866
  const detail = await fetchBoard(ctx.boardId, ctx.baseUrl);
4810
4867
  const list = opts.list ? findListByName(detail, opts.list) : detail.lists[0];
4811
4868
  if (!list)
4812
4869
  throw new Error(`Board ${ctx.boardId} has no lists`);
4870
+ const description = opts.descFile ? await readTextArg(opts.descFile) : opts.desc;
4813
4871
  const card = await api(
4814
4872
  `/api/lists/${list.id}/cards`,
4815
- { method: "POST", body: JSON.stringify({ title }) },
4873
+ { method: "POST", body: JSON.stringify(description ? { title, description } : { title }) },
4816
4874
  ctx.baseUrl
4817
4875
  );
4818
- if (opts.desc) {
4819
- await api(
4820
- `/api/cards/${card.id}`,
4821
- { method: "PATCH", body: JSON.stringify({ description: opts.desc }) },
4822
- ctx.baseUrl
4823
- );
4824
- }
4825
4876
  show(card, [`\u2713 created ${card.id} "${title}" in ${list.title} (board ${ctx.entry.label})`]);
4826
4877
  });
4827
4878
  program2.command("read").description(`Show the resolved board \u2014 lists + cards (${CONFIG_FILENAME}, default "main")`).option("-b, --board <label>", "board label from config").option("--board-id <id>", "explicit board id (bypass config)").action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smashspace",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "SmashSpace CLI + MCP — operate SmashSpace boards from your terminal, Claude Code, Cursor, and Codex.",
5
5
  "type": "module",
6
6
  "bin": {