@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.2
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 +47 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-xfpkakrn.md} +47 -0
- package/dist/cli.js +3891 -3885
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +16 -5
- package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
- package/dist/types/edit/hashline/diff.d.ts +4 -4
- package/dist/types/eval/executor-base.d.ts +27 -0
- package/dist/types/eval/js/context-manager.d.ts +7 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +64 -5
- package/dist/types/extensibility/extensions/types.d.ts +15 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/live/visualizer.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -3
- package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +22 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/codex-auto-reset.d.ts +182 -64
- package/dist/types/session/session-storage.d.ts +5 -2
- package/dist/types/session/session-tools.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +7 -1
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +27 -0
- package/dist/types/task/persisted-revive.d.ts +7 -0
- package/dist/types/tools/bash-interceptor.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +9 -1
- package/dist/types/tools/browser/launch.d.ts +20 -1
- package/dist/types/tools/browser/registry.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/shell-tokenize.d.ts +11 -0
- package/dist/types/utils/image-loading.d.ts +2 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +6 -1
- package/src/config/settings-schema.ts +16 -3
- package/src/edit/hashline/block-resolver.ts +5 -5
- package/src/edit/hashline/diff.ts +22 -15
- package/src/edit/hashline/execute.ts +14 -13
- package/src/edit/renderer.ts +3 -3
- package/src/eval/executor-base.ts +39 -0
- package/src/eval/jl/executor.ts +13 -2
- package/src/eval/js/context-manager.ts +97 -14
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/py/executor.ts +55 -15
- package/src/eval/rb/executor.ts +8 -1
- package/src/extensibility/extensions/runner.ts +121 -4
- package/src/extensibility/extensions/types.ts +15 -0
- package/src/extensibility/extensions/wrapper.ts +19 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
- package/src/live/visualizer.ts +8 -1
- package/src/lsp/index.ts +51 -11
- package/src/main.ts +3 -0
- package/src/modes/acp/acp-agent.ts +16 -5
- package/src/modes/components/assistant-message.ts +5 -10
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/keybinding-hints.ts +4 -13
- package/src/modes/components/status-line/segments.ts +4 -5
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/input-controller.ts +18 -40
- package/src/modes/controllers/live-command-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +28 -11
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/theme/theme.ts +13 -6
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/sdk.ts +47 -16
- package/src/secrets/index.ts +83 -3
- package/src/secrets/obfuscator.ts +70 -15
- package/src/session/agent-session-types.ts +7 -1
- package/src/session/agent-session.ts +393 -112
- package/src/session/codex-auto-reset.ts +594 -123
- package/src/session/eval-runner.ts +2 -0
- package/src/session/messages.ts +1 -1
- package/src/session/session-maintenance.ts +48 -1
- package/src/session/session-manager.ts +4 -3
- package/src/session/session-stats.ts +14 -3
- package/src/session/session-storage.ts +47 -15
- package/src/session/session-tools.ts +30 -2
- package/src/session/turn-recovery.ts +15 -2
- package/src/slash-commands/builtin-registry.ts +21 -15
- package/src/slash-commands/helpers/reset-usage.ts +2 -0
- package/src/system-prompt.ts +13 -1
- package/src/task/executor.ts +168 -15
- package/src/task/persisted-revive.ts +26 -1
- package/src/telemetry-export.ts +8 -4
- package/src/tools/bash-interceptor.ts +85 -8
- package/src/tools/bash.ts +1 -1
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +60 -11
- package/src/tools/browser/registry.ts +18 -2
- package/src/tools/browser/tab-protocol.ts +8 -0
- package/src/tools/browser/tab-supervisor.ts +31 -4
- package/src/tools/browser/tab-worker.ts +32 -2
- package/src/tools/output-meta.ts +16 -1
- package/src/tools/shell-tokenize.ts +101 -0
- package/src/utils/image-loading.ts +8 -3
- package/src/utils/shell-snapshot.ts +31 -21
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
|
@@ -59,12 +59,20 @@ export type WorkerInitPayload =
|
|
|
59
59
|
safeDir: string;
|
|
60
60
|
targetId: string;
|
|
61
61
|
dialogs?: "accept" | "dismiss";
|
|
62
|
+
url?: string;
|
|
63
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
64
|
+
timeoutMs: number;
|
|
62
65
|
/**
|
|
63
66
|
* Post-timeout recycle: before adopting the page, dismiss any open JS dialog and
|
|
64
67
|
* stop a pending navigation so a blocked target cannot stall worker init (which
|
|
65
68
|
* previously force-killed the tab). Never set for first-time Electron attach.
|
|
66
69
|
*/
|
|
67
70
|
recover?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the worker may raise this tab before capturing a screenshot. Unset
|
|
73
|
+
* behaves as `true`; the supervisor clears it for browsers we did not launch.
|
|
74
|
+
*/
|
|
75
|
+
activateForScreenshot?: boolean;
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunErrorPayload };
|
|
@@ -5,7 +5,7 @@ import { webpExclusionForModel } from "../../utils/image-loading";
|
|
|
5
5
|
import type { ToolSession } from "../index";
|
|
6
6
|
import { expandPath } from "../path-utils";
|
|
7
7
|
import { ToolAbortError, ToolError } from "../tool-errors";
|
|
8
|
-
import { pickElectronTarget } from "./attach";
|
|
8
|
+
import { pickElectronTarget, shouldPreserveConnectedBrowserFocus } from "./attach";
|
|
9
9
|
import { CmuxTab, runCmuxCode } from "./cmux/cmux-tab";
|
|
10
10
|
import { mapWaitUntil } from "./cmux/rpc";
|
|
11
11
|
import { DEFAULT_VIEWPORT } from "./launch";
|
|
@@ -81,6 +81,7 @@ interface TabSessionBase<TBrowser extends BrowserHandle = BrowserHandle> {
|
|
|
81
81
|
export interface WorkerTabSession extends TabSessionBase<PuppeteerBrowserHandle> {
|
|
82
82
|
backend: "worker";
|
|
83
83
|
worker: WorkerHandle;
|
|
84
|
+
activateForScreenshot: boolean;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export interface CmuxTabSession extends TabSessionBase<CmuxBrowserHandle> {
|
|
@@ -139,6 +140,18 @@ const GRACE_MS = 750;
|
|
|
139
140
|
const killedTabs = new Map<string, string>();
|
|
140
141
|
const DEFAULT_TAB_CLOSE_TIMEOUT_MS = 5_000;
|
|
141
142
|
class RecoverableWorkerError extends ToolError {}
|
|
143
|
+
const REPORTED_INIT_FAILURE = Symbol("reported-init-failure");
|
|
144
|
+
|
|
145
|
+
type ReportedInitFailure = Error & { [REPORTED_INIT_FAILURE]?: true };
|
|
146
|
+
|
|
147
|
+
function markReportedInitFailure(error: Error): Error {
|
|
148
|
+
(error as ReportedInitFailure)[REPORTED_INIT_FAILURE] = true;
|
|
149
|
+
return error;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isReportedInitFailure(error: unknown): boolean {
|
|
153
|
+
return error instanceof Error && (error as ReportedInitFailure)[REPORTED_INIT_FAILURE] === true;
|
|
154
|
+
}
|
|
142
155
|
|
|
143
156
|
async function waitForTabCleanup<T>(
|
|
144
157
|
tab: TabSession,
|
|
@@ -265,7 +278,7 @@ async function acquireTabImpl(
|
|
|
265
278
|
// after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
|
|
266
279
|
// the inline worker here so module-resolution failures don't poison every tab open.
|
|
267
280
|
await worker.terminate().catch(() => undefined);
|
|
268
|
-
if (worker.mode === "inline") {
|
|
281
|
+
if (worker.mode === "inline" || isReportedInitFailure(error)) {
|
|
269
282
|
if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
|
|
270
283
|
throw error;
|
|
271
284
|
}
|
|
@@ -312,6 +325,7 @@ async function acquireTabImpl(
|
|
|
312
325
|
pending: new Map(),
|
|
313
326
|
dialogPolicy: opts.dialogs,
|
|
314
327
|
kindTag: browser.kind.kind,
|
|
328
|
+
activateForScreenshot: initPayload.mode === "headless" || initPayload.activateForScreenshot !== false,
|
|
315
329
|
ownerSessionId: opts.ownerSessionId,
|
|
316
330
|
};
|
|
317
331
|
worker.onMessage(msg => handleTabMessage(tab, msg));
|
|
@@ -684,7 +698,14 @@ async function buildInitPayload(browser: PuppeteerBrowserHandle, opts: AcquireTa
|
|
|
684
698
|
timeoutMs: opts.timeoutMs,
|
|
685
699
|
};
|
|
686
700
|
}
|
|
687
|
-
|
|
701
|
+
// A connected browser is user-driven. When no target is requested, adopt its
|
|
702
|
+
// visible tab and avoid raising it before screenshots. An explicit target may
|
|
703
|
+
// be backgrounded, so retain activation to guarantee target-correct pixels.
|
|
704
|
+
const activateForScreenshot = browser.kind.kind !== "connected" || !shouldPreserveConnectedBrowserFocus(opts.target);
|
|
705
|
+
const page = await pickElectronTarget(browser.browser, {
|
|
706
|
+
matcher: opts.target,
|
|
707
|
+
preferVisible: !activateForScreenshot,
|
|
708
|
+
});
|
|
688
709
|
const targetId = await targetIdForPage(page);
|
|
689
710
|
return {
|
|
690
711
|
mode: "attach",
|
|
@@ -692,6 +713,10 @@ async function buildInitPayload(browser: PuppeteerBrowserHandle, opts: AcquireTa
|
|
|
692
713
|
safeDir,
|
|
693
714
|
targetId,
|
|
694
715
|
dialogs: opts.dialogs,
|
|
716
|
+
url: opts.url,
|
|
717
|
+
waitUntil: opts.waitUntil,
|
|
718
|
+
timeoutMs: opts.timeoutMs,
|
|
719
|
+
activateForScreenshot,
|
|
695
720
|
};
|
|
696
721
|
}
|
|
697
722
|
|
|
@@ -793,6 +818,8 @@ async function recycleTimedOutWorkerTab(tab: WorkerTabSession, timeoutMs: number
|
|
|
793
818
|
// Unblock a wedged page (open JS dialog, hung navigation) before adopting it —
|
|
794
819
|
// otherwise init stalls, times out, and the tab gets force-killed.
|
|
795
820
|
recover: true,
|
|
821
|
+
timeoutMs,
|
|
822
|
+
activateForScreenshot: tab.activateForScreenshot,
|
|
796
823
|
};
|
|
797
824
|
let worker = await spawnTabWorker();
|
|
798
825
|
try {
|
|
@@ -1010,7 +1037,7 @@ async function initializeTabWorker(
|
|
|
1010
1037
|
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
|
|
1011
1038
|
const unlisten = worker.onMessage(msg => {
|
|
1012
1039
|
if (msg.type === "ready") resolve(msg.info);
|
|
1013
|
-
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
|
|
1040
|
+
else if (msg.type === "init-failed") reject(markReportedInitFailure(errorFromPayload(msg.error)));
|
|
1014
1041
|
else if (msg.type === "log") logWorkerMessage(msg);
|
|
1015
1042
|
});
|
|
1016
1043
|
const unlistenError = worker.onError(error => {
|
|
@@ -72,6 +72,7 @@ declare global {
|
|
|
72
72
|
var innerHeight: number;
|
|
73
73
|
var document: {
|
|
74
74
|
elementFromPoint(x: number, y: number): Element | null;
|
|
75
|
+
readonly visibilityState: "visible" | "hidden";
|
|
75
76
|
};
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -711,6 +712,22 @@ export function describeScreenshot(opts?: ScreenshotOptions): string {
|
|
|
711
712
|
if (opts?.fullPage) return "tab.screenshot({ fullPage: true })";
|
|
712
713
|
return "tab.screenshot()";
|
|
713
714
|
}
|
|
715
|
+
export async function preparePageForScreenshot(
|
|
716
|
+
page: Pick<Page, "bringToFront" | "evaluate">,
|
|
717
|
+
signal: AbortSignal | undefined,
|
|
718
|
+
activate: boolean,
|
|
719
|
+
): Promise<void> {
|
|
720
|
+
if (activate) {
|
|
721
|
+
await untilAborted(signal, () => page.bringToFront()).catch(() => undefined);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const visible = await untilAborted(signal, () => page.evaluate(() => document.visibilityState === "visible")).catch(
|
|
725
|
+
() => false,
|
|
726
|
+
);
|
|
727
|
+
if (!visible) {
|
|
728
|
+
throw new ToolError("The attached browser tab is not visible; switch to it before taking a screenshot");
|
|
729
|
+
}
|
|
730
|
+
}
|
|
714
731
|
|
|
715
732
|
/** Summarize still-running helpers (oldest first) so a cell timeout names what stalled. */
|
|
716
733
|
export function describeInflight(inflight: Map<number, InflightOp>): string {
|
|
@@ -732,6 +749,7 @@ export class WorkerCore {
|
|
|
732
749
|
#runtime: JsRuntime | null = null;
|
|
733
750
|
#unsub: () => void;
|
|
734
751
|
#mode?: WorkerInitPayload["mode"];
|
|
752
|
+
#activateForScreenshot = true;
|
|
735
753
|
#dialogPolicy?: DialogPolicy;
|
|
736
754
|
#dialogHandler?: (dialog: Dialog) => void;
|
|
737
755
|
#openDialog?: OpenDialogInfo;
|
|
@@ -780,6 +798,7 @@ export class WorkerCore {
|
|
|
780
798
|
async #init(payload: WorkerInitPayload): Promise<void> {
|
|
781
799
|
try {
|
|
782
800
|
this.#mode = payload.mode;
|
|
801
|
+
this.#activateForScreenshot = payload.mode === "headless" || payload.activateForScreenshot !== false;
|
|
783
802
|
const puppeteer = await loadPuppeteerInWorker(payload.safeDir);
|
|
784
803
|
this.#browser = await puppeteer.connect({
|
|
785
804
|
browserWSEndpoint: payload.browserWSEndpoint,
|
|
@@ -810,6 +829,13 @@ export class WorkerCore {
|
|
|
810
829
|
this.#page = page;
|
|
811
830
|
this.#observeDialogs();
|
|
812
831
|
if (payload.dialogs) this.#applyDialogPolicy(payload.dialogs);
|
|
832
|
+
if (payload.url) {
|
|
833
|
+
await this.#page.goto(payload.url, {
|
|
834
|
+
// Same default as the headless arm: dev servers with HMR/WS never reach networkidle.
|
|
835
|
+
waitUntil: payload.waitUntil ?? "load",
|
|
836
|
+
timeout: payload.timeoutMs,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
813
839
|
}
|
|
814
840
|
this.#targetId = await targetIdForPage(this.#page);
|
|
815
841
|
this.#transport.send({ type: "ready", info: await this.#currentReadyInfo() });
|
|
@@ -1521,11 +1547,15 @@ export class WorkerCore {
|
|
|
1521
1547
|
const page = this.#requirePage();
|
|
1522
1548
|
// Multiple tabs can share one Chromium (sibling headless tabs on a shared
|
|
1523
1549
|
// endpoint, cdp/app attach). CDP `Page.captureScreenshot` reads the
|
|
1524
|
-
// compositor surface, which follows the *active* target
|
|
1550
|
+
// compositor surface, which follows the *active* target: a backgrounded
|
|
1525
1551
|
// page can stall waiting for a fresh frame (the 20s screenshot timeouts)
|
|
1526
1552
|
// or hand back a sibling tab's pixels. Activate first; best-effort so an
|
|
1527
1553
|
// already-active or freshly-closed target never fails the capture.
|
|
1528
|
-
|
|
1554
|
+
//
|
|
1555
|
+
// For a user-driven browser, redundant activation would steal window focus.
|
|
1556
|
+
// The supervisor disables it only after adopting the visible tab; if the user
|
|
1557
|
+
// later switches away, reject capture rather than risk sibling-tab pixels.
|
|
1558
|
+
await preparePageForScreenshot(page, signal, this.#activateForScreenshot);
|
|
1529
1559
|
const fullPage = opts.selector ? false : (opts.fullPage ?? false);
|
|
1530
1560
|
const captureType = "png";
|
|
1531
1561
|
const captureMime = "image/png" as const;
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -458,6 +458,9 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
|
|
|
458
458
|
} else {
|
|
459
459
|
notice = `Showing ${truncation.outputLines} of ${totalLines} lines; middle elided`;
|
|
460
460
|
}
|
|
461
|
+
if (truncation.nextOffset != null) {
|
|
462
|
+
notice += `. Use :${truncation.nextOffset} to continue`;
|
|
463
|
+
}
|
|
461
464
|
if (truncation.artifactId != null) {
|
|
462
465
|
notice += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
463
466
|
}
|
|
@@ -674,13 +677,23 @@ async function spillLargeResultToArtifact(
|
|
|
674
677
|
): Promise<AgentToolResult> {
|
|
675
678
|
const sessionManager = context?.sessionManager;
|
|
676
679
|
if (!sessionManager) return result;
|
|
677
|
-
if (toolName === "read") return result;
|
|
678
680
|
const { threshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
679
681
|
|
|
680
682
|
// Skip if tool already saved an artifact
|
|
681
683
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
682
684
|
if (existingMeta?.truncation?.artifactId) return result;
|
|
683
685
|
|
|
686
|
+
// Reading an artifact already addresses recoverable full output. Spilling that
|
|
687
|
+
// read would only create a redundant artifact containing another artifact's
|
|
688
|
+
// page (and can repeat indefinitely on subsequent reads).
|
|
689
|
+
if (
|
|
690
|
+
toolName === "read" &&
|
|
691
|
+
existingMeta?.source?.type === "internal" &&
|
|
692
|
+
existingMeta.source.value.startsWith("artifact://")
|
|
693
|
+
) {
|
|
694
|
+
return result;
|
|
695
|
+
}
|
|
696
|
+
|
|
684
697
|
// Measure total text content
|
|
685
698
|
const textParts: string[] = [];
|
|
686
699
|
for (const block of result.content) {
|
|
@@ -760,6 +773,7 @@ async function spillLargeResultToArtifact(
|
|
|
760
773
|
elidedLines,
|
|
761
774
|
elidedBytes,
|
|
762
775
|
artifactId,
|
|
776
|
+
nextOffset: existingMeta?.truncation?.nextOffset,
|
|
763
777
|
};
|
|
764
778
|
} else {
|
|
765
779
|
const shownStart = truncated.totalLines - outputLines + 1;
|
|
@@ -773,6 +787,7 @@ async function spillLargeResultToArtifact(
|
|
|
773
787
|
maxBytes: tailBytes,
|
|
774
788
|
shownRange: { start: shownStart, end: truncated.totalLines },
|
|
775
789
|
artifactId,
|
|
790
|
+
nextOffset: existingMeta?.truncation?.nextOffset,
|
|
776
791
|
};
|
|
777
792
|
}
|
|
778
793
|
|
|
@@ -81,3 +81,104 @@ export function tokenizeShellSegments(command: string): string[][] {
|
|
|
81
81
|
pushSegment();
|
|
82
82
|
return segments;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the original text of flat shell command segments. Unlike
|
|
87
|
+
* `tokenizeShellSegments`, this preserves quoting and escaping so the results
|
|
88
|
+
* are safe to match against user-configured regular expressions.
|
|
89
|
+
*
|
|
90
|
+
* The extractor deliberately declines to split syntax whose execution context
|
|
91
|
+
* cannot be determined with this small scanner (heredocs, command substitution,
|
|
92
|
+
* backticks, grouping, and malformed quoting). Callers must still check the
|
|
93
|
+
* complete input in that case.
|
|
94
|
+
*/
|
|
95
|
+
export function extractFlatShellCommandSegments(command: string): string[] {
|
|
96
|
+
const segments: string[] = [];
|
|
97
|
+
let segmentStart = 0;
|
|
98
|
+
let inSingle = false;
|
|
99
|
+
let inDouble = false;
|
|
100
|
+
let atWordStart = true;
|
|
101
|
+
|
|
102
|
+
const pushSegment = (end: number) => {
|
|
103
|
+
const segment = command.slice(segmentStart, end).trim();
|
|
104
|
+
if (segment.length > 0) segments.push(segment);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < command.length; i++) {
|
|
108
|
+
const ch = command[i];
|
|
109
|
+
if (inSingle) {
|
|
110
|
+
if (ch === "'") inSingle = false;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (inDouble) {
|
|
114
|
+
if (ch === "\\") {
|
|
115
|
+
if (i + 1 >= command.length) return [];
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (ch === '"') {
|
|
120
|
+
inDouble = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === "`" || (ch === "$" && command[i + 1] === "(")) return [];
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (ch === "'") {
|
|
128
|
+
inSingle = true;
|
|
129
|
+
atWordStart = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === '"') {
|
|
133
|
+
inDouble = true;
|
|
134
|
+
atWordStart = false;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (ch === "\\") {
|
|
138
|
+
if (i + 1 >= command.length) return [];
|
|
139
|
+
i++;
|
|
140
|
+
atWordStart = false;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
ch === "`" ||
|
|
145
|
+
ch === "(" ||
|
|
146
|
+
ch === ")" ||
|
|
147
|
+
(ch === "$" && command[i + 1] === "(") ||
|
|
148
|
+
(ch === "$" && command[i + 1] === "{") ||
|
|
149
|
+
(ch === "<" && command[i + 1] === "<") ||
|
|
150
|
+
((ch === "{" || ch === "}") &&
|
|
151
|
+
atWordStart &&
|
|
152
|
+
(command[i + 1] === undefined || /[ \t\n;]/.test(command[i + 1])))
|
|
153
|
+
) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (ch === "#" && atWordStart) {
|
|
157
|
+
pushSegment(i);
|
|
158
|
+
const newline = command.indexOf("\n", i + 1);
|
|
159
|
+
if (newline === -1) return segments;
|
|
160
|
+
i = newline;
|
|
161
|
+
segmentStart = newline + 1;
|
|
162
|
+
atWordStart = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const isRedirectionOperatorCharacter =
|
|
166
|
+
ch === "|"
|
|
167
|
+
? command[i - 1] === ">"
|
|
168
|
+
: ch === "&"
|
|
169
|
+
? command[i - 1] === ">" || command[i - 1] === "<" || command[i + 1] === ">"
|
|
170
|
+
: false;
|
|
171
|
+
if ((ch === "\n" || ch === ";" || ch === "|" || ch === "&") && !isRedirectionOperatorCharacter) {
|
|
172
|
+
pushSegment(i);
|
|
173
|
+
if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
|
|
174
|
+
segmentStart = i + 1;
|
|
175
|
+
atWordStart = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
atWordStart = ch === " " || ch === "\t";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (inSingle || inDouble) return [];
|
|
182
|
+
pushSegment(command.length);
|
|
183
|
+
return segments;
|
|
184
|
+
}
|
|
@@ -80,14 +80,19 @@ export class ImageInputTooLargeError extends Error {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Converts an image to PNG, rejecting when the runtime cannot decode or encode it. */
|
|
84
|
+
export async function convertImageToPng(image: ImageContent): Promise<ImageContent> {
|
|
85
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
86
|
+
const data = await new Bun.Image(bytes).png().toBase64();
|
|
87
|
+
return { ...image, data, mimeType: "image/png" };
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
export async function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null> {
|
|
84
91
|
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
|
85
92
|
return image;
|
|
86
93
|
}
|
|
87
94
|
try {
|
|
88
|
-
|
|
89
|
-
const data = await new Bun.Image(bytes).png().toBase64();
|
|
90
|
-
return { type: "image", data, mimeType: "image/png" };
|
|
95
|
+
return await convertImageToPng(image);
|
|
91
96
|
} catch {
|
|
92
97
|
return null;
|
|
93
98
|
}
|
|
@@ -230,32 +230,42 @@ export async function getOrCreateSnapshot(
|
|
|
230
230
|
|
|
231
231
|
const rcFile = getShellConfigFile(shell, env);
|
|
232
232
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
fs.chmodSync(snapshotDir, 0o700);
|
|
242
|
-
} catch {
|
|
243
|
-
// best-effort
|
|
244
|
-
}
|
|
233
|
+
// Snapshot dir is per-uid. `os.tmpdir()` is shared between accounts on Linux and
|
|
234
|
+
// this dir is 0700 because the script may inline env-var values referenced by
|
|
235
|
+
// captured functions (#3470), so a single shared name hands the first account an
|
|
236
|
+
// exclusive dir and every other account's pre-create write below fails with
|
|
237
|
+
// EACCES — which used to escape into `executeBash` and break every bash call.
|
|
238
|
+
const uid = process.getuid?.();
|
|
239
|
+
const snapshotDir = path.join(os.tmpdir(), uid === undefined ? "omp-shell-snapshots" : `omp-shell-snapshots-${uid}`);
|
|
245
240
|
|
|
246
241
|
// Generate unique snapshot path
|
|
247
242
|
const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
|
|
248
243
|
const snapshotPath = path.join(snapshotDir, `snapshot-${shellName}-${crypto.randomUUID()}.sh`);
|
|
249
244
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
try {
|
|
246
|
+
fs.mkdirSync(snapshotDir, { recursive: true, mode: 0o700 });
|
|
247
|
+
// `mode` only applies to a fresh mkdir; an existing dir keeps its mode, so
|
|
248
|
+
// chmod it defensively. Ignore EPERM (dir owned by another account).
|
|
249
|
+
try {
|
|
250
|
+
fs.chmodSync(snapshotDir, 0o700);
|
|
251
|
+
} catch {
|
|
252
|
+
// best-effort
|
|
253
|
+
}
|
|
254
|
+
// Pre-create the snapshot file at 0600 so the shell's `>|` (truncate) and
|
|
255
|
+
// `>>` (append) redirections inside `generateSnapshotScript` operate on an
|
|
256
|
+
// existing inode and preserve the private mode, regardless of the umask
|
|
257
|
+
// state inside the spawned shell. Without this, a `.bashrc` that sets
|
|
258
|
+
// `umask 022` (the typical interactive default) before the script's first
|
|
259
|
+
// redirection would create the file world-readable; the JS-side post-spawn
|
|
260
|
+
// chmod would tighten it, but only after the shell finished writing every
|
|
261
|
+
// captured env value to disk.
|
|
262
|
+
fs.writeFileSync(snapshotPath, "", { mode: 0o600 });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// Unusable snapshot dir (foreign owner, read-only or full /tmp): run without
|
|
265
|
+
// a snapshot instead of failing the caller's command.
|
|
266
|
+
logger.debug("shell-snapshot: snapshot dir unusable", { dir: snapshotDir, err: String(err) });
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
259
269
|
|
|
260
270
|
// Generate and execute snapshot script
|
|
261
271
|
const script = generateSnapshotScript(shell, snapshotPath, rcFile);
|