soulsync 1.0.12 → 1.0.14

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/index.js CHANGED
@@ -1,31 +1,229 @@
1
1
  const { spawn } = require('child_process');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
+ const https = require('https');
5
+ const http = require('http');
6
+ const crypto = require('crypto');
7
+ const os = require('os');
8
+
9
+ const { getDeviceName, loadConfig, saveConfig, isAuthenticated } = require('./src/config');
4
10
 
5
11
  let pythonProcess = null;
12
+ let deviceCodePolling = null;
6
13
 
7
14
  function getPluginDir() {
8
15
  return path.dirname(__filename);
9
16
  }
10
17
 
11
- function checkConfigExists(pluginDir) {
12
- const configPath = path.join(pluginDir, 'config.json');
13
- if (!fs.existsSync(configPath)) {
14
- const examplePath = path.join(pluginDir, 'config.json.example');
15
- if (fs.existsSync(examplePath)) {
16
- fs.copyFileSync(examplePath, configPath);
17
- console.log('[SoulSync] Created config.json from template');
18
+ function getCloudUrl() {
19
+ const cfg = loadConfig();
20
+ return (cfg && cfg.cloud_url) || 'https://soulsync.work';
21
+ }
22
+
23
+ function makeRequest(method, urlPath, body = null, token = null) {
24
+ return new Promise((resolve, reject) => {
25
+ const cloudUrl = getCloudUrl();
26
+ const isHttps = cloudUrl.startsWith('https');
27
+ const client = isHttps ? https : http;
28
+ const urlObj = new URL(cloudUrl + urlPath);
29
+
30
+ const options = {
31
+ hostname: urlObj.hostname,
32
+ port: urlObj.port || (isHttps ? 443 : 80),
33
+ path: urlObj.pathname + urlObj.search,
34
+ method: method,
35
+ headers: { 'Content-Type': 'application/json' }
36
+ };
37
+
38
+ if (token) {
39
+ options.headers['Authorization'] = `Bearer ${token}`;
40
+ }
41
+
42
+ const req = client.request(options, (res) => {
43
+ let data = '';
44
+ res.on('data', chunk => data += chunk);
45
+ res.on('end', () => {
46
+ try {
47
+ const json = JSON.parse(data);
48
+ resolve({ status: res.statusCode, body: json });
49
+ } catch (e) {
50
+ resolve({ status: res.statusCode, body: data });
51
+ }
52
+ });
53
+ });
54
+
55
+ req.on('error', reject);
56
+ if (body) req.write(JSON.stringify(body));
57
+ req.end();
58
+ });
59
+ }
60
+
61
+ async function getUserGreeting(token, deviceName) {
62
+ let userName = '朋友';
63
+ try {
64
+ const userResult = await makeRequest('GET', '/api/auth/user/info', null, token);
65
+ if (userResult.status === 200 && userResult.body) {
66
+ if (userResult.body.name) {
67
+ userName = userResult.body.name;
68
+ } else if (userResult.body.email) {
69
+ userName = userResult.body.email.split('@')[0];
70
+ }
18
71
  }
19
- return false;
72
+ } catch (e) {
73
+ console.error('[SoulSync] Failed to get user info:', e.message);
20
74
  }
75
+ return `${userName}你好,我是三澍,非常高兴能在 ${deviceName} 再次与你相遇。\n\n已同步: SOUL.md / USER.md / MEMORY.md`;
76
+ }
77
+
78
+ function detectAuthMode(api) {
79
+ const hasChatAPI = api && typeof api.registerTool === 'function';
80
+ const isLocalTTY = process.stdin.isTTY && !process.env.SSH_CLIENT && !process.env.SSH_TTY;
81
+ const isSSH = process.env.SSH_CLIENT || process.env.SSH_TTY;
82
+
83
+ if (!hasChatAPI && isLocalTTY) return 'oauth-local';
84
+ if (!hasChatAPI && isSSH) return 'device-code-cli';
85
+ return 'device-code-chat';
86
+ }
87
+
88
+ async function startOAuthLocal() {
89
+ const { createOAuthServer } = require('./src/oauth-server');
90
+ const open = require('open');
21
91
 
22
92
  try {
23
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
24
- const email = (config.email || '').trim();
25
- const password = (config.password || '').trim();
26
- return email && password && email !== 'your-email@example.com' && password !== 'your-password';
93
+ const { server, port } = await createOAuthServer();
94
+ const callbackUrl = `http://localhost:${port}/callback`;
95
+ const state = crypto.randomBytes(16).toString('hex');
96
+ const authUrl = `${getCloudUrl()}/auth/oauth/start?port=${port}&callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
97
+
98
+ console.log('[SoulSync] Opening browser for authorization...');
99
+ await open(authUrl);
100
+
101
+ console.log('[SoulSync] Waiting for authorization...');
102
+
103
+ const { token } = await new Promise((resolve, reject) => {
104
+ server.on('close', () => reject(new Error('Server closed without receiving token')));
105
+ });
106
+
107
+ const deviceId = crypto.randomUUID();
108
+ const deviceName = getDeviceName('local');
109
+
110
+ saveConfig({
111
+ email: 'device',
112
+ token: token,
113
+ device_id: deviceId,
114
+ device_name: deviceName,
115
+ device_type: 'local',
116
+ cloud_url: getCloudUrl()
117
+ });
118
+
119
+ await registerDevice(deviceId, deviceName, 'local', token);
120
+ startPythonService('--start');
121
+
122
+ const greeting = await getUserGreeting(token, deviceName);
123
+ return { success: true, message: greeting };
124
+ } catch (err) {
125
+ return { success: false, message: `OAuth Error: ${err.message}` };
126
+ }
127
+ }
128
+
129
+ async function startDeviceCodeCLI() {
130
+ try {
131
+ const result = await makeRequest('POST', '/api/auth/device-code', {});
132
+
133
+ if (result.status !== 200 || !result.body.device_code) {
134
+ return { success: false, message: 'Failed to start device authorization.' };
135
+ }
136
+
137
+ const { device_code, auth_url } = result.body;
138
+
139
+ const homeDir = os.homedir();
140
+ const pendingDir = path.join(homeDir, '.soulsync');
141
+ if (!fs.existsSync(pendingDir)) {
142
+ fs.mkdirSync(pendingDir, { recursive: true });
143
+ }
144
+ fs.writeFileSync(path.join(pendingDir, '.pending_auth'), JSON.stringify({
145
+ deviceCode: device_code,
146
+ authUrl: auth_url,
147
+ timestamp: Date.now()
148
+ }));
149
+
150
+ console.log('[SoulSync] Please visit the following URL to authorize:');
151
+ console.log(` ${auth_url}`);
152
+ console.log('[SoulSync] Or say "connect SoulSync" in chat to continue...');
153
+
154
+ const token = await waitForAuthCompletion(device_code);
155
+
156
+ const deviceId = crypto.randomUUID();
157
+ const deviceName = getDeviceName('ssh');
158
+
159
+ saveConfig({
160
+ email: 'device',
161
+ token: token,
162
+ device_id: deviceId,
163
+ device_name: deviceName,
164
+ device_type: 'ssh',
165
+ cloud_url: getCloudUrl()
166
+ });
167
+
168
+ registerDevice(deviceId, deviceName, 'ssh', token);
169
+ startPythonService('--start');
170
+
171
+ const greeting = await getUserGreeting(token, deviceName);
172
+ return { success: true, message: greeting };
173
+ } catch (err) {
174
+ return { success: false, message: `Error: ${err.message}` };
175
+ }
176
+ }
177
+
178
+ async function waitForAuthCompletion(device_code) {
179
+ return new Promise((resolve, reject) => {
180
+ const timeout = setTimeout(() => {
181
+ if (deviceCodePolling) {
182
+ clearInterval(deviceCodePolling);
183
+ deviceCodePolling = null;
184
+ }
185
+ reject(new Error('Authorization timeout'));
186
+ }, 900000);
187
+
188
+ deviceCodePolling = setInterval(async () => {
189
+ try {
190
+ const pollResult = await makeRequest('GET', `/api/auth/device-code/${device_code}/status`);
191
+
192
+ if (pollResult.body.status === 'authorized' && pollResult.body.token) {
193
+ clearInterval(deviceCodePolling);
194
+ deviceCodePolling = null;
195
+ clearTimeout(timeout);
196
+ resolve(pollResult.body.token);
197
+ } else if (pollResult.body.status === 'expired') {
198
+ clearInterval(deviceCodePolling);
199
+ deviceCodePolling = null;
200
+ clearTimeout(timeout);
201
+ reject(new Error('Authorization expired'));
202
+ } else if (pollResult.body.status === 'not_found') {
203
+ clearInterval(deviceCodePolling);
204
+ deviceCodePolling = null;
205
+ clearTimeout(timeout);
206
+ reject(new Error('Device code not found'));
207
+ }
208
+ } catch (e) {
209
+ clearInterval(deviceCodePolling);
210
+ deviceCodePolling = null;
211
+ clearTimeout(timeout);
212
+ reject(e);
213
+ }
214
+ }, 3000);
215
+ });
216
+ }
217
+
218
+ async function registerDevice(deviceId, deviceName, deviceType, token) {
219
+ try {
220
+ await makeRequest('POST', '/api/devices', {
221
+ device_id: deviceId,
222
+ device_name: deviceName,
223
+ device_type: deviceType
224
+ }, token);
27
225
  } catch (e) {
28
- return false;
226
+ console.error('[SoulSync] Failed to register device:', e.message);
29
227
  }
30
228
  }
31
229
 
@@ -36,63 +234,305 @@ function startPythonService(mode = '--start') {
36
234
 
37
235
  console.log(`[SoulSync] Starting Python service (${mode})...`);
38
236
 
237
+ if (pythonProcess) {
238
+ pythonProcess.kill();
239
+ pythonProcess = null;
240
+ }
241
+
39
242
  pythonProcess = spawn(pythonPath, [pythonScript, mode], {
40
243
  cwd: pluginDir,
41
- env: {
42
- ...process.env,
43
- OPENCLAW_PLUGIN: 'true',
44
- PLUGIN_DIR: pluginDir
45
- },
244
+ env: { ...process.env, OPENCLAW_PLUGIN: 'true', PLUGIN_DIR: pluginDir },
46
245
  stdio: 'inherit'
47
246
  });
48
-
247
+
49
248
  pythonProcess.on('close', (code) => {
50
249
  console.log(`[SoulSync] Python process exited with code ${code}`);
51
250
  pythonProcess = null;
52
-
53
- // If setup succeeded (code 0), auto-start sync
54
- if (mode === '--setup' && code === 0) {
55
- console.log('[SoulSync] Setup completed, starting sync service...');
56
- startPythonService('--start');
57
- }
58
251
  });
59
252
 
60
253
  pythonProcess.on('error', (err) => {
61
254
  console.error(`[SoulSync] Failed to start Python process: ${err}`);
62
255
  pythonProcess = null;
63
256
  });
257
+ }
258
+
259
+ function stopPythonService() {
260
+ if (pythonProcess) {
261
+ console.log('[SoulSync] Stopping Python service...');
262
+ pythonProcess.kill();
263
+ pythonProcess = null;
264
+ }
265
+ }
266
+
267
+ function checkConnection() {
268
+ const cfg = loadConfig();
269
+ if (!cfg || !cfg.token) {
270
+ return Promise.resolve({ connected: false, reason: 'not_configured' });
271
+ }
64
272
 
65
- return pythonProcess;
273
+ return makeRequest('GET', '/api/profiles', null, cfg.token)
274
+ .then(result => {
275
+ if (result.status === 200) return { connected: true, data: result.body };
276
+ if (result.status === 401) return { connected: false, reason: 'token_expired' };
277
+ return { connected: false, reason: 'server_error', status: result.status };
278
+ })
279
+ .catch(err => ({ connected: false, reason: 'connection_error', error: err.message }));
280
+ }
281
+
282
+ async function startDeviceCodeFlow() {
283
+ try {
284
+ const result = await makeRequest('POST', '/api/auth/device-code', {});
285
+
286
+ if (result.status !== 200 || !result.body.device_code) {
287
+ return { success: false, message: 'Failed to start device authorization.' };
288
+ }
289
+
290
+ const { device_code, auth_url, expires_in } = result.body;
291
+
292
+ const token = await new Promise((resolve, reject) => {
293
+ const timeout = setTimeout(() => {
294
+ if (deviceCodePolling) {
295
+ clearInterval(deviceCodePolling);
296
+ deviceCodePolling = null;
297
+ }
298
+ reject(new Error('Authorization timeout'));
299
+ }, expires_in * 1000);
300
+
301
+ deviceCodePolling = setInterval(async () => {
302
+ try {
303
+ const pollResult = await makeRequest('GET', `/api/auth/device-code/${device_code}/status`);
304
+
305
+ if (pollResult.body.status === 'authorized' && pollResult.body.token) {
306
+ clearInterval(deviceCodePolling);
307
+ deviceCodePolling = null;
308
+ clearTimeout(timeout);
309
+ resolve(pollResult.body.token);
310
+ } else if (pollResult.body.status === 'expired') {
311
+ clearInterval(deviceCodePolling);
312
+ deviceCodePolling = null;
313
+ clearTimeout(timeout);
314
+ reject(new Error('Authorization expired'));
315
+ } else if (pollResult.body.status === 'not_found') {
316
+ clearInterval(deviceCodePolling);
317
+ deviceCodePolling = null;
318
+ clearTimeout(timeout);
319
+ reject(new Error('Device code not found'));
320
+ }
321
+ } catch (e) {
322
+ clearInterval(deviceCodePolling);
323
+ deviceCodePolling = null;
324
+ clearTimeout(timeout);
325
+ reject(e);
326
+ }
327
+ }, 3000);
328
+ });
329
+
330
+ const deviceId = crypto.randomUUID();
331
+ const deviceName = getDeviceName('cloud');
332
+
333
+ saveConfig({
334
+ email: 'device',
335
+ token: token,
336
+ device_id: deviceId,
337
+ device_name: deviceName,
338
+ device_type: 'cloud',
339
+ cloud_url: getCloudUrl()
340
+ });
341
+
342
+ registerDevice(deviceId, deviceName, 'cloud', token);
343
+ startPythonService('--start');
344
+
345
+ const greeting = await getUserGreeting(token, deviceName);
346
+ return { success: true, message: greeting };
347
+ } catch (err) {
348
+ return { success: false, message: `Error: ${err.message}` };
349
+ }
66
350
  }
67
351
 
68
352
  module.exports = function register(api) {
69
353
  console.log('[SoulSync] Registering plugin...');
70
-
71
- api.registerCli(
72
- ({ program }) => {
73
- program
74
- .command('soulsync:setup')
75
- .description('首次配置:注册或登录 SoulSync 账号')
76
- .action(() => {
77
- console.log('[SoulSync] Starting setup...');
78
- startPythonService('--setup');
354
+
355
+ const mode = detectAuthMode(api);
356
+ console.log(`[SoulSync] Detected auth mode: ${mode}`);
357
+
358
+ if (!isAuthenticated()) {
359
+ switch (mode) {
360
+ case 'oauth-local':
361
+ console.log('[SoulSync] Starting OAuth local flow...');
362
+ startOAuthLocal().then(result => {
363
+ if (result.success) registerChatTools(api);
79
364
  });
80
- },
81
- { commands: ['soulsync:setup'] }
82
- );
365
+ break;
366
+ case 'device-code-cli':
367
+ console.log('[SoulSync] Starting device code CLI flow...');
368
+ startDeviceCodeCLI().then(result => {
369
+ console.log('[SoulSync]', result.message);
370
+ });
371
+ break;
372
+ case 'device-code-chat':
373
+ console.log('[SoulSync] Waiting for user to say "connect SoulSync"...');
374
+ break;
375
+ }
376
+ } else {
377
+ console.log('[SoulSync] Already authenticated, starting sync...');
378
+ startPythonService('--start');
379
+ registerChatTools(api);
380
+ }
381
+
382
+ function registerChatTools(api) {
383
+ api.registerTool({
384
+ name: 'soulsync_status',
385
+ description: 'Check SoulSync plugin status for OpenClaw (小龙虾). Call when user mentions SoulSync, soul sync, 灵魂同步, OpenClaw plugin, 小龙虾插件, or asks about sync status.',
386
+ input_schema: { type: 'object', properties: {}, required: [] }
387
+ }, async () => {
388
+ const cfg = loadConfig();
389
+ if (!cfg || !cfg.token) {
390
+ return { configured: false, message: 'SoulSync is not configured. Would you like to connect your SoulSync account?' };
391
+ }
392
+
393
+ const conn = await checkConnection();
394
+ if (conn.connected) {
395
+ return { configured: true, connected: true, email: cfg.email, message: `SoulSync is connected and syncing for ${cfg.email}.` };
396
+ } else if (conn.reason === 'token_expired') {
397
+ return { configured: true, connected: false, message: 'SoulSync token has expired. Would you like to reconnect your account?' };
398
+ } else {
399
+ return { configured: true, connected: false, message: `SoulSync is configured but cannot connect to server: ${conn.reason}` };
400
+ }
401
+ });
402
+
403
+ api.registerTool({
404
+ name: 'soulsync_connect',
405
+ description: 'Install and configure SoulSync plugin for OpenClaw (小龙虾). Call when user says "安装soulsync插件", "配置soulsync插件", "设置小龙虾同步", "连接soulsync". No credentials needed - will provide a browser link for authorization.',
406
+ input_schema: { type: 'object', properties: {}, required: [] }
407
+ }, async () => {
408
+ if (isAuthenticated()) {
409
+ return 'SoulSync is already connected. Use soulsync_status to check the current status.';
410
+ }
411
+
412
+ const result = await startDeviceCodeFlow();
413
+ return result.message;
414
+ });
415
+
416
+ api.registerTool({
417
+ name: 'soulsync_sync',
418
+ description: 'Manually trigger SoulSync sync. Call when user says "同步soulsync", "手动同步", "强制同步", or wants to force sync.',
419
+ input_schema: { type: 'object', properties: {}, required: [] }
420
+ }, async () => {
421
+ const cfg = loadConfig();
422
+ if (!cfg || !cfg.token) {
423
+ return 'SoulSync is not configured. Please connect first.';
424
+ }
425
+
426
+ if (!pythonProcess) {
427
+ startPythonService('--sync');
428
+ return 'Sync triggered.';
429
+ }
430
+ return 'Sync already in progress.';
431
+ });
83
432
 
433
+ api.registerTool({
434
+ name: 'soulsync_logout',
435
+ description: 'Logout and unbind device from SoulSync. Call when user says "退出soulsync", "解绑设备", "断开soulsync连接".',
436
+ input_schema: { type: 'object', properties: {}, required: [] }
437
+ }, async () => {
438
+ stopPythonService();
439
+
440
+ const cfg = loadConfig();
441
+ if (cfg && cfg.device_id && cfg.token) {
442
+ try {
443
+ await makeRequest('DELETE', `/api/devices/${cfg.device_id}`, null, cfg.token);
444
+ } catch (e) {
445
+ console.error('[SoulSync] Failed to delete device:', e.message);
446
+ }
447
+ }
448
+
449
+ try {
450
+ const pluginDir = getPluginDir();
451
+ const configPath = path.join(pluginDir, 'config.json');
452
+ if (fs.existsSync(configPath)) {
453
+ const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
454
+ delete existing.token;
455
+ delete existing.email;
456
+ delete existing.device_id;
457
+ delete existing.device_name;
458
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
459
+ }
460
+ } catch (e) {
461
+ console.error('[SoulSync] Error clearing config:', e);
462
+ }
463
+
464
+ return 'Logged out successfully. Your local data is preserved. To reconnect, say "connect SoulSync".';
465
+ });
466
+
467
+ api.registerTool({
468
+ name: 'soulsync_devices',
469
+ description: 'List connected SoulSync devices. Call when user says "查看soulsync设备", "我的设备列表", "已连接设备", or asks about connected devices.',
470
+ input_schema: { type: 'object', properties: {}, required: [] }
471
+ }, async () => {
472
+ const cfg = loadConfig();
473
+ if (!cfg || !cfg.token) {
474
+ return 'Not logged in / 未登录';
475
+ }
476
+
477
+ try {
478
+ const result = await makeRequest('GET', '/api/devices', null, cfg.token);
479
+ if (result.status === 200) {
480
+ const devices = result.body.devices || [];
481
+ if (devices.length === 0) return 'No devices found / 未找到设备';
482
+ const list = devices.map(d =>
483
+ `- ${d.device_name || 'Unnamed'} (${d.device_type || 'local'}) - Last sync: ${d.last_sync_at || 'Never'}`
484
+ ).join('\n');
485
+ return `Connected devices / 已连接设备:\n${list}`;
486
+ }
487
+ return 'Failed to get devices / 获取设备列表失败';
488
+ } catch (e) {
489
+ return `Error: ${e.message}`;
490
+ }
491
+ });
492
+
493
+ api.registerTool({
494
+ name: 'soulsync_rename_device',
495
+ description: 'Rename current SoulSync device. Call when user says "重命名设备", "修改设备名称".',
496
+ input_schema: {
497
+ type: 'object',
498
+ properties: {
499
+ name: { type: 'string', description: 'New device name / 新设备名称' }
500
+ },
501
+ required: ['name']
502
+ }
503
+ }, async ({ name }) => {
504
+ const cfg = loadConfig();
505
+ if (!cfg || !cfg.token || !cfg.device_id) {
506
+ return 'Not logged in / 未登录';
507
+ }
508
+
509
+ try {
510
+ const result = await makeRequest('PUT', `/api/devices/${cfg.device_id}`, { device_name: name }, cfg.token);
511
+ if (result.status === 200) {
512
+ const pluginDir = getPluginDir();
513
+ const configPath = path.join(pluginDir, 'config.json');
514
+ cfg.device_name = name;
515
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
516
+ return `Device renamed to "${name}" / 设备已重命名为 "${name}"`;
517
+ }
518
+ return 'Failed to rename device / 重命名设备失败';
519
+ } catch (e) {
520
+ return `Error: ${e.message}`;
521
+ }
522
+ });
523
+ }
524
+
84
525
  api.registerCli(
85
526
  ({ program }) => {
86
527
  program
87
528
  .command('soulsync:start')
88
529
  .description('启动 SoulSync 同步服务')
89
530
  .action(() => {
90
- const pluginDir = getPluginDir();
91
- const configPath = path.join(pluginDir, 'config.json');
92
-
93
- if (!checkConfigExists(pluginDir)) {
94
- console.log('[SoulSync] Not configured. Starting setup...');
95
- startPythonService('--setup');
531
+ if (!isAuthenticated()) {
532
+ console.log('[SoulSync] Not configured. Starting device authorization flow...');
533
+ startDeviceCodeFlow().then(result => {
534
+ console.log('[SoulSync]', result.message);
535
+ });
96
536
  return;
97
537
  }
98
538
 
@@ -113,18 +553,19 @@ module.exports = function register(api) {
113
553
  .command('soulsync:stop')
114
554
  .description('停止 SoulSync 同步服务')
115
555
  .action(() => {
116
- if (pythonProcess) {
117
- console.log('[SoulSync] Stopping Python service...');
118
- pythonProcess.kill();
119
- pythonProcess = null;
120
- } else {
121
- console.log('[SoulSync] Service not running');
122
- }
556
+ stopPythonService();
557
+ console.log('[SoulSync] Service stopped');
123
558
  });
124
559
  },
125
560
  { commands: ['soulsync:stop'] }
126
561
  );
127
562
 
563
+ if (isAuthenticated()) {
564
+ console.log('[SoulSync] Auto-starting sync service...');
565
+ startPythonService('--start');
566
+ registerChatTools(api);
567
+ }
568
+
128
569
  console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
129
570
  console.log('[SoulSync] Plugin registered successfully');
130
- };
571
+ };
@@ -7,12 +7,12 @@
7
7
  "properties": {
8
8
  "cloud_url": { "type": "string" },
9
9
  "email": { "type": "string" },
10
- "password": { "type": "string" }
10
+ "token": { "type": "string" }
11
11
  }
12
12
  },
13
13
  "uiHints": {
14
- "cloud_url": { "label": "服务器地址", "placeholder": "http://localhost:3000" },
15
- "email": { "label": "邮箱" },
16
- "password": { "label": "密码", "sensitive": true }
14
+ "cloud_url": { "label": "服务器地址 / Server URL", "placeholder": "https://soulsync.work" },
15
+ "email": { "label": "邮箱 / Email" },
16
+ "token": { "label": "Token", "sensitive": true }
17
17
  }
18
18
  }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "soulsync",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "SoulSync plugin for OpenClaw - cross-bot memory synchronization",
5
5
  "main": "index.js",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/alanliuc-a11y/soulsync"
9
9
  },
10
+ "dependencies": {
11
+ "open": "^10.1.0"
12
+ },
10
13
  "openclaw": {
11
14
  "extensions": [
12
15
  "./index.js"