infrawise 0.1.2 → 0.2.0

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.
@@ -0,0 +1,347 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.currentFindings = exports.currentGraph = void 0;
7
+ exports.setGraphState = setGraphState;
8
+ exports.createServer = createServer;
9
+ const fastify_1 = __importDefault(require("fastify"));
10
+ const cors_1 = __importDefault(require("@fastify/cors"));
11
+ const core_1 = require("../core");
12
+ const analyzers_1 = require("../analyzers");
13
+ const graph_1 = require("../graph");
14
+ // ── State ────────────────────────────────────────────────────────────────────
15
+ let currentGraph = { nodes: [], edges: [] };
16
+ exports.currentGraph = currentGraph;
17
+ let currentFindings = [];
18
+ exports.currentFindings = currentFindings;
19
+ function setGraphState(graph, findings) {
20
+ exports.currentGraph = currentGraph = graph;
21
+ exports.currentFindings = currentFindings = findings;
22
+ }
23
+ function toText(data) {
24
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
25
+ }
26
+ // ── Tools ────────────────────────────────────────────────────────────────────
27
+ const TOOLS = [
28
+ {
29
+ name: 'get_infra_overview',
30
+ description: 'Returns a complete snapshot of all infrastructure: databases, queues, topics, secrets, parameters, log groups, lambdas, and all findings. Start here for a full picture.',
31
+ inputSchema: { type: 'object', properties: {} },
32
+ handler: async () => {
33
+ const tables = (0, graph_1.getTableNodes)(currentGraph);
34
+ const queues = (0, graph_1.getQueueNodes)(currentGraph);
35
+ const topics = (0, graph_1.getTopicNodes)(currentGraph);
36
+ const secrets = (0, graph_1.getSecretNodes)(currentGraph);
37
+ const parameters = (0, graph_1.getParameterNodes)(currentGraph);
38
+ const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph);
39
+ const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
40
+ const functions = (0, graph_1.getFunctionNodes)(currentGraph);
41
+ return toText({
42
+ summary: {
43
+ tables: tables.length, functions: functions.length,
44
+ queues: queues.length, topics: topics.length,
45
+ secrets: secrets.length, parameters: parameters.length,
46
+ logGroups: logGroups.length, lambdas: lambdas.length,
47
+ totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
48
+ findings: (0, analyzers_1.summarizeFindings)(currentFindings),
49
+ },
50
+ databases: tables.map((t) => ({ name: t.name, type: t.databaseType })),
51
+ queues: queues.map((q) => ({ name: q.name, hasDLQ: q.hasDLQ, encrypted: q.encrypted, approximateMessages: q.approximateMessages })),
52
+ topics: topics.map((t) => ({ name: t.name, subscriptions: t.subscriptionCount })),
53
+ secrets: secrets.map((s) => ({ name: s.name, rotationEnabled: s.rotationEnabled })),
54
+ parameters: parameters.map((p) => ({ name: p.name, type: p.paramType, tier: p.tier })),
55
+ lambdas: lambdas.map((l) => ({ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB })),
56
+ logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never', errorCount: lg.errorCount })),
57
+ highFindings: currentFindings.filter((f) => f.severity === 'high').map((f) => ({ issue: f.issue, recommendation: f.recommendation })),
58
+ });
59
+ },
60
+ },
61
+ {
62
+ name: 'get_graph_summary',
63
+ description: 'Returns the full infrastructure graph (all nodes and edges) plus findings summary.',
64
+ inputSchema: { type: 'object', properties: {} },
65
+ handler: async () => toText({
66
+ nodes: currentGraph.nodes,
67
+ edges: currentGraph.edges,
68
+ findings: currentFindings,
69
+ summary: {
70
+ totalNodes: currentGraph.nodes.length, totalEdges: currentGraph.edges.length,
71
+ tables: (0, graph_1.getTableNodes)(currentGraph).length, functions: (0, graph_1.getFunctionNodes)(currentGraph).length,
72
+ queues: (0, graph_1.getQueueNodes)(currentGraph).length, scans: (0, graph_1.getScanEdges)(currentGraph).length,
73
+ ...(0, analyzers_1.summarizeFindings)(currentFindings),
74
+ },
75
+ }),
76
+ },
77
+ {
78
+ name: 'analyze_function',
79
+ description: 'Analyze a specific function for all infrastructure issues: DB queries, queue publishing, secret access, etc.',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: { function: { type: 'string', description: 'Function name to analyze' } },
83
+ required: ['function'],
84
+ },
85
+ handler: async ({ function: functionName }) => {
86
+ const funcNode = currentGraph.nodes.find((n) => n.type === 'function' && n.name === functionName);
87
+ if (!funcNode) {
88
+ return toText({ function: functionName, found: false, issues: [], recommendations: [`Function "${String(functionName)}" not found in the analyzed codebase.`] });
89
+ }
90
+ const outEdges = (0, graph_1.getOutgoingEdges)(currentGraph, funcNode.id);
91
+ const relatedFindings = currentFindings.filter((f) => {
92
+ const meta = f.metadata;
93
+ return meta?.functionName === functionName || String(meta?.callerFunctions ?? '').includes(String(functionName));
94
+ });
95
+ return toText({
96
+ function: functionName, found: true,
97
+ file: funcNode.type === 'function' ? funcNode.file : undefined,
98
+ accesses: outEdges.map((e) => {
99
+ const target = currentGraph.nodes.find((n) => n.id === e.to);
100
+ return { targetId: e.to, edgeType: e.type, targetName: target && 'name' in target ? target.name : e.to, targetType: target?.type };
101
+ }),
102
+ issues: relatedFindings.map((f) => ({ severity: f.severity, issue: f.issue, description: f.description })),
103
+ recommendations: [...new Set(relatedFindings.map((f) => f.recommendation))],
104
+ });
105
+ },
106
+ },
107
+ {
108
+ name: 'suggest_gsi',
109
+ description: 'Get GSI suggestions for a DynamoDB table and attribute',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ table: { type: 'string', description: 'DynamoDB table name' },
114
+ attribute: { type: 'string', description: 'Attribute to create the GSI on' },
115
+ },
116
+ required: ['table', 'attribute'],
117
+ },
118
+ handler: async ({ table: tableName, attribute }) => {
119
+ const sanitizedAttr = String(attribute).replace(/[^a-zA-Z0-9_]/g, '_');
120
+ const indexName = `${String(tableName)}-${sanitizedAttr}-index`;
121
+ const tableNode = currentGraph.nodes.find((n) => n.type === 'table' && n.databaseType === 'dynamodb' && 'name' in n && n.name === tableName);
122
+ return toText({
123
+ table: tableName, attribute, found: !!tableNode,
124
+ index: { name: indexName, partitionKey: attribute, projectionType: 'ALL', billingMode: 'PAY_PER_REQUEST' },
125
+ rationale: `A GSI on "${String(attribute)}" allows Query instead of Scan when filtering by this attribute.`,
126
+ recommendation: `Add GSI "${indexName}" with partition key "${String(attribute)}" to your IaC definition.`,
127
+ });
128
+ },
129
+ },
130
+ {
131
+ name: 'postgres_index_suggestions',
132
+ description: 'Get PostgreSQL index suggestions for a table column',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ table: { type: 'string', description: 'PostgreSQL table name' },
137
+ column: { type: 'string', description: 'Column name to index' },
138
+ },
139
+ required: ['table', 'column'],
140
+ },
141
+ handler: async ({ table: tableName, column }) => {
142
+ const sanitizedCol = String(column).replace(/[^a-zA-Z0-9_]/g, '_');
143
+ const sanitizedTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, '_');
144
+ const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
145
+ return toText({
146
+ table: tableName, column,
147
+ recommendation: `CREATE INDEX CONCURRENTLY ${indexName} ON ${String(tableName)} (${String(column)});`,
148
+ rationale: `An index on "${String(column)}" eliminates sequential scans when filtering on this column.`,
149
+ notes: ['Use CONCURRENTLY to avoid locking the table', 'Run ANALYZE after creation',
150
+ `Partial index: CREATE INDEX CONCURRENTLY ${indexName}_partial ON ${String(tableName)} (${String(column)}) WHERE ${String(column)} IS NOT NULL;`],
151
+ });
152
+ },
153
+ },
154
+ {
155
+ name: 'suggest_mongo_index',
156
+ description: 'Get index suggestions for a MongoDB collection field',
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ collection: { type: 'string', description: 'MongoDB collection name' },
161
+ field: { type: 'string', description: 'Field name to index' },
162
+ },
163
+ required: ['collection', 'field'],
164
+ },
165
+ handler: async ({ collection, field }) => toText({
166
+ collection, field,
167
+ recommendation: `db.${String(collection)}.createIndex({ ${String(field)}: 1 })`,
168
+ rationale: `An index on "${String(field)}" eliminates full collection scans when filtering on this field.`,
169
+ notes: [
170
+ `Compound: db.${String(collection)}.createIndex({ ${String(field)}: 1, otherField: 1 })`,
171
+ `Text: db.${String(collection)}.createIndex({ ${String(field)}: "text" })`,
172
+ `Verify: db.${String(collection)}.explain("executionStats").find({ ${String(field)}: value })`,
173
+ ],
174
+ }),
175
+ },
176
+ {
177
+ name: 'mysql_index_suggestions',
178
+ description: 'Get MySQL index suggestions for a table column',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ table: { type: 'string', description: 'MySQL table name' },
183
+ column: { type: 'string', description: 'Column name to index' },
184
+ },
185
+ required: ['table', 'column'],
186
+ },
187
+ handler: async ({ table: tableName, column }) => {
188
+ const sanitizedCol = String(column).replace(/[^a-zA-Z0-9_]/g, '_');
189
+ const sanitizedTable = String(tableName).replace(/[^a-zA-Z0-9_]/g, '_');
190
+ const indexName = `idx_${sanitizedTable}_${sanitizedCol}`;
191
+ return toText({
192
+ table: tableName, column,
193
+ recommendation: `ALTER TABLE ${String(tableName)} ADD INDEX ${indexName} (${String(column)});`,
194
+ rationale: `An index on "${String(column)}" eliminates full table scans when filtering on this column.`,
195
+ notes: ['MySQL InnoDB adds indexes online (no full lock for 5.6+)', 'EXPLAIN SELECT ... to verify after adding',
196
+ `Composite: ALTER TABLE ${String(tableName)} ADD INDEX idx_composite (${String(column)}, other_column);`],
197
+ });
198
+ },
199
+ },
200
+ {
201
+ name: 'get_queue_details',
202
+ description: 'Returns all SQS queues with DLQ status, encryption, message counts, and retention.',
203
+ inputSchema: { type: 'object', properties: {} },
204
+ handler: async () => {
205
+ const queues = (0, graph_1.getQueueNodes)(currentGraph);
206
+ const queueFindings = currentFindings.filter((f) => f.metadata?.queueName);
207
+ return toText({
208
+ total: queues.length,
209
+ queues: queues.map((q) => ({
210
+ name: q.name, provider: q.provider, hasDLQ: q.hasDLQ, encrypted: q.encrypted,
211
+ approximateMessages: q.approximateMessages, retentionDays: q.retentionDays,
212
+ findings: queueFindings.filter((f) => f.metadata.queueName === q.name).map((f) => ({ severity: f.severity, issue: f.issue })),
213
+ })),
214
+ });
215
+ },
216
+ },
217
+ {
218
+ name: 'get_topic_details',
219
+ description: 'Returns all SNS topics with subscription counts and protocols.',
220
+ inputSchema: { type: 'object', properties: {} },
221
+ handler: async () => {
222
+ const topics = (0, graph_1.getTopicNodes)(currentGraph);
223
+ return toText({ total: topics.length, topics: topics.map((t) => ({ name: t.name, provider: t.provider, subscriptionCount: t.subscriptionCount, encrypted: t.encrypted })) });
224
+ },
225
+ },
226
+ {
227
+ name: 'get_secrets_overview',
228
+ description: 'Returns all Secrets Manager secrets: names, rotation status. Secret VALUES are never included.',
229
+ inputSchema: { type: 'object', properties: {} },
230
+ handler: async () => {
231
+ const secrets = (0, graph_1.getSecretNodes)(currentGraph);
232
+ const secretFindings = currentFindings.filter((f) => f.metadata?.secretName);
233
+ return toText({
234
+ total: secrets.length, note: 'Secret values are never included in this response.',
235
+ secrets: secrets.map((s) => ({
236
+ name: s.name, provider: s.provider, rotationEnabled: s.rotationEnabled, rotationDays: s.rotationDays,
237
+ findings: secretFindings.filter((f) => f.metadata.secretName === s.name).map((f) => ({ severity: f.severity, issue: f.issue })),
238
+ })),
239
+ });
240
+ },
241
+ },
242
+ {
243
+ name: 'get_parameter_overview',
244
+ description: 'Returns all SSM Parameter Store parameters: names, types, tiers. Parameter VALUES are never included.',
245
+ inputSchema: { type: 'object', properties: {} },
246
+ handler: async () => {
247
+ const parameters = (0, graph_1.getParameterNodes)(currentGraph);
248
+ return toText({
249
+ total: parameters.length, note: 'Parameter values are never included in this response.',
250
+ parameters: parameters.map((p) => ({ name: p.name, provider: p.provider, type: p.paramType, tier: p.tier })),
251
+ });
252
+ },
253
+ },
254
+ {
255
+ name: 'get_lambda_overview',
256
+ description: 'Returns all Lambda functions: runtime, memory, timeout, env var key names (values never included).',
257
+ inputSchema: { type: 'object', properties: {} },
258
+ handler: async () => {
259
+ const lambdas = (0, graph_1.getLambdaNodes)(currentGraph);
260
+ const lambdaFindings = currentFindings.filter((f) => f.metadata?.functionName);
261
+ return toText({
262
+ total: lambdas.length, note: 'Environment variable values are never included.',
263
+ lambdas: lambdas.map((l) => ({
264
+ name: l.name, runtime: l.runtime, memoryMB: l.memoryMB, timeoutSec: l.timeoutSec,
265
+ envVarCount: l.envVarKeys?.length ?? 0, envVarKeys: l.envVarKeys,
266
+ findings: lambdaFindings.filter((f) => f.metadata.functionName === l.name).map((f) => ({ severity: f.severity, issue: f.issue })),
267
+ })),
268
+ });
269
+ },
270
+ },
271
+ {
272
+ name: 'get_log_errors',
273
+ description: 'Returns recent error patterns from CloudWatch log groups. Returns pattern counts and frequencies — never raw log messages.',
274
+ inputSchema: {
275
+ type: 'object',
276
+ properties: { logGroup: { type: 'string', description: 'Filter to a specific log group name (optional)' } },
277
+ },
278
+ handler: async ({ logGroup: filterName }) => {
279
+ const logGroups = (0, graph_1.getLogGroupNodes)(currentGraph).filter((lg) => !filterName || lg.name.includes(String(filterName)));
280
+ return toText({
281
+ note: 'Only error patterns and counts are returned — no raw log messages.',
282
+ windowHours: 24,
283
+ logGroups: logGroups.map((lg) => ({ name: lg.name, retentionDays: lg.retentionDays ?? 'never-expires', errorCount: lg.errorCount, topErrorPatterns: lg.topErrorPatterns })),
284
+ });
285
+ },
286
+ },
287
+ ];
288
+ const ok = (id, result) => ({ jsonrpc: '2.0', id, result });
289
+ const rpcErr = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
290
+ async function handleMcp(body) {
291
+ const { method, params = {}, id } = body;
292
+ if (method === 'initialize') {
293
+ return ok(id, { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'infrawise', version: '0.1.0' } });
294
+ }
295
+ if (method === 'notifications/initialized' || method === 'ping') {
296
+ return id != null ? ok(id, {}) : null;
297
+ }
298
+ if (method === 'tools/list') {
299
+ return ok(id, { tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) });
300
+ }
301
+ if (method === 'tools/call') {
302
+ const { name, arguments: args = {} } = params;
303
+ const tool = TOOLS.find((t) => t.name === name);
304
+ if (!tool)
305
+ return rpcErr(id, -32601, `Unknown tool: ${name}`);
306
+ try {
307
+ return ok(id, await tool.handler(args));
308
+ }
309
+ catch (e) {
310
+ return rpcErr(id, -32603, e instanceof Error ? e.message : String(e));
311
+ }
312
+ }
313
+ return rpcErr(id, -32601, `Method not found: ${method}`);
314
+ }
315
+ // ── Fastify server ────────────────────────────────────────────────────────────
316
+ function createServer(port = 3000) {
317
+ const fastify = (0, fastify_1.default)({ logger: false });
318
+ fastify.register(cors_1.default, { origin: true });
319
+ fastify.get('/health', async () => ({
320
+ status: 'ok', version: '0.1.0',
321
+ graphNodes: currentGraph.nodes.length,
322
+ graphEdges: currentGraph.edges.length,
323
+ findings: currentFindings.length,
324
+ }));
325
+ fastify.get('/mcp/tools', async () => ({
326
+ tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
327
+ }));
328
+ fastify.post('/mcp', async (request, reply) => {
329
+ const response = await handleMcp(request.body);
330
+ if (response === null)
331
+ return reply.code(204).send();
332
+ return response;
333
+ });
334
+ return {
335
+ fastify,
336
+ start: async () => {
337
+ try {
338
+ await fastify.listen({ port, host: '0.0.0.0' });
339
+ core_1.logger.info(`Infrawise MCP server running at http://localhost:${port}`);
340
+ }
341
+ catch (e) {
342
+ core_1.logger.error(`Failed to start server: ${e instanceof Error ? e.message : String(e)}`);
343
+ process.exit(1);
344
+ }
345
+ },
346
+ };
347
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infrawise",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "CLI-first infrastructure intelligence platform — analyzes DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, SNS, SSM, Secrets Manager, Lambda, CloudWatch Logs and exposes findings as an MCP server for Claude Code",
5
5
  "keywords": [
6
6
  "infrastructure",
@@ -32,44 +32,34 @@
32
32
  },
33
33
  "license": "MIT",
34
34
  "bin": {
35
- "infrawise": "dist/index.js"
35
+ "infrawise": "dist/cli/index.js"
36
36
  },
37
- "main": "dist/index.js",
37
+ "main": "dist/cli/index.js",
38
38
  "engines": {
39
- "node": ">=24.0.0"
39
+ "node": ">=24.0.0",
40
+ "pnpm": ">=9.0.0"
40
41
  },
42
+ "packageManager": "pnpm@10.11.0",
41
43
  "scripts": {
42
- "build": "tsup && tsc --noEmit",
44
+ "build": "tsc --noEmit false --outDir dist",
43
45
  "test": "vitest run --passWithNoTests",
46
+ "coverage": "vitest run --coverage",
44
47
  "lint": "eslint src",
45
- "typecheck": "tsc --noEmit",
46
- "dev": "node dist/index.js dev",
47
- "prepack": "cp ../../README.md README.md 2>/dev/null || cp ../../../README.md README.md 2>/dev/null || true",
48
- "postpack": "rm -f README.md"
48
+ "typecheck": "tsc",
49
+ "dev": "node dist/cli/index.js dev",
50
+ "release": "node scripts/release.mjs"
49
51
  },
50
52
  "dependencies": {
53
+ "@aws-sdk/client-cloudwatch-logs": "^3.0.0",
51
54
  "@aws-sdk/client-dynamodb": "^3.0.0",
52
- "@aws-sdk/client-sqs": "^3.0.0",
55
+ "@aws-sdk/client-lambda": "^3.0.0",
56
+ "@aws-sdk/client-rds": "^3.0.0",
57
+ "@aws-sdk/client-secrets-manager": "^3.0.0",
53
58
  "@aws-sdk/client-sns": "^3.0.0",
59
+ "@aws-sdk/client-sqs": "^3.0.0",
54
60
  "@aws-sdk/client-ssm": "^3.0.0",
55
- "@aws-sdk/client-secrets-manager": "^3.0.0",
56
- "@aws-sdk/client-lambda": "^3.0.0",
57
- "@aws-sdk/client-cloudwatch-logs": "^3.0.0",
58
61
  "@aws-sdk/credential-providers": "^3.0.0",
59
62
  "@fastify/cors": "^9.0.0",
60
- "@infrawise/adapters-dynamodb": "workspace:*",
61
- "@infrawise/adapters-mongodb": "workspace:*",
62
- "@infrawise/adapters-mysql": "workspace:*",
63
- "@infrawise/adapters-postgres": "workspace:*",
64
- "@infrawise/adapters-terraform": "workspace:*",
65
- "@infrawise/adapters-aws-services": "workspace:*",
66
- "@infrawise/adapters-logs": "workspace:*",
67
- "@infrawise/analyzers": "workspace:*",
68
- "@infrawise/context": "workspace:*",
69
- "@infrawise/core": "workspace:*",
70
- "@infrawise/graph": "workspace:*",
71
- "@infrawise/server": "workspace:*",
72
- "@infrawise/shared": "workspace:*",
73
63
  "chalk": "^4.1.2",
74
64
  "commander": "^12.0.0",
75
65
  "fastify": "^4.26.0",
@@ -82,8 +72,8 @@
82
72
  "pino": "^8.19.0",
83
73
  "pino-pretty": "^11.0.0",
84
74
  "ts-morph": "^22.0.0",
85
- "typescript": "^5.4.0",
86
- "zod": "^3.22.0"
75
+ "typescript": "^5.9.3",
76
+ "zod": "^3.25.76"
87
77
  },
88
78
  "devDependencies": {
89
79
  "@types/inquirer": "^8.2.12",
@@ -92,9 +82,9 @@
92
82
  "@types/pg": "^8.11.0",
93
83
  "@typescript-eslint/eslint-plugin": "^7.0.0",
94
84
  "@typescript-eslint/parser": "^7.0.0",
85
+ "@vitest/coverage-v8": "^1.6.1",
95
86
  "eslint": "^8.57.0",
96
- "tsup": "^8.5.1",
97
- "typescript": "^5.4.0",
87
+ "typescript": "^5.9.3",
98
88
  "vitest": "^1.5.0"
99
89
  },
100
90
  "files": [