@teamvibe/poller 0.1.17 → 0.1.19
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/brain-manager.d.ts +2 -5
- package/dist/brain-manager.js +51 -14
- package/dist/cli/commands.js +2 -1
- package/dist/cli/install.js +87 -25
- package/dist/poller.js +2 -2
- package/package.json +1 -1
package/dist/brain-manager.d.ts
CHANGED
|
@@ -3,10 +3,7 @@ interface BrainConfig {
|
|
|
3
3
|
gitRepoUrl: string;
|
|
4
4
|
branch: string;
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
* Get or clone brain repo. Returns the working directory path.
|
|
8
|
-
*/
|
|
9
|
-
export declare function getBrainPath(brain?: BrainConfig, channelId?: string): Promise<string>;
|
|
6
|
+
export declare function getBrainPath(brain?: BrainConfig, channelId?: string, workspaceId?: string): Promise<string>;
|
|
10
7
|
/**
|
|
11
8
|
* Ensure base brain repo is cloned and up to date.
|
|
12
9
|
* Uses same cooldown pattern as channel brains.
|
|
@@ -20,7 +17,7 @@ export declare function getBaseBrainPath(): string;
|
|
|
20
17
|
* Commit and push any changes in a brain repo.
|
|
21
18
|
* Silently skips if there are no changes or no remote.
|
|
22
19
|
*/
|
|
23
|
-
export declare function pushBrainChanges(brainDir: string, brainId: string): Promise<void>;
|
|
20
|
+
export declare function pushBrainChanges(brainDir: string, brainId: string, workspaceId?: string): Promise<void>;
|
|
24
21
|
/**
|
|
25
22
|
* Ensure base paths exist
|
|
26
23
|
*/
|
package/dist/brain-manager.js
CHANGED
|
@@ -5,12 +5,43 @@ import { join } from 'path';
|
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
|
+
// Clean env for git commands — npm/npx sets GIT_ASKPASS=echo which
|
|
9
|
+
// prevents git credential helpers (like gh) from working.
|
|
10
|
+
const gitEnv = { ...process.env };
|
|
11
|
+
delete gitEnv['GIT_ASKPASS'];
|
|
12
|
+
function gitExec(command, options) {
|
|
13
|
+
return execAsync(command, { ...options, env: gitEnv });
|
|
14
|
+
}
|
|
8
15
|
// Cooldown tracker per brain
|
|
9
16
|
const lastUpdateTimes = new Map();
|
|
10
17
|
/**
|
|
11
18
|
* Get or clone brain repo. Returns the working directory path.
|
|
12
19
|
*/
|
|
13
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Report a brain sync event (pull/push) to the TeamVibe API.
|
|
22
|
+
* Fire-and-forget — errors are logged but not thrown.
|
|
23
|
+
*/
|
|
24
|
+
async function reportBrainSync(brainId, workspaceId, event) {
|
|
25
|
+
if (!config.TEAMVIBE_API_URL || !config.TEAMVIBE_POLLER_TOKEN)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(`${config.TEAMVIBE_API_URL}/brain-sync`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${config.TEAMVIBE_POLLER_TOKEN}`,
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({ brainId, workspaceId, event }),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
logger.warn(`Brain sync report failed (${response.status})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.warn(`Brain sync report error: ${error instanceof Error ? error.message : error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function getBrainPath(brain, channelId, workspaceId) {
|
|
14
45
|
if (!brain?.gitRepoUrl) {
|
|
15
46
|
if (channelId) {
|
|
16
47
|
const channelDir = join(config.BRAINS_PATH, `channel_${channelId}`);
|
|
@@ -24,23 +55,27 @@ export async function getBrainPath(brain, channelId) {
|
|
|
24
55
|
const brainDir = join(config.BRAINS_PATH, brain.brainId);
|
|
25
56
|
if (!existsSync(brainDir)) {
|
|
26
57
|
logger.info(`Cloning brain ${brain.brainId} from ${brain.gitRepoUrl} (branch: ${brain.branch})...`);
|
|
27
|
-
await
|
|
58
|
+
await gitExec(`git clone --recurse-submodules ${brain.gitRepoUrl} ${brainDir}`);
|
|
28
59
|
// Ensure the desired branch exists (handles empty repos and missing branches)
|
|
29
|
-
const { stdout: currentBranch } = await
|
|
60
|
+
const { stdout: currentBranch } = await gitExec('git branch --show-current', { cwd: brainDir });
|
|
30
61
|
if (currentBranch.trim() !== brain.branch) {
|
|
31
62
|
// Check if the branch exists on remote
|
|
32
|
-
const { stdout: remoteBranches } = await
|
|
63
|
+
const { stdout: remoteBranches } = await gitExec('git branch -r', { cwd: brainDir });
|
|
33
64
|
if (remoteBranches.includes(`origin/${brain.branch}`)) {
|
|
34
|
-
await
|
|
65
|
+
await gitExec(`git checkout ${brain.branch}`, { cwd: brainDir });
|
|
35
66
|
}
|
|
36
67
|
else {
|
|
37
|
-
await
|
|
68
|
+
await gitExec(`git checkout -b ${brain.branch}`, { cwd: brainDir });
|
|
38
69
|
}
|
|
39
70
|
}
|
|
40
71
|
logger.info(`Brain ${brain.brainId} cloned successfully`);
|
|
72
|
+
if (workspaceId)
|
|
73
|
+
reportBrainSync(brain.brainId, workspaceId, 'pulled');
|
|
41
74
|
}
|
|
42
75
|
else if (config.BRAIN_AUTO_UPDATE) {
|
|
43
76
|
await updateBrain(brainDir, brain.brainId, brain.branch);
|
|
77
|
+
if (workspaceId)
|
|
78
|
+
reportBrainSync(brain.brainId, workspaceId, 'pulled');
|
|
44
79
|
}
|
|
45
80
|
return brainDir;
|
|
46
81
|
}
|
|
@@ -56,7 +91,7 @@ async function updateBrain(brainDir, brainId, branch) {
|
|
|
56
91
|
}
|
|
57
92
|
try {
|
|
58
93
|
logger.info(`Updating brain ${brainId} at ${brainDir}...`);
|
|
59
|
-
await
|
|
94
|
+
await gitExec(`git fetch origin ${branch} && git reset --hard origin/${branch} && git submodule update --init --recursive`, {
|
|
60
95
|
cwd: brainDir,
|
|
61
96
|
});
|
|
62
97
|
lastUpdateTimes.set(brainId, Date.now());
|
|
@@ -75,7 +110,7 @@ export async function ensureBaseBrain() {
|
|
|
75
110
|
const brainDir = config.BASE_BRAIN_PATH;
|
|
76
111
|
if (!existsSync(brainDir)) {
|
|
77
112
|
logger.info(`Cloning base brain from ${config.BASE_BRAIN_REPO} (branch: ${config.BASE_BRAIN_BRANCH})...`);
|
|
78
|
-
await
|
|
113
|
+
await gitExec(`git clone --recurse-submodules --branch ${config.BASE_BRAIN_BRANCH} ${config.BASE_BRAIN_REPO} ${brainDir}`);
|
|
79
114
|
lastUpdateTimes.set('__base_brain__', Date.now());
|
|
80
115
|
logger.info('Base brain cloned successfully');
|
|
81
116
|
}
|
|
@@ -93,21 +128,21 @@ export function getBaseBrainPath() {
|
|
|
93
128
|
* Commit and push any changes in a brain repo.
|
|
94
129
|
* Silently skips if there are no changes or no remote.
|
|
95
130
|
*/
|
|
96
|
-
export async function pushBrainChanges(brainDir, brainId) {
|
|
131
|
+
export async function pushBrainChanges(brainDir, brainId, workspaceId) {
|
|
97
132
|
try {
|
|
98
133
|
// Check if this is a git repo with a remote
|
|
99
134
|
try {
|
|
100
|
-
await
|
|
135
|
+
await gitExec('git remote get-url origin', { cwd: brainDir });
|
|
101
136
|
}
|
|
102
137
|
catch {
|
|
103
138
|
logger.debug(`Brain ${brainId} has no git remote, skipping push`);
|
|
104
139
|
return;
|
|
105
140
|
}
|
|
106
141
|
// Stage all changes
|
|
107
|
-
await
|
|
142
|
+
await gitExec('git add -A', { cwd: brainDir });
|
|
108
143
|
// Check if there are staged changes
|
|
109
144
|
try {
|
|
110
|
-
await
|
|
145
|
+
await gitExec('git diff --cached --quiet', { cwd: brainDir });
|
|
111
146
|
// If the command succeeds (exit 0), there are no changes
|
|
112
147
|
logger.debug(`Brain ${brainId} has no changes to push`);
|
|
113
148
|
return;
|
|
@@ -115,9 +150,11 @@ export async function pushBrainChanges(brainDir, brainId) {
|
|
|
115
150
|
catch {
|
|
116
151
|
// Exit code 1 means there are changes — continue
|
|
117
152
|
}
|
|
118
|
-
await
|
|
119
|
-
await
|
|
153
|
+
await gitExec('git commit -m "auto: session update"', { cwd: brainDir });
|
|
154
|
+
await gitExec('git push', { cwd: brainDir });
|
|
120
155
|
logger.info(`Brain ${brainId} changes pushed successfully`);
|
|
156
|
+
if (workspaceId)
|
|
157
|
+
reportBrainSync(brainId, workspaceId, 'pushed');
|
|
121
158
|
}
|
|
122
159
|
catch (error) {
|
|
123
160
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
package/dist/cli/commands.js
CHANGED
|
@@ -10,7 +10,8 @@ Usage: poller [command]
|
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
12
|
(no command) Start the poller (default)
|
|
13
|
-
install Install as a macOS launchd service
|
|
13
|
+
install Install as a macOS launchd service
|
|
14
|
+
Flags: --token <token> --claude-token <token> [--api-url <url>] [--max-concurrent <n>]
|
|
14
15
|
uninstall Remove the launchd service
|
|
15
16
|
update Update to the latest version and restart
|
|
16
17
|
start Start the installed service
|
package/dist/cli/install.js
CHANGED
|
@@ -17,18 +17,37 @@ function tryGetClaudeSetupToken() {
|
|
|
17
17
|
}
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
|
+
function parseFlags(args) {
|
|
21
|
+
const flags = {};
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const arg = args[i];
|
|
24
|
+
const next = args[i + 1];
|
|
25
|
+
if (arg.startsWith('--') && next && !next.startsWith('--')) {
|
|
26
|
+
flags[arg.slice(2)] = next;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return flags;
|
|
31
|
+
}
|
|
20
32
|
export async function install() {
|
|
21
33
|
if (process.platform !== 'darwin') {
|
|
22
34
|
console.error('Error: Service installation is only supported on macOS.');
|
|
23
35
|
process.exit(1);
|
|
24
36
|
}
|
|
37
|
+
const flags = parseFlags(process.argv.slice(3));
|
|
38
|
+
const nonInteractive = !!(flags['token']);
|
|
25
39
|
console.log('TeamVibe Poller - Service Installation\n');
|
|
26
40
|
// Check if already installed
|
|
27
41
|
if (fs.existsSync(PLIST_PATH)) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
if (nonInteractive) {
|
|
43
|
+
console.log('Service already installed, overwriting...');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const overwrite = await confirm('Service is already installed. Overwrite?', false);
|
|
47
|
+
if (!overwrite) {
|
|
48
|
+
console.log('Aborted.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
// Unload existing service
|
|
34
53
|
try {
|
|
@@ -41,34 +60,70 @@ export async function install() {
|
|
|
41
60
|
// Load existing .env values as defaults
|
|
42
61
|
const existingEnv = loadExistingEnv();
|
|
43
62
|
// Collect configuration
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
let token;
|
|
64
|
+
if (flags['token']) {
|
|
65
|
+
token = flags['token'];
|
|
66
|
+
console.log(`Using provided token: ${token.slice(0, 8)}...`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log('Step 1: Poller Token\n');
|
|
70
|
+
console.log(' Get your token from the TeamVibe dashboard (Pollers > Setup Instructions).');
|
|
71
|
+
console.log(' If you lost it, use "Regenerate Token" from the poller menu.\n');
|
|
72
|
+
token = await prompt('TEAMVIBE_POLLER_TOKEN', existingEnv['TEAMVIBE_POLLER_TOKEN']);
|
|
73
|
+
}
|
|
48
74
|
if (!token) {
|
|
49
75
|
console.error('\nError: TEAMVIBE_POLLER_TOKEN is required.');
|
|
50
76
|
process.exit(1);
|
|
51
77
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
let claudeOAuthToken = flags['claude-token'] || existingEnv['CLAUDE_CODE_OAUTH_TOKEN'] || '';
|
|
79
|
+
if (!claudeOAuthToken) {
|
|
80
|
+
if (nonInteractive) {
|
|
81
|
+
console.log('\nDetecting Claude Code token...');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log('\nStep 2: Claude Code Authentication\n');
|
|
85
|
+
console.log(' The poller needs Claude Code credentials to run AI sessions.');
|
|
86
|
+
console.log(' Run `claude setup-token` in a terminal to generate a token.\n');
|
|
87
|
+
}
|
|
88
|
+
const autoToken = tryGetClaudeSetupToken();
|
|
89
|
+
if (autoToken) {
|
|
90
|
+
if (nonInteractive) {
|
|
91
|
+
claudeOAuthToken = autoToken;
|
|
92
|
+
console.log('Auto-detected Claude setup token.');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(' Auto-detected Claude setup token.');
|
|
96
|
+
const useAuto = await confirm(' Use the detected token?');
|
|
97
|
+
if (useAuto) {
|
|
98
|
+
claudeOAuthToken = autoToken;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!claudeOAuthToken && !nonInteractive) {
|
|
103
|
+
claudeOAuthToken = await prompt('CLAUDE_CODE_OAUTH_TOKEN');
|
|
62
104
|
}
|
|
63
105
|
}
|
|
64
|
-
if (
|
|
65
|
-
|
|
106
|
+
else if (flags['claude-token']) {
|
|
107
|
+
console.log('Using provided Claude Code token.');
|
|
108
|
+
}
|
|
109
|
+
let apiUrl;
|
|
110
|
+
let maxConcurrent;
|
|
111
|
+
if (nonInteractive) {
|
|
112
|
+
apiUrl = flags['api-url'] || existingEnv['TEAMVIBE_API_URL'] || 'https://poller.api.teamvibe.ai';
|
|
113
|
+
maxConcurrent = flags['max-concurrent'] || existingEnv['MAX_CONCURRENT_SESSIONS'] || '5';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log('\nStep 3: Optional Settings\n');
|
|
117
|
+
apiUrl = await prompt('TEAMVIBE_API_URL', existingEnv['TEAMVIBE_API_URL'] || 'https://poller.api.teamvibe.ai');
|
|
118
|
+
maxConcurrent = await prompt('MAX_CONCURRENT_SESSIONS', existingEnv['MAX_CONCURRENT_SESSIONS'] || '5');
|
|
66
119
|
}
|
|
67
|
-
console.log('\nStep 3: Optional Settings\n');
|
|
68
|
-
const apiUrl = await prompt('TEAMVIBE_API_URL', existingEnv['TEAMVIBE_API_URL'] || 'https://poller.api.teamvibe.ai');
|
|
69
|
-
const maxConcurrent = await prompt('MAX_CONCURRENT_SESSIONS', existingEnv['MAX_CONCURRENT_SESSIONS'] || '5');
|
|
70
120
|
// Resolve claude CLI path
|
|
71
|
-
|
|
121
|
+
if (!nonInteractive) {
|
|
122
|
+
console.log('\nStep 4: Detecting paths\n');
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log('\nDetecting paths...');
|
|
126
|
+
}
|
|
72
127
|
let claudePath = '';
|
|
73
128
|
try {
|
|
74
129
|
claudePath = execSync('which claude', { encoding: 'utf-8' }).trim();
|
|
@@ -77,6 +132,10 @@ export async function install() {
|
|
|
77
132
|
// Not found
|
|
78
133
|
}
|
|
79
134
|
if (!claudePath) {
|
|
135
|
+
if (nonInteractive) {
|
|
136
|
+
console.error('Error: claude CLI not found in PATH.');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
80
139
|
claudePath = await prompt('Path to claude CLI (not found in PATH)');
|
|
81
140
|
if (!claudePath) {
|
|
82
141
|
console.error('Error: claude CLI path is required.');
|
|
@@ -106,6 +165,9 @@ export async function install() {
|
|
|
106
165
|
catch {
|
|
107
166
|
console.error('\n Warning: Could not install globally. The service may not start after reboot.');
|
|
108
167
|
console.error(' Run `npm install -g @teamvibe/poller` manually, then re-run `poller install`.');
|
|
168
|
+
if (nonInteractive) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
109
171
|
const cont = await confirm(' Continue anyway?', false);
|
|
110
172
|
if (!cont)
|
|
111
173
|
return;
|
|
@@ -128,7 +190,7 @@ export async function install() {
|
|
|
128
190
|
envLines.push(`CLAUDE_CODE_OAUTH_TOKEN=${claudeOAuthToken}`);
|
|
129
191
|
}
|
|
130
192
|
let writeEnv = true;
|
|
131
|
-
if (fs.existsSync(envPath)) {
|
|
193
|
+
if (fs.existsSync(envPath) && !nonInteractive) {
|
|
132
194
|
writeEnv = await confirm('\n.env file already exists. Overwrite?', false);
|
|
133
195
|
if (!writeEnv) {
|
|
134
196
|
console.log('Keeping existing .env file.');
|
package/dist/poller.js
CHANGED
|
@@ -82,7 +82,7 @@ async function processMessage(received) {
|
|
|
82
82
|
// Get brain path for this channel
|
|
83
83
|
let kbPath;
|
|
84
84
|
try {
|
|
85
|
-
kbPath = await getBrainPath(queueMessage.teamvibe.brain, queueMessage.response_context.slack?.channel);
|
|
85
|
+
kbPath = await getBrainPath(queueMessage.teamvibe.brain, queueMessage.response_context.slack?.channel, queueMessage.teamvibe.workspaceId);
|
|
86
86
|
}
|
|
87
87
|
catch (error) {
|
|
88
88
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -139,7 +139,7 @@ async function processMessage(received) {
|
|
|
139
139
|
sessionLog.info('Claude Code completed successfully');
|
|
140
140
|
// Push any changes in the channel brain repo
|
|
141
141
|
if (queueMessage.teamvibe.brain?.brainId) {
|
|
142
|
-
await pushBrainChanges(kbPath, queueMessage.teamvibe.brain.brainId);
|
|
142
|
+
await pushBrainChanges(kbPath, queueMessage.teamvibe.brain.brainId, queueMessage.teamvibe.workspaceId);
|
|
143
143
|
}
|
|
144
144
|
if (lockToken) {
|
|
145
145
|
const lastMessageTs = queueMessage.response_context.slack?.message_ts;
|