acp-devtools 0.1.0 → 0.2.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/index.js CHANGED
@@ -8480,7 +8480,7 @@ function extractRpcId(line) {
8480
8480
  }
8481
8481
 
8482
8482
  // ../core/dist/storage/sqlite.js
8483
- import { existsSync } from "fs";
8483
+ import { chmodSync, existsSync, statSync } from "fs";
8484
8484
  import Database from "better-sqlite3";
8485
8485
  var SCHEMA_VERSION = 4;
8486
8486
  var SCHEMA_SQL = `
@@ -8523,12 +8523,27 @@ function hasColumn(db, table, column) {
8523
8523
  const cols = db.pragma(`table_info(${table})`);
8524
8524
  return cols.some((c) => c.name === column);
8525
8525
  }
8526
+ function restrictDbFile(path) {
8527
+ if (path === ":memory:" || process.platform === "win32")
8528
+ return;
8529
+ try {
8530
+ if (!existsSync(path))
8531
+ return;
8532
+ const mode = statSync(path).mode & 511;
8533
+ if (mode !== 384)
8534
+ chmodSync(path, 384);
8535
+ } catch {
8536
+ }
8537
+ }
8526
8538
  function openDatabase(path) {
8527
8539
  const db = new Database(path);
8540
+ restrictDbFile(path);
8528
8541
  db.pragma("busy_timeout = 5000");
8529
8542
  db.pragma("journal_mode = WAL");
8530
8543
  db.pragma("foreign_keys = ON");
8531
8544
  db.pragma("synchronous = NORMAL");
8545
+ restrictDbFile(`${path}-wal`);
8546
+ restrictDbFile(`${path}-shm`);
8532
8547
  db.exec(SCHEMA_SQL);
8533
8548
  if (!hasColumn(db, "sessions", "client_name")) {
8534
8549
  db.exec(`ALTER TABLE sessions ADD COLUMN client_name TEXT`);
@@ -8852,6 +8867,108 @@ function isObject(value) {
8852
8867
  return typeof value === "object" && value !== null && !Array.isArray(value);
8853
8868
  }
8854
8869
 
8870
+ // ../core/dist/storage/redact.js
8871
+ var REDACTED_PLACEHOLDER = "<REDACTED>";
8872
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
8873
+ "authorization",
8874
+ "proxy-authorization",
8875
+ "proxy-authentication",
8876
+ "x-api-key",
8877
+ "x-api-token",
8878
+ "x-auth-token",
8879
+ "api-key",
8880
+ "api_key",
8881
+ "proxy_key",
8882
+ "cookie",
8883
+ "set-cookie"
8884
+ ]);
8885
+ function redactMessage(message) {
8886
+ if (message.payload == null) {
8887
+ return { redacted: message, count: 0 };
8888
+ }
8889
+ const { value, count } = redactValue(message.payload, { inProxyConfig: false });
8890
+ if (count === 0) {
8891
+ return { redacted: message, count: 0 };
8892
+ }
8893
+ const redactedPayload = value;
8894
+ return {
8895
+ redacted: {
8896
+ ...message,
8897
+ payload: redactedPayload,
8898
+ raw: JSON.stringify(redactedPayload)
8899
+ },
8900
+ count
8901
+ };
8902
+ }
8903
+ function redactValue(value, ctx) {
8904
+ if (Array.isArray(value)) {
8905
+ let count = 0;
8906
+ const out = new Array(value.length);
8907
+ for (let i = 0; i < value.length; i++) {
8908
+ const r = redactValue(value[i], ctx);
8909
+ out[i] = r.value;
8910
+ count += r.count;
8911
+ }
8912
+ return { value: out, count };
8913
+ }
8914
+ if (value !== null && typeof value === "object") {
8915
+ let count = 0;
8916
+ const out = {};
8917
+ for (const [k, v] of Object.entries(value)) {
8918
+ if (typeof v === "string" && SENSITIVE_HEADER_NAMES.has(k.toLowerCase())) {
8919
+ out[k] = REDACTED_PLACEHOLDER;
8920
+ count += 1;
8921
+ continue;
8922
+ }
8923
+ if (ctx.inProxyConfig && k === "headers" && v !== null && typeof v === "object" && !Array.isArray(v)) {
8924
+ const r2 = redactHeadersBlock(v);
8925
+ out[k] = r2.value;
8926
+ count += r2.count;
8927
+ continue;
8928
+ }
8929
+ const childCtx = ctx.inProxyConfig || k.toLowerCase() === "proxyconfig" ? { inProxyConfig: true } : ctx;
8930
+ const r = redactValue(v, childCtx);
8931
+ out[k] = r.value;
8932
+ count += r.count;
8933
+ }
8934
+ return { value: out, count };
8935
+ }
8936
+ return { value, count: 0 };
8937
+ }
8938
+ function redactHeadersBlock(obj) {
8939
+ let count = 0;
8940
+ const out = {};
8941
+ for (const [k, v] of Object.entries(obj)) {
8942
+ if (typeof v === "string") {
8943
+ out[k] = REDACTED_PLACEHOLDER;
8944
+ count += 1;
8945
+ } else {
8946
+ out[k] = v;
8947
+ }
8948
+ }
8949
+ return { value: out, count };
8950
+ }
8951
+ function redactSessionExport(exp) {
8952
+ let messagesAffected = 0;
8953
+ let fieldsRedacted = 0;
8954
+ const messages = exp.messages.map((msg) => {
8955
+ const r = redactMessage(msg);
8956
+ if (r.count > 0) {
8957
+ messagesAffected += 1;
8958
+ fieldsRedacted += r.count;
8959
+ }
8960
+ return r.redacted;
8961
+ });
8962
+ if (fieldsRedacted === 0) {
8963
+ return { export: exp, messagesAffected: 0, fieldsRedacted: 0 };
8964
+ }
8965
+ return {
8966
+ export: { ...exp, messages },
8967
+ messagesAffected,
8968
+ fieldsRedacted
8969
+ };
8970
+ }
8971
+
8855
8972
  // ../core/dist/ws/server.js
8856
8973
  import { WebSocketServer } from "ws";
8857
8974
  var WsBroadcaster = class {
@@ -9918,7 +10035,7 @@ function fmtValue(v) {
9918
10035
  }
9919
10036
 
9920
10037
  // src/commands/doctor.ts
9921
- import { existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "fs";
10038
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
9922
10039
  import { homedir as homedir3, platform, release } from "os";
9923
10040
  import { join as join3 } from "path";
9924
10041
  var STATUS_MARK = {
@@ -9983,7 +10100,7 @@ function checkState() {
9983
10100
  ) m ON m.session_id = s.id`
9984
10101
  ).get();
9985
10102
  db.close();
9986
- const size = statSync(dbPath).size;
10103
+ const size = statSync2(dbPath).size;
9987
10104
  results.push({
9988
10105
  status: "ok",
9989
10106
  label: `captures database: ${row2.sessions} session${row2.sessions === 1 ? "" : "s"}, ${row2.messages} message${row2.messages === 1 ? "" : "s"}`,
@@ -10065,7 +10182,7 @@ function probeJetBrains() {
10065
10182
  if (!JETBRAINS_VERSIONED.test(entry) && !JETBRAINS_UNVERSIONED.has(entry)) continue;
10066
10183
  const full = join3(base, entry);
10067
10184
  try {
10068
- if (!statSync(full).isDirectory()) continue;
10185
+ if (!statSync2(full).isDirectory()) continue;
10069
10186
  } catch {
10070
10187
  continue;
10071
10188
  }
@@ -10161,11 +10278,15 @@ function registerDoctorCommand(program2) {
10161
10278
  import { writeFileSync as writeFileSync2 } from "fs";
10162
10279
 
10163
10280
  // src/version.ts
10164
- var CLI_VERSION = "0.1.0";
10281
+ var CLI_VERSION = "0.2.0";
10165
10282
 
10166
10283
  // src/commands/export.ts
10167
10284
  function registerExportCommand(program2) {
10168
- program2.command("export").description("Export a recorded session as self-contained JSON (suitable for sharing and for mock-replay)").argument("[id]", "session id to export (default: latest in the database)").option("--db <path>", "captures database", defaultCapturesDbPath()).option("-o, --output <file>", "write to a file instead of stdout").option("--no-pretty", "emit compact JSON (no indent, single line)").action((idArg, opts) => {
10285
+ program2.command("export").description("Export a recorded session as self-contained JSON (suitable for sharing and for mock-replay)").argument("[id]", "session id to export (default: latest in the database)").option("--db <path>", "captures database", defaultCapturesDbPath()).option("-o, --output <file>", "write to a file instead of stdout").option("--no-pretty", "emit compact JSON (no indent, single line)").option(
10286
+ "--raw",
10287
+ "skip default redaction of auth headers / proxy tokens (use only when the export stays on YOUR machine)",
10288
+ false
10289
+ ).action((idArg, opts) => {
10169
10290
  let id = null;
10170
10291
  if (idArg !== void 0) {
10171
10292
  id = Number(idArg);
@@ -10194,20 +10315,31 @@ function registerExportCommand(program2) {
10194
10315
  db.close();
10195
10316
  process.exit(1);
10196
10317
  }
10197
- const exp = exportSession(session, {
10318
+ const rawExp = exportSession(session, {
10198
10319
  tool: { name: "acp-devtools", version: CLI_VERSION }
10199
10320
  });
10200
- const json = serializeExport(exp, opts.pretty ? 4 : 0);
10321
+ const { export: finalExp, fieldsRedacted, messagesAffected } = opts.raw ? { export: rawExp, fieldsRedacted: 0, messagesAffected: 0 } : redactSessionExport(rawExp);
10322
+ const json = serializeExport(finalExp, opts.pretty ? 4 : 0);
10201
10323
  db.close();
10202
10324
  if (opts.output) {
10203
10325
  writeFileSync2(opts.output, json);
10204
10326
  process.stderr.write(
10205
- `acp-devtools: exported session #${exp.session.id} (${exp.messages.length} messages) \u2192 ${opts.output}
10327
+ `acp-devtools: exported session #${finalExp.session.id} (${finalExp.messages.length} messages) \u2192 ${opts.output}
10206
10328
  `
10207
10329
  );
10208
10330
  } else {
10209
10331
  process.stdout.write(json);
10210
10332
  }
10333
+ if (opts.raw) {
10334
+ process.stderr.write(
10335
+ "acp-devtools: --raw was set; export may contain auth headers / proxy tokens \u2014 do not share publicly\n"
10336
+ );
10337
+ } else if (fieldsRedacted > 0) {
10338
+ process.stderr.write(
10339
+ `acp-devtools: redacted ${fieldsRedacted} field(s) across ${messagesAffected} message(s) \u2014 re-run with --raw to keep them
10340
+ `
10341
+ );
10342
+ }
10211
10343
  });
10212
10344
  }
10213
10345
 
@@ -10646,7 +10778,23 @@ it avoids re-parsing the text payload.
10646
10778
  Schema notes: sessions have a structured-metadata layer populated by the
10647
10779
  \`backfill-metadata\` CLI; \`client_name\` / \`client_version\` /
10648
10780
  \`client_platform\` may be null on freshly imported captures until that
10649
- command runs.`;
10781
+ command runs.
10782
+
10783
+ Redaction: every tool that returns frame contents OR derived views
10784
+ of them (\`get_message\`, \`get_session_messages\`, \`search_messages\`,
10785
+ \`get_session_metadata\`, \`get_session_summary\`, \`diff_sessions\`)
10786
+ redacts auth headers and proxy tokens (\`Authorization\`,
10787
+ \`X-Api-Key\`, JetBrains \`_meta.proxyConfig.proxies[*].proxy.headers.*\`,
10788
+ etc). Sensitive fields appear as \`<REDACTED>\`. This is
10789
+ unconditional; there is no opt-out flag, because the human owns the
10790
+ share decision (use \`acp-devtools export <id> --raw\` on the CLI when
10791
+ the export stays on your machine). \`search_messages\` indexes the
10792
+ pre-redaction bytes so you can still find a frame by a token fragment,
10793
+ but the returned \`raw\` is the redacted copy \u2014 do not quote the live
10794
+ secret back to the user. \`diff_sessions\` operates on already-redacted
10795
+ frames, so a token that rotated between two sessions shows up as equal
10796
+ ("<REDACTED>" on both sides) rather than as a change \u2014 this is
10797
+ intentional, treating "token rotated" as a leaked side channel.`;
10650
10798
  function annotations(meta) {
10651
10799
  return {
10652
10800
  title: meta.title,
@@ -10779,7 +10927,7 @@ function buildMcpServer(opts) {
10779
10927
  annotations: annotations({ title: "Get session metadata" })
10780
10928
  },
10781
10929
  async ({ session_id }) => {
10782
- const messages = readMessages(opts.db, session_id);
10930
+ const messages = redactedReadMessages(opts.db, session_id);
10783
10931
  return structuredResult({
10784
10932
  session_id,
10785
10933
  metadata: extractSessionMetadata(messages)
@@ -10854,7 +11002,7 @@ function buildMcpServer(opts) {
10854
11002
  annotations: annotations({ title: "One-call session digest" })
10855
11003
  },
10856
11004
  async ({ session_id }) => {
10857
- const messages = readMessages(opts.db, session_id);
11005
+ const messages = redactedReadMessages(opts.db, session_id);
10858
11006
  const perMethod = buildPerMethodStats(messages);
10859
11007
  const insights = buildPerformanceInsights(messages, perMethod);
10860
11008
  const pairs = buildPairIndex(messages);
@@ -10932,7 +11080,7 @@ function buildMcpServer(opts) {
10932
11080
  total,
10933
11081
  offset: start,
10934
11082
  returned: end - start,
10935
- messages: filtered.slice(start, end)
11083
+ messages: filtered.slice(start, end).map((m) => redactMessage(m).redacted)
10936
11084
  });
10937
11085
  }
10938
11086
  );
@@ -10956,7 +11104,7 @@ function buildMcpServer(opts) {
10956
11104
  if (!msg) {
10957
11105
  return errorResult(`session ${session_id} has no message #${seq}`);
10958
11106
  }
10959
- return structuredResult({ session_id, message: msg });
11107
+ return structuredResult({ session_id, message: redactMessage(msg).redacted });
10960
11108
  }
10961
11109
  );
10962
11110
  server.registerTool(
@@ -11010,7 +11158,8 @@ function buildMcpServer(opts) {
11010
11158
  const matches = [];
11011
11159
  for (const m of messages) {
11012
11160
  if (m.raw.toLowerCase().includes(needle)) {
11013
- matches.push({ seq: m.seq, method: m.method ?? null, raw: m.raw });
11161
+ const safe = redactMessage(m).redacted;
11162
+ matches.push({ seq: m.seq, method: m.method ?? null, raw: safe.raw });
11014
11163
  if (matches.length >= (limit ?? 50)) break;
11015
11164
  }
11016
11165
  }
@@ -11133,8 +11282,8 @@ function buildMcpServer(opts) {
11133
11282
  annotations: annotations({ title: "Diff two sessions" })
11134
11283
  },
11135
11284
  async ({ session_a, session_b, include_equal }) => {
11136
- const a = readMessages(opts.db, session_a);
11137
- const b = readMessages(opts.db, session_b);
11285
+ const a = redactedReadMessages(opts.db, session_a);
11286
+ const b = redactedReadMessages(opts.db, session_b);
11138
11287
  const diff = buildSessionDiff(a, b);
11139
11288
  const meta = buildMetadataDiff(a, b);
11140
11289
  const perMethod = buildMethodStatsDiff(a, b);
@@ -11257,6 +11406,9 @@ Cite the chosen session ids and their message counts so I can replicate in the i
11257
11406
  })
11258
11407
  );
11259
11408
  }
11409
+ function redactedReadMessages(dbPath, sessionId) {
11410
+ return readMessages(dbPath, sessionId).map((m) => redactMessage(m).redacted);
11411
+ }
11260
11412
  function readMessages(dbPath, sessionId) {
11261
11413
  const db = openDatabase(dbPath);
11262
11414
  try {
@@ -12531,7 +12683,7 @@ function renderPerMethod(s, rows) {
12531
12683
  }
12532
12684
 
12533
12685
  // src/commands/ui.ts
12534
- import { createReadStream, existsSync as existsSync7, statSync as statSync2 } from "fs";
12686
+ import { createReadStream, existsSync as existsSync7, statSync as statSync3 } from "fs";
12535
12687
  import { createServer } from "http";
12536
12688
  import { dirname as dirname5, extname, join as join4, normalize, resolve, sep } from "path";
12537
12689
  import { fileURLToPath } from "url";
@@ -12578,7 +12730,7 @@ function serveStatic(req, res, rootDir) {
12578
12730
  return;
12579
12731
  }
12580
12732
  let filePath = candidate;
12581
- if (!existsSync7(filePath) || !statSync2(filePath).isFile()) {
12733
+ if (!existsSync7(filePath) || !statSync3(filePath).isFile()) {
12582
12734
  filePath = join4(rootDir, "index.html");
12583
12735
  if (!existsSync7(filePath)) {
12584
12736
  res.statusCode = 404;
@@ -12587,7 +12739,7 @@ function serveStatic(req, res, rootDir) {
12587
12739
  return;
12588
12740
  }
12589
12741
  }
12590
- const stat = statSync2(filePath);
12742
+ const stat = statSync3(filePath);
12591
12743
  res.statusCode = 200;
12592
12744
  res.setHeader("content-type", contentTypeFor(filePath));
12593
12745
  res.setHeader("content-length", stat.size);