feat(config): add escapeJson parameter to substitute for raw content handling

Add optional `escapeJson` parameter to `ConfigPaths.substitute` to control
whether file content should be JSON-escaped. When `false`, multiline content
and special characters are preserved as-is, which is required for agent
markdown prompts that need raw file content rather than escaped strings.

Update agent config processing to pass `escapeJson: false` when substituting
file content into markdown prompts, ensuring proper handling of multiline
files without unwanted escape sequences.

Add comprehensive test coverage for the new parameter and existing
substitution functionality.
This commit is contained in:
Maksym Mospanenko
2026-05-12 23:44:06 -03:00
committed by Catriel Müller
parent 80edeee58f
commit 3ca9941bbf
4 changed files with 131 additions and 1 deletions
+1
View File
@@ -169,6 +169,7 @@ export async function load(dir: string, warnings?: Warning[]) {
dir: path.dirname(item),
source: item,
missing: "empty",
escapeJson: false,
})
const config = {
+3 -1
View File
@@ -19,6 +19,7 @@ type ParseSource =
type SubstituteInput = ParseSource & {
text: string
missing?: "error" | "empty"
escapeJson?: boolean
}
function source(input: ParseSource) {
@@ -32,6 +33,7 @@ function dir(input: ParseSource) {
/** Apply {env:VAR} and {file:path} substitutions to config text. */
export async function substitute(input: SubstituteInput) {
const missing = input.missing ?? "error"
const escape = input.escapeJson ?? true
let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
})
@@ -81,7 +83,7 @@ export async function substitute(input: SubstituteInput) {
})
).trim()
out += JSON.stringify(fileContent).slice(1, -1)
out += escape ? JSON.stringify(fileContent).slice(1, -1) : fileContent
cursor = index + token.length
}
@@ -85,4 +85,32 @@ describe("agent {file:...} syntax in markdown body", () => {
},
})
})
test("preserves multiline content without escaping", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const multilineContent = ["Line 1", "Line 2", "Line 3"].join("\n")
await Bun.write(path.join(dir, "multiline.md"), multilineContent)
const agentsDir = path.join(dir, ".kilo", "agents")
await fs.mkdir(agentsDir, { recursive: true })
await Bun.write(
path.join(agentsDir, "test.md"),
["---", "description: Test", "---", "Base", "", "{file:../../multiline.md}", ""].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("test")
expect(agent).toBeDefined()
expect(agent?.prompt).toContain("Line 1\nLine 2\nLine 3")
expect(agent?.prompt).not.toContain("Line 1\\nLine 2\\nLine 3")
},
})
})
})
@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test"
import path from "node:path"
import fs from "node:fs/promises"
import { tmpdir } from "../fixture/fixture"
import { ConfigPaths } from "../../src/config/paths"
describe("ConfigPaths.substitute", () => {
describe("escapeJson parameter", () => {
test("escapes newlines when escapeJson is true (default)", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.txt"), "Line 1\nLine 2\nLine 3")
},
})
const result = await ConfigPaths.substitute(
"{file:./test.txt}",
{ dir: tmp.path, source: "config.json" },
"error",
true,
)
expect(result).toBe("Line 1\\nLine 2\\nLine 3")
})
test("preserves newlines when escapeJson is false", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.txt"), "Line 1\nLine 2\nLine 3")
},
})
const result = await ConfigPaths.substitute(
"{file:./test.txt}",
{ dir: tmp.path, source: "agent.md" },
"empty",
false,
)
expect(result).toBe("Line 1\nLine 2\nLine 3")
})
test("escapes quotes when escapeJson is true", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.txt"), 'Text with "quotes"')
},
})
const result = await ConfigPaths.substitute(
"{file:./test.txt}",
{ dir: tmp.path, source: "config.json" },
"error",
true,
)
expect(result).toBe('Text with \\"quotes\\"')
})
test("preserves quotes when escapeJson is false", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
await Bun.write(path.join(dir, "test.txt"), 'Text with "quotes"')
},
})
const result = await ConfigPaths.substitute(
"{file:./test.txt}",
{ dir: tmp.path, source: "agent.md" },
"empty",
false,
)
expect(result).toBe('Text with "quotes"')
})
})
describe("existing functionality", () => {
test("substitutes {env:VAR} with environment variable", async () => {
process.env.TEST_VAR = "test_value"
const result = await ConfigPaths.substitute("Value: {env:TEST_VAR}", { dir: "/tmp", source: "test.json" })
expect(result).toBe("Value: test_value")
delete process.env.TEST_VAR
})
test("replaces missing {env:VAR} with empty string", async () => {
const result = await ConfigPaths.substitute("Value: {env:NONEXISTENT_VAR}", { dir: "/tmp", source: "test.json" })
expect(result).toBe("Value: ")
})
})
})