@xynogen/pix-ssh 0.5.5 → 0.5.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/package.json +4 -4
- package/src/index.ts +56 -53
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-ssh",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Pi tool
|
|
3
|
+
"version": "0.5.7",
|
|
4
|
+
"description": "Pi tool \u2014 ssh_run: run remote commands over SSH with password/key auth and remote sudo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@xynogen/pix-pretty": "^1.
|
|
41
|
-
"@xynogen/pix-runtime": "^0.
|
|
40
|
+
"@xynogen/pix-pretty": "^1.30.0",
|
|
41
|
+
"@xynogen/pix-runtime": "^0.13.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"typebox": "*",
|
package/src/index.ts
CHANGED
|
@@ -27,24 +27,28 @@
|
|
|
27
27
|
|
|
28
28
|
import type { AgentToolUpdateCallback, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import { Text } from "@earendil-works/pi-tui";
|
|
30
|
-
import {
|
|
30
|
+
import { resolveBaseBackground } from "@xynogen/pix-pretty/ansi";
|
|
31
|
+
import { collapsedCommandRow, commandPreview } from "@xynogen/pix-pretty/command-preview";
|
|
31
32
|
import { MAX_PREVIEW_LINES } from "@xynogen/pix-pretty/config";
|
|
32
33
|
import { type OverlayResult, showOverlay } from "@xynogen/pix-pretty/gate-overlay";
|
|
33
34
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
34
35
|
import { renderBashOutput } from "@xynogen/pix-pretty/renderers";
|
|
35
36
|
import type { RenderContextLike, ThemeLike, ToolResultLike } from "@xynogen/pix-pretty/types";
|
|
36
37
|
import {
|
|
38
|
+
BODY_PAD,
|
|
39
|
+
bodyLine,
|
|
37
40
|
dotJoin,
|
|
38
41
|
fillToolBackground,
|
|
42
|
+
formatCollapsedToolRow,
|
|
43
|
+
formatToolCallTitle,
|
|
39
44
|
frameToolResult,
|
|
40
45
|
getErrorMessage,
|
|
41
46
|
getTextContent,
|
|
42
47
|
hideCollapsedToolCall,
|
|
48
|
+
moreLines,
|
|
43
49
|
normalizeLineEndings,
|
|
44
|
-
renderCollapsedToolRow,
|
|
45
50
|
renderToolError,
|
|
46
51
|
ruleFrame,
|
|
47
|
-
sectionRule,
|
|
48
52
|
termW,
|
|
49
53
|
unframeToolResult,
|
|
50
54
|
} from "@xynogen/pix-pretty/utils";
|
|
@@ -400,75 +404,70 @@ export default function (pi: ExtensionAPI): void {
|
|
|
400
404
|
...({ exposure: "deferred" } as const),
|
|
401
405
|
label: "Run over SSH",
|
|
402
406
|
description:
|
|
403
|
-
"
|
|
404
|
-
"
|
|
405
|
-
"
|
|
406
|
-
"
|
|
407
|
-
|
|
408
|
-
"
|
|
409
|
-
'To discover hosts without reading `~/.ssh/config`, use `action: "info"` — omit `host` to list configured aliases, or pass a `host` to get its effective config (no connection). ' +
|
|
410
|
-
"Always provide a clear `reason`.",
|
|
407
|
+
"Operate on a REMOTE host over SSH (for the LOCAL machine use `bash`, or `sudo_run` for local root). " +
|
|
408
|
+
"Pick one `action`; each has its own parameters:\n" +
|
|
409
|
+
"- command: run a command. Required: host, command. Optional: sudo (remote POSIX root).\n" +
|
|
410
|
+
"- file: upload/download a file or directory (may overwrite the destination). Required: host, direction, source, destination. Optional: recursive.\n" +
|
|
411
|
+
"- info: read SSH config, no connection. Optional: host (omit = list aliases, set = effective config).\n" +
|
|
412
|
+
"Always set `reason`. Windows/PowerShell remotes are best-effort.",
|
|
411
413
|
promptSnippet: "Run a remote command, transfer files, or read SSH config over SSH",
|
|
412
414
|
promptGuidelines: [
|
|
413
|
-
'ssh_run: REMOTE host only — use `bash`/`sudo_run`
|
|
415
|
+
'ssh_run: REMOTE host only — use `bash`/`sudo_run` locally. `action` is required: "command" (host, command, sudo?), "file" (host, direction, source, destination, recursive?), "info" (host? — no host = list aliases). `host` is `[user@]host[:port]`. Use "info" instead of reading `~/.ssh/config`. Always set `reason`.',
|
|
414
416
|
],
|
|
415
417
|
|
|
416
418
|
renderShell: "self",
|
|
417
419
|
|
|
418
|
-
// Single Type.Object (root `type: "object"`) rather than Type.Union — a union
|
|
420
|
+
// Single flat Type.Object (root `type: "object"`) rather than Type.Union — a union
|
|
419
421
|
// serializes to `anyOf` with no root type, which strict OpenAI-compatible
|
|
420
|
-
// providers (e.g. DeepSeek) reject with `type: null`.
|
|
421
|
-
//
|
|
422
|
+
// providers (e.g. DeepSeek) reject with `type: null`. Only `action` is
|
|
423
|
+
// schema-required; per-action fields are tagged in their descriptions and
|
|
424
|
+
// validated at runtime via normalizeOperation.
|
|
422
425
|
parameters: Type.Object({
|
|
423
|
-
action: Type.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}),
|
|
428
|
-
),
|
|
426
|
+
action: Type.Union([Type.Literal("command"), Type.Literal("file"), Type.Literal("info")], {
|
|
427
|
+
description:
|
|
428
|
+
'"command" runs a remote command, "file" transfers a file/directory, "info" reads SSH config (no connection).',
|
|
429
|
+
}),
|
|
429
430
|
host: Type.Optional(
|
|
430
431
|
Type.String({
|
|
431
432
|
description:
|
|
432
|
-
"
|
|
433
|
+
"`[user@]host[:port]`, e.g. `deploy@10.0.0.5:2222`. REQUIRED for command and file. For info: omit to list aliases, set to resolve one host.",
|
|
433
434
|
}),
|
|
434
435
|
),
|
|
436
|
+
// command
|
|
435
437
|
command: Type.Optional(
|
|
436
438
|
Type.String({
|
|
437
|
-
description:
|
|
438
|
-
'Command sent to the remote host\'s configured SSH shell. Required when action is "command".',
|
|
439
|
+
description: "[command] REQUIRED. Sent to the remote host's configured SSH shell.",
|
|
439
440
|
}),
|
|
440
441
|
),
|
|
441
442
|
sudo: Type.Optional(
|
|
442
443
|
Type.Boolean({
|
|
443
|
-
description:
|
|
444
|
-
"Run the command as root on the remote host via sudo. Default false. Command action only.",
|
|
444
|
+
description: "[command] Optional, default false. Run as root on the remote via sudo.",
|
|
445
445
|
}),
|
|
446
446
|
),
|
|
447
|
+
// file
|
|
447
448
|
direction: Type.Optional(
|
|
448
449
|
Type.Union([Type.Literal("upload"), Type.Literal("download")], {
|
|
449
|
-
description:
|
|
450
|
+
description: "[file] REQUIRED. Transfer direction.",
|
|
450
451
|
}),
|
|
451
452
|
),
|
|
452
453
|
source: Type.Optional(
|
|
453
454
|
Type.String({
|
|
454
|
-
description:
|
|
455
|
-
'Source path (local for upload, remote for download). Required when action is "file".',
|
|
455
|
+
description: "[file] REQUIRED. Local path for upload, remote path for download.",
|
|
456
456
|
}),
|
|
457
457
|
),
|
|
458
458
|
destination: Type.Optional(
|
|
459
459
|
Type.String({
|
|
460
|
-
description:
|
|
461
|
-
'Destination path (remote for upload, local for download). Required when action is "file".',
|
|
460
|
+
description: "[file] REQUIRED. Remote path for upload, local path for download.",
|
|
462
461
|
}),
|
|
463
462
|
),
|
|
464
463
|
recursive: Type.Optional(
|
|
465
464
|
Type.Boolean({
|
|
466
|
-
description: "
|
|
465
|
+
description: "[file] Optional, default false. Copy a directory recursively.",
|
|
467
466
|
}),
|
|
468
467
|
),
|
|
469
468
|
reason: Type.Optional(
|
|
470
469
|
Type.String({
|
|
471
|
-
description: "Short plain-English
|
|
470
|
+
description: "All actions. Short plain-English intent, shown to the user.",
|
|
472
471
|
}),
|
|
473
472
|
),
|
|
474
473
|
}),
|
|
@@ -884,20 +883,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
884
883
|
if (args.action === "info") {
|
|
885
884
|
text.setText(
|
|
886
885
|
fillToolBackground(
|
|
887
|
-
`${theme
|
|
886
|
+
`${formatToolCallTitle(theme, "ssh info", renderCtx)} ${theme.fg("dim", host || "list aliases")}`,
|
|
888
887
|
),
|
|
889
888
|
);
|
|
890
889
|
return text;
|
|
891
890
|
}
|
|
892
891
|
const operation = normalizeOperation(args);
|
|
893
|
-
const command =
|
|
892
|
+
const command = operation.command || "(empty command)";
|
|
894
893
|
const prefix = operation.sudo ? "sudo " : "";
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
894
|
+
return commandPreview(
|
|
895
|
+
`${formatToolCallTitle(theme, operation.action === "file" ? "ssh file" : "ssh", renderCtx)} ${theme.fg("dim", host)}`,
|
|
896
|
+
prefix + command,
|
|
897
|
+
"bash",
|
|
898
|
+
theme,
|
|
899
|
+
renderCtx.state,
|
|
900
|
+
renderCtx.invalidate,
|
|
901
|
+
renderCtx.expanded,
|
|
899
902
|
);
|
|
900
|
-
return text;
|
|
901
903
|
}) as never,
|
|
902
904
|
|
|
903
905
|
renderResult: ((
|
|
@@ -917,7 +919,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
917
919
|
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
918
920
|
} else {
|
|
919
921
|
text.setText(
|
|
920
|
-
fillToolBackground(
|
|
922
|
+
fillToolBackground(`${BODY_PAD}${theme.fg("muted", getTextContent(result) || "done")}`),
|
|
921
923
|
);
|
|
922
924
|
}
|
|
923
925
|
return isPartial ? text : completed(renderCtx.isError);
|
|
@@ -934,21 +936,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
934
936
|
)
|
|
935
937
|
) {
|
|
936
938
|
const status = details.outcome === "success" ? "success" : "error";
|
|
937
|
-
|
|
938
|
-
|
|
939
|
+
return collapsedCommandRow(
|
|
940
|
+
formatCollapsedToolRow(
|
|
939
941
|
theme,
|
|
940
942
|
"ssh",
|
|
941
|
-
|
|
943
|
+
details.host,
|
|
942
944
|
terminalMeta(details),
|
|
943
945
|
status,
|
|
944
|
-
),
|
|
946
|
+
).trimEnd(),
|
|
947
|
+
safeOneLine(details.command),
|
|
948
|
+
theme,
|
|
945
949
|
);
|
|
946
|
-
return text;
|
|
947
950
|
}
|
|
948
951
|
|
|
949
952
|
if (details.outcome === "awaiting-approval" || details.outcome === "running") {
|
|
950
953
|
text.setText(
|
|
951
|
-
fillToolBackground(
|
|
954
|
+
fillToolBackground(
|
|
955
|
+
`${BODY_PAD}${theme.fg("muted", getTextContent(result) || "working")}`,
|
|
956
|
+
),
|
|
952
957
|
);
|
|
953
958
|
return text;
|
|
954
959
|
}
|
|
@@ -958,7 +963,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
958
963
|
text.setText(
|
|
959
964
|
details.outcome === "error"
|
|
960
965
|
? renderToolError(diagnostic, theme)
|
|
961
|
-
: fillToolBackground(
|
|
966
|
+
: fillToolBackground(`${BODY_PAD}${theme.fg("warning", diagnostic)}`),
|
|
962
967
|
);
|
|
963
968
|
return isPartial ? text : completed(true);
|
|
964
969
|
}
|
|
@@ -970,18 +975,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
970
975
|
const lineCount = lines.length;
|
|
971
976
|
|
|
972
977
|
if (!rendered) {
|
|
973
|
-
text.setText(fillToolBackground(
|
|
978
|
+
text.setText(fillToolBackground(`${BODY_PAD}${summary}`));
|
|
974
979
|
return isPartial ? text : completed(details.outcome !== "success");
|
|
975
980
|
}
|
|
976
981
|
|
|
977
982
|
const maxShow = renderCtx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
978
983
|
const show = lines.slice(0, maxShow);
|
|
979
|
-
const footer =
|
|
980
|
-
lineCount > maxShow ? [`${FG_DIM} … ${lineCount - maxShow} more lines${RST}`] : [];
|
|
984
|
+
const footer = lineCount > maxShow ? [moreLines(lineCount - maxShow, theme)] : [];
|
|
981
985
|
const statusKey = details.outcome === "success" ? "success" : "error";
|
|
982
986
|
const paint = (s: string) => theme.fg(statusKey, s);
|
|
983
|
-
const
|
|
984
|
-
const body = show.map((line) => ` ${sectionRule(line, theme, sw) ?? line}`);
|
|
987
|
+
const body = show.map((line) => bodyLine(line, theme));
|
|
985
988
|
const out = isPartial ? [...body, ...footer] : ruleFrame(body, footer, termW(), paint);
|
|
986
989
|
text.setText(fillToolBackground(out.join("\n")));
|
|
987
990
|
return text;
|