@oh-my-pi/pi-coding-agent 16.1.7 → 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 +71 -0
- package/dist/cli.js +4266 -3446
- package/dist/types/cli/flag-tables.d.ts +17 -0
- package/dist/types/cli/session-picker.d.ts +0 -1
- package/dist/types/cli/tiny-models-cli.d.ts +2 -0
- package/dist/types/cli-commands.d.ts +4 -2
- package/dist/types/collab/protocol.d.ts +20 -1
- package/dist/types/config/settings-schema.d.ts +57 -6
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
- package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
- package/dist/types/mcp/loader.d.ts +3 -2
- package/dist/types/mcp/manager.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +21 -4
- package/dist/types/mnemopi/config.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/btw-panel.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +0 -2
- package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
- package/dist/types/modes/interactive-mode.d.ts +4 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/session/agent-session.d.ts +18 -3
- package/dist/types/session/session-history-format.d.ts +25 -0
- package/dist/types/session/snapcompact-inline.d.ts +1 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +1 -0
- package/dist/types/tiny/models.d.ts +11 -7
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/todo.d.ts +1 -0
- package/dist/types/utils/thinking-display.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +104 -0
- package/src/advisor/runtime.ts +38 -2
- 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/session-picker.ts +1 -2
- package/src/cli/tiny-models-cli.ts +7 -2
- package/src/cli-commands.ts +48 -3
- package/src/collab/guest.ts +172 -20
- package/src/collab/host.ts +47 -5
- package/src/collab/protocol.ts +16 -1
- package/src/config/model-registry.ts +6 -7
- package/src/config/settings-schema.ts +59 -5
- package/src/edit/renderer.ts +8 -12
- package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/filesystem-resource.ts +34 -0
- package/src/internal-urls/local-protocol.ts +7 -1
- package/src/internal-urls/memory-protocol.ts +5 -1
- package/src/internal-urls/skill-protocol.ts +20 -4
- package/src/internal-urls/vault-protocol.ts +5 -2
- package/src/lsp/client.ts +39 -25
- package/src/main.ts +8 -8
- package/src/mcp/loader.ts +4 -3
- package/src/mcp/manager.ts +35 -15
- package/src/mcp/startup-events.ts +106 -11
- package/src/mnemopi/config.ts +2 -0
- package/src/mnemopi/state.ts +3 -1
- package/src/modes/components/agent-hub.ts +4 -0
- package/src/modes/components/agent-transcript-viewer.ts +2 -0
- package/src/modes/components/assistant-message.ts +217 -18
- package/src/modes/components/btw-panel.ts +15 -3
- package/src/modes/components/chat-transcript-builder.ts +8 -2
- package/src/modes/components/model-selector.ts +72 -9
- package/src/modes/components/omfg-panel.ts +1 -1
- package/src/modes/components/session-selector.ts +4 -9
- package/src/modes/components/snapcompact-shape-preview.ts +1 -1
- package/src/modes/components/tool-execution.ts +10 -2
- package/src/modes/controllers/btw-controller.ts +32 -7
- package/src/modes/controllers/command-controller.ts +18 -3
- package/src/modes/controllers/event-controller.ts +24 -4
- package/src/modes/controllers/input-controller.ts +43 -21
- package/src/modes/controllers/mcp-command-controller.ts +40 -12
- package/src/modes/controllers/selector-controller.ts +23 -13
- package/src/modes/controllers/streaming-reveal.ts +78 -20
- package/src/modes/controllers/todo-command-controller.ts +9 -10
- package/src/modes/interactive-mode.ts +84 -9
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/system/side-channel-no-tools.md +3 -0
- package/src/sdk.ts +12 -8
- package/src/session/agent-session.ts +385 -85
- package/src/session/history-storage.ts +15 -16
- package/src/session/session-history-format.ts +41 -1
- package/src/session/snapcompact-inline.ts +9 -6
- package/src/slash-commands/builtin-registry.ts +171 -87
- package/src/slash-commands/helpers/todo.ts +12 -6
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +6 -11
- package/src/tiny/models.ts +7 -3
- package/src/tiny/title-client.ts +8 -2
- package/src/tiny/worker.ts +1 -0
- 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/tools/search.ts +10 -1
- package/src/tools/sqlite-reader.ts +59 -3
- package/src/tools/todo.ts +6 -0
- package/src/tools/write.ts +4 -6
- package/src/utils/thinking-display.ts +78 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
1
|
import type { TodoPhase } from "../../tools/todo";
|
|
3
2
|
import {
|
|
4
3
|
applyOpsToPhases,
|
|
5
4
|
getLatestTodoPhasesFromEntries,
|
|
6
5
|
markdownToPhases,
|
|
7
6
|
phasesToMarkdown,
|
|
7
|
+
resolveTodoMarkdownPath,
|
|
8
8
|
USER_TODO_EDIT_CUSTOM_TYPE,
|
|
9
9
|
} from "../../tools/todo";
|
|
10
10
|
import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
|
|
11
|
-
import { commandConsumed, parseSubcommand, usage } from "./parse";
|
|
11
|
+
import { commandConsumed, errorMessage, parseSubcommand, usage } from "./parse";
|
|
12
12
|
|
|
13
13
|
type TodoMutationVerb = "done" | "drop" | "rm";
|
|
14
14
|
|
|
@@ -127,19 +127,25 @@ async function handleTodoExportCommand(restArgs: string, runtime: SlashCommandRu
|
|
|
127
127
|
await runtime.output("No todos to export.");
|
|
128
128
|
return commandConsumed();
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
let target: string;
|
|
131
|
+
try {
|
|
132
|
+
target = resolveTodoMarkdownPath(restArgs, runtime.sessionManager.getCwd());
|
|
133
|
+
await Bun.write(target, phasesToMarkdown(phases));
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return usage(`Failed to write todos: ${errorMessage(err)}`, runtime);
|
|
136
|
+
}
|
|
132
137
|
await runtime.output(`Wrote todos to ${target}`);
|
|
133
138
|
return commandConsumed();
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
async function handleTodoImportCommand(restArgs: string, runtime: SlashCommandRuntime): Promise<SlashCommandResult> {
|
|
137
|
-
|
|
142
|
+
let target: string;
|
|
138
143
|
let content: string;
|
|
139
144
|
try {
|
|
145
|
+
target = resolveTodoMarkdownPath(restArgs, runtime.sessionManager.getCwd());
|
|
140
146
|
content = await Bun.file(target).text();
|
|
141
147
|
} catch (err) {
|
|
142
|
-
return usage(`Failed to read
|
|
148
|
+
return usage(`Failed to read todos: ${errorMessage(err)}`, runtime);
|
|
143
149
|
}
|
|
144
150
|
const { phases, errors } = markdownToPhases(content);
|
|
145
151
|
if (errors.length > 0) return usage(`Could not parse ${target}:\n ${errors.join("\n ")}`, runtime);
|
|
@@ -20,6 +20,8 @@ export interface BuiltinSlashCommand {
|
|
|
20
20
|
subcommands?: SubcommandDef[];
|
|
21
21
|
/** Static inline hint when command takes a simple argument (no subcommands). */
|
|
22
22
|
inlineHint?: string;
|
|
23
|
+
/** TUI-only dynamic status text for command-name autocomplete. Static `description` remains canonical for ACP/help. */
|
|
24
|
+
getTuiAutocompleteDescription?: (runtime: TuiSlashCommandRuntime) => string | undefined;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Parsed slash-command text after stripping the leading "/". */
|
package/src/system-prompt.ts
CHANGED
|
@@ -327,6 +327,8 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
|
|
|
327
327
|
return userLevel?.content ?? null;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
export const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES = ["read", "bash", "eval", "edit", "write"] as const;
|
|
331
|
+
|
|
330
332
|
export interface SystemPromptToolMetadata {
|
|
331
333
|
label: string;
|
|
332
334
|
description: string;
|
|
@@ -584,18 +586,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
584
586
|
const dateTime = date;
|
|
585
587
|
const promptCwd = shortenPath(resolvedCwd.replace(/\\/g, "/"));
|
|
586
588
|
|
|
587
|
-
// Build tool metadata for system prompt rendering
|
|
588
|
-
// Priority: explicit list > tools map >
|
|
589
|
-
// Default includes both bash and python; actual availability determined by settings in createTools
|
|
589
|
+
// Build tool metadata for system prompt rendering.
|
|
590
|
+
// Priority: explicit list > tools map > conservative SDK fallback.
|
|
590
591
|
let toolNames = providedToolNames;
|
|
591
592
|
if (!toolNames) {
|
|
592
|
-
|
|
593
|
-
// Tools map provided
|
|
594
|
-
toolNames = Array.from(tools.keys());
|
|
595
|
-
} else {
|
|
596
|
-
// Use defaults
|
|
597
|
-
toolNames = ["read", "bash", "eval", "edit", "write"]; // TODO: Why?
|
|
598
|
-
}
|
|
593
|
+
toolNames = tools ? Array.from(tools.keys()) : [...DEFAULT_SYSTEM_PROMPT_TOOL_NAMES];
|
|
599
594
|
}
|
|
600
595
|
|
|
601
596
|
// Build tool descriptions for system prompt rendering.
|
|
@@ -625,7 +620,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
625
620
|
// Filter skills for the rendered system prompt:
|
|
626
621
|
// - require the `read` tool so the model can actually fetch skill content;
|
|
627
622
|
// - drop skills with frontmatter `hide: true` (still loadable via skill:// and /skill:<name>).
|
|
628
|
-
const hasRead =
|
|
623
|
+
const hasRead = toolNames.includes("read");
|
|
629
624
|
const filteredSkills = hasRead ? skills.filter(skill => skill.hide !== true) : [];
|
|
630
625
|
|
|
631
626
|
const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
|
package/src/tiny/models.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface TinyTitleLocalModelSpec {
|
|
|
12
12
|
contextNote: string;
|
|
13
13
|
/** Model family emits hidden reasoning unless the chat template disables it. */
|
|
14
14
|
reasoning?: boolean;
|
|
15
|
+
/** Reason this model is blocked before loading the ONNX runtime. */
|
|
16
|
+
unsupportedReason?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const TINY_TITLE_LOCAL_MODELS = [
|
|
@@ -108,7 +110,7 @@ export function getTinyTitleModelSpec(key: TinyTitleLocalModelKey): (typeof TINY
|
|
|
108
110
|
/** Default memory model: the online path (the configured smol / remote LLM; no local download). */
|
|
109
111
|
export const ONLINE_MEMORY_MODEL_KEY = "online";
|
|
110
112
|
/** Recommended local model for memory tasks when none is named. */
|
|
111
|
-
export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "
|
|
113
|
+
export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "lfm2-1.2b";
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Local models for Mnemopi memory tasks (fact extraction + consolidation).
|
|
@@ -123,9 +125,11 @@ export const TINY_MEMORY_LOCAL_MODELS = [
|
|
|
123
125
|
dtype: "q4",
|
|
124
126
|
label: "Qwen3 1.7B",
|
|
125
127
|
description:
|
|
126
|
-
"
|
|
127
|
-
contextNote: "
|
|
128
|
+
"Disabled for local inference: onnxruntime-node cannot run this ONNX export's RotaryEmbedding cache updates.",
|
|
129
|
+
contextNote: "Blocked before load to avoid the unsupported RotaryEmbedding runtime path.",
|
|
128
130
|
reasoning: true,
|
|
131
|
+
unsupportedReason:
|
|
132
|
+
"onnxruntime-node does not support Qwen3 RotaryEmbedding cache updates in onnx-community/Qwen3-1.7B-ONNX",
|
|
129
133
|
},
|
|
130
134
|
{
|
|
131
135
|
key: "gemma-3-1b",
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -296,6 +296,7 @@ export class TinyTitleClient {
|
|
|
296
296
|
#unsubscribeMessage: (() => void) | null = null;
|
|
297
297
|
#unsubscribeError: (() => void) | null = null;
|
|
298
298
|
#pending = new Map<string, PendingRequest>();
|
|
299
|
+
#failedModels = new Set<TinyLocalModelKey>();
|
|
299
300
|
#progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
|
|
300
301
|
#nextRequestId = 0;
|
|
301
302
|
#spawnWorker: () => WorkerHandle;
|
|
@@ -318,7 +319,7 @@ export class TinyTitleClient {
|
|
|
318
319
|
): Promise<string | null> {
|
|
319
320
|
const options = normalizeTinyTitleGenerateOptions(optionsOrSignal);
|
|
320
321
|
if (!isTinyTitleLocalModelKey(modelKey)) return null;
|
|
321
|
-
if (options.signal?.aborted) return null;
|
|
322
|
+
if (options.signal?.aborted || this.#failedModels.has(modelKey)) return null;
|
|
322
323
|
|
|
323
324
|
try {
|
|
324
325
|
const worker = this.#ensureWorker();
|
|
@@ -357,7 +358,7 @@ export class TinyTitleClient {
|
|
|
357
358
|
options: { maxTokens?: number; signal?: AbortSignal } = {},
|
|
358
359
|
): Promise<string | null> {
|
|
359
360
|
if (!isTinyMemoryLocalModelKey(modelKey)) return null;
|
|
360
|
-
if (options.signal?.aborted) return null;
|
|
361
|
+
if (options.signal?.aborted || this.#failedModels.has(modelKey)) return null;
|
|
361
362
|
|
|
362
363
|
try {
|
|
363
364
|
const worker = this.#ensureWorker();
|
|
@@ -478,12 +479,17 @@ export class TinyTitleClient {
|
|
|
478
479
|
return;
|
|
479
480
|
}
|
|
480
481
|
logger.debug("tiny-title: worker returned error", { error: message.error });
|
|
482
|
+
this.#markFailedModel(pending);
|
|
481
483
|
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
482
484
|
if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
|
|
483
485
|
else pending.resolve(false);
|
|
484
486
|
void this.terminate();
|
|
485
487
|
}
|
|
486
488
|
|
|
489
|
+
#markFailedModel(pending: PendingRequest): void {
|
|
490
|
+
if (pending.kind === "generate" || pending.kind === "complete") this.#failedModels.add(pending.modelKey);
|
|
491
|
+
}
|
|
492
|
+
|
|
487
493
|
#emitProgress(event: TinyTitleProgressEvent): void {
|
|
488
494
|
for (const listener of this.#progressListeners) listener(event);
|
|
489
495
|
}
|
package/src/tiny/worker.ts
CHANGED
|
@@ -317,6 +317,7 @@ async function loadPipeline(
|
|
|
317
317
|
): Promise<TextGenerationPipeline> {
|
|
318
318
|
const spec = getTinyLocalModelSpec(modelKey);
|
|
319
319
|
if (!spec) throw new Error(`Unknown tiny local model: ${modelKey}`);
|
|
320
|
+
if (spec.unsupportedReason) throw new Error(`${modelKey} is unavailable: ${spec.unsupportedReason}`);
|
|
320
321
|
const cached = pipelines.get(modelKey);
|
|
321
322
|
if (cached) {
|
|
322
323
|
void cached
|
|
@@ -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
|
+
});
|
|
@@ -1,20 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
const visibilityDescriptors = Object_getOwnPropertyDescriptors({
|
|
3
|
+
get hidden() {
|
|
4
|
+
return false;
|
|
5
|
+
},
|
|
6
|
+
get visibilityState() {
|
|
7
|
+
return "visible";
|
|
8
|
+
},
|
|
9
|
+
get webkitHidden() {
|
|
10
|
+
return false;
|
|
11
|
+
},
|
|
12
|
+
get webkitVisibilityState() {
|
|
13
|
+
return "visible";
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
for (const key of Object_keys(visibilityDescriptors)) {
|
|
17
|
+
const descriptor = visibilityDescriptors[key];
|
|
18
|
+
if (descriptor && typeof descriptor.get === "function") {
|
|
19
|
+
patchToString(descriptor.get, "get " + key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const focusDescriptor = Object_getOwnPropertyDescriptor(
|
|
23
|
+
{
|
|
24
|
+
hasFocus() {
|
|
25
|
+
return document.visibilityState === "visible";
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"hasFocus",
|
|
29
|
+
);
|
|
30
|
+
if (focusDescriptor && typeof focusDescriptor.value === "function") {
|
|
31
|
+
patchToString(focusDescriptor.value, "hasFocus");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const clearOwnSlot = (name) => {
|
|
35
|
+
const ownDescriptor = Object_getOwnPropertyDescriptor(document, name);
|
|
36
|
+
if (!ownDescriptor) return true;
|
|
37
|
+
if (ownDescriptor.configurable !== true) return false;
|
|
38
|
+
return Reflect_deleteProperty(document, name);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const inheritedSlot = (name, expectedKind) => {
|
|
42
|
+
let proto = Object_getPrototypeOf(document);
|
|
43
|
+
while (proto) {
|
|
44
|
+
const descriptor = Object_getOwnPropertyDescriptor(proto, name);
|
|
45
|
+
if (descriptor && typeof descriptor[expectedKind] === "function") {
|
|
46
|
+
return [proto, descriptor];
|
|
47
|
+
}
|
|
48
|
+
proto = Object_getPrototypeOf(proto);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const defineAccessor = (name) => {
|
|
53
|
+
if (!clearOwnSlot(name)) return;
|
|
54
|
+
const slot = inheritedSlot(name, "get");
|
|
55
|
+
if (!slot || slot[1].configurable !== true) return;
|
|
56
|
+
|
|
57
|
+
Object_defineProperty(slot[0], name, {
|
|
58
|
+
get: visibilityDescriptors[name].get,
|
|
59
|
+
enumerable: slot[1].enumerable,
|
|
60
|
+
configurable: true,
|
|
7
61
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
defineAccessor("hidden");
|
|
65
|
+
defineAccessor("visibilityState");
|
|
66
|
+
defineAccessor("webkitHidden");
|
|
67
|
+
defineAccessor("webkitVisibilityState");
|
|
68
|
+
|
|
69
|
+
if (clearOwnSlot("hasFocus")) {
|
|
70
|
+
const slot = inheritedSlot("hasFocus", "value");
|
|
71
|
+
if (slot && slot[1].configurable === true) {
|
|
72
|
+
Object_defineProperty(slot[0], "hasFocus", {
|
|
73
|
+
value: focusDescriptor.value,
|
|
74
|
+
writable: slot[1].writable === true,
|
|
75
|
+
enumerable: slot[1].enumerable,
|
|
76
|
+
configurable: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -1,11 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
{
|
|
2
|
+
const htmlElementOffsetHeightDescriptor =
|
|
3
|
+
typeof HTMLElement === "undefined"
|
|
4
|
+
? undefined
|
|
5
|
+
: Object_getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
|
|
6
|
+
const htmlDivElementOffsetHeightDescriptor =
|
|
7
|
+
typeof HTMLDivElement === "undefined"
|
|
8
|
+
? undefined
|
|
9
|
+
: Object_getOwnPropertyDescriptor(HTMLDivElement.prototype, "offsetHeight");
|
|
10
|
+
const offsetHeightDescriptor =
|
|
11
|
+
htmlDivElementOffsetHeightDescriptor || htmlElementOffsetHeightDescriptor;
|
|
12
|
+
const offsetHeightPrototype = htmlDivElementOffsetHeightDescriptor
|
|
13
|
+
? HTMLDivElement.prototype
|
|
14
|
+
: htmlElementOffsetHeightDescriptor
|
|
15
|
+
? HTMLElement.prototype
|
|
16
|
+
: undefined;
|
|
17
|
+
const elementIdDescriptor =
|
|
18
|
+
typeof Element === "undefined"
|
|
19
|
+
? undefined
|
|
20
|
+
: Object_getOwnPropertyDescriptor(Element.prototype, "id");
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
typeof HTMLDivElement !== "undefined" &&
|
|
24
|
+
offsetHeightPrototype &&
|
|
25
|
+
offsetHeightDescriptor &&
|
|
26
|
+
typeof offsetHeightDescriptor.get === "function" &&
|
|
27
|
+
offsetHeightDescriptor.configurable &&
|
|
28
|
+
elementIdDescriptor &&
|
|
29
|
+
typeof elementIdDescriptor.get === "function"
|
|
30
|
+
) {
|
|
31
|
+
const offsetHeightGetter = new Window_Proxy(offsetHeightDescriptor.get, {
|
|
32
|
+
apply(target, thisArg, args) {
|
|
33
|
+
const height = Reflect_apply(target, thisArg, args);
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
height === 0 &&
|
|
37
|
+
Object_getPrototypeOf(thisArg) === HTMLDivElement.prototype &&
|
|
38
|
+
Reflect_apply(elementIdDescriptor.get, thisArg, []) === "modernizr"
|
|
39
|
+
) {
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return height;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
patchToString(offsetHeightGetter, "get offsetHeight");
|
|
48
|
+
|
|
49
|
+
Object_defineProperty(
|
|
50
|
+
offsetHeightPrototype,
|
|
51
|
+
"offsetHeight",
|
|
52
|
+
Object_assign({}, offsetHeightDescriptor, {
|
|
53
|
+
get: offsetHeightGetter,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|