@acontext/acontext 0.1.2 → 0.1.3

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.
@@ -64,6 +64,9 @@ export declare class TextEditorTool extends AbstractBaseTool {
64
64
  };
65
65
  view_range: {
66
66
  type: string[];
67
+ items: {
68
+ type: string;
69
+ };
67
70
  description: string;
68
71
  };
69
72
  };
@@ -167,6 +167,7 @@ class TextEditorTool extends base_1.AbstractBaseTool {
167
167
  },
168
168
  view_range: {
169
169
  type: ['array', 'null'],
170
+ items: { type: 'integer' },
170
171
  description: "Optional for 'view' command. An array [start_line, end_line] to view specific lines. If not provided, shows the first 200 lines.",
171
172
  },
172
173
  };
@@ -22,23 +22,22 @@ export interface CreateFileResult {
22
22
  stderr?: string;
23
23
  }
24
24
  export interface StrReplaceResult {
25
- oldStart?: number;
26
- oldLines?: number;
27
- newStart?: number;
28
- newLines?: number;
29
- lines?: string[];
25
+ msg?: string;
30
26
  error?: string;
31
27
  stderr?: string;
32
28
  }
33
29
  /**
34
30
  * View file content with line numbers.
35
31
  */
36
- export declare function viewFile(ctx: SandboxContext, path: string, viewRange: number[] | null, timeout?: number): Promise<ViewFileResult>;
32
+ export declare function viewFile(ctx: SandboxContext, filePath: string, viewRange: number[] | null, timeout?: number): Promise<ViewFileResult>;
37
33
  /**
38
34
  * Create a new file with content.
39
35
  */
40
- export declare function createFile(ctx: SandboxContext, path: string, fileText: string, timeout?: number): Promise<CreateFileResult>;
36
+ export declare function createFile(ctx: SandboxContext, filePath: string, fileText: string, timeout?: number): Promise<CreateFileResult>;
41
37
  /**
42
38
  * Replace a string in a file.
39
+ *
40
+ * Uses a Python script on the sandbox to avoid transferring the entire file.
41
+ * Only the base64-encoded oldStr and newStr are sent.
43
42
  */
44
- export declare function strReplace(ctx: SandboxContext, path: string, oldStr: string, newStr: string, timeout?: number): Promise<StrReplaceResult>;
43
+ export declare function strReplace(ctx: SandboxContext, filePath: string, oldStr: string, newStr: string, timeout?: number): Promise<StrReplaceResult>;
@@ -2,11 +2,15 @@
2
2
  /**
3
3
  * Text editor file operations for sandbox environments.
4
4
  */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
5
8
  Object.defineProperty(exports, "__esModule", { value: true });
6
9
  exports.escapeForShell = escapeForShell;
7
10
  exports.viewFile = viewFile;
8
11
  exports.createFile = createFile;
9
12
  exports.strReplace = strReplace;
13
+ const path_1 = __importDefault(require("path"));
10
14
  const MAX_CONTENT_CHARS = 20000;
11
15
  /**
12
16
  * Truncate text to maxChars, appending a truncation flag if needed.
@@ -27,56 +31,48 @@ function escapeForShell(s) {
27
31
  /**
28
32
  * View file content with line numbers.
29
33
  */
30
- async function viewFile(ctx, path, viewRange, timeout) {
31
- // First check if file exists and get total lines
32
- const checkCmd = `wc -l < ${escapeForShell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'`;
33
- const checkResult = await ctx.client.sandboxes.execCommand({
34
- sandboxId: ctx.sandboxId,
35
- command: checkCmd,
36
- timeout,
37
- });
38
- if (checkResult.stdout.includes('FILE_NOT_FOUND') || checkResult.exit_code !== 0) {
39
- return {
40
- error: `File not found: ${path}`,
41
- stderr: checkResult.stderr,
42
- };
43
- }
44
- const totalLines = /^\d+$/.test(checkResult.stdout.trim())
45
- ? parseInt(checkResult.stdout.trim(), 10)
46
- : 0;
47
- // Build the view command with line numbers
48
- let cmd;
34
+ async function viewFile(ctx, filePath, viewRange, timeout) {
35
+ const escapedPath = escapeForShell(filePath);
36
+ // Build combined command: check existence, get total lines, and view content in one exec
37
+ let viewCmd;
49
38
  let startLine;
50
39
  if (viewRange && viewRange.length === 2) {
51
40
  const [rangeStart, rangeEnd] = viewRange;
52
- cmd = `sed -n '${rangeStart},${rangeEnd}p' ${escapeForShell(path)} | nl -ba -v ${rangeStart}`;
41
+ viewCmd = `sed -n '${rangeStart},${rangeEnd}p' ${escapedPath} | nl -ba -v ${rangeStart}`;
53
42
  startLine = rangeStart;
54
43
  }
55
44
  else {
56
- // Default to first 200 lines if no range specified
57
45
  const maxLines = 200;
58
- cmd = `head -n ${maxLines} ${escapeForShell(path)} | nl -ba`;
46
+ viewCmd = `head -n ${maxLines} ${escapedPath} | nl -ba`;
59
47
  startLine = 1;
60
48
  }
49
+ // Single combined command: outputs "TOTAL:<n>" on first line, then file content
50
+ const cmd = `if [ ! -f ${escapedPath} ]; then echo 'FILE_NOT_FOUND'; exit 1; fi; echo "TOTAL:$(wc -l < ${escapedPath})"; ${viewCmd}`;
61
51
  const result = await ctx.client.sandboxes.execCommand({
62
52
  sandboxId: ctx.sandboxId,
63
53
  command: cmd,
64
54
  timeout,
65
55
  });
66
- if (result.exit_code !== 0) {
56
+ if (result.exit_code !== 0 || result.stdout.includes('FILE_NOT_FOUND')) {
67
57
  return {
68
- error: `Failed to view file: ${path}`,
58
+ error: `File not found: ${filePath}`,
69
59
  stderr: result.stderr,
70
60
  };
71
61
  }
72
- // Count lines in output
73
- const contentLines = result.stdout.trim()
74
- ? result.stdout.trimEnd().split('\n')
75
- : [];
62
+ // Parse output: first line is "TOTAL:<n>", rest is content
63
+ const lines = result.stdout.split('\n');
64
+ let totalLines = 0;
65
+ let content = '';
66
+ if (lines.length > 0 && lines[0].startsWith('TOTAL:')) {
67
+ const totalStr = lines[0].substring(6).trim();
68
+ totalLines = /^\d+$/.test(totalStr) ? parseInt(totalStr, 10) : 0;
69
+ content = lines.slice(1).join('\n');
70
+ }
71
+ const contentLines = content.trim() ? content.trimEnd().split('\n') : [];
76
72
  const numLines = contentLines.length;
77
73
  return {
78
74
  file_type: 'text',
79
- content: truncateContent(result.stdout),
75
+ content: truncateContent(content),
80
76
  numLines,
81
77
  startLine: viewRange ? startLine : 1,
82
78
  totalLines: totalLines + 1, // wc -l doesn't count last line without newline
@@ -85,129 +81,83 @@ async function viewFile(ctx, path, viewRange, timeout) {
85
81
  /**
86
82
  * Create a new file with content.
87
83
  */
88
- async function createFile(ctx, path, fileText, timeout) {
89
- // Check if file already exists
90
- const checkCmd = `test -f ${escapeForShell(path)} && echo 'EXISTS' || echo 'NEW'`;
91
- const checkResult = await ctx.client.sandboxes.execCommand({
92
- sandboxId: ctx.sandboxId,
93
- command: checkCmd,
94
- timeout,
95
- });
96
- const isUpdate = checkResult.stdout.includes('EXISTS');
97
- // Create directory if needed
98
- const parts = path.split('/');
99
- parts.pop();
100
- const dirPath = parts.join('/');
101
- if (dirPath) {
102
- const mkdirCmd = `mkdir -p ${escapeForShell(dirPath)}`;
103
- await ctx.client.sandboxes.execCommand({
104
- sandboxId: ctx.sandboxId,
105
- command: mkdirCmd,
106
- timeout,
107
- });
108
- }
109
- // Write file using base64 encoding to safely transfer content
84
+ async function createFile(ctx, filePath, fileText, timeout) {
85
+ const escapedPath = escapeForShell(filePath);
110
86
  const encodedContent = Buffer.from(fileText, 'utf-8').toString('base64');
111
- const writeCmd = `echo ${escapeForShell(encodedContent)} | base64 -d > ${escapeForShell(path)}`;
87
+ // Get directory path for mkdir
88
+ const dirPath = path_1.default.posix.dirname(filePath);
89
+ const mkdirPart = dirPath && dirPath !== '.' ? `mkdir -p ${escapeForShell(dirPath)} && ` : '';
90
+ // Single combined command: check existence, create dir, write file
91
+ const cmd = `is_update=$(test -f ${escapedPath} && echo 1 || echo 0); ${mkdirPart}echo ${escapeForShell(encodedContent)} | base64 -d > ${escapedPath} && echo "STATUS:$is_update"`;
112
92
  const result = await ctx.client.sandboxes.execCommand({
113
93
  sandboxId: ctx.sandboxId,
114
- command: writeCmd,
94
+ command: cmd,
115
95
  timeout,
116
96
  });
117
- if (result.exit_code !== 0) {
97
+ if (result.exit_code !== 0 || !result.stdout.includes('STATUS:')) {
118
98
  return {
119
- error: `Failed to create file: ${path}`,
99
+ error: `Failed to create file: ${filePath}`,
120
100
  stderr: result.stderr,
121
101
  };
122
102
  }
103
+ const isUpdate = result.stdout.includes('STATUS:1');
123
104
  return {
124
105
  is_file_update: isUpdate,
125
- message: `File ${isUpdate ? 'updated' : 'created'}: ${path}`,
106
+ message: `File ${isUpdate ? 'updated' : 'created'}: ${filePath}`,
126
107
  };
127
108
  }
128
109
  /**
129
110
  * Replace a string in a file.
111
+ *
112
+ * Uses a Python script on the sandbox to avoid transferring the entire file.
113
+ * Only the base64-encoded oldStr and newStr are sent.
130
114
  */
131
- async function strReplace(ctx, path, oldStr, newStr, timeout) {
132
- // First read the file content
133
- const readCmd = `cat ${escapeForShell(path)}`;
115
+ async function strReplace(ctx, filePath, oldStr, newStr, timeout) {
116
+ const oldB64 = Buffer.from(oldStr, 'utf-8').toString('base64');
117
+ const newB64 = Buffer.from(newStr, 'utf-8').toString('base64');
118
+ // Write Python script and base64 encode it to avoid shell escaping issues
119
+ const pyScript = `import sys, base64, os
120
+ old = base64.b64decode("${oldB64}").decode()
121
+ new = base64.b64decode("${newB64}").decode()
122
+ path = "${filePath}"
123
+ if not os.path.exists(path):
124
+ print("FILE_NOT_FOUND")
125
+ sys.exit(1)
126
+ with open(path, "r") as f:
127
+ content = f.read()
128
+ count = content.count(old)
129
+ if count == 0:
130
+ print("NOT_FOUND")
131
+ sys.exit(0)
132
+ if count > 1:
133
+ print(f"MULTIPLE:{count}")
134
+ sys.exit(0)
135
+ with open(path, "w") as f:
136
+ f.write(content.replace(old, new, 1))
137
+ print("SUCCESS")
138
+ `;
139
+ const scriptB64 = Buffer.from(pyScript, 'utf-8').toString('base64');
140
+ const cmd = `echo ${escapeForShell(scriptB64)} | base64 -d | python3`;
134
141
  const result = await ctx.client.sandboxes.execCommand({
135
142
  sandboxId: ctx.sandboxId,
136
- command: readCmd,
143
+ command: cmd,
137
144
  timeout,
138
145
  });
139
- if (result.exit_code !== 0) {
140
- return {
141
- error: `File not found: ${path}`,
142
- stderr: result.stderr,
143
- };
144
- }
145
- const originalContent = result.stdout;
146
- // Check if oldStr exists in the file
147
- if (!originalContent.includes(oldStr)) {
148
- return {
149
- error: `String not found in file: ${oldStr.substring(0, 50)}...`,
150
- };
151
- }
152
- // Count occurrences
153
- let occurrences = 0;
154
- let searchIndex = 0;
155
- while (searchIndex < originalContent.length) {
156
- const foundIndex = originalContent.indexOf(oldStr, searchIndex);
157
- if (foundIndex === -1)
158
- break;
159
- occurrences++;
160
- searchIndex = foundIndex + oldStr.length;
161
- }
162
- if (occurrences > 1) {
163
- return {
164
- error: `Multiple occurrences (${occurrences}) of the string found. Please provide more context to make the match unique.`,
165
- };
146
+ const output = result.stdout.trim();
147
+ if (result.exit_code !== 0 || output === 'FILE_NOT_FOUND') {
148
+ return { error: `File not found: ${filePath}`, stderr: result.stderr };
166
149
  }
167
- // Perform the replacement
168
- const newContent = originalContent.replace(oldStr, newStr);
169
- // Find the line numbers affected
170
- const oldLines = originalContent.split('\n');
171
- const newLines = newContent.split('\n');
172
- // Find where the change starts
173
- let oldStart = 1;
174
- const minLen = Math.min(oldLines.length, newLines.length);
175
- for (let i = 0; i < minLen; i++) {
176
- if (oldLines[i] !== newLines[i]) {
177
- oldStart = i + 1;
178
- break;
179
- }
150
+ if (output === 'NOT_FOUND') {
151
+ return { error: `String not found in file: ${oldStr.substring(0, 50)}...` };
180
152
  }
181
- // Write the new content
182
- const encodedContent = Buffer.from(newContent, 'utf-8').toString('base64');
183
- const writeCmd = `echo ${escapeForShell(encodedContent)} | base64 -d > ${escapeForShell(path)}`;
184
- const writeResult = await ctx.client.sandboxes.execCommand({
185
- sandboxId: ctx.sandboxId,
186
- command: writeCmd,
187
- timeout,
188
- });
189
- if (writeResult.exit_code !== 0) {
153
+ if (output.startsWith('MULTIPLE:')) {
154
+ const count = output.split(':')[1];
190
155
  return {
191
- error: `Failed to write file: ${path}`,
192
- stderr: writeResult.stderr,
156
+ error: `Multiple occurrences (${count}) of the string found. Please provide more context to make the match unique.`,
193
157
  };
194
158
  }
195
- // Calculate diff info
196
- const oldStrLines = oldStr.split('\n').length;
197
- const newStrLines = newStr.split('\n').length;
198
- // Build diff lines
199
- const diffLines = [];
200
- for (const line of oldStr.split('\n')) {
201
- diffLines.push(`-${line}`);
159
+ if (output === 'SUCCESS') {
160
+ return { msg: 'Successfully replaced text at exactly one location.' };
202
161
  }
203
- for (const line of newStr.split('\n')) {
204
- diffLines.push(`+${line}`);
205
- }
206
- return {
207
- oldStart,
208
- oldLines: oldStrLines,
209
- newStart: oldStart,
210
- newLines: newStrLines,
211
- lines: diffLines,
212
- };
162
+ return { error: `Unexpected response: ${output}`, stderr: result.stderr };
213
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acontext/acontext",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "TypeScript SDK for the Acontext API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",