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