db-mcp 1.0.1
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 +21 -0
- package/README.md +860 -0
- package/dist/adapters/DatabaseAdapter.d.ts +141 -0
- package/dist/adapters/DatabaseAdapter.d.ts.map +1 -0
- package/dist/adapters/DatabaseAdapter.js +131 -0
- package/dist/adapters/DatabaseAdapter.js.map +1 -0
- package/dist/adapters/sqlite/SchemaManager.d.ts +58 -0
- package/dist/adapters/sqlite/SchemaManager.d.ts.map +1 -0
- package/dist/adapters/sqlite/SchemaManager.js +187 -0
- package/dist/adapters/sqlite/SchemaManager.js.map +1 -0
- package/dist/adapters/sqlite/SqliteAdapter.d.ts +161 -0
- package/dist/adapters/sqlite/SqliteAdapter.d.ts.map +1 -0
- package/dist/adapters/sqlite/SqliteAdapter.js +741 -0
- package/dist/adapters/sqlite/SqliteAdapter.js.map +1 -0
- package/dist/adapters/sqlite/index.d.ts +9 -0
- package/dist/adapters/sqlite/index.d.ts.map +1 -0
- package/dist/adapters/sqlite/index.js +8 -0
- package/dist/adapters/sqlite/index.js.map +1 -0
- package/dist/adapters/sqlite/json-utils.d.ts +100 -0
- package/dist/adapters/sqlite/json-utils.d.ts.map +1 -0
- package/dist/adapters/sqlite/json-utils.js +274 -0
- package/dist/adapters/sqlite/json-utils.js.map +1 -0
- package/dist/adapters/sqlite/output-schemas.d.ts +1187 -0
- package/dist/adapters/sqlite/output-schemas.d.ts.map +1 -0
- package/dist/adapters/sqlite/output-schemas.js +1337 -0
- package/dist/adapters/sqlite/output-schemas.js.map +1 -0
- package/dist/adapters/sqlite/prompts.d.ts +13 -0
- package/dist/adapters/sqlite/prompts.d.ts.map +1 -0
- package/dist/adapters/sqlite/prompts.js +605 -0
- package/dist/adapters/sqlite/prompts.js.map +1 -0
- package/dist/adapters/sqlite/resources.d.ts +13 -0
- package/dist/adapters/sqlite/resources.d.ts.map +1 -0
- package/dist/adapters/sqlite/resources.js +251 -0
- package/dist/adapters/sqlite/resources.js.map +1 -0
- package/dist/adapters/sqlite/tools/admin.d.ts +14 -0
- package/dist/adapters/sqlite/tools/admin.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/admin.js +788 -0
- package/dist/adapters/sqlite/tools/admin.js.map +1 -0
- package/dist/adapters/sqlite/tools/core.d.ts +25 -0
- package/dist/adapters/sqlite/tools/core.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/core.js +359 -0
- package/dist/adapters/sqlite/tools/core.js.map +1 -0
- package/dist/adapters/sqlite/tools/fts.d.ts +13 -0
- package/dist/adapters/sqlite/tools/fts.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/fts.js +347 -0
- package/dist/adapters/sqlite/tools/fts.js.map +1 -0
- package/dist/adapters/sqlite/tools/geo.d.ts +14 -0
- package/dist/adapters/sqlite/tools/geo.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/geo.js +252 -0
- package/dist/adapters/sqlite/tools/geo.js.map +1 -0
- package/dist/adapters/sqlite/tools/index.d.ts +30 -0
- package/dist/adapters/sqlite/tools/index.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/index.js +61 -0
- package/dist/adapters/sqlite/tools/index.js.map +1 -0
- package/dist/adapters/sqlite/tools/json-helpers.d.ts +14 -0
- package/dist/adapters/sqlite/tools/json-helpers.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/json-helpers.js +477 -0
- package/dist/adapters/sqlite/tools/json-helpers.js.map +1 -0
- package/dist/adapters/sqlite/tools/json-operations.d.ts +14 -0
- package/dist/adapters/sqlite/tools/json-operations.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/json-operations.js +839 -0
- package/dist/adapters/sqlite/tools/json-operations.js.map +1 -0
- package/dist/adapters/sqlite/tools/stats.d.ts +15 -0
- package/dist/adapters/sqlite/tools/stats.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/stats.js +1219 -0
- package/dist/adapters/sqlite/tools/stats.js.map +1 -0
- package/dist/adapters/sqlite/tools/text.d.ts +14 -0
- package/dist/adapters/sqlite/tools/text.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/text.js +1141 -0
- package/dist/adapters/sqlite/tools/text.js.map +1 -0
- package/dist/adapters/sqlite/tools/vector.d.ts +14 -0
- package/dist/adapters/sqlite/tools/vector.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/vector.js +613 -0
- package/dist/adapters/sqlite/tools/vector.js.map +1 -0
- package/dist/adapters/sqlite/tools/virtual.d.ts +13 -0
- package/dist/adapters/sqlite/tools/virtual.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/virtual.js +930 -0
- package/dist/adapters/sqlite/tools/virtual.js.map +1 -0
- package/dist/adapters/sqlite/types.d.ts +207 -0
- package/dist/adapters/sqlite/types.d.ts.map +1 -0
- package/dist/adapters/sqlite/types.js +186 -0
- package/dist/adapters/sqlite/types.js.map +1 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts +163 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js +748 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js.map +1 -0
- package/dist/adapters/sqlite-native/index.d.ts +11 -0
- package/dist/adapters/sqlite-native/index.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/index.js +11 -0
- package/dist/adapters/sqlite-native/index.js.map +1 -0
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts +19 -0
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/spatialite.js +628 -0
- package/dist/adapters/sqlite-native/tools/spatialite.js.map +1 -0
- package/dist/adapters/sqlite-native/tools/transactions.d.ts +12 -0
- package/dist/adapters/sqlite-native/tools/transactions.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/transactions.js +255 -0
- package/dist/adapters/sqlite-native/tools/transactions.js.map +1 -0
- package/dist/adapters/sqlite-native/tools/window.d.ts +12 -0
- package/dist/adapters/sqlite-native/tools/window.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window.js +370 -0
- package/dist/adapters/sqlite-native/tools/window.js.map +1 -0
- package/dist/auth/AuthorizationServerDiscovery.d.ts +90 -0
- package/dist/auth/AuthorizationServerDiscovery.d.ts.map +1 -0
- package/dist/auth/AuthorizationServerDiscovery.js +204 -0
- package/dist/auth/AuthorizationServerDiscovery.js.map +1 -0
- package/dist/auth/OAuthResourceServer.d.ts +65 -0
- package/dist/auth/OAuthResourceServer.d.ts.map +1 -0
- package/dist/auth/OAuthResourceServer.js +121 -0
- package/dist/auth/OAuthResourceServer.js.map +1 -0
- package/dist/auth/TokenValidator.d.ts +60 -0
- package/dist/auth/TokenValidator.d.ts.map +1 -0
- package/dist/auth/TokenValidator.js +235 -0
- package/dist/auth/TokenValidator.js.map +1 -0
- package/dist/auth/errors.d.ts +74 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +133 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +81 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +291 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/scopes.d.ts +136 -0
- package/dist/auth/scopes.d.ts.map +1 -0
- package/dist/auth/scopes.js +349 -0
- package/dist/auth/scopes.js.map +1 -0
- package/dist/auth/types.d.ts +257 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +8 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +236 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants/ServerInstructions.d.ts +45 -0
- package/dist/constants/ServerInstructions.d.ts.map +1 -0
- package/dist/constants/ServerInstructions.js +356 -0
- package/dist/constants/ServerInstructions.js.map +1 -0
- package/dist/filtering/ToolConstants.d.ts +34 -0
- package/dist/filtering/ToolConstants.d.ts.map +1 -0
- package/dist/filtering/ToolConstants.js +174 -0
- package/dist/filtering/ToolConstants.js.map +1 -0
- package/dist/filtering/ToolFilter.d.ts +82 -0
- package/dist/filtering/ToolFilter.d.ts.map +1 -0
- package/dist/filtering/ToolFilter.js +296 -0
- package/dist/filtering/ToolFilter.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/server/McpServer.d.ts +61 -0
- package/dist/server/McpServer.d.ts.map +1 -0
- package/dist/server/McpServer.js +270 -0
- package/dist/server/McpServer.js.map +1 -0
- package/dist/transports/http.d.ts +134 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +516 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/index.d.ts +5 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +5 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/types/index.d.ts +380 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +68 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/annotations.d.ts +44 -0
- package/dist/utils/annotations.d.ts.map +1 -0
- package/dist/utils/annotations.js +77 -0
- package/dist/utils/annotations.js.map +1 -0
- package/dist/utils/errors.d.ts +155 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +329 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/identifiers.d.ts +121 -0
- package/dist/utils/identifiers.d.ts.map +1 -0
- package/dist/utils/identifiers.js +319 -0
- package/dist/utils/identifiers.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/insightsManager.d.ts +39 -0
- package/dist/utils/insightsManager.d.ts.map +1 -0
- package/dist/utils/insightsManager.js +63 -0
- package/dist/utils/insightsManager.js.map +1 -0
- package/dist/utils/logger.d.ts +189 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +394 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/progress-utils.d.ts +54 -0
- package/dist/utils/progress-utils.d.ts.map +1 -0
- package/dist/utils/progress-utils.js +74 -0
- package/dist/utils/progress-utils.js.map +1 -0
- package/dist/utils/resourceAnnotations.d.ts +36 -0
- package/dist/utils/resourceAnnotations.d.ts.map +1 -0
- package/dist/utils/resourceAnnotations.js +57 -0
- package/dist/utils/resourceAnnotations.js.map +1 -0
- package/dist/utils/where-clause.d.ts +41 -0
- package/dist/utils/where-clause.d.ts.map +1 -0
- package/dist/utils/where-clause.js +116 -0
- package/dist/utils/where-clause.js.map +1 -0
- package/package.json +83 -0
- package/server.json +53 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Text Processing Tools
|
|
3
|
+
*
|
|
4
|
+
* String manipulation and pattern matching:
|
|
5
|
+
* regex, split, concat, format, fuzzy match, phonetic, normalize, validate.
|
|
6
|
+
* 13 tools total.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { readOnly, write } from "../../../utils/annotations.js";
|
|
10
|
+
import { validateWhereClause, sanitizeIdentifier, } from "../../../utils/index.js";
|
|
11
|
+
import { RegexMatchOutputSchema, TextSplitOutputSchema, RegexReplaceOutputSchema, } from "../output-schemas.js";
|
|
12
|
+
// Text tool schemas
|
|
13
|
+
const RegexExtractSchema = z.object({
|
|
14
|
+
table: z.string().describe("Table name"),
|
|
15
|
+
column: z.string().describe("Column to extract from"),
|
|
16
|
+
pattern: z.string().describe("Regular expression pattern"),
|
|
17
|
+
groupIndex: z.number().optional().default(0).describe("Capture group index"),
|
|
18
|
+
whereClause: z.string().optional(),
|
|
19
|
+
limit: z.number().optional().default(100),
|
|
20
|
+
});
|
|
21
|
+
const RegexMatchSchema = z.object({
|
|
22
|
+
table: z.string().describe("Table name"),
|
|
23
|
+
column: z.string().describe("Column to match"),
|
|
24
|
+
pattern: z.string().describe("Regular expression pattern"),
|
|
25
|
+
whereClause: z.string().optional(),
|
|
26
|
+
limit: z.number().optional().default(100),
|
|
27
|
+
});
|
|
28
|
+
const TextSplitSchema = z.object({
|
|
29
|
+
table: z.string().describe("Table name"),
|
|
30
|
+
column: z.string().describe("Column to split"),
|
|
31
|
+
delimiter: z.string().describe("Delimiter string"),
|
|
32
|
+
whereClause: z.string().optional(),
|
|
33
|
+
limit: z.number().optional().default(100),
|
|
34
|
+
});
|
|
35
|
+
const TextConcatSchema = z.object({
|
|
36
|
+
table: z.string().describe("Table name"),
|
|
37
|
+
columns: z.array(z.string()).describe("Columns to concatenate"),
|
|
38
|
+
separator: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.default("")
|
|
42
|
+
.describe("Separator between values"),
|
|
43
|
+
whereClause: z.string().optional(),
|
|
44
|
+
limit: z.number().optional().default(100),
|
|
45
|
+
});
|
|
46
|
+
const TextReplaceSchema = z.object({
|
|
47
|
+
table: z.string().describe("Table name"),
|
|
48
|
+
column: z.string().describe("Column to update"),
|
|
49
|
+
searchPattern: z.string().describe("Text to search for"),
|
|
50
|
+
replaceWith: z.string().describe("Replacement text"),
|
|
51
|
+
whereClause: z.string().describe("WHERE clause"),
|
|
52
|
+
});
|
|
53
|
+
const TextTrimSchema = z.object({
|
|
54
|
+
table: z.string().describe("Table name"),
|
|
55
|
+
column: z.string().describe("Column to trim"),
|
|
56
|
+
mode: z.enum(["both", "left", "right"]).optional().default("both"),
|
|
57
|
+
whereClause: z.string().optional(),
|
|
58
|
+
limit: z.number().optional().default(100),
|
|
59
|
+
});
|
|
60
|
+
const TextCaseSchema = z.object({
|
|
61
|
+
table: z.string().describe("Table name"),
|
|
62
|
+
column: z.string().describe("Column to transform"),
|
|
63
|
+
mode: z.enum(["upper", "lower"]).describe("Case transformation"),
|
|
64
|
+
whereClause: z.string().optional(),
|
|
65
|
+
limit: z.number().optional().default(100),
|
|
66
|
+
});
|
|
67
|
+
const TextSubstringSchema = z.object({
|
|
68
|
+
table: z.string().describe("Table name"),
|
|
69
|
+
column: z.string().describe("Column to extract from"),
|
|
70
|
+
start: z.number().describe("Start position (1-indexed)"),
|
|
71
|
+
length: z.number().optional().describe("Number of characters"),
|
|
72
|
+
whereClause: z.string().optional(),
|
|
73
|
+
limit: z.number().optional().default(100),
|
|
74
|
+
});
|
|
75
|
+
// New text tool schemas
|
|
76
|
+
const FuzzyMatchSchema = z.object({
|
|
77
|
+
table: z.string().describe("Table name"),
|
|
78
|
+
column: z.string().describe("Column to search"),
|
|
79
|
+
search: z.string().describe("Search string"),
|
|
80
|
+
maxDistance: z
|
|
81
|
+
.number()
|
|
82
|
+
.optional()
|
|
83
|
+
.default(3)
|
|
84
|
+
.describe("Maximum Levenshtein distance"),
|
|
85
|
+
tokenize: z
|
|
86
|
+
.boolean()
|
|
87
|
+
.optional()
|
|
88
|
+
.default(true)
|
|
89
|
+
.describe("Split column values into words and match against tokens (default: true). Set false to match entire column value."),
|
|
90
|
+
limit: z.number().optional().default(10),
|
|
91
|
+
});
|
|
92
|
+
const PhoneticMatchSchema = z.object({
|
|
93
|
+
table: z.string().describe("Table name"),
|
|
94
|
+
column: z.string().describe("Column to search"),
|
|
95
|
+
search: z.string().describe("Search string"),
|
|
96
|
+
algorithm: z.enum(["soundex", "metaphone"]).optional().default("soundex"),
|
|
97
|
+
limit: z.number().optional().default(100),
|
|
98
|
+
includeRowData: z
|
|
99
|
+
.boolean()
|
|
100
|
+
.optional()
|
|
101
|
+
.default(true)
|
|
102
|
+
.describe("Include full row data in results (default: true)"),
|
|
103
|
+
});
|
|
104
|
+
const TextNormalizeSchema = z.object({
|
|
105
|
+
table: z.string().describe("Table name"),
|
|
106
|
+
column: z.string().describe("Column to normalize"),
|
|
107
|
+
mode: z
|
|
108
|
+
.enum(["nfc", "nfd", "nfkc", "nfkd", "strip_accents"])
|
|
109
|
+
.describe("Normalization mode"),
|
|
110
|
+
whereClause: z.string().optional(),
|
|
111
|
+
limit: z.number().optional().default(100),
|
|
112
|
+
});
|
|
113
|
+
const TextValidateSchema = z.object({
|
|
114
|
+
table: z.string().describe("Table name"),
|
|
115
|
+
column: z.string().describe("Column to validate"),
|
|
116
|
+
pattern: z
|
|
117
|
+
.enum(["email", "phone", "url", "uuid", "ipv4", "custom"])
|
|
118
|
+
.describe("Validation pattern"),
|
|
119
|
+
customPattern: z
|
|
120
|
+
.string()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Custom regex (required if pattern=custom)"),
|
|
123
|
+
whereClause: z.string().optional(),
|
|
124
|
+
limit: z.number().optional().default(100),
|
|
125
|
+
});
|
|
126
|
+
const AdvancedSearchSchema = z.object({
|
|
127
|
+
table: z.string().describe("Table name"),
|
|
128
|
+
column: z.string().describe("Column to search"),
|
|
129
|
+
searchTerm: z.string().describe("Search term"),
|
|
130
|
+
techniques: z
|
|
131
|
+
.array(z.enum(["exact", "fuzzy", "phonetic"]))
|
|
132
|
+
.optional()
|
|
133
|
+
.default(["exact", "fuzzy", "phonetic"])
|
|
134
|
+
.describe("Search techniques to use"),
|
|
135
|
+
fuzzyThreshold: z
|
|
136
|
+
.number()
|
|
137
|
+
.optional()
|
|
138
|
+
.default(0.6)
|
|
139
|
+
.describe("Fuzzy match similarity threshold (0-1). Lower values are more lenient: 0.3-0.4 for loose matching (e.g., 'laptob' matches 'laptop'), 0.6-0.8 for strict matching."),
|
|
140
|
+
whereClause: z.string().optional(),
|
|
141
|
+
limit: z.number().optional().default(100),
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* Get all text processing tools
|
|
145
|
+
*/
|
|
146
|
+
export function getTextTools(adapter) {
|
|
147
|
+
return [
|
|
148
|
+
createRegexExtractTool(adapter),
|
|
149
|
+
createRegexMatchTool(adapter),
|
|
150
|
+
createTextSplitTool(adapter),
|
|
151
|
+
createTextConcatTool(adapter),
|
|
152
|
+
createTextReplaceTool(adapter),
|
|
153
|
+
createTextTrimTool(adapter),
|
|
154
|
+
createTextCaseTool(adapter),
|
|
155
|
+
createTextSubstringTool(adapter),
|
|
156
|
+
createFuzzyMatchTool(adapter),
|
|
157
|
+
createPhoneticMatchTool(adapter),
|
|
158
|
+
createTextNormalizeTool(adapter),
|
|
159
|
+
createTextValidateTool(adapter),
|
|
160
|
+
createAdvancedSearchTool(adapter),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extract text using regex pattern
|
|
165
|
+
* Note: SQLite doesn't have native regex, we do this in JS
|
|
166
|
+
*/
|
|
167
|
+
function createRegexExtractTool(adapter) {
|
|
168
|
+
return {
|
|
169
|
+
name: "sqlite_regex_extract",
|
|
170
|
+
description: "Extract text matching a regex pattern. Processed in JavaScript after fetching data.",
|
|
171
|
+
group: "text",
|
|
172
|
+
inputSchema: RegexExtractSchema,
|
|
173
|
+
outputSchema: RegexMatchOutputSchema,
|
|
174
|
+
requiredScopes: ["read"],
|
|
175
|
+
annotations: readOnly("Regex Extract"),
|
|
176
|
+
handler: async (params, _context) => {
|
|
177
|
+
const input = RegexExtractSchema.parse(params);
|
|
178
|
+
// Validate and quote identifiers
|
|
179
|
+
const table = sanitizeIdentifier(input.table);
|
|
180
|
+
const column = sanitizeIdentifier(input.column);
|
|
181
|
+
let sql = `SELECT rowid as id, ${column} as value FROM ${table}`;
|
|
182
|
+
if (input.whereClause) {
|
|
183
|
+
validateWhereClause(input.whereClause);
|
|
184
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
185
|
+
}
|
|
186
|
+
sql += ` LIMIT ${input.limit}`;
|
|
187
|
+
const result = await adapter.executeReadQuery(sql);
|
|
188
|
+
// Apply regex in JavaScript
|
|
189
|
+
const regex = new RegExp(input.pattern);
|
|
190
|
+
const extracts = (result.rows ?? [])
|
|
191
|
+
.map((row) => {
|
|
192
|
+
const rawValue = row["value"];
|
|
193
|
+
const value = typeof rawValue === "string"
|
|
194
|
+
? rawValue
|
|
195
|
+
: JSON.stringify(rawValue ?? "");
|
|
196
|
+
const match = regex.exec(value);
|
|
197
|
+
// Safely coerce rowid to number, defaulting to row index or 0
|
|
198
|
+
const rawRowid = row["id"];
|
|
199
|
+
const rowid = typeof rawRowid === "number"
|
|
200
|
+
? rawRowid
|
|
201
|
+
: typeof rawRowid === "string"
|
|
202
|
+
? parseInt(rawRowid, 10) || 0
|
|
203
|
+
: 0;
|
|
204
|
+
return {
|
|
205
|
+
rowid,
|
|
206
|
+
original: value,
|
|
207
|
+
extracted: match ? (match[input.groupIndex] ?? match[0]) : null,
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
.filter((r) => r.extracted !== null);
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
rowCount: extracts.length,
|
|
214
|
+
matches: extracts,
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Match rows using regex pattern
|
|
221
|
+
*/
|
|
222
|
+
function createRegexMatchTool(adapter) {
|
|
223
|
+
return {
|
|
224
|
+
name: "sqlite_regex_match",
|
|
225
|
+
description: "Find rows where column matches a regex pattern. Processed in JavaScript.",
|
|
226
|
+
group: "text",
|
|
227
|
+
inputSchema: RegexMatchSchema,
|
|
228
|
+
outputSchema: RegexMatchOutputSchema,
|
|
229
|
+
requiredScopes: ["read"],
|
|
230
|
+
annotations: readOnly("Regex Match"),
|
|
231
|
+
handler: async (params, _context) => {
|
|
232
|
+
const input = RegexMatchSchema.parse(params);
|
|
233
|
+
// Validate and quote identifiers
|
|
234
|
+
const table = sanitizeIdentifier(input.table);
|
|
235
|
+
const column = sanitizeIdentifier(input.column);
|
|
236
|
+
let sql = `SELECT rowid as id, ${column} as value FROM ${table}`;
|
|
237
|
+
if (input.whereClause) {
|
|
238
|
+
validateWhereClause(input.whereClause);
|
|
239
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
240
|
+
}
|
|
241
|
+
sql += ` LIMIT ${input.limit}`;
|
|
242
|
+
const result = await adapter.executeReadQuery(sql);
|
|
243
|
+
// Apply regex in JavaScript
|
|
244
|
+
const regex = new RegExp(input.pattern);
|
|
245
|
+
const matches = (result.rows ?? [])
|
|
246
|
+
.filter((row) => {
|
|
247
|
+
const rawValue = row["value"];
|
|
248
|
+
const value = typeof rawValue === "string"
|
|
249
|
+
? rawValue
|
|
250
|
+
: JSON.stringify(rawValue ?? "");
|
|
251
|
+
return regex.test(value);
|
|
252
|
+
})
|
|
253
|
+
.map((row) => {
|
|
254
|
+
// Ensure rowid is a number for output schema compliance
|
|
255
|
+
const rawRowid = row["id"];
|
|
256
|
+
const rowid = typeof rawRowid === "number"
|
|
257
|
+
? rawRowid
|
|
258
|
+
: typeof rawRowid === "string"
|
|
259
|
+
? parseInt(rawRowid, 10) || 0
|
|
260
|
+
: 0;
|
|
261
|
+
return { ...row, rowid };
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
success: true,
|
|
265
|
+
rowCount: matches.length,
|
|
266
|
+
matches,
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Split text into array
|
|
273
|
+
*/
|
|
274
|
+
function createTextSplitTool(adapter) {
|
|
275
|
+
return {
|
|
276
|
+
name: "sqlite_text_split",
|
|
277
|
+
description: "Split a text column by delimiter into array results.",
|
|
278
|
+
group: "text",
|
|
279
|
+
inputSchema: TextSplitSchema,
|
|
280
|
+
outputSchema: TextSplitOutputSchema,
|
|
281
|
+
requiredScopes: ["read"],
|
|
282
|
+
annotations: readOnly("Text Split"),
|
|
283
|
+
handler: async (params, _context) => {
|
|
284
|
+
const input = TextSplitSchema.parse(params);
|
|
285
|
+
// Validate and quote identifiers
|
|
286
|
+
const table = sanitizeIdentifier(input.table);
|
|
287
|
+
const column = sanitizeIdentifier(input.column);
|
|
288
|
+
let sql = `SELECT rowid as id, ${column} as value FROM ${table}`;
|
|
289
|
+
if (input.whereClause) {
|
|
290
|
+
validateWhereClause(input.whereClause);
|
|
291
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
292
|
+
}
|
|
293
|
+
sql += ` LIMIT ${input.limit}`;
|
|
294
|
+
const result = await adapter.executeReadQuery(sql);
|
|
295
|
+
// Split in JavaScript - return per-row results for traceability
|
|
296
|
+
const rows = (result.rows ?? []).map((row) => {
|
|
297
|
+
const rawRowid = row["id"];
|
|
298
|
+
const rowid = typeof rawRowid === "number"
|
|
299
|
+
? rawRowid
|
|
300
|
+
: typeof rawRowid === "string"
|
|
301
|
+
? parseInt(rawRowid, 10) || 0
|
|
302
|
+
: 0;
|
|
303
|
+
const rawValue = row["value"];
|
|
304
|
+
const original = typeof rawValue === "string"
|
|
305
|
+
? rawValue
|
|
306
|
+
: rawValue === null || rawValue === undefined
|
|
307
|
+
? ""
|
|
308
|
+
: JSON.stringify(rawValue);
|
|
309
|
+
const parts = original.split(input.delimiter);
|
|
310
|
+
return {
|
|
311
|
+
rowid,
|
|
312
|
+
original: rawValue === null ? null : original,
|
|
313
|
+
parts,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
rowCount: rows.length,
|
|
319
|
+
rows,
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Concatenate columns
|
|
326
|
+
*/
|
|
327
|
+
function createTextConcatTool(adapter) {
|
|
328
|
+
return {
|
|
329
|
+
name: "sqlite_text_concat",
|
|
330
|
+
description: "Concatenate multiple columns with optional separator.",
|
|
331
|
+
group: "text",
|
|
332
|
+
inputSchema: TextConcatSchema,
|
|
333
|
+
requiredScopes: ["read"],
|
|
334
|
+
annotations: readOnly("Text Concat"),
|
|
335
|
+
handler: async (params, _context) => {
|
|
336
|
+
const input = TextConcatSchema.parse(params);
|
|
337
|
+
// Validate and quote identifiers
|
|
338
|
+
const table = sanitizeIdentifier(input.table);
|
|
339
|
+
const quotedCols = input.columns.map((c) => sanitizeIdentifier(c));
|
|
340
|
+
// Build concatenation expression using || operator
|
|
341
|
+
const sep = input.separator.replace(/'/g, "''");
|
|
342
|
+
// Build: COALESCE(col1, '') || 'sep' || COALESCE(col2, '') || ...
|
|
343
|
+
const concatExpr = quotedCols
|
|
344
|
+
.map((c) => `COALESCE(${c}, '')`)
|
|
345
|
+
.join(` || '${sep}' || `);
|
|
346
|
+
let sql = `SELECT ${concatExpr} as concatenated FROM ${table}`;
|
|
347
|
+
if (input.whereClause) {
|
|
348
|
+
validateWhereClause(input.whereClause);
|
|
349
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
350
|
+
}
|
|
351
|
+
sql += ` LIMIT ${input.limit}`;
|
|
352
|
+
const result = await adapter.executeReadQuery(sql);
|
|
353
|
+
return {
|
|
354
|
+
success: true,
|
|
355
|
+
rowCount: result.rows?.length ?? 0,
|
|
356
|
+
values: result.rows?.map((r) => r["concatenated"]),
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Replace text in column
|
|
363
|
+
*/
|
|
364
|
+
function createTextReplaceTool(adapter) {
|
|
365
|
+
return {
|
|
366
|
+
name: "sqlite_text_replace",
|
|
367
|
+
description: "Replace text in a column using SQLite replace() function.",
|
|
368
|
+
group: "text",
|
|
369
|
+
inputSchema: TextReplaceSchema,
|
|
370
|
+
outputSchema: RegexReplaceOutputSchema,
|
|
371
|
+
requiredScopes: ["write"],
|
|
372
|
+
annotations: write("Text Replace"),
|
|
373
|
+
handler: async (params, _context) => {
|
|
374
|
+
const input = TextReplaceSchema.parse(params);
|
|
375
|
+
// Validate and quote identifiers
|
|
376
|
+
const table = sanitizeIdentifier(input.table);
|
|
377
|
+
const column = sanitizeIdentifier(input.column);
|
|
378
|
+
const search = input.searchPattern.replace(/'/g, "''");
|
|
379
|
+
const replace = input.replaceWith.replace(/'/g, "''");
|
|
380
|
+
validateWhereClause(input.whereClause);
|
|
381
|
+
const sql = `UPDATE ${table} SET ${column} = replace(${column}, '${search}', '${replace}') WHERE ${input.whereClause}`;
|
|
382
|
+
const result = await adapter.executeWriteQuery(sql);
|
|
383
|
+
return {
|
|
384
|
+
success: true,
|
|
385
|
+
rowsAffected: result.rowsAffected,
|
|
386
|
+
};
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Trim whitespace
|
|
392
|
+
*/
|
|
393
|
+
function createTextTrimTool(adapter) {
|
|
394
|
+
return {
|
|
395
|
+
name: "sqlite_text_trim",
|
|
396
|
+
description: "Trim whitespace from text column values.",
|
|
397
|
+
group: "text",
|
|
398
|
+
inputSchema: TextTrimSchema,
|
|
399
|
+
requiredScopes: ["read"],
|
|
400
|
+
annotations: readOnly("Text Trim"),
|
|
401
|
+
handler: async (params, _context) => {
|
|
402
|
+
const input = TextTrimSchema.parse(params);
|
|
403
|
+
// Validate and quote identifiers
|
|
404
|
+
const table = sanitizeIdentifier(input.table);
|
|
405
|
+
const column = sanitizeIdentifier(input.column);
|
|
406
|
+
let trimFunc;
|
|
407
|
+
switch (input.mode) {
|
|
408
|
+
case "left":
|
|
409
|
+
trimFunc = "ltrim";
|
|
410
|
+
break;
|
|
411
|
+
case "right":
|
|
412
|
+
trimFunc = "rtrim";
|
|
413
|
+
break;
|
|
414
|
+
default:
|
|
415
|
+
trimFunc = "trim";
|
|
416
|
+
}
|
|
417
|
+
let sql = `SELECT rowid, ${column} as original, ${trimFunc}(${column}) as trimmed FROM ${table}`;
|
|
418
|
+
if (input.whereClause) {
|
|
419
|
+
validateWhereClause(input.whereClause);
|
|
420
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
421
|
+
}
|
|
422
|
+
sql += ` LIMIT ${input.limit}`;
|
|
423
|
+
const result = await adapter.executeReadQuery(sql);
|
|
424
|
+
return {
|
|
425
|
+
success: true,
|
|
426
|
+
rowCount: result.rows?.length ?? 0,
|
|
427
|
+
results: result.rows,
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Change text case
|
|
434
|
+
*/
|
|
435
|
+
function createTextCaseTool(adapter) {
|
|
436
|
+
return {
|
|
437
|
+
name: "sqlite_text_case",
|
|
438
|
+
description: "Convert text to uppercase or lowercase.",
|
|
439
|
+
group: "text",
|
|
440
|
+
inputSchema: TextCaseSchema,
|
|
441
|
+
requiredScopes: ["read"],
|
|
442
|
+
annotations: readOnly("Text Case"),
|
|
443
|
+
handler: async (params, _context) => {
|
|
444
|
+
const input = TextCaseSchema.parse(params);
|
|
445
|
+
// Validate and quote identifiers
|
|
446
|
+
const table = sanitizeIdentifier(input.table);
|
|
447
|
+
const column = sanitizeIdentifier(input.column);
|
|
448
|
+
const caseFunc = input.mode === "upper" ? "upper" : "lower";
|
|
449
|
+
let sql = `SELECT rowid, ${column} as original, ${caseFunc}(${column}) as transformed FROM ${table}`;
|
|
450
|
+
if (input.whereClause) {
|
|
451
|
+
validateWhereClause(input.whereClause);
|
|
452
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
453
|
+
}
|
|
454
|
+
sql += ` LIMIT ${input.limit}`;
|
|
455
|
+
const result = await adapter.executeReadQuery(sql);
|
|
456
|
+
return {
|
|
457
|
+
success: true,
|
|
458
|
+
rowCount: result.rows?.length ?? 0,
|
|
459
|
+
results: result.rows,
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Extract substring
|
|
466
|
+
*/
|
|
467
|
+
function createTextSubstringTool(adapter) {
|
|
468
|
+
return {
|
|
469
|
+
name: "sqlite_text_substring",
|
|
470
|
+
description: "Extract a substring from text column using substr().",
|
|
471
|
+
group: "text",
|
|
472
|
+
inputSchema: TextSubstringSchema,
|
|
473
|
+
requiredScopes: ["read"],
|
|
474
|
+
annotations: readOnly("Text Substring"),
|
|
475
|
+
handler: async (params, _context) => {
|
|
476
|
+
const input = TextSubstringSchema.parse(params);
|
|
477
|
+
// Validate and quote identifiers
|
|
478
|
+
const table = sanitizeIdentifier(input.table);
|
|
479
|
+
const column = sanitizeIdentifier(input.column);
|
|
480
|
+
const substrExpr = input.length !== undefined
|
|
481
|
+
? `substr(${column}, ${input.start}, ${input.length})`
|
|
482
|
+
: `substr(${column}, ${input.start})`;
|
|
483
|
+
let sql = `SELECT rowid, ${column} as original, ${substrExpr} as substring FROM ${table}`;
|
|
484
|
+
if (input.whereClause) {
|
|
485
|
+
validateWhereClause(input.whereClause);
|
|
486
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
487
|
+
}
|
|
488
|
+
sql += ` LIMIT ${input.limit}`;
|
|
489
|
+
const result = await adapter.executeReadQuery(sql);
|
|
490
|
+
return {
|
|
491
|
+
success: true,
|
|
492
|
+
rowCount: result.rows?.length ?? 0,
|
|
493
|
+
results: result.rows,
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
// =============================================================================
|
|
499
|
+
// New Text Tools: Fuzzy, Phonetic, Normalize, Validate
|
|
500
|
+
// =============================================================================
|
|
501
|
+
/**
|
|
502
|
+
* Calculate Levenshtein distance between two strings
|
|
503
|
+
*/
|
|
504
|
+
function levenshtein(a, b) {
|
|
505
|
+
const aLower = a.toLowerCase();
|
|
506
|
+
const bLower = b.toLowerCase();
|
|
507
|
+
// Handle edge cases
|
|
508
|
+
if (aLower === bLower)
|
|
509
|
+
return 0;
|
|
510
|
+
if (aLower.length === 0)
|
|
511
|
+
return bLower.length;
|
|
512
|
+
if (bLower.length === 0)
|
|
513
|
+
return aLower.length;
|
|
514
|
+
// Create matrix with proper initialization
|
|
515
|
+
const rows = bLower.length + 1;
|
|
516
|
+
const cols = aLower.length + 1;
|
|
517
|
+
const matrix = [];
|
|
518
|
+
for (let i = 0; i < rows; i++) {
|
|
519
|
+
matrix.push(new Array(cols).fill(0));
|
|
520
|
+
}
|
|
521
|
+
// Initialize first column and row
|
|
522
|
+
for (let i = 0; i < rows; i++) {
|
|
523
|
+
const row = matrix[i];
|
|
524
|
+
if (row)
|
|
525
|
+
row[0] = i;
|
|
526
|
+
}
|
|
527
|
+
for (let j = 0; j < cols; j++) {
|
|
528
|
+
const firstRow = matrix[0];
|
|
529
|
+
if (firstRow)
|
|
530
|
+
firstRow[j] = j;
|
|
531
|
+
}
|
|
532
|
+
// Fill in the rest of the matrix
|
|
533
|
+
for (let i = 1; i < rows; i++) {
|
|
534
|
+
for (let j = 1; j < cols; j++) {
|
|
535
|
+
const currentRow = matrix[i];
|
|
536
|
+
const prevRow = matrix[i - 1];
|
|
537
|
+
if (!currentRow || !prevRow)
|
|
538
|
+
continue;
|
|
539
|
+
const cost = bLower[i - 1] === aLower[j - 1] ? 0 : 1;
|
|
540
|
+
const del = (prevRow[j] ?? 0) + 1;
|
|
541
|
+
const ins = (currentRow[j - 1] ?? 0) + 1;
|
|
542
|
+
const sub = (prevRow[j - 1] ?? 0) + cost;
|
|
543
|
+
currentRow[j] = Math.min(del, ins, sub);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const lastRow = matrix[bLower.length];
|
|
547
|
+
return lastRow?.[aLower.length] ?? 0;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Simple Metaphone implementation
|
|
551
|
+
*/
|
|
552
|
+
function metaphone(word) {
|
|
553
|
+
const vowels = "AEIOU";
|
|
554
|
+
let result = "";
|
|
555
|
+
const w = word.toUpperCase().replace(/[^A-Z]/g, "");
|
|
556
|
+
for (let i = 0; i < w.length && result.length < 4; i++) {
|
|
557
|
+
const c = w.charAt(i);
|
|
558
|
+
const prev = i > 0 ? w.charAt(i - 1) : "";
|
|
559
|
+
const next = i < w.length - 1 ? w.charAt(i + 1) : "";
|
|
560
|
+
// Skip duplicate adjacent letters
|
|
561
|
+
if (c === prev && c !== "C")
|
|
562
|
+
continue;
|
|
563
|
+
// Skip vowels except at start
|
|
564
|
+
if (vowels.includes(c) && i > 0)
|
|
565
|
+
continue;
|
|
566
|
+
// Consonant rules (simplified)
|
|
567
|
+
if (c === "B" && i === w.length - 1 && prev === "M")
|
|
568
|
+
continue;
|
|
569
|
+
if (c === "C") {
|
|
570
|
+
if (next === "H") {
|
|
571
|
+
result += "X";
|
|
572
|
+
i++;
|
|
573
|
+
}
|
|
574
|
+
else if ("IEY".includes(next))
|
|
575
|
+
result += "S";
|
|
576
|
+
else
|
|
577
|
+
result += "K";
|
|
578
|
+
}
|
|
579
|
+
else if (c === "D") {
|
|
580
|
+
const afterNext = w.charAt(i + 2);
|
|
581
|
+
if (next === "G" && "IEY".includes(afterNext)) {
|
|
582
|
+
result += "J";
|
|
583
|
+
i++;
|
|
584
|
+
}
|
|
585
|
+
else
|
|
586
|
+
result += "T";
|
|
587
|
+
}
|
|
588
|
+
else if (c === "G") {
|
|
589
|
+
if (next === "H")
|
|
590
|
+
continue;
|
|
591
|
+
if ("IEY".includes(next))
|
|
592
|
+
result += "J";
|
|
593
|
+
else
|
|
594
|
+
result += "K";
|
|
595
|
+
}
|
|
596
|
+
else if (c === "K" && prev === "C")
|
|
597
|
+
continue;
|
|
598
|
+
else if (c === "P" && next === "H") {
|
|
599
|
+
result += "F";
|
|
600
|
+
i++;
|
|
601
|
+
}
|
|
602
|
+
else if (c === "Q")
|
|
603
|
+
result += "K";
|
|
604
|
+
else if (c === "S" && next === "H") {
|
|
605
|
+
result += "X";
|
|
606
|
+
i++;
|
|
607
|
+
}
|
|
608
|
+
else if (c === "T" && next === "H") {
|
|
609
|
+
result += "0";
|
|
610
|
+
i++;
|
|
611
|
+
}
|
|
612
|
+
else if (c === "W" && vowels.includes(next))
|
|
613
|
+
result += "W";
|
|
614
|
+
else if (c === "X")
|
|
615
|
+
result += "KS";
|
|
616
|
+
else if (c === "Z")
|
|
617
|
+
result += "S";
|
|
618
|
+
else if (!"HW".includes(c))
|
|
619
|
+
result += c;
|
|
620
|
+
}
|
|
621
|
+
return result;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Strip accents from text
|
|
625
|
+
*/
|
|
626
|
+
function stripAccents(text) {
|
|
627
|
+
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Simple Soundex implementation for phonetic matching
|
|
631
|
+
*/
|
|
632
|
+
function soundex(word) {
|
|
633
|
+
if (!word)
|
|
634
|
+
return "0000";
|
|
635
|
+
const upper = word.toUpperCase().replace(/[^A-Z]/g, "");
|
|
636
|
+
if (!upper)
|
|
637
|
+
return "0000";
|
|
638
|
+
const first = upper[0] ?? "0";
|
|
639
|
+
const mapping = {
|
|
640
|
+
B: "1",
|
|
641
|
+
F: "1",
|
|
642
|
+
P: "1",
|
|
643
|
+
V: "1",
|
|
644
|
+
C: "2",
|
|
645
|
+
G: "2",
|
|
646
|
+
J: "2",
|
|
647
|
+
K: "2",
|
|
648
|
+
Q: "2",
|
|
649
|
+
S: "2",
|
|
650
|
+
X: "2",
|
|
651
|
+
Z: "2",
|
|
652
|
+
D: "3",
|
|
653
|
+
T: "3",
|
|
654
|
+
L: "4",
|
|
655
|
+
M: "5",
|
|
656
|
+
N: "5",
|
|
657
|
+
R: "6",
|
|
658
|
+
};
|
|
659
|
+
let result = first;
|
|
660
|
+
for (let i = 1; i < upper.length && result.length < 4; i++) {
|
|
661
|
+
const char = upper[i];
|
|
662
|
+
if (char && mapping[char]) {
|
|
663
|
+
const code = mapping[char];
|
|
664
|
+
if (code && !result.endsWith(code)) {
|
|
665
|
+
result += code;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return (result + "000").slice(0, 4);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Validation patterns
|
|
673
|
+
*/
|
|
674
|
+
const VALIDATION_PATTERNS = {
|
|
675
|
+
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
676
|
+
phone: /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]{7,}$/,
|
|
677
|
+
url: /^https?:\/\/[^\s/$.?#].[^\s]*$/i,
|
|
678
|
+
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
679
|
+
ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* Fuzzy match using Levenshtein distance
|
|
683
|
+
*/
|
|
684
|
+
function createFuzzyMatchTool(adapter) {
|
|
685
|
+
return {
|
|
686
|
+
name: "sqlite_fuzzy_match",
|
|
687
|
+
description: "Find fuzzy matches using Levenshtein distance. By default, splits values into tokens and matches against each word (use tokenize:false to match entire value). Use maxDistance 1-3 for similar-length strings.",
|
|
688
|
+
group: "text",
|
|
689
|
+
inputSchema: FuzzyMatchSchema,
|
|
690
|
+
outputSchema: z.object({
|
|
691
|
+
success: z.boolean(),
|
|
692
|
+
matchCount: z.number(),
|
|
693
|
+
tokenized: z.boolean(),
|
|
694
|
+
matches: z.array(z.object({
|
|
695
|
+
value: z.string(),
|
|
696
|
+
matchedToken: z.string().optional(),
|
|
697
|
+
tokenDistance: z.number().optional(),
|
|
698
|
+
distance: z.number(),
|
|
699
|
+
})),
|
|
700
|
+
}),
|
|
701
|
+
requiredScopes: ["read"],
|
|
702
|
+
annotations: readOnly("Fuzzy Match"),
|
|
703
|
+
handler: async (params, _context) => {
|
|
704
|
+
const input = FuzzyMatchSchema.parse(params);
|
|
705
|
+
// Validate and quote identifiers
|
|
706
|
+
const table = sanitizeIdentifier(input.table);
|
|
707
|
+
const column = sanitizeIdentifier(input.column);
|
|
708
|
+
const sql = `SELECT ${column} FROM ${table} WHERE ${column} IS NOT NULL LIMIT 1000`;
|
|
709
|
+
const result = await adapter.executeReadQuery(sql);
|
|
710
|
+
const matches = [];
|
|
711
|
+
for (const row of result.rows ?? []) {
|
|
712
|
+
const rawValue = row[input.column];
|
|
713
|
+
const value = typeof rawValue === "string"
|
|
714
|
+
? rawValue
|
|
715
|
+
: JSON.stringify(rawValue ?? "");
|
|
716
|
+
if (input.tokenize) {
|
|
717
|
+
// Token-based matching: split into words and find best match
|
|
718
|
+
const tokens = value.split(/\s+/).filter((t) => t.length > 0);
|
|
719
|
+
let bestToken = "";
|
|
720
|
+
let bestDistance = Infinity;
|
|
721
|
+
for (const token of tokens) {
|
|
722
|
+
const dist = levenshtein(input.search, token);
|
|
723
|
+
if (dist < bestDistance) {
|
|
724
|
+
bestDistance = dist;
|
|
725
|
+
bestToken = token;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (bestDistance <= input.maxDistance) {
|
|
729
|
+
matches.push({
|
|
730
|
+
value,
|
|
731
|
+
matchedToken: bestToken,
|
|
732
|
+
tokenDistance: bestDistance,
|
|
733
|
+
distance: bestDistance,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// Legacy behavior: match against entire column value
|
|
739
|
+
const distance = levenshtein(input.search, value);
|
|
740
|
+
if (distance <= input.maxDistance) {
|
|
741
|
+
matches.push({ value, distance });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Sort by distance (ascending) and limit
|
|
746
|
+
matches.sort((a, b) => a.distance - b.distance);
|
|
747
|
+
const limited = matches.slice(0, input.limit);
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
matchCount: limited.length,
|
|
751
|
+
tokenized: input.tokenize,
|
|
752
|
+
matches: limited,
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Phonetic match using Soundex or Metaphone
|
|
759
|
+
*/
|
|
760
|
+
function createPhoneticMatchTool(adapter) {
|
|
761
|
+
return {
|
|
762
|
+
name: "sqlite_phonetic_match",
|
|
763
|
+
description: "Find phonetically similar values using Soundex (SQLite native) or Metaphone algorithm.",
|
|
764
|
+
group: "text",
|
|
765
|
+
inputSchema: PhoneticMatchSchema,
|
|
766
|
+
outputSchema: z.object({
|
|
767
|
+
success: z.boolean(),
|
|
768
|
+
searchCode: z.string(),
|
|
769
|
+
matchCount: z.number(),
|
|
770
|
+
matches: z.array(z.object({
|
|
771
|
+
value: z.string(),
|
|
772
|
+
phoneticCode: z.string(),
|
|
773
|
+
row: z.record(z.string(), z.unknown()).optional(),
|
|
774
|
+
})),
|
|
775
|
+
}),
|
|
776
|
+
requiredScopes: ["read"],
|
|
777
|
+
annotations: readOnly("Phonetic Match"),
|
|
778
|
+
handler: async (params, _context) => {
|
|
779
|
+
const input = PhoneticMatchSchema.parse(params);
|
|
780
|
+
// Validate and quote identifiers
|
|
781
|
+
const table = sanitizeIdentifier(input.table);
|
|
782
|
+
const column = sanitizeIdentifier(input.column);
|
|
783
|
+
const searchCode = input.algorithm === "metaphone"
|
|
784
|
+
? metaphone(input.search)
|
|
785
|
+
: soundex(input.search); // Compute locally to ensure it's always available
|
|
786
|
+
let sql;
|
|
787
|
+
if (input.algorithm === "soundex") {
|
|
788
|
+
// Try SQLite's native soundex function first, fall back to JS implementation
|
|
789
|
+
sql = `SELECT *, soundex(${column}) as _phonetic FROM ${table} WHERE soundex(${column}) = soundex('${input.search.replace(/'/g, "''")}') LIMIT ${input.limit}`;
|
|
790
|
+
try {
|
|
791
|
+
const result = await adapter.executeReadQuery(sql);
|
|
792
|
+
const matches = (result.rows ?? []).map((row) => {
|
|
793
|
+
const rawValue = row[input.column];
|
|
794
|
+
const rawPhonetic = row["_phonetic"];
|
|
795
|
+
const match = {
|
|
796
|
+
value: typeof rawValue === "string"
|
|
797
|
+
? rawValue
|
|
798
|
+
: JSON.stringify(rawValue ?? ""),
|
|
799
|
+
phoneticCode: typeof rawPhonetic === "string" ? rawPhonetic : "",
|
|
800
|
+
};
|
|
801
|
+
if (input.includeRowData) {
|
|
802
|
+
match.row = row;
|
|
803
|
+
}
|
|
804
|
+
return match;
|
|
805
|
+
});
|
|
806
|
+
return {
|
|
807
|
+
success: true,
|
|
808
|
+
searchCode, // Use pre-computed local soundex code
|
|
809
|
+
matchCount: matches.length,
|
|
810
|
+
matches,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
// If SQLite soundex() is unavailable (WASM mode), fall back to JS implementation
|
|
815
|
+
if (error instanceof Error &&
|
|
816
|
+
error.message.toLowerCase().includes("no such function: soundex")) {
|
|
817
|
+
// Fall through to JS-based soundex matching below
|
|
818
|
+
sql = `SELECT * FROM ${table} WHERE ${column} IS NOT NULL LIMIT 1000`;
|
|
819
|
+
const result = await adapter.executeReadQuery(sql);
|
|
820
|
+
const matches = [];
|
|
821
|
+
for (const row of result.rows ?? []) {
|
|
822
|
+
const rawValue = row[input.column];
|
|
823
|
+
const value = typeof rawValue === "string"
|
|
824
|
+
? rawValue
|
|
825
|
+
: JSON.stringify(rawValue ?? "");
|
|
826
|
+
const code = soundex(value); // Use JS-based soundex
|
|
827
|
+
if (code === searchCode) {
|
|
828
|
+
const match = { value, phoneticCode: code };
|
|
829
|
+
if (input.includeRowData) {
|
|
830
|
+
match.row = row;
|
|
831
|
+
}
|
|
832
|
+
matches.push(match);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
success: true,
|
|
837
|
+
searchCode,
|
|
838
|
+
matchCount: matches.length,
|
|
839
|
+
matches: matches.slice(0, input.limit),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// Metaphone in JS
|
|
847
|
+
sql = `SELECT * FROM ${table} WHERE ${column} IS NOT NULL LIMIT 1000`;
|
|
848
|
+
const result = await adapter.executeReadQuery(sql);
|
|
849
|
+
const matches = [];
|
|
850
|
+
for (const row of result.rows ?? []) {
|
|
851
|
+
const rawValue = row[input.column];
|
|
852
|
+
const value = typeof rawValue === "string"
|
|
853
|
+
? rawValue
|
|
854
|
+
: JSON.stringify(rawValue ?? "");
|
|
855
|
+
const code = metaphone(value);
|
|
856
|
+
if (code === searchCode) {
|
|
857
|
+
const match = { value, phoneticCode: code };
|
|
858
|
+
if (input.includeRowData) {
|
|
859
|
+
match.row = row;
|
|
860
|
+
}
|
|
861
|
+
matches.push(match);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
success: true,
|
|
866
|
+
searchCode,
|
|
867
|
+
matchCount: matches.length,
|
|
868
|
+
matches: matches.slice(0, input.limit),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Normalize text (Unicode normalization or accent stripping)
|
|
876
|
+
*/
|
|
877
|
+
function createTextNormalizeTool(adapter) {
|
|
878
|
+
return {
|
|
879
|
+
name: "sqlite_text_normalize",
|
|
880
|
+
description: "Normalize text using Unicode normalization (NFC, NFD, NFKC, NFKD) or strip accents.",
|
|
881
|
+
group: "text",
|
|
882
|
+
inputSchema: TextNormalizeSchema,
|
|
883
|
+
outputSchema: z.object({
|
|
884
|
+
success: z.boolean(),
|
|
885
|
+
rowCount: z.number(),
|
|
886
|
+
rows: z.array(z.object({
|
|
887
|
+
original: z.string(),
|
|
888
|
+
normalized: z.string(),
|
|
889
|
+
})),
|
|
890
|
+
}),
|
|
891
|
+
requiredScopes: ["read"],
|
|
892
|
+
annotations: readOnly("Text Normalize"),
|
|
893
|
+
handler: async (params, _context) => {
|
|
894
|
+
const input = TextNormalizeSchema.parse(params);
|
|
895
|
+
// Validate and quote identifiers
|
|
896
|
+
const table = sanitizeIdentifier(input.table);
|
|
897
|
+
const column = sanitizeIdentifier(input.column);
|
|
898
|
+
let sql = `SELECT ${column} as original FROM ${table}`;
|
|
899
|
+
if (input.whereClause) {
|
|
900
|
+
validateWhereClause(input.whereClause);
|
|
901
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
902
|
+
}
|
|
903
|
+
sql += ` LIMIT ${input.limit}`;
|
|
904
|
+
const result = await adapter.executeReadQuery(sql);
|
|
905
|
+
const rows = (result.rows ?? []).map((row) => {
|
|
906
|
+
const rawOriginal = row["original"];
|
|
907
|
+
const original = typeof rawOriginal === "string"
|
|
908
|
+
? rawOriginal
|
|
909
|
+
: JSON.stringify(rawOriginal ?? "");
|
|
910
|
+
let normalized;
|
|
911
|
+
if (input.mode === "strip_accents") {
|
|
912
|
+
normalized = stripAccents(original);
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
normalized = original.normalize(input.mode.toUpperCase());
|
|
916
|
+
}
|
|
917
|
+
return { original, normalized };
|
|
918
|
+
});
|
|
919
|
+
return {
|
|
920
|
+
success: true,
|
|
921
|
+
rowCount: rows.length,
|
|
922
|
+
rows,
|
|
923
|
+
};
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Validate text against common patterns
|
|
929
|
+
*/
|
|
930
|
+
function createTextValidateTool(adapter) {
|
|
931
|
+
return {
|
|
932
|
+
name: "sqlite_text_validate",
|
|
933
|
+
description: "Validate text values against patterns: email, phone, URL, UUID, IPv4, or custom regex.",
|
|
934
|
+
group: "text",
|
|
935
|
+
inputSchema: TextValidateSchema,
|
|
936
|
+
outputSchema: z.object({
|
|
937
|
+
success: z.boolean(),
|
|
938
|
+
totalRows: z.number(),
|
|
939
|
+
validCount: z.number(),
|
|
940
|
+
invalidCount: z.number(),
|
|
941
|
+
invalidRows: z.array(z.object({
|
|
942
|
+
value: z.string().nullable(),
|
|
943
|
+
rowid: z.number().optional(),
|
|
944
|
+
})),
|
|
945
|
+
}),
|
|
946
|
+
requiredScopes: ["read"],
|
|
947
|
+
annotations: readOnly("Text Validate"),
|
|
948
|
+
handler: async (params, _context) => {
|
|
949
|
+
const input = TextValidateSchema.parse(params);
|
|
950
|
+
// Validate and quote identifiers
|
|
951
|
+
const table = sanitizeIdentifier(input.table);
|
|
952
|
+
const column = sanitizeIdentifier(input.column);
|
|
953
|
+
// Get validation pattern
|
|
954
|
+
let pattern;
|
|
955
|
+
if (input.pattern === "custom") {
|
|
956
|
+
if (!input.customPattern) {
|
|
957
|
+
throw new Error("customPattern is required when pattern='custom'");
|
|
958
|
+
}
|
|
959
|
+
// Normalize pattern: handle common JSON double-escaping issues
|
|
960
|
+
// e.g., "\\." in JSON becomes "\." in JavaScript string, but some clients
|
|
961
|
+
// may send "\\\\." which becomes "\\" - normalize excessive backslashes
|
|
962
|
+
const normalizedPattern = input.customPattern.replace(/\\\\/g, "\\");
|
|
963
|
+
try {
|
|
964
|
+
pattern = new RegExp(normalizedPattern);
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
throw new Error(`Invalid regex pattern: ${input.customPattern} (normalized to: ${normalizedPattern})`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
const foundPattern = VALIDATION_PATTERNS[input.pattern];
|
|
972
|
+
if (!foundPattern) {
|
|
973
|
+
throw new Error(`Unknown pattern: ${input.pattern}`);
|
|
974
|
+
}
|
|
975
|
+
pattern = foundPattern;
|
|
976
|
+
}
|
|
977
|
+
let sql = `SELECT rowid, ${column} as value FROM ${table}`;
|
|
978
|
+
if (input.whereClause) {
|
|
979
|
+
validateWhereClause(input.whereClause);
|
|
980
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
981
|
+
}
|
|
982
|
+
sql += ` LIMIT ${input.limit}`;
|
|
983
|
+
const result = await adapter.executeReadQuery(sql);
|
|
984
|
+
const invalidRows = [];
|
|
985
|
+
let validCount = 0;
|
|
986
|
+
for (const row of result.rows ?? []) {
|
|
987
|
+
const rawValue = row["value"];
|
|
988
|
+
// Handle null/empty values with user-friendly display
|
|
989
|
+
const value = rawValue === null || rawValue === undefined
|
|
990
|
+
? ""
|
|
991
|
+
: typeof rawValue === "string"
|
|
992
|
+
? rawValue
|
|
993
|
+
: JSON.stringify(rawValue);
|
|
994
|
+
// Display actual value: null for null/undefined, otherwise the value (truncated if long)
|
|
995
|
+
const displayValue = rawValue === null || rawValue === undefined
|
|
996
|
+
? null
|
|
997
|
+
: value.length > 100
|
|
998
|
+
? value.slice(0, 100) + "..."
|
|
999
|
+
: value;
|
|
1000
|
+
if (pattern.test(value)) {
|
|
1001
|
+
validCount++;
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
const rowid = row["rowid"];
|
|
1005
|
+
if (typeof rowid === "number") {
|
|
1006
|
+
invalidRows.push({ value: displayValue, rowid });
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
invalidRows.push({ value: displayValue });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
success: true,
|
|
1015
|
+
totalRows: result.rows?.length ?? 0,
|
|
1016
|
+
validCount,
|
|
1017
|
+
invalidCount: invalidRows.length,
|
|
1018
|
+
invalidRows,
|
|
1019
|
+
};
|
|
1020
|
+
},
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Output schema for advanced search
|
|
1025
|
+
*/
|
|
1026
|
+
const AdvancedSearchOutputSchema = z.object({
|
|
1027
|
+
success: z.boolean(),
|
|
1028
|
+
searchTerm: z.string(),
|
|
1029
|
+
techniques: z.array(z.string()),
|
|
1030
|
+
matchCount: z.number(),
|
|
1031
|
+
matches: z.array(z.object({
|
|
1032
|
+
rowid: z.number(),
|
|
1033
|
+
text: z.string(),
|
|
1034
|
+
matchTypes: z.array(z.string()),
|
|
1035
|
+
bestScore: z.number(),
|
|
1036
|
+
bestType: z.string(),
|
|
1037
|
+
})),
|
|
1038
|
+
});
|
|
1039
|
+
/**
|
|
1040
|
+
* Advanced search combining multiple text processing techniques
|
|
1041
|
+
*/
|
|
1042
|
+
function createAdvancedSearchTool(adapter) {
|
|
1043
|
+
return {
|
|
1044
|
+
name: "sqlite_advanced_search",
|
|
1045
|
+
description: "Advanced search combining exact, fuzzy (Levenshtein), and phonetic (Soundex) matching",
|
|
1046
|
+
group: "text",
|
|
1047
|
+
inputSchema: AdvancedSearchSchema,
|
|
1048
|
+
outputSchema: AdvancedSearchOutputSchema,
|
|
1049
|
+
requiredScopes: ["read"],
|
|
1050
|
+
annotations: readOnly("Advanced Search"),
|
|
1051
|
+
handler: async (params, _context) => {
|
|
1052
|
+
const input = AdvancedSearchSchema.parse(params);
|
|
1053
|
+
// Validate and quote identifiers
|
|
1054
|
+
const table = sanitizeIdentifier(input.table);
|
|
1055
|
+
const column = sanitizeIdentifier(input.column);
|
|
1056
|
+
// Fetch candidate rows
|
|
1057
|
+
let whereClause = "";
|
|
1058
|
+
if (input.whereClause) {
|
|
1059
|
+
validateWhereClause(input.whereClause);
|
|
1060
|
+
whereClause = ` AND ${input.whereClause}`;
|
|
1061
|
+
}
|
|
1062
|
+
const query = `SELECT rowid as id, ${column} AS value FROM ${table} WHERE ${column} IS NOT NULL${whereClause} LIMIT 1000`;
|
|
1063
|
+
const result = await adapter.executeQuery(query);
|
|
1064
|
+
if (!result.rows || result.rows.length === 0) {
|
|
1065
|
+
return {
|
|
1066
|
+
success: true,
|
|
1067
|
+
searchTerm: input.searchTerm,
|
|
1068
|
+
techniques: input.techniques,
|
|
1069
|
+
matchCount: 0,
|
|
1070
|
+
matches: [],
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const searchLower = input.searchTerm.toLowerCase();
|
|
1074
|
+
const searchSoundex = soundex(input.searchTerm);
|
|
1075
|
+
const allMatches = [];
|
|
1076
|
+
for (const row of result.rows) {
|
|
1077
|
+
const rawValue = row["value"];
|
|
1078
|
+
const text = typeof rawValue === "string"
|
|
1079
|
+
? rawValue
|
|
1080
|
+
: typeof rawValue === "number"
|
|
1081
|
+
? String(rawValue)
|
|
1082
|
+
: "";
|
|
1083
|
+
const textLower = text.toLowerCase();
|
|
1084
|
+
const matches = [];
|
|
1085
|
+
// Exact match (case-insensitive substring)
|
|
1086
|
+
if (input.techniques.includes("exact")) {
|
|
1087
|
+
if (textLower.includes(searchLower)) {
|
|
1088
|
+
matches.push({ type: "exact", score: 1.0 });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
// Fuzzy match (Levenshtein ratio)
|
|
1092
|
+
if (input.techniques.includes("fuzzy")) {
|
|
1093
|
+
const distance = levenshtein(input.searchTerm, text);
|
|
1094
|
+
const maxLen = Math.max(input.searchTerm.length, text.length);
|
|
1095
|
+
const similarity = maxLen === 0 ? 1 : 1 - distance / maxLen;
|
|
1096
|
+
if (similarity >= input.fuzzyThreshold) {
|
|
1097
|
+
matches.push({ type: "fuzzy", score: similarity });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// Phonetic match (Soundex)
|
|
1101
|
+
if (input.techniques.includes("phonetic")) {
|
|
1102
|
+
const words = text.split(/\s+/);
|
|
1103
|
+
for (const word of words) {
|
|
1104
|
+
if (soundex(word) === searchSoundex) {
|
|
1105
|
+
matches.push({ type: "phonetic", score: 0.8 });
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (matches.length > 0) {
|
|
1111
|
+
const best = matches.reduce((a, b) => (a.score > b.score ? a : b));
|
|
1112
|
+
// Safely coerce rowid to number, defaulting to 0 if undefined/null
|
|
1113
|
+
const rawRowid = row["id"];
|
|
1114
|
+
const rowid = typeof rawRowid === "number"
|
|
1115
|
+
? rawRowid
|
|
1116
|
+
: typeof rawRowid === "string"
|
|
1117
|
+
? parseInt(rawRowid, 10) || 0
|
|
1118
|
+
: 0;
|
|
1119
|
+
allMatches.push({
|
|
1120
|
+
rowid,
|
|
1121
|
+
text: text.length > 100 ? text.slice(0, 100) + "..." : text,
|
|
1122
|
+
matchTypes: matches.map((m) => m.type),
|
|
1123
|
+
bestScore: Math.round(best.score * 1000) / 1000,
|
|
1124
|
+
bestType: best.type,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
// Sort by score and limit
|
|
1129
|
+
allMatches.sort((a, b) => b.bestScore - a.bestScore);
|
|
1130
|
+
const limited = allMatches.slice(0, input.limit);
|
|
1131
|
+
return {
|
|
1132
|
+
success: true,
|
|
1133
|
+
searchTerm: input.searchTerm,
|
|
1134
|
+
techniques: input.techniques,
|
|
1135
|
+
matchCount: limited.length,
|
|
1136
|
+
matches: limited,
|
|
1137
|
+
};
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
//# sourceMappingURL=text.js.map
|