@oh-my-pi/pi-coding-agent 3.33.0 → 3.34.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +34 -9
  2. package/docs/custom-tools.md +1 -1
  3. package/docs/extensions.md +4 -4
  4. package/docs/hooks.md +2 -2
  5. package/docs/sdk.md +4 -8
  6. package/examples/custom-tools/README.md +2 -2
  7. package/examples/extensions/README.md +1 -1
  8. package/examples/extensions/todo.ts +1 -1
  9. package/examples/hooks/custom-compaction.ts +4 -2
  10. package/examples/hooks/handoff.ts +1 -1
  11. package/examples/hooks/qna.ts +1 -1
  12. package/examples/sdk/02-custom-model.ts +1 -1
  13. package/examples/sdk/README.md +1 -1
  14. package/package.json +5 -5
  15. package/src/capability/ssh.ts +42 -0
  16. package/src/cli/file-processor.ts +1 -1
  17. package/src/cli/list-models.ts +1 -1
  18. package/src/core/agent-session.ts +19 -5
  19. package/src/core/auth-storage.ts +1 -1
  20. package/src/core/compaction/branch-summarization.ts +2 -2
  21. package/src/core/compaction/compaction.ts +2 -2
  22. package/src/core/compaction/utils.ts +1 -1
  23. package/src/core/custom-tools/types.ts +1 -1
  24. package/src/core/extensions/runner.ts +1 -1
  25. package/src/core/extensions/types.ts +1 -1
  26. package/src/core/extensions/wrapper.ts +1 -1
  27. package/src/core/hooks/runner.ts +2 -2
  28. package/src/core/hooks/types.ts +1 -1
  29. package/src/core/index.ts +11 -0
  30. package/src/core/messages.ts +1 -1
  31. package/src/core/model-registry.ts +1 -1
  32. package/src/core/model-resolver.ts +7 -6
  33. package/src/core/sdk.ts +26 -2
  34. package/src/core/session-manager.ts +1 -1
  35. package/src/core/ssh/connection-manager.ts +466 -0
  36. package/src/core/ssh/ssh-executor.ts +190 -0
  37. package/src/core/ssh/sshfs-mount.ts +162 -0
  38. package/src/core/ssh-executor.ts +5 -0
  39. package/src/core/system-prompt.ts +424 -1
  40. package/src/core/title-generator.ts +2 -2
  41. package/src/core/tools/index.test.ts +1 -0
  42. package/src/core/tools/index.ts +3 -0
  43. package/src/core/tools/output.ts +1 -1
  44. package/src/core/tools/read.ts +24 -11
  45. package/src/core/tools/renderers.ts +2 -0
  46. package/src/core/tools/ssh.ts +302 -0
  47. package/src/core/tools/task/index.ts +1 -1
  48. package/src/core/tools/task/types.ts +1 -1
  49. package/src/core/tools/task/worker.ts +1 -1
  50. package/src/core/voice.ts +1 -1
  51. package/src/discovery/index.ts +3 -0
  52. package/src/discovery/ssh.ts +162 -0
  53. package/src/main.ts +1 -1
  54. package/src/modes/interactive/components/assistant-message.ts +1 -1
  55. package/src/modes/interactive/components/custom-message.ts +1 -1
  56. package/src/modes/interactive/components/footer.ts +1 -1
  57. package/src/modes/interactive/components/hook-message.ts +1 -1
  58. package/src/modes/interactive/components/model-selector.ts +1 -1
  59. package/src/modes/interactive/components/oauth-selector.ts +1 -1
  60. package/src/modes/interactive/components/status-line.ts +1 -1
  61. package/src/modes/interactive/interactive-mode.ts +1 -1
  62. package/src/modes/print-mode.ts +1 -1
  63. package/src/modes/rpc/rpc-client.ts +1 -1
  64. package/src/modes/rpc/rpc-types.ts +1 -1
  65. package/src/prompts/system-prompt.md +4 -0
  66. package/src/prompts/tools/ssh.md +74 -0
  67. package/src/utils/image-resize.ts +1 -1
@@ -0,0 +1,302 @@
1
+ import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
2
+ import type { Component } from "@oh-my-pi/pi-tui";
3
+ import { Text } from "@oh-my-pi/pi-tui";
4
+ import { Type } from "@sinclair/typebox";
5
+ import type { SSHHost } from "../../capability/ssh";
6
+ import { sshCapability } from "../../capability/ssh";
7
+ import { loadSync } from "../../discovery/index";
8
+ import type { Theme } from "../../modes/interactive/theme/theme";
9
+ import sshDescriptionBase from "../../prompts/tools/ssh.md" with { type: "text" };
10
+ import type { RenderResultOptions } from "../custom-tools/types";
11
+ import type { SSHHostInfo } from "../ssh/connection-manager";
12
+ import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
13
+ import { executeSSH } from "../ssh/ssh-executor";
14
+ import type { ToolSession } from "./index";
15
+ import { createToolUIKit } from "./render-utils";
16
+ import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
17
+
18
+ const sshSchema = Type.Object({
19
+ host: Type.String({ description: "Host name from ssh.json or .ssh.json" }),
20
+ command: Type.String({ description: "Command to execute on the remote host" }),
21
+ cwd: Type.Optional(Type.String({ description: "Remote working directory (optional)" })),
22
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
23
+ });
24
+
25
+ export interface SSHToolDetails {
26
+ truncation?: TruncationResult;
27
+ fullOutputPath?: string;
28
+ }
29
+
30
+ function formatHostEntry(host: SSHHost): string {
31
+ const info = getHostInfoForHost(host);
32
+
33
+ let shell: string;
34
+ if (!info) {
35
+ shell = "detecting...";
36
+ } else if (info.os === "windows") {
37
+ if (info.compatEnabled) {
38
+ const compatShell = info.compatShell || "bash";
39
+ shell = `windows/${compatShell}`;
40
+ } else if (info.shell === "powershell") {
41
+ shell = "windows/powershell";
42
+ } else {
43
+ shell = "windows/cmd";
44
+ }
45
+ } else if (info.os === "linux") {
46
+ shell = `linux/${info.shell}`;
47
+ } else if (info.os === "macos") {
48
+ shell = `macos/${info.shell}`;
49
+ } else {
50
+ shell = `unknown/${info.shell}`;
51
+ }
52
+
53
+ return `- ${host.name} (${host.host}) | ${shell}`;
54
+ }
55
+
56
+ function formatDescription(hosts: SSHHost[]): string {
57
+ if (hosts.length === 0) {
58
+ return sshDescriptionBase;
59
+ }
60
+ const hostList = hosts.map(formatHostEntry).join("\n");
61
+ return `${sshDescriptionBase}\n\nAvailable hosts:\n${hostList}`;
62
+ }
63
+
64
+ function quoteRemotePath(value: string): string {
65
+ if (value.length === 0) {
66
+ return "''";
67
+ }
68
+ const escaped = value.replace(/'/g, "'\\''");
69
+ return `'${escaped}'`;
70
+ }
71
+
72
+ function quotePowerShellPath(value: string): string {
73
+ if (value.length === 0) {
74
+ return "''";
75
+ }
76
+ const escaped = value.replace(/'/g, "''");
77
+ return `'${escaped}'`;
78
+ }
79
+
80
+ function quoteCmdPath(value: string): string {
81
+ const escaped = value.replace(/"/g, '""');
82
+ return `"${escaped}"`;
83
+ }
84
+
85
+ function buildRemoteCommand(command: string, cwd: string | undefined, info: SSHHostInfo): string {
86
+ if (!cwd) return command;
87
+
88
+ if (info.os === "windows" && !info.compatEnabled) {
89
+ if (info.shell === "powershell") {
90
+ return `Set-Location -Path ${quotePowerShellPath(cwd)}; ${command}`;
91
+ }
92
+ return `cd /d ${quoteCmdPath(cwd)} && ${command}`;
93
+ }
94
+
95
+ return `cd -- ${quoteRemotePath(cwd)} && ${command}`;
96
+ }
97
+
98
+ function loadHosts(session: ToolSession): {
99
+ hostNames: string[];
100
+ hostsByName: Map<string, SSHHost>;
101
+ } {
102
+ const result = loadSync<SSHHost>(sshCapability.id, { cwd: session.cwd });
103
+ const hostsByName = new Map<string, SSHHost>();
104
+ for (const host of result.items) {
105
+ if (!hostsByName.has(host.name)) {
106
+ hostsByName.set(host.name, host);
107
+ }
108
+ }
109
+ const hostNames = Array.from(hostsByName.keys()).sort();
110
+ return { hostNames, hostsByName };
111
+ }
112
+
113
+ export function createSshTool(session: ToolSession): AgentTool<typeof sshSchema> | null {
114
+ const { hostNames, hostsByName } = loadHosts(session);
115
+ if (hostNames.length === 0) {
116
+ return null;
117
+ }
118
+
119
+ const allowedHosts = new Set(hostNames);
120
+
121
+ const descriptionHosts = hostNames
122
+ .map((name) => hostsByName.get(name))
123
+ .filter((host): host is SSHHost => host !== undefined);
124
+
125
+ return {
126
+ name: "ssh",
127
+ label: "SSH",
128
+ description: formatDescription(descriptionHosts),
129
+ parameters: sshSchema,
130
+ execute: async (
131
+ _toolCallId: string,
132
+ { host, command, cwd, timeout }: { host: string; command: string; cwd?: string; timeout?: number },
133
+ signal?: AbortSignal,
134
+ onUpdate?,
135
+ _ctx?: AgentToolContext,
136
+ ) => {
137
+ if (!allowedHosts.has(host)) {
138
+ throw new Error(`Unknown SSH host: ${host}. Available hosts: ${hostNames.join(", ")}`);
139
+ }
140
+
141
+ const hostConfig = hostsByName.get(host);
142
+ if (!hostConfig) {
143
+ throw new Error(`SSH host not loaded: ${host}`);
144
+ }
145
+
146
+ const hostInfo = await ensureHostInfo(hostConfig);
147
+ const remoteCommand = buildRemoteCommand(command, cwd, hostInfo);
148
+ let currentOutput = "";
149
+
150
+ const result = await executeSSH(hostConfig, remoteCommand, {
151
+ timeout: timeout ? timeout * 1000 : undefined,
152
+ signal,
153
+ compatEnabled: hostInfo.compatEnabled,
154
+ onChunk: (chunk) => {
155
+ currentOutput += chunk;
156
+ if (onUpdate) {
157
+ const truncation = truncateTail(currentOutput);
158
+ onUpdate({
159
+ content: [{ type: "text", text: truncation.content || "" }],
160
+ details: {
161
+ truncation: truncation.truncated ? truncation : undefined,
162
+ },
163
+ });
164
+ }
165
+ },
166
+ });
167
+
168
+ if (result.cancelled) {
169
+ throw new Error(result.output || "Command aborted");
170
+ }
171
+
172
+ const truncation = truncateTail(result.output);
173
+ let outputText = truncation.content || "(no output)";
174
+
175
+ let details: SSHToolDetails | undefined;
176
+
177
+ if (truncation.truncated) {
178
+ details = {
179
+ truncation,
180
+ fullOutputPath: result.fullOutputPath,
181
+ };
182
+
183
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
184
+ const endLine = truncation.totalLines;
185
+
186
+ if (truncation.lastLinePartial) {
187
+ const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
188
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${result.fullOutputPath}]`;
189
+ } else if (truncation.truncatedBy === "lines") {
190
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${result.fullOutputPath}]`;
191
+ } else {
192
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${result.fullOutputPath}]`;
193
+ }
194
+ }
195
+
196
+ if (result.exitCode !== 0 && result.exitCode !== undefined) {
197
+ outputText += `\n\nCommand exited with code ${result.exitCode}`;
198
+ throw new Error(outputText);
199
+ }
200
+
201
+ return { content: [{ type: "text", text: outputText }], details };
202
+ },
203
+ };
204
+ }
205
+
206
+ // =============================================================================
207
+ // TUI Renderer
208
+ // =============================================================================
209
+
210
+ interface SshRenderArgs {
211
+ host?: string;
212
+ command?: string;
213
+ timeout?: number;
214
+ }
215
+
216
+ interface SshRenderContext {
217
+ /** Visual lines for truncated output (pre-computed by tool-execution) */
218
+ visualLines?: string[];
219
+ /** Number of lines skipped */
220
+ skippedCount?: number;
221
+ /** Total visual lines */
222
+ totalVisualLines?: number;
223
+ }
224
+
225
+ export const sshToolRenderer = {
226
+ renderCall(args: SshRenderArgs, uiTheme: Theme): Component {
227
+ const ui = createToolUIKit(uiTheme);
228
+ const host = args.host || uiTheme.format.ellipsis;
229
+ const command = args.command || uiTheme.format.ellipsis;
230
+ const text = ui.title(`[${host}] $ ${command}`);
231
+ return new Text(text, 0, 0);
232
+ },
233
+
234
+ renderResult(
235
+ result: {
236
+ content: Array<{ type: string; text?: string }>;
237
+ details?: SSHToolDetails;
238
+ },
239
+ options: RenderResultOptions & { renderContext?: SshRenderContext },
240
+ uiTheme: Theme,
241
+ ): Component {
242
+ const ui = createToolUIKit(uiTheme);
243
+ const { expanded, renderContext } = options;
244
+ const details = result.details;
245
+ const lines: string[] = [];
246
+
247
+ const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
248
+ const output = textContent.trim();
249
+
250
+ if (output) {
251
+ if (expanded) {
252
+ const styledOutput = output
253
+ .split("\n")
254
+ .map((line) => uiTheme.fg("toolOutput", line))
255
+ .join("\n");
256
+ lines.push(styledOutput);
257
+ } else if (renderContext?.visualLines) {
258
+ const { visualLines, skippedCount = 0, totalVisualLines = visualLines.length } = renderContext;
259
+ if (skippedCount > 0) {
260
+ lines.push(
261
+ uiTheme.fg(
262
+ "dim",
263
+ `${uiTheme.format.ellipsis} (${skippedCount} earlier lines, showing ${visualLines.length} of ${totalVisualLines}) (ctrl+o to expand)`,
264
+ ),
265
+ );
266
+ }
267
+ lines.push(...visualLines);
268
+ } else {
269
+ const outputLines = output.split("\n");
270
+ const maxLines = 5;
271
+ const displayLines = outputLines.slice(0, maxLines);
272
+ const remaining = outputLines.length - maxLines;
273
+
274
+ lines.push(...displayLines.map((line) => uiTheme.fg("toolOutput", line)));
275
+ if (remaining > 0) {
276
+ lines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${remaining} more lines) (ctrl+o to expand)`));
277
+ }
278
+ }
279
+ }
280
+
281
+ const truncation = details?.truncation;
282
+ const fullOutputPath = details?.fullOutputPath;
283
+ if (truncation?.truncated || fullOutputPath) {
284
+ const warnings: string[] = [];
285
+ if (fullOutputPath) {
286
+ warnings.push(`Full output: ${fullOutputPath}`);
287
+ }
288
+ if (truncation?.truncated) {
289
+ if (truncation.truncatedBy === "lines") {
290
+ warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
291
+ } else {
292
+ warnings.push(
293
+ `Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
294
+ );
295
+ }
296
+ }
297
+ lines.push(uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". "))));
298
+ }
299
+
300
+ return new Text(lines.join("\n"), 0, 0);
301
+ },
302
+ };
@@ -13,8 +13,8 @@
13
13
  * - Session artifacts for debugging
14
14
  */
15
15
 
16
- import type { Usage } from "@mariozechner/pi-ai";
17
16
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
+ import type { Usage } from "@oh-my-pi/pi-ai";
18
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
19
19
  import taskDescriptionTemplate from "../../../prompts/tools/task.md" with { type: "text" };
20
20
  import { formatDuration } from "../render-utils";
@@ -1,4 +1,4 @@
1
- import type { Usage } from "@mariozechner/pi-ai";
1
+ import type { Usage } from "@oh-my-pi/pi-ai";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
3
 
4
4
  /** Source of an agent definition */
@@ -13,8 +13,8 @@
13
13
  * 5. Parent can send { type: "abort" } to request cancellation
14
14
  */
15
15
 
16
- import type { Api, Model } from "@mariozechner/pi-ai";
17
16
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
18
18
  import type { AgentSessionEvent } from "../../agent-session";
19
19
  import { parseModelPattern, parseModelString } from "../../model-resolver";
20
20
  import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
package/src/core/voice.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { unlinkSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { completeSimple, type Model } from "@mariozechner/pi-ai";
4
+ import { completeSimple, type Model } from "@oh-my-pi/pi-ai";
5
5
  import { nanoid } from "nanoid";
6
6
  import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
7
7
  import { logger } from "./logger";
@@ -18,6 +18,7 @@ import "../capability/settings";
18
18
  import "../capability/skill";
19
19
  import "../capability/slash-command";
20
20
  import "../capability/system-prompt";
21
+ import "../capability/ssh";
21
22
  import "../capability/tool";
22
23
 
23
24
  // Import providers (each registers itself on import)
@@ -32,6 +33,7 @@ import "./github";
32
33
  import "./vscode";
33
34
  import "./agents-md";
34
35
  import "./mcp-json";
36
+ import "./ssh";
35
37
 
36
38
  export type { ContextFile } from "../capability/context-file";
37
39
  export type { Extension, ExtensionManifest } from "../capability/extension";
@@ -70,6 +72,7 @@ export type { Rule, RuleFrontmatter } from "../capability/rule";
70
72
  export type { Settings } from "../capability/settings";
71
73
  export type { Skill, SkillFrontmatter } from "../capability/skill";
72
74
  export type { SlashCommand } from "../capability/slash-command";
75
+ export type { SSHHost } from "../capability/ssh";
73
76
  export type { SystemPrompt } from "../capability/system-prompt";
74
77
  export type { CustomTool } from "../capability/tool";
75
78
  // Re-export types
@@ -0,0 +1,162 @@
1
+ /**
2
+ * SSH JSON Provider
3
+ *
4
+ * Discovers SSH hosts from ssh.json or .ssh.json in the project root.
5
+ * Priority: 5 (low, project-level only)
6
+ */
7
+
8
+ import { join } from "node:path";
9
+ import { registerProvider } from "../capability/index";
10
+ import { type SSHHost, sshCapability } from "../capability/ssh";
11
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
12
+ import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
13
+
14
+ const PROVIDER_ID = "ssh-json";
15
+ const DISPLAY_NAME = "SSH Config";
16
+
17
+ interface SSHConfigFile {
18
+ hosts?: Record<
19
+ string,
20
+ {
21
+ host?: string;
22
+ username?: string;
23
+ port?: number | string;
24
+ compat?: boolean | string;
25
+ key?: string;
26
+ keyPath?: string;
27
+ description?: string;
28
+ }
29
+ >;
30
+ }
31
+
32
+ function expandTilde(value: string, home: string): string {
33
+ if (value === "~") return home;
34
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
35
+ return `${home}${value.slice(1)}`;
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function parsePort(value: number | string | undefined): number | undefined {
41
+ if (value === undefined) return undefined;
42
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
43
+ const parsed = Number.parseInt(value, 10);
44
+ return Number.isNaN(parsed) ? undefined : parsed;
45
+ }
46
+
47
+ function parseCompat(value: boolean | string | undefined): boolean | undefined {
48
+ if (value === undefined) return undefined;
49
+ if (typeof value === "boolean") return value;
50
+ const normalized = value.trim().toLowerCase();
51
+ if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
52
+ if (normalized === "false" || normalized === "0" || normalized === "no") return false;
53
+ return undefined;
54
+ }
55
+
56
+ function normalizeHost(
57
+ name: string,
58
+ raw: NonNullable<SSHConfigFile["hosts"]>[string],
59
+ source: SourceMeta,
60
+ home: string,
61
+ warnings: string[],
62
+ ): SSHHost | null {
63
+ if (!raw.host) {
64
+ warnings.push(`Missing host for SSH entry: ${name}`);
65
+ return null;
66
+ }
67
+
68
+ const port = parsePort(raw.port);
69
+ if (raw.port !== undefined && port === undefined) {
70
+ warnings.push(`Invalid port for SSH entry ${name}: ${String(raw.port)}`);
71
+ }
72
+
73
+ const compat = parseCompat(raw.compat);
74
+ if (raw.compat !== undefined && compat === undefined) {
75
+ warnings.push(`Invalid compat flag for SSH entry ${name}: ${String(raw.compat)}`);
76
+ }
77
+
78
+ const keyValue = raw.keyPath ?? raw.key;
79
+ const keyPath = keyValue ? expandTilde(keyValue, home) : undefined;
80
+
81
+ return {
82
+ name,
83
+ host: raw.host,
84
+ username: raw.username,
85
+ port,
86
+ keyPath,
87
+ description: raw.description,
88
+ compat,
89
+ _source: source,
90
+ };
91
+ }
92
+
93
+ function loadSshJsonFile(ctx: LoadContext, path: string): LoadResult<SSHHost> {
94
+ const items: SSHHost[] = [];
95
+ const warnings: string[] = [];
96
+
97
+ if (!ctx.fs.isFile(path)) {
98
+ return { items, warnings };
99
+ }
100
+
101
+ const content = ctx.fs.readFile(path);
102
+ if (content === null) {
103
+ warnings.push(`Failed to read ${path}`);
104
+ return { items, warnings };
105
+ }
106
+
107
+ const parsed = parseJSON<SSHConfigFile>(content);
108
+ if (!parsed) {
109
+ warnings.push(`Failed to parse JSON in ${path}`);
110
+ return { items, warnings };
111
+ }
112
+
113
+ const config = expandEnvVarsDeep(parsed);
114
+ if (!config.hosts || typeof config.hosts !== "object") {
115
+ warnings.push(`Missing hosts in ${path}`);
116
+ return { items, warnings };
117
+ }
118
+
119
+ const source = createSourceMeta(PROVIDER_ID, path, "project");
120
+ for (const [name, rawHost] of Object.entries(config.hosts)) {
121
+ if (!name.trim()) {
122
+ warnings.push(`Invalid SSH host name in ${path}`);
123
+ continue;
124
+ }
125
+ if (!rawHost || typeof rawHost !== "object") {
126
+ warnings.push(`Invalid host entry in ${path}: ${name}`);
127
+ continue;
128
+ }
129
+ const host = normalizeHost(name, rawHost, source, ctx.home, warnings);
130
+ if (host) items.push(host);
131
+ }
132
+
133
+ return {
134
+ items,
135
+ warnings: warnings.length > 0 ? warnings : undefined,
136
+ };
137
+ }
138
+
139
+ function load(ctx: LoadContext): LoadResult<SSHHost> {
140
+ const allItems: SSHHost[] = [];
141
+ const allWarnings: string[] = [];
142
+
143
+ for (const filename of ["ssh.json", ".ssh.json"]) {
144
+ const path = join(ctx.cwd, filename);
145
+ const result = loadSshJsonFile(ctx, path);
146
+ allItems.push(...result.items);
147
+ if (result.warnings) allWarnings.push(...result.warnings);
148
+ }
149
+
150
+ return {
151
+ items: allItems,
152
+ warnings: allWarnings.length > 0 ? allWarnings : undefined,
153
+ };
154
+ }
155
+
156
+ registerProvider(sshCapability.id, {
157
+ id: PROVIDER_ID,
158
+ displayName: DISPLAY_NAME,
159
+ description: "Load SSH hosts from ssh.json or .ssh.json in the project root",
160
+ priority: 5,
161
+ load,
162
+ });
package/src/main.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { join, resolve } from "node:path";
10
- import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
10
+ import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
11
11
  import chalk from "chalk";
12
12
  import { type Args, parseArgs, printHelp } from "./cli/args";
13
13
  import { processFileArguments } from "./cli/file-processor";
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage } from "@mariozechner/pi-ai";
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
3
3
  import { getMarkdownTheme, theme } from "../theme/theme";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { TextContent } from "@mariozechner/pi-ai";
1
+ import type { TextContent } from "@oh-my-pi/pi-ai";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import type { MessageRenderer } from "../../../core/extensions/types";
@@ -1,5 +1,5 @@
1
1
  import { existsSync, type FSWatcher, readFileSync, watch } from "node:fs";
2
- import type { AssistantMessage } from "@mariozechner/pi-ai";
2
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
3
3
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
4
4
  import { dirname, join } from "path";
5
5
  import type { AgentSession } from "../../../core/agent-session";
@@ -1,4 +1,4 @@
1
- import type { TextContent } from "@mariozechner/pi-ai";
1
+ import type { TextContent } from "@oh-my-pi/pi-ai";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Box, Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
4
4
  import type { HookMessageRenderer } from "../../../core/hooks/types";
@@ -1,4 +1,4 @@
1
- import { type Model, modelsAreEqual } from "@mariozechner/pi-ai";
1
+ import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
2
2
  import {
3
3
  Container,
4
4
  Input,
@@ -1,4 +1,4 @@
1
- import { getOAuthProviders, type OAuthProviderInfo } from "@mariozechner/pi-ai";
1
+ import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
2
2
  import { Container, isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
3
  import type { AuthStorage } from "../../../core/auth-storage";
4
4
  import { theme } from "../theme/theme";
@@ -1,4 +1,4 @@
1
- import type { AssistantMessage } from "@mariozechner/pi-ai";
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
3
3
  import { type FSWatcher, watch } from "fs";
4
4
  import { dirname, join } from "path";
@@ -6,8 +6,8 @@
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@mariozechner/pi-ai";
10
9
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
10
+ import type { AssistantMessage, ImageContent, Message, OAuthProvider } from "@oh-my-pi/pi-ai";
11
11
  import type { SlashCommand } from "@oh-my-pi/pi-tui";
12
12
  import {
13
13
  CombinedAutocompleteProvider,
@@ -6,7 +6,7 @@
6
6
  * - `omp --mode json "prompt"` - JSON event stream
7
7
  */
8
8
 
9
- import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai";
9
+ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
10
10
  import type { AgentSession } from "../core/agent-session";
11
11
 
12
12
  /**
@@ -4,8 +4,8 @@
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
6
 
7
- import type { ImageContent } from "@mariozechner/pi-ai";
8
7
  import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
9
9
  import type { Subprocess } from "bun";
10
10
  import type { SessionStats } from "../../core/agent-session";
11
11
  import type { BashResult } from "../../core/bash-executor";
@@ -5,8 +5,8 @@
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
7
 
8
- import type { ImageContent, Model } from "@mariozechner/pi-ai";
9
8
  import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
+ import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
10
10
  import type { SessionStats } from "../../core/agent-session";
11
11
  import type { BashResult } from "../../core/bash-executor";
12
12
  import type { CompactionResult } from "../../core/compaction/index";
@@ -21,6 +21,10 @@ Core behavior:
21
21
  - After each tool result, check relevance; iterate or clarify if results conflict or are insufficient.
22
22
  - Use concise, scannable responses; include file paths in backticks; use short bullets for multi-item lists; avoid dumping large files.
23
23
 
24
+ <environment>
25
+ {{environmentInfo}}
26
+ </environment>
27
+
24
28
  Documentation:
25
29
  - Main documentation: {{readmePath}}
26
30
  - Additional docs: {{docsPath}}