@tdsoft-tech/aikit 0.1.31 → 0.1.32

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.
@@ -173,6 +173,756 @@ var init_version = __esm({
173
173
  }
174
174
  });
175
175
 
176
+ // src/utils/logger.ts
177
+ import chalk from "chalk";
178
+ var logger;
179
+ var init_logger = __esm({
180
+ "src/utils/logger.ts"() {
181
+ "use strict";
182
+ init_esm_shims();
183
+ logger = {
184
+ info(...args) {
185
+ console.log(chalk.blue("\u2139"), ...args);
186
+ },
187
+ success(...args) {
188
+ console.log(chalk.green("\u2713"), ...args);
189
+ },
190
+ warn(...args) {
191
+ console.log(chalk.yellow("\u26A0"), ...args);
192
+ },
193
+ error(...args) {
194
+ console.error(chalk.red("\u2716"), ...args);
195
+ },
196
+ debug(...args) {
197
+ if (process.env.DEBUG || process.env.AIKIT_DEBUG) {
198
+ console.log(chalk.gray("\u22EF"), ...args);
199
+ }
200
+ },
201
+ step(step, total, message) {
202
+ console.log(chalk.cyan(`[${step}/${total}]`), message);
203
+ },
204
+ header(message) {
205
+ console.log(chalk.bold.underline(`
206
+ ${message}
207
+ `));
208
+ },
209
+ list(items, prefix = "\u2022") {
210
+ for (const item of items) {
211
+ console.log(` ${prefix} ${item}`);
212
+ }
213
+ }
214
+ };
215
+ }
216
+ });
217
+
218
+ // src/core/database/schema.ts
219
+ function initializeSchema(db) {
220
+ try {
221
+ logger.info("Initializing Figma database schema...");
222
+ db.pragma("foreign_keys = ON");
223
+ db.pragma("journal_mode = WAL");
224
+ Object.entries(SCHEMA_SQL).forEach(([tableName, sql]) => {
225
+ logger.debug(`Creating table: ${tableName}`);
226
+ db.exec(sql);
227
+ });
228
+ Object.entries(INDEX_SQL).forEach(([indexName, sql]) => {
229
+ logger.debug(`Creating index: ${indexName}`);
230
+ db.exec(sql);
231
+ });
232
+ Object.entries(TRIGGER_SQL).forEach(([triggerName, sql]) => {
233
+ logger.debug(`Creating trigger: ${triggerName}`);
234
+ db.exec(sql);
235
+ });
236
+ const currentVersion = getCurrentSchemaVersion(db);
237
+ if (currentVersion < DATABASE_VERSION) {
238
+ db.prepare("INSERT OR REPLACE INTO schema_version (version) VALUES (?)").run(DATABASE_VERSION);
239
+ logger.info(`Schema updated to version ${DATABASE_VERSION}`);
240
+ }
241
+ logger.info("Database schema initialized successfully");
242
+ } catch (error) {
243
+ logger.error(`Failed to initialize database schema: ${error instanceof Error ? error.message : String(error)}`);
244
+ throw error;
245
+ }
246
+ }
247
+ function getCurrentSchemaVersion(db) {
248
+ try {
249
+ const result = db.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
250
+ return result?.version || 0;
251
+ } catch {
252
+ return 0;
253
+ }
254
+ }
255
+ var DATABASE_VERSION, SCHEMA_SQL, INDEX_SQL, TRIGGER_SQL;
256
+ var init_schema = __esm({
257
+ "src/core/database/schema.ts"() {
258
+ "use strict";
259
+ init_esm_shims();
260
+ init_logger();
261
+ DATABASE_VERSION = 1;
262
+ SCHEMA_SQL = {
263
+ // Files table - stores Figma file metadata
264
+ figma_files: `
265
+ CREATE TABLE IF NOT EXISTS figma_files (
266
+ id TEXT PRIMARY KEY,
267
+ url TEXT NOT NULL UNIQUE,
268
+ name TEXT,
269
+ file_key TEXT NOT NULL,
270
+ last_analyzed DATETIME,
271
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
272
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
273
+ )
274
+ `,
275
+ // Screens table - stores screen/frame information
276
+ figma_screens: `
277
+ CREATE TABLE IF NOT EXISTS figma_screens (
278
+ id TEXT PRIMARY KEY,
279
+ file_id TEXT NOT NULL,
280
+ name TEXT NOT NULL,
281
+ width INTEGER,
282
+ height INTEGER,
283
+ type TEXT,
284
+ description TEXT,
285
+ children_count INTEGER,
286
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE
287
+ )
288
+ `,
289
+ // Nodes table - stores hierarchical component structure
290
+ figma_nodes: `
291
+ CREATE TABLE IF NOT EXISTS figma_nodes (
292
+ id TEXT PRIMARY KEY,
293
+ file_id TEXT NOT NULL,
294
+ screen_id TEXT,
295
+ parent_id TEXT,
296
+ name TEXT NOT NULL,
297
+ type TEXT NOT NULL,
298
+ content TEXT,
299
+ position_x REAL,
300
+ position_y REAL,
301
+ width REAL,
302
+ height REAL,
303
+ styles TEXT, -- JSON string
304
+ children_ids TEXT, -- JSON string array
305
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE,
306
+ FOREIGN KEY (screen_id) REFERENCES figma_screens(id) ON DELETE CASCADE,
307
+ FOREIGN KEY (parent_id) REFERENCES figma_nodes(id) ON DELETE CASCADE
308
+ )
309
+ `,
310
+ // Design tokens table - stores extracted design tokens
311
+ design_tokens: `
312
+ CREATE TABLE IF NOT EXISTS design_tokens (
313
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
314
+ file_id TEXT NOT NULL,
315
+ type TEXT NOT NULL, -- 'color', 'typography', 'spacing', 'component'
316
+ name TEXT NOT NULL,
317
+ value TEXT NOT NULL,
318
+ category TEXT,
319
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE
320
+ )
321
+ `,
322
+ // Assets table - stores downloadable assets
323
+ figma_assets: `
324
+ CREATE TABLE IF NOT EXISTS figma_assets (
325
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
326
+ file_id TEXT NOT NULL,
327
+ node_id TEXT NOT NULL,
328
+ node_name TEXT,
329
+ node_type TEXT,
330
+ format TEXT, -- 'png', 'svg', 'jpg'
331
+ file_path TEXT,
332
+ url TEXT,
333
+ width INTEGER,
334
+ height INTEGER,
335
+ FOREIGN KEY (file_id) REFERENCES figma_files(id) ON DELETE CASCADE
336
+ )
337
+ `,
338
+ // Schema version tracking
339
+ schema_version: `
340
+ CREATE TABLE IF NOT EXISTS schema_version (
341
+ version INTEGER PRIMARY KEY,
342
+ applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
343
+ )
344
+ `
345
+ };
346
+ INDEX_SQL = {
347
+ // Files indexes
348
+ idx_files_url: "CREATE INDEX IF NOT EXISTS idx_files_url ON figma_files(url)",
349
+ idx_files_file_key: "CREATE INDEX IF NOT EXISTS idx_files_file_key ON figma_files(file_key)",
350
+ idx_files_last_analyzed: "CREATE INDEX IF NOT EXISTS idx_files_last_analyzed ON figma_files(last_analyzed)",
351
+ // Screens indexes
352
+ idx_screens_file_id: "CREATE INDEX IF NOT EXISTS idx_screens_file_id ON figma_screens(file_id)",
353
+ idx_screens_type: "CREATE INDEX IF NOT EXISTS idx_screens_type ON figma_screens(type)",
354
+ // Nodes indexes
355
+ idx_nodes_file_id: "CREATE INDEX IF NOT EXISTS idx_nodes_file_id ON figma_nodes(file_id)",
356
+ idx_nodes_screen_id: "CREATE INDEX IF NOT EXISTS idx_nodes_screen_id ON figma_nodes(screen_id)",
357
+ idx_nodes_parent_id: "CREATE INDEX IF NOT EXISTS idx_nodes_parent_id ON figma_nodes(parent_id)",
358
+ idx_nodes_type: "CREATE INDEX IF NOT EXISTS idx_nodes_type ON figma_nodes(type)",
359
+ // Design tokens indexes
360
+ idx_tokens_file_id: "CREATE INDEX IF NOT EXISTS idx_tokens_file_id ON design_tokens(file_id)",
361
+ idx_tokens_type: "CREATE INDEX IF NOT EXISTS idx_tokens_type ON design_tokens(type)",
362
+ idx_tokens_file_type: "CREATE INDEX IF NOT EXISTS idx_tokens_file_type ON design_tokens(file_id, type)",
363
+ // Assets indexes
364
+ idx_assets_file_id: "CREATE INDEX IF NOT EXISTS idx_assets_file_id ON figma_assets(file_id)",
365
+ idx_assets_node_id: "CREATE INDEX IF NOT EXISTS idx_assets_node_id ON figma_assets(node_id)",
366
+ idx_assets_format: "CREATE INDEX IF NOT EXISTS idx_assets_format ON figma_assets(format)"
367
+ };
368
+ TRIGGER_SQL = {
369
+ // Update timestamp trigger for files table
370
+ trigger_files_updated_at: `
371
+ CREATE TRIGGER IF NOT EXISTS trigger_files_updated_at
372
+ AFTER UPDATE ON figma_files
373
+ FOR EACH ROW
374
+ BEGIN
375
+ UPDATE figma_files SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
376
+ END
377
+ `
378
+ };
379
+ }
380
+ });
381
+
382
+ // src/core/database/figma-db.ts
383
+ import Database from "better-sqlite3";
384
+ var FigmaDatabase;
385
+ var init_figma_db = __esm({
386
+ "src/core/database/figma-db.ts"() {
387
+ "use strict";
388
+ init_esm_shims();
389
+ init_logger();
390
+ init_schema();
391
+ FigmaDatabase = class {
392
+ db;
393
+ constructor(dbPath = ":memory:") {
394
+ try {
395
+ this.db = new Database(dbPath);
396
+ initializeSchema(this.db);
397
+ logger.info(`Figma database initialized at: ${dbPath}`);
398
+ } catch (error) {
399
+ logger.error(`Failed to initialize database: ${error instanceof Error ? error.message : String(error)}`);
400
+ throw error;
401
+ }
402
+ }
403
+ /**
404
+ * Export database data to JSON
405
+ */
406
+ async exportData(fileId) {
407
+ try {
408
+ const files = fileId ? [await this.getFile(fileId)].filter(Boolean) : await this.listFiles();
409
+ const fileIds = files.map((f) => f.id);
410
+ let screens = [];
411
+ let nodes = [];
412
+ let tokens = [];
413
+ let assets = [];
414
+ for (const fId of fileIds) {
415
+ screens.push(...await this.getScreensByFile(fId));
416
+ nodes.push(...await this.getNodesByFile(fId));
417
+ tokens.push(...await this.getDesignTokensByFile(fId));
418
+ assets.push(...await this.getAssetsByFile(fId));
419
+ }
420
+ return {
421
+ files,
422
+ screens,
423
+ nodes,
424
+ tokens,
425
+ assets,
426
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
427
+ };
428
+ } catch (error) {
429
+ logger.error(`Failed to export data: ${error instanceof Error ? error.message : String(error)}`);
430
+ throw error;
431
+ }
432
+ }
433
+ /**
434
+ * Import database data from JSON
435
+ */
436
+ async importData(data) {
437
+ try {
438
+ const stats = { files: 0, screens: 0, nodes: 0, tokens: 0, assets: 0 };
439
+ for (const file of data.files) {
440
+ await this.upsertFile(file);
441
+ stats.files++;
442
+ }
443
+ for (const screen of data.screens) {
444
+ await this.upsertScreen(screen);
445
+ stats.screens++;
446
+ }
447
+ for (const node of data.nodes) {
448
+ await this.upsertNode(node);
449
+ stats.nodes++;
450
+ }
451
+ for (const token of data.tokens) {
452
+ await this.upsertDesignToken(token);
453
+ stats.tokens++;
454
+ }
455
+ for (const asset of data.assets) {
456
+ await this.upsertAsset(asset);
457
+ stats.assets++;
458
+ }
459
+ logger.info(`Imported ${stats.files} files, ${stats.screens} screens, ${stats.nodes} nodes, ${stats.tokens} tokens, ${stats.assets} assets`);
460
+ return stats;
461
+ } catch (error) {
462
+ logger.error(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
463
+ throw error;
464
+ }
465
+ }
466
+ /**
467
+ * Utility Operations
468
+ */
469
+ async upsertFile(file) {
470
+ try {
471
+ const lastAnalyzed = file.last_analyzed instanceof Date ? file.last_analyzed.toISOString() : file.last_analyzed || null;
472
+ const existing = this.db.prepare("SELECT id FROM figma_files WHERE id = ?").get(file.id);
473
+ if (existing) {
474
+ this.db.prepare(`
475
+ UPDATE figma_files
476
+ SET url = ?, name = ?, file_key = ?, last_analyzed = ?, updated_at = CURRENT_TIMESTAMP
477
+ WHERE id = ?
478
+ `).run(file.url, file.name || null, file.file_key, lastAnalyzed, file.id);
479
+ return { id: file.id, created: false, updated: true };
480
+ } else {
481
+ this.db.prepare(`
482
+ INSERT INTO figma_files (id, url, name, file_key, last_analyzed)
483
+ VALUES (?, ?, ?, ?, ?)
484
+ `).run(file.id, file.url, file.name || null, file.file_key, lastAnalyzed);
485
+ return { id: file.id, created: true, updated: false };
486
+ }
487
+ } catch (error) {
488
+ logger.error(`Failed to upsert file: ${error instanceof Error ? error.message : String(error)}`);
489
+ throw error;
490
+ }
491
+ }
492
+ async getFile(id) {
493
+ try {
494
+ const row = this.db.prepare("SELECT * FROM figma_files WHERE id = ?").get(id);
495
+ if (!row) return null;
496
+ return {
497
+ ...row,
498
+ created_at: new Date(row.created_at),
499
+ updated_at: new Date(row.updated_at),
500
+ last_analyzed: row.last_analyzed ? new Date(row.last_analyzed) : void 0
501
+ };
502
+ } catch (error) {
503
+ logger.error(`Failed to get file: ${error instanceof Error ? error.message : String(error)}`);
504
+ return null;
505
+ }
506
+ }
507
+ async getFileByUrl(url) {
508
+ try {
509
+ const row = this.db.prepare("SELECT * FROM figma_files WHERE url = ?").get(url);
510
+ if (!row) return null;
511
+ return {
512
+ ...row,
513
+ created_at: new Date(row.created_at),
514
+ updated_at: new Date(row.updated_at),
515
+ last_analyzed: row.last_analyzed ? new Date(row.last_analyzed) : void 0
516
+ };
517
+ } catch (error) {
518
+ logger.error(`Failed to get file by URL: ${error instanceof Error ? error.message : String(error)}`);
519
+ return null;
520
+ }
521
+ }
522
+ async deleteFile(id) {
523
+ try {
524
+ const result = this.db.prepare("DELETE FROM figma_files WHERE id = ?").run(id);
525
+ return result.changes > 0;
526
+ } catch (error) {
527
+ logger.error(`Failed to delete file: ${error instanceof Error ? error.message : String(error)}`);
528
+ return false;
529
+ }
530
+ }
531
+ async listFiles(options) {
532
+ try {
533
+ const limit = options?.limit || 50;
534
+ const offset = options?.offset || 0;
535
+ const orderBy = options?.orderBy || "updated_at";
536
+ const orderDirection = options?.orderDirection || "DESC";
537
+ const query = `
538
+ SELECT * FROM figma_files
539
+ ORDER BY ${orderBy} ${orderDirection}
540
+ LIMIT ? OFFSET ?
541
+ `;
542
+ const rows = this.db.prepare(query).all(limit, offset);
543
+ return rows.map((row) => ({
544
+ ...row,
545
+ created_at: new Date(row.created_at),
546
+ updated_at: new Date(row.updated_at),
547
+ last_analyzed: row.last_analyzed ? new Date(row.last_analyzed) : void 0
548
+ }));
549
+ } catch (error) {
550
+ logger.error(`Failed to list files: ${error instanceof Error ? error.message : String(error)}`);
551
+ return [];
552
+ }
553
+ }
554
+ /**
555
+ * Screen Operations
556
+ */
557
+ async upsertScreen(screen) {
558
+ try {
559
+ const existing = this.db.prepare("SELECT id FROM figma_screens WHERE id = ?").get(screen.id);
560
+ if (existing) {
561
+ this.db.prepare(`
562
+ UPDATE figma_screens
563
+ SET file_id = ?, name = ?, width = ?, height = ?, type = ?, description = ?, children_count = ?
564
+ WHERE id = ?
565
+ `).run(
566
+ screen.file_id,
567
+ screen.name,
568
+ screen.width || null,
569
+ screen.height || null,
570
+ screen.type || null,
571
+ screen.description || null,
572
+ screen.children_count || null,
573
+ screen.id
574
+ );
575
+ return { id: screen.id, created: false, updated: true };
576
+ } else {
577
+ this.db.prepare(`
578
+ INSERT INTO figma_screens (id, file_id, name, width, height, type, description, children_count)
579
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
580
+ `).run(
581
+ screen.id,
582
+ screen.file_id,
583
+ screen.name,
584
+ screen.width || null,
585
+ screen.height || null,
586
+ screen.type || null,
587
+ screen.description || null,
588
+ screen.children_count || null
589
+ );
590
+ return { id: screen.id, created: true, updated: false };
591
+ }
592
+ } catch (error) {
593
+ logger.error(`Failed to upsert screen: ${error instanceof Error ? error.message : String(error)}`);
594
+ throw error;
595
+ }
596
+ }
597
+ async getScreen(id) {
598
+ try {
599
+ const row = this.db.prepare("SELECT * FROM figma_screens WHERE id = ?").get(id);
600
+ return row || null;
601
+ } catch (error) {
602
+ logger.error(`Failed to get screen: ${error instanceof Error ? error.message : String(error)}`);
603
+ return null;
604
+ }
605
+ }
606
+ async getScreensByFile(fileId) {
607
+ try {
608
+ const rows = this.db.prepare("SELECT * FROM figma_screens WHERE file_id = ? ORDER BY name").all(fileId);
609
+ return rows;
610
+ } catch (error) {
611
+ logger.error(`Failed to get screens by file: ${error instanceof Error ? error.message : String(error)}`);
612
+ return [];
613
+ }
614
+ }
615
+ async deleteScreen(id) {
616
+ try {
617
+ const result = this.db.prepare("DELETE FROM figma_screens WHERE id = ?").run(id);
618
+ return result.changes > 0;
619
+ } catch (error) {
620
+ logger.error(`Failed to delete screen: ${error instanceof Error ? error.message : String(error)}`);
621
+ return false;
622
+ }
623
+ }
624
+ /**
625
+ * Node Operations
626
+ */
627
+ async upsertNode(node) {
628
+ try {
629
+ const existing = this.db.prepare("SELECT id FROM figma_nodes WHERE id = ?").get(node.id);
630
+ if (existing) {
631
+ this.db.prepare(`
632
+ UPDATE figma_nodes
633
+ SET file_id = ?, screen_id = ?, parent_id = ?, name = ?, type = ?, content = ?,
634
+ position_x = ?, position_y = ?, width = ?, height = ?, styles = ?, children_ids = ?
635
+ WHERE id = ?
636
+ `).run(
637
+ node.file_id,
638
+ node.screen_id || null,
639
+ node.parent_id || null,
640
+ node.name,
641
+ node.type,
642
+ node.content || null,
643
+ node.position_x || null,
644
+ node.position_y || null,
645
+ node.width || null,
646
+ node.height || null,
647
+ node.styles || null,
648
+ node.children_ids || null,
649
+ node.id
650
+ );
651
+ return { id: node.id, created: false, updated: true };
652
+ } else {
653
+ this.db.prepare(`
654
+ INSERT INTO figma_nodes (id, file_id, screen_id, parent_id, name, type, content,
655
+ position_x, position_y, width, height, styles, children_ids)
656
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
657
+ `).run(
658
+ node.id,
659
+ node.file_id,
660
+ node.screen_id || null,
661
+ node.parent_id || null,
662
+ node.name,
663
+ node.type,
664
+ node.content || null,
665
+ node.position_x || null,
666
+ node.position_y || null,
667
+ node.width || null,
668
+ node.height || null,
669
+ node.styles || null,
670
+ node.children_ids || null
671
+ );
672
+ return { id: node.id, created: true, updated: false };
673
+ }
674
+ } catch (error) {
675
+ logger.error(`Failed to upsert node: ${error instanceof Error ? error.message : String(error)}`);
676
+ throw error;
677
+ }
678
+ }
679
+ async getNode(id) {
680
+ try {
681
+ const row = this.db.prepare("SELECT * FROM figma_nodes WHERE id = ?").get(id);
682
+ return row || null;
683
+ } catch (error) {
684
+ logger.error(`Failed to get node: ${error instanceof Error ? error.message : String(error)}`);
685
+ return null;
686
+ }
687
+ }
688
+ async getNodesByScreen(screenId) {
689
+ try {
690
+ const rows = this.db.prepare("SELECT * FROM figma_nodes WHERE screen_id = ? ORDER BY name").all(screenId);
691
+ return rows;
692
+ } catch (error) {
693
+ logger.error(`Failed to get nodes by screen: ${error instanceof Error ? error.message : String(error)}`);
694
+ return [];
695
+ }
696
+ }
697
+ async getNodesByFile(fileId) {
698
+ try {
699
+ const rows = this.db.prepare("SELECT * FROM figma_nodes WHERE file_id = ? ORDER BY name").all(fileId);
700
+ return rows;
701
+ } catch (error) {
702
+ logger.error(`Failed to get nodes by file: ${error instanceof Error ? error.message : String(error)}`);
703
+ return [];
704
+ }
705
+ }
706
+ async deleteNode(id) {
707
+ try {
708
+ const result = this.db.prepare("DELETE FROM figma_nodes WHERE id = ?").run(id);
709
+ return result.changes > 0;
710
+ } catch (error) {
711
+ logger.error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
712
+ return false;
713
+ }
714
+ }
715
+ /**
716
+ * Design Token Operations
717
+ */
718
+ async upsertDesignToken(token) {
719
+ try {
720
+ const existing = this.db.prepare(`
721
+ SELECT id FROM design_tokens
722
+ WHERE file_id = ? AND type = ? AND name = ?
723
+ `).get(token.file_id, token.type, token.name);
724
+ if (existing) {
725
+ this.db.prepare(`
726
+ UPDATE design_tokens
727
+ SET value = ?, category = ?
728
+ WHERE id = ?
729
+ `).run(token.value, token.category || null, existing.id);
730
+ return { id: existing.id.toString(), created: false, updated: true };
731
+ } else {
732
+ const result = this.db.prepare(`
733
+ INSERT INTO design_tokens (file_id, type, name, value, category)
734
+ VALUES (?, ?, ?, ?, ?)
735
+ `).run(token.file_id, token.type, token.name, token.value, token.category || null);
736
+ return { id: result.lastInsertRowid.toString(), created: true, updated: false };
737
+ }
738
+ } catch (error) {
739
+ logger.error(`Failed to upsert design token: ${error instanceof Error ? error.message : String(error)}`);
740
+ throw error;
741
+ }
742
+ }
743
+ async getDesignTokensByFile(fileId, type) {
744
+ try {
745
+ 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";
746
+ const rows = type ? this.db.prepare(query).all(fileId, type) : this.db.prepare(query).all(fileId);
747
+ return rows;
748
+ } catch (error) {
749
+ logger.error(`Failed to get design tokens: ${error instanceof Error ? error.message : String(error)}`);
750
+ return [];
751
+ }
752
+ }
753
+ async deleteDesignTokensByFile(fileId) {
754
+ try {
755
+ const result = this.db.prepare("DELETE FROM design_tokens WHERE file_id = ?").run(fileId);
756
+ return result.changes > 0;
757
+ } catch (error) {
758
+ logger.error(`Failed to delete design tokens: ${error instanceof Error ? error.message : String(error)}`);
759
+ return false;
760
+ }
761
+ }
762
+ /**
763
+ * Asset Operations
764
+ */
765
+ async upsertAsset(asset) {
766
+ try {
767
+ const existing = this.db.prepare(`
768
+ SELECT id FROM figma_assets
769
+ WHERE file_id = ? AND node_id = ?
770
+ `).get(asset.file_id, asset.node_id);
771
+ if (existing) {
772
+ this.db.prepare(`
773
+ UPDATE figma_assets
774
+ SET node_name = ?, node_type = ?, format = ?, file_path = ?, url = ?, width = ?, height = ?
775
+ WHERE id = ?
776
+ `).run(
777
+ asset.node_name || null,
778
+ asset.node_type || null,
779
+ asset.format || null,
780
+ asset.file_path || null,
781
+ asset.url || null,
782
+ asset.width || null,
783
+ asset.height || null,
784
+ existing.id
785
+ );
786
+ return { id: existing.id.toString(), created: false, updated: true };
787
+ } else {
788
+ const result = this.db.prepare(`
789
+ INSERT INTO figma_assets (file_id, node_id, node_name, node_type, format, file_path, url, width, height)
790
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
791
+ `).run(
792
+ asset.file_id,
793
+ asset.node_id,
794
+ asset.node_name || null,
795
+ asset.node_type || null,
796
+ asset.format || null,
797
+ asset.file_path || null,
798
+ asset.url || null,
799
+ asset.width || null,
800
+ asset.height || null
801
+ );
802
+ return { id: result.lastInsertRowid.toString(), created: true, updated: false };
803
+ }
804
+ } catch (error) {
805
+ logger.error(`Failed to upsert asset: ${error instanceof Error ? error.message : String(error)}`);
806
+ throw error;
807
+ }
808
+ }
809
+ async getAssetsByFile(fileId) {
810
+ try {
811
+ const rows = this.db.prepare("SELECT * FROM figma_assets WHERE file_id = ? ORDER BY node_name").all(fileId);
812
+ return rows;
813
+ } catch (error) {
814
+ logger.error(`Failed to get assets by file: ${error instanceof Error ? error.message : String(error)}`);
815
+ return [];
816
+ }
817
+ }
818
+ async deleteAssetsByFile(fileId) {
819
+ try {
820
+ const result = this.db.prepare("DELETE FROM figma_assets WHERE file_id = ?").run(fileId);
821
+ return result.changes > 0;
822
+ } catch (error) {
823
+ logger.error(`Failed to delete assets by file: ${error instanceof Error ? error.message : String(error)}`);
824
+ return false;
825
+ }
826
+ }
827
+ /**
828
+ * Cache invalidation logic
829
+ */
830
+ async isCacheValid(url, maxAgeHours = 24) {
831
+ try {
832
+ const file = await this.getFileByUrl(url);
833
+ if (!file || !file.last_analyzed) return false;
834
+ const now = /* @__PURE__ */ new Date();
835
+ const ageHours = (now.getTime() - file.last_analyzed.getTime()) / (1e3 * 60 * 60);
836
+ return ageHours < maxAgeHours;
837
+ } catch (error) {
838
+ logger.error(`Failed to check cache validity: ${error instanceof Error ? error.message : String(error)}`);
839
+ return false;
840
+ }
841
+ }
842
+ /**
843
+ * Clear cache for a specific file
844
+ */
845
+ async clearFileCache(url) {
846
+ try {
847
+ const file = await this.getFileByUrl(url);
848
+ if (!file) return false;
849
+ await this.deleteDesignTokensByFile(file.id);
850
+ await this.deleteAssetsByFile(file.id);
851
+ const nodes = await this.getNodesByFile(file.id);
852
+ for (const node of nodes) {
853
+ await this.deleteNode(node.id);
854
+ }
855
+ const screens = await this.getScreensByFile(file.id);
856
+ for (const screen of screens) {
857
+ await this.deleteScreen(screen.id);
858
+ }
859
+ return await this.deleteFile(file.id);
860
+ } catch (error) {
861
+ logger.error(`Failed to clear file cache: ${error instanceof Error ? error.message : String(error)}`);
862
+ return false;
863
+ }
864
+ }
865
+ /**
866
+ * Clear all expired cache entries
867
+ */
868
+ async clearExpiredCache(maxAgeHours = 24 * 7) {
869
+ try {
870
+ const cutoffDate = new Date(Date.now() - maxAgeHours * 60 * 60 * 1e3);
871
+ const expiredFiles = this.db.prepare(`
872
+ SELECT * FROM figma_files
873
+ WHERE last_analyzed < ? OR last_analyzed IS NULL
874
+ `).all(cutoffDate.toISOString());
875
+ let clearedCount = 0;
876
+ for (const file of expiredFiles) {
877
+ if (await this.clearFileCache(file.url)) {
878
+ clearedCount++;
879
+ }
880
+ }
881
+ return clearedCount;
882
+ } catch (error) {
883
+ logger.error(`Failed to clear expired cache: ${error instanceof Error ? error.message : String(error)}`);
884
+ return 0;
885
+ }
886
+ }
887
+ async close() {
888
+ try {
889
+ this.db.close();
890
+ logger.info("Database connection closed");
891
+ } catch (error) {
892
+ logger.error(`Failed to close database: ${error instanceof Error ? error.message : String(error)}`);
893
+ }
894
+ }
895
+ async vacuum() {
896
+ try {
897
+ this.db.exec("VACUUM");
898
+ logger.info("Database vacuumed");
899
+ } catch (error) {
900
+ logger.error(`Failed to vacuum database: ${error instanceof Error ? error.message : String(error)}`);
901
+ }
902
+ }
903
+ async getStats() {
904
+ try {
905
+ const files = this.db.prepare("SELECT COUNT(*) as count FROM figma_files").get();
906
+ const screens = this.db.prepare("SELECT COUNT(*) as count FROM figma_screens").get();
907
+ const nodes = this.db.prepare("SELECT COUNT(*) as count FROM figma_nodes").get();
908
+ const tokens = this.db.prepare("SELECT COUNT(*) as count FROM design_tokens").get();
909
+ const assets = this.db.prepare("SELECT COUNT(*) as count FROM figma_assets").get();
910
+ return {
911
+ files: files.count,
912
+ screens: screens.count,
913
+ nodes: nodes.count,
914
+ tokens: tokens.count,
915
+ assets: assets.count
916
+ };
917
+ } catch (error) {
918
+ logger.error(`Failed to get stats: ${error instanceof Error ? error.message : String(error)}`);
919
+ return { files: 0, screens: 0, nodes: 0, tokens: 0, assets: 0 };
920
+ }
921
+ }
922
+ };
923
+ }
924
+ });
925
+
176
926
  // src/core/config.ts
177
927
  var config_exports = {};
178
928
  __export(config_exports, {
@@ -230,15 +980,36 @@ function deepMerge(base, override) {
230
980
  }
231
981
  return result;
232
982
  }
233
- var ConfigSchema, Config;
983
+ var PlatformConfigSchema, ConfigSchema, Config;
234
984
  var init_config = __esm({
235
985
  "src/core/config.ts"() {
236
986
  "use strict";
237
987
  init_esm_shims();
238
988
  init_paths();
239
989
  init_version();
990
+ init_figma_db();
991
+ PlatformConfigSchema = z.object({
992
+ /**
993
+ * Primary platform to use (affects default behavior)
994
+ */
995
+ primary: z.enum(["opencode", "claude"]).default("opencode"),
996
+ /**
997
+ * Enable OpenCode platform support
998
+ * Default: true (OpenCode is the primary focus)
999
+ */
1000
+ opencode: z.boolean().default(true),
1001
+ /**
1002
+ * Enable Claude Code platform support
1003
+ * Default: false (archived, can be re-enabled later)
1004
+ */
1005
+ claude: z.boolean().default(false)
1006
+ }).default({});
240
1007
  ConfigSchema = z.object({
241
1008
  version: z.string(),
1009
+ /**
1010
+ * Platform configuration - controls which platforms are active
1011
+ */
1012
+ platform: PlatformConfigSchema,
242
1013
  skills: z.object({
243
1014
  enabled: z.boolean().default(true),
244
1015
  directory: z.string().optional()
@@ -270,6 +1041,11 @@ var init_config = __esm({
270
1041
  specFile: z.string().default("spec.md"),
271
1042
  reviewFile: z.string().default("review.md")
272
1043
  }).default({}),
1044
+ database: z.object({
1045
+ enabled: z.boolean().default(true),
1046
+ path: z.string().optional()
1047
+ // If not provided, will use default path
1048
+ }).default({}),
273
1049
  mcp: z.object({
274
1050
  context7: z.boolean().default(false),
275
1051
  githubGrep: z.boolean().default(false),
@@ -309,12 +1085,43 @@ var init_config = __esm({
309
1085
  get antiHallucination() {
310
1086
  return this.config.antiHallucination;
311
1087
  }
1088
+ get database() {
1089
+ return this.config.database;
1090
+ }
312
1091
  get mode() {
313
1092
  return this.config.mode;
314
1093
  }
1094
+ get platform() {
1095
+ return this.config.platform;
1096
+ }
315
1097
  get configPath() {
316
1098
  return this.config.configPath;
317
1099
  }
1100
+ /**
1101
+ * Check if a specific platform is enabled
1102
+ */
1103
+ isPlatformEnabled(platform) {
1104
+ return this.config.platform[platform] ?? false;
1105
+ }
1106
+ /**
1107
+ * Get the primary platform
1108
+ */
1109
+ getPrimaryPlatform() {
1110
+ return this.config.platform.primary;
1111
+ }
1112
+ /**
1113
+ * Get list of all enabled platforms
1114
+ */
1115
+ getEnabledPlatforms() {
1116
+ const platforms = [];
1117
+ if (this.config.platform.opencode) {
1118
+ platforms.push("opencode");
1119
+ }
1120
+ if (this.config.platform.claude) {
1121
+ platforms.push("claude");
1122
+ }
1123
+ return platforms;
1124
+ }
318
1125
  get projectPath() {
319
1126
  return this.config.projectPath;
320
1127
  }
@@ -322,49 +1129,20 @@ var init_config = __esm({
322
1129
  * Get path to a specific resource
323
1130
  */
324
1131
  getPath(resource) {
1132
+ if (resource === "database") {
1133
+ return this.config.database.path || join3(this.configPath, "figma.db");
1134
+ }
325
1135
  return paths[resource](this.configPath);
326
1136
  }
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}`);
1137
+ /**
1138
+ * Get database instance
1139
+ */
1140
+ getDatabase() {
1141
+ if (this.config.database?.enabled === false) {
1142
+ throw new Error("Database is disabled in configuration");
367
1143
  }
1144
+ const dbPath = this.getPath("database");
1145
+ return new FigmaDatabase(dbPath);
368
1146
  }
369
1147
  };
370
1148
  }
@@ -602,8 +1380,10 @@ var init_figma_mcp = __esm({
602
1380
  init_logger();
603
1381
  FigmaMcpClient = class {
604
1382
  apiKey;
605
- constructor(apiKey, _configManager) {
1383
+ database;
1384
+ constructor(apiKey, _configManager, database) {
606
1385
  this.apiKey = apiKey;
1386
+ this.database = database;
607
1387
  }
608
1388
  /**
609
1389
  * Fetch helper with simple retry/backoff for 429/5xx
@@ -686,14 +1466,22 @@ ${text}`);
686
1466
  return data;
687
1467
  }
688
1468
  /**
689
- * Extract design tokens from Figma file
1469
+ * Extract design tokens from Figma file and optionally persist to database
690
1470
  */
691
1471
  async extractDesignTokens(url, downloadAssets = true, assetsDir) {
692
- const fileData = await this.getFileData(url);
693
1472
  const fileKey = this.extractFileKey(url);
694
1473
  if (!fileKey) {
695
1474
  throw new Error(`Invalid Figma URL: ${url}`);
696
1475
  }
1476
+ if (this.database) {
1477
+ const cachedData = await this.getFromDatabase(fileKey);
1478
+ if (cachedData) {
1479
+ logger.info(`\u26A1 Using cached data from database (analyzed ${this.getTimeAgo(cachedData.last_analyzed)})`);
1480
+ return cachedData.tokens;
1481
+ }
1482
+ }
1483
+ logger.info("\u23F3 Fetching fresh data from Figma API...");
1484
+ const fileData = await this.getFileData(url);
697
1485
  const tokens = {
698
1486
  colors: [],
699
1487
  typography: [],
@@ -738,8 +1526,227 @@ ${text}`);
738
1526
  logger.warn(`Failed to download assets: ${error instanceof Error ? error.message : String(error)}`);
739
1527
  }
740
1528
  }
1529
+ if (this.database) {
1530
+ await this.persistToDatabase(url, fileKey, tokens);
1531
+ }
741
1532
  return tokens;
742
1533
  }
1534
+ /**
1535
+ * Persist extracted tokens to database
1536
+ */
1537
+ async persistToDatabase(url, fileKey, tokens) {
1538
+ if (!this.database) return;
1539
+ try {
1540
+ logger.info("Persisting Figma data to database...");
1541
+ const fileId = fileKey;
1542
+ await this.database.upsertFile({
1543
+ id: fileId,
1544
+ url,
1545
+ name: url.split("/").pop() || "Figma Design",
1546
+ file_key: fileKey,
1547
+ last_analyzed: /* @__PURE__ */ new Date()
1548
+ });
1549
+ for (const screen of tokens.screens) {
1550
+ await this.database.upsertScreen({
1551
+ id: screen.id,
1552
+ file_id: fileId,
1553
+ name: screen.name,
1554
+ width: screen.width,
1555
+ height: screen.height,
1556
+ type: screen.type,
1557
+ children_count: screen.childrenCount
1558
+ });
1559
+ }
1560
+ if (tokens.structure?.nodes) {
1561
+ for (const node of tokens.structure.nodes) {
1562
+ await this.database.upsertNode({
1563
+ id: node.id,
1564
+ file_id: fileId,
1565
+ screen_id: this.findScreenIdForNode(node.id, tokens.screens),
1566
+ parent_id: this.findParentId(node.id, tokens.structure.nodes),
1567
+ name: node.name,
1568
+ type: node.type,
1569
+ content: node.content,
1570
+ position_x: node.position?.x,
1571
+ position_y: node.position?.y,
1572
+ width: node.position?.width,
1573
+ height: node.position?.height,
1574
+ styles: node.styles ? JSON.stringify(node.styles) : void 0,
1575
+ children_ids: node.children ? JSON.stringify(node.children) : void 0
1576
+ });
1577
+ }
1578
+ }
1579
+ for (const color of tokens.colors) {
1580
+ await this.database.upsertDesignToken({
1581
+ file_id: fileId,
1582
+ type: "color",
1583
+ name: color.name,
1584
+ value: color.hex,
1585
+ category: "color"
1586
+ });
1587
+ }
1588
+ for (const typo of tokens.typography) {
1589
+ await this.database.upsertDesignToken({
1590
+ file_id: fileId,
1591
+ type: "typography",
1592
+ name: typo.name,
1593
+ value: JSON.stringify({
1594
+ fontFamily: typo.fontFamily,
1595
+ fontSize: typo.fontSize,
1596
+ fontWeight: typo.fontWeight,
1597
+ lineHeight: typo.lineHeight
1598
+ }),
1599
+ category: "typography"
1600
+ });
1601
+ }
1602
+ for (const component of tokens.components) {
1603
+ await this.database.upsertDesignToken({
1604
+ file_id: fileId,
1605
+ type: "component",
1606
+ name: component.name,
1607
+ value: component.type,
1608
+ category: component.description
1609
+ });
1610
+ }
1611
+ if (tokens.assets) {
1612
+ for (const asset of tokens.assets) {
1613
+ await this.database.upsertAsset({
1614
+ file_id: fileId,
1615
+ node_id: asset.nodeId,
1616
+ node_name: asset.nodeName,
1617
+ node_type: asset.nodeType,
1618
+ format: asset.format,
1619
+ file_path: asset.path,
1620
+ url: asset.url,
1621
+ width: asset.width,
1622
+ height: asset.height
1623
+ });
1624
+ }
1625
+ }
1626
+ logger.info("Successfully persisted Figma data to database");
1627
+ } catch (error) {
1628
+ logger.error(`Failed to persist to database: ${error instanceof Error ? error.message : String(error)}`);
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Get cached data from database
1633
+ */
1634
+ async getFromDatabase(fileKey) {
1635
+ if (!this.database) return null;
1636
+ try {
1637
+ const file = await this.database.getFile(fileKey);
1638
+ if (!file || !file.last_analyzed) return null;
1639
+ const cacheAge = Date.now() - file.last_analyzed.getTime();
1640
+ const maxAge = 24 * 60 * 60 * 1e3;
1641
+ if (cacheAge > maxAge) {
1642
+ logger.info("Cache expired, fetching fresh data...");
1643
+ return null;
1644
+ }
1645
+ const screens = await this.database.getScreensByFile(fileKey);
1646
+ const nodes = await this.database.getNodesByFile(fileKey);
1647
+ const tokenRecords = await this.database.getDesignTokensByFile(fileKey);
1648
+ const assets = await this.database.getAssetsByFile(fileKey);
1649
+ const tokens = {
1650
+ colors: tokenRecords.filter((t) => t.type === "color").map((t) => ({ name: t.name, hex: t.value, rgba: t.value })),
1651
+ typography: tokenRecords.filter((t) => t.type === "typography").map((t) => {
1652
+ const parsed = JSON.parse(t.value);
1653
+ return {
1654
+ name: t.name,
1655
+ fontFamily: parsed.fontFamily,
1656
+ fontSize: parsed.fontSize,
1657
+ fontWeight: parsed.fontWeight,
1658
+ lineHeight: parsed.lineHeight,
1659
+ letterSpacing: parsed.letterSpacing
1660
+ };
1661
+ }),
1662
+ spacing: {
1663
+ unit: 8,
1664
+ scale: [4, 8, 12, 16, 24, 32, 48, 64]
1665
+ },
1666
+ components: tokenRecords.filter((t) => t.type === "component").map((t) => ({ name: t.name, type: t.value, description: t.category })),
1667
+ screens: screens.map((s) => ({
1668
+ id: s.id,
1669
+ name: s.name,
1670
+ width: s.width || 0,
1671
+ height: s.height || 0,
1672
+ type: s.type || "FRAME",
1673
+ description: s.description,
1674
+ childrenCount: s.children_count
1675
+ })),
1676
+ breakpoints: [375, 768, 1024, 1280, 1920],
1677
+ structure: {
1678
+ nodes: nodes.map((n) => ({
1679
+ id: n.id,
1680
+ name: n.name,
1681
+ type: n.type,
1682
+ content: n.content,
1683
+ position: n.position_x !== void 0 ? {
1684
+ x: n.position_x,
1685
+ y: n.position_y || 0,
1686
+ width: n.width || 0,
1687
+ height: n.height || 0
1688
+ } : void 0,
1689
+ styles: n.styles ? JSON.parse(n.styles) : void 0,
1690
+ children: n.children_ids ? JSON.parse(n.children_ids) : void 0
1691
+ })),
1692
+ hierarchy: this.buildHierarchy(nodes)
1693
+ },
1694
+ assets: assets.map((a) => ({
1695
+ nodeId: a.node_id,
1696
+ nodeName: a.node_name || "",
1697
+ nodeType: a.node_type || "",
1698
+ format: a.format || "png",
1699
+ path: a.file_path || "",
1700
+ url: a.url || "",
1701
+ width: a.width,
1702
+ height: a.height
1703
+ }))
1704
+ };
1705
+ return { tokens, last_analyzed: file.last_analyzed };
1706
+ } catch (error) {
1707
+ logger.warn(`Failed to retrieve from database: ${error instanceof Error ? error.message : String(error)}`);
1708
+ return null;
1709
+ }
1710
+ }
1711
+ /**
1712
+ * Build hierarchy string from nodes
1713
+ */
1714
+ buildHierarchy(nodes) {
1715
+ return nodes.map((n) => `${n.type} "${n.name}" [${n.id}]`).join("\n");
1716
+ }
1717
+ /**
1718
+ * Get human-readable time ago
1719
+ */
1720
+ getTimeAgo(date) {
1721
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
1722
+ if (seconds < 60) return `${seconds} seconds ago`;
1723
+ const minutes = Math.floor(seconds / 60);
1724
+ if (minutes < 60) return `${minutes} minutes ago`;
1725
+ const hours = Math.floor(minutes / 60);
1726
+ if (hours < 24) return `${hours} hours ago`;
1727
+ const days = Math.floor(hours / 24);
1728
+ return `${days} days ago`;
1729
+ }
1730
+ /**
1731
+ * Helper to find which screen a node belongs to
1732
+ */
1733
+ findScreenIdForNode(nodeId, screens) {
1734
+ if (screens.find((s) => s.id === nodeId)) {
1735
+ return nodeId;
1736
+ }
1737
+ return void 0;
1738
+ }
1739
+ /**
1740
+ * Helper to find parent node ID
1741
+ */
1742
+ findParentId(nodeId, nodes) {
1743
+ for (const node of nodes) {
1744
+ if (node.children?.includes(nodeId)) {
1745
+ return node.id;
1746
+ }
1747
+ }
1748
+ return void 0;
1749
+ }
743
1750
  /**
744
1751
  * Recursively extract colors from nodes
745
1752
  */
@@ -1010,11 +2017,66 @@ ${text}`);
1010
2017
  var figma_screen_developer_exports = {};
1011
2018
  __export(figma_screen_developer_exports, {
1012
2019
  checkCurrentCodeStatus: () => checkCurrentCodeStatus,
1013
- compareCodeWithFigma: () => compareCodeWithFigma
2020
+ compareCodeWithFigma: () => compareCodeWithFigma,
2021
+ getCachedFigmaData: () => getCachedFigmaData
1014
2022
  });
1015
2023
  import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
1016
2024
  import { join as join8 } from "path";
1017
2025
  import { existsSync as existsSync3 } from "fs";
2026
+ async function getCachedFigmaData(url, database) {
2027
+ if (!database) return null;
2028
+ try {
2029
+ const file = await database.getFileByUrl(url);
2030
+ if (!file) return null;
2031
+ const screens = await database.getScreensByFile(file.id);
2032
+ const nodes = await database.getNodesByFile(file.id);
2033
+ const tokens = await database.getDesignTokensByFile(file.id);
2034
+ const assets = await database.getAssetsByFile(file.id);
2035
+ return {
2036
+ screens: screens.map((s) => ({
2037
+ id: s.id,
2038
+ name: s.name,
2039
+ width: s.width,
2040
+ height: s.height,
2041
+ type: s.type,
2042
+ childrenCount: s.children_count
2043
+ })),
2044
+ nodes: nodes.map((n) => ({
2045
+ id: n.id,
2046
+ name: n.name,
2047
+ type: n.type,
2048
+ content: n.content,
2049
+ position: n.position_x ? {
2050
+ x: n.position_x,
2051
+ y: n.position_y,
2052
+ width: n.width,
2053
+ height: n.height
2054
+ } : void 0,
2055
+ styles: n.styles ? JSON.parse(n.styles) : void 0,
2056
+ children: n.children_ids ? JSON.parse(n.children_ids) : void 0
2057
+ })),
2058
+ tokens: tokens.map((t) => ({
2059
+ type: t.type,
2060
+ name: t.name,
2061
+ value: t.value,
2062
+ category: t.category
2063
+ })),
2064
+ assets: assets.map((a) => ({
2065
+ nodeId: a.node_id,
2066
+ nodeName: a.node_name,
2067
+ nodeType: a.node_type,
2068
+ format: a.format,
2069
+ path: a.file_path,
2070
+ url: a.url,
2071
+ width: a.width,
2072
+ height: a.height
2073
+ }))
2074
+ };
2075
+ } catch (error) {
2076
+ logger.warn(`Failed to get cached Figma data: ${error instanceof Error ? error.message : String(error)}`);
2077
+ return null;
2078
+ }
2079
+ }
1018
2080
  async function checkCurrentCodeStatus(projectPath = process.cwd()) {
1019
2081
  const status = {
1020
2082
  hasHTML: false,
@@ -1074,7 +2136,12 @@ async function checkCurrentCodeStatus(projectPath = process.cwd()) {
1074
2136
  }
1075
2137
  return status;
1076
2138
  }
1077
- async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath = process.cwd()) {
2139
+ async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath = process.cwd(), database, figmaUrl) {
2140
+ let cachedData;
2141
+ if (database && figmaUrl) {
2142
+ cachedData = await getCachedFigmaData(figmaUrl, database);
2143
+ }
2144
+ const dataToUse = cachedData || figmaTokens;
1078
2145
  const codeStatus = await checkCurrentCodeStatus(projectPath);
1079
2146
  const result = {
1080
2147
  missingSections: [],
@@ -1082,17 +2149,18 @@ async function compareCodeWithFigma(figmaTokens, selectedScreenId, projectPath =
1082
2149
  needsUpdate: false,
1083
2150
  recommendations: []
1084
2151
  };
1085
- const selectedScreen = figmaTokens.screens?.find((s) => s.id === selectedScreenId);
2152
+ const selectedScreen = dataToUse.screens?.find((s) => s.id === selectedScreenId);
1086
2153
  if (!selectedScreen) {
1087
2154
  result.recommendations.push("Selected screen not found in Figma design");
1088
2155
  return result;
1089
2156
  }
1090
2157
  const figmaSections = [];
1091
- if (figmaTokens.structure?.nodes) {
1092
- const screenNode = figmaTokens.structure.nodes.find((n) => n.id === selectedScreenId);
2158
+ if (dataToUse.structure?.nodes || dataToUse.nodes) {
2159
+ const nodes = dataToUse.structure?.nodes || dataToUse.nodes;
2160
+ const screenNode = nodes.find((n) => n.id === selectedScreenId);
1093
2161
  if (screenNode?.children) {
1094
2162
  screenNode.children.forEach((childId) => {
1095
- const childNode = figmaTokens.structure.nodes.find((n) => n.id === childId);
2163
+ const childNode = nodes.find((n) => n.id === childId);
1096
2164
  if (childNode && (childNode.type === "FRAME" || childNode.type === "COMPONENT")) {
1097
2165
  const sectionName = childNode.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-");
1098
2166
  figmaSections.push(sectionName);
@@ -4794,7 +5862,15 @@ This will guide you through setting up your Figma Personal Access Token.`;
4794
5862
  }
4795
5863
  try {
4796
5864
  const { FigmaMcpClient: FigmaMcpClient2 } = await Promise.resolve().then(() => (init_figma_mcp(), figma_mcp_exports));
4797
- const client = new FigmaMcpClient2(apiKey, configManager);
5865
+ let database;
5866
+ try {
5867
+ if (context?.config) {
5868
+ database = context.config.getDatabase();
5869
+ }
5870
+ } catch (error) {
5871
+ logger.info("Database not available for Figma persistence");
5872
+ }
5873
+ const client = new FigmaMcpClient2(apiKey, configManager, database);
4798
5874
  const assetsDir = "./assets/images";
4799
5875
  const tokens = await client.extractDesignTokens(url, false, assetsDir);
4800
5876
  let result = `# Figma Design Analysis
@@ -5025,25 +6101,6 @@ Please check:
5025
6101
  }
5026
6102
  }
5027
6103
  },
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
6104
  {
5048
6105
  name: "develop_figma_screen",
5049
6106
  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 +6132,15 @@ Next steps:
5075
6132
  try {
5076
6133
  const { FigmaMcpClient: FigmaMcpClient2 } = await Promise.resolve().then(() => (init_figma_mcp(), figma_mcp_exports));
5077
6134
  const { checkCurrentCodeStatus: checkCurrentCodeStatus2, compareCodeWithFigma: compareCodeWithFigma2 } = await Promise.resolve().then(() => (init_figma_screen_developer(), figma_screen_developer_exports));
5078
- const client = new FigmaMcpClient2(apiKey, configManager);
6135
+ let database;
6136
+ try {
6137
+ if (context?.config) {
6138
+ database = context.config.getDatabase();
6139
+ }
6140
+ } catch (error) {
6141
+ logger.info("Database not available for Figma persistence");
6142
+ }
6143
+ const client = new FigmaMcpClient2(apiKey, configManager, database);
5079
6144
  const tokens = await client.extractDesignTokens(figmaUrl, false, "./assets/images");
5080
6145
  const screenIdStr = String(screenId);
5081
6146
  const selectedScreen = tokens.screens?.find(
@@ -5638,8 +6703,10 @@ ${argsDesc}
5638
6703
 
5639
6704
  // src/core/tool-config.ts
5640
6705
  init_esm_shims();
6706
+ init_logger();
5641
6707
  import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir7, access as access4, constants as constants4 } from "fs/promises";
5642
6708
  import { join as join11 } from "path";
6709
+ import { homedir as homedir2 } from "os";
5643
6710
  import { z as z3 } from "zod";
5644
6711
  var ToolConfigSchema = z3.object({
5645
6712
  name: z3.string(),
@@ -5710,13 +6777,25 @@ var ToolConfigManager = class {
5710
6777
  */
5711
6778
  async isToolReady(toolName) {
5712
6779
  const toolConfig = await this.getToolConfig(toolName);
5713
- return toolConfig?.status === "ready";
6780
+ const isReady = toolConfig?.status === "ready";
6781
+ logger.info(`\u{1F50D} Tool ready check for ${toolName}:`, {
6782
+ hasConfig: !!toolConfig,
6783
+ status: toolConfig?.status,
6784
+ isReady
6785
+ });
6786
+ return isReady;
5714
6787
  }
5715
6788
  /**
5716
6789
  * Get API key for a tool (if configured)
5717
6790
  */
5718
6791
  async getApiKey(toolName) {
5719
6792
  const toolConfig = await this.getToolConfig(toolName);
6793
+ logger.info(`\u{1F50D} Getting API key for ${toolName}:`, {
6794
+ hasConfig: !!toolConfig,
6795
+ hasApiKey: !!toolConfig?.config?.apiKey,
6796
+ status: toolConfig?.status,
6797
+ apiKeyLength: typeof toolConfig?.config?.apiKey === "string" ? toolConfig.config.apiKey.length : 0
6798
+ });
5720
6799
  if (toolConfig?.config?.apiKey && typeof toolConfig.config.apiKey === "string") {
5721
6800
  return toolConfig.config.apiKey;
5722
6801
  }
@@ -5727,30 +6806,53 @@ var ToolConfigManager = class {
5727
6806
  */
5728
6807
  determineStatus(tool, saved) {
5729
6808
  if (tool.configMethod === "none") {
6809
+ logger.info(`\u{1F50D} Tool ${tool.name} uses 'none' config method - marking as ready`);
5730
6810
  return "ready";
5731
6811
  }
5732
6812
  if (saved?.errorMessage) {
6813
+ logger.warn(`\u{1F50D} Tool ${tool.name} has error: ${saved.errorMessage}`);
5733
6814
  return "error";
5734
6815
  }
5735
6816
  if (tool.configMethod === "oauth" || tool.configMethod === "manual") {
5736
- if (saved?.config?.apiKey && typeof saved.config.apiKey === "string" && saved.config.apiKey.length > 0) {
6817
+ const hasApiKey = saved?.config?.apiKey && typeof saved.config.apiKey === "string" && saved.config.apiKey.length > 0;
6818
+ logger.info(`\u{1F50D} Tool ${tool.name} status check:`, {
6819
+ configMethod: tool.configMethod,
6820
+ hasSaved: !!saved,
6821
+ hasConfig: !!saved?.config,
6822
+ hasApiKey,
6823
+ apiKeyLength: typeof saved?.config?.apiKey === "string" ? saved.config.apiKey.length : 0
6824
+ });
6825
+ if (hasApiKey) {
5737
6826
  return "ready";
5738
6827
  }
5739
6828
  return "needs_config";
5740
6829
  }
6830
+ logger.warn(`\u{1F50D} Tool ${tool.name} fell through to default 'needs_config'`);
5741
6831
  return "needs_config";
5742
6832
  }
5743
6833
  /**
5744
6834
  * Load saved configurations
6835
+ * Checks both global and project configs, project takes precedence
5745
6836
  */
5746
6837
  async loadConfigs() {
6838
+ const globalConfigPath = join11(homedir2(), ".config", "aikit", "config", "tools.json");
6839
+ let configs = {};
6840
+ try {
6841
+ await access4(globalConfigPath, constants4.R_OK);
6842
+ const content = await readFile7(globalConfigPath, "utf-8");
6843
+ configs = JSON.parse(content);
6844
+ logger.info("Loaded global tool configs");
6845
+ } catch {
6846
+ }
5747
6847
  try {
5748
6848
  await access4(this.toolsConfigPath, constants4.R_OK);
5749
6849
  const content = await readFile7(this.toolsConfigPath, "utf-8");
5750
- return JSON.parse(content);
6850
+ const projectConfigs = JSON.parse(content);
6851
+ configs = { ...configs, ...projectConfigs };
6852
+ logger.info("Loaded project tool configs");
5751
6853
  } catch {
5752
- return {};
5753
6854
  }
6855
+ return configs;
5754
6856
  }
5755
6857
  /**
5756
6858
  * Save configurations
@@ -5760,6 +6862,57 @@ var ToolConfigManager = class {
5760
6862
  await mkdir7(configDir, { recursive: true });
5761
6863
  await writeFile7(this.toolsConfigPath, JSON.stringify(configs, null, 2));
5762
6864
  }
6865
+ /**
6866
+ * Configure Claude Desktop MCP server for a tool
6867
+ * This adds the MCP server configuration to Claude Desktop's config file
6868
+ */
6869
+ async configureMcpServer(toolName, apiKey) {
6870
+ if (toolName !== "figma-analysis") {
6871
+ logger.info(`MCP server configuration not implemented for tool: ${toolName}`);
6872
+ return;
6873
+ }
6874
+ const isWindows = process.platform === "win32";
6875
+ const claudeConfigBase = isWindows ? process.env.APPDATA || join11(homedir2(), "AppData", "Roaming") : join11(homedir2(), ".config");
6876
+ const claudeConfigPath = join11(claudeConfigBase, "claude", "claude_desktop_config.json");
6877
+ try {
6878
+ let claudeConfig = {};
6879
+ try {
6880
+ const content = await readFile7(claudeConfigPath, "utf-8");
6881
+ claudeConfig = JSON.parse(content);
6882
+ } catch {
6883
+ claudeConfig = {};
6884
+ }
6885
+ if (!claudeConfig.mcpServers) {
6886
+ claudeConfig.mcpServers = {};
6887
+ }
6888
+ claudeConfig.mcpServers.figma = {
6889
+ command: "npx",
6890
+ args: ["-y", "figma-developer-mcp", `--figma-oauth-token=${apiKey}`, "--stdio"]
6891
+ };
6892
+ const claudeConfigDir = join11(claudeConfigBase, "claude");
6893
+ await mkdir7(claudeConfigDir, { recursive: true });
6894
+ await writeFile7(claudeConfigPath, JSON.stringify(claudeConfig, null, 2));
6895
+ logger.success("\u2705 Claude Desktop MCP server configured");
6896
+ logger.info(` Config file: ${claudeConfigPath}`);
6897
+ logger.info("");
6898
+ logger.info("\u26A0\uFE0F IMPORTANT: Restart Claude Desktop for changes to take effect");
6899
+ } catch (error) {
6900
+ logger.warn(`Could not configure MCP server automatically: ${error instanceof Error ? error.message : String(error)}`);
6901
+ logger.info("");
6902
+ logger.info("Please configure manually:");
6903
+ logger.info(`1. Edit: ${claudeConfigPath}`);
6904
+ logger.info('2. Add the following to "mcpServers":');
6905
+ logger.info(JSON.stringify({
6906
+ figma: {
6907
+ command: "npx",
6908
+ args: ["-y", "figma-developer-mcp"],
6909
+ env: {
6910
+ FIGMA_OAUTH_TOKEN: apiKey
6911
+ }
6912
+ }
6913
+ }, null, 2));
6914
+ }
6915
+ }
5763
6916
  };
5764
6917
 
5765
6918
  // src/mcp-server.ts
@@ -5771,6 +6924,8 @@ var AiKitMcpServer = class {
5771
6924
  commandRunner;
5772
6925
  toolRegistry;
5773
6926
  toolConfigManager;
6927
+ config;
6928
+ // Store config instance
5774
6929
  currentMode = "build";
5775
6930
  // Default mode
5776
6931
  constructor() {
@@ -5951,7 +7106,10 @@ Reasoning: ${decision.reason}`;
5951
7106
  if (!this.toolConfigManager) {
5952
7107
  result = `Error: Tool configuration manager not initialized. MCP server may not be properly started.`;
5953
7108
  } else {
5954
- const context = { toolConfigManager: this.toolConfigManager };
7109
+ const context = {
7110
+ toolConfigManager: this.toolConfigManager,
7111
+ config: this.config
7112
+ };
5955
7113
  result = await this.toolRegistry.executeTool(toolName, args || {}, context);
5956
7114
  }
5957
7115
  } catch (error) {
@@ -6019,6 +7177,7 @@ Reasoning: ${decision.reason}`;
6019
7177
  async initialize() {
6020
7178
  try {
6021
7179
  const config = await loadConfig();
7180
+ this.config = config;
6022
7181
  this.skillEngine = new SkillEngine(config);
6023
7182
  this.agentManager = new AgentManager(config);
6024
7183
  this.commandRunner = new CommandRunner(config);