From 052beb364ffafd7682557901561269a1fd83abb2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Feb 2026 16:24:43 +0900 Subject: [PATCH] refactor(task-tool): split task.ts into per-action modules Extract CRUD actions into dedicated modules: - task-action-create.ts, task-action-get.ts - task-action-list.ts, task-action-update.ts, task-action-delete.ts - task-id-validator.ts: ID validation logic --- src/tools/task/task-action-create.ts | 46 +++++ src/tools/task/task-action-delete.ts | 36 ++++ src/tools/task/task-action-get.ts | 21 +++ src/tools/task/task-action-list.ts | 60 +++++++ src/tools/task/task-action-update.ts | 57 +++++++ src/tools/task/task-id-validator.ts | 6 + src/tools/task/task.ts | 240 +-------------------------- 7 files changed, 232 insertions(+), 234 deletions(-) create mode 100644 src/tools/task/task-action-create.ts create mode 100644 src/tools/task/task-action-delete.ts create mode 100644 src/tools/task/task-action-get.ts create mode 100644 src/tools/task/task-action-list.ts create mode 100644 src/tools/task/task-action-update.ts create mode 100644 src/tools/task/task-id-validator.ts diff --git a/src/tools/task/task-action-create.ts b/src/tools/task/task-action-create.ts new file mode 100644 index 000000000..be60b1864 --- /dev/null +++ b/src/tools/task/task-action-create.ts @@ -0,0 +1,46 @@ +import { join } from "path" +import type { OhMyOpenCodeConfig } from "../../config/schema" +import type { TaskObject } from "./types" +import { TaskCreateInputSchema, TaskObjectSchema } from "./types" +import { + acquireLock, + generateTaskId, + getTaskDir, + writeJsonAtomic, +} from "../../features/claude-tasks/storage" + +export async function handleCreate( + args: Record, + config: Partial, + context: { sessionID: string } +): Promise { + const validatedArgs = TaskCreateInputSchema.parse(args) + const taskDir = getTaskDir(config) + const lock = acquireLock(taskDir) + + if (!lock.acquired) { + return JSON.stringify({ error: "task_lock_unavailable" }) + } + + try { + const taskId = generateTaskId() + const task: TaskObject = { + id: taskId, + subject: validatedArgs.subject, + description: validatedArgs.description ?? "", + status: "pending", + blocks: validatedArgs.blocks ?? [], + blockedBy: validatedArgs.blockedBy ?? [], + repoURL: validatedArgs.repoURL, + parentID: validatedArgs.parentID, + threadID: context.sessionID, + } + + const validatedTask = TaskObjectSchema.parse(task) + writeJsonAtomic(join(taskDir, `${taskId}.json`), validatedTask) + + return JSON.stringify({ task: validatedTask }) + } finally { + lock.release() + } +} diff --git a/src/tools/task/task-action-delete.ts b/src/tools/task/task-action-delete.ts new file mode 100644 index 000000000..a66992c97 --- /dev/null +++ b/src/tools/task/task-action-delete.ts @@ -0,0 +1,36 @@ +import { existsSync, unlinkSync } from "fs" +import { join } from "path" +import type { OhMyOpenCodeConfig } from "../../config/schema" +import { TaskDeleteInputSchema } from "./types" +import { acquireLock, getTaskDir } from "../../features/claude-tasks/storage" +import { parseTaskId } from "./task-id-validator" + +export async function handleDelete( + args: Record, + config: Partial +): Promise { + const validatedArgs = TaskDeleteInputSchema.parse(args) + const taskId = parseTaskId(validatedArgs.id) + if (!taskId) { + return JSON.stringify({ error: "invalid_task_id" }) + } + const taskDir = getTaskDir(config) + const lock = acquireLock(taskDir) + + if (!lock.acquired) { + return JSON.stringify({ error: "task_lock_unavailable" }) + } + + try { + const taskPath = join(taskDir, `${taskId}.json`) + + if (!existsSync(taskPath)) { + return JSON.stringify({ error: "task_not_found" }) + } + + unlinkSync(taskPath) + return JSON.stringify({ success: true }) + } finally { + lock.release() + } +} diff --git a/src/tools/task/task-action-get.ts b/src/tools/task/task-action-get.ts new file mode 100644 index 000000000..65a59a983 --- /dev/null +++ b/src/tools/task/task-action-get.ts @@ -0,0 +1,21 @@ +import { join } from "path" +import type { OhMyOpenCodeConfig } from "../../config/schema" +import { TaskGetInputSchema, TaskObjectSchema } from "./types" +import { getTaskDir, readJsonSafe } from "../../features/claude-tasks/storage" +import { parseTaskId } from "./task-id-validator" + +export async function handleGet( + args: Record, + config: Partial +): Promise { + const validatedArgs = TaskGetInputSchema.parse(args) + const taskId = parseTaskId(validatedArgs.id) + if (!taskId) { + return JSON.stringify({ error: "invalid_task_id" }) + } + const taskDir = getTaskDir(config) + const taskPath = join(taskDir, `${taskId}.json`) + + const task = readJsonSafe(taskPath, TaskObjectSchema) + return JSON.stringify({ task: task ?? null }) +} diff --git a/src/tools/task/task-action-list.ts b/src/tools/task/task-action-list.ts new file mode 100644 index 000000000..ec57830b6 --- /dev/null +++ b/src/tools/task/task-action-list.ts @@ -0,0 +1,60 @@ +import { existsSync } from "fs" +import { join } from "path" +import type { OhMyOpenCodeConfig } from "../../config/schema" +import type { TaskObject } from "./types" +import { TaskListInputSchema, TaskObjectSchema } from "./types" +import { getTaskDir, listTaskFiles, readJsonSafe } from "../../features/claude-tasks/storage" + +export async function handleList( + args: Record, + config: Partial +): Promise { + const validatedArgs = TaskListInputSchema.parse(args) + const taskDir = getTaskDir(config) + + if (!existsSync(taskDir)) { + return JSON.stringify({ tasks: [] }) + } + + const files = listTaskFiles(config) + if (files.length === 0) { + return JSON.stringify({ tasks: [] }) + } + + const allTasks: TaskObject[] = [] + for (const fileId of files) { + const task = readJsonSafe(join(taskDir, `${fileId}.json`), TaskObjectSchema) + if (task) { + allTasks.push(task) + } + } + + let tasks = allTasks.filter((task) => task.status !== "completed") + + if (validatedArgs.status) { + tasks = tasks.filter((task) => task.status === validatedArgs.status) + } + + if (validatedArgs.parentID) { + tasks = tasks.filter((task) => task.parentID === validatedArgs.parentID) + } + + const ready = args["ready"] === true + if (ready) { + tasks = tasks.filter((task) => { + if (task.blockedBy.length === 0) return true + return task.blockedBy.every((depId) => { + const depTask = allTasks.find((t) => t.id === depId) + return depTask?.status === "completed" + }) + }) + } + + const limitRaw = args["limit"] + const limit = typeof limitRaw === "number" ? limitRaw : undefined + if (limit !== undefined && limit > 0) { + tasks = tasks.slice(0, limit) + } + + return JSON.stringify({ tasks }) +} diff --git a/src/tools/task/task-action-update.ts b/src/tools/task/task-action-update.ts new file mode 100644 index 000000000..ebdecdf70 --- /dev/null +++ b/src/tools/task/task-action-update.ts @@ -0,0 +1,57 @@ +import { join } from "path" +import type { OhMyOpenCodeConfig } from "../../config/schema" +import { TaskUpdateInputSchema, TaskObjectSchema } from "./types" +import { acquireLock, getTaskDir, readJsonSafe, writeJsonAtomic } from "../../features/claude-tasks/storage" +import { parseTaskId } from "./task-id-validator" + +export async function handleUpdate( + args: Record, + config: Partial +): Promise { + const validatedArgs = TaskUpdateInputSchema.parse(args) + const taskId = parseTaskId(validatedArgs.id) + if (!taskId) { + return JSON.stringify({ error: "invalid_task_id" }) + } + const taskDir = getTaskDir(config) + const lock = acquireLock(taskDir) + + if (!lock.acquired) { + return JSON.stringify({ error: "task_lock_unavailable" }) + } + + try { + const taskPath = join(taskDir, `${taskId}.json`) + const task = readJsonSafe(taskPath, TaskObjectSchema) + + if (!task) { + return JSON.stringify({ error: "task_not_found" }) + } + + if (validatedArgs.subject !== undefined) { + task.subject = validatedArgs.subject + } + if (validatedArgs.description !== undefined) { + task.description = validatedArgs.description + } + if (validatedArgs.status !== undefined) { + task.status = validatedArgs.status + } + if (validatedArgs.addBlockedBy !== undefined) { + task.blockedBy = [...task.blockedBy, ...validatedArgs.addBlockedBy] + } + if (validatedArgs.repoURL !== undefined) { + task.repoURL = validatedArgs.repoURL + } + if (validatedArgs.parentID !== undefined) { + task.parentID = validatedArgs.parentID + } + + const validatedTask = TaskObjectSchema.parse(task) + writeJsonAtomic(taskPath, validatedTask) + + return JSON.stringify({ task: validatedTask }) + } finally { + lock.release() + } +} diff --git a/src/tools/task/task-id-validator.ts b/src/tools/task/task-id-validator.ts new file mode 100644 index 000000000..acb83be0a --- /dev/null +++ b/src/tools/task/task-id-validator.ts @@ -0,0 +1,6 @@ +const TASK_ID_PATTERN = /^T-[A-Za-z0-9-]+$/ + +export function parseTaskId(id: string): string | null { + if (!TASK_ID_PATTERN.test(id)) return null + return id +} diff --git a/src/tools/task/task.ts b/src/tools/task/task.ts index dc6602810..df6b6aa01 100644 --- a/src/tools/task/task.ts +++ b/src/tools/task/task.ts @@ -1,38 +1,10 @@ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" -import { existsSync, readdirSync, unlinkSync } from "fs" -import { join } from "path" import type { OhMyOpenCodeConfig } from "../../config/schema" -import type { - TaskObject, - TaskCreateInput, - TaskListInput, - TaskGetInput, - TaskUpdateInput, - TaskDeleteInput, -} from "./types" -import { - TaskObjectSchema, - TaskCreateInputSchema, - TaskListInputSchema, - TaskGetInputSchema, - TaskUpdateInputSchema, - TaskDeleteInputSchema, -} from "./types" -import { - getTaskDir, - readJsonSafe, - writeJsonAtomic, - acquireLock, - generateTaskId, - listTaskFiles, -} from "../../features/claude-tasks/storage" - -const TASK_ID_PATTERN = /^T-[A-Za-z0-9-]+$/ - -function parseTaskId(id: string): string | null { - if (!TASK_ID_PATTERN.test(id)) return null - return id -} +import { handleCreate } from "./task-action-create" +import { handleDelete } from "./task-action-delete" +import { handleGet } from "./task-action-get" +import { handleList } from "./task-action-list" +import { handleUpdate } from "./task-action-update" export function createTask(config: Partial): ToolDefinition { return tool({ @@ -66,9 +38,7 @@ All actions return JSON strings.`, limit: tool.schema.number().optional().describe("Maximum number of tasks to return"), }, execute: async (args, context) => { - const action = args.action as "create" | "list" | "get" | "update" | "delete" - - switch (action) { + switch (args.action) { case "create": return handleCreate(args, config, context) case "list": @@ -85,201 +55,3 @@ All actions return JSON strings.`, }, }) } - -async function handleCreate( - args: Record, - config: Partial, - context: { sessionID: string } -): Promise { - const validatedArgs = TaskCreateInputSchema.parse(args) - const taskDir = getTaskDir(config) - const lock = acquireLock(taskDir) - - if (!lock.acquired) { - return JSON.stringify({ error: "task_lock_unavailable" }) - } - - try { - const taskId = generateTaskId() - const task: TaskObject = { - id: taskId, - subject: validatedArgs.subject, - description: validatedArgs.description ?? "", - status: "pending", - blocks: validatedArgs.blocks ?? [], - blockedBy: validatedArgs.blockedBy ?? [], - repoURL: validatedArgs.repoURL, - parentID: validatedArgs.parentID, - threadID: context.sessionID, - } - - const validatedTask = TaskObjectSchema.parse(task) - writeJsonAtomic(join(taskDir, `${taskId}.json`), validatedTask) - - return JSON.stringify({ task: validatedTask }) - } finally { - lock.release() - } -} - -async function handleList( - args: Record, - config: Partial -): Promise { - const validatedArgs = TaskListInputSchema.parse(args) - const taskDir = getTaskDir(config) - - if (!existsSync(taskDir)) { - return JSON.stringify({ tasks: [] }) - } - - const files = listTaskFiles(config) - if (files.length === 0) { - return JSON.stringify({ tasks: [] }) - } - - const allTasks: TaskObject[] = [] - for (const fileId of files) { - const task = readJsonSafe(join(taskDir, `${fileId}.json`), TaskObjectSchema) - if (task) { - allTasks.push(task) - } - } - - // Filter out completed tasks by default - let tasks = allTasks.filter((task) => task.status !== "completed") - - // Apply status filter if provided - if (validatedArgs.status) { - tasks = tasks.filter((task) => task.status === validatedArgs.status) - } - - // Apply parentID filter if provided - if (validatedArgs.parentID) { - tasks = tasks.filter((task) => task.parentID === validatedArgs.parentID) - } - - // Apply ready filter if requested - if (args.ready) { - tasks = tasks.filter((task) => { - if (task.blockedBy.length === 0) { - return true - } - - // All blocking tasks must be completed - return task.blockedBy.every((depId: string) => { - const depTask = allTasks.find((t) => t.id === depId) - return depTask?.status === "completed" - }) - }) - } - - // Apply limit if provided - const limit = args.limit as number | undefined - if (limit !== undefined && limit > 0) { - tasks = tasks.slice(0, limit) - } - - return JSON.stringify({ tasks }) -} - -async function handleGet( - args: Record, - config: Partial -): Promise { - const validatedArgs = TaskGetInputSchema.parse(args) - const taskId = parseTaskId(validatedArgs.id) - if (!taskId) { - return JSON.stringify({ error: "invalid_task_id" }) - } - const taskDir = getTaskDir(config) - const taskPath = join(taskDir, `${taskId}.json`) - - const task = readJsonSafe(taskPath, TaskObjectSchema) - - return JSON.stringify({ task: task ?? null }) -} - -async function handleUpdate( - args: Record, - config: Partial -): Promise { - const validatedArgs = TaskUpdateInputSchema.parse(args) - const taskId = parseTaskId(validatedArgs.id) - if (!taskId) { - return JSON.stringify({ error: "invalid_task_id" }) - } - const taskDir = getTaskDir(config) - const lock = acquireLock(taskDir) - - if (!lock.acquired) { - return JSON.stringify({ error: "task_lock_unavailable" }) - } - - try { - const taskPath = join(taskDir, `${taskId}.json`) - const task = readJsonSafe(taskPath, TaskObjectSchema) - - if (!task) { - return JSON.stringify({ error: "task_not_found" }) - } - - // Update fields if provided - if (validatedArgs.subject !== undefined) { - task.subject = validatedArgs.subject - } - if (validatedArgs.description !== undefined) { - task.description = validatedArgs.description - } - if (validatedArgs.status !== undefined) { - task.status = validatedArgs.status - } - if (validatedArgs.addBlockedBy !== undefined) { - task.blockedBy = [...task.blockedBy, ...validatedArgs.addBlockedBy] - } - if (validatedArgs.repoURL !== undefined) { - task.repoURL = validatedArgs.repoURL - } - if (validatedArgs.parentID !== undefined) { - task.parentID = validatedArgs.parentID - } - - const validatedTask = TaskObjectSchema.parse(task) - writeJsonAtomic(taskPath, validatedTask) - - return JSON.stringify({ task: validatedTask }) - } finally { - lock.release() - } -} - -async function handleDelete( - args: Record, - config: Partial -): Promise { - const validatedArgs = TaskDeleteInputSchema.parse(args) - const taskId = parseTaskId(validatedArgs.id) - if (!taskId) { - return JSON.stringify({ error: "invalid_task_id" }) - } - const taskDir = getTaskDir(config) - const lock = acquireLock(taskDir) - - if (!lock.acquired) { - return JSON.stringify({ error: "task_lock_unavailable" }) - } - - try { - const taskPath = join(taskDir, `${taskId}.json`) - - if (!existsSync(taskPath)) { - return JSON.stringify({ error: "task_not_found" }) - } - - unlinkSync(taskPath) - - return JSON.stringify({ success: true }) - } finally { - lock.release() - } -}