@goodandready/dsh-remote-workspace 0.3.5 → 0.3.7
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/docs/design/DESIGN.md +6 -0
- package/lib/mirror-sync-service.js +11 -6
- package/lib/remote-fs-service.js +4 -0
- package/lib/routes/docker.js +2 -0
- package/lib/routes/files.js +17 -4
- package/lib/routes/helpers.js +46 -4
- package/lib/routes/profiles.js +12 -2
- package/lib/routes/terminal.js +3 -2
- package/lib/routes/tunnels.js +1 -0
- package/lib/ssh-service.js +3 -0
- package/lib/tar-sync-service.js +12 -3
- package/lib/tools.js +21 -19
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -83,3 +83,9 @@ emote_tunnel: создание/остановка/листинг туннеле
|
|
|
83
83
|
- `src/client/plugin-card.js` — Plugin card and status chip components
|
|
84
84
|
- `src/client/entry.js` — Cordis client plugin apply and slot registration
|
|
85
85
|
- Added native builder `scripts/build-client.mjs` hooked into `prepack` and `npm test`.
|
|
86
|
+
|
|
87
|
+
### v0.3.6 Model Tools JSON Schema Standardization (Refs: #33)
|
|
88
|
+
- Все 9 инструментов (`remote_exec`, `remote_fs`, `remote_sync`, `remote_tunnel`, `remote_docker`, `remote_service`, `remote_transfer`, `remote_diagnose`, `remote_env`) переведены на компиляцию через `defineTool()` из `@deepseek-ai/dsh-tools`.
|
|
89
|
+
- Параметры инструментов компилируются в строгий JSON Schema с корнем `{ type: 'object', properties: ..., required: [...] }`.
|
|
90
|
+
- Предотвращены ошибки валидации схем в OpenAI-совместимых провайдерах (`Invalid schema: schema must be a JSON Schema of 'type: "object"', got 'type: null'`).
|
|
91
|
+
- Схема вывода стандартизирована с явным `{ type: 'object', additionalProperties: true }`.
|
|
@@ -11,6 +11,7 @@ export class MirrorSyncService {
|
|
|
11
11
|
this.remoteFs = remoteFs;
|
|
12
12
|
this.sshService = sshService;
|
|
13
13
|
this.defaultIgnore = [
|
|
14
|
+
'.dsh-sync-snapshot.json',
|
|
14
15
|
'.git',
|
|
15
16
|
'node_modules',
|
|
16
17
|
'.worktrees',
|
|
@@ -96,7 +97,7 @@ export class MirrorSyncService {
|
|
|
96
97
|
baseRemoteSize === remoteSize
|
|
97
98
|
) {
|
|
98
99
|
// Check if local was modified
|
|
99
|
-
const localContent = fs.readFileSync(localFullPath
|
|
100
|
+
const localContent = fs.readFileSync(localFullPath);
|
|
100
101
|
const localHash = this.computeHash(localContent);
|
|
101
102
|
if (localHash === baseHash) {
|
|
102
103
|
// File unchanged on both ends
|
|
@@ -107,7 +108,7 @@ export class MirrorSyncService {
|
|
|
107
108
|
// Download content only when potentially changed
|
|
108
109
|
let remoteContent = '';
|
|
109
110
|
try {
|
|
110
|
-
remoteContent = await this.remoteFs.readFile(profile, remoteFullPath,
|
|
111
|
+
remoteContent = await this.remoteFs.readFile(profile, remoteFullPath, null);
|
|
111
112
|
} catch (err) {
|
|
112
113
|
continue; // skip unreadable files
|
|
113
114
|
}
|
|
@@ -130,7 +131,7 @@ export class MirrorSyncService {
|
|
|
130
131
|
updated.push(relPath);
|
|
131
132
|
if (!dryRun) {
|
|
132
133
|
fs.mkdirSync(path.dirname(localFullPath), { recursive: true });
|
|
133
|
-
fs.writeFileSync(localFullPath, remoteContent
|
|
134
|
+
fs.writeFileSync(localFullPath, remoteContent);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -195,11 +196,12 @@ export class MirrorSyncService {
|
|
|
195
196
|
};
|
|
196
197
|
|
|
197
198
|
const localFiles = walkLocal(localDir, localDir);
|
|
199
|
+
const createdDirs = new Set(['.', '/', '']);
|
|
198
200
|
for (const rel of localFiles) {
|
|
199
201
|
const localFull = path.join(localDir, rel);
|
|
200
202
|
const remoteFull = path.posix.join(remoteDir, rel.replace(/\\/g, '/'));
|
|
201
203
|
const stat = fs.statSync(localFull);
|
|
202
|
-
const content = fs.readFileSync(localFull
|
|
204
|
+
const content = fs.readFileSync(localFull);
|
|
203
205
|
const localHash = this.computeHash(content);
|
|
204
206
|
|
|
205
207
|
const baseEntry = snapshot[rel];
|
|
@@ -211,8 +213,11 @@ export class MirrorSyncService {
|
|
|
211
213
|
|
|
212
214
|
if (!dryRun) {
|
|
213
215
|
const parent = path.posix.dirname(remoteFull);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
if (!createdDirs.has(parent)) {
|
|
217
|
+
await this.remoteFs.mkdir(profile, parent);
|
|
218
|
+
createdDirs.add(parent);
|
|
219
|
+
}
|
|
220
|
+
await this.remoteFs.writeFile(profile, remoteFull, content);
|
|
216
221
|
}
|
|
217
222
|
pushed.push(rel);
|
|
218
223
|
|
package/lib/remote-fs-service.js
CHANGED
|
@@ -33,6 +33,10 @@ export class RemoteFsService {
|
|
|
33
33
|
/**
|
|
34
34
|
* Read directory entries with in-memory TTL cache
|
|
35
35
|
*/
|
|
36
|
+
async list(profile, remotePath, bypassCache = false) {
|
|
37
|
+
return this.listDir(profile, remotePath, bypassCache);
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
async listDir(profile, remotePath, bypassCache = false) {
|
|
37
41
|
const key = this.cacheKey(profile.id, 'list', remotePath);
|
|
38
42
|
if (!bypassCache) {
|
package/lib/routes/docker.js
CHANGED
|
@@ -7,6 +7,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
7
7
|
path: '/dsh-remote-workspace/health',
|
|
8
8
|
handler: async (req, res) => {
|
|
9
9
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
10
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
10
11
|
try {
|
|
11
12
|
const url = new URL(req.url, 'http://localhost');
|
|
12
13
|
const profileId = url.searchParams.get('profileId');
|
|
@@ -27,6 +28,7 @@ export function registerDockerRoutes(sctx, webServer, { dockerService, healthSer
|
|
|
27
28
|
path: '/dsh-remote-workspace/docker/list',
|
|
28
29
|
handler: async (req, res) => {
|
|
29
30
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
31
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
30
32
|
try {
|
|
31
33
|
const url = new URL(req.url, 'http://localhost');
|
|
32
34
|
const profileId = url.searchParams.get('profileId');
|
package/lib/routes/files.js
CHANGED
|
@@ -9,11 +9,24 @@ export function registerFileRoutes(sctx, webServer, { remoteFs, store, sshServic
|
|
|
9
9
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' });
|
|
10
10
|
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
11
11
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const body = await readBody(req);
|
|
13
|
+
let profile = null;
|
|
14
|
+
if (body.profileId) {
|
|
15
|
+
profile = store.getProfile(body.profileId);
|
|
16
|
+
} else if (typeof body.profile === 'string') {
|
|
17
|
+
profile = store.getProfile(body.profile);
|
|
18
|
+
} else if (body.profile && typeof body.profile === 'object' && body.profile.host) {
|
|
19
|
+
profile = body.profile;
|
|
20
|
+
} else if (body.profile?.id) {
|
|
21
|
+
profile = store.getProfile(body.profile.id);
|
|
22
|
+
} else {
|
|
23
|
+
profile = store.getActiveProfile();
|
|
24
|
+
}
|
|
14
25
|
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
|
|
27
|
+
const targetPath = body.remotePath || body.path || profile.remoteWorkspace || profile.remotePath || '.';
|
|
28
|
+
const entries = await remoteFs.listDir(profile, targetPath);
|
|
29
|
+
writeJson(res, 200, { ok: true, entries, items: entries, currentPath: targetPath });
|
|
17
30
|
} catch (err) {
|
|
18
31
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
19
32
|
}
|
package/lib/routes/helpers.js
CHANGED
|
@@ -25,14 +25,56 @@ export function readBody(req) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export function parseCookies(cookieHeader) {
|
|
29
|
+
const cookies = {};
|
|
30
|
+
if (typeof cookieHeader !== 'string') return cookies;
|
|
31
|
+
for (const pair of cookieHeader.split(';')) {
|
|
32
|
+
const idx = pair.indexOf('=');
|
|
33
|
+
if (idx !== -1) {
|
|
34
|
+
const key = pair.slice(0, idx).trim();
|
|
35
|
+
const val = pair.slice(idx + 1).trim();
|
|
36
|
+
cookies[key] = decodeURIComponent(val);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return cookies;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
export function isTrustedSettingsRequest(req) {
|
|
43
|
+
const secFetch = req.headers['sec-fetch-site'];
|
|
44
|
+
// Fail-closed: unconditionally reject cross-site and cross-origin requests
|
|
45
|
+
if (secFetch && secFetch !== 'same-origin') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If origin is present, ensure it matches the request host
|
|
50
|
+
const origin = req.headers['origin'];
|
|
51
|
+
const host = req.headers['host'];
|
|
52
|
+
if (origin && host) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(origin);
|
|
55
|
+
if (url.host !== host) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
const rawAuth = req.headers['authorization'];
|
|
30
|
-
if (typeof rawAuth === 'string' && rawAuth.startsWith('Bearer ')) return true;
|
|
31
64
|
const cookieHeader = req.headers['cookie'];
|
|
32
|
-
|
|
33
|
-
|
|
65
|
+
const expectedToken = process.env.DSH_AUTH_TOKEN || process.env.DSH_TOKEN;
|
|
66
|
+
|
|
67
|
+
if (expectedToken) {
|
|
68
|
+
if (typeof rawAuth === 'string' && rawAuth.startsWith('Bearer ')) {
|
|
69
|
+
const token = rawAuth.slice(7).trim();
|
|
70
|
+
if (token && token === expectedToken) return true;
|
|
71
|
+
}
|
|
72
|
+
if (typeof cookieHeader === 'string') {
|
|
73
|
+
const cookies = parseCookies(cookieHeader);
|
|
74
|
+
const token = cookies['token'] || cookies['dsh_token'];
|
|
75
|
+
if (token && token === expectedToken) return true;
|
|
76
|
+
}
|
|
34
77
|
}
|
|
35
|
-
const secFetch = req.headers['sec-fetch-site'];
|
|
36
78
|
if (secFetch === 'same-origin') return true;
|
|
37
79
|
const ip = req.socket?.remoteAddress || '';
|
|
38
80
|
if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') return true;
|
package/lib/routes/profiles.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { writeJson, readBody, isTrustedSettingsRequest } from './helpers.js';
|
|
2
2
|
import { SshConfigParser } from '../ssh-config-parser.js';
|
|
3
3
|
|
|
4
|
+
function maskProfile(p) {
|
|
5
|
+
if (!p || typeof p !== 'object') return p;
|
|
6
|
+
const masked = { ...p };
|
|
7
|
+
if (masked.password) masked.password = '••••••••';
|
|
8
|
+
if (masked.privateKey) masked.privateKey = '••••••••';
|
|
9
|
+
if (masked.passphrase) masked.passphrase = '••••••••';
|
|
10
|
+
return masked;
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
export function registerProfileRoutes(sctx, webServer, { store, sshService, alertService, tunnelService }) {
|
|
5
14
|
// 1. GET /dsh-remote-workspace/state
|
|
6
15
|
sctx.effect(() => webServer.register({
|
|
@@ -8,13 +17,14 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
8
17
|
path: '/dsh-remote-workspace/state',
|
|
9
18
|
handler: async (req, res) => {
|
|
10
19
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
20
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
11
21
|
try {
|
|
12
22
|
const profiles = store.getProfiles();
|
|
13
23
|
const activeId = store.getActiveId() || (profiles[0]?.id || '');
|
|
14
24
|
const tunnels = tunnelService.listActiveTunnels();
|
|
15
25
|
const autoSync = store.isAutoSyncEnabled ? store.isAutoSyncEnabled() : false;
|
|
16
26
|
const alerts = alertService ? alertService.getAlerts() : [];
|
|
17
|
-
const sanitized = profiles.map((p) => (store.vault ? store.vault.sanitizeProfile(p) : p));
|
|
27
|
+
const sanitized = profiles.map((p) => (store.vault ? store.vault.sanitizeProfile(p) : maskProfile(p)));
|
|
18
28
|
writeJson(res, 200, { ok: true, activeId, profiles: sanitized, tunnels, autoSync, alerts });
|
|
19
29
|
} catch (err) {
|
|
20
30
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
@@ -36,7 +46,7 @@ export function registerProfileRoutes(sctx, webServer, { store, sshService, aler
|
|
|
36
46
|
}
|
|
37
47
|
sshService.invalidate(profile.id);
|
|
38
48
|
await store.saveProfile(profile);
|
|
39
|
-
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : profile;
|
|
49
|
+
const sanitized = store.vault ? store.vault.sanitizeProfile(profile) : maskProfile(profile);
|
|
40
50
|
writeJson(res, 200, { ok: true, profile: sanitized });
|
|
41
51
|
} catch (err) {
|
|
42
52
|
writeJson(res, 500, { ok: false, error: err.message });
|
package/lib/routes/terminal.js
CHANGED
|
@@ -10,9 +10,9 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
10
10
|
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
11
11
|
try {
|
|
12
12
|
const { profileId, cols, rows } = await readBody(req);
|
|
13
|
-
const profile = store.getProfile(profileId);
|
|
13
|
+
const profile = profileId ? store.getProfile(profileId) : store.getActiveProfile();
|
|
14
14
|
if (!profile) return writeJson(res, 404, { ok: false, error: 'Profile not found' });
|
|
15
|
-
const session = await sshService.createTerminalSession(profile, cols || 80, rows || 24);
|
|
15
|
+
const session = await sshService.createTerminalSession(profile, { cols: cols || 80, rows: rows || 24 });
|
|
16
16
|
writeJson(res, 200, { ok: true, sessionId: session.id });
|
|
17
17
|
} catch (err) {
|
|
18
18
|
writeJson(res, 500, { ok: false, error: err.message });
|
|
@@ -50,6 +50,7 @@ export function registerTerminalRoutes(sctx, webServer, { sshService, store }) {
|
|
|
50
50
|
path: '/dsh-remote-workspace/terminal/stream',
|
|
51
51
|
handler: async (req, res) => {
|
|
52
52
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
53
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
53
54
|
try {
|
|
54
55
|
const url = new URL(req.url, 'http://localhost');
|
|
55
56
|
const sessionId = url.searchParams.get('sessionId');
|
package/lib/routes/tunnels.js
CHANGED
|
@@ -49,6 +49,7 @@ export function registerTunnelRoutes(sctx, webServer, { tunnelService, store })
|
|
|
49
49
|
path: '/dsh-remote-workspace/tunnels/telemetry',
|
|
50
50
|
handler: async (req, res) => {
|
|
51
51
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' });
|
|
52
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'Forbidden' });
|
|
52
53
|
try {
|
|
53
54
|
writeJson(res, 200, { ok: true, tunnels: tunnelService.listActiveTunnels() });
|
|
54
55
|
} catch (err) {
|
package/lib/ssh-service.js
CHANGED
|
@@ -313,6 +313,9 @@ export class SshService {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
async createTerminalSession(profile, options = {}) {
|
|
316
|
+
if (typeof options === 'number') {
|
|
317
|
+
options = { cols: options, rows: arguments[2] };
|
|
318
|
+
}
|
|
316
319
|
const conn = await this.getConnection(profile);
|
|
317
320
|
const cols = options.cols || 80;
|
|
318
321
|
const rows = options.rows || 24;
|
package/lib/tar-sync-service.js
CHANGED
|
@@ -2,6 +2,11 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
|
|
5
|
+
export function shellQuote(arg) {
|
|
6
|
+
if (typeof arg !== 'string') arg = String(arg || '');
|
|
7
|
+
return "'" + arg.replace(/'/g, "'\\''") + "'";
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export class TarSyncService {
|
|
6
11
|
/**
|
|
7
12
|
* @param {import('./ssh-service.js').SshService} sshService
|
|
@@ -17,11 +22,13 @@ export class TarSyncService {
|
|
|
17
22
|
*/
|
|
18
23
|
async pullTar(profile, remoteDir, localDir) {
|
|
19
24
|
if (!profile) throw new Error('No profile provided');
|
|
25
|
+
if (!remoteDir || typeof remoteDir !== 'string') throw new Error('remoteDir must be a non-empty string');
|
|
20
26
|
fs.mkdirSync(localDir, { recursive: true });
|
|
21
27
|
|
|
22
28
|
const client = await this.sshService.getConnection(profile);
|
|
23
29
|
return new Promise((resolve, reject) => {
|
|
24
|
-
const
|
|
30
|
+
const safeRemoteDir = shellQuote(remoteDir);
|
|
31
|
+
const remoteCmd = `tar --exclude='.git' --exclude='node_modules' -czf - -C ${safeRemoteDir} .`;
|
|
25
32
|
client.exec(remoteCmd, (err, stream) => {
|
|
26
33
|
if (err) return reject(err);
|
|
27
34
|
|
|
@@ -52,14 +59,16 @@ export class TarSyncService {
|
|
|
52
59
|
*/
|
|
53
60
|
async pushTar(profile, localDir, remoteDir) {
|
|
54
61
|
if (!profile) throw new Error('No profile provided');
|
|
62
|
+
if (!remoteDir || typeof remoteDir !== 'string') throw new Error('remoteDir must be a non-empty string');
|
|
55
63
|
if (!fs.existsSync(localDir)) throw new Error(`Local dir does not exist: ${localDir}`);
|
|
56
64
|
|
|
65
|
+
const safeRemoteDir = shellQuote(remoteDir);
|
|
57
66
|
const client = await this.sshService.getConnection(profile);
|
|
58
67
|
// Ensure remoteDir exists
|
|
59
|
-
await this.sshService.exec(profile, `mkdir -p
|
|
68
|
+
await this.sshService.exec(profile, `mkdir -p ${safeRemoteDir}`);
|
|
60
69
|
|
|
61
70
|
return new Promise((resolve, reject) => {
|
|
62
|
-
const remoteCmd = `tar -xzf - -C
|
|
71
|
+
const remoteCmd = `tar -xzf - -C ${safeRemoteDir}`;
|
|
63
72
|
client.exec(remoteCmd, (err, stream) => {
|
|
64
73
|
if (err) return reject(err);
|
|
65
74
|
|
package/lib/tools.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
|
|
1
3
|
export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunnelService, getActiveProfile, getProfileById, dockerService, diagnoseService, envService, tarSyncService) {
|
|
2
4
|
if (!ctx.tools || typeof ctx.tools.register !== 'function') {
|
|
3
5
|
return;
|
|
4
6
|
}
|
|
5
7
|
|
|
6
8
|
const genericOutput = {
|
|
7
|
-
schema: { type: 'object' },
|
|
9
|
+
schema: { type: 'object', additionalProperties: true },
|
|
8
10
|
render: (_args, value) => ({ type: 'text', content: typeof value === 'string' ? value : JSON.stringify(value, null, 2) })
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
// 1. remote_exec
|
|
12
|
-
ctx.tools.register({
|
|
14
|
+
ctx.tools.register(defineTool({
|
|
13
15
|
name: 'remote_exec',
|
|
14
16
|
description: 'Execute bash/shell command on the active remote workspace host with timeout and ANSI cleaning',
|
|
15
17
|
parameters: {
|
|
@@ -41,10 +43,10 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
41
43
|
signal: res.signal
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
|
-
});
|
|
46
|
+
}));
|
|
45
47
|
|
|
46
48
|
// 2. remote_fs
|
|
47
|
-
ctx.tools.register({
|
|
49
|
+
ctx.tools.register(defineTool({
|
|
48
50
|
name: 'remote_fs',
|
|
49
51
|
description: 'Perform file operations (read, write, stat, list, mkdir, remove) on remote workspace with caching',
|
|
50
52
|
parameters: {
|
|
@@ -91,10 +93,10 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
91
93
|
throw new Error(`Unknown action: ${action}`);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
|
-
});
|
|
96
|
+
}));
|
|
95
97
|
|
|
96
98
|
// 3. remote_sync
|
|
97
|
-
ctx.tools.register({
|
|
99
|
+
ctx.tools.register(defineTool({
|
|
98
100
|
name: 'remote_sync',
|
|
99
101
|
description: 'Perform 3-way delta or streaming tarball synchronization between local mirror and remote server',
|
|
100
102
|
parameters: {
|
|
@@ -125,10 +127,10 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
125
127
|
return await mirrorSync.push(profile, profile.localMirrorPath, profile.remoteWorkspace, args.force, args.dryRun);
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
});
|
|
130
|
+
}));
|
|
129
131
|
|
|
130
132
|
// 4. remote_tunnel
|
|
131
|
-
ctx.tools.register({
|
|
133
|
+
ctx.tools.register(defineTool({
|
|
132
134
|
name: 'remote_tunnel',
|
|
133
135
|
description: 'Manage SSH port forwarding tunnels (start, stop, list) with telemetry metrics',
|
|
134
136
|
parameters: {
|
|
@@ -157,11 +159,11 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
157
159
|
return { success: ok };
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
|
-
});
|
|
162
|
+
}));
|
|
161
163
|
|
|
162
164
|
// 5. remote_docker
|
|
163
165
|
if (dockerService) {
|
|
164
|
-
ctx.tools.register({
|
|
166
|
+
ctx.tools.register(defineTool({
|
|
165
167
|
name: 'remote_docker',
|
|
166
168
|
description: 'Manage Docker/Podman containers and Docker Compose on remote host',
|
|
167
169
|
parameters: {
|
|
@@ -194,11 +196,11 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
194
196
|
throw new Error(`Unknown docker action: ${args.action}`);
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
|
-
});
|
|
199
|
+
}));
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
// 6. remote_service
|
|
201
|
-
ctx.tools.register({
|
|
203
|
+
ctx.tools.register(defineTool({
|
|
202
204
|
name: 'remote_service',
|
|
203
205
|
description: 'Inspect and manage Linux systemd or PM2 services on the remote machine',
|
|
204
206
|
parameters: {
|
|
@@ -232,10 +234,10 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
232
234
|
output: res.stdout || res.stderr
|
|
233
235
|
};
|
|
234
236
|
}
|
|
235
|
-
});
|
|
237
|
+
}));
|
|
236
238
|
|
|
237
239
|
// 7. remote_transfer
|
|
238
|
-
ctx.tools.register({
|
|
240
|
+
ctx.tools.register(defineTool({
|
|
239
241
|
name: 'remote_transfer',
|
|
240
242
|
description: 'Transfer a file directly between two configured remote servers via in-memory SFTP streams',
|
|
241
243
|
parameters: {
|
|
@@ -254,11 +256,11 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
254
256
|
|
|
255
257
|
return await sshService.transferFileBetweenHosts(srcProfile, args.sourcePath, destProfile, args.destPath);
|
|
256
258
|
}
|
|
257
|
-
});
|
|
259
|
+
}));
|
|
258
260
|
|
|
259
261
|
// 8. remote_diagnose (New in v0.3.1)
|
|
260
262
|
if (diagnoseService) {
|
|
261
|
-
ctx.tools.register({
|
|
263
|
+
ctx.tools.register(defineTool({
|
|
262
264
|
name: 'remote_diagnose',
|
|
263
265
|
description: 'Run targeted system health diagnostics on remote host (ports, disk, memory, oom_killer, service_logs)',
|
|
264
266
|
parameters: {
|
|
@@ -278,12 +280,12 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
278
280
|
if (!profile) throw new Error('No active remote workspace profile selected');
|
|
279
281
|
return await diagnoseService.diagnose(profile, args.category, args.target);
|
|
280
282
|
}
|
|
281
|
-
});
|
|
283
|
+
}));
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
// 9. remote_env (New in v0.3.1)
|
|
285
287
|
if (envService) {
|
|
286
|
-
ctx.tools.register({
|
|
288
|
+
ctx.tools.register(defineTool({
|
|
287
289
|
name: 'remote_env',
|
|
288
290
|
description: 'Inspect and atomically update remote .env files preserving comments and structure',
|
|
289
291
|
parameters: {
|
|
@@ -320,6 +322,6 @@ export function registerRemoteTools(ctx, sshService, remoteFs, mirrorSync, tunne
|
|
|
320
322
|
throw new Error(`Unknown env action: ${args.action}`);
|
|
321
323
|
}
|
|
322
324
|
}
|
|
323
|
-
});
|
|
325
|
+
}));
|
|
324
326
|
}
|
|
325
327
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-remote-workspace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Enterprise Remote Workspace Plugin for DeepSeek Harness: SSH2 connection pool, SFTP operations, 3-way conflict-aware mirror sync, port forwarding, and native DSH UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|