@kythin/stackydo 0.9.2

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/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kythin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ # stackydo
2
+
3
+ **One person's entire workload, in one place.**
4
+
5
+ Stackydo is a context-aware CLI task manager designed for individual engineers, leads, and makers who juggle work across many projects, teams, and responsibilities. Tasks are plain markdown files with YAML frontmatter — no database, no server, no vendor lock-in.
6
+
7
+ The core idea: your work doesn't live in one project. You might be debugging a production incident, reviewing a teammate's PR, prepping a conference talk, and planning next sprint — all in the same afternoon. Stackydo uses **stacks** to separate these workstreams while keeping everything in a single, searchable task store.
8
+
9
+ ## Why This Exists
10
+
11
+ Most task managers are built for teams or for single projects. Stackydo is built for **you** — the individual who needs to:
12
+
13
+ - Track work across multiple projects, random ideas, ad-hoc tasks, team duties, and personal goals simultaneously
14
+ - Create tasks from wherever you are — terminal, editor, scripts, or AI agents — so fast it becomes muscle memory to offload every stray thought
15
+ - Search across everything at once ("what was that security thing last week?")
16
+ - Let AI tools triage, summarize, and report on your workload via the built-in MCP server
17
+ - Own your data as plain files you can grep, version, and back up
18
+
19
+ ## Features
20
+
21
+ - **Headless CLI** — create, list, search, update, complete, delete from scripts and pipelines
22
+ - **MCP server** — gives AI assistants (Claude Desktop, Claude Code, etc.) full access to your task store
23
+ - **Stacks** — organize tasks into named workstreams (e.g. "work", "personal", "sprint-12")
24
+ - **Short IDs** — human-friendly IDs like SD1, SD2 alongside ULIDs; all commands accept either
25
+ - **Automatic context capture** — records git branch/commit, working directory, and project context on task creation
26
+ - **Task graph** — subtasks, dependencies (blocked-by, blocks, related-to)
27
+ - **AI-friendly storage** — plain markdown+YAML files that any LLM or script can read and write
28
+ - **`.stackydo-context` files** — define project-level context that gets attached to new tasks automatically
29
+ - **Session chaining** — tracks the last task ID created per shell session via `$STACKYDO_LAST_ID`
30
+ - **Configurable storage** — set `dir` in `.stackydo-context` for per-project workspaces, or `$STACKYDO_DIR` for per-session overrides (defaults to `~/.stackydo/`)
31
+ - **Multi-workspace** — discover, list, and migrate tasks across workspaces
32
+ - **Doctor** — diagnose and auto-fix workspace issues (missing short IDs, orphan refs, corrupt files)
33
+
34
+ ## Install
35
+
36
+ ### Homebrew (macOS/Linux)
37
+
38
+ ```bash
39
+ brew tap kythin/homebrew-tap && brew install stackydo
40
+ ```
41
+
42
+ ### Shell (curl one-liner)
43
+
44
+ ```bash
45
+ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/kythin/stackydo-cli/releases/latest/download/stackydo-installer.sh | sh
46
+ ```
47
+
48
+ ### PowerShell (Windows)
49
+
50
+ ```powershell
51
+ powershell -c "irm https://github.com/kythin/stackydo-cli/releases/latest/download/stackydo-installer.ps1 | iex"
52
+ ```
53
+
54
+ ### From source
55
+
56
+ ```bash
57
+ cargo install --git https://github.com/kythin/stackydo-cli
58
+ ```
59
+
60
+ ### Update
61
+
62
+ ```bash
63
+ # Homebrew
64
+ brew upgrade stackydo
65
+
66
+ # Shell — re-run the curl installer, or use the built-in updater:
67
+ stackydo-update
68
+ ```
69
+
70
+ All methods install two binaries: `stackydo` (CLI) and `stackydo-mcp` (MCP server).
71
+
72
+ ## Quick Start
73
+
74
+ ```bash
75
+ # Create a task
76
+ stackydo create --title "Fix auth bug" --tags "backend,urgent" --priority high --stack work \
77
+ -- The login endpoint returns 500 when the token expires
78
+
79
+ # List tasks
80
+ stackydo list
81
+ stackydo list --status todo --sort priority
82
+ stackydo list --stack work
83
+ stackydo list --overdue
84
+
85
+ # Show task detail (prefix matching and short IDs work everywhere)
86
+ stackydo show SD1
87
+ stackydo show 01HQ
88
+
89
+ # Update a task
90
+ stackydo update SD1 --status in_progress --note "Investigating root cause"
91
+
92
+ # Complete a task
93
+ stackydo complete SD1
94
+
95
+ # Search
96
+ stackydo search "auth"
97
+
98
+ # Add a comment
99
+ stackydo comment SD1 "Turns out it was a timezone issue"
100
+
101
+ # Check workspace health
102
+ stackydo doctor
103
+ stackydo doctor --fix
104
+ ```
105
+
106
+ ## CLI Commands
107
+
108
+ | Command | Description |
109
+ |---------|-------------|
110
+ | `create` | Create a new task with title, tags, priority, stack, body, due date, dependencies |
111
+ | `list` | List/filter tasks by status, stage, tag, priority, stack, due date; sort, group, paginate |
112
+ | `show` | Show a task's full details |
113
+ | `update` | Update fields, append body text, sed-style substitution, add timestamped notes |
114
+ | `complete` | Mark task(s) as done (single or bulk with `--all`) |
115
+ | `delete` | Permanently delete task(s) (single or bulk with `--all`) |
116
+ | `search` | Search title and body (case-insensitive) with same filters as `list` |
117
+ | `comment` | Add a comment to a task |
118
+ | `stats` | Summary statistics: totals, overdue count, breakdowns by status/stack/tag |
119
+ | `stacks` | All stacks with per-stack task counts and status breakdowns |
120
+ | `context` | Preview what context would be captured for a new task |
121
+ | `init` | Initialize a new workspace (optionally with `--here` and `--git`) |
122
+ | `doctor` | Diagnose and optionally fix workspace issues |
123
+ | `shuffle` | Randomise the order of tasks in a stack+status group |
124
+ | `draw` | Draw the top task from one group and move it to another |
125
+ | `list-workspaces` | Discover all stackydo workspaces on the system |
126
+ | `migrate` | Move or copy tasks between workspaces |
127
+ | `import` | Import tasks from stdin (JSON or YAML) |
128
+ | `mcp-setup` | Register `stackydo-mcp` with Claude Code via `claude mcp add` |
129
+ | `agent-setup` | Generate an AI agent playbook in your project's CLAUDE.md |
130
+
131
+ ## Stacks
132
+
133
+ A task can belong to one **stack** — a named group like "work", "personal", or "sprint-12". Think of stacks as physical piles of tasks rather than flat database categories.
134
+
135
+ ```bash
136
+ # Create a task on a stack
137
+ stackydo create --title "Deploy v2" --stack work
138
+
139
+ # Filter tasks by stack
140
+ stackydo list --stack work
141
+ stackydo list --stack personal --status todo
142
+
143
+ # See all stacks with counts
144
+ stackydo stacks
145
+ ```
146
+
147
+ The manifest tracks known stack names. Tasks without a stack are unstacked and won't appear in stack-filtered results.
148
+
149
+ ## Workflows
150
+
151
+ Workflows define the statuses available to tasks and how they map to lifecycle stages (backlog, active, archive). Each stack can use a different workflow.
152
+
153
+ ### Built-in Workflows
154
+
155
+ **Kanban** (default) — traditional task board:
156
+
157
+ | Stage | Statuses |
158
+ |-------|----------|
159
+ | Backlog | `todo`, `on_hold` |
160
+ | Active | `in_progress`, `blocked`, `in_review` |
161
+ | Archive | `done`, `cancelled` |
162
+
163
+ **Deck** — card game metaphor for randomised/prioritised work:
164
+
165
+ | Stage | Statuses |
166
+ |-------|----------|
167
+ | Backlog | `deck` |
168
+ | Active | `hand`, `table` |
169
+ | Archive | `discard` |
170
+
171
+ ### Per-Stack Assignment
172
+
173
+ Set a stack's workflow in `manifest.json`:
174
+ ```json
175
+ {
176
+ "stack_workflows": {
177
+ "ideas": "deck",
178
+ "work": "kanban"
179
+ }
180
+ }
181
+ ```
182
+
183
+ Stacks without a workflow inherit the workspace default (kanban).
184
+
185
+ ### Index Ordering
186
+
187
+ Tasks within the same stack+status group have an `index` field for positional ordering. Indexes are maintained automatically when tasks are created, moved, or deleted. Use `shuffle` to randomise order, and `draw` to pull the top task from one group to another:
188
+
189
+ ```bash
190
+ # Randomise the deck
191
+ stackydo shuffle --stack ideas --status deck
192
+
193
+ # Draw the top card into your hand
194
+ stackydo draw --source ideas/deck --target ideas/hand
195
+ ```
196
+
197
+ ## MCP Server (Claude Desktop / Claude Code)
198
+
199
+ Stackydo includes an MCP server that gives AI assistants full access to your task store.
200
+
201
+ ### Setup
202
+
203
+ Install stackydo using any method from the [Install](#install) section. Both `stackydo` and `stackydo-mcp` are included.
204
+
205
+ **Claude Code** (quickest):
206
+
207
+ ```bash
208
+ stackydo mcp-setup
209
+ ```
210
+
211
+ This runs `claude mcp add` to register the server. Use `--scope user` for global access or `--scope project` (default) for per-project.
212
+
213
+ **Claude Desktop** — add to your config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
214
+
215
+ ```json
216
+ {
217
+ "mcpServers": {
218
+ "stackydo": {
219
+ "command": "stackydo-mcp"
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ To use a non-default storage directory, add an `env` key:
226
+
227
+ ```json
228
+ {
229
+ "mcpServers": {
230
+ "stackydo": {
231
+ "command": "stackydo-mcp",
232
+ "env": {
233
+ "STACKYDO_DIR": "/path/to/your/tasks"
234
+ }
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ Restart Claude Desktop after editing the config.
241
+
242
+ ### Available MCP Tools
243
+
244
+ | Tool | Description |
245
+ |------|-------------|
246
+ | `list_tasks` | List/filter tasks by status, tag, priority, stack, due date; sort and group |
247
+ | `get_task` | Get a single task by ID (prefix matching) |
248
+ | `create_task` | Create a task with title, priority, tags, stack, body, due date |
249
+ | `update_task` | Update fields, append timestamped notes, body editing |
250
+ | `complete_task` | Mark a task as done |
251
+ | `delete_task` | Permanently delete a task |
252
+ | `search_tasks` | Search title and body (case-insensitive) |
253
+ | `add_comment` | Add a comment to a task |
254
+ | `get_stats` | Summary statistics: totals, overdue count, breakdowns by status/stack/tag |
255
+ | `get_stacks` | All stacks with per-stack task counts and status breakdowns |
256
+ | `list_workspaces` | Discover all stackydo workspaces on the system |
257
+ | `migrate_tasks` | Move or copy tasks between workspaces |
258
+ | `doctor` | Diagnose and optionally fix workspace issues |
259
+ | `shuffle` | Randomise task order in a stack+status group |
260
+ | `draw` | Draw top task from one group and move to another |
261
+
262
+ The server also exposes a `stackydo://guide` resource with a full agent guide, and prompt templates for triage, planning, and task extraction.
263
+
264
+ ## AI Agent Setup
265
+
266
+ Stackydo is designed to be used by AI coding agents (Claude Code, Cursor, etc.) as their own task tracker:
267
+
268
+ ```bash
269
+ # Generate a CLAUDE.md playbook for your project
270
+ stackydo agent-setup
271
+
272
+ # Register the MCP server with Claude Code
273
+ stackydo mcp-setup
274
+ ```
275
+
276
+ `agent-setup` writes instructions into your project's CLAUDE.md so agents know how to create, update, and complete tasks as they work.
277
+
278
+ ## Environment Variables
279
+
280
+ | Variable | Description |
281
+ |----------|-------------|
282
+ | `STACKYDO_DIR` | Override the task storage directory (highest priority, overrides `.stackydo-context`; default: `~/.stackydo/`) |
283
+ | `STACKYDO_LAST_ID` | Set automatically by `stackydo create`; chains tasks in a shell session |
284
+
285
+ ## Task Storage
286
+
287
+ Each task is a markdown file at `<STACKYDO_DIR>/<ULID>.md`:
288
+
289
+ ```markdown
290
+ ---
291
+ id: 01HQXYZ...
292
+ short_id: SD1
293
+ title: Fix auth bug
294
+ status: todo
295
+ priority: high
296
+ tags: [backend, urgent]
297
+ stack: work
298
+ index: 0
299
+ created: 2025-02-13T10:30:00Z
300
+ modified: 2025-02-13T10:30:00Z
301
+ context:
302
+ working_dir: /home/user/project
303
+ git_branch: main
304
+ git_commit: a3f7d92
305
+ ---
306
+
307
+ The login endpoint returns 500 when the token expires.
308
+ ```
309
+
310
+ A manifest at `<STACKYDO_DIR>/manifest.json` tracks tags, stacks, short ID counter, and settings.
311
+
312
+ ## Context Discovery
313
+
314
+ On task creation, stackydo automatically captures:
315
+
316
+ 1. Current working directory
317
+ 2. Git branch, remote URL, and HEAD commit (if in a repo)
318
+ 3. Content from the nearest `.stackydo-context` file (walks up from CWD, falls back to `~/.stackydo-context`)
319
+ 4. `$STACKYDO_LAST_ID` — the ID of the previous task created in the same shell session
320
+
321
+ Use `stackydo context` to preview what would be captured without creating a task.
322
+
323
+ ### Workspace Resolution
324
+
325
+ The `.stackydo-context` file can set the task store location via a `dir` field. This lets a project root define a shared workspace without requiring every user to set an environment variable.
326
+
327
+ Resolution priority:
328
+ 1. `$STACKYDO_DIR` env var (highest — per-session override)
329
+ 2. `dir` field in the nearest `.stackydo-context` (per-project)
330
+ 3. `~/.stackydo/` (default)
331
+
332
+ Example `.stackydo-context`:
333
+
334
+ ```yaml
335
+ dir: .stackydo-workspace
336
+ project: my-app
337
+ stack: dev
338
+ description: Project-level context captured on new tasks
339
+ ```
340
+
341
+ The `dir` path is resolved relative to the config file's location. Use `stackydo context` to see which source resolved the task store.
342
+
343
+ To set up a project workspace:
344
+
345
+ ```bash
346
+ stackydo init --here --dir .stackydo-workspace
347
+ ```
348
+
349
+ ## Contributing
350
+
351
+ ```bash
352
+ # Build
353
+ cargo build
354
+
355
+ # Run tests
356
+ cargo test # unit tests
357
+ cargo build && bash tests/smoke_test.sh # CLI smoke tests
358
+ bash tests/test_all.sh # full suite: clippy + unit + build + smoke + scenario + scale
359
+
360
+ # Lint
361
+ cargo clippy
362
+ ```
363
+
364
+ Tests use `$STACKYDO_DIR` to write to a local `tests/.test-data/` directory — they never touch `~/.stackydo/`.
365
+
366
+ ## License
367
+
368
+ MIT
@@ -0,0 +1,212 @@
1
+ const { createWriteStream, existsSync, mkdirSync, mkdtemp } = require("fs");
2
+ const { join, sep } = require("path");
3
+ const { spawnSync } = require("child_process");
4
+ const { tmpdir } = require("os");
5
+
6
+ const axios = require("axios");
7
+ const rimraf = require("rimraf");
8
+ const tmpDir = tmpdir();
9
+
10
+ const error = (msg) => {
11
+ console.error(msg);
12
+ process.exit(1);
13
+ };
14
+
15
+ class Package {
16
+ constructor(platform, name, url, filename, zipExt, binaries) {
17
+ let errors = [];
18
+ if (typeof url !== "string") {
19
+ errors.push("url must be a string");
20
+ } else {
21
+ try {
22
+ new URL(url);
23
+ } catch (e) {
24
+ errors.push(e);
25
+ }
26
+ }
27
+ if (name && typeof name !== "string") {
28
+ errors.push("package name must be a string");
29
+ }
30
+ if (!name) {
31
+ errors.push("You must specify the name of your package");
32
+ }
33
+ if (binaries && typeof binaries !== "object") {
34
+ errors.push("binaries must be a string => string map");
35
+ }
36
+ if (!binaries) {
37
+ errors.push("You must specify the binaries in the package");
38
+ }
39
+
40
+ if (errors.length > 0) {
41
+ let errorMsg =
42
+ "One or more of the parameters you passed to the Binary constructor are invalid:\n";
43
+ errors.forEach((error) => {
44
+ errorMsg += error;
45
+ });
46
+ errorMsg +=
47
+ '\n\nCorrect usage: new Package("my-binary", "https://example.com/binary/download.tar.gz", {"my-binary": "my-binary"})';
48
+ error(errorMsg);
49
+ }
50
+
51
+ this.platform = platform;
52
+ this.url = url;
53
+ this.name = name;
54
+ this.filename = filename;
55
+ this.zipExt = zipExt;
56
+ this.installDirectory = join(__dirname, "node_modules", ".bin_real");
57
+ this.binaries = binaries;
58
+
59
+ if (!existsSync(this.installDirectory)) {
60
+ mkdirSync(this.installDirectory, { recursive: true });
61
+ }
62
+ }
63
+
64
+ exists() {
65
+ for (const binaryName in this.binaries) {
66
+ const binRelPath = this.binaries[binaryName];
67
+ const binPath = join(this.installDirectory, binRelPath);
68
+ if (!existsSync(binPath)) {
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ install(fetchOptions, suppressLogs = false) {
76
+ if (this.exists()) {
77
+ if (!suppressLogs) {
78
+ console.error(
79
+ `${this.name} is already installed, skipping installation.`,
80
+ );
81
+ }
82
+ return Promise.resolve();
83
+ }
84
+
85
+ if (existsSync(this.installDirectory)) {
86
+ rimraf.sync(this.installDirectory);
87
+ }
88
+
89
+ mkdirSync(this.installDirectory, { recursive: true });
90
+
91
+ if (!suppressLogs) {
92
+ console.error(`Downloading release from ${this.url}`);
93
+ }
94
+
95
+ return axios({ ...fetchOptions, url: this.url, responseType: "stream" })
96
+ .then((res) => {
97
+ return new Promise((resolve, reject) => {
98
+ mkdtemp(`${tmpDir}${sep}`, (err, directory) => {
99
+ let tempFile = join(directory, this.filename);
100
+ const sink = res.data.pipe(createWriteStream(tempFile));
101
+ sink.on("error", (err) => reject(err));
102
+ sink.on("close", () => {
103
+ if (/\.tar\.*/.test(this.zipExt)) {
104
+ const result = spawnSync("tar", [
105
+ "xf",
106
+ tempFile,
107
+ // The tarballs are stored with a leading directory
108
+ // component; we strip one component in the
109
+ // shell installers too.
110
+ "--strip-components",
111
+ "1",
112
+ "-C",
113
+ this.installDirectory,
114
+ ]);
115
+ if (result.status == 0) {
116
+ resolve();
117
+ } else if (result.error) {
118
+ reject(result.error);
119
+ } else {
120
+ reject(
121
+ new Error(
122
+ `An error occurred untarring the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
123
+ ),
124
+ );
125
+ }
126
+ } else if (this.zipExt == ".zip") {
127
+ let result;
128
+ if (this.platform.artifactName.includes("windows")) {
129
+ // Windows does not have "unzip" by default on many installations, instead
130
+ // we use Expand-Archive from powershell
131
+ result = spawnSync("powershell.exe", [
132
+ "-NoProfile",
133
+ "-NonInteractive",
134
+ "-Command",
135
+ `& {
136
+ param([string]$LiteralPath, [string]$DestinationPath)
137
+ Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force
138
+ }`,
139
+ tempFile,
140
+ this.installDirectory,
141
+ ]);
142
+ } else {
143
+ result = spawnSync("unzip", [
144
+ "-q",
145
+ tempFile,
146
+ "-d",
147
+ this.installDirectory,
148
+ ]);
149
+ }
150
+
151
+ if (result.status == 0) {
152
+ resolve();
153
+ } else if (result.error) {
154
+ reject(result.error);
155
+ } else {
156
+ reject(
157
+ new Error(
158
+ `An error occurred unzipping the artifact: stdout: ${result.stdout}; stderr: ${result.stderr}`,
159
+ ),
160
+ );
161
+ }
162
+ } else {
163
+ reject(
164
+ new Error(`Unrecognized file extension: ${this.zipExt}`),
165
+ );
166
+ }
167
+ });
168
+ });
169
+ });
170
+ })
171
+ .then(() => {
172
+ if (!suppressLogs) {
173
+ console.error(`${this.name} has been installed!`);
174
+ }
175
+ })
176
+ .catch((e) => {
177
+ error(`Error fetching release: ${e.message}`);
178
+ });
179
+ }
180
+
181
+ run(binaryName, fetchOptions) {
182
+ const promise = !this.exists()
183
+ ? this.install(fetchOptions, true)
184
+ : Promise.resolve();
185
+
186
+ promise
187
+ .then(() => {
188
+ const [, , ...args] = process.argv;
189
+
190
+ const options = { cwd: process.cwd(), stdio: "inherit" };
191
+
192
+ const binRelPath = this.binaries[binaryName];
193
+ if (!binRelPath) {
194
+ error(`${binaryName} is not a known binary in ${this.name}`);
195
+ }
196
+ const binPath = join(this.installDirectory, binRelPath);
197
+ const result = spawnSync(binPath, args, options);
198
+
199
+ if (result.error) {
200
+ error(result.error);
201
+ }
202
+
203
+ process.exit(result.status);
204
+ })
205
+ .catch((e) => {
206
+ error(e.message);
207
+ process.exit(1);
208
+ });
209
+ }
210
+ }
211
+
212
+ module.exports.Package = Package;