botforje 0.0.1 → 0.0.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/botforje.svg)](https://www.npmjs.com/package/botforje)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js Version](https://img.shields.io/badge/node-%3E%3D16-brightgreen.svg)](https://nodejs.org)
5
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22.13-brightgreen.svg)](https://nodejs.org)
6
6
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/felixzsh/botforje/pulls)
7
7
 
8
8
  **Create multiple WhatsApp bots without writing code** - Just configure in YAML!
package/dist/cli.js CHANGED
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
- const child_process_1 = require("child_process");
39
38
  const path = __importStar(require("path"));
40
39
  const fs = __importStar(require("fs"));
41
40
  const fleet_1 = require("./fleet");
@@ -59,13 +58,6 @@ program
59
58
  .description('CLI tool for creating and managing WhatsApp bots')
60
59
  .version(packageJson.version)
61
60
  .option('-c, --config <path>', `Path to config file (default: ${(0, yaml_1.getDefaultConfigPath)()})`);
62
- program
63
- .command('setup')
64
- .description('Setup/repair systemd service')
65
- .action(() => {
66
- const setupScript = path.join(__dirname, '..', 'scripts', 'setup-systemd.js');
67
- (0, child_process_1.execSync)(`node ${setupScript}`, { stdio: 'inherit' });
68
- });
69
61
  program
70
62
  .command('guide')
71
63
  .description('Show AI agent configuration guide')
@@ -555,7 +555,6 @@ curl http://localhost:3000/api/config/status
555
555
  botforje daemon # Start the bot daemon
556
556
  botforje status # Show bot session status
557
557
  botforje auth <botId> # Authenticate a bot
558
- botforje setup # Setup systemd service
559
558
  botforje guide # Show this guide
560
559
  \`\`\`
561
560
  `;
@@ -467,7 +467,7 @@ function validateIdWithContext(id, kind, ctx) {
467
467
  }
468
468
  }
469
469
  async function validateConfig(configPath) {
470
- const targetPath = configPath || (0, yaml_1.getDefaultConfigPath)();
470
+ const targetPath = configPath || (0, yaml_1.getConfigPath)();
471
471
  const configDir = path.dirname(targetPath);
472
472
  const errors = [];
473
473
  const actionsDir = path.join(configDir, 'actions');
@@ -48,7 +48,13 @@ const logger_1 = require("../helpers/logger");
48
48
  let configPath;
49
49
  function getDefaultConfigPath() {
50
50
  const home = process.env.HOME || os.homedir();
51
- return path.join(home, '.config', 'botforje', 'config.yml');
51
+ const userConfig = path.join(home, '.config', 'botforje', 'config.yml');
52
+ const systemConfig = '/etc/botforje/config.yml';
53
+ if (fsSync.existsSync(userConfig))
54
+ return userConfig;
55
+ if (fsSync.existsSync(systemConfig))
56
+ return systemConfig;
57
+ return userConfig;
52
58
  }
53
59
  function getConfigPath() {
54
60
  return configPath || getDefaultConfigPath();
@@ -9,9 +9,6 @@ exports.getLogger = getLogger;
9
9
  exports.setGlobalLogger = setGlobalLogger;
10
10
  const pino_1 = __importDefault(require("pino"));
11
11
  function getCallerInfo() {
12
- const isDevelopment = process.env.NODE_ENV !== 'production';
13
- if (!isDevelopment)
14
- return undefined;
15
12
  const originalPrepareStackTrace = Error.prepareStackTrace;
16
13
  try {
17
14
  const err = new Error();
@@ -38,10 +35,10 @@ function getCallerInfo() {
38
35
  return undefined;
39
36
  }
40
37
  function createLogger(logLevel = 'info') {
41
- const isDevelopment = process.env.NODE_ENV !== 'production';
38
+ const usePretty = process.stdout.isTTY;
42
39
  const logger = (0, pino_1.default)({
43
40
  level: logLevel,
44
- transport: isDevelopment
41
+ transport: usePretty
45
42
  ? {
46
43
  target: 'pino-pretty',
47
44
  options: {
@@ -97,9 +97,13 @@ class WhatsAppChannel {
97
97
  this.stateChangeHandlers = [];
98
98
  this.authRequiredHandlers = [];
99
99
  this.isConnected = false;
100
+ this.dedupCache = new Set();
100
101
  this.channelId = clientId;
101
102
  this.client = new whatsapp_web_js_1.Client(getClientOptions(clientId));
102
103
  this.setupEventListeners();
104
+ this.dedupInterval = setInterval(() => {
105
+ this.dedupCache.clear();
106
+ }, 30000);
103
107
  }
104
108
  get logger() {
105
109
  return (0, logger_1.getLogger)();
@@ -113,6 +117,12 @@ class WhatsAppChannel {
113
117
  this.readyHandlers.forEach(handler => handler());
114
118
  });
115
119
  this.client.on('message', async (msg) => {
120
+ const rawId = msg.id._serialized;
121
+ if (this.dedupCache.has(rawId)) {
122
+ this.logger.debug(`Skipping duplicate message ${rawId}`);
123
+ return;
124
+ }
125
+ this.dedupCache.add(rawId);
116
126
  const domainMessage = (0, whatsapp_1.toDomainMessage)(msg);
117
127
  if (msg.from.includes('@lid')) {
118
128
  try {
@@ -184,6 +194,9 @@ class WhatsAppChannel {
184
194
  await this.client.initialize();
185
195
  }
186
196
  async disconnect() {
197
+ if (this.dedupInterval) {
198
+ clearInterval(this.dedupInterval);
199
+ }
187
200
  if (this.client) {
188
201
  await this.client.destroy();
189
202
  this.isConnected = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botforje",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Create powerful WhatsApp bots with YAML configuration",
5
5
  "jest": {
6
6
  "preset": "ts-jest",
@@ -58,6 +58,7 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@types/express": "^5.0.3",
61
+ "botforje": "link:",
61
62
  "commander": "^12.1.0",
62
63
  "express": "^5.1.0",
63
64
  "fuse.js": "^7.4.2",
@@ -65,8 +66,7 @@
65
66
  "pino": "^10.0.0",
66
67
  "pino-pretty": "^13.1.2",
67
68
  "qrcode-terminal": "^0.12.0",
68
- "whatsapp-web.js": "^1.34.7",
69
- "botforje": "link:"
69
+ "whatsapp-web.js": "^1.34.7"
70
70
  },
71
71
  "engines": {
72
72
  "node": ">=22.13.0"
@@ -1,45 +1,142 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { execSync } = require('child_process');
3
+ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
 
7
- console.log('\nSetting up Botforje...\n');
7
+ const BANNER = `
8
+ ██████ ██████ ████████ ███████ ██████ ██████ ██ ███████
9
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██
10
+ ██████ ██ ██ ██ █████ ██ ██ ██████ ██ █████
11
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
12
+ ██████ ██████ ██ ██ ██████ ██ ██ ██ ███████
13
+ `;
14
+
15
+ console.log(BANNER);
16
+ console.log(` Botforje v${require('../package.json').version} installed!\n`);
17
+
18
+ const scriptDir = __dirname;
19
+ const templatePath = path.join(scriptDir, '..', 'service', 'botforje.service.template');
20
+ const exampleConfigPath = path.join(scriptDir, '..', 'config.example.yml');
21
+
22
+ function renderServiceFile(template, vars) {
23
+ return Object.entries(vars).reduce(
24
+ (content, [key, value]) => content.replace(new RegExp(`{{${key}}}`, 'g'), value),
25
+ template
26
+ );
27
+ }
28
+
29
+ async function setupSystemd() {
30
+ if (os.platform() !== 'linux') return 'no-systemd';
8
31
 
9
- function hasSystemd() {
10
- if (os.platform() !== 'linux') return false;
11
32
  try {
12
- execSync('systemctl --version', { stdio: 'ignore' });
13
- return true;
33
+ require('child_process').execSync('systemctl --version', { stdio: 'ignore' });
14
34
  } catch {
15
- return false;
35
+ return 'no-systemd';
36
+ }
37
+
38
+ const isRoot = process.getuid && process.getuid() === 0;
39
+ const homeDir = os.homedir();
40
+ const nodePath = process.execPath;
41
+
42
+ const nodeModulesDir = path.dirname(path.dirname(scriptDir));
43
+ const pkgDir = path.resolve(scriptDir, '..');
44
+ const scriptPath = path.join(pkgDir, 'dist', 'cli.js');
45
+
46
+ if (!fs.existsSync(scriptPath)) {
47
+ console.log(' (dist/cli.js not found — run "pnpm build" first for dev mode)\n');
48
+ return 'dev-mode';
49
+ }
50
+
51
+ let serviceDir, configDir, scopeLabel, wantedBy;
52
+
53
+ if (isRoot) {
54
+ serviceDir = '/etc/systemd/system';
55
+ configDir = '/etc/botforje';
56
+ scopeLabel = 'system';
57
+ wantedBy = 'multi-user.target';
58
+ } else {
59
+ serviceDir = path.join(homeDir, '.config', 'systemd', 'user');
60
+ configDir = path.join(homeDir, '.config', 'botforje');
61
+ scopeLabel = 'user';
62
+ wantedBy = 'default.target';
63
+ }
64
+
65
+ // Create config directory
66
+ if (!fs.existsSync(configDir)) {
67
+ fs.mkdirSync(configDir, { recursive: true });
68
+ }
69
+
70
+ // Copy example config (commented) if config.yml doesn't exist
71
+ const targetConfigPath = path.join(configDir, 'config.yml');
72
+ if (fs.existsSync(exampleConfigPath) && !fs.existsSync(targetConfigPath)) {
73
+ const exampleContent = fs.readFileSync(exampleConfigPath, 'utf8');
74
+ const commentedContent = exampleContent
75
+ .split('\n')
76
+ .map(line => line.trim() ? `# ${line}` : line)
77
+ .join('\n');
78
+ fs.writeFileSync(targetConfigPath, commentedContent);
16
79
  }
80
+
81
+ // Create service file
82
+ if (!fs.existsSync(serviceDir)) {
83
+ fs.mkdirSync(serviceDir, { recursive: true });
84
+ }
85
+
86
+ const template = fs.readFileSync(templatePath, 'utf8');
87
+ const serviceContent = renderServiceFile(template, {
88
+ NODE: nodePath,
89
+ SCRIPT: scriptPath,
90
+ WANTED_BY: wantedBy,
91
+ });
92
+
93
+ const servicePath = path.join(serviceDir, 'botforje.service');
94
+ fs.writeFileSync(servicePath, serviceContent);
95
+
96
+ return { scope: scopeLabel, configDir, servicePath, isRoot, homeDir };
17
97
  }
18
98
 
19
- if (hasSystemd()) {
20
- try {
21
- console.log('Detected Linux system with systemd');
22
- console.log('Configuring systemd service automatically...\n');
23
-
24
- const setupScript = path.join(__dirname, 'setup-systemd.js');
25
- execSync(`node "${setupScript}"`, { stdio: 'inherit' });
26
-
27
- console.log('Installation complete!\n');
28
- console.log('Quick Start:\n');
29
- console.log(' 1. Start service: systemctl --user start botforje');
30
- console.log(' 2. Enable on boot: systemctl --user enable botforje');
31
- console.log(' 3. Check status: botforje status');
32
- console.log(' 4. Authenticate: botforje auth <botId>');
33
- console.log(' 5. View logs: journalctl --user -u botforje -f\n');
34
-
35
- } catch (error) {
36
- console.error('Automatic setup failed:', error.message);
37
- console.log('\nYou can set it up manually: botforje setup\n');
38
- process.exit(0);
39
- }
40
- } else {
41
- console.log('Systemd not detected (macOS/Windows/Container)');
42
- console.log('\nTo use Botforje:\n');
43
- console.log('\n 1. Start daemon: botforje daemon');
44
- console.log(' 2. Authenticate: botforje auth <botId>\n');
45
- }
99
+ async function main() {
100
+ const result = await setupSystemd();
101
+
102
+ if (result === 'no-systemd') {
103
+ console.log(' (systemd not detected — run manually)\n');
104
+ console.log(' ── Next steps ──\n');
105
+ console.log(' Start daemon: botforje daemon');
106
+ console.log(' Authenticate: botforje auth <botId>\n');
107
+ return;
108
+ }
109
+
110
+ if (result === 'dev-mode') {
111
+ console.log(' (dev mode systemd service skipped)\n');
112
+ console.log(' ── Next steps ──\n');
113
+ console.log(' Build: pnpm build');
114
+ console.log(' Test daemon: pnpm start daemon\n');
115
+ return;
116
+ }
117
+
118
+ const { scope, configDir, servicePath, isRoot } = result;
119
+ const scopeFlag = scope === 'user' ? '--user ' : '';
120
+
121
+ console.log(` Service file: ${servicePath}`);
122
+ console.log(` Config dir: ${configDir}\n`);
123
+
124
+ if (scope === 'system') {
125
+ console.log(` ── Next steps ──\n`);
126
+ console.log(` Edit config: ${configDir}/config.yml`);
127
+ console.log(` Start service: systemctl enable --now botforje`);
128
+ console.log(` View logs: journalctl -u botforje -f`);
129
+ console.log(` Authenticate: sudo botforje auth <botId>\n`);
130
+ } else {
131
+ console.log(` ── Next steps ──\n`);
132
+ console.log(` Edit config: ${configDir}/config.yml`);
133
+ console.log(` Start service: systemctl --user enable --now botforje`);
134
+ console.log(` View logs: journalctl --user -u botforje -f`);
135
+ console.log(` Authenticate: botforje auth <botId>\n`);
136
+ }
137
+ }
138
+
139
+ main().catch(err => {
140
+ console.error('postinstall error:', err.message);
141
+ process.exit(0);
142
+ });
@@ -8,41 +8,44 @@ const os = require('os');
8
8
  function uninstallService() {
9
9
  if (os.platform() !== 'linux') return;
10
10
 
11
+ const isRoot = process.getuid && process.getuid() === 0;
12
+ const homeDir = os.homedir();
13
+
14
+ const userServicePath = path.join(homeDir, '.config', 'systemd', 'user', 'botforje.service');
15
+ const userConfigDir = path.join(homeDir, '.config', 'botforje');
16
+ const systemServicePath = '/etc/systemd/system/botforje.service';
17
+ const systemConfigDir = '/etc/botforje';
18
+
19
+ const isUserService = fs.existsSync(userServicePath);
20
+ const isSystemService = fs.existsSync(systemServicePath);
21
+
22
+ if (!isUserService && !isSystemService) return;
23
+
24
+ const scopeFlag = isUserService ? '--user ' : '';
25
+ const scope = isUserService ? 'user' : 'system';
26
+ const servicePath = isUserService ? userServicePath : systemServicePath;
27
+ const configDir = isUserService ? userConfigDir : systemConfigDir;
28
+
29
+ console.log('\nUninstalling Botforje service...\n');
30
+
11
31
  try {
12
- const homeDir = os.homedir();
13
- const servicePath = path.join(homeDir, '.config', 'systemd', 'user', 'botforje.service');
14
- const configDir = path.join(homeDir, '.config', 'botforje');
15
-
16
- if (!fs.existsSync(servicePath)) return;
17
-
18
- console.log('\nUninstalling Botforje service...\n');
19
-
20
- // Stop service first to prevent hanging
21
- console.log('Stopping service...');
22
- try {
23
- execSync('systemctl --user stop botforje 2>/dev/null', { stdio: 'inherit', timeout: 10000 });
24
- } catch (error) {
25
- console.log('Service may not be running or failed to stop gracefully');
26
- }
27
-
28
- // Disable service
29
- try {
30
- execSync('systemctl --user disable botforje 2>/dev/null', { stdio: 'ignore' });
31
- } catch {}
32
-
33
- // Eliminar archivo .service
34
- fs.unlinkSync(servicePath);
35
- console.log('Service removed');
36
-
37
- // Recargar systemd
38
- execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
39
-
40
- console.log(`\nConfiguration kept at: ${configDir}`);
41
- console.log(` To remove: rm -rf ${configDir}\n`);
42
-
43
- } catch (error) {
44
- console.error('Cleanup warning:', error.message);
32
+ console.log(' Stopping service...');
33
+ execSync(`systemctl ${scopeFlag}stop botforje 2>/dev/null`, { stdio: 'inherit', timeout: 10000 });
34
+ } catch {
35
+ console.log(' (service was not running)');
45
36
  }
37
+
38
+ try {
39
+ execSync(`systemctl ${scopeFlag}disable botforje 2>/dev/null`, { stdio: 'ignore' });
40
+ } catch {}
41
+
42
+ fs.unlinkSync(servicePath);
43
+ console.log(' Service file removed');
44
+
45
+ execSync(`systemctl ${scopeFlag}daemon-reload`, { stdio: 'ignore' });
46
+
47
+ console.log(`\n Config kept at: ${configDir}`);
48
+ console.log(` To remove: rm -rf ${configDir}\n`);
46
49
  }
47
50
 
48
- uninstallService();
51
+ uninstallService();
@@ -5,21 +5,14 @@ Documentation=https://github.com/felixzsh/botforje
5
5
 
6
6
  [Service]
7
7
  Type=simple
8
- WorkingDirectory={{INSTALL_DIR}}
9
- ExecStart=xvfb-run -a {{NODE_PATH}} {{INSTALL_DIR}}/dist/cli.js daemon
8
+ ExecStart={{NODE}} {{SCRIPT}} daemon
10
9
  Restart=on-failure
11
10
  RestartSec=10
12
11
  StandardOutput=journal
13
12
  StandardError=journal
14
13
  SyslogIdentifier=botforje
15
-
16
- # Variables de entorno
17
- Environment="NODE_ENV=production"
18
- Environment="CONFIG_DIR={{CONFIG_DIR}}"
19
-
20
- # Límites de seguridad
21
14
  LimitNOFILE=65536
22
15
  PrivateTmp=true
23
16
 
24
17
  [Install]
25
- WantedBy=multi-user.target
18
+ WantedBy={{WANTED_BY}}
@@ -1,91 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { execSync } = require('child_process');
6
- const os = require('os');
7
-
8
- function setupSystemd() {
9
- try {
10
- if (os.platform() !== 'linux') {
11
- console.log('Systemd setup is only available on Linux');
12
- return;
13
- }
14
-
15
- const user = os.userInfo().username;
16
- const homeDir = os.homedir();
17
- const configDir = path.join(homeDir, '.config', 'botforje');
18
- const systemdUserDir = path.join(homeDir, '.config', 'systemd', 'user');
19
-
20
- const globalNpmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
21
- const scriptDir = path.dirname(__filename);
22
- const projectRoot = path.resolve(scriptDir, '..');
23
-
24
- // Detect mode: If project root is within global npm root, it's production
25
- const isProduction = projectRoot.startsWith(globalNpmRoot);
26
- const installDir = isProduction
27
- ? path.join(globalNpmRoot, 'botforje')
28
- : projectRoot;
29
-
30
- // For Node path, use 'which node' in both cases, but log a warning for dev
31
- const nodePath = execSync('which node', { encoding: 'utf8' }).trim();
32
- if (!isProduction) {
33
- console.log('Development mode detected: Using local paths for testing');
34
- console.log(` Install dir: ${installDir}`);
35
- console.log(` Node path: ${nodePath}`);
36
- console.log(' Note: Ensure dist/cli.js exists (run build first)');
37
- }
38
-
39
- console.log('Setting up Botforje systemd service...');
40
-
41
- if (!fs.existsSync(configDir)) {
42
- fs.mkdirSync(configDir, { recursive: true });
43
- console.log(`Created config directory: ${configDir}`);
44
- }
45
-
46
- // Create example config file only if config.yml doesn't exist
47
- const exampleConfigPath = path.join(__dirname, '..', 'config.example.yml');
48
- const targetConfigPath = path.join(configDir, 'config.yml');
49
-
50
- if (fs.existsSync(exampleConfigPath) && !fs.existsSync(targetConfigPath)) {
51
- const exampleContent = fs.readFileSync(exampleConfigPath, 'utf8');
52
- // Comment out all lines for the default config
53
- const commentedContent = exampleContent
54
- .split('\n')
55
- .map(line => line.trim() ? `# ${line}` : line)
56
- .join('\n');
57
- fs.writeFileSync(targetConfigPath, commentedContent);
58
- console.log(`Created example config file: ${targetConfigPath}`);
59
- console.log(` Uncomment and edit this file to configure your bots`);
60
- } else if (fs.existsSync(targetConfigPath)) {
61
- console.log(`Config file already exists: ${targetConfigPath}`);
62
- }
63
-
64
- if (!fs.existsSync(systemdUserDir)) {
65
- fs.mkdirSync(systemdUserDir, { recursive: true });
66
- console.log(`Created systemd user directory: ${systemdUserDir}`);
67
- }
68
-
69
- const templatePath = path.join(__dirname, '..', 'service', 'botforje.service.template');
70
- let serviceContent = fs.readFileSync(templatePath, 'utf8');
71
-
72
- serviceContent = serviceContent
73
- .replace(/{{USER}}/g, user)
74
- .replace(/{{INSTALL_DIR}}/g, installDir)
75
- .replace(/{{NODE_PATH}}/g, nodePath)
76
- .replace(/{{CONFIG_DIR}}/g, configDir);
77
-
78
- const servicePath = path.join(systemdUserDir, 'botforje.service');
79
- fs.writeFileSync(servicePath, serviceContent);
80
- console.log(`Created service file: ${servicePath}`);
81
-
82
- execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
83
- console.log('Reloaded systemd daemon\n');
84
-
85
- } catch (error) {
86
- console.error('Error setting up systemd service:', error.message);
87
- throw error;
88
- }
89
- }
90
-
91
- setupSystemd();