@steipete/oracle 0.5.3 → 0.5.6
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 +2 -0
- package/dist/src/browser/actions/assistantResponse.js +3 -2
- package/dist/src/browser/actions/attachments.js +267 -49
- package/dist/src/browser/actions/promptComposer.js +62 -5
- package/dist/src/browser/chromeLifecycle.js +13 -4
- package/dist/src/browser/constants.js +7 -4
- package/dist/src/browser/index.js +10 -5
- package/dist/src/oracle/errors.js +1 -1
- package/dist/src/oracle/run.js +8 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +38 -26
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/.DS_Store +0 -0
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Oracle bundles your prompt and files so another AI can answer with real context.
|
|
|
17
17
|
|
|
18
18
|
Install globally: `npm install -g @steipete/oracle`
|
|
19
19
|
|
|
20
|
+
Use `npx -y @steipete/oracle …` (not `pnpx`)—pnpx's sandboxed cache can’t load the sqlite bindings and will throw missing `node_sqlite3.node` errors.
|
|
21
|
+
|
|
20
22
|
```bash
|
|
21
23
|
# Copy the bundle and paste into ChatGPT
|
|
22
24
|
npx @steipete/oracle --render --copy -p "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"
|
|
@@ -291,8 +291,9 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
291
291
|
if (!stop) {
|
|
292
292
|
return;
|
|
293
293
|
}
|
|
294
|
-
const
|
|
295
|
-
|
|
294
|
+
const isStopButton =
|
|
295
|
+
stop.getAttribute('data-testid') === 'stop-button' || stop.getAttribute('aria-label')?.toLowerCase()?.includes('stop');
|
|
296
|
+
if (isStopButton) {
|
|
296
297
|
return;
|
|
297
298
|
}
|
|
298
299
|
dispatchClickSequence(stop);
|
|
@@ -7,32 +7,163 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
7
7
|
if (!dom) {
|
|
8
8
|
throw new Error('DOM domain unavailable while uploading attachments.');
|
|
9
9
|
}
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
const isAttachmentPresent = async (name) => {
|
|
11
|
+
const check = await runtime.evaluate({
|
|
12
|
+
expression: `(() => {
|
|
13
|
+
const expected = ${JSON.stringify(name.toLowerCase())};
|
|
14
|
+
const selectors = [
|
|
15
|
+
'[data-testid*="attachment"]',
|
|
16
|
+
'[data-testid*="chip"]',
|
|
17
|
+
'[data-testid*="upload"]'
|
|
18
|
+
];
|
|
19
|
+
const chips = selectors.some((selector) =>
|
|
20
|
+
Array.from(document.querySelectorAll(selector)).some((node) =>
|
|
21
|
+
(node?.textContent || '').toLowerCase().includes(expected),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
if (chips) return true;
|
|
25
|
+
const cardTexts = Array.from(document.querySelectorAll('[aria-label="Remove file"]')).map((btn) =>
|
|
26
|
+
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
27
|
+
);
|
|
28
|
+
if (cardTexts.some((text) => text.includes(expected))) return true;
|
|
29
|
+
|
|
30
|
+
const filesPill = Array.from(document.querySelectorAll('button,div')).some((node) => {
|
|
31
|
+
const text = (node?.textContent || '').toLowerCase();
|
|
32
|
+
return /\bfiles\b/.test(text) && text.includes('file');
|
|
33
|
+
});
|
|
34
|
+
if (filesPill) return true;
|
|
35
|
+
|
|
36
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]')).some((el) =>
|
|
37
|
+
Array.from(el.files || []).some((f) => f?.name?.toLowerCase?.().includes(expected)),
|
|
38
|
+
);
|
|
39
|
+
return inputs;
|
|
40
|
+
})()`,
|
|
41
|
+
returnByValue: true,
|
|
42
|
+
});
|
|
43
|
+
return Boolean(check?.result?.value);
|
|
44
|
+
};
|
|
45
|
+
// New ChatGPT UI hides the real file input behind a composer "+" menu; click it pre-emptively.
|
|
46
|
+
await Promise.resolve(runtime.evaluate({
|
|
47
|
+
expression: `(() => {
|
|
48
|
+
const selectors = [
|
|
49
|
+
'#composer-plus-btn',
|
|
50
|
+
'button[data-testid="composer-plus-btn"]',
|
|
51
|
+
'[data-testid*="plus"]',
|
|
52
|
+
'button[aria-label*="add"]',
|
|
53
|
+
'button[aria-label*="attachment"]',
|
|
54
|
+
'button[aria-label*="file"]',
|
|
55
|
+
];
|
|
56
|
+
for (const selector of selectors) {
|
|
57
|
+
const el = document.querySelector(selector);
|
|
58
|
+
if (el instanceof HTMLElement) {
|
|
59
|
+
el.click();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
18
62
|
}
|
|
63
|
+
return false;
|
|
64
|
+
})()`,
|
|
65
|
+
returnByValue: true,
|
|
66
|
+
})).catch(() => undefined);
|
|
67
|
+
await delay(250);
|
|
68
|
+
// Helper to click the upload menu item (if present) to reveal the real attachment input.
|
|
69
|
+
await Promise.resolve(runtime.evaluate({
|
|
70
|
+
expression: `(() => {
|
|
71
|
+
const menuItems = Array.from(document.querySelectorAll('[data-testid*="upload"],[data-testid*="attachment"], [role="menuitem"], [data-radix-collection-item]'));
|
|
72
|
+
for (const el of menuItems) {
|
|
73
|
+
const text = (el.textContent || '').toLowerCase();
|
|
74
|
+
const tid = el.getAttribute?.('data-testid')?.toLowerCase?.() || '';
|
|
75
|
+
if (tid.includes('upload') || tid.includes('attachment') || text.includes('upload') || text.includes('file')) {
|
|
76
|
+
if (el instanceof HTMLElement) { el.click(); return true; }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
})()`,
|
|
81
|
+
returnByValue: true,
|
|
82
|
+
})).catch(() => undefined);
|
|
83
|
+
const expectedName = path.basename(attachment.path);
|
|
84
|
+
if (await isAttachmentPresent(expectedName)) {
|
|
85
|
+
logger(`Attachment already present: ${path.basename(attachment.path)}`);
|
|
86
|
+
return;
|
|
19
87
|
}
|
|
20
|
-
|
|
21
|
-
|
|
88
|
+
// Find a real input; prefer non-image accept fields and tag it for DOM.setFileInputFiles.
|
|
89
|
+
const markResult = await runtime.evaluate({
|
|
90
|
+
expression: `(() => {
|
|
91
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
92
|
+
const acceptIsImageOnly = (accept) => {
|
|
93
|
+
if (!accept) return false;
|
|
94
|
+
const parts = String(accept)
|
|
95
|
+
.split(',')
|
|
96
|
+
.map((p) => p.trim().toLowerCase())
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
return parts.length > 0 && parts.every((p) => p.startsWith('image/'));
|
|
99
|
+
};
|
|
100
|
+
const nonImage = inputs.filter((el) => !acceptIsImageOnly(el.getAttribute('accept')));
|
|
101
|
+
const target = (nonImage.length ? nonImage[nonImage.length - 1] : inputs[inputs.length - 1]) ?? null;
|
|
102
|
+
if (target) {
|
|
103
|
+
target.setAttribute('data-oracle-upload-target', 'true');
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
})()`,
|
|
108
|
+
returnByValue: true,
|
|
109
|
+
});
|
|
110
|
+
const marked = Boolean(markResult?.result?.value);
|
|
111
|
+
if (!marked) {
|
|
112
|
+
await logDomFailure(runtime, logger, 'file-input-missing');
|
|
22
113
|
throw new Error('Unable to locate ChatGPT file attachment input.');
|
|
23
114
|
}
|
|
24
|
-
await dom.
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
115
|
+
const documentNode = await dom.getDocument();
|
|
116
|
+
const resultNode = await dom.querySelector({ nodeId: documentNode.root.nodeId, selector: 'input[type="file"][data-oracle-upload-target="true"]' });
|
|
117
|
+
if (!resultNode?.nodeId) {
|
|
118
|
+
await logDomFailure(runtime, logger, 'file-input-missing');
|
|
119
|
+
throw new Error('Unable to locate ChatGPT file attachment input.');
|
|
120
|
+
}
|
|
121
|
+
const resolvedNodeId = resultNode.nodeId;
|
|
122
|
+
const dispatchEvents = FILE_INPUT_SELECTORS
|
|
123
|
+
.map((selector) => `
|
|
124
|
+
(() => {
|
|
125
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
126
|
+
if (el instanceof HTMLInputElement) {
|
|
127
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
128
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
`)
|
|
132
|
+
.join('\\n');
|
|
133
|
+
const tryFileInput = async () => {
|
|
134
|
+
await dom.setFileInputFiles({ nodeId: resolvedNodeId, files: [attachment.path] });
|
|
135
|
+
await runtime.evaluate({ expression: `(function(){${dispatchEvents} return true;})()`, returnByValue: true });
|
|
136
|
+
};
|
|
137
|
+
await tryFileInput();
|
|
138
|
+
// Snapshot the attachment state immediately after setting files so we can detect silent failures.
|
|
139
|
+
const snapshotExpr = `(() => {
|
|
140
|
+
const chips = Array.from(document.querySelectorAll('[data-testid*="attachment"],[data-testid*="chip"],[data-testid*="upload"],[aria-label="Remove file"]'))
|
|
141
|
+
.map((node) => (node?.textContent || '').trim())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]')).map((el) => ({
|
|
144
|
+
files: Array.from(el.files || []).map((f) => f?.name ?? ''),
|
|
145
|
+
}));
|
|
146
|
+
return { chips, inputs };
|
|
147
|
+
})()`;
|
|
148
|
+
const snapshot = await runtime
|
|
149
|
+
.evaluate({ expression: snapshotExpr, returnByValue: true })
|
|
150
|
+
.then((res) => res?.result?.value)
|
|
151
|
+
.catch(() => undefined);
|
|
152
|
+
if (snapshot) {
|
|
153
|
+
logger?.(`Attachment snapshot after setFileInputFiles: chips=${JSON.stringify(snapshot.chips || [])} inputs=${JSON.stringify(snapshot.inputs || [])}`);
|
|
154
|
+
}
|
|
155
|
+
const inputHasFile = snapshot?.inputs?.some((entry) => (entry.files || []).some((name) => name?.toLowerCase?.().includes(expectedName.toLowerCase()))) ?? false;
|
|
156
|
+
if (await waitForAttachmentAnchored(runtime, expectedName, 20_000)) {
|
|
157
|
+
await waitForAttachmentVisible(runtime, expectedName, 20_000, logger);
|
|
158
|
+
logger(inputHasFile ? 'Attachment queued (file input, confirmed present)' : 'Attachment queued (file input)');
|
|
159
|
+
return;
|
|
30
160
|
}
|
|
31
|
-
await
|
|
32
|
-
|
|
161
|
+
await logDomFailure(runtime, logger, 'file-upload-missing');
|
|
162
|
+
throw new Error('Attachment did not register with the ChatGPT composer in time.');
|
|
33
163
|
}
|
|
34
|
-
export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
164
|
+
export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNames = [], logger) {
|
|
35
165
|
const deadline = Date.now() + timeoutMs;
|
|
166
|
+
const expectedNormalized = expectedNames.map((name) => name.toLowerCase());
|
|
36
167
|
const expression = `(() => {
|
|
37
168
|
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
38
169
|
let button = null;
|
|
@@ -58,24 +189,40 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
|
58
189
|
return text.includes('upload') || text.includes('processing') || text.includes('uploading');
|
|
59
190
|
});
|
|
60
191
|
});
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
192
|
+
const attachmentSelectors = ['[data-testid*="chip"]', '[data-testid*="attachment"]', '[data-testid*="upload"]'];
|
|
193
|
+
const attachedNames = [];
|
|
194
|
+
for (const selector of attachmentSelectors) {
|
|
195
|
+
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
196
|
+
const text = node?.textContent?.toLowerCase?.();
|
|
197
|
+
if (text) attachedNames.push(text);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const input of Array.from(document.querySelectorAll('input[type="file"]'))) {
|
|
201
|
+
if (!(input instanceof HTMLInputElement) || !input.files?.length) continue;
|
|
202
|
+
for (const file of Array.from(input.files)) {
|
|
203
|
+
if (file?.name) attachedNames.push(file.name.toLowerCase());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const cardTexts = Array.from(document.querySelectorAll('[aria-label="Remove file"]')).map((btn) =>
|
|
207
|
+
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
67
208
|
);
|
|
68
|
-
|
|
209
|
+
attachedNames.push(...cardTexts.filter(Boolean));
|
|
210
|
+
const filesAttached = attachedNames.length > 0;
|
|
211
|
+
return { state: button ? (disabled ? 'disabled' : 'ready') : 'missing', uploading, filesAttached, attachedNames };
|
|
69
212
|
})()`;
|
|
70
213
|
while (Date.now() < deadline) {
|
|
71
214
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
72
215
|
const value = result?.value;
|
|
73
216
|
if (value && !value.uploading) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
217
|
+
const attached = new Set((value.attachedNames ?? []).map((name) => name.toLowerCase()));
|
|
218
|
+
const missing = expectedNormalized.filter((name) => !attached.has(name));
|
|
219
|
+
if (missing.length === 0) {
|
|
220
|
+
if (value.state === 'ready') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (value.state === 'missing' && value.filesAttached) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
79
226
|
}
|
|
80
227
|
}
|
|
81
228
|
await delay(250);
|
|
@@ -85,15 +232,76 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, logger) {
|
|
|
85
232
|
throw new Error('Attachments did not finish uploading before timeout.');
|
|
86
233
|
}
|
|
87
234
|
export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs, logger) {
|
|
88
|
-
|
|
235
|
+
// Attachments can take a few seconds to render in the composer (headless/remote Chrome is slower),
|
|
236
|
+
// so respect the caller-provided timeout instead of capping at 2s.
|
|
237
|
+
const deadline = Date.now() + timeoutMs;
|
|
89
238
|
const expression = `(() => {
|
|
90
239
|
const expected = ${JSON.stringify(expectedName)};
|
|
240
|
+
const normalized = expected.toLowerCase();
|
|
241
|
+
const matchNode = (node) => {
|
|
242
|
+
if (!node) return false;
|
|
243
|
+
const text = (node.textContent || '').toLowerCase();
|
|
244
|
+
const aria = node.getAttribute?.('aria-label')?.toLowerCase?.() ?? '';
|
|
245
|
+
const title = node.getAttribute?.('title')?.toLowerCase?.() ?? '';
|
|
246
|
+
const testId = node.getAttribute?.('data-testid')?.toLowerCase?.() ?? '';
|
|
247
|
+
const alt = node.getAttribute?.('alt')?.toLowerCase?.() ?? '';
|
|
248
|
+
return [text, aria, title, testId, alt].some((value) => value.includes(normalized));
|
|
249
|
+
};
|
|
250
|
+
|
|
91
251
|
const turns = Array.from(document.querySelectorAll('article[data-testid^="conversation-turn"]'));
|
|
92
252
|
const userTurns = turns.filter((node) => node.querySelector('[data-message-author-role="user"]'));
|
|
93
253
|
const lastUser = userTurns[userTurns.length - 1];
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
254
|
+
if (lastUser) {
|
|
255
|
+
const turnMatch = Array.from(lastUser.querySelectorAll('*')).some(matchNode);
|
|
256
|
+
if (turnMatch) return { found: true, userTurns: userTurns.length, source: 'turn' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const composerSelectors = [
|
|
260
|
+
'[data-testid*="composer"]',
|
|
261
|
+
'form textarea',
|
|
262
|
+
'form [data-testid*="attachment"]',
|
|
263
|
+
'[data-testid*="upload"]',
|
|
264
|
+
'[data-testid*="chip"]',
|
|
265
|
+
'form',
|
|
266
|
+
'button',
|
|
267
|
+
'label'
|
|
268
|
+
];
|
|
269
|
+
const composerMatch = composerSelectors.some((selector) =>
|
|
270
|
+
Array.from(document.querySelectorAll(selector)).some(matchNode),
|
|
271
|
+
);
|
|
272
|
+
if (composerMatch) {
|
|
273
|
+
return { found: true, userTurns: userTurns.length, source: 'composer' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const attachmentSelectors = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]'];
|
|
277
|
+
const attachmentMatch = attachmentSelectors.some((selector) =>
|
|
278
|
+
Array.from(document.querySelectorAll(selector)).some(matchNode),
|
|
279
|
+
);
|
|
280
|
+
if (attachmentMatch) {
|
|
281
|
+
return { found: true, userTurns: userTurns.length, source: 'attachments' };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const cardTexts = Array.from(document.querySelectorAll('[aria-label="Remove file"]')).map((btn) =>
|
|
285
|
+
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
286
|
+
);
|
|
287
|
+
if (cardTexts.some((text) => text.includes(normalized))) {
|
|
288
|
+
return { found: true, userTurns: userTurns.length, source: 'attachment-cards' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const attrMatch = Array.from(document.querySelectorAll('[aria-label], [title], [data-testid]')).some(matchNode);
|
|
292
|
+
if (attrMatch) {
|
|
293
|
+
return { found: true, userTurns: userTurns.length, source: 'attrs' };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const bodyMatch = (document.body?.innerText || '').toLowerCase().includes(normalized);
|
|
297
|
+
if (bodyMatch) {
|
|
298
|
+
return { found: true, userTurns: userTurns.length, source: 'body' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const inputHit = Array.from(document.querySelectorAll('input[type="file"]')).some((el) =>
|
|
302
|
+
Array.from(el.files || []).some((file) => file?.name?.toLowerCase?.().includes(normalized)),
|
|
303
|
+
);
|
|
304
|
+
return { found: inputHit, userTurns: userTurns.length, source: inputHit ? 'input' : undefined };
|
|
97
305
|
})()`;
|
|
98
306
|
while (Date.now() < deadline) {
|
|
99
307
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
@@ -107,31 +315,41 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
107
315
|
await logDomFailure(Runtime, logger ?? (() => { }), 'attachment-visible');
|
|
108
316
|
throw new Error('Attachment did not appear in ChatGPT composer.');
|
|
109
317
|
}
|
|
110
|
-
async function
|
|
318
|
+
async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
111
319
|
const deadline = Date.now() + timeoutMs;
|
|
112
320
|
const expression = `(() => {
|
|
113
|
-
const
|
|
321
|
+
const normalized = ${JSON.stringify(expectedName.toLowerCase())};
|
|
322
|
+
const selectors = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]'];
|
|
114
323
|
for (const selector of selectors) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
const names = Array.from(input.files ?? []).map((file) => file?.name ?? '');
|
|
121
|
-
if (names.some((name) => name === ${JSON.stringify(expectedName)})) {
|
|
122
|
-
return { matched: true, names };
|
|
324
|
+
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
325
|
+
const text = (node?.textContent || '').toLowerCase();
|
|
326
|
+
if (text.includes(normalized)) {
|
|
327
|
+
return { found: true, text };
|
|
123
328
|
}
|
|
124
329
|
}
|
|
125
330
|
}
|
|
126
|
-
|
|
331
|
+
const cards = Array.from(document.querySelectorAll('[aria-label="Remove file"]')).map((btn) =>
|
|
332
|
+
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
333
|
+
);
|
|
334
|
+
if (cards.some((text) => text.includes(normalized))) {
|
|
335
|
+
return { found: true, text: cards.find((t) => t.includes(normalized)) };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// As a last resort, treat file inputs that hold the target name as anchored. Some UIs delay chip rendering.
|
|
339
|
+
const inputHit = Array.from(document.querySelectorAll('input[type="file"]')).some((el) =>
|
|
340
|
+
Array.from(el.files || []).some((file) => file?.name?.toLowerCase?.().includes(normalized)),
|
|
341
|
+
);
|
|
342
|
+
if (inputHit) {
|
|
343
|
+
return { found: true, text: 'input-only' };
|
|
344
|
+
}
|
|
345
|
+
return { found: false };
|
|
127
346
|
})()`;
|
|
128
347
|
while (Date.now() < deadline) {
|
|
129
348
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
130
|
-
|
|
131
|
-
if (matched) {
|
|
349
|
+
if (result?.value?.found) {
|
|
132
350
|
return true;
|
|
133
351
|
}
|
|
134
|
-
await delay(
|
|
352
|
+
await delay(200);
|
|
135
353
|
}
|
|
136
354
|
return false;
|
|
137
355
|
}
|
|
@@ -11,6 +11,7 @@ const ENTER_KEY_EVENT = {
|
|
|
11
11
|
const ENTER_KEY_TEXT = '\r';
|
|
12
12
|
export async function submitPrompt(deps, prompt, logger) {
|
|
13
13
|
const { runtime, input } = deps;
|
|
14
|
+
await waitForDomReady(runtime, logger);
|
|
14
15
|
const encodedPrompt = JSON.stringify(prompt);
|
|
15
16
|
const focusResult = await runtime.evaluate({
|
|
16
17
|
expression: `(() => {
|
|
@@ -89,7 +90,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
89
90
|
})()`,
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
|
-
const clicked = await attemptSendButton(runtime);
|
|
93
|
+
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames);
|
|
93
94
|
if (!clicked) {
|
|
94
95
|
await input.dispatchKeyEvent({
|
|
95
96
|
type: 'keyDown',
|
|
@@ -109,7 +110,27 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
109
110
|
await verifyPromptCommitted(runtime, prompt, 30_000, logger);
|
|
110
111
|
await clickAnswerNowIfPresent(runtime, logger);
|
|
111
112
|
}
|
|
112
|
-
async function
|
|
113
|
+
async function waitForDomReady(Runtime, logger) {
|
|
114
|
+
const deadline = Date.now() + 10_000;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
const { result } = await Runtime.evaluate({
|
|
117
|
+
expression: `(() => {
|
|
118
|
+
const ready = document.readyState === 'complete';
|
|
119
|
+
const composer = document.querySelector('[data-testid*="composer"]') || document.querySelector('form');
|
|
120
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
121
|
+
return { ready, composer: Boolean(composer), fileInput: Boolean(fileInput) };
|
|
122
|
+
})()`,
|
|
123
|
+
returnByValue: true,
|
|
124
|
+
});
|
|
125
|
+
const value = result?.value;
|
|
126
|
+
if (value?.ready && value.composer) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await delay(150);
|
|
130
|
+
}
|
|
131
|
+
logger?.('Page did not reach ready/composer state within 10s; continuing cautiously.');
|
|
132
|
+
}
|
|
133
|
+
async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
113
134
|
const script = `(() => {
|
|
114
135
|
${buildClickDispatcher()}
|
|
115
136
|
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
@@ -133,8 +154,31 @@ async function attemptSendButton(Runtime) {
|
|
|
133
154
|
dispatchClickSequence(button);
|
|
134
155
|
return 'clicked';
|
|
135
156
|
})()`;
|
|
136
|
-
const deadline = Date.now() +
|
|
157
|
+
const deadline = Date.now() + 8_000;
|
|
137
158
|
while (Date.now() < deadline) {
|
|
159
|
+
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
160
|
+
if (needAttachment) {
|
|
161
|
+
const ready = await Runtime.evaluate({
|
|
162
|
+
expression: `(() => {
|
|
163
|
+
const names = ${JSON.stringify(attachmentNames.map((n) => n.toLowerCase()))};
|
|
164
|
+
const match = (n, name) => (n?.textContent || '').toLowerCase().includes(name);
|
|
165
|
+
const chipsReady = names.every((name) =>
|
|
166
|
+
Array.from(document.querySelectorAll('[data-testid*="chip"],[data-testid*="attachment"],a,div,span')).some((node) => match(node, name)),
|
|
167
|
+
);
|
|
168
|
+
const inputsReady = names.every((name) =>
|
|
169
|
+
Array.from(document.querySelectorAll('input[type="file"]')).some((el) =>
|
|
170
|
+
Array.from(el.files || []).some((f) => f?.name?.toLowerCase?.().includes(name)),
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
return chipsReady || inputsReady;
|
|
174
|
+
})()`,
|
|
175
|
+
returnByValue: true,
|
|
176
|
+
});
|
|
177
|
+
if (!ready?.result?.value) {
|
|
178
|
+
await delay(150);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
138
182
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
139
183
|
if (result.value === 'clicked') {
|
|
140
184
|
return true;
|
|
@@ -187,24 +231,37 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger) {
|
|
|
187
231
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
188
232
|
const normalize = (value) => value?.toLowerCase?.().replace(/\\s+/g, ' ').trim() ?? '';
|
|
189
233
|
const normalizedPrompt = normalize(${encodedPrompt});
|
|
234
|
+
const normalizedPromptPrefix = normalizedPrompt.slice(0, 120);
|
|
190
235
|
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
191
236
|
const articles = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
192
|
-
const
|
|
237
|
+
const normalizedTurns = articles.map((node) => normalize(node?.innerText));
|
|
238
|
+
const userMatched = normalizedTurns.some((text) => text.includes(normalizedPrompt));
|
|
239
|
+
const prefixMatched =
|
|
240
|
+
normalizedPromptPrefix.length > 30 &&
|
|
241
|
+
normalizedTurns.some((text) => text.includes(normalizedPromptPrefix));
|
|
242
|
+
const lastTurn = normalizedTurns[normalizedTurns.length - 1] ?? '';
|
|
193
243
|
return {
|
|
194
244
|
userMatched,
|
|
245
|
+
prefixMatched,
|
|
195
246
|
fallbackValue: fallback?.value ?? '',
|
|
196
247
|
editorValue: editor?.innerText ?? '',
|
|
248
|
+
lastTurn,
|
|
249
|
+
turnsCount: normalizedTurns.length,
|
|
197
250
|
};
|
|
198
251
|
})()`;
|
|
199
252
|
while (Date.now() < deadline) {
|
|
200
253
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
201
254
|
const info = result.value;
|
|
202
|
-
if (info?.userMatched) {
|
|
255
|
+
if (info?.userMatched || info?.prefixMatched) {
|
|
203
256
|
return;
|
|
204
257
|
}
|
|
205
258
|
await delay(100);
|
|
206
259
|
}
|
|
207
260
|
if (logger) {
|
|
261
|
+
logger(`Prompt commit check failed; latest state: ${await Runtime.evaluate({
|
|
262
|
+
expression: script,
|
|
263
|
+
returnByValue: true,
|
|
264
|
+
}).then((res) => JSON.stringify(res?.result?.value)).catch(() => 'unavailable')}`);
|
|
208
265
|
await logDomFailure(Runtime, logger, 'prompt-commit');
|
|
209
266
|
}
|
|
210
267
|
throw new Error('Prompt did not appear in conversation before timeout (send may have failed)');
|
|
@@ -32,7 +32,7 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
32
32
|
logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
|
|
33
33
|
return Object.assign(launcher, { host: connectHost ?? '127.0.0.1' });
|
|
34
34
|
}
|
|
35
|
-
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger) {
|
|
35
|
+
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
|
|
36
36
|
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
37
37
|
let handling;
|
|
38
38
|
const handleSignal = (signal) => {
|
|
@@ -40,14 +40,23 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
handling = true;
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const inFlight = opts?.isInFlight?.() ?? false;
|
|
44
|
+
const leaveRunning = keepBrowser || inFlight;
|
|
45
|
+
if (leaveRunning) {
|
|
46
|
+
logger(`Received ${signal}; leaving Chrome running${inFlight ? ' (assistant response pending)' : ''}`);
|
|
45
47
|
}
|
|
46
48
|
else {
|
|
47
49
|
logger(`Received ${signal}; terminating Chrome process`);
|
|
48
50
|
}
|
|
49
51
|
void (async () => {
|
|
50
|
-
if (
|
|
52
|
+
if (leaveRunning) {
|
|
53
|
+
// Ensure reattach hints are written before we exit.
|
|
54
|
+
await opts?.emitRuntimeHint?.().catch(() => undefined);
|
|
55
|
+
if (inFlight) {
|
|
56
|
+
logger('Session still in flight; reattach with "oracle session <slug>" to continue.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
51
60
|
try {
|
|
52
61
|
await chrome.kill();
|
|
53
62
|
}
|
|
@@ -29,6 +29,9 @@ export const FILE_INPUT_SELECTORS = [
|
|
|
29
29
|
'input[type="file"][multiple]:not([accept])',
|
|
30
30
|
'input[type="file"][multiple]',
|
|
31
31
|
'input[type="file"]:not([accept])',
|
|
32
|
+
'form input[type="file"][accept]',
|
|
33
|
+
'input[type="file"][accept]',
|
|
34
|
+
'input[type="file"]',
|
|
32
35
|
'input[type="file"][data-testid*="file"]',
|
|
33
36
|
];
|
|
34
37
|
// Legacy single selectors kept for compatibility with older call-sites
|
|
@@ -48,11 +51,11 @@ export const UPLOAD_STATUS_SELECTORS = [
|
|
|
48
51
|
];
|
|
49
52
|
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
50
53
|
export const SEND_BUTTON_SELECTORS = [
|
|
51
|
-
'[data-testid="send-button"]',
|
|
52
|
-
'button[data-testid
|
|
53
|
-
'button[
|
|
54
|
-
'button[aria-label*="Send"]',
|
|
54
|
+
'button[data-testid="send-button"]',
|
|
55
|
+
'button[data-testid*="composer-send"]',
|
|
56
|
+
'form button[type="submit"]',
|
|
55
57
|
'button[type="submit"][data-testid*="send"]',
|
|
58
|
+
'button[aria-label*="Send"]',
|
|
56
59
|
];
|
|
57
60
|
export const SEND_BUTTON_SELECTOR = SEND_BUTTON_SELECTORS[0];
|
|
58
61
|
export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"]';
|
|
@@ -98,7 +98,10 @@ export async function runBrowserMode(options) {
|
|
|
98
98
|
const chromeHost = chrome.host ?? '127.0.0.1';
|
|
99
99
|
let removeTerminationHooks = null;
|
|
100
100
|
try {
|
|
101
|
-
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger
|
|
101
|
+
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
102
|
+
isInFlight: () => runStatus !== 'complete',
|
|
103
|
+
emitRuntimeHint,
|
|
104
|
+
});
|
|
102
105
|
}
|
|
103
106
|
catch {
|
|
104
107
|
// ignore failure; cleanup still happens below
|
|
@@ -251,6 +254,7 @@ export async function runBrowserMode(options) {
|
|
|
251
254
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
252
255
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
253
256
|
}
|
|
257
|
+
const attachmentNames = attachments.map((a) => path.basename(a.path));
|
|
254
258
|
if (attachments.length > 0) {
|
|
255
259
|
if (!DOM) {
|
|
256
260
|
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
@@ -260,10 +264,10 @@ export async function runBrowserMode(options) {
|
|
|
260
264
|
await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
261
265
|
}
|
|
262
266
|
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
263
|
-
await raceWithDisconnect(waitForAttachmentCompletion(Runtime, waitBudget, logger));
|
|
267
|
+
await raceWithDisconnect(waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger));
|
|
264
268
|
logger('All attachments uploaded');
|
|
265
269
|
}
|
|
266
|
-
await raceWithDisconnect(submitPrompt({ runtime: Runtime, input: Input }, promptText, logger));
|
|
270
|
+
await raceWithDisconnect(submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, promptText, logger));
|
|
267
271
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
268
272
|
const answer = await raceWithDisconnect(waitForAssistantResponse(Runtime, config.timeoutMs, logger));
|
|
269
273
|
answerText = answer.text;
|
|
@@ -632,6 +636,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
632
636
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
633
637
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
634
638
|
}
|
|
639
|
+
const attachmentNames = attachments.map((a) => path.basename(a.path));
|
|
635
640
|
if (attachments.length > 0) {
|
|
636
641
|
if (!DOM) {
|
|
637
642
|
throw new Error('Chrome DOM domain unavailable while uploading attachments.');
|
|
@@ -642,10 +647,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
642
647
|
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
643
648
|
}
|
|
644
649
|
const waitBudget = Math.max(config.inputTimeoutMs ?? 30_000, 30_000);
|
|
645
|
-
await waitForAttachmentCompletion(Runtime, waitBudget, logger);
|
|
650
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
646
651
|
logger('All attachments uploaded');
|
|
647
652
|
}
|
|
648
|
-
await submitPrompt({ runtime: Runtime, input: Input }, promptText, logger);
|
|
653
|
+
await submitPrompt({ runtime: Runtime, input: Input, attachmentNames }, promptText, logger);
|
|
649
654
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
650
655
|
const answer = await waitForAssistantResponse(Runtime, config.timeoutMs, logger);
|
|
651
656
|
answerText = answer.text;
|
|
@@ -101,7 +101,7 @@ export function toTransportError(error, model) {
|
|
|
101
101
|
messageText.includes('does not exist') ||
|
|
102
102
|
messageText.includes('unknown model') ||
|
|
103
103
|
messageText.includes('model_not_found'))) {
|
|
104
|
-
return new OracleTransportError('model-unavailable', 'gpt-5.1-pro is not yet available on this API base.
|
|
104
|
+
return new OracleTransportError('model-unavailable', 'gpt-5.1-pro is not yet available on this API base. Using gpt-5-pro until OpenAI enables it. // TODO: Remove once gpt-5.1-pro is available', apiError);
|
|
105
105
|
}
|
|
106
106
|
if (apiError.status === 404 || apiError.status === 405) {
|
|
107
107
|
return new OracleTransportError('unsupported-endpoint', 'HTTP 404/405 from the Responses API; this base URL or gateway likely does not expose /v1/responses. Set OPENAI_BASE_URL to api.openai.com/v1, update your Azure API version/deployment, or use the browser engine.', apiError);
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -186,14 +186,17 @@ export async function runOracle(options, deps = {}) {
|
|
|
186
186
|
: DEFAULT_TIMEOUT_NON_PRO_MS / 1000
|
|
187
187
|
: options.timeoutSeconds;
|
|
188
188
|
const timeoutMs = timeoutSeconds * 1000;
|
|
189
|
+
const apiModelFromConfig = modelConfig.apiModel ?? modelConfig.model;
|
|
190
|
+
const modelDowngraded = apiModelFromConfig === 'gpt-5.1-pro';
|
|
191
|
+
const resolvedApiModelId = modelDowngraded ? 'gpt-5-pro' : apiModelFromConfig;
|
|
189
192
|
// Track the concrete model id we dispatch to (especially for Gemini preview aliases)
|
|
190
193
|
const effectiveModelId = options.effectiveModelId ??
|
|
191
194
|
(options.model.startsWith('gemini')
|
|
192
195
|
? resolveGeminiModelId(options.model)
|
|
193
|
-
:
|
|
196
|
+
: resolvedApiModelId);
|
|
194
197
|
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
195
198
|
const requestBody = buildRequestBody({
|
|
196
|
-
modelConfig,
|
|
199
|
+
modelConfig: { ...modelConfig, apiModel: resolvedApiModelId },
|
|
197
200
|
systemPrompt,
|
|
198
201
|
userPrompt: promptWithFiles,
|
|
199
202
|
searchEnabled,
|
|
@@ -219,6 +222,9 @@ export async function runOracle(options, deps = {}) {
|
|
|
219
222
|
if (baseUrl) {
|
|
220
223
|
log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
|
|
221
224
|
}
|
|
225
|
+
if (modelDowngraded) {
|
|
226
|
+
log(dim('gpt-5.1-pro is not yet available via API; sending request with gpt-5-pro instead.'));
|
|
227
|
+
}
|
|
222
228
|
if (options.background && !supportsBackground) {
|
|
223
229
|
log(dim('Background runs are not supported for this model; streaming in foreground instead.'));
|
|
224
230
|
}
|
|
File without changes
|
|
File without changes
|
package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier
CHANGED
|
File without changes
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.1 Pro, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
@@ -8,6 +8,26 @@
|
|
|
8
8
|
"oracle": "dist/bin/oracle-cli.js",
|
|
9
9
|
"oracle-mcp": "dist/bin/oracle-mcp.js"
|
|
10
10
|
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"docs:list": "tsx scripts/docs-list.ts",
|
|
13
|
+
"build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
|
|
14
|
+
"build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const src=path.join('vendor','oracle-notifier'); const dest=path.join('dist','vendor','oracle-notifier'); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){ fs.cpSync(src,dest,{recursive:true,force:true}); }\"",
|
|
15
|
+
"start": "pnpm run build && node ./dist/scripts/run-cli.js",
|
|
16
|
+
"oracle": "pnpm start",
|
|
17
|
+
"check": "pnpm run typecheck",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"lint": "pnpm run typecheck && biome lint .",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
|
|
22
|
+
"test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
|
|
23
|
+
"test:mcp:mcporter": "npx -y mcporter list oracle-local --schema --config config/mcporter.json && npx -y mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
|
|
24
|
+
"test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
|
|
25
|
+
"test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
|
|
26
|
+
"test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
28
|
+
"prepare": "pnpm run build",
|
|
29
|
+
"mcp": "pnpm run build && node ./dist/bin/oracle-mcp.js"
|
|
30
|
+
},
|
|
11
31
|
"files": [
|
|
12
32
|
"dist/**/*",
|
|
13
33
|
"assets-oracle-icon.png",
|
|
@@ -30,11 +50,7 @@
|
|
|
30
50
|
"name": "node",
|
|
31
51
|
"version": ">=20"
|
|
32
52
|
}
|
|
33
|
-
]
|
|
34
|
-
"packageManager": {
|
|
35
|
-
"name": "pnpm",
|
|
36
|
-
"version": ">=8"
|
|
37
|
-
}
|
|
53
|
+
]
|
|
38
54
|
},
|
|
39
55
|
"keywords": [],
|
|
40
56
|
"author": "",
|
|
@@ -87,23 +103,19 @@
|
|
|
87
103
|
"optionalDependencies": {
|
|
88
104
|
"win-dpapi": "npm:@primno/dpapi@2.0.1"
|
|
89
105
|
},
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"test:coverage": "vitest run --coverage",
|
|
107
|
-
"mcp": "pnpm run build && node ./dist/bin/oracle-mcp.js"
|
|
108
|
-
}
|
|
109
|
-
}
|
|
106
|
+
"pnpm": {
|
|
107
|
+
"overrides": {
|
|
108
|
+
"zod": "4.1.13",
|
|
109
|
+
"win-dpapi": "npm:@primno/dpapi@2.0.1",
|
|
110
|
+
"zod-to-json-schema": "3.25.0",
|
|
111
|
+
"devtools-protocol": "0.0.1551306"
|
|
112
|
+
},
|
|
113
|
+
"onlyBuiltDependencies": [
|
|
114
|
+
"@cdktf/node-pty-prebuilt-multiarch",
|
|
115
|
+
"keytar",
|
|
116
|
+
"sqlite3",
|
|
117
|
+
"win-dpapi"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
|
|
121
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Oracle Notifier helper (macOS, arm64)
|
|
2
|
+
|
|
3
|
+
Builds a tiny signed helper app for macOS notifications with the Oracle icon.
|
|
4
|
+
|
|
5
|
+
## Build
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd vendor/oracle-notifier
|
|
9
|
+
# Optional: notarize by setting App Store Connect key credentials
|
|
10
|
+
export APP_STORE_CONNECT_API_KEY_P8="$(cat AuthKey_XXXXXX.p8)" # with literal newlines or \n escaped
|
|
11
|
+
export APP_STORE_CONNECT_KEY_ID=XXXXXX
|
|
12
|
+
export APP_STORE_CONNECT_ISSUER_ID=YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
|
|
13
|
+
./build-notifier.sh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Requires Xcode command line tools (swiftc) and a macOS Developer ID certificate. Without a valid cert, the build fails (no ad-hoc fallback).
|
|
17
|
+
- If `APP_STORE_CONNECT_*` vars are set, the script notarizes and staples the ticket.
|
|
18
|
+
- Output: `OracleNotifier.app` (arm64 only), bundled with `OracleIcon.icns`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
The CLI prefers this helper on macOS; if it fails or is missing, it falls back to toasted-notifier/terminal-notifier.
|
|
22
|
+
|
|
23
|
+
## Permissions
|
|
24
|
+
After first run, allow notifications for “Oracle Notifier” in System Settings → Notifications.
|
|
File without changes
|
package/dist/.DS_Store
DELETED
|
Binary file
|