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 +1 -1
- package/dist/index.d.ts +4 -9
- package/dist/index.js +163 -57
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 "
|
|
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)
|
|
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: {
|
|
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 ||
|
|
34
|
-
projectPath
|
|
35
|
-
projectName:
|
|
36
|
-
model:
|
|
37
|
-
provider:
|
|
38
|
-
promptTokens
|
|
39
|
-
completionTokens
|
|
40
|
-
cost
|
|
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
|
-
|
|
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,
|
|
80
|
+
function doSyncMessage(sessionId, messageId, role, textContent, metadata) {
|
|
48
81
|
try {
|
|
49
82
|
const config = getConfig();
|
|
50
|
-
if (!config?.apiKey || !config?.convexUrl)
|
|
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: {
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
110
|
+
},
|
|
55
111
|
body: JSON.stringify({
|
|
56
112
|
sessionExternalId: sessionId,
|
|
57
|
-
externalId:
|
|
58
|
-
role:
|
|
59
|
-
textContent
|
|
60
|
-
model:
|
|
61
|
-
promptTokens:
|
|
62
|
-
completionTokens:
|
|
63
|
-
durationMs
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
78
|
-
if (
|
|
79
|
-
if (event.type === "session.created"
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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.
|
|
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": ">=
|
|
45
|
+
"@opencode-ai/plugin": ">=1.0.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependenciesMeta": {
|
|
47
48
|
"@opencode-ai/plugin": {
|