@steipete/oracle 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +66 -25
- package/dist/src/browser/actions/modelSelection.js +36 -4
- package/dist/src/browser/actions/thinkingTime.js +11 -5
- package/dist/src/browser/browserConnection.js +59 -0
- package/dist/src/browser/chatgptImages.js +1 -1
- package/dist/src/browser/chromeLifecycle.js +105 -36
- package/dist/src/browser/index.js +41 -8
- package/dist/src/browser/liveTabs.js +94 -29
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/reattach.js +157 -108
- package/dist/src/browser/recoveryTarget.js +31 -6
- package/dist/src/browser/sessionRunner.js +30 -7
- package/dist/src/browser/targetClaim.js +2 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +105 -20
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/sessionDisplay.js +11 -3
- package/dist/src/cli/sessionRunner.js +150 -37
- package/dist/src/mcp/tools/consult.js +1 -7
- package/dist/src/remote/client.js +71 -14
- package/dist/src/remote/health.js +1 -0
- package/dist/src/remote/server.js +52 -10
- package/dist/src/remote/types.js +13 -0
- package/dist/src/sessionManager.js +8 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +9 -9
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSubmittedPromptFingerprint, readUserMessageIds } from "./promptFingerprint.js";
|
|
1
2
|
import { mkdtemp, rm, mkdir } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import os from "node:os";
|
|
@@ -708,6 +709,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
708
709
|
let lastTargetId;
|
|
709
710
|
let lastUrl;
|
|
710
711
|
let promptSubmitted = false;
|
|
712
|
+
let submittedPromptHash = null;
|
|
711
713
|
let ownedRecoveryTarget;
|
|
712
714
|
const targetClaimId = randomUUID();
|
|
713
715
|
let modelSelectionEvidence;
|
|
@@ -728,6 +730,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
728
730
|
tabUrl: lastUrl,
|
|
729
731
|
conversationId,
|
|
730
732
|
promptSubmitted,
|
|
733
|
+
submittedPromptHash,
|
|
731
734
|
ownedRecoveryTarget,
|
|
732
735
|
userDataDir,
|
|
733
736
|
controllerPid: process.pid,
|
|
@@ -748,10 +751,8 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
748
751
|
}
|
|
749
752
|
};
|
|
750
753
|
const markPromptSubmitted = async () => {
|
|
751
|
-
if (promptSubmitted) {
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
754
|
promptSubmitted = true;
|
|
755
|
+
submittedPromptHash = null;
|
|
755
756
|
await emitRuntimeHint();
|
|
756
757
|
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
757
758
|
};
|
|
@@ -986,6 +987,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
986
987
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
987
988
|
: undefined,
|
|
988
989
|
promptSubmitted,
|
|
990
|
+
submittedPromptHash,
|
|
989
991
|
ownedRecoveryTarget,
|
|
990
992
|
controllerPid: process.pid,
|
|
991
993
|
researchPlan,
|
|
@@ -1208,7 +1210,9 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1208
1210
|
await captureRuntimeSnapshot();
|
|
1209
1211
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
1210
1212
|
if (config.desiredModel && modelStrategy !== "ignore" && !isResumingConversation) {
|
|
1211
|
-
modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy
|
|
1213
|
+
modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy, {
|
|
1214
|
+
implicitDefault: config.modelIsImplicitDefault,
|
|
1215
|
+
}), {
|
|
1212
1216
|
retries: 2,
|
|
1213
1217
|
delayMs: 300,
|
|
1214
1218
|
onRetry: (attempt, error) => {
|
|
@@ -1332,6 +1336,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1332
1336
|
const deepResearchTargetBaseline = deepResearch && client
|
|
1333
1337
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
1334
1338
|
: undefined;
|
|
1339
|
+
const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
|
|
1335
1340
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
1336
1341
|
prompt,
|
|
1337
1342
|
evaluate: async () => undefined,
|
|
@@ -1341,6 +1346,11 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1341
1346
|
});
|
|
1342
1347
|
await markPromptSubmitted();
|
|
1343
1348
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
1349
|
+
const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
|
|
1350
|
+
if (renderedPromptHash) {
|
|
1351
|
+
submittedPromptHash = renderedPromptHash;
|
|
1352
|
+
await emitRuntimeHint();
|
|
1353
|
+
}
|
|
1344
1354
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
1345
1355
|
baselineTurns = providerBaselineTurns;
|
|
1346
1356
|
}
|
|
@@ -1463,6 +1473,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1463
1473
|
tabUrl: lastUrl,
|
|
1464
1474
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1465
1475
|
promptSubmitted,
|
|
1476
|
+
submittedPromptHash,
|
|
1466
1477
|
ownedRecoveryTarget,
|
|
1467
1478
|
controllerPid: process.pid,
|
|
1468
1479
|
researchPlan,
|
|
@@ -1555,6 +1566,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1555
1566
|
tabUrl: lastUrl,
|
|
1556
1567
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1557
1568
|
promptSubmitted,
|
|
1569
|
+
submittedPromptHash,
|
|
1558
1570
|
ownedRecoveryTarget,
|
|
1559
1571
|
controllerPid: process.pid,
|
|
1560
1572
|
},
|
|
@@ -1612,6 +1624,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1612
1624
|
tabUrl: lastUrl,
|
|
1613
1625
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1614
1626
|
promptSubmitted,
|
|
1627
|
+
submittedPromptHash,
|
|
1615
1628
|
ownedRecoveryTarget,
|
|
1616
1629
|
controllerPid: process.pid,
|
|
1617
1630
|
};
|
|
@@ -1838,6 +1851,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1838
1851
|
tabUrl: lastUrl,
|
|
1839
1852
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1840
1853
|
promptSubmitted,
|
|
1854
|
+
submittedPromptHash,
|
|
1841
1855
|
ownedRecoveryTarget,
|
|
1842
1856
|
controllerPid: process.pid,
|
|
1843
1857
|
},
|
|
@@ -1906,6 +1920,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1906
1920
|
tabUrl: lastUrl,
|
|
1907
1921
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1908
1922
|
promptSubmitted,
|
|
1923
|
+
submittedPromptHash,
|
|
1909
1924
|
ownedRecoveryTarget,
|
|
1910
1925
|
controllerPid: process.pid,
|
|
1911
1926
|
};
|
|
@@ -1933,6 +1948,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1933
1948
|
chromeTargetId: lastTargetId,
|
|
1934
1949
|
tabUrl: lastUrl,
|
|
1935
1950
|
promptSubmitted,
|
|
1951
|
+
submittedPromptHash,
|
|
1936
1952
|
ownedRecoveryTarget,
|
|
1937
1953
|
controllerPid: process.pid,
|
|
1938
1954
|
};
|
|
@@ -1997,6 +2013,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1997
2013
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1998
2014
|
: undefined,
|
|
1999
2015
|
promptSubmitted,
|
|
2016
|
+
submittedPromptHash,
|
|
2000
2017
|
ownedRecoveryTarget,
|
|
2001
2018
|
controllerPid: process.pid,
|
|
2002
2019
|
researchPlan,
|
|
@@ -2397,6 +2414,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2397
2414
|
let tabLease = null;
|
|
2398
2415
|
let lastUrl;
|
|
2399
2416
|
let promptSubmitted = false;
|
|
2417
|
+
let submittedPromptHash = null;
|
|
2400
2418
|
let ownedRecoveryTarget;
|
|
2401
2419
|
const targetClaimId = randomUUID();
|
|
2402
2420
|
let modelSelectionEvidence;
|
|
@@ -2419,6 +2437,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2419
2437
|
tabUrl: lastUrl,
|
|
2420
2438
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2421
2439
|
promptSubmitted,
|
|
2440
|
+
submittedPromptHash,
|
|
2422
2441
|
ownedRecoveryTarget,
|
|
2423
2442
|
controllerPid: process.pid,
|
|
2424
2443
|
researchPlan,
|
|
@@ -2436,10 +2455,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2436
2455
|
}
|
|
2437
2456
|
};
|
|
2438
2457
|
const markPromptSubmitted = async () => {
|
|
2439
|
-
if (promptSubmitted) {
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
2458
|
promptSubmitted = true;
|
|
2459
|
+
submittedPromptHash = null;
|
|
2443
2460
|
await emitRuntimeHint();
|
|
2444
2461
|
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
2445
2462
|
};
|
|
@@ -2475,6 +2492,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2475
2492
|
const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
|
|
2476
2493
|
host,
|
|
2477
2494
|
port,
|
|
2495
|
+
browserWSEndpoint,
|
|
2496
|
+
approvalWaitMs: config.approvalWaitMs,
|
|
2478
2497
|
ref: tabRef,
|
|
2479
2498
|
}), (attached) => attached.client.close());
|
|
2480
2499
|
client = cancellation.client(attached.client);
|
|
@@ -2590,7 +2609,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2590
2609
|
}
|
|
2591
2610
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
2592
2611
|
if (config.desiredModel && modelStrategy !== "ignore" && !config.resumeConversationUrl) {
|
|
2593
|
-
modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy
|
|
2612
|
+
modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy, {
|
|
2613
|
+
implicitDefault: config.modelIsImplicitDefault,
|
|
2614
|
+
}), {
|
|
2594
2615
|
retries: 2,
|
|
2595
2616
|
delayMs: 300,
|
|
2596
2617
|
onRetry: (attempt, error) => {
|
|
@@ -2685,6 +2706,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2685
2706
|
const deepResearchTargetBaseline = deepResearch && client
|
|
2686
2707
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
2687
2708
|
: undefined;
|
|
2709
|
+
const previousUserMessageIds = await readUserMessageIds(Runtime, config.inputTimeoutMs);
|
|
2688
2710
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
2689
2711
|
prompt,
|
|
2690
2712
|
evaluate: async () => undefined,
|
|
@@ -2694,6 +2716,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2694
2716
|
});
|
|
2695
2717
|
await markPromptSubmitted();
|
|
2696
2718
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
2719
|
+
const renderedPromptHash = await readSubmittedPromptFingerprint(Runtime, previousUserMessageIds, config.inputTimeoutMs);
|
|
2720
|
+
if (renderedPromptHash) {
|
|
2721
|
+
submittedPromptHash = renderedPromptHash;
|
|
2722
|
+
await emitRuntimeHint();
|
|
2723
|
+
}
|
|
2697
2724
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
2698
2725
|
baselineTurns = providerBaselineTurns;
|
|
2699
2726
|
}
|
|
@@ -2790,6 +2817,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2790
2817
|
tabUrl: lastUrl,
|
|
2791
2818
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2792
2819
|
promptSubmitted,
|
|
2820
|
+
submittedPromptHash,
|
|
2793
2821
|
ownedRecoveryTarget,
|
|
2794
2822
|
controllerPid: process.pid,
|
|
2795
2823
|
researchPlan,
|
|
@@ -2881,6 +2909,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2881
2909
|
tabUrl: lastUrl,
|
|
2882
2910
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2883
2911
|
promptSubmitted,
|
|
2912
|
+
submittedPromptHash,
|
|
2884
2913
|
ownedRecoveryTarget,
|
|
2885
2914
|
controllerPid: process.pid,
|
|
2886
2915
|
},
|
|
@@ -2940,6 +2969,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2940
2969
|
tabUrl: lastUrl,
|
|
2941
2970
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2942
2971
|
promptSubmitted,
|
|
2972
|
+
submittedPromptHash,
|
|
2943
2973
|
ownedRecoveryTarget,
|
|
2944
2974
|
controllerPid: process.pid,
|
|
2945
2975
|
};
|
|
@@ -3124,6 +3154,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3124
3154
|
tabUrl: lastUrl,
|
|
3125
3155
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
3126
3156
|
promptSubmitted,
|
|
3157
|
+
submittedPromptHash,
|
|
3127
3158
|
ownedRecoveryTarget,
|
|
3128
3159
|
controllerPid: process.pid,
|
|
3129
3160
|
},
|
|
@@ -3187,6 +3218,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3187
3218
|
tabUrl: lastUrl,
|
|
3188
3219
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
3189
3220
|
promptSubmitted,
|
|
3221
|
+
submittedPromptHash,
|
|
3190
3222
|
ownedRecoveryTarget,
|
|
3191
3223
|
artifacts: savedArtifacts,
|
|
3192
3224
|
generatedImages: imageArtifacts.generatedImages,
|
|
@@ -3236,6 +3268,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3236
3268
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
3237
3269
|
: undefined,
|
|
3238
3270
|
promptSubmitted,
|
|
3271
|
+
submittedPromptHash,
|
|
3239
3272
|
ownedRecoveryTarget,
|
|
3240
3273
|
controllerPid: process.pid,
|
|
3241
3274
|
researchPlan,
|
|
@@ -5,6 +5,7 @@ import { captureAssistantMarkdown, readAssistantSnapshot } from "./actions/assis
|
|
|
5
5
|
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
6
6
|
import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
|
|
7
7
|
import { delay } from "./utils.js";
|
|
8
|
+
import { connectToRemoteChromeTarget, listRemoteChromeTargets } from "./chromeLifecycle.js";
|
|
8
9
|
export const DEFAULT_REMOTE_CHROME_HOST = "127.0.0.1";
|
|
9
10
|
export const DEFAULT_REMOTE_CHROME_PORT = 9222;
|
|
10
11
|
const LOGIN_CTA_PATTERN = /\b(log in|login|sign up|sign in|continue with google|continue with microsoft)\b/i;
|
|
@@ -22,8 +23,15 @@ function normalizeHostPort(input = {}) {
|
|
|
22
23
|
return {
|
|
23
24
|
host: input.host ?? DEFAULT_REMOTE_CHROME_HOST,
|
|
24
25
|
port: input.port ?? DEFAULT_REMOTE_CHROME_PORT,
|
|
26
|
+
...(input.browserWSEndpoint
|
|
27
|
+
? { browserWSEndpoint: input.browserWSEndpoint, approvalWaitMs: input.approvalWaitMs }
|
|
28
|
+
: {}),
|
|
25
29
|
};
|
|
26
30
|
}
|
|
31
|
+
function chromeTransportError(operation, endpoint, error) {
|
|
32
|
+
const detail = error instanceof Error ? error.message.trim() : String(error ?? "").trim();
|
|
33
|
+
return new Error(`Unable to ${operation} on Chrome at ${endpoint.host}:${endpoint.port}. ${detail || "Chrome returned no error details."} Check that Chrome is running and remote debugging is enabled, then retry and allow the connection prompt.`, { cause: error });
|
|
34
|
+
}
|
|
27
35
|
function normalizeUrl(value) {
|
|
28
36
|
return String(value ?? "").trim();
|
|
29
37
|
}
|
|
@@ -162,6 +170,9 @@ function buildTabInspectionExpression() {
|
|
|
162
170
|
const assistantCount = new Set(assistantOwners).size;
|
|
163
171
|
const lastAssistantText = normalize(lastAssistantNode?.textContent);
|
|
164
172
|
const lastUserText = normalize(lastUserTurn?.textContent);
|
|
173
|
+
const lastUserMessage = lastUserTurn?.matches?.('[data-message-author-role="user"]')
|
|
174
|
+
? lastUserTurn : lastUserTurn?.querySelector?.('[data-message-author-role="user"]');
|
|
175
|
+
const userContent = lastUserMessage?.querySelectorAll?.('[class~="whitespace-pre-wrap"]');
|
|
165
176
|
const authenticated = !loginButtonExists && (promptReady || sendExists || stopExists || assistantCount > 0);
|
|
166
177
|
return {
|
|
167
178
|
title: normalize(document.title),
|
|
@@ -178,6 +189,9 @@ function buildTabInspectionExpression() {
|
|
|
178
189
|
lastAssistantTurnIndex,
|
|
179
190
|
lastUserTurnIndex,
|
|
180
191
|
lastUserText,
|
|
192
|
+
lastUserTextRaw: lastUserMessage?.textContent,
|
|
193
|
+
lastUserContentText: userContent?.length === 1 ? userContent[0].textContent : undefined,
|
|
194
|
+
lastUserMessageId: lastUserMessage?.getAttribute?.('data-message-id'),
|
|
181
195
|
visibilityState: document.visibilityState,
|
|
182
196
|
focused: Boolean(document.hasFocus?.()),
|
|
183
197
|
};
|
|
@@ -187,26 +201,67 @@ export function buildTabInspectionExpressionForTest() {
|
|
|
187
201
|
return buildTabInspectionExpression();
|
|
188
202
|
}
|
|
189
203
|
export async function listChatGptTargets(options = {}) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
const endpoint = normalizeHostPort(options);
|
|
205
|
+
try {
|
|
206
|
+
const targets = await listRemoteChromeTargets(endpoint);
|
|
207
|
+
return targets.filter(isChatGptTarget);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw chromeTransportError("list ChatGPT tabs", endpoint, error);
|
|
211
|
+
}
|
|
193
212
|
}
|
|
194
213
|
export async function openChatGptTarget(options = {}) {
|
|
195
|
-
const
|
|
214
|
+
const endpoint = normalizeHostPort(options);
|
|
215
|
+
const { host, port } = endpoint;
|
|
196
216
|
const url = options.url ?? "https://chatgpt.com/";
|
|
197
|
-
|
|
198
|
-
|
|
217
|
+
try {
|
|
218
|
+
if (endpoint.browserWSEndpoint) {
|
|
219
|
+
const connection = await connectToRemoteChromeTarget(host, port, noopLogger, {
|
|
220
|
+
...endpoint,
|
|
221
|
+
targetUrl: url,
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
if (!connection.targetId)
|
|
225
|
+
throw new Error("Chrome did not return a target ID.");
|
|
226
|
+
return connection.targetId;
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
await connection.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const target = await CDP.New({ host, port, url });
|
|
233
|
+
return target.id;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw chromeTransportError("open saved ChatGPT conversation", endpoint, error);
|
|
237
|
+
}
|
|
199
238
|
}
|
|
200
|
-
async function connectToTarget(
|
|
201
|
-
const
|
|
239
|
+
async function connectToTarget(options, targetId) {
|
|
240
|
+
const endpoint = normalizeHostPort(options);
|
|
241
|
+
const { host, port } = endpoint;
|
|
242
|
+
const connection = await connectToRemoteChromeTarget(host, port, noopLogger, {
|
|
243
|
+
...endpoint,
|
|
244
|
+
targetId,
|
|
245
|
+
}).catch((error) => {
|
|
246
|
+
throw chromeTransportError("inspect ChatGPT tab", endpoint, error);
|
|
247
|
+
});
|
|
248
|
+
const client = Object.create(connection.client);
|
|
249
|
+
// Session-bound clients detach only; the owning connection also closes its browser socket.
|
|
250
|
+
client.close = connection.close;
|
|
202
251
|
const { Runtime, DOM } = client;
|
|
203
|
-
|
|
204
|
-
|
|
252
|
+
try {
|
|
253
|
+
if (Runtime?.enable) {
|
|
254
|
+
await Runtime.enable();
|
|
255
|
+
}
|
|
256
|
+
if (DOM?.enable) {
|
|
257
|
+
await DOM.enable();
|
|
258
|
+
}
|
|
259
|
+
return client;
|
|
205
260
|
}
|
|
206
|
-
|
|
207
|
-
await
|
|
261
|
+
catch (error) {
|
|
262
|
+
await connection.close().catch(() => undefined);
|
|
263
|
+
throw error;
|
|
208
264
|
}
|
|
209
|
-
return client;
|
|
210
265
|
}
|
|
211
266
|
export async function inspectChatGptTab(options) {
|
|
212
267
|
const { host, port } = normalizeHostPort(options);
|
|
@@ -215,7 +270,7 @@ export async function inspectChatGptTab(options) {
|
|
|
215
270
|
if (!targetId) {
|
|
216
271
|
throw new Error("inspectChatGptTab requires a target with targetId.");
|
|
217
272
|
}
|
|
218
|
-
const client = await connectToTarget(
|
|
273
|
+
const client = await connectToTarget(options, targetId);
|
|
219
274
|
try {
|
|
220
275
|
const { Runtime } = client;
|
|
221
276
|
const evaluation = await Runtime.evaluate({
|
|
@@ -263,6 +318,9 @@ export async function inspectChatGptTab(options) {
|
|
|
263
318
|
: undefined,
|
|
264
319
|
lastAssistantSnippet: trimToSnippet(lastAssistantText),
|
|
265
320
|
lastUserText,
|
|
321
|
+
lastUserTextRaw: info.lastUserTextRaw,
|
|
322
|
+
lastUserContentText: info.lastUserContentText,
|
|
323
|
+
lastUserMessageId: info.lastUserMessageId,
|
|
266
324
|
lastUserSnippet: trimToSnippet(lastUserText),
|
|
267
325
|
focused: Boolean(info.focused),
|
|
268
326
|
visibilityState: typeof info.visibilityState === "string" ? info.visibilityState : "",
|
|
@@ -299,11 +357,11 @@ export function classifyTabState(summary) {
|
|
|
299
357
|
}
|
|
300
358
|
export async function collectChatGptTabs(options = {}) {
|
|
301
359
|
const { host, port } = normalizeHostPort(options);
|
|
302
|
-
const targets = await listChatGptTargets(
|
|
360
|
+
const targets = await listChatGptTargets(options);
|
|
303
361
|
const summaries = [];
|
|
304
362
|
for (const target of targets) {
|
|
305
363
|
try {
|
|
306
|
-
const summary = await inspectChatGptTab({
|
|
364
|
+
const summary = await inspectChatGptTab({ ...options, target });
|
|
307
365
|
summaries.push(summary);
|
|
308
366
|
}
|
|
309
367
|
catch (error) {
|
|
@@ -377,28 +435,33 @@ export function resolveChatGptTabFromSummariesForTest(summaries, ref) {
|
|
|
377
435
|
return resolveChatGptTabFromSummaries(summaries, ref);
|
|
378
436
|
}
|
|
379
437
|
export async function resolveChatGptTab(options = {}) {
|
|
380
|
-
const
|
|
381
|
-
|
|
438
|
+
const ref = options.ref?.trim();
|
|
439
|
+
if (ref && ref.toLowerCase() !== "current") {
|
|
440
|
+
const targets = await listChatGptTargets(options);
|
|
441
|
+
const exact = targets.find((target) => extractTargetId(target) === ref ||
|
|
442
|
+
target.url === ref ||
|
|
443
|
+
extractConversationIdFromUrl(target.url ?? "") === ref);
|
|
444
|
+
if (exact)
|
|
445
|
+
return inspectChatGptTab({ ...options, target: exact });
|
|
446
|
+
}
|
|
447
|
+
const summaries = await collectChatGptTabs(options);
|
|
382
448
|
return resolveChatGptTabFromSummaries(summaries, options.ref);
|
|
383
449
|
}
|
|
384
450
|
export async function connectToExistingChatGptTab(options = {}) {
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
const client = await connectToTarget(host, port, tab.targetId);
|
|
451
|
+
const tab = await resolveChatGptTab(options);
|
|
452
|
+
const client = await connectToTarget(options, tab.targetId);
|
|
388
453
|
return { client, targetId: tab.targetId, tab };
|
|
389
454
|
}
|
|
390
455
|
export async function harvestChatGptTab(options = {}) {
|
|
391
|
-
const { host, port } = normalizeHostPort(options);
|
|
392
456
|
const resolved = options.target
|
|
393
|
-
? await inspectChatGptTab({
|
|
394
|
-
: await resolveChatGptTab(
|
|
395
|
-
const client = await connectToTarget(
|
|
457
|
+
? await inspectChatGptTab({ ...options, target: options.target })
|
|
458
|
+
: await resolveChatGptTab(options);
|
|
459
|
+
const client = await connectToTarget(options, resolved.targetId);
|
|
396
460
|
try {
|
|
397
461
|
const { Runtime } = client;
|
|
398
462
|
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
399
463
|
const nowSummary = await inspectChatGptTab({
|
|
400
|
-
|
|
401
|
-
port,
|
|
464
|
+
...options,
|
|
402
465
|
target: {
|
|
403
466
|
targetId: resolved.targetId,
|
|
404
467
|
title: resolved.title,
|
|
@@ -442,8 +505,7 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
442
505
|
const firstFingerprint = harvested.fingerprint;
|
|
443
506
|
await delay(options.stallWindowMs);
|
|
444
507
|
const followup = await inspectChatGptTab({
|
|
445
|
-
|
|
446
|
-
port,
|
|
508
|
+
...options,
|
|
447
509
|
target: {
|
|
448
510
|
targetId: harvested.targetId,
|
|
449
511
|
title: harvested.title,
|
|
@@ -461,6 +523,9 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
461
523
|
harvested.authenticated = followup.authenticated;
|
|
462
524
|
harvested.loginButtonExists = followup.loginButtonExists;
|
|
463
525
|
harvested.lastUserText = followup.lastUserText;
|
|
526
|
+
harvested.lastUserTextRaw = followup.lastUserTextRaw;
|
|
527
|
+
harvested.lastUserContentText = followup.lastUserContentText;
|
|
528
|
+
harvested.lastUserMessageId = followup.lastUserMessageId;
|
|
464
529
|
harvested.lastUserSnippet = followup.lastUserSnippet;
|
|
465
530
|
harvested.assistantFollowsLatestUser = followup.assistantFollowsLatestUser;
|
|
466
531
|
harvested.lastAssistantTurnIndex = followup.lastAssistantTurnIndex;
|
|
@@ -224,7 +224,12 @@ export async function readProcessStartTimeMs(pid) {
|
|
|
224
224
|
if (Math.trunc(pid) === process.pid) {
|
|
225
225
|
// Use the same OS identity as peer controllers, not wall time minus uptime.
|
|
226
226
|
// Cache our own PID only: it cannot be reused during this process's lifetime.
|
|
227
|
-
return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid))
|
|
227
|
+
return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid).then((startedAt) => {
|
|
228
|
+
// A transient probe failure is not a permanent process identity.
|
|
229
|
+
if (startedAt === null)
|
|
230
|
+
ownProcessStartTime = undefined;
|
|
231
|
+
return startedAt;
|
|
232
|
+
}));
|
|
228
233
|
}
|
|
229
234
|
return queryProcessStartTimeMs(pid);
|
|
230
235
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
3
|
+
export function browserPromptFingerprint(value, messageId) {
|
|
4
|
+
return createHash("sha256")
|
|
5
|
+
.update(JSON.stringify([messageId, String(value ?? "").replace(/\r\n?/g, "\n")]))
|
|
6
|
+
.digest("hex");
|
|
7
|
+
}
|
|
8
|
+
export function readUserMessageIds(runtime, timeoutMs = 0) {
|
|
9
|
+
return readDomUntil(runtime, `Array.from(document.querySelectorAll('[data-message-author-role="user"]'), user => user.getAttribute('data-message-id'))`, timeoutMs, (value) => Array.isArray(value) && value.every((id) => typeof id === "string" && id.trim())
|
|
10
|
+
? value
|
|
11
|
+
: undefined);
|
|
12
|
+
}
|
|
13
|
+
export async function readSubmittedPromptFingerprint(runtime, previousMessageIds, timeoutMs = 0) {
|
|
14
|
+
if (previousMessageIds === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
const previous = new Set(previousMessageIds);
|
|
17
|
+
return readDomUntil(runtime, `(() => {
|
|
18
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
19
|
+
for (let index = turns.length - 1; index >= 0; index--) {
|
|
20
|
+
const turn = turns[index];
|
|
21
|
+
const user = turn.matches('[data-message-author-role="user"]') ? turn : turn.querySelector('[data-message-author-role="user"]');
|
|
22
|
+
if (user) return { text: user.textContent, messageId: user.getAttribute('data-message-id') };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
})()`, timeoutMs, (value) => {
|
|
26
|
+
const turn = value;
|
|
27
|
+
if (typeof turn?.text === "string" &&
|
|
28
|
+
turn.text.trim() &&
|
|
29
|
+
typeof turn.messageId === "string" &&
|
|
30
|
+
turn.messageId.trim() &&
|
|
31
|
+
!previous.has(turn.messageId))
|
|
32
|
+
return browserPromptFingerprint(turn.text, turn.messageId);
|
|
33
|
+
return undefined;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function readDomUntil(runtime, expression, timeoutMs, select) {
|
|
37
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
38
|
+
for (;;) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await runtime.evaluate({ expression, returnByValue: true });
|
|
41
|
+
const selected = select(result.result?.value);
|
|
42
|
+
if (selected !== undefined)
|
|
43
|
+
return selected;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (!/Cannot find (?:default )?(?:execution )?context|Execution context (?:was destroyed|is not available)/i.test(message))
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (Date.now() >= deadline)
|
|
51
|
+
return undefined;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, deadline - Date.now())));
|
|
53
|
+
}
|
|
54
|
+
}
|