@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.
- package/dist/cli.js +2024 -707
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +309 -4
- package/dist/index.js +1384 -104
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +1235 -76
- package/dist/mcp-server.js.map +1 -1
- package/package.json +3 -1
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
1092
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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);
|