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:
46
src/tools/task/task-action-create.ts
Normal file
46
src/tools/task/task-action-create.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
36
src/tools/task/task-action-delete.ts
Normal file
36
src/tools/task/task-action-delete.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
21
src/tools/task/task-action-get.ts
Normal file
21
src/tools/task/task-action-get.ts
Normal 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 })
|
||||
}
|
||||
60
src/tools/task/task-action-list.ts
Normal file
60
src/tools/task/task-action-list.ts
Normal 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 })
|
||||
}
|
||||
57
src/tools/task/task-action-update.ts
Normal file
57
src/tools/task/task-action-update.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
6
src/tools/task/task-id-validator.ts
Normal file
6
src/tools/task/task-id-validator.ts
Normal 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
|
||||
}
|
||||
@@ -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<OhMyOpenCodeConfig>): 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<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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user