opencode-sync-plugin 0.2.6 → 0.2.7

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 CHANGED
@@ -27,7 +27,7 @@ npm run build
27
27
 
28
28
  ### 1. Get your credentials
29
29
 
30
- You need two things from your OpenSync deployment:
30
+ You need two things from your OpenSync deployment
31
31
 
32
32
  - **Convex URL**: Your deployment URL from the Convex dashboard (e.g., `https://your-project-123.convex.cloud`)
33
33
  - **API Key**: Generated in the OpenSync dashboard at **Settings > API Key** (starts with `osk_`)
package/dist/index.d.ts CHANGED
@@ -1,10 +1,5 @@
1
- declare const OpenCodeSyncPlugin: (_ctx: Record<string, unknown>) => Promise<{
2
- event: (input: {
3
- event: {
4
- type: string;
5
- properties?: Record<string, unknown>;
6
- };
7
- }) => Promise<void>;
8
- }>;
1
+ import { Plugin } from '@opencode-ai/plugin';
9
2
 
10
- export { OpenCodeSyncPlugin as default };
3
+ declare const OpenCodeSyncPlugin: Plugin;
4
+
5
+ export { OpenCodeSyncPlugin, OpenCodeSyncPlugin as default };
package/dist/index.js CHANGED
@@ -3,102 +3,208 @@ import {
3
3
  } from "./chunk-JPPDGYOB.js";
4
4
 
5
5
  // src/index.ts
6
- var syncedMessages = /* @__PURE__ */ new Set();
7
6
  var syncedSessions = /* @__PURE__ */ new Set();
8
- function extractText(content) {
9
- if (typeof content === "string") return content;
10
- if (Array.isArray(content)) {
11
- return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
7
+ var syncedMessages = /* @__PURE__ */ new Set();
8
+ var messagePartsText = /* @__PURE__ */ new Map();
9
+ var messageMetadata = /* @__PURE__ */ new Map();
10
+ var syncTimeouts = /* @__PURE__ */ new Map();
11
+ var DEBOUNCE_MS = 800;
12
+ function inferRole(textContent) {
13
+ const assistantPatterns = [
14
+ /^(I'll|Let me|Here's|I can|I've|I'm going to|I will|Sure|Certainly|Of course)/i,
15
+ /```[\s\S]+```/,
16
+ // Code blocks
17
+ /^(Yes|No),?\s+(I|you|we|this|that)/i,
18
+ // Answering patterns
19
+ /\*\*[^*]+\*\*/,
20
+ // Bold markdown (explanations)
21
+ /^\d+\.\s+\*\*/
22
+ // Numbered lists with bold
23
+ ];
24
+ const userPatterns = [
25
+ /\?$/,
26
+ // Questions
27
+ /^(create|fix|add|update|show|make|build|implement|write|delete|remove|change|modify|help|can you|please|I want|I need)/i,
28
+ /^@/
29
+ // File references
30
+ ];
31
+ for (const pattern of assistantPatterns) {
32
+ if (pattern.test(textContent)) {
33
+ return "assistant";
34
+ }
12
35
  }
13
- return "";
14
- }
15
- function getTitle(session) {
16
- const first = session.messages?.find((m) => m.role === "user");
17
- if (first) {
18
- const text = extractText(first.content);
19
- if (text) return text.slice(0, 100) + (text.length > 100 ? "..." : "");
36
+ for (const pattern of userPatterns) {
37
+ if (pattern.test(textContent)) {
38
+ return "user";
39
+ }
20
40
  }
21
- return "Untitled Session";
41
+ return textContent.length > 500 ? "assistant" : "user";
22
42
  }
23
43
  function doSyncSession(session) {
24
44
  try {
25
45
  const config = getConfig();
26
- if (!config?.apiKey || !config?.convexUrl) return;
46
+ if (!config?.apiKey || !config?.convexUrl) {
47
+ console.error("[opencode-sync] Missing config - cannot sync session");
48
+ return;
49
+ }
27
50
  const url = config.convexUrl.replace(".convex.cloud", ".convex.site");
51
+ console.log("[opencode-sync] Syncing session:", session.id);
52
+ const projectPath = session.path?.cwd || session.cwd || session.directory;
53
+ const modelId = session.modelID || session.model?.modelID || session.model;
54
+ const providerId = session.providerID || session.model?.providerID || session.provider;
55
+ const promptTokens = session.tokens?.input || session.usage?.promptTokens || 0;
56
+ const completionTokens = session.tokens?.output || session.usage?.completionTokens || 0;
57
+ const cost = session.cost || session.usage?.cost || 0;
28
58
  fetch(`${url}/sync/session`, {
29
59
  method: "POST",
30
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ Authorization: `Bearer ${config.apiKey}`
63
+ },
31
64
  body: JSON.stringify({
32
65
  externalId: session.id,
33
- title: session.title || getTitle(session),
34
- projectPath: session.cwd,
35
- projectName: session.cwd?.split("/").pop(),
36
- model: session.model,
37
- provider: session.provider,
38
- promptTokens: session.usage?.promptTokens || 0,
39
- completionTokens: session.usage?.completionTokens || 0,
40
- cost: session.usage?.cost || 0
66
+ title: session.title || "Untitled Session",
67
+ projectPath,
68
+ projectName: projectPath?.split("/").pop(),
69
+ model: modelId,
70
+ provider: providerId,
71
+ promptTokens,
72
+ completionTokens,
73
+ cost
41
74
  })
42
- }).catch(() => {
43
- });
44
- } catch {
75
+ }).then((r) => r.json()).then((data) => console.log("[opencode-sync] Session sync response:", data)).catch((err) => console.error("[opencode-sync] Session sync error:", err));
76
+ } catch (err) {
77
+ console.error("[opencode-sync] doSyncSession error:", err);
45
78
  }
46
79
  }
47
- function doSyncMessage(sessionId, message) {
80
+ function doSyncMessage(sessionId, messageId, role, textContent, metadata) {
48
81
  try {
49
82
  const config = getConfig();
50
- if (!config?.apiKey || !config?.convexUrl) return;
83
+ if (!config?.apiKey || !config?.convexUrl) {
84
+ console.error("[opencode-sync] Missing config - cannot sync message");
85
+ return;
86
+ }
87
+ if (!textContent || textContent.trim().length === 0) {
88
+ console.log("[opencode-sync] Skipping empty message:", messageId);
89
+ return;
90
+ }
91
+ const finalRole = role === "unknown" || !role ? inferRole(textContent) : role;
51
92
  const url = config.convexUrl.replace(".convex.cloud", ".convex.site");
93
+ console.log(
94
+ "[opencode-sync] Syncing message:",
95
+ messageId,
96
+ "role:",
97
+ finalRole,
98
+ "text length:",
99
+ textContent.length
100
+ );
101
+ let durationMs;
102
+ if (metadata?.time?.completed && metadata?.time?.created) {
103
+ durationMs = metadata.time.completed - metadata.time.created;
104
+ }
52
105
  fetch(`${url}/sync/message`, {
53
106
  method: "POST",
54
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.apiKey}` },
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ Authorization: `Bearer ${config.apiKey}`
110
+ },
55
111
  body: JSON.stringify({
56
112
  sessionExternalId: sessionId,
57
- externalId: message.id,
58
- role: message.role,
59
- textContent: extractText(message.content),
60
- model: message.model,
61
- promptTokens: message.usage?.promptTokens,
62
- completionTokens: message.usage?.completionTokens,
63
- durationMs: message.duration
113
+ externalId: messageId,
114
+ role: finalRole,
115
+ textContent,
116
+ model: metadata?.modelID,
117
+ promptTokens: metadata?.tokens?.input,
118
+ completionTokens: metadata?.tokens?.output,
119
+ durationMs
64
120
  })
65
- }).catch(() => {
66
- });
67
- } catch {
121
+ }).then((r) => r.json()).then((data) => console.log("[opencode-sync] Message sync response:", data)).catch((err) => console.error("[opencode-sync] Message sync error:", err));
122
+ } catch (err) {
123
+ console.error("[opencode-sync] doSyncMessage error:", err);
68
124
  }
69
125
  }
70
- var OpenCodeSyncPlugin = async (_ctx) => {
126
+ function trySyncMessage(messageId) {
127
+ if (syncedMessages.has(messageId)) return;
128
+ const metadata = messageMetadata.get(messageId);
129
+ const textParts = messagePartsText.get(messageId);
130
+ if (!metadata || !textParts || textParts.length === 0) return;
131
+ const textContent = textParts.join("");
132
+ if (!textContent.trim()) return;
133
+ syncedMessages.add(messageId);
134
+ doSyncMessage(metadata.sessionId, messageId, metadata.role, textContent, metadata.info);
135
+ messagePartsText.delete(messageId);
136
+ messageMetadata.delete(messageId);
137
+ }
138
+ function scheduleSyncMessage(messageId) {
139
+ const existing = syncTimeouts.get(messageId);
140
+ if (existing) clearTimeout(existing);
141
+ const timeout = setTimeout(() => {
142
+ syncTimeouts.delete(messageId);
143
+ trySyncMessage(messageId);
144
+ }, DEBOUNCE_MS);
145
+ syncTimeouts.set(messageId, timeout);
146
+ }
147
+ var OpenCodeSyncPlugin = async (input) => {
148
+ console.log("[opencode-sync] Plugin initialized for project:", input.project?.id);
71
149
  return {
72
- event: async (input) => {
150
+ event: async ({ event }) => {
73
151
  try {
74
- const { event } = input;
75
152
  const props = event.properties;
76
153
  if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.idle") {
77
- const session = props;
78
- if (session?.id) {
79
- if (event.type === "session.created" && syncedSessions.has(session.id)) return;
80
- if (event.type === "session.created") syncedSessions.add(session.id);
81
- doSyncSession(session);
154
+ const sessionId = props?.id;
155
+ if (sessionId) {
156
+ if (event.type === "session.created") {
157
+ if (syncedSessions.has(sessionId)) return;
158
+ syncedSessions.add(sessionId);
159
+ }
160
+ doSyncSession(props);
161
+ }
162
+ }
163
+ if (event.type === "message.updated") {
164
+ const info = props?.info;
165
+ if (info?.id && info?.sessionID && info?.role) {
166
+ console.log("[opencode-sync] Message metadata received:", info.id, "role:", info.role);
167
+ messageMetadata.set(info.id, {
168
+ role: info.role,
169
+ sessionId: info.sessionID,
170
+ info
171
+ });
172
+ if (messagePartsText.has(info.id)) {
173
+ scheduleSyncMessage(info.id);
174
+ }
82
175
  }
83
176
  }
84
- if (event.type === "message.updated" || event.type === "message.part.updated") {
85
- const messageProps = props;
86
- const sessionId = messageProps?.sessionId;
87
- const message = messageProps?.message;
88
- if (sessionId && message?.id && !syncedMessages.has(message.id)) {
89
- if (event.type === "message.part.updated" && message.status !== "completed" && message.role !== "user") {
90
- return;
177
+ if (event.type === "message.part.updated") {
178
+ const part = props?.part;
179
+ if (part?.type === "text" && part?.messageID && part?.sessionID) {
180
+ const messageId = part.messageID;
181
+ const text = part.text || "";
182
+ console.log(
183
+ "[opencode-sync] Text part received for message:",
184
+ messageId,
185
+ "length:",
186
+ text.length
187
+ );
188
+ messagePartsText.set(messageId, [text]);
189
+ if (!messageMetadata.has(messageId)) {
190
+ messageMetadata.set(messageId, {
191
+ role: "unknown",
192
+ // Will be inferred or updated from message.updated
193
+ sessionId: part.sessionID,
194
+ info: {}
195
+ });
91
196
  }
92
- syncedMessages.add(message.id);
93
- doSyncMessage(sessionId, message);
197
+ scheduleSyncMessage(messageId);
94
198
  }
95
199
  }
96
- } catch {
200
+ } catch (err) {
201
+ console.error("[opencode-sync] Event handler error:", err);
97
202
  }
98
203
  }
99
204
  };
100
205
  };
101
206
  var index_default = OpenCodeSyncPlugin;
102
207
  export {
208
+ OpenCodeSyncPlugin,
103
209
  index_default as default
104
210
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sync-plugin",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Sync your OpenCode sessions to the cloud",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,12 +36,13 @@
36
36
  },
37
37
  "license": "MIT",
38
38
  "devDependencies": {
39
+ "@opencode-ai/plugin": "^1.1.25",
39
40
  "@types/node": "^20.0.0",
40
41
  "tsup": "^8.0.0",
41
42
  "typescript": "^5.3.0"
42
43
  },
43
44
  "peerDependencies": {
44
- "@opencode-ai/plugin": ">=0.1.0"
45
+ "@opencode-ai/plugin": ">=1.0.0"
45
46
  },
46
47
  "peerDependenciesMeta": {
47
48
  "@opencode-ai/plugin": {