@acontext/acontext 0.1.2 → 0.1.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.
package/dist/agent/sandbox.d.ts
CHANGED
package/dist/agent/sandbox.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: `
|
|
58
|
+
error: `File not found: ${filePath}`,
|
|
69
59
|
stderr: result.stderr,
|
|
70
60
|
};
|
|
71
61
|
}
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
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(
|
|
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,
|
|
89
|
-
|
|
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
|
-
|
|
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:
|
|
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: ${
|
|
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'}: ${
|
|
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,
|
|
132
|
-
|
|
133
|
-
const
|
|
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:
|
|
143
|
+
command: cmd,
|
|
137
144
|
timeout,
|
|
138
145
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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: `
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -9,11 +9,26 @@ export type MessageBlob = AcontextMessage | Record<string, unknown>;
|
|
|
9
9
|
export declare class SessionsAPI {
|
|
10
10
|
private requester;
|
|
11
11
|
constructor(requester: RequesterProtocol);
|
|
12
|
+
/**
|
|
13
|
+
* List all sessions in the project.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Options for listing sessions.
|
|
16
|
+
* @param options.user - Filter by user identifier.
|
|
17
|
+
* @param options.limit - Maximum number of sessions to return.
|
|
18
|
+
* @param options.cursor - Cursor for pagination.
|
|
19
|
+
* @param options.timeDesc - Order by created_at descending if true, ascending if false.
|
|
20
|
+
* @param options.filterByConfigs - Filter by session configs using JSONB containment.
|
|
21
|
+
* Only sessions where configs contains all key-value pairs in this object will be returned.
|
|
22
|
+
* Supports nested objects. Note: Matching is case-sensitive and type-sensitive.
|
|
23
|
+
* Sessions with NULL configs are excluded from filtered results.
|
|
24
|
+
* @returns ListSessionsOutput containing the list of sessions and pagination information.
|
|
25
|
+
*/
|
|
12
26
|
list(options?: {
|
|
13
27
|
user?: string | null;
|
|
14
28
|
limit?: number | null;
|
|
15
29
|
cursor?: string | null;
|
|
16
30
|
timeDesc?: boolean | null;
|
|
31
|
+
filterByConfigs?: Record<string, unknown> | null;
|
|
17
32
|
}): Promise<ListSessionsOutput>;
|
|
18
33
|
create(options?: {
|
|
19
34
|
user?: string | null;
|
|
@@ -11,11 +11,29 @@ class SessionsAPI {
|
|
|
11
11
|
constructor(requester) {
|
|
12
12
|
this.requester = requester;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* List all sessions in the project.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Options for listing sessions.
|
|
18
|
+
* @param options.user - Filter by user identifier.
|
|
19
|
+
* @param options.limit - Maximum number of sessions to return.
|
|
20
|
+
* @param options.cursor - Cursor for pagination.
|
|
21
|
+
* @param options.timeDesc - Order by created_at descending if true, ascending if false.
|
|
22
|
+
* @param options.filterByConfigs - Filter by session configs using JSONB containment.
|
|
23
|
+
* Only sessions where configs contains all key-value pairs in this object will be returned.
|
|
24
|
+
* Supports nested objects. Note: Matching is case-sensitive and type-sensitive.
|
|
25
|
+
* Sessions with NULL configs are excluded from filtered results.
|
|
26
|
+
* @returns ListSessionsOutput containing the list of sessions and pagination information.
|
|
27
|
+
*/
|
|
14
28
|
async list(options) {
|
|
15
29
|
const params = {};
|
|
16
30
|
if (options?.user) {
|
|
17
31
|
params.user = options.user;
|
|
18
32
|
}
|
|
33
|
+
// Handle filterByConfigs - JSON encode, skip empty object
|
|
34
|
+
if (options?.filterByConfigs && Object.keys(options.filterByConfigs).length > 0) {
|
|
35
|
+
params.filter_by_configs = JSON.stringify(options.filterByConfigs);
|
|
36
|
+
}
|
|
19
37
|
Object.assign(params, (0, utils_1.buildParams)({
|
|
20
38
|
limit: options?.limit ?? null,
|
|
21
39
|
cursor: options?.cursor ?? null,
|