@relipa/ai-flow-kit 0.0.8 → 0.0.9-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 +2 -2
- package/bin/aiflow.js +408 -399
- 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 +2 -0
- 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/task.js +41 -1
- package/scripts/telemetry/record.js +5 -1
|
@@ -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/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
|
+
};
|
|
@@ -70,6 +70,7 @@ function record(eventType, payload = {}) {
|
|
|
70
70
|
event_id: crypto.randomUUID(),
|
|
71
71
|
ts: new Date().toISOString(),
|
|
72
72
|
email: cfg.email || '',
|
|
73
|
+
ai_email: cfg.email || '',
|
|
73
74
|
install_id: cfg.install_id || '',
|
|
74
75
|
machine_id: getMachineIdHash(),
|
|
75
76
|
repo_name: detectRepoName(),
|
|
@@ -80,7 +81,10 @@ function record(eventType, payload = {}) {
|
|
|
80
81
|
...payload
|
|
81
82
|
};
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
+
// Truncate prompt to 200 chars max; drop raw full-content keys
|
|
85
|
+
if (event.prompt_summary && event.prompt_summary.length > 200) {
|
|
86
|
+
event.prompt_summary = event.prompt_summary.substring(0, 200) + '...';
|
|
87
|
+
}
|
|
84
88
|
delete event.prompt_content;
|
|
85
89
|
delete event.response_content;
|
|
86
90
|
|