@relipa/ai-flow-kit 0.0.9-beta.1 → 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.
@@ -1,105 +1,105 @@
1
- # Figma → Component Workflow
2
-
3
- Generate UI components directly from Figma designs using the `figma-to-component` skill.
4
-
5
- ---
6
-
7
- ## Prerequisites
8
-
9
- Choose one adapter based on your setup:
10
-
11
- ### Option A — REST API (recommended, no Desktop required)
12
-
13
- ```bash
14
- ak init -a figma
15
- # Prompts for FIGMA_API_TOKEN
16
- ```
17
-
18
- Get your token: [figma.com/settings](https://www.figma.com/settings) → Security → Personal access tokens → Create new token (File content: read).
19
-
20
- ### Option B — Official Figma MCP (requires Figma Desktop app)
21
-
22
- ```bash
23
- ak init -a figma-desktop
24
- # No token needed — uses Desktop app session
25
- ```
26
-
27
- Requirements: Figma Desktop app installed and the target file open before invoking the skill.
28
-
29
- ---
30
-
31
- ## Getting a Figma Node URL
32
-
33
- 1. Open your file in Figma (browser or Desktop)
34
- 2. Right-click on a frame or component → **Copy link to selection**
35
- 3. The URL looks like:
36
- ```
37
- https://www.figma.com/design/FILEKEY/FileName?node-id=123-456
38
- ```
39
- - `FILEKEY` — the file identifier (between `/design/` and the next `/`)
40
- - `node-id=123-456` — the specific frame or component to generate
41
-
42
- You can also link to the entire file (no `node-id`) to let the skill list available frames.
43
-
44
- ---
45
-
46
- ## Trigger the Skill
47
-
48
- Paste the Figma URL into Claude Code with a generation request:
49
-
50
- ```
51
- Generate component from this Figma frame:
52
- https://www.figma.com/design/XXXXX/App?node-id=123-456
53
- ```
54
-
55
- ```
56
- Implement the UserCard UI from Figma:
57
- https://www.figma.com/design/XXXXX/App?node-id=78-910
58
- ```
59
-
60
- ```
61
- Read Figma and generate component following my project conventions:
62
- https://www.figma.com/design/XXXXX/App?node-id=42-0
63
- ```
64
-
65
- ---
66
-
67
- ## What to Expect
68
-
69
- The skill outputs in this order:
70
-
71
- 1. **Design Summary** — layout structure, color palette, typography scale used in the design
72
- 2. **Component file** — complete code for your detected framework (React, Next.js App Router, Vue 3, or Angular)
73
- 3. **Usage example** — how to import and use the component
74
- 4. **Notes** — anything that couldn't be mapped 1:1 from Figma (custom fonts, unsupported effects, etc.)
75
-
76
- **Framework auto-detection order:**
77
- 1. Read `CLAUDE.md` for framework identifier (`nextjs-app-router`, `reactjs`, `vue-nuxt`, `angular`)
78
- 2. Scan project files (`app/` dir → App Router, `angular.json` → Angular, `nuxt.config.*` → Vue)
79
- 3. Fallback → Next.js / React
80
-
81
- ---
82
-
83
- ## Review Checklist
84
-
85
- After the component is generated, verify:
86
-
87
- - [ ] Layout matches Figma (flex direction, alignment, gap)
88
- - [ ] Colors are correct (Tailwind class or arbitrary value)
89
- - [ ] Typography matches (size, weight, line-height)
90
- - [ ] Spacing correct (padding, margin, gap)
91
- - [ ] Interactive states handled (`'use client'` in App Router, variants in React/Vue/Angular)
92
- - [ ] Images use correct component (`next/image` for Next.js, `<img>` for others)
93
- - [ ] `className` / `class` prop exposed for external overrides
94
-
95
- ---
96
-
97
- ## Troubleshooting
98
-
99
- | Problem | Fix |
100
- |---|---|
101
- | `get_figma_data` tool not found | Run `ak init -a figma` to configure the MCP |
102
- | "Invalid API token" | Token expired — create a new one at figma.com/settings |
103
- | Wrong node generated | Check the `node-id` in the URL matches your selection in Figma |
104
- | Figma Desktop MCP not responding | Ensure Figma Desktop app is open with the file loaded |
105
- | Component uses wrong framework | Add framework to `CLAUDE.md` (e.g. `nextjs-app-router`) |
1
+ # Figma → Component Workflow
2
+
3
+ Generate UI components directly from Figma designs using the `figma-to-component` skill.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ Choose one adapter based on your setup:
10
+
11
+ ### Option A — REST API (recommended, no Desktop required)
12
+
13
+ ```bash
14
+ ak init -a figma
15
+ # Prompts for FIGMA_API_TOKEN
16
+ ```
17
+
18
+ Get your token: [figma.com/settings](https://www.figma.com/settings) → Security → Personal access tokens → Create new token (File content: read).
19
+
20
+ ### Option B — Official Figma MCP (requires Figma Desktop app)
21
+
22
+ ```bash
23
+ ak init -a figma-desktop
24
+ # No token needed — uses Desktop app session
25
+ ```
26
+
27
+ Requirements: Figma Desktop app installed and the target file open before invoking the skill.
28
+
29
+ ---
30
+
31
+ ## Getting a Figma Node URL
32
+
33
+ 1. Open your file in Figma (browser or Desktop)
34
+ 2. Right-click on a frame or component → **Copy link to selection**
35
+ 3. The URL looks like:
36
+ ```
37
+ https://www.figma.com/design/FILEKEY/FileName?node-id=123-456
38
+ ```
39
+ - `FILEKEY` — the file identifier (between `/design/` and the next `/`)
40
+ - `node-id=123-456` — the specific frame or component to generate
41
+
42
+ You can also link to the entire file (no `node-id`) to let the skill list available frames.
43
+
44
+ ---
45
+
46
+ ## Trigger the Skill
47
+
48
+ Paste the Figma URL into Claude Code with a generation request:
49
+
50
+ ```
51
+ Generate component from this Figma frame:
52
+ https://www.figma.com/design/XXXXX/App?node-id=123-456
53
+ ```
54
+
55
+ ```
56
+ Implement the UserCard UI from Figma:
57
+ https://www.figma.com/design/XXXXX/App?node-id=78-910
58
+ ```
59
+
60
+ ```
61
+ Read Figma and generate component following my project conventions:
62
+ https://www.figma.com/design/XXXXX/App?node-id=42-0
63
+ ```
64
+
65
+ ---
66
+
67
+ ## What to Expect
68
+
69
+ The skill outputs in this order:
70
+
71
+ 1. **Design Summary** — layout structure, color palette, typography scale used in the design
72
+ 2. **Component file** — complete code for your detected framework (React, Next.js App Router, Vue 3, or Angular)
73
+ 3. **Usage example** — how to import and use the component
74
+ 4. **Notes** — anything that couldn't be mapped 1:1 from Figma (custom fonts, unsupported effects, etc.)
75
+
76
+ **Framework auto-detection order:**
77
+ 1. Read `CLAUDE.md` for framework identifier (`nextjs-app-router`, `reactjs`, `vue-nuxt`, `angular`)
78
+ 2. Scan project files (`app/` dir → App Router, `angular.json` → Angular, `nuxt.config.*` → Vue)
79
+ 3. Fallback → Next.js / React
80
+
81
+ ---
82
+
83
+ ## Review Checklist
84
+
85
+ After the component is generated, verify:
86
+
87
+ - [ ] Layout matches Figma (flex direction, alignment, gap)
88
+ - [ ] Colors are correct (Tailwind class or arbitrary value)
89
+ - [ ] Typography matches (size, weight, line-height)
90
+ - [ ] Spacing correct (padding, margin, gap)
91
+ - [ ] Interactive states handled (`'use client'` in App Router, variants in React/Vue/Angular)
92
+ - [ ] Images use correct component (`next/image` for Next.js, `<img>` for others)
93
+ - [ ] `className` / `class` prop exposed for external overrides
94
+
95
+ ---
96
+
97
+ ## Troubleshooting
98
+
99
+ | Problem | Fix |
100
+ |---|---|
101
+ | `get_figma_data` tool not found | Run `ak init -a figma` to configure the MCP |
102
+ | "Invalid API token" | Token expired — create a new one at figma.com/settings |
103
+ | Wrong node generated | Check the `node-id` in the URL matches your selection in Figma |
104
+ | Figma Desktop MCP not responding | Ensure Figma Desktop app is open with the file loaded |
105
+ | Component uses wrong framework | Add framework to `CLAUDE.md` (e.g. `nextjs-app-router`) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relipa/ai-flow-kit",
3
- "version": "0.0.9-beta.1",
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'],