@steipete/oracle 0.17.1 → 0.17.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 +1 -1
- package/dist/docs-site/browser-mode.html +7 -5
- package/dist/docs-site/cli-reference.html +1 -1
- package/dist/docs-site/configuration.html +1 -1
- package/dist/docs-site/manual-tests.html +2 -1
- package/dist/docs-site/mcp.html +2 -2
- package/dist/docs-site/windows-work.html +1 -1
- package/dist/docs-site/windows.html +1 -1
- package/dist/scripts/git-policy.js +0 -8
- package/dist/scripts/runner.js +1 -5
- package/dist/src/browser/actions/modelSelection.js +164 -5
- package/dist/src/browser/actions/thinkingTime.js +181 -11
- package/dist/src/cli/browserConfig.js +5 -2
- package/dist/src/cli/options.js +1 -1
- package/dist/src/oracle/thinkingTime.js +6 -0
- package/dist/src/sessionManager.js +90 -9
- package/package.json +10 -10
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
|
@@ -31,7 +31,11 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
31
31
|
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
32
32
|
const targetModelKind = inferThinkingTargetModelKind(desiredModel);
|
|
33
33
|
const observedModelKind = result && "modelKind" in result ? result.modelKind : null;
|
|
34
|
-
|
|
34
|
+
// Pro is expensive and rate-limited, so a Pro request must never degrade quietly
|
|
35
|
+
// into a cheaper tier. Requesting it explicitly (level "pro") fails closed on its
|
|
36
|
+
// own, independently of the legacy Pro-model + "extended" combination.
|
|
37
|
+
const strictProEffort = level === "pro" ||
|
|
38
|
+
((targetModelKind === "pro" || observedModelKind === "pro") && level === "extended");
|
|
35
39
|
switch (result?.status) {
|
|
36
40
|
case "already-selected":
|
|
37
41
|
logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} (already selected)`));
|
|
@@ -53,18 +57,24 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
53
57
|
: "";
|
|
54
58
|
const message = `Thinking time: ${result.status.replaceAll("-", " ")}${kindHint} (requested ${capitalizedLevel})`;
|
|
55
59
|
if (strictProEffort) {
|
|
56
|
-
|
|
60
|
+
const target = level === "pro" ? "Pro" : "Pro Extended";
|
|
61
|
+
throw new Error(`${message}; refusing to submit without confirmed ${target}.`);
|
|
57
62
|
}
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
63
|
+
// "selection-unverified" is the one status here that already dispatched a
|
|
64
|
+
// click, so the effort may or may not have moved. Every other status left
|
|
65
|
+
// the tab on whatever effort it had — which is not necessarily the default.
|
|
66
|
+
const outcome = result.status === "selection-unverified"
|
|
67
|
+
? "the effort in ChatGPT is unconfirmed"
|
|
68
|
+
: "keeping the effort already selected in ChatGPT";
|
|
69
|
+
logger(formatBrowserThinkingLog(`${message}; ${outcome}.`));
|
|
61
70
|
return;
|
|
62
71
|
}
|
|
63
72
|
default: {
|
|
64
73
|
await logDomFailure(Runtime, logger, "thinking-time-unknown");
|
|
65
74
|
logPickerDiagnostic(result, logger);
|
|
66
75
|
if (strictProEffort) {
|
|
67
|
-
|
|
76
|
+
const target = level === "pro" ? "Pro" : "Pro Extended";
|
|
77
|
+
throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed ${target}.`);
|
|
68
78
|
}
|
|
69
79
|
logger(formatBrowserThinkingLog(`unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`));
|
|
70
80
|
return;
|
|
@@ -74,7 +84,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
74
84
|
/**
|
|
75
85
|
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
76
86
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
77
|
-
* @param level - The thinking time intensity: 'light', 'standard', 'extended', 'extra-high', or 'heavy'
|
|
87
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', 'extra-high', 'pro', or 'heavy'
|
|
78
88
|
*/
|
|
79
89
|
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desiredModel) {
|
|
80
90
|
try {
|
|
@@ -145,7 +155,12 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
145
155
|
'extra-high': ['extra high', 'sehr hoch', '极高'],
|
|
146
156
|
heavy: ['heavy', 'schwer', '重度', '加重'],
|
|
147
157
|
};
|
|
148
|
-
|
|
158
|
+
// Pro is a tier you can request, but it is also a MODEL name, so it must never
|
|
159
|
+
// be used to decide whether a control or a menu is an effort owner: a model pill
|
|
160
|
+
// reading "Pro" would be claimed as the effort pill, and a model menu listing
|
|
161
|
+
// "Instant"/"Pro" would look like a tier list. Keep it to target matching only.
|
|
162
|
+
const TARGET_LEVEL_TOKENS = { ...LEVEL_TOKENS, pro: ['pro'] };
|
|
163
|
+
const targetTokens = TARGET_LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
|
|
149
164
|
|
|
150
165
|
const INITIAL_WAIT_MS = 150;
|
|
151
166
|
const STEP_WAIT_MS = 200;
|
|
@@ -372,7 +387,20 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
372
387
|
diagnostic: collectPickerDiagnostic(),
|
|
373
388
|
});
|
|
374
389
|
const findOptionInMenu = (menu, modelKindOverride = null) => {
|
|
375
|
-
|
|
390
|
+
// Container controls reveal other controls; they are not tiers you can pick.
|
|
391
|
+
// Two shapes exist and both can collide with a tier label: a submenu opener
|
|
392
|
+
// ("ModelGPT-5.6 Pro" would satisfy a Pro request) and a disclosure toggle
|
|
393
|
+
// (German "Erweitert" is literally one of the extended tokens, so the
|
|
394
|
+
// Advanced toggle would satisfy an extended request and be clicked in place
|
|
395
|
+
// of the tier). Detect them structurally rather than by label: a real tier row
|
|
396
|
+
// carries a checked state, while a container carries expansion state.
|
|
397
|
+
const isContainerControl = (node) =>
|
|
398
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' ||
|
|
399
|
+
(node?.getAttribute?.('aria-expanded') !== null &&
|
|
400
|
+
node?.getAttribute?.('aria-checked') === null);
|
|
401
|
+
const items = Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(
|
|
402
|
+
(item) => !isContainerControl(item),
|
|
403
|
+
);
|
|
376
404
|
const modelKind = modelKindOverride || effectiveTargetModelKind();
|
|
377
405
|
if (modelKind === 'pro') {
|
|
378
406
|
// GPT-5.6's unified Intelligence picker exposes Pro as the highest
|
|
@@ -414,7 +442,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
414
442
|
const itemText = normalize(
|
|
415
443
|
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
416
444
|
);
|
|
417
|
-
|
|
445
|
+
// Pro rows are skipped for non-Pro targets so "High" never resolves to
|
|
446
|
+
// "Pro". Two things lift this: an explicit TARGET_LEVEL of 'pro', and a
|
|
447
|
+
// legacy Pro-model menu (modelKind === 'pro'), whose rows are all Pro
|
|
448
|
+
// variants so excluding them would leave nothing to match.
|
|
449
|
+
if (TARGET_LEVEL !== 'pro' && modelKind !== 'pro' && hasToken(itemText, 'pro')) {
|
|
418
450
|
continue;
|
|
419
451
|
}
|
|
420
452
|
if (
|
|
@@ -644,6 +676,119 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
644
676
|
return null;
|
|
645
677
|
};
|
|
646
678
|
|
|
679
|
+
// ---------- Unified Intelligence picker: Advanced -> Effort submenu ----------
|
|
680
|
+
// A newer ChatGPT layout replaces the flat effort rows with a "power" slider
|
|
681
|
+
// (simple view) plus an "Advanced" view holding two submenu openers: Model and
|
|
682
|
+
// Effort. The tier rows only exist inside the Effort submenu, so the flat scan
|
|
683
|
+
// of the top-level menu finds nothing and we must expand and descend.
|
|
684
|
+
const ADVANCED_VIEW_SELECTOR = '[data-testid="composer-model-picker-slider-advanced-view"]';
|
|
685
|
+
const SUBMENU_OPENER_SELECTOR = '[role="menuitem"][aria-haspopup="menu"]';
|
|
686
|
+
const nodeLabel = (node) =>
|
|
687
|
+
normalize((node?.getAttribute?.('aria-label') ?? '') + ' ' + (node?.textContent ?? ''));
|
|
688
|
+
// Row labels in this menu concatenate without separators ("EffortHigh"), so
|
|
689
|
+
// token matching cannot be used here — substring is deliberate, as in
|
|
690
|
+
// countEffortLevels above.
|
|
691
|
+
const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', 'avanzado', 'avancado', 'avance'];
|
|
692
|
+
const EFFORT_WORDS = [
|
|
693
|
+
'effort', 'aufwand', '强度', '努力',
|
|
694
|
+
'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
|
|
695
|
+
];
|
|
696
|
+
const containsAny = (label, words) => words.some((word) => label.includes(word));
|
|
697
|
+
const findAdvancedToggle = (menu) => {
|
|
698
|
+
for (const item of (menu || document).querySelectorAll('[role="menuitem"]')) {
|
|
699
|
+
if (!isVisible(item)) continue;
|
|
700
|
+
if (containsAny(nodeLabel(item), ADVANCED_WORDS)) return item;
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
};
|
|
704
|
+
// Verify the shape rather than trusting the selector: a tier row must never be
|
|
705
|
+
// mistaken for the opener, or hovering it would silently change the effort.
|
|
706
|
+
const isSubmenuOpener = (node) =>
|
|
707
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' &&
|
|
708
|
+
(node?.getAttribute?.('role') ?? '') === 'menuitem';
|
|
709
|
+
// The Effort opener's label is the word "Effort" plus the tier it currently sits
|
|
710
|
+
// on ("EffortHigh"). Positive identification only: the sibling Model opener has
|
|
711
|
+
// the identical shape and can read "ModelGPT-5.6 Pro", so guessing from tier
|
|
712
|
+
// words would pick it and then click model rows as if they were efforts. An
|
|
713
|
+
// unrecognised language yields no opener, and the caller fails instead of
|
|
714
|
+
// gambling on the wrong control.
|
|
715
|
+
const findEffortSubmenuOpener = (menu) => {
|
|
716
|
+
const scope = menu?.querySelector?.(ADVANCED_VIEW_SELECTOR) || menu || document;
|
|
717
|
+
for (const item of scope.querySelectorAll(SUBMENU_OPENER_SELECTOR)) {
|
|
718
|
+
if (!isVisible(item) || !isSubmenuOpener(item)) continue;
|
|
719
|
+
if (containsAny(nodeLabel(item), EFFORT_WORDS)) return item;
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
};
|
|
723
|
+
// countEffortLevels reads aggregate menu text, where one "Extra High" row scores
|
|
724
|
+
// twice (once as "high", once as "extra high"). Count distinct levels across
|
|
725
|
+
// distinct rows instead, so ">= 2" really means two selectable tiers.
|
|
726
|
+
const countDistinctTierRows = (menu) => {
|
|
727
|
+
if (!menu) return 0;
|
|
728
|
+
const matched = new Set();
|
|
729
|
+
for (const row of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
|
|
730
|
+
if (isSubmenuOpener(row)) continue;
|
|
731
|
+
const text = nodeLabel(row);
|
|
732
|
+
for (const [level, tokens] of Object.entries(LEVEL_TOKENS)) {
|
|
733
|
+
if (matchesTokens(text, tokens)) matched.add(level);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return matched.size;
|
|
737
|
+
};
|
|
738
|
+
const resolveSubmenuFor = (opener, parentMenu) => {
|
|
739
|
+
const id = opener?.getAttribute?.('aria-controls');
|
|
740
|
+
if (id) {
|
|
741
|
+
const node = document.getElementById?.(id);
|
|
742
|
+
if (isVisible(node) && countDistinctTierRows(node) >= 2) return node;
|
|
743
|
+
}
|
|
744
|
+
let best = null;
|
|
745
|
+
for (const menu of document.querySelectorAll(MENU_CONTAINER_SELECTOR)) {
|
|
746
|
+
if (menu === parentMenu || menu.contains?.(opener) || !isVisible(menu)) continue;
|
|
747
|
+
const hits = countDistinctTierRows(menu);
|
|
748
|
+
if (hits >= 2 && (!best || hits > best.hits)) best = { menu, hits };
|
|
749
|
+
}
|
|
750
|
+
return best?.menu ?? null;
|
|
751
|
+
};
|
|
752
|
+
const selectEffortFromAdvancedSubmenu = async (parentMenu, modelKindOverride = null) => {
|
|
753
|
+
if (!parentMenu) return null;
|
|
754
|
+
// React can mount the advanced view well after the click under load, so poll
|
|
755
|
+
// for the opener to the same deadline the submenu gets instead of assuming it
|
|
756
|
+
// rendered within one STEP_WAIT_MS. Re-expand if the toggle collapses again.
|
|
757
|
+
let opener = null;
|
|
758
|
+
const openerDeadline = performance.now() + MAX_WAIT_MS;
|
|
759
|
+
while (!opener && performance.now() < openerDeadline) {
|
|
760
|
+
const toggle = findAdvancedToggle(parentMenu);
|
|
761
|
+
if (toggle && toggle.getAttribute?.('aria-expanded') === 'false') {
|
|
762
|
+
dispatchClickSequence(toggle);
|
|
763
|
+
await sleep(STEP_WAIT_MS);
|
|
764
|
+
}
|
|
765
|
+
opener = findEffortSubmenuOpener(parentMenu);
|
|
766
|
+
if (opener) break;
|
|
767
|
+
await sleep(100);
|
|
768
|
+
}
|
|
769
|
+
if (!opener) return null;
|
|
770
|
+
dispatchHoverSequence(opener);
|
|
771
|
+
if (opener.getAttribute?.('aria-expanded') !== 'true') {
|
|
772
|
+
dispatchClickSequence(opener);
|
|
773
|
+
}
|
|
774
|
+
const deadline = performance.now() + MAX_WAIT_MS;
|
|
775
|
+
while (performance.now() < deadline) {
|
|
776
|
+
const submenu = resolveSubmenuFor(opener, parentMenu);
|
|
777
|
+
if (submenu) {
|
|
778
|
+
return selectAndVerify(
|
|
779
|
+
opener,
|
|
780
|
+
() => {
|
|
781
|
+
const current = resolveSubmenuFor(opener, parentMenu);
|
|
782
|
+
return current ? findOptionInMenu(current, modelKindOverride) : null;
|
|
783
|
+
},
|
|
784
|
+
modelKindOverride,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
await sleep(100);
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
790
|
+
};
|
|
791
|
+
|
|
647
792
|
// Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
|
|
648
793
|
// controlled menu contains the effort levels. Prefer this ownership boundary
|
|
649
794
|
// before probing older model-picker layouts.
|
|
@@ -740,9 +885,26 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
740
885
|
}
|
|
741
886
|
if (composerEffortPill) {
|
|
742
887
|
if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
|
|
888
|
+
// In the unified Intelligence picker the composer pill shows the current
|
|
889
|
+
// EFFORT ("Pro", "High"), not the model. Reading a Pro *model* out of it would
|
|
890
|
+
// lift the Pro-row exclusion in findOptionInMenu and let a lower-tier request
|
|
891
|
+
// settle on Pro. Only a pill naming a tier and nothing else qualifies: legacy
|
|
892
|
+
// pills read "Pro Extended" (model + effort) and must keep naming their model,
|
|
893
|
+
// or a Pro Extended user asking for extended would be moved down to High.
|
|
894
|
+
const pillLabel = normalize(
|
|
895
|
+
(composerEffortPill.getAttribute?.('aria-label') ?? '') +
|
|
896
|
+
' ' +
|
|
897
|
+
(composerEffortPill.textContent ?? ''),
|
|
898
|
+
);
|
|
899
|
+
const pillIsBareEffortTier = Object.values(TARGET_LEVEL_TOKENS).some((tokens) =>
|
|
900
|
+
tokens.some((token) => normalize(token) === pillLabel),
|
|
901
|
+
);
|
|
902
|
+
const pillNamesEffortNotModel =
|
|
903
|
+
TARGET_IS_GPT56_MODEL ||
|
|
904
|
+
(pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
|
|
743
905
|
const composerModelKind =
|
|
744
906
|
TARGET_MODEL_KIND ||
|
|
745
|
-
(
|
|
907
|
+
(pillNamesEffortNotModel ? 'versioned' : modelKindFromNode(composerEffortPill));
|
|
746
908
|
if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
|
|
747
909
|
dispatchClickSequence(composerEffortPill);
|
|
748
910
|
await sleep(INITIAL_WAIT_MS);
|
|
@@ -755,6 +917,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
755
917
|
if (proEffortResult) {
|
|
756
918
|
return proEffortResult;
|
|
757
919
|
}
|
|
920
|
+
// Flat rows win when present; only descend into Advanced -> Effort when
|
|
921
|
+
// this menu has no matching tier of its own (the slider layout).
|
|
922
|
+
if (!findOptionInMenu(menu, composerModelKind)) {
|
|
923
|
+
const advancedResult = await selectEffortFromAdvancedSubmenu(menu, composerModelKind);
|
|
924
|
+
if (advancedResult) {
|
|
925
|
+
return advancedResult;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
758
928
|
return selectAndVerify(
|
|
759
929
|
composerEffortPill,
|
|
760
930
|
() => {
|
|
@@ -19,7 +19,7 @@ const BROWSER_MODEL_LABELS = [
|
|
|
19
19
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
20
20
|
["gpt-5.6-sol", "GPT-5.6 Sol"],
|
|
21
21
|
["gpt-5.6", "GPT-5.6 Sol"],
|
|
22
|
-
["gpt-5.5-pro", "
|
|
22
|
+
["gpt-5.5-pro", "GPT-5.5"],
|
|
23
23
|
["gpt-5.5-instant", "GPT-5.5 Instant"],
|
|
24
24
|
["gpt-5.5", "Thinking 5.5"],
|
|
25
25
|
["gpt-5.4-pro", "Pro"],
|
|
@@ -85,8 +85,11 @@ export async function buildBrowserConfig(options) {
|
|
|
85
85
|
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
|
|
86
86
|
const baseModel = options.model.toLowerCase();
|
|
87
87
|
const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
|
|
88
|
+
const normalizedBrowserModel = normalizeChatGptModelForBrowser(options.model);
|
|
88
89
|
const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
89
90
|
const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
|
|
91
|
+
const thinkingTime = normalizeThinkingTimeLevel(options.browserThinkingTime) ??
|
|
92
|
+
(modelStrategy === "select" && normalizedBrowserModel === "gpt-5.5-pro" ? "pro" : undefined);
|
|
90
93
|
assertBrowserModelAvailable(options.model, modelStrategy);
|
|
91
94
|
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
92
95
|
let inline = await resolveInlineCookies({
|
|
@@ -175,7 +178,7 @@ export async function buildBrowserConfig(options) {
|
|
|
175
178
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
176
179
|
remoteChrome,
|
|
177
180
|
browserTabRef: options.browserTab ?? undefined,
|
|
178
|
-
thinkingTime
|
|
181
|
+
thinkingTime,
|
|
179
182
|
researchMode: options.browserResearch === "deep" ? "deep" : "off",
|
|
180
183
|
archiveConversations: options.browserArchive,
|
|
181
184
|
};
|
package/dist/src/cli/options.js
CHANGED
|
@@ -133,7 +133,7 @@ export function parseThinkingTimeOption(value) {
|
|
|
133
133
|
if (normalized) {
|
|
134
134
|
return normalized;
|
|
135
135
|
}
|
|
136
|
-
throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "extra-high", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "xhigh".');
|
|
136
|
+
throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "extra-high", "pro", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "xhigh".');
|
|
137
137
|
}
|
|
138
138
|
export function normalizeModelOption(value) {
|
|
139
139
|
return (value ?? "").trim();
|
|
@@ -3,6 +3,10 @@ export const THINKING_TIME_LEVELS = [
|
|
|
3
3
|
"standard",
|
|
4
4
|
"extended",
|
|
5
5
|
"extra-high",
|
|
6
|
+
// ChatGPT's unified Intelligence picker exposes Pro as the top effort tier of
|
|
7
|
+
// the active model rather than a separate model row, so it is a level here.
|
|
8
|
+
// Kept distinct from "heavy": requesting Pro must be deliberate.
|
|
9
|
+
"pro",
|
|
6
10
|
"heavy",
|
|
7
11
|
];
|
|
8
12
|
export const THINKING_TIME_ALIASES = [
|
|
@@ -38,6 +42,8 @@ export function normalizeThinkingTimeLevel(value) {
|
|
|
38
42
|
case "extrahigh":
|
|
39
43
|
case "xhigh":
|
|
40
44
|
return "extra-high";
|
|
45
|
+
case "pro":
|
|
46
|
+
return "pro";
|
|
41
47
|
case "heavy":
|
|
42
48
|
return "heavy";
|
|
43
49
|
default:
|
|
@@ -23,11 +23,86 @@ const DEFAULT_SLUG = "session";
|
|
|
23
23
|
const MAX_SLUG_WORDS = 5;
|
|
24
24
|
const MIN_CUSTOM_SLUG_WORDS = 3;
|
|
25
25
|
const MAX_SLUG_WORD_LENGTH = 10;
|
|
26
|
+
// Session artifacts (prompt, attached file contents, model responses) are sensitive.
|
|
27
|
+
// Keep them owner-only, matching the meta.json / bridge-config posture (0o600/0o700).
|
|
28
|
+
const SESSION_DIR_MODE = 0o700;
|
|
29
|
+
const SESSION_FILE_MODE = 0o600;
|
|
30
|
+
const sessionStorageHardening = new Map();
|
|
26
31
|
async function ensureDir(dirPath) {
|
|
27
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
32
|
+
await fs.mkdir(dirPath, { recursive: true, mode: SESSION_DIR_MODE });
|
|
33
|
+
}
|
|
34
|
+
function isMissingPathError(error) {
|
|
35
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
36
|
+
}
|
|
37
|
+
async function chmodIfPresent(targetPath, mode) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.chmod(targetPath, mode);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isMissingPathError(error)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function hardenSessionStorageEntry(targetPath) {
|
|
50
|
+
let stats;
|
|
51
|
+
try {
|
|
52
|
+
stats = await fs.lstat(targetPath);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isMissingPathError(error)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (stats.isSymbolicLink()) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (stats.isDirectory()) {
|
|
64
|
+
if (!(await chmodIfPresent(targetPath, SESSION_DIR_MODE))) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = await fs.readdir(targetPath);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isMissingPathError(error)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
await hardenSessionStorageEntry(path.join(targetPath, entry));
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (stats.isFile()) {
|
|
83
|
+
await chmodIfPresent(targetPath, SESSION_FILE_MODE);
|
|
84
|
+
}
|
|
28
85
|
}
|
|
29
86
|
export async function ensureSessionStorage() {
|
|
30
|
-
|
|
87
|
+
const sessionsDir = getSessionsDir();
|
|
88
|
+
await ensureDir(sessionsDir);
|
|
89
|
+
if (process.platform === "win32") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const stats = await fs.lstat(sessionsDir);
|
|
93
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const identity = `${sessionsDir}:${stats.dev}:${stats.ino}`;
|
|
97
|
+
let hardening = sessionStorageHardening.get(identity);
|
|
98
|
+
if (!hardening) {
|
|
99
|
+
hardening = hardenSessionStorageEntry(sessionsDir).catch((error) => {
|
|
100
|
+
sessionStorageHardening.delete(identity);
|
|
101
|
+
throw error;
|
|
102
|
+
});
|
|
103
|
+
sessionStorageHardening.set(identity, hardening);
|
|
104
|
+
}
|
|
105
|
+
await hardening;
|
|
31
106
|
}
|
|
32
107
|
function slugify(text, maxWords = MAX_SLUG_WORDS) {
|
|
33
108
|
const normalized = text?.toLowerCase() ?? "";
|
|
@@ -110,7 +185,7 @@ async function reserveUniqueSessionDir(baseSlug) {
|
|
|
110
185
|
for (;;) {
|
|
111
186
|
const dir = sessionDir(candidate);
|
|
112
187
|
try {
|
|
113
|
-
await fs.mkdir(dir, { recursive: false });
|
|
188
|
+
await fs.mkdir(dir, { recursive: false, mode: SESSION_DIR_MODE });
|
|
114
189
|
return candidate;
|
|
115
190
|
}
|
|
116
191
|
catch (error) {
|
|
@@ -171,7 +246,10 @@ export async function updateModelRunMetadata(sessionId, model, updates) {
|
|
|
171
246
|
...updates,
|
|
172
247
|
model,
|
|
173
248
|
});
|
|
174
|
-
await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2),
|
|
249
|
+
await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2), {
|
|
250
|
+
encoding: "utf8",
|
|
251
|
+
mode: SESSION_FILE_MODE,
|
|
252
|
+
});
|
|
175
253
|
return next;
|
|
176
254
|
}
|
|
177
255
|
export async function readModelRunMetadata(sessionId, model) {
|
|
@@ -261,10 +339,13 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
261
339
|
status: "pending",
|
|
262
340
|
log: { path: path.relative(sessionDir(sessionId), logFilePath) },
|
|
263
341
|
};
|
|
264
|
-
await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2),
|
|
265
|
-
|
|
342
|
+
await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2), {
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
mode: SESSION_FILE_MODE,
|
|
345
|
+
});
|
|
346
|
+
await fs.writeFile(logFilePath, "", { encoding: "utf8", mode: SESSION_FILE_MODE });
|
|
266
347
|
}));
|
|
267
|
-
await fs.writeFile(logPath(sessionId), "", "utf8");
|
|
348
|
+
await fs.writeFile(logPath(sessionId), "", { encoding: "utf8", mode: SESSION_FILE_MODE });
|
|
268
349
|
return metadata;
|
|
269
350
|
}
|
|
270
351
|
export async function readSessionMetadata(sessionId) {
|
|
@@ -350,9 +431,9 @@ async function attachModelRuns(meta, sessionId) {
|
|
|
350
431
|
export function createSessionLogWriter(sessionId, model) {
|
|
351
432
|
const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
|
|
352
433
|
if (model) {
|
|
353
|
-
mkdirSync(modelsDir(sessionId), { recursive: true });
|
|
434
|
+
mkdirSync(modelsDir(sessionId), { recursive: true, mode: SESSION_DIR_MODE });
|
|
354
435
|
}
|
|
355
|
-
const stream = createWriteStream(targetPath, { flags: "a" });
|
|
436
|
+
const stream = createWriteStream(targetPath, { flags: "a", mode: SESSION_FILE_MODE });
|
|
356
437
|
const logLine = (line = "") => {
|
|
357
438
|
stream.write(`${line}\n`);
|
|
358
439
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.6 Sol, GPT-5.6, GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://askoracle.sh",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
61
|
-
"@google/genai": "^2.
|
|
61
|
+
"@google/genai": "^2.16.0",
|
|
62
62
|
"@google/generative-ai": "^0.24.1",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
64
64
|
"@steipete/sweet-cookie": "^0.4.1",
|
|
@@ -77,24 +77,24 @@
|
|
|
77
77
|
"openai": "^7.4.0",
|
|
78
78
|
"osc-progress": "^0.3.2",
|
|
79
79
|
"qs": "^6.15.3",
|
|
80
|
-
"shiki": "^4.4.
|
|
80
|
+
"shiki": "^4.4.3",
|
|
81
81
|
"toasted-notifier": "^10.1.0",
|
|
82
|
-
"tokentally": "^0.1.
|
|
82
|
+
"tokentally": "^0.1.4",
|
|
83
83
|
"zod": "^4.4.3"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
87
87
|
"@types/chrome-remote-interface": "^0.34.0",
|
|
88
88
|
"@types/inquirer": "^9.0.10",
|
|
89
|
-
"@types/node": "^26.
|
|
89
|
+
"@types/node": "^26.2.0",
|
|
90
90
|
"@vitest/coverage-v8": "4.1.10",
|
|
91
|
-
"devtools-protocol": "0.0.
|
|
91
|
+
"devtools-protocol": "0.0.1676105",
|
|
92
92
|
"es-toolkit": "^1.50.0",
|
|
93
|
-
"esbuild": "^0.28.
|
|
94
|
-
"oxfmt": "0.
|
|
95
|
-
"oxlint": "^1.
|
|
93
|
+
"esbuild": "^0.28.2",
|
|
94
|
+
"oxfmt": "0.63.0",
|
|
95
|
+
"oxlint": "^1.78.0",
|
|
96
96
|
"puppeteer-core": "^25.5.0",
|
|
97
|
-
"tsx": "^4.23.
|
|
97
|
+
"tsx": "^4.23.12",
|
|
98
98
|
"typescript": "^7.0.2",
|
|
99
99
|
"vitest": "^4.1.10"
|
|
100
100
|
},
|
|
Binary file
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>CFBundleIdentifier</key>
|
|
6
|
-
<string>com.steipete.oracle.notifier</string>
|
|
7
|
-
<key>CFBundleName</key>
|
|
8
|
-
<string>OracleNotifier</string>
|
|
9
|
-
<key>CFBundleDisplayName</key>
|
|
10
|
-
<string>Oracle Notifier</string>
|
|
11
|
-
<key>CFBundleExecutable</key>
|
|
12
|
-
<string>OracleNotifier</string>
|
|
13
|
-
<key>CFBundleIconFile</key>
|
|
14
|
-
<string>OracleIcon</string>
|
|
15
|
-
<key>CFBundlePackageType</key>
|
|
16
|
-
<string>APPL</string>
|
|
17
|
-
<key>LSMinimumSystemVersion</key>
|
|
18
|
-
<string>13.0</string>
|
|
19
|
-
</dict>
|
|
20
|
-
</plist>
|
|
Binary file
|
|
Binary file
|
package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>files</key>
|
|
6
|
-
<dict>
|
|
7
|
-
<key>Resources/OracleIcon.icns</key>
|
|
8
|
-
<data>
|
|
9
|
-
edUHAMetayIv3xtc3Vb92VXRLfM=
|
|
10
|
-
</data>
|
|
11
|
-
</dict>
|
|
12
|
-
<key>files2</key>
|
|
13
|
-
<dict>
|
|
14
|
-
<key>Resources/OracleIcon.icns</key>
|
|
15
|
-
<dict>
|
|
16
|
-
<key>hash2</key>
|
|
17
|
-
<data>
|
|
18
|
-
AVPJK/6w6IOsDLmZTW4hL+Za+/4wHMxZiIp0t6m3NRA=
|
|
19
|
-
</data>
|
|
20
|
-
</dict>
|
|
21
|
-
</dict>
|
|
22
|
-
<key>rules</key>
|
|
23
|
-
<dict>
|
|
24
|
-
<key>^Resources/</key>
|
|
25
|
-
<true/>
|
|
26
|
-
<key>^Resources/.*\.lproj/</key>
|
|
27
|
-
<dict>
|
|
28
|
-
<key>optional</key>
|
|
29
|
-
<true/>
|
|
30
|
-
<key>weight</key>
|
|
31
|
-
<real>1000</real>
|
|
32
|
-
</dict>
|
|
33
|
-
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
34
|
-
<dict>
|
|
35
|
-
<key>omit</key>
|
|
36
|
-
<true/>
|
|
37
|
-
<key>weight</key>
|
|
38
|
-
<real>1100</real>
|
|
39
|
-
</dict>
|
|
40
|
-
<key>^Resources/Base\.lproj/</key>
|
|
41
|
-
<dict>
|
|
42
|
-
<key>weight</key>
|
|
43
|
-
<real>1010</real>
|
|
44
|
-
</dict>
|
|
45
|
-
<key>^version.plist$</key>
|
|
46
|
-
<true/>
|
|
47
|
-
</dict>
|
|
48
|
-
<key>rules2</key>
|
|
49
|
-
<dict>
|
|
50
|
-
<key>.*\.dSYM($|/)</key>
|
|
51
|
-
<dict>
|
|
52
|
-
<key>weight</key>
|
|
53
|
-
<real>11</real>
|
|
54
|
-
</dict>
|
|
55
|
-
<key>^(.*/)?\.DS_Store$</key>
|
|
56
|
-
<dict>
|
|
57
|
-
<key>omit</key>
|
|
58
|
-
<true/>
|
|
59
|
-
<key>weight</key>
|
|
60
|
-
<real>2000</real>
|
|
61
|
-
</dict>
|
|
62
|
-
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
|
63
|
-
<dict>
|
|
64
|
-
<key>nested</key>
|
|
65
|
-
<true/>
|
|
66
|
-
<key>weight</key>
|
|
67
|
-
<real>10</real>
|
|
68
|
-
</dict>
|
|
69
|
-
<key>^.*</key>
|
|
70
|
-
<true/>
|
|
71
|
-
<key>^Info\.plist$</key>
|
|
72
|
-
<dict>
|
|
73
|
-
<key>omit</key>
|
|
74
|
-
<true/>
|
|
75
|
-
<key>weight</key>
|
|
76
|
-
<real>20</real>
|
|
77
|
-
</dict>
|
|
78
|
-
<key>^PkgInfo$</key>
|
|
79
|
-
<dict>
|
|
80
|
-
<key>omit</key>
|
|
81
|
-
<true/>
|
|
82
|
-
<key>weight</key>
|
|
83
|
-
<real>20</real>
|
|
84
|
-
</dict>
|
|
85
|
-
<key>^Resources/</key>
|
|
86
|
-
<dict>
|
|
87
|
-
<key>weight</key>
|
|
88
|
-
<real>20</real>
|
|
89
|
-
</dict>
|
|
90
|
-
<key>^Resources/.*\.lproj/</key>
|
|
91
|
-
<dict>
|
|
92
|
-
<key>optional</key>
|
|
93
|
-
<true/>
|
|
94
|
-
<key>weight</key>
|
|
95
|
-
<real>1000</real>
|
|
96
|
-
</dict>
|
|
97
|
-
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
98
|
-
<dict>
|
|
99
|
-
<key>omit</key>
|
|
100
|
-
<true/>
|
|
101
|
-
<key>weight</key>
|
|
102
|
-
<real>1100</real>
|
|
103
|
-
</dict>
|
|
104
|
-
<key>^Resources/Base\.lproj/</key>
|
|
105
|
-
<dict>
|
|
106
|
-
<key>weight</key>
|
|
107
|
-
<real>1010</real>
|
|
108
|
-
</dict>
|
|
109
|
-
<key>^[^/]+$</key>
|
|
110
|
-
<dict>
|
|
111
|
-
<key>nested</key>
|
|
112
|
-
<true/>
|
|
113
|
-
<key>weight</key>
|
|
114
|
-
<real>10</real>
|
|
115
|
-
</dict>
|
|
116
|
-
<key>^embedded\.provisionprofile$</key>
|
|
117
|
-
<dict>
|
|
118
|
-
<key>weight</key>
|
|
119
|
-
<real>20</real>
|
|
120
|
-
</dict>
|
|
121
|
-
<key>^version\.plist$</key>
|
|
122
|
-
<dict>
|
|
123
|
-
<key>weight</key>
|
|
124
|
-
<real>20</real>
|
|
125
|
-
</dict>
|
|
126
|
-
</dict>
|
|
127
|
-
</dict>
|
|
128
|
-
</plist>
|