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/api.js
CHANGED
|
@@ -317,8 +317,6 @@ function encodeToolResult(toolResult) {
|
|
|
317
317
|
content = toolResult;
|
|
318
318
|
} else if (toolResult.output) {
|
|
319
319
|
content = toolResult.output;
|
|
320
|
-
/* DEAD CODE */ } else if (toolResult.data) {
|
|
321
|
-
content = JSON.stringify(toolResult.data); /* DEAD CODE */
|
|
322
320
|
} else if (toolResult.success !== undefined) {
|
|
323
321
|
// Handle { success: true/false, output/error: ... } format
|
|
324
322
|
content = toolResult.success ? (toolResult.output || JSON.stringify(toolResult)) : (toolResult.error || 'Unknown error');
|
|
@@ -429,9 +427,7 @@ function formatToolsForAnthropic() {
|
|
|
429
427
|
*/
|
|
430
428
|
async function sendMessageOpenAICompatible(providerConfig, messages, tools) {
|
|
431
429
|
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
432
|
-
const endpoint = baseUrl
|
|
433
|
-
? 'https://api.natureco.me/api/v1/chat/completions'
|
|
434
|
-
: `${baseUrl}/chat/completions`;
|
|
430
|
+
const endpoint = `${baseUrl}/chat/completions`;
|
|
435
431
|
const requestBody = {
|
|
436
432
|
model: providerConfig.model,
|
|
437
433
|
messages: messages,
|
|
@@ -859,16 +855,8 @@ async function getBots(apiKey) {
|
|
|
859
855
|
}
|
|
860
856
|
} catch (e) {
|
|
861
857
|
debugLog('[getBots] NatureCo API error:', e.message);
|
|
858
|
+
return { bots: [], error: e.message };
|
|
862
859
|
}
|
|
863
|
-
// Fallback — API'den bot gelmezse universal provider döndür
|
|
864
|
-
return {
|
|
865
|
-
bots: [{
|
|
866
|
-
id: 'universal-provider',
|
|
867
|
-
name: 'Universal Provider (NatureCo)',
|
|
868
|
-
ai_provider: 'natureco',
|
|
869
|
-
model: 'natureco-default',
|
|
870
|
-
}]
|
|
871
|
-
};
|
|
872
860
|
}
|
|
873
861
|
|
|
874
862
|
// Diğer provider'lar — universal provider döndür
|
|
@@ -889,9 +877,6 @@ async function getBots(apiKey) {
|
|
|
889
877
|
// ── Streaming Support ────────────────────────────────────────────────────────────
|
|
890
878
|
|
|
891
879
|
async function streamProviderCompletion(providerConfig, messages, tools) {
|
|
892
|
-
if (providerConfig.url.includes('api.natureco.me')) {
|
|
893
|
-
return sendMessageOpenAICompatible(providerConfig, messages, tools);
|
|
894
|
-
}
|
|
895
880
|
if (providerConfig.isAnthropic) {
|
|
896
881
|
return streamAnthropicCompletion(providerConfig, messages);
|
|
897
882
|
}
|
|
@@ -900,9 +885,7 @@ async function streamProviderCompletion(providerConfig, messages, tools) {
|
|
|
900
885
|
|
|
901
886
|
async function streamOpenAICompletion(providerConfig, messages, tools) {
|
|
902
887
|
const baseUrl = providerConfig.url.replace(/\/+$/, '');
|
|
903
|
-
const endpoint = baseUrl
|
|
904
|
-
? 'https://api.natureco.me/api/v1/chat/completions'
|
|
905
|
-
: `${baseUrl}/chat/completions`;
|
|
888
|
+
const endpoint = `${baseUrl}/chat/completions`;
|
|
906
889
|
|
|
907
890
|
const requestBody = {
|
|
908
891
|
model: providerConfig.model,
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const inquirer = require('./inquirer-wrapper');
|
|
7
|
+
const { NatureCoError } = require('./errors');
|
|
8
|
+
|
|
9
|
+
const APPROVALS_FILE = path.join(os.homedir(), '.natureco', 'exec-approvals.json');
|
|
10
|
+
const APPROVALS_SOCKET_PATH = path.join(os.homedir(), '.natureco', 'exec-approvals.sock');
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 1800000; // 30 min
|
|
12
|
+
|
|
13
|
+
class ExecApprovalError extends NatureCoError {
|
|
14
|
+
constructor(message, options = {}) {
|
|
15
|
+
super(message, options);
|
|
16
|
+
this.command = options.command || null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// -- Data types --
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {'deny'|'allowlist'|'full'} ExecSecurity
|
|
24
|
+
* @typedef {'off'|'on-miss'|'always'} ExecAsk
|
|
25
|
+
* @typedef {'deny'|'allowlist'|'ask'|'auto'|'full'} ExecMode
|
|
26
|
+
* @typedef {{ id?: string, pattern: string, argPattern?: string, source?: string, lastUsedAt?: string, lastUsedCommand?: string }} AllowlistEntry
|
|
27
|
+
* @typedef {{ version: 1, defaults?: { security?: ExecSecurity, ask?: ExecAsk }, agents?: Record<string, { security?: ExecSecurity, ask?: ExecAsk, allowlist?: AllowlistEntry[] }> }} ApprovalsFile
|
|
28
|
+
* @typedef {'allow-once'|'allow-always'|'deny'} ApprovalDecision
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function getApprovalsPath() {
|
|
32
|
+
return APPROVALS_FILE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadApprovals() {
|
|
36
|
+
if (!fs.existsSync(APPROVALS_FILE)) {
|
|
37
|
+
return { version: 1, defaults: { security: 'full', ask: 'off' }, agents: {} };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fs.readFileSync(APPROVALS_FILE, 'utf8'));
|
|
41
|
+
} catch {
|
|
42
|
+
return { version: 1, defaults: { security: 'full', ask: 'off' }, agents: {} };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveApprovals(data) {
|
|
47
|
+
const dir = path.dirname(APPROVALS_FILE);
|
|
48
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
fs.writeFileSync(APPROVALS_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveEffectivePolicy(agentId) {
|
|
53
|
+
const file = loadApprovals();
|
|
54
|
+
const defaults = file.defaults || { security: 'full', ask: 'off' };
|
|
55
|
+
if (!agentId || !file.agents?.[agentId]) {
|
|
56
|
+
return { security: defaults.security || 'full', ask: defaults.ask || 'off', allowlist: [] };
|
|
57
|
+
}
|
|
58
|
+
const agent = file.agents[agentId];
|
|
59
|
+
return {
|
|
60
|
+
security: agent.security || defaults.security || 'full',
|
|
61
|
+
ask: agent.ask || defaults.ask || 'off',
|
|
62
|
+
allowlist: agent.allowlist || [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveMode(security, ask) {
|
|
67
|
+
if (security === 'deny') return 'deny';
|
|
68
|
+
if (security === 'allowlist' && ask === 'always') return 'ask';
|
|
69
|
+
if (security === 'allowlist') return 'allowlist';
|
|
70
|
+
if (security === 'full') return 'full';
|
|
71
|
+
return 'full';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchAllowlist(entries, command) {
|
|
75
|
+
if (!entries || !command) return null;
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
try {
|
|
78
|
+
const pattern = new RegExp(entry.pattern, 'i');
|
|
79
|
+
if (pattern.test(command)) {
|
|
80
|
+
if (entry.argPattern) {
|
|
81
|
+
const argRe = new RegExp(entry.argPattern, 'i');
|
|
82
|
+
const args = command.split(/\s+/).slice(1).join(' ');
|
|
83
|
+
if (!argRe.test(args)) continue;
|
|
84
|
+
}
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function requiresApproval({ command, agentId, security, ask }) {
|
|
93
|
+
const policy = resolveEffectivePolicy(agentId);
|
|
94
|
+
const mode = resolveMode(security || policy.security, ask || policy.ask);
|
|
95
|
+
|
|
96
|
+
if (mode === 'deny') return { required: true, reason: 'deny' };
|
|
97
|
+
if (mode === 'full') return { required: false, reason: 'full' };
|
|
98
|
+
|
|
99
|
+
// Check allowlist
|
|
100
|
+
const match = matchAllowlist(policy.allowlist, command);
|
|
101
|
+
if (match) return { required: false, reason: 'allowlist', entry: match };
|
|
102
|
+
|
|
103
|
+
if (mode === 'allowlist') return { required: true, reason: 'not-in-allowlist' };
|
|
104
|
+
if (mode === 'ask') return { required: true, reason: 'ask' };
|
|
105
|
+
|
|
106
|
+
return { required: true, reason: 'unknown' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Built-in safe commands that never need approval
|
|
110
|
+
const SAFE_COMMANDS = new Set([
|
|
111
|
+
'ls', 'cat', 'head', 'tail', 'echo', 'pwd', 'date', 'whoami',
|
|
112
|
+
'node -e', 'node -v', 'npm -v', 'git status', 'git diff', 'git log',
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
function isSafeCommand(command) {
|
|
116
|
+
if (SAFE_COMMANDS.has(command.trim())) return true;
|
|
117
|
+
for (const safe of SAFE_COMMANDS) {
|
|
118
|
+
if (command.trim().startsWith(safe)) return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Known dangerous patterns that should always warn
|
|
124
|
+
const DANGEROUS_PATTERNS = [
|
|
125
|
+
/^rm\s+-rf\s+\/\s*$/,
|
|
126
|
+
/^mkfs/,
|
|
127
|
+
/^dd\s+if=.*\s+of=\/dev/,
|
|
128
|
+
/^:\(\)\s*\{.*:\(\)\s*;\s*\};/,
|
|
129
|
+
/^chmod\s+-R\s+777\s+\//,
|
|
130
|
+
/^chown\s+-R/,
|
|
131
|
+
/^>\/dev\/sda/,
|
|
132
|
+
/^\|.*sh$/,
|
|
133
|
+
/^curl.*\|.*sh$/,
|
|
134
|
+
/^wget.*\|.*sh$/,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
function isDangerousCommand(command) {
|
|
138
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
139
|
+
if (pattern.test(command.trim())) return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function requestUserApproval(command, options = {}) {
|
|
145
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, agentId } = options;
|
|
146
|
+
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(chalk.yellow(' ⚠️ Command requires approval'));
|
|
149
|
+
console.log(chalk.gray(' ─'.repeat(30)));
|
|
150
|
+
console.log(chalk.white(' ') + command);
|
|
151
|
+
console.log(chalk.gray(' ─'.repeat(30)));
|
|
152
|
+
|
|
153
|
+
const choices = [
|
|
154
|
+
{ value: 'allow-once', name: 'Allow once' },
|
|
155
|
+
{ value: 'allow-always', name: 'Always allow this command' },
|
|
156
|
+
{ value: 'deny', name: 'Deny' },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// Add edit option if command is dangerous
|
|
160
|
+
if (options.isDangerous) {
|
|
161
|
+
choices.push({ value: 'edit', name: 'Edit command' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
process.stdin.resume();
|
|
165
|
+
const { decision } = await inquirer.prompt([{
|
|
166
|
+
type: 'list',
|
|
167
|
+
name: 'decision',
|
|
168
|
+
message: 'What would you like to do?',
|
|
169
|
+
choices,
|
|
170
|
+
}]);
|
|
171
|
+
|
|
172
|
+
if (decision === 'edit') {
|
|
173
|
+
const { edited } = await inquirer.prompt([{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'edited',
|
|
176
|
+
message: 'Edit command:',
|
|
177
|
+
default: command,
|
|
178
|
+
}]);
|
|
179
|
+
return { decision: 'allow-once', command: edited };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (decision === 'allow-always') {
|
|
183
|
+
addAllowlistEntry(agentId, command);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { decision, command };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function addAllowlistEntry(agentId, command) {
|
|
190
|
+
const file = loadApprovals();
|
|
191
|
+
if (!file.agents) file.agents = {};
|
|
192
|
+
if (!file.agents[agentId]) file.agents[agentId] = { allowlist: [] };
|
|
193
|
+
|
|
194
|
+
// Escape special regex chars in the command for pattern matching
|
|
195
|
+
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
196
|
+
const entry = {
|
|
197
|
+
id: `auto-${Date.now()}`,
|
|
198
|
+
pattern: `^${escaped}$`,
|
|
199
|
+
source: 'allow-always',
|
|
200
|
+
lastUsedAt: new Date().toISOString(),
|
|
201
|
+
lastUsedCommand: command,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
file.agents[agentId].allowlist.push(entry);
|
|
205
|
+
saveApprovals(file);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function setSecurityPolicy(agentId, options = {}) {
|
|
209
|
+
const file = loadApprovals();
|
|
210
|
+
if (!file.agents) file.agents = {};
|
|
211
|
+
if (!file.agents[agentId]) file.agents[agentId] = {};
|
|
212
|
+
|
|
213
|
+
if (options.security) file.agents[agentId].security = options.security;
|
|
214
|
+
if (options.ask !== undefined) file.agents[agentId].ask = options.ask;
|
|
215
|
+
|
|
216
|
+
saveApprovals(file);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function checkCommand(command, options = {}) {
|
|
220
|
+
const { agentId = 'default' } = options;
|
|
221
|
+
|
|
222
|
+
// Empty command
|
|
223
|
+
if (!command || !command.trim()) {
|
|
224
|
+
return { allowed: false, reason: 'empty' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if safe
|
|
228
|
+
if (isSafeCommand(command)) {
|
|
229
|
+
return { allowed: true, reason: 'safe-command' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if dangerous
|
|
233
|
+
const dangerous = isDangerousCommand(command);
|
|
234
|
+
const policy = resolveEffectivePolicy(agentId);
|
|
235
|
+
const mode = resolveMode(policy.security, policy.ask);
|
|
236
|
+
|
|
237
|
+
if (mode === 'deny') {
|
|
238
|
+
return { allowed: false, reason: 'denied-by-policy', policy };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check allowlist
|
|
242
|
+
const match = matchAllowlist(policy.allowlist, command);
|
|
243
|
+
if (match) {
|
|
244
|
+
return { allowed: true, reason: 'allowlist', entry: match };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (mode === 'allowlist') {
|
|
248
|
+
return { allowed: false, reason: 'not-in-allowlist', policy };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (mode === 'ask') {
|
|
252
|
+
const result = await requestUserApproval(command, { ...options, isDangerous: dangerous });
|
|
253
|
+
return {
|
|
254
|
+
allowed: result.decision === 'allow-once' || result.decision === 'allow-always',
|
|
255
|
+
reason: result.decision,
|
|
256
|
+
editedCommand: result.command,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Full mode - always allow
|
|
261
|
+
return { allowed: true, reason: 'full-mode' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function listAllowlist(agentId) {
|
|
265
|
+
const policy = resolveEffectivePolicy(agentId);
|
|
266
|
+
return policy.allowlist || [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function removeAllowlistEntry(agentId, entryId) {
|
|
270
|
+
const file = loadApprovals();
|
|
271
|
+
if (!file.agents?.[agentId]?.allowlist) return false;
|
|
272
|
+
const before = file.agents[agentId].allowlist.length;
|
|
273
|
+
file.agents[agentId].allowlist = file.agents[agentId].allowlist.filter(e => e.id !== entryId);
|
|
274
|
+
saveApprovals(file);
|
|
275
|
+
return file.agents[agentId].allowlist.length < before;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
ExecApprovalError,
|
|
280
|
+
loadApprovals,
|
|
281
|
+
saveApprovals,
|
|
282
|
+
resolveEffectivePolicy,
|
|
283
|
+
resolveMode,
|
|
284
|
+
matchAllowlist,
|
|
285
|
+
requiresApproval,
|
|
286
|
+
isSafeCommand,
|
|
287
|
+
isDangerousCommand,
|
|
288
|
+
requestUserApproval,
|
|
289
|
+
addAllowlistEntry,
|
|
290
|
+
setSecurityPolicy,
|
|
291
|
+
checkCommand,
|
|
292
|
+
listAllowlist,
|
|
293
|
+
removeAllowlistEntry,
|
|
294
|
+
getApprovalsPath,
|
|
295
|
+
DANGEROUS_PATTERNS,
|
|
296
|
+
SAFE_COMMANDS,
|
|
297
|
+
};
|
package/src/utils/background.js
CHANGED
|
@@ -1,66 +1,223 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const TASKS_FILE = path.join(os.homedir(), '.natureco', 'tasks.json');
|
|
6
|
+
const TASK_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7
|
+
const STALE_QUEUED_MS = 10 * 60 * 1000;
|
|
8
|
+
const STALE_RUNNING_MS = 30 * 60 * 1000;
|
|
9
|
+
const LOST_GRACE_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
const TASK_STATUSES = ['queued', 'running', 'succeeded', 'failed', 'timed_out', 'cancelled', 'lost'];
|
|
12
|
+
const TERMINAL_STATUSES = ['succeeded', 'failed', 'timed_out', 'cancelled', 'lost'];
|
|
13
|
+
const RUNTIME_TYPES = ['cli', 'cron', 'subagent', 'acp'];
|
|
14
|
+
const NOTIFY_POLICIES = ['done_only', 'state_changes', 'silent'];
|
|
15
|
+
|
|
16
|
+
function ensureDir() {
|
|
17
|
+
const dir = path.dirname(TASKS_FILE);
|
|
18
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadTasks() {
|
|
22
|
+
ensureDir();
|
|
23
|
+
if (!fs.existsSync(TASKS_FILE)) return [];
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(TASKS_FILE, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveTasks(tasks) {
|
|
32
|
+
ensureDir();
|
|
33
|
+
fs.writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2), 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateId() {
|
|
37
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nowISO() {
|
|
41
|
+
return new Date().toISOString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createTask(data) {
|
|
45
|
+
const tasks = loadTasks();
|
|
46
|
+
const task = {
|
|
47
|
+
id: generateId(),
|
|
48
|
+
status: 'queued',
|
|
49
|
+
runtime: data.runtime || 'cli',
|
|
50
|
+
message: data.message || '',
|
|
51
|
+
botName: data.botName || 'default',
|
|
52
|
+
childSessionKey: data.childSessionKey || null,
|
|
53
|
+
requesterSessionKey: data.requesterSessionKey || null,
|
|
54
|
+
runId: data.runId || null,
|
|
55
|
+
notifyPolicy: data.notifyPolicy || 'done_only',
|
|
56
|
+
createdAt: nowISO(),
|
|
57
|
+
startedAt: null,
|
|
58
|
+
endedAt: null,
|
|
59
|
+
error: null,
|
|
60
|
+
result: null,
|
|
61
|
+
cleanupAfter: null,
|
|
62
|
+
metadata: data.metadata || {},
|
|
63
|
+
};
|
|
64
|
+
tasks.push(task);
|
|
65
|
+
saveTasks(tasks);
|
|
66
|
+
return task;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function updateTask(id, updates) {
|
|
70
|
+
const tasks = loadTasks();
|
|
71
|
+
const idx = tasks.findIndex(t => t.id === id);
|
|
72
|
+
if (idx === -1) return null;
|
|
73
|
+
|
|
74
|
+
const now = nowISO();
|
|
75
|
+
if (updates.status === 'running' && !tasks[idx].startedAt) {
|
|
76
|
+
updates.startedAt = now;
|
|
77
|
+
}
|
|
78
|
+
if (updates.status && TERMINAL_STATUSES.includes(updates.status) && !tasks[idx].endedAt) {
|
|
79
|
+
updates.endedAt = now;
|
|
80
|
+
updates.cleanupAfter = new Date(Date.now() + TASK_RETENTION_MS).toISOString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tasks[idx] = { ...tasks[idx], ...updates };
|
|
84
|
+
saveTasks(tasks);
|
|
85
|
+
return tasks[idx];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getTask(id) {
|
|
89
|
+
const tasks = loadTasks();
|
|
90
|
+
return tasks.find(t => t.id === id) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getTasksBySession(sessionKey) {
|
|
94
|
+
const tasks = loadTasks();
|
|
95
|
+
return tasks.filter(t => t.requesterSessionKey === sessionKey || t.childSessionKey === sessionKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cancelTask(id) {
|
|
99
|
+
const task = getTask(id);
|
|
100
|
+
if (!task) return null;
|
|
101
|
+
if (TERMINAL_STATUSES.includes(task.status)) return task;
|
|
102
|
+
return updateTask(id, { status: 'cancelled' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function listTasks(options = {}) {
|
|
106
|
+
let tasks = loadTasks();
|
|
107
|
+
|
|
108
|
+
if (options.runtime) {
|
|
109
|
+
tasks = tasks.filter(t => t.runtime === options.runtime);
|
|
110
|
+
}
|
|
111
|
+
if (options.status) {
|
|
112
|
+
tasks = tasks.filter(t => t.status === options.status);
|
|
113
|
+
}
|
|
114
|
+
if (options.limit) {
|
|
115
|
+
tasks = tasks.slice(0, options.limit);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return tasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function auditTasks() {
|
|
122
|
+
const tasks = loadTasks();
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const findings = [];
|
|
125
|
+
|
|
126
|
+
tasks.forEach(t => {
|
|
127
|
+
const createdAt = new Date(t.createdAt).getTime();
|
|
128
|
+
|
|
129
|
+
if (t.status === 'queued' && (now - createdAt) > STALE_QUEUED_MS) {
|
|
130
|
+
findings.push({ id: t.id, severity: 'warn', code: 'stale_queued', message: `${Math.round((now - createdAt) / 1000)}s boyunca kuyrukta bekliyor` });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (t.status === 'running' && t.startedAt) {
|
|
134
|
+
const startedAt = new Date(t.startedAt).getTime();
|
|
135
|
+
if ((now - startedAt) > STALE_RUNNING_MS) {
|
|
136
|
+
findings.push({ id: t.id, severity: 'error', code: 'stale_running', message: `${Math.round((now - startedAt) / 1000)}s boyunca çalışıyor` });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (t.status === 'lost') {
|
|
141
|
+
const sev = t.cleanupAfter && new Date(t.cleanupAfter).getTime() > now ? 'warn' : 'error';
|
|
142
|
+
findings.push({ id: t.id, severity: sev, code: 'lost', message: `Runtime backing kayboldu` });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (TERMINAL_STATUSES.includes(t.status) && !t.cleanupAfter) {
|
|
146
|
+
findings.push({ id: t.id, severity: 'warn', code: 'missing_cleanup', message: 'Terminal task has no cleanup timestamp' });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (t.endedAt && t.startedAt && new Date(t.endedAt) < new Date(t.startedAt)) {
|
|
150
|
+
findings.push({ id: t.id, severity: 'warn', code: 'inconsistent_timestamps', message: 'Bitiş zamanı başlangıçtan önce' });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function maintenanceTasks(dryRun = true) {
|
|
158
|
+
const tasks = loadTasks();
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const stats = { pruned: 0, reconciled: 0, cleaned: 0 };
|
|
161
|
+
|
|
162
|
+
const remaining = tasks.filter(t => {
|
|
163
|
+
if (t.cleanupAfter && new Date(t.cleanupAfter).getTime() < now) {
|
|
164
|
+
if (!dryRun) stats.pruned++;
|
|
165
|
+
return dryRun;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (t.status === 'queued' && (now - new Date(t.createdAt).getTime()) > LOST_GRACE_MS) {
|
|
169
|
+
if (!dryRun) {
|
|
170
|
+
t.status = 'lost';
|
|
171
|
+
t.endedAt = nowISO();
|
|
172
|
+
t.cleanupAfter = new Date(now + TASK_RETENTION_MS).toISOString();
|
|
173
|
+
}
|
|
174
|
+
stats.reconciled++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (t.status === 'running' && t.startedAt && (now - new Date(t.startedAt).getTime()) > (STALE_RUNNING_MS + LOST_GRACE_MS)) {
|
|
178
|
+
if (!dryRun) {
|
|
179
|
+
t.status = 'lost';
|
|
180
|
+
t.endedAt = nowISO();
|
|
181
|
+
t.cleanupAfter = new Date(now + TASK_RETENTION_MS).toISOString();
|
|
182
|
+
}
|
|
183
|
+
stats.reconciled++;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (TERMINAL_STATUSES.includes(t.status) && !t.cleanupAfter) {
|
|
187
|
+
if (!dryRun) {
|
|
188
|
+
t.cleanupAfter = new Date(now + TASK_RETENTION_MS).toISOString();
|
|
189
|
+
}
|
|
190
|
+
stats.cleaned++;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!dryRun) saveTasks(remaining);
|
|
197
|
+
return { ...stats, remaining: dryRun ? remaining.length : remaining.length };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getTaskSummary() {
|
|
201
|
+
const tasks = loadTasks();
|
|
202
|
+
const active = tasks.filter(t => t.status === 'queued' || t.status === 'running').length;
|
|
203
|
+
const failures = tasks.filter(t => ['failed', 'timed_out', 'lost'].includes(t.status)).length;
|
|
204
|
+
const byRuntime = {};
|
|
205
|
+
RUNTIME_TYPES.forEach(r => { byRuntime[r] = tasks.filter(t => t.runtime === r).length; });
|
|
206
|
+
return { active, failures, byRuntime, total: tasks.length };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
createTask,
|
|
211
|
+
updateTask,
|
|
212
|
+
getTask,
|
|
213
|
+
getTasksBySession,
|
|
214
|
+
cancelTask,
|
|
215
|
+
listTasks,
|
|
216
|
+
auditTasks,
|
|
217
|
+
maintenanceTasks,
|
|
218
|
+
getTaskSummary,
|
|
219
|
+
TASK_STATUSES,
|
|
220
|
+
TERMINAL_STATUSES,
|
|
221
|
+
RUNTIME_TYPES,
|
|
222
|
+
NOTIFY_POLICIES,
|
|
223
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let _makeWASocket, _useMultiFileAuthState, _DisconnectReason, _fetchLatestBaileysVersion, _Browsers;
|
|
2
|
+
|
|
3
|
+
function loadBaileys() {
|
|
4
|
+
if (!_makeWASocket) {
|
|
5
|
+
const baileys = require('@whiskeysockets/baileys');
|
|
6
|
+
_makeWASocket = baileys.default;
|
|
7
|
+
_useMultiFileAuthState = baileys.useMultiFileAuthState;
|
|
8
|
+
_DisconnectReason = baileys.DisconnectReason;
|
|
9
|
+
_fetchLatestBaileysVersion = baileys.fetchLatestBaileysVersion;
|
|
10
|
+
_Browsers = baileys.Browsers;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
makeWASocket: _makeWASocket,
|
|
14
|
+
useMultiFileAuthState: _useMultiFileAuthState,
|
|
15
|
+
DisconnectReason: _DisconnectReason,
|
|
16
|
+
fetchLatestBaileysVersion: _fetchLatestBaileysVersion,
|
|
17
|
+
Browsers: _Browsers,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { loadBaileys };
|