File size: 4,342 Bytes
1f21206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
 * TaskService — CLI Task V2 的读取与查询
 *
 * 任务信息存储在 ~/.claude/tasks/<task_list_id>/ 目录下,每个任务一个 JSON 文件。
 * Task list ID 可以是 session ID、team name 等。
 */

import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'

export type TaskStatus = 'pending' | 'in_progress' | 'completed'

export type TaskInfo = {
  id: string
  subject: string
  description: string
  activeForm?: string
  owner?: string
  status: TaskStatus
  blocks: string[]
  blockedBy: string[]
  metadata?: Record<string, unknown>
  taskListId: string
}

export type TaskListSummary = {
  id: string
  taskCount: number
  completedCount: number
  inProgressCount: number
  pendingCount: number
}

export class TaskService {
  private getConfigDir(): string {
    return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
  }

  private getTasksDir(): string {
    return path.join(this.getConfigDir(), 'tasks')
  }

  /** 列出所有 task list (目录) */
  async listTaskLists(): Promise<TaskListSummary[]> {
    const tasksDir = this.getTasksDir()
    try {
      const entries = await fs.readdir(tasksDir, { withFileTypes: true })
      const results: TaskListSummary[] = []

      for (const entry of entries) {
        if (!entry.isDirectory()) continue
        const tasks = await this.getTasksForList(entry.name)
        if (tasks.length === 0) continue

        results.push({
          id: entry.name,
          taskCount: tasks.length,
          completedCount: tasks.filter((t) => t.status === 'completed').length,
          inProgressCount: tasks.filter((t) => t.status === 'in_progress').length,
          pendingCount: tasks.filter((t) => t.status === 'pending').length,
        })
      }

      return results
    } catch (err: any) {
      if (err.code === 'ENOENT') return []
      throw err
    }
  }

  /** 获取指定 task list 的所有任务 */
  async getTasksForList(taskListId: string): Promise<TaskInfo[]> {
    const listDir = path.join(this.getTasksDir(), taskListId)
    try {
      const entries = await fs.readdir(listDir)
      const tasks: TaskInfo[] = []

      for (const filename of entries) {
        if (!filename.endsWith('.json')) continue
        try {
          const raw = await fs.readFile(path.join(listDir, filename), 'utf-8')
          const data = JSON.parse(raw)
          const task = this.parseTaskFile(data, taskListId)
          if (task) tasks.push(task)
        } catch {
          // skip unparseable files
        }
      }

      // Sort by numeric ID
      return tasks.sort((a, b) => {
        const numA = parseInt(a.id, 10)
        const numB = parseInt(b.id, 10)
        if (!isNaN(numA) && !isNaN(numB)) return numA - numB
        return a.id.localeCompare(b.id)
      })
    } catch (err: any) {
      if (err.code === 'ENOENT') return []
      throw err
    }
  }

  /** 列出所有任务(跨所有 task list) */
  async listTasks(): Promise<TaskInfo[]> {
    const taskLists = await this.listTaskLists()
    const allTasks: TaskInfo[] = []
    for (const list of taskLists) {
      const tasks = await this.getTasksForList(list.id)
      allTasks.push(...tasks)
    }
    return allTasks
  }

  /** 获取单个任务详情 */
  async getTask(taskListId: string, taskId: string): Promise<TaskInfo | null> {
    const tasks = await this.getTasksForList(taskListId)
    return tasks.find((t) => t.id === taskId) || null
  }

  /** 解析单个任务文件 — 匹配 CLI V2 Task 格式 */
  private parseTaskFile(data: any, taskListId: string): TaskInfo | null {
    if (!data || typeof data !== 'object') return null
    if (!data.id || !data.subject) return null

    // Skip internal tasks
    if (data.metadata?._internal) return null

    return {
      id: String(data.id),
      subject: data.subject || '',
      description: data.description || '',
      activeForm: data.activeForm,
      owner: data.owner,
      status: (['pending', 'in_progress', 'completed'].includes(data.status)
        ? data.status
        : 'pending') as TaskStatus,
      blocks: Array.isArray(data.blocks) ? data.blocks : [],
      blockedBy: Array.isArray(data.blockedBy) ? data.blockedBy : [],
      metadata: data.metadata,
      taskListId,
    }
  }
}

export const taskService = new TaskService()