soulsync 1.0.13 → 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
@@ -4,74 +4,27 @@ const fs = require('fs');
4
4
  const https = require('https');
5
5
  const http = require('http');
6
6
  const crypto = require('crypto');
7
+ const os = require('os');
8
+
9
+ const { getDeviceName, loadConfig, saveConfig, isAuthenticated } = require('./src/config');
7
10
 
8
11
  let pythonProcess = null;
9
- let config = null;
10
12
  let deviceCodePolling = null;
11
13
 
12
14
  function getPluginDir() {
13
15
  return path.dirname(__filename);
14
16
  }
15
17
 
16
- function loadConfig(pluginDir) {
17
- const configPath = path.join(pluginDir, 'config.json');
18
- try {
19
- if (fs.existsSync(configPath)) {
20
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
21
- }
22
- } catch (e) {
23
- console.error('[SoulSync] Error loading config:', e);
24
- }
25
- return null;
26
- }
27
-
28
- function checkConfigExists(pluginDir) {
29
- const configPath = path.join(pluginDir, 'config.json');
30
- if (!fs.existsSync(configPath)) {
31
- const examplePath = path.join(pluginDir, 'config.json.example');
32
- if (fs.existsSync(examplePath)) {
33
- fs.copyFileSync(examplePath, configPath);
34
- console.log('[SoulSync] Created config.json from template');
35
- }
36
- return false;
37
- }
38
-
39
- config = loadConfig(pluginDir);
40
- if (!config) return false;
41
-
42
- const email = (config.email || '').trim();
43
- const token = config.token;
44
- return email && token && email !== 'your-email@example.com';
45
- }
46
-
47
- function saveConfig(pluginDir, newConfig) {
48
- const configPath = path.join(pluginDir, 'config.json');
49
- try {
50
- let existing = {};
51
- if (fs.existsSync(configPath)) {
52
- existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
53
- }
54
- const merged = { ...existing, ...newConfig };
55
- fs.writeFileSync(configPath, JSON.stringify(merged, null, 2));
56
- config = merged;
57
- return true;
58
- } catch (e) {
59
- console.error('[SoulSync] Error saving config:', e);
60
- return false;
61
- }
62
- }
63
-
64
- function getCloudUrl(pluginDir) {
65
- const cfg = loadConfig(pluginDir);
18
+ function getCloudUrl() {
19
+ const cfg = loadConfig();
66
20
  return (cfg && cfg.cloud_url) || 'https://soulsync.work';
67
21
  }
68
22
 
69
23
  function makeRequest(method, urlPath, body = null, token = null) {
70
24
  return new Promise((resolve, reject) => {
71
- const cloudUrl = getCloudUrl(getPluginDir());
25
+ const cloudUrl = getCloudUrl();
72
26
  const isHttps = cloudUrl.startsWith('https');
73
27
  const client = isHttps ? https : http;
74
-
75
28
  const urlObj = new URL(cloudUrl + urlPath);
76
29
 
77
30
  const options = {
@@ -79,9 +32,7 @@ function makeRequest(method, urlPath, body = null, token = null) {
79
32
  port: urlObj.port || (isHttps ? 443 : 80),
80
33
  path: urlObj.pathname + urlObj.search,
81
34
  method: method,
82
- headers: {
83
- 'Content-Type': 'application/json'
84
- }
35
+ headers: { 'Content-Type': 'application/json' }
85
36
  };
86
37
 
87
38
  if (token) {
@@ -102,14 +53,180 @@ function makeRequest(method, urlPath, body = null, token = null) {
102
53
  });
103
54
 
104
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
+ }
71
+ }
72
+ } catch (e) {
73
+ console.error('[SoulSync] Failed to get user info:', e.message);
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');
91
+
92
+ try {
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
+ });
105
118
 
106
- if (body) {
107
- req.write(JSON.stringify(body));
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.' };
108
135
  }
109
- req.end();
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);
110
215
  });
111
216
  }
112
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);
225
+ } catch (e) {
226
+ console.error('[SoulSync] Failed to register device:', e.message);
227
+ }
228
+ }
229
+
113
230
  function startPythonService(mode = '--start') {
114
231
  const pluginDir = getPluginDir();
115
232
  const pythonScript = path.join(pluginDir, 'src', 'main.py');
@@ -117,16 +234,17 @@ function startPythonService(mode = '--start') {
117
234
 
118
235
  console.log(`[SoulSync] Starting Python service (${mode})...`);
119
236
 
237
+ if (pythonProcess) {
238
+ pythonProcess.kill();
239
+ pythonProcess = null;
240
+ }
241
+
120
242
  pythonProcess = spawn(pythonPath, [pythonScript, mode], {
121
243
  cwd: pluginDir,
122
- env: {
123
- ...process.env,
124
- OPENCLAW_PLUGIN: 'true',
125
- PLUGIN_DIR: pluginDir
126
- },
244
+ env: { ...process.env, OPENCLAW_PLUGIN: 'true', PLUGIN_DIR: pluginDir },
127
245
  stdio: 'inherit'
128
246
  });
129
-
247
+
130
248
  pythonProcess.on('close', (code) => {
131
249
  console.log(`[SoulSync] Python process exited with code ${code}`);
132
250
  pythonProcess = null;
@@ -136,8 +254,6 @@ function startPythonService(mode = '--start') {
136
254
  console.error(`[SoulSync] Failed to start Python process: ${err}`);
137
255
  pythonProcess = null;
138
256
  });
139
-
140
- return pythonProcess;
141
257
  }
142
258
 
143
259
  function stopPythonService() {
@@ -149,26 +265,18 @@ function stopPythonService() {
149
265
  }
150
266
 
151
267
  function checkConnection() {
152
- const pluginDir = getPluginDir();
153
- const cfg = loadConfig(pluginDir);
154
-
268
+ const cfg = loadConfig();
155
269
  if (!cfg || !cfg.token) {
156
- return { connected: false, reason: 'not_configured' };
270
+ return Promise.resolve({ connected: false, reason: 'not_configured' });
157
271
  }
158
272
 
159
273
  return makeRequest('GET', '/api/profiles', null, cfg.token)
160
274
  .then(result => {
161
- if (result.status === 200) {
162
- return { connected: true, data: result.body };
163
- } else if (result.status === 401) {
164
- return { connected: false, reason: 'token_expired' };
165
- } else {
166
- return { connected: false, reason: 'server_error', status: result.status };
167
- }
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 };
168
278
  })
169
- .catch(err => {
170
- return { connected: false, reason: 'connection_error', error: err.message };
171
- });
279
+ .catch(err => ({ connected: false, reason: 'connection_error', error: err.message }));
172
280
  }
173
281
 
174
282
  async function startDeviceCodeFlow() {
@@ -176,243 +284,192 @@ async function startDeviceCodeFlow() {
176
284
  const result = await makeRequest('POST', '/api/auth/device-code', {});
177
285
 
178
286
  if (result.status !== 200 || !result.body.device_code) {
179
- return {
180
- success: false,
181
- message: 'Failed to start device authorization. Please try again.'
182
- };
287
+ return { success: false, message: 'Failed to start device authorization.' };
183
288
  }
184
289
 
185
290
  const { device_code, auth_url, expires_in } = result.body;
186
291
 
187
- deviceCodePolling = setInterval(async () => {
188
- try {
189
- const pollResult = await makeRequest('GET', `/api/auth/device-code/${device_code}/status`);
190
-
191
- if (pollResult.body.status === 'authorized' && pollResult.body.token) {
292
+ const token = await new Promise((resolve, reject) => {
293
+ const timeout = setTimeout(() => {
294
+ if (deviceCodePolling) {
192
295
  clearInterval(deviceCodePolling);
193
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`);
194
304
 
195
- const deviceId = crypto.randomUUID();
196
- const pluginDir = getPluginDir();
197
- saveConfig(pluginDir, {
198
- email: 'device',
199
- token: pollResult.body.token,
200
- device_id: deviceId,
201
- device_name: 'Device',
202
- cloud_url: getCloudUrl(pluginDir)
203
- });
204
-
205
- makeRequest('POST', '/api/devices', {
206
- device_id: deviceId,
207
- device_name: 'Device',
208
- device_type: 'local'
209
- }, pollResult.body.token).catch(err => {
210
- console.error('[SoulSync] Failed to register device:', err.message);
211
- });
212
-
213
- startPythonService('--start');
214
-
215
- console.log('[SoulSync] Authorization successful! Syncing started.');
216
- } else if (pollResult.body.status === 'expired') {
217
- clearInterval(deviceCodePolling);
218
- deviceCodePolling = null;
219
- console.log('[SoulSync] Authorization expired. Please try again.');
220
- } else if (pollResult.body.status === 'not_found') {
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) {
221
322
  clearInterval(deviceCodePolling);
222
323
  deviceCodePolling = null;
223
- console.log('[SoulSync] Device code not found. Please try again.');
324
+ clearTimeout(timeout);
325
+ reject(e);
224
326
  }
225
- } catch (e) {
226
- console.error('[SoulSync] Polling error:', e);
227
- }
228
- }, 3000);
327
+ }, 3000);
328
+ });
229
329
 
230
- setTimeout(() => {
231
- if (deviceCodePolling) {
232
- clearInterval(deviceCodePolling);
233
- deviceCodePolling = null;
234
- }
235
- }, expires_in * 1000);
330
+ const deviceId = crypto.randomUUID();
331
+ const deviceName = getDeviceName('cloud');
236
332
 
237
- return {
238
- success: true,
239
- message: `Please open ${auth_url} in your browser to complete authorization. This page will expire in ${Math.floor(expires_in / 60)} minutes.`
240
- };
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 };
241
347
  } catch (err) {
242
- return {
243
- success: false,
244
- message: `Error: ${err.message}`
245
- };
348
+ return { success: false, message: `Error: ${err.message}` };
246
349
  }
247
350
  }
248
351
 
249
352
  module.exports = function register(api) {
250
353
  console.log('[SoulSync] Registering plugin...');
251
-
252
- api.registerTool({
253
- name: 'soulsync_status',
254
- description: 'Check SoulSync plugin status for OpenClaw (小龙虾). Call when user mentions SoulSync, soul sync, 灵魂同步, OpenClaw plugin, 小龙虾插件, or asks about sync status.',
255
- input_schema: {
256
- type: 'object',
257
- properties: {},
258
- required: []
259
- }
260
- }, async () => {
261
- const pluginDir = getPluginDir();
262
- const cfg = loadConfig(pluginDir);
263
-
264
- if (!cfg || !cfg.token) {
265
- return {
266
- configured: false,
267
- message: 'SoulSync is not configured. Would you like to connect your SoulSync account?'
268
- };
269
- }
270
-
271
- const conn = await checkConnection();
272
-
273
- if (conn.connected) {
274
- return {
275
- configured: true,
276
- connected: true,
277
- email: cfg.email,
278
- message: `SoulSync is connected and syncing for ${cfg.email}.`
279
- };
280
- } else if (conn.reason === 'token_expired') {
281
- return {
282
- configured: true,
283
- connected: false,
284
- message: 'SoulSync token has expired. Would you like to reconnect your account?'
285
- };
286
- } else {
287
- return {
288
- configured: true,
289
- connected: false,
290
- message: `SoulSync is configured but cannot connect to server: ${conn.reason}`
291
- };
292
- }
293
- });
294
-
295
- api.registerTool({
296
- name: 'soulsync_sync',
297
- description: 'Manually trigger SoulSync OpenClaw plugin (小龙虾插件) to sync soul files (SOUL.md, USER.md, MEMORY.md) between local and cloud.',
298
- input_schema: {
299
- type: 'object',
300
- properties: {},
301
- required: []
302
- }
303
- }, async () => {
304
- const pluginDir = getPluginDir();
305
- const cfg = loadConfig(pluginDir);
306
-
307
- if (!cfg || !cfg.token) {
308
- return {
309
- success: false,
310
- message: 'SoulSync is not configured. Would you like to connect your SoulSync account?'
311
- };
312
- }
313
-
314
- const conn = await checkConnection();
315
-
316
- if (!conn.connected) {
317
- return {
318
- success: false,
319
- message: `Cannot sync: ${conn.reason}. Please check your connection.`
320
- };
321
- }
322
-
323
- if (pythonProcess) {
324
- return {
325
- success: true,
326
- message: 'Sync is already running in the background.'
327
- };
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);
364
+ });
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;
328
375
  }
329
-
376
+ } else {
377
+ console.log('[SoulSync] Already authenticated, starting sync...');
330
378
  startPythonService('--start');
331
-
332
- return {
333
- success: true,
334
- message: 'Sync started. Your soul files are being synchronized.'
335
- };
336
- });
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
+ });
337
402
 
338
- api.registerTool({
339
- name: 'soulsync_connect',
340
- description: 'Connect SoulSync account using device authorization flow. User will be given a URL to open in browser to complete login/registration. Call when user wants to register, login, connect, or setup SoulSync.',
341
- input_schema: {
342
- type: 'object',
343
- properties: {},
344
- required: []
345
- }
346
- }, async () => {
347
- if (deviceCodePolling) {
348
- return {
349
- success: false,
350
- message: 'Authorization in progress. Please check your browser and complete the authorization.'
351
- };
352
- }
353
-
354
- return await startDeviceCodeFlow();
355
- });
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
+ });
356
415
 
357
- api.registerTool({
358
- name: 'soulsync_logout',
359
- description: 'Disconnect SoulSync account and stop sync service. This will remove local authentication token.',
360
- input_schema: {
361
- type: 'object',
362
- properties: {},
363
- required: []
364
- }
365
- }, async () => {
366
- stopPythonService();
367
-
368
- const pluginDir = getPluginDir();
369
- const configPath = path.join(pluginDir, 'config.json');
370
- const cfg = loadConfig(pluginDir);
371
-
372
- if (cfg && cfg.device_id && cfg.token) {
373
- try {
374
- await makeRequest('DELETE', `/api/devices/${cfg.device_id}`, null, cfg.token);
375
- } catch (e) {
376
- console.error('[SoulSync] Failed to delete device:', e.message);
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.';
377
424
  }
378
- }
379
-
380
- try {
381
- if (fs.existsSync(configPath)) {
382
- const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
383
- delete existing.token;
384
- delete existing.email;
385
- delete existing.device_id;
386
- delete existing.device_name;
387
- fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
425
+
426
+ if (!pythonProcess) {
427
+ startPythonService('--sync');
428
+ return 'Sync triggered.';
388
429
  }
389
- } catch (e) {
390
- console.error('[SoulSync] Error clearing config:', e);
391
- }
392
-
393
- if (deviceCodePolling) {
394
- clearInterval(deviceCodePolling);
395
- deviceCodePolling = null;
396
- }
397
-
398
- return {
399
- success: true,
400
- message: 'SoulSync disconnected. Your local data is preserved. To reconnect, say "connect SoulSync".'
401
- };
402
- });
430
+ return 'Sync already in progress.';
431
+ });
403
432
 
404
- api.registerTool({
405
- name: 'soulsync_devices',
406
- description: 'List all devices connected to your SoulSync account / 查看已连接设备列表',
407
- schema: {
408
- type: 'object',
409
- properties: {},
410
- required: []
411
- },
412
- async handler() {
413
- const pluginDir = getPluginDir();
414
- const cfg = loadConfig(pluginDir);
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();
415
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();
416
473
  if (!cfg || !cfg.token) {
417
474
  return 'Not logged in / 未登录';
418
475
  }
@@ -421,9 +478,7 @@ module.exports = function register(api) {
421
478
  const result = await makeRequest('GET', '/api/devices', null, cfg.token);
422
479
  if (result.status === 200) {
423
480
  const devices = result.body.devices || [];
424
- if (devices.length === 0) {
425
- return 'No devices found / 未找到设备';
426
- }
481
+ if (devices.length === 0) return 'No devices found / 未找到设备';
427
482
  const list = devices.map(d =>
428
483
  `- ${d.device_name || 'Unnamed'} (${d.device_type || 'local'}) - Last sync: ${d.last_sync_at || 'Never'}`
429
484
  ).join('\n');
@@ -433,23 +488,20 @@ module.exports = function register(api) {
433
488
  } catch (e) {
434
489
  return `Error: ${e.message}`;
435
490
  }
436
- }
437
- });
491
+ });
438
492
 
439
- api.registerTool({
440
- name: 'soulsync_rename_device',
441
- description: 'Rename the current device / 重命名当前设备',
442
- schema: {
443
- type: 'object',
444
- properties: {
445
- name: { type: 'string', description: 'New device name / 新设备名称' }
446
- },
447
- required: ['name']
448
- },
449
- async handler({ name }) {
450
- const pluginDir = getPluginDir();
451
- const cfg = loadConfig(pluginDir);
452
-
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();
453
505
  if (!cfg || !cfg.token || !cfg.device_id) {
454
506
  return 'Not logged in / 未登录';
455
507
  }
@@ -457,6 +509,7 @@ module.exports = function register(api) {
457
509
  try {
458
510
  const result = await makeRequest('PUT', `/api/devices/${cfg.device_id}`, { device_name: name }, cfg.token);
459
511
  if (result.status === 200) {
512
+ const pluginDir = getPluginDir();
460
513
  const configPath = path.join(pluginDir, 'config.json');
461
514
  cfg.device_name = name;
462
515
  fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
@@ -466,18 +519,16 @@ module.exports = function register(api) {
466
519
  } catch (e) {
467
520
  return `Error: ${e.message}`;
468
521
  }
469
- }
470
- });
471
-
522
+ });
523
+ }
524
+
472
525
  api.registerCli(
473
526
  ({ program }) => {
474
527
  program
475
528
  .command('soulsync:start')
476
529
  .description('启动 SoulSync 同步服务')
477
530
  .action(() => {
478
- const pluginDir = getPluginDir();
479
-
480
- if (!checkConfigExists(pluginDir)) {
531
+ if (!isAuthenticated()) {
481
532
  console.log('[SoulSync] Not configured. Starting device authorization flow...');
482
533
  startDeviceCodeFlow().then(result => {
483
534
  console.log('[SoulSync]', result.message);
@@ -509,12 +560,12 @@ module.exports = function register(api) {
509
560
  { commands: ['soulsync:stop'] }
510
561
  );
511
562
 
512
- const pluginDir = getPluginDir();
513
- if (checkConfigExists(pluginDir)) {
563
+ if (isAuthenticated()) {
514
564
  console.log('[SoulSync] Auto-starting sync service...');
515
565
  startPythonService('--start');
566
+ registerChatTools(api);
516
567
  }
517
568
 
518
569
  console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
519
570
  console.log('[SoulSync] Plugin registered successfully');
520
- };
571
+ };