@kmgeon/taskflow 0.1.3

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 (158) hide show
  1. package/README.md +374 -0
  2. package/bin/task-mcp.mjs +19 -0
  3. package/bin/task.mjs +19 -0
  4. package/docs/clean-code.md +29 -0
  5. package/docs/git.md +36 -0
  6. package/docs/guideline.md +25 -0
  7. package/docs/security.md +32 -0
  8. package/docs/step-by-step.md +29 -0
  9. package/docs/superpowers/specs/2026-03-21-cli-advisor-design.md +383 -0
  10. package/docs/superpowers/specs/2026-03-21-init-redesign-design.md +429 -0
  11. package/docs/superpowers/specs/2026-03-21-skill-architecture-design.md +362 -0
  12. package/docs/superpowers/specs/2026-03-23-t-create-task-run-design.md +40 -0
  13. package/docs/superpowers/specs/2026-03-23-task-run-design.md +44 -0
  14. package/docs/tdd.md +41 -0
  15. package/package.json +114 -0
  16. package/src/app/(protected)/dashboard/page.tsx +7 -0
  17. package/src/app/(protected)/layout.tsx +10 -0
  18. package/src/app/api/[[...hono]]/route.ts +13 -0
  19. package/src/app/example/page.tsx +11 -0
  20. package/src/app/favicon.ico +0 -0
  21. package/src/app/globals.css +168 -0
  22. package/src/app/layout.tsx +35 -0
  23. package/src/app/page.tsx +5 -0
  24. package/src/app/providers.tsx +57 -0
  25. package/src/backend/config/index.ts +36 -0
  26. package/src/backend/hono/app.ts +32 -0
  27. package/src/backend/hono/context.ts +38 -0
  28. package/src/backend/http/response.ts +64 -0
  29. package/src/backend/middleware/context.ts +23 -0
  30. package/src/backend/middleware/error.ts +31 -0
  31. package/src/backend/middleware/supabase.ts +23 -0
  32. package/src/backend/supabase/client.ts +17 -0
  33. package/src/cli/commands/__tests__/task-commands.test.ts +170 -0
  34. package/src/cli/commands/advisor.ts +45 -0
  35. package/src/cli/commands/ask.ts +50 -0
  36. package/src/cli/commands/board.ts +72 -0
  37. package/src/cli/commands/init.ts +184 -0
  38. package/src/cli/commands/list.ts +138 -0
  39. package/src/cli/commands/run.ts +143 -0
  40. package/src/cli/commands/set-status.ts +50 -0
  41. package/src/cli/commands/show.ts +28 -0
  42. package/src/cli/commands/tree.ts +72 -0
  43. package/src/cli/index.ts +38 -0
  44. package/src/cli/lib/__tests__/formatter.test.ts +123 -0
  45. package/src/cli/lib/error-boundary.test.ts +135 -0
  46. package/src/cli/lib/error-boundary.ts +70 -0
  47. package/src/cli/lib/formatter.ts +764 -0
  48. package/src/cli/lib/trd.ts +33 -0
  49. package/src/cli/lib/validate.test.ts +89 -0
  50. package/src/cli/lib/validate.ts +43 -0
  51. package/src/cli/prompts/task-run.md +25 -0
  52. package/src/components/layout/AppLayout.tsx +15 -0
  53. package/src/components/layout/Sidebar.tsx +124 -0
  54. package/src/components/ui/accordion.tsx +58 -0
  55. package/src/components/ui/avatar.tsx +50 -0
  56. package/src/components/ui/badge.tsx +36 -0
  57. package/src/components/ui/button.tsx +56 -0
  58. package/src/components/ui/card.tsx +79 -0
  59. package/src/components/ui/checkbox.tsx +30 -0
  60. package/src/components/ui/dialog.tsx +122 -0
  61. package/src/components/ui/dropdown-menu.tsx +200 -0
  62. package/src/components/ui/file-upload.tsx +50 -0
  63. package/src/components/ui/form.tsx +179 -0
  64. package/src/components/ui/input.tsx +25 -0
  65. package/src/components/ui/label.tsx +26 -0
  66. package/src/components/ui/scroll-area.tsx +48 -0
  67. package/src/components/ui/select.tsx +160 -0
  68. package/src/components/ui/separator.tsx +31 -0
  69. package/src/components/ui/sheet.tsx +140 -0
  70. package/src/components/ui/textarea.tsx +22 -0
  71. package/src/components/ui/toast.tsx +129 -0
  72. package/src/components/ui/toaster.tsx +35 -0
  73. package/src/core/ai/claude-client.ts +79 -0
  74. package/src/core/claude-runner/flag-builder.ts +57 -0
  75. package/src/core/claude-runner/index.ts +2 -0
  76. package/src/core/claude-runner/spawner.ts +86 -0
  77. package/src/core/prd/__tests__/auto-analyzer.test.ts +35 -0
  78. package/src/core/prd/__tests__/generator.test.ts +26 -0
  79. package/src/core/prd/__tests__/scanner.test.ts +35 -0
  80. package/src/core/prd/auto-analyzer.ts +9 -0
  81. package/src/core/prd/generator.ts +8 -0
  82. package/src/core/prd/scanner.ts +117 -0
  83. package/src/core/project/__tests__/claude-setup.test.ts +133 -0
  84. package/src/core/project/__tests__/config.test.ts +30 -0
  85. package/src/core/project/__tests__/init.test.ts +37 -0
  86. package/src/core/project/__tests__/skill-setup.test.ts +62 -0
  87. package/src/core/project/claude-setup.ts +224 -0
  88. package/src/core/project/config.ts +34 -0
  89. package/src/core/project/docs-setup.ts +26 -0
  90. package/src/core/project/docs-templates.ts +205 -0
  91. package/src/core/project/init.ts +40 -0
  92. package/src/core/project/skill-setup.ts +32 -0
  93. package/src/core/project/skill-templates.ts +277 -0
  94. package/src/core/task/index.ts +16 -0
  95. package/src/core/types.ts +58 -0
  96. package/src/features/example/backend/error.ts +9 -0
  97. package/src/features/example/backend/route.ts +52 -0
  98. package/src/features/example/backend/schema.ts +25 -0
  99. package/src/features/example/backend/service.ts +73 -0
  100. package/src/features/example/components/example-status.test.tsx +97 -0
  101. package/src/features/example/components/example-status.tsx +160 -0
  102. package/src/features/example/hooks/useExampleQuery.ts +23 -0
  103. package/src/features/example/lib/dto.test.ts +57 -0
  104. package/src/features/example/lib/dto.ts +5 -0
  105. package/src/features/kanban/backend/__tests__/sse-broadcaster.test.ts +137 -0
  106. package/src/features/kanban/backend/__tests__/sse-event-format.test.ts +55 -0
  107. package/src/features/kanban/backend/route.ts +55 -0
  108. package/src/features/kanban/backend/sse-broadcaster.ts +142 -0
  109. package/src/features/kanban/backend/sse-route.ts +43 -0
  110. package/src/features/kanban/components/KanbanBoard.tsx +105 -0
  111. package/src/features/kanban/components/KanbanColumn.tsx +51 -0
  112. package/src/features/kanban/components/KanbanError.tsx +29 -0
  113. package/src/features/kanban/components/KanbanSkeleton.tsx +46 -0
  114. package/src/features/kanban/components/ProgressCard.tsx +42 -0
  115. package/src/features/kanban/components/TaskCard.tsx +76 -0
  116. package/src/features/kanban/components/__tests__/kanban-components.test.tsx +86 -0
  117. package/src/features/kanban/hooks/useTaskSse.ts +66 -0
  118. package/src/features/kanban/hooks/useTasksQuery.ts +52 -0
  119. package/src/features/kanban/lib/__tests__/kanban-utils.test.ts +97 -0
  120. package/src/features/kanban/lib/kanban-utils.ts +37 -0
  121. package/src/features/taskflow/constants.ts +54 -0
  122. package/src/features/taskflow/index.ts +27 -0
  123. package/src/features/taskflow/lib/__tests__/filter.test.ts +89 -0
  124. package/src/features/taskflow/lib/__tests__/graph.test.ts +247 -0
  125. package/src/features/taskflow/lib/__tests__/repository.test.ts +233 -0
  126. package/src/features/taskflow/lib/__tests__/serializer.test.ts +98 -0
  127. package/src/features/taskflow/lib/advisor/__tests__/advisor-integration.test.ts +98 -0
  128. package/src/features/taskflow/lib/advisor/ai-advisor.test.ts +40 -0
  129. package/src/features/taskflow/lib/advisor/ai-advisor.ts +20 -0
  130. package/src/features/taskflow/lib/advisor/context-builder.test.ts +73 -0
  131. package/src/features/taskflow/lib/advisor/context-builder.ts +151 -0
  132. package/src/features/taskflow/lib/advisor/db.test.ts +106 -0
  133. package/src/features/taskflow/lib/advisor/db.ts +185 -0
  134. package/src/features/taskflow/lib/advisor/local-summary.test.ts +53 -0
  135. package/src/features/taskflow/lib/advisor/local-summary.ts +72 -0
  136. package/src/features/taskflow/lib/advisor/prompts.ts +86 -0
  137. package/src/features/taskflow/lib/filter.ts +54 -0
  138. package/src/features/taskflow/lib/fs-utils.ts +50 -0
  139. package/src/features/taskflow/lib/graph.ts +148 -0
  140. package/src/features/taskflow/lib/index-builder.ts +42 -0
  141. package/src/features/taskflow/lib/repository.ts +168 -0
  142. package/src/features/taskflow/lib/serializer.ts +62 -0
  143. package/src/features/taskflow/lib/watcher.ts +40 -0
  144. package/src/features/taskflow/types.ts +71 -0
  145. package/src/hooks/use-toast.ts +194 -0
  146. package/src/lib/remote/api-client.ts +40 -0
  147. package/src/lib/supabase/client.ts +8 -0
  148. package/src/lib/supabase/server.ts +46 -0
  149. package/src/lib/supabase/types.ts +3 -0
  150. package/src/lib/utils.ts +6 -0
  151. package/src/mcp/index.ts +7 -0
  152. package/src/mcp/server.ts +21 -0
  153. package/src/mcp/tools/brainstorm.ts +48 -0
  154. package/src/mcp/tools/prd.ts +71 -0
  155. package/src/mcp/tools/project.ts +39 -0
  156. package/src/mcp/tools/task-status.ts +40 -0
  157. package/src/mcp/tools/task.ts +82 -0
  158. package/src/mcp/util.ts +6 -0
@@ -0,0 +1,168 @@
1
+ import fs from "node:fs/promises";
2
+ import crypto from "node:crypto";
3
+ import {
4
+ getTasksDir,
5
+ getTaskflowRoot,
6
+ getTaskFilePath,
7
+ extractTaskId,
8
+ TASK_FILE_PREFIX,
9
+ TASK_FILE_EXT,
10
+ } from "../constants";
11
+ import type { Task, TaskCreateInput, TaskFilter, TaskSortKey, TaskSortOrder, TaskUpdateInput } from "../types";
12
+ import { atomicWrite, ensureDir, safeReadFile, safeRemove } from "./fs-utils";
13
+ import { parseTask, serializeTask } from "./serializer";
14
+ import { rebuildIndex } from "./index-builder";
15
+ import { filterTasks, sortTasks } from "./filter";
16
+
17
+ function generateId(): string {
18
+ const num = crypto.randomInt(0, 1000).toString().padStart(3, "0");
19
+ return num;
20
+ }
21
+
22
+ async function findNextId(projectRoot: string): Promise<string> {
23
+ const tasksDir = getTasksDir(projectRoot);
24
+ const files = await listTaskFiles(tasksDir);
25
+ const existingIds = files
26
+ .map(extractTaskId)
27
+ .filter(Boolean)
28
+ .map((id) => parseInt(id!, 10))
29
+ .filter((n) => !Number.isNaN(n));
30
+
31
+ const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0;
32
+ return String(maxId + 1).padStart(3, "0");
33
+ }
34
+
35
+ async function listTaskFiles(tasksDir: string): Promise<string[]> {
36
+ try {
37
+ const entries = await fs.readdir(tasksDir);
38
+ return entries.filter(
39
+ (f) => f.startsWith(TASK_FILE_PREFIX) && f.endsWith(TASK_FILE_EXT),
40
+ );
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ export async function ensureRepo(projectRoot: string): Promise<void> {
47
+ const root = getTaskflowRoot(projectRoot);
48
+ await ensureDir(root);
49
+ await ensureDir(getTasksDir(projectRoot));
50
+ await ensureDir(`${root}/index`);
51
+ await ensureDir(`${root}/logs`);
52
+ await ensureDir(`${root}/cache`);
53
+ }
54
+
55
+ export async function readTask(projectRoot: string, id: string): Promise<Task | null> {
56
+ const filePath = getTaskFilePath(projectRoot, id);
57
+ const content = await safeReadFile(filePath);
58
+ if (!content) return null;
59
+ return parseTask(content);
60
+ }
61
+
62
+ export async function listTasks(
63
+ projectRoot: string,
64
+ options?: {
65
+ filter?: TaskFilter;
66
+ sortKey?: TaskSortKey;
67
+ sortOrder?: TaskSortOrder;
68
+ },
69
+ ): Promise<Task[]> {
70
+ const tasksDir = getTasksDir(projectRoot);
71
+ const files = await listTaskFiles(tasksDir);
72
+
73
+ const tasks: Task[] = [];
74
+ for (const file of files) {
75
+ const content = await safeReadFile(`${tasksDir}/${file}`);
76
+ if (!content) continue;
77
+
78
+ try {
79
+ tasks.push(parseTask(content));
80
+ } catch {
81
+ // skip malformed task files
82
+ }
83
+ }
84
+
85
+ const filtered = options?.filter ? filterTasks(tasks, options.filter) : tasks;
86
+ return sortTasks(filtered, options?.sortKey, options?.sortOrder);
87
+ }
88
+
89
+ export async function createTask(
90
+ projectRoot: string,
91
+ input: TaskCreateInput,
92
+ ): Promise<Task> {
93
+ await ensureRepo(projectRoot);
94
+ const id = await findNextId(projectRoot);
95
+ const now = new Date().toISOString();
96
+
97
+ const task: Task = {
98
+ id,
99
+ title: input.title,
100
+ status: input.status ?? "Todo",
101
+ priority: input.priority ?? 0,
102
+ dependencies: input.dependencies ?? [],
103
+ parentId: input.parentId,
104
+ group: input.group,
105
+ createdAt: now,
106
+ updatedAt: now,
107
+ description: input.description ?? "",
108
+ };
109
+
110
+ const filePath = getTaskFilePath(projectRoot, id);
111
+ await atomicWrite(filePath, serializeTask(task));
112
+
113
+ const allTasks = await listTasks(projectRoot);
114
+ await rebuildIndex(projectRoot, allTasks);
115
+
116
+ return task;
117
+ }
118
+
119
+ export async function updateTask(
120
+ projectRoot: string,
121
+ id: string,
122
+ patch: TaskUpdateInput,
123
+ ): Promise<Task> {
124
+ const existing = await readTask(projectRoot, id);
125
+ if (!existing) {
126
+ throw new Error(`Task not found: ${id}`);
127
+ }
128
+
129
+ const updated: Task = {
130
+ ...existing,
131
+ ...patch,
132
+ id: existing.id,
133
+ createdAt: existing.createdAt,
134
+ updatedAt: new Date().toISOString(),
135
+ };
136
+
137
+ const filePath = getTaskFilePath(projectRoot, id);
138
+ await atomicWrite(filePath, serializeTask(updated));
139
+
140
+ const allTasks = await listTasks(projectRoot);
141
+ await rebuildIndex(projectRoot, allTasks);
142
+
143
+ return updated;
144
+ }
145
+
146
+ export async function deleteTask(projectRoot: string, id: string): Promise<boolean> {
147
+ const filePath = getTaskFilePath(projectRoot, id);
148
+ const removed = await safeRemove(filePath);
149
+
150
+ if (removed) {
151
+ const allTasks = await listTasks(projectRoot);
152
+ await rebuildIndex(projectRoot, allTasks);
153
+ }
154
+
155
+ return removed;
156
+ }
157
+
158
+ export async function searchTasks(projectRoot: string, query: string): Promise<Task[]> {
159
+ const allTasks = await listTasks(projectRoot);
160
+ const lowerQuery = query.toLowerCase();
161
+
162
+ return allTasks.filter(
163
+ (task) =>
164
+ task.title.toLowerCase().includes(lowerQuery) ||
165
+ task.description.toLowerCase().includes(lowerQuery) ||
166
+ task.id.includes(lowerQuery),
167
+ );
168
+ }
@@ -0,0 +1,62 @@
1
+ import matter from "gray-matter";
2
+ import type { Task, TaskStatus } from "../types";
3
+ import { TASK_STATUSES } from "../types";
4
+
5
+ interface TaskFrontmatter {
6
+ id: string;
7
+ title: string;
8
+ status: string;
9
+ priority?: number;
10
+ dependencies?: string[];
11
+ parentId?: string;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ }
15
+
16
+ function isValidStatus(value: string): value is TaskStatus {
17
+ return (TASK_STATUSES as readonly string[]).includes(value);
18
+ }
19
+
20
+ export function parseTask(raw: string): Task {
21
+ const { data, content } = matter(raw);
22
+ const fm = data as TaskFrontmatter;
23
+
24
+ if (!fm.id || !fm.title) {
25
+ throw new Error(`Invalid task: missing required fields (id, title)`);
26
+ }
27
+
28
+ const status = isValidStatus(fm.status) ? fm.status : "Todo";
29
+
30
+ return {
31
+ id: fm.id,
32
+ title: fm.title,
33
+ status,
34
+ priority: fm.priority ?? 0,
35
+ dependencies: fm.dependencies ?? [],
36
+ parentId: fm.parentId,
37
+ createdAt: fm.createdAt ?? new Date().toISOString(),
38
+ updatedAt: fm.updatedAt ?? new Date().toISOString(),
39
+ description: content.trim(),
40
+ };
41
+ }
42
+
43
+ export function serializeTask(task: Task): string {
44
+ const frontmatter: Record<string, unknown> = {
45
+ id: task.id,
46
+ title: task.title,
47
+ status: task.status,
48
+ priority: task.priority,
49
+ createdAt: task.createdAt,
50
+ updatedAt: task.updatedAt,
51
+ };
52
+
53
+ if (task.dependencies.length > 0) {
54
+ frontmatter.dependencies = task.dependencies;
55
+ }
56
+
57
+ if (task.parentId) {
58
+ frontmatter.parentId = task.parentId;
59
+ }
60
+
61
+ return matter.stringify(task.description || "", frontmatter);
62
+ }
@@ -0,0 +1,40 @@
1
+ import { watch, type FSWatcher } from "chokidar";
2
+ import path from "node:path";
3
+ import { getTasksDir, extractTaskId } from "../constants";
4
+
5
+ export type WatchEvent = "created" | "updated" | "deleted";
6
+
7
+ export interface TaskFileEvent {
8
+ event: WatchEvent;
9
+ taskId: string;
10
+ filePath: string;
11
+ }
12
+
13
+ export type TaskFileEventHandler = (event: TaskFileEvent) => void;
14
+
15
+ export function createTaskWatcher(
16
+ projectRoot: string,
17
+ onEvent: TaskFileEventHandler,
18
+ ): FSWatcher {
19
+ const tasksDir = getTasksDir(projectRoot);
20
+ const glob = path.join(tasksDir, "task-*.md");
21
+
22
+ const watcher = watch(glob, {
23
+ ignoreInitial: true,
24
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
25
+ });
26
+
27
+ function handleEvent(event: WatchEvent) {
28
+ return (filePath: string) => {
29
+ const taskId = extractTaskId(path.basename(filePath));
30
+ if (!taskId) return;
31
+ onEvent({ event, taskId, filePath });
32
+ };
33
+ }
34
+
35
+ watcher.on("add", handleEvent("created"));
36
+ watcher.on("change", handleEvent("updated"));
37
+ watcher.on("unlink", handleEvent("deleted"));
38
+
39
+ return watcher;
40
+ }
@@ -0,0 +1,71 @@
1
+ export const TASK_STATUSES = ["Todo", "InProgress", "Blocked", "Done"] as const;
2
+
3
+ export type TaskStatus = (typeof TASK_STATUSES)[number];
4
+
5
+ export interface Task {
6
+ id: string;
7
+ title: string;
8
+ status: TaskStatus;
9
+ priority: number;
10
+ dependencies: string[];
11
+ parentId?: string;
12
+ group?: string;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ description: string;
16
+ }
17
+
18
+ export type TaskCreateInput = Pick<Task, "title"> &
19
+ Partial<Omit<Task, "id" | "title" | "createdAt" | "updatedAt">>;
20
+
21
+ export type TaskUpdateInput = Partial<Omit<Task, "id" | "createdAt">>;
22
+
23
+ export interface TaskFilter {
24
+ status?: TaskStatus | TaskStatus[];
25
+ priority?: number;
26
+ parentId?: string;
27
+ updatedSince?: string;
28
+ hasDependency?: string;
29
+ }
30
+
31
+ export type TaskSortKey = "priority" | "status" | "createdAt" | "updatedAt" | "title";
32
+
33
+ export type TaskSortOrder = "asc" | "desc";
34
+
35
+ // Advisor types
36
+ export type SessionType = "prd" | "parse-prd" | "trd" | "brainstorm" | "ask" | "refine";
37
+
38
+ export interface ConvLog {
39
+ id: number;
40
+ sessionType: SessionType;
41
+ sessionId: string;
42
+ role: "user" | "assistant";
43
+ content: string;
44
+ createdAt: string;
45
+ }
46
+
47
+ export interface Decision {
48
+ id: number;
49
+ sessionId: string;
50
+ decision: string;
51
+ reason: string;
52
+ relatedTasks: string[];
53
+ createdAt: string;
54
+ }
55
+
56
+ export interface TaskSummary {
57
+ id: string;
58
+ title: string;
59
+ status: TaskStatus;
60
+ priority: number;
61
+ dependencies: string[];
62
+ }
63
+
64
+ export interface AdvisorContext {
65
+ tasks: TaskSummary[];
66
+ decisions: Decision[];
67
+ trdContent?: string;
68
+ prdContent?: string;
69
+ gitDiff?: string;
70
+ conversationLogs?: ConvLog[];
71
+ }
@@ -0,0 +1,194 @@
1
+ "use client"
2
+
3
+ // Inspired by react-hot-toast library
4
+ import * as React from "react"
5
+
6
+ import type {
7
+ ToastActionElement,
8
+ ToastProps,
9
+ } from "@/components/ui/toast"
10
+
11
+ const TOAST_LIMIT = 1
12
+ const TOAST_REMOVE_DELAY = 1000000
13
+
14
+ type ToasterToast = ToastProps & {
15
+ id: string
16
+ title?: React.ReactNode
17
+ description?: React.ReactNode
18
+ action?: ToastActionElement
19
+ }
20
+
21
+ const actionTypes = {
22
+ ADD_TOAST: "ADD_TOAST",
23
+ UPDATE_TOAST: "UPDATE_TOAST",
24
+ DISMISS_TOAST: "DISMISS_TOAST",
25
+ REMOVE_TOAST: "REMOVE_TOAST",
26
+ } as const
27
+
28
+ let count = 0
29
+
30
+ function genId() {
31
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
32
+ return count.toString()
33
+ }
34
+
35
+ type ActionType = typeof actionTypes
36
+
37
+ type Action =
38
+ | {
39
+ type: ActionType["ADD_TOAST"]
40
+ toast: ToasterToast
41
+ }
42
+ | {
43
+ type: ActionType["UPDATE_TOAST"]
44
+ toast: Partial<ToasterToast>
45
+ }
46
+ | {
47
+ type: ActionType["DISMISS_TOAST"]
48
+ toastId?: ToasterToast["id"]
49
+ }
50
+ | {
51
+ type: ActionType["REMOVE_TOAST"]
52
+ toastId?: ToasterToast["id"]
53
+ }
54
+
55
+ interface State {
56
+ toasts: ToasterToast[]
57
+ }
58
+
59
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
60
+
61
+ const addToRemoveQueue = (toastId: string) => {
62
+ if (toastTimeouts.has(toastId)) {
63
+ return
64
+ }
65
+
66
+ const timeout = setTimeout(() => {
67
+ toastTimeouts.delete(toastId)
68
+ dispatch({
69
+ type: "REMOVE_TOAST",
70
+ toastId: toastId,
71
+ })
72
+ }, TOAST_REMOVE_DELAY)
73
+
74
+ toastTimeouts.set(toastId, timeout)
75
+ }
76
+
77
+ export const reducer = (state: State, action: Action): State => {
78
+ switch (action.type) {
79
+ case "ADD_TOAST":
80
+ return {
81
+ ...state,
82
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83
+ }
84
+
85
+ case "UPDATE_TOAST":
86
+ return {
87
+ ...state,
88
+ toasts: state.toasts.map((t) =>
89
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
90
+ ),
91
+ }
92
+
93
+ case "DISMISS_TOAST": {
94
+ const { toastId } = action
95
+
96
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
97
+ // but I'll keep it here for simplicity
98
+ if (toastId) {
99
+ addToRemoveQueue(toastId)
100
+ } else {
101
+ state.toasts.forEach((toast) => {
102
+ addToRemoveQueue(toast.id)
103
+ })
104
+ }
105
+
106
+ return {
107
+ ...state,
108
+ toasts: state.toasts.map((t) =>
109
+ t.id === toastId || toastId === undefined
110
+ ? {
111
+ ...t,
112
+ open: false,
113
+ }
114
+ : t
115
+ ),
116
+ }
117
+ }
118
+ case "REMOVE_TOAST":
119
+ if (action.toastId === undefined) {
120
+ return {
121
+ ...state,
122
+ toasts: [],
123
+ }
124
+ }
125
+ return {
126
+ ...state,
127
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
128
+ }
129
+ }
130
+ }
131
+
132
+ const listeners: Array<(state: State) => void> = []
133
+
134
+ let memoryState: State = { toasts: [] }
135
+
136
+ function dispatch(action: Action) {
137
+ memoryState = reducer(memoryState, action)
138
+ listeners.forEach((listener) => {
139
+ listener(memoryState)
140
+ })
141
+ }
142
+
143
+ type Toast = Omit<ToasterToast, "id">
144
+
145
+ function toast({ ...props }: Toast) {
146
+ const id = genId()
147
+
148
+ const update = (props: ToasterToast) =>
149
+ dispatch({
150
+ type: "UPDATE_TOAST",
151
+ toast: { ...props, id },
152
+ })
153
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154
+
155
+ dispatch({
156
+ type: "ADD_TOAST",
157
+ toast: {
158
+ ...props,
159
+ id,
160
+ open: true,
161
+ onOpenChange: (open) => {
162
+ if (!open) dismiss()
163
+ },
164
+ },
165
+ })
166
+
167
+ return {
168
+ id: id,
169
+ dismiss,
170
+ update,
171
+ }
172
+ }
173
+
174
+ function useToast() {
175
+ const [state, setState] = React.useState<State>(memoryState)
176
+
177
+ React.useEffect(() => {
178
+ listeners.push(setState)
179
+ return () => {
180
+ const index = listeners.indexOf(setState)
181
+ if (index > -1) {
182
+ listeners.splice(index, 1)
183
+ }
184
+ }
185
+ }, [state])
186
+
187
+ return {
188
+ ...state,
189
+ toast,
190
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191
+ }
192
+ }
193
+
194
+ export { useToast, toast }
@@ -0,0 +1,40 @@
1
+ import axios, { isAxiosError } from "axios";
2
+
3
+ const apiClient = axios.create({
4
+ baseURL: process.env.NEXT_PUBLIC_API_BASE_URL ?? "",
5
+ headers: {
6
+ "Content-Type": "application/json",
7
+ },
8
+ });
9
+
10
+ type ErrorPayload = {
11
+ error?: {
12
+ message?: string;
13
+ };
14
+ message?: string;
15
+ };
16
+
17
+ export const extractApiErrorMessage = (
18
+ error: unknown,
19
+ fallbackMessage = "API request failed."
20
+ ) => {
21
+ if (isAxiosError(error)) {
22
+ const payload = error.response?.data as ErrorPayload | undefined;
23
+
24
+ if (typeof payload?.error?.message === "string") {
25
+ return payload.error.message;
26
+ }
27
+
28
+ if (typeof payload?.message === "string") {
29
+ return payload.message;
30
+ }
31
+ }
32
+
33
+ if (error instanceof Error && error.message) {
34
+ return error.message;
35
+ }
36
+
37
+ return fallbackMessage;
38
+ };
39
+
40
+ export { apiClient, isAxiosError };
@@ -0,0 +1,8 @@
1
+ import { createBrowserClient } from "@supabase/ssr";
2
+
3
+ export function createClient() {
4
+ return createBrowserClient(
5
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
6
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
7
+ );
8
+ }
@@ -0,0 +1,46 @@
1
+ import "server-only";
2
+
3
+ import { createServerClient } from "@supabase/ssr";
4
+ import { cookies } from "next/headers";
5
+
6
+ export async function createClient() {
7
+ const cookieStore = await cookies();
8
+
9
+ return createServerClient(
10
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
11
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
12
+ {
13
+ cookies: {
14
+ getAll() {
15
+ return cookieStore.getAll();
16
+ },
17
+ setAll(cookiesToSet) {
18
+ try {
19
+ cookiesToSet.forEach(({ name, value, options }) =>
20
+ cookieStore.set(name, value, options)
21
+ );
22
+ } catch {
23
+ // The `setAll` method was called from a Server Component.
24
+ // This can be ignored if you have middleware refreshing
25
+ // user sessions.
26
+ }
27
+ },
28
+ },
29
+ }
30
+ );
31
+ }
32
+
33
+ export async function createPureClient() {
34
+ return createServerClient(
35
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
36
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
37
+ {
38
+ cookies: {
39
+ getAll() {
40
+ return [];
41
+ },
42
+ setAll() {},
43
+ },
44
+ }
45
+ );
46
+ }
@@ -0,0 +1,3 @@
1
+ export type Database = Record<string, never>;
2
+
3
+ export type SupabaseUserMetadata = Record<string, unknown>;
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,7 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createMcpServer } from "./server.js";
3
+
4
+ const server = createMcpServer();
5
+ const transport = new StdioServerTransport();
6
+
7
+ await server.connect(transport);
@@ -0,0 +1,21 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerProjectTools } from "./tools/project.js";
3
+ import { registerTaskTools } from "./tools/task.js";
4
+ import { registerTaskStatusTools } from "./tools/task-status.js";
5
+ import { registerPrdTools } from "./tools/prd.js";
6
+ import { registerBrainstormTools } from "./tools/brainstorm.js";
7
+
8
+ export function createMcpServer(): McpServer {
9
+ const server = new McpServer({
10
+ name: "taskflow",
11
+ version: "0.2.0",
12
+ });
13
+
14
+ registerProjectTools(server);
15
+ registerTaskTools(server);
16
+ registerTaskStatusTools(server);
17
+ registerPrdTools(server);
18
+ registerBrainstormTools(server);
19
+
20
+ return server;
21
+ }