@relipa/ai-flow-kit 0.0.9 → 0.1.0-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relipa/ai-flow-kit",
3
- "version": "0.0.9",
3
+ "version": "0.1.0-beta.0",
4
4
  "description": "All-in-one AI Flow Kit for team development with Claude AI - skills, templates, and MCP adapters",
5
5
  "author": "Example Team",
6
6
  "publishConfig": {
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "scripts": {
43
43
  "aiflow": "node bin/aiflow.js",
44
- "test": "echo \"Add tests in future\" && exit 0",
44
+ "test": "jest",
45
45
  "lint": "echo \"Add linting in future\" && exit 0",
46
46
  "docs": "echo \"Documentation available in ./docs\"",
47
47
  "publish:beta": "npm publish --tag beta",
@@ -55,6 +55,13 @@
55
55
  "semver": "^7.7.4"
56
56
  },
57
57
  "devDependencies": {
58
+ "exceljs": "^4.4.0",
59
+ "jest": "^30.4.2",
58
60
  "typescript": "^6.0.2"
61
+ },
62
+ "jest": {
63
+ "testMatch": [
64
+ "<rootDir>/tests/**/*.test.js"
65
+ ]
59
66
  }
60
- }
67
+ }
@@ -0,0 +1,354 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const ExcelJS = require('exceljs');
4
+
5
+ // ── Normalize command aliases ──────────────────────────────────────────────
6
+ const ALIAS_MAP = {
7
+ u: 'update', up: 'update',
8
+ i: 'init',
9
+ v: 'version',
10
+ h: 'help',
11
+ p: 'prompt', promt: 'prompt', prmopt: 'prompt', promlist: 'prompt', propt: 'prompt',
12
+ g: 'guide', guid: 'guide',
13
+ t: 'task', st: 'status', s: 'status',
14
+ sw: 'switch', l: 'list', ls: 'list',
15
+ ctx: 'context',
16
+ };
17
+
18
+ const EXPLORE_CMDS = new Set(['init', 'update', 'version', 'help', 'status', 'unknown_flag']);
19
+ const USE_CMDS = new Set(['use', 'use.help', 'prompt', 'guide', 'guideline', 'sync-skills', 'detect']);
20
+
21
+ function norm(cmd) {
22
+ if (!cmd || cmd === '-' || cmd.trim() === '') return null;
23
+ const c = cmd.toLowerCase().trim();
24
+ return ALIAS_MAP[c] || c;
25
+ }
26
+
27
+ // ── Parse log file ─────────────────────────────────────────────────────────
28
+ function parseLog(filePath) {
29
+ const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
30
+ const users = {};
31
+
32
+ for (let i = 1; i < lines.length; i++) {
33
+ const line = lines[i].trim();
34
+ if (!line) continue;
35
+ const cols = line.split('\t');
36
+ // Cols: Timestamp[0] Repo[1] User[2] OS[3] Node[4] Version[5] Command[6]
37
+ // AI Tool[7] Gate[8] Ticket[9] ...
38
+ const user = cols[2];
39
+ if (!user || user === '-' || user === '') continue;
40
+
41
+ const timestamp = cols[0] || '';
42
+ const command = cols[6] || '';
43
+ const ticket = cols[9] || '';
44
+
45
+ const date = timestamp.split(' ')[0]; // dd/MM/yyyy
46
+
47
+ if (!users[user]) {
48
+ users[user] = { email: user, days: new Set(), events: 0, rawCmds: new Set(), gateCmds: [] };
49
+ }
50
+ const u = users[user];
51
+ u.events++;
52
+ if (date) u.days.add(date);
53
+
54
+ const cmd = norm(command);
55
+ if (cmd) u.rawCmds.add(cmd);
56
+
57
+ const gateMatch = command.match(/^gate(\d+)\.(start|approved)$/i);
58
+ if (gateMatch) {
59
+ u.gateCmds.push({ gateNum: +gateMatch[1], type: gateMatch[2].toLowerCase(), ticket });
60
+ }
61
+ // generic "gate.approved" (no number)
62
+ if (/^gate\.approved$/i.test(command)) {
63
+ u.gateCmds.push({ gateNum: 0, type: 'approved', ticket });
64
+ }
65
+ }
66
+ return users;
67
+ }
68
+
69
+ // ── Score calculator ───────────────────────────────────────────────────────
70
+ function calcScore(u) {
71
+ const { events, days, rawCmds, gateCmds } = u;
72
+ const activeDays = days.size;
73
+
74
+ if (events === 0) return 0;
75
+
76
+ // Gate aggregation
77
+ const gateStarted = new Set(); // gate numbers
78
+ const gateApproved = new Set();
79
+ const ticketsStarted = new Set();
80
+ const ticketsCompleted = new Set(); // gate4.approved or gate5.start
81
+
82
+ for (const g of gateCmds) {
83
+ const hasTicket = g.ticket && g.ticket !== '-' && g.ticket !== '';
84
+ if (g.type === 'start') {
85
+ gateStarted.add(g.gateNum);
86
+ if (hasTicket) ticketsStarted.add(g.ticket);
87
+ }
88
+ if (g.type === 'approved') {
89
+ gateApproved.add(g.gateNum);
90
+ if (hasTicket && g.gateNum >= 4) ticketsCompleted.add(g.ticket);
91
+ }
92
+ }
93
+ // gate5.start also counts as completed
94
+ for (const g of gateCmds) {
95
+ if (g.gateNum === 5 && g.type === 'start' && g.ticket && g.ticket !== '-') {
96
+ ticketsCompleted.add(g.ticket);
97
+ }
98
+ }
99
+
100
+ const hasUse = USE_CMDS.has('use') && rawCmds.has('use');
101
+ const hasPrompt = rawCmds.has('prompt');
102
+ const hasGuide = rawCmds.has('guide') || rawCmds.has('guideline') || rawCmds.has('sync-skills');
103
+ const hasGateHighApproved = gateApproved.has(3) || gateApproved.has(4);
104
+
105
+ // Score 5: gate 3-5 approved + ≥ 3 tickets completed
106
+ if (hasGateHighApproved && ticketsCompleted.size >= 3) return 5;
107
+
108
+ // Score 4: gate.approved ≥ 1 + ≥ 1 ticket completed
109
+ if (gateApproved.size > 0 && ticketsCompleted.size >= 1) return 4;
110
+
111
+ // Score 3: gate.start with real ticket + ≥ 3 active days
112
+ if (ticketsStarted.size > 0 && activeDays >= 3) return 3;
113
+
114
+ // Score 2: has use/prompt/guide OR ≥ 2 active days
115
+ if (hasUse || hasPrompt || hasGuide || activeDays >= 2) return 2;
116
+
117
+ // Score 1: only exploration commands
118
+ const onlyExplore = [...rawCmds].every(c => EXPLORE_CMDS.has(c));
119
+ if (onlyExplore && rawCmds.size > 0) return 1;
120
+
121
+ return 0;
122
+ }
123
+
124
+ const SCORE_LABELS = [
125
+ 'Không hoạt động',
126
+ 'Mới khám phá',
127
+ 'Bắt đầu dùng',
128
+ 'Dùng có workflow',
129
+ 'Hoàn thành workflow',
130
+ 'Sử dụng thường xuyên workflow',
131
+ ];
132
+
133
+ // ── Build summary row for each user ───────────────────────────────────────
134
+ function buildRow(email, u) {
135
+ const { events, days, rawCmds, gateCmds } = u;
136
+ const activeDays = days.size;
137
+ const score = calcScore(u);
138
+
139
+ // Key commands: deduplicated, meaningful, sorted
140
+ const ignoredCmds = new Set([
141
+ 'unknown_flag', 'use.help', 'tel', 'tel.help', 'telementry',
142
+ 'flush', 'flush.help', 'reset', 'context', 'memory.search',
143
+ 'upgrade', 'gate.help', 'prompt.help', 'task.help',
144
+ ]);
145
+ const gatePattern = /^gate\d*\.(start|approved|help)$|^gate\.(approved|help)$/i;
146
+
147
+ const displayCmds = [...rawCmds]
148
+ .filter(c => !ignoredCmds.has(c) && !gatePattern.test(c))
149
+ .sort();
150
+
151
+ // Compact gate summary
152
+ const startedGates = [...new Set(gateCmds.filter(g => g.type === 'start').map(g => g.gateNum))].sort((a, b) => a - b);
153
+ const approvedGates = [...new Set(gateCmds.filter(g => g.type === 'approved').map(g => g.gateNum))].sort((a, b) => a - b);
154
+
155
+ const ticketsStarted = [...new Set(gateCmds.filter(g => g.type === 'start' && g.ticket && g.ticket !== '-').map(g => g.ticket))];
156
+ const ticketsApproved = [...new Set(gateCmds.filter(g => g.type === 'approved' && g.ticket && g.ticket !== '-').map(g => g.ticket))];
157
+ const ticketsCompleted = [...new Set(gateCmds.filter(g =>
158
+ ((g.type === 'approved' && g.gateNum >= 4) || (g.gateNum === 5 && g.type === 'start'))
159
+ && g.ticket && g.ticket !== '-'
160
+ ).map(g => g.ticket))];
161
+
162
+ const gateStartStr = startedGates.length ? `G${startedGates.join(',')} / ${ticketsStarted.length} ticket(s)` : '';
163
+ const gateApprovedStr = approvedGates.length ? `G${approvedGates.join(',')} / ${ticketsApproved.length} ticket(s)` : '';
164
+ const ticketsStr = ticketsCompleted.length ? ticketsCompleted.join(', ') : '';
165
+
166
+ // Notes
167
+ let notes = '';
168
+ if (score === 0) notes = 'Không có event nào trong tuần';
169
+ else if (score === 1) notes = 'Chỉ có lệnh khám phá cơ bản (init/update/version/help)';
170
+ else if (score === 2) {
171
+ if (ticketsStarted.length > 0) notes = `Có gate.start nhưng chưa gate4.approved, ${activeDays} ngày active`;
172
+ else notes = `${displayCmds.slice(0, 3).join('/')}, ${activeDays} ngày active, chưa dùng gate workflow`;
173
+ }
174
+ else if (score === 3) notes = `Gate workflow bắt đầu, ${ticketsStarted.length} ticket(s), chưa hoàn thành`;
175
+ else if (score === 4) notes = `${ticketsCompleted.length} ticket hoàn thành (gate4+)`;
176
+ else if (score === 5) notes = `Gate 3-5 approved, ${ticketsCompleted.length} tickets hoàn thành`;
177
+
178
+ return {
179
+ email,
180
+ events,
181
+ activeDays,
182
+ keyCommands: displayCmds.join(', '),
183
+ gateStart: gateStartStr,
184
+ gateApproved: gateApprovedStr,
185
+ tickets: ticketsStr,
186
+ depthScore: score,
187
+ depthLabel: SCORE_LABELS[score],
188
+ impactScore: '',
189
+ impactLabel: '',
190
+ totalScore: score,
191
+ notes,
192
+ };
193
+ }
194
+
195
+ // ── Excel writer ───────────────────────────────────────────────────────────
196
+ async function writeExcel(rows, outputPath) {
197
+ const workbook = new ExcelJS.Workbook();
198
+ const sheet = workbook.addWorksheet('Score 18-24.05.2026');
199
+
200
+ sheet.columns = [
201
+ { key: 'no', width: 5 },
202
+ { key: 'email', width: 30 },
203
+ { key: 'events', width: 8 },
204
+ { key: 'activeDays', width: 10 },
205
+ { key: 'keyCommands', width: 40 },
206
+ { key: 'gateStart', width: 24 },
207
+ { key: 'gateApproved', width: 24 },
208
+ { key: 'tickets', width: 44 },
209
+ { key: 'depthScore', width: 8 },
210
+ { key: 'depthLabel', width: 28 },
211
+ { key: 'impactScore', width: 8 },
212
+ { key: 'impactLabel', width: 18 },
213
+ { key: 'totalScore', width: 8 },
214
+ { key: 'notes', width: 54 },
215
+ ];
216
+
217
+ const border = {
218
+ top: { style: 'thin' }, left: { style: 'thin' },
219
+ bottom: { style: 'thin' }, right: { style: 'thin' },
220
+ };
221
+
222
+ // Title row
223
+ sheet.addRow(['AI Flow Kit — Weekly Score Report: 18/05/2026 – 24/05/2026']);
224
+ sheet.mergeCells('A1:N1');
225
+ const title = sheet.getCell('A1');
226
+ title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
227
+ title.alignment = { vertical: 'middle', horizontal: 'center' };
228
+ title.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD6DCE4' } };
229
+ sheet.getRow(1).height = 28;
230
+
231
+ // Header row
232
+ const HEADERS = [
233
+ '#', 'Email', 'Events', 'Active\nDays', 'Key Commands',
234
+ 'Gate Start', 'Gate Approved', 'Tickets Processed',
235
+ 'Depth\nScore', 'Depth Label', 'Impact\nScore', 'Impact Label',
236
+ 'Total\nScore', 'Notes',
237
+ ];
238
+ const hRow = sheet.addRow(HEADERS);
239
+ hRow.height = 36;
240
+ hRow.eachCell(cell => {
241
+ cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } };
242
+ cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 };
243
+ cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
244
+ cell.border = border;
245
+ });
246
+
247
+ // Score → row color
248
+ const COLORS = {
249
+ 0: 'FFFFC7CE',
250
+ 1: 'FFFFEB9C',
251
+ 2: 'FFFFFFCC',
252
+ 3: 'FFD9EAD3',
253
+ 4: 'FF93C47D',
254
+ 5: 'FF6AA84F',
255
+ };
256
+
257
+ rows.forEach((row, idx) => {
258
+ const excelRowNum = idx + 3; // 1=title, 2=header
259
+ const r = sheet.addRow([
260
+ idx + 1, row.email, row.events, row.activeDays,
261
+ row.keyCommands, row.gateStart, row.gateApproved, row.tickets,
262
+ row.depthScore, row.depthLabel, row.impactScore, row.impactLabel,
263
+ null, row.notes,
264
+ ]);
265
+ r.height = 20;
266
+
267
+ const fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS[row.depthScore] || 'FFFFFFFF' } };
268
+ r.eachCell({ includeEmpty: true }, (cell, col) => {
269
+ cell.border = border;
270
+ cell.fill = fill;
271
+ cell.alignment = (col <= 4 || (col >= 9 && col <= 13))
272
+ ? { vertical: 'middle', horizontal: 'center' }
273
+ : { vertical: 'middle', horizontal: 'left', wrapText: false };
274
+ });
275
+
276
+ // Total Score = Depth + Impact formula
277
+ r.getCell(13).value = { formula: `I${excelRowNum}+K${excelRowNum}` };
278
+ });
279
+
280
+ sheet.views = [{ state: 'frozen', ySplit: 2 }];
281
+ sheet.autoFilter = { from: 'A2', to: 'N2' };
282
+
283
+ await workbook.xlsx.writeFile(outputPath);
284
+ console.log(`Written: ${outputPath} (${rows.length} users)`);
285
+ }
286
+
287
+ // ── Canonical user list (fixed order) ─────────────────────────────────────
288
+ const CANONICAL_USERS = [
289
+ 'anhtt@relipasoft.com',
290
+ 'hoangdv@relipasoft.com',
291
+ 'chinhtt@relipasoft.com',
292
+ 'hieptv@relipasoft.com',
293
+ 'phongnx@relipasoft.com',
294
+ 'dungha@relipasoft.com',
295
+ 'thangnv@relipasoft.com',
296
+ 'nguyenlt@relipasoft.com',
297
+ 'thuongvv@relipasoft.com',
298
+ 'yenvtb@relipasoft.com',
299
+ 'tructth@relipasoft.com',
300
+ 'thainq@relipasoft.com',
301
+ 'tuoittx@relipasoft.com',
302
+ 'hungdv@relipasoft.com',
303
+ 'hiepnk@relipasoft.com',
304
+ 'khoihn@relipasoft.com',
305
+ 'huynq@relipasoft.com',
306
+ 'thinhvq@relipasoft.com',
307
+ 'banv@relipasoft.com',
308
+ 'duongnt@relipasoft.com',
309
+ 'anhnt@relipasoft.com',
310
+ 'khanhdd@relipasoft.com',
311
+ 'hieunv@relipasoft.com',
312
+ ];
313
+
314
+ // Log email → canonical email (for slight mismatches)
315
+ const LOG_EMAIL_ALIAS = {
316
+ 'tructh@relipasoft.com': 'tructth@relipasoft.com',
317
+ 'duongnt1@relipasoft.com': 'duongnt@relipasoft.com',
318
+ };
319
+
320
+ const EMPTY_USER = (email) => ({
321
+ email, events: 0, days: new Set(), rawCmds: new Set(), gateCmds: [],
322
+ });
323
+
324
+ // ── Main ───────────────────────────────────────────────────────────────────
325
+ (async () => {
326
+ const logFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026');
327
+ const outputFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026_new.xlsx');
328
+
329
+ const rawUsers = parseLog(logFile);
330
+
331
+ // Re-key log users using alias map (tructh → tructth, duongnt1 → duongnt)
332
+ const users = {};
333
+ for (const [logEmail, data] of Object.entries(rawUsers)) {
334
+ const canonical = LOG_EMAIL_ALIAS[logEmail] || logEmail;
335
+ if (!users[canonical]) {
336
+ users[canonical] = { ...data, email: canonical };
337
+ } else {
338
+ // merge (shouldn't happen but just in case)
339
+ const u = users[canonical];
340
+ u.events += data.events;
341
+ data.days.forEach(d => u.days.add(d));
342
+ data.rawCmds.forEach(c => u.rawCmds.add(c));
343
+ u.gateCmds.push(...data.gateCmds);
344
+ }
345
+ }
346
+
347
+ // Build rows in canonical order; fill score 0 for absent users
348
+ const rows = CANONICAL_USERS.map(email => {
349
+ const u = users[email] || EMPTY_USER(email);
350
+ return buildRow(email, u);
351
+ });
352
+
353
+ await writeExcel(rows, outputFile);
354
+ })();
package/scripts/guide.js CHANGED
@@ -164,6 +164,7 @@ function showCommands() {
164
164
  group: 'Workflow (use per task)',
165
165
  items: [
166
166
  ['aiflow use <TICKET-ID>', '', 'Load context from Backlog/Jira'],
167
+ ['aiflow use <T1> <T2> ...', '', 'Multi-target: 1st is primary, rest supplementary (v0.1.0+)'],
167
168
  ['aiflow use', '--fast', 'Fast Mode: minimal Q&A (Default)'],
168
169
  ['aiflow use', '--full', 'Full Mode: deep analysis with Q&A'],
169
170
  ['aiflow use', '--file <path>', 'Load from local text/json file'],
@@ -171,6 +172,7 @@ function showCommands() {
171
172
  ['aiflow use', '--with-comments', 'Include all comments'],
172
173
  ['aiflow use', '--comments-last <n>', 'Only get last N comments'],
173
174
  ['aiflow use', '--comments-from <n>', 'Get comments from index N onward'],
175
+ ['aiflow fetch-links <url>', '', 'Fetch Backlog/Jira link → SupplementaryContext JSON (v0.1.0+)'],
174
176
  ['aiflow prompt <type>', '', 'Generate AI prompt'],
175
177
  ['aiflow prompt', '--list', 'View all prompt types'],
176
178
  ['aiflow prompt', '--output <file>', 'Save prompt to file'],