@agiflowai/scaffold-mcp 1.0.21 → 1.0.23

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.
@@ -1,15 +1,172 @@
1
- const require_ListScaffoldingMethodsTool = require('./ListScaffoldingMethodsTool-Dnd3E5X_.cjs');
1
+ const require_ListScaffoldingMethodsTool = require('./ListScaffoldingMethodsTool-CkIkgP_u.cjs');
2
+ require('./tools-CC-lrhQ8.cjs');
3
+ let __agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
2
4
  let node_path = require("node:path");
3
5
  node_path = require_ListScaffoldingMethodsTool.__toESM(node_path);
4
- let __agiflowai_aicode_utils = require("@agiflowai/aicode-utils");
5
6
  let node_fs_promises = require("node:fs/promises");
6
7
  node_fs_promises = require_ListScaffoldingMethodsTool.__toESM(node_fs_promises);
7
8
  let node_os = require("node:os");
8
9
  node_os = require_ListScaffoldingMethodsTool.__toESM(node_os);
9
10
  let __agiflowai_hooks_adapter = require("@agiflowai/hooks-adapter");
11
+ let node_child_process = require("node:child_process");
10
12
 
13
+ //#region src/hooks/claudeCode/phantomCodeCheck.ts
14
+ /**
15
+ * PhantomCodeCheck Hook for Claude Code
16
+ *
17
+ * DESIGN PATTERNS:
18
+ * - Class-based hook pattern: Encapsulates lifecycle hooks in a single class
19
+ * - Fail-open pattern: Errors allow operation to proceed (return DECISION_SKIP)
20
+ * - Marker-based detection: Scans for scaffold marker comments in code files
21
+ *
22
+ * CODING STANDARDS:
23
+ * - Export a class with stop, userPromptSubmit, taskCompleted methods
24
+ * - Handle all errors gracefully with fail-open behavior
25
+ * - Use execFileSync with args array to avoid shell injection
26
+ *
27
+ * AVOID:
28
+ * - Blocking operations on errors
29
+ * - Shell injection via marker parameter
30
+ * - Mutating context object
31
+ */
32
+ const EXCLUDED_DIRS = [
33
+ "node_modules",
34
+ "dist",
35
+ ".git",
36
+ ".next",
37
+ "build",
38
+ "coverage",
39
+ ".claude"
40
+ ];
41
+ /**
42
+ * PhantomCodeCheckHook — scans for unimplemented scaffold files containing marker comments.
43
+ *
44
+ * Checks at session boundaries (Stop, UserPromptSubmit, TaskCompleted) whether
45
+ * any generated files still carry the `// <marker>` comment, indicating they
46
+ * have not yet been implemented by the AI agent.
47
+ */
48
+ var PhantomCodeCheckHook = class {
49
+ markerComment;
50
+ constructor(marker = "@scaffold-generated") {
51
+ this.markerComment = `// ${marker}`;
52
+ }
53
+ /**
54
+ * Scans cwd for files containing the scaffold marker comment.
55
+ * Returns relative file paths. Returns empty array on any error (fail-open).
56
+ */
57
+ scanForPhantomFiles(cwd) {
58
+ try {
59
+ return (0, node_child_process.execFileSync)("grep", [
60
+ "-rl",
61
+ this.markerComment,
62
+ "--include=*.ts",
63
+ "--include=*.tsx",
64
+ "--include=*.js",
65
+ "--include=*.jsx",
66
+ ...EXCLUDED_DIRS.map((dir) => `--exclude-dir=${dir}`),
67
+ "."
68
+ ], {
69
+ cwd,
70
+ timeout: 1e4,
71
+ encoding: "utf8"
72
+ }).trim().split("\n").filter(Boolean).map((f) => f.replace(/^\.\//, ""));
73
+ } catch (error) {
74
+ if (error instanceof Error && "status" in error && error.status === 1) return [];
75
+ return [];
76
+ }
77
+ }
78
+ /**
79
+ * Stop hook — blocks session end if phantom files are found.
80
+ * Returns DECISION_DENY to prevent Claude from stopping with unimplemented files.
81
+ */
82
+ async stop(context) {
83
+ try {
84
+ const phantomFiles = this.scanForPhantomFiles(context.cwd);
85
+ if (phantomFiles.length === 0) return {
86
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
87
+ message: "No phantom scaffold files found"
88
+ };
89
+ const fileList = phantomFiles.map((f) => ` - ${f}`).join("\n");
90
+ return {
91
+ decision: __agiflowai_hooks_adapter.DECISION_DENY,
92
+ message: `⚠️ ${phantomFiles.length} scaffold file(s) still contain \`${this.markerComment}\` and have not been implemented:\n${fileList}\n\nPlease implement these files and remove the marker comment before ending the session.`
93
+ };
94
+ } catch {
95
+ return {
96
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
97
+ message: "PhantomCodeCheckHook.stop error — skipping"
98
+ };
99
+ }
100
+ }
101
+ /**
102
+ * UserPromptSubmit hook — warns about phantom files without blocking.
103
+ * Returns DECISION_ALLOW with userMessage written to stderr (visible to user, not LLM).
104
+ */
105
+ async userPromptSubmit(context) {
106
+ try {
107
+ const phantomFiles = this.scanForPhantomFiles(context.cwd);
108
+ if (phantomFiles.length === 0) return {
109
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
110
+ message: "No phantom scaffold files found"
111
+ };
112
+ const fileList = phantomFiles.map((f) => ` - ${f}`).join("\n");
113
+ return {
114
+ decision: __agiflowai_hooks_adapter.DECISION_ALLOW,
115
+ message: "",
116
+ userMessage: `⚠️ Reminder: ${phantomFiles.length} scaffold file(s) still contain \`${this.markerComment}\`:\n${fileList}\n\nPlease implement these files and remove the marker comment.`
117
+ };
118
+ } catch {
119
+ return {
120
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
121
+ message: "PhantomCodeCheckHook.userPromptSubmit error — skipping"
122
+ };
123
+ }
124
+ }
125
+ /**
126
+ * TaskCompleted hook — blocks task completion if phantom files are found.
127
+ * Returns DECISION_DENY with exitCode 2 to signal incomplete scaffolding.
128
+ */
129
+ async taskCompleted(context) {
130
+ try {
131
+ const phantomFiles = this.scanForPhantomFiles(context.cwd);
132
+ if (phantomFiles.length === 0) return {
133
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
134
+ message: "No phantom scaffold files found"
135
+ };
136
+ const fileList = phantomFiles.map((f) => ` - ${f}`).join("\n");
137
+ return {
138
+ decision: __agiflowai_hooks_adapter.DECISION_DENY,
139
+ exitCode: 2,
140
+ message: `⚠️ ${phantomFiles.length} scaffold file(s) still contain \`${this.markerComment}\` and have not been implemented:\n${fileList}\n\nTask cannot complete until all scaffold files are implemented.`
141
+ };
142
+ } catch {
143
+ return {
144
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
145
+ message: "PhantomCodeCheckHook.taskCompleted error — skipping"
146
+ };
147
+ }
148
+ }
149
+ };
150
+
151
+ //#endregion
11
152
  //#region src/hooks/claudeCode/useScaffoldMethod.ts
12
153
  /**
154
+ * Type guard for ScaffoldMethodsResponse
155
+ */
156
+ function isScaffoldMethodsResponse(value) {
157
+ if (typeof value !== "object" || value === null) return false;
158
+ if ("methods" in value && !Array.isArray(value.methods)) return false;
159
+ if ("nextCursor" in value && typeof value.nextCursor !== "string") return false;
160
+ return true;
161
+ }
162
+ /**
163
+ * Type guard for PendingScaffoldLogEntry
164
+ */
165
+ function isPendingScaffoldLogEntry(value) {
166
+ if (typeof value !== "object" || value === null) return false;
167
+ return "scaffoldId" in value && typeof value.scaffoldId === "string" && "generatedFiles" in value && Array.isArray(value.generatedFiles) && "projectPath" in value && typeof value.projectPath === "string";
168
+ }
169
+ /**
13
170
  * UseScaffoldMethod Hook class for Claude Code
14
171
  *
15
172
  * Provides lifecycle hooks for tool execution:
@@ -40,6 +197,17 @@ var UseScaffoldMethodHook = class {
40
197
  decision: __agiflowai_hooks_adapter.DECISION_SKIP,
41
198
  message: "File is outside working directory - skipping scaffold method check"
42
199
  };
200
+ let fileExists = false;
201
+ try {
202
+ await node_fs_promises.default.access(absoluteFilePath);
203
+ fileExists = true;
204
+ } catch (accessErr) {
205
+ if (!(accessErr instanceof Error && "code" in accessErr && accessErr.code === "ENOENT")) throw accessErr;
206
+ }
207
+ if (fileExists) return {
208
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
209
+ message: "File already exists - skipping scaffold method check"
210
+ };
43
211
  const executionLog = new __agiflowai_hooks_adapter.ExecutionLogService(context.session_id);
44
212
  if (await executionLog.hasExecuted({
45
213
  filePath,
@@ -70,7 +238,12 @@ var UseScaffoldMethodHook = class {
70
238
  decision: __agiflowai_hooks_adapter.DECISION_SKIP,
71
239
  message: "⚠️ Invalid response format from scaffolding methods tool"
72
240
  };
73
- const data = JSON.parse(resultText);
241
+ const parsed = JSON.parse(resultText);
242
+ if (!isScaffoldMethodsResponse(parsed)) return {
243
+ decision: __agiflowai_hooks_adapter.DECISION_SKIP,
244
+ message: "⚠️ Unexpected response shape from scaffolding methods tool"
245
+ };
246
+ const data = parsed;
74
247
  if (!data.methods || data.methods.length === 0) {
75
248
  await executionLog.logExecution({
76
249
  filePath,
@@ -82,19 +255,9 @@ var UseScaffoldMethodHook = class {
82
255
  message: "No scaffolding methods are available for this project template. You should write new files directly using the Write tool."
83
256
  };
84
257
  }
85
- let message = "🎯 **Scaffolding Methods Available**\\n\\n";
86
- message += "Before writing new files, check if any of these scaffolding methods match your needs:\\n\\n";
87
- for (const method of data.methods) {
88
- message += `**${method.name}**\\n`;
89
- message += `${method.instruction || method.description || "No description available"}\\n`;
90
- if (method.variables_schema?.required && method.variables_schema.required.length > 0) message += `Required: ${method.variables_schema.required.join(", ")}\\n`;
91
- message += "\\n";
92
- }
93
- if (data.nextCursor) message += `\\n_Note: More methods available. Use cursor "${data.nextCursor}" to see more._\\n\\n`;
94
- message += "\\n**Instructions:**\\n";
95
- message += "1. If one of these scaffold methods matches what you need to create, use the `use-scaffold-method` MCP tool instead of writing files manually\\n";
96
- message += "2. If none of these methods are relevant to your task, proceed to write new files directly using the Write tool\\n";
97
- message += "3. Using scaffold methods ensures consistency with project patterns and includes all necessary boilerplate\\n";
258
+ let message = "Before writing new files, use `use-scaffold-method` if any of these match your needs:\n\n";
259
+ for (const method of data.methods) message += `- **${method.name}**: ${method.description || "No description available"}\n`;
260
+ if (data.nextCursor) message += `\n_More methods available (cursor: "${data.nextCursor}")._\n`;
98
261
  await executionLog.logExecution({
99
262
  filePath,
100
263
  operation: "list-scaffold-methods",
@@ -181,7 +344,7 @@ var UseScaffoldMethodHook = class {
181
344
  };
182
345
  }
183
346
  if (isScaffoldedFile) {
184
- const remainingFilesList = remainingFiles.map((f) => ` - ${f}`).join("\\n");
347
+ const remainingFilesList = remainingFiles.map((f) => ` - ${f}`).join("\n");
185
348
  return {
186
349
  decision: __agiflowai_hooks_adapter.DECISION_ALLOW,
187
350
  message: `
@@ -217,7 +380,8 @@ function extractScaffoldId(toolResult) {
217
380
  if (match) return match[1];
218
381
  }
219
382
  return null;
220
- } catch {
383
+ } catch (error) {
384
+ console.error("extractScaffoldId: failed to parse tool result:", error);
221
385
  return null;
222
386
  }
223
387
  }
@@ -225,28 +389,38 @@ function extractScaffoldId(toolResult) {
225
389
  * Helper function to get the last scaffold execution for a session
226
390
  */
227
391
  async function getLastScaffoldExecution(executionLog) {
228
- const entries = await executionLog.loadLog();
229
- for (let i = entries.length - 1; i >= 0; i--) {
230
- const entry = entries[i];
231
- if (entry.operation === "scaffold" && entry.scaffoldId && entry.generatedFiles && entry.generatedFiles.length > 0) return {
232
- scaffoldId: entry.scaffoldId,
233
- generatedFiles: entry.generatedFiles,
234
- featureName: entry.featureName
235
- };
392
+ try {
393
+ const entries = await executionLog.loadLog();
394
+ for (let i = entries.length - 1; i >= 0; i--) {
395
+ const entry = entries[i];
396
+ if (entry.operation === "scaffold" && entry.scaffoldId && entry.generatedFiles && entry.generatedFiles.length > 0) return {
397
+ scaffoldId: entry.scaffoldId,
398
+ generatedFiles: entry.generatedFiles,
399
+ featureName: entry.featureName
400
+ };
401
+ }
402
+ return null;
403
+ } catch (error) {
404
+ console.error("getLastScaffoldExecution: failed to load log:", error);
405
+ return null;
236
406
  }
237
- return null;
238
407
  }
239
408
  /**
240
409
  * Helper function to get list of edited scaffold files
241
410
  */
242
411
  async function getEditedScaffoldFiles(executionLog, scaffoldId) {
243
- const entries = await executionLog.loadLog();
244
- const editedFiles = [];
245
- for (const entry of entries) if (entry.operation === "scaffold-file-edit" && entry.filePath.startsWith(`scaffold-edit-${scaffoldId}-`)) {
246
- const filePath = entry.filePath.replace(`scaffold-edit-${scaffoldId}-`, "");
247
- editedFiles.push(filePath);
412
+ try {
413
+ const entries = await executionLog.loadLog();
414
+ const editedFiles = [];
415
+ for (const entry of entries) if (entry.operation === "scaffold-file-edit" && entry.filePath.startsWith(`scaffold-edit-${scaffoldId}-`)) {
416
+ const filePath = entry.filePath.replace(`scaffold-edit-${scaffoldId}-`, "");
417
+ editedFiles.push(filePath);
418
+ }
419
+ return editedFiles;
420
+ } catch (error) {
421
+ console.error("getEditedScaffoldFiles: failed to load log:", error);
422
+ return [];
248
423
  }
249
- return editedFiles;
250
424
  }
251
425
  /**
252
426
  * Process pending scaffold logs from temp file and copy to ExecutionLogService
@@ -255,27 +429,33 @@ async function getEditedScaffoldFiles(executionLog, scaffoldId) {
255
429
  async function processPendingScaffoldLogs(sessionId, scaffoldId) {
256
430
  const tempLogFile = node_path.default.join(node_os.default.tmpdir(), `scaffold-mcp-pending-${scaffoldId}.jsonl`);
257
431
  try {
258
- const lines = (await node_fs_promises.default.readFile(tempLogFile, "utf-8")).trim().split("\\n").filter(Boolean);
432
+ const lines = (await node_fs_promises.default.readFile(tempLogFile, "utf-8")).trim().split("\n").filter(Boolean);
259
433
  const executionLog = new __agiflowai_hooks_adapter.ExecutionLogService(sessionId);
260
434
  try {
261
435
  for (const line of lines) try {
262
- const entry = JSON.parse(line);
436
+ const parsed = JSON.parse(line);
437
+ if (!isPendingScaffoldLogEntry(parsed)) {
438
+ console.error("processPendingScaffoldLogs: skipping malformed entry:", line);
439
+ continue;
440
+ }
263
441
  await executionLog.logExecution({
264
- filePath: `scaffold-${entry.scaffoldId}`,
442
+ filePath: `scaffold-${parsed.scaffoldId}`,
265
443
  operation: "scaffold",
266
444
  decision: __agiflowai_hooks_adapter.DECISION_ALLOW,
267
- generatedFiles: entry.generatedFiles,
268
- scaffoldId: entry.scaffoldId,
269
- projectPath: entry.projectPath,
270
- featureName: entry.featureName
445
+ generatedFiles: parsed.generatedFiles,
446
+ scaffoldId: parsed.scaffoldId,
447
+ projectPath: parsed.projectPath,
448
+ featureName: parsed.featureName
271
449
  });
272
450
  } catch (parseError) {
273
- console.error("Failed to parse pending scaffold log entry:", parseError);
451
+ console.error("processPendingScaffoldLogs: failed to parse line:", parseError);
274
452
  }
275
453
  } finally {
276
454
  try {
277
455
  await node_fs_promises.default.unlink(tempLogFile);
278
- } catch {}
456
+ } catch (unlinkError) {
457
+ if (!(unlinkError instanceof Error && "code" in unlinkError && unlinkError.code === "ENOENT")) console.error("processPendingScaffoldLogs: failed to delete temp log file:", unlinkError);
458
+ }
279
459
  }
280
460
  } catch (error) {
281
461
  if (error instanceof Error && "code" in error && error.code !== "ENOENT") console.error("Error processing pending scaffold logs:", error);
@@ -283,4 +463,5 @@ async function processPendingScaffoldLogs(sessionId, scaffoldId) {
283
463
  }
284
464
 
285
465
  //#endregion
466
+ exports.PhantomCodeCheckHook = PhantomCodeCheckHook;
286
467
  exports.UseScaffoldMethodHook = UseScaffoldMethodHook;