@workscribe/cli 3.0.1 → 3.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
@@ -378,7 +378,7 @@ var init_logger = __esm({
378
378
  success: (msg) => console.log(import_chalk.default.green("\u2713"), msg),
379
379
  warn: (msg) => console.log(import_chalk.default.yellow("\u26A0"), msg),
380
380
  error: (msg) => console.error(import_chalk.default.red("\u2717"), msg),
381
- /** A warning on stderr — keeps stdout clean for JSON output (summary.format=json) and pipes. */
381
+ /** A warning on stderr — keeps stdout clean for JSON and plain text output, and pipes. */
382
382
  notice: (msg) => process.stderr.write(`${import_chalk.default.yellow("\u26A0")} ${msg}
383
383
  `),
384
384
  plain: (msg) => console.log(msg)
@@ -972,6 +972,13 @@ function maskQuotedText(text) {
972
972
  }
973
973
  return masked.join("");
974
974
  }
975
+ function maskLiteralText(text) {
976
+ const masked = Array.from({ length: text.length }, () => "_");
977
+ for (const { index, quote } of quotedCharacters(text)) {
978
+ if (quote !== "'") masked[index] = text[index];
979
+ }
980
+ return masked.join("");
981
+ }
975
982
  function startsWithWord(step2, word) {
976
983
  return step2 === word || step2.startsWith(`${word} `) || step2.startsWith(`${word} `);
977
984
  }
@@ -1175,57 +1182,6 @@ var init_exclusions = __esm({
1175
1182
  }
1176
1183
  });
1177
1184
 
1178
- // src/normalize/compound.ts
1179
- function rankOf(category) {
1180
- const index = STEP_CATEGORY_PRIORITY.indexOf(category);
1181
- if (index !== -1) return index;
1182
- if (category === UNCLASSIFIED.category) return UNCLASSIFIED_RANK;
1183
- if (DEFAULT_EXCLUDED_CATEGORIES.has(category)) return EXCLUDED_CATEGORY_RANK;
1184
- return OTHER_CATEGORY_RANK;
1185
- }
1186
- function primaryStepEvent(steps, matchCommand) {
1187
- const events = steps.map((step2) => ({ ...matchCommand(simpleCommandOf(step2), step2), matchedStep: step2 }));
1188
- return events.reduce(
1189
- (best, event) => rankOf(event.category) < rankOf(best.category) ? event : best,
1190
- UNCLASSIFIED
1191
- );
1192
- }
1193
- function classifyCompound(command, matchCommand) {
1194
- const workSteps = commandSteps(command).filter((step2) => !isGlueStep(step2));
1195
- const wholeCommandEvent = {
1196
- ...matchCommand(command, command),
1197
- matchedStep: workSteps.length === 1 ? workSteps[0] : command
1198
- };
1199
- const isRecognisedSingleCommand = workSteps.length <= 1 && wholeCommandEvent.category !== UNCLASSIFIED.category;
1200
- if (isRecognisedSingleCommand || workSteps.length === 0) return wholeCommandEvent;
1201
- const primaryEvent = primaryStepEvent(workSteps, matchCommand);
1202
- return primaryEvent.category === UNCLASSIFIED.category ? wholeCommandEvent : primaryEvent;
1203
- }
1204
- var UNCLASSIFIED, STEP_CATEGORY_PRIORITY, OTHER_CATEGORY_RANK, EXCLUDED_CATEGORY_RANK, UNCLASSIFIED_RANK;
1205
- var init_compound = __esm({
1206
- "src/normalize/compound.ts"() {
1207
- "use strict";
1208
- init_command_steps();
1209
- init_exclusions();
1210
- UNCLASSIFIED = { category: "other", metadata: {} };
1211
- STEP_CATEGORY_PRIORITY = [
1212
- "testing",
1213
- "building",
1214
- "releasing",
1215
- "shipping",
1216
- "dependencies",
1217
- "database",
1218
- "infrastructure",
1219
- "running",
1220
- "files",
1221
- "versioning"
1222
- ];
1223
- OTHER_CATEGORY_RANK = STEP_CATEGORY_PRIORITY.length;
1224
- EXCLUDED_CATEGORY_RANK = OTHER_CATEGORY_RANK + 1;
1225
- UNCLASSIFIED_RANK = EXCLUDED_CATEGORY_RANK + 1;
1226
- }
1227
- });
1228
-
1229
1185
  // src/normalize/git-invocation.ts
1230
1186
  function parseGitInvocation(command) {
1231
1187
  const words = shellWords(command.trim());
@@ -1282,6 +1238,198 @@ var init_git_invocation = __esm({
1282
1238
  }
1283
1239
  });
1284
1240
 
1241
+ // src/normalize/read-only.ts
1242
+ function hasNoOption(args, options) {
1243
+ return !args.some((arg) => options.some((option) => arg === option || arg.startsWith(`${option}=`)));
1244
+ }
1245
+ function runsInlineCode(args, inlineFlags) {
1246
+ const firstMeaningful = args.find(
1247
+ (arg) => inlineFlags.includes(arg) || arg.startsWith("--eval=") || !arg.startsWith("-")
1248
+ );
1249
+ return firstMeaningful !== void 0 && (inlineFlags.includes(firstMeaningful) || firstMeaningful.startsWith("--eval="));
1250
+ }
1251
+ function readSedShortOptions(option, nextArg, parsed) {
1252
+ const letters = option.slice(1);
1253
+ const expressionAt = letters.indexOf("e");
1254
+ if (SED_UNSAFE_SHORT_OPTION.test(letters) || expressionAt !== -1 && expressionAt !== letters.length - 1) {
1255
+ parsed.isSafe = false;
1256
+ }
1257
+ if (letters.includes("n")) parsed.isQuiet = true;
1258
+ if (expressionAt === -1) return 0;
1259
+ parsed.scripts.push(nextArg ?? "");
1260
+ return 1;
1261
+ }
1262
+ function readSedArguments(args) {
1263
+ const parsed = { isQuiet: false, isSafe: true, scripts: [] };
1264
+ const positional = [];
1265
+ for (let index = 0; index < args.length; index++) {
1266
+ const arg = args[index];
1267
+ if (arg === "--quiet" || arg === "--silent") parsed.isQuiet = true;
1268
+ else if (arg.startsWith("--in-place") || arg.startsWith("--file")) parsed.isSafe = false;
1269
+ else if (arg === "--expression") parsed.scripts.push(args[++index] ?? "");
1270
+ else if (arg.startsWith("--expression=")) parsed.scripts.push(arg.slice("--expression=".length));
1271
+ else if (SED_SHORT_OPTIONS.test(arg)) index += readSedShortOptions(arg, args[index + 1], parsed);
1272
+ else if (!arg.startsWith("-")) positional.push(arg);
1273
+ }
1274
+ return { ...parsed, positional };
1275
+ }
1276
+ function sedOnlyPrints(args) {
1277
+ const { isQuiet, isSafe, scripts, positional } = readSedArguments(args);
1278
+ const allScripts = scripts.length > 0 ? scripts : positional.slice(0, 1);
1279
+ if (!isQuiet || !isSafe || allScripts.length === 0) return false;
1280
+ return allScripts.every(
1281
+ (script) => script.split(";").filter((part) => part.trim() !== "").every((part) => SED_PRINT_COMMAND.test(part.trim()))
1282
+ );
1283
+ }
1284
+ function isGitRead(command) {
1285
+ const invocation = parseGitInvocation(command);
1286
+ if (!invocation?.subcommand) return false;
1287
+ const args = invocation.args.map(({ word }) => word);
1288
+ if (invocation.subcommand === "branch") return args.length === 0;
1289
+ return GIT_READS.has(invocation.subcommand) && hasNoOption(args, GIT_UNSAFE_OPTIONS);
1290
+ }
1291
+ function isReadingStage(stage, checks) {
1292
+ const command = simpleCommandOf(stage);
1293
+ const [name, ...args] = shellWords(command).map(({ word }) => word);
1294
+ if (name === "git") return isGitRead(command);
1295
+ return Object.hasOwn(checks, name ?? "") && checks[name](args);
1296
+ }
1297
+ function substitutionsOnlyRead(step2) {
1298
+ if (!containsCommandSubstitution(step2)) return true;
1299
+ if (PROCESS_SUBSTITUTION.test(maskQuotedText(step2))) return false;
1300
+ const literalMasked = maskLiteralText(step2);
1301
+ const commands = substitutionCommands(literalMasked);
1302
+ const openers = literalMasked.split("$(").length - 1 + Math.ceil((literalMasked.split("`").length - 1) / 2);
1303
+ return commands.length === openers && commands.every(isReadOnlyCommand);
1304
+ }
1305
+ function isReadOnlyStep(step2) {
1306
+ if (!substitutionsOnlyRead(step2)) return false;
1307
+ const [firstStage = "", ...laterStages] = splitOutsideQuotes(step2, PIPE_SEPARATORS2);
1308
+ return isReadingStage(firstStage, READERS) && laterStages.every((stage) => isReadingStage(stage, { ...READERS, ...PIPE_FILTERS }));
1309
+ }
1310
+ function writesToFile(step2) {
1311
+ return [...maskQuotedText(step2).matchAll(OUTPUT_REDIRECTION)].some(([, target]) => !HARMLESS_TARGET.test(target));
1312
+ }
1313
+ function isReadOnlyCommand(command) {
1314
+ const steps = commandSteps(command);
1315
+ if (steps.some(writesToFile)) return false;
1316
+ const workSteps = steps.filter((step2) => !isGlueStep(step2));
1317
+ return workSteps.length > 0 && workSteps.every(isReadOnlyStep);
1318
+ }
1319
+ function readOnlyMetadata(command) {
1320
+ const firstWorkStep = commandSteps(command).find((step2) => !isGlueStep(step2)) ?? "";
1321
+ const [firstStage = ""] = splitOutsideQuotes(firstWorkStep, PIPE_SEPARATORS2);
1322
+ const simple = simpleCommandOf(firstStage);
1323
+ const invocation = parseGitInvocation(simple);
1324
+ return invocation ? { subcommand: invocation.subcommand } : { tool: shellWords(simple)[0]?.word ?? null };
1325
+ }
1326
+ var READ_ONLY_CATEGORY, PIPE_SEPARATORS2, ALWAYS_READS, GIT_READS, GIT_UNSAFE_OPTIONS, FIND_UNSAFE_ACTION, SED_ADDRESS, SED_PRINT_COMMAND, SED_SHORT_OPTIONS, SED_UNSAFE_SHORT_OPTION, OUTPUT_REDIRECTION, HARMLESS_TARGET, PROCESS_SUBSTITUTION, READERS, PIPE_FILTERS;
1327
+ var init_read_only = __esm({
1328
+ "src/normalize/read-only.ts"() {
1329
+ "use strict";
1330
+ init_command_steps();
1331
+ init_shell_syntax();
1332
+ init_git_invocation();
1333
+ READ_ONLY_CATEGORY = "exploring";
1334
+ PIPE_SEPARATORS2 = ["|"];
1335
+ ALWAYS_READS = () => true;
1336
+ GIT_READS = /* @__PURE__ */ new Set(["diff", "log", "show", "status"]);
1337
+ GIT_UNSAFE_OPTIONS = ["--output", "--ext-diff", "--textconv"];
1338
+ FIND_UNSAFE_ACTION = /^-(?:delete|exec|execdir|ok|okdir|fls|fprint\w*)$/;
1339
+ SED_ADDRESS = String.raw`(?:\d+|\$|/(?:[^/\\]|\\.)*/)`;
1340
+ SED_PRINT_COMMAND = new RegExp(String.raw`^(?:${SED_ADDRESS}(?:,${SED_ADDRESS})?)?\s*!?\s*[p=]$`);
1341
+ SED_SHORT_OPTIONS = /^-[a-zA-Z]+$/;
1342
+ SED_UNSAFE_SHORT_OPTION = /[iIf]/;
1343
+ OUTPUT_REDIRECTION = /(?:&>>?|\d*>>?\|?)\s*(&?[^\s;|&()]*)/g;
1344
+ HARMLESS_TARGET = /^(?:&\d+|\/dev\/null)$/;
1345
+ PROCESS_SUBSTITUTION = /[<>]\(/;
1346
+ READERS = {
1347
+ grep: ALWAYS_READS,
1348
+ egrep: ALWAYS_READS,
1349
+ fgrep: ALWAYS_READS,
1350
+ ag: ALWAYS_READS,
1351
+ ack: ALWAYS_READS,
1352
+ cat: ALWAYS_READS,
1353
+ bat: ALWAYS_READS,
1354
+ less: ALWAYS_READS,
1355
+ more: ALWAYS_READS,
1356
+ head: ALWAYS_READS,
1357
+ tail: ALWAYS_READS,
1358
+ ls: ALWAYS_READS,
1359
+ wc: ALWAYS_READS,
1360
+ // `--pre` runs a command on every file
1361
+ rg: (args) => hasNoOption(args, ["--pre"]),
1362
+ ripgrep: (args) => hasNoOption(args, ["--pre"]),
1363
+ find: (args) => !args.some((arg) => FIND_UNSAFE_ACTION.test(arg)),
1364
+ sed: sedOnlyPrints,
1365
+ python: (args) => runsInlineCode(args, ["-c", "-"]),
1366
+ python3: (args) => runsInlineCode(args, ["-c", "-"]),
1367
+ node: (args) => runsInlineCode(args, ["-e", "--eval"])
1368
+ };
1369
+ PIPE_FILTERS = {
1370
+ sort: (args) => hasNoOption(args, ["-o", "--output"]) && !args.some((arg) => /^-[a-zA-Z]*o/.test(arg)),
1371
+ // `uniq in out` writes to `out`
1372
+ uniq: (args) => args.filter((arg) => !arg.startsWith("-")).length <= 1,
1373
+ cut: ALWAYS_READS,
1374
+ tr: ALWAYS_READS,
1375
+ jq: ALWAYS_READS
1376
+ };
1377
+ }
1378
+ });
1379
+
1380
+ // src/normalize/compound.ts
1381
+ function rankOf(category) {
1382
+ const index = STEP_CATEGORY_PRIORITY.indexOf(category);
1383
+ if (index !== -1) return index;
1384
+ if (category === UNCLASSIFIED.category) return UNCLASSIFIED_RANK;
1385
+ if (category === READ_ONLY_CATEGORY) return READ_ONLY_RANK;
1386
+ if (DEFAULT_EXCLUDED_CATEGORIES.has(category)) return EXCLUDED_CATEGORY_RANK;
1387
+ return OTHER_CATEGORY_RANK;
1388
+ }
1389
+ function primaryStepEvent(stepEvents) {
1390
+ return stepEvents.reduce((best, event) => rankOf(event.category) < rankOf(best.category) ? event : best);
1391
+ }
1392
+ function classifyCompound(command, matchCommand) {
1393
+ const workSteps = commandSteps(command).filter((step2) => !isGlueStep(step2));
1394
+ const wholeCommandEvent = {
1395
+ ...matchCommand(command, command),
1396
+ matchedStep: workSteps.length === 1 ? workSteps[0] : command
1397
+ };
1398
+ const isRecognisedSingleCommand = workSteps.length <= 1 && wholeCommandEvent.category !== UNCLASSIFIED.category;
1399
+ if (isRecognisedSingleCommand || workSteps.length === 0) return wholeCommandEvent;
1400
+ const stepEvents = workSteps.map((step2) => ({ ...matchCommand(simpleCommandOf(step2), step2), matchedStep: step2 }));
1401
+ const primaryEvent = primaryStepEvent(stepEvents);
1402
+ if (primaryEvent.category !== UNCLASSIFIED.category) return primaryEvent;
1403
+ const isAnyStepRecognised = stepEvents.some((event) => event.category !== UNCLASSIFIED.category);
1404
+ return isAnyStepRecognised ? { ...UNCLASSIFIED, matchedStep: command } : wholeCommandEvent;
1405
+ }
1406
+ var UNCLASSIFIED, STEP_CATEGORY_PRIORITY, OTHER_CATEGORY_RANK, EXCLUDED_CATEGORY_RANK, UNCLASSIFIED_RANK, READ_ONLY_RANK;
1407
+ var init_compound = __esm({
1408
+ "src/normalize/compound.ts"() {
1409
+ "use strict";
1410
+ init_command_steps();
1411
+ init_exclusions();
1412
+ init_read_only();
1413
+ UNCLASSIFIED = { category: "other", metadata: {} };
1414
+ STEP_CATEGORY_PRIORITY = [
1415
+ "testing",
1416
+ "building",
1417
+ "releasing",
1418
+ "shipping",
1419
+ "dependencies",
1420
+ "database",
1421
+ "infrastructure",
1422
+ "running",
1423
+ "files",
1424
+ "versioning"
1425
+ ];
1426
+ OTHER_CATEGORY_RANK = STEP_CATEGORY_PRIORITY.length;
1427
+ EXCLUDED_CATEGORY_RANK = OTHER_CATEGORY_RANK + 1;
1428
+ UNCLASSIFIED_RANK = EXCLUDED_CATEGORY_RANK + 1;
1429
+ READ_ONLY_RANK = UNCLASSIFIED_RANK + 1;
1430
+ }
1431
+ });
1432
+
1285
1433
  // src/normalize/commit-message.ts
1286
1434
  function firstLineOf(text) {
1287
1435
  const line = text?.split("\n").map((part) => part.trim()).find((part) => part !== "");
@@ -1435,7 +1583,7 @@ function matchRules(command, context) {
1435
1583
  function normalizeCommand(command) {
1436
1584
  return classifyCompound(command, (simple, raw) => matchRules(simple, { raw, original: command }));
1437
1585
  }
1438
- var NX_TARGET_CATEGORIES, NX_COMMAND, nxRule, BASE_RULES, activeRules;
1586
+ var NX_TARGET_CATEGORIES, NX_COMMAND, nxRule, readOnlyRule, BASE_RULES, activeRules;
1439
1587
  var init_patterns = __esm({
1440
1588
  "src/normalize/patterns.ts"() {
1441
1589
  "use strict";
@@ -1446,6 +1594,7 @@ var init_patterns = __esm({
1446
1594
  init_commit_message();
1447
1595
  init_dependency_metadata();
1448
1596
  init_git_invocation();
1597
+ init_read_only();
1449
1598
  NX_TARGET_CATEGORIES = {
1450
1599
  build: "building",
1451
1600
  test: "testing",
@@ -1463,7 +1612,13 @@ var init_patterns = __esm({
1463
1612
  return category ? { category, metadata: { tool: "nx", target } } : null;
1464
1613
  }
1465
1614
  };
1615
+ readOnlyRule = {
1616
+ match(_cmd, { raw }) {
1617
+ return isReadOnlyCommand(raw) ? { category: READ_ONLY_CATEGORY, metadata: readOnlyMetadata(raw) } : null;
1618
+ }
1619
+ };
1466
1620
  BASE_RULES = [
1621
+ readOnlyRule,
1467
1622
  // ─── Git operations ───────────────────────────────────────────────────────
1468
1623
  makeRule(/^git\s+commit\b/, "shipping", (_m, _cmd, { raw, original }) => {
1469
1624
  const commitMessage = commitMessageOf(raw, original);
@@ -1648,13 +1803,6 @@ var init_patterns = __esm({
1648
1803
  tool: "bun",
1649
1804
  script: cmd.split(/\s+/)[2]
1650
1805
  })),
1651
- // ─── Code search ─────────────────────────────────────────────────────────
1652
- makeRule(/^(grep|egrep|fgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
1653
- makeRule(/^(rg|ag|ack|ripgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
1654
- makeRule(/^find\s/, "exploring", () => ({ tool: "find" })),
1655
- // ─── Code read ────────────────────────────────────────────────────────────
1656
- makeRule(/^(cat|bat|less|more)\s+\S+/, "exploring", (match) => ({ tool: match[1] })),
1657
- makeRule(/^(head|tail)\b/, "exploring", (match) => ({ tool: match[1] })),
1658
1806
  // ─── File operations ──────────────────────────────────────────────────────
1659
1807
  makeRule(/^(cp|rsync)\b/, "files", (match) => ({ tool: match[1] })),
1660
1808
  makeRule(/^(mv)\b/, "files", () => ({ tool: "mv" })),
@@ -2635,6 +2783,29 @@ var init_db = __esm({
2635
2783
  });
2636
2784
 
2637
2785
  // src/capture/redact.ts
2786
+ function sameCommand(maxParts) {
2787
+ const quoted = String.raw`"[^"]{0,${MAX_QUOTED_LENGTH}}"|'[^']{0,${MAX_QUOTED_LENGTH}}'`;
2788
+ return String.raw`(?:(?:${quoted}|\\\n|[^|;&\n"']){0,${maxParts}}?|[^|;&\n]{0,${maxParts}}?)`;
2789
+ }
2790
+ function valueAfter(prefix, flags = "g") {
2791
+ return new RegExp(`(?<=${prefix})${VALUE}`, flags);
2792
+ }
2793
+ function toolFlagValue(tools, flag) {
2794
+ return valueAfter(String.raw`${TOOL_WORD_START}(?:${tools})${TOOL_WORD_END}${SAME_COMMAND}\s${flag}`);
2795
+ }
2796
+ function quotedValuesAfterName(prefix, name, flags = "g") {
2797
+ return [
2798
+ new RegExp(String.raw`(?<=${prefix}"${name})[^"\s][^"\n]*`, flags),
2799
+ new RegExp(String.raw`(?<=${prefix}'${name})[^'\s][^'\n]*`, flags)
2800
+ ];
2801
+ }
2802
+ function valuesAfterName(prefix, name, flags = "g") {
2803
+ return [
2804
+ ...quotedValuesAfterName(prefix, name, flags),
2805
+ new RegExp(String.raw`(?<=${prefix}${name})(?:${QUOTED})`, flags),
2806
+ new RegExp(String.raw`(?<=${prefix}${name})[^\s"'|;&]+`, flags)
2807
+ ];
2808
+ }
2638
2809
  function redact(command, extraPatterns = []) {
2639
2810
  let result = command;
2640
2811
  const allPatterns = [...BUILTIN_PATTERNS];
@@ -2646,22 +2817,73 @@ function redact(command, extraPatterns = []) {
2646
2817
  }
2647
2818
  for (const pattern of allPatterns) {
2648
2819
  pattern.lastIndex = 0;
2649
- result = result.replace(pattern, "[REDACTED]");
2820
+ result = result.replace(pattern, REDACTED_TEXT);
2650
2821
  }
2651
2822
  return result;
2652
2823
  }
2653
- var BUILTIN_PATTERNS;
2824
+ var QUOTED, WORD_REST, VALUE, VALUE_OR_DASHED, MAX_COMMAND_PARTS, MAX_QUOTED_LENGTH, MAX_PARTS_BEFORE_LOGIN, MAX_PARTS_AFTER_LOGIN, SAME_COMMAND, NOT_PART_OF_A_WORD, NOT_A_NAME, NOT_IN_A_MESSAGE, TOOL_WORD_START, TOOL_WORD_END, NOT_A_PORT_MAPPING, LONG_SECRET_FLAGS, LONG_FLAG_VALUE_START, MAX_SORT_FIELD_DIGITS, SORT_FIELD, NOT_A_KEY, MAX_SLUG_WORD_LENGTH, MAX_SLUG_WORDS_AFTER_FIRST, SLUG_WORD, UUID_IN_A_KEY, SLUG_AFTER_SK, CONTAINER_LOGIN_TOOLS, EVERYDAY_HEADERS, DISTINCTIVE_HEADERS, HEADER_FLAG, SENSITIVE_VARIABLE_WORDS, QUERY_SECRET_NAME, JSON_SECRET_NAME, BUILTIN_PATTERNS, REDACTED_TEXT;
2654
2825
  var init_redact = __esm({
2655
2826
  "src/capture/redact.ts"() {
2656
2827
  "use strict";
2828
+ QUOTED = String.raw`"[^"]*"|'[^']*'`;
2829
+ WORD_REST = String.raw`(?:[^\s"');&|]|["');&|](?=[^\s;&|)]))*`;
2830
+ VALUE = String.raw`(?:${QUOTED}|[^\s"'\-]${WORD_REST})`;
2831
+ VALUE_OR_DASHED = String.raw`(?:${QUOTED}|[^\s"']${WORD_REST})`;
2832
+ MAX_COMMAND_PARTS = 1e3;
2833
+ MAX_QUOTED_LENGTH = 64e3;
2834
+ MAX_PARTS_BEFORE_LOGIN = 200;
2835
+ MAX_PARTS_AFTER_LOGIN = 300;
2836
+ SAME_COMMAND = sameCommand(MAX_COMMAND_PARTS);
2837
+ NOT_PART_OF_A_WORD = String.raw`(?<![\w.:@-])`;
2838
+ NOT_A_NAME = String.raw`(?<!--name(?:\s+|=)|\bssh\s+)`;
2839
+ NOT_IN_A_MESSAGE = String.raw`(?<!\s(?:-[as]*m|--message|--title|--body|--subject)[\s=]*["'][^"'\n]{0,${MAX_COMMAND_PARTS}})`;
2840
+ TOOL_WORD_START = `${NOT_IN_A_MESSAGE}${NOT_A_NAME}${NOT_PART_OF_A_WORD}`;
2841
+ TOOL_WORD_END = String.raw`(?:\.exe)?["'\x60)]?(?=\s)`;
2842
+ NOT_A_PORT_MAPPING = String.raw`(?!(?:[\d.]+:)?\d+:\d+(?:\/\w+)?(?=\s|$))`;
2843
+ LONG_SECRET_FLAGS = [
2844
+ "password",
2845
+ "(?!no-)[\\w-]*-password",
2846
+ "passwd",
2847
+ "passphrase",
2848
+ "pass",
2849
+ "pwd",
2850
+ "secret(?:-key)?",
2851
+ "client[-_]secret",
2852
+ "token",
2853
+ "access-token",
2854
+ "refresh-token",
2855
+ "auth[-_]?token",
2856
+ "private-key",
2857
+ "api[-_]?key"
2858
+ ].join("|");
2859
+ LONG_FLAG_VALUE_START = String.raw`(?:=|\s+(?!--))`;
2860
+ MAX_SORT_FIELD_DIGITS = 2;
2861
+ SORT_FIELD = String.raw`\d{1,${MAX_SORT_FIELD_DIGITS}}`;
2862
+ NOT_A_KEY = String.raw`(?:${SORT_FIELD}(?:[.,]${SORT_FIELD})*|[\w./~-]*\.(?:key|pem|crt|p12|pfx|der)|[\w./~-]*\/id_\w+)(?=\s|$)`;
2863
+ MAX_SLUG_WORD_LENGTH = 15;
2864
+ MAX_SLUG_WORDS_AFTER_FIRST = 8;
2865
+ SLUG_WORD = `[a-z0-9]{1,${MAX_SLUG_WORD_LENGTH}}`;
2866
+ UUID_IN_A_KEY = String.raw`[a-z-]{0,20}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`;
2867
+ SLUG_AFTER_SK = String.raw`(?!${UUID_IN_A_KEY})${SLUG_WORD}(?:-${SLUG_WORD}){0,${MAX_SLUG_WORDS_AFTER_FIRST}}(?![\w-])`;
2868
+ CONTAINER_LOGIN_TOOLS = "docker|podman|podman-remote|nerdctl|helm";
2869
+ EVERYDAY_HEADERS = "api-key|cookie|set-cookie";
2870
+ DISTINCTIVE_HEADERS = "x-api-key|x-auth-token|x-access-token";
2871
+ HEADER_FLAG = String.raw`(?<![\w-])(?:-\w*H|--(?:proxy-)?header)[\s=]*`;
2872
+ SENSITIVE_VARIABLE_WORDS = "TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL|_PWD";
2873
+ QUERY_SECRET_NAME = String.raw`key|[\w-]*(?:token|secret|password|passwd|pwd|api[-_]?key)`;
2874
+ JSON_SECRET_NAME = String.raw`[\w-]*(?:token|secret|password|passwd|pwd|api[-_]?key)`;
2657
2875
  BUILTIN_PATTERNS = [
2658
2876
  // AWS access keys
2659
2877
  /AKIA[0-9A-Z]{16}/g,
2660
2878
  // AWS secret access keys (40 char base64-ish after common env vars)
2661
- /(?<=AWS_SECRET_ACCESS_KEY[=\s]+)\S+/gi,
2879
+ new RegExp(String.raw`(?<=AWS_SECRET_ACCESS_KEY[=\s]+)${VALUE_OR_DASHED}`, "gi"),
2662
2880
  // GitHub tokens
2663
- /ghp_[a-zA-Z0-9]{36}/g,
2881
+ /\bgh[pousr]_[a-zA-Z0-9]{36}/g,
2664
2882
  /github_pat_[a-zA-Z0-9_]{82}/g,
2883
+ // AI provider, Google and npm keys
2884
+ new RegExp(String.raw`\bsk-(?!${SLUG_AFTER_SK})[\w-]{20,}`, "g"),
2885
+ /\bAIza[a-zA-Z0-9_-]{30,}/g,
2886
+ /\bnpm_[a-zA-Z0-9]{30,}/g,
2665
2887
  // GitLab tokens
2666
2888
  /glpat-[a-zA-Z0-9\-]{20}/g,
2667
2889
  // Generic JWTs (three base64url segments)
@@ -2671,17 +2893,85 @@ var init_redact = __esm({
2671
2893
  /https:\/\/hooks\.slack\.com\/services\/[A-Z0-9]+\/[A-Z0-9]+\/[a-zA-Z0-9]+/g,
2672
2894
  // Stripe keys
2673
2895
  /sk_(live|test)_[a-zA-Z0-9]{24,}/g,
2674
- // Values after long-form secret flags (single-char flags like -t/-p/-k are too ambiguous)
2675
- /(?<=--(password|passwd|secret|token|key|api[-_]?key|auth[-_]?token)\s+)\S+/gi,
2896
+ // Values after long-form secret flags, `--flag value` or `--flag=value`. `--password-stdin` isn't one: no match on its
2897
+ // `-stdin`. A bare short flag means nothing without knowing the tool: see below
2898
+ new RegExp(String.raw`(?<=(?<![\w-])--(?:${LONG_SECRET_FLAGS})${LONG_FLAG_VALUE_START})${VALUE_OR_DASHED}`, "gi"),
2899
+ new RegExp(String.raw`(?<=(?<![\w-])--key${LONG_FLAG_VALUE_START})(?!${NOT_A_KEY})${VALUE_OR_DASHED}`, "gi"),
2900
+ // A password attached to the flag: `mysql -pSECRET`. `mysql -p db` prompts, so there's no value to remove, and
2901
+ // `psql -p 5432` is a port, so psql isn't listed
2902
+ toolFlagValue(
2903
+ "mysql|mysqldump|mysqladmin|mysqlimport|mysqlshow|mysqlcheck|mariadb|mariadb-dump|mariadb-admin|mariadb-import|mariadb-show|mariadb-check",
2904
+ "-p"
2905
+ ),
2906
+ // Short flags that take a secret, for the tools where they do
2907
+ toolFlagValue("sshpass", String.raw`-p\s*`),
2908
+ toolFlagValue("mongosh|mongo|mongodump|mongorestore|mongoexport|mongoimport", String.raw`-p\s*${NOT_A_PORT_MAPPING}`),
2909
+ toolFlagValue("redis-cli", String.raw`-a\s*`),
2910
+ toolFlagValue("ssh-keygen", String.raw`-[NP]\s*`),
2911
+ toolFlagValue("zip|unzip", String.raw`-P\s*`),
2912
+ valueAfter(
2913
+ String.raw`${TOOL_WORD_START}(?:${CONTAINER_LOGIN_TOOLS})${TOOL_WORD_END}[^|;&\n]{0,${MAX_PARTS_BEFORE_LOGIN}}?\slogin` + String.raw`${TOOL_WORD_END}${sameCommand(MAX_PARTS_AFTER_LOGIN)}\s-p\s*${NOT_A_PORT_MAPPING}`
2914
+ ),
2915
+ // openssl's `-pass pass:SECRET`, also -passin and -passout
2916
+ /(?<=\s-pass(?:in|out|word)?\s+pass:)\S+/g,
2917
+ // curl's cookies, which also covers a cookie file's name: `curl -b s=SECRET`
2918
+ valueAfter(String.raw`\bcurl\b["']?${SAME_COMMAND}\s(?:--cookie|-\w*b)[\s=]+`),
2919
+ // The password half of `curl -u user:password`
2920
+ ...valuesAfterName(String.raw`\bcurl\b["']?${SAME_COMMAND}\s(?:-\w*u[\s=]*|--user[\s=]+)`, String.raw`[^:\s"']*:`),
2921
+ // Tokens handed over on stdin: `echo SECRET | docker login --password-stdin`, `gh auth login --with-token <<< SECRET`
2922
+ new RegExp(
2923
+ String.raw`(?<=\b(?:echo(?:\s+-[neE]+)*|printf(?:\s+["']?%s(?:\\n)?["']?)?)\s+)${VALUE}` + String.raw`(?=\s*\|[^;&\n]{0,${MAX_COMMAND_PARTS}}?--(?:password-stdin|with-token))`,
2924
+ "g"
2925
+ ),
2926
+ valueAfter(String.raw`--(?:password-stdin|with-token)\s*<<<\s*`),
2927
+ // Headers. A scheme or a header flag is required for Authorization, so prose like "Authorization: added middleware"
2928
+ // stays. An everyday word needs to look like a header, so `git commit -m "cookie: rotate on login"` stays: a header
2929
+ // flag, `Cookie:value` with no space, or a quoted `"Cookie: value"` outside a message
2930
+ /(?<=\bauthorization:\s*(?:bearer|basic|token)\s+)[^\s"']+/gi,
2931
+ /(?<=\bbearer\s+)(?=[A-Za-z0-9._~+/=-]*\d)[A-Za-z0-9._~+/=-]{8,}/gi,
2932
+ ...valuesAfterName(HEADER_FLAG, String.raw`(?:${EVERYDAY_HEADERS}):\s*`, "gi"),
2933
+ ...valuesAfterName(HEADER_FLAG, String.raw`authorization:\s*(?!(?:bearer|basic|token)\s)`, "gi"),
2934
+ ...quotedValuesAfterName("", String.raw`${NOT_IN_A_MESSAGE}(?:${EVERYDAY_HEADERS}):\s*`, "gi"),
2935
+ ...valuesAfterName("", String.raw`(?<![\w-])(?:${EVERYDAY_HEADERS}):`, "gi"),
2936
+ ...valuesAfterName("", String.raw`\b(?:${DISTINCTIVE_HEADERS}):\s*`, "gi"),
2937
+ // Private keys, whole blocks, as heredocs and `echo` can carry them
2938
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
2939
+ // password=... inside a string or query: `psql "host=x password=SECRET"`, `--set db.password=SECRET`
2940
+ /(?<=(?:^|[\s"'?&=.])(?:password|passwd|pwd)=)[^\s"';&]+/gi,
2941
+ // A secret in a query string or in JSON: `curl "x.io?token=SECRET"`, `curl -d '{"password":"SECRET"}'`
2942
+ new RegExp(String.raw`(?<=[?&](?:${QUERY_SECRET_NAME})=)[^\s"'&#]+`, "gi"),
2943
+ new RegExp(String.raw`(?<=["'](?:${JSON_SECRET_NAME})\\?["']\s*:\s*\\?")[^"\\\n]+`, "gi"),
2944
+ // npm's auth settings, with or without `=`: `npm config set //registry/:_password SECRET`
2945
+ /(?<=:_(?:password|auth|authToken)[=\s]+)\S+/g,
2676
2946
  // export VAR=VALUE where VAR looks sensitive
2677
- /(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
2947
+ new RegExp(String.raw`(?<=export\s+(?:[A-Z_]*(?:${SENSITIVE_VARIABLE_WORDS})[A-Z_]*)=)${VALUE_OR_DASHED}`, "gi"),
2678
2948
  // Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
2679
2949
  // Lookbehind matches only the variable-name= prefix, so only the value is replaced
2680
- /(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi,
2950
+ new RegExp(String.raw`(?<=[A-Z_][A-Z0-9_]*(?:${SENSITIVE_VARIABLE_WORDS})[A-Z0-9_]*=)${VALUE_OR_DASHED}`, "gi"),
2951
+ // The same with nothing before the keyword: `TOKEN=SECRET npm start`. Capitals only, so `--author=me` and `key=value` stay
2952
+ new RegExp(
2953
+ String.raw`(?<=(?:^|[\s;&|(])(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIALS?)=)${VALUE_OR_DASHED}`,
2954
+ "g"
2955
+ ),
2681
2956
  // Credentials embedded in connection string URLs (e.g. DATABASE_URL=postgres://user:pass@host/db)
2682
2957
  // Matches the password segment between :// user : password @ — preserves scheme and host
2683
- /(?<=[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]+:)[^@\s]+(?=@)/g
2958
+ /(?<=[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]*:)[^@\s]+(?=@)/g,
2959
+ // A token as the whole userinfo of an https URL: `git clone https://TOKEN@github.com/org/repo`
2960
+ /(?<=https?:\/\/)[^\s:@/?#]+(?=@)/g
2684
2961
  ];
2962
+ REDACTED_TEXT = "[REDACTED]";
2963
+ }
2964
+ });
2965
+
2966
+ // src/capture/hint.ts
2967
+ function commandHint(safeCommand) {
2968
+ return safeCommand.trim().slice(0, HINT_LENGTH);
2969
+ }
2970
+ var HINT_LENGTH;
2971
+ var init_hint = __esm({
2972
+ "src/capture/hint.ts"() {
2973
+ "use strict";
2974
+ HINT_LENGTH = 120;
2685
2975
  }
2686
2976
  });
2687
2977
 
@@ -2853,13 +3143,13 @@ var init_git_snapshot = __esm({
2853
3143
  });
2854
3144
 
2855
3145
  // src/capture/aider.ts
2856
- function readLastAiderPrompt(cwd) {
3146
+ function readLastAiderPrompt(cwd, extraRedactions = []) {
2857
3147
  try {
2858
3148
  const histPath = import_path8.default.join(cwd, HISTORY_FILE);
2859
3149
  const content = import_fs10.default.readFileSync(histPath, "utf-8");
2860
3150
  const matches = [...content.matchAll(/^#### (.+)$/gm)];
2861
3151
  if (!matches.length) return null;
2862
- return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
3152
+ return redact(matches[matches.length - 1][1].trim(), extraRedactions).slice(0, PROMPT_MAX_LENGTH);
2863
3153
  } catch {
2864
3154
  return null;
2865
3155
  }
@@ -2870,6 +3160,7 @@ var init_aider = __esm({
2870
3160
  "use strict";
2871
3161
  import_fs10 = __toESM(require("fs"));
2872
3162
  import_path8 = __toESM(require("path"));
3163
+ init_redact();
2873
3164
  HISTORY_FILE = ".aider.chat.history.md";
2874
3165
  PROMPT_MAX_LENGTH = 200;
2875
3166
  }
@@ -2878,7 +3169,7 @@ var init_aider = __esm({
2878
3169
  // src/capture/workscribe-command.ts
2879
3170
  function isWorkscribeStep(step2) {
2880
3171
  if (containsCommandSubstitution(step2)) return false;
2881
- const stages = splitOutsideQuotes(step2, PIPE_SEPARATORS2);
3172
+ const stages = splitOutsideQuotes(step2, PIPE_SEPARATORS3);
2882
3173
  if (startsWithWord(stages[0] ?? "", "workscribe")) return true;
2883
3174
  const isTextOrWorkscribe = (stage) => startsWithWord(stage, "workscribe") || TEXT_SOURCES.some((source) => startsWithWord(stage, source));
2884
3175
  return stages.every(isTextOrWorkscribe) && stages.some((stage) => startsWithWord(stage, "workscribe"));
@@ -2888,13 +3179,13 @@ function isWorkscribeOnlyCommand(rawCommand) {
2888
3179
  const workSteps = steps.filter((step2) => !isGlueStep(step2));
2889
3180
  return workSteps.length > 0 && workSteps.every(isWorkscribeStep);
2890
3181
  }
2891
- var PIPE_SEPARATORS2;
3182
+ var PIPE_SEPARATORS3;
2892
3183
  var init_workscribe_command = __esm({
2893
3184
  "src/capture/workscribe-command.ts"() {
2894
3185
  "use strict";
2895
3186
  init_shell_syntax();
2896
3187
  init_command_steps();
2897
- PIPE_SEPARATORS2 = ["|"];
3188
+ PIPE_SEPARATORS3 = ["|"];
2898
3189
  }
2899
3190
  });
2900
3191
 
@@ -3115,7 +3406,7 @@ function captureCommand(opts) {
3115
3406
  if (Date.now() - lastTime < TASK_RUN_DEDUP_WINDOW_MS) return;
3116
3407
  }
3117
3408
  }
3118
- const command2 = opts.command ?? opts.file ?? opts.type;
3409
+ const command2 = opts.command === void 0 ? opts.file ?? opts.type : redact(opts.command, config.redact.extra);
3119
3410
  const context2 = getRepoContext(cwd, config);
3120
3411
  const { repoName: repoName2, branch: branch2 } = context2;
3121
3412
  const ideEvent = {
@@ -3149,17 +3440,17 @@ function captureCommand(opts) {
3149
3440
  if (category === "other") {
3150
3441
  const firstToken = safeCommand.trim().split(/\s+/)[0];
3151
3442
  if (firstToken) metadata.tool = firstToken;
3152
- metadata.hint = safeCommand.trim().slice(0, 120);
3443
+ metadata.hint = commandHint(safeCommand);
3153
3444
  }
3154
3445
  if (category === "ai_session") {
3155
3446
  if (opts.gitBefore) {
3156
3447
  const commits = getCommitsSince(opts.gitBefore, workDirectory);
3157
- if (commits.length > 0) metadata.commits = commits;
3448
+ if (commits.length > 0) metadata.commits = commits.map((subject) => redact(subject, config.redact.extra));
3158
3449
  }
3159
3450
  if (config.capture.includeAiderPrompt !== false) {
3160
3451
  const toolEntry = loadAIToolRegistry().find((entry) => entry.tool === metadata.tool);
3161
3452
  if (toolEntry?.contextSource === "history-file") {
3162
- const prompt = readLastAiderPrompt(cwd);
3453
+ const prompt = readLastAiderPrompt(cwd, config.redact.extra);
3163
3454
  if (prompt) metadata.prompt = prompt;
3164
3455
  }
3165
3456
  }
@@ -3192,6 +3483,7 @@ var init_capture = __esm({
3192
3483
  import_fs14 = __toESM(require("fs"));
3193
3484
  init_db();
3194
3485
  init_redact();
3486
+ init_hint();
3195
3487
  init_git();
3196
3488
  init_git_snapshot();
3197
3489
  init_aider();
@@ -4373,10 +4665,10 @@ function installIntoFolder(options) {
4373
4665
  const outcome = installSkill({
4374
4666
  sourceDir: options.sourceDir,
4375
4667
  skillsFolder: target.skillsFolder,
4376
- version: "3.0.1",
4668
+ version: "3.2.0",
4377
4669
  force: options.force
4378
4670
  });
4379
- return describeInstallOutcome({ ...target, outcome, version: "3.0.1" });
4671
+ return describeInstallOutcome({ ...target, outcome, version: "3.2.0" });
4380
4672
  } catch (error) {
4381
4673
  return describeInstallFailure({ ...target, error });
4382
4674
  }
@@ -4395,8 +4687,8 @@ function removeSkills(agents, options) {
4395
4687
  }
4396
4688
  function formatAgentSkillLine(agent, homeDir = import_os6.default.homedir()) {
4397
4689
  const location = displayPath(import_path19.default.join(homeDir, agent.skillsFolder, SKILL_NAME), homeDir);
4398
- const installState = readInstallState(import_path19.default.join(homeDir, agent.skillsFolder), "3.0.1");
4399
- const status = describeSkillStatus(installState, "3.0.1");
4690
+ const installState = readInstallState(import_path19.default.join(homeDir, agent.skillsFolder), "3.2.0");
4691
+ const status = describeSkillStatus(installState, "3.2.0");
4400
4692
  return `${agent.name.padEnd(AGENT_NAME_COLUMN_WIDTH)} ${STATUS_COLOURS[status.level](status.text)} ${import_chalk2.default.dim(location)}`;
4401
4693
  }
4402
4694
  function formatUndetectedAgentLine(agent) {
@@ -4688,10 +4980,10 @@ async function runAIToolsStep() {
4688
4980
  }
4689
4981
  async function offerSkillInstall(group, sourceDir) {
4690
4982
  const target = describeTarget(group);
4691
- const state = readInstallState(target.skillsFolder, "3.0.1");
4983
+ const state = readInstallState(target.skillsFolder, "3.2.0");
4692
4984
  const knownOutcome = outcomeWithoutPrompt(state);
4693
4985
  if (knownOutcome) {
4694
- printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "3.0.1" }));
4986
+ printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "3.2.0" }));
4695
4987
  return;
4696
4988
  }
4697
4989
  const { installSkill: shouldInstall } = await (0, import_prompts2.default)({
@@ -5668,6 +5960,184 @@ var init_ticket_refs = __esm({
5668
5960
  }
5669
5961
  });
5670
5962
 
5963
+ // src/db/jobs/scrub-secrets.ts
5964
+ function scrubValue(value, command) {
5965
+ if (command.after.includes(value)) return value;
5966
+ const redacted = redact(value);
5967
+ if (redacted !== value) return redacted;
5968
+ return command.before.includes(value) ? REDACTED_TEXT : value;
5969
+ }
5970
+ function scrubStrings(value, command) {
5971
+ if (typeof value === "string") return scrubValue(value, command);
5972
+ if (Array.isArray(value)) return value.map((item) => scrubStrings(item, command));
5973
+ if (value === null || typeof value !== "object") return value;
5974
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, scrubStrings(item, command)]));
5975
+ }
5976
+ function hasHint(value) {
5977
+ return typeof value?.hint === "string";
5978
+ }
5979
+ function scrubMetadata(metadata, command) {
5980
+ if (metadata === null) return null;
5981
+ let parsed;
5982
+ try {
5983
+ parsed = JSON.parse(metadata);
5984
+ } catch {
5985
+ return redact(metadata);
5986
+ }
5987
+ const scrubbed = scrubStrings(parsed, command);
5988
+ if (command.after !== command.before && hasHint(scrubbed)) scrubbed.hint = commandHint(command.after);
5989
+ const rewritten = JSON.stringify(scrubbed);
5990
+ return rewritten === JSON.stringify(parsed) ? metadata : rewritten;
5991
+ }
5992
+ function scrubEvent({ command, metadata }) {
5993
+ const scrubbedCommand = redact(command);
5994
+ const scrubbedMetadata = scrubMetadata(metadata, { before: command, after: scrubbedCommand });
5995
+ if (scrubbedCommand === command && scrubbedMetadata === metadata) return null;
5996
+ return { command: scrubbedCommand, metadata: scrubbedMetadata };
5997
+ }
5998
+ function readEvents(db, afterId, limit) {
5999
+ return db.prepare(
6000
+ `SELECT id, command, metadata FROM events
6001
+ WHERE id > ? AND command IS NOT file_path
6002
+ ORDER BY id LIMIT ?`
6003
+ ).all(afterId, limit);
6004
+ }
6005
+ function runBatch5(db, afterId, limit) {
6006
+ const events = readEvents(db, afterId, limit);
6007
+ if (events.length === 0) return { lastId: null, issues: [] };
6008
+ const update = db.prepare("UPDATE events SET command = ?, metadata = ? WHERE id = ?");
6009
+ for (const event of events) {
6010
+ const scrubbed = scrubEvent(event);
6011
+ if (scrubbed) update.run(scrubbed.command, scrubbed.metadata, event.id);
6012
+ }
6013
+ return { lastId: events[events.length - 1].id, issues: [] };
6014
+ }
6015
+ function verify4(db) {
6016
+ let checked = 0;
6017
+ let unresolved = 0;
6018
+ let afterId = 0;
6019
+ for (; ; ) {
6020
+ const events = readEvents(db, afterId, VERIFY_PAGE_SIZE);
6021
+ if (events.length === 0) break;
6022
+ checked += events.length;
6023
+ unresolved += events.filter((event) => scrubEvent(event) !== null).length;
6024
+ afterId = events[events.length - 1].id;
6025
+ }
6026
+ const notes = unresolved > 0 ? [`events that would still change: ${unresolved}`] : [];
6027
+ return { checked, resolved: checked - unresolved, unresolved, hasPassed: true, notes };
6028
+ }
6029
+ function cleanUpFile(db) {
6030
+ db.exec(`PRAGMA busy_timeout = ${CLEANUP_LOCK_WAIT_MS}`);
6031
+ try {
6032
+ if (getMetaValue(db, CLEANUP_KEY) === CLEANUP_NEEDS_VACUUM) {
6033
+ db.exec("VACUUM");
6034
+ setMetaValue(db, CLEANUP_KEY, CLEANUP_NEEDS_CHECKPOINT);
6035
+ }
6036
+ const result = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
6037
+ if (result?.busy) {
6038
+ debugLog(`data job ${JOB_NAME}: file cleanup`, "left for later, a reader is busy");
6039
+ return;
6040
+ }
6041
+ deleteMetaValue(db, CLEANUP_KEY);
6042
+ } finally {
6043
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
6044
+ }
6045
+ }
6046
+ var JOB_NAME, VERIFY_PAGE_SIZE, CLEANUP_KEY, CLEANUP_NEEDS_VACUUM, CLEANUP_NEEDS_CHECKPOINT, CLEANUP_LOCK_WAIT_MS, scrubSecretsJob;
6047
+ var init_scrub_secrets = __esm({
6048
+ "src/db/jobs/scrub-secrets.ts"() {
6049
+ "use strict";
6050
+ init_transaction();
6051
+ init_store2();
6052
+ init_hint();
6053
+ init_redact();
6054
+ init_logger();
6055
+ JOB_NAME = "scrub-secrets";
6056
+ VERIFY_PAGE_SIZE = 500;
6057
+ CLEANUP_KEY = "scrub-secrets:file-cleanup";
6058
+ CLEANUP_NEEDS_VACUUM = "vacuum";
6059
+ CLEANUP_NEEDS_CHECKPOINT = "checkpoint";
6060
+ CLEANUP_LOCK_WAIT_MS = 200;
6061
+ scrubSecretsJob = {
6062
+ name: JOB_NAME,
6063
+ version: 1,
6064
+ description: "Remove secrets from stored commands and metadata",
6065
+ // Redacting what's already redacted changes nothing, so a rerun only needs the file cleaned again
6066
+ prepare: (db) => setMetaValue(db, CLEANUP_KEY, CLEANUP_NEEDS_VACUUM),
6067
+ runBatch: runBatch5,
6068
+ verify: verify4,
6069
+ cleanup: { isPending: (db) => getMetaValue(db, CLEANUP_KEY) !== null, run: cleanUpFile }
6070
+ };
6071
+ }
6072
+ });
6073
+
6074
+ // src/db/jobs/read-only-commands.ts
6075
+ function parsedObject(metadata) {
6076
+ try {
6077
+ const parsed = JSON.parse(metadata);
6078
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
6079
+ } catch {
6080
+ return null;
6081
+ }
6082
+ }
6083
+ function movedMetadata({ command, metadata }) {
6084
+ if (metadata === null) return JSON.stringify(readOnlyMetadata(command));
6085
+ const parsed = parsedObject(metadata);
6086
+ if (parsed === null) return metadata;
6087
+ const { hint: _hint, tool: _tool, ...kept } = parsed;
6088
+ return JSON.stringify({ ...kept, ...readOnlyMetadata(command) });
6089
+ }
6090
+ function readCandidates(db, afterId, limit) {
6091
+ return db.prepare(
6092
+ `SELECT id, command, metadata FROM events
6093
+ WHERE id > ? AND category IN (${CATEGORY_PLACEHOLDERS})
6094
+ ORDER BY id LIMIT ?`
6095
+ ).all(afterId, ...MOVABLE_CATEGORIES, limit);
6096
+ }
6097
+ function runBatch6(db, afterId, limit) {
6098
+ const events = readCandidates(db, afterId, limit);
6099
+ if (events.length === 0) return { lastId: null, issues: [] };
6100
+ const update = db.prepare("UPDATE events SET category = ?, metadata = ? WHERE id = ?");
6101
+ for (const event of events) {
6102
+ if (isReadOnlyCommand(event.command)) update.run(READ_ONLY_CATEGORY, movedMetadata(event), event.id);
6103
+ }
6104
+ return { lastId: events[events.length - 1].id, issues: [] };
6105
+ }
6106
+ function verify5(db) {
6107
+ let checked = 0;
6108
+ let unresolved = 0;
6109
+ let afterId = 0;
6110
+ for (; ; ) {
6111
+ const events = readCandidates(db, afterId, VERIFY_PAGE_SIZE2);
6112
+ if (events.length === 0) break;
6113
+ checked += events.length;
6114
+ unresolved += events.filter((event) => isReadOnlyCommand(event.command)).length;
6115
+ afterId = events[events.length - 1].id;
6116
+ }
6117
+ const notes = unresolved > 0 ? [`read-only events not moved: ${unresolved}`] : [];
6118
+ return { checked, resolved: checked - unresolved, unresolved, hasPassed: true, notes };
6119
+ }
6120
+ var JOB_NAME2, VERIFY_PAGE_SIZE2, MOVABLE_CATEGORIES, CATEGORY_PLACEHOLDERS, readOnlyCommandsJob;
6121
+ var init_read_only_commands = __esm({
6122
+ "src/db/jobs/read-only-commands.ts"() {
6123
+ "use strict";
6124
+ init_read_only();
6125
+ JOB_NAME2 = "read-only-commands";
6126
+ VERIFY_PAGE_SIZE2 = 500;
6127
+ MOVABLE_CATEGORIES = ["versioning", "other", "files", "running"];
6128
+ CATEGORY_PLACEHOLDERS = MOVABLE_CATEGORIES.map(() => "?").join(", ");
6129
+ readOnlyCommandsJob = {
6130
+ name: JOB_NAME2,
6131
+ version: 1,
6132
+ description: "Move stored read-only commands out of summaries",
6133
+ // Moved rows are simply not candidates any more, so a rerun has nothing to clear first
6134
+ prepare: () => void 0,
6135
+ runBatch: runBatch6,
6136
+ verify: verify5
6137
+ };
6138
+ }
6139
+ });
6140
+
5671
6141
  // src/db/jobs/index.ts
5672
6142
  var JOBS;
5673
6143
  var init_jobs = __esm({
@@ -5678,10 +6148,14 @@ var init_jobs = __esm({
5678
6148
  init_summary_anchors();
5679
6149
  init_drop_old_sessions();
5680
6150
  init_ticket_refs();
6151
+ init_scrub_secrets();
6152
+ init_read_only_commands();
5681
6153
  JOBS = [
5682
6154
  createProjectDimensionsJob({ probeDirectory }),
5683
6155
  summaryAnchorsJob,
5684
6156
  ticketRefsJob,
6157
+ scrubSecretsJob,
6158
+ readOnlyCommandsJob,
5685
6159
  dropOldSessionsJob
5686
6160
  ];
5687
6161
  }
@@ -5846,9 +6320,22 @@ function recordFailure(db, job, message) {
5846
6320
  debugLog(`data job ${job.name}: couldn't record the failure`, error);
5847
6321
  }
5848
6322
  }
6323
+ function runPendingCleanups(db, jobs) {
6324
+ for (const job of jobs) {
6325
+ if (!job.cleanup || !isJobDone(getJobRow(db, job.name), job)) continue;
6326
+ try {
6327
+ if (job.cleanup.isPending(db)) job.cleanup.run(db);
6328
+ } catch (error) {
6329
+ debugLog(`data job ${job.name}: cleanup`, error);
6330
+ }
6331
+ }
6332
+ }
5849
6333
  function runPendingJobs(db, jobs, options) {
5850
6334
  const pending = jobs.filter((job) => !isJobDone(getJobRow(db, job.name), job));
5851
- if (pending.length === 0) return { backup: "not-needed", jobs: [] };
6335
+ if (pending.length === 0) {
6336
+ runPendingCleanups(db, jobs);
6337
+ return { backup: "not-needed", jobs: [] };
6338
+ }
5852
6339
  const backup = options.ensureBackup();
5853
6340
  if (backup !== "ready") return { backup, jobs: [] };
5854
6341
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
@@ -5865,6 +6352,7 @@ function runPendingJobs(db, jobs, options) {
5865
6352
  results.push(result);
5866
6353
  if (result.outcome !== "done") break;
5867
6354
  }
6355
+ runPendingCleanups(db, jobs);
5868
6356
  return { backup: "ready", jobs: results };
5869
6357
  }
5870
6358
  var DEFAULT_BATCH_SIZE, BATCH_LOCK_WAIT_MS, NEWER_VERSION_ERROR, SupersededError;
@@ -6565,6 +7053,7 @@ function eventRowToPayload(row) {
6565
7053
  branch: row.branch,
6566
7054
  capturedAt: row.captured_at,
6567
7055
  ...row.source ? { source: row.source } : {},
7056
+ ...row.agent_session_id == null ? {} : { agentSessionId: row.agent_session_id },
6568
7057
  ...gitMovesField(row)
6569
7058
  };
6570
7059
  if (row.category !== "other" || typeof metadata.hint !== "string") return payload;
@@ -7378,8 +7867,52 @@ var init_sessions = __esm({
7378
7867
  }
7379
7868
  });
7380
7869
 
7870
+ // src/ai/engines/template/utils.ts
7871
+ function pluralize(count, noun) {
7872
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
7873
+ }
7874
+ function stringsIn(value) {
7875
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "") : [];
7876
+ }
7877
+ function unique(values) {
7878
+ return [...new Set(values)];
7879
+ }
7880
+ function eventsOf(events, category) {
7881
+ return events.filter((event) => event.category === category);
7882
+ }
7883
+ function toBulletList(bullets) {
7884
+ return bullets.map((bullet) => `- ${bullet}`).join("\n");
7885
+ }
7886
+ var init_utils3 = __esm({
7887
+ "src/ai/engines/template/utils.ts"() {
7888
+ "use strict";
7889
+ }
7890
+ });
7891
+
7892
+ // src/ai/engines/template/agents.ts
7893
+ function joinNames(names) {
7894
+ if (names.length <= 1) return names.join("");
7895
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
7896
+ }
7897
+ function describeAgents(events) {
7898
+ const agentEvents = events.filter((event) => event.source !== void 0 && AGENT_NAMES.has(event.source));
7899
+ if (agentEvents.length === 0) return null;
7900
+ const names = [...new Set(agentEvents.map((event) => AGENT_NAMES.get(event.source)))];
7901
+ const sessionIds = new Set(agentEvents.flatMap((event) => event.agentSessionId ?? []));
7902
+ return sessionIds.size > 0 ? `${joinNames(names)} (${pluralize(sessionIds.size, "session")})` : joinNames(names);
7903
+ }
7904
+ var AGENT_NAMES;
7905
+ var init_agents2 = __esm({
7906
+ "src/ai/engines/template/agents.ts"() {
7907
+ "use strict";
7908
+ init_ai_tools();
7909
+ init_utils3();
7910
+ AGENT_NAMES = new Map(ai_tools_default.flatMap((tool) => tool.hook ? [[tool.hook, tool.displayName]] : []));
7911
+ }
7912
+ });
7913
+
7381
7914
  // src/ai/engines/template/constants.ts
7382
- var MAX_SESSION_BULLETS, MAX_COMMIT_BULLETS, MAX_DAY_BULLETS, MAX_LISTED_PACKAGES, AGENT_SOURCES, TEST_WORDING, BUILD_WORDING, STORY_TEST_WORDING, STORY_BUILD_WORDING;
7915
+ var MAX_SESSION_BULLETS, MAX_COMMIT_BULLETS, MAX_DAY_BULLETS, MAX_LISTED_PACKAGES, TEST_WORDING, BUILD_WORDING, STORY_TEST_WORDING, STORY_BUILD_WORDING;
7383
7916
  var init_constants2 = __esm({
7384
7917
  "src/ai/engines/template/constants.ts"() {
7385
7918
  "use strict";
@@ -7387,7 +7920,6 @@ var init_constants2 = __esm({
7387
7920
  MAX_COMMIT_BULLETS = 3;
7388
7921
  MAX_DAY_BULLETS = 5;
7389
7922
  MAX_LISTED_PACKAGES = 6;
7390
- AGENT_SOURCES = ["claude-code"];
7391
7923
  TEST_WORDING = {
7392
7924
  category: "testing",
7393
7925
  resolved: "Resolved failing tests",
@@ -7416,28 +7948,6 @@ var init_constants2 = __esm({
7416
7948
  }
7417
7949
  });
7418
7950
 
7419
- // src/ai/engines/template/utils.ts
7420
- function pluralize(count, noun) {
7421
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
7422
- }
7423
- function stringsIn(value) {
7424
- return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item !== "") : [];
7425
- }
7426
- function unique(values) {
7427
- return [...new Set(values)];
7428
- }
7429
- function eventsOf(events, category) {
7430
- return events.filter((event) => event.category === category);
7431
- }
7432
- function toBulletList(bullets) {
7433
- return bullets.map((bullet) => `- ${bullet}`).join("\n");
7434
- }
7435
- var init_utils3 = __esm({
7436
- "src/ai/engines/template/utils.ts"() {
7437
- "use strict";
7438
- }
7439
- });
7440
-
7441
7951
  // src/ai/engines/template/describe.ts
7442
7952
  function describeNotes(events) {
7443
7953
  return eventsOf(events, "note").flatMap((event) => stringsIn([event.metadata.text]).map((text) => `Note: ${text}`));
@@ -7509,8 +8019,8 @@ function describePushes(events) {
7509
8019
  return targets.length > 0 ? `Pushed to ${targets.join(", ")}` : null;
7510
8020
  }
7511
8021
  function describeAgentWork(events) {
7512
- const agentCommands = events.filter((event) => event.source !== void 0 && AGENT_SOURCES.includes(event.source));
7513
- return agentCommands.length > 0 ? `Worked with an AI coding assistant (${pluralize(agentCommands.length, "command")})` : null;
8022
+ const agents = describeAgents(events);
8023
+ return agents === null ? null : `Worked with ${agents}`;
7514
8024
  }
7515
8025
  function describeEdits(events) {
7516
8026
  const saves = eventsOf(events, "code_edit").length;
@@ -7542,6 +8052,7 @@ var init_describe = __esm({
7542
8052
  "use strict";
7543
8053
  init_commits();
7544
8054
  init_exclusions();
8055
+ init_agents2();
7545
8056
  init_constants2();
7546
8057
  init_utils3();
7547
8058
  }
@@ -7681,12 +8192,10 @@ function branchBullets(group) {
7681
8192
  ].filter((bullet) => bullet !== null);
7682
8193
  }
7683
8194
  function describeActivityLine(allEvents, events) {
7684
- const agentCommands = allEvents.filter(
7685
- (event) => event.source !== void 0 && AGENT_SOURCES.includes(event.source)
7686
- ).length;
8195
+ const agents = describeAgents(allEvents);
7687
8196
  const saves = eventsOf(events, "code_edit").length;
7688
8197
  const parts = [
7689
- agentCommands > 0 ? `With an AI coding assistant (${pluralize(agentCommands, "command")})` : null,
8198
+ agents === null ? null : `With ${agents}`,
7690
8199
  saves > 0 ? `${pluralize(saves, "file save")} in the editor` : null
7691
8200
  ].filter((part) => part !== null);
7692
8201
  return parts.length > 0 ? parts.join(" \xB7 ") : null;
@@ -7731,6 +8240,7 @@ var init_story = __esm({
7731
8240
  "src/ai/engines/template/story.ts"() {
7732
8241
  "use strict";
7733
8242
  init_heading();
8243
+ init_agents2();
7734
8244
  init_constants2();
7735
8245
  init_describe();
7736
8246
  init_branch_moves();
@@ -8120,6 +8630,28 @@ var init_day = __esm({
8120
8630
  }
8121
8631
  });
8122
8632
 
8633
+ // src/summary/output-format.ts
8634
+ function keepsStdoutClean(format) {
8635
+ return format === "json" || format === "text";
8636
+ }
8637
+ var SUMMARY_FORMATS;
8638
+ var init_output_format = __esm({
8639
+ "src/summary/output-format.ts"() {
8640
+ "use strict";
8641
+ SUMMARY_FORMATS = ["markdown", "json", "text"];
8642
+ }
8643
+ });
8644
+
8645
+ // src/summary/plain-text.ts
8646
+ function toPlainText(markdown) {
8647
+ return markdown.replace(/^#{1,6} +/gm, "").replace(/^([ \t]*)[*•] +/gm, "$1- ").replace(/\*\*([^*\n]+)\*\*/g, "$1").replace(/`([^`\n]+)`/g, "$1").replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, "$1 ($2)").trim();
8648
+ }
8649
+ var init_plain_text = __esm({
8650
+ "src/summary/plain-text.ts"() {
8651
+ "use strict";
8652
+ }
8653
+ });
8654
+
8123
8655
  // src/cli/day-notes.ts
8124
8656
  function printExcludedFooter(sessionPayloads, excluded) {
8125
8657
  const excludedCounts = countExcludedEvents(sessionPayloads, excluded);
@@ -8195,10 +8727,11 @@ async function runWeeklySummary({ options, config, provider, loadDay }) {
8195
8727
  }
8196
8728
  const payload = { weekStart, weekEnd: today, by: options.by ?? "project", days };
8197
8729
  const summary = await writeWeeklySummary(provider, payload);
8198
- const isJson = config.summary?.format === "json";
8199
- if (isJson) log.plain(weekJson(payload, config.summary?.includeCategories ?? []));
8730
+ const format = options.format ?? config.summary?.format;
8731
+ if (format === "json") log.plain(weekJson(payload, config.summary?.includeCategories ?? []));
8732
+ else if (format === "text") log.plain(toPlainText(summary));
8200
8733
  else printSummary(summary);
8201
- await postSummary({ options, config, summary, statusToStderr: isJson });
8734
+ await postSummary({ options, config, summary, statusToStderr: keepsStdoutClean(format) });
8202
8735
  }
8203
8736
  var WEEK_GIT_BUDGET_MS;
8204
8737
  var init_weekly = __esm({
@@ -8209,6 +8742,8 @@ var init_weekly = __esm({
8209
8742
  init_payload();
8210
8743
  init_webhook2();
8211
8744
  init_day_commits();
8745
+ init_output_format();
8746
+ init_plain_text();
8212
8747
  init_week();
8213
8748
  WEEK_GIT_BUDGET_MS = DAY_GIT_BUDGET_MS;
8214
8749
  }
@@ -8224,14 +8759,24 @@ function parseWeekLayout(value) {
8224
8759
  if (!layout) fail("--by must be project or day");
8225
8760
  return layout;
8226
8761
  }
8762
+ function addSummaryFormatOption(command) {
8763
+ return command.option("--format <format>", "print as markdown, json or text, overriding summary.format");
8764
+ }
8765
+ function parseSummaryFormat(value) {
8766
+ if (value === void 0) return void 0;
8767
+ const format = SUMMARY_FORMATS.find((candidate) => candidate === value);
8768
+ if (!format) fail("--format must be markdown, json or text");
8769
+ return format;
8770
+ }
8227
8771
  async function runWeekCommand(options) {
8228
8772
  const by = parseWeekLayout(options.by);
8773
+ const format = parseSummaryFormat(options.format);
8229
8774
  const config = readConfig();
8230
8775
  printLegacyAiNotice(config);
8231
8776
  requireThreadData();
8232
8777
  try {
8233
8778
  await runWeeklySummary({
8234
- options: { ...options, by },
8779
+ options: { ...options, by, format },
8235
8780
  config,
8236
8781
  provider: createSummaryWriter(config),
8237
8782
  loadDay: loadDayThreads
@@ -8242,13 +8787,14 @@ async function runWeekCommand(options) {
8242
8787
  }
8243
8788
  function registerWeek(program) {
8244
8789
  const weekCommand = program.command("week").description("Summarize the current week (Mon\u2013today) \u2014 the same as summary --week").option("--fresh", "accepted for 1.x scripts \u2014 the week is always rendered fresh").option("--slack", "post to Slack webhook from config").option("--webhook <url>", "post to a custom webhook URL");
8245
- addWeekLayoutOption(weekCommand).action(runWeekCommand);
8790
+ addSummaryFormatOption(addWeekLayoutOption(weekCommand)).action(runWeekCommand);
8246
8791
  }
8247
8792
  var WEEK_LAYOUTS;
8248
8793
  var init_week2 = __esm({
8249
8794
  "src/cli/week.ts"() {
8250
8795
  "use strict";
8251
8796
  init_config();
8797
+ init_output_format();
8252
8798
  init_weekly();
8253
8799
  init_writer();
8254
8800
  init_notices();
@@ -8293,8 +8839,9 @@ function printCapturedEvents(sessionPayloads, excluded) {
8293
8839
  }
8294
8840
  function registerSummary(program) {
8295
8841
  const summaryCommand = program.command("summary").description("Generate a work summary").option("--date <YYYY-MM-DD>", "date to summarize (default: today)").option("--week", "summarize the current week").option("--fresh", "ignore saved summaries and render the template").option("--verbose", "show all captured events, including those left out of the summary").option("--slack", "post summary to Slack webhook from config").option("--webhook <url>", "post summary to a custom webhook URL");
8296
- addWeekLayoutOption(summaryCommand).action(async (options) => {
8842
+ addSummaryFormatOption(addWeekLayoutOption(summaryCommand)).action(async (options) => {
8297
8843
  if (options.by !== void 0 && !options.week) fail("--by only applies to --week");
8844
+ const formatOption = parseSummaryFormat(options.format);
8298
8845
  if (options.week) {
8299
8846
  await runWeekCommand(options);
8300
8847
  return;
@@ -8317,7 +8864,8 @@ function registerSummary(program) {
8317
8864
  log.plain(`No activity captured for ${date}`);
8318
8865
  return;
8319
8866
  }
8320
- const jsonMode = config.summary?.format === "json";
8867
+ const format = formatOption ?? config.summary?.format;
8868
+ const keepsStdoutToOutput = keepsStdoutClean(format);
8321
8869
  let day;
8322
8870
  try {
8323
8871
  day = await renderDay({ date, blocks, config, provider, fresh: options.fresh });
@@ -8328,11 +8876,13 @@ function registerSummary(program) {
8328
8876
  const { sessionPayloads, saved, fullSummary, needsGeneration } = day;
8329
8877
  const excluded = buildExcludedSet(config.summary?.includeCategories ?? []);
8330
8878
  if (needsGeneration && options.verbose) {
8331
- if (jsonMode) log.notice("--verbose is skipped when summary.format is json, to keep the JSON output clean");
8879
+ if (keepsStdoutToOutput) log.notice(`--verbose is skipped with the ${format} format, to keep the output clean`);
8332
8880
  else printCapturedEvents(sessionPayloads, excluded);
8333
8881
  }
8334
- if (jsonMode) {
8882
+ if (format === "json") {
8335
8883
  log.plain(toJsonOutput(day, date));
8884
+ } else if (format === "text") {
8885
+ log.plain(toPlainText(fullSummary));
8336
8886
  } else {
8337
8887
  log.plain("");
8338
8888
  log.plain(fullSummary);
@@ -8340,7 +8890,7 @@ function registerSummary(program) {
8340
8890
  printSavedNote(saved);
8341
8891
  if (needsGeneration) printExcludedFooter(sessionPayloads, excluded);
8342
8892
  }
8343
- await postSummary({ options, config, summary: fullSummary, statusToStderr: jsonMode });
8893
+ await postSummary({ options, config, summary: fullSummary, statusToStderr: keepsStdoutToOutput });
8344
8894
  });
8345
8895
  }
8346
8896
  var import_chalk8;
@@ -8357,6 +8907,8 @@ var init_summary = __esm({
8357
8907
  init_notices();
8358
8908
  init_webhook2();
8359
8909
  init_day();
8910
+ init_output_format();
8911
+ init_plain_text();
8360
8912
  init_day_notes();
8361
8913
  init_thread_day();
8362
8914
  init_week2();
@@ -8980,7 +9532,8 @@ var init_profile = __esm({
8980
9532
  function registerNote(program) {
8981
9533
  program.command("note <text>").description("Attach a free-text annotation to the current session").action((text) => {
8982
9534
  const directory = currentDirectory();
8983
- const context = getRepoContext(directory, readConfig());
9535
+ const config = readConfig();
9536
+ const context = getRepoContext(directory, config);
8984
9537
  const { repoName, branch } = context;
8985
9538
  insertEvent({
8986
9539
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8991,7 +9544,7 @@ function registerNote(program) {
8991
9544
  repo_name: repoName,
8992
9545
  branch,
8993
9546
  category: "note",
8994
- metadata: JSON.stringify({ text }),
9547
+ metadata: JSON.stringify({ text: redact(text, config.redact.extra) }),
8995
9548
  source: null,
8996
9549
  file_path: null,
8997
9550
  dimensions: eventDimensions(context)
@@ -9005,6 +9558,7 @@ var init_note = __esm({
9005
9558
  init_db();
9006
9559
  init_git();
9007
9560
  init_dimensions3();
9561
+ init_redact();
9008
9562
  init_config();
9009
9563
  init_working_directory();
9010
9564
  init_logger();
@@ -9034,7 +9588,7 @@ function registerBugReport(program) {
9034
9588
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9035
9589
  const reportPath = import_path30.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
9036
9590
  const systemInfo = [
9037
- `Workscribe version : ${"3.0.1"}`,
9591
+ `Workscribe version : ${"3.2.0"}`,
9038
9592
  `Node.js version : ${process.version}`,
9039
9593
  `Platform : ${process.platform}`,
9040
9594
  `Architecture : ${process.arch}`,
@@ -9065,7 +9619,7 @@ function registerBugReport(program) {
9065
9619
  process.exit(1);
9066
9620
  }
9067
9621
  const params = new URLSearchParams({
9068
- title: `Bug report \u2014 workscribe ${"3.0.1"}`,
9622
+ title: `Bug report \u2014 workscribe ${"3.2.0"}`,
9069
9623
  body: `<!-- Attach ${import_path30.default.basename(reportPath)} or paste the relevant sections below -->
9070
9624
 
9071
9625
  **Describe the issue:**
@@ -9156,7 +9710,7 @@ function reportRun(result) {
9156
9710
  function registerUpgrade(program) {
9157
9711
  program.command("upgrade").description("Bring your stored data up to date after updating Workscribe (backs it up first)").action(() => {
9158
9712
  const announced = /* @__PURE__ */ new Set();
9159
- const result = runDataJobs("3.0.1", {
9713
+ const result = runDataJobs("3.2.0", {
9160
9714
  onProgress: ({ job }) => {
9161
9715
  if (announced.has(job.name)) return;
9162
9716
  announced.add(job.name);
@@ -9646,7 +10200,7 @@ function isNewer(latest, current) {
9646
10200
  return latestPatch > currentPatch;
9647
10201
  }
9648
10202
  function checkForUpdate() {
9649
- const current = "3.0.1";
10203
+ const current = "3.2.0";
9650
10204
  const cache = readCache();
9651
10205
  if (cache && isNewer(cache.latestVersion, current)) {
9652
10206
  process.stderr.write(
@@ -9669,7 +10223,7 @@ function checkForUpdate() {
9669
10223
  function buildTelemetryPayload(config) {
9670
10224
  return {
9671
10225
  installId: config.telemetry.installId,
9672
- cliVersion: "3.0.1",
10226
+ cliVersion: "3.2.0",
9673
10227
  platform: import_os12.default.platform(),
9674
10228
  nodeVersion: process.version
9675
10229
  };
@@ -9747,10 +10301,10 @@ var require_main = __commonJS({
9747
10301
  pingTelemetry();
9748
10302
  }
9749
10303
  if (shouldRunDataJobs(process.argv.slice(2))) {
9750
- runDataJobsBeforeCommand(invokedCommand, "3.0.1");
10304
+ runDataJobsBeforeCommand(invokedCommand, "3.2.0");
9751
10305
  }
9752
10306
  var program = new import_commander.Command();
9753
- program.name("workscribe").description("Automatic developer work journal").version("3.0.1");
10307
+ program.name("workscribe").description("Automatic developer work journal").version("3.2.0");
9754
10308
  registerCapture(program);
9755
10309
  registerHook(program);
9756
10310
  registerInit(program);