jira-pilot 2.0.1 → 2.0.2

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.
@@ -1,107 +1,165 @@
1
- import axios from 'axios';
2
- import { getCredentials } from '../utils/config.js';
3
-
4
- export class AiService {
5
- constructor() { }
6
-
7
- async generate(prompt) {
8
- const { aiKey, aiProvider, aiEnabled } = getCredentials();
9
-
10
- if (!aiEnabled) {
11
- throw new Error('AI features are disabled. Run "jira config ai enable" to enable.');
12
- }
13
-
14
- if (!aiKey) {
15
- throw new Error('AI API Key not configured. Run "jira config ai enable" or "jira config setup".');
16
- }
17
-
18
- const provider = aiProvider || 'openai';
19
-
20
- switch (provider) {
21
- case 'openai':
22
- return this.callOpenAI(aiKey, prompt);
23
- case 'gemini':
24
- return this.callGemini(aiKey, prompt);
25
- case 'anthropic':
26
- return this.callAnthropic(aiKey, prompt);
27
- default:
28
- throw new Error(`Unsupported AI Provider: ${provider}. Supported: openai, gemini, anthropic`);
29
- }
30
- }
31
-
32
- async callOpenAI(key, prompt) {
33
- try {
34
- const response = await axios.post('https://api.openai.com/v1/chat/completions', {
35
- model: 'gpt-4o',
36
- messages: [{ role: 'user', content: prompt }],
37
- temperature: 0.7
38
- }, {
39
- headers: { 'Authorization': `Bearer ${key}` }
40
- });
41
- return response.data.choices[0].message.content;
42
- } catch (e) {
43
- throw new Error(`OpenAI API Error: ${e.response?.data?.error?.message || e.message}`);
44
- }
45
- }
46
-
47
- async callGemini(key, prompt) {
48
- try {
49
- // Gemini REST API — uses generativelanguage.googleapis.com
50
- const model = 'gemini-2.0-flash';
51
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
52
-
53
- const response = await axios.post(url, {
54
- contents: [{
55
- parts: [{ text: prompt }]
56
- }],
57
- generationConfig: {
58
- temperature: 0.7,
59
- maxOutputTokens: 2048
60
- }
61
- }, {
62
- headers: { 'Content-Type': 'application/json' }
63
- });
64
-
65
- const candidates = response.data.candidates;
66
- if (!candidates || candidates.length === 0) {
67
- throw new Error('No response generated by Gemini.');
68
- }
69
-
70
- return candidates[0].content.parts.map(p => p.text).join('');
71
- } catch (e) {
72
- if (e.response?.data?.error) {
73
- throw new Error(`Gemini API Error: ${e.response.data.error.message}`);
74
- }
75
- throw new Error(`Gemini API Error: ${e.message}`);
76
- }
77
- }
78
-
79
- async callAnthropic(key, prompt) {
80
- try {
81
- // Anthropic Messages API
82
- const response = await axios.post('https://api.anthropic.com/v1/messages', {
83
- model: 'claude-sonnet-4-20250514',
84
- max_tokens: 2048,
85
- messages: [{ role: 'user', content: prompt }]
86
- }, {
87
- headers: {
88
- 'x-api-key': key,
89
- 'anthropic-version': '2023-06-01',
90
- 'Content-Type': 'application/json'
91
- }
92
- });
93
-
94
- return response.data.content
95
- .filter(block => block.type === 'text')
96
- .map(block => block.text)
97
- .join('');
98
- } catch (e) {
99
- if (e.response?.data?.error) {
100
- throw new Error(`Anthropic API Error: ${e.response.data.error.message}`);
101
- }
102
- throw new Error(`Anthropic API Error: ${e.message}`);
103
- }
104
- }
105
- }
106
-
107
- export const aiService = new AiService();
1
+ import axios from 'axios';
2
+ import { getCredentials } from '../utils/config.js';
3
+
4
+ export class AiService {
5
+ constructor() { }
6
+
7
+ async generate(prompt) {
8
+ const { aiKey, aiProvider, aiEnabled } = getCredentials();
9
+
10
+ if (!aiEnabled) {
11
+ throw new Error('AI features are disabled. Run "jira config ai enable" to enable.');
12
+ }
13
+
14
+ if (!aiKey) {
15
+ throw new Error('AI API Key not configured. Run "jira config ai enable" or "jira config setup".');
16
+ }
17
+
18
+ const provider = aiProvider || 'openai';
19
+
20
+ switch (provider) {
21
+ case 'openai':
22
+ return this.callOpenAI(aiKey, prompt);
23
+ case 'gemini':
24
+ return this.callGemini(aiKey, prompt);
25
+ case 'anthropic':
26
+ return this.callAnthropic(aiKey, prompt);
27
+ default:
28
+ throw new Error(`Unsupported AI Provider: ${provider}. Supported: openai, gemini, anthropic`);
29
+ }
30
+ }
31
+
32
+ async reviewCode(diff, context) {
33
+ const prompt = `Review the following code changes against the issue context.
34
+ Issue Context: ${context}
35
+
36
+ Code Diff:
37
+ \`\`\`diff
38
+ ${diff.substring(0, 10000)}
39
+ \`\`\`
40
+ (Diff truncated to 10k chars if longer)
41
+
42
+ Provide a concise code review. Focus on:
43
+ 1. Does it meet the issue requirements?
44
+ 2. Potential bugs or security issues.
45
+ 3. Code quality improvements.
46
+ 4. Verify if tests are included if applicable.`;
47
+ return this.generate(prompt);
48
+ }
49
+
50
+ async breakdownEpic(summary, description) {
51
+ const prompt = `Break down the following Epic into child Stories and Tasks.
52
+ Epic Summary: ${summary}
53
+ Epic Description: ${description || 'N/A'}
54
+
55
+ Return ONLY a valid JSON array of objects with "type" (Story, Task, Bug), "summary", and "description" fields.
56
+ Example: [{"type": "Story", "summary": "...", "description": "..."}]`;
57
+ const response = await this.generate(prompt);
58
+ // Clean markdown code blocks if present
59
+ const jsonStr = response.replace(/```json/g, '').replace(/```/g, '').trim();
60
+ return JSON.parse(jsonStr);
61
+ }
62
+
63
+ async generateStandup(yesterday, today) {
64
+ const prompt = `Generate a daily standup update based on my Jira activity.
65
+ Activity (Last 24h):
66
+ ${yesterday}
67
+
68
+ Current Assignments (Today):
69
+ ${today}
70
+
71
+ Format:
72
+ * **Yesterday**: (Completed items, progress made)
73
+ * **Today**: (Plan for today based on assignments)
74
+ * **Blockers**: (Identify potential blockers or ask if any)
75
+
76
+ Keep it concise and professional.`;
77
+ return this.generate(prompt);
78
+ }
79
+
80
+ async generateJql(query) {
81
+ const prompt = `Convert the following natural language query into Jira JQL.
82
+ Query: "${query}"
83
+
84
+ Return ONLY the raw JQL string. No markdown, no explanations.
85
+ Today is ${new Date().toISOString().split('T')[0]}.`;
86
+ const response = await this.generate(prompt);
87
+ return response.replace(/```/g, '').trim();
88
+ }
89
+
90
+ async callOpenAI(key, prompt) {
91
+ try {
92
+ const response = await axios.post('https://api.openai.com/v1/chat/completions', {
93
+ model: 'gpt-4o',
94
+ messages: [{ role: 'user', content: prompt }],
95
+ temperature: 0.7
96
+ }, {
97
+ headers: { 'Authorization': `Bearer ${key}` }
98
+ });
99
+ return response.data.choices[0].message.content;
100
+ } catch (e) {
101
+ throw new Error(`OpenAI API Error: ${e.response?.data?.error?.message || e.message}`);
102
+ }
103
+ }
104
+
105
+ async callGemini(key, prompt) {
106
+ try {
107
+ // Gemini REST API uses generativelanguage.googleapis.com
108
+ const model = 'gemini-2.0-flash';
109
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`;
110
+
111
+ const response = await axios.post(url, {
112
+ contents: [{
113
+ parts: [{ text: prompt }]
114
+ }],
115
+ generationConfig: {
116
+ temperature: 0.7,
117
+ maxOutputTokens: 2048
118
+ }
119
+ }, {
120
+ headers: { 'Content-Type': 'application/json' }
121
+ });
122
+
123
+ const candidates = response.data.candidates;
124
+ if (!candidates || candidates.length === 0) {
125
+ throw new Error('No response generated by Gemini.');
126
+ }
127
+
128
+ return candidates[0].content.parts.map(p => p.text).join('');
129
+ } catch (e) {
130
+ if (e.response?.data?.error) {
131
+ throw new Error(`Gemini API Error: ${e.response.data.error.message}`);
132
+ }
133
+ throw new Error(`Gemini API Error: ${e.message}`);
134
+ }
135
+ }
136
+
137
+ async callAnthropic(key, prompt) {
138
+ try {
139
+ // Anthropic Messages API
140
+ const response = await axios.post('https://api.anthropic.com/v1/messages', {
141
+ model: 'claude-sonnet-4-20250514',
142
+ max_tokens: 2048,
143
+ messages: [{ role: 'user', content: prompt }]
144
+ }, {
145
+ headers: {
146
+ 'x-api-key': key,
147
+ 'anthropic-version': '2023-06-01',
148
+ 'Content-Type': 'application/json'
149
+ }
150
+ });
151
+
152
+ return response.data.content
153
+ .filter(block => block.type === 'text')
154
+ .map(block => block.text)
155
+ .join('');
156
+ } catch (e) {
157
+ if (e.response?.data?.error) {
158
+ throw new Error(`Anthropic API Error: ${e.response.data.error.message}`);
159
+ }
160
+ throw new Error(`Anthropic API Error: ${e.message}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ export const aiService = new AiService();
@@ -1,115 +1,115 @@
1
- import axios from 'axios';
2
- import chalk from 'chalk';
3
- import { getCredentials } from '../utils/config.js';
4
-
5
- export class ApiService {
6
- constructor() {
7
- this.init();
8
- }
9
-
10
- init() {
11
- const { jiraUrl, email, apiToken } = getCredentials();
12
-
13
- if (!jiraUrl || !email || !apiToken) {
14
- this.client = null;
15
- this._domain = null;
16
- return;
17
- }
18
-
19
- const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
20
- this._domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
21
-
22
- const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`;
23
-
24
- // Standard REST API v3 client
25
- this.client = axios.create({
26
- baseURL: `${this._domain}/rest/api/3`,
27
- headers: {
28
- 'Authorization': authHeader,
29
- 'Accept': 'application/json',
30
- 'Content-Type': 'application/json'
31
- }
32
- });
33
-
34
- // Agile REST API v1 client (for boards, sprints, etc.)
35
- this.agileClient = axios.create({
36
- baseURL: `${this._domain}/rest/agile/1.0`,
37
- headers: {
38
- 'Authorization': authHeader,
39
- 'Accept': 'application/json',
40
- 'Content-Type': 'application/json'
41
- }
42
- });
43
-
44
- // Shared response interceptor
45
- const errorInterceptor = (error) => {
46
- if (error.response) {
47
- if (error.response.status === 401) {
48
- console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
49
- } else if (error.response.status === 403) {
50
- console.error(chalk.red('Access denied. You may not have permission for this resource.'));
51
- }
52
- }
53
- return Promise.reject(error);
54
- };
55
-
56
- this.client.interceptors.response.use(r => r, errorInterceptor);
57
- this.agileClient.interceptors.response.use(r => r, errorInterceptor);
58
- }
59
-
60
- /** @returns {string} The Jira domain URL */
61
- get domain() {
62
- return this._domain;
63
- }
64
-
65
- ensureClient() {
66
- if (!this.client) {
67
- this.init();
68
- if (!this.client) {
69
- throw new Error('Jira credentials not configured. Run "jira config" first.');
70
- }
71
- }
72
- }
73
-
74
- // ── Standard REST API v3 Methods ────────────────────────────────
75
-
76
- async get(url, config = {}) {
77
- this.ensureClient();
78
- const response = await this.client.get(url, config);
79
- return response.data;
80
- }
81
-
82
- async post(url, data, config = {}) {
83
- this.ensureClient();
84
- const response = await this.client.post(url, data, config);
85
- return response.data;
86
- }
87
-
88
- async put(url, data, config = {}) {
89
- this.ensureClient();
90
- const response = await this.client.put(url, data, config);
91
- return response.data;
92
- }
93
-
94
- async delete(url, config = {}) {
95
- this.ensureClient();
96
- const response = await this.client.delete(url, config);
97
- return response.data;
98
- }
99
-
100
- // ── Agile REST API v1 Methods ───────────────────────────────────
101
-
102
- async agileGet(url, config = {}) {
103
- this.ensureClient();
104
- const response = await this.agileClient.get(url, config);
105
- return response.data;
106
- }
107
-
108
- async agilePost(url, data, config = {}) {
109
- this.ensureClient();
110
- const response = await this.agileClient.post(url, data, config);
111
- return response.data;
112
- }
113
- }
114
-
115
- export const api = new ApiService();
1
+ import axios from 'axios';
2
+ import chalk from 'chalk';
3
+ import { getCredentials } from '../utils/config.js';
4
+
5
+ export class ApiService {
6
+ constructor() {
7
+ this.init();
8
+ }
9
+
10
+ init() {
11
+ const { jiraUrl, email, apiToken } = getCredentials();
12
+
13
+ if (!jiraUrl || !email || !apiToken) {
14
+ this.client = null;
15
+ this._domain = null;
16
+ return;
17
+ }
18
+
19
+ const match = jiraUrl.match(/^https?:\/\/(.+?)(\/|$)/);
20
+ this._domain = match ? match[0].replace(/\/$/, '') : jiraUrl;
21
+
22
+ const authHeader = `Basic ${Buffer.from(`${email}:${apiToken}`).toString('base64')}`;
23
+
24
+ // Standard REST API v3 client
25
+ this.client = axios.create({
26
+ baseURL: `${this._domain}/rest/api/3`,
27
+ headers: {
28
+ 'Authorization': authHeader,
29
+ 'Accept': 'application/json',
30
+ 'Content-Type': 'application/json'
31
+ }
32
+ });
33
+
34
+ // Agile REST API v1 client (for boards, sprints, etc.)
35
+ this.agileClient = axios.create({
36
+ baseURL: `${this._domain}/rest/agile/1.0`,
37
+ headers: {
38
+ 'Authorization': authHeader,
39
+ 'Accept': 'application/json',
40
+ 'Content-Type': 'application/json'
41
+ }
42
+ });
43
+
44
+ // Shared response interceptor
45
+ const errorInterceptor = (error) => {
46
+ if (error.response) {
47
+ if (error.response.status === 401) {
48
+ console.error(chalk.red('Authentication failed. Please check your credentials using "jira config".'));
49
+ } else if (error.response.status === 403) {
50
+ console.error(chalk.red('Access denied. You may not have permission for this resource.'));
51
+ }
52
+ }
53
+ return Promise.reject(error);
54
+ };
55
+
56
+ this.client.interceptors.response.use(r => r, errorInterceptor);
57
+ this.agileClient.interceptors.response.use(r => r, errorInterceptor);
58
+ }
59
+
60
+ /** @returns {string} The Jira domain URL */
61
+ get domain() {
62
+ return this._domain;
63
+ }
64
+
65
+ ensureClient() {
66
+ if (!this.client) {
67
+ this.init();
68
+ if (!this.client) {
69
+ throw new Error('Jira credentials not configured. Run "jira config" first.');
70
+ }
71
+ }
72
+ }
73
+
74
+ // ── Standard REST API v3 Methods ────────────────────────────────
75
+
76
+ async get(url, config = {}) {
77
+ this.ensureClient();
78
+ const response = await this.client.get(url, config);
79
+ return response.data;
80
+ }
81
+
82
+ async post(url, data, config = {}) {
83
+ this.ensureClient();
84
+ const response = await this.client.post(url, data, config);
85
+ return response.data;
86
+ }
87
+
88
+ async put(url, data, config = {}) {
89
+ this.ensureClient();
90
+ const response = await this.client.put(url, data, config);
91
+ return response.data;
92
+ }
93
+
94
+ async delete(url, config = {}) {
95
+ this.ensureClient();
96
+ const response = await this.client.delete(url, config);
97
+ return response.data;
98
+ }
99
+
100
+ // ── Agile REST API v1 Methods ───────────────────────────────────
101
+
102
+ async agileGet(url, config = {}) {
103
+ this.ensureClient();
104
+ const response = await this.agileClient.get(url, config);
105
+ return response.data;
106
+ }
107
+
108
+ async agilePost(url, data, config = {}) {
109
+ this.ensureClient();
110
+ const response = await this.agileClient.post(url, data, config);
111
+ return response.data;
112
+ }
113
+ }
114
+
115
+ export const api = new ApiService();
@@ -1,49 +1,49 @@
1
- export function parseADF(content) {
2
- if (!content) return '';
3
- if (typeof content === 'string') return content;
4
-
5
- if (content.type === 'doc') {
6
- return content.content.map(node => parseNode(node)).join('\n');
7
- }
8
-
9
- return JSON.stringify(content);
10
- }
11
-
12
- function parseNode(node) {
13
- if (!node) return '';
14
-
15
- switch (node.type) {
16
- case 'paragraph':
17
- return parseParagraph(node);
18
- case 'text':
19
- return node.text;
20
- case 'bulletList':
21
- return parseList(node, '•');
22
- case 'orderedList':
23
- return parseList(node, '1.');
24
- case 'heading':
25
- return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map(c => parseNode(c)).join('')}\n`;
26
- case 'codeBlock':
27
- return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map(c => c.text).join('')}\n\`\`\`\n`;
28
- case 'blockquote':
29
- return `> ${node.content.map(c => parseNode(c)).join('')}`;
30
- default:
31
- if (node.content) {
32
- return node.content.map(c => parseNode(c)).join('');
33
- }
34
- return ''; // Unknown node, skip or fallback
35
- }
36
- }
37
-
38
- function parseParagraph(node) {
39
- if (!node.content) return '\n';
40
- return node.content.map(c => parseNode(c)).join('') + '\n';
41
- }
42
-
43
- function parseList(node, marker) {
44
- if (!node.content) return '';
45
- return node.content.map((item, index) => {
46
- const prefix = marker === '1.' ? `${index + 1}. ` : `${marker} `;
47
- return `${prefix}${item.content.map(c => parseNode(c)).join('')}`;
48
- }).join('\n') + '\n';
49
- }
1
+ export function parseADF(content) {
2
+ if (!content) return '';
3
+ if (typeof content === 'string') return content;
4
+
5
+ if (content.type === 'doc') {
6
+ return content.content.map(node => parseNode(node)).join('\n');
7
+ }
8
+
9
+ return JSON.stringify(content);
10
+ }
11
+
12
+ function parseNode(node) {
13
+ if (!node) return '';
14
+
15
+ switch (node.type) {
16
+ case 'paragraph':
17
+ return parseParagraph(node);
18
+ case 'text':
19
+ return node.text;
20
+ case 'bulletList':
21
+ return parseList(node, '•');
22
+ case 'orderedList':
23
+ return parseList(node, '1.');
24
+ case 'heading':
25
+ return `\n${'#'.repeat(node.attrs?.level || 1)} ${node.content.map(c => parseNode(c)).join('')}\n`;
26
+ case 'codeBlock':
27
+ return `\n\`\`\`${node.attrs?.language || ''}\n${node.content.map(c => c.text).join('')}\n\`\`\`\n`;
28
+ case 'blockquote':
29
+ return `> ${node.content.map(c => parseNode(c)).join('')}`;
30
+ default:
31
+ if (node.content) {
32
+ return node.content.map(c => parseNode(c)).join('');
33
+ }
34
+ return ''; // Unknown node, skip or fallback
35
+ }
36
+ }
37
+
38
+ function parseParagraph(node) {
39
+ if (!node.content) return '\n';
40
+ return node.content.map(c => parseNode(c)).join('') + '\n';
41
+ }
42
+
43
+ function parseList(node, marker) {
44
+ if (!node.content) return '';
45
+ return node.content.map((item, index) => {
46
+ const prefix = marker === '1.' ? `${index + 1}. ` : `${marker} `;
47
+ return `${prefix}${item.content.map(c => parseNode(c)).join('')}`;
48
+ }).join('\n') + '\n';
49
+ }