@dypai-ai/mcp 1.6.20 → 1.7.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.
@@ -55,6 +55,8 @@ const IGNORE_DIRS_ANYWHERE = new Set([
55
55
  // under `dypai/` are filtered by path below.
56
56
  const IGNORE_DIRS_AT_ROOT = new Set([])
57
57
 
58
+ const IGNORE_FILES = [".dypaiignore", ".gitignore"]
59
+
58
60
  // ─── Accepted file extensions ───────────────────────────────────────────────
59
61
 
60
62
  const CODE_EXTS = new Set([
@@ -116,6 +118,87 @@ function sha256(buf) {
116
118
  return createHash("sha256").update(buf).digest("hex")
117
119
  }
118
120
 
121
+ function escapeRegExp(value) {
122
+ return String(value).replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
123
+ }
124
+
125
+ function globSegmentToRegExp(pattern) {
126
+ const body = String(pattern)
127
+ .split("*")
128
+ .map((part) => escapeRegExp(part))
129
+ .join(".*")
130
+ return new RegExp(`^${body}$`)
131
+ }
132
+
133
+ function readIgnoreRules(sourceDirectory) {
134
+ const rules = []
135
+ for (const filename of IGNORE_FILES) {
136
+ const filePath = join(sourceDirectory, filename)
137
+ if (!existsSync(filePath)) continue
138
+ let lines = []
139
+ try {
140
+ lines = readFileSync(filePath, "utf-8").split(/\r?\n/)
141
+ } catch {
142
+ continue
143
+ }
144
+ for (const rawLine of lines) {
145
+ let pattern = rawLine.trim()
146
+ if (!pattern || pattern.startsWith("#")) continue
147
+ if (pattern.startsWith("!")) continue
148
+ pattern = pattern.replace(/\\/g, "/")
149
+
150
+ const directoryOnly = pattern.endsWith("/")
151
+ if (directoryOnly) pattern = pattern.replace(/\/+$/, "")
152
+ if (!pattern) continue
153
+
154
+ const anchored = pattern.startsWith("/")
155
+ if (anchored) pattern = pattern.replace(/^\/+/, "")
156
+ if (!pattern) continue
157
+
158
+ rules.push({
159
+ source: filename,
160
+ pattern: rawLine.trim(),
161
+ value: pattern,
162
+ anchored,
163
+ directoryOnly,
164
+ hasSlash: pattern.includes("/"),
165
+ re: globSegmentToRegExp(pattern),
166
+ })
167
+ }
168
+ }
169
+ return rules
170
+ }
171
+
172
+ function pathMatchesIgnoreRule(path, isDirectory, rule) {
173
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "")
174
+ if (!normalized) return false
175
+ if (rule.directoryOnly && !isDirectory) return false
176
+
177
+ if (!rule.hasSlash) {
178
+ const segments = normalized.split("/")
179
+ if (rule.directoryOnly) return segments.some((segment) => rule.re.test(segment))
180
+ return rule.re.test(segments[segments.length - 1] || "")
181
+ }
182
+
183
+ const value = rule.value.replace(/\/+$/, "")
184
+ const re = globSegmentToRegExp(value)
185
+ if (rule.anchored) {
186
+ return normalized === value || normalized.startsWith(`${value}/`) || re.test(normalized)
187
+ }
188
+
189
+ return (
190
+ normalized === value
191
+ || normalized.endsWith(`/${value}`)
192
+ || normalized.startsWith(`${value}/`)
193
+ || normalized.includes(`/${value}/`)
194
+ || re.test(normalized)
195
+ )
196
+ }
197
+
198
+ function ignoredByRule(path, isDirectory, rules) {
199
+ return rules.find((rule) => pathMatchesIgnoreRule(path, isDirectory, rule)) || null
200
+ }
201
+
119
202
  function isAllowedRootDypaiSourcePath(path) {
120
203
  if (!path.startsWith("dypai/")) return true
121
204
  if (
@@ -213,6 +296,18 @@ export function updateMediaManifest(sourceDirectory, localPath, entry) {
213
296
  writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n")
214
297
  }
215
298
 
299
+ /** Merge many manifest entries in one read/write (batch uploads). */
300
+ export function writeMediaManifestBatch(sourceDirectory, entriesByLocalPath) {
301
+ const path = mediaManifestPath(sourceDirectory)
302
+ const dir = dirname(path)
303
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
304
+ const manifest = readMediaManifest(sourceDirectory)
305
+ for (const [localPath, entry] of Object.entries(entriesByLocalPath)) {
306
+ manifest[localPath] = entry
307
+ }
308
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n")
309
+ }
310
+
216
311
  export function removeFromMediaManifest(sourceDirectory, localPath) {
217
312
  const path = mediaManifestPath(sourceDirectory)
218
313
  if (!existsSync(path)) return
@@ -246,9 +341,11 @@ export function collectSource(dir) {
246
341
  const skipped = []
247
342
  const textByPath = new Map()
248
343
  let total = 0
344
+ let bundleLimitExceeded = false
345
+ const ignoreRules = readIgnoreRules(dir)
249
346
 
250
347
  function walk(d, rel) {
251
- if (total > MAX_SOURCE_SIZE) return
348
+ if (bundleLimitExceeded) return
252
349
  let entries
253
350
  try { entries = readdirSync(d) } catch { return }
254
351
  for (const entry of entries) {
@@ -267,6 +364,15 @@ export function collectSource(dir) {
267
364
  if (stat.isDirectory()) {
268
365
  if (entry.startsWith(".")) continue
269
366
  const nextRel = rel ? `${rel}/${entry}` : entry
367
+ const ignored = ignoredByRule(nextRel, true, ignoreRules)
368
+ if (ignored) {
369
+ skipped.push({
370
+ local_path: `${nextRel}/`,
371
+ reason_code: "ignored_by_ignore_file",
372
+ reason_human: `Ignored by ${ignored.source}: ${ignored.pattern}`,
373
+ })
374
+ continue
375
+ }
270
376
  if (isSkippedRootDypaiDirectory(nextRel)) continue
271
377
  walk(full, nextRel)
272
378
  continue
@@ -276,6 +382,15 @@ export function collectSource(dir) {
276
382
  if (BLOCKED.has(entry)) continue
277
383
 
278
384
  const path = rel ? `${rel}/${entry}` : entry
385
+ const ignored = ignoredByRule(path, false, ignoreRules)
386
+ if (ignored) {
387
+ skipped.push({
388
+ local_path: path,
389
+ reason_code: "ignored_by_ignore_file",
390
+ reason_human: `Ignored by ${ignored.source}: ${ignored.pattern}`,
391
+ })
392
+ continue
393
+ }
279
394
  if (!isAllowedRootDypaiSourcePath(path)) continue
280
395
  try {
281
396
  const stat = statSync(full)
@@ -296,10 +411,11 @@ export function collectSource(dir) {
296
411
 
297
412
  const content = readFileSync(full)
298
413
  if (total + content.length > MAX_SOURCE_SIZE) {
414
+ bundleLimitExceeded = true
299
415
  skipped.push({
300
416
  local_path: path, ext, size_bytes: stat.size, size_mb: formatMb(stat.size),
301
417
  reason_code: "bundle_size_limit_reached",
302
- reason_human: `Total upload size limit (${formatMb(MAX_SOURCE_SIZE)} MB) reached.`,
418
+ reason_human: `Total upload size would exceed ${formatMb(MAX_SOURCE_SIZE)} MB. Add local-only folders to .dypaiignore/.gitignore or upload large assets with manage_storage.`,
303
419
  })
304
420
  return
305
421
  }
@@ -316,7 +432,7 @@ export function collectSource(dir) {
316
432
  }
317
433
 
318
434
  walk(dir, "")
319
- return { allFiles, total, skipped, textByPath }
435
+ return { allFiles, total, skipped, textByPath, bundleLimitExceeded }
320
436
  }
321
437
 
322
438
  // ─── Reference grep for assets_requiring_action ─────────────────────────────
@@ -476,12 +592,34 @@ export async function deployFromSource({
476
592
  _lastSourceDirectory = resolve(sourceDirectory)
477
593
 
478
594
  const framework = detectFramework(sourceDirectory)
479
- const { allFiles, total, skipped, textByPath } = collectSource(sourceDirectory)
595
+ const { allFiles, total, skipped, textByPath, bundleLimitExceeded } = collectSource(sourceDirectory)
480
596
 
481
597
  if (!allFiles.length) {
482
598
  return { error: "No source files found." }
483
599
  }
484
600
 
601
+ if (bundleLimitExceeded) {
602
+ const blockers = skipped
603
+ .filter((item) => item.reason_code === "bundle_size_limit_reached")
604
+ .slice(0, 10)
605
+ .map((item) => ({
606
+ local_path: item.local_path,
607
+ size_mb: item.size_mb,
608
+ reason: item.reason_human,
609
+ }))
610
+ return {
611
+ success: false,
612
+ error_code: "frontend_source_too_large",
613
+ error: `Frontend source exceeds the deploy bundle limit (${formatMb(MAX_SOURCE_SIZE)} MB). No deploy was sent.`,
614
+ bytes_collected_mb: formatMb(total),
615
+ files_collected: allFiles.length,
616
+ blockers,
617
+ advice:
618
+ "If these files are local-only, add them to .dypaiignore (for example: imagenes/). " +
619
+ "If the app needs them at runtime, upload them with manage_storage(upload_file/upload_files) and reference the returned public URLs.",
620
+ }
621
+ }
622
+
485
623
  // ── Skip-if-identical check ─────────────────────────────────────────────
486
624
  // One hash represents the whole project state. Same hash → nothing to do.
487
625
  // Different hash (or no previous hash) → full deploy.
@@ -4,6 +4,7 @@ import YAML from "yaml"
4
4
  import { deployFromSource } from "./deploy.js"
5
5
  import { proxyToolCall } from "./proxy.js"
6
6
  import { resolveOutDir } from "./sync/path-resolver.js"
7
+ import { refreshLocalStateSnapshot } from "./sync/planner.js"
7
8
 
8
9
  function resolveWorkspace({ workspace_root, sourceDirectory, root_dir } = {}) {
9
10
  if (workspace_root) {
@@ -172,6 +173,14 @@ export const dypaiDeployProductionTool = {
172
173
  live_changed: false,
173
174
  }
174
175
  }
176
+ try {
177
+ await refreshLocalStateSnapshot({ projectId: targetProjectId, rootDir })
178
+ } catch (refreshError) {
179
+ backend = {
180
+ ...backend,
181
+ state_baseline_refresh_warning: refreshError.message,
182
+ }
183
+ }
175
184
  }
176
185
  }
177
186