@cliangdev/flux-plugin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +11 -7
  2. package/agents/coder.md +150 -25
  3. package/bin/install.cjs +171 -16
  4. package/commands/breakdown.md +47 -10
  5. package/commands/dashboard.md +29 -0
  6. package/commands/flux.md +92 -12
  7. package/commands/implement.md +166 -17
  8. package/commands/linear.md +6 -5
  9. package/commands/prd.md +996 -82
  10. package/manifest.json +2 -1
  11. package/package.json +9 -11
  12. package/skills/flux-orchestrator/SKILL.md +11 -3
  13. package/skills/prd-writer/SKILL.md +761 -0
  14. package/skills/ux-ui-design/SKILL.md +346 -0
  15. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  16. package/src/__tests__/version.test.ts +37 -0
  17. package/src/adapters/local/.gitkeep +0 -0
  18. package/src/dashboard/__tests__/api.test.ts +211 -0
  19. package/src/dashboard/browser.ts +35 -0
  20. package/src/dashboard/public/app.js +869 -0
  21. package/src/dashboard/public/index.html +90 -0
  22. package/src/dashboard/public/styles.css +807 -0
  23. package/src/dashboard/public/vendor/highlight.css +10 -0
  24. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  25. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  26. package/src/dashboard/server.ts +296 -0
  27. package/src/dashboard/watchers.ts +83 -0
  28. package/src/server/__tests__/config.test.ts +163 -0
  29. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  30. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  31. package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
  32. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  33. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  34. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  35. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  36. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  37. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  38. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  39. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  40. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  41. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  42. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  43. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  44. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  45. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  46. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  47. package/src/server/adapters/factory.ts +90 -0
  48. package/src/server/adapters/index.ts +9 -0
  49. package/src/server/adapters/linear/adapter.ts +1141 -0
  50. package/src/server/adapters/linear/client.ts +169 -0
  51. package/src/server/adapters/linear/config.ts +152 -0
  52. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  53. package/src/server/adapters/linear/helpers/index.ts +7 -0
  54. package/src/server/adapters/linear/index.ts +16 -0
  55. package/src/server/adapters/linear/mappers/description.ts +136 -0
  56. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  57. package/src/server/adapters/linear/mappers/index.ts +27 -0
  58. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  59. package/src/server/adapters/linear/mappers/task.ts +82 -0
  60. package/src/server/adapters/linear/types.ts +264 -0
  61. package/src/server/adapters/local-adapter.ts +1009 -0
  62. package/src/server/adapters/types.ts +293 -0
  63. package/src/server/config.ts +73 -0
  64. package/src/server/db/__tests__/queries.test.ts +473 -0
  65. package/src/server/db/ids.ts +17 -0
  66. package/src/server/db/index.ts +69 -0
  67. package/src/server/db/queries.ts +142 -0
  68. package/src/server/db/refs.ts +60 -0
  69. package/src/server/db/schema.ts +97 -0
  70. package/src/server/db/sqlite.ts +10 -0
  71. package/src/server/index.ts +81 -0
  72. package/src/server/tools/__tests__/crud.test.ts +411 -0
  73. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  74. package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
  75. package/src/server/tools/__tests__/query.test.ts +405 -0
  76. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  77. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  78. package/src/server/tools/configure-linear.ts +373 -0
  79. package/src/server/tools/create-epic.ts +44 -0
  80. package/src/server/tools/create-prd.ts +40 -0
  81. package/src/server/tools/create-task.ts +47 -0
  82. package/src/server/tools/criteria.ts +50 -0
  83. package/src/server/tools/delete-entity.ts +76 -0
  84. package/src/server/tools/dependencies.ts +55 -0
  85. package/src/server/tools/get-entity.ts +240 -0
  86. package/src/server/tools/get-linear-url.ts +28 -0
  87. package/src/server/tools/get-stats.ts +52 -0
  88. package/src/server/tools/get-version.ts +20 -0
  89. package/src/server/tools/index.ts +158 -0
  90. package/src/server/tools/init-project.ts +108 -0
  91. package/src/server/tools/query-entities.ts +167 -0
  92. package/src/server/tools/render-status.ts +219 -0
  93. package/src/server/tools/update-entity.ts +140 -0
  94. package/src/server/tools/update-status.ts +166 -0
  95. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  96. package/src/server/utils/logger.ts +9 -0
  97. package/src/server/utils/mcp-response.ts +254 -0
  98. package/src/server/utils/status-transitions.ts +160 -0
  99. package/src/status-line/__tests__/status-line.test.ts +215 -0
  100. package/src/status-line/index.ts +147 -0
  101. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  102. package/src/utils/__tests__/display.test.ts +97 -0
  103. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  104. package/src/utils/display.ts +62 -0
  105. package/src/utils/status-renderer.ts +214 -0
  106. package/src/version.ts +5 -0
  107. package/dist/server/index.js +0 -87063
  108. package/skills/prd-template/SKILL.md +0 -242
@@ -0,0 +1,240 @@
1
+ import { z } from "zod";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import { toMcpEpic, toMcpPrd, toMcpTask } from "../utils/mcp-response.js";
4
+ import {
5
+ type EntityType,
6
+ getValidTransitions,
7
+ } from "../utils/status-transitions.js";
8
+ import type { ToolDefinition } from "./index.js";
9
+
10
+ const inputSchema = z.object({
11
+ ref: z.string().min(1, "Entity reference is required"),
12
+ include: z.array(z.string()).optional(),
13
+ });
14
+
15
+ function getEntityTypeInfo(ref: string): {
16
+ code: "P" | "E" | "T";
17
+ type: EntityType;
18
+ } | null {
19
+ // Try Flux-style ref format first (e.g., FLUX-P1, FLUX-E2, FLUX-T3)
20
+ const match = ref.match(/-([PET])\d+$/);
21
+ if (match) {
22
+ const code = match[1] as "P" | "E" | "T";
23
+ const type: EntityType =
24
+ code === "P" ? "prd" : code === "E" ? "epic" : "task";
25
+ return { code, type };
26
+ }
27
+ // For other ref formats (e.g., Linear: CAL-10), return null to indicate
28
+ // we need to try each entity type
29
+ return null;
30
+ }
31
+
32
+ // Valid includes per entity type - now returns summaries, not full data
33
+ const VALID_INCLUDES: Record<string, string[]> = {
34
+ P: ["epics", "tasks", "criteria", "dependencies"],
35
+ E: ["tasks", "criteria", "dependencies"],
36
+ T: ["criteria", "dependencies"],
37
+ };
38
+
39
+ async function handler(input: unknown) {
40
+ const parsed = inputSchema.parse(input);
41
+ const adapter = getAdapter();
42
+
43
+ const typeInfo = getEntityTypeInfo(parsed.ref);
44
+ const includes = parsed.include || [];
45
+
46
+ // If ref format is unknown (e.g., Linear refs like CAL-10), try each entity type
47
+ let entityCode: "P" | "E" | "T";
48
+ let entityType: EntityType;
49
+
50
+ if (typeInfo) {
51
+ entityCode = typeInfo.code;
52
+ entityType = typeInfo.type;
53
+ } else {
54
+ // Try PRD first, then Epic, then Task
55
+ const prd = await adapter.getPrd(parsed.ref);
56
+ if (prd) {
57
+ entityCode = "P";
58
+ entityType = "prd";
59
+ } else {
60
+ const epic = await adapter.getEpic(parsed.ref);
61
+ if (epic) {
62
+ entityCode = "E";
63
+ entityType = "epic";
64
+ } else {
65
+ const task = await adapter.getTask(parsed.ref);
66
+ if (task) {
67
+ entityCode = "T";
68
+ entityType = "task";
69
+ } else {
70
+ throw new Error(`Entity not found: ${parsed.ref}`);
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ const validIncludes = VALID_INCLUDES[entityCode];
77
+ let result: Record<string, unknown>;
78
+
79
+ if (entityCode === "P") {
80
+ const prd = await adapter.getPrd(parsed.ref);
81
+ if (!prd) throw new Error(`Entity not found: ${parsed.ref}`);
82
+
83
+ result = { ...toMcpPrd(prd) };
84
+
85
+ // Add available status transitions
86
+ result.available_transitions = getValidTransitions(entityType, prd.status);
87
+
88
+ // Always include dependencies by default
89
+ const deps = await adapter.getDependencies(parsed.ref);
90
+ result.dependencies = deps;
91
+
92
+ // Process includes
93
+ for (const inc of includes) {
94
+ if (!validIncludes.includes(inc)) continue;
95
+
96
+ if (inc === "epics") {
97
+ const epicsResult = await adapter.listEpics(
98
+ { prdRef: parsed.ref },
99
+ { limit: 100 },
100
+ );
101
+ result.epics = epicsResult.items.map((e) => ({
102
+ ref: e.ref,
103
+ title: e.title,
104
+ status: e.status,
105
+ }));
106
+ result.epic_count = epicsResult.total;
107
+ }
108
+
109
+ if (inc === "tasks") {
110
+ // Get tasks via epics
111
+ const epicsResult = await adapter.listEpics(
112
+ { prdRef: parsed.ref },
113
+ { limit: 100 },
114
+ );
115
+ const taskRefs: { ref: string; title: string; status: string }[] = [];
116
+ for (const epic of epicsResult.items) {
117
+ const tasksResult = await adapter.listTasks(
118
+ { epicRef: epic.ref },
119
+ { limit: 100 },
120
+ );
121
+ taskRefs.push(
122
+ ...tasksResult.items.map((t) => ({
123
+ ref: t.ref,
124
+ title: t.title,
125
+ status: t.status,
126
+ })),
127
+ );
128
+ }
129
+ result.tasks = taskRefs;
130
+ result.task_count = taskRefs.length;
131
+ }
132
+
133
+ if (inc === "criteria") {
134
+ // PRD: get criteria count from epics and tasks
135
+ const epicsResult = await adapter.listEpics(
136
+ { prdRef: parsed.ref },
137
+ { limit: 100 },
138
+ );
139
+ let totalCriteria = 0;
140
+ let metCriteria = 0;
141
+ for (const epic of epicsResult.items) {
142
+ const epicCriteria = await adapter.getCriteria(epic.ref);
143
+ totalCriteria += epicCriteria.length;
144
+ metCriteria += epicCriteria.filter((c) => c.isMet).length;
145
+
146
+ const tasksResult = await adapter.listTasks(
147
+ { epicRef: epic.ref },
148
+ { limit: 100 },
149
+ );
150
+ for (const task of tasksResult.items) {
151
+ const taskCriteria = await adapter.getCriteria(task.ref);
152
+ totalCriteria += taskCriteria.length;
153
+ metCriteria += taskCriteria.filter((c) => c.isMet).length;
154
+ }
155
+ }
156
+ result.criteria_count = totalCriteria;
157
+ result.criteria_met = metCriteria;
158
+ }
159
+ }
160
+
161
+ // Add hint for PRDs about where to find full content
162
+ if (prd.folderPath) {
163
+ result.content_hint =
164
+ "Use Read tool on folder_path + '/prd.md' for full PRD content";
165
+ }
166
+ } else if (entityCode === "E") {
167
+ const epic = await adapter.getEpic(parsed.ref);
168
+ if (!epic) throw new Error(`Entity not found: ${parsed.ref}`);
169
+
170
+ result = { ...toMcpEpic(epic) };
171
+ result.available_transitions = getValidTransitions(entityType, epic.status);
172
+
173
+ // Always include dependencies by default
174
+ const deps = await adapter.getDependencies(parsed.ref);
175
+ result.dependencies = deps;
176
+
177
+ for (const inc of includes) {
178
+ if (!validIncludes.includes(inc)) continue;
179
+
180
+ if (inc === "tasks") {
181
+ const tasksResult = await adapter.listTasks(
182
+ { epicRef: parsed.ref },
183
+ { limit: 100 },
184
+ );
185
+ result.tasks = tasksResult.items.map((t) => ({
186
+ ref: t.ref,
187
+ title: t.title,
188
+ status: t.status,
189
+ }));
190
+ result.task_count = tasksResult.total;
191
+ }
192
+
193
+ if (inc === "criteria") {
194
+ const criteria = await adapter.getCriteria(parsed.ref);
195
+ result.criteria = criteria.map((c) => ({
196
+ id: c.id,
197
+ criteria: c.criteria,
198
+ is_met: c.isMet,
199
+ }));
200
+ result.criteria_count = criteria.length;
201
+ result.criteria_met = criteria.filter((c) => c.isMet).length;
202
+ }
203
+ }
204
+ } else {
205
+ const task = await adapter.getTask(parsed.ref);
206
+ if (!task) throw new Error(`Entity not found: ${parsed.ref}`);
207
+
208
+ result = { ...toMcpTask(task) };
209
+ result.available_transitions = getValidTransitions(entityType, task.status);
210
+
211
+ // Always include dependencies by default
212
+ const deps = await adapter.getDependencies(parsed.ref);
213
+ result.dependencies = deps;
214
+
215
+ for (const inc of includes) {
216
+ if (!validIncludes.includes(inc)) continue;
217
+
218
+ if (inc === "criteria") {
219
+ const criteria = await adapter.getCriteria(parsed.ref);
220
+ result.criteria = criteria.map((c) => ({
221
+ id: c.id,
222
+ criteria: c.criteria,
223
+ is_met: c.isMet,
224
+ }));
225
+ result.criteria_count = criteria.length;
226
+ result.criteria_met = criteria.filter((c) => c.isMet).length;
227
+ }
228
+ }
229
+ }
230
+
231
+ return result;
232
+ }
233
+
234
+ export const getEntityTool: ToolDefinition = {
235
+ name: "get_entity",
236
+ description:
237
+ "Get a single entity by reference with optional summaries. Required: ref (e.g., 'FLUX-P1'). Optional: include array - PRD: ['epics','tasks','criteria','dependencies'], Epic: ['tasks','criteria','dependencies'], Task: ['criteria','dependencies']. Returns entity with available_transitions (valid next statuses), counts, and refs. For PRDs, use Read tool on folder_path + '/prd.md' for full content.",
238
+ inputSchema,
239
+ handler,
240
+ };
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import type { LinearAdapter } from "../adapters/linear/adapter.js";
4
+ import { linearConfigExists } from "../adapters/linear/config.js";
5
+ import type { ToolDefinition } from "./index.js";
6
+
7
+ const inputSchema = z.object({
8
+ ref: z.string().min(1, "Entity ref is required"),
9
+ });
10
+
11
+ async function handler(input: unknown) {
12
+ const parsed = inputSchema.parse(input);
13
+
14
+ if (!linearConfigExists()) {
15
+ throw new Error("Linear not configured. Run configure_linear first.");
16
+ }
17
+
18
+ const adapter = getAdapter() as LinearAdapter;
19
+ return adapter.getLinearUrl(parsed.ref);
20
+ }
21
+
22
+ export const getLinearUrlTool: ToolDefinition = {
23
+ name: "get_linear_url",
24
+ description:
25
+ "Get the Linear web URL for any entity ref. Required: ref (project ID like 'proj_abc123' or issue identifier like 'ENG-42'). Returns {url, type, name/identifier}.",
26
+ inputSchema,
27
+ handler,
28
+ };
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { getAdapter } from "../adapters/index.js";
3
+ import { toMcpStats } from "../utils/mcp-response.js";
4
+ import type { ToolDefinition } from "./index.js";
5
+
6
+ const inputSchema = z.object({});
7
+
8
+ // Re-export interface for backward compatibility with render-status.ts
9
+ export interface ProjectStats {
10
+ prds: {
11
+ total: number;
12
+ draft: number;
13
+ approved: number;
14
+ completed: number;
15
+ archived: number;
16
+ };
17
+ epics: {
18
+ total: number;
19
+ pending: number;
20
+ in_progress: number;
21
+ completed: number;
22
+ };
23
+ tasks: {
24
+ total: number;
25
+ pending: number;
26
+ in_progress: number;
27
+ completed: number;
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Compute project statistics using the adapter.
33
+ * Returns MCP-formatted stats (snake_case).
34
+ * This is exported for use by render-status.ts
35
+ */
36
+ export async function computeStats(): Promise<ProjectStats> {
37
+ const adapter = getAdapter();
38
+ const stats = await adapter.getStats();
39
+ return toMcpStats(stats);
40
+ }
41
+
42
+ async function handler(_input: unknown) {
43
+ return computeStats();
44
+ }
45
+
46
+ export const getStatsTool: ToolDefinition = {
47
+ name: "get_stats",
48
+ description:
49
+ "Get aggregated project statistics. No parameters. Returns counts by status: {prds: {total, draft, approved, completed, archived}, epics: {total, pending, in_progress, completed}, tasks: {total, pending, in_progress, completed}}. Note: totals exclude archived PRDs and their epics/tasks. The archived count shows how many PRDs are archived.",
50
+ inputSchema,
51
+ handler,
52
+ };
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ import { VERSION } from "../../version.js";
3
+ import type { ToolDefinition } from "./index.js";
4
+
5
+ const inputSchema = z.object({});
6
+
7
+ async function handler(_input: unknown) {
8
+ return {
9
+ version: VERSION,
10
+ name: "flux-plugin",
11
+ };
12
+ }
13
+
14
+ export const getVersionTool: ToolDefinition = {
15
+ name: "get_version",
16
+ description:
17
+ "Get Flux plugin version information. No parameters required. Returns {version: string, name: 'flux-plugin'}.",
18
+ inputSchema,
19
+ handler,
20
+ };
@@ -0,0 +1,158 @@
1
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ CallToolRequestSchema,
4
+ ListToolsRequestSchema,
5
+ } from "@modelcontextprotocol/sdk/types.js";
6
+ import { z } from "zod";
7
+ import { config } from "../config.js";
8
+ import { logger } from "../utils/logger.js";
9
+
10
+ const TOOLS_WITHOUT_PROJECT = ["init_project", "get_version"];
11
+
12
+ export interface ToolDefinition {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: z.ZodType;
16
+ handler: (input: unknown) => Promise<unknown>;
17
+ }
18
+
19
+ export interface ToolError {
20
+ error: true;
21
+ message: string;
22
+ code: string;
23
+ }
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
+
38
+ export function createError(message: string, code: string): ToolError {
39
+ return { error: true, message, code };
40
+ }
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
+
75
+ export function registerTools(server: Server, tools: ToolDefinition[]) {
76
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
77
+ tools: tools.map((t) => ({
78
+ name: t.name,
79
+ description: t.description,
80
+ inputSchema: z.toJSONSchema(t.inputSchema),
81
+ })),
82
+ }));
83
+
84
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
85
+ const toolName = request.params.name;
86
+ logger.debug(`Calling tool: ${toolName}`, request.params.arguments);
87
+
88
+ const tool = tools.find((t) => t.name === toolName);
89
+ if (!tool) {
90
+ logger.error(`Unknown tool: ${toolName}`);
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: JSON.stringify(
96
+ createError(`Unknown tool: ${toolName}`, "UNKNOWN_TOOL"),
97
+ ),
98
+ },
99
+ ],
100
+ isError: true,
101
+ };
102
+ }
103
+
104
+ if (!TOOLS_WITHOUT_PROJECT.includes(toolName) && !config.projectExists) {
105
+ const error = createProjectNotInitializedError(
106
+ process.cwd(),
107
+ config.projectRoot,
108
+ );
109
+ logger.error(error.message);
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: JSON.stringify(error, null, 2),
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+
121
+ try {
122
+ const result = await tool.handler(request.params.arguments);
123
+ logger.debug(`Tool ${toolName} completed successfully`);
124
+ return {
125
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
126
+ };
127
+ } catch (err) {
128
+ const message = err instanceof Error ? err.message : String(err);
129
+ logger.error(`Tool ${toolName} failed: ${message}`);
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: JSON.stringify(createError(message, "TOOL_ERROR")),
135
+ },
136
+ ],
137
+ isError: true,
138
+ };
139
+ }
140
+ });
141
+ }
142
+
143
+ export { configureLinearTool } from "./configure-linear.js";
144
+ export { createEpicTool } from "./create-epic.js";
145
+ export { createPrdTool } from "./create-prd.js";
146
+ export { createTaskTool } from "./create-task.js";
147
+ export { addCriteriaTool, markCriteriaMetTool } from "./criteria.js";
148
+ export { deleteEntityTool } from "./delete-entity.js";
149
+ export { addDependencyTool, removeDependencyTool } from "./dependencies.js";
150
+ export { getEntityTool } from "./get-entity.js";
151
+ export { getLinearUrlTool } from "./get-linear-url.js";
152
+ export { getStatsTool } from "./get-stats.js";
153
+ export { getVersionTool } from "./get-version.js";
154
+ export { initProjectTool } from "./init-project.js";
155
+ export { queryEntitiesTool } from "./query-entities.js";
156
+ export { renderStatusTool } from "./render-status.js";
157
+ export { updateEntityTool } from "./update-entity.js";
158
+ export { updateStatusTool } from "./update-status.js";
@@ -0,0 +1,108 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { z } from "zod";
4
+ import type { AdapterConfig } from "../adapters/types.js";
5
+ import { config } from "../config.js";
6
+ import {
7
+ generateId,
8
+ generatePrefix,
9
+ getDb,
10
+ initDb,
11
+ insert,
12
+ } from "../db/index.js";
13
+ import type { ToolDefinition } from "./index.js";
14
+
15
+ function getStatusLineBinaryPath(): string {
16
+ const thisDir = dirname(new URL(import.meta.url).pathname);
17
+ return join(thisDir, "..", "..", "..", "bin", "flux-status");
18
+ }
19
+
20
+ function setupClaudeSettings(projectRoot: string): void {
21
+ const claudeDir = join(projectRoot, ".claude");
22
+ const settingsPath = join(claudeDir, "settings.local.json");
23
+ const statusBinaryPath = getStatusLineBinaryPath();
24
+
25
+ if (!existsSync(statusBinaryPath)) {
26
+ return;
27
+ }
28
+
29
+ if (!existsSync(claudeDir)) {
30
+ mkdirSync(claudeDir, { recursive: true });
31
+ }
32
+
33
+ let settings: Record<string, unknown> = {};
34
+ if (existsSync(settingsPath)) {
35
+ try {
36
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
37
+ } catch {
38
+ settings = {};
39
+ }
40
+ }
41
+
42
+ if (!settings.statusLine) {
43
+ settings.statusLine = {
44
+ type: "command",
45
+ command: statusBinaryPath,
46
+ };
47
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
48
+ }
49
+ }
50
+
51
+ const adapterTypes = ["local", "specflux", "linear", "notion"] as const;
52
+
53
+ const inputSchema = z.object({
54
+ name: z.string().min(1, "Project name is required"),
55
+ vision: z.string().min(1, "Project vision is required"),
56
+ adapter: z.enum(adapterTypes).optional().default("local"),
57
+ });
58
+
59
+ async function handler(input: unknown) {
60
+ const parsed = inputSchema.parse(input);
61
+
62
+ if (existsSync(config.fluxPath)) {
63
+ throw new Error("Flux project already initialized in this directory");
64
+ }
65
+
66
+ mkdirSync(config.fluxPath, { recursive: true });
67
+
68
+ const refPrefix = generatePrefix(parsed.name);
69
+ const adapterConfig: AdapterConfig = { type: parsed.adapter };
70
+
71
+ const projectJson = {
72
+ name: parsed.name,
73
+ vision: parsed.vision,
74
+ ref_prefix: refPrefix,
75
+ project_root: config.projectRoot,
76
+ adapter: adapterConfig,
77
+ created_at: new Date().toISOString(),
78
+ };
79
+
80
+ writeFileSync(config.projectJsonPath, JSON.stringify(projectJson, null, 2));
81
+ config.clearCache();
82
+
83
+ initDb();
84
+
85
+ const projectId = generateId("proj");
86
+ const db = getDb();
87
+ insert(db, "projects", {
88
+ id: projectId,
89
+ name: parsed.name,
90
+ ref_prefix: refPrefix,
91
+ });
92
+
93
+ setupClaudeSettings(config.projectRoot);
94
+
95
+ return {
96
+ success: true,
97
+ project: projectJson,
98
+ message: `Flux project initialized. Run /flux:prd to start planning.`,
99
+ };
100
+ }
101
+
102
+ export const initProjectTool: ToolDefinition = {
103
+ name: "init_project",
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.",
106
+ inputSchema,
107
+ handler,
108
+ };