forked from laurentenhoor/devclaw
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtask-list.ts
More file actions
147 lines (134 loc) · 5.74 KB
/
task-list.ts
File metadata and controls
147 lines (134 loc) · 5.74 KB
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
/**
* task_list — Browse issues by workflow state.
*
* Lists issues grouped by state label with optional filtering by state type,
* specific label, or text search. Supports terminal (closed) issues.
*/
import { jsonResult } from "../../json-result.js";
import type { PluginContext } from "../../context.js";
import type { ToolContext } from "../../types.js";
import { log as auditLog } from "../../audit.js";
import { requireWorkspaceDir, resolveChannelId, resolveProject, resolveProvider } from "../helpers.js";
import { loadWorkflow, StateType, findStateByLabel } from "../../workflow/index.js";
export function createTaskListTool(ctx: PluginContext) {
return (toolCtx: ToolContext) => ({
name: "task_list",
label: "Task List",
description: `Browse issues for a project by workflow state. Shows issues grouped by state label. Use \`tasks_status\` for a quick issue dashboard, this tool for filtered browsing.`,
parameters: {
type: "object",
required: ["channelId"],
properties: {
channelId: {
type: "string",
description: "YOUR chat/group ID — the numeric ID of the chat you are in right now (e.g. '-1003844794417'). Do NOT guess; use the ID of the conversation this message came from.",
},
messageThreadId: {
type: "number",
description: "Optional Telegram forum topic ID for this project (message_thread_id). When provided, resolves the topic-bound project within the chat.",
},
stateType: {
type: "string",
enum: ["queue", "active", "hold", "terminal", "all"],
description: "Filter by state type. Defaults to all non-terminal states.",
},
label: {
type: "string",
description: "Filter by specific state label (e.g. 'Planning', 'Done'). Overrides stateType.",
},
search: {
type: "string",
description: "Text search in issue titles (case-insensitive).",
},
limit: {
type: "number",
description: "Max issues per state. Defaults to 20.",
},
},
},
async execute(_id: string, params: Record<string, unknown>) {
const workspaceDir = requireWorkspaceDir(toolCtx);
const channelId = resolveChannelId(toolCtx, params.channelId as string | undefined);
const messageThreadId = params.messageThreadId as number | undefined;
const stateType = params.stateType as string | undefined;
const label = params.label as string | undefined;
const search = params.search as string | undefined;
const limit = (params.limit as number) ?? 20;
const channelType = (toolCtx.messageChannel as string | undefined) ?? "telegram";
const accountId = toolCtx.agentAccountId as string | undefined;
const { project } = await resolveProject(workspaceDir, channelId, {
channel: channelType,
accountId,
messageThreadId,
});
const { provider } = await resolveProvider(project, ctx.runCommand);
const workflow = await loadWorkflow(workspaceDir, project.name);
// Determine which labels to fetch
type FetchEntry = { label: string; type: string; role?: string; issueState: "open" | "closed" | "all" };
const labelsToFetch: FetchEntry[] = [];
if (label) {
const stateConfig = findStateByLabel(workflow, label);
if (!stateConfig) throw new Error(`Unknown state label "${label}". Check workflow_guide for valid states.`);
labelsToFetch.push({
label: stateConfig.label,
type: stateConfig.type,
role: stateConfig.role,
issueState: stateConfig.type === StateType.TERMINAL ? "closed" : "open",
});
} else {
const includeTerminal = stateType === "terminal" || stateType === "all";
for (const state of Object.values(workflow.states)) {
if (state.type === StateType.TERMINAL && !includeTerminal) continue;
if (stateType && stateType !== "all" && state.type !== stateType) continue;
labelsToFetch.push({
label: state.label,
type: state.type,
role: state.role,
issueState: state.type === StateType.TERMINAL ? "closed" : "open",
});
}
}
// Fetch and filter
const searchLower = search?.toLowerCase();
const results: Array<{
label: string;
type: string;
role?: string;
issues: Array<{ id: number; title: string; url: string }>;
total: number;
}> = [];
for (const entry of labelsToFetch) {
let issues = await provider.listIssues({ label: entry.label, state: entry.issueState }).catch(() => []);
if (searchLower) {
issues = issues.filter((i) => i.title.toLowerCase().includes(searchLower));
}
const total = issues.length;
const limited = issues.slice(0, limit);
results.push({
label: entry.label,
type: entry.type,
role: entry.role,
issues: limited.map((i) => ({ id: i.iid, title: i.title, url: i.web_url })),
total,
});
}
// Only include states that have issues (unless a specific label was requested)
const nonEmpty = label ? results : results.filter((r) => r.total > 0);
const totalIssues = results.reduce((sum, r) => sum + r.total, 0);
await auditLog(workspaceDir, "task_list", {
project: project.name,
stateType: stateType ?? (label ? undefined : "non-terminal"),
label,
search,
totalIssues,
});
return jsonResult({
success: true,
project: project.name,
filter: { stateType: stateType ?? null, label: label ?? null, search: search ?? null },
states: nonEmpty,
totalIssues,
});
},
});
}