figma-relai 0.5.0 → 0.5.2
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 +1 -1
- package/dist/chunk-6ABTHOFJ.js +106 -0
- package/dist/chunk-6ABTHOFJ.js.map +1 -0
- package/dist/{chunk-2MSOOX4G.js → chunk-7KACLWNP.js} +25 -8
- package/dist/chunk-7KACLWNP.js.map +1 -0
- package/dist/{doctor-P36IPZHH.js → doctor-INFHE5OE.js} +30 -11
- package/dist/doctor-INFHE5OE.js.map +1 -0
- package/dist/index.js +29 -8
- package/dist/index.js.map +1 -1
- package/dist/login-3TGCNCQB.js +95 -0
- package/dist/login-3TGCNCQB.js.map +1 -0
- package/dist/{manifest-DBJJSLW2.js → manifest-LYWTTJTX.js} +3 -2
- package/dist/{manifest-DBJJSLW2.js.map → manifest-LYWTTJTX.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-2MSOOX4G.js.map +0 -1
- package/dist/chunk-XBWGSCRZ.js +0 -33
- package/dist/chunk-XBWGSCRZ.js.map +0 -1
- package/dist/doctor-P36IPZHH.js.map +0 -1
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ file precedent attached — "…is on purpose" · the edit backs off
|
|
|
83
83
|
|
|
84
84
|
The panel's Memory row lists every entry; delete any of them any time. Nothing is recorded silently.
|
|
85
85
|
|
|
86
|
-
**Kits.** One designer usually runs one product, and its files share the same law. Save this file’s rules under a name and other files can
|
|
86
|
+
**Kits.** One designer usually runs one product, and its files share the same law. Save this file’s rules under a name and other files can use it. Figma drops file-carried law on branch merges; the kit puts it back — one click, or automatically if you flip that kit’s auto-restore on.
|
|
87
87
|
|
|
88
88
|
**AI no-go zones** fence off whole pages — brand masters, legal, the one that is already pixel-perfect. Writes into a guarded page are rejected before they run, with the reason on the receipt, and only you edit the guard list.
|
|
89
89
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/state.ts
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
function stateDir() {
|
|
8
|
+
return process.env.FIGMA_RELAI_STATE_DIR ?? join(homedir(), ".figma-relai");
|
|
9
|
+
}
|
|
10
|
+
function statePath() {
|
|
11
|
+
return join(stateDir(), "state.json");
|
|
12
|
+
}
|
|
13
|
+
function loadState() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(statePath(), "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function saveState(patch) {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
23
|
+
const next = { ...loadState(), ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
24
|
+
writeFileSync(statePath(), JSON.stringify(next, null, 2) + "\n");
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/credentials.ts
|
|
30
|
+
import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
31
|
+
import { join as join2 } from "path";
|
|
32
|
+
function credentialsPath() {
|
|
33
|
+
return join2(stateDir(), "credentials.json");
|
|
34
|
+
}
|
|
35
|
+
function readFile() {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync2(credentialsPath(), "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function loadToken() {
|
|
43
|
+
const fromEnv = process.env.FIGMA_TOKEN?.trim();
|
|
44
|
+
if (fromEnv) return fromEnv;
|
|
45
|
+
const fromFile = readFile().token?.trim();
|
|
46
|
+
return fromFile || void 0;
|
|
47
|
+
}
|
|
48
|
+
function tokenSource() {
|
|
49
|
+
if (process.env.FIGMA_TOKEN?.trim()) return "env";
|
|
50
|
+
if (readFile().token?.trim()) return "file";
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function saveToken(token) {
|
|
54
|
+
const dir = stateDir();
|
|
55
|
+
mkdirSync2(dir, { recursive: true });
|
|
56
|
+
try {
|
|
57
|
+
chmodSync(dir, 448);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
const path = credentialsPath();
|
|
61
|
+
writeFileSync2(path, JSON.stringify({ token, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n", {
|
|
62
|
+
mode: 384
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
chmodSync(path, 384);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
function clearToken() {
|
|
71
|
+
try {
|
|
72
|
+
rmSync(credentialsPath());
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function verifyToken(token) {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch("https://api.figma.com/v1/me", {
|
|
81
|
+
headers: { "X-Figma-Token": token }
|
|
82
|
+
});
|
|
83
|
+
if (res.status === 403 || res.status === 401) {
|
|
84
|
+
return { ok: false, reason: "Figma rejected the token (401/403) \u2014 check it was copied whole." };
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
return { ok: false, reason: `Figma answered ${res.status} ${res.statusText}.` };
|
|
88
|
+
}
|
|
89
|
+
const me = await res.json();
|
|
90
|
+
return { ok: true, handle: me.handle ?? "(unknown)", email: me.email };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { ok: false, reason: `Could not reach api.figma.com: ${err.message}` };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
loadState,
|
|
98
|
+
saveState,
|
|
99
|
+
credentialsPath,
|
|
100
|
+
loadToken,
|
|
101
|
+
tokenSource,
|
|
102
|
+
saveToken,
|
|
103
|
+
clearToken,
|
|
104
|
+
verifyToken
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=chunk-6ABTHOFJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/state.ts","../src/credentials.ts"],"sourcesContent":["import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n// Persisted pairing state so the AI reconnects to the same room across\n// sessions without the designer re-pasting a join command.\n\nexport interface RelaiState {\n room?: string;\n updatedAt?: string;\n}\n\nexport function stateDir(): string {\n return process.env.FIGMA_RELAI_STATE_DIR ?? join(homedir(), \".figma-relai\");\n}\n\nfunction statePath(): string {\n return join(stateDir(), \"state.json\");\n}\n\nexport function loadState(): RelaiState {\n try {\n return JSON.parse(readFileSync(statePath(), \"utf8\")) as RelaiState;\n } catch {\n return {};\n }\n}\n\nexport function saveState(patch: Partial<RelaiState>): void {\n try {\n mkdirSync(stateDir(), { recursive: true });\n const next = { ...loadState(), ...patch, updatedAt: new Date().toISOString() };\n writeFileSync(statePath(), JSON.stringify(next, null, 2) + \"\\n\");\n } catch {\n // State persistence is best-effort; pairing still works via list_rooms\n }\n}\n","import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { stateDir } from \"./state.js\";\nimport { join } from \"node:path\";\n\n// A Figma personal access token is account-wide and long-lived, so it never\n// travels the relay socket — the plugin has no business holding it. It lives\n// either in the MCP config env or in a 0600 file this process owns, and both\n// are read here so every consumer resolves it the same way.\n\nexport type TokenSource = \"env\" | \"file\" | null;\n\ninterface CredentialsFile {\n token?: string;\n savedAt?: string;\n}\n\nexport function credentialsPath(): string {\n return join(stateDir(), \"credentials.json\");\n}\n\nfunction readFile(): CredentialsFile {\n try {\n return JSON.parse(readFileSync(credentialsPath(), \"utf8\")) as CredentialsFile;\n } catch {\n return {};\n }\n}\n\n/** env wins over the stored file so a per-project override stays possible. */\nexport function loadToken(): string | undefined {\n const fromEnv = process.env.FIGMA_TOKEN?.trim();\n if (fromEnv) return fromEnv;\n const fromFile = readFile().token?.trim();\n return fromFile || undefined;\n}\n\nexport function tokenSource(): TokenSource {\n if (process.env.FIGMA_TOKEN?.trim()) return \"env\";\n if (readFile().token?.trim()) return \"file\";\n return null;\n}\n\nexport function saveToken(token: string): string {\n const dir = stateDir();\n mkdirSync(dir, { recursive: true });\n try {\n chmodSync(dir, 0o700);\n } catch {\n // Directory permissions are best-effort (Windows, exotic filesystems)\n }\n const path = credentialsPath();\n // Create with the restrictive mode rather than widening then narrowing:\n // an 0644 window, however brief, is a window.\n writeFileSync(path, JSON.stringify({ token, savedAt: new Date().toISOString() }, null, 2) + \"\\n\", {\n mode: 0o600,\n });\n try {\n chmodSync(path, 0o600);\n } catch {\n // Pre-existing file may keep its mode on some platforms; reported by doctor\n }\n return path;\n}\n\nexport function clearToken(): boolean {\n try {\n rmSync(credentialsPath());\n return true;\n } catch {\n return false;\n }\n}\n\n/** Confirms the token is live and tells the caller whose it is. */\nexport async function verifyToken(\n token: string\n): Promise<{ ok: true; handle: string; email?: string } | { ok: false; reason: string }> {\n try {\n const res = await fetch(\"https://api.figma.com/v1/me\", {\n headers: { \"X-Figma-Token\": token },\n });\n if (res.status === 403 || res.status === 401) {\n return { ok: false, reason: \"Figma rejected the token (401/403) — check it was copied whole.\" };\n }\n if (!res.ok) {\n return { ok: false, reason: `Figma answered ${res.status} ${res.statusText}.` };\n }\n const me = (await res.json()) as { handle?: string; email?: string };\n return { ok: true, handle: me.handle ?? \"(unknown)\", email: me.email };\n } catch (err) {\n return { ok: false, reason: `Could not reach api.figma.com: ${(err as Error).message}` };\n }\n}\n"],"mappings":";;;AAAA,SAAS,WAAW,cAAc,qBAAqB;AACvD,SAAS,eAAe;AACxB,SAAS,YAAY;AAUd,SAAS,WAAmB;AACjC,SAAO,QAAQ,IAAI,yBAAyB,KAAK,QAAQ,GAAG,cAAc;AAC5E;AAEA,SAAS,YAAoB;AAC3B,SAAO,KAAK,SAAS,GAAG,YAAY;AACtC;AAEO,SAAS,YAAwB;AACtC,MAAI;AACF,WAAO,KAAK,MAAM,aAAa,UAAU,GAAG,MAAM,CAAC;AAAA,EACrD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,UAAU,OAAkC;AAC1D,MAAI;AACF,cAAU,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AACzC,UAAM,OAAO,EAAE,GAAG,UAAU,GAAG,GAAG,OAAO,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE;AAC7E,kBAAc,UAAU,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,IAAI;AAAA,EACjE,QAAQ;AAAA,EAER;AACF;;;ACpCA,SAAS,WAAW,aAAAA,YAAW,gBAAAC,eAAc,QAAQ,iBAAAC,sBAAqB;AAE1E,SAAS,QAAAC,aAAY;AAcd,SAAS,kBAA0B;AACxC,SAAOA,MAAK,SAAS,GAAG,kBAAkB;AAC5C;AAEA,SAAS,WAA4B;AACnC,MAAI;AACF,WAAO,KAAK,MAAMC,cAAa,gBAAgB,GAAG,MAAM,CAAC;AAAA,EAC3D,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAGO,SAAS,YAAgC;AAC9C,QAAM,UAAU,QAAQ,IAAI,aAAa,KAAK;AAC9C,MAAI,QAAS,QAAO;AACpB,QAAM,WAAW,SAAS,EAAE,OAAO,KAAK;AACxC,SAAO,YAAY;AACrB;AAEO,SAAS,cAA2B;AACzC,MAAI,QAAQ,IAAI,aAAa,KAAK,EAAG,QAAO;AAC5C,MAAI,SAAS,EAAE,OAAO,KAAK,EAAG,QAAO;AACrC,SAAO;AACT;AAEO,SAAS,UAAU,OAAuB;AAC/C,QAAM,MAAM,SAAS;AACrB,EAAAC,WAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,MAAI;AACF,cAAU,KAAK,GAAK;AAAA,EACtB,QAAQ;AAAA,EAER;AACA,QAAM,OAAO,gBAAgB;AAG7B,EAAAC,eAAc,MAAM,KAAK,UAAU,EAAE,OAAO,UAAS,oBAAI,KAAK,GAAE,YAAY,EAAE,GAAG,MAAM,CAAC,IAAI,MAAM;AAAA,IAChG,MAAM;AAAA,EACR,CAAC;AACD,MAAI;AACF,cAAU,MAAM,GAAK;AAAA,EACvB,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEO,SAAS,aAAsB;AACpC,MAAI;AACF,WAAO,gBAAgB,CAAC;AACxB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,YACpB,OACuF;AACvF,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,+BAA+B;AAAA,MACrD,SAAS,EAAE,iBAAiB,MAAM;AAAA,IACpC,CAAC;AACD,QAAI,IAAI,WAAW,OAAO,IAAI,WAAW,KAAK;AAC5C,aAAO,EAAE,IAAI,OAAO,QAAQ,uEAAkE;AAAA,IAChG;AACA,QAAI,CAAC,IAAI,IAAI;AACX,aAAO,EAAE,IAAI,OAAO,QAAQ,kBAAkB,IAAI,MAAM,IAAI,IAAI,UAAU,IAAI;AAAA,IAChF;AACA,UAAM,KAAM,MAAM,IAAI,KAAK;AAC3B,WAAO,EAAE,IAAI,MAAM,QAAQ,GAAG,UAAU,aAAa,OAAO,GAAG,MAAM;AAAA,EACvE,SAAS,KAAK;AACZ,WAAO,EAAE,IAAI,OAAO,QAAQ,kCAAmC,IAAc,OAAO,GAAG;AAAA,EACzF;AACF;","names":["mkdirSync","readFileSync","writeFileSync","join","readFileSync","mkdirSync","writeFileSync"]}
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadUserSkills
|
|
4
4
|
} from "./chunk-SDINVROO.js";
|
|
5
|
+
import {
|
|
6
|
+
loadToken
|
|
7
|
+
} from "./chunk-6ABTHOFJ.js";
|
|
5
8
|
import {
|
|
6
9
|
__export
|
|
7
10
|
} from "./chunk-2H7UOFLK.js";
|
|
@@ -12,7 +15,7 @@ function createServer() {
|
|
|
12
15
|
return new McpServer(
|
|
13
16
|
{
|
|
14
17
|
name: "Relai",
|
|
15
|
-
version: "0.5.
|
|
18
|
+
version: "0.5.2"
|
|
16
19
|
},
|
|
17
20
|
{
|
|
18
21
|
instructions: `
|
|
@@ -515,10 +518,12 @@ var RelayCore = class {
|
|
|
515
518
|
staleTimer = null;
|
|
516
519
|
version;
|
|
517
520
|
log;
|
|
521
|
+
features;
|
|
518
522
|
constructor(options = {}) {
|
|
519
523
|
this.version = options.version ?? "unknown";
|
|
520
524
|
this.log = options.log ?? (() => {
|
|
521
525
|
});
|
|
526
|
+
this.features = options.features ?? null;
|
|
522
527
|
}
|
|
523
528
|
handleOpen(ws) {
|
|
524
529
|
this.lastActivity.set(ws, Date.now());
|
|
@@ -534,9 +539,21 @@ var RelayCore = class {
|
|
|
534
539
|
return;
|
|
535
540
|
}
|
|
536
541
|
switch (data.type) {
|
|
537
|
-
case "hello":
|
|
538
|
-
|
|
542
|
+
case "hello": {
|
|
543
|
+
const hello = {
|
|
544
|
+
type: "hello",
|
|
545
|
+
server: "figma-relai",
|
|
546
|
+
version: this.version
|
|
547
|
+
};
|
|
548
|
+
if (this.features) {
|
|
549
|
+
try {
|
|
550
|
+
hello.features = this.features();
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
this.send(ws, hello);
|
|
539
555
|
return;
|
|
556
|
+
}
|
|
540
557
|
case "join": {
|
|
541
558
|
const room = data.room;
|
|
542
559
|
if (!room || typeof room !== "string") {
|
|
@@ -1747,10 +1764,10 @@ function register15(server, sendCommand) {
|
|
|
1747
1764
|
unresolved: z16.boolean().optional().describe("list: only unresolved threads")
|
|
1748
1765
|
},
|
|
1749
1766
|
async ({ action, fileUrl, message, commentId, nodeId, x, y, since, unresolved }) => {
|
|
1750
|
-
const token =
|
|
1767
|
+
const token = loadToken();
|
|
1751
1768
|
if (!token) {
|
|
1752
1769
|
return textResult(
|
|
1753
|
-
'Comments need a Figma personal access token. Generate one at figma.com \u2192 Settings \u2192 Security \u2192 Personal access tokens (enable comment scopes), then
|
|
1770
|
+
'Comments need a Figma personal access token. Generate one at figma.com \u2192 Settings \u2192 Security \u2192 Personal access tokens (enable comment scopes), then store it with `npx figma-relai login` (it is verified and written to ~/.figma-relai/credentials.json, mode 0600). An MCP config "env": { "FIGMA_TOKEN": "figd_..." } still works and takes precedence. Everything else works without it.'
|
|
1754
1771
|
);
|
|
1755
1772
|
}
|
|
1756
1773
|
try {
|
|
@@ -1922,10 +1939,10 @@ function register16(server, sendCommand) {
|
|
|
1922
1939
|
);
|
|
1923
1940
|
}
|
|
1924
1941
|
async function fetchLibraryCatalog(libraryFileUrl) {
|
|
1925
|
-
const token =
|
|
1942
|
+
const token = loadToken();
|
|
1926
1943
|
if (!token) {
|
|
1927
1944
|
return {
|
|
1928
|
-
note: "Full library catalogs need a
|
|
1945
|
+
note: "Full library catalogs need a personal access token (file content read scope): store it once with `npx figma-relai login`, or set FIGMA_TOKEN in the MCP config env. Without it, components the file already uses are still listed above \u2014 or keep a 'DS palette' page with one instance of each key component so they show up there."
|
|
1929
1946
|
};
|
|
1930
1947
|
}
|
|
1931
1948
|
const fileKey = parseFileKey(libraryFileUrl);
|
|
@@ -3870,4 +3887,4 @@ export {
|
|
|
3870
3887
|
listToolCatalog,
|
|
3871
3888
|
registerPrompts
|
|
3872
3889
|
};
|
|
3873
|
-
//# sourceMappingURL=chunk-
|
|
3890
|
+
//# sourceMappingURL=chunk-7KACLWNP.js.map
|