@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.
@@ -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"}