@steipete/oracle 0.10.0 → 0.11.1
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/README.md +56 -11
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +236 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +86 -16
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +27 -9
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1234 -479
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +41 -8
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- package/package.json +7 -6
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
export function isProjectChatgptUrl(url) {
|
|
2
|
+
return /\/project(?:[/?#]|$)/i.test(url ?? "");
|
|
3
|
+
}
|
|
4
|
+
export function isTemporaryChatgptUrl(url) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = new URL(url ?? "");
|
|
7
|
+
return (parsed.searchParams.get("temporary-chat") ?? "").trim().toLowerCase() === "true";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function resolveBrowserArchiveDecision({ mode = "auto", chatgptUrl, conversationUrl, researchMode, followUpCount, }) {
|
|
14
|
+
if (mode === "never") {
|
|
15
|
+
return { mode, shouldArchive: false, reason: "disabled" };
|
|
16
|
+
}
|
|
17
|
+
if (!conversationUrl) {
|
|
18
|
+
return { mode, shouldArchive: false, reason: "missing-conversation-url" };
|
|
19
|
+
}
|
|
20
|
+
if (isTemporaryChatgptUrl(chatgptUrl) || isTemporaryChatgptUrl(conversationUrl)) {
|
|
21
|
+
return { mode, shouldArchive: false, reason: "temporary-chat" };
|
|
22
|
+
}
|
|
23
|
+
if (mode === "always") {
|
|
24
|
+
return { mode, shouldArchive: true, reason: "forced" };
|
|
25
|
+
}
|
|
26
|
+
if (isProjectChatgptUrl(chatgptUrl) || isProjectChatgptUrl(conversationUrl)) {
|
|
27
|
+
return { mode, shouldArchive: false, reason: "project-conversation" };
|
|
28
|
+
}
|
|
29
|
+
if (researchMode === "deep") {
|
|
30
|
+
return { mode, shouldArchive: false, reason: "deep-research" };
|
|
31
|
+
}
|
|
32
|
+
if ((followUpCount ?? 0) > 0) {
|
|
33
|
+
return { mode, shouldArchive: false, reason: "multi-turn" };
|
|
34
|
+
}
|
|
35
|
+
return { mode, shouldArchive: true, reason: "successful-one-shot" };
|
|
36
|
+
}
|
|
37
|
+
export async function archiveChatGptConversation(Runtime, logger, { mode, conversationUrl, }) {
|
|
38
|
+
const evaluated = await Runtime.evaluate({
|
|
39
|
+
expression: buildArchiveConversationExpression(),
|
|
40
|
+
awaitPromise: true,
|
|
41
|
+
returnByValue: true,
|
|
42
|
+
});
|
|
43
|
+
const value = evaluated.result?.value;
|
|
44
|
+
const resolvedUrl = value?.conversationUrl ?? conversationUrl ?? undefined;
|
|
45
|
+
if (value?.status === "archived") {
|
|
46
|
+
logger("[browser] Archived ChatGPT conversation after saving local artifacts.");
|
|
47
|
+
return { mode, attempted: true, archived: true, conversationUrl: resolvedUrl };
|
|
48
|
+
}
|
|
49
|
+
const reason = value?.status === "skipped" ? value.reason : "archive-failed";
|
|
50
|
+
const error = value?.status === "failed" ? value.error : undefined;
|
|
51
|
+
logger(`[browser] ChatGPT archive skipped (${error ?? reason}).`);
|
|
52
|
+
return {
|
|
53
|
+
mode,
|
|
54
|
+
attempted: true,
|
|
55
|
+
archived: false,
|
|
56
|
+
reason,
|
|
57
|
+
conversationUrl: resolvedUrl,
|
|
58
|
+
error,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function buildArchiveConversationExpressionForTest() {
|
|
62
|
+
return buildArchiveConversationExpression();
|
|
63
|
+
}
|
|
64
|
+
function buildArchiveConversationExpression() {
|
|
65
|
+
return `(() => {
|
|
66
|
+
const conversationUrl = typeof location === 'object' ? location.href : null;
|
|
67
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
const normalize = (value) =>
|
|
69
|
+
String(value ?? '')
|
|
70
|
+
.replace(/\\s+/g, ' ')
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase();
|
|
73
|
+
const isVisible = (element) => {
|
|
74
|
+
if (!element || !(element instanceof HTMLElement)) return false;
|
|
75
|
+
const rect = element.getBoundingClientRect();
|
|
76
|
+
const style = getComputedStyle(element);
|
|
77
|
+
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
|
|
78
|
+
};
|
|
79
|
+
const labelFor = (element) =>
|
|
80
|
+
normalize([
|
|
81
|
+
element.getAttribute?.('aria-label'),
|
|
82
|
+
element.getAttribute?.('title'),
|
|
83
|
+
element.textContent,
|
|
84
|
+
].filter(Boolean).join(' '));
|
|
85
|
+
const click = (element) => {
|
|
86
|
+
const rect = element.getBoundingClientRect();
|
|
87
|
+
const eventInit = {
|
|
88
|
+
bubbles: true,
|
|
89
|
+
cancelable: true,
|
|
90
|
+
view: window,
|
|
91
|
+
clientX: rect.left + rect.width / 2,
|
|
92
|
+
clientY: rect.top + rect.height / 2,
|
|
93
|
+
button: 0,
|
|
94
|
+
};
|
|
95
|
+
if (typeof PointerEvent === 'function') {
|
|
96
|
+
element.dispatchEvent(new PointerEvent('pointerdown', {
|
|
97
|
+
...eventInit,
|
|
98
|
+
buttons: 1,
|
|
99
|
+
pointerId: 1,
|
|
100
|
+
pointerType: 'mouse',
|
|
101
|
+
isPrimary: true,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
element.dispatchEvent(new MouseEvent('mousedown', { ...eventInit, buttons: 1 }));
|
|
105
|
+
if (typeof PointerEvent === 'function') {
|
|
106
|
+
element.dispatchEvent(new PointerEvent('pointerup', {
|
|
107
|
+
...eventInit,
|
|
108
|
+
buttons: 0,
|
|
109
|
+
pointerId: 1,
|
|
110
|
+
pointerType: 'mouse',
|
|
111
|
+
isPrimary: true,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
element.dispatchEvent(new MouseEvent('mouseup', { ...eventInit, buttons: 0 }));
|
|
115
|
+
element.dispatchEvent(new MouseEvent('click', { ...eventInit, buttons: 0 }));
|
|
116
|
+
};
|
|
117
|
+
const findConversationMenuButton = () => {
|
|
118
|
+
const buttons = Array.from(document.querySelectorAll('button,[role="button"]'))
|
|
119
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
120
|
+
const labelled = buttons
|
|
121
|
+
.map((element) => ({ element, label: labelFor(element), rect: element.getBoundingClientRect() }))
|
|
122
|
+
.filter(({ label }) =>
|
|
123
|
+
label.includes('more') ||
|
|
124
|
+
label.includes('conversation options') ||
|
|
125
|
+
label.includes('open menu') ||
|
|
126
|
+
label.includes('więcej') ||
|
|
127
|
+
label.includes('opcje')
|
|
128
|
+
);
|
|
129
|
+
const headerCandidates = labelled
|
|
130
|
+
.filter(({ rect }) => rect.top < 180 && rect.right > window.innerWidth - 420)
|
|
131
|
+
.sort((a, b) => b.rect.right - a.rect.right);
|
|
132
|
+
return (headerCandidates[0] ?? labelled[0])?.element ?? null;
|
|
133
|
+
};
|
|
134
|
+
const visibleMenuCandidates = () => {
|
|
135
|
+
const menuRoots = Array.from(document.querySelectorAll('[role="menu"]'))
|
|
136
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
137
|
+
const roots = menuRoots.length > 0 ? menuRoots : [document];
|
|
138
|
+
return roots.flatMap((root) =>
|
|
139
|
+
Array.from(root.querySelectorAll('[role="menuitem"],[role="option"],button,div[tabindex],a')),
|
|
140
|
+
).filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
141
|
+
};
|
|
142
|
+
const findArchiveMenuItem = () => {
|
|
143
|
+
const candidates = visibleMenuCandidates();
|
|
144
|
+
return candidates.find((element) => {
|
|
145
|
+
const label = labelFor(element);
|
|
146
|
+
if (!label) return false;
|
|
147
|
+
if (label.includes('unarchive') || label.includes('restore')) return false;
|
|
148
|
+
return label.includes('archive') || label.includes('archiwizuj');
|
|
149
|
+
}) ?? null;
|
|
150
|
+
};
|
|
151
|
+
const findArchiveConfirmationButton = () => {
|
|
152
|
+
const candidates = Array.from(document.querySelectorAll('[role="dialog"] button,[role="dialog"] [role="button"]'))
|
|
153
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
154
|
+
return candidates.find((element) => {
|
|
155
|
+
const label = labelFor(element);
|
|
156
|
+
if (!label) return false;
|
|
157
|
+
if (label.includes('unarchive') || label.includes('restore')) return false;
|
|
158
|
+
return label === 'archive' || label === 'archiwizuj' || label.includes('archive conversation');
|
|
159
|
+
}) ?? null;
|
|
160
|
+
};
|
|
161
|
+
const hasUnarchiveMenuItem = () => {
|
|
162
|
+
const candidates = visibleMenuCandidates();
|
|
163
|
+
return candidates.some((element) => {
|
|
164
|
+
const label = labelFor(element);
|
|
165
|
+
return (
|
|
166
|
+
label.includes('unarchive') ||
|
|
167
|
+
label.includes('restore') ||
|
|
168
|
+
label.includes('przywróć') ||
|
|
169
|
+
label.includes('przywroc')
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const hasArchiveConfirmation = () => {
|
|
174
|
+
const visibleText = Array.from(document.querySelectorAll('[role="status"],[role="alert"],[data-testid*="toast"],[class*="toast"],[class*="snackbar"]'))
|
|
175
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element))
|
|
176
|
+
.map((element) => labelFor(element))
|
|
177
|
+
.join(' ');
|
|
178
|
+
return (
|
|
179
|
+
visibleText.includes('archived') ||
|
|
180
|
+
visibleText.includes('conversation archived') ||
|
|
181
|
+
visibleText.includes('chat archived') ||
|
|
182
|
+
visibleText.includes('zarchiwizowano') ||
|
|
183
|
+
visibleText.includes('archiwum')
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
const waitForArchiveConfirmation = async () => {
|
|
187
|
+
const deadline = Date.now() + 3000;
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
if (conversationUrl && location.href !== conversationUrl) return true;
|
|
190
|
+
if (hasArchiveConfirmation()) return true;
|
|
191
|
+
await sleep(150);
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
};
|
|
195
|
+
const verifyArchivedStateFromMenu = async () => {
|
|
196
|
+
const menuButton = findConversationMenuButton();
|
|
197
|
+
if (!menuButton) return false;
|
|
198
|
+
click(menuButton);
|
|
199
|
+
await sleep(300);
|
|
200
|
+
const archived = hasUnarchiveMenuItem();
|
|
201
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
202
|
+
return archived;
|
|
203
|
+
};
|
|
204
|
+
return (async () => {
|
|
205
|
+
const menuButton = findConversationMenuButton();
|
|
206
|
+
if (!menuButton) {
|
|
207
|
+
return { status: 'skipped', reason: 'conversation-menu-not-found', conversationUrl };
|
|
208
|
+
}
|
|
209
|
+
click(menuButton);
|
|
210
|
+
await sleep(350);
|
|
211
|
+
const archiveItem = findArchiveMenuItem();
|
|
212
|
+
if (!archiveItem) {
|
|
213
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
214
|
+
return { status: 'skipped', reason: 'archive-menu-item-not-found', conversationUrl };
|
|
215
|
+
}
|
|
216
|
+
click(archiveItem);
|
|
217
|
+
await sleep(350);
|
|
218
|
+
const confirmButton = findArchiveConfirmationButton();
|
|
219
|
+
if (confirmButton) {
|
|
220
|
+
click(confirmButton);
|
|
221
|
+
await sleep(500);
|
|
222
|
+
}
|
|
223
|
+
if (await waitForArchiveConfirmation()) {
|
|
224
|
+
return { status: 'archived', conversationUrl };
|
|
225
|
+
}
|
|
226
|
+
if (await verifyArchivedStateFromMenu()) {
|
|
227
|
+
return { status: 'archived', conversationUrl };
|
|
228
|
+
}
|
|
229
|
+
return { status: 'skipped', reason: 'archive-not-confirmed', conversationUrl };
|
|
230
|
+
})().catch((error) => ({
|
|
231
|
+
status: 'failed',
|
|
232
|
+
error: error instanceof Error ? error.message : String(error),
|
|
233
|
+
conversationUrl,
|
|
234
|
+
}));
|
|
235
|
+
})()`;
|
|
236
|
+
}
|
|
@@ -109,6 +109,10 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
109
109
|
}
|
|
110
110
|
const refreshed = await refreshAssistantSnapshot(Runtime, parsed, logger, minTurnIndex, expectedConversationId);
|
|
111
111
|
const candidate = refreshed ?? parsed;
|
|
112
|
+
if (isGeneratedImageAssistantAnswer(candidate)) {
|
|
113
|
+
logger("Captured assistant generated image response");
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
112
116
|
// The evaluation path can race ahead of completion. If ChatGPT is still streaming, wait for the watchdog poller.
|
|
113
117
|
const elapsedMs = Date.now() - start;
|
|
114
118
|
const remainingMs = Math.max(0, timeoutMs - elapsedMs);
|
|
@@ -308,6 +312,9 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expecte
|
|
|
308
312
|
isStopButtonVisible(Runtime),
|
|
309
313
|
isCompletionVisible(Runtime),
|
|
310
314
|
]);
|
|
315
|
+
if (isGeneratedImageAssistantAnswer(normalized)) {
|
|
316
|
+
return normalized;
|
|
317
|
+
}
|
|
311
318
|
const shortAnswer = currentLength > 0 && currentLength < 16;
|
|
312
319
|
const mediumAnswer = currentLength >= 16 && currentLength < 40;
|
|
313
320
|
const longAnswer = currentLength >= 40 && currentLength < 500;
|
|
@@ -413,6 +420,9 @@ function normalizeAssistantSnapshot(snapshot) {
|
|
|
413
420
|
meta: { turnId: snapshot?.turnId ?? undefined, messageId: snapshot?.messageId ?? undefined },
|
|
414
421
|
};
|
|
415
422
|
}
|
|
423
|
+
function isGeneratedImageAssistantAnswer(answer) {
|
|
424
|
+
return Boolean(answer?.html?.includes("/backend-api/estuary/content?id=file_"));
|
|
425
|
+
}
|
|
416
426
|
async function waitForCondition(getter, timeoutMs, pollIntervalMs = 400) {
|
|
417
427
|
const deadline = Date.now() + timeoutMs;
|
|
418
428
|
while (Date.now() < deadline) {
|
|
@@ -627,6 +637,9 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
627
637
|
};
|
|
628
638
|
|
|
629
639
|
const waitForSettle = async (snapshot) => {
|
|
640
|
+
if (String(snapshot?.html ?? '').includes('/backend-api/estuary/content?id=file_')) {
|
|
641
|
+
return snapshot;
|
|
642
|
+
}
|
|
630
643
|
// Learned: short answers can be 1-2 tokens; enforce longer settle windows to avoid truncation.
|
|
631
644
|
// Learned: long streaming responses (esp. thinking models) can pause mid-stream;
|
|
632
645
|
// use progressively longer windows to avoid truncation (#71).
|
|
@@ -754,6 +767,19 @@ function buildAssistantExtractor(functionName) {
|
|
|
754
767
|
const html = contentRoot?.innerHTML ?? '';
|
|
755
768
|
const messageId = messageRoot.getAttribute('data-message-id');
|
|
756
769
|
const turnId = messageRoot.getAttribute('data-testid');
|
|
770
|
+
const generatedImages = Array.from(messageRoot.querySelectorAll('img')).filter((img) =>
|
|
771
|
+
String(img?.src || '').includes('/backend-api/estuary/content?id=file_')
|
|
772
|
+
);
|
|
773
|
+
const normalizedText = String(text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
774
|
+
const imageOnlyChrome =
|
|
775
|
+
!normalizedText ||
|
|
776
|
+
normalizedText === 'edit' ||
|
|
777
|
+
normalizedText === 'stopped thinking' ||
|
|
778
|
+
normalizedText === 'stopped thinking edit';
|
|
779
|
+
if (generatedImages.length > 0 && imageOnlyChrome) {
|
|
780
|
+
const label = generatedImages.length === 1 ? 'Generated image.' : \`Generated \${generatedImages.length} images.\`;
|
|
781
|
+
return { text: label, html: messageRoot?.innerHTML ?? html, messageId, turnId, turnIndex: index };
|
|
782
|
+
}
|
|
757
783
|
if (text.trim()) {
|
|
758
784
|
return { text, html, messageId, turnId, turnIndex: index };
|
|
759
785
|
}
|