@acontext/acontext 0.1.0 → 0.1.2

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 (47) hide show
  1. package/dist/agent/base.d.ts +2 -1
  2. package/dist/agent/disk.d.ts +8 -7
  3. package/dist/agent/disk.js +16 -7
  4. package/dist/agent/index.d.ts +2 -0
  5. package/dist/agent/index.js +2 -0
  6. package/dist/agent/prompts.d.ts +6 -0
  7. package/dist/agent/prompts.js +102 -0
  8. package/dist/agent/sandbox.d.ts +102 -0
  9. package/dist/agent/sandbox.js +320 -0
  10. package/dist/agent/skill.d.ts +2 -8
  11. package/dist/agent/skill.js +26 -24
  12. package/dist/agent/text-editor.d.ts +44 -0
  13. package/dist/agent/text-editor.js +213 -0
  14. package/dist/client-types.d.ts +1 -0
  15. package/dist/client.d.ts +1 -4
  16. package/dist/client.js +4 -6
  17. package/dist/resources/index.d.ts +0 -2
  18. package/dist/resources/index.js +0 -2
  19. package/dist/resources/sandboxes.d.ts +18 -1
  20. package/dist/resources/sandboxes.js +24 -0
  21. package/dist/resources/sessions.d.ts +1 -17
  22. package/dist/resources/sessions.js +0 -26
  23. package/dist/resources/skills.d.ts +14 -1
  24. package/dist/resources/skills.js +17 -0
  25. package/dist/resources/users.d.ts +2 -2
  26. package/dist/resources/users.js +2 -2
  27. package/dist/types/index.d.ts +0 -2
  28. package/dist/types/index.js +0 -2
  29. package/dist/types/sandbox.d.ts +47 -0
  30. package/dist/types/sandbox.js +24 -1
  31. package/dist/types/session.d.ts +0 -12
  32. package/dist/types/session.js +1 -8
  33. package/dist/types/skill.d.ts +7 -0
  34. package/dist/types/skill.js +7 -1
  35. package/dist/types/tool.d.ts +0 -4
  36. package/dist/types/tool.js +1 -4
  37. package/dist/types/user.d.ts +0 -2
  38. package/dist/types/user.js +0 -1
  39. package/package.json +8 -8
  40. package/dist/resources/blocks.d.ts +0 -33
  41. package/dist/resources/blocks.js +0 -85
  42. package/dist/resources/spaces.d.ts +0 -68
  43. package/dist/resources/spaces.js +0 -109
  44. package/dist/types/block.d.ts +0 -18
  45. package/dist/types/block.js +0 -20
  46. package/dist/types/space.d.ts +0 -67
  47. package/dist/types/space.js +0 -44
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ /**
3
+ * Agent tools for sandbox operations using the Acontext Sandbox API.
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SANDBOX_TOOLS = exports.SandboxToolPool = exports.ExportSandboxFileTool = exports.TextEditorTool = exports.BashTool = void 0;
40
+ const path = __importStar(require("path"));
41
+ const base_1 = require("./base");
42
+ const prompts_1 = require("./prompts");
43
+ const text_editor_1 = require("./text-editor");
44
+ const MAX_OUTPUT_CHARS = 20000;
45
+ function truncateOutput(text, maxChars = MAX_OUTPUT_CHARS) {
46
+ if (text.length > maxChars) {
47
+ return text.slice(0, maxChars) + '...[truncated]';
48
+ }
49
+ return text;
50
+ }
51
+ function formatMountedSkills(mountedSkillPaths) {
52
+ if (mountedSkillPaths.size === 0) {
53
+ return '';
54
+ }
55
+ // Sort by skill name
56
+ const sortedSkills = Array.from(mountedSkillPaths.values()).sort((a, b) => a.name.localeCompare(b.name));
57
+ const skillEntries = sortedSkills.map((skill) => {
58
+ const location = path.posix.join(skill.basePath, 'SKILL.md');
59
+ return `<skill>
60
+ <name>${skill.name}</name>
61
+ <description>${skill.description}</description>
62
+ <location>${location}</location>
63
+ </skill>`;
64
+ });
65
+ return skillEntries.join('\n');
66
+ }
67
+ function getSandboxContextPrompt(mountedSkillPaths) {
68
+ let baseBody = `<text_editor_sandbox>
69
+ ${prompts_1.SANDBOX_TEXT_EDITOR_REMINDER}
70
+ </text_editor_sandbox>
71
+ <bash_execution_sandbox>
72
+ ${prompts_1.SANDBOX_BASH_REMINDER}
73
+ </bash_execution_sandbox>`;
74
+ if (mountedSkillPaths.size > 0) {
75
+ const formattedSkills = formatMountedSkills(mountedSkillPaths);
76
+ baseBody += `
77
+ <skills>
78
+ ${prompts_1.SKILL_REMINDER}
79
+ <available_skills>
80
+ ${formattedSkills}
81
+ </available_skills>
82
+ </skills>`;
83
+ }
84
+ return `<sandbox>
85
+ By default, you are in \`/workspace\`.
86
+ ${baseBody}
87
+ </sandbox>
88
+ `;
89
+ }
90
+ function normalizePath(path) {
91
+ if (!path) {
92
+ return '/';
93
+ }
94
+ let normalized = path.startsWith('/') ? path : `/${path}`;
95
+ if (!normalized.endsWith('/')) {
96
+ normalized += '/';
97
+ }
98
+ return normalized;
99
+ }
100
+ class BashTool extends base_1.AbstractBaseTool {
101
+ constructor(timeout) {
102
+ super();
103
+ this.name = 'bash_execution_sandbox';
104
+ this.description = 'The bash_execution_sandbox tool enables execution of bash scripts in a secure sandboxed container environment.';
105
+ this.arguments = {
106
+ command: {
107
+ type: 'string',
108
+ description: "The bash command to execute. Examples: 'ls -la', 'python3 script.py', 'sed -i 's/old_string/new_string/g' file.py'",
109
+ },
110
+ timeout: {
111
+ type: ['number', 'null'],
112
+ description: 'Optional timeout in seconds for this command. Use for long-running commands that may exceed the default timeout.',
113
+ },
114
+ };
115
+ this.requiredArguments = ['command'];
116
+ this._timeout = timeout;
117
+ }
118
+ async execute(ctx, llmArguments) {
119
+ const command = llmArguments.command;
120
+ const timeout = llmArguments.timeout ?? this._timeout;
121
+ if (!command) {
122
+ throw new Error('command is required');
123
+ }
124
+ const result = await ctx.client.sandboxes.execCommand({
125
+ sandboxId: ctx.sandboxId,
126
+ command,
127
+ timeout,
128
+ });
129
+ return JSON.stringify({
130
+ stdout: truncateOutput(result.stdout),
131
+ stderr: truncateOutput(result.stderr),
132
+ exit_code: result.exit_code,
133
+ });
134
+ }
135
+ }
136
+ exports.BashTool = BashTool;
137
+ class TextEditorTool extends base_1.AbstractBaseTool {
138
+ constructor(timeout) {
139
+ super();
140
+ this.name = 'text_editor_sandbox';
141
+ this.description = 'A tool for viewing, creating, and editing text files in the sandbox.';
142
+ this.arguments = {
143
+ command: {
144
+ type: 'string',
145
+ enum: ['view', 'create', 'str_replace'],
146
+ description: "The operation to perform: 'view', 'create', or 'str_replace'. " +
147
+ "Required parameters per command: " +
148
+ "'view' requires path (view_range is optional); " +
149
+ "'create' requires path and file_text; " +
150
+ "'str_replace' requires path, old_str, and new_str.",
151
+ },
152
+ path: {
153
+ type: 'string',
154
+ description: "Required for all commands. The file path in the sandbox (e.g., '/workspace/script.py')",
155
+ },
156
+ file_text: {
157
+ type: ['string', 'null'],
158
+ description: "Required for 'create' command. The content to write to the file.",
159
+ },
160
+ old_str: {
161
+ type: ['string', 'null'],
162
+ description: "Required for 'str_replace' command. The exact string to find and replace.",
163
+ },
164
+ new_str: {
165
+ type: ['string', 'null'],
166
+ description: "Required for 'str_replace' command. The string to replace old_str with.",
167
+ },
168
+ view_range: {
169
+ type: ['array', 'null'],
170
+ 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
+ };
173
+ this.requiredArguments = ['command', 'path'];
174
+ this._timeout = timeout;
175
+ }
176
+ async execute(ctx, llmArguments) {
177
+ const command = llmArguments.command;
178
+ const path = llmArguments.path;
179
+ if (!command) {
180
+ throw new Error('command is required');
181
+ }
182
+ if (!path) {
183
+ throw new Error('path is required');
184
+ }
185
+ if (command === 'view') {
186
+ const viewRange = llmArguments.view_range;
187
+ const result = await (0, text_editor_1.viewFile)(ctx, path, viewRange, this._timeout);
188
+ return JSON.stringify(result);
189
+ }
190
+ else if (command === 'create') {
191
+ const fileText = llmArguments.file_text;
192
+ if (fileText === null || fileText === undefined) {
193
+ throw new Error('file_text is required for create command');
194
+ }
195
+ const result = await (0, text_editor_1.createFile)(ctx, path, fileText, this._timeout);
196
+ return JSON.stringify(result);
197
+ }
198
+ else if (command === 'str_replace') {
199
+ const oldStr = llmArguments.old_str;
200
+ const newStr = llmArguments.new_str;
201
+ if (oldStr === null || oldStr === undefined) {
202
+ throw new Error('old_str is required for str_replace command');
203
+ }
204
+ if (newStr === null || newStr === undefined) {
205
+ throw new Error('new_str is required for str_replace command');
206
+ }
207
+ const result = await (0, text_editor_1.strReplace)(ctx, path, oldStr, newStr, this._timeout);
208
+ return JSON.stringify(result);
209
+ }
210
+ else {
211
+ throw new Error(`Unknown command: ${command}. Must be 'view', 'create', or 'str_replace'`);
212
+ }
213
+ }
214
+ }
215
+ exports.TextEditorTool = TextEditorTool;
216
+ class ExportSandboxFileTool extends base_1.AbstractBaseTool {
217
+ constructor() {
218
+ super(...arguments);
219
+ this.name = 'export_file_sandbox';
220
+ this.description = `Export a file from the sandbox to persistent, shared disk storage, and return you a public download URL.
221
+ If the sandbox file is changed, the disk file won't be updated unless you export the file again.`;
222
+ this.arguments = {
223
+ sandbox_path: {
224
+ type: 'string',
225
+ description: "The directory path in the sandbox where the file is located. Must end with '/'. Examples: '/workspace/', '/home/user/output/'",
226
+ },
227
+ sandbox_filename: {
228
+ type: 'string',
229
+ description: 'The name of the file to export from the sandbox.',
230
+ },
231
+ };
232
+ this.requiredArguments = ['sandbox_path', 'sandbox_filename'];
233
+ }
234
+ async execute(ctx, llmArguments) {
235
+ const sandboxPath = llmArguments.sandbox_path;
236
+ const sandboxFilename = llmArguments.sandbox_filename;
237
+ const diskPath = '/artifacts/';
238
+ if (!sandboxPath) {
239
+ throw new Error('sandbox_path is required');
240
+ }
241
+ if (!sandboxFilename) {
242
+ throw new Error('sandbox_filename is required');
243
+ }
244
+ const normalizedSandboxPath = normalizePath(sandboxPath);
245
+ const normalizedDiskPath = normalizePath(diskPath);
246
+ const artifact = await ctx.client.disks.artifacts.uploadFromSandbox(ctx.diskId, {
247
+ sandboxId: ctx.sandboxId,
248
+ sandboxPath: normalizedSandboxPath,
249
+ sandboxFilename,
250
+ filePath: normalizedDiskPath,
251
+ });
252
+ // Get the public URL for the uploaded artifact
253
+ const artifactInfo = await ctx.client.disks.artifacts.get(ctx.diskId, {
254
+ filePath: artifact.path,
255
+ filename: artifact.filename,
256
+ withPublicUrl: true,
257
+ withContent: false,
258
+ });
259
+ return JSON.stringify({
260
+ message: 'successfully exported file to disk',
261
+ public_url: artifactInfo.public_url,
262
+ });
263
+ }
264
+ }
265
+ exports.ExportSandboxFileTool = ExportSandboxFileTool;
266
+ class SandboxToolPool extends base_1.BaseToolPool {
267
+ /**
268
+ * Create a sandbox context.
269
+ *
270
+ * @param client - The Acontext client instance.
271
+ * @param sandboxId - The UUID of the sandbox.
272
+ * @param diskId - The UUID of the disk for file exports.
273
+ * @param mountSkills - Optional list of skill IDs to download to the sandbox.
274
+ * Skills are downloaded to /skills/{skill_name}/ in the sandbox.
275
+ * @returns Promise resolving to SandboxContext for use with sandbox tools.
276
+ */
277
+ async formatContext(client, sandboxId, diskId, mountSkills) {
278
+ const mountedSkillPaths = new Map();
279
+ const ctx = {
280
+ client,
281
+ sandboxId,
282
+ diskId,
283
+ mountedSkillPaths,
284
+ getContextPrompt() {
285
+ return getSandboxContextPrompt(mountedSkillPaths);
286
+ },
287
+ formatMountedSkills() {
288
+ return formatMountedSkills(mountedSkillPaths);
289
+ },
290
+ async mountSkills(skillIds) {
291
+ for (const skillId of skillIds) {
292
+ if (mountedSkillPaths.has(skillId)) {
293
+ // Skip already mounted skills
294
+ continue;
295
+ }
296
+ const result = await client.skills.downloadToSandbox(skillId, {
297
+ sandboxId,
298
+ });
299
+ if (result.success) {
300
+ mountedSkillPaths.set(skillId, {
301
+ basePath: result.dir_path,
302
+ name: result.name,
303
+ description: result.description,
304
+ });
305
+ }
306
+ }
307
+ },
308
+ };
309
+ if (mountSkills && mountSkills.length > 0) {
310
+ await ctx.mountSkills(mountSkills);
311
+ }
312
+ return ctx;
313
+ }
314
+ }
315
+ exports.SandboxToolPool = SandboxToolPool;
316
+ // Pre-configured tool pool with sandbox tools
317
+ exports.SANDBOX_TOOLS = new SandboxToolPool();
318
+ exports.SANDBOX_TOOLS.addTool(new BashTool());
319
+ exports.SANDBOX_TOOLS.addTool(new TextEditorTool());
320
+ exports.SANDBOX_TOOLS.addTool(new ExportSandboxFileTool());
@@ -10,6 +10,7 @@ import { AbstractBaseTool, BaseContext, BaseToolPool } from './base';
10
10
  export interface SkillContext extends BaseContext {
11
11
  client: AcontextClient;
12
12
  skills: Map<string, Skill>;
13
+ getContextPrompt(): string;
13
14
  }
14
15
  /**
15
16
  * Create a SkillContext by preloading skills from a list of skill IDs.
@@ -33,13 +34,6 @@ export declare function getSkillFromContext(ctx: SkillContext, skillName: string
33
34
  * Return list of available skill names in this context.
34
35
  */
35
36
  export declare function listSkillNamesFromContext(ctx: SkillContext): string[];
36
- export declare class ListSkillsTool extends AbstractBaseTool {
37
- readonly name = "list_skills";
38
- readonly description = "List all available skills in the current context with their names and descriptions.";
39
- readonly arguments: {};
40
- readonly requiredArguments: string[];
41
- execute(ctx: SkillContext, _llmArguments: Record<string, unknown>): Promise<string>;
42
- }
43
37
  export declare class GetSkillTool extends AbstractBaseTool {
44
38
  readonly name = "get_skill";
45
39
  readonly description = "Get a skill by its name. Returns the skill information including the relative paths of the files and their mime type categories.";
@@ -65,7 +59,7 @@ export declare class GetSkillFileTool extends AbstractBaseTool {
65
59
  description: string;
66
60
  };
67
61
  expire: {
68
- type: string;
62
+ type: string[];
69
63
  description: string;
70
64
  };
71
65
  };
@@ -3,7 +3,7 @@
3
3
  * Skill tools for agent operations.
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SKILL_TOOLS = exports.SkillToolPool = exports.GetSkillFileTool = exports.GetSkillTool = exports.ListSkillsTool = void 0;
6
+ exports.SKILL_TOOLS = exports.SkillToolPool = exports.GetSkillFileTool = exports.GetSkillTool = void 0;
7
7
  exports.createSkillContext = createSkillContext;
8
8
  exports.getSkillFromContext = getSkillFromContext;
9
9
  exports.listSkillNamesFromContext = listSkillNamesFromContext;
@@ -27,7 +27,30 @@ async function createSkillContext(client, skillIds) {
27
27
  }
28
28
  skills.set(skill.name, skill);
29
29
  }
30
- return { client, skills };
30
+ return {
31
+ client,
32
+ skills,
33
+ getContextPrompt() {
34
+ if (skills.size === 0) {
35
+ return '';
36
+ }
37
+ const lines = ['<available_skills>'];
38
+ for (const [skillName, skill] of skills.entries()) {
39
+ lines.push('<skill>');
40
+ lines.push(`<name>${skillName}</name>`);
41
+ lines.push(`<description>${skill.description}</description>`);
42
+ lines.push('</skill>');
43
+ }
44
+ lines.push('</available_skills>');
45
+ const skillSection = lines.join('\n');
46
+ return `<skill_view>
47
+ Use get_skill and get_skill_file to view the available skills and their contexts.
48
+ Below is the list of available skills:
49
+ ${skillSection}
50
+ </skill_view>
51
+ `;
52
+ },
53
+ };
31
54
  }
32
55
  /**
33
56
  * Get a skill by name from the preloaded skills.
@@ -51,26 +74,6 @@ function getSkillFromContext(ctx, skillName) {
51
74
  function listSkillNamesFromContext(ctx) {
52
75
  return Array.from(ctx.skills.keys());
53
76
  }
54
- class ListSkillsTool extends base_1.AbstractBaseTool {
55
- constructor() {
56
- super(...arguments);
57
- this.name = 'list_skills';
58
- this.description = 'List all available skills in the current context with their names and descriptions.';
59
- this.arguments = {};
60
- this.requiredArguments = [];
61
- }
62
- async execute(ctx, _llmArguments) {
63
- if (ctx.skills.size === 0) {
64
- return 'No skills available in the current context.';
65
- }
66
- const skillList = [];
67
- for (const [skillName, skill] of ctx.skills.entries()) {
68
- skillList.push(`- ${skillName}: ${skill.description}`);
69
- }
70
- return `Available skills (${ctx.skills.size}):\n${skillList.join('\n')}`;
71
- }
72
- }
73
- exports.ListSkillsTool = ListSkillsTool;
74
77
  class GetSkillTool extends base_1.AbstractBaseTool {
75
78
  constructor() {
76
79
  super(...arguments);
@@ -124,7 +127,7 @@ class GetSkillFileTool extends base_1.AbstractBaseTool {
124
127
  description: "Relative path to the file within the skill (e.g., 'scripts/extract_text.json').",
125
128
  },
126
129
  expire: {
127
- type: 'number',
130
+ type: ['integer', 'null'],
128
131
  description: 'URL expiration time in seconds (only used for non-parseable files). Defaults to 900 (15 minutes).',
129
132
  },
130
133
  };
@@ -179,6 +182,5 @@ class SkillToolPool extends base_1.BaseToolPool {
179
182
  }
180
183
  exports.SkillToolPool = SkillToolPool;
181
184
  exports.SKILL_TOOLS = new SkillToolPool();
182
- exports.SKILL_TOOLS.addTool(new ListSkillsTool());
183
185
  exports.SKILL_TOOLS.addTool(new GetSkillTool());
184
186
  exports.SKILL_TOOLS.addTool(new GetSkillFileTool());
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Text editor file operations for sandbox environments.
3
+ */
4
+ import type { SandboxContext } from './sandbox';
5
+ /**
6
+ * Escape a string for safe use in shell commands.
7
+ */
8
+ export declare function escapeForShell(s: string): string;
9
+ export interface ViewFileResult {
10
+ file_type?: string;
11
+ content?: string;
12
+ numLines?: number;
13
+ startLine?: number;
14
+ totalLines?: number;
15
+ error?: string;
16
+ stderr?: string;
17
+ }
18
+ export interface CreateFileResult {
19
+ is_file_update?: boolean;
20
+ message?: string;
21
+ error?: string;
22
+ stderr?: string;
23
+ }
24
+ export interface StrReplaceResult {
25
+ oldStart?: number;
26
+ oldLines?: number;
27
+ newStart?: number;
28
+ newLines?: number;
29
+ lines?: string[];
30
+ error?: string;
31
+ stderr?: string;
32
+ }
33
+ /**
34
+ * View file content with line numbers.
35
+ */
36
+ export declare function viewFile(ctx: SandboxContext, path: string, viewRange: number[] | null, timeout?: number): Promise<ViewFileResult>;
37
+ /**
38
+ * Create a new file with content.
39
+ */
40
+ export declare function createFile(ctx: SandboxContext, path: string, fileText: string, timeout?: number): Promise<CreateFileResult>;
41
+ /**
42
+ * Replace a string in a file.
43
+ */
44
+ export declare function strReplace(ctx: SandboxContext, path: string, oldStr: string, newStr: string, timeout?: number): Promise<StrReplaceResult>;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Text editor file operations for sandbox environments.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.escapeForShell = escapeForShell;
7
+ exports.viewFile = viewFile;
8
+ exports.createFile = createFile;
9
+ exports.strReplace = strReplace;
10
+ const MAX_CONTENT_CHARS = 20000;
11
+ /**
12
+ * Truncate text to maxChars, appending a truncation flag if needed.
13
+ */
14
+ function truncateContent(text, maxChars = MAX_CONTENT_CHARS) {
15
+ if (text.length > maxChars) {
16
+ return text.slice(0, maxChars) + '...[truncated]';
17
+ }
18
+ return text;
19
+ }
20
+ /**
21
+ * Escape a string for safe use in shell commands.
22
+ */
23
+ function escapeForShell(s) {
24
+ // Use single quotes and escape any single quotes in the string
25
+ return "'" + s.replace(/'/g, "'\"'\"'") + "'";
26
+ }
27
+ /**
28
+ * View file content with line numbers.
29
+ */
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;
49
+ let startLine;
50
+ if (viewRange && viewRange.length === 2) {
51
+ const [rangeStart, rangeEnd] = viewRange;
52
+ cmd = `sed -n '${rangeStart},${rangeEnd}p' ${escapeForShell(path)} | nl -ba -v ${rangeStart}`;
53
+ startLine = rangeStart;
54
+ }
55
+ else {
56
+ // Default to first 200 lines if no range specified
57
+ const maxLines = 200;
58
+ cmd = `head -n ${maxLines} ${escapeForShell(path)} | nl -ba`;
59
+ startLine = 1;
60
+ }
61
+ const result = await ctx.client.sandboxes.execCommand({
62
+ sandboxId: ctx.sandboxId,
63
+ command: cmd,
64
+ timeout,
65
+ });
66
+ if (result.exit_code !== 0) {
67
+ return {
68
+ error: `Failed to view file: ${path}`,
69
+ stderr: result.stderr,
70
+ };
71
+ }
72
+ // Count lines in output
73
+ const contentLines = result.stdout.trim()
74
+ ? result.stdout.trimEnd().split('\n')
75
+ : [];
76
+ const numLines = contentLines.length;
77
+ return {
78
+ file_type: 'text',
79
+ content: truncateContent(result.stdout),
80
+ numLines,
81
+ startLine: viewRange ? startLine : 1,
82
+ totalLines: totalLines + 1, // wc -l doesn't count last line without newline
83
+ };
84
+ }
85
+ /**
86
+ * Create a new file with content.
87
+ */
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
110
+ const encodedContent = Buffer.from(fileText, 'utf-8').toString('base64');
111
+ const writeCmd = `echo ${escapeForShell(encodedContent)} | base64 -d > ${escapeForShell(path)}`;
112
+ const result = await ctx.client.sandboxes.execCommand({
113
+ sandboxId: ctx.sandboxId,
114
+ command: writeCmd,
115
+ timeout,
116
+ });
117
+ if (result.exit_code !== 0) {
118
+ return {
119
+ error: `Failed to create file: ${path}`,
120
+ stderr: result.stderr,
121
+ };
122
+ }
123
+ return {
124
+ is_file_update: isUpdate,
125
+ message: `File ${isUpdate ? 'updated' : 'created'}: ${path}`,
126
+ };
127
+ }
128
+ /**
129
+ * Replace a string in a file.
130
+ */
131
+ async function strReplace(ctx, path, oldStr, newStr, timeout) {
132
+ // First read the file content
133
+ const readCmd = `cat ${escapeForShell(path)}`;
134
+ const result = await ctx.client.sandboxes.execCommand({
135
+ sandboxId: ctx.sandboxId,
136
+ command: readCmd,
137
+ timeout,
138
+ });
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
+ };
166
+ }
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
+ }
180
+ }
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) {
190
+ return {
191
+ error: `Failed to write file: ${path}`,
192
+ stderr: writeResult.stderr,
193
+ };
194
+ }
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}`);
202
+ }
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
+ };
213
+ }
@@ -12,5 +12,6 @@ export interface RequesterProtocol {
12
12
  contentType: string;
13
13
  }>;
14
14
  unwrap?: boolean;
15
+ timeout?: number;
15
16
  }): Promise<T>;
16
17
  }