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
This commit is contained in:
YeonGyu-Kim
2026-02-08 16:24:43 +09:00
parent 4400e18a52
commit 052beb364f
7 changed files with 232 additions and 234 deletions

View File

@@ -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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>,
context: { sessionID: string }
): Promise<string> {
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()
}
}

View File

@@ -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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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()
}
}

View File

@@ -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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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 })
}

View File

@@ -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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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 })
}

View File

@@ -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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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()
}
}

View File

@@ -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
}

View File

@@ -1,38 +1,10 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" 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 { OhMyOpenCodeConfig } from "../../config/schema"
import type { import { handleCreate } from "./task-action-create"
TaskObject, import { handleDelete } from "./task-action-delete"
TaskCreateInput, import { handleGet } from "./task-action-get"
TaskListInput, import { handleList } from "./task-action-list"
TaskGetInput, import { handleUpdate } from "./task-action-update"
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
}
export function createTask(config: Partial<OhMyOpenCodeConfig>): ToolDefinition { export function createTask(config: Partial<OhMyOpenCodeConfig>): ToolDefinition {
return tool({ return tool({
@@ -66,9 +38,7 @@ All actions return JSON strings.`,
limit: tool.schema.number().optional().describe("Maximum number of tasks to return"), limit: tool.schema.number().optional().describe("Maximum number of tasks to return"),
}, },
execute: async (args, context) => { execute: async (args, context) => {
const action = args.action as "create" | "list" | "get" | "update" | "delete" switch (args.action) {
switch (action) {
case "create": case "create":
return handleCreate(args, config, context) return handleCreate(args, config, context)
case "list": case "list":
@@ -85,201 +55,3 @@ All actions return JSON strings.`,
}, },
}) })
} }
async function handleCreate(
args: Record<string, unknown>,
config: Partial<OhMyOpenCodeConfig>,
context: { sessionID: string }
): Promise<string> {
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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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<string, unknown>,
config: Partial<OhMyOpenCodeConfig>
): Promise<string> {
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()
}
}