@teamvibe/poller 0.5.0 → 0.5.2

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.
@@ -134,7 +134,7 @@ export async function buildContextHint(p) {
134
134
  if (!p.isFirstMessage && p.lastMessageTs && p.slack?.channel && p.slack?.thread_ts) {
135
135
  const newMessageCount = await p.countNewSinceTs(p.slack.channel, p.slack.thread_ts, p.lastMessageTs);
136
136
  return newMessageCount > 0
137
- ? `**Resumed session:** ${newMessageCount} new message(s) in the thread since your last response. Consider using \`read_thread\` to see what was discussed before responding.`
137
+ ? `**Resumed session:** ${newMessageCount} new message(s) in the thread since your last response.\nCall \`read_thread\` first to load conversation context.`
138
138
  : `**Resumed session:** No new messages since your last response.`;
139
139
  }
140
140
  if (p.isFirstMessage &&
@@ -405,14 +405,22 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
405
405
  // Placed FIRST so subsequent spreads can overwrite. Belt-and-
406
406
  // suspenders: even if the API's reserved-prefix validator fails
407
407
  // (`AWS_`, `TEAMVIBE_`, `SLACK_`, `CLAUDE_CODE_`, `ANTHROPIC_`)
408
- // a name collision is silently won by the platform variable.
408
+ // a name collision is silently won by the platform variable
409
+ // explicit field below.
410
+ //
411
+ // Phase 2 of teamvibe.ai#215 (poller v0.5.1): the explicit
412
+ // `SLACK_BOT_TOKEN: msg.teamvibe.botToken` injection that used
413
+ // to live here was removed. `SLACK_BOT_TOKEN` now sources from
414
+ // the `/secrets/spawn` platform overlay (the spread above) —
415
+ // backend v0.2.2+ resolves it via the `Channel → SlackBot`
416
+ // relation. Requires backend on Phase 1; pollers on 0.5.0 still
417
+ // read the explicit field, so split rollout is safe.
409
418
  ...spawnSecrets.env.unwrap(),
410
419
  ...process.env,
411
420
  CLAUDECODE: undefined,
412
421
  PATH: envPath,
413
422
  CI: 'true',
414
423
  CLAUDE_CONFIG_DIR: getBaseBrainPath(),
415
- SLACK_BOT_TOKEN: msg.teamvibe.botToken,
416
424
  ...(config.PERSISTENT_STORAGE_PATH && {
417
425
  PERSISTENT_STORAGE_PATH: config.PERSISTENT_STORAGE_PATH,
418
426
  }),
@@ -1,7 +1,7 @@
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, isContainerRuntime } from './plist.js';
4
+ import { PLIST_PATH, SERVICE_LABEL, TEAMVIBE_DIR, LOGS_DIR, isContainerRuntime, plistHasStaleNodePath } from './plist.js';
5
5
  function ok(name, message) {
6
6
  return { name, status: 'ok', message };
7
7
  }
@@ -78,9 +78,46 @@ function checkLaunchdPlist() {
78
78
  if (content.includes('/_npx/')) {
79
79
  throw new Error('Plist references stale npx cache path. Run \'poller update\' to fix.');
80
80
  }
81
+ if (plistHasStaleNodePath(content)) {
82
+ throw new Error('Plist references brew Cellar Node path (e.g. /opt/homebrew/Cellar/node/<version>/bin/node). ' +
83
+ 'This path disappears on `brew upgrade node`, causing launchd exit 78 (EX_CONFIG). ' +
84
+ 'Run \'poller update\' to migrate to /usr/bin/env node.');
85
+ }
81
86
  return 'Installed with KeepAlive';
82
87
  });
83
88
  }
89
+ /**
90
+ * Verify the binary referenced as the first ProgramArguments entry actually exists.
91
+ * Catches cases where launchd cannot spawn the program (manifesting as exit 78
92
+ * with no logs, since the process never ran).
93
+ */
94
+ function checkPlistBinary() {
95
+ if (process.platform !== 'darwin')
96
+ return skipped('Plist binary', 'not macOS');
97
+ return check('Plist binary', () => {
98
+ if (!fs.existsSync(PLIST_PATH)) {
99
+ throw new Error(`Plist not found at ${PLIST_PATH}`);
100
+ }
101
+ const content = fs.readFileSync(PLIST_PATH, 'utf-8');
102
+ const argsMatch = content.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
103
+ if (!argsMatch?.[1]) {
104
+ throw new Error('Plist has no ProgramArguments');
105
+ }
106
+ const firstArg = argsMatch[1].match(/<string>([^<]+)<\/string>/)?.[1];
107
+ if (!firstArg) {
108
+ throw new Error('Plist ProgramArguments is empty');
109
+ }
110
+ if (!path.isAbsolute(firstArg)) {
111
+ throw new Error(`Plist ProgramArguments[0] is not absolute: ${firstArg}`);
112
+ }
113
+ if (!fs.existsSync(firstArg)) {
114
+ throw new Error(`Plist references non-existent binary: ${firstArg}. ` +
115
+ 'Likely cause: brew upgraded Node and the old Cellar path is gone. ' +
116
+ 'Run \'poller update\' to regenerate plist with /usr/bin/env node.');
117
+ }
118
+ return firstArg;
119
+ });
120
+ }
84
121
  function checkServiceRunning() {
85
122
  if (process.platform !== 'darwin')
86
123
  return skipped('Service status', 'not macOS');
@@ -184,6 +221,7 @@ export async function doctor() {
184
221
  checkTeamVibeDir(container),
185
222
  checkEnvFile(container),
186
223
  checkLaunchdPlist(),
224
+ checkPlistBinary(),
187
225
  checkServiceRunning(),
188
226
  checkLogsDir(container),
189
227
  checkClaudeCli(),
@@ -160,8 +160,8 @@ export async function install() {
160
160
  else {
161
161
  console.log(` Found claude CLI at: ${claudePath}`);
162
162
  }
163
- // Resolve node and poller paths
164
- const nodePath = process.execPath;
163
+ // Resolve poller path. Node is resolved at runtime via /usr/bin/env from
164
+ // EnvironmentVariables.PATH below, so we do not pin a specific node binary.
165
165
  let pollerPath = path.resolve(process.argv[1] ?? '.');
166
166
  // Detect if running via npx (temporary cache path) — need a stable path for launchd
167
167
  const isNpx = pollerPath.includes('/_npx/') || pollerPath.includes('\\_npx\\');
@@ -188,8 +188,8 @@ export async function install() {
188
188
  return;
189
189
  }
190
190
  }
191
- console.log(` Node path: ${nodePath}`);
192
191
  console.log(` Poller path: ${pollerPath}`);
192
+ console.log(` Node: resolved at runtime via /usr/bin/env from launchd PATH`);
193
193
  // Create directories
194
194
  fs.mkdirSync(TEAMVIBE_DIR, { recursive: true });
195
195
  fs.mkdirSync(LOGS_DIR, { recursive: true });
@@ -231,7 +231,7 @@ export async function install() {
231
231
  PATH: Array.from(pathDirs).join(':'),
232
232
  };
233
233
  // Write plist
234
- const plistContent = generatePlist(nodePath, pollerPath, plistEnvVars);
234
+ const plistContent = generatePlist(pollerPath, plistEnvVars);
235
235
  const launchAgentsDir = path.dirname(PLIST_PATH);
236
236
  fs.mkdirSync(launchAgentsDir, { recursive: true });
237
237
  fs.writeFileSync(PLIST_PATH, plistContent);
@@ -7,4 +7,10 @@ export declare const LOGS_DIR: string;
7
7
  * In containers, env vars are injected directly — no ~/.teamvibe directory exists.
8
8
  */
9
9
  export declare function isContainerRuntime(): boolean;
10
- export declare function generatePlist(nodePath: string, pollerPath: string, envVars?: Record<string, string>): string;
10
+ export declare function generatePlist(pollerPath: string, envVars?: Record<string, string>): string;
11
+ /**
12
+ * Heuristic: does this plist content reference a Node binary path that can
13
+ * disappear on `brew upgrade node`? The legacy installer wrote
14
+ * /opt/homebrew/Cellar/node/<version>/bin/node directly into ProgramArguments.
15
+ */
16
+ export declare function plistHasStaleNodePath(plistContent: string): boolean;
package/dist/cli/plist.js CHANGED
@@ -22,7 +22,12 @@ export function isContainerRuntime() {
22
22
  return true;
23
23
  return false;
24
24
  }
25
- export function generatePlist(nodePath, pollerPath, envVars) {
25
+ // `/usr/bin/env` is part of macOS base install and resolves `node` from
26
+ // EnvironmentVariables.PATH. This avoids hardcoding a Node binary path that
27
+ // can disappear after `brew upgrade node` (e.g. /opt/homebrew/Cellar/node/<version>/bin/node).
28
+ const ENV_BIN = '/usr/bin/env';
29
+ const NODE_BIN = 'node';
30
+ export function generatePlist(pollerPath, envVars) {
26
31
  let envSection = '';
27
32
  if (envVars && Object.keys(envVars).length > 0) {
28
33
  const entries = Object.entries(envVars)
@@ -43,7 +48,8 @@ ${entries}
43
48
 
44
49
  <key>ProgramArguments</key>
45
50
  <array>
46
- <string>${escapeXml(nodePath)}</string>
51
+ <string>${ENV_BIN}</string>
52
+ <string>${NODE_BIN}</string>
47
53
  <string>${escapeXml(pollerPath)}</string>
48
54
  </array>
49
55
 
@@ -68,6 +74,14 @@ ${entries}
68
74
  </plist>
69
75
  `;
70
76
  }
77
+ /**
78
+ * Heuristic: does this plist content reference a Node binary path that can
79
+ * disappear on `brew upgrade node`? The legacy installer wrote
80
+ * /opt/homebrew/Cellar/node/<version>/bin/node directly into ProgramArguments.
81
+ */
82
+ export function plistHasStaleNodePath(plistContent) {
83
+ return /\/Cellar\/node\/[^/<]+\/bin\/node/.test(plistContent);
84
+ }
71
85
  function escapeXml(str) {
72
86
  return str
73
87
  .replace(/&/g, '&amp;')
@@ -1,6 +1,50 @@
1
1
  import * as fs from 'fs';
2
2
  import { execSync } from 'child_process';
3
- import { generatePlist, PLIST_PATH, SERVICE_LABEL } from './plist.js';
3
+ import { generatePlist, plistHasStaleNodePath, PLIST_PATH, SERVICE_LABEL } from './plist.js';
4
+ function parseEnvVars(plistContent) {
5
+ const envVars = {};
6
+ const envMatch = plistContent.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
7
+ if (envMatch?.[1]) {
8
+ const pairs = envMatch[1].matchAll(/<key>(.*?)<\/key>\s*<string>(.*?)<\/string>/g);
9
+ for (const pair of pairs) {
10
+ envVars[pair[1]] = pair[2];
11
+ }
12
+ }
13
+ return envVars;
14
+ }
15
+ function resolveGlobalPollerPath() {
16
+ try {
17
+ const binPath = execSync('which poller', { encoding: 'utf-8' }).trim();
18
+ if (binPath)
19
+ return fs.realpathSync(binPath);
20
+ }
21
+ catch {
22
+ // Not in PATH
23
+ }
24
+ return null;
25
+ }
26
+ function regeneratePlist(reason) {
27
+ const plistContent = fs.readFileSync(PLIST_PATH, 'utf-8');
28
+ console.log(`\n${reason}, regenerating plist...`);
29
+ const globalPollerPath = resolveGlobalPollerPath();
30
+ if (!globalPollerPath) {
31
+ console.error(' Could not resolve global poller binary. Run: npm install -g @teamvibe/poller');
32
+ return false;
33
+ }
34
+ const envVars = parseEnvVars(plistContent);
35
+ // Ensure PATH covers common Node install locations so /usr/bin/env node resolves.
36
+ if (!envVars['PATH']) {
37
+ envVars['PATH'] = '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin';
38
+ }
39
+ const newPlist = generatePlist(globalPollerPath, envVars);
40
+ try {
41
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' });
42
+ }
43
+ catch { /* ignore */ }
44
+ fs.writeFileSync(PLIST_PATH, newPlist);
45
+ console.log(` Updated plist; poller binary: ${globalPollerPath}`);
46
+ return true;
47
+ }
4
48
  export function update() {
5
49
  console.log('TeamVibe Poller - Update\n');
6
50
  console.log('Updating @teamvibe/poller...');
@@ -11,66 +55,30 @@ export function update() {
11
55
  console.error('Failed to update. Try running: npm install -g @teamvibe/poller@latest');
12
56
  process.exit(1);
13
57
  }
14
- // Fix plist if it points to a stale npx cache path
15
- if (process.platform === 'darwin' && fs.existsSync(PLIST_PATH)) {
16
- const plistContent = fs.readFileSync(PLIST_PATH, 'utf-8');
17
- if (plistContent.includes('/_npx/')) {
18
- console.log('\nDetected stale npx path in service plist, fixing...');
19
- // Resolve the globally installed poller path
20
- let globalPollerPath = '';
21
- try {
22
- const binPath = execSync('which poller', { encoding: 'utf-8' }).trim();
23
- if (binPath) {
24
- globalPollerPath = fs.realpathSync(binPath);
25
- }
26
- }
27
- catch {
28
- // Fall back to current process
29
- }
30
- if (globalPollerPath) {
31
- // Extract existing env vars from plist
32
- const envVars = {};
33
- const envMatch = plistContent.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
34
- if (envMatch) {
35
- const pairs = envMatch[1].matchAll(/<key>(.*?)<\/key>\s*<string>(.*?)<\/string>/g);
36
- for (const pair of pairs) {
37
- envVars[pair[1]] = pair[2];
38
- }
39
- }
40
- const nodePath = process.execPath;
41
- const newPlist = generatePlist(nodePath, globalPollerPath, envVars);
42
- // Unload, write, reload
43
- try {
44
- execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' });
45
- }
46
- catch { /* ignore */ }
47
- fs.writeFileSync(PLIST_PATH, newPlist);
48
- console.log(`Updated plist to use: ${globalPollerPath}`);
49
- try {
50
- execSync(`launchctl load "${PLIST_PATH}"`);
51
- console.log('Service restarted.');
52
- }
53
- catch (error) {
54
- console.error(`Failed to restart: ${error instanceof Error ? error.message : error}`);
55
- }
56
- return;
57
- }
58
- }
59
- // Normal restart (no npx fix needed)
60
- console.log(`\nRestarting service '${SERVICE_LABEL}'...`);
61
- try {
62
- execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' });
63
- }
64
- catch { /* ignore */ }
65
- try {
66
- execSync(`launchctl load "${PLIST_PATH}"`);
67
- console.log('Service restarted.');
68
- }
69
- catch (error) {
70
- console.error(`Failed to restart: ${error instanceof Error ? error.message : error}`);
71
- }
72
- }
73
- else {
58
+ if (process.platform !== 'darwin' || !fs.existsSync(PLIST_PATH)) {
74
59
  console.log('\nUpdate complete. Restart the poller to use the new version.');
60
+ return;
61
+ }
62
+ const plistContent = fs.readFileSync(PLIST_PATH, 'utf-8');
63
+ const hasNpxPath = plistContent.includes('/_npx/');
64
+ const hasStaleNode = plistHasStaleNodePath(plistContent);
65
+ if (hasNpxPath || hasStaleNode) {
66
+ const reason = hasNpxPath
67
+ ? 'Detected stale npx path in service plist'
68
+ : 'Detected hardcoded brew Cellar Node path in service plist (will break on `brew upgrade node`)';
69
+ if (!regeneratePlist(reason))
70
+ return;
71
+ }
72
+ console.log(`\nRestarting service '${SERVICE_LABEL}'...`);
73
+ try {
74
+ execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' });
75
+ }
76
+ catch { /* ignore */ }
77
+ try {
78
+ execSync(`launchctl load "${PLIST_PATH}"`);
79
+ console.log('Service restarted.');
80
+ }
81
+ catch (error) {
82
+ console.error(`Failed to restart: ${error instanceof Error ? error.message : error}`);
75
83
  }
76
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {