@carllee1983/dbcli 1.19.1 → 1.20.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/dist/cli.mjs CHANGED
@@ -2161,6 +2161,38 @@ var init_messages = __esm(() => {
2161
2161
  snippet_not_found: "Snippet not found: {name}",
2162
2162
  dry_run_header: "Dry-run preview (no execution):"
2163
2163
  },
2164
+ audit: {
2165
+ description: "Inspect, look up, clear, or check the health of the audit log",
2166
+ tail: {
2167
+ description: "Show recent audit entries (default: last 10 from current connection)"
2168
+ },
2169
+ show: {
2170
+ description: "Look up a single audit entry by id (UUID prefix >=4) or --recovery-ref"
2171
+ },
2172
+ clear: {
2173
+ description: "Delete audit log files (.jsonl + .jsonl.1) for the current connection",
2174
+ prompt_header: "About to clear audit log for connection '{conn}':",
2175
+ prompt_file_line: " {file} \u2014 {entries} entries, {size}",
2176
+ prompt_continue: "Continue? [y/N] ",
2177
+ requires_tty_or_yes: "Cannot prompt for confirmation in non-interactive session. Use --yes to clear without prompt.",
2178
+ summary_cleared: "Cleared {count} entries from '{conn}'.",
2179
+ summary_nothing: "Nothing to clear.",
2180
+ summary_failed: "Failed to clear: {message}."
2181
+ },
2182
+ health: {
2183
+ description: "Show AuditLogger health snapshot (size, entries, lock state, last write)"
2184
+ },
2185
+ disabled_hint: "Audit is disabled (audit.enabled = false in .dbcli). Use 'dbcli audit health' for details.",
2186
+ no_entries: "No audit entries.",
2187
+ show_no_match: "No audit entry matches '{prefix}'.",
2188
+ show_ambiguous: "Ambiguous prefix '{prefix}': matches {count} entries. Please use a longer prefix.",
2189
+ show_prefix_too_short: "Prefix must be at least 4 characters.",
2190
+ show_recovery_no_match: "No audit entry has recovery_ref '{ref}'.",
2191
+ show_recovery_ambiguous: "Multiple entries reference recovery_ref '{ref}': matches {count}. This should be rare; inspect file directly.",
2192
+ show_mutex_violation: "Provide either <id> argument or --recovery-ref, not both.",
2193
+ n_capped_warning: "--n value {requested} exceeds max {max}; capped to {max}.",
2194
+ n_must_be_positive: "--n must be a positive integer."
2195
+ },
2164
2196
  queries: {
2165
2197
  description: "Manage saved query snippets",
2166
2198
  list_description: "List saved snippets",
@@ -2411,6 +2443,38 @@ var init_messages2 = __esm(() => {
2411
2443
  snippet_not_found: "\u627E\u4E0D\u5230\u7247\u6BB5\uFF1A{name}",
2412
2444
  dry_run_header: "Dry-run \u9810\u89BD\uFF08\u4E0D\u6703\u57F7\u884C\uFF09\uFF1A"
2413
2445
  },
2446
+ audit: {
2447
+ description: "\u6AA2\u8996\u3001\u67E5\u8A62\u3001\u6E05\u7A7A\u6216\u6AA2\u67E5 audit log \u5065\u5EB7\u72C0\u614B",
2448
+ tail: {
2449
+ description: "\u986F\u793A\u6700\u8FD1\u7684 audit \u7D00\u9304\uFF08\u9810\u8A2D\uFF1A\u7576\u524D\u9023\u7DDA\u6700\u5F8C 10 \u7B46\uFF09"
2450
+ },
2451
+ show: {
2452
+ description: "\u4F9D id\uFF08UUID \u524D\u7DB4 \u22654 \u78BC\uFF09\u6216 --recovery-ref \u67E5\u8A62\u55AE\u7B46 audit \u7D00\u9304"
2453
+ },
2454
+ clear: {
2455
+ description: "\u522A\u9664\u7576\u524D\u9023\u7DDA\u7684 audit log \u6A94\u6848\uFF08.jsonl \u8207 .jsonl.1\uFF09",
2456
+ prompt_header: "\u5373\u5C07\u6E05\u7A7A\u9023\u7DDA '{conn}' \u7684 audit log\uFF1A",
2457
+ prompt_file_line: " {file} \u2014 {entries} \u7B46\uFF0C{size}",
2458
+ prompt_continue: "\u78BA\u8A8D\u57F7\u884C\uFF1F[y/N] ",
2459
+ requires_tty_or_yes: "\u975E\u4E92\u52D5 session \u7121\u6CD5\u986F\u793A\u78BA\u8A8D\u63D0\u793A\u3002\u8ACB\u4F7F\u7528 --yes \u76F4\u63A5\u6E05\u7A7A\u3002",
2460
+ summary_cleared: "\u5DF2\u5F9E '{conn}' \u6E05\u9664 {count} \u7B46 audit \u7D00\u9304\u3002",
2461
+ summary_nothing: "\u6C92\u6709\u53EF\u6E05\u9664\u7684\u5167\u5BB9\u3002",
2462
+ summary_failed: "\u6E05\u9664\u5931\u6557\uFF1A{message}\u3002"
2463
+ },
2464
+ health: {
2465
+ description: "\u986F\u793A AuditLogger \u5065\u5EB7\u72C0\u614B\uFF08\u6A94\u6848\u5927\u5C0F\u3001\u7B46\u6578\u3001lock \u72C0\u614B\u3001\u6700\u5F8C\u5BEB\u5165\u7D50\u679C\uFF09"
2466
+ },
2467
+ disabled_hint: "Audit \u5DF2\u505C\u7528\uFF08.dbcli \u4E2D audit.enabled = false\uFF09\u3002\u57F7\u884C `dbcli audit health` \u67E5\u770B\u8A73\u60C5\u3002",
2468
+ no_entries: "\u6C92\u6709 audit \u7D00\u9304\u3002",
2469
+ show_no_match: "\u627E\u4E0D\u5230\u7B26\u5408 '{prefix}' \u7684 audit \u7D00\u9304\u3002",
2470
+ show_ambiguous: "\u524D\u7DB4 '{prefix}' \u4E0D\u5920\u7CBE\u78BA\uFF1A\u5339\u914D\u5230 {count} \u7B46\u7D00\u9304\uFF0C\u8ACB\u6539\u7528\u66F4\u9577\u7684\u524D\u7DB4\u3002",
2471
+ show_prefix_too_short: "\u524D\u7DB4\u81F3\u5C11\u9700 4 \u500B\u5B57\u5143\u3002",
2472
+ show_recovery_no_match: "\u6C92\u6709\u4EFB\u4F55 audit \u7D00\u9304\u7684 recovery_ref \u70BA '{ref}'\u3002",
2473
+ show_recovery_ambiguous: "\u6709 {count} \u7B46\u7D00\u9304\u7684 recovery_ref \u70BA '{ref}'\uFF0C\u9019\u61C9\u5C6C\u7F55\u898B\u60C5\u6CC1\uFF1B\u8ACB\u76F4\u63A5\u6AA2\u8996\u6A94\u6848\u3002",
2474
+ show_mutex_violation: "\u8ACB\u64C7\u4E00\u63D0\u4F9B <id> \u53C3\u6578\u6216 --recovery-ref\uFF0C\u5169\u8005\u4E0D\u53EF\u540C\u6642\u4F7F\u7528\u3002",
2475
+ n_capped_warning: "--n \u503C {requested} \u8D85\u904E\u4E0A\u9650 {max}\uFF0C\u5DF2\u81EA\u52D5\u7E2E\u6E1B\u70BA {max}\u3002",
2476
+ n_must_be_positive: "--n \u5FC5\u9808\u70BA\u6B63\u6574\u6578\u3002"
2477
+ },
2414
2478
  queries: {
2415
2479
  description: "\u7BA1\u7406\u5DF2\u4FDD\u5B58\u7684\u67E5\u8A62\u7247\u6BB5",
2416
2480
  list_description: "\u5217\u51FA\u6240\u6709\u7247\u6BB5",
@@ -6776,7 +6840,9 @@ __export(exports_validation, {
6776
6840
  DbcliConfigV2Schema: () => DbcliConfigV2Schema,
6777
6841
  DbcliConfigSchema: () => DbcliConfigSchema,
6778
6842
  ConnectionConfigSchema: () => ConnectionConfigSchema,
6779
- BlacklistConfigSchema: () => BlacklistConfigSchema
6843
+ BlacklistConfigSchema: () => BlacklistConfigSchema,
6844
+ AuditRotationConfigSchema: () => AuditRotationConfigSchema,
6845
+ AuditConfigSchema: () => AuditConfigSchema
6780
6846
  });
6781
6847
  function validateFormat(value, allowedFormats, commandName) {
6782
6848
  if (!allowedFormats.includes(value)) {
@@ -6784,7 +6850,7 @@ function validateFormat(value, allowedFormats, commandName) {
6784
6850
  throw new Error(`Invalid format "${value}" for ${commandName}. Allowed: ${allowed}`);
6785
6851
  }
6786
6852
  }
6787
- var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, MetadataSchema, BlacklistConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionSchema, DbcliConfigV2Schema;
6853
+ var EnvRefSchema, StringOrEnvRef, NumberOrEnvRef, OptStringOrEnvRef, OptNumberOrEnvRef, MongoDBConnectionConfigSchema, SqlConnectionConfigSchema, RedisConnectionConfigSchema, ElasticsearchConnectionConfigSchema, ConnectionConfigSchema, PermissionSchema, MetadataSchema, BlacklistConfigSchema, AuditRotationConfigSchema, AuditConfigSchema, DbcliConfigSchema, SqlNamedConnectionSchema, MongoDBNamedConnectionSchema, RedisNamedConnectionSchema, ElasticsearchNamedConnectionSchema, NamedConnectionSchema, DbcliConfigV2Schema;
6788
6854
  var init_validation = __esm(() => {
6789
6855
  init_zod();
6790
6856
  EnvRefSchema = exports_external.object({
@@ -6850,12 +6916,24 @@ var init_validation = __esm(() => {
6850
6916
  tables: exports_external.array(exports_external.string()).default([]),
6851
6917
  columns: exports_external.record(exports_external.array(exports_external.string())).default({})
6852
6918
  }).optional().default({ tables: [], columns: {} });
6919
+ AuditRotationConfigSchema = exports_external.object({
6920
+ max_bytes: exports_external.number().int().positive().default(10485760),
6921
+ max_entries: exports_external.number().int().positive().default(1000)
6922
+ }).optional().default({ max_bytes: 10485760, max_entries: 1000 });
6923
+ AuditConfigSchema = exports_external.object({
6924
+ enabled: exports_external.boolean().default(true),
6925
+ rotation: AuditRotationConfigSchema
6926
+ }).optional().default({
6927
+ enabled: true,
6928
+ rotation: { max_bytes: 10485760, max_entries: 1000 }
6929
+ });
6853
6930
  DbcliConfigSchema = exports_external.object({
6854
6931
  connection: ConnectionConfigSchema,
6855
6932
  permission: PermissionSchema,
6856
6933
  schema: exports_external.record(exports_external.any()).optional().default({}),
6857
6934
  metadata: MetadataSchema,
6858
- blacklist: BlacklistConfigSchema
6935
+ blacklist: BlacklistConfigSchema,
6936
+ audit: AuditConfigSchema
6859
6937
  });
6860
6938
  SqlNamedConnectionSchema = SqlConnectionConfigSchema.extend({
6861
6939
  permission: PermissionSchema,
@@ -6888,7 +6966,8 @@ var init_validation = __esm(() => {
6888
6966
  schema: exports_external.record(exports_external.any()).optional().default({}),
6889
6967
  schemas: exports_external.record(exports_external.record(exports_external.any())).optional().default({}),
6890
6968
  metadata: MetadataSchema,
6891
- blacklist: BlacklistConfigSchema
6969
+ blacklist: BlacklistConfigSchema,
6970
+ audit: AuditConfigSchema
6892
6971
  }).refine((config) => (config.default in config.connections), {
6893
6972
  message: "Default connection must exist in connections",
6894
6973
  path: ["default"]
@@ -8043,6 +8122,13 @@ var init_schema_loader = __esm(() => {
8043
8122
  });
8044
8123
 
8045
8124
  // src/core/config.ts
8125
+ var exports_config = {};
8126
+ __export(exports_config, {
8127
+ setGlobalConnectionName: () => setGlobalConnectionName,
8128
+ getSchemaIsolationConnectionName: () => getSchemaIsolationConnectionName,
8129
+ getGlobalConnectionName: () => getGlobalConnectionName,
8130
+ configModule: () => configModule
8131
+ });
8046
8132
  import { join as join7 } from "path";
8047
8133
  function setGlobalConnectionName(name) {
8048
8134
  _globalConnectionName = name;
@@ -8131,7 +8217,11 @@ var init_config = __esm(() => {
8131
8217
  metadata: {
8132
8218
  version: "1.0"
8133
8219
  },
8134
- blacklist: { tables: [], columns: {} }
8220
+ blacklist: { tables: [], columns: {} },
8221
+ audit: {
8222
+ enabled: true,
8223
+ rotation: { max_bytes: 10485760, max_entries: 1000 }
8224
+ }
8135
8225
  };
8136
8226
  configModule = {
8137
8227
  async read(path, connectionName) {
@@ -8201,7 +8291,8 @@ var init_config = __esm(() => {
8201
8291
  permission: resolved.permission,
8202
8292
  schema,
8203
8293
  metadata: v2Config.metadata,
8204
- blacklist: v2Config.blacklist
8294
+ blacklist: v2Config.blacklist,
8295
+ audit: v2Config.audit
8205
8296
  });
8206
8297
  }
8207
8298
  const resolvedConfig = resolveEnvReferences(config, process.env, undefined, false);
@@ -20672,6 +20763,7 @@ var init_capabilities = __esm(() => {
20672
20763
  "init",
20673
20764
  "use",
20674
20765
  "list",
20766
+ "schema",
20675
20767
  "schemaSingle",
20676
20768
  "schemaFullScan",
20677
20769
  "query",
@@ -20696,18 +20788,27 @@ var init_capabilities = __esm(() => {
20696
20788
  "completion",
20697
20789
  "upgrade",
20698
20790
  "recover",
20699
- "skill"
20791
+ "skill",
20792
+ "auditTail",
20793
+ "auditShow",
20794
+ "auditClear",
20795
+ "auditHealth"
20700
20796
  ]);
20701
20797
  ENGINE_INDEPENDENT = {
20702
20798
  completion: cap("not-applicable", "none", "Shell completion is engine-independent."),
20703
20799
  upgrade: cap("not-applicable", "local-write", "Package update checks are engine-independent."),
20704
20800
  recover: cap("not-applicable", "dry-run", "Recovery operates on saved envelopes and gated command steps."),
20705
- skill: cap("not-applicable", "local-write", "Skill and task-pack generation are engine-independent.")
20801
+ skill: cap("not-applicable", "local-write", "Skill and task-pack generation are engine-independent."),
20802
+ auditTail: cap("supported", "readonly", "Reads JSONL audit entries; never writes to engines."),
20803
+ auditShow: cap("supported", "readonly", "Looks up a single audit entry by id prefix or recovery_ref."),
20804
+ auditHealth: cap("supported", "readonly", "Renders AuditLogger.getHealth() snapshot."),
20805
+ auditClear: cap("supported", "local-write", "Removes <conn>.jsonl + .jsonl.1 from local disk; never touches DB.")
20706
20806
  };
20707
20807
  SQL_BASE = {
20708
20808
  init: cap("supported", "interactive", "SQL connection initialization is supported."),
20709
20809
  use: cap("supported", "local-write", "V2 config supports named SQL connections."),
20710
20810
  list: cap("supported", "readonly", "Lists relational tables."),
20811
+ schema: cap("supported", "readonly", "Unified schema command (single-table or full scan)."),
20711
20812
  schemaSingle: cap("supported", "readonly", "Reads a single table schema."),
20712
20813
  schemaFullScan: cap("supported", "readonly", "Full scan, refresh, and reset are supported."),
20713
20814
  query: cap("supported", "readonly", "Runs SQL through permission and blacklist guards."),
@@ -24225,6 +24326,507 @@ var init_blacklist = __esm(() => {
24225
24326
  };
24226
24327
  });
24227
24328
 
24329
+ // src/core/audit/lock.ts
24330
+ import { hostname } from "os";
24331
+ import { dirname } from "path";
24332
+ import { mkdir as mkdir2, open, rm } from "fs/promises";
24333
+
24334
+ class AuditLockManager {
24335
+ auditFilePath;
24336
+ lockPath;
24337
+ lockAcquiredAt = null;
24338
+ constructor(auditFilePath) {
24339
+ this.auditFilePath = auditFilePath;
24340
+ this.lockPath = `${auditFilePath}.lock`;
24341
+ }
24342
+ getLockfilePath() {
24343
+ return this.lockPath;
24344
+ }
24345
+ isLockHeld() {
24346
+ return this.lockAcquiredAt !== null;
24347
+ }
24348
+ async acquireLock(operationName = "audit-write") {
24349
+ const startTime = Date.now();
24350
+ let backoffMs = LOCK_BACKOFF_START_MS;
24351
+ while (true) {
24352
+ const elapsed = Date.now() - startTime;
24353
+ if (elapsed > LOCK_RETRY_BUDGET_MS) {
24354
+ return false;
24355
+ }
24356
+ if (await this.tryAcquireLock(operationName)) {
24357
+ this.lockAcquiredAt = Date.now();
24358
+ return true;
24359
+ }
24360
+ const waitTime = Math.min(backoffMs, LOCK_BACKOFF_MAX_MS);
24361
+ await new Promise((resolve2) => setTimeout(resolve2, waitTime));
24362
+ backoffMs = Math.min(backoffMs * 1.5, LOCK_BACKOFF_MAX_MS);
24363
+ }
24364
+ }
24365
+ async releaseLock() {
24366
+ if (!this.lockAcquiredAt)
24367
+ return false;
24368
+ try {
24369
+ await rm(this.lockPath, { force: true });
24370
+ this.lockAcquiredAt = null;
24371
+ return true;
24372
+ } catch {
24373
+ this.lockAcquiredAt = null;
24374
+ return false;
24375
+ }
24376
+ }
24377
+ async withLock(operation, operationName = "audit-write") {
24378
+ const acquired = await this.acquireLock(operationName);
24379
+ if (!acquired) {
24380
+ return { skipped: "lock-budget-exhausted" };
24381
+ }
24382
+ try {
24383
+ return await operation();
24384
+ } finally {
24385
+ await this.releaseLock();
24386
+ }
24387
+ }
24388
+ async tryAcquireLock(operationName) {
24389
+ try {
24390
+ await mkdir2(dirname(this.lockPath), { recursive: true });
24391
+ const lockFile = Bun.file(this.lockPath);
24392
+ if (await lockFile.exists()) {
24393
+ const lockContent = await lockFile.json();
24394
+ const lockAge = Date.now() - lockContent.timestamp;
24395
+ const staleLockThresholdMs = LOCK_RETRY_BUDGET_MS * STALE_LOCK_MULTIPLIER;
24396
+ if (lockAge > staleLockThresholdMs) {
24397
+ await rm(this.lockPath, { force: true });
24398
+ } else {
24399
+ return false;
24400
+ }
24401
+ }
24402
+ const lockData = {
24403
+ pid: process.pid,
24404
+ operation: operationName,
24405
+ timestamp: Date.now(),
24406
+ hostname: hostname()
24407
+ };
24408
+ const handle = await open(this.lockPath, "wx");
24409
+ try {
24410
+ await handle.writeFile(JSON.stringify(lockData), "utf8");
24411
+ } finally {
24412
+ await handle.close();
24413
+ }
24414
+ return true;
24415
+ } catch {
24416
+ return false;
24417
+ }
24418
+ }
24419
+ }
24420
+ var LOCK_RETRY_BUDGET_MS = 200, LOCK_BACKOFF_START_MS = 5, LOCK_BACKOFF_MAX_MS = 50, STALE_LOCK_MULTIPLIER = 10;
24421
+ var init_lock = () => {};
24422
+
24423
+ // src/core/audit/rotation.ts
24424
+ import { rename } from "fs/promises";
24425
+ function shouldRotate(stats, thresholds, nextLineByteLength) {
24426
+ const bytesAfter = stats.currentSizeBytes + nextLineByteLength;
24427
+ const entriesAfter = stats.currentEntryCount + 1;
24428
+ return bytesAfter >= thresholds.maxBytes || entriesAfter > thresholds.maxEntries;
24429
+ }
24430
+ async function rotate(currentPath, previousPath) {
24431
+ try {
24432
+ await rename(currentPath, previousPath);
24433
+ } catch {}
24434
+ }
24435
+ var init_rotation = () => {};
24436
+
24437
+ // src/core/audit/logger.ts
24438
+ import { appendFile, mkdir as mkdir3, readFile, stat } from "fs/promises";
24439
+ import { join as join10 } from "path";
24440
+ import { randomUUID as randomUUID2 } from "crypto";
24441
+
24442
+ class AuditLogger {
24443
+ auditDir;
24444
+ auditFilePath;
24445
+ previousFilePath;
24446
+ enabled;
24447
+ maxBytes;
24448
+ maxEntries;
24449
+ sessionIdService;
24450
+ lockManager;
24451
+ writerInitialized = false;
24452
+ currentSizeBytes = 0;
24453
+ currentEntryCount = 0;
24454
+ lastWrite = null;
24455
+ lastError = null;
24456
+ warnedOnceThisProcess = false;
24457
+ cachedSessionId = null;
24458
+ lastRotatedAt;
24459
+ lastRotatedPrevious;
24460
+ writeChain = Promise.resolve();
24461
+ constructor(opts) {
24462
+ this.enabled = opts.enabled;
24463
+ this.maxBytes = opts.rotation.maxBytes;
24464
+ this.maxEntries = opts.rotation.maxEntries;
24465
+ this.auditDir = join10(opts.storagePath, ".dbcli", "audit");
24466
+ this.auditFilePath = join10(this.auditDir, `${opts.connectionName}.jsonl`);
24467
+ this.previousFilePath = `${this.auditFilePath}.1`;
24468
+ this.sessionIdService = opts.sessionIdService;
24469
+ this.lockManager = opts.lockManager ?? new AuditLockManager(this.auditFilePath);
24470
+ }
24471
+ async write(entry) {
24472
+ const writeOp = this.writeChain.then(() => this.writeInternal(entry));
24473
+ this.writeChain = writeOp.then(() => {
24474
+ return;
24475
+ }, () => {
24476
+ return;
24477
+ });
24478
+ return writeOp;
24479
+ }
24480
+ async writeInternal(entry) {
24481
+ if (!this.enabled) {
24482
+ return { skipped: "disabled" };
24483
+ }
24484
+ try {
24485
+ const sessionId = await this.sessionIdService.resolve();
24486
+ this.cachedSessionId = sessionId;
24487
+ await mkdir3(this.auditDir, { recursive: true });
24488
+ if (!this.writerInitialized) {
24489
+ await this.syncCountersFromDisk();
24490
+ this.writerInitialized = true;
24491
+ }
24492
+ const id = randomUUID2();
24493
+ const ts = new Date().toISOString();
24494
+ const enriched = {
24495
+ ...entry,
24496
+ id,
24497
+ ts,
24498
+ session_id: sessionId
24499
+ };
24500
+ const line = JSON.stringify(enriched) + `
24501
+ `;
24502
+ const lineBytes = Buffer.byteLength(line, "utf8");
24503
+ const lockResult = await this.lockManager.withLock(async () => {
24504
+ let rotated = false;
24505
+ if (shouldRotate({
24506
+ currentSizeBytes: this.currentSizeBytes,
24507
+ currentEntryCount: this.currentEntryCount
24508
+ }, { maxBytes: this.maxBytes, maxEntries: this.maxEntries }, lineBytes)) {
24509
+ await rotate(this.auditFilePath, this.previousFilePath);
24510
+ this.lastRotatedAt = new Date().toISOString();
24511
+ this.lastRotatedPrevious = this.previousFilePath;
24512
+ this.currentSizeBytes = 0;
24513
+ this.currentEntryCount = 0;
24514
+ rotated = true;
24515
+ }
24516
+ await appendFile(this.auditFilePath, line, { encoding: "utf8" });
24517
+ this.currentSizeBytes += lineBytes;
24518
+ this.currentEntryCount += 1;
24519
+ return { rotated };
24520
+ }, "audit-write");
24521
+ if (lockResult !== null && typeof lockResult === "object" && "skipped" in lockResult) {
24522
+ this.handleFailure("lock-budget-exhausted");
24523
+ return { skipped: "lock-budget-exhausted" };
24524
+ }
24525
+ this.lastWrite = { ts, success: true };
24526
+ return { success: true, rotated: lockResult.rotated, id };
24527
+ } catch (err) {
24528
+ const message = err instanceof Error ? err.message : String(err);
24529
+ this.handleFailure(message);
24530
+ return { skipped: "write-failed", error: message };
24531
+ }
24532
+ }
24533
+ getHealth() {
24534
+ return {
24535
+ enabled: this.enabled,
24536
+ writerInitialized: this.writerInitialized,
24537
+ currentFile: this.auditFilePath,
24538
+ currentSizeBytes: this.currentSizeBytes,
24539
+ currentEntryCount: this.currentEntryCount,
24540
+ rotationUsage: {
24541
+ bytes: {
24542
+ current: this.currentSizeBytes,
24543
+ max: this.maxBytes,
24544
+ pct: this.maxBytes > 0 ? this.currentSizeBytes / this.maxBytes * 100 : 0
24545
+ },
24546
+ entries: {
24547
+ current: this.currentEntryCount,
24548
+ max: this.maxEntries,
24549
+ pct: this.maxEntries > 0 ? this.currentEntryCount / this.maxEntries * 100 : 0
24550
+ }
24551
+ },
24552
+ lock: { state: this.lockManager.isLockHeld() ? "held" : "free" },
24553
+ lastWrite: this.lastWrite,
24554
+ lastError: this.lastError,
24555
+ sessionId: this.cachedSessionId,
24556
+ rotation: {
24557
+ lastRotatedAt: this.lastRotatedAt,
24558
+ previousFile: this.lastRotatedPrevious
24559
+ }
24560
+ };
24561
+ }
24562
+ async syncCountersFromDisk() {
24563
+ try {
24564
+ const s = await stat(this.auditFilePath);
24565
+ this.currentSizeBytes = s.size;
24566
+ const raw = await readFile(this.auditFilePath, "utf8");
24567
+ this.currentEntryCount = raw.split(`
24568
+ `).filter(Boolean).length;
24569
+ } catch {
24570
+ this.currentSizeBytes = 0;
24571
+ this.currentEntryCount = 0;
24572
+ }
24573
+ }
24574
+ handleFailure(message) {
24575
+ const ts = new Date().toISOString();
24576
+ this.lastError = { ts, message };
24577
+ this.lastWrite = { ts, success: false, error: message };
24578
+ if (!this.warnedOnceThisProcess) {
24579
+ process.stderr.write(`${WARN_PREFIX} warning: audit write failed (${message}); subsequent failures suppressed this process.
24580
+ `);
24581
+ this.warnedOnceThisProcess = true;
24582
+ }
24583
+ }
24584
+ }
24585
+ var WARN_PREFIX = "[dbcli audit]";
24586
+ var init_logger = __esm(() => {
24587
+ init_lock();
24588
+ init_rotation();
24589
+ });
24590
+
24591
+ // src/core/audit/session-id.ts
24592
+ import { mkdir as mkdir4, readFile as readFile2, rename as rename2, stat as stat2, writeFile } from "fs/promises";
24593
+ import { randomBytes } from "crypto";
24594
+ import { dirname as dirname2, join as join11 } from "path";
24595
+ function generateSessionId(pid, nowMs) {
24596
+ const random = randomBytes(3).toString("hex");
24597
+ return `${pid}-${nowMs}-${random}`;
24598
+ }
24599
+ async function readSessionIdFile(storagePath) {
24600
+ const target = join11(storagePath, LAST_SESSION_ID_RELATIVE);
24601
+ try {
24602
+ await stat2(target);
24603
+ } catch {
24604
+ return null;
24605
+ }
24606
+ try {
24607
+ const raw = await readFile2(target, "utf8");
24608
+ const parsed = JSON.parse(raw);
24609
+ if (parsed && typeof parsed.sessionId === "string" && typeof parsed.pid === "number" && typeof parsed.createdAt === "string") {
24610
+ return {
24611
+ sessionId: parsed.sessionId,
24612
+ pid: parsed.pid,
24613
+ createdAt: parsed.createdAt
24614
+ };
24615
+ }
24616
+ return null;
24617
+ } catch {
24618
+ return null;
24619
+ }
24620
+ }
24621
+ async function writeSessionIdFile(storagePath, payload) {
24622
+ const target = join11(storagePath, LAST_SESSION_ID_RELATIVE);
24623
+ const tmp = `${target}.tmp`;
24624
+ try {
24625
+ await mkdir4(dirname2(target), { recursive: true });
24626
+ await writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
24627
+ await rename2(tmp, target);
24628
+ } catch {}
24629
+ }
24630
+
24631
+ class SessionIdService {
24632
+ storagePath;
24633
+ cached = null;
24634
+ constructor(storagePath) {
24635
+ this.storagePath = storagePath;
24636
+ }
24637
+ async resolve() {
24638
+ if (this.cached !== null)
24639
+ return this.cached;
24640
+ const fromEnv = process.env.DBCLI_SESSION_ID;
24641
+ if (fromEnv && fromEnv.length > 0) {
24642
+ this.cached = fromEnv;
24643
+ return fromEnv;
24644
+ }
24645
+ const saved = await readSessionIdFile(this.storagePath);
24646
+ if (saved && saved.pid === process.pid) {
24647
+ this.cached = saved.sessionId;
24648
+ return saved.sessionId;
24649
+ }
24650
+ const id = generateSessionId(process.pid, Date.now());
24651
+ await writeSessionIdFile(this.storagePath, {
24652
+ sessionId: id,
24653
+ pid: process.pid,
24654
+ createdAt: new Date().toISOString()
24655
+ });
24656
+ this.cached = id;
24657
+ return id;
24658
+ }
24659
+ reset() {
24660
+ this.cached = null;
24661
+ }
24662
+ }
24663
+ var LAST_SESSION_ID_RELATIVE = ".dbcli/last-session-id";
24664
+ var init_session_id = () => {};
24665
+
24666
+ // src/utils/redaction.ts
24667
+ function redactArgv(argv) {
24668
+ if (argv.length === 0)
24669
+ return "<unknown>";
24670
+ const out = [argv[0]];
24671
+ if (argv.length === 1)
24672
+ return out.join(" ");
24673
+ const sub = argv[1];
24674
+ out.push(sub);
24675
+ for (let i = 2;i < argv.length; i++) {
24676
+ const tok = argv[i];
24677
+ if (tok.startsWith("--")) {
24678
+ const [name, inlineValue] = tok.split("=");
24679
+ if (REDACTED_VALUE_FLAGS.has(name)) {
24680
+ out.push(`${name} <redacted>`);
24681
+ if (inlineValue === undefined)
24682
+ i++;
24683
+ continue;
24684
+ }
24685
+ if (KEEP_VALUE_FLAGS.has(name)) {
24686
+ if (inlineValue !== undefined) {
24687
+ out.push(tok);
24688
+ } else {
24689
+ out.push(name);
24690
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
24691
+ out.push(argv[++i]);
24692
+ }
24693
+ }
24694
+ continue;
24695
+ }
24696
+ out.push(tok);
24697
+ continue;
24698
+ }
24699
+ if (i === 2 && SQL_SUBCOMMANDS.has(sub)) {
24700
+ out.push("<sql>");
24701
+ continue;
24702
+ }
24703
+ out.push(tok);
24704
+ }
24705
+ return out.join(" ");
24706
+ }
24707
+ function redactSensitive(text2) {
24708
+ return text2.replace(/\b(password|token|apiKey|secret|key|token|auth|credential|pass|pwd|sid)([:=]|\s+)([^\s"';,]+)/gi, "$1$2<redacted>");
24709
+ }
24710
+ function redactSql(sql) {
24711
+ const redacted = sql.replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g, "'?'").replace(/\b\d+(\.\d+)?\b/g, "0");
24712
+ return redactSensitive(redacted);
24713
+ }
24714
+ var SQL_SUBCOMMANDS, REDACTED_VALUE_FLAGS, KEEP_VALUE_FLAGS;
24715
+ var init_redaction = __esm(() => {
24716
+ SQL_SUBCOMMANDS = new Set(["query", "export"]);
24717
+ REDACTED_VALUE_FLAGS = new Set([
24718
+ "--where",
24719
+ "--set",
24720
+ "--data",
24721
+ "--param",
24722
+ "--param-file",
24723
+ "--config",
24724
+ "--use",
24725
+ "--password",
24726
+ "--token",
24727
+ "--secret"
24728
+ ]);
24729
+ KEEP_VALUE_FLAGS = new Set(["--format", "--conn-name"]);
24730
+ });
24731
+
24732
+ // src/utils/engine-hints.ts
24733
+ var exports_engine_hints = {};
24734
+ __export(exports_engine_hints, {
24735
+ getOperationTarget: () => getOperationTarget,
24736
+ extractTableName: () => extractTableName
24737
+ });
24738
+ function extractTableName(sql) {
24739
+ const match = sql.match(TABLE_NAME_RE);
24740
+ return match ? match[1] ?? null : null;
24741
+ }
24742
+ function getOperationTarget(system, command, options, sql) {
24743
+ if (system === "mongodb") {
24744
+ return options.collection || "<unknown-collection>";
24745
+ }
24746
+ if (system === "elasticsearch") {
24747
+ return options.index || options.collection || "<unknown-index>";
24748
+ }
24749
+ if (system === "redis") {
24750
+ return options.table || options.key || "<unknown-key>";
24751
+ }
24752
+ if (sql) {
24753
+ const table = extractTableName(sql);
24754
+ if (table)
24755
+ return table;
24756
+ }
24757
+ return options.table || "<unknown-target>";
24758
+ }
24759
+ var TABLE_NAME_RE;
24760
+ var init_engine_hints = __esm(() => {
24761
+ TABLE_NAME_RE = /\b(?:FROM|INTO|UPDATE)\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i;
24762
+ });
24763
+
24764
+ // src/core/audit/integration-helper.ts
24765
+ async function getAuditLogger(config, configPath) {
24766
+ const storagePath = await resolveConfigStoragePath(configPath);
24767
+ const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
24768
+ const key = `${storagePath}:${connName}`;
24769
+ if (!_loggers.has(key)) {
24770
+ if (!_sessionIdService) {
24771
+ _sessionIdService = new SessionIdService(storagePath);
24772
+ }
24773
+ _loggers.set(key, new AuditLogger({
24774
+ storagePath,
24775
+ connectionName: connName,
24776
+ enabled: config.audit?.enabled ?? true,
24777
+ rotation: {
24778
+ maxBytes: config.audit?.rotation?.max_bytes ?? 10485760,
24779
+ maxEntries: config.audit?.rotation?.max_entries ?? 1000
24780
+ },
24781
+ sessionIdService: _sessionIdService
24782
+ }));
24783
+ }
24784
+ return _loggers.get(key);
24785
+ }
24786
+ async function writeAuditEntry(config, commandName, options, outcome) {
24787
+ try {
24788
+ const logger = await getAuditLogger(config, options.config || ".dbcli");
24789
+ const engine = config.connection?.system || "postgresql";
24790
+ const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
24791
+ let tier = getEngineCapability(engine, commandName).tier;
24792
+ if (options.dryRun || options.plan) {
24793
+ tier = "dry-run";
24794
+ }
24795
+ let errorMessage;
24796
+ if (outcome.error) {
24797
+ errorMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
24798
+ errorMessage = redactSensitive(errorMessage);
24799
+ }
24800
+ const entry = {
24801
+ engine,
24802
+ command: commandName,
24803
+ side_effect_tier: tier,
24804
+ target,
24805
+ success: outcome.success,
24806
+ redacted_query: redactArgv(process.argv),
24807
+ ...outcome.sql && { redacted_sql: redactSql(outcome.sql) },
24808
+ ...errorMessage && { error: errorMessage },
24809
+ ...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
24810
+ metadata: outcome.metadata
24811
+ };
24812
+ const result = await logger.write(entry);
24813
+ return "success" in result ? result.id : null;
24814
+ } catch {
24815
+ return null;
24816
+ }
24817
+ }
24818
+ var _sessionIdService = null, _loggers;
24819
+ var init_integration_helper = __esm(() => {
24820
+ init_logger();
24821
+ init_session_id();
24822
+ init_config_binding();
24823
+ init_config();
24824
+ init_capabilities();
24825
+ init_redaction();
24826
+ init_engine_hints();
24827
+ _loggers = new Map;
24828
+ });
24829
+
24228
24830
  // src/core/recovery/types.ts
24229
24831
  var RECOVERY_SCHEMA_VERSION = 1, RECOVERY_CODES, SchemaCacheMissingError, RECOVERY_CODE_METADATA;
24230
24832
  var init_types3 = __esm(() => {
@@ -25417,8 +26019,9 @@ var init_render_markdown = __esm(() => {
25417
26019
  });
25418
26020
 
25419
26021
  // src/core/recovery/last-envelope.ts
25420
- import { writeFile, readFile, rename, mkdir as mkdir2, stat } from "fs/promises";
25421
- import { dirname, join as join10 } from "path";
26022
+ import { writeFile as writeFile2, readFile as readFile3, rename as rename3, mkdir as mkdir5, stat as stat3 } from "fs/promises";
26023
+ import { dirname as dirname3, join as join12 } from "path";
26024
+ import { randomUUID as randomUUID3 } from "crypto";
25422
26025
  function sanitizeCommandSummary(argv) {
25423
26026
  if (argv.length === 0)
25424
26027
  return "<unknown>";
@@ -25431,13 +26034,13 @@ function sanitizeCommandSummary(argv) {
25431
26034
  const tok = argv[i];
25432
26035
  if (tok.startsWith("--")) {
25433
26036
  const [name, inlineValue] = tok.split("=");
25434
- if (REDACTED_VALUE_FLAGS.has(name)) {
26037
+ if (REDACTED_VALUE_FLAGS2.has(name)) {
25435
26038
  out.push(`${name} <redacted>`);
25436
26039
  if (inlineValue === undefined)
25437
26040
  i++;
25438
26041
  continue;
25439
26042
  }
25440
- if (KEEP_VALUE_FLAGS.has(name)) {
26043
+ if (KEEP_VALUE_FLAGS2.has(name)) {
25441
26044
  if (inlineValue !== undefined) {
25442
26045
  out.push(tok);
25443
26046
  } else {
@@ -25451,7 +26054,7 @@ function sanitizeCommandSummary(argv) {
25451
26054
  out.push(tok);
25452
26055
  continue;
25453
26056
  }
25454
- if (i === 2 && SQL_SUBCOMMANDS.has(sub)) {
26057
+ if (i === 2 && SQL_SUBCOMMANDS2.has(sub)) {
25455
26058
  out.push("<sql>");
25456
26059
  continue;
25457
26060
  }
@@ -25459,46 +26062,48 @@ function sanitizeCommandSummary(argv) {
25459
26062
  }
25460
26063
  return out.join(" ");
25461
26064
  }
25462
- async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date) {
25463
- const target = join10(cwd, LAST_ENVELOPE_PATH);
26065
+ async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id = randomUUID3(), auditRef) {
26066
+ const target = join12(cwd, LAST_ENVELOPE_PATH);
25464
26067
  const tmp = `${target}.tmp`;
25465
26068
  const payload = {
25466
26069
  schemaVersion: 1,
26070
+ id,
26071
+ ...auditRef !== undefined && { audit_ref: auditRef },
25467
26072
  savedAt: now().toISOString(),
25468
26073
  command: sanitizeCommandSummary(argv),
25469
26074
  cwd,
25470
26075
  envelope
25471
26076
  };
25472
26077
  try {
25473
- await mkdir2(dirname(target), { recursive: true });
25474
- await writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
25475
- await rename(tmp, target);
26078
+ await mkdir5(dirname3(target), { recursive: true });
26079
+ await writeFile2(tmp, JSON.stringify(payload, null, 2), "utf8");
26080
+ await rename3(tmp, target);
25476
26081
  } catch {}
25477
26082
  }
25478
26083
  async function readLastEnvelope(cwd) {
25479
- const target = join10(cwd, LAST_ENVELOPE_PATH);
26084
+ const target = join12(cwd, LAST_ENVELOPE_PATH);
25480
26085
  try {
25481
- await stat(target);
26086
+ await stat3(target);
25482
26087
  } catch {
25483
26088
  return null;
25484
26089
  }
25485
26090
  try {
25486
- const raw = await readFile(target, "utf8");
26091
+ const raw = await readFile3(target, "utf8");
25487
26092
  return JSON.parse(raw);
25488
26093
  } catch {
25489
26094
  return null;
25490
26095
  }
25491
26096
  }
25492
26097
  async function readLastEnvelopeRaw(cwd) {
25493
- const target = join10(cwd, LAST_ENVELOPE_PATH);
26098
+ const target = join12(cwd, LAST_ENVELOPE_PATH);
25494
26099
  try {
25495
- await stat(target);
26100
+ await stat3(target);
25496
26101
  } catch {
25497
26102
  return null;
25498
26103
  }
25499
26104
  let raw;
25500
26105
  try {
25501
- raw = await readFile(target, "utf8");
26106
+ raw = await readFile3(target, "utf8");
25502
26107
  } catch {
25503
26108
  return null;
25504
26109
  }
@@ -25508,10 +26113,10 @@ async function readLastEnvelopeRaw(cwd) {
25508
26113
  return { __malformedJson: true, raw };
25509
26114
  }
25510
26115
  }
25511
- var LAST_ENVELOPE_PATH = ".dbcli/last-recovery.json", SQL_SUBCOMMANDS, REDACTED_VALUE_FLAGS, KEEP_VALUE_FLAGS;
26116
+ var LAST_ENVELOPE_PATH = ".dbcli/last-recovery.json", SQL_SUBCOMMANDS2, REDACTED_VALUE_FLAGS2, KEEP_VALUE_FLAGS2;
25512
26117
  var init_last_envelope = __esm(() => {
25513
- SQL_SUBCOMMANDS = new Set(["query", "export"]);
25514
- REDACTED_VALUE_FLAGS = new Set([
26118
+ SQL_SUBCOMMANDS2 = new Set(["query", "export"]);
26119
+ REDACTED_VALUE_FLAGS2 = new Set([
25515
26120
  "--where",
25516
26121
  "--set",
25517
26122
  "--data",
@@ -25520,17 +26125,19 @@ var init_last_envelope = __esm(() => {
25520
26125
  "--config",
25521
26126
  "--use"
25522
26127
  ]);
25523
- KEEP_VALUE_FLAGS = new Set(["--format"]);
26128
+ KEEP_VALUE_FLAGS2 = new Set(["--format"]);
25524
26129
  });
25525
26130
 
25526
26131
  // src/core/recovery/emit.ts
25527
26132
  import { writeFileSync as writeFileSync2, mkdirSync, renameSync } from "fs";
25528
- import { dirname as dirname2, join as join11 } from "path";
26133
+ import { dirname as dirname4, join as join13 } from "path";
26134
+ import { randomUUID as randomUUID4 } from "crypto";
25529
26135
  function emitRecoveryEnvelope(error, ctx, options = {}) {
25530
26136
  const envelope = classifyError(error, ctx);
25531
26137
  const cwd = options.cwd ?? process.cwd();
25532
26138
  const argv = options.argv ?? buildArgvFromProcess();
25533
- writeLastEnvelopeSync(cwd, envelope, argv);
26139
+ const envelopeId = options.envelopeId ?? randomUUID4();
26140
+ writeLastEnvelopeSync(cwd, envelope, argv, envelopeId, options.auditRef);
25534
26141
  process.stdout.write(renderJson(envelope, { brief: options.brief === true }) + `
25535
26142
  `);
25536
26143
  process.exit(options.exitCode ?? 1);
@@ -25539,18 +26146,20 @@ function buildArgvFromProcess() {
25539
26146
  const userArgs = process.argv.slice(2);
25540
26147
  return ["dbcli", ...userArgs];
25541
26148
  }
25542
- function writeLastEnvelopeSync(cwd, envelope, argv) {
25543
- const target = join11(cwd, LAST_ENVELOPE_PATH);
26149
+ function writeLastEnvelopeSync(cwd, envelope, argv, id, auditRef) {
26150
+ const target = join13(cwd, LAST_ENVELOPE_PATH);
25544
26151
  const tmp = `${target}.tmp`;
25545
26152
  const payload = {
25546
26153
  schemaVersion: 1,
26154
+ id,
26155
+ ...auditRef !== undefined && { audit_ref: auditRef },
25547
26156
  savedAt: new Date().toISOString(),
25548
26157
  command: sanitizeCommandSummary(argv),
25549
26158
  cwd,
25550
26159
  envelope
25551
26160
  };
25552
26161
  try {
25553
- mkdirSync(dirname2(target), { recursive: true });
26162
+ mkdirSync(dirname4(target), { recursive: true });
25554
26163
  writeFileSync2(tmp, JSON.stringify(payload, null, 2), "utf8");
25555
26164
  renameSync(tmp, target);
25556
26165
  } catch {}
@@ -26278,7 +26887,7 @@ function renderStep2(r) {
26278
26887
  var NEXT_SCHEMA_VERSION = 1, STEP_RESULT_SUMMARY_FIELD_CAP = 4096;
26279
26888
 
26280
26889
  // src/core/recovery/next-step-schema.ts
26281
- import { stat as stat2 } from "fs/promises";
26890
+ import { stat as stat4 } from "fs/promises";
26282
26891
  import { resolve as resolve2 } from "path";
26283
26892
  function summarizeZodError(err) {
26284
26893
  return err.issues.map((iss) => {
@@ -26300,7 +26909,7 @@ async function loadStepResultSummary(arg, cwd) {
26300
26909
  if (arg.startsWith("@")) {
26301
26910
  const path3 = resolve2(cwd, arg.slice(1));
26302
26911
  try {
26303
- await stat2(path3);
26912
+ await stat4(path3);
26304
26913
  } catch {
26305
26914
  return { ok: false, reason: `--result @<file>: ${path3} not readable.` };
26306
26915
  }
@@ -27430,7 +28039,7 @@ var init_parser = __esm(() => {
27430
28039
 
27431
28040
  // src/core/saved-queries/loader.ts
27432
28041
  import { readdir } from "fs/promises";
27433
- import { join as join14, relative, sep } from "path";
28042
+ import { join as join16, relative, sep } from "path";
27434
28043
  async function loadSnippets(opts) {
27435
28044
  const builtin = await walkAndParse(opts.builtinDir, "builtin");
27436
28045
  const shared = await walkAndParse(opts.sharedDir, "shared");
@@ -27497,7 +28106,7 @@ async function collectFiles(root) {
27497
28106
  async function walk(dir) {
27498
28107
  const entries = await readdir(dir, { withFileTypes: true });
27499
28108
  for (const e of entries) {
27500
- const full = join14(dir, e.name);
28109
+ const full = join16(dir, e.name);
27501
28110
  if (e.isDirectory())
27502
28111
  await walk(full);
27503
28112
  else
@@ -27581,23 +28190,23 @@ var init_runner = __esm(() => {
27581
28190
  });
27582
28191
 
27583
28192
  // src/core/saved-queries/snippet-paths.ts
27584
- import { join as join15 } from "path";
28193
+ import { join as join17 } from "path";
27585
28194
  function resolveBuiltinDir() {
27586
28195
  return packageAssetPath("snippets");
27587
28196
  }
27588
28197
  function resolveSnippetDirs(workspaceRoot) {
27589
28198
  return {
27590
28199
  builtinDir: resolveBuiltinDir(),
27591
- sharedDir: join15(workspaceRoot, ".dbcli-shared", "queries"),
27592
- localDir: join15(workspaceRoot, ".dbcli", "queries")
28200
+ sharedDir: join17(workspaceRoot, ".dbcli-shared", "queries"),
28201
+ localDir: join17(workspaceRoot, ".dbcli", "queries")
27593
28202
  };
27594
28203
  }
27595
28204
  function snippetKeyToFile(workspaceRoot, key, source) {
27596
28205
  const rel = key.replace(/^@/, "") + ".sql";
27597
28206
  if (source === "builtin")
27598
- return join15(resolveBuiltinDir(), rel);
28207
+ return join17(resolveBuiltinDir(), rel);
27599
28208
  const dir = source === "shared" ? ".dbcli-shared/queries" : ".dbcli/queries";
27600
- return join15(workspaceRoot, dir, rel);
28209
+ return join17(workspaceRoot, dir, rel);
27601
28210
  }
27602
28211
  var init_snippet_paths = __esm(() => {
27603
28212
  init_package_root();
@@ -27660,7 +28269,7 @@ var exports_queries_delete = {};
27660
28269
  __export(exports_queries_delete, {
27661
28270
  queriesDelete: () => queriesDelete
27662
28271
  });
27663
- import { rm } from "fs/promises";
28272
+ import { rm as rm2 } from "fs/promises";
27664
28273
  async function queriesDelete(name, options = {}) {
27665
28274
  if (!name.startsWith("@")) {
27666
28275
  throw new Error(`Snippet name must start with '@' (got '${name}')`);
@@ -27685,7 +28294,7 @@ async function queriesDelete(name, options = {}) {
27685
28294
  return;
27686
28295
  }
27687
28296
  for (const v of localVariants) {
27688
- await rm(v.query.file, { force: true });
28297
+ await rm2(v.query.file, { force: true });
27689
28298
  console.log(`deleted ${v.query.file}`);
27690
28299
  }
27691
28300
  }
@@ -27699,8 +28308,8 @@ var exports_queries_rename = {};
27699
28308
  __export(exports_queries_rename, {
27700
28309
  queriesRename: () => queriesRename
27701
28310
  });
27702
- import { rename as rename2, mkdir as mkdir3 } from "fs/promises";
27703
- import { dirname as dirname4 } from "path";
28311
+ import { rename as rename4, mkdir as mkdir6 } from "fs/promises";
28312
+ import { dirname as dirname6 } from "path";
27704
28313
  async function queriesRename(oldName, newName, options = {}) {
27705
28314
  if (!oldName.startsWith("@") || !newName.startsWith("@")) {
27706
28315
  throw new Error(`Both names must start with '@'`);
@@ -27721,8 +28330,8 @@ async function queriesRename(oldName, newName, options = {}) {
27721
28330
  for (const v of local) {
27722
28331
  const dst = snippetKeyToFile(cwd, newName, "local");
27723
28332
  const dstWithSuffix = preserveEngineSuffix(v.query.file, dst);
27724
- await mkdir3(dirname4(dstWithSuffix), { recursive: true });
27725
- await rename2(v.query.file, dstWithSuffix);
28333
+ await mkdir6(dirname6(dstWithSuffix), { recursive: true });
28334
+ await rename4(v.query.file, dstWithSuffix);
27726
28335
  await rewriteFrontmatterName(dstWithSuffix, newName.slice(1));
27727
28336
  console.log(`renamed ${v.query.file} \u2192 ${dstWithSuffix}`);
27728
28337
  }
@@ -27747,8 +28356,8 @@ var exports_queries_copy = {};
27747
28356
  __export(exports_queries_copy, {
27748
28357
  queriesCopy: () => queriesCopy
27749
28358
  });
27750
- import { mkdir as mkdir4, copyFile } from "fs/promises";
27751
- import { dirname as dirname5, basename as basename2 } from "path";
28359
+ import { mkdir as mkdir7, copyFile } from "fs/promises";
28360
+ import { dirname as dirname7, basename as basename2 } from "path";
27752
28361
  async function queriesCopy(src, dst, options = {}) {
27753
28362
  if (!src.startsWith("@") || !dst.startsWith("@")) {
27754
28363
  throw new Error(`Both names must start with '@'`);
@@ -27766,7 +28375,7 @@ async function queriesCopy(src, dst, options = {}) {
27766
28375
  }
27767
28376
  for (const v of variants) {
27768
28377
  const dstFile = mapEngineSuffix(v.query.file, snippetKeyToFile(cwd, dst, "local"));
27769
- await mkdir4(dirname5(dstFile), { recursive: true });
28378
+ await mkdir7(dirname7(dstFile), { recursive: true });
27770
28379
  await copyFile(v.query.file, dstFile);
27771
28380
  console.log(`copied ${v.query.file} \u2192 ${dstFile}`);
27772
28381
  }
@@ -27784,21 +28393,21 @@ var exports_queries_import = {};
27784
28393
  __export(exports_queries_import, {
27785
28394
  queriesImport: () => queriesImport
27786
28395
  });
27787
- import { stat as stat3, mkdir as mkdir5, copyFile as copyFile2 } from "fs/promises";
27788
- import { basename as basename3, join as join17, extname } from "path";
28396
+ import { stat as stat5, mkdir as mkdir8, copyFile as copyFile2 } from "fs/promises";
28397
+ import { basename as basename3, join as join19, extname } from "path";
27789
28398
  async function queriesImport(filePath, options = {}) {
27790
28399
  const cwd = options.cwd ?? process.cwd();
27791
28400
  if (extname(filePath) !== ".sql") {
27792
28401
  throw new Error(`Expected .sql file, got ${filePath}`);
27793
28402
  }
27794
- await stat3(filePath);
28403
+ await stat5(filePath);
27795
28404
  const text2 = await Bun.file(filePath).text();
27796
28405
  const baseName = options.as ? options.as.replace(/^@/, "") : basename3(filePath, ".sql").replace(/\.(postgres|mysql)$/, "");
27797
28406
  const key = "@" + baseName;
27798
28407
  parseSavedQuery({ key, file: filePath, source: "local", text: text2 });
27799
- const targetDir = join17(cwd, ".dbcli/queries");
27800
- await mkdir5(targetDir, { recursive: true });
27801
- const target = join17(targetDir, basename3(filePath));
28408
+ const targetDir = join19(cwd, ".dbcli/queries");
28409
+ await mkdir8(targetDir, { recursive: true });
28410
+ const target = join19(targetDir, basename3(filePath));
27802
28411
  if (await Bun.file(target).exists()) {
27803
28412
  if (!options.force) {
27804
28413
  const ok = await dist_default6({ message: `Overwrite ${target}?`, default: false });
@@ -27819,7 +28428,7 @@ var exports_queries_export = {};
27819
28428
  __export(exports_queries_export, {
27820
28429
  queriesExport: () => queriesExport
27821
28430
  });
27822
- import { writeFile as writeFile2 } from "fs/promises";
28431
+ import { writeFile as writeFile3 } from "fs/promises";
27823
28432
  async function queriesExport(name, options = {}) {
27824
28433
  const cwd = options.cwd ?? process.cwd();
27825
28434
  const dirs = resolveSnippetDirs(cwd);
@@ -27840,7 +28449,7 @@ async function queriesExport(name, options = {}) {
27840
28449
  }
27841
28450
  const text2 = await Bun.file(chosen[0].query.file).text();
27842
28451
  if (options.output) {
27843
- await writeFile2(options.output, text2, "utf8");
28452
+ await writeFile3(options.output, text2, "utf8");
27844
28453
  console.log(`wrote ${options.output}`);
27845
28454
  } else {
27846
28455
  process.stdout.write(text2);
@@ -27923,17 +28532,17 @@ var init_collect_snippets = __esm(() => {
27923
28532
  });
27924
28533
 
27925
28534
  // src/core/inspect/collect-schema-cache.ts
27926
- import { join as join21 } from "path";
28535
+ import { join as join23 } from "path";
27927
28536
  async function collectSchemaCache(opts) {
27928
28537
  const warnings = [];
27929
- if (opts.system && !SQL_SYSTEMS.includes(opts.system)) {
28538
+ if (opts.system && !SQL_SYSTEMS2.includes(opts.system)) {
27930
28539
  return {
27931
28540
  section: { available: false, unavailable: true, reason: "schema cache is sql-only" },
27932
28541
  warnings
27933
28542
  };
27934
28543
  }
27935
28544
  const root = resolveSchemaPath(opts.dbcliPath, opts.connectionName);
27936
- const indexPath = join21(root, "index.json");
28545
+ const indexPath = join23(root, "index.json");
27937
28546
  const file = Bun.file(indexPath);
27938
28547
  if (!await file.exists()) {
27939
28548
  return { section: { available: false }, warnings };
@@ -27960,11 +28569,11 @@ async function collectSchemaCache(opts) {
27960
28569
  };
27961
28570
  }
27962
28571
  }
27963
- var STALE_AFTER_MS, SQL_SYSTEMS;
28572
+ var STALE_AFTER_MS, SQL_SYSTEMS2;
27964
28573
  var init_collect_schema_cache = __esm(() => {
27965
28574
  init_schema_path();
27966
28575
  STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
27967
- SQL_SYSTEMS = ["postgresql", "mysql", "mariadb"];
28576
+ SQL_SYSTEMS2 = ["postgresql", "mysql", "mariadb"];
27968
28577
  });
27969
28578
 
27970
28579
  // src/core/inspect/collect-objects.ts
@@ -28024,7 +28633,7 @@ function suggestCommands(snap, options = {}) {
28024
28633
  if (!snap.system)
28025
28634
  return ["dbcli init"];
28026
28635
  const out = [];
28027
- const isSql = SQL_SYSTEMS2.includes(snap.system);
28636
+ const isSql = SQL_SYSTEMS3.includes(snap.system);
28028
28637
  if (isSql && (!snap.schemaCache.available || snap.schemaCache.stale)) {
28029
28638
  out.push("dbcli schema --refresh");
28030
28639
  }
@@ -28039,13 +28648,13 @@ function suggestCommands(snap, options = {}) {
28039
28648
  const cap2 = options.brief ? 1 : 5;
28040
28649
  return out.slice(0, cap2);
28041
28650
  }
28042
- var SQL_SYSTEMS2;
28651
+ var SQL_SYSTEMS3;
28043
28652
  var init_suggest_commands = __esm(() => {
28044
- SQL_SYSTEMS2 = ["postgresql", "mysql", "mariadb"];
28653
+ SQL_SYSTEMS3 = ["postgresql", "mysql", "mariadb"];
28045
28654
  });
28046
28655
 
28047
28656
  // src/core/inspect/collector.ts
28048
- import { join as join22 } from "path";
28657
+ import { join as join24 } from "path";
28049
28658
  async function collectInspect(opts) {
28050
28659
  const warnings = [];
28051
28660
  let config = null;
@@ -28121,11 +28730,11 @@ async function collectInspect(opts) {
28121
28730
  return { ...snapWithoutSuggestions, suggestedCommands, warnings };
28122
28731
  }
28123
28732
  async function hasConfig(configPath) {
28124
- if (await Bun.file(join22(configPath, "config.json")).exists())
28733
+ if (await Bun.file(join24(configPath, "config.json")).exists())
28125
28734
  return true;
28126
28735
  if (await Bun.file(configPath).exists()) {
28127
- const stat4 = await Bun.file(configPath).stat().catch(() => null);
28128
- return stat4?.isFile() === true;
28736
+ const stat6 = await Bun.file(configPath).stat().catch(() => null);
28737
+ return stat6?.isFile() === true;
28129
28738
  }
28130
28739
  return false;
28131
28740
  }
@@ -28247,16 +28856,16 @@ function renderMarkdown2(snap, options = {}) {
28247
28856
  function requireSchemaCacheOrThrow(section, system) {
28248
28857
  if (!system)
28249
28858
  return;
28250
- if (!SQL_SYSTEMS3.includes(system))
28859
+ if (!SQL_SYSTEMS4.includes(system))
28251
28860
  return;
28252
28861
  if (section.available === true)
28253
28862
  return;
28254
28863
  throw new SchemaCacheMissingError(`Schema cache unavailable for ${system} connection; run 'dbcli schema --refresh' to populate it.`);
28255
28864
  }
28256
- var SQL_SYSTEMS3;
28865
+ var SQL_SYSTEMS4;
28257
28866
  var init_require_schema_cache = __esm(() => {
28258
28867
  init_recovery();
28259
- SQL_SYSTEMS3 = ["postgresql", "mysql", "mariadb"];
28868
+ SQL_SYSTEMS4 = ["postgresql", "mysql", "mariadb"];
28260
28869
  });
28261
28870
 
28262
28871
  // src/core/inspect/index.ts
@@ -28273,6 +28882,125 @@ var init_inspect = __esm(() => {
28273
28882
  init_collector();
28274
28883
  });
28275
28884
 
28885
+ // src/core/audit/reader.ts
28886
+ import { readFile as readFile4, readdir as readdir3 } from "fs/promises";
28887
+ async function readSingle(path5) {
28888
+ let raw;
28889
+ try {
28890
+ raw = await readFile4(path5, "utf8");
28891
+ } catch (err) {
28892
+ if (err.code === "ENOENT")
28893
+ return [];
28894
+ throw err;
28895
+ }
28896
+ const lines = raw.split(`
28897
+ `).filter(Boolean);
28898
+ const out = [];
28899
+ for (let i = 0;i < lines.length; i++) {
28900
+ try {
28901
+ out.push(JSON.parse(lines[i]));
28902
+ } catch {
28903
+ if (i === lines.length - 1) {
28904
+ process.stderr.write(`${WARN_PREFIX2} skipping truncated last line in ${path5}
28905
+ `);
28906
+ continue;
28907
+ }
28908
+ throw new Error(`${WARN_PREFIX2} corrupted line ${i + 1} in ${path5}. Run \`dbcli audit clear\` to reset.`);
28909
+ }
28910
+ }
28911
+ return out;
28912
+ }
28913
+ async function readEntries(auditFilePath, opts) {
28914
+ if (opts?.include_rotated === true) {
28915
+ const rotated = await readSingle(`${auditFilePath}.1`);
28916
+ const current = await readSingle(auditFilePath);
28917
+ return rotated.concat(current);
28918
+ }
28919
+ return readSingle(auditFilePath);
28920
+ }
28921
+ async function discoverConnections(auditDir) {
28922
+ let entries;
28923
+ try {
28924
+ entries = await readdir3(auditDir, { withFileTypes: true });
28925
+ } catch (err) {
28926
+ if (err.code === "ENOENT")
28927
+ return [];
28928
+ throw err;
28929
+ }
28930
+ const map = new Map;
28931
+ for (const e of entries) {
28932
+ if (!e.isFile())
28933
+ continue;
28934
+ if (!(e.name.endsWith(".jsonl") || e.name.endsWith(".jsonl.1")))
28935
+ continue;
28936
+ const conn = e.name.replace(/\.jsonl(?:\.1)?$/, "");
28937
+ const full = `${auditDir}/${e.name}`;
28938
+ const list = map.get(conn) ?? [];
28939
+ list.push(full);
28940
+ map.set(conn, list);
28941
+ }
28942
+ for (const files of map.values()) {
28943
+ files.sort((a, b) => Number(b.endsWith(".jsonl.1")) - Number(a.endsWith(".jsonl.1")));
28944
+ }
28945
+ return Array.from(map.entries()).map(([connection, files]) => ({ connection, files })).sort((a, b) => a.connection.localeCompare(b.connection));
28946
+ }
28947
+ function tailEntries(entries, n) {
28948
+ if (n <= 0)
28949
+ return [];
28950
+ return entries.slice().sort((a, b) => a.ts.localeCompare(b.ts)).slice(-n);
28951
+ }
28952
+ function mergeByTimestamp(byConn) {
28953
+ const flat = [];
28954
+ for (const [connection, entries] of byConn) {
28955
+ for (const entry of entries)
28956
+ flat.push({ connection, entry });
28957
+ }
28958
+ flat.sort((a, b) => {
28959
+ const t2 = a.entry.ts.localeCompare(b.entry.ts);
28960
+ return t2 !== 0 ? t2 : a.connection.localeCompare(b.connection);
28961
+ });
28962
+ return flat;
28963
+ }
28964
+ var WARN_PREFIX2 = "[dbcli audit]";
28965
+ var init_reader = () => {};
28966
+
28967
+ // src/core/audit/recent.ts
28968
+ var exports_recent = {};
28969
+ __export(exports_recent, {
28970
+ shouldEmbedRecent: () => shouldEmbedRecent,
28971
+ loadRecentAudit: () => loadRecentAudit,
28972
+ RECENT_AUDIT_DEFAULT_N: () => RECENT_AUDIT_DEFAULT_N
28973
+ });
28974
+ function shouldEmbedRecent(opts) {
28975
+ return opts.forAgent === true || opts.format === "json";
28976
+ }
28977
+ function briefifyForRecent(entry) {
28978
+ return {
28979
+ id: entry.id,
28980
+ ts: entry.ts,
28981
+ command: entry.command,
28982
+ target: entry.target,
28983
+ success: entry.success
28984
+ };
28985
+ }
28986
+ async function loadRecentAudit(config, configPath, n = RECENT_AUDIT_DEFAULT_N) {
28987
+ try {
28988
+ if (config.audit?.enabled === false)
28989
+ return [];
28990
+ const logger = await getAuditLogger(config, configPath);
28991
+ const auditFile = logger.getHealth().currentFile;
28992
+ const entries = await readEntries(auditFile, { include_rotated: true });
28993
+ return tailEntries(entries, n).map(briefifyForRecent);
28994
+ } catch {
28995
+ return [];
28996
+ }
28997
+ }
28998
+ var RECENT_AUDIT_DEFAULT_N = 5;
28999
+ var init_recent = __esm(() => {
29000
+ init_integration_helper();
29001
+ init_reader();
29002
+ });
29003
+
28276
29004
  // node_modules/commander/esm.mjs
28277
29005
  var import__ = __toESM(require_commander(), 1);
28278
29006
  var {
@@ -28291,7 +29019,7 @@ var {
28291
29019
  // package.json
28292
29020
  var package_default = {
28293
29021
  name: "@carllee1983/dbcli",
28294
- version: "1.19.1",
29022
+ version: "1.20.1",
28295
29023
  description: "Database CLI for AI agents",
28296
29024
  type: "module",
28297
29025
  publishConfig: {
@@ -28341,6 +29069,7 @@ var package_default = {
28341
29069
  "test:unit": "bun test tests/unit tests/core",
28342
29070
  "test:integration": "bun test tests/integration",
28343
29071
  "test:docker": "docker compose -f docker-compose.test.yml up -d --wait && bun test tests/integration/adapters; docker compose -f docker-compose.test.yml down",
29072
+ "docs:check": "bun run scripts/check-user-docs.ts",
28344
29073
  typecheck: "tsc --noEmit --pretty false",
28345
29074
  "test:perf": "bun test ./tests/perf/*.bench.ts",
28346
29075
  lint: "eslint src tests --ext .ts --max-warnings=0",
@@ -28741,7 +29470,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28741
29470
  schema: v1Config.schema || {},
28742
29471
  schemas: { default: v1Config.schema || {} },
28743
29472
  metadata: v1Config.metadata || { version: "1.0" },
28744
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29473
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29474
+ audit: v1Config.audit
28745
29475
  };
28746
29476
  }
28747
29477
  } else if (await projectConfigFile.exists()) {
@@ -28760,7 +29490,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28760
29490
  schema: v1Config.schema || {},
28761
29491
  schemas: { default: v1Config.schema || {} },
28762
29492
  metadata: v1Config.metadata || { version: "1.0" },
28763
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29493
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29494
+ audit: v1Config.audit
28764
29495
  };
28765
29496
  } else {
28766
29497
  const v1Config = await configModule.read(configPath);
@@ -28776,7 +29507,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28776
29507
  schema: v1Config.schema || {},
28777
29508
  schemas: { default: v1Config.schema || {} },
28778
29509
  metadata: v1Config.metadata || { version: "1.0" },
28779
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29510
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29511
+ audit: v1Config.audit
28780
29512
  };
28781
29513
  }
28782
29514
  } else {
@@ -28797,7 +29529,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28797
29529
  schema: v1Config.schema || {},
28798
29530
  schemas: { default: v1Config.schema || {} },
28799
29531
  metadata: v1Config.metadata || { version: "1.0" },
28800
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29532
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29533
+ audit: v1Config.audit
28801
29534
  };
28802
29535
  }
28803
29536
  }
@@ -28824,7 +29557,11 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28824
29557
  schema: {},
28825
29558
  schemas: {},
28826
29559
  metadata: { version: "1.0", createdAt: new Date().toISOString() },
28827
- blacklist: { tables: [], columns: {} }
29560
+ blacklist: { tables: [], columns: {} },
29561
+ audit: {
29562
+ enabled: true,
29563
+ rotation: { max_bytes: 10485760, max_entries: 1000 }
29564
+ }
28828
29565
  };
28829
29566
  await writeV2Config(storagePath, v2Config);
28830
29567
  await migrateLegacyProjectEnvLocal(configPath, storagePath);
@@ -29482,6 +30219,7 @@ async function elasticsearchListBranch(config, format, includeSystem) {
29482
30219
  }
29483
30220
 
29484
30221
  // src/commands/schema.ts
30222
+ import crypto from "crypto";
29485
30223
  init_message_loader();
29486
30224
  init_adapters();
29487
30225
  init_config();
@@ -30155,15 +30893,17 @@ function rowsOf(result) {
30155
30893
  init_schema_loader();
30156
30894
 
30157
30895
  // src/commands/schema.ts
30896
+ init_integration_helper();
30158
30897
  init_validation();
30159
30898
  var ALLOWED_FORMATS2 = ["table", "json"];
30160
30899
  var schemaCommand = new Command().name("schema").description("Display table schema, scan database schema, or refresh existing schema with detected changes").argument("[table]", "Optional: table name to inspect (if omitted, scans all tables)").option("--format <format>", "Output format: table (default) or json", "table").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--refresh", "Refresh schema by detecting changes from database", false).option("--reset", "Clear all existing schema data and re-fetch from database", false).option("--force", "Skip confirmation when updating schema data", false).option("--sample-size <n>", "MongoDB only: number of documents to sample for schema inference (default 50, max 1000). Ignored on SQL connections.").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(schemaAction);
30161
30900
  async function schemaAction(table, options, command) {
30901
+ let config;
30162
30902
  try {
30163
30903
  validateFormat(options.format, ALLOWED_FORMATS2, "schema");
30164
30904
  const configPath = resolveConfigPath(command, options);
30165
30905
  const storagePath = await resolveConfigStoragePath(configPath);
30166
- const config = await configModule.read(configPath);
30906
+ config = await configModule.read(configPath);
30167
30907
  if (!config.connection) {
30168
30908
  console.error("Database not configured. Run: dbcli init");
30169
30909
  process.exit(1);
@@ -30219,6 +30959,10 @@ Key: ${schema.name}`);
30219
30959
  console.log(` ${col.name}: ${col.type}`);
30220
30960
  }
30221
30961
  }
30962
+ await writeAuditEntry(config, "schema", options, {
30963
+ success: true,
30964
+ target: table
30965
+ });
30222
30966
  } finally {
30223
30967
  await redisAdapter.disconnect();
30224
30968
  }
@@ -30232,9 +30976,17 @@ Key: ${schema.name}`);
30232
30976
  if (!esAdapter.getTableSchema)
30233
30977
  throw new Error("Elasticsearch adapter does not implement getTableSchema");
30234
30978
  await handleSingleTableSchema(esAdapter, table, options.format, inferenceOptions);
30979
+ await writeAuditEntry(config, "schema", options, {
30980
+ success: true,
30981
+ target: table
30982
+ });
30235
30983
  return;
30236
30984
  }
30237
30985
  await handleFullDatabaseScan(esAdapter, config, options, connectionName, existingSchemaCount, storagePath, inferenceOptions);
30986
+ await writeAuditEntry(config, "schema", options, {
30987
+ success: true,
30988
+ target: "*"
30989
+ });
30238
30990
  return;
30239
30991
  } finally {
30240
30992
  await esAdapter.disconnect();
@@ -30252,13 +31004,30 @@ Key: ${schema.name}`);
30252
31004
  } else {
30253
31005
  await handleFullDatabaseScan(adapter, config, options, connectionName, existingSchemaCount, storagePath, inferenceOptions);
30254
31006
  }
31007
+ await writeAuditEntry(config, "schema", options, {
31008
+ success: true,
31009
+ target: table ?? "*"
31010
+ });
30255
31011
  } finally {
30256
31012
  await adapter.disconnect();
30257
31013
  }
30258
31014
  } catch (error) {
31015
+ let auditId = null;
31016
+ let envelopeId;
30259
31017
  if (options.recovery === true) {
31018
+ envelopeId = crypto.randomUUID();
31019
+ }
31020
+ if (config) {
31021
+ auditId = await writeAuditEntry(config, "schema", options, {
31022
+ success: false,
31023
+ target: table ?? "*",
31024
+ error,
31025
+ ...envelopeId && { recovery_ref: envelopeId }
31026
+ });
31027
+ }
31028
+ if (envelopeId !== undefined) {
30260
31029
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
30261
- emitRecoveryEnvelope2(error, { operation: "schema", table });
31030
+ emitRecoveryEnvelope2(error, { operation: "schema", table }, { envelopeId, auditRef: auditId ?? undefined });
30262
31031
  }
30263
31032
  if (error instanceof Error) {
30264
31033
  console.error(t_vars("errors.message", { message: error.message }));
@@ -30496,6 +31265,7 @@ async function writeSchema(configPath, config, connectionName) {
30496
31265
  // src/commands/query.ts
30497
31266
  init_message_loader();
30498
31267
  init_adapters();
31268
+ import crypto2 from "crypto";
30499
31269
 
30500
31270
  // src/formatters/html-formatter.ts
30501
31271
  init_package_root();
@@ -30513,14 +31283,14 @@ async function generateHtmlReport(payload) {
30513
31283
  }
30514
31284
 
30515
31285
  // src/utils/opener.ts
30516
- import open from "open";
31286
+ import open2 from "open";
30517
31287
  async function openInBrowser(target) {
30518
31288
  if (process.env.DBCLI_NO_OPEN === "1" || false) {
30519
31289
  console.log(`[Opener] DBCLI_NO_OPEN is set. Skipping browser launch for: ${target}`);
30520
31290
  return;
30521
31291
  }
30522
31292
  try {
30523
- await open(target);
31293
+ await open2(target);
30524
31294
  } catch (err) {
30525
31295
  console.error(`[Opener] Failed to open ${target}:`, err);
30526
31296
  }
@@ -30528,7 +31298,7 @@ async function openInBrowser(target) {
30528
31298
 
30529
31299
  // src/commands/query.ts
30530
31300
  import { tmpdir } from "os";
30531
- import { join as join13 } from "path";
31301
+ import { join as join15 } from "path";
30532
31302
 
30533
31303
  // src/core/query-executor.ts
30534
31304
  init_permission_guard();
@@ -30596,20 +31366,30 @@ async function getAllTablesFromAdapter(adapter) {
30596
31366
  }
30597
31367
  }
30598
31368
 
31369
+ // src/core/query-executor.ts
31370
+ init_engine_hints();
31371
+
30599
31372
  // src/core/limits.ts
30600
31373
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
30601
31374
 
30602
31375
  // src/core/query-executor.ts
31376
+ init_integration_helper();
31377
+
30603
31378
  class QueryExecutor {
30604
31379
  adapter;
30605
31380
  permission;
30606
31381
  blacklistValidator;
30607
- constructor(adapter, permission, blacklistValidator) {
31382
+ config;
31383
+ options;
31384
+ constructor(adapter, permission, blacklistValidator, config, options = {}) {
30608
31385
  this.adapter = adapter;
30609
31386
  this.permission = permission;
30610
31387
  this.blacklistValidator = blacklistValidator;
31388
+ this.config = config;
31389
+ this.options = options;
30611
31390
  }
30612
31391
  async execute(sql, options) {
31392
+ const start = performance.now();
30613
31393
  try {
30614
31394
  const classification = enforcePermission(sql, this.permission);
30615
31395
  if (classification.isDangerous && this.permission === "admin") {
@@ -30627,7 +31407,6 @@ class QueryExecutor {
30627
31407
  this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
30628
31408
  }
30629
31409
  }
30630
- const start = performance.now();
30631
31410
  const resultData = await this.adapter.execute(executeSql);
30632
31411
  const executionTimeMs = Math.round(performance.now() - start);
30633
31412
  const rows = resultData.rows;
@@ -30662,8 +31441,28 @@ class QueryExecutor {
30662
31441
  ...securityNotification ? { securityNotification } : {}
30663
31442
  }
30664
31443
  };
31444
+ if (this.config) {
31445
+ await writeAuditEntry(this.config, "query", this.options, {
31446
+ success: true,
31447
+ sql,
31448
+ metadata: {
31449
+ rows_affected: affectedRows,
31450
+ execution_ms: executionTimeMs
31451
+ }
31452
+ });
31453
+ }
30665
31454
  return result;
30666
31455
  } catch (error) {
31456
+ if (this.config) {
31457
+ await writeAuditEntry(this.config, "query", this.options, {
31458
+ success: false,
31459
+ sql,
31460
+ error,
31461
+ metadata: {
31462
+ execution_ms: Math.round(performance.now() - start)
31463
+ }
31464
+ });
31465
+ }
30667
31466
  if (error instanceof PermissionError) {
30668
31467
  throw error;
30669
31468
  }
@@ -30685,25 +31484,6 @@ class QueryExecutor {
30685
31484
  }
30686
31485
  }
30687
31486
  }
30688
- function extractTableName(sql) {
30689
- const match = sql.match(/\bFROM\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
30690
- if (match) {
30691
- return match[1] ?? null;
30692
- }
30693
- const insertMatch = sql.match(/\bINSERT\s+INTO\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
30694
- if (insertMatch) {
30695
- return insertMatch[1] ?? null;
30696
- }
30697
- const updateMatch = sql.match(/\bUPDATE\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
30698
- if (updateMatch) {
30699
- return updateMatch[1] ?? null;
30700
- }
30701
- const deleteMatch = sql.match(/\bDELETE\s+FROM\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
30702
- if (deleteMatch) {
30703
- return deleteMatch[1] ?? null;
30704
- }
30705
- return null;
30706
- }
30707
31487
  function inferColumnType(value) {
30708
31488
  if (value === null || value === undefined) {
30709
31489
  return "null";
@@ -30728,6 +31508,7 @@ init_config();
30728
31508
  init_permission_guard();
30729
31509
  init_blacklist();
30730
31510
  init_validation();
31511
+ init_integration_helper();
30731
31512
  function requireSqlConnection2(connection) {
30732
31513
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
30733
31514
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -30736,6 +31517,7 @@ function requireSqlConnection2(connection) {
30736
31517
  }
30737
31518
  var ALLOWED_FORMATS3 = ["table", "json", "csv"];
30738
31519
  async function queryCommand(sql, options, command) {
31520
+ let config;
30739
31521
  try {
30740
31522
  if (!sql || sql.trim() === "") {
30741
31523
  throw new Error("SQL query required");
@@ -30745,7 +31527,7 @@ async function queryCommand(sql, options, command) {
30745
31527
  }
30746
31528
  sql = sql.trim();
30747
31529
  const configPath = resolveConfigPath(command, options);
30748
- const config = await configModule.read(configPath);
31530
+ config = await configModule.read(configPath);
30749
31531
  if (!config.connection) {
30750
31532
  throw new Error('Run "dbcli init" first');
30751
31533
  }
@@ -30758,7 +31540,8 @@ async function queryCommand(sql, options, command) {
30758
31540
  if (config.connection.system === "elasticsearch") {
30759
31541
  return await elasticsearchQueryBranch(sql, options, config);
30760
31542
  }
30761
- const mainTable = extractMainTable(sql);
31543
+ const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
31544
+ const mainTable = extractTableName2(sql);
30762
31545
  if (mainTable && config.schema && !options.noLimit) {
30763
31546
  const tableSchema = config.schema[mainTable];
30764
31547
  if (tableSchema) {
@@ -30774,7 +31557,7 @@ async function queryCommand(sql, options, command) {
30774
31557
  try {
30775
31558
  const blacklistManager = new BlacklistManager(config);
30776
31559
  const blacklistValidator = new BlacklistValidator(blacklistManager);
30777
- const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator);
31560
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
30778
31561
  const autoLimit = !options.noLimit;
30779
31562
  const result = await executor3.execute(sql, {
30780
31563
  autoLimit,
@@ -30792,7 +31575,7 @@ async function queryCommand(sql, options, command) {
30792
31575
  rows: result.rows
30793
31576
  });
30794
31577
  if (options.ui) {
30795
- const tempPath = join13(tmpdir(), `dbcli-query-${Date.now()}.html`);
31578
+ const tempPath = join15(tmpdir(), `dbcli-query-${Date.now()}.html`);
30796
31579
  await Bun.write(tempPath, html);
30797
31580
  await openInBrowser(tempPath);
30798
31581
  } else {
@@ -30809,11 +31592,27 @@ async function queryCommand(sql, options, command) {
30809
31592
  await adapter.disconnect();
30810
31593
  }
30811
31594
  } catch (error) {
31595
+ let auditId = null;
31596
+ let envelopeId;
30812
31597
  if (options.recovery === true) {
31598
+ envelopeId = crypto2.randomUUID();
31599
+ }
31600
+ if (config) {
31601
+ auditId = await writeAuditEntry(config, "query", options, {
31602
+ success: false,
31603
+ sql,
31604
+ error,
31605
+ ...envelopeId && { recovery_ref: envelopeId }
31606
+ });
31607
+ }
31608
+ if (envelopeId !== undefined) {
30813
31609
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
30814
31610
  emitRecoveryEnvelope2(error, {
30815
31611
  operation: "query",
30816
- table: extractMainTable(sql) ?? undefined
31612
+ table: (await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints))).extractTableName(sql) ?? undefined
31613
+ }, {
31614
+ envelopeId,
31615
+ auditRef: auditId ?? undefined
30817
31616
  });
30818
31617
  }
30819
31618
  if (error instanceof BlacklistError) {
@@ -30834,21 +31633,17 @@ async function queryCommand(sql, options, command) {
30834
31633
  process.exit(1);
30835
31634
  }
30836
31635
  }
30837
- function extractMainTable(sql) {
30838
- const match = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
30839
- return match?.[1] ?? null;
30840
- }
30841
- var SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
30842
31636
  async function mongoQueryBranch(queryStr, options, config) {
30843
31637
  const collection = options.collection;
30844
31638
  const format = options.format ?? "table";
31639
+ if (!collection) {
31640
+ throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
31641
+ }
31642
+ const SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
30845
31643
  if (SQL_PATTERN.test(queryStr)) {
30846
31644
  throw new Error(`\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u8A9E\u6CD5\u3002
30847
31645
  \u7BC4\u4F8B\uFF1Adbcli query '{"field": "value"}' --collection <name>`);
30848
31646
  }
30849
- if (!collection) {
30850
- throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
30851
- }
30852
31647
  try {
30853
31648
  JSON.parse(queryStr);
30854
31649
  } catch {
@@ -30881,8 +31676,18 @@ async function mongoQueryBranch(queryStr, options, config) {
30881
31676
  }
30882
31677
  const mongoAdapter = AdapterFactory.createMongoDBAdapter(config.connection);
30883
31678
  await mongoAdapter.connect();
31679
+ const start = performance.now();
30884
31680
  try {
30885
31681
  const result = await mongoAdapter.execute(queryStr, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit } : undefined);
31682
+ const executionTimeMs = Math.round(performance.now() - start);
31683
+ await writeAuditEntry(config, "query", options, {
31684
+ success: true,
31685
+ target: collection,
31686
+ metadata: {
31687
+ rows_affected: result.rows.length,
31688
+ execution_ms: executionTimeMs
31689
+ }
31690
+ });
30886
31691
  const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
30887
31692
  const filterResult = blacklistValidator.filterColumns(collection, result.rows, columnNames);
30888
31693
  const queryResult = {
@@ -30894,8 +31699,8 @@ async function mongoQueryBranch(queryStr, options, config) {
30894
31699
  const output = formatter.format(queryResult, {
30895
31700
  format
30896
31701
  });
30897
- const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
30898
31702
  console.log(output);
31703
+ const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
30899
31704
  if (securityNote) {
30900
31705
  console.log(`
30901
31706
  \u2139 ${securityNote}`);
@@ -30909,15 +31714,24 @@ async function redisQueryBranch(command, options, config) {
30909
31714
  const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
30910
31715
  const head = command.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
30911
31716
  if (head === "KEYS" && options.recovery !== true) {
30912
- console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers as it blocks the main thread.');
30913
- console.error(' Please use "SCAN" instead for better performance and safety.');
30914
- console.error(" For more info: https://redis.io/commands/keys/");
31717
+ console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers.');
30915
31718
  }
30916
31719
  enforceRedisPermission2(command, config.permission);
30917
31720
  const redisAdapter = AdapterFactory.createRedisAdapter(config.connection);
30918
31721
  await redisAdapter.connect();
31722
+ const start = performance.now();
30919
31723
  try {
30920
31724
  const result = await redisAdapter.execute(command);
31725
+ const executionTimeMs = Math.round(performance.now() - start);
31726
+ const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
31727
+ await writeAuditEntry(config, "query", options, {
31728
+ success: true,
31729
+ target,
31730
+ metadata: {
31731
+ rows_affected: result.rows.length,
31732
+ execution_ms: executionTimeMs
31733
+ }
31734
+ });
30921
31735
  const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : ["value"];
30922
31736
  const queryResult = {
30923
31737
  rows: result.rows,
@@ -30949,23 +31763,28 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
30949
31763
  if (options.noLimit) {
30950
31764
  effectiveLimit = 1e4;
30951
31765
  console.error("Elasticsearch --no-limit is capped at size 10000; for more rows use saved-query with search_after.");
30952
- } else if (typeof options.limit === "number") {
30953
- effectiveLimit = options.limit;
30954
31766
  } else {
30955
- effectiveLimit = DEFAULT_QUERY_ONLY_LIMIT;
30956
- if (config.permission === "query-only") {
30957
- console.error(`Query-only mode: auto-limiting to ${effectiveLimit} rows`);
30958
- }
31767
+ effectiveLimit = options.limit || DEFAULT_QUERY_ONLY_LIMIT;
30959
31768
  }
30960
31769
  const blacklistManager = new BlacklistManager(config);
30961
31770
  const blacklistValidator = new BlacklistValidator(blacklistManager);
30962
31771
  blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
30963
31772
  const esAdapter = AdapterFactory.createElasticsearchAdapter(config.connection);
30964
31773
  await esAdapter.connect();
31774
+ const start = performance.now();
30965
31775
  try {
30966
31776
  const result = await esAdapter.execute(queryStr, [indexName], {
30967
31777
  limit: effectiveLimit
30968
31778
  });
31779
+ const executionTimeMs = Math.round(performance.now() - start);
31780
+ await writeAuditEntry(config, "query", options, {
31781
+ success: true,
31782
+ target: indexName,
31783
+ metadata: {
31784
+ rows_affected: result.rows.length,
31785
+ execution_ms: executionTimeMs
31786
+ }
31787
+ });
30969
31788
  const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
30970
31789
  const filterResult = blacklistValidator.filterColumns(indexName, result.rows, columnNames);
30971
31790
  const queryResult = {
@@ -30977,11 +31796,12 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
30977
31796
  const output = formatter.format(queryResult, {
30978
31797
  format
30979
31798
  });
30980
- const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
30981
31799
  console.log(output);
30982
- if (securityNote)
31800
+ const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
31801
+ if (securityNote) {
30983
31802
  console.log(`
30984
31803
  \u2139 ${securityNote}`);
31804
+ }
30985
31805
  } finally {
30986
31806
  await esAdapter.disconnect();
30987
31807
  }
@@ -31338,8 +32158,10 @@ function buildSuggestedCommands(facts, gap) {
31338
32158
  // src/commands/plan.ts
31339
32159
  init_config();
31340
32160
  init_validation();
32161
+ init_integration_helper();
31341
32162
  var ALLOWED_FORMATS4 = ["text", "json"];
31342
32163
  async function planCommand(sql, options, command) {
32164
+ let config;
31343
32165
  try {
31344
32166
  if (!sql || sql.trim() === "") {
31345
32167
  throw new Error("SQL statement required");
@@ -31347,7 +32169,7 @@ async function planCommand(sql, options, command) {
31347
32169
  const format = options.format ?? "text";
31348
32170
  validateFormat(format, ALLOWED_FORMATS4, "plan");
31349
32171
  const configPath = resolveConfigPath(command, options);
31350
- const config = await configModule.read(configPath);
32172
+ config = await configModule.read(configPath);
31351
32173
  if (!config.connection) {
31352
32174
  throw new Error('Run "dbcli init" first');
31353
32175
  }
@@ -31363,7 +32185,25 @@ async function planCommand(sql, options, command) {
31363
32185
  }
31364
32186
  });
31365
32187
  console.log(formatPlanResult(result, format));
32188
+ if (config) {
32189
+ await writeAuditEntry(config, "plan", { ...options, plan: true }, {
32190
+ success: true,
32191
+ sql,
32192
+ target: result.targetTables[0] || "*",
32193
+ metadata: {
32194
+ decision: result.decision,
32195
+ risk_factors: result.riskFactors.map((f) => f.code)
32196
+ }
32197
+ });
32198
+ }
31366
32199
  } catch (error) {
32200
+ if (config) {
32201
+ await writeAuditEntry(config, "plan", { ...options, plan: true }, {
32202
+ success: false,
32203
+ sql,
32204
+ error
32205
+ });
32206
+ }
31367
32207
  console.error(error.message);
31368
32208
  process.exit(1);
31369
32209
  }
@@ -31400,12 +32240,15 @@ function formatPlanText(result) {
31400
32240
  init_message_loader();
31401
32241
  init_adapters();
31402
32242
  init_config();
32243
+ import crypto3 from "crypto";
31403
32244
  init_blacklist();
31404
32245
  init_permission_guard();
32246
+ init_engine_hints();
32247
+ init_integration_helper();
31405
32248
  init_saved_queries();
31406
32249
  init_strategies();
31407
32250
  import { tmpdir as tmpdir2 } from "os";
31408
- import { join as join16 } from "path";
32251
+ import { join as join18 } from "path";
31409
32252
  function formatDryRun(input) {
31410
32253
  const lines = ["Dry-run preview (no execution):"];
31411
32254
  if (input.family === "es") {
@@ -31430,12 +32273,14 @@ function formatDryRun(input) {
31430
32273
  `);
31431
32274
  }
31432
32275
  async function qCommand(name, options, command) {
32276
+ let config;
32277
+ let targetNameForAudit = name;
31433
32278
  try {
31434
32279
  if (!name?.startsWith("@")) {
31435
32280
  throw new Error(`Snippet name must start with '@' (got '${name}')`);
31436
32281
  }
31437
32282
  const configPath = resolveConfigPath(command, options);
31438
- const config = await configModule.read(configPath);
32283
+ config = await configModule.read(configPath);
31439
32284
  if (!config.connection)
31440
32285
  throw new Error('Run "dbcli init" first');
31441
32286
  const engine = mapSystemToEngine(config.connection.system);
@@ -31459,12 +32304,19 @@ async function qCommand(name, options, command) {
31459
32304
  values: prepared.driver.values,
31460
32305
  execHints: prepared.execHints
31461
32306
  }));
32307
+ await writeAuditEntry(config, "q", options, {
32308
+ success: true,
32309
+ target: name,
32310
+ sql: prepared.driver.sql,
32311
+ metadata: { dry_run: true }
32312
+ });
31462
32313
  return;
31463
32314
  }
31464
32315
  const blacklistManager = new BlacklistManager(config);
31465
32316
  const blacklistValidator = new BlacklistValidator(blacklistManager);
31466
32317
  const family = engineFamily(engine);
31467
32318
  const targetName = family === "sql" ? extractTableName(prepared.rewrittenSql) ?? "" : family === "es" ? prepared.execHints?.index ?? "" : "";
32319
+ targetNameForAudit = targetName || name;
31468
32320
  if (family !== "redis" && targetName) {
31469
32321
  blacklistValidator.checkTableBlacklist("SELECT", targetName);
31470
32322
  }
@@ -31486,12 +32338,21 @@ async function qCommand(name, options, command) {
31486
32338
  rows: filtered.filteredRows
31487
32339
  });
31488
32340
  if (options.ui) {
31489
- const tempPath = join16(tmpdir2(), `dbcli-report-${Date.now()}.html`);
32341
+ const tempPath = join18(tmpdir2(), `dbcli-report-${Date.now()}.html`);
31490
32342
  await Bun.write(tempPath, html);
31491
32343
  await openInBrowser(tempPath);
31492
32344
  } else {
31493
32345
  console.log(html);
31494
32346
  }
32347
+ await writeAuditEntry(config, "q", options, {
32348
+ success: true,
32349
+ target: targetName || name,
32350
+ sql: prepared.driver.sql,
32351
+ metadata: {
32352
+ rows_affected: filtered.filteredRows.length,
32353
+ execution_ms: executionTimeMs
32354
+ }
32355
+ });
31495
32356
  return;
31496
32357
  }
31497
32358
  const formatter = new QueryResultFormatter;
@@ -31510,11 +32371,20 @@ async function qCommand(name, options, command) {
31510
32371
  }
31511
32372
  }, { format: options.format ?? "table" });
31512
32373
  console.log(out);
32374
+ await writeAuditEntry(config, "q", options, {
32375
+ success: true,
32376
+ target: targetName || name,
32377
+ sql: prepared.driver.sql,
32378
+ metadata: {
32379
+ rows_affected: filtered.filteredRows.length,
32380
+ execution_ms: executionTimeMs
32381
+ }
32382
+ });
31513
32383
  } finally {
31514
32384
  await adapter.disconnect();
31515
32385
  }
31516
32386
  } catch (error) {
31517
- await handleQError(error, name, options.recovery === true);
32387
+ await handleQError(error, targetNameForAudit, options, config);
31518
32388
  }
31519
32389
  }
31520
32390
  function parseCliParams(list) {
@@ -31537,14 +32407,24 @@ async function readParamFile(path4) {
31537
32407
  }
31538
32408
  return parsed;
31539
32409
  }
31540
- async function handleQError(error, snippetName, useRecovery) {
31541
- if (useRecovery) {
31542
- const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
31543
- emitRecoveryEnvelope2(error, {
31544
- operation: "q",
31545
- snippet: snippetName
32410
+ async function handleQError(error, snippetName, options, config) {
32411
+ let auditId = null;
32412
+ let envelopeId;
32413
+ if (options.recovery === true) {
32414
+ envelopeId = crypto3.randomUUID();
32415
+ }
32416
+ if (config) {
32417
+ auditId = await writeAuditEntry(config, "q", options, {
32418
+ success: false,
32419
+ target: snippetName,
32420
+ error,
32421
+ ...envelopeId && { recovery_ref: envelopeId }
31546
32422
  });
31547
32423
  }
32424
+ if (envelopeId !== undefined) {
32425
+ const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
32426
+ emitRecoveryEnvelope2(error, { operation: "q", snippet: snippetName }, { envelopeId, auditRef: auditId ?? undefined });
32427
+ }
31548
32428
  if (error instanceof SavedQueryError) {
31549
32429
  console.error(error.message);
31550
32430
  process.exit(1);
@@ -31568,8 +32448,8 @@ async function handleQError(error, snippetName, useRecovery) {
31568
32448
  // src/commands/queries.ts
31569
32449
  init_message_loader();
31570
32450
  init_config();
31571
- import { mkdir as mkdir6, writeFile as writeFile3 } from "fs/promises";
31572
- import { dirname as dirname6 } from "path";
32451
+ import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
32452
+ import { dirname as dirname8 } from "path";
31573
32453
  import { spawn as spawn2 } from "child_process";
31574
32454
  init_saved_queries();
31575
32455
  init_fold();
@@ -31817,8 +32697,8 @@ async function queriesNew(name, options) {
31817
32697
  process.exit(1);
31818
32698
  return;
31819
32699
  }
31820
- await mkdir6(dirname6(file), { recursive: true });
31821
- await writeFile3(file, scaffold(name), "utf8");
32700
+ await mkdir9(dirname8(file), { recursive: true });
32701
+ await writeFile4(file, scaffold(name), "utf8");
31822
32702
  console.log(`Created ${file}`);
31823
32703
  if (source === "shared")
31824
32704
  console.log(t("queries.first_run_hint"));
@@ -32136,6 +33016,7 @@ queriesCommand.command("suggest [intent]").description(t("queries.suggest_descri
32136
33016
  // src/commands/insert.ts
32137
33017
  init_message_loader();
32138
33018
  init_adapters();
33019
+ import crypto4 from "crypto";
32139
33020
 
32140
33021
  // src/core/data-executor.ts
32141
33022
  init_permission_guard();
@@ -32441,7 +33322,695 @@ function previewDelete(collection, filter) {
32441
33322
  return `db.${collection}.deleteMany(${pretty(filter)})`;
32442
33323
  }
32443
33324
 
33325
+ // src/commands/dml-plan.ts
33326
+ init_config();
33327
+ init_validation();
33328
+
33329
+ // src/core/dml-plan-sql.ts
33330
+ var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
33331
+ function assertIdentifier(value, role) {
33332
+ if (!value || !IDENTIFIER_RE.test(value)) {
33333
+ throw new Error(`Invalid ${role} identifier: ${JSON.stringify(value)}. ` + `${role === "table" ? "Table" : "Column"} names must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
33334
+ }
33335
+ }
33336
+ function normalizeTable(table) {
33337
+ if (!table || table.trim() === "") {
33338
+ throw new Error("Table name required for plan SQL");
33339
+ }
33340
+ const trimmed = table.trim();
33341
+ assertIdentifier(trimmed, "table");
33342
+ return trimmed;
33343
+ }
33344
+ function normalizeColumns(data, role) {
33345
+ const keys = Object.keys(data);
33346
+ if (keys.length === 0) {
33347
+ throw new Error(role === "data" ? "INSERT plan requires at least one column in --data" : "UPDATE plan requires at least one column in --set");
33348
+ }
33349
+ for (const key of keys) {
33350
+ assertIdentifier(key, "column");
33351
+ }
33352
+ return keys;
33353
+ }
33354
+ function normalizeWhere(where) {
33355
+ const keys = Object.keys(where);
33356
+ if (keys.length === 0) {
33357
+ throw new Error("WHERE clause is required for plan SQL");
33358
+ }
33359
+ for (const key of keys) {
33360
+ assertIdentifier(key, "column");
33361
+ }
33362
+ return keys;
33363
+ }
33364
+ function buildInsertPlanSql(table, data) {
33365
+ const t2 = normalizeTable(table);
33366
+ const columns = normalizeColumns(data, "data");
33367
+ const placeholders = columns.map(() => "?").join(", ");
33368
+ return `INSERT INTO ${t2} (${columns.join(", ")}) VALUES (${placeholders})`;
33369
+ }
33370
+ function buildUpdatePlanSql(table, set, where) {
33371
+ const t2 = normalizeTable(table);
33372
+ const setCols = normalizeColumns(set, "set");
33373
+ const whereCols = normalizeWhere(where);
33374
+ const setClause = setCols.map((col) => `${col} = ?`).join(", ");
33375
+ const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
33376
+ return `UPDATE ${t2} SET ${setClause} WHERE ${whereClause}`;
33377
+ }
33378
+ function buildDeletePlanSql(table, where) {
33379
+ const t2 = normalizeTable(table);
33380
+ const whereCols = normalizeWhere(where);
33381
+ const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
33382
+ return `DELETE FROM ${t2} WHERE ${whereClause}`;
33383
+ }
33384
+
33385
+ // src/utils/where-parser.ts
33386
+ function parseWhereClause(whereClause) {
33387
+ if (!whereClause || whereClause.trim() === "") {
33388
+ throw new Error("WHERE clause cannot be empty");
33389
+ }
33390
+ const conditions = {};
33391
+ const andParts = whereClause.split(/\s+AND\s+/i);
33392
+ for (const part of andParts) {
33393
+ const match = part.match(/^(\w+)\s*=\s*(.+)$/);
33394
+ if (!match) {
33395
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
33396
+ }
33397
+ const column = match[1];
33398
+ const valueStr = match[2];
33399
+ if (valueStr === undefined || column === undefined) {
33400
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
33401
+ }
33402
+ const trimmed = valueStr.trim();
33403
+ const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
33404
+ if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
33405
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
33406
+ }
33407
+ const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
33408
+ let value = stripped;
33409
+ if (stripped !== "" && !isNaN(Number(stripped))) {
33410
+ value = Number(stripped);
33411
+ }
33412
+ if (stripped === "true")
33413
+ value = true;
33414
+ else if (stripped === "false")
33415
+ value = false;
33416
+ else if (stripped === "null")
33417
+ value = null;
33418
+ conditions[column] = value;
33419
+ }
33420
+ return conditions;
33421
+ }
33422
+
33423
+ // src/core/mongo/dml-plan.ts
33424
+ init_permission_guard();
33425
+ var MONGO_UPDATE_OPERATOR_ALLOWLIST = new Set(["$set", "$unset"]);
33426
+ var MONGO_BROAD_FILTER_OPERATORS = new Set([
33427
+ "$regex",
33428
+ "$in",
33429
+ "$nin",
33430
+ "$gt",
33431
+ "$gte",
33432
+ "$lt",
33433
+ "$lte",
33434
+ "$ne",
33435
+ "$exists",
33436
+ "$or",
33437
+ "$and",
33438
+ "$nor",
33439
+ "$not",
33440
+ "$expr",
33441
+ "$text"
33442
+ ]);
33443
+ function permissionSql(operation) {
33444
+ if (operation === "insert")
33445
+ return "INSERT INTO dummy";
33446
+ if (operation === "update")
33447
+ return "UPDATE dummy";
33448
+ return "DELETE FROM dummy";
33449
+ }
33450
+ function broadOperation(operation) {
33451
+ if (operation === "insert")
33452
+ return "INSERT";
33453
+ if (operation === "update")
33454
+ return "UPDATE";
33455
+ return "DELETE";
33456
+ }
33457
+ function pushFactor2(factors, code, severity, message) {
33458
+ if (factors.some((f) => f.code === code && f.message === message))
33459
+ return;
33460
+ factors.push({ code, severity, message });
33461
+ }
33462
+ function decide2(factors) {
33463
+ if (factors.some((f) => f.severity === "block"))
33464
+ return "BLOCK";
33465
+ if (factors.some((f) => f.severity === "warn"))
33466
+ return "WARN";
33467
+ return "ALLOW";
33468
+ }
33469
+ function extractMongoWrittenFields(setDoc) {
33470
+ const fields = new Set;
33471
+ const operators = [];
33472
+ let hasUnsupportedOperator = false;
33473
+ const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
33474
+ if (!hasAnyOperator) {
33475
+ for (const k of Object.keys(setDoc))
33476
+ fields.add(k);
33477
+ return { fields: Array.from(fields), operators: [], hasUnsupportedOperator: false };
33478
+ }
33479
+ for (const [op, payload] of Object.entries(setDoc)) {
33480
+ operators.push(op);
33481
+ if (!MONGO_UPDATE_OPERATOR_ALLOWLIST.has(op)) {
33482
+ hasUnsupportedOperator = true;
33483
+ continue;
33484
+ }
33485
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
33486
+ for (const k of Object.keys(payload))
33487
+ fields.add(k);
33488
+ }
33489
+ }
33490
+ return { fields: Array.from(fields), operators, hasUnsupportedOperator };
33491
+ }
33492
+ function hasIdEquality(where) {
33493
+ if (!("_id" in where))
33494
+ return false;
33495
+ const value = where._id;
33496
+ if (value === null || value === undefined)
33497
+ return false;
33498
+ if (typeof value === "object")
33499
+ return false;
33500
+ return true;
33501
+ }
33502
+ function hasNonIdEquality(where) {
33503
+ for (const [key, value] of Object.entries(where)) {
33504
+ if (key === "_id")
33505
+ continue;
33506
+ if (key.startsWith("$"))
33507
+ continue;
33508
+ if (value === null)
33509
+ continue;
33510
+ if (typeof value === "object")
33511
+ continue;
33512
+ return true;
33513
+ }
33514
+ return false;
33515
+ }
33516
+ function hasBroadFilter(where) {
33517
+ for (const [key, value] of Object.entries(where)) {
33518
+ if (MONGO_BROAD_FILTER_OPERATORS.has(key))
33519
+ return true;
33520
+ if (value && typeof value === "object" && !Array.isArray(value)) {
33521
+ for (const inner of Object.keys(value)) {
33522
+ if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
33523
+ return true;
33524
+ }
33525
+ }
33526
+ }
33527
+ return false;
33528
+ }
33529
+ function applyPermission(operation, context, factors) {
33530
+ const permResult = checkPermission(permissionSql(operation), context.permission);
33531
+ if (!permResult.allowed) {
33532
+ pushFactor2(factors, "permission_denied", "block", permResult.reason);
33533
+ }
33534
+ }
33535
+ function applyTableBlacklist(target, context, factors) {
33536
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
33537
+ if (blacklisted.includes(target.toLowerCase())) {
33538
+ pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
33539
+ }
33540
+ }
33541
+ function applyColumnBlacklist(target, fields, context, factors) {
33542
+ const columns = context.blacklist.columns ?? {};
33543
+ const lower = target.toLowerCase();
33544
+ let blacklistedFields = [];
33545
+ for (const [t2, cols] of Object.entries(columns)) {
33546
+ if (t2.toLowerCase() === lower) {
33547
+ blacklistedFields = cols;
33548
+ break;
33549
+ }
33550
+ }
33551
+ if (blacklistedFields.length === 0)
33552
+ return;
33553
+ for (const field of fields) {
33554
+ if (blacklistedFields.includes(field)) {
33555
+ pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted field ${target}.${field}.`);
33556
+ }
33557
+ }
33558
+ }
33559
+ function applySchemaCoverage(target, context, factors) {
33560
+ const schemaTables = Object.keys(context.schema);
33561
+ if (schemaTables.length === 0) {
33562
+ pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
33563
+ return;
33564
+ }
33565
+ const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
33566
+ if (!known) {
33567
+ pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
33568
+ }
33569
+ }
33570
+ function buildRecommendations2(factors) {
33571
+ const out = new Set;
33572
+ const codes = new Set(factors.map((f) => f.code));
33573
+ if (codes.has("nonsql_filter_empty")) {
33574
+ out.add("Add an _id equality filter before executing this MongoDB write.");
33575
+ }
33576
+ if (codes.has("nonsql_missing_id")) {
33577
+ out.add("Prefer an _id equality filter before executing this MongoDB write.");
33578
+ }
33579
+ if (codes.has("nonsql_filter_broad")) {
33580
+ out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
33581
+ }
33582
+ if (codes.has("nonsql_unsupported_operator")) {
33583
+ out.add("Restrict the update document to $set / $unset for planner-safe writes.");
33584
+ }
33585
+ if (codes.has("permission_denied")) {
33586
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
33587
+ }
33588
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
33589
+ out.add("Review blacklist rules before accessing sensitive data.");
33590
+ }
33591
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33592
+ out.add("Refresh schema cache for the target collection before executing.");
33593
+ }
33594
+ out.add("Use --dry-run on the actual write command.");
33595
+ return Array.from(out);
33596
+ }
33597
+ function buildSuggestedCommands2(target, factors) {
33598
+ const codes = new Set(factors.map((f) => f.code));
33599
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33600
+ return [`dbcli schema ${target} --format json`];
33601
+ }
33602
+ return [];
33603
+ }
33604
+ var analyzeMongoDmlRisk = (intent, context) => {
33605
+ const factors = [];
33606
+ applyPermission(intent.operation, context, factors);
33607
+ applyTableBlacklist(intent.target, context, factors);
33608
+ if (intent.operation === "insert") {
33609
+ applyColumnBlacklist(intent.target, Object.keys(intent.data), context, factors);
33610
+ } else if (intent.operation === "update") {
33611
+ const { fields, hasUnsupportedOperator } = extractMongoWrittenFields(intent.set);
33612
+ if (hasUnsupportedOperator) {
33613
+ pushFactor2(factors, "nonsql_unsupported_operator", "block", "MongoDB update uses an operator outside the MVP allowlist ($set, $unset).");
33614
+ }
33615
+ applyColumnBlacklist(intent.target, fields, context, factors);
33616
+ const where = intent.where ?? {};
33617
+ if (Object.keys(where).length === 0) {
33618
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
33619
+ } else if (!hasIdEquality(where)) {
33620
+ if (hasBroadFilter(where)) {
33621
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
33622
+ } else if (hasNonIdEquality(where)) {
33623
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
33624
+ } else {
33625
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
33626
+ }
33627
+ }
33628
+ } else {
33629
+ const where = intent.where ?? {};
33630
+ if (Object.keys(where).length === 0) {
33631
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
33632
+ } else if (!hasIdEquality(where)) {
33633
+ if (hasBroadFilter(where)) {
33634
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
33635
+ } else if (hasNonIdEquality(where)) {
33636
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
33637
+ } else {
33638
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
33639
+ }
33640
+ }
33641
+ }
33642
+ applySchemaCoverage(intent.target, context, factors);
33643
+ return {
33644
+ decision: decide2(factors),
33645
+ operation: broadOperation(intent.operation),
33646
+ targetTables: [intent.target],
33647
+ riskFactors: factors,
33648
+ recommendations: buildRecommendations2(factors),
33649
+ suggestedCommands: buildSuggestedCommands2(intent.target, factors)
33650
+ };
33651
+ };
33652
+
33653
+ // src/core/redis/dml-plan.ts
33654
+ init_permission_guard();
33655
+ function permissionSql2(operation) {
33656
+ if (operation === "insert")
33657
+ return "INSERT INTO dummy";
33658
+ if (operation === "update")
33659
+ return "UPDATE dummy";
33660
+ return "DELETE FROM dummy";
33661
+ }
33662
+ function broadOperation2(operation) {
33663
+ if (operation === "insert")
33664
+ return "INSERT";
33665
+ if (operation === "update")
33666
+ return "UPDATE";
33667
+ return "DELETE";
33668
+ }
33669
+ function pushFactor3(factors, code, severity, message) {
33670
+ if (factors.some((f) => f.code === code && f.message === message))
33671
+ return;
33672
+ factors.push({ code, severity, message });
33673
+ }
33674
+ function decide3(factors) {
33675
+ if (factors.some((f) => f.severity === "block"))
33676
+ return "BLOCK";
33677
+ if (factors.some((f) => f.severity === "warn"))
33678
+ return "WARN";
33679
+ return "ALLOW";
33680
+ }
33681
+ function classifyKey(target) {
33682
+ if (target === "*")
33683
+ return "all";
33684
+ if (target.includes("*") || target.includes("?") || /\[.+\]/.test(target))
33685
+ return "pattern";
33686
+ return "single";
33687
+ }
33688
+ function applyPermission2(operation, context, factors) {
33689
+ const result = checkPermission(permissionSql2(operation), context.permission);
33690
+ if (!result.allowed) {
33691
+ pushFactor3(factors, "permission_denied", "block", result.reason);
33692
+ }
33693
+ }
33694
+ function applyTableBlacklist2(target, context, factors) {
33695
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
33696
+ if (blacklisted.includes(target.toLowerCase())) {
33697
+ pushFactor3(factors, "table_blacklisted", "block", `Redis key ${target} is blacklisted.`);
33698
+ }
33699
+ }
33700
+ function applyColumnBlacklist2(target, fields, context, factors) {
33701
+ const columns = context.blacklist.columns ?? {};
33702
+ const lower = target.toLowerCase();
33703
+ let blacklistedFields = [];
33704
+ for (const [t2, cols] of Object.entries(columns)) {
33705
+ if (t2.toLowerCase() === lower) {
33706
+ blacklistedFields = cols;
33707
+ break;
33708
+ }
33709
+ }
33710
+ if (blacklistedFields.length === 0)
33711
+ return;
33712
+ for (const field of fields) {
33713
+ if (blacklistedFields.includes(field)) {
33714
+ pushFactor3(factors, "blacklisted_column", "block", `Redis write would touch blacklisted field ${target}.${field}.`);
33715
+ }
33716
+ }
33717
+ }
33718
+ function redisSchemaKeyMatches(schemaKey, target) {
33719
+ const lowerKey = schemaKey.toLowerCase();
33720
+ const lowerTarget = target.toLowerCase();
33721
+ if (lowerKey === lowerTarget)
33722
+ return true;
33723
+ if (!lowerKey.includes("*") && !lowerKey.includes("?"))
33724
+ return false;
33725
+ const pattern = lowerKey.split("").map((ch) => {
33726
+ if (ch === "*")
33727
+ return ".*";
33728
+ if (ch === "?")
33729
+ return ".";
33730
+ return ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
33731
+ }).join("");
33732
+ return new RegExp(`^${pattern}$`).test(lowerTarget);
33733
+ }
33734
+ function applySchemaCoverage2(target, context, factors) {
33735
+ const tables = Object.keys(context.schema);
33736
+ if (tables.length === 0) {
33737
+ pushFactor3(factors, "schema_cache_missing", "warn", "Schema/key metadata is missing for the selected Redis connection.");
33738
+ return;
33739
+ }
33740
+ const known = tables.some((t2) => redisSchemaKeyMatches(t2, target));
33741
+ if (!known) {
33742
+ pushFactor3(factors, "schema_table_unknown", "warn", `Redis key namespace ${target} is missing from cached metadata.`);
33743
+ }
33744
+ }
33745
+ function buildRecommendations3(factors) {
33746
+ const out = new Set;
33747
+ const codes = new Set(factors.map((f) => f.code));
33748
+ if (codes.has("nonsql_key_pattern_broad")) {
33749
+ out.add("Specify a concrete single key instead of a wildcard pattern.");
33750
+ }
33751
+ if (codes.has("nonsql_overwrite_unknown")) {
33752
+ out.add("Provide --set field information so the planner can verify the write target.");
33753
+ }
33754
+ if (codes.has("permission_denied")) {
33755
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
33756
+ }
33757
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
33758
+ out.add("Review blacklist rules before accessing sensitive data.");
33759
+ }
33760
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33761
+ out.add("Refresh schema cache for the target Redis namespace before executing.");
33762
+ }
33763
+ out.add("Use --dry-run on the actual write command.");
33764
+ return Array.from(out);
33765
+ }
33766
+ function buildSuggestedCommands3(target, factors) {
33767
+ const codes = new Set(factors.map((f) => f.code));
33768
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33769
+ return [`dbcli schema ${target} --format json`];
33770
+ }
33771
+ return [];
33772
+ }
33773
+ var analyzeRedisDmlRisk = (intent, context) => {
33774
+ const factors = [];
33775
+ applyPermission2(intent.operation, context, factors);
33776
+ applyTableBlacklist2(intent.target, context, factors);
33777
+ const keyClass = classifyKey(intent.target);
33778
+ if (keyClass === "all") {
33779
+ pushFactor3(factors, "nonsql_key_pattern_broad", "block", 'Redis key "*" matches every key and is unsafe for planner-level writes.');
33780
+ } else if (keyClass === "pattern") {
33781
+ pushFactor3(factors, "nonsql_key_pattern_broad", "warn", `Redis target ${intent.target} contains wildcard characters and may match multiple keys.`);
33782
+ }
33783
+ if (intent.operation === "insert") {
33784
+ applyColumnBlacklist2(intent.target, Object.keys(intent.data), context, factors);
33785
+ } else if (intent.operation === "update") {
33786
+ const fields = Object.keys(intent.set);
33787
+ if (fields.length === 0) {
33788
+ pushFactor3(factors, "nonsql_overwrite_unknown", "warn", "Redis update has no field information and overwrite impact cannot be verified.");
33789
+ }
33790
+ applyColumnBlacklist2(intent.target, fields, context, factors);
33791
+ }
33792
+ applySchemaCoverage2(intent.target, context, factors);
33793
+ return {
33794
+ decision: decide3(factors),
33795
+ operation: broadOperation2(intent.operation),
33796
+ targetTables: [intent.target],
33797
+ riskFactors: factors,
33798
+ recommendations: buildRecommendations3(factors),
33799
+ suggestedCommands: buildSuggestedCommands3(intent.target, factors)
33800
+ };
33801
+ };
33802
+
33803
+ // src/core/elasticsearch/dml-plan.ts
33804
+ init_permission_guard();
33805
+ function permissionSql3(operation) {
33806
+ if (operation === "insert")
33807
+ return "INSERT INTO dummy";
33808
+ if (operation === "update")
33809
+ return "UPDATE dummy";
33810
+ return "DELETE FROM dummy";
33811
+ }
33812
+ function broadOperation3(operation) {
33813
+ if (operation === "insert")
33814
+ return "INSERT";
33815
+ if (operation === "update")
33816
+ return "UPDATE";
33817
+ return "DELETE";
33818
+ }
33819
+ function pushFactor4(factors, code, severity, message) {
33820
+ if (factors.some((f) => f.code === code && f.message === message))
33821
+ return;
33822
+ factors.push({ code, severity, message });
33823
+ }
33824
+ function decide4(factors) {
33825
+ if (factors.some((f) => f.severity === "block"))
33826
+ return "BLOCK";
33827
+ if (factors.some((f) => f.severity === "warn"))
33828
+ return "WARN";
33829
+ return "ALLOW";
33830
+ }
33831
+ function hasIdEquality2(where) {
33832
+ if (!where)
33833
+ return false;
33834
+ if (!("_id" in where))
33835
+ return false;
33836
+ const value = where._id;
33837
+ if (value === null || value === undefined)
33838
+ return false;
33839
+ if (typeof value === "object")
33840
+ return false;
33841
+ return true;
33842
+ }
33843
+ function applyPermission3(operation, context, factors) {
33844
+ const result = checkPermission(permissionSql3(operation), context.permission);
33845
+ if (!result.allowed) {
33846
+ pushFactor4(factors, "permission_denied", "block", result.reason);
33847
+ }
33848
+ }
33849
+ function applyTableBlacklist3(target, context, factors) {
33850
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
33851
+ if (blacklisted.includes(target.toLowerCase())) {
33852
+ pushFactor4(factors, "table_blacklisted", "block", `Elasticsearch index ${target} is blacklisted.`);
33853
+ }
33854
+ }
33855
+ function applyColumnBlacklist3(target, fields, context, factors) {
33856
+ const columns = context.blacklist.columns ?? {};
33857
+ const lower = target.toLowerCase();
33858
+ let blacklistedFields = [];
33859
+ for (const [t2, cols] of Object.entries(columns)) {
33860
+ if (t2.toLowerCase() === lower) {
33861
+ blacklistedFields = cols;
33862
+ break;
33863
+ }
33864
+ }
33865
+ if (blacklistedFields.length === 0)
33866
+ return;
33867
+ for (const field of fields) {
33868
+ if (blacklistedFields.includes(field)) {
33869
+ pushFactor4(factors, "blacklisted_column", "block", `Elasticsearch write would touch blacklisted field ${target}.${field}.`);
33870
+ }
33871
+ }
33872
+ }
33873
+ function applySchemaCoverage3(target, context, factors) {
33874
+ const tables = Object.keys(context.schema);
33875
+ if (tables.length === 0) {
33876
+ pushFactor4(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected Elasticsearch connection.");
33877
+ return;
33878
+ }
33879
+ const known = tables.some((t2) => t2.toLowerCase() === target.toLowerCase());
33880
+ if (!known) {
33881
+ pushFactor4(factors, "schema_table_unknown", "warn", `Target index ${target} is missing from schema cache.`);
33882
+ }
33883
+ }
33884
+ function buildRecommendations4(factors) {
33885
+ const out = new Set;
33886
+ const codes = new Set(factors.map((f) => f.code));
33887
+ if (codes.has("nonsql_missing_id")) {
33888
+ out.add("Add an _id equality filter (or include _id in the document) before executing.");
33889
+ }
33890
+ if (codes.has("nonsql_unsupported_bulk")) {
33891
+ out.add("Use a document-level write instead of a bulk or by-query operation.");
33892
+ }
33893
+ if (codes.has("permission_denied")) {
33894
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
33895
+ }
33896
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
33897
+ out.add("Review blacklist rules before accessing sensitive data.");
33898
+ }
33899
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33900
+ out.add("Refresh schema cache for the target index before executing.");
33901
+ }
33902
+ out.add("Use --dry-run on the actual write command.");
33903
+ return Array.from(out);
33904
+ }
33905
+ function buildSuggestedCommands4(target, factors) {
33906
+ const codes = new Set(factors.map((f) => f.code));
33907
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33908
+ return [`dbcli schema ${target} --format json`];
33909
+ }
33910
+ return [];
33911
+ }
33912
+ var analyzeElasticsearchDmlRisk = (intent, context) => {
33913
+ const factors = [];
33914
+ applyPermission3(intent.operation, context, factors);
33915
+ applyTableBlacklist3(intent.target, context, factors);
33916
+ if (intent.operation === "insert") {
33917
+ const fields = Object.keys(intent.data);
33918
+ applyColumnBlacklist3(intent.target, fields, context, factors);
33919
+ if (intent.data._id === undefined || intent.data._id === null || intent.data._id === "") {
33920
+ pushFactor4(factors, "nonsql_missing_id", "warn", "Elasticsearch insert has no _id; auto-id generation will make later correlation harder.");
33921
+ }
33922
+ } else if (intent.operation === "update") {
33923
+ applyColumnBlacklist3(intent.target, Object.keys(intent.set), context, factors);
33924
+ if (!hasIdEquality2(intent.where)) {
33925
+ pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch update requires _id in --where for planner-safe document-level update.");
33926
+ }
33927
+ } else {
33928
+ if (!hasIdEquality2(intent.where)) {
33929
+ pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch delete requires _id in --where for planner-safe document-level deletion.");
33930
+ }
33931
+ }
33932
+ applySchemaCoverage3(intent.target, context, factors);
33933
+ return {
33934
+ decision: decide4(factors),
33935
+ operation: broadOperation3(intent.operation),
33936
+ targetTables: [intent.target],
33937
+ riskFactors: factors,
33938
+ recommendations: buildRecommendations4(factors),
33939
+ suggestedCommands: buildSuggestedCommands4(intent.target, factors)
33940
+ };
33941
+ };
33942
+
33943
+ // src/commands/dml-plan.ts
33944
+ var ALLOWED_FORMATS5 = ["text", "json"];
33945
+ var SQL_SYSTEMS = new Set(["postgresql", "mysql", "mariadb"]);
33946
+ function buildPlanSqlForIntent(intent) {
33947
+ if (intent.operation === "insert") {
33948
+ return buildInsertPlanSql(intent.target, intent.data);
33949
+ }
33950
+ if (intent.operation === "update") {
33951
+ if (!intent.where || Object.keys(intent.where).length === 0) {
33952
+ throw new Error("WHERE clause is required for SQL update plan");
33953
+ }
33954
+ return buildUpdatePlanSql(intent.target, intent.set, intent.where);
33955
+ }
33956
+ if (!intent.where || Object.keys(intent.where).length === 0) {
33957
+ throw new Error("WHERE clause is required for SQL delete plan");
33958
+ }
33959
+ return buildDeletePlanSql(intent.target, intent.where);
33960
+ }
33961
+ async function runDmlPlanAnalysis(intent, options, command) {
33962
+ try {
33963
+ const format = options.format ?? "text";
33964
+ validateFormat(format, ALLOWED_FORMATS5, "plan");
33965
+ const configPath = resolveConfigPath(command, options);
33966
+ const config = await configModule.read(configPath);
33967
+ if (!config.connection) {
33968
+ throw new Error('Run "dbcli init" to configure database connection');
33969
+ }
33970
+ const system = config.connection.system;
33971
+ const schema = config.schema ?? {};
33972
+ const blacklist = config.blacklist ?? { tables: [], columns: {} };
33973
+ if (SQL_SYSTEMS.has(system)) {
33974
+ const planSql = buildPlanSqlForIntent(intent);
33975
+ const schemaTableCount = Object.keys(schema).length;
33976
+ const result = analyzeQueryRisk({
33977
+ sql: planSql,
33978
+ permission: config.permission,
33979
+ blacklist,
33980
+ schemaLookup: {
33981
+ tables: schema,
33982
+ cacheAvailable: schemaTableCount > 0
33983
+ }
33984
+ });
33985
+ console.log(formatPlanResult(result, format));
33986
+ return;
33987
+ }
33988
+ const context = {
33989
+ permission: config.permission,
33990
+ blacklist,
33991
+ schema
33992
+ };
33993
+ if (system === "mongodb") {
33994
+ console.log(formatPlanResult(analyzeMongoDmlRisk(intent, context), format));
33995
+ return;
33996
+ }
33997
+ if (system === "redis") {
33998
+ console.log(formatPlanResult(analyzeRedisDmlRisk(intent, context), format));
33999
+ return;
34000
+ }
34001
+ if (system === "elasticsearch") {
34002
+ console.log(formatPlanResult(analyzeElasticsearchDmlRisk(intent, context), format));
34003
+ return;
34004
+ }
34005
+ throw new Error(`--plan does not support connection system "${system}" yet`);
34006
+ } catch (error) {
34007
+ console.error(error.message);
34008
+ process.exit(1);
34009
+ }
34010
+ }
34011
+
32444
34012
  // src/commands/insert.ts
34013
+ init_integration_helper();
32445
34014
  function requireSqlConnection3(connection) {
32446
34015
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
32447
34016
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -32467,6 +34036,7 @@ function isStdinAvailable() {
32467
34036
  }
32468
34037
  }
32469
34038
  async function insertCommand(table, options, command) {
34039
+ let config;
32470
34040
  try {
32471
34041
  if (!table || table.trim() === "") {
32472
34042
  throw new Error("Table name required");
@@ -32493,10 +34063,24 @@ async function insertCommand(table, options, command) {
32493
34063
  throw new Error('JSON must be an object (e.g. {"name":"Alice","email":"a@b.com"})');
32494
34064
  }
32495
34065
  const configPath = resolveConfigPath(command, options);
32496
- const config = await configModule.read(configPath);
34066
+ config = await configModule.read(configPath);
32497
34067
  if (!config.connection) {
32498
34068
  throw new Error('Run "dbcli init" to configure database connection');
32499
34069
  }
34070
+ if (options.plan) {
34071
+ if (options.dryRun) {
34072
+ console.error("--plan cannot be used with --dry-run");
34073
+ process.exit(1);
34074
+ return;
34075
+ }
34076
+ await runDmlPlanAnalysis({ operation: "insert", target: table, data }, { format: options.format, config: options.config }, command);
34077
+ await writeAuditEntry(config, "insert", options, {
34078
+ success: true,
34079
+ target: table,
34080
+ metadata: { plan: true }
34081
+ });
34082
+ return;
34083
+ }
32500
34084
  if (config.connection.system === "redis") {
32501
34085
  enforcePermission("INSERT INTO dummy", config.permission);
32502
34086
  const blacklistManager = new BlacklistManager(config);
@@ -32512,6 +34096,11 @@ async function insertCommand(table, options, command) {
32512
34096
  timestamp: new Date().toISOString()
32513
34097
  };
32514
34098
  console.log(JSON.stringify(output, null, 2));
34099
+ await writeAuditEntry(config, "insert", options, {
34100
+ success: true,
34101
+ target: table,
34102
+ metadata: { rows_affected: 0, dry_run: true }
34103
+ });
32515
34104
  return;
32516
34105
  }
32517
34106
  const adapter2 = AdapterFactory.createRedisAdapter(config.connection);
@@ -32525,6 +34114,11 @@ async function insertCommand(table, options, command) {
32525
34114
  timestamp: new Date().toISOString()
32526
34115
  };
32527
34116
  console.log(JSON.stringify(output, null, 2));
34117
+ await writeAuditEntry(config, "insert", options, {
34118
+ success: true,
34119
+ target: table,
34120
+ metadata: { rows_affected: result.affectedRows }
34121
+ });
32528
34122
  return;
32529
34123
  } finally {
32530
34124
  await adapter2.disconnect();
@@ -32538,6 +34132,11 @@ async function insertCommand(table, options, command) {
32538
34132
  timestamp: new Date().toISOString(),
32539
34133
  error: "Elasticsearch \u4E0D\u652F\u63F4 insert \u6307\u4EE4\uFF1B\u76EE\u524D\u8ACB\u4F7F\u7528\u5916\u90E8\u5DE5\u5177\uFF08\u5982 curl \u6216 Kibana DevTools\uFF09\u9032\u884C\u6587\u4EF6\u5BEB\u5165"
32540
34134
  };
34135
+ await writeAuditEntry(config, "insert", options, {
34136
+ success: false,
34137
+ target: table,
34138
+ error: new Error("Elasticsearch does not support insert command")
34139
+ });
32541
34140
  console.log(JSON.stringify(output, null, 2));
32542
34141
  process.exit(1);
32543
34142
  }
@@ -32556,6 +34155,11 @@ async function insertCommand(table, options, command) {
32556
34155
  timestamp: new Date().toISOString()
32557
34156
  };
32558
34157
  console.log(JSON.stringify(output, null, 2));
34158
+ await writeAuditEntry(config, "insert", options, {
34159
+ success: true,
34160
+ target: table,
34161
+ metadata: { rows_affected: 0, dry_run: true }
34162
+ });
32559
34163
  return;
32560
34164
  }
32561
34165
  const adapter2 = AdapterFactory.createMongoDBAdapter(config.connection);
@@ -32570,6 +34174,11 @@ async function insertCommand(table, options, command) {
32570
34174
  lastInsertId: result.lastInsertId
32571
34175
  };
32572
34176
  console.log(JSON.stringify(output, null, 2));
34177
+ await writeAuditEntry(config, "insert", options, {
34178
+ success: true,
34179
+ target: table,
34180
+ metadata: { rows_affected: result.affectedRows }
34181
+ });
32573
34182
  return;
32574
34183
  } finally {
32575
34184
  await adapter2.disconnect();
@@ -32596,6 +34205,18 @@ async function insertCommand(table, options, command) {
32596
34205
  ...result.error && { error: result.error }
32597
34206
  };
32598
34207
  console.log(JSON.stringify(output, null, 2));
34208
+ await writeAuditEntry(config, "insert", options, {
34209
+ success: result.status === "success",
34210
+ target: table,
34211
+ ...result.sql && { sql: result.sql },
34212
+ metadata: {
34213
+ rows_affected: result.rows_affected,
34214
+ ...options.dryRun && { dry_run: true }
34215
+ },
34216
+ ...result.status === "error" && {
34217
+ error: new Error(result.error ?? "insert failed")
34218
+ }
34219
+ });
32599
34220
  if (result.status === "error") {
32600
34221
  process.exit(1);
32601
34222
  }
@@ -32603,14 +34224,23 @@ async function insertCommand(table, options, command) {
32603
34224
  await adapter.disconnect();
32604
34225
  }
32605
34226
  } catch (error) {
34227
+ let auditId = null;
34228
+ let envelopeId;
32606
34229
  if (options.recovery === true) {
32607
- const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
32608
- emitRecoveryEnvelope2(error, {
32609
- operation: "insert",
32610
- table,
32611
- writeOperation: "INSERT"
34230
+ envelopeId = crypto4.randomUUID();
34231
+ }
34232
+ if (config) {
34233
+ auditId = await writeAuditEntry(config, "insert", options, {
34234
+ success: false,
34235
+ target: table,
34236
+ error,
34237
+ ...envelopeId && { recovery_ref: envelopeId }
32612
34238
  });
32613
34239
  }
34240
+ if (envelopeId !== undefined) {
34241
+ const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
34242
+ emitRecoveryEnvelope2(error, { operation: "insert", table, writeOperation: "INSERT" }, { envelopeId, auditRef: auditId ?? undefined });
34243
+ }
32614
34244
  if (error instanceof BlacklistError) {
32615
34245
  const output2 = {
32616
34246
  status: "error",
@@ -32645,61 +34275,32 @@ async function insertCommand(table, options, command) {
32645
34275
  // src/commands/update.ts
32646
34276
  init_message_loader();
32647
34277
  init_adapters();
34278
+ import crypto5 from "crypto";
32648
34279
  init_config();
32649
34280
  init_permission_guard();
32650
34281
  init_blacklist();
34282
+ init_integration_helper();
32651
34283
  function requireSqlConnection4(connection) {
32652
34284
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
32653
34285
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
32654
34286
  }
32655
34287
  return connection;
32656
34288
  }
32657
- function parseWhereClause(whereClause) {
32658
- if (!whereClause || whereClause.trim() === "") {
32659
- throw new Error("WHERE clause cannot be empty");
32660
- }
32661
- const conditions = {};
32662
- const andParts = whereClause.split(/\s+AND\s+/i);
32663
- for (const part of andParts) {
32664
- const match = part.match(/^(\w+)\s*=\s*(.+)$/);
32665
- if (!match) {
32666
- throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
32667
- }
32668
- const column = match[1];
32669
- const valueStr = match[2];
32670
- if (valueStr === undefined || column === undefined) {
32671
- throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
32672
- }
32673
- const trimmed = valueStr.trim();
32674
- const stripped = trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
32675
- let value = stripped;
32676
- if (stripped !== "" && !isNaN(Number(stripped))) {
32677
- value = Number(stripped);
32678
- }
32679
- if (stripped === "true")
32680
- value = true;
32681
- else if (stripped === "false")
32682
- value = false;
32683
- else if (stripped === "null")
32684
- value = null;
32685
- conditions[column] = value;
32686
- }
32687
- return conditions;
32688
- }
32689
34289
  async function updateCommand(table, options, command) {
34290
+ let config;
32690
34291
  try {
32691
34292
  if (!table || table.trim() === "") {
32692
34293
  throw new Error("Table name required");
32693
34294
  }
32694
34295
  table = table.trim();
32695
- if (!options.where || options.where.trim() === "") {
34296
+ if (!options.plan && (!options.where || options.where.trim() === "")) {
32696
34297
  throw new Error('UPDATE requires --where clause (e.g. --where "id=1")');
32697
34298
  }
32698
34299
  if (!options.set || options.set.trim() === "") {
32699
34300
  throw new Error(`UPDATE requires --set flag with JSON data (e.g. --set '{"name":"Bob"}')`);
32700
34301
  }
32701
34302
  const configPath = resolveConfigPath(command, options);
32702
- const config = await configModule.read(configPath);
34303
+ config = await configModule.read(configPath);
32703
34304
  if (!config.connection) {
32704
34305
  throw new Error('Run "dbcli init" to configure database connection');
32705
34306
  }
@@ -32712,6 +34313,45 @@ async function updateCommand(table, options, command) {
32712
34313
  if (!setData || typeof setData !== "object" || Array.isArray(setData)) {
32713
34314
  throw new Error('JSON in --set must be an object (e.g. {"name":"Bob","email":"b@example.com"})');
32714
34315
  }
34316
+ if (options.plan) {
34317
+ if (options.dryRun) {
34318
+ console.error("--plan cannot be used with --dry-run");
34319
+ process.exit(1);
34320
+ return;
34321
+ }
34322
+ const rawWhere = options.where ?? "";
34323
+ let whereForPlan = null;
34324
+ if (rawWhere.trim() !== "") {
34325
+ try {
34326
+ const parsed = JSON.parse(rawWhere);
34327
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
34328
+ whereForPlan = parsed;
34329
+ }
34330
+ } catch {
34331
+ whereForPlan = null;
34332
+ }
34333
+ if (whereForPlan === null) {
34334
+ try {
34335
+ whereForPlan = parseWhereClause(rawWhere);
34336
+ } catch {
34337
+ whereForPlan = {};
34338
+ }
34339
+ }
34340
+ }
34341
+ await runDmlPlanAnalysis({
34342
+ operation: "update",
34343
+ target: table,
34344
+ set: setData,
34345
+ where: whereForPlan,
34346
+ rawWhere
34347
+ }, { format: options.format, config: options.config }, command);
34348
+ await writeAuditEntry(config, "update", options, {
34349
+ success: true,
34350
+ target: table,
34351
+ metadata: { plan: true }
34352
+ });
34353
+ return;
34354
+ }
32715
34355
  if (config.connection?.system === "redis") {
32716
34356
  enforcePermission("UPDATE dummy", config.permission);
32717
34357
  const blacklistManager = new BlacklistManager(config);
@@ -32726,6 +34366,11 @@ async function updateCommand(table, options, command) {
32726
34366
  timestamp: new Date().toISOString()
32727
34367
  };
32728
34368
  console.log(JSON.stringify(output, null, 2));
34369
+ await writeAuditEntry(config, "update", options, {
34370
+ success: true,
34371
+ target: table,
34372
+ metadata: { rows_affected: 0, dry_run: true }
34373
+ });
32729
34374
  return;
32730
34375
  }
32731
34376
  const adapter2 = AdapterFactory.createRedisAdapter(config.connection);
@@ -32739,6 +34384,11 @@ async function updateCommand(table, options, command) {
32739
34384
  timestamp: new Date().toISOString()
32740
34385
  };
32741
34386
  console.log(JSON.stringify(output, null, 2));
34387
+ await writeAuditEntry(config, "update", options, {
34388
+ success: true,
34389
+ target: table,
34390
+ metadata: { rows_affected: result.affectedRows }
34391
+ });
32742
34392
  return;
32743
34393
  } finally {
32744
34394
  await adapter2.disconnect();
@@ -32752,6 +34402,11 @@ async function updateCommand(table, options, command) {
32752
34402
  timestamp: new Date().toISOString(),
32753
34403
  error: "Elasticsearch \u4E0D\u652F\u63F4 update \u6307\u4EE4\uFF1B\u76EE\u524D\u8ACB\u4F7F\u7528\u5916\u90E8\u5DE5\u5177\uFF08\u5982 curl \u6216 Kibana DevTools\uFF09\u9032\u884C\u6587\u4EF6\u66F4\u65B0"
32754
34404
  };
34405
+ await writeAuditEntry(config, "update", options, {
34406
+ success: false,
34407
+ target: table,
34408
+ error: new Error("Elasticsearch does not support update command")
34409
+ });
32755
34410
  console.log(JSON.stringify(output, null, 2));
32756
34411
  process.exit(1);
32757
34412
  }
@@ -32791,6 +34446,11 @@ async function updateCommand(table, options, command) {
32791
34446
  timestamp: new Date().toISOString()
32792
34447
  };
32793
34448
  console.log(JSON.stringify(output, null, 2));
34449
+ await writeAuditEntry(config, "update", options, {
34450
+ success: true,
34451
+ target: table,
34452
+ metadata: { rows_affected: 0, dry_run: true }
34453
+ });
32794
34454
  return;
32795
34455
  }
32796
34456
  const adapter2 = AdapterFactory.createMongoDBAdapter(config.connection);
@@ -32804,6 +34464,11 @@ async function updateCommand(table, options, command) {
32804
34464
  timestamp: new Date().toISOString()
32805
34465
  };
32806
34466
  console.log(JSON.stringify(output, null, 2));
34467
+ await writeAuditEntry(config, "update", options, {
34468
+ success: true,
34469
+ target: table,
34470
+ metadata: { rows_affected: result.affectedRows }
34471
+ });
32807
34472
  return;
32808
34473
  } finally {
32809
34474
  await adapter2.disconnect();
@@ -32836,6 +34501,18 @@ async function updateCommand(table, options, command) {
32836
34501
  ...result.error && { error: result.error }
32837
34502
  };
32838
34503
  console.log(JSON.stringify(output, null, 2));
34504
+ await writeAuditEntry(config, "update", options, {
34505
+ success: result.status === "success",
34506
+ target: table,
34507
+ ...result.sql && { sql: result.sql },
34508
+ metadata: {
34509
+ rows_affected: result.rows_affected,
34510
+ ...options.dryRun && { dry_run: true }
34511
+ },
34512
+ ...result.status === "error" && {
34513
+ error: new Error(result.error ?? "update failed")
34514
+ }
34515
+ });
32839
34516
  if (result.status === "error") {
32840
34517
  process.exit(1);
32841
34518
  }
@@ -32843,14 +34520,23 @@ async function updateCommand(table, options, command) {
32843
34520
  await adapter.disconnect();
32844
34521
  }
32845
34522
  } catch (error) {
34523
+ let auditId = null;
34524
+ let envelopeId;
32846
34525
  if (options.recovery === true) {
32847
- const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
32848
- emitRecoveryEnvelope2(error, {
32849
- operation: "update",
32850
- table,
32851
- writeOperation: "UPDATE"
34526
+ envelopeId = crypto5.randomUUID();
34527
+ }
34528
+ if (config) {
34529
+ auditId = await writeAuditEntry(config, "update", options, {
34530
+ success: false,
34531
+ target: table,
34532
+ error,
34533
+ ...envelopeId && { recovery_ref: envelopeId }
32852
34534
  });
32853
34535
  }
34536
+ if (envelopeId !== undefined) {
34537
+ const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
34538
+ emitRecoveryEnvelope2(error, { operation: "update", table, writeOperation: "UPDATE" }, { envelopeId, auditRef: auditId ?? undefined });
34539
+ }
32854
34540
  if (error instanceof BlacklistError) {
32855
34541
  const output2 = {
32856
34542
  status: "error",
@@ -32885,57 +34571,70 @@ async function updateCommand(table, options, command) {
32885
34571
  // src/commands/delete.ts
32886
34572
  init_message_loader();
32887
34573
  init_adapters();
34574
+ import crypto6 from "crypto";
32888
34575
  init_config();
32889
34576
  init_permission_guard();
32890
34577
  init_blacklist();
34578
+ init_integration_helper();
32891
34579
  function requireSqlConnection5(connection) {
32892
34580
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
32893
34581
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
32894
34582
  }
32895
34583
  return connection;
32896
34584
  }
32897
- function parseWhereClause2(whereClause) {
32898
- if (!whereClause || whereClause.trim() === "") {
32899
- throw new Error('DELETE requires --where clause (e.g. --where "id=1")');
32900
- }
32901
- const conditions = {};
32902
- const andParts = whereClause.split(/\s+AND\s+/i);
32903
- for (const part of andParts) {
32904
- const match = part.match(/^(\w+)\s*=\s*(.+)$/);
32905
- if (!match || !match[1] || !match[2]) {
32906
- throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
32907
- }
32908
- const column = match[1];
32909
- const trimmed = match[2].trim();
32910
- const stripped = trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
32911
- let value = stripped;
32912
- if (stripped !== "" && !isNaN(Number(stripped))) {
32913
- value = Number(stripped);
32914
- }
32915
- if (stripped === "true")
32916
- value = true;
32917
- else if (stripped === "false")
32918
- value = false;
32919
- else if (stripped === "null")
32920
- value = null;
32921
- conditions[column] = value;
32922
- }
32923
- return conditions;
32924
- }
32925
34585
  async function deleteCommand(table, options, command) {
34586
+ let config;
32926
34587
  try {
32927
34588
  if (!table || table.trim() === "") {
32928
34589
  throw new Error("Table name required");
32929
34590
  }
32930
34591
  table = table.trim();
32931
- if (!options.where || options.where.trim() === "") {
34592
+ if (!options.plan && (!options.where || options.where.trim() === "")) {
32932
34593
  throw new Error('DELETE requires --where clause (e.g. --where "id=1")');
32933
34594
  }
32934
34595
  const configPath = resolveConfigPath(command, options);
32935
- const config = await configModule.read(configPath);
34596
+ config = await configModule.read(configPath);
32936
34597
  if (!config.connection) {
32937
34598
  throw new Error('Run "dbcli init" to configure database connection');
32938
34599
  }
34600
+ if (options.plan) {
34601
+ if (options.dryRun) {
34602
+ console.error("--plan cannot be used with --dry-run");
34603
+ process.exit(1);
34604
+ return;
34605
+ }
34606
+ const rawWhere = options.where ?? "";
34607
+ let whereForPlan = null;
34608
+ if (rawWhere.trim() !== "") {
34609
+ try {
34610
+ const parsed = JSON.parse(rawWhere);
34611
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
34612
+ whereForPlan = parsed;
34613
+ }
34614
+ } catch {
34615
+ whereForPlan = null;
34616
+ }
34617
+ if (whereForPlan === null) {
34618
+ try {
34619
+ whereForPlan = parseWhereClause(rawWhere);
34620
+ } catch {
34621
+ whereForPlan = {};
34622
+ }
34623
+ }
34624
+ }
34625
+ await runDmlPlanAnalysis({
34626
+ operation: "delete",
34627
+ target: table,
34628
+ where: whereForPlan,
34629
+ rawWhere
34630
+ }, { format: options.format, config: options.config }, command);
34631
+ await writeAuditEntry(config, "delete", options, {
34632
+ success: true,
34633
+ target: table,
34634
+ metadata: { plan: true }
34635
+ });
34636
+ return;
34637
+ }
32939
34638
  if (config.permission !== "data-admin" && config.permission !== "admin") {
32940
34639
  throw new PermissionError(t("delete.admin_only"), {
32941
34640
  type: "DELETE",
@@ -32953,7 +34652,7 @@ async function deleteCommand(table, options, command) {
32953
34652
  try {
32954
34653
  filter = JSON.parse(options.where);
32955
34654
  } catch {
32956
- filter = parseWhereClause2(options.where);
34655
+ filter = parseWhereClause(options.where);
32957
34656
  }
32958
34657
  if (options.dryRun) {
32959
34658
  const output = {
@@ -32964,6 +34663,11 @@ async function deleteCommand(table, options, command) {
32964
34663
  timestamp: new Date().toISOString()
32965
34664
  };
32966
34665
  console.log(JSON.stringify(output, null, 2));
34666
+ await writeAuditEntry(config, "delete", options, {
34667
+ success: true,
34668
+ target: table,
34669
+ metadata: { rows_affected: 0, dry_run: true }
34670
+ });
32967
34671
  return;
32968
34672
  }
32969
34673
  const adapter2 = AdapterFactory.createRedisAdapter(config.connection);
@@ -32977,6 +34681,11 @@ async function deleteCommand(table, options, command) {
32977
34681
  timestamp: new Date().toISOString()
32978
34682
  };
32979
34683
  console.log(JSON.stringify(output, null, 2));
34684
+ await writeAuditEntry(config, "delete", options, {
34685
+ success: true,
34686
+ target: table,
34687
+ metadata: { rows_affected: result.affectedRows }
34688
+ });
32980
34689
  return;
32981
34690
  } finally {
32982
34691
  await adapter2.disconnect();
@@ -32990,6 +34699,11 @@ async function deleteCommand(table, options, command) {
32990
34699
  timestamp: new Date().toISOString(),
32991
34700
  error: "Elasticsearch \u4E0D\u652F\u63F4 delete \u6307\u4EE4\uFF1B\u76EE\u524D\u8ACB\u4F7F\u7528\u5916\u90E8\u5DE5\u5177\uFF08\u5982 curl \u6216 Kibana DevTools\uFF09\u9032\u884C\u6587\u4EF6\u522A\u9664"
32992
34701
  };
34702
+ await writeAuditEntry(config, "delete", options, {
34703
+ success: false,
34704
+ target: table,
34705
+ error: new Error("Elasticsearch does not support delete command")
34706
+ });
32993
34707
  console.log(JSON.stringify(output, null, 2));
32994
34708
  process.exit(1);
32995
34709
  }
@@ -33001,7 +34715,7 @@ async function deleteCommand(table, options, command) {
33001
34715
  try {
33002
34716
  filter = JSON.parse(options.where);
33003
34717
  } catch {
33004
- filter = parseWhereClause2(options.where);
34718
+ filter = parseWhereClause(options.where);
33005
34719
  }
33006
34720
  if (options.dryRun) {
33007
34721
  const output = {
@@ -33012,6 +34726,11 @@ async function deleteCommand(table, options, command) {
33012
34726
  timestamp: new Date().toISOString()
33013
34727
  };
33014
34728
  console.log(JSON.stringify(output, null, 2));
34729
+ await writeAuditEntry(config, "delete", options, {
34730
+ success: true,
34731
+ target: table,
34732
+ metadata: { rows_affected: 0, dry_run: true }
34733
+ });
33015
34734
  return;
33016
34735
  }
33017
34736
  const adapter2 = AdapterFactory.createMongoDBAdapter(config.connection);
@@ -33025,6 +34744,11 @@ async function deleteCommand(table, options, command) {
33025
34744
  timestamp: new Date().toISOString()
33026
34745
  };
33027
34746
  console.log(JSON.stringify(output, null, 2));
34747
+ await writeAuditEntry(config, "delete", options, {
34748
+ success: true,
34749
+ target: table,
34750
+ metadata: { rows_affected: result.affectedRows }
34751
+ });
33028
34752
  return;
33029
34753
  } finally {
33030
34754
  await adapter2.disconnect();
@@ -33032,7 +34756,7 @@ async function deleteCommand(table, options, command) {
33032
34756
  }
33033
34757
  let whereConditions;
33034
34758
  try {
33035
- whereConditions = parseWhereClause2(options.where);
34759
+ whereConditions = parseWhereClause(options.where);
33036
34760
  } catch (error) {
33037
34761
  throw new Error(`WHERE clause parsing failed: ${error.message}`);
33038
34762
  }
@@ -33057,6 +34781,18 @@ async function deleteCommand(table, options, command) {
33057
34781
  ...result.error && { error: result.error }
33058
34782
  };
33059
34783
  console.log(JSON.stringify(output, null, 2));
34784
+ await writeAuditEntry(config, "delete", options, {
34785
+ success: result.status === "success",
34786
+ target: table,
34787
+ ...result.sql && { sql: result.sql },
34788
+ metadata: {
34789
+ rows_affected: result.rows_affected,
34790
+ ...options.dryRun && { dry_run: true }
34791
+ },
34792
+ ...result.status === "error" && {
34793
+ error: new Error(result.error ?? "delete failed")
34794
+ }
34795
+ });
33060
34796
  if (result.status === "error") {
33061
34797
  process.exit(1);
33062
34798
  }
@@ -33064,14 +34800,23 @@ async function deleteCommand(table, options, command) {
33064
34800
  await adapter.disconnect();
33065
34801
  }
33066
34802
  } catch (error) {
34803
+ let auditId = null;
34804
+ let envelopeId;
33067
34805
  if (options.recovery === true) {
33068
- const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
33069
- emitRecoveryEnvelope2(error, {
33070
- operation: "delete",
33071
- table,
33072
- writeOperation: "DELETE"
34806
+ envelopeId = crypto6.randomUUID();
34807
+ }
34808
+ if (config) {
34809
+ auditId = await writeAuditEntry(config, "delete", options, {
34810
+ success: false,
34811
+ target: table,
34812
+ error,
34813
+ ...envelopeId && { recovery_ref: envelopeId }
33073
34814
  });
33074
34815
  }
34816
+ if (envelopeId !== undefined) {
34817
+ const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
34818
+ emitRecoveryEnvelope2(error, { operation: "delete", table, writeOperation: "DELETE" }, { envelopeId, auditRef: auditId ?? undefined });
34819
+ }
33075
34820
  if (error instanceof BlacklistError) {
33076
34821
  const output2 = {
33077
34822
  status: "error",
@@ -33106,17 +34851,21 @@ async function deleteCommand(table, options, command) {
33106
34851
  // src/commands/export.ts
33107
34852
  init_message_loader();
33108
34853
  init_adapters();
34854
+ import crypto7 from "crypto";
33109
34855
  init_config();
33110
34856
  init_permission_guard();
33111
34857
  init_blacklist();
34858
+ init_integration_helper();
34859
+ init_engine_hints();
33112
34860
  function requireSqlConnection6(connection) {
33113
34861
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
33114
34862
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
33115
34863
  }
33116
34864
  return connection;
33117
34865
  }
33118
- var SQL_PATTERN2 = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
34866
+ var SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
33119
34867
  async function exportCommand(sql, options, command) {
34868
+ let config;
33120
34869
  try {
33121
34870
  if (!sql || sql.trim() === "") {
33122
34871
  throw new Error("Query required");
@@ -33126,7 +34875,7 @@ async function exportCommand(sql, options, command) {
33126
34875
  }
33127
34876
  sql = sql.trim();
33128
34877
  const configPath = resolveConfigPath(command, options);
33129
- const config = await configModule.read(configPath);
34878
+ config = await configModule.read(configPath);
33130
34879
  if (!config.connection) {
33131
34880
  throw new Error('Run "dbcli init" first');
33132
34881
  }
@@ -33182,18 +34931,39 @@ async function exportCommand(sql, options, command) {
33182
34931
  } else {
33183
34932
  console.log(formatted);
33184
34933
  }
34934
+ await writeAuditEntry(config, "export", options, {
34935
+ success: true,
34936
+ target: extractTableName(sql) ?? "*",
34937
+ sql,
34938
+ metadata: {
34939
+ rows_affected: result.rowCount,
34940
+ output_format: options.format,
34941
+ ...options.output && { output_file: options.output }
34942
+ }
34943
+ });
33185
34944
  } finally {
33186
34945
  await adapter.disconnect();
33187
34946
  }
33188
34947
  } catch (error) {
34948
+ let auditId = null;
34949
+ let envelopeId;
33189
34950
  if (options.recovery === true) {
33190
- const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
33191
- const m = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
33192
- emitRecoveryEnvelope2(error, {
33193
- operation: "export",
33194
- table: m?.[1]
34951
+ envelopeId = crypto7.randomUUID();
34952
+ }
34953
+ const m = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
34954
+ if (config) {
34955
+ auditId = await writeAuditEntry(config, "export", options, {
34956
+ success: false,
34957
+ target: m?.[1] ?? "*",
34958
+ sql,
34959
+ error,
34960
+ ...envelopeId && { recovery_ref: envelopeId }
33195
34961
  });
33196
34962
  }
34963
+ if (envelopeId !== undefined) {
34964
+ const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
34965
+ emitRecoveryEnvelope2(error, { operation: "export", table: m?.[1] }, { envelopeId, auditRef: auditId ?? undefined });
34966
+ }
33197
34967
  if (error instanceof PermissionError) {
33198
34968
  console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
33199
34969
  console.error(` Operation: ${error.classification.type}`);
@@ -33214,17 +34984,7 @@ async function exportCommand(sql, options, command) {
33214
34984
  }
33215
34985
  async function redisExportBranch(command, options, config) {
33216
34986
  const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
33217
- try {
33218
- enforceRedisPermission2(command, config.permission);
33219
- } catch (error) {
33220
- if (error instanceof PermissionError) {
33221
- console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
33222
- console.error(` Operation: ${error.classification.type}`);
33223
- console.error(` Message: ${error.message}`);
33224
- process.exit(1);
33225
- }
33226
- throw error;
33227
- }
34987
+ enforceRedisPermission2(command, config.permission);
33228
34988
  const redisAdapter = AdapterFactory.createRedisAdapter(config.connection);
33229
34989
  await redisAdapter.connect();
33230
34990
  try {
@@ -33255,12 +35015,22 @@ async function redisExportBranch(command, options, config) {
33255
35015
  } else {
33256
35016
  console.log(formatted);
33257
35017
  }
35018
+ const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
35019
+ await writeAuditEntry(config, "export", options, {
35020
+ success: true,
35021
+ target,
35022
+ metadata: {
35023
+ rows_affected: result.rowCount ?? result.rows.length ?? 0,
35024
+ output_format: options.format,
35025
+ ...options.output && { output_file: options.output }
35026
+ }
35027
+ });
33258
35028
  } finally {
33259
35029
  await redisAdapter.disconnect();
33260
35030
  }
33261
35031
  }
33262
35032
  async function mongoExportBranch(query, options, config) {
33263
- if (SQL_PATTERN2.test(query)) {
35033
+ if (SQL_PATTERN.test(query)) {
33264
35034
  console.error("\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u6216 aggregation pipeline\u3002");
33265
35035
  console.error(`\u7BC4\u4F8B\uFF1Adbcli export '{"status":"open"}' --collection orders --format jsonl`);
33266
35036
  process.exit(1);
@@ -33318,6 +35088,15 @@ async function mongoExportBranch(query, options, config) {
33318
35088
  if (securityNote) {
33319
35089
  console.error(`\u2139 ${securityNote}`);
33320
35090
  }
35091
+ await writeAuditEntry(config, "export", options, {
35092
+ success: true,
35093
+ target: collection,
35094
+ metadata: {
35095
+ rows_affected: filterResult.filteredRows.length,
35096
+ output_format: options.format,
35097
+ ...options.output && { output_file: options.output }
35098
+ }
35099
+ });
33321
35100
  } finally {
33322
35101
  await adapter.disconnect();
33323
35102
  }
@@ -33380,6 +35159,11 @@ import * as path4 from "path";
33380
35159
  import { homedir as homedir2 } from "os";
33381
35160
  var SKILL_SOURCE_PATH = packageAssetPath("SKILL.md");
33382
35161
  var REFERENCE_SOURCE_PATH = packageAssetPath("reference.md");
35162
+ function resolveSkillSource(lang) {
35163
+ if (lang === "zh-TW")
35164
+ return packageAssetPath("SKILL.zh-TW.md");
35165
+ return packageAssetPath("SKILL.md");
35166
+ }
33383
35167
  var SUPPORTED_PLATFORMS = [
33384
35168
  "claude",
33385
35169
  "gemini",
@@ -33390,9 +35174,11 @@ var SUPPORTED_PLATFORMS = [
33390
35174
  ];
33391
35175
  async function skillCommand(_program, options) {
33392
35176
  try {
33393
- const skillFile = Bun.file(SKILL_SOURCE_PATH);
35177
+ const lang = options.lang ?? "en";
35178
+ const skillSourcePath = resolveSkillSource(lang);
35179
+ const skillFile = Bun.file(skillSourcePath);
33394
35180
  if (!await skillFile.exists()) {
33395
- throw new Error(`Skill source not found: ${SKILL_SOURCE_PATH}`);
35181
+ throw new Error(`Skill source not found: ${skillSourcePath}`);
33396
35182
  }
33397
35183
  const refFile = Bun.file(REFERENCE_SOURCE_PATH);
33398
35184
  if (!await refFile.exists()) {
@@ -33480,15 +35266,15 @@ async function ensureDir(dirPath) {
33480
35266
  await $`mkdir -p ${dirPath}`.quiet();
33481
35267
  } catch {
33482
35268
  try {
33483
- const { mkdir: mkdir7 } = await import("fs/promises");
33484
- await mkdir7(dirPath, { recursive: true });
35269
+ const { mkdir: mkdir10 } = await import("fs/promises");
35270
+ await mkdir10(dirPath, { recursive: true });
33485
35271
  } catch {
33486
35272
  throw new Error(`Cannot create directory: ${dirPath}`);
33487
35273
  }
33488
35274
  }
33489
35275
  }
33490
35276
  function registerSkillCommand(program2) {
33491
- return program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, copilot, cursor, codex, windsurf)").option("--output <path>", "Write skill to file instead of stdout").action(async (options) => {
35277
+ return program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, copilot, cursor, codex, windsurf)").option("--output <path>", "Write skill to file instead of stdout").addOption(new Option("--lang <lang>", "Source language for SKILL content").choices(["en", "zh-TW"]).default("en")).action(async (options) => {
33492
35278
  try {
33493
35279
  await skillCommand(program2, options);
33494
35280
  } catch (error) {
@@ -33512,15 +35298,15 @@ class AgentTaskError extends Error {
33512
35298
  }
33513
35299
  // src/core/agent-tasks/task-paths.ts
33514
35300
  init_package_root();
33515
- import { join as join19 } from "path";
35301
+ import { join as join21 } from "path";
33516
35302
  function resolveBuiltinDir2() {
33517
35303
  return packageAssetPath("tasks");
33518
35304
  }
33519
35305
  function resolveAgentTaskDirs(workspaceRoot) {
33520
35306
  return {
33521
35307
  builtinDir: resolveBuiltinDir2(),
33522
- sharedDir: join19(workspaceRoot, ".dbcli-shared", "tasks"),
33523
- localDir: join19(workspaceRoot, ".dbcli", "tasks")
35308
+ sharedDir: join21(workspaceRoot, ".dbcli-shared", "tasks"),
35309
+ localDir: join21(workspaceRoot, ".dbcli", "tasks")
33524
35310
  };
33525
35311
  }
33526
35312
  // src/core/agent-tasks/parser.ts
@@ -33658,7 +35444,7 @@ function parseSteps(value, input) {
33658
35444
  }
33659
35445
  // src/core/agent-tasks/loader.ts
33660
35446
  import { readdir as readdir2 } from "fs/promises";
33661
- import { join as join20, relative as relative2, sep as sep2 } from "path";
35447
+ import { join as join22, relative as relative2, sep as sep2 } from "path";
33662
35448
  async function loadAgentTasks(opts, flags) {
33663
35449
  const errors3 = [];
33664
35450
  const builtin = await walkAndParse2(opts.builtinDir, "builtin", errors3);
@@ -33715,7 +35501,7 @@ async function safeCollectMd(root) {
33715
35501
  return;
33716
35502
  }
33717
35503
  for (const e of entries) {
33718
- const full = join20(dir, e.name);
35504
+ const full = join22(dir, e.name);
33719
35505
  if (e.isDirectory())
33720
35506
  await walk(full);
33721
35507
  else
@@ -34303,11 +36089,11 @@ function requireSqlConnection7(connection) {
34303
36089
  }
34304
36090
  return connection;
34305
36091
  }
34306
- var ALLOWED_FORMATS5 = ["json", "table"];
36092
+ var ALLOWED_FORMATS6 = ["json", "table"];
34307
36093
  var checkCommand = new Command().name("check").description("Run data health checks on tables").argument("[table]", "Table to check (omit for --all)").option("--all", "Check all tables (skips huge tables unless --include-large)", false).option("--include-large", "Include huge tables in --all scan", false).option("--checks <types>", "Comma-separated checks: nulls,duplicates,orphans,emptyStrings", undefined).option("--sample <number>", "Sample size for large tables (default: 10000)", "10000").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(checkAction);
34308
36094
  async function checkAction(table, options) {
34309
36095
  try {
34310
- validateFormat(options.format, ALLOWED_FORMATS5, "check");
36096
+ validateFormat(options.format, ALLOWED_FORMATS6, "check");
34311
36097
  const config = await configModule.read(options.config);
34312
36098
  if (!config.connection) {
34313
36099
  console.error("Database not configured. Run: dbcli init");
@@ -34440,7 +36226,7 @@ function requireSqlConnection8(connection) {
34440
36226
  }
34441
36227
  return connection;
34442
36228
  }
34443
- var ALLOWED_FORMATS6 = ["json", "table"];
36229
+ var ALLOWED_FORMATS7 = ["json", "table"];
34444
36230
  function compareSnapshots(before, after) {
34445
36231
  const beforeTables = new Set(Object.keys(before.tables));
34446
36232
  const afterTables = new Set(Object.keys(after.tables));
@@ -34534,7 +36320,7 @@ function compareSnapshots(before, after) {
34534
36320
  var diffCommand = new Command().name("diff").description("Compare schema snapshots to detect changes").option("--snapshot <path>", "Save current schema snapshot to file").option("--against <path>", "Compare current schema against a snapshot file").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(diffAction);
34535
36321
  async function diffAction(options) {
34536
36322
  try {
34537
- validateFormat(options.format, ALLOWED_FORMATS6, "diff");
36323
+ validateFormat(options.format, ALLOWED_FORMATS7, "diff");
34538
36324
  if (!options.snapshot && !options.against) {
34539
36325
  console.error("Specify --snapshot <path> to save, or --against <path> to compare");
34540
36326
  process.exit(1);
@@ -34643,10 +36429,10 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
34643
36429
  // src/commands/status.ts
34644
36430
  init_config();
34645
36431
  init_validation();
34646
- var ALLOWED_FORMATS7 = ["text", "json"];
36432
+ var ALLOWED_FORMATS8 = ["text", "json"];
34647
36433
  var statusCommand = new Command("status").description("Show current configuration status (safe for AI agents, no credentials exposed)").option("--format <type>", "Output format: text, json", "json").action(async (options) => {
34648
36434
  try {
34649
- validateFormat(options.format, ALLOWED_FORMATS7, "status");
36435
+ validateFormat(options.format, ALLOWED_FORMATS8, "status");
34650
36436
  const configPath = resolveConfigPath(statusCommand);
34651
36437
  const config = await configModule.read(configPath);
34652
36438
  if (!config.connection) {
@@ -34681,16 +36467,21 @@ var statusCommand = new Command("status").description("Show current configuratio
34681
36467
 
34682
36468
  // src/commands/inspect.ts
34683
36469
  init_message_loader();
36470
+ import crypto8 from "crypto";
34684
36471
  init_validation();
34685
36472
  init_inspect();
34686
- var ALLOWED_FORMATS8 = ["json", "markdown"];
36473
+ init_config();
36474
+ init_integration_helper();
36475
+ var ALLOWED_FORMATS9 = ["json", "markdown"];
34687
36476
  var inspectCommand = new Command().name("inspect").description(t("inspect.description")).option("--format <format>", "Output format: json (default) or markdown", "json").option("--brief", "Trim samples / intents / commands for compact output", false).option("--for-agent", "Shortcut for --format json --brief", false).option("--no-connect", "Skip the database connect + version probe").option("--probe-timeout <ms>", "Hard timeout for cheap version/object probe (default 1500)", (v) => parseInt(v, 10), 1500).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).option("--require-schema-cache", "Throw SCHEMA_CACHE_MISSING (recovery code) when the active SQL connection has no usable schema cache", false).action(async (options, command) => {
36477
+ let config;
34688
36478
  try {
34689
36479
  const forAgent = options.forAgent === true;
34690
36480
  const format = forAgent ? "json" : options.format;
34691
36481
  const brief = forAgent || options.brief === true;
34692
- validateFormat(format, ALLOWED_FORMATS8, "inspect");
36482
+ validateFormat(format, ALLOWED_FORMATS9, "inspect");
34693
36483
  const configPath = resolveConfigPath(command, options);
36484
+ config = await configModule.read(configPath);
34694
36485
  const snap = await collectInspect({
34695
36486
  workspace: process.cwd(),
34696
36487
  configPath,
@@ -34702,12 +36493,40 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
34702
36493
  const { requireSchemaCacheOrThrow: requireSchemaCacheOrThrow2 } = await Promise.resolve().then(() => (init_inspect(), exports_inspect));
34703
36494
  requireSchemaCacheOrThrow2(snap.schemaCache, snap.system ?? null);
34704
36495
  }
36496
+ if (config) {
36497
+ const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
36498
+ if (shouldEmbedRecent2({ forAgent, format })) {
36499
+ snap.audit_recent = await loadRecentAudit2(config, configPath);
36500
+ }
36501
+ }
34705
36502
  const out = format === "markdown" ? renderMarkdown2(snap, { brief }) : renderJson2(snap, { brief });
34706
36503
  console.log(out);
36504
+ if (config) {
36505
+ await writeAuditEntry(config, "inspect", options, {
36506
+ success: true,
36507
+ target: "*"
36508
+ });
36509
+ }
34707
36510
  } catch (err) {
36511
+ let auditId = null;
36512
+ let envelopeId;
34708
36513
  if (options.recovery === true) {
36514
+ envelopeId = crypto8.randomUUID();
36515
+ }
36516
+ if (config) {
36517
+ auditId = await writeAuditEntry(config, "inspect", options, {
36518
+ success: false,
36519
+ target: "*",
36520
+ error: err,
36521
+ ...envelopeId && { recovery_ref: envelopeId }
36522
+ });
36523
+ }
36524
+ if (envelopeId !== undefined) {
34709
36525
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
34710
- emitRecoveryEnvelope2(err, { operation: "inspect" });
36526
+ emitRecoveryEnvelope2(err, { operation: "inspect" }, {
36527
+ envelopeId,
36528
+ auditRef: auditId ?? undefined
36529
+ });
34711
36530
  }
34712
36531
  console.error(err.message);
34713
36532
  process.exit(1);
@@ -34717,6 +36536,8 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
34717
36536
  // src/commands/report.ts
34718
36537
  init_message_loader();
34719
36538
  init_validation();
36539
+ init_config();
36540
+ init_integration_helper();
34720
36541
 
34721
36542
  // src/core/report/types.ts
34722
36543
  var REPORT_SCHEMA_VERSION = 1;
@@ -34971,10 +36792,10 @@ function finalize(input) {
34971
36792
  context: input.context,
34972
36793
  sections: input.sections,
34973
36794
  warnings: input.warnings,
34974
- suggestedCommands: buildSuggestedCommands2(input.context, input.sections)
36795
+ suggestedCommands: buildSuggestedCommands5(input.context, input.sections)
34975
36796
  };
34976
36797
  }
34977
- function buildSuggestedCommands2(context, sections) {
36798
+ function buildSuggestedCommands5(context, sections) {
34978
36799
  if (!context.system)
34979
36800
  return ["dbcli init"];
34980
36801
  const out = ["dbcli inspect --format json"];
@@ -35092,7 +36913,7 @@ function formatCell(value) {
35092
36913
  return String(value);
35093
36914
  }
35094
36915
  // src/commands/report.ts
35095
- var ALLOWED_FORMATS9 = ["json", "markdown"];
36916
+ var ALLOWED_FORMATS10 = ["json", "markdown"];
35096
36917
  function parseSections(value) {
35097
36918
  const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
35098
36919
  for (const p of parts) {
@@ -35103,12 +36924,14 @@ function parseSections(value) {
35103
36924
  return parts;
35104
36925
  }
35105
36926
  var reportCommand = new Command().name("report").description(t("report.description")).option("--format <format>", "Output format: json (default) or markdown", "json").option("--section <list>", `Comma-separated subset of [${ALLOWED_SECTIONS.join(", ")}] (default: all)`, parseSections).option("--brief", "Trim evidence rows for compact output", false).option("--for-agent", "Shortcut for --format json --brief", false).option("--no-connect", "Emit context-only snapshot (skip diagnostics)").option("--per-snippet-timeout <ms>", "Hard timeout per diagnostic snippet (default 3000)", (v) => parseInt(v, 10), 3000).option("--max-rows-per-evidence <n>", "Cap rows kept per evidence (default 50)", (v) => parseInt(v, 10), 50).option("--probe-timeout <ms>", "Inspect probe timeout for cheap version/object check (default 1500)", (v) => parseInt(v, 10), 1500).action(async (options, command) => {
36927
+ let config;
35106
36928
  try {
35107
36929
  const forAgent = options.forAgent === true;
35108
36930
  const format = forAgent ? "json" : options.format;
35109
36931
  const brief = forAgent || options.brief === true;
35110
- validateFormat(format, ALLOWED_FORMATS9, "report");
36932
+ validateFormat(format, ALLOWED_FORMATS10, "report");
35111
36933
  const configPath = resolveConfigPath(command, options);
36934
+ config = await configModule.read(configPath);
35112
36935
  const snap = await collectReport({
35113
36936
  workspace: process.cwd(),
35114
36937
  configPath,
@@ -35121,7 +36944,23 @@ var reportCommand = new Command().name("report").description(t("report.descripti
35121
36944
  });
35122
36945
  const out = format === "markdown" ? renderMarkdown3(snap, { brief }) : renderJson3(snap, { brief });
35123
36946
  console.log(out);
36947
+ if (config) {
36948
+ await writeAuditEntry(config, "report", options, {
36949
+ success: true,
36950
+ target: "*",
36951
+ metadata: {
36952
+ sections: options.section || ALLOWED_SECTIONS
36953
+ }
36954
+ });
36955
+ }
35124
36956
  } catch (err) {
36957
+ if (config) {
36958
+ await writeAuditEntry(config, "report", options, {
36959
+ success: false,
36960
+ target: "*",
36961
+ error: err
36962
+ });
36963
+ }
35125
36964
  console.error(err.message);
35126
36965
  process.exit(1);
35127
36966
  }
@@ -35130,6 +36969,8 @@ var reportCommand = new Command().name("report").description(t("report.descripti
35130
36969
  // src/commands/guide.ts
35131
36970
  init_message_loader();
35132
36971
  init_validation();
36972
+ init_config();
36973
+ init_integration_helper();
35133
36974
 
35134
36975
  // src/core/guide/types.ts
35135
36976
  var GUIDE_SCHEMA_VERSION = 1;
@@ -35471,7 +37312,7 @@ function renderGoalList() {
35471
37312
  `);
35472
37313
  }
35473
37314
  // src/commands/guide.ts
35474
- var ALLOWED_FORMATS10 = ["json", "markdown"];
37315
+ var ALLOWED_FORMATS11 = ["json", "markdown"];
35475
37316
  function parseGoal(value) {
35476
37317
  const normalized = value.trim();
35477
37318
  if (!ALLOWED_GOALS.includes(normalized)) {
@@ -35480,11 +37321,12 @@ function parseGoal(value) {
35480
37321
  return normalized;
35481
37322
  }
35482
37323
  var guideCommand = new Command().name("guide").description(t("guide.description")).argument("[goal]", `Guide goal ID; one of [${ALLOWED_GOALS.join(", ")}]`).option("--format <format>", "Output format: json (default) or markdown", "json").option("--brief", "Trim rationale/expects for compact output", false).option("--for-agent", "Shortcut for --format json --brief", false).option("--list", "List available guide goals and exit", false).option("--probe", "Refresh inspect context via live probe (default: cache-first)", false).option("--probe-timeout <ms>", "Inspect probe timeout for cheap version/object check (default 1500)", (v) => parseInt(v, 10), 1500).action(async (goal, options, command) => {
37324
+ let config;
35483
37325
  try {
35484
37326
  const forAgent = options.forAgent === true;
35485
37327
  const format = forAgent ? "json" : options.format;
35486
37328
  const brief = forAgent || options.brief === true;
35487
- validateFormat(format, ALLOWED_FORMATS10, "guide");
37329
+ validateFormat(format, ALLOWED_FORMATS11, "guide");
35488
37330
  if (options.list === true) {
35489
37331
  if (format === "markdown") {
35490
37332
  console.log(renderGoalList());
@@ -35503,6 +37345,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
35503
37345
  }
35504
37346
  const validated = parseGoal(goal);
35505
37347
  const configPath = resolveConfigPath(command, options);
37348
+ config = await configModule.read(configPath);
35506
37349
  const snap = await collectGuide({
35507
37350
  workspace: process.cwd(),
35508
37351
  configPath,
@@ -35511,9 +37354,28 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
35511
37354
  brief,
35512
37355
  probeTimeoutMs: options.probeTimeout
35513
37356
  });
37357
+ if (config) {
37358
+ const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
37359
+ if (shouldEmbedRecent2({ forAgent, format })) {
37360
+ snap.audit_recent = await loadRecentAudit2(config, configPath);
37361
+ }
37362
+ }
35514
37363
  const out = format === "markdown" ? renderMarkdown4(snap, { brief }) : renderJson4(snap, { brief });
35515
37364
  console.log(out);
37365
+ if (config) {
37366
+ await writeAuditEntry(config, "guide", options, {
37367
+ success: true,
37368
+ target: goal
37369
+ });
37370
+ }
35516
37371
  } catch (err) {
37372
+ if (config) {
37373
+ await writeAuditEntry(config, "guide", options, {
37374
+ success: false,
37375
+ target: goal || "*",
37376
+ error: err
37377
+ });
37378
+ }
35517
37379
  console.error(err.message);
35518
37380
  process.exit(1);
35519
37381
  }
@@ -35523,7 +37385,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
35523
37385
  init_message_loader();
35524
37386
  init_validation();
35525
37387
  init_recovery();
35526
- var ALLOWED_FORMATS11 = ["json", "markdown"];
37388
+ var ALLOWED_FORMATS12 = ["json", "markdown"];
35527
37389
  function parseCode(value) {
35528
37390
  const normalized = value.trim();
35529
37391
  if (!RECOVERY_CODES.includes(normalized)) {
@@ -35559,7 +37421,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
35559
37421
  const forAgent = options.forAgent === true;
35560
37422
  const format = forAgent ? "json" : options.format;
35561
37423
  const brief = forAgent || options.brief === true;
35562
- validateFormat(format, ALLOWED_FORMATS11, "recovery");
37424
+ validateFormat(format, ALLOWED_FORMATS12, "recovery");
35563
37425
  if (options.list === true) {
35564
37426
  if (format === "markdown") {
35565
37427
  console.log(renderCodeList());
@@ -35600,7 +37462,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
35600
37462
  // src/commands/recover.ts
35601
37463
  init_recovery();
35602
37464
  init_last_envelope();
35603
- import { stat as stat4 } from "fs/promises";
37465
+ import { stat as stat6 } from "fs/promises";
35604
37466
  import { resolve as resolve4 } from "path";
35605
37467
 
35606
37468
  // src/core/recovery/envelope-schema.ts
@@ -35652,6 +37514,8 @@ var recoveryEnvelopeSchema = exports_external.object({
35652
37514
  }).strict();
35653
37515
  var savedRecoveryEnvelopeSchema = exports_external.object({
35654
37516
  schemaVersion: exports_external.literal(1),
37517
+ id: exports_external.string().optional(),
37518
+ audit_ref: exports_external.string().optional(),
35655
37519
  savedAt: exports_external.string().min(1),
35656
37520
  command: exports_external.string(),
35657
37521
  cwd: exports_external.string().min(1),
@@ -35682,7 +37546,7 @@ function looksLikeSavedEnvelope(x) {
35682
37546
  // src/commands/recover.ts
35683
37547
  init_next_step_schema();
35684
37548
  init_validation();
35685
- var ALLOWED_FORMATS12 = ["json", "markdown"];
37549
+ var ALLOWED_FORMATS13 = ["json", "markdown"];
35686
37550
  var ALLOWED_TIERS = ["readonly-cmd", "write-cmd"];
35687
37551
 
35688
37552
  class RecoverCliError extends Error {
@@ -35716,7 +37580,7 @@ async function resolveApplySource(opts) {
35716
37580
  }
35717
37581
  const saved2 = r3.value;
35718
37582
  try {
35719
- await stat4(saved2.cwd);
37583
+ await stat6(saved2.cwd);
35720
37584
  } catch {
35721
37585
  throw new RecoverCliError(`--from ${opts.from}: saved cwd '${saved2.cwd}' no longer exists.`, EXIT_CODE.malformed);
35722
37586
  }
@@ -35750,7 +37614,7 @@ async function resolveApplySource(opts) {
35750
37614
  }
35751
37615
  const saved = r.value;
35752
37616
  try {
35753
- await stat4(saved.cwd);
37617
+ await stat6(saved.cwd);
35754
37618
  } catch {
35755
37619
  throw new RecoverCliError(`Auto-saved ${LAST_ENVELOPE_PATH} references cwd '${saved.cwd}' that no longer exists.`, EXIT_CODE.malformed);
35756
37620
  }
@@ -35824,7 +37688,7 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35824
37688
  }
35825
37689
  const explicitFormat = options.format;
35826
37690
  const format = explicitFormat ?? (options.apply === true || options.next === true ? "json" : "markdown");
35827
- validateFormat(format, ALLOWED_FORMATS12, "recover");
37691
+ validateFormat(format, ALLOWED_FORMATS13, "recover");
35828
37692
  let allowWrite = "none";
35829
37693
  if (options.next !== true) {
35830
37694
  const allowWriteRaw = options.allowWrite;
@@ -35839,15 +37703,32 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35839
37703
  from: options.from,
35840
37704
  cwd: process.cwd()
35841
37705
  });
37706
+ let audit_recent = [];
37707
+ if (format === "json" && options.next !== true) {
37708
+ try {
37709
+ const { configModule: configModule2 } = await Promise.resolve().then(() => (init_config(), exports_config));
37710
+ const configPath = `${process.cwd()}/.dbcli`;
37711
+ const config = await configModule2.read(configPath);
37712
+ if (config) {
37713
+ const { loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
37714
+ audit_recent = await loadRecentAudit2(config, configPath);
37715
+ }
37716
+ } catch {
37717
+ audit_recent = [];
37718
+ }
37719
+ }
35842
37720
  if (options.next === true) {
35843
37721
  const result2 = await runNext(options, source);
35844
- const out2 = format === "markdown" ? renderNextMarkdown(result2) : renderNextJson(result2);
35845
- console.log(out2);
37722
+ const out = format === "markdown" ? renderNextMarkdown(result2) : renderNextJson(result2);
37723
+ console.log(out);
35846
37724
  return;
35847
37725
  }
35848
37726
  if (options.apply !== true) {
35849
- const out2 = format === "markdown" ? renderMarkdown(source.envelope) : renderJson(source.envelope);
35850
- console.log(out2);
37727
+ if (format === "markdown") {
37728
+ console.log(renderMarkdown(source.envelope));
37729
+ } else {
37730
+ console.log(JSON.stringify({ ...source.envelope, audit_recent }, null, 2));
37731
+ }
35851
37732
  return;
35852
37733
  }
35853
37734
  const noVerify = options.verify === false;
@@ -35856,8 +37737,11 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35856
37737
  cwd: source.cwd,
35857
37738
  source: { kind: source.kind, path: source.path }
35858
37739
  }, { allowWrite, noVerify });
35859
- const out = format === "markdown" ? renderApplyMarkdown(result) : renderApplyJson(result);
35860
- console.log(out);
37740
+ if (format === "markdown") {
37741
+ console.log(renderApplyMarkdown(result));
37742
+ } else {
37743
+ console.log(JSON.stringify({ ...result, audit_recent }, null, 2));
37744
+ }
35861
37745
  process.exit(exitCodeFor(result.finalStatus));
35862
37746
  } catch (err) {
35863
37747
  if (err instanceof RecoverCliError) {
@@ -35869,6 +37753,440 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35869
37753
  }
35870
37754
  });
35871
37755
 
37756
+ // src/commands/audit.ts
37757
+ import { rm as rm3, stat as stat7 } from "fs/promises";
37758
+ import { join as join25 } from "path";
37759
+ init_message_loader();
37760
+ init_validation();
37761
+ init_config();
37762
+ init_config_binding();
37763
+ init_reader();
37764
+ init_integration_helper();
37765
+ var ALLOWED_FORMATS14 = ["table", "json"];
37766
+ var DEFAULT_TAIL_N = 10;
37767
+ var MAX_TAIL_N = 1e4;
37768
+ var SHORT_ID_LEN = 8;
37769
+ var MISSING_PLACEHOLDER = "\u2014";
37770
+ var PREFIX_MIN = 4;
37771
+ async function resolveAuditPaths(configPath, config) {
37772
+ const storagePath = await resolveConfigStoragePath(configPath);
37773
+ const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
37774
+ const auditDir = join25(storagePath, ".dbcli", "audit");
37775
+ const auditFile = join25(auditDir, `${connName}.jsonl`);
37776
+ return { auditDir, connectionName: connName, auditFile };
37777
+ }
37778
+ function isAuditDisabled(config) {
37779
+ return config?.audit?.enabled === false;
37780
+ }
37781
+ function emitDisabledAndExit0() {
37782
+ console.error(t("audit.disabled_hint"));
37783
+ process.exit(0);
37784
+ }
37785
+ function parseTailN(raw) {
37786
+ const requested = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
37787
+ if (!Number.isFinite(requested) || !Number.isInteger(requested) || requested <= 0) {
37788
+ console.error(t("audit.n_must_be_positive"));
37789
+ process.exit(1);
37790
+ }
37791
+ if (requested > MAX_TAIL_N) {
37792
+ console.error(t_vars("audit.n_capped_warning", {
37793
+ requested: String(requested),
37794
+ max: String(MAX_TAIL_N)
37795
+ }));
37796
+ return MAX_TAIL_N;
37797
+ }
37798
+ return requested;
37799
+ }
37800
+ function briefify(entry) {
37801
+ return {
37802
+ ts: entry.ts,
37803
+ command: entry.command,
37804
+ target: entry.target,
37805
+ success: entry.success
37806
+ };
37807
+ }
37808
+ function shortId(id) {
37809
+ if (!id)
37810
+ return MISSING_PLACEHOLDER;
37811
+ return id.length <= SHORT_ID_LEN ? id : id.slice(0, SHORT_ID_LEN);
37812
+ }
37813
+ function renderTable(rows, headers) {
37814
+ const allRows = [headers, ...rows];
37815
+ const widths = headers.map((_, col) => Math.max(...allRows.map((r) => (r[col] ?? "").length)));
37816
+ const fmt = (r) => r.map((cell, i) => (cell ?? "").padEnd(widths[i] ?? 0)).join(" ").trimEnd();
37817
+ const sep3 = widths.map((w) => "-".repeat(w)).join(" ").trimEnd();
37818
+ return [fmt(headers), sep3, ...rows.map(fmt)].join(`
37819
+ `);
37820
+ }
37821
+ function renderTailTable(entries) {
37822
+ const headers = ["ts", "command", "target", "tier", "success", "id", "recovery_ref"];
37823
+ const rows = entries.map((e) => [
37824
+ e.ts,
37825
+ e.command,
37826
+ e.target,
37827
+ e.side_effect_tier,
37828
+ String(e.success),
37829
+ shortId(e.id),
37830
+ shortId(e.recovery_ref)
37831
+ ]);
37832
+ return renderTable(rows, headers);
37833
+ }
37834
+ function renderTailAllTable(envelopes) {
37835
+ const headers = ["connection", "ts", "command", "target", "tier", "success", "id", "recovery_ref"];
37836
+ const rows = envelopes.map(({ connection, entry: e }) => [
37837
+ connection,
37838
+ e.ts,
37839
+ e.command,
37840
+ e.target,
37841
+ e.side_effect_tier,
37842
+ String(e.success),
37843
+ shortId(e.id),
37844
+ shortId(e.recovery_ref)
37845
+ ]);
37846
+ return renderTable(rows, headers);
37847
+ }
37848
+ function briefifyShow(entry) {
37849
+ const { metadata, redacted_query, ...rest } = entry;
37850
+ return rest;
37851
+ }
37852
+ function renderEntryTable(entry) {
37853
+ const lines = [];
37854
+ if (entry.id !== undefined)
37855
+ lines.push(`Id: ${entry.id}`);
37856
+ if (entry.ts !== undefined)
37857
+ lines.push(`Ts: ${entry.ts}`);
37858
+ if (entry.session_id !== undefined)
37859
+ lines.push(`Session id: ${entry.session_id}`);
37860
+ if (entry.engine !== undefined)
37861
+ lines.push(`Engine: ${entry.engine}`);
37862
+ if (entry.command !== undefined)
37863
+ lines.push(`Command: ${entry.command}`);
37864
+ if (entry.side_effect_tier !== undefined)
37865
+ lines.push(`Side effect tier: ${entry.side_effect_tier}`);
37866
+ if (entry.target !== undefined)
37867
+ lines.push(`Target: ${entry.target}`);
37868
+ if (entry.success !== undefined)
37869
+ lines.push(`Success: ${entry.success}`);
37870
+ if (entry.recovery_ref !== undefined)
37871
+ lines.push(`Recovery ref: ${entry.recovery_ref}`);
37872
+ if (entry.redacted_query !== undefined)
37873
+ lines.push(`Redacted query: ${entry.redacted_query}`);
37874
+ if (entry.redacted_sql !== undefined)
37875
+ lines.push(`Redacted SQL: ${entry.redacted_sql}`);
37876
+ if (entry.error !== undefined)
37877
+ lines.push(`Error: ${entry.error}`);
37878
+ if (entry.metadata !== undefined)
37879
+ lines.push(`Metadata: ${JSON.stringify(entry.metadata)}`);
37880
+ return lines.join(`
37881
+ `);
37882
+ }
37883
+ function renderShowResult(hit, opts) {
37884
+ if (opts.format === "json") {
37885
+ const entryView = opts.brief ? briefifyShow(hit.entry) : hit.entry;
37886
+ const payload = opts.all ? { connection: hit.connection, entry: entryView } : entryView;
37887
+ console.log(JSON.stringify(payload, null, 2));
37888
+ } else {
37889
+ if (opts.all)
37890
+ console.log(`Connection: ${hit.connection}`);
37891
+ console.log(renderEntryTable(opts.brief ? briefifyShow(hit.entry) : hit.entry));
37892
+ }
37893
+ }
37894
+ function briefifyHealth(h) {
37895
+ return { enabled: h.enabled, lastWrite: h.lastWrite, rotationUsage: h.rotationUsage };
37896
+ }
37897
+ function formatBytes(n) {
37898
+ if (n < 1024)
37899
+ return `${n} B`;
37900
+ if (n < 1024 * 1024)
37901
+ return `${(n / 1024).toFixed(1)} KB`;
37902
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
37903
+ }
37904
+ function renderHealthTable(h) {
37905
+ const sizePct = Math.round(h.rotationUsage.bytes.pct);
37906
+ const entriesPct = Math.round(h.rotationUsage.entries.pct);
37907
+ const lastWrite = h.lastWrite ? `${h.lastWrite.ts} (${h.lastWrite.success ? "success" : "failed"}${h.lastWrite.error ? `: ${h.lastWrite.error}` : ""})` : MISSING_PLACEHOLDER;
37908
+ const lastError = h.lastError ? `${h.lastError.ts} ${h.lastError.message}` : MISSING_PLACEHOLDER;
37909
+ const lastRotation = h.rotation.lastRotatedAt ? `${h.rotation.lastRotatedAt} (${h.rotation.previousFile ?? MISSING_PLACEHOLDER})` : MISSING_PLACEHOLDER;
37910
+ return [
37911
+ `Enabled: ${h.enabled}`,
37912
+ `File: ${h.currentFile}`,
37913
+ `Size: ${formatBytes(h.currentSizeBytes)} / ${formatBytes(h.rotationUsage.bytes.max)} (${sizePct}%)`,
37914
+ `Entries: ${h.currentEntryCount} / ${h.rotationUsage.entries.max} (${entriesPct}%)`,
37915
+ `Lock: ${h.lock.state}${h.lock.heldByPid !== undefined ? ` (pid ${h.lock.heldByPid})` : ""}`,
37916
+ `Last write: ${lastWrite}`,
37917
+ `Last error: ${lastError}`,
37918
+ `Session id: ${h.sessionId ?? MISSING_PLACEHOLDER}`,
37919
+ `Last rotation: ${lastRotation}`
37920
+ ].join(`
37921
+ `);
37922
+ }
37923
+ function readLineFromStdinWithStderrPrompt(prompt) {
37924
+ return new Promise((resolve5) => {
37925
+ process.stderr.write(prompt);
37926
+ const chunks = [];
37927
+ let data = "";
37928
+ const onData = (chunk) => {
37929
+ chunks.push(chunk);
37930
+ data = Buffer.concat(chunks).toString();
37931
+ const lines = data.split(`
37932
+ `);
37933
+ if (lines.length > 1) {
37934
+ process.stdin.pause();
37935
+ process.stdin.removeListener("data", onData);
37936
+ process.stdin.removeListener("end", onEnd);
37937
+ resolve5((lines[0] ?? "").trim());
37938
+ }
37939
+ };
37940
+ const onEnd = () => {
37941
+ process.stdin.removeListener("data", onData);
37942
+ resolve5(data.trim());
37943
+ };
37944
+ process.stdin.on("data", onData);
37945
+ process.stdin.on("end", onEnd);
37946
+ process.stdin.resume();
37947
+ });
37948
+ }
37949
+ async function statAuditFile(file) {
37950
+ try {
37951
+ const s = await stat7(file);
37952
+ const entries = (await readEntries(file)).length;
37953
+ return { entries, size: formatBytes(s.size) };
37954
+ } catch {
37955
+ return null;
37956
+ }
37957
+ }
37958
+ var auditCommand = new Command("audit").description(t("audit.description"));
37959
+ auditCommand.command("tail").description(t("audit.tail.description")).option("--n <number>", `Number of recent entries to show (1..${MAX_TAIL_N})`, String(DEFAULT_TAIL_N)).option("--all", "Merge entries across all connections", false).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim each entry to ts/command/target/success", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
37960
+ const forAgent = options.forAgent === true;
37961
+ const format = forAgent ? "json" : options.format;
37962
+ const briefSource = command.getOptionValueSource("brief");
37963
+ const briefExplicit = briefSource !== undefined && briefSource !== "default";
37964
+ const brief = briefExplicit ? options.brief === true : forAgent;
37965
+ validateFormat(format, ALLOWED_FORMATS14, "audit tail");
37966
+ const n = parseTailN(options.n);
37967
+ const configPath = resolveConfigPath(command, options);
37968
+ const config = await configModule.read(configPath);
37969
+ if (isAuditDisabled(config))
37970
+ emitDisabledAndExit0();
37971
+ const { auditDir, auditFile } = await resolveAuditPaths(configPath, config);
37972
+ if (options.all === true) {
37973
+ const conns = await discoverConnections(auditDir);
37974
+ const byConn = new Map;
37975
+ for (const c of conns) {
37976
+ const merged = [];
37977
+ for (const f of c.files) {
37978
+ const part = await readEntries(f);
37979
+ merged.push(...part);
37980
+ }
37981
+ byConn.set(c.connection, merged);
37982
+ }
37983
+ const envelopes = mergeByTimestamp(byConn).slice(-n);
37984
+ if (format === "json") {
37985
+ const payload = envelopes.map((e) => ({
37986
+ connection: e.connection,
37987
+ entry: brief ? briefify(e.entry) : e.entry
37988
+ }));
37989
+ console.log(JSON.stringify(payload, null, 2));
37990
+ } else if (envelopes.length === 0) {
37991
+ console.error(t("audit.no_entries"));
37992
+ } else {
37993
+ console.log(renderTailAllTable(envelopes));
37994
+ }
37995
+ return;
37996
+ }
37997
+ const entries = await readEntries(auditFile, { include_rotated: true });
37998
+ const tail = tailEntries(entries, n);
37999
+ if (format === "json") {
38000
+ const payload = brief ? tail.map(briefify) : tail;
38001
+ console.log(JSON.stringify(payload, null, 2));
38002
+ } else if (tail.length === 0) {
38003
+ console.error(t("audit.no_entries"));
38004
+ } else {
38005
+ console.log(renderTailTable(tail));
38006
+ }
38007
+ });
38008
+ auditCommand.command("show [id]").description(t("audit.show.description")).option("--all", "Search across all connections", false).option("--recovery-ref <ref>", "Look up by entry.recovery_ref (exact match)").option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim metadata + redacted_query", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (id, options, command) => {
38009
+ if (id && options.recoveryRef) {
38010
+ console.error(t("audit.show_mutex_violation"));
38011
+ process.exit(1);
38012
+ }
38013
+ if (!id && !options.recoveryRef) {
38014
+ console.error(t("audit.show_mutex_violation"));
38015
+ process.exit(1);
38016
+ }
38017
+ const forAgent = options.forAgent === true;
38018
+ const format = forAgent ? "json" : options.format;
38019
+ const briefSource = command.getOptionValueSource("brief");
38020
+ const briefExplicit = briefSource !== undefined && briefSource !== "default";
38021
+ const brief = briefExplicit ? options.brief === true : forAgent;
38022
+ validateFormat(format, ALLOWED_FORMATS14, "audit show");
38023
+ const configPath = resolveConfigPath(command, options);
38024
+ const config = await configModule.read(configPath);
38025
+ if (isAuditDisabled(config))
38026
+ emitDisabledAndExit0();
38027
+ const { auditDir, auditFile, connectionName } = await resolveAuditPaths(configPath, config);
38028
+ const all = options.all === true;
38029
+ if (options.recoveryRef) {
38030
+ const ref = String(options.recoveryRef);
38031
+ const matches2 = [];
38032
+ if (all) {
38033
+ const conns = await discoverConnections(auditDir);
38034
+ for (const c of conns) {
38035
+ for (const f of c.files) {
38036
+ for (const e of await readEntries(f)) {
38037
+ if (e.recovery_ref === ref)
38038
+ matches2.push({ connection: c.connection, entry: e });
38039
+ }
38040
+ }
38041
+ }
38042
+ } else {
38043
+ for (const e of await readEntries(auditFile, { include_rotated: true })) {
38044
+ if (e.recovery_ref === ref)
38045
+ matches2.push({ connection: connectionName, entry: e });
38046
+ }
38047
+ }
38048
+ if (matches2.length === 0) {
38049
+ console.error(t_vars("audit.show_recovery_no_match", { ref }));
38050
+ process.exit(1);
38051
+ }
38052
+ if (matches2.length > 1) {
38053
+ console.error(t_vars("audit.show_recovery_ambiguous", {
38054
+ ref,
38055
+ count: String(matches2.length)
38056
+ }));
38057
+ process.exit(1);
38058
+ }
38059
+ renderShowResult(matches2[0], { all, format, brief });
38060
+ return;
38061
+ }
38062
+ const lookup = String(id);
38063
+ if (lookup.length < PREFIX_MIN) {
38064
+ console.error(t("audit.show_prefix_too_short"));
38065
+ process.exit(1);
38066
+ }
38067
+ const matches = [];
38068
+ if (all) {
38069
+ const conns = await discoverConnections(auditDir);
38070
+ for (const c of conns) {
38071
+ for (const f of c.files) {
38072
+ for (const e of await readEntries(f)) {
38073
+ if (e.id === lookup || e.id.startsWith(lookup))
38074
+ matches.push({ connection: c.connection, entry: e });
38075
+ }
38076
+ }
38077
+ }
38078
+ } else {
38079
+ for (const e of await readEntries(auditFile, { include_rotated: true })) {
38080
+ if (e.id === lookup || e.id.startsWith(lookup))
38081
+ matches.push({ connection: connectionName, entry: e });
38082
+ }
38083
+ }
38084
+ if (matches.length === 0) {
38085
+ console.error(t_vars("audit.show_no_match", { prefix: lookup }));
38086
+ process.exit(1);
38087
+ }
38088
+ if (matches.length > 1) {
38089
+ console.error(t_vars("audit.show_ambiguous", {
38090
+ prefix: lookup,
38091
+ count: String(matches.length)
38092
+ }));
38093
+ process.exit(1);
38094
+ }
38095
+ renderShowResult(matches[0], { all, format, brief });
38096
+ });
38097
+ auditCommand.command("clear").description(t("audit.clear.description")).option("--yes", "Skip confirmation prompt and delete immediately", false).action(async (options, command) => {
38098
+ const configPath = resolveConfigPath(command, options);
38099
+ const config = await configModule.read(configPath);
38100
+ const { auditFile, connectionName } = await resolveAuditPaths(configPath, config);
38101
+ const rotatedFile = `${auditFile}.1`;
38102
+ const lockFile = `${auditFile}.lock`;
38103
+ const currentInfo = await statAuditFile(auditFile);
38104
+ const rotatedInfo = await statAuditFile(rotatedFile);
38105
+ if (currentInfo === null && rotatedInfo === null) {
38106
+ process.stderr.write(t("audit.clear.summary_nothing") + `
38107
+ `);
38108
+ process.exit(0);
38109
+ }
38110
+ if (options.yes !== true) {
38111
+ if (!process.stdin.isTTY) {
38112
+ console.error(t("audit.clear.requires_tty_or_yes"));
38113
+ process.exit(1);
38114
+ }
38115
+ process.stderr.write(t_vars("audit.clear.prompt_header", { conn: connectionName }) + `
38116
+ `);
38117
+ if (currentInfo) {
38118
+ process.stderr.write(t_vars("audit.clear.prompt_file_line", {
38119
+ file: auditFile,
38120
+ entries: String(currentInfo.entries),
38121
+ size: currentInfo.size
38122
+ }) + `
38123
+ `);
38124
+ }
38125
+ if (rotatedInfo) {
38126
+ process.stderr.write(t_vars("audit.clear.prompt_file_line", {
38127
+ file: rotatedFile,
38128
+ entries: String(rotatedInfo.entries),
38129
+ size: rotatedInfo.size
38130
+ }) + `
38131
+ `);
38132
+ }
38133
+ const answer = await readLineFromStdinWithStderrPrompt(t("audit.clear.prompt_continue"));
38134
+ const lower = answer.toLowerCase();
38135
+ const proceed = lower === "y" || lower === "yes";
38136
+ if (!proceed) {
38137
+ process.stderr.write(t("audit.clear.summary_nothing") + `
38138
+ `);
38139
+ process.exit(0);
38140
+ }
38141
+ }
38142
+ let cleared = 0;
38143
+ try {
38144
+ if (currentInfo)
38145
+ cleared += currentInfo.entries;
38146
+ if (rotatedInfo)
38147
+ cleared += rotatedInfo.entries;
38148
+ await rm3(auditFile, { force: true });
38149
+ await rm3(rotatedFile, { force: true });
38150
+ await rm3(lockFile, { force: true });
38151
+ } catch (err) {
38152
+ const msg = err instanceof Error ? err.message : String(err);
38153
+ process.stderr.write(t_vars("audit.clear.summary_failed", { message: msg }) + `
38154
+ `);
38155
+ process.exit(1);
38156
+ }
38157
+ process.stderr.write(t_vars("audit.clear.summary_cleared", {
38158
+ count: String(cleared),
38159
+ conn: connectionName
38160
+ }) + `
38161
+ `);
38162
+ process.exit(0);
38163
+ });
38164
+ auditCommand.command("health").description(t("audit.health.description")).option("--format <format>", `Output format: ${ALLOWED_FORMATS14.join(" | ")} (default: table)`, "table").option("--brief", "Trim to enabled / lastWrite / rotationUsage", false).option("--no-brief", "Disable brief mode (override --for-agent default)").option("--for-agent", "Shortcut for --format json --brief", false).action(async (options, command) => {
38165
+ const forAgent = options.forAgent === true;
38166
+ const format = forAgent ? "json" : options.format;
38167
+ const briefSource = command.getOptionValueSource("brief");
38168
+ const briefExplicit = briefSource !== undefined && briefSource !== "default";
38169
+ const brief = briefExplicit ? options.brief === true : forAgent;
38170
+ validateFormat(format, ALLOWED_FORMATS14, "audit health");
38171
+ const configPath = resolveConfigPath(command, options);
38172
+ const config = await configModule.read(configPath);
38173
+ const logger = await getAuditLogger(config, configPath);
38174
+ const health = logger.getHealth();
38175
+ if (format === "json") {
38176
+ const payload = brief ? briefifyHealth(health) : health;
38177
+ console.log(JSON.stringify(payload, null, 2));
38178
+ } else if (brief) {
38179
+ const sizePct = Math.round(health.rotationUsage.bytes.pct);
38180
+ const entriesPct = Math.round(health.rotationUsage.entries.pct);
38181
+ const lastWriteLine = health.lastWrite ? `${health.lastWrite.ts} (${health.lastWrite.success ? "success" : "failed"})` : MISSING_PLACEHOLDER;
38182
+ console.log(`Enabled: ${health.enabled}`);
38183
+ console.log(`Last write: ${lastWriteLine}`);
38184
+ console.log(`Rotation usage: ${sizePct}% bytes, ${entriesPct}% entries`);
38185
+ } else {
38186
+ console.log(renderHealthTable(health));
38187
+ }
38188
+ });
38189
+
35872
38190
  // src/commands/doctor.ts
35873
38191
  init_colors();
35874
38192
  init_config();
@@ -35880,7 +38198,8 @@ init_config_v2();
35880
38198
  init_config_binding();
35881
38199
  init_schema_path();
35882
38200
  init_config();
35883
- import { join as join23 } from "path";
38201
+ init_integration_helper();
38202
+ import { join as join26 } from "path";
35884
38203
  import { resolveSrv as resolveSrv2 } from "dns/promises";
35885
38204
  function requireSqlConnection9(connection) {
35886
38205
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
@@ -35888,7 +38207,7 @@ function requireSqlConnection9(connection) {
35888
38207
  }
35889
38208
  return connection;
35890
38209
  }
35891
- var ALLOWED_FORMATS13 = ["text", "json"];
38210
+ var ALLOWED_FORMATS15 = ["text", "json"];
35892
38211
  var SENSITIVE_PATTERNS = [
35893
38212
  "password",
35894
38213
  "passwd",
@@ -35960,7 +38279,7 @@ var runDoctorChecks = {
35960
38279
  }
35961
38280
  },
35962
38281
  async checkConfigExists(configPath, existsFn) {
35963
- const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join23(configPath, "config.json")).exists();
38282
+ const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join26(configPath, "config.json")).exists();
35964
38283
  return {
35965
38284
  group: "Configuration",
35966
38285
  label: "Config exists",
@@ -36117,7 +38436,7 @@ var runDoctorChecks = {
36117
38436
  async checkV2Config(configPath) {
36118
38437
  const results = [];
36119
38438
  const storagePath = await resolveConfigStoragePath(configPath);
36120
- const configFile = Bun.file(join23(storagePath, "config.json"));
38439
+ const configFile = Bun.file(join26(storagePath, "config.json"));
36121
38440
  if (!await configFile.exists())
36122
38441
  return results;
36123
38442
  let raw;
@@ -36157,7 +38476,7 @@ var runDoctorChecks = {
36157
38476
  }
36158
38477
  for (const [name, conn] of Object.entries(config.connections)) {
36159
38478
  if (conn.envFile) {
36160
- const envPath = join23(storagePath, conn.envFile);
38479
+ const envPath = join26(storagePath, conn.envFile);
36161
38480
  const exists = await Bun.file(envPath).exists();
36162
38481
  results.push({
36163
38482
  group: "Configuration",
@@ -36302,7 +38621,7 @@ async function collectElasticsearchDoctorResults(config) {
36302
38621
  return results;
36303
38622
  }
36304
38623
  var doctorCommand = new Command("doctor").description("Run diagnostic checks on dbcli configuration, environment, and connection").option("--format <type>", "Output format: text, json", "text").action(async (options) => {
36305
- validateFormat(options.format, ALLOWED_FORMATS13, "doctor");
38624
+ validateFormat(options.format, ALLOWED_FORMATS15, "doctor");
36306
38625
  const logger = getLogger();
36307
38626
  const results = [];
36308
38627
  const configPath = resolveConfigPath(doctorCommand);
@@ -36376,7 +38695,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
36376
38695
  }
36377
38696
  try {
36378
38697
  const schemaConnName = await getSchemaIsolationConnectionName(configPath);
36379
- const indexPath = join23(resolveSchemaPath(storagePath, schemaConnName), "index.json");
38698
+ const indexPath = join26(resolveSchemaPath(storagePath, schemaConnName), "index.json");
36380
38699
  const indexFile = Bun.file(indexPath);
36381
38700
  let indexParsed = null;
36382
38701
  if (await indexFile.exists()) {
@@ -36407,6 +38726,17 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
36407
38726
  }
36408
38727
  }
36409
38728
  const hasError = results.some((r) => r.status === "error");
38729
+ try {
38730
+ const config = await configModule.read(configPath);
38731
+ await writeAuditEntry(config, "doctor", options, {
38732
+ success: !hasError,
38733
+ target: "*",
38734
+ metadata: {
38735
+ error_count: results.filter((r) => r.status === "error").length,
38736
+ warn_count: results.filter((r) => r.status === "warn").length
38737
+ }
38738
+ });
38739
+ } catch {}
36410
38740
  if (options.format === "json") {
36411
38741
  console.log(JSON.stringify({ results, hasError }, null, 2));
36412
38742
  } else {
@@ -36419,7 +38749,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
36419
38749
 
36420
38750
  // src/commands/completion.ts
36421
38751
  init_colors();
36422
- import { join as join24 } from "path";
38752
+ import { join as join27 } from "path";
36423
38753
  import { homedir as homedir3 } from "os";
36424
38754
  function extractCommands(program2) {
36425
38755
  return program2.commands.map((cmd) => ({
@@ -36521,11 +38851,11 @@ function getInstallPath2(shell) {
36521
38851
  const home = homedir3();
36522
38852
  switch (shell) {
36523
38853
  case "bash":
36524
- return join24(home, ".bashrc");
38854
+ return join27(home, ".bashrc");
36525
38855
  case "zsh":
36526
- return join24(home, ".zshrc");
38856
+ return join27(home, ".zshrc");
36527
38857
  case "fish":
36528
- return join24(home, ".config", "fish", "completions", "dbcli.fish");
38858
+ return join27(home, ".config", "fish", "completions", "dbcli.fish");
36529
38859
  default:
36530
38860
  throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
36531
38861
  }
@@ -36545,7 +38875,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
36545
38875
  async function installCompletion(shell, script) {
36546
38876
  const targetPath = getInstallPath2(shell);
36547
38877
  if (shell === "fish") {
36548
- const dir = join24(homedir3(), ".config", "fish", "completions");
38878
+ const dir = join27(homedir3(), ".config", "fish", "completions");
36549
38879
  await Bun.$`mkdir -p ${dir}`.quiet();
36550
38880
  await Bun.file(targetPath).write(script);
36551
38881
  console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
@@ -36778,7 +39108,7 @@ ${t("upgrade.failed")}`));
36778
39108
  init_config();
36779
39109
  init_adapters();
36780
39110
  import { createInterface as createInterface2 } from "readline";
36781
- import { join as join25 } from "path";
39111
+ import { join as join28 } from "path";
36782
39112
  import { homedir as homedir4 } from "os";
36783
39113
 
36784
39114
  // src/core/repl/types.ts
@@ -37123,7 +39453,7 @@ function isKnownCommand(name) {
37123
39453
 
37124
39454
  // src/core/repl/history-manager.ts
37125
39455
  import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
37126
- import { dirname as dirname8 } from "path";
39456
+ import { dirname as dirname10 } from "path";
37127
39457
  var DEFAULT_MAX = 1000;
37128
39458
 
37129
39459
  class HistoryManager {
@@ -37151,7 +39481,7 @@ class HistoryManager {
37151
39481
  return this.entries;
37152
39482
  }
37153
39483
  async save() {
37154
- const dir = dirname8(this.filePath);
39484
+ const dir = dirname10(this.filePath);
37155
39485
  if (!existsSync(dir)) {
37156
39486
  mkdirSync2(dir, { recursive: true });
37157
39487
  }
@@ -37509,7 +39839,7 @@ function requireSqlConnection10(connection) {
37509
39839
  }
37510
39840
  return connection;
37511
39841
  }
37512
- var HISTORY_PATH = join25(homedir4(), ".dbcli_history");
39842
+ var HISTORY_PATH = join28(homedir4(), ".dbcli_history");
37513
39843
  var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
37514
39844
  async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
37515
39845
  const columnsByTable = {};
@@ -38496,7 +40826,7 @@ init_config();
38496
40826
  init_errors();
38497
40827
  init_message_loader();
38498
40828
  init_config_binding();
38499
- import { join as join26 } from "path";
40829
+ import { join as join29 } from "path";
38500
40830
  async function switchDefault(configPath, name, config) {
38501
40831
  if (!config.connections[name]) {
38502
40832
  const available = Object.keys(config.connections).join(", ");
@@ -38519,7 +40849,7 @@ function listConnectionsForDisplay(config) {
38519
40849
  }
38520
40850
  async function ensureV2Config(configPath) {
38521
40851
  const storagePath = await resolveConfigStoragePath(configPath);
38522
- const configFile = Bun.file(join26(storagePath, "config.json"));
40852
+ const configFile = Bun.file(join29(storagePath, "config.json"));
38523
40853
  const legacyFile = Bun.file(configPath);
38524
40854
  if (!await configFile.exists() && !await legacyFile.exists()) {
38525
40855
  throw new ConfigError(t("init.config_not_found"));
@@ -38544,7 +40874,8 @@ async function ensureV2Config(configPath) {
38544
40874
  schema: v1Config.schema || {},
38545
40875
  schemas: { default: v1Config.schema || {} },
38546
40876
  metadata: v1Config.metadata || { version: "1.0" },
38547
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
40877
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
40878
+ audit: v1Config.audit
38548
40879
  };
38549
40880
  }
38550
40881
  return readV2Config(storagePath);
@@ -38582,7 +40913,7 @@ var useCommand = new Command("use").description("Switch or display the default d
38582
40913
 
38583
40914
  // src/cli.ts
38584
40915
  init_config();
38585
- import { join as join27 } from "path";
40916
+ import { join as join30 } from "path";
38586
40917
  var _bgVersionCheckResult;
38587
40918
  function shouldSkipBackgroundChecks() {
38588
40919
  return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
@@ -38611,7 +40942,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
38611
40942
  try {
38612
40943
  let cache = null;
38613
40944
  try {
38614
- const cacheFile = Bun.file(join27(configPath, "version-check.json"));
40945
+ const cacheFile = Bun.file(join30(configPath, "version-check.json"));
38615
40946
  if (await cacheFile.exists()) {
38616
40947
  cache = await cacheFile.json();
38617
40948
  }
@@ -38659,7 +40990,7 @@ program2.command("plan <sql>").description("Analyze SQL risk without executing")
38659
40990
  program2.command("q <name>").description(t("q.description")).option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--no-limit", "Disable size guard wrap (LIMIT 1000)").option("--dry-run", "Show final SQL + bind values; do not execute").option("--param <kv>", "Pass parameter as key=value (repeatable)", (val, prev = []) => prev.concat([val]), []).option("--param-file <path>", "JSON file containing param values").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (name, options, command) => {
38660
40991
  await qCommand(name, options, command);
38661
40992
  });
38662
- program2.command("insert <table>").description(t("insert.description")).option("--data <json>", "JSON object to insert").option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
40993
+ program2.command("insert <table>").description(t("insert.description")).option("--data <json>", "JSON object to insert").option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
38663
40994
  try {
38664
40995
  await insertCommand(table, options, command);
38665
40996
  } catch (error) {
@@ -38671,7 +41002,7 @@ program2.command("insert <table>").description(t("insert.description")).option("
38671
41002
  process.exit(1);
38672
41003
  }
38673
41004
  });
38674
- program2.command("update <table>").description(t("update.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--set <json>", `JSON with fields to update (required, e.g. '{"name":"Bob"}')`).option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
41005
+ program2.command("update <table>").description(t("update.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--set <json>", `JSON with fields to update (required, e.g. '{"name":"Bob"}')`).option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
38675
41006
  try {
38676
41007
  await updateCommand(table, options, command);
38677
41008
  } catch (error) {
@@ -38683,7 +41014,7 @@ program2.command("update <table>").description(t("update.description")).option("
38683
41014
  process.exit(1);
38684
41015
  }
38685
41016
  });
38686
- program2.command("delete <table>").description(t("delete.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
41017
+ program2.command("delete <table>").description(t("delete.description")).option("--where <condition>", 'WHERE clause (required, e.g. "id=1")').option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
38687
41018
  try {
38688
41019
  await deleteCommand(table, options, command);
38689
41020
  } catch (error) {
@@ -38721,6 +41052,7 @@ program2.addCommand(reportCommand);
38721
41052
  program2.addCommand(guideCommand);
38722
41053
  program2.addCommand(recoveryCommand);
38723
41054
  program2.addCommand(recoverCommand);
41055
+ program2.addCommand(auditCommand);
38724
41056
  program2.addCommand(doctorCommand);
38725
41057
  program2.addCommand(completionCommand);
38726
41058
  program2.addCommand(upgradeCommand);