@pixel-normal-edit/mcp 2.0.8 → 2.1.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.
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * workflow.js — Workflow Engine (Strict step ordering with modes, phases, layers)
4
+ */
5
+
6
+ const { WORKFLOW_STEPS, MODES, STATUSES, DRAWING_PHASES } = require('./rules');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const STATE_FILE = path.join(__dirname, '..', 'workflow_state.json');
11
+ let sessions = new Map();
12
+
13
+ // --- PERSISTENCE ---
14
+ function saveState() {
15
+ const data = {};
16
+ for (const [key, val] of sessions.entries()) {
17
+ data[key] = val;
18
+ }
19
+ try {
20
+ fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), 'utf-8');
21
+ } catch (e) {
22
+ console.error('Failed to save workflow state:', e);
23
+ }
24
+ }
25
+
26
+ function loadState() {
27
+ if (fs.existsSync(STATE_FILE)) {
28
+ try {
29
+ const content = fs.readFileSync(STATE_FILE, 'utf-8');
30
+ const data = JSON.parse(content);
31
+ sessions = new Map(Object.entries(data));
32
+ } catch (e) {
33
+ console.error('Failed to load workflow state:', e);
34
+ }
35
+ }
36
+ }
37
+
38
+ // Load state on startup
39
+ loadState();
40
+
41
+
42
+ /**
43
+ * Start a new workflow for a session
44
+ * @param {string} sessionId - Session ID (usually MCP_SESSION)
45
+ * @param {string} mode - The mode to start in (CREATE_IMAGE, EDIT_IMAGE, CREATE_ANIMATION)
46
+ * @returns {Object} Initial workflow state
47
+ */
48
+ function start(sessionId, mode = MODES.CREATE_IMAGE) {
49
+ if (!WORKFLOW_STEPS[mode]) {
50
+ throw new Error(`Invalid mode: ${mode}`);
51
+ }
52
+
53
+ const state = {
54
+ current_mode: mode,
55
+ currentStepIndex: 0,
56
+ steps: WORKFLOW_STEPS[mode].map(s => ({ ...s, status: STATUSES.PENDING })),
57
+ current_layer: 0,
58
+ current_frame: mode === MODES.CREATE_ANIMATION ? 0 : null,
59
+ current_phase: null,
60
+ animation_meta: { fps: null, total_frames: null, loop: true },
61
+ step_status: STATUSES.IN_PROGRESS,
62
+ user_approval_required: false,
63
+ user_input_request: null, // { id, type, fields, status, created_at, response }
64
+ step_completion_flags: {}, // track internal flags for steps
65
+ completedSteps: [],
66
+ history: [],
67
+ startedAt: Date.now(),
68
+ };
69
+
70
+ sessions.set(sessionId, state);
71
+ saveState();
72
+ return getState(sessionId);
73
+ }
74
+
75
+ /**
76
+ * Get current workflow state
77
+ * @param {string} sessionId
78
+ * @returns {Object|null}
79
+ */
80
+ function getState(sessionId) {
81
+ return sessions.get(sessionId) || null;
82
+ }
83
+
84
+ /**
85
+ * Get current step
86
+ * @param {string} sessionId
87
+ * @returns {Object|null}
88
+ */
89
+ function getCurrentStep(sessionId) {
90
+ const state = sessions.get(sessionId);
91
+ if (!state) return null;
92
+ return state.steps[state.currentStepIndex] || null;
93
+ }
94
+
95
+ /**
96
+ * Check if the agent is allowed to perform this action
97
+ * @param {string} sessionId
98
+ * @param {string} stepId - ID of the step to perform
99
+ * @returns {{ allowed: boolean, message?: string, currentStep?: Object }}
100
+ */
101
+ function validateStep(sessionId, stepId) {
102
+ const state = sessions.get(sessionId);
103
+ if (!state) {
104
+ return {
105
+ allowed: false,
106
+ message: '⚠️ Workflow has not started. Call workflow_start first!',
107
+ };
108
+ }
109
+
110
+ if (state.user_approval_required) {
111
+ return {
112
+ allowed: false,
113
+ message: '⛔ Workflow is waiting for user approval. Ask the user for confirmation.',
114
+ currentStep: state.steps[state.currentStepIndex]
115
+ };
116
+ }
117
+
118
+ const current = state.steps[state.currentStepIndex];
119
+ if (!current) {
120
+ return {
121
+ allowed: false,
122
+ message: '✅ Workflow is complete! No remaining steps.',
123
+ };
124
+ }
125
+
126
+ const targetIndex = state.steps.findIndex(s => s.id === stepId);
127
+ if (targetIndex === -1) {
128
+ return {
129
+ allowed: false,
130
+ message: `❌ Step "${stepId}" not found in current mode workflow.`,
131
+ };
132
+ }
133
+
134
+ if (targetIndex < state.currentStepIndex) {
135
+ return {
136
+ allowed: true,
137
+ message: `⚠️ Step "${stepId}" was already completed. Are you sure you want to redo it?`,
138
+ currentStep: current,
139
+ };
140
+ }
141
+
142
+ if (targetIndex === state.currentStepIndex) {
143
+ return {
144
+ allowed: true,
145
+ currentStep: current,
146
+ };
147
+ }
148
+
149
+ return {
150
+ allowed: false,
151
+ message: `⛔ You cannot perform "${stepId}" yet. First complete: "${current.label}" (${current.description})`,
152
+ currentStep: current,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Set user approval requirement
158
+ * @param {string} sessionId
159
+ * @param {boolean} required
160
+ */
161
+ function setApprovalRequired(sessionId, required) {
162
+ const state = sessions.get(sessionId);
163
+ if (!state) return false;
164
+ state.user_approval_required = required;
165
+ if (required) {
166
+ state.step_status = STATUSES.WAITING_FOR_USER;
167
+ } else {
168
+ if (state.user_input_request && state.user_input_request.status === 'WAITING_FOR_USER') {
169
+ return false; // Cannot unpause if waiting for explicit input request
170
+ }
171
+ state.step_status = STATUSES.IN_PROGRESS;
172
+ }
173
+ saveState();
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Create a user input request
179
+ * @param {string} sessionId
180
+ * @param {string} type - e.g. CANVAS_SIZE, APPROVE_PLAN, SELECT_EDIT_REGION
181
+ * @param {Object} fields - fields required
182
+ */
183
+ function createInputRequest(sessionId, type, fields = {}) {
184
+ const state = sessions.get(sessionId);
185
+ if (!state) return false;
186
+
187
+ state.user_input_request = {
188
+ id: `req_${Date.now()}_${Math.floor(Math.random()*1000)}`,
189
+ type,
190
+ fields,
191
+ status: 'WAITING_FOR_USER',
192
+ created_at: Date.now()
193
+ };
194
+ state.step_status = STATUSES.WAITING_FOR_USER;
195
+ state.history.push({ event: 'input_request_created', type, time: Date.now() });
196
+ saveState();
197
+ return state.user_input_request.id;
198
+ }
199
+
200
+ /**
201
+ * Complete a user input request
202
+ * @param {string} sessionId
203
+ * @param {string} reqId
204
+ * @param {string} response
205
+ */
206
+ function completeInputRequest(sessionId, reqId, response) {
207
+ const state = sessions.get(sessionId);
208
+ if (!state || !state.user_input_request || state.user_input_request.id !== reqId) return false;
209
+
210
+ state.user_input_request.status = 'COMPLETED';
211
+ state.user_input_request.response = response;
212
+ state.step_status = STATUSES.IN_PROGRESS; // Unpause workflow
213
+
214
+ state.history.push({ event: 'input_request_completed', reqId, time: Date.now() });
215
+ saveState();
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * Update layer/frame
221
+ */
222
+ function setLayerAndFrame(sessionId, layer, frame = null) {
223
+ const state = sessions.get(sessionId);
224
+ if (!state) return false;
225
+
226
+ if (layer !== undefined && layer !== null) {
227
+ state.current_layer = layer;
228
+ }
229
+ if (frame !== undefined && frame !== null) {
230
+ state.current_frame = frame;
231
+ }
232
+ state.history.push({
233
+ event: 'layer_frame_change',
234
+ layer: state.current_layer,
235
+ frame: state.current_frame,
236
+ time: Date.now()
237
+ });
238
+ saveState();
239
+ return true;
240
+ }
241
+
242
+ /**
243
+ * Update phase
244
+ */
245
+ function setPhase(sessionId, phase) {
246
+ const state = sessions.get(sessionId);
247
+ if (!state) return false;
248
+
249
+ state.current_phase = phase;
250
+ state.history.push({
251
+ event: 'phase_change',
252
+ phase: phase,
253
+ time: Date.now()
254
+ });
255
+ saveState();
256
+ return true;
257
+ }
258
+
259
+ /**
260
+ * Set animation meta
261
+ */
262
+ function setAnimationMeta(sessionId, fps, total_frames, loop) {
263
+ const state = sessions.get(sessionId);
264
+ if (!state) return false;
265
+
266
+ if (fps !== undefined) state.animation_meta.fps = fps;
267
+ if (total_frames !== undefined) state.animation_meta.total_frames = total_frames;
268
+ if (loop !== undefined) state.animation_meta.loop = loop;
269
+
270
+ saveState();
271
+ return true;
272
+ }
273
+
274
+ /**
275
+ * Set step completion flag (internal validation)
276
+ */
277
+ function setStepFlag(sessionId, flagName, value) {
278
+ const state = sessions.get(sessionId);
279
+ if (!state) return false;
280
+ state.step_completion_flags[flagName] = value;
281
+ saveState();
282
+ return true;
283
+ }
284
+
285
+ /**
286
+ * Mark current step as completed and advance to the next step
287
+ * @param {string} sessionId
288
+ * @returns {Object} Step transition result
289
+ */
290
+ function advance(sessionId) {
291
+ const state = sessions.get(sessionId);
292
+ if (!state) {
293
+ return { success: false, message: 'Workflow has not started' };
294
+ }
295
+
296
+ if (state.user_approval_required) {
297
+ return { success: false, message: 'Cannot advance: Waiting for user approval.' };
298
+ }
299
+
300
+ if (state.step_status === STATUSES.WAITING_FOR_USER) {
301
+ return { success: false, message: 'Cannot advance: Waiting for user approval or input.' };
302
+ }
303
+
304
+ const current = state.steps[state.currentStepIndex];
305
+ if (!current) {
306
+ return { success: false, message: 'Workflow is complete' };
307
+ }
308
+
309
+ // Pre-condition validation block
310
+ if (current.id === 'INITIALIZE_CANVAS' && !state.step_completion_flags.canvas_initialized) {
311
+ return { success: false, message: 'Cannot advance: Canvas has not been initialized. Create a canvas first.' };
312
+ }
313
+ if (current.id === 'PLAN_DRAWING' && !state.step_completion_flags.plan_created) {
314
+ return { success: false, message: 'Cannot advance: Drawing plan has not been finalized.' };
315
+ }
316
+
317
+ current.status = STATUSES.COMPLETED;
318
+ state.completedSteps.push(current.id);
319
+ state.history.push({ event: 'step_completed', step: current.id, layer: state.current_layer, frame: state.current_frame, time: Date.now() });
320
+
321
+ state.currentStepIndex++;
322
+
323
+ const next = state.steps[state.currentStepIndex] || null;
324
+ if (next) {
325
+ next.status = STATUSES.IN_PROGRESS;
326
+ state.step_status = STATUSES.IN_PROGRESS;
327
+ saveState();
328
+ return {
329
+ success: true,
330
+ completedStep: current,
331
+ nextStep: next,
332
+ message: `✅ Completed: "${current.label}". Next step: "${next.label}" — ${next.description}`,
333
+ };
334
+ }
335
+
336
+ state.step_status = STATUSES.COMPLETED;
337
+ saveState();
338
+ return {
339
+ success: true,
340
+ completedStep: current,
341
+ nextStep: null,
342
+ message: `🎉 All steps completed! The task is ready.`,
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Reset workflow to the beginning
348
+ * @param {string} sessionId
349
+ */
350
+ function reset(sessionId) {
351
+ const state = sessions.get(sessionId);
352
+ const mode = state ? state.current_mode : MODES.CREATE_IMAGE;
353
+ sessions.delete(sessionId);
354
+ saveState();
355
+ return start(sessionId, mode);
356
+ }
357
+
358
+ /**
359
+ * Get text-based progress report for the agent
360
+ * @param {string} sessionId
361
+ * @returns {string}
362
+ */
363
+ function getProgressReport(sessionId) {
364
+ const state = sessions.get(sessionId);
365
+ if (!state) return 'Workflow has not started.';
366
+
367
+ const lines = [
368
+ `📋 **Workflow Progress: [Mode: ${state.current_mode}]**`,
369
+ `Layer: ${state.current_layer} | Frame: ${state.current_frame !== null ? state.current_frame : 'N/A'}`,
370
+ `Phase: ${state.current_phase !== null ? state.current_phase : 'N/A'}`,
371
+ `Animation Meta: FPS=${state.animation_meta.fps || 'N/A'}, Frames=${state.animation_meta.total_frames || 'N/A'}, Loop=${state.animation_meta.loop}`,
372
+ `Status: ${state.step_status}`,
373
+ `User Approval Required: ${state.user_approval_required ? 'YES' : 'NO'}`,
374
+ state.user_input_request && state.user_input_request.status === 'WAITING_FOR_USER' ? `⚠️ PENDING REQUEST: ${state.user_input_request.type}` : '',
375
+ `\n**Steps:**`
376
+ ];
377
+
378
+ state.steps.forEach((step, i) => {
379
+ const icon = step.status === STATUSES.COMPLETED ? '✅' :
380
+ step.status === STATUSES.IN_PROGRESS ? '🔄' : '⬜';
381
+ const marker = i === state.currentStepIndex ? ' ← **IN PROGRESS**' : '';
382
+ lines.push(`${icon} **${step.label}**${marker}`);
383
+ if (step.status === STATUSES.IN_PROGRESS || i === state.currentStepIndex) {
384
+ lines.push(` → ${step.description}`);
385
+ }
386
+ });
387
+
388
+ return lines.join('\n');
389
+ }
390
+
391
+ module.exports = {
392
+ start,
393
+ getState,
394
+ getCurrentStep,
395
+ validateStep,
396
+ advance,
397
+ reset,
398
+ getProgressReport,
399
+ setApprovalRequired,
400
+ createInputRequest,
401
+ completeInputRequest,
402
+ setLayerAndFrame,
403
+ setPhase,
404
+ setAnimationMeta,
405
+ setStepFlag
406
+ };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * anchors.js — Coordinate Anchor tools
3
+ * anchor-tools.js — Coordinate Anchor tools
4
4
  *
5
5
  * Tools: anchor_set, anchor_get, anchor_list
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * animation.js — Animation Frame tools
3
+ * animation-tools.js — Animation Frame tools
4
4
  *
5
5
  * Tools: animation_enable, animation_add_frame, animation_go_to_frame,
6
6
  * animation_ensure_frame, animation_get_info, animation_remove_frame,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * canvas.js — Canvas & Setup tools
3
+ * canvas-tools.js — Canvas & Setup tools
4
4
  *
5
5
  * Tools: canvas_get_size, canvas_resize, canvas_clear, canvas_trim
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * colors.js — Color tools
3
+ * color-tools.js — Color tools
4
4
  *
5
5
  * Tools: color_set, color_get
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * drawing.js — Drawing Primitives tools
3
+ * drawing-tools.js — Drawing Primitives tools
4
4
  *
5
5
  * Tools: draw_pixel, draw_erase, draw_line, draw_rect, draw_circle,
6
6
  * draw_ellipse, draw_polygon, draw_fill, draw_gradient_rect,
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * filters.js — Visual Filter tools
3
+ * filter-tools.js — Visual Filter tools
4
4
  *
5
5
  * Tools: filter_apply
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * health.js — Health Check / Ping tools
3
+ * health-tools.js — Health Check / Ping tools
4
4
  *
5
5
  * Tools: ping
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * history.js — History (Undo/Redo) tools
3
+ * history-tools.js — History (Undo/Redo) tools
4
4
  *
5
5
  * Tools: history_undo, history_redo
6
6
  */
package/tools/index.js CHANGED
@@ -4,21 +4,37 @@
4
4
  *
5
5
  * Imports and registers all core drawing/utility tools on an MCP server instance.
6
6
  * To add a new tool category, create a new file in this folder and add it here.
7
+ *
8
+ * File naming convention: {category}-tools.js
9
+ * - canvas-tools.js → Canvas operations
10
+ * - workspace-tools.js → Tab/workspace management
11
+ * - animation-tools.js → Animation frames
12
+ * - drawing-tools.js → Drawing primitives
13
+ * - sprite-tools.js → Sprite/stamp system
14
+ * - region-tools.js → Region clipboard
15
+ * - filter-tools.js → Visual filters
16
+ * - anchor-tools.js → Coordinate anchors
17
+ * - query-tools.js → Query & visual feedback
18
+ * - history-tools.js → Undo/redo
19
+ * - mode-tools.js → Mode settings
20
+ * - color-tools.js → Color management
21
+ * - health-tools.js → Health check
22
+ * - layer-tools.js → Layer management
7
23
  */
8
- const canvas = require('./canvas');
9
- const workspace = require('./workspace');
10
- const animation = require('./animation');
11
- const drawing = require('./drawing');
12
- const sprite = require('./sprite');
13
- const region = require('./region');
14
- const filters = require('./filters');
15
- const anchors = require('./anchors');
16
- const query = require('./query');
17
- const history = require('./history');
18
- const modes = require('./modes');
19
- const colors = require('./colors');
20
- const health = require('./health');
21
- const layer = require('./layer');
24
+ const canvas = require('./canvas-tools');
25
+ const workspace = require('./workspace-tools');
26
+ const animation = require('./animation-tools');
27
+ const drawing = require('./drawing-tools');
28
+ const sprite = require('./sprite-tools');
29
+ const region = require('./region-tools');
30
+ const filters = require('./filter-tools');
31
+ const anchors = require('./anchor-tools');
32
+ const query = require('./query-tools');
33
+ const history = require('./history-tools');
34
+ const modes = require('./mode-tools');
35
+ const colors = require('./color-tools');
36
+ const health = require('./health-tools');
37
+ const layer = require('./layer-tools');
22
38
 
23
39
  /**
24
40
  * Register all core tools on the given MCP server instance.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * layer.js — MCP Tool Registration for Layers
3
+ * layer-tools.js — MCP Tool Registration for Layers
4
4
  *
5
5
  * Exposes the layer management functionality to MCP clients.
6
6
  */
@@ -77,4 +77,4 @@ function register(server) {
77
77
  });
78
78
  }
79
79
 
80
- module.exports = { register };
80
+ module.exports = { register };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * modes.js — Mode settings tools
3
+ * mode-tools.js — Mode settings tools
4
4
  *
5
5
  * Tools: mode_set_mirror, mode_set_onion_skin, mode_set_grid
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * query.js — Query & Visual Feedback tools
3
+ * query-tools.js — Query & Visual Feedback tools
4
4
  *
5
5
  * Tools: query_snapshot, query_bounding_box, query_palette, query_pixel,
6
6
  * query_export_image, query_document_state
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * region.js — Region Clipboard tools
3
+ * region-tools.js — Region Clipboard tools
4
4
  *
5
5
  * Tools: region_copy, region_paste
6
6
  */
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * sprite.js — Sprite / Stamp System tools
3
+ * sprite-tools.js — Sprite / Stamp System tools
4
4
  *
5
5
  * Tools: sprite_draw, sprite_save_stamp, sprite_use_stamp, sprite_list_stamps
6
6
  */
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * workspace.js — Workspace / Tabs tools
3
+ * workspace-tools.js — Workspace / Tabs tools
4
4
  *
5
5
  * Tools: workspace_list_tabs, workspace_get_active_tab, workspace_create_tab,
6
6
  * workspace_switch_tab, workspace_rename_tab, workspace_save
7
7
  */
8
8
  const { z } = require('zod');
9
- const { registerTool } = require('../core/server');
9
+ const { registerTool, sendCommand } = require('../core/server');
10
10
 
11
11
  function register(server) {
12
12
  registerTool(server, 'workspace_list_tabs',
@@ -19,7 +19,6 @@ function register(server) {
19
19
  {},
20
20
  () => ({ action: 'getActiveTabId' }));
21
21
 
22
- const { sendCommand } = require('../core/server');
23
22
  server.tool('workspace_create_tab',
24
23
  'Create a new blank canvas tab',
25
24
  {