@jackchen_me/open-multi-agent 0.1.0

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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +280 -0
  3. package/dist/agent/agent.d.ts +121 -0
  4. package/dist/agent/agent.d.ts.map +1 -0
  5. package/dist/agent/agent.js +294 -0
  6. package/dist/agent/agent.js.map +1 -0
  7. package/dist/agent/pool.d.ts +128 -0
  8. package/dist/agent/pool.d.ts.map +1 -0
  9. package/dist/agent/pool.js +236 -0
  10. package/dist/agent/pool.js.map +1 -0
  11. package/dist/agent/runner.d.ts +120 -0
  12. package/dist/agent/runner.d.ts.map +1 -0
  13. package/dist/agent/runner.js +274 -0
  14. package/dist/agent/runner.js.map +1 -0
  15. package/dist/index.d.ts +73 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +87 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/llm/adapter.d.ts +38 -0
  20. package/dist/llm/adapter.d.ts.map +1 -0
  21. package/dist/llm/adapter.js +46 -0
  22. package/dist/llm/adapter.js.map +1 -0
  23. package/dist/llm/anthropic.d.ts +56 -0
  24. package/dist/llm/anthropic.d.ts.map +1 -0
  25. package/dist/llm/anthropic.js +307 -0
  26. package/dist/llm/anthropic.js.map +1 -0
  27. package/dist/llm/openai.d.ts +62 -0
  28. package/dist/llm/openai.d.ts.map +1 -0
  29. package/dist/llm/openai.js +424 -0
  30. package/dist/llm/openai.js.map +1 -0
  31. package/dist/memory/shared.d.ts +86 -0
  32. package/dist/memory/shared.d.ts.map +1 -0
  33. package/dist/memory/shared.js +155 -0
  34. package/dist/memory/shared.js.map +1 -0
  35. package/dist/memory/store.d.ts +64 -0
  36. package/dist/memory/store.d.ts.map +1 -0
  37. package/dist/memory/store.js +103 -0
  38. package/dist/memory/store.js.map +1 -0
  39. package/dist/orchestrator/orchestrator.d.ts +173 -0
  40. package/dist/orchestrator/orchestrator.d.ts.map +1 -0
  41. package/dist/orchestrator/orchestrator.js +698 -0
  42. package/dist/orchestrator/orchestrator.js.map +1 -0
  43. package/dist/orchestrator/scheduler.d.ts +112 -0
  44. package/dist/orchestrator/scheduler.d.ts.map +1 -0
  45. package/dist/orchestrator/scheduler.js +282 -0
  46. package/dist/orchestrator/scheduler.js.map +1 -0
  47. package/dist/task/queue.d.ts +160 -0
  48. package/dist/task/queue.d.ts.map +1 -0
  49. package/dist/task/queue.js +337 -0
  50. package/dist/task/queue.js.map +1 -0
  51. package/dist/task/task.d.ts +86 -0
  52. package/dist/task/task.d.ts.map +1 -0
  53. package/dist/task/task.js +201 -0
  54. package/dist/task/task.js.map +1 -0
  55. package/dist/team/messaging.d.ts +106 -0
  56. package/dist/team/messaging.d.ts.map +1 -0
  57. package/dist/team/messaging.js +182 -0
  58. package/dist/team/messaging.js.map +1 -0
  59. package/dist/team/team.d.ts +141 -0
  60. package/dist/team/team.d.ts.map +1 -0
  61. package/dist/team/team.js +282 -0
  62. package/dist/team/team.js.map +1 -0
  63. package/dist/tool/built-in/bash.d.ts +12 -0
  64. package/dist/tool/built-in/bash.d.ts.map +1 -0
  65. package/dist/tool/built-in/bash.js +133 -0
  66. package/dist/tool/built-in/bash.js.map +1 -0
  67. package/dist/tool/built-in/file-edit.d.ts +14 -0
  68. package/dist/tool/built-in/file-edit.d.ts.map +1 -0
  69. package/dist/tool/built-in/file-edit.js +130 -0
  70. package/dist/tool/built-in/file-edit.js.map +1 -0
  71. package/dist/tool/built-in/file-read.d.ts +12 -0
  72. package/dist/tool/built-in/file-read.d.ts.map +1 -0
  73. package/dist/tool/built-in/file-read.js +82 -0
  74. package/dist/tool/built-in/file-read.js.map +1 -0
  75. package/dist/tool/built-in/file-write.d.ts +11 -0
  76. package/dist/tool/built-in/file-write.d.ts.map +1 -0
  77. package/dist/tool/built-in/file-write.js +70 -0
  78. package/dist/tool/built-in/file-write.js.map +1 -0
  79. package/dist/tool/built-in/grep.d.ts +15 -0
  80. package/dist/tool/built-in/grep.d.ts.map +1 -0
  81. package/dist/tool/built-in/grep.js +287 -0
  82. package/dist/tool/built-in/grep.js.map +1 -0
  83. package/dist/tool/built-in/index.d.ts +36 -0
  84. package/dist/tool/built-in/index.d.ts.map +1 -0
  85. package/dist/tool/built-in/index.js +45 -0
  86. package/dist/tool/built-in/index.js.map +1 -0
  87. package/dist/tool/executor.d.ts +71 -0
  88. package/dist/tool/executor.d.ts.map +1 -0
  89. package/dist/tool/executor.js +116 -0
  90. package/dist/tool/executor.js.map +1 -0
  91. package/dist/tool/framework.d.ts +143 -0
  92. package/dist/tool/framework.d.ts.map +1 -0
  93. package/dist/tool/framework.js +371 -0
  94. package/dist/tool/framework.js.map +1 -0
  95. package/dist/types.d.ts +285 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +8 -0
  98. package/dist/types.js.map +1 -0
  99. package/dist/utils/semaphore.d.ts +47 -0
  100. package/dist/utils/semaphore.d.ts.map +1 -0
  101. package/dist/utils/semaphore.js +85 -0
  102. package/dist/utils/semaphore.js.map +1 -0
  103. package/examples/01-single-agent.ts +131 -0
  104. package/examples/02-team-collaboration.ts +167 -0
  105. package/examples/03-task-pipeline.ts +201 -0
  106. package/examples/04-multi-model-team.ts +261 -0
  107. package/package.json +49 -0
  108. package/src/agent/agent.ts +364 -0
  109. package/src/agent/pool.ts +278 -0
  110. package/src/agent/runner.ts +413 -0
  111. package/src/index.ts +166 -0
  112. package/src/llm/adapter.ts +74 -0
  113. package/src/llm/anthropic.ts +388 -0
  114. package/src/llm/openai.ts +522 -0
  115. package/src/memory/shared.ts +181 -0
  116. package/src/memory/store.ts +124 -0
  117. package/src/orchestrator/orchestrator.ts +851 -0
  118. package/src/orchestrator/scheduler.ts +352 -0
  119. package/src/task/queue.ts +394 -0
  120. package/src/task/task.ts +232 -0
  121. package/src/team/messaging.ts +230 -0
  122. package/src/team/team.ts +334 -0
  123. package/src/tool/built-in/bash.ts +187 -0
  124. package/src/tool/built-in/file-edit.ts +154 -0
  125. package/src/tool/built-in/file-read.ts +105 -0
  126. package/src/tool/built-in/file-write.ts +81 -0
  127. package/src/tool/built-in/grep.ts +362 -0
  128. package/src/tool/built-in/index.ts +50 -0
  129. package/src/tool/executor.ts +178 -0
  130. package/src/tool/framework.ts +557 -0
  131. package/src/types.ts +362 -0
  132. package/src/utils/semaphore.ts +89 -0
  133. package/tsconfig.json +25 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Built-in bash tool.
3
+ *
4
+ * Executes a shell command and returns its stdout + stderr. Supports an
5
+ * optional timeout and a custom working directory.
6
+ */
7
+
8
+ import { spawn } from 'child_process'
9
+ import { z } from 'zod'
10
+ import { defineTool } from '../framework.js'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Constants
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const DEFAULT_TIMEOUT_MS = 30_000
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Tool definition
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export const bashTool = defineTool({
23
+ name: 'bash',
24
+ description:
25
+ 'Execute a bash command and return its stdout and stderr. ' +
26
+ 'Use this for file system operations, running scripts, installing packages, ' +
27
+ 'and any task that requires shell access. ' +
28
+ 'The command runs in a non-interactive shell (bash -c). ' +
29
+ 'Long-running commands should use the timeout parameter.',
30
+
31
+ inputSchema: z.object({
32
+ command: z.string().describe('The bash command to execute.'),
33
+ timeout: z
34
+ .number()
35
+ .optional()
36
+ .describe(
37
+ `Timeout in milliseconds before the command is forcibly killed. ` +
38
+ `Defaults to ${DEFAULT_TIMEOUT_MS} ms.`,
39
+ ),
40
+ cwd: z
41
+ .string()
42
+ .optional()
43
+ .describe('Working directory in which to run the command.'),
44
+ }),
45
+
46
+ execute: async (input, context) => {
47
+ const timeoutMs = input.timeout ?? DEFAULT_TIMEOUT_MS
48
+
49
+ const { stdout, stderr, exitCode } = await runCommand(
50
+ input.command,
51
+ { cwd: input.cwd, timeoutMs },
52
+ context.abortSignal,
53
+ )
54
+
55
+ const combined = buildOutput(stdout, stderr, exitCode)
56
+ const isError = exitCode !== 0
57
+
58
+ return {
59
+ data: combined,
60
+ isError,
61
+ }
62
+ },
63
+ })
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Internal helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ interface RunResult {
70
+ stdout: string
71
+ stderr: string
72
+ exitCode: number
73
+ }
74
+
75
+ interface RunOptions {
76
+ cwd: string | undefined
77
+ timeoutMs: number
78
+ }
79
+
80
+ /**
81
+ * Spawn a bash subprocess, capture its output, and resolve when it exits or
82
+ * the abort signal fires.
83
+ */
84
+ function runCommand(
85
+ command: string,
86
+ options: RunOptions,
87
+ signal: AbortSignal | undefined,
88
+ ): Promise<RunResult> {
89
+ return new Promise<RunResult>((resolve) => {
90
+ const stdoutChunks: Buffer[] = []
91
+ const stderrChunks: Buffer[] = []
92
+
93
+ const child = spawn('bash', ['-c', command], {
94
+ cwd: options.cwd,
95
+ env: process.env,
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ })
98
+
99
+ child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk))
100
+ child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk))
101
+
102
+ let timedOut = false
103
+ let settled = false
104
+
105
+ const done = (exitCode: number): void => {
106
+ if (settled) return
107
+ settled = true
108
+ clearTimeout(timer)
109
+ if (signal !== undefined) {
110
+ signal.removeEventListener('abort', onAbort)
111
+ }
112
+
113
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8')
114
+ const stderr = Buffer.concat(stderrChunks).toString('utf8')
115
+
116
+ resolve({ stdout, stderr, exitCode })
117
+ }
118
+
119
+ // Timeout handler
120
+ const timer = setTimeout(() => {
121
+ timedOut = true
122
+ child.kill('SIGKILL')
123
+ }, options.timeoutMs)
124
+
125
+ // Abort-signal handler
126
+ const onAbort = (): void => {
127
+ child.kill('SIGKILL')
128
+ }
129
+
130
+ if (signal !== undefined) {
131
+ signal.addEventListener('abort', onAbort, { once: true })
132
+ }
133
+
134
+ child.on('close', (code: number | null) => {
135
+ const exitCode = code ?? (timedOut ? 124 : 1)
136
+ done(exitCode)
137
+ })
138
+
139
+ child.on('error', (err: Error) => {
140
+ if (!settled) {
141
+ settled = true
142
+ clearTimeout(timer)
143
+ if (signal !== undefined) {
144
+ signal.removeEventListener('abort', onAbort)
145
+ }
146
+ resolve({
147
+ stdout: '',
148
+ stderr: err.message,
149
+ exitCode: 127,
150
+ })
151
+ }
152
+ })
153
+ })
154
+ }
155
+
156
+ /**
157
+ * Format captured output into a single readable string.
158
+ * When only stdout is present its content is returned as-is.
159
+ * When stderr is also present both sections are labelled.
160
+ */
161
+ function buildOutput(stdout: string, stderr: string, exitCode: number): string {
162
+ const parts: string[] = []
163
+
164
+ if (stdout.length > 0) {
165
+ parts.push(stdout)
166
+ }
167
+
168
+ if (stderr.length > 0) {
169
+ parts.push(
170
+ stdout.length > 0
171
+ ? `--- stderr ---\n${stderr}`
172
+ : stderr,
173
+ )
174
+ }
175
+
176
+ if (parts.length === 0) {
177
+ return exitCode === 0
178
+ ? '(command completed with no output)'
179
+ : `(command exited with code ${exitCode}, no output)`
180
+ }
181
+
182
+ if (exitCode !== 0 && parts.length > 0) {
183
+ parts.push(`\n(exit code: ${exitCode})`)
184
+ }
185
+
186
+ return parts.join('\n')
187
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Built-in file-edit tool.
3
+ *
4
+ * Performs a targeted string replacement inside an existing file.
5
+ * The uniqueness invariant (one match unless replace_all is set) prevents the
6
+ * common class of bugs where a generic pattern matches the wrong occurrence.
7
+ */
8
+
9
+ import { readFile, writeFile } from 'fs/promises'
10
+ import { z } from 'zod'
11
+ import { defineTool } from '../framework.js'
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Tool definition
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export const fileEditTool = defineTool({
18
+ name: 'file_edit',
19
+ description:
20
+ 'Edit a file by replacing a specific string with new content. ' +
21
+ 'The `old_string` must appear verbatim in the file. ' +
22
+ 'By default the tool errors if `old_string` appears more than once — ' +
23
+ 'use `replace_all: true` to replace every occurrence. ' +
24
+ 'Use file_write when you need to create a new file or rewrite it entirely.',
25
+
26
+ inputSchema: z.object({
27
+ path: z
28
+ .string()
29
+ .describe('Absolute path to the file to edit.'),
30
+ old_string: z
31
+ .string()
32
+ .describe(
33
+ 'The exact string to find and replace. ' +
34
+ 'Must match character-for-character including whitespace and newlines.',
35
+ ),
36
+ new_string: z
37
+ .string()
38
+ .describe('The replacement string that will be inserted in place of `old_string`.'),
39
+ replace_all: z
40
+ .boolean()
41
+ .optional()
42
+ .describe(
43
+ 'When true, replace every occurrence of `old_string` instead of requiring it ' +
44
+ 'to be unique. Defaults to false.',
45
+ ),
46
+ }),
47
+
48
+ execute: async (input) => {
49
+ // Read the existing file.
50
+ let original: string
51
+ try {
52
+ const buffer = await readFile(input.path)
53
+ original = buffer.toString('utf8')
54
+ } catch (err) {
55
+ const message =
56
+ err instanceof Error ? err.message : 'Unknown error reading file.'
57
+ return {
58
+ data: `Could not read "${input.path}": ${message}`,
59
+ isError: true,
60
+ }
61
+ }
62
+
63
+ const occurrences = countOccurrences(original, input.old_string)
64
+
65
+ if (occurrences === 0) {
66
+ return {
67
+ data:
68
+ `The string to replace was not found in "${input.path}".\n` +
69
+ 'Make sure `old_string` matches the file contents exactly, ' +
70
+ 'including indentation and line endings.',
71
+ isError: true,
72
+ }
73
+ }
74
+
75
+ const replaceAll = input.replace_all ?? false
76
+
77
+ if (occurrences > 1 && !replaceAll) {
78
+ return {
79
+ data:
80
+ `\`old_string\` appears ${occurrences} times in "${input.path}". ` +
81
+ 'Provide a more specific string to uniquely identify the section you want ' +
82
+ 'to replace, or set `replace_all: true` to replace every occurrence.',
83
+ isError: true,
84
+ }
85
+ }
86
+
87
+ // Perform the replacement.
88
+ const updated = replaceAll
89
+ ? replaceAllOccurrences(original, input.old_string, input.new_string)
90
+ : original.replace(input.old_string, input.new_string)
91
+
92
+ // Persist the result.
93
+ try {
94
+ await writeFile(input.path, updated, 'utf8')
95
+ } catch (err) {
96
+ const message =
97
+ err instanceof Error ? err.message : 'Unknown error writing file.'
98
+ return {
99
+ data: `Failed to write "${input.path}": ${message}`,
100
+ isError: true,
101
+ }
102
+ }
103
+
104
+ const replacedCount = replaceAll ? occurrences : 1
105
+ return {
106
+ data:
107
+ `Replaced ${replacedCount} occurrence${replacedCount === 1 ? '' : 's'} ` +
108
+ `in "${input.path}".`,
109
+ isError: false,
110
+ }
111
+ },
112
+ })
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Internal helpers
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Count how many times `needle` appears in `haystack`.
120
+ * Uses a plain loop to avoid constructing a potentially large regex from
121
+ * untrusted input.
122
+ */
123
+ function countOccurrences(haystack: string, needle: string): number {
124
+ if (needle.length === 0) return 0
125
+ let count = 0
126
+ let pos = 0
127
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
128
+ count++
129
+ pos += needle.length
130
+ }
131
+ return count
132
+ }
133
+
134
+ /**
135
+ * Replace all occurrences of `needle` in `haystack` with `replacement`
136
+ * without using a regex (avoids regex-special-character escaping issues).
137
+ */
138
+ function replaceAllOccurrences(
139
+ haystack: string,
140
+ needle: string,
141
+ replacement: string,
142
+ ): string {
143
+ if (needle.length === 0) return haystack
144
+ const parts: string[] = []
145
+ let pos = 0
146
+ let next: number
147
+ while ((next = haystack.indexOf(needle, pos)) !== -1) {
148
+ parts.push(haystack.slice(pos, next))
149
+ parts.push(replacement)
150
+ pos = next + needle.length
151
+ }
152
+ parts.push(haystack.slice(pos))
153
+ return parts.join('')
154
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Built-in file-read tool.
3
+ *
4
+ * Reads a file from disk and returns its contents with 1-based line numbers.
5
+ * Supports reading a slice of lines via `offset` and `limit` for large files.
6
+ */
7
+
8
+ import { readFile } from 'fs/promises'
9
+ import { z } from 'zod'
10
+ import { defineTool } from '../framework.js'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Tool definition
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export const fileReadTool = defineTool({
17
+ name: 'file_read',
18
+ description:
19
+ 'Read the contents of a file from disk. ' +
20
+ 'Returns the file contents with line numbers prefixed in the format "N\\t<line>". ' +
21
+ 'Use `offset` and `limit` to read large files in chunks without loading the ' +
22
+ 'entire file into the context window.',
23
+
24
+ inputSchema: z.object({
25
+ path: z.string().describe('Absolute path to the file to read.'),
26
+ offset: z
27
+ .number()
28
+ .int()
29
+ .nonnegative()
30
+ .optional()
31
+ .describe(
32
+ '1-based line number to start reading from. ' +
33
+ 'When omitted the file is read from the beginning.',
34
+ ),
35
+ limit: z
36
+ .number()
37
+ .int()
38
+ .positive()
39
+ .optional()
40
+ .describe(
41
+ 'Maximum number of lines to return. ' +
42
+ 'When omitted all lines from `offset` to the end of the file are returned.',
43
+ ),
44
+ }),
45
+
46
+ execute: async (input) => {
47
+ let raw: string
48
+ try {
49
+ const buffer = await readFile(input.path)
50
+ raw = buffer.toString('utf8')
51
+ } catch (err) {
52
+ const message =
53
+ err instanceof Error ? err.message : 'Unknown error reading file.'
54
+ return {
55
+ data: `Could not read file "${input.path}": ${message}`,
56
+ isError: true,
57
+ }
58
+ }
59
+
60
+ // Split preserving trailing newlines correctly
61
+ const lines = raw.split('\n')
62
+
63
+ // Remove the last empty string produced by a trailing newline
64
+ if (lines.length > 0 && lines[lines.length - 1] === '') {
65
+ lines.pop()
66
+ }
67
+
68
+ const totalLines = lines.length
69
+
70
+ // Apply offset (convert from 1-based to 0-based)
71
+ const startIndex =
72
+ input.offset !== undefined ? Math.max(0, input.offset - 1) : 0
73
+
74
+ if (startIndex >= totalLines && totalLines > 0) {
75
+ return {
76
+ data:
77
+ `File "${input.path}" has ${totalLines} line${totalLines === 1 ? '' : 's'} ` +
78
+ `but offset ${input.offset} is beyond the end.`,
79
+ isError: true,
80
+ }
81
+ }
82
+
83
+ const endIndex =
84
+ input.limit !== undefined
85
+ ? Math.min(startIndex + input.limit, totalLines)
86
+ : totalLines
87
+
88
+ const slice = lines.slice(startIndex, endIndex)
89
+
90
+ // Build line-numbered output (1-based line numbers matching file positions)
91
+ const numbered = slice
92
+ .map((line, i) => `${startIndex + i + 1}\t${line}`)
93
+ .join('\n')
94
+
95
+ const meta =
96
+ endIndex < totalLines
97
+ ? `\n\n(showing lines ${startIndex + 1}–${endIndex} of ${totalLines})`
98
+ : ''
99
+
100
+ return {
101
+ data: numbered + meta,
102
+ isError: false,
103
+ }
104
+ },
105
+ })
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Built-in file-write tool.
3
+ *
4
+ * Creates or overwrites a file with the supplied content. Parent directories
5
+ * are created automatically (equivalent to `mkdir -p`).
6
+ */
7
+
8
+ import { mkdir, stat, writeFile } from 'fs/promises'
9
+ import { dirname } from 'path'
10
+ import { z } from 'zod'
11
+ import { defineTool } from '../framework.js'
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Tool definition
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export const fileWriteTool = defineTool({
18
+ name: 'file_write',
19
+ description:
20
+ 'Write content to a file, creating it (and any missing parent directories) if it ' +
21
+ 'does not already exist, or overwriting it if it does. ' +
22
+ 'Prefer this tool for creating new files; use file_edit for targeted in-place edits ' +
23
+ 'of existing files.',
24
+
25
+ inputSchema: z.object({
26
+ path: z
27
+ .string()
28
+ .describe(
29
+ 'Absolute path to the file to write. ' +
30
+ 'The path must be absolute (starting with /).',
31
+ ),
32
+ content: z.string().describe('The full content to write to the file.'),
33
+ }),
34
+
35
+ execute: async (input) => {
36
+ // Determine whether the file already exists so we can report create vs update.
37
+ let existed = false
38
+ try {
39
+ await stat(input.path)
40
+ existed = true
41
+ } catch {
42
+ // File does not exist — will be created.
43
+ }
44
+
45
+ // Ensure parent directory hierarchy exists.
46
+ const parentDir = dirname(input.path)
47
+ try {
48
+ await mkdir(parentDir, { recursive: true })
49
+ } catch (err) {
50
+ const message =
51
+ err instanceof Error ? err.message : 'Unknown error creating directories.'
52
+ return {
53
+ data: `Failed to create parent directory "${parentDir}": ${message}`,
54
+ isError: true,
55
+ }
56
+ }
57
+
58
+ // Write the file.
59
+ try {
60
+ await writeFile(input.path, input.content, 'utf8')
61
+ } catch (err) {
62
+ const message =
63
+ err instanceof Error ? err.message : 'Unknown error writing file.'
64
+ return {
65
+ data: `Failed to write file "${input.path}": ${message}`,
66
+ isError: true,
67
+ }
68
+ }
69
+
70
+ const lineCount = input.content.split('\n').length
71
+ const byteCount = Buffer.byteLength(input.content, 'utf8')
72
+ const action = existed ? 'Updated' : 'Created'
73
+
74
+ return {
75
+ data:
76
+ `${action} "${input.path}" ` +
77
+ `(${lineCount} line${lineCount === 1 ? '' : 's'}, ${byteCount} bytes).`,
78
+ isError: false,
79
+ }
80
+ },
81
+ })