@eforest-finance/agent-skills 0.2.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/.env.example +13 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/bin/platforms/claude.ts +65 -0
- package/bin/platforms/cursor.ts +95 -0
- package/bin/platforms/openclaw.ts +125 -0
- package/bin/platforms/utils.ts +180 -0
- package/bin/setup.ts +201 -0
- package/create_token_skill.ts +218 -0
- package/index.ts +75 -0
- package/lib/aelf-client.ts +136 -0
- package/lib/api-client.ts +188 -0
- package/lib/config.ts +139 -0
- package/lib/types.ts +319 -0
- package/openclaw.json +170 -0
- package/package.json +60 -0
- package/src/core/index.ts +6 -0
- package/src/core/issue.ts +194 -0
- package/src/core/seed.ts +236 -0
- package/src/core/token.ts +244 -0
- package/src/mcp/server.ts +223 -0
package/.env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# aelf wallet private key (hex string, 64 chars)
|
|
2
|
+
# Used for signing transactions on aelf blockchain.
|
|
3
|
+
# WARNING: Never commit real private keys to version control.
|
|
4
|
+
AELF_PRIVATE_KEY=your_private_key_here
|
|
5
|
+
|
|
6
|
+
# Optional: override environment (default: mainnet)
|
|
7
|
+
# AELF_ENV=testnet
|
|
8
|
+
|
|
9
|
+
# Optional: override API base URL
|
|
10
|
+
# AELF_API_URL=https://test.eforest.finance/api
|
|
11
|
+
|
|
12
|
+
# Optional: override RPC URL
|
|
13
|
+
# AELF_RPC_URL=https://aelf-test-node.aelf.io
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 eForest Finance
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
[English](README.md) | [中文](README_zh-CN.md)
|
|
2
|
+
|
|
3
|
+
# eForest Agent Skills
|
|
4
|
+
|
|
5
|
+
AI Agent Kit for aelf token lifecycle on [eForest](https://www.eforest.finance). Provides CLI, MCP Server, and SDK interfaces for:
|
|
6
|
+
|
|
7
|
+
- **buy-seed** — Purchase a SEED from the SymbolRegister contract
|
|
8
|
+
- **create-token** — Create a new FT token using an owned SEED (with cross-chain sync)
|
|
9
|
+
- **issue-token** — Issue tokens via Proxy ForwardCall
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Prerequisites
|
|
14
|
+
|
|
15
|
+
- [Bun](https://bun.sh) >= 1.0
|
|
16
|
+
- An aelf wallet private key with ELF balance
|
|
17
|
+
|
|
18
|
+
### Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/eforest-finance/eforest-agent-skills.git
|
|
22
|
+
cd eforest-agent-skills
|
|
23
|
+
bun install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Configure
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cp .env.example .env
|
|
30
|
+
# Edit .env and set your AELF_PRIVATE_KEY
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### CLI Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Check SEED price (dry-run)
|
|
37
|
+
bun run cli buy-seed --symbol MYTOKEN --issuer <your-address> --dry-run
|
|
38
|
+
|
|
39
|
+
# Buy SEED (max 2 ELF)
|
|
40
|
+
bun run cli buy-seed --symbol MYTOKEN --issuer <your-address> --force 2
|
|
41
|
+
|
|
42
|
+
# Create token on tDVV side chain
|
|
43
|
+
bun run cli create-token \
|
|
44
|
+
--symbol MYTOKEN --token-name "My Token" \
|
|
45
|
+
--seed-symbol SEED-321 \
|
|
46
|
+
--total-supply 100000000 --decimals 8 \
|
|
47
|
+
--issue-chain tDVV
|
|
48
|
+
|
|
49
|
+
# Issue tokens
|
|
50
|
+
bun run cli issue-token \
|
|
51
|
+
--symbol MYTOKEN --amount 10000000000000000 \
|
|
52
|
+
--to <recipient-address> --chain tDVV
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## MCP Server (Claude Desktop / Cursor)
|
|
56
|
+
|
|
57
|
+
One-command setup for AI platforms:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Claude Desktop
|
|
61
|
+
bun run setup claude
|
|
62
|
+
|
|
63
|
+
# Cursor IDE (project-level)
|
|
64
|
+
bun run setup cursor
|
|
65
|
+
|
|
66
|
+
# Cursor IDE (global)
|
|
67
|
+
bun run setup cursor --global
|
|
68
|
+
|
|
69
|
+
# Check status
|
|
70
|
+
bun run setup list
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then edit the generated config to replace `<YOUR_PRIVATE_KEY>` with your actual key.
|
|
74
|
+
|
|
75
|
+
### Manual MCP Config
|
|
76
|
+
|
|
77
|
+
If you prefer manual configuration, add this to your MCP settings:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"eforest-token": {
|
|
83
|
+
"command": "bun",
|
|
84
|
+
"args": ["run", "/path/to/eforest-agent-skills/src/mcp/server.ts"],
|
|
85
|
+
"env": {
|
|
86
|
+
"AELF_PRIVATE_KEY": "your_private_key",
|
|
87
|
+
"EFOREST_NETWORK": "mainnet"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## OpenClaw
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Generate OpenClaw config with absolute paths
|
|
98
|
+
bun run setup openclaw
|
|
99
|
+
|
|
100
|
+
# Merge into existing config
|
|
101
|
+
bun run setup openclaw --config-path /path/to/openclaw.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## SDK Usage
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { buySeed, createToken, issueToken } from '@eforest-finance/agent-skills';
|
|
108
|
+
import { getNetworkConfig } from '@eforest-finance/agent-skills';
|
|
109
|
+
|
|
110
|
+
const config = await getNetworkConfig({ env: 'mainnet', privateKey: '...' });
|
|
111
|
+
|
|
112
|
+
const seedResult = await buySeed(config, {
|
|
113
|
+
symbol: 'MYTOKEN',
|
|
114
|
+
issueTo: config.walletAddress,
|
|
115
|
+
force: 2,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const tokenResult = await createToken(config, {
|
|
119
|
+
symbol: 'MYTOKEN',
|
|
120
|
+
tokenName: 'My Token',
|
|
121
|
+
seedSymbol: seedResult.seedSymbol!,
|
|
122
|
+
totalSupply: '100000000',
|
|
123
|
+
decimals: 8,
|
|
124
|
+
issuer: config.walletAddress,
|
|
125
|
+
issueChain: 'tDVV',
|
|
126
|
+
isBurnable: true,
|
|
127
|
+
tokenImage: '',
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Architecture
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
eforest-agent-skills/
|
|
135
|
+
├── lib/ # Infrastructure layer
|
|
136
|
+
│ ├── types.ts # Interfaces, constants, validators
|
|
137
|
+
│ ├── config.ts # Network config & .env loader
|
|
138
|
+
│ ├── aelf-client.ts # aelf-sdk wrapper
|
|
139
|
+
│ └── api-client.ts # eForest backend API client
|
|
140
|
+
├── src/
|
|
141
|
+
│ ├── core/ # Pure business logic (no I/O side effects)
|
|
142
|
+
│ │ ├── seed.ts # buySeed + parseSeedSymbolFromLogs
|
|
143
|
+
│ │ ├── token.ts # createToken
|
|
144
|
+
│ │ └── issue.ts # issueToken + encodeIssueInput
|
|
145
|
+
│ └── mcp/
|
|
146
|
+
│ └── server.ts # MCP Server adapter (Zod validation)
|
|
147
|
+
├── bin/
|
|
148
|
+
│ ├── setup.ts # Setup CLI entry point
|
|
149
|
+
│ └── platforms/ # Claude, Cursor, OpenClaw adapters
|
|
150
|
+
├── create_token_skill.ts # CLI adapter (thin wrapper)
|
|
151
|
+
├── index.ts # SDK entry (re-exports)
|
|
152
|
+
├── openclaw.json # OpenClaw tool definitions
|
|
153
|
+
└── __tests__/ # Unit + Integration tests
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Configuration Priority
|
|
157
|
+
|
|
158
|
+
Settings are resolved in this order (highest priority first):
|
|
159
|
+
|
|
160
|
+
1. Function params (SDK callers)
|
|
161
|
+
2. CLI args (`--env`, `--rpc-url`)
|
|
162
|
+
3. `EFOREST_*` / `AELF_*` environment variables
|
|
163
|
+
4. `.env` file
|
|
164
|
+
5. CMS remote config
|
|
165
|
+
6. Code defaults (ENV_PRESETS)
|
|
166
|
+
|
|
167
|
+
## Testing
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
bun test # All tests
|
|
171
|
+
bun test:unit # Unit tests only
|
|
172
|
+
bun test:integration # Integration tests only
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Environment Variables
|
|
176
|
+
|
|
177
|
+
| Variable | Description | Default |
|
|
178
|
+
|----------|-------------|---------|
|
|
179
|
+
| `AELF_PRIVATE_KEY` | aelf wallet private key | (required) |
|
|
180
|
+
| `EFOREST_NETWORK` / `AELF_ENV` | `mainnet` or `testnet` | `mainnet` |
|
|
181
|
+
| `EFOREST_API_URL` / `AELF_API_URL` | Backend API URL | auto |
|
|
182
|
+
| `EFOREST_RPC_URL` / `AELF_RPC_URL` | AELF MainChain RPC | auto |
|
|
183
|
+
| `EFOREST_RPC_URL_TDVV` | tDVV RPC URL | auto |
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Setup: Claude Desktop — write MCP config
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getPlatformPaths,
|
|
7
|
+
readJsonFile,
|
|
8
|
+
writeJsonFile,
|
|
9
|
+
generateMcpEntry,
|
|
10
|
+
mergeMcpConfig,
|
|
11
|
+
removeMcpEntry,
|
|
12
|
+
SERVER_NAME,
|
|
13
|
+
LOG,
|
|
14
|
+
} from './utils';
|
|
15
|
+
|
|
16
|
+
export function setupClaude(opts: {
|
|
17
|
+
configPath?: string;
|
|
18
|
+
serverPath?: string;
|
|
19
|
+
force?: boolean;
|
|
20
|
+
}): boolean {
|
|
21
|
+
const configPath = opts.configPath || getPlatformPaths().claude;
|
|
22
|
+
const entry = generateMcpEntry(opts.serverPath);
|
|
23
|
+
|
|
24
|
+
LOG.step(`Config file: ${configPath}`);
|
|
25
|
+
LOG.step(`MCP server: ${entry.args[1]}`);
|
|
26
|
+
|
|
27
|
+
const existing = readJsonFile(configPath);
|
|
28
|
+
const { config, action } = mergeMcpConfig(
|
|
29
|
+
existing,
|
|
30
|
+
SERVER_NAME,
|
|
31
|
+
entry,
|
|
32
|
+
opts.force,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (action === 'skipped') {
|
|
36
|
+
LOG.warn(`"${SERVER_NAME}" already exists in Claude Desktop config.`);
|
|
37
|
+
LOG.info('Use --force to overwrite.');
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeJsonFile(configPath, config);
|
|
42
|
+
LOG.success(`Claude Desktop MCP config ${action}: ${configPath}`);
|
|
43
|
+
LOG.info('');
|
|
44
|
+
LOG.warn(
|
|
45
|
+
'IMPORTANT: Edit the config file and replace <YOUR_PRIVATE_KEY> with your actual aelf private key.',
|
|
46
|
+
);
|
|
47
|
+
LOG.info('Then restart Claude Desktop to pick up the new MCP server.');
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function uninstallClaude(opts: { configPath?: string }): boolean {
|
|
52
|
+
const configPath = opts.configPath || getPlatformPaths().claude;
|
|
53
|
+
const existing = readJsonFile(configPath);
|
|
54
|
+
const { config, removed } = removeMcpEntry(existing, SERVER_NAME);
|
|
55
|
+
|
|
56
|
+
if (!removed) {
|
|
57
|
+
LOG.info(`"${SERVER_NAME}" not found in Claude Desktop config.`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
writeJsonFile(configPath, config);
|
|
62
|
+
LOG.success(`Removed "${SERVER_NAME}" from Claude Desktop config.`);
|
|
63
|
+
LOG.info('Restart Claude Desktop to apply changes.');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Setup: Cursor — write MCP config (project or global)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getPlatformPaths,
|
|
7
|
+
getCursorProjectPath,
|
|
8
|
+
readJsonFile,
|
|
9
|
+
writeJsonFile,
|
|
10
|
+
generateMcpEntry,
|
|
11
|
+
mergeMcpConfig,
|
|
12
|
+
removeMcpEntry,
|
|
13
|
+
SERVER_NAME,
|
|
14
|
+
LOG,
|
|
15
|
+
} from './utils';
|
|
16
|
+
|
|
17
|
+
export function setupCursor(opts: {
|
|
18
|
+
global?: boolean;
|
|
19
|
+
configPath?: string;
|
|
20
|
+
serverPath?: string;
|
|
21
|
+
force?: boolean;
|
|
22
|
+
projectDir?: string;
|
|
23
|
+
}): boolean {
|
|
24
|
+
let configPath: string;
|
|
25
|
+
let scope: string;
|
|
26
|
+
|
|
27
|
+
if (opts.configPath) {
|
|
28
|
+
configPath = opts.configPath;
|
|
29
|
+
scope = 'custom';
|
|
30
|
+
} else if (opts.global) {
|
|
31
|
+
configPath = getPlatformPaths().cursorGlobal;
|
|
32
|
+
scope = 'global';
|
|
33
|
+
} else {
|
|
34
|
+
configPath = getCursorProjectPath(opts.projectDir);
|
|
35
|
+
scope = 'project';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const entry = generateMcpEntry(opts.serverPath);
|
|
39
|
+
|
|
40
|
+
LOG.step(`Scope: ${scope}`);
|
|
41
|
+
LOG.step(`Config file: ${configPath}`);
|
|
42
|
+
LOG.step(`MCP server: ${entry.args[1]}`);
|
|
43
|
+
|
|
44
|
+
const existing = readJsonFile(configPath);
|
|
45
|
+
const { config, action } = mergeMcpConfig(
|
|
46
|
+
existing,
|
|
47
|
+
SERVER_NAME,
|
|
48
|
+
entry,
|
|
49
|
+
opts.force,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (action === 'skipped') {
|
|
53
|
+
LOG.warn(`"${SERVER_NAME}" already exists in Cursor ${scope} config.`);
|
|
54
|
+
LOG.info('Use --force to overwrite.');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
writeJsonFile(configPath, config);
|
|
59
|
+
LOG.success(`Cursor ${scope} MCP config ${action}: ${configPath}`);
|
|
60
|
+
LOG.info('');
|
|
61
|
+
LOG.warn(
|
|
62
|
+
'IMPORTANT: Edit the config file and replace <YOUR_PRIVATE_KEY> with your actual aelf private key.',
|
|
63
|
+
);
|
|
64
|
+
LOG.info(
|
|
65
|
+
'Cursor will auto-detect the new MCP server (or restart Cursor).',
|
|
66
|
+
);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function uninstallCursor(opts: {
|
|
71
|
+
global?: boolean;
|
|
72
|
+
configPath?: string;
|
|
73
|
+
projectDir?: string;
|
|
74
|
+
}): boolean {
|
|
75
|
+
let configPath: string;
|
|
76
|
+
if (opts.configPath) {
|
|
77
|
+
configPath = opts.configPath;
|
|
78
|
+
} else if (opts.global) {
|
|
79
|
+
configPath = getPlatformPaths().cursorGlobal;
|
|
80
|
+
} else {
|
|
81
|
+
configPath = getCursorProjectPath(opts.projectDir);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existing = readJsonFile(configPath);
|
|
85
|
+
const { config, removed } = removeMcpEntry(existing, SERVER_NAME);
|
|
86
|
+
|
|
87
|
+
if (!removed) {
|
|
88
|
+
LOG.info(`"${SERVER_NAME}" not found in Cursor config: ${configPath}`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writeJsonFile(configPath, config);
|
|
93
|
+
LOG.success(`Removed "${SERVER_NAME}" from Cursor config: ${configPath}`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Setup: OpenClaw — register skills from openclaw.json
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { getPackageRoot, readJsonFile, writeJsonFile, LOG } from './utils';
|
|
8
|
+
|
|
9
|
+
/** Skill names owned by this package (for uninstall) */
|
|
10
|
+
const EFOREST_SKILL_NAMES = new Set([
|
|
11
|
+
'aelf-buy-seed',
|
|
12
|
+
'aelf-create-token',
|
|
13
|
+
'aelf-issue-token',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function setupOpenClaw(opts: {
|
|
17
|
+
configPath?: string;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
force?: boolean;
|
|
20
|
+
}): boolean {
|
|
21
|
+
const packageRoot = getPackageRoot();
|
|
22
|
+
const sourceFile = path.join(packageRoot, 'openclaw.json');
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(sourceFile)) {
|
|
25
|
+
LOG.error(`openclaw.json not found at ${sourceFile}`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const source = readJsonFile(sourceFile);
|
|
30
|
+
const skills: any[] = source.skills || [];
|
|
31
|
+
|
|
32
|
+
if (!skills.length) {
|
|
33
|
+
LOG.error('No skills found in openclaw.json');
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve working_directory for all skills — use explicit cwd, or package root
|
|
38
|
+
const resolvedCwd = opts.cwd || packageRoot;
|
|
39
|
+
|
|
40
|
+
const updatedSkills = skills.map((skill: any) => ({
|
|
41
|
+
...skill,
|
|
42
|
+
working_directory: resolvedCwd,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// If user specified a target config path, merge into it
|
|
46
|
+
if (opts.configPath) {
|
|
47
|
+
LOG.step(`Merging ${skills.length} skills into: ${opts.configPath}`);
|
|
48
|
+
|
|
49
|
+
const existing = readJsonFile(opts.configPath);
|
|
50
|
+
if (!existing.skills) existing.skills = [];
|
|
51
|
+
|
|
52
|
+
let added = 0;
|
|
53
|
+
let updated = 0;
|
|
54
|
+
let skipped = 0;
|
|
55
|
+
|
|
56
|
+
for (const skill of updatedSkills) {
|
|
57
|
+
const idx = existing.skills.findIndex(
|
|
58
|
+
(s: any) => s.name === skill.name,
|
|
59
|
+
);
|
|
60
|
+
if (idx >= 0) {
|
|
61
|
+
if (opts.force) {
|
|
62
|
+
existing.skills[idx] = skill;
|
|
63
|
+
updated++;
|
|
64
|
+
} else {
|
|
65
|
+
skipped++;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
existing.skills.push(skill);
|
|
69
|
+
added++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeJsonFile(opts.configPath, existing);
|
|
74
|
+
LOG.success(
|
|
75
|
+
`OpenClaw config updated: ${added} added, ${updated} updated, ${skipped} skipped.`,
|
|
76
|
+
);
|
|
77
|
+
if (skipped > 0) {
|
|
78
|
+
LOG.info('Use --force to overwrite existing skills.');
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// No target path: generate a standalone config file in current dir
|
|
82
|
+
const outPath = path.join(process.cwd(), 'eforest-openclaw.json');
|
|
83
|
+
LOG.step(`Generating OpenClaw config: ${outPath}`);
|
|
84
|
+
LOG.step(`Skill working_directory: ${resolvedCwd}`);
|
|
85
|
+
|
|
86
|
+
writeJsonFile(outPath, { skills: updatedSkills });
|
|
87
|
+
LOG.success(`OpenClaw config generated: ${outPath}`);
|
|
88
|
+
LOG.info(
|
|
89
|
+
`Contains ${updatedSkills.length} skills with working_directory set to: ${resolvedCwd}`,
|
|
90
|
+
);
|
|
91
|
+
LOG.info('Import this file into your OpenClaw configuration.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function uninstallOpenClaw(opts: { configPath?: string }): boolean {
|
|
98
|
+
if (!opts.configPath) {
|
|
99
|
+
LOG.info(
|
|
100
|
+
'OpenClaw: no --config-path specified. Remove skills manually from your OpenClaw config.',
|
|
101
|
+
);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const existing = readJsonFile(opts.configPath);
|
|
106
|
+
if (!existing.skills?.length) {
|
|
107
|
+
LOG.info('No skills found in config.');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const before = existing.skills.length;
|
|
112
|
+
existing.skills = existing.skills.filter(
|
|
113
|
+
(s: any) => !EFOREST_SKILL_NAMES.has(s.name),
|
|
114
|
+
);
|
|
115
|
+
const removed = before - existing.skills.length;
|
|
116
|
+
|
|
117
|
+
if (removed === 0) {
|
|
118
|
+
LOG.info('No eForest skills found in config.');
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
writeJsonFile(opts.configPath, existing);
|
|
123
|
+
LOG.success(`Removed ${removed} eForest skills from OpenClaw config.`);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Setup Utilities — path detection, JSON merge, cross-platform
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const SERVER_NAME = 'eforest-token';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Package root detection
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/** Resolve the absolute path to this package's root directory */
|
|
20
|
+
export function getPackageRoot(): string {
|
|
21
|
+
// import.meta.dir points to bin/platforms/ — go up 2 levels
|
|
22
|
+
return path.resolve(import.meta.dir, '..', '..');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve the absolute path to the MCP server.ts */
|
|
26
|
+
export function getMcpServerPath(): string {
|
|
27
|
+
return path.join(getPackageRoot(), 'src', 'mcp', 'server.ts');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Detect bun executable path */
|
|
31
|
+
export function getBunPath(): string {
|
|
32
|
+
const platform = os.platform();
|
|
33
|
+
try {
|
|
34
|
+
const cmd = platform === 'win32' ? 'where bun' : 'which bun';
|
|
35
|
+
const result = Bun.spawnSync(cmd.split(' '));
|
|
36
|
+
const stdout = result.stdout.toString().trim();
|
|
37
|
+
if (stdout) return stdout.split('\n')[0].trim();
|
|
38
|
+
} catch {}
|
|
39
|
+
return 'bun';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Platform config paths
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
export interface PlatformPaths {
|
|
47
|
+
claude: string;
|
|
48
|
+
cursorGlobal: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getPlatformPaths(): PlatformPaths {
|
|
52
|
+
const home = os.homedir();
|
|
53
|
+
const platform = os.platform();
|
|
54
|
+
|
|
55
|
+
let claude: string;
|
|
56
|
+
if (platform === 'darwin') {
|
|
57
|
+
claude = path.join(
|
|
58
|
+
home,
|
|
59
|
+
'Library',
|
|
60
|
+
'Application Support',
|
|
61
|
+
'Claude',
|
|
62
|
+
'claude_desktop_config.json',
|
|
63
|
+
);
|
|
64
|
+
} else if (platform === 'win32') {
|
|
65
|
+
claude = path.join(
|
|
66
|
+
process.env.APPDATA || path.join(home, 'AppData', 'Roaming'),
|
|
67
|
+
'Claude',
|
|
68
|
+
'claude_desktop_config.json',
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
claude = path.join(
|
|
72
|
+
home,
|
|
73
|
+
'.config',
|
|
74
|
+
'Claude',
|
|
75
|
+
'claude_desktop_config.json',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const cursorGlobal = path.join(home, '.cursor', 'mcp.json');
|
|
80
|
+
|
|
81
|
+
return { claude, cursorGlobal };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getCursorProjectPath(projectDir?: string): string {
|
|
85
|
+
const dir = projectDir || process.cwd();
|
|
86
|
+
return path.join(dir, '.cursor', 'mcp.json');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// JSON file operations (safe merge)
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
/** Read a JSON file, return empty object if not exists or invalid */
|
|
94
|
+
export function readJsonFile(filePath: string): any {
|
|
95
|
+
try {
|
|
96
|
+
if (!fs.existsSync(filePath)) return {};
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
return JSON.parse(content);
|
|
99
|
+
} catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Write JSON to file, creating parent dirs if needed */
|
|
105
|
+
export function writeJsonFile(filePath: string, data: any): void {
|
|
106
|
+
const dir = path.dirname(filePath);
|
|
107
|
+
if (!fs.existsSync(dir)) {
|
|
108
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// MCP config generation
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
export interface McpServerEntry {
|
|
118
|
+
command: string;
|
|
119
|
+
args: string[];
|
|
120
|
+
env: Record<string, string>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function generateMcpEntry(customPath?: string): McpServerEntry {
|
|
124
|
+
const serverPath = customPath || getMcpServerPath();
|
|
125
|
+
return {
|
|
126
|
+
command: getBunPath(),
|
|
127
|
+
args: ['run', serverPath],
|
|
128
|
+
env: {
|
|
129
|
+
AELF_PRIVATE_KEY: '<YOUR_PRIVATE_KEY>',
|
|
130
|
+
EFOREST_NETWORK: 'mainnet',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Merge our MCP server entry into an existing config.
|
|
137
|
+
* Does NOT overwrite other servers — only operates on `serverName`.
|
|
138
|
+
*/
|
|
139
|
+
export function mergeMcpConfig(
|
|
140
|
+
existing: any,
|
|
141
|
+
serverName: string,
|
|
142
|
+
entry: McpServerEntry,
|
|
143
|
+
force: boolean = false,
|
|
144
|
+
): { config: any; action: 'created' | 'updated' | 'skipped' } {
|
|
145
|
+
const config = { ...existing };
|
|
146
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
147
|
+
|
|
148
|
+
if (config.mcpServers[serverName] && !force) {
|
|
149
|
+
return { config, action: 'skipped' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const action = config.mcpServers[serverName] ? 'updated' : 'created';
|
|
153
|
+
config.mcpServers[serverName] = entry;
|
|
154
|
+
return { config, action };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Remove our MCP server entry from config */
|
|
158
|
+
export function removeMcpEntry(
|
|
159
|
+
existing: any,
|
|
160
|
+
serverName: string,
|
|
161
|
+
): { config: any; removed: boolean } {
|
|
162
|
+
const config = { ...existing };
|
|
163
|
+
if (!config.mcpServers || !config.mcpServers[serverName]) {
|
|
164
|
+
return { config, removed: false };
|
|
165
|
+
}
|
|
166
|
+
delete config.mcpServers[serverName];
|
|
167
|
+
return { config, removed: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Console output helpers
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
export const LOG = {
|
|
175
|
+
success: (msg: string) => console.log(` ✅ ${msg}`),
|
|
176
|
+
info: (msg: string) => console.log(` ℹ️ ${msg}`),
|
|
177
|
+
warn: (msg: string) => console.log(` ⚠️ ${msg}`),
|
|
178
|
+
error: (msg: string) => console.error(` ❌ ${msg}`),
|
|
179
|
+
step: (msg: string) => console.log(` → ${msg}`),
|
|
180
|
+
};
|