@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
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 +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -25,6 +25,15 @@ export interface SSHHostInfo {
|
|
|
25
25
|
version: number;
|
|
26
26
|
os: SSHHostOs;
|
|
27
27
|
shell: SSHHostShell;
|
|
28
|
+
/**
|
|
29
|
+
* Shell name OMP verified can execute the POSIX transfer snippets
|
|
30
|
+
* (`head`/`cat`/`mv`/`test`/`ls`) `ssh://` uses. Probed by running
|
|
31
|
+
* `sh -lc` / `bash -lc` / `zsh -lc` against the remote and keeping the
|
|
32
|
+
* first one that round-trips a known marker. Independent of `shell`
|
|
33
|
+
* (the self-reported login shell), which may be noisy, exotic, or simply
|
|
34
|
+
* mis-classified — only `transferShell` gates ssh:// transfers.
|
|
35
|
+
*/
|
|
36
|
+
transferShell?: "sh" | "bash" | "zsh";
|
|
28
37
|
compatShell?: "bash" | "sh";
|
|
29
38
|
compatEnabled: boolean;
|
|
30
39
|
}
|
|
@@ -32,7 +41,7 @@ export interface SSHHostInfo {
|
|
|
32
41
|
const CONTROL_DIR = getSshControlDir();
|
|
33
42
|
const CONTROL_PATH = path.join(CONTROL_DIR, "%C.sock");
|
|
34
43
|
const HOST_INFO_DIR = getRemoteHostDir();
|
|
35
|
-
const HOST_INFO_VERSION =
|
|
44
|
+
const HOST_INFO_VERSION = 4;
|
|
36
45
|
|
|
37
46
|
const activeHosts = new Map<string, SSHConnectionTarget>();
|
|
38
47
|
const pendingConnections = new Map<string, Promise<void>>();
|
|
@@ -166,6 +175,11 @@ function parseCompatShell(value: unknown): "bash" | "sh" | undefined {
|
|
|
166
175
|
return undefined;
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
function parseTransferShell(value: unknown): SSHHostInfo["transferShell"] {
|
|
179
|
+
if (value === "sh" || value === "bash" || value === "zsh") return value;
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
function applyCompatOverride(host: SSHConnectionTarget, info: SSHHostInfo): SSHHostInfo {
|
|
170
184
|
const compatShell =
|
|
171
185
|
info.compatShell ??
|
|
@@ -184,18 +198,26 @@ function applyCompatOverride(host: SSHConnectionTarget, info: SSHHostInfo): SSHH
|
|
|
184
198
|
return { ...info, version: info.version ?? 0, compatShell, compatEnabled };
|
|
185
199
|
}
|
|
186
200
|
|
|
187
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Parse a raw cache-file value (or any unknown) into a normalized
|
|
203
|
+
* {@link SSHHostInfo}, dropping fields that don't pass the per-field guards.
|
|
204
|
+
* Exported so cache-layer round-tripping (incl. the new `transferShell`
|
|
205
|
+
* field, #3719) is testable without touching disk.
|
|
206
|
+
*/
|
|
207
|
+
export function parseHostInfo(value: unknown): SSHHostInfo | null {
|
|
188
208
|
if (!value || typeof value !== "object") return null;
|
|
189
209
|
const record = value as Record<string, unknown>;
|
|
190
210
|
const os = parseOs(record.os) ?? "unknown";
|
|
191
211
|
const shell = parseShell(record.shell) ?? "unknown";
|
|
192
212
|
const compatShell = parseCompatShell(record.compatShell);
|
|
213
|
+
const transferShell = parseTransferShell(record.transferShell);
|
|
193
214
|
const compatEnabled = typeof record.compatEnabled === "boolean" ? record.compatEnabled : false;
|
|
194
215
|
const version = typeof record.version === "number" ? record.version : 0;
|
|
195
216
|
return {
|
|
196
217
|
version,
|
|
197
218
|
os,
|
|
198
219
|
shell,
|
|
220
|
+
transferShell,
|
|
199
221
|
compatShell,
|
|
200
222
|
compatEnabled,
|
|
201
223
|
};
|
|
@@ -208,6 +230,11 @@ function shouldRefreshHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): bo
|
|
|
208
230
|
if (info.os === "windows" && info.compatEnabled && !info.compatShell) return true;
|
|
209
231
|
if (info.os === "windows" && info.compatShell === "bash" && info.shell === "unknown") return true;
|
|
210
232
|
if (host.compat === true && info.os === "windows" && !info.compatShell) return true;
|
|
233
|
+
// A non-Windows host with no verified POSIX transfer shell is ambiguous —
|
|
234
|
+
// either the probe never ran capability checks, or every candidate failed.
|
|
235
|
+
// Re-probe rather than letting the ssh:// transfer guard reject it on a
|
|
236
|
+
// stale `shell: "unknown"` classification (#3719).
|
|
237
|
+
if (info.os !== "windows" && !info.transferShell) return true;
|
|
211
238
|
return false;
|
|
212
239
|
}
|
|
213
240
|
|
|
@@ -252,15 +279,99 @@ async function persistHostInfo(host: SSHConnectionTarget, info: SSHHostInfo): Pr
|
|
|
252
279
|
}
|
|
253
280
|
}
|
|
254
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Frame marker emitted by the remote OS/shell probe. The probe wraps its
|
|
284
|
+
* payload in this prefix so the parser can ignore startup-file noise (banners,
|
|
285
|
+
* `motd`, login messages, `Last login: …`) instead of trusting only the first
|
|
286
|
+
* line of stdout. See #3719.
|
|
287
|
+
*/
|
|
288
|
+
export const HOST_PROBE_MARKER = "PI_HOST_PROBE=";
|
|
289
|
+
|
|
290
|
+
/** Marker for the transfer-shell capability probe. */
|
|
291
|
+
export const TRANSFER_PROBE_MARKER = "PI_TRANSFER_OK|";
|
|
292
|
+
|
|
293
|
+
/** sh / bash / zsh, in the order we'll try as `transferShell` candidates. */
|
|
294
|
+
const TRANSFER_SHELL_CANDIDATES = ["sh", "bash", "zsh"] as const;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Find the first line of `stdout`/`stderr` that begins with `marker` and
|
|
298
|
+
* return everything after it. Used by the SSH host probe so noisy login
|
|
299
|
+
* dotfiles can't corrupt OS/shell classification by emitting text on the
|
|
300
|
+
* first line of `ssh` output.
|
|
301
|
+
*
|
|
302
|
+
* Returns `null` when no marker line is found in either stream.
|
|
303
|
+
*/
|
|
304
|
+
export function extractProbePayload(stdout: string, stderr: string, marker = HOST_PROBE_MARKER): string | null {
|
|
305
|
+
for (const blob of [stdout, stderr]) {
|
|
306
|
+
if (!blob) continue;
|
|
307
|
+
for (const line of blob.split("\n")) {
|
|
308
|
+
const trimmed = line.trim();
|
|
309
|
+
if (trimmed.startsWith(marker)) {
|
|
310
|
+
return trimmed.slice(marker.length);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Find `marker` anywhere in `stdout` or `stderr` and return everything that
|
|
319
|
+
* follows it, scanning stdout first. Returns `null` when the marker is in
|
|
320
|
+
* neither stream.
|
|
321
|
+
*
|
|
322
|
+
* Used by the transfer-shell capability probe. Some remotes have broken
|
|
323
|
+
* login dotfiles that swap fd 1/2, so the marker can land on stderr even
|
|
324
|
+
* though the probe ran the printf successfully (matches the host-info
|
|
325
|
+
* probe's stderr fallback). See #3719.
|
|
326
|
+
*/
|
|
327
|
+
export function findProbeMarker(stdout: string, stderr: string, marker: string): string | null {
|
|
328
|
+
for (const blob of [stdout, stderr]) {
|
|
329
|
+
if (!blob) continue;
|
|
330
|
+
const idx = blob.indexOf(marker);
|
|
331
|
+
if (idx !== -1) return blob.slice(idx + marker.length);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Classify a POSIX-ish `uname -s` payload from the transfer-shell probe. */
|
|
337
|
+
export function osFromUname(value: string): SSHHostOs | undefined {
|
|
338
|
+
const uname = value.toLowerCase();
|
|
339
|
+
if (uname.includes("darwin")) return "macos";
|
|
340
|
+
if (uname.includes("linux") || uname.includes("gnu")) return "linux";
|
|
341
|
+
if (uname.includes("mingw") || uname.includes("msys") || uname.includes("cygwin") || uname.includes("windows")) {
|
|
342
|
+
return "windows";
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function probeTransferShell(
|
|
348
|
+
host: SSHConnectionTarget,
|
|
349
|
+
): Promise<{ shell: SSHHostInfo["transferShell"]; uname: string }> {
|
|
350
|
+
for (const candidate of TRANSFER_SHELL_CANDIDATES) {
|
|
351
|
+
// `printf` is POSIX and emits no trailing newline, so we can pin the
|
|
352
|
+
// marker right against the uname output and split on it cleanly.
|
|
353
|
+
const remote = `${candidate} -lc 'printf "${TRANSFER_PROBE_MARKER}"; uname -s 2>/dev/null || true'`;
|
|
354
|
+
const probe = await runSshCaptureSync(await buildRemoteCommand(host, remote));
|
|
355
|
+
if (probe.exitCode !== 0) continue;
|
|
356
|
+
const tail = findProbeMarker(probe.stdout, probe.stderr, TRANSFER_PROBE_MARKER);
|
|
357
|
+
if (tail === null) continue;
|
|
358
|
+
return { shell: candidate, uname: tail.trim() };
|
|
359
|
+
}
|
|
360
|
+
return { shell: undefined, uname: "" };
|
|
361
|
+
}
|
|
362
|
+
|
|
255
363
|
async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
|
|
256
|
-
const command =
|
|
364
|
+
const command = `echo "${HOST_PROBE_MARKER}$OSTYPE|$SHELL|$BASH_VERSION" 2>/dev/null || echo "${HOST_PROBE_MARKER}%OS%|%COMSPEC%|"`;
|
|
257
365
|
const result = await runSshCaptureSync(await buildRemoteCommand(host, command));
|
|
258
|
-
|
|
366
|
+
const payload = extractProbePayload(result.stdout, result.stderr);
|
|
367
|
+
if (payload === null) {
|
|
259
368
|
logger.debug("SSH host probe failed", { host: host.name, error: result.stderr });
|
|
369
|
+
const transferProbe = await probeTransferShell(host);
|
|
260
370
|
const fallback: SSHHostInfo = {
|
|
261
371
|
version: HOST_INFO_VERSION,
|
|
262
|
-
os: "unknown",
|
|
372
|
+
os: transferProbe.shell ? (osFromUname(transferProbe.uname) ?? "unknown") : "unknown",
|
|
263
373
|
shell: "unknown",
|
|
374
|
+
transferShell: transferProbe.shell,
|
|
264
375
|
compatShell: undefined,
|
|
265
376
|
compatEnabled: false,
|
|
266
377
|
};
|
|
@@ -268,27 +379,26 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
|
|
|
268
379
|
return fallback;
|
|
269
380
|
}
|
|
270
381
|
|
|
271
|
-
const
|
|
272
|
-
const [rawOs = "", rawShell = "", rawBash = ""] = output.split("|");
|
|
382
|
+
const [rawOs = "", rawShell = "", rawBash = ""] = payload.split("|");
|
|
273
383
|
const ostype = rawOs.trim();
|
|
274
384
|
const shellRaw = rawShell.trim();
|
|
275
385
|
const bashVersion = rawBash.trim();
|
|
276
|
-
const
|
|
386
|
+
const payloadLower = payload.toLowerCase();
|
|
277
387
|
const osLower = ostype.toLowerCase();
|
|
278
388
|
const shellLower = shellRaw.toLowerCase();
|
|
279
389
|
const unexpandedPosixVars =
|
|
280
|
-
|
|
390
|
+
payload.includes("$OSTYPE") || payload.includes("$SHELL") || payload.includes("$BASH_VERSION");
|
|
281
391
|
const windowsDetected =
|
|
282
392
|
osLower.includes("windows") ||
|
|
283
393
|
osLower.includes("msys") ||
|
|
284
394
|
osLower.includes("cygwin") ||
|
|
285
395
|
osLower.includes("mingw") ||
|
|
286
|
-
|
|
287
|
-
|
|
396
|
+
payloadLower.includes("windows_nt") ||
|
|
397
|
+
payloadLower.includes("comspec") ||
|
|
288
398
|
shellLower.includes("cmd") ||
|
|
289
399
|
shellLower.includes("powershell") ||
|
|
290
400
|
unexpandedPosixVars ||
|
|
291
|
-
|
|
401
|
+
payload.includes("%OS%");
|
|
292
402
|
|
|
293
403
|
let os: SSHHostOs = "unknown";
|
|
294
404
|
if (windowsDetected) {
|
|
@@ -305,6 +415,22 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
|
|
|
305
415
|
shell = "cmd";
|
|
306
416
|
}
|
|
307
417
|
|
|
418
|
+
// For any non-Windows host (including `unknown`, which is often a misclassified
|
|
419
|
+
// POSIX remote with noisy login output) verify a working transfer shell by
|
|
420
|
+
// running `sh -lc` / `bash -lc` / `zsh -lc` against it. The first one whose
|
|
421
|
+
// printf round-trips becomes `transferShell`; ssh:// gates on this rather
|
|
422
|
+
// than the self-reported login-shell name (#3719).
|
|
423
|
+
let transferShell: SSHHostInfo["transferShell"];
|
|
424
|
+
if (os !== "windows") {
|
|
425
|
+
const probe = await probeTransferShell(host);
|
|
426
|
+
transferShell = probe.shell;
|
|
427
|
+
// `uname -s` from the same probe lets us recover the OS when the first
|
|
428
|
+
// probe couldn't classify it (e.g. the remote silently nuked `$OSTYPE`).
|
|
429
|
+
if (transferShell && os === "unknown") {
|
|
430
|
+
os = osFromUname(probe.uname) ?? os;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
308
434
|
const hasBash = !unexpandedPosixVars && (Boolean(bashVersion) || shell === "bash");
|
|
309
435
|
let compatShell: SSHHostInfo["compatShell"];
|
|
310
436
|
if (os === "windows" && host.compat !== false) {
|
|
@@ -328,6 +454,7 @@ async function probeHostInfo(host: SSHConnectionTarget): Promise<SSHHostInfo> {
|
|
|
328
454
|
version: HOST_INFO_VERSION,
|
|
329
455
|
os,
|
|
330
456
|
shell,
|
|
457
|
+
transferShell,
|
|
331
458
|
compatShell,
|
|
332
459
|
compatEnabled,
|
|
333
460
|
});
|
package/src/ssh/file-transfer.ts
CHANGED
|
@@ -8,20 +8,24 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ptree } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
11
|
-
import { quotePosixPath } from "./utils";
|
|
11
|
+
import { quotePosixPath, wrapInPosixShell } from "./utils";
|
|
12
12
|
|
|
13
13
|
/** Per-operation timeout for remote transfers (matches the ssh tool's grep window). */
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Ensure the ControlMaster connection and
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* Ensure the ControlMaster connection and pick the verified POSIX shell to
|
|
18
|
+
* run transfer commands under. Returns the shell name so the caller can
|
|
19
|
+
* wrap its snippet in `<shell> -c '…'`; OpenSSH otherwise hands the command
|
|
20
|
+
* to the user's login shell, which on fish/csh/tcsh hosts can't parse our
|
|
21
|
+
* `if [ … ]; then …` constructs (#3719).
|
|
22
|
+
*
|
|
23
|
+
* Windows hosts are refused up front — `ssh://` runs `head`/`cat`/`mv`/`test`
|
|
24
|
+
* directly and cmd/powershell can't drive those. Everywhere else, we require
|
|
25
|
+
* a non-empty `transferShell` (set by `probeHostInfo` after `sh -lc` /
|
|
26
|
+
* `bash -lc` / `zsh -lc` round-trips a marker against the remote).
|
|
23
27
|
*/
|
|
24
|
-
async function ensurePosixRemote(target: SSHConnectionTarget): Promise<
|
|
28
|
+
async function ensurePosixRemote(target: SSHConnectionTarget): Promise<"sh" | "bash" | "zsh"> {
|
|
25
29
|
await ensureConnection(target);
|
|
26
30
|
const info = await ensureHostInfo(target);
|
|
27
31
|
if (info.os === "windows") {
|
|
@@ -29,11 +33,12 @@ async function ensurePosixRemote(target: SSHConnectionTarget): Promise<void> {
|
|
|
29
33
|
`ssh://: ${target.name} is a Windows host; ssh:// supports POSIX remotes only (head/cat/mv) — use the ssh tool for Windows hosts`,
|
|
30
34
|
);
|
|
31
35
|
}
|
|
32
|
-
if (info.
|
|
36
|
+
if (!info.transferShell) {
|
|
33
37
|
throw new Error(
|
|
34
|
-
`ssh://: ${target.name}
|
|
38
|
+
`ssh://: ${target.name} has no verified POSIX shell for ssh:// read/write — none of sh/bash/zsh round-tripped a capability probe (use the ssh tool for this host)`,
|
|
35
39
|
);
|
|
36
40
|
}
|
|
41
|
+
return info.transferShell;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export interface RemoteFileReadOptions {
|
|
@@ -67,9 +72,9 @@ export async function readRemoteFile(
|
|
|
67
72
|
remotePath: string,
|
|
68
73
|
opts: RemoteFileReadOptions,
|
|
69
74
|
): Promise<RemoteFileReadResult> {
|
|
70
|
-
await ensurePosixRemote(target);
|
|
75
|
+
const shell = await ensurePosixRemote(target);
|
|
71
76
|
const command = `head -c ${opts.maxBytes + 1} ${quotePosixPath(remotePath)}`;
|
|
72
|
-
const args = await buildRemoteCommand(target, command);
|
|
77
|
+
const args = await buildRemoteCommand(target, wrapInPosixShell(shell, command));
|
|
73
78
|
using child = ptree.spawn(["ssh", ...args], {
|
|
74
79
|
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
75
80
|
});
|
|
@@ -110,7 +115,7 @@ export async function writeRemoteFile(
|
|
|
110
115
|
content: Uint8Array,
|
|
111
116
|
opts: RemoteFileWriteOptions,
|
|
112
117
|
): Promise<void> {
|
|
113
|
-
await ensurePosixRemote(target);
|
|
118
|
+
const shell = await ensurePosixRemote(target);
|
|
114
119
|
if (remotePath.endsWith("/")) {
|
|
115
120
|
throw new Error("ssh://: destination is a directory path (trailing '/'); ssh:// write requires a file path");
|
|
116
121
|
}
|
|
@@ -135,7 +140,7 @@ export async function writeRemoteFile(
|
|
|
135
140
|
`elif [ -e ${dest} ] && [ ! -L ${dest} ]; then echo 'ssh://: destination is a special file (not a regular file)' >&2; exit 1; ` +
|
|
136
141
|
`else mv "$t" ${dest}; fi; ` +
|
|
137
142
|
`}`;
|
|
138
|
-
const args = await buildRemoteCommand(target, command, { allowStdin: true });
|
|
143
|
+
const args = await buildRemoteCommand(target, wrapInPosixShell(shell, command), { allowStdin: true });
|
|
139
144
|
using child = ptree.spawn(["ssh", ...args], {
|
|
140
145
|
stdin: content,
|
|
141
146
|
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
@@ -155,10 +160,10 @@ export async function statRemotePath(
|
|
|
155
160
|
remotePath: string,
|
|
156
161
|
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
157
162
|
): Promise<RemotePathKind> {
|
|
158
|
-
await ensurePosixRemote(target);
|
|
163
|
+
const shell = await ensurePosixRemote(target);
|
|
159
164
|
const p = quotePosixPath(remotePath);
|
|
160
165
|
const command = `if [ -d ${p} ]; then echo directory; elif [ -f ${p} ]; then echo file; elif [ -e ${p} ]; then echo other; else echo missing; fi`;
|
|
161
|
-
const args = await buildRemoteCommand(target, command);
|
|
166
|
+
const args = await buildRemoteCommand(target, wrapInPosixShell(shell, command));
|
|
162
167
|
using child = ptree.spawn(["ssh", ...args], {
|
|
163
168
|
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
164
169
|
});
|
|
@@ -188,9 +193,9 @@ export async function listRemoteDir(
|
|
|
188
193
|
remotePath: string,
|
|
189
194
|
opts: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
190
195
|
): Promise<RemoteDirEntry[]> {
|
|
191
|
-
await ensurePosixRemote(target);
|
|
196
|
+
const shell = await ensurePosixRemote(target);
|
|
192
197
|
const command = `LC_ALL=C ls -1Ap -- ${quotePosixPath(remotePath)}`;
|
|
193
|
-
const args = await buildRemoteCommand(target, command);
|
|
198
|
+
const args = await buildRemoteCommand(target, wrapInPosixShell(shell, command));
|
|
194
199
|
using child = ptree.spawn(["ssh", ...args], {
|
|
195
200
|
signal: ptree.combineSignals(opts.signal, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
196
201
|
});
|
package/src/ssh/ssh-executor.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { OutputSink } from "../session/streaming-output";
|
|
|
4
4
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
5
5
|
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
|
6
6
|
import { hasSshfs, mountRemote } from "./sshfs-mount";
|
|
7
|
+
import { wrapInPosixShell } from "./utils";
|
|
7
8
|
|
|
8
9
|
export interface SSHExecutorOptions {
|
|
9
10
|
/** Timeout in milliseconds */
|
|
@@ -78,18 +79,6 @@ function createAbortWaiter(
|
|
|
78
79
|
return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function quoteForCompatShell(command: string): string {
|
|
82
|
-
if (command.length === 0) {
|
|
83
|
-
return "''";
|
|
84
|
-
}
|
|
85
|
-
const escaped = command.replace(/'/g, "'\\''");
|
|
86
|
-
return `'${escaped}'`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function buildCompatCommand(shell: "bash" | "sh", command: string): string {
|
|
90
|
-
return `${shell} -c ${quoteForCompatShell(command)}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
82
|
export async function executeSSH(
|
|
94
83
|
host: SSHConnectionTarget,
|
|
95
84
|
command: string,
|
|
@@ -108,7 +97,7 @@ export async function executeSSH(
|
|
|
108
97
|
if (options?.compatEnabled) {
|
|
109
98
|
const info = await ensureHostInfo(host);
|
|
110
99
|
if (info.compatShell) {
|
|
111
|
-
resolvedCommand =
|
|
100
|
+
resolvedCommand = wrapInPosixShell(info.compatShell, command);
|
|
112
101
|
} else {
|
|
113
102
|
logger.warn("SSH compat enabled without detected compat shell", { host: host.name });
|
|
114
103
|
}
|
package/src/ssh/utils.ts
CHANGED
|
@@ -30,3 +30,22 @@ export function quotePosixPath(value: string): string {
|
|
|
30
30
|
if (value.length === 0) return "''";
|
|
31
31
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a POSIX command in `<shell> -c '<command>'` so it runs under the
|
|
36
|
+
* named shell rather than whatever `$SHELL` happens to be on the remote.
|
|
37
|
+
*
|
|
38
|
+
* Used by the `ssh://` transfer helpers and the Windows compat dispatch:
|
|
39
|
+
* OpenSSH passes our snippets to `<login-shell> -c`, so a remote whose
|
|
40
|
+
* login shell is fish/csh/tcsh (or cmd/powershell on Windows compat)
|
|
41
|
+
* can't parse `if [ … ]; then …`. Wrapping forces parsing under the
|
|
42
|
+
* shell OMP actually verified can run the snippet.
|
|
43
|
+
*
|
|
44
|
+
* `-c` (not `-lc`): the transfer snippets only call absolute POSIX
|
|
45
|
+
* builtins (`head`/`cat`/`mv`/`test`/`ls`/`mkdir`/`rm`/`dirname`) and
|
|
46
|
+
* don't need login-profile setup. Capability *probing* still uses
|
|
47
|
+
* `-lc` to mirror the user's real environment.
|
|
48
|
+
*/
|
|
49
|
+
export function wrapInPosixShell(shell: "sh" | "bash" | "zsh", command: string): string {
|
|
50
|
+
return `${shell} -c ${quotePosixPath(command)}`;
|
|
51
|
+
}
|
package/src/task/executor.ts
CHANGED
|
@@ -71,7 +71,11 @@ import {
|
|
|
71
71
|
TASK_SUBAGENT_LIFECYCLE_CHANNEL,
|
|
72
72
|
TASK_SUBAGENT_PROGRESS_CHANNEL,
|
|
73
73
|
type TaskToolDetails,
|
|
74
|
+
type YieldItem,
|
|
74
75
|
} from "./types";
|
|
76
|
+
import { arrayValuedLabels, assembleYieldResult } from "./yield-assembly";
|
|
77
|
+
|
|
78
|
+
export type { YieldItem } from "./types";
|
|
75
79
|
|
|
76
80
|
const MCP_CALL_TIMEOUT_MS = 60_000;
|
|
77
81
|
|
|
@@ -517,21 +521,6 @@ function resolveFallbackCompletion(rawOutput: string, outputSchema: unknown): {
|
|
|
517
521
|
return { data: candidate };
|
|
518
522
|
}
|
|
519
523
|
|
|
520
|
-
export interface YieldItem {
|
|
521
|
-
data?: unknown;
|
|
522
|
-
status?: "success" | "aborted";
|
|
523
|
-
error?: string;
|
|
524
|
-
/**
|
|
525
|
-
* Set by the in-tool yield validator when it exhausted its retry budget
|
|
526
|
-
* (MAX_SCHEMA_RETRIES) and accepted a schema-invalid payload anyway.
|
|
527
|
-
* `finalizeSubprocessOutput` honors this by serializing the payload and
|
|
528
|
-
* surfacing a stderr warning, instead of re-emitting `schema_violation`
|
|
529
|
-
* — which would silently swap the subagent's "accepted" view for a
|
|
530
|
-
* different, opaque error blob in the parent's view of the result.
|
|
531
|
-
*/
|
|
532
|
-
schemaOverridden?: boolean;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
524
|
interface FinalizeSubprocessOutputArgs {
|
|
536
525
|
rawOutput: string;
|
|
537
526
|
exitCode: number;
|
|
@@ -541,6 +530,7 @@ interface FinalizeSubprocessOutputArgs {
|
|
|
541
530
|
yieldItems?: YieldItem[];
|
|
542
531
|
reportFindings?: ReviewFinding[];
|
|
543
532
|
outputSchema: unknown;
|
|
533
|
+
lastAssistantText?: string;
|
|
544
534
|
}
|
|
545
535
|
|
|
546
536
|
interface FinalizeSubprocessOutputResult {
|
|
@@ -583,7 +573,7 @@ function buildSchemaViolationOutcome(
|
|
|
583
573
|
|
|
584
574
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
585
575
|
let { rawOutput, exitCode, stderr } = args;
|
|
586
|
-
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
576
|
+
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema, lastAssistantText } = args;
|
|
587
577
|
let abortedViaYield = false;
|
|
588
578
|
const hasYield = Array.isArray(yieldItems) && yieldItems.length > 0;
|
|
589
579
|
const hadFailureBeforeYield = exitCode !== 0 && stderr.trim().length > 0;
|
|
@@ -600,15 +590,16 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
600
590
|
rawOutput = `{"aborted":true,"error":"${lastYield.error || "Unknown error"}"}`;
|
|
601
591
|
}
|
|
602
592
|
} else {
|
|
603
|
-
const
|
|
604
|
-
if (
|
|
593
|
+
const assembled = assembleYieldResult(yieldItems, lastAssistantText, arrayValuedLabels(outputSchema));
|
|
594
|
+
if (!assembled || assembled.missingData) {
|
|
605
595
|
rawOutput = rawOutput ? `${SUBAGENT_WARNING_NULL_YIELD}\n\n${rawOutput}` : SUBAGENT_WARNING_NULL_YIELD;
|
|
606
596
|
} else {
|
|
607
597
|
const { validator, error: schemaError } = buildOutputValidator(outputSchema);
|
|
608
|
-
const
|
|
609
|
-
|
|
598
|
+
const completeData = assembled.rawText
|
|
599
|
+
? assembled.data
|
|
600
|
+
: normalizeCompleteData(assembled.data, reportFindings, validator);
|
|
610
601
|
const result =
|
|
611
|
-
schemaError ||
|
|
602
|
+
schemaError || assembled.schemaOverridden
|
|
612
603
|
? { success: true as const }
|
|
613
604
|
: (validator?.validate(completeData) ?? { success: true as const });
|
|
614
605
|
if (!result.success) {
|
|
@@ -619,14 +610,17 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
619
610
|
exitCode = outcome.exitCode;
|
|
620
611
|
} else {
|
|
621
612
|
try {
|
|
622
|
-
rawOutput =
|
|
613
|
+
rawOutput =
|
|
614
|
+
assembled.rawText && typeof completeData === "string"
|
|
615
|
+
? completeData
|
|
616
|
+
: (JSON.stringify(completeData, null, 2) ?? "null");
|
|
623
617
|
} catch (err) {
|
|
624
618
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
625
619
|
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
626
620
|
}
|
|
627
621
|
if (!hadFailureBeforeYield) {
|
|
628
622
|
exitCode = 0;
|
|
629
|
-
stderr =
|
|
623
|
+
stderr = assembled.schemaOverridden
|
|
630
624
|
? SUBAGENT_WARNING_SCHEMA_OVERRIDDEN
|
|
631
625
|
: schemaError
|
|
632
626
|
? `invalid output schema: ${schemaError}`
|
|
@@ -925,6 +919,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
925
919
|
cacheRead: 0,
|
|
926
920
|
cacheWrite: 0,
|
|
927
921
|
totalTokens: 0,
|
|
922
|
+
reasoningTokens: 0,
|
|
928
923
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
929
924
|
};
|
|
930
925
|
let hasUsage = false;
|
|
@@ -1315,6 +1310,8 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1315
1310
|
accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
|
|
1316
1311
|
accumulatedUsage.cacheWrite += getNumberField(usageRecord, "cacheWrite") ?? 0;
|
|
1317
1312
|
accumulatedUsage.totalTokens += getNumberField(usageRecord, "totalTokens") ?? 0;
|
|
1313
|
+
accumulatedUsage.reasoningTokens =
|
|
1314
|
+
(accumulatedUsage.reasoningTokens ?? 0) + (getNumberField(usageRecord, "reasoningTokens") ?? 0);
|
|
1318
1315
|
if (costRecord) {
|
|
1319
1316
|
accumulatedUsage.cost.input += getNumberField(costRecord, "input") ?? 0;
|
|
1320
1317
|
accumulatedUsage.cost.output += getNumberField(costRecord, "output") ?? 0;
|
|
@@ -1665,6 +1662,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1665
1662
|
yieldItems,
|
|
1666
1663
|
reportFindings,
|
|
1667
1664
|
outputSchema: args.outputSchema,
|
|
1665
|
+
lastAssistantText: monitor.lastAssistantSalvageText(),
|
|
1668
1666
|
});
|
|
1669
1667
|
} finally {
|
|
1670
1668
|
popLoopPhase();
|