@tiphareth/dsh-hardssh 0.1.2 → 0.2.3
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/LICENSE +30 -0
- package/README.md +77 -50
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1171 -1321
- package/lib/fs.js +333 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +42 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +59 -26
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -96
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +171 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -225
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
package/src/tools.ts
CHANGED
|
@@ -1,221 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent tools: the remote-workspace counterpart of the local fs tools. Every
|
|
3
|
-
* tool is bound to the SSH workspace OF THE CALLING SESSION: the tool's
|
|
4
|
-
* `exec` carries `agent.session.header.cwd` (the session's workspace), which
|
|
5
|
-
* the
|
|
6
|
-
* session in a local workspace gets a clear "this workspace is
|
|
7
|
-
* error; sessions in SSH-bound workspaces operate on the remote
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import
|
|
23
|
-
import type {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Run one remote op, catching errors into the failure envelope. */
|
|
78
|
-
const run = async <T>(operation: () => Promise<T>): Promise<T | ToolFailure> => {
|
|
79
|
-
try {
|
|
80
|
-
return await operation()
|
|
81
|
-
} catch (error) {
|
|
82
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
83
|
-
return { ok: false, error: message }
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return [
|
|
88
|
-
defineTool({
|
|
89
|
-
name: 'remote_status',
|
|
90
|
-
description: 'List the SSH-bound workspaces and report whether the CALLING SESSION\'s workspace is SSH-bound (and to which host/root). Call this before any remote_* tool. Triggers: SSH mode, remote workspace, where am I working, which servers are bound.',
|
|
91
|
-
parameters: {},
|
|
92
|
-
output: {
|
|
93
|
-
schema: {
|
|
94
|
-
type: 'object',
|
|
95
|
-
additionalProperties: false,
|
|
96
|
-
properties: {
|
|
97
|
-
ok: { type: 'boolean', required: true },
|
|
98
|
-
bound: { type: 'boolean', required: true },
|
|
99
|
-
workspaceTitle: { type: 'string' },
|
|
100
|
-
alias: { type: 'string' },
|
|
101
|
-
remoteRoot: { type: 'string' },
|
|
102
|
-
anchorPath: { type: 'string' },
|
|
103
|
-
workspaces: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
104
|
-
id: { type: 'string', required: true }, title: { type: 'string', required: true }, alias: { type: 'string', required: true }, remoteRoot: { type: 'string', required: true }, anchorPath: { type: 'string', required: true },
|
|
105
|
-
} } },
|
|
106
|
-
error: { type: 'string' },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
render: (_args, value) => {
|
|
110
|
-
if (value.ok !== true) return text(`remote_status failed: ${value.error ?? 'unknown error'}`)
|
|
111
|
-
const list = (value.workspaces ?? []).map((w: { title: string; alias: string; remoteRoot: string; anchorPath: string }) =>
|
|
112
|
-
`- ${w.title} (${w.alias} @ ${w.remoteRoot}, anchor ${w.anchorPath})`)
|
|
113
|
-
const
|
|
114
|
-
? `current session: BOUND -> ${value.workspaceTitle ?? ''} (${value.alias ?? ''} @ ${value.remoteRoot ?? ''})`
|
|
115
|
-
: 'current session: NOT bound (this workspace is local)'
|
|
116
|
-
return text([
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
async execute(_args, exec) {
|
|
120
|
-
const
|
|
121
|
-
const workspace = await boundWorkspace(
|
|
122
|
-
return {
|
|
123
|
-
ok: true,
|
|
124
|
-
bound: workspace !== undefined,
|
|
125
|
-
workspaceTitle: workspace?.title,
|
|
126
|
-
alias: workspace?.alias,
|
|
127
|
-
remoteRoot: workspace?.remoteRoot,
|
|
128
|
-
anchorPath: workspace?.anchorPath,
|
|
129
|
-
workspaces:
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
}),
|
|
133
|
-
|
|
134
|
-
defineTool({
|
|
135
|
-
name: 'remote_ls',
|
|
136
|
-
description: 'List a directory on the SSH host bound to the CALLING SESSION\'s workspace (the path must be inside that workspace\'s remote root). Triggers: list remote directory, remote files, ls on the server.',
|
|
137
|
-
parameters: {
|
|
138
|
-
path: { type: 'string', required: true, description: 'Absolute remote directory path inside the workspace\'s remote root (e.g. /home/user/project/src).' },
|
|
139
|
-
},
|
|
140
|
-
output: {
|
|
141
|
-
schema: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
additionalProperties: false,
|
|
144
|
-
properties: {
|
|
145
|
-
ok: { type: 'boolean', required: true },
|
|
146
|
-
path: { type: 'string' },
|
|
147
|
-
entries: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
148
|
-
name: { type: 'string', required: true }, type: { type: 'string', required: true }, size: { type: 'integer', required: true }, mtimeMs: { type: 'integer', required: true },
|
|
149
|
-
} } },
|
|
150
|
-
error: { type: 'string' },
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
render: (_args, value) => {
|
|
154
|
-
if (value.ok !== true) return text(`remote_ls failed: ${value.error ?? 'unknown error'}`)
|
|
155
|
-
const rows = (value.entries ?? []).map((entry: { name: string; type: string; size: number }) =>
|
|
156
|
-
`${entry.type === 'dir' ? 'dir ' : 'file'} ${entry.name}${entry.type === 'file' ? ` (${entry.size} bytes)` : ''}`)
|
|
157
|
-
return text([`${value.path}`, ...(rows.length > 0 ? rows : ['(empty)'])].join('\n'))
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
async execute(args, exec) {
|
|
161
|
-
const check = await bound(exec)
|
|
162
|
-
if ('error' in check) return check
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
},
|
|
219
|
-
}),
|
|
220
|
-
]
|
|
221
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools: the remote-workspace counterpart of the local fs tools. Every
|
|
3
|
+
* tool is bound to the SSH workspace OF THE CALLING SESSION: the tool's
|
|
4
|
+
* `exec` carries `agent.session.header.cwd` (the session's workspace), which
|
|
5
|
+
* the SSH-workspace record source resolves to a bound SSH workspace (alias +
|
|
6
|
+
* remote root). A session in a local workspace gets a clear "this workspace is
|
|
7
|
+
* not SSH-bound" error; sessions in SSH-bound workspaces operate on the remote
|
|
8
|
+
* host.
|
|
9
|
+
*
|
|
10
|
+
* Plain file operations (read / write / edit / mkdir / rm / rename) are NOT
|
|
11
|
+
* duplicated here: the fs seam routes them automatically by the session cwd
|
|
12
|
+
* (SFTP on the remote host). This surface keeps only the tools the seam does
|
|
13
|
+
* not cover: status (self-description), directory listing, and search.
|
|
14
|
+
*
|
|
15
|
+
* The bound-workspace operations (listing / glob / grep) come from the
|
|
16
|
+
* `ops` resolver supplied at mount: it opens the logical connection via
|
|
17
|
+
* `WorkspaceCore` and uses its `workspace.fs` / `workspace.search`
|
|
18
|
+
* capabilities (provider-neutral). The tools never branch on `provider.id`.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-hardssh/tools
|
|
21
|
+
*/
|
|
22
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
23
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
24
|
+
import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
|
|
25
|
+
import { isInside, type WorkspaceStoreView } from './backend.ts'
|
|
26
|
+
import type { SshWorkspaceRecord } from './protocol.ts'
|
|
27
|
+
import type { ToolOpsResolver } from './workspace-tool-ops.ts'
|
|
28
|
+
|
|
29
|
+
/** One text content block (the only render shape these tools emit). */
|
|
30
|
+
function text(value: string): ContentBlock[] {
|
|
31
|
+
return [{ type: 'text', text: value }]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Tool-set dependencies. */
|
|
35
|
+
export interface WorkspaceToolsDeps {
|
|
36
|
+
/** SSH-workspace record source (the generic projection store). */
|
|
37
|
+
workspaces: WorkspaceStoreView
|
|
38
|
+
/** Bound-workspace operations provider (generic capability seam). Resolves
|
|
39
|
+
* the calling session's cwd to its ops. */
|
|
40
|
+
ops: ToolOpsResolver
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Failure envelope shared by every tool. */
|
|
44
|
+
interface ToolFailure {
|
|
45
|
+
ok: false
|
|
46
|
+
error: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The bound workspace record for the calling session's cwd, if any. */
|
|
50
|
+
async function boundWorkspace(workspaces: WorkspaceStoreView, exec: ToolRunContext): Promise<SshWorkspaceRecord | undefined> {
|
|
51
|
+
const cwd = exec.agent?.session?.header?.cwd
|
|
52
|
+
if (cwd === undefined || cwd === '') return undefined
|
|
53
|
+
return workspaces.findByAnchor(cwd)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Build every remote_* tool (registered by the host half). */
|
|
57
|
+
export function makeWorkspaceTools(deps: WorkspaceToolsDeps) {
|
|
58
|
+
const { workspaces, ops } = deps
|
|
59
|
+
|
|
60
|
+
/** The bound workspace + ops for this call, or a failure when the session's
|
|
61
|
+
* workspace is not SSH-bound. */
|
|
62
|
+
const bound = async (exec: ToolRunContext): Promise<{ record: SshWorkspaceRecord; ops: import('./workspace-tool-ops.ts').WorkspaceToolOps; resolve: (abs: string) => string | undefined } | ToolFailure> => {
|
|
63
|
+
const cwd = exec.agent?.session?.header?.cwd
|
|
64
|
+
const outcome = await ops(cwd)
|
|
65
|
+
if (outcome === null) {
|
|
66
|
+
return { ok: false, error: 'this session\'s workspace is not SSH-bound — create an SSH workspace and open a session in it first' }
|
|
67
|
+
}
|
|
68
|
+
const root = outcome.record.remoteRoot
|
|
69
|
+
return {
|
|
70
|
+
record: outcome.record,
|
|
71
|
+
ops: outcome.ops,
|
|
72
|
+
// Return undefined when the absolute path escapes the workspace root.
|
|
73
|
+
resolve: (abs: string): string | undefined => (isInside(root, abs) ? abs : undefined),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Run one remote op, catching errors into the failure envelope. */
|
|
78
|
+
const run = async <T>(operation: () => Promise<T>): Promise<T | ToolFailure> => {
|
|
79
|
+
try {
|
|
80
|
+
return await operation()
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
83
|
+
return { ok: false, error: message }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
defineTool({
|
|
89
|
+
name: 'remote_status',
|
|
90
|
+
description: 'List the SSH-bound workspaces and report whether the CALLING SESSION\'s workspace is SSH-bound (and to which host/root). Call this before any remote_* tool. Triggers: SSH mode, remote workspace, where am I working, which servers are bound.',
|
|
91
|
+
parameters: {},
|
|
92
|
+
output: {
|
|
93
|
+
schema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
ok: { type: 'boolean', required: true },
|
|
98
|
+
bound: { type: 'boolean', required: true },
|
|
99
|
+
workspaceTitle: { type: 'string' },
|
|
100
|
+
alias: { type: 'string' },
|
|
101
|
+
remoteRoot: { type: 'string' },
|
|
102
|
+
anchorPath: { type: 'string' },
|
|
103
|
+
workspaces: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
104
|
+
id: { type: 'string', required: true }, title: { type: 'string', required: true }, alias: { type: 'string', required: true }, remoteRoot: { type: 'string', required: true }, anchorPath: { type: 'string', required: true },
|
|
105
|
+
} } },
|
|
106
|
+
error: { type: 'string' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
render: (_args, value) => {
|
|
110
|
+
if (value.ok !== true) return text(`remote_status failed: ${value.error ?? 'unknown error'}`)
|
|
111
|
+
const list = (value.workspaces ?? []).map((w: { title: string; alias: string; remoteRoot: string; anchorPath: string }) =>
|
|
112
|
+
`- ${w.title} (${w.alias} @ ${w.remoteRoot}, anchor ${w.anchorPath})`)
|
|
113
|
+
const boundText = value.bound === true
|
|
114
|
+
? `current session: BOUND -> ${value.workspaceTitle ?? ''} (${value.alias ?? ''} @ ${value.remoteRoot ?? ''})`
|
|
115
|
+
: 'current session: NOT bound (this workspace is local)'
|
|
116
|
+
return text([boundText, '', 'SSH workspaces:', ...(list.length > 0 ? list : ['(none)'])].join('\n'))
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
async execute(_args, exec) {
|
|
120
|
+
const records = await workspaces.list()
|
|
121
|
+
const workspace = await boundWorkspace(workspaces, exec)
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
bound: workspace !== undefined,
|
|
125
|
+
workspaceTitle: workspace?.title,
|
|
126
|
+
alias: workspace?.alias,
|
|
127
|
+
remoteRoot: workspace?.remoteRoot,
|
|
128
|
+
anchorPath: workspace?.anchorPath,
|
|
129
|
+
workspaces: records.map((w) => ({ id: w.id, title: w.title, alias: w.alias, remoteRoot: w.remoteRoot, anchorPath: w.anchorPath })),
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
defineTool({
|
|
135
|
+
name: 'remote_ls',
|
|
136
|
+
description: 'List a directory on the SSH host bound to the CALLING SESSION\'s workspace (the path must be inside that workspace\'s remote root). Triggers: list remote directory, remote files, ls on the server.',
|
|
137
|
+
parameters: {
|
|
138
|
+
path: { type: 'string', required: true, description: 'Absolute remote directory path inside the workspace\'s remote root (e.g. /home/user/project/src).' },
|
|
139
|
+
},
|
|
140
|
+
output: {
|
|
141
|
+
schema: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
additionalProperties: false,
|
|
144
|
+
properties: {
|
|
145
|
+
ok: { type: 'boolean', required: true },
|
|
146
|
+
path: { type: 'string' },
|
|
147
|
+
entries: { type: 'array', items: { type: 'object', additionalProperties: false, properties: {
|
|
148
|
+
name: { type: 'string', required: true }, type: { type: 'string', required: true }, size: { type: 'integer', required: true }, mtimeMs: { type: 'integer', required: true },
|
|
149
|
+
} } },
|
|
150
|
+
error: { type: 'string' },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
render: (_args, value) => {
|
|
154
|
+
if (value.ok !== true) return text(`remote_ls failed: ${value.error ?? 'unknown error'}`)
|
|
155
|
+
const rows = (value.entries ?? []).map((entry: { name: string; type: string; size: number }) =>
|
|
156
|
+
`${entry.type === 'dir' ? 'dir ' : 'file'} ${entry.name}${entry.type === 'file' ? ` (${entry.size} bytes)` : ''}`)
|
|
157
|
+
return text([`${value.path}`, ...(rows.length > 0 ? rows : ['(empty)'])].join('\n'))
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
async execute(args, exec) {
|
|
161
|
+
const check = await bound(exec)
|
|
162
|
+
if ('error' in check) return check
|
|
163
|
+
const abs = check.resolve(args.path)
|
|
164
|
+
if (abs === undefined) {
|
|
165
|
+
return { ok: false, error: `path '${args.path}' is outside the remote root '${check.record.remoteRoot}' of workspace '${check.record.title}'` }
|
|
166
|
+
}
|
|
167
|
+
return run(async () => {
|
|
168
|
+
const entries = await check.ops.listDir(abs)
|
|
169
|
+
return { ok: true, path: abs, entries }
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
defineTool({
|
|
175
|
+
name: 'remote_search',
|
|
176
|
+
description: 'Search the SSH workspace bound to the CALLING SESSION. mode="glob" matches FILE NAMES by pattern (root-relative, e.g. src/**/*.ts or *.log; capped at 200 hits); mode="grep" searches file CONTENTS for a FIXED STRING (not a regex; skips .git and node_modules) and returns matched `path:line:content` records. Triggers: find remote files by pattern, glob on the server, grep remote code, search remote contents.',
|
|
177
|
+
parameters: {
|
|
178
|
+
mode: { type: 'string', enum: ['glob', 'grep'], required: true, description: 'glob = match file names by pattern; grep = search file contents for a fixed string.' },
|
|
179
|
+
pattern: { type: 'string', required: true, description: 'glob: root-relative pattern like src/**/*.ts or *.log; grep: the literal text to find (not a regular expression).' },
|
|
180
|
+
},
|
|
181
|
+
output: {
|
|
182
|
+
schema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
additionalProperties: false,
|
|
185
|
+
properties: {
|
|
186
|
+
ok: { type: 'boolean', required: true },
|
|
187
|
+
mode: { type: 'string' },
|
|
188
|
+
hits: { type: 'array', items: { type: 'string' } },
|
|
189
|
+
lines: { type: 'array', items: { type: 'string' } },
|
|
190
|
+
truncated: { type: 'boolean' },
|
|
191
|
+
error: { type: 'string' },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
render: (_args, value) => {
|
|
195
|
+
if (value.ok !== true) return text(`remote_search failed: ${value.error ?? 'unknown error'}`)
|
|
196
|
+
const tail = value.truncated === true
|
|
197
|
+
? value.mode === 'grep' ? '\n[output truncated]' : '\n[truncated at 200 hits]'
|
|
198
|
+
: ''
|
|
199
|
+
const body = value.mode === 'grep' ? (value.lines ?? []) : (value.hits ?? [])
|
|
200
|
+
return text(body.length > 0 ? body.join('\n') + tail : '(no matches)')
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
async execute(args, exec) {
|
|
204
|
+
const check = await bound(exec)
|
|
205
|
+
if ('error' in check) return check
|
|
206
|
+
return run(async () => {
|
|
207
|
+
if (args.mode === 'grep') {
|
|
208
|
+
const found = await check.ops.grep(args.pattern)
|
|
209
|
+
return { ok: true, mode: 'grep', lines: found.lines, hits: [], truncated: found.truncated }
|
|
210
|
+
}
|
|
211
|
+
const found = await check.ops.glob(args.pattern)
|
|
212
|
+
return { ok: true, mode: 'glob', hits: found.hits, lines: [], truncated: found.truncated }
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
]
|
|
217
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound-workspace operations for the `remote_*` agent tools.
|
|
3
|
+
*
|
|
4
|
+
* The tools resolve the CALLING SESSION's bound workspace from its cwd, then
|
|
5
|
+
* run a small set of operations (directory listing, glob / content-grep)
|
|
6
|
+
* against it. The resolver opens the bound record through WorkspaceCore and
|
|
7
|
+
* uses its `workspace.fs` / `workspace.search` capabilities. It fails closed
|
|
8
|
+
* when the provider omits a required capability rather than falling back to a
|
|
9
|
+
* second engine-backed implementation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { FileSystem } from '@deepseek-ai/dsh-fs'
|
|
13
|
+
import type { WorkspaceSearchService } from './base/capability.ts'
|
|
14
|
+
import { isInside, type WorkspaceStoreView } from './backend.ts'
|
|
15
|
+
import type { SshWorkspaceRecord, WorkspaceEntry } from './protocol.ts'
|
|
16
|
+
import type { WorkspaceCore } from './runtime/workspace-core.ts'
|
|
17
|
+
import type { RemoteGlobResult, RemoteGrepResult } from './remote-search.ts'
|
|
18
|
+
|
|
19
|
+
/** Operations one bound workspace exposes to the remote_* tools. */
|
|
20
|
+
export interface WorkspaceToolOps {
|
|
21
|
+
/** List one absolute directory path inside the workspace remote root. */
|
|
22
|
+
listDir(abs: string): Promise<WorkspaceEntry[]>
|
|
23
|
+
/** glob filename search (pattern keeps glob semantics). */
|
|
24
|
+
glob(pattern: string): Promise<RemoteGlobResult>
|
|
25
|
+
/** fixed-string content grep (returns matched `path:line:content` records). */
|
|
26
|
+
grep(fixedPhrase: string): Promise<RemoteGrepResult>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The bound record + its ops for one session cwd. */
|
|
30
|
+
export interface BoundToolOps {
|
|
31
|
+
record: SshWorkspaceRecord
|
|
32
|
+
ops: WorkspaceToolOps
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve the calling session cwd to a bound workspace, or null when local. */
|
|
36
|
+
export type ToolOpsResolver = (cwd: string | undefined) => Promise<BoundToolOps | null>
|
|
37
|
+
|
|
38
|
+
/** workspace-relative path for an absolute path known to be inside the root. */
|
|
39
|
+
function relOf(root: string, abs: string): string {
|
|
40
|
+
const normalized = root.endsWith('/') ? root : `${root}/`
|
|
41
|
+
if (abs === root) return ''
|
|
42
|
+
if (!abs.startsWith(normalized)) throw new Error(`path '${abs}' is outside remote root '${root}'`)
|
|
43
|
+
return abs.slice(normalized.length)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** map a DSH directory entry to the tool DTO shape (mtime is opaque on capabilities). */
|
|
47
|
+
function toEntry(name: string, type: 'directory' | 'file' | 'other'): WorkspaceEntry {
|
|
48
|
+
return { name, type: type === 'directory' ? 'dir' : type, size: 0, mtimeMs: 0 }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function outsideError(root: string, abs: string): Error {
|
|
52
|
+
return new Error(`path '${abs}' is outside remote root '${root}'`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Capability-backed ops over one open connection. `root` confines every
|
|
57
|
+
* operation; search operations fail closed when the provider offers no
|
|
58
|
+
* `workspace.search` capability (a limited silent fallback would hide the gap).
|
|
59
|
+
*/
|
|
60
|
+
function capabilityOps(root: string, fs: FileSystem, search: WorkspaceSearchService | undefined): WorkspaceToolOps {
|
|
61
|
+
return {
|
|
62
|
+
async listDir(abs) {
|
|
63
|
+
if (!isInside(root, abs)) throw outsideError(root, abs)
|
|
64
|
+
const target = await fs.resolve(relOf(root, abs) === '' ? '.' : relOf(root, abs))
|
|
65
|
+
const info = await fs.stat(target)
|
|
66
|
+
if (info === undefined) throw new Error(`remote_ls: '${abs}' not found`)
|
|
67
|
+
if (info.type !== 'directory') throw new Error(`remote_ls: '${abs}' is not a directory`)
|
|
68
|
+
const entries = await fs.listDir(target)
|
|
69
|
+
return entries.map(entry => toEntry(entry.name, entry.type))
|
|
70
|
+
},
|
|
71
|
+
async glob(pattern) {
|
|
72
|
+
if (search === undefined) throw new Error('remote_search (glob): this workspace provider exposes no workspace.search capability')
|
|
73
|
+
const found = await search.glob(pattern)
|
|
74
|
+
return { hits: found.hits.map(hit => hit.path), truncated: found.truncated }
|
|
75
|
+
},
|
|
76
|
+
async grep(fixedPhrase) {
|
|
77
|
+
if (search === undefined) throw new Error('remote_search (grep): this workspace provider exposes no workspace.search capability')
|
|
78
|
+
const found = await search.grep(fixedPhrase)
|
|
79
|
+
return { lines: found.hits.map(hit => hit.match ?? hit.path), truncated: found.truncated }
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolver that opens each bound record through the generic WorkspaceCore. */
|
|
85
|
+
export function capabilityToolOpsResolver(store: WorkspaceStoreView, core: WorkspaceCore): ToolOpsResolver {
|
|
86
|
+
return async (cwd) => {
|
|
87
|
+
if (cwd === undefined || cwd === '') return null
|
|
88
|
+
const record = await store.findByAnchor(cwd)
|
|
89
|
+
if (record === undefined) return null
|
|
90
|
+
const connection = await core.openById(record.id)
|
|
91
|
+
if (connection === undefined) {
|
|
92
|
+
throw new Error(`remote workspace '${record.id}' is not open (failed closed)`)
|
|
93
|
+
}
|
|
94
|
+
const fs = connection.get('workspace.fs') as FileSystem | undefined
|
|
95
|
+
if (fs === undefined) {
|
|
96
|
+
throw new Error(`remote workspace '${record.id}' provides no workspace.fs capability`)
|
|
97
|
+
}
|
|
98
|
+
const search = connection.get('workspace.search') as WorkspaceSearchService | undefined
|
|
99
|
+
return { record, ops: capabilityOps(record.remoteRoot, fs, search) }
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public workspace API entry — the stable surface third-party plugins consume.
|
|
3
|
+
* Exports only platform-neutral record/provider/connection/capability types,
|
|
4
|
+
* the `WorkspaceCore` consumer interface, and the stable provider API version.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately NOT exported from here: `SshEngine`, concrete SSH connection /
|
|
7
|
+
* provider classes, the ledger/router/registry internals, and migration or
|
|
8
|
+
* legacy compatibility code — so implementation swaps never leak through this
|
|
9
|
+
* boundary (migration plan Phase 6, §「稳定导出」).
|
|
10
|
+
*
|
|
11
|
+
* @module @tiphareth/dsh-hardssh/workspace
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Platform-neutral model: records, provider/connection contracts, manifest.
|
|
15
|
+
export type {
|
|
16
|
+
ConnectionRef,
|
|
17
|
+
WorkspaceAnchor,
|
|
18
|
+
WorkspaceCapabilityMap,
|
|
19
|
+
WorkspaceConnection,
|
|
20
|
+
WorkspaceCreateInput,
|
|
21
|
+
WorkspaceId,
|
|
22
|
+
WorkspaceLocation,
|
|
23
|
+
WorkspaceProvider,
|
|
24
|
+
WorkspaceProviderManifest,
|
|
25
|
+
WorkspaceProviderRef,
|
|
26
|
+
WorkspaceRecord,
|
|
27
|
+
WorkspaceUpdate,
|
|
28
|
+
} from './base/model.ts'
|
|
29
|
+
export { WORKSPACE_PROVIDER_API_VERSION } from './base/model.ts'
|
|
30
|
+
|
|
31
|
+
// The capability resource contracts a provider may serve (path-style fs,
|
|
32
|
+
// search, and DSH FileSystem/SubprocessRuntime live on the runtime map above).
|
|
33
|
+
export type {
|
|
34
|
+
WorkspaceDirEntry,
|
|
35
|
+
WorkspaceFileSystem,
|
|
36
|
+
WorkspaceProcessResult,
|
|
37
|
+
WorkspaceProcessRuntime,
|
|
38
|
+
WorkspaceSearchHit,
|
|
39
|
+
WorkspaceSearchService,
|
|
40
|
+
WorkspaceStat,
|
|
41
|
+
} from './base/capability.ts'
|
|
42
|
+
|
|
43
|
+
// The high-level consumer surface (ready/openById/openByAnchor/CRUD/
|
|
44
|
+
// registerProvider/subscribe/closeAll). The concrete workspace-core
|
|
45
|
+
// implementation stays internal; consumers depend on this interface only.
|
|
46
|
+
export type {
|
|
47
|
+
WorkspaceCore,
|
|
48
|
+
WorkspaceListener,
|
|
49
|
+
} from './runtime/workspace-core.ts'
|
|
50
|
+
|
|
51
|
+
// The registry surface used to register third-party providers.
|
|
52
|
+
export type { WorkspaceRegistry } from './base/registry.ts'
|