clanka 0.2.10 → 0.2.12

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.
@@ -11,6 +11,7 @@ import type { AgentFinished, Output } from "./Agent.ts"
11
11
  import chalk from "chalk"
12
12
  import type * as Prompt from "effect/unstable/ai/Prompt"
13
13
  import * as Cause from "effect/Cause"
14
+ import { identity } from "effect/Function"
14
15
 
15
16
  /**
16
17
  * @since 1.0.0
@@ -24,70 +25,87 @@ export type OutputFormatter = <E, R>(
24
25
  * @since 1.0.0
25
26
  * @category Pretty
26
27
  */
27
- export const pretty: OutputFormatter = (stream) =>
28
- stream.pipe(
29
- Stream.map((output) => {
30
- let prefix = ""
31
- if (output._tag === "SubagentPart") {
32
- prefix = chalk.magenta(`Subagent #${output.id}:`) + " "
33
- output = output.part
34
- }
35
- switch (output._tag) {
36
- case "AgentStart": {
37
- return `${chalkAgentHeading(`${subagentIcon} Agent #${output.id} starting (${output.modelAndProvider})`)}\n\n${promptToString(output.prompt)}\n\n`
28
+ export const pretty = (options?: {
29
+ readonly outputTruncation?: number | boolean | undefined
30
+ }): OutputFormatter => {
31
+ const maxLines =
32
+ typeof options?.outputTruncation === "number"
33
+ ? options.outputTruncation
34
+ : 20
35
+ const truncate =
36
+ options?.outputTruncation === true ||
37
+ typeof options?.outputTruncation === "number"
38
+ ? (text: string): string => {
39
+ const lines = text.split("\n")
40
+ if (lines.length > maxLines) {
41
+ return (
42
+ lines.slice(0, maxLines).join("\n") +
43
+ `\n... (truncated, total ${lines.length} lines)`
44
+ )
45
+ }
46
+ return text
38
47
  }
39
- case "SubagentStart": {
40
- return `${chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} starting (${output.modelAndProvider})`)}
48
+ : identity
49
+ return (stream) =>
50
+ stream.pipe(
51
+ Stream.map((output) => {
52
+ let prefix = ""
53
+ if (output._tag === "SubagentPart") {
54
+ prefix = chalk.magenta(`Subagent #${output.id}:`) + " "
55
+ output = output.part
56
+ }
57
+ switch (output._tag) {
58
+ case "AgentStart": {
59
+ return `${chalkAgentHeading(`${subagentIcon} Agent #${output.id} starting (${output.modelAndProvider})`)}\n\n${promptToString(output.prompt)}\n\n`
60
+ }
61
+ case "SubagentStart": {
62
+ return `${chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} starting (${output.modelAndProvider})`)}
41
63
 
42
64
  ${chalk.dim(output.prompt)}\n\n`
43
- }
44
- case "SubagentComplete": {
45
- return `${chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} complete`)}
65
+ }
66
+ case "SubagentComplete": {
67
+ return `${chalkSubagentHeading(`${subagentIcon} Subagent #${output.id} complete`)}
46
68
 
47
69
  ${output.summary}\n\n`
70
+ }
71
+ case "ReasoningStart": {
72
+ return (
73
+ prefix + chalkReasoningHeading(`${thinkingIcon} Thinking:`) + " "
74
+ )
75
+ }
76
+ case "ReasoningDelta": {
77
+ return output.delta
78
+ }
79
+ case "ReasoningEnd": {
80
+ return "\n\n"
81
+ }
82
+ case "ScriptStart": {
83
+ return `${prefix}${chalkScriptHeading(`${scriptIcon} Executing script`)}\n\n`
84
+ }
85
+ case "ScriptDelta": {
86
+ return chalk.dim(output.delta)
87
+ }
88
+ case "ScriptEnd": {
89
+ return "\n\n"
90
+ }
91
+ case "ScriptOutput": {
92
+ return `${prefix}${chalkScriptHeading(`${scriptIcon} Script output`)}\n\n${chalk.dim(truncate(output.output))}\n\n`
93
+ }
94
+ case "ErrorRetry": {
95
+ return `${prefix}${chalk.red(`Error: ${output.error.reason._tag}. Retrying...`)}\n\n${chalk.dim(Cause.pretty(Cause.fail(output.error)))}\n\n`
96
+ }
97
+ case "Usage": {
98
+ return `${prefix}${chalkInfoHeading(`${infoIcon} Usage:`)} ${numberFormat.format(output.contextTokens)} context / ${numberFormat.format(output.inputTokens)} input / ${numberFormat.format(output.outputTokens)} output\n\n`
99
+ }
48
100
  }
49
- case "ReasoningStart": {
50
- return (
51
- prefix + chalkReasoningHeading(`${thinkingIcon} Thinking:`) + " "
52
- )
53
- }
54
- case "ReasoningDelta": {
55
- return output.delta
56
- }
57
- case "ReasoningEnd": {
58
- return "\n\n"
59
- }
60
- case "ScriptStart": {
61
- return `${prefix}${chalkScriptHeading(`${scriptIcon} Executing script`)}\n\n`
62
- }
63
- case "ScriptDelta": {
64
- return chalk.dim(output.delta)
65
- }
66
- case "ScriptEnd": {
67
- return "\n\n"
68
- }
69
- case "ScriptOutput": {
70
- const lines = output.output.split("\n")
71
- const truncated =
72
- lines.length > 20
73
- ? lines.slice(0, 20).join("\n") + "\n... (truncated)"
74
- : output.output
75
- return `${prefix}${chalkScriptHeading(`${scriptIcon} Script output`)}\n\n${chalk.dim(truncated)}\n\n`
76
- }
77
- case "ErrorRetry": {
78
- return `${prefix}${chalk.red(`Error: ${output.error.reason._tag}. Retrying...`)}\n\n${chalk.dim(Cause.pretty(Cause.fail(output.error)))}\n\n`
79
- }
80
- case "Usage": {
81
- return `${prefix}${chalkInfoHeading(`${infoIcon} Usage:`)} ${numberFormat.format(output.contextTokens)} context / ${numberFormat.format(output.inputTokens)} input / ${numberFormat.format(output.outputTokens)} output\n\n`
82
- }
83
- }
84
- }),
85
- Stream.catchTag("AgentFinished", (finished) =>
86
- Stream.succeed(
87
- `\n${chalk.bold.green(`${doneIcon} Task complete:`)}\n\n${(finished as AgentFinished).summary}`,
101
+ }),
102
+ Stream.catchTag("AgentFinished", (finished) =>
103
+ Stream.succeed(
104
+ `\n${chalk.bold.green(`${doneIcon} Task complete:`)}\n\n${(finished as AgentFinished).summary}`,
105
+ ),
88
106
  ),
89
- ),
90
- )
107
+ )
108
+ }
91
109
 
92
110
  const promptToString = (prompt: Prompt.Prompt): string => {
93
111
  let textParts: Array<string> = []
@@ -0,0 +1,171 @@
1
+ import { assert, describe, it } from "@effect/vitest"
2
+ import { preprocessScript } from "./ScriptPreprocessing.ts"
3
+ import { readFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+
6
+ const tick = "`"
7
+ const escaped = "\\`"
8
+ const escapedInterpolation = "\\${"
9
+ const wrapTemplate = (value: string): string => `${tick}${value}${tick}`
10
+
11
+ describe("preprocessScript", () => {
12
+ it("escapes internal backticks in applyPatch templates", () => {
13
+ const input = [
14
+ "await applyPatch(`",
15
+ "*** Begin Patch",
16
+ "*** Update File: src/example.ts",
17
+ "@@",
18
+ "-const oldValue = `old`",
19
+ "+const newValue = `new`",
20
+ "*** End Patch",
21
+ "`)",
22
+ ].join("\n")
23
+
24
+ const output = preprocessScript(input)
25
+
26
+ assert.strictEqual(
27
+ output.includes(`-const oldValue = ${escaped}old${escaped}`),
28
+ true,
29
+ )
30
+ assert.strictEqual(
31
+ output.includes(`+const newValue = ${escaped}new${escaped}`),
32
+ true,
33
+ )
34
+ })
35
+
36
+ it("escapes internal interpolations in applyPatch templates", () => {
37
+ const input = [
38
+ "await applyPatch(`",
39
+ "*** Begin Patch",
40
+ "*** Update File: src/example.ts",
41
+ "@@",
42
+ "+const value = ${nextValue}",
43
+ "*** End Patch",
44
+ "`)",
45
+ ].join("\n")
46
+
47
+ const output = preprocessScript(input)
48
+
49
+ assert.strictEqual(
50
+ output.includes(`+const value = ${escapedInterpolation}nextValue}`),
51
+ true,
52
+ )
53
+ })
54
+
55
+ it("escapes internal backticks in writeFile content templates", () => {
56
+ const input = [
57
+ "await writeFile({",
58
+ ' path: "src/example.ts",',
59
+ " content: `const value = `next``,",
60
+ "})",
61
+ ].join("\n")
62
+
63
+ const output = preprocessScript(input)
64
+
65
+ assert.strictEqual(
66
+ output.includes(
67
+ `content: ${wrapTemplate(`const value = ${escaped}next${escaped}`)},`,
68
+ ),
69
+ true,
70
+ )
71
+ })
72
+
73
+ it("escapes internal backticks in taskComplete templates", () => {
74
+ const input = "await taskComplete(`Implemented `TypeBuilder` updates.`)"
75
+
76
+ const output = preprocessScript(input)
77
+
78
+ assert.strictEqual(
79
+ output,
80
+ `await taskComplete(${wrapTemplate(`Implemented ${escaped}TypeBuilder${escaped} updates.`)})`,
81
+ )
82
+ })
83
+
84
+ it("does not change scripts when target templates are already escaped", () => {
85
+ const input = [
86
+ `await applyPatch(${wrapTemplate(`const value = ${escaped}safe${escaped}`)})`,
87
+ `await applyPatch(${wrapTemplate(`const value = ${escapedInterpolation}safe}`)})`,
88
+ `await writeFile({ path: "src/example.ts", content: ${wrapTemplate(`already ${escaped}safe${escaped}`)} })`,
89
+ `await taskComplete(${wrapTemplate(`All done with ${escaped}safe${escaped} backticks.`)})`,
90
+ ].join("\n")
91
+
92
+ assert.strictEqual(preprocessScript(input), input)
93
+ })
94
+
95
+ it("does not modify non-target function calls", () => {
96
+ const input = "await otherTool(`Keep `this` untouched.`)"
97
+
98
+ assert.strictEqual(preprocessScript(input), input)
99
+ })
100
+
101
+ it("escapes internal backticks in applyPatch templates assigned to variables", () => {
102
+ const input = [
103
+ "const patch = `*** Begin Patch",
104
+ "*** Update File: src/example.ts",
105
+ "@@",
106
+ "-const oldValue = `old`",
107
+ "+const newValue = `new`",
108
+ "*** End Patch`;",
109
+ "await applyPatch(patch)",
110
+ ].join("\n")
111
+
112
+ const output = preprocessScript(input)
113
+
114
+ assert.strictEqual(
115
+ output.includes(`-const oldValue = ${escaped}old${escaped}`),
116
+ true,
117
+ )
118
+ assert.strictEqual(
119
+ output.includes(`+const newValue = ${escaped}new${escaped}`),
120
+ true,
121
+ )
122
+ })
123
+
124
+ it("escapes internal backticks in taskComplete templates assigned to variables", () => {
125
+ const input = [
126
+ "const summary = `Implemented `TypeBuilder` updates.`;",
127
+ "await taskComplete(summary)",
128
+ ].join("\n")
129
+
130
+ const output = preprocessScript(input)
131
+
132
+ assert.strictEqual(
133
+ output,
134
+ [
135
+ `const summary = ${wrapTemplate(`Implemented ${escaped}TypeBuilder${escaped} updates.`)};`,
136
+ "await taskComplete(summary)",
137
+ ].join("\n"),
138
+ )
139
+ })
140
+
141
+ it("escapes internal backticks in writeFile content assigned to variables", () => {
142
+ const input = [
143
+ "const body = `const value = `next``;",
144
+ "await writeFile({",
145
+ ' path: "src/example.ts",',
146
+ " content: body,",
147
+ "})",
148
+ ].join("\n")
149
+
150
+ const output = preprocessScript(input)
151
+
152
+ assert.strictEqual(
153
+ output.includes(
154
+ `const body = ${wrapTemplate(`const value = ${escaped}next${escaped}`)};`,
155
+ ),
156
+ true,
157
+ )
158
+ })
159
+
160
+ it.each(["patch", "patch2"])("fixes broken %s", (fixture) => {
161
+ const content = readFileSync(
162
+ join(__dirname, "fixtures", `${fixture}-broken.txt`),
163
+ "utf-8",
164
+ )
165
+ const fixed = readFileSync(
166
+ join(__dirname, "fixtures", `${fixture}-fixed.txt`),
167
+ "utf-8",
168
+ )
169
+ assert.equal(preprocessScript(content), fixed)
170
+ })
171
+ })