@iinm/plain-agent 1.8.3 → 1.8.4

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 (86) hide show
  1. package/README.md +2 -2
  2. package/bin/plain +1 -1
  3. package/config/config.predefined.json +1 -1
  4. package/config/prompts.predefined/shortcuts/configure.md +1 -1
  5. package/dist/main.mjs +473 -0
  6. package/dist/main.mjs.map +7 -0
  7. package/package.json +5 -7
  8. package/src/agent.d.ts +0 -52
  9. package/src/agent.mjs +0 -204
  10. package/src/agentLoop.mjs +0 -419
  11. package/src/agentState.mjs +0 -41
  12. package/src/claudeCodePlugin.mjs +0 -164
  13. package/src/cliArgs.mjs +0 -175
  14. package/src/cliBatch.mjs +0 -147
  15. package/src/cliCommands.mjs +0 -283
  16. package/src/cliCompleter.mjs +0 -227
  17. package/src/cliCost.mjs +0 -309
  18. package/src/cliFormatter.mjs +0 -413
  19. package/src/cliInteractive.mjs +0 -529
  20. package/src/cliInterruptTransform.mjs +0 -51
  21. package/src/cliMuteTransform.mjs +0 -26
  22. package/src/cliPasteTransform.mjs +0 -183
  23. package/src/config.d.ts +0 -36
  24. package/src/config.mjs +0 -197
  25. package/src/context/loadAgentRoles.mjs +0 -283
  26. package/src/context/loadPrompts.mjs +0 -324
  27. package/src/context/loadUserMessageContext.mjs +0 -147
  28. package/src/costTracker.mjs +0 -210
  29. package/src/env.mjs +0 -44
  30. package/src/main.mjs +0 -279
  31. package/src/mcpClient.mjs +0 -351
  32. package/src/mcpIntegration.mjs +0 -160
  33. package/src/model.d.ts +0 -109
  34. package/src/modelCaller.mjs +0 -32
  35. package/src/modelDefinition.d.ts +0 -92
  36. package/src/prompt.mjs +0 -138
  37. package/src/providers/anthropic.d.ts +0 -248
  38. package/src/providers/anthropic.mjs +0 -587
  39. package/src/providers/bedrock.d.ts +0 -249
  40. package/src/providers/bedrock.mjs +0 -700
  41. package/src/providers/gemini.d.ts +0 -208
  42. package/src/providers/gemini.mjs +0 -754
  43. package/src/providers/openai.d.ts +0 -281
  44. package/src/providers/openai.mjs +0 -544
  45. package/src/providers/openaiCompatible.d.ts +0 -147
  46. package/src/providers/openaiCompatible.mjs +0 -652
  47. package/src/providers/platform/awsSigV4.mjs +0 -184
  48. package/src/providers/platform/azure.mjs +0 -42
  49. package/src/providers/platform/bedrock.mjs +0 -78
  50. package/src/providers/platform/googleCloud.mjs +0 -34
  51. package/src/subagent.mjs +0 -265
  52. package/src/tmpfile.mjs +0 -27
  53. package/src/tool.d.ts +0 -74
  54. package/src/toolExecutor.mjs +0 -236
  55. package/src/toolInputValidator.mjs +0 -183
  56. package/src/toolUseApprover.mjs +0 -99
  57. package/src/tools/askURL.mjs +0 -209
  58. package/src/tools/askWeb.mjs +0 -208
  59. package/src/tools/compactContext.d.ts +0 -4
  60. package/src/tools/compactContext.mjs +0 -87
  61. package/src/tools/delegateToSubagent.d.ts +0 -4
  62. package/src/tools/delegateToSubagent.mjs +0 -48
  63. package/src/tools/execCommand.d.ts +0 -22
  64. package/src/tools/execCommand.mjs +0 -200
  65. package/src/tools/patchFile.d.ts +0 -4
  66. package/src/tools/patchFile.mjs +0 -133
  67. package/src/tools/reportAsSubagent.d.ts +0 -3
  68. package/src/tools/reportAsSubagent.mjs +0 -44
  69. package/src/tools/tmuxCommand.d.ts +0 -14
  70. package/src/tools/tmuxCommand.mjs +0 -194
  71. package/src/tools/writeFile.d.ts +0 -4
  72. package/src/tools/writeFile.mjs +0 -56
  73. package/src/usageStore.mjs +0 -167
  74. package/src/utils/evalJSONConfig.mjs +0 -72
  75. package/src/utils/matchValue.d.ts +0 -6
  76. package/src/utils/matchValue.mjs +0 -40
  77. package/src/utils/noThrow.mjs +0 -31
  78. package/src/utils/notify.mjs +0 -29
  79. package/src/utils/parseFileRange.mjs +0 -18
  80. package/src/utils/readFileRange.mjs +0 -33
  81. package/src/utils/retryOnError.mjs +0 -41
  82. package/src/voiceInput.mjs +0 -61
  83. package/src/voiceInputGemini.mjs +0 -105
  84. package/src/voiceInputOpenAI.mjs +0 -104
  85. package/src/voiceInputSession.mjs +0 -543
  86. package/src/voiceToggleKey.mjs +0 -62
@@ -1,200 +0,0 @@
1
- /**
2
- * @import { Tool } from '../tool'
3
- * @import { ExecCommandConfig, ExecCommandInput, ExecCommandSanboxConfig } from './execCommand'
4
- */
5
-
6
- import { execFile } from "node:child_process";
7
- import { writeTmpFile } from "../tmpfile.mjs";
8
- import { matchValue } from "../utils/matchValue.mjs";
9
- import { noThrow } from "../utils/noThrow.mjs";
10
-
11
- const OUTPUT_MAX_LENGTH = 1024 * 8;
12
- const OUTPUT_TRUNCATED_LENGTH = 1024 * 2;
13
-
14
- /**
15
- * @param {ExecCommandConfig=} config
16
- * @returns {Tool}
17
- */
18
- export function createExecCommandTool(config) {
19
- /** @type {Tool} */
20
- return {
21
- def: {
22
- name: "exec_command",
23
- description: "Run a command without shell interpretation.",
24
- inputSchema: {
25
- type: "object",
26
- properties: {
27
- command: {
28
- description: "The executable name or path. e.g., rg",
29
- type: "string",
30
- },
31
- args: {
32
- // Gemini 3 flashが command: rg, args: [rg, ...] のようにargsにコマンドを含めることがある
33
- description:
34
- "Array of arguments to pass to the command. Do not include the command name itself in this array.",
35
- type: "array",
36
- items: {
37
- type: "string",
38
- },
39
- },
40
- },
41
- required: ["command"],
42
- },
43
- },
44
-
45
- validateInput: (input) => {
46
- if (typeof input.command !== "string") {
47
- return new Error("command must be a string");
48
- }
49
-
50
- // Example: fd<arg_key>args</arg_key><arg_value>[... (GLM-5)
51
- if (input.command.match(/[<>]/)) {
52
- return new Error(
53
- `invalid tool use format: command=${JSON.stringify(input.command)}`,
54
- );
55
- }
56
-
57
- if (input.command.startsWith("-")) {
58
- return new Error("command must not start with '-'");
59
- }
60
-
61
- if (input.args && !Array.isArray(input.args)) {
62
- return new Error("args must be an array of strings");
63
- }
64
-
65
- return;
66
- },
67
-
68
- /**
69
- * @param {ExecCommandInput} input
70
- * @returns {Promise<string | Error>}
71
- */
72
- impl: async (input) =>
73
- await noThrow(async () => {
74
- const { command, args } = config?.sandbox
75
- ? rewriteInputForSandbox(input, config.sandbox)
76
- : input;
77
- return new Promise((resolve, _reject) => {
78
- const child = execFile(
79
- command,
80
- args,
81
- {
82
- shell: false,
83
- env: {
84
- PWD: process.env.PWD,
85
- PATH: process.env.PATH,
86
- HOME: process.env.HOME,
87
- },
88
- timeout: 5 * 60 * 1000,
89
- },
90
- async (err, stdout, stderr) => {
91
- /**
92
- * @param {string} content
93
- * @param {string} type
94
- * @returns {Promise<string>}
95
- */
96
- const formatOutput = async (content, type) => {
97
- if (content.length <= OUTPUT_MAX_LENGTH) {
98
- return content;
99
- }
100
-
101
- let fileExtension = "txt";
102
- try {
103
- JSON.parse(content);
104
- fileExtension = "json";
105
- } catch {
106
- // not JSON
107
- }
108
-
109
- const prefix = `exec_command-${type}`;
110
- const filePath = await writeTmpFile(
111
- content,
112
- prefix,
113
- fileExtension,
114
- );
115
- const lineCount = content.split("\n").length;
116
-
117
- const head = content.slice(0, OUTPUT_TRUNCATED_LENGTH);
118
- const tail = content.slice(
119
- Math.max(content.length - OUTPUT_TRUNCATED_LENGTH, 0),
120
- );
121
-
122
- return [
123
- `Content is too large (${content.length} characters, ${lineCount} lines). Saved to ${filePath}.`,
124
- `<truncated_output part="start" length="${OUTPUT_TRUNCATED_LENGTH}" total_length="${content.length}">\n${head}\n</truncated_output>`,
125
- `<truncated_output part="end" length="${OUTPUT_TRUNCATED_LENGTH}" total_length="${content.length}">\n${tail}</truncated_output>\n`,
126
- ].join("\n\n");
127
- };
128
-
129
- const stdoutOrMessage = await formatOutput(stdout, "stdout");
130
- const stderrOrMessage = await formatOutput(stderr, "stderr");
131
-
132
- const result = [
133
- stdoutOrMessage
134
- ? `<stdout>\n${stdoutOrMessage}</stdout>`
135
- : "<stdout></stdout>",
136
- "",
137
- stderrOrMessage
138
- ? `<stderr>\n${stderrOrMessage}</stderr>`
139
- : "<stderr></stderr>",
140
- ];
141
-
142
- if (err) {
143
- // rg: 何もマッチしない場合は exit status != 0 になるので無視
144
- const ignoreError = [command, ...(args || [])].includes("rg");
145
- if (!ignoreError) {
146
- // err.message が長過ぎる場合は先頭を表示
147
- const errMessageTruncated = err.message.slice(
148
- 0,
149
- OUTPUT_TRUNCATED_LENGTH,
150
- );
151
- const isErrMessageTruncated =
152
- err.message.length > OUTPUT_MAX_LENGTH;
153
- result.push(
154
- `\n<error>\n${err.name}: ${errMessageTruncated}${isErrMessageTruncated ? "... (Message truncated)" : ""}</error>`,
155
- );
156
- }
157
- }
158
- return resolve(result.join("\n"));
159
- },
160
- );
161
- child.stdin?.end();
162
- });
163
- }),
164
- };
165
- }
166
-
167
- /**
168
- * @param {ExecCommandInput} input
169
- * @param {ExecCommandSanboxConfig} sandbox
170
- * @returns {ExecCommandInput}
171
- */
172
- function rewriteInputForSandbox(input, sandbox) {
173
- const matchedRule = (sandbox.rules || []).find((rule) =>
174
- matchValue(input, rule.pattern),
175
- );
176
-
177
- if (matchedRule?.mode === "unsandboxed") {
178
- return input;
179
- }
180
-
181
- const args = [
182
- ...(sandbox.args || []),
183
- ...(matchedRule?.additionalArgs || []),
184
- ];
185
-
186
- if (sandbox.separator) {
187
- args.push(sandbox.separator);
188
- }
189
-
190
- args.push(input.command);
191
-
192
- if (input.args) {
193
- args.push(...input.args);
194
- }
195
-
196
- return {
197
- command: sandbox.command,
198
- args,
199
- };
200
- }
@@ -1,4 +0,0 @@
1
- export type PatchFileInput = {
2
- filePath: string;
3
- diff: string;
4
- };
@@ -1,133 +0,0 @@
1
- /**
2
- * @import { Tool } from '../tool'
3
- * @import { PatchFileInput } from './patchFile'
4
- */
5
-
6
- import fs from "node:fs/promises";
7
- import { noThrow } from "../utils/noThrow.mjs";
8
-
9
- /**
10
- * @param {string} [nonce]
11
- * @returns {Tool}
12
- */
13
- export function createPatchFileTool(
14
- nonce = Math.random().toString(36).substring(2, 5),
15
- ) {
16
- return {
17
- def: {
18
- name: "patch_file",
19
- description:
20
- "Modify a file by replacing specific content with new content.",
21
- inputSchema: {
22
- type: "object",
23
- properties: {
24
- filePath: {
25
- type: "string",
26
- },
27
- diff: {
28
- description: `
29
- Format:
30
- <<< ${nonce} <<< SEARCH
31
- old content
32
- === ${nonce} ===
33
- new content
34
- >>> ${nonce} >>> REPLACE
35
-
36
- <<< ${nonce} <<< SEARCH
37
- other old content
38
- === ${nonce} ===
39
- other new content
40
- >>> ${nonce} >>> REPLACE
41
-
42
- - Content is searched as an exact match including indentation and line breaks.
43
- - The first match found will be replaced if there are multiple matches.
44
- `.trim(),
45
- type: "string",
46
- },
47
- },
48
- required: ["filePath", "diff"],
49
- },
50
- },
51
-
52
- /**
53
- * @param {PatchFileInput} input
54
- * @returns {Promise<string | Error>}
55
- */
56
- impl: async (input) =>
57
- await noThrow(async () => {
58
- const { filePath, diff } = input;
59
-
60
- // Validate marker counts: each block needs exactly one of each marker.
61
- // Since nonce is random, duplicate markers mean the user accidentally
62
- // included a marker line in their search/replace content (copy-paste error).
63
- const searchMarker = `<<< ${nonce} <<< SEARCH`;
64
- const sepMarker = `=== ${nonce} ===`;
65
- const replaceMarker = `>>> ${nonce} >>> REPLACE`;
66
- /** @type {(s: string, sub: string) => number} */
67
- const count = (s, sub) => s.split(sub).length - 1;
68
- const nSearch = count(diff, searchMarker);
69
- const nSep = count(diff, sepMarker);
70
- const nReplace = count(diff, replaceMarker);
71
-
72
- if (nSearch !== nReplace) {
73
- throw new Error(
74
- `Mismatched block markers: found ${nSearch} "${searchMarker}" but ${nReplace} "${replaceMarker}". ` +
75
- "Did you accidentally include a marker in your search/replace content?",
76
- );
77
- }
78
- if (nSep !== nSearch) {
79
- throw new Error(
80
- `Each diff block needs exactly one "${sepMarker}" separator, ` +
81
- `but found ${nSep} separators for ${nSearch} block(s). ` +
82
- "Did you accidentally include the separator marker in your search/replace content?",
83
- );
84
- }
85
-
86
- const content = await fs.readFile(filePath, "utf8");
87
- const matches = Array.from(
88
- diff.matchAll(
89
- new RegExp(
90
- `<<< ${nonce} <<< SEARCH\\n(.*?)\\n=== ${nonce} ===\\n(.*?)\\n?>>> ${nonce} >>> REPLACE`,
91
- "gs",
92
- ),
93
- ),
94
- );
95
- if (matches.length === 0) {
96
- throw new Error(
97
- `Invalid diff format. Each markers must include the nonce: <<< ${nonce} <<< SEARCH, === ${nonce} ===, >>> ${nonce} >>> REPLACE`,
98
- );
99
- }
100
- let newContent = content;
101
- for (const match of matches) {
102
- const [_, search, replace] = match;
103
- if (!newContent.includes(search)) {
104
- throw new Error(
105
- JSON.stringify(`Search content not found: ${search}`),
106
- );
107
- }
108
- // Escape $ characters in replacement string to prevent interpretation of $& $1 $$ patterns
109
- const escapedReplace = replace.replace(/\$/g, "$$$$");
110
- if (replace === "" && newContent.includes(`${search}\n`)) {
111
- newContent = newContent.replace(`${search}\n`, "");
112
- } else if (replace === "" && newContent.includes(`\n${search}`)) {
113
- newContent = newContent.replace(`\n${search}`, "");
114
- } else {
115
- newContent = newContent.replace(search, escapedReplace);
116
- }
117
- }
118
- await fs.writeFile(filePath, newContent);
119
- return `Patched file: ${filePath}`;
120
- }),
121
-
122
- /**
123
- * @param {Record<string, unknown>} input
124
- * @returns {Record<string, unknown>}
125
- */
126
- maskApprovalInput: (input) => {
127
- const patchFileInput = /** @type {PatchFileInput} */ (input);
128
- return {
129
- filePath: patchFileInput.filePath,
130
- };
131
- },
132
- };
133
- }
@@ -1,3 +0,0 @@
1
- export type ReportAsSubagentInput = {
2
- memoryPath: string;
3
- };
@@ -1,44 +0,0 @@
1
- /**
2
- * @import { Tool, ToolImplementation } from '../tool'
3
- */
4
-
5
- export const reportAsSubagentToolName = "report_as_subagent";
6
-
7
- /** @returns {Tool} */
8
- export function createReportAsSubagentTool() {
9
- /** @type {ToolImplementation} */
10
- let impl = async () => {
11
- throw new Error("Not implemented");
12
- };
13
-
14
- /** @type {Tool} */
15
- const tool = {
16
- def: {
17
- name: reportAsSubagentToolName,
18
- description:
19
- "End the subagent role and report the result to the main agent.",
20
- inputSchema: {
21
- type: "object",
22
- properties: {
23
- memoryPath: {
24
- type: "string",
25
- description:
26
- "Path to the memory file containing the result of the subagent's task.",
27
- },
28
- },
29
- required: ["memoryPath"],
30
- },
31
- },
32
-
33
- // Implementation will be injected by the agent to access its state
34
- get impl() {
35
- return impl;
36
- },
37
-
38
- injectImpl(fn) {
39
- impl = fn;
40
- },
41
- };
42
-
43
- return tool;
44
- }
@@ -1,14 +0,0 @@
1
- import type { Tool } from "../tool";
2
- import type { ExecCommandSanboxConfig } from "./execCommand";
3
-
4
- export type TmuxCommandInput = {
5
- command: string;
6
- args?: string[];
7
- };
8
-
9
- export type TmuxCommandConfig = {
10
- sandbox?: ExecCommandSanboxConfig;
11
- };
12
-
13
- export function createTmuxCommandTool(config?: TmuxCommandConfig): Tool;
14
- export const tmuxCommandTool: Tool;
@@ -1,194 +0,0 @@
1
- /**
2
- * @import { Tool } from '../tool'
3
- * @import { TmuxCommandConfig, TmuxCommandInput } from './tmuxCommand'
4
- */
5
-
6
- import { execFile } from "node:child_process";
7
- import { noThrow } from "../utils/noThrow.mjs";
8
-
9
- const OUTPUT_MAX_LENGTH = 1024 * 8;
10
-
11
- /**
12
- + * Sandbox-aware tmux command tool
13
- + * @param {TmuxCommandConfig=} config
14
- + * @returns {Tool}
15
- + */
16
- export function createTmuxCommandTool(config) {
17
- /** @type {Tool} */
18
- return {
19
- def: {
20
- name: "tmux_command",
21
- description: "Run a tmux command",
22
- inputSchema: {
23
- type: "object",
24
- properties: {
25
- command: {
26
- description: "The tmux command to run",
27
- type: "string",
28
- },
29
- args: {
30
- description: "Arguments to pass to the tmux command",
31
- type: "array",
32
- items: {
33
- type: "string",
34
- },
35
- },
36
- },
37
- required: ["command"],
38
- },
39
- },
40
-
41
- /**
42
- * @param {TmuxCommandInput} input
43
- * @returns {Promise<string | Error>}
44
- */
45
- impl: async (input) =>
46
- await noThrow(async () => {
47
- const { command } = input;
48
- const args = input.args || [];
49
-
50
- // tmuxはセミコロンを複数コマンドの区切りとして扱うためエスケープが必要
51
- // LLMがこのルールを無視するのでここでエスケープする
52
- if (command === "send-keys") {
53
- for (let i = 1; i < args.length; i++) {
54
- const arg = args[i];
55
- if (arg.endsWith(";") && !arg.endsWith("\\;")) {
56
- args[i] = `${arg.slice(0, -1)}\\;`;
57
- }
58
- }
59
- }
60
-
61
- const execFileOptions = {
62
- shell: false,
63
- env: {
64
- PWD: process.env.PWD,
65
- PATH: process.env.PATH,
66
- HOME: process.env.HOME,
67
- },
68
- };
69
-
70
- /**
71
- * @param {{command: string, args: string[]}} input
72
- * @returns {{command: string, args: string[]}}
73
- */
74
- const useSandbox = ({ command, args }) => {
75
- if (config?.sandbox) {
76
- return {
77
- command: config.sandbox.command,
78
- args: [...(config.sandbox.args || []), command, ...args],
79
- };
80
- }
81
- return { command, args };
82
- };
83
-
84
- const execFileTmuxCommandInput = useSandbox({
85
- command: "tmux",
86
- args: [command, ...args],
87
- });
88
-
89
- return new Promise((resolve, _reject) => {
90
- execFile(
91
- execFileTmuxCommandInput.command,
92
- execFileTmuxCommandInput.args,
93
- execFileOptions,
94
- async (err, stdout, stderr) => {
95
- // capture-pane の結果に空白の行が含まれることがあるためtrim する
96
- const stdoutTruncated = stdout.trim().slice(-OUTPUT_MAX_LENGTH);
97
- const isStdoutTruncated =
98
- stdout.trim().length > OUTPUT_MAX_LENGTH;
99
- const stderrTruncated = stderr.trim().slice(-OUTPUT_MAX_LENGTH);
100
- const isStderrTruncated =
101
- stderr.trim().length > OUTPUT_MAX_LENGTH;
102
- const result = [
103
- stdoutTruncated
104
- ? `<stdout>\n${isStdoutTruncated ? "(Output truncated) ..." : ""}${stdoutTruncated}\n</stdout>`
105
- : "<stdout></stdout>",
106
- "",
107
- stderrTruncated
108
- ? `<stderr>\n${isStderrTruncated ? "(Output truncated) ..." : ""}${stderrTruncated}\n</stderr>`
109
- : "<stderr></stderr>",
110
- ];
111
- if (err) {
112
- const errMessageTruncated = err.message.slice(
113
- 0,
114
- OUTPUT_MAX_LENGTH,
115
- );
116
- const isErrMessageTruncated =
117
- err.message.length > OUTPUT_MAX_LENGTH;
118
- result.push(
119
- `\n<error>\n${err.name}: ${errMessageTruncated}${isErrMessageTruncated ? "... (Message truncated)" : ""}</error>`,
120
- );
121
- }
122
-
123
- if (["new-session", "new", "new-window"].includes(command)) {
124
- // show window list after creating a new session or window
125
- const targetPosition = command.includes("window")
126
- ? args.indexOf("-t") + 1
127
- : args.indexOf("-s") + 1;
128
- const target = args[targetPosition];
129
-
130
- const execFileTmuxListWindowInput = useSandbox({
131
- command: "tmux",
132
- args: ["list-windows", "-t", target],
133
- });
134
- const listWindowResult = await new Promise(
135
- (resolve, _reject) => {
136
- execFile(
137
- execFileTmuxListWindowInput.command,
138
- execFileTmuxListWindowInput.args,
139
- execFileOptions,
140
- (err, stdout, _stderr) => {
141
- if (err) {
142
- console.error(
143
- `Failed to list tmux windows: ${err.message}, stack=${err.stack}`,
144
- );
145
- }
146
- return resolve(stdout);
147
- },
148
- );
149
- },
150
- );
151
- result.push(
152
- `\n<tmux:list-windows>\n${listWindowResult}</tmux:list-windows>`,
153
- );
154
- }
155
-
156
- if (command === "send-keys") {
157
- // capture the pane after sending keys
158
- // wait for the command to be executed
159
- await new Promise((resolve) => setTimeout(resolve, 2000));
160
- const targetPosition = args.indexOf("-t") + 1;
161
- const target = args[targetPosition];
162
- const execFileTmuxCapturePaneInput = useSandbox({
163
- command: "tmux",
164
- args: ["capture-pane", "-p", "-t", target],
165
- });
166
- const captured = await new Promise((resolve, _reject) => {
167
- execFile(
168
- execFileTmuxCapturePaneInput.command,
169
- execFileTmuxCapturePaneInput.args,
170
- execFileOptions,
171
- (err, stdout, _stderr) => {
172
- if (err) {
173
- console.error(
174
- `Failed to capture tmux pane: ${err.message}, stack=${err.stack}`,
175
- );
176
- }
177
- return resolve(stdout.trim());
178
- },
179
- );
180
- });
181
- const capturedTruncated = captured.slice(-OUTPUT_MAX_LENGTH);
182
- const isCapturedTruncated = captured.length > OUTPUT_MAX_LENGTH;
183
- result.push(
184
- `\n<tmux:capture-pane target="${target}"">\n${isCapturedTruncated ? "(Output truncated) ..." : ""}${capturedTruncated}\n</tmux:capture-pane>`,
185
- );
186
- }
187
-
188
- return resolve(result.join("\n"));
189
- },
190
- );
191
- });
192
- }),
193
- };
194
- }
@@ -1,4 +0,0 @@
1
- export type WriteFileInput = {
2
- filePath: string;
3
- content: string;
4
- };
@@ -1,56 +0,0 @@
1
- /**
2
- * @import { Tool } from '../tool'
3
- * @import { WriteFileInput } from './writeFile'
4
- */
5
-
6
- import fs from "node:fs/promises";
7
- import path from "node:path";
8
- import { noThrow } from "../utils/noThrow.mjs";
9
-
10
- /** @type {Tool} */
11
- export const writeFileTool = {
12
- def: {
13
- name: "write_file",
14
- description: "Write a file",
15
- inputSchema: {
16
- type: "object",
17
- properties: {
18
- filePath: {
19
- type: "string",
20
- },
21
- content: {
22
- type: "string",
23
- },
24
- },
25
- required: ["filePath", "content"],
26
- },
27
- },
28
-
29
- /**
30
- * @param {WriteFileInput} input
31
- * @returns {Promise<string | Error>}
32
- */
33
- impl: async (input) =>
34
- await noThrow(async () => {
35
- const { filePath, content } = input;
36
-
37
- const absFilePath = path.resolve(filePath);
38
-
39
- // Ensure the destination directory exists before writing
40
- const dir = path.dirname(absFilePath);
41
- await fs.mkdir(dir, { recursive: true });
42
- await fs.writeFile(absFilePath, content, "utf8");
43
- return `Wrote to file: ${filePath}`;
44
- }),
45
-
46
- /**
47
- * @param {Record<string, unknown>} input
48
- * @returns {Record<string, unknown>}
49
- */
50
- maskApprovalInput: (input) => {
51
- const writeFileInput = /** @type {WriteFileInput} */ (input);
52
- return {
53
- filePath: writeFileInput.filePath,
54
- };
55
- },
56
- };