@mmmbuto/nexuscrew 0.2.1 → 0.2.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.
Files changed (52) hide show
  1. package/README.md +122 -67
  2. package/bin/nexuscrew.js +194 -470
  3. package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +3 -11
  6. package/lib/server/routes/hosts.js +22 -12
  7. package/lib/server/routes/launchers.js +12 -0
  8. package/lib/server/routes/models.js +16 -63
  9. package/lib/server/routes/runtime.js +10 -0
  10. package/lib/server/routes/send.js +97 -258
  11. package/lib/server/routes/sessions.js +13 -42
  12. package/lib/server/routes/status.js +18 -22
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +60 -81
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/log-watcher.js +22 -40
  17. package/lib/services/session-store.js +88 -62
  18. package/lib/services/tmux-manager.js +111 -173
  19. package/package.json +5 -9
  20. package/frontend/dist/apple-touch-icon.png +0 -0
  21. package/frontend/dist/assets/index-CiAtinNP.css +0 -1
  22. package/frontend/dist/favicon.svg +0 -20
  23. package/frontend/dist/icon-192.png +0 -0
  24. package/frontend/dist/icon-512.png +0 -0
  25. package/frontend/dist/site.webmanifest +0 -29
  26. package/lib/config/hosts.js +0 -67
  27. package/lib/config/manager.js +0 -379
  28. package/lib/config/models.js +0 -408
  29. package/lib/server/db/adapter.js +0 -274
  30. package/lib/server/db/drivers/sql-js.js +0 -75
  31. package/lib/server/db/migrate.js +0 -174
  32. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  33. package/lib/server/middleware/auth.js +0 -134
  34. package/lib/server/middleware/rate-limit.js +0 -63
  35. package/lib/server/models/User.js +0 -128
  36. package/lib/server/routes/auth.js +0 -168
  37. package/lib/server/routes/keys.js +0 -28
  38. package/lib/server/routes/runtimes.js +0 -34
  39. package/lib/server/routes/speech.js +0 -46
  40. package/lib/server/routes/upload.js +0 -135
  41. package/lib/server/routes/wake-lock.js +0 -95
  42. package/lib/server/routes/workspaces.js +0 -101
  43. package/lib/server/services/attachment-manager.js +0 -57
  44. package/lib/server/services/context-bridge.js +0 -425
  45. package/lib/server/services/runtime-manager.js +0 -462
  46. package/lib/server/services/speech-manager.js +0 -76
  47. package/lib/server/services/summary-generator.js +0 -309
  48. package/lib/server/services/workspace-manager.js +0 -79
  49. package/lib/services/remote-pane-watcher.js +0 -155
  50. package/lib/setup/postinstall.js +0 -38
  51. package/lib/utils/paths.js +0 -124
  52. package/lib/utils/termux.js +0 -182
@@ -1,168 +0,0 @@
1
- const express = require('express');
2
- const rateLimit = require('express-rate-limit');
3
- const bcrypt = require('bcryptjs');
4
- const User = require('../models/User');
5
- const { generateToken, authMiddleware } = require('../middleware/auth');
6
- const { getConfig } = require('../../config/manager');
7
-
8
- const router = express.Router();
9
-
10
- /**
11
- * Check config user (admin from init)
12
- * Returns user object compatible with DB user structure
13
- */
14
- function findConfigUser(username) {
15
- const config = getConfig();
16
- if (config.auth && config.auth.user === username && config.auth.pass_hash) {
17
- return {
18
- id: 'config-admin',
19
- username: config.auth.user,
20
- password_hash: config.auth.pass_hash,
21
- role: 'admin',
22
- is_locked: false,
23
- failed_attempts: 0,
24
- created_at: Date.now()
25
- };
26
- }
27
- return null;
28
- }
29
-
30
- /**
31
- * Verify password for config user
32
- */
33
- function verifyConfigPassword(passHash, password) {
34
- return bcrypt.compareSync(password, passHash);
35
- }
36
-
37
- // Rate limiter: max 5 login attempts per 15 minutes per IP
38
- const loginLimiter = rateLimit({
39
- windowMs: 15 * 60 * 1000, // 15 minutes
40
- max: 10,
41
- message: {
42
- error: 'Too many login attempts, please try again later',
43
- retry_after: 15 * 60
44
- },
45
- standardHeaders: true,
46
- legacyHeaders: false
47
- });
48
-
49
- // POST /api/v1/auth/login
50
- router.post('/login', loginLimiter, async (req, res) => {
51
- try {
52
- const { username, password } = req.body;
53
- const ipAddress = req.ip || req.connection.remoteAddress;
54
-
55
- if (!username || !password) {
56
- return res.status(400).json({ error: 'Username and password required' });
57
- }
58
-
59
- // Check IP rate limiting (additional layer)
60
- const recentAttempts = User.getRecentLoginAttempts(ipAddress);
61
- if (recentAttempts > 20) {
62
- return res.status(429).json({
63
- error: 'Too many failed attempts from this IP',
64
- retry_after: 15 * 60
65
- });
66
- }
67
-
68
- // First check config user (admin from init)
69
- let user = findConfigUser(username);
70
- let isConfigUser = !!user;
71
-
72
- // If not config user, check database
73
- if (!user) {
74
- user = User.findByUsername(username);
75
- }
76
-
77
- if (!user) {
78
- // Log failed attempt even for non-existent user
79
- User.logLoginAttempt(ipAddress, username, false);
80
- return res.status(401).json({ error: 'Invalid credentials' });
81
- }
82
-
83
- // Check if account is locked (only for DB users)
84
- if (!isConfigUser && User.isAccountLocked(user)) {
85
- User.logLoginAttempt(ipAddress, username, false);
86
- const remainingMs = user.locked_until - Date.now();
87
- return res.status(403).json({
88
- error: 'Account locked due to failed login attempts',
89
- locked_until: user.locked_until,
90
- retry_after: Math.ceil(remainingMs / 1000)
91
- });
92
- }
93
-
94
- // Verify password
95
- let isValid;
96
- if (isConfigUser) {
97
- isValid = verifyConfigPassword(user.password_hash, password);
98
- } else {
99
- isValid = User.verifyPassword(user, password);
100
- }
101
-
102
- if (!isValid) {
103
- User.logLoginAttempt(ipAddress, username, false);
104
- if (!isConfigUser) {
105
- User.incrementFailedAttempts(user.id);
106
- }
107
- return res.status(401).json({ error: 'Invalid credentials' });
108
- }
109
-
110
- // Success
111
- User.logLoginAttempt(ipAddress, username, true);
112
- if (!isConfigUser) {
113
- User.resetFailedAttempts(user.id);
114
- User.updateLastLogin(user.id);
115
- }
116
-
117
- const token = generateToken(user);
118
-
119
- res.json({
120
- token,
121
- user: {
122
- id: user.id,
123
- username: user.username,
124
- role: user.role
125
- }
126
- });
127
- } catch (error) {
128
- console.error('Login error:', error);
129
- res.status(500).json({ error: 'Internal server error' });
130
- }
131
- });
132
-
133
- // GET /api/v1/auth/me
134
- router.get('/me', authMiddleware, (req, res) => {
135
- // Config user is already validated by authMiddleware
136
- // Just return the user info from req.user
137
- if (req.user.id === 'config-admin') {
138
- const config = getConfig();
139
- return res.json({
140
- id: req.user.id,
141
- username: req.user.username,
142
- role: req.user.role,
143
- created_at: Date.now(),
144
- last_login: null
145
- });
146
- }
147
-
148
- const user = User.findById(req.user.id);
149
- if (!user) {
150
- return res.status(404).json({ error: 'User not found' });
151
- }
152
-
153
- res.json({
154
- id: user.id,
155
- username: user.username,
156
- role: user.role,
157
- created_at: user.created_at,
158
- last_login: user.last_login
159
- });
160
- });
161
-
162
- // POST /api/v1/auth/logout
163
- router.post('/logout', authMiddleware, (req, res) => {
164
- // With JWT, logout is handled client-side by removing token
165
- res.json({ message: 'Logged out successfully' });
166
- });
167
-
168
- module.exports = router;
@@ -1,28 +0,0 @@
1
- const express = require('express');
2
- const router = express.Router();
3
- const { getApiKey } = require('../db/adapter');
4
-
5
- /**
6
- * GET /api/v1/keys/check/:provider
7
- * Check if an API key is configured for a provider
8
- * Returns: { configured: boolean }
9
- */
10
- router.get('/check/:provider', (req, res) => {
11
- if (req.params.provider === 'stt') {
12
- const speech = req.speech || req.app?.locals?.speech;
13
- if (!speech) {
14
- return res.json({ configured: false, provider: 'unknown' });
15
- }
16
- const status = speech.getStatus();
17
- return res.json({
18
- configured: Boolean(status.whisper?.configured && status.whisper?.available),
19
- provider: status.provider,
20
- fallback: status.fallback
21
- });
22
- }
23
-
24
- const key = getApiKey(req.params.provider);
25
- res.json({ configured: !!key });
26
- });
27
-
28
- module.exports = router;
@@ -1,34 +0,0 @@
1
- const express = require('express');
2
- const RuntimeManager = require('../services/runtime-manager');
3
-
4
- const router = express.Router();
5
- const runtimeManager = new RuntimeManager();
6
-
7
- router.get('/', async (_req, res) => {
8
- try {
9
- const inventory = await runtimeManager.getRuntimeInventory();
10
- res.json({
11
- platform: runtimeManager.platformId,
12
- runtimes: inventory,
13
- });
14
- } catch (error) {
15
- console.error('[Runtimes] Inventory error:', error);
16
- res.status(500).json({ error: 'Failed to fetch runtime inventory' });
17
- }
18
- });
19
-
20
- router.post('/check', async (_req, res) => {
21
- try {
22
- const inventory = await runtimeManager.getRuntimeInventory();
23
- res.json({
24
- platform: runtimeManager.platformId,
25
- runtimes: inventory,
26
- checkedAt: new Date().toISOString(),
27
- });
28
- } catch (error) {
29
- console.error('[Runtimes] Check error:', error);
30
- res.status(500).json({ error: 'Failed to check runtimes' });
31
- }
32
- });
33
-
34
- module.exports = router;
@@ -1,46 +0,0 @@
1
- const express = require('express');
2
- const router = express.Router();
3
- const multer = require('multer');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const { PATHS } = require('../../utils/paths');
8
-
9
- const upload = multer({
10
- storage: multer.memoryStorage(),
11
- limits: { fileSize: 25 * 1024 * 1024 }
12
- });
13
-
14
- router.get('/status', (req, res) => {
15
- res.json(req.speech.getStatus());
16
- });
17
-
18
- router.post('/transcribe', upload.single('audio'), async (req, res) => {
19
- let tempPath = null;
20
-
21
- try {
22
- if (!req.file) {
23
- return res.status(400).json({ error: 'No audio file provided' });
24
- }
25
-
26
- fs.mkdirSync(PATHS.DATA_DIR, { recursive: true });
27
- tempPath = path.join(PATHS.DATA_DIR, `stt-${Date.now()}-${Math.random().toString(16).slice(2)}.webm`);
28
- fs.writeFileSync(tempPath, req.file.buffer);
29
-
30
- const result = await req.speech.transcribe({
31
- audioPath: tempPath,
32
- language: req.body.language
33
- });
34
-
35
- res.json(result);
36
- } catch (error) {
37
- console.error('[Speech] Transcription error:', error);
38
- res.status(500).json({ error: error.message });
39
- } finally {
40
- if (tempPath && fs.existsSync(tempPath)) {
41
- fs.unlinkSync(tempPath);
42
- }
43
- }
44
- });
45
-
46
- module.exports = router;
@@ -1,135 +0,0 @@
1
- const express = require('express');
2
- const multer = require('multer');
3
- const path = require('path');
4
- const fs = require('fs');
5
- const { PATHS } = require('../../utils/paths');
6
-
7
- const router = express.Router();
8
-
9
- // NexusCrew: attachments in ~/.nexuscrew/attachments
10
- const ATTACHMENTS_DIR = path.join(PATHS.ATTACHMENTS_DIR, 'incoming');
11
-
12
- // Ensure directory exists
13
- if (!fs.existsSync(ATTACHMENTS_DIR)) {
14
- fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true });
15
- console.log(`[Upload] Created attachments directory: ${ATTACHMENTS_DIR}`);
16
- }
17
-
18
- // Multer storage config
19
- const storage = multer.diskStorage({
20
- destination: (req, file, cb) => {
21
- cb(null, ATTACHMENTS_DIR);
22
- },
23
- filename: (req, file, cb) => {
24
- // Unique filename: timestamp_originalname
25
- const timestamp = Date.now();
26
- const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
27
- cb(null, `${timestamp}_${safeName}`);
28
- }
29
- });
30
-
31
- // File filter - allow images and documents
32
- const fileFilter = (req, file, cb) => {
33
- const allowedMimes = [
34
- // Images
35
- 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
36
- // Documents
37
- 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
38
- 'application/json', 'application/xml', 'application/pdf',
39
- 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
40
- 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
41
- // Code files (often sent as octet-stream)
42
- 'application/octet-stream', 'text/x-python', 'text/javascript', 'application/javascript'
43
- ];
44
-
45
- // Also allow by extension for code files
46
- const allowedExts = ['.js', '.ts', '.py', '.java', '.c', '.cpp', '.h', '.rb', '.php', '.go', '.rs', '.sh', '.bash', '.zsh', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sql', '.log'];
47
- const ext = path.extname(file.originalname).toLowerCase();
48
-
49
- if (allowedMimes.includes(file.mimetype) || allowedExts.includes(ext)) {
50
- cb(null, true);
51
- } else {
52
- cb(new Error(`File type not allowed: ${file.mimetype} (${ext})`), false);
53
- }
54
- };
55
-
56
- // Multer config - 50MB limit
57
- const upload = multer({
58
- storage,
59
- fileFilter,
60
- limits: { fileSize: 50 * 1024 * 1024 }
61
- });
62
-
63
- // POST /api/v1/upload - Single file upload
64
- router.post('/', upload.single('file'), (req, res) => {
65
- try {
66
- if (!req.file) {
67
- return res.status(400).json({ error: 'No file uploaded' });
68
- }
69
-
70
- const filePath = req.file.path;
71
- const fileName = req.file.filename;
72
- const originalName = req.file.originalname;
73
- const mimeType = req.file.mimetype;
74
- const size = req.file.size;
75
-
76
- console.log(`[Upload] File saved: ${filePath} (${size} bytes, ${mimeType})`);
77
-
78
- res.json({
79
- success: true,
80
- file: {
81
- path: filePath,
82
- name: fileName,
83
- originalName,
84
- mimeType,
85
- size
86
- }
87
- });
88
- } catch (error) {
89
- console.error('[Upload] Error:', error);
90
- res.status(500).json({ error: error.message });
91
- }
92
- });
93
-
94
- // POST /api/v1/upload/multiple - Multiple files upload
95
- router.post('/multiple', upload.array('files', 10), (req, res) => {
96
- try {
97
- if (!req.files || req.files.length === 0) {
98
- return res.status(400).json({ error: 'No files uploaded' });
99
- }
100
-
101
- const files = req.files.map(f => ({
102
- path: f.path,
103
- name: f.filename,
104
- originalName: f.originalname,
105
- mimeType: f.mimetype,
106
- size: f.size
107
- }));
108
-
109
- console.log(`[Upload] ${files.length} files saved`);
110
-
111
- res.json({
112
- success: true,
113
- files
114
- });
115
- } catch (error) {
116
- console.error('[Upload] Error:', error);
117
- res.status(500).json({ error: error.message });
118
- }
119
- });
120
-
121
- // Error handler for multer
122
- router.use((error, req, res, next) => {
123
- if (error instanceof multer.MulterError) {
124
- if (error.code === 'LIMIT_FILE_SIZE') {
125
- return res.status(400).json({ error: 'File too large (max 50MB)' });
126
- }
127
- return res.status(400).json({ error: error.message });
128
- }
129
- if (error) {
130
- return res.status(400).json({ error: error.message });
131
- }
132
- next();
133
- });
134
-
135
- module.exports = router;
@@ -1,95 +0,0 @@
1
- const express = require('express');
2
- const { execSync } = require('child_process');
3
- const router = express.Router();
4
-
5
- // State to track wake lock status
6
- let wakeLockAcquired = false;
7
-
8
- /**
9
- * POST /api/v1/wake-lock
10
- * Acquire wake lock (prevent Android from killing process)
11
- * On non-Termux systems, this is a no-op but returns success to avoid errors
12
- */
13
- router.post('/wake-lock', (req, res) => {
14
- try {
15
- console.log('[WakeLock] Acquiring wake lock...');
16
-
17
- const isTermux = process.env.PREFIX?.includes('com.termux');
18
-
19
- if (isTermux) {
20
- // Only execute on Termux/Android
21
- try {
22
- execSync('termux-wake-lock', { stdio: 'ignore' });
23
- } catch (e) {
24
- console.warn('[WakeLock] termux-wake-lock command failed (may not be available)');
25
- }
26
- } else {
27
- console.log('[WakeLock] Not on Termux - wake-lock is a no-op');
28
- }
29
-
30
- wakeLockAcquired = true;
31
-
32
- console.log('[WakeLock] ✅ Wake lock acquired (or skipped on non-Termux)');
33
-
34
- res.json({
35
- status: 'ok',
36
- message: 'Wake lock acquired',
37
- acquired: wakeLockAcquired,
38
- platform: isTermux ? 'termux' : 'linux'
39
- });
40
- } catch (err) {
41
- console.error('[WakeLock] ❌ Unexpected error:', err.message);
42
-
43
- // Still return success to avoid breaking the app
44
- res.status(200).json({
45
- status: 'ok',
46
- message: 'Wake lock handler executed',
47
- acquired: true,
48
- error: err.message // Log the error but don't fail the request
49
- });
50
- }
51
- });
52
-
53
- /**
54
- * DELETE /api/v1/wake-lock
55
- * Release wake lock
56
- */
57
- router.delete('/wake-lock', (req, res) => {
58
- try {
59
- console.log('[WakeLock] Releasing wake lock...');
60
-
61
- // Execute termux-wake-unlock
62
- execSync('termux-wake-unlock', { stdio: 'ignore' });
63
-
64
- wakeLockAcquired = false;
65
-
66
- console.log('[WakeLock] ✅ Wake lock released');
67
-
68
- res.json({
69
- status: 'ok',
70
- message: 'Wake lock released',
71
- acquired: wakeLockAcquired
72
- });
73
- } catch (err) {
74
- console.error('[WakeLock] ❌ Failed to release wake lock:', err.message);
75
-
76
- res.status(500).json({
77
- status: 'error',
78
- message: 'Failed to release wake lock',
79
- error: err.message
80
- });
81
- }
82
- });
83
-
84
- /**
85
- * GET /api/v1/wake-lock
86
- * Get wake lock status
87
- */
88
- router.get('/wake-lock', (req, res) => {
89
- res.json({
90
- status: 'ok',
91
- acquired: wakeLockAcquired
92
- });
93
- });
94
-
95
- module.exports = router;
@@ -1,101 +0,0 @@
1
- /**
2
- * Workspace Routes - Express router for workspace management
3
- *
4
- * Endpoints:
5
- * - GET /api/v1/workspaces - List discovered workspaces
6
- * - POST /api/v1/workspaces/mount - Validate and set active workspace
7
- */
8
-
9
- const express = require('express');
10
- const router = express.Router();
11
-
12
- /**
13
- * GET /api/v1/workspaces
14
- * List all discovered workspaces from conversations table
15
- */
16
- router.get('/', (req, res) => {
17
- try {
18
- const WorkspaceManager = require('../services/workspace-manager');
19
- const wm = new WorkspaceManager();
20
- const workspaces = wm.discoverWorkspaces();
21
-
22
- res.json({
23
- workspaces,
24
- count: workspaces.length
25
- });
26
- } catch (error) {
27
- console.error('[Workspaces] Error listing workspaces:', error);
28
- res.status(500).json({ error: 'Failed to list workspaces' });
29
- }
30
- });
31
-
32
- /**
33
- * POST /api/v1/workspaces/mount
34
- * Validate workspace path and optionally return workspace details
35
- * Body: { path: string }
36
- */
37
- router.post('/mount', (req, res) => {
38
- try {
39
- const { path: wsPath } = req.body;
40
-
41
- if (!wsPath) {
42
- return res.status(400).json({ error: 'Workspace path is required' });
43
- }
44
-
45
- const WorkspaceManager = require('../services/workspace-manager');
46
- const wm = new WorkspaceManager();
47
-
48
- // Validate workspace exists
49
- const valid = wm.validateWorkspace(wsPath);
50
-
51
- if (!valid) {
52
- return res.status(400).json({ error: 'Invalid workspace path' });
53
- }
54
-
55
- // Get workspace details if it exists in conversations
56
- const stats = wm.getWorkspaceStats(wsPath);
57
- const conversations = wm.getWorkspaceConversations(wsPath);
58
-
59
- res.json({
60
- workspace: wsPath,
61
- mounted: true,
62
- stats,
63
- conversations: conversations.slice(0, 10) // Return first 10
64
- });
65
- } catch (error) {
66
- console.error('[Workspaces] Error mounting workspace:', error);
67
- res.status(500).json({ error: 'Failed to mount workspace' });
68
- }
69
- });
70
-
71
- /**
72
- * GET /api/v1/workspaces/:path/stats
73
- * Get statistics for a specific workspace
74
- */
75
- router.get('/:path/stats', (req, res) => {
76
- try {
77
- const { path: wsPath } = req.params;
78
-
79
- const WorkspaceManager = require('../services/workspace-manager');
80
- const wm = new WorkspaceManager();
81
-
82
- // Validate workspace exists
83
- const valid = wm.validateWorkspace(wsPath);
84
-
85
- if (!valid) {
86
- return res.status(404).json({ error: 'Workspace not found' });
87
- }
88
-
89
- const stats = wm.getWorkspaceStats(wsPath);
90
-
91
- res.json({
92
- workspace: wsPath,
93
- stats
94
- });
95
- } catch (error) {
96
- console.error('[Workspaces] Error getting workspace stats:', error);
97
- res.status(500).json({ error: 'Failed to get workspace stats' });
98
- }
99
- });
100
-
101
- module.exports = router;
@@ -1,57 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { PATHS } = require('../../utils/paths');
4
-
5
- class AttachmentManager {
6
- constructor(tmux) {
7
- this.tmux = tmux;
8
- }
9
-
10
- getLocalAttachmentDir(conversationId = 'shared') {
11
- return path.join(PATHS.ATTACHMENTS_DIR, 'local', conversationId);
12
- }
13
-
14
- getRemoteAttachmentDir(conversationId = 'shared') {
15
- return path.posix.join('.nexuscrew', 'attachments', conversationId);
16
- }
17
-
18
- stageForConversation(file, conversationId, hostRef) {
19
- if (!file?.path) {
20
- throw new Error('Attachment path is required');
21
- }
22
-
23
- const host = this.tmux.resolveHost(hostRef);
24
- const baseName = path.basename(file.path);
25
-
26
- if (!host || host.type === 'local') {
27
- const localDir = this.getLocalAttachmentDir(conversationId);
28
- fs.mkdirSync(localDir, { recursive: true });
29
- const localPath = path.join(localDir, baseName);
30
- if (file.path !== localPath) {
31
- fs.copyFileSync(file.path, localPath);
32
- }
33
- return {
34
- ...file,
35
- host: 'local',
36
- localPath,
37
- promptPath: localPath,
38
- ready: true
39
- };
40
- }
41
-
42
- const remoteDir = this.getRemoteAttachmentDir(conversationId);
43
- const remotePath = path.posix.join(remoteDir, baseName);
44
- this.tmux.copyToHost(file.path, remotePath, host);
45
-
46
- return {
47
- ...file,
48
- host: host.name,
49
- localPath: file.path,
50
- remotePath,
51
- promptPath: `~/${remotePath}`,
52
- ready: true
53
- };
54
- }
55
- }
56
-
57
- module.exports = AttachmentManager;