runtape 0.4.0 → 0.6.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
@@ -40,9 +40,17 @@ var paths = {
40
40
  };
41
41
 
42
42
  // src/lib/config.ts
43
+ var WatchMode = z.enum(["allow_all", "deny_list", "allow_list"]);
44
+ var WatchConfig = z.object({
45
+ mode: WatchMode.default("allow_all"),
46
+ paths: z.array(z.string()).default([])
47
+ });
43
48
  var Config = z.object({
44
49
  api_key: z.string().regex(/^rtk_[a-f0-9]{64}$/, "api_key must be rtk_ followed by 64 hex chars"),
45
- server_url: z.string().url()
50
+ server_url: z.string().url(),
51
+ // Optional so any existing 0.5.x config keeps parsing — push.ts treats
52
+ // missing/undefined as allow_all.
53
+ watch: WatchConfig.optional()
46
54
  });
47
55
  var DEFAULT_SERVER_URL = process.env.RUNTAPE_API_URL ?? "https://runtape.dev";
48
56
  async function readConfig() {
@@ -742,7 +750,34 @@ async function persistSubagentCursor(parentSessionId, agentToolUseId, seen, newl
742
750
  await writeFile6(file, trimmed.join("\n") + "\n");
743
751
  }
744
752
 
753
+ // src/lib/watch.ts
754
+ import { homedir as homedir2 } from "os";
755
+ import { resolve as resolve2 } from "path";
756
+ function isCaptureAllowed(watch, cwd) {
757
+ if (!watch || watch.mode === "allow_all") return true;
758
+ if (watch.paths.length === 0) {
759
+ return watch.mode === "deny_list";
760
+ }
761
+ const cwdNorm = normalizePath(cwd);
762
+ const matched = watch.paths.some((p) => isPrefixMatch(cwdNorm, normalizePath(p)));
763
+ return watch.mode === "allow_list" ? matched : !matched;
764
+ }
765
+ function isPrefixMatch(cwd, prefix) {
766
+ return cwd === prefix || cwd.startsWith(prefix + "/");
767
+ }
768
+ function normalizePath(input4) {
769
+ let expanded = input4;
770
+ if (expanded === "~") expanded = homedir2();
771
+ else if (expanded.startsWith("~/")) expanded = `${homedir2()}/${expanded.slice(2)}`;
772
+ const abs = resolve2(expanded);
773
+ return abs.replace(/\/+$/, "") || "/";
774
+ }
775
+
745
776
  // src/commands/push.ts
777
+ function isDisabledByEnv() {
778
+ const v = (process.env.RUNTAPE_DISABLE ?? "").trim().toLowerCase();
779
+ return v === "1" || v === "true" || v === "yes";
780
+ }
746
781
  async function readStdin() {
747
782
  if (process.stdin.isTTY) return "";
748
783
  const chunks = [];
@@ -761,6 +796,7 @@ function spawnFlusher(cliBinPath) {
761
796
  }
762
797
  async function pushCommand(opts) {
763
798
  try {
799
+ if (isDisabledByEnv()) return 0;
764
800
  const cfg = await readConfig();
765
801
  if (!cfg) {
766
802
  process.stderr.write("runtape: not logged in \u2014 skipping event\n");
@@ -783,6 +819,10 @@ async function pushCommand(opts) {
783
819
  process.stderr.write("runtape: missing session_id on hook payload\n");
784
820
  return 0;
785
821
  }
822
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
823
+ if (cwd && !isCaptureAllowed(cfg.watch, cwd)) {
824
+ return 0;
825
+ }
786
826
  const sequence = await nextSequence(sessionId);
787
827
  const result = mapHookPayload(opts.event, payload, {
788
828
  wall_ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -838,13 +878,13 @@ async function pushCommand(opts) {
838
878
  try {
839
879
  const { emits, seen } = await readNewSubagentEvents(sessionId, agentToolUseId, subTranscript);
840
880
  const newlyEmitted = [];
841
- const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
881
+ const cwd2 = typeof payload.cwd === "string" ? payload.cwd : "";
842
882
  for (const e of emits) {
843
883
  const seq = await nextSequence(sessionId);
844
884
  const baseEnvelope = {
845
885
  session_id: sessionId,
846
886
  transcript_path: subTranscript,
847
- cwd,
887
+ cwd: cwd2,
848
888
  hook_event_name: opts.event,
849
889
  permission_mode: typeof payload.permission_mode === "string" ? payload.permission_mode : void 0,
850
890
  wall_ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1101,12 +1141,115 @@ Step 3/3 \u2014 Claude Code hooks
1101
1141
  return 0;
1102
1142
  }
1103
1143
 
1144
+ // src/commands/watch.ts
1145
+ function modeLabel(mode) {
1146
+ if (mode === "allow_all") return "allow_all (capturing every session)";
1147
+ if (mode === "allow_list") return "allow_list (capturing ONLY listed paths)";
1148
+ return "deny_list (capturing every session EXCEPT listed paths)";
1149
+ }
1150
+ async function readOrExit() {
1151
+ const cfg = await readConfig();
1152
+ if (!cfg) {
1153
+ process.stderr.write("Not logged in. Run `runtape login` first.\n");
1154
+ process.exit(1);
1155
+ }
1156
+ return cfg;
1157
+ }
1158
+ function ensureWatch(cfg) {
1159
+ return cfg.watch ? { mode: cfg.watch.mode, paths: [...cfg.watch.paths] } : { mode: "allow_all", paths: [] };
1160
+ }
1161
+ async function watchListCommand() {
1162
+ const cfg = await readOrExit();
1163
+ const w = ensureWatch(cfg);
1164
+ process.stdout.write(`Mode: ${modeLabel(w.mode)}
1165
+ `);
1166
+ if (w.paths.length === 0) {
1167
+ process.stdout.write("Paths: (none)\n");
1168
+ } else {
1169
+ process.stdout.write("Paths:\n");
1170
+ for (const p of w.paths) process.stdout.write(` ${p}
1171
+ `);
1172
+ }
1173
+ process.stdout.write("\nEnv override: set RUNTAPE_DISABLE=1 to skip capture for a single session.\n");
1174
+ return 0;
1175
+ }
1176
+ async function watchIgnoreCommand(path) {
1177
+ const cfg = await readOrExit();
1178
+ const w = ensureWatch(cfg);
1179
+ if (w.mode === "allow_list") {
1180
+ process.stderr.write(
1181
+ "Current mode is allow_list (only listed paths are captured). `ignore` only makes sense in allow_all/deny_list mode.\nRun `runtape watch reset` to start over, or use `runtape watch only <path>` to add to the allow list.\n"
1182
+ );
1183
+ return 1;
1184
+ }
1185
+ const normalized = normalizePath(path);
1186
+ if (w.paths.includes(normalized)) {
1187
+ process.stdout.write(`Already ignored: ${normalized}
1188
+ `);
1189
+ return 0;
1190
+ }
1191
+ w.paths.push(normalized);
1192
+ const next = w.mode === "allow_all" ? "deny_list" : w.mode;
1193
+ await writeConfig({ ...cfg, watch: { mode: next, paths: w.paths } });
1194
+ if (next !== w.mode) process.stdout.write(`Mode switched to deny_list.
1195
+ `);
1196
+ process.stdout.write(`Ignoring ${normalized}
1197
+ `);
1198
+ return 0;
1199
+ }
1200
+ async function watchOnlyCommand(path) {
1201
+ const cfg = await readOrExit();
1202
+ const w = ensureWatch(cfg);
1203
+ if (w.mode === "deny_list") {
1204
+ process.stderr.write(
1205
+ "Current mode is deny_list (all except listed paths). `only` would conflict.\nRun `runtape watch reset` to start over, then `runtape watch only <path>` to switch to allow_list.\n"
1206
+ );
1207
+ return 1;
1208
+ }
1209
+ const normalized = normalizePath(path);
1210
+ if (w.paths.includes(normalized)) {
1211
+ process.stdout.write(`Already in allow list: ${normalized}
1212
+ `);
1213
+ return 0;
1214
+ }
1215
+ w.paths.push(normalized);
1216
+ const next = w.mode === "allow_all" ? "allow_list" : w.mode;
1217
+ await writeConfig({ ...cfg, watch: { mode: next, paths: w.paths } });
1218
+ if (next !== w.mode) process.stdout.write(`Mode switched to allow_list.
1219
+ `);
1220
+ process.stdout.write(`Capturing only: ${normalized}
1221
+ `);
1222
+ return 0;
1223
+ }
1224
+ async function watchUnignoreCommand(path) {
1225
+ const cfg = await readOrExit();
1226
+ const w = ensureWatch(cfg);
1227
+ const normalized = normalizePath(path);
1228
+ const filtered = w.paths.filter((p) => p !== normalized);
1229
+ if (filtered.length === w.paths.length) {
1230
+ process.stdout.write(`Not in the list: ${normalized}
1231
+ `);
1232
+ return 0;
1233
+ }
1234
+ await writeConfig({ ...cfg, watch: { mode: w.mode, paths: filtered } });
1235
+ process.stdout.write(`Removed: ${normalized}
1236
+ `);
1237
+ return 0;
1238
+ }
1239
+ async function watchResetCommand() {
1240
+ const cfg = await readOrExit();
1241
+ const { watch: _unused, ...rest } = cfg;
1242
+ await writeConfig(rest);
1243
+ process.stdout.write("Reset to allow_all. All sessions will be captured.\n");
1244
+ return 0;
1245
+ }
1246
+
1104
1247
  // src/lib/flusher.ts
1105
1248
  import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile8, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
1106
1249
  import { dirname as dirname7 } from "path";
1107
- var POLL_INTERVAL_MS = 1500;
1250
+ var POLL_INTERVAL_MS = 5e3;
1108
1251
  var IDLE_EXIT_MS = 3e4;
1109
- var BATCH_MAX = 100;
1252
+ var BATCH_MAX = 500;
1110
1253
  var BACKOFF_STEPS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
1111
1254
  async function log(line) {
1112
1255
  try {
@@ -1147,7 +1290,7 @@ async function releasePidLock() {
1147
1290
  }
1148
1291
  }
1149
1292
  function delay(ms) {
1150
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1293
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1151
1294
  }
1152
1295
  async function drainSession(sessionId, serverUrl, apiKey) {
1153
1296
  const snapshot = await readBufferedSession(sessionId);
@@ -1243,6 +1386,12 @@ if (process.argv.includes("--internal-flusher")) {
1243
1386
  program.command("push").description("Internal: invoked by Claude Code hooks. Reads stdin and buffers an event.").requiredOption("--event <name>", "Claude hook event name (SessionStart, PostToolUse, \u2026)").action(async (opts) => process.exit(await pushCommand(opts)));
1244
1387
  program.command("status").description("Show current login, buffer state, and server reachability.").action(async () => process.exit(await statusCommand()));
1245
1388
  program.command("runs").description("Open your Runtape dashboard in the default browser.").action(async () => process.exit(await runsCommand()));
1389
+ const watch = program.command("watch").description("Control which directories the hooks capture (allow / deny lists).");
1390
+ watch.command("list").description("Show the current watch mode and paths.").action(async () => process.exit(await watchListCommand()));
1391
+ watch.command("ignore <path>").description("Stop capturing sessions whose cwd is under <path>. Switches mode to deny_list.").action(async (path) => process.exit(await watchIgnoreCommand(path)));
1392
+ watch.command("only <path>").description("Capture ONLY sessions whose cwd is under <path>. Switches mode to allow_list.").action(async (path) => process.exit(await watchOnlyCommand(path)));
1393
+ watch.command("unignore <path>").description("Remove <path> from the current watch list.").action(async (path) => process.exit(await watchUnignoreCommand(path)));
1394
+ watch.command("reset").description("Capture every session (clear all watch settings).").action(async () => process.exit(await watchResetCommand()));
1246
1395
  program.parseAsync(process.argv).catch((err) => {
1247
1396
  console.error(err);
1248
1397
  process.exit(1);