canicode 0.5.2 → 0.6.0
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 +7 -4
- package/dist/cli/index.js +36 -243
- package/dist/cli/index.js.map +1 -1
- package/dist/mcp/server.js +91 -116
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1063,6 +1063,86 @@ var FigmaUrlParseError = class extends Error {
|
|
|
1063
1063
|
function buildFigmaDeepLink(fileKey, nodeId) {
|
|
1064
1064
|
return `https://www.figma.com/design/${fileKey}?node-id=${encodeURIComponent(nodeId)}`;
|
|
1065
1065
|
}
|
|
1066
|
+
var AIREADY_DIR = join(homedir(), ".canicode");
|
|
1067
|
+
var CONFIG_PATH = join(AIREADY_DIR, "config.json");
|
|
1068
|
+
var REPORTS_DIR = join(AIREADY_DIR, "reports");
|
|
1069
|
+
function ensureDir(dir) {
|
|
1070
|
+
if (!existsSync(dir)) {
|
|
1071
|
+
mkdirSync(dir, { recursive: true });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function readConfig() {
|
|
1075
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
1076
|
+
return {};
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
1080
|
+
return JSON.parse(raw);
|
|
1081
|
+
} catch {
|
|
1082
|
+
return {};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function getFigmaToken() {
|
|
1086
|
+
return process.env["FIGMA_TOKEN"] ?? readConfig().figmaToken;
|
|
1087
|
+
}
|
|
1088
|
+
function getReportsDir() {
|
|
1089
|
+
return REPORTS_DIR;
|
|
1090
|
+
}
|
|
1091
|
+
function ensureReportsDir() {
|
|
1092
|
+
ensureDir(REPORTS_DIR);
|
|
1093
|
+
}
|
|
1094
|
+
function getTelemetryEnabled() {
|
|
1095
|
+
return readConfig().telemetry !== false;
|
|
1096
|
+
}
|
|
1097
|
+
function getPosthogApiKey() {
|
|
1098
|
+
return process.env["POSTHOG_API_KEY"] ?? readConfig().posthogApiKey;
|
|
1099
|
+
}
|
|
1100
|
+
function getSentryDsn() {
|
|
1101
|
+
return process.env["SENTRY_DSN"] ?? readConfig().sentryDsn;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/core/loader.ts
|
|
1105
|
+
function isFigmaUrl(input) {
|
|
1106
|
+
return input.includes("figma.com/");
|
|
1107
|
+
}
|
|
1108
|
+
function isJsonFile(input) {
|
|
1109
|
+
return input.endsWith(".json");
|
|
1110
|
+
}
|
|
1111
|
+
async function loadFile(input, token) {
|
|
1112
|
+
if (isJsonFile(input)) {
|
|
1113
|
+
const filePath = resolve(input);
|
|
1114
|
+
if (!existsSync(filePath)) {
|
|
1115
|
+
throw new Error(`File not found: ${filePath}`);
|
|
1116
|
+
}
|
|
1117
|
+
console.log(`Loading from JSON: ${filePath}`);
|
|
1118
|
+
return { file: await loadFigmaFileFromJson(filePath) };
|
|
1119
|
+
}
|
|
1120
|
+
if (isFigmaUrl(input)) {
|
|
1121
|
+
const { fileKey, nodeId } = parseFigmaUrl(input);
|
|
1122
|
+
return loadFromApi(fileKey, nodeId, token);
|
|
1123
|
+
}
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`Invalid input: ${input}. Provide a Figma URL or JSON file path.`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
async function loadFromApi(fileKey, nodeId, token) {
|
|
1129
|
+
console.log(`Fetching from Figma REST API: ${fileKey}`);
|
|
1130
|
+
if (nodeId) {
|
|
1131
|
+
console.log(`Target node: ${nodeId}`);
|
|
1132
|
+
}
|
|
1133
|
+
const figmaToken = token ?? getFigmaToken();
|
|
1134
|
+
if (!figmaToken) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
"Figma token required. Run 'canicode init --token YOUR_TOKEN' or set FIGMA_TOKEN env var."
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const client = new FigmaClient({ token: figmaToken });
|
|
1140
|
+
const response = await client.getFile(fileKey);
|
|
1141
|
+
return {
|
|
1142
|
+
file: transformFigmaResponse(fileKey, response),
|
|
1143
|
+
nodeId
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1066
1146
|
|
|
1067
1147
|
// src/adapters/figma-mcp-adapter.ts
|
|
1068
1148
|
var TAG_TYPE_MAP = {
|
|
@@ -1194,118 +1274,6 @@ function parseMcpMetadataXml(xml, fileKey, fileName) {
|
|
|
1194
1274
|
styles: {}
|
|
1195
1275
|
};
|
|
1196
1276
|
}
|
|
1197
|
-
var AIREADY_DIR = join(homedir(), ".canicode");
|
|
1198
|
-
var CONFIG_PATH = join(AIREADY_DIR, "config.json");
|
|
1199
|
-
var REPORTS_DIR = join(AIREADY_DIR, "reports");
|
|
1200
|
-
function ensureDir(dir) {
|
|
1201
|
-
if (!existsSync(dir)) {
|
|
1202
|
-
mkdirSync(dir, { recursive: true });
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
function readConfig() {
|
|
1206
|
-
if (!existsSync(CONFIG_PATH)) {
|
|
1207
|
-
return {};
|
|
1208
|
-
}
|
|
1209
|
-
try {
|
|
1210
|
-
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
1211
|
-
return JSON.parse(raw);
|
|
1212
|
-
} catch {
|
|
1213
|
-
return {};
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
function getFigmaToken() {
|
|
1217
|
-
return process.env["FIGMA_TOKEN"] ?? readConfig().figmaToken;
|
|
1218
|
-
}
|
|
1219
|
-
function getReportsDir() {
|
|
1220
|
-
return REPORTS_DIR;
|
|
1221
|
-
}
|
|
1222
|
-
function ensureReportsDir() {
|
|
1223
|
-
ensureDir(REPORTS_DIR);
|
|
1224
|
-
}
|
|
1225
|
-
function getTelemetryEnabled() {
|
|
1226
|
-
return readConfig().telemetry !== false;
|
|
1227
|
-
}
|
|
1228
|
-
function getPosthogApiKey() {
|
|
1229
|
-
return process.env["POSTHOG_API_KEY"] ?? readConfig().posthogApiKey;
|
|
1230
|
-
}
|
|
1231
|
-
function getSentryDsn() {
|
|
1232
|
-
return process.env["SENTRY_DSN"] ?? readConfig().sentryDsn;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// src/core/loader.ts
|
|
1236
|
-
function isFigmaUrl(input) {
|
|
1237
|
-
return input.includes("figma.com/");
|
|
1238
|
-
}
|
|
1239
|
-
function isJsonFile(input) {
|
|
1240
|
-
return input.endsWith(".json");
|
|
1241
|
-
}
|
|
1242
|
-
async function loadFile(input, token, mode = "auto") {
|
|
1243
|
-
if (isJsonFile(input)) {
|
|
1244
|
-
const filePath = resolve(input);
|
|
1245
|
-
if (!existsSync(filePath)) {
|
|
1246
|
-
throw new Error(`File not found: ${filePath}`);
|
|
1247
|
-
}
|
|
1248
|
-
console.log(`Loading from JSON: ${filePath}`);
|
|
1249
|
-
return { file: await loadFigmaFileFromJson(filePath) };
|
|
1250
|
-
}
|
|
1251
|
-
if (isFigmaUrl(input)) {
|
|
1252
|
-
const { fileKey, nodeId, fileName } = parseFigmaUrl(input);
|
|
1253
|
-
if (mode === "mcp") {
|
|
1254
|
-
return loadFromMcp(fileKey, nodeId, fileName);
|
|
1255
|
-
}
|
|
1256
|
-
if (mode === "api") {
|
|
1257
|
-
return loadFromApi(fileKey, nodeId, token);
|
|
1258
|
-
}
|
|
1259
|
-
try {
|
|
1260
|
-
console.log("Auto-detecting data source... trying MCP first.");
|
|
1261
|
-
return await loadFromMcp(fileKey, nodeId, fileName);
|
|
1262
|
-
} catch (mcpError) {
|
|
1263
|
-
const mcpMsg = mcpError instanceof Error ? mcpError.message : String(mcpError);
|
|
1264
|
-
console.log(`MCP unavailable (${mcpMsg}). Falling back to REST API.`);
|
|
1265
|
-
return loadFromApi(fileKey, nodeId, token);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
throw new Error(
|
|
1269
|
-
`Invalid input: ${input}. Provide a Figma URL or JSON file path.`
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
async function loadFromMcp(fileKey, nodeId, fileName) {
|
|
1273
|
-
console.log(`Loading via MCP: ${fileKey} (node: ${nodeId ?? "root"})`);
|
|
1274
|
-
const file = await loadViaMcp(fileKey, nodeId ?? "0:1", fileName);
|
|
1275
|
-
return { file, nodeId };
|
|
1276
|
-
}
|
|
1277
|
-
async function loadFromApi(fileKey, nodeId, token) {
|
|
1278
|
-
console.log(`Fetching from Figma REST API: ${fileKey}`);
|
|
1279
|
-
if (nodeId) {
|
|
1280
|
-
console.log(`Target node: ${nodeId}`);
|
|
1281
|
-
}
|
|
1282
|
-
const figmaToken = token ?? getFigmaToken();
|
|
1283
|
-
if (!figmaToken) {
|
|
1284
|
-
throw new Error(
|
|
1285
|
-
"Figma token required. Run 'canicode init --token YOUR_TOKEN' or set FIGMA_TOKEN env var."
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1288
|
-
const client = new FigmaClient({ token: figmaToken });
|
|
1289
|
-
const response = await client.getFile(fileKey);
|
|
1290
|
-
return {
|
|
1291
|
-
file: transformFigmaResponse(fileKey, response),
|
|
1292
|
-
nodeId
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
|
-
async function loadViaMcp(fileKey, nodeId, fileName) {
|
|
1296
|
-
const { execSync } = await import('child_process');
|
|
1297
|
-
const result = execSync(
|
|
1298
|
-
`claude --print "Use the mcp__figma__get_metadata tool with fileKey=\\"${fileKey}\\" and nodeId=\\"${nodeId.replace(/-/g, ":")}\\" \u2014 return ONLY the raw XML output, nothing else."`,
|
|
1299
|
-
{ encoding: "utf-8", timeout: 12e4 }
|
|
1300
|
-
);
|
|
1301
|
-
const xmlStart = result.indexOf("<");
|
|
1302
|
-
const xmlEnd = result.lastIndexOf(">");
|
|
1303
|
-
if (xmlStart === -1 || xmlEnd === -1) {
|
|
1304
|
-
throw new Error("MCP did not return valid XML metadata");
|
|
1305
|
-
}
|
|
1306
|
-
const xml = result.slice(xmlStart, xmlEnd + 1);
|
|
1307
|
-
return parseMcpMetadataXml(xml, fileKey, fileName);
|
|
1308
|
-
}
|
|
1309
1277
|
|
|
1310
1278
|
// src/core/design-data-parser.ts
|
|
1311
1279
|
function parseDesignData(data, fileKey, fileName) {
|
|
@@ -3254,9 +3222,16 @@ Two ways to provide design data:
|
|
|
3254
3222
|
1. designData \u2014 Pass Figma node data directly (from Figma MCP get_metadata). Recommended when using Figma MCP.
|
|
3255
3223
|
2. input \u2014 Figma URL (fetches via REST API, requires FIGMA_TOKEN).
|
|
3256
3224
|
|
|
3257
|
-
Typical flow with Figma MCP:
|
|
3258
|
-
Step 1: Call Figma MCP get_metadata to get the node tree
|
|
3259
|
-
Step 2: Pass the result as designData to this tool
|
|
3225
|
+
Typical flow with Figma MCP (recommended, no token needed):
|
|
3226
|
+
Step 1: Call the official Figma MCP's get_metadata tool to get the node tree
|
|
3227
|
+
Step 2: Pass the result as designData to this tool
|
|
3228
|
+
|
|
3229
|
+
IMPORTANT \u2014 Before calling this tool, check which data source is available:
|
|
3230
|
+
- If the official Figma MCP (https://mcp.figma.com/mcp) is connected: use get_metadata \u2192 designData flow. No token needed.
|
|
3231
|
+
- If Figma MCP is NOT connected: use the input parameter with a Figma URL. This requires a FIGMA_TOKEN.
|
|
3232
|
+
Tell the user: "The official Figma MCP server is not connected. To use without a token, set it up:
|
|
3233
|
+
claude mcp add -s project -t http figma https://mcp.figma.com/mcp
|
|
3234
|
+
Otherwise, provide a Figma API token via FIGMA_TOKEN env var or the token parameter."`,
|
|
3260
3235
|
{
|
|
3261
3236
|
designData: z.string().optional().describe("Figma node data from Figma MCP get_metadata (XML or JSON). Pass this instead of input when using Figma MCP."),
|
|
3262
3237
|
input: z.string().optional().describe("Figma URL. Used when designData is not provided. Requires FIGMA_TOKEN."),
|
|
@@ -3282,7 +3257,7 @@ Typical flow with Figma MCP:
|
|
|
3282
3257
|
if (designData) {
|
|
3283
3258
|
file = parseDesignData(designData, fileKey ?? "unknown", fileName);
|
|
3284
3259
|
} else if (input) {
|
|
3285
|
-
const loaded = await loadFile(input, token
|
|
3260
|
+
const loaded = await loadFile(input, token);
|
|
3286
3261
|
file = loaded.file;
|
|
3287
3262
|
nodeId = loaded.nodeId;
|
|
3288
3263
|
} else {
|