@prmichaelsen/acp-mcp 0.6.0 → 0.7.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.
@@ -0,0 +1,191 @@
1
+ # Task 7: Implement Progress Streaming in Execute Command
2
+
3
+ **Milestone**: M4 - Progress Streaming - Server Implementation
4
+ **Estimated Time**: 4-5 hours
5
+ **Dependencies**: Task 6 (SSH stream execution)
6
+ **Status**: Not Started
7
+ **Priority**: High
8
+
9
+ ---
10
+
11
+ ## Objective
12
+
13
+ Update `acp_remote_execute_command` tool to support progress streaming when client provides a `progressToken`. Implement graceful fallback to timeout mode when no progress token is provided.
14
+
15
+ ## Context
16
+
17
+ **Design Reference**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
18
+
19
+ **Current State**: Tool uses `execWithTimeout()` and returns all output at once
20
+
21
+ **Desired State**: Tool detects `progressToken`, streams output with progress notifications, maintains backward compatibility
22
+
23
+ ---
24
+
25
+ ## Steps
26
+
27
+ ### 1. Update Handler Signature
28
+
29
+ **File**: `src/tools/acp-remote-execute-command.ts`
30
+
31
+ **Actions**:
32
+ - Add `extra?: { progressToken?: string | number }` parameter
33
+ - Import server instance for sending notifications
34
+ - Add type definitions for progress params
35
+
36
+ ### 2. Add Progress Detection Logic
37
+
38
+ **Actions**:
39
+ - Check if `extra?.progressToken` exists
40
+ - If yes: call `executeWithProgress()`
41
+ - If no: use existing `execWithTimeout()` (fallback)
42
+
43
+ ### 3. Implement executeWithProgress() Function
44
+
45
+ **Actions**:
46
+ - Call `sshConnection.execStream()`
47
+ - Set up stdout data handler
48
+ - Send progress notification for each chunk
49
+ - Collect full output for final result
50
+ - Handle stderr separately (no progress)
51
+ - Wait for exit code
52
+ - Return structured result
53
+
54
+ **Code Pattern**:
55
+ ```typescript
56
+ async function executeWithProgress(
57
+ command: string,
58
+ cwd: string | undefined,
59
+ sshConnection: SSHConnectionManager,
60
+ progressToken: string | number
61
+ ): Promise<{ content: Array<{ type: string; text: string }> }> {
62
+ const { stream, stderr: stderrStream, exitCode } = await sshConnection.execStream(command, cwd);
63
+
64
+ let stdout = '';
65
+ let stderr = '';
66
+ let bytesReceived = 0;
67
+
68
+ stream.on('data', (chunk: Buffer) => {
69
+ const text = chunk.toString();
70
+ stdout += text;
71
+ bytesReceived += chunk.length;
72
+
73
+ // Send progress notification
74
+ server.notification({
75
+ method: 'notifications/progress',
76
+ params: {
77
+ progressToken,
78
+ progress: bytesReceived,
79
+ total: undefined,
80
+ message: text,
81
+ },
82
+ });
83
+ });
84
+
85
+ stderrStream.on('data', (chunk: Buffer) => {
86
+ stderr += chunk.toString();
87
+ });
88
+
89
+ const finalExitCode = await exitCode;
90
+
91
+ return {
92
+ content: [{
93
+ type: 'text',
94
+ text: JSON.stringify({
95
+ stdout,
96
+ stderr,
97
+ exitCode: finalExitCode,
98
+ timedOut: false,
99
+ streamed: true,
100
+ }, null, 2),
101
+ }],
102
+ };
103
+ }
104
+ ```
105
+
106
+ ### 4. Add Rate Limiting
107
+
108
+ **Actions**:
109
+ - Limit progress notifications to max 10/second
110
+ - Batch small chunks
111
+ - Track last notification time
112
+ - Only send if enough time elapsed
113
+
114
+ **Code Pattern**:
115
+ ```typescript
116
+ let lastProgressTime = 0;
117
+ const MIN_PROGRESS_INTERVAL = 100; // 100ms
118
+
119
+ stream.on('data', (chunk: Buffer) => {
120
+ stdout += chunk.toString();
121
+ bytesReceived += chunk.length;
122
+
123
+ const now = Date.now();
124
+ if (now - lastProgressTime >= MIN_PROGRESS_INTERVAL) {
125
+ server.notification({ /* ... */ });
126
+ lastProgressTime = now;
127
+ }
128
+ });
129
+ ```
130
+
131
+ ### 5. Add Error Handling
132
+
133
+ **Actions**:
134
+ - Handle stream errors
135
+ - Handle connection loss
136
+ - Send error via progress notification
137
+ - Return error in final result
138
+
139
+ ### 6. Update Tool Schema
140
+
141
+ **Actions**:
142
+ - Update description to mention progress support
143
+ - Note that timeout is ignored when streaming
144
+ - Add examples of progress usage
145
+
146
+ ### 7. Test Implementation
147
+
148
+ **Actions**:
149
+ - Build project
150
+ - Test with progressToken (streaming mode)
151
+ - Test without progressToken (fallback mode)
152
+ - Test with long-running command
153
+ - Test with command that fails
154
+ - Test error scenarios
155
+
156
+ ---
157
+
158
+ ## Verification
159
+
160
+ - [ ] Handler accepts `extra` parameter
161
+ - [ ] Detects `progressToken` correctly
162
+ - [ ] Calls `executeWithProgress()` when token provided
163
+ - [ ] Falls back to `execWithTimeout()` when no token
164
+ - [ ] Sends progress notifications for stdout chunks
165
+ - [ ] Rate limiting prevents notification spam
166
+ - [ ] Collects full output for final result
167
+ - [ ] Handles errors gracefully
168
+ - [ ] TypeScript compiles without errors
169
+ - [ ] Build completes successfully
170
+ - [ ] Both modes tested and working
171
+
172
+ ---
173
+
174
+ ## Expected Output
175
+
176
+ ### Files Modified
177
+ - `src/tools/acp-remote-execute-command.ts` - Progress streaming implementation
178
+
179
+ ### New Function
180
+ ```typescript
181
+ async function executeWithProgress(
182
+ command: string,
183
+ cwd: string | undefined,
184
+ sshConnection: SSHConnectionManager,
185
+ progressToken: string | number
186
+ ): Promise<{ content: Array<{ type: string; text: string }> }>
187
+ ```
188
+
189
+ ---
190
+
191
+ **Next Task**: [Task 8: Update Server Request Handlers](task-8-update-server-handlers.md)
@@ -0,0 +1,109 @@
1
+ # Task 8: Update Server Request Handlers
2
+
3
+ **Milestone**: M4 - Progress Streaming - Server Implementation
4
+ **Estimated Time**: 1-2 hours
5
+ **Dependencies**: Task 7 (Progress streaming implementation)
6
+ **Status**: Not Started
7
+ **Priority**: Medium
8
+
9
+ ---
10
+
11
+ ## Objective
12
+
13
+ Update server request handlers in both `server.ts` and `server-factory.ts` to pass the `extra` parameter (containing `progressToken`) to tool handlers.
14
+
15
+ ## Context
16
+
17
+ **Design Reference**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
18
+
19
+ **Current State**: Handlers don't pass `extra` parameter to tools
20
+
21
+ **Desired State**: Handlers pass `extra` to `acp_remote_execute_command` handler
22
+
23
+ ---
24
+
25
+ ## Steps
26
+
27
+ ### 1. Update server.ts Handler
28
+
29
+ **File**: `src/server.ts`
30
+
31
+ **Actions**:
32
+ - Modify `CallToolRequestSchema` handler signature to accept `extra` parameter
33
+ - Pass `extra` to `handleAcpRemoteExecuteCommand()`
34
+ - Keep other tool handlers unchanged (they don't need progress)
35
+
36
+ **Code Pattern**:
37
+ ```typescript
38
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
39
+ const startTime = Date.now();
40
+ logger.toolInvoked(request.params.name, request.params.arguments);
41
+
42
+ try {
43
+ let result;
44
+
45
+ if (request.params.name === 'acp_remote_execute_command') {
46
+ result = await handleAcpRemoteExecuteCommand(
47
+ request.params.arguments,
48
+ sshConnection,
49
+ extra // Pass extra with progressToken
50
+ );
51
+ } else if (request.params.name === 'acp_remote_list_files') {
52
+ result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
53
+ }
54
+ // ... other tools
55
+
56
+ return result;
57
+ } catch (error) {
58
+ logger.toolFailed(request.params.name, error as Error, request.params.arguments);
59
+ throw error;
60
+ }
61
+ });
62
+ ```
63
+
64
+ ### 2. Update server-factory.ts Handler
65
+
66
+ **File**: `src/server-factory.ts`
67
+
68
+ **Actions**:
69
+ - Same changes as server.ts
70
+ - Ensure consistency between both files
71
+
72
+ ### 3. Verify No Breaking Changes
73
+
74
+ **Actions**:
75
+ - Confirm other tools still work
76
+ - Verify `extra` parameter is optional
77
+ - Test that undefined `extra` doesn't break anything
78
+
79
+ ### 4. Test Both Server Modes
80
+
81
+ **Actions**:
82
+ - Build project
83
+ - Test standalone server (server.ts)
84
+ - Test factory server (server-factory.ts)
85
+ - Verify both pass `extra` correctly
86
+
87
+ ---
88
+
89
+ ## Verification
90
+
91
+ - [ ] server.ts handler updated
92
+ - [ ] server-factory.ts handler updated
93
+ - [ ] `extra` parameter passed to execute_command handler
94
+ - [ ] Other tools unaffected
95
+ - [ ] TypeScript compiles without errors
96
+ - [ ] Build completes successfully
97
+ - [ ] Both server modes tested
98
+
99
+ ---
100
+
101
+ ## Expected Output
102
+
103
+ ### Files Modified
104
+ - `src/server.ts` - Pass `extra` to handler
105
+ - `src/server-factory.ts` - Pass `extra` to handler
106
+
107
+ ---
108
+
109
+ **Next Task**: [Task 9: Testing and Documentation](task-9-testing-documentation.md)
@@ -0,0 +1,192 @@
1
+ # Task 9: Testing and Documentation
2
+
3
+ **Milestone**: M4 - Progress Streaming - Server Implementation
4
+ **Estimated Time**: 3-4 hours
5
+ **Dependencies**: Tasks 6, 7, 8 (All implementation complete)
6
+ **Status**: Not Started
7
+ **Priority**: High
8
+
9
+ ---
10
+
11
+ ## Objective
12
+
13
+ Add comprehensive tests for progress streaming functionality and update all documentation for v0.7.0 release.
14
+
15
+ ## Context
16
+
17
+ **Design Reference**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
18
+
19
+ **Current State**: Implementation complete, needs testing and documentation
20
+
21
+ **Desired State**: Full test coverage, updated documentation, ready for v0.7.0 release
22
+
23
+ ---
24
+
25
+ ## Steps
26
+
27
+ ### 1. Write Unit Tests
28
+
29
+ **File**: Create `src/utils/ssh-connection.test.ts` (if doesn't exist)
30
+
31
+ **Actions**:
32
+ - Test `execStream()` returns correct types
33
+ - Test stream emits data events
34
+ - Test exit code promise resolves
35
+ - Test error handling
36
+ - Mock SSH client for tests
37
+
38
+ ### 2. Write Integration Tests
39
+
40
+ **File**: Create `src/tools/acp-remote-execute-command.test.ts`
41
+
42
+ **Actions**:
43
+ - Test with `progressToken` (streaming mode)
44
+ - Test without `progressToken` (fallback mode)
45
+ - Test progress notifications are sent
46
+ - Test rate limiting works
47
+ - Test error scenarios
48
+ - Use real SSH connection (or mock)
49
+
50
+ ### 3. Manual Testing
51
+
52
+ **Actions**:
53
+ - Test with simple command: `echo "test"`
54
+ - Test with long command: `for i in 1 2 3 4 5; do echo "Line $i"; sleep 0.5; done`
55
+ - Test with failing command
56
+ - Test with very long output
57
+ - Test connection loss scenario
58
+
59
+ ### 4. Update README.md
60
+
61
+ **File**: `README.md`
62
+
63
+ **Actions**:
64
+ - Add section on progress streaming support
65
+ - Document that progress requires MCP SDK v1.26.0+
66
+ - Add examples of progress usage
67
+ - Note client requirements
68
+ - Update tool description for `acp_remote_execute_command`
69
+
70
+ **Example Addition**:
71
+ ```markdown
72
+ ### Progress Streaming (v0.7.0+)
73
+
74
+ `acp_remote_execute_command` supports real-time progress streaming for long-running commands. When a client provides a `progressToken`, the tool will send progress notifications with live output.
75
+
76
+ **Requirements**:
77
+ - MCP SDK v1.26.0+ (server and client)
78
+ - Client must provide `progressToken` in request
79
+ - Client must handle `onprogress` callback
80
+
81
+ **Example** (client-side):
82
+ \`\`\`typescript
83
+ const result = await client.request({
84
+ method: 'tools/call',
85
+ params: {
86
+ name: 'acp_remote_execute_command',
87
+ arguments: { command: 'npm run build' }
88
+ }
89
+ }, {
90
+ progressToken: 'build-123',
91
+ onprogress: (progress) => {
92
+ console.log(progress.message); // Live output
93
+ }
94
+ });
95
+ \`\`\`
96
+
97
+ **Fallback**: If no `progressToken` provided, uses timeout-based execution (backward compatible).
98
+ ```
99
+
100
+ ### 5. Update CHANGELOG.md
101
+
102
+ **File**: `CHANGELOG.md`
103
+
104
+ **Actions**:
105
+ - Add v0.7.0 section
106
+ - Document progress streaming feature
107
+ - Note backward compatibility
108
+ - List all changes
109
+
110
+ **Example Entry**:
111
+ ```markdown
112
+ ## [0.7.0] - 2026-02-24
113
+
114
+ ### Added
115
+ - **Progress Streaming** for `acp_remote_execute_command` tool
116
+ - Real-time output streaming for long-running commands
117
+ - Uses MCP SDK's native progress notification system
118
+ - Graceful fallback to timeout mode for clients without progress support
119
+ - Rate limiting prevents notification spam (max 10/second)
120
+ - Supports commands like `npm run build`, `npm run dev`, `npm test`
121
+
122
+ ### Changed
123
+ - `acp_remote_execute_command` now accepts optional `progressToken` parameter
124
+ - Added `execStream()` method to SSHConnectionManager
125
+ - Server handlers now pass `extra` parameter to tool handlers
126
+
127
+ ### Technical Details
128
+ - Requires MCP SDK v1.26.0+ for progress support
129
+ - Progress notifications sent via `notifications/progress` method
130
+ - Backward compatible - existing clients unaffected
131
+ - No breaking changes to API
132
+ ```
133
+
134
+ ### 6. Update package.json
135
+
136
+ **File**: `package.json`
137
+
138
+ **Actions**:
139
+ - Bump version to 0.7.0
140
+ - Verify dependencies are correct
141
+
142
+ ### 7. Build and Verify
143
+
144
+ **Actions**:
145
+ - Run `npm run build`
146
+ - Verify TypeScript compiles
147
+ - Run tests: `npm test`
148
+ - Verify all tests pass
149
+ - Check for any warnings
150
+
151
+ ---
152
+
153
+ ## Verification
154
+
155
+ - [ ] Unit tests written and passing
156
+ - [ ] Integration tests written and passing
157
+ - [ ] Manual testing completed successfully
158
+ - [ ] README.md updated with progress documentation
159
+ - [ ] CHANGELOG.md updated for v0.7.0
160
+ - [ ] package.json version bumped to 0.7.0
161
+ - [ ] TypeScript compiles without errors
162
+ - [ ] Build completes successfully
163
+ - [ ] All tests pass
164
+ - [ ] No warnings or errors
165
+
166
+ ---
167
+
168
+ ## Expected Output
169
+
170
+ ### Files Created
171
+ - `src/utils/ssh-connection.test.ts` - Unit tests (if doesn't exist)
172
+ - `src/tools/acp-remote-execute-command.test.ts` - Integration tests (if doesn't exist)
173
+
174
+ ### Files Modified
175
+ - `README.md` - Progress streaming documentation
176
+ - `CHANGELOG.md` - v0.7.0 entry
177
+ - `package.json` - Version bump to 0.7.0
178
+
179
+ ### Test Results
180
+ ```
181
+ ✓ SSH stream execution tests (5 passed)
182
+ ✓ Progress streaming tests (8 passed)
183
+ ✓ Fallback mode tests (3 passed)
184
+ ✓ Error handling tests (4 passed)
185
+
186
+ Total: 20 tests passed
187
+ ```
188
+
189
+ ---
190
+
191
+ **Next Milestone**: M5 - Progress Streaming - Wrapper Integration (mcp-auth)
192
+ **Related Design**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
@@ -179,7 +179,7 @@ var logger = new Logger();
179
179
  // src/tools/acp-remote-execute-command.ts
180
180
  var acpRemoteExecuteCommandTool = {
181
181
  name: "acp_remote_execute_command",
182
- description: "Execute a shell command on the remote machine via SSH",
182
+ description: "Execute a shell command on the remote machine via SSH. Supports real-time progress streaming if client provides progressToken.",
183
183
  inputSchema: {
184
184
  type: "object",
185
185
  properties: {
@@ -193,17 +193,21 @@ var acpRemoteExecuteCommandTool = {
193
193
  },
194
194
  timeout: {
195
195
  type: "number",
196
- description: "Timeout in seconds (default: 30)",
196
+ description: "Timeout in seconds (default: 30). Ignored if progress streaming is used.",
197
197
  default: 30
198
198
  }
199
199
  },
200
200
  required: ["command"]
201
201
  }
202
202
  };
203
- async function handleAcpRemoteExecuteCommand(args, sshConnection) {
203
+ async function handleAcpRemoteExecuteCommand(args, sshConnection, extra, server) {
204
204
  const { command, cwd, timeout = 30 } = args;
205
- logger.debug("Executing remote command", { command, cwd, timeout });
205
+ const progressToken = extra?._meta?.progressToken;
206
+ logger.debug("Executing remote command", { command, cwd, timeout, hasProgressToken: !!progressToken });
206
207
  try {
208
+ if (progressToken && server) {
209
+ return await executeWithProgress(command, cwd, sshConnection, progressToken, server);
210
+ }
207
211
  const fullCommand = cwd ? `cd ${cwd} && ${command}` : command;
208
212
  const result = await sshConnection.execWithTimeout(fullCommand, timeout);
209
213
  logger.debug("Command execution result", {
@@ -244,6 +248,75 @@ async function handleAcpRemoteExecuteCommand(args, sshConnection) {
244
248
  };
245
249
  }
246
250
  }
251
+ async function executeWithProgress(command, cwd, sshConnection, progressToken, server) {
252
+ logger.debug("Starting streaming execution", { command, cwd, progressToken });
253
+ const { stream, stderr: stderrStream, exitCode } = await sshConnection.execStream(command, cwd);
254
+ let stdout = "";
255
+ let stderr = "";
256
+ let bytesReceived = 0;
257
+ let lastProgressTime = 0;
258
+ const MIN_PROGRESS_INTERVAL = 100;
259
+ stream.on("data", (chunk) => {
260
+ const text = chunk.toString();
261
+ stdout += text;
262
+ bytesReceived += chunk.length;
263
+ const now = Date.now();
264
+ if (now - lastProgressTime >= MIN_PROGRESS_INTERVAL) {
265
+ try {
266
+ server.notification({
267
+ method: "notifications/progress",
268
+ params: {
269
+ progressToken,
270
+ progress: bytesReceived,
271
+ total: void 0,
272
+ // Unknown total for streaming
273
+ message: text
274
+ }
275
+ });
276
+ lastProgressTime = now;
277
+ logger.debug("Progress notification sent", {
278
+ progressToken,
279
+ bytes: bytesReceived,
280
+ chunkSize: chunk.length
281
+ });
282
+ } catch (error) {
283
+ logger.warn("Failed to send progress notification", {
284
+ error: error instanceof Error ? error.message : String(error)
285
+ });
286
+ }
287
+ }
288
+ });
289
+ stderrStream.on("data", (chunk) => {
290
+ stderr += chunk.toString();
291
+ });
292
+ stream.on("error", (error) => {
293
+ logger.error("Stream error during execution", {
294
+ command,
295
+ error: error.message
296
+ });
297
+ });
298
+ const finalExitCode = await exitCode;
299
+ logger.debug("Streaming execution completed", {
300
+ command,
301
+ exitCode: finalExitCode,
302
+ stdoutBytes: stdout.length,
303
+ stderrBytes: stderr.length
304
+ });
305
+ const output = {
306
+ stdout,
307
+ stderr,
308
+ exitCode: finalExitCode,
309
+ timedOut: false,
310
+ streamed: true
311
+ // Indicate this was streamed
312
+ };
313
+ return {
314
+ content: [{
315
+ type: "text",
316
+ text: JSON.stringify(output, null, 2)
317
+ }]
318
+ };
319
+ }
247
320
 
248
321
  // src/tools/acp-remote-read-file.ts
249
322
  var acpRemoteReadFileTool = {
@@ -559,6 +632,57 @@ var SSHConnectionManager = class {
559
632
  throw error;
560
633
  }
561
634
  }
635
+ /**
636
+ * Execute a command on the remote server with streaming output
637
+ * Returns streams instead of buffered output for real-time progress
638
+ *
639
+ * @param command - Shell command to execute
640
+ * @param cwd - Optional working directory
641
+ * @returns Object with stdout stream, stderr stream, and exit code promise
642
+ */
643
+ async execStream(command, cwd) {
644
+ if (!this.connected) {
645
+ await this.connect();
646
+ }
647
+ const fullCommand = cwd ? `cd "${cwd}" && ${command}` : command;
648
+ const startTime = Date.now();
649
+ logger.sshCommand(fullCommand, cwd);
650
+ return new Promise((resolve, reject) => {
651
+ this.client.exec(fullCommand, (err, stream) => {
652
+ if (err) {
653
+ logger.error("SSH exec failed", {
654
+ command: fullCommand,
655
+ error: err.message
656
+ });
657
+ reject(err);
658
+ return;
659
+ }
660
+ logger.debug("SSH stream started", { command: fullCommand });
661
+ const exitCodePromise = new Promise((resolveExit) => {
662
+ stream.on("close", (code) => {
663
+ const duration = Date.now() - startTime;
664
+ logger.debug("SSH stream closed", {
665
+ command: fullCommand,
666
+ exitCode: code,
667
+ duration: `${duration}ms`
668
+ });
669
+ resolveExit(code);
670
+ });
671
+ });
672
+ stream.on("error", (error) => {
673
+ logger.error("SSH stream error", {
674
+ command: fullCommand,
675
+ error: error.message
676
+ });
677
+ });
678
+ resolve({
679
+ stream,
680
+ stderr: stream.stderr,
681
+ exitCode: exitCodePromise
682
+ });
683
+ });
684
+ });
685
+ }
562
686
  /**
563
687
  * Get SFTP wrapper for file operations
564
688
  */
@@ -845,7 +969,7 @@ async function createServer(serverConfig) {
845
969
  logger.debug(`Returning ${tools.length} tools`, { tools: tools.map((t) => t.name), userId: serverConfig.userId });
846
970
  return { tools };
847
971
  });
848
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
972
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
849
973
  const startTime = Date.now();
850
974
  logger.toolInvoked(request.params.name, request.params.arguments, serverConfig.userId);
851
975
  try {
@@ -853,7 +977,7 @@ async function createServer(serverConfig) {
853
977
  if (request.params.name === "acp_remote_list_files") {
854
978
  result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
855
979
  } else if (request.params.name === "acp_remote_execute_command") {
856
- result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
980
+ result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection, extra, server);
857
981
  } else if (request.params.name === "acp_remote_read_file") {
858
982
  result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
859
983
  } else if (request.params.name === "acp_remote_write_file") {