@finityno/claude-code-acp 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-agent.d.ts +11 -0
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +131 -6
- package/dist/lib.d.ts +5 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +7 -0
- package/dist/mcp-server.d.ts +2 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +14 -1
- package/dist/subagent-tracker.d.ts +102 -2
- package/dist/subagent-tracker.d.ts.map +1 -1
- package/dist/subagent-tracker.js +189 -22
- package/dist/task-manager.d.ts +106 -0
- package/dist/task-manager.d.ts.map +1 -0
- package/dist/task-manager.js +231 -0
- package/dist/task-mcp-tools.d.ts +16 -0
- package/dist/task-mcp-tools.d.ts.map +1 -0
- package/dist/task-mcp-tools.js +256 -0
- package/dist/task-store.d.ts +123 -0
- package/dist/task-store.d.ts.map +1 -0
- package/dist/task-store.js +292 -0
- package/dist/work-item-mcp-tools.d.ts +12 -0
- package/dist/work-item-mcp-tools.d.ts.map +1 -0
- package/dist/work-item-mcp-tools.js +296 -0
- package/package.json +2 -2
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TaskManager } from "./task-manager.js";
|
|
3
|
+
/**
|
|
4
|
+
* Register MCP tools for task management
|
|
5
|
+
*/
|
|
6
|
+
export function registerTaskMcpTools(server, options) {
|
|
7
|
+
const { tracker, taskManager, sessionId } = options;
|
|
8
|
+
// List all tasks
|
|
9
|
+
server.registerTool("ListTasks", {
|
|
10
|
+
title: "List Tasks",
|
|
11
|
+
description: `List all tracked tasks/subagents across sessions.
|
|
12
|
+
Use this to see running, completed, failed, or cancelled tasks.
|
|
13
|
+
Can filter by status, session, or background execution.`,
|
|
14
|
+
inputSchema: {
|
|
15
|
+
status: z
|
|
16
|
+
.array(z.enum(["pending", "running", "completed", "failed", "cancelled", "stopped"]))
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Filter by task status"),
|
|
19
|
+
sessionId: z.string().optional().describe("Filter by session ID"),
|
|
20
|
+
backgroundOnly: z.boolean().optional().describe("Only show background tasks"),
|
|
21
|
+
subagentType: z.string().optional().describe("Filter by subagent type"),
|
|
22
|
+
limit: z.number().optional().default(20).describe("Maximum number of tasks to return"),
|
|
23
|
+
},
|
|
24
|
+
annotations: {
|
|
25
|
+
title: "List tasks",
|
|
26
|
+
readOnlyHint: true,
|
|
27
|
+
},
|
|
28
|
+
}, async (input) => {
|
|
29
|
+
const manager = taskManager ?? createTempManager(tracker);
|
|
30
|
+
const tasks = manager.getAllTasks({
|
|
31
|
+
status: input.status,
|
|
32
|
+
sessionId: input.sessionId,
|
|
33
|
+
runInBackground: input.backgroundOnly,
|
|
34
|
+
subagentType: input.subagentType,
|
|
35
|
+
});
|
|
36
|
+
const limited = tasks.slice(0, input.limit);
|
|
37
|
+
return {
|
|
38
|
+
content: [{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: JSON.stringify({
|
|
41
|
+
total: tasks.length,
|
|
42
|
+
returned: limited.length,
|
|
43
|
+
tasks: limited.map((t) => ({
|
|
44
|
+
id: t.id,
|
|
45
|
+
type: t.subagentType,
|
|
46
|
+
description: t.description,
|
|
47
|
+
status: t.status,
|
|
48
|
+
runInBackground: t.runInBackground,
|
|
49
|
+
createdAt: new Date(t.createdAt).toISOString(),
|
|
50
|
+
durationMs: t.completedAt && t.startedAt ? t.completedAt - t.startedAt : undefined,
|
|
51
|
+
agentId: t.agentId,
|
|
52
|
+
canResume: !!(t.agentId && ["completed", "failed", "stopped"].includes(t.status)),
|
|
53
|
+
summary: t.summary,
|
|
54
|
+
})),
|
|
55
|
+
}, null, 2),
|
|
56
|
+
}],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
// Get task status
|
|
60
|
+
server.registerTool("GetTaskStatus", {
|
|
61
|
+
title: "Get Task Status",
|
|
62
|
+
description: "Get detailed status and metadata for a specific task.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
taskId: z.string().describe("The task ID to get status for"),
|
|
65
|
+
},
|
|
66
|
+
annotations: {
|
|
67
|
+
title: "Get task status",
|
|
68
|
+
readOnlyHint: true,
|
|
69
|
+
},
|
|
70
|
+
}, async (input) => {
|
|
71
|
+
const task = tracker.getSubagent(input.taskId);
|
|
72
|
+
if (!task) {
|
|
73
|
+
return {
|
|
74
|
+
isError: true,
|
|
75
|
+
content: [{ type: "text", text: `Task not found: ${input.taskId}` }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: JSON.stringify({
|
|
82
|
+
id: task.id,
|
|
83
|
+
type: task.subagentType,
|
|
84
|
+
description: task.description,
|
|
85
|
+
prompt: task.prompt,
|
|
86
|
+
status: task.status,
|
|
87
|
+
model: task.model,
|
|
88
|
+
runInBackground: task.runInBackground,
|
|
89
|
+
parentSessionId: task.parentSessionId,
|
|
90
|
+
parentToolUseId: task.parentToolUseId,
|
|
91
|
+
createdAt: new Date(task.createdAt).toISOString(),
|
|
92
|
+
startedAt: task.startedAt ? new Date(task.startedAt).toISOString() : undefined,
|
|
93
|
+
completedAt: task.completedAt ? new Date(task.completedAt).toISOString() : undefined,
|
|
94
|
+
durationMs: task.completedAt && task.startedAt ? task.completedAt - task.startedAt : undefined,
|
|
95
|
+
agentId: task.agentId,
|
|
96
|
+
agentName: task.agentName,
|
|
97
|
+
teamName: task.teamName,
|
|
98
|
+
permissionMode: task.permissionMode,
|
|
99
|
+
outputFile: task.outputFile,
|
|
100
|
+
summary: task.summary,
|
|
101
|
+
result: task.result,
|
|
102
|
+
error: task.error,
|
|
103
|
+
isResumed: task.isResumed,
|
|
104
|
+
originalTaskId: task.originalTaskId,
|
|
105
|
+
canResume: !!(task.agentId && ["completed", "failed", "stopped"].includes(task.status)),
|
|
106
|
+
}, null, 2),
|
|
107
|
+
}],
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
// Get task output
|
|
111
|
+
server.registerTool("GetTaskOutput", {
|
|
112
|
+
title: "Get Task Output",
|
|
113
|
+
description: "Read output file content for a background task.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
taskId: z.string().describe("The task ID to get output for"),
|
|
116
|
+
tail: z.number().optional().describe("Only return last N lines"),
|
|
117
|
+
},
|
|
118
|
+
annotations: {
|
|
119
|
+
title: "Get task output",
|
|
120
|
+
readOnlyHint: true,
|
|
121
|
+
},
|
|
122
|
+
}, async (input) => {
|
|
123
|
+
const task = tracker.getSubagent(input.taskId);
|
|
124
|
+
if (!task) {
|
|
125
|
+
return {
|
|
126
|
+
isError: true,
|
|
127
|
+
content: [{ type: "text", text: `Task not found: ${input.taskId}` }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!task.outputFile) {
|
|
131
|
+
return {
|
|
132
|
+
isError: true,
|
|
133
|
+
content: [{ type: "text", text: `Task ${input.taskId} has no output file` }],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const manager = taskManager ?? createTempManager(tracker);
|
|
137
|
+
const output = input.tail
|
|
138
|
+
? await manager.getTaskOutputTail(input.taskId, input.tail)
|
|
139
|
+
: await manager.getTaskOutput(input.taskId);
|
|
140
|
+
if (output === null) {
|
|
141
|
+
return {
|
|
142
|
+
isError: true,
|
|
143
|
+
content: [{ type: "text", text: `Output file not found: ${task.outputFile}` }],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { content: [{ type: "text", text: output }] };
|
|
147
|
+
});
|
|
148
|
+
// Cancel task
|
|
149
|
+
server.registerTool("CancelTask", {
|
|
150
|
+
title: "Cancel Task",
|
|
151
|
+
description: "Cancel a running task/subagent.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
taskId: z.string().describe("The task ID to cancel"),
|
|
154
|
+
},
|
|
155
|
+
annotations: {
|
|
156
|
+
title: "Cancel task",
|
|
157
|
+
destructiveHint: true,
|
|
158
|
+
},
|
|
159
|
+
}, async (input) => {
|
|
160
|
+
const task = tracker.getSubagent(input.taskId);
|
|
161
|
+
if (!task) {
|
|
162
|
+
return {
|
|
163
|
+
isError: true,
|
|
164
|
+
content: [{ type: "text", text: `Task not found: ${input.taskId}` }],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (task.status !== "running") {
|
|
168
|
+
return {
|
|
169
|
+
isError: true,
|
|
170
|
+
content: [{ type: "text", text: `Task not running: ${task.status}` }],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
await tracker.cancelSubagent(input.taskId);
|
|
174
|
+
return { content: [{ type: "text", text: `Task ${input.taskId} cancelled` }] };
|
|
175
|
+
});
|
|
176
|
+
// List resumable tasks
|
|
177
|
+
server.registerTool("ListResumableTasks", {
|
|
178
|
+
title: "List Resumable Tasks",
|
|
179
|
+
description: "List tasks that can be resumed via Task tool's resume parameter.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
limit: z.number().optional().default(10).describe("Maximum number of tasks to return"),
|
|
182
|
+
},
|
|
183
|
+
annotations: {
|
|
184
|
+
title: "List resumable tasks",
|
|
185
|
+
readOnlyHint: true,
|
|
186
|
+
},
|
|
187
|
+
}, async (input) => {
|
|
188
|
+
const tasks = tracker.getResumableTasks().slice(0, input.limit);
|
|
189
|
+
return {
|
|
190
|
+
content: [{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: JSON.stringify({
|
|
193
|
+
count: tasks.length,
|
|
194
|
+
tasks: tasks.map((t) => ({
|
|
195
|
+
id: t.id,
|
|
196
|
+
agentId: t.agentId,
|
|
197
|
+
type: t.subagentType,
|
|
198
|
+
description: t.description,
|
|
199
|
+
status: t.status,
|
|
200
|
+
completedAt: t.completedAt ? new Date(t.completedAt).toISOString() : undefined,
|
|
201
|
+
summary: t.summary,
|
|
202
|
+
error: t.error,
|
|
203
|
+
})),
|
|
204
|
+
}, null, 2),
|
|
205
|
+
}],
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
// Get task stats
|
|
209
|
+
server.registerTool("GetTaskStats", {
|
|
210
|
+
title: "Get Task Statistics",
|
|
211
|
+
description: "Get statistics about all tracked tasks.",
|
|
212
|
+
inputSchema: {},
|
|
213
|
+
annotations: {
|
|
214
|
+
title: "Get task statistics",
|
|
215
|
+
readOnlyHint: true,
|
|
216
|
+
},
|
|
217
|
+
}, async () => {
|
|
218
|
+
const stats = tracker.getStats();
|
|
219
|
+
return { content: [{ type: "text", text: JSON.stringify(stats, null, 2) }] };
|
|
220
|
+
});
|
|
221
|
+
// Get running tasks
|
|
222
|
+
server.registerTool("GetRunningTasks", {
|
|
223
|
+
title: "Get Running Tasks",
|
|
224
|
+
description: "Get all currently running tasks.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
allSessions: z.boolean().optional().default(false).describe("Include all sessions"),
|
|
227
|
+
},
|
|
228
|
+
annotations: {
|
|
229
|
+
title: "Get running tasks",
|
|
230
|
+
readOnlyHint: true,
|
|
231
|
+
},
|
|
232
|
+
}, async (input) => {
|
|
233
|
+
const tasks = input.allSessions
|
|
234
|
+
? tracker.getRunningSubagents()
|
|
235
|
+
: tracker.getRunningSubagentsForSession(sessionId);
|
|
236
|
+
return {
|
|
237
|
+
content: [{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: JSON.stringify({
|
|
240
|
+
count: tasks.length,
|
|
241
|
+
tasks: tasks.map((t) => ({
|
|
242
|
+
id: t.id,
|
|
243
|
+
type: t.subagentType,
|
|
244
|
+
description: t.description,
|
|
245
|
+
runInBackground: t.runInBackground,
|
|
246
|
+
startedAt: t.startedAt ? new Date(t.startedAt).toISOString() : undefined,
|
|
247
|
+
elapsedMs: t.startedAt ? Date.now() - t.startedAt : undefined,
|
|
248
|
+
})),
|
|
249
|
+
}, null, 2),
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function createTempManager(tracker) {
|
|
255
|
+
return new TaskManager(tracker, { autoSave: false });
|
|
256
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Logger } from "./acp-agent.js";
|
|
2
|
+
/**
|
|
3
|
+
* Task status matching Claude Code's internal system
|
|
4
|
+
*/
|
|
5
|
+
export type TaskStatus = "pending" | "in_progress" | "completed";
|
|
6
|
+
/**
|
|
7
|
+
* Task structure matching Claude Code's ~/.claude/tasks/ format
|
|
8
|
+
*/
|
|
9
|
+
export interface Task {
|
|
10
|
+
id: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
description: string;
|
|
13
|
+
activeForm: string;
|
|
14
|
+
status: TaskStatus;
|
|
15
|
+
owner?: string;
|
|
16
|
+
blocks: string[];
|
|
17
|
+
blockedBy: string[];
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Input for creating a new task
|
|
22
|
+
*/
|
|
23
|
+
export interface TaskCreateInput {
|
|
24
|
+
subject: string;
|
|
25
|
+
description: string;
|
|
26
|
+
activeForm?: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Input for updating a task
|
|
31
|
+
*/
|
|
32
|
+
export interface TaskUpdateInput {
|
|
33
|
+
status?: TaskStatus;
|
|
34
|
+
subject?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
activeForm?: string;
|
|
37
|
+
owner?: string;
|
|
38
|
+
addBlocks?: string[];
|
|
39
|
+
addBlockedBy?: string[];
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Options for TaskStore
|
|
44
|
+
*/
|
|
45
|
+
export interface TaskStoreOptions {
|
|
46
|
+
/** Task list ID (UUID or custom ID) */
|
|
47
|
+
taskListId: string;
|
|
48
|
+
/** Base path for task storage (default: ~/.claude/tasks) */
|
|
49
|
+
basePath?: string;
|
|
50
|
+
/** Logger instance */
|
|
51
|
+
logger?: Logger;
|
|
52
|
+
/** Callback when tasks change (from file watcher) */
|
|
53
|
+
onChange?: (tasks: Task[]) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* TaskStore manages reading/writing tasks to ~/.claude/tasks/
|
|
57
|
+
* Compatible with Claude Code's internal task system
|
|
58
|
+
*/
|
|
59
|
+
export declare class TaskStore {
|
|
60
|
+
private taskListId;
|
|
61
|
+
private basePath;
|
|
62
|
+
private logger;
|
|
63
|
+
private onChange?;
|
|
64
|
+
private watcher;
|
|
65
|
+
private nextId;
|
|
66
|
+
private initialized;
|
|
67
|
+
constructor(options: TaskStoreOptions);
|
|
68
|
+
/**
|
|
69
|
+
* Get the directory path for this task list
|
|
70
|
+
*/
|
|
71
|
+
get taskListPath(): string;
|
|
72
|
+
/**
|
|
73
|
+
* Initialize the task store (create directory, determine next ID)
|
|
74
|
+
*/
|
|
75
|
+
init(): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Create a new task
|
|
78
|
+
*/
|
|
79
|
+
create(input: TaskCreateInput): Promise<Task>;
|
|
80
|
+
/**
|
|
81
|
+
* Get a task by ID
|
|
82
|
+
*/
|
|
83
|
+
get(taskId: string): Promise<Task | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Update a task
|
|
86
|
+
*/
|
|
87
|
+
update(taskId: string, input: TaskUpdateInput): Promise<Task>;
|
|
88
|
+
/**
|
|
89
|
+
* List all tasks
|
|
90
|
+
*/
|
|
91
|
+
list(): Promise<Task[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Delete a task
|
|
94
|
+
*/
|
|
95
|
+
delete(taskId: string): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* Start watching for changes from other sessions
|
|
98
|
+
*/
|
|
99
|
+
watch(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Stop watching for changes
|
|
102
|
+
*/
|
|
103
|
+
close(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Get task statistics
|
|
106
|
+
*/
|
|
107
|
+
getStats(): Promise<{
|
|
108
|
+
total: number;
|
|
109
|
+
pending: number;
|
|
110
|
+
inProgress: number;
|
|
111
|
+
completed: number;
|
|
112
|
+
blocked: number;
|
|
113
|
+
}>;
|
|
114
|
+
private getTaskFilePath;
|
|
115
|
+
private saveTask;
|
|
116
|
+
/**
|
|
117
|
+
* Generate activeForm from subject (convert to present participle)
|
|
118
|
+
* "Fix bug" -> "Fixing bug"
|
|
119
|
+
* "Add feature" -> "Adding feature"
|
|
120
|
+
*/
|
|
121
|
+
private generateActiveForm;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=task-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../src/task-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, readdir, rm } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
/**
|
|
5
|
+
* TaskStore manages reading/writing tasks to ~/.claude/tasks/
|
|
6
|
+
* Compatible with Claude Code's internal task system
|
|
7
|
+
*/
|
|
8
|
+
export class TaskStore {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.watcher = null;
|
|
11
|
+
this.nextId = 1;
|
|
12
|
+
this.initialized = false;
|
|
13
|
+
this.taskListId = options.taskListId;
|
|
14
|
+
this.basePath = options.basePath ?? join(homedir(), ".claude", "tasks");
|
|
15
|
+
this.logger = options.logger ?? console;
|
|
16
|
+
this.onChange = options.onChange;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get the directory path for this task list
|
|
20
|
+
*/
|
|
21
|
+
get taskListPath() {
|
|
22
|
+
return join(this.basePath, this.taskListId);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the task store (create directory, determine next ID)
|
|
26
|
+
*/
|
|
27
|
+
async init() {
|
|
28
|
+
if (this.initialized)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
await mkdir(this.taskListPath, { recursive: true });
|
|
32
|
+
// Determine next ID from existing tasks
|
|
33
|
+
const tasks = await this.list();
|
|
34
|
+
if (tasks.length > 0) {
|
|
35
|
+
const maxId = Math.max(...tasks.map((t) => parseInt(t.id, 10) || 0));
|
|
36
|
+
this.nextId = maxId + 1;
|
|
37
|
+
}
|
|
38
|
+
this.initialized = true;
|
|
39
|
+
this.logger.log(`[TaskStore] Initialized at ${this.taskListPath}, next ID: ${this.nextId}`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.logger.error(`[TaskStore] Failed to initialize:`, err);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Create a new task
|
|
48
|
+
*/
|
|
49
|
+
async create(input) {
|
|
50
|
+
await this.init();
|
|
51
|
+
const task = {
|
|
52
|
+
id: String(this.nextId++),
|
|
53
|
+
subject: input.subject,
|
|
54
|
+
description: input.description,
|
|
55
|
+
activeForm: input.activeForm ?? this.generateActiveForm(input.subject),
|
|
56
|
+
status: "pending",
|
|
57
|
+
blocks: [],
|
|
58
|
+
blockedBy: [],
|
|
59
|
+
metadata: input.metadata,
|
|
60
|
+
};
|
|
61
|
+
await this.saveTask(task);
|
|
62
|
+
this.logger.log(`[TaskStore] Created task ${task.id}: ${task.subject}`);
|
|
63
|
+
return task;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get a task by ID
|
|
67
|
+
*/
|
|
68
|
+
async get(taskId) {
|
|
69
|
+
await this.init();
|
|
70
|
+
const filePath = this.getTaskFilePath(taskId);
|
|
71
|
+
try {
|
|
72
|
+
const content = await readFile(filePath, "utf8");
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err.code === "ENOENT") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Update a task
|
|
84
|
+
*/
|
|
85
|
+
async update(taskId, input) {
|
|
86
|
+
await this.init();
|
|
87
|
+
const task = await this.get(taskId);
|
|
88
|
+
if (!task) {
|
|
89
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
90
|
+
}
|
|
91
|
+
// Apply updates
|
|
92
|
+
if (input.status !== undefined)
|
|
93
|
+
task.status = input.status;
|
|
94
|
+
if (input.subject !== undefined)
|
|
95
|
+
task.subject = input.subject;
|
|
96
|
+
if (input.description !== undefined)
|
|
97
|
+
task.description = input.description;
|
|
98
|
+
if (input.activeForm !== undefined)
|
|
99
|
+
task.activeForm = input.activeForm;
|
|
100
|
+
if (input.owner !== undefined)
|
|
101
|
+
task.owner = input.owner;
|
|
102
|
+
// Handle blocks/blockedBy additions
|
|
103
|
+
if (input.addBlocks) {
|
|
104
|
+
for (const blockId of input.addBlocks) {
|
|
105
|
+
if (!task.blocks.includes(blockId)) {
|
|
106
|
+
task.blocks.push(blockId);
|
|
107
|
+
}
|
|
108
|
+
// Also update the blocked task's blockedBy
|
|
109
|
+
const blockedTask = await this.get(blockId);
|
|
110
|
+
if (blockedTask && !blockedTask.blockedBy.includes(taskId)) {
|
|
111
|
+
blockedTask.blockedBy.push(taskId);
|
|
112
|
+
await this.saveTask(blockedTask);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (input.addBlockedBy) {
|
|
117
|
+
for (const blockerId of input.addBlockedBy) {
|
|
118
|
+
if (!task.blockedBy.includes(blockerId)) {
|
|
119
|
+
task.blockedBy.push(blockerId);
|
|
120
|
+
}
|
|
121
|
+
// Also update the blocking task's blocks
|
|
122
|
+
const blockerTask = await this.get(blockerId);
|
|
123
|
+
if (blockerTask && !blockerTask.blocks.includes(taskId)) {
|
|
124
|
+
blockerTask.blocks.push(taskId);
|
|
125
|
+
await this.saveTask(blockerTask);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Merge metadata
|
|
130
|
+
if (input.metadata) {
|
|
131
|
+
task.metadata = { ...task.metadata, ...input.metadata };
|
|
132
|
+
// Remove null values (deletion)
|
|
133
|
+
for (const [key, value] of Object.entries(input.metadata)) {
|
|
134
|
+
if (value === null && task.metadata) {
|
|
135
|
+
delete task.metadata[key];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// When completing a task, update blockedBy for tasks it blocks
|
|
140
|
+
if (input.status === "completed") {
|
|
141
|
+
for (const blockedId of task.blocks) {
|
|
142
|
+
const blockedTask = await this.get(blockedId);
|
|
143
|
+
if (blockedTask) {
|
|
144
|
+
blockedTask.blockedBy = blockedTask.blockedBy.filter((id) => id !== taskId);
|
|
145
|
+
await this.saveTask(blockedTask);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await this.saveTask(task);
|
|
150
|
+
this.logger.log(`[TaskStore] Updated task ${taskId}: ${task.subject} -> ${task.status}`);
|
|
151
|
+
return task;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* List all tasks
|
|
155
|
+
*/
|
|
156
|
+
async list() {
|
|
157
|
+
await this.init();
|
|
158
|
+
try {
|
|
159
|
+
const files = await readdir(this.taskListPath);
|
|
160
|
+
const tasks = [];
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
if (!file.endsWith(".json"))
|
|
163
|
+
continue;
|
|
164
|
+
const taskId = file.replace(".json", "");
|
|
165
|
+
const task = await this.get(taskId);
|
|
166
|
+
if (task)
|
|
167
|
+
tasks.push(task);
|
|
168
|
+
}
|
|
169
|
+
// Sort by ID (numeric)
|
|
170
|
+
return tasks.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (err.code === "ENOENT") {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Delete a task
|
|
181
|
+
*/
|
|
182
|
+
async delete(taskId) {
|
|
183
|
+
await this.init();
|
|
184
|
+
const task = await this.get(taskId);
|
|
185
|
+
if (!task)
|
|
186
|
+
return false;
|
|
187
|
+
// Remove from blocks/blockedBy of other tasks
|
|
188
|
+
for (const blockedId of task.blocks) {
|
|
189
|
+
const blockedTask = await this.get(blockedId);
|
|
190
|
+
if (blockedTask) {
|
|
191
|
+
blockedTask.blockedBy = blockedTask.blockedBy.filter((id) => id !== taskId);
|
|
192
|
+
await this.saveTask(blockedTask);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const blockerId of task.blockedBy) {
|
|
196
|
+
const blockerTask = await this.get(blockerId);
|
|
197
|
+
if (blockerTask) {
|
|
198
|
+
blockerTask.blocks = blockerTask.blocks.filter((id) => id !== taskId);
|
|
199
|
+
await this.saveTask(blockerTask);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await rm(this.getTaskFilePath(taskId));
|
|
204
|
+
this.logger.log(`[TaskStore] Deleted task ${taskId}`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Start watching for changes from other sessions
|
|
213
|
+
*/
|
|
214
|
+
async watch() {
|
|
215
|
+
if (this.watcher)
|
|
216
|
+
return;
|
|
217
|
+
await this.init();
|
|
218
|
+
try {
|
|
219
|
+
const fsWatch = await import("fs");
|
|
220
|
+
this.watcher = fsWatch.watch(this.taskListPath, async (eventType, filename) => {
|
|
221
|
+
if (!filename?.endsWith(".json"))
|
|
222
|
+
return;
|
|
223
|
+
this.logger.log(`[TaskStore] File change detected: ${eventType} ${filename}`);
|
|
224
|
+
if (this.onChange) {
|
|
225
|
+
const tasks = await this.list();
|
|
226
|
+
this.onChange(tasks);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.logger.log(`[TaskStore] Watching for changes at ${this.taskListPath}`);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
this.logger.error(`[TaskStore] Failed to start watcher:`, err);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Stop watching for changes
|
|
237
|
+
*/
|
|
238
|
+
close() {
|
|
239
|
+
if (this.watcher) {
|
|
240
|
+
this.watcher.close();
|
|
241
|
+
this.watcher = null;
|
|
242
|
+
this.logger.log(`[TaskStore] Stopped watching`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get task statistics
|
|
247
|
+
*/
|
|
248
|
+
async getStats() {
|
|
249
|
+
const tasks = await this.list();
|
|
250
|
+
return {
|
|
251
|
+
total: tasks.length,
|
|
252
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
253
|
+
inProgress: tasks.filter((t) => t.status === "in_progress").length,
|
|
254
|
+
completed: tasks.filter((t) => t.status === "completed").length,
|
|
255
|
+
blocked: tasks.filter((t) => t.blockedBy.length > 0 && t.status !== "completed").length,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// Private helpers
|
|
259
|
+
getTaskFilePath(taskId) {
|
|
260
|
+
return join(this.taskListPath, `${taskId}.json`);
|
|
261
|
+
}
|
|
262
|
+
async saveTask(task) {
|
|
263
|
+
const filePath = this.getTaskFilePath(task.id);
|
|
264
|
+
await writeFile(filePath, JSON.stringify(task, null, 2), "utf8");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Generate activeForm from subject (convert to present participle)
|
|
268
|
+
* "Fix bug" -> "Fixing bug"
|
|
269
|
+
* "Add feature" -> "Adding feature"
|
|
270
|
+
*/
|
|
271
|
+
generateActiveForm(subject) {
|
|
272
|
+
const words = subject.split(" ");
|
|
273
|
+
if (words.length === 0)
|
|
274
|
+
return subject;
|
|
275
|
+
const verb = words[0].toLowerCase();
|
|
276
|
+
let participle;
|
|
277
|
+
// Handle common verb endings
|
|
278
|
+
if (verb.endsWith("e")) {
|
|
279
|
+
participle = verb.slice(0, -1) + "ing";
|
|
280
|
+
}
|
|
281
|
+
else if (verb.match(/[aeiou][^aeiou]$/)) {
|
|
282
|
+
// Double consonant for short vowel + consonant
|
|
283
|
+
participle = verb + verb.slice(-1) + "ing";
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
participle = verb + "ing";
|
|
287
|
+
}
|
|
288
|
+
// Capitalize first letter
|
|
289
|
+
participle = participle.charAt(0).toUpperCase() + participle.slice(1);
|
|
290
|
+
return [participle, ...words.slice(1)].join(" ");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { TaskStore } from "./task-store.js";
|
|
3
|
+
export interface WorkItemMcpToolsOptions {
|
|
4
|
+
/** The TaskStore instance for managing work item tasks */
|
|
5
|
+
taskStore: TaskStore;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Register MCP tools for work item task management (TaskCreate, TaskGet, TaskUpdate, TaskList)
|
|
9
|
+
* These match Claude Code's internal task system stored in ~/.claude/tasks/
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerWorkItemMcpTools(server: McpServer, options: WorkItemMcpToolsOptions): void;
|
|
12
|
+
//# sourceMappingURL=work-item-mcp-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-item-mcp-tools.d.ts","sourceRoot":"","sources":["../src/work-item-mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,SAAS,EAAoB,MAAM,iBAAiB,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,uBAAuB,GAC/B,IAAI,CAgUN"}
|