@blogic-cz/agent-tools 0.14.10 → 0.14.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.10",
3
+ "version": "0.14.12",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -5,18 +5,39 @@
5
5
  * This is a known limitation — callers must validate the table name
6
6
  * via `isValidTableName()` before calling `getColumns()`.
7
7
  */
8
+ export const SYSTEM_SCHEMAS_SQL = "'pg_catalog', 'information_schema'";
9
+
10
+ export function parseTableReference(tableName: string): { schemaName?: string; tableName: string } {
11
+ const [schemaName, name, ...extra] = tableName.split(".");
12
+
13
+ if (name && extra.length === 0) {
14
+ return { schemaName, tableName: name };
15
+ }
16
+
17
+ return { tableName };
18
+ }
19
+
8
20
  export const SCHEMA_QUERIES = {
9
21
  tables: `
10
- SELECT tablename as name
22
+ SELECT
23
+ schemaname as schema,
24
+ tablename as name,
25
+ schemaname || '.' || tablename as qualified_name
11
26
  FROM pg_tables
12
- WHERE schemaname = 'public'
13
- ORDER BY tablename
27
+ WHERE schemaname NOT IN (${SYSTEM_SCHEMAS_SQL})
28
+ ORDER BY schemaname, tablename
14
29
  `,
15
30
  columns: (tableName: string) => {
16
- const escapedTableName = tableName.replaceAll("'", "''");
31
+ const tableReference = parseTableReference(tableName);
32
+ const escapedTableName = tableReference.tableName.replaceAll("'", "''");
33
+ const schemaFilter = tableReference.schemaName
34
+ ? `AND c.table_schema = '${tableReference.schemaName.replaceAll("'", "''")}'`
35
+ : `AND c.table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})`;
17
36
 
18
37
  return `
19
38
  SELECT
39
+ c.table_schema as schema,
40
+ c.table_name as table,
20
41
  c.column_name as name,
21
42
  c.data_type as type,
22
43
  c.is_nullable = 'YES' as nullable,
@@ -32,14 +53,16 @@ export const SCHEMA_QUERIES = {
32
53
  ) as is_primary_key
33
54
  FROM information_schema.columns c
34
55
  WHERE c.table_name = '${escapedTableName}'
35
- AND c.table_schema = 'public'
36
- ORDER BY c.ordinal_position
56
+ ${schemaFilter}
57
+ ORDER BY c.table_schema, c.ordinal_position
37
58
  `;
38
59
  },
39
60
  relationships: `
40
61
  SELECT
62
+ tc.table_schema as from_schema,
41
63
  tc.table_name as from_table,
42
64
  kcu.column_name as from_column,
65
+ ccu.table_schema as to_schema,
43
66
  ccu.table_name as to_table,
44
67
  ccu.column_name as to_column,
45
68
  tc.constraint_name
@@ -49,10 +72,10 @@ export const SCHEMA_QUERIES = {
49
72
  AND tc.table_schema = kcu.table_schema
50
73
  JOIN information_schema.constraint_column_usage ccu
51
74
  ON ccu.constraint_name = tc.constraint_name
52
- AND ccu.table_schema = tc.table_schema
75
+ AND ccu.constraint_schema = tc.constraint_schema
53
76
  WHERE tc.constraint_type = 'FOREIGN KEY'
54
- AND tc.table_schema = 'public'
55
- ORDER BY tc.table_name, kcu.column_name
77
+ AND tc.table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})
78
+ ORDER BY tc.table_schema, tc.table_name, kcu.column_name
56
79
  `,
57
80
  };
58
81
 
@@ -10,7 +10,7 @@ const MUTATION_PATTERNS = [
10
10
  /^\s*CREATE\s+/i,
11
11
  ];
12
12
 
13
- const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
13
+ const TABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
14
14
 
15
15
  /**
16
16
  * Strip SQL comments (block and line) while preserving string literals.
@@ -15,7 +15,13 @@ import {
15
15
  DbTunnelError,
16
16
  type DbError,
17
17
  } from "./errors";
18
- import { getColumns, getRelationships, getTableNames } from "./schema";
18
+ import {
19
+ getColumns,
20
+ getRelationships,
21
+ getTableNames,
22
+ parseTableReference,
23
+ SYSTEM_SCHEMAS_SQL,
24
+ } from "./schema";
19
25
  import { detectSchemaError, isValidTableName, isMutationQuery } from "./security";
20
26
  import { transformQueryResult } from "./transformers";
21
27
 
@@ -324,7 +330,7 @@ export class DbService extends Context.Service<
324
330
  ) {
325
331
  const command = buildPsqlCommand(
326
332
  config,
327
- "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename;",
333
+ `SELECT schemaname || '.' || tablename FROM pg_tables WHERE schemaname NOT IN (${SYSTEM_SCHEMAS_SQL}) ORDER BY schemaname, tablename;`,
328
334
  password,
329
335
  true,
330
336
  );
@@ -356,11 +362,16 @@ export class DbService extends Context.Service<
356
362
  return [] as string[];
357
363
  }
358
364
 
359
- const escapedTableName = tableName.replaceAll("'", "''");
365
+ const tableReference = parseTableReference(tableName);
366
+ const escapedSchemaName = tableReference.schemaName?.replaceAll("'", "''");
367
+ const escapedTableName = tableReference.tableName.replaceAll("'", "''");
368
+ const schemaFilter = escapedSchemaName
369
+ ? `AND table_schema = '${escapedSchemaName}'`
370
+ : `AND table_schema NOT IN (${SYSTEM_SCHEMAS_SQL})`;
360
371
 
361
372
  const command = buildPsqlCommand(
362
373
  config,
363
- `SELECT column_name FROM information_schema.columns WHERE table_name = '${escapedTableName}' AND table_schema = 'public' ORDER BY ordinal_position;`,
374
+ `SELECT column_name FROM information_schema.columns WHERE table_name = '${escapedTableName}' ${schemaFilter} ORDER BY table_schema, ordinal_position;`,
364
375
  password,
365
376
  true,
366
377
  );
@@ -516,21 +527,25 @@ export class DbService extends Context.Service<
516
527
  }
517
528
 
518
529
  const tables = tablesResult.data as {
530
+ schema?: string;
519
531
  name: string;
532
+ qualified_name?: string;
520
533
  }[];
521
534
  const fullSchema: Record<string, unknown>[] = [];
522
535
 
523
536
  for (const table of tables) {
524
537
  const columnsResult = yield* executeSelectQuery(
525
538
  config,
526
- getColumns(table.name),
539
+ getColumns(table.qualified_name ?? table.name),
527
540
  password,
528
541
  startTimeMs,
529
542
  ).pipe(Effect.catch(() => Effect.succeed(null)));
530
543
 
531
544
  if (columnsResult && columnsResult.success && columnsResult.data) {
532
545
  fullSchema.push({
546
+ schema: table.schema,
533
547
  table: table.name,
548
+ qualified_name: table.qualified_name ?? table.name,
534
549
  columns: columnsResult.data,
535
550
  });
536
551
  }
@@ -668,7 +683,7 @@ export class DbService extends Context.Service<
668
683
  return {
669
684
  success: false,
670
685
  error:
671
- "Invalid table name. Use only letters, numbers, and underscores, and start with a letter or underscore.",
686
+ "Invalid table name. Use only letters, numbers, underscores, and an optional schema prefix, and start each identifier with a letter or underscore.",
672
687
  executionTimeMs: Number(endTime) - Number(startTimeMs),
673
688
  };
674
689
  }
@@ -12,28 +12,25 @@ import {
12
12
  resolveConfig,
13
13
  } from "./shared";
14
14
 
15
- type LogLine = {
16
- readonly timestamp: string;
17
- readonly line: string;
18
- readonly body?: string;
19
- readonly severity?: string;
20
- readonly attributes?: Record<string, unknown>;
21
- readonly labels: Record<string, string>;
22
- };
23
-
24
- type StructuredLogLine = {
25
- readonly body?: string;
26
- readonly severity?: string;
27
- readonly attributes?: Record<string, unknown>;
28
- };
15
+ import type { LogLine, StructuredLogLine } from "./types";
16
+
17
+ function isStringRecord(value: unknown): value is Record<string, string> {
18
+ return (
19
+ typeof value === "object" &&
20
+ value !== null &&
21
+ !Array.isArray(value) &&
22
+ Object.values(value).every((item) => typeof item === "string")
23
+ );
24
+ }
29
25
 
30
26
  function parseLabel(value: string | Record<string, string>): Record<string, string> {
31
- if (typeof value === "object" && value !== null) {
27
+ if (isStringRecord(value)) {
32
28
  return value;
33
29
  }
34
30
 
35
31
  try {
36
- return JSON.parse(value) as Record<string, string>;
32
+ const parsed = JSON.parse(value) as unknown;
33
+ return isStringRecord(parsed) ? parsed : {};
37
34
  } catch {
38
35
  return {};
39
36
  }
@@ -64,7 +61,7 @@ export function extractLogsFromDsQuery(response: {
64
61
  results: {
65
62
  A: {
66
63
  frames?: Array<{
67
- schema: { fields: Array<{ name: string; type?: string }> };
64
+ schema: { fields: Array<{ name: string; type?: string; labels?: Record<string, string> }> };
68
65
  data: { values: unknown[][] };
69
66
  }>;
70
67
  };
@@ -81,15 +78,14 @@ export function extractLogsFromDsQuery(response: {
81
78
  const lineIndex = fields.findIndex(
82
79
  (field) => field.name === "body" || field.name === "Line" || field.name === "line",
83
80
  );
84
- const labelsIndex = fields.findIndex(
85
- (field) => field.name === "labels" || field.name === "labelTypes",
86
- );
81
+ const labelsIndex = fields.findIndex((field) => field.name === "labels");
87
82
 
88
83
  const timestamps = (timeIndex >= 0 ? values[timeIndex] : []) as Array<string | number>;
89
84
  const lines = (lineIndex >= 0 ? values[lineIndex] : []) as string[];
90
85
  const labelValues = (labelsIndex >= 0 ? values[labelsIndex] : []) as Array<
91
86
  string | Record<string, string>
92
87
  >;
88
+ const streamLabels = lineIndex >= 0 ? fields[lineIndex]?.labels : undefined;
93
89
 
94
90
  for (const [index, line] of lines.entries()) {
95
91
  const structured = parseStructuredLogLine(line);
@@ -97,7 +93,7 @@ export function extractLogsFromDsQuery(response: {
97
93
  timestamp: String(timestamps[index] ?? ""),
98
94
  line,
99
95
  ...structured,
100
- labels: labelValues[index] ? parseLabel(labelValues[index]) : {},
96
+ labels: labelValues[index] ? parseLabel(labelValues[index]) : (streamLabels ?? {}),
101
97
  });
102
98
  }
103
99
  }
@@ -163,7 +159,7 @@ const queryCommand = Command.make(
163
159
 
164
160
  yield* Console.log(formatOutput(result, format));
165
161
  }).pipe(
166
- Effect.catch((error) =>
162
+ Effect.catchTag("ObservabilityToolError", (error) =>
167
163
  Effect.gen(function* () {
168
164
  const result = {
169
165
  success: false,
@@ -8,6 +8,21 @@ export type ObservabilityEnvConfig = {
8
8
  tempoUid: string;
9
9
  };
10
10
 
11
+ export type LogLine = {
12
+ readonly timestamp: string;
13
+ readonly line: string;
14
+ readonly body?: string;
15
+ readonly severity?: string;
16
+ readonly attributes?: Record<string, unknown>;
17
+ readonly labels: Record<string, string>;
18
+ };
19
+
20
+ export type StructuredLogLine = {
21
+ readonly body?: string;
22
+ readonly severity?: string;
23
+ readonly attributes?: Record<string, unknown>;
24
+ };
25
+
11
26
  export type GrafanaDatasource = {
12
27
  uid: string;
13
28
  name: string;