matimo-examples 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/.env.example +36 -0
- package/LICENSE +21 -0
- package/README.md +525 -0
- package/agents/decorator-pattern-agent.ts +368 -0
- package/agents/factory-pattern-agent.ts +253 -0
- package/agents/langchain-agent.ts +146 -0
- package/edit/edit-decorator.ts +128 -0
- package/edit/edit-factory.ts +120 -0
- package/edit/edit-langchain.ts +272 -0
- package/execute/execute-decorator.ts +49 -0
- package/execute/execute-factory.ts +46 -0
- package/execute/execute-langchain.ts +163 -0
- package/gmail/README.md +345 -0
- package/gmail/gmail-decorator.ts +216 -0
- package/gmail/gmail-factory.ts +231 -0
- package/gmail/gmail-langchain.ts +201 -0
- package/package.json +58 -0
- package/postgres/README.md +188 -0
- package/postgres/postgres-decorator.ts +198 -0
- package/postgres/postgres-factory.ts +180 -0
- package/postgres/postgres-langchain.ts +213 -0
- package/postgres/postgres-with-approval.ts +250 -0
- package/read/read-decorator.ts +107 -0
- package/read/read-factory.ts +104 -0
- package/read/read-langchain.ts +253 -0
- package/search/search-decorator.ts +154 -0
- package/search/search-factory.ts +129 -0
- package/search/search-langchain.ts +215 -0
- package/slack/README.md +339 -0
- package/slack/slack-decorator.ts +245 -0
- package/slack/slack-factory.ts +226 -0
- package/slack/slack-langchain.ts +242 -0
- package/tsconfig.json +20 -0
- package/web/web-decorator.ts +52 -0
- package/web/web-factory.ts +70 -0
- package/web/web-langchain.ts +163 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* POSTGRES TOOLS - LANGCHAIN AI AGENT EXAMPLE
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* PATTERN: True AI Agent with OpenAI + LangChain
|
|
8
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
* This is a REAL AI agent that:
|
|
10
|
+
* 1. Takes natural language user requests
|
|
11
|
+
* 2. Uses OpenAI LLM (GPT-4o-mini) to decide which Postgres tools to use
|
|
12
|
+
* 3. Generates appropriate SQL based on context
|
|
13
|
+
* 4. Executes queries autonomously
|
|
14
|
+
* 5. Processes results and responds naturally
|
|
15
|
+
*
|
|
16
|
+
* CRITICAL: Database credentials are NEVER exposed to the LLM
|
|
17
|
+
* - LLM only sees tool descriptions and results
|
|
18
|
+
* - Matimo handles all credential loading from environment variables
|
|
19
|
+
* - Tool execution is secure and isolated
|
|
20
|
+
*
|
|
21
|
+
* Use this pattern when:
|
|
22
|
+
* ✅ Building true autonomous AI agents
|
|
23
|
+
* ✅ LLM should decide which tools to use
|
|
24
|
+
* ✅ Complex workflows with LLM reasoning
|
|
25
|
+
* ✅ Multi-step agentic processes
|
|
26
|
+
* ✅ User gives high-level instructions (not SQL queries)
|
|
27
|
+
*
|
|
28
|
+
* SETUP:
|
|
29
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
30
|
+
* 1. Make sure Postgres is running (see POSTGRES_EXAMPLE_SETUP.md)
|
|
31
|
+
* 2. Create .env file with database credentials:
|
|
32
|
+
* MATIMO_POSTGRES_HOST=localhost
|
|
33
|
+
* MATIMO_POSTGRES_PORT=5432
|
|
34
|
+
* MATIMO_POSTGRES_USER=matimo
|
|
35
|
+
* MATIMO_POSTGRES_PASSWORD=development
|
|
36
|
+
* MATIMO_POSTGRES_DB=matimo
|
|
37
|
+
* OPENAI_API_KEY=sk-xxxxxxxxxxxxx
|
|
38
|
+
*
|
|
39
|
+
* USAGE:
|
|
40
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
* pnpm postgres:langchain
|
|
42
|
+
*
|
|
43
|
+
* WHAT IT DOES:
|
|
44
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
45
|
+
* This example shows an AI agent that can:
|
|
46
|
+
* 1. Explore database tables and schema
|
|
47
|
+
* 2. Execute SELECT queries to analyze data
|
|
48
|
+
* 3. Respond naturally in conversation style
|
|
49
|
+
* 4. Multi-step reasoning without exposing credentials
|
|
50
|
+
*
|
|
51
|
+
* Example:
|
|
52
|
+
* User: "What tables exist in matimo?"
|
|
53
|
+
* AI Agent: "Let me query the database to see what tables are available..."
|
|
54
|
+
* [Agent calls postgres-execute-sql tool with SELECT query]
|
|
55
|
+
* AI Agent: "The matimo database contains these tables: ..."
|
|
56
|
+
*
|
|
57
|
+
* ============================================================================
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import 'dotenv/config';
|
|
61
|
+
import { MatimoInstance, convertToolsToLangChain, ToolDefinition } from 'matimo';
|
|
62
|
+
import { createAgent } from 'langchain';
|
|
63
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run AI Agent with Postgres tools
|
|
67
|
+
* The agent receives natural language requests and decides which Postgres tools to use
|
|
68
|
+
*/
|
|
69
|
+
async function runPostgresAIAgent() {
|
|
70
|
+
console.info('\n╔════════════════════════════════════════════════════════╗');
|
|
71
|
+
console.info('║ Postgres AI Agent - LangChain + OpenAI ║');
|
|
72
|
+
console.info('║ True autonomous agent with LLM reasoning ║');
|
|
73
|
+
console.info('║ Database credentials: NEVER exposed to LLM ║');
|
|
74
|
+
console.info('╚════════════════════════════════════════════════════════╝\n');
|
|
75
|
+
|
|
76
|
+
// Check required environment variables
|
|
77
|
+
const pgConnected =
|
|
78
|
+
process.env.MATIMO_POSTGRES_HOST &&
|
|
79
|
+
process.env.MATIMO_POSTGRES_USER &&
|
|
80
|
+
process.env.MATIMO_POSTGRES_PASSWORD &&
|
|
81
|
+
process.env.MATIMO_POSTGRES_DB;
|
|
82
|
+
|
|
83
|
+
if (!pgConnected) {
|
|
84
|
+
console.error('❌ Error: Postgres connection not configured in .env');
|
|
85
|
+
console.info(' Required environment variables:');
|
|
86
|
+
console.info(' MATIMO_POSTGRES_HOST');
|
|
87
|
+
console.info(' MATIMO_POSTGRES_USER');
|
|
88
|
+
console.info(' MATIMO_POSTGRES_PASSWORD');
|
|
89
|
+
console.info(' MATIMO_POSTGRES_DB');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
94
|
+
if (!openaiKey) {
|
|
95
|
+
console.error('❌ Error: OPENAI_API_KEY not set in .env');
|
|
96
|
+
console.info(' Set it: export OPENAI_API_KEY="sk-..."');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.info(
|
|
101
|
+
`📍 Postgres: ${process.env.MATIMO_POSTGRES_DB}@${process.env.MATIMO_POSTGRES_HOST}`
|
|
102
|
+
);
|
|
103
|
+
console.info(`🤖 Using OpenAI (GPT-4o-mini) as the AI agent\n`);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Initialize Matimo with auto-discovery
|
|
107
|
+
console.info('🚀 Initializing Matimo...');
|
|
108
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
109
|
+
|
|
110
|
+
// Get Postgres tools and convert to LangChain format
|
|
111
|
+
console.info('💬 Loading Postgres tools...');
|
|
112
|
+
const matimoTools = matimo.listTools();
|
|
113
|
+
const postgresTools = matimoTools.filter((t) => t.name.startsWith('postgres'));
|
|
114
|
+
console.info(`✅ Loaded ${postgresTools.length} Postgres tool(s)\n`);
|
|
115
|
+
|
|
116
|
+
// ✅ Convert Matimo tools to LangChain format using the built-in integration
|
|
117
|
+
const langchainTools = await convertToolsToLangChain(
|
|
118
|
+
postgresTools as ToolDefinition[],
|
|
119
|
+
matimo,
|
|
120
|
+
{
|
|
121
|
+
// Pass database credentials so LLM tool calls can execute securely
|
|
122
|
+
// The LLM will NEVER see these values - only Matimo tool execution will use them
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Initialize OpenAI LLM
|
|
127
|
+
console.info('🤖 Initializing OpenAI (GPT-4o-mini) LLM...');
|
|
128
|
+
const model = new ChatOpenAI({
|
|
129
|
+
modelName: 'gpt-4o-mini',
|
|
130
|
+
temperature: 0.7,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Create agent
|
|
134
|
+
console.info('🔧 Creating agent...\n');
|
|
135
|
+
const agent = await createAgent({
|
|
136
|
+
model,
|
|
137
|
+
tools: langchainTools as any[], // Type casting for LangChain tools
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Define agent tasks (sequential - each task uses results from previous)
|
|
141
|
+
const userRequests = [
|
|
142
|
+
{
|
|
143
|
+
title: 'Step 1: Discover tables',
|
|
144
|
+
request:
|
|
145
|
+
'Get a list of all tables in the matimo database. Query the information_schema to show me all tables and their row counts.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
title: 'Step 2: Analyze data volume',
|
|
149
|
+
request:
|
|
150
|
+
'Now that you know which tables exist, tell me the total number of records across all tables and which tables have the most data.',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
title: 'Step 3: Show schema for main table',
|
|
154
|
+
request:
|
|
155
|
+
'For the table with the most records, show me the column structure (column names and data types) so I understand what data is stored there.',
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
console.info('🧪 Running AI Agent Tasks (Sequential Workflow)');
|
|
160
|
+
console.info('═'.repeat(60));
|
|
161
|
+
|
|
162
|
+
// Run each task through the agent sequentially
|
|
163
|
+
// Each task builds on previous results instead of making assumptions
|
|
164
|
+
for (const task of userRequests) {
|
|
165
|
+
console.info(`\n${task.title}`);
|
|
166
|
+
console.info('─'.repeat(60));
|
|
167
|
+
console.info(`👤 User: "${task.request}"\n`);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const response = await agent.invoke({
|
|
171
|
+
messages: [
|
|
172
|
+
{
|
|
173
|
+
role: 'user',
|
|
174
|
+
content: task.request,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Get the last message from the agent
|
|
180
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
181
|
+
if (lastMessage) {
|
|
182
|
+
if (typeof lastMessage.content === 'string') {
|
|
183
|
+
console.info(`🤖 Agent: ${lastMessage.content}\n`);
|
|
184
|
+
} else {
|
|
185
|
+
console.info(`🤖 Agent:`, lastMessage.content, '\n');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
190
|
+
console.info(`⚠️ Agent error: ${errorMsg}\n`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.info('═'.repeat(60));
|
|
195
|
+
console.info('✨ AI Agent Examples Complete!\n');
|
|
196
|
+
console.info('Key Features:');
|
|
197
|
+
console.info(' ✅ Real LLM (OpenAI) decides which tools to use');
|
|
198
|
+
console.info(' ✅ Natural language requests, not SQL queries');
|
|
199
|
+
console.info(' ✅ LLM generates SQL based on context');
|
|
200
|
+
console.info(' ✅ Database credentials NEVER exposed to LLM');
|
|
201
|
+
console.info(' ✅ Matimo handles secure credential injection');
|
|
202
|
+
console.info(' ✅ Agentic reasoning and decision-making\n');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('❌ Error:', error instanceof Error ? error.message : String(error));
|
|
205
|
+
if (error instanceof Error && error.stack) {
|
|
206
|
+
console.error('Stack:', error.stack);
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Run the AI agent
|
|
213
|
+
runPostgresAIAgent().catch(console.error);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres Tool Example with Real Database and SQL Approval Flow
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* 1. Connecting to a real Postgres database
|
|
6
|
+
* 2. Executing non-destructive queries (SELECT)
|
|
7
|
+
* 3. Executing destructive queries with approval flow
|
|
8
|
+
* 4. Setting up approval callbacks
|
|
9
|
+
*
|
|
10
|
+
* Setup Instructions:
|
|
11
|
+
* 1. Make sure you have a Postgres instance running locally or accessible
|
|
12
|
+
* 2. Set the connection string via environment variable:
|
|
13
|
+
* export MATIMO_POSTGRES_URL="postgresql://user:password@localhost:5432/dbname"
|
|
14
|
+
* OR set individual components:
|
|
15
|
+
* export MATIMO_POSTGRES_HOST="localhost"
|
|
16
|
+
* export MATIMO_POSTGRES_PORT="5432"
|
|
17
|
+
* export MATIMO_POSTGRES_USER="user"
|
|
18
|
+
* export MATIMO_POSTGRES_PASSWORD="password"
|
|
19
|
+
* export MATIMO_POSTGRES_DB="dbname"
|
|
20
|
+
*
|
|
21
|
+
* 3. Run the example:
|
|
22
|
+
* pnpm tsx examples/tools/postgres/postgres-with-approval.ts
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import 'dotenv/config';
|
|
26
|
+
import { MatimoInstance, getSQLApprovalManager, setSQLApprovalManager } from '@matimo/core';
|
|
27
|
+
import * as path from 'path';
|
|
28
|
+
import * as readline from 'readline';
|
|
29
|
+
|
|
30
|
+
// Interactive approval callback with better input handling
|
|
31
|
+
function createInteractiveApprovalCallback() {
|
|
32
|
+
return async (sql: string, mode: string): Promise<boolean> => {
|
|
33
|
+
// Check if stdin is a TTY (interactive terminal)
|
|
34
|
+
const isInteractive = process.stdin.isTTY;
|
|
35
|
+
|
|
36
|
+
console.info(`\n⚠️ Approval Required for ${mode.toUpperCase()} operation:`);
|
|
37
|
+
console.info(`SQL: ${sql}`);
|
|
38
|
+
console.info('');
|
|
39
|
+
|
|
40
|
+
if (!isInteractive) {
|
|
41
|
+
console.info('❌ REJECTED - Non-interactive mode (no terminal input available)');
|
|
42
|
+
console.info('');
|
|
43
|
+
console.info('To run this example interactively:');
|
|
44
|
+
console.info(' 1. Run directly: pnpm postgres:approval');
|
|
45
|
+
console.info(' 2. Or set MATIMO_SQL_AUTO_APPROVE=true for automated approval');
|
|
46
|
+
console.info('');
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rl = readline.createInterface({
|
|
51
|
+
input: process.stdin,
|
|
52
|
+
output: process.stdout,
|
|
53
|
+
terminal: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
rl.question('Do you approve? (yes/no): ', (answer) => {
|
|
58
|
+
const approved = answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y';
|
|
59
|
+
console.info(`Result: ${approved ? '✅ APPROVED' : '❌ REJECTED'}\n`);
|
|
60
|
+
rl.close();
|
|
61
|
+
resolve(approved);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
// Check if database connection is available
|
|
69
|
+
if (
|
|
70
|
+
!process.env.MATIMO_POSTGRES_URL &&
|
|
71
|
+
(!process.env.MATIMO_POSTGRES_HOST ||
|
|
72
|
+
!process.env.MATIMO_POSTGRES_USER ||
|
|
73
|
+
!process.env.MATIMO_POSTGRES_PASSWORD ||
|
|
74
|
+
!process.env.MATIMO_POSTGRES_DB)
|
|
75
|
+
) {
|
|
76
|
+
console.error('❌ Database connection not configured.');
|
|
77
|
+
console.error('Please set one of:');
|
|
78
|
+
console.error(' - MATIMO_POSTGRES_URL="postgresql://user:password@host:port/db"');
|
|
79
|
+
console.error(
|
|
80
|
+
' - MATIMO_POSTGRES_HOST, MATIMO_POSTGRES_PORT, MATIMO_POSTGRES_USER, MATIMO_POSTGRES_PASSWORD, MATIMO_POSTGRES_DB'
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Initialize Matimo with postgres tools
|
|
86
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
87
|
+
|
|
88
|
+
console.info('🚀 Postgres Tool Example with Approval Flow');
|
|
89
|
+
console.info('='.repeat(60));
|
|
90
|
+
|
|
91
|
+
// Configure SQL approval manager
|
|
92
|
+
const approvalManager = getSQLApprovalManager();
|
|
93
|
+
approvalManager.setApprovalCallback(createInteractiveApprovalCallback());
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// 1. List available tools
|
|
97
|
+
const tools = matimo.listTools();
|
|
98
|
+
const postgresTools = tools.filter((t) => t.name.startsWith('postgres'));
|
|
99
|
+
console.info(`\n📋 Available Postgres tools: ${postgresTools.map((t) => t.name).join(', ')}`);
|
|
100
|
+
|
|
101
|
+
// Sequential discovery workflow
|
|
102
|
+
|
|
103
|
+
// STEP 1: Discover tables (SELECT - no approval needed)
|
|
104
|
+
console.info('\n\n1️⃣ DISCOVER TABLES (Step 1/3 - No approval needed)');
|
|
105
|
+
console.info('-'.repeat(60));
|
|
106
|
+
console.info('SQL: SELECT table_name FROM information_schema.tables WHERE table_schema = $1\n');
|
|
107
|
+
|
|
108
|
+
let discoveredTables: string[] = [];
|
|
109
|
+
try {
|
|
110
|
+
const tablesResult = await matimo.execute('postgres-execute-sql', {
|
|
111
|
+
sql: `
|
|
112
|
+
SELECT table_name
|
|
113
|
+
FROM information_schema.tables
|
|
114
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
115
|
+
ORDER BY table_name;
|
|
116
|
+
`,
|
|
117
|
+
});
|
|
118
|
+
if (
|
|
119
|
+
tablesResult &&
|
|
120
|
+
typeof tablesResult === 'object' &&
|
|
121
|
+
'success' in tablesResult &&
|
|
122
|
+
(tablesResult as any).success === false
|
|
123
|
+
) {
|
|
124
|
+
throw new Error((tablesResult as any).error || 'Query failed');
|
|
125
|
+
}
|
|
126
|
+
console.info('✅ Query executed successfully (no approval needed for SELECT)');
|
|
127
|
+
if (tablesResult && typeof tablesResult === 'object' && 'rows' in tablesResult) {
|
|
128
|
+
const rows = (tablesResult as any).rows;
|
|
129
|
+
if (rows && rows.length > 0) {
|
|
130
|
+
console.info('Tables found:');
|
|
131
|
+
rows.forEach((row: any) => {
|
|
132
|
+
console.info(` - ${row.table_name}`);
|
|
133
|
+
discoveredTables.push(row.table_name);
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
console.info('(No tables found in public schema)');
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
console.info('(No tables found in public schema)');
|
|
140
|
+
}
|
|
141
|
+
} catch (err: any) {
|
|
142
|
+
console.error('❌ Error:', err.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// STEP 2: Get row counts (SELECT - no approval needed)
|
|
146
|
+
console.info('\n\n2️⃣ COUNT RECORDS (Step 2/3 - No approval needed)');
|
|
147
|
+
console.info('-'.repeat(60));
|
|
148
|
+
console.info('SQL: SELECT tablename, n_live_tup FROM pg_stat_user_tables\n');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const countsResult = await matimo.execute('postgres-execute-sql', {
|
|
152
|
+
sql: `
|
|
153
|
+
SELECT
|
|
154
|
+
table_name,
|
|
155
|
+
(SELECT count(*) FROM information_schema.columns
|
|
156
|
+
WHERE information_schema.columns.table_name = t.table_name) as columns
|
|
157
|
+
FROM information_schema.tables t
|
|
158
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
159
|
+
ORDER BY table_name;
|
|
160
|
+
`,
|
|
161
|
+
});
|
|
162
|
+
if (
|
|
163
|
+
countsResult &&
|
|
164
|
+
typeof countsResult === 'object' &&
|
|
165
|
+
'success' in countsResult &&
|
|
166
|
+
(countsResult as any).success === false
|
|
167
|
+
) {
|
|
168
|
+
throw new Error((countsResult as any).error || 'Query failed');
|
|
169
|
+
}
|
|
170
|
+
console.info('✅ Query executed successfully (no approval needed for SELECT)');
|
|
171
|
+
if (countsResult && typeof countsResult === 'object' && 'rows' in countsResult) {
|
|
172
|
+
const rows = (countsResult as any).rows;
|
|
173
|
+
if (rows && rows.length > 0) {
|
|
174
|
+
console.info('Tables found:');
|
|
175
|
+
rows.forEach((row: any) => {
|
|
176
|
+
console.info(` - ${row.table_name} (${row.columns} columns)`);
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
console.info('(No tables found in public schema)');
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
console.info('(No tables found in public schema)');
|
|
183
|
+
}
|
|
184
|
+
} catch (err: any) {
|
|
185
|
+
console.error('❌ Error:', err.message);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// STEP 3: Try a destructive operation that requires approval
|
|
189
|
+
console.info('\n\n3️⃣ DESTRUCTIVE OPERATION (Step 3/3 - Requires approval)');
|
|
190
|
+
console.info('-'.repeat(60));
|
|
191
|
+
console.info('Attempting DELETE on first discovered table (if any)...\n');
|
|
192
|
+
|
|
193
|
+
if (discoveredTables.length > 0) {
|
|
194
|
+
const tableName = discoveredTables[0];
|
|
195
|
+
console.info(`SQL: DELETE FROM ${tableName} WHERE 1=0 (safe - matches no rows)\n`);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
// Safe DELETE that matches no rows (WHERE 1=0)
|
|
199
|
+
const deleteResult = await matimo.execute('postgres-execute-sql', {
|
|
200
|
+
sql: `DELETE FROM ${tableName} WHERE 1=0;`,
|
|
201
|
+
});
|
|
202
|
+
console.info('✅ DELETE approved and executed successfully');
|
|
203
|
+
console.info(' (0 rows deleted - safe WHERE clause)\n');
|
|
204
|
+
} catch (err: any) {
|
|
205
|
+
const errorMsg = err?.message || String(err);
|
|
206
|
+
if (
|
|
207
|
+
errorMsg.includes('not approved') ||
|
|
208
|
+
errorMsg.includes('approval') ||
|
|
209
|
+
errorMsg.includes('callback')
|
|
210
|
+
) {
|
|
211
|
+
console.info('⚠️ DELETE was REJECTED by user');
|
|
212
|
+
console.info(
|
|
213
|
+
' Approval is required for destructive operations (CREATE/DROP/ALTER/DELETE/UPDATE)\n'
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
console.error('❌ Error:', errorMsg);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
console.info('⚠️ No tables found to demonstrate destructive operation');
|
|
221
|
+
console.info(' (Would require approval when attempting DELETE/UPDATE/CREATE/DROP/ALTER)\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// STEP 4: Show approval configuration
|
|
225
|
+
console.info('\n4️⃣ APPROVAL CONFIGURATION');
|
|
226
|
+
console.info('-'.repeat(60));
|
|
227
|
+
console.info('Current approval settings:');
|
|
228
|
+
console.info(
|
|
229
|
+
` - MATIMO_SQL_AUTO_APPROVE=${process.env.MATIMO_SQL_AUTO_APPROVE || '(not set)'}`
|
|
230
|
+
);
|
|
231
|
+
console.info(` - Interactive approval callback: ${approvalManager ? 'Enabled' : 'Disabled'}`);
|
|
232
|
+
console.info('\nFor CI/automated environments, set:');
|
|
233
|
+
console.info(' export MATIMO_SQL_AUTO_APPROVE=true\n');
|
|
234
|
+
|
|
235
|
+
console.info('✨ Sequential discovery with approval workflow complete!');
|
|
236
|
+
console.info('='.repeat(60));
|
|
237
|
+
console.info('Pattern: 1) Discover tables 2) Count records 3) Attempt destructive op\n');
|
|
238
|
+
} catch (err: any) {
|
|
239
|
+
console.error('\n❌ Error:', err.message);
|
|
240
|
+
if (err.code === 'ECONNREFUSED') {
|
|
241
|
+
console.error('\nDatabase connection refused. Make sure your Postgres instance is running.');
|
|
242
|
+
}
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main().catch((err) => {
|
|
248
|
+
console.error('Fatal error:', err);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MatimoInstance,
|
|
3
|
+
setGlobalMatimoInstance,
|
|
4
|
+
tool,
|
|
5
|
+
getPathApprovalManager,
|
|
6
|
+
} from '@matimo/core';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import readline from 'readline';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
// Create readline interface for interactive approval prompts
|
|
14
|
+
const rl = readline.createInterface({
|
|
15
|
+
input: process.stdin,
|
|
16
|
+
output: process.stdout,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let isReadlineClosed = false;
|
|
20
|
+
|
|
21
|
+
// Track when readline closes (e.g., piped input ends)
|
|
22
|
+
rl.on('close', () => {
|
|
23
|
+
isReadlineClosed = true;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Prompt user for approval decision
|
|
28
|
+
*/
|
|
29
|
+
async function promptForApproval(
|
|
30
|
+
filePath: string,
|
|
31
|
+
mode: 'read' | 'write' | 'search'
|
|
32
|
+
): Promise<boolean> {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
// If readline is closed (e.g., non-TTY/piped input), auto-approve
|
|
35
|
+
if (isReadlineClosed) {
|
|
36
|
+
console.info(
|
|
37
|
+
`[${mode.toUpperCase()}] Access to ${filePath} auto-approved (non-interactive mode)`
|
|
38
|
+
);
|
|
39
|
+
resolve(true);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
rl.question(`[${mode.toUpperCase()}] Approve access to ${filePath}? (y/n): `, (answer) => {
|
|
43
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Example: Read tool using @tool decorator pattern
|
|
50
|
+
* Demonstrates class-based file reading with automatic decoration
|
|
51
|
+
*/
|
|
52
|
+
class FileReader {
|
|
53
|
+
@tool('read')
|
|
54
|
+
async readFile(filePath: string, startLine: number, endLine: number): Promise<unknown> {
|
|
55
|
+
// Decorator automatically intercepts and executes via Matimo
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@tool('read')
|
|
60
|
+
async getFileMetadata(filePath: string): Promise<unknown> {
|
|
61
|
+
// Decorator automatically intercepts and executes via Matimo
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function decoratorExample() {
|
|
67
|
+
// Set up decorator support with autoDiscover
|
|
68
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
69
|
+
setGlobalMatimoInstance(matimo);
|
|
70
|
+
|
|
71
|
+
// Set up approval callback for interactive approval
|
|
72
|
+
const approvalManager = getPathApprovalManager();
|
|
73
|
+
approvalManager.setApprovalCallback(promptForApproval);
|
|
74
|
+
|
|
75
|
+
console.info('=== Read Tool - Decorator Pattern (Interactive Approval) ===\n');
|
|
76
|
+
|
|
77
|
+
const reader = new FileReader();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Example 1: Read file through decorated method
|
|
81
|
+
console.info('1. Reading file: read-factory.ts (lines 1-15)\n');
|
|
82
|
+
const result1 = await reader.readFile(path.join(__dirname, './read-factory.ts'), 1, 15);
|
|
83
|
+
console.info('File:', (result1 as any).filePath);
|
|
84
|
+
console.info('Lines read:', (result1 as any).readLines);
|
|
85
|
+
console.info('Content preview:');
|
|
86
|
+
console.info((result1 as any).content?.substring(0, 200));
|
|
87
|
+
console.info('---\n');
|
|
88
|
+
|
|
89
|
+
// Example 2: Read another file
|
|
90
|
+
console.info('2. Reading file: package.json (lines 1-10)\n');
|
|
91
|
+
const result2 = await reader.getFileMetadata(path.join(__dirname, '../../../package.json'));
|
|
92
|
+
console.info('File:', (result2 as any).filePath);
|
|
93
|
+
console.info('Lines read:', (result2 as any).readLines);
|
|
94
|
+
console.info('Content preview:');
|
|
95
|
+
console.info((result2 as any).content?.substring(0, 200));
|
|
96
|
+
console.info('---\n');
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
console.error('Error:', error.message);
|
|
99
|
+
} finally {
|
|
100
|
+
if (!isReadlineClosed) {
|
|
101
|
+
rl.close();
|
|
102
|
+
isReadlineClosed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
decoratorExample();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { MatimoInstance } from '@matimo/core';
|
|
2
|
+
import { getPathApprovalManager } from '@matimo/core';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import readline from 'readline';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
// Reusable readline interface kept open for multiple prompts
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let isReadlineClosed = false;
|
|
16
|
+
|
|
17
|
+
// Track when readline closes (e.g., piped input ends)
|
|
18
|
+
rl.on('close', () => {
|
|
19
|
+
isReadlineClosed = true;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Interactive prompt helper for approval decisions
|
|
24
|
+
*/
|
|
25
|
+
async function promptForApproval(
|
|
26
|
+
filePath: string,
|
|
27
|
+
mode: 'read' | 'write' | 'search'
|
|
28
|
+
): Promise<boolean> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
// If readline is closed (e.g., non-TTY/piped input), auto-approve
|
|
31
|
+
if (isReadlineClosed) {
|
|
32
|
+
console.info(
|
|
33
|
+
`[${mode.toUpperCase()}] Access to ${filePath} auto-approved (non-interactive mode)`
|
|
34
|
+
);
|
|
35
|
+
resolve(true);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
rl.question(`[${mode.toUpperCase()}] Approve access to ${filePath}? (y/n): `, (answer) => {
|
|
39
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Example: Read tool using factory pattern
|
|
46
|
+
* Demonstrates reading file contents and metadata with interactive approval
|
|
47
|
+
*/
|
|
48
|
+
async function readExample() {
|
|
49
|
+
// Initialize Matimo with autoDiscover to find all tools (core + providers)
|
|
50
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
51
|
+
|
|
52
|
+
// Set up approval callback for examples with INTERACTIVE PROMPTS
|
|
53
|
+
const approvalManager = getPathApprovalManager();
|
|
54
|
+
approvalManager.setApprovalCallback(promptForApproval);
|
|
55
|
+
|
|
56
|
+
console.info('=== Read Tool - Factory Pattern (Interactive Approval) ===\n');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Example 1: Read this example file
|
|
60
|
+
console.info('1. Reading read-factory.ts\n');
|
|
61
|
+
const result1 = await matimo.execute('read', {
|
|
62
|
+
filePath: path.join(__dirname, './read-factory.ts'),
|
|
63
|
+
startLine: 1,
|
|
64
|
+
endLine: 20,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if ((result1 as any).success) {
|
|
68
|
+
console.info('File:', (result1 as any).filePath);
|
|
69
|
+
console.info('Lines read:', (result1 as any).readLines);
|
|
70
|
+
console.info('Content preview:');
|
|
71
|
+
console.info((result1 as any).content?.substring(0, 300));
|
|
72
|
+
} else {
|
|
73
|
+
console.info('Access denied:', (result1 as any).error);
|
|
74
|
+
}
|
|
75
|
+
console.info('---\n');
|
|
76
|
+
|
|
77
|
+
// Example 2: Read package.json
|
|
78
|
+
console.info('2. Reading package.json\n');
|
|
79
|
+
const result2 = await matimo.execute('read', {
|
|
80
|
+
filePath: path.join(__dirname, '../../../package.json'),
|
|
81
|
+
startLine: 1,
|
|
82
|
+
endLine: 15,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if ((result2 as any).success) {
|
|
86
|
+
console.info('File:', (result2 as any).filePath);
|
|
87
|
+
console.info('Lines read:', (result2 as any).readLines);
|
|
88
|
+
console.info('Content preview:');
|
|
89
|
+
console.info((result2 as any).content?.substring(0, 200));
|
|
90
|
+
} else {
|
|
91
|
+
console.info('Access denied:', (result2 as any).error);
|
|
92
|
+
}
|
|
93
|
+
console.info('---\n');
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
console.error('Error reading file:', error.message, error.code, error.details);
|
|
96
|
+
} finally {
|
|
97
|
+
if (!isReadlineClosed) {
|
|
98
|
+
rl.close();
|
|
99
|
+
isReadlineClosed = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
readExample();
|