@nodii/dev 0.0.0-stage → 0.3.1

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/hooks.js ADDED
@@ -0,0 +1,153 @@
1
+ import { CliError, hubRequest } from "./core.js";
2
+
3
+ const FIGMA_PREFIXES = [
4
+ "mcp__figma__",
5
+ "mcp__claude_ai_Figma__",
6
+ "mcp__figma-dev-mode__",
7
+ ];
8
+ const READ_EXACT = new Set([
9
+ "whoami",
10
+ "get_metadata",
11
+ "get_screenshot",
12
+ "get_design_context",
13
+ "get_variable_defs",
14
+ "get_libraries",
15
+ "search_design_system",
16
+ "get_code_connect_map",
17
+ "get_code_connect_suggestions",
18
+ "list_file_components_for_code_connect",
19
+ "get_figjam",
20
+ "get_figma_skill",
21
+ "create_session",
22
+ "get_context_for_code_connect",
23
+ "get_motion_context",
24
+ "download_assets",
25
+ ]);
26
+ const suffix = (name) =>
27
+ FIGMA_PREFIXES.find((p) => name.startsWith(p))
28
+ ? name.slice(FIGMA_PREFIXES.find((p) => name.startsWith(p)).length)
29
+ : null;
30
+ export const isFigmaTool = (name) => suffix(name) !== null;
31
+ export const isReadTool = (name) => {
32
+ const s = suffix(name);
33
+ return (
34
+ s !== null &&
35
+ (READ_EXACT.has(s) || s.startsWith("list_") || s.startsWith("get_"))
36
+ );
37
+ };
38
+
39
+ export function extractFileKey(input) {
40
+ if (!input || typeof input !== "object") return null;
41
+ for (const key of ["fileKey", "file_key"])
42
+ if (typeof input[key] === "string" && input[key]) return input[key];
43
+ for (const key of ["url", "figmaUrl", "figma_url"])
44
+ if (typeof input[key] === "string") {
45
+ const match = input[key].match(
46
+ /figma\.com\/(?:design|file|board)\/([^/?#]+)/,
47
+ );
48
+ if (match) return match[1];
49
+ }
50
+ for (const value of Object.values(input)) {
51
+ const found = extractFileKey(value);
52
+ if (found) return found;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export function extractNodeIds(value, out = new Set()) {
58
+ if (!value || typeof value !== "object") return [...out];
59
+ for (const [key, item] of Object.entries(value)) {
60
+ if (/^(node_?ids?|id)$/i.test(key))
61
+ for (const v of Array.isArray(item) ? item : [item])
62
+ if (
63
+ typeof v === "string" &&
64
+ /^(?:\d+[:;-]\d+|[A-Za-z0-9_-]{8,})$/.test(v)
65
+ )
66
+ out.add(v);
67
+ if (typeof item === "object") extractNodeIds(item, out);
68
+ }
69
+ return [...out];
70
+ }
71
+
72
+ export async function preHook(payload, request = hubRequest) {
73
+ const name = payload.tool_name ?? "";
74
+ if (!isFigmaTool(name) || isReadTool(name))
75
+ return { allowed: true, ignored: true };
76
+ const fileKey = extractFileKey(payload.tool_input);
77
+ if (!fileKey)
78
+ throw new CliError(
79
+ "FIGMA_FILE_KEY_MISSING",
80
+ "Blocked Figma write: no fileKey/file_key/Figma URL was found.",
81
+ 2,
82
+ );
83
+ const config = (await import("./core.js")).readConfig;
84
+ const { agentId } = await config();
85
+ let response;
86
+ try {
87
+ response = await request(
88
+ "GET",
89
+ `/api/v1/design/files/${encodeURIComponent(fileKey)}/lock/check?agentId=${encodeURIComponent(agentId)}`,
90
+ );
91
+ } catch (error) {
92
+ throw new CliError(
93
+ "FIGMA_CHECK_FAILED",
94
+ `Blocked Figma write: ${error.message}`,
95
+ 2,
96
+ );
97
+ }
98
+ if (!response.data?.allowed)
99
+ throw new CliError(
100
+ "FIGMA_LOCK_DENIED",
101
+ response.data?.reason ?? "Blocked Figma write: lock not held.",
102
+ 2,
103
+ response.data,
104
+ );
105
+ return { allowed: true, fileKey, changeSetId: response.data.changeSetId };
106
+ }
107
+
108
+ export async function postHook(payload, request = hubRequest) {
109
+ const name = payload.tool_name ?? "";
110
+ if (!isFigmaTool(name) || isReadTool(name))
111
+ return { logged: false, ignored: true };
112
+ const fileKey = extractFileKey(payload.tool_input);
113
+ if (!fileKey) return { logged: false, reason: "no file key" };
114
+ const { agentId } = await (await import("./core.js")).readConfig();
115
+ const checked = await request(
116
+ "GET",
117
+ `/api/v1/design/files/${encodeURIComponent(fileKey)}/lock/check?agentId=${encodeURIComponent(agentId)}`,
118
+ );
119
+ if (!checked.data?.allowed || !checked.data.changeSetId)
120
+ return { logged: false, reason: checked.data?.reason ?? "lock denied" };
121
+ const nodeIds = extractNodeIds({
122
+ input: payload.tool_input,
123
+ response: payload.tool_response,
124
+ });
125
+ if (!nodeIds.length) nodeIds.push("unknown");
126
+ await request(
127
+ "POST",
128
+ `/api/v1/design/change-sets/${encodeURIComponent(checked.data.changeSetId)}/writes`,
129
+ { tool: name, nodeIds },
130
+ );
131
+ return { logged: true, changeSetId: checked.data.changeSetId, nodeIds };
132
+ }
133
+
134
+ export const settingsSnippet = () => ({
135
+ hooks: {
136
+ PreToolUse: [
137
+ {
138
+ matcher: "^mcp__(figma|claude_ai_Figma|figma-dev-mode)__.*$",
139
+ hooks: [
140
+ { type: "command", command: "nodii-dev hook claude-figma-pre" },
141
+ ],
142
+ },
143
+ ],
144
+ PostToolUse: [
145
+ {
146
+ matcher: "^mcp__(figma|claude_ai_Figma|figma-dev-mode)__.*$",
147
+ hooks: [
148
+ { type: "command", command: "nodii-dev hook claude-figma-post" },
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ });