@sqaoss/flowy 0.1.1 → 1.0.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/README.md CHANGED
@@ -1,81 +1,95 @@
1
1
  # Flowy
2
2
 
3
- CLI for Flowy — project management for AI coding agents.
3
+ Project management for AI coding agents.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@sqaoss/flowy)](https://www.npmjs.com/package/@sqaoss/flowy)
6
- [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE)
6
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
7
7
  [![CI](https://github.com/sqaoss/flowy/actions/workflows/ci.yml/badge.svg)](https://github.com/sqaoss/flowy/actions/workflows/ci.yml)
8
8
 
9
- > **Note:** Registration is temporarily closed while the project is in rapid development. Existing users are unaffected.
10
-
11
9
  ## What is Flowy
12
10
 
13
- Flowy is a hosted backend project management service built for AI coding agents. It uses a graph data model (nodes and edges) to represent projects, features, epics, and tasks with typed relationships between them. This package provides the CLI that agents use to interact with the Flowy API.
11
+ Flowy is a backend + CLI that gives AI coding agents structured project management. It enforces a strict hierarchy -- **client -> project -> feature -> task** -- so agents always have clear context about what they're working on.
12
+
13
+ Run it locally with SQLite and Docker, or connect to the hosted SaaS for multi-agent team collaboration.
14
+
15
+ ## Quick Start (Local Mode)
14
16
 
15
- ## Quick Start
17
+ Local mode runs a Flowy server on your machine using Docker. No account needed.
16
18
 
17
19
  ```bash
18
20
  # Install
19
21
  bun add -g @sqaoss/flowy # or: npm i -g @sqaoss/flowy
20
22
 
21
- # Register and get your API key
22
- flowy register --email you@example.com
23
- export FLOWY_API_KEY=flowy_xxx_yyy
23
+ # Start the local server (requires Docker)
24
+ flowy setup local
24
25
 
25
- # Create a project
26
- flowy node create --type project --title "Auth System"
26
+ # Set your client name
27
+ flowy client set name "Acme Corp"
27
28
 
28
- # Add tasks
29
- flowy node create --type task --title "Implement OAuth"
30
- flowy node create --type task --title "Write auth tests"
29
+ # Create a project and map it to the current directory
30
+ flowy project create "Auth System"
31
+ flowy project set "Auth System"
31
32
 
32
- # Link tasks to project
33
- flowy edge create --source <task-id> --target <project-id> --relation part_of
33
+ # Plan a feature
34
+ flowy feature create --title "SSO Support" --description sso-spec.md
35
+ flowy feature set "SSO Support"
34
36
 
35
- # Track status
37
+ # Create tasks
38
+ flowy task create --title "Implement OAuth" --description oauth.md
39
+ flowy task create --title "Write auth tests" --description "Unit + integration tests"
40
+
41
+ # Track progress
36
42
  flowy status <task-id> in_progress
37
43
  flowy status <task-id> done
38
44
 
39
45
  # Search and explore
40
46
  flowy search "OAuth" --type task
41
- flowy tree subtree <project-id> --depth 3
47
+ flowy tree <project-id> --depth 3
42
48
  ```
43
49
 
50
+ ## Remote Mode (Coming Soon)
51
+
52
+ Remote mode connects to the hosted Flowy SaaS for multi-agent collaboration, shared project state across teams, and persistent history. Registration and API key setup will happen directly through the CLI -- no website needed. This is currently a work in progress.
53
+
44
54
  ## Command Reference
45
55
 
46
56
  | Command | Description |
47
57
  |---------|-------------|
48
- | `register --email <email>` | Register and get API key |
58
+ | `setup local` | Start a local Docker server and configure the CLI |
59
+ | `setup remote` | Connect to the hosted SaaS (coming soon) |
49
60
  | `whoami` | Show current user |
50
- | `node create --type <type> --title <title> [--description] [--status] [--metadata]` | Create node |
51
- | `node get --id <id>` | Get node |
52
- | `node list [--type] [--status] [--limit] [--offset]` | List nodes |
53
- | `node update --id <id> [--title] [--description] [--status] [--metadata]` | Update node |
54
- | `node delete --id <id>` | Delete node |
61
+ | `client set name <name>` | Set client display name |
62
+ | `project create <name>` | Create project |
63
+ | `project set <name>` | Map current directory to project |
64
+ | `project list` | List all projects |
65
+ | `project show [<id>]` | Show project details |
66
+ | `feature create --title <t> --description <d>` | Create feature (requires active project) |
67
+ | `feature set <name-or-id>` | Set active feature |
68
+ | `feature unset` | Clear active feature |
69
+ | `feature list` | List features in active project |
70
+ | `feature show [<id>]` | Show feature details |
71
+ | `task create --title <t> --description <d>` | Create task (requires active feature) |
72
+ | `task list` | List tasks in active feature |
73
+ | `task show <id>` | Show task details |
74
+ | `task block <id1> <id2>` | Mark task as blocking another |
75
+ | `task unblock <id1> <id2>` | Remove block |
55
76
  | `status <id> <status>` | Update status (shorthand) |
56
- | `approve <id>` | Approve node (must be pending_review) |
57
- | `edge create --source <id> --target <id> --relation <rel>` | Create edge |
58
- | `edge list [--node <id>] [--relation <rel>]` | List edges |
59
- | `edge remove --source <id> --target <id> --relation <rel>` | Remove edge |
60
- | `search <query> [--type] [--status] [--limit]` | Search nodes |
61
- | `tree subtree <id> [--depth N]` | Show subtree |
62
- | `tree ancestors <id> [--depth N] [--relation <rel>]` | Show ancestors |
63
- | `tree descendants <id> [--depth N] [--relation <rel>]` | Show descendants |
77
+ | `approve <id>` | Approve (must be pending_review) |
78
+ | `search <query> [--type] [--status] [--limit]` | Full-text search |
79
+ | `tree <id> [--depth N]` | Show subtree |
64
80
 
65
- All commands output JSON.
81
+ All commands output JSON to stdout.
66
82
 
67
83
  ## Data Model
68
84
 
69
- ### Node Types
85
+ ### Entity Hierarchy
70
86
 
71
- `client`, `project`, `feature`, `epic`, `task`
72
-
73
- ### Edge Relations
87
+ ```
88
+ client -> project -> feature -> task
89
+ 1:many 1:many 1:many
90
+ ```
74
91
 
75
- - `part_of` -- child belongs to parent
76
- - `depends_on` -- must complete before starting
77
- - `blocks` -- prevents progress on target
78
- - `informs` -- provides context to target
92
+ Every entity belongs to its parent. No orphans.
79
93
 
80
94
  ### Status Flow
81
95
 
@@ -85,15 +99,44 @@ draft -> pending_review -> approved -> in_progress -> done
85
99
 
86
100
  Also: `blocked`, `cancelled`
87
101
 
102
+ ### Description Field
103
+
104
+ `--description` accepts a file path or an inline string:
105
+ - `--description spec.md` -- reads file content
106
+ - `--description "Do the thing"` -- sends string as-is
107
+
88
108
  ## Configuration
89
109
 
110
+ Config is stored at `~/.config/flowy/config.json`.
111
+
90
112
  | Variable | Description | Default |
91
113
  |----------|-------------|---------|
92
- | `FLOWY_API_URL` | GraphQL endpoint | `https://flowy-ai.fly.dev/graphql` |
93
- | `FLOWY_API_KEY` | API key from register | -- |
114
+ | `FLOWY_API_URL` | GraphQL endpoint | `http://localhost:4000/graphql` |
115
+ | `FLOWY_API_KEY` | API key (remote mode only) | -- |
116
+ | `FLOWY_PROJECT` | Override active project by name | -- |
117
+ | `FLOWY_FEATURE` | Override active feature by ID | -- |
94
118
 
95
- ## License
119
+ ## For AI Agents
120
+
121
+ Flowy integrates with [TanStack Intent](https://github.com/TanStack/intent) for automatic tool discovery. Run:
122
+
123
+ ```bash
124
+ npx @tanstack/intent install
125
+ ```
126
+
127
+ This auto-discovers the Flowy skill and makes it available to your agent. See [`skills/using-flowy/SKILL.md`](skills/using-flowy/SKILL.md) for the full skill reference.
96
128
 
97
- Copyright (C) 2026 SQA & Automation SRL
129
+ ## Development
130
+
131
+ ```bash
132
+ bun run test # Run CLI tests (Vitest)
133
+ bun run check # Biome lint + format
134
+ bun run typecheck # TypeScript type checking
135
+
136
+ # Server tests
137
+ cd server && bunx --bun vitest run
138
+ ```
139
+
140
+ ## License
98
141
 
99
- [AGPL-3.0](LICENSE)
142
+ Apache-2.0 -- Copyright 2026 SQA & Automation SRL
@@ -0,0 +1,14 @@
1
+ services:
2
+ server:
3
+ build: ./server
4
+ ports:
5
+ - "4000:4000"
6
+ environment:
7
+ - NODE_ENV=production
8
+ restart: unless-stopped
9
+ healthcheck:
10
+ test: ["CMD", "bun", "-e", "const r = await fetch('http://localhost:4000/health'); if (!r.ok) process.exit(1)"]
11
+ interval: 10s
12
+ timeout: 5s
13
+ retries: 3
14
+ start_period: 5s
package/package.json CHANGED
@@ -1,20 +1,28 @@
1
1
  {
2
2
  "name": "@sqaoss/flowy",
3
- "version": "0.1.1",
4
- "description": "CLI for Flowy — project management for AI coding agents. NOTE: Registration is temporarily closed while in rapid development.",
3
+ "version": "1.0.2",
4
+ "description": "CLI for Flowy — project management for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "flowy": "./src/index.ts"
8
8
  },
9
9
  "files": [
10
10
  "src/",
11
+ "server/src/",
12
+ "server/package.json",
13
+ "server/Dockerfile",
14
+ "skills/",
15
+ "!skills/_artifacts",
16
+ "docker-compose.yml",
11
17
  "LICENSE",
12
18
  "README.md"
13
19
  ],
14
20
  "publishConfig": {
15
- "access": "public"
21
+ "access": "public",
22
+ "provenance": true,
23
+ "registry": "https://registry.npmjs.org/"
16
24
  },
17
- "license": "AGPL-3.0-only",
25
+ "license": "Apache-2.0",
18
26
  "repository": {
19
27
  "type": "git",
20
28
  "url": "https://github.com/sqaoss/flowy.git"
@@ -33,24 +41,33 @@
33
41
  "coding-agent",
34
42
  "ai-agents",
35
43
  "graphql",
36
- "bun"
44
+ "bun",
45
+ "tanstack-intent"
37
46
  ],
38
47
  "scripts": {
39
48
  "cli": "bun src/index.ts",
40
49
  "check": "biome check --write .",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
41
52
  "typecheck": "tsc --noEmit",
42
53
  "prepare": "husky"
43
54
  },
44
55
  "dependencies": {
45
- "commander": "^14.0.3"
56
+ "@semantic-release/changelog": "6.0.3",
57
+ "@semantic-release/git": "10.0.1",
58
+ "commander": "^14.0.3",
59
+ "semantic-release": "25.0.3"
46
60
  },
47
61
  "devDependencies": {
48
62
  "@biomejs/biome": "2.4.4",
63
+ "@tanstack/intent": "^0.0.23",
49
64
  "@commitlint/cli": "^20.4.2",
50
65
  "@commitlint/config-conventional": "^20.4.2",
51
66
  "husky": "^9.1.7",
52
67
  "lint-staged": "^16.3.0",
53
- "typescript": "^5"
68
+ "tdd-guard-vitest": "^0.1.6",
69
+ "typescript": "^5",
70
+ "vitest": "^4.1.2"
54
71
  },
55
72
  "lint-staged": {
56
73
  "*.{ts,tsx,js,jsx}": [
@@ -0,0 +1,14 @@
1
+ FROM oven/bun:1.3.11 AS base
2
+ WORKDIR /app
3
+
4
+ FROM base AS install
5
+ COPY package.json bun.lock ./
6
+ RUN bun install --frozen-lockfile --production
7
+
8
+ FROM base
9
+ COPY --from=install /app/node_modules node_modules
10
+ COPY src src
11
+ COPY tsconfig.json .
12
+
13
+ EXPOSE 4000
14
+ CMD ["bun", "src/index.ts"]
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@sqaoss/flowy-server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch src/index.ts",
8
+ "start": "bun src/index.ts",
9
+ "test": "bunx --bun vitest run",
10
+ "test:watch": "bunx --bun vitest",
11
+ "check": "biome check --write .",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "graphql": "16.13.2",
16
+ "graphql-yoga": "5.18.1",
17
+ "nanoid": "5.1.7"
18
+ },
19
+ "devDependencies": {
20
+ "@biomejs/biome": "2.4.4",
21
+ "@types/bun": "^1.3.11",
22
+ "typescript": "^5",
23
+ "vitest": "^4.1.2"
24
+ }
25
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createDb } from './db.ts'
3
+
4
+ describe('createDb', () => {
5
+ it('creates nodes and edges tables', () => {
6
+ const db = createDb(':memory:')
7
+
8
+ const tables = db.raw
9
+ .query<{ name: string }, []>(
10
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
11
+ )
12
+ .all()
13
+ .map((r) => r.name)
14
+
15
+ expect(tables).toContain('nodes')
16
+ expect(tables).toContain('edges')
17
+
18
+ db.close()
19
+ })
20
+
21
+ it('rejects invalid node type', () => {
22
+ const db = createDb(':memory:')
23
+
24
+ expect(() =>
25
+ db.raw.run(
26
+ "INSERT INTO nodes (id, type, title) VALUES ('n1', 'invalid_type', 'Test')",
27
+ ),
28
+ ).toThrow()
29
+
30
+ db.close()
31
+ })
32
+
33
+ it('rejects invalid status', () => {
34
+ const db = createDb(':memory:')
35
+
36
+ expect(() =>
37
+ db.raw.run(
38
+ "INSERT INTO nodes (id, type, title, status) VALUES ('n1', 'project', 'Test', 'invalid_status')",
39
+ ),
40
+ ).toThrow()
41
+
42
+ db.close()
43
+ })
44
+
45
+ it('rejects invalid edge relation', () => {
46
+ const db = createDb(':memory:')
47
+
48
+ db.raw.run(
49
+ "INSERT INTO nodes (id, type, title) VALUES ('n1', 'project', 'P1')",
50
+ )
51
+ db.raw.run(
52
+ "INSERT INTO nodes (id, type, title) VALUES ('n2', 'feature', 'F1')",
53
+ )
54
+
55
+ expect(() =>
56
+ db.raw.run(
57
+ "INSERT INTO edges (source_id, target_id, relation) VALUES ('n2', 'n1', 'invalid_rel')",
58
+ ),
59
+ ).toThrow()
60
+
61
+ db.close()
62
+ })
63
+
64
+ it('creates indexes on nodes and edges', () => {
65
+ const db = createDb(':memory:')
66
+
67
+ const indexes = db.raw
68
+ .query<{ name: string }, []>(
69
+ "SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%' ORDER BY name",
70
+ )
71
+ .all()
72
+ .map((r) => r.name)
73
+
74
+ expect(indexes).toContain('idx_nodes_type')
75
+ expect(indexes).toContain('idx_nodes_status')
76
+ expect(indexes).toContain('idx_edges_target')
77
+ expect(indexes).toContain('idx_edges_source')
78
+
79
+ db.close()
80
+ })
81
+
82
+ it('enables foreign keys', () => {
83
+ const db = createDb(':memory:')
84
+
85
+ const result = db.raw
86
+ .query<{ foreign_keys: number }, []>('PRAGMA foreign_keys')
87
+ .get()
88
+
89
+ expect(result?.foreign_keys).toBe(1)
90
+
91
+ db.close()
92
+ })
93
+ })
@@ -0,0 +1,47 @@
1
+ import { Database } from 'bun:sqlite'
2
+
3
+ export type FlowyDb = ReturnType<typeof createDb>
4
+
5
+ export function createDb(path: string) {
6
+ const db = new Database(path)
7
+
8
+ db.run('PRAGMA journal_mode = WAL')
9
+ db.run('PRAGMA foreign_keys = ON')
10
+
11
+ db.run(`
12
+ CREATE TABLE IF NOT EXISTS nodes (
13
+ id TEXT PRIMARY KEY,
14
+ type TEXT NOT NULL CHECK(type IN ('project', 'feature', 'task')),
15
+ title TEXT NOT NULL,
16
+ description TEXT,
17
+ status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'pending_review', 'approved', 'in_progress', 'done', 'blocked', 'cancelled')),
18
+ metadata TEXT,
19
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
20
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
21
+ )
22
+ `)
23
+
24
+ db.run(`
25
+ CREATE TABLE IF NOT EXISTS edges (
26
+ source_id TEXT NOT NULL REFERENCES nodes(id),
27
+ target_id TEXT NOT NULL REFERENCES nodes(id),
28
+ relation TEXT NOT NULL CHECK(relation IN ('part_of', 'blocks')),
29
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
30
+ PRIMARY KEY (source_id, target_id, relation)
31
+ )
32
+ `)
33
+
34
+ db.run('CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type)')
35
+ db.run('CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status)')
36
+ db.run(
37
+ 'CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id, relation)',
38
+ )
39
+ db.run(
40
+ 'CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id, relation)',
41
+ )
42
+
43
+ return {
44
+ raw: db,
45
+ close: () => db.close(),
46
+ }
47
+ }
@@ -0,0 +1,25 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { createServer } from './index.ts'
3
+
4
+ describe('createServer', () => {
5
+ let instance: ReturnType<typeof createServer> | undefined
6
+
7
+ afterEach(() => {
8
+ instance?.close()
9
+ instance = undefined
10
+ })
11
+
12
+ it('exports a createServer function', () => {
13
+ expect(typeof createServer).toBe('function')
14
+ })
15
+
16
+ it('responds to /health with status ok', async () => {
17
+ instance = createServer({ dbPath: ':memory:', port: 0 })
18
+
19
+ const res = await fetch(`http://localhost:${instance.port}/health`)
20
+ const json = await res.json()
21
+
22
+ expect(res.status).toBe(200)
23
+ expect(json).toEqual({ status: 'ok' })
24
+ })
25
+ })
@@ -0,0 +1,45 @@
1
+ import { createSchema, createYoga } from 'graphql-yoga'
2
+ import { createDb } from './db.ts'
3
+ import { createResolvers } from './resolvers.ts'
4
+ import { typeDefs } from './schema.ts'
5
+
6
+ export function createServer(opts?: { dbPath?: string; port?: number }) {
7
+ const dbPath = opts?.dbPath ?? process.env.FLOWY_DB_PATH ?? './flowy.sqlite'
8
+ const port = opts?.port ?? Number(process.env.PORT ?? 4000)
9
+
10
+ const db = createDb(dbPath)
11
+ const resolvers = createResolvers(db)
12
+
13
+ const yoga = createYoga({
14
+ schema: createSchema({ typeDefs, resolvers }),
15
+ graphqlEndpoint: '/graphql',
16
+ })
17
+
18
+ const server = Bun.serve({
19
+ port,
20
+ fetch(req) {
21
+ const url = new URL(req.url)
22
+ if (url.pathname === '/health' && req.method === 'GET') {
23
+ return Response.json({ status: 'ok' })
24
+ }
25
+ return yoga.fetch(req)
26
+ },
27
+ })
28
+
29
+ return {
30
+ server,
31
+ port: server.port,
32
+ db,
33
+ close() {
34
+ server.stop()
35
+ db.close()
36
+ },
37
+ }
38
+ }
39
+
40
+ if (import.meta.main) {
41
+ const { port } = createServer()
42
+ console.log(`Flowy local server running on http://localhost:${port}`)
43
+ console.log(` GraphQL: http://localhost:${port}/graphql`)
44
+ console.log(` Health: http://localhost:${port}/health`)
45
+ }