2ndbrain 2026.2.2 → 2026.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/package.json +1 -1
- package/src/claude/bridge.js +80 -12
- package/src/index.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2ndbrain",
|
|
3
|
-
"version": "2026.2.
|
|
3
|
+
"version": "2026.2.3",
|
|
4
4
|
"description": "Always-on Node.js service bridging Telegram messaging to Claude AI with knowledge graph, journal, project management, and semantic search.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/claude/bridge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -42,6 +43,11 @@ class ClaudeBridge extends EventEmitter {
|
|
|
42
43
|
const startTime = Date.now();
|
|
43
44
|
const args = this._buildArgs(sessionId, systemPrompt);
|
|
44
45
|
|
|
46
|
+
// Pre-spawn MCP config validation (new sessions only)
|
|
47
|
+
if (!sessionId) {
|
|
48
|
+
this._validateMcpConfig();
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
this.logger.info('claude', `Spawning: claude ${args.join(' ')}`);
|
|
46
52
|
|
|
47
53
|
const runtimeDir = path.join(this.config.DATA_DIR, 'claude-runtime');
|
|
@@ -54,6 +60,7 @@ class ClaudeBridge extends EventEmitter {
|
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
this.activeProcess = proc;
|
|
63
|
+
this.logger.info('claude', `Subprocess spawned (pid=${proc.pid}, cwd=${runtimeDir})`);
|
|
57
64
|
|
|
58
65
|
let stdoutBuffer = '';
|
|
59
66
|
let stderrBuffer = '';
|
|
@@ -63,17 +70,38 @@ class ClaudeBridge extends EventEmitter {
|
|
|
63
70
|
let timedOut = false;
|
|
64
71
|
let receivedFirstOutput = false;
|
|
65
72
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// Progressive startup watchdog: warn at escalating intervals if no stdout arrives.
|
|
74
|
+
// Delays between checks: 10s, 10s, 10s, 15s, 15s → cumulative: 10, 20, 30, 45, 60s, then every 30s.
|
|
75
|
+
const WATCHDOG_DELAYS = [10_000, 10_000, 10_000, 15_000, 15_000];
|
|
76
|
+
let watchdogStep = 0;
|
|
77
|
+
let startupTimeout = null;
|
|
78
|
+
|
|
79
|
+
const scheduleWatchdog = () => {
|
|
80
|
+
const delay = watchdogStep < WATCHDOG_DELAYS.length
|
|
81
|
+
? WATCHDOG_DELAYS[watchdogStep]
|
|
82
|
+
: 30_000;
|
|
83
|
+
|
|
84
|
+
startupTimeout = setTimeout(() => {
|
|
85
|
+
if (receivedFirstOutput) return;
|
|
86
|
+
|
|
87
|
+
const elapsed = Date.now() - startTime;
|
|
88
|
+
const level = elapsed >= 30_000 ? 'warn' : 'info';
|
|
89
|
+
const stderrTail = stderrBuffer.trim()
|
|
90
|
+
? stderrBuffer.trim().slice(-500)
|
|
91
|
+
: '(empty)';
|
|
92
|
+
|
|
93
|
+
this.logger[level](
|
|
70
94
|
'claude',
|
|
71
|
-
|
|
72
|
-
'
|
|
73
|
-
`stderr
|
|
95
|
+
`No stdout after ${Math.round(elapsed / 1000)}s (pid=${proc.pid}) -- ` +
|
|
96
|
+
'possible causes: MCP server init (npx download), API queueing, network issue. ' +
|
|
97
|
+
`stderr tail: ${stderrTail}`,
|
|
74
98
|
);
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
|
|
100
|
+
watchdogStep++;
|
|
101
|
+
scheduleWatchdog();
|
|
102
|
+
}, delay);
|
|
103
|
+
};
|
|
104
|
+
scheduleWatchdog();
|
|
77
105
|
|
|
78
106
|
// Set up the timeout guard
|
|
79
107
|
const timeout = setTimeout(() => {
|
|
@@ -85,13 +113,14 @@ class ClaudeBridge extends EventEmitter {
|
|
|
85
113
|
// Pipe the user message via stdin and close it
|
|
86
114
|
proc.stdin.write(message);
|
|
87
115
|
proc.stdin.end();
|
|
116
|
+
this.logger.info('claude', `Message piped to stdin and closed (${message.length} chars)`);
|
|
88
117
|
|
|
89
118
|
// Collect and parse stdout stream-json chunks
|
|
90
119
|
proc.stdout.on('data', (chunk) => {
|
|
91
120
|
if (!receivedFirstOutput) {
|
|
92
121
|
receivedFirstOutput = true;
|
|
93
122
|
clearTimeout(startupTimeout);
|
|
94
|
-
this.logger.
|
|
123
|
+
this.logger.info('claude', `First stdout received after ${Date.now() - startTime}ms (pid=${proc.pid})`);
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
stdoutBuffer += chunk.toString();
|
|
@@ -119,19 +148,27 @@ class ClaudeBridge extends EventEmitter {
|
|
|
119
148
|
}
|
|
120
149
|
});
|
|
121
150
|
|
|
122
|
-
// Monitor stderr for errors (log in real time for diagnostics)
|
|
151
|
+
// Monitor stderr for errors (log in real time for diagnostics).
|
|
152
|
+
// During startup (before first stdout), promote to INFO so MCP init
|
|
153
|
+
// messages (npx downloads, connection errors) are visible at default log level.
|
|
123
154
|
proc.stderr.on('data', (chunk) => {
|
|
124
155
|
const text = chunk.toString();
|
|
125
156
|
stderrBuffer += text;
|
|
126
157
|
for (const line of text.split('\n')) {
|
|
127
158
|
const trimmed = line.trim();
|
|
128
159
|
if (trimmed) {
|
|
129
|
-
|
|
160
|
+
if (!receivedFirstOutput) {
|
|
161
|
+
this.logger.info('claude-stderr', trimmed);
|
|
162
|
+
} else {
|
|
163
|
+
this.logger.debug('claude-stderr', trimmed);
|
|
164
|
+
}
|
|
130
165
|
}
|
|
131
166
|
}
|
|
132
167
|
});
|
|
133
168
|
|
|
134
169
|
proc.on('close', (code) => {
|
|
170
|
+
const elapsed = Date.now() - startTime;
|
|
171
|
+
this.logger.info('claude', `Subprocess exited (pid=${proc.pid}, code=${code}, elapsed=${elapsed}ms)`);
|
|
135
172
|
clearTimeout(timeout);
|
|
136
173
|
clearTimeout(startupTimeout);
|
|
137
174
|
this.activeProcess = null;
|
|
@@ -234,6 +271,37 @@ class ClaudeBridge extends EventEmitter {
|
|
|
234
271
|
return args;
|
|
235
272
|
}
|
|
236
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Validate MCP config file before spawning. Logs diagnostics for common
|
|
276
|
+
* issues (missing file, npx-based servers that may download on first run).
|
|
277
|
+
* Does NOT block the spawn -- purely diagnostic.
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
_validateMcpConfig() {
|
|
281
|
+
const configPath = this.config.MCP_CONFIG_PATH;
|
|
282
|
+
if (!configPath) {
|
|
283
|
+
this.logger.info('claude', 'No MCP_CONFIG_PATH configured; skipping MCP config validation');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
289
|
+
const parsed = JSON.parse(raw);
|
|
290
|
+
const servers = parsed.mcpServers || {};
|
|
291
|
+
const serverNames = Object.keys(servers);
|
|
292
|
+
|
|
293
|
+
this.logger.info('claude', `MCP config: ${configPath} (${serverNames.length} server(s): ${serverNames.join(', ')})`);
|
|
294
|
+
|
|
295
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
296
|
+
if (server.command === 'npx') {
|
|
297
|
+
this.logger.info('claude', `MCP server "${name}" uses npx -- first-run download may cause startup delay`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
this.logger.warn('claude', `MCP config validation failed (${configPath}): ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
237
305
|
/**
|
|
238
306
|
* Handle a single parsed stream-json chunk.
|
|
239
307
|
*
|
package/src/index.js
CHANGED
|
@@ -409,7 +409,9 @@ async function main() {
|
|
|
409
409
|
mcpConfigPath = generateMcpConfig(config);
|
|
410
410
|
// Override MCP_CONFIG_PATH to use the generated one
|
|
411
411
|
config.MCP_CONFIG_PATH = mcpConfigPath;
|
|
412
|
-
|
|
412
|
+
const mcpContent = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
|
|
413
|
+
const mcpServerNames = Object.keys(mcpContent.mcpServers || {});
|
|
414
|
+
logger.info('startup', `MCP config written to ${mcpConfigPath} (servers: ${mcpServerNames.join(', ')})`);
|
|
413
415
|
} catch (err) {
|
|
414
416
|
logger.error('startup', `Failed to generate MCP config: ${err.message}`);
|
|
415
417
|
}
|