@teamvibe/poller 0.2.1 → 0.2.3
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/dist/cli/doctor.js +67 -43
- package/dist/cli/plist.d.ts +5 -0
- package/dist/cli/plist.js +18 -0
- package/dist/logger.js +28 -22
- package/package.json +1 -1
- package/dist/control-handler.test.d.ts +0 -1
- package/dist/control-handler.test.js +0 -74
package/dist/cli/doctor.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
-
import { PLIST_PATH, SERVICE_LABEL, TEAMVIBE_DIR, LOGS_DIR } from './plist.js';
|
|
4
|
+
import { PLIST_PATH, SERVICE_LABEL, TEAMVIBE_DIR, LOGS_DIR, isContainerRuntime } from './plist.js';
|
|
5
|
+
function ok(name, message) {
|
|
6
|
+
return { name, status: 'ok', message };
|
|
7
|
+
}
|
|
8
|
+
function fail(name, message) {
|
|
9
|
+
return { name, status: 'fail', message };
|
|
10
|
+
}
|
|
11
|
+
function skipped(name, reason) {
|
|
12
|
+
return { name, status: 'skipped', message: `Skipped (${reason})` };
|
|
13
|
+
}
|
|
5
14
|
function check(name, fn) {
|
|
6
15
|
try {
|
|
7
16
|
const result = fn();
|
|
8
|
-
|
|
9
|
-
return { name, ok: true, message: 'OK' };
|
|
10
|
-
}
|
|
11
|
-
return { name, ok: true, message: result };
|
|
17
|
+
return ok(name, result === true ? 'OK' : result);
|
|
12
18
|
}
|
|
13
19
|
catch (error) {
|
|
14
|
-
return
|
|
20
|
+
return fail(name, error instanceof Error ? error.message : String(error));
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
function checkNodeVersion() {
|
|
@@ -24,7 +30,9 @@ function checkNodeVersion() {
|
|
|
24
30
|
return `${version} (>= v18)`;
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
|
-
function checkTeamVibeDir() {
|
|
33
|
+
function checkTeamVibeDir(container) {
|
|
34
|
+
if (container)
|
|
35
|
+
return skipped('TeamVibe directory', 'container');
|
|
28
36
|
return check('TeamVibe directory', () => {
|
|
29
37
|
if (!fs.existsSync(TEAMVIBE_DIR)) {
|
|
30
38
|
throw new Error(`${TEAMVIBE_DIR} does not exist. Run 'poller install' first.`);
|
|
@@ -32,7 +40,16 @@ function checkTeamVibeDir() {
|
|
|
32
40
|
return TEAMVIBE_DIR;
|
|
33
41
|
});
|
|
34
42
|
}
|
|
35
|
-
function checkEnvFile() {
|
|
43
|
+
function checkEnvFile(container) {
|
|
44
|
+
if (container) {
|
|
45
|
+
// In containers, check process.env instead of .env file
|
|
46
|
+
const hasToken = !!process.env['TEAMVIBE_POLLER_TOKEN'];
|
|
47
|
+
const hasDirect = !!(process.env['SQS_QUEUE_URL'] && process.env['SESSIONS_TABLE']);
|
|
48
|
+
if (!hasToken && !hasDirect) {
|
|
49
|
+
return fail('Environment config', 'Missing auth env vars (TEAMVIBE_POLLER_TOKEN or SQS_QUEUE_URL + SESSIONS_TABLE)');
|
|
50
|
+
}
|
|
51
|
+
return ok('Environment config', hasToken ? 'Token mode (env vars)' : 'Direct mode (env vars)');
|
|
52
|
+
}
|
|
36
53
|
return check('Environment config', () => {
|
|
37
54
|
const envPath = path.join(TEAMVIBE_DIR, '.env');
|
|
38
55
|
if (!fs.existsSync(envPath)) {
|
|
@@ -48,10 +65,9 @@ function checkEnvFile() {
|
|
|
48
65
|
});
|
|
49
66
|
}
|
|
50
67
|
function checkLaunchdPlist() {
|
|
68
|
+
if (process.platform !== 'darwin')
|
|
69
|
+
return skipped('Launchd plist', 'not macOS');
|
|
51
70
|
return check('Launchd plist', () => {
|
|
52
|
-
if (process.platform !== 'darwin') {
|
|
53
|
-
return 'Skipped (not macOS)';
|
|
54
|
-
}
|
|
55
71
|
if (!fs.existsSync(PLIST_PATH)) {
|
|
56
72
|
throw new Error(`Plist not found at ${PLIST_PATH}. Run 'poller install'.`);
|
|
57
73
|
}
|
|
@@ -66,10 +82,9 @@ function checkLaunchdPlist() {
|
|
|
66
82
|
});
|
|
67
83
|
}
|
|
68
84
|
function checkServiceRunning() {
|
|
85
|
+
if (process.platform !== 'darwin')
|
|
86
|
+
return skipped('Service status', 'not macOS');
|
|
69
87
|
return check('Service status', () => {
|
|
70
|
-
if (process.platform !== 'darwin') {
|
|
71
|
-
return 'Skipped (not macOS)';
|
|
72
|
-
}
|
|
73
88
|
try {
|
|
74
89
|
const output = execSync(`launchctl list | grep ${SERVICE_LABEL}`, { encoding: 'utf-8' });
|
|
75
90
|
if (output.trim()) {
|
|
@@ -91,7 +106,9 @@ function checkServiceRunning() {
|
|
|
91
106
|
}
|
|
92
107
|
});
|
|
93
108
|
}
|
|
94
|
-
function checkLogsDir() {
|
|
109
|
+
function checkLogsDir(container) {
|
|
110
|
+
if (container)
|
|
111
|
+
return skipped('Logs directory', 'container uses stdout');
|
|
95
112
|
return check('Logs directory', () => {
|
|
96
113
|
if (!fs.existsSync(LOGS_DIR)) {
|
|
97
114
|
throw new Error(`${LOGS_DIR} does not exist`);
|
|
@@ -140,48 +157,55 @@ function checkBrainsDir() {
|
|
|
140
157
|
return `${brains.length} brain(s) cloned`;
|
|
141
158
|
});
|
|
142
159
|
}
|
|
143
|
-
function checkDiskSpace() {
|
|
160
|
+
function checkDiskSpace(container) {
|
|
144
161
|
return check('Disk space', () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
if (error instanceof Error && error.message.includes('Disk'))
|
|
161
|
-
throw error;
|
|
162
|
-
throw new Error('Could not check disk space');
|
|
163
|
-
}
|
|
162
|
+
// Use TEAMVIBE_DIR for installed setups, fall back to / for containers or when dir doesn't exist
|
|
163
|
+
const dfTarget = !container && fs.existsSync(TEAMVIBE_DIR) ? TEAMVIBE_DIR : '/';
|
|
164
|
+
const output = execSync(`df -h "${dfTarget}" | tail -1`, { encoding: 'utf-8' }).trim();
|
|
165
|
+
const parts = output.split(/\s+/);
|
|
166
|
+
const available = parts[3];
|
|
167
|
+
const capacityStr = parts[4]?.replace('%', '');
|
|
168
|
+
const capacity = parseInt(capacityStr || '0', 10);
|
|
169
|
+
if (capacity > 95) {
|
|
170
|
+
throw new Error(`Disk ${capacity}% full — only ${available} available`);
|
|
171
|
+
}
|
|
172
|
+
if (capacity > 85) {
|
|
173
|
+
return `Warning: ${capacity}% used (${available} available)`;
|
|
174
|
+
}
|
|
175
|
+
return `${available} available (${capacity}% used)`;
|
|
164
176
|
});
|
|
165
177
|
}
|
|
166
178
|
export async function doctor() {
|
|
167
|
-
|
|
179
|
+
const container = isContainerRuntime();
|
|
180
|
+
const mode = container ? 'container' : 'installed';
|
|
181
|
+
console.log(`TeamVibe Poller - Health Check (${mode})\n`);
|
|
168
182
|
const results = [
|
|
169
183
|
checkNodeVersion(),
|
|
170
|
-
checkTeamVibeDir(),
|
|
171
|
-
checkEnvFile(),
|
|
184
|
+
checkTeamVibeDir(container),
|
|
185
|
+
checkEnvFile(container),
|
|
172
186
|
checkLaunchdPlist(),
|
|
173
187
|
checkServiceRunning(),
|
|
174
|
-
checkLogsDir(),
|
|
188
|
+
checkLogsDir(container),
|
|
175
189
|
checkClaudeCli(),
|
|
176
190
|
checkBrainsDir(),
|
|
177
|
-
checkDiskSpace(),
|
|
191
|
+
checkDiskSpace(container),
|
|
178
192
|
];
|
|
179
193
|
let hasFailure = false;
|
|
180
194
|
for (const result of results) {
|
|
181
|
-
|
|
195
|
+
let icon;
|
|
196
|
+
switch (result.status) {
|
|
197
|
+
case 'ok':
|
|
198
|
+
icon = '\x1b[32m✓\x1b[0m';
|
|
199
|
+
break;
|
|
200
|
+
case 'skipped':
|
|
201
|
+
icon = '\x1b[33m–\x1b[0m';
|
|
202
|
+
break;
|
|
203
|
+
case 'fail':
|
|
204
|
+
icon = '\x1b[31m✗\x1b[0m';
|
|
205
|
+
hasFailure = true;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
182
208
|
console.log(` ${icon} ${result.name}: ${result.message}`);
|
|
183
|
-
if (!result.ok)
|
|
184
|
-
hasFailure = true;
|
|
185
209
|
}
|
|
186
210
|
console.log('');
|
|
187
211
|
if (hasFailure) {
|
package/dist/cli/plist.d.ts
CHANGED
|
@@ -2,4 +2,9 @@ export declare const SERVICE_LABEL = "ai.teamvibe.poller";
|
|
|
2
2
|
export declare const PLIST_PATH: string;
|
|
3
3
|
export declare const TEAMVIBE_DIR: string;
|
|
4
4
|
export declare const LOGS_DIR: string;
|
|
5
|
+
/**
|
|
6
|
+
* Detect container runtime (Railway, Docker, etc.) where `poller install` was never run.
|
|
7
|
+
* In containers, env vars are injected directly — no ~/.teamvibe directory exists.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isContainerRuntime(): boolean;
|
|
5
10
|
export declare function generatePlist(nodePath: string, pollerPath: string, envVars?: Record<string, string>): string;
|
package/dist/cli/plist.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
1
2
|
import * as path from 'path';
|
|
2
3
|
import * as os from 'os';
|
|
3
4
|
export const SERVICE_LABEL = 'ai.teamvibe.poller';
|
|
4
5
|
export const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
|
|
5
6
|
export const TEAMVIBE_DIR = path.join(os.homedir(), '.teamvibe');
|
|
6
7
|
export const LOGS_DIR = path.join(TEAMVIBE_DIR, 'logs');
|
|
8
|
+
/**
|
|
9
|
+
* Detect container runtime (Railway, Docker, etc.) where `poller install` was never run.
|
|
10
|
+
* In containers, env vars are injected directly — no ~/.teamvibe directory exists.
|
|
11
|
+
*/
|
|
12
|
+
export function isContainerRuntime() {
|
|
13
|
+
// Railway-specific env vars
|
|
14
|
+
if (process.env['RAILWAY_ENVIRONMENT'] || process.env['RAILWAY_SERVICE_ID'])
|
|
15
|
+
return true;
|
|
16
|
+
// Generic container signals
|
|
17
|
+
if (process.env['CONTAINER'] === 'true' || process.env['DOCKER'] === 'true')
|
|
18
|
+
return true;
|
|
19
|
+
// Fallback heuristic: has auth env vars but no ~/.teamvibe directory
|
|
20
|
+
const hasEnvAuth = !!(process.env['TEAMVIBE_POLLER_TOKEN'] || (process.env['SQS_QUEUE_URL'] && process.env['SESSIONS_TABLE']));
|
|
21
|
+
if (hasEnvAuth && !fs.existsSync(TEAMVIBE_DIR))
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
7
25
|
export function generatePlist(nodePath, pollerPath, envVars) {
|
|
8
26
|
let envSection = '';
|
|
9
27
|
if (envVars && Object.keys(envVars).length > 0) {
|
package/dist/logger.js
CHANGED
|
@@ -14,8 +14,8 @@ if (!existsSync(SESSIONS_DIR)) {
|
|
|
14
14
|
function formatDate(date) {
|
|
15
15
|
return date.toISOString();
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const now = () => formatDate(new Date());
|
|
18
|
+
function writeToFile(level, message, timestamp, ...args) {
|
|
19
19
|
const formattedArgs = args
|
|
20
20
|
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
|
21
21
|
.join(' ');
|
|
@@ -35,8 +35,7 @@ export class SessionLogger {
|
|
|
35
35
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
36
36
|
this.logFile = join(SESSIONS_DIR, `${timestamp}_${sessionId}.log`);
|
|
37
37
|
}
|
|
38
|
-
write(level, message) {
|
|
39
|
-
const timestamp = formatDate(new Date());
|
|
38
|
+
write(level, message, timestamp) {
|
|
40
39
|
const logLine = `[${timestamp}] [${level}] ${message}\n`;
|
|
41
40
|
try {
|
|
42
41
|
appendFileSync(this.logFile, logLine);
|
|
@@ -46,27 +45,30 @@ export class SessionLogger {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
info(message) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const ts = now();
|
|
49
|
+
console.log(`[${ts}] [${this.sessionId}] ${message}`);
|
|
50
|
+
this.write('INFO', message, ts);
|
|
51
|
+
writeToFile('INFO', `[${this.sessionId}] ${message}`, ts);
|
|
52
52
|
}
|
|
53
53
|
error(message) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const ts = now();
|
|
55
|
+
console.error(`[${ts}] [${this.sessionId}] ${message}`);
|
|
56
|
+
this.write('ERROR', message, ts);
|
|
57
|
+
writeToFile('ERROR', `[${this.sessionId}] ${message}`, ts);
|
|
57
58
|
}
|
|
58
59
|
claude(stream, message) {
|
|
60
|
+
const ts = now();
|
|
59
61
|
const prefix = `[Claude ${stream}]`;
|
|
60
62
|
if (stream === 'stderr') {
|
|
61
|
-
console.error(`[${this.sessionId}] ${prefix}`, message);
|
|
63
|
+
console.error(`[${ts}] [${this.sessionId}] ${prefix}`, message);
|
|
62
64
|
}
|
|
63
65
|
else {
|
|
64
|
-
console.log(`[${this.sessionId}] ${prefix}`, message);
|
|
66
|
+
console.log(`[${ts}] [${this.sessionId}] ${prefix}`, message);
|
|
65
67
|
}
|
|
66
|
-
this.write(`CLAUDE_${stream.toUpperCase()}`, message);
|
|
68
|
+
this.write(`CLAUDE_${stream.toUpperCase()}`, message, ts);
|
|
67
69
|
}
|
|
68
70
|
logPrompt(prompt) {
|
|
69
|
-
this.write('PROMPT', `\n${'='.repeat(50)}\n${prompt}\n${'='.repeat(50)}
|
|
71
|
+
this.write('PROMPT', `\n${'='.repeat(50)}\n${prompt}\n${'='.repeat(50)}`, now());
|
|
70
72
|
}
|
|
71
73
|
getLogFile() {
|
|
72
74
|
return this.logFile;
|
|
@@ -74,22 +76,26 @@ export class SessionLogger {
|
|
|
74
76
|
}
|
|
75
77
|
export const logger = {
|
|
76
78
|
info(message, ...args) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const ts = now();
|
|
80
|
+
console.log(`[${ts}] [INFO] ${message}`, ...args);
|
|
81
|
+
writeToFile('INFO', message, ts, ...args);
|
|
79
82
|
},
|
|
80
83
|
error(message, ...args) {
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
const ts = now();
|
|
85
|
+
console.error(`[${ts}] [ERROR] ${message}`, ...args);
|
|
86
|
+
writeToFile('ERROR', message, ts, ...args);
|
|
83
87
|
},
|
|
84
88
|
warn(message, ...args) {
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
const ts = now();
|
|
90
|
+
console.warn(`[${ts}] [WARN] ${message}`, ...args);
|
|
91
|
+
writeToFile('WARN', message, ts, ...args);
|
|
87
92
|
},
|
|
88
93
|
debug(message, ...args) {
|
|
94
|
+
const ts = now();
|
|
89
95
|
if (process.env['DEBUG']) {
|
|
90
|
-
console.debug(message
|
|
96
|
+
console.debug(`[${ts}] [DEBUG] ${message}`, ...args);
|
|
91
97
|
}
|
|
92
|
-
writeToFile('DEBUG', message, ...args);
|
|
98
|
+
writeToFile('DEBUG', message, ts, ...args);
|
|
93
99
|
},
|
|
94
100
|
createSession(sessionId) {
|
|
95
101
|
return new SessionLogger(sessionId);
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { isControlMessage } from './control-handler.js';
|
|
3
|
-
import { classifyUpdate } from './auto-update.js';
|
|
4
|
-
describe('isControlMessage', () => {
|
|
5
|
-
test('valid restart message', () => {
|
|
6
|
-
expect(isControlMessage({ source: 'control', action: 'restart' })).toBe(true);
|
|
7
|
-
});
|
|
8
|
-
test('valid update message', () => {
|
|
9
|
-
expect(isControlMessage({ source: 'control', action: 'update' })).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
test('valid update with targetVersion', () => {
|
|
12
|
-
expect(isControlMessage({ source: 'control', action: 'update', targetVersion: '1.2.3' })).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
test('rejects non-control source', () => {
|
|
15
|
-
expect(isControlMessage({ source: 'slack', action: 'restart' })).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
test('rejects unknown action', () => {
|
|
18
|
-
expect(isControlMessage({ source: 'control', action: 'delete' })).toBe(false);
|
|
19
|
-
});
|
|
20
|
-
test('rejects null', () => {
|
|
21
|
-
expect(isControlMessage(null)).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
test('rejects undefined', () => {
|
|
24
|
-
expect(isControlMessage(undefined)).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
test('rejects string', () => {
|
|
27
|
-
expect(isControlMessage('control')).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
test('rejects empty object', () => {
|
|
30
|
-
expect(isControlMessage({})).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
test('rejects regular queue message', () => {
|
|
33
|
-
expect(isControlMessage({
|
|
34
|
-
id: '123',
|
|
35
|
-
source: 'cron',
|
|
36
|
-
thread_id: 'thread1',
|
|
37
|
-
sender: { id: 'user1', name: 'Test' },
|
|
38
|
-
text: 'hello',
|
|
39
|
-
})).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('classifyUpdate', () => {
|
|
43
|
-
test('detects patch update', () => {
|
|
44
|
-
expect(classifyUpdate('1.2.3', '1.2.4')).toBe('patch');
|
|
45
|
-
});
|
|
46
|
-
test('detects minor update', () => {
|
|
47
|
-
expect(classifyUpdate('1.2.3', '1.3.0')).toBe('minor');
|
|
48
|
-
});
|
|
49
|
-
test('detects major update', () => {
|
|
50
|
-
expect(classifyUpdate('1.2.3', '2.0.0')).toBe('major');
|
|
51
|
-
});
|
|
52
|
-
test('returns null for same version', () => {
|
|
53
|
-
expect(classifyUpdate('1.2.3', '1.2.3')).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
test('returns null for downgrade (patch)', () => {
|
|
56
|
-
expect(classifyUpdate('1.2.4', '1.2.3')).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
test('returns null for downgrade (minor)', () => {
|
|
59
|
-
expect(classifyUpdate('1.3.0', '1.2.5')).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
test('returns null for downgrade (major)', () => {
|
|
62
|
-
expect(classifyUpdate('2.0.0', '1.9.9')).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
test('handles zero versions', () => {
|
|
65
|
-
expect(classifyUpdate('0.0.1', '0.0.2')).toBe('patch');
|
|
66
|
-
expect(classifyUpdate('0.1.0', '0.2.0')).toBe('minor');
|
|
67
|
-
});
|
|
68
|
-
test('minor update with different patch', () => {
|
|
69
|
-
expect(classifyUpdate('1.2.3', '1.3.5')).toBe('minor');
|
|
70
|
-
});
|
|
71
|
-
test('major update with different minor/patch', () => {
|
|
72
|
-
expect(classifyUpdate('1.2.3', '2.1.0')).toBe('major');
|
|
73
|
-
});
|
|
74
|
-
});
|