jira-pilot 2.0.4 → 2.1.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.
Files changed (35) hide show
  1. package/README.md +216 -173
  2. package/bin/{jira.js → jira.ts} +10 -1
  3. package/dist/bin/jira.js +64 -0
  4. package/package.json +21 -15
  5. package/src/commands/ai-actions/{plan.js → plan.ts} +9 -9
  6. package/src/commands/ai-actions/{review.js → review.ts} +2 -2
  7. package/src/commands/ai-actions/{standup.js → standup.ts} +4 -4
  8. package/src/commands/{ai.js → ai.ts} +11 -11
  9. package/src/commands/{board.js → board.ts} +11 -11
  10. package/src/commands/bulk.ts +230 -0
  11. package/src/commands/{config.js → config.ts} +57 -8
  12. package/src/commands/dashboard.ts +222 -0
  13. package/src/commands/filter.ts +84 -0
  14. package/src/commands/{git.js → git.ts} +4 -4
  15. package/src/commands/issue-attach.ts +44 -0
  16. package/src/commands/issue-pr.ts +87 -0
  17. package/src/commands/issue-worklog.ts +90 -0
  18. package/src/commands/{issue.js → issue.ts} +359 -68
  19. package/src/commands/{mcp.js → mcp.ts} +2 -2
  20. package/src/commands/{project.js → project.ts} +11 -11
  21. package/src/commands/sprint.ts +269 -0
  22. package/src/server/{mcp-server.js → mcp-server.ts} +235 -8
  23. package/src/services/{ai-service.js → ai-service.ts} +16 -16
  24. package/src/services/{api-service.js → api-service.ts} +33 -9
  25. package/src/services/config-service.ts +21 -0
  26. package/src/types.ts +68 -0
  27. package/src/utils/{adf-parser.js → adf-parser.ts} +12 -12
  28. package/src/utils/config-store.ts +109 -0
  29. package/src/utils/{config.js → config.ts} +14 -41
  30. package/src/utils/{error-handler.js → error-handler.ts} +2 -1
  31. package/src/utils/{text-to-adf.js → text-to-adf.ts} +1 -1
  32. package/src/utils/{validators.js → validators.ts} +4 -4
  33. package/src/commands/bulk.js +0 -108
  34. package/src/commands/dashboard.js +0 -89
  35. package/src/commands/sprint.js +0 -153
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { startServer } from '../server/mcp-server.js';
4
4
 
5
- export function registerMcpCommand(program) {
5
+ export function registerMcpCommand(program: Command) {
6
6
  const mcpCmd = new Command('mcp')
7
7
  .description('Start MCP Agent Server (Stdio)')
8
8
  .action(async () => {
@@ -17,7 +17,7 @@ export function registerMcpCommand(program) {
17
17
 
18
18
  try {
19
19
  await startServer();
20
- } catch (e) {
20
+ } catch (e: any) {
21
21
  console.error('MCP Server Error:', e);
22
22
  process.exit(1);
23
23
  }
@@ -1,11 +1,11 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { table } from 'table';
3
+ import Table from 'cli-table3';
4
4
  import { api } from '../services/api-service.js';
5
5
  import ora from 'ora';
6
6
  import { handleCommandError } from '../utils/error-handler.js';
7
7
 
8
- export function registerProjectCommand(program) {
8
+ export function registerProjectCommand(program: Command) {
9
9
  const projectCmd = new Command('project')
10
10
  .description('Manage Jira projects')
11
11
  .addHelpText('after', `
@@ -17,7 +17,7 @@ Common Actions:
17
17
  .command('list')
18
18
  .description('List accessible projects')
19
19
  .option('-o, --output <format>', 'Output format (json)')
20
- .action(async (options) => {
20
+ .action(async (options: any) => {
21
21
  const spinner = ora('Fetching projects...').start();
22
22
  try {
23
23
  const data = await api.get('/project/search');
@@ -29,19 +29,19 @@ Common Actions:
29
29
  }
30
30
 
31
31
  if (options.output === 'json') {
32
- console.log(JSON.stringify(data.values.map(p => ({
32
+ console.log(JSON.stringify(data.values.map((p: any) => ({
33
33
  key: p.key, name: p.name,
34
34
  lead: p.lead?.displayName || null, style: p.style
35
35
  })), null, 2));
36
36
  return;
37
37
  }
38
38
 
39
- const tableData = [
40
- [chalk.bold('Key'), chalk.bold('Name'), chalk.bold('Leader'), chalk.bold('Style')]
41
- ];
39
+ const table = new Table({
40
+ head: [chalk.bold('Key'), chalk.bold('Name'), chalk.bold('Leader'), chalk.bold('Style')]
41
+ });
42
42
 
43
- data.values.forEach(p => {
44
- tableData.push([
43
+ data.values.forEach((p: any) => {
44
+ table.push([
45
45
  chalk.cyan(p.key),
46
46
  p.name,
47
47
  p.lead ? p.lead.displayName : 'N/A',
@@ -49,8 +49,8 @@ Common Actions:
49
49
  ]);
50
50
  });
51
51
 
52
- console.log(table(tableData));
53
- } catch (e) {
52
+ console.log(table.toString());
53
+ } catch (e: any) {
54
54
  handleCommandError(spinner, e, 'Failed to list projects');
55
55
  }
56
56
  });
@@ -0,0 +1,269 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import Table from 'cli-table3';
4
+ import { api } from '../services/api-service.js';
5
+ import ora from 'ora';
6
+ import enquirer from 'enquirer';
7
+ import { handleCommandError } from '../utils/error-handler.js';
8
+
9
+ export function registerSprintCommand(program: Command) {
10
+ const sprintCmd = new Command('sprint')
11
+ .description('Manage Sprints')
12
+ .addHelpText('after', `
13
+ Common Actions:
14
+ $ jira sprint list --board <ID|Name> # List sprints for a board
15
+ `);
16
+
17
+ sprintCmd
18
+ .command('list')
19
+ .description('List sprints for a board')
20
+ .requiredOption('-b, --board <id>', 'Board ID or name')
21
+ .option('-s, --state <state>', 'State (active, future, closed)', 'active,future')
22
+ .action(async (options: any) => {
23
+ const spinner = ora(`Fetching sprints for board ${options.board}...`).start();
24
+ try {
25
+ let boardId = options.board;
26
+
27
+ // If board option is not a number, look it up by name
28
+ if (isNaN(boardId)) {
29
+ spinner.text = `Looking up board "${options.board}"...`;
30
+ const boardData = await api.agileGet(`/board?name=${encodeURIComponent(options.board)}`);
31
+
32
+ if (!boardData.values || boardData.values.length === 0) {
33
+ throw new Error(`Board with name "${options.board}" not found. Please provide the numeric Board ID.`);
34
+ }
35
+
36
+ if (boardData.values.length > 1) {
37
+ const exact = boardData.values.find((b: any) => b.name.toLowerCase() === options.board.toLowerCase());
38
+ if (exact) {
39
+ boardId = exact.id;
40
+ } else {
41
+ console.log(chalk.yellow(`\nMultiple boards found for "${options.board}". Using "${boardData.values[0].name}" (ID: ${boardData.values[0].id}).`));
42
+ boardId = boardData.values[0].id;
43
+ }
44
+ } else {
45
+ boardId = boardData.values[0].id;
46
+ }
47
+ spinner.text = `Fetching sprints for board ${options.board} (ID: ${boardId})...`;
48
+ }
49
+
50
+ const data = await api.agileGet(`/board/${boardId}/sprint?state=${options.state}`);
51
+ spinner.stop();
52
+
53
+ if (!data.values || data.values.length === 0) {
54
+ console.log(chalk.yellow('No sprints found.'));
55
+ return;
56
+ }
57
+
58
+ const table = new Table({
59
+ head: [chalk.bold('ID'), chalk.bold('Name'), chalk.bold('State'), chalk.bold('Dates')]
60
+ });
61
+
62
+ data.values.forEach((s: any) => {
63
+ table.push([
64
+ s.id,
65
+ s.name,
66
+ s.state === 'active' ? chalk.green(s.state) : s.state,
67
+ `${s.startDate ? s.startDate.split('T')[0] : ''} -> ${s.endDate ? s.endDate.split('T')[0] : ''}`
68
+ ]);
69
+ });
70
+
71
+ console.log(table.toString());
72
+
73
+ } catch (e: any) {
74
+ handleCommandError(spinner, e, 'Failed to list sprints');
75
+ }
76
+ });
77
+
78
+ // ── SPRINT ISSUES ────────────────────────────────────────────────
79
+ sprintCmd
80
+ .command('issues')
81
+ .description('List issues in the active sprint')
82
+ .requiredOption('-b, --board <id>', 'Board ID or name')
83
+ .option('-o, --output <format>', 'Output format (json)')
84
+ .addHelpText('after', `
85
+ Examples:
86
+ $ jira sprint issues --board 5
87
+ $ jira sprint issues --board "My Board" --output json
88
+ `)
89
+ .action(async (options: any) => {
90
+ const spinner = ora('Fetching active sprint...').start();
91
+ try {
92
+ let boardId = options.board;
93
+
94
+ if (isNaN(boardId)) {
95
+ spinner.text = `Looking up board "${options.board}"...`;
96
+ const boardData = await api.agileGet(`/board?name=${encodeURIComponent(options.board)}`);
97
+ if (!boardData.values || boardData.values.length === 0) {
98
+ throw new Error(`Board "${options.board}" not found.`);
99
+ }
100
+ boardId = boardData.values[0].id;
101
+ }
102
+
103
+ // Get active sprint
104
+ const sprints = await api.agileGet(`/board/${boardId}/sprint?state=active`);
105
+ if (!sprints.values || sprints.values.length === 0) {
106
+ spinner.stop();
107
+ console.log(chalk.yellow('No active sprint found.'));
108
+ return;
109
+ }
110
+
111
+ const activeSprint = sprints.values[0];
112
+ spinner.text = `Fetching issues for sprint "${activeSprint.name}"...`;
113
+
114
+ const issues = await api.agileGet(`/sprint/${activeSprint.id}/issue?maxResults=50&fields=summary,status,assignee,priority`);
115
+ spinner.stop();
116
+
117
+ if (!issues.issues || issues.issues.length === 0) {
118
+ console.log(chalk.yellow('No issues in active sprint.'));
119
+ return;
120
+ }
121
+
122
+ console.log(chalk.bold(`\n🏃 Sprint: ${activeSprint.name}\n`));
123
+
124
+ if (options.output === 'json') {
125
+ console.log(JSON.stringify(issues.issues.map((i: any) => ({
126
+ key: i.key, summary: i.fields.summary,
127
+ status: i.fields.status?.name, assignee: i.fields.assignee?.displayName || null,
128
+ priority: i.fields.priority?.name
129
+ })), null, 2));
130
+ return;
131
+ }
132
+
133
+ const table = new Table({
134
+ head: [chalk.bold('Key'), chalk.bold('Summary'), chalk.bold('Status'), chalk.bold('Assignee'), chalk.bold('Priority')]
135
+ });
136
+ issues.issues.forEach((i: any) => {
137
+ table.push([
138
+ chalk.cyan(i.key),
139
+ i.fields.summary ? (i.fields.summary.length > 50 ? i.fields.summary.substring(0, 47) + '...' : i.fields.summary) : '',
140
+ i.fields.status?.name || '',
141
+ i.fields.assignee?.displayName || 'Unassigned',
142
+ i.fields.priority?.name || ''
143
+ ]);
144
+ });
145
+ console.log(table.toString());
146
+ console.log(chalk.grey(`${issues.issues.length} issue(s) in sprint`));
147
+
148
+ } catch (e: any) {
149
+ handleCommandError(spinner, e, 'Failed to list sprint issues');
150
+ }
151
+ });
152
+
153
+
154
+
155
+ // ── START SPRINT ──────────────────────────────────────────────────
156
+ sprintCmd
157
+ .command('start')
158
+ .description('Start a future sprint')
159
+ .argument('<sprintId>', 'Sprint ID')
160
+ .option('--start-date <date>', 'Start date (YYYY-MM-DD)')
161
+ .option('--end-date <date>', 'End date (YYYY-MM-DD)')
162
+ .action(async (sprintId, options) => {
163
+ const spinner = ora(`Fetching sprint ${sprintId}...`).start();
164
+ try {
165
+ const sprint = await api.agileGet(`/sprint/${sprintId}`);
166
+
167
+ if (sprint.state === 'active') {
168
+ spinner.fail(`Sprint "${sprint.name}" is already active.`);
169
+ return;
170
+ }
171
+ if (sprint.state === 'closed') {
172
+ spinner.fail(`Sprint "${sprint.name}" is closed.`);
173
+ return;
174
+ }
175
+ spinner.stop();
176
+
177
+ let startDate = options.startDate;
178
+ let endDate = options.endDate;
179
+
180
+ if (!startDate || !endDate) {
181
+ console.log(chalk.bold(`\nStarting Sprint: ${sprint.name}`));
182
+
183
+ const now = new Date();
184
+ const twoWeeks = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
185
+
186
+ const answers = await enquirer.prompt([
187
+ {
188
+ type: 'input',
189
+ name: 'startDate',
190
+ message: 'Start Date (YYYY-MM-DD):',
191
+ initial: now.toISOString().split('T')[0],
192
+ skip: !!startDate
193
+ },
194
+ {
195
+ type: 'input',
196
+ name: 'endDate',
197
+ message: 'End Date (YYYY-MM-DD):',
198
+ initial: twoWeeks.toISOString().split('T')[0],
199
+ skip: !!endDate
200
+ }
201
+ ]) as any;
202
+
203
+ if (!startDate) startDate = answers.startDate;
204
+ if (!endDate) endDate = answers.endDate;
205
+ }
206
+
207
+ // Append time to dates if missing (Jira requires ISO with time)
208
+ const formatISO = (dateStr: string) => {
209
+ return dateStr.includes('T') ? dateStr : `${dateStr}T10:00:00.000+0000`;
210
+ };
211
+
212
+ const updateSpinner = ora('Starting sprint...').start();
213
+ await api.agilePost(`/sprint/${sprintId}`, {
214
+ state: 'active',
215
+ startDate: formatISO(startDate),
216
+ endDate: formatISO(endDate)
217
+ });
218
+ updateSpinner.succeed(chalk.green(`Sprint "${chalk.bold(sprint.name)}" is now ACTIVE.`));
219
+
220
+ } catch (e: any) {
221
+ handleCommandError(spinner, e, 'Failed to start sprint');
222
+ }
223
+ });
224
+
225
+ // ── COMPLETE SPRINT ───────────────────────────────────────────────
226
+ sprintCmd
227
+ .command('complete')
228
+ .description('Complete (close) an active sprint')
229
+ .argument('<sprintId>', 'Sprint ID')
230
+ .action(async (sprintId) => {
231
+ const spinner = ora(`Fetching sprint ${sprintId}...`).start();
232
+ try {
233
+ const sprint = await api.agileGet(`/sprint/${sprintId}`);
234
+
235
+ if (sprint.state !== 'active') {
236
+ spinner.fail(`Sprint "${sprint.name}" is not active (State: ${sprint.state}).`);
237
+ return;
238
+ }
239
+ spinner.stop();
240
+
241
+ const { confirmed } = await enquirer.prompt({
242
+ type: 'confirm',
243
+ name: 'confirmed',
244
+ message: `Are you sure you want to complete sprint "${chalk.cyan(sprint.name)}"?`,
245
+ initial: false
246
+ }) as any;
247
+
248
+ if (!confirmed) return;
249
+
250
+ const closeSpinner = ora('Completing sprint...').start();
251
+ // Note: If there are incomplete issues, Jira API might error or require specific handling (swap).
252
+ // For simplified flow, we try basic close. If it fails, we inform user.
253
+ await api.agilePost(`/sprint/${sprintId}`, {
254
+ state: 'closed'
255
+ });
256
+ closeSpinner.succeed(chalk.green(`Sprint "${chalk.bold(sprint.name)}" completed.`));
257
+
258
+ } catch (e: any) {
259
+ if (e.response && e.response.status === 400) {
260
+ // Check if it mentions incomplete issues
261
+ handleCommandError(spinner, e, 'Failed to complete sprint (Check if there are incomplete issues that need moving)');
262
+ } else {
263
+ handleCommandError(spinner, e, 'Failed to complete sprint');
264
+ }
265
+ }
266
+ });
267
+
268
+ program.addCommand(sprintCmd);
269
+ }
@@ -60,6 +60,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
60
60
  required: ["projectKey", "summary"]
61
61
  }
62
62
  },
63
+ {
64
+ name: "jira_update_issue",
65
+ description: "Update an existing Jira issue. Supports updating summary, description, priority, and assignee.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ issueKey: { type: "string", description: "Issue Key (e.g., PROJ-123)" },
70
+ summary: { type: "string", description: "New summary" },
71
+ description: { type: "string", description: "New description (plain text)" },
72
+ priority: { type: "string", description: "New priority name" },
73
+ assigneeId: { type: "string", description: "New assignee account ID (or 'me', 'none')" }
74
+ },
75
+ required: ["issueKey"]
76
+ }
77
+ },
63
78
  {
64
79
  name: "jira_transition_issue",
65
80
  description: "Transition a Jira issue to a new status. First call with only issueKey to see available transitions, then call again with the transitionId.",
@@ -97,6 +112,27 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
97
112
  }
98
113
  },
99
114
 
115
+ // ── Users ───────────────────────────────────────
116
+ {
117
+ name: "jira_search_users",
118
+ description: "Search for Jira users by name or email. Returns accountId and displayName.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ query: { type: "string", description: "Name, email, or part of it" }
123
+ },
124
+ required: ["query"]
125
+ }
126
+ },
127
+ {
128
+ name: "jira_myself",
129
+ description: "Get details about the current authenticated user.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {}
133
+ }
134
+ },
135
+
100
136
  // ── Projects & Sprints ──────────────────────────
101
137
  {
102
138
  name: "jira_list_projects",
@@ -119,6 +155,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
119
155
  },
120
156
  required: ["boardId"]
121
157
  }
158
+ },
159
+ {
160
+ name: "jira_add_worklog",
161
+ description: "Log work to a Jira issue.",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {
165
+ issueKey: { type: "string", description: "Issue Key" },
166
+ timeSpent: { type: "string", description: "Time spent (e.g., '2h 30m', '1d')" },
167
+ comment: { type: "string", description: "Worklog comment" }
168
+ },
169
+ required: ["issueKey", "timeSpent"]
170
+ }
171
+ },
172
+ {
173
+ name: "jira_create_subtask",
174
+ description: "Create a subtask for a parent issue.",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ parentKey: { type: "string", description: "Parent Issue Key" },
179
+ summary: { type: "string", description: "Subtask summary" },
180
+ description: { type: "string", description: "Subtask description" },
181
+ priority: { type: "string", description: "Priority name" },
182
+ assigneeId: { type: "string", description: "Assignee Account ID" }
183
+ },
184
+ required: ["parentKey", "summary"]
185
+ }
186
+ },
187
+ {
188
+ name: "jira_add_attachment",
189
+ description: "Attach a file to a Jira issue.",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ issueKey: { type: "string", description: "Issue Key" },
194
+ filePath: { type: "string", description: "Absolute path to the file to extract/upload" }
195
+ },
196
+ required: ["issueKey", "filePath"]
197
+ }
122
198
  }
123
199
  ]
124
200
  };
@@ -126,7 +202,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
126
202
 
127
203
  // ── Tool Handlers ────────────────────────────────────────────────────
128
204
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
129
- const { name, arguments: args } = request.params;
205
+ const { name, arguments: args } = request.params as { name: string; arguments: any };
130
206
 
131
207
  try {
132
208
  // ── jira_list_issues ────────────────────────────────
@@ -140,7 +216,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
140
216
  });
141
217
 
142
218
  // Return a cleaner format for LLM consumption
143
- const issues = (data.issues || []).map(i => ({
219
+ const issues = (data.issues || []).map((i: any) => ({
144
220
  key: i.key,
145
221
  summary: i.fields.summary,
146
222
  status: i.fields.status?.name,
@@ -173,7 +249,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
173
249
  updated: data.fields.updated,
174
250
  description: data.fields.description,
175
251
  labels: data.fields.labels,
176
- comments: data.fields.comment?.comments?.map(c => ({
252
+ comments: data.fields.comment?.comments?.map((c: any) => ({
177
253
  author: c.author.displayName,
178
254
  body: c.body,
179
255
  created: c.created
@@ -187,7 +263,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
187
263
 
188
264
  // ── jira_create_issue ───────────────────────────────
189
265
  if (name === "jira_create_issue") {
190
- const body = {
266
+ const body: any = {
191
267
  fields: {
192
268
  project: { key: args.projectKey },
193
269
  summary: args.summary,
@@ -225,7 +301,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
225
301
  issueKey: args.issueKey,
226
302
  summary: issue.fields.summary,
227
303
  currentStatus: issue.fields.status?.name,
228
- availableTransitions: (transData.transitions || []).map(t => ({
304
+ availableTransitions: (transData.transitions || []).map((t: any) => ({
229
305
  id: t.id,
230
306
  name: t.name,
231
307
  toStatus: t.to.name
@@ -277,12 +353,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
277
353
  };
278
354
  }
279
355
 
356
+ // ── jira_update_issue ───────────────────────────────
357
+ if (name === "jira_update_issue") {
358
+ const updateBody: any = { fields: {} };
359
+
360
+ if (args.summary) updateBody.fields.summary = args.summary;
361
+ if (args.description) updateBody.fields.description = textToADF(args.description);
362
+ if (args.priority) updateBody.fields.priority = { name: args.priority };
363
+
364
+ if (args.assigneeId) {
365
+ let accId = args.assigneeId;
366
+ if (accId === 'me') {
367
+ const myself = await api.get('/myself');
368
+ accId = myself.accountId;
369
+ } else if (accId === 'none') {
370
+ accId = null;
371
+ }
372
+ updateBody.fields.assignee = { accountId: accId };
373
+ }
374
+
375
+ if (Object.keys(updateBody.fields).length === 0) {
376
+ return {
377
+ content: [{ type: "text", text: "No fields to update provided." }],
378
+ isError: true
379
+ };
380
+ }
381
+
382
+ await api.put(`/issue/${args.issueKey}`, updateBody);
383
+
384
+ return {
385
+ content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey }) }]
386
+ };
387
+ }
388
+
389
+ // ── jira_search_users ───────────────────────────────
390
+ if (name === "jira_search_users") {
391
+ const users = await api.get(`/user/search?query=${encodeURIComponent(args.query)}`);
392
+
393
+ const results = (users || []).map((u: any) => ({
394
+ accountId: u.accountId,
395
+ displayName: u.displayName,
396
+ email: u.emailAddress,
397
+ active: u.active
398
+ }));
399
+
400
+ return {
401
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
402
+ };
403
+ }
404
+
405
+ // ── jira_myself ─────────────────────────────────────
406
+ if (name === "jira_myself") {
407
+ const myself = await api.get('/myself');
408
+ const result = {
409
+ accountId: myself.accountId,
410
+ displayName: myself.displayName,
411
+ email: myself.emailAddress,
412
+ active: myself.active,
413
+ timeZone: myself.timeZone
414
+ };
415
+
416
+ return {
417
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
418
+ };
419
+ }
420
+
280
421
  // ── jira_list_projects ──────────────────────────────
281
422
  if (name === "jira_list_projects") {
282
423
  const limit = args.limit || 50;
283
424
  const data = await api.get(`/project/search?maxResults=${limit}`);
284
425
 
285
- const projects = (data.values || []).map(p => ({
426
+ const projects = (data.values || []).map((p: any) => ({
286
427
  key: p.key,
287
428
  name: p.name,
288
429
  lead: p.lead?.displayName || 'N/A',
@@ -300,7 +441,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
300
441
  const state = args.state || 'active,future';
301
442
  const data = await api.agileGet(`/board/${args.boardId}/sprint?state=${state}`);
302
443
 
303
- const sprints = (data.values || []).map(s => ({
444
+ const sprints = (data.values || []).map((s: any) => ({
304
445
  id: s.id,
305
446
  name: s.name,
306
447
  state: s.state,
@@ -314,9 +455,95 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
314
455
  };
315
456
  }
316
457
 
458
+ // ── jira_add_worklog ────────────────────────────────
459
+ if (name === "jira_add_worklog") {
460
+ const body: any = {
461
+ timeSpent: args.timeSpent
462
+ };
463
+ if (args.comment) {
464
+ body.comment = textToADF(args.comment);
465
+ }
466
+
467
+ await api.post(`/issue/${args.issueKey}/worklog`, body);
468
+
469
+ return {
470
+ content: [{ type: "text", text: JSON.stringify({ success: true, issueKey: args.issueKey, timeSpent: args.timeSpent }) }]
471
+ };
472
+ }
473
+
474
+ // ── jira_create_subtask ─────────────────────────────
475
+ if (name === "jira_create_subtask") {
476
+ // 1. Fetch parent to get project
477
+ const parent = await api.get(`/issue/${args.parentKey}?fields=project`);
478
+ const projectKey = parent.fields.project.key;
479
+
480
+ // 2. Find subtask issue type
481
+ const meta = await api.get(`/issue/createmeta/${projectKey}/issuetypes`);
482
+ const allTypes = meta.issueTypes || meta.values || [];
483
+ const subtaskTypes = allTypes.filter((t: any) => t.subtask);
484
+
485
+ if (subtaskTypes.length === 0) {
486
+ return {
487
+ content: [{ type: "text", text: `Error: No subtask types found in project ${projectKey}` }],
488
+ isError: true
489
+ };
490
+ }
491
+ const subtaskId = subtaskTypes[0].id; // Default to first available
492
+
493
+ const body: any = {
494
+ fields: {
495
+ project: { key: projectKey },
496
+ parent: { key: args.parentKey },
497
+ issuetype: { id: subtaskId },
498
+ summary: args.summary
499
+ }
500
+ };
501
+
502
+ if (args.description) body.fields.description = textToADF(args.description);
503
+ if (args.priority) body.fields.priority = { name: args.priority };
504
+ if (args.assigneeId) {
505
+ let accId = args.assigneeId;
506
+ if (accId === 'me') {
507
+ const myself = await api.get('/myself');
508
+ accId = myself.accountId;
509
+ }
510
+ body.fields.assignee = { accountId: accId };
511
+ }
512
+
513
+ const data = await api.post('/issue', body);
514
+ return {
515
+ content: [{ type: "text", text: JSON.stringify({ key: data.key, self: data.self }, null, 2) }]
516
+ };
517
+ }
518
+
519
+ // ── jira_add_attachment ─────────────────────────────
520
+ if (name === "jira_add_attachment") {
521
+ try {
522
+ // Dynamically import fs/path to avoid top-level node dependencies if this runs in browser-like env (unlikely but safe)
523
+ const { openAsBlob } = await import('node:fs');
524
+ const path = await import('node:path');
525
+
526
+ const filePath = args.filePath;
527
+ const file = await openAsBlob(filePath);
528
+ const formData = new FormData();
529
+ formData.append('file', file, path.default.basename(filePath));
530
+
531
+ const result = await api.upload(`/issue/${args.issueKey}/attachments`, formData);
532
+
533
+ return {
534
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
535
+ };
536
+ } catch (e: any) {
537
+ return {
538
+ content: [{ type: "text", text: `Error attaching file: ${e.message}` }],
539
+ isError: true
540
+ };
541
+ }
542
+ }
543
+
317
544
  throw new Error(`Unknown tool: ${name}`);
318
545
 
319
- } catch (e) {
546
+ } catch (e: any) {
320
547
  const errorMessage = e.response?.data ? JSON.stringify(e.response.data) : e.message;
321
548
  return {
322
549
  content: [{ type: "text", text: `Error: ${errorMessage}` }],