@relipa/ai-flow-kit 0.0.7 → 0.0.8-beta.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.
- package/README.md +7 -6
- package/bin/aiflow.js +400 -360
- package/bin/ak.js +4 -0
- package/custom/templates/php-plain.md +261 -261
- package/custom/templates/php.md +261 -0
- package/custom/templates/python.md +79 -0
- package/docs/common/CHANGELOG.md +43 -1
- package/docs/common/QUICK_START.md +57 -50
- package/docs/common/cli-reference.md +161 -196
- package/docs/common/getting-started.md +3 -3
- package/docs/common/troubleshooting.md +7 -7
- package/package.json +2 -1
- package/scripts/checkpoint.js +46 -46
- package/scripts/gitnexus-worker.js +94 -94
- package/scripts/hooks/session-stop.js +55 -55
- package/scripts/init.js +7 -4
- package/scripts/task.js +21 -0
- package/scripts/use.js +880 -625
package/scripts/use.js
CHANGED
|
@@ -1,63 +1,67 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const os = require(
|
|
4
|
-
const chalk = require(
|
|
5
|
-
const https = require(
|
|
6
|
-
const { select, input } = require(
|
|
1
|
+
const fs = require("fs-extra");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const { select, input } = require("@inquirer/prompts");
|
|
7
7
|
|
|
8
8
|
const PROJECT_DIR = process.cwd();
|
|
9
|
-
const AIFLOW_DIR = path.join(PROJECT_DIR,
|
|
10
|
-
const STATE_FILE = path.join(AIFLOW_DIR,
|
|
11
|
-
const CONTEXT_DIR = path.join(PROJECT_DIR,
|
|
9
|
+
const AIFLOW_DIR = path.join(PROJECT_DIR, ".aiflow");
|
|
10
|
+
const STATE_FILE = path.join(AIFLOW_DIR, "state.json");
|
|
11
|
+
const CONTEXT_DIR = path.join(PROJECT_DIR, ".aiflow", "context");
|
|
12
12
|
|
|
13
13
|
// ──────────────────────────────────────────────────────────────
|
|
14
14
|
// Entry point
|
|
15
15
|
// ──────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
17
|
module.exports = async function use(target, options = {}) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// ── Load from local file ──
|
|
32
|
-
if (options.file) {
|
|
33
|
-
return await loadFromFile(options.file, options);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── No target → version switcher ──
|
|
37
|
-
if (!target) {
|
|
38
|
-
return await switchVersion();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Detect target type and dispatch ──
|
|
42
|
-
if (isVersion(target)) {
|
|
43
|
-
return await switchVersion(target);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (isTicketId(target)) {
|
|
47
|
-
const adapter = await resolveTicketAdapter();
|
|
48
|
-
if (adapter === 'jira') return await loadFromJira(target, options);
|
|
49
|
-
return await loadFromBacklog(target, options);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (isBacklogUrl(target)) {
|
|
53
|
-
const id = extractBacklogId(target);
|
|
54
|
-
return await loadFromBacklog(id, options);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Fallback: try Backlog then manual ──
|
|
58
|
-
console.log(chalk.yellow(`Cannot detect format for: ${target}`));
|
|
59
|
-
console.log(chalk.gray('Supported: PROJ-33, APP-123, version number, --manual'));
|
|
18
|
+
if (!(await fs.pathExists(STATE_FILE))) {
|
|
19
|
+
console.log(
|
|
20
|
+
chalk.red("Project is not initialized. Run `aiflow init` first."),
|
|
21
|
+
);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── GitNexus index notification (all AI tools see this) ──────
|
|
26
|
+
await checkGitNexusIndexStatus();
|
|
27
|
+
|
|
28
|
+
// ── Manual entry ──
|
|
29
|
+
if (options.manual) {
|
|
60
30
|
return await manualContext();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Load from local file ──
|
|
34
|
+
if (options.file) {
|
|
35
|
+
return await loadFromFile(options.file, options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── No target → version switcher ──
|
|
39
|
+
if (!target) {
|
|
40
|
+
return await switchVersion();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Detect target type and dispatch ──
|
|
44
|
+
if (isVersion(target)) {
|
|
45
|
+
return await switchVersion(target);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isTicketId(target)) {
|
|
49
|
+
const adapter = await resolveTicketAdapter();
|
|
50
|
+
if (adapter === "jira") return await loadFromJira(target, options);
|
|
51
|
+
return await loadFromBacklog(target, options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isBacklogUrl(target)) {
|
|
55
|
+
const id = extractBacklogId(target);
|
|
56
|
+
return await loadFromBacklog(id, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Fallback: try Backlog then manual ──
|
|
60
|
+
console.log(chalk.yellow(`Cannot detect format for: ${target}`));
|
|
61
|
+
console.log(
|
|
62
|
+
chalk.gray("Supported: PROJ-33, APP-123, version number, --manual"),
|
|
63
|
+
);
|
|
64
|
+
return await manualContext();
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -65,12 +69,12 @@ module.exports = async function use(target, options = {}) {
|
|
|
65
69
|
// ──────────────────────────────────────────────────────────────
|
|
66
70
|
|
|
67
71
|
function isVersion(s) {
|
|
68
|
-
|
|
72
|
+
return /^\d+\.\d+\.\d+$/.test(s);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/** Both Backlog and Jira use the same PROJECT-NUMBER format. */
|
|
72
76
|
function isTicketId(s) {
|
|
73
|
-
|
|
77
|
+
return /^[A-Z][A-Z0-9_]+-\d+$/.test(s);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/**
|
|
@@ -78,25 +82,27 @@ function isTicketId(s) {
|
|
|
78
82
|
* in state.json. Falls back to backlog if both or neither are configured.
|
|
79
83
|
*/
|
|
80
84
|
async function resolveTicketAdapter() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
try {
|
|
86
|
+
if (await fs.pathExists(STATE_FILE)) {
|
|
87
|
+
const state = await fs.readJson(STATE_FILE);
|
|
88
|
+
const adapters = state.adapters || [];
|
|
89
|
+
if (adapters.includes("jira") && !adapters.includes("backlog")) {
|
|
90
|
+
return "jira";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (_) {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
return "backlog";
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
function isBacklogUrl(s) {
|
|
94
|
-
|
|
100
|
+
return s.includes("backlog.com/view/") || s.includes("backlogtool.com/view/");
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
function extractBacklogId(url) {
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
const match = url.match(/\/view\/([A-Z][A-Z0-9_]+-\d+)/);
|
|
105
|
+
return match ? match[1] : null;
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -104,47 +110,51 @@ function extractBacklogId(url) {
|
|
|
104
110
|
// ──────────────────────────────────────────────────────────────
|
|
105
111
|
|
|
106
112
|
async function switchVersion(versionReq) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
const versionsDir = path.join(AIFLOW_DIR, "versions");
|
|
114
|
+
if (!(await fs.pathExists(versionsDir))) {
|
|
115
|
+
console.log(chalk.red("No versions directory found."));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const available = await fs.readdir(versionsDir);
|
|
120
|
+
if (!available.length) {
|
|
121
|
+
console.log(chalk.red("No installed versions found."));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let targetVersion = versionReq;
|
|
126
|
+
if (!targetVersion) {
|
|
127
|
+
targetVersion = await select({
|
|
128
|
+
message: "Select version to use:",
|
|
129
|
+
choices: available.map((v) => ({ name: `v${v}`, value: v })),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
126
132
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
if (!available.includes(targetVersion)) {
|
|
134
|
+
console.log(chalk.red(`Version v${targetVersion} is not installed.`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
console.log(chalk.blue(`Switching to v${targetVersion}...`));
|
|
139
|
+
const versionDir = path.join(versionsDir, targetVersion);
|
|
134
140
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
const claudeDir = path.join(PROJECT_DIR, ".claude");
|
|
142
|
+
await fs.emptyDir(claudeDir);
|
|
143
|
+
await fs.copy(
|
|
144
|
+
path.join(versionDir, "skills"),
|
|
145
|
+
path.join(claudeDir, "skills"),
|
|
146
|
+
{ overwrite: true },
|
|
147
|
+
);
|
|
138
148
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
const rulesDir = path.join(PROJECT_DIR, ".rules");
|
|
150
|
+
await fs.emptyDir(rulesDir);
|
|
151
|
+
await fs.copy(path.join(versionDir, "rules"), rulesDir, { overwrite: true });
|
|
142
152
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
const state = await fs.readJson(STATE_FILE);
|
|
154
|
+
state.current_version = targetVersion;
|
|
155
|
+
await fs.writeJson(STATE_FILE, state);
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
console.log(chalk.green(`✓ Switched to v${targetVersion}.`));
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -152,47 +162,72 @@ async function switchVersion(versionReq) {
|
|
|
152
162
|
// ──────────────────────────────────────────────────────────────
|
|
153
163
|
|
|
154
164
|
async function loadFromBacklog(issueKey, options = {}) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const creds = await loadCredentials();
|
|
166
|
+
const apiKey = process.env.BACKLOG_API_KEY || creds.BACKLOG_API_KEY;
|
|
167
|
+
const spaceKey =
|
|
168
|
+
process.env.BACKLOG_SPACE_KEY ||
|
|
169
|
+
creds.BACKLOG_SPACE_KEY ||
|
|
170
|
+
process.env.BACKLOG_DOMAIN ||
|
|
171
|
+
creds.BACKLOG_DOMAIN;
|
|
172
|
+
|
|
173
|
+
if (!apiKey || !spaceKey) {
|
|
174
|
+
console.log(chalk.yellow("⚠ Backlog credentials not set."));
|
|
175
|
+
console.log(chalk.gray("Run: aiflow init --adapter backlog"));
|
|
176
|
+
console.log(chalk.gray("Or set environment variables:"));
|
|
177
|
+
console.log(chalk.gray(" BACKLOG_API_KEY=your-api-key"));
|
|
178
|
+
console.log(chalk.gray(" BACKLOG_SPACE_KEY=your-space (e.g. mycompany)"));
|
|
179
|
+
console.log(chalk.gray("\nFalling back to manual entry...\n"));
|
|
180
|
+
return await manualContext(issueKey);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// spaceKey can be full domain (mycompany.backlog.com) or just the space (mycompany)
|
|
184
|
+
const domain = spaceKey.includes(".") ? spaceKey : `${spaceKey}.backlog.com`;
|
|
185
|
+
|
|
186
|
+
console.log(chalk.blue(`Fetching context from Backlog: ${issueKey}...`));
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Default: description only. Comments loaded only when explicitly requested.
|
|
190
|
+
const loadComments =
|
|
191
|
+
options.coms ||
|
|
192
|
+
options.withComs ||
|
|
193
|
+
options.withComments ||
|
|
194
|
+
options["with-comments"] ||
|
|
195
|
+
options.with_comments ||
|
|
196
|
+
options.cid != null ||
|
|
197
|
+
options.commentId != null ||
|
|
198
|
+
options["comment-id"] != null ||
|
|
199
|
+
options.clast != null ||
|
|
200
|
+
options.commentsLast != null ||
|
|
201
|
+
options.cfrom != null ||
|
|
202
|
+
options.commentsFrom != null ||
|
|
203
|
+
options.cto != null ||
|
|
204
|
+
options.commentsTo != null ||
|
|
205
|
+
options["comments-to"] != null;
|
|
206
|
+
|
|
207
|
+
if (loadComments) {
|
|
208
|
+
console.log(chalk.gray(" ℹ Comments requested..."));
|
|
168
209
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
fetchBacklogIssue(domain, apiKey, issueKey),
|
|
180
|
-
loadComments
|
|
181
|
-
? fetchBacklogComments(domain, apiKey, issueKey)
|
|
182
|
-
: Promise.resolve([]),
|
|
183
|
-
]);
|
|
184
|
-
|
|
185
|
-
const comments = filterComments(rawComments, options);
|
|
186
|
-
const context = buildContextFromBacklog(issue, comments, issueKey, domain);
|
|
187
|
-
context.mode = options.full ? 'full' : 'fast';
|
|
188
|
-
await saveContext(context, options.save);
|
|
189
|
-
printContextSummary(context);
|
|
190
|
-
await suggestNextStep();
|
|
191
|
-
} catch (err) {
|
|
192
|
-
console.log(chalk.yellow(`⚠ Could not fetch from Backlog: ${err.message}`));
|
|
193
|
-
console.log(chalk.gray('Falling back to manual entry...\n'));
|
|
194
|
-
await manualContext(issueKey);
|
|
210
|
+
const [issue, rawComments] = await Promise.all([
|
|
211
|
+
fetchBacklogIssue(domain, apiKey, issueKey),
|
|
212
|
+
loadComments
|
|
213
|
+
? fetchBacklogComments(domain, apiKey, issueKey)
|
|
214
|
+
: Promise.resolve([]),
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
const comments = filterComments(rawComments, options);
|
|
218
|
+
if (loadComments) {
|
|
219
|
+
console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
|
|
195
220
|
}
|
|
221
|
+
const context = buildContextFromBacklog(issue, comments, issueKey, domain);
|
|
222
|
+
context.mode = options.full ? "full" : "fast";
|
|
223
|
+
await saveContext(context, options.save);
|
|
224
|
+
printContextSummary(context);
|
|
225
|
+
await suggestNextStep();
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.log(chalk.yellow(`⚠ Could not fetch from Backlog: ${err.message}`));
|
|
228
|
+
console.log(chalk.gray("Falling back to manual entry...\n"));
|
|
229
|
+
await manualContext(issueKey);
|
|
230
|
+
}
|
|
196
231
|
}
|
|
197
232
|
|
|
198
233
|
/**
|
|
@@ -203,120 +238,160 @@ async function loadFromBacklog(issueKey, options = {}) {
|
|
|
203
238
|
* (default) → all comments
|
|
204
239
|
*/
|
|
205
240
|
function filterComments(comments, options = {}) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
if (!comments || !comments.length) return [];
|
|
242
|
+
|
|
243
|
+
let result = [...comments];
|
|
244
|
+
|
|
245
|
+
// 1. Filter by specific ID if provided
|
|
246
|
+
const targetId = options.cid || options.commentId || options["comment-id"];
|
|
247
|
+
if (targetId) {
|
|
248
|
+
result = result.filter((c) => String(c.id) === String(targetId));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 2. Filter by starting ID (from)
|
|
252
|
+
const fromId = options.cfrom || options.commentsFrom || options["comments-from"];
|
|
253
|
+
if (fromId) {
|
|
254
|
+
result = result.filter((c) => Number(c.id) >= Number(fromId));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 3. Filter by ending ID (to)
|
|
258
|
+
const toId = options.cto || options.commentsTo || options["comments-to"];
|
|
259
|
+
if (toId) {
|
|
260
|
+
result = result.filter((c) => Number(c.id) <= Number(toId));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 4. Filter by last N comments (if not using specific ID/range filters)
|
|
264
|
+
const lastN = options.clast || options.commentsLast;
|
|
265
|
+
if (lastN != null && !targetId && !fromId && !toId) {
|
|
266
|
+
const n = Math.max(1, lastN);
|
|
267
|
+
return result.slice(-n);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
219
271
|
}
|
|
220
272
|
|
|
221
273
|
/**
|
|
222
274
|
* Generic Backlog GET helper
|
|
223
275
|
*/
|
|
224
276
|
function backlogGet(url) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
})
|
|
241
|
-
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
https
|
|
279
|
+
.get(url, (res) => {
|
|
280
|
+
let data = "";
|
|
281
|
+
res.on("data", (chunk) => (data += chunk));
|
|
282
|
+
res.on("end", () => {
|
|
283
|
+
if (res.statusCode !== 200) {
|
|
284
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
resolve(JSON.parse(data));
|
|
289
|
+
} catch (e) {
|
|
290
|
+
reject(new Error("Invalid JSON response from Backlog"));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
})
|
|
294
|
+
.on("error", reject);
|
|
295
|
+
});
|
|
242
296
|
}
|
|
243
297
|
|
|
244
298
|
/**
|
|
245
299
|
* Fetch issue detail from Backlog REST API
|
|
246
300
|
*/
|
|
247
301
|
function fetchBacklogIssue(domain, apiKey, issueKey) {
|
|
248
|
-
|
|
302
|
+
return backlogGet(
|
|
303
|
+
`https://${domain}/api/v2/issues/${issueKey}?apiKey=${apiKey}`,
|
|
304
|
+
);
|
|
249
305
|
}
|
|
250
306
|
|
|
251
307
|
/**
|
|
252
|
-
* Fetch ALL comments of an issue from Backlog REST API
|
|
308
|
+
* Fetch ALL comments of an issue from Backlog REST API.
|
|
309
|
+
* Uses minId for pagination because 'offset' is not supported for comments.
|
|
253
310
|
*/
|
|
254
311
|
async function fetchBacklogComments(domain, apiKey, issueKey) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
312
|
+
const all = [];
|
|
313
|
+
let minId = null;
|
|
314
|
+
try {
|
|
315
|
+
while (true) {
|
|
316
|
+
let url = `https://${domain}/api/v2/issues/${issueKey}/comments?apiKey=${apiKey}&count=100&order=asc`;
|
|
317
|
+
if (minId) {
|
|
318
|
+
url += `&minId=${minId}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const batch = await backlogGet(url);
|
|
322
|
+
if (!batch || batch.length === 0) break;
|
|
323
|
+
|
|
324
|
+
all.push(...batch);
|
|
325
|
+
if (batch.length < 100) break;
|
|
326
|
+
|
|
327
|
+
// Use the last ID as minId for the next batch
|
|
328
|
+
minId = batch[batch.length - 1].id;
|
|
268
329
|
}
|
|
269
|
-
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.log(
|
|
332
|
+
chalk.yellow(` ⚠ Warning: Could not fetch comments: ${err.message}`),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
return all;
|
|
270
336
|
}
|
|
271
337
|
|
|
272
338
|
/**
|
|
273
339
|
* Transform Backlog API response + comments to internal context format
|
|
274
340
|
*/
|
|
275
341
|
function buildContextFromBacklog(issue, comments, issueKey, domain) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
342
|
+
const type = detectTaskType(issue);
|
|
343
|
+
|
|
344
|
+
// Format comments into readable text for AI context
|
|
345
|
+
const formattedComments = (comments || [])
|
|
346
|
+
.map((c) => {
|
|
347
|
+
// Backlog comments can have text in 'content' or be just changelogs
|
|
348
|
+
const text = c.content || "";
|
|
349
|
+
if (!text.trim()) return null;
|
|
350
|
+
|
|
351
|
+
const author = c.createdUser?.name || "Unknown";
|
|
352
|
+
const date = c.created
|
|
353
|
+
? new Date(c.created).toLocaleDateString("vi-VN")
|
|
354
|
+
: "";
|
|
355
|
+
return `[${author} - ${date}]\n${text.trim()}`;
|
|
356
|
+
})
|
|
357
|
+
.filter(Boolean);
|
|
358
|
+
|
|
359
|
+
const allText = [
|
|
360
|
+
issue.description || "",
|
|
361
|
+
...formattedComments.map((c) => c),
|
|
362
|
+
].join("\n");
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
taskId: issueKey,
|
|
366
|
+
taskType: type,
|
|
367
|
+
title: issue.summary || "",
|
|
368
|
+
description: issue.description || "",
|
|
369
|
+
status: issue.status?.name || "Unknown",
|
|
370
|
+
priority: issue.priority?.name || "Unknown",
|
|
371
|
+
assignee: issue.assignee?.name || "Unassigned",
|
|
372
|
+
|
|
373
|
+
// Comments from all participants
|
|
374
|
+
comments: formattedComments,
|
|
375
|
+
commentCount: formattedComments.length,
|
|
376
|
+
|
|
377
|
+
// Parse checklist-style acceptance criteria from description
|
|
378
|
+
acceptanceCriteria: extractCriteria(issue.description || ""),
|
|
379
|
+
|
|
380
|
+
// Related files mentioned anywhere in ticket
|
|
381
|
+
context: {
|
|
382
|
+
files: extractFiles(allText),
|
|
383
|
+
mentionedServices: [],
|
|
384
|
+
relatedTickets: extractRelatedTickets(allText),
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
metadata: {
|
|
388
|
+
created: issue.created,
|
|
389
|
+
updated: issue.updated,
|
|
390
|
+
loadedFrom: "backlog",
|
|
391
|
+
adapter: "backlog",
|
|
392
|
+
sourceUrl: `https://${domain}/view/${issueKey}`,
|
|
393
|
+
},
|
|
394
|
+
};
|
|
320
395
|
}
|
|
321
396
|
|
|
322
397
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -324,167 +399,282 @@ function buildContextFromBacklog(issue, comments, issueKey, domain) {
|
|
|
324
399
|
// ──────────────────────────────────────────────────────────────
|
|
325
400
|
|
|
326
401
|
async function loadFromJira(issueKey, options = {}) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
402
|
+
const creds = await loadCredentials();
|
|
403
|
+
const apiToken = process.env.JIRA_API_TOKEN || creds.JIRA_API_TOKEN;
|
|
404
|
+
const email = process.env.JIRA_EMAIL || creds.JIRA_EMAIL;
|
|
405
|
+
const domain = process.env.JIRA_DOMAIN || creds.JIRA_DOMAIN;
|
|
406
|
+
|
|
407
|
+
if (!apiToken || !email || !domain) {
|
|
408
|
+
console.log(chalk.yellow("⚠ Jira credentials not set."));
|
|
409
|
+
console.log(chalk.gray("Run: aiflow init --adapter jira"));
|
|
410
|
+
console.log(
|
|
411
|
+
chalk.gray(
|
|
412
|
+
"Or set environment variables: JIRA_API_TOKEN, JIRA_EMAIL, JIRA_DOMAIN",
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
console.log(chalk.gray("\nFalling back to manual entry...\n"));
|
|
416
|
+
return await manualContext(issueKey);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(chalk.blue(`Fetching context from Jira: ${issueKey}...`));
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const loadComments =
|
|
423
|
+
options.coms ||
|
|
424
|
+
options.withComs ||
|
|
425
|
+
options.withComments ||
|
|
426
|
+
options["with-comments"] ||
|
|
427
|
+
options.with_comments ||
|
|
428
|
+
options.cid != null ||
|
|
429
|
+
options.commentId != null ||
|
|
430
|
+
options["comment-id"] != null ||
|
|
431
|
+
options.clast != null ||
|
|
432
|
+
options.commentsLast != null ||
|
|
433
|
+
options.cfrom != null ||
|
|
434
|
+
options.commentsFrom != null ||
|
|
435
|
+
options.cto != null ||
|
|
436
|
+
options.commentsTo != null ||
|
|
437
|
+
options["comments-to"] != null;
|
|
438
|
+
|
|
439
|
+
if (loadComments) {
|
|
440
|
+
console.log(chalk.gray(" ℹ Comments requested..."));
|
|
338
441
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} catch (err) {
|
|
350
|
-
console.log(chalk.yellow(`⚠ Could not fetch from Jira: ${err.message}`));
|
|
351
|
-
console.log(chalk.gray('Falling back to manual entry...\n'));
|
|
352
|
-
await manualContext(issueKey);
|
|
442
|
+
const [issue, rawComments] = await Promise.all([
|
|
443
|
+
fetchJiraIssue(domain, email, apiToken, issueKey),
|
|
444
|
+
loadComments
|
|
445
|
+
? fetchJiraComments(domain, email, apiToken, issueKey)
|
|
446
|
+
: Promise.resolve([]),
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
const comments = filterComments(rawComments, options);
|
|
450
|
+
if (loadComments) {
|
|
451
|
+
console.log(chalk.gray(` ℹ Comments: ${rawComments.length} fetched, ${comments.length} kept after filtering.`));
|
|
353
452
|
}
|
|
453
|
+
const context = buildContextFromJira(issue, comments, issueKey, domain);
|
|
454
|
+
context.mode = options.full ? "full" : "fast";
|
|
455
|
+
await saveContext(context, options.save);
|
|
456
|
+
printContextSummary(context);
|
|
457
|
+
await suggestNextStep();
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.log(chalk.yellow(`⚠ Could not fetch from Jira: ${err.message}`));
|
|
460
|
+
console.log(chalk.gray("Falling back to manual entry...\n"));
|
|
461
|
+
await manualContext(issueKey);
|
|
462
|
+
}
|
|
354
463
|
}
|
|
355
464
|
|
|
356
465
|
function fetchJiraIssue(domain, email, apiToken, issueKey) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
})
|
|
382
|
-
|
|
466
|
+
return new Promise((resolve, reject) => {
|
|
467
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
468
|
+
const url = `https://${domain}.atlassian.net/rest/api/3/issue/${issueKey}`;
|
|
469
|
+
const options = {
|
|
470
|
+
headers: {
|
|
471
|
+
Authorization: `Basic ${auth}`,
|
|
472
|
+
Accept: "application/json",
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
https
|
|
477
|
+
.get(url, options, (res) => {
|
|
478
|
+
let data = "";
|
|
479
|
+
res.on("data", (chunk) => (data += chunk));
|
|
480
|
+
res.on("end", () => {
|
|
481
|
+
if (res.statusCode !== 200) {
|
|
482
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
resolve(JSON.parse(data));
|
|
487
|
+
} catch (e) {
|
|
488
|
+
reject(new Error("Invalid JSON response from Jira"));
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
})
|
|
492
|
+
.on("error", reject);
|
|
493
|
+
});
|
|
383
494
|
}
|
|
384
495
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
priority: fields.priority?.name || 'Unknown',
|
|
398
|
-
assignee: fields.assignee?.displayName || 'Unassigned',
|
|
399
|
-
acceptanceCriteria: extractCriteria(fields.description?.toString() || ''),
|
|
400
|
-
context: {
|
|
401
|
-
files: [],
|
|
402
|
-
relatedTickets: []
|
|
403
|
-
},
|
|
404
|
-
metadata: {
|
|
405
|
-
created: fields.created,
|
|
406
|
-
updated: fields.updated,
|
|
407
|
-
loadedFrom: 'jira',
|
|
408
|
-
adapter: 'jira',
|
|
409
|
-
sourceUrl: `https://${domain}.atlassian.net/browse/${issueKey}`
|
|
410
|
-
}
|
|
496
|
+
/**
|
|
497
|
+
* Fetch ALL comments of an issue from Jira REST API.
|
|
498
|
+
*/
|
|
499
|
+
async function fetchJiraComments(domain, email, apiToken, issueKey) {
|
|
500
|
+
return new Promise((resolve, reject) => {
|
|
501
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
502
|
+
const url = `https://${domain}.atlassian.net/rest/api/3/issue/${issueKey}/comment`;
|
|
503
|
+
const options = {
|
|
504
|
+
headers: {
|
|
505
|
+
Authorization: `Basic ${auth}`,
|
|
506
|
+
Accept: "application/json",
|
|
507
|
+
},
|
|
411
508
|
};
|
|
509
|
+
|
|
510
|
+
https
|
|
511
|
+
.get(url, options, (res) => {
|
|
512
|
+
let data = "";
|
|
513
|
+
res.on("data", (chunk) => (data += chunk));
|
|
514
|
+
res.on("end", () => {
|
|
515
|
+
if (res.statusCode !== 200) {
|
|
516
|
+
// Comments are optional, but log if we failed
|
|
517
|
+
console.log(
|
|
518
|
+
chalk.yellow(
|
|
519
|
+
` ⚠ Warning: Jira comments API returned ${res.statusCode}`,
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
resolve([]);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const json = JSON.parse(data);
|
|
527
|
+
resolve(json.comments || []);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
resolve([]);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
})
|
|
533
|
+
.on("error", () => resolve([]));
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function buildContextFromJira(issue, comments, issueKey, domain) {
|
|
538
|
+
const fields = issue.fields || {};
|
|
539
|
+
const type = detectTaskTypeFromString(fields.issuetype?.name || "");
|
|
540
|
+
|
|
541
|
+
// Format comments for Jira (v3 uses ADF format for body)
|
|
542
|
+
const formattedComments = (comments || [])
|
|
543
|
+
.map((c) => {
|
|
544
|
+
const author = c.author?.displayName || "Unknown";
|
|
545
|
+
const date = c.created
|
|
546
|
+
? new Date(c.created).toLocaleDateString("vi-VN")
|
|
547
|
+
: "";
|
|
548
|
+
const body =
|
|
549
|
+
c.body?.content
|
|
550
|
+
?.map(
|
|
551
|
+
(block) => block.content?.map((inner) => inner.text).join("") || "",
|
|
552
|
+
)
|
|
553
|
+
.join("\n") || "";
|
|
554
|
+
return body.trim() ? `[${author} - ${date}]\n${body.trim()}` : null;
|
|
555
|
+
})
|
|
556
|
+
.filter(Boolean);
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
taskId: issueKey,
|
|
560
|
+
taskType: type,
|
|
561
|
+
title: fields.summary || "",
|
|
562
|
+
description:
|
|
563
|
+
fields.description?.content
|
|
564
|
+
?.map((block) => block.content?.map((c) => c.text).join("") || "")
|
|
565
|
+
.join("\n") || "",
|
|
566
|
+
status: fields.status?.name || "Unknown",
|
|
567
|
+
priority: fields.priority?.name || "Unknown",
|
|
568
|
+
assignee: fields.assignee?.displayName || "Unassigned",
|
|
569
|
+
|
|
570
|
+
// Comments
|
|
571
|
+
comments: formattedComments,
|
|
572
|
+
commentCount: formattedComments.length,
|
|
573
|
+
|
|
574
|
+
acceptanceCriteria: extractCriteria(fields.description?.toString() || ""),
|
|
575
|
+
context: {
|
|
576
|
+
files: [],
|
|
577
|
+
relatedTickets: [],
|
|
578
|
+
},
|
|
579
|
+
metadata: {
|
|
580
|
+
created: fields.created,
|
|
581
|
+
updated: fields.updated,
|
|
582
|
+
loadedFrom: "jira",
|
|
583
|
+
adapter: "jira",
|
|
584
|
+
sourceUrl: `https://${domain}.atlassian.net/browse/${issueKey}`,
|
|
585
|
+
},
|
|
586
|
+
};
|
|
412
587
|
}
|
|
413
588
|
|
|
414
589
|
// ──────────────────────────────────────────────────────────────
|
|
415
590
|
// Manual context entry
|
|
416
591
|
// ──────────────────────────────────────────────────────────────
|
|
417
592
|
|
|
418
|
-
async function manualContext(prefillId =
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
593
|
+
async function manualContext(prefillId = "") {
|
|
594
|
+
// Load existing context for pre-fill (Edit mode)
|
|
595
|
+
let existing = {};
|
|
596
|
+
const currentPath = path.join(CONTEXT_DIR, "current.json");
|
|
597
|
+
if (await fs.pathExists(currentPath)) {
|
|
598
|
+
existing = await fs.readJson(currentPath).catch(() => ({}));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const isEdit = !!existing.taskId;
|
|
602
|
+
if (isEdit) {
|
|
603
|
+
console.log(chalk.cyan("\nEdit Context\n"));
|
|
604
|
+
console.log(
|
|
605
|
+
chalk.gray("Press Enter to keep existing value shown in [brackets]\n"),
|
|
606
|
+
);
|
|
607
|
+
} else {
|
|
608
|
+
console.log(chalk.cyan("\nManual Context Entry\n"));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Ticket ID
|
|
612
|
+
const defaultId = prefillId || existing.taskId || "";
|
|
613
|
+
const idHint = defaultId ? chalk.gray(` [${defaultId}]`) : "";
|
|
614
|
+
const idInput = await input({
|
|
615
|
+
message: `Ticket ID (e.g. PROJ-33)${idHint}:`,
|
|
616
|
+
default: "",
|
|
617
|
+
});
|
|
618
|
+
const taskId = idInput.trim() || defaultId;
|
|
619
|
+
|
|
620
|
+
// Title
|
|
621
|
+
const defaultTitle = existing.title || "";
|
|
622
|
+
const titlePreview =
|
|
623
|
+
defaultTitle.length > 50
|
|
624
|
+
? defaultTitle.substring(0, 50) + "…"
|
|
625
|
+
: defaultTitle;
|
|
626
|
+
const titleHint = defaultTitle ? chalk.gray(` [${titlePreview}]`) : "";
|
|
627
|
+
const titleInput = await input({
|
|
628
|
+
message: `Title${titleHint}:`,
|
|
629
|
+
default: "",
|
|
630
|
+
});
|
|
631
|
+
const title = titleInput.trim() || defaultTitle;
|
|
632
|
+
|
|
633
|
+
// Description
|
|
634
|
+
const defaultDesc = existing.description || "";
|
|
635
|
+
const descPreview =
|
|
636
|
+
defaultDesc.length > 60 ? defaultDesc.substring(0, 60) + "…" : defaultDesc;
|
|
637
|
+
const descHint = defaultDesc ? chalk.gray(` [${descPreview}]`) : "";
|
|
638
|
+
const descInput = await input({
|
|
639
|
+
message: `Description (brief)${descHint}:`,
|
|
640
|
+
default: "",
|
|
641
|
+
});
|
|
642
|
+
const description = descInput.trim() || defaultDesc;
|
|
643
|
+
|
|
644
|
+
// Task type — pre-select existing value if available
|
|
645
|
+
const taskType = await select({
|
|
646
|
+
message: "Task type:",
|
|
647
|
+
choices: [
|
|
648
|
+
{ name: "🐛 Bug Fix", value: "bug-fix" },
|
|
649
|
+
{ name: "✨ Feature", value: "feature" },
|
|
650
|
+
{ name: "🔍 Investigation", value: "investigation" },
|
|
651
|
+
{ name: "♻️ Refactor", value: "refactor" },
|
|
652
|
+
{ name: "📊 Impact Analysis", value: "impact-analysis" },
|
|
653
|
+
{ name: "📖 Documentation", value: "documentation" },
|
|
654
|
+
],
|
|
655
|
+
default: existing.taskType || undefined,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const context = {
|
|
659
|
+
taskId,
|
|
660
|
+
taskType,
|
|
661
|
+
title,
|
|
662
|
+
description,
|
|
663
|
+
status: existing.status || "In Progress",
|
|
664
|
+
mode: existing.mode || "auto",
|
|
665
|
+
acceptanceCriteria: existing.acceptanceCriteria || [],
|
|
666
|
+
context: existing.context || { files: [], relatedTickets: [] },
|
|
667
|
+
metadata: {
|
|
668
|
+
created: existing.metadata?.created || new Date().toISOString(),
|
|
669
|
+
updated: new Date().toISOString(),
|
|
670
|
+
loadedFrom: "manual",
|
|
671
|
+
adapter: "manual",
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
await saveContext(context);
|
|
676
|
+
printContextSummary(context);
|
|
677
|
+
await suggestNextStep();
|
|
488
678
|
}
|
|
489
679
|
|
|
490
680
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -492,94 +682,104 @@ async function manualContext(prefillId = '') {
|
|
|
492
682
|
// ──────────────────────────────────────────────────────────────
|
|
493
683
|
|
|
494
684
|
async function loadFromFile(filePath, options = {}) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
685
|
+
if (!(await fs.pathExists(filePath))) {
|
|
686
|
+
console.log(chalk.red(`File not found: ${filePath}`));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
691
|
+
let context;
|
|
692
|
+
|
|
693
|
+
// Try JSON first; fall back to plain-text auto-detection
|
|
694
|
+
try {
|
|
695
|
+
context = JSON.parse(raw);
|
|
696
|
+
} catch (_) {
|
|
697
|
+
console.log(
|
|
698
|
+
chalk.gray(
|
|
699
|
+
" File is not JSON — auto-detecting task info from content...",
|
|
700
|
+
),
|
|
701
|
+
);
|
|
702
|
+
context = buildContextFromPlainText(raw, filePath);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
context.mode = options.full ? "full" : "fast";
|
|
706
|
+
|
|
707
|
+
// Prompt for task type
|
|
708
|
+
context.taskType = await select({
|
|
709
|
+
message: "Task type:",
|
|
710
|
+
choices: [
|
|
711
|
+
{ name: "🐛 Bug Fix", value: "bug-fix" },
|
|
712
|
+
{ name: "✨ Feature", value: "feature" },
|
|
713
|
+
{ name: "🔍 Investigation", value: "investigation" },
|
|
714
|
+
{ name: "♻️ Refactor", value: "refactor" },
|
|
715
|
+
{ name: "📊 Impact Analysis", value: "impact-analysis" },
|
|
716
|
+
{ name: "📖 Documentation", value: "documentation" },
|
|
717
|
+
],
|
|
718
|
+
default: context.taskType || "feature",
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
await saveContext(context, options.save);
|
|
722
|
+
printContextSummary(context);
|
|
723
|
+
await suggestNextStep();
|
|
530
724
|
}
|
|
531
725
|
|
|
532
726
|
/**
|
|
533
727
|
* Build a minimal context object from arbitrary plain-text file content.
|
|
534
728
|
* Tries to detect ticket ID (e.g. PROJ-33, APP-123) and a title from the first non-blank line.
|
|
535
729
|
*/
|
|
536
|
-
function buildContextFromPlainText(text, filePath =
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
730
|
+
function buildContextFromPlainText(text, filePath = "") {
|
|
731
|
+
// Detect ticket ID: first PROJ-123 style token anywhere in the text
|
|
732
|
+
const idMatch = text.match(/\b([A-Z][A-Z0-9_]+-\d+)\b/);
|
|
733
|
+
|
|
734
|
+
let taskId;
|
|
735
|
+
if (idMatch) {
|
|
736
|
+
taskId = idMatch[1];
|
|
737
|
+
} else {
|
|
738
|
+
const fileNameOnly = filePath
|
|
739
|
+
? path.basename(filePath, path.extname(filePath))
|
|
740
|
+
: "";
|
|
741
|
+
const cleanWords = fileNameOnly
|
|
742
|
+
.replace(/[^a-zA-Z0-9]/g, " ")
|
|
743
|
+
.split(/\s+/)
|
|
744
|
+
.filter(Boolean)
|
|
745
|
+
.slice(0, 5)
|
|
746
|
+
.join("-")
|
|
747
|
+
.toUpperCase();
|
|
748
|
+
const prefix = cleanWords ? `TASK-${cleanWords}-` : "TASK-";
|
|
749
|
+
taskId = `${prefix}${Date.now()}`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Title: use full filename if available, otherwise first non-blank line
|
|
753
|
+
const fullFileName = filePath ? path.basename(filePath) : "";
|
|
754
|
+
const lines = text
|
|
755
|
+
.split(/\r?\n/)
|
|
756
|
+
.map((l) => l.trim())
|
|
757
|
+
.filter(Boolean);
|
|
758
|
+
const titleLine = fullFileName || lines.find((l) => l.length > 3) || taskId;
|
|
759
|
+
const title = titleLine.substring(0, 120);
|
|
760
|
+
|
|
761
|
+
// Task type detection from text
|
|
762
|
+
const taskType = detectTaskTypeFromString(text);
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
taskId,
|
|
766
|
+
taskType,
|
|
767
|
+
title,
|
|
768
|
+
description: text.substring(0, 3000),
|
|
769
|
+
status: "In Progress",
|
|
770
|
+
assignee: "Unassigned",
|
|
771
|
+
acceptanceCriteria: extractCriteria(text),
|
|
772
|
+
context: {
|
|
773
|
+
files: extractFiles(text),
|
|
774
|
+
relatedTickets: extractRelatedTickets(text),
|
|
775
|
+
},
|
|
776
|
+
metadata: {
|
|
777
|
+
created: new Date().toISOString(),
|
|
778
|
+
updated: new Date().toISOString(),
|
|
779
|
+
loadedFrom: "file",
|
|
780
|
+
adapter: "file",
|
|
781
|
+
},
|
|
782
|
+
};
|
|
583
783
|
}
|
|
584
784
|
|
|
585
785
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -587,17 +787,25 @@ function buildContextFromPlainText(text, filePath = '') {
|
|
|
587
787
|
// ──────────────────────────────────────────────────────────────
|
|
588
788
|
|
|
589
789
|
async function loadCredentials() {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
790
|
+
const globalCredsFile = path.join(
|
|
791
|
+
os.homedir(),
|
|
792
|
+
".aiflow",
|
|
793
|
+
"credentials.json",
|
|
794
|
+
);
|
|
795
|
+
if (await fs.pathExists(globalCredsFile)) {
|
|
796
|
+
const data = await fs.readJson(globalCredsFile).catch(() => ({}));
|
|
797
|
+
if (data.mcp && Object.keys(data.mcp).length > 0) return data.mcp;
|
|
798
|
+
}
|
|
799
|
+
// Fallback: project-level credentials (legacy support)
|
|
800
|
+
const projectCredsFile = path.join(
|
|
801
|
+
PROJECT_DIR,
|
|
802
|
+
".aiflow",
|
|
803
|
+
"credentials.json",
|
|
804
|
+
);
|
|
805
|
+
if (await fs.pathExists(projectCredsFile)) {
|
|
806
|
+
return await fs.readJson(projectCredsFile).catch(() => ({}));
|
|
807
|
+
}
|
|
808
|
+
return {};
|
|
601
809
|
}
|
|
602
810
|
|
|
603
811
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -605,69 +813,85 @@ async function loadCredentials() {
|
|
|
605
813
|
// ──────────────────────────────────────────────────────────────
|
|
606
814
|
|
|
607
815
|
async function saveContext(context, saveName) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// Save named snapshot if requested
|
|
650
|
-
if (saveName) {
|
|
651
|
-
const namedFile = path.join(CONTEXT_DIR, 'history', `${saveName}.json`);
|
|
652
|
-
await fs.writeJson(namedFile, context, { spaces: 2 });
|
|
653
|
-
console.log(chalk.gray(` Context also saved as: ${saveName}`));
|
|
816
|
+
await fs.ensureDir(CONTEXT_DIR);
|
|
817
|
+
await fs.ensureDir(path.join(CONTEXT_DIR, "history"));
|
|
818
|
+
|
|
819
|
+
// Auto-pause any currently active task that differs from this one
|
|
820
|
+
const currentFile = path.join(CONTEXT_DIR, "current.json");
|
|
821
|
+
if (context.taskId && (await fs.pathExists(currentFile))) {
|
|
822
|
+
const prev = await fs.readJson(currentFile).catch(() => null);
|
|
823
|
+
if (prev && prev.taskId && prev.taskId !== context.taskId) {
|
|
824
|
+
try {
|
|
825
|
+
const { createOrActivateTaskState } = require("./task");
|
|
826
|
+
// Save previous task state as pending before switching
|
|
827
|
+
const prevTaskDir = path.join(AIFLOW_DIR, "tasks", prev.taskId);
|
|
828
|
+
const prevStatePath = path.join(prevTaskDir, "task-state.json");
|
|
829
|
+
await fs.ensureDir(prevTaskDir);
|
|
830
|
+
await fs.writeJson(path.join(prevTaskDir, "context.json"), prev, {
|
|
831
|
+
spaces: 2,
|
|
832
|
+
});
|
|
833
|
+
const prevState = (await fs.pathExists(prevStatePath))
|
|
834
|
+
? await fs.readJson(prevStatePath).catch(() => ({}))
|
|
835
|
+
: {};
|
|
836
|
+
const now = new Date().toISOString();
|
|
837
|
+
await fs.writeJson(
|
|
838
|
+
prevStatePath,
|
|
839
|
+
{
|
|
840
|
+
...prevState,
|
|
841
|
+
taskId: prev.taskId,
|
|
842
|
+
title: prev.title || "",
|
|
843
|
+
status: "pending",
|
|
844
|
+
updatedAt: now,
|
|
845
|
+
pausedAt: now,
|
|
846
|
+
currentGate: prevState.currentGate || 1,
|
|
847
|
+
gateApprovals: prevState.gateApprovals || {},
|
|
848
|
+
notes: prevState.notes || "",
|
|
849
|
+
createdAt: prevState.createdAt || now,
|
|
850
|
+
},
|
|
851
|
+
{ spaces: 2 },
|
|
852
|
+
);
|
|
853
|
+
console.log(chalk.gray(` Auto-paused previous task: ${prev.taskId}`));
|
|
854
|
+
} catch (_) {
|
|
855
|
+
/* task module not available */
|
|
856
|
+
}
|
|
654
857
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Save as current
|
|
861
|
+
await fs.writeJson(currentFile, context, { spaces: 2 });
|
|
862
|
+
|
|
863
|
+
// Save to history
|
|
864
|
+
const histFile = path.join(
|
|
865
|
+
CONTEXT_DIR,
|
|
866
|
+
"history",
|
|
867
|
+
`${context.taskId || "manual"}.json`,
|
|
868
|
+
);
|
|
869
|
+
await fs.writeJson(histFile, context, { spaces: 2 });
|
|
870
|
+
|
|
871
|
+
// Save named snapshot if requested
|
|
872
|
+
if (saveName) {
|
|
873
|
+
const namedFile = path.join(CONTEXT_DIR, "history", `${saveName}.json`);
|
|
874
|
+
await fs.writeJson(namedFile, context, { spaces: 2 });
|
|
875
|
+
console.log(chalk.gray(` Context also saved as: ${saveName}`));
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Create / activate task-state for the new task
|
|
879
|
+
if (context.taskId) {
|
|
880
|
+
try {
|
|
881
|
+
const { createOrActivateTaskState } = require("./task");
|
|
882
|
+
await createOrActivateTaskState(context);
|
|
883
|
+
} catch (_) {
|
|
884
|
+
/* task module not available */
|
|
662
885
|
}
|
|
886
|
+
}
|
|
663
887
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
888
|
+
// Update state
|
|
889
|
+
if (await fs.pathExists(STATE_FILE)) {
|
|
890
|
+
const state = await fs.readJson(STATE_FILE);
|
|
891
|
+
state.current_context = context.taskId;
|
|
892
|
+
state.current_context_type = context.taskType;
|
|
893
|
+
await fs.writeJson(STATE_FILE, state);
|
|
894
|
+
}
|
|
671
895
|
}
|
|
672
896
|
|
|
673
897
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -675,55 +899,66 @@ async function saveContext(context, saveName) {
|
|
|
675
899
|
// ──────────────────────────────────────────────────────────────
|
|
676
900
|
|
|
677
901
|
function detectTaskType(issue) {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
902
|
+
const name = issue.issueType?.name || issue.type?.name || "";
|
|
903
|
+
const summary = issue.summary || "";
|
|
904
|
+
return detectTaskTypeFromString(name || summary);
|
|
681
905
|
}
|
|
682
906
|
|
|
683
907
|
function detectTaskTypeFromString(text) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return
|
|
908
|
+
const lower = text.toLowerCase();
|
|
909
|
+
if (["bug", "defect", "バグ"].some((k) => lower.includes(k)))
|
|
910
|
+
return "bug-fix";
|
|
911
|
+
if (["task", "story", "feature", "タスク"].some((k) => lower.includes(k)))
|
|
912
|
+
return "feature";
|
|
913
|
+
if (["research", "investigation", "調査"].some((k) => lower.includes(k)))
|
|
914
|
+
return "investigation";
|
|
915
|
+
return "bug-fix"; // default for unknown
|
|
689
916
|
}
|
|
690
917
|
|
|
691
918
|
function extractCriteria(text) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
919
|
+
const lines = text.split("\n");
|
|
920
|
+
return lines
|
|
921
|
+
.filter((l) => l.trim().match(/^[-*•]|^\d+\./))
|
|
922
|
+
.map((l) => l.replace(/^[-*•\d.]+\s*/, "").trim())
|
|
923
|
+
.filter((l) => l.length > 0)
|
|
924
|
+
.slice(0, 10);
|
|
698
925
|
}
|
|
699
926
|
|
|
700
927
|
function extractFiles(text) {
|
|
701
|
-
|
|
702
|
-
|
|
928
|
+
const matches = text.match(/[\w/]+\.(php|js|ts|vue|jsx|tsx|css|html)/g) || [];
|
|
929
|
+
return [...new Set(matches)];
|
|
703
930
|
}
|
|
704
931
|
|
|
705
932
|
function extractRelatedTickets(text) {
|
|
706
|
-
|
|
707
|
-
|
|
933
|
+
const matches = text.match(/[A-Z][A-Z0-9_]+-\d+/g) || [];
|
|
934
|
+
return [...new Set(matches)];
|
|
708
935
|
}
|
|
709
936
|
|
|
710
937
|
function printContextSummary(context) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
938
|
+
console.log(chalk.green("\n✓ Context loaded\n"));
|
|
939
|
+
console.log(` ${chalk.white("Ticket:")} ${context.taskId}`);
|
|
940
|
+
console.log(` ${chalk.white("Type:")} ${context.taskType}`);
|
|
941
|
+
console.log(
|
|
942
|
+
` ${chalk.white("Title:")} ${context.title.substring(0, 70)}`,
|
|
943
|
+
);
|
|
944
|
+
console.log(` ${chalk.white("Status:")} ${context.status}`);
|
|
945
|
+
console.log(` ${chalk.white("Assignee:")} ${context.assignee}`);
|
|
946
|
+
if (context.description) {
|
|
947
|
+
console.log(
|
|
948
|
+
` ${chalk.white("Desc:")} ${context.description.substring(0, 80)}...`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
if (context.acceptanceCriteria?.length) {
|
|
952
|
+
console.log(
|
|
953
|
+
` ${chalk.white("Criteria:")} ${context.acceptanceCriteria.length} item(s)`,
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (context.commentCount > 0) {
|
|
957
|
+
console.log(
|
|
958
|
+
` ${chalk.white("Comments:")} ${context.commentCount} comment(s) loaded`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
console.log();
|
|
727
962
|
}
|
|
728
963
|
|
|
729
964
|
// ── GitNexus index status notification ───────────────────────────
|
|
@@ -732,70 +967,90 @@ function printContextSummary(context) {
|
|
|
732
967
|
const GITNEXUS_STALE_MS = 3 * 60 * 60 * 1000; // 3 hours — if still "running" after this, process died
|
|
733
968
|
|
|
734
969
|
async function checkGitNexusIndexStatus() {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
970
|
+
const statusPath = path.join(AIFLOW_DIR, "gitnexus-status.json");
|
|
971
|
+
try {
|
|
972
|
+
if (!(await fs.pathExists(statusPath))) return;
|
|
973
|
+
const gn = await fs.readJson(statusPath);
|
|
974
|
+
if (gn.notified) return;
|
|
975
|
+
|
|
976
|
+
// Detect stale "running" — process was killed without updating status
|
|
977
|
+
if (gn.status === "running" && gn.startedAt) {
|
|
978
|
+
const age = Date.now() - new Date(gn.startedAt).getTime();
|
|
979
|
+
if (age > GITNEXUS_STALE_MS) {
|
|
980
|
+
gn.status = "error";
|
|
981
|
+
gn.error =
|
|
982
|
+
"timed out — process may have been killed or ran out of memory";
|
|
983
|
+
gn.notified = false;
|
|
984
|
+
await fs.writeJson(statusPath, gn, { spaces: 2 });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (gn.status === "done") {
|
|
989
|
+
console.log(
|
|
990
|
+
chalk.green(
|
|
991
|
+
"✓ GitNexus indexing complete — code intelligence tools are ready.",
|
|
992
|
+
),
|
|
993
|
+
);
|
|
994
|
+
gn.notified = true;
|
|
995
|
+
await fs.writeJson(statusPath, gn, { spaces: 2 });
|
|
996
|
+
} else if (gn.status === "error") {
|
|
997
|
+
const detail = gn.error
|
|
998
|
+
? ` (${gn.error})`
|
|
999
|
+
: gn.exitCode
|
|
1000
|
+
? ` (exit ${gn.exitCode})`
|
|
1001
|
+
: "";
|
|
1002
|
+
console.log(chalk.red(`✗ GitNexus indexing failed${detail}.`));
|
|
1003
|
+
// console.log(chalk.gray(' Retry: aiflow init --with-gitnexus'));
|
|
1004
|
+
gn.notified = true;
|
|
1005
|
+
await fs.writeJson(statusPath, gn, { spaces: 2 });
|
|
1006
|
+
}
|
|
1007
|
+
// status === 'running' and not yet stale: still in progress, stay silent
|
|
1008
|
+
} catch (_) {}
|
|
765
1009
|
}
|
|
766
1010
|
|
|
767
1011
|
async function suggestNextStep() {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
} catch (_) {}
|
|
777
|
-
|
|
778
|
-
console.log(chalk.cyan('\nNext Steps:'));
|
|
779
|
-
|
|
780
|
-
const hasCLI = aiTools.includes('claude') || aiTools.includes('gemini');
|
|
781
|
-
const hasIDE = aiTools.includes('claude') || aiTools.includes('cursor') || aiTools.includes('copilot');
|
|
782
|
-
|
|
783
|
-
if (hasCLI) {
|
|
784
|
-
console.log(chalk.white('\n CLI:'));
|
|
785
|
-
let step = 0;
|
|
786
|
-
if (aiTools.includes('claude')) {
|
|
787
|
-
console.log(` ${++step}. ${chalk.white('Claude:')} Run ${chalk.bold.green('claude start')} in terminal. ${chalk.gray('(Quickest way to start)')}`);
|
|
788
|
-
}
|
|
789
|
-
if (aiTools.includes('gemini')) {
|
|
790
|
-
console.log(` ${++step}. ${chalk.white('Gemini:')} Run ${chalk.bold.green('gemini')} then type ${chalk.bold.green('"start"')} or ${chalk.bold.green('"Gate 1"')}.`);
|
|
791
|
-
}
|
|
1012
|
+
let aiTools = ["claude"]; // Default
|
|
1013
|
+
try {
|
|
1014
|
+
if (await fs.pathExists(STATE_FILE)) {
|
|
1015
|
+
const state = await fs.readJson(STATE_FILE);
|
|
1016
|
+
if (state.aiTools && state.aiTools.length) {
|
|
1017
|
+
aiTools = state.aiTools;
|
|
1018
|
+
}
|
|
792
1019
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1020
|
+
} catch (_) {}
|
|
1021
|
+
|
|
1022
|
+
console.log(chalk.cyan("\nNext Steps:"));
|
|
1023
|
+
|
|
1024
|
+
const hasCLI = aiTools.includes("claude") || aiTools.includes("gemini");
|
|
1025
|
+
const hasIDE =
|
|
1026
|
+
aiTools.includes("claude") ||
|
|
1027
|
+
aiTools.includes("cursor") ||
|
|
1028
|
+
aiTools.includes("copilot");
|
|
1029
|
+
|
|
1030
|
+
if (hasCLI) {
|
|
1031
|
+
console.log(chalk.white("\n CLI:"));
|
|
1032
|
+
let step = 0;
|
|
1033
|
+
if (aiTools.includes("claude")) {
|
|
1034
|
+
console.log(
|
|
1035
|
+
` ${++step}. ${chalk.white("Claude:")} Run ${chalk.bold.green("claude start")} in terminal. ${chalk.gray("(Quickest way to start)")}`,
|
|
1036
|
+
);
|
|
797
1037
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1038
|
+
if (aiTools.includes("gemini")) {
|
|
1039
|
+
console.log(
|
|
1040
|
+
` ${++step}. ${chalk.white("Gemini:")} Run ${chalk.bold.green("gemini")} then type ${chalk.bold.green('"start"')} or ${chalk.bold.green('"Gate 1"')}.`,
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (hasIDE) {
|
|
1046
|
+
console.log(chalk.white("\n IDE Extension Chat:"));
|
|
1047
|
+
console.log(
|
|
1048
|
+
` Run ${chalk.bold.green("aiflow prompt")} → prompt auto-copied to clipboard → paste into your AI extension chat.`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
console.log(
|
|
1053
|
+
`\n ${chalk.yellow("Note:")} If the AI does not start automatically, type ${chalk.bold.green('"Gate 1"')} or ${chalk.bold.green('"Analyze ticket"')} to begin.`,
|
|
1054
|
+
);
|
|
1055
|
+
console.log();
|
|
801
1056
|
}
|