fotric-claw 0.1.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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/backend/.env.example +26 -0
  4. package/backend/nest-cli.json +8 -0
  5. package/backend/package-lock.json +13239 -0
  6. package/backend/package.json +82 -0
  7. package/backend/src/agent/agent.module.ts +10 -0
  8. package/backend/src/agent/agent.service.ts +210 -0
  9. package/backend/src/agent/index.ts +4 -0
  10. package/backend/src/agent/llm.factory.ts +20 -0
  11. package/backend/src/agent/tools/fetch.tool.ts +128 -0
  12. package/backend/src/agent/tools/file-read.tool.ts +99 -0
  13. package/backend/src/agent/tools/index.ts +55 -0
  14. package/backend/src/agent/tools/node-repl.tool.ts +82 -0
  15. package/backend/src/agent/tools/rag.tool.ts +192 -0
  16. package/backend/src/agent/tools/shell.tool.ts +65 -0
  17. package/backend/src/app.module.ts +26 -0
  18. package/backend/src/chat/chat.controller.ts +34 -0
  19. package/backend/src/chat/chat.module.ts +12 -0
  20. package/backend/src/chat/chat.service.ts +52 -0
  21. package/backend/src/chat/dto/chat.dto.ts +12 -0
  22. package/backend/src/chat/dto/index.ts +1 -0
  23. package/backend/src/chat/index.ts +4 -0
  24. package/backend/src/config/config.controller.ts +92 -0
  25. package/backend/src/config/config.module.ts +7 -0
  26. package/backend/src/config/constants.ts +56 -0
  27. package/backend/src/config/index.ts +3 -0
  28. package/backend/src/files/files.controller.ts +87 -0
  29. package/backend/src/files/files.module.ts +7 -0
  30. package/backend/src/files/index.ts +2 -0
  31. package/backend/src/main.ts +21 -0
  32. package/backend/src/memory/index.ts +3 -0
  33. package/backend/src/memory/memory.module.ts +10 -0
  34. package/backend/src/memory/memory.service.ts +329 -0
  35. package/backend/src/memory/memory.types.ts +38 -0
  36. package/backend/src/sessions/default.json +7 -0
  37. package/backend/src/sessions/index.ts +2 -0
  38. package/backend/src/sessions/main_session.json +40 -0
  39. package/backend/src/sessions/sessions.controller.ts +25 -0
  40. package/backend/src/sessions/sessions.module.ts +9 -0
  41. package/backend/src/sessions/test.json +16 -0
  42. package/backend/src/skills/browser_search/SKILL.md +81 -0
  43. package/backend/src/skills/get_weather/SKILL.md +72 -0
  44. package/backend/src/skills/index.ts +3 -0
  45. package/backend/src/skills/skill.types.ts +27 -0
  46. package/backend/src/skills/skills.module.ts +8 -0
  47. package/backend/src/skills/skills.service.ts +139 -0
  48. package/backend/src/skills/web_search/SKILL.md +76 -0
  49. package/backend/src/workspace/AGENTS.md +47 -0
  50. package/backend/src/workspace/IDENTITY.md +32 -0
  51. package/backend/src/workspace/MEMORY.md +15 -0
  52. package/backend/src/workspace/SOUL.md +29 -0
  53. package/backend/src/workspace/USER.md +8 -0
  54. package/backend/tsconfig.build.json +4 -0
  55. package/backend/tsconfig.json +26 -0
  56. package/bin/fotric-claw.js +281 -0
  57. package/frontend/next.config.js +14 -0
  58. package/frontend/package-lock.json +5700 -0
  59. package/frontend/package.json +33 -0
  60. package/frontend/postcss.config.js +6 -0
  61. package/frontend/src/app/globals.css +41 -0
  62. package/frontend/src/app/layout.tsx +22 -0
  63. package/frontend/src/app/page.tsx +405 -0
  64. package/frontend/src/lib/api.ts +157 -0
  65. package/frontend/src/lib/utils.ts +3 -0
  66. package/frontend/tailwind.config.js +32 -0
  67. package/frontend/tsconfig.json +26 -0
  68. package/knowledge/README.md +21 -0
  69. package/package.json +49 -0
  70. package/scripts/init-skills.ts +95 -0
  71. package/storage/.gitkeep +5 -0
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "fotric-claw-backend",
3
+ "version": "0.1.0",
4
+ "description": "FotricCalw - A lightweight, transparent AI Agent system",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "build": "nest build",
10
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "start": "nest start",
12
+ "start:dev": "nest start --watch",
13
+ "start:debug": "nest start --debug --watch",
14
+ "start:prod": "node dist/main",
15
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16
+ "test": "jest",
17
+ "test:watch": "jest --watch",
18
+ "test:cov": "jest --coverage",
19
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20
+ "test:e2e": "jest --config ./test/jest-e2e.json"
21
+ },
22
+ "dependencies": {
23
+ "@langchain/community": "^0.3.0",
24
+ "@langchain/core": "^0.3.0",
25
+ "@langchain/langgraph": "^0.2.0",
26
+ "@langchain/openai": "^0.3.0",
27
+ "@nestjs/common": "^10.3.0",
28
+ "@nestjs/config": "^4.0.3",
29
+ "@nestjs/core": "^10.3.0",
30
+ "@nestjs/platform-express": "^10.3.0",
31
+ "@xenova/transformers": "^2.14.0",
32
+ "cheerio": "^1.0.0",
33
+ "front-matter": "^4.0.2",
34
+ "glob": "^10.3.10",
35
+ "html-to-text": "^9.0.5",
36
+ "llamaindex": "^0.4.0",
37
+ "pino": "^8.17.2",
38
+ "pino-pretty": "^10.3.1",
39
+ "reflect-metadata": "^0.2.2",
40
+ "rxjs": "^7.8.1",
41
+ "zod": "^3.22.4"
42
+ },
43
+ "devDependencies": {
44
+ "@nestjs/cli": "^10.3.0",
45
+ "@nestjs/schematics": "^10.1.0",
46
+ "@nestjs/testing": "^10.3.0",
47
+ "@types/express": "^4.17.21",
48
+ "@types/html-to-text": "^9.0.4",
49
+ "@types/jest": "^29.5.11",
50
+ "@types/node": "^20.11.0",
51
+ "@types/supertest": "^6.0.2",
52
+ "eslint": "^8.56.0",
53
+ "eslint-config-prettier": "^9.1.0",
54
+ "eslint-plugin-prettier": "^5.1.3",
55
+ "jest": "^29.7.0",
56
+ "prettier": "^3.2.4",
57
+ "source-map-support": "^0.5.21",
58
+ "supertest": "^6.3.4",
59
+ "ts-jest": "^29.1.1",
60
+ "ts-loader": "^9.5.1",
61
+ "ts-node": "^10.9.2",
62
+ "tsconfig-paths": "^4.2.0",
63
+ "typescript": "^5.3.3"
64
+ },
65
+ "jest": {
66
+ "moduleFileExtensions": [
67
+ "js",
68
+ "json",
69
+ "ts"
70
+ ],
71
+ "rootDir": "src",
72
+ "testRegex": ".*\\.spec\\.ts$",
73
+ "transform": {
74
+ "^.+\\.(t|j)s$": "ts-jest"
75
+ },
76
+ "collectCoverageFrom": [
77
+ "**/*.(t|j)s"
78
+ ],
79
+ "coverageDirectory": "../coverage",
80
+ "testEnvironment": "node"
81
+ }
82
+ }
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { AgentService } from './agent.service';
3
+ import { MemoryModule } from '../memory';
4
+
5
+ @Module({
6
+ imports: [MemoryModule],
7
+ providers: [AgentService],
8
+ exports: [AgentService],
9
+ })
10
+ export class AgentModule {}
@@ -0,0 +1,210 @@
1
+ import { ChatOpenAI } from '@langchain/openai';
2
+ import { BaseMessage, HumanMessage, AIMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
3
+ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
4
+ import { FOTRIC_CONFIG } from '../config';
5
+ import { createCoreTools, ToolsConfig } from './tools';
6
+ import { createLLM } from './llm.factory';
7
+ import { MemoryService } from '../memory';
8
+ import { SessionMessage } from '../memory/memory.types';
9
+
10
+ export interface AgentState {
11
+ messages: BaseMessage[];
12
+ sessionId: string;
13
+ }
14
+
15
+ @Injectable()
16
+ export class AgentService implements OnModuleInit {
17
+ private readonly logger = new Logger('FOTRIC-CLAW:Agent');
18
+ private toolsConfig?: ToolsConfig;
19
+
20
+ constructor(private readonly memoryService: MemoryService) {}
21
+
22
+ private getLLM(): ChatOpenAI {
23
+ return createLLM();
24
+ }
25
+
26
+ onModuleInit() {
27
+ this.logger.log('Agent service initialized');
28
+ }
29
+
30
+ setToolsConfig(config: ToolsConfig) {
31
+ this.toolsConfig = config;
32
+ }
33
+
34
+ private convertToLangChainMessages(messages: SessionMessage[]): BaseMessage[] {
35
+ if (!messages || !Array.isArray(messages)) {
36
+ return [];
37
+ }
38
+
39
+ return messages.map(msg => {
40
+ const content = msg.content || '';
41
+
42
+ switch (msg.role) {
43
+ case 'user':
44
+ return new HumanMessage(content);
45
+ case 'assistant':
46
+ return new AIMessage(content);
47
+ case 'system':
48
+ return new SystemMessage(content);
49
+ case 'tool':
50
+ return new ToolMessage({
51
+ content: content,
52
+ tool_call_id: msg.toolCallId || 'unknown',
53
+ });
54
+ default:
55
+ return new HumanMessage(content);
56
+ }
57
+ });
58
+ }
59
+
60
+ async *streamChat(
61
+ userMessage: string,
62
+ sessionId: string,
63
+ onToolCall?: (toolName: string, args: unknown) => void
64
+ ): AsyncGenerator<{ type: 'thought' | 'tool_call' | 'content' | 'done'; content: string; toolName?: string; toolArgs?: unknown }> {
65
+ this.logger.log(`Starting chat for session: ${sessionId}`);
66
+
67
+ const systemPrompt = await this.memoryService.buildSystemPrompt();
68
+
69
+ let session = await this.memoryService.getSession(sessionId);
70
+ if (!session) {
71
+ session = await this.memoryService.createSession(sessionId);
72
+ }
73
+
74
+ const history = this.convertToLangChainMessages(session.messages || []);
75
+
76
+ const tools = createCoreTools(this.toolsConfig);
77
+ const llm = this.getLLM();
78
+ const llmWithTools = llm.bindTools(tools);
79
+
80
+ const messages: BaseMessage[] = [
81
+ new SystemMessage(systemPrompt),
82
+ ...history,
83
+ new HumanMessage(userMessage),
84
+ ];
85
+
86
+ await this.memoryService.addMessageToSession(sessionId, {
87
+ role: 'user',
88
+ content: userMessage,
89
+ });
90
+
91
+ let iteration = 0;
92
+ const maxIterations = 10;
93
+ let lastContent = '';
94
+ let lastToolResult = '';
95
+
96
+ while (iteration < maxIterations) {
97
+ iteration++;
98
+
99
+ try {
100
+ const response = await llmWithTools.invoke(messages);
101
+
102
+ if (response.tool_calls && response.tool_calls.length > 0) {
103
+ for (const toolCall of response.tool_calls) {
104
+ const toolName = toolCall.name;
105
+ const toolArgs = toolCall.args;
106
+
107
+ yield {
108
+ type: 'tool_call',
109
+ content: `Using tool: ${toolName}`,
110
+ toolName,
111
+ toolArgs,
112
+ };
113
+
114
+ onToolCall?.(toolName, toolArgs);
115
+
116
+ const tool = tools.find(t => t.name === toolName);
117
+ if (tool) {
118
+ try {
119
+ const toolResult = await tool.invoke(toolArgs);
120
+ lastToolResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
121
+
122
+ yield {
123
+ type: 'thought',
124
+ content: `Tool ${toolName} result: ${lastToolResult.length > 500
125
+ ? lastToolResult.substring(0, 500) + '...'
126
+ : lastToolResult}`,
127
+ };
128
+
129
+ messages.push(response);
130
+ messages.push(new ToolMessage({
131
+ content: lastToolResult,
132
+ tool_call_id: toolCall.id || '',
133
+ }));
134
+ } catch (error) {
135
+ const errorMessage = error instanceof Error ? error.message : String(error);
136
+ messages.push(response);
137
+ messages.push(new ToolMessage({
138
+ content: `Error: ${errorMessage}`,
139
+ tool_call_id: toolCall.id || '',
140
+ }));
141
+ }
142
+ } else {
143
+ this.logger.warn(`Tool not found: ${toolName}`);
144
+ messages.push(response);
145
+ messages.push(new ToolMessage({
146
+ content: `Error: Tool '${toolName}' not found. Available tools: ${tools.map(t => t.name).join(', ')}`,
147
+ tool_call_id: toolCall.id || '',
148
+ }));
149
+ }
150
+ }
151
+ } else {
152
+ lastContent = typeof response.content === 'string'
153
+ ? response.content
154
+ : JSON.stringify(response.content);
155
+
156
+ yield {
157
+ type: 'content',
158
+ content: lastContent,
159
+ };
160
+
161
+ await this.memoryService.addMessageToSession(sessionId, {
162
+ role: 'assistant',
163
+ content: lastContent,
164
+ });
165
+
166
+ break;
167
+ }
168
+ } catch (error) {
169
+ const errorMessage = error instanceof Error ? error.message : String(error);
170
+ this.logger.error(`Agent error: ${errorMessage}`);
171
+
172
+ yield {
173
+ type: 'content',
174
+ content: `Error: ${errorMessage}`,
175
+ };
176
+ break;
177
+ }
178
+ }
179
+
180
+ if (iteration >= maxIterations) {
181
+ if (lastToolResult) {
182
+ lastContent = `任务已执行完成。工具执行结果:${lastToolResult.length > 1000 ? lastToolResult.substring(0, 1000) + '...' : lastToolResult}`;
183
+ } else {
184
+ lastContent = '操作已完成,但未能获取明确的响应。请尝试更具体的请求。';
185
+ }
186
+
187
+ yield {
188
+ type: 'content',
189
+ content: lastContent,
190
+ };
191
+
192
+ await this.memoryService.addMessageToSession(sessionId, {
193
+ role: 'assistant',
194
+ content: lastContent,
195
+ });
196
+ }
197
+
198
+ yield { type: 'done', content: '' };
199
+ }
200
+
201
+ async chat(userMessage: string, sessionId: string): Promise<string> {
202
+ let result = '';
203
+ for await (const chunk of this.streamChat(userMessage, sessionId)) {
204
+ if (chunk.type === 'content') {
205
+ result = chunk.content;
206
+ }
207
+ }
208
+ return result;
209
+ }
210
+ }
@@ -0,0 +1,4 @@
1
+ export * from './agent.service';
2
+ export * from './agent.module';
3
+ export * from './llm.factory';
4
+ export * from './tools';
@@ -0,0 +1,20 @@
1
+ import { ChatOpenAI } from '@langchain/openai';
2
+
3
+ export function createLLM(options?: {
4
+ temperature?: number;
5
+ maxTokens?: number;
6
+ }): ChatOpenAI {
7
+ const apiKey = process.env.FOTRIC_API_KEY || '';
8
+ const baseURL = process.env.FOTRIC_API_BASE_URL || 'https://api.openai.com/v1';
9
+ const modelName = process.env.FOTRIC_MODEL_NAME || 'gpt-4o';
10
+
11
+ return new ChatOpenAI({
12
+ modelName,
13
+ openAIApiKey: apiKey || 'placeholder',
14
+ configuration: {
15
+ baseURL,
16
+ },
17
+ temperature: options?.temperature ?? 0.7,
18
+ maxTokens: options?.maxTokens ?? 4096,
19
+ });
20
+ }
@@ -0,0 +1,128 @@
1
+ import { Tool } from '@langchain/core/tools';
2
+ import * as cheerio from 'cheerio';
3
+ import { htmlToText } from 'html-to-text';
4
+ import { FOTRIC_CONFIG } from '../../config';
5
+
6
+ export interface FetchURLInput {
7
+ url: string;
8
+ selector: string;
9
+ }
10
+
11
+ export class FetchURLTool extends Tool {
12
+ name = 'fetch_url';
13
+
14
+ description = `Fetch content from a URL and return it as cleaned text/markdown.
15
+ Input should be a JSON object with a "url" field (required) and optional "selector" field.
16
+ Example: {"url": "https://example.com"} or {"url": "https://example.com", "selector": "article"}
17
+ Returns the extracted text content from the webpage.`;
18
+
19
+ async _call(input: string): Promise<string> {
20
+ let parsedInput: FetchURLInput;
21
+
22
+ try {
23
+ parsedInput = JSON.parse(input);
24
+ } catch {
25
+ if (input.startsWith('http')) {
26
+ parsedInput = { url: input, selector: '' };
27
+ } else {
28
+ return 'Error: Invalid input. Please provide a JSON object with a "url" field.';
29
+ }
30
+ }
31
+
32
+ const { url, selector = '' } = parsedInput;
33
+
34
+ if (!url) {
35
+ return 'Error: URL is required.';
36
+ }
37
+
38
+ try {
39
+ new URL(url);
40
+ } catch {
41
+ return 'Error: Invalid URL format.';
42
+ }
43
+
44
+ try {
45
+ const response = await fetch(url, {
46
+ headers: {
47
+ 'User-Agent': 'Mozilla/5.0 (compatible; FotricCalw/0.1.0)',
48
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
49
+ },
50
+ signal: AbortSignal.timeout(30000),
51
+ });
52
+
53
+ if (!response.ok) {
54
+ return `Error: HTTP ${response.status} - ${response.statusText}`;
55
+ }
56
+
57
+ const contentType = response.headers.get('content-type') || '';
58
+
59
+ if (!contentType.includes('text/html') && !contentType.includes('application/json')) {
60
+ const text = await response.text();
61
+ if (text.length > 10000) {
62
+ return text.substring(0, 10000) + '\n...[content truncated]';
63
+ }
64
+ return text;
65
+ }
66
+
67
+ const html = await response.text();
68
+
69
+ if (contentType.includes('application/json')) {
70
+ try {
71
+ const json = JSON.parse(html);
72
+ const stringified = JSON.stringify(json, null, 2);
73
+ if (stringified.length > 10000) {
74
+ return stringified.substring(0, 10000) + '\n...[content truncated]';
75
+ }
76
+ return stringified;
77
+ } catch {
78
+ return html;
79
+ }
80
+ }
81
+
82
+ const $ = cheerio.load(html);
83
+
84
+ $('script, style, nav, footer, header, aside, noscript, iframe, svg').remove();
85
+
86
+ $('[class*="sidebar"], [class*="menu"], [class*="nav"], [class*="footer"], [class*="header"]').remove();
87
+ $('[id*="sidebar"], [id*="menu"], [id*="nav"], [id*="footer"], [id*="header"]').remove();
88
+
89
+ let contentElement = selector && selector.trim() ? $(selector) : $('body');
90
+
91
+ if (contentElement.length === 0) {
92
+ contentElement = $('body');
93
+ }
94
+
95
+ const contentHtml = contentElement.html() || '';
96
+
97
+ const text = htmlToText(contentHtml, {
98
+ wordwrap: false,
99
+ selectors: [
100
+ { selector: 'a', options: { ignoreHref: true } },
101
+ { selector: 'img', format: 'skip' },
102
+ { selector: 'h1', options: { uppercase: false } },
103
+ { selector: 'h2', options: { uppercase: false } },
104
+ { selector: 'h3', options: { uppercase: false } },
105
+ ],
106
+ });
107
+
108
+ const cleanedText = text
109
+ .replace(/\n{3,}/g, '\n\n')
110
+ .replace(/[ \t]{2,}/g, ' ')
111
+ .trim();
112
+
113
+ if (cleanedText.length > 15000) {
114
+ return cleanedText.substring(0, 15000) + '\n\n...[content truncated]';
115
+ }
116
+
117
+ return cleanedText || 'No content extracted from the page.';
118
+ } catch (error) {
119
+ if (error instanceof Error) {
120
+ if (error.name === 'AbortError' || error.message.includes('timeout')) {
121
+ return 'Error: Request timed out after 30 seconds.';
122
+ }
123
+ return `Error: ${error.message}`;
124
+ }
125
+ return `Error: ${String(error)}`;
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,99 @@
1
+ import { Tool } from '@langchain/core/tools';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import { FOTRIC_CONFIG } from '../../config';
5
+
6
+ export interface FileReadInput {
7
+ path: string;
8
+ encoding: string;
9
+ startLine: number;
10
+ endLine: number;
11
+ }
12
+
13
+ export class FileReadTool extends Tool {
14
+ name = 'read_file';
15
+
16
+ description = `Read the content of a file from the local file system.
17
+ Input should be a JSON object with a "path" field (required) and optional fields:
18
+ - encoding: file encoding (default: utf-8)
19
+ - startLine: start reading from this line number
20
+ - endLine: stop reading at this line number
21
+ Example: {"path": "./src/skills/example/SKILL.md"}
22
+ Returns the file content or error message.`;
23
+
24
+ private rootDir: string;
25
+
26
+ constructor(options?: { rootDir?: string }) {
27
+ super();
28
+ this.rootDir = path.resolve(options?.rootDir ?? FOTRIC_CONFIG.PATHS.ROOT_DIR);
29
+ }
30
+
31
+ private isPathSafe(filePath: string): boolean {
32
+ const resolvedPath = path.resolve(this.rootDir, filePath);
33
+ const relativePath = path.relative(this.rootDir, resolvedPath);
34
+ return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
35
+ }
36
+
37
+ async _call(input: string): Promise<string> {
38
+ let parsedInput: FileReadInput;
39
+
40
+ try {
41
+ parsedInput = JSON.parse(input);
42
+ } catch {
43
+ parsedInput = { path: input, encoding: 'utf-8', startLine: 0, endLine: 0 };
44
+ }
45
+
46
+ const { path: filePath, encoding = 'utf-8', startLine = 0, endLine = 0 } = parsedInput;
47
+
48
+ if (!filePath) {
49
+ return 'Error: File path is required.';
50
+ }
51
+
52
+ if (!this.isPathSafe(filePath)) {
53
+ return `Error: Access denied. Path must be within the project directory: ${this.rootDir}`;
54
+ }
55
+
56
+ const absolutePath = path.resolve(this.rootDir, filePath);
57
+
58
+ try {
59
+ const stats = await fs.stat(absolutePath);
60
+
61
+ if (stats.size > 1024 * 1024) {
62
+ return `Error: File is too large (${(stats.size / 1024 / 1024).toFixed(2)} MB). Maximum size is 1 MB.`;
63
+ }
64
+
65
+ const content = await fs.readFile(absolutePath, encoding as BufferEncoding);
66
+
67
+ if (startLine > 0 || endLine > 0) {
68
+ const lines = content.split('\n');
69
+ const start = Math.max(0, startLine > 0 ? startLine - 1 : 0);
70
+ const end = endLine > 0 ? Math.min(lines.length, endLine) : lines.length;
71
+ const selectedLines = lines.slice(start, end);
72
+ return selectedLines.join('\n');
73
+ }
74
+
75
+ if (content.length > 50000) {
76
+ return content.substring(0, 50000) + '\n\n...[content truncated]';
77
+ }
78
+
79
+ return content;
80
+ } catch (error) {
81
+ if (error instanceof Error) {
82
+ if ('code' in error) {
83
+ const nodeError = error as NodeJS.ErrnoException;
84
+ if (nodeError.code === 'ENOENT') {
85
+ return `Error: File not found: ${filePath}`;
86
+ }
87
+ if (nodeError.code === 'EACCES') {
88
+ return `Error: Permission denied: ${filePath}`;
89
+ }
90
+ if (nodeError.code === 'EISDIR') {
91
+ return `Error: Path is a directory, not a file: ${filePath}`;
92
+ }
93
+ }
94
+ return `Error: ${error.message}`;
95
+ }
96
+ return `Error: ${String(error)}`;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,55 @@
1
+ export { ShellTool } from './shell.tool';
2
+ export { NodeREPLTool } from './node-repl.tool';
3
+ export { FetchURLTool, type FetchURLInput } from './fetch.tool';
4
+ export { FileReadTool, type FileReadInput } from './file-read.tool';
5
+ export { HybridRetrievalTool, type SearchKnowledgeInput } from './rag.tool';
6
+
7
+ import { ShellTool } from './shell.tool';
8
+ import { NodeREPLTool } from './node-repl.tool';
9
+ import { FetchURLTool } from './fetch.tool';
10
+ import { FileReadTool } from './file-read.tool';
11
+ import { HybridRetrievalTool } from './rag.tool';
12
+ import { DynamicModule, Provider } from '@nestjs/common';
13
+
14
+ export interface ToolsConfig {
15
+ shell?: { timeout?: number; cwd?: string };
16
+ repl?: { timeout?: number };
17
+ fetch?: {};
18
+ fileRead?: { rootDir?: string };
19
+ rag?: { knowledgeDir?: string; storageDir?: string };
20
+ }
21
+
22
+ export const TOOLS_CONFIG = 'TOOLS_CONFIG';
23
+
24
+ export const coreToolsProviders: Provider[] = [
25
+ {
26
+ provide: ShellTool,
27
+ useFactory: (config?: ToolsConfig) => new ShellTool(config?.shell),
28
+ },
29
+ {
30
+ provide: NodeREPLTool,
31
+ useFactory: (config?: ToolsConfig) => new NodeREPLTool(config?.repl),
32
+ },
33
+ {
34
+ provide: FetchURLTool,
35
+ useFactory: () => new FetchURLTool(),
36
+ },
37
+ {
38
+ provide: FileReadTool,
39
+ useFactory: (config?: ToolsConfig) => new FileReadTool(config?.fileRead),
40
+ },
41
+ {
42
+ provide: HybridRetrievalTool,
43
+ useFactory: (config?: ToolsConfig) => new HybridRetrievalTool(config?.rag),
44
+ },
45
+ ];
46
+
47
+ export function createCoreTools(config?: ToolsConfig) {
48
+ return [
49
+ new ShellTool(config?.shell),
50
+ new NodeREPLTool(config?.repl),
51
+ new FetchURLTool(),
52
+ new FileReadTool(config?.fileRead),
53
+ new HybridRetrievalTool(config?.rag),
54
+ ];
55
+ }
@@ -0,0 +1,82 @@
1
+ import { Tool } from '@langchain/core/tools';
2
+ import { createContext, runInContext } from 'node:vm';
3
+ import { FOTRIC_CONFIG } from '../../config';
4
+
5
+ export class NodeREPLTool extends Tool {
6
+ name = 'node_repl';
7
+
8
+ description = `Execute JavaScript code in a sandboxed Node.js environment.
9
+ Input should be valid JavaScript code.
10
+ The sandbox provides access to common globals like console, Math, JSON, Array, Object, etc.
11
+ File system and network access are restricted.
12
+ Returns the result of the code execution or error message.`;
13
+
14
+ private timeout: number;
15
+ private context: Record<string, unknown>;
16
+
17
+ constructor(options?: { timeout?: number }) {
18
+ super();
19
+ this.timeout = options?.timeout ?? FOTRIC_CONFIG.REPL.TIMEOUT;
20
+ this.context = this.createSandboxContext();
21
+ }
22
+
23
+ private createSandboxContext(): Record<string, unknown> {
24
+ const sandbox: Record<string, unknown> = {};
25
+
26
+ for (const globalName of FOTRIC_CONFIG.REPL.ALLOWED_GLOBALS) {
27
+ if (globalName in global) {
28
+ sandbox[globalName] = (global as Record<string, unknown>)[globalName];
29
+ }
30
+ }
31
+
32
+ sandbox['setTimeout'] = setTimeout;
33
+ sandbox['setInterval'] = setInterval;
34
+ sandbox['clearTimeout'] = clearTimeout;
35
+ sandbox['clearInterval'] = clearInterval;
36
+
37
+ return sandbox;
38
+ }
39
+
40
+ async _call(code: string): Promise<string> {
41
+ try {
42
+ const wrappedCode = `
43
+ (async () => {
44
+ ${code}
45
+ })()
46
+ `;
47
+
48
+ const vmContext = createContext(this.context);
49
+
50
+ const result = await runInContext(wrappedCode, vmContext, {
51
+ timeout: this.timeout,
52
+ displayErrors: true,
53
+ });
54
+
55
+ if (result === undefined) {
56
+ return 'undefined';
57
+ }
58
+
59
+ if (typeof result === 'string') {
60
+ return result;
61
+ }
62
+
63
+ try {
64
+ const stringified = JSON.stringify(result, null, 2);
65
+ if (stringified && stringified.length > 10000) {
66
+ return stringified.substring(0, 10000) + '\n...[result truncated]';
67
+ }
68
+ return stringified;
69
+ } catch {
70
+ return String(result);
71
+ }
72
+ } catch (error) {
73
+ if (error instanceof Error) {
74
+ if (error.message.includes('timeout')) {
75
+ return `Error: Code execution timed out after ${this.timeout}ms`;
76
+ }
77
+ return `Error: ${error.message}`;
78
+ }
79
+ return `Error: ${String(error)}`;
80
+ }
81
+ }
82
+ }