bloby-bot 0.66.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,58 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import url from 'node:url';
5
-
6
- import type { BotConfig } from '../../shared/config.js';
7
-
8
- export const PLATFORM = os.platform();
9
-
10
- const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
11
- export const REPO_ROOT = path.resolve(__dirname, '../..');
12
-
13
- export const DATA_DIR = path.join(
14
- process.env.BLOBY_REAL_HOME || os.homedir(),
15
- '.bloby',
16
- );
17
-
18
- export const IS_DEV = fs.existsSync(path.join(REPO_ROOT, '.git'));
19
- export const ROOT = IS_DEV ? REPO_ROOT : DATA_DIR;
20
- export const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
21
- export const BIN_DIR = path.join(DATA_DIR, 'bin');
22
- export const CF_PATH = path.join(
23
- BIN_DIR,
24
- 'cloudflared' + (PLATFORM === 'win32' ? '.exe' : ''),
25
- );
26
-
27
- export const SERVICE_NAME = 'bloby';
28
-
29
- export const pkg = JSON.parse(
30
- fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'),
31
- );
32
-
33
- export function loadConfig(): BotConfig {
34
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
35
- }
36
-
37
- export function createConfig() {
38
- fs.mkdirSync(DATA_DIR, { recursive: true });
39
- if (fs.existsSync(CONFIG_PATH)) return;
40
-
41
- const config: Partial<BotConfig> = {
42
- port: 7400,
43
- username: '',
44
- ai: { provider: '', model: '', apiKey: '' },
45
- tunnel: { mode: 'quick' },
46
- relay: { token: '', tier: '', url: '' },
47
- };
48
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
49
- }
50
-
51
- export function safeLoadConfig(fallback: Partial<BotConfig> = {}): BotConfig {
52
- if (!fs.existsSync(CONFIG_PATH)) return fallback as BotConfig;
53
- try {
54
- return loadConfig();
55
- } catch {
56
- return fallback as BotConfig;
57
- }
58
- }
@@ -1,31 +0,0 @@
1
- import os from 'node:os';
2
-
3
- export function getPlatform(): string {
4
- return os.platform();
5
- }
6
-
7
- export function isWindows(): boolean {
8
- return getPlatform() === 'win32';
9
- }
10
-
11
- export function isUnixLike(): boolean {
12
- return getPlatform() !== 'win32';
13
- }
14
-
15
- export function getRawArchitecture(): string {
16
- return (process.env.PROCESSOR_ARCHITECTURE || os.arch()).toLowerCase();
17
- }
18
-
19
- /**
20
- * Normalizes the architecture specifically for cloudflared download links.
21
- */
22
- export function getNormalizedArchitecture(): string {
23
- const rawArch = getRawArchitecture();
24
-
25
- if (['arm64', 'aarch64'].includes(rawArch)) return 'arm64';
26
- if (['arm', 'armv7l'].includes(rawArch)) return 'arm';
27
- if (rawArch === 'x64' || rawArch === 'amd64' || rawArch === 'x86_64') return 'amd64';
28
- if (rawArch === '386' || rawArch === 'ia32' || rawArch === 'x86') return '386';
29
-
30
- return 'amd64';
31
- }
@@ -1,87 +0,0 @@
1
- import { spawn, type ChildProcess } from 'node:child_process';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
- import { ROOT, CONFIG_PATH } from './config.js';
5
- import type { BotConfig } from '../../shared/config.js';
6
-
7
- export interface BootResult {
8
- child: ChildProcess;
9
- tunnelUrl: string;
10
- relayUrl: string | null;
11
- tunnelFailed: boolean;
12
- viteWarm: Promise<void>;
13
- }
14
-
15
- export function bootServer({
16
- onTunnelUp,
17
- onReady,
18
- }: {
19
- onTunnelUp?: (url?: string) => void;
20
- onReady?: () => void;
21
- } = {}): Promise<BootResult> {
22
- return new Promise(resolve => {
23
- const child = spawn(
24
- process.execPath,
25
- ['--import', 'tsx/esm', path.join(ROOT, 'supervisor/index.ts')],
26
- { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } },
27
- );
28
-
29
- let tunnelUrl: string | undefined;
30
- let relayUrl: string | undefined;
31
- let resolved = false;
32
- let tunnelFired = false;
33
- let tunnelFailed = false;
34
-
35
- let viteWarmResolve: (value: void | PromiseLike<void>) => void;
36
- const viteWarm = new Promise<void>(r => { viteWarmResolve = r as any; });
37
-
38
- const doResolve = () => {
39
- if (resolved) return;
40
- resolved = true;
41
-
42
- if (!tunnelFired && onTunnelUp) onTunnelUp();
43
- if (onReady) onReady();
44
-
45
- let config: Partial<BotConfig> = {};
46
- if (fs.existsSync(CONFIG_PATH)) {
47
- config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
48
- }
49
-
50
- resolve({
51
- child,
52
- tunnelUrl: tunnelUrl || `http://localhost:${config.port || 7400}`,
53
- relayUrl: relayUrl || config.relay?.url || null,
54
- tunnelFailed,
55
- viteWarm,
56
- });
57
- };
58
-
59
- const handleData = (data: any) => {
60
- const text = data.toString();
61
-
62
- const tunnelMatch = text.match(/__TUNNEL_URL__=(\S+)/);
63
- if (tunnelMatch) {
64
- tunnelUrl = tunnelMatch[1];
65
- if (!tunnelFired && onTunnelUp) {
66
- tunnelFired = true;
67
- onTunnelUp(tunnelUrl);
68
- }
69
- }
70
-
71
- const relayMatch = text.match(/__RELAY_URL__=(\S+)/);
72
- if (relayMatch) relayUrl = relayMatch[1];
73
-
74
- if (text.includes('__VITE_WARM__')) viteWarmResolve();
75
- if (text.includes('__READY__')) { doResolve(); return; }
76
- if (text.includes('__TUNNEL_FAILED__')) { tunnelFailed = true; doResolve(); }
77
- };
78
-
79
- setTimeout(doResolve, 45_000);
80
-
81
- child.stdout?.on('data', handleData);
82
- child.stderr?.on('data', handleData);
83
-
84
- process.on('SIGINT', () => child.kill('SIGINT'));
85
- process.on('SIGTERM', () => child.kill('SIGTERM'));
86
- });
87
- }
package/cli/core/types.ts DELETED
@@ -1,15 +0,0 @@
1
- export type DaemonAction =
2
- | 'install'
3
- | 'stop'
4
- | 'start'
5
- | 'restart'
6
- | 'status'
7
- | 'logs'
8
- | 'uninstall';
9
-
10
- export interface DaemonConfig {
11
- user: string;
12
- home: string;
13
- nodePath: string;
14
- dataDir: string;
15
- }
package/cli/index.ts DELETED
@@ -1,72 +0,0 @@
1
- import { Command } from 'commander';
2
- import os from 'node:os';
3
- import fs from 'node:fs';
4
-
5
- import { getAdapter } from './platforms/index.js';
6
- import { DATA_DIR, CONFIG_PATH, pkg } from './core/config.js';
7
- import { registerDaemonCommand } from './commands/daemon.js';
8
- import { registerStartCommand } from './commands/start.js';
9
- import { registerTunnelCommand } from './commands/tunnel.js';
10
- import { registerUpdateCommand } from './commands/update.js';
11
- import { registerInitCommand } from './commands/init.js';
12
-
13
- const program = new Command();
14
-
15
- program
16
- .name('bloby')
17
- .description('Bloby Local AI and Tunneling Supervisor')
18
- .version(pkg.version);
19
-
20
- registerStartCommand(program);
21
- registerDaemonCommand(program);
22
- registerTunnelCommand(program);
23
- registerUpdateCommand(program);
24
- registerInitCommand(program);
25
-
26
- // Aliases for convenience matching the old CLI
27
- program
28
- .command('stop')
29
- .description('Stop the Bloby daemon')
30
- .action(() => {
31
- getAdapter().handleDaemonAction('stop', {
32
- user: process.env.SUDO_USER || os.userInfo().username,
33
- home: process.env.BLOBY_REAL_HOME || os.homedir(),
34
- nodePath: process.env.BLOBY_NODE_PATH || process.execPath,
35
- dataDir: DATA_DIR,
36
- });
37
- });
38
-
39
- program
40
- .command('status')
41
- .description('Check the Bloby daemon status')
42
- .action(() => {
43
- getAdapter().handleDaemonAction('status', {
44
- user: process.env.SUDO_USER || os.userInfo().username,
45
- home: process.env.BLOBY_REAL_HOME || os.homedir(),
46
- nodePath: process.env.BLOBY_NODE_PATH || process.execPath,
47
- dataDir: DATA_DIR,
48
- });
49
- });
50
-
51
- program
52
- .command('logs')
53
- .description('View the Bloby daemon logs')
54
- .action(() => {
55
- getAdapter().handleDaemonAction('logs', {
56
- user: process.env.SUDO_USER || os.userInfo().username,
57
- home: process.env.BLOBY_REAL_HOME || os.homedir(),
58
- nodePath: process.env.BLOBY_NODE_PATH || process.execPath,
59
- dataDir: DATA_DIR,
60
- });
61
- });
62
-
63
- // Default: if no command given, start or init
64
- if (process.argv.length === 2) {
65
- if (fs.existsSync(CONFIG_PATH)) {
66
- process.argv.push('start');
67
- } else {
68
- process.argv.push('init');
69
- }
70
- }
71
-
72
- program.parse(process.argv);
@@ -1,110 +0,0 @@
1
- import { execSync, spawnSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- import { BaseAdapter } from '../core/base-adapter.js';
7
- import type { DaemonConfig } from '../core/types.js';
8
-
9
- const LAUNCHD_LABEL = 'com.bloby.bot';
10
- const LAUNCHD_PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
11
- const LAUNCHD_LOG_DIR = path.join(os.homedir(), 'Library', 'Logs', 'bloby');
12
-
13
- function generateLaunchdPlist({ nodePath, dataDir }: DaemonConfig) {
14
- const nodeBinDir = path.dirname(nodePath);
15
- fs.mkdirSync(LAUNCHD_LOG_DIR, { recursive: true });
16
- return `<?xml version="1.0" encoding="UTF-8"?>
17
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
18
- <plist version="1.0">
19
- <dict>
20
- <key>Label</key>
21
- <string>${LAUNCHD_LABEL}</string>
22
- <key>ProgramArguments</key>
23
- <array>
24
- <string>${nodePath}</string>
25
- <string>--import</string>
26
- <string>tsx/esm</string>
27
- <string>${dataDir}/supervisor/index.ts</string>
28
- </array>
29
- <key>WorkingDirectory</key>
30
- <string>${dataDir}</string>
31
- <key>EnvironmentVariables</key>
32
- <dict>
33
- <key>HOME</key>
34
- <string>${os.homedir()}</string>
35
- <key>NODE_ENV</key>
36
- <string>development</string>
37
- <key>NODE_PATH</key>
38
- <string>${dataDir}/node_modules</string>
39
- <key>PATH</key>
40
- <string>${nodeBinDir}:${dataDir}/node_modules/.bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
41
- </dict>
42
- <key>RunAtLoad</key>
43
- <true/>
44
- <key>KeepAlive</key>
45
- <dict>
46
- <key>SuccessfulExit</key>
47
- <false/>
48
- </dict>
49
- <key>ThrottleInterval</key>
50
- <integer>5</integer>
51
- <key>StandardOutPath</key>
52
- <string>${LAUNCHD_LOG_DIR}/bloby.log</string>
53
- <key>StandardErrorPath</key>
54
- <string>${LAUNCHD_LOG_DIR}/bloby.error.log</string>
55
- </dict>
56
- </plist>`;
57
- }
58
-
59
- export class DarwinAdapter extends BaseAdapter {
60
- get hasDaemonSupport() { return true; }
61
-
62
- get isInstalled() { return fs.existsSync(LAUNCHD_PLIST_PATH); }
63
-
64
- get isActive() {
65
- try {
66
- const out = execSync(`launchctl list ${LAUNCHD_LABEL} 2>/dev/null`, { encoding: 'utf-8' });
67
- const pidLine = out.split('\n').find(l => l.includes('PID'));
68
- if (pidLine) {
69
- const pid = pidLine.split('=')[1]?.trim();
70
- return Boolean(pid && pid !== '0' && pid !== '-');
71
- }
72
- return !out.includes('"PID" = 0;');
73
- } catch {
74
- return false;
75
- }
76
- }
77
-
78
- protected installService(config: DaemonConfig): void {
79
- const plist = generateLaunchdPlist(config);
80
- fs.mkdirSync(path.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
81
- fs.writeFileSync(LAUNCHD_PLIST_PATH, plist);
82
- execSync(`launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'inherit' });
83
- }
84
-
85
- protected startService(): void {
86
- execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null; launchctl load "${LAUNCHD_PLIST_PATH}"`, { stdio: 'inherit' });
87
- }
88
-
89
- protected stopService(): void {
90
- execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}"`, { stdio: 'inherit' });
91
- }
92
-
93
- protected uninstallService(): void {
94
- try { execSync(`launchctl unload "${LAUNCHD_PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' }); } catch {}
95
- if (fs.existsSync(LAUNCHD_PLIST_PATH)) fs.unlinkSync(LAUNCHD_PLIST_PATH);
96
- }
97
-
98
- protected showLogs(): void {
99
- const logPath = `${LAUNCHD_LOG_DIR}/bloby.log`;
100
- if (fs.existsSync(logPath)) {
101
- spawnSync('tail', ['-f', logPath], { stdio: 'inherit' });
102
- } else {
103
- console.log('Log file not found yet.');
104
- }
105
- }
106
-
107
- protected checkStatus(): void {
108
- try { execSync(`launchctl list ${LAUNCHD_LABEL}`, { stdio: 'inherit' }); } catch {}
109
- }
110
- }
@@ -1,20 +0,0 @@
1
- import { getPlatform } from '../core/os-detector.js';
2
- import { BaseAdapter } from '../core/base-adapter.js';
3
-
4
- import { DarwinAdapter } from './darwin.js';
5
- import { LinuxAdapter } from './linux.js';
6
- import { WindowsAdapter } from './win32.js';
7
-
8
- export function getAdapter(): BaseAdapter {
9
- const platform = getPlatform();
10
-
11
- switch (platform) {
12
- case 'darwin':
13
- return new DarwinAdapter();
14
- case 'win32':
15
- return new WindowsAdapter();
16
- case 'linux':
17
- default:
18
- return new LinuxAdapter();
19
- }
20
- }
@@ -1,116 +0,0 @@
1
- import { execSync, spawnSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import pc from 'picocolors';
6
-
7
- import { BaseAdapter } from '../core/base-adapter.js';
8
- import { SERVICE_NAME } from '../core/config.js';
9
- import type { DaemonAction, DaemonConfig } from '../core/types.js';
10
-
11
- const SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
12
-
13
- function generateUnitFile({ user, home, nodePath, dataDir }: DaemonConfig) {
14
- const nodeBinDir = path.dirname(nodePath);
15
- return `[Unit]
16
- Description=Bloby Bot
17
- After=network-online.target
18
- Wants=network-online.target
19
-
20
- [Service]
21
- Type=simple
22
- User=${user}
23
- WorkingDirectory=${dataDir}
24
- ExecStart=${nodePath} --import tsx/esm ${dataDir}/supervisor/index.ts
25
- Restart=on-failure
26
- RestartSec=5
27
- Environment=HOME=${home}
28
- Environment=NODE_ENV=development
29
- Environment=NODE_PATH=${dataDir}/node_modules
30
- Environment=PATH=${nodeBinDir}:${dataDir}/node_modules/.bin:/usr/local/bin:/usr/bin:/bin
31
- StandardOutput=journal
32
- StandardError=journal
33
- SyslogIdentifier=bloby
34
-
35
- [Install]
36
- WantedBy=multi-user.target
37
- `;
38
- }
39
-
40
- export class LinuxAdapter extends BaseAdapter {
41
- get hasDaemonSupport(): boolean {
42
- try { execSync('systemctl --version', { stdio: 'ignore' }); return true; } catch { return false; }
43
- }
44
-
45
- get isInstalled(): boolean { return fs.existsSync(SERVICE_PATH); }
46
-
47
- get isActive(): boolean {
48
- try { execSync(`systemctl is-active ${SERVICE_NAME}`, { stdio: 'ignore' }); return true; } catch { return false; }
49
- }
50
-
51
- protected requiresPrivilegeEscalation(action: DaemonAction): boolean {
52
- if (action === 'status' || action === 'logs') return false;
53
- return process.getuid?.() !== 0;
54
- }
55
-
56
- protected escalatePrivileges(): void {
57
- const nodePath = process.env.BLOBY_NODE_PATH || process.execPath;
58
- const realHome = process.env.BLOBY_REAL_HOME || this.getRealHome();
59
- const args = process.argv.slice(1);
60
-
61
- console.log(pc.yellow('Sudo is required for this action.'));
62
- const result = spawnSync('sudo', [
63
- `BLOBY_NODE_PATH=${nodePath}`,
64
- `BLOBY_REAL_HOME=${realHome}`,
65
- nodePath, ...args,
66
- ], { stdio: 'inherit' });
67
- process.exit(result.status ?? 1);
68
- }
69
-
70
- private getRealUser() {
71
- return process.env.SUDO_USER || os.userInfo().username;
72
- }
73
-
74
- private getRealHome() {
75
- try {
76
- return execSync(`getent passwd ${this.getRealUser()}`, { encoding: 'utf-8' }).split(':')[5].trim();
77
- } catch {
78
- return os.homedir();
79
- }
80
- }
81
-
82
- protected installService(config: DaemonConfig): void {
83
- const unit = generateUnitFile(config);
84
- fs.writeFileSync(SERVICE_PATH, unit);
85
- execSync([
86
- `systemctl daemon-reload`,
87
- `systemctl enable ${SERVICE_NAME}`,
88
- `systemctl start ${SERVICE_NAME}`,
89
- ].join('; '), { stdio: 'inherit' });
90
- }
91
-
92
- protected startService(): void {
93
- execSync(`systemctl start ${SERVICE_NAME}`, { stdio: 'inherit' });
94
- }
95
-
96
- protected stopService(): void {
97
- execSync(`systemctl stop ${SERVICE_NAME}`, { stdio: 'inherit' });
98
- }
99
-
100
- protected uninstallService(): void {
101
- execSync([
102
- `systemctl disable ${SERVICE_NAME}`,
103
- `systemctl stop ${SERVICE_NAME}`,
104
- ].join('; '), { stdio: 'ignore' });
105
- if (fs.existsSync(SERVICE_PATH)) fs.unlinkSync(SERVICE_PATH);
106
- execSync(`systemctl daemon-reload`, { stdio: 'ignore' });
107
- }
108
-
109
- protected showLogs(): void {
110
- spawnSync('journalctl', ['-u', SERVICE_NAME, '-f', '-n', '50'], { stdio: 'inherit' });
111
- }
112
-
113
- protected checkStatus(): void {
114
- try { execSync(`systemctl status ${SERVICE_NAME}`, { stdio: 'inherit' }); } catch {}
115
- }
116
- }
@@ -1,20 +0,0 @@
1
- import pc from 'picocolors';
2
-
3
- import { BaseAdapter } from '../core/base-adapter.js';
4
- import type { DaemonConfig } from '../core/types.js';
5
-
6
- export class WindowsAdapter extends BaseAdapter {
7
- get hasDaemonSupport() { return false; }
8
- get isInstalled() { return false; }
9
- get isActive() { return false; }
10
-
11
- protected installService(_config: DaemonConfig): void {
12
- console.log(pc.yellow('Daemon mode is not supported on Windows natively yet. Use Task Scheduler.'));
13
- }
14
-
15
- protected startService(): void {}
16
- protected stopService(): void {}
17
- protected uninstallService(): void {}
18
- protected showLogs(): void {}
19
- protected checkStatus(): void {}
20
- }
package/cli/utils/ui.ts DELETED
@@ -1,38 +0,0 @@
1
- import pc from 'picocolors';
2
- import { pkg } from '../core/config.js';
3
-
4
- // 24-bit truecolor wrapper (picocolors only ships the 16-color set). Respects
5
- // picocolors' own support detection so NO_COLOR / piped output stays clean.
6
- const tc = (r: number, g: number, b: number) => (s: string) =>
7
- pc.isColorSupported ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m` : s;
8
-
9
- // Morphy logo gradient: #00ADFE (top) -> #0158FB (bottom)
10
- const grad = [
11
- tc(0, 173, 254),
12
- tc(0, 159, 254),
13
- tc(0, 145, 253),
14
- tc(1, 131, 253),
15
- tc(1, 116, 252),
16
- tc(1, 102, 251),
17
- tc(1, 88, 251),
18
- ];
19
-
20
- const logo = [
21
- ' █▄ ',
22
- ' ▄ ▄ ██ ',
23
- ' ███▄███▄ ▄███▄ ████▄████▄ ████▄ ██ ██',
24
- ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▄██',
25
- ' ▄██ ██ ▀█▄▀███▀▄█▀ ▄████▀▄██ ██▄▄▀██▀',
26
- ' ██ ██ ',
27
- ' ▀ ▀▀▀ ',
28
- ];
29
-
30
- export function banner() {
31
- console.log('');
32
- logo.forEach((row, i) => console.log(grad[i](pc.bold(row))));
33
- console.log(pc.dim(`v${pkg.version || '1.0.0'} · Self-hosted AI agent`));
34
- }
35
-
36
- export function commandExample(name: string, cmd: string) {
37
- return ` ${pc.dim(name)} ${tc(0, 173, 254)(cmd)}`;
38
- }