@oh-my-pi/pi-coding-agent 16.3.3 → 16.3.5
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 +24 -0
- package/dist/cli.js +3405 -3382
- package/dist/types/hindsight/content.d.ts +4 -0
- package/dist/types/internal-urls/artifact-protocol.d.ts +8 -0
- package/dist/types/internal-urls/types.d.ts +10 -0
- package/dist/types/lsp/config.d.ts +4 -0
- package/dist/types/lsp/edits.d.ts +2 -0
- package/dist/types/mcp/oauth-discovery.d.ts +30 -0
- package/dist/types/modes/components/login-dialog.d.ts +10 -2
- package/dist/types/modes/components/transcript-container.d.ts +0 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +20 -3
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -3
- package/dist/types/modes/rpc/rpc-types.d.ts +6 -0
- package/dist/types/subprocess/worker-runtime.d.ts +11 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/utils/git.d.ts +16 -0
- package/package.json +12 -12
- package/src/cli/auth-broker-cli.ts +13 -2
- package/src/cli/tiny-models-cli.ts +12 -5
- package/src/cli/usage-cli.ts +34 -5
- package/src/hindsight/content.ts +31 -0
- package/src/internal-urls/artifact-protocol.ts +97 -53
- package/src/internal-urls/types.ts +10 -0
- package/src/lsp/config.ts +15 -0
- package/src/lsp/edits.ts +28 -7
- package/src/lsp/index.ts +46 -4
- package/src/mcp/oauth-discovery.ts +88 -18
- package/src/mnemopi/state.ts +26 -2
- package/src/modes/components/login-dialog.ts +16 -2
- package/src/modes/components/mcp-add-wizard.ts +9 -1
- package/src/modes/components/transcript-container.ts +0 -26
- package/src/modes/controllers/mcp-command-controller.ts +106 -29
- package/src/modes/controllers/selector-controller.ts +9 -1
- package/src/modes/rpc/rpc-client.ts +8 -4
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/modes/rpc/rpc-types.ts +13 -1
- package/src/modes/setup-wizard/scenes/sign-in.ts +18 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/subprocess/worker-runtime.ts +219 -2
- package/src/task/worktree.ts +28 -6
- package/src/tiny/worker.ts +14 -4
- package/src/tools/bash-skill-urls.ts +3 -3
- package/src/tools/grep.ts +19 -2
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +198 -1
- package/src/utils/git.ts +20 -0
- package/src/utils/open.ts +51 -6
package/src/tiny/worker.ts
CHANGED
|
@@ -10,12 +10,14 @@ import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with
|
|
|
10
10
|
import {
|
|
11
11
|
errorMessage,
|
|
12
12
|
errorText,
|
|
13
|
+
formatOnnxRuntimeCudaDiagnostics,
|
|
13
14
|
getTransformersVersionSpec,
|
|
14
15
|
loadTransformersRuntime,
|
|
15
16
|
MemoizedRuntime,
|
|
16
17
|
replayCachedReady,
|
|
17
18
|
sendLog,
|
|
18
19
|
sendProgress,
|
|
20
|
+
type TransformersRuntimeMetadata,
|
|
19
21
|
} from "../subprocess/worker-runtime";
|
|
20
22
|
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
|
|
21
23
|
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
|
|
@@ -39,7 +41,7 @@ const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
|
|
|
39
41
|
const tinyModelDevicePreference = resolveTinyModelDevicePreference();
|
|
40
42
|
const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
|
|
41
43
|
|
|
42
|
-
interface TransformersRuntime {
|
|
44
|
+
interface TransformersRuntime extends TransformersRuntimeMetadata {
|
|
43
45
|
env: {
|
|
44
46
|
cacheDir?: string;
|
|
45
47
|
allowLocalModels?: boolean;
|
|
@@ -136,6 +138,7 @@ async function loadPipelineWithDeviceFallback(
|
|
|
136
138
|
device: devices[0],
|
|
137
139
|
});
|
|
138
140
|
}
|
|
141
|
+
let cudaDiagnostics: string | null = null;
|
|
139
142
|
for (let i = 0; i < devices.length; i += 1) {
|
|
140
143
|
const device = devices[i]!;
|
|
141
144
|
try {
|
|
@@ -144,15 +147,22 @@ async function loadPipelineWithDeviceFallback(
|
|
|
144
147
|
device,
|
|
145
148
|
};
|
|
146
149
|
} catch (error) {
|
|
147
|
-
|
|
150
|
+
const deviceDiagnostics = await formatOnnxRuntimeCudaDiagnostics(transformers, device, error);
|
|
151
|
+
if (deviceDiagnostics) cudaDiagnostics = deviceDiagnostics;
|
|
152
|
+
if (i === devices.length - 1) {
|
|
153
|
+
if (cudaDiagnostics) throw new Error(`${errorText(error)}\n${cudaDiagnostics}`);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
148
156
|
const fallbackDevice = devices[i + 1]!;
|
|
149
|
-
|
|
157
|
+
const meta: Record<string, unknown> = {
|
|
150
158
|
modelKey,
|
|
151
159
|
repo: spec.repo,
|
|
152
160
|
device,
|
|
153
161
|
fallbackDevice,
|
|
154
162
|
error: errorMessage(error),
|
|
155
|
-
}
|
|
163
|
+
};
|
|
164
|
+
if (deviceDiagnostics) meta.cudaDiagnostics = deviceDiagnostics;
|
|
165
|
+
sendLog(transport, "warn", "tiny-model: accelerated device failed; falling back", meta);
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
throw new Error("No tiny model devices configured");
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { Skill } from "../extensibility/skills";
|
|
4
4
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
5
5
|
import { validateRelativePath } from "../internal-urls/skill-protocol";
|
|
6
|
-
import type { InternalResource } from "../internal-urls/types";
|
|
6
|
+
import type { InternalResource, ResolveContext } from "../internal-urls/types";
|
|
7
7
|
import { normalizeLocalScheme } from "./path-utils";
|
|
8
8
|
import { ToolError } from "./tool-errors";
|
|
9
9
|
|
|
@@ -19,7 +19,7 @@ type SupportedInternalScheme = (typeof SUPPORTED_INTERNAL_SCHEMES)[number];
|
|
|
19
19
|
|
|
20
20
|
interface InternalUrlResolver {
|
|
21
21
|
canHandle(input: string): boolean;
|
|
22
|
-
resolve(input: string): Promise<InternalResource>;
|
|
22
|
+
resolve(input: string, context?: ResolveContext): Promise<InternalResource>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface InternalUrlExpansionOptions {
|
|
@@ -184,7 +184,7 @@ async function resolveInternalUrlToPath(
|
|
|
184
184
|
|
|
185
185
|
let resource: InternalResource;
|
|
186
186
|
try {
|
|
187
|
-
resource = await internalRouter.resolve(url);
|
|
187
|
+
resource = await internalRouter.resolve(url, { pathOnly: true });
|
|
188
188
|
} catch (error) {
|
|
189
189
|
const message = error instanceof Error ? error.message : String(error);
|
|
190
190
|
throw new ToolError(`Failed to resolve ${scheme}:// URL in bash command: ${url}\n${message}`);
|
package/src/tools/grep.ts
CHANGED
|
@@ -750,6 +750,11 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
750
750
|
localProtocolOptions: opts.localProtocolOptions,
|
|
751
751
|
skills: opts.skills,
|
|
752
752
|
skipDirectoryListing: true,
|
|
753
|
+
// Try path-only first so large artifacts (and any other handler that
|
|
754
|
+
// separates path from content) resolve without materializing bytes.
|
|
755
|
+
// Handlers that ignore the flag still return content, and virtual
|
|
756
|
+
// resources without a sourcePath fall through to a second resolve.
|
|
757
|
+
pathOnly: true,
|
|
753
758
|
};
|
|
754
759
|
|
|
755
760
|
for (let idx = 0; idx < paths.length; idx++) {
|
|
@@ -764,7 +769,7 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
764
769
|
if (hasGlobPathChars(globTarget)) {
|
|
765
770
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
766
771
|
}
|
|
767
|
-
|
|
772
|
+
let resource = await internalRouter.resolve(rawPath, context);
|
|
768
773
|
// A directory listing with no backing local path (e.g. a remote ssh:// dir)
|
|
769
774
|
// has no real contents to grep — searching its listing text would be
|
|
770
775
|
// misleading. Local/skill/vault dir resources set `sourcePath` and skip this.
|
|
@@ -781,8 +786,20 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
781
786
|
continue;
|
|
782
787
|
}
|
|
783
788
|
|
|
789
|
+
// No sourcePath: this handler needs its content materialized so the
|
|
790
|
+
// virtual expansion can search it. Re-resolve without pathOnly.
|
|
791
|
+
if (context.pathOnly) {
|
|
792
|
+
resource = await internalRouter.resolve(rawPath, { ...context, pathOnly: false });
|
|
793
|
+
}
|
|
794
|
+
|
|
784
795
|
const ranges = opts.pathSpecs[idx]?.ranges;
|
|
785
|
-
const expanded = await expandVirtualInternalResource(
|
|
796
|
+
const expanded = await expandVirtualInternalResource(
|
|
797
|
+
rawPath,
|
|
798
|
+
resource,
|
|
799
|
+
internalRouter,
|
|
800
|
+
{ ...context, pathOnly: false },
|
|
801
|
+
ranges,
|
|
802
|
+
);
|
|
786
803
|
virtualInputIndexes.add(idx);
|
|
787
804
|
for (const virtual of expanded) {
|
|
788
805
|
virtualResources.push(virtual);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -1201,6 +1201,10 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
1201
1201
|
signal: opts.signal,
|
|
1202
1202
|
localProtocolOptions: opts.localProtocolOptions,
|
|
1203
1203
|
skills: opts.skills,
|
|
1204
|
+
// Tool-scope resolution only needs `sourcePath`; skip content
|
|
1205
|
+
// materialization so large artifacts (or any handler that separates
|
|
1206
|
+
// path from content) stay searchable without OOM risk.
|
|
1207
|
+
pathOnly: true,
|
|
1204
1208
|
});
|
|
1205
1209
|
if (!resource.sourcePath) {
|
|
1206
1210
|
throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|
package/src/tools/read.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { normalizeToLF } from "../edit/normalize";
|
|
|
37
37
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
38
38
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
39
39
|
import { InternalUrlRouter, resolveLocalUrlToFile } from "../internal-urls";
|
|
40
|
+
import { type ResolvedArtifactFile, resolveArtifactFile } from "../internal-urls/artifact-protocol";
|
|
40
41
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
41
42
|
import type { InternalUrl } from "../internal-urls/types";
|
|
42
43
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -151,6 +152,7 @@ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx"
|
|
|
151
152
|
|
|
152
153
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
153
154
|
const MAX_SUMMARY_LINES = 20_000;
|
|
155
|
+
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
154
156
|
/**
|
|
155
157
|
* Per-line column cap for file reads. Lines wider than the value of
|
|
156
158
|
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
@@ -1519,6 +1521,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1519
1521
|
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
1520
1522
|
suffixResolution: { from: string; to: string } | undefined,
|
|
1521
1523
|
signal: AbortSignal | undefined,
|
|
1524
|
+
allowBridge = true,
|
|
1522
1525
|
): Promise<{
|
|
1523
1526
|
outputText: string;
|
|
1524
1527
|
columnTruncated: number;
|
|
@@ -1528,7 +1531,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1528
1531
|
const rawSelector = isRawSelector(parsed);
|
|
1529
1532
|
|
|
1530
1533
|
// ACP bridge first — the editor's in-memory buffer is source of truth.
|
|
1531
|
-
const bridgePromise = this.#routeReadThroughBridge(absolutePath);
|
|
1534
|
+
const bridgePromise = allowBridge ? this.#routeReadThroughBridge(absolutePath) : undefined;
|
|
1532
1535
|
if (bridgePromise !== undefined) {
|
|
1533
1536
|
try {
|
|
1534
1537
|
const bridgeText = await bridgePromise;
|
|
@@ -2802,6 +2805,197 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2802
2805
|
return toolResult<ReadToolDetails>(details).text(summary).sourcePath(absolutePath).done();
|
|
2803
2806
|
}
|
|
2804
2807
|
|
|
2808
|
+
#formatArtifactWorkflowNotice(artifact: ResolvedArtifactFile, artifactUrl: string): string {
|
|
2809
|
+
const displayPath = shortenPath(artifact.path);
|
|
2810
|
+
return `Artifact storage: ${displayPath} (${formatBytes(artifact.size)}). Use ${artifactUrl}:N-M to page, ${artifactUrl}:raw:N-M for verbatim chunks, and the artifact file path for search/copy workflows.`;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
#formatRawArtifactBlockedNotice(artifact: ResolvedArtifactFile, artifactUrl: string): string {
|
|
2814
|
+
const displayPath = shortenPath(artifact.path);
|
|
2815
|
+
return `Unbounded raw read blocked for ${artifactUrl} (${formatBytes(
|
|
2816
|
+
artifact.size,
|
|
2817
|
+
)}). Reading the whole artifact verbatim can exhaust memory. Use ${artifactUrl}:raw:1-3000 for bounded verbatim chunks, ${artifactUrl}:1-3000 for numbered exploration, and the artifact file path for search/copy workflows: ${displayPath}`;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
async #readArtifactFile(
|
|
2821
|
+
url: InternalUrl,
|
|
2822
|
+
parsedSel: ParsedSelector,
|
|
2823
|
+
signal?: AbortSignal,
|
|
2824
|
+
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2825
|
+
const artifact = await resolveArtifactFile(url, {
|
|
2826
|
+
cwd: this.session.cwd,
|
|
2827
|
+
settings: this.session.settings,
|
|
2828
|
+
signal,
|
|
2829
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
2830
|
+
skills: this.session.skills,
|
|
2831
|
+
});
|
|
2832
|
+
const artifactUrl = `artifact://${artifact.id}`;
|
|
2833
|
+
const details: ReadToolDetails = {
|
|
2834
|
+
resolvedPath: artifact.path,
|
|
2835
|
+
contentType: "text/plain",
|
|
2836
|
+
};
|
|
2837
|
+
|
|
2838
|
+
if (parsedSel.kind === "raw" && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
|
|
2839
|
+
return toolResult<ReadToolDetails>(details)
|
|
2840
|
+
.text(this.#formatRawArtifactBlockedNotice(artifact, artifactUrl))
|
|
2841
|
+
.sourcePath(artifact.path)
|
|
2842
|
+
.sourceInternal(url.href)
|
|
2843
|
+
.done();
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
const rawSelector = isRawSelector(parsedSel);
|
|
2847
|
+
const displayMode = resolveFileDisplayMode(this.session, { raw: rawSelector, immutable: true });
|
|
2848
|
+
if (isMultiRange(parsedSel) && parsedSel.kind === "lines") {
|
|
2849
|
+
const read = await this.#readLocalFileMultiRange(
|
|
2850
|
+
artifact.path,
|
|
2851
|
+
parsedSel.ranges,
|
|
2852
|
+
artifact.size,
|
|
2853
|
+
parsedSel,
|
|
2854
|
+
displayMode,
|
|
2855
|
+
undefined,
|
|
2856
|
+
signal,
|
|
2857
|
+
false,
|
|
2858
|
+
);
|
|
2859
|
+
if (read.bridgeResult) return read.bridgeResult;
|
|
2860
|
+
if (read.displayContent) details.displayContent = read.displayContent;
|
|
2861
|
+
let text = read.outputText;
|
|
2862
|
+
if (!rawSelector && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
|
|
2863
|
+
text = text
|
|
2864
|
+
? `${text}\n\n[${this.#formatArtifactWorkflowNotice(artifact, artifactUrl)}]`
|
|
2865
|
+
: this.#formatArtifactWorkflowNotice(artifact, artifactUrl);
|
|
2866
|
+
}
|
|
2867
|
+
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
2868
|
+
.text(text)
|
|
2869
|
+
.sourcePath(artifact.path)
|
|
2870
|
+
.sourceInternal(url.href);
|
|
2871
|
+
if (read.columnTruncated > 0) resultBuilder.limits({ columnMax: read.columnTruncated });
|
|
2872
|
+
return resultBuilder.done();
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
const { offset, limit } = selToOffsetLimit(parsedSel);
|
|
2876
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
2877
|
+
const expandStart = offset !== undefined && offset > 1;
|
|
2878
|
+
const expandEnd = limit !== undefined;
|
|
2879
|
+
const leadingContext = expandStart ? Math.min(requestedStart, RANGE_LEADING_CONTEXT_LINES) : 0;
|
|
2880
|
+
const trailingContext = expandEnd ? RANGE_TRAILING_CONTEXT_LINES : 0;
|
|
2881
|
+
const startLine = requestedStart - leadingContext;
|
|
2882
|
+
const startLineDisplay = startLine + 1;
|
|
2883
|
+
const effectiveLimit = limit ?? this.#defaultLimit;
|
|
2884
|
+
const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
2885
|
+
const selectedLineLimit = effectiveLimit + leadingContext + trailingContext;
|
|
2886
|
+
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
2887
|
+
const streamResult = await streamLinesFromFile(
|
|
2888
|
+
artifact.path,
|
|
2889
|
+
startLine,
|
|
2890
|
+
maxLinesToCollect,
|
|
2891
|
+
maxBytesForRead,
|
|
2892
|
+
selectedLineLimit,
|
|
2893
|
+
signal,
|
|
2894
|
+
artifact.size > SNAPSHOT_MAX_BYTES,
|
|
2895
|
+
);
|
|
2896
|
+
const {
|
|
2897
|
+
lines: collectedLines,
|
|
2898
|
+
totalFileLines,
|
|
2899
|
+
collectedBytes,
|
|
2900
|
+
stoppedByByteLimit,
|
|
2901
|
+
firstLinePreview,
|
|
2902
|
+
firstLineByteLength,
|
|
2903
|
+
reachedEof,
|
|
2904
|
+
} = streamResult;
|
|
2905
|
+
|
|
2906
|
+
if (requestedStart >= totalFileLines) {
|
|
2907
|
+
const suggestion =
|
|
2908
|
+
totalFileLines === 0
|
|
2909
|
+
? "The artifact is empty."
|
|
2910
|
+
: `Use ${artifactUrl}:1 to read from the start, or ${artifactUrl}:${totalFileLines} to read the last line.`;
|
|
2911
|
+
return toolResult<ReadToolDetails>(details)
|
|
2912
|
+
.text(`Line ${requestedStart + 1} is beyond end of artifact (${totalFileLines} lines total). ${suggestion}`)
|
|
2913
|
+
.sourcePath(artifact.path)
|
|
2914
|
+
.sourceInternal(url.href)
|
|
2915
|
+
.done();
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
const shouldAddLineNumbers = rawSelector ? false : displayMode.hashLines ? false : displayMode.lineNumbers;
|
|
2919
|
+
const selectedContent = collectedLines.join("\n");
|
|
2920
|
+
const totalSelectedLines = totalFileLines - startLine;
|
|
2921
|
+
const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit;
|
|
2922
|
+
const firstLineExceedsLimit = firstLineByteLength !== undefined && firstLineByteLength > maxBytesForRead;
|
|
2923
|
+
const truncation: TruncationResult = {
|
|
2924
|
+
content: selectedContent,
|
|
2925
|
+
truncated: wasTruncated,
|
|
2926
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
2927
|
+
totalLines: totalSelectedLines,
|
|
2928
|
+
totalBytes: collectedBytes,
|
|
2929
|
+
outputLines: collectedLines.length,
|
|
2930
|
+
outputBytes: collectedBytes,
|
|
2931
|
+
lastLinePartial: false,
|
|
2932
|
+
firstLineExceedsLimit,
|
|
2933
|
+
};
|
|
2934
|
+
|
|
2935
|
+
let displayContent: { text: string; startLine: number; lineNumbers?: Array<number | null> } | undefined;
|
|
2936
|
+
const formatText = (text: string, startNum: number): string => {
|
|
2937
|
+
const lineCount = countTextLines(text);
|
|
2938
|
+
displayContent = {
|
|
2939
|
+
text,
|
|
2940
|
+
startLine: startNum,
|
|
2941
|
+
lineNumbers: Array.from({ length: lineCount }, (_, i) => startNum + i),
|
|
2942
|
+
};
|
|
2943
|
+
return formatTextWithMode(text, startNum, false, shouldAddLineNumbers);
|
|
2944
|
+
};
|
|
2945
|
+
|
|
2946
|
+
let outputText: string;
|
|
2947
|
+
let truncationInfo:
|
|
2948
|
+
| { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
|
|
2949
|
+
| undefined;
|
|
2950
|
+
if (truncation.firstLineExceedsLimit) {
|
|
2951
|
+
const firstLineBytes = firstLineByteLength ?? 0;
|
|
2952
|
+
const snippet = firstLinePreview ?? { text: "", bytes: 0 };
|
|
2953
|
+
outputText =
|
|
2954
|
+
snippet.text.length > 0
|
|
2955
|
+
? formatText(snippet.text, startLineDisplay)
|
|
2956
|
+
: `[Line ${startLineDisplay} is ${formatBytes(
|
|
2957
|
+
firstLineBytes,
|
|
2958
|
+
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
2959
|
+
truncationInfo = {
|
|
2960
|
+
result: truncation,
|
|
2961
|
+
options: {
|
|
2962
|
+
direction: "head",
|
|
2963
|
+
startLine: startLineDisplay,
|
|
2964
|
+
totalFileLines: reachedEof ? totalFileLines : undefined,
|
|
2965
|
+
},
|
|
2966
|
+
};
|
|
2967
|
+
} else {
|
|
2968
|
+
outputText = formatText(truncation.content, startLineDisplay);
|
|
2969
|
+
if (truncation.truncated) {
|
|
2970
|
+
truncationInfo = {
|
|
2971
|
+
result: truncation,
|
|
2972
|
+
options: {
|
|
2973
|
+
direction: "head",
|
|
2974
|
+
startLine: startLineDisplay,
|
|
2975
|
+
totalFileLines: reachedEof ? totalFileLines : undefined,
|
|
2976
|
+
},
|
|
2977
|
+
};
|
|
2978
|
+
} else if (startLine + collectedLines.length < totalFileLines || !reachedEof) {
|
|
2979
|
+
const nextOffset = startLine + collectedLines.length + 1;
|
|
2980
|
+
outputText += reachedEof
|
|
2981
|
+
? `\n\n[${totalFileLines - (startLine + collectedLines.length)} more lines in artifact. Use ${artifactUrl}:${nextOffset} to continue]`
|
|
2982
|
+
: `\n\n[More lines in artifact (${formatBytes(artifact.size)} total; not scanned to EOF). Use ${artifactUrl}:${nextOffset} to continue]`;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
if (!rawSelector && artifact.size > MAX_ARTIFACT_RAW_INLINE_BYTES) {
|
|
2987
|
+
outputText += `\n\n[${this.#formatArtifactWorkflowNotice(artifact, artifactUrl)}]`;
|
|
2988
|
+
}
|
|
2989
|
+
if (displayContent) details.displayContent = displayContent;
|
|
2990
|
+
if (truncationInfo) details.truncation = truncationInfo.result;
|
|
2991
|
+
const resultBuilder = toolResult<ReadToolDetails>(details)
|
|
2992
|
+
.text(outputText)
|
|
2993
|
+
.sourcePath(artifact.path)
|
|
2994
|
+
.sourceInternal(url.href);
|
|
2995
|
+
if (truncationInfo) resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
2996
|
+
return resultBuilder.done();
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2805
2999
|
/**
|
|
2806
3000
|
* Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://).
|
|
2807
3001
|
* Supports pagination via offset/limit but rejects them when query extraction is used.
|
|
@@ -2829,6 +3023,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2829
3023
|
const hasQueryExtraction = queryParam !== null && queryParam !== "";
|
|
2830
3024
|
hasExtraction = hasPathExtraction || hasQueryExtraction;
|
|
2831
3025
|
}
|
|
3026
|
+
if (scheme === "artifact") {
|
|
3027
|
+
return this.#readArtifactFile(urlMeta, parsedSel, signal);
|
|
3028
|
+
}
|
|
2832
3029
|
|
|
2833
3030
|
// local:// files are real on-disk paths. Detect image files and emit a
|
|
2834
3031
|
// decoded image block before the text-only resource contract UTF-8
|
package/src/utils/git.ts
CHANGED
|
@@ -1726,6 +1726,26 @@ export const cherryPick = Object.assign(
|
|
|
1726
1726
|
async abort(cwd: string, signal?: AbortSignal): Promise<void> {
|
|
1727
1727
|
await runEffect(cwd, ["cherry-pick", "--abort"], { signal });
|
|
1728
1728
|
},
|
|
1729
|
+
/**
|
|
1730
|
+
* Skip the current commit of an in-progress cherry-pick sequence and
|
|
1731
|
+
* continue with the rest of the range. Use after {@link isEmptyError}
|
|
1732
|
+
* reports the current attempt collapsed to a no-op — the alternative,
|
|
1733
|
+
* `--abort`, throws away every remaining commit in the range.
|
|
1734
|
+
*/
|
|
1735
|
+
async skip(cwd: string, signal?: AbortSignal): Promise<void> {
|
|
1736
|
+
await runEffect(cwd, ["cherry-pick", "--skip"], { signal });
|
|
1737
|
+
},
|
|
1738
|
+
/**
|
|
1739
|
+
* True when a cherry-pick failure was caused by the current commit
|
|
1740
|
+
* being empty against HEAD — either redundant with an already-applied
|
|
1741
|
+
* change, or auto-resolved to HEAD by a 3-way merge. Callers should
|
|
1742
|
+
* `--skip` in this case to advance the sequencer rather than aborting
|
|
1743
|
+
* the whole range: an empty commit is not a merge conflict, and any
|
|
1744
|
+
* later commits in the range still deserve to land.
|
|
1745
|
+
*/
|
|
1746
|
+
isEmptyError(err: unknown): boolean {
|
|
1747
|
+
return err instanceof GitCommandError && /the previous cherry-pick is now empty/i.test(err.result.stderr);
|
|
1748
|
+
},
|
|
1729
1749
|
},
|
|
1730
1750
|
);
|
|
1731
1751
|
|
package/src/utils/open.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as url from "node:url";
|
|
4
|
-
import
|
|
4
|
+
import { $which, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
const URL_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:/;
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ function getExistingWslLocalPath(urlOrPath: string): string | undefined {
|
|
|
9
9
|
if (
|
|
10
10
|
process.platform !== "linux" ||
|
|
11
11
|
!(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) ||
|
|
12
|
-
|
|
12
|
+
!$which("wslview")
|
|
13
13
|
) {
|
|
14
14
|
return undefined;
|
|
15
15
|
}
|
|
@@ -31,6 +31,24 @@ function getExistingWslLocalPath(urlOrPath: string): string | undefined {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the Windows `rundll32.exe` command used to hand a URL/path to the
|
|
36
|
+
* user's registered protocol handler. Anchoring to `%SystemRoot%\System32`
|
|
37
|
+
* (rather than relying on `rundll32` being on `PATH`) survives environments
|
|
38
|
+
* where the machine `PATH` no longer references `System32` — a common
|
|
39
|
+
* real-world misconfiguration where `System32\Wbem` / `WindowsPowerShell` /
|
|
40
|
+
* `OpenSSH` survive but `System32` itself is dropped. Bare `rundll32` on
|
|
41
|
+
* such boxes throws `Executable not found in $PATH: "rundll32"` from
|
|
42
|
+
* `Bun.spawn` before ShellExecute ever sees the URL.
|
|
43
|
+
*/
|
|
44
|
+
function windowsOpenerCommand(target: string): string[] {
|
|
45
|
+
const systemRoot = process.env.SystemRoot?.trim() || process.env.SYSTEMROOT?.trim() || "C:\\Windows";
|
|
46
|
+
// `path.win32` (not the platform-adaptive `path.join`) keeps Windows path
|
|
47
|
+
// separators when tests run under a POSIX host and matches Windows call
|
|
48
|
+
// conventions on the real target.
|
|
49
|
+
const rundll32 = path.win32.join(systemRoot, "System32", "rundll32.exe");
|
|
50
|
+
return [rundll32, "url.dll,FileProtocolHandler", target];
|
|
51
|
+
}
|
|
34
52
|
/** Open a URL or file path in the default browser/application. Best-effort, never throws. */
|
|
35
53
|
export function openPath(urlOrPath: string): void {
|
|
36
54
|
let cmd: string[];
|
|
@@ -39,7 +57,7 @@ export function openPath(urlOrPath: string): void {
|
|
|
39
57
|
cmd = ["open", urlOrPath];
|
|
40
58
|
break;
|
|
41
59
|
case "win32":
|
|
42
|
-
cmd =
|
|
60
|
+
cmd = windowsOpenerCommand(urlOrPath);
|
|
43
61
|
break;
|
|
44
62
|
default: {
|
|
45
63
|
const wslPath = getExistingWslLocalPath(urlOrPath);
|
|
@@ -47,9 +65,36 @@ export function openPath(urlOrPath: string): void {
|
|
|
47
65
|
break;
|
|
48
66
|
}
|
|
49
67
|
}
|
|
68
|
+
let child: Bun.Subprocess | undefined;
|
|
50
69
|
try {
|
|
51
|
-
Bun.spawn(cmd, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
52
|
-
} catch {
|
|
53
|
-
//
|
|
70
|
+
child = Bun.spawn(cmd, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// Spawn threw synchronously (missing binary, denied exec, sandbox
|
|
73
|
+
// restriction, …). Best-effort: log so the failure isn't invisible while
|
|
74
|
+
// still letting the caller advertise a copy-URL fallback.
|
|
75
|
+
logger.warn("Failed to open external URL/path", {
|
|
76
|
+
command: cmd[0],
|
|
77
|
+
target: urlOrPath,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
54
81
|
}
|
|
82
|
+
// Detect delayed failures (exec succeeded but the opener exited non-zero)
|
|
83
|
+
// without blocking the caller. Recording them makes silent misconfigurations
|
|
84
|
+
// (e.g. `xdg-open` present but no MIME handler for `https`) diagnosable from
|
|
85
|
+
// `~/.omp/logs/omp.*.log`.
|
|
86
|
+
child.exited.then(
|
|
87
|
+
exitCode => {
|
|
88
|
+
if (typeof exitCode === "number" && exitCode !== 0) {
|
|
89
|
+
logger.warn("External opener exited with non-zero status", {
|
|
90
|
+
command: cmd[0],
|
|
91
|
+
target: urlOrPath,
|
|
92
|
+
exitCode,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
() => {
|
|
97
|
+
// Ignore — awaiting the subprocess is best-effort telemetry.
|
|
98
|
+
},
|
|
99
|
+
);
|
|
55
100
|
}
|