@cliangdev/flux-plugin 0.2.0-dev.dc5e2c4 → 0.2.0-dev.e34d43b

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.
@@ -10,7 +10,12 @@ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
10
10
  import { z } from "zod";
11
11
  import { config } from "../../config.js";
12
12
  import { closeDb, initDb } from "../../db/index.js";
13
- import { createError, registerTools, type ToolDefinition } from "../index.js";
13
+ import {
14
+ createError,
15
+ createProjectNotInitializedError,
16
+ registerTools,
17
+ type ToolDefinition,
18
+ } from "../index.js";
14
19
 
15
20
  describe("MCP Interface", () => {
16
21
  beforeEach(() => {
@@ -44,6 +49,42 @@ describe("MCP Interface", () => {
44
49
  });
45
50
  });
46
51
 
52
+ describe("createProjectNotInitializedError", () => {
53
+ test("creates error with setup instructions", () => {
54
+ const error = createProjectNotInitializedError("/test/cwd", "/test/root");
55
+
56
+ expect(error.error).toBe(true);
57
+ expect(error.code).toBe("PROJECT_NOT_INITIALIZED");
58
+ expect(error.message).toContain("/test/cwd");
59
+ expect(error.message).toContain("/test/root");
60
+ expect(error.setup.instructions).toBeDefined();
61
+ expect(error.setup.options).toHaveLength(2);
62
+ });
63
+
64
+ test("includes command option for interactive setup", () => {
65
+ const error = createProjectNotInitializedError("/cwd", "/root");
66
+ const commandOption = error.setup.options.find(
67
+ (o) => o.method === "command",
68
+ );
69
+
70
+ expect(commandOption).toBeDefined();
71
+ expect(commandOption?.name).toBe("/flux");
72
+ expect(commandOption?.description).toContain("Interactive");
73
+ });
74
+
75
+ test("includes tool option with required params", () => {
76
+ const error = createProjectNotInitializedError("/cwd", "/root");
77
+ const toolOption = error.setup.options.find((o) => o.method === "tool");
78
+
79
+ expect(toolOption).toBeDefined();
80
+ expect(toolOption?.name).toBe("init_project");
81
+ expect(toolOption?.params).toBeDefined();
82
+ expect(toolOption?.params?.name).toBeDefined();
83
+ expect(toolOption?.params?.vision).toBeDefined();
84
+ expect(toolOption?.params?.adapter).toBeDefined();
85
+ });
86
+ });
87
+
47
88
  describe("registerTools", () => {
48
89
  test("sets up list tools handler", () => {
49
90
  const handlers: Record<string, Function> = {};
@@ -287,7 +328,7 @@ describe("Project Validation", () => {
287
328
  }
288
329
  });
289
330
 
290
- test("returns PROJECT_NOT_FOUND for tools requiring project", async () => {
331
+ test("returns PROJECT_NOT_INITIALIZED with setup instructions for tools requiring project", async () => {
291
332
  const handlers: Record<string, Function> = {};
292
333
  const mockServer = {
293
334
  setRequestHandler: mock((schema: any, handler: Function) => {
@@ -317,7 +358,11 @@ describe("Project Validation", () => {
317
358
  expect(result.isError).toBe(true);
318
359
  const parsedError = JSON.parse(result.content[0].text);
319
360
  expect(parsedError.error).toBe(true);
320
- expect(parsedError.code).toBe("PROJECT_NOT_FOUND");
361
+ expect(parsedError.code).toBe("PROJECT_NOT_INITIALIZED");
362
+ expect(parsedError.setup).toBeDefined();
363
+ expect(parsedError.setup.instructions).toBeDefined();
364
+ expect(parsedError.setup.options).toBeDefined();
365
+ expect(parsedError.setup.options.length).toBe(2);
321
366
  });
322
367
 
323
368
  test("allows init_project without existing project", async () => {
@@ -353,7 +398,7 @@ describe("Project Validation", () => {
353
398
  expect(handlerMock).toHaveBeenCalled();
354
399
  });
355
400
 
356
- test("allows get_project_context without existing project", async () => {
401
+ test("allows get_version without existing project", async () => {
357
402
  const handlers: Record<string, Function> = {};
358
403
  const mockServer = {
359
404
  setRequestHandler: mock((schema: any, handler: Function) => {
@@ -363,11 +408,11 @@ describe("Project Validation", () => {
363
408
  }),
364
409
  };
365
410
 
366
- const handlerMock = mock(async () => ({ initialized: false }));
411
+ const handlerMock = mock(async () => ({ version: "1.0.0" }));
367
412
 
368
413
  const testTool: ToolDefinition = {
369
- name: "get_project_context",
370
- description: "Get project context",
414
+ name: "get_version",
415
+ description: "Get version",
371
416
  inputSchema: z.object({}),
372
417
  handler: handlerMock,
373
418
  };
@@ -377,7 +422,7 @@ describe("Project Validation", () => {
377
422
  const callHandler = handlers.call;
378
423
  const result = await callHandler({
379
424
  params: {
380
- name: "get_project_context",
425
+ name: "get_version",
381
426
  arguments: {},
382
427
  },
383
428
  });
@@ -385,4 +430,49 @@ describe("Project Validation", () => {
385
430
  expect(result.isError).toBeUndefined();
386
431
  expect(handlerMock).toHaveBeenCalled();
387
432
  });
433
+
434
+ test("setup options have correct structure", async () => {
435
+ const handlers: Record<string, Function> = {};
436
+ const mockServer = {
437
+ setRequestHandler: mock((schema: any, handler: Function) => {
438
+ if (schema.shape?.method?.value === "tools/call") {
439
+ handlers.call = handler;
440
+ }
441
+ }),
442
+ };
443
+
444
+ const testTool: ToolDefinition = {
445
+ name: "query_entities",
446
+ description: "Query entities",
447
+ inputSchema: z.object({ type: z.string() }),
448
+ handler: async () => ({ items: [] }),
449
+ };
450
+
451
+ registerTools(mockServer as any, [testTool]);
452
+
453
+ const callHandler = handlers.call;
454
+ const result = await callHandler({
455
+ params: {
456
+ name: "query_entities",
457
+ arguments: { type: "prd" },
458
+ },
459
+ });
460
+
461
+ expect(result.isError).toBe(true);
462
+ const parsedError = JSON.parse(result.content[0].text);
463
+
464
+ const commandOption = parsedError.setup.options.find(
465
+ (o: any) => o.method === "command",
466
+ );
467
+ expect(commandOption.name).toBe("/flux");
468
+ expect(commandOption.description).toBeTruthy();
469
+
470
+ const toolOption = parsedError.setup.options.find(
471
+ (o: any) => o.method === "tool",
472
+ );
473
+ expect(toolOption.name).toBe("init_project");
474
+ expect(toolOption.params.name).toBeTruthy();
475
+ expect(toolOption.params.vision).toBeTruthy();
476
+ expect(toolOption.params.adapter).toBeTruthy();
477
+ });
388
478
  });
@@ -16,7 +16,6 @@ import { createPrdTool } from "../create-prd.js";
16
16
  import { createTaskTool } from "../create-task.js";
17
17
  import { addDependencyTool } from "../dependencies.js";
18
18
  import { getEntityTool } from "../get-entity.js";
19
- import { getProjectContextTool } from "../get-project-context.js";
20
19
  import { getStatsTool } from "../get-stats.js";
21
20
  import { initProjectTool } from "../init-project.js";
22
21
  import { queryEntitiesTool } from "../query-entities.js";
@@ -242,24 +241,6 @@ describe("Query MCP Tools", () => {
242
241
  });
243
242
  });
244
243
 
245
- describe("get_project_context", () => {
246
- test("returns project context when initialized", async () => {
247
- const result = (await getProjectContextTool.handler({})) as any;
248
-
249
- expect(result.initialized).toBe(true);
250
- expect(result.name).toBe("test-project");
251
- expect(result.ref_prefix).toBe("TEST");
252
- });
253
-
254
- test("returns initialized false when no project", async () => {
255
- // Remove the project.json
256
- rmSync(join(FLUX_DIR, "project.json"));
257
-
258
- const result = (await getProjectContextTool.handler({})) as any;
259
- expect(result.initialized).toBe(false);
260
- });
261
- });
262
-
263
244
  describe("get_stats", () => {
264
245
  test("returns zeroes for empty project", async () => {
265
246
  const result = (await getStatsTool.handler({})) as any;
@@ -7,11 +7,7 @@ import { z } from "zod";
7
7
  import { config } from "../config.js";
8
8
  import { logger } from "../utils/logger.js";
9
9
 
10
- const TOOLS_WITHOUT_PROJECT = [
11
- "init_project",
12
- "get_project_context",
13
- "get_version",
14
- ];
10
+ const TOOLS_WITHOUT_PROJECT = ["init_project", "get_version"];
15
11
 
16
12
  export interface ToolDefinition {
17
13
  name: string;
@@ -26,10 +22,56 @@ export interface ToolError {
26
22
  code: string;
27
23
  }
28
24
 
25
+ export interface ProjectNotInitializedError extends ToolError {
26
+ code: "PROJECT_NOT_INITIALIZED";
27
+ setup: {
28
+ instructions: string;
29
+ options: Array<{
30
+ method: "command" | "tool";
31
+ name: string;
32
+ description: string;
33
+ params?: Record<string, string>;
34
+ }>;
35
+ };
36
+ }
37
+
29
38
  export function createError(message: string, code: string): ToolError {
30
39
  return { error: true, message, code };
31
40
  }
32
41
 
42
+ export function createProjectNotInitializedError(
43
+ cwd: string,
44
+ projectRoot: string,
45
+ ): ProjectNotInitializedError {
46
+ return {
47
+ error: true,
48
+ code: "PROJECT_NOT_INITIALIZED",
49
+ message: `No Flux project found. Current directory: ${cwd}, resolved project root: ${projectRoot}`,
50
+ setup: {
51
+ instructions:
52
+ "Initialize a Flux project before using Flux tools. Use one of the following options:",
53
+ options: [
54
+ {
55
+ method: "command",
56
+ name: "/flux",
57
+ description:
58
+ "Interactive setup with guided prompts (recommended for first-time setup)",
59
+ },
60
+ {
61
+ method: "tool",
62
+ name: "init_project",
63
+ description: "Direct initialization via MCP tool",
64
+ params: {
65
+ name: "Project name (required)",
66
+ vision: "Brief project description (required)",
67
+ adapter: "local | linear (default: local)",
68
+ },
69
+ },
70
+ ],
71
+ },
72
+ };
73
+ }
74
+
33
75
  export function registerTools(server: Server, tools: ToolDefinition[]) {
34
76
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
35
77
  tools: tools.map((t) => ({
@@ -60,13 +102,16 @@ export function registerTools(server: Server, tools: ToolDefinition[]) {
60
102
  }
61
103
 
62
104
  if (!TOOLS_WITHOUT_PROJECT.includes(toolName) && !config.projectExists) {
63
- const message = `No Flux project found. Run init_project first or ensure you're in a directory with a .flux folder (or a subdirectory of one). Current directory: ${process.cwd()}, resolved project root: ${config.projectRoot}`;
64
- logger.error(message);
105
+ const error = createProjectNotInitializedError(
106
+ process.cwd(),
107
+ config.projectRoot,
108
+ );
109
+ logger.error(error.message);
65
110
  return {
66
111
  content: [
67
112
  {
68
113
  type: "text",
69
- text: JSON.stringify(createError(message, "PROJECT_NOT_FOUND")),
114
+ text: JSON.stringify(error, null, 2),
70
115
  },
71
116
  ],
72
117
  isError: true,
@@ -104,7 +149,6 @@ export { deleteEntityTool } from "./delete-entity.js";
104
149
  export { addDependencyTool, removeDependencyTool } from "./dependencies.js";
105
150
  export { getEntityTool } from "./get-entity.js";
106
151
  export { getLinearUrlTool } from "./get-linear-url.js";
107
- export { getProjectContextTool } from "./get-project-context.js";
108
152
  export { getStatsTool } from "./get-stats.js";
109
153
  export { getVersionTool } from "./get-version.js";
110
154
  export { initProjectTool } from "./init-project.js";
@@ -102,7 +102,7 @@ async function handler(input: unknown) {
102
102
  export const initProjectTool: ToolDefinition = {
103
103
  name: "init_project",
104
104
  description:
105
- "Initialize a new Flux project. Required: name, vision. Optional: adapter (local|specflux|linear|notion, default 'local'). Creates .flux/ directory with project.json and SQLite database. Returns {success, project, message}. Fails if .flux/ already exists. Run get_project_context first to check.",
105
+ "Initialize a new Flux project. Required: name, vision. Optional: adapter (local|specflux|linear|notion, default 'local'). Creates .flux/ directory with project.json and SQLite database. Returns {success, project, message}. Fails if .flux/ already exists.",
106
106
  inputSchema,
107
107
  handler,
108
108
  };
@@ -1,242 +0,0 @@
1
- ---
2
- name: flux:prd-template
3
- description: PRD structure and patterns for Flux. Use when creating or refining product requirement documents. PRDs should be concise for humans but detailed enough for AI agents to implement.
4
- user-invocable: false
5
- ---
6
-
7
- # PRD Template Skill
8
-
9
- PRDs should be **concise for human review** but contain **enough detail for AI implementation**.
10
-
11
- ## PRD Structure (Required)
12
-
13
- ```markdown
14
- # {Project Name}
15
-
16
- ## Table of Contents
17
- - [Problem](#problem)
18
- - [Users](#users)
19
- - [Solution](#solution)
20
- - [Features (MVP)](#features-mvp)
21
- - [P0: Must Have](#p0-must-have)
22
- - [P1: Should Have](#p1-should-have)
23
- - [Out of Scope](#out-of-scope)
24
- - [Constraints](#constraints)
25
- - [Open Questions](#open-questions)
26
-
27
- ## Problem
28
- {2-3 sentences: What problem are we solving? Why does it matter?}
29
-
30
- ## Users
31
- {Who has this problem? 1-2 user segments.}
32
-
33
- ## Solution
34
- {1 paragraph: What are we building to solve this?}
35
-
36
- ## Features (MVP)
37
-
38
- ### P0: Must Have
39
- - **{Feature 1}**: {One sentence description}
40
- - {Acceptance criterion 1}
41
- - {Acceptance criterion 2}
42
- - **{Feature 2}**: {One sentence description}
43
- - {Acceptance criterion}
44
-
45
- ### P1: Should Have
46
- - **{Feature}**: {Description}
47
-
48
- ### Out of Scope
49
- - {What we're NOT building}
50
- - {Future considerations}
51
-
52
- ## Constraints
53
- - **Tech Stack**: {Required technologies, if any}
54
- - **Timeline**: {Any deadlines}
55
- - **Other**: {Budget, compliance, etc.}
56
-
57
- ## Open Questions
58
- - {Unresolved decisions needing input}
59
- ```
60
-
61
- ### TOC Guidelines
62
-
63
- **For Local Adapter** (files in `.flux/prds/`):
64
- - Include TOC with anchor links (e.g., `[Problem](#problem)`)
65
- - Use lowercase with hyphens for anchors
66
- - Nest sub-sections under parent sections
67
-
68
- **For External Adapters** (Linear, Notion):
69
- - **Skip TOC** - External systems have their own navigation (Linear's outline, collapsible sections)
70
- - Linear generates unique anchor suffixes making pre-built links impossible
71
-
72
- ## Guidelines
73
-
74
- ### Keep It Short
75
- - Problem: 2-3 sentences max
76
- - Features: 3-5 P0 features for MVP
77
- - Each feature: 1 sentence + 2-4 acceptance criteria
78
- - Total PRD: 1-2 pages
79
-
80
- ### Write for AI Implementation
81
- - Acceptance criteria should be testable
82
- - Use specific, unambiguous language
83
- - Include edge cases in criteria when important
84
-
85
- ### Bad vs Good Examples
86
-
87
- **Bad feature:**
88
- > - User authentication
89
-
90
- **Good feature:**
91
- > - **User Authentication**: Users can sign up and log in with email/password
92
- > - Sign up requires email, password (min 8 chars), and name
93
- > - Login with email + password returns JWT token
94
- > - Invalid credentials show error message
95
- > - Forgot password sends reset email
96
-
97
- ## Supporting Documents (Optional)
98
-
99
- Generate based on **agent confidence** and **user request**.
100
-
101
- | Document | When to Generate |
102
- |----------|------------------|
103
- | `architecture.md` | Complex systems, multiple components, API integrations |
104
- | `wireframes.md` | UI-heavy features, user specifically requests |
105
- | `data-model.md` | Custom data storage, complex relationships |
106
- | `user-flows.md` | Multi-step processes, complex user journeys |
107
-
108
- ### Asking About Supporting Docs
109
-
110
- After PRD outline approval, ask:
111
-
112
- ```
113
- PRD outline looks good. I can also generate:
114
- - Architecture diagram (recommended for this project)
115
- - UI wireframes
116
- - Data model
117
-
118
- Which would you like? Or should I proceed with just the PRD?
119
- ```
120
-
121
- Recommend based on project type:
122
- - **Web App**: architecture, wireframes
123
- - **API/Backend**: architecture, data-model
124
- - **CLI Tool**: usually just PRD
125
- - **Mobile App**: architecture, wireframes
126
-
127
- ## Supporting Doc Templates
128
-
129
- ### architecture.md
130
-
131
- ```markdown
132
- # Architecture
133
-
134
- ## Overview
135
- {One paragraph system description}
136
-
137
- ## Components
138
-
139
- ```mermaid
140
- graph TB
141
- Client[Client] --> API[API Server]
142
- API --> DB[(Database)]
143
- API --> Cache[(Redis)]
144
- ```
145
-
146
- ## API Endpoints
147
-
148
- | Method | Path | Description |
149
- |--------|------|-------------|
150
- | POST | /api/users | Create user |
151
- | GET | /api/users/:id | Get user |
152
-
153
- ## Data Flow
154
- {Describe key data flows}
155
-
156
- ## Tech Stack
157
- - **Frontend**: {tech}
158
- - **Backend**: {tech}
159
- - **Database**: {tech}
160
- ```
161
-
162
- ### wireframes.md
163
-
164
- ```markdown
165
- # Wireframes
166
-
167
- ## Screen: {Name}
168
-
169
- ```
170
- +----------------------------------+
171
- | Logo [Login] [Sign Up]
172
- +----------------------------------+
173
- | |
174
- | Welcome to {App Name} |
175
- | |
176
- | [ Email ] |
177
- | [Password ] |
178
- | [ Login ] |
179
- | |
180
- | Forgot password? |
181
- +----------------------------------+
182
- ```
183
-
184
- **Elements:**
185
- - Logo: links to home
186
- - Login button: submits form
187
- - Forgot password: opens reset flow
188
- ```
189
-
190
- ### data-model.md
191
-
192
- ```markdown
193
- # Data Model
194
-
195
- ## ERD
196
-
197
- ```mermaid
198
- erDiagram
199
- User ||--o{ Post : creates
200
- Post ||--o{ Comment : has
201
- ```
202
-
203
- ## Tables
204
-
205
- ### users
206
- | Column | Type | Constraints |
207
- |--------|------|-------------|
208
- | id | uuid | PK |
209
- | email | varchar | unique, not null |
210
- | created_at | timestamp | default now() |
211
-
212
- ### posts
213
- | Column | Type | Constraints |
214
- |--------|------|-------------|
215
- | id | uuid | PK |
216
- | user_id | uuid | FK users.id |
217
- | content | text | not null |
218
- ```
219
-
220
- ## Workflow
221
-
222
- Check adapter type via `get_project_context` to determine storage behavior.
223
-
224
- ### Local Adapter (`adapter.type === "local"`)
225
-
226
- 1. **Interview** → Collect answers via AskUserQuestion
227
- 2. **Outline** → Generate brief outline, ask for approval
228
- 3. **Create entity** → Call `create_prd` with title and short description
229
- 4. **Write PRD** → Write full PRD to `.flux/prds/{slug}/prd.md`
230
- 5. **Set folder_path** → Call `update_entity` with `folder_path`: `.flux/prds/{slug}`
231
- 6. **Ask about docs** → Offer relevant supporting docs
232
- 7. **Generate docs** → Write requested docs to same folder
233
-
234
- ### External Adapters (`adapter.type === "linear"`, `"notion"`, etc.)
235
-
236
- 1. **Interview** → Collect answers via AskUserQuestion
237
- 2. **Outline** → Generate brief outline, ask for approval
238
- 3. **Create entity** → Call `create_prd` with title and short description
239
- 4. **Sync content** → Call `update_entity` with `description`: Full PRD markdown content
240
- 5. **Ask about docs** → Offer relevant supporting docs (stored as attachments in external system)
241
-
242
- **Note**: External adapters store all content in the external system. No local `.flux/prds/` files are created.
@@ -1,33 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { z } from "zod";
3
- import { config } from "../config.js";
4
- import type { ToolDefinition } from "./index.js";
5
-
6
- const inputSchema = z.object({});
7
-
8
- async function handler(_input: unknown) {
9
- const projectJsonPath = config.projectJsonPath;
10
-
11
- if (!existsSync(projectJsonPath)) {
12
- return { initialized: false };
13
- }
14
-
15
- try {
16
- const content = readFileSync(projectJsonPath, "utf-8");
17
- const project = JSON.parse(content);
18
- return { initialized: true, ...project };
19
- } catch (_err) {
20
- return {
21
- initialized: false,
22
- error: "Failed to read project.json",
23
- };
24
- }
25
- }
26
-
27
- export const getProjectContextTool: ToolDefinition = {
28
- name: "get_project_context",
29
- description:
30
- "Get project context from .flux/project.json. No parameters required. Returns {initialized: boolean, name?, vision?, ref_prefix?, adapter?: {type: 'local'|'specflux'|'linear'|'notion'}, created_at?}. Use this to check if a Flux project exists before other operations.",
31
- inputSchema,
32
- handler,
33
- };