postgresai 0.16.0-rc.4 → 0.16.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.
Files changed (85) hide show
  1. package/README.md +154 -0
  2. package/dist/bin/postgres-ai.js +2911 -255
  3. package/package.json +12 -3
  4. package/schemas/A002.schema.json +63 -0
  5. package/schemas/A003.schema.json +73 -0
  6. package/schemas/A004.schema.json +81 -0
  7. package/schemas/A007.schema.json +71 -0
  8. package/schemas/A013.schema.json +61 -0
  9. package/schemas/D001.schema.json +71 -0
  10. package/schemas/D004.schema.json +136 -0
  11. package/schemas/F001.schema.json +73 -0
  12. package/schemas/F002.schema.json +108 -0
  13. package/schemas/F003.schema.json +138 -0
  14. package/schemas/F004.schema.json +125 -0
  15. package/schemas/F005.schema.json +131 -0
  16. package/schemas/F009.schema.json +155 -0
  17. package/schemas/G001.schema.json +135 -0
  18. package/schemas/G003.schema.json +90 -0
  19. package/schemas/H001.schema.json +141 -0
  20. package/schemas/H002.schema.json +129 -0
  21. package/schemas/H004.schema.json +128 -0
  22. package/schemas/I001.schema.json +149 -0
  23. package/schemas/K001.schema.json +161 -0
  24. package/schemas/K003.schema.json +163 -0
  25. package/schemas/K004.schema.json +110 -0
  26. package/schemas/K005.schema.json +110 -0
  27. package/schemas/K006.schema.json +110 -0
  28. package/schemas/K007.schema.json +110 -0
  29. package/schemas/K008.schema.json +110 -0
  30. package/schemas/M001.schema.json +119 -0
  31. package/schemas/M002.schema.json +110 -0
  32. package/schemas/M003.schema.json +128 -0
  33. package/schemas/N001.schema.json +161 -0
  34. package/schemas/query.schema.json +62 -0
  35. package/CHANGELOG.md +0 -11
  36. package/bin/postgres-ai.ts +0 -5578
  37. package/bun.lock +0 -258
  38. package/bunfig.toml +0 -20
  39. package/lib/aas-onboard.ts +0 -251
  40. package/lib/auth-server.ts +0 -285
  41. package/lib/checkup-api.ts +0 -526
  42. package/lib/checkup-dictionary.ts +0 -103
  43. package/lib/checkup-summary.ts +0 -338
  44. package/lib/checkup.ts +0 -2261
  45. package/lib/config.ts +0 -171
  46. package/lib/init.ts +0 -1152
  47. package/lib/instances.ts +0 -245
  48. package/lib/issues.ts +0 -1060
  49. package/lib/mcp-server.ts +0 -667
  50. package/lib/metrics-loader.ts +0 -134
  51. package/lib/pkce.ts +0 -79
  52. package/lib/reports.ts +0 -373
  53. package/lib/storage.ts +0 -367
  54. package/lib/supabase.ts +0 -826
  55. package/lib/util.ts +0 -134
  56. package/packages/postgres-ai/README.md +0 -26
  57. package/packages/postgres-ai/bin/postgres-ai.js +0 -27
  58. package/packages/postgres-ai/package.json +0 -27
  59. package/scripts/embed-checkup-dictionary.ts +0 -115
  60. package/scripts/embed-metrics.ts +0 -160
  61. package/scripts/generate-release-notes.ts +0 -668
  62. package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
  63. package/test/aas-onboard.test.ts +0 -301
  64. package/test/auth.test.ts +0 -287
  65. package/test/checkup.integration.test.ts +0 -413
  66. package/test/checkup.test.ts +0 -3626
  67. package/test/compose-cmd.test.ts +0 -120
  68. package/test/config-consistency.test.ts +0 -352
  69. package/test/init.integration.test.ts +0 -438
  70. package/test/init.test.ts +0 -1816
  71. package/test/issues.cli.test.ts +0 -1162
  72. package/test/issues.test.ts +0 -456
  73. package/test/mcp-server.test.ts +0 -2530
  74. package/test/monitoring.test.ts +0 -746
  75. package/test/permission-check-sql.test.ts +0 -116
  76. package/test/reports.cli.test.ts +0 -793
  77. package/test/reports.test.ts +0 -977
  78. package/test/schema-validation.test.ts +0 -231
  79. package/test/storage.test.ts +0 -935
  80. package/test/supabase.test.ts +0 -709
  81. package/test/targets-add-config.test.ts +0 -28
  82. package/test/test-utils.ts +0 -190
  83. package/test/upgrade.test.ts +0 -1056
  84. package/test/util.test.ts +0 -44
  85. package/tsconfig.json +0 -20
@@ -1,134 +0,0 @@
1
- /**
2
- * Metrics loader for express checkup reports
3
- *
4
- * Loads SQL queries from embedded metrics data (generated from metrics.yml at build time).
5
- * Provides version-aware query selection and row transformation utilities.
6
- */
7
-
8
- import { METRICS, MetricDefinition } from "./metrics-embedded";
9
-
10
- /**
11
- * Get SQL query for a specific metric, selecting the appropriate version.
12
- *
13
- * @param metricName - Name of the metric (e.g., "settings", "db_stats")
14
- * @param pgMajorVersion - PostgreSQL major version (default: 16)
15
- * @returns SQL query string
16
- * @throws Error if metric not found or no compatible version available
17
- */
18
- export function getMetricSql(metricName: string, pgMajorVersion: number = 16): string {
19
- const metric = METRICS[metricName];
20
-
21
- if (!metric) {
22
- throw new Error(`Metric "${metricName}" not found. Available metrics: ${Object.keys(METRICS).join(", ")}`);
23
- }
24
-
25
- // Find the best matching version: highest version <= pgMajorVersion
26
- const availableVersions = Object.keys(metric.sqls)
27
- .map(v => parseInt(v, 10))
28
- .sort((a, b) => b - a); // Sort descending
29
-
30
- const matchingVersion = availableVersions.find(v => v <= pgMajorVersion);
31
-
32
- if (matchingVersion === undefined) {
33
- throw new Error(
34
- `No compatible SQL version for metric "${metricName}" with PostgreSQL ${pgMajorVersion}. ` +
35
- `Available versions: ${availableVersions.join(", ")}`
36
- );
37
- }
38
-
39
- return metric.sqls[matchingVersion];
40
- }
41
-
42
- /**
43
- * Get metric definition including all metadata.
44
- *
45
- * @param metricName - Name of the metric
46
- * @returns MetricDefinition or undefined if not found
47
- */
48
- export function getMetricDefinition(metricName: string): MetricDefinition | undefined {
49
- return METRICS[metricName];
50
- }
51
-
52
- /**
53
- * List all available metric names.
54
- */
55
- export function listMetricNames(): string[] {
56
- return Object.keys(METRICS);
57
- }
58
-
59
- /**
60
- * Metric names that correspond to express report checks.
61
- * Maps check IDs and logical names to metric names in the METRICS object.
62
- */
63
- export const METRIC_NAMES = {
64
- // Index health checks
65
- H001: "pg_invalid_indexes",
66
- H002: "unused_indexes",
67
- H004: "redundant_indexes",
68
- // Dead tuples and per-table autovacuum overrides
69
- F003: "pg_dead_tuples",
70
- // Bloat estimation
71
- F004: "pg_table_bloat",
72
- F005: "pg_btree_bloat",
73
- // Settings and version info (A002, A003, A007, A013)
74
- settings: "settings",
75
- // Database statistics (A004)
76
- dbStats: "db_stats",
77
- dbSize: "db_size",
78
- // Stats reset info (H002)
79
- statsReset: "stats_reset",
80
- // I/O statistics (I001) - PostgreSQL 16+
81
- I001: "pg_stat_io",
82
- } as const;
83
-
84
- /**
85
- * Transform a row from metrics query output to JSON report format.
86
- * Metrics use `tag_` prefix for dimensions; we strip it for JSON reports.
87
- * Also removes Prometheus-specific fields like epoch_ns, num, tag_datname.
88
- */
89
- export function transformMetricRow(row: Record<string, unknown>): Record<string, unknown> {
90
- const result: Record<string, unknown> = {};
91
-
92
- for (const [key, value] of Object.entries(row)) {
93
- // Skip Prometheus-specific fields
94
- if (key === "epoch_ns" || key === "num" || key === "tag_datname") {
95
- continue;
96
- }
97
-
98
- // Strip tag_ prefix
99
- const newKey = key.startsWith("tag_") ? key.slice(4) : key;
100
- result[newKey] = value;
101
- }
102
-
103
- return result;
104
- }
105
-
106
- /**
107
- * Transform settings metric row to the format expected by express reports.
108
- * The settings metric returns one row per setting with tag_setting_name as key.
109
- */
110
- export function transformSettingsRow(row: Record<string, unknown>): {
111
- name: string;
112
- setting: string;
113
- unit: string;
114
- category: string;
115
- vartype: string;
116
- is_default: boolean;
117
- } {
118
- return {
119
- name: String(row.tag_setting_name || ""),
120
- setting: String(row.tag_setting_value || ""),
121
- unit: String(row.tag_unit || ""),
122
- category: String(row.tag_category || ""),
123
- vartype: String(row.tag_vartype || ""),
124
- is_default: row.is_default === 1 || row.is_default === true,
125
- };
126
- }
127
-
128
- // Re-export types for convenience
129
- export type { MetricDefinition } from "./metrics-embedded";
130
-
131
- // Legacy export for backward compatibility
132
- export function loadMetricsYml(): { metrics: Record<string, unknown> } {
133
- return { metrics: METRICS };
134
- }
package/lib/pkce.ts DELETED
@@ -1,79 +0,0 @@
1
- import * as crypto from "crypto";
2
-
3
- /**
4
- * PKCE parameters for OAuth 2.0 Authorization Code Flow with PKCE
5
- */
6
- export interface PKCEParams {
7
- codeVerifier: string;
8
- codeChallenge: string;
9
- codeChallengeMethod: "S256";
10
- state: string;
11
- }
12
-
13
- /**
14
- * Generate a cryptographically random string for PKCE
15
- * @param length - Length of the string (43-128 characters per RFC 7636)
16
- * @returns Base64URL-encoded random string
17
- */
18
- function generateRandomString(length: number = 64): string {
19
- const bytes = crypto.randomBytes(length);
20
- return base64URLEncode(bytes);
21
- }
22
-
23
- /**
24
- * Base64URL encode (without padding)
25
- * @param buffer - Buffer to encode
26
- * @returns Base64URL-encoded string
27
- */
28
- function base64URLEncode(buffer: Buffer): string {
29
- return buffer
30
- .toString("base64")
31
- .replace(/\+/g, "-")
32
- .replace(/\//g, "_")
33
- .replace(/=/g, "");
34
- }
35
-
36
- /**
37
- * Generate PKCE code verifier
38
- * @returns Random code verifier (43-128 characters)
39
- */
40
- export function generateCodeVerifier(): string {
41
- return generateRandomString(32); // 32 bytes = 43 chars after base64url encoding
42
- }
43
-
44
- /**
45
- * Generate PKCE code challenge from verifier
46
- * Uses S256 method (SHA256)
47
- * @param verifier - Code verifier string
48
- * @returns Base64URL-encoded SHA256 hash of verifier
49
- */
50
- export function generateCodeChallenge(verifier: string): string {
51
- const hash = crypto.createHash("sha256").update(verifier).digest();
52
- return base64URLEncode(hash);
53
- }
54
-
55
- /**
56
- * Generate random state for CSRF protection
57
- * @returns Random state string
58
- */
59
- export function generateState(): string {
60
- return generateRandomString(16); // 16 bytes = 22 chars
61
- }
62
-
63
- /**
64
- * Generate complete PKCE parameters
65
- * @returns Object with verifier, challenge, challengeMethod, and state
66
- */
67
- export function generatePKCEParams(): PKCEParams {
68
- const verifier = generateCodeVerifier();
69
- const challenge = generateCodeChallenge(verifier);
70
- const state = generateState();
71
-
72
- return {
73
- codeVerifier: verifier,
74
- codeChallenge: challenge,
75
- codeChallengeMethod: "S256",
76
- state: state,
77
- };
78
- }
79
-
package/lib/reports.ts DELETED
@@ -1,373 +0,0 @@
1
- import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
2
-
3
- // ============================================================================
4
- // Types
5
- // ============================================================================
6
-
7
- export interface CheckupReport {
8
- id: number;
9
- org_id: number;
10
- org_name: string;
11
- project_id: number;
12
- project_name: string;
13
- created_at: string;
14
- created_formatted: string;
15
- epoch: number;
16
- status: string;
17
- }
18
-
19
- export interface CheckupReportFile {
20
- id: number;
21
- checkup_report_id: number;
22
- filename: string;
23
- check_id: string;
24
- type: "json" | "md";
25
- created_at: string;
26
- created_formatted: string;
27
- project_id: number;
28
- project_name: string;
29
- }
30
-
31
- export interface CheckupReportFileData extends CheckupReportFile {
32
- data: string;
33
- }
34
-
35
- // ============================================================================
36
- // Date parsing
37
- // ============================================================================
38
-
39
- /**
40
- * Parse a date string in various formats into an ISO 8601 string.
41
- * Supported formats:
42
- * YYYY-MM-DD 2025-01-15
43
- * YYYY-MM-DDTHH:mm:ss 2025-01-15T10:30:00
44
- * YYYY-MM-DD HH:mm:ss 2025-01-15 10:30:00
45
- * YYYY-MM-DD HH:mm 2025-01-15 10:30
46
- * DD.MM.YYYY 15.01.2025
47
- * DD.MM.YYYY HH:mm 15.01.2025 10:30
48
- * DD.MM.YYYY HH:mm:ss 15.01.2025 10:30:00
49
- */
50
- export function parseFlexibleDate(input: string): string {
51
- const s = input.trim();
52
-
53
- // DD.MM.YYYY [HH:mm[:ss]]
54
- const dotMatch = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
55
- if (dotMatch) {
56
- const [, dd, mm, yyyy, hh, min, ss] = dotMatch;
57
- const iso = `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}T${(hh ?? "00").padStart(2, "0")}:${(min ?? "00").padStart(2, "0")}:${(ss ?? "00").padStart(2, "0")}Z`;
58
- const d = new Date(iso);
59
- if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
60
- return d.toISOString();
61
- }
62
-
63
- // YYYY-MM-DD[T ]HH:mm[:ss] or YYYY-MM-DD
64
- const isoMatch = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
65
- if (isoMatch) {
66
- const [, yyyy, mm, dd, hh, min, ss] = isoMatch;
67
- const iso = `${yyyy}-${mm}-${dd}T${hh ?? "00"}:${min ?? "00"}:${ss ?? "00"}Z`;
68
- const d = new Date(iso);
69
- if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
70
- return d.toISOString();
71
- }
72
-
73
- throw new Error(`Unrecognized date format: ${input}. Use YYYY-MM-DD or DD.MM.YYYY`);
74
- }
75
-
76
- // ============================================================================
77
- // Params
78
- // ============================================================================
79
-
80
- export interface FetchReportsParams {
81
- apiKey: string;
82
- apiBaseUrl: string;
83
- projectId?: number;
84
- status?: string;
85
- limit?: number;
86
- beforeDate?: string;
87
- /** @internal Used by fetchAllReports for keyset pagination */
88
- beforeId?: number;
89
- debug?: boolean;
90
- }
91
-
92
- export interface FetchReportFilesParams {
93
- apiKey: string;
94
- apiBaseUrl: string;
95
- reportId?: number;
96
- type?: "json" | "md";
97
- checkId?: string;
98
- debug?: boolean;
99
- }
100
-
101
- export interface FetchReportFileDataParams {
102
- apiKey: string;
103
- apiBaseUrl: string;
104
- reportId?: number;
105
- type?: "json" | "md";
106
- checkId?: string;
107
- debug?: boolean;
108
- }
109
-
110
- // ============================================================================
111
- // API functions
112
- // ============================================================================
113
-
114
- export async function fetchReports(params: FetchReportsParams): Promise<CheckupReport[]> {
115
- const { apiKey, apiBaseUrl, projectId, status, limit = 20, beforeDate, beforeId, debug } = params;
116
- if (!apiKey) {
117
- throw new Error("API key is required");
118
- }
119
-
120
- const base = normalizeBaseUrl(apiBaseUrl);
121
- const url = new URL(`${base}/checkup_reports`);
122
- url.searchParams.set("order", "id.desc");
123
- url.searchParams.set("limit", String(limit));
124
- if (typeof projectId === "number") {
125
- url.searchParams.set("project_id", `eq.${projectId}`);
126
- }
127
- if (status) {
128
- url.searchParams.set("status", `eq.${status}`);
129
- }
130
- if (beforeDate) {
131
- url.searchParams.set("created_at", `lt.${beforeDate}`);
132
- }
133
- if (typeof beforeId === "number") {
134
- url.searchParams.set("id", `lt.${beforeId}`);
135
- }
136
-
137
- const headers: Record<string, string> = {
138
- "access-token": apiKey,
139
- "Prefer": "return=representation",
140
- "Content-Type": "application/json",
141
- "Connection": "close",
142
- };
143
-
144
- if (debug) {
145
- const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
146
- console.error(`Debug: Resolved API base URL: ${base}`);
147
- console.error(`Debug: GET URL: ${url.toString()}`);
148
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
149
- }
150
-
151
- const response = await fetch(url.toString(), { method: "GET", headers });
152
-
153
- if (debug) {
154
- console.error(`Debug: Response status: ${response.status}`);
155
- }
156
-
157
- const data = await response.text();
158
-
159
- if (response.ok) {
160
- try {
161
- return JSON.parse(data) as CheckupReport[];
162
- } catch {
163
- throw new Error(`Failed to parse reports response: ${data}`);
164
- }
165
- } else {
166
- throw new Error(formatHttpError("Failed to fetch reports", response.status, data));
167
- }
168
- }
169
-
170
- const MAX_ALL_REPORTS = 10000;
171
-
172
- export async function fetchAllReports(params: Omit<FetchReportsParams, "beforeId" | "beforeDate">): Promise<CheckupReport[]> {
173
- const pageSize = params.limit ?? 100;
174
- const all: CheckupReport[] = [];
175
- let beforeId: number | undefined;
176
-
177
- while (true) {
178
- const page = await fetchReports({ ...params, limit: pageSize, beforeId });
179
- if (page.length === 0) break;
180
- all.push(...page);
181
- if (all.length >= MAX_ALL_REPORTS) {
182
- console.warn(`Warning: reached maximum of ${MAX_ALL_REPORTS} reports, stopping pagination`);
183
- break;
184
- }
185
- beforeId = page[page.length - 1].id;
186
- if (page.length < pageSize) break;
187
- }
188
-
189
- return all;
190
- }
191
-
192
- export async function fetchReportFiles(params: FetchReportFilesParams): Promise<CheckupReportFile[]> {
193
- const { apiKey, apiBaseUrl, reportId, type, checkId, debug } = params;
194
- if (!apiKey) {
195
- throw new Error("API key is required");
196
- }
197
- if (reportId === undefined && !checkId) {
198
- throw new Error("Either reportId or checkId is required");
199
- }
200
-
201
- const base = normalizeBaseUrl(apiBaseUrl);
202
- const url = new URL(`${base}/checkup_report_files`);
203
- if (typeof reportId === "number") {
204
- url.searchParams.set("checkup_report_id", `eq.${reportId}`);
205
- }
206
- url.searchParams.set("order", "id.asc");
207
- if (type) {
208
- url.searchParams.set("type", `eq.${type}`);
209
- }
210
- if (checkId) {
211
- url.searchParams.set("check_id", `eq.${checkId}`);
212
- }
213
-
214
- const headers: Record<string, string> = {
215
- "access-token": apiKey,
216
- "Prefer": "return=representation",
217
- "Content-Type": "application/json",
218
- "Connection": "close",
219
- };
220
-
221
- if (debug) {
222
- const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
223
- console.error(`Debug: Resolved API base URL: ${base}`);
224
- console.error(`Debug: GET URL: ${url.toString()}`);
225
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
226
- }
227
-
228
- const response = await fetch(url.toString(), { method: "GET", headers });
229
-
230
- if (debug) {
231
- console.error(`Debug: Response status: ${response.status}`);
232
- }
233
-
234
- const data = await response.text();
235
-
236
- if (response.ok) {
237
- try {
238
- return JSON.parse(data) as CheckupReportFile[];
239
- } catch {
240
- throw new Error(`Failed to parse report files response: ${data}`);
241
- }
242
- } else {
243
- throw new Error(formatHttpError("Failed to fetch report files", response.status, data));
244
- }
245
- }
246
-
247
- export async function fetchReportFileData(params: FetchReportFileDataParams): Promise<CheckupReportFileData[]> {
248
- const { apiKey, apiBaseUrl, reportId, type, checkId, debug } = params;
249
- if (!apiKey) {
250
- throw new Error("API key is required");
251
- }
252
- if (reportId === undefined && !checkId) {
253
- throw new Error("Either reportId or checkId is required");
254
- }
255
-
256
- const base = normalizeBaseUrl(apiBaseUrl);
257
- const url = new URL(`${base}/checkup_report_file_data`);
258
- if (typeof reportId === "number") {
259
- url.searchParams.set("checkup_report_id", `eq.${reportId}`);
260
- }
261
- url.searchParams.set("order", "id.asc");
262
- if (type) {
263
- url.searchParams.set("type", `eq.${type}`);
264
- }
265
- if (checkId) {
266
- url.searchParams.set("check_id", `eq.${checkId}`);
267
- }
268
-
269
- const headers: Record<string, string> = {
270
- "access-token": apiKey,
271
- "Prefer": "return=representation",
272
- "Content-Type": "application/json",
273
- "Connection": "close",
274
- };
275
-
276
- if (debug) {
277
- const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
278
- console.error(`Debug: Resolved API base URL: ${base}`);
279
- console.error(`Debug: GET URL: ${url.toString()}`);
280
- console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
281
- }
282
-
283
- const response = await fetch(url.toString(), { method: "GET", headers });
284
-
285
- if (debug) {
286
- console.error(`Debug: Response status: ${response.status}`);
287
- }
288
-
289
- const data = await response.text();
290
-
291
- if (response.ok) {
292
- try {
293
- return JSON.parse(data) as CheckupReportFileData[];
294
- } catch {
295
- throw new Error(`Failed to parse report file data response: ${data}`);
296
- }
297
- } else {
298
- throw new Error(formatHttpError("Failed to fetch report file data", response.status, data));
299
- }
300
- }
301
-
302
- // ============================================================================
303
- // Lightweight markdown terminal renderer
304
- // ============================================================================
305
-
306
- export function renderMarkdownForTerminal(md: string): string {
307
- if (!md) return "";
308
-
309
- const RESET = "\x1b[0m";
310
- const BOLD = "\x1b[1m";
311
- const BOLD_UNDERLINE = "\x1b[1;4m";
312
- const DIM = "\x1b[2m";
313
- const ITALIC = "\x1b[3m";
314
- const CYAN = "\x1b[36m";
315
-
316
- const lines = md.split("\n");
317
- const output: string[] = [];
318
- let inCodeBlock = false;
319
-
320
- for (const line of lines) {
321
- // Code block toggle
322
- if (line.trimStart().startsWith("```")) {
323
- inCodeBlock = !inCodeBlock;
324
- if (inCodeBlock) {
325
- output.push(`${DIM}${"─".repeat(40)}${RESET}`);
326
- } else {
327
- output.push(`${DIM}${"─".repeat(40)}${RESET}`);
328
- }
329
- continue;
330
- }
331
-
332
- // Inside code block — dim output
333
- if (inCodeBlock) {
334
- output.push(`${DIM} ${line}${RESET}`);
335
- continue;
336
- }
337
-
338
- // Horizontal rule
339
- if (/^-{3,}$/.test(line.trim()) || /^\*{3,}$/.test(line.trim()) || /^_{3,}$/.test(line.trim())) {
340
- output.push(`${DIM}${"─".repeat(60)}${RESET}`);
341
- continue;
342
- }
343
-
344
- // Headings
345
- const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
346
- if (headingMatch) {
347
- const level = headingMatch[1].length;
348
- const text = headingMatch[2];
349
- if (level === 1) {
350
- output.push(`${BOLD_UNDERLINE}${text}${RESET}`);
351
- } else {
352
- output.push(`${BOLD}${text}${RESET}`);
353
- }
354
- continue;
355
- }
356
-
357
- // Inline formatting
358
- let formatted = line;
359
- // Bold: **text** or __text__
360
- formatted = formatted.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`);
361
- formatted = formatted.replace(/__(.+?)__/g, `${BOLD}$1${RESET}`);
362
- // Italic: *text* (only single, not inside **)
363
- formatted = formatted.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `${ITALIC}$1${RESET}`);
364
- // Italic: _text_ — only at word boundaries (not inside identifiers like foo_bar_baz)
365
- formatted = formatted.replace(/(?<=^|[\s(])_([^\s_](?:.*?[^\s_])?)_(?=$|[\s),.:;!?])/g, `${ITALIC}$1${RESET}`);
366
- // Inline code: `text`
367
- formatted = formatted.replace(/`([^`]+)`/g, `${CYAN}$1${RESET}`);
368
-
369
- output.push(formatted);
370
- }
371
-
372
- return output.join("\n");
373
- }