@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.
- package/CHANGELOG.md +34 -9
- 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 +19 -5
- 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 +26 -2
- package/src/core/session-manager.ts +1 -1
- 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/index.test.ts +1 -0
- package/src/core/tools/index.ts +3 -0
- package/src/core/tools/output.ts +1 -1
- package/src/core/tools/read.ts +24 -11
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/ssh.ts +302 -0
- package/src/core/tools/task/index.ts +1 -1
- 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 +1 -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/interactive-mode.ts +1 -1
- 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
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* System prompt construction and project context loading
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
import chalk from "chalk";
|
|
7
9
|
import { contextFileCapability } from "../capability/context-file";
|
|
8
10
|
import type { Rule } from "../capability/rule";
|
|
@@ -70,6 +72,7 @@ const toolDescriptions: Record<ToolName, string> = {
|
|
|
70
72
|
ask: "Ask user for input or clarification",
|
|
71
73
|
read: "Read file contents",
|
|
72
74
|
bash: "Execute bash commands (npm, docker, etc.)",
|
|
75
|
+
ssh: "Execute commands on remote hosts via SSH",
|
|
73
76
|
edit: "Make surgical edits to files (find exact text and replace)",
|
|
74
77
|
write: "Create or overwrite files",
|
|
75
78
|
grep: "Search file contents for patterns (respects .gitignore)",
|
|
@@ -126,6 +129,406 @@ function buildPromptFooter(dateTime: string, cwd: string): string {
|
|
|
126
129
|
return `Current date and time: ${dateTime}\nCurrent working directory: ${cwd}`;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
function execCommand(args: string[]): string | null {
|
|
133
|
+
const result = Bun.spawnSync(args, { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
134
|
+
if (result.exitCode !== 0) return null;
|
|
135
|
+
const output = result.stdout.toString().trim();
|
|
136
|
+
return output.length > 0 ? output : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function execIfExists(command: string, args: string[]): string | null {
|
|
140
|
+
if (!Bun.which(command)) return null;
|
|
141
|
+
return execCommand([command, ...args]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function firstNonEmpty(values: Array<string | undefined | null>): string | null {
|
|
145
|
+
for (const value of values) {
|
|
146
|
+
const trimmed = value?.trim();
|
|
147
|
+
if (trimmed) return trimmed;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function firstNonEmptyLine(value: string | null): string | null {
|
|
153
|
+
if (!value) return null;
|
|
154
|
+
const line = value
|
|
155
|
+
.split("\n")
|
|
156
|
+
.map((entry) => entry.trim())
|
|
157
|
+
.filter(Boolean)[0];
|
|
158
|
+
return line ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseWmicTable(output: string, header: string): string | null {
|
|
162
|
+
const lines = output
|
|
163
|
+
.split("\n")
|
|
164
|
+
.map((line) => line.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
const filtered = lines.filter((line) => line.toLowerCase() !== header.toLowerCase());
|
|
167
|
+
return filtered[0] ?? null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseKeyValueOutput(output: string): Record<string, string> {
|
|
171
|
+
const result: Record<string, string> = {};
|
|
172
|
+
for (const line of output.split("\n")) {
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
if (!trimmed) continue;
|
|
175
|
+
const [key, ...rest] = trimmed.split("=");
|
|
176
|
+
if (!key || rest.length === 0) continue;
|
|
177
|
+
const value = rest.join("=").trim();
|
|
178
|
+
if (value) result[key.trim()] = value;
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stripQuotes(value: string): string {
|
|
184
|
+
return value.replace(/^"|"$/g, "");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getOsName(): string {
|
|
188
|
+
switch (process.platform) {
|
|
189
|
+
case "win32":
|
|
190
|
+
return "Windows";
|
|
191
|
+
case "darwin":
|
|
192
|
+
return "macOS";
|
|
193
|
+
case "linux":
|
|
194
|
+
return "Linux";
|
|
195
|
+
case "freebsd":
|
|
196
|
+
return "FreeBSD";
|
|
197
|
+
case "openbsd":
|
|
198
|
+
return "OpenBSD";
|
|
199
|
+
case "netbsd":
|
|
200
|
+
return "NetBSD";
|
|
201
|
+
case "aix":
|
|
202
|
+
return "AIX";
|
|
203
|
+
default:
|
|
204
|
+
return process.platform || "unknown";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getKernelVersion(): string {
|
|
209
|
+
if (process.platform === "win32") {
|
|
210
|
+
return execCommand(["cmd", "/c", "ver"]) ?? "unknown";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return execCommand(["uname", "-sr"]) ?? "unknown";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getOsDistro(): string | null {
|
|
217
|
+
switch (process.platform) {
|
|
218
|
+
case "win32": {
|
|
219
|
+
const output = execIfExists("wmic", ["os", "get", "Caption,Version", "/value"]);
|
|
220
|
+
if (!output) return null;
|
|
221
|
+
const parsed = parseKeyValueOutput(output);
|
|
222
|
+
const caption = parsed.Caption;
|
|
223
|
+
const version = parsed.Version;
|
|
224
|
+
if (caption && version) return `${caption} ${version}`.trim();
|
|
225
|
+
return caption ?? version ?? null;
|
|
226
|
+
}
|
|
227
|
+
case "darwin": {
|
|
228
|
+
const name = firstNonEmptyLine(execIfExists("sw_vers", ["-productName"]));
|
|
229
|
+
const version = firstNonEmptyLine(execIfExists("sw_vers", ["-productVersion"]));
|
|
230
|
+
if (name && version) return `${name} ${version}`.trim();
|
|
231
|
+
return name ?? version ?? null;
|
|
232
|
+
}
|
|
233
|
+
case "linux": {
|
|
234
|
+
const lsb = firstNonEmptyLine(execIfExists("lsb_release", ["-ds"]));
|
|
235
|
+
if (lsb) return stripQuotes(lsb);
|
|
236
|
+
const osRelease = execIfExists("cat", ["/etc/os-release"]);
|
|
237
|
+
if (!osRelease) return null;
|
|
238
|
+
const parsed = parseKeyValueOutput(osRelease);
|
|
239
|
+
const pretty = parsed.PRETTY_NAME ?? parsed.NAME;
|
|
240
|
+
const version = parsed.VERSION ?? parsed.VERSION_ID;
|
|
241
|
+
if (pretty) return stripQuotes(pretty);
|
|
242
|
+
if (parsed.NAME && version) return `${stripQuotes(parsed.NAME)} ${stripQuotes(version)}`.trim();
|
|
243
|
+
return parsed.NAME ? stripQuotes(parsed.NAME) : null;
|
|
244
|
+
}
|
|
245
|
+
default:
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getCpuArch(): string {
|
|
251
|
+
return process.arch || "unknown";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getCpuModel(): string | null {
|
|
255
|
+
switch (process.platform) {
|
|
256
|
+
case "win32": {
|
|
257
|
+
const output = execIfExists("wmic", ["cpu", "get", "Name"]);
|
|
258
|
+
return output ? parseWmicTable(output, "Name") : null;
|
|
259
|
+
}
|
|
260
|
+
case "darwin": {
|
|
261
|
+
return firstNonEmptyLine(execIfExists("sysctl", ["-n", "machdep.cpu.brand_string"]));
|
|
262
|
+
}
|
|
263
|
+
case "linux": {
|
|
264
|
+
const lscpu = execIfExists("lscpu", []);
|
|
265
|
+
if (lscpu) {
|
|
266
|
+
const match = lscpu
|
|
267
|
+
.split("\n")
|
|
268
|
+
.map((line) => line.trim())
|
|
269
|
+
.find((line) => line.toLowerCase().startsWith("model name:"));
|
|
270
|
+
if (match) return match.split(":").slice(1).join(":").trim();
|
|
271
|
+
}
|
|
272
|
+
const cpuInfo = execIfExists("cat", ["/proc/cpuinfo"]);
|
|
273
|
+
if (!cpuInfo) return null;
|
|
274
|
+
for (const line of cpuInfo.split("\n")) {
|
|
275
|
+
const [key, ...rest] = line.split(":");
|
|
276
|
+
if (!key || rest.length === 0) continue;
|
|
277
|
+
const normalized = key.trim().toLowerCase();
|
|
278
|
+
if (normalized === "model name" || normalized === "hardware" || normalized === "processor") {
|
|
279
|
+
return rest.join(":").trim();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getGpuModel(): string | null {
|
|
290
|
+
switch (process.platform) {
|
|
291
|
+
case "win32": {
|
|
292
|
+
const output = execIfExists("wmic", ["path", "win32_VideoController", "get", "name"]);
|
|
293
|
+
return output ? parseWmicTable(output, "Name") : null;
|
|
294
|
+
}
|
|
295
|
+
case "linux": {
|
|
296
|
+
const output = execIfExists("lspci", []);
|
|
297
|
+
if (!output) return null;
|
|
298
|
+
const gpus: Array<{ name: string; priority: number }> = [];
|
|
299
|
+
for (const line of output.split("\n")) {
|
|
300
|
+
if (!/(VGA|3D|Display)/i.test(line)) continue;
|
|
301
|
+
const parts = line.split(":");
|
|
302
|
+
const name = parts.length > 1 ? parts.slice(1).join(":").trim() : line.trim();
|
|
303
|
+
const nameLower = name.toLowerCase();
|
|
304
|
+
// Skip BMC/server management adapters
|
|
305
|
+
if (/aspeed|matrox g200|mgag200/i.test(name)) continue;
|
|
306
|
+
// Prioritize discrete GPUs
|
|
307
|
+
let priority = 0;
|
|
308
|
+
if (
|
|
309
|
+
nameLower.includes("nvidia") ||
|
|
310
|
+
nameLower.includes("geforce") ||
|
|
311
|
+
nameLower.includes("quadro") ||
|
|
312
|
+
nameLower.includes("rtx")
|
|
313
|
+
) {
|
|
314
|
+
priority = 3;
|
|
315
|
+
} else if (nameLower.includes("amd") || nameLower.includes("radeon") || nameLower.includes("rx ")) {
|
|
316
|
+
priority = 3;
|
|
317
|
+
} else if (nameLower.includes("intel")) {
|
|
318
|
+
priority = 1;
|
|
319
|
+
} else {
|
|
320
|
+
priority = 2;
|
|
321
|
+
}
|
|
322
|
+
gpus.push({ name, priority });
|
|
323
|
+
}
|
|
324
|
+
if (gpus.length === 0) return null;
|
|
325
|
+
gpus.sort((a, b) => b.priority - a.priority);
|
|
326
|
+
return gpus[0].name;
|
|
327
|
+
}
|
|
328
|
+
default:
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getShellName(): string {
|
|
334
|
+
const shell = firstNonEmpty([process.env.SHELL, process.env.ComSpec]);
|
|
335
|
+
return shell ?? "unknown";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getTerminalName(): string {
|
|
339
|
+
const termProgram = process.env.TERM_PROGRAM;
|
|
340
|
+
const termProgramVersion = process.env.TERM_PROGRAM_VERSION;
|
|
341
|
+
if (termProgram) {
|
|
342
|
+
return termProgramVersion ? `${termProgram} ${termProgramVersion}` : termProgram;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (process.env.WT_SESSION) return "Windows Terminal";
|
|
346
|
+
|
|
347
|
+
const term = firstNonEmpty([process.env.TERM, process.env.COLORTERM, process.env.TERMINAL_EMULATOR]);
|
|
348
|
+
return term ?? "unknown";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function normalizeDesktopValue(value: string): string {
|
|
352
|
+
const trimmed = value.trim();
|
|
353
|
+
if (!trimmed) return "unknown";
|
|
354
|
+
const parts = trimmed
|
|
355
|
+
.split(":")
|
|
356
|
+
.map((part) => part.trim())
|
|
357
|
+
.filter(Boolean);
|
|
358
|
+
return parts[0] ?? trimmed;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function getDesktopEnvironment(): string {
|
|
362
|
+
if (process.env.KDE_FULL_SESSION === "true") return "KDE";
|
|
363
|
+
const raw = firstNonEmpty([
|
|
364
|
+
process.env.XDG_CURRENT_DESKTOP,
|
|
365
|
+
process.env.DESKTOP_SESSION,
|
|
366
|
+
process.env.XDG_SESSION_DESKTOP,
|
|
367
|
+
process.env.GDMSESSION,
|
|
368
|
+
]);
|
|
369
|
+
return raw ? normalizeDesktopValue(raw) : "unknown";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function matchKnownWindowManager(value: string): string | null {
|
|
373
|
+
const normalized = value.toLowerCase();
|
|
374
|
+
const candidates = [
|
|
375
|
+
"sway",
|
|
376
|
+
"i3",
|
|
377
|
+
"i3wm",
|
|
378
|
+
"bspwm",
|
|
379
|
+
"openbox",
|
|
380
|
+
"awesome",
|
|
381
|
+
"herbstluftwm",
|
|
382
|
+
"fluxbox",
|
|
383
|
+
"icewm",
|
|
384
|
+
"dwm",
|
|
385
|
+
"hyprland",
|
|
386
|
+
"wayfire",
|
|
387
|
+
"river",
|
|
388
|
+
"labwc",
|
|
389
|
+
"qtile",
|
|
390
|
+
];
|
|
391
|
+
for (const candidate of candidates) {
|
|
392
|
+
if (normalized.includes(candidate)) return candidate;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getWindowManager(): string {
|
|
398
|
+
const explicit = firstNonEmpty([process.env.WINDOWMANAGER]);
|
|
399
|
+
if (explicit) return explicit;
|
|
400
|
+
|
|
401
|
+
const desktop = firstNonEmpty([process.env.XDG_CURRENT_DESKTOP, process.env.DESKTOP_SESSION]);
|
|
402
|
+
if (desktop) {
|
|
403
|
+
const matched = matchKnownWindowManager(desktop);
|
|
404
|
+
if (matched) return matched;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return "unknown";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Cached system info structure */
|
|
411
|
+
interface SystemInfoCache {
|
|
412
|
+
os: string;
|
|
413
|
+
distro: string;
|
|
414
|
+
kernel: string;
|
|
415
|
+
arch: string;
|
|
416
|
+
cpu: string;
|
|
417
|
+
gpu: string;
|
|
418
|
+
disk: string;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getSystemInfoCachePath(): string {
|
|
422
|
+
return join(homedir(), ".omp", "system_info.json");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function loadSystemInfoCache(): SystemInfoCache | null {
|
|
426
|
+
try {
|
|
427
|
+
const cachePath = getSystemInfoCachePath();
|
|
428
|
+
if (!existsSync(cachePath)) return null;
|
|
429
|
+
const content = readFileSync(cachePath, "utf-8");
|
|
430
|
+
return JSON.parse(content) as SystemInfoCache;
|
|
431
|
+
} catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function saveSystemInfoCache(info: SystemInfoCache): void {
|
|
437
|
+
try {
|
|
438
|
+
const cachePath = getSystemInfoCachePath();
|
|
439
|
+
const dir = join(homedir(), ".omp");
|
|
440
|
+
if (!existsSync(dir)) {
|
|
441
|
+
mkdirSync(dir, { recursive: true });
|
|
442
|
+
}
|
|
443
|
+
writeFileSync(cachePath, JSON.stringify(info, null, "\t"), "utf-8");
|
|
444
|
+
} catch {
|
|
445
|
+
// Silently ignore cache write failures
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function collectSystemInfo(): SystemInfoCache {
|
|
450
|
+
return {
|
|
451
|
+
os: getOsName(),
|
|
452
|
+
distro: getOsDistro() ?? "unknown",
|
|
453
|
+
kernel: getKernelVersion(),
|
|
454
|
+
arch: getCpuArch(),
|
|
455
|
+
cpu: getCpuModel() ?? "unknown",
|
|
456
|
+
gpu: getGpuModel() ?? "unknown",
|
|
457
|
+
disk: getDiskInfo() ?? "unknown",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatBytes(bytes: number): string {
|
|
462
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
463
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
464
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
465
|
+
if (bytes < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
466
|
+
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(1)}TB`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function getDiskInfo(): string | null {
|
|
470
|
+
switch (process.platform) {
|
|
471
|
+
case "win32": {
|
|
472
|
+
const output = execIfExists("wmic", ["logicaldisk", "get", "Caption,Size,FreeSpace", "/format:csv"]);
|
|
473
|
+
if (!output) return null;
|
|
474
|
+
const lines = output.split("\n").filter((l) => l.trim() && !l.startsWith("Node"));
|
|
475
|
+
const disks: string[] = [];
|
|
476
|
+
for (const line of lines) {
|
|
477
|
+
const parts = line.split(",");
|
|
478
|
+
if (parts.length < 4) continue;
|
|
479
|
+
const caption = parts[1]?.trim();
|
|
480
|
+
const freeSpace = Number.parseInt(parts[2]?.trim() ?? "", 10);
|
|
481
|
+
const size = Number.parseInt(parts[3]?.trim() ?? "", 10);
|
|
482
|
+
if (!caption || Number.isNaN(size) || size === 0) continue;
|
|
483
|
+
const used = size - (Number.isNaN(freeSpace) ? 0 : freeSpace);
|
|
484
|
+
const pct = Math.round((used / size) * 100);
|
|
485
|
+
disks.push(`${caption} ${formatBytes(used)}/${formatBytes(size)} (${pct}%)`);
|
|
486
|
+
}
|
|
487
|
+
return disks.length > 0 ? disks.join(", ") : null;
|
|
488
|
+
}
|
|
489
|
+
case "linux":
|
|
490
|
+
case "darwin": {
|
|
491
|
+
const output = execIfExists("df", ["-h", "/"]);
|
|
492
|
+
if (!output) return null;
|
|
493
|
+
const lines = output.split("\n");
|
|
494
|
+
if (lines.length < 2) return null;
|
|
495
|
+
const parts = lines[1].split(/\s+/);
|
|
496
|
+
if (parts.length < 5) return null;
|
|
497
|
+
const size = parts[1];
|
|
498
|
+
const used = parts[2];
|
|
499
|
+
const pct = parts[4];
|
|
500
|
+
return `/ ${used}/${size} (${pct})`;
|
|
501
|
+
}
|
|
502
|
+
default:
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function formatEnvironmentInfo(): string {
|
|
508
|
+
// Load cached system info or collect fresh
|
|
509
|
+
let sysInfo = loadSystemInfoCache();
|
|
510
|
+
if (!sysInfo) {
|
|
511
|
+
sysInfo = collectSystemInfo();
|
|
512
|
+
saveSystemInfoCache(sysInfo);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Session-specific values (not cached)
|
|
516
|
+
const items: Array<[string, string]> = [
|
|
517
|
+
["OS", sysInfo.os],
|
|
518
|
+
["Distro", sysInfo.distro],
|
|
519
|
+
["Kernel", sysInfo.kernel],
|
|
520
|
+
["Arch", sysInfo.arch],
|
|
521
|
+
["CPU", sysInfo.cpu],
|
|
522
|
+
["GPU", sysInfo.gpu],
|
|
523
|
+
["Disk", sysInfo.disk],
|
|
524
|
+
["Shell", getShellName()],
|
|
525
|
+
["Terminal", getTerminalName()],
|
|
526
|
+
["DE", getDesktopEnvironment()],
|
|
527
|
+
["WM", getWindowManager()],
|
|
528
|
+
];
|
|
529
|
+
return items.map(([label, value]) => `- ${label}: ${value}`).join("\n");
|
|
530
|
+
}
|
|
531
|
+
|
|
129
532
|
/**
|
|
130
533
|
* Generate anti-bash rules section if the agent has both bash and specialized tools.
|
|
131
534
|
* Only include rules for tools that are actually available.
|
|
@@ -201,6 +604,24 @@ function generateAntiBashRules(tools: ToolName[]): string | null {
|
|
|
201
604
|
);
|
|
202
605
|
}
|
|
203
606
|
|
|
607
|
+
// Add SSH remote filesystem guidance if available
|
|
608
|
+
const hasSSH = tools.includes("ssh");
|
|
609
|
+
if (hasSSH) {
|
|
610
|
+
lines.push("\n### SSH Command Execution");
|
|
611
|
+
lines.push(
|
|
612
|
+
"**Critical**: Each SSH host runs a specific shell. **You MUST match commands to the host's shell type**.",
|
|
613
|
+
);
|
|
614
|
+
lines.push("Check the host list in the ssh tool description. Shell types:");
|
|
615
|
+
lines.push("- linux/bash, linux/zsh, macos/bash, macos/zsh: ls, cat, grep, find, ps, df, uname");
|
|
616
|
+
lines.push("- windows/bash, windows/sh: ls, cat, grep, find (Windows with WSL/Cygwin — Unix commands)");
|
|
617
|
+
lines.push("- windows/cmd: dir, type, findstr, tasklist, systeminfo");
|
|
618
|
+
lines.push("- windows/powershell: Get-ChildItem, Get-Content, Select-String, Get-Process");
|
|
619
|
+
lines.push("");
|
|
620
|
+
lines.push("### SSH Filesystems");
|
|
621
|
+
lines.push("Mounted at `~/.omp/remote/<hostname>/` — use read/edit/write tools directly.");
|
|
622
|
+
lines.push("Windows paths need colon: `~/.omp/remote/host/C:/Users/...` not `C/Users/...`\n");
|
|
623
|
+
}
|
|
624
|
+
|
|
204
625
|
// Add search-first protocol
|
|
205
626
|
if (hasGrep || hasFind) {
|
|
206
627
|
lines.push("\n### Search-First Protocol");
|
|
@@ -389,6 +810,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
389
810
|
|
|
390
811
|
// Generate anti-bash rules (returns null if not applicable)
|
|
391
812
|
const antiBashSection = generateAntiBashRules(Array.from(tools?.keys() ?? []));
|
|
813
|
+
const environmentInfo = formatEnvironmentInfo();
|
|
392
814
|
|
|
393
815
|
// Build guidelines based on which tools are actually available
|
|
394
816
|
const guidelinesList: string[] = [];
|
|
@@ -446,6 +868,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
446
868
|
toolsList,
|
|
447
869
|
antiBashSection: antiBashBlock,
|
|
448
870
|
guidelines,
|
|
871
|
+
environmentInfo,
|
|
449
872
|
readmePath,
|
|
450
873
|
docsPath,
|
|
451
874
|
examplesPath,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Generate session titles using a smol, fast model.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Api, Model } from "@
|
|
6
|
-
import { completeSimple } from "@
|
|
5
|
+
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import titleSystemPrompt from "../prompts/title-system.md" with { type: "text" };
|
|
8
8
|
import { logger } from "./logger";
|
|
9
9
|
import type { ModelRegistry } from "./model-registry";
|
package/src/core/tools/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { createOutputTool, type OutputToolDetails } from "./output";
|
|
|
26
26
|
export { createReadTool, type ReadToolDetails } from "./read";
|
|
27
27
|
export { reportFindingTool, type SubmitReviewDetails } from "./review";
|
|
28
28
|
export { filterRulebookRules, formatRulesForPrompt, type RulebookToolDetails } from "./rulebook";
|
|
29
|
+
export { createSshTool, type SSHToolDetails } from "./ssh";
|
|
29
30
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
30
31
|
export type { TruncationResult } from "./truncate";
|
|
31
32
|
export { createWebFetchTool, type WebFetchToolDetails } from "./web-fetch";
|
|
@@ -68,6 +69,7 @@ import { createOutputTool } from "./output";
|
|
|
68
69
|
import { createReadTool } from "./read";
|
|
69
70
|
import { reportFindingTool } from "./review";
|
|
70
71
|
import { createRulebookTool } from "./rulebook";
|
|
72
|
+
import { createSshTool } from "./ssh";
|
|
71
73
|
import { createTaskTool } from "./task/index";
|
|
72
74
|
import { createWebFetchTool } from "./web-fetch";
|
|
73
75
|
import { createWebSearchTool } from "./web-search/index";
|
|
@@ -115,6 +117,7 @@ type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
|
115
117
|
export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
116
118
|
ask: createAskTool,
|
|
117
119
|
bash: createBashTool,
|
|
120
|
+
ssh: createSshTool,
|
|
118
121
|
edit: createEditTool,
|
|
119
122
|
find: createFindTool,
|
|
120
123
|
git: createGitTool,
|
package/src/core/tools/output.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import type { TextContent } from "@mariozechner/pi-ai";
|
|
10
9
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
13
13
|
import { Type } from "@sinclair/typebox";
|
package/src/core/tools/read.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
3
3
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Type } from "@sinclair/typebox";
|
|
8
|
+
import { CONFIG_DIR_NAME } from "../../config";
|
|
7
9
|
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme";
|
|
8
10
|
import readDescription from "../../prompts/tools/read.md" with { type: "text" };
|
|
9
11
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
|
|
@@ -27,6 +29,13 @@ import {
|
|
|
27
29
|
// Document types convertible via markitdown
|
|
28
30
|
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
29
31
|
|
|
32
|
+
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
|
|
33
|
+
const REMOTE_MOUNT_PREFIX = path.join(homedir(), CONFIG_DIR_NAME, "remote") + path.sep;
|
|
34
|
+
|
|
35
|
+
function isRemoteMountPath(absolutePath: string): boolean {
|
|
36
|
+
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
// Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization
|
|
31
40
|
const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
|
|
32
41
|
const MAX_FUZZY_RESULTS = 5;
|
|
@@ -438,19 +447,23 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
438
447
|
isDirectory = stat.isDirectory();
|
|
439
448
|
} catch (error) {
|
|
440
449
|
if (isNotFoundError(error)) {
|
|
441
|
-
const suggestions = await findReadPathSuggestions(readPath, session.cwd, signal);
|
|
442
450
|
let message = `File not found: ${readPath}`;
|
|
443
451
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
// Skip fuzzy matching for remote mounts (sshfs) to avoid hangs
|
|
453
|
+
if (!isRemoteMountPath(absolutePath)) {
|
|
454
|
+
const suggestions = await findReadPathSuggestions(readPath, session.cwd, signal);
|
|
455
|
+
|
|
456
|
+
if (suggestions?.suggestions.length) {
|
|
457
|
+
const scopeLabel = suggestions.scopeLabel ? ` in ${suggestions.scopeLabel}` : "";
|
|
458
|
+
message += `\n\nClosest matches${scopeLabel}:\n${suggestions.suggestions.map((match) => `- ${match}`).join("\n")}`;
|
|
459
|
+
if (suggestions.truncated) {
|
|
460
|
+
message += `\n[Search truncated to first ${MAX_FUZZY_CANDIDATES} paths. Refine the path if the match isn't listed.]`;
|
|
461
|
+
}
|
|
462
|
+
} else if (suggestions?.error) {
|
|
463
|
+
message += `\n\nFuzzy match failed: ${suggestions.error}`;
|
|
464
|
+
} else if (suggestions?.scopeLabel) {
|
|
465
|
+
message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
|
|
449
466
|
}
|
|
450
|
-
} else if (suggestions?.error) {
|
|
451
|
-
message += `\n\nFuzzy match failed: ${suggestions.error}`;
|
|
452
|
-
} else if (suggestions?.scopeLabel) {
|
|
453
|
-
message += `\n\nNo similar paths found in ${suggestions.scopeLabel}.`;
|
|
454
467
|
}
|
|
455
468
|
|
|
456
469
|
throw new Error(message);
|
|
@@ -17,6 +17,7 @@ import { lspToolRenderer } from "./lsp/render";
|
|
|
17
17
|
import { notebookToolRenderer } from "./notebook";
|
|
18
18
|
import { outputToolRenderer } from "./output";
|
|
19
19
|
import { readToolRenderer } from "./read";
|
|
20
|
+
import { sshToolRenderer } from "./ssh";
|
|
20
21
|
import { taskToolRenderer } from "./task/render";
|
|
21
22
|
import { webFetchToolRenderer } from "./web-fetch";
|
|
22
23
|
import { webSearchToolRenderer } from "./web-search/render";
|
|
@@ -43,6 +44,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
43
44
|
notebook: notebookToolRenderer as ToolRenderer,
|
|
44
45
|
output: outputToolRenderer as ToolRenderer,
|
|
45
46
|
read: readToolRenderer as ToolRenderer,
|
|
47
|
+
ssh: sshToolRenderer as ToolRenderer,
|
|
46
48
|
task: taskToolRenderer as ToolRenderer,
|
|
47
49
|
web_fetch: webFetchToolRenderer as ToolRenderer,
|
|
48
50
|
web_search: webSearchToolRenderer as ToolRenderer,
|