@oh-my-pi/pi-coding-agent 16.1.8 → 16.1.9
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 +25 -0
- package/dist/cli.js +4621 -3814
- package/dist/types/cli/flag-tables.d.ts +17 -0
- package/dist/types/cli-commands.d.ts +4 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/package.json +12 -12
- package/src/cli/args.ts +0 -1
- package/src/cli/flag-tables.ts +42 -0
- package/src/cli/profile-bootstrap.ts +1 -11
- package/src/cli-commands.ts +48 -3
- package/src/config/model-registry.ts +6 -7
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +39 -25
- package/src/modes/controllers/command-controller.ts +18 -3
- package/src/modes/controllers/mcp-command-controller.ts +40 -12
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/types.ts +1 -1
- package/src/session/agent-session.ts +36 -1
- package/src/slash-commands/builtin-registry.ts +24 -66
- package/src/tools/browser/launch.ts +107 -31
- package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
- package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
- package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
- package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
- package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
- package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
- package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
- package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
- package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
- package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
- package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
- package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
- package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
- package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
package/src/lsp/client.ts
CHANGED
|
@@ -327,9 +327,45 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
327
327
|
try {
|
|
328
328
|
const message: LspJsonRpcResponse | LspJsonRpcNotification = JSON.parse(messageText);
|
|
329
329
|
|
|
330
|
-
// Route message
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
// Route message. A JSON-RPC message carrying a `method` is always
|
|
331
|
+
// server-originated: a request when it also has an `id`, a
|
|
332
|
+
// notification otherwise. A message with only an `id` is a response
|
|
333
|
+
// to one of our requests. Disambiguate on `method` FIRST: a
|
|
334
|
+
// server's request ids live in its own id space and routinely
|
|
335
|
+
// collide with our in-flight client request ids (e.g. a
|
|
336
|
+
// basedpyright `workspace/configuration` pull arriving while a
|
|
337
|
+
// `documentSymbol` request with the same id is pending). Matching
|
|
338
|
+
// pending requests first would swallow that pull as a bogus
|
|
339
|
+
// response -- dropping the config answer the server blocks on and
|
|
340
|
+
// resolving our request with `undefined`, wedging the lazy
|
|
341
|
+
// cold-start handshake (#3001).
|
|
342
|
+
if ("method" in message) {
|
|
343
|
+
if ("id" in message && message.id !== undefined) {
|
|
344
|
+
// Server-initiated request: must be answered.
|
|
345
|
+
await handleServerRequest(client, message as LspJsonRpcRequest);
|
|
346
|
+
} else {
|
|
347
|
+
// Server notification
|
|
348
|
+
if (message.method === "textDocument/publishDiagnostics" && message.params) {
|
|
349
|
+
const params = message.params as PublishDiagnosticsParams;
|
|
350
|
+
client.diagnostics.set(params.uri, {
|
|
351
|
+
diagnostics: params.diagnostics,
|
|
352
|
+
version: params.version ?? null,
|
|
353
|
+
});
|
|
354
|
+
client.diagnosticsVersion += 1;
|
|
355
|
+
} else if (message.method === "$/progress" && message.params) {
|
|
356
|
+
const params = message.params as { token: string | number; value?: { kind?: string } };
|
|
357
|
+
if (params.value?.kind === "begin") {
|
|
358
|
+
client.activeProgressTokens.add(params.token);
|
|
359
|
+
} else if (params.value?.kind === "end") {
|
|
360
|
+
client.activeProgressTokens.delete(params.token);
|
|
361
|
+
if (client.activeProgressTokens.size === 0) {
|
|
362
|
+
client.resolveProjectLoaded();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} else if ("id" in message && message.id !== undefined) {
|
|
368
|
+
// Response to one of our requests.
|
|
333
369
|
const pending = client.pendingRequests.get(message.id);
|
|
334
370
|
if (pending) {
|
|
335
371
|
client.pendingRequests.delete(message.id);
|
|
@@ -338,28 +374,6 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|
|
338
374
|
} else {
|
|
339
375
|
pending.resolve(message.result);
|
|
340
376
|
}
|
|
341
|
-
} else if ("method" in message) {
|
|
342
|
-
await handleServerRequest(client, message as LspJsonRpcRequest);
|
|
343
|
-
}
|
|
344
|
-
} else if ("method" in message) {
|
|
345
|
-
// Server notification
|
|
346
|
-
if (message.method === "textDocument/publishDiagnostics" && message.params) {
|
|
347
|
-
const params = message.params as PublishDiagnosticsParams;
|
|
348
|
-
client.diagnostics.set(params.uri, {
|
|
349
|
-
diagnostics: params.diagnostics,
|
|
350
|
-
version: params.version ?? null,
|
|
351
|
-
});
|
|
352
|
-
client.diagnosticsVersion += 1;
|
|
353
|
-
} else if (message.method === "$/progress" && message.params) {
|
|
354
|
-
const params = message.params as { token: string | number; value?: { kind?: string } };
|
|
355
|
-
if (params.value?.kind === "begin") {
|
|
356
|
-
client.activeProgressTokens.add(params.token);
|
|
357
|
-
} else if (params.value?.kind === "end") {
|
|
358
|
-
client.activeProgressTokens.delete(params.token);
|
|
359
|
-
if (client.activeProgressTokens.size === 0) {
|
|
360
|
-
client.resolveProjectLoaded();
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
377
|
}
|
|
364
378
|
}
|
|
365
379
|
} catch (err) {
|
|
@@ -85,15 +85,30 @@ export class CommandController {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
handleDumpCommand() {
|
|
88
|
+
async handleDumpCommand(): Promise<void> {
|
|
89
89
|
try {
|
|
90
90
|
const formatted = this.ctx.session.formatSessionAsText();
|
|
91
91
|
if (!formatted) {
|
|
92
92
|
this.ctx.showError("No messages to dump yet.");
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Build the LLM request JSON sidecar first so its path (and a
|
|
96
|
+
// raw-context warning) can be appended to the copied transcript.
|
|
97
|
+
let sidecarPath: string | undefined;
|
|
98
|
+
let sidecarError: string | undefined;
|
|
99
|
+
try {
|
|
100
|
+
sidecarPath = await this.ctx.session.dumpLlmRequestToTmpDir();
|
|
101
|
+
} catch (error: unknown) {
|
|
102
|
+
sidecarError = error instanceof Error ? error.message : "Unknown error";
|
|
103
|
+
}
|
|
104
|
+
const doc = sidecarPath
|
|
105
|
+
? `${formatted}\n\n---\nLLM request JSON: ${sidecarPath}\nThis file persists on disk and may contain raw context/secrets — treat accordingly.`
|
|
106
|
+
: formatted;
|
|
107
|
+
await copyToClipboard(doc);
|
|
108
|
+
const statusParts = ["Session copied to clipboard"];
|
|
109
|
+
if (sidecarPath) statusParts.push(`LLM request JSON: ${sidecarPath}`);
|
|
110
|
+
if (sidecarError) statusParts.push(`LLM request JSON unavailable: ${sidecarError}`);
|
|
111
|
+
this.ctx.showStatus(statusParts.join("\n"));
|
|
97
112
|
} catch (error: unknown) {
|
|
98
113
|
this.ctx.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
99
114
|
}
|
|
@@ -8,7 +8,7 @@ import { type Component, replaceTabs, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type { SourceMeta } from "../../capability/types";
|
|
10
10
|
import { expandEnvVarsDeep } from "../../discovery/helpers";
|
|
11
|
-
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
|
|
11
|
+
import { analyzeAuthError, discoverOAuthEndpoints, loadAllMCPConfigs, MCPManager } from "../../mcp";
|
|
12
12
|
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
|
|
13
13
|
import {
|
|
14
14
|
addMCPServer,
|
|
@@ -1383,7 +1383,7 @@ export class MCPCommandController {
|
|
|
1383
1383
|
}
|
|
1384
1384
|
await setServerDisabled(userConfigPath, name, !enabled);
|
|
1385
1385
|
if (enabled) {
|
|
1386
|
-
await this.#
|
|
1386
|
+
await this.#connectEnabledMCPServer(name);
|
|
1387
1387
|
const state = await this.#waitForServerConnectionWithAnimation(name);
|
|
1388
1388
|
const status =
|
|
1389
1389
|
state === "connected"
|
|
@@ -1419,7 +1419,12 @@ export class MCPCommandController {
|
|
|
1419
1419
|
|
|
1420
1420
|
const updated: MCPServerConfig = { ...found.config, enabled };
|
|
1421
1421
|
await updateMCPServer(found.filePath, name, updated);
|
|
1422
|
-
|
|
1422
|
+
if (enabled) {
|
|
1423
|
+
await this.#connectEnabledMCPServer(name);
|
|
1424
|
+
} else {
|
|
1425
|
+
await this.ctx.mcpManager?.disconnectServer(name);
|
|
1426
|
+
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []);
|
|
1427
|
+
}
|
|
1423
1428
|
|
|
1424
1429
|
let status = "";
|
|
1425
1430
|
if (enabled) {
|
|
@@ -1671,6 +1676,37 @@ export class MCPCommandController {
|
|
|
1671
1676
|
}
|
|
1672
1677
|
}
|
|
1673
1678
|
|
|
1679
|
+
async #connectEnabledMCPServer(name: string): Promise<void> {
|
|
1680
|
+
if (!this.ctx.mcpManager) {
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const { configs, sources } = await loadAllMCPConfigs(getProjectDir());
|
|
1685
|
+
const config = configs[name];
|
|
1686
|
+
if (!config) {
|
|
1687
|
+
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const source = sources[name];
|
|
1692
|
+
const result = await this.ctx.mcpManager.connectServers({ [name]: config }, source ? { [name]: source } : {});
|
|
1693
|
+
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
1694
|
+
this.#showMCPConnectionErrors(result.errors);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
#showMCPConnectionErrors(errors: Map<string, string>): void {
|
|
1698
|
+
if (errors.size === 0) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const errorLines = ["", theme.fg("warning", "Some servers failed to connect:"), ""];
|
|
1703
|
+
for (const [serverName, error] of errors.entries()) {
|
|
1704
|
+
errorLines.push(` ${serverName}: ${error}`);
|
|
1705
|
+
}
|
|
1706
|
+
errorLines.push("");
|
|
1707
|
+
this.#showMessage(errorLines.join("\n"));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1674
1710
|
/**
|
|
1675
1711
|
* Reload MCP manager with new configs
|
|
1676
1712
|
*/
|
|
@@ -1686,15 +1722,7 @@ export class MCPCommandController {
|
|
|
1686
1722
|
const result = await this.ctx.mcpManager.discoverAndConnect();
|
|
1687
1723
|
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
|
|
1688
1724
|
|
|
1689
|
-
|
|
1690
|
-
if (result.errors.size > 0) {
|
|
1691
|
-
const errorLines = ["", theme.fg("warning", "Some servers failed to connect:"), ""];
|
|
1692
|
-
for (const [serverName, error] of result.errors.entries()) {
|
|
1693
|
-
errorLines.push(` ${serverName}: ${error}`);
|
|
1694
|
-
}
|
|
1695
|
-
errorLines.push("");
|
|
1696
|
-
this.#showMessage(errorLines.join("\n"));
|
|
1697
|
-
}
|
|
1725
|
+
this.#showMCPConnectionErrors(result.errors);
|
|
1698
1726
|
}
|
|
1699
1727
|
|
|
1700
1728
|
/**
|
|
@@ -3491,7 +3491,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3491
3491
|
return this.#commandController.handleExportCommand(text);
|
|
3492
3492
|
}
|
|
3493
3493
|
|
|
3494
|
-
handleDumpCommand() {
|
|
3494
|
+
async handleDumpCommand(): Promise<void> {
|
|
3495
3495
|
return this.#commandController.handleDumpCommand();
|
|
3496
3496
|
}
|
|
3497
3497
|
|
package/src/modes/types.ts
CHANGED
|
@@ -290,7 +290,7 @@ export interface InteractiveModeContext {
|
|
|
290
290
|
handleHotkeysCommand(): void;
|
|
291
291
|
handleToolsCommand(): void;
|
|
292
292
|
handleContextCommand(): void;
|
|
293
|
-
handleDumpCommand(): void
|
|
293
|
+
handleDumpCommand(): Promise<void>;
|
|
294
294
|
handleAdvisorDumpCommand(isRaw?: boolean): void;
|
|
295
295
|
handleDebugTranscriptCommand(): Promise<void>;
|
|
296
296
|
handleClearCommand(): Promise<void>;
|
|
@@ -103,7 +103,7 @@ import {
|
|
|
103
103
|
resolveServiceTier,
|
|
104
104
|
streamSimple,
|
|
105
105
|
} from "@oh-my-pi/pi-ai";
|
|
106
|
-
import { stripToolDescriptions } from "@oh-my-pi/pi-ai/utils/schema";
|
|
106
|
+
import { stripToolDescriptions, toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
107
107
|
import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
|
|
108
108
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
109
109
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
@@ -12606,6 +12606,41 @@ export class AgentSession {
|
|
|
12606
12606
|
});
|
|
12607
12607
|
}
|
|
12608
12608
|
|
|
12609
|
+
/**
|
|
12610
|
+
* Dump the current session's LLM-facing request context as JSON to a
|
|
12611
|
+
* auto-named file in `os.tmpdir()`. This is the synchronous
|
|
12612
|
+
* `convertToLlm`-boundary snapshot — system prompt, tools (wire schemas),
|
|
12613
|
+
* thinking/service tier, and converted messages — with no network round-trip
|
|
12614
|
+
* and no arming flag, so advisor/side requests cannot intercept it.
|
|
12615
|
+
*
|
|
12616
|
+
* The file persists on disk and may contain the same raw context/secrets
|
|
12617
|
+
* as `/dump`; treat the path accordingly.
|
|
12618
|
+
*
|
|
12619
|
+
* @returns the written file path, or `undefined` when there are no messages.
|
|
12620
|
+
*/
|
|
12621
|
+
async dumpLlmRequestToTmpDir(): Promise<string | undefined> {
|
|
12622
|
+
const messages = this.messages;
|
|
12623
|
+
if (messages.length === 0) return undefined;
|
|
12624
|
+
const llmMessages = await this.convertMessagesToLlm(messages);
|
|
12625
|
+
const payload = {
|
|
12626
|
+
model: this.agent.state.model ?? null,
|
|
12627
|
+
thinkingLevel: this.#thinkingLevel ?? null,
|
|
12628
|
+
serviceTier: this.agent.serviceTier ?? null,
|
|
12629
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
12630
|
+
tools: this.agent.state.tools.map(tool => ({
|
|
12631
|
+
name: tool.name,
|
|
12632
|
+
description: tool.description,
|
|
12633
|
+
parameters: toolWireSchema(tool),
|
|
12634
|
+
...(tool.strict !== undefined ? { strict: tool.strict } : {}),
|
|
12635
|
+
...(tool.customWireName ? { customWireName: tool.customWireName } : {}),
|
|
12636
|
+
})),
|
|
12637
|
+
messages: llmMessages,
|
|
12638
|
+
};
|
|
12639
|
+
const filePath = path.join(os.tmpdir(), `omp-llm-request-${Snowflake.next()}.json`);
|
|
12640
|
+
await Bun.write(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
12641
|
+
return filePath;
|
|
12642
|
+
}
|
|
12643
|
+
|
|
12609
12644
|
/**
|
|
12610
12645
|
* Enable or disable the advisor for this session. The setting is overridden for the session,
|
|
12611
12646
|
* and the runtime is started or stopped to match.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
5
|
-
import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
|
|
6
4
|
import { type AutocompleteItem, Spacer } from "@oh-my-pi/pi-tui";
|
|
7
5
|
import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
8
6
|
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
@@ -192,46 +190,6 @@ async function handleUsageResetCommand(
|
|
|
192
190
|
await output(describeRedeemOutcome(outcome, target.label));
|
|
193
191
|
}
|
|
194
192
|
|
|
195
|
-
const DEBUG_DUMP_NEXT_REQUEST_USAGE = "Usage: /debug dump-next-request <path>";
|
|
196
|
-
|
|
197
|
-
function resolveDebugRequestDumpPath(target: string, cwd: string): string {
|
|
198
|
-
const expanded =
|
|
199
|
-
target === "~"
|
|
200
|
-
? os.homedir()
|
|
201
|
-
: target.startsWith("~/") || target.startsWith("~\\")
|
|
202
|
-
? path.join(os.homedir(), target.slice(2))
|
|
203
|
-
: target;
|
|
204
|
-
return path.resolve(cwd, expanded);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function handleDebugSubcommand(
|
|
208
|
-
args: string,
|
|
209
|
-
cwd: string,
|
|
210
|
-
output: (text: string) => Promise<void> | void,
|
|
211
|
-
): Promise<SlashCommandResult> {
|
|
212
|
-
const { verb, rest } = parseSubcommand(args);
|
|
213
|
-
switch (verb) {
|
|
214
|
-
case "":
|
|
215
|
-
await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
|
|
216
|
-
return commandConsumed();
|
|
217
|
-
case "dump-next-request":
|
|
218
|
-
case "dump-request":
|
|
219
|
-
case "next-request": {
|
|
220
|
-
if (!rest) {
|
|
221
|
-
await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
|
|
222
|
-
return commandConsumed();
|
|
223
|
-
}
|
|
224
|
-
const requestPath = resolveDebugRequestDumpPath(rest, cwd);
|
|
225
|
-
setNextRequestDebugPath(requestPath);
|
|
226
|
-
await output(`Next AI provider request will be dumped to ${requestPath}`);
|
|
227
|
-
return commandConsumed();
|
|
228
|
-
}
|
|
229
|
-
default:
|
|
230
|
-
await output(`Unknown /debug subcommand "${verb}". ${DEBUG_DUMP_NEXT_REQUEST_USAGE}`);
|
|
231
|
-
return commandConsumed();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
193
|
/** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
|
|
236
194
|
function parseShakeMode(args: string): ShakeMode | { error: string } {
|
|
237
195
|
const verb = args.trim().toLowerCase();
|
|
@@ -612,16 +570,33 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
612
570
|
},
|
|
613
571
|
{
|
|
614
572
|
name: "dump",
|
|
615
|
-
description: "Copy session transcript to clipboard",
|
|
616
|
-
acpDescription: "Return full transcript as plain text",
|
|
573
|
+
description: "Copy session transcript to clipboard (and write LLM request JSON to tmp)",
|
|
574
|
+
acpDescription: "Return full transcript as plain text, with LLM request JSON path",
|
|
617
575
|
allowArgs: true,
|
|
618
576
|
handle: async (_command, runtime) => {
|
|
619
577
|
const text = runtime.session.formatSessionAsText();
|
|
620
|
-
|
|
578
|
+
if (!text) {
|
|
579
|
+
await runtime.output("No messages to dump yet.");
|
|
580
|
+
return commandConsumed();
|
|
581
|
+
}
|
|
582
|
+
let sidecarPath: string | undefined;
|
|
583
|
+
try {
|
|
584
|
+
sidecarPath = await runtime.session.dumpLlmRequestToTmpDir();
|
|
585
|
+
} catch {
|
|
586
|
+
// Sidecar is best-effort; the transcript is still output below.
|
|
587
|
+
}
|
|
588
|
+
const lines = [text];
|
|
589
|
+
if (sidecarPath)
|
|
590
|
+
lines.push(
|
|
591
|
+
"",
|
|
592
|
+
`LLM request JSON: ${sidecarPath}`,
|
|
593
|
+
"This file persists on disk and may contain raw context/secrets — treat accordingly.",
|
|
594
|
+
);
|
|
595
|
+
await runtime.output(lines.join("\n"));
|
|
621
596
|
return commandConsumed();
|
|
622
597
|
},
|
|
623
|
-
handleTui: (_command, runtime) => {
|
|
624
|
-
runtime.ctx.handleDumpCommand();
|
|
598
|
+
handleTui: async (_command, runtime) => {
|
|
599
|
+
await runtime.ctx.handleDumpCommand();
|
|
625
600
|
runtime.ctx.editor.setText("");
|
|
626
601
|
},
|
|
627
602
|
},
|
|
@@ -1509,25 +1484,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1509
1484
|
{
|
|
1510
1485
|
name: "debug",
|
|
1511
1486
|
description: "Open debug tools selector",
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
{
|
|
1515
|
-
name: "dump-next-request",
|
|
1516
|
-
description: "Dump the next AI provider HTTP request as JSON",
|
|
1517
|
-
usage: "<path>",
|
|
1518
|
-
},
|
|
1519
|
-
],
|
|
1520
|
-
handle: async (command, runtime) =>
|
|
1521
|
-
handleDebugSubcommand(command.args, runtime.cwd, text => runtime.output(text)),
|
|
1522
|
-
handleTui: async (command, runtime) => {
|
|
1523
|
-
const args = command.args.trim();
|
|
1524
|
-
if (args.length === 0) {
|
|
1525
|
-
runtime.ctx.showDebugSelector();
|
|
1526
|
-
} else {
|
|
1527
|
-
await handleDebugSubcommand(args, runtime.ctx.sessionManager.getCwd(), text =>
|
|
1528
|
-
runtime.ctx.showStatus(text),
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1487
|
+
handleTui: async (_command, runtime) => {
|
|
1488
|
+
await runtime.ctx.showDebugSelector();
|
|
1531
1489
|
runtime.ctx.editor.setText("");
|
|
1532
1490
|
},
|
|
1533
1491
|
},
|
|
@@ -29,10 +29,23 @@ export const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1
|
|
|
29
29
|
* connection dropped, etc.).
|
|
30
30
|
*/
|
|
31
31
|
export const BROWSER_PROTOCOL_TIMEOUT_MS = 60_000;
|
|
32
|
+
// Automation-tell launch flags that puppeteer-core adds by default. We suppress
|
|
33
|
+
// them via `ignoreDefaultArgs` (the supported escape hatch) to mirror xxxx's
|
|
34
|
+
// chromiumSwitches patch. `--enable-automation` is the loudest: it sets
|
|
35
|
+
// navigator.webdriver=true and shows the "controlled by automated software" infobar.
|
|
36
|
+
// `ignoreDefaultArgs` does exact-string matching, so each entry must be a flag that
|
|
37
|
+
// puppeteer emits verbatim. The default `--disable-features=...` string can't be
|
|
38
|
+
// matched this way; it is neutralized in the puppeteer-core patch (ChromeLauncher).
|
|
32
39
|
const STEALTH_IGNORE_DEFAULT_ARGS = [
|
|
40
|
+
"--enable-automation",
|
|
33
41
|
"--disable-extensions",
|
|
34
42
|
"--disable-default-apps",
|
|
35
43
|
"--disable-component-extensions-with-background-pages",
|
|
44
|
+
"--disable-popup-blocking",
|
|
45
|
+
"--disable-client-side-phishing-detection",
|
|
46
|
+
"--allow-pre-commit-input",
|
|
47
|
+
"--disable-ipc-flooding-protection",
|
|
48
|
+
"--metrics-recording-only",
|
|
36
49
|
];
|
|
37
50
|
const STEALTH_ACCEPT_LANGUAGE = "en-US,en";
|
|
38
51
|
|
|
@@ -309,9 +322,11 @@ export interface UserAgentOverride {
|
|
|
309
322
|
userAgentMetadata: {
|
|
310
323
|
brands: Array<{ brand: string; version: string }>;
|
|
311
324
|
fullVersion: string;
|
|
325
|
+
fullVersionList: Array<{ brand: string; version: string }>;
|
|
312
326
|
platform: string;
|
|
313
327
|
platformVersion: string;
|
|
314
328
|
architecture: string;
|
|
329
|
+
bitness: string;
|
|
315
330
|
model: string;
|
|
316
331
|
mobile: boolean;
|
|
317
332
|
};
|
|
@@ -371,6 +386,26 @@ function patchSourceUrl(page: Page): void {
|
|
|
371
386
|
};
|
|
372
387
|
}
|
|
373
388
|
|
|
389
|
+
async function resolveMacOsProductVersion(): Promise<string> {
|
|
390
|
+
if (os.platform() !== "darwin") return "";
|
|
391
|
+
try {
|
|
392
|
+
const plist = await Bun.file("/System/Library/CoreServices/SystemVersion.plist").text();
|
|
393
|
+
return plist.match(/<key>ProductVersion<\/key>\s*<string>([^<]+)<\/string>/)?.[1] ?? "";
|
|
394
|
+
} catch {
|
|
395
|
+
return "";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function resolveHostArchitecture(): string {
|
|
400
|
+
if (os.arch() === "arm64") return "arm";
|
|
401
|
+
if (os.arch().includes("64")) return "x86";
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function resolveHostBitness(): string {
|
|
406
|
+
return os.arch().includes("64") ? "64" : "";
|
|
407
|
+
}
|
|
408
|
+
|
|
374
409
|
async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride> {
|
|
375
410
|
const rawUserAgent = await page.browser().userAgent();
|
|
376
411
|
let userAgent = rawUserAgent.replace("HeadlessChrome/", "Chrome/");
|
|
@@ -379,32 +414,24 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
|
|
|
379
414
|
}
|
|
380
415
|
|
|
381
416
|
const uaVersionMatch = userAgent.match(/Chrome\/([\d|.]+)/);
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
const
|
|
417
|
+
const browserVersionMatch = (await page.browser().version()).match(/\/([\d|.]+)/);
|
|
418
|
+
const legacyVersion = uaVersionMatch?.[1] ?? browserVersionMatch?.[1] ?? "0";
|
|
419
|
+
const fullVersion = browserVersionMatch?.[1] ?? legacyVersion;
|
|
420
|
+
const majorVersion = Number.parseInt(legacyVersion.split(".")[0] ?? "0", 10) || 0;
|
|
385
421
|
const isAndroid = userAgent.includes("Android");
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
: "Win32";
|
|
393
|
-
const platformFull = userAgent.includes("Mac OS X")
|
|
394
|
-
? "Mac OS X"
|
|
395
|
-
: isAndroid
|
|
396
|
-
? "Android"
|
|
397
|
-
: userAgent.includes("Linux")
|
|
398
|
-
? "Linux"
|
|
399
|
-
: "Windows";
|
|
400
|
-
const platformVersion = userAgent.includes("Mac OS X ")
|
|
401
|
-
? (userAgent.match(/Mac OS X ([^)]+)/)?.[1] ?? "")
|
|
422
|
+
const isMac = userAgent.includes("Mac OS X");
|
|
423
|
+
const isWindows = userAgent.includes("Windows");
|
|
424
|
+
const platform = isMac ? "MacIntel" : isAndroid ? "Android" : userAgent.includes("Linux") ? "Linux" : "Win32";
|
|
425
|
+
const platformFull = isMac ? "macOS" : isAndroid ? "Android" : userAgent.includes("Linux") ? "Linux" : "Windows";
|
|
426
|
+
const platformVersion = isMac
|
|
427
|
+
? await resolveMacOsProductVersion()
|
|
402
428
|
: userAgent.includes("Android ")
|
|
403
429
|
? (userAgent.match(/Android ([^;]+)/)?.[1] ?? "")
|
|
404
|
-
:
|
|
405
|
-
? (userAgent.match(/Windows
|
|
430
|
+
: isWindows
|
|
431
|
+
? (userAgent.match(/Windows NT ([\d.]+)/)?.[1] ?? "")
|
|
406
432
|
: "";
|
|
407
|
-
const architecture = isAndroid ? "" :
|
|
433
|
+
const architecture = isAndroid ? "" : resolveHostArchitecture();
|
|
434
|
+
const bitness = isAndroid ? "" : resolveHostBitness();
|
|
408
435
|
const model = isAndroid ? (userAgent.match(/Android.*?;\s([^)]+)/)?.[1] ?? "") : "";
|
|
409
436
|
|
|
410
437
|
const brandOrders = [
|
|
@@ -422,6 +449,10 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
|
|
|
422
449
|
brands[order[0]!] = { brand: greaseyBrand, version: "99" };
|
|
423
450
|
brands[order[1]!] = { brand: "Chromium", version: String(majorVersion) };
|
|
424
451
|
brands[order[2]!] = { brand: "Google Chrome", version: String(majorVersion) };
|
|
452
|
+
const fullVersionList = brands.map(({ brand }) => ({
|
|
453
|
+
brand,
|
|
454
|
+
version: brand === greaseyBrand ? "99.0.0.0" : fullVersion,
|
|
455
|
+
}));
|
|
425
456
|
|
|
426
457
|
return {
|
|
427
458
|
userAgent,
|
|
@@ -429,10 +460,12 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
|
|
|
429
460
|
acceptLanguage: STEALTH_ACCEPT_LANGUAGE,
|
|
430
461
|
userAgentMetadata: {
|
|
431
462
|
brands,
|
|
432
|
-
fullVersion
|
|
463
|
+
fullVersion,
|
|
464
|
+
fullVersionList,
|
|
433
465
|
platform: platformFull,
|
|
434
466
|
platformVersion,
|
|
435
467
|
architecture,
|
|
468
|
+
bitness,
|
|
436
469
|
model,
|
|
437
470
|
mobile: isAndroid,
|
|
438
471
|
},
|
|
@@ -578,15 +611,29 @@ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_
|
|
|
578
611
|
.join(";\n");
|
|
579
612
|
|
|
580
613
|
return `(() => {
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
614
|
+
const Page_Function_toString = Function.prototype.toString;
|
|
615
|
+
const Page_FunctionToStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "toString");
|
|
616
|
+
const Page_Proxy = Proxy;
|
|
617
|
+
const Page_WeakMap = WeakMap;
|
|
618
|
+
const Page_WeakMap_get = Page_WeakMap.prototype.get;
|
|
619
|
+
const Page_WeakMap_set = Page_WeakMap.prototype.set;
|
|
620
|
+
// Native function cache - captured before any tampering.
|
|
621
|
+
// A same-origin iframe yields natives uncontaminated by page-level
|
|
622
|
+
// tampering, but at document-start (when this preload runs) there is
|
|
623
|
+
// no documentElement to attach it to. In that case the page itself
|
|
624
|
+
// hasn't executed yet, so window's own natives are still pristine —
|
|
625
|
+
// fall back to window instead of bailing, otherwise none of the
|
|
626
|
+
// fingerprint patches below would ever run.
|
|
627
|
+
let iframe = null;
|
|
584
628
|
const container = document.head ?? document.documentElement;
|
|
585
|
-
if (
|
|
586
|
-
|
|
629
|
+
if (container) {
|
|
630
|
+
iframe = document.createElement("iframe");
|
|
631
|
+
iframe.style.display = "none";
|
|
632
|
+
container.appendChild(iframe);
|
|
633
|
+
if (!iframe.contentWindow) iframe = null;
|
|
634
|
+
}
|
|
587
635
|
try {
|
|
588
|
-
const nativeWindow = iframe.contentWindow;
|
|
589
|
-
if (!nativeWindow) return;
|
|
636
|
+
const nativeWindow = iframe ? iframe.contentWindow : window;
|
|
590
637
|
|
|
591
638
|
// Cache pristine native functions
|
|
592
639
|
const Function_toString = nativeWindow.Function.prototype.toString;
|
|
@@ -626,9 +673,38 @@ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_
|
|
|
626
673
|
const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
|
|
627
674
|
const Date_constructor = nativeWindow.Date;
|
|
628
675
|
|
|
676
|
+
const nativeFunctionSources = new Page_WeakMap();
|
|
677
|
+
const makeNativeString = (name) => "function " + (name || "") + "() { [native code] }";
|
|
678
|
+
const registerNativeSource = (fn, source) => {
|
|
679
|
+
if (typeof fn === "function") Reflect_apply(Page_WeakMap_set, nativeFunctionSources, [fn, source]);
|
|
680
|
+
return fn;
|
|
681
|
+
};
|
|
682
|
+
const patchToString = (fn, name) => registerNativeSource(fn, makeNativeString(name));
|
|
683
|
+
if (${scripts.length > 0 ? "true" : "false"}) {
|
|
684
|
+
const functionToStringProxy = new Page_Proxy(Page_Function_toString, {
|
|
685
|
+
apply(target, thisArg, args) {
|
|
686
|
+
const source = Reflect_apply(Page_WeakMap_get, nativeFunctionSources, [thisArg]);
|
|
687
|
+
if (source) return source;
|
|
688
|
+
return Reflect_apply(target, thisArg, args || []);
|
|
689
|
+
},
|
|
690
|
+
get(target, key, receiver) {
|
|
691
|
+
return Reflect_get(target, key, receiver);
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
registerNativeSource(functionToStringProxy, makeNativeString("toString"));
|
|
695
|
+
Object_defineProperty(Function.prototype, "toString", {
|
|
696
|
+
...(Page_FunctionToStringDescriptor || {
|
|
697
|
+
writable: true,
|
|
698
|
+
configurable: true,
|
|
699
|
+
enumerable: false,
|
|
700
|
+
}),
|
|
701
|
+
value: functionToStringProxy,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
629
705
|
${joint}
|
|
630
706
|
} finally {
|
|
631
|
-
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
707
|
+
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
632
708
|
}})();`;
|
|
633
709
|
}
|
|
634
710
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
"function " + (name || "") + "() { [native code] }";
|
|
4
|
-
|
|
5
|
-
// Patch toString for common fingerprinted functions
|
|
1
|
+
// Register native-looking source for common fingerprinted functions without
|
|
2
|
+
// adding own toString properties.
|
|
6
3
|
const patchedFns = [
|
|
7
4
|
[window.alert, "alert"],
|
|
8
5
|
[window.prompt, "prompt"],
|
|
@@ -20,44 +17,28 @@ const patchedFns = [
|
|
|
20
17
|
];
|
|
21
18
|
|
|
22
19
|
for (const [fn, name] of patchedFns) {
|
|
23
|
-
|
|
24
|
-
const nativeStr = makeNativeString(name);
|
|
25
|
-
Object_defineProperty(fn, "toString", {
|
|
26
|
-
value: function toString() { return nativeStr; },
|
|
27
|
-
writable: false,
|
|
28
|
-
configurable: true,
|
|
29
|
-
enumerable: false,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
20
|
+
patchToString(fn, name);
|
|
32
21
|
}
|
|
33
22
|
|
|
34
|
-
// Patch Object.getOwnPropertyDescriptor to return native-looking
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
// Patch Object.getOwnPropertyDescriptor to return native-looking accessor
|
|
24
|
+
// source through the shared Function.prototype.toString registry.
|
|
25
|
+
const patchedGetOwnPropertyDescriptor = function getOwnPropertyDescriptor(obj, prop) {
|
|
26
|
+
const descriptor = Reflect_apply(Object_getOwnPropertyDescriptor, this, [obj, prop]);
|
|
37
27
|
if (!descriptor) return descriptor;
|
|
38
28
|
|
|
39
|
-
// Make patched descriptors look native
|
|
40
29
|
if (descriptor.get && typeof descriptor.get === "function") {
|
|
41
|
-
|
|
42
|
-
Object_defineProperty(descriptor.get, "toString", {
|
|
43
|
-
value: function toString() { return getStr; },
|
|
44
|
-
writable: false,
|
|
45
|
-
configurable: true,
|
|
46
|
-
enumerable: false,
|
|
47
|
-
});
|
|
30
|
+
patchToString(descriptor.get, "get " + String(prop));
|
|
48
31
|
}
|
|
49
32
|
if (descriptor.set && typeof descriptor.set === "function") {
|
|
50
|
-
|
|
51
|
-
Object_defineProperty(descriptor.set, "toString", {
|
|
52
|
-
value: function toString() { return setStr; },
|
|
53
|
-
writable: false,
|
|
54
|
-
configurable: true,
|
|
55
|
-
enumerable: false,
|
|
56
|
-
});
|
|
33
|
+
patchToString(descriptor.set, "set " + String(prop));
|
|
57
34
|
}
|
|
58
35
|
|
|
59
36
|
return descriptor;
|
|
60
37
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
38
|
+
patchToString(patchedGetOwnPropertyDescriptor, "getOwnPropertyDescriptor");
|
|
39
|
+
Object_defineProperty(Object, "getOwnPropertyDescriptor", {
|
|
40
|
+
value: patchedGetOwnPropertyDescriptor,
|
|
41
|
+
writable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
enumerable: false,
|
|
44
|
+
});
|