opencode-sync-plugin 0.2.4 → 0.2.6
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 +2 -0
- package/dist/cli.js +239 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -116,6 +116,8 @@ Data is stored in your Convex deployment. You can view, search, and share sessio
|
|
|
116
116
|
|---------|-------------|
|
|
117
117
|
| `opencode-sync login` | Configure with Convex URL and API Key |
|
|
118
118
|
| `opencode-sync verify` | Verify credentials and OpenCode config |
|
|
119
|
+
| `opencode-sync sync` | Test connectivity and create a test session |
|
|
120
|
+
| `opencode-sync sync --all` | Sync all local OpenCode sessions to the cloud |
|
|
119
121
|
| `opencode-sync logout` | Clear stored credentials |
|
|
120
122
|
| `opencode-sync status` | Show authentication status |
|
|
121
123
|
| `opencode-sync config` | Show current configuration |
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-JPPDGYOB.js";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import { readFileSync, existsSync } from "fs";
|
|
9
|
+
import { readFileSync, existsSync, readdirSync } from "fs";
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join, dirname } from "path";
|
|
12
12
|
import { createInterface } from "readline";
|
|
@@ -41,6 +41,9 @@ async function main() {
|
|
|
41
41
|
case "config":
|
|
42
42
|
showConfig();
|
|
43
43
|
break;
|
|
44
|
+
case "sync":
|
|
45
|
+
await sync();
|
|
46
|
+
break;
|
|
44
47
|
case "version":
|
|
45
48
|
case "-v":
|
|
46
49
|
case "--version":
|
|
@@ -199,21 +202,246 @@ function showConfig() {
|
|
|
199
202
|
console.log(" API Key:", config.apiKey ? config.apiKey.slice(0, 8) + "..." + config.apiKey.slice(-4) : "Not set");
|
|
200
203
|
console.log();
|
|
201
204
|
}
|
|
205
|
+
async function sync() {
|
|
206
|
+
const syncAll = args.includes("--all");
|
|
207
|
+
const config = getConfig();
|
|
208
|
+
if (!config || !config.apiKey || !config.convexUrl) {
|
|
209
|
+
console.log("\n Status: Not configured\n");
|
|
210
|
+
console.log(" Run: opencode-sync login\n");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const siteUrl = config.convexUrl.replace(".convex.cloud", ".convex.site");
|
|
214
|
+
if (syncAll) {
|
|
215
|
+
await syncAllSessions(siteUrl, config.apiKey);
|
|
216
|
+
} else {
|
|
217
|
+
await syncConnectivityTest(siteUrl, config.apiKey);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function syncConnectivityTest(siteUrl, apiKey) {
|
|
221
|
+
console.log("\n OpenSync Connectivity Test\n");
|
|
222
|
+
console.log(" Testing backend health...");
|
|
223
|
+
try {
|
|
224
|
+
const healthRes = await fetch(`${siteUrl}/health`);
|
|
225
|
+
if (healthRes.ok) {
|
|
226
|
+
const healthData = await healthRes.json();
|
|
227
|
+
console.log(" Health: OK");
|
|
228
|
+
console.log(" Response:", JSON.stringify(healthData));
|
|
229
|
+
} else {
|
|
230
|
+
console.log(" Health: FAILED");
|
|
231
|
+
console.log(" Status:", healthRes.status);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.log(" Health: FAILED");
|
|
236
|
+
console.log(" Error:", e instanceof Error ? e.message : String(e));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(" Testing sync endpoint...");
|
|
241
|
+
const testSessionId = `test-${Date.now()}`;
|
|
242
|
+
try {
|
|
243
|
+
const syncRes = await fetch(`${siteUrl}/sync/session`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: {
|
|
246
|
+
"Content-Type": "application/json",
|
|
247
|
+
Authorization: `Bearer ${apiKey}`
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({
|
|
250
|
+
externalId: testSessionId,
|
|
251
|
+
title: "CLI Sync Test",
|
|
252
|
+
projectPath: process.cwd(),
|
|
253
|
+
projectName: process.cwd().split("/").pop(),
|
|
254
|
+
model: "test",
|
|
255
|
+
provider: "opencode-sync-cli",
|
|
256
|
+
promptTokens: 0,
|
|
257
|
+
completionTokens: 0,
|
|
258
|
+
cost: 0
|
|
259
|
+
})
|
|
260
|
+
});
|
|
261
|
+
if (syncRes.ok) {
|
|
262
|
+
const syncData = await syncRes.json();
|
|
263
|
+
console.log(" Sync: OK");
|
|
264
|
+
console.log(" Response:", JSON.stringify(syncData));
|
|
265
|
+
console.log();
|
|
266
|
+
console.log(" Test session created. Check your OpenSync dashboard.\n");
|
|
267
|
+
} else {
|
|
268
|
+
console.log(" Sync: FAILED");
|
|
269
|
+
console.log(" Status:", syncRes.status);
|
|
270
|
+
const text = await syncRes.text();
|
|
271
|
+
if (text) console.log(" Body:", text);
|
|
272
|
+
console.log();
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.log(" Sync: FAILED");
|
|
276
|
+
console.log(" Error:", e instanceof Error ? e.message : String(e));
|
|
277
|
+
console.log();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function syncAllSessions(siteUrl, apiKey) {
|
|
281
|
+
console.log("\n OpenSync: Syncing All Local Sessions\n");
|
|
282
|
+
const opencodePath = join(homedir(), ".local", "share", "opencode", "storage");
|
|
283
|
+
const sessionPath = join(opencodePath, "session");
|
|
284
|
+
const messagePath = join(opencodePath, "message");
|
|
285
|
+
if (!existsSync(sessionPath)) {
|
|
286
|
+
console.log(" No OpenCode sessions found.");
|
|
287
|
+
console.log(" Expected path:", sessionPath);
|
|
288
|
+
console.log();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const sessions = [];
|
|
292
|
+
try {
|
|
293
|
+
const projectDirs = readdirSync(sessionPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
294
|
+
for (const projectDir of projectDirs) {
|
|
295
|
+
const projectSessionPath = join(sessionPath, projectDir);
|
|
296
|
+
const sessionFiles = readdirSync(projectSessionPath).filter((f) => f.endsWith(".json"));
|
|
297
|
+
for (const file of sessionFiles) {
|
|
298
|
+
try {
|
|
299
|
+
const content = readFileSync(join(projectSessionPath, file), "utf8");
|
|
300
|
+
const data = JSON.parse(content);
|
|
301
|
+
if (data.id) {
|
|
302
|
+
sessions.push({ file, data });
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.log(" Error reading sessions:", e instanceof Error ? e.message : String(e));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
console.log(` Found ${sessions.length} sessions
|
|
313
|
+
`);
|
|
314
|
+
if (sessions.length === 0) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let syncedSessions = 0;
|
|
318
|
+
let syncedMessages = 0;
|
|
319
|
+
let failedSessions = 0;
|
|
320
|
+
for (const session of sessions) {
|
|
321
|
+
const { data } = session;
|
|
322
|
+
process.stdout.write(` Syncing: ${data.title || data.slug || data.id}... `);
|
|
323
|
+
let totalPromptTokens = 0;
|
|
324
|
+
let totalCompletionTokens = 0;
|
|
325
|
+
let totalCost = 0;
|
|
326
|
+
let model = "";
|
|
327
|
+
let provider = "";
|
|
328
|
+
const sessionMessagePath = join(messagePath, data.id);
|
|
329
|
+
const messages = [];
|
|
330
|
+
if (existsSync(sessionMessagePath)) {
|
|
331
|
+
try {
|
|
332
|
+
const messageFiles = readdirSync(sessionMessagePath).filter((f) => f.endsWith(".json"));
|
|
333
|
+
for (const msgFile of messageFiles) {
|
|
334
|
+
try {
|
|
335
|
+
const msgContent = readFileSync(join(sessionMessagePath, msgFile), "utf8");
|
|
336
|
+
const msgData = JSON.parse(msgContent);
|
|
337
|
+
if (msgData.id && msgData.sessionID === data.id) {
|
|
338
|
+
messages.push(msgData);
|
|
339
|
+
if (msgData.tokens) {
|
|
340
|
+
totalPromptTokens += msgData.tokens.input || 0;
|
|
341
|
+
totalCompletionTokens += msgData.tokens.output || 0;
|
|
342
|
+
}
|
|
343
|
+
if (msgData.cost) {
|
|
344
|
+
totalCost += msgData.cost;
|
|
345
|
+
}
|
|
346
|
+
if (msgData.modelID && !model) {
|
|
347
|
+
model = msgData.modelID;
|
|
348
|
+
}
|
|
349
|
+
if (msgData.providerID && !provider) {
|
|
350
|
+
provider = msgData.providerID;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const sessionRes = await fetch(`${siteUrl}/sync/session`, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: {
|
|
363
|
+
"Content-Type": "application/json",
|
|
364
|
+
Authorization: `Bearer ${apiKey}`
|
|
365
|
+
},
|
|
366
|
+
body: JSON.stringify({
|
|
367
|
+
externalId: data.id,
|
|
368
|
+
title: data.title || data.slug || "Untitled",
|
|
369
|
+
projectPath: data.directory,
|
|
370
|
+
projectName: data.directory?.split("/").pop(),
|
|
371
|
+
model: model || "unknown",
|
|
372
|
+
provider: provider || "opencode",
|
|
373
|
+
promptTokens: totalPromptTokens,
|
|
374
|
+
completionTokens: totalCompletionTokens,
|
|
375
|
+
cost: totalCost
|
|
376
|
+
})
|
|
377
|
+
});
|
|
378
|
+
if (!sessionRes.ok) {
|
|
379
|
+
console.log("FAILED");
|
|
380
|
+
failedSessions++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
syncedSessions++;
|
|
384
|
+
let msgCount = 0;
|
|
385
|
+
for (const msg of messages) {
|
|
386
|
+
try {
|
|
387
|
+
const msgRes = await fetch(`${siteUrl}/sync/message`, {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: {
|
|
390
|
+
"Content-Type": "application/json",
|
|
391
|
+
Authorization: `Bearer ${apiKey}`
|
|
392
|
+
},
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
sessionExternalId: data.id,
|
|
395
|
+
externalId: msg.id,
|
|
396
|
+
role: msg.role,
|
|
397
|
+
textContent: "",
|
|
398
|
+
// We don't have content in the message metadata files
|
|
399
|
+
model: msg.modelID,
|
|
400
|
+
promptTokens: msg.tokens?.input,
|
|
401
|
+
completionTokens: msg.tokens?.output,
|
|
402
|
+
durationMs: msg.time?.completed && msg.time?.created ? msg.time.completed - msg.time.created : void 0
|
|
403
|
+
})
|
|
404
|
+
});
|
|
405
|
+
if (msgRes.ok) {
|
|
406
|
+
msgCount++;
|
|
407
|
+
syncedMessages++;
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
console.log(`OK (${msgCount} messages)`);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.log("FAILED");
|
|
415
|
+
failedSessions++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
console.log();
|
|
419
|
+
console.log(` Summary:`);
|
|
420
|
+
console.log(` Sessions synced: ${syncedSessions}`);
|
|
421
|
+
console.log(` Messages synced: ${syncedMessages}`);
|
|
422
|
+
if (failedSessions > 0) {
|
|
423
|
+
console.log(` Failed: ${failedSessions}`);
|
|
424
|
+
}
|
|
425
|
+
console.log();
|
|
426
|
+
console.log(" Check your OpenSync dashboard to view synced sessions.\n");
|
|
427
|
+
}
|
|
202
428
|
function help() {
|
|
203
429
|
const version = getVersion();
|
|
204
430
|
console.log(`
|
|
205
431
|
OpenSync CLI v${version}
|
|
206
432
|
|
|
207
|
-
Usage: opencode-sync <command>
|
|
433
|
+
Usage: opencode-sync <command> [options]
|
|
208
434
|
|
|
209
435
|
Commands:
|
|
210
|
-
login
|
|
211
|
-
verify
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
436
|
+
login Configure with Convex URL and API Key
|
|
437
|
+
verify Verify credentials and OpenCode config
|
|
438
|
+
sync Test connectivity and create a test session
|
|
439
|
+
sync --all Sync all local OpenCode sessions to the cloud
|
|
440
|
+
logout Clear stored credentials
|
|
441
|
+
status Show current authentication status
|
|
442
|
+
config Show current configuration
|
|
443
|
+
version Show version number
|
|
444
|
+
help Show this help message
|
|
217
445
|
|
|
218
446
|
Setup:
|
|
219
447
|
1. Go to your OpenSync dashboard Settings page
|
|
@@ -222,6 +450,8 @@ function help() {
|
|
|
222
450
|
4. Enter your Convex URL and API Key
|
|
223
451
|
5. Add plugin to opencode.json (see instructions after login)
|
|
224
452
|
6. Run: opencode-sync verify
|
|
453
|
+
7. Run: opencode-sync sync (to test connectivity)
|
|
454
|
+
8. Run: opencode-sync sync --all (to sync existing sessions)
|
|
225
455
|
`);
|
|
226
456
|
}
|
|
227
457
|
function prompt(question) {
|