HuggingClaw-MissionControl / src /lib /claude-tasks.ts
nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
Raw
History Blame Contribute Delete
4.03 kB
/**
* Claude Code Task & Team Scanner
*
* Read-only bridge that discovers Claude Code's:
* - Team tasks from ~/.claude/tasks/<team>/<N>.json
* - Team configs from ~/.claude/teams/<name>/config.json
*
* Follows the same throttled-scan pattern as claude-sessions.ts.
*/
import { readdirSync, readFileSync, statSync } from 'fs'
import { join } from 'path'
import { config } from './config'
import { logger } from './logger'
export interface ClaudeCodeTask {
id: string
teamName: string
subject: string
description: string
status: string
owner: string
blocks: string[]
blockedBy: string[]
activeForm?: string
}
export interface ClaudeCodeTeam {
name: string
description: string
createdAt: number
leadAgentId: string
members: Array<{
agentId: string
name: string
agentType: string
model: string
}>
}
export interface ClaudeCodeScanResult {
teams: ClaudeCodeTeam[]
tasks: ClaudeCodeTask[]
}
function safeParse<T>(filePath: string): T | null {
try {
return JSON.parse(readFileSync(filePath, 'utf-8'))
} catch {
return null
}
}
function scanTeams(claudeHome: string): ClaudeCodeTeam[] {
const teamsDir = join(claudeHome, 'teams')
let teamDirs: string[]
try {
teamDirs = readdirSync(teamsDir)
} catch {
return []
}
const teams: ClaudeCodeTeam[] = []
for (const teamName of teamDirs) {
const configPath = join(teamsDir, teamName, 'config.json')
try {
if (!statSync(configPath).isFile()) continue
} catch {
continue
}
const data = safeParse<any>(configPath)
if (!data?.name) continue
teams.push({
name: data.name,
description: data.description || '',
createdAt: data.createdAt || 0,
leadAgentId: data.leadAgentId || '',
members: Array.isArray(data.members)
? data.members.map((m: any) => ({
agentId: m.agentId || '',
name: m.name || '',
agentType: m.agentType || '',
model: m.model || '',
}))
: [],
})
}
return teams
}
function scanTasks(claudeHome: string): ClaudeCodeTask[] {
const tasksDir = join(claudeHome, 'tasks')
let teamDirs: string[]
try {
teamDirs = readdirSync(tasksDir)
} catch {
return []
}
const tasks: ClaudeCodeTask[] = []
for (const teamName of teamDirs) {
const teamDir = join(tasksDir, teamName)
try {
if (!statSync(teamDir).isDirectory()) continue
} catch {
continue
}
// Skip .lock files, only read JSON task files
let files: string[]
try {
files = readdirSync(teamDir).filter(f => f.endsWith('.json'))
} catch {
continue
}
for (const file of files) {
const data = safeParse<any>(join(teamDir, file))
if (!data?.id) continue
tasks.push({
id: `${teamName}/${data.id}`,
teamName,
subject: data.subject || data.title || `Task ${data.id}`,
description: data.description || '',
status: data.status || 'unknown',
owner: data.owner || '',
blocks: Array.isArray(data.blocks) ? data.blocks : [],
blockedBy: Array.isArray(data.blockedBy) ? data.blockedBy : [],
activeForm: data.activeForm,
})
}
}
return tasks
}
export function scanClaudeCodeTasks(): ClaudeCodeScanResult {
const claudeHome = config.claudeHome
if (!claudeHome) return { teams: [], tasks: [] }
return {
teams: scanTeams(claudeHome),
tasks: scanTasks(claudeHome),
}
}
// Throttle full disk scans
let lastScanAt = 0
let cachedResult: ClaudeCodeScanResult = { teams: [], tasks: [] }
const SCAN_THROTTLE_MS = 30_000
export function getClaudeCodeTasks(force = false): ClaudeCodeScanResult {
const now = Date.now()
if (!force && lastScanAt > 0 && (now - lastScanAt) < SCAN_THROTTLE_MS) {
return cachedResult
}
try {
cachedResult = scanClaudeCodeTasks()
lastScanAt = now
} catch (err) {
logger.warn({ err }, 'Claude Code task scan failed')
}
return cachedResult
}