smashspace 0.1.8 → 0.2.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 +18 -6
- package/dist/smash.mjs +116 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,11 +55,13 @@ It writes (and is safe to re-run — existing config is kept unless `--force`):
|
|
|
55
55
|
- `.smashspace.json` — pins the instance + space (which space this dir talks to).
|
|
56
56
|
- `.mcp.json` — wires the `smashspace` MCP server (merged, other servers kept).
|
|
57
57
|
|
|
58
|
-
Plain `smash init` (no space flag)
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
Plain `smash init` (no space flag) doesn't guess: it prints how to specify a
|
|
59
|
+
space (`--new-space` / `--space-id`) and exits without writing anything. Same,
|
|
60
|
+
predictable behaviour in a terminal, an agent, or CI — nothing hangs, nothing is
|
|
61
|
+
written half-way. (`--skills-only` installs just the skill, no space needed.)
|
|
61
62
|
|
|
62
|
-
Flags: `--new-space <name>`, `--space-id <id>` (
|
|
63
|
+
Flags: `--new-space <name>`, `--space-id <id-or-url>` (accepts a full
|
|
64
|
+
`https://…/space/s_xxx` URL too; `--board-id` is a legacy alias),
|
|
63
65
|
`--skills-only` (just the operator skill), `--no-mcp`, `--force`. Sign in once
|
|
64
66
|
with `smash login` first (`--new-space` needs it).
|
|
65
67
|
|
|
@@ -83,13 +85,23 @@ prefer, so `smash` knows which board to operate on:
|
|
|
83
85
|
```bash
|
|
84
86
|
smash status # what context is this directory in? (see below)
|
|
85
87
|
smash read # show the board (lists + cards)
|
|
86
|
-
smash scan --list "やること" #
|
|
87
|
-
smash
|
|
88
|
+
smash scan --list "やること" # cards as JSON (id/title/list only)
|
|
89
|
+
smash scan --list "やること" --full # + description/agentMeta/labels — pick up work in ONE request
|
|
90
|
+
smash create "Fix signup bug" # add a card (to the first list, or --list; --desc for a body)
|
|
88
91
|
smash move <cardId> "レビュー" # move a card to a list by name
|
|
89
92
|
smash archive <cardId> # archive (soft-delete) a card
|
|
90
93
|
smash --help # full command list (board/list/card/…)
|
|
91
94
|
```
|
|
92
95
|
|
|
96
|
+
Output is a concise line by default; add `--json` for machine-readable output on
|
|
97
|
+
any command. For agents:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
smash scan --full # or --fields description,agentMeta
|
|
101
|
+
smash agent-meta set <cardId> --set status=working --set progress=0.4 --merge
|
|
102
|
+
smash card update <cardId> --desc-file spec.md # multi-line body from a file / - (stdin)
|
|
103
|
+
```
|
|
104
|
+
|
|
93
105
|
Pick a board with `-b <label>` (defaults to `main`), or bypass config with
|
|
94
106
|
`--board-id <id>`.
|
|
95
107
|
|
package/dist/smash.mjs
CHANGED
|
@@ -3811,7 +3811,10 @@ function mcpServerEntry(baseUrl, boardId) {
|
|
|
3811
3811
|
// ../../cli/index.ts
|
|
3812
3812
|
var DEFAULT_BASE_URL = process.env.SMASH_BASE_URL ?? "https://smashspace.app";
|
|
3813
3813
|
var program2 = new Command();
|
|
3814
|
-
program2.name("smash").description("SmashSpace CLI \u2014 Trello as a spreadsheet for the AI agent era").option("--base-url <url>", "API base URL", DEFAULT_BASE_URL).option("--json", "Output raw JSON", false)
|
|
3814
|
+
program2.name("smash").description("SmashSpace CLI \u2014 Trello as a spreadsheet for the AI agent era").option("--base-url <url>", "API base URL", DEFAULT_BASE_URL).option("--json", "Output raw JSON", false).showSuggestionAfterError().addHelpText(
|
|
3815
|
+
"after",
|
|
3816
|
+
'\nTop-level verbs (create/read/move/scan/archive) act on the space resolved\nfrom .smashspace.json (default "main"). The card/board/list groups take\nexplicit ids. Default output is a concise line; add --json for machine-readable.'
|
|
3817
|
+
);
|
|
3815
3818
|
function getOpts() {
|
|
3816
3819
|
const o = program2.opts();
|
|
3817
3820
|
return { baseUrl: o.baseUrl ?? DEFAULT_BASE_URL, json: !!o.json };
|
|
@@ -3841,8 +3844,11 @@ function show(data, headerLines = []) {
|
|
|
3841
3844
|
console.log(JSON.stringify(data, null, 2));
|
|
3842
3845
|
return;
|
|
3843
3846
|
}
|
|
3844
|
-
|
|
3845
|
-
|
|
3847
|
+
if (headerLines.length > 0) {
|
|
3848
|
+
for (const l of headerLines)
|
|
3849
|
+
console.log(l);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3846
3852
|
if (data !== void 0)
|
|
3847
3853
|
console.log(JSON.stringify(data, null, 2));
|
|
3848
3854
|
}
|
|
@@ -4060,7 +4066,7 @@ cardCmd.command("create <listId> <title>").description("Create a card in a list"
|
|
|
4060
4066
|
});
|
|
4061
4067
|
show(card, [`\u2713 card ${card.id} created in ${listId}`]);
|
|
4062
4068
|
});
|
|
4063
|
-
cardCmd.command("show <cardId>").description("Show card detail (description, checklist, comments, activity)").action(async (cardId) => {
|
|
4069
|
+
cardCmd.command("show <cardId>").alias("get").description("Show card detail (description, checklist, comments, activity)").action(async (cardId) => {
|
|
4064
4070
|
const detail = await api(`/api/cards/${cardId}`);
|
|
4065
4071
|
if (getOpts().json) {
|
|
4066
4072
|
console.log(JSON.stringify(detail, null, 2));
|
|
@@ -4084,15 +4090,16 @@ ${detail.card.description}`);
|
|
|
4084
4090
|
}
|
|
4085
4091
|
}
|
|
4086
4092
|
});
|
|
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(
|
|
4093
|
+
cardCmd.command("update <cardId>").description("Update card fields (title, description, due date, cover, checklist title)").option("--title <title>", "New title").option("-d, --description <description>", "New description (markdown)").option("--desc <description>", "alias for --description").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(
|
|
4088
4094
|
async (cardId, opts) => {
|
|
4089
4095
|
const patch = {};
|
|
4090
4096
|
if (opts.title !== void 0)
|
|
4091
4097
|
patch.title = opts.title;
|
|
4098
|
+
const desc = opts.description ?? opts.desc;
|
|
4092
4099
|
if (opts.descFile !== void 0)
|
|
4093
4100
|
patch.description = await readTextArg(opts.descFile);
|
|
4094
|
-
else if (
|
|
4095
|
-
patch.description =
|
|
4101
|
+
else if (desc !== void 0)
|
|
4102
|
+
patch.description = desc;
|
|
4096
4103
|
if (opts.due !== void 0)
|
|
4097
4104
|
patch.dueAt = opts.due === "" ? null : opts.due;
|
|
4098
4105
|
if (opts.cover !== void 0)
|
|
@@ -4393,29 +4400,57 @@ labelCmd.command("delete <labelId>").description("Delete a label from the board"
|
|
|
4393
4400
|
console.log(`\u2713 label ${labelId} deleted`);
|
|
4394
4401
|
});
|
|
4395
4402
|
var agentMetaCmd = program2.command("agent-meta").description("Set or clear a card's agent_meta (structured field for external agents)");
|
|
4396
|
-
agentMetaCmd.command("set <cardId>").description("Set agent_meta on a card.
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
process.exit(1);
|
|
4401
|
-
} else if (opts.data !== void 0) {
|
|
4402
|
-
raw = opts.data;
|
|
4403
|
-
} else if (opts.file !== void 0) {
|
|
4404
|
-
raw = opts.file === "-" ? await new Response(process.stdin).text() : await readFile2(opts.file, "utf8");
|
|
4405
|
-
} else {
|
|
4406
|
-
console.error("Provide --data '<...>' or --file <path>");
|
|
4407
|
-
process.exit(1);
|
|
4408
|
-
}
|
|
4403
|
+
agentMetaCmd.command("set <cardId>").description("Set agent_meta on a card. Use --set key=value (repeatable), or --data/--file for full JSON.").option("--set <pair>", "key=value (repeatable; value parsed as JSON if valid, else string)", (v, acc) => {
|
|
4404
|
+
acc.push(v);
|
|
4405
|
+
return acc;
|
|
4406
|
+
}, []).option("--merge", "merge into the existing agent_meta instead of replacing it").option("--data <json>", "Inline JSON string (full object)").option("--file <path>", "Read JSON from a file path (use - for stdin)").action(async (cardId, opts) => {
|
|
4409
4407
|
let parsed;
|
|
4410
|
-
|
|
4411
|
-
parsed =
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4408
|
+
if (opts.set.length > 0) {
|
|
4409
|
+
parsed = {};
|
|
4410
|
+
for (const pair of opts.set) {
|
|
4411
|
+
const i = pair.indexOf("=");
|
|
4412
|
+
if (i < 0) {
|
|
4413
|
+
console.error(`--set expects key=value, got: ${pair}`);
|
|
4414
|
+
process.exit(1);
|
|
4415
|
+
}
|
|
4416
|
+
const key = pair.slice(0, i);
|
|
4417
|
+
const rawVal = pair.slice(i + 1);
|
|
4418
|
+
try {
|
|
4419
|
+
parsed[key] = JSON.parse(rawVal);
|
|
4420
|
+
} catch {
|
|
4421
|
+
parsed[key] = rawVal;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
} else {
|
|
4425
|
+
let raw;
|
|
4426
|
+
if (opts.data && opts.file) {
|
|
4427
|
+
console.error("Provide --set, --data, or --file (not more than one source)");
|
|
4428
|
+
process.exit(1);
|
|
4429
|
+
} else if (opts.data !== void 0) {
|
|
4430
|
+
raw = opts.data;
|
|
4431
|
+
} else if (opts.file !== void 0) {
|
|
4432
|
+
raw = opts.file === "-" ? await new Response(process.stdin).text() : await readFile2(opts.file, "utf8");
|
|
4433
|
+
} else {
|
|
4434
|
+
console.error("Provide --set key=value, --data '<json>', or --file <path>");
|
|
4435
|
+
process.exit(1);
|
|
4436
|
+
}
|
|
4437
|
+
let p;
|
|
4438
|
+
try {
|
|
4439
|
+
p = JSON.parse(raw);
|
|
4440
|
+
} catch (e) {
|
|
4441
|
+
console.error(`Invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
4442
|
+
process.exit(1);
|
|
4443
|
+
}
|
|
4444
|
+
if (typeof p !== "object" || p === null || Array.isArray(p)) {
|
|
4445
|
+
console.error("agent_meta must be a JSON object");
|
|
4446
|
+
process.exit(1);
|
|
4447
|
+
}
|
|
4448
|
+
parsed = p;
|
|
4415
4449
|
}
|
|
4416
|
-
if (
|
|
4417
|
-
|
|
4418
|
-
|
|
4450
|
+
if (opts.merge) {
|
|
4451
|
+
const detail = await api(`/api/cards/${cardId}`);
|
|
4452
|
+
const existing = detail.card.agentMeta ?? {};
|
|
4453
|
+
parsed = { ...existing, ...parsed };
|
|
4419
4454
|
}
|
|
4420
4455
|
const card = await api(`/api/cards/${cardId}`, {
|
|
4421
4456
|
method: "PATCH",
|
|
@@ -4781,34 +4816,47 @@ program2.command("init").description(
|
|
|
4781
4816
|
return false;
|
|
4782
4817
|
}
|
|
4783
4818
|
};
|
|
4819
|
+
const baseUrl = resolveBaseUrl();
|
|
4820
|
+
if (!opts.skillsOnly && !opts.newSpace && !opts.spaceId && !opts.boardId) {
|
|
4821
|
+
console.error(
|
|
4822
|
+
[
|
|
4823
|
+
"\u30B9\u30DA\u30FC\u30B9\u306E\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002\u6B21\u306E\u3069\u3061\u3089\u304B\u3092\u4ED8\u3051\u3066\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044:",
|
|
4824
|
+
"",
|
|
4825
|
+
' smash init --new-space "Your Project" # \u65B0\u3057\u304F\u4F5C\u3063\u3066\u7D50\u3073\u3064\u3051\u308B',
|
|
4826
|
+
" smash init --space-id s_xxxxxxxx # \u65E2\u5B58\u306E\u30B9\u30DA\u30FC\u30B9\u306B\u7D50\u3073\u3064\u3051\u308B",
|
|
4827
|
+
"",
|
|
4828
|
+
"\u65E2\u5B58\u30B9\u30DA\u30FC\u30B9\u306E id \u306F `smash space list` \u3067\u78BA\u8A8D\u3067\u304D\u307E\u3059\u3002",
|
|
4829
|
+
"\u30B9\u30AD\u30EB\u3060\u3051\u5165\u308C\u305F\u3044\u5834\u5408\u306F `smash init --skills-only`\u3002"
|
|
4830
|
+
].join("\n")
|
|
4831
|
+
);
|
|
4832
|
+
process.exit(1);
|
|
4833
|
+
}
|
|
4784
4834
|
await put(".claude/skills/smash/SKILL.md", SMASH_SKILL_MD);
|
|
4785
4835
|
await put(".codex/skills/smash/SKILL.md", SMASH_SKILL_MD);
|
|
4786
4836
|
await put(".codex/skills/smash/agents/openai.yaml", OPENAI_AGENT_YAML);
|
|
4787
|
-
actions.push(
|
|
4788
|
-
|
|
4789
|
-
)
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4837
|
+
actions.push("\u2713 operator skill \u2192 .claude/skills/smash/ + .codex/skills/smash/");
|
|
4838
|
+
let boardId = "";
|
|
4839
|
+
if (!opts.skillsOnly) {
|
|
4840
|
+
if (opts.newSpace) {
|
|
4841
|
+
const space = await api(
|
|
4842
|
+
"/api/boards",
|
|
4843
|
+
{ method: "POST", body: JSON.stringify({ title: opts.newSpace }) },
|
|
4844
|
+
baseUrl
|
|
4845
|
+
);
|
|
4846
|
+
boardId = space.id;
|
|
4847
|
+
actions.push(`\u2713 created space "${space.title || opts.newSpace}" (${space.id})`);
|
|
4848
|
+
} else {
|
|
4849
|
+
const raw = opts.spaceId ?? opts.boardId;
|
|
4850
|
+
const m = raw.match(/\/(?:board|space)\/((?:board_|b_|s_)[a-z0-9]+)/i);
|
|
4851
|
+
boardId = m ? m[1] : raw;
|
|
4852
|
+
}
|
|
4802
4853
|
}
|
|
4803
|
-
const placeholder = boardId === "s_xxxxxxxx";
|
|
4804
4854
|
if (!opts.skillsOnly) {
|
|
4805
4855
|
if (await exists(CONFIG_FILENAME) && !opts.force) {
|
|
4806
4856
|
actions.push(`\u2022 ${CONFIG_FILENAME} exists \u2014 kept (use --force)`);
|
|
4807
4857
|
} else {
|
|
4808
4858
|
await put(CONFIG_FILENAME, smashspaceJson(baseUrl, boardId));
|
|
4809
|
-
actions.push(
|
|
4810
|
-
`\u2713 ${CONFIG_FILENAME}${placeholder ? " (placeholder space_id \u2014 set a real one below)" : ` \u2192 ${boardId}`}`
|
|
4811
|
-
);
|
|
4859
|
+
actions.push(`\u2713 ${CONFIG_FILENAME} \u2192 ${boardId}`);
|
|
4812
4860
|
}
|
|
4813
4861
|
if (opts.mcp !== false) {
|
|
4814
4862
|
let mcp = { mcpServers: {} };
|
|
@@ -4843,19 +4891,14 @@ program2.command("init").description(
|
|
|
4843
4891
|
}
|
|
4844
4892
|
}
|
|
4845
4893
|
const next = ["", "Next:"];
|
|
4846
|
-
if (!opts.skillsOnly
|
|
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) {
|
|
4894
|
+
if (!opts.skillsOnly) {
|
|
4852
4895
|
next.push(" smash login # if you haven't yet \u2014 then you're ready.");
|
|
4853
4896
|
}
|
|
4854
4897
|
next.push(
|
|
4855
4898
|
" \u2192 Claude Code / Codex pick up .claude|.codex/skills/smash automatically"
|
|
4856
4899
|
);
|
|
4857
4900
|
if (getOpts().json) {
|
|
4858
|
-
show({ cwd, actions, baseUrl, boardId:
|
|
4901
|
+
show({ cwd, actions, baseUrl, boardId: boardId || null });
|
|
4859
4902
|
return;
|
|
4860
4903
|
}
|
|
4861
4904
|
show(void 0, [...actions, ...next]);
|
|
@@ -4901,19 +4944,29 @@ program2.command("move <cardId> <listName>").description("Move a card to a list
|
|
|
4901
4944
|
);
|
|
4902
4945
|
show(card, [`\u2713 moved ${cardId} \u2192 ${list.title} (board ${ctx.entry.label})`]);
|
|
4903
4946
|
});
|
|
4904
|
-
program2.command("scan").description("Emit the board's cards as JSON (agent-facing: pick up work programmatically)").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").action(async (opts) => {
|
|
4947
|
+
program2.command("scan").description("Emit the board's cards as JSON (agent-facing: pick up work programmatically)").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").option("--full", "include all card fields (description, agentMeta, labelIds, \u2026) \u2014 no extra requests").option("--fields <list>", "comma-separated extra fields to include, e.g. description,agentMeta").action(async (opts) => {
|
|
4905
4948
|
const ctx = resolveContext(opts);
|
|
4906
4949
|
const detail = await fetchBoard(ctx.boardId, ctx.baseUrl);
|
|
4907
4950
|
const lists = opts.list ? [findListByName(detail, opts.list)] : detail.lists;
|
|
4951
|
+
const extraFields = opts.fields ? opts.fields.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
4908
4952
|
const cards = lists.flatMap(
|
|
4909
|
-
(l) => l.cards.map((c) =>
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4953
|
+
(l) => l.cards.map((c) => {
|
|
4954
|
+
if (opts.full)
|
|
4955
|
+
return { list: l.title, ...c };
|
|
4956
|
+
const base = {
|
|
4957
|
+
id: c.id,
|
|
4958
|
+
title: c.title,
|
|
4959
|
+
list: l.title,
|
|
4960
|
+
listId: l.id,
|
|
4961
|
+
position: c.position,
|
|
4962
|
+
dueAt: c.dueAt ?? null
|
|
4963
|
+
};
|
|
4964
|
+
for (const f of extraFields) {
|
|
4965
|
+
if (f in c)
|
|
4966
|
+
base[f] = c[f];
|
|
4967
|
+
}
|
|
4968
|
+
return base;
|
|
4969
|
+
})
|
|
4917
4970
|
);
|
|
4918
4971
|
console.log(
|
|
4919
4972
|
JSON.stringify(
|