natureco-cli 2.23.28 → 2.23.30
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/README.md +94 -11
- package/bin/natureco.js +470 -10
- package/package.json +10 -6
- package/src/commands/admin-rpc.js +219 -0
- package/src/commands/agent.js +89 -0
- package/src/commands/approvals.js +53 -0
- package/src/commands/backup.js +124 -0
- package/src/commands/bonjour.js +167 -0
- package/src/commands/capability.js +64 -0
- package/src/commands/channels.js +94 -4
- package/src/commands/chat.js +11 -25
- package/src/commands/clickclack.js +130 -0
- package/src/commands/commitments.js +32 -0
- package/src/commands/completion.js +76 -0
- package/src/commands/config.js +111 -68
- package/src/commands/configure.js +93 -0
- package/src/commands/crestodian.js +92 -0
- package/src/commands/daemon.js +60 -0
- package/src/commands/device-pair.js +248 -0
- package/src/commands/devices.js +110 -0
- package/src/commands/directory.js +47 -0
- package/src/commands/dns.js +58 -0
- package/src/commands/docs.js +43 -0
- package/src/commands/doctor.js +121 -16
- package/src/commands/exec-policy.js +71 -0
- package/src/commands/gateway-server.js +1175 -30
- package/src/commands/gateway.js +11 -20
- package/src/commands/health.js +18 -0
- package/src/commands/help.js +6 -0
- package/src/commands/imessage.js +169 -0
- package/src/commands/infer.js +73 -0
- package/src/commands/irc.js +119 -0
- package/src/commands/mattermost.js +164 -0
- package/src/commands/memory-cmd.js +134 -1
- package/src/commands/message.js +30 -4
- package/src/commands/migrate.js +213 -2
- package/src/commands/models.js +584 -216
- package/src/commands/node.js +98 -0
- package/src/commands/nodes.js +106 -0
- package/src/commands/oc-path.js +200 -0
- package/src/commands/onboard.js +70 -0
- package/src/commands/open-prose.js +67 -0
- package/src/commands/plugins.js +415 -172
- package/src/commands/policy.js +176 -0
- package/src/commands/proxy.js +155 -0
- package/src/commands/qr.js +28 -0
- package/src/commands/sandbox.js +125 -0
- package/src/commands/secrets.js +118 -0
- package/src/commands/security.js +149 -1
- package/src/commands/setup.js +114 -10
- package/src/commands/signal.js +495 -0
- package/src/commands/skills.js +20 -29
- package/src/commands/sms.js +168 -0
- package/src/commands/system.js +53 -0
- package/src/commands/tasks.js +328 -79
- package/src/commands/terminal.js +21 -0
- package/src/commands/thread-ownership.js +157 -0
- package/src/commands/transcripts.js +72 -0
- package/src/commands/voice.js +82 -0
- package/src/commands/vydra.js +98 -0
- package/src/commands/webhooks.js +79 -0
- package/src/commands/whatsapp.js +7 -21
- package/src/commands/workboard.js +207 -0
- package/src/tools/audio_understanding.js +154 -0
- package/src/tools/bash.js +63 -29
- package/src/tools/browser.js +112 -0
- package/src/tools/canvas.js +104 -0
- package/src/tools/document_extract.js +84 -0
- package/src/tools/duckduckgo.js +54 -0
- package/src/tools/exa_search.js +66 -0
- package/src/tools/firecrawl.js +104 -0
- package/src/tools/image_generation.js +99 -0
- package/src/tools/llm_task.js +118 -0
- package/src/tools/media_understanding.js +128 -0
- package/src/tools/music_generation.js +113 -0
- package/src/tools/parallel_search.js +77 -0
- package/src/tools/phone_control.js +80 -0
- package/src/tools/phone_control_enhanced.js +184 -0
- package/src/tools/searxng.js +61 -0
- package/src/tools/speech_to_text.js +135 -0
- package/src/tools/text_to_speech.js +105 -0
- package/src/tools/thread_ownership.js +88 -0
- package/src/tools/video_generation.js +72 -0
- package/src/tools/web_readability.js +104 -0
- 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/memory.js +200 -0
- 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
|
@@ -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 };
|