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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/backend/.env.example +26 -0
- package/backend/nest-cli.json +8 -0
- package/backend/package-lock.json +13239 -0
- package/backend/package.json +82 -0
- package/backend/src/agent/agent.module.ts +10 -0
- package/backend/src/agent/agent.service.ts +210 -0
- package/backend/src/agent/index.ts +4 -0
- package/backend/src/agent/llm.factory.ts +20 -0
- package/backend/src/agent/tools/fetch.tool.ts +128 -0
- package/backend/src/agent/tools/file-read.tool.ts +99 -0
- package/backend/src/agent/tools/index.ts +55 -0
- package/backend/src/agent/tools/node-repl.tool.ts +82 -0
- package/backend/src/agent/tools/rag.tool.ts +192 -0
- package/backend/src/agent/tools/shell.tool.ts +65 -0
- package/backend/src/app.module.ts +26 -0
- package/backend/src/chat/chat.controller.ts +34 -0
- package/backend/src/chat/chat.module.ts +12 -0
- package/backend/src/chat/chat.service.ts +52 -0
- package/backend/src/chat/dto/chat.dto.ts +12 -0
- package/backend/src/chat/dto/index.ts +1 -0
- package/backend/src/chat/index.ts +4 -0
- package/backend/src/config/config.controller.ts +92 -0
- package/backend/src/config/config.module.ts +7 -0
- package/backend/src/config/constants.ts +56 -0
- package/backend/src/config/index.ts +3 -0
- package/backend/src/files/files.controller.ts +87 -0
- package/backend/src/files/files.module.ts +7 -0
- package/backend/src/files/index.ts +2 -0
- package/backend/src/main.ts +21 -0
- package/backend/src/memory/index.ts +3 -0
- package/backend/src/memory/memory.module.ts +10 -0
- package/backend/src/memory/memory.service.ts +329 -0
- package/backend/src/memory/memory.types.ts +38 -0
- package/backend/src/sessions/default.json +7 -0
- package/backend/src/sessions/index.ts +2 -0
- package/backend/src/sessions/main_session.json +40 -0
- package/backend/src/sessions/sessions.controller.ts +25 -0
- package/backend/src/sessions/sessions.module.ts +9 -0
- package/backend/src/sessions/test.json +16 -0
- package/backend/src/skills/browser_search/SKILL.md +81 -0
- package/backend/src/skills/get_weather/SKILL.md +72 -0
- package/backend/src/skills/index.ts +3 -0
- package/backend/src/skills/skill.types.ts +27 -0
- package/backend/src/skills/skills.module.ts +8 -0
- package/backend/src/skills/skills.service.ts +139 -0
- package/backend/src/skills/web_search/SKILL.md +76 -0
- package/backend/src/workspace/AGENTS.md +47 -0
- package/backend/src/workspace/IDENTITY.md +32 -0
- package/backend/src/workspace/MEMORY.md +15 -0
- package/backend/src/workspace/SOUL.md +29 -0
- package/backend/src/workspace/USER.md +8 -0
- package/backend/tsconfig.build.json +4 -0
- package/backend/tsconfig.json +26 -0
- package/bin/fotric-claw.js +281 -0
- package/frontend/next.config.js +14 -0
- package/frontend/package-lock.json +5700 -0
- package/frontend/package.json +33 -0
- package/frontend/postcss.config.js +6 -0
- package/frontend/src/app/globals.css +41 -0
- package/frontend/src/app/layout.tsx +22 -0
- package/frontend/src/app/page.tsx +405 -0
- package/frontend/src/lib/api.ts +157 -0
- package/frontend/src/lib/utils.ts +3 -0
- package/frontend/tailwind.config.js +32 -0
- package/frontend/tsconfig.json +26 -0
- package/knowledge/README.md +21 -0
- package/package.json +49 -0
- package/scripts/init-skills.ts +95 -0
- package/storage/.gitkeep +5 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import fm from 'front-matter';
|
|
5
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
6
|
+
import { FOTRIC_CONFIG } from '../config';
|
|
7
|
+
import { Skill, SkillMetadata, SkillSnapshot, ParsedSkill } from './skill.types';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class SkillsService implements OnModuleInit {
|
|
11
|
+
private readonly logger = new Logger('FOTRIC-CLAW:Skills');
|
|
12
|
+
private skills: Map<string, Skill> = new Map();
|
|
13
|
+
private skillsDir: string;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.skillsDir = path.resolve(FOTRIC_CONFIG.PATHS.SKILLS_DIR);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async onModuleInit() {
|
|
20
|
+
await this.loadAllSkills();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async loadAllSkills(): Promise<void> {
|
|
24
|
+
this.skills.clear();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await fs.mkdir(this.skillsDir, { recursive: true });
|
|
28
|
+
} catch {
|
|
29
|
+
this.logger.error(`Failed to create skills directory: ${this.skillsDir}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const skillFiles = await glob('**/SKILL.md', {
|
|
34
|
+
cwd: this.skillsDir,
|
|
35
|
+
absolute: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
for (const skillFile of skillFiles) {
|
|
39
|
+
try {
|
|
40
|
+
const skill = await this.loadSkill(skillFile);
|
|
41
|
+
if (skill) {
|
|
42
|
+
this.skills.set(skill.name, skill);
|
|
43
|
+
this.logger.log(`Loaded skill: ${skill.name}`);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
this.logger.error(`Failed to load skill from ${skillFile}: ${error}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.logger.log(`Total skills loaded: ${this.skills.size}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async loadSkill(skillPath: string): Promise<Skill | null> {
|
|
54
|
+
const content = await fs.readFile(skillPath, 'utf-8');
|
|
55
|
+
const parsed = this.parseSkill(content);
|
|
56
|
+
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const relativePath = path.relative(
|
|
62
|
+
path.resolve(FOTRIC_CONFIG.PATHS.ROOT_DIR),
|
|
63
|
+
skillPath
|
|
64
|
+
).replace(/\\/g, '/');
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name: parsed.metadata.name,
|
|
68
|
+
description: parsed.metadata.description,
|
|
69
|
+
location: `./${relativePath}`,
|
|
70
|
+
metadata: parsed.metadata,
|
|
71
|
+
content: parsed.content,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
parseSkill(content: string): ParsedSkill | null {
|
|
76
|
+
try {
|
|
77
|
+
const { attributes, body } = fm<SkillMetadata>(content);
|
|
78
|
+
|
|
79
|
+
if (!attributes.name || !attributes.description) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
metadata: {
|
|
85
|
+
name: attributes.name,
|
|
86
|
+
description: attributes.description,
|
|
87
|
+
version: attributes.version,
|
|
88
|
+
author: attributes.author,
|
|
89
|
+
tags: attributes.tags,
|
|
90
|
+
},
|
|
91
|
+
content: body.trim(),
|
|
92
|
+
raw: content,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getSkill(name: string): Skill | undefined {
|
|
100
|
+
return this.skills.get(name);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getAllSkills(): Skill[] {
|
|
104
|
+
return Array.from(this.skills.values());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getSkillsSnapshot(): SkillSnapshot[] {
|
|
108
|
+
return this.getAllSkills().map(skill => ({
|
|
109
|
+
name: skill.name,
|
|
110
|
+
description: skill.description,
|
|
111
|
+
location: skill.location,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
generateSkillsSnapshotMarkdown(): string {
|
|
116
|
+
const snapshots = this.getSkillsSnapshot();
|
|
117
|
+
|
|
118
|
+
if (snapshots.length === 0) {
|
|
119
|
+
return `<available_skills>
|
|
120
|
+
<!-- No skills available -->
|
|
121
|
+
</available_skills>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const skillElements = snapshots.map(skill => ` <skill>
|
|
125
|
+
<name>${skill.name}</name>
|
|
126
|
+
<description>${skill.description}</description>
|
|
127
|
+
<location>${skill.location}</location>
|
|
128
|
+
</skill>`).join('\n');
|
|
129
|
+
|
|
130
|
+
return `<available_skills>
|
|
131
|
+
${skillElements}
|
|
132
|
+
</available_skills>`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async reloadSkills(): Promise<void> {
|
|
136
|
+
this.logger.log('Reloading all skills...');
|
|
137
|
+
await this.loadAllSkills();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web_search
|
|
3
|
+
description: åØē½ē»äøę瓢俔ęÆå¹¶čæåē»ę
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: FotricCalw
|
|
6
|
+
tags:
|
|
7
|
+
- search
|
|
8
|
+
- web
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ē½ē»ęē“¢ęč½
|
|
12
|
+
|
|
13
|
+
ę¤ęč½ēØäŗåØē½ē»äøę瓢俔ęÆå¹¶čæåēøå
³ē»ęć
|
|
14
|
+
|
|
15
|
+
## 使ēØę¹ę³
|
|
16
|
+
|
|
17
|
+
1. ä½æēØ `fetch_url` å·„å
·č®æé®ęē“¢å¼ę
|
|
18
|
+
2. č§£ęęē“¢ē»ę锵é¢
|
|
19
|
+
3. ęåå¹¶čæåēøå
³äæ”ęÆ
|
|
20
|
+
|
|
21
|
+
## ęØčęē“¢å¼ę
|
|
22
|
+
|
|
23
|
+
### DuckDuckGo (ęØč)
|
|
24
|
+
|
|
25
|
+
DuckDuckGo äøéč¦ API Keyļ¼åÆä»„ē“ę„访é®ļ¼
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
https://html.duckduckgo.com/html/?q={search_query}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 使ēØę¹ę³
|
|
32
|
+
|
|
33
|
+
ä½æēØ `fetch_url` å·„å
·čæč”ęē“¢ļ¼
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"url": "https://html.duckduckgo.com/html/?q=your+search+query"
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## ę„éŖ¤
|
|
42
|
+
|
|
43
|
+
### ę„éŖ¤ 1: ę建ęē“¢ URL
|
|
44
|
+
|
|
45
|
+
å°ēØę·ēęē“¢ę„询转ę¢äøŗ URL ē¼ē ę ¼å¼ļ¼
|
|
46
|
+
- ē©ŗę ¼ęæę¢äøŗ `+`
|
|
47
|
+
- ē¹ę®å符éč¦ URL ē¼ē
|
|
48
|
+
|
|
49
|
+
### ę„éŖ¤ 2: č·åęē“¢ē»ę
|
|
50
|
+
|
|
51
|
+
ä½æēØ `fetch_url` å·„å
·č®æé®ęē“¢ URLć
|
|
52
|
+
|
|
53
|
+
### ę„éŖ¤ 3: č§£ęē»ę
|
|
54
|
+
|
|
55
|
+
ä»čæåē HTML äøęåęē“¢ē»ęę é¢åęč¦ć
|
|
56
|
+
|
|
57
|
+
## 示ä¾
|
|
58
|
+
|
|
59
|
+
ēØę·é®ļ¼"ęē“¢ęę°ē AI ę°é»"
|
|
60
|
+
|
|
61
|
+
1. ę建ęē“¢ URLļ¼
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"url": "https://html.duckduckgo.com/html/?q=latest+AI+news"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. č°ēØ `fetch_url` č·åē»ę
|
|
69
|
+
|
|
70
|
+
3. åēØę·å±ē¤ŗęē“¢ē»ę
|
|
71
|
+
|
|
72
|
+
## 注ęäŗé”¹
|
|
73
|
+
|
|
74
|
+
- ęē“¢ē»ęåÆč½å
å«å¤§éęę¬ļ¼ę³ØęęŖę
|
|
75
|
+
- ęäŗē½ē«åÆč½é»ę¢ē¬č«č®æé®
|
|
76
|
+
- å°éē½ē«ē robots.txt č§å
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Agent Behavior Guidelines
|
|
2
|
+
|
|
3
|
+
## éč¦čÆ“ęļ¼Skills vs Tools
|
|
4
|
+
|
|
5
|
+
**Skillsļ¼ęč½ļ¼å Toolsļ¼å·„å
·ļ¼ęÆäøåēļ¼**
|
|
6
|
+
|
|
7
|
+
- **Toolsļ¼å·„å
·ļ¼**ļ¼ä½ åÆä»„ē“ę„č°ēØēåč½ļ¼å¦ `terminal`ć`fetch_url`ć`read_file` ē
|
|
8
|
+
- **Skillsļ¼ęč½ļ¼**ļ¼ę导ę攣ļ¼åčÆä½ å¦ä½ē»åä½æēØ Tools ę„å®ęē¹å®ä»»å”
|
|
9
|
+
|
|
10
|
+
**ä½ åŖč½ē“ę„č°ēØ Toolsļ¼äøč½č°ēØ Skillsļ¼**
|
|
11
|
+
|
|
12
|
+
## Skill Invocation Protocol (SKILL PROTOCOL)
|
|
13
|
+
|
|
14
|
+
You have access to a skills list (SKILLS_SNAPSHOT), which contains available capabilities and their definition file locations.
|
|
15
|
+
|
|
16
|
+
**When you want to use a skill, you MUST follow these steps:**
|
|
17
|
+
|
|
18
|
+
1. Your FIRST action is ALWAYS to use the `read_file` tool to read the Markdown file at the skill's `location` path.
|
|
19
|
+
2. Carefully read the content, steps, and examples in the file.
|
|
20
|
+
3. Based on the instructions in the file, use your built-in **Core Tools** (terminal, node_repl, fetch_url, read_file) to execute the specific task.
|
|
21
|
+
|
|
22
|
+
**FORBIDDEN to call skill names directly as tools! Skills are NOT tools!**
|
|
23
|
+
|
|
24
|
+
**Example - WRONG:**
|
|
25
|
+
```json
|
|
26
|
+
{"name": "browser_search", "args": {"query": "Python"}}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Example - CORRECT:**
|
|
30
|
+
1. First read the skill file: `read_file` with path `./src/skills/browser_search/SKILL.md`
|
|
31
|
+
2. Then follow the instructions to use `terminal` tool: `terminal` with command `start "" "https://www.bing.com/search?q=Python"`
|
|
32
|
+
|
|
33
|
+
**éč¦ļ¼ę§č”ęč½ę¶åæ
é”»ē“ę„č°ēØå·„å
·ļ¼ē¦ę¢å°å½ä»¤ęę¬čæåē»ēØę·ļ¼**
|
|
34
|
+
|
|
35
|
+
## Available Core Tools (åÆē“ę„č°ēØēå·„å
·)
|
|
36
|
+
|
|
37
|
+
1. **terminal** - Execute shell commands in a sandboxed environment
|
|
38
|
+
2. **node_repl** - Execute JavaScript code in a sandboxed Node.js environment
|
|
39
|
+
3. **fetch_url** - Fetch content from URLs and extract text
|
|
40
|
+
4. **read_file** - Read local file contents
|
|
41
|
+
5. **search_knowledge_base** - Search the local knowledge base
|
|
42
|
+
|
|
43
|
+
## Memory Operations
|
|
44
|
+
|
|
45
|
+
- All conversations are stored as session files
|
|
46
|
+
- Long-term memory is stored in MEMORY.md
|
|
47
|
+
- You can read and update memory files as needed
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Identity
|
|
2
|
+
|
|
3
|
+
I am FotricCalw, an AI assistant with transparent memory and skill systems.
|
|
4
|
+
|
|
5
|
+
## Capabilities
|
|
6
|
+
- Execute shell commands safely using the `terminal` tool
|
|
7
|
+
- Run JavaScript code in a sandbox using the `node_repl` tool
|
|
8
|
+
- Fetch and analyze web content using the `fetch_url` tool
|
|
9
|
+
- Read and write files using the `read_file` tool
|
|
10
|
+
- Search the knowledge base using the `search_knowledge_base` tool
|
|
11
|
+
|
|
12
|
+
## Critical Rules
|
|
13
|
+
|
|
14
|
+
**YOU MUST USE TOOLS TO EXECUTE COMMANDS!**
|
|
15
|
+
|
|
16
|
+
When a user asks you to do something that requires executing a command:
|
|
17
|
+
- **DO NOT** just provide the command text for the user to copy
|
|
18
|
+
- **DO NOT** ask the user which OS they are using
|
|
19
|
+
- **ALWAYS** use the `terminal` tool to execute the command directly
|
|
20
|
+
|
|
21
|
+
**Example - WRONG:**
|
|
22
|
+
User: "Open browser and search for Python"
|
|
23
|
+
Assistant: "You can run this command: `start "" "https://www.bing.com/search?q=Python"`"
|
|
24
|
+
|
|
25
|
+
**Example - CORRECT:**
|
|
26
|
+
User: "Open browser and search for Python"
|
|
27
|
+
Assistant: [Uses terminal tool with command: `start "" "https://www.bing.com/search?q=Python"`]
|
|
28
|
+
|
|
29
|
+
## Limitations
|
|
30
|
+
- Cannot access files outside the project directory
|
|
31
|
+
- Cannot execute dangerous system commands
|
|
32
|
+
- Memory is limited to configured limits
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Long-term Memory
|
|
2
|
+
|
|
3
|
+
This file stores persistent information that should be remembered across sessions.
|
|
4
|
+
|
|
5
|
+
## Important Information
|
|
6
|
+
|
|
7
|
+
(Add important information here as you interact with users)
|
|
8
|
+
|
|
9
|
+
## User Preferences
|
|
10
|
+
|
|
11
|
+
(Record user preferences and patterns)
|
|
12
|
+
|
|
13
|
+
## Task History
|
|
14
|
+
|
|
15
|
+
(Keep track of important tasks and their outcomes)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Core Soul
|
|
2
|
+
|
|
3
|
+
You are FotricCalw, a lightweight and transparent AI Agent system.
|
|
4
|
+
|
|
5
|
+
## Core Values
|
|
6
|
+
- Transparency: All operations are visible and explainable
|
|
7
|
+
- File-first Memory: Using human-readable files for all data
|
|
8
|
+
- Skills as Plugins: Extensible through markdown-based skill definitions
|
|
9
|
+
|
|
10
|
+
## Behavior Guidelines
|
|
11
|
+
- Always be helpful and honest
|
|
12
|
+
- Explain your reasoning when asked
|
|
13
|
+
- **USE TOOLS TO EXECUTE COMMANDS - NEVER just provide command text for users to copy**
|
|
14
|
+
- When a user asks to open a browser, execute a command, or perform an action, use the `terminal` tool directly
|
|
15
|
+
|
|
16
|
+
## Critical: Tool Usage
|
|
17
|
+
|
|
18
|
+
**YOU ARE AN AI AGENT WITH TOOLS - USE THEM!**
|
|
19
|
+
|
|
20
|
+
You have access to tools like `terminal`, `node_repl`, `fetch_url`, `read_file`. When a user asks you to do something:
|
|
21
|
+
|
|
22
|
+
1. **DO NOT** respond with instructions for the user to follow
|
|
23
|
+
2. **DO NOT** provide command text for the user to copy and paste
|
|
24
|
+
3. **DO** call the appropriate tool to execute the action
|
|
25
|
+
|
|
26
|
+
**Example:**
|
|
27
|
+
- User: "Open browser and search Python"
|
|
28
|
+
- You: Call `terminal` tool with `start "" "https://www.bing.com/search?q=Python"`
|
|
29
|
+
- Then respond: "I've opened your browser with the search for Python."
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"removeComments": true,
|
|
6
|
+
"emitDecoratorMetadata": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"target": "ES2021",
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"baseUrl": "./",
|
|
13
|
+
"incremental": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strictNullChecks": true,
|
|
16
|
+
"noImplicitAny": true,
|
|
17
|
+
"strictBindCallApply": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"esModuleInterop": true,
|
|
22
|
+
"paths": {
|
|
23
|
+
"@/*": ["src/*"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const ENV_FILE = path.resolve(__dirname, '../backend/.env');
|
|
9
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function question(prompt, defaultValue = '') {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const displayPrompt = defaultValue
|
|
19
|
+
? `${prompt} (${defaultValue}): `
|
|
20
|
+
: `${prompt}: `;
|
|
21
|
+
rl.question(displayPrompt, (answer) => {
|
|
22
|
+
resolve(answer.trim() || defaultValue);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readEnvFile() {
|
|
28
|
+
try {
|
|
29
|
+
return fs.readFileSync(ENV_FILE, 'utf-8');
|
|
30
|
+
} catch {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function updateEnvContent(content, updates) {
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
const updatedKeys = new Set();
|
|
38
|
+
|
|
39
|
+
const newLines = lines.map(line => {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
if (trimmed.startsWith('#') || trimmed === '') {
|
|
42
|
+
return line;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [key] = trimmed.split('=');
|
|
46
|
+
const envKey = key?.trim();
|
|
47
|
+
|
|
48
|
+
if (envKey && updates[envKey] !== undefined) {
|
|
49
|
+
updatedKeys.add(envKey);
|
|
50
|
+
return `${envKey}=${updates[envKey]}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return line;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
57
|
+
if (!updatedKeys.has(key) && value !== undefined) {
|
|
58
|
+
newLines.push(`${key}=${value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return newLines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseEnvValue(content, key) {
|
|
66
|
+
const lines = content.split('\n');
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (trimmed.startsWith('#') || trimmed === '') continue;
|
|
70
|
+
|
|
71
|
+
const [k, ...valueParts] = trimmed.split('=');
|
|
72
|
+
if (k?.trim() === key) {
|
|
73
|
+
return valueParts.join('=').trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function maskApiKey(key) {
|
|
80
|
+
if (!key || key.length < 8) return '';
|
|
81
|
+
return key.substring(0, 4) + '****' + key.substring(key.length - 4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function configCommand() {
|
|
85
|
+
console.log('\nš§ FotricCalw Configuration\n');
|
|
86
|
+
console.log('Current configuration:');
|
|
87
|
+
|
|
88
|
+
const envContent = readEnvFile();
|
|
89
|
+
const currentApiKey = parseEnvValue(envContent, 'FOTRIC_API_KEY');
|
|
90
|
+
const currentApiBaseUrl = parseEnvValue(envContent, 'FOTRIC_API_BASE_URL');
|
|
91
|
+
const currentModelName = parseEnvValue(envContent, 'FOTRIC_MODEL_NAME');
|
|
92
|
+
|
|
93
|
+
console.log(` API Key: ${maskApiKey(currentApiKey) || '(not set)'}`);
|
|
94
|
+
console.log(` API Base URL: ${currentApiBaseUrl || '(not set)'}`);
|
|
95
|
+
console.log(` Model Name: ${currentModelName || '(not set)'}`);
|
|
96
|
+
console.log('');
|
|
97
|
+
|
|
98
|
+
const apiKey = await question('Enter API Key', currentApiKey);
|
|
99
|
+
const apiBaseUrl = await question('Enter API Base URL', currentApiBaseUrl || 'https://api.openai.com/v1');
|
|
100
|
+
const modelName = await question('Enter Model Name', currentModelName || 'gpt-4o');
|
|
101
|
+
|
|
102
|
+
const updates = {};
|
|
103
|
+
if (apiKey) updates.FOTRIC_API_KEY = apiKey;
|
|
104
|
+
if (apiBaseUrl) updates.FOTRIC_API_BASE_URL = apiBaseUrl;
|
|
105
|
+
if (modelName) updates.FOTRIC_MODEL_NAME = modelName;
|
|
106
|
+
|
|
107
|
+
const updatedContent = updateEnvContent(envContent, updates);
|
|
108
|
+
fs.writeFileSync(ENV_FILE, updatedContent, 'utf-8');
|
|
109
|
+
|
|
110
|
+
console.log('\nā
Configuration saved to backend/.env');
|
|
111
|
+
console.log(' Please restart the server for changes to take effect.\n');
|
|
112
|
+
|
|
113
|
+
rl.close();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function showHelp() {
|
|
117
|
+
console.log(`
|
|
118
|
+
FotricCalw CLI
|
|
119
|
+
|
|
120
|
+
Usage:
|
|
121
|
+
fotric-claw <command> [options]
|
|
122
|
+
|
|
123
|
+
Commands:
|
|
124
|
+
start Start backend and frontend services (dev mode)
|
|
125
|
+
start:prod Start backend and frontend services (production mode)
|
|
126
|
+
build Build backend and frontend for production
|
|
127
|
+
config Configure LLM API settings
|
|
128
|
+
help Show this help message
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
fotric-claw start
|
|
132
|
+
fotric-claw start:prod
|
|
133
|
+
fotric-claw build
|
|
134
|
+
fotric-claw config
|
|
135
|
+
fotric-claw help
|
|
136
|
+
`);
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function startCommand() {
|
|
141
|
+
console.log('\nš Starting FotricCalw (Development Mode)...\n');
|
|
142
|
+
|
|
143
|
+
const isWindows = process.platform === 'win32';
|
|
144
|
+
const shell = isWindows ? true : false;
|
|
145
|
+
|
|
146
|
+
const backend = spawn('npm', ['run', 'backend'], {
|
|
147
|
+
cwd: PROJECT_ROOT,
|
|
148
|
+
shell,
|
|
149
|
+
stdio: 'inherit'
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
const frontend = spawn('npm', ['run', 'frontend'], {
|
|
154
|
+
cwd: PROJECT_ROOT,
|
|
155
|
+
shell,
|
|
156
|
+
stdio: 'inherit'
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
frontend.on('error', (err) => {
|
|
160
|
+
console.error('Failed to start frontend:', err.message);
|
|
161
|
+
});
|
|
162
|
+
}, 3000);
|
|
163
|
+
|
|
164
|
+
backend.on('error', (err) => {
|
|
165
|
+
console.error('Failed to start backend:', err.message);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log('Backend starting on http://localhost:8002');
|
|
169
|
+
console.log('Frontend starting on http://localhost:3000');
|
|
170
|
+
console.log('\nPress Ctrl+C to stop the services.\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function startProdCommand() {
|
|
174
|
+
console.log('\nš Starting FotricCalw (Production Mode)...\n');
|
|
175
|
+
|
|
176
|
+
const isWindows = process.platform === 'win32';
|
|
177
|
+
const shell = isWindows ? true : false;
|
|
178
|
+
|
|
179
|
+
const backend = spawn('node', ['dist/main'], {
|
|
180
|
+
cwd: path.resolve(PROJECT_ROOT, 'backend'),
|
|
181
|
+
shell,
|
|
182
|
+
stdio: 'inherit'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
const frontend = spawn('npm', ['run', 'start'], {
|
|
187
|
+
cwd: path.resolve(PROJECT_ROOT, 'frontend'),
|
|
188
|
+
shell,
|
|
189
|
+
stdio: 'inherit'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
frontend.on('error', (err) => {
|
|
193
|
+
console.error('Failed to start frontend:', err.message);
|
|
194
|
+
});
|
|
195
|
+
}, 3000);
|
|
196
|
+
|
|
197
|
+
backend.on('error', (err) => {
|
|
198
|
+
console.error('Failed to start backend:', err.message);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log('Backend running on http://localhost:8002');
|
|
202
|
+
console.log('Frontend running on http://localhost:3000');
|
|
203
|
+
console.log('\nPress Ctrl+C to stop the services.\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildCommand() {
|
|
207
|
+
console.log('\nš¦ Building FotricCalw for production...\n');
|
|
208
|
+
|
|
209
|
+
const isWindows = process.platform === 'win32';
|
|
210
|
+
const shell = isWindows ? true : false;
|
|
211
|
+
|
|
212
|
+
console.log('Building backend...');
|
|
213
|
+
const backendBuild = spawn('npm', ['run', 'build'], {
|
|
214
|
+
cwd: path.resolve(PROJECT_ROOT, 'backend'),
|
|
215
|
+
shell,
|
|
216
|
+
stdio: 'inherit'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
backendBuild.on('close', (code) => {
|
|
220
|
+
if (code !== 0) {
|
|
221
|
+
console.error('ā Backend build failed');
|
|
222
|
+
process.exit(code);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log('\nā
Backend build complete\n');
|
|
226
|
+
console.log('Building frontend...');
|
|
227
|
+
|
|
228
|
+
const frontendBuild = spawn('npm', ['run', 'build'], {
|
|
229
|
+
cwd: path.resolve(PROJECT_ROOT, 'frontend'),
|
|
230
|
+
shell,
|
|
231
|
+
stdio: 'inherit'
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
frontendBuild.on('close', (frontendCode) => {
|
|
235
|
+
if (frontendCode !== 0) {
|
|
236
|
+
console.error('ā Frontend build failed');
|
|
237
|
+
process.exit(frontendCode);
|
|
238
|
+
}
|
|
239
|
+
console.log('\nā
Frontend build complete');
|
|
240
|
+
console.log('\nš Build complete! Run "fotric-claw start:prod" to start in production mode.\n');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
frontendBuild.on('error', (err) => {
|
|
244
|
+
console.error('Failed to build frontend:', err.message);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
backendBuild.on('error', (err) => {
|
|
249
|
+
console.error('Failed to build backend:', err.message);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function main() {
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
const command = args[0] || 'help';
|
|
256
|
+
|
|
257
|
+
switch (command) {
|
|
258
|
+
case 'start':
|
|
259
|
+
startCommand();
|
|
260
|
+
break;
|
|
261
|
+
case 'start:prod':
|
|
262
|
+
startProdCommand();
|
|
263
|
+
break;
|
|
264
|
+
case 'build':
|
|
265
|
+
buildCommand();
|
|
266
|
+
break;
|
|
267
|
+
case 'config':
|
|
268
|
+
await configCommand();
|
|
269
|
+
break;
|
|
270
|
+
case 'help':
|
|
271
|
+
case '--help':
|
|
272
|
+
case '-h':
|
|
273
|
+
await showHelp();
|
|
274
|
+
break;
|
|
275
|
+
default:
|
|
276
|
+
console.log(`Unknown command: ${command}`);
|
|
277
|
+
await showHelp();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** @type {import('next').NextConfig} */
|
|
2
|
+
const nextConfig = {
|
|
3
|
+
reactStrictMode: true,
|
|
4
|
+
async rewrites() {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
source: '/api/:path*',
|
|
8
|
+
destination: 'http://localhost:8002/api/:path*',
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = nextConfig;
|