@relipa/ai-flow-kit 0.2.0-beta.0 → 0.2.0-beta.1

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 (44) hide show
  1. package/bin/aiflow.js +56 -0
  2. package/custom/rules/output-language.md +36 -0
  3. package/custom/skills/automation-testing/SKILL.md +1 -1
  4. package/custom/skills/ba-skills/skill-ba-build-business-rules-v1.md +2 -0
  5. package/custom/skills/ba-skills/skill-ba-initial-analysis-v1.md +2 -0
  6. package/custom/skills/ba-skills/skill-ba-mermaid-flowchart-v1.md +2 -0
  7. package/custom/skills/ba-skills/skill-ba-qna-v1.md +2 -0
  8. package/custom/skills/ba-skills/skill-ba-ui-prototype-v1.md +2 -0
  9. package/custom/skills/ba-skills/skill-ba-write-uc-spec-v1.md +2 -0
  10. package/custom/skills/coverage-check/SKILL.md +2 -0
  11. package/custom/skills/create-system-requirement/SKILL.md +11 -7
  12. package/custom/skills/deploy-model/SKILL.md +2 -0
  13. package/custom/skills/design-experiment/SKILL.md +2 -0
  14. package/custom/skills/evaluate-model/SKILL.md +2 -0
  15. package/custom/skills/evidence-aggregation/SKILL.md +2 -0
  16. package/custom/skills/execute-flow/SKILL.md +2 -0
  17. package/custom/skills/explore-data/SKILL.md +2 -0
  18. package/custom/skills/frame-ml-problem/SKILL.md +2 -0
  19. package/custom/skills/gate-review/SKILL.md +2 -0
  20. package/custom/skills/generate-spec/SKILL.md +2 -0
  21. package/custom/skills/generate-test-report/SKILL.md +2 -0
  22. package/custom/skills/generate-testcase/SKILL.md +2 -0
  23. package/custom/skills/impact-analysis/SKILL.md +2 -0
  24. package/custom/skills/improve-algorithm/SKILL.md +2 -0
  25. package/custom/skills/ingest-data/SKILL.md +116 -2
  26. package/custom/skills/log-bug/SKILL.md +2 -0
  27. package/custom/skills/pr-impact-analysis/SKILL.md +2 -0
  28. package/custom/skills/read-study-requirement/SKILL.md +13 -7
  29. package/custom/skills/report-customer/SKILL.md +6 -2
  30. package/custom/skills/retest-orchestration/SKILL.md +2 -0
  31. package/custom/skills/review-plan/SKILL.md +2 -0
  32. package/custom/skills/script-sync/SKILL.md +1 -1
  33. package/custom/skills/test-analysis/SKILL.md +2 -0
  34. package/custom/skills/test-skills/rules/qa-writing-standards.md +2 -2
  35. package/custom/skills/train-model/SKILL.md +2 -0
  36. package/custom/templates/shared/create-testcase-workflow.md +32 -1
  37. package/custom/templates/shared/gate-workflow.md +15 -0
  38. package/docs/common/CHANGELOG.md +13 -0
  39. package/package.json +1 -1
  40. package/scripts/create-score-excel.js +4 -111
  41. package/scripts/init.js +24 -1
  42. package/scripts/link-resolver.js +93 -0
  43. package/scripts/ticket-writer.js +247 -0
  44. package/scripts/use.js +141 -0
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+ // Vấn đề 5 (docs/internal/PM Workflow_v1.0.md) — turns an AI-proposed, PM-approved task list
3
+ // (born out of `ingest-data` Gate 3) into real tickets on Backlog/Jira.
4
+ //
5
+ // Read access (fetch-links, listing projects/issueTypes/priorities) reuses the existing
6
+ // read-only credentials already configured for `ak use`/`ak fetch-links`. Creating a ticket is a
7
+ // WRITE action and requires a SEPARATE credential (`BACKLOG_API_KEY_WRITE` / `JIRA_API_TOKEN_WRITE`)
8
+ // — same "distinct WRITE key" policy the PM Workflow doc calls for, kept apart from the read key so
9
+ // a compromised/misconfigured read key can never create tickets.
10
+
11
+ const fs = require('fs-extra');
12
+ const chalk = require('chalk');
13
+ const useCommand = require('./use');
14
+ const linkResolver = require('./link-resolver');
15
+
16
+ const {
17
+ loadCredentials,
18
+ resolveBacklogCredentials,
19
+ resolveJiraCredentials,
20
+ saveMcpCredentials,
21
+ } = useCommand;
22
+
23
+ const {
24
+ fetchBacklogIssueTypes,
25
+ fetchBacklogPriorities,
26
+ createBacklogIssue,
27
+ createJiraIssue,
28
+ } = linkResolver;
29
+
30
+ // Only these keys can be written via `ak credentials set` — dedicated adapter credentials
31
+ // (BACKLOG_API_KEY, JIRA_API_TOKEN, ...) still go through `ak init --adapter` on purpose.
32
+ const SETTABLE_CREDENTIAL_KEYS = [
33
+ 'BACKLOG_API_KEY_WRITE',
34
+ 'JIRA_API_TOKEN_WRITE',
35
+ 'JIRA_EMAIL_WRITE',
36
+ 'BACKLOG_DEFAULT_PROJECT_ID',
37
+ 'BACKLOG_DEFAULT_PROJECT_KEY',
38
+ 'JIRA_DEFAULT_PROJECT_KEY',
39
+ ];
40
+
41
+ function jiraFullDomain(domain) {
42
+ return domain.includes('.') ? domain : `${domain}.atlassian.net`;
43
+ }
44
+
45
+ function missingCredResult(target, field, message) {
46
+ return { ok: false, error: 'missing-write-credentials', target, field, message };
47
+ }
48
+
49
+ async function resolveBacklogWrite(creds) {
50
+ const read = resolveBacklogCredentials(creds);
51
+ if (!read) {
52
+ return missingCredResult(
53
+ 'backlog', 'BACKLOG_API_KEY / BACKLOG_SPACE_KEY',
54
+ 'Chưa cấu hình credentials đọc Backlog. Chạy `ak init --adapter backlog` trước.'
55
+ );
56
+ }
57
+ const apiKeyWrite = process.env.BACKLOG_API_KEY_WRITE || creds.BACKLOG_API_KEY_WRITE;
58
+ if (!apiKeyWrite) {
59
+ return missingCredResult(
60
+ 'backlog', 'BACKLOG_API_KEY_WRITE',
61
+ 'Cần API key Backlog có quyền tạo issue (khác key đọc hiện tại). Cung cấp API key này, mình sẽ lưu lại và tạo ticket ngay.'
62
+ );
63
+ }
64
+ return { ok: true, domain: read.domain, apiKeyRead: read.apiKey, apiKeyWrite };
65
+ }
66
+
67
+ async function resolveJiraWrite(creds) {
68
+ const read = resolveJiraCredentials(creds);
69
+ if (!read) {
70
+ return missingCredResult(
71
+ 'jira', 'JIRA_API_TOKEN / JIRA_EMAIL / JIRA_DOMAIN',
72
+ 'Chưa cấu hình credentials đọc Jira. Chạy `ak init --adapter jira` trước.'
73
+ );
74
+ }
75
+ const apiTokenWrite = process.env.JIRA_API_TOKEN_WRITE || creds.JIRA_API_TOKEN_WRITE;
76
+ if (!apiTokenWrite) {
77
+ return missingCredResult(
78
+ 'jira', 'JIRA_API_TOKEN_WRITE',
79
+ 'Cần Jira API token có quyền tạo issue (khác token đọc hiện tại). Cung cấp token này, mình sẽ lưu lại và tạo ticket ngay.'
80
+ );
81
+ }
82
+ const emailWrite = process.env.JIRA_EMAIL_WRITE || creds.JIRA_EMAIL_WRITE || read.email;
83
+ return { ok: true, domain: jiraFullDomain(read.domain), emailWrite, apiTokenWrite };
84
+ }
85
+
86
+ async function pickBacklogDefaults(domain, apiKeyRead, projectId) {
87
+ const [issueTypes, priorities] = await Promise.all([
88
+ fetchBacklogIssueTypes(domain, apiKeyRead, projectId),
89
+ fetchBacklogPriorities(domain, apiKeyRead),
90
+ ]);
91
+ if (!issueTypes || !issueTypes.length) throw new Error(`Project ${projectId} không có issueType nào — không thể tạo issue.`);
92
+ const defaultIssueTypeId = issueTypes[0].id;
93
+ const normal = (priorities || []).find((p) => /normal|trung bình/i.test(p.name));
94
+ const defaultPriorityId = (normal || (priorities || [])[0] || {}).id;
95
+ if (!defaultPriorityId) throw new Error('Không lấy được danh sách priority từ Backlog.');
96
+ return { defaultIssueTypeId, defaultPriorityId };
97
+ }
98
+
99
+ /**
100
+ * `ak tasks create-tickets <file.json> [--json]`
101
+ *
102
+ * Input file shape:
103
+ * {
104
+ * "target": "backlog" | "jira",
105
+ * "projectId": "12345", // optional — Backlog only, overrides BACKLOG_DEFAULT_PROJECT_ID
106
+ * "projectKey": "PROJ", // optional — Jira only, overrides JIRA_DEFAULT_PROJECT_KEY
107
+ * "tasks": [
108
+ * { "type": "spec|coding|test|other", "title": "...", "description": "..." }
109
+ * ]
110
+ * }
111
+ *
112
+ * `description` is expected to already be fully composed by the AI per the ticket template in
113
+ * PM Workflow_v1.0.md §6.1 (Nội dung task + Nguồn tham chiếu) — this command posts it as-is.
114
+ */
115
+ async function createTicketsCommand(inputFile, options = {}) {
116
+ const emit = (obj) => {
117
+ if (options.json) { process.stdout.write(JSON.stringify(obj)); return; }
118
+ if (obj.ok === false) {
119
+ console.log(chalk.red(`✗ ${obj.message || obj.error}`));
120
+ } else {
121
+ console.log(chalk.green(`✓ ${obj.results.filter(r => r.ok).length}/${obj.results.length} ticket(s) created on ${obj.target}.`));
122
+ for (const r of obj.results) {
123
+ console.log(r.ok ? ` ${chalk.green('✓')} ${r.ticketId} ${chalk.gray(r.title)}` : ` ${chalk.red('✗')} ${chalk.gray(r.title)} — ${r.error}`);
124
+ }
125
+ }
126
+ console.log(JSON.stringify(obj));
127
+ };
128
+
129
+ let input;
130
+ try {
131
+ input = await fs.readJson(inputFile);
132
+ } catch (err) {
133
+ emit({ ok: false, error: 'invalid-input-file', message: `Không đọc được ${inputFile}: ${err.message}` });
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+
138
+ const target = input.target;
139
+ const tasks = input.tasks || [];
140
+ if (!['backlog', 'jira'].includes(target)) {
141
+ emit({ ok: false, error: 'invalid-target', message: `target phải là "backlog" hoặc "jira", nhận được: ${target}` });
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ if (!tasks.length) {
146
+ emit({ ok: false, error: 'empty-tasks', message: 'File input không có task nào trong "tasks".' });
147
+ process.exitCode = 1;
148
+ return;
149
+ }
150
+
151
+ const creds = await loadCredentials();
152
+
153
+ if (target === 'backlog') {
154
+ const write = await resolveBacklogWrite(creds);
155
+ if (!write.ok) { emit(write); process.exitCode = 2; return; }
156
+
157
+ const projectId = input.projectId || creds.BACKLOG_DEFAULT_PROJECT_ID;
158
+ if (!projectId) {
159
+ emit({
160
+ ok: false, error: 'missing-project',
161
+ message: 'Chưa xác định projectId đích trên Backlog. Chạy `ak backlog-projects` để xem danh sách, rồi `ak backlog-set-default-project <id> <key>`.',
162
+ });
163
+ process.exitCode = 2;
164
+ return;
165
+ }
166
+
167
+ let defaults;
168
+ try {
169
+ defaults = await pickBacklogDefaults(write.domain, write.apiKeyRead, projectId);
170
+ } catch (err) {
171
+ emit({ ok: false, error: 'project-lookup-failed', message: err.message });
172
+ process.exitCode = 1;
173
+ return;
174
+ }
175
+
176
+ const results = [];
177
+ for (const task of tasks) {
178
+ try {
179
+ const created = await createBacklogIssue(write.domain, write.apiKeyWrite, {
180
+ projectId,
181
+ summary: task.title,
182
+ description: task.description,
183
+ issueTypeId: task.issueTypeId || defaults.defaultIssueTypeId,
184
+ priorityId: task.priorityId || defaults.defaultPriorityId,
185
+ });
186
+ results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
187
+ } catch (err) {
188
+ results.push({ type: task.type, title: task.title, ok: false, error: err.message });
189
+ }
190
+ }
191
+ emit({ ok: true, target, projectId, results });
192
+ return;
193
+ }
194
+
195
+ // target === 'jira'
196
+ const write = await resolveJiraWrite(creds);
197
+ if (!write.ok) { emit(write); process.exitCode = 2; return; }
198
+
199
+ const projectKey = input.projectKey || creds.JIRA_DEFAULT_PROJECT_KEY;
200
+ if (!projectKey) {
201
+ emit({
202
+ ok: false, error: 'missing-project',
203
+ message: 'Chưa xác định project đích trên Jira. Chạy `ak jira-projects` để xem danh sách, rồi `ak jira-set-default-project <key>`.',
204
+ });
205
+ process.exitCode = 2;
206
+ return;
207
+ }
208
+
209
+ const results = [];
210
+ for (const task of tasks) {
211
+ try {
212
+ const created = await createJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, {
213
+ projectKey,
214
+ summary: task.title,
215
+ description: task.description,
216
+ issueTypeName: task.issueType || 'Task',
217
+ });
218
+ results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
219
+ } catch (err) {
220
+ results.push({ type: task.type, title: task.title, ok: false, error: err.message });
221
+ }
222
+ }
223
+ emit({ ok: true, target, projectKey, results });
224
+ }
225
+
226
+ /**
227
+ * `ak credentials set <KEY> <VALUE>` — lets the AI persist a WRITE key the user pasted in chat
228
+ * (after being asked, per the missing-write-credentials error above) without a bespoke command
229
+ * per key. Restricted to an allowlist — dedicated adapter read-credentials still go through
230
+ * `ak init --adapter`, this is only for the pieces that flow introduced.
231
+ */
232
+ async function setCredentialCommand(key, value) {
233
+ if (!SETTABLE_CREDENTIAL_KEYS.includes(key)) {
234
+ console.log(chalk.red(`✗ Unknown credential key: ${key}.`));
235
+ console.log(chalk.gray(` Allowed: ${SETTABLE_CREDENTIAL_KEYS.join(', ')}`));
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+ await saveMcpCredentials({ [key]: value });
240
+ console.log(chalk.green(`✓ Saved ${key} to ~/.aiflow/credentials.json`));
241
+ }
242
+
243
+ module.exports = {
244
+ createTicketsCommand,
245
+ setCredentialCommand,
246
+ SETTABLE_CREDENTIAL_KEYS,
247
+ };
package/scripts/use.js CHANGED
@@ -360,6 +360,14 @@ function backlogGet(url) {
360
360
  });
361
361
  }
362
362
 
363
+ /**
364
+ * Fetch the list of projects in the Backlog space — used to let the user pick a default
365
+ * project for tasks created manually (no ticket to derive a project from).
366
+ */
367
+ function fetchBacklogProjects(domain, apiKey) {
368
+ return backlogGet(`https://${domain}/api/v2/projects?apiKey=${apiKey}`);
369
+ }
370
+
363
371
  /**
364
372
  * Fetch issue detail from Backlog REST API
365
373
  */
@@ -584,6 +592,64 @@ function fetchJiraIssue(domain, email, apiToken, issueKey) {
584
592
  });
585
593
  }
586
594
 
595
+ /**
596
+ * `ak jira-projects [--json]` — lists the projects visible to the configured Jira account, so the
597
+ * user can pick a default project for tasks created from Meeting Minutes/QnA (see Vấn đề 5.1 in
598
+ * PM Workflow_v1.0.md). Mirrors `listBacklogProjectsCommand` above.
599
+ */
600
+ function fetchJiraProjects(domain, email, apiToken) {
601
+ return new Promise((resolve, reject) => {
602
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
603
+ const url = `https://${domain}.atlassian.net/rest/api/3/project`;
604
+ https
605
+ .get(url, { headers: { Authorization: `Basic ${auth}`, Accept: "application/json" } }, (res) => {
606
+ let data = "";
607
+ res.on("data", (chunk) => (data += chunk));
608
+ res.on("end", () => {
609
+ if (res.statusCode !== 200) {
610
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
611
+ return;
612
+ }
613
+ try { resolve(JSON.parse(data)); }
614
+ catch (e) { reject(new Error("Invalid JSON response from Jira")); }
615
+ });
616
+ })
617
+ .on("error", reject);
618
+ });
619
+ }
620
+
621
+ async function listJiraProjectsCommand(options = {}) {
622
+ const creds = await loadCredentials();
623
+ const resolved = resolveJiraCredentials(creds);
624
+ if (!resolved) {
625
+ const msg = "Jira credentials not set. Run `aiflow init --adapter jira` first.";
626
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: msg })); return; }
627
+ console.log(chalk.yellow(`⚠ ${msg}`));
628
+ return;
629
+ }
630
+ try {
631
+ const projects = await fetchJiraProjects(resolved.domain, resolved.email, resolved.apiToken);
632
+ const list = projects.map((p) => ({ id: p.id, key: p.key, name: p.name }));
633
+ if (options.json) {
634
+ process.stdout.write(JSON.stringify({ ok: true, projects: list, defaultProjectKey: creds.JIRA_DEFAULT_PROJECT_KEY ?? null }));
635
+ return;
636
+ }
637
+ console.log(chalk.cyan(`\nJira projects (${list.length}):`));
638
+ for (const p of list) {
639
+ const isDefault = p.key === creds.JIRA_DEFAULT_PROJECT_KEY;
640
+ console.log(` ${isDefault ? chalk.green("●") : " "} ${p.key} ${chalk.gray(p.name)} ${chalk.gray(`(id: ${p.id})`)}`);
641
+ }
642
+ } catch (err) {
643
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: err.message })); return; }
644
+ console.log(chalk.red(`✗ Could not fetch Jira projects: ${err.message}`));
645
+ }
646
+ }
647
+
648
+ async function setDefaultJiraProjectCommand(key) {
649
+ await saveMcpCredentials({ JIRA_DEFAULT_PROJECT_KEY: key });
650
+ console.log(chalk.green(`✓ Default Jira project set to ${key}.`));
651
+ }
652
+
587
653
  /**
588
654
  * Fetch ALL comments of an issue from Jira REST API.
589
655
  */
@@ -787,11 +853,20 @@ async function manualContext(prefillId = "", options = {}) {
787
853
  // Task type — pre-select existing value if available
788
854
  const taskType = await promptForTaskType(existing.taskType, options.taskType);
789
855
 
856
+ // Stamp the default Backlog project (`ak backlog-set-default-project`), if one was ever set —
857
+ // manual tasks have no ticket to derive a project from otherwise. Kept for later use: syncing a
858
+ // locally-created task up to Backlog as a new issue needs to know which project it belongs to.
859
+ const creds = await loadCredentials();
860
+ const projectId = existing.projectId ?? creds.BACKLOG_DEFAULT_PROJECT_ID ?? null;
861
+ const projectKey = existing.projectKey ?? creds.BACKLOG_DEFAULT_PROJECT_KEY ?? null;
862
+
790
863
  const context = {
791
864
  taskId,
792
865
  taskType,
793
866
  title,
794
867
  description,
868
+ projectId,
869
+ projectKey,
795
870
  status: existing.status || "In Progress",
796
871
  mode: existing.mode || "auto",
797
872
  acceptanceCriteria: existing.acceptanceCriteria || [],
@@ -967,6 +1042,63 @@ async function loadCredentials() {
967
1042
  return {};
968
1043
  }
969
1044
 
1045
+ /** Always the global file (`~/.aiflow/credentials.json`) — same path `loadCredentials` reads
1046
+ * first — so a value written here is picked up by every later `loadCredentials()` call. */
1047
+ function globalCredentialsPath() {
1048
+ return path.join(os.homedir(), ".aiflow", "credentials.json");
1049
+ }
1050
+
1051
+ /** Merges `patch` into the saved `.mcp` credentials block, creating the file if needed. */
1052
+ async function saveMcpCredentials(patch) {
1053
+ const file = globalCredentialsPath();
1054
+ await fs.ensureDir(path.dirname(file));
1055
+ const data = (await fs.pathExists(file)) ? await fs.readJson(file).catch(() => ({})) : {};
1056
+ data.mcp = { ...(data.mcp || {}), ...patch };
1057
+ await fs.writeJson(file, data, { spaces: 2 });
1058
+ }
1059
+
1060
+ /**
1061
+ * `ak backlog-projects [--json]` — lists the projects in the configured Backlog space, so the
1062
+ * user (or a headless caller like the VS Code extension) can pick one as the default project for
1063
+ * manually-created tasks (see `manualContext` below, which stamps that default onto new tasks).
1064
+ */
1065
+ async function listBacklogProjectsCommand(options = {}) {
1066
+ const creds = await loadCredentials();
1067
+ const resolved = resolveBacklogCredentials(creds);
1068
+ if (!resolved) {
1069
+ const msg = "Backlog credentials not set. Run `aiflow init --adapter backlog` first.";
1070
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: msg })); return; }
1071
+ console.log(chalk.yellow(`⚠ ${msg}`));
1072
+ return;
1073
+ }
1074
+ try {
1075
+ const projects = await fetchBacklogProjects(resolved.domain, resolved.apiKey);
1076
+ const list = projects.map((p) => ({ id: p.id, projectKey: p.projectKey, name: p.name }));
1077
+ if (options.json) {
1078
+ process.stdout.write(JSON.stringify({ ok: true, projects: list, defaultProjectId: creds.BACKLOG_DEFAULT_PROJECT_ID ?? null }));
1079
+ return;
1080
+ }
1081
+ console.log(chalk.cyan(`\nBacklog projects (${list.length}):`));
1082
+ for (const p of list) {
1083
+ const isDefault = String(p.id) === String(creds.BACKLOG_DEFAULT_PROJECT_ID);
1084
+ console.log(` ${isDefault ? chalk.green("●") : " "} ${p.projectKey} ${chalk.gray(p.name)} ${chalk.gray(`(id: ${p.id})`)}`);
1085
+ }
1086
+ } catch (err) {
1087
+ if (options.json) { process.stdout.write(JSON.stringify({ ok: false, error: err.message })); return; }
1088
+ console.log(chalk.red(`✗ Could not fetch Backlog projects: ${err.message}`));
1089
+ }
1090
+ }
1091
+
1092
+ /**
1093
+ * `ak backlog-set-default-project <id> <key>` — saves the chosen default project, so future
1094
+ * manually-created tasks (`ak use --manual`) get stamped with it automatically. Non-interactive —
1095
+ * safe to spawn without a TTY (see New-ex-architecture_v1.0.md §1.1 for why that distinction matters).
1096
+ */
1097
+ async function setDefaultBacklogProjectCommand(id, key) {
1098
+ await saveMcpCredentials({ BACKLOG_DEFAULT_PROJECT_ID: id, BACKLOG_DEFAULT_PROJECT_KEY: key });
1099
+ console.log(chalk.green(`✓ Default Backlog project set to ${key} (id: ${id}).`));
1100
+ }
1101
+
970
1102
  // ──────────────────────────────────────────────────────────────
971
1103
  // Context persistence
972
1104
  // ──────────────────────────────────────────────────────────────
@@ -1353,3 +1485,12 @@ module.exports.detectTaskTypeFromString = detectTaskTypeFromString;
1353
1485
  module.exports.manualContext = manualContext;
1354
1486
  module.exports.promptForTaskType = promptForTaskType;
1355
1487
  module.exports.previewTarget = previewTarget;
1488
+ module.exports.resolveBacklogCredentials = resolveBacklogCredentials;
1489
+ module.exports.fetchBacklogProjects = fetchBacklogProjects;
1490
+ module.exports.listBacklogProjectsCommand = listBacklogProjectsCommand;
1491
+ module.exports.setDefaultBacklogProjectCommand = setDefaultBacklogProjectCommand;
1492
+ module.exports.resolveJiraCredentials = resolveJiraCredentials;
1493
+ module.exports.fetchJiraProjects = fetchJiraProjects;
1494
+ module.exports.listJiraProjectsCommand = listJiraProjectsCommand;
1495
+ module.exports.setDefaultJiraProjectCommand = setDefaultJiraProjectCommand;
1496
+ module.exports.saveMcpCredentials = saveMcpCredentials;