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 =
|
|
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 (!
|
|
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.
|
|
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)").
|
|
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
|
|
10318
|
+
const rawExp = exportSession(session, {
|
|
10198
10319
|
tool: { name: "acp-devtools", version: CLI_VERSION }
|
|
10199
10320
|
});
|
|
10200
|
-
const
|
|
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 #${
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
11137
|
-
const 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
|
|
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) || !
|
|
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 =
|
|
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);
|