@tdsoft-tech/aikit 0.1.31 → 0.1.33

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.
@@ -144,6 +144,34 @@ var init_paths = __esm({
144
144
  */
145
145
  claudeAgents(project) {
146
146
  return join(this.claudeConfig(project ? "project" : "user"), "agents");
147
+ },
148
+ /**
149
+ * Get Cursor configuration directory
150
+ */
151
+ cursorConfig(scope) {
152
+ if (scope === "project") {
153
+ return join(process.cwd(), ".cursor");
154
+ }
155
+ const base = process.platform === "win32" ? join(homedir(), "AppData", "Local", "Cursor") : join(homedir(), ".cursor");
156
+ return base;
157
+ },
158
+ /**
159
+ * Get Cursor commands directory
160
+ */
161
+ cursorCommands(project) {
162
+ return join(this.cursorConfig(project ? "project" : "user"), "commands");
163
+ },
164
+ /**
165
+ * Get Cursor skills directory
166
+ */
167
+ cursorSkills(project) {
168
+ return join(this.cursorConfig(project ? "project" : "user"), "skills");
169
+ },
170
+ /**
171
+ * Get Cursor agents directory
172
+ */
173
+ cursorAgents(project) {
174
+ return join(this.cursorConfig(project ? "project" : "user"), "agents");
147
175
  }
148
176
  };
149
177
  }
@@ -173,6 +201,756 @@ var init_version = __esm({
173
201
  }
174
202
  });
175
203
 
204
+ // src/utils/logger.ts
205
+ import chalk from "chalk";
206
+ var logger;
207
+ var init_logger = __esm({
208
+ "src/utils/logger.ts"() {
209
+ "use strict";
210
+ init_esm_shims();
211
+ logger = {
212
+ info(...args) {
213
+ console.log(chalk.blue("\u2139"), ...args);
214
+ },
215
+ success(...args) {
216
+ console.log(chalk.green("\u2713"), ...args);
217
+ },
218
+ warn(...args) {
219
+ console.log(chalk.yellow("\u26A0"), ...args);
220
+ },
221
+ error(...args) {
222
+ console.error(chalk.red("\u2716"), ...args);
223
+ },
224
+ debug(...args) {
225
+ if (process.env.DEBUG || process.env.AIKIT_DEBUG) {
226
+ console.log(chalk.gray("\u22EF"), ...args);
227
+ }
228
+ },
229
+ step(step, total, message) {
230
+ console.log(chalk.cyan(`[${step}/${total}]`), message);
231
+ },
232
+ header(message) {
233
+ console.log(chalk.bold.underline(`
234
+ ${message}
235
+ `));
236
+ },
237
+ list(items, prefix = "\u2022") {
238
+ for (const item of items) {
239
+ console.log(` ${prefix} ${item}`);
240
+ }
241
+ }
242
+ };
243
+ }
244
+ });
245
+
246
+ // src/core/database/schema.ts
247
+ function initializeSchema(db) {
248
+ try {
249
+ logger.info("Initializing Figma database schema...");
250
+ db.pragma("foreign_keys = ON");
251
+ db.pragma("journal_mode = WAL");
252
+ Object.entries(SCHEMA_SQL).forEach(([tableName, sql]) => {
253
+ logger.debug(`Creating table: ${tableName}`);
254
+ db.exec(sql);
255
+ });
256
+ Object.entries(INDEX_SQL).forEach(([indexName, sql]) => {
257
+ logger.debug(`Creating index: ${indexName}`);
258
+ db.exec(sql);
259
+ });
260
+ Object.entries(TRIGGER_SQL).forEach(([triggerName, sql]) => {
261
+ logger.debug(`Creating trigger: ${triggerName}`);
262
+ db.exec(sql);
263
+ });
264
+ const currentVersion = getCurrentSchemaVersion(db);
265
+ if (currentVersion < DATABASE_VERSION) {
266
+ db.prepare("INSERT OR REPLACE INTO schema_version (version) VALUES (?)").run(DATABASE_VERSION);
267
+ logger.info(`Schema updated to version ${DATABASE_VERSION}`);
268
+ }
269
+ logger.info("Database schema initialized successfully");
270
+ } catch (error) {
271
+ logger.error(`Failed to initialize database schema: ${error instanceof Error ? error.message : String(error)}`);
272
+ throw error;
273
+ }
274
+ }
275
+ function getCurrentSchemaVersion(db) {
276
+ try {
277
+ const result = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
278
+ return result?.version || 0;
279
+ } catch {
280
+ return 0;
281
+ }
282
+ }
283
+ var DATABASE_VERSION, SCHEMA_SQL, INDEX_SQL, TRIGGER_SQL;
284
+ var init_schema = __esm({
285
+ "src/core/database/schema.ts"() {
286
+ "use strict";
287
+ init_esm_shims();
288
+ init_logger();
289
+ DATABASE_VERSION = 1;
290
+ SCHEMA_SQL = {
291
+ // Files table - stores Figma file metadata
292
+ figma_files: `
293
+ CREATE TABLE IF NOT EXISTS figma_files (
294
+ id TEXT PRIMARY KEY,
295
+ url TEXT NOT NULL UNIQUE,
296
+ name TEXT,
297
+ file_key TEXT NOT NULL,
298
+ last_analyzed DATETIME,
299
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
300
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
301
+ )
302
+ `,
303
+ // Screens table - stores screen/frame information
304
+ figma_screens: `
305
+ CREATE TABLE IF NOT EXISTS figma_screens (
306
+ id TEXT PRIMARY KEY,
307
+ file_id TEXT NOT NULL,
308
+ name TEXT NOT NULL,
309
+ width INTEGER,
310
+ height INTEGER,
311
+ type TEXT,
312
+ description TEXT,
313
+ children_count INTEGER,
314
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE
315
+ )
316
+ `,
317
+ // Nodes table - stores hierarchical component structure
318
+ figma_nodes: `
319
+ CREATE TABLE IF NOT EXISTS figma_nodes (
320
+ id TEXT PRIMARY KEY,
321
+ file_id TEXT NOT NULL,
322
+ screen_id TEXT,
323
+ parent_id TEXT,
324
+ name TEXT NOT NULL,
325
+ type TEXT NOT NULL,
326
+ content TEXT,
327
+ position_x REAL,
328
+ position_y REAL,
329
+ width REAL,
330
+ height REAL,
331
+ styles TEXT, -- JSON string
332
+ children_ids TEXT, -- JSON string array
333
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE,
334
+ FOREIGN KEY (screen_id) REFERENCES figma_screens(id) ON DELETE CASCADE,
335
+ FOREIGN KEY (parent_id) REFERENCES figma_nodes(id) ON DELETE CASCADE
336
+ )
337
+ `,
338
+ // Design tokens table - stores extracted design tokens
339
+ design_tokens: `
340
+ CREATE TABLE IF NOT EXISTS design_tokens (
341
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
342
+ file_id TEXT NOT NULL,
343
+ type TEXT NOT NULL, -- 'color', 'typography', 'spacing', 'component'
344
+ name TEXT NOT NULL,
345
+ value TEXT NOT NULL,
346
+ category TEXT,
347
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE
348
+ )
349
+ `,
350
+ // Assets table - stores downloadable assets
351
+ figma_assets: `
352
+ CREATE TABLE IF NOT EXISTS figma_assets (
353
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
354
+ file_id TEXT NOT NULL,
355
+ node_id TEXT NOT NULL,
356
+ node_name TEXT,
357
+ node_type TEXT,
358
+ format TEXT, -- 'png', 'svg', 'jpg'
359
+ file_path TEXT,
360
+ url TEXT,
361
+ width INTEGER,
362
+ height INTEGER,
363
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE
364
+ )
365
+ `,
366
+ // Schema version tracking
367
+ schema_version: `
368
+ CREATE TABLE IF NOT EXISTS schema_version (
369
+ version INTEGER PRIMARY KEY,
370
+ applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
371
+ )
372
+ `
373
+ };
374
+ INDEX_SQL = {
375
+ // Files indexes
376
+ idx_files_url: "CREATE INDEX IF NOT EXISTS idx_files_url ON figma_files(url)",
377
+ idx_files_file_key: "CREATE INDEX IF NOT EXISTS idx_files_file_key ON figma_files(file_key)",
378
+ idx_files_last_analyzed: "CREATE INDEX IF NOT EXISTS idx_files_last_analyzed ON figma_files(last_analyzed)",
379
+ // Screens indexes
380
+ idx_screens_file_id: "CREATE INDEX IF NOT EXISTS idx_screens_file_id ON figma_screens(file_id)",
381
+ idx_screens_type: "CREATE INDEX IF NOT EXISTS idx_screens_type ON figma_screens(type)",
382
+ // Nodes indexes
383
+ idx_nodes_file_id: "CREATE INDEX IF NOT EXISTS idx_nodes_file_id ON figma_nodes(file_id)",
384
+ idx_nodes_screen_id: "CREATE INDEX IF NOT EXISTS idx_nodes_screen_id ON figma_nodes(screen_id)",
385
+ idx_nodes_parent_id: "CREATE INDEX IF NOT EXISTS idx_nodes_parent_id ON figma_nodes(parent_id)",
386
+ idx_nodes_type: "CREATE INDEX IF NOT EXISTS idx_nodes_type ON figma_nodes(type)",
387
+ // Design tokens indexes
388
+ idx_tokens_file_id: "CREATE INDEX IF NOT EXISTS idx_tokens_file_id ON design_tokens(file_id)",
389
+ idx_tokens_type: "CREATE INDEX IF NOT EXISTS idx_tokens_type ON design_tokens(type)",
390
+ idx_tokens_file_type: "CREATE INDEX IF NOT EXISTS idx_tokens_file_type ON design_tokens(file_id, type)",
391
+ // Assets indexes
392
+ idx_assets_file_id: "CREATE INDEX IF NOT EXISTS idx_assets_file_id ON figma_assets(file_id)",
393
+ idx_assets_node_id: "CREATE INDEX IF NOT EXISTS idx_assets_node_id ON figma_assets(node_id)",
394
+ idx_assets_format: "CREATE INDEX IF NOT EXISTS idx_assets_format ON figma_assets(format)"
395
+ };
396
+ TRIGGER_SQL = {
397
+ // Update timestamp trigger for files table
398
+ trigger_files_updated_at: `
399
+ CREATE TRIGGER IF NOT EXISTS trigger_files_updated_at
400
+ AFTER UPDATE ON figma_files
401
+ FOR EACH ROW
402
+ BEGIN
403
+ UPDATE figma_files SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
404
+ END
405
+ `
406
+ };
407
+ }
408
+ });
409
+
410
+ // src/core/database/figma-db.ts
411
+ import Database from "better-sqlite3";
412
+ var FigmaDatabase;
413
+ var init_figma_db = __esm({
414
+ "src/core/database/figma-db.ts"() {
415
+ "use strict";
416
+ init_esm_shims();
417
+ init_logger();
418
+ init_schema();
419
+ FigmaDatabase = class {
420
+ db;
421
+ constructor(dbPath = ":memory:") {
422
+ try {
423
+ this.db = new Database(dbPath);
424
+ initializeSchema(this.db);
425
+ logger.info(`Figma database initialized at: ${dbPath}`);
426
+ } catch (error) {
427
+ logger.error(`Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`);
428
+ throw error;
429
+ }
430
+ }
431
+ /**
432
+ * Export database data to JSON
433
+ */
434
+ async exportData(fileId) {
435
+ try {
436
+ const files = fileId ? [await this.getFile(fileId)].filter(Boolean) : await this.listFiles();
437
+ const fileIds = files.map((f) => f.id);
438
+ let screens = [];
439
+ let nodes = [];
440
+ let tokens = [];
441
+ let assets = [];
442
+ for (const fId of fileIds) {
443
+ screens.push(...await this.getScreensByFile(fId));
444
+ nodes.push(...await this.getNodesByFile(fId));
445
+ tokens.push(...await this.getDesignTokensByFile(fId));
446
+ assets.push(...await this.getAssetsByFile(fId));
447
+ }
448
+ return {
449
+ files,
450
+ screens,
451
+ nodes,
452
+ tokens,
453
+ assets,
454
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
455
+ };
456
+ } catch (error) {
457
+ logger.error(`Failed to export data: ${error instanceof Error ? error.message : String(error)}`);
458
+ throw error;
459
+ }
460
+ }
461
+ /**
462
+ * Import database data from JSON
463
+ */
464
+ async importData(data) {
465
+ try {
466
+ const stats = { files: 0, screens: 0, nodes: 0, tokens: 0, assets: 0 };
467
+ for (const file of data.files) {
468
+ await this.upsertFile(file);
469
+ stats.files++;
470
+ }
471
+ for (const screen of data.screens) {
472
+ await this.upsertScreen(screen);
473
+ stats.screens++;
474
+ }
475
+ for (const node of data.nodes) {
476
+ await this.upsertNode(node);
477
+ stats.nodes++;
478
+ }
479
+ for (const token of data.tokens) {
480
+ await this.upsertDesignToken(token);
481
+ stats.tokens++;
482
+ }
483
+ for (const asset of data.assets) {
484
+ await this.upsertAsset(asset);
485
+ stats.assets++;
486
+ }
487
+ logger.info(`Imported ${stats.files} files, ${stats.screens} screens, ${stats.nodes} nodes, ${stats.tokens} tokens, ${stats.assets} assets`);
488
+ return stats;
489
+ } catch (error) {
490
+ logger.error(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
491
+ throw error;
492
+ }
493
+ }
494
+ /**
495
+ * Utility Operations
496
+ */
497
+ async upsertFile(file) {
498
+ try {
499
+ const lastAnalyzed = file.last_analyzed instanceof Date ? file.last_analyzed.toISOString() : file.last_analyzed || null;
500
+ const existing = this.db.prepare("SELECT id FROM figma_files WHERE id = ?").get(file.id);
501
+ if (existing) {
502
+ this.db.prepare(`
503
+ UPDATE figma_files
504
+ SET url = ?, name = ?, file_key = ?, last_analyzed = ?, updated_at = CURRENT_TIMESTAMP
505
+ WHERE id = ?
506
+ `).run(file.url, file.name || null, file.file_key, lastAnalyzed, file.id);
507
+ return { id: file.id, created: false, updated: true };
508
+ } else {
509
+ this.db.prepare(`
510
+ INSERT INTO figma_files (id, url, name, file_key, last_analyzed)
511
+ VALUES (?, ?, ?, ?, ?)
512
+ `).run(file.id, file.url, file.name || null, file.file_key, lastAnalyzed);
513
+ return { id: file.id, created: true, updated: false };
514
+ }
515
+ } catch (error) {
516
+ logger.error(`Failed to upsert file: ${error instanceof Error ? error.message : String(error)}`);
517
+ throw error;
518
+ }
519
+ }
520
+ async getFile(id) {
521
+ try {
522
+ const row = this.db.prepare("SELECT * FROM figma_files WHERE id = ?").get(id);
523
+ if (!row) return null;
524
+ return {
525
+ ...row,
526
+ created_at: new Date(row.created_at),
527
+ updated_at: new Date(row.updated_at),
528
+ last_analyzed: row.last_analyzed ? new Date(row.last_analyzed) : void 0
529
+ };
530
+ } catch (error) {
531
+ logger.error(`Failed to get file: ${error instanceof Error ? error.message : String(error)}`);
532
+ return null;
533
+ }
534
+ }
535
+ async getFileByUrl(url) {
536
+ try {
537
+ const row = this.db.prepare("SELECT * FROM figma_files WHERE url = ?").get(url);
538
+ if (!row) return null;
539
+ return {
540
+ ...row,
541
+ created_at: new Date(row.created_at),
542
+ updated_at: new Date(row.updated_at),
543
+ last_analyzed: row.last_analyzed ? new Date(row.last_analyzed) : void 0
544
+ };
545
+ } catch (error) {
546
+ logger.error(`Failed to get file by URL: ${error instanceof Error ? error.message : String(error)}`);
547
+ return null;
548
+ }
549
+ }
550
+ async deleteFile(id) {
551
+ try {
552
+ const result = this.db.prepare("DELETE FROM figma_files WHERE id = ?").run(id);
553
+ return result.changes > 0;
554
+ } catch (error) {
555
+ logger.error(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`);
556
+ return false;
557
+ }
558
+ }
559
+ async listFiles(options) {
560
+ try {
561
+ const limit = options?.limit || 50;
562
+ const offset = options?.offset || 0;
563
+ const orderBy = options?.orderBy || "updated_at";
564
+ const orderDirection = options?.orderDirection || "DESC";
565
+ const query = `
566
+ SELECT * FROM figma_files
567
+ ORDER BY ${orderBy} ${orderDirection}
568
+ LIMIT ? OFFSET ?
569
+ `;
570
+ const rows = this.db.prepare(query).all(limit, offset);
571
+ return rows.map((row) => ({
572
+ ...row,
573
+ created_at: new Date(row.created_at),
574
+ updated_at: new Date(row.updated_at),
575
+ last_analyzed: row.last_analyzed ? new Date(row.last_analyzed) : void 0
576
+ }));
577
+ } catch (error) {
578
+ logger.error(`Failed to list files: ${error instanceof Error ? error.message : String(error)}`);
579
+ return [];
580
+ }
581
+ }
582
+ /**
583
+ * Screen Operations
584
+ */
585
+ async upsertScreen(screen) {
586
+ try {
587
+ const existing = this.db.prepare("SELECT id FROM figma_screens WHERE id = ?").get(screen.id);
588
+ if (existing) {
589
+ this.db.prepare(`
590
+ UPDATE figma_screens
591
+ SET file_id = ?, name = ?, width = ?, height = ?, type = ?, description = ?, children_count = ?
592
+ WHERE id = ?
593
+ `).run(
594
+ screen.file_id,
595
+ screen.name,
596
+ screen.width || null,
597
+ screen.height || null,
598
+ screen.type || null,
599
+ screen.description || null,
600
+ screen.children_count || null,
601
+ screen.id
602
+ );
603
+ return { id: screen.id, created: false, updated: true };
604
+ } else {
605
+ this.db.prepare(`
606
+ INSERT INTO figma_screens (id, file_id, name, width, height, type, description, children_count)
607
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
608
+ `).run(
609
+ screen.id,
610
+ screen.file_id,
611
+ screen.name,
612
+ screen.width || null,
613
+ screen.height || null,
614
+ screen.type || null,
615
+ screen.description || null,
616
+ screen.children_count || null
617
+ );
618
+ return { id: screen.id, created: true, updated: false };
619
+ }
620
+ } catch (error) {
621
+ logger.error(`Failed to upsert screen: ${error instanceof Error ? error.message : String(error)}`);
622
+ throw error;
623
+ }
624
+ }
625
+ async getScreen(id) {
626
+ try {
627
+ const row = this.db.prepare("SELECT * FROM figma_screens WHERE id = ?").get(id);
628
+ return row || null;
629
+ } catch (error) {
630
+ logger.error(`Failed to get screen: ${error instanceof Error ? error.message : String(error)}`);
631
+ return null;
632
+ }
633
+ }
634
+ async getScreensByFile(fileId) {
635
+ try {
636
+ const rows = this.db.prepare("SELECT * FROM figma_screens WHERE file_id = ? ORDER BY name").all(fileId);
637
+ return rows;
638
+ } catch (error) {
639
+ logger.error(`Failed to get screens by file: ${error instanceof Error ? error.message : String(error)}`);
640
+ return [];
641
+ }
642
+ }
643
+ async deleteScreen(id) {
644
+ try {
645
+ const result = this.db.prepare("DELETE FROM figma_screens WHERE id = ?").run(id);
646
+ return result.changes > 0;
647
+ } catch (error) {
648
+ logger.error(`Failed to delete screen: ${error instanceof Error ? error.message : String(error)}`);
649
+ return false;
650
+ }
651
+ }
652
+ /**
653
+ * Node Operations
654
+ */
655
+ async upsertNode(node) {
656
+ try {
657
+ const existing = this.db.prepare("SELECT id FROM figma_nodes WHERE id = ?").get(node.id);
658
+ if (existing) {
659
+ this.db.prepare(`
660
+ UPDATE figma_nodes
661
+ SET file_id = ?, screen_id = ?, parent_id = ?, name = ?, type = ?, content = ?,
662
+ position_x = ?, position_y = ?, width = ?, height = ?, styles = ?, children_ids = ?
663
+ WHERE id = ?
664
+ `).run(
665
+ node.file_id,
666
+ node.screen_id || null,
667
+ node.parent_id || null,
668
+ node.name,
669
+ node.type,
670
+ node.content || null,
671
+ node.position_x || null,
672
+ node.position_y || null,
673
+ node.width || null,
674
+ node.height || null,
675
+ node.styles || null,
676
+ node.children_ids || null,
677
+ node.id
678
+ );
679
+ return { id: node.id, created: false, updated: true };
680
+ } else {
681
+ this.db.prepare(`
682
+ INSERT INTO figma_nodes (id, file_id, screen_id, parent_id, name, type, content,
683
+ position_x, position_y, width, height, styles, children_ids)
684
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
685
+ `).run(
686
+ node.id,
687
+ node.file_id,
688
+ node.screen_id || null,
689
+ node.parent_id || null,
690
+ node.name,
691
+ node.type,
692
+ node.content || null,
693
+ node.position_x || null,
694
+ node.position_y || null,
695
+ node.width || null,
696
+ node.height || null,
697
+ node.styles || null,
698
+ node.children_ids || null
699
+ );
700
+ return { id: node.id, created: true, updated: false };
701
+ }
702
+ } catch (error) {
703
+ logger.error(`Failed to upsert node: ${error instanceof Error ? error.message : String(error)}`);
704
+ throw error;
705
+ }
706
+ }
707
+ async getNode(id) {
708
+ try {
709
+ const row = this.db.prepare("SELECT * FROM figma_nodes WHERE id = ?").get(id);
710
+ return row || null;
711
+ } catch (error) {
712
+ logger.error(`Failed to get node: ${error instanceof Error ? error.message : String(error)}`);
713
+ return null;
714
+ }
715
+ }
716
+ async getNodesByScreen(screenId) {
717
+ try {
718
+ const rows = this.db.prepare("SELECT * FROM figma_nodes WHERE screen_id = ? ORDER BY name").all(screenId);
719
+ return rows;
720
+ } catch (error) {
721
+ logger.error(`Failed to get nodes by screen: ${error instanceof Error ? error.message : String(error)}`);
722
+ return [];
723
+ }
724
+ }
725
+ async getNodesByFile(fileId) {
726
+ try {
727
+ const rows = this.db.prepare("SELECT * FROM figma_nodes WHERE file_id = ? ORDER BY name").all(fileId);
728
+ return rows;
729
+ } catch (error) {
730
+ logger.error(`Failed to get nodes by file: ${error instanceof Error ? error.message : String(error)}`);
731
+ return [];
732
+ }
733
+ }
734
+ async deleteNode(id) {
735
+ try {
736
+ const result = this.db.prepare("DELETE FROM figma_nodes WHERE id = ?").run(id);
737
+ return result.changes > 0;
738
+ } catch (error) {
739
+ logger.error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
740
+ return false;
741
+ }
742
+ }
743
+ /**
744
+ * Design Token Operations
745
+ */
746
+ async upsertDesignToken(token) {
747
+ try {
748
+ const existing = this.db.prepare(`
749
+ SELECT id FROM design_tokens
750
+ WHERE file_id = ? AND type = ? AND name = ?
751
+ `).get(token.file_id, token.type, token.name);
752
+ if (existing) {
753
+ this.db.prepare(`
754
+ UPDATE design_tokens
755
+ SET value = ?, category = ?
756
+ WHERE id = ?
757
+ `).run(token.value, token.category || null, existing.id);
758
+ return { id: existing.id.toString(), created: false, updated: true };
759
+ } else {
760
+ const result = this.db.prepare(`
761
+ INSERT INTO design_tokens (file_id, type, name, value, category)
762
+ VALUES (?, ?, ?, ?, ?)
763
+ `).run(token.file_id, token.type, token.name, token.value, token.category || null);
764
+ return { id: result.lastInsertRowid.toString(), created: true, updated: false };
765
+ }
766
+ } catch (error) {
767
+ logger.error(`Failed to upsert design token: ${error instanceof Error ? error.message : String(error)}`);
768
+ throw error;
769
+ }
770
+ }
771
+ async getDesignTokensByFile(fileId, type) {
772
+ try {
773
+ const query = type ? "SELECT * FROM design_tokens WHERE file_id = ? AND type = ? ORDER BY name" : "SELECT * FROM design_tokens WHERE file_id = ? ORDER BY type, name";
774
+ const rows = type ? this.db.prepare(query).all(fileId, type) : this.db.prepare(query).all(fileId);
775
+ return rows;
776
+ } catch (error) {
777
+ logger.error(`Failed to get design tokens: ${error instanceof Error ? error.message : String(error)}`);
778
+ return [];
779
+ }
780
+ }
781
+ async deleteDesignTokensByFile(fileId) {
782
+ try {
783
+ const result = this.db.prepare("DELETE FROM design_tokens WHERE file_id = ?").run(fileId);
784
+ return result.changes > 0;
785
+ } catch (error) {
786
+ logger.error(`Failed to delete design tokens: ${error instanceof Error ? error.message : String(error)}`);
787
+ return false;
788
+ }
789
+ }
790
+ /**
791
+ * Asset Operations
792
+ */
793
+ async upsertAsset(asset) {
794
+ try {
795
+ const existing = this.db.prepare(`
796
+ SELECT id FROM figma_assets
797
+ WHERE file_id = ? AND node_id = ?
798
+ `).get(asset.file_id, asset.node_id);
799
+ if (existing) {
800
+ this.db.prepare(`
801
+ UPDATE figma_assets
802
+ SET node_name = ?, node_type = ?, format = ?, file_path = ?, url = ?, width = ?, height = ?
803
+ WHERE id = ?
804
+ `).run(
805
+ asset.node_name || null,
806
+ asset.node_type || null,
807
+ asset.format || null,
808
+ asset.file_path || null,
809
+ asset.url || null,
810
+ asset.width || null,
811
+ asset.height || null,
812
+ existing.id
813
+ );
814
+ return { id: existing.id.toString(), created: false, updated: true };
815
+ } else {
816
+ const result = this.db.prepare(`
817
+ INSERT INTO figma_assets (file_id, node_id, node_name, node_type, format, file_path, url, width, height)
818
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
819
+ `).run(
820
+ asset.file_id,
821
+ asset.node_id,
822
+ asset.node_name || null,
823
+ asset.node_type || null,
824
+ asset.format || null,
825
+ asset.file_path || null,
826
+ asset.url || null,
827
+ asset.width || null,
828
+ asset.height || null
829
+ );
830
+ return { id: result.lastInsertRowid.toString(), created: true, updated: false };
831
+ }
832
+ } catch (error) {
833
+ logger.error(`Failed to upsert asset: ${error instanceof Error ? error.message : String(error)}`);
834
+ throw error;
835
+ }
836
+ }
837
+ async getAssetsByFile(fileId) {
838
+ try {
839
+ const rows = this.db.prepare("SELECT * FROM figma_assets WHERE file_id = ? ORDER BY node_name").all(fileId);
840
+ return rows;
841
+ } catch (error) {
842
+ logger.error(`Failed to get assets by file: ${error instanceof Error ? error.message : String(error)}`);
843
+ return [];
844
+ }
845
+ }
846
+ async deleteAssetsByFile(fileId) {
847
+ try {
848
+ const result = this.db.prepare("DELETE FROM figma_assets WHERE file_id = ?").run(fileId);
849
+ return result.changes > 0;
850
+ } catch (error) {
851
+ logger.error(`Failed to delete assets by file: ${error instanceof Error ? error.message : String(error)}`);
852
+ return false;
853
+ }
854
+ }
855
+ /**
856
+ * Cache invalidation logic
857
+ */
858
+ async isCacheValid(url, maxAgeHours = 24) {
859
+ try {
860
+ const file = await this.getFileByUrl(url);
861
+ if (!file || !file.last_analyzed) return false;
862
+ const now = /* @__PURE__ */ new Date();
863
+ const ageHours = (now.getTime() - file.last_analyzed.getTime()) / (1e3 * 60 * 60);
864
+ return ageHours < maxAgeHours;
865
+ } catch (error) {
866
+ logger.error(`Failed to check cache validity: ${error instanceof Error ? error.message : String(error)}`);
867
+ return false;
868
+ }
869
+ }
870
+ /**
871
+ * Clear cache for a specific file
872
+ */
873
+ async clearFileCache(url) {
874
+ try {
875
+ const file = await this.getFileByUrl(url);
876
+ if (!file) return false;
877
+ await this.deleteDesignTokensByFile(file.id);
878
+ await this.deleteAssetsByFile(file.id);
879
+ const nodes = await this.getNodesByFile(file.id);
880
+ for (const node of nodes) {
881
+ await this.deleteNode(node.id);
882
+ }
883
+ const screens = await this.getScreensByFile(file.id);
884
+ for (const screen of screens) {
885
+ await this.deleteScreen(screen.id);
886
+ }
887
+ return await this.deleteFile(file.id);
888
+ } catch (error) {
889
+ logger.error(`Failed to clear file cache: ${error instanceof Error ? error.message : String(error)}`);
890
+ return false;
891
+ }
892
+ }
893
+ /**
894
+ * Clear all expired cache entries
895
+ */
896
+ async clearExpiredCache(maxAgeHours = 24 * 7) {
897
+ try {
898
+ const cutoffDate = new Date(Date.now() - maxAgeHours * 60 * 60 * 1e3);
899
+ const expiredFiles = this.db.prepare(`
900
+ SELECT * FROM figma_files
901
+ WHERE last_analyzed < ? OR last_analyzed IS NULL
902
+ `).all(cutoffDate.toISOString());
903
+ let clearedCount = 0;
904
+ for (const file of expiredFiles) {
905
+ if (await this.clearFileCache(file.url)) {
906
+ clearedCount++;
907
+ }
908
+ }
909
+ return clearedCount;
910
+ } catch (error) {
911
+ logger.error(`Failed to clear expired cache: ${error instanceof Error ? error.message : String(error)}`);
912
+ return 0;
913
+ }
914
+ }
915
+ async close() {
916
+ try {
917
+ this.db.close();
918
+ logger.info("Database connection closed");
919
+ } catch (error) {
920
+ logger.error(`Failed to close database: ${error instanceof Error ? error.message : String(error)}`);
921
+ }
922
+ }
923
+ async vacuum() {
924
+ try {
925
+ this.db.exec("VACUUM");
926
+ logger.info("Database vacuumed");
927
+ } catch (error) {
928
+ logger.error(`Failed to vacuum database: ${error instanceof Error ? error.message : String(error)}`);
929
+ }
930
+ }
931
+ async getStats() {
932
+ try {
933
+ const files = this.db.prepare("SELECT COUNT(*) as count FROM figma_files").get();
934
+ const screens = this.db.prepare("SELECT COUNT(*) as count FROM figma_screens").get();
935
+ const nodes = this.db.prepare("SELECT COUNT(*) as count FROM figma_nodes").get();
936
+ const tokens = this.db.prepare("SELECT COUNT(*) as count FROM design_tokens").get();
937
+ const assets = this.db.prepare("SELECT COUNT(*) as count FROM figma_assets").get();
938
+ return {
939
+ files: files.count,
940
+ screens: screens.count,
941
+ nodes: nodes.count,
942
+ tokens: tokens.count,
943
+ assets: assets.count
944
+ };
945
+ } catch (error) {
946
+ logger.error(`Failed to get stats: ${error instanceof Error ? error.message : String(error)}`);
947
+ return { files: 0, screens: 0, nodes: 0, tokens: 0, assets: 0 };
948
+ }
949
+ }
950
+ };
951
+ }
952
+ });
953
+
176
954
  // src/core/config.ts
177
955
  var config_exports = {};
178
956
  __export(config_exports, {
@@ -230,15 +1008,41 @@ function deepMerge(base, override) {
230
1008
  }
231
1009
  return result;
232
1010
  }
233
- var ConfigSchema, Config;
1011
+ var PlatformConfigSchema, ConfigSchema, Config;
234
1012
  var init_config = __esm({
235
1013
  "src/core/config.ts"() {
236
1014
  "use strict";
237
1015
  init_esm_shims();
238
1016
  init_paths();
239
1017
  init_version();
1018
+ init_figma_db();
1019
+ PlatformConfigSchema = z.object({
1020
+ /**
1021
+ * Primary platform to use (affects default behavior)
1022
+ */
1023
+ primary: z.enum(["opencode", "claude", "cursor"]).default("opencode"),
1024
+ /**
1025
+ * Enable OpenCode platform support
1026
+ * Default: true (OpenCode is the primary focus)
1027
+ */
1028
+ opencode: z.boolean().default(true),
1029
+ /**
1030
+ * Enable Claude Code platform support
1031
+ * Default: false (archived, can be re-enabled later)
1032
+ */
1033
+ claude: z.boolean().default(false),
1034
+ /**
1035
+ * Enable Cursor platform support
1036
+ * Default: false (opt-in)
1037
+ */
1038
+ cursor: z.boolean().default(false)
1039
+ }).default({});
240
1040
  ConfigSchema = z.object({
241
1041
  version: z.string(),
1042
+ /**
1043
+ * Platform configuration - controls which platforms are active
1044
+ */
1045
+ platform: PlatformConfigSchema,
242
1046
  skills: z.object({
243
1047
  enabled: z.boolean().default(true),
244
1048
  directory: z.string().optional()
@@ -270,6 +1074,11 @@ var init_config = __esm({
270
1074
  specFile: z.string().default("spec.md"),
271
1075
  reviewFile: z.string().default("review.md")
272
1076
  }).default({}),
1077
+ database: z.object({
1078
+ enabled: z.boolean().default(true),
1079
+ path: z.string().optional()
1080
+ // If not provided, will use default path
1081
+ }).default({}),
273
1082
  mcp: z.object({
274
1083
  context7: z.boolean().default(false),
275
1084
  githubGrep: z.boolean().default(false),
@@ -309,12 +1118,46 @@ var init_config = __esm({
309
1118
  get antiHallucination() {
310
1119
  return this.config.antiHallucination;
311
1120
  }
1121
+ get database() {
1122
+ return this.config.database;
1123
+ }
312
1124
  get mode() {
313
1125
  return this.config.mode;
314
1126
  }
1127
+ get platform() {
1128
+ return this.config.platform;
1129
+ }
315
1130
  get configPath() {
316
1131
  return this.config.configPath;
317
1132
  }
1133
+ /**
1134
+ * Check if a specific platform is enabled
1135
+ */
1136
+ isPlatformEnabled(platform) {
1137
+ return this.config.platform[platform] ?? false;
1138
+ }
1139
+ /**
1140
+ * Get the primary platform
1141
+ */
1142
+ getPrimaryPlatform() {
1143
+ return this.config.platform.primary;
1144
+ }
1145
+ /**
1146
+ * Get list of all enabled platforms
1147
+ */
1148
+ getEnabledPlatforms() {
1149
+ const platforms = [];
1150
+ if (this.config.platform.opencode) {
1151
+ platforms.push("opencode");
1152
+ }
1153
+ if (this.config.platform.claude) {
1154
+ platforms.push("claude");
1155
+ }
1156
+ if (this.config.platform.cursor) {
1157
+ platforms.push("cursor");
1158
+ }
1159
+ return platforms;
1160
+ }
318
1161
  get projectPath() {
319
1162
  return this.config.projectPath;
320
1163
  }
@@ -322,49 +1165,20 @@ var init_config = __esm({
322
1165
  * Get path to a specific resource
323
1166
  */
324
1167
  getPath(resource) {
1168
+ if (resource === "database") {
1169
+ return this.config.database.path || join3(this.configPath, "figma.db");
1170
+ }
325
1171
  return paths[resource](this.configPath);
326
1172
  }
327
- };
328
- }
329
- });
330
-
331
- // src/utils/logger.ts
332
- import chalk from "chalk";
333
- var logger;
334
- var init_logger = __esm({
335
- "src/utils/logger.ts"() {
336
- "use strict";
337
- init_esm_shims();
338
- logger = {
339
- info(...args) {
340
- console.log(chalk.blue("\u2139"), ...args);
341
- },
342
- success(...args) {
343
- console.log(chalk.green("\u2713"), ...args);
344
- },
345
- warn(...args) {
346
- console.log(chalk.yellow("\u26A0"), ...args);
347
- },
348
- error(...args) {
349
- console.error(chalk.red("\u2716"), ...args);
350
- },
351
- debug(...args) {
352
- if (process.env.DEBUG || process.env.AIKIT_DEBUG) {
353
- console.log(chalk.gray("\u22EF"), ...args);
354
- }
355
- },
356
- step(step, total, message) {
357
- console.log(chalk.cyan(`[${step}/${total}]`), message);
358
- },
359
- header(message) {
360
- console.log(chalk.bold.underline(`
361
- ${message}
362
- `));
363
- },
364
- list(items, prefix = "\u2022") {
365
- for (const item of items) {
366
- console.log(` ${prefix} ${item}`);
1173
+ /**
1174
+ * Get database instance
1175
+ */
1176
+ getDatabase() {
1177
+ if (this.config.database?.enabled === false) {
1178
+ throw new Error("Database is disabled in configuration");
367
1179
  }
1180
+ const dbPath = this.getPath("database");
1181
+ return new FigmaDatabase(dbPath);
368
1182
  }
369
1183
  };
370
1184
  }
@@ -602,8 +1416,10 @@ var init_figma_mcp = __esm({
602
1416
  init_logger();
603
1417
  FigmaMcpClient = class {
604
1418
  apiKey;
605
- constructor(apiKey, _configManager) {
1419
+ database;
1420
+ constructor(apiKey, _configManager, database) {
606
1421
  this.apiKey = apiKey;
1422
+ this.database = database;
607
1423
  }
608
1424
  /**
609
1425
  * Fetch helper with simple retry/backoff for 429/5xx
@@ -686,14 +1502,22 @@ ${text}`);
686
1502
  return data;
687
1503
  }
688
1504
  /**
689
- * Extract design tokens from Figma file
1505
+ * Extract design tokens from Figma file and optionally persist to database
690
1506
  */
691
1507
  async extractDesignTokens(url, downloadAssets = true, assetsDir) {
692
- const fileData = await this.getFileData(url);
693
1508
  const fileKey = this.extractFileKey(url);
694
1509
  if (!fileKey) {
695
1510
  throw new Error(`Invalid Figma URL: ${url}`);
696
1511
  }
1512
+ if (this.database) {
1513
+ const cachedData = await this.getFromDatabase(fileKey);
1514
+ if (cachedData) {
1515
+ logger.info(`\u26A1 Using cached data from database (analyzed ${this.getTimeAgo(cachedData.last_analyzed)})`);
1516
+ return cachedData.tokens;
1517
+ }
1518
+ }
1519
+ logger.info("\u23F3 Fetching fresh data from Figma API...");
1520
+ const fileData = await this.getFileData(url);
697
1521
  const tokens = {
698
1522
  colors: [],
699
1523
  typography: [],
@@ -738,8 +1562,227 @@ ${text}`);
738
1562
  logger.warn(`Failed to download assets: ${error instanceof Error ? error.message : String(error)}`);
739
1563
  }
740
1564
  }
1565
+ if (this.database) {
1566
+ await this.persistToDatabase(url, fileKey, tokens);
1567
+ }
741
1568
  return tokens;
742
1569
  }
1570
+ /**
1571
+ * Persist extracted tokens to database
1572
+ */
1573
+ async persistToDatabase(url, fileKey, tokens) {
1574
+ if (!this.database) return;
1575
+ try {
1576
+ logger.info("Persisting Figma data to database...");
1577
+ const fileId = fileKey;
1578
+ await this.database.upsertFile({
1579
+ id: fileId,
1580
+ url,
1581
+ name: url.split("/").pop() || "Figma Design",
1582
+ file_key: fileKey,
1583
+ last_analyzed: /* @__PURE__ */ new Date()
1584
+ });
1585
+ for (const screen of tokens.screens) {
1586
+ await this.database.upsertScreen({
1587
+ id: screen.id,
1588
+ file_id: fileId,
1589
+ name: screen.name,
1590
+ width: screen.width,
1591
+ height: screen.height,
1592
+ type: screen.type,
1593
+ children_count: screen.childrenCount
1594
+ });
1595
+ }
1596
+ if (tokens.structure?.nodes) {
1597
+ for (const node of tokens.structure.nodes) {
1598
+ await this.database.upsertNode({
1599
+ id: node.id,
1600
+ file_id: fileId,
1601
+ screen_id: this.findScreenIdForNode(node.id, tokens.screens),
1602
+ parent_id: this.findParentId(node.id, tokens.structure.nodes),
1603
+ name: node.name,
1604
+ type: node.type,
1605
+ content: node.content,
1606
+ position_x: node.position?.x,
1607
+ position_y: node.position?.y,
1608
+ width: node.position?.width,
1609
+ height: node.position?.height,
1610
+ styles: node.styles ? JSON.stringify(node.styles) : void 0,
1611
+ children_ids: node.children ? JSON.stringify(node.children) : void 0
1612
+ });
1613
+ }
1614
+ }
1615
+ for (const color of tokens.colors) {
1616
+ await this.database.upsertDesignToken({
1617
+ file_id: fileId,
1618
+ type: "color",
1619
+ name: color.name,
1620
+ value: color.hex,
1621
+ category: "color"
1622
+ });
1623
+ }
1624
+ for (const typo of tokens.typography) {
1625
+ await this.database.upsertDesignToken({
1626
+ file_id: fileId,
1627
+ type: "typography",
1628
+ name: typo.name,
1629
+ value: JSON.stringify({
1630
+ fontFamily: typo.fontFamily,
1631
+ fontSize: typo.fontSize,
1632
+ fontWeight: typo.fontWeight,
1633
+ lineHeight: typo.lineHeight
1634
+ }),
1635
+ category: "typography"
1636
+ });
1637
+ }
1638
+ for (const component of tokens.components) {
1639
+ await this.database.upsertDesignToken({
1640
+ file_id: fileId,
1641
+ type: "component",
1642
+ name: component.name,
1643
+ value: component.type,
1644
+ category: component.description
1645
+ });
1646
+ }
1647
+ if (tokens.assets) {
1648
+ for (const asset of tokens.assets) {
1649
+ await this.database.upsertAsset({
1650
+ file_id: fileId,
1651
+ node_id: asset.nodeId,
1652
+ node_name: asset.nodeName,
1653
+ node_type: asset.nodeType,
1654
+ format: asset.format,
1655
+ file_path: asset.path,
1656
+ url: asset.url,
1657
+ width: asset.width,
1658
+ height: asset.height
1659
+ });
1660
+ }
1661
+ }
1662
+ logger.info("Successfully persisted Figma data to database");
1663
+ } catch (error) {
1664
+ logger.error(`Failed to persist to database: ${error instanceof Error ? error.message : String(error)}`);
1665
+ }
1666
+ }
1667
+ /**
1668
+ * Get cached data from database
1669
+ */
1670
+ async getFromDatabase(fileKey) {
1671
+ if (!this.database) return null;
1672
+ try {
1673
+ const file = await this.database.getFile(fileKey);
1674
+ if (!file || !file.last_analyzed) return null;
1675
+ const cacheAge = Date.now() - file.last_analyzed.getTime();
1676
+ const maxAge = 24 * 60 * 60 * 1e3;
1677
+ if (cacheAge > maxAge) {
1678
+ logger.info("Cache expired, fetching fresh data...");
1679
+ return null;
1680
+ }
1681
+ const screens = await this.database.getScreensByFile(fileKey);
1682
+ const nodes = await this.database.getNodesByFile(fileKey);
1683
+ const tokenRecords = await this.database.getDesignTokensByFile(fileKey);
1684
+ const assets = await this.database.getAssetsByFile(fileKey);
1685
+ const tokens = {
1686
+ colors: tokenRecords.filter((t) => t.type === "color").map((t) => ({ name: t.name, hex: t.value, rgba: t.value })),
1687
+ typography: tokenRecords.filter((t) => t.type === "typography").map((t) => {
1688
+ const parsed = JSON.parse(t.value);
1689
+ return {
1690
+ name: t.name,
1691
+ fontFamily: parsed.fontFamily,
1692
+ fontSize: parsed.fontSize,
1693
+ fontWeight: parsed.fontWeight,
1694
+ lineHeight: parsed.lineHeight,
1695
+ letterSpacing: parsed.letterSpacing
1696
+ };
1697
+ }),
1698
+ spacing: {
1699
+ unit: 8,
1700
+ scale: [4, 8, 12, 16, 24, 32, 48, 64]
1701
+ },
1702
+ components: tokenRecords.filter((t) => t.type === "component").map((t) => ({ name: t.name, type: t.value, description: t.category })),
1703
+ screens: screens.map((s) => ({
1704
+ id: s.id,
1705
+ name: s.name,
1706
+ width: s.width || 0,
1707
+ height: s.height || 0,
1708
+ type: s.type || "FRAME",
1709
+ description: s.description,
1710
+ childrenCount: s.children_count
1711
+ })),
1712
+ breakpoints: [375, 768, 1024, 1280, 1920],
1713
+ structure: {
1714
+ nodes: nodes.map((n) => ({
1715
+ id: n.id,
1716
+ name: n.name,
1717
+ type: n.type,
1718
+ content: n.content,
1719
+ position: n.position_x !== void 0 ? {
1720
+ x: n.position_x,
1721
+ y: n.position_y || 0,
1722
+ width: n.width || 0,
1723
+ height: n.height || 0
1724
+ } : void 0,
1725
+ styles: n.styles ? JSON.parse(n.styles) : void 0,
1726
+ children: n.children_ids ? JSON.parse(n.children_ids) : void 0
1727
+ })),
1728
+ hierarchy: this.buildHierarchy(nodes)
1729
+ },
1730
+ assets: assets.map((a) => ({
1731
+ nodeId: a.node_id,
1732
+ nodeName: a.node_name || "",
1733
+ nodeType: a.node_type || "",
1734
+ format: a.format || "png",
1735
+ path: a.file_path || "",
1736
+ url: a.url || "",
1737
+ width: a.width,
1738
+ height: a.height
1739
+ }))
1740
+ };
1741
+ return { tokens, last_analyzed: file.last_analyzed };
1742
+ } catch (error) {
1743
+ logger.warn(`Failed to retrieve from database: ${error instanceof Error ? error.message : String(error)}`);
1744
+ return null;
1745
+ }
1746
+ }
1747
+ /**
1748
+ * Build hierarchy string from nodes
1749
+ */
1750
+ buildHierarchy(nodes) {
1751
+ return nodes.map((n) => `${n.type} "${n.name}" [${n.id}]`).join("\n");
1752
+ }
1753
+ /**
1754
+ * Get human-readable time ago
1755
+ */
1756
+ getTimeAgo(date) {
1757
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
1758
+ if (seconds < 60) return `${seconds} seconds ago`;
1759
+ const minutes = Math.floor(seconds / 60);
1760
+ if (minutes < 60) return `${minutes} minutes ago`;
1761
+ const hours = Math.floor(minutes / 60);
1762
+ if (hours < 24) return `${hours} hours ago`;
1763
+ const days = Math.floor(hours / 24);
1764
+ return `${days} days ago`;
1765
+ }
1766
+ /**
1767
+ * Helper to find which screen a node belongs to
1768
+ */
1769
+ findScreenIdForNode(nodeId, screens) {
1770
+ if (screens.find((s) => s.id === nodeId)) {
1771
+ return nodeId;
1772
+ }
1773
+ return void 0;
1774
+ }
1775
+ /**
1776
+ * Helper to find parent node ID
1777
+ */
1778
+ findParentId(nodeId, nodes) {
1779
+ for (const node of nodes) {
1780
+ if (node.children?.includes(nodeId)) {
1781
+ return node.id;
1782
+ }
1783
+ }
1784
+ return void 0;
1785
+ }
743
1786
  /**
744
1787
  * Recursively extract colors from nodes
745
1788
  */
@@ -1010,11 +2053,66 @@ ${text}`);
1010
2053
  var figma_screen_developer_exports = {};
1011
2054
  __export(figma_screen_developer_exports, {
1012
2055
  checkCurrentCodeStatus: () => checkCurrentCodeStatus,
1013
- compareCodeWithFigma: () => compareCodeWithFigma
2056
+ compareCodeWithFigma: () => compareCodeWithFigma,
2057
+ getCachedFigmaData: () => getCachedFigmaData
1014
2058
  });
1015
2059
  import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
1016
2060
  import { join as join8 } from "path";
1017
2061
  import { existsSync as existsSync3 } from "fs";
2062
+ async function getCachedFigmaData(url, database) {
2063
+ if (!database) return null;
2064
+ try {
2065
+ const file = await database.getFileByUrl(url);
2066
+ if (!file) return null;
2067
+ const screens = await database.getScreensByFile(file.id);
2068
+ const nodes = await database.getNodesByFile(file.id);
2069
+ const tokens = await database.getDesignTokensByFile(file.id);
2070
+ const assets = await database.getAssetsByFile(file.id);
2071
+ return {
2072
+ screens: screens.map((s) => ({
2073
+ id: s.id,
2074
+ name: s.name,
2075
+ width: s.width,
2076
+ height: s.height,
2077
+ type: s.type,
2078
+ childrenCount: s.children_count
2079
+ })),
2080
+ nodes: nodes.map((n) => ({
2081
+ id: n.id,
2082
+ name: n.name,
2083
+ type: n.type,
2084
+ content: n.content,
2085
+ position: n.position_x ? {
2086
+ x: n.position_x,
2087
+ y: n.position_y,
2088
+ width: n.width,
2089
+ height: n.height
2090
+ } : void 0,
2091
+ styles: n.styles ? JSON.parse(n.styles) : void 0,
2092
+ children: n.children_ids ? JSON.parse(n.children_ids) : void 0
2093
+ })),
2094
+ tokens: tokens.map((t) => ({
2095
+ type: t.type,
2096
+ name: t.name,
2097
+ value: t.value,
2098
+ category: t.category
2099
+ })),
2100
+ assets: assets.map((a) => ({
2101
+ nodeId: a.node_id,
2102
+ nodeName: a.node_name,
2103
+ nodeType: a.node_type,
2104
+ format: a.format,
2105
+ path: a.file_path,
2106
+ url: a.url,
2107
+ width: a.width,
2108
+ height: a.height
2109
+ }))
2110
+ };
2111
+ } catch (error) {
2112
+ logger.warn(`Failed to get cached Figma data: ${error instanceof Error ? error.message : String(error)}`);
2113
+ return null;
2114
+ }
2115
+ }
1018
2116
  async function checkCurrentCodeStatus(projectPath = process.cwd()) {
1019
2117
  const status = {
1020
2118
  hasHTML: false,
@@ -1074,7 +2172,12 @@ async function checkCurrentCodeStatus(projectPath = process.cwd()) {
1074
2172
  }
1075
2173
  return status;
1076
2174
  }
1077
- async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath = process.cwd()) {
2175
+ async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath = process.cwd(), database, figmaUrl) {
2176
+ let cachedData;
2177
+ if (database && figmaUrl) {
2178
+ cachedData = await getCachedFigmaData(figmaUrl, database);
2179
+ }
2180
+ const dataToUse = cachedData || figmaTokens;
1078
2181
  const codeStatus = await checkCurrentCodeStatus(projectPath);
1079
2182
  const result = {
1080
2183
  missingSections: [],
@@ -1082,17 +2185,18 @@ async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath =
1082
2185
  needsUpdate: false,
1083
2186
  recommendations: []
1084
2187
  };
1085
- const selectedScreen = figmaTokens.screens?.find((s) => s.id === selectedScreenId);
2188
+ const selectedScreen = dataToUse.screens?.find((s) => s.id === selectedScreenId);
1086
2189
  if (!selectedScreen) {
1087
2190
  result.recommendations.push("Selected screen not found in Figma design");
1088
2191
  return result;
1089
2192
  }
1090
2193
  const figmaSections = [];
1091
- if (figmaTokens.structure?.nodes) {
1092
- const screenNode = figmaTokens.structure.nodes.find((n) => n.id === selectedScreenId);
2194
+ if (dataToUse.structure?.nodes || dataToUse.nodes) {
2195
+ const nodes = dataToUse.structure?.nodes || dataToUse.nodes;
2196
+ const screenNode = nodes.find((n) => n.id === selectedScreenId);
1093
2197
  if (screenNode?.children) {
1094
2198
  screenNode.children.forEach((childId) => {
1095
- const childNode = figmaTokens.structure.nodes.find((n) => n.id === childId);
2199
+ const childNode = nodes.find((n) => n.id === childId);
1096
2200
  if (childNode && (childNode.type === "FRAME" || childNode.type === "COMPONENT")) {
1097
2201
  const sectionName = childNode.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-");
1098
2202
  figmaSections.push(sectionName);
@@ -4794,7 +5898,15 @@ This will guide you through setting up your Figma Personal Access Token.`;
4794
5898
  }
4795
5899
  try {
4796
5900
  const { FigmaMcpClient: FigmaMcpClient2 } = await Promise.resolve().then(() => (init_figma_mcp(), figma_mcp_exports));
4797
- const client = new FigmaMcpClient2(apiKey, configManager);
5901
+ let database;
5902
+ try {
5903
+ if (context?.config) {
5904
+ database = context.config.getDatabase();
5905
+ }
5906
+ } catch (error) {
5907
+ logger.info("Database not available for Figma persistence");
5908
+ }
5909
+ const client = new FigmaMcpClient2(apiKey, configManager, database);
4798
5910
  const assetsDir = "./assets/images";
4799
5911
  const tokens = await client.extractDesignTokens(url, false, assetsDir);
4800
5912
  let result = `# Figma Design Analysis
@@ -5025,25 +6137,6 @@ Please check:
5025
6137
  }
5026
6138
  }
5027
6139
  },
5028
- {
5029
- name: "analyze_figma",
5030
- description: "Analyze a Figma design URL and extract all design tokens automatically. The URL should be provided in the user input after the command.",
5031
- args: {
5032
- url: {
5033
- type: "string",
5034
- description: "Figma design URL to analyze",
5035
- required: true
5036
- }
5037
- },
5038
- async execute({ url }) {
5039
- return `Figma analysis tool called for: ${url}
5040
-
5041
- Next steps:
5042
- 1. Use @vision agent to analyze the design
5043
- 2. Extract all design tokens
5044
- 3. Save to memory/research/figma-analysis.md`;
5045
- }
5046
- },
5047
6140
  {
5048
6141
  name: "develop_figma_screen",
5049
6142
  description: "Smart workflow to develop a specific Figma screen: check current code, pull needed assets, plan, and develop. User just needs to confirm the screen number/name.",
@@ -5075,7 +6168,15 @@ Next steps:
5075
6168
  try {
5076
6169
  const { FigmaMcpClient: FigmaMcpClient2 } = await Promise.resolve().then(() => (init_figma_mcp(), figma_mcp_exports));
5077
6170
  const { checkCurrentCodeStatus: checkCurrentCodeStatus2, compareCodeWithFigma: compareCodeWithFigma2 } = await Promise.resolve().then(() => (init_figma_screen_developer(), figma_screen_developer_exports));
5078
- const client = new FigmaMcpClient2(apiKey, configManager);
6171
+ let database;
6172
+ try {
6173
+ if (context?.config) {
6174
+ database = context.config.getDatabase();
6175
+ }
6176
+ } catch (error) {
6177
+ logger.info("Database not available for Figma persistence");
6178
+ }
6179
+ const client = new FigmaMcpClient2(apiKey, configManager, database);
5079
6180
  const tokens = await client.extractDesignTokens(figmaUrl, false, "./assets/images");
5080
6181
  const screenIdStr = String(screenId);
5081
6182
  const selectedScreen = tokens.screens?.find(
@@ -5638,8 +6739,10 @@ ${argsDesc}
5638
6739
 
5639
6740
  // src/core/tool-config.ts
5640
6741
  init_esm_shims();
6742
+ init_logger();
5641
6743
  import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir7, access as access4, constants as constants4 } from "fs/promises";
5642
6744
  import { join as join11 } from "path";
6745
+ import { homedir as homedir2 } from "os";
5643
6746
  import { z as z3 } from "zod";
5644
6747
  var ToolConfigSchema = z3.object({
5645
6748
  name: z3.string(),
@@ -5710,13 +6813,25 @@ var ToolConfigManager = class {
5710
6813
  */
5711
6814
  async isToolReady(toolName) {
5712
6815
  const toolConfig = await this.getToolConfig(toolName);
5713
- return toolConfig?.status === "ready";
6816
+ const isReady = toolConfig?.status === "ready";
6817
+ logger.info(`\u{1F50D} Tool ready check for ${toolName}:`, {
6818
+ hasConfig: !!toolConfig,
6819
+ status: toolConfig?.status,
6820
+ isReady
6821
+ });
6822
+ return isReady;
5714
6823
  }
5715
6824
  /**
5716
6825
  * Get API key for a tool (if configured)
5717
6826
  */
5718
6827
  async getApiKey(toolName) {
5719
6828
  const toolConfig = await this.getToolConfig(toolName);
6829
+ logger.info(`\u{1F50D} Getting API key for ${toolName}:`, {
6830
+ hasConfig: !!toolConfig,
6831
+ hasApiKey: !!toolConfig?.config?.apiKey,
6832
+ status: toolConfig?.status,
6833
+ apiKeyLength: typeof toolConfig?.config?.apiKey === "string" ? toolConfig.config.apiKey.length : 0
6834
+ });
5720
6835
  if (toolConfig?.config?.apiKey && typeof toolConfig.config.apiKey === "string") {
5721
6836
  return toolConfig.config.apiKey;
5722
6837
  }
@@ -5727,30 +6842,53 @@ var ToolConfigManager = class {
5727
6842
  */
5728
6843
  determineStatus(tool, saved) {
5729
6844
  if (tool.configMethod === "none") {
6845
+ logger.info(`\u{1F50D} Tool ${tool.name} uses 'none' config method - marking as ready`);
5730
6846
  return "ready";
5731
6847
  }
5732
6848
  if (saved?.errorMessage) {
6849
+ logger.warn(`\u{1F50D} Tool ${tool.name} has error: ${saved.errorMessage}`);
5733
6850
  return "error";
5734
6851
  }
5735
6852
  if (tool.configMethod === "oauth" || tool.configMethod === "manual") {
5736
- if (saved?.config?.apiKey && typeof saved.config.apiKey === "string" && saved.config.apiKey.length > 0) {
6853
+ const hasApiKey = saved?.config?.apiKey && typeof saved.config.apiKey === "string" && saved.config.apiKey.length > 0;
6854
+ logger.info(`\u{1F50D} Tool ${tool.name} status check:`, {
6855
+ configMethod: tool.configMethod,
6856
+ hasSaved: !!saved,
6857
+ hasConfig: !!saved?.config,
6858
+ hasApiKey,
6859
+ apiKeyLength: typeof saved?.config?.apiKey === "string" ? saved.config.apiKey.length : 0
6860
+ });
6861
+ if (hasApiKey) {
5737
6862
  return "ready";
5738
6863
  }
5739
6864
  return "needs_config";
5740
6865
  }
6866
+ logger.warn(`\u{1F50D} Tool ${tool.name} fell through to default 'needs_config'`);
5741
6867
  return "needs_config";
5742
6868
  }
5743
6869
  /**
5744
6870
  * Load saved configurations
6871
+ * Checks both global and project configs, project takes precedence
5745
6872
  */
5746
6873
  async loadConfigs() {
6874
+ const globalConfigPath = join11(homedir2(), ".config", "aikit", "config", "tools.json");
6875
+ let configs = {};
6876
+ try {
6877
+ await access4(globalConfigPath, constants4.R_OK);
6878
+ const content = await readFile7(globalConfigPath, "utf-8");
6879
+ configs = JSON.parse(content);
6880
+ logger.info("Loaded global tool configs");
6881
+ } catch {
6882
+ }
5747
6883
  try {
5748
6884
  await access4(this.toolsConfigPath, constants4.R_OK);
5749
6885
  const content = await readFile7(this.toolsConfigPath, "utf-8");
5750
- return JSON.parse(content);
6886
+ const projectConfigs = JSON.parse(content);
6887
+ configs = { ...configs, ...projectConfigs };
6888
+ logger.info("Loaded project tool configs");
5751
6889
  } catch {
5752
- return {};
5753
6890
  }
6891
+ return configs;
5754
6892
  }
5755
6893
  /**
5756
6894
  * Save configurations
@@ -5760,6 +6898,57 @@ var ToolConfigManager = class {
5760
6898
  await mkdir7(configDir, { recursive: true });
5761
6899
  await writeFile7(this.toolsConfigPath, JSON.stringify(configs, null, 2));
5762
6900
  }
6901
+ /**
6902
+ * Configure Claude Desktop MCP server for a tool
6903
+ * This adds the MCP server configuration to Claude Desktop's config file
6904
+ */
6905
+ async configureMcpServer(toolName, apiKey) {
6906
+ if (toolName !== "figma-analysis") {
6907
+ logger.info(`MCP server configuration not implemented for tool: ${toolName}`);
6908
+ return;
6909
+ }
6910
+ const isWindows = process.platform === "win32";
6911
+ const claudeConfigBase = isWindows ? process.env.APPDATA || join11(homedir2(), "AppData", "Roaming") : join11(homedir2(), ".config");
6912
+ const claudeConfigPath = join11(claudeConfigBase, "claude", "claude_desktop_config.json");
6913
+ try {
6914
+ let claudeConfig = {};
6915
+ try {
6916
+ const content = await readFile7(claudeConfigPath, "utf-8");
6917
+ claudeConfig = JSON.parse(content);
6918
+ } catch {
6919
+ claudeConfig = {};
6920
+ }
6921
+ if (!claudeConfig.mcpServers) {
6922
+ claudeConfig.mcpServers = {};
6923
+ }
6924
+ claudeConfig.mcpServers.figma = {
6925
+ command: "npx",
6926
+ args: ["-y", "figma-developer-mcp", `--figma-oauth-token=${apiKey}`, "--stdio"]
6927
+ };
6928
+ const claudeConfigDir = join11(claudeConfigBase, "claude");
6929
+ await mkdir7(claudeConfigDir, { recursive: true });
6930
+ await writeFile7(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
6931
+ logger.success("\u2705 Claude Desktop MCP server configured");
6932
+ logger.info(` Config file: ${claudeConfigPath}`);
6933
+ logger.info("");
6934
+ logger.info("\u26A0\uFE0F IMPORTANT: Restart Claude Desktop for changes to take effect");
6935
+ } catch (error) {
6936
+ logger.warn(`Could not configure MCP server automatically: ${error instanceof Error ? error.message : String(error)}`);
6937
+ logger.info("");
6938
+ logger.info("Please configure manually:");
6939
+ logger.info(`1. Edit: ${claudeConfigPath}`);
6940
+ logger.info('2. Add the following to "mcpServers":');
6941
+ logger.info(JSON.stringify({
6942
+ figma: {
6943
+ command: "npx",
6944
+ args: ["-y", "figma-developer-mcp"],
6945
+ env: {
6946
+ FIGMA_OAUTH_TOKEN: apiKey
6947
+ }
6948
+ }
6949
+ }, null, 2));
6950
+ }
6951
+ }
5763
6952
  };
5764
6953
 
5765
6954
  // src/mcp-server.ts
@@ -5771,6 +6960,8 @@ var AiKitMcpServer = class {
5771
6960
  commandRunner;
5772
6961
  toolRegistry;
5773
6962
  toolConfigManager;
6963
+ config;
6964
+ // Store config instance
5774
6965
  currentMode = "build";
5775
6966
  // Default mode
5776
6967
  constructor() {
@@ -5951,7 +7142,10 @@ Reasoning: ${decision.reason}`;
5951
7142
  if (!this.toolConfigManager) {
5952
7143
  result = `Error: Tool configuration manager not initialized. MCP server may not be properly started.`;
5953
7144
  } else {
5954
- const context = { toolConfigManager: this.toolConfigManager };
7145
+ const context = {
7146
+ toolConfigManager: this.toolConfigManager,
7147
+ config: this.config
7148
+ };
5955
7149
  result = await this.toolRegistry.executeTool(toolName, args || {}, context);
5956
7150
  }
5957
7151
  } catch (error) {
@@ -6019,6 +7213,7 @@ Reasoning: ${decision.reason}`;
6019
7213
  async initialize() {
6020
7214
  try {
6021
7215
  const config = await loadConfig();
7216
+ this.config = config;
6022
7217
  this.skillEngine = new SkillEngine(config);
6023
7218
  this.agentManager = new AgentManager(config);
6024
7219
  this.commandRunner = new CommandRunner(config);