diff --git a/src/parser.ts b/src/parser.ts index 845adfc..e701a6e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -60,18 +60,42 @@ function tokenizeCommand(input) { const ch = input[i]; if (quote) { + if (quote === "'") { + if (ch === '\\' && input[i + 1] === quote) { + current += quote; + i++; + continue; + } + if (ch === quote) { + quote = null; + continue; + } + current += ch; + continue; + } + + // Double-quoted mode: preserve unknown escapes (\n, \t, etc) while + // unescaping only shell-like quote/backslash escapes. if (ch === '\\') { const next = input[i + 1]; - if (next) { + if (next === '"' || next === '\\' || next === '$' || next === '`') { current += next; i++; continue; } + if (next === '\n') { + i++; + continue; + } + current += ch; + continue; } + if (ch === quote) { quote = null; continue; } + current += ch; continue; } diff --git a/test/parser.test.ts b/test/parser.test.ts index 2db7cbf..fda4412 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -18,3 +18,26 @@ test('parsePipeline keeps quoted pipes', () => { assert.equal(p.length, 2); assert.deepEqual(p[0].args._, ['echo', 'a|b']); }); + +test('parsePipeline preserves JSON escapes in double-quoted args', () => { + const p = parsePipeline('openclaw.invoke --tool llm-task --action json --args-json "{\\"prompt\\":\\"line1\\\\nline2\\",\\"schema\\":{\\"type\\":\\"object\\"}}"'); + assert.equal(p.length, 1); + const raw = p[0].args['args-json']; + const parsed = JSON.parse(raw); + assert.equal(parsed.prompt, 'line1\nline2'); + assert.equal(parsed.schema.type, 'object'); +}); + +test('parsePipeline keeps single-quoted args literal', () => { + const p = parsePipeline("openclaw.invoke --args-json '{\"x\":\"a\\\\nb\"}'"); + assert.equal(p.length, 1); + assert.equal(p[0].args['args-json'], '{"x":"a\\\\nb"}'); +}); + +test('parsePipeline preserves escaped apostrophes in single-quoted args', () => { + const p = parsePipeline("openclaw.invoke --args-json '{\"prompt\":\"don\\'t\"}'"); + assert.equal(p.length, 1); + const raw = p[0].args['args-json']; + const parsed = JSON.parse(raw); + assert.equal(parsed.prompt, "don't"); +});