@oh-my-pi/pi-coding-agent 3.33.0 → 3.35.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 +57 -8
- package/docs/custom-tools.md +1 -1
- package/docs/extensions.md +4 -4
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +4 -8
- package/examples/custom-tools/README.md +2 -2
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/todo.ts +1 -1
- package/examples/hooks/custom-compaction.ts +4 -2
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/capability/ssh.ts +42 -0
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +214 -31
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/index.ts +11 -0
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +7 -6
- package/src/core/sdk.ts +33 -4
- package/src/core/session-manager.ts +16 -1
- package/src/core/settings-manager.ts +20 -6
- package/src/core/ssh/connection-manager.ts +466 -0
- package/src/core/ssh/ssh-executor.ts +190 -0
- package/src/core/ssh/sshfs-mount.ts +162 -0
- package/src/core/ssh-executor.ts +5 -0
- package/src/core/system-prompt.ts +424 -1
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/edit.ts +1 -0
- package/src/core/tools/grep.ts +1 -1
- package/src/core/tools/index.test.ts +1 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/output.ts +1 -1
- package/src/core/tools/read.ts +24 -11
- package/src/core/tools/renderers.ts +3 -0
- package/src/core/tools/ssh.ts +302 -0
- package/src/core/tools/task/index.ts +11 -2
- package/src/core/tools/task/model-resolver.ts +5 -4
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/task/worker.ts +1 -1
- package/src/core/voice.ts +1 -1
- package/src/discovery/index.ts +3 -0
- package/src/discovery/ssh.ts +162 -0
- package/src/main.ts +4 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +15 -12
- package/src/modes/interactive/interactive-mode.ts +43 -9
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/tools/ssh.md +74 -0
- 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";
|
|
@@ -135,7 +135,12 @@ export async function createTaskTool(
|
|
|
135
135
|
const startTime = Date.now();
|
|
136
136
|
const { agents, projectAgentsDir } = await discoverAgents(session.cwd);
|
|
137
137
|
const { agent: agentName, context, model, output: outputSchema } = params;
|
|
138
|
-
|
|
138
|
+
|
|
139
|
+
const isDefaultModelAlias = (value: string | undefined): boolean => {
|
|
140
|
+
if (!value) return true;
|
|
141
|
+
const normalized = value.trim().toLowerCase();
|
|
142
|
+
return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
|
|
143
|
+
};
|
|
139
144
|
|
|
140
145
|
// Validate agent exists
|
|
141
146
|
const agent = getAgent(agents, agentName);
|
|
@@ -156,6 +161,10 @@ export async function createTaskTool(
|
|
|
156
161
|
};
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
const shouldInheritSessionModel = model === undefined && isDefaultModelAlias(agent.model);
|
|
165
|
+
const sessionModel = shouldInheritSessionModel ? session.getActiveModelString?.() : undefined;
|
|
166
|
+
const modelOverride = model ?? sessionModel ?? session.getModelString?.();
|
|
167
|
+
|
|
159
168
|
// Handle empty or missing tasks
|
|
160
169
|
if (!params.tasks || params.tasks.length === 0) {
|
|
161
170
|
return {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Fuzzy match: "opus" → "p-anthropic/claude-opus-4-5"
|
|
9
9
|
* - Comma fallback: "gpt, opus" → tries gpt first, then opus
|
|
10
10
|
* - "default" → undefined (use system default)
|
|
11
|
-
* - "omp/slow" → configured slow model from settings
|
|
11
|
+
* - "omp/slow" or "pi/slow" → configured slow model from settings
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { type Settings, settingsCapability } from "../../../capability/settings";
|
|
@@ -145,9 +145,10 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
|
|
|
145
145
|
.filter(Boolean);
|
|
146
146
|
|
|
147
147
|
for (const p of patterns) {
|
|
148
|
-
// Handle omp/<role> aliases - looks up role in settings.modelRoles
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
// Handle omp/<role> or pi/<role> aliases - looks up role in settings.modelRoles
|
|
149
|
+
const lower = p.toLowerCase();
|
|
150
|
+
if (lower.startsWith("omp/") || lower.startsWith("pi/")) {
|
|
151
|
+
const role = lower.startsWith("omp/") ? p.slice(4) : p.slice(3);
|
|
151
152
|
const resolved = resolveOmpAlias(role, models);
|
|
152
153
|
if (resolved) return resolved;
|
|
153
154
|
continue; // Role not configured, try next pattern
|
|
@@ -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 "@
|
|
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";
|
package/src/discovery/index.ts
CHANGED
|
@@ -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 "@
|
|
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";
|
|
@@ -289,6 +289,9 @@ async function buildSessionOptions(
|
|
|
289
289
|
process.exit(1);
|
|
290
290
|
}
|
|
291
291
|
options.model = model;
|
|
292
|
+
settingsManager.applyOverrides({
|
|
293
|
+
modelRoles: { default: `${model.provider}/${model.id}` },
|
|
294
|
+
});
|
|
292
295
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
293
296
|
options.model = scopedModels[0].model;
|
|
294
297
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TextContent } from "@
|
|
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 "@
|
|
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 "@
|
|
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 { getOAuthProviders, type OAuthProviderInfo } from "@
|
|
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 "@
|
|
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";
|
|
@@ -369,20 +369,23 @@ export class ToolExecutionComponent extends Container {
|
|
|
369
369
|
this.contentBox.setBgFn(bgFn);
|
|
370
370
|
this.contentBox.clear();
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
component
|
|
372
|
+
const shouldRenderCall = !this.result || !renderer.mergeCallAndResult;
|
|
373
|
+
if (shouldRenderCall) {
|
|
374
|
+
// Render call component
|
|
375
|
+
try {
|
|
376
|
+
const callComponent = renderer.renderCall(this.args, theme);
|
|
377
|
+
if (callComponent) {
|
|
378
|
+
// Ensure component has invalidate() method for Component interface
|
|
379
|
+
const component = callComponent as any;
|
|
380
|
+
if (!component.invalidate) {
|
|
381
|
+
component.invalidate = () => {};
|
|
382
|
+
}
|
|
383
|
+
this.contentBox.addChild(component);
|
|
380
384
|
}
|
|
381
|
-
|
|
385
|
+
} catch {
|
|
386
|
+
// Fall back to default on error
|
|
387
|
+
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
382
388
|
}
|
|
383
|
-
} catch {
|
|
384
|
-
// Fall back to default on error
|
|
385
|
-
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolLabel)), 0, 0));
|
|
386
389
|
}
|
|
387
390
|
|
|
388
391
|
// Render result component if we have a result
|