diff --git a/releases/v1.5.7/Scarf-v1.5.7-ARM64.zip b/releases/v1.5.7/Scarf-v1.5.7-ARM64.zip new file mode 100644 index 0000000..746f357 Binary files /dev/null and b/releases/v1.5.7/Scarf-v1.5.7-ARM64.zip differ diff --git a/releases/v1.5.7/Scarf-v1.5.7-Universal.zip b/releases/v1.5.7/Scarf-v1.5.7-Universal.zip new file mode 100644 index 0000000..28df7cd Binary files /dev/null and b/releases/v1.5.7/Scarf-v1.5.7-Universal.zip differ diff --git a/scarf/scarf.xcodeproj/project.pbxproj b/scarf/scarf.xcodeproj/project.pbxproj index 5342181..d6eaf46 100644 --- a/scarf/scarf.xcodeproj/project.pbxproj +++ b/scarf/scarf.xcodeproj/project.pbxproj @@ -407,7 +407,7 @@ CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 3Q6X2L86C4; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -422,7 +422,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 1.5.6; + MARKETING_VERSION = 1.5.7; PRODUCT_BUNDLE_IDENTIFIER = com.scarf; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -444,7 +444,7 @@ CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 3Q6X2L86C4; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; @@ -459,7 +459,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 1.5.6; + MARKETING_VERSION = 1.5.7; PRODUCT_BUNDLE_IDENTIFIER = com.scarf; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; diff --git a/scarf/scarf/Core/Services/HermesFileService.swift b/scarf/scarf/Core/Services/HermesFileService.swift index baa1af2..3ec45c0 100644 --- a/scarf/scarf/Core/Services/HermesFileService.swift +++ b/scarf/scarf/Core/Services/HermesFileService.swift @@ -437,6 +437,20 @@ struct HermesFileService: Sendable { if blockStart < 0 { return MCPBlockLocation(prefix: lines, block: [], suffix: []) } + // Trim trailing blank lines and comments from the block — they belong + // to the file footer, not the mcp_servers section. Without this, when + // mcp_servers is the last top-level key, the block would extend to EOF + // and any inserted content (args, env, headers, tools) would land + // after the trailing comments. + while blockEnd > blockStart + 1 { + let line = lines[blockEnd - 1] + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty || trimmed.hasPrefix("#") { + blockEnd -= 1 + } else { + break + } + } return MCPBlockLocation( prefix: Array(lines[0..= 0 else { return false } + // Trim trailing blank lines and comments off the entry so inserts land + // immediately after the entry's last real key, not after intervening + // comments that conceptually belong to the next entry (or the file + // footer when this is the last entry in the block). + while entryEnd > entryStart + 1 { + let line = block[entryEnd - 1] + let trimmed = line.trimmingCharacters(in: .whitespaces) + if trimmed.isEmpty || trimmed.hasPrefix("#") { + entryEnd -= 1 + } else { + break + } + } + var entryLines = Array(block[entryStart.. String { if value.isEmpty { return "\"\"" } + // YAML 1.2 reserved indicators that change meaning at the start of a + // scalar: @ * & ? | > ! % , [ ] { } < ` ' " — plus space (would be + // trimmed) and dash (looks like a sequence). Anything starting with + // one of these must be quoted or YAML treats the value as an alias, + // tag, flow collection, etc., and parsing breaks. + let reservedFirstChars: Set = [ + "@", "*", "&", "?", "|", ">", "!", "%", ",", + "[", "]", "{", "}", "<", "`", "'", "\"" + ] + let firstCharNeedsQuoting = value.first.map { reservedFirstChars.contains($0) } ?? false let needsQuoting = value.contains(":") || value.contains("#") || value.contains("\"") || value.hasPrefix(" ") || value.hasSuffix(" ") || value.hasPrefix("-") || ["true", "false", "null", "yes", "no"].contains(value.lowercased()) + || firstCharNeedsQuoting if needsQuoting { let escaped = value.replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"")