@dohmmit/rtk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @dohmmit/rtk
2
+
3
+ OpenCode plugin that integrates [rtk](https://github.com/rtk-ai/rtk) to transparently rewrite bash commands for 60-90% token savings.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @dohmmit/rtk
9
+ ```
10
+
11
+ Then add to your `opencode.json`:
12
+
13
+ ```json
14
+ {
15
+ "plugin": ["@dohmmit/rtk"]
16
+ }
17
+ ```
18
+
19
+ **Prerequisite:** [rtk](https://github.com/rtk-ai/rtk) must be installed (>= 0.23.0).
20
+
21
+ ## Configuration
22
+
23
+ Config file at `~/.config/opencode/rtk-wrapper-config.json` (auto-created on first load):
24
+
25
+ ```json
26
+ {
27
+ "version": 1,
28
+ "excludeCommands": ["rtk", "cd"],
29
+ "settings": {
30
+ "ultraCompact": false
31
+ }
32
+ }
33
+ ```
34
+
35
+ - **excludeCommands:** Commands that pass through unrewritten. Default: `["rtk", "cd"]`
36
+ - **settings.ultraCompact:** Append `--ultra-compact` to all rewritten commands
37
+
38
+ ## Example
39
+
40
+ ```bash
41
+ # Without rtk plugin: raw git output (50+ lines)
42
+ git status
43
+
44
+ # With rtk plugin: automatically becomes
45
+ rtk git status
46
+ # Output: "3 modified, 1 untracked" (one line)
47
+ ```
48
+
49
+ ## License
50
+
51
+ MIT
@@ -0,0 +1,29 @@
1
+ /** Config shape contract per D-01. Changing this is a file-format migration. */
2
+ export interface RtkConfig {
3
+ version: number;
4
+ excludeCommands: string[];
5
+ settings: Record<string, unknown>;
6
+ }
7
+ /** D-02: Sensible defaults — zero-config for 90% of users. */
8
+ export declare const DEFAULTS: RtkConfig;
9
+ /** Cross-platform config path via Node.js built-ins (PLAT-02). */
10
+ export declare function getConfigPath(): string;
11
+ /**
12
+ * Validate and sanitize parsed JSON into an RtkConfig.
13
+ * Forward-compat: ignores unknown top-level keys.
14
+ * open question #1 resolution: trims excludeCommands entries.
15
+ */
16
+ export declare function validateConfig(raw: unknown, _configPath: string): RtkConfig;
17
+ /**
18
+ * Load the config file or create it with sensible defaults.
19
+ *
20
+ * CONF-04: Resolves ~/.config/opencode/rtk-wrapper-config.json via
21
+ * os.homedir() + path.join(). Auto-creates parent directories and file
22
+ * on first access (QUAL-02 — zero-config works out of the box).
23
+ *
24
+ * D-03: On ANY error (malformed JSON, permissions, etc.), log a single
25
+ * warning, overwrite with fresh defaults, and continue. No partial-salvage.
26
+ *
27
+ * PLAT-02: Uses only Node.js built-ins — no bash, jq, or shell calls.
28
+ */
29
+ export declare function loadOrCreateConfig(): RtkConfig;
package/dist/config.js ADDED
@@ -0,0 +1,85 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, dirname } from "node:path";
4
+ /** D-02: Sensible defaults — zero-config for 90% of users. */
5
+ export const DEFAULTS = {
6
+ version: 1,
7
+ excludeCommands: ["rtk", "cd"],
8
+ settings: {},
9
+ };
10
+ /** Cross-platform config path via Node.js built-ins (PLAT-02). */
11
+ export function getConfigPath() {
12
+ return join(homedir(), ".config", "opencode", "rtk-wrapper-config.json");
13
+ }
14
+ /**
15
+ * Validate and sanitize parsed JSON into an RtkConfig.
16
+ * Forward-compat: ignores unknown top-level keys.
17
+ * open question #1 resolution: trims excludeCommands entries.
18
+ */
19
+ export function validateConfig(raw, _configPath) {
20
+ if (!raw || typeof raw !== "object") {
21
+ throw new SyntaxError("Config must be a JSON object");
22
+ }
23
+ const obj = raw;
24
+ const version = typeof obj.version === "number" ? obj.version : 1;
25
+ const excludeCommands = [];
26
+ if (Array.isArray(obj.excludeCommands)) {
27
+ for (const item of obj.excludeCommands) {
28
+ if (typeof item === "string") {
29
+ const trimmed = item.trim();
30
+ if (trimmed.length > 0) {
31
+ excludeCommands.push(trimmed);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ const settings = obj.settings && typeof obj.settings === "object"
37
+ ? { ...obj.settings }
38
+ : {};
39
+ return { version, excludeCommands, settings };
40
+ }
41
+ /**
42
+ * Load the config file or create it with sensible defaults.
43
+ *
44
+ * CONF-04: Resolves ~/.config/opencode/rtk-wrapper-config.json via
45
+ * os.homedir() + path.join(). Auto-creates parent directories and file
46
+ * on first access (QUAL-02 — zero-config works out of the box).
47
+ *
48
+ * D-03: On ANY error (malformed JSON, permissions, etc.), log a single
49
+ * warning, overwrite with fresh defaults, and continue. No partial-salvage.
50
+ *
51
+ * PLAT-02: Uses only Node.js built-ins — no bash, jq, or shell calls.
52
+ */
53
+ export function loadOrCreateConfig() {
54
+ const configPath = getConfigPath();
55
+ if (!existsSync(configPath)) {
56
+ mkdirSync(dirname(configPath), { recursive: true });
57
+ writeFileSync(configPath, JSON.stringify(DEFAULTS, null, 2), "utf-8");
58
+ return { ...DEFAULTS };
59
+ }
60
+ try {
61
+ const raw = readFileSync(configPath, "utf-8");
62
+ const parsed = JSON.parse(raw);
63
+ return validateConfig(parsed, configPath);
64
+ }
65
+ catch (err) {
66
+ // Distinguish parse failures from I/O failures in the log message
67
+ // (RESEARCH.md Pitfall 1: catch both SyntaxError and EACCES/EISDIR).
68
+ if (err instanceof SyntaxError) {
69
+ console.warn(`[rtk] Malformed config at ${configPath}: ${err.message}. ` +
70
+ "Recreating with defaults.");
71
+ }
72
+ else {
73
+ console.warn(`[rtk] Cannot read config at ${configPath}: ` +
74
+ `${err.message}. Using defaults.`);
75
+ }
76
+ try {
77
+ mkdirSync(dirname(configPath), { recursive: true });
78
+ writeFileSync(configPath, JSON.stringify(DEFAULTS, null, 2), "utf-8");
79
+ }
80
+ catch {
81
+ // Recovery failed — still return defaults (e.g., config path is a directory)
82
+ }
83
+ return { ...DEFAULTS };
84
+ }
85
+ }
@@ -0,0 +1,2 @@
1
+ import { type Plugin } from "@opencode-ai/plugin";
2
+ export declare const RtkPlugin: Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { loadOrCreateConfig } from "./config.js";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { StatsTracker, formatSessionStats, formatCompactionSummary } from "./stats.js";
6
+ export const RtkPlugin = async ({ $, client }) => {
7
+ // Load config BEFORE init-time rtk check (D-04).
8
+ // Auto-creates at ~/.config/opencode/rtk-wrapper-config.json on first load.
9
+ const config = loadOrCreateConfig();
10
+ const excludeSet = new Set(config.excludeCommands);
11
+ // Init-time rtk availability and version check.
12
+ // Uses `rtk rewrite --help` rather than `rtk --version` — this verifies
13
+ // both that rtk is installed AND that it supports the `rewrite` subcommand
14
+ // (requires rtk >= 0.23.0). If rtk is absent, too old, or the subprocess
15
+ // fails, return {} (empty hooks) — the plugin is a no-op and other plugins
16
+ // continue to load.
17
+ const rtkCheck = await $ `rtk rewrite --help`.quiet().nothrow();
18
+ if (rtkCheck.exitCode !== 0) {
19
+ const configPath = join(homedir(), ".config", "opencode", "rtk-wrapper-config.json");
20
+ console.warn("[rtk] rtk binary not found or too old (requires rtk >= 0.23.0 for rewrite support). " +
21
+ "Commands will pass through unchanged. " +
22
+ `Config: ${configPath} — add excluded commands here. ` +
23
+ "Install: cargo install rtk or visit https://github.com/rtk-ai/rtk");
24
+ return {};
25
+ }
26
+ // Per-command result cache — prevents redundant subprocess spawns for
27
+ // repeated commands. QUAL-01: <1ms on cache hit.
28
+ // Scoped inside the plugin function so the cache is only allocated when
29
+ // rtk is confirmed available.
30
+ const cache = new Map();
31
+ // D-01: Stats tracker — closure-scoped, only allocated when rtk is
32
+ // confirmed available. Tracks per-command rewrite counts and cumulative
33
+ // token savings.
34
+ const stats = new StatsTracker();
35
+ // D-03: Gain pre-snapshot map — stores rtk gain total_saved before
36
+ // command execution, keyed by input.callID. Cleaned up after every
37
+ // after-hook lookup to prevent unbounded growth.
38
+ const gainPreSnapshots = new Map();
39
+ // D-05: Gain failure counter — incremented when rtk gain subprocess
40
+ // fails (non-zero exit or exception). Tracked for observability but
41
+ // never blocks execution.
42
+ const gainFailures = { count: 0 };
43
+ // D-04: Debug-level log helper. Uses client.app.log() when client is
44
+ // available; falls back to console.debug() otherwise.
45
+ const logDebug = (msg) => {
46
+ if (client) {
47
+ client.app.log({
48
+ body: { service: "rtk", level: "debug", message: msg },
49
+ });
50
+ }
51
+ else {
52
+ console.debug(msg);
53
+ }
54
+ };
55
+ return {
56
+ "tool.execute.before": async (input, output) => {
57
+ // Guard 1 — tool type. Only intercept bash/shell tool calls.
58
+ // All other tool types (read, write, grep, glob, edit, etc.)
59
+ // pass through unchanged.
60
+ const tool = String(input?.tool ?? "").toLowerCase();
61
+ if (tool !== "bash" && tool !== "shell") {
62
+ // FEED-01: Debug-level passthrough log for non-bash tools
63
+ logDebug(`[rtk] passthrough: '${tool}' (non-bash tool)`);
64
+ return;
65
+ }
66
+ // Guard 2 — args existence. Defensive: output.args should always
67
+ // exist for bash tool calls, but guard against undefined/null/non-object.
68
+ const args = output?.args;
69
+ if (!args || typeof args !== "object")
70
+ return;
71
+ // Guard 3 — command presence. Extract the command string.
72
+ // Empty/absent commands pass through — nothing to rewrite.
73
+ const command = args.command;
74
+ if (typeof command !== "string" || !command) {
75
+ // FEED-01: Debug-level passthrough log for empty commands
76
+ logDebug("[rtk] passthrough: empty command");
77
+ return;
78
+ }
79
+ // Guard 4 — config exclusion list. Commands matching the user's
80
+ // excludeCommands skip rewriting entirely (no cache lookup, no
81
+ // subprocess spawn). D-02: defaults are ["rtk", "cd"]. rtk prevents
82
+ // double-prefixing at config level (cheaper than reaching subprocess).
83
+ // cd is a pure shell builtin that rtk has no filter for.
84
+ if (excludeSet.has(command)) {
85
+ // FEED-01: Debug-level passthrough log for excluded commands
86
+ logDebug(`[rtk] passthrough: '${command}' (excluded)`);
87
+ return;
88
+ }
89
+ // Cache hit — use cached result, skip subprocess spawn.
90
+ // Only successful rewrites are cached (cache.set is after the
91
+ // mutation guard below), so a cache hit means a known rewrite.
92
+ const cached = cache.get(command);
93
+ if (cached !== undefined) {
94
+ ;
95
+ args.command = cached;
96
+ return;
97
+ }
98
+ // Call rtk rewrite as a subprocess. Use Bun's $ shell API
99
+ // with .quiet() (suppress stdout passthrough) and .nothrow()
100
+ // (never throw on non-zero exit). Wrap in try/catch for any
101
+ // unexpected failures — pass command through unchanged.
102
+ // The catch block is deliberately silent — logging in the hot
103
+ // path is noise. The init-time detection above already warns
104
+ // once at startup.
105
+ try {
106
+ const result = await $ `rtk rewrite ${command}`.quiet().nothrow();
107
+ const rewritten = String(result.stdout).trim();
108
+ // Exit code non-zero OR empty output means no rewrite
109
+ // available — pass command through unchanged.
110
+ // Empty output guard: never set args.command to an empty
111
+ // string (PITFALLS.md security section: undefined behavior).
112
+ if (result.exitCode !== 0 || !rewritten || rewritten.length === 0)
113
+ return;
114
+ // PITFALLS.md Pitfall 4: mutate args.command in-place only.
115
+ // Never replace the entire args object — that would drop
116
+ // timeout, workdir, and description fields.
117
+ // PITFALLS.md Pitfall 2: compare before mutating — the
118
+ // rewritten !== command guard prevents double-wrapping.
119
+ if (rewritten !== command) {
120
+ // D-08: Ultra-compact mode (CONF-03).
121
+ // Append BEFORE cache.set() and BEFORE args.command mutation
122
+ // (Pitfall 5: cache MUST store the full rewritten command
123
+ // including the ultra-compact modifier).
124
+ const ultraCompact = Boolean(config.settings.ultraCompact);
125
+ const finalCommand = ultraCompact
126
+ ? rewritten + " --ultra-compact"
127
+ : rewritten;
128
+ args.command = finalCommand;
129
+ // Cache ONLY on successful rewrite. If rtk rewrite returned
130
+ // empty or exit code 1, do not cache (future calls may
131
+ // succeed if rtk is updated or the command context changes).
132
+ cache.set(command, finalCommand);
133
+ // FEED-02 / D-03: Capture pre-execution gain snapshot.
134
+ // Uses rtk gain --format json (NOT --bytes — RESEARCH.md Pitfall 1).
135
+ // Stored keyed by input.callID for delta computation in after hook.
136
+ try {
137
+ const preResult = await $ `rtk gain --format json`.quiet().nothrow();
138
+ if (preResult.exitCode === 0) {
139
+ const pre = JSON.parse(String(preResult.stdout)).summary.total_saved;
140
+ gainPreSnapshots.set(input.callID, pre);
141
+ }
142
+ }
143
+ catch {
144
+ // Gain subprocess failure → no pre-snapshot stored.
145
+ // Delta will be 0 for this call. Never block tool execution.
146
+ }
147
+ // FEED-01: Structured logging at info level.
148
+ // D-04: null-guarded — if client is absent, silently skip.
149
+ // info-level has no console fallback.
150
+ client?.app.log({
151
+ body: {
152
+ service: "rtk",
153
+ level: "info",
154
+ message: `[rtk] rewrote '${command}' → '${finalCommand}'`,
155
+ },
156
+ });
157
+ }
158
+ }
159
+ catch {
160
+ // rtk rewrite subprocess failed (crash, timeout, missing
161
+ // binary). Pass command through unchanged — never block
162
+ // tool execution because of a failed rewrite. No logging
163
+ // in hot path — the init-time check already warned at startup.
164
+ }
165
+ },
166
+ // D-03: tool.execute.after — full pre/post gain delta pipeline.
167
+ // Replaces the tracer's simple stats.record(command, 0) with real
168
+ // gain measurements from rtk gain --format json snapshots.
169
+ // FEED-02: Accumulates accurate token savings per command.
170
+ "tool.execute.after": async (input, _output) => {
171
+ const tool = String(input?.tool ?? "").toLowerCase();
172
+ if (tool !== "bash" && tool !== "shell")
173
+ return;
174
+ const args = input?.args;
175
+ if (!args || typeof args !== "object")
176
+ return;
177
+ const command = args.command;
178
+ if (typeof command !== "string" || !command)
179
+ return;
180
+ // Look up pre-snapshot and clean up regardless of outcome
181
+ const preTotal = gainPreSnapshots.get(input.callID);
182
+ gainPreSnapshots.delete(input.callID);
183
+ let delta = 0;
184
+ try {
185
+ const postResult = await $ `rtk gain --format json`.quiet().nothrow();
186
+ if (postResult.exitCode === 0) {
187
+ const post = JSON.parse(String(postResult.stdout)).summary.total_saved;
188
+ if (preTotal !== undefined) {
189
+ // D-03 Pitfall 3: clamp negative deltas to 0
190
+ // (pre/post mismatch where post < pre — e.g., gain reset)
191
+ delta = Math.max(0, post - preTotal);
192
+ }
193
+ }
194
+ else {
195
+ gainFailures.count++;
196
+ }
197
+ }
198
+ catch {
199
+ gainFailures.count++;
200
+ }
201
+ // Extract the base command (first word) for aggregation.
202
+ // "rtk git status" → "rtk", "git status" → "git"
203
+ const baseCommand = command.split(" ")[0];
204
+ if (preTotal !== undefined) {
205
+ // Rewrite was attempted — record with real delta
206
+ stats.record(baseCommand, delta);
207
+ }
208
+ },
209
+ // FEED-03: rtk_gain custom LLM tool (D-06, D-07).
210
+ // Stateless — reads from closure-scoped stats, no side effects.
211
+ tool: {
212
+ rtk_gain: tool({
213
+ description: "Show cumulative rtk token savings for the current session. " +
214
+ "Returns a formatted table of per-command rewrite counts and tokens saved.",
215
+ args: {},
216
+ async execute(_args, _ctx) {
217
+ return formatSessionStats(stats);
218
+ },
219
+ }),
220
+ },
221
+ // FEED-04: Session compaction savings summary (D-09).
222
+ // If the hook is never called at runtime (API may not fire in all
223
+ // OpenCode versions), rtk_gain remains the primary interface.
224
+ "experimental.session.compacting": async (_input, output) => {
225
+ const summary = formatCompactionSummary(stats);
226
+ if (summary) {
227
+ output.context.push(summary);
228
+ }
229
+ },
230
+ };
231
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Closure-scoped stats tracking for the rtk plugin.
3
+ *
4
+ * D-01: Allocated inside the plugin function closure, only when rtk is
5
+ * confirmed available at init. Never exposed to module-level globals.
6
+ *
7
+ * D-02: Track two metrics per command — `count` (times rewritten) and
8
+ * `tokensSaved` (cumulative rtk gain delta). Both are positive integers.
9
+ */
10
+ export declare class StatsTracker {
11
+ private data;
12
+ /**
13
+ * Record a command rewrite.
14
+ * Multiple calls for the same command aggregate count and tokensSaved.
15
+ */
16
+ record(command: string, tokensSaved: number): void;
17
+ /** Yield all tracked entries for read-only iteration. */
18
+ entries(): IterableIterator<[string, {
19
+ count: number;
20
+ tokensSaved: number;
21
+ }]>;
22
+ }
23
+ /**
24
+ * D-07: Format session stats as a Markdown table.
25
+ *
26
+ * Columns: Command, Rewrites, Tokens Saved.
27
+ * Rows sorted by tokensSaved descending. Total row at bottom.
28
+ *
29
+ * Returns "No commands rewritten this session." if stats is empty.
30
+ */
31
+ export declare function formatSessionStats(stats: StatsTracker): string;
32
+ /**
33
+ * One-line session compaction summary.
34
+ *
35
+ * Format: "[rtk] This session: N commands rewritten, N tokens saved (~N bytes)"
36
+ * Returns "" if no rewrites recorded.
37
+ */
38
+ export declare function formatCompactionSummary(stats: StatsTracker): string;
package/dist/stats.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Closure-scoped stats tracking for the rtk plugin.
3
+ *
4
+ * D-01: Allocated inside the plugin function closure, only when rtk is
5
+ * confirmed available at init. Never exposed to module-level globals.
6
+ *
7
+ * D-02: Track two metrics per command — `count` (times rewritten) and
8
+ * `tokensSaved` (cumulative rtk gain delta). Both are positive integers.
9
+ */
10
+ export class StatsTracker {
11
+ data = new Map();
12
+ /**
13
+ * Record a command rewrite.
14
+ * Multiple calls for the same command aggregate count and tokensSaved.
15
+ */
16
+ record(command, tokensSaved) {
17
+ const existing = this.data.get(command);
18
+ if (existing) {
19
+ existing.count++;
20
+ existing.tokensSaved += tokensSaved;
21
+ }
22
+ else {
23
+ this.data.set(command, { count: 1, tokensSaved });
24
+ }
25
+ }
26
+ /** Yield all tracked entries for read-only iteration. */
27
+ entries() {
28
+ return this.data.entries();
29
+ }
30
+ }
31
+ /**
32
+ * D-07: Format session stats as a Markdown table.
33
+ *
34
+ * Columns: Command, Rewrites, Tokens Saved.
35
+ * Rows sorted by tokensSaved descending. Total row at bottom.
36
+ *
37
+ * Returns "No commands rewritten this session." if stats is empty.
38
+ */
39
+ export function formatSessionStats(stats) {
40
+ const entries = Array.from(stats.entries())
41
+ .filter(([_, v]) => v.count > 0)
42
+ .sort((a, b) => b[1].tokensSaved - a[1].tokensSaved);
43
+ if (entries.length === 0)
44
+ return "No commands rewritten this session.";
45
+ let totalRewrites = 0;
46
+ let totalTokens = 0;
47
+ const rows = entries.map(([cmd, { count, tokensSaved }]) => {
48
+ totalRewrites += count;
49
+ totalTokens += tokensSaved;
50
+ return `| ${cmd} | ${count.toLocaleString()} | ${tokensSaved.toLocaleString()} |`;
51
+ });
52
+ return [
53
+ "| Command | Rewrites | Tokens Saved |",
54
+ "|---------|---------|-------------|",
55
+ ...rows,
56
+ `| **Total** | **${totalRewrites.toLocaleString()}** | **${totalTokens.toLocaleString()}** |`,
57
+ ].join("\n");
58
+ }
59
+ /**
60
+ * One-line session compaction summary.
61
+ *
62
+ * Format: "[rtk] This session: N commands rewritten, N tokens saved (~N bytes)"
63
+ * Returns "" if no rewrites recorded.
64
+ */
65
+ export function formatCompactionSummary(stats) {
66
+ let totalRewrites = 0;
67
+ let totalTokens = 0;
68
+ for (const [, { count, tokensSaved }] of stats.entries()) {
69
+ totalRewrites += count;
70
+ totalTokens += tokensSaved;
71
+ }
72
+ if (totalRewrites === 0)
73
+ return "";
74
+ return (`[rtk] This session: ${totalRewrites.toLocaleString()} commands rewritten, ` +
75
+ `${totalTokens.toLocaleString()} tokens saved (~${Math.round(totalTokens / 4).toLocaleString()} bytes)`);
76
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@dohmmit/rtk",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin that integrates rtk to transparently rewrite bash commands for token savings",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/user/opencode-rtk.git"
21
+ },
22
+ "keywords": ["opencode", "rtk", "plugin", "tokens"],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "node --experimental-strip-types --test tests/*.test.ts",
27
+ "prepublishOnly": "npm test"
28
+ },
29
+ "devDependencies": {
30
+ "@opencode-ai/plugin": "^1.18.4",
31
+ "@types/node": "^26.1.1",
32
+ "typescript": "^5.9.3"
33
+ }
34
+ }