2ndbrain 2026.1.32 → 2026.1.33

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.
@@ -11,7 +11,10 @@
11
11
  "WebFetch(domain:latenode.com)",
12
12
  "Bash(claude:*)",
13
13
  "Bash(printf:*)",
14
- "Bash(ls -la \"c:\\\\dev\\\\fingerskier\\\\agent\\\\2ndbrain\\\\db\\\\migrations\"\" 2>/dev/null || echo \"No migrations directory \")"
14
+ "Bash(ls -la \"c:\\\\dev\\\\fingerskier\\\\agent\\\\2ndbrain\\\\db\\\\migrations\"\" 2>/dev/null || echo \"No migrations directory \")",
15
+ "Bash(node:*)",
16
+ "Bash(ls -la \"c:\\\\dev\\\\fingerskier\\\\agent\\\\2ndbrain\\\\src\\\\web\"\" 2>/dev/null || echo \"web directory not found \")",
17
+ "Bash(ls -la \"c:\\\\dev\\\\fingerskier\\\\agent\\\\2ndbrain\\\\db\"\" 2>/dev/null || echo \"Directory not accessible \")"
15
18
  ]
16
19
  }
17
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2ndbrain",
3
- "version": "2026.1.32",
3
+ "version": "2026.1.33",
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/config.js CHANGED
@@ -60,6 +60,19 @@ const config = {
60
60
  EMBEDDING_BASE_URL: env.EMBEDDING_BASE_URL || '',
61
61
  };
62
62
 
63
+ // Ensure DATABASE_URL includes a database name (default: 2ndbrain)
64
+ if (config.DATABASE_URL) {
65
+ try {
66
+ const url = new URL(config.DATABASE_URL);
67
+ if (!url.pathname || url.pathname === '/') {
68
+ url.pathname = '/2ndbrain';
69
+ config.DATABASE_URL = url.toString();
70
+ }
71
+ } catch {
72
+ // leave as-is if URL parsing fails
73
+ }
74
+ }
75
+
63
76
  const REQUIRED_VARS = ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_ALLOWED_USERS', 'DATABASE_URL'];
64
77
 
65
78
  /**
package/src/db/pool.js CHANGED
@@ -12,6 +12,40 @@ pool.on('error', (err) => {
12
12
  console.error(`[${new Date().toISOString()}] [error] [db/pool] Unexpected idle client error:`, err.message);
13
13
  });
14
14
 
15
+ /**
16
+ * Ensure the target database exists, creating it if necessary.
17
+ * Connects to the 'postgres' maintenance database to check/create.
18
+ */
19
+ async function ensureDatabase() {
20
+ try {
21
+ const url = new URL(config.DATABASE_URL);
22
+ const dbName = url.pathname.slice(1); // strip leading '/'
23
+ if (!dbName) return;
24
+
25
+ // Build a maintenance URL pointing at the 'postgres' system database
26
+ const maintenanceUrl = new URL(config.DATABASE_URL);
27
+ maintenanceUrl.pathname = '/postgres';
28
+
29
+ const client = new pg.Client({ connectionString: maintenanceUrl.toString() });
30
+ await client.connect();
31
+
32
+ const result = await client.query(
33
+ 'SELECT 1 FROM pg_database WHERE datname = $1',
34
+ [dbName],
35
+ );
36
+
37
+ if (result.rows.length === 0) {
38
+ // Use double-quoted identifier to handle special characters in name
39
+ await client.query(`CREATE DATABASE "${dbName}"`);
40
+ console.log(`[${new Date().toISOString()}] [info] [db/pool] Created database "${dbName}".`);
41
+ }
42
+
43
+ await client.end();
44
+ } catch (err) {
45
+ console.error(`[${new Date().toISOString()}] [error] [db/pool] ensureDatabase failed: ${err.message}`);
46
+ }
47
+ }
48
+
15
49
  /**
16
50
  * Execute a parameterized SQL query against the pool.
17
51
  * @param {string} text - SQL query string
@@ -29,5 +63,5 @@ async function close() {
29
63
  await pool.end();
30
64
  }
31
65
 
32
- export { pool, query, close };
66
+ export { pool, query, close, ensureDatabase };
33
67
  export default pool;
package/src/index.js CHANGED
@@ -20,7 +20,7 @@ import path from 'node:path';
20
20
  import fs from 'node:fs';
21
21
 
22
22
  import { config, validateConfig, isFirstRun, PROJECT_ROOT } from './config.js';
23
- import { pool, query, close as closeDb } from './db/pool.js';
23
+ import { pool, query, close as closeDb, ensureDatabase } from './db/pool.js';
24
24
  import { migrate } from './db/migrate.js';
25
25
  import logger from './logging.js';
26
26
  import { createRateLimiters } from './rate-limiter.js';
@@ -329,6 +329,7 @@ async function main() {
329
329
  let dbReady = false;
330
330
  if (config.DATABASE_URL) {
331
331
  try {
332
+ await ensureDatabase();
332
333
  await pool.query('SELECT 1');
333
334
  logger.info('startup', 'Database connection established.');
334
335
  dbReady = true;
package/src/web/server.js CHANGED
@@ -187,8 +187,10 @@ class WebServer {
187
187
  'SELECT COUNT(*)::int AS count FROM conversation_messages',
188
188
  );
189
189
  data.messageCount = countRes.rows[0]?.count ?? 0;
190
- } catch {
190
+ } catch (err) {
191
191
  data.dbAvailable = false;
192
+ data.dbError = diagnosePgError(err);
193
+ this._logger.error('web', `Dashboard DB error: ${err.message} (code=${err.code || 'none'})`);
192
194
  }
193
195
 
194
196
  if (data.dbAvailable) {
@@ -299,14 +301,19 @@ class WebServer {
299
301
 
300
302
  try {
301
303
  await this._db.query('SELECT 1');
302
- } catch {
303
- health.components.database = 'error';
304
+ } catch (err) {
305
+ health.components.database = {
306
+ status: 'error',
307
+ error: err.message || 'Unknown error',
308
+ code: err.code || undefined,
309
+ };
304
310
  }
305
311
 
306
312
  // Derive overall status from component states
307
313
  const states = Object.values(health.components);
308
- if (states.includes('error')) {
309
- health.status = states.every((s) => s === 'error') ? 'error' : 'degraded';
314
+ const isErr = (s) => s === 'error' || (typeof s === 'object' && s.status === 'error');
315
+ if (states.some(isErr)) {
316
+ health.status = states.every(isErr) ? 'error' : 'degraded';
310
317
  }
311
318
 
312
319
  const httpStatus = health.status === 'error' ? 503 : 200;
@@ -351,7 +358,7 @@ class WebServer {
351
358
 
352
359
  const tablesResult = await this._db.query(`
353
360
  SELECT
354
- relname AS name,
361
+ c.relname AS name,
355
362
  n_live_tup AS row_count,
356
363
  pg_size_pretty(pg_total_relation_size(c.oid)) AS size
357
364
  FROM pg_class c
@@ -372,7 +379,9 @@ class WebServer {
372
379
  data.dbSize = sizeResult.rows[0]?.size || '';
373
380
  } catch (err) {
374
381
  data.dbAvailable = false;
375
- this._logger.error('web', `Database page error: ${err.message}`);
382
+ data.dbError = diagnosePgError(err);
383
+ data.dbUrl = maskDatabaseUrl(this._config.DATABASE_URL);
384
+ this._logger.error('web', `Database page error: ${err.message} (code=${err.code || 'none'})`);
376
385
  }
377
386
 
378
387
  res.send(databaseHTML(data));
@@ -499,6 +508,83 @@ function maskValue(value) {
499
508
  return s.slice(0, 4) + '*'.repeat(Math.min(s.length - 8, 20)) + s.slice(-4);
500
509
  }
501
510
 
511
+ /** Mask the password in a PostgreSQL connection URL for safe display. */
512
+ function maskDatabaseUrl(url) {
513
+ if (!url) return '(not set)';
514
+ const schemeEnd = url.indexOf('://');
515
+ const lastAt = url.lastIndexOf('@');
516
+ if (schemeEnd >= 0 && lastAt > schemeEnd) {
517
+ const userInfo = url.slice(schemeEnd + 3, lastAt);
518
+ const colonPos = userInfo.indexOf(':');
519
+ if (colonPos >= 0) {
520
+ return url.slice(0, schemeEnd + 3 + colonPos + 1) + '****' + url.slice(lastAt);
521
+ }
522
+ }
523
+ return maskValue(url);
524
+ }
525
+
526
+ /**
527
+ * Extract diagnostic information from a pg / Node.js connection error.
528
+ * Returns { message, code, detail, hint, diagnosis }.
529
+ */
530
+ function diagnosePgError(err) {
531
+ const info = {
532
+ message: err.message || 'Unknown error',
533
+ code: err.code || '',
534
+ detail: err.detail || '',
535
+ hint: err.hint || '',
536
+ diagnosis: '',
537
+ };
538
+
539
+ switch (err.code) {
540
+ case 'ECONNREFUSED':
541
+ info.diagnosis = 'Cannot connect to the database server. Verify the host and port are correct and that PostgreSQL is running.';
542
+ break;
543
+ case 'ENOTFOUND':
544
+ info.diagnosis = 'DNS lookup failed. The database hostname could not be resolved. Check the host in your DATABASE_URL.';
545
+ break;
546
+ case 'ETIMEDOUT':
547
+ info.diagnosis = 'Connection timed out. The database server may be unreachable or behind a firewall.';
548
+ break;
549
+ case 'ECONNRESET':
550
+ info.diagnosis = 'Connection was reset by the server. This may indicate a network issue or server restart.';
551
+ break;
552
+ default:
553
+ break;
554
+ }
555
+
556
+ if (!info.diagnosis && typeof err.code === 'string' && err.code.length === 5) {
557
+ const cls = err.code.substring(0, 2);
558
+ switch (cls) {
559
+ case '08':
560
+ info.diagnosis = 'Connection exception. The database server rejected or dropped the connection.';
561
+ break;
562
+ case '28':
563
+ info.diagnosis = 'Authentication failed. Check the username and password in your DATABASE_URL.';
564
+ break;
565
+ case '3D':
566
+ info.diagnosis = 'The specified database does not exist. Verify the database name in your DATABASE_URL.';
567
+ break;
568
+ case '53':
569
+ info.diagnosis = 'The database server has insufficient resources (too many connections, out of memory, or disk full).';
570
+ break;
571
+ case '57':
572
+ info.diagnosis = 'The database server is shutting down or not accepting connections.';
573
+ break;
574
+ default:
575
+ break;
576
+ }
577
+ }
578
+
579
+ if (!info.diagnosis && err.message) {
580
+ if (/ssl/i.test(err.message) || /certificate/i.test(err.message)) {
581
+ info.diagnosis = 'SSL/TLS error. Check your SSL configuration or try adding ?sslmode=require or ?sslmode=no-verify to your DATABASE_URL.';
582
+ }
583
+ }
584
+
585
+ return info;
586
+ }
587
+
502
588
  // ---------------------------------------------------------------------------
503
589
  // HTML template functions
504
590
  // ---------------------------------------------------------------------------
@@ -670,6 +756,28 @@ function layoutHTML(title, content) {
670
756
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
671
757
  margin-bottom: 0.25rem;
672
758
  }
759
+ .secret-input-wrapper {
760
+ position: relative;
761
+ }
762
+ .secret-input-wrapper input {
763
+ padding-right: 2.5rem;
764
+ }
765
+ .secret-toggle {
766
+ position: absolute;
767
+ right: 0.4rem;
768
+ top: 50%;
769
+ transform: translateY(-50%);
770
+ background: none;
771
+ border: none;
772
+ color: #8b949e;
773
+ cursor: pointer;
774
+ padding: 0.2rem;
775
+ font-size: 0.85rem;
776
+ line-height: 1;
777
+ }
778
+ .secret-toggle:hover {
779
+ color: #c9d1d9;
780
+ }
673
781
  .checkbox-group {
674
782
  display: flex;
675
783
  align-items: center;
@@ -828,9 +936,17 @@ function dashboardHTML(data) {
828
936
  </div>`;
829
937
 
830
938
  // -- DB warning ----------------------------------------------------------
831
- const dbWarn = data.dbAvailable
832
- ? ''
833
- : '<div class="db-warn">Database is unavailable. Dashboard data may be incomplete.</div>';
939
+ let dbWarn = '';
940
+ if (!data.dbAvailable) {
941
+ const e = data.dbError || {};
942
+ dbWarn = '<div class="db-warn">Database is unavailable. Dashboard data may be incomplete.';
943
+ if (e.diagnosis) {
944
+ dbWarn += `<br><span style="font-size:0.82rem;">${esc(e.diagnosis)}</span>`;
945
+ } else if (e.message) {
946
+ dbWarn += `<br><span style="font-size:0.82rem;">${esc(e.message)}</span>`;
947
+ }
948
+ dbWarn += '</div>';
949
+ }
834
950
 
835
951
  // -- Recent messages -----------------------------------------------------
836
952
  let messagesSection;
@@ -900,15 +1016,23 @@ function settingsHTML(config, message) {
900
1016
  let inputArea;
901
1017
 
902
1018
  if (field.secret) {
1019
+ const maskedDisplay = field.key === 'DATABASE_URL'
1020
+ ? maskDatabaseUrl(String(value))
1021
+ : maskValue(String(value));
903
1022
  const status = value
904
- ? `Current: ${esc(maskValue(String(value)))}`
1023
+ ? `Current: ${esc(maskedDisplay)}`
905
1024
  : '(not set)';
906
1025
  inputArea = `
907
1026
  <div>
908
1027
  <div class="secret-current">${status}</div>
909
- <input type="password" name="${esc(field.key)}"
910
- value="" placeholder="Enter new value to change"
911
- autocomplete="off" />
1028
+ <div class="secret-input-wrapper">
1029
+ <input type="password" id="input-${esc(field.key)}" name="${esc(field.key)}"
1030
+ value="" placeholder="Enter new value to change"
1031
+ autocomplete="off" />
1032
+ <button type="button" class="secret-toggle"
1033
+ onclick="const inp=document.getElementById('input-${esc(field.key)}');const show=inp.type==='password';inp.type=show?'text':'password';this.textContent=show?'&#x25C9;':'&#x25CE;';"
1034
+ title="Toggle visibility">&#x25CE;</button>
1035
+ </div>
912
1036
  </div>`;
913
1037
  } else if (field.type === 'boolean') {
914
1038
  const checked = String(value).toLowerCase() === 'true' ? ' checked' : '';
@@ -991,9 +1115,29 @@ function databaseHTML(data) {
991
1115
  ? `<div class="alert alert-${esc(data.message.type)}">${esc(data.message.text)}</div>`
992
1116
  : '';
993
1117
 
994
- const dbWarn = data.dbAvailable
995
- ? ''
996
- : '<div class="db-warn">Database is unavailable. Cannot retrieve database information.</div>';
1118
+ let dbWarn = '';
1119
+ if (!data.dbAvailable) {
1120
+ const e = data.dbError || {};
1121
+ dbWarn = '<div class="db-warn">';
1122
+ dbWarn += '<strong>Database is unavailable.</strong> Cannot retrieve database information.';
1123
+ if (e.diagnosis) {
1124
+ dbWarn += `<br><br><strong>Diagnosis:</strong> ${esc(e.diagnosis)}`;
1125
+ }
1126
+ dbWarn += `<br><br><strong>Error:</strong> ${esc(e.message || 'Unknown')}`;
1127
+ if (e.code) {
1128
+ dbWarn += ` <span class="muted">(code: ${esc(e.code)})</span>`;
1129
+ }
1130
+ if (e.detail) {
1131
+ dbWarn += `<br><strong>Detail:</strong> ${esc(e.detail)}`;
1132
+ }
1133
+ if (e.hint) {
1134
+ dbWarn += `<br><strong>Hint:</strong> ${esc(e.hint)}`;
1135
+ }
1136
+ if (data.dbUrl) {
1137
+ dbWarn += `<br><br><strong>Connection URL:</strong> <code style="font-size:0.82rem;">${esc(data.dbUrl)}</code>`;
1138
+ }
1139
+ dbWarn += '</div>';
1140
+ }
997
1141
 
998
1142
  const stats = data.dbAvailable
999
1143
  ? `