@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.
@@ -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
  */
@@ -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
- export async function getBrainPath(brain, channelId) {
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 execAsync(`git clone --recurse-submodules ${brain.gitRepoUrl} ${brainDir}`);
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 execAsync('git branch --show-current', { cwd: brainDir });
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 execAsync('git branch -r', { cwd: brainDir });
63
+ const { stdout: remoteBranches } = await gitExec('git branch -r', { cwd: brainDir });
33
64
  if (remoteBranches.includes(`origin/${brain.branch}`)) {
34
- await execAsync(`git checkout ${brain.branch}`, { cwd: brainDir });
65
+ await gitExec(`git checkout ${brain.branch}`, { cwd: brainDir });
35
66
  }
36
67
  else {
37
- await execAsync(`git checkout -b ${brain.branch}`, { cwd: brainDir });
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 execAsync(`git fetch origin ${branch} && git reset --hard origin/${branch} && git submodule update --init --recursive`, {
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 execAsync(`git clone --recurse-submodules --branch ${config.BASE_BRAIN_BRANCH} ${config.BASE_BRAIN_REPO} ${brainDir}`);
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 execAsync('git remote get-url origin', { cwd: brainDir });
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 execAsync('git add -A', { cwd: brainDir });
142
+ await gitExec('git add -A', { cwd: brainDir });
108
143
  // Check if there are staged changes
109
144
  try {
110
- await execAsync('git diff --cached --quiet', { cwd: brainDir });
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 execAsync('git commit -m "auto: session update"', { cwd: brainDir });
119
- await execAsync('git push', { cwd: brainDir });
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);
@@ -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 (interactive)
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
@@ -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
- const overwrite = await confirm('Service is already installed. Overwrite?', false);
29
- if (!overwrite) {
30
- console.log('Aborted.');
31
- return;
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
- console.log('Step 1: Poller Token\n');
45
- console.log(' Get your token from the TeamVibe dashboard (Pollers > Setup Instructions).');
46
- console.log(' If you lost it, use "Regenerate Token" from the poller menu.\n');
47
- const token = await prompt('TEAMVIBE_POLLER_TOKEN', existingEnv['TEAMVIBE_POLLER_TOKEN']);
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
- console.log('\nStep 2: Claude Code Authentication\n');
53
- console.log(' The poller needs Claude Code credentials to run AI sessions.');
54
- console.log(' Run `claude setup-token` in a terminal to generate a token.\n');
55
- let claudeOAuthToken = existingEnv['CLAUDE_CODE_OAUTH_TOKEN'] || '';
56
- const autoToken = tryGetClaudeSetupToken();
57
- if (autoToken) {
58
- console.log(' Auto-detected Claude setup token.');
59
- const useAuto = await confirm(' Use the detected token?');
60
- if (useAuto) {
61
- claudeOAuthToken = autoToken;
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 (!claudeOAuthToken) {
65
- claudeOAuthToken = await prompt('CLAUDE_CODE_OAUTH_TOKEN');
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
- console.log('\nStep 4: Detecting paths\n');
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {