iosm-cli 0.2.8 → 0.2.9

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 (93) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -3
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +7 -3
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-profiles.d.ts.map +1 -1
  7. package/dist/core/agent-profiles.js +5 -1
  8. package/dist/core/agent-profiles.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +3 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +188 -2
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/sdk.d.ts +2 -2
  14. package/dist/core/sdk.d.ts.map +1 -1
  15. package/dist/core/sdk.js +7 -4
  16. package/dist/core/sdk.js.map +1 -1
  17. package/dist/core/settings-manager.d.ts +18 -0
  18. package/dist/core/settings-manager.d.ts.map +1 -1
  19. package/dist/core/settings-manager.js +29 -0
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/core/shadow-guard.d.ts.map +1 -1
  22. package/dist/core/shadow-guard.js +12 -1
  23. package/dist/core/shadow-guard.js.map +1 -1
  24. package/dist/core/system-prompt.d.ts.map +1 -1
  25. package/dist/core/system-prompt.js +32 -1
  26. package/dist/core/system-prompt.js.map +1 -1
  27. package/dist/core/tools/db-run.d.ts +84 -0
  28. package/dist/core/tools/db-run.d.ts.map +1 -0
  29. package/dist/core/tools/db-run.js +690 -0
  30. package/dist/core/tools/db-run.js.map +1 -0
  31. package/dist/core/tools/index.d.ts +44 -0
  32. package/dist/core/tools/index.d.ts.map +1 -1
  33. package/dist/core/tools/index.js +16 -0
  34. package/dist/core/tools/index.js.map +1 -1
  35. package/dist/core/tools/lint-run.d.ts +42 -0
  36. package/dist/core/tools/lint-run.d.ts.map +1 -0
  37. package/dist/core/tools/lint-run.js +276 -0
  38. package/dist/core/tools/lint-run.js.map +1 -0
  39. package/dist/core/tools/test-run.d.ts +36 -0
  40. package/dist/core/tools/test-run.d.ts.map +1 -0
  41. package/dist/core/tools/test-run.js +255 -0
  42. package/dist/core/tools/test-run.js.map +1 -0
  43. package/dist/core/tools/typecheck-run.d.ts +44 -0
  44. package/dist/core/tools/typecheck-run.d.ts.map +1 -0
  45. package/dist/core/tools/typecheck-run.js +343 -0
  46. package/dist/core/tools/typecheck-run.js.map +1 -0
  47. package/dist/core/tools/verification-runner.d.ts +53 -0
  48. package/dist/core/tools/verification-runner.d.ts.map +1 -0
  49. package/dist/core/tools/verification-runner.js +235 -0
  50. package/dist/core/tools/verification-runner.js.map +1 -0
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/branch-summary-message.js +2 -1
  57. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  58. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/compaction-summary-message.js +2 -1
  60. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  61. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/custom-message.js +2 -1
  63. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  64. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/skill-invocation-message.js +4 -2
  66. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  67. package/dist/modes/interactive/components/subagent-message.d.ts.map +1 -1
  68. package/dist/modes/interactive/components/subagent-message.js +3 -1
  69. package/dist/modes/interactive/components/subagent-message.js.map +1 -1
  70. package/dist/modes/interactive/components/task-plan-message.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/task-plan-message.js +2 -1
  72. package/dist/modes/interactive/components/task-plan-message.js.map +1 -1
  73. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  74. package/dist/modes/interactive/components/tool-execution.js +25 -7
  75. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  76. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/user-message.js +2 -1
  78. package/dist/modes/interactive/components/user-message.js.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +494 -9
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/interactive/theme/dark.json +39 -38
  84. package/dist/modes/interactive/theme/light.json +29 -29
  85. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  86. package/dist/modes/interactive/theme/theme.js +16 -25
  87. package/dist/modes/interactive/theme/theme.js.map +1 -1
  88. package/dist/modes/interactive/theme/universal.json +85 -0
  89. package/docs/cli-reference.md +16 -1
  90. package/docs/configuration.md +76 -1
  91. package/docs/development-and-testing.md +1 -1
  92. package/docs/interactive-mode.md +7 -2
  93. package/package.json +1 -1
@@ -0,0 +1,690 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { resolveToCwd } from "./path-utils.js";
5
+ import { commandExists, detectPackageManager, ensureCommandOrThrow, formatVerificationOutput, readPackageJson, resolvePackageManagerRunInvocation, runVerificationCommand, } from "./verification-runner.js";
6
+ const dbRunSchema = Type.Object({
7
+ action: Type.Optional(Type.Union([
8
+ Type.Literal("query"),
9
+ Type.Literal("exec"),
10
+ Type.Literal("schema"),
11
+ Type.Literal("migrate"),
12
+ Type.Literal("explain"),
13
+ ], { description: "DB action: query | exec | schema | migrate | explain (default: query)." })),
14
+ adapter: Type.Optional(Type.Union([
15
+ Type.Literal("auto"),
16
+ Type.Literal("postgres"),
17
+ Type.Literal("mysql"),
18
+ Type.Literal("sqlite"),
19
+ Type.Literal("mongodb"),
20
+ Type.Literal("redis"),
21
+ ], { description: "Database adapter: auto | postgres | mysql | sqlite | mongodb | redis" })),
22
+ connection: Type.Optional(Type.String({
23
+ description: "Named dbTools connection profile. Defaults to dbTools.defaultConnection.",
24
+ })),
25
+ statement: Type.Optional(Type.String({
26
+ description: "Query/command statement for query/exec/explain (optional for schema with adapter defaults).",
27
+ })),
28
+ format: Type.Optional(Type.Union([Type.Literal("table"), Type.Literal("json"), Type.Literal("raw")], {
29
+ description: "Output format hint: table | json | raw (default: table).",
30
+ })),
31
+ allow_write: Type.Optional(Type.Boolean({
32
+ description: "Allow write actions (required for exec/migrate). Defaults to false.",
33
+ })),
34
+ migrate_runner: Type.Optional(Type.Union([Type.Literal("auto"), Type.Literal("script")], {
35
+ description: "Migrate mode: auto | script (default: auto).",
36
+ })),
37
+ script: Type.Optional(Type.String({
38
+ description: "Package script for migrate_runner=script/auto (default: db:migrate).",
39
+ })),
40
+ args: Type.Optional(Type.Array(Type.String(), {
41
+ description: "Additional arguments forwarded to adapter CLI or migration script.",
42
+ })),
43
+ path: Type.Optional(Type.String({ description: "Working directory for db operations (default: current directory)." })),
44
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 300)." })),
45
+ });
46
+ export const DEFAULT_DB_RUN_TIMEOUT_SECONDS = 300;
47
+ const defaultOps = {
48
+ runCommand: runVerificationCommand,
49
+ commandExists,
50
+ };
51
+ function normalizeTimeoutSeconds(raw) {
52
+ if (raw === undefined)
53
+ return DEFAULT_DB_RUN_TIMEOUT_SECONDS;
54
+ const value = Math.floor(raw);
55
+ if (!Number.isFinite(value) || value <= 0) {
56
+ throw new Error("timeout must be a positive number.");
57
+ }
58
+ return value;
59
+ }
60
+ function normalizeStringArray(raw) {
61
+ return (raw ?? []).map((item) => String(item));
62
+ }
63
+ function normalizeAction(raw) {
64
+ return raw ?? "query";
65
+ }
66
+ function normalizeFormat(raw) {
67
+ return raw ?? "table";
68
+ }
69
+ function normalizeScriptName(raw) {
70
+ if (raw === undefined)
71
+ return undefined;
72
+ const script = raw.trim();
73
+ if (!script) {
74
+ throw new Error("script must not be empty.");
75
+ }
76
+ return script;
77
+ }
78
+ function normalizeConnectionName(raw) {
79
+ if (raw === undefined)
80
+ return undefined;
81
+ const value = raw.trim();
82
+ if (!value) {
83
+ throw new Error("connection must not be empty.");
84
+ }
85
+ return value;
86
+ }
87
+ function normalizeRuntimeConfig(raw) {
88
+ const connections = {};
89
+ for (const [name, connection] of Object.entries(raw?.connections ?? {})) {
90
+ const normalizedName = name.trim();
91
+ if (!normalizedName)
92
+ continue;
93
+ const adapter = connection?.adapter;
94
+ if (adapter !== "postgres" &&
95
+ adapter !== "mysql" &&
96
+ adapter !== "sqlite" &&
97
+ adapter !== "mongodb" &&
98
+ adapter !== "redis") {
99
+ continue;
100
+ }
101
+ connections[normalizedName] = {
102
+ name: normalizedName,
103
+ adapter,
104
+ dsnEnv: connection?.dsnEnv?.trim() || undefined,
105
+ sqlitePath: connection?.sqlitePath?.trim() || undefined,
106
+ clientArgs: normalizeStringArray(connection?.clientArgs),
107
+ migrate: {
108
+ script: connection?.migrate?.script?.trim() || undefined,
109
+ cwd: connection?.migrate?.cwd?.trim() || undefined,
110
+ args: normalizeStringArray(connection?.migrate?.args),
111
+ },
112
+ };
113
+ }
114
+ const defaultConnection = raw?.defaultConnection?.trim() || undefined;
115
+ return { defaultConnection, connections };
116
+ }
117
+ function isLikelyPathLikeConnection(value) {
118
+ const normalized = value.trim();
119
+ if (!normalized)
120
+ return false;
121
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.startsWith(".") || normalized.startsWith("~")) {
122
+ return true;
123
+ }
124
+ return /\.(sqlite|sqlite3|db)$/i.test(normalized) || normalized.includes(":memory:");
125
+ }
126
+ function buildDbToolsSetupHint(sqlitePathHint) {
127
+ return [
128
+ 'Configure a named profile in ".iosm/settings.json", for example:',
129
+ "{",
130
+ ' "dbTools": {',
131
+ ' "defaultConnection": "main",',
132
+ ' "connections": {',
133
+ ` "main": { "adapter": "sqlite", "sqlitePath": "${sqlitePathHint}" }`,
134
+ " }",
135
+ " }",
136
+ "}",
137
+ 'Then call db_run with connection="main" (or omit connection when defaultConnection is set).',
138
+ "If you edited settings.json outside the current session, run /reload or restart the session before retrying.",
139
+ ].join("\n");
140
+ }
141
+ function resolveConnectionProfile(input) {
142
+ const effectiveConnection = normalizeConnectionName(input.connection) ?? input.runtimeConfig.defaultConnection;
143
+ if (!effectiveConnection) {
144
+ throw new Error([
145
+ "No db connection selected. Configure dbTools.defaultConnection or pass connection explicitly.",
146
+ buildDbToolsSetupHint("./test_database.sqlite"),
147
+ ].join("\n\n"));
148
+ }
149
+ const profile = input.runtimeConfig.connections[effectiveConnection];
150
+ if (!profile) {
151
+ if (isLikelyPathLikeConnection(effectiveConnection)) {
152
+ throw new Error([
153
+ `Connection profile "${effectiveConnection}" is not defined in settings dbTools.connections.`,
154
+ `The "connection" field expects a profile name, not a file path.`,
155
+ buildDbToolsSetupHint(effectiveConnection),
156
+ ].join("\n\n"));
157
+ }
158
+ throw new Error([
159
+ `Connection profile "${effectiveConnection}" is not defined in settings dbTools.connections.`,
160
+ buildDbToolsSetupHint("./test_database.sqlite"),
161
+ ].join("\n\n"));
162
+ }
163
+ if (input.adapter !== "auto" && input.adapter !== profile.adapter) {
164
+ throw new Error(`Adapter "${input.adapter}" does not match connection "${effectiveConnection}" adapter "${profile.adapter}".`);
165
+ }
166
+ if (profile.adapter === "sqlite") {
167
+ if (!profile.sqlitePath) {
168
+ throw new Error(`Connection "${effectiveConnection}" requires sqlitePath for sqlite adapter.`);
169
+ }
170
+ }
171
+ else if (!profile.dsnEnv) {
172
+ throw new Error(`Connection "${effectiveConnection}" requires dsnEnv for ${profile.adapter} adapter.`);
173
+ }
174
+ return profile;
175
+ }
176
+ function ensureWriteAllowed(action, allowWrite) {
177
+ if ((action === "exec" || action === "migrate") && !allowWrite) {
178
+ throw new Error(`Action "${action}" requires allow_write=true.`);
179
+ }
180
+ }
181
+ function normalizeStatement(statement) {
182
+ if (statement === undefined)
183
+ return undefined;
184
+ const normalized = statement.trim();
185
+ if (!normalized) {
186
+ throw new Error("statement must not be empty when provided.");
187
+ }
188
+ return normalized;
189
+ }
190
+ function isLikelyMutatingSql(statement) {
191
+ return /\b(insert|update|delete|drop|alter|create|truncate|replace|grant|revoke|merge|upsert)\b/i.test(statement);
192
+ }
193
+ function isLikelyMutatingMongo(statement) {
194
+ return /\b(insert|update|delete|drop|create|rename|set|remove|replace|findOneAndUpdate|findAndModify)\b/i.test(statement);
195
+ }
196
+ function isLikelyMutatingRedis(statement) {
197
+ const firstToken = statement.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
198
+ const mutatingCommands = new Set([
199
+ "SET",
200
+ "DEL",
201
+ "HSET",
202
+ "HDEL",
203
+ "LPUSH",
204
+ "RPUSH",
205
+ "SADD",
206
+ "SREM",
207
+ "ZADD",
208
+ "ZREM",
209
+ "INCR",
210
+ "DECR",
211
+ "FLUSHDB",
212
+ "FLUSHALL",
213
+ "EXPIRE",
214
+ "PERSIST",
215
+ ]);
216
+ return mutatingCommands.has(firstToken);
217
+ }
218
+ function enforceReadFirstSafety(adapter, action, statement) {
219
+ if (action !== "query" || !statement)
220
+ return;
221
+ if (adapter === "postgres" || adapter === "mysql" || adapter === "sqlite") {
222
+ if (isLikelyMutatingSql(statement)) {
223
+ throw new Error('Mutating statement detected for action=query. Use action="exec" with allow_write=true.');
224
+ }
225
+ return;
226
+ }
227
+ if (adapter === "mongodb" && isLikelyMutatingMongo(statement)) {
228
+ throw new Error('Mutating statement detected for action=query. Use action="exec" with allow_write=true.');
229
+ }
230
+ if (adapter === "redis" && isLikelyMutatingRedis(statement)) {
231
+ throw new Error('Mutating statement detected for action=query. Use action="exec" with allow_write=true.');
232
+ }
233
+ }
234
+ function parseDsn(dsn, adapter) {
235
+ try {
236
+ const parsed = new URL(dsn);
237
+ const protocol = parsed.protocol.replace(":", "");
238
+ if (adapter === "postgres" && protocol !== "postgres" && protocol !== "postgresql") {
239
+ throw new Error("Expected postgres:// or postgresql:// DSN.");
240
+ }
241
+ if (adapter === "mysql" && protocol !== "mysql" && protocol !== "mariadb") {
242
+ throw new Error("Expected mysql:// or mariadb:// DSN.");
243
+ }
244
+ if (adapter === "mongodb" && protocol !== "mongodb" && protocol !== "mongodb+srv") {
245
+ throw new Error("Expected mongodb:// or mongodb+srv:// DSN.");
246
+ }
247
+ if (adapter === "redis" && protocol !== "redis" && protocol !== "rediss") {
248
+ throw new Error("Expected redis:// or rediss:// DSN.");
249
+ }
250
+ return parsed;
251
+ }
252
+ catch (error) {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ throw new Error(`Invalid DSN for ${adapter}: ${message}`);
255
+ }
256
+ }
257
+ function resolveSqlStatement(adapter, action, statement) {
258
+ const schemaDefaults = {
259
+ postgres: "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog','information_schema') ORDER BY 1,2;",
260
+ mysql: "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY 1,2;",
261
+ sqlite: "SELECT name, type FROM sqlite_master WHERE type IN ('table','view') ORDER BY name;",
262
+ };
263
+ if (action === "schema") {
264
+ return statement ?? schemaDefaults[adapter];
265
+ }
266
+ if (!statement) {
267
+ throw new Error(`Action "${action}" requires statement.`);
268
+ }
269
+ if (action === "explain") {
270
+ return /^\s*explain\b/i.test(statement) ? statement : `EXPLAIN ${statement}`;
271
+ }
272
+ return statement;
273
+ }
274
+ function resolveMongoStatement(action, statement) {
275
+ if (action === "schema") {
276
+ return statement ?? "db.getCollectionInfos()";
277
+ }
278
+ if (!statement) {
279
+ throw new Error(`Action "${action}" requires statement.`);
280
+ }
281
+ if (action === "explain") {
282
+ return statement;
283
+ }
284
+ return statement;
285
+ }
286
+ function resolveRedisStatement(action, statement) {
287
+ if (action === "schema") {
288
+ return statement ?? "INFO keyspace";
289
+ }
290
+ if (!statement) {
291
+ throw new Error(`Action "${action}" requires statement.`);
292
+ }
293
+ return statement;
294
+ }
295
+ function ensureAdapterCommandAvailable(ops, command, hint) {
296
+ if (ops.commandExists(command)) {
297
+ return;
298
+ }
299
+ throw new Error(hint);
300
+ }
301
+ function resolveSqlCommand(input) {
302
+ const statement = resolveSqlStatement(input.adapter, input.action, input.statement);
303
+ if (input.adapter === "postgres") {
304
+ ensureAdapterCommandAvailable(input.ops, "psql", 'Command "psql" is required for postgres adapter.');
305
+ const dsnValue = process.env[input.connection.dsnEnv];
306
+ if (!dsnValue) {
307
+ throw new Error(`Environment variable "${input.connection.dsnEnv}" is required for connection "${input.connection.name}".`);
308
+ }
309
+ const parsed = parseDsn(dsnValue, "postgres");
310
+ const env = {};
311
+ if (parsed.hostname)
312
+ env.PGHOST = parsed.hostname;
313
+ if (parsed.port)
314
+ env.PGPORT = parsed.port;
315
+ if (parsed.username)
316
+ env.PGUSER = decodeURIComponent(parsed.username);
317
+ if (parsed.password)
318
+ env.PGPASSWORD = decodeURIComponent(parsed.password);
319
+ const dbName = parsed.pathname.replace(/^\//, "");
320
+ if (dbName)
321
+ env.PGDATABASE = decodeURIComponent(dbName);
322
+ const sslMode = parsed.searchParams.get("sslmode");
323
+ if (sslMode)
324
+ env.PGSSLMODE = sslMode;
325
+ const args = [...input.connection.clientArgs];
326
+ if (input.format === "raw" || input.format === "json") {
327
+ args.push("-A", "-t");
328
+ }
329
+ args.push("-c", statement);
330
+ return {
331
+ command: "psql",
332
+ args,
333
+ env,
334
+ secretValues: [dsnValue, decodeURIComponent(parsed.password || "")].filter(Boolean),
335
+ };
336
+ }
337
+ if (input.adapter === "mysql") {
338
+ ensureAdapterCommandAvailable(input.ops, "mysql", 'Command "mysql" is required for mysql adapter.');
339
+ const dsnValue = process.env[input.connection.dsnEnv];
340
+ if (!dsnValue) {
341
+ throw new Error(`Environment variable "${input.connection.dsnEnv}" is required for connection "${input.connection.name}".`);
342
+ }
343
+ const parsed = parseDsn(dsnValue, "mysql");
344
+ const args = [...input.connection.clientArgs];
345
+ if (parsed.hostname)
346
+ args.push("--host", parsed.hostname);
347
+ if (parsed.port)
348
+ args.push("--port", parsed.port);
349
+ if (parsed.username)
350
+ args.push("--user", decodeURIComponent(parsed.username));
351
+ if (input.format === "raw" || input.format === "json") {
352
+ args.push("--batch", "--raw", "--skip-column-names");
353
+ }
354
+ const dbName = parsed.pathname.replace(/^\//, "");
355
+ if (dbName)
356
+ args.push(decodeURIComponent(dbName));
357
+ args.push("-e", statement);
358
+ const env = {};
359
+ if (parsed.password) {
360
+ env.MYSQL_PWD = decodeURIComponent(parsed.password);
361
+ }
362
+ return {
363
+ command: "mysql",
364
+ args,
365
+ env,
366
+ secretValues: [dsnValue, decodeURIComponent(parsed.password || "")].filter(Boolean),
367
+ };
368
+ }
369
+ ensureAdapterCommandAvailable(input.ops, "sqlite3", 'Command "sqlite3" is required for sqlite adapter.');
370
+ const sqlitePath = resolveToCwd(input.connection.sqlitePath, input.executionCwd);
371
+ const sqliteDir = join(sqlitePath, "..");
372
+ if (!existsSync(sqliteDir)) {
373
+ throw new Error(`sqlitePath parent directory does not exist: ${sqliteDir}`);
374
+ }
375
+ const args = [...input.connection.clientArgs];
376
+ if (input.format === "raw") {
377
+ args.push("-noheader");
378
+ }
379
+ if (input.format === "json") {
380
+ args.push("-json");
381
+ }
382
+ args.push(sqlitePath, statement);
383
+ return {
384
+ command: "sqlite3",
385
+ args,
386
+ secretValues: [],
387
+ };
388
+ }
389
+ function resolveMongoCommand(input) {
390
+ ensureAdapterCommandAvailable(input.ops, "mongosh", 'Command "mongosh" is required for mongodb adapter.');
391
+ const dsnValue = process.env[input.connection.dsnEnv];
392
+ if (!dsnValue) {
393
+ throw new Error(`Environment variable "${input.connection.dsnEnv}" is required for connection "${input.connection.name}".`);
394
+ }
395
+ parseDsn(dsnValue, "mongodb");
396
+ const statement = resolveMongoStatement(input.action, input.statement);
397
+ const args = [...input.connection.clientArgs, dsnValue, "--quiet", "--eval", statement];
398
+ return {
399
+ command: "mongosh",
400
+ args,
401
+ secretValues: [dsnValue],
402
+ };
403
+ }
404
+ function resolveRedisCommand(input) {
405
+ ensureAdapterCommandAvailable(input.ops, "redis-cli", 'Command "redis-cli" is required for redis adapter.');
406
+ const dsnValue = process.env[input.connection.dsnEnv];
407
+ if (!dsnValue) {
408
+ throw new Error(`Environment variable "${input.connection.dsnEnv}" is required for connection "${input.connection.name}".`);
409
+ }
410
+ const parsed = parseDsn(dsnValue, "redis");
411
+ const statement = resolveRedisStatement(input.action, input.statement);
412
+ const args = [...input.connection.clientArgs];
413
+ if (parsed.protocol === "rediss:") {
414
+ args.push("--tls");
415
+ }
416
+ if (parsed.hostname)
417
+ args.push("-h", parsed.hostname);
418
+ if (parsed.port)
419
+ args.push("-p", parsed.port);
420
+ if (parsed.username)
421
+ args.push("--user", decodeURIComponent(parsed.username));
422
+ const dbName = parsed.pathname.replace(/^\//, "");
423
+ if (dbName)
424
+ args.push("-n", decodeURIComponent(dbName));
425
+ if (input.format === "raw" || input.format === "json") {
426
+ args.push("--raw");
427
+ }
428
+ const env = {};
429
+ if (parsed.password) {
430
+ env.REDISCLI_AUTH = decodeURIComponent(parsed.password);
431
+ }
432
+ return {
433
+ command: "redis-cli",
434
+ args,
435
+ stdin: `${statement}\n`,
436
+ env,
437
+ secretValues: [dsnValue, decodeURIComponent(parsed.password || "")].filter(Boolean),
438
+ };
439
+ }
440
+ function resolveMigrateCommand(input) {
441
+ if (input.action !== "migrate") {
442
+ throw new Error("Internal error: resolveMigrateCommand called for non-migrate action.");
443
+ }
444
+ ensureWriteAllowed("migrate", input.allowWrite);
445
+ const migrateCwd = resolveToCwd(input.connection.migrate.cwd ?? ".", input.executionCwd);
446
+ const packageJson = readPackageJson(migrateCwd);
447
+ if (!packageJson) {
448
+ throw new Error("package.json is required for migrate action.");
449
+ }
450
+ const scriptCandidates = [];
451
+ if (input.script)
452
+ scriptCandidates.push(input.script);
453
+ if (input.connection.migrate.script)
454
+ scriptCandidates.push(input.connection.migrate.script);
455
+ if (input.migrateRunner === "auto") {
456
+ scriptCandidates.push("db:migrate", "migrate");
457
+ }
458
+ if (input.migrateRunner === "script" && scriptCandidates.length === 0) {
459
+ scriptCandidates.push("db:migrate");
460
+ }
461
+ const selectedScript = scriptCandidates.find((candidate) => packageJson.scripts[candidate]);
462
+ if (!selectedScript) {
463
+ throw new Error(`No migration script found. Checked: ${scriptCandidates.join(", ")} in ${packageJson.path}.`);
464
+ }
465
+ const packageManager = detectPackageManager(migrateCwd);
466
+ ensureCommandOrThrow(packageManager, `Command "${packageManager}" is required for migrate action.`);
467
+ const invocation = resolvePackageManagerRunInvocation(packageManager, selectedScript, [
468
+ ...input.connection.migrate.args,
469
+ ...input.args,
470
+ ]);
471
+ return {
472
+ cwd: migrateCwd,
473
+ command: {
474
+ command: invocation.command,
475
+ args: invocation.args,
476
+ secretValues: [],
477
+ },
478
+ };
479
+ }
480
+ function mapDbStatus(exitCode) {
481
+ if (exitCode === 0)
482
+ return "passed";
483
+ if (exitCode === 1)
484
+ return "failed";
485
+ return "error";
486
+ }
487
+ function redactArgs(args, secrets) {
488
+ const normalizedSecrets = secrets
489
+ .map((secret) => secret.trim())
490
+ .filter((secret) => secret.length > 0)
491
+ .sort((a, b) => b.length - a.length);
492
+ return args.map((arg) => {
493
+ let sanitized = arg;
494
+ for (const secret of normalizedSecrets) {
495
+ if (sanitized.includes(secret)) {
496
+ sanitized = sanitized.replaceAll(secret, "[REDACTED]");
497
+ }
498
+ }
499
+ if (/\b(password|passwd|pwd|token|secret)=/i.test(sanitized)) {
500
+ return sanitized.replace(/=(.+)$/i, "=[REDACTED]");
501
+ }
502
+ return sanitized;
503
+ });
504
+ }
505
+ function parseCounts(output) {
506
+ const selectMatch = output.match(/\bSELECT\s+(\d+)\b/i);
507
+ const updateMatch = output.match(/\b(?:UPDATE|DELETE)\s+(\d+)\b/i);
508
+ const insertMatch = output.match(/\bINSERT\s+\d+\s+(\d+)\b/i);
509
+ const rowsInSetMatch = output.match(/\b(\d+)\s+rows?\s+in set\b/i);
510
+ const mysqlChangedMatch = output.match(/\bRows matched:\s*(\d+)\s+Changed:\s*(\d+)\b/i);
511
+ const rowCount = selectMatch?.[1]
512
+ ? Number.parseInt(selectMatch[1], 10)
513
+ : rowsInSetMatch?.[1]
514
+ ? Number.parseInt(rowsInSetMatch[1], 10)
515
+ : undefined;
516
+ const affectedCount = updateMatch?.[1]
517
+ ? Number.parseInt(updateMatch[1], 10)
518
+ : insertMatch?.[1]
519
+ ? Number.parseInt(insertMatch[1], 10)
520
+ : mysqlChangedMatch?.[2]
521
+ ? Number.parseInt(mysqlChangedMatch[2], 10)
522
+ : undefined;
523
+ return {
524
+ rowCount: Number.isFinite(rowCount) ? rowCount : undefined,
525
+ affectedCount: Number.isFinite(affectedCount) ? affectedCount : undefined,
526
+ };
527
+ }
528
+ function renderSummary(details, output) {
529
+ const argsText = details.resolvedArgs.length > 0 ? ` ${details.resolvedArgs.join(" ")}` : "";
530
+ const lines = [
531
+ `db_run status: ${details.status}`,
532
+ `action: ${details.action}`,
533
+ `adapter: ${details.adapter}`,
534
+ `connection: ${details.connection}`,
535
+ `command: ${details.resolvedCommand}${argsText}`,
536
+ `cwd: ${details.cwd}`,
537
+ `write_requested: ${details.writeRequested ? "true" : "false"}`,
538
+ `write_allowed: ${details.writeAllowed ? "true" : "false"}`,
539
+ `exit_code: ${details.exitCode}`,
540
+ `duration_ms: ${details.durationMs}`,
541
+ ];
542
+ if (details.rowCount !== undefined) {
543
+ lines.push(`row_count: ${details.rowCount}`);
544
+ }
545
+ if (details.affectedCount !== undefined) {
546
+ lines.push(`affected_count: ${details.affectedCount}`);
547
+ }
548
+ lines.push("", output);
549
+ return lines.join("\n");
550
+ }
551
+ export function createDbRunTool(cwd, options) {
552
+ const permissionGuard = options?.permissionGuard;
553
+ const ops = {
554
+ ...defaultOps,
555
+ ...(options?.operations ?? {}),
556
+ };
557
+ return {
558
+ name: "db_run",
559
+ label: "db_run",
560
+ description: "Structured database operations across postgres/mysql/sqlite/mongodb/redis using named settings profiles with read-first safety and normalized status reporting.",
561
+ parameters: dbRunSchema,
562
+ execute: async (_toolCallId, input, signal) => {
563
+ const action = normalizeAction(input.action);
564
+ const requestedAdapter = input.adapter ?? "auto";
565
+ const format = normalizeFormat(input.format);
566
+ const allowWrite = input.allow_write === true;
567
+ const timeoutSeconds = normalizeTimeoutSeconds(input.timeout);
568
+ const statement = normalizeStatement(input.statement);
569
+ const args = normalizeStringArray(input.args);
570
+ const executionCwd = resolveToCwd(input.path || ".", cwd);
571
+ const migrateRunner = input.migrate_runner ?? "auto";
572
+ const script = normalizeScriptName(input.script);
573
+ const runtimeConfig = normalizeRuntimeConfig(options?.resolveRuntimeConfig?.());
574
+ const connection = resolveConnectionProfile({
575
+ runtimeConfig,
576
+ connection: input.connection,
577
+ adapter: requestedAdapter,
578
+ });
579
+ const resolvedAdapter = requestedAdapter === "auto" ? connection.adapter : requestedAdapter;
580
+ const writeRequested = action === "exec" || action === "migrate";
581
+ ensureWriteAllowed(action, allowWrite);
582
+ enforceReadFirstSafety(resolvedAdapter, action, statement);
583
+ if (permissionGuard) {
584
+ const allowed = await permissionGuard({
585
+ toolName: "db_run",
586
+ cwd: executionCwd,
587
+ input: {
588
+ action,
589
+ adapter: resolvedAdapter,
590
+ connection: connection.name,
591
+ format,
592
+ allowWrite,
593
+ args,
594
+ },
595
+ summary: `${action} on ${connection.name} (${resolvedAdapter})`,
596
+ });
597
+ if (!allowed) {
598
+ throw new Error("Permission denied for db_run operation.");
599
+ }
600
+ }
601
+ let commandCwd = executionCwd;
602
+ let resolvedCommand;
603
+ if (action === "migrate") {
604
+ const migrated = resolveMigrateCommand({
605
+ action,
606
+ allowWrite,
607
+ executionCwd,
608
+ migrateRunner,
609
+ connection,
610
+ script,
611
+ args,
612
+ });
613
+ resolvedCommand = migrated.command;
614
+ commandCwd = migrated.cwd;
615
+ }
616
+ else if (resolvedAdapter === "postgres" || resolvedAdapter === "mysql" || resolvedAdapter === "sqlite") {
617
+ resolvedCommand = resolveSqlCommand({
618
+ ops,
619
+ adapter: resolvedAdapter,
620
+ action,
621
+ statement,
622
+ format,
623
+ executionCwd,
624
+ connection,
625
+ });
626
+ if (args.length > 0) {
627
+ resolvedCommand.args = [...resolvedCommand.args, ...args];
628
+ }
629
+ }
630
+ else if (resolvedAdapter === "mongodb") {
631
+ resolvedCommand = resolveMongoCommand({
632
+ ops,
633
+ action,
634
+ statement,
635
+ connection,
636
+ });
637
+ if (args.length > 0) {
638
+ resolvedCommand.args = [...resolvedCommand.args, ...args];
639
+ }
640
+ }
641
+ else {
642
+ resolvedCommand = resolveRedisCommand({
643
+ ops,
644
+ action,
645
+ statement,
646
+ format,
647
+ connection,
648
+ });
649
+ if (args.length > 0) {
650
+ resolvedCommand.args = [...resolvedCommand.args, ...args];
651
+ }
652
+ }
653
+ const result = await ops.runCommand({
654
+ command: resolvedCommand.command,
655
+ args: resolvedCommand.args,
656
+ cwd: commandCwd,
657
+ timeoutMs: timeoutSeconds * 1000,
658
+ signal,
659
+ env: resolvedCommand.env,
660
+ stdin: resolvedCommand.stdin,
661
+ });
662
+ const status = mapDbStatus(result.exitCode);
663
+ const formatted = formatVerificationOutput(result.stdout, result.stderr, result.captureTruncated, "No database output");
664
+ const counts = parseCounts(`${result.stdout}\n${result.stderr}`);
665
+ const details = {
666
+ action,
667
+ adapter: resolvedAdapter,
668
+ connection: connection.name,
669
+ resolvedCommand: resolvedCommand.command,
670
+ resolvedArgs: redactArgs(resolvedCommand.args, resolvedCommand.secretValues),
671
+ cwd: commandCwd,
672
+ exitCode: result.exitCode,
673
+ status,
674
+ durationMs: result.durationMs,
675
+ rowCount: counts.rowCount,
676
+ affectedCount: counts.affectedCount,
677
+ writeRequested,
678
+ writeAllowed: allowWrite,
679
+ captureTruncated: result.captureTruncated || undefined,
680
+ truncation: formatted.truncation,
681
+ };
682
+ return {
683
+ content: [{ type: "text", text: renderSummary(details, formatted.text) }],
684
+ details,
685
+ };
686
+ },
687
+ };
688
+ }
689
+ export const dbRunTool = createDbRunTool(process.cwd());
690
+ //# sourceMappingURL=db-run.js.map