@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.
- package/README.md +374 -0
- package/bin/task-mcp.mjs +19 -0
- package/bin/task.mjs +19 -0
- package/docs/clean-code.md +29 -0
- package/docs/git.md +36 -0
- package/docs/guideline.md +25 -0
- package/docs/security.md +32 -0
- package/docs/step-by-step.md +29 -0
- package/docs/superpowers/specs/2026-03-21-cli-advisor-design.md +383 -0
- package/docs/superpowers/specs/2026-03-21-init-redesign-design.md +429 -0
- package/docs/superpowers/specs/2026-03-21-skill-architecture-design.md +362 -0
- package/docs/superpowers/specs/2026-03-23-t-create-task-run-design.md +40 -0
- package/docs/superpowers/specs/2026-03-23-task-run-design.md +44 -0
- package/docs/tdd.md +41 -0
- package/package.json +114 -0
- package/src/app/(protected)/dashboard/page.tsx +7 -0
- package/src/app/(protected)/layout.tsx +10 -0
- package/src/app/api/[[...hono]]/route.ts +13 -0
- package/src/app/example/page.tsx +11 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +168 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +5 -0
- package/src/app/providers.tsx +57 -0
- package/src/backend/config/index.ts +36 -0
- package/src/backend/hono/app.ts +32 -0
- package/src/backend/hono/context.ts +38 -0
- package/src/backend/http/response.ts +64 -0
- package/src/backend/middleware/context.ts +23 -0
- package/src/backend/middleware/error.ts +31 -0
- package/src/backend/middleware/supabase.ts +23 -0
- package/src/backend/supabase/client.ts +17 -0
- package/src/cli/commands/__tests__/task-commands.test.ts +170 -0
- package/src/cli/commands/advisor.ts +45 -0
- package/src/cli/commands/ask.ts +50 -0
- package/src/cli/commands/board.ts +72 -0
- package/src/cli/commands/init.ts +184 -0
- package/src/cli/commands/list.ts +138 -0
- package/src/cli/commands/run.ts +143 -0
- package/src/cli/commands/set-status.ts +50 -0
- package/src/cli/commands/show.ts +28 -0
- package/src/cli/commands/tree.ts +72 -0
- package/src/cli/index.ts +38 -0
- package/src/cli/lib/__tests__/formatter.test.ts +123 -0
- package/src/cli/lib/error-boundary.test.ts +135 -0
- package/src/cli/lib/error-boundary.ts +70 -0
- package/src/cli/lib/formatter.ts +764 -0
- package/src/cli/lib/trd.ts +33 -0
- package/src/cli/lib/validate.test.ts +89 -0
- package/src/cli/lib/validate.ts +43 -0
- package/src/cli/prompts/task-run.md +25 -0
- package/src/components/layout/AppLayout.tsx +15 -0
- package/src/components/layout/Sidebar.tsx +124 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/avatar.tsx +50 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/file-upload.tsx +50 -0
- package/src/components/ui/form.tsx +179 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +129 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/core/ai/claude-client.ts +79 -0
- package/src/core/claude-runner/flag-builder.ts +57 -0
- package/src/core/claude-runner/index.ts +2 -0
- package/src/core/claude-runner/spawner.ts +86 -0
- package/src/core/prd/__tests__/auto-analyzer.test.ts +35 -0
- package/src/core/prd/__tests__/generator.test.ts +26 -0
- package/src/core/prd/__tests__/scanner.test.ts +35 -0
- package/src/core/prd/auto-analyzer.ts +9 -0
- package/src/core/prd/generator.ts +8 -0
- package/src/core/prd/scanner.ts +117 -0
- package/src/core/project/__tests__/claude-setup.test.ts +133 -0
- package/src/core/project/__tests__/config.test.ts +30 -0
- package/src/core/project/__tests__/init.test.ts +37 -0
- package/src/core/project/__tests__/skill-setup.test.ts +62 -0
- package/src/core/project/claude-setup.ts +224 -0
- package/src/core/project/config.ts +34 -0
- package/src/core/project/docs-setup.ts +26 -0
- package/src/core/project/docs-templates.ts +205 -0
- package/src/core/project/init.ts +40 -0
- package/src/core/project/skill-setup.ts +32 -0
- package/src/core/project/skill-templates.ts +277 -0
- package/src/core/task/index.ts +16 -0
- package/src/core/types.ts +58 -0
- package/src/features/example/backend/error.ts +9 -0
- package/src/features/example/backend/route.ts +52 -0
- package/src/features/example/backend/schema.ts +25 -0
- package/src/features/example/backend/service.ts +73 -0
- package/src/features/example/components/example-status.test.tsx +97 -0
- package/src/features/example/components/example-status.tsx +160 -0
- package/src/features/example/hooks/useExampleQuery.ts +23 -0
- package/src/features/example/lib/dto.test.ts +57 -0
- package/src/features/example/lib/dto.ts +5 -0
- package/src/features/kanban/backend/__tests__/sse-broadcaster.test.ts +137 -0
- package/src/features/kanban/backend/__tests__/sse-event-format.test.ts +55 -0
- package/src/features/kanban/backend/route.ts +55 -0
- package/src/features/kanban/backend/sse-broadcaster.ts +142 -0
- package/src/features/kanban/backend/sse-route.ts +43 -0
- package/src/features/kanban/components/KanbanBoard.tsx +105 -0
- package/src/features/kanban/components/KanbanColumn.tsx +51 -0
- package/src/features/kanban/components/KanbanError.tsx +29 -0
- package/src/features/kanban/components/KanbanSkeleton.tsx +46 -0
- package/src/features/kanban/components/ProgressCard.tsx +42 -0
- package/src/features/kanban/components/TaskCard.tsx +76 -0
- package/src/features/kanban/components/__tests__/kanban-components.test.tsx +86 -0
- package/src/features/kanban/hooks/useTaskSse.ts +66 -0
- package/src/features/kanban/hooks/useTasksQuery.ts +52 -0
- package/src/features/kanban/lib/__tests__/kanban-utils.test.ts +97 -0
- package/src/features/kanban/lib/kanban-utils.ts +37 -0
- package/src/features/taskflow/constants.ts +54 -0
- package/src/features/taskflow/index.ts +27 -0
- package/src/features/taskflow/lib/__tests__/filter.test.ts +89 -0
- package/src/features/taskflow/lib/__tests__/graph.test.ts +247 -0
- package/src/features/taskflow/lib/__tests__/repository.test.ts +233 -0
- package/src/features/taskflow/lib/__tests__/serializer.test.ts +98 -0
- package/src/features/taskflow/lib/advisor/__tests__/advisor-integration.test.ts +98 -0
- package/src/features/taskflow/lib/advisor/ai-advisor.test.ts +40 -0
- package/src/features/taskflow/lib/advisor/ai-advisor.ts +20 -0
- package/src/features/taskflow/lib/advisor/context-builder.test.ts +73 -0
- package/src/features/taskflow/lib/advisor/context-builder.ts +151 -0
- package/src/features/taskflow/lib/advisor/db.test.ts +106 -0
- package/src/features/taskflow/lib/advisor/db.ts +185 -0
- package/src/features/taskflow/lib/advisor/local-summary.test.ts +53 -0
- package/src/features/taskflow/lib/advisor/local-summary.ts +72 -0
- package/src/features/taskflow/lib/advisor/prompts.ts +86 -0
- package/src/features/taskflow/lib/filter.ts +54 -0
- package/src/features/taskflow/lib/fs-utils.ts +50 -0
- package/src/features/taskflow/lib/graph.ts +148 -0
- package/src/features/taskflow/lib/index-builder.ts +42 -0
- package/src/features/taskflow/lib/repository.ts +168 -0
- package/src/features/taskflow/lib/serializer.ts +62 -0
- package/src/features/taskflow/lib/watcher.ts +40 -0
- package/src/features/taskflow/types.ts +71 -0
- package/src/hooks/use-toast.ts +194 -0
- package/src/lib/remote/api-client.ts +40 -0
- package/src/lib/supabase/client.ts +8 -0
- package/src/lib/supabase/server.ts +46 -0
- package/src/lib/supabase/types.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/server.ts +21 -0
- package/src/mcp/tools/brainstorm.ts +48 -0
- package/src/mcp/tools/prd.ts +71 -0
- package/src/mcp/tools/project.ts +39 -0
- package/src/mcp/tools/task-status.ts +40 -0
- package/src/mcp/tools/task.ts +82 -0
- 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,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
|
+
}
|
package/src/lib/utils.ts
ADDED
package/src/mcp/index.ts
ADDED
|
@@ -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
|
+
}
|