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