@jimmyyen/opencode-context-cache 1.0.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 +21 -0
- package/README.md +68 -0
- package/index.mjs +337 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JackDrogon
|
|
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,68 @@
|
|
|
1
|
+
# @jimmyyen/opencode-context-cache
|
|
2
|
+
|
|
3
|
+
OpenCode plugin for stable, privacy-preserving prompt cache key and sticky session management.
|
|
4
|
+
|
|
5
|
+
Forked from [JackDrogon/opencode-context-cache](https://github.com/JackDrogon/opencode-context-cache).
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
OpenCode sessions can lose cache efficiency when session identifiers vary between providers, environments, or runs. This plugin standardizes cache key generation with predictable precedence and sends only a SHA256 digest upstream, so the AI provider sees a stable cache identity.
|
|
10
|
+
|
|
11
|
+
Observed result from a real run: input cache hit rate improved from a near-zero baseline to **97.99%** (`164736 / 168112`).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @jimmyyen/opencode-context-cache
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Add to your `opencode.jsonc` plugin array. The package is resolved via Node module resolution, so you reference it by name:
|
|
22
|
+
|
|
23
|
+
```jsonc
|
|
24
|
+
{
|
|
25
|
+
"$schema": "https://opencode.ai/config.json",
|
|
26
|
+
"plugin": [
|
|
27
|
+
"@jimmyyen/opencode-context-cache"
|
|
28
|
+
],
|
|
29
|
+
"provider": {
|
|
30
|
+
"openai": {
|
|
31
|
+
"options": {
|
|
32
|
+
"setCacheKey": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The plugin auto-registers on the `chat.params` hook at import time. No additional setup needed after listing it in `plugin`.
|
|
40
|
+
|
|
41
|
+
## Cache key precedence
|
|
42
|
+
|
|
43
|
+
1. `OPENCODE_PROMPT_CACHE_KEY` env var
|
|
44
|
+
2. `OPENCODE_STICKY_SESSION_ID` env var
|
|
45
|
+
3. Auto-generated `user@host:<absolute_cwd>`
|
|
46
|
+
4. Existing model headers (`x-session-id`, `conversation_id`, `session_id`)
|
|
47
|
+
5. OpenCode `sessionID`
|
|
48
|
+
|
|
49
|
+
The key is SHA256-hashed before being sent to the server. Already-hashed values (64-char hex) are detected and passed through without re-hashing.
|
|
50
|
+
|
|
51
|
+
## Debug logging
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export OPENCODE_CONTEXT_CACHE_DEBUG=1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Logs are written to `context-cache.log` next to the plugin file with timestamps, PID, and key resolution details.
|
|
58
|
+
|
|
59
|
+
## Exports
|
|
60
|
+
|
|
61
|
+
| Export | Type | Description |
|
|
62
|
+
|--------|------|-------------|
|
|
63
|
+
| `OpenCodeContextCachePlugin` (default) | async factory | Main plugin factory |
|
|
64
|
+
| `EnhancedCachePlugin` | alias | Backward-compatible alias |
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT. See `LICENSE`.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode plugin: OpenCode Context Cache
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Per-project cache isolation using absolute path with user@host prefix
|
|
6
|
+
* - Support for ALL providers (not just specific ones)
|
|
7
|
+
* - Debug logging to file (same directory as plugin)
|
|
8
|
+
* - Smart cache key generation with multiple fallbacks
|
|
9
|
+
* - Unified session header and cache key management
|
|
10
|
+
* - SHA256 hashed cache key for privacy (server sees only hash)
|
|
11
|
+
*
|
|
12
|
+
* Cache Key Format (raw): {user}@{host}:{directory}
|
|
13
|
+
* Cache Key Format (sent to server): SHA256(raw)
|
|
14
|
+
* Example: c@my-laptop:revm -> sha256:abc123...
|
|
15
|
+
*
|
|
16
|
+
* Cache Key Precedence:
|
|
17
|
+
* 1. OPENCODE_PROMPT_CACHE_KEY env var (manual override)
|
|
18
|
+
* 2. OPENCODE_STICKY_SESSION_ID env var (manual override)
|
|
19
|
+
* 3. User@Host:Directory (auto-generated)
|
|
20
|
+
* 4. Model headers (x-session-id / conversation_id / session_id)
|
|
21
|
+
* 5. opencode sessionID (fallback)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { hostname, userInfo } from "os";
|
|
25
|
+
import { dirname, join } from "path";
|
|
26
|
+
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
import { createHash } from "crypto";
|
|
29
|
+
|
|
30
|
+
const SESSION_ID_HEADER_NAMES = ["x-session-id", "conversation_id", "session_id"];
|
|
31
|
+
const PROMPT_CACHE_KEY_ENV_VAR = "OPENCODE_PROMPT_CACHE_KEY";
|
|
32
|
+
const STICKY_SESSION_ID_ENV_VAR = "OPENCODE_STICKY_SESSION_ID";
|
|
33
|
+
const CACHE_DEBUG_ENV_VAR = "OPENCODE_CONTEXT_CACHE_DEBUG";
|
|
34
|
+
|
|
35
|
+
// Get plugin directory (where this file is located)
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = dirname(__filename);
|
|
38
|
+
const LOG_FILE_PATH = join(__dirname, "context-cache.log");
|
|
39
|
+
|
|
40
|
+
class DebugLogger {
|
|
41
|
+
constructor(logFilePath) {
|
|
42
|
+
this.logFilePath = logFilePath;
|
|
43
|
+
this.debugEnabled = null;
|
|
44
|
+
this.loggedInputStructure = false;
|
|
45
|
+
this.ensureLogDirectory();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ensureLogDirectory() {
|
|
49
|
+
try {
|
|
50
|
+
const logDir = dirname(this.logFilePath);
|
|
51
|
+
if (!existsSync(logDir)) {
|
|
52
|
+
mkdirSync(logDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore errors, fallback will use console.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
isEnabled() {
|
|
60
|
+
if (this.debugEnabled === null) {
|
|
61
|
+
this.debugEnabled =
|
|
62
|
+
process?.env?.[CACHE_DEBUG_ENV_VAR] === "1" ||
|
|
63
|
+
process?.env?.[CACHE_DEBUG_ENV_VAR] === "true";
|
|
64
|
+
}
|
|
65
|
+
return this.debugEnabled;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
toLogString(value) {
|
|
69
|
+
if (typeof value !== "object" || value === null) {
|
|
70
|
+
return String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
} catch {
|
|
76
|
+
return String(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log(...args) {
|
|
81
|
+
if (!this.isEnabled()) return;
|
|
82
|
+
|
|
83
|
+
const timestamp = new Date().toISOString();
|
|
84
|
+
const pid = process.pid;
|
|
85
|
+
const message = args.map((arg) => this.toLogString(arg)).join(" ");
|
|
86
|
+
|
|
87
|
+
// Keep each log entry on a single physical line.
|
|
88
|
+
const safeMessage = message.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
89
|
+
const logLine = `[${timestamp}] [pid:${pid}] [context-cache] ${safeMessage}\n`;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// O_APPEND keeps each append atomic on POSIX filesystems.
|
|
93
|
+
appendFileSync(this.logFilePath, logLine, "utf8");
|
|
94
|
+
} catch {
|
|
95
|
+
// Fallback to stderr when file append fails.
|
|
96
|
+
console.error(`[pid:${pid}] [context-cache]`, ...args);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logInputStructureOnce(input) {
|
|
101
|
+
if (this.loggedInputStructure) return;
|
|
102
|
+
this.loggedInputStructure = true;
|
|
103
|
+
|
|
104
|
+
const safeInput = {
|
|
105
|
+
hasProvider: !!input?.provider,
|
|
106
|
+
providerKeys: input?.provider ? Object.keys(input.provider) : [],
|
|
107
|
+
hasModel: !!input?.model,
|
|
108
|
+
modelKeys: input?.model ? Object.keys(input.model) : [],
|
|
109
|
+
hasSessionID: !!input?.sessionID,
|
|
110
|
+
};
|
|
111
|
+
this.log("Input structure:", safeInput);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class CacheKeyResolver {
|
|
116
|
+
constructor(logger) {
|
|
117
|
+
this.logger = logger;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
sha256(input) {
|
|
121
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isSha256Hex(value) {
|
|
125
|
+
if (typeof value !== "string") return false;
|
|
126
|
+
const v = value.trim();
|
|
127
|
+
if (v.length !== 64) return false;
|
|
128
|
+
return /^[a-fA-F0-9]{64}$/.test(v);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getTrimmedEnv(name) {
|
|
132
|
+
const value = process?.env?.[name];
|
|
133
|
+
return typeof value === "string" ? value.trim() : "";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getUsername() {
|
|
137
|
+
try {
|
|
138
|
+
const ui = userInfo();
|
|
139
|
+
if (ui && ui.username) {
|
|
140
|
+
return ui.username;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// userInfo may fail in restricted environments.
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
process?.env?.USER ||
|
|
148
|
+
process?.env?.USERNAME ||
|
|
149
|
+
process?.env?.LOGNAME ||
|
|
150
|
+
"unknown"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getUserHostDirectoryKey() {
|
|
155
|
+
try {
|
|
156
|
+
const user = this.getUsername();
|
|
157
|
+
const host = hostname();
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
return `${user}@${host}:${cwd}`;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getSessionIdFromHeaders(input) {
|
|
166
|
+
const headers =
|
|
167
|
+
input?.model?.headers && typeof input.model.headers === "object"
|
|
168
|
+
? input.model.headers
|
|
169
|
+
: {};
|
|
170
|
+
|
|
171
|
+
const value = SESSION_ID_HEADER_NAMES.map((key) => headers[key])
|
|
172
|
+
.find((v) => typeof v === "string" && v.trim())
|
|
173
|
+
?.trim?.();
|
|
174
|
+
|
|
175
|
+
return value || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
resolveCacheKey(input) {
|
|
179
|
+
let rawKey = null;
|
|
180
|
+
let source = null;
|
|
181
|
+
let alreadyHashed = false;
|
|
182
|
+
|
|
183
|
+
// 1) Explicit env override.
|
|
184
|
+
const promptCacheKey = this.getTrimmedEnv(PROMPT_CACHE_KEY_ENV_VAR);
|
|
185
|
+
if (promptCacheKey) {
|
|
186
|
+
rawKey = promptCacheKey;
|
|
187
|
+
source = PROMPT_CACHE_KEY_ENV_VAR;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2) Secondary env override.
|
|
191
|
+
if (!rawKey) {
|
|
192
|
+
const stickySessionKey = this.getTrimmedEnv(STICKY_SESSION_ID_ENV_VAR);
|
|
193
|
+
if (stickySessionKey) {
|
|
194
|
+
rawKey = stickySessionKey;
|
|
195
|
+
source = STICKY_SESSION_ID_ENV_VAR;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 3) Preferred stable default.
|
|
200
|
+
if (!rawKey) {
|
|
201
|
+
const userHostDirKey = this.getUserHostDirectoryKey();
|
|
202
|
+
if (userHostDirKey) {
|
|
203
|
+
rawKey = userHostDirKey;
|
|
204
|
+
source = "user@host:directory";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 4) Existing model headers only when no stable default exists.
|
|
209
|
+
if (!rawKey) {
|
|
210
|
+
const headerValue = this.getSessionIdFromHeaders(input);
|
|
211
|
+
if (headerValue) {
|
|
212
|
+
rawKey = headerValue;
|
|
213
|
+
source = "model headers";
|
|
214
|
+
alreadyHashed = this.isSha256Hex(rawKey);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 5) OpenCode session fallback.
|
|
219
|
+
if (!rawKey) {
|
|
220
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : "";
|
|
221
|
+
if (sessionID) {
|
|
222
|
+
rawKey = sessionID;
|
|
223
|
+
source = "opencode sessionID";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!rawKey) {
|
|
228
|
+
this.logger.log("No stable cache key found");
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const hashedKey = alreadyHashed ? rawKey : this.sha256(rawKey);
|
|
233
|
+
|
|
234
|
+
if (alreadyHashed) {
|
|
235
|
+
this.logger.log("Cache key already looks hashed; skipping sha256");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.logger.log(`Using cache key from ${source}`);
|
|
239
|
+
this.logger.log(` Raw: ${rawKey}`);
|
|
240
|
+
this.logger.log(` Hash: ${hashedKey}`);
|
|
241
|
+
|
|
242
|
+
return { raw: rawKey, hashed: hashedKey };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class CacheKeyApplier {
|
|
247
|
+
constructor(logger) {
|
|
248
|
+
this.logger = logger;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
applyPromptCacheKey(output, cacheKey) {
|
|
252
|
+
const existingOutputOptions =
|
|
253
|
+
output?.options && typeof output.options === "object" ? output.options : {};
|
|
254
|
+
|
|
255
|
+
output.options = {
|
|
256
|
+
...existingOutputOptions,
|
|
257
|
+
promptCacheKey: cacheKey,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
applySessionHeaders(input, cacheKey) {
|
|
262
|
+
if (input?.model && typeof input.model === "object") {
|
|
263
|
+
const headers =
|
|
264
|
+
input.model.headers && typeof input.model.headers === "object"
|
|
265
|
+
? input.model.headers
|
|
266
|
+
: (input.model.headers = {});
|
|
267
|
+
|
|
268
|
+
for (const headerKey of SESSION_ID_HEADER_NAMES) {
|
|
269
|
+
headers[headerKey] = cacheKey;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (this.logger.isEnabled()) {
|
|
273
|
+
headers["x-cache-debug"] = "1";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.logger.log("Set final cache key (hashed):", cacheKey);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.logger.log("Input model is missing or not an object, cannot set session headers");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
apply(input, output, cacheKey) {
|
|
284
|
+
this.applyPromptCacheKey(output, cacheKey);
|
|
285
|
+
this.applySessionHeaders(input, cacheKey);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
class ContextCachePluginRuntime {
|
|
290
|
+
constructor({ logger, keyResolver, keyApplier }) {
|
|
291
|
+
this.logger = logger;
|
|
292
|
+
this.keyResolver = keyResolver;
|
|
293
|
+
this.keyApplier = keyApplier;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
initialize() {
|
|
297
|
+
this.logger.log("Plugin initialized");
|
|
298
|
+
this.logger.log("Log file location:", this.logger.logFilePath);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
handleChatParams(input, output) {
|
|
302
|
+
this.logger.logInputStructureOnce(input);
|
|
303
|
+
this.logger.log("Processing provider");
|
|
304
|
+
|
|
305
|
+
const cacheKeyInfo = this.keyResolver.resolveCacheKey(input);
|
|
306
|
+
if (!cacheKeyInfo) {
|
|
307
|
+
this.logger.log("No cache key available");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.keyApplier.apply(input, output, cacheKeyInfo.hashed);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const logger = new DebugLogger(LOG_FILE_PATH);
|
|
316
|
+
const keyResolver = new CacheKeyResolver(logger);
|
|
317
|
+
const keyApplier = new CacheKeyApplier(logger);
|
|
318
|
+
const runtime = new ContextCachePluginRuntime({
|
|
319
|
+
logger,
|
|
320
|
+
keyResolver,
|
|
321
|
+
keyApplier,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
export const OpenCodeContextCachePlugin = async () => {
|
|
325
|
+
runtime.initialize();
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
"chat.params": async (input, output) => {
|
|
329
|
+
runtime.handleChatParams(input, output);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Backward-compatible export alias.
|
|
335
|
+
export const EnhancedCachePlugin = OpenCodeContextCachePlugin;
|
|
336
|
+
|
|
337
|
+
export default OpenCodeContextCachePlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jimmyyen/opencode-context-cache",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode plugin for stable, privacy-preserving prompt cache key and sticky session management. Fork of JackDrogon/opencode-context-cache.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./index.mjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.mjs",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --check index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"opencode",
|
|
22
|
+
"plugin",
|
|
23
|
+
"prompt-cache",
|
|
24
|
+
"context-cache",
|
|
25
|
+
"session",
|
|
26
|
+
"ai"
|
|
27
|
+
],
|
|
28
|
+
"author": "jimmyyen",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/cawa0505/opencode-context-cache.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/cawa0505/opencode-context-cache/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/cawa0505/opencode-context-cache#readme",
|
|
38
|
+
"contributors": [
|
|
39
|
+
{
|
|
40
|
+
"name": "Jack Drogon",
|
|
41
|
+
"email": "jack.xsuperman@gmail.com",
|
|
42
|
+
"url": "https://github.com/JackDrogon"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
}
|
|
51
|
+
}
|