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
@@ -4,10 +4,10 @@ import { getCredentials } from '../utils/config.js';
4
4
  export class AiService {
5
5
  constructor() { }
6
6
 
7
- async generate(prompt) {
8
- const { aiKey, aiProvider, aiEnabled } = getCredentials();
7
+ async generate(prompt: string): Promise<string> {
8
+ const { aiKey, aiProvider, enableAi } = getCredentials();
9
9
 
10
- if (!aiEnabled) {
10
+ if (!enableAi) {
11
11
  throw new Error('AI features are disabled. Run "jira config ai enable" to enable.');
12
12
  }
13
13
 
@@ -29,7 +29,7 @@ export class AiService {
29
29
  }
30
30
  }
31
31
 
32
- async reviewCode(diff, context) {
32
+ async reviewCode(diff: string, context: string): Promise<string> {
33
33
  const prompt = `Review the following code changes against the issue context.
34
34
  Issue Context: ${context}
35
35
 
@@ -47,7 +47,7 @@ Provide a concise code review. Focus on:
47
47
  return this.generate(prompt);
48
48
  }
49
49
 
50
- async breakdownEpic(summary, description) {
50
+ async breakdownEpic(summary: string, description: string): Promise<any> {
51
51
  const prompt = `Break down the following Epic into child Stories and Tasks.
52
52
  Epic Summary: ${summary}
53
53
  Epic Description: ${description || 'N/A'}
@@ -60,7 +60,7 @@ Example: [{"type": "Story", "summary": "...", "description": "..."}]`;
60
60
  return JSON.parse(jsonStr);
61
61
  }
62
62
 
63
- async generateStandup(yesterday, today) {
63
+ async generateStandup(yesterday: string, today: string): Promise<string> {
64
64
  const prompt = `Generate a daily standup update based on my Jira activity.
65
65
  Activity (Last 24h):
66
66
  ${yesterday}
@@ -77,7 +77,7 @@ Keep it concise and professional.`;
77
77
  return this.generate(prompt);
78
78
  }
79
79
 
80
- async generateJql(query) {
80
+ async generateJql(query: string): Promise<string> {
81
81
  const prompt = `Convert the following natural language query into Jira JQL.
82
82
  Query: "${query}"
83
83
 
@@ -87,7 +87,7 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
87
87
  return response.replace(/```/g, '').trim();
88
88
  }
89
89
 
90
- async callOpenAI(key, prompt) {
90
+ async callOpenAI(key: string, prompt: string): Promise<string> {
91
91
  try {
92
92
  const response = await axios.post('https://api.openai.com/v1/chat/completions', {
93
93
  model: 'gpt-4o',
@@ -97,12 +97,12 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
97
97
  headers: { 'Authorization': `Bearer ${key}` }
98
98
  });
99
99
  return response.data.choices[0].message.content;
100
- } catch (e) {
100
+ } catch (e: any) {
101
101
  throw new Error(`OpenAI API Error: ${e.response?.data?.error?.message || e.message}`);
102
102
  }
103
103
  }
104
104
 
105
- async callGemini(key, prompt) {
105
+ async callGemini(key: string, prompt: string): Promise<string> {
106
106
  try {
107
107
  // Gemini REST API — uses generativelanguage.googleapis.com
108
108
  const model = 'gemini-2.0-flash';
@@ -125,8 +125,8 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
125
125
  throw new Error('No response generated by Gemini.');
126
126
  }
127
127
 
128
- return candidates[0].content.parts.map(p => p.text).join('');
129
- } catch (e) {
128
+ return candidates[0].content.parts.map((p: any) => p.text).join('');
129
+ } catch (e: any) {
130
130
  if (e.response?.data?.error) {
131
131
  throw new Error(`Gemini API Error: ${e.response.data.error.message}`);
132
132
  }
@@ -134,7 +134,7 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
134
134
  }
135
135
  }
136
136
 
137
- async callAnthropic(key, prompt) {
137
+ async callAnthropic(key: string, prompt: string): Promise<string> {
138
138
  try {
139
139
  // Anthropic Messages API
140
140
  const response = await axios.post('https://api.anthropic.com/v1/messages', {
@@ -150,10 +150,10 @@ Today is ${new Date().toISOString().split('T')[0]}.`;
150
150
  });
151
151
 
152
152
  return response.data.content
153
- .filter(block => block.type === 'text')
154
- .map(block => block.text)
153
+ .filter((block: any) => block.type === 'text')
154
+ .map((block: any) => block.text)
155
155
  .join('');
156
- } catch (e) {
156
+ } catch (e: any) {
157
157
  if (e.response?.data?.error) {
158
158
  throw new Error(`Anthropic API Error: ${e.response.data.error.message}`);
159
159
  }
@@ -3,10 +3,16 @@ import chalk from 'chalk';
3
3
  import { getCredentials } from '../utils/config.js';
4
4
 
5
5
  export class ApiService {
6
+ private client: any;
7
+ private agileClient: any;
8
+ private _domain: string | null = null;
9
+
6
10
  constructor() {
7
11
  this.init();
8
12
  }
9
13
 
14
+
15
+
10
16
  init() {
11
17
  const { jiraUrl, email, apiToken } = getCredentials();
12
18
 
@@ -42,7 +48,7 @@ export class ApiService {
42
48
  });
43
49
 
44
50
  // Shared response interceptor
45
- const errorInterceptor = (error) => {
51
+ const errorInterceptor = (error: any) => {
46
52
  if (error.response) {
47
53
  if (error.response.status === 401) {
48
54
  console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
@@ -53,8 +59,8 @@ export class ApiService {
53
59
  return Promise.reject(error);
54
60
  };
55
61
 
56
- this.client.interceptors.response.use(r => r, errorInterceptor);
57
- this.agileClient.interceptors.response.use(r => r, errorInterceptor);
62
+ this.client.interceptors.response.use((r: any) => r, errorInterceptor);
63
+ this.agileClient.interceptors.response.use((r: any) => r, errorInterceptor);
58
64
  }
59
65
 
60
66
  /** @returns {string} The Jira domain URL */
@@ -73,39 +79,57 @@ export class ApiService {
73
79
 
74
80
  // ── Standard REST API v3 Methods ────────────────────────────────
75
81
 
76
- async get(url, config = {}) {
82
+ async get(url: string, config: any = {}) {
77
83
  this.ensureClient();
78
84
  const response = await this.client.get(url, config);
79
85
  return response.data;
80
86
  }
81
87
 
82
- async post(url, data, config = {}) {
88
+ async post(url: string, data: any, config: any = {}) {
83
89
  this.ensureClient();
84
90
  const response = await this.client.post(url, data, config);
85
91
  return response.data;
86
92
  }
87
93
 
88
- async put(url, data, config = {}) {
94
+ async put(url: string, data: any, config: any = {}) {
89
95
  this.ensureClient();
90
96
  const response = await this.client.put(url, data, config);
91
97
  return response.data;
92
98
  }
93
99
 
94
- async delete(url, config = {}) {
100
+ async delete(url: string, config: any = {}) {
95
101
  this.ensureClient();
96
102
  const response = await this.client.delete(url, config);
97
103
  return response.data;
98
104
  }
99
105
 
106
+ async upload(url: string, formData: any) {
107
+ this.ensureClient();
108
+ // Jira requires this header for attachments
109
+ const headers: any = {
110
+ 'X-Atlassian-Token': 'no-check'
111
+ };
112
+
113
+ // If using 'form-data' package, it has getHeaders().
114
+ // If using native FormData, axios/adapter handles Content-Type + boundary.
115
+ if (formData.getHeaders) {
116
+ Object.assign(headers, formData.getHeaders());
117
+ }
118
+
119
+ const config = { headers };
120
+ const response = await this.client.post(url, formData, config);
121
+ return response.data;
122
+ }
123
+
100
124
  // ── Agile REST API v1 Methods ───────────────────────────────────
101
125
 
102
- async agileGet(url, config = {}) {
126
+ async agileGet(url: string, config: any = {}) {
103
127
  this.ensureClient();
104
128
  const response = await this.agileClient.get(url, config);
105
129
  return response.data;
106
130
  }
107
131
 
108
- async agilePost(url, data, config = {}) {
132
+ async agilePost(url: string, data: any, config: any = {}) {
109
133
  this.ensureClient();
110
134
  const response = await this.agileClient.post(url, data, config);
111
135
  return response.data;
@@ -0,0 +1,21 @@
1
+ import ConfigStore from '../utils/config-store.js';
2
+
3
+ const config = new ConfigStore('jira-pilot');
4
+
5
+ export class ConfigService {
6
+ static getConfig() {
7
+ return config.all;
8
+ }
9
+
10
+ static saveConfig(newConfig: any) {
11
+ config.all = newConfig;
12
+ }
13
+
14
+ static get(key: string) {
15
+ return config.get(key);
16
+ }
17
+
18
+ static set(key: string, value: any) {
19
+ config.set(key, value);
20
+ }
21
+ }
package/src/types.ts ADDED
@@ -0,0 +1,68 @@
1
+ export interface JiraIssue {
2
+ key: string;
3
+ fields: {
4
+ summary: string;
5
+ description?: any; // ADF
6
+ status: { name: string; id?: string };
7
+ priority?: { name: string; id?: string };
8
+ assignee?: { accountId: string; displayName: string; emailAddress?: string };
9
+ issuetype: { name: string; id?: string; subtask?: boolean };
10
+ created: string;
11
+ updated: string;
12
+ comment?: {
13
+ comments: Array<{
14
+ id: string;
15
+ body: string;
16
+ author: { displayName: string; accountId: string };
17
+ created: string;
18
+ }>;
19
+ total: number;
20
+ };
21
+ [key: string]: any;
22
+ };
23
+ }
24
+
25
+ export interface JiraProject {
26
+ key: string;
27
+ name: string;
28
+ id: string;
29
+ lead?: { displayName: string };
30
+ style?: string; // classic / next-gen
31
+ issueTypes?: Array<{ name: string; subtask?: boolean; id: string }>;
32
+ }
33
+
34
+ export interface JiraSprint {
35
+ id: number;
36
+ name: string;
37
+ state: 'active' | 'future' | 'closed';
38
+ startDate?: string;
39
+ endDate?: string;
40
+ originBoardId?: number;
41
+ }
42
+
43
+ export interface JiraBoard {
44
+ id: number;
45
+ name: string;
46
+ type: string;
47
+ location?: { projectId: number; displayName: string; projectKey: string };
48
+ }
49
+
50
+ export interface ConfigProfile {
51
+ jiraUrl: string;
52
+ email: string;
53
+ apiToken: string;
54
+ aiEnabled?: boolean; // Unified key
55
+ enableAi?: boolean; // Legacy key support
56
+ aiProvider?: 'openai' | 'gemini' | 'anthropic';
57
+ aiKey?: string; // Legacy
58
+ aiConfig?: {
59
+ apiKey?: string;
60
+ model?: string;
61
+ };
62
+ githubToken?: string;
63
+ }
64
+
65
+ export interface GlobalConfig {
66
+ profiles: Record<string, ConfigProfile>;
67
+ currentProfile?: string;
68
+ }
@@ -1,15 +1,15 @@
1
- export function parseADF(content) {
1
+ export function parseADF(content: any) {
2
2
  if (!content) return '';
3
3
  if (typeof content === 'string') return content;
4
4
 
5
5
  if (content.type === 'doc') {
6
- return content.content.map(node => parseNode(node)).join('\n');
6
+ return content.content.map((node: any) => parseNode(node)).join('\n');
7
7
  }
8
8
 
9
9
  return JSON.stringify(content);
10
10
  }
11
11
 
12
- function parseNode(node) {
12
+ function parseNode(node: any): string {
13
13
  if (!node) return '';
14
14
 
15
15
  switch (node.type) {
@@ -22,28 +22,28 @@ function parseNode(node) {
22
22
  case 'orderedList':
23
23
  return parseList(node, '1.');
24
24
  case 'heading':
25
- return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map(c => parseNode(c)).join('')}\n`;
25
+ return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map((c: any) => parseNode(c)).join('')}\n`;
26
26
  case 'codeBlock':
27
- return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map(c => c.text).join('')}\n\`\`\`\n`;
27
+ return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map((c: any) => c.text).join('')}\n\`\`\`\n`;
28
28
  case 'blockquote':
29
- return `> ${node.content.map(c => parseNode(c)).join('')}`;
29
+ return `> ${node.content.map((c: any) => parseNode(c)).join('')}`;
30
30
  default:
31
31
  if (node.content) {
32
- return node.content.map(c => parseNode(c)).join('');
32
+ return node.content.map((c: any) => parseNode(c)).join('');
33
33
  }
34
34
  return ''; // Unknown node, skip or fallback
35
35
  }
36
36
  }
37
37
 
38
- function parseParagraph(node) {
38
+ function parseParagraph(node: any) {
39
39
  if (!node.content) return '\n';
40
- return node.content.map(c => parseNode(c)).join('') + '\n';
40
+ return node.content.map((c: any) => parseNode(c)).join('') + '\n';
41
41
  }
42
42
 
43
- function parseList(node, marker) {
43
+ function parseList(node: any, marker: string) {
44
44
  if (!node.content) return '';
45
- return node.content.map((item, index) => {
45
+ return node.content.map((item: any, index: number) => {
46
46
  const prefix = marker === '1.' ? `${index + 1}. ` : `${marker} `;
47
- return `${prefix}${item.content.map(c => parseNode(c)).join('')}`;
47
+ return `${prefix}${item.content.map((c: any) => parseNode(c)).join('')}`;
48
48
  }).join('\n') + '\n';
49
49
  }
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ function getAppDataPath() {
6
+ if (process.platform === 'win32') {
7
+ return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
8
+ }
9
+ if (process.platform === 'darwin') {
10
+ return path.join(os.homedir(), 'Library', 'Preferences');
11
+ }
12
+ return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
13
+ }
14
+
15
+ class ConfigStore {
16
+ private dir: string;
17
+ private path: string;
18
+
19
+ constructor(projectName: string) {
20
+ this.dir = path.join(getAppDataPath(), projectName);
21
+ this.path = path.join(this.dir, 'config.json');
22
+ this.ensureDir();
23
+ }
24
+
25
+ ensureDir() {
26
+ if (!fs.existsSync(this.dir)) {
27
+ fs.mkdirSync(this.dir, { recursive: true });
28
+ }
29
+ }
30
+
31
+ load() {
32
+ try {
33
+ if (!fs.existsSync(this.path)) return {};
34
+ return JSON.parse(fs.readFileSync(this.path, 'utf8'));
35
+ } catch (e) {
36
+ return {};
37
+ }
38
+ }
39
+
40
+ save(data: any) {
41
+ this.ensureDir();
42
+ fs.writeFileSync(this.path, JSON.stringify(data, null, 4), 'utf8');
43
+ }
44
+
45
+ get(key?: string): any {
46
+ const data = this.load();
47
+ if (!key) return data;
48
+
49
+ // Support dot notation: profiles.work
50
+ const parts = key.split('.');
51
+ let current = data;
52
+ for (const part of parts) {
53
+ if (current === undefined || current === null) return undefined;
54
+ current = current[part];
55
+ }
56
+ return current;
57
+ }
58
+
59
+ set(key: string, value: any): void {
60
+ const data = this.load();
61
+
62
+ const parts = key.split('.');
63
+ const last = parts.pop();
64
+ if (!last) return;
65
+
66
+ let current = data;
67
+ for (const part of parts) {
68
+ if (typeof current[part] !== 'object' || current[part] === null) {
69
+ current[part] = {};
70
+ }
71
+ current = current[part];
72
+ }
73
+
74
+ current[last] = value;
75
+ this.save(data);
76
+ }
77
+
78
+ delete(key: string): void {
79
+ const data = this.load();
80
+ const parts = key.split('.');
81
+ const last = parts.pop();
82
+ if (!last) return; // Should not happen
83
+
84
+ let current = data;
85
+ for (const part of parts) {
86
+ if (current === undefined || current === null) return; // Key doesn't exist
87
+ current = current[part];
88
+ }
89
+
90
+ if (current && typeof current === 'object') {
91
+ delete current[last];
92
+ this.save(data);
93
+ }
94
+ }
95
+
96
+ clear() {
97
+ this.save({});
98
+ }
99
+
100
+ get all() {
101
+ return this.load();
102
+ }
103
+
104
+ set all(data: any) {
105
+ this.save(data);
106
+ }
107
+ }
108
+
109
+ export default ConfigStore;
@@ -1,57 +1,30 @@
1
- import Conf from 'conf';
1
+ import ConfigStore from './config-store.js';
2
+ import { ConfigProfile } from '../types.js';
2
3
 
3
- const schema = {
4
- jiraUrl: {
5
- type: 'string',
6
- format: 'url'
7
- },
8
- email: {
9
- type: 'string',
10
- format: 'email'
11
- },
12
- apiToken: {
13
- type: 'string'
14
- },
15
- aiKey: {
16
- type: 'string'
17
- },
18
- aiProvider: {
19
- type: 'string',
20
- default: 'openai'
21
- },
22
- aiEnabled: {
23
- type: 'boolean',
24
- default: false
25
- },
26
- githubToken: {
27
- type: 'string'
28
- }
29
- };
4
+ const config = new ConfigStore('jira-pilot');
30
5
 
31
- const config = new Conf({
32
- projectName: 'jira-pilot',
33
- schema
34
- });
35
-
36
- export const getCredentials = () => {
6
+ export const getCredentials = (): Partial<ConfigProfile> => {
37
7
  return {
38
8
  jiraUrl: config.get('jiraUrl'),
39
9
  email: config.get('email'),
40
10
  apiToken: config.get('apiToken'),
41
11
  aiKey: config.get('aiKey'),
42
12
  aiProvider: config.get('aiProvider'),
43
- aiEnabled: config.get('aiEnabled'),
13
+ enableAi: config.get('aiEnabled'), // Map legacy key
44
14
  githubToken: config.get('githubToken')
45
15
  };
46
16
  };
47
17
 
48
- export const setCredentials = ({ jiraUrl, email, apiToken, aiKey, aiProvider, aiEnabled, githubToken }) => {
18
+ export const setCredentials = ({ jiraUrl, email, apiToken, aiConfig, aiProvider, enableAi, githubToken }: Partial<ConfigProfile>) => {
49
19
  if (jiraUrl) config.set('jiraUrl', jiraUrl);
50
20
  if (email) config.set('email', email);
51
21
  if (apiToken) config.set('apiToken', apiToken);
52
- if (aiKey) config.set('aiKey', aiKey);
22
+ if (aiConfig) {
23
+ if (aiConfig.apiKey) config.set('aiKey', aiConfig.apiKey);
24
+ config.set('aiConfig', aiConfig);
25
+ }
53
26
  if (aiProvider) config.set('aiProvider', aiProvider);
54
- if (typeof aiEnabled !== 'undefined') config.set('aiEnabled', aiEnabled);
27
+ if (typeof enableAi !== 'undefined') config.set('enableAi', enableAi);
55
28
  if (githubToken) config.set('githubToken', githubToken);
56
29
  };
57
30
 
@@ -66,13 +39,13 @@ export const hasCredentials = () => {
66
39
 
67
40
  // ── Profile Management ──────────────────────────────────────────────
68
41
 
69
- export const saveProfile = (name) => {
42
+ export const saveProfile = (name: string): void => {
70
43
  const creds = getCredentials();
71
44
  config.set(`profiles.${name}`, creds);
72
45
  config.set('activeProfile', name);
73
46
  };
74
47
 
75
- export const loadProfile = (name) => {
48
+ export const loadProfile = (name: string): boolean => {
76
49
  const profile = config.get(`profiles.${name}`);
77
50
  if (!profile) return false;
78
51
  setCredentials(profile);
@@ -80,7 +53,7 @@ export const loadProfile = (name) => {
80
53
  return true;
81
54
  };
82
55
 
83
- export const deleteProfile = (name) => {
56
+ export const deleteProfile = (name: string): void => {
84
57
  config.delete(`profiles.${name}`);
85
58
  if (config.get('activeProfile') === name) {
86
59
  config.delete('activeProfile');
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { Ora } from 'ora';
2
3
 
3
4
  /**
4
5
  * Standardized error handler for CLI commands.
@@ -8,7 +9,7 @@ import chalk from 'chalk';
8
9
  * @param {Error} error - The error object
9
10
  * @param {string} [context] - Optional context (e.g., "Failed to list issues")
10
11
  */
11
- export function handleCommandError(spinner, error, context = 'Operation failed') {
12
+ export function handleCommandError(spinner: Ora | null, error: any, context = 'Operation failed') {
12
13
  // Handle user cancellation (Ctrl+C in enquirer)
13
14
  if (error === '' || (error && error.message === '')) {
14
15
  if (spinner) spinner.stop();
@@ -5,7 +5,7 @@
5
5
  * @param {string} text - Plain text string
6
6
  * @returns {object} ADF document node
7
7
  */
8
- export function textToADF(text) {
8
+ export function textToADF(text: any) {
9
9
  if (!text || typeof text !== 'string') {
10
10
  return {
11
11
  type: 'doc',
@@ -8,7 +8,7 @@
8
8
  * @param {string} key - The issue key to validate.
9
9
  * @returns {{ valid: boolean, message?: string }}
10
10
  */
11
- export function validateIssueKey(key) {
11
+ export function validateIssueKey(key: any): { valid: boolean; message?: string } {
12
12
  if (!key || typeof key !== 'string') {
13
13
  return { valid: false, message: 'Issue key is required.' };
14
14
  }
@@ -31,7 +31,7 @@ export function validateIssueKey(key) {
31
31
  * @param {string} key - The project key to validate.
32
32
  * @returns {{ valid: boolean, message?: string }}
33
33
  */
34
- export function validateProjectKey(key) {
34
+ export function validateProjectKey(key: any): { valid: boolean; message?: string } {
35
35
  if (!key || typeof key !== 'string') {
36
36
  return { valid: false, message: 'Project key is required.' };
37
37
  }
@@ -54,7 +54,7 @@ export function validateProjectKey(key) {
54
54
  * @param {string} url - The URL to validate.
55
55
  * @returns {{ valid: boolean, message?: string }}
56
56
  */
57
- export function validateUrl(url) {
57
+ export function validateUrl(url: any): { valid: boolean; message?: string } {
58
58
  if (!url || typeof url !== 'string') {
59
59
  return { valid: false, message: 'URL is required.' };
60
60
  }
@@ -78,7 +78,7 @@ export function validateUrl(url) {
78
78
  * @param {string} jql - The JQL query string.
79
79
  * @returns {string} The sanitized JQL string.
80
80
  */
81
- export function sanitizeJql(jql) {
81
+ export function sanitizeJql(jql: any): string {
82
82
  if (!jql || typeof jql !== 'string') {
83
83
  return '';
84
84
  }