@relipa/ai-flow-kit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scripts/prompt.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const { spawnSync } = require('child_process');
5
+ const { confirm } = require('@inquirer/prompts');
5
6
 
6
7
  const PROJECT_DIR = process.cwd();
7
8
  const CURRENT_FILE = path.join(PROJECT_DIR, '.aiflow', 'context', 'current.json');
@@ -288,15 +289,12 @@ module.exports = async function generatePrompt(type, options = {}) {
288
289
  return;
289
290
  }
290
291
 
291
- const copied = copyToClipboard(prompt);
292
- if (copied) {
292
+ const clipResult = copyToClipboard(prompt);
293
+ if (clipResult.success) {
293
294
  const ticket = ctx?.taskId ? chalk.gray(` [${ctx.taskId}]`) : '';
294
295
  console.log(chalk.green(`✓ Prompt copied to clipboard!${ticket} Paste it into your AI chat to begin.`));
295
296
  } else {
296
- console.log(chalk.cyan('\n' + '─'.repeat(60)));
297
- console.log(prompt);
298
- console.log('─'.repeat(60) + '\n');
299
- console.log(chalk.yellow('Could not copy to clipboard. Paste the prompt above manually.'));
297
+ await handleClipboardFailure(prompt, clipResult);
300
298
  }
301
299
  };
302
300
 
@@ -423,6 +421,33 @@ function isWSL() {
423
421
  } catch (_) { return false; }
424
422
  }
425
423
 
424
+ function detectLinuxPkgManager() {
425
+ const candidates = [
426
+ { cmd: 'apt-get', installFlag: 'install -y' },
427
+ { cmd: 'dnf', installFlag: 'install -y' },
428
+ { cmd: 'pacman', installFlag: '-S --noconfirm' },
429
+ { cmd: 'zypper', installFlag: 'install -y' },
430
+ ];
431
+ for (const c of candidates) {
432
+ const r = spawnSync('which', [c.cmd], { stdio: 'ignore' });
433
+ if (r.status === 0) return c;
434
+ }
435
+ return null;
436
+ }
437
+
438
+ function linuxClipboardInfo() {
439
+ const isWayland = !!process.env.WAYLAND_DISPLAY;
440
+ const pkg = isWayland ? 'wl-clipboard' : 'xclip';
441
+ const mgr = detectLinuxPkgManager();
442
+ return {
443
+ pkg,
444
+ pkgManager: mgr ? mgr.cmd : null,
445
+ installArgs: mgr ? [...mgr.installFlag.split(' '), pkg] : null,
446
+ installDisplayCmd: mgr ? `sudo ${mgr.cmd} ${mgr.installFlag} ${pkg}` : null,
447
+ };
448
+ }
449
+
450
+ // Returns { success: true } or { success: false, pkg, pkgManager, installArgs, installDisplayCmd }
426
451
  function copyToClipboard(text) {
427
452
  try {
428
453
  if (process.platform === 'win32') {
@@ -432,8 +457,10 @@ function copyToClipboard(text) {
432
457
  '[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())'],
433
458
  { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] }
434
459
  );
460
+ return { success: true };
435
461
  } else if (process.platform === 'darwin') {
436
462
  spawnSync('pbcopy', [], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
463
+ return { success: true };
437
464
  } else if (isWSL()) {
438
465
  spawnSync(
439
466
  'powershell.exe',
@@ -441,12 +468,71 @@ function copyToClipboard(text) {
441
468
  '[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())'],
442
469
  { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] }
443
470
  );
471
+ return { success: true };
444
472
  } else {
473
+ // Try wl-copy (Wayland), then xclip, then xsel
474
+ if (process.env.WAYLAND_DISPLAY) {
475
+ const r = spawnSync('wl-copy', [], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
476
+ if (!r.error && r.status === 0) return { success: true };
477
+ }
445
478
  const r = spawnSync('xclip', ['-selection', 'clipboard'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
446
- if (r.error) spawnSync('xsel', ['--clipboard', '--input'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
479
+ if (!r.error && r.status === 0) return { success: true };
480
+ const r2 = spawnSync('xsel', ['--clipboard', '--input'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
481
+ if (!r2.error && r2.status === 0) return { success: true };
482
+ return { success: false, ...linuxClipboardInfo() };
483
+ }
484
+ } catch (_) {
485
+ return { success: false };
486
+ }
487
+ }
488
+
489
+ function showPromptFallback(text) {
490
+ console.log(chalk.cyan('\n' + '─'.repeat(60)));
491
+ console.log(text);
492
+ console.log(chalk.cyan('─'.repeat(60) + '\n'));
493
+ console.log(chalk.yellow('Clipboard unavailable — paste the prompt above into your AI chat.'));
494
+ }
495
+
496
+ async function handleClipboardFailure(text, info) {
497
+ if (!info.pkg) {
498
+ // Unknown platform failure
499
+ showPromptFallback(text);
500
+ return;
501
+ }
502
+
503
+ console.log(chalk.yellow(`\n⚠ Clipboard tool not found (need: ${chalk.white(info.pkg)})`));
504
+
505
+ if (!info.installDisplayCmd) {
506
+ console.log(chalk.gray(' Could not detect a package manager. Install manually:'));
507
+ console.log(chalk.gray(' Wayland: sudo apt install wl-clipboard'));
508
+ console.log(chalk.gray(' X11: sudo apt install xclip\n'));
509
+ showPromptFallback(text);
510
+ return;
511
+ }
512
+
513
+ console.log(chalk.gray(` Install command: ${chalk.white(info.installDisplayCmd)}\n`));
514
+
515
+ try {
516
+ const doInstall = await confirm({
517
+ message: `Install ${info.pkg} now? (requires sudo)`,
518
+ default: true,
519
+ });
520
+
521
+ if (doInstall) {
522
+ console.log(chalk.cyan(`\nRunning: sudo ${info.pkgManager} ${info.installArgs.join(' ')}`));
523
+ const r = spawnSync('sudo', [info.pkgManager, ...info.installArgs], { stdio: 'inherit' });
524
+ if (r.status === 0) {
525
+ const retry = copyToClipboard(text);
526
+ if (retry.success) {
527
+ console.log(chalk.green('\n✓ Installed! Prompt copied to clipboard. Paste it into your AI chat.'));
528
+ return;
529
+ }
530
+ }
531
+ console.log(chalk.red('\nInstallation failed. Showing prompt below:'));
447
532
  }
448
- return true;
449
533
  } catch (_) {
450
- return false;
534
+ // Non-interactive terminal (piped) — skip the question
451
535
  }
536
+
537
+ showPromptFallback(text);
452
538
  }
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const XLSX = require('xlsx');
7
+
8
+ const LOG_FILE = path.resolve(__dirname, '../tellog/log_25-05-2026_31-05-2026');
9
+ const OUTPUT_FILE = path.resolve(__dirname, '../tellog/log_25-05-2026_31-05-2026.xlsx');
10
+
11
+ const MEMBERS = [
12
+ 'anhtt@relipasoft.com',
13
+ 'hoangdv@relipasoft.com',
14
+ 'chinhtt@relipasoft.com',
15
+ 'phongnx@relipasoft.com',
16
+ 'dungha@relipasoft.com',
17
+ 'thangnv@relipasoft.com',
18
+ 'nguyenlt@relipasoft.com',
19
+ 'thuongvv@relipasoft.com',
20
+ 'yenvtb@relipasoft.com',
21
+ 'tructh@relipasoft.com',
22
+ 'thainq@relipasoft.com',
23
+ 'tuoittx@relipasoft.com',
24
+ 'hungdv@relipasoft.com',
25
+ 'hiepnk@relipasoft.com',
26
+ 'khoihn@relipasoft.com',
27
+ 'huynq@relipasoft.com',
28
+ 'thinhvq@relipasoft.com',
29
+ 'banv@relipasoft.com',
30
+ 'duongnt1@relipasoft.com',
31
+ 'anhnt@relipasoft.com',
32
+ 'khanhdd@relipasoft.com',
33
+ 'hieunv@relipasoft.com',
34
+ ];
35
+
36
+ const EXPLORE_COMMANDS = new Set(['init', 'update', 'version', 'help', 'init.help', 'unknown_flag']);
37
+ const USE_COMMANDS = new Set(['use', 'prompt', 'guide', 'guideline', 'guiline', 'guide?']);
38
+
39
+ // Tickets that are clearly not real work (test, demo, throwaway)
40
+ const FAKE_TICKET_PATTERNS = /^(test|test-direct|demo|investigation|sample|trial|dummy)(-.*)?$/i;
41
+
42
+ function isRealTicket(ticket) {
43
+ if (!ticket || ticket === '-' || ticket.length < 3) return false;
44
+ if (FAKE_TICKET_PATTERNS.test(ticket)) return false;
45
+ // Real tickets typically follow PROJECT-ID pattern with uppercase prefix
46
+ return true;
47
+ }
48
+
49
+ function parseLog() {
50
+ const content = fs.readFileSync(LOG_FILE, 'utf8');
51
+ const lines = content.split('\n');
52
+
53
+ // Skip header (line 0)
54
+ const events = [];
55
+ for (let i = 1; i < lines.length; i++) {
56
+ const line = lines[i].trim();
57
+ if (!line) continue;
58
+
59
+ // TSV format: columns separated by tabs
60
+ const cols = line.split('\t');
61
+ if (cols.length < 7) continue;
62
+
63
+ const timestamp = cols[0];
64
+ const repo = cols[1];
65
+ const user = cols[2];
66
+ const command = (cols[6] || '').trim();
67
+ const gate = (cols[8] || '').trim();
68
+ const ticket = (cols[9] || '').trim();
69
+
70
+ if (!timestamp || !user) continue;
71
+
72
+ // Parse date from "DD/MM/YYYY HH:MM" format
73
+ const dateMatch = timestamp.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
74
+ if (!dateMatch) continue;
75
+ const dateStr = `${dateMatch[3]}-${dateMatch[2]}-${dateMatch[1]}`;
76
+
77
+ events.push({ timestamp, dateStr, repo, user, command, gate, ticket });
78
+ }
79
+ return events;
80
+ }
81
+
82
+ function detectSessions(userEvents) {
83
+ // A new session = gap of more than 30 minutes between consecutive events
84
+ // or a new day; simplified: count distinct hour blocks per day
85
+ const sessionKeys = new Set();
86
+ for (const e of userEvents) {
87
+ const hourMatch = e.timestamp.match(/(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):/);
88
+ if (hourMatch) {
89
+ sessionKeys.add(`${hourMatch[3]}-${hourMatch[2]}-${hourMatch[1]}-${hourMatch[4]}`);
90
+ }
91
+ }
92
+ return sessionKeys.size;
93
+ }
94
+
95
+ function score(stats) {
96
+ const { totalEvents, activeDays, sessions, gateStarts, gateApproveds, ticketsProcessed,
97
+ ticketsInWeek, hasUseOrPrompt, hasGateStartWithTicket, hasGate3to5Approved } = stats;
98
+
99
+ if (totalEvents === 0) return { depth: 0, depthLabel: 'Không hoạt động' };
100
+
101
+ const hasOnlyExplore = stats.onlyExploreCommands;
102
+
103
+ // Score 5: Gate 3-5 approved + >= 3 real tickets active in week
104
+ if (hasGate3to5Approved && ticketsInWeek >= 3) {
105
+ return { depth: 5, depthLabel: 'Sử dụng thường xuyên workflow' };
106
+ }
107
+
108
+ // Score 4: gate.approved >= 1 + >= 1 ticket processed to end
109
+ if (gateApproveds >= 1 && ticketsProcessed >= 1) {
110
+ return { depth: 4, depthLabel: 'Hoàn thành workflow' };
111
+ }
112
+
113
+ // Score 3: gate.start with real ticket + >= 3 sessions
114
+ if (hasGateStartWithTicket && sessions >= 3) {
115
+ return { depth: 3, depthLabel: 'Dùng có workflow' };
116
+ }
117
+
118
+ // Score 2: has use/prompt/guideline OR >= 2 sessions
119
+ if (hasUseOrPrompt || sessions >= 2) {
120
+ return { depth: 2, depthLabel: 'Bắt đầu dùng' };
121
+ }
122
+
123
+ // Score 1: only explore commands
124
+ if (hasOnlyExplore) {
125
+ return { depth: 1, depthLabel: 'Mới khám phá' };
126
+ }
127
+
128
+ return { depth: 1, depthLabel: 'Mới khám phá' };
129
+ }
130
+
131
+ function computeImpactScore(stats) {
132
+ // Impact: based on tickets processed and gate depth
133
+ const { ticketsProcessed, gateApproveds, hasGate3to5Approved } = stats;
134
+ if (ticketsProcessed >= 3 && hasGate3to5Approved) return { impact: 5, impactLabel: 'Rất cao' };
135
+ if (ticketsProcessed >= 2 && gateApproveds >= 2) return { impact: 4, impactLabel: 'Cao' };
136
+ if (ticketsProcessed >= 1 && gateApproveds >= 1) return { impact: 3, impactLabel: 'Trung bình' };
137
+ if (stats.gateStarts >= 1) return { impact: 2, impactLabel: 'Thấp' };
138
+ if (stats.totalEvents > 0) return { impact: 1, impactLabel: 'Rất thấp' };
139
+ return { impact: 0, impactLabel: 'Không có' };
140
+ }
141
+
142
+ function buildNotes(stats) {
143
+ const parts = [];
144
+ if (stats.hasGate3to5Approved) parts.push('Gate 3-5 approved');
145
+ if (stats.ticketsProcessed > 0) parts.push(`${stats.ticketsProcessed} ticket(s) hoàn thành`);
146
+ if (stats.ticketsInWeek > stats.ticketsProcessed) parts.push(`${stats.ticketsInWeek} ticket(s) trong tuần`);
147
+ if (stats.gateStarts > 0 && stats.ticketsProcessed === 0) parts.push('Có gate.start nhưng chưa hoàn thành ticket');
148
+ if (stats.onlyExploreCommands && stats.totalEvents > 0) parts.push('Chỉ dùng lệnh khám phá');
149
+ if (stats.totalEvents === 0) parts.push('Không có hoạt động');
150
+ return parts.join('; ');
151
+ }
152
+
153
+ function analyzeMembers(events) {
154
+ const results = [];
155
+
156
+ for (const email of MEMBERS) {
157
+ const userEvents = events.filter(e => e.user === email);
158
+ const totalEvents = userEvents.length;
159
+
160
+ const activeDaysSet = new Set(userEvents.map(e => e.dateStr));
161
+ const activeDays = activeDaysSet.size;
162
+
163
+ const sessions = detectSessions(userEvents);
164
+
165
+ // Collect unique commands (non-empty, non-dash)
166
+ const commandSet = new Set(
167
+ userEvents.map(e => e.command).filter(c => c && c !== '-')
168
+ );
169
+ const keyCommands = [...commandSet].join(', ');
170
+
171
+ // Gate starts (gate1.start, gate2.start, ...)
172
+ const gateStartEvents = userEvents.filter(e => e.command && e.command.match(/^gate\d+\.start$/));
173
+ const gateStarts = gateStartEvents.length;
174
+
175
+ // Gate approved
176
+ const gateApprovedEvents = userEvents.filter(e => e.command && e.command.match(/^gate\d+\.approved$/));
177
+ const gateApproveds = gateApprovedEvents.length;
178
+
179
+ // Tickets processed to end (reached gate4.approved or gate5.start) - real tickets only
180
+ const completedTicketEvents = userEvents.filter(e =>
181
+ e.command && (e.command === 'gate4.approved' || e.command === 'gate5.start') && isRealTicket(e.ticket)
182
+ );
183
+ const ticketsProcessedSet = new Set(completedTicketEvents.map(e => e.ticket));
184
+ const ticketsProcessed = ticketsProcessedSet.size;
185
+
186
+ // Tickets active in week (any gate event with real ticket)
187
+ const ticketsInWeekSet = new Set(
188
+ userEvents
189
+ .filter(e => e.command && e.command.match(/^gate\d+\.(start|approved)$/) && isRealTicket(e.ticket))
190
+ .map(e => e.ticket)
191
+ );
192
+ const ticketsInWeek = ticketsInWeekSet.size;
193
+
194
+ const hasUseOrPrompt = userEvents.some(e =>
195
+ e.command && (USE_COMMANDS.has(e.command) || e.command.startsWith('guide'))
196
+ );
197
+
198
+ const hasGateStartWithRealTicket = gateStartEvents.some(e => isRealTicket(e.ticket));
199
+
200
+ // Gate 3-5 approved
201
+ const hasGate3to5Approved = userEvents.some(e =>
202
+ e.command && e.command.match(/^gate[345]\.approved$/)
203
+ );
204
+
205
+ const nonExploreCommands = userEvents.filter(e =>
206
+ e.command && e.command !== '-' && !EXPLORE_COMMANDS.has(e.command)
207
+ );
208
+ const onlyExploreCommands = totalEvents > 0 && nonExploreCommands.length === 0;
209
+
210
+ const stats = {
211
+ totalEvents, activeDays, sessions, gateStarts, gateApproveds,
212
+ ticketsProcessed, ticketsInWeek, hasUseOrPrompt, hasGateStartWithTicket: hasGateStartWithRealTicket,
213
+ hasGate3to5Approved, onlyExploreCommands
214
+ };
215
+
216
+ const { depth, depthLabel } = score(stats);
217
+ const { impact, impactLabel } = computeImpactScore(stats);
218
+ const totalScore = depth + impact;
219
+ const notes = buildNotes(stats);
220
+
221
+ results.push({
222
+ email, totalEvents, activeDays, keyCommands,
223
+ gateStarts, gateApproveds, ticketsProcessed,
224
+ depth, depthLabel, impact, impactLabel, totalScore, notes
225
+ });
226
+ }
227
+
228
+ return results;
229
+ }
230
+
231
+ function buildExcel(results) {
232
+ const wb = XLSX.utils.book_new();
233
+
234
+ const headers = [
235
+ '#', 'Email', 'Events', 'Active\nDays', 'Key Commands',
236
+ 'Gate Start', 'Gate Approved', 'Tickets Processed',
237
+ 'Depth\nScore', 'Depth Label',
238
+ 'Impact\nScore', 'Impact Label',
239
+ 'Total\nScore', 'Notes'
240
+ ];
241
+
242
+ const rows = [headers];
243
+ results.forEach((r, i) => {
244
+ rows.push([
245
+ i + 1,
246
+ r.email,
247
+ r.totalEvents,
248
+ r.activeDays,
249
+ r.keyCommands,
250
+ r.gateStarts,
251
+ r.gateApproveds,
252
+ r.ticketsProcessed,
253
+ r.depth,
254
+ r.depthLabel,
255
+ r.impact,
256
+ r.impactLabel,
257
+ r.totalScore,
258
+ r.notes
259
+ ]);
260
+ });
261
+
262
+ const ws = XLSX.utils.aoa_to_sheet(rows);
263
+
264
+ // Column widths
265
+ ws['!cols'] = [
266
+ { wch: 4 }, // #
267
+ { wch: 32 }, // Email
268
+ { wch: 8 }, // Events
269
+ { wch: 8 }, // Active Days
270
+ { wch: 50 }, // Key Commands
271
+ { wch: 10 }, // Gate Start
272
+ { wch: 13 }, // Gate Approved
273
+ { wch: 16 }, // Tickets Processed
274
+ { wch: 8 }, // Depth Score
275
+ { wch: 28 }, // Depth Label
276
+ { wch: 8 }, // Impact Score
277
+ { wch: 14 }, // Impact Label
278
+ { wch: 8 }, // Total Score
279
+ { wch: 50 }, // Notes
280
+ ];
281
+
282
+ XLSX.utils.book_append_sheet(wb, ws, 'Scoring');
283
+ XLSX.writeFile(wb, OUTPUT_FILE);
284
+ console.log(`✅ Excel saved to: ${OUTPUT_FILE}`);
285
+ }
286
+
287
+ const events = parseLog();
288
+ console.log(`Parsed ${events.length} events`);
289
+
290
+ const results = analyzeMembers(events);
291
+ buildExcel(results);
292
+
293
+ // Print summary
294
+ console.log('\n=== SCORING SUMMARY ===');
295
+ console.log(
296
+ '#'.padEnd(3),
297
+ 'Email'.padEnd(34),
298
+ 'Events'.padEnd(7),
299
+ 'Days'.padEnd(5),
300
+ 'GS'.padEnd(4),
301
+ 'GA'.padEnd(4),
302
+ 'TP'.padEnd(4),
303
+ 'Depth'.padEnd(6),
304
+ 'Impact'.padEnd(7),
305
+ 'Total'
306
+ );
307
+ results.forEach((r, i) => {
308
+ console.log(
309
+ String(i + 1).padEnd(3),
310
+ r.email.padEnd(34),
311
+ String(r.totalEvents).padEnd(7),
312
+ String(r.activeDays).padEnd(5),
313
+ String(r.gateStarts).padEnd(4),
314
+ String(r.gateApproveds).padEnd(4),
315
+ String(r.ticketsProcessed).padEnd(4),
316
+ `${r.depth}(${r.depthLabel.slice(0, 4)})`.padEnd(6),
317
+ String(r.impact).padEnd(7),
318
+ r.totalScore
319
+ );
320
+ });