opencode-cliproxyapi-sync 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/index.js +208 -0
  4. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 itsmylife44
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,166 @@
1
+ # opencode-cliproxyapi-sync
2
+
3
+ Auto-sync OpenCode configs from CLIProxyAPI Dashboard.
4
+
5
+ ## What it does
6
+
7
+ This plugin automatically syncs your OpenCode configuration (`opencode.json` and `oh-my-opencode.json`) from the CLIProxyAPI Dashboard. It checks for config updates on OpenCode startup and applies them automatically.
8
+
9
+ ## Features
10
+
11
+ - 🔄 **Automatic Sync**: Checks for config updates on OpenCode startup
12
+ - ⚡ **Fast**: Only syncs when configs actually change (version-based)
13
+ - 🔒 **Secure**: Uses sync tokens for authentication
14
+ - 📝 **Smart Notifications**: Only shows toast when `opencode.json` changes (requiring restart)
15
+ - 💾 **Atomic Writes**: Safe config file updates (no partial writes)
16
+
17
+ ## Installation
18
+
19
+ ### Option 1: NPX Install (Recommended)
20
+
21
+ ```bash
22
+ npx opencode install opencode-cliproxyapi-sync
23
+ ```
24
+
25
+ ### Option 2: Manual Configuration
26
+
27
+ Add to your `~/.config/opencode/opencode.json` plugins array:
28
+
29
+ ```json
30
+ {
31
+ "plugin": [
32
+ "opencode-cliproxyapi-sync",
33
+ "oh-my-opencode@latest",
34
+ "..."
35
+ ]
36
+ }
37
+ ```
38
+
39
+ **Important**: Place `opencode-cliproxyapi-sync` **before** `oh-my-opencode` in the plugins array to ensure config is synced before oh-my-opencode loads.
40
+
41
+ ## Setup
42
+
43
+ ### 1. Generate Sync Token
44
+
45
+ 1. Open the CLIProxyAPI Dashboard
46
+ 2. Navigate to **Settings** → **Config Sync**
47
+ 3. Click **Generate Sync Token**
48
+ 4. Copy the token (it starts with `tok_...`)
49
+
50
+ ### 2. Create Plugin Config File
51
+
52
+ Create the config file at `~/.config/opencode-cliproxyapi-sync/config.json`:
53
+
54
+ ```json
55
+ {
56
+ "dashboardUrl": "https://dashboard.yourdomain.com",
57
+ "syncToken": "tok_abc123...",
58
+ "lastKnownVersion": null
59
+ }
60
+ ```
61
+
62
+ **Config Fields**:
63
+ - `dashboardUrl`: Your CLIProxyAPI Dashboard URL (no trailing slash)
64
+ - `syncToken`: The sync token from Step 1
65
+ - `lastKnownVersion`: Leave as `null` (plugin manages this automatically)
66
+
67
+ **Security**: The plugin automatically sets restrictive permissions (chmod 600) on the config file.
68
+
69
+ ## How it works
70
+
71
+ 1. **On OpenCode Startup**: Plugin loads and checks your config
72
+ 2. **Version Check**: Fetches current config version from dashboard
73
+ 3. **Compare**: If version differs from `lastKnownVersion`, fetches full bundle
74
+ 4. **Write Configs**: Atomically writes `opencode.json` and `oh-my-opencode.json`
75
+ 5. **Notify**: Shows toast if `opencode.json` changed (requiring restart)
76
+ 6. **Update Version**: Saves new version to plugin config
77
+
78
+ ## Configuration Sync Behavior
79
+
80
+ - **Sync Timing**: Only on OpenCode startup (not while running)
81
+ - **Network Timeout**: 5s for version check, 10s for bundle download
82
+ - **Retries**: Up to 2 retries with exponential backoff (1s, 2s)
83
+ - **Error Handling**: All errors logged but never crash OpenCode
84
+
85
+ ## Notifications
86
+
87
+ The plugin shows a toast notification **only when** `opencode.json` changes:
88
+
89
+ > ⚠️ **Config Sync**
90
+ > opencode.json updated. Restart OpenCode to apply provider changes.
91
+
92
+ If only `oh-my-opencode.json` changes, no notification is shown (those changes apply immediately on next session).
93
+
94
+ ## File Locations
95
+
96
+ - **Plugin Config**: `~/.config/opencode-cliproxyapi-sync/config.json`
97
+ - **OpenCode Config**: `~/.config/opencode/opencode.json`
98
+ - **Oh My OpenCode Config**: `~/.config/opencode/oh-my-opencode.json`
99
+
100
+ (Respects `XDG_CONFIG_HOME` environment variable if set)
101
+
102
+ ## Troubleshooting
103
+
104
+ ### Plugin doesn't sync
105
+
106
+ **Check config file exists and is valid**:
107
+ ```bash
108
+ cat ~/.config/opencode-cliproxyapi-sync/config.json
109
+ ```
110
+
111
+ **Check OpenCode logs** for sync messages:
112
+ ```bash
113
+ # Look for lines like:
114
+ [cliproxyapi-sync] Synced to version abc123...
115
+ [cliproxyapi-sync] Failed to check version. Skipping sync.
116
+ ```
117
+
118
+ ### Authentication errors (401)
119
+
120
+ - Verify your `syncToken` is correct
121
+ - Check if token was revoked in Dashboard → Settings → Config Sync
122
+ - Generate a new token if needed
123
+
124
+ ### Config not updating
125
+
126
+ - Ensure dashboard URL is correct (no trailing slash)
127
+ - Check firewall/network allows access to dashboard
128
+ - Verify dashboard is running and accessible
129
+
130
+ ### Notification not showing
131
+
132
+ - Notification only appears when `opencode.json` changes
133
+ - If only `oh-my-opencode.json` changed, no notification is shown (expected)
134
+
135
+ ## Development
136
+
137
+ ### Build from source
138
+
139
+ ```bash
140
+ cd plugin
141
+ bun install
142
+ bun build src/index.ts --outdir dist --target node
143
+ ```
144
+
145
+ ### Local testing
146
+
147
+ 1. Build the plugin (see above)
148
+ 2. Link locally:
149
+ ```bash
150
+ cd plugin
151
+ npm link
152
+ cd ~/.config/opencode
153
+ npm link opencode-cliproxyapi-sync
154
+ ```
155
+ 3. Add `"opencode-cliproxyapi-sync"` to your `opencode.json` plugins array
156
+ 4. Restart OpenCode
157
+
158
+ ## License
159
+
160
+ MIT
161
+
162
+ ## Support
163
+
164
+ - **Dashboard Issues**: https://github.com/itsmylife44/opencode-cliproxyapi-sync/issues
165
+ - **CLIProxyAPI**: https://github.com/router-for-me/CLIProxyAPI
166
+ - **OpenCode**: https://opencode.ai
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/index.ts
5
+ import { join as join3 } from "path";
6
+
7
+ // src/config.ts
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+ function getConfigDir() {
12
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
13
+ const baseDir = xdgConfigHome || join(homedir(), ".config");
14
+ return join(baseDir, "opencode-cliproxyapi-sync");
15
+ }
16
+ function getConfigPath() {
17
+ return join(getConfigDir(), "config.json");
18
+ }
19
+ function loadPluginConfig() {
20
+ try {
21
+ const configPath = getConfigPath();
22
+ if (!existsSync(configPath)) {
23
+ return null;
24
+ }
25
+ const content = readFileSync(configPath, "utf-8");
26
+ const config = JSON.parse(content);
27
+ if (!config.dashboardUrl || !config.syncToken) {
28
+ console.error("[cliproxyapi-sync] Invalid config: missing dashboardUrl or syncToken");
29
+ return null;
30
+ }
31
+ return config;
32
+ } catch (error) {
33
+ console.error("[cliproxyapi-sync] Failed to load config:", error);
34
+ return null;
35
+ }
36
+ }
37
+ function savePluginConfig(config) {
38
+ try {
39
+ const configDir = getConfigDir();
40
+ const configPath = getConfigPath();
41
+ if (!existsSync(configDir)) {
42
+ mkdirSync(configDir, { recursive: true });
43
+ }
44
+ const tempPath = `${configPath}.tmp`;
45
+ writeFileSync(tempPath, JSON.stringify(config, null, 2), "utf-8");
46
+ chmodSync(tempPath, 384);
47
+ writeFileSync(configPath, readFileSync(tempPath));
48
+ chmodSync(configPath, 384);
49
+ try {
50
+ const { unlinkSync } = __require("fs");
51
+ unlinkSync(tempPath);
52
+ } catch {}
53
+ } catch (error) {
54
+ console.error("[cliproxyapi-sync] Failed to save config:", error);
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ // src/sync.ts
60
+ async function fetchWithTimeout(url, options, timeoutMs) {
61
+ const controller = new AbortController;
62
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
63
+ try {
64
+ const response = await fetch(url, {
65
+ ...options,
66
+ signal: controller.signal
67
+ });
68
+ return response;
69
+ } finally {
70
+ clearTimeout(timeoutId);
71
+ }
72
+ }
73
+ async function retryFetch(fetcher, maxRetries = 2) {
74
+ const delays = [1000, 2000];
75
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
76
+ try {
77
+ return await fetcher();
78
+ } catch (error) {
79
+ if (attempt === maxRetries) {
80
+ return null;
81
+ }
82
+ const delay = delays[attempt] || 2000;
83
+ await new Promise((resolve) => setTimeout(resolve, delay));
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ async function checkVersion(dashboardUrl, token) {
89
+ return retryFetch(async () => {
90
+ const url = `${dashboardUrl}/api/config-sync/version`;
91
+ const response = await fetchWithTimeout(url, {
92
+ method: "GET",
93
+ headers: {
94
+ Authorization: `Bearer ${token}`
95
+ }
96
+ }, 5000);
97
+ if (!response.ok) {
98
+ if (response.status === 401) {
99
+ console.error("[cliproxyapi-sync] Invalid or revoked sync token (401)");
100
+ }
101
+ throw new Error(`HTTP ${response.status}`);
102
+ }
103
+ return await response.json();
104
+ });
105
+ }
106
+ async function fetchBundle(dashboardUrl, token) {
107
+ return retryFetch(async () => {
108
+ const url = `${dashboardUrl}/api/config-sync/bundle`;
109
+ const response = await fetchWithTimeout(url, {
110
+ method: "GET",
111
+ headers: {
112
+ Authorization: `Bearer ${token}`
113
+ }
114
+ }, 1e4);
115
+ if (!response.ok) {
116
+ if (response.status === 401) {
117
+ console.error("[cliproxyapi-sync] Invalid or revoked sync token (401)");
118
+ }
119
+ throw new Error(`HTTP ${response.status}`);
120
+ }
121
+ return await response.json();
122
+ });
123
+ }
124
+
125
+ // src/writer.ts
126
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, renameSync } from "fs";
127
+ import { createHash } from "crypto";
128
+ import { homedir as homedir2 } from "os";
129
+ import { join as join2 } from "path";
130
+ function getOpenCodeConfigDir() {
131
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
132
+ if (xdgConfigHome) {
133
+ return join2(xdgConfigHome, "opencode");
134
+ }
135
+ return join2(homedir2(), ".config", "opencode");
136
+ }
137
+ async function writeConfigAtomic(filePath, content) {
138
+ const tempPath = `${filePath}.cliproxy-sync.tmp`;
139
+ writeFileSync2(tempPath, content, "utf-8");
140
+ renameSync(tempPath, filePath);
141
+ }
142
+ async function readFileHash(filePath) {
143
+ try {
144
+ if (!existsSync2(filePath)) {
145
+ return null;
146
+ }
147
+ const content = readFileSync2(filePath, "utf-8");
148
+ const hash = createHash("sha256");
149
+ hash.update(content);
150
+ return hash.digest("hex");
151
+ } catch (error) {
152
+ console.error(`[cliproxyapi-sync] Failed to hash ${filePath}:`, error);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ // src/index.ts
158
+ var ConfigSyncPlugin = async (ctx) => {
159
+ try {
160
+ const config = loadPluginConfig();
161
+ if (!config || !config.syncToken || !config.dashboardUrl) {
162
+ console.log("[cliproxyapi-sync] No config found. Skipping sync.");
163
+ return {};
164
+ }
165
+ const versionResult = await checkVersion(config.dashboardUrl, config.syncToken);
166
+ if (!versionResult) {
167
+ console.error("[cliproxyapi-sync] Failed to check version. Skipping sync.");
168
+ return {};
169
+ }
170
+ if (versionResult.version === config.lastKnownVersion) {
171
+ return {};
172
+ }
173
+ const bundle = await fetchBundle(config.dashboardUrl, config.syncToken);
174
+ if (!bundle) {
175
+ console.error("[cliproxyapi-sync] Failed to fetch bundle. Skipping sync.");
176
+ return {};
177
+ }
178
+ const configDir = getOpenCodeConfigDir();
179
+ const opencodeConfigPath = join3(configDir, "opencode.json");
180
+ const ohMyConfigPath = join3(configDir, "oh-my-opencode.json");
181
+ const oldOpencodeHash = await readFileHash(opencodeConfigPath);
182
+ await writeConfigAtomic(opencodeConfigPath, JSON.stringify(bundle.opencode, null, 2));
183
+ if (bundle.ohMyOpencode) {
184
+ await writeConfigAtomic(ohMyConfigPath, JSON.stringify(bundle.ohMyOpencode, null, 2));
185
+ }
186
+ const newOpencodeHash = await readFileHash(opencodeConfigPath);
187
+ if (oldOpencodeHash !== null && oldOpencodeHash !== newOpencodeHash) {
188
+ ctx.client.tui.showToast({
189
+ body: {
190
+ title: "Config Sync",
191
+ message: "opencode.json updated. Restart OpenCode to apply provider changes.",
192
+ variant: "warning",
193
+ duration: 8000
194
+ }
195
+ });
196
+ }
197
+ config.lastKnownVersion = bundle.version;
198
+ savePluginConfig(config);
199
+ console.log(`[cliproxyapi-sync] Synced to version ${bundle.version}`);
200
+ } catch (error) {
201
+ console.error("[cliproxyapi-sync] Sync error:", error);
202
+ }
203
+ return {};
204
+ };
205
+ var src_default = ConfigSyncPlugin;
206
+ export {
207
+ src_default as default
208
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "opencode-cliproxyapi-sync",
3
+ "version": "1.0.0",
4
+ "description": "Auto-sync OpenCode configs from CLIProxyAPI Dashboard",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "bun build src/index.ts --outdir dist --target node"
9
+ },
10
+ "peerDependencies": {
11
+ "@opencode-ai/plugin": "*"
12
+ },
13
+ "devDependencies": {
14
+ "@opencode-ai/plugin": "latest",
15
+ "typescript": "^5.0.0",
16
+ "@types/node": "^22.0.0"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "keywords": [
23
+ "opencode",
24
+ "plugin",
25
+ "config-sync",
26
+ "cliproxyapi"
27
+ ],
28
+ "license": "MIT",
29
+ "author": "itsmylife44",
30
+ "homepage": "https://github.com/itsmylife44/opencode-cliproxyapi-sync#readme",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/itsmylife44/opencode-cliproxyapi-sync.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/itsmylife44/opencode-cliproxyapi-sync/issues"
37
+ }
38
+ }