@matimo/postgres 0.1.0-alpha.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tallclub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,440 @@
1
+ # @matimo/postgres — Postgres Tools for Matimo
2
+
3
+ Secure, approval-aware SQL execution for Postgres databases with built-in protection for destructive operations.
4
+
5
+ ## Features
6
+
7
+ ✨ **Core Capabilities**
8
+ - Execute any SQL query (SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, etc.)
9
+ - Automatic destructive operation detection
10
+ - Approval workflow for dangerous operations
11
+ - Flexible connection options (URL or individual parameters)
12
+ - Full parameter binding support
13
+ - Sequential discovery pattern for safe database exploration
14
+
15
+ 🔒 **Safety & Security**
16
+ - Detects destructive SQL: `CREATE`, `DROP`, `ALTER`, `TRUNCATE`, `DELETE`, `UPDATE`, `INSERT`
17
+ - Requires explicit approval before executing destructive operations
18
+ - Support for both interactive approval (CLI) and automated approval (CI/CD)
19
+ - Never exposes database credentials to external systems
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pnpm add @matimo/postgres
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Quick Start
32
+
33
+ ### 1. Set Up Database Connection
34
+
35
+ Choose one approach:
36
+
37
+ **Option A: Connection String**
38
+ ```bash
39
+ export MATIMO_POSTGRES_URL="postgresql://user:password@localhost:5432/dbname"
40
+ ```
41
+
42
+ **Option B: Individual Parameters**
43
+ ```bash
44
+ export MATIMO_POSTGRES_HOST="localhost"
45
+ export MATIMO_POSTGRES_PORT="5432"
46
+ export MATIMO_POSTGRES_USER="user"
47
+ export MATIMO_POSTGRES_PASSWORD="password"
48
+ export MATIMO_POSTGRES_DB="dbname"
49
+ ```
50
+
51
+ ### 2. Use in Code
52
+
53
+ ```typescript
54
+ import { MatimoInstance } from '@matimo/core';
55
+
56
+ // Initialize with auto-discovery
57
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
58
+
59
+ // Execute a safe SELECT query (no approval needed)
60
+ const result = await matimo.execute('postgres-execute-sql', {
61
+ sql: 'SELECT * FROM users WHERE id = $1;',
62
+ params: [42]
63
+ });
64
+
65
+ console.log(result.rows);
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Available Tools
71
+
72
+ ### `postgres-execute-sql`
73
+
74
+ Execute SQL queries against a Postgres database with automatic approval for destructive operations.
75
+
76
+ #### Parameters
77
+
78
+ | Parameter | Type | Required | Description |
79
+ |-----------|------|----------|-------------|
80
+ | `sql` | `string` | ✅ | SQL query to execute. Use `$1`, `$2`, etc. for parameterized queries. |
81
+ | `params` | `unknown[]` \| `unknown[][]` | ❌ | Query parameters for parameterized SQL. |
82
+
83
+ #### Returns
84
+
85
+ ```typescript
86
+ {
87
+ success: boolean;
88
+ rows?: any[]; // Returned rows (for SELECT queries)
89
+ rowCount?: number; // Affected rows (for INSERT/UPDATE/DELETE)
90
+ command?: string; // SQL command executed
91
+ error?: string; // Error message (if failed)
92
+ }
93
+ ```
94
+
95
+ #### Examples
96
+
97
+ **Safe SELECT (no approval needed)**
98
+ ```typescript
99
+ const result = await matimo.execute('postgres-execute-sql', {
100
+ sql: 'SELECT id, name FROM users LIMIT 10;'
101
+ });
102
+ ```
103
+
104
+ **Parameterized Query (prevents SQL injection)**
105
+ ```typescript
106
+ const result = await matimo.execute('postgres-execute-sql', {
107
+ sql: 'SELECT * FROM users WHERE name = $1 AND age > $2;',
108
+ params: ['Alice', 25]
109
+ });
110
+ ```
111
+
112
+ **Destructive Operation (requires approval)**
113
+ ```typescript
114
+ // This will trigger approval workflow
115
+ const result = await matimo.execute('postgres-execute-sql', {
116
+ sql: 'UPDATE users SET last_login = NOW() WHERE id = $1;',
117
+ params: [42]
118
+ });
119
+ // ⚠️ User will be prompted for approval (unless auto-approved)
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Approval Flow
125
+
126
+ ### Automatic Detection
127
+
128
+ The tool automatically detects destructive operations:
129
+
130
+ | Operation | Detected | Auto-Approved | Requires Approval |
131
+ |-----------|----------|---------------|-------------------|
132
+ | SELECT | ✅ | ✅ Yes | ❌ No |
133
+ | INSERT | ✅ | ✅ Yes| ❌ No|
134
+ | UPDATE | ✅ | ❌ No | ✅ Yes |
135
+ | DELETE | ✅ | ❌ No | ✅ Yes |
136
+ | CREATE | ✅ | ❌ No | ✅ Yes |
137
+ | DROP | ✅ | ❌ No | ✅ Yes |
138
+ | ALTER | ✅ | ❌ No | ✅ Yes |
139
+ | TRUNCATE | ✅ | ❌ No | ✅ Yes |
140
+
141
+ ### Approval Methods
142
+
143
+ #### 1. **Interactive Approval (CLI)**
144
+ For interactive terminal environments:
145
+
146
+ ```typescript
147
+ import { getSQLApprovalManager } from '@matimo/core';
148
+ import * as readline from 'readline';
149
+
150
+ const manager = getSQLApprovalManager();
151
+
152
+ // Set interactive callback
153
+ manager.setApprovalCallback(async (sql: string, mode: string) => {
154
+ const rl = readline.createInterface({
155
+ input: process.stdin,
156
+ output: process.stdout
157
+ });
158
+
159
+ return new Promise(resolve => {
160
+ rl.question(`Approve ${mode} operation: ${sql}? (yes/no): `, answer => {
161
+ rl.close();
162
+ resolve(answer.toLowerCase() === 'yes');
163
+ });
164
+ });
165
+ });
166
+ ```
167
+
168
+ #### 2. **Automatic Approval (CI/CD)**
169
+ For automated environments:
170
+
171
+ ```bash
172
+ # Enable auto-approval for all destructive operations
173
+ export MATIMO_SQL_AUTO_APPROVE=true
174
+ ```
175
+
176
+ #### 3. **Pattern-Based Approval**
177
+ Pre-approve specific patterns:
178
+
179
+ ```bash
180
+ # Approve all DELETE queries and UPDATE queries on users table
181
+ export MATIMO_SQL_APPROVED_PATTERNS="DELETE.*,UPDATE users.*"
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Integration Patterns
187
+
188
+ ### Factory Pattern
189
+
190
+ ```typescript
191
+ import { MatimoInstance } from '@matimo/core';
192
+
193
+ async function main() {
194
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
195
+
196
+ // Step 1: Discover tables
197
+ const tables = await matimo.execute('postgres-execute-sql', {
198
+ sql: `
199
+ SELECT table_name
200
+ FROM information_schema.tables
201
+ WHERE table_schema = 'public'
202
+ ORDER BY table_name;
203
+ `
204
+ });
205
+
206
+ console.log('Tables:', tables.rows?.map(r => r.table_name));
207
+ }
208
+
209
+ main();
210
+ ```
211
+
212
+ ### Decorator Pattern
213
+
214
+ ```typescript
215
+ import { MatimoInstance, setGlobalMatimoInstance, tool } from '@matimo/core';
216
+
217
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
218
+ setGlobalMatimoInstance(matimo);
219
+
220
+ class DatabaseClient {
221
+ @tool('postgres-execute-sql')
222
+ async queryUsers(minAge: number) {
223
+ // Auto-executes via Matimo
224
+ }
225
+
226
+ @tool('postgres-execute-sql')
227
+ async updateUserStatus(userId: number, status: string) {
228
+ // Requires approval (UPDATE operation)
229
+ }
230
+ }
231
+
232
+ const db = new DatabaseClient();
233
+ ```
234
+
235
+ ### LangChain Integration
236
+
237
+ ```typescript
238
+ import { MatimoInstance, convertToolsToLangChain } from '@matimo/core';
239
+ import { ChatOpenAI } from '@langchain/openai';
240
+
241
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
242
+
243
+ // Convert Postgres tool to LangChain format
244
+ const tools = await convertToolsToLangChain(
245
+ [matimo.getTool('postgres-execute-sql')!],
246
+ matimo
247
+ );
248
+
249
+ // LLM can now use SQL execution tool
250
+ const agent = await createAgent({
251
+ model: new ChatOpenAI({ modelName: 'gpt-4o-mini' }),
252
+ tools: tools
253
+ });
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Sequential Discovery Pattern
259
+
260
+ The recommended workflow for safe database exploration:
261
+
262
+ ```
263
+ Step 1: Discover Tables (SELECT - no approval)
264
+
265
+ Step 2: Get Table Counts/Structure (SELECT - no approval)
266
+
267
+ Step 3: Execute Destructive Operations (requires approval)
268
+
269
+ Step 4: Use Discovered Data
270
+ ```
271
+
272
+ Example implementation:
273
+
274
+ ```typescript
275
+ // 1. What tables exist?
276
+ const tables = await matimo.execute('postgres-execute-sql', {
277
+ sql: `SELECT table_name FROM information_schema.tables
278
+ WHERE table_schema = 'public' ORDER BY table_name;`
279
+ });
280
+
281
+ // 2. How much data in each table?
282
+ const counts = await matimo.execute('postgres-execute-sql', {
283
+ sql: `SELECT table_name, (SELECT count(*) FROM X)
284
+ FROM information_schema.tables WHERE table_schema = 'public';`
285
+ });
286
+
287
+ // 3. Now safely operate on discovered tables
288
+ if (tables.rows?.length > 0) {
289
+ const tableName = tables.rows[0].table_name;
290
+
291
+ // This will require approval
292
+ const result = await matimo.execute('postgres-execute-sql', {
293
+ sql: `DELETE FROM ${tableName} WHERE archived = true;`
294
+ });
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Error Handling
301
+
302
+ ```typescript
303
+ import { MatimoError, ErrorCode } from '@matimo/core';
304
+
305
+ try {
306
+ const result = await matimo.execute('postgres-execute-sql', {
307
+ sql: 'SELECT * FROM nonexistent_table;'
308
+ });
309
+ } catch (err) {
310
+ if (err instanceof MatimoError) {
311
+ console.error(`Code: ${err.code}`);
312
+ console.error(`Message: ${err.message}`);
313
+ console.error(`Details:`, err.details);
314
+ }
315
+ }
316
+ ```
317
+
318
+ Common error codes:
319
+ - `INVALID_SCHEMA` — Missing required SQL parameter
320
+ - `EXECUTION_FAILED` — Query execution error (connection, syntax, etc.)
321
+ - `AUTH_FAILED` — Destructive operation not approved
322
+
323
+ ---
324
+
325
+ ## Connection String Format
326
+
327
+ Standard PostgreSQL connection string format:
328
+
329
+ ```
330
+ postgresql://[user[:password]@][host][:port][/database]
331
+ ```
332
+
333
+ **Examples:**
334
+ ```
335
+ postgresql://user:password@localhost:5432/mydb
336
+ postgresql://localhost/mydb # Local with defaults
337
+ postgresql://user@localhost # Without port
338
+ ```
339
+
340
+ ---
341
+
342
+ ## Environment Variables
343
+
344
+ ### Connection
345
+
346
+ | Variable | Required | Example | Description |
347
+ |----------|----------|---------|-------------|
348
+ | `MATIMO_POSTGRES_URL` | One of these | `postgresql://...` | Full connection string |
349
+ | `MATIMO_POSTGRES_HOST` | ✅ (if not URL) | `localhost` | Database host |
350
+ | `MATIMO_POSTGRES_PORT` | ❌ | `5432` | Database port (default: 5432) |
351
+ | `MATIMO_POSTGRES_USER` | ✅ (if not URL) | `postgres` | Database user |
352
+ | `MATIMO_POSTGRES_PASSWORD` | ✅ (if not URL) | `secret` | Database password |
353
+ | `MATIMO_POSTGRES_DB` | ✅ (if not URL) | `mydb` | Database name |
354
+
355
+ ### Approval
356
+
357
+ | Variable | Values | Description |
358
+ |----------|--------|-------------|
359
+ | `MATIMO_SQL_AUTO_APPROVE` | `true` / `false` | Auto-approve all destructive operations (for CI/CD) |
360
+ | `MATIMO_SQL_APPROVED_PATTERNS` | Regex patterns | Comma-separated patterns for pre-approved queries |
361
+
362
+ ---
363
+
364
+ ## Examples
365
+
366
+ See `examples/tools/postgres/` in the Matimo repository:
367
+
368
+ - **`postgres-factory.ts`** — Factory pattern with sequential discovery
369
+ - **`postgres-decorator.ts`** — Class-based decorator pattern
370
+ - **`postgres-langchain.ts`** — AI agent using LangChain (GPT-4o-mini)
371
+ - **`postgres-with-approval.ts`** — Interactive approval workflow
372
+
373
+ Run examples:
374
+ ```bash
375
+ cd examples/tools
376
+
377
+ # Factory pattern
378
+ pnpm postgres:factory
379
+
380
+ # Decorator pattern
381
+ pnpm postgres:decorator
382
+
383
+ # LangChain AI agent
384
+ pnpm postgres:langchain
385
+
386
+ # Interactive approval flow (requires terminal)
387
+ pnpm postgres:approval
388
+ ```
389
+
390
+ ---
391
+
392
+ ## Troubleshooting
393
+
394
+ ### Connection Refused
395
+ **Error:** `connect ECONNREFUSED`
396
+
397
+ **Solution:**
398
+ - Check Postgres is running: `pg_isready -h localhost -p 5432`
399
+ - Verify credentials and host
400
+ - Ensure database exists: `createdb mydb`
401
+
402
+ ### Authentication Failed
403
+ **Error:** `password authentication failed`
404
+
405
+ **Solution:**
406
+ - Check `MATIMO_POSTGRES_USER` and `MATIMO_POSTGRES_PASSWORD`
407
+ - Reset password: `ALTER USER postgres WITH PASSWORD 'newpassword';`
408
+
409
+ ### Approval Required Error
410
+ **Error:** `Destructive SQL requires approval`
411
+
412
+ **Solution:**
413
+ - Set `MATIMO_SQL_AUTO_APPROVE=true` in CI/CD
414
+ - Or use interactive approval: `pnpm postgres:approval`
415
+ - Or pre-approve patterns: `export MATIMO_SQL_APPROVED_PATTERNS="DELETE.*,UPDATE.*"`
416
+
417
+ ### Parameter Binding Error
418
+ **Error:** `bind message supplies X parameters, but prepared statement requires Y`
419
+
420
+ **Solution:**
421
+ - Use placeholders for all parameters: `$1`, `$2`, etc.
422
+ - Match number of `params` to number of placeholders
423
+ - Example: `'WHERE id = $1'` with `params: [42]`
424
+
425
+ ---
426
+
427
+ ## Contributing
428
+
429
+ Found a bug or want to request a feature?
430
+ - [Open an issue](https://github.com/tallclub/matimo/issues)
431
+ - [Start a discussion](https://github.com/tallclub/matimo/discussions)
432
+
433
+ ---
434
+
435
+ ## Part of the Matimo Ecosystem
436
+
437
+ Learn more about Matimo:
438
+ - 📖 [Documentation](https://matimo.dev/docs)
439
+ - 🔗 [GitHub Repository](https://github.com/tallclub/matimo)
440
+ - ⭐ [Star the project](https://github.com/tallclub/matimo)
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@matimo/postgres",
3
+ "version": "0.1.0-alpha.7",
4
+ "description": "Postgres tools for Matimo",
5
+ "type": "module",
6
+ "files": [
7
+ "tools",
8
+ "README.md",
9
+ "definition.yaml"
10
+ ],
11
+ "dependencies": {
12
+ "pg": "^8.18.0",
13
+ "@matimo/core": "0.1.0-alpha.7"
14
+ },
15
+ "devDependencies": {
16
+ "@types/pg": "^8.6.6"
17
+ }
18
+ }
@@ -0,0 +1,59 @@
1
+ name: postgres-execute-sql
2
+ version: '1.0.0'
3
+ description: Execute arbitrary SQL against a Postgres database. Supports both connection string and explicit env var configuration.
4
+
5
+ parameters:
6
+ sql:
7
+ type: string
8
+ description: "SQL statement to execute. Use parameterized queries for safety (e.g. $1, $2)."
9
+ required: true
10
+ params:
11
+ type: array
12
+ description: "Optional array of parameters to pass to the query."
13
+ required: false
14
+ schema:
15
+ type: string
16
+ description: "Optional schema name to search for tables or qualify queries."
17
+ required: false
18
+
19
+ execution:
20
+ type: function
21
+ # The function file path is resolved relative to this definition.yaml
22
+ code: ./execute-sql.ts
23
+ timeout: 30000
24
+
25
+ authentication:
26
+ type: custom
27
+ notes: |
28
+ The tool supports two authentication modes (choose either):
29
+ 1. Connection string - set `MATIMO_POSTGRES_URL` to a full Postgres connection string (recommended).
30
+ 2. Separate env vars - set `MATIMO_POSTGRES_HOST`, `MATIMO_POSTGRES_PORT`, `MATIMO_POSTGRES_USER`, `MATIMO_POSTGRES_PASSWORD`, `MATIMO_POSTGRES_DB`.
31
+ # Consumers should treat secrets as env variables. This custom auth type
32
+ # documents expected environment variables but does not enforce a single
33
+ # auth scheme - the executor reads either `MATIMO_POSTGRES_URL` or the
34
+ # individual env vars at runtime.
35
+
36
+ error_handling:
37
+ retry: 1
38
+ backoff_type: exponential
39
+ initial_delay_ms: 500
40
+
41
+ output_schema:
42
+ type: object
43
+ properties:
44
+ rows:
45
+ type: array
46
+ items:
47
+ type: object
48
+ rowCount:
49
+ type: number
50
+
51
+ examples:
52
+ - name: Simple select
53
+ params:
54
+ sql: "SELECT id, name FROM users WHERE id = $1"
55
+ params: [1]
56
+
57
+ - name: Parameterless query
58
+ params:
59
+ sql: "SELECT count(*) as cnt FROM users"
@@ -0,0 +1,100 @@
1
+ import { Client } from 'pg';
2
+ import { MatimoError, ErrorCode, getSQLApprovalManager } from '@matimo/core';
3
+
4
+ export default async function (input: Record<string, unknown>) {
5
+ const sql = (input.sql as string) || '';
6
+ const params = (input.params as unknown) as unknown[] | undefined;
7
+
8
+ if (!sql || sql.trim().length === 0) {
9
+ throw new MatimoError('Missing SQL statement', ErrorCode.EXECUTION_FAILED, {
10
+ toolName: 'postgres-execute-sql',
11
+ });
12
+ }
13
+
14
+ // Build connection string from either MATIMO_POSTGRES_URL or separate env vars
15
+ const envUrl = process.env.MATIMO_POSTGRES_URL;
16
+ let connectionString: string | undefined = envUrl;
17
+
18
+ // Detect destructive SQL and require approval
19
+ const destructiveRegex = /^\s*(CREATE|DROP|ALTER|TRUNCATE|DELETE|UPDATE)\b/i;
20
+ const isDestructive = destructiveRegex.test(sql);
21
+ if (isDestructive) {
22
+ const manager = getSQLApprovalManager();
23
+ try {
24
+ const ok = await manager.isApproved(sql, 'write');
25
+ if (!ok) {
26
+ throw new MatimoError('Destructive SQL not approved', ErrorCode.EXECUTION_FAILED, {
27
+ toolName: 'postgres-execute-sql',
28
+ hint:
29
+ 'Destructive SQL requires approval. Use getSQLApprovalManager().setApprovalCallback() or set MATIMO_SQL_APPROVED_PATTERNS / MATIMO_SQL_AUTO_APPROVE=true',
30
+ });
31
+ }
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ } catch (e: any) {
34
+ // Re-throw MatimoError or wrap
35
+ if (e instanceof MatimoError) throw e;
36
+ throw new MatimoError('SQL approval check failed', ErrorCode.EXECUTION_FAILED, {
37
+ toolName: 'postgres-execute-sql',
38
+ details: { message: e?.message || String(e) },
39
+ });
40
+ }
41
+ }
42
+
43
+ if (!connectionString) {
44
+ const host = process.env.MATIMO_POSTGRES_HOST;
45
+ const port = process.env.MATIMO_POSTGRES_PORT || '5432';
46
+ const user = process.env.MATIMO_POSTGRES_USER;
47
+ const password = process.env.MATIMO_POSTGRES_PASSWORD;
48
+ const database = process.env.MATIMO_POSTGRES_DB;
49
+
50
+ if (host && user && password && database) {
51
+ // Build a simple connection string. Do not log secrets.
52
+ connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(
53
+ password
54
+ )}@${host}:${port}/${database}`;
55
+ }
56
+ }
57
+
58
+ if (!connectionString) {
59
+ throw new MatimoError(
60
+ 'Postgres connection information not provided. Set MATIMO_POSTGRES_URL or MATIMO_POSTGRES_HOST/PORT/USER/PASSWORD/DB',
61
+ ErrorCode.EXECUTION_FAILED,
62
+ { toolName: 'postgres-execute-sql' }
63
+ );
64
+ }
65
+
66
+ const client = new Client({ connectionString });
67
+ try {
68
+ await client.connect();
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const result = await client.query(sql, (params ?? []) as any);
71
+ return { rows: result.rows, rowCount: result.rowCount };
72
+ } catch (err) {
73
+ // Extract meaningful error message
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ const originalError = (err as any)?.message || String(err);
76
+ const details: Record<string, unknown> = {
77
+ originalMessage: originalError,
78
+ };
79
+ // Check if it's a connection error vs query error
80
+ if (originalError.includes('ECONNREFUSED')) {
81
+ details.hint = 'Connection refused - is Postgres running at the configured host/port?';
82
+ } else if (originalError.includes('role') && originalError.includes('does not exist')) {
83
+ details.hint = 'Database user does not exist - check MATIMO_POSTGRES_USER env var';
84
+ } else if (originalError.includes('database') && originalError.includes('does not exist')) {
85
+ details.hint = 'Database does not exist - check MATIMO_POSTGRES_DB env var';
86
+ }
87
+ // Wrap errors to avoid leaking secrets
88
+ throw new MatimoError(`Postgres query failed: ${originalError}`, ErrorCode.EXECUTION_FAILED, {
89
+ toolName: 'postgres-execute-sql',
90
+ details,
91
+ });
92
+ } finally {
93
+ try {
94
+ await client.end();
95
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
96
+ } catch (_e) {
97
+ // ignore
98
+ }
99
+ }
100
+ }