@c4t4/heyamigo 0.1.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.
- package/.gitignore +35 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/config/access.example.json +88 -0
- package/config/config.example.json +72 -0
- package/config/import-instructions.HOWTO.md +58 -0
- package/config/import-instructions.md +67 -0
- package/config/memory-instructions.md +40 -0
- package/config/personalities/casual.md +24 -0
- package/config/personalities/professional.md +25 -0
- package/config/personalities/sharp.md +45 -0
- package/dist/ai/claude.js +153 -0
- package/dist/ai/sessions.js +63 -0
- package/dist/cli/import.js +17 -0
- package/dist/cli/index.js +70 -0
- package/dist/cli/service.js +105 -0
- package/dist/cli/setup.js +701 -0
- package/dist/cli/start.js +37 -0
- package/dist/cli/supervisor.js +37 -0
- package/dist/config.js +104 -0
- package/dist/gateway/bootstrap.js +56 -0
- package/dist/gateway/commands.js +58 -0
- package/dist/gateway/incoming.js +239 -0
- package/dist/gateway/outgoing.js +168 -0
- package/dist/gateway/triggers.js +75 -0
- package/dist/index.js +30 -0
- package/dist/logger.js +7 -0
- package/dist/memory/digest-flag.js +8 -0
- package/dist/memory/digest.js +211 -0
- package/dist/memory/frontmatter.js +100 -0
- package/dist/memory/importer.js +103 -0
- package/dist/memory/paths.js +26 -0
- package/dist/memory/preamble.js +98 -0
- package/dist/memory/router.js +90 -0
- package/dist/memory/scheduler.js +85 -0
- package/dist/memory/store.js +183 -0
- package/dist/promptlog.js +52 -0
- package/dist/queue/persistence.js +68 -0
- package/dist/queue/queue.js +49 -0
- package/dist/queue/types.js +1 -0
- package/dist/queue/worker.js +51 -0
- package/dist/store/media.js +108 -0
- package/dist/store/messages.js +33 -0
- package/dist/wa/auth.js +9 -0
- package/dist/wa/sender.js +79 -0
- package/dist/wa/socket.js +84 -0
- package/dist/wa/whitelist.js +213 -0
- package/package.json +63 -0
- package/scripts/start-browser.sh +158 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Personality: Professional
|
|
2
|
+
|
|
3
|
+
You answer WhatsApp messages for the account owner in business and professional contexts.
|
|
4
|
+
|
|
5
|
+
## Voice
|
|
6
|
+
|
|
7
|
+
Clear, warm, efficient. Competent without being stiff.
|
|
8
|
+
|
|
9
|
+
- Complete sentences but not wordy.
|
|
10
|
+
- Polite and direct. Get to the answer quickly.
|
|
11
|
+
- No slang. Use contractions sparingly.
|
|
12
|
+
- Match the formality of the person writing to you.
|
|
13
|
+
|
|
14
|
+
## Rules
|
|
15
|
+
|
|
16
|
+
- Lead with the answer, then reasoning if needed.
|
|
17
|
+
- Be specific with numbers, dates, and facts.
|
|
18
|
+
- If something is unclear, ask instead of guessing.
|
|
19
|
+
- Never break the frame with "As an AI assistant..." or similar.
|
|
20
|
+
- Keep messages compact. If a response gets long, break it into short paragraphs.
|
|
21
|
+
- Plain text only. No markdown headers, no bullet lists, no bold.
|
|
22
|
+
|
|
23
|
+
## Self-check
|
|
24
|
+
|
|
25
|
+
Would a colleague read this and feel respected? If it feels cold or bureaucratic, warm it up. If it feels sloppy, tighten it.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Personality: Sharp (default)
|
|
2
|
+
|
|
3
|
+
You answer WhatsApp messages for the account owner. You are not customer service and not marketing copy. You are a conversational peer who is sharp, direct, and actually useful.
|
|
4
|
+
|
|
5
|
+
## Voice
|
|
6
|
+
|
|
7
|
+
Talk like a friend at dinner, not a brochure. If you wouldn't say it out loud to someone you respect, don't type it.
|
|
8
|
+
|
|
9
|
+
- Be specific. Vague is what people say when they have nothing real to say. Numbers beat adjectives, concrete beats abstract.
|
|
10
|
+
- Use the person's own words. Mirror how they talk. Casual if they're casual. Terse if they're terse.
|
|
11
|
+
- Cut marketing speak. No "experience the difference", no "discover the power of". Kill it on sight.
|
|
12
|
+
|
|
13
|
+
## Energy
|
|
14
|
+
|
|
15
|
+
Confident, charming, magnetic. Never arrogant, weak, or desperate.
|
|
16
|
+
|
|
17
|
+
- Confident: state things without hedging. No "maybe", no "possibly", no "könnte eventuell".
|
|
18
|
+
- Charming: light enough that people nod or smile, not feel lectured.
|
|
19
|
+
- Magnetic: don't chase, don't beg, don't push.
|
|
20
|
+
|
|
21
|
+
Avoid: lecturing (arrogant), hedging (weak), over-eager pushing (desperate).
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- Name the elephant. If something obvious is awkward, say it first.
|
|
26
|
+
- Reality over aspiration. Meet people where they are, not where you'd like them to be.
|
|
27
|
+
- Specificity beats generality. If it could apply to 10 situations, it applies to none.
|
|
28
|
+
- One person, one moment. Answer this question for this person right now.
|
|
29
|
+
- Never use the em-dash character. Use a period, comma, or colon.
|
|
30
|
+
- Don't jump to conclusions. Watch for red herrings. If you're not sure, say so. Being wrong confidently is worse than being uncertain honestly.
|
|
31
|
+
- Prefer being less wrong over sounding more right. Check your reasoning before you commit to an answer.
|
|
32
|
+
|
|
33
|
+
## WhatsApp constraints
|
|
34
|
+
|
|
35
|
+
- Keep replies short, usually a few sentences. Go longer only if asked.
|
|
36
|
+
- Plain text. No markdown headers, no bold, no bullet lists. WhatsApp renders them poorly.
|
|
37
|
+
- In groups, don't dominate. Respect the conversation flow.
|
|
38
|
+
- Never break the frame with "As an AI assistant..." or similar.
|
|
39
|
+
|
|
40
|
+
## Self-check before every reply
|
|
41
|
+
|
|
42
|
+
1. Could this reply work for any random person in any random context? If yes, rewrite specifically.
|
|
43
|
+
2. Would I be embarrassed to say this to a friend? If yes, rewrite.
|
|
44
|
+
3. Am I hedging, being arrogant, or desperate? If yes, rewrite.
|
|
45
|
+
4. Any em-dash? If yes, replace with period or comma.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { logger } from '../logger.js';
|
|
6
|
+
import { logPrompt } from '../promptlog.js';
|
|
7
|
+
let cachedSystemPrompt = null;
|
|
8
|
+
function systemPrompt() {
|
|
9
|
+
if (cachedSystemPrompt !== null)
|
|
10
|
+
return cachedSystemPrompt;
|
|
11
|
+
const personality = readFileSync(resolve(process.cwd(), config.claude.personalityFile), 'utf-8');
|
|
12
|
+
let memoryInstructions = '';
|
|
13
|
+
try {
|
|
14
|
+
memoryInstructions = readFileSync(resolve(process.cwd(), config.memory.instructionsFile), 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// memory instructions optional
|
|
18
|
+
}
|
|
19
|
+
cachedSystemPrompt = memoryInstructions
|
|
20
|
+
? `${personality}\n\n---\n\n${memoryInstructions}`
|
|
21
|
+
: personality;
|
|
22
|
+
return cachedSystemPrompt;
|
|
23
|
+
}
|
|
24
|
+
export function reloadSystemPrompt() {
|
|
25
|
+
cachedSystemPrompt = null;
|
|
26
|
+
}
|
|
27
|
+
function buildArgs(params) {
|
|
28
|
+
const args = [
|
|
29
|
+
'-p',
|
|
30
|
+
'--output-format',
|
|
31
|
+
config.claude.outputFormat,
|
|
32
|
+
'--model',
|
|
33
|
+
config.claude.model,
|
|
34
|
+
'--permission-mode',
|
|
35
|
+
'acceptEdits',
|
|
36
|
+
];
|
|
37
|
+
if (params.sessionId) {
|
|
38
|
+
args.push('--resume', params.sessionId);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
args.push('--append-system-prompt', systemPrompt());
|
|
42
|
+
for (const dir of config.claude.addDirs) {
|
|
43
|
+
args.push('--add-dir', resolve(process.cwd(), dir));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Memory + media dirs for file access (only effective if tools allow Read)
|
|
47
|
+
args.push('--add-dir', resolve(process.cwd(), config.memory.dir));
|
|
48
|
+
args.push('--add-dir', resolve(process.cwd(), config.storage.mediaDir));
|
|
49
|
+
// Tool restriction per role
|
|
50
|
+
if (params.allowedTools &&
|
|
51
|
+
params.allowedTools !== 'all' &&
|
|
52
|
+
params.allowedTools.length > 0) {
|
|
53
|
+
args.push('--allowedTools', params.allowedTools.join(','));
|
|
54
|
+
}
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
export async function askClaude(params) {
|
|
58
|
+
const args = buildArgs(params);
|
|
59
|
+
const startedAt = Date.now();
|
|
60
|
+
logger.debug({ resume: !!params.sessionId, inputChars: params.input.length }, 'spawning claude');
|
|
61
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
62
|
+
const child = spawn('claude', args, {
|
|
63
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
});
|
|
66
|
+
let stdout = '';
|
|
67
|
+
let stderr = '';
|
|
68
|
+
let timedOut = false;
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
timedOut = true;
|
|
71
|
+
child.kill('SIGTERM');
|
|
72
|
+
}, config.claude.timeoutMs);
|
|
73
|
+
child.stdout.on('data', (chunk) => {
|
|
74
|
+
stdout += chunk.toString('utf-8');
|
|
75
|
+
});
|
|
76
|
+
child.stderr.on('data', (chunk) => {
|
|
77
|
+
stderr += chunk.toString('utf-8');
|
|
78
|
+
});
|
|
79
|
+
child.on('error', (err) => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
void logPrompt({
|
|
82
|
+
ts: Math.floor(startedAt / 1000),
|
|
83
|
+
caller: 'worker',
|
|
84
|
+
args,
|
|
85
|
+
input: params.input,
|
|
86
|
+
error: `spawn failed: ${err.message}`,
|
|
87
|
+
durationMs: Date.now() - startedAt,
|
|
88
|
+
});
|
|
89
|
+
rejectPromise(new Error(`claude spawn failed: ${err.message}`));
|
|
90
|
+
});
|
|
91
|
+
child.on('close', (code) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
if (timedOut) {
|
|
94
|
+
void logPrompt({
|
|
95
|
+
ts: Math.floor(startedAt / 1000),
|
|
96
|
+
caller: 'worker',
|
|
97
|
+
args,
|
|
98
|
+
input: params.input,
|
|
99
|
+
error: 'timed out',
|
|
100
|
+
durationMs: Date.now() - startedAt,
|
|
101
|
+
});
|
|
102
|
+
return rejectPromise(new Error(`claude timed out after ${config.claude.timeoutMs}ms`));
|
|
103
|
+
}
|
|
104
|
+
if (code !== 0) {
|
|
105
|
+
void logPrompt({
|
|
106
|
+
ts: Math.floor(startedAt / 1000),
|
|
107
|
+
caller: 'worker',
|
|
108
|
+
args,
|
|
109
|
+
input: params.input,
|
|
110
|
+
error: `exit ${code}: ${stderr.slice(0, 500)}`,
|
|
111
|
+
durationMs: Date.now() - startedAt,
|
|
112
|
+
});
|
|
113
|
+
return rejectPromise(new Error(`claude exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(stdout);
|
|
117
|
+
if (parsed.is_error || parsed.subtype !== 'success') {
|
|
118
|
+
return rejectPromise(new Error(`claude returned error (subtype=${parsed.subtype}): ${parsed.result ?? stderr.slice(0, 200)}`));
|
|
119
|
+
}
|
|
120
|
+
if (!parsed.result || !parsed.session_id) {
|
|
121
|
+
return rejectPromise(new Error(`claude output missing result or session_id: ${stdout.slice(0, 200)}`));
|
|
122
|
+
}
|
|
123
|
+
const result = {
|
|
124
|
+
reply: parsed.result,
|
|
125
|
+
sessionId: parsed.session_id,
|
|
126
|
+
usage: {
|
|
127
|
+
inputTokens: parsed.usage?.input_tokens ?? 0,
|
|
128
|
+
cacheReadTokens: parsed.usage?.cache_read_input_tokens ?? 0,
|
|
129
|
+
cacheCreationTokens: parsed.usage?.cache_creation_input_tokens ?? 0,
|
|
130
|
+
outputTokens: parsed.usage?.output_tokens ?? 0,
|
|
131
|
+
numTurns: parsed.num_turns ?? 0,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
void logPrompt({
|
|
135
|
+
ts: Math.floor(startedAt / 1000),
|
|
136
|
+
caller: 'worker',
|
|
137
|
+
args,
|
|
138
|
+
input: params.input,
|
|
139
|
+
output: result.reply,
|
|
140
|
+
sessionId: result.sessionId,
|
|
141
|
+
usage: result.usage,
|
|
142
|
+
durationMs: Date.now() - startedAt,
|
|
143
|
+
});
|
|
144
|
+
resolvePromise(result);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
rejectPromise(new Error(`failed to parse claude output: ${err.message}\nstdout: ${stdout.slice(0, 500)}`));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
child.stdin.write(params.input);
|
|
151
|
+
child.stdin.end();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname, resolve } from 'path';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { logger } from '../logger.js';
|
|
5
|
+
function sessionsPath() {
|
|
6
|
+
return resolve(process.cwd(), config.storage.sessionsFile);
|
|
7
|
+
}
|
|
8
|
+
let sessions = load();
|
|
9
|
+
function load() {
|
|
10
|
+
const path = sessionsPath();
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [jid, v] of Object.entries(raw)) {
|
|
17
|
+
if (typeof v === 'string') {
|
|
18
|
+
out[jid] = { sessionId: v }; // migrate old flat string format
|
|
19
|
+
}
|
|
20
|
+
else if (v && typeof v === 'object' && 'sessionId' in v) {
|
|
21
|
+
out[jid] = v;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
logger.warn({ err, path }, 'failed to load sessions.json, starting empty');
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function save() {
|
|
32
|
+
const path = sessionsPath();
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
34
|
+
writeFileSync(path, JSON.stringify(sessions, null, 2) + '\n', 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
export function getSession(jid) {
|
|
37
|
+
return sessions[jid]?.sessionId;
|
|
38
|
+
}
|
|
39
|
+
export function getSessionInfo(jid) {
|
|
40
|
+
return sessions[jid];
|
|
41
|
+
}
|
|
42
|
+
export function setSession(jid, sessionId) {
|
|
43
|
+
const existing = sessions[jid];
|
|
44
|
+
sessions[jid] = { sessionId, usage: existing?.usage };
|
|
45
|
+
save();
|
|
46
|
+
}
|
|
47
|
+
export function setUsage(jid, usage) {
|
|
48
|
+
const existing = sessions[jid];
|
|
49
|
+
if (!existing)
|
|
50
|
+
return;
|
|
51
|
+
sessions[jid] = { ...existing, usage };
|
|
52
|
+
save();
|
|
53
|
+
}
|
|
54
|
+
export function clearSession(jid) {
|
|
55
|
+
if (!(jid in sessions))
|
|
56
|
+
return false;
|
|
57
|
+
delete sessions[jid];
|
|
58
|
+
save();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
export function listSessions() {
|
|
62
|
+
return sessions;
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runImport } from '../memory/importer.js';
|
|
2
|
+
async function main() {
|
|
3
|
+
const source = process.argv[2];
|
|
4
|
+
if (!source) {
|
|
5
|
+
console.error('Usage: npm run import -- <path-to-source-folder>');
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
await runImport(source);
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
console.error('Import failed:', err.message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
void main();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
const program = new Command();
|
|
4
|
+
program
|
|
5
|
+
.name('heyamigo')
|
|
6
|
+
.description('WhatsApp AI Bot powered by Claude')
|
|
7
|
+
.version('0.1.0');
|
|
8
|
+
program
|
|
9
|
+
.command('setup')
|
|
10
|
+
.description('Run the setup wizard')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const { runSetup } = await import('./setup.js');
|
|
13
|
+
await runSetup();
|
|
14
|
+
});
|
|
15
|
+
program
|
|
16
|
+
.command('start')
|
|
17
|
+
.description('Start the bot as a background service')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
const { serviceCmd } = await import('./service.js');
|
|
20
|
+
await serviceCmd('start');
|
|
21
|
+
});
|
|
22
|
+
program
|
|
23
|
+
.command('stop')
|
|
24
|
+
.description('Stop the bot')
|
|
25
|
+
.action(async () => {
|
|
26
|
+
const { serviceCmd } = await import('./service.js');
|
|
27
|
+
await serviceCmd('stop');
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command('restart')
|
|
31
|
+
.description('Restart the bot')
|
|
32
|
+
.action(async () => {
|
|
33
|
+
const { serviceCmd } = await import('./service.js');
|
|
34
|
+
await serviceCmd('restart');
|
|
35
|
+
});
|
|
36
|
+
program
|
|
37
|
+
.command('logs')
|
|
38
|
+
.description('Tail live logs')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
const { serviceCmd } = await import('./service.js');
|
|
41
|
+
await serviceCmd('logs');
|
|
42
|
+
});
|
|
43
|
+
program
|
|
44
|
+
.command('status')
|
|
45
|
+
.description('Check if the bot is running')
|
|
46
|
+
.action(async () => {
|
|
47
|
+
const { serviceCmd } = await import('./service.js');
|
|
48
|
+
await serviceCmd('status');
|
|
49
|
+
});
|
|
50
|
+
program
|
|
51
|
+
.command('import <path>')
|
|
52
|
+
.description('Import external knowledge folder into memory')
|
|
53
|
+
.action(async (path) => {
|
|
54
|
+
const { runImport } = await import('../memory/importer.js');
|
|
55
|
+
try {
|
|
56
|
+
await runImport(path);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error('Import failed:', err.message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
program
|
|
64
|
+
.command('dev')
|
|
65
|
+
.description('Start in foreground with file watching (development)')
|
|
66
|
+
.action(async () => {
|
|
67
|
+
const { main } = await import('./start.js');
|
|
68
|
+
await main();
|
|
69
|
+
});
|
|
70
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
|
|
3
|
+
import { dirname, resolve } from 'path';
|
|
4
|
+
const cwd = process.cwd();
|
|
5
|
+
const PID_FILE = resolve(cwd, 'storage/heyamigo.pid');
|
|
6
|
+
const LOG_FILE = resolve(cwd, 'storage/logs/heyamigo.log');
|
|
7
|
+
function readPid() {
|
|
8
|
+
if (!existsSync(PID_FILE))
|
|
9
|
+
return null;
|
|
10
|
+
const raw = readFileSync(PID_FILE, 'utf-8').trim();
|
|
11
|
+
const pid = parseInt(raw, 10);
|
|
12
|
+
return isNaN(pid) ? null : pid;
|
|
13
|
+
}
|
|
14
|
+
function isAlive(pid) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function cleanPid() {
|
|
24
|
+
if (existsSync(PID_FILE))
|
|
25
|
+
unlinkSync(PID_FILE);
|
|
26
|
+
}
|
|
27
|
+
export async function serviceCmd(action) {
|
|
28
|
+
switch (action) {
|
|
29
|
+
case 'start': {
|
|
30
|
+
const existing = readPid();
|
|
31
|
+
if (existing && isAlive(existing)) {
|
|
32
|
+
console.log(`Already running (PID: ${existing})`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
cleanPid();
|
|
36
|
+
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
37
|
+
const logFd = openSync(LOG_FILE, 'a');
|
|
38
|
+
const child = spawn(process.execPath, [
|
|
39
|
+
'--import',
|
|
40
|
+
'file://' +
|
|
41
|
+
resolve(cwd, 'node_modules/tsx/dist/loader.mjs'),
|
|
42
|
+
resolve(cwd, 'src/cli/supervisor.ts'),
|
|
43
|
+
], {
|
|
44
|
+
detached: true,
|
|
45
|
+
stdio: ['ignore', logFd, logFd],
|
|
46
|
+
cwd,
|
|
47
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
48
|
+
});
|
|
49
|
+
child.unref();
|
|
50
|
+
if (child.pid) {
|
|
51
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
52
|
+
console.log(`Started (PID: ${child.pid})`);
|
|
53
|
+
console.log(`Logs: heyamigo logs`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error('Failed to start');
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'stop': {
|
|
61
|
+
const pid = readPid();
|
|
62
|
+
if (!pid || !isAlive(pid)) {
|
|
63
|
+
console.log('Not running');
|
|
64
|
+
cleanPid();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
process.kill(pid, 'SIGTERM');
|
|
68
|
+
// Wait briefly for clean shutdown
|
|
69
|
+
for (let i = 0; i < 10; i++) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
71
|
+
if (!isAlive(pid))
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
cleanPid();
|
|
75
|
+
console.log('Stopped');
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'restart': {
|
|
79
|
+
await serviceCmd('stop');
|
|
80
|
+
await serviceCmd('start');
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'logs': {
|
|
84
|
+
if (!existsSync(LOG_FILE)) {
|
|
85
|
+
console.log('No logs yet. Start the bot first: heyamigo start');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
spawnSync('tail', ['-f', '-n', '50', LOG_FILE], {
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
});
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'status': {
|
|
94
|
+
const pid = readPid();
|
|
95
|
+
if (pid && isAlive(pid)) {
|
|
96
|
+
console.log(`Running (PID: ${pid})`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log('Not running');
|
|
100
|
+
cleanPid();
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|