@sqaoss/flowy 1.6.0 → 1.6.1

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
@@ -14,40 +14,107 @@ You get full observability on what every agent planned, built, and shipped.
14
14
 
15
15
  ## Get Started
16
16
 
17
- ### Install (once)
17
+ Flowy runs in one of two modes. Pick the one that fits:
18
+
19
+ - **Self-hosted** — a local server you run yourself (`flowy serve`). No account, no subscription, your data stays on your machine. Start here if you just want to try Flowy.
20
+ - **Remote (hosted)** — the managed service at `flowy-ai.fly.dev`. Register with an email, then subscribe at checkout. The hosted server gates data operations behind an active subscription.
21
+
22
+ ### Quickstart (self-hosted, no account)
18
23
 
19
24
  ```bash
20
25
  npm i -g @sqaoss/flowy
21
- flowy setup remote --email you@example.com
26
+ flowy setup local # installs the bundled server, points the CLI at localhost
27
+ flowy serve & # starts the local server on 127.0.0.1:4000
28
+
29
+ cd my-project
30
+ flowy init # auto-detects the git repo, creates + maps a project
31
+
32
+ flowy feature create --title "User Auth" --description "Email + OAuth login"
33
+ flowy feature set "User Auth"
34
+
35
+ flowy task create --title "Implement OAuth" --description "Wire up the OAuth provider"
36
+ flowy status <task-id> in_progress
37
+ flowy status <task-id> done
22
38
  ```
23
39
 
24
- ### Initialize a project
40
+ `flowy serve` runs in the foreground; the `&` backgrounds it. Stop it with `kill %1` or run it in a separate terminal. Data lives in `./flowy.sqlite`.
41
+
42
+ ### Quickstart (remote/hosted)
25
43
 
26
44
  ```bash
45
+ npm i -g @sqaoss/flowy
46
+ flowy setup remote --email you@example.com # registers; prints an apiKey + checkoutUrl
47
+
27
48
  cd my-project
28
- flowy init # auto-detects repo, creates project
49
+ flowy init # auto-detects the git repo, creates + maps a project
50
+ flowy task create --title "First task" --description "Try it out"
29
51
  ```
30
52
 
31
- ### Start planning
53
+ `setup remote` registers your email and stores the returned API key. It prints a `checkoutUrl` — **open it to start a subscription**. Until you do, the hosted server may reject data operations with `An active subscription is required`. You no longer need to choose a tier up front (`--tier` is optional); pick one at checkout.
54
+
55
+ Every command outputs JSON. Your agent reads it, acts on it, moves to the next task.
56
+
57
+ ### Descriptions: literal vs. file
58
+
59
+ `--description` is **always literal text** — it is never read as a file path. To load a description from a file (or stdin), use `--description-file`:
32
60
 
33
61
  ```bash
34
- flowy feature create --title "User Auth" --description auth-spec.md
35
- flowy feature set "User Auth"
62
+ flowy task create --title "Write tests" --description "Unit + integration tests"
63
+ flowy feature create --title "User Auth" --description-file auth-spec.md
64
+ flowy task create --title "From stdin" --description-file - # reads stdin
65
+ ```
36
66
 
37
- flowy task create --title "Implement OAuth" --description oauth.md
38
- flowy task create --title "Write tests" --description "Unit + integration"
67
+ ### Dependencies and ready work
39
68
 
40
- flowy status <task-id> in_progress
41
- flowy status <task-id> done
69
+ Tasks can block one another. Mark a dependency, inspect it, and ask for only the tasks that are actually actionable right now:
70
+
71
+ ```bash
72
+ flowy task block <blocker-id> <blocked-id> # blocker must finish before blocked
73
+ flowy task deps <id> # what blocks this task, and what it blocks
74
+ flowy task show <id> # task details, now including blockedBy/blocks
75
+
76
+ flowy task list --ready # only unblocked, not-done tasks (active project)
77
+ flowy task list --ready --project <project-id> # ...scoped to a specific project
78
+ flowy task list --all # every task across the whole backlog
42
79
  ```
43
80
 
44
- Every command outputs JSON. Your agent reads it, acts on it, moves to the next task.
81
+ `--ready` returns tasks that are not `done`/`cancelled` and have zero unfinished blockers the work an agent can pick up next.
82
+
83
+ ### Import and export
84
+
85
+ Move a whole backlog in or out as a single JSON manifest. Import is **idempotent**: each node carries a stable `key` (a client-key), so re-importing updates the matching nodes in place instead of duplicating them. Edges (`part_of`, `blocks`) round-trip through the real edge model, so a `block` you created by hand is captured on export and not re-created on the next import.
86
+
87
+ ```bash
88
+ flowy export # print the active project's manifest to stdout
89
+ flowy export backlog.json # ...or write it to a file
90
+ flowy import backlog.json # ingest a manifest (create new, update existing by key)
91
+ ```
92
+
93
+ A manifest looks like:
94
+
95
+ ```json
96
+ {
97
+ "version": 1,
98
+ "nodes": [
99
+ { "key": "proj", "type": "project", "title": "My Project" },
100
+ { "key": "auth", "type": "feature", "title": "User Auth", "parent": "proj" },
101
+ { "key": "oauth", "type": "task", "title": "Implement OAuth", "parent": "auth", "status": "draft" }
102
+ ],
103
+ "edges": [
104
+ { "source": "oauth", "target": "auth", "relation": "part_of" }
105
+ ]
106
+ }
107
+ ```
108
+
109
+ Each node's `parent` implies a `part_of` edge, so the simplest manifests need no explicit `edges`. `blocks` dependencies go in `edges`. The reserved `__flowyKey` metadata field stores the client-key; your own `metadata` is preserved alongside it and stripped back out on export.
45
110
 
46
111
  ## Agent Skill
47
112
 
48
- Flowy installs an agent skill during setup. Your AI agent automatically knows every command. No manual configuration needed.
113
+ `flowy setup` installs an agent skill so your AI agent automatically knows every command. If that install step fails (offline, no `npx`, registry hiccup), setup prints a warning telling you to install it manually:
49
114
 
50
- Or install the skill manually: `npx skills add sqaoss/flowy`
115
+ ```bash
116
+ npx skills add sqaoss/flowy
117
+ ```
51
118
 
52
119
  See [skills/using-flowy/SKILL.md](skills/using-flowy/SKILL.md) for the full skill reference.
53
120
 
@@ -70,50 +137,65 @@ Also: `blocked`, `cancelled`. Only `pending_review` entities can be approved.
70
137
 
71
138
  ## Self-Hosted
72
139
 
73
- Run Flowy on your own machine with SQLite and Docker. Same CLI, same commands.
140
+ Run Flowy on your own machine no Docker, no account, no subscription. `flowy setup local` installs a bundled server pinned to your CLI version and points the CLI at `localhost`; `flowy serve` runs it natively over SQLite.
74
141
 
75
142
  ```bash
76
- flowy setup local # starts a local server via Docker
77
- flowy init # auto-detects repo
143
+ flowy setup local # install the bundled server, configure the CLI
144
+ flowy serve # bind 127.0.0.1:4000, store data in ./flowy.sqlite
145
+ flowy serve --port 5000 --host 0.0.0.0 --db ~/flowy.sqlite # override defaults
78
146
  ```
79
147
 
148
+ The self-hosted server supports the full planning workflow — `init`, `project`/`feature`/`task` CRUD, `status`, `approve`, `search`, `tree`, `task deps`, `task list --ready/--all`, and `import`/`export`. Account-only commands (`whoami`, `billing`, `key`) are remote-mode features and don't apply locally.
149
+
80
150
  ## Command Reference
81
151
 
82
152
  | Command | Description |
83
153
  |---------|-------------|
84
- | `setup remote --email <email>` | Register and connect to the hosted server |
85
- | `setup local` | Start a local Docker server and configure the CLI |
154
+ | `setup local` | Install the bundled local server and point the CLI at it |
155
+ | `setup remote --email <email> [--tier <tier>]` | Register with the hosted server (`--tier` optional) |
156
+ | `serve [--port] [--host] [--db]` | Run the bundled local server (self-hosted mode) |
86
157
  | `init` | Auto-detect repo and create/map project |
87
- | `whoami` | Show current user |
88
158
  | `client set name <name>` | Set client display name |
89
159
  | `project create <name>` | Create project |
90
- | `project set <name>` | Map current directory to project |
160
+ | `project set <name>` | Map current directory to a project |
91
161
  | `project list` | List all projects |
92
- | `project show [<id>]` | Show project details |
93
- | `feature create --title <t> --description <d>` | Create feature (requires active project) |
162
+ | `project show [<id>]` | Show project details (defaults to active) |
163
+ | `project update [<id>] [--title] [--description\|--description-file] [--metadata]` | Update a project |
164
+ | `project delete [<id>]` | Delete a project (defaults to active) |
165
+ | `feature create --title <t> [--description <text>\|--description-file <path>]` | Create feature (requires active project) |
94
166
  | `feature set <name-or-id>` | Set active feature |
95
167
  | `feature unset` | Clear active feature |
96
168
  | `feature list` | List features in active project |
97
- | `feature show [<id>]` | Show feature details |
98
- | `task create --title <t> --description <d>` | Create task (requires active feature) |
99
- | `task list` | List tasks in active feature |
100
- | `task show <id>` | Show task details |
101
- | `task block <id1> <id2>` | Mark task as blocking another |
102
- | `task unblock <id1> <id2>` | Remove block |
169
+ | `feature show [<id>]` | Show feature details (defaults to active) |
170
+ | `feature update [<id>] [--title] [--description\|--description-file] [--metadata]` | Update a feature |
171
+ | `feature delete [<id>]` | Delete a feature (defaults to active) |
172
+ | `task create --title <t> [--description <text>\|--description-file <path>]` | Create task (requires active feature) |
173
+ | `task list [--ready] [--all] [--project <id>]` | List tasks: active feature, or `--ready`/`--all` (optionally scoped to a project) |
174
+ | `task show <id>` | Show task details, including `blockedBy`/`blocks` |
175
+ | `task update <id> [--title] [--description\|--description-file] [--metadata]` | Update a task |
176
+ | `task delete <id>` | Delete a task |
177
+ | `task block <id1> <id2>` | Mark `id1` as blocking `id2` |
178
+ | `task unblock <id1> <id2>` | Remove a blocking relationship |
179
+ | `task deps <id>` | Show what blocks a task and what it blocks |
103
180
  | `status <id> <status>` | Update status (shorthand) |
104
181
  | `approve <id>` | Approve (must be pending_review) |
105
182
  | `search <query> [--type] [--status] [--limit]` | Full-text search |
106
- | `tree <id> [--depth N]` | Show subtree |
183
+ | `tree <id> [--depth N]` | Show subtree from any entity |
184
+ | `import <manifest>` | Ingest a JSON manifest of nodes + edges (idempotent by client-key) |
185
+ | `export [output]` | Dump the active project as a manifest (stdout or file) |
186
+ | `whoami` | Show current user (remote mode) |
187
+ | `billing checkout --tier <tier>` | Get a checkout URL for a subscription (remote mode) |
188
+ | `key rotate` | Revoke all API keys and issue a new one (remote mode) |
107
189
 
108
- All commands output JSON to stdout.
190
+ All commands output JSON to stdout; errors go to stderr as `{ "error": "message" }`.
109
191
 
110
192
  ## Configuration
111
193
 
112
- Config is stored at `~/.config/flowy/config.json`.
194
+ Config is stored at `~/.config/flowy/config.json`. These environment variables override config:
113
195
 
114
196
  | Variable | Description | Default |
115
197
  |----------|-------------|---------|
116
- | `FLOWY_API_URL` | GraphQL endpoint | `https://flowy-ai.fly.dev/graphql` |
198
+ | `FLOWY_API_URL` | GraphQL endpoint | `https://flowy-ai.fly.dev/graphql` (remote) / `http://localhost:4000/graphql` (local) |
117
199
  | `FLOWY_API_KEY` | API key (remote mode) | -- |
118
200
  | `FLOWY_PROJECT` | Override active project by name | -- |
119
201
  | `FLOWY_FEATURE` | Override active feature by ID | -- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqaoss/flowy",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Agentic persistent planning",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,30 +11,54 @@ Flowy gives you a persistent store for plans and execution tracking. Features ar
11
11
 
12
12
  Without Flowy, your plans live in markdown files that clutter git history, get deleted when done, and leave no record of what you accomplished. With Flowy, plans persist in a database. You flow through work without friction. Your human gets full observability.
13
13
 
14
+ ## Two Modes
15
+
16
+ Flowy runs against one of two backends. Which one you're in determines which commands work — check `~/.config/flowy/config.json` (`mode` is `local` or `remote`).
17
+
18
+ | | Local (self-hosted) | Remote (hosted SaaS) |
19
+ |---|---|---|
20
+ | Setup | `flowy setup local` then `flowy serve` | `flowy setup remote --email <email>` |
21
+ | Server | A bundled server you run (`flowy serve`, SQLite, `127.0.0.1:4000`) | `flowy-ai.fly.dev` |
22
+ | Account / API key | None | Email registration; API key stored in config |
23
+ | Subscription | None — fully free | Data operations require an active subscription |
24
+ | Planning workflow | Full (`init`, project/feature/task CRUD, status, approve, search, tree, `task deps`, `task list --ready/--all`, `import`/`export`) | Full, same commands |
25
+ | `whoami` / `billing` / `key` | **Not available** — these hard-fail locally | Available |
26
+
27
+ The planning workflow is **identical** in both modes. Only the account/billing commands differ.
28
+
14
29
  ## First Time in a Project
15
30
 
16
31
  ```bash
17
32
  flowy init # auto-detects the git repo, creates a project, maps this directory
18
33
  ```
19
34
 
20
- If Flowy isn't set up yet, the human needs to run:
35
+ If Flowy isn't set up yet, the human needs to choose a mode:
36
+
21
37
  ```bash
22
38
  npm i -g @sqaoss/flowy
23
- flowy setup remote --email their@email.com --tier explorer
39
+
40
+ # Self-hosted (free, no account):
41
+ flowy setup local # installs the bundled server, points the CLI at localhost
42
+ flowy serve # starts the local server on 127.0.0.1:4000 (run in its own terminal)
43
+
44
+ # OR hosted (managed service):
45
+ flowy setup remote --email their@email.com # registers; prints apiKey + checkoutUrl
24
46
  ```
25
47
 
26
- After registration, complete payment at the checkout URL provided.
48
+ `setup remote` prints a `checkoutUrl`. On the hosted server, data operations are rejected until a subscription is active — the human opens that URL to subscribe. `--tier` is optional at registration; a tier is chosen at checkout.
49
+
50
+ `flowy setup` also installs this agent skill via `npx skills add sqaoss/flowy`. If that step fails, setup prints a warning with the manual install command — the skill is not installed until you run it.
27
51
 
28
52
  ## Core Workflow
29
53
 
30
54
  ```bash
31
55
  # 1. Plan a feature (master plan)
32
- flowy feature create --title "User Auth" --description auth-spec.md
56
+ flowy feature create --title "User Auth" --description "Email + OAuth login"
33
57
  flowy feature set "User Auth"
34
58
 
35
59
  # 2. Break into tasks (execution steps)
36
- flowy task create --title "Implement OAuth" --description oauth.md
37
- flowy task create --title "Write tests" --description "Unit + integration tests"
60
+ flowy task create --title "Implement OAuth" --description "Wire up the OAuth provider"
61
+ flowy task create --title "Write tests" --description-file tests-plan.md
38
62
 
39
63
  # 3. Execute and track
40
64
  flowy status <task-id> in_progress
@@ -42,31 +66,19 @@ flowy status <task-id> in_progress
42
66
  flowy status <task-id> done
43
67
 
44
68
  # 4. Move to next task or feature
45
- flowy feature create --title "API Rate Limiting" --description rate-limit.md
69
+ flowy feature create --title "API Rate Limiting" --description-file rate-limit.md
46
70
  flowy feature set "API Rate Limiting"
47
71
  ```
48
72
 
49
73
  ## Entity Hierarchy
50
74
 
51
75
  ```
52
- project -> feature -> task
53
- 1:many 1:many
76
+ client -> project -> feature -> task
77
+ 1:many 1:many
54
78
  ```
55
79
 
56
80
  Every task belongs to a feature. Every feature belongs to a project. No orphans. The project is set automatically by `flowy init`.
57
81
 
58
- ## Subscription Tiers
59
-
60
- Flowy requires an active subscription for all data operations.
61
-
62
- | Tier | Projects | Description |
63
- |------|----------|-------------|
64
- | `explorer` | Up to 10 | For individual developers |
65
- | `pro` | Unlimited | For power users |
66
- | `team` | Unlimited | For teams |
67
-
68
- After registration, complete payment at the checkout URL. Existing users can get a new checkout URL with `flowy billing checkout --tier <tier>`.
69
-
70
82
  ## Status Flow
71
83
 
72
84
  ```
@@ -75,85 +87,145 @@ draft -> pending_review -> approved -> in_progress -> done
75
87
 
76
88
  Also: `blocked`, `cancelled`
77
89
 
78
- Only `pending_review` entities can be approved via `flowy approve <id>`.
90
+ Use `flowy status <id> <status>` to move a node. Only `pending_review` nodes can be approved via `flowy approve <id>`.
79
91
 
80
92
  ## Commands
81
93
 
94
+ ### Setup and Server
95
+ ```bash
96
+ flowy setup local # install bundled local server, configure CLI
97
+ flowy serve # run the local server (127.0.0.1:4000, ./flowy.sqlite)
98
+ flowy serve --port 5000 --host 0.0.0.0 --db ~/flowy.sqlite
99
+ flowy setup remote --email <email> # register with the hosted server (--tier optional)
100
+ flowy setup remote --email <email> --tier explorer
101
+ ```
102
+
82
103
  ### Project Context
83
104
  ```bash
84
- flowy init # Auto-detect repo, create + set project
85
- flowy project list # List all projects
86
- flowy project show [<id>] # Show project details
105
+ flowy init # auto-detect repo, create + map project
106
+ flowy project create <name> # create a project by name
107
+ flowy project set <name> # map current directory to an existing project
108
+ flowy project list # list all projects
109
+ flowy project show [<id>] # show project details (defaults to active)
110
+ flowy project update [<id>] --title <t> # update title/description/metadata
111
+ flowy project delete [<id>] # delete project (defaults to active)
87
112
  ```
88
113
 
89
114
  ### Features (requires active project)
90
115
  ```bash
91
- flowy feature create --title "Title" --description "description or file.md"
92
- flowy feature set "Title or ID" # Set active feature
93
- flowy feature unset # Clear active feature
94
- flowy feature list # List features in project
95
- flowy feature show [<id>] # Show feature details
116
+ flowy feature create --title "Title" --description "text"
117
+ flowy feature create --title "Title" --description-file spec.md
118
+ flowy feature set "Title or ID" # set active feature
119
+ flowy feature unset # clear active feature
120
+ flowy feature list # list features in active project
121
+ flowy feature show [<id>] # show feature (defaults to active)
122
+ flowy feature update [<id>] --title <t> # update title/description/metadata
123
+ flowy feature delete [<id>] # delete feature (defaults to active)
96
124
  ```
97
125
 
98
126
  ### Tasks (requires active feature)
99
127
  ```bash
100
- flowy task create --title "Title" --description "description or file.md"
101
- flowy task list # List tasks in feature
102
- flowy task show <id> # Show task details
103
- flowy task block <blocker> <blocked> # Mark dependency
104
- flowy task unblock <blocker> <blocked>
128
+ flowy task create --title "Title" --description "text"
129
+ flowy task create --title "Title" --description-file spec.md
130
+ flowy task list # tasks in active feature
131
+ flowy task list --ready # only actionable tasks (active project)
132
+ flowy task list --ready --project <id> # ...scoped to a specific project
133
+ flowy task list --all # every task across the whole backlog
134
+ flowy task show <id> # task details, incl. blockedBy/blocks
135
+ flowy task update <id> --title <t> # update title/description/metadata
136
+ flowy task delete <id> # delete task
137
+ flowy task block <id1> <id2> # mark id1 as blocking id2
138
+ flowy task unblock <id1> <id2> # remove a blocking relationship
139
+ flowy task deps <id> # what blocks this task, and what it blocks
105
140
  ```
106
141
 
142
+ `task list --ready` returns only tasks that are not `done`/`cancelled` and have zero unfinished blockers — the next work an agent can pick up. Without `--project` it scopes to the active project; with `--all` it spans the whole backlog. `task deps <id>` (and the `blockedBy`/`blocks` fields on `task show`) report the dependency graph built from `task block`.
143
+
107
144
  ### Status and Approval
108
145
  ```bash
109
146
  flowy status <id> in_progress
110
147
  flowy status <id> pending_review
111
- flowy approve <id> # Only works on pending_review
148
+ flowy approve <id> # only works on pending_review
112
149
  flowy status <id> done
113
150
  ```
114
151
 
115
152
  ### Search and Explore
116
153
  ```bash
117
154
  flowy search "query" --type task --status draft --limit 10
118
- flowy tree <project-id> --depth 3 # Show full subtree
155
+ flowy tree <id> --depth 3 # show subtree from any entity
119
156
  ```
120
157
 
121
- ### Setup
158
+ ### Import and Export
122
159
  ```bash
123
- flowy setup remote --email <email> --tier <tier> # Register (tier: explorer, pro, team)
124
- flowy setup local # Self-hosted Docker server
125
- flowy whoami # Show user info (includes grace period)
160
+ flowy export # print active project's manifest to stdout
161
+ flowy export backlog.json # ...or write it to a file
162
+ flowy import backlog.json # ingest a manifest (idempotent by client-key)
126
163
  ```
127
164
 
128
- ### Billing
165
+ A manifest is a single JSON document describing a backlog: `nodes` (projects, features, tasks) plus dependency `edges`. Each node is addressed by a stable **client-key** (`key`), not a server id:
166
+
167
+ ```json
168
+ {
169
+ "version": 1,
170
+ "nodes": [
171
+ { "key": "proj", "type": "project", "title": "My Project" },
172
+ { "key": "auth", "type": "feature", "title": "User Auth", "parent": "proj" },
173
+ { "key": "oauth", "type": "task", "title": "Implement OAuth", "parent": "auth", "status": "draft" }
174
+ ],
175
+ "edges": [
176
+ { "source": "oauth", "target": "auth", "relation": "part_of" }
177
+ ]
178
+ }
179
+ ```
180
+
181
+ **Idempotency:** import upserts by client-key — re-importing the same manifest updates the matching nodes in place instead of creating duplicates. The key is stored in node metadata under the reserved `__flowyKey` field (your own `metadata` is preserved alongside it and stripped back out on export). A node's `parent` implies a `part_of` edge, so simple manifests need no explicit `edges`; `blocks` dependencies are listed in `edges`. Edges live in the real edge model (`createEdge` / `Query.edges`), so a `blocks` edge created by hand with `task block` is captured on export and never re-created on the next import. Works in both local and remote modes.
182
+
183
+ ### Remote-only (hosted mode)
184
+ These hit account/billing resolvers that do **not** exist on the local server; they fail in local mode.
129
185
  ```bash
130
- flowy billing checkout --tier <tier> # Get checkout URL for subscription
186
+ flowy whoami # show current user (id, email, tier, graceEndsAt)
187
+ flowy billing checkout --tier <tier> # get a checkout URL (tier: explorer, pro, team)
188
+ flowy key rotate # revoke all API keys and issue a new one
131
189
  ```
132
190
 
133
- ### API Key Management
191
+ ### Client Settings (local config)
134
192
  ```bash
135
- flowy key rotate # Revoke all keys and generate a new one
193
+ flowy client set name "Your Name" # set client display name in local config
194
+ ```
195
+
196
+ ## Descriptions: literal vs. file
197
+
198
+ `create` and `update` commands take a description two ways. They are mutually exclusive.
199
+
200
+ - `--description <text>` — **always literal**. The text is used verbatim and is *never* interpreted as a file path. `--description plan.md` stores the string `plan.md`, not the file's contents.
201
+ - `--description-file <path>` — reads the file's contents as the description. Use `-` to read from stdin.
202
+
203
+ ```bash
204
+ flowy task create --title "T" --description "Do the thing"
205
+ flowy task create --title "T" --description-file plan.md
206
+ flowy task create --title "T" --description-file - # from stdin
136
207
  ```
137
208
 
138
209
  ## Validation Rules
139
210
 
140
- - **Title is required** and cannot be empty
141
- - **Description** is optional, but if provided cannot be empty
142
- - **--description** accepts a file path (reads content) or an inline string
143
- - **Search** requires at least 3 characters
144
- - **Status** must be one of: draft, pending_review, approved, in_progress, done, blocked, cancelled
145
- - **Blocking**: a task cannot block itself
146
- - **Edges**: both source and target nodes must exist
211
+ - **Title is required** on every `create` (cannot be empty).
212
+ - **Description is required** on `create`: pass exactly one of `--description` or `--description-file`. Passing both is an error; passing neither is an error.
213
+ - **Search** requires a non-empty query and returns up to `--limit` results (default 50).
214
+ - **Status** must be one of: `draft`, `pending_review`, `approved`, `in_progress`, `done`, `blocked`, `cancelled`.
215
+ - **Blocking** creates a `blocks` edge between two existing tasks; both nodes must exist.
216
+ - **Approve** only succeeds on a node currently in `pending_review`.
147
217
 
148
218
  ## Output Format
149
219
 
150
- All commands output JSON to stdout. Errors go to stderr as `{ "error": "message" }`.
220
+ All commands output JSON to stdout. Errors go to stderr as `{ "error": "message" }` (with an optional `code`), and the process exits non-zero.
221
+
222
+ On the hosted server, an expired or missing subscription surfaces as an error like `An active subscription is required. Run \`flowy billing checkout\` to subscribe.` This never happens in local mode.
151
223
 
152
224
  ## Environment Variables
153
225
 
154
226
  | Variable | Description |
155
227
  |----------|-------------|
228
+ | `FLOWY_API_URL` | GraphQL endpoint (defaults: hosted in remote mode, `http://localhost:4000/graphql` in local mode) |
229
+ | `FLOWY_API_KEY` | API key (remote mode; set by `flowy setup remote`) |
156
230
  | `FLOWY_PROJECT` | Override active project by name |
157
231
  | `FLOWY_FEATURE` | Override active feature by ID |
158
- | `FLOWY_API_URL` | GraphQL endpoint |
159
- | `FLOWY_API_KEY` | API key (from setup) |
@@ -97,16 +97,37 @@ describe('setup command', () => {
97
97
  )
98
98
  })
99
99
 
100
- test('setup remote requires --tier', async () => {
100
+ test('setup remote registers without --tier (tier is optional)', async () => {
101
+ const mockGraphql = vi.fn().mockResolvedValue({
102
+ register: {
103
+ user: {
104
+ id: 'user_1',
105
+ email: 'test@example.com',
106
+ tier: null,
107
+ createdAt: '2026-06-13T00:00:00Z',
108
+ graceEndsAt: null,
109
+ },
110
+ apiKey: 'flowy_key',
111
+ checkoutUrl: null,
112
+ },
113
+ })
114
+ vi.doMock('../util/client.ts', () => ({
115
+ graphql: mockGraphql,
116
+ }))
117
+ mockSpawnSync.mockReturnValue({ status: 0, stdout: Buffer.from('') })
118
+
101
119
  const { setupCommand } = await import('./setup.ts')
102
120
  await setupCommand.parseAsync(['remote', '--email', 'test@example.com'], {
103
121
  from: 'user',
104
122
  })
105
123
 
106
- expect(mockOutputError).toHaveBeenCalledWith(
107
- expect.objectContaining({
108
- message: expect.stringContaining('--tier is required'),
109
- }),
124
+ // No tier required: registration proceeds, no usage error raised.
125
+ expect(mockOutputError).not.toHaveBeenCalled()
126
+ expect(mockGraphql).toHaveBeenCalledOnce()
127
+ const [, variables] = mockGraphql.mock.calls[0]!
128
+ expect(variables).toEqual({ email: 'test@example.com', tier: undefined })
129
+ expect(mockSaveConfig).toHaveBeenCalledWith(
130
+ expect.objectContaining({ mode: 'remote', apiKey: 'flowy_key' }),
110
131
  )
111
132
  })
112
133
 
@@ -221,4 +242,68 @@ describe('setup command', () => {
221
242
  }),
222
243
  )
223
244
  })
245
+
246
+ test('setup local warns when the skill install fails', async () => {
247
+ // bun add succeeds, npx skills add fails (non-zero status).
248
+ mockSpawnSync.mockImplementation((cmd: string) =>
249
+ cmd === 'npx' ? { status: 1 } : { status: 0 },
250
+ )
251
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
252
+
253
+ const { setupCommand } = await import('./setup.ts')
254
+ await setupCommand.parseAsync(['local'], { from: 'user' })
255
+
256
+ const warned = errSpy.mock.calls.map((c) => String(c[0])).join('\n')
257
+ expect(warned).toMatch(/skill/i)
258
+ expect(warned).toContain('npx skills add sqaoss/flowy')
259
+ // Setup itself still completes successfully (config saved, result emitted).
260
+ expect(mockOutput).toHaveBeenCalled()
261
+ expect(mockOutputError).not.toHaveBeenCalled()
262
+ errSpy.mockRestore()
263
+ })
264
+
265
+ test('setup local does not warn when the skill install succeeds', async () => {
266
+ mockSpawnSync.mockReturnValue({ status: 0 })
267
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
268
+
269
+ const { setupCommand } = await import('./setup.ts')
270
+ await setupCommand.parseAsync(['local'], { from: 'user' })
271
+
272
+ const warned = errSpy.mock.calls.map((c) => String(c[0])).join('\n')
273
+ expect(warned).not.toMatch(/skill/i)
274
+ errSpy.mockRestore()
275
+ })
276
+
277
+ test('setup remote warns when the skill install fails', async () => {
278
+ const mockGraphql = vi.fn().mockResolvedValue({
279
+ register: {
280
+ user: {
281
+ id: 'user_1',
282
+ email: 'a@b.com',
283
+ tier: null,
284
+ createdAt: '2026-06-13T00:00:00Z',
285
+ graceEndsAt: null,
286
+ },
287
+ apiKey: 'flowy_key',
288
+ checkoutUrl: null,
289
+ },
290
+ })
291
+ vi.doMock('../util/client.ts', () => ({ graphql: mockGraphql }))
292
+ mockSpawnSync.mockImplementation((cmd: string) =>
293
+ cmd === 'npx' ? { status: 1 } : { status: 0 },
294
+ )
295
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
296
+
297
+ const { setupCommand } = await import('./setup.ts')
298
+ await setupCommand.parseAsync(['remote', '--email', 'a@b.com'], {
299
+ from: 'user',
300
+ })
301
+
302
+ const warned = errSpy.mock.calls.map((c) => String(c[0])).join('\n')
303
+ expect(warned).toMatch(/skill/i)
304
+ expect(warned).toContain('npx skills add sqaoss/flowy')
305
+ expect(mockOutput).toHaveBeenCalled()
306
+ expect(mockOutputError).not.toHaveBeenCalled()
307
+ errSpy.mockRestore()
308
+ })
224
309
  })
@@ -8,6 +8,32 @@ export const setupCommand = new Command('setup').description(
8
8
  'Configure the Flowy CLI \u2014 use "flowy setup local" or "flowy setup remote"',
9
9
  )
10
10
 
11
+ const SKILL_PACKAGE = 'sqaoss/flowy'
12
+
13
+ /**
14
+ * Install the Flowy agent skill, surfacing failure instead of swallowing it.
15
+ *
16
+ * `npx skills add` can fail (offline, npx unavailable, registry hiccup). If it
17
+ * does, setup should still succeed \u2014 but the user must be told the skill was
18
+ * NOT installed, with the exact command to retry, rather than silently
19
+ * assuming their agent now knows the commands (F14).
20
+ */
21
+ function installSkill(): void {
22
+ const result = spawnSync('npx', ['skills', 'add', SKILL_PACKAGE, '--yes'], {
23
+ stdio: 'inherit',
24
+ })
25
+ if (result.error != null || result.status !== 0) {
26
+ const reason = result.error
27
+ ? result.error.message
28
+ : `exit code ${result.status}`
29
+ console.error(
30
+ `Warning: failed to install the Flowy agent skill (${reason}). ` +
31
+ `Your agent will not know Flowy's commands until you install it manually:\n` +
32
+ ` npx skills add ${SKILL_PACKAGE}`,
33
+ )
34
+ }
35
+ }
36
+
11
37
  setupCommand
12
38
  .command('local')
13
39
  .description('Set up Flowy with a native local server (no Docker)')
@@ -26,9 +52,7 @@ setupCommand
26
52
  config.mode = 'local'
27
53
  config.apiUrl = apiUrl
28
54
  saveConfig(config)
29
- spawnSync('npx', ['skills', 'add', 'sqaoss/flowy', '--yes'], {
30
- stdio: 'inherit',
31
- })
55
+ installSkill()
32
56
 
33
57
  output({
34
58
  mode: 'local',
@@ -46,20 +70,16 @@ setupCommand
46
70
  .description('Connect to the hosted Flowy service')
47
71
  .option('--email <email>', 'Email address for registration')
48
72
  .addOption(
49
- new Option('--tier <tier>', 'Subscription tier').choices([
50
- 'explorer',
51
- 'pro',
52
- 'team',
53
- ]),
73
+ new Option(
74
+ '--tier <tier>',
75
+ 'Subscription tier (optional — pick one later at checkout)',
76
+ ).choices(['explorer', 'pro', 'team']),
54
77
  )
55
78
  .action(async (opts) => {
56
79
  try {
57
80
  if (!opts.email) {
58
81
  throw new Error('--email is required for registration')
59
82
  }
60
- if (!opts.tier) {
61
- throw new Error('--tier is required for registration')
62
- }
63
83
 
64
84
  const { graphql } = await import('../util/client.ts')
65
85
 
@@ -81,7 +101,7 @@ setupCommand
81
101
  checkoutUrl: string
82
102
  }
83
103
  }>(
84
- `mutation Register($email: String!, $tier: String!) {
104
+ `mutation Register($email: String!, $tier: String) {
85
105
  register(email: $email, tier: $tier) {
86
106
  user { id email tier createdAt graceEndsAt }
87
107
  apiKey
@@ -94,9 +114,7 @@ setupCommand
94
114
  config.apiKey = data.register.apiKey
95
115
  saveConfig(config)
96
116
 
97
- spawnSync('npx', ['skills', 'add', 'sqaoss/flowy', '--yes'], {
98
- stdio: 'inherit',
99
- })
117
+ installSkill()
100
118
 
101
119
  output(data.register)
102
120
  } catch (error) {