@autobest-ui/agent 1.0.0 → 1.0.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.
Files changed (30) hide show
  1. package/README.md +69 -10
  2. package/mcp/azurepr-mcp-bridge/azure-devops.js +82 -112
  3. package/mcp/azurepr-mcp-bridge/index.js +21 -25
  4. package/mcp/azurepr-mcp-bridge/index.test.js +50 -59
  5. package/mcp/figma-mcp-bridge/LICENSE +21 -0
  6. package/mcp/figma-mcp-bridge/README.md +25 -0
  7. package/mcp/figma-mcp-bridge/config.toml.example +10 -0
  8. package/mcp/figma-mcp-bridge/index.test.js +47 -0
  9. package/mcp/figma-mcp-bridge/package.json +16 -0
  10. package/mcp/figma-mcp-bridge/skills/figma-bridge/SKILL.md +98 -0
  11. package/mcp/figma-mcp-bridge/src/index.js +68 -0
  12. package/mcp/figma-mcp-bridge/src/server.js +159 -0
  13. package/mcp/figma-mcp-bridge/src/tools/context.js +75 -0
  14. package/mcp/figma-mcp-bridge/src/tools/index.js +2148 -0
  15. package/mcp/figma-mcp-bridge/src/tools/mutations.js +5829 -0
  16. package/mcp/figma-mcp-bridge/src/tools/nodes.js +99 -0
  17. package/mcp/figma-mcp-bridge/src/tools/pages.js +70 -0
  18. package/mcp/figma-mcp-bridge/src/websocket.js +255 -0
  19. package/mcp/rag-mcp-bridge/README.md +24 -12
  20. package/mcp/rag-mcp-bridge/config.toml.example +1 -1
  21. package/mcp/rag-mcp-bridge/index.js +80 -125
  22. package/mcp/rag-mcp-bridge/index.test.js +21 -25
  23. package/package.json +9 -4
  24. package/plugins/figma-plugin/LICENSE +21 -0
  25. package/plugins/figma-plugin/README.md +25 -0
  26. package/plugins/figma-plugin/code.js +6608 -0
  27. package/plugins/figma-plugin/manifest.json +32 -0
  28. package/plugins/figma-plugin/scripts/setup.mjs +72 -0
  29. package/plugins/figma-plugin/scripts/setup.test.mjs +45 -0
  30. package/plugins/figma-plugin/ui.html +235 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * figma_get_nodes tool
3
+ * Returns detailed information about specific nodes by their IDs
4
+ */
5
+
6
+ export const nodesTool = {
7
+ name: 'figma_get_nodes',
8
+ description:
9
+ 'Get detailed information about specific Figma nodes by their IDs. Returns node properties including type, position, size, fills, strokes, auto-layout (including layoutWrap and counterAxisSpacing), clipsContent, node-level boundVariables, explicitVariableModes, and more.',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ nodeIds: {
14
+ type: 'array',
15
+ items: { type: 'string' },
16
+ description: 'Array of Figma node IDs (e.g., ["1:23", "4:56"])'
17
+ }
18
+ },
19
+ required: ['nodeIds']
20
+ }
21
+ };
22
+
23
+ export async function handleGetNodes(bridge, args) {
24
+ if (!bridge.isConnected()) {
25
+ return {
26
+ content: [
27
+ {
28
+ type: 'text',
29
+ text: JSON.stringify(
30
+ {
31
+ error: {
32
+ code: 'NOT_CONNECTED',
33
+ message: 'Figma plugin is not connected. Please open Figma and run the Autobest Figma Plugin.'
34
+ }
35
+ },
36
+ null,
37
+ 2
38
+ )
39
+ }
40
+ ],
41
+ isError: true
42
+ };
43
+ }
44
+
45
+ const { nodeIds, depth } = args;
46
+
47
+ if (!nodeIds || !Array.isArray(nodeIds) || nodeIds.length === 0) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: JSON.stringify(
53
+ {
54
+ error: {
55
+ code: 'INVALID_PARAMS',
56
+ message: 'nodeIds must be a non-empty array of node IDs'
57
+ }
58
+ },
59
+ null,
60
+ 2
61
+ )
62
+ }
63
+ ],
64
+ isError: true
65
+ };
66
+ }
67
+
68
+ try {
69
+ const result = await bridge.sendCommand('get_nodes', { nodeIds, depth });
70
+
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: JSON.stringify(result, null, 2)
76
+ }
77
+ ]
78
+ };
79
+ } catch (error) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: JSON.stringify(
85
+ {
86
+ error: {
87
+ code: error.code || 'UNKNOWN_ERROR',
88
+ message: error.message
89
+ }
90
+ },
91
+ null,
92
+ 2
93
+ )
94
+ }
95
+ ],
96
+ isError: true
97
+ };
98
+ }
99
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * figma_list_pages tool
3
+ * Returns all pages in the current Figma document
4
+ */
5
+
6
+ export const pagesTool = {
7
+ name: 'figma_list_pages',
8
+ description:
9
+ 'List all pages in the current Figma document. Returns page IDs, names, and indicates which page is currently active.',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {},
13
+ required: []
14
+ }
15
+ };
16
+
17
+ export async function handleListPages(bridge) {
18
+ if (!bridge.isConnected()) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: JSON.stringify(
24
+ {
25
+ error: {
26
+ code: 'NOT_CONNECTED',
27
+ message: 'Figma plugin is not connected. Please open Figma and run the Autobest Figma Plugin.'
28
+ }
29
+ },
30
+ null,
31
+ 2
32
+ )
33
+ }
34
+ ],
35
+ isError: true
36
+ };
37
+ }
38
+
39
+ try {
40
+ const result = await bridge.sendCommand('list_pages', {});
41
+
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text',
46
+ text: JSON.stringify(result, null, 2)
47
+ }
48
+ ]
49
+ };
50
+ } catch (error) {
51
+ return {
52
+ content: [
53
+ {
54
+ type: 'text',
55
+ text: JSON.stringify(
56
+ {
57
+ error: {
58
+ code: error.code || 'UNKNOWN_ERROR',
59
+ message: error.message
60
+ }
61
+ },
62
+ null,
63
+ 2
64
+ )
65
+ }
66
+ ],
67
+ isError: true
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,255 @@
1
+ import { EventEmitter } from 'events';
2
+ import { WebSocketServer } from 'ws';
3
+
4
+ const DEFAULT_PORT = 3055;
5
+ const REQUEST_TIMEOUT = 30000; // 30 seconds
6
+ const HEARTBEAT_INTERVAL = 30000; // 30 seconds
7
+
8
+ export class FigmaBridge extends EventEmitter {
9
+ constructor(port = DEFAULT_PORT) {
10
+ super();
11
+ this.port = port;
12
+ this.wss = null;
13
+ this.connection = null;
14
+ this.connectionState = 'disconnected';
15
+ this.documentInfo = null;
16
+ this.pendingRequests = new Map();
17
+ this.requestIdCounter = 0;
18
+ this.heartbeatInterval = null;
19
+ }
20
+
21
+ async start() {
22
+ const maxPort = DEFAULT_PORT + 15;
23
+
24
+ const tryPort = port => {
25
+ return new Promise((resolve, reject) => {
26
+ const wss = new WebSocketServer({ port });
27
+
28
+ wss.on('listening', () => {
29
+ // eslint-disable-next-line no-console
30
+ console.error(`[FigmaBridge] WebSocket server listening on port ${port}`);
31
+ this.wss = wss;
32
+ this.port = port;
33
+ resolve();
34
+ });
35
+
36
+ wss.on('error', error => {
37
+ if (error.code === 'EADDRINUSE') {
38
+ // eslint-disable-next-line no-console
39
+ console.error(`[FigmaBridge] Port ${port} in use, trying ${port + 1}`);
40
+ wss.close();
41
+ if (port < maxPort) {
42
+ tryPort(port + 1).then(resolve, reject);
43
+ } else {
44
+ reject(new Error(`Could not find available port (tried ${DEFAULT_PORT}-${maxPort})`));
45
+ }
46
+ } else {
47
+ reject(error);
48
+ }
49
+ });
50
+
51
+ wss.on('connection', ws => this._handleConnection(ws));
52
+ });
53
+ };
54
+
55
+ return tryPort(this.port);
56
+ }
57
+
58
+ _handleConnection(ws) {
59
+ // eslint-disable-next-line no-console
60
+ console.error('[FigmaBridge] Plugin connected');
61
+
62
+ // Replace existing connection if any
63
+ if (this.connection) {
64
+ // eslint-disable-next-line no-console
65
+ console.error('[FigmaBridge] Replacing existing connection');
66
+ this.connection.close(1000, 'New connection');
67
+ }
68
+
69
+ this.connection = ws;
70
+ this.connectionState = 'connecting';
71
+
72
+ ws.on('message', data => this._handleMessage(data));
73
+ ws.on('close', () => this._handleClose());
74
+ ws.on('error', error => this._handleError(error));
75
+ }
76
+
77
+ _handleMessage(data) {
78
+ let message;
79
+ try {
80
+ message = JSON.parse(data.toString());
81
+ } catch (error) {
82
+ // eslint-disable-next-line no-console
83
+ console.error('[FigmaBridge] Failed to parse message:', error);
84
+ return;
85
+ }
86
+
87
+ // Handle handshake from plugin
88
+ if (message.type === 'handshake') {
89
+ this.documentInfo = message.payload;
90
+ this.connectionState = 'connected';
91
+ // eslint-disable-next-line no-console
92
+ console.error(`[FigmaBridge] Handshake complete: ${message.payload.fileName}`);
93
+
94
+ // Send handshake acknowledgment
95
+ this.connection.send(
96
+ JSON.stringify({
97
+ type: 'handshake_ack',
98
+ payload: {
99
+ serverVersion: '0.1.0',
100
+ sessionId: `sess_${Date.now()}`
101
+ }
102
+ })
103
+ );
104
+
105
+ this._startHeartbeat();
106
+ this.emit('connected', this.documentInfo);
107
+ return;
108
+ }
109
+
110
+ // Handle pong (heartbeat response)
111
+ if (message.type === 'pong') {
112
+ return;
113
+ }
114
+
115
+ // Handle command response
116
+ if (message.responseTo) {
117
+ const pending = this.pendingRequests.get(message.responseTo);
118
+ if (pending) {
119
+ this.pendingRequests.delete(message.responseTo);
120
+ clearTimeout(pending.timeout);
121
+
122
+ if (message.payload?.error) {
123
+ pending.reject(
124
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
125
+ new BridgeError(
126
+ message.payload.error.code || 'UNKNOWN_ERROR',
127
+ message.payload.error.message || 'Unknown error',
128
+ message.payload.error.details
129
+ )
130
+ );
131
+ } else {
132
+ pending.resolve(message.payload);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ _handleClose() {
139
+ // eslint-disable-next-line no-console
140
+ console.error('[FigmaBridge] Connection closed');
141
+ this.connectionState = 'disconnected';
142
+ this.connection = null;
143
+ this.documentInfo = null;
144
+ this._stopHeartbeat();
145
+
146
+ // Reject all pending requests
147
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
148
+ for (const [id, pending] of this.pendingRequests) {
149
+ clearTimeout(pending.timeout);
150
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
151
+ pending.reject(new BridgeError('CONNECTION_CLOSED', 'Connection closed'));
152
+ }
153
+ this.pendingRequests.clear();
154
+
155
+ this.emit('disconnected');
156
+ }
157
+
158
+ _handleError(error) {
159
+ // eslint-disable-next-line no-console
160
+ console.error('[FigmaBridge] WebSocket error:', error.message);
161
+ this.emit('error', error);
162
+ }
163
+
164
+ _startHeartbeat() {
165
+ this._stopHeartbeat();
166
+ this.heartbeatInterval = setInterval(() => {
167
+ if (this.connection && this.connectionState === 'connected') {
168
+ this.connection.send(
169
+ JSON.stringify({
170
+ type: 'ping',
171
+ timestamp: Date.now()
172
+ })
173
+ );
174
+ }
175
+ }, HEARTBEAT_INTERVAL);
176
+ }
177
+
178
+ _stopHeartbeat() {
179
+ if (this.heartbeatInterval) {
180
+ clearInterval(this.heartbeatInterval);
181
+ this.heartbeatInterval = null;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Send a command to the Figma plugin and wait for response
187
+ * @param {string} command - Command name
188
+ * @param {object} payload - Command payload
189
+ * @returns {Promise<object>} Response payload
190
+ */
191
+ async sendCommand(command, payload = {}) {
192
+ if (this.connectionState !== 'connected') {
193
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
194
+ throw new BridgeError(
195
+ 'NOT_CONNECTED',
196
+ 'Figma plugin is not connected. Please open Figma and run the Autobest Figma Plugin.'
197
+ );
198
+ }
199
+
200
+ const requestId = `req_${++this.requestIdCounter}`;
201
+
202
+ return new Promise((resolve, reject) => {
203
+ const timeout = setTimeout(() => {
204
+ this.pendingRequests.delete(requestId);
205
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
206
+ reject(new BridgeError('TIMEOUT', `Command "${command}" timed out after ${REQUEST_TIMEOUT}ms`));
207
+ }, REQUEST_TIMEOUT);
208
+
209
+ this.pendingRequests.set(requestId, { resolve, reject, timeout });
210
+
211
+ this.connection.send(
212
+ JSON.stringify({
213
+ requestId,
214
+ command,
215
+ payload
216
+ })
217
+ );
218
+ });
219
+ }
220
+
221
+ isConnected() {
222
+ return this.connectionState === 'connected';
223
+ }
224
+
225
+ getDocumentInfo() {
226
+ return this.documentInfo;
227
+ }
228
+
229
+ async stop() {
230
+ this._stopHeartbeat();
231
+
232
+ if (this.connection) {
233
+ this.connection.close(1000, 'Server shutdown');
234
+ }
235
+
236
+ if (this.wss) {
237
+ return new Promise(resolve => {
238
+ this.wss.close(() => {
239
+ // eslint-disable-next-line no-console
240
+ console.error('[FigmaBridge] WebSocket server stopped');
241
+ resolve();
242
+ });
243
+ });
244
+ }
245
+ }
246
+ }
247
+
248
+ export class BridgeError extends Error {
249
+ constructor(code, message, details = null) {
250
+ super(message);
251
+ this.name = 'BridgeError';
252
+ this.code = code;
253
+ this.details = details;
254
+ }
255
+ }
@@ -4,13 +4,33 @@
4
4
 
5
5
  ## 启动
6
6
 
7
- 先启动 PRD Knowledge HTTP API,并在 Codex 的 `config.toml` 中加入 [config.toml.example](config.toml.example) 的配置。Codex 将通过 npm 安装并启动发布包中的 MCP:
7
+ 先启动 PRD Knowledge HTTP API。直接在终端验证时,必须在同一条命令中提供 API 根地址:
8
8
 
9
9
  ```bash
10
- npx --yes --package=@autobest-ui/agent@latest autobest-rag-mcp
10
+ RAG_API_BASE_URL="http://192.168.1.12:3000/api/knowledge" \
11
+ npx --yes \
12
+ --package=@autobest-ui/agent@latest \
13
+ autobest-rag-mcp
11
14
  ```
12
15
 
13
- `@latest` 可以替换为明确版本,例如 `@1.0.0`。STDIO 用于 MCP 协议通信,服务不会向标准输出写日志。
16
+ `@latest` 可以替换为明确版本,例如 `@1.0.0`。启动成功后,服务会通过 stdio 持续等待 MCP 客户端请求,并且不会向标准输出写日志;终端看起来没有响应属于正常状态,可按 `Ctrl+C` 停止。
17
+
18
+ 供 Codex 长期使用时,将下面的完整配置加入 `~/.codex/config.toml`,然后重启 Codex:
19
+
20
+ ```toml
21
+ [mcp_servers.rag-mcp-bridge]
22
+ command = "npx"
23
+ args = ["--yes", "--package=@autobest-ui/agent@latest", "autobest-rag-mcp"]
24
+ startup_timeout_sec = 30
25
+ tool_timeout_sec = 120
26
+ enabled = true
27
+
28
+ [mcp_servers.rag-mcp-bridge.env]
29
+ RAG_API_BASE_URL = "http://192.168.1.12:3000/api/knowledge"
30
+ RAG_MCP_HTTP_TIMEOUT_MS = "120000"
31
+ ```
32
+
33
+ 完整配置文件也可直接参考 [config.toml.example](config.toml.example)。
14
34
 
15
35
  ## 环境变量
16
36
 
@@ -19,15 +39,7 @@ npx --yes --package=@autobest-ui/agent@latest autobest-rag-mcp
19
39
  | `RAG_API_BASE_URL` | 无,必填 | PRD Knowledge HTTP API 根地址,必须通过 `config.toml` 的 `[mcp_servers.rag-mcp-bridge.env]` 配置 |
20
40
  | `RAG_MCP_HTTP_TIMEOUT_MS` | `120000` | 单次 HTTP 请求超时,单位为毫秒 |
21
41
 
22
- 示例:
23
-
24
- ```toml
25
- [mcp_servers.rag-mcp-bridge.env]
26
- RAG_API_BASE_URL = "http://127.0.0.1:3000/api/knowledge"
27
- RAG_MCP_HTTP_TIMEOUT_MS = "120000"
28
- ```
29
-
30
- `RAG_API_BASE_URL` 仅在 MCP 客户端配置中提供,服务代码不包含默认 API 地址。完整配置见 [config.toml.example](config.toml.example)。
42
+ `RAG_API_BASE_URL` 仅在 MCP 客户端配置中提供,服务代码不包含默认 API 地址。
31
43
 
32
44
  ## 工具
33
45
 
@@ -7,6 +7,6 @@ enabled = true
7
7
 
8
8
  [mcp_servers.rag-mcp-bridge.env]
9
9
  # 必填:按实际部署地址修改,MCP 服务本身不提供默认地址。
10
- RAG_API_BASE_URL = "http://127.0.0.1:3000/api/knowledge"
10
+ RAG_API_BASE_URL = "http://192.168.1.12:3000/api/knowledge"
11
11
  # 可选:单次 HTTP 请求超时,单位为毫秒。
12
12
  RAG_MCP_HTTP_TIMEOUT_MS = "120000"