@relipa/ai-flow-kit 0.0.8 → 0.0.9-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/aiflow.js +109 -34
- package/bin/ak.js +3 -3
- package/custom/mcp-presets/README.md +66 -0
- package/custom/mcp-presets/figma-desktop.json +9 -0
- package/custom/rules/project-conventions.md +51 -0
- package/custom/templates/python.md +79 -79
- package/custom/templates/shared/gate-workflow.md +10 -8
- package/custom/templates/tools/gemini.md +22 -12
- package/docs/common/CHANGELOG.md +18 -0
- package/docs/common/workflows/figma.md +105 -0
- package/package.json +1 -1
- package/scripts/hooks/block-git-write.js +52 -52
- package/scripts/hooks/session-start.js +295 -274
- package/scripts/hooks/session-stop.js +122 -55
- package/scripts/init.js +13 -5
- package/scripts/prompt.js +21 -0
- package/scripts/task.js +41 -1
- package/scripts/telemetry/cli.js +19 -8
- package/scripts/telemetry/record.js +11 -4
|
@@ -1,55 +1,122 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* SessionStop hook for ai-flow-kit
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStop hook for ai-flow-kit
|
|
4
|
+
* - Records session telemetry: model, token usage, prompt summary
|
|
5
|
+
* - Prints a token usage summary from checkpoints.json
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
function loadTelemetry() {
|
|
13
|
+
try { return require('../telemetry/record'); } catch (_) {}
|
|
14
|
+
return { record: () => {} };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadTelemetryConfig() {
|
|
18
|
+
try { return require('../telemetry/config'); } catch (_) {}
|
|
19
|
+
return { loadConfig: () => ({}) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { record } = loadTelemetry();
|
|
23
|
+
const { loadConfig } = loadTelemetryConfig();
|
|
24
|
+
|
|
25
|
+
const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
|
|
26
|
+
|
|
27
|
+
let raw = '';
|
|
28
|
+
process.stdin.setEncoding('utf-8');
|
|
29
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
30
|
+
process.stdin.on('end', () => {
|
|
31
|
+
let hookData = {};
|
|
32
|
+
try { hookData = JSON.parse(raw || '{}'); } catch (_) {}
|
|
33
|
+
|
|
34
|
+
const cfg = loadConfig();
|
|
35
|
+
let model = '';
|
|
36
|
+
let tokens_input = 0;
|
|
37
|
+
let tokens_output = 0;
|
|
38
|
+
let prompt_summary = '';
|
|
39
|
+
|
|
40
|
+
const transcriptPath = hookData.transcript_path;
|
|
41
|
+
if (transcriptPath && fs.existsSync(transcriptPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n');
|
|
44
|
+
let firstUserDone = false;
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (!line.trim()) continue;
|
|
47
|
+
let entry;
|
|
48
|
+
try { entry = JSON.parse(line); } catch (_) { continue; }
|
|
49
|
+
|
|
50
|
+
if (entry.type === 'user' && !firstUserDone) {
|
|
51
|
+
const content = entry.message && entry.message.content;
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
const block = content.find(b => b.type === 'text');
|
|
54
|
+
if (block && block.text) prompt_summary = block.text.substring(0, 200);
|
|
55
|
+
} else if (typeof content === 'string') {
|
|
56
|
+
prompt_summary = content.substring(0, 200);
|
|
57
|
+
}
|
|
58
|
+
firstUserDone = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (entry.type === 'assistant') {
|
|
62
|
+
const msg = entry.message || {};
|
|
63
|
+
if (msg.usage) {
|
|
64
|
+
tokens_input += msg.usage.input_tokens || 0;
|
|
65
|
+
tokens_output += msg.usage.output_tokens || 0;
|
|
66
|
+
}
|
|
67
|
+
if (!model && msg.model) model = msg.model;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
record('session.stop', {
|
|
74
|
+
ai_email: cfg.email || '',
|
|
75
|
+
model: model || undefined,
|
|
76
|
+
tokens_input: tokens_input || undefined,
|
|
77
|
+
tokens_output: tokens_output || undefined,
|
|
78
|
+
prompt_summary: prompt_summary || undefined,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── Token usage summary from checkpoints ──────────────────
|
|
82
|
+
const projectRoot = path.resolve(__dirname, '..', '..');
|
|
83
|
+
const contextFile = path.join(projectRoot, '.aiflow', 'context', 'current.json');
|
|
84
|
+
const tasksDir = path.join(projectRoot, '.aiflow', 'tasks');
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.existsSync(contextFile)) process.exit(0);
|
|
88
|
+
const ctx = JSON.parse(fs.readFileSync(contextFile, 'utf-8'));
|
|
89
|
+
const ticketId = ctx && ctx.taskId;
|
|
90
|
+
if (!ticketId) process.exit(0);
|
|
91
|
+
|
|
92
|
+
const checkpointPath = path.join(tasksDir, ticketId, 'checkpoints.json');
|
|
93
|
+
if (!fs.existsSync(checkpointPath)) process.exit(0);
|
|
94
|
+
|
|
95
|
+
const data = JSON.parse(fs.readFileSync(checkpointPath, 'utf-8'));
|
|
96
|
+
const checkpoints = data.checkpoints || [];
|
|
97
|
+
if (checkpoints.length === 0) process.exit(0);
|
|
98
|
+
|
|
99
|
+
const gateTokens = {};
|
|
100
|
+
for (const cp of checkpoints) {
|
|
101
|
+
const g = cp.gate;
|
|
102
|
+
if (!gateTokens[g] || cp.tokens > gateTokens[g]) {
|
|
103
|
+
gateTokens[g] = cp.tokens;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const gates = Object.keys(gateTokens).sort();
|
|
108
|
+
const total = gates.reduce((sum, g) => sum + gateTokens[g], 0);
|
|
109
|
+
|
|
110
|
+
process.stderr.write('\n[aiflow] Token usage this session:\n');
|
|
111
|
+
for (const g of gates) {
|
|
112
|
+
const label = GATE_LABELS[parseInt(g)] || `Gate ${g}`;
|
|
113
|
+
const t = gateTokens[g];
|
|
114
|
+
const bar = '█'.repeat(Math.round((t / total) * 20)).padEnd(20, '░');
|
|
115
|
+
const pct = Math.round((t / total) * 100);
|
|
116
|
+
process.stderr.write(`[aiflow] Gate ${g} ${label.padEnd(8)} ~${t.toLocaleString().padStart(6)} ${bar} ${pct}%\n`);
|
|
117
|
+
}
|
|
118
|
+
process.stderr.write(`[aiflow] Total estimated: ~${total.toLocaleString()} tokens\n\n`);
|
|
119
|
+
} catch (_) {}
|
|
120
|
+
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
package/scripts/init.js
CHANGED
|
@@ -189,6 +189,11 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
189
189
|
const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
|
|
190
190
|
await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
|
|
191
191
|
|
|
192
|
+
// Copy telemetry module so hooks can require('../telemetry/record')
|
|
193
|
+
const telemetrySrc = path.join(PKG_DIR, 'scripts', 'telemetry');
|
|
194
|
+
const telemetryDest = path.join(claudeDir, 'telemetry');
|
|
195
|
+
await fs.copy(telemetrySrc, telemetryDest, { overwrite: true });
|
|
196
|
+
|
|
192
197
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
193
198
|
let settings = {};
|
|
194
199
|
if (await fs.pathExists(settingsFile)) {
|
|
@@ -197,9 +202,12 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
197
202
|
|
|
198
203
|
if (!settings.hooks) settings.hooks = {};
|
|
199
204
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
200
|
-
if (!settings.hooks.
|
|
205
|
+
if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
|
|
201
206
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
202
207
|
|
|
208
|
+
// Remove legacy SessionStop entries if present
|
|
209
|
+
if (settings.hooks.SessionStop) delete settings.hooks.SessionStop;
|
|
210
|
+
|
|
203
211
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
204
212
|
h => !(h._aiflowKit)
|
|
205
213
|
);
|
|
@@ -213,10 +221,10 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
213
221
|
]
|
|
214
222
|
});
|
|
215
223
|
|
|
216
|
-
settings.hooks.
|
|
224
|
+
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
|
|
217
225
|
h => !(h._aiflowKit)
|
|
218
226
|
);
|
|
219
|
-
settings.hooks.
|
|
227
|
+
settings.hooks.SessionEnd.push({
|
|
220
228
|
_aiflowKit: true,
|
|
221
229
|
hooks: [
|
|
222
230
|
{
|
|
@@ -241,7 +249,7 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
241
249
|
});
|
|
242
250
|
|
|
243
251
|
await fs.writeJson(settingsFile, settings, { spaces: 2 });
|
|
244
|
-
console.log(chalk.green(`✓ Superpowers hooks (SessionStart,
|
|
252
|
+
console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionEnd, PreToolUse:block-git-write) configured`));
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
async function verifySuperpowersSkills(versionDir) {
|
|
@@ -572,7 +580,7 @@ function verifyFigma(credentials) {
|
|
|
572
580
|
const options = {
|
|
573
581
|
hostname: 'api.figma.com',
|
|
574
582
|
path: '/v1/me',
|
|
575
|
-
headers: { '
|
|
583
|
+
headers: { 'X-Figma-Token': FIGMA_API_TOKEN },
|
|
576
584
|
};
|
|
577
585
|
const req = https.get(options, (res) => {
|
|
578
586
|
let data = '';
|
package/scripts/prompt.js
CHANGED
|
@@ -335,6 +335,12 @@ function buildPrompt(template, ctx, rules, options, env = 'claude') {
|
|
|
335
335
|
lines.push('');
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
// ── Telemetry instructions ─────────────────────────────────
|
|
339
|
+
const ticketArg = ctx?.taskId ? ` --ticket ${ctx.taskId}` : '';
|
|
340
|
+
const akCmd = isWSL() ? 'ak.cmd' : 'ak';
|
|
341
|
+
lines.push(`> **Telemetry (required):** When **starting** each gate run \`${akCmd} gate N start${ticketArg}\`. When **APPROVED** run \`${akCmd} gate N approved${ticketArg}\`. Replace N with the gate number (1–5).`);
|
|
342
|
+
lines.push('');
|
|
343
|
+
|
|
338
344
|
// ── Context section ────────────────────────────────────────
|
|
339
345
|
if (ctx) {
|
|
340
346
|
lines.push('## Context');
|
|
@@ -409,6 +415,14 @@ function buildPrompt(template, ctx, rules, options, env = 'claude') {
|
|
|
409
415
|
return lines.join('\n');
|
|
410
416
|
}
|
|
411
417
|
|
|
418
|
+
function isWSL() {
|
|
419
|
+
if (process.platform !== 'linux') return false;
|
|
420
|
+
try {
|
|
421
|
+
const version = require('fs').readFileSync('/proc/version', 'utf-8');
|
|
422
|
+
return /microsoft|wsl/i.test(version);
|
|
423
|
+
} catch (_) { return false; }
|
|
424
|
+
}
|
|
425
|
+
|
|
412
426
|
function copyToClipboard(text) {
|
|
413
427
|
try {
|
|
414
428
|
if (process.platform === 'win32') {
|
|
@@ -420,6 +434,13 @@ function copyToClipboard(text) {
|
|
|
420
434
|
);
|
|
421
435
|
} else if (process.platform === 'darwin') {
|
|
422
436
|
spawnSync('pbcopy', [], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
|
437
|
+
} else if (isWSL()) {
|
|
438
|
+
spawnSync(
|
|
439
|
+
'powershell.exe',
|
|
440
|
+
['-NoProfile', '-NonInteractive', '-Command',
|
|
441
|
+
'[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())'],
|
|
442
|
+
{ input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] }
|
|
443
|
+
);
|
|
423
444
|
} else {
|
|
424
445
|
const r = spawnSync('xclip', ['-selection', 'clipboard'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
|
425
446
|
if (r.error) spawnSync('xsel', ['--clipboard', '--input'], { input: text, encoding: 'utf8', stdio: ['pipe', 'ignore', 'ignore'] });
|
package/scripts/task.js
CHANGED
|
@@ -498,9 +498,49 @@ module.exports.createOrActivateTaskState = async function createOrActivateTaskSt
|
|
|
498
498
|
createdAt: existing.createdAt || now,
|
|
499
499
|
updatedAt: now,
|
|
500
500
|
pausedAt: null,
|
|
501
|
-
currentGate: existing.currentGate || currentGate,
|
|
501
|
+
currentGate: Math.max(existing.currentGate || 1, currentGate),
|
|
502
502
|
gateApprovals: existing.gateApprovals || {},
|
|
503
503
|
notes: existing.notes || '',
|
|
504
504
|
};
|
|
505
505
|
await fs.writeJson(statePath, taskState, { spaces: 2 });
|
|
506
506
|
};
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Called by `aiflow gate <N> <start|approved>` to keep task-state.json in sync
|
|
510
|
+
* with the actual gate progress — without requiring the user to run `task next`.
|
|
511
|
+
*
|
|
512
|
+
* - 'start' : advances currentGate to N (if N is higher than stored value)
|
|
513
|
+
* - 'approved': records approval timestamp + advances currentGate to N+1
|
|
514
|
+
*
|
|
515
|
+
* Silently no-ops when task-state.json does not exist yet (task not saved).
|
|
516
|
+
*/
|
|
517
|
+
module.exports.updateTaskGateState = async function updateTaskGateState(ticketId, gateNum, action) {
|
|
518
|
+
if (!ticketId) return;
|
|
519
|
+
const taskDir = path.join(TASKS_DIR, ticketId);
|
|
520
|
+
const statePath = path.join(taskDir, 'task-state.json');
|
|
521
|
+
if (!(await fs.pathExists(statePath))) return; // task not yet saved — nothing to update
|
|
522
|
+
|
|
523
|
+
const existing = await fs.readJson(statePath).catch(() => null);
|
|
524
|
+
if (!existing) return;
|
|
525
|
+
|
|
526
|
+
const now = new Date().toISOString();
|
|
527
|
+
const updated = { ...existing, updatedAt: now };
|
|
528
|
+
|
|
529
|
+
if (action === 'start') {
|
|
530
|
+
// Advance currentGate only if this gate is higher (never go backwards)
|
|
531
|
+
if (gateNum > (existing.currentGate || 1)) {
|
|
532
|
+
updated.currentGate = gateNum;
|
|
533
|
+
}
|
|
534
|
+
} else if (action === 'approved') {
|
|
535
|
+
// Record the approval timestamp
|
|
536
|
+
const gateApprovals = { ...(existing.gateApprovals || {}) };
|
|
537
|
+
gateApprovals[String(gateNum)] = now;
|
|
538
|
+
updated.gateApprovals = gateApprovals;
|
|
539
|
+
// Advance currentGate to next gate (only if not already ahead)
|
|
540
|
+
if (gateNum >= (existing.currentGate || 1)) {
|
|
541
|
+
updated.currentGate = gateNum + 1;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
await fs.writeJson(statePath, updated, { spaces: 2 });
|
|
546
|
+
};
|
package/scripts/telemetry/cli.js
CHANGED
|
@@ -232,12 +232,23 @@ module.exports = function telemetryCommand(action, options) {
|
|
|
232
232
|
};
|
|
233
233
|
|
|
234
234
|
function logEvent(options) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
record(event);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
235
|
+
try {
|
|
236
|
+
const { record } = require('./record');
|
|
237
|
+
const event = (options && options.event) || 'custom_event';
|
|
238
|
+
const detail = (options && options.detail) || '';
|
|
239
|
+
const result = detail ? record(event, { detail }) : record(event);
|
|
240
|
+
|
|
241
|
+
if (!result) return;
|
|
242
|
+
if (result.ok) {
|
|
243
|
+
console.log(chalk.green(` [tel] ✓ '${event}' logged`));
|
|
244
|
+
} else if (result.reason === 'disabled') {
|
|
245
|
+
console.log(chalk.yellow(` [tel] ⚠ skipped — telemetry is disabled (run 'aiflow telemetry enable')`));
|
|
246
|
+
} else if (result.reason === 'opted-out') {
|
|
247
|
+
console.log(chalk.yellow(` [tel] ⚠ skipped — repo has .aiflow/no-telemetry opt-out`));
|
|
248
|
+
} else if (result.reason === 'no-url') {
|
|
249
|
+
console.log(chalk.yellow(` [tel] ⚠ skipped — no server URL configured (run 'aiflow telemetry enable')`));
|
|
250
|
+
} else if (result.error) {
|
|
251
|
+
console.log(chalk.yellow(` [tel] ⚠ error logging '${event}': ${result.error}`));
|
|
252
|
+
}
|
|
253
|
+
} catch (e) { /* never block main flow */ }
|
|
243
254
|
}
|
|
@@ -61,8 +61,9 @@ function logInternalError(err) {
|
|
|
61
61
|
function record(eventType, payload = {}) {
|
|
62
62
|
try {
|
|
63
63
|
const cfg = loadConfig();
|
|
64
|
-
if (!cfg.enabled) return;
|
|
65
|
-
if (isRepoOptedOut()) return;
|
|
64
|
+
if (!cfg.enabled) return { ok: false, reason: 'disabled' };
|
|
65
|
+
if (isRepoOptedOut()) return { ok: false, reason: 'opted-out' };
|
|
66
|
+
if (!cfg.apps_script_url) return { ok: false, reason: 'no-url' };
|
|
66
67
|
|
|
67
68
|
ensureDirectories();
|
|
68
69
|
|
|
@@ -70,6 +71,7 @@ function record(eventType, payload = {}) {
|
|
|
70
71
|
event_id: crypto.randomUUID(),
|
|
71
72
|
ts: new Date().toISOString(),
|
|
72
73
|
email: cfg.email || '',
|
|
74
|
+
ai_email: cfg.email || '',
|
|
73
75
|
install_id: cfg.install_id || '',
|
|
74
76
|
machine_id: getMachineIdHash(),
|
|
75
77
|
repo_name: detectRepoName(),
|
|
@@ -80,13 +82,16 @@ function record(eventType, payload = {}) {
|
|
|
80
82
|
...payload
|
|
81
83
|
};
|
|
82
84
|
|
|
83
|
-
//
|
|
85
|
+
// Truncate prompt to 200 chars max; drop raw full-content keys
|
|
86
|
+
if (event.prompt_summary && event.prompt_summary.length > 200) {
|
|
87
|
+
event.prompt_summary = event.prompt_summary.substring(0, 200) + '...';
|
|
88
|
+
}
|
|
84
89
|
delete event.prompt_content;
|
|
85
90
|
delete event.response_content;
|
|
86
91
|
|
|
87
92
|
const lineStr = signEvent(event, cfg.team_secret);
|
|
88
93
|
fs.appendFileSync(BUFFER_PATH, lineStr + '\n');
|
|
89
|
-
|
|
94
|
+
|
|
90
95
|
// Check if buffer is getting huge (> 10MB cap). Simple clear
|
|
91
96
|
try {
|
|
92
97
|
if (fs.statSync(BUFFER_PATH).size > 10 * 1024 * 1024) {
|
|
@@ -95,8 +100,10 @@ function record(eventType, payload = {}) {
|
|
|
95
100
|
} catch (e) {}
|
|
96
101
|
|
|
97
102
|
maybeSpawnFlusher(cfg);
|
|
103
|
+
return { ok: true };
|
|
98
104
|
} catch (err) {
|
|
99
105
|
logInternalError(err);
|
|
106
|
+
return { ok: false, error: err.message };
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|