clawmate 1.4.0 → 1.4.2
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 +441 -442
- package/main/ai-bridge.js +59 -59
- package/main/ai-connector.js +60 -60
- package/main/autostart.js +6 -6
- package/main/desktop-path.js +4 -4
- package/main/file-command-parser.js +46 -46
- package/main/file-ops.js +27 -27
- package/main/index.js +17 -17
- package/main/ipc-handlers.js +24 -24
- package/main/manifest.js +2 -2
- package/main/platform.js +16 -16
- package/main/smart-file-ops.js +64 -64
- package/main/store.js +1 -1
- package/main/telegram.js +137 -137
- package/main/tray.js +61 -61
- package/main/updater.js +13 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/preload/preload.js +18 -18
- package/renderer/css/effects.css +6 -6
- package/renderer/css/pet.css +8 -8
- package/renderer/css/speech.css +5 -5
- package/renderer/first-run.html +14 -14
- package/renderer/index.html +4 -4
- package/renderer/js/ai-controller.js +91 -91
- package/renderer/js/app.js +24 -24
- package/renderer/js/browser-watcher.js +32 -32
- package/renderer/js/character.js +33 -33
- package/renderer/js/interactions.js +21 -21
- package/renderer/js/memory.js +60 -60
- package/renderer/js/metrics.js +141 -141
- package/renderer/js/mode-manager.js +13 -13
- package/renderer/js/pet-engine.js +236 -236
- package/renderer/js/speech.js +19 -19
- package/renderer/js/state-machine.js +23 -23
- package/renderer/js/time-aware.js +15 -15
- package/renderer/launcher.html +8 -8
- package/shared/constants.js +11 -11
- package/shared/messages.js +130 -130
- package/shared/personalities.js +44 -44
- package/skills/launch-pet/index.js +57 -47
- package/skills/launch-pet/skill.json +12 -23
package/main/ipc-handlers.js
CHANGED
|
@@ -18,7 +18,7 @@ const memoryStore = new Store('clawmate-memory', {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
21
|
-
//
|
|
21
|
+
// Click-through control
|
|
22
22
|
ipcMain.on('set-click-through', (event, ignore) => {
|
|
23
23
|
const win = getMainWindow();
|
|
24
24
|
if (win) {
|
|
@@ -26,14 +26,14 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// File operations
|
|
30
30
|
ipcMain.handle('get-desktop-files', async () => getDesktopFiles());
|
|
31
31
|
ipcMain.handle('move-file', async (_, fileName, newPos) => moveFile(fileName, newPos));
|
|
32
32
|
ipcMain.handle('undo-file-move', async (_, moveId) => undoFileMove(moveId));
|
|
33
33
|
ipcMain.handle('undo-all-moves', async () => undoAllMoves());
|
|
34
34
|
ipcMain.handle('get-file-manifest', async () => getFileManifest());
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Mode
|
|
37
37
|
ipcMain.handle('get-mode', () => store.get('mode'));
|
|
38
38
|
ipcMain.handle('set-mode', (_, mode) => {
|
|
39
39
|
store.set('mode', mode);
|
|
@@ -42,7 +42,7 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
42
42
|
return mode;
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Config
|
|
46
46
|
ipcMain.handle('get-config', () => store.getAll());
|
|
47
47
|
ipcMain.handle('set-config', (_, key, value) => {
|
|
48
48
|
store.set(key, value);
|
|
@@ -51,20 +51,20 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
51
51
|
return true;
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// Memory
|
|
55
55
|
ipcMain.handle('get-memory', () => memoryStore.getAll());
|
|
56
56
|
ipcMain.handle('save-memory', (_, data) => {
|
|
57
57
|
Object.entries(data).forEach(([key, value]) => memoryStore.set(key, value));
|
|
58
58
|
return true;
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Screen size
|
|
62
62
|
ipcMain.handle('get-screen-size', () => {
|
|
63
63
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
|
64
64
|
return { width, height };
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
//
|
|
67
|
+
// Screen capture
|
|
68
68
|
ipcMain.handle('capture-screen', async () => {
|
|
69
69
|
try {
|
|
70
70
|
const primaryDisplay = screen.getPrimaryDisplay();
|
|
@@ -76,7 +76,7 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
if (sources.length > 0) {
|
|
79
|
-
// NativeImage
|
|
79
|
+
// Convert NativeImage to base64 JPEG (size optimized)
|
|
80
80
|
const thumbnail = sources[0].thumbnail;
|
|
81
81
|
const jpegBuffer = thumbnail.toJPEG(60);
|
|
82
82
|
return {
|
|
@@ -93,9 +93,9 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
// === AI
|
|
96
|
+
// === AI Communication ===
|
|
97
97
|
|
|
98
|
-
//
|
|
98
|
+
// Forward user events to AI Bridge (renderer -> main -> AI)
|
|
99
99
|
ipcMain.on('report-to-ai', (_, event, data) => {
|
|
100
100
|
const bridge = getAIBridge();
|
|
101
101
|
if (bridge && bridge.isConnected()) {
|
|
@@ -125,24 +125,24 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
125
125
|
bridge.reportIdleTime(data.idleSeconds);
|
|
126
126
|
break;
|
|
127
127
|
case 'browsing':
|
|
128
|
-
//
|
|
128
|
+
// Browsing context (title + cursor position + screen capture) -> AI comment generation
|
|
129
129
|
bridge.send('user_event', { event: 'browsing', ...data });
|
|
130
130
|
break;
|
|
131
131
|
default:
|
|
132
|
-
//
|
|
132
|
+
// Forward unknown events to AI as well (extensibility)
|
|
133
133
|
bridge.send('user_event', { event, ...data });
|
|
134
134
|
break;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
// AI
|
|
139
|
+
// Check AI connection status
|
|
140
140
|
ipcMain.handle('is-ai-connected', () => {
|
|
141
141
|
const bridge = getAIBridge();
|
|
142
142
|
return bridge ? bridge.isConnected() : false;
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// Metrics reporting (renderer -> main -> AI)
|
|
146
146
|
ipcMain.on('report-metrics', (_, summary) => {
|
|
147
147
|
const bridge = getAIBridge();
|
|
148
148
|
if (bridge && bridge.isConnected()) {
|
|
@@ -150,38 +150,38 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
//
|
|
153
|
+
// Get open window positions/sizes
|
|
154
154
|
ipcMain.handle('get-window-positions', async () => {
|
|
155
155
|
const { getWindowPositions } = require('./platform');
|
|
156
156
|
return await getWindowPositions();
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// Get active window title (for browser monitoring)
|
|
160
160
|
ipcMain.handle('get-active-window-title', async () => {
|
|
161
161
|
const { getActiveWindowTitle } = require('./platform');
|
|
162
162
|
return await getActiveWindowTitle();
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
//
|
|
165
|
+
// Get cursor position (screen coordinates)
|
|
166
166
|
ipcMain.handle('get-cursor-position', () => {
|
|
167
167
|
const point = screen.getCursorScreenPoint();
|
|
168
168
|
return { x: point.x, y: point.y };
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
// ===
|
|
171
|
+
// === Smart File Operation IPC ===
|
|
172
172
|
|
|
173
|
-
//
|
|
173
|
+
// Parse file command (also available from renderer)
|
|
174
174
|
ipcMain.handle('parse-file-command', (_, text) => {
|
|
175
175
|
return parseMessage(text);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
//
|
|
178
|
+
// Get filtered file list
|
|
179
179
|
ipcMain.handle('list-filtered-files', async (_, sourceDir, filter) => {
|
|
180
180
|
return listFilteredFiles(sourceDir, filter);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
183
|
+
// Execute smart file operation
|
|
184
|
+
// Used when executed directly from renderer (not via Telegram)
|
|
185
185
|
ipcMain.handle('smart-file-op', async (_, command) => {
|
|
186
186
|
const win = getMainWindow();
|
|
187
187
|
const callbacks = {
|
|
@@ -230,12 +230,12 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
230
230
|
return await executeSmartFileOp(command, callbacks);
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
-
//
|
|
233
|
+
// Undo smart move (single)
|
|
234
234
|
ipcMain.handle('undo-smart-move', async (_, moveId) => {
|
|
235
235
|
return undoSmartMove(moveId);
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
-
//
|
|
238
|
+
// Undo all smart moves
|
|
239
239
|
ipcMain.handle('undo-all-smart-moves', async () => {
|
|
240
240
|
return undoAllSmartMoves();
|
|
241
241
|
});
|
package/main/manifest.js
CHANGED
|
@@ -3,8 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const { app } = require('electron');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* File move history management (Undo support)
|
|
7
|
+
* Records all file moves and manages restoration
|
|
8
8
|
*/
|
|
9
9
|
const MANIFEST_FILE = () => path.join(app.getPath('userData'), 'file-manifest.json');
|
|
10
10
|
|
package/main/platform.js
CHANGED
|
@@ -47,9 +47,9 @@ function isLinux() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Get position/size info of open windows per OS
|
|
51
|
+
* Return format: [{ id, title, x, y, width, height }]
|
|
52
|
+
* Returns empty array on failure (safe fallback)
|
|
53
53
|
*/
|
|
54
54
|
async function getWindowPositions() {
|
|
55
55
|
if (platform === 'win32') {
|
|
@@ -62,12 +62,12 @@ async function getWindowPositions() {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* Windows:
|
|
66
|
-
*
|
|
65
|
+
* Windows: Get visible window list + position/size via PowerShell
|
|
66
|
+
* Calls Win32 API (GetWindowRect) via Add-Type
|
|
67
67
|
*/
|
|
68
68
|
async function getWindowPositionsWindows() {
|
|
69
69
|
try {
|
|
70
|
-
//
|
|
70
|
+
// Access Win32 API via inline C# compilation in PowerShell
|
|
71
71
|
const psScript = `
|
|
72
72
|
Add-Type @"
|
|
73
73
|
using System;
|
|
@@ -101,7 +101,7 @@ public class WinInfo {
|
|
|
101
101
|
[WinInfo]::GetWindows()
|
|
102
102
|
`.trim();
|
|
103
103
|
|
|
104
|
-
// PowerShell
|
|
104
|
+
// Pass PowerShell script via stdin without temp files
|
|
105
105
|
const { stdout } = await execAsync(
|
|
106
106
|
`powershell -NoProfile -Command -`,
|
|
107
107
|
{ input: psScript, timeout: 5000, encoding: 'utf-8' }
|
|
@@ -129,11 +129,11 @@ public class WinInfo {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* macOS:
|
|
132
|
+
* macOS: Get visible window positions/sizes via AppleScript
|
|
133
133
|
*/
|
|
134
134
|
async function getWindowPositionsMac() {
|
|
135
135
|
try {
|
|
136
|
-
//
|
|
136
|
+
// Collect window info of visible processes via AppleScript
|
|
137
137
|
const script = `
|
|
138
138
|
tell application "System Events"
|
|
139
139
|
set output to ""
|
|
@@ -180,8 +180,8 @@ end tell
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Linux: wmctrl -l -G
|
|
184
|
-
*
|
|
183
|
+
* Linux: Get window positions/sizes via wmctrl -l -G
|
|
184
|
+
* Returns empty array if wmctrl is not installed
|
|
185
185
|
*/
|
|
186
186
|
async function getWindowPositionsLinux() {
|
|
187
187
|
try {
|
|
@@ -193,8 +193,8 @@ async function getWindowPositionsLinux() {
|
|
|
193
193
|
const result = (stdout || '').trim();
|
|
194
194
|
if (!result) return [];
|
|
195
195
|
|
|
196
|
-
// wmctrl -l -G
|
|
197
|
-
// 0x02000003 0 0 0 1920 1080 hostname
|
|
196
|
+
// wmctrl -l -G output format:
|
|
197
|
+
// 0x02000003 0 0 0 1920 1080 hostname Desktop
|
|
198
198
|
// ID desktop x y width height hostname title
|
|
199
199
|
return result.split('\n').filter(Boolean).map((line, i) => {
|
|
200
200
|
const parts = line.trim().split(/\s+/);
|
|
@@ -203,7 +203,7 @@ async function getWindowPositionsLinux() {
|
|
|
203
203
|
const y = parseInt(parts[3], 10) || 0;
|
|
204
204
|
const width = parseInt(parts[4], 10) || 0;
|
|
205
205
|
const height = parseInt(parts[5], 10) || 0;
|
|
206
|
-
// hostname
|
|
206
|
+
// Everything after hostname is the title (may contain spaces)
|
|
207
207
|
const title = parts.slice(7).join(' ');
|
|
208
208
|
if (width < 50 || height < 50) return null;
|
|
209
209
|
return { id: `win_${i}`, title, x, y, width, height };
|
|
@@ -214,8 +214,8 @@ async function getWindowPositionsLinux() {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
217
|
+
* Return title of currently focused (foreground) window
|
|
218
|
+
* Detects browser tab titles so the pet can comment on them
|
|
219
219
|
*/
|
|
220
220
|
async function getActiveWindowTitle() {
|
|
221
221
|
try {
|
package/main/smart-file-ops.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Smart File Operation System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Moves files via Telegram or AI commands in a "pet carries them" fashion.
|
|
5
|
+
* Pet jumps to file location -> picks up -> moves to target folder -> drops off,
|
|
6
|
+
* performing animation and actual filesystem move simultaneously.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* - .exe/.dll/.sys
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
8
|
+
* Safety measures:
|
|
9
|
+
* - Excludes dangerous extensions like .exe/.dll/.sys
|
|
10
|
+
* - Excludes files over 100MB
|
|
11
|
+
* - Records all moves in manifest (undo supported)
|
|
12
|
+
* - Files already moved during interruption are recorded in manifest for restoration
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
@@ -18,46 +18,46 @@ const { getDesktopPath } = require('./desktop-path');
|
|
|
18
18
|
const manifest = require('./manifest');
|
|
19
19
|
const { AUTO_CATEGORIES } = require('./file-command-parser');
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// Safety constants
|
|
22
22
|
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
|
23
23
|
const EXCLUDED_EXTS = new Set([
|
|
24
24
|
'.exe', '.dll', '.sys', '.lnk', '.ini', '.bat', '.cmd',
|
|
25
25
|
'.ps1', '.msi', '.scr', '.com', '.pif', '.vbs', '.wsf',
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// Delay between file moves (ms) - gives time for pet animation
|
|
29
29
|
const PER_FILE_DELAY = 2500;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @param {string} filePath -
|
|
32
|
+
* Validate whether file can be moved
|
|
33
|
+
* @param {string} filePath - Full file path
|
|
34
34
|
* @returns {{ safe: boolean, reason?: string }}
|
|
35
35
|
*/
|
|
36
36
|
function validateFile(filePath) {
|
|
37
37
|
const ext = path.extname(filePath).toLowerCase();
|
|
38
38
|
if (EXCLUDED_EXTS.has(ext)) {
|
|
39
|
-
return { safe: false, reason:
|
|
39
|
+
return { safe: false, reason: `Protected file type (${ext})` };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
43
|
const stat = fs.statSync(filePath);
|
|
44
44
|
if (stat.size > MAX_FILE_SIZE) {
|
|
45
|
-
return { safe: false, reason:
|
|
45
|
+
return { safe: false, reason: `File size exceeded (${Math.round(stat.size / 1024 / 1024)}MB > 100MB)` };
|
|
46
46
|
}
|
|
47
47
|
if (!stat.isFile()) {
|
|
48
|
-
return { safe: false, reason: '
|
|
48
|
+
return { safe: false, reason: 'Not a file' };
|
|
49
49
|
}
|
|
50
50
|
} catch {
|
|
51
|
-
return { safe: false, reason: '
|
|
51
|
+
return { safe: false, reason: 'File inaccessible' };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return { safe: true };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
59
|
-
* @param {string} sourceDir -
|
|
60
|
-
* @param {string} filter -
|
|
58
|
+
* List files matching filter criteria in source directory
|
|
59
|
+
* @param {string} sourceDir - Source directory path
|
|
60
|
+
* @param {string} filter - Extension filter (e.g., ".md", "*")
|
|
61
61
|
* @returns {Array<{ name: string, path: string, ext: string, size: number }>}
|
|
62
62
|
*/
|
|
63
63
|
function listFilteredFiles(sourceDir, filter) {
|
|
@@ -72,10 +72,10 @@ function listFilteredFiles(sourceDir, filter) {
|
|
|
72
72
|
const filePath = path.join(sourceDir, entry.name);
|
|
73
73
|
const ext = path.extname(entry.name).toLowerCase();
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// Apply extension filter
|
|
76
76
|
if (filter !== '*' && ext !== filter.toLowerCase()) continue;
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Safety validation
|
|
79
79
|
const validation = validateFile(filePath);
|
|
80
80
|
if (!validation.safe) continue;
|
|
81
81
|
|
|
@@ -99,16 +99,16 @@ function listFilteredFiles(sourceDir, filter) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
103
|
-
* @param {string} sourceDir -
|
|
104
|
-
* @returns {Map<string, Array>}
|
|
102
|
+
* Auto-categorize mode: sort files into folders by extension
|
|
103
|
+
* @param {string} sourceDir - Source directory
|
|
104
|
+
* @returns {Map<string, Array>} Category name -> file list
|
|
105
105
|
*/
|
|
106
106
|
function categorizeFiles(sourceDir) {
|
|
107
107
|
const files = listFilteredFiles(sourceDir, '*');
|
|
108
108
|
const categories = new Map();
|
|
109
109
|
|
|
110
110
|
for (const file of files) {
|
|
111
|
-
const category = AUTO_CATEGORIES[file.ext] || '
|
|
111
|
+
const category = AUTO_CATEGORIES[file.ext] || 'Other';
|
|
112
112
|
if (!categories.has(category)) {
|
|
113
113
|
categories.set(category, []);
|
|
114
114
|
}
|
|
@@ -119,10 +119,10 @@ function categorizeFiles(sourceDir) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
*
|
|
123
|
-
* @param {string} sourceDir -
|
|
124
|
-
* @param {string} targetName -
|
|
125
|
-
* @returns {string}
|
|
122
|
+
* Create target folder (if it doesn't exist)
|
|
123
|
+
* @param {string} sourceDir - Source directory (parent of target folder)
|
|
124
|
+
* @param {string} targetName - Target folder name
|
|
125
|
+
* @returns {string} Full path of target folder
|
|
126
126
|
*/
|
|
127
127
|
function ensureTargetDir(sourceDir, targetName) {
|
|
128
128
|
const targetDir = path.join(sourceDir, targetName);
|
|
@@ -133,16 +133,16 @@ function ensureTargetDir(sourceDir, targetName) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
137
|
-
* @param {string} filePath -
|
|
138
|
-
* @param {string} targetDir -
|
|
136
|
+
* Execute single file move + record in manifest
|
|
137
|
+
* @param {string} filePath - Original file path
|
|
138
|
+
* @param {string} targetDir - Target directory
|
|
139
139
|
* @returns {{ success: boolean, newPath?: string, error?: string, moveId?: string }}
|
|
140
140
|
*/
|
|
141
141
|
function moveFileToTarget(filePath, targetDir) {
|
|
142
142
|
const fileName = path.basename(filePath);
|
|
143
143
|
let newPath = path.join(targetDir, fileName);
|
|
144
144
|
|
|
145
|
-
//
|
|
145
|
+
// Number files if same name exists
|
|
146
146
|
if (fs.existsSync(newPath)) {
|
|
147
147
|
const ext = path.extname(fileName);
|
|
148
148
|
const base = path.basename(fileName, ext);
|
|
@@ -156,7 +156,7 @@ function moveFileToTarget(filePath, targetDir) {
|
|
|
156
156
|
try {
|
|
157
157
|
fs.renameSync(filePath, newPath);
|
|
158
158
|
|
|
159
|
-
// manifest
|
|
159
|
+
// Record in manifest (undo support)
|
|
160
160
|
const entry = manifest.addEntry({
|
|
161
161
|
fileName,
|
|
162
162
|
originalPath: filePath,
|
|
@@ -172,39 +172,39 @@ function moveFileToTarget(filePath, targetDir) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
176
|
-
* @param {string} moveId -
|
|
175
|
+
* Undo smart file move (single)
|
|
176
|
+
* @param {string} moveId - Manifest entry ID
|
|
177
177
|
* @returns {{ success: boolean, error?: string }}
|
|
178
178
|
*/
|
|
179
179
|
function undoSmartMove(moveId) {
|
|
180
180
|
const entries = manifest.getAll();
|
|
181
181
|
const entry = entries.find(e => e.id === moveId && e.action === 'smart_move');
|
|
182
182
|
if (!entry) {
|
|
183
|
-
return { success: false, error: '
|
|
183
|
+
return { success: false, error: 'Move record not found' };
|
|
184
184
|
}
|
|
185
185
|
if (entry.restored) {
|
|
186
|
-
return { success: false, error: '
|
|
186
|
+
return { success: false, error: 'Already restored' };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
try {
|
|
190
|
-
//
|
|
190
|
+
// Restore from new location to original location
|
|
191
191
|
if (fs.existsSync(entry.newPath)) {
|
|
192
|
-
//
|
|
192
|
+
// Prevent conflict if same name file exists at original location
|
|
193
193
|
if (fs.existsSync(entry.originalPath)) {
|
|
194
|
-
return { success: false, error: '
|
|
194
|
+
return { success: false, error: 'File with same name exists at original location' };
|
|
195
195
|
}
|
|
196
196
|
fs.renameSync(entry.newPath, entry.originalPath);
|
|
197
197
|
manifest.markRestored(moveId);
|
|
198
198
|
return { success: true };
|
|
199
199
|
}
|
|
200
|
-
return { success: false, error: '
|
|
200
|
+
return { success: false, error: 'Moved file not found' };
|
|
201
201
|
} catch (err) {
|
|
202
202
|
return { success: false, error: err.message };
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
*
|
|
207
|
+
* Undo all smart file moves
|
|
208
208
|
* @returns {{ success: boolean, restoredCount: number, errors: string[] }}
|
|
209
209
|
*/
|
|
210
210
|
function undoAllSmartMoves() {
|
|
@@ -213,7 +213,7 @@ function undoAllSmartMoves() {
|
|
|
213
213
|
let restoredCount = 0;
|
|
214
214
|
const errors = [];
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Restore in reverse order starting from most recent
|
|
217
217
|
for (const entry of smartMoves.reverse()) {
|
|
218
218
|
const result = undoSmartMove(entry.id);
|
|
219
219
|
if (result.success) {
|
|
@@ -227,33 +227,33 @@ function undoAllSmartMoves() {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
*
|
|
230
|
+
* Execute smart file operation (full flow)
|
|
231
231
|
*
|
|
232
|
-
*
|
|
232
|
+
* Sequentially moves files while controlling pet animation via callbacks.
|
|
233
233
|
*
|
|
234
|
-
* @param {object} command -
|
|
235
|
-
* - source:
|
|
236
|
-
* - filter:
|
|
237
|
-
* - target:
|
|
238
|
-
* - autoCategory:
|
|
239
|
-
* @param {object} callbacks -
|
|
240
|
-
* - onStart(totalFiles):
|
|
241
|
-
* - onPickUp(fileName, index):
|
|
242
|
-
* - onDrop(fileName, targetName, index):
|
|
243
|
-
* - onComplete(result):
|
|
244
|
-
* - onError(error):
|
|
234
|
+
* @param {object} command - Parsed file command
|
|
235
|
+
* - source: Source directory path
|
|
236
|
+
* - filter: Extension filter (e.g., ".md", "*")
|
|
237
|
+
* - target: Target folder name or "auto"
|
|
238
|
+
* - autoCategory: Whether to auto-categorize
|
|
239
|
+
* @param {object} callbacks - Pet animation callbacks
|
|
240
|
+
* - onStart(totalFiles): When operation starts
|
|
241
|
+
* - onPickUp(fileName, index): When picking up file
|
|
242
|
+
* - onDrop(fileName, targetName, index): When dropping file
|
|
243
|
+
* - onComplete(result): When operation completes
|
|
244
|
+
* - onError(error): When error occurs
|
|
245
245
|
* @returns {Promise<{ success: boolean, movedCount: number, errors: string[], moveIds: string[] }>}
|
|
246
246
|
*/
|
|
247
247
|
async function executeSmartFileOp(command, callbacks = {}) {
|
|
248
248
|
const { source, filter, target, autoCategory } = command;
|
|
249
249
|
|
|
250
250
|
try {
|
|
251
|
-
//
|
|
251
|
+
// Auto-categorize mode
|
|
252
252
|
if (autoCategory) {
|
|
253
253
|
return await _executeAutoCategory(source, callbacks);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
//
|
|
256
|
+
// Move to specific target folder
|
|
257
257
|
return await _executeTargetMove(source, filter, target, callbacks);
|
|
258
258
|
} catch (err) {
|
|
259
259
|
if (callbacks.onError) callbacks.onError(err.message);
|
|
@@ -262,7 +262,7 @@ async function executeSmartFileOp(command, callbacks = {}) {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
|
-
*
|
|
265
|
+
* Execute auto-categorization
|
|
266
266
|
*/
|
|
267
267
|
async function _executeAutoCategory(sourceDir, callbacks) {
|
|
268
268
|
const categories = categorizeFiles(sourceDir);
|
|
@@ -286,8 +286,8 @@ async function _executeAutoCategory(sourceDir, callbacks) {
|
|
|
286
286
|
let fileIndex = 0;
|
|
287
287
|
|
|
288
288
|
for (const [category, files] of categories) {
|
|
289
|
-
// "
|
|
290
|
-
if (category === '
|
|
289
|
+
// Skip "Other" category if few files
|
|
290
|
+
if (category === 'Other' && files.length <= 2) continue;
|
|
291
291
|
|
|
292
292
|
const targetDir = ensureTargetDir(sourceDir, category);
|
|
293
293
|
|
|
@@ -315,7 +315,7 @@ async function _executeAutoCategory(sourceDir, callbacks) {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
/**
|
|
318
|
-
*
|
|
318
|
+
* Execute move to specific target folder
|
|
319
319
|
*/
|
|
320
320
|
async function _executeTargetMove(sourceDir, filter, targetName, callbacks) {
|
|
321
321
|
const files = listFilteredFiles(sourceDir, filter);
|