natureco-cli 2.23.28 → 2.23.29
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/bin/natureco.js +68 -6
- package/package.json +10 -6
- package/src/commands/channels.js +94 -4
- package/src/commands/chat.js +11 -25
- package/src/commands/config.js +111 -68
- package/src/commands/doctor.js +121 -16
- package/src/commands/gateway-server.js +35 -21
- package/src/commands/gateway.js +11 -20
- package/src/commands/help.js +6 -0
- package/src/commands/imessage.js +55 -0
- package/src/commands/irc.js +70 -0
- package/src/commands/mattermost.js +62 -0
- package/src/commands/message.js +24 -4
- package/src/commands/models.js +584 -216
- package/src/commands/plugins.js +415 -172
- package/src/commands/security.js +149 -1
- package/src/commands/setup.js +1 -3
- package/src/commands/signal.js +66 -0
- package/src/commands/skills.js +20 -29
- package/src/commands/sms.js +64 -0
- package/src/commands/tasks.js +328 -79
- package/src/commands/webhooks.js +79 -0
- package/src/commands/whatsapp.js +7 -21
- package/src/tools/bash.js +63 -29
- package/src/utils/api.js +3 -20
- package/src/utils/approvals.js +297 -0
- package/src/utils/background.js +223 -66
- package/src/utils/baileys.js +21 -0
- package/src/utils/config.js +141 -10
- package/src/utils/errors.js +148 -0
- package/src/utils/inquirer-wrapper.js +1 -2
- package/src/utils/path-utils.js +13 -13
- package/src/utils/plugin-registry.js +238 -0
- package/src/utils/secrets.js +177 -0
- package/src/utils/skills.js +10 -23
package/src/utils/config.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { ConfigParseError, ConfigMutationConflictError, ConfigValidationError, handleError } = require('./errors');
|
|
6
|
+
|
|
7
|
+
let json5;
|
|
8
|
+
try {
|
|
9
|
+
json5 = require('json5');
|
|
10
|
+
} catch {
|
|
11
|
+
json5 = null;
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
const CONFIG_DIR = path.join(os.homedir(), '.natureco');
|
|
6
15
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
16
|
+
const CONFIG_BACKUP_DIR = path.join(CONFIG_DIR, 'backups');
|
|
17
|
+
const MAX_BACKUPS = 10;
|
|
18
|
+
|
|
19
|
+
let _configCache = null;
|
|
20
|
+
let _configHash = null;
|
|
7
21
|
|
|
8
|
-
// --profile flag desteği: ~/.natureco-<profile>/
|
|
9
22
|
function getProfileDir() {
|
|
10
23
|
const profileArg = process.argv.find(a => a.startsWith('--profile='));
|
|
11
24
|
const profileIdx = process.argv.indexOf('--profile');
|
|
@@ -27,50 +40,134 @@ function ensureConfigDir() {
|
|
|
27
40
|
}
|
|
28
41
|
}
|
|
29
42
|
|
|
30
|
-
function
|
|
43
|
+
function computeHash(data) {
|
|
44
|
+
return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createBackup() {
|
|
48
|
+
if (!fs.existsSync(ACTIVE_CONFIG_FILE)) return;
|
|
31
49
|
ensureConfigDir();
|
|
32
|
-
fs.
|
|
50
|
+
if (!fs.existsSync(CONFIG_BACKUP_DIR)) {
|
|
51
|
+
fs.mkdirSync(CONFIG_BACKUP_DIR, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
54
|
+
const backupFile = path.join(CONFIG_BACKUP_DIR, `config-${timestamp}.json`);
|
|
55
|
+
fs.copyFileSync(ACTIVE_CONFIG_FILE, backupFile);
|
|
56
|
+
const backups = fs.readdirSync(CONFIG_BACKUP_DIR)
|
|
57
|
+
.filter(f => f.startsWith('config-') && f.endsWith('.json'))
|
|
58
|
+
.sort()
|
|
59
|
+
.reverse();
|
|
60
|
+
if (backups.length > MAX_BACKUPS) {
|
|
61
|
+
backups.slice(MAX_BACKUPS).forEach(f => {
|
|
62
|
+
try { fs.unlinkSync(path.join(CONFIG_BACKUP_DIR, f)); } catch {}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseConfigContent(content) {
|
|
68
|
+
if (!content || !content.trim()) return {};
|
|
69
|
+
const trimmed = content.trim();
|
|
70
|
+
if (trimmed.startsWith('{')) {
|
|
71
|
+
if (json5) {
|
|
72
|
+
try { return json5.parse(trimmed); } catch {}
|
|
73
|
+
}
|
|
74
|
+
return JSON.parse(trimmed);
|
|
75
|
+
}
|
|
76
|
+
return JSON.parse(trimmed);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateConfig(data) {
|
|
80
|
+
if (data === null || data === undefined) throw new ConfigValidationError('Config cannot be null', { field: 'root' });
|
|
81
|
+
if (typeof data !== 'object' || Array.isArray(data)) throw new ConfigValidationError('Config must be a JSON object', { field: 'root' });
|
|
82
|
+
if (data.apiKey !== undefined && typeof data.apiKey !== 'string') throw new ConfigValidationError('apiKey must be a string', { field: 'apiKey' });
|
|
83
|
+
if (data.providerUrl !== undefined && typeof data.providerUrl !== 'string') throw new ConfigValidationError('providerUrl must be a string', { field: 'providerUrl' });
|
|
84
|
+
if (data.providerModel !== undefined && typeof data.providerModel !== 'string') throw new ConfigValidationError('providerModel must be a string', { field: 'providerModel' });
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function saveConfig(data, options = {}) {
|
|
89
|
+
const { skipBackup = false, skipValidation = false } = options;
|
|
90
|
+
ensureConfigDir();
|
|
91
|
+
if (!skipValidation) validateConfig(data);
|
|
92
|
+
if (!skipBackup) createBackup();
|
|
93
|
+
const content = JSON.stringify(data, null, 2);
|
|
94
|
+
fs.writeFileSync(ACTIVE_CONFIG_FILE, content, 'utf8');
|
|
95
|
+
_configCache = data;
|
|
96
|
+
_configHash = computeHash(data);
|
|
33
97
|
}
|
|
34
98
|
|
|
35
|
-
function loadConfig() {
|
|
99
|
+
function loadConfig(options = {}) {
|
|
100
|
+
const { useCache = true, skipValidation = false } = options;
|
|
101
|
+
if (useCache && _configCache) return _configCache;
|
|
36
102
|
if (!fs.existsSync(ACTIVE_CONFIG_FILE)) {
|
|
103
|
+
_configCache = null;
|
|
104
|
+
_configHash = null;
|
|
37
105
|
return null;
|
|
38
106
|
}
|
|
39
107
|
try {
|
|
40
108
|
const content = fs.readFileSync(ACTIVE_CONFIG_FILE, 'utf8');
|
|
41
|
-
|
|
109
|
+
const data = parseConfigContent(content);
|
|
110
|
+
if (!skipValidation) validateConfig(data);
|
|
111
|
+
_configCache = data;
|
|
112
|
+
_configHash = computeHash(data);
|
|
113
|
+
return data;
|
|
42
114
|
} catch (err) {
|
|
115
|
+
_configCache = null;
|
|
116
|
+
_configHash = null;
|
|
117
|
+
if (err instanceof ConfigValidationError || err instanceof ConfigParseError) throw err;
|
|
43
118
|
return null;
|
|
44
119
|
}
|
|
45
120
|
}
|
|
46
121
|
|
|
122
|
+
function loadConfigWithRetry(maxRetries = 3) {
|
|
123
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
124
|
+
try {
|
|
125
|
+
return loadConfig({ useCache: false });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (i === maxRetries - 1) throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
47
133
|
function deleteConfig() {
|
|
48
134
|
if (fs.existsSync(ACTIVE_CONFIG_FILE)) {
|
|
135
|
+
createBackup();
|
|
49
136
|
fs.unlinkSync(ACTIVE_CONFIG_FILE);
|
|
50
137
|
}
|
|
138
|
+
_configCache = null;
|
|
139
|
+
_configHash = null;
|
|
51
140
|
}
|
|
52
141
|
|
|
53
142
|
function getApiKey() {
|
|
54
143
|
const config = loadConfig();
|
|
55
|
-
return config?.apiKey
|
|
144
|
+
return config?.apiKey ?? null;
|
|
56
145
|
}
|
|
57
146
|
|
|
58
147
|
function saveApiKey(apiKey) {
|
|
59
|
-
const config = loadConfig()
|
|
148
|
+
const config = loadConfig() ?? {};
|
|
60
149
|
config.apiKey = apiKey;
|
|
61
150
|
saveConfig(config);
|
|
62
151
|
}
|
|
63
152
|
|
|
64
153
|
function getConfig() {
|
|
65
|
-
|
|
154
|
+
try {
|
|
155
|
+
return loadConfig() ?? {};
|
|
156
|
+
} catch {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
66
159
|
}
|
|
67
160
|
|
|
68
161
|
function getAllConfig() {
|
|
69
|
-
|
|
162
|
+
try {
|
|
163
|
+
return loadConfig() ?? {};
|
|
164
|
+
} catch {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
70
167
|
}
|
|
71
168
|
|
|
72
169
|
function setConfigValue(key, value) {
|
|
73
|
-
const config = loadConfig()
|
|
170
|
+
const config = loadConfig() ?? {};
|
|
74
171
|
const keys = key.split('.');
|
|
75
172
|
let current = config;
|
|
76
173
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
@@ -83,15 +180,49 @@ function setConfigValue(key, value) {
|
|
|
83
180
|
saveConfig(config);
|
|
84
181
|
}
|
|
85
182
|
|
|
183
|
+
function getConfigHash() {
|
|
184
|
+
return _configHash;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function listBackups() {
|
|
188
|
+
if (!fs.existsSync(CONFIG_BACKUP_DIR)) return [];
|
|
189
|
+
return fs.readdirSync(CONFIG_BACKUP_DIR)
|
|
190
|
+
.filter(f => f.startsWith('config-') && f.endsWith('.json'))
|
|
191
|
+
.sort()
|
|
192
|
+
.reverse();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function restoreConfig(backupFile) {
|
|
196
|
+
const backupPath = path.isAbsolute(backupFile)
|
|
197
|
+
? backupFile
|
|
198
|
+
: path.join(CONFIG_BACKUP_DIR, backupFile);
|
|
199
|
+
if (!fs.existsSync(backupPath)) {
|
|
200
|
+
throw new ConfigValidationError(`Yedek dosyası bulunamadı: ${backupPath}`, { field: 'backupFile' });
|
|
201
|
+
}
|
|
202
|
+
const content = fs.readFileSync(backupPath, 'utf8');
|
|
203
|
+
const data = parseConfigContent(content);
|
|
204
|
+
validateConfig(data);
|
|
205
|
+
createBackup();
|
|
206
|
+
fs.writeFileSync(ACTIVE_CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
207
|
+
_configCache = data;
|
|
208
|
+
_configHash = computeHash(data);
|
|
209
|
+
return { path: backupPath, timestamp: path.basename(backupPath).replace(/^config-|\.json$/g, '') };
|
|
210
|
+
}
|
|
211
|
+
|
|
86
212
|
module.exports = {
|
|
87
213
|
saveConfig,
|
|
88
214
|
loadConfig,
|
|
215
|
+
loadConfigWithRetry,
|
|
89
216
|
deleteConfig,
|
|
90
217
|
getApiKey,
|
|
91
218
|
saveApiKey,
|
|
92
219
|
getConfig,
|
|
93
220
|
getAllConfig,
|
|
94
221
|
setConfigValue,
|
|
222
|
+
getConfigHash,
|
|
223
|
+
listBackups,
|
|
224
|
+
restoreConfig,
|
|
95
225
|
CONFIG_FILE: ACTIVE_CONFIG_FILE,
|
|
96
226
|
CONFIG_DIR: ACTIVE_CONFIG_DIR,
|
|
227
|
+
CONFIG_BACKUP_DIR,
|
|
97
228
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
class NatureCoError extends Error {
|
|
4
|
+
constructor(message, options = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = this.constructor.name;
|
|
7
|
+
this.cause = options.cause || null;
|
|
8
|
+
this.exitCode = options.exitCode || 1;
|
|
9
|
+
if (Error.captureStackTrace) {
|
|
10
|
+
Error.captureStackTrace(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class ConfigError extends NatureCoError {
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.configPath = options.configPath || null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class ConfigParseError extends ConfigError {
|
|
23
|
+
constructor(message, options = {}) {
|
|
24
|
+
super(`Config parse error: ${message}`, options);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class ConfigMutationConflictError extends ConfigError {
|
|
29
|
+
constructor(message, options = {}) {
|
|
30
|
+
super(`Config conflict: ${message}`, options);
|
|
31
|
+
this.currentHash = options.currentHash || null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ConfigValidationError extends ConfigError {
|
|
36
|
+
constructor(message, options = {}) {
|
|
37
|
+
super(`Config validation error: ${message}`, options);
|
|
38
|
+
this.field = options.field || null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ApiError extends NatureCoError {
|
|
43
|
+
constructor(message, options = {}) {
|
|
44
|
+
super(message, options);
|
|
45
|
+
this.statusCode = options.statusCode || null;
|
|
46
|
+
this.provider = options.provider || null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class ProviderError extends ApiError {
|
|
51
|
+
constructor(message, options = {}) {
|
|
52
|
+
super(`Provider error (${options.provider || 'unknown'}): ${message}`, options);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class AuthenticationError extends ApiError {
|
|
57
|
+
constructor(message, options = {}) {
|
|
58
|
+
super(`Authentication error: ${message}`, options);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class ToolError extends NatureCoError {
|
|
63
|
+
constructor(message, options = {}) {
|
|
64
|
+
super(message, options);
|
|
65
|
+
this.toolName = options.toolName || null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ToolInputError extends ToolError {
|
|
70
|
+
constructor(message, options = {}) {
|
|
71
|
+
super(`Invalid tool input: ${message}`, options);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class ToolExecutionError extends ToolError {
|
|
76
|
+
constructor(message, options = {}) {
|
|
77
|
+
super(`Tool execution failed: ${message}`, options);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class ChannelError extends NatureCoError {
|
|
82
|
+
constructor(message, options = {}) {
|
|
83
|
+
super(message, options);
|
|
84
|
+
this.channel = options.channel || null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class GatewayError extends NatureCoError {
|
|
89
|
+
constructor(message, options = {}) {
|
|
90
|
+
super(message, options);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class PluginError extends NatureCoError {
|
|
95
|
+
constructor(message, options = {}) {
|
|
96
|
+
super(message, options);
|
|
97
|
+
this.plugin = options.plugin || null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class SkillError extends NatureCoError {
|
|
102
|
+
constructor(message, options = {}) {
|
|
103
|
+
super(message, options);
|
|
104
|
+
this.skill = options.skill || null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class MigrationError extends NatureCoError {
|
|
109
|
+
constructor(message, options = {}) {
|
|
110
|
+
super(message, options);
|
|
111
|
+
this.step = options.step || null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function handleError(err, options = {}) {
|
|
116
|
+
const { prefix = '', exit = true, log = true } = options;
|
|
117
|
+
|
|
118
|
+
if (log) {
|
|
119
|
+
const message = err instanceof NatureCoError
|
|
120
|
+
? `${prefix}${err.message}`
|
|
121
|
+
: `${prefix}${err.message || 'An unknown error occurred'}`;
|
|
122
|
+
console.log(chalk.red(`\n${message}\n`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (exit) {
|
|
126
|
+
process.exit(err instanceof NatureCoError ? err.exitCode : 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
NatureCoError,
|
|
132
|
+
ConfigError,
|
|
133
|
+
ConfigParseError,
|
|
134
|
+
ConfigMutationConflictError,
|
|
135
|
+
ConfigValidationError,
|
|
136
|
+
ApiError,
|
|
137
|
+
ProviderError,
|
|
138
|
+
AuthenticationError,
|
|
139
|
+
ToolError,
|
|
140
|
+
ToolInputError,
|
|
141
|
+
ToolExecutionError,
|
|
142
|
+
ChannelError,
|
|
143
|
+
GatewayError,
|
|
144
|
+
PluginError,
|
|
145
|
+
SkillError,
|
|
146
|
+
MigrationError,
|
|
147
|
+
handleError,
|
|
148
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { select, input, password, confirm } = require('@inquirer/prompts');
|
|
1
|
+
const { select, input, password, confirm, checkbox } = require('@inquirer/prompts');
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
async prompt(questions) {
|
|
@@ -14,7 +14,6 @@ module.exports = {
|
|
|
14
14
|
} else if (q.type === 'password') {
|
|
15
15
|
results[q.name] = await password({ message: q.message, mask: q.mask });
|
|
16
16
|
} else if (q.type === 'checkbox') {
|
|
17
|
-
const { checkbox } = require('@inquirer/prompts');
|
|
18
17
|
results[q.name] = await checkbox({
|
|
19
18
|
message: q.message,
|
|
20
19
|
choices: q.choices.map(c =>
|
package/src/utils/path-utils.js
CHANGED
|
@@ -3,19 +3,19 @@ const os = require('os');
|
|
|
3
3
|
function normalizeWindowsPaths(str) {
|
|
4
4
|
let result = str.replace(/\\/g, '/');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
const homeDir = os.homedir().replace(/\\/g, '/');
|
|
7
|
+
|
|
8
|
+
// Replace any Windows user profile path with actual homedir
|
|
9
|
+
result = result.replace(/[A-Za-z]:\/Users\/[^/]+\//g, `${homeDir}/`);
|
|
10
|
+
|
|
11
|
+
// Replace bare drive-letter references pointing to .openclaw
|
|
12
|
+
result = result.replace(/[A-Za-z]:\/\.openclaw\//g, `${homeDir}/.natureco/`);
|
|
13
|
+
|
|
14
|
+
// Migrate .openclaw paths to .natureco
|
|
15
|
+
result = result.replace(/\.openclaw\//g, '.natureco/');
|
|
16
|
+
|
|
17
|
+
// Normalize mixed path separators
|
|
18
|
+
result = result.replace(/workspace\/scripts\\/g, 'workspace/scripts/');
|
|
19
19
|
|
|
20
20
|
return result;
|
|
21
21
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { PluginError, handleError } = require('./errors');
|
|
6
|
+
|
|
7
|
+
const PLUGINS_DIR = path.join(os.homedir(), '.natureco', 'plugins');
|
|
8
|
+
const REGISTRY_FILE = path.join(os.homedir(), '.natureco', 'plugin-registry.json');
|
|
9
|
+
|
|
10
|
+
function ensureDir(dir) {
|
|
11
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadRegistry() {
|
|
15
|
+
if (!fs.existsSync(REGISTRY_FILE)) return { version: 1, plugins: [], updatedAt: null };
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return { version: 1, plugins: [], updatedAt: null };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function saveRegistry(registry) {
|
|
24
|
+
ensureDir(path.dirname(REGISTRY_FILE));
|
|
25
|
+
registry.updatedAt = new Date().toISOString();
|
|
26
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(registry, null, 2), 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scanInstalled() {
|
|
30
|
+
ensureDir(PLUGINS_DIR);
|
|
31
|
+
return fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
|
32
|
+
.filter(d => d.isDirectory())
|
|
33
|
+
.map(d => {
|
|
34
|
+
const manifestFile = path.join(PLUGINS_DIR, d.name, 'plugin.json');
|
|
35
|
+
const pkgFile = path.join(PLUGINS_DIR, d.name, 'package.json');
|
|
36
|
+
const manifest = readManifest(manifestFile, pkgFile, d.name);
|
|
37
|
+
const disabledFile = path.join(PLUGINS_DIR, d.name, '.disabled');
|
|
38
|
+
return { ...manifest, slug: d.name, enabled: !fs.existsSync(disabledFile), installPath: path.join(PLUGINS_DIR, d.name) };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readManifest(manifestFile, pkgFile, fallbackName) {
|
|
43
|
+
let meta = { name: fallbackName, description: '', version: '1.0.0', author: '', license: 'MIT', keywords: [], entry: 'index.js' };
|
|
44
|
+
if (fs.existsSync(pkgFile)) {
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
|
|
47
|
+
meta = { ...meta, name: pkg.name || meta.name, description: pkg.description || meta.description, version: pkg.version || meta.version, author: pkg.author || meta.author, license: pkg.license || meta.license, keywords: pkg.keywords || meta.keywords, entry: pkg.main || meta.entry, dependencies: pkg.dependencies, openclaw: pkg.openclaw };
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
if (fs.existsSync(manifestFile)) {
|
|
51
|
+
try {
|
|
52
|
+
const m = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
53
|
+
meta = { ...meta, ...m };
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
return meta;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateManifest(manifest) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
if (!manifest.name) errors.push('Plugin adı (name) gerekli');
|
|
62
|
+
if (!manifest.entry && !manifest.openclaw?.tool) errors.push('Entry noktası (entry/index.js) gerekli');
|
|
63
|
+
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) errors.push('Version semver formatında olmalı (x.y.z)');
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getInstalledIds() {
|
|
68
|
+
const registry = loadRegistry();
|
|
69
|
+
const installed = new Set(registry.plugins.map(p => p.id));
|
|
70
|
+
scanInstalled().forEach(p => installed.add(p.slug));
|
|
71
|
+
return [...installed];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getPlugin(slug) {
|
|
75
|
+
return scanInstalled().find(p => p.slug === slug) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function installPlugin(spec) {
|
|
79
|
+
ensureDir(PLUGINS_DIR);
|
|
80
|
+
|
|
81
|
+
if (spec.startsWith('./') || spec.startsWith('/') || spec.startsWith('.\\') || spec.includes(':\\') || spec.startsWith('\\\\')) {
|
|
82
|
+
return installLocal(spec);
|
|
83
|
+
}
|
|
84
|
+
if (spec.startsWith('clawhub:') || spec.startsWith('naturehub:')) {
|
|
85
|
+
return installFromHub(spec);
|
|
86
|
+
}
|
|
87
|
+
if (spec.startsWith('npm:')) {
|
|
88
|
+
return installFromNpm(spec.slice(4));
|
|
89
|
+
}
|
|
90
|
+
if (spec.startsWith('git:')) {
|
|
91
|
+
return installFromGit(spec.slice(4));
|
|
92
|
+
}
|
|
93
|
+
if (spec.includes('/') && !spec.startsWith('@')) {
|
|
94
|
+
return installFromNpm(spec);
|
|
95
|
+
}
|
|
96
|
+
return installFromNpm(spec);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function installLocal(spec) {
|
|
100
|
+
const src = path.resolve(spec);
|
|
101
|
+
if (!fs.existsSync(src)) throw new PluginError(`Yol bulunamadı: ${src}`, 'install', spec);
|
|
102
|
+
const slug = path.basename(src);
|
|
103
|
+
const dest = path.join(PLUGINS_DIR, slug);
|
|
104
|
+
if (fs.existsSync(dest)) {
|
|
105
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
108
|
+
const manifest = readManifest(path.join(dest, 'plugin.json'), path.join(dest, 'package.json'), slug);
|
|
109
|
+
const errors = validateManifest(manifest);
|
|
110
|
+
if (errors.length > 0) {
|
|
111
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
112
|
+
throw new PluginError(`Geçersiz manifest: ${errors.join(', ')}`, 'install', spec);
|
|
113
|
+
}
|
|
114
|
+
const registry = loadRegistry();
|
|
115
|
+
registry.plugins = registry.plugins.filter(p => p.id !== slug);
|
|
116
|
+
registry.plugins.push({ id: slug, name: manifest.name, version: manifest.version, source: 'local', spec, installedAt: new Date().toISOString() });
|
|
117
|
+
saveRegistry(registry);
|
|
118
|
+
return { slug, name: manifest.name, version: manifest.version, source: 'local' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function installFromNpm(pkg) {
|
|
122
|
+
const tmpDir = path.join(os.tmpdir(), `nc-plugin-${Date.now()}`);
|
|
123
|
+
ensureDir(tmpDir);
|
|
124
|
+
try {
|
|
125
|
+
execSync(`npm install ${pkg} --prefix "${tmpDir}" --no-save --ignore-scripts --no-audit --no-fund`, { stdio: 'pipe', timeout: 120000 });
|
|
126
|
+
const pkgDir = path.join(tmpDir, 'node_modules', pkg.split('/').pop());
|
|
127
|
+
const scopedPkgDir = pkg.startsWith('@') ? path.join(tmpDir, 'node_modules', pkg) : null;
|
|
128
|
+
const srcDir = scopedPkgDir && fs.existsSync(scopedPkgDir) ? scopedPkgDir : (fs.existsSync(pkgDir) ? pkgDir : null);
|
|
129
|
+
if (!srcDir) throw new PluginError(`Paket bulunamadı: ${pkg}`, 'install', pkg);
|
|
130
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(srcDir, 'package.json'), 'utf-8'));
|
|
131
|
+
const slug = pkgJson.name?.replace(/@/g, '').replace(/\//g, '-') || pkg.replace(/[@\/]/g, '-').replace(/^-/, '');
|
|
132
|
+
const dest = path.join(PLUGINS_DIR, slug);
|
|
133
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
134
|
+
fs.cpSync(srcDir, dest, { recursive: true });
|
|
135
|
+
const manifest = readManifest(path.join(dest, 'plugin.json'), path.join(dest, 'package.json'), slug);
|
|
136
|
+
const errors = validateManifest(manifest);
|
|
137
|
+
if (errors.length > 0) {
|
|
138
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
139
|
+
throw new PluginError(`Geçersiz manifest: ${errors.join(', ')}`, 'install', pkg);
|
|
140
|
+
}
|
|
141
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
142
|
+
const registry = loadRegistry();
|
|
143
|
+
registry.plugins = registry.plugins.filter(p => p.id !== slug);
|
|
144
|
+
registry.plugins.push({ id: slug, name: manifest.name, version: manifest.version, source: 'npm', spec: pkg, installedAt: new Date().toISOString() });
|
|
145
|
+
saveRegistry(registry);
|
|
146
|
+
return { slug, name: manifest.name, version: manifest.version, source: 'npm' };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function installFromGit(spec) {
|
|
154
|
+
const repoUrl = spec.startsWith('github.com/') ? `https://${spec}` : spec.startsWith('http') ? spec : spec.includes('/') ? `https://github.com/${spec}` : spec;
|
|
155
|
+
const tmpDir = path.join(os.tmpdir(), `nc-plugin-git-${Date.now()}`);
|
|
156
|
+
ensureDir(tmpDir);
|
|
157
|
+
try {
|
|
158
|
+
execSync(`git clone --depth 1 "${repoUrl}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
159
|
+
const slug = spec.split('/').pop().replace(/\.git$/, '');
|
|
160
|
+
const dest = path.join(PLUGINS_DIR, slug);
|
|
161
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
162
|
+
const items = fs.readdirSync(tmpDir);
|
|
163
|
+
const subDir = items.length === 1 && fs.statSync(path.join(tmpDir, items[0])).isDirectory() ? path.join(tmpDir, items[0]) : tmpDir;
|
|
164
|
+
fs.cpSync(subDir, dest, { recursive: true });
|
|
165
|
+
const manifest = readManifest(path.join(dest, 'plugin.json'), path.join(dest, 'package.json'), slug);
|
|
166
|
+
const errors = validateManifest(manifest);
|
|
167
|
+
if (errors.length > 0) {
|
|
168
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
169
|
+
throw new PluginError(`Geçersiz manifest: ${errors.join(', ')}`, 'install', spec);
|
|
170
|
+
}
|
|
171
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
172
|
+
const registry = loadRegistry();
|
|
173
|
+
registry.plugins = registry.plugins.filter(p => p.id !== slug);
|
|
174
|
+
registry.plugins.push({ id: slug, name: manifest.name, version: manifest.version, source: 'git', spec: repoUrl, installedAt: new Date().toISOString() });
|
|
175
|
+
saveRegistry(registry);
|
|
176
|
+
return { slug, name: manifest.name, version: manifest.version, source: 'git' };
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function installFromHub(spec) {
|
|
184
|
+
const pkg = spec.replace(/^(clawhub|naturehub):\/?/, '');
|
|
185
|
+
return installFromNpm(pkg);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function uninstallPlugin(slug, options = {}) {
|
|
189
|
+
const plugin = getPlugin(slug);
|
|
190
|
+
if (!plugin) throw new PluginError(`Plugin bulunamadı: ${slug}`, 'uninstall', slug);
|
|
191
|
+
if (!options.keepFiles) {
|
|
192
|
+
fs.rmSync(plugin.installPath, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
const registry = loadRegistry();
|
|
195
|
+
registry.plugins = registry.plugins.filter(p => p.id !== slug);
|
|
196
|
+
saveRegistry(registry);
|
|
197
|
+
return { slug, name: plugin.name };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function updatePlugin(slug) {
|
|
201
|
+
const plugin = getPlugin(slug);
|
|
202
|
+
if (!plugin) throw new PluginError(`Plugin bulunamadı: ${slug}`, 'update', slug);
|
|
203
|
+
const registry = loadRegistry();
|
|
204
|
+
const record = registry.plugins.find(p => p.id === slug);
|
|
205
|
+
if (!record || !record.spec) throw new PluginError(`Plugin kaydı bulunamadı veya spec eksik: ${slug}`, 'update', slug);
|
|
206
|
+
if (record.source === 'npm') {
|
|
207
|
+
fs.rmSync(plugin.installPath, { recursive: true, force: true });
|
|
208
|
+
return installFromNpm(record.spec);
|
|
209
|
+
}
|
|
210
|
+
if (record.source === 'git' && record.spec) {
|
|
211
|
+
fs.rmSync(plugin.installPath, { recursive: true, force: true });
|
|
212
|
+
return installFromGit(record.spec);
|
|
213
|
+
}
|
|
214
|
+
throw new PluginError(`${record.source} kaynağından güncelleme desteklenmiyor`, 'update', slug);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function searchRegistry(query) {
|
|
218
|
+
const registry = loadRegistry();
|
|
219
|
+
const q = query.toLowerCase();
|
|
220
|
+
return registry.plugins.filter(p =>
|
|
221
|
+
p.id.toLowerCase().includes(q) ||
|
|
222
|
+
(p.name || '').toLowerCase().includes(q)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
PLUGINS_DIR,
|
|
228
|
+
loadRegistry,
|
|
229
|
+
saveRegistry,
|
|
230
|
+
scanInstalled,
|
|
231
|
+
getPlugin,
|
|
232
|
+
getInstalledIds,
|
|
233
|
+
installPlugin,
|
|
234
|
+
uninstallPlugin,
|
|
235
|
+
updatePlugin,
|
|
236
|
+
searchRegistry,
|
|
237
|
+
validateManifest,
|
|
238
|
+
};
|