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.
- package/LICENSE +5 -5
- package/README.md +465 -373
- package/bin/jira.js +62 -55
- package/package.json +90 -90
- package/src/commands/ai-actions/plan.js +119 -0
- package/src/commands/ai-actions/review.js +109 -0
- package/src/commands/ai-actions/standup.js +42 -0
- package/src/commands/ai.js +232 -209
- package/src/commands/board.js +75 -66
- package/src/commands/bulk.js +108 -0
- package/src/commands/config.js +224 -154
- package/src/commands/dashboard.js +89 -0
- package/src/commands/git.js +63 -63
- package/src/commands/issue.js +985 -707
- package/src/commands/mcp.js +20 -20
- package/src/commands/project.js +59 -50
- package/src/commands/sprint.js +153 -78
- package/src/server/mcp-server.js +332 -332
- package/src/services/ai-service.js +165 -107
- package/src/services/api-service.js +115 -115
- package/src/utils/adf-parser.js +49 -49
- package/src/utils/config.js +97 -60
- package/src/utils/error-handler.js +41 -41
- package/src/utils/text-to-adf.js +34 -34
- package/src/utils/validators.js +88 -88
|
@@ -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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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();
|
package/src/utils/adf-parser.js
CHANGED
|
@@ -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
|
+
}
|