@oh-my-pi/pi-coding-agent 12.12.2 → 12.13.0
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/CHANGELOG.md +30 -0
- package/package.json +7 -7
- package/src/cli/ssh-cli.ts +179 -0
- package/src/cli.ts +1 -0
- package/src/commands/ssh.ts +60 -0
- package/src/commit/prompts/analysis-system.md +1 -3
- package/src/config/prompt-templates.ts +20 -5
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/ssh.ts +26 -19
- package/src/modes/controllers/ssh-command-controller.ts +452 -0
- package/src/modes/interactive-mode.ts +6 -0
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +237 -135
- package/src/patch/index.ts +37 -39
- package/src/patch/shared.ts +37 -23
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/grep.md +12 -8
- package/src/prompts/tools/hashline.md +98 -53
- package/src/prompts/tools/read.md +1 -3
- package/src/session/agent-session.ts +1 -1
- package/src/session/auth-storage.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/ssh/config-writer.ts +183 -0
- package/src/tools/bash-interactive.ts +47 -7
- package/src/tools/grep.ts +1 -1
- package/src/tools/read.ts +2 -2
- package/src/tools/ssh.ts +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Command Controller
|
|
3
|
+
*
|
|
4
|
+
* Handles /ssh subcommands for managing SSH host configurations.
|
|
5
|
+
*/
|
|
6
|
+
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { getProjectDir, getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs";
|
|
8
|
+
import { type SSHHost, sshCapability } from "../../capability/ssh";
|
|
9
|
+
import { loadCapability } from "../../discovery";
|
|
10
|
+
import { addSSHHost, readSSHConfigFile, removeSSHHost, type SSHHostConfig } from "../../ssh/config-writer";
|
|
11
|
+
import { DynamicBorder } from "../components/dynamic-border";
|
|
12
|
+
import { theme } from "../theme/theme";
|
|
13
|
+
import type { InteractiveModeContext } from "../types";
|
|
14
|
+
|
|
15
|
+
function parseCommandArgs(argsString: string): string[] {
|
|
16
|
+
const args: string[] = [];
|
|
17
|
+
let current = "";
|
|
18
|
+
let inQuote: string | null = null;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
21
|
+
const char = argsString[i];
|
|
22
|
+
|
|
23
|
+
if (inQuote) {
|
|
24
|
+
if (char === inQuote) {
|
|
25
|
+
inQuote = null;
|
|
26
|
+
} else {
|
|
27
|
+
current += char;
|
|
28
|
+
}
|
|
29
|
+
} else if (char === '"' || char === "'") {
|
|
30
|
+
inQuote = char;
|
|
31
|
+
} else if (char === " " || char === "\t") {
|
|
32
|
+
if (current) {
|
|
33
|
+
args.push(current);
|
|
34
|
+
current = "";
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
current += char;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (current) {
|
|
42
|
+
args.push(current);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type SSHAddScope = "user" | "project";
|
|
49
|
+
|
|
50
|
+
export class SSHCommandController {
|
|
51
|
+
constructor(private ctx: InteractiveModeContext) {}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle /ssh command and route to subcommands
|
|
55
|
+
*/
|
|
56
|
+
async handle(text: string): Promise<void> {
|
|
57
|
+
const parts = text.trim().split(/\s+/);
|
|
58
|
+
const subcommand = parts[1]?.toLowerCase();
|
|
59
|
+
|
|
60
|
+
if (!subcommand || subcommand === "help") {
|
|
61
|
+
this.#showHelp();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
switch (subcommand) {
|
|
66
|
+
case "add":
|
|
67
|
+
await this.#handleAdd(text);
|
|
68
|
+
break;
|
|
69
|
+
case "list":
|
|
70
|
+
await this.#handleList();
|
|
71
|
+
break;
|
|
72
|
+
case "remove":
|
|
73
|
+
case "rm":
|
|
74
|
+
await this.#handleRemove(text);
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
this.ctx.showError(`Unknown subcommand: ${subcommand}. Type /ssh help for usage.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Show help text
|
|
83
|
+
*/
|
|
84
|
+
#showHelp(): void {
|
|
85
|
+
const helpText = [
|
|
86
|
+
"",
|
|
87
|
+
theme.bold("SSH Host Management"),
|
|
88
|
+
"",
|
|
89
|
+
"Manage SSH host configurations for remote command execution.",
|
|
90
|
+
"",
|
|
91
|
+
theme.fg("accent", "Commands:"),
|
|
92
|
+
" /ssh add <name> --host <host> [--user <user>] [--port <port>] [--key <keyPath>] [--desc <description>] [--compat] [--scope project|user]",
|
|
93
|
+
" /ssh list List all configured SSH hosts",
|
|
94
|
+
" /ssh remove <name> [--scope project|user] Remove an SSH host (default: project)",
|
|
95
|
+
" /ssh help Show this help message",
|
|
96
|
+
"",
|
|
97
|
+
].join("\n");
|
|
98
|
+
|
|
99
|
+
this.#showMessage(helpText);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle /ssh add - parse flags and add host to config
|
|
104
|
+
*/
|
|
105
|
+
async #handleAdd(text: string): Promise<void> {
|
|
106
|
+
const prefixMatch = text.match(/^\/ssh\s+add\b\s*(.*)$/i);
|
|
107
|
+
const rest = prefixMatch?.[1]?.trim() ?? "";
|
|
108
|
+
if (!rest) {
|
|
109
|
+
this.ctx.showError(
|
|
110
|
+
"Usage: /ssh add <name> --host <host> [--user <user>] [--port <port>] [--key <keyPath>] [--desc <description>] [--compat] [--scope project|user]",
|
|
111
|
+
);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const tokens = parseCommandArgs(rest);
|
|
116
|
+
if (tokens.length === 0) {
|
|
117
|
+
this.ctx.showError(
|
|
118
|
+
"Usage: /ssh add <name> --host <host> [--user <user>] [--port <port>] [--key <keyPath>] [--desc <description>] [--compat] [--scope project|user]",
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let name: string | undefined;
|
|
124
|
+
let scope: SSHAddScope = "project";
|
|
125
|
+
let host: string | undefined;
|
|
126
|
+
let username: string | undefined;
|
|
127
|
+
let port: number | undefined;
|
|
128
|
+
let keyPath: string | undefined;
|
|
129
|
+
let description: string | undefined;
|
|
130
|
+
let compat = false;
|
|
131
|
+
|
|
132
|
+
let i = 0;
|
|
133
|
+
if (!tokens[0].startsWith("-")) {
|
|
134
|
+
name = tokens[0];
|
|
135
|
+
i = 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
while (i < tokens.length) {
|
|
139
|
+
const argToken = tokens[i];
|
|
140
|
+
if (argToken === "--host") {
|
|
141
|
+
const value = tokens[i + 1];
|
|
142
|
+
if (!value) {
|
|
143
|
+
this.ctx.showError("Missing value for --host.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
host = value;
|
|
147
|
+
i += 2;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (argToken === "--user") {
|
|
151
|
+
const value = tokens[i + 1];
|
|
152
|
+
if (!value) {
|
|
153
|
+
this.ctx.showError("Missing value for --user.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
username = value;
|
|
157
|
+
i += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (argToken === "--port") {
|
|
161
|
+
const value = tokens[i + 1];
|
|
162
|
+
if (!value) {
|
|
163
|
+
this.ctx.showError("Missing value for --port.");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const parsed = Number.parseInt(value, 10);
|
|
167
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
168
|
+
this.ctx.showError("Invalid --port value. Must be an integer between 1 and 65535.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
port = parsed;
|
|
172
|
+
i += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (argToken === "--key") {
|
|
176
|
+
const value = tokens[i + 1];
|
|
177
|
+
if (!value) {
|
|
178
|
+
this.ctx.showError("Missing value for --key.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
keyPath = value;
|
|
182
|
+
i += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (argToken === "--desc") {
|
|
186
|
+
const value = tokens[i + 1];
|
|
187
|
+
if (!value) {
|
|
188
|
+
this.ctx.showError("Missing value for --desc.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
description = value;
|
|
192
|
+
i += 2;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (argToken === "--compat") {
|
|
196
|
+
compat = true;
|
|
197
|
+
i += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (argToken === "--scope") {
|
|
201
|
+
const value = tokens[i + 1];
|
|
202
|
+
if (!value || (value !== "project" && value !== "user")) {
|
|
203
|
+
this.ctx.showError("Invalid --scope value. Use project or user.");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
scope = value;
|
|
207
|
+
i += 2;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
this.ctx.showError(`Unknown option: ${argToken}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!name) {
|
|
215
|
+
this.ctx.showError("Host name required. Usage: /ssh add <name> --host <host> ...");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!host) {
|
|
220
|
+
this.ctx.showError("--host is required. Usage: /ssh add <name> --host <host> ...");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const cwd = getProjectDir();
|
|
226
|
+
const filePath = getSSHConfigPath(scope, cwd);
|
|
227
|
+
|
|
228
|
+
const hostConfig: SSHHostConfig = { host };
|
|
229
|
+
if (username) hostConfig.username = username;
|
|
230
|
+
if (port) hostConfig.port = port;
|
|
231
|
+
if (keyPath) hostConfig.keyPath = keyPath;
|
|
232
|
+
if (description) hostConfig.description = description;
|
|
233
|
+
if (compat) hostConfig.compat = true;
|
|
234
|
+
|
|
235
|
+
await addSSHHost(filePath, name, hostConfig);
|
|
236
|
+
|
|
237
|
+
const scopeLabel = scope === "user" ? "user" : "project";
|
|
238
|
+
const lines = [
|
|
239
|
+
"",
|
|
240
|
+
theme.fg("success", `✓ Added SSH host "${name}" to ${scopeLabel} config`),
|
|
241
|
+
"",
|
|
242
|
+
` Host: ${host}`,
|
|
243
|
+
];
|
|
244
|
+
if (username) lines.push(` User: ${username}`);
|
|
245
|
+
if (port) lines.push(` Port: ${port}`);
|
|
246
|
+
if (keyPath) lines.push(` Key: ${keyPath}`);
|
|
247
|
+
if (description) lines.push(` Desc: ${description}`);
|
|
248
|
+
if (compat) lines.push(` Compat: true`);
|
|
249
|
+
lines.push("");
|
|
250
|
+
lines.push(theme.fg("muted", `Run ${theme.fg("accent", "/ssh list")} to see all configured hosts.`));
|
|
251
|
+
lines.push("");
|
|
252
|
+
|
|
253
|
+
this.#showMessage(lines.join("\n"));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
256
|
+
|
|
257
|
+
let helpText = "";
|
|
258
|
+
if (errorMsg.includes("already exists")) {
|
|
259
|
+
helpText = `\n\nTip: Use ${theme.fg("accent", "/ssh remove")} first, or choose a different name.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.ctx.showError(`Failed to add host: ${errorMsg}${helpText}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Handle /ssh list - show all configured SSH hosts
|
|
268
|
+
*/
|
|
269
|
+
async #handleList(): Promise<void> {
|
|
270
|
+
try {
|
|
271
|
+
const cwd = getProjectDir();
|
|
272
|
+
|
|
273
|
+
// Load from both user and project configs
|
|
274
|
+
const userPath = getSSHConfigPath("user", cwd);
|
|
275
|
+
const projectPath = getSSHConfigPath("project", cwd);
|
|
276
|
+
|
|
277
|
+
const [userConfig, projectConfig] = await Promise.all([
|
|
278
|
+
readSSHConfigFile(userPath),
|
|
279
|
+
readSSHConfigFile(projectPath),
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
const userHosts = Object.keys(userConfig.hosts ?? {});
|
|
283
|
+
const projectHosts = Object.keys(projectConfig.hosts ?? {});
|
|
284
|
+
|
|
285
|
+
// Load discovered hosts via capability system
|
|
286
|
+
const configHostNames = new Set([...userHosts, ...projectHosts]);
|
|
287
|
+
let discoveredHosts: SSHHost[] = [];
|
|
288
|
+
try {
|
|
289
|
+
const result = await loadCapability<SSHHost>(sshCapability.id, { cwd });
|
|
290
|
+
discoveredHosts = result.items.filter(h => !configHostNames.has(h.name));
|
|
291
|
+
} catch {
|
|
292
|
+
// Ignore discovery errors
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (userHosts.length === 0 && projectHosts.length === 0 && discoveredHosts.length === 0) {
|
|
296
|
+
this.#showMessage(
|
|
297
|
+
[
|
|
298
|
+
"",
|
|
299
|
+
theme.fg("muted", "No SSH hosts configured."),
|
|
300
|
+
"",
|
|
301
|
+
`Use ${theme.fg("accent", "/ssh add")} to add a host.`,
|
|
302
|
+
"",
|
|
303
|
+
].join("\n"),
|
|
304
|
+
);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const lines: string[] = ["", theme.bold("Configured SSH Hosts"), ""];
|
|
309
|
+
|
|
310
|
+
// Show user-level hosts
|
|
311
|
+
if (userHosts.length > 0) {
|
|
312
|
+
lines.push(theme.fg("accent", "User level") + theme.fg("muted", ` (~/.omp/agent/ssh.json):`));
|
|
313
|
+
for (const name of userHosts) {
|
|
314
|
+
const config = userConfig.hosts![name];
|
|
315
|
+
const details = this.#formatHostDetails(config);
|
|
316
|
+
lines.push(` ${theme.fg("accent", name)} ${details}`);
|
|
317
|
+
}
|
|
318
|
+
lines.push("");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Show project-level hosts
|
|
322
|
+
if (projectHosts.length > 0) {
|
|
323
|
+
lines.push(theme.fg("accent", "Project level") + theme.fg("muted", ` (.omp/ssh.json):`));
|
|
324
|
+
for (const name of projectHosts) {
|
|
325
|
+
const config = projectConfig.hosts![name];
|
|
326
|
+
const details = this.#formatHostDetails(config);
|
|
327
|
+
lines.push(` ${theme.fg("accent", name)} ${details}`);
|
|
328
|
+
}
|
|
329
|
+
lines.push("");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Show discovered hosts (from ssh.json, .ssh.json in project root, etc.)
|
|
333
|
+
if (discoveredHosts.length > 0) {
|
|
334
|
+
// Group by source
|
|
335
|
+
const bySource = new Map<string, SSHHost[]>();
|
|
336
|
+
for (const host of discoveredHosts) {
|
|
337
|
+
const key = `${host._source.providerName}|${host._source.path}`;
|
|
338
|
+
let group = bySource.get(key);
|
|
339
|
+
if (!group) {
|
|
340
|
+
group = [];
|
|
341
|
+
bySource.set(key, group);
|
|
342
|
+
}
|
|
343
|
+
group.push(host);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const [key, hosts] of bySource) {
|
|
347
|
+
const sepIdx = key.indexOf("|");
|
|
348
|
+
const providerName = key.slice(0, sepIdx);
|
|
349
|
+
const sourcePath = key.slice(sepIdx + 1);
|
|
350
|
+
const shortPath = sourcePath.replace(process.env.HOME ?? "", "~");
|
|
351
|
+
lines.push(
|
|
352
|
+
theme.fg("accent", "Discovered") +
|
|
353
|
+
theme.fg("muted", ` (${providerName}: ${shortPath}):`) +
|
|
354
|
+
theme.fg("dim", " read-only"),
|
|
355
|
+
);
|
|
356
|
+
for (const host of hosts) {
|
|
357
|
+
const details = this.#formatHostDetails({
|
|
358
|
+
host: host.host,
|
|
359
|
+
username: host.username,
|
|
360
|
+
port: host.port,
|
|
361
|
+
});
|
|
362
|
+
lines.push(` ${theme.fg("accent", host.name)} ${details}`);
|
|
363
|
+
}
|
|
364
|
+
lines.push("");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.#showMessage(lines.join("\n"));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.ctx.showError(`Failed to list hosts: ${error instanceof Error ? error.message : String(error)}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Format host details (host, user, port) for display
|
|
376
|
+
*/
|
|
377
|
+
#formatHostDetails(config: { host?: string; username?: string; port?: number }): string {
|
|
378
|
+
const parts: string[] = [];
|
|
379
|
+
if (config.host) parts.push(config.host);
|
|
380
|
+
if (config.username) parts.push(`user=${config.username}`);
|
|
381
|
+
if (config.port && config.port !== 22) parts.push(`port=${config.port}`);
|
|
382
|
+
return theme.fg("dim", parts.length > 0 ? `[${parts.join(", ")}]` : "");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Handle /ssh remove <name> - remove a host from config
|
|
387
|
+
*/
|
|
388
|
+
async #handleRemove(text: string): Promise<void> {
|
|
389
|
+
const match = text.match(/^\/ssh\s+(?:remove|rm)\b\s*(.*)$/i);
|
|
390
|
+
const rest = match?.[1]?.trim() ?? "";
|
|
391
|
+
const tokens = parseCommandArgs(rest);
|
|
392
|
+
|
|
393
|
+
let name: string | undefined;
|
|
394
|
+
let scope: "project" | "user" = "project";
|
|
395
|
+
let i = 0;
|
|
396
|
+
|
|
397
|
+
if (tokens.length > 0 && !tokens[0].startsWith("-")) {
|
|
398
|
+
name = tokens[0];
|
|
399
|
+
i = 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
while (i < tokens.length) {
|
|
403
|
+
const token = tokens[i];
|
|
404
|
+
if (token === "--scope") {
|
|
405
|
+
const value = tokens[i + 1];
|
|
406
|
+
if (!value || (value !== "project" && value !== "user")) {
|
|
407
|
+
this.ctx.showError("Invalid --scope value. Use project or user.");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
scope = value;
|
|
411
|
+
i += 2;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
this.ctx.showError(`Unknown option: ${token}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!name) {
|
|
419
|
+
this.ctx.showError("Host name required. Usage: /ssh remove <name> [--scope project|user]");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const cwd = getProjectDir();
|
|
425
|
+
const filePath = getSSHConfigPath(scope, cwd);
|
|
426
|
+
const config = await readSSHConfigFile(filePath);
|
|
427
|
+
if (!config.hosts?.[name]) {
|
|
428
|
+
this.ctx.showError(`Host "${name}" not found in ${scope} config.`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await removeSSHHost(filePath, name);
|
|
433
|
+
|
|
434
|
+
this.#showMessage(
|
|
435
|
+
["", theme.fg("success", `✓ Removed SSH host "${name}" from ${scope} config`), ""].join("\n"),
|
|
436
|
+
);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
this.ctx.showError(`Failed to remove host: ${error instanceof Error ? error.message : String(error)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Show a message in the chat
|
|
444
|
+
*/
|
|
445
|
+
#showMessage(text: string): void {
|
|
446
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
447
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
448
|
+
this.ctx.chatContainer.addChild(new Text(text, 1, 1));
|
|
449
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
450
|
+
this.ctx.ui.requestRender();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -50,6 +50,7 @@ import { ExtensionUiController } from "./controllers/extension-ui-controller";
|
|
|
50
50
|
import { InputController } from "./controllers/input-controller";
|
|
51
51
|
import { MCPCommandController } from "./controllers/mcp-command-controller";
|
|
52
52
|
import { SelectorController } from "./controllers/selector-controller";
|
|
53
|
+
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
53
54
|
import { setMermaidRenderCallback } from "./theme/mermaid-cache";
|
|
54
55
|
import type { Theme } from "./theme/theme";
|
|
55
56
|
import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
|
|
@@ -1052,6 +1053,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1052
1053
|
await controller.handle(text);
|
|
1053
1054
|
}
|
|
1054
1055
|
|
|
1056
|
+
async handleSSHCommand(text: string): Promise<void> {
|
|
1057
|
+
const controller = new SSHCommandController(this);
|
|
1058
|
+
await controller.handle(text);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1055
1061
|
handleCompactCommand(customInstructions?: string): Promise<void> {
|
|
1056
1062
|
return this.#commandController.handleCompactCommand(customInstructions);
|
|
1057
1063
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -148,6 +148,7 @@ export interface InteractiveModeContext {
|
|
|
148
148
|
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
|
|
149
149
|
handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
|
|
150
150
|
handleMCPCommand(text: string): Promise<void>;
|
|
151
|
+
handleSSHCommand(text: string): Promise<void>;
|
|
151
152
|
handleCompactCommand(customInstructions?: string): Promise<void>;
|
|
152
153
|
handleHandoffCommand(customInstructions?: string): Promise<void>;
|
|
153
154
|
handleMoveCommand(targetPath: string): Promise<void>;
|