@carllee1983/dbcli 1.19.1 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);
@@ -20696,13 +20787,21 @@ var init_capabilities = __esm(() => {
20696
20787
  "completion",
20697
20788
  "upgrade",
20698
20789
  "recover",
20699
- "skill"
20790
+ "skill",
20791
+ "auditTail",
20792
+ "auditShow",
20793
+ "auditClear",
20794
+ "auditHealth"
20700
20795
  ]);
20701
20796
  ENGINE_INDEPENDENT = {
20702
20797
  completion: cap("not-applicable", "none", "Shell completion is engine-independent."),
20703
20798
  upgrade: cap("not-applicable", "local-write", "Package update checks are engine-independent."),
20704
20799
  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.")
20800
+ skill: cap("not-applicable", "local-write", "Skill and task-pack generation are engine-independent."),
20801
+ auditTail: cap("supported", "readonly", "Reads JSONL audit entries; never writes to engines."),
20802
+ auditShow: cap("supported", "readonly", "Looks up a single audit entry by id prefix or recovery_ref."),
20803
+ auditHealth: cap("supported", "readonly", "Renders AuditLogger.getHealth() snapshot."),
20804
+ auditClear: cap("supported", "local-write", "Removes <conn>.jsonl + .jsonl.1 from local disk; never touches DB.")
20706
20805
  };
20707
20806
  SQL_BASE = {
20708
20807
  init: cap("supported", "interactive", "SQL connection initialization is supported."),
@@ -25419,6 +25518,7 @@ var init_render_markdown = __esm(() => {
25419
25518
  // src/core/recovery/last-envelope.ts
25420
25519
  import { writeFile, readFile, rename, mkdir as mkdir2, stat } from "fs/promises";
25421
25520
  import { dirname, join as join10 } from "path";
25521
+ import { randomUUID as randomUUID2 } from "crypto";
25422
25522
  function sanitizeCommandSummary(argv) {
25423
25523
  if (argv.length === 0)
25424
25524
  return "<unknown>";
@@ -25459,11 +25559,13 @@ function sanitizeCommandSummary(argv) {
25459
25559
  }
25460
25560
  return out.join(" ");
25461
25561
  }
25462
- async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date) {
25562
+ async function writeLastEnvelope(cwd, envelope, argv, now = () => new Date, id = randomUUID2(), auditRef) {
25463
25563
  const target = join10(cwd, LAST_ENVELOPE_PATH);
25464
25564
  const tmp = `${target}.tmp`;
25465
25565
  const payload = {
25466
25566
  schemaVersion: 1,
25567
+ id,
25568
+ ...auditRef !== undefined && { audit_ref: auditRef },
25467
25569
  savedAt: now().toISOString(),
25468
25570
  command: sanitizeCommandSummary(argv),
25469
25571
  cwd,
@@ -25526,11 +25628,13 @@ var init_last_envelope = __esm(() => {
25526
25628
  // src/core/recovery/emit.ts
25527
25629
  import { writeFileSync as writeFileSync2, mkdirSync, renameSync } from "fs";
25528
25630
  import { dirname as dirname2, join as join11 } from "path";
25631
+ import { randomUUID as randomUUID3 } from "crypto";
25529
25632
  function emitRecoveryEnvelope(error, ctx, options = {}) {
25530
25633
  const envelope = classifyError(error, ctx);
25531
25634
  const cwd = options.cwd ?? process.cwd();
25532
25635
  const argv = options.argv ?? buildArgvFromProcess();
25533
- writeLastEnvelopeSync(cwd, envelope, argv);
25636
+ const envelopeId = options.envelopeId ?? randomUUID3();
25637
+ writeLastEnvelopeSync(cwd, envelope, argv, envelopeId, options.auditRef);
25534
25638
  process.stdout.write(renderJson(envelope, { brief: options.brief === true }) + `
25535
25639
  `);
25536
25640
  process.exit(options.exitCode ?? 1);
@@ -25539,11 +25643,13 @@ function buildArgvFromProcess() {
25539
25643
  const userArgs = process.argv.slice(2);
25540
25644
  return ["dbcli", ...userArgs];
25541
25645
  }
25542
- function writeLastEnvelopeSync(cwd, envelope, argv) {
25646
+ function writeLastEnvelopeSync(cwd, envelope, argv, id, auditRef) {
25543
25647
  const target = join11(cwd, LAST_ENVELOPE_PATH);
25544
25648
  const tmp = `${target}.tmp`;
25545
25649
  const payload = {
25546
25650
  schemaVersion: 1,
25651
+ id,
25652
+ ...auditRef !== undefined && { audit_ref: auditRef },
25547
25653
  savedAt: new Date().toISOString(),
25548
25654
  command: sanitizeCommandSummary(argv),
25549
25655
  cwd,
@@ -26518,6 +26624,507 @@ function levenshteinDistance(a, b) {
26518
26624
  return dp[lenB]?.[lenA] ?? 0;
26519
26625
  }
26520
26626
 
26627
+ // src/utils/engine-hints.ts
26628
+ var exports_engine_hints = {};
26629
+ __export(exports_engine_hints, {
26630
+ getOperationTarget: () => getOperationTarget,
26631
+ extractTableName: () => extractTableName
26632
+ });
26633
+ function extractTableName(sql) {
26634
+ const match = sql.match(TABLE_NAME_RE);
26635
+ return match ? match[1] ?? null : null;
26636
+ }
26637
+ function getOperationTarget(system, command, options, sql) {
26638
+ if (system === "mongodb") {
26639
+ return options.collection || "<unknown-collection>";
26640
+ }
26641
+ if (system === "elasticsearch") {
26642
+ return options.index || options.collection || "<unknown-index>";
26643
+ }
26644
+ if (system === "redis") {
26645
+ return options.table || options.key || "<unknown-key>";
26646
+ }
26647
+ if (sql) {
26648
+ const table = extractTableName(sql);
26649
+ if (table)
26650
+ return table;
26651
+ }
26652
+ return options.table || "<unknown-target>";
26653
+ }
26654
+ var TABLE_NAME_RE;
26655
+ var init_engine_hints = __esm(() => {
26656
+ TABLE_NAME_RE = /\b(?:FROM|INTO|UPDATE)\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i;
26657
+ });
26658
+
26659
+ // src/core/audit/lock.ts
26660
+ import { hostname } from "os";
26661
+ import { dirname as dirname4 } from "path";
26662
+ import { mkdir as mkdir3, open as open2, rm } from "fs/promises";
26663
+
26664
+ class AuditLockManager {
26665
+ auditFilePath;
26666
+ lockPath;
26667
+ lockAcquiredAt = null;
26668
+ constructor(auditFilePath) {
26669
+ this.auditFilePath = auditFilePath;
26670
+ this.lockPath = `${auditFilePath}.lock`;
26671
+ }
26672
+ getLockfilePath() {
26673
+ return this.lockPath;
26674
+ }
26675
+ isLockHeld() {
26676
+ return this.lockAcquiredAt !== null;
26677
+ }
26678
+ async acquireLock(operationName = "audit-write") {
26679
+ const startTime = Date.now();
26680
+ let backoffMs = LOCK_BACKOFF_START_MS;
26681
+ while (true) {
26682
+ const elapsed = Date.now() - startTime;
26683
+ if (elapsed > LOCK_RETRY_BUDGET_MS) {
26684
+ return false;
26685
+ }
26686
+ if (await this.tryAcquireLock(operationName)) {
26687
+ this.lockAcquiredAt = Date.now();
26688
+ return true;
26689
+ }
26690
+ const waitTime = Math.min(backoffMs, LOCK_BACKOFF_MAX_MS);
26691
+ await new Promise((resolve4) => setTimeout(resolve4, waitTime));
26692
+ backoffMs = Math.min(backoffMs * 1.5, LOCK_BACKOFF_MAX_MS);
26693
+ }
26694
+ }
26695
+ async releaseLock() {
26696
+ if (!this.lockAcquiredAt)
26697
+ return false;
26698
+ try {
26699
+ await rm(this.lockPath, { force: true });
26700
+ this.lockAcquiredAt = null;
26701
+ return true;
26702
+ } catch {
26703
+ this.lockAcquiredAt = null;
26704
+ return false;
26705
+ }
26706
+ }
26707
+ async withLock(operation, operationName = "audit-write") {
26708
+ const acquired = await this.acquireLock(operationName);
26709
+ if (!acquired) {
26710
+ return { skipped: "lock-budget-exhausted" };
26711
+ }
26712
+ try {
26713
+ return await operation();
26714
+ } finally {
26715
+ await this.releaseLock();
26716
+ }
26717
+ }
26718
+ async tryAcquireLock(operationName) {
26719
+ try {
26720
+ await mkdir3(dirname4(this.lockPath), { recursive: true });
26721
+ const lockFile = Bun.file(this.lockPath);
26722
+ if (await lockFile.exists()) {
26723
+ const lockContent = await lockFile.json();
26724
+ const lockAge = Date.now() - lockContent.timestamp;
26725
+ const staleLockThresholdMs = LOCK_RETRY_BUDGET_MS * STALE_LOCK_MULTIPLIER;
26726
+ if (lockAge > staleLockThresholdMs) {
26727
+ await rm(this.lockPath, { force: true });
26728
+ } else {
26729
+ return false;
26730
+ }
26731
+ }
26732
+ const lockData = {
26733
+ pid: process.pid,
26734
+ operation: operationName,
26735
+ timestamp: Date.now(),
26736
+ hostname: hostname()
26737
+ };
26738
+ const handle = await open2(this.lockPath, "wx");
26739
+ try {
26740
+ await handle.writeFile(JSON.stringify(lockData), "utf8");
26741
+ } finally {
26742
+ await handle.close();
26743
+ }
26744
+ return true;
26745
+ } catch {
26746
+ return false;
26747
+ }
26748
+ }
26749
+ }
26750
+ var LOCK_RETRY_BUDGET_MS = 200, LOCK_BACKOFF_START_MS = 5, LOCK_BACKOFF_MAX_MS = 50, STALE_LOCK_MULTIPLIER = 10;
26751
+ var init_lock = () => {};
26752
+
26753
+ // src/core/audit/rotation.ts
26754
+ import { rename as rename2 } from "fs/promises";
26755
+ function shouldRotate(stats, thresholds, nextLineByteLength) {
26756
+ const bytesAfter = stats.currentSizeBytes + nextLineByteLength;
26757
+ const entriesAfter = stats.currentEntryCount + 1;
26758
+ return bytesAfter >= thresholds.maxBytes || entriesAfter > thresholds.maxEntries;
26759
+ }
26760
+ async function rotate(currentPath, previousPath) {
26761
+ try {
26762
+ await rename2(currentPath, previousPath);
26763
+ } catch {}
26764
+ }
26765
+ var init_rotation = () => {};
26766
+
26767
+ // src/core/audit/logger.ts
26768
+ import { appendFile, mkdir as mkdir4, readFile as readFile2, stat as stat3 } from "fs/promises";
26769
+ import { join as join13 } from "path";
26770
+ import { randomUUID as randomUUID4 } from "crypto";
26771
+
26772
+ class AuditLogger {
26773
+ auditDir;
26774
+ auditFilePath;
26775
+ previousFilePath;
26776
+ enabled;
26777
+ maxBytes;
26778
+ maxEntries;
26779
+ sessionIdService;
26780
+ lockManager;
26781
+ writerInitialized = false;
26782
+ currentSizeBytes = 0;
26783
+ currentEntryCount = 0;
26784
+ lastWrite = null;
26785
+ lastError = null;
26786
+ warnedOnceThisProcess = false;
26787
+ cachedSessionId = null;
26788
+ lastRotatedAt;
26789
+ lastRotatedPrevious;
26790
+ writeChain = Promise.resolve();
26791
+ constructor(opts) {
26792
+ this.enabled = opts.enabled;
26793
+ this.maxBytes = opts.rotation.maxBytes;
26794
+ this.maxEntries = opts.rotation.maxEntries;
26795
+ this.auditDir = join13(opts.storagePath, ".dbcli", "audit");
26796
+ this.auditFilePath = join13(this.auditDir, `${opts.connectionName}.jsonl`);
26797
+ this.previousFilePath = `${this.auditFilePath}.1`;
26798
+ this.sessionIdService = opts.sessionIdService;
26799
+ this.lockManager = opts.lockManager ?? new AuditLockManager(this.auditFilePath);
26800
+ }
26801
+ async write(entry) {
26802
+ const writeOp = this.writeChain.then(() => this.writeInternal(entry));
26803
+ this.writeChain = writeOp.then(() => {
26804
+ return;
26805
+ }, () => {
26806
+ return;
26807
+ });
26808
+ return writeOp;
26809
+ }
26810
+ async writeInternal(entry) {
26811
+ if (!this.enabled) {
26812
+ return { skipped: "disabled" };
26813
+ }
26814
+ try {
26815
+ const sessionId = await this.sessionIdService.resolve();
26816
+ this.cachedSessionId = sessionId;
26817
+ await mkdir4(this.auditDir, { recursive: true });
26818
+ if (!this.writerInitialized) {
26819
+ await this.syncCountersFromDisk();
26820
+ this.writerInitialized = true;
26821
+ }
26822
+ const id = randomUUID4();
26823
+ const ts = new Date().toISOString();
26824
+ const enriched = {
26825
+ ...entry,
26826
+ id,
26827
+ ts,
26828
+ session_id: sessionId
26829
+ };
26830
+ const line = JSON.stringify(enriched) + `
26831
+ `;
26832
+ const lineBytes = Buffer.byteLength(line, "utf8");
26833
+ const lockResult = await this.lockManager.withLock(async () => {
26834
+ let rotated = false;
26835
+ if (shouldRotate({
26836
+ currentSizeBytes: this.currentSizeBytes,
26837
+ currentEntryCount: this.currentEntryCount
26838
+ }, { maxBytes: this.maxBytes, maxEntries: this.maxEntries }, lineBytes)) {
26839
+ await rotate(this.auditFilePath, this.previousFilePath);
26840
+ this.lastRotatedAt = new Date().toISOString();
26841
+ this.lastRotatedPrevious = this.previousFilePath;
26842
+ this.currentSizeBytes = 0;
26843
+ this.currentEntryCount = 0;
26844
+ rotated = true;
26845
+ }
26846
+ await appendFile(this.auditFilePath, line, { encoding: "utf8" });
26847
+ this.currentSizeBytes += lineBytes;
26848
+ this.currentEntryCount += 1;
26849
+ return { rotated };
26850
+ }, "audit-write");
26851
+ if (lockResult !== null && typeof lockResult === "object" && "skipped" in lockResult) {
26852
+ this.handleFailure("lock-budget-exhausted");
26853
+ return { skipped: "lock-budget-exhausted" };
26854
+ }
26855
+ this.lastWrite = { ts, success: true };
26856
+ return { success: true, rotated: lockResult.rotated, id };
26857
+ } catch (err) {
26858
+ const message = err instanceof Error ? err.message : String(err);
26859
+ this.handleFailure(message);
26860
+ return { skipped: "write-failed", error: message };
26861
+ }
26862
+ }
26863
+ getHealth() {
26864
+ return {
26865
+ enabled: this.enabled,
26866
+ writerInitialized: this.writerInitialized,
26867
+ currentFile: this.auditFilePath,
26868
+ currentSizeBytes: this.currentSizeBytes,
26869
+ currentEntryCount: this.currentEntryCount,
26870
+ rotationUsage: {
26871
+ bytes: {
26872
+ current: this.currentSizeBytes,
26873
+ max: this.maxBytes,
26874
+ pct: this.maxBytes > 0 ? this.currentSizeBytes / this.maxBytes * 100 : 0
26875
+ },
26876
+ entries: {
26877
+ current: this.currentEntryCount,
26878
+ max: this.maxEntries,
26879
+ pct: this.maxEntries > 0 ? this.currentEntryCount / this.maxEntries * 100 : 0
26880
+ }
26881
+ },
26882
+ lock: { state: this.lockManager.isLockHeld() ? "held" : "free" },
26883
+ lastWrite: this.lastWrite,
26884
+ lastError: this.lastError,
26885
+ sessionId: this.cachedSessionId,
26886
+ rotation: {
26887
+ lastRotatedAt: this.lastRotatedAt,
26888
+ previousFile: this.lastRotatedPrevious
26889
+ }
26890
+ };
26891
+ }
26892
+ async syncCountersFromDisk() {
26893
+ try {
26894
+ const s = await stat3(this.auditFilePath);
26895
+ this.currentSizeBytes = s.size;
26896
+ const raw = await readFile2(this.auditFilePath, "utf8");
26897
+ this.currentEntryCount = raw.split(`
26898
+ `).filter(Boolean).length;
26899
+ } catch {
26900
+ this.currentSizeBytes = 0;
26901
+ this.currentEntryCount = 0;
26902
+ }
26903
+ }
26904
+ handleFailure(message) {
26905
+ const ts = new Date().toISOString();
26906
+ this.lastError = { ts, message };
26907
+ this.lastWrite = { ts, success: false, error: message };
26908
+ if (!this.warnedOnceThisProcess) {
26909
+ process.stderr.write(`${WARN_PREFIX} warning: audit write failed (${message}); subsequent failures suppressed this process.
26910
+ `);
26911
+ this.warnedOnceThisProcess = true;
26912
+ }
26913
+ }
26914
+ }
26915
+ var WARN_PREFIX = "[dbcli audit]";
26916
+ var init_logger = __esm(() => {
26917
+ init_lock();
26918
+ init_rotation();
26919
+ });
26920
+
26921
+ // src/core/audit/session-id.ts
26922
+ import { mkdir as mkdir5, readFile as readFile3, rename as rename3, stat as stat4, writeFile as writeFile2 } from "fs/promises";
26923
+ import { randomBytes } from "crypto";
26924
+ import { dirname as dirname5, join as join14 } from "path";
26925
+ function generateSessionId(pid, nowMs) {
26926
+ const random = randomBytes(3).toString("hex");
26927
+ return `${pid}-${nowMs}-${random}`;
26928
+ }
26929
+ async function readSessionIdFile(storagePath) {
26930
+ const target = join14(storagePath, LAST_SESSION_ID_RELATIVE);
26931
+ try {
26932
+ await stat4(target);
26933
+ } catch {
26934
+ return null;
26935
+ }
26936
+ try {
26937
+ const raw = await readFile3(target, "utf8");
26938
+ const parsed = JSON.parse(raw);
26939
+ if (parsed && typeof parsed.sessionId === "string" && typeof parsed.pid === "number" && typeof parsed.createdAt === "string") {
26940
+ return {
26941
+ sessionId: parsed.sessionId,
26942
+ pid: parsed.pid,
26943
+ createdAt: parsed.createdAt
26944
+ };
26945
+ }
26946
+ return null;
26947
+ } catch {
26948
+ return null;
26949
+ }
26950
+ }
26951
+ async function writeSessionIdFile(storagePath, payload) {
26952
+ const target = join14(storagePath, LAST_SESSION_ID_RELATIVE);
26953
+ const tmp = `${target}.tmp`;
26954
+ try {
26955
+ await mkdir5(dirname5(target), { recursive: true });
26956
+ await writeFile2(tmp, JSON.stringify(payload, null, 2), "utf8");
26957
+ await rename3(tmp, target);
26958
+ } catch {}
26959
+ }
26960
+
26961
+ class SessionIdService {
26962
+ storagePath;
26963
+ cached = null;
26964
+ constructor(storagePath) {
26965
+ this.storagePath = storagePath;
26966
+ }
26967
+ async resolve() {
26968
+ if (this.cached !== null)
26969
+ return this.cached;
26970
+ const fromEnv = process.env.DBCLI_SESSION_ID;
26971
+ if (fromEnv && fromEnv.length > 0) {
26972
+ this.cached = fromEnv;
26973
+ return fromEnv;
26974
+ }
26975
+ const saved = await readSessionIdFile(this.storagePath);
26976
+ if (saved && saved.pid === process.pid) {
26977
+ this.cached = saved.sessionId;
26978
+ return saved.sessionId;
26979
+ }
26980
+ const id = generateSessionId(process.pid, Date.now());
26981
+ await writeSessionIdFile(this.storagePath, {
26982
+ sessionId: id,
26983
+ pid: process.pid,
26984
+ createdAt: new Date().toISOString()
26985
+ });
26986
+ this.cached = id;
26987
+ return id;
26988
+ }
26989
+ reset() {
26990
+ this.cached = null;
26991
+ }
26992
+ }
26993
+ var LAST_SESSION_ID_RELATIVE = ".dbcli/last-session-id";
26994
+ var init_session_id = () => {};
26995
+
26996
+ // src/utils/redaction.ts
26997
+ function redactArgv(argv) {
26998
+ if (argv.length === 0)
26999
+ return "<unknown>";
27000
+ const out = [argv[0]];
27001
+ if (argv.length === 1)
27002
+ return out.join(" ");
27003
+ const sub = argv[1];
27004
+ out.push(sub);
27005
+ for (let i = 2;i < argv.length; i++) {
27006
+ const tok = argv[i];
27007
+ if (tok.startsWith("--")) {
27008
+ const [name, inlineValue] = tok.split("=");
27009
+ if (REDACTED_VALUE_FLAGS2.has(name)) {
27010
+ out.push(`${name} <redacted>`);
27011
+ if (inlineValue === undefined)
27012
+ i++;
27013
+ continue;
27014
+ }
27015
+ if (KEEP_VALUE_FLAGS2.has(name)) {
27016
+ if (inlineValue !== undefined) {
27017
+ out.push(tok);
27018
+ } else {
27019
+ out.push(name);
27020
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
27021
+ out.push(argv[++i]);
27022
+ }
27023
+ }
27024
+ continue;
27025
+ }
27026
+ out.push(tok);
27027
+ continue;
27028
+ }
27029
+ if (i === 2 && SQL_SUBCOMMANDS2.has(sub)) {
27030
+ out.push("<sql>");
27031
+ continue;
27032
+ }
27033
+ out.push(tok);
27034
+ }
27035
+ return out.join(" ");
27036
+ }
27037
+ function redactSensitive(text2) {
27038
+ return text2.replace(/\b(password|token|apiKey|secret|key|token|auth|credential|pass|pwd|sid)([:=]|\s+)([^\s"';,]+)/gi, "$1$2<redacted>");
27039
+ }
27040
+ function redactSql(sql) {
27041
+ const redacted = sql.replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g, "'?'").replace(/\b\d+(\.\d+)?\b/g, "0");
27042
+ return redactSensitive(redacted);
27043
+ }
27044
+ var SQL_SUBCOMMANDS2, REDACTED_VALUE_FLAGS2, KEEP_VALUE_FLAGS2;
27045
+ var init_redaction = __esm(() => {
27046
+ SQL_SUBCOMMANDS2 = new Set(["query", "export"]);
27047
+ REDACTED_VALUE_FLAGS2 = new Set([
27048
+ "--where",
27049
+ "--set",
27050
+ "--data",
27051
+ "--param",
27052
+ "--param-file",
27053
+ "--config",
27054
+ "--use",
27055
+ "--password",
27056
+ "--token",
27057
+ "--secret"
27058
+ ]);
27059
+ KEEP_VALUE_FLAGS2 = new Set(["--format", "--conn-name"]);
27060
+ });
27061
+
27062
+ // src/core/audit/integration-helper.ts
27063
+ async function getAuditLogger(config, configPath) {
27064
+ const storagePath = await resolveConfigStoragePath(configPath);
27065
+ const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
27066
+ const key = `${storagePath}:${connName}`;
27067
+ if (!_loggers.has(key)) {
27068
+ if (!_sessionIdService) {
27069
+ _sessionIdService = new SessionIdService(storagePath);
27070
+ }
27071
+ _loggers.set(key, new AuditLogger({
27072
+ storagePath,
27073
+ connectionName: connName,
27074
+ enabled: config.audit?.enabled ?? true,
27075
+ rotation: {
27076
+ maxBytes: config.audit?.rotation?.max_bytes ?? 10485760,
27077
+ maxEntries: config.audit?.rotation?.max_entries ?? 1000
27078
+ },
27079
+ sessionIdService: _sessionIdService
27080
+ }));
27081
+ }
27082
+ return _loggers.get(key);
27083
+ }
27084
+ async function writeAuditEntry(config, commandName, options, outcome) {
27085
+ try {
27086
+ const logger = await getAuditLogger(config, options.config || ".dbcli");
27087
+ const engine = config.connection?.system || "postgresql";
27088
+ const target = outcome.target || getOperationTarget(engine, commandName, options, outcome.sql);
27089
+ let tier = getEngineCapability(engine, commandName).tier;
27090
+ if (options.dryRun || options.plan) {
27091
+ tier = "dry-run";
27092
+ }
27093
+ let errorMessage;
27094
+ if (outcome.error) {
27095
+ errorMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
27096
+ errorMessage = redactSensitive(errorMessage);
27097
+ }
27098
+ const entry = {
27099
+ engine,
27100
+ command: commandName,
27101
+ side_effect_tier: tier,
27102
+ target,
27103
+ success: outcome.success,
27104
+ redacted_query: redactArgv(process.argv),
27105
+ ...outcome.sql && { redacted_sql: redactSql(outcome.sql) },
27106
+ ...errorMessage && { error: errorMessage },
27107
+ ...outcome.recovery_ref && { recovery_ref: outcome.recovery_ref },
27108
+ metadata: outcome.metadata
27109
+ };
27110
+ const result = await logger.write(entry);
27111
+ return "success" in result ? result.id : null;
27112
+ } catch {
27113
+ return null;
27114
+ }
27115
+ }
27116
+ var _sessionIdService = null, _loggers;
27117
+ var init_integration_helper = __esm(() => {
27118
+ init_logger();
27119
+ init_session_id();
27120
+ init_config_binding();
27121
+ init_config();
27122
+ init_capabilities();
27123
+ init_redaction();
27124
+ init_engine_hints();
27125
+ _loggers = new Map;
27126
+ });
27127
+
26521
27128
  // src/commands/query-size-guard.ts
26522
27129
  var exports_query_size_guard = {};
26523
27130
  __export(exports_query_size_guard, {
@@ -27430,7 +28037,7 @@ var init_parser = __esm(() => {
27430
28037
 
27431
28038
  // src/core/saved-queries/loader.ts
27432
28039
  import { readdir } from "fs/promises";
27433
- import { join as join14, relative, sep } from "path";
28040
+ import { join as join16, relative, sep } from "path";
27434
28041
  async function loadSnippets(opts) {
27435
28042
  const builtin = await walkAndParse(opts.builtinDir, "builtin");
27436
28043
  const shared = await walkAndParse(opts.sharedDir, "shared");
@@ -27497,7 +28104,7 @@ async function collectFiles(root) {
27497
28104
  async function walk(dir) {
27498
28105
  const entries = await readdir(dir, { withFileTypes: true });
27499
28106
  for (const e of entries) {
27500
- const full = join14(dir, e.name);
28107
+ const full = join16(dir, e.name);
27501
28108
  if (e.isDirectory())
27502
28109
  await walk(full);
27503
28110
  else
@@ -27581,23 +28188,23 @@ var init_runner = __esm(() => {
27581
28188
  });
27582
28189
 
27583
28190
  // src/core/saved-queries/snippet-paths.ts
27584
- import { join as join15 } from "path";
28191
+ import { join as join17 } from "path";
27585
28192
  function resolveBuiltinDir() {
27586
28193
  return packageAssetPath("snippets");
27587
28194
  }
27588
28195
  function resolveSnippetDirs(workspaceRoot) {
27589
28196
  return {
27590
28197
  builtinDir: resolveBuiltinDir(),
27591
- sharedDir: join15(workspaceRoot, ".dbcli-shared", "queries"),
27592
- localDir: join15(workspaceRoot, ".dbcli", "queries")
28198
+ sharedDir: join17(workspaceRoot, ".dbcli-shared", "queries"),
28199
+ localDir: join17(workspaceRoot, ".dbcli", "queries")
27593
28200
  };
27594
28201
  }
27595
28202
  function snippetKeyToFile(workspaceRoot, key, source) {
27596
28203
  const rel = key.replace(/^@/, "") + ".sql";
27597
28204
  if (source === "builtin")
27598
- return join15(resolveBuiltinDir(), rel);
28205
+ return join17(resolveBuiltinDir(), rel);
27599
28206
  const dir = source === "shared" ? ".dbcli-shared/queries" : ".dbcli/queries";
27600
- return join15(workspaceRoot, dir, rel);
28207
+ return join17(workspaceRoot, dir, rel);
27601
28208
  }
27602
28209
  var init_snippet_paths = __esm(() => {
27603
28210
  init_package_root();
@@ -27660,7 +28267,7 @@ var exports_queries_delete = {};
27660
28267
  __export(exports_queries_delete, {
27661
28268
  queriesDelete: () => queriesDelete
27662
28269
  });
27663
- import { rm } from "fs/promises";
28270
+ import { rm as rm2 } from "fs/promises";
27664
28271
  async function queriesDelete(name, options = {}) {
27665
28272
  if (!name.startsWith("@")) {
27666
28273
  throw new Error(`Snippet name must start with '@' (got '${name}')`);
@@ -27685,7 +28292,7 @@ async function queriesDelete(name, options = {}) {
27685
28292
  return;
27686
28293
  }
27687
28294
  for (const v of localVariants) {
27688
- await rm(v.query.file, { force: true });
28295
+ await rm2(v.query.file, { force: true });
27689
28296
  console.log(`deleted ${v.query.file}`);
27690
28297
  }
27691
28298
  }
@@ -27699,8 +28306,8 @@ var exports_queries_rename = {};
27699
28306
  __export(exports_queries_rename, {
27700
28307
  queriesRename: () => queriesRename
27701
28308
  });
27702
- import { rename as rename2, mkdir as mkdir3 } from "fs/promises";
27703
- import { dirname as dirname4 } from "path";
28309
+ import { rename as rename4, mkdir as mkdir6 } from "fs/promises";
28310
+ import { dirname as dirname6 } from "path";
27704
28311
  async function queriesRename(oldName, newName, options = {}) {
27705
28312
  if (!oldName.startsWith("@") || !newName.startsWith("@")) {
27706
28313
  throw new Error(`Both names must start with '@'`);
@@ -27721,8 +28328,8 @@ async function queriesRename(oldName, newName, options = {}) {
27721
28328
  for (const v of local) {
27722
28329
  const dst = snippetKeyToFile(cwd, newName, "local");
27723
28330
  const dstWithSuffix = preserveEngineSuffix(v.query.file, dst);
27724
- await mkdir3(dirname4(dstWithSuffix), { recursive: true });
27725
- await rename2(v.query.file, dstWithSuffix);
28331
+ await mkdir6(dirname6(dstWithSuffix), { recursive: true });
28332
+ await rename4(v.query.file, dstWithSuffix);
27726
28333
  await rewriteFrontmatterName(dstWithSuffix, newName.slice(1));
27727
28334
  console.log(`renamed ${v.query.file} \u2192 ${dstWithSuffix}`);
27728
28335
  }
@@ -27747,8 +28354,8 @@ var exports_queries_copy = {};
27747
28354
  __export(exports_queries_copy, {
27748
28355
  queriesCopy: () => queriesCopy
27749
28356
  });
27750
- import { mkdir as mkdir4, copyFile } from "fs/promises";
27751
- import { dirname as dirname5, basename as basename2 } from "path";
28357
+ import { mkdir as mkdir7, copyFile } from "fs/promises";
28358
+ import { dirname as dirname7, basename as basename2 } from "path";
27752
28359
  async function queriesCopy(src, dst, options = {}) {
27753
28360
  if (!src.startsWith("@") || !dst.startsWith("@")) {
27754
28361
  throw new Error(`Both names must start with '@'`);
@@ -27766,7 +28373,7 @@ async function queriesCopy(src, dst, options = {}) {
27766
28373
  }
27767
28374
  for (const v of variants) {
27768
28375
  const dstFile = mapEngineSuffix(v.query.file, snippetKeyToFile(cwd, dst, "local"));
27769
- await mkdir4(dirname5(dstFile), { recursive: true });
28376
+ await mkdir7(dirname7(dstFile), { recursive: true });
27770
28377
  await copyFile(v.query.file, dstFile);
27771
28378
  console.log(`copied ${v.query.file} \u2192 ${dstFile}`);
27772
28379
  }
@@ -27784,21 +28391,21 @@ var exports_queries_import = {};
27784
28391
  __export(exports_queries_import, {
27785
28392
  queriesImport: () => queriesImport
27786
28393
  });
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";
28394
+ import { stat as stat5, mkdir as mkdir8, copyFile as copyFile2 } from "fs/promises";
28395
+ import { basename as basename3, join as join19, extname } from "path";
27789
28396
  async function queriesImport(filePath, options = {}) {
27790
28397
  const cwd = options.cwd ?? process.cwd();
27791
28398
  if (extname(filePath) !== ".sql") {
27792
28399
  throw new Error(`Expected .sql file, got ${filePath}`);
27793
28400
  }
27794
- await stat3(filePath);
28401
+ await stat5(filePath);
27795
28402
  const text2 = await Bun.file(filePath).text();
27796
28403
  const baseName = options.as ? options.as.replace(/^@/, "") : basename3(filePath, ".sql").replace(/\.(postgres|mysql)$/, "");
27797
28404
  const key = "@" + baseName;
27798
28405
  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));
28406
+ const targetDir = join19(cwd, ".dbcli/queries");
28407
+ await mkdir8(targetDir, { recursive: true });
28408
+ const target = join19(targetDir, basename3(filePath));
27802
28409
  if (await Bun.file(target).exists()) {
27803
28410
  if (!options.force) {
27804
28411
  const ok = await dist_default6({ message: `Overwrite ${target}?`, default: false });
@@ -27819,7 +28426,7 @@ var exports_queries_export = {};
27819
28426
  __export(exports_queries_export, {
27820
28427
  queriesExport: () => queriesExport
27821
28428
  });
27822
- import { writeFile as writeFile2 } from "fs/promises";
28429
+ import { writeFile as writeFile3 } from "fs/promises";
27823
28430
  async function queriesExport(name, options = {}) {
27824
28431
  const cwd = options.cwd ?? process.cwd();
27825
28432
  const dirs = resolveSnippetDirs(cwd);
@@ -27840,7 +28447,7 @@ async function queriesExport(name, options = {}) {
27840
28447
  }
27841
28448
  const text2 = await Bun.file(chosen[0].query.file).text();
27842
28449
  if (options.output) {
27843
- await writeFile2(options.output, text2, "utf8");
28450
+ await writeFile3(options.output, text2, "utf8");
27844
28451
  console.log(`wrote ${options.output}`);
27845
28452
  } else {
27846
28453
  process.stdout.write(text2);
@@ -27923,17 +28530,17 @@ var init_collect_snippets = __esm(() => {
27923
28530
  });
27924
28531
 
27925
28532
  // src/core/inspect/collect-schema-cache.ts
27926
- import { join as join21 } from "path";
28533
+ import { join as join23 } from "path";
27927
28534
  async function collectSchemaCache(opts) {
27928
28535
  const warnings = [];
27929
- if (opts.system && !SQL_SYSTEMS.includes(opts.system)) {
28536
+ if (opts.system && !SQL_SYSTEMS2.includes(opts.system)) {
27930
28537
  return {
27931
28538
  section: { available: false, unavailable: true, reason: "schema cache is sql-only" },
27932
28539
  warnings
27933
28540
  };
27934
28541
  }
27935
28542
  const root = resolveSchemaPath(opts.dbcliPath, opts.connectionName);
27936
- const indexPath = join21(root, "index.json");
28543
+ const indexPath = join23(root, "index.json");
27937
28544
  const file = Bun.file(indexPath);
27938
28545
  if (!await file.exists()) {
27939
28546
  return { section: { available: false }, warnings };
@@ -27960,11 +28567,11 @@ async function collectSchemaCache(opts) {
27960
28567
  };
27961
28568
  }
27962
28569
  }
27963
- var STALE_AFTER_MS, SQL_SYSTEMS;
28570
+ var STALE_AFTER_MS, SQL_SYSTEMS2;
27964
28571
  var init_collect_schema_cache = __esm(() => {
27965
28572
  init_schema_path();
27966
28573
  STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
27967
- SQL_SYSTEMS = ["postgresql", "mysql", "mariadb"];
28574
+ SQL_SYSTEMS2 = ["postgresql", "mysql", "mariadb"];
27968
28575
  });
27969
28576
 
27970
28577
  // src/core/inspect/collect-objects.ts
@@ -28024,7 +28631,7 @@ function suggestCommands(snap, options = {}) {
28024
28631
  if (!snap.system)
28025
28632
  return ["dbcli init"];
28026
28633
  const out = [];
28027
- const isSql = SQL_SYSTEMS2.includes(snap.system);
28634
+ const isSql = SQL_SYSTEMS3.includes(snap.system);
28028
28635
  if (isSql && (!snap.schemaCache.available || snap.schemaCache.stale)) {
28029
28636
  out.push("dbcli schema --refresh");
28030
28637
  }
@@ -28039,13 +28646,13 @@ function suggestCommands(snap, options = {}) {
28039
28646
  const cap2 = options.brief ? 1 : 5;
28040
28647
  return out.slice(0, cap2);
28041
28648
  }
28042
- var SQL_SYSTEMS2;
28649
+ var SQL_SYSTEMS3;
28043
28650
  var init_suggest_commands = __esm(() => {
28044
- SQL_SYSTEMS2 = ["postgresql", "mysql", "mariadb"];
28651
+ SQL_SYSTEMS3 = ["postgresql", "mysql", "mariadb"];
28045
28652
  });
28046
28653
 
28047
28654
  // src/core/inspect/collector.ts
28048
- import { join as join22 } from "path";
28655
+ import { join as join24 } from "path";
28049
28656
  async function collectInspect(opts) {
28050
28657
  const warnings = [];
28051
28658
  let config = null;
@@ -28121,11 +28728,11 @@ async function collectInspect(opts) {
28121
28728
  return { ...snapWithoutSuggestions, suggestedCommands, warnings };
28122
28729
  }
28123
28730
  async function hasConfig(configPath) {
28124
- if (await Bun.file(join22(configPath, "config.json")).exists())
28731
+ if (await Bun.file(join24(configPath, "config.json")).exists())
28125
28732
  return true;
28126
28733
  if (await Bun.file(configPath).exists()) {
28127
- const stat4 = await Bun.file(configPath).stat().catch(() => null);
28128
- return stat4?.isFile() === true;
28734
+ const stat6 = await Bun.file(configPath).stat().catch(() => null);
28735
+ return stat6?.isFile() === true;
28129
28736
  }
28130
28737
  return false;
28131
28738
  }
@@ -28247,16 +28854,16 @@ function renderMarkdown2(snap, options = {}) {
28247
28854
  function requireSchemaCacheOrThrow(section, system) {
28248
28855
  if (!system)
28249
28856
  return;
28250
- if (!SQL_SYSTEMS3.includes(system))
28857
+ if (!SQL_SYSTEMS4.includes(system))
28251
28858
  return;
28252
28859
  if (section.available === true)
28253
28860
  return;
28254
28861
  throw new SchemaCacheMissingError(`Schema cache unavailable for ${system} connection; run 'dbcli schema --refresh' to populate it.`);
28255
28862
  }
28256
- var SQL_SYSTEMS3;
28863
+ var SQL_SYSTEMS4;
28257
28864
  var init_require_schema_cache = __esm(() => {
28258
28865
  init_recovery();
28259
- SQL_SYSTEMS3 = ["postgresql", "mysql", "mariadb"];
28866
+ SQL_SYSTEMS4 = ["postgresql", "mysql", "mariadb"];
28260
28867
  });
28261
28868
 
28262
28869
  // src/core/inspect/index.ts
@@ -28273,6 +28880,125 @@ var init_inspect = __esm(() => {
28273
28880
  init_collector();
28274
28881
  });
28275
28882
 
28883
+ // src/core/audit/reader.ts
28884
+ import { readFile as readFile4, readdir as readdir3 } from "fs/promises";
28885
+ async function readSingle(path5) {
28886
+ let raw;
28887
+ try {
28888
+ raw = await readFile4(path5, "utf8");
28889
+ } catch (err) {
28890
+ if (err.code === "ENOENT")
28891
+ return [];
28892
+ throw err;
28893
+ }
28894
+ const lines = raw.split(`
28895
+ `).filter(Boolean);
28896
+ const out = [];
28897
+ for (let i = 0;i < lines.length; i++) {
28898
+ try {
28899
+ out.push(JSON.parse(lines[i]));
28900
+ } catch {
28901
+ if (i === lines.length - 1) {
28902
+ process.stderr.write(`${WARN_PREFIX2} skipping truncated last line in ${path5}
28903
+ `);
28904
+ continue;
28905
+ }
28906
+ throw new Error(`${WARN_PREFIX2} corrupted line ${i + 1} in ${path5}. Run \`dbcli audit clear\` to reset.`);
28907
+ }
28908
+ }
28909
+ return out;
28910
+ }
28911
+ async function readEntries(auditFilePath, opts) {
28912
+ if (opts?.include_rotated === true) {
28913
+ const rotated = await readSingle(`${auditFilePath}.1`);
28914
+ const current = await readSingle(auditFilePath);
28915
+ return rotated.concat(current);
28916
+ }
28917
+ return readSingle(auditFilePath);
28918
+ }
28919
+ async function discoverConnections(auditDir) {
28920
+ let entries;
28921
+ try {
28922
+ entries = await readdir3(auditDir, { withFileTypes: true });
28923
+ } catch (err) {
28924
+ if (err.code === "ENOENT")
28925
+ return [];
28926
+ throw err;
28927
+ }
28928
+ const map = new Map;
28929
+ for (const e of entries) {
28930
+ if (!e.isFile())
28931
+ continue;
28932
+ if (!(e.name.endsWith(".jsonl") || e.name.endsWith(".jsonl.1")))
28933
+ continue;
28934
+ const conn = e.name.replace(/\.jsonl(?:\.1)?$/, "");
28935
+ const full = `${auditDir}/${e.name}`;
28936
+ const list = map.get(conn) ?? [];
28937
+ list.push(full);
28938
+ map.set(conn, list);
28939
+ }
28940
+ for (const files of map.values()) {
28941
+ files.sort((a, b) => Number(b.endsWith(".jsonl.1")) - Number(a.endsWith(".jsonl.1")));
28942
+ }
28943
+ return Array.from(map.entries()).map(([connection, files]) => ({ connection, files })).sort((a, b) => a.connection.localeCompare(b.connection));
28944
+ }
28945
+ function tailEntries(entries, n) {
28946
+ if (n <= 0)
28947
+ return [];
28948
+ return entries.slice().sort((a, b) => a.ts.localeCompare(b.ts)).slice(-n);
28949
+ }
28950
+ function mergeByTimestamp(byConn) {
28951
+ const flat = [];
28952
+ for (const [connection, entries] of byConn) {
28953
+ for (const entry of entries)
28954
+ flat.push({ connection, entry });
28955
+ }
28956
+ flat.sort((a, b) => {
28957
+ const t2 = a.entry.ts.localeCompare(b.entry.ts);
28958
+ return t2 !== 0 ? t2 : a.connection.localeCompare(b.connection);
28959
+ });
28960
+ return flat;
28961
+ }
28962
+ var WARN_PREFIX2 = "[dbcli audit]";
28963
+ var init_reader = () => {};
28964
+
28965
+ // src/core/audit/recent.ts
28966
+ var exports_recent = {};
28967
+ __export(exports_recent, {
28968
+ shouldEmbedRecent: () => shouldEmbedRecent,
28969
+ loadRecentAudit: () => loadRecentAudit,
28970
+ RECENT_AUDIT_DEFAULT_N: () => RECENT_AUDIT_DEFAULT_N
28971
+ });
28972
+ function shouldEmbedRecent(opts) {
28973
+ return opts.forAgent === true || opts.format === "json";
28974
+ }
28975
+ function briefifyForRecent(entry) {
28976
+ return {
28977
+ id: entry.id,
28978
+ ts: entry.ts,
28979
+ command: entry.command,
28980
+ target: entry.target,
28981
+ success: entry.success
28982
+ };
28983
+ }
28984
+ async function loadRecentAudit(config, configPath, n = RECENT_AUDIT_DEFAULT_N) {
28985
+ try {
28986
+ if (config.audit?.enabled === false)
28987
+ return [];
28988
+ const logger = await getAuditLogger(config, configPath);
28989
+ const auditFile = logger.getHealth().currentFile;
28990
+ const entries = await readEntries(auditFile, { include_rotated: true });
28991
+ return tailEntries(entries, n).map(briefifyForRecent);
28992
+ } catch {
28993
+ return [];
28994
+ }
28995
+ }
28996
+ var RECENT_AUDIT_DEFAULT_N = 5;
28997
+ var init_recent = __esm(() => {
28998
+ init_integration_helper();
28999
+ init_reader();
29000
+ });
29001
+
28276
29002
  // node_modules/commander/esm.mjs
28277
29003
  var import__ = __toESM(require_commander(), 1);
28278
29004
  var {
@@ -28291,7 +29017,7 @@ var {
28291
29017
  // package.json
28292
29018
  var package_default = {
28293
29019
  name: "@carllee1983/dbcli",
28294
- version: "1.19.1",
29020
+ version: "1.20.0",
28295
29021
  description: "Database CLI for AI agents",
28296
29022
  type: "module",
28297
29023
  publishConfig: {
@@ -28341,6 +29067,7 @@ var package_default = {
28341
29067
  "test:unit": "bun test tests/unit tests/core",
28342
29068
  "test:integration": "bun test tests/integration",
28343
29069
  "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",
29070
+ "docs:check": "bun run scripts/check-user-docs.ts",
28344
29071
  typecheck: "tsc --noEmit --pretty false",
28345
29072
  "test:perf": "bun test ./tests/perf/*.bench.ts",
28346
29073
  lint: "eslint src tests --ext .ts --max-warnings=0",
@@ -28741,7 +29468,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28741
29468
  schema: v1Config.schema || {},
28742
29469
  schemas: { default: v1Config.schema || {} },
28743
29470
  metadata: v1Config.metadata || { version: "1.0" },
28744
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29471
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29472
+ audit: v1Config.audit
28745
29473
  };
28746
29474
  }
28747
29475
  } else if (await projectConfigFile.exists()) {
@@ -28760,7 +29488,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28760
29488
  schema: v1Config.schema || {},
28761
29489
  schemas: { default: v1Config.schema || {} },
28762
29490
  metadata: v1Config.metadata || { version: "1.0" },
28763
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29491
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29492
+ audit: v1Config.audit
28764
29493
  };
28765
29494
  } else {
28766
29495
  const v1Config = await configModule.read(configPath);
@@ -28776,7 +29505,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28776
29505
  schema: v1Config.schema || {},
28777
29506
  schemas: { default: v1Config.schema || {} },
28778
29507
  metadata: v1Config.metadata || { version: "1.0" },
28779
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29508
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29509
+ audit: v1Config.audit
28780
29510
  };
28781
29511
  }
28782
29512
  } else {
@@ -28797,7 +29527,8 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28797
29527
  schema: v1Config.schema || {},
28798
29528
  schemas: { default: v1Config.schema || {} },
28799
29529
  metadata: v1Config.metadata || { version: "1.0" },
28800
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
29530
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
29531
+ audit: v1Config.audit
28801
29532
  };
28802
29533
  }
28803
29534
  }
@@ -28824,7 +29555,11 @@ async function writeV2InitConfig(configPath, connectionName, connection, permiss
28824
29555
  schema: {},
28825
29556
  schemas: {},
28826
29557
  metadata: { version: "1.0", createdAt: new Date().toISOString() },
28827
- blacklist: { tables: [], columns: {} }
29558
+ blacklist: { tables: [], columns: {} },
29559
+ audit: {
29560
+ enabled: true,
29561
+ rotation: { max_bytes: 10485760, max_entries: 1000 }
29562
+ }
28828
29563
  };
28829
29564
  await writeV2Config(storagePath, v2Config);
28830
29565
  await migrateLegacyProjectEnvLocal(configPath, storagePath);
@@ -30496,6 +31231,7 @@ async function writeSchema(configPath, config, connectionName) {
30496
31231
  // src/commands/query.ts
30497
31232
  init_message_loader();
30498
31233
  init_adapters();
31234
+ import crypto from "crypto";
30499
31235
 
30500
31236
  // src/formatters/html-formatter.ts
30501
31237
  init_package_root();
@@ -30528,7 +31264,7 @@ async function openInBrowser(target) {
30528
31264
 
30529
31265
  // src/commands/query.ts
30530
31266
  import { tmpdir } from "os";
30531
- import { join as join13 } from "path";
31267
+ import { join as join15 } from "path";
30532
31268
 
30533
31269
  // src/core/query-executor.ts
30534
31270
  init_permission_guard();
@@ -30596,20 +31332,30 @@ async function getAllTablesFromAdapter(adapter) {
30596
31332
  }
30597
31333
  }
30598
31334
 
31335
+ // src/core/query-executor.ts
31336
+ init_engine_hints();
31337
+
30599
31338
  // src/core/limits.ts
30600
31339
  var DEFAULT_QUERY_ONLY_LIMIT = 1000;
30601
31340
 
30602
31341
  // src/core/query-executor.ts
31342
+ init_integration_helper();
31343
+
30603
31344
  class QueryExecutor {
30604
31345
  adapter;
30605
31346
  permission;
30606
31347
  blacklistValidator;
30607
- constructor(adapter, permission, blacklistValidator) {
31348
+ config;
31349
+ options;
31350
+ constructor(adapter, permission, blacklistValidator, config, options = {}) {
30608
31351
  this.adapter = adapter;
30609
31352
  this.permission = permission;
30610
31353
  this.blacklistValidator = blacklistValidator;
31354
+ this.config = config;
31355
+ this.options = options;
30611
31356
  }
30612
31357
  async execute(sql, options) {
31358
+ const start = performance.now();
30613
31359
  try {
30614
31360
  const classification = enforcePermission(sql, this.permission);
30615
31361
  if (classification.isDangerous && this.permission === "admin") {
@@ -30627,7 +31373,6 @@ class QueryExecutor {
30627
31373
  this.blacklistValidator.checkTableBlacklist(classification.type, tableName, []);
30628
31374
  }
30629
31375
  }
30630
- const start = performance.now();
30631
31376
  const resultData = await this.adapter.execute(executeSql);
30632
31377
  const executionTimeMs = Math.round(performance.now() - start);
30633
31378
  const rows = resultData.rows;
@@ -30662,8 +31407,28 @@ class QueryExecutor {
30662
31407
  ...securityNotification ? { securityNotification } : {}
30663
31408
  }
30664
31409
  };
31410
+ if (this.config) {
31411
+ await writeAuditEntry(this.config, "query", this.options, {
31412
+ success: true,
31413
+ sql,
31414
+ metadata: {
31415
+ rows_affected: affectedRows,
31416
+ execution_ms: executionTimeMs
31417
+ }
31418
+ });
31419
+ }
30665
31420
  return result;
30666
31421
  } catch (error) {
31422
+ if (this.config) {
31423
+ await writeAuditEntry(this.config, "query", this.options, {
31424
+ success: false,
31425
+ sql,
31426
+ error,
31427
+ metadata: {
31428
+ execution_ms: Math.round(performance.now() - start)
31429
+ }
31430
+ });
31431
+ }
30667
31432
  if (error instanceof PermissionError) {
30668
31433
  throw error;
30669
31434
  }
@@ -30685,25 +31450,6 @@ class QueryExecutor {
30685
31450
  }
30686
31451
  }
30687
31452
  }
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
31453
  function inferColumnType(value) {
30708
31454
  if (value === null || value === undefined) {
30709
31455
  return "null";
@@ -30728,6 +31474,7 @@ init_config();
30728
31474
  init_permission_guard();
30729
31475
  init_blacklist();
30730
31476
  init_validation();
31477
+ init_integration_helper();
30731
31478
  function requireSqlConnection2(connection) {
30732
31479
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
30733
31480
  throw new Error(`This command requires a SQL connection, got: ${connection.system}`);
@@ -30736,6 +31483,7 @@ function requireSqlConnection2(connection) {
30736
31483
  }
30737
31484
  var ALLOWED_FORMATS3 = ["table", "json", "csv"];
30738
31485
  async function queryCommand(sql, options, command) {
31486
+ let config;
30739
31487
  try {
30740
31488
  if (!sql || sql.trim() === "") {
30741
31489
  throw new Error("SQL query required");
@@ -30745,7 +31493,7 @@ async function queryCommand(sql, options, command) {
30745
31493
  }
30746
31494
  sql = sql.trim();
30747
31495
  const configPath = resolveConfigPath(command, options);
30748
- const config = await configModule.read(configPath);
31496
+ config = await configModule.read(configPath);
30749
31497
  if (!config.connection) {
30750
31498
  throw new Error('Run "dbcli init" first');
30751
31499
  }
@@ -30758,7 +31506,8 @@ async function queryCommand(sql, options, command) {
30758
31506
  if (config.connection.system === "elasticsearch") {
30759
31507
  return await elasticsearchQueryBranch(sql, options, config);
30760
31508
  }
30761
- const mainTable = extractMainTable(sql);
31509
+ const { extractTableName: extractTableName2 } = await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints));
31510
+ const mainTable = extractTableName2(sql);
30762
31511
  if (mainTable && config.schema && !options.noLimit) {
30763
31512
  const tableSchema = config.schema[mainTable];
30764
31513
  if (tableSchema) {
@@ -30774,7 +31523,7 @@ async function queryCommand(sql, options, command) {
30774
31523
  try {
30775
31524
  const blacklistManager = new BlacklistManager(config);
30776
31525
  const blacklistValidator = new BlacklistValidator(blacklistManager);
30777
- const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator);
31526
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, options);
30778
31527
  const autoLimit = !options.noLimit;
30779
31528
  const result = await executor3.execute(sql, {
30780
31529
  autoLimit,
@@ -30792,7 +31541,7 @@ async function queryCommand(sql, options, command) {
30792
31541
  rows: result.rows
30793
31542
  });
30794
31543
  if (options.ui) {
30795
- const tempPath = join13(tmpdir(), `dbcli-query-${Date.now()}.html`);
31544
+ const tempPath = join15(tmpdir(), `dbcli-query-${Date.now()}.html`);
30796
31545
  await Bun.write(tempPath, html);
30797
31546
  await openInBrowser(tempPath);
30798
31547
  } else {
@@ -30809,11 +31558,27 @@ async function queryCommand(sql, options, command) {
30809
31558
  await adapter.disconnect();
30810
31559
  }
30811
31560
  } catch (error) {
31561
+ let auditId = null;
31562
+ let envelopeId;
30812
31563
  if (options.recovery === true) {
31564
+ envelopeId = crypto.randomUUID();
31565
+ }
31566
+ if (config) {
31567
+ auditId = await writeAuditEntry(config, "query", options, {
31568
+ success: false,
31569
+ sql,
31570
+ error,
31571
+ ...envelopeId && { recovery_ref: envelopeId }
31572
+ });
31573
+ }
31574
+ if (envelopeId !== undefined) {
30813
31575
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
30814
31576
  emitRecoveryEnvelope2(error, {
30815
31577
  operation: "query",
30816
- table: extractMainTable(sql) ?? undefined
31578
+ table: (await Promise.resolve().then(() => (init_engine_hints(), exports_engine_hints))).extractTableName(sql) ?? undefined
31579
+ }, {
31580
+ envelopeId,
31581
+ auditRef: auditId ?? undefined
30817
31582
  });
30818
31583
  }
30819
31584
  if (error instanceof BlacklistError) {
@@ -30834,21 +31599,17 @@ async function queryCommand(sql, options, command) {
30834
31599
  process.exit(1);
30835
31600
  }
30836
31601
  }
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
31602
  async function mongoQueryBranch(queryStr, options, config) {
30843
31603
  const collection = options.collection;
30844
31604
  const format = options.format ?? "table";
31605
+ if (!collection) {
31606
+ throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
31607
+ }
31608
+ const SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
30845
31609
  if (SQL_PATTERN.test(queryStr)) {
30846
31610
  throw new Error(`\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u8A9E\u6CD5\u3002
30847
31611
  \u7BC4\u4F8B\uFF1Adbcli query '{"field": "value"}' --collection <name>`);
30848
31612
  }
30849
- if (!collection) {
30850
- throw new Error("MongoDB \u67E5\u8A62\u9700\u8981\u6307\u5B9A --collection <name>");
30851
- }
30852
31613
  try {
30853
31614
  JSON.parse(queryStr);
30854
31615
  } catch {
@@ -30881,8 +31642,18 @@ async function mongoQueryBranch(queryStr, options, config) {
30881
31642
  }
30882
31643
  const mongoAdapter = AdapterFactory.createMongoDBAdapter(config.connection);
30883
31644
  await mongoAdapter.connect();
31645
+ const start = performance.now();
30884
31646
  try {
30885
31647
  const result = await mongoAdapter.execute(queryStr, [collection], effectiveLimit !== undefined ? { limit: effectiveLimit } : undefined);
31648
+ const executionTimeMs = Math.round(performance.now() - start);
31649
+ await writeAuditEntry(config, "query", options, {
31650
+ success: true,
31651
+ target: collection,
31652
+ metadata: {
31653
+ rows_affected: result.rows.length,
31654
+ execution_ms: executionTimeMs
31655
+ }
31656
+ });
30886
31657
  const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
30887
31658
  const filterResult = blacklistValidator.filterColumns(collection, result.rows, columnNames);
30888
31659
  const queryResult = {
@@ -30894,8 +31665,8 @@ async function mongoQueryBranch(queryStr, options, config) {
30894
31665
  const output = formatter.format(queryResult, {
30895
31666
  format
30896
31667
  });
30897
- const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
30898
31668
  console.log(output);
31669
+ const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
30899
31670
  if (securityNote) {
30900
31671
  console.log(`
30901
31672
  \u2139 ${securityNote}`);
@@ -30909,15 +31680,24 @@ async function redisQueryBranch(command, options, config) {
30909
31680
  const { enforceRedisPermission: enforceRedisPermission2 } = await Promise.resolve().then(() => (init_permission_guard(), exports_permission_guard));
30910
31681
  const head = command.trim().split(/\s+/)[0]?.toUpperCase() ?? "";
30911
31682
  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/");
31683
+ console.error('\u26A0 Warning: "KEYS" command is dangerous on production servers.');
30915
31684
  }
30916
31685
  enforceRedisPermission2(command, config.permission);
30917
31686
  const redisAdapter = AdapterFactory.createRedisAdapter(config.connection);
30918
31687
  await redisAdapter.connect();
31688
+ const start = performance.now();
30919
31689
  try {
30920
31690
  const result = await redisAdapter.execute(command);
31691
+ const executionTimeMs = Math.round(performance.now() - start);
31692
+ const target = command.trim().split(/\s+/)[1] || "<unknown-key>";
31693
+ await writeAuditEntry(config, "query", options, {
31694
+ success: true,
31695
+ target,
31696
+ metadata: {
31697
+ rows_affected: result.rows.length,
31698
+ execution_ms: executionTimeMs
31699
+ }
31700
+ });
30921
31701
  const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : ["value"];
30922
31702
  const queryResult = {
30923
31703
  rows: result.rows,
@@ -30949,23 +31729,28 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
30949
31729
  if (options.noLimit) {
30950
31730
  effectiveLimit = 1e4;
30951
31731
  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
31732
  } 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
- }
31733
+ effectiveLimit = options.limit || DEFAULT_QUERY_ONLY_LIMIT;
30959
31734
  }
30960
31735
  const blacklistManager = new BlacklistManager(config);
30961
31736
  const blacklistValidator = new BlacklistValidator(blacklistManager);
30962
31737
  blacklistValidator.checkTableBlacklist("SELECT", indexName, []);
30963
31738
  const esAdapter = AdapterFactory.createElasticsearchAdapter(config.connection);
30964
31739
  await esAdapter.connect();
31740
+ const start = performance.now();
30965
31741
  try {
30966
31742
  const result = await esAdapter.execute(queryStr, [indexName], {
30967
31743
  limit: effectiveLimit
30968
31744
  });
31745
+ const executionTimeMs = Math.round(performance.now() - start);
31746
+ await writeAuditEntry(config, "query", options, {
31747
+ success: true,
31748
+ target: indexName,
31749
+ metadata: {
31750
+ rows_affected: result.rows.length,
31751
+ execution_ms: executionTimeMs
31752
+ }
31753
+ });
30969
31754
  const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
30970
31755
  const filterResult = blacklistValidator.filterColumns(indexName, result.rows, columnNames);
30971
31756
  const queryResult = {
@@ -30977,11 +31762,12 @@ async function elasticsearchQueryBranch(queryStr, options, config) {
30977
31762
  const output = formatter.format(queryResult, {
30978
31763
  format
30979
31764
  });
30980
- const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
30981
31765
  console.log(output);
30982
- if (securityNote)
31766
+ const securityNote = blacklistValidator.buildSecurityNotification(indexName, filterResult.omittedColumns);
31767
+ if (securityNote) {
30983
31768
  console.log(`
30984
31769
  \u2139 ${securityNote}`);
31770
+ }
30985
31771
  } finally {
30986
31772
  await esAdapter.disconnect();
30987
31773
  }
@@ -31338,8 +32124,10 @@ function buildSuggestedCommands(facts, gap) {
31338
32124
  // src/commands/plan.ts
31339
32125
  init_config();
31340
32126
  init_validation();
32127
+ init_integration_helper();
31341
32128
  var ALLOWED_FORMATS4 = ["text", "json"];
31342
32129
  async function planCommand(sql, options, command) {
32130
+ let config;
31343
32131
  try {
31344
32132
  if (!sql || sql.trim() === "") {
31345
32133
  throw new Error("SQL statement required");
@@ -31347,7 +32135,7 @@ async function planCommand(sql, options, command) {
31347
32135
  const format = options.format ?? "text";
31348
32136
  validateFormat(format, ALLOWED_FORMATS4, "plan");
31349
32137
  const configPath = resolveConfigPath(command, options);
31350
- const config = await configModule.read(configPath);
32138
+ config = await configModule.read(configPath);
31351
32139
  if (!config.connection) {
31352
32140
  throw new Error('Run "dbcli init" first');
31353
32141
  }
@@ -31363,7 +32151,25 @@ async function planCommand(sql, options, command) {
31363
32151
  }
31364
32152
  });
31365
32153
  console.log(formatPlanResult(result, format));
32154
+ if (config) {
32155
+ await writeAuditEntry(config, "plan", { ...options, plan: true }, {
32156
+ success: true,
32157
+ sql,
32158
+ target: result.targetTables[0] || "*",
32159
+ metadata: {
32160
+ decision: result.decision,
32161
+ risk_factors: result.riskFactors.map((f) => f.code)
32162
+ }
32163
+ });
32164
+ }
31366
32165
  } catch (error) {
32166
+ if (config) {
32167
+ await writeAuditEntry(config, "plan", { ...options, plan: true }, {
32168
+ success: false,
32169
+ sql,
32170
+ error
32171
+ });
32172
+ }
31367
32173
  console.error(error.message);
31368
32174
  process.exit(1);
31369
32175
  }
@@ -31402,10 +32208,11 @@ init_adapters();
31402
32208
  init_config();
31403
32209
  init_blacklist();
31404
32210
  init_permission_guard();
32211
+ init_engine_hints();
31405
32212
  init_saved_queries();
31406
32213
  init_strategies();
31407
32214
  import { tmpdir as tmpdir2 } from "os";
31408
- import { join as join16 } from "path";
32215
+ import { join as join18 } from "path";
31409
32216
  function formatDryRun(input) {
31410
32217
  const lines = ["Dry-run preview (no execution):"];
31411
32218
  if (input.family === "es") {
@@ -31486,7 +32293,7 @@ async function qCommand(name, options, command) {
31486
32293
  rows: filtered.filteredRows
31487
32294
  });
31488
32295
  if (options.ui) {
31489
- const tempPath = join16(tmpdir2(), `dbcli-report-${Date.now()}.html`);
32296
+ const tempPath = join18(tmpdir2(), `dbcli-report-${Date.now()}.html`);
31490
32297
  await Bun.write(tempPath, html);
31491
32298
  await openInBrowser(tempPath);
31492
32299
  } else {
@@ -31568,8 +32375,8 @@ async function handleQError(error, snippetName, useRecovery) {
31568
32375
  // src/commands/queries.ts
31569
32376
  init_message_loader();
31570
32377
  init_config();
31571
- import { mkdir as mkdir6, writeFile as writeFile3 } from "fs/promises";
31572
- import { dirname as dirname6 } from "path";
32378
+ import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
32379
+ import { dirname as dirname8 } from "path";
31573
32380
  import { spawn as spawn2 } from "child_process";
31574
32381
  init_saved_queries();
31575
32382
  init_fold();
@@ -31817,8 +32624,8 @@ async function queriesNew(name, options) {
31817
32624
  process.exit(1);
31818
32625
  return;
31819
32626
  }
31820
- await mkdir6(dirname6(file), { recursive: true });
31821
- await writeFile3(file, scaffold(name), "utf8");
32627
+ await mkdir9(dirname8(file), { recursive: true });
32628
+ await writeFile4(file, scaffold(name), "utf8");
31822
32629
  console.log(`Created ${file}`);
31823
32630
  if (source === "shared")
31824
32631
  console.log(t("queries.first_run_hint"));
@@ -32441,6 +33248,693 @@ function previewDelete(collection, filter) {
32441
33248
  return `db.${collection}.deleteMany(${pretty(filter)})`;
32442
33249
  }
32443
33250
 
33251
+ // src/commands/dml-plan.ts
33252
+ init_config();
33253
+ init_validation();
33254
+
33255
+ // src/core/dml-plan-sql.ts
33256
+ var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
33257
+ function assertIdentifier(value, role) {
33258
+ if (!value || !IDENTIFIER_RE.test(value)) {
33259
+ throw new Error(`Invalid ${role} identifier: ${JSON.stringify(value)}. ` + `${role === "table" ? "Table" : "Column"} names must match /^[A-Za-z_][A-Za-z0-9_]*$/.`);
33260
+ }
33261
+ }
33262
+ function normalizeTable(table) {
33263
+ if (!table || table.trim() === "") {
33264
+ throw new Error("Table name required for plan SQL");
33265
+ }
33266
+ const trimmed = table.trim();
33267
+ assertIdentifier(trimmed, "table");
33268
+ return trimmed;
33269
+ }
33270
+ function normalizeColumns(data, role) {
33271
+ const keys = Object.keys(data);
33272
+ if (keys.length === 0) {
33273
+ throw new Error(role === "data" ? "INSERT plan requires at least one column in --data" : "UPDATE plan requires at least one column in --set");
33274
+ }
33275
+ for (const key of keys) {
33276
+ assertIdentifier(key, "column");
33277
+ }
33278
+ return keys;
33279
+ }
33280
+ function normalizeWhere(where) {
33281
+ const keys = Object.keys(where);
33282
+ if (keys.length === 0) {
33283
+ throw new Error("WHERE clause is required for plan SQL");
33284
+ }
33285
+ for (const key of keys) {
33286
+ assertIdentifier(key, "column");
33287
+ }
33288
+ return keys;
33289
+ }
33290
+ function buildInsertPlanSql(table, data) {
33291
+ const t2 = normalizeTable(table);
33292
+ const columns = normalizeColumns(data, "data");
33293
+ const placeholders = columns.map(() => "?").join(", ");
33294
+ return `INSERT INTO ${t2} (${columns.join(", ")}) VALUES (${placeholders})`;
33295
+ }
33296
+ function buildUpdatePlanSql(table, set, where) {
33297
+ const t2 = normalizeTable(table);
33298
+ const setCols = normalizeColumns(set, "set");
33299
+ const whereCols = normalizeWhere(where);
33300
+ const setClause = setCols.map((col) => `${col} = ?`).join(", ");
33301
+ const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
33302
+ return `UPDATE ${t2} SET ${setClause} WHERE ${whereClause}`;
33303
+ }
33304
+ function buildDeletePlanSql(table, where) {
33305
+ const t2 = normalizeTable(table);
33306
+ const whereCols = normalizeWhere(where);
33307
+ const whereClause = whereCols.map((col) => `${col} = ?`).join(" AND ");
33308
+ return `DELETE FROM ${t2} WHERE ${whereClause}`;
33309
+ }
33310
+
33311
+ // src/utils/where-parser.ts
33312
+ function parseWhereClause(whereClause) {
33313
+ if (!whereClause || whereClause.trim() === "") {
33314
+ throw new Error("WHERE clause cannot be empty");
33315
+ }
33316
+ const conditions = {};
33317
+ const andParts = whereClause.split(/\s+AND\s+/i);
33318
+ for (const part of andParts) {
33319
+ const match = part.match(/^(\w+)\s*=\s*(.+)$/);
33320
+ if (!match) {
33321
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
33322
+ }
33323
+ const column = match[1];
33324
+ const valueStr = match[2];
33325
+ if (valueStr === undefined || column === undefined) {
33326
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
33327
+ }
33328
+ const trimmed = valueStr.trim();
33329
+ const isFullyQuoted = trimmed.length >= 2 && (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"'));
33330
+ if (!isFullyQuoted && /\s+AND\s*$/i.test(trimmed)) {
33331
+ throw new Error(`Cannot parse WHERE clause: "${part}". Use format "column=value" or "col1=val1 AND col2=val2"`);
33332
+ }
33333
+ const stripped = isFullyQuoted ? trimmed.slice(1, -1) : trimmed;
33334
+ let value = stripped;
33335
+ if (stripped !== "" && !isNaN(Number(stripped))) {
33336
+ value = Number(stripped);
33337
+ }
33338
+ if (stripped === "true")
33339
+ value = true;
33340
+ else if (stripped === "false")
33341
+ value = false;
33342
+ else if (stripped === "null")
33343
+ value = null;
33344
+ conditions[column] = value;
33345
+ }
33346
+ return conditions;
33347
+ }
33348
+
33349
+ // src/core/mongo/dml-plan.ts
33350
+ init_permission_guard();
33351
+ var MONGO_UPDATE_OPERATOR_ALLOWLIST = new Set(["$set", "$unset"]);
33352
+ var MONGO_BROAD_FILTER_OPERATORS = new Set([
33353
+ "$regex",
33354
+ "$in",
33355
+ "$nin",
33356
+ "$gt",
33357
+ "$gte",
33358
+ "$lt",
33359
+ "$lte",
33360
+ "$ne",
33361
+ "$exists",
33362
+ "$or",
33363
+ "$and",
33364
+ "$nor",
33365
+ "$not",
33366
+ "$expr",
33367
+ "$text"
33368
+ ]);
33369
+ function permissionSql(operation) {
33370
+ if (operation === "insert")
33371
+ return "INSERT INTO dummy";
33372
+ if (operation === "update")
33373
+ return "UPDATE dummy";
33374
+ return "DELETE FROM dummy";
33375
+ }
33376
+ function broadOperation(operation) {
33377
+ if (operation === "insert")
33378
+ return "INSERT";
33379
+ if (operation === "update")
33380
+ return "UPDATE";
33381
+ return "DELETE";
33382
+ }
33383
+ function pushFactor2(factors, code, severity, message) {
33384
+ if (factors.some((f) => f.code === code && f.message === message))
33385
+ return;
33386
+ factors.push({ code, severity, message });
33387
+ }
33388
+ function decide2(factors) {
33389
+ if (factors.some((f) => f.severity === "block"))
33390
+ return "BLOCK";
33391
+ if (factors.some((f) => f.severity === "warn"))
33392
+ return "WARN";
33393
+ return "ALLOW";
33394
+ }
33395
+ function extractMongoWrittenFields(setDoc) {
33396
+ const fields = new Set;
33397
+ const operators = [];
33398
+ let hasUnsupportedOperator = false;
33399
+ const hasAnyOperator = Object.keys(setDoc).some((k) => k.startsWith("$"));
33400
+ if (!hasAnyOperator) {
33401
+ for (const k of Object.keys(setDoc))
33402
+ fields.add(k);
33403
+ return { fields: Array.from(fields), operators: [], hasUnsupportedOperator: false };
33404
+ }
33405
+ for (const [op, payload] of Object.entries(setDoc)) {
33406
+ operators.push(op);
33407
+ if (!MONGO_UPDATE_OPERATOR_ALLOWLIST.has(op)) {
33408
+ hasUnsupportedOperator = true;
33409
+ continue;
33410
+ }
33411
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
33412
+ for (const k of Object.keys(payload))
33413
+ fields.add(k);
33414
+ }
33415
+ }
33416
+ return { fields: Array.from(fields), operators, hasUnsupportedOperator };
33417
+ }
33418
+ function hasIdEquality(where) {
33419
+ if (!("_id" in where))
33420
+ return false;
33421
+ const value = where._id;
33422
+ if (value === null || value === undefined)
33423
+ return false;
33424
+ if (typeof value === "object")
33425
+ return false;
33426
+ return true;
33427
+ }
33428
+ function hasNonIdEquality(where) {
33429
+ for (const [key, value] of Object.entries(where)) {
33430
+ if (key === "_id")
33431
+ continue;
33432
+ if (key.startsWith("$"))
33433
+ continue;
33434
+ if (value === null)
33435
+ continue;
33436
+ if (typeof value === "object")
33437
+ continue;
33438
+ return true;
33439
+ }
33440
+ return false;
33441
+ }
33442
+ function hasBroadFilter(where) {
33443
+ for (const [key, value] of Object.entries(where)) {
33444
+ if (MONGO_BROAD_FILTER_OPERATORS.has(key))
33445
+ return true;
33446
+ if (value && typeof value === "object" && !Array.isArray(value)) {
33447
+ for (const inner of Object.keys(value)) {
33448
+ if (MONGO_BROAD_FILTER_OPERATORS.has(inner))
33449
+ return true;
33450
+ }
33451
+ }
33452
+ }
33453
+ return false;
33454
+ }
33455
+ function applyPermission(operation, context, factors) {
33456
+ const permResult = checkPermission(permissionSql(operation), context.permission);
33457
+ if (!permResult.allowed) {
33458
+ pushFactor2(factors, "permission_denied", "block", permResult.reason);
33459
+ }
33460
+ }
33461
+ function applyTableBlacklist(target, context, factors) {
33462
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
33463
+ if (blacklisted.includes(target.toLowerCase())) {
33464
+ pushFactor2(factors, "table_blacklisted", "block", `Target collection ${target} is blacklisted.`);
33465
+ }
33466
+ }
33467
+ function applyColumnBlacklist(target, fields, context, factors) {
33468
+ const columns = context.blacklist.columns ?? {};
33469
+ const lower = target.toLowerCase();
33470
+ let blacklistedFields = [];
33471
+ for (const [t2, cols] of Object.entries(columns)) {
33472
+ if (t2.toLowerCase() === lower) {
33473
+ blacklistedFields = cols;
33474
+ break;
33475
+ }
33476
+ }
33477
+ if (blacklistedFields.length === 0)
33478
+ return;
33479
+ for (const field of fields) {
33480
+ if (blacklistedFields.includes(field)) {
33481
+ pushFactor2(factors, "blacklisted_column", "block", `MongoDB write would touch blacklisted field ${target}.${field}.`);
33482
+ }
33483
+ }
33484
+ }
33485
+ function applySchemaCoverage(target, context, factors) {
33486
+ const schemaTables = Object.keys(context.schema);
33487
+ if (schemaTables.length === 0) {
33488
+ pushFactor2(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
33489
+ return;
33490
+ }
33491
+ const known = schemaTables.some((t2) => t2.toLowerCase() === target.toLowerCase());
33492
+ if (!known) {
33493
+ pushFactor2(factors, "schema_table_unknown", "warn", `Target collection ${target} is missing from schema cache.`);
33494
+ }
33495
+ }
33496
+ function buildRecommendations2(factors) {
33497
+ const out = new Set;
33498
+ const codes = new Set(factors.map((f) => f.code));
33499
+ if (codes.has("nonsql_filter_empty")) {
33500
+ out.add("Add an _id equality filter before executing this MongoDB write.");
33501
+ }
33502
+ if (codes.has("nonsql_missing_id")) {
33503
+ out.add("Prefer an _id equality filter before executing this MongoDB write.");
33504
+ }
33505
+ if (codes.has("nonsql_filter_broad")) {
33506
+ out.add("Narrow the filter to an _id equality or a tightly bounded condition.");
33507
+ }
33508
+ if (codes.has("nonsql_unsupported_operator")) {
33509
+ out.add("Restrict the update document to $set / $unset for planner-safe writes.");
33510
+ }
33511
+ if (codes.has("permission_denied")) {
33512
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
33513
+ }
33514
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
33515
+ out.add("Review blacklist rules before accessing sensitive data.");
33516
+ }
33517
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33518
+ out.add("Refresh schema cache for the target collection before executing.");
33519
+ }
33520
+ out.add("Use --dry-run on the actual write command.");
33521
+ return Array.from(out);
33522
+ }
33523
+ function buildSuggestedCommands2(target, factors) {
33524
+ const codes = new Set(factors.map((f) => f.code));
33525
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33526
+ return [`dbcli schema ${target} --format json`];
33527
+ }
33528
+ return [];
33529
+ }
33530
+ var analyzeMongoDmlRisk = (intent, context) => {
33531
+ const factors = [];
33532
+ applyPermission(intent.operation, context, factors);
33533
+ applyTableBlacklist(intent.target, context, factors);
33534
+ if (intent.operation === "insert") {
33535
+ applyColumnBlacklist(intent.target, Object.keys(intent.data), context, factors);
33536
+ } else if (intent.operation === "update") {
33537
+ const { fields, hasUnsupportedOperator } = extractMongoWrittenFields(intent.set);
33538
+ if (hasUnsupportedOperator) {
33539
+ pushFactor2(factors, "nonsql_unsupported_operator", "block", "MongoDB update uses an operator outside the MVP allowlist ($set, $unset).");
33540
+ }
33541
+ applyColumnBlacklist(intent.target, fields, context, factors);
33542
+ const where = intent.where ?? {};
33543
+ if (Object.keys(where).length === 0) {
33544
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB update filter is empty and would match every document.");
33545
+ } else if (!hasIdEquality(where)) {
33546
+ if (hasBroadFilter(where)) {
33547
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter uses a broad operator and may match multiple documents.");
33548
+ } else if (hasNonIdEquality(where)) {
33549
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB update filter does not use _id and may match multiple documents.");
33550
+ } else {
33551
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB update filter is not an _id equality and may match multiple documents.");
33552
+ }
33553
+ }
33554
+ } else {
33555
+ const where = intent.where ?? {};
33556
+ if (Object.keys(where).length === 0) {
33557
+ pushFactor2(factors, "nonsql_filter_empty", "block", "MongoDB delete filter is empty and would match every document.");
33558
+ } else if (!hasIdEquality(where)) {
33559
+ if (hasBroadFilter(where)) {
33560
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter uses a broad operator and may match multiple documents.");
33561
+ } else if (hasNonIdEquality(where)) {
33562
+ pushFactor2(factors, "nonsql_missing_id", "warn", "MongoDB delete filter does not use _id and may match multiple documents.");
33563
+ } else {
33564
+ pushFactor2(factors, "nonsql_filter_broad", "warn", "MongoDB delete filter is not an _id equality and may match multiple documents.");
33565
+ }
33566
+ }
33567
+ }
33568
+ applySchemaCoverage(intent.target, context, factors);
33569
+ return {
33570
+ decision: decide2(factors),
33571
+ operation: broadOperation(intent.operation),
33572
+ targetTables: [intent.target],
33573
+ riskFactors: factors,
33574
+ recommendations: buildRecommendations2(factors),
33575
+ suggestedCommands: buildSuggestedCommands2(intent.target, factors)
33576
+ };
33577
+ };
33578
+
33579
+ // src/core/redis/dml-plan.ts
33580
+ init_permission_guard();
33581
+ function permissionSql2(operation) {
33582
+ if (operation === "insert")
33583
+ return "INSERT INTO dummy";
33584
+ if (operation === "update")
33585
+ return "UPDATE dummy";
33586
+ return "DELETE FROM dummy";
33587
+ }
33588
+ function broadOperation2(operation) {
33589
+ if (operation === "insert")
33590
+ return "INSERT";
33591
+ if (operation === "update")
33592
+ return "UPDATE";
33593
+ return "DELETE";
33594
+ }
33595
+ function pushFactor3(factors, code, severity, message) {
33596
+ if (factors.some((f) => f.code === code && f.message === message))
33597
+ return;
33598
+ factors.push({ code, severity, message });
33599
+ }
33600
+ function decide3(factors) {
33601
+ if (factors.some((f) => f.severity === "block"))
33602
+ return "BLOCK";
33603
+ if (factors.some((f) => f.severity === "warn"))
33604
+ return "WARN";
33605
+ return "ALLOW";
33606
+ }
33607
+ function classifyKey(target) {
33608
+ if (target === "*")
33609
+ return "all";
33610
+ if (target.includes("*") || target.includes("?") || /\[.+\]/.test(target))
33611
+ return "pattern";
33612
+ return "single";
33613
+ }
33614
+ function applyPermission2(operation, context, factors) {
33615
+ const result = checkPermission(permissionSql2(operation), context.permission);
33616
+ if (!result.allowed) {
33617
+ pushFactor3(factors, "permission_denied", "block", result.reason);
33618
+ }
33619
+ }
33620
+ function applyTableBlacklist2(target, context, factors) {
33621
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
33622
+ if (blacklisted.includes(target.toLowerCase())) {
33623
+ pushFactor3(factors, "table_blacklisted", "block", `Redis key ${target} is blacklisted.`);
33624
+ }
33625
+ }
33626
+ function applyColumnBlacklist2(target, fields, context, factors) {
33627
+ const columns = context.blacklist.columns ?? {};
33628
+ const lower = target.toLowerCase();
33629
+ let blacklistedFields = [];
33630
+ for (const [t2, cols] of Object.entries(columns)) {
33631
+ if (t2.toLowerCase() === lower) {
33632
+ blacklistedFields = cols;
33633
+ break;
33634
+ }
33635
+ }
33636
+ if (blacklistedFields.length === 0)
33637
+ return;
33638
+ for (const field of fields) {
33639
+ if (blacklistedFields.includes(field)) {
33640
+ pushFactor3(factors, "blacklisted_column", "block", `Redis write would touch blacklisted field ${target}.${field}.`);
33641
+ }
33642
+ }
33643
+ }
33644
+ function redisSchemaKeyMatches(schemaKey, target) {
33645
+ const lowerKey = schemaKey.toLowerCase();
33646
+ const lowerTarget = target.toLowerCase();
33647
+ if (lowerKey === lowerTarget)
33648
+ return true;
33649
+ if (!lowerKey.includes("*") && !lowerKey.includes("?"))
33650
+ return false;
33651
+ const pattern = lowerKey.split("").map((ch) => {
33652
+ if (ch === "*")
33653
+ return ".*";
33654
+ if (ch === "?")
33655
+ return ".";
33656
+ return ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
33657
+ }).join("");
33658
+ return new RegExp(`^${pattern}$`).test(lowerTarget);
33659
+ }
33660
+ function applySchemaCoverage2(target, context, factors) {
33661
+ const tables = Object.keys(context.schema);
33662
+ if (tables.length === 0) {
33663
+ pushFactor3(factors, "schema_cache_missing", "warn", "Schema/key metadata is missing for the selected Redis connection.");
33664
+ return;
33665
+ }
33666
+ const known = tables.some((t2) => redisSchemaKeyMatches(t2, target));
33667
+ if (!known) {
33668
+ pushFactor3(factors, "schema_table_unknown", "warn", `Redis key namespace ${target} is missing from cached metadata.`);
33669
+ }
33670
+ }
33671
+ function buildRecommendations3(factors) {
33672
+ const out = new Set;
33673
+ const codes = new Set(factors.map((f) => f.code));
33674
+ if (codes.has("nonsql_key_pattern_broad")) {
33675
+ out.add("Specify a concrete single key instead of a wildcard pattern.");
33676
+ }
33677
+ if (codes.has("nonsql_overwrite_unknown")) {
33678
+ out.add("Provide --set field information so the planner can verify the write target.");
33679
+ }
33680
+ if (codes.has("permission_denied")) {
33681
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
33682
+ }
33683
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
33684
+ out.add("Review blacklist rules before accessing sensitive data.");
33685
+ }
33686
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33687
+ out.add("Refresh schema cache for the target Redis namespace before executing.");
33688
+ }
33689
+ out.add("Use --dry-run on the actual write command.");
33690
+ return Array.from(out);
33691
+ }
33692
+ function buildSuggestedCommands3(target, factors) {
33693
+ const codes = new Set(factors.map((f) => f.code));
33694
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33695
+ return [`dbcli schema ${target} --format json`];
33696
+ }
33697
+ return [];
33698
+ }
33699
+ var analyzeRedisDmlRisk = (intent, context) => {
33700
+ const factors = [];
33701
+ applyPermission2(intent.operation, context, factors);
33702
+ applyTableBlacklist2(intent.target, context, factors);
33703
+ const keyClass = classifyKey(intent.target);
33704
+ if (keyClass === "all") {
33705
+ pushFactor3(factors, "nonsql_key_pattern_broad", "block", 'Redis key "*" matches every key and is unsafe for planner-level writes.');
33706
+ } else if (keyClass === "pattern") {
33707
+ pushFactor3(factors, "nonsql_key_pattern_broad", "warn", `Redis target ${intent.target} contains wildcard characters and may match multiple keys.`);
33708
+ }
33709
+ if (intent.operation === "insert") {
33710
+ applyColumnBlacklist2(intent.target, Object.keys(intent.data), context, factors);
33711
+ } else if (intent.operation === "update") {
33712
+ const fields = Object.keys(intent.set);
33713
+ if (fields.length === 0) {
33714
+ pushFactor3(factors, "nonsql_overwrite_unknown", "warn", "Redis update has no field information and overwrite impact cannot be verified.");
33715
+ }
33716
+ applyColumnBlacklist2(intent.target, fields, context, factors);
33717
+ }
33718
+ applySchemaCoverage2(intent.target, context, factors);
33719
+ return {
33720
+ decision: decide3(factors),
33721
+ operation: broadOperation2(intent.operation),
33722
+ targetTables: [intent.target],
33723
+ riskFactors: factors,
33724
+ recommendations: buildRecommendations3(factors),
33725
+ suggestedCommands: buildSuggestedCommands3(intent.target, factors)
33726
+ };
33727
+ };
33728
+
33729
+ // src/core/elasticsearch/dml-plan.ts
33730
+ init_permission_guard();
33731
+ function permissionSql3(operation) {
33732
+ if (operation === "insert")
33733
+ return "INSERT INTO dummy";
33734
+ if (operation === "update")
33735
+ return "UPDATE dummy";
33736
+ return "DELETE FROM dummy";
33737
+ }
33738
+ function broadOperation3(operation) {
33739
+ if (operation === "insert")
33740
+ return "INSERT";
33741
+ if (operation === "update")
33742
+ return "UPDATE";
33743
+ return "DELETE";
33744
+ }
33745
+ function pushFactor4(factors, code, severity, message) {
33746
+ if (factors.some((f) => f.code === code && f.message === message))
33747
+ return;
33748
+ factors.push({ code, severity, message });
33749
+ }
33750
+ function decide4(factors) {
33751
+ if (factors.some((f) => f.severity === "block"))
33752
+ return "BLOCK";
33753
+ if (factors.some((f) => f.severity === "warn"))
33754
+ return "WARN";
33755
+ return "ALLOW";
33756
+ }
33757
+ function hasIdEquality2(where) {
33758
+ if (!where)
33759
+ return false;
33760
+ if (!("_id" in where))
33761
+ return false;
33762
+ const value = where._id;
33763
+ if (value === null || value === undefined)
33764
+ return false;
33765
+ if (typeof value === "object")
33766
+ return false;
33767
+ return true;
33768
+ }
33769
+ function applyPermission3(operation, context, factors) {
33770
+ const result = checkPermission(permissionSql3(operation), context.permission);
33771
+ if (!result.allowed) {
33772
+ pushFactor4(factors, "permission_denied", "block", result.reason);
33773
+ }
33774
+ }
33775
+ function applyTableBlacklist3(target, context, factors) {
33776
+ const blacklisted = (context.blacklist.tables ?? []).map((t2) => t2.toLowerCase());
33777
+ if (blacklisted.includes(target.toLowerCase())) {
33778
+ pushFactor4(factors, "table_blacklisted", "block", `Elasticsearch index ${target} is blacklisted.`);
33779
+ }
33780
+ }
33781
+ function applyColumnBlacklist3(target, fields, context, factors) {
33782
+ const columns = context.blacklist.columns ?? {};
33783
+ const lower = target.toLowerCase();
33784
+ let blacklistedFields = [];
33785
+ for (const [t2, cols] of Object.entries(columns)) {
33786
+ if (t2.toLowerCase() === lower) {
33787
+ blacklistedFields = cols;
33788
+ break;
33789
+ }
33790
+ }
33791
+ if (blacklistedFields.length === 0)
33792
+ return;
33793
+ for (const field of fields) {
33794
+ if (blacklistedFields.includes(field)) {
33795
+ pushFactor4(factors, "blacklisted_column", "block", `Elasticsearch write would touch blacklisted field ${target}.${field}.`);
33796
+ }
33797
+ }
33798
+ }
33799
+ function applySchemaCoverage3(target, context, factors) {
33800
+ const tables = Object.keys(context.schema);
33801
+ if (tables.length === 0) {
33802
+ pushFactor4(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected Elasticsearch connection.");
33803
+ return;
33804
+ }
33805
+ const known = tables.some((t2) => t2.toLowerCase() === target.toLowerCase());
33806
+ if (!known) {
33807
+ pushFactor4(factors, "schema_table_unknown", "warn", `Target index ${target} is missing from schema cache.`);
33808
+ }
33809
+ }
33810
+ function buildRecommendations4(factors) {
33811
+ const out = new Set;
33812
+ const codes = new Set(factors.map((f) => f.code));
33813
+ if (codes.has("nonsql_missing_id")) {
33814
+ out.add("Add an _id equality filter (or include _id in the document) before executing.");
33815
+ }
33816
+ if (codes.has("nonsql_unsupported_bulk")) {
33817
+ out.add("Use a document-level write instead of a bulk or by-query operation.");
33818
+ }
33819
+ if (codes.has("permission_denied")) {
33820
+ out.add("Switch to a connection with sufficient permission only if the operation is intended.");
33821
+ }
33822
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
33823
+ out.add("Review blacklist rules before accessing sensitive data.");
33824
+ }
33825
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33826
+ out.add("Refresh schema cache for the target index before executing.");
33827
+ }
33828
+ out.add("Use --dry-run on the actual write command.");
33829
+ return Array.from(out);
33830
+ }
33831
+ function buildSuggestedCommands4(target, factors) {
33832
+ const codes = new Set(factors.map((f) => f.code));
33833
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown")) {
33834
+ return [`dbcli schema ${target} --format json`];
33835
+ }
33836
+ return [];
33837
+ }
33838
+ var analyzeElasticsearchDmlRisk = (intent, context) => {
33839
+ const factors = [];
33840
+ applyPermission3(intent.operation, context, factors);
33841
+ applyTableBlacklist3(intent.target, context, factors);
33842
+ if (intent.operation === "insert") {
33843
+ const fields = Object.keys(intent.data);
33844
+ applyColumnBlacklist3(intent.target, fields, context, factors);
33845
+ if (intent.data._id === undefined || intent.data._id === null || intent.data._id === "") {
33846
+ pushFactor4(factors, "nonsql_missing_id", "warn", "Elasticsearch insert has no _id; auto-id generation will make later correlation harder.");
33847
+ }
33848
+ } else if (intent.operation === "update") {
33849
+ applyColumnBlacklist3(intent.target, Object.keys(intent.set), context, factors);
33850
+ if (!hasIdEquality2(intent.where)) {
33851
+ pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch update requires _id in --where for planner-safe document-level update.");
33852
+ }
33853
+ } else {
33854
+ if (!hasIdEquality2(intent.where)) {
33855
+ pushFactor4(factors, "nonsql_missing_id", "block", "Elasticsearch delete requires _id in --where for planner-safe document-level deletion.");
33856
+ }
33857
+ }
33858
+ applySchemaCoverage3(intent.target, context, factors);
33859
+ return {
33860
+ decision: decide4(factors),
33861
+ operation: broadOperation3(intent.operation),
33862
+ targetTables: [intent.target],
33863
+ riskFactors: factors,
33864
+ recommendations: buildRecommendations4(factors),
33865
+ suggestedCommands: buildSuggestedCommands4(intent.target, factors)
33866
+ };
33867
+ };
33868
+
33869
+ // src/commands/dml-plan.ts
33870
+ var ALLOWED_FORMATS5 = ["text", "json"];
33871
+ var SQL_SYSTEMS = new Set(["postgresql", "mysql", "mariadb"]);
33872
+ function buildPlanSqlForIntent(intent) {
33873
+ if (intent.operation === "insert") {
33874
+ return buildInsertPlanSql(intent.target, intent.data);
33875
+ }
33876
+ if (intent.operation === "update") {
33877
+ if (!intent.where || Object.keys(intent.where).length === 0) {
33878
+ throw new Error("WHERE clause is required for SQL update plan");
33879
+ }
33880
+ return buildUpdatePlanSql(intent.target, intent.set, intent.where);
33881
+ }
33882
+ if (!intent.where || Object.keys(intent.where).length === 0) {
33883
+ throw new Error("WHERE clause is required for SQL delete plan");
33884
+ }
33885
+ return buildDeletePlanSql(intent.target, intent.where);
33886
+ }
33887
+ async function runDmlPlanAnalysis(intent, options, command) {
33888
+ try {
33889
+ const format = options.format ?? "text";
33890
+ validateFormat(format, ALLOWED_FORMATS5, "plan");
33891
+ const configPath = resolveConfigPath(command, options);
33892
+ const config = await configModule.read(configPath);
33893
+ if (!config.connection) {
33894
+ throw new Error('Run "dbcli init" to configure database connection');
33895
+ }
33896
+ const system = config.connection.system;
33897
+ const schema = config.schema ?? {};
33898
+ const blacklist = config.blacklist ?? { tables: [], columns: {} };
33899
+ if (SQL_SYSTEMS.has(system)) {
33900
+ const planSql = buildPlanSqlForIntent(intent);
33901
+ const schemaTableCount = Object.keys(schema).length;
33902
+ const result = analyzeQueryRisk({
33903
+ sql: planSql,
33904
+ permission: config.permission,
33905
+ blacklist,
33906
+ schemaLookup: {
33907
+ tables: schema,
33908
+ cacheAvailable: schemaTableCount > 0
33909
+ }
33910
+ });
33911
+ console.log(formatPlanResult(result, format));
33912
+ return;
33913
+ }
33914
+ const context = {
33915
+ permission: config.permission,
33916
+ blacklist,
33917
+ schema
33918
+ };
33919
+ if (system === "mongodb") {
33920
+ console.log(formatPlanResult(analyzeMongoDmlRisk(intent, context), format));
33921
+ return;
33922
+ }
33923
+ if (system === "redis") {
33924
+ console.log(formatPlanResult(analyzeRedisDmlRisk(intent, context), format));
33925
+ return;
33926
+ }
33927
+ if (system === "elasticsearch") {
33928
+ console.log(formatPlanResult(analyzeElasticsearchDmlRisk(intent, context), format));
33929
+ return;
33930
+ }
33931
+ throw new Error(`--plan does not support connection system "${system}" yet`);
33932
+ } catch (error) {
33933
+ console.error(error.message);
33934
+ process.exit(1);
33935
+ }
33936
+ }
33937
+
32444
33938
  // src/commands/insert.ts
32445
33939
  function requireSqlConnection3(connection) {
32446
33940
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
@@ -32492,6 +33986,15 @@ async function insertCommand(table, options, command) {
32492
33986
  if (!data || typeof data !== "object" || Array.isArray(data)) {
32493
33987
  throw new Error('JSON must be an object (e.g. {"name":"Alice","email":"a@b.com"})');
32494
33988
  }
33989
+ if (options.plan) {
33990
+ if (options.dryRun) {
33991
+ console.error("--plan cannot be used with --dry-run");
33992
+ process.exit(1);
33993
+ return;
33994
+ }
33995
+ await runDmlPlanAnalysis({ operation: "insert", target: table, data }, { format: options.format, config: options.config }, command);
33996
+ return;
33997
+ }
32495
33998
  const configPath = resolveConfigPath(command, options);
32496
33999
  const config = await configModule.read(configPath);
32497
34000
  if (!config.connection) {
@@ -32654,45 +34157,13 @@ function requireSqlConnection4(connection) {
32654
34157
  }
32655
34158
  return connection;
32656
34159
  }
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
34160
  async function updateCommand(table, options, command) {
32690
34161
  try {
32691
34162
  if (!table || table.trim() === "") {
32692
34163
  throw new Error("Table name required");
32693
34164
  }
32694
34165
  table = table.trim();
32695
- if (!options.where || options.where.trim() === "") {
34166
+ if (!options.plan && (!options.where || options.where.trim() === "")) {
32696
34167
  throw new Error('UPDATE requires --where clause (e.g. --where "id=1")');
32697
34168
  }
32698
34169
  if (!options.set || options.set.trim() === "") {
@@ -32712,6 +34183,40 @@ async function updateCommand(table, options, command) {
32712
34183
  if (!setData || typeof setData !== "object" || Array.isArray(setData)) {
32713
34184
  throw new Error('JSON in --set must be an object (e.g. {"name":"Bob","email":"b@example.com"})');
32714
34185
  }
34186
+ if (options.plan) {
34187
+ if (options.dryRun) {
34188
+ console.error("--plan cannot be used with --dry-run");
34189
+ process.exit(1);
34190
+ return;
34191
+ }
34192
+ const rawWhere = options.where ?? "";
34193
+ let whereForPlan = null;
34194
+ if (rawWhere.trim() !== "") {
34195
+ try {
34196
+ const parsed = JSON.parse(rawWhere);
34197
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
34198
+ whereForPlan = parsed;
34199
+ }
34200
+ } catch {
34201
+ whereForPlan = null;
34202
+ }
34203
+ if (whereForPlan === null) {
34204
+ try {
34205
+ whereForPlan = parseWhereClause(rawWhere);
34206
+ } catch {
34207
+ whereForPlan = {};
34208
+ }
34209
+ }
34210
+ }
34211
+ await runDmlPlanAnalysis({
34212
+ operation: "update",
34213
+ target: table,
34214
+ set: setData,
34215
+ where: whereForPlan,
34216
+ rawWhere
34217
+ }, { format: options.format, config: options.config }, command);
34218
+ return;
34219
+ }
32715
34220
  if (config.connection?.system === "redis") {
32716
34221
  enforcePermission("UPDATE dummy", config.permission);
32717
34222
  const blacklistManager = new BlacklistManager(config);
@@ -32894,41 +34399,13 @@ function requireSqlConnection5(connection) {
32894
34399
  }
32895
34400
  return connection;
32896
34401
  }
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
34402
  async function deleteCommand(table, options, command) {
32926
34403
  try {
32927
34404
  if (!table || table.trim() === "") {
32928
34405
  throw new Error("Table name required");
32929
34406
  }
32930
34407
  table = table.trim();
32931
- if (!options.where || options.where.trim() === "") {
34408
+ if (!options.plan && (!options.where || options.where.trim() === "")) {
32932
34409
  throw new Error('DELETE requires --where clause (e.g. --where "id=1")');
32933
34410
  }
32934
34411
  const configPath = resolveConfigPath(command, options);
@@ -32936,6 +34413,39 @@ async function deleteCommand(table, options, command) {
32936
34413
  if (!config.connection) {
32937
34414
  throw new Error('Run "dbcli init" to configure database connection');
32938
34415
  }
34416
+ if (options.plan) {
34417
+ if (options.dryRun) {
34418
+ console.error("--plan cannot be used with --dry-run");
34419
+ process.exit(1);
34420
+ return;
34421
+ }
34422
+ const rawWhere = options.where ?? "";
34423
+ let whereForPlan = null;
34424
+ if (rawWhere.trim() !== "") {
34425
+ try {
34426
+ const parsed = JSON.parse(rawWhere);
34427
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
34428
+ whereForPlan = parsed;
34429
+ }
34430
+ } catch {
34431
+ whereForPlan = null;
34432
+ }
34433
+ if (whereForPlan === null) {
34434
+ try {
34435
+ whereForPlan = parseWhereClause(rawWhere);
34436
+ } catch {
34437
+ whereForPlan = {};
34438
+ }
34439
+ }
34440
+ }
34441
+ await runDmlPlanAnalysis({
34442
+ operation: "delete",
34443
+ target: table,
34444
+ where: whereForPlan,
34445
+ rawWhere
34446
+ }, { format: options.format, config: options.config }, command);
34447
+ return;
34448
+ }
32939
34449
  if (config.permission !== "data-admin" && config.permission !== "admin") {
32940
34450
  throw new PermissionError(t("delete.admin_only"), {
32941
34451
  type: "DELETE",
@@ -32953,7 +34463,7 @@ async function deleteCommand(table, options, command) {
32953
34463
  try {
32954
34464
  filter = JSON.parse(options.where);
32955
34465
  } catch {
32956
- filter = parseWhereClause2(options.where);
34466
+ filter = parseWhereClause(options.where);
32957
34467
  }
32958
34468
  if (options.dryRun) {
32959
34469
  const output = {
@@ -33001,7 +34511,7 @@ async function deleteCommand(table, options, command) {
33001
34511
  try {
33002
34512
  filter = JSON.parse(options.where);
33003
34513
  } catch {
33004
- filter = parseWhereClause2(options.where);
34514
+ filter = parseWhereClause(options.where);
33005
34515
  }
33006
34516
  if (options.dryRun) {
33007
34517
  const output = {
@@ -33032,7 +34542,7 @@ async function deleteCommand(table, options, command) {
33032
34542
  }
33033
34543
  let whereConditions;
33034
34544
  try {
33035
- whereConditions = parseWhereClause2(options.where);
34545
+ whereConditions = parseWhereClause(options.where);
33036
34546
  } catch (error) {
33037
34547
  throw new Error(`WHERE clause parsing failed: ${error.message}`);
33038
34548
  }
@@ -33115,7 +34625,7 @@ function requireSqlConnection6(connection) {
33115
34625
  }
33116
34626
  return connection;
33117
34627
  }
33118
- var SQL_PATTERN2 = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
34628
+ var SQL_PATTERN = /^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SHOW|DESCRIBE)\b/i;
33119
34629
  async function exportCommand(sql, options, command) {
33120
34630
  try {
33121
34631
  if (!sql || sql.trim() === "") {
@@ -33260,7 +34770,7 @@ async function redisExportBranch(command, options, config) {
33260
34770
  }
33261
34771
  }
33262
34772
  async function mongoExportBranch(query, options, config) {
33263
- if (SQL_PATTERN2.test(query)) {
34773
+ if (SQL_PATTERN.test(query)) {
33264
34774
  console.error("\u9019\u662F MongoDB \u9023\u7DDA\uFF0C\u8ACB\u4F7F\u7528 JSON filter \u6216 aggregation pipeline\u3002");
33265
34775
  console.error(`\u7BC4\u4F8B\uFF1Adbcli export '{"status":"open"}' --collection orders --format jsonl`);
33266
34776
  process.exit(1);
@@ -33380,6 +34890,11 @@ import * as path4 from "path";
33380
34890
  import { homedir as homedir2 } from "os";
33381
34891
  var SKILL_SOURCE_PATH = packageAssetPath("SKILL.md");
33382
34892
  var REFERENCE_SOURCE_PATH = packageAssetPath("reference.md");
34893
+ function resolveSkillSource(lang) {
34894
+ if (lang === "zh-TW")
34895
+ return packageAssetPath("SKILL.zh-TW.md");
34896
+ return packageAssetPath("SKILL.md");
34897
+ }
33383
34898
  var SUPPORTED_PLATFORMS = [
33384
34899
  "claude",
33385
34900
  "gemini",
@@ -33390,9 +34905,11 @@ var SUPPORTED_PLATFORMS = [
33390
34905
  ];
33391
34906
  async function skillCommand(_program, options) {
33392
34907
  try {
33393
- const skillFile = Bun.file(SKILL_SOURCE_PATH);
34908
+ const lang = options.lang ?? "en";
34909
+ const skillSourcePath = resolveSkillSource(lang);
34910
+ const skillFile = Bun.file(skillSourcePath);
33394
34911
  if (!await skillFile.exists()) {
33395
- throw new Error(`Skill source not found: ${SKILL_SOURCE_PATH}`);
34912
+ throw new Error(`Skill source not found: ${skillSourcePath}`);
33396
34913
  }
33397
34914
  const refFile = Bun.file(REFERENCE_SOURCE_PATH);
33398
34915
  if (!await refFile.exists()) {
@@ -33480,15 +34997,15 @@ async function ensureDir(dirPath) {
33480
34997
  await $`mkdir -p ${dirPath}`.quiet();
33481
34998
  } catch {
33482
34999
  try {
33483
- const { mkdir: mkdir7 } = await import("fs/promises");
33484
- await mkdir7(dirPath, { recursive: true });
35000
+ const { mkdir: mkdir10 } = await import("fs/promises");
35001
+ await mkdir10(dirPath, { recursive: true });
33485
35002
  } catch {
33486
35003
  throw new Error(`Cannot create directory: ${dirPath}`);
33487
35004
  }
33488
35005
  }
33489
35006
  }
33490
35007
  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) => {
35008
+ 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
35009
  try {
33493
35010
  await skillCommand(program2, options);
33494
35011
  } catch (error) {
@@ -33512,15 +35029,15 @@ class AgentTaskError extends Error {
33512
35029
  }
33513
35030
  // src/core/agent-tasks/task-paths.ts
33514
35031
  init_package_root();
33515
- import { join as join19 } from "path";
35032
+ import { join as join21 } from "path";
33516
35033
  function resolveBuiltinDir2() {
33517
35034
  return packageAssetPath("tasks");
33518
35035
  }
33519
35036
  function resolveAgentTaskDirs(workspaceRoot) {
33520
35037
  return {
33521
35038
  builtinDir: resolveBuiltinDir2(),
33522
- sharedDir: join19(workspaceRoot, ".dbcli-shared", "tasks"),
33523
- localDir: join19(workspaceRoot, ".dbcli", "tasks")
35039
+ sharedDir: join21(workspaceRoot, ".dbcli-shared", "tasks"),
35040
+ localDir: join21(workspaceRoot, ".dbcli", "tasks")
33524
35041
  };
33525
35042
  }
33526
35043
  // src/core/agent-tasks/parser.ts
@@ -33658,7 +35175,7 @@ function parseSteps(value, input) {
33658
35175
  }
33659
35176
  // src/core/agent-tasks/loader.ts
33660
35177
  import { readdir as readdir2 } from "fs/promises";
33661
- import { join as join20, relative as relative2, sep as sep2 } from "path";
35178
+ import { join as join22, relative as relative2, sep as sep2 } from "path";
33662
35179
  async function loadAgentTasks(opts, flags) {
33663
35180
  const errors3 = [];
33664
35181
  const builtin = await walkAndParse2(opts.builtinDir, "builtin", errors3);
@@ -33715,7 +35232,7 @@ async function safeCollectMd(root) {
33715
35232
  return;
33716
35233
  }
33717
35234
  for (const e of entries) {
33718
- const full = join20(dir, e.name);
35235
+ const full = join22(dir, e.name);
33719
35236
  if (e.isDirectory())
33720
35237
  await walk(full);
33721
35238
  else
@@ -34303,11 +35820,11 @@ function requireSqlConnection7(connection) {
34303
35820
  }
34304
35821
  return connection;
34305
35822
  }
34306
- var ALLOWED_FORMATS5 = ["json", "table"];
35823
+ var ALLOWED_FORMATS6 = ["json", "table"];
34307
35824
  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
35825
  async function checkAction(table, options) {
34309
35826
  try {
34310
- validateFormat(options.format, ALLOWED_FORMATS5, "check");
35827
+ validateFormat(options.format, ALLOWED_FORMATS6, "check");
34311
35828
  const config = await configModule.read(options.config);
34312
35829
  if (!config.connection) {
34313
35830
  console.error("Database not configured. Run: dbcli init");
@@ -34440,7 +35957,7 @@ function requireSqlConnection8(connection) {
34440
35957
  }
34441
35958
  return connection;
34442
35959
  }
34443
- var ALLOWED_FORMATS6 = ["json", "table"];
35960
+ var ALLOWED_FORMATS7 = ["json", "table"];
34444
35961
  function compareSnapshots(before, after) {
34445
35962
  const beforeTables = new Set(Object.keys(before.tables));
34446
35963
  const afterTables = new Set(Object.keys(after.tables));
@@ -34534,7 +36051,7 @@ function compareSnapshots(before, after) {
34534
36051
  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
36052
  async function diffAction(options) {
34536
36053
  try {
34537
- validateFormat(options.format, ALLOWED_FORMATS6, "diff");
36054
+ validateFormat(options.format, ALLOWED_FORMATS7, "diff");
34538
36055
  if (!options.snapshot && !options.against) {
34539
36056
  console.error("Specify --snapshot <path> to save, or --against <path> to compare");
34540
36057
  process.exit(1);
@@ -34643,10 +36160,10 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
34643
36160
  // src/commands/status.ts
34644
36161
  init_config();
34645
36162
  init_validation();
34646
- var ALLOWED_FORMATS7 = ["text", "json"];
36163
+ var ALLOWED_FORMATS8 = ["text", "json"];
34647
36164
  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
36165
  try {
34649
- validateFormat(options.format, ALLOWED_FORMATS7, "status");
36166
+ validateFormat(options.format, ALLOWED_FORMATS8, "status");
34650
36167
  const configPath = resolveConfigPath(statusCommand);
34651
36168
  const config = await configModule.read(configPath);
34652
36169
  if (!config.connection) {
@@ -34681,16 +36198,21 @@ var statusCommand = new Command("status").description("Show current configuratio
34681
36198
 
34682
36199
  // src/commands/inspect.ts
34683
36200
  init_message_loader();
36201
+ import crypto2 from "crypto";
34684
36202
  init_validation();
34685
36203
  init_inspect();
34686
- var ALLOWED_FORMATS8 = ["json", "markdown"];
36204
+ init_config();
36205
+ init_integration_helper();
36206
+ var ALLOWED_FORMATS9 = ["json", "markdown"];
34687
36207
  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) => {
36208
+ let config;
34688
36209
  try {
34689
36210
  const forAgent = options.forAgent === true;
34690
36211
  const format = forAgent ? "json" : options.format;
34691
36212
  const brief = forAgent || options.brief === true;
34692
- validateFormat(format, ALLOWED_FORMATS8, "inspect");
36213
+ validateFormat(format, ALLOWED_FORMATS9, "inspect");
34693
36214
  const configPath = resolveConfigPath(command, options);
36215
+ config = await configModule.read(configPath);
34694
36216
  const snap = await collectInspect({
34695
36217
  workspace: process.cwd(),
34696
36218
  configPath,
@@ -34702,12 +36224,40 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
34702
36224
  const { requireSchemaCacheOrThrow: requireSchemaCacheOrThrow2 } = await Promise.resolve().then(() => (init_inspect(), exports_inspect));
34703
36225
  requireSchemaCacheOrThrow2(snap.schemaCache, snap.system ?? null);
34704
36226
  }
36227
+ if (config) {
36228
+ const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
36229
+ if (shouldEmbedRecent2({ forAgent, format })) {
36230
+ snap.audit_recent = await loadRecentAudit2(config, configPath);
36231
+ }
36232
+ }
34705
36233
  const out = format === "markdown" ? renderMarkdown2(snap, { brief }) : renderJson2(snap, { brief });
34706
36234
  console.log(out);
36235
+ if (config) {
36236
+ await writeAuditEntry(config, "inspect", options, {
36237
+ success: true,
36238
+ target: "*"
36239
+ });
36240
+ }
34707
36241
  } catch (err) {
36242
+ let auditId = null;
36243
+ let envelopeId;
34708
36244
  if (options.recovery === true) {
36245
+ envelopeId = crypto2.randomUUID();
36246
+ }
36247
+ if (config) {
36248
+ auditId = await writeAuditEntry(config, "inspect", options, {
36249
+ success: false,
36250
+ target: "*",
36251
+ error: err,
36252
+ ...envelopeId && { recovery_ref: envelopeId }
36253
+ });
36254
+ }
36255
+ if (envelopeId !== undefined) {
34709
36256
  const { emitRecoveryEnvelope: emitRecoveryEnvelope2 } = await Promise.resolve().then(() => (init_recovery(), exports_recovery));
34710
- emitRecoveryEnvelope2(err, { operation: "inspect" });
36257
+ emitRecoveryEnvelope2(err, { operation: "inspect" }, {
36258
+ envelopeId,
36259
+ auditRef: auditId ?? undefined
36260
+ });
34711
36261
  }
34712
36262
  console.error(err.message);
34713
36263
  process.exit(1);
@@ -34717,6 +36267,8 @@ var inspectCommand = new Command().name("inspect").description(t("inspect.descri
34717
36267
  // src/commands/report.ts
34718
36268
  init_message_loader();
34719
36269
  init_validation();
36270
+ init_config();
36271
+ init_integration_helper();
34720
36272
 
34721
36273
  // src/core/report/types.ts
34722
36274
  var REPORT_SCHEMA_VERSION = 1;
@@ -34971,10 +36523,10 @@ function finalize(input) {
34971
36523
  context: input.context,
34972
36524
  sections: input.sections,
34973
36525
  warnings: input.warnings,
34974
- suggestedCommands: buildSuggestedCommands2(input.context, input.sections)
36526
+ suggestedCommands: buildSuggestedCommands5(input.context, input.sections)
34975
36527
  };
34976
36528
  }
34977
- function buildSuggestedCommands2(context, sections) {
36529
+ function buildSuggestedCommands5(context, sections) {
34978
36530
  if (!context.system)
34979
36531
  return ["dbcli init"];
34980
36532
  const out = ["dbcli inspect --format json"];
@@ -35092,7 +36644,7 @@ function formatCell(value) {
35092
36644
  return String(value);
35093
36645
  }
35094
36646
  // src/commands/report.ts
35095
- var ALLOWED_FORMATS9 = ["json", "markdown"];
36647
+ var ALLOWED_FORMATS10 = ["json", "markdown"];
35096
36648
  function parseSections(value) {
35097
36649
  const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
35098
36650
  for (const p of parts) {
@@ -35103,12 +36655,14 @@ function parseSections(value) {
35103
36655
  return parts;
35104
36656
  }
35105
36657
  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) => {
36658
+ let config;
35106
36659
  try {
35107
36660
  const forAgent = options.forAgent === true;
35108
36661
  const format = forAgent ? "json" : options.format;
35109
36662
  const brief = forAgent || options.brief === true;
35110
- validateFormat(format, ALLOWED_FORMATS9, "report");
36663
+ validateFormat(format, ALLOWED_FORMATS10, "report");
35111
36664
  const configPath = resolveConfigPath(command, options);
36665
+ config = await configModule.read(configPath);
35112
36666
  const snap = await collectReport({
35113
36667
  workspace: process.cwd(),
35114
36668
  configPath,
@@ -35121,7 +36675,23 @@ var reportCommand = new Command().name("report").description(t("report.descripti
35121
36675
  });
35122
36676
  const out = format === "markdown" ? renderMarkdown3(snap, { brief }) : renderJson3(snap, { brief });
35123
36677
  console.log(out);
36678
+ if (config) {
36679
+ await writeAuditEntry(config, "report", options, {
36680
+ success: true,
36681
+ target: "*",
36682
+ metadata: {
36683
+ sections: options.section || ALLOWED_SECTIONS
36684
+ }
36685
+ });
36686
+ }
35124
36687
  } catch (err) {
36688
+ if (config) {
36689
+ await writeAuditEntry(config, "report", options, {
36690
+ success: false,
36691
+ target: "*",
36692
+ error: err
36693
+ });
36694
+ }
35125
36695
  console.error(err.message);
35126
36696
  process.exit(1);
35127
36697
  }
@@ -35130,6 +36700,8 @@ var reportCommand = new Command().name("report").description(t("report.descripti
35130
36700
  // src/commands/guide.ts
35131
36701
  init_message_loader();
35132
36702
  init_validation();
36703
+ init_config();
36704
+ init_integration_helper();
35133
36705
 
35134
36706
  // src/core/guide/types.ts
35135
36707
  var GUIDE_SCHEMA_VERSION = 1;
@@ -35471,7 +37043,7 @@ function renderGoalList() {
35471
37043
  `);
35472
37044
  }
35473
37045
  // src/commands/guide.ts
35474
- var ALLOWED_FORMATS10 = ["json", "markdown"];
37046
+ var ALLOWED_FORMATS11 = ["json", "markdown"];
35475
37047
  function parseGoal(value) {
35476
37048
  const normalized = value.trim();
35477
37049
  if (!ALLOWED_GOALS.includes(normalized)) {
@@ -35480,11 +37052,12 @@ function parseGoal(value) {
35480
37052
  return normalized;
35481
37053
  }
35482
37054
  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) => {
37055
+ let config;
35483
37056
  try {
35484
37057
  const forAgent = options.forAgent === true;
35485
37058
  const format = forAgent ? "json" : options.format;
35486
37059
  const brief = forAgent || options.brief === true;
35487
- validateFormat(format, ALLOWED_FORMATS10, "guide");
37060
+ validateFormat(format, ALLOWED_FORMATS11, "guide");
35488
37061
  if (options.list === true) {
35489
37062
  if (format === "markdown") {
35490
37063
  console.log(renderGoalList());
@@ -35503,6 +37076,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
35503
37076
  }
35504
37077
  const validated = parseGoal(goal);
35505
37078
  const configPath = resolveConfigPath(command, options);
37079
+ config = await configModule.read(configPath);
35506
37080
  const snap = await collectGuide({
35507
37081
  workspace: process.cwd(),
35508
37082
  configPath,
@@ -35511,9 +37085,28 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
35511
37085
  brief,
35512
37086
  probeTimeoutMs: options.probeTimeout
35513
37087
  });
37088
+ if (config) {
37089
+ const { shouldEmbedRecent: shouldEmbedRecent2, loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
37090
+ if (shouldEmbedRecent2({ forAgent, format })) {
37091
+ snap.audit_recent = await loadRecentAudit2(config, configPath);
37092
+ }
37093
+ }
35514
37094
  const out = format === "markdown" ? renderMarkdown4(snap, { brief }) : renderJson4(snap, { brief });
35515
37095
  console.log(out);
37096
+ if (config) {
37097
+ await writeAuditEntry(config, "guide", options, {
37098
+ success: true,
37099
+ target: goal
37100
+ });
37101
+ }
35516
37102
  } catch (err) {
37103
+ if (config) {
37104
+ await writeAuditEntry(config, "guide", options, {
37105
+ success: false,
37106
+ target: goal || "*",
37107
+ error: err
37108
+ });
37109
+ }
35517
37110
  console.error(err.message);
35518
37111
  process.exit(1);
35519
37112
  }
@@ -35523,7 +37116,7 @@ var guideCommand = new Command().name("guide").description(t("guide.description"
35523
37116
  init_message_loader();
35524
37117
  init_validation();
35525
37118
  init_recovery();
35526
- var ALLOWED_FORMATS11 = ["json", "markdown"];
37119
+ var ALLOWED_FORMATS12 = ["json", "markdown"];
35527
37120
  function parseCode(value) {
35528
37121
  const normalized = value.trim();
35529
37122
  if (!RECOVERY_CODES.includes(normalized)) {
@@ -35559,7 +37152,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
35559
37152
  const forAgent = options.forAgent === true;
35560
37153
  const format = forAgent ? "json" : options.format;
35561
37154
  const brief = forAgent || options.brief === true;
35562
- validateFormat(format, ALLOWED_FORMATS11, "recovery");
37155
+ validateFormat(format, ALLOWED_FORMATS12, "recovery");
35563
37156
  if (options.list === true) {
35564
37157
  if (format === "markdown") {
35565
37158
  console.log(renderCodeList());
@@ -35600,7 +37193,7 @@ var recoveryCommand = new Command().name("recovery").description(t("recovery.des
35600
37193
  // src/commands/recover.ts
35601
37194
  init_recovery();
35602
37195
  init_last_envelope();
35603
- import { stat as stat4 } from "fs/promises";
37196
+ import { stat as stat6 } from "fs/promises";
35604
37197
  import { resolve as resolve4 } from "path";
35605
37198
 
35606
37199
  // src/core/recovery/envelope-schema.ts
@@ -35652,6 +37245,8 @@ var recoveryEnvelopeSchema = exports_external.object({
35652
37245
  }).strict();
35653
37246
  var savedRecoveryEnvelopeSchema = exports_external.object({
35654
37247
  schemaVersion: exports_external.literal(1),
37248
+ id: exports_external.string().optional(),
37249
+ audit_ref: exports_external.string().optional(),
35655
37250
  savedAt: exports_external.string().min(1),
35656
37251
  command: exports_external.string(),
35657
37252
  cwd: exports_external.string().min(1),
@@ -35682,7 +37277,7 @@ function looksLikeSavedEnvelope(x) {
35682
37277
  // src/commands/recover.ts
35683
37278
  init_next_step_schema();
35684
37279
  init_validation();
35685
- var ALLOWED_FORMATS12 = ["json", "markdown"];
37280
+ var ALLOWED_FORMATS13 = ["json", "markdown"];
35686
37281
  var ALLOWED_TIERS = ["readonly-cmd", "write-cmd"];
35687
37282
 
35688
37283
  class RecoverCliError extends Error {
@@ -35716,7 +37311,7 @@ async function resolveApplySource(opts) {
35716
37311
  }
35717
37312
  const saved2 = r3.value;
35718
37313
  try {
35719
- await stat4(saved2.cwd);
37314
+ await stat6(saved2.cwd);
35720
37315
  } catch {
35721
37316
  throw new RecoverCliError(`--from ${opts.from}: saved cwd '${saved2.cwd}' no longer exists.`, EXIT_CODE.malformed);
35722
37317
  }
@@ -35750,7 +37345,7 @@ async function resolveApplySource(opts) {
35750
37345
  }
35751
37346
  const saved = r.value;
35752
37347
  try {
35753
- await stat4(saved.cwd);
37348
+ await stat6(saved.cwd);
35754
37349
  } catch {
35755
37350
  throw new RecoverCliError(`Auto-saved ${LAST_ENVELOPE_PATH} references cwd '${saved.cwd}' that no longer exists.`, EXIT_CODE.malformed);
35756
37351
  }
@@ -35824,7 +37419,7 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35824
37419
  }
35825
37420
  const explicitFormat = options.format;
35826
37421
  const format = explicitFormat ?? (options.apply === true || options.next === true ? "json" : "markdown");
35827
- validateFormat(format, ALLOWED_FORMATS12, "recover");
37422
+ validateFormat(format, ALLOWED_FORMATS13, "recover");
35828
37423
  let allowWrite = "none";
35829
37424
  if (options.next !== true) {
35830
37425
  const allowWriteRaw = options.allowWrite;
@@ -35839,15 +37434,32 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35839
37434
  from: options.from,
35840
37435
  cwd: process.cwd()
35841
37436
  });
37437
+ let audit_recent = [];
37438
+ if (format === "json" && options.next !== true) {
37439
+ try {
37440
+ const { configModule: configModule2 } = await Promise.resolve().then(() => (init_config(), exports_config));
37441
+ const configPath = `${process.cwd()}/.dbcli`;
37442
+ const config = await configModule2.read(configPath);
37443
+ if (config) {
37444
+ const { loadRecentAudit: loadRecentAudit2 } = await Promise.resolve().then(() => (init_recent(), exports_recent));
37445
+ audit_recent = await loadRecentAudit2(config, configPath);
37446
+ }
37447
+ } catch {
37448
+ audit_recent = [];
37449
+ }
37450
+ }
35842
37451
  if (options.next === true) {
35843
37452
  const result2 = await runNext(options, source);
35844
- const out2 = format === "markdown" ? renderNextMarkdown(result2) : renderNextJson(result2);
35845
- console.log(out2);
37453
+ const out = format === "markdown" ? renderNextMarkdown(result2) : renderNextJson(result2);
37454
+ console.log(out);
35846
37455
  return;
35847
37456
  }
35848
37457
  if (options.apply !== true) {
35849
- const out2 = format === "markdown" ? renderMarkdown(source.envelope) : renderJson(source.envelope);
35850
- console.log(out2);
37458
+ if (format === "markdown") {
37459
+ console.log(renderMarkdown(source.envelope));
37460
+ } else {
37461
+ console.log(JSON.stringify({ ...source.envelope, audit_recent }, null, 2));
37462
+ }
35851
37463
  return;
35852
37464
  }
35853
37465
  const noVerify = options.verify === false;
@@ -35856,8 +37468,11 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35856
37468
  cwd: source.cwd,
35857
37469
  source: { kind: source.kind, path: source.path }
35858
37470
  }, { allowWrite, noVerify });
35859
- const out = format === "markdown" ? renderApplyMarkdown(result) : renderApplyJson(result);
35860
- console.log(out);
37471
+ if (format === "markdown") {
37472
+ console.log(renderApplyMarkdown(result));
37473
+ } else {
37474
+ console.log(JSON.stringify({ ...result, audit_recent }, null, 2));
37475
+ }
35861
37476
  process.exit(exitCodeFor(result.finalStatus));
35862
37477
  } catch (err) {
35863
37478
  if (err instanceof RecoverCliError) {
@@ -35869,6 +37484,440 @@ var recoverCommand = new Command().name("recover").description("Inspect or apply
35869
37484
  }
35870
37485
  });
35871
37486
 
37487
+ // src/commands/audit.ts
37488
+ import { rm as rm3, stat as stat7 } from "fs/promises";
37489
+ import { join as join25 } from "path";
37490
+ init_message_loader();
37491
+ init_validation();
37492
+ init_config();
37493
+ init_config_binding();
37494
+ init_reader();
37495
+ init_integration_helper();
37496
+ var ALLOWED_FORMATS14 = ["table", "json"];
37497
+ var DEFAULT_TAIL_N = 10;
37498
+ var MAX_TAIL_N = 1e4;
37499
+ var SHORT_ID_LEN = 8;
37500
+ var MISSING_PLACEHOLDER = "\u2014";
37501
+ var PREFIX_MIN = 4;
37502
+ async function resolveAuditPaths(configPath, config) {
37503
+ const storagePath = await resolveConfigStoragePath(configPath);
37504
+ const connName = config.effectiveConnectionName || getGlobalConnectionName() || "default";
37505
+ const auditDir = join25(storagePath, ".dbcli", "audit");
37506
+ const auditFile = join25(auditDir, `${connName}.jsonl`);
37507
+ return { auditDir, connectionName: connName, auditFile };
37508
+ }
37509
+ function isAuditDisabled(config) {
37510
+ return config?.audit?.enabled === false;
37511
+ }
37512
+ function emitDisabledAndExit0() {
37513
+ console.error(t("audit.disabled_hint"));
37514
+ process.exit(0);
37515
+ }
37516
+ function parseTailN(raw) {
37517
+ const requested = typeof raw === "number" ? raw : parseInt(String(raw ?? ""), 10);
37518
+ if (!Number.isFinite(requested) || !Number.isInteger(requested) || requested <= 0) {
37519
+ console.error(t("audit.n_must_be_positive"));
37520
+ process.exit(1);
37521
+ }
37522
+ if (requested > MAX_TAIL_N) {
37523
+ console.error(t_vars("audit.n_capped_warning", {
37524
+ requested: String(requested),
37525
+ max: String(MAX_TAIL_N)
37526
+ }));
37527
+ return MAX_TAIL_N;
37528
+ }
37529
+ return requested;
37530
+ }
37531
+ function briefify(entry) {
37532
+ return {
37533
+ ts: entry.ts,
37534
+ command: entry.command,
37535
+ target: entry.target,
37536
+ success: entry.success
37537
+ };
37538
+ }
37539
+ function shortId(id) {
37540
+ if (!id)
37541
+ return MISSING_PLACEHOLDER;
37542
+ return id.length <= SHORT_ID_LEN ? id : id.slice(0, SHORT_ID_LEN);
37543
+ }
37544
+ function renderTable(rows, headers) {
37545
+ const allRows = [headers, ...rows];
37546
+ const widths = headers.map((_, col) => Math.max(...allRows.map((r) => (r[col] ?? "").length)));
37547
+ const fmt = (r) => r.map((cell, i) => (cell ?? "").padEnd(widths[i] ?? 0)).join(" ").trimEnd();
37548
+ const sep3 = widths.map((w) => "-".repeat(w)).join(" ").trimEnd();
37549
+ return [fmt(headers), sep3, ...rows.map(fmt)].join(`
37550
+ `);
37551
+ }
37552
+ function renderTailTable(entries) {
37553
+ const headers = ["ts", "command", "target", "tier", "success", "id", "recovery_ref"];
37554
+ const rows = entries.map((e) => [
37555
+ e.ts,
37556
+ e.command,
37557
+ e.target,
37558
+ e.side_effect_tier,
37559
+ String(e.success),
37560
+ shortId(e.id),
37561
+ shortId(e.recovery_ref)
37562
+ ]);
37563
+ return renderTable(rows, headers);
37564
+ }
37565
+ function renderTailAllTable(envelopes) {
37566
+ const headers = ["connection", "ts", "command", "target", "tier", "success", "id", "recovery_ref"];
37567
+ const rows = envelopes.map(({ connection, entry: e }) => [
37568
+ connection,
37569
+ e.ts,
37570
+ e.command,
37571
+ e.target,
37572
+ e.side_effect_tier,
37573
+ String(e.success),
37574
+ shortId(e.id),
37575
+ shortId(e.recovery_ref)
37576
+ ]);
37577
+ return renderTable(rows, headers);
37578
+ }
37579
+ function briefifyShow(entry) {
37580
+ const { metadata, redacted_query, ...rest } = entry;
37581
+ return rest;
37582
+ }
37583
+ function renderEntryTable(entry) {
37584
+ const lines = [];
37585
+ if (entry.id !== undefined)
37586
+ lines.push(`Id: ${entry.id}`);
37587
+ if (entry.ts !== undefined)
37588
+ lines.push(`Ts: ${entry.ts}`);
37589
+ if (entry.session_id !== undefined)
37590
+ lines.push(`Session id: ${entry.session_id}`);
37591
+ if (entry.engine !== undefined)
37592
+ lines.push(`Engine: ${entry.engine}`);
37593
+ if (entry.command !== undefined)
37594
+ lines.push(`Command: ${entry.command}`);
37595
+ if (entry.side_effect_tier !== undefined)
37596
+ lines.push(`Side effect tier: ${entry.side_effect_tier}`);
37597
+ if (entry.target !== undefined)
37598
+ lines.push(`Target: ${entry.target}`);
37599
+ if (entry.success !== undefined)
37600
+ lines.push(`Success: ${entry.success}`);
37601
+ if (entry.recovery_ref !== undefined)
37602
+ lines.push(`Recovery ref: ${entry.recovery_ref}`);
37603
+ if (entry.redacted_query !== undefined)
37604
+ lines.push(`Redacted query: ${entry.redacted_query}`);
37605
+ if (entry.redacted_sql !== undefined)
37606
+ lines.push(`Redacted SQL: ${entry.redacted_sql}`);
37607
+ if (entry.error !== undefined)
37608
+ lines.push(`Error: ${entry.error}`);
37609
+ if (entry.metadata !== undefined)
37610
+ lines.push(`Metadata: ${JSON.stringify(entry.metadata)}`);
37611
+ return lines.join(`
37612
+ `);
37613
+ }
37614
+ function renderShowResult(hit, opts) {
37615
+ if (opts.format === "json") {
37616
+ const entryView = opts.brief ? briefifyShow(hit.entry) : hit.entry;
37617
+ const payload = opts.all ? { connection: hit.connection, entry: entryView } : entryView;
37618
+ console.log(JSON.stringify(payload, null, 2));
37619
+ } else {
37620
+ if (opts.all)
37621
+ console.log(`Connection: ${hit.connection}`);
37622
+ console.log(renderEntryTable(opts.brief ? briefifyShow(hit.entry) : hit.entry));
37623
+ }
37624
+ }
37625
+ function briefifyHealth(h) {
37626
+ return { enabled: h.enabled, lastWrite: h.lastWrite, rotationUsage: h.rotationUsage };
37627
+ }
37628
+ function formatBytes(n) {
37629
+ if (n < 1024)
37630
+ return `${n} B`;
37631
+ if (n < 1024 * 1024)
37632
+ return `${(n / 1024).toFixed(1)} KB`;
37633
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
37634
+ }
37635
+ function renderHealthTable(h) {
37636
+ const sizePct = Math.round(h.rotationUsage.bytes.pct);
37637
+ const entriesPct = Math.round(h.rotationUsage.entries.pct);
37638
+ const lastWrite = h.lastWrite ? `${h.lastWrite.ts} (${h.lastWrite.success ? "success" : "failed"}${h.lastWrite.error ? `: ${h.lastWrite.error}` : ""})` : MISSING_PLACEHOLDER;
37639
+ const lastError = h.lastError ? `${h.lastError.ts} ${h.lastError.message}` : MISSING_PLACEHOLDER;
37640
+ const lastRotation = h.rotation.lastRotatedAt ? `${h.rotation.lastRotatedAt} (${h.rotation.previousFile ?? MISSING_PLACEHOLDER})` : MISSING_PLACEHOLDER;
37641
+ return [
37642
+ `Enabled: ${h.enabled}`,
37643
+ `File: ${h.currentFile}`,
37644
+ `Size: ${formatBytes(h.currentSizeBytes)} / ${formatBytes(h.rotationUsage.bytes.max)} (${sizePct}%)`,
37645
+ `Entries: ${h.currentEntryCount} / ${h.rotationUsage.entries.max} (${entriesPct}%)`,
37646
+ `Lock: ${h.lock.state}${h.lock.heldByPid !== undefined ? ` (pid ${h.lock.heldByPid})` : ""}`,
37647
+ `Last write: ${lastWrite}`,
37648
+ `Last error: ${lastError}`,
37649
+ `Session id: ${h.sessionId ?? MISSING_PLACEHOLDER}`,
37650
+ `Last rotation: ${lastRotation}`
37651
+ ].join(`
37652
+ `);
37653
+ }
37654
+ function readLineFromStdinWithStderrPrompt(prompt) {
37655
+ return new Promise((resolve5) => {
37656
+ process.stderr.write(prompt);
37657
+ const chunks = [];
37658
+ let data = "";
37659
+ const onData = (chunk) => {
37660
+ chunks.push(chunk);
37661
+ data = Buffer.concat(chunks).toString();
37662
+ const lines = data.split(`
37663
+ `);
37664
+ if (lines.length > 1) {
37665
+ process.stdin.pause();
37666
+ process.stdin.removeListener("data", onData);
37667
+ process.stdin.removeListener("end", onEnd);
37668
+ resolve5((lines[0] ?? "").trim());
37669
+ }
37670
+ };
37671
+ const onEnd = () => {
37672
+ process.stdin.removeListener("data", onData);
37673
+ resolve5(data.trim());
37674
+ };
37675
+ process.stdin.on("data", onData);
37676
+ process.stdin.on("end", onEnd);
37677
+ process.stdin.resume();
37678
+ });
37679
+ }
37680
+ async function statAuditFile(file) {
37681
+ try {
37682
+ const s = await stat7(file);
37683
+ const entries = (await readEntries(file)).length;
37684
+ return { entries, size: formatBytes(s.size) };
37685
+ } catch {
37686
+ return null;
37687
+ }
37688
+ }
37689
+ var auditCommand = new Command("audit").description(t("audit.description"));
37690
+ 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) => {
37691
+ const forAgent = options.forAgent === true;
37692
+ const format = forAgent ? "json" : options.format;
37693
+ const briefSource = command.getOptionValueSource("brief");
37694
+ const briefExplicit = briefSource !== undefined && briefSource !== "default";
37695
+ const brief = briefExplicit ? options.brief === true : forAgent;
37696
+ validateFormat(format, ALLOWED_FORMATS14, "audit tail");
37697
+ const n = parseTailN(options.n);
37698
+ const configPath = resolveConfigPath(command, options);
37699
+ const config = await configModule.read(configPath);
37700
+ if (isAuditDisabled(config))
37701
+ emitDisabledAndExit0();
37702
+ const { auditDir, auditFile } = await resolveAuditPaths(configPath, config);
37703
+ if (options.all === true) {
37704
+ const conns = await discoverConnections(auditDir);
37705
+ const byConn = new Map;
37706
+ for (const c of conns) {
37707
+ const merged = [];
37708
+ for (const f of c.files) {
37709
+ const part = await readEntries(f);
37710
+ merged.push(...part);
37711
+ }
37712
+ byConn.set(c.connection, merged);
37713
+ }
37714
+ const envelopes = mergeByTimestamp(byConn).slice(-n);
37715
+ if (format === "json") {
37716
+ const payload = envelopes.map((e) => ({
37717
+ connection: e.connection,
37718
+ entry: brief ? briefify(e.entry) : e.entry
37719
+ }));
37720
+ console.log(JSON.stringify(payload, null, 2));
37721
+ } else if (envelopes.length === 0) {
37722
+ console.error(t("audit.no_entries"));
37723
+ } else {
37724
+ console.log(renderTailAllTable(envelopes));
37725
+ }
37726
+ return;
37727
+ }
37728
+ const entries = await readEntries(auditFile, { include_rotated: true });
37729
+ const tail = tailEntries(entries, n);
37730
+ if (format === "json") {
37731
+ const payload = brief ? tail.map(briefify) : tail;
37732
+ console.log(JSON.stringify(payload, null, 2));
37733
+ } else if (tail.length === 0) {
37734
+ console.error(t("audit.no_entries"));
37735
+ } else {
37736
+ console.log(renderTailTable(tail));
37737
+ }
37738
+ });
37739
+ 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) => {
37740
+ if (id && options.recoveryRef) {
37741
+ console.error(t("audit.show_mutex_violation"));
37742
+ process.exit(1);
37743
+ }
37744
+ if (!id && !options.recoveryRef) {
37745
+ console.error(t("audit.show_mutex_violation"));
37746
+ process.exit(1);
37747
+ }
37748
+ const forAgent = options.forAgent === true;
37749
+ const format = forAgent ? "json" : options.format;
37750
+ const briefSource = command.getOptionValueSource("brief");
37751
+ const briefExplicit = briefSource !== undefined && briefSource !== "default";
37752
+ const brief = briefExplicit ? options.brief === true : forAgent;
37753
+ validateFormat(format, ALLOWED_FORMATS14, "audit show");
37754
+ const configPath = resolveConfigPath(command, options);
37755
+ const config = await configModule.read(configPath);
37756
+ if (isAuditDisabled(config))
37757
+ emitDisabledAndExit0();
37758
+ const { auditDir, auditFile, connectionName } = await resolveAuditPaths(configPath, config);
37759
+ const all = options.all === true;
37760
+ if (options.recoveryRef) {
37761
+ const ref = String(options.recoveryRef);
37762
+ const matches2 = [];
37763
+ if (all) {
37764
+ const conns = await discoverConnections(auditDir);
37765
+ for (const c of conns) {
37766
+ for (const f of c.files) {
37767
+ for (const e of await readEntries(f)) {
37768
+ if (e.recovery_ref === ref)
37769
+ matches2.push({ connection: c.connection, entry: e });
37770
+ }
37771
+ }
37772
+ }
37773
+ } else {
37774
+ for (const e of await readEntries(auditFile, { include_rotated: true })) {
37775
+ if (e.recovery_ref === ref)
37776
+ matches2.push({ connection: connectionName, entry: e });
37777
+ }
37778
+ }
37779
+ if (matches2.length === 0) {
37780
+ console.error(t_vars("audit.show_recovery_no_match", { ref }));
37781
+ process.exit(1);
37782
+ }
37783
+ if (matches2.length > 1) {
37784
+ console.error(t_vars("audit.show_recovery_ambiguous", {
37785
+ ref,
37786
+ count: String(matches2.length)
37787
+ }));
37788
+ process.exit(1);
37789
+ }
37790
+ renderShowResult(matches2[0], { all, format, brief });
37791
+ return;
37792
+ }
37793
+ const lookup = String(id);
37794
+ if (lookup.length < PREFIX_MIN) {
37795
+ console.error(t("audit.show_prefix_too_short"));
37796
+ process.exit(1);
37797
+ }
37798
+ const matches = [];
37799
+ if (all) {
37800
+ const conns = await discoverConnections(auditDir);
37801
+ for (const c of conns) {
37802
+ for (const f of c.files) {
37803
+ for (const e of await readEntries(f)) {
37804
+ if (e.id === lookup || e.id.startsWith(lookup))
37805
+ matches.push({ connection: c.connection, entry: e });
37806
+ }
37807
+ }
37808
+ }
37809
+ } else {
37810
+ for (const e of await readEntries(auditFile, { include_rotated: true })) {
37811
+ if (e.id === lookup || e.id.startsWith(lookup))
37812
+ matches.push({ connection: connectionName, entry: e });
37813
+ }
37814
+ }
37815
+ if (matches.length === 0) {
37816
+ console.error(t_vars("audit.show_no_match", { prefix: lookup }));
37817
+ process.exit(1);
37818
+ }
37819
+ if (matches.length > 1) {
37820
+ console.error(t_vars("audit.show_ambiguous", {
37821
+ prefix: lookup,
37822
+ count: String(matches.length)
37823
+ }));
37824
+ process.exit(1);
37825
+ }
37826
+ renderShowResult(matches[0], { all, format, brief });
37827
+ });
37828
+ auditCommand.command("clear").description(t("audit.clear.description")).option("--yes", "Skip confirmation prompt and delete immediately", false).action(async (options, command) => {
37829
+ const configPath = resolveConfigPath(command, options);
37830
+ const config = await configModule.read(configPath);
37831
+ const { auditFile, connectionName } = await resolveAuditPaths(configPath, config);
37832
+ const rotatedFile = `${auditFile}.1`;
37833
+ const lockFile = `${auditFile}.lock`;
37834
+ const currentInfo = await statAuditFile(auditFile);
37835
+ const rotatedInfo = await statAuditFile(rotatedFile);
37836
+ if (currentInfo === null && rotatedInfo === null) {
37837
+ process.stderr.write(t("audit.clear.summary_nothing") + `
37838
+ `);
37839
+ process.exit(0);
37840
+ }
37841
+ if (options.yes !== true) {
37842
+ if (!process.stdin.isTTY) {
37843
+ console.error(t("audit.clear.requires_tty_or_yes"));
37844
+ process.exit(1);
37845
+ }
37846
+ process.stderr.write(t_vars("audit.clear.prompt_header", { conn: connectionName }) + `
37847
+ `);
37848
+ if (currentInfo) {
37849
+ process.stderr.write(t_vars("audit.clear.prompt_file_line", {
37850
+ file: auditFile,
37851
+ entries: String(currentInfo.entries),
37852
+ size: currentInfo.size
37853
+ }) + `
37854
+ `);
37855
+ }
37856
+ if (rotatedInfo) {
37857
+ process.stderr.write(t_vars("audit.clear.prompt_file_line", {
37858
+ file: rotatedFile,
37859
+ entries: String(rotatedInfo.entries),
37860
+ size: rotatedInfo.size
37861
+ }) + `
37862
+ `);
37863
+ }
37864
+ const answer = await readLineFromStdinWithStderrPrompt(t("audit.clear.prompt_continue"));
37865
+ const lower = answer.toLowerCase();
37866
+ const proceed = lower === "y" || lower === "yes";
37867
+ if (!proceed) {
37868
+ process.stderr.write(t("audit.clear.summary_nothing") + `
37869
+ `);
37870
+ process.exit(0);
37871
+ }
37872
+ }
37873
+ let cleared = 0;
37874
+ try {
37875
+ if (currentInfo)
37876
+ cleared += currentInfo.entries;
37877
+ if (rotatedInfo)
37878
+ cleared += rotatedInfo.entries;
37879
+ await rm3(auditFile, { force: true });
37880
+ await rm3(rotatedFile, { force: true });
37881
+ await rm3(lockFile, { force: true });
37882
+ } catch (err) {
37883
+ const msg = err instanceof Error ? err.message : String(err);
37884
+ process.stderr.write(t_vars("audit.clear.summary_failed", { message: msg }) + `
37885
+ `);
37886
+ process.exit(1);
37887
+ }
37888
+ process.stderr.write(t_vars("audit.clear.summary_cleared", {
37889
+ count: String(cleared),
37890
+ conn: connectionName
37891
+ }) + `
37892
+ `);
37893
+ process.exit(0);
37894
+ });
37895
+ 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) => {
37896
+ const forAgent = options.forAgent === true;
37897
+ const format = forAgent ? "json" : options.format;
37898
+ const briefSource = command.getOptionValueSource("brief");
37899
+ const briefExplicit = briefSource !== undefined && briefSource !== "default";
37900
+ const brief = briefExplicit ? options.brief === true : forAgent;
37901
+ validateFormat(format, ALLOWED_FORMATS14, "audit health");
37902
+ const configPath = resolveConfigPath(command, options);
37903
+ const config = await configModule.read(configPath);
37904
+ const logger = await getAuditLogger(config, configPath);
37905
+ const health = logger.getHealth();
37906
+ if (format === "json") {
37907
+ const payload = brief ? briefifyHealth(health) : health;
37908
+ console.log(JSON.stringify(payload, null, 2));
37909
+ } else if (brief) {
37910
+ const sizePct = Math.round(health.rotationUsage.bytes.pct);
37911
+ const entriesPct = Math.round(health.rotationUsage.entries.pct);
37912
+ const lastWriteLine = health.lastWrite ? `${health.lastWrite.ts} (${health.lastWrite.success ? "success" : "failed"})` : MISSING_PLACEHOLDER;
37913
+ console.log(`Enabled: ${health.enabled}`);
37914
+ console.log(`Last write: ${lastWriteLine}`);
37915
+ console.log(`Rotation usage: ${sizePct}% bytes, ${entriesPct}% entries`);
37916
+ } else {
37917
+ console.log(renderHealthTable(health));
37918
+ }
37919
+ });
37920
+
35872
37921
  // src/commands/doctor.ts
35873
37922
  init_colors();
35874
37923
  init_config();
@@ -35880,7 +37929,8 @@ init_config_v2();
35880
37929
  init_config_binding();
35881
37930
  init_schema_path();
35882
37931
  init_config();
35883
- import { join as join23 } from "path";
37932
+ init_integration_helper();
37933
+ import { join as join26 } from "path";
35884
37934
  import { resolveSrv as resolveSrv2 } from "dns/promises";
35885
37935
  function requireSqlConnection9(connection) {
35886
37936
  if (!["postgresql", "mysql", "mariadb"].includes(connection.system)) {
@@ -35888,7 +37938,7 @@ function requireSqlConnection9(connection) {
35888
37938
  }
35889
37939
  return connection;
35890
37940
  }
35891
- var ALLOWED_FORMATS13 = ["text", "json"];
37941
+ var ALLOWED_FORMATS15 = ["text", "json"];
35892
37942
  var SENSITIVE_PATTERNS = [
35893
37943
  "password",
35894
37944
  "passwd",
@@ -35960,7 +38010,7 @@ var runDoctorChecks = {
35960
38010
  }
35961
38011
  },
35962
38012
  async checkConfigExists(configPath, existsFn) {
35963
- const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join23(configPath, "config.json")).exists();
38013
+ const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join26(configPath, "config.json")).exists();
35964
38014
  return {
35965
38015
  group: "Configuration",
35966
38016
  label: "Config exists",
@@ -36117,7 +38167,7 @@ var runDoctorChecks = {
36117
38167
  async checkV2Config(configPath) {
36118
38168
  const results = [];
36119
38169
  const storagePath = await resolveConfigStoragePath(configPath);
36120
- const configFile = Bun.file(join23(storagePath, "config.json"));
38170
+ const configFile = Bun.file(join26(storagePath, "config.json"));
36121
38171
  if (!await configFile.exists())
36122
38172
  return results;
36123
38173
  let raw;
@@ -36157,7 +38207,7 @@ var runDoctorChecks = {
36157
38207
  }
36158
38208
  for (const [name, conn] of Object.entries(config.connections)) {
36159
38209
  if (conn.envFile) {
36160
- const envPath = join23(storagePath, conn.envFile);
38210
+ const envPath = join26(storagePath, conn.envFile);
36161
38211
  const exists = await Bun.file(envPath).exists();
36162
38212
  results.push({
36163
38213
  group: "Configuration",
@@ -36302,7 +38352,7 @@ async function collectElasticsearchDoctorResults(config) {
36302
38352
  return results;
36303
38353
  }
36304
38354
  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");
38355
+ validateFormat(options.format, ALLOWED_FORMATS15, "doctor");
36306
38356
  const logger = getLogger();
36307
38357
  const results = [];
36308
38358
  const configPath = resolveConfigPath(doctorCommand);
@@ -36376,7 +38426,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
36376
38426
  }
36377
38427
  try {
36378
38428
  const schemaConnName = await getSchemaIsolationConnectionName(configPath);
36379
- const indexPath = join23(resolveSchemaPath(storagePath, schemaConnName), "index.json");
38429
+ const indexPath = join26(resolveSchemaPath(storagePath, schemaConnName), "index.json");
36380
38430
  const indexFile = Bun.file(indexPath);
36381
38431
  let indexParsed = null;
36382
38432
  if (await indexFile.exists()) {
@@ -36407,6 +38457,17 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
36407
38457
  }
36408
38458
  }
36409
38459
  const hasError = results.some((r) => r.status === "error");
38460
+ try {
38461
+ const config = await configModule.read(configPath);
38462
+ await writeAuditEntry(config, "doctor", options, {
38463
+ success: !hasError,
38464
+ target: "*",
38465
+ metadata: {
38466
+ error_count: results.filter((r) => r.status === "error").length,
38467
+ warn_count: results.filter((r) => r.status === "warn").length
38468
+ }
38469
+ });
38470
+ } catch {}
36410
38471
  if (options.format === "json") {
36411
38472
  console.log(JSON.stringify({ results, hasError }, null, 2));
36412
38473
  } else {
@@ -36419,7 +38480,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
36419
38480
 
36420
38481
  // src/commands/completion.ts
36421
38482
  init_colors();
36422
- import { join as join24 } from "path";
38483
+ import { join as join27 } from "path";
36423
38484
  import { homedir as homedir3 } from "os";
36424
38485
  function extractCommands(program2) {
36425
38486
  return program2.commands.map((cmd) => ({
@@ -36521,11 +38582,11 @@ function getInstallPath2(shell) {
36521
38582
  const home = homedir3();
36522
38583
  switch (shell) {
36523
38584
  case "bash":
36524
- return join24(home, ".bashrc");
38585
+ return join27(home, ".bashrc");
36525
38586
  case "zsh":
36526
- return join24(home, ".zshrc");
38587
+ return join27(home, ".zshrc");
36527
38588
  case "fish":
36528
- return join24(home, ".config", "fish", "completions", "dbcli.fish");
38589
+ return join27(home, ".config", "fish", "completions", "dbcli.fish");
36529
38590
  default:
36530
38591
  throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
36531
38592
  }
@@ -36545,7 +38606,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
36545
38606
  async function installCompletion(shell, script) {
36546
38607
  const targetPath = getInstallPath2(shell);
36547
38608
  if (shell === "fish") {
36548
- const dir = join24(homedir3(), ".config", "fish", "completions");
38609
+ const dir = join27(homedir3(), ".config", "fish", "completions");
36549
38610
  await Bun.$`mkdir -p ${dir}`.quiet();
36550
38611
  await Bun.file(targetPath).write(script);
36551
38612
  console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
@@ -36778,7 +38839,7 @@ ${t("upgrade.failed")}`));
36778
38839
  init_config();
36779
38840
  init_adapters();
36780
38841
  import { createInterface as createInterface2 } from "readline";
36781
- import { join as join25 } from "path";
38842
+ import { join as join28 } from "path";
36782
38843
  import { homedir as homedir4 } from "os";
36783
38844
 
36784
38845
  // src/core/repl/types.ts
@@ -37123,7 +39184,7 @@ function isKnownCommand(name) {
37123
39184
 
37124
39185
  // src/core/repl/history-manager.ts
37125
39186
  import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
37126
- import { dirname as dirname8 } from "path";
39187
+ import { dirname as dirname10 } from "path";
37127
39188
  var DEFAULT_MAX = 1000;
37128
39189
 
37129
39190
  class HistoryManager {
@@ -37151,7 +39212,7 @@ class HistoryManager {
37151
39212
  return this.entries;
37152
39213
  }
37153
39214
  async save() {
37154
- const dir = dirname8(this.filePath);
39215
+ const dir = dirname10(this.filePath);
37155
39216
  if (!existsSync(dir)) {
37156
39217
  mkdirSync2(dir, { recursive: true });
37157
39218
  }
@@ -37509,7 +39570,7 @@ function requireSqlConnection10(connection) {
37509
39570
  }
37510
39571
  return connection;
37511
39572
  }
37512
- var HISTORY_PATH = join25(homedir4(), ".dbcli_history");
39573
+ var HISTORY_PATH = join28(homedir4(), ".dbcli_history");
37513
39574
  var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
37514
39575
  async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
37515
39576
  const columnsByTable = {};
@@ -38496,7 +40557,7 @@ init_config();
38496
40557
  init_errors();
38497
40558
  init_message_loader();
38498
40559
  init_config_binding();
38499
- import { join as join26 } from "path";
40560
+ import { join as join29 } from "path";
38500
40561
  async function switchDefault(configPath, name, config) {
38501
40562
  if (!config.connections[name]) {
38502
40563
  const available = Object.keys(config.connections).join(", ");
@@ -38519,7 +40580,7 @@ function listConnectionsForDisplay(config) {
38519
40580
  }
38520
40581
  async function ensureV2Config(configPath) {
38521
40582
  const storagePath = await resolveConfigStoragePath(configPath);
38522
- const configFile = Bun.file(join26(storagePath, "config.json"));
40583
+ const configFile = Bun.file(join29(storagePath, "config.json"));
38523
40584
  const legacyFile = Bun.file(configPath);
38524
40585
  if (!await configFile.exists() && !await legacyFile.exists()) {
38525
40586
  throw new ConfigError(t("init.config_not_found"));
@@ -38544,7 +40605,8 @@ async function ensureV2Config(configPath) {
38544
40605
  schema: v1Config.schema || {},
38545
40606
  schemas: { default: v1Config.schema || {} },
38546
40607
  metadata: v1Config.metadata || { version: "1.0" },
38547
- blacklist: v1Config.blacklist || { tables: [], columns: {} }
40608
+ blacklist: v1Config.blacklist || { tables: [], columns: {} },
40609
+ audit: v1Config.audit
38548
40610
  };
38549
40611
  }
38550
40612
  return readV2Config(storagePath);
@@ -38582,7 +40644,7 @@ var useCommand = new Command("use").description("Switch or display the default d
38582
40644
 
38583
40645
  // src/cli.ts
38584
40646
  init_config();
38585
- import { join as join27 } from "path";
40647
+ import { join as join30 } from "path";
38586
40648
  var _bgVersionCheckResult;
38587
40649
  function shouldSkipBackgroundChecks() {
38588
40650
  return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
@@ -38611,7 +40673,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
38611
40673
  try {
38612
40674
  let cache = null;
38613
40675
  try {
38614
- const cacheFile = Bun.file(join27(configPath, "version-check.json"));
40676
+ const cacheFile = Bun.file(join30(configPath, "version-check.json"));
38615
40677
  if (await cacheFile.exists()) {
38616
40678
  cache = await cacheFile.json();
38617
40679
  }
@@ -38659,7 +40721,7 @@ program2.command("plan <sql>").description("Analyze SQL risk without executing")
38659
40721
  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
40722
  await qCommand(name, options, command);
38661
40723
  });
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) => {
40724
+ 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
40725
  try {
38664
40726
  await insertCommand(table, options, command);
38665
40727
  } catch (error) {
@@ -38671,7 +40733,7 @@ program2.command("insert <table>").description(t("insert.description")).option("
38671
40733
  process.exit(1);
38672
40734
  }
38673
40735
  });
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) => {
40736
+ 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
40737
  try {
38676
40738
  await updateCommand(table, options, command);
38677
40739
  } catch (error) {
@@ -38683,7 +40745,7 @@ program2.command("update <table>").description(t("update.description")).option("
38683
40745
  process.exit(1);
38684
40746
  }
38685
40747
  });
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) => {
40748
+ 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
40749
  try {
38688
40750
  await deleteCommand(table, options, command);
38689
40751
  } catch (error) {
@@ -38721,6 +40783,7 @@ program2.addCommand(reportCommand);
38721
40783
  program2.addCommand(guideCommand);
38722
40784
  program2.addCommand(recoveryCommand);
38723
40785
  program2.addCommand(recoverCommand);
40786
+ program2.addCommand(auditCommand);
38724
40787
  program2.addCommand(doctorCommand);
38725
40788
  program2.addCommand(completionCommand);
38726
40789
  program2.addCommand(upgradeCommand);