opencode-rtk-plugin 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/index.js +68 -18
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -4,28 +4,74 @@
4
4
  // model in a token-efficient form. All rewrite logic lives in rtk itself; this
5
5
  // plugin is a thin delegating adapter for OpenCode's shell hook.
6
6
  //
7
- // Requires: rtk >= 0.23.0 on PATH (https://github.com/rtk-ai/rtk)
7
+ // Requires: rtk (https://github.com/rtk-ai/rtk)
8
8
 
9
9
  import { execFile } from "node:child_process";
10
10
 
11
- /** Run `rtk rewrite <command>`; resolve to the rewritten command or null. */
11
+ const DEFAULT_TIMEOUT_MS = 5000;
12
+ const MAX_TIMEOUT_MS = 600_000;
13
+
14
+ /**
15
+ * Run `rtk rewrite <command>` and resolve to the rewritten command, or null.
16
+ *
17
+ * rtk signals the result on STDOUT, not via exit status: since v0.34.0
18
+ * `rtk rewrite` exits 3 with the rewritten command on stdout (its
19
+ * permission-verdict protocol). So the exit code must be ignored and the
20
+ * decision made on stdout alone.
21
+ *
22
+ * Never rejects and never hangs: a spawn failure, timeout, or empty/unchanged
23
+ * output resolves to null so the original command runs.
24
+ */
12
25
  function rewriteCommand(binary, command, timeoutMs) {
13
26
  return new Promise((resolve) => {
14
- const child = execFile(
15
- binary,
16
- ["rewrite", command],
17
- { timeout: timeoutMs },
18
- (err, stdout) => {
19
- if (err) return resolve(null);
20
- const rewritten = String(stdout).trim();
21
- resolve(rewritten && rewritten !== command ? rewritten : null);
27
+ let settled = false;
28
+ let timer;
29
+ const done = (value) => {
30
+ if (settled) return;
31
+ settled = true;
32
+ if (timer) clearTimeout(timer);
33
+ resolve(value);
34
+ };
35
+
36
+ let child;
37
+ try {
38
+ // timeout: 0 disables execFile's own SIGTERM-only timeout; we enforce a
39
+ // hard SIGKILL deadline below so a SIGTERM-ignoring rtk cannot hang.
40
+ child = execFile(
41
+ binary,
42
+ ["rewrite", command],
43
+ { timeout: 0, encoding: "utf8" },
44
+ (_err, stdout) => {
45
+ const out = String(stdout ?? "").trim();
46
+ done(out && out !== command ? out : null);
47
+ }
48
+ );
49
+ } catch {
50
+ return done(null);
51
+ }
52
+
53
+ child.on("error", () => done(null));
54
+
55
+ timer = setTimeout(() => {
56
+ try {
57
+ child.kill("SIGKILL");
58
+ } catch {
59
+ /* already gone */
22
60
  }
23
- );
24
- // execFile can emit before the callback; never let it surface.
25
- child.on("error", () => resolve(null));
61
+ done(null);
62
+ }, timeoutMs + 250);
26
63
  });
27
64
  }
28
65
 
66
+ function resolveBinary(value) {
67
+ return typeof value === "string" && value.trim() !== "" ? value : "rtk";
68
+ }
69
+
70
+ function resolveTimeout(value) {
71
+ if (Number.isInteger(value) && value >= 1 && value <= MAX_TIMEOUT_MS) return value;
72
+ return DEFAULT_TIMEOUT_MS;
73
+ }
74
+
29
75
  export default {
30
76
  id: "rtk",
31
77
 
@@ -33,13 +79,17 @@ export default {
33
79
  const options = ctx.options ?? {};
34
80
  if (options.enabled === false) return;
35
81
 
36
- const binary = typeof options.binary === "string" ? options.binary : "rtk";
37
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 5000;
82
+ const binary = resolveBinary(options.binary);
83
+ const timeoutMs = resolveTimeout(options.timeoutMs);
38
84
 
39
85
  await ctx.shell.hook("create.before", async (event) => {
40
- if (!event || typeof event.command !== "string" || !event.command) return;
41
- const rewritten = await rewriteCommand(binary, event.command, timeoutMs);
42
- if (rewritten) event.command = rewritten;
86
+ try {
87
+ if (!event || typeof event.command !== "string" || !event.command) return;
88
+ const rewritten = await rewriteCommand(binary, event.command, timeoutMs);
89
+ if (rewritten) event.command = rewritten;
90
+ } catch {
91
+ // Never fail the shell call: any error leaves the command unchanged.
92
+ }
43
93
  });
44
94
  },
45
95
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rtk-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "OpenCode v2 plugin that rewrites shell commands through rtk to cut LLM token usage by 60-90%.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "scripts": {
44
44
  "lint": "markdownlint-cli2 \"**/*.md\"",
45
- "test": "node --check index.js && node -e \"import('./index.js').then(m => { if (!m.default?.id || typeof m.default.setup !== 'function') { console.error('invalid plugin export'); process.exit(1) } console.log('plugin id:', m.default.id) })\"",
45
+ "test": "node --check index.js && node --test test/*.test.mjs",
46
46
  "check": "npm run lint && npm test"
47
47
  },
48
48
  "devDependencies": {