opencode-sync-plugin 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/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # opencode-sync-plugin
2
+
3
+ Sync your OpenCode sessions to the cloud. Search, share, and access your coding history from anywhere.
4
+
5
+ ## Installation
6
+
7
+ ### From npm
8
+
9
+ ```bash
10
+ npm install -g opencode-sync-plugin
11
+ ```
12
+
13
+ ### From source
14
+
15
+ ```bash
16
+ git clone https://github.com/waynesutton/opencode-sync-plugin
17
+ cd opencode-sync-plugin
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ ## Setup
23
+
24
+ ### 1. Get your credentials
25
+
26
+ You need two things from your OpenSync deployment:
27
+
28
+ - **Convex URL**: Your deployment URL from the Convex dashboard (e.g., `https://your-project-123.convex.cloud`)
29
+ - **API Key**: Generated in the OpenSync dashboard at **Settings > API Key** (starts with `osk_`)
30
+
31
+ The plugin automatically converts the `.cloud` URL to `.site` for API calls.
32
+
33
+ ### 2. Configure the plugin
34
+
35
+ ```bash
36
+ opencode-sync login
37
+ ```
38
+
39
+ Follow the prompts:
40
+
41
+ 1. Enter your Convex URL
42
+ 2. Enter your API Key
43
+
44
+ No browser authentication required.
45
+
46
+ ### 3. Add to OpenCode
47
+
48
+ Add the plugin to your `opencode.json`:
49
+
50
+ ```json
51
+ {
52
+ "$schema": "https://opencode.ai/config.json",
53
+ "plugin": ["opencode-sync-plugin"]
54
+ }
55
+ ```
56
+
57
+ Or add globally at `~/.config/opencode/opencode.json`.
58
+
59
+ ## How it works
60
+
61
+ The plugin hooks into OpenCode events and syncs data automatically:
62
+
63
+ | Event | Action |
64
+ |-------|--------|
65
+ | `session.created` | Creates session record in cloud |
66
+ | `session.updated` | Updates session metadata |
67
+ | `session.idle` | Final sync with token counts and cost |
68
+ | `message.updated` | Syncs user and assistant messages |
69
+ | `message.part.updated` | Syncs completed message parts |
70
+
71
+ Data is stored in your Convex deployment. You can view, search, and share sessions via the web UI.
72
+
73
+ ## CLI Commands
74
+
75
+ | Command | Description |
76
+ |---------|-------------|
77
+ | `opencode-sync login` | Configure with Convex URL and API Key |
78
+ | `opencode-sync logout` | Clear stored credentials |
79
+ | `opencode-sync status` | Show authentication status |
80
+ | `opencode-sync config` | Show current configuration |
81
+
82
+ ## Configuration storage
83
+
84
+ Credentials are stored at:
85
+
86
+ ```
87
+ ~/.config/opencode-sync/
88
+ config.json # Convex URL, API Key
89
+ ```
90
+
91
+ ## Plugin architecture
92
+
93
+ This plugin follows the [OpenCode plugin specification](https://opencode.ai/docs/plugins/):
94
+
95
+ ```typescript
96
+ import type { Plugin } from "@opencode-ai/plugin";
97
+
98
+ export const OpenCodeSyncPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
99
+ // Initialize plugin
100
+ await client.app.log({
101
+ service: "opencode-sync",
102
+ level: "info",
103
+ message: "Plugin initialized",
104
+ });
105
+
106
+ return {
107
+ // Subscribe to events
108
+ event: async ({ event }) => {
109
+ if (event.type === "session.created") {
110
+ // Sync session to cloud
111
+ }
112
+ if (event.type === "message.updated") {
113
+ // Sync message to cloud
114
+ }
115
+ },
116
+ };
117
+ };
118
+ ```
119
+
120
+ ## Troubleshooting
121
+
122
+ ### "Not authenticated" errors
123
+
124
+ ```bash
125
+ opencode-sync login
126
+ ```
127
+
128
+ ### Invalid API Key
129
+
130
+ 1. Go to your OpenSync dashboard
131
+ 2. Navigate to Settings
132
+ 3. Generate a new API Key
133
+ 4. Run `opencode-sync login` with the new key
134
+
135
+ ### Check status
136
+
137
+ ```bash
138
+ opencode-sync status
139
+ ```
140
+
141
+ ### View logs
142
+
143
+ Plugin logs are available in OpenCode's log output. Look for entries with `service: "opencode-sync"`.
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ # Install dependencies
149
+ npm install
150
+
151
+ # Build
152
+ npm run build
153
+
154
+ # Watch mode
155
+ npm run dev
156
+ ```
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,242 @@
1
+ // src/index.ts
2
+ import Conf from "conf";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ var config = new Conf({
6
+ projectName: "opencode-sync",
7
+ cwd: join(homedir(), ".config", "opencode-sync"),
8
+ configName: "config"
9
+ });
10
+ function getConfig() {
11
+ const url = config.get("convexUrl");
12
+ const key = config.get("apiKey");
13
+ if (!url) return null;
14
+ return { convexUrl: url, apiKey: key || "" };
15
+ }
16
+ function setConfig(cfg) {
17
+ config.set("convexUrl", cfg.convexUrl);
18
+ config.set("apiKey", cfg.apiKey);
19
+ }
20
+ function clearConfig() {
21
+ config.clear();
22
+ }
23
+ function getApiKey() {
24
+ const cfg = getConfig();
25
+ if (!cfg || !cfg.apiKey) return null;
26
+ return cfg.apiKey;
27
+ }
28
+ function normalizeToSiteUrl(url) {
29
+ if (url.includes(".convex.cloud")) {
30
+ return url.replace(".convex.cloud", ".convex.site");
31
+ }
32
+ return url;
33
+ }
34
+ function getSiteUrl() {
35
+ const cfg = getConfig();
36
+ if (!cfg || !cfg.convexUrl) return null;
37
+ return normalizeToSiteUrl(cfg.convexUrl);
38
+ }
39
+ async function syncSession(session, client) {
40
+ const apiKey = getApiKey();
41
+ const siteUrl = getSiteUrl();
42
+ if (!apiKey || !siteUrl) {
43
+ await client.app.log({
44
+ service: "opencode-sync",
45
+ level: "warn",
46
+ message: "Not authenticated. Run: opencode-sync login"
47
+ });
48
+ return;
49
+ }
50
+ try {
51
+ const response = await fetch(`${siteUrl}/sync/session`, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ Authorization: `Bearer ${apiKey}`
56
+ },
57
+ body: JSON.stringify({
58
+ externalId: session.id,
59
+ title: session.title || extractTitle(session),
60
+ projectPath: session.cwd,
61
+ projectName: session.cwd?.split("/").pop(),
62
+ model: session.model,
63
+ provider: session.provider,
64
+ promptTokens: session.usage?.promptTokens || 0,
65
+ completionTokens: session.usage?.completionTokens || 0,
66
+ cost: session.usage?.cost || 0
67
+ })
68
+ });
69
+ if (!response.ok) {
70
+ const errorText = await response.text();
71
+ await client.app.log({
72
+ service: "opencode-sync",
73
+ level: "error",
74
+ message: `Session sync failed: ${errorText}`
75
+ });
76
+ }
77
+ } catch (e) {
78
+ await client.app.log({
79
+ service: "opencode-sync",
80
+ level: "error",
81
+ message: `Session sync error: ${e}`
82
+ });
83
+ }
84
+ }
85
+ async function syncMessage(sessionId, message, client) {
86
+ const apiKey = getApiKey();
87
+ const siteUrl = getSiteUrl();
88
+ if (!apiKey || !siteUrl) return;
89
+ const parts = extractParts(message.content);
90
+ try {
91
+ const response = await fetch(`${siteUrl}/sync/message`, {
92
+ method: "POST",
93
+ headers: {
94
+ "Content-Type": "application/json",
95
+ Authorization: `Bearer ${apiKey}`
96
+ },
97
+ body: JSON.stringify({
98
+ sessionExternalId: sessionId,
99
+ externalId: message.id,
100
+ role: message.role,
101
+ textContent: extractTextContent(message.content),
102
+ model: message.model,
103
+ promptTokens: message.usage?.promptTokens,
104
+ completionTokens: message.usage?.completionTokens,
105
+ durationMs: message.duration,
106
+ parts
107
+ })
108
+ });
109
+ if (!response.ok) {
110
+ const errorText = await response.text();
111
+ await client.app.log({
112
+ service: "opencode-sync",
113
+ level: "error",
114
+ message: `Message sync failed: ${errorText}`
115
+ });
116
+ }
117
+ } catch (e) {
118
+ await client.app.log({
119
+ service: "opencode-sync",
120
+ level: "error",
121
+ message: `Message sync error: ${e}`
122
+ });
123
+ }
124
+ }
125
+ function extractTitle(session) {
126
+ const firstMessage = session.messages?.find((m) => m.role === "user");
127
+ if (firstMessage) {
128
+ const text = extractTextContent(firstMessage.content);
129
+ if (text) {
130
+ return text.slice(0, 100) + (text.length > 100 ? "..." : "");
131
+ }
132
+ }
133
+ return "Untitled Session";
134
+ }
135
+ function extractTextContent(content) {
136
+ if (typeof content === "string") return content;
137
+ if (Array.isArray(content)) {
138
+ return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
139
+ }
140
+ return "";
141
+ }
142
+ function extractParts(content) {
143
+ if (typeof content === "string") {
144
+ return [{ type: "text", content }];
145
+ }
146
+ if (Array.isArray(content)) {
147
+ return content.map((part) => {
148
+ if (part.type === "text") {
149
+ return { type: "text", content: part.text };
150
+ }
151
+ if (part.type === "tool_use" || part.type === "tool-call") {
152
+ const toolPart = part;
153
+ return {
154
+ type: "tool-call",
155
+ content: { name: toolPart.name, args: toolPart.input || toolPart.args || {} }
156
+ };
157
+ }
158
+ if (part.type === "tool_result" || part.type === "tool-result") {
159
+ const resultPart = part;
160
+ return {
161
+ type: "tool-result",
162
+ content: { result: resultPart.content || resultPart.result }
163
+ };
164
+ }
165
+ return { type: part.type, content: part };
166
+ });
167
+ }
168
+ return [];
169
+ }
170
+ var syncedMessages = /* @__PURE__ */ new Set();
171
+ var syncedSessions = /* @__PURE__ */ new Set();
172
+ var OpenCodeSyncPlugin = async ({ project, client, $, directory, worktree }) => {
173
+ const cfg = getConfig();
174
+ if (!cfg || !cfg.apiKey) {
175
+ await client.app.log({
176
+ service: "opencode-sync",
177
+ level: "warn",
178
+ message: "Not configured. Run: opencode-sync login"
179
+ });
180
+ } else {
181
+ await client.app.log({
182
+ service: "opencode-sync",
183
+ level: "info",
184
+ message: "Plugin initialized",
185
+ extra: { directory, worktree }
186
+ });
187
+ }
188
+ return {
189
+ // Handle all events via generic event handler
190
+ event: async ({ event }) => {
191
+ const props = event.properties;
192
+ if (event.type === "session.created") {
193
+ const session = props;
194
+ if (session?.id && !syncedSessions.has(session.id)) {
195
+ syncedSessions.add(session.id);
196
+ await syncSession(session, client);
197
+ }
198
+ }
199
+ if (event.type === "session.updated") {
200
+ const session = props;
201
+ if (session?.id) {
202
+ await syncSession(session, client);
203
+ }
204
+ }
205
+ if (event.type === "session.idle") {
206
+ const session = props;
207
+ if (session?.id) {
208
+ await syncSession(session, client);
209
+ }
210
+ }
211
+ if (event.type === "message.updated") {
212
+ const messageProps = props;
213
+ const sessionId = messageProps?.sessionId;
214
+ const message = messageProps?.message;
215
+ if (sessionId && message?.id && !syncedMessages.has(message.id)) {
216
+ syncedMessages.add(message.id);
217
+ await syncMessage(sessionId, message, client);
218
+ }
219
+ }
220
+ if (event.type === "message.part.updated") {
221
+ const messageProps = props;
222
+ const sessionId = messageProps?.sessionId;
223
+ const message = messageProps?.message;
224
+ if (sessionId && message?.id) {
225
+ if (message.status === "completed" || message.role === "user") {
226
+ if (!syncedMessages.has(message.id)) {
227
+ syncedMessages.add(message.id);
228
+ await syncMessage(sessionId, message, client);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ };
235
+ };
236
+
237
+ export {
238
+ getConfig,
239
+ setConfig,
240
+ clearConfig,
241
+ OpenCodeSyncPlugin
242
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ clearConfig,
4
+ getConfig,
5
+ setConfig
6
+ } from "./chunk-LXC2JAKD.js";
7
+
8
+ // src/cli.ts
9
+ var args = process.argv.slice(2);
10
+ var command = args[0];
11
+ async function main() {
12
+ switch (command) {
13
+ case "login":
14
+ await login();
15
+ break;
16
+ case "logout":
17
+ logout();
18
+ break;
19
+ case "status":
20
+ status();
21
+ break;
22
+ case "config":
23
+ showConfig();
24
+ break;
25
+ default:
26
+ help();
27
+ }
28
+ }
29
+ async function login() {
30
+ console.log("\n OpenSync Login\n");
31
+ const convexUrl = await prompt("Convex URL (e.g., https://your-project.convex.cloud): ");
32
+ if (!convexUrl) {
33
+ console.error("Convex URL is required");
34
+ process.exit(1);
35
+ }
36
+ if (!convexUrl.includes(".convex.cloud") && !convexUrl.includes(".convex.site")) {
37
+ console.error("Invalid Convex URL. Should end with .convex.cloud or .convex.site");
38
+ process.exit(1);
39
+ }
40
+ const apiKey = await prompt("API Key (from Settings page, starts with osk_): ");
41
+ if (!apiKey) {
42
+ console.error("API Key is required");
43
+ process.exit(1);
44
+ }
45
+ if (!apiKey.startsWith("osk_")) {
46
+ console.error("Invalid API Key format. Should start with 'osk_'");
47
+ process.exit(1);
48
+ }
49
+ const siteUrl = convexUrl.replace(".convex.cloud", ".convex.site");
50
+ console.log("\nVerifying credentials...");
51
+ try {
52
+ const response = await fetch(`${siteUrl}/health`);
53
+ if (!response.ok) {
54
+ console.error("\nFailed to connect to OpenSync backend.");
55
+ console.error("Please verify your Convex URL is correct.");
56
+ process.exit(1);
57
+ }
58
+ setConfig({ convexUrl, apiKey });
59
+ console.log("\nLogin successful!\n");
60
+ console.log(" Convex URL:", convexUrl);
61
+ console.log(" API Key:", apiKey.slice(0, 8) + "..." + apiKey.slice(-4));
62
+ console.log("\n Add the plugin to your opencode.json:");
63
+ console.log(' { "plugin": ["opencode-sync-plugin"] }\n');
64
+ } catch (e) {
65
+ console.error("\nFailed to connect to OpenSync backend.");
66
+ console.error("Please verify your Convex URL is correct.");
67
+ process.exit(1);
68
+ }
69
+ }
70
+ function logout() {
71
+ clearConfig();
72
+ console.log("\nLogged out successfully\n");
73
+ }
74
+ function status() {
75
+ const config = getConfig();
76
+ console.log("\n OpenSync Status\n");
77
+ if (!config) {
78
+ console.log(" Status: Not configured\n");
79
+ console.log(" Run: opencode-sync login\n");
80
+ return;
81
+ }
82
+ if (!config.apiKey) {
83
+ console.log(" Status: Not authenticated\n");
84
+ console.log(" Convex URL:", config.convexUrl);
85
+ console.log("\n Run: opencode-sync login\n");
86
+ return;
87
+ }
88
+ console.log(" Status: Configured\n");
89
+ console.log(" Convex URL:", config.convexUrl);
90
+ console.log(" API Key:", config.apiKey.slice(0, 8) + "..." + config.apiKey.slice(-4));
91
+ console.log();
92
+ }
93
+ function showConfig() {
94
+ const config = getConfig();
95
+ console.log("\n OpenSync Config\n");
96
+ if (!config) {
97
+ console.log(" No configuration found.\n");
98
+ console.log(" Run: opencode-sync login\n");
99
+ return;
100
+ }
101
+ console.log(" Convex URL:", config.convexUrl);
102
+ console.log(" API Key:", config.apiKey ? config.apiKey.slice(0, 8) + "..." + config.apiKey.slice(-4) : "Not set");
103
+ console.log();
104
+ }
105
+ function help() {
106
+ console.log(`
107
+ OpenSync CLI
108
+
109
+ Usage: opencode-sync <command>
110
+
111
+ Commands:
112
+ login Configure with Convex URL and API Key
113
+ logout Clear stored credentials
114
+ status Show current authentication status
115
+ config Show current configuration
116
+
117
+ Setup:
118
+ 1. Go to your OpenSync dashboard Settings page
119
+ 2. Generate an API Key (starts with osk_)
120
+ 3. Run: opencode-sync login
121
+ 4. Enter your Convex URL and API Key
122
+ 5. Add plugin to opencode.json: { "plugin": ["opencode-sync-plugin"] }
123
+ `);
124
+ }
125
+ function prompt(question) {
126
+ return new Promise((resolve) => {
127
+ process.stdout.write(question);
128
+ let input = "";
129
+ process.stdin.setEncoding("utf8");
130
+ process.stdin.once("data", (data) => {
131
+ input = data.toString().trim();
132
+ resolve(input);
133
+ });
134
+ });
135
+ }
136
+ main().catch(console.error);
@@ -0,0 +1,42 @@
1
+ interface PluginClient {
2
+ app: {
3
+ log: (entry: {
4
+ service: string;
5
+ level: "debug" | "info" | "warn" | "error";
6
+ message: string;
7
+ extra?: Record<string, unknown>;
8
+ }) => Promise<void>;
9
+ };
10
+ }
11
+ interface PluginContext {
12
+ project: unknown;
13
+ client: PluginClient;
14
+ $: unknown;
15
+ directory: string;
16
+ worktree: string;
17
+ }
18
+ interface PluginEvent {
19
+ type: string;
20
+ properties?: Record<string, unknown>;
21
+ }
22
+ interface PluginHooks {
23
+ event?: (input: {
24
+ event: PluginEvent;
25
+ }) => Promise<void>;
26
+ }
27
+ type Plugin = (ctx: PluginContext) => Promise<PluginHooks>;
28
+ interface Config {
29
+ convexUrl: string;
30
+ apiKey: string;
31
+ }
32
+ declare function getConfig(): Config | null;
33
+ declare function setConfig(cfg: Config): void;
34
+ declare function clearConfig(): void;
35
+ /**
36
+ * OpenCode Sync Plugin
37
+ * Syncs sessions and messages to cloud storage via Convex backend
38
+ * Authentication: API Key (osk_*) from OpenSync Settings page
39
+ */
40
+ declare const OpenCodeSyncPlugin: Plugin;
41
+
42
+ export { OpenCodeSyncPlugin, clearConfig, getConfig, setConfig };
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ OpenCodeSyncPlugin,
3
+ clearConfig,
4
+ getConfig,
5
+ setConfig
6
+ } from "./chunk-LXC2JAKD.js";
7
+ export {
8
+ OpenCodeSyncPlugin,
9
+ clearConfig,
10
+ getConfig,
11
+ setConfig
12
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "opencode-sync-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Sync your OpenCode sessions to the cloud",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "opencode-sync": "dist/cli.js"
16
+ },
17
+ "scripts": {
18
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
19
+ "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "keywords": [
25
+ "opencode",
26
+ "opencode-plugin",
27
+ "ai",
28
+ "sync",
29
+ "sessions",
30
+ "convex"
31
+ ],
32
+ "author": "waynesutton",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/waynesutton/opencode-sync-plugin"
36
+ },
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "conf": "^12.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.0.0",
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.3.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@opencode-ai/plugin": ">=0.1.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@opencode-ai/plugin": {
51
+ "optional": true
52
+ }
53
+ }
54
+ }