runtape 0.5.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 +153 -4
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +64 -64
- package/package.json +1 -1
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
|
|
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,6 +1141,109 @@ 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";
|
|
@@ -1147,7 +1290,7 @@ async function releasePidLock() {
|
|
|
1147
1290
|
}
|
|
1148
1291
|
}
|
|
1149
1292
|
function delay(ms) {
|
|
1150
|
-
return new Promise((
|
|
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);
|