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.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/index.js +208 -0
- 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
|
+
}
|