@relipa/ai-flow-kit 0.0.9-beta.0 → 0.0.9

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 CHANGED
@@ -128,6 +128,16 @@ aiflow init --framework spring-boot,reactjs --adapter backlog,jira
128
128
 
129
129
  **Supported Adapters:** `backlog`, `jira`, `google-sheets`, `figma`, `figma-desktop`
130
130
 
131
+ **Figma adapters:**
132
+
133
+ | Adapter | Command | Auth | Requirement |
134
+ |---------|---------|------|-------------|
135
+ | `figma` | `ak init -a figma` | Personal Access Token (`figd_...`) | None — REST API |
136
+ | `figma-desktop` | `ak init -a figma-desktop` | Desktop session (no token needed) | Figma Desktop app installed & open |
137
+
138
+ > The `figma-desktop` adapter uses the official `@figma/mcp-server` package from Figma Inc. and is the recommended option if you already use Figma Desktop.
139
+ > See [Figma workflow guide](docs/common/workflows/figma.md) for usage with the `figma-to-component` skill.
140
+
131
141
  ---
132
142
 
133
143
  ### `aiflow sync-skills`
package/bin/aiflow.js CHANGED
@@ -1,409 +1,475 @@
1
1
  #!/usr/bin/env node
2
-
3
- const { program } = require('commander');
4
- const chalk = require('chalk');
5
- const packageJson = require('../package.json');
6
- const initCommand = require('../scripts/init');
7
- const updateCommand = require('../scripts/update');
8
- const doctorCommand = require('../scripts/doctor');
9
- const useCommand = require('../scripts/use');
10
- const TaskDetector = require('../scripts/detect');
11
- const validateCommand = require('../scripts/validate');
12
- const memoryCommand = require('../scripts/memory');
13
- const contextCommand = require('../scripts/context');
14
- const promptCommand = require('../scripts/prompt');
15
- const removeCommand = require('../scripts/remove');
16
- const guideCommand = require('../scripts/guide');
17
- const telemetryCommand= require('../scripts/telemetry/cli');
18
- const taskCommand = require('../scripts/task');
19
- const checkpointCommand = require('../scripts/checkpoint');
20
- const { record } = require('../scripts/telemetry/record');
21
- const { updateTaskGateState } = require('../scripts/task');
22
-
23
- // ── DEPRECATION WARNING ───────────────────────────────────────
24
- if (!process.env.AIFLOW_IS_AK) {
25
- console.warn(chalk.yellow('\n⚠ Warning: "aiflow" command is deprecated and will be removed in future versions.'));
26
- console.warn(chalk.yellow('Please use the shorter "ak" command instead.\n'));
27
- }
28
-
29
- program
30
- .version(packageJson.version)
31
- .description('AI Flow Kit CLI - AI-powered development workflows for teams');
32
-
33
- // ── GLOBAL TELEMETRY HOOK ────────────────────────────────────
34
- // We intercept raw argv here so we can catch --version, --help,
35
- // and invalid commands *before* Commander exits the process.
36
- // Also alias `-v` to commander's `-V` at the root only — subcommands still
37
- // keep `-v` as `--verbose`. And alias `--fw` to `--framework`.
38
- (() => {
39
- if (process.argv[2] === '-v') process.argv[2] = '-V';
40
- for (let i = 3; i < process.argv.length; i++) {
41
- if (process.argv[i] === '--fw') process.argv[i] = '--framework';
42
- }
43
- const args = process.argv.slice(2);
44
- let cmd = 'help';
45
- if (args.length > 0) {
46
- const first = args[0];
47
- if (['-V', '-v', '--version'].includes(first)) {
48
- cmd = 'version';
49
- } else if (['-h', '--help'].includes(first)) {
50
- cmd = 'help';
51
- } else if (first.startsWith('-')) {
52
- cmd = 'unknown_flag';
53
- if (args.includes('--help') || args.includes('-h')) cmd = 'help';
54
- } else if (first === 'memory') {
55
- const sec = args[1];
56
- cmd = (sec && !sec.startsWith('-')) ? `memory.${sec}` : 'memory';
57
- } else {
58
- cmd = first;
59
- if (args.includes('--help') || args.includes('-h')) {
60
- cmd = `${first}.help`;
61
- }
62
- }
63
- }
64
- // Record everything except telemetry and gate actions (gate records its own rich event)
65
- if (cmd !== 'telemetry' && !cmd.startsWith('telemetry.') && cmd !== 'gate') {
66
- record('command.invoked', { command: cmd });
67
- }
68
- })();
69
-
70
-
71
- // ── init ──────────────────────────────────────────────────────
72
- program
73
- .command('init')
74
- .alias('i')
75
- .description('Initialize AI Flow Kit in your project')
76
- .option('-f, --framework <types>', 'framework(s), comma-separated (e.g. spring-boot,reactjs)')
77
- .option('-a, --adapter <types>', 'adapter(s), comma-separated (e.g. backlog,jira)')
78
- .option('-e, --env <types>', 'AI environment(s)/tool(s), comma-separated (e.g. cursor,gemini,copilot)')
79
- .option('--with-rtk', 'force enable RTK token compression hook')
80
- .option('--no-rtk', 'skip RTK setup even if RTK is detected')
81
- .action((options) => {
82
- options.frameworks = options.framework
83
- ? options.framework.split(',').map(s => s.trim()).filter(Boolean)
84
- : [];
85
- options.adapters = options.adapter
86
- ? options.adapter.split(',').map(s => s.trim()).filter(Boolean)
87
- : [];
88
- options.aiTools = options.env
89
- ? options.env.split(',').map(s => s.trim()).filter(Boolean)
90
- : Object.keys(require('../scripts/init').AI_TOOL_FILES || {}).filter(t => t !== 'generic');
91
-
92
- record('command.invoked', { command: 'init' });
93
- initCommand(options);
94
- });
95
-
96
- // ── use ───────────────────────────────────────────────────────
97
- program
98
- .command('use [target]')
99
- .alias('u')
100
- .description('Load context from ticket, version, or file')
101
- .option('-m, --manual', 'manual context entry')
102
- .option('-f, --file <path>', 'load from file')
103
- .option('-s, --save <name>', 'save as named context')
104
- .option('-c, --coms', 'load all comments (alias for --with-comments)')
105
- .option('--with-comments', 'load all comments')
106
- .option('--cid <id>', 'load a specific comment by ID')
107
- .option('--clast <n>', 'load the last N comments', parseInt)
108
- .option('--cfrom <id>', 'load comments from ID N onward', parseInt)
109
- .option('--cto <id>', 'load comments up to ID N', parseInt)
110
- .option('-F, --fast', 'fast mode: minimal Q&A, concise requirement doc (Default)')
111
- .option('-U, --full', 'full mode: force complete analysis with Q&A (default)')
112
- .action((target, options) => {
113
- record('command.invoked', { command: 'use' });
114
- useCommand(target, options);
115
- });
116
-
117
- // ── prompt ────────────────────────────────────────────────────
118
- program
119
- .command('prompt [type]')
120
- .alias('p')
121
- .description('Generate AI prompt with context and rules')
122
- .option('-l, --list', 'list available prompt types')
123
- .option('-o, --output <file>', 'save to file')
124
- .option('-L, --lang <lang>', 'language: english (default) or vietnamese')
125
- .option('-d, --detail <level>', 'detail level: minimal | standard (default) | comprehensive')
126
- .action((type, options) => {
127
- record('command.invoked', { command: 'prompt' });
128
- promptCommand(type, options);
129
- });
130
-
131
- // ── detect ────────────────────────────────────────────────────
132
- program
133
- .command('detect [description]')
134
- .alias('d')
135
- .description('Auto-detect task type from description')
136
- .option('-v, --verbose', 'show detection details')
137
- .option('-t, --threshold <number>', 'confidence threshold (0-100)')
138
- .action((description, options) => {
139
- record('command.invoked', { command: 'detect' });
140
- const detector = new TaskDetector();
141
- const threshold = options.threshold ? parseInt(options.threshold) / 100 : 0.8;
142
- const result = detector.detect(description, threshold);
143
- console.log(chalk.cyan('\nTask Detection Results:\n'));
144
- console.log(detector.formatOutput(result, options.verbose));
145
- });
146
-
147
- // ── task ─────────────────────────────────────────────────────
148
- const taskCmd = program.command('task').alias('t').description('Manage multiple tasks pause, switch, resume');
149
-
150
- taskCmd
151
- .command('status')
152
- .alias('st')
153
- .description('Show active task and pending tasks')
154
- .action(() => { record('command.invoked', { command: 'task.status' }); taskCommand('status'); });
155
-
156
- taskCmd
157
- .command('list')
158
- .alias('ls')
159
- .description('List all saved tasks')
160
- .action(() => { record('command.invoked', { command: 'task.list' }); taskCommand('list'); });
161
-
162
- taskCmd
163
- .command('pause')
164
- .alias('p')
165
- .description('Pause current task and save progress')
166
- .option('-n, --note <text>', 'note about why you are pausing')
167
- .action((opts) => { record('command.invoked', { command: 'task.pause' }); taskCommand('pause', { note: opts.note }); });
168
-
169
- taskCmd
170
- .command('switch <ticket-id>')
171
- .alias('sw')
172
- .description('Pause current task and switch to another')
173
- .action((ticketId) => { record('command.invoked', { command: 'task.switch' }); taskCommand('switch', { taskId: ticketId }); });
174
-
175
- taskCmd
176
- .command('resume <ticket-id>')
177
- .alias('r')
178
- .description('Resume a paused task')
179
- .action((ticketId) => { record('command.invoked', { command: 'task.resume' }); taskCommand('resume', { taskId: ticketId }); });
180
-
181
- taskCmd
182
- .command('reset <ticket-id>')
183
- .alias('rst')
184
- .description('Reset task to Gate 1 (keeps ticket context, clears gate progress)')
185
- .action((ticketId) => { record('command.invoked', { command: 'task.reset' }); taskCommand('reset', { taskId: ticketId }); });
186
-
187
- taskCmd
188
- .command('remove <ticket-id>')
189
- .alias('rm')
190
- .description('Permanently delete all saved data for a task')
191
- .action((ticketId) => { record('command.invoked', { command: 'task.remove' }); taskCommand('remove', { taskId: ticketId }); });
192
-
193
- taskCmd
194
- .command('next')
195
- .alias('n')
196
- .description('Approve current gate and prepare next session (saves state for resume)')
197
- .option('-t, --ticket <id>', 'ticket ID (defaults to active task)')
198
- .action((opts) => {
199
- record('command.invoked', { command: 'task.next' });
200
- taskCommand('next', { taskId: opts.ticket });
201
- });
202
-
203
- // ── context ───────────────────────────────────────────────────
204
- program
205
- .command('context [action]')
206
- .alias('ctx')
207
- .description('Manage task contexts (list|show|save <name>|clear)')
208
- .option('-s, --save <name>', 'save current context with name')
209
- .option('-l, --load <name>', 'load saved context by name')
210
- .option('-d, --delete <name>', 'delete saved context')
211
- .option('--clear', 'clear all saved contexts')
212
- .option('--show', 'show current context')
213
- .action((action, options) => {
214
- record('command.invoked', { command: 'context' });
215
- contextCommand(action, options);
216
- });
217
-
218
- // ── checkpoint ────────────────────────────────────────────────
219
- program
220
- .command('checkpoint')
221
- .alias('cp')
222
- .description('Record token usage checkpoint (called by AI during gate work)')
223
- .requiredOption('-g, --gate <n>', 'gate number (1–5)', parseInt)
224
- .requiredOption('-s, --step <name>', 'step name (e.g. "tests-written")')
225
- .requiredOption('-n, --tokens <n>', 'estimated token count', parseInt)
226
- .option('-t, --ticket <id>', 'ticket ID (defaults to active task)')
227
- .action((opts) => {
228
- record('command.invoked', { command: 'checkpoint' });
229
- checkpointCommand({ gate: opts.gate, step: opts.step, tokens: opts.tokens, ticket: opts.ticket });
230
- });
231
-
232
- // ── validate ──────────────────────────────────────────────────
233
- program
234
- .command('validate <file>')
235
- .alias('vl')
236
- .description('Validate output against team rules')
237
- .option('-r, --ruleset <set>', 'rule set: default | strict | lenient', 'default')
238
- .option('-v, --verbose', 'detailed report')
239
- .option('-x, --fix', 'auto-fix trailing whitespace')
240
- .action((file, options) => {
241
- record('command.invoked', { command: 'validate' });
242
- validateCommand(file, {
243
- ruleset: options.ruleset,
244
- verbose: options.verbose,
245
- fix: options.fix
246
- });
247
- });
248
-
249
- // ── memory ────────────────────────────────────────────────────
250
- // Sub-commands to avoid the ambiguous "memory <action> --save key" pattern
251
- const memCmd = program.command('memory').alias('mem').description('Manage team knowledge and memory');
252
-
253
- memCmd
254
- .command('save <key> <value>')
255
- .alias('s')
256
- .description('Save a memory entry')
257
- .action((key, value) => { record('command.invoked', { command: 'memory.save' }); memoryCommand('save', { key, value }); });
258
-
259
- memCmd
260
- .command('get <key>')
261
- .alias('g')
262
- .description('Retrieve a memory entry')
263
- .action((key) => { record('command.invoked', { command: 'memory.get' }); memoryCommand('get', { key }); });
264
-
265
- memCmd
266
- .command('list')
267
- .alias('ls')
268
- .description('List all memories')
269
- .action(() => { record('command.invoked', { command: 'memory.list' }); memoryCommand('list'); });
270
-
271
- memCmd
272
- .command('search <query>')
273
- .alias('sr')
274
- .description('Search memories by keyword')
275
- .action((query) => { record('command.invoked', { command: 'memory.search' }); memoryCommand('search', { query }); });
276
-
277
- memCmd
278
- .command('delete <key>')
279
- .alias('d')
280
- .description('Delete a memory entry')
281
- .action((key) => { record('command.invoked', { command: 'memory.delete' }); memoryCommand('delete', { key }); });
282
-
283
- memCmd
284
- .command('clear')
285
- .alias('cl')
286
- .description('Clear all memories')
287
- .action(() => { record('command.invoked', { command: 'memory.clear' }); memoryCommand('clear'); });
288
-
289
- // ── guide ─────────────────────────────────────────────────────
290
- program
291
- .command('guide')
292
- .alias('g')
293
- .description('Show quickstart guide, architecture, and command reference')
294
- .option('-f, --flow', 'show architecture & flow diagram only')
295
- .option('-c, --commands', 'show command reference only')
296
- .action((options) => {
297
- record('command.invoked', { command: 'guide' });
298
- guideCommand(options);
299
- });
300
-
301
- // ── gate (telemetry for gate workflow events) ─────────────────
302
- program
303
- .command('gate <number> <action>')
304
- .description('Log a gate workflow event (start|approved) — called by AI during gate workflow')
305
- .option('--ticket <id>', 'ticket ID')
306
- .option('--ai-tool <tool>', 'AI tool name')
307
- .action((number, action, options) => {
308
- const gateNum = parseInt(number);
309
- if (isNaN(gateNum) || gateNum < 1 || gateNum > 5) return;
310
- if (!['start', 'approved'].includes(action)) return;
311
-
312
- const ticketId = options.ticket || '';
313
-
314
- record(`gate.${action}`, {
315
- gate: gateNum,
316
- ticket_id: ticketId,
317
- command: `gate${gateNum}.${action}`,
318
- ai_tool: options.aiTool || ''
319
- });
320
-
321
- // Keep task-state.json currentGate in sync (silent — never blocks the AI)
322
- if (ticketId) {
323
- updateTaskGateState(ticketId, gateNum, action).catch(() => {});
324
- }
325
-
326
- if (action === 'approved') {
327
- console.log(chalk.cyan(`\n Gate ${gateNum} approved!`));
328
- console.log(chalk.yellow(` Pro-Tip: To avoid context pollution, it's recommended to start a fresh chat session.`));
329
- console.log(chalk.gray(` Run: aiflow task next${ticketId ? ` --ticket ${ticketId}` : ''} to save progress,`));
330
- console.log(chalk.gray(` then open a NEW chatbox and type "continue".\n`));
331
- }
332
- });
333
-
334
- // ── remove ────────────────────────────────────────────────────
335
- program
336
- .command('remove')
337
- .alias('rm')
338
- .description('Remove ai-flow-kit from project, a cached version, or uninstall globally')
339
- .option('--version <ver>', 'remove a specific cached version from .aiflow/versions/')
340
- .option('-g, --global', 'uninstall the global npm package (removes `aiflow` command)')
341
- .action((options) => {
342
- record('command.invoked', { command: 'remove' });
343
- removeCommand(options);
344
- });
345
-
346
- // ── update ────────────────────────────────────────────────────
347
- program
348
- .command('update')
349
- .alias('up')
350
- .description('Update to latest version')
351
- .option('-f, --force', 'force update even if already on latest')
352
- .action((options) => {
353
- record('command.invoked', { command: 'update' });
354
- updateCommand(options);
355
- });
356
-
357
- // ── sync-skills ───────────────────────────────────────────────
358
- program
359
- .command('sync-skills')
360
- .alias('sync')
361
- .description('Manually synchronize AI Instruction files with local custom skills')
362
- .action(async () => {
363
- record('command.invoked', { command: 'sync-skills' });
364
- const projectDir = process.cwd();
365
- const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('../scripts/init');
366
- const fs = require('fs-extra');
367
- const path = require('path');
368
- const chalk = require('chalk');
369
-
370
- const stateFile = path.join(projectDir, '.aiflow', 'state.json');
371
- if (!(await fs.pathExists(stateFile))) {
372
- console.log(chalk.red('Project is not initialized. Please run `aiflow init` first.'));
373
- return;
374
- }
375
-
376
- const state = await fs.readJson(stateFile);
377
- const frameworks = state.frameworks || [];
378
- const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
379
-
380
- console.log(chalk.cyan('⟳ Syncing AI Instruction files...'));
381
- for (const fw of frameworks) {
382
- await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools);
383
- }
384
- await ensureAiflowGitignored(projectDir);
385
- console.log(chalk.green('✨ Sync completed!'));
386
- });
387
-
388
- // ── doctor ────────────────────────────────────────────────────
389
- program
390
- .command('doctor')
391
- .alias('dr')
392
- .description('Health check of AI Flow Kit setup')
393
- .option('-t, --ticket <id>', 'show token breakdown for specific ticket')
394
- .option('-v, --verbose', 'detailed output')
395
- .action((opts) => {
396
- record('command.invoked', { command: 'doctor' });
397
- doctorCommand({ ticket: opts.ticket, verbose: opts.verbose });
398
- });
399
-
400
- // ── telemetry ──────────────────────────────────────────────────
401
- program
402
- .command('telemetry [action]')
403
- .alias('tel')
404
- .description('Manage telemetry logging (enable|disable|status)')
405
- .action((action) => {
406
- telemetryCommand(action || 'status');
407
- });
408
-
409
- program.parse(process.argv);
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const packageJson = require('../package.json');
6
+ const initCommand = require('../scripts/init');
7
+ const updateCommand = require('../scripts/update');
8
+ const doctorCommand = require('../scripts/doctor');
9
+ const useCommand = require('../scripts/use');
10
+ const TaskDetector = require('../scripts/detect');
11
+ const validateCommand = require('../scripts/validate');
12
+ const memoryCommand = require('../scripts/memory');
13
+ const contextCommand = require('../scripts/context');
14
+ const promptCommand = require('../scripts/prompt');
15
+ const removeCommand = require('../scripts/remove');
16
+ const guideCommand = require('../scripts/guide');
17
+ const telemetryCommand= require('../scripts/telemetry/cli');
18
+ const taskCommand = require('../scripts/task');
19
+ const checkpointCommand = require('../scripts/checkpoint');
20
+ const { record } = require('../scripts/telemetry/record');
21
+ const { updateTaskGateState } = require('../scripts/task');
22
+
23
+ // ── DEPRECATION WARNING ───────────────────────────────────────
24
+ if (!process.env.AIFLOW_IS_AK) {
25
+ console.warn(chalk.yellow('\n⚠ Warning: "aiflow" command is deprecated and will be removed in future versions.'));
26
+ console.warn(chalk.yellow('Please use the shorter "ak" command instead.\n'));
27
+ }
28
+
29
+ program
30
+ .version(packageJson.version)
31
+ .description('AI Flow Kit CLI - AI-powered development workflows for teams');
32
+
33
+ // ── GLOBAL TELEMETRY HOOK ────────────────────────────────────
34
+ // We intercept raw argv here so we can catch --version, --help,
35
+ // and invalid commands *before* Commander exits the process.
36
+ // Also alias `-v` to commander's `-V` at the root only — subcommands still
37
+ // keep `-v` as `--verbose`. And alias `--fw` to `--framework`.
38
+ (() => {
39
+ if (process.argv[2] === '-v') process.argv[2] = '-V';
40
+ for (let i = 3; i < process.argv.length; i++) {
41
+ if (process.argv[i] === '--fw') process.argv[i] = '--framework';
42
+ }
43
+ const args = process.argv.slice(2);
44
+ let cmd = 'help';
45
+ if (args.length > 0) {
46
+ let first = args[0];
47
+
48
+ // 1. Phân giải alias cho root commands
49
+ const aliases = {
50
+ 'i': 'init',
51
+ 'u': 'use',
52
+ 'p': 'prompt',
53
+ 'd': 'detect',
54
+ 't': 'task',
55
+ 'ctx': 'context',
56
+ 'cp': 'checkpoint',
57
+ 'vl': 'validate',
58
+ 'mem': 'memory',
59
+ 'g': 'guide',
60
+ 'rm': 'remove',
61
+ 'up': 'update',
62
+ 'sync': 'sync-skills',
63
+ 'dr': 'doctor',
64
+ 'tel': 'telemetry'
65
+ };
66
+ if (aliases[first]) {
67
+ first = aliases[first];
68
+ }
69
+
70
+ if (['-V', '-v', '--version'].includes(first)) {
71
+ cmd = 'version';
72
+ } else if (['-h', '--help'].includes(first)) {
73
+ cmd = 'help';
74
+ } else if (first.startsWith('-')) {
75
+ cmd = 'unknown_flag';
76
+ if (args.includes('--help') || args.includes('-h')) cmd = 'help';
77
+ } else if (first === 'memory') {
78
+ const sec = args[1];
79
+ // Phân giải alias cho memory subcommands
80
+ let sub = sec;
81
+ if (sec && !sec.startsWith('-')) {
82
+ const memAliases = {
83
+ 's': 'save',
84
+ 'g': 'get',
85
+ 'ls': 'list',
86
+ 'sr': 'search',
87
+ 'd': 'delete',
88
+ 'cl': 'clear'
89
+ };
90
+ if (memAliases[sec]) sub = memAliases[sec];
91
+ cmd = `memory.${sub}`;
92
+ } else {
93
+ cmd = 'memory';
94
+ }
95
+ } else if (first === 'task') {
96
+ const sec = args[1];
97
+ // Phân giải alias subcommand của task
98
+ let sub = sec;
99
+ if (sec && !sec.startsWith('-')) {
100
+ const subAliases = {
101
+ 'st': 'status',
102
+ 'ls': 'list',
103
+ 'p': 'pause',
104
+ 'sw': 'switch',
105
+ 'r': 'resume',
106
+ 'rst': 'reset',
107
+ 'rm': 'remove',
108
+ 'n': 'next'
109
+ };
110
+ if (subAliases[sec]) sub = subAliases[sec];
111
+ cmd = `task.${sub}`;
112
+ } else {
113
+ cmd = 'task';
114
+ }
115
+ } else {
116
+ cmd = first;
117
+ if (args.includes('--help') || args.includes('-h')) {
118
+ cmd = `${first}.help`;
119
+ }
120
+ }
121
+ }
122
+ // Record everything except telemetry and gate actions (gate records its own rich event)
123
+ if (cmd !== 'telemetry' && !cmd.startsWith('telemetry.') && cmd !== 'gate') {
124
+ record('command.invoked', { command: cmd });
125
+ }
126
+ })();
127
+
128
+
129
+ // ── init ──────────────────────────────────────────────────────
130
+ program
131
+ .command('init')
132
+ .alias('i')
133
+ .description('Initialize AI Flow Kit in your project')
134
+ .option('-f, --framework <types>', 'framework(s), comma-separated (e.g. spring-boot,reactjs)')
135
+ .option('-a, --adapter <types>', 'adapter(s), comma-separated (e.g. backlog,jira)')
136
+ .option('-e, --env <types>', 'AI environment(s)/tool(s), comma-separated (e.g. cursor,gemini,copilot)')
137
+ .option('--with-rtk', 'force enable RTK token compression hook')
138
+ .option('--no-rtk', 'skip RTK setup even if RTK is detected')
139
+ .action((options) => {
140
+ options.frameworks = options.framework
141
+ ? options.framework.split(',').map(s => s.trim()).filter(Boolean)
142
+ : [];
143
+ options.adapters = options.adapter
144
+ ? options.adapter.split(',').map(s => s.trim()).filter(Boolean)
145
+ : [];
146
+ options.aiTools = options.env
147
+ ? options.env.split(',').map(s => s.trim()).filter(Boolean)
148
+ : Object.keys(require('../scripts/init').AI_TOOL_FILES || {}).filter(t => t !== 'generic');
149
+
150
+ initCommand(options);
151
+ });
152
+
153
+ // ── use ───────────────────────────────────────────────────────
154
+ program
155
+ .command('use [target]')
156
+ .alias('u')
157
+ .description('Load context from ticket, version, or file')
158
+ .option('-m, --manual', 'manual context entry')
159
+ .option('-f, --file <path>', 'load from file')
160
+ .option('-s, --save <name>', 'save as named context')
161
+ .option('-c, --coms', 'load all comments (alias for --with-comments)')
162
+ .option('--with-comments', 'load all comments')
163
+ .option('--cid <id>', 'load a specific comment by ID')
164
+ .option('--clast <n>', 'load the last N comments', parseInt)
165
+ .option('--cfrom <id>', 'load comments from ID N onward', parseInt)
166
+ .option('--cto <id>', 'load comments up to ID N', parseInt)
167
+ .option('-F, --fast', 'fast mode: minimal Q&A, concise requirement doc (Default)')
168
+ .option('-U, --full', 'full mode: force complete analysis with Q&A (default)')
169
+ .action((target, options) => {
170
+ useCommand(target, options);
171
+ });
172
+
173
+ // ── prompt ────────────────────────────────────────────────────
174
+ program
175
+ .command('prompt [type]')
176
+ .alias('p')
177
+ .description('Generate AI prompt with context and rules')
178
+ .option('-l, --list', 'list available prompt types')
179
+ .option('-o, --output <file>', 'save to file')
180
+ .option('-L, --lang <lang>', 'language: english (default) or vietnamese')
181
+ .option('-d, --detail <level>', 'detail level: minimal | standard (default) | comprehensive')
182
+ .action((type, options) => {
183
+ promptCommand(type, options);
184
+ });
185
+
186
+ // ── detect ────────────────────────────────────────────────────
187
+ program
188
+ .command('detect [description]')
189
+ .alias('d')
190
+ .description('Auto-detect task type from description')
191
+ .option('-v, --verbose', 'show detection details')
192
+ .option('-t, --threshold <number>', 'confidence threshold (0-100)')
193
+ .action((description, options) => {
194
+ const detector = new TaskDetector();
195
+ const threshold = options.threshold ? parseInt(options.threshold) / 100 : 0.8;
196
+ const result = detector.detect(description, threshold);
197
+ console.log(chalk.cyan('\nTask Detection Results:\n'));
198
+ console.log(detector.formatOutput(result, options.verbose));
199
+ });
200
+
201
+ // ── task ─────────────────────────────────────────────────────
202
+ const taskCmd = program.command('task').alias('t').description('Manage multiple tasks — pause, switch, resume');
203
+
204
+ taskCmd
205
+ .command('status')
206
+ .alias('st')
207
+ .description('Show active task and pending tasks')
208
+ .action(() => { taskCommand('status'); });
209
+
210
+ taskCmd
211
+ .command('list')
212
+ .alias('ls')
213
+ .description('List all saved tasks')
214
+ .action(() => { taskCommand('list'); });
215
+
216
+ taskCmd
217
+ .command('pause')
218
+ .alias('p')
219
+ .description('Pause current task and save progress')
220
+ .option('-n, --note <text>', 'note about why you are pausing')
221
+ .action((opts) => { taskCommand('pause', { note: opts.note }); });
222
+
223
+ taskCmd
224
+ .command('switch <ticket-id>')
225
+ .alias('sw')
226
+ .description('Pause current task and switch to another')
227
+ .action((ticketId) => { taskCommand('switch', { taskId: ticketId }); });
228
+
229
+ taskCmd
230
+ .command('resume <ticket-id>')
231
+ .alias('r')
232
+ .description('Resume a paused task')
233
+ .action((ticketId) => { taskCommand('resume', { taskId: ticketId }); });
234
+
235
+ taskCmd
236
+ .command('reset <ticket-id>')
237
+ .alias('rst')
238
+ .description('Reset task to Gate 1 (keeps ticket context, clears gate progress)')
239
+ .action((ticketId) => { taskCommand('reset', { taskId: ticketId }); });
240
+
241
+ taskCmd
242
+ .command('remove <ticket-id>')
243
+ .alias('rm')
244
+ .description('Permanently delete all saved data for a task')
245
+ .action((ticketId) => { taskCommand('remove', { taskId: ticketId }); });
246
+
247
+ taskCmd
248
+ .command('next')
249
+ .alias('n')
250
+ .description('Approve current gate and prepare next session (saves state for resume)')
251
+ .option('-t, --ticket <id>', 'ticket ID (defaults to active task)')
252
+ .action((opts) => {
253
+ taskCommand('next', { taskId: opts.ticket });
254
+ });
255
+
256
+ // ── context ───────────────────────────────────────────────────
257
+ program
258
+ .command('context [action]')
259
+ .alias('ctx')
260
+ .description('Manage task contexts (list|show|save <name>|clear)')
261
+ .option('-s, --save <name>', 'save current context with name')
262
+ .option('-l, --load <name>', 'load saved context by name')
263
+ .option('-d, --delete <name>', 'delete saved context')
264
+ .option('--clear', 'clear all saved contexts')
265
+ .option('--show', 'show current context')
266
+ .action((action, options) => {
267
+ contextCommand(action, options);
268
+ });
269
+
270
+ // ── checkpoint ────────────────────────────────────────────────
271
+ program
272
+ .command('checkpoint')
273
+ .alias('cp')
274
+ .description('Record token usage checkpoint (called by AI during gate work)')
275
+ .requiredOption('-g, --gate <n>', 'gate number (1–5)', parseInt)
276
+ .requiredOption('-s, --step <name>', 'step name (e.g. "tests-written")')
277
+ .requiredOption('-n, --tokens <n>', 'estimated token count', parseInt)
278
+ .option('-t, --ticket <id>', 'ticket ID (defaults to active task)')
279
+ .action((opts) => {
280
+ checkpointCommand({ gate: opts.gate, step: opts.step, tokens: opts.tokens, ticket: opts.ticket });
281
+ });
282
+
283
+ // ── validate ──────────────────────────────────────────────────
284
+ program
285
+ .command('validate <file>')
286
+ .alias('vl')
287
+ .description('Validate output against team rules')
288
+ .option('-r, --ruleset <set>', 'rule set: default | strict | lenient', 'default')
289
+ .option('-v, --verbose', 'detailed report')
290
+ .option('-x, --fix', 'auto-fix trailing whitespace')
291
+ .action((file, options) => {
292
+ validateCommand(file, {
293
+ ruleset: options.ruleset,
294
+ verbose: options.verbose,
295
+ fix: options.fix
296
+ });
297
+ });
298
+
299
+ // ── memory ────────────────────────────────────────────────────
300
+ // Sub-commands to avoid the ambiguous "memory <action> --save key" pattern
301
+ const memCmd = program.command('memory').alias('mem').description('Manage team knowledge and memory');
302
+
303
+ memCmd
304
+ .command('save <key> <value>')
305
+ .alias('s')
306
+ .description('Save a memory entry')
307
+ .action((key, value) => { memoryCommand('save', { key, value }); });
308
+
309
+ memCmd
310
+ .command('get <key>')
311
+ .alias('g')
312
+ .description('Retrieve a memory entry')
313
+ .action((key) => { memoryCommand('get', { key }); });
314
+
315
+ memCmd
316
+ .command('list')
317
+ .alias('ls')
318
+ .description('List all memories')
319
+ .action(() => { memoryCommand('list'); });
320
+
321
+ memCmd
322
+ .command('search <query>')
323
+ .alias('sr')
324
+ .description('Search memories by keyword')
325
+ .action((query) => { memoryCommand('search', { query }); });
326
+
327
+ memCmd
328
+ .command('delete <key>')
329
+ .alias('d')
330
+ .description('Delete a memory entry')
331
+ .action((key) => { memoryCommand('delete', { key }); });
332
+
333
+ memCmd
334
+ .command('clear')
335
+ .alias('cl')
336
+ .description('Clear all memories')
337
+ .action(() => { memoryCommand('clear'); });
338
+
339
+ // ── guide ─────────────────────────────────────────────────────
340
+ program
341
+ .command('guide')
342
+ .alias('g')
343
+ .description('Show quickstart guide, architecture, and command reference')
344
+ .option('-f, --flow', 'show architecture & flow diagram only')
345
+ .option('-c, --commands', 'show command reference only')
346
+ .action((options) => {
347
+ guideCommand(options);
348
+ });
349
+
350
+ // ── telemetry feedback helper ─────────────────────────────────
351
+ function showTelResult(result, label) {
352
+ try {
353
+ if (!result) return;
354
+ if (result.ok) {
355
+ console.log(chalk.green(` [tel] ✓ ${label} logged`));
356
+ } else if (result.reason === 'disabled') {
357
+ console.log(chalk.yellow(` [tel] skipped — telemetry is disabled (run 'aiflow telemetry enable')`));
358
+ } else if (result.reason === 'opted-out') {
359
+ console.log(chalk.yellow(` [tel] ⚠ skipped — repo has .aiflow/no-telemetry opt-out`));
360
+ } else if (result.reason === 'no-url') {
361
+ console.log(chalk.yellow(` [tel] skipped no server URL configured (run 'aiflow telemetry enable')`));
362
+ } else if (result.error) {
363
+ console.log(chalk.yellow(` [tel] error logging '${label}': ${result.error}`));
364
+ }
365
+ } catch (e) { /* never block main flow */ }
366
+ }
367
+
368
+ // ── gate (telemetry for gate workflow events) ─────────────────
369
+ program
370
+ .command('gate <number> <action>')
371
+ .description('Log a gate workflow event (start|approved) — called by AI during gate workflow')
372
+ .option('--ticket <id>', 'ticket ID')
373
+ .option('--ai-tool <tool>', 'AI tool name')
374
+ .action((number, action, options) => {
375
+ const gateNum = parseInt(number);
376
+ if (isNaN(gateNum) || gateNum < 1 || gateNum > 5) return;
377
+ if (!['start', 'approved'].includes(action)) return;
378
+
379
+ const ticketId = options.ticket || '';
380
+
381
+ const telResult = record(`gate.${action}`, {
382
+ gate: gateNum,
383
+ ticket_id: ticketId,
384
+ command: `gate${gateNum}.${action}`,
385
+ ai_tool: options.aiTool || ''
386
+ });
387
+ showTelResult(telResult, `gate${gateNum}.${action}`);
388
+
389
+ // Keep task-state.json currentGate in sync (silent — never blocks the AI)
390
+ if (ticketId) {
391
+ updateTaskGateState(ticketId, gateNum, action).catch(() => {});
392
+ }
393
+
394
+ if (action === 'approved') {
395
+ console.log(chalk.cyan(`\n Gate ${gateNum} approved!`));
396
+ console.log(chalk.yellow(` Pro-Tip: To avoid context pollution, it's recommended to start a fresh chat session.`));
397
+ console.log(chalk.gray(` Run: aiflow task next${ticketId ? ` --ticket ${ticketId}` : ''} to save progress,`));
398
+ console.log(chalk.gray(` then open a NEW chatbox and type "continue".\n`));
399
+ }
400
+ });
401
+
402
+ // ── remove ────────────────────────────────────────────────────
403
+ program
404
+ .command('remove')
405
+ .alias('rm')
406
+ .description('Remove ai-flow-kit from project, a cached version, or uninstall globally')
407
+ .option('--version <ver>', 'remove a specific cached version from .aiflow/versions/')
408
+ .option('-g, --global', 'uninstall the global npm package (removes `aiflow` command)')
409
+ .action((options) => {
410
+ removeCommand(options);
411
+ });
412
+
413
+ // ── update ────────────────────────────────────────────────────
414
+ program
415
+ .command('update')
416
+ .alias('up')
417
+ .description('Update to latest version')
418
+ .option('-f, --force', 'force update even if already on latest')
419
+ .action((options) => {
420
+ updateCommand(options);
421
+ });
422
+
423
+ // ── sync-skills ───────────────────────────────────────────────
424
+ program
425
+ .command('sync-skills')
426
+ .alias('sync')
427
+ .description('Manually synchronize AI Instruction files with local custom skills')
428
+ .action(async () => {
429
+ const projectDir = process.cwd();
430
+ const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('../scripts/init');
431
+ const fs = require('fs-extra');
432
+ const path = require('path');
433
+ const chalk = require('chalk');
434
+
435
+ const stateFile = path.join(projectDir, '.aiflow', 'state.json');
436
+ if (!(await fs.pathExists(stateFile))) {
437
+ console.log(chalk.red('Project is not initialized. Please run `aiflow init` first.'));
438
+ return;
439
+ }
440
+
441
+ const state = await fs.readJson(stateFile);
442
+ const frameworks = state.frameworks || [];
443
+ const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
444
+
445
+ console.log(chalk.cyan('⟳ Syncing AI Instruction files...'));
446
+ for (const fw of frameworks) {
447
+ await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools);
448
+ }
449
+ await ensureAiflowGitignored(projectDir);
450
+ console.log(chalk.green('✨ Sync completed!'));
451
+ });
452
+
453
+ // ── doctor ────────────────────────────────────────────────────
454
+ program
455
+ .command('doctor')
456
+ .alias('dr')
457
+ .description('Health check of AI Flow Kit setup')
458
+ .option('-t, --ticket <id>', 'show token breakdown for specific ticket')
459
+ .option('-v, --verbose', 'detailed output')
460
+ .action((opts) => {
461
+ doctorCommand({ ticket: opts.ticket, verbose: opts.verbose });
462
+ });
463
+
464
+ // ── telemetry ──────────────────────────────────────────────────
465
+ program
466
+ .command('telemetry [action]')
467
+ .alias('tel')
468
+ .description('Manage telemetry logging (enable|disable|status|log)')
469
+ .option('--event <event>', 'event name (for log action)')
470
+ .option('--detail <detail>', 'detail string (for log action)')
471
+ .action((action, options) => {
472
+ telemetryCommand(action || 'status', options);
473
+ });
474
+
475
+ program.parse(process.argv);
@@ -29,8 +29,8 @@ AI actively reads ticket + source code to understand the requirement:
29
29
 
30
30
  DO NOT just check format — **understand the content and propose solutions**.
31
31
 
32
- > **Telemetry:** Run `aiflow gate 1 start --ticket [ticket-id]` when starting this gate (auto-start or when developer types "start"/"Gate 1").
33
- > Run `aiflow gate 1 approved --ticket [ticket-id]` immediately when APPROVED is received.
32
+ > **Telemetry:** Run `ak gate 1 start --ticket [ticket-id]` when starting this gate (auto-start or when developer types "start"/"Gate 1"). Run as-is — do NOT append shell redirects like `2>$null` or `>/dev/null`.
33
+ > Run `ak gate 1 approved --ticket [ticket-id]` immediately when APPROVED is received. Run as-is — do NOT append shell redirects.
34
34
 
35
35
  ---
36
36
 
@@ -43,8 +43,8 @@ DO NOT just check format — **understand the content and propose solutions**.
43
43
  - Display: "GATE 2 PAUSED: type APPROVED to start coding".
44
44
  - CODE WILL NOT BE GENERATED until "APPROVED" is received.
45
45
 
46
- > **Telemetry:** Run `aiflow gate 2 start --ticket [ticket-id]` when starting this gate.
47
- > Run `aiflow gate 2 approved --ticket [ticket-id]` immediately when APPROVED is received.
46
+ > **Telemetry:** Run `ak gate 2 start --ticket [ticket-id]` when starting this gate. Run as-is — do NOT append shell redirects.
47
+ > Run `ak gate 2 approved --ticket [ticket-id]` immediately when APPROVED is received. Run as-is — do NOT append shell redirects.
48
48
 
49
49
  ---
50
50
 
@@ -57,7 +57,7 @@ Only runs after Gate 2 has been APPROVED.
57
57
  - Write tests FIRST — run to confirm FAIL -> implement -> PASS.
58
58
  - Bug fix EXTRA: `superpowers:systematic-debugging` + `investigate-bug` skill first.
59
59
 
60
- > **Telemetry:** Run `aiflow gate 3 start --ticket [ticket-id]` when starting this gate.
60
+ > **Telemetry:** Run `ak gate 3 start --ticket [ticket-id]` when starting this gate. Run as-is — do NOT append shell redirects.
61
61
 
62
62
  ---
63
63
 
@@ -75,8 +75,8 @@ Then: "GATE 4 PAUSED: type APPROVED or BUG: [description]"
75
75
  - Coding bug -> fix -> repeat Gate 4.
76
76
  - Requirement bug -> return to Gate 1.
77
77
 
78
- > **Telemetry:** Run `aiflow gate 4 start --ticket [ticket-id]` when starting this gate.
79
- > Run `aiflow gate 4 approved --ticket [ticket-id]` immediately when APPROVED is received.
78
+ > **Telemetry:** Run `ak gate 4 start --ticket [ticket-id]` when starting this gate. Run as-is — do NOT append shell redirects.
79
+ > Run `ak gate 4 approved --ticket [ticket-id]` immediately when APPROVED is received. Run as-is — do NOT append shell redirects.
80
80
 
81
81
  ---
82
82
 
@@ -87,4 +87,4 @@ Only runs after Gate 4 has been APPROVED.
87
87
  **INVOKE:** `superpowers:requesting-code-review`
88
88
  Guide on creating a Pull Request with the ticket link.
89
89
 
90
- > **Telemetry:** Run `aiflow gate 5 start --ticket [ticket-id]` when starting this gate.
90
+ > **Telemetry:** Run `ak gate 5 start --ticket [ticket-id]` when starting this gate. Run as-is — do NOT append shell redirects.
@@ -1,12 +1,22 @@
1
- # Gemini AI System Instructions
2
-
3
- You are an expert AI assistant specialized in this project's stack. Follow the Gate Workflow and Team Rules strictly.
4
-
5
- > **Important:** When starting a session, always read `.aiflow/context/current.json` to load ticket context and check the `plan/` directory for existing progress.
6
-
7
- If no instructions are automatically followed, wait for the developer to type **"start"** or **"Gate 1"** to start the analysis.
8
-
9
- ## Interaction Rules:
10
- - **COLLABORATIVE SKILLS:** When a skill (like `read-study-requirement`) says to "ask one question at a time", you MUST stop and wait for the developer's reply before proceeding.
11
- - **NEVER BATCH QUESTIONS:** Only ask one question per message.
12
- - **WAIT FOR APPROVAL:** Do not move to the next Gate until you receive "APPROVED".
1
+ # Gemini AI System Instructions
2
+
3
+ You are an expert AI assistant specialized in this project's stack. Follow the Gate Workflow and Team Rules strictly.
4
+
5
+ > **Important:** When starting a session, always read `.aiflow/context/current.json` to load ticket context and check the `plan/` directory for existing progress.
6
+
7
+ If no instructions are automatically followed, wait for the developer to type **"start"** or **"Gate 1"** to start the analysis.
8
+
9
+ ## Interaction Rules:
10
+ - **COLLABORATIVE SKILLS:** When a skill (like `read-study-requirement`) says to "ask one question at a time", you MUST stop and wait for the developer's reply before proceeding.
11
+ - **NEVER BATCH QUESTIONS:** Only ask one question per message.
12
+ - **WAIT FOR APPROVAL:** Do not move to the next Gate until you receive "APPROVED".
13
+ - **PROJECT CONVENTIONS:** Before writing any output file (plan, requirement, summary), read `custom/rules/project-conventions.md`. These rules override upstream skill defaults.
14
+
15
+ ## Telemetry Command (detect once, use everywhere)
16
+
17
+ Before the first gate, run this to detect the correct `ak` command for your environment:
18
+ ```bash
19
+ command -v ak >/dev/null 2>&1 && echo "USE: ak" || echo "USE: ak.cmd"
20
+ ```
21
+ Use whichever prints (e.g. `ak` on native Linux/Mac/Windows, `ak.cmd` on WSL without ak installed).
22
+ All `ak gate ...` examples below assume the detected command. Substitute accordingly.
@@ -7,7 +7,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
- ## [0.0.9] - 2026-05-21
10
+ ## [0.0.9] - 2026-05-25
11
11
 
12
12
  ### Added
13
13
 
@@ -17,11 +17,24 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
17
17
  - **4-tier framework detection in `figma-to-component` skill** — Detection order: (1) `CLAUDE.md` identifier → (2) project file scan (`app/` dir, `angular.json`, `nuxt.config.*`) → (3) fallback to Next.js/React. Previously only supported React and Vue.
18
18
  - **Figma workflow guide** — New developer guide at [`docs/common/workflows/figma.md`](figma.md) covering prerequisites (REST API vs Desktop), how to get node URLs, trigger commands, expected output format, review checklist, and troubleshooting table.
19
19
  - **MCP presets documentation** — Added full Figma REST API and Figma Desktop sections to [`custom/mcp-presets/README.md`](../../custom/mcp-presets/README.md), including a side-by-side comparison table.
20
+ - **`ak gate N start|approved` auto-syncs task state** — Calling `ak gate 1 start --ticket PROJ-33` now automatically advances `currentGate` in `task-state.json`, removing the need to always call `ak task next` for gate tracking. `approved` records the approval timestamp and advances to the next gate. Uses `Math.max()` guard to never regress gate progress.
21
+ - **Session telemetry enrichment** — `session.start` now captures the active AI model name and user email from config. `session.stop` records model, input/output token counts, and a prompt summary extracted from the session transcript (`transcript_path` in hookData).
22
+ - **`project-conventions.md`** — New mandatory override file at `custom/rules/project-conventions.md`. Enforces that Gate 2 plans are always saved to `plan/[ticket-id]/plan.md` (overriding the skill default `docs/superpowers/plans/`). Includes a checklist and path table for all Gate outputs.
23
+ - **Gate workflow: explicit plan output path** — Gate 2 instruction in `gate-workflow.md` now explicitly states that the plan must be saved to `plan/[ticket-id]/plan.md`, with a note that the `writing-plans` skill default path is overridden.
24
+ - **WSL clipboard support in `ak prompt`** — `ak prompt` now correctly copies output to the Windows clipboard when running inside WSL using `powershell.exe Set-Clipboard`. Previously fell through to `xclip`/`xsel` which are unavailable on WSL without extra setup.
25
+ - **`appscript.js`** — Google Apps Script source for the self-hosted telemetry backend. Receives HMAC-signed events from the `ak` CLI, validates them, and appends rows to Google Sheets.
26
+ - **Telemetry `record()` returns result object** — `record()` now returns `{ok: true}` on success or `{ok: false, reason, error}` on failure, enabling `ak gate` and `ak telemetry log` to display ✓ / ⚠ feedback per event.
27
+ - **`ak init` remembers previous framework selection** — Re-running `ak init` in an already-configured project now pre-selects the previously chosen frameworks (read from `.aiflow/state.json`) instead of falling back to auto-detection. A "Previously selected: ..." hint is shown above the checkbox list.
20
28
 
21
29
  ### Fixed
22
30
 
23
31
  - **Critical: `figma-to-component` skill used non-existent tool names** — The skill referenced `figma_get_file_nodes`, `figma_get_file_styles`, `figma_get_file_components`, and `figma_get_image` — none of which exist in `figma-developer-mcp` or any other MCP package, causing the skill to silently fail on every run. Replaced with the correct tools: `get_figma_data(fileKey, nodeId, depth=2)` and `download_figma_images(fileKey, nodes, localPath)`.
24
32
  - **Figma API token verification using wrong auth header** — `verifyFigma()` in `scripts/init.js` was sending `Authorization: Bearer <token>`, which is only valid for OAuth tokens. Figma Personal Access Tokens (`figd_...`) require `X-Figma-Token: <token>`. This caused every PAT to be rejected as invalid during `ak init -a figma` even when the token was correct.
33
+ - **Critical: `currentGate` not advancing after gate transitions** — `task-state.json` was not updating `currentGate` when the AI called `ak gate N start`, so resuming a task always restarted from the wrong gate. Added dedicated `updateTaskGateState()` and fixed `createOrActivateTaskState()` to use `Math.max()`.
34
+ - **Duplicate `session.start` telemetry event** — `session-start.js` was calling `record()` before reading stdin, causing the event to fire twice in some hook configurations. Fixed by moving `record()` inside the `stdin.on('end')` handler.
35
+ - **Short command aliases not resolving for telemetry** — `task` and `memory` subcommand short aliases (`t st`, `t ls`, `mem s`, etc.) were not being resolved before telemetry pre-processing, causing `unknown` in the command column. Fixed alias resolution in the pre-hook telemetry mapping.
36
+ - **`ak telemetry log` silent on failure** — `ak telemetry log --event X` previously swallowed all errors silently (no output). Now shows ✓ on success and ⚠ with reason (`disabled`, `opted-out`, `no-url`) on skip/failure.
37
+ - **Gate telemetry blocked by bash redirect error** — AI was appending `2>$null` (PowerShell syntax) to `ak gate` commands when following "run silently" instructions, causing bash to fail with `/usr/bin/bash: $null: ambiguous redirect`. Added explicit "do NOT append shell redirects" instruction to `gate-workflow.md` for all gate telemetry commands.
25
38
 
26
39
  ---
27
40
 
@@ -352,6 +352,45 @@ aiflow use PROJ-88
352
352
  claude # AI auto-starts: map dependencies → assess risk → output report
353
353
  ```
354
354
 
355
+ ### Figma Design → Code
356
+
357
+ Turn a Figma design into a React / Next.js / Vue / Angular component automatically.
358
+
359
+ **Step 1: Set up Figma MCP adapter (one-time)**
360
+
361
+ Choose one option:
362
+
363
+ | Option | Command | Requirement |
364
+ |--------|---------|-------------|
365
+ | Figma Desktop (recommended) | `ak init -a figma-desktop` | Figma Desktop app installed and open |
366
+ | Figma REST API | `ak init -a figma` | Figma Personal Access Token (`figd_...`) |
367
+
368
+ ```bash
369
+ # Option A: Figma Desktop — no API token needed, uses Desktop session
370
+ ak init -a figma-desktop
371
+
372
+ # Option B: Figma REST API — requires Personal Access Token
373
+ ak init -a figma
374
+ # Enter your token when prompted (Settings → Security → Personal access tokens)
375
+ ```
376
+
377
+ **Step 2: Get the Figma node URL**
378
+
379
+ In Figma Desktop or web, right-click the frame/component → **Copy link to selection**.
380
+
381
+ URL format: `https://www.figma.com/design/<fileKey>/...?node-id=<nodeId>`
382
+
383
+ **Step 3: Trigger the skill in Claude**
384
+
385
+ ```
386
+ Generate component from this Figma frame:
387
+ https://www.figma.com/design/abc123/MyDesign?node-id=12:34
388
+ ```
389
+
390
+ Claude uses the `figma-to-component` skill and auto-detects your framework (Next.js App Router, React, Vue, Angular).
391
+
392
+ > Full guide: [docs/common/workflows/figma.md](./workflows/figma.md)
393
+
355
394
  ---
356
395
 
357
396
  ## All Commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relipa/ai-flow-kit",
3
- "version": "0.0.9-beta.0",
3
+ "version": "0.0.9",
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": {
package/scripts/init.js CHANGED
@@ -903,9 +903,22 @@ async function init(options) {
903
903
 
904
904
  // ── Auto-detect or prompt for framework if not supplied ──────
905
905
  if (frameworks.length === 0) {
906
+ const stateFilePath = path.join(aiflowDir, 'state.json');
907
+ let previousFrameworks = [];
908
+ if (await fs.pathExists(stateFilePath)) {
909
+ const savedState = await fs.readJson(stateFilePath);
910
+ previousFrameworks = savedState.frameworks || [];
911
+ }
912
+
906
913
  const detected = await detectFramework(projectDir);
907
914
  const hint = chalk.gray(' Space to select/unselect · Enter to confirm · Leave empty to skip');
908
- if (detected) {
915
+ if (previousFrameworks.length > 0) {
916
+ const labels = previousFrameworks
917
+ .map(v => FRAMEWORK_CHOICES.find(c => c.value === v)?.name || v)
918
+ .join(', ');
919
+ console.log(chalk.cyan(`\n Previously selected: ${chalk.bold(labels)}`));
920
+ console.log(hint);
921
+ } else if (detected) {
909
922
  const label = FRAMEWORK_CHOICES.find(c => c.value === detected)?.name || detected;
910
923
  console.log(chalk.cyan(`\n Detected framework: ${chalk.bold(label)}`));
911
924
  console.log(hint);
@@ -917,7 +930,9 @@ async function init(options) {
917
930
  message: 'Select framework(s):',
918
931
  choices: FRAMEWORK_CHOICES.map(c => ({
919
932
  ...c,
920
- checked: c.value === detected,
933
+ checked: previousFrameworks.length > 0
934
+ ? previousFrameworks.includes(c.value)
935
+ : c.value === detected,
921
936
  })),
922
937
  });
923
938
  frameworks = chosen;
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'] });
@@ -232,12 +232,23 @@ module.exports = function telemetryCommand(action, options) {
232
232
  };
233
233
 
234
234
  function logEvent(options) {
235
- const { record } = require('./record');
236
- const event = options.event || 'custom_event';
237
- const detail = options.detail || '';
238
- if (!detail) {
239
- record(event);
240
- } else {
241
- record(event, { detail });
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
 
@@ -90,7 +91,7 @@ function record(eventType, payload = {}) {
90
91
 
91
92
  const lineStr = signEvent(event, cfg.team_secret);
92
93
  fs.appendFileSync(BUFFER_PATH, lineStr + '\n');
93
-
94
+
94
95
  // Check if buffer is getting huge (> 10MB cap). Simple clear
95
96
  try {
96
97
  if (fs.statSync(BUFFER_PATH).size > 10 * 1024 * 1024) {
@@ -99,8 +100,10 @@ function record(eventType, payload = {}) {
99
100
  } catch (e) {}
100
101
 
101
102
  maybeSpawnFlusher(cfg);
103
+ return { ok: true };
102
104
  } catch (err) {
103
105
  logInternalError(err);
106
+ return { ok: false, error: err.message };
104
107
  }
105
108
  }
106
109