@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli.js +4266 -3446
  3. package/dist/types/cli/flag-tables.d.ts +17 -0
  4. package/dist/types/cli/session-picker.d.ts +0 -1
  5. package/dist/types/cli/tiny-models-cli.d.ts +2 -0
  6. package/dist/types/cli-commands.d.ts +4 -2
  7. package/dist/types/collab/protocol.d.ts +20 -1
  8. package/dist/types/config/settings-schema.d.ts +57 -6
  9. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
  10. package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
  11. package/dist/types/mcp/loader.d.ts +3 -2
  12. package/dist/types/mcp/manager.d.ts +4 -3
  13. package/dist/types/mcp/startup-events.d.ts +21 -4
  14. package/dist/types/mnemopi/config.d.ts +1 -0
  15. package/dist/types/modes/components/agent-hub.d.ts +1 -0
  16. package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  18. package/dist/types/modes/components/btw-panel.d.ts +2 -0
  19. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
  20. package/dist/types/modes/components/session-selector.d.ts +0 -2
  21. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  22. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  24. package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
  25. package/dist/types/modes/interactive-mode.d.ts +4 -1
  26. package/dist/types/modes/types.d.ts +4 -1
  27. package/dist/types/session/agent-session.d.ts +18 -3
  28. package/dist/types/session/session-history-format.d.ts +25 -0
  29. package/dist/types/session/snapcompact-inline.d.ts +1 -1
  30. package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
  31. package/dist/types/slash-commands/types.d.ts +2 -0
  32. package/dist/types/system-prompt.d.ts +1 -0
  33. package/dist/types/tiny/models.d.ts +11 -7
  34. package/dist/types/tools/browser/launch.d.ts +5 -0
  35. package/dist/types/tools/todo.d.ts +1 -0
  36. package/dist/types/utils/thinking-display.d.ts +2 -0
  37. package/package.json +12 -12
  38. package/src/advisor/__tests__/advisor.test.ts +104 -0
  39. package/src/advisor/runtime.ts +38 -2
  40. package/src/cli/args.ts +0 -1
  41. package/src/cli/flag-tables.ts +42 -0
  42. package/src/cli/profile-bootstrap.ts +1 -11
  43. package/src/cli/session-picker.ts +1 -2
  44. package/src/cli/tiny-models-cli.ts +7 -2
  45. package/src/cli-commands.ts +48 -3
  46. package/src/collab/guest.ts +172 -20
  47. package/src/collab/host.ts +47 -5
  48. package/src/collab/protocol.ts +16 -1
  49. package/src/config/model-registry.ts +6 -7
  50. package/src/config/settings-schema.ts +59 -5
  51. package/src/edit/renderer.ts +8 -12
  52. package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
  53. package/src/internal-urls/docs-index.generated.txt +1 -1
  54. package/src/internal-urls/filesystem-resource.ts +34 -0
  55. package/src/internal-urls/local-protocol.ts +7 -1
  56. package/src/internal-urls/memory-protocol.ts +5 -1
  57. package/src/internal-urls/skill-protocol.ts +20 -4
  58. package/src/internal-urls/vault-protocol.ts +5 -2
  59. package/src/lsp/client.ts +39 -25
  60. package/src/main.ts +8 -8
  61. package/src/mcp/loader.ts +4 -3
  62. package/src/mcp/manager.ts +35 -15
  63. package/src/mcp/startup-events.ts +106 -11
  64. package/src/mnemopi/config.ts +2 -0
  65. package/src/mnemopi/state.ts +3 -1
  66. package/src/modes/components/agent-hub.ts +4 -0
  67. package/src/modes/components/agent-transcript-viewer.ts +2 -0
  68. package/src/modes/components/assistant-message.ts +217 -18
  69. package/src/modes/components/btw-panel.ts +15 -3
  70. package/src/modes/components/chat-transcript-builder.ts +8 -2
  71. package/src/modes/components/model-selector.ts +72 -9
  72. package/src/modes/components/omfg-panel.ts +1 -1
  73. package/src/modes/components/session-selector.ts +4 -9
  74. package/src/modes/components/snapcompact-shape-preview.ts +1 -1
  75. package/src/modes/components/tool-execution.ts +10 -2
  76. package/src/modes/controllers/btw-controller.ts +32 -7
  77. package/src/modes/controllers/command-controller.ts +18 -3
  78. package/src/modes/controllers/event-controller.ts +24 -4
  79. package/src/modes/controllers/input-controller.ts +43 -21
  80. package/src/modes/controllers/mcp-command-controller.ts +40 -12
  81. package/src/modes/controllers/selector-controller.ts +23 -13
  82. package/src/modes/controllers/streaming-reveal.ts +78 -20
  83. package/src/modes/controllers/todo-command-controller.ts +9 -10
  84. package/src/modes/interactive-mode.ts +84 -9
  85. package/src/modes/types.ts +4 -1
  86. package/src/modes/utils/ui-helpers.ts +1 -0
  87. package/src/prompts/advisor/system.md +1 -1
  88. package/src/prompts/system/side-channel-no-tools.md +3 -0
  89. package/src/sdk.ts +12 -8
  90. package/src/session/agent-session.ts +385 -85
  91. package/src/session/history-storage.ts +15 -16
  92. package/src/session/session-history-format.ts +41 -1
  93. package/src/session/snapcompact-inline.ts +9 -6
  94. package/src/slash-commands/builtin-registry.ts +171 -87
  95. package/src/slash-commands/helpers/todo.ts +12 -6
  96. package/src/slash-commands/types.ts +2 -0
  97. package/src/system-prompt.ts +6 -11
  98. package/src/tiny/models.ts +7 -3
  99. package/src/tiny/title-client.ts +8 -2
  100. package/src/tiny/worker.ts +1 -0
  101. package/src/tools/browser/launch.ts +107 -31
  102. package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
  103. package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
  104. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
  105. package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
  106. package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
  107. package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
  108. package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
  109. package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
  110. package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
  111. package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
  112. package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
  113. package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
  114. package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
  115. package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
  116. package/src/tools/search.ts +10 -1
  117. package/src/tools/sqlite-reader.ts +59 -3
  118. package/src/tools/todo.ts +6 -0
  119. package/src/tools/write.ts +4 -6
  120. 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
- const target = restArgs ? path.resolve(runtime.cwd, restArgs) : path.resolve(runtime.cwd, "TODO.md");
131
- await Bun.write(target, phasesToMarkdown(phases));
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
- const target = restArgs ? path.resolve(runtime.cwd, restArgs) : path.resolve(runtime.cwd, "TODO.md");
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 ${target}: ${err instanceof Error ? err.message : String(err)}`, runtime);
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 "/". */
@@ -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 > defaults
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
- if (tools) {
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 = tools?.has("read");
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, [
@@ -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 = "qwen3-1.7b";
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
- "Recommended; most disciplined extraction (ignores chit-chat), good consolidation, about 1.1 GB cached.",
127
- contextNote: "Best single-model pick for memory from the local experiment.",
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",
@@ -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
  }
@@ -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 fallbackVersionMatch = uaVersionMatch ?? (await page.browser().version()).match(/\/([\d|.]+)/);
383
- const uaVersion = fallbackVersionMatch?.[1] ?? "0";
384
- const majorVersion = Number.parseInt(uaVersion.split(".")[0] ?? "0", 10) || 0;
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 platform = userAgent.includes("Mac OS X")
387
- ? "MacIntel"
388
- : isAndroid
389
- ? "Android"
390
- : userAgent.includes("Linux")
391
- ? "Linux"
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
- : userAgent.includes("Windows ")
405
- ? (userAgent.match(/Windows .*?([\d|.]+);?/)?.[1] ?? "")
430
+ : isWindows
431
+ ? (userAgent.match(/Windows NT ([\d.]+)/)?.[1] ?? "")
406
432
  : "";
407
- const architecture = isAndroid ? "" : "x86";
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: uaVersion,
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
- // Native function cache - captured before any tampering
582
- const iframe = document.createElement("iframe");
583
- iframe.style.display = "none";
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 (!container) return;
586
- container.appendChild(iframe);
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
- // Helper to generate native code string
2
- const makeNativeString = (name) =>
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
- if (typeof fn === "function") {
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 descriptors
35
- Object.getOwnPropertyDescriptor = function (obj, prop) {
36
- const descriptor = Object_getOwnPropertyDescriptor.call(this, obj, prop);
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
- const getStr = makeNativeString("get " + String(prop));
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
- const setStr = makeNativeString("set " + String(prop));
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
- // Cleanup
63
- document.head.removeChild(iframe);
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
- const scheduleActivity = () => {
2
- const delay = 3000 + Math_random() * 4000;
3
- Window_setTimeout(() => {
4
- Object_defineProperty(document, "hidden", { get: () => false });
5
- Object_defineProperty(document, "visibilityState", {
6
- get: () => "visible",
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
- Object_defineProperty(document, "webkitVisibilityState", {
9
- get: () => "visible",
10
- });
11
- document.dispatchEvent(new Window_Event("visibilitychange"));
12
- if (Math_random() < 0.4) window.dispatchEvent(new Window_Event("focus"));
13
- document.hasFocus = () => true;
14
- if (Math_random() < 0.3) window.dispatchEvent(new Window_Event("scroll"));
15
- if (navigator.wakeLock)
16
- navigator.wakeLock.request("screen").catch(() => {});
17
- scheduleActivity();
18
- }, delay);
19
- };
20
- scheduleActivity();
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
- const elementDescriptor = Object_getOwnPropertyDescriptor(
2
- HTMLElement.prototype,
3
- "offsetHeight",
4
- );
5
- Object_defineProperty(HTMLDivElement.prototype, "offsetHeight", {
6
- ...elementDescriptor,
7
- get: function () {
8
- if (this.id === "modernizr") return 1;
9
- return Reflect_apply(elementDescriptor.get, this, []);
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
+ }