@steipete/oracle 0.6.1 → 0.7.0
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 +16 -8
- package/dist/bin/oracle-cli.js +33 -13
- package/dist/src/browser/prompt.js +30 -6
- package/dist/src/browser/sessionRunner.js +0 -5
- package/dist/src/cli/runOptions.js +6 -7
- package/dist/src/cli/sessionRunner.js +0 -3
- package/dist/src/gemini-web/client.js +322 -0
- package/dist/src/gemini-web/executor.js +204 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/package.json +2 -2
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +0 -45
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +0 -24
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -93
package/README.md
CHANGED
|
@@ -21,26 +21,29 @@ Use `npx -y @steipete/oracle …` (not `pnpx`)—pnpx's sandboxed cache can’t
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
# Copy the bundle and paste into ChatGPT
|
|
24
|
-
npx @steipete/oracle --render --copy -p "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"
|
|
24
|
+
npx -y @steipete/oracle --render --copy -p "Review the TS data layer for schema drift" --file "src/**/*.ts,*/*.test.ts"
|
|
25
25
|
|
|
26
26
|
# Minimal API run (expects OPENAI_API_KEY in your env)
|
|
27
|
-
npx @steipete/oracle -p "Write a concise architecture note for the storage adapters" --file src/storage/README.md
|
|
27
|
+
npx -y @steipete/oracle -p "Write a concise architecture note for the storage adapters" --file src/storage/README.md
|
|
28
28
|
|
|
29
29
|
# Multi-model API run
|
|
30
|
-
npx @steipete/oracle -p "Cross-check the data layer assumptions" --models gpt-5.1-pro,gemini-3-pro --file "src/**/*.ts"
|
|
30
|
+
npx -y @steipete/oracle -p "Cross-check the data layer assumptions" --models gpt-5.1-pro,gemini-3-pro --file "src/**/*.ts"
|
|
31
31
|
|
|
32
32
|
# Preview without spending tokens
|
|
33
|
-
npx @steipete/oracle --dry-run summary -p "Check release notes" --file docs/release-notes.md
|
|
33
|
+
npx -y @steipete/oracle --dry-run summary -p "Check release notes" --file docs/release-notes.md
|
|
34
34
|
|
|
35
35
|
# Browser run (no API key, will open ChatGPT)
|
|
36
|
-
npx @steipete/oracle --engine browser -p "Walk through the UI smoke test" --file "src/**/*.ts"
|
|
36
|
+
npx -y @steipete/oracle --engine browser -p "Walk through the UI smoke test" --file "src/**/*.ts"
|
|
37
|
+
|
|
38
|
+
# Gemini browser mode (no API key; uses Chrome cookies from gemini.google.com)
|
|
39
|
+
npx -y @steipete/oracle --engine browser --model gemini-3-pro --prompt "a cute robot holding a banana" --generate-image out.jpg --aspect 1:1
|
|
37
40
|
|
|
38
41
|
# Sessions (list and replay)
|
|
39
|
-
npx @steipete/oracle status --hours 72
|
|
40
|
-
npx @steipete/oracle session <id> --render
|
|
42
|
+
npx -y @steipete/oracle status --hours 72
|
|
43
|
+
npx -y @steipete/oracle session <id> --render
|
|
41
44
|
|
|
42
45
|
# TUI (interactive, only for humans)
|
|
43
|
-
npx @steipete/oracle tui
|
|
46
|
+
npx -y @steipete/oracle tui
|
|
44
47
|
```
|
|
45
48
|
|
|
46
49
|
Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser is stable on macOS and works on Linux and Windows. On Linux pass `--browser-chrome-path/--browser-cookie-path` if detection fails; on Windows prefer `--browser-manual-login` or inline cookies if decryption is blocked.
|
|
@@ -49,6 +52,8 @@ Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser i
|
|
|
49
52
|
|
|
50
53
|
**CLI**
|
|
51
54
|
- API mode expects API keys in your environment: `OPENAI_API_KEY` (GPT-5.x), `GEMINI_API_KEY` (Gemini 3 Pro), `ANTHROPIC_API_KEY` (Claude Sonnet 4.5 / Opus 4.1).
|
|
55
|
+
- Gemini browser mode uses Chrome cookies instead of an API key—just be logged into `gemini.google.com` in Chrome (no Python/venv required).
|
|
56
|
+
- If your Gemini account can’t access “Pro”, Oracle auto-falls back to a supported model for web runs (and logs the fallback in verbose mode).
|
|
52
57
|
- Prefer API mode or `--copy` + manual paste; browser automation is experimental.
|
|
53
58
|
- Browser support: stable on macOS; works on Linux (add `--browser-chrome-path/--browser-cookie-path` when needed) and Windows (manual-login or inline cookies recommended when app-bound cookies block decryption).
|
|
54
59
|
- Remote browser service: `oracle serve` on a signed-in host; clients use `--remote-host/--remote-token`.
|
|
@@ -109,6 +114,9 @@ npx -y @steipete/oracle oracle-mcp
|
|
|
109
114
|
| `--dry-run [summary\|json\|full]` | Preview without sending. |
|
|
110
115
|
| `--remote-host`, `--remote-token` | Use a remote `oracle serve` host (browser). |
|
|
111
116
|
| `--remote-chrome <host:port>` | Attach to an existing remote Chrome session (browser). |
|
|
117
|
+
| `--youtube <url>` | YouTube video URL to analyze (Gemini browser mode). |
|
|
118
|
+
| `--generate-image <file>` | Generate image and save to file (Gemini browser mode). |
|
|
119
|
+
| `--edit-image <file>` | Edit existing image with `--output` (Gemini browser mode). |
|
|
112
120
|
| `--azure-endpoint`, `--azure-deployment`, `--azure-api-version` | Target Azure OpenAI endpoints (picks Azure client automatically). |
|
|
113
121
|
|
|
114
122
|
## Configuration
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import { DEFAULT_MODEL, MODEL_CONFIGS, readFiles, estimateRequestTokens, buildRe
|
|
|
18
18
|
import { isKnownModel } from '../src/oracle/modelResolver.js';
|
|
19
19
|
import { CHATGPT_URL } from '../src/browserMode.js';
|
|
20
20
|
import { createRemoteBrowserExecutor } from '../src/remote/client.js';
|
|
21
|
+
import { createGeminiWebExecutor } from '../src/gemini-web/index.js';
|
|
21
22
|
import { applyHelpStyling } from '../src/cli/help.js';
|
|
22
23
|
import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, mergePathLikeOptions, } from '../src/cli/options.js';
|
|
23
24
|
import { copyToClipboard } from '../src/cli/clipboard.js';
|
|
@@ -26,6 +27,7 @@ import { shouldDetachSession } from '../src/cli/detach.js';
|
|
|
26
27
|
import { applyHiddenAliases } from '../src/cli/hiddenAliases.js';
|
|
27
28
|
import { buildBrowserConfig, resolveBrowserModelLabel } from '../src/cli/browserConfig.js';
|
|
28
29
|
import { performSessionRun } from '../src/cli/sessionRunner.js';
|
|
30
|
+
import { isMediaFile } from '../src/browser/prompt.js';
|
|
29
31
|
import { attachSession, showStatus, formatCompletionSummary } from '../src/cli/sessionDisplay.js';
|
|
30
32
|
import { formatCompactNumber } from '../src/cli/format.js';
|
|
31
33
|
import { formatIntroLine } from '../src/cli/tagline.js';
|
|
@@ -114,7 +116,7 @@ program
|
|
|
114
116
|
.addOption(new Option('--models <models>', 'Comma-separated API model list to query in parallel (e.g., "gpt-5.1-pro,gemini-3-pro").')
|
|
115
117
|
.argParser(collectModelList)
|
|
116
118
|
.default([]))
|
|
117
|
-
.addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser).
|
|
119
|
+
.addOption(new Option('-e, --engine <mode>', 'Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.').choices(['api', 'browser']))
|
|
118
120
|
.addOption(new Option('--mode <mode>', 'Alias for --engine (api | browser).').choices(['api', 'browser']).hideHelp())
|
|
119
121
|
.option('--files-report', 'Show token usage per attached file (also prints automatically when files exceed the token budget).', false)
|
|
120
122
|
.option('-v, --verbose', 'Enable verbose logging for all operations.', false)
|
|
@@ -182,6 +184,12 @@ program
|
|
|
182
184
|
.addOption(new Option('--remote-token <token>', 'Access token for the remote `oracle serve` instance.'))
|
|
183
185
|
.addOption(new Option('--browser-inline-files', 'Alias for --browser-attachments never (force pasting file contents inline).').default(false))
|
|
184
186
|
.addOption(new Option('--browser-bundle-files', 'Bundle all attachments into a single archive before uploading.').default(false))
|
|
187
|
+
.addOption(new Option('--youtube <url>', 'YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com).'))
|
|
188
|
+
.addOption(new Option('--generate-image <file>', 'Generate image and save to file (Gemini web/cookie mode only; requires gemini.google.com Chrome cookies).'))
|
|
189
|
+
.addOption(new Option('--edit-image <file>', 'Edit existing image (use with --output, Gemini web/cookie mode only).'))
|
|
190
|
+
.addOption(new Option('--output <file>', 'Output file path for image operations (Gemini web/cookie mode only).'))
|
|
191
|
+
.addOption(new Option('--aspect <ratio>', 'Aspect ratio for image generation: 16:9, 1:1, 4:3, 3:4 (Gemini web/cookie mode only).'))
|
|
192
|
+
.addOption(new Option('--gemini-show-thoughts', 'Display Gemini thinking process (Gemini web/cookie mode only).').default(false))
|
|
185
193
|
.option('--retain-hours <hours>', 'Prune stored sessions older than this many hours before running (set 0 to disable).', parseFloatOption)
|
|
186
194
|
.option('--force', 'Force start a new session even if an identical prompt is already running.', false)
|
|
187
195
|
.option('--debug-help', 'Show the advanced/debug option set and exit.', false)
|
|
@@ -512,18 +520,13 @@ async function runRootCommand(options) {
|
|
|
512
520
|
const isCodex = primaryModelCandidate.startsWith('gpt-5.1-codex');
|
|
513
521
|
const isClaude = primaryModelCandidate.startsWith('claude');
|
|
514
522
|
const userForcedBrowser = options.browser || options.engine === 'browser';
|
|
515
|
-
const
|
|
523
|
+
const isBrowserCompatible = (model) => model.startsWith('gpt-') || model.startsWith('gemini');
|
|
524
|
+
const hasNonBrowserCompatibleTarget = (engine === 'browser' || userForcedBrowser) &&
|
|
516
525
|
(normalizedMultiModels.length > 0
|
|
517
|
-
? normalizedMultiModels.some((model) => !model
|
|
518
|
-
: !resolvedModelCandidate
|
|
519
|
-
if (
|
|
520
|
-
throw new Error('Browser engine only supports GPT
|
|
521
|
-
}
|
|
522
|
-
if (isGemini && userForcedBrowser) {
|
|
523
|
-
throw new Error('Gemini is only supported via API. Use --engine api.');
|
|
524
|
-
}
|
|
525
|
-
if (isGemini && engine === 'browser') {
|
|
526
|
-
engine = 'api';
|
|
526
|
+
? normalizedMultiModels.some((model) => !isBrowserCompatible(model))
|
|
527
|
+
: !isBrowserCompatible(resolvedModelCandidate));
|
|
528
|
+
if (hasNonBrowserCompatibleTarget) {
|
|
529
|
+
throw new Error('Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.');
|
|
527
530
|
}
|
|
528
531
|
if (isClaude && engine === 'browser') {
|
|
529
532
|
console.log(chalk.dim('Browser engine is not supported for Claude models; switching to API.'));
|
|
@@ -672,7 +675,11 @@ async function runRootCommand(options) {
|
|
|
672
675
|
return;
|
|
673
676
|
}
|
|
674
677
|
if (options.file && options.file.length > 0) {
|
|
675
|
-
|
|
678
|
+
const isBrowserMode = engine === 'browser' || userForcedBrowser;
|
|
679
|
+
const filesToValidate = isBrowserMode ? options.file.filter((f) => !isMediaFile(f)) : options.file;
|
|
680
|
+
if (filesToValidate.length > 0) {
|
|
681
|
+
await readFiles(filesToValidate, { cwd: process.cwd() });
|
|
682
|
+
}
|
|
676
683
|
}
|
|
677
684
|
const getSource = (key) => program.getOptionValueSource?.(key) ?? undefined;
|
|
678
685
|
applyBrowserDefaultsFromConfig(options, userConfig, getSource);
|
|
@@ -698,6 +705,19 @@ async function runRootCommand(options) {
|
|
|
698
705
|
};
|
|
699
706
|
console.log(chalk.dim(`Routing browser automation to remote host ${remoteHost}`));
|
|
700
707
|
}
|
|
708
|
+
else if (browserConfig && resolvedModel.startsWith('gemini')) {
|
|
709
|
+
browserDeps = {
|
|
710
|
+
executeBrowser: createGeminiWebExecutor({
|
|
711
|
+
youtube: options.youtube,
|
|
712
|
+
generateImage: options.generateImage,
|
|
713
|
+
editImage: options.editImage,
|
|
714
|
+
outputPath: options.output,
|
|
715
|
+
aspectRatio: options.aspect,
|
|
716
|
+
showThoughts: options.geminiShowThoughts,
|
|
717
|
+
}),
|
|
718
|
+
};
|
|
719
|
+
console.log(chalk.dim('Using Gemini web client for browser automation'));
|
|
720
|
+
}
|
|
701
721
|
const remoteExecutionActive = Boolean(browserDeps);
|
|
702
722
|
if (options.dryRun) {
|
|
703
723
|
const baseRunOptions = buildRunOptions(resolvedOptions, {
|
|
@@ -6,10 +6,32 @@ import { isKnownModel } from '../oracle/modelResolver.js';
|
|
|
6
6
|
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
7
7
|
import { buildAttachmentPlan } from './policies.js';
|
|
8
8
|
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
9
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
10
|
+
'.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v',
|
|
11
|
+
'.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a',
|
|
12
|
+
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif',
|
|
13
|
+
'.pdf',
|
|
14
|
+
]);
|
|
15
|
+
export function isMediaFile(filePath) {
|
|
16
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
17
|
+
return MEDIA_EXTENSIONS.has(ext);
|
|
18
|
+
}
|
|
9
19
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
10
20
|
const cwd = deps.cwd ?? process.cwd();
|
|
11
21
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
12
|
-
const
|
|
22
|
+
const allFilePaths = runOptions.file ?? [];
|
|
23
|
+
const textFilePaths = allFilePaths.filter((f) => !isMediaFile(f));
|
|
24
|
+
const mediaFilePaths = allFilePaths.filter((f) => isMediaFile(f));
|
|
25
|
+
const mediaAttachments = await Promise.all(mediaFilePaths.map(async (filePath) => {
|
|
26
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
27
|
+
const stats = await fs.stat(resolvedPath);
|
|
28
|
+
return {
|
|
29
|
+
path: resolvedPath,
|
|
30
|
+
displayPath: path.relative(cwd, resolvedPath) || path.basename(resolvedPath),
|
|
31
|
+
sizeBytes: stats.size,
|
|
32
|
+
};
|
|
33
|
+
}));
|
|
34
|
+
const files = await readFilesFn(textFilePaths, { cwd });
|
|
13
35
|
const basePrompt = (runOptions.prompt ?? '').trim();
|
|
14
36
|
const userPrompt = basePrompt;
|
|
15
37
|
const systemPrompt = runOptions.system?.trim() || '';
|
|
@@ -40,9 +62,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
40
62
|
.filter(Boolean)
|
|
41
63
|
.join('\n\n')
|
|
42
64
|
.trim();
|
|
43
|
-
const attachments = selectedPlan.attachments
|
|
65
|
+
const attachments = [...selectedPlan.attachments, ...mediaAttachments];
|
|
44
66
|
const shouldBundle = selectedPlan.shouldBundle;
|
|
45
67
|
let bundleText = null;
|
|
68
|
+
let bundled = null;
|
|
46
69
|
if (shouldBundle) {
|
|
47
70
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
48
71
|
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
@@ -59,6 +82,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
59
82
|
displayPath: bundlePath,
|
|
60
83
|
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
61
84
|
});
|
|
85
|
+
attachments.push(...mediaAttachments);
|
|
86
|
+
bundled = { originalCount: sections.length, bundlePath };
|
|
62
87
|
}
|
|
63
88
|
const inlineFileCount = selectedPlan.inlineFileCount;
|
|
64
89
|
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
@@ -85,7 +110,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
85
110
|
let fallback = null;
|
|
86
111
|
if (attachmentsPolicy === 'auto' && selectedPlan.mode === 'inline' && sections.length > 0) {
|
|
87
112
|
const fallbackComposerText = baseComposerSections.join('\n\n').trim();
|
|
88
|
-
const fallbackAttachments = uploadPlan.attachments
|
|
113
|
+
const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
|
|
89
114
|
let fallbackBundled = null;
|
|
90
115
|
if (uploadPlan.shouldBundle) {
|
|
91
116
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
@@ -103,6 +128,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
103
128
|
displayPath: bundlePath,
|
|
104
129
|
sizeBytes: Buffer.byteLength(fallbackBundleText, 'utf8'),
|
|
105
130
|
});
|
|
131
|
+
fallbackAttachments.push(...mediaAttachments);
|
|
106
132
|
fallbackBundled = { originalCount: sections.length, bundlePath };
|
|
107
133
|
}
|
|
108
134
|
fallback = {
|
|
@@ -121,8 +147,6 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
121
147
|
attachmentsPolicy,
|
|
122
148
|
attachmentMode: selectedPlan.mode,
|
|
123
149
|
fallback,
|
|
124
|
-
bundled
|
|
125
|
-
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
126
|
-
: null,
|
|
150
|
+
bundled,
|
|
127
151
|
};
|
|
128
152
|
}
|
|
@@ -5,11 +5,6 @@ import { runBrowserMode } from '../browserMode.js';
|
|
|
5
5
|
import { assembleBrowserPrompt } from './prompt.js';
|
|
6
6
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
7
7
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
8
|
-
if (runOptions.model.startsWith('gemini')) {
|
|
9
|
-
throw new BrowserAutomationError('Gemini models are not available in browser mode. Re-run with --engine api.', {
|
|
10
|
-
stage: 'preflight',
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
8
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
14
9
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
15
10
|
const promptArtifacts = await assemblePrompt(runOptions, { cwd });
|
|
@@ -13,7 +13,6 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
13
13
|
const resolvedModel = resolvedEngine === 'browser' && normalizedRequestedModels.length === 0
|
|
14
14
|
? inferModelFromLabel(cliModelArg)
|
|
15
15
|
: resolveApiModel(cliModelArg);
|
|
16
|
-
const isGemini = resolvedModel.startsWith('gemini');
|
|
17
16
|
const isCodex = resolvedModel.startsWith('gpt-5.1-codex');
|
|
18
17
|
const isClaude = resolvedModel.startsWith('claude');
|
|
19
18
|
const isGrok = resolvedModel.startsWith('grok');
|
|
@@ -21,13 +20,13 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
21
20
|
const allModels = normalizedRequestedModels.length > 0
|
|
22
21
|
? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
|
|
23
22
|
: [resolvedModel];
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const isBrowserCompatible = (m) => m.startsWith('gpt-') || m.startsWith('gemini');
|
|
24
|
+
const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) && allModels.some((m) => !isBrowserCompatible(m));
|
|
25
|
+
if (hasNonBrowserCompatibleTarget) {
|
|
26
|
+
throw new PromptValidationError('Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.', { engine: 'browser', models: allModels });
|
|
27
27
|
}
|
|
28
|
-
const engineCoercedToApi = engineWasBrowser && (
|
|
29
|
-
|
|
30
|
-
const fixedEngine = isGemini || isCodex || isClaude || isGrok || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
|
|
28
|
+
const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok);
|
|
29
|
+
const fixedEngine = isCodex || isClaude || isGrok || normalizedRequestedModels.length > 0 ? 'api' : resolvedEngine;
|
|
31
30
|
const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
|
|
32
31
|
? `${prompt.trim()}\n${userConfig.promptSuffix}`
|
|
33
32
|
: prompt;
|
|
@@ -38,9 +38,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
38
38
|
const modelForStatus = runOptions.model ?? sessionMeta.model;
|
|
39
39
|
try {
|
|
40
40
|
if (mode === 'browser') {
|
|
41
|
-
if (runOptions.model.startsWith('gemini')) {
|
|
42
|
-
throw new Error('Gemini models are not available in browser mode. Re-run with --engine api.');
|
|
43
|
-
}
|
|
44
41
|
if (!browserConfig) {
|
|
45
42
|
throw new Error('Missing browser configuration for session.');
|
|
46
43
|
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
4
|
+
const MODEL_HEADER_NAME = 'x-goog-ext-525001261-jspb';
|
|
5
|
+
const MODEL_HEADERS = {
|
|
6
|
+
'gemini-3-pro': '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]',
|
|
7
|
+
'gemini-2.5-pro': '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
|
|
8
|
+
'gemini-2.5-flash': '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
|
|
9
|
+
};
|
|
10
|
+
const GEMINI_APP_URL = 'https://gemini.google.com/app';
|
|
11
|
+
const GEMINI_STREAM_GENERATE_URL = 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate';
|
|
12
|
+
const GEMINI_UPLOAD_URL = 'https://content-push.googleapis.com/upload';
|
|
13
|
+
const GEMINI_UPLOAD_PUSH_ID = 'feeds/mcudyrk2a4khkz';
|
|
14
|
+
function getNestedValue(value, pathParts, fallback) {
|
|
15
|
+
let current = value;
|
|
16
|
+
for (const part of pathParts) {
|
|
17
|
+
if (current == null)
|
|
18
|
+
return fallback;
|
|
19
|
+
if (typeof part === 'number') {
|
|
20
|
+
if (!Array.isArray(current))
|
|
21
|
+
return fallback;
|
|
22
|
+
current = current[part];
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (typeof current !== 'object')
|
|
26
|
+
return fallback;
|
|
27
|
+
current = current[part];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return current ?? fallback;
|
|
31
|
+
}
|
|
32
|
+
function buildCookieHeader(cookieMap) {
|
|
33
|
+
return Object.entries(cookieMap)
|
|
34
|
+
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
|
35
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
36
|
+
.join('; ');
|
|
37
|
+
}
|
|
38
|
+
export async function fetchGeminiAccessToken(cookieMap) {
|
|
39
|
+
const cookieHeader = buildCookieHeader(cookieMap);
|
|
40
|
+
const res = await fetch(GEMINI_APP_URL, {
|
|
41
|
+
redirect: 'follow',
|
|
42
|
+
headers: {
|
|
43
|
+
cookie: cookieHeader,
|
|
44
|
+
'user-agent': USER_AGENT,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const html = await res.text();
|
|
48
|
+
const tokens = ['SNlM0e', 'thykhd'];
|
|
49
|
+
for (const key of tokens) {
|
|
50
|
+
const match = html.match(new RegExp(`"${key}":"(.*?)"`));
|
|
51
|
+
if (match?.[1])
|
|
52
|
+
return match[1];
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Unable to locate Gemini access token on gemini.google.com/app (missing SNlM0e/thykhd).');
|
|
55
|
+
}
|
|
56
|
+
function trimGeminiJsonEnvelope(text) {
|
|
57
|
+
const start = text.indexOf('[');
|
|
58
|
+
const end = text.lastIndexOf(']');
|
|
59
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
60
|
+
throw new Error('Gemini response did not contain a JSON payload.');
|
|
61
|
+
}
|
|
62
|
+
return text.slice(start, end + 1);
|
|
63
|
+
}
|
|
64
|
+
function extractErrorCode(responseJson) {
|
|
65
|
+
const code = getNestedValue(responseJson, [0, 5, 2, 0, 1, 0], -1);
|
|
66
|
+
return typeof code === 'number' && code >= 0 ? code : undefined;
|
|
67
|
+
}
|
|
68
|
+
function extractGgdlUrls(rawText) {
|
|
69
|
+
const matches = rawText.match(/https:\/\/lh3\.googleusercontent\.com\/gg-dl\/[^\s"']+/g) ?? [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const urls = [];
|
|
72
|
+
for (const match of matches) {
|
|
73
|
+
if (seen.has(match))
|
|
74
|
+
continue;
|
|
75
|
+
seen.add(match);
|
|
76
|
+
urls.push(match);
|
|
77
|
+
}
|
|
78
|
+
return urls;
|
|
79
|
+
}
|
|
80
|
+
function ensureFullSizeImageUrl(url) {
|
|
81
|
+
if (url.includes('=s2048'))
|
|
82
|
+
return url;
|
|
83
|
+
if (url.includes('=s'))
|
|
84
|
+
return url;
|
|
85
|
+
return `${url}=s2048`;
|
|
86
|
+
}
|
|
87
|
+
async function fetchWithCookiePreservingRedirects(url, init, maxRedirects = 10) {
|
|
88
|
+
let current = url;
|
|
89
|
+
for (let i = 0; i <= maxRedirects; i += 1) {
|
|
90
|
+
const res = await fetch(current, { ...init, redirect: 'manual' });
|
|
91
|
+
if (res.status >= 300 && res.status < 400) {
|
|
92
|
+
const location = res.headers.get('location');
|
|
93
|
+
if (!location)
|
|
94
|
+
return res;
|
|
95
|
+
current = new URL(location, current).toString();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
return res;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Too many redirects while downloading image (>${maxRedirects}).`);
|
|
101
|
+
}
|
|
102
|
+
async function downloadGeminiImage(url, cookieMap, outputPath) {
|
|
103
|
+
const cookieHeader = buildCookieHeader(cookieMap);
|
|
104
|
+
const res = await fetchWithCookiePreservingRedirects(ensureFullSizeImageUrl(url), {
|
|
105
|
+
headers: {
|
|
106
|
+
cookie: cookieHeader,
|
|
107
|
+
'user-agent': USER_AGENT,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
throw new Error(`Failed to download image: ${res.status} ${res.statusText} (${res.url})`);
|
|
112
|
+
}
|
|
113
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
114
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
115
|
+
await writeFile(outputPath, data);
|
|
116
|
+
}
|
|
117
|
+
async function uploadGeminiFile(filePath) {
|
|
118
|
+
const absPath = path.resolve(process.cwd(), filePath);
|
|
119
|
+
const data = await readFile(absPath);
|
|
120
|
+
const fileName = path.basename(absPath);
|
|
121
|
+
const form = new FormData();
|
|
122
|
+
form.append('file', new Blob([data]), fileName);
|
|
123
|
+
const res = await fetch(GEMINI_UPLOAD_URL, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
redirect: 'follow',
|
|
126
|
+
headers: {
|
|
127
|
+
'push-id': GEMINI_UPLOAD_PUSH_ID,
|
|
128
|
+
'user-agent': USER_AGENT,
|
|
129
|
+
},
|
|
130
|
+
body: form,
|
|
131
|
+
});
|
|
132
|
+
const text = await res.text();
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
throw new Error(`File upload failed: ${res.status} ${res.statusText} (${text.slice(0, 200)})`);
|
|
135
|
+
}
|
|
136
|
+
return { id: text, name: fileName };
|
|
137
|
+
}
|
|
138
|
+
function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
|
|
139
|
+
const promptPayload = uploaded.length > 0
|
|
140
|
+
? [
|
|
141
|
+
prompt,
|
|
142
|
+
0,
|
|
143
|
+
null,
|
|
144
|
+
// Matches gemini-webapi payload format: [[[fileId, 1]]] for a single attachment.
|
|
145
|
+
// Keep it extensible for multiple uploads by emitting one [[id, 1]] entry per file.
|
|
146
|
+
uploaded.map((file) => [[file.id, 1]]),
|
|
147
|
+
]
|
|
148
|
+
: [prompt];
|
|
149
|
+
const innerList = [promptPayload, null, chatMetadata ?? null];
|
|
150
|
+
return JSON.stringify([null, JSON.stringify(innerList)]);
|
|
151
|
+
}
|
|
152
|
+
export function parseGeminiStreamGenerateResponse(rawText) {
|
|
153
|
+
const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
|
|
154
|
+
const errorCode = extractErrorCode(responseJson);
|
|
155
|
+
const parts = Array.isArray(responseJson) ? responseJson : [];
|
|
156
|
+
let bodyIndex = 0;
|
|
157
|
+
let body = null;
|
|
158
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
159
|
+
const partBody = getNestedValue(parts[i], [2], null);
|
|
160
|
+
if (!partBody)
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(partBody);
|
|
164
|
+
const candidateList = getNestedValue(parsed, [4], []);
|
|
165
|
+
if (Array.isArray(candidateList) && candidateList.length > 0) {
|
|
166
|
+
bodyIndex = i;
|
|
167
|
+
body = parsed;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const candidateList = getNestedValue(body, [4], []);
|
|
176
|
+
const firstCandidate = candidateList[0];
|
|
177
|
+
const textRaw = getNestedValue(firstCandidate, [1, 0], '');
|
|
178
|
+
const cardContent = /^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(textRaw);
|
|
179
|
+
const text = cardContent
|
|
180
|
+
? (getNestedValue(firstCandidate, [22, 0], null) ?? textRaw)
|
|
181
|
+
: textRaw;
|
|
182
|
+
const thoughts = getNestedValue(firstCandidate, [37, 0, 0], null);
|
|
183
|
+
const metadata = getNestedValue(body, [1], []);
|
|
184
|
+
const images = [];
|
|
185
|
+
const webImages = getNestedValue(firstCandidate, [12, 1], []);
|
|
186
|
+
for (const webImage of webImages) {
|
|
187
|
+
const url = getNestedValue(webImage, [0, 0, 0], null);
|
|
188
|
+
if (!url)
|
|
189
|
+
continue;
|
|
190
|
+
images.push({
|
|
191
|
+
kind: 'web',
|
|
192
|
+
url,
|
|
193
|
+
title: getNestedValue(webImage, [7, 0], undefined),
|
|
194
|
+
alt: getNestedValue(webImage, [0, 4], undefined),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const hasGenerated = Boolean(getNestedValue(firstCandidate, [12, 7, 0], null));
|
|
198
|
+
if (hasGenerated) {
|
|
199
|
+
let imgBody = null;
|
|
200
|
+
for (let i = bodyIndex; i < parts.length; i += 1) {
|
|
201
|
+
const partBody = getNestedValue(parts[i], [2], null);
|
|
202
|
+
if (!partBody)
|
|
203
|
+
continue;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(partBody);
|
|
206
|
+
const candidateImages = getNestedValue(parsed, [4, 0, 12, 7, 0], null);
|
|
207
|
+
if (candidateImages != null) {
|
|
208
|
+
imgBody = parsed;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const imgCandidate = getNestedValue(imgBody ?? body, [4, 0], null);
|
|
217
|
+
const generated = getNestedValue(imgCandidate, [12, 7, 0], []);
|
|
218
|
+
for (const genImage of generated) {
|
|
219
|
+
const url = getNestedValue(genImage, [0, 3, 3], null);
|
|
220
|
+
if (!url)
|
|
221
|
+
continue;
|
|
222
|
+
images.push({
|
|
223
|
+
kind: 'generated',
|
|
224
|
+
url,
|
|
225
|
+
title: '[Generated Image]',
|
|
226
|
+
alt: '',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { metadata, text, thoughts, images, errorCode };
|
|
231
|
+
}
|
|
232
|
+
export function isGeminiModelUnavailable(errorCode) {
|
|
233
|
+
return errorCode === 1052;
|
|
234
|
+
}
|
|
235
|
+
export async function runGeminiWebOnce(input) {
|
|
236
|
+
const cookieHeader = buildCookieHeader(input.cookieMap);
|
|
237
|
+
const at = await fetchGeminiAccessToken(input.cookieMap);
|
|
238
|
+
const uploaded = [];
|
|
239
|
+
for (const file of input.files ?? []) {
|
|
240
|
+
uploaded.push(await uploadGeminiFile(file));
|
|
241
|
+
}
|
|
242
|
+
const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
|
|
243
|
+
const params = new URLSearchParams();
|
|
244
|
+
params.set('at', at);
|
|
245
|
+
params.set('f.req', fReq);
|
|
246
|
+
const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
redirect: 'follow',
|
|
249
|
+
headers: {
|
|
250
|
+
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
|
|
251
|
+
origin: 'https://gemini.google.com',
|
|
252
|
+
referer: 'https://gemini.google.com/',
|
|
253
|
+
'x-same-domain': '1',
|
|
254
|
+
'user-agent': USER_AGENT,
|
|
255
|
+
cookie: cookieHeader,
|
|
256
|
+
[MODEL_HEADER_NAME]: MODEL_HEADERS[input.model],
|
|
257
|
+
},
|
|
258
|
+
body: params.toString(),
|
|
259
|
+
});
|
|
260
|
+
const rawResponseText = await res.text();
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
return {
|
|
263
|
+
rawResponseText,
|
|
264
|
+
text: '',
|
|
265
|
+
thoughts: null,
|
|
266
|
+
metadata: input.chatMetadata ?? null,
|
|
267
|
+
images: [],
|
|
268
|
+
errorMessage: `Gemini request failed: ${res.status} ${res.statusText}`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const parsed = parseGeminiStreamGenerateResponse(rawResponseText);
|
|
273
|
+
return {
|
|
274
|
+
rawResponseText,
|
|
275
|
+
text: parsed.text ?? '',
|
|
276
|
+
thoughts: parsed.thoughts,
|
|
277
|
+
metadata: parsed.metadata,
|
|
278
|
+
images: parsed.images,
|
|
279
|
+
errorCode: parsed.errorCode,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
let responseJson = null;
|
|
284
|
+
try {
|
|
285
|
+
responseJson = JSON.parse(trimGeminiJsonEnvelope(rawResponseText));
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
responseJson = null;
|
|
289
|
+
}
|
|
290
|
+
const errorCode = extractErrorCode(responseJson);
|
|
291
|
+
return {
|
|
292
|
+
rawResponseText,
|
|
293
|
+
text: '',
|
|
294
|
+
thoughts: null,
|
|
295
|
+
metadata: input.chatMetadata ?? null,
|
|
296
|
+
images: [],
|
|
297
|
+
errorCode: typeof errorCode === 'number' ? errorCode : undefined,
|
|
298
|
+
errorMessage: error instanceof Error ? error.message : String(error ?? ''),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export async function runGeminiWebWithFallback(input) {
|
|
303
|
+
const attempt = await runGeminiWebOnce(input);
|
|
304
|
+
if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== 'gemini-2.5-flash') {
|
|
305
|
+
const fallback = await runGeminiWebOnce({ ...input, model: 'gemini-2.5-flash' });
|
|
306
|
+
return { ...fallback, effectiveModel: 'gemini-2.5-flash' };
|
|
307
|
+
}
|
|
308
|
+
return { ...attempt, effectiveModel: input.model };
|
|
309
|
+
}
|
|
310
|
+
export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath) {
|
|
311
|
+
const generatedOrWeb = output.images.find((img) => img.kind === 'generated') ?? output.images[0];
|
|
312
|
+
if (generatedOrWeb?.url) {
|
|
313
|
+
await downloadGeminiImage(generatedOrWeb.url, cookieMap, outputPath);
|
|
314
|
+
return { saved: true, imageCount: output.images.length };
|
|
315
|
+
}
|
|
316
|
+
const ggdl = extractGgdlUrls(output.rawResponseText);
|
|
317
|
+
if (ggdl[0]) {
|
|
318
|
+
await downloadGeminiImage(ggdl[0], cookieMap, outputPath);
|
|
319
|
+
return { saved: true, imageCount: ggdl.length };
|
|
320
|
+
}
|
|
321
|
+
return { saved: false, imageCount: 0 };
|
|
322
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from './client.js';
|
|
3
|
+
function estimateTokenCount(text) {
|
|
4
|
+
return Math.ceil(text.length / 4);
|
|
5
|
+
}
|
|
6
|
+
function resolveInvocationPath(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return undefined;
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
return undefined;
|
|
12
|
+
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
|
13
|
+
}
|
|
14
|
+
function resolveGeminiWebModel(desiredModel, log) {
|
|
15
|
+
const desired = typeof desiredModel === 'string' ? desiredModel.trim() : '';
|
|
16
|
+
if (!desired)
|
|
17
|
+
return 'gemini-3-pro';
|
|
18
|
+
switch (desired) {
|
|
19
|
+
case 'gemini-3-pro':
|
|
20
|
+
case 'gemini-3.0-pro':
|
|
21
|
+
return 'gemini-3-pro';
|
|
22
|
+
case 'gemini-2.5-pro':
|
|
23
|
+
return 'gemini-2.5-pro';
|
|
24
|
+
case 'gemini-2.5-flash':
|
|
25
|
+
return 'gemini-2.5-flash';
|
|
26
|
+
default:
|
|
27
|
+
if (desired.startsWith('gemini-')) {
|
|
28
|
+
log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
|
|
29
|
+
}
|
|
30
|
+
return 'gemini-3-pro';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function loadGeminiCookiesFromChrome(browserConfig, log) {
|
|
34
|
+
try {
|
|
35
|
+
const mod = (await import('chrome-cookies-secure'));
|
|
36
|
+
const chromeCookies = mod.default ??
|
|
37
|
+
mod;
|
|
38
|
+
const profile = typeof browserConfig?.chromeProfile === 'string' &&
|
|
39
|
+
browserConfig.chromeProfile.trim().length > 0
|
|
40
|
+
? browserConfig.chromeProfile.trim()
|
|
41
|
+
: undefined;
|
|
42
|
+
const sources = [
|
|
43
|
+
'https://gemini.google.com',
|
|
44
|
+
'https://accounts.google.com',
|
|
45
|
+
'https://www.google.com',
|
|
46
|
+
];
|
|
47
|
+
const wantNames = [
|
|
48
|
+
'__Secure-1PSID',
|
|
49
|
+
'__Secure-1PSIDTS',
|
|
50
|
+
'__Secure-1PSIDCC',
|
|
51
|
+
'__Secure-1PAPISID',
|
|
52
|
+
'NID',
|
|
53
|
+
'AEC',
|
|
54
|
+
'SOCS',
|
|
55
|
+
'__Secure-BUCKET',
|
|
56
|
+
'__Secure-ENID',
|
|
57
|
+
'SID',
|
|
58
|
+
'HSID',
|
|
59
|
+
'SSID',
|
|
60
|
+
'APISID',
|
|
61
|
+
'SAPISID',
|
|
62
|
+
'__Secure-3PSID',
|
|
63
|
+
'__Secure-3PSIDTS',
|
|
64
|
+
'__Secure-3PAPISID',
|
|
65
|
+
'SIDCC',
|
|
66
|
+
];
|
|
67
|
+
const cookieMap = {};
|
|
68
|
+
for (const url of sources) {
|
|
69
|
+
const cookies = (await chromeCookies.getCookiesPromised(url, 'puppeteer', profile));
|
|
70
|
+
for (const name of wantNames) {
|
|
71
|
+
if (cookieMap[name])
|
|
72
|
+
continue;
|
|
73
|
+
const matches = cookies.filter((cookie) => cookie.name === name);
|
|
74
|
+
if (matches.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
const preferredDomain = matches.find((cookie) => cookie.domain === '.google.com' && (cookie.path ?? '/') === '/');
|
|
77
|
+
const googleDomain = matches.find((cookie) => (cookie.domain ?? '').endsWith('google.com'));
|
|
78
|
+
const value = (preferredDomain ?? googleDomain ?? matches[0])?.value;
|
|
79
|
+
if (value)
|
|
80
|
+
cookieMap[name] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!cookieMap['__Secure-1PSID'] || !cookieMap['__Secure-1PSIDTS']) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
log?.(`[gemini-web] Loaded Gemini cookies from Chrome (node): ${Object.keys(cookieMap).length} cookie(s).`);
|
|
87
|
+
return cookieMap;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
log?.(`[gemini-web] Failed to load Chrome cookies via node: ${error instanceof Error ? error.message : String(error ?? '')}`);
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function createGeminiWebExecutor(geminiOptions) {
|
|
95
|
+
return async (runOptions) => {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
const log = runOptions.log;
|
|
98
|
+
log?.('[gemini-web] Starting Gemini web executor (TypeScript)');
|
|
99
|
+
const cookieMap = await loadGeminiCookiesFromChrome(runOptions.config, log);
|
|
100
|
+
if (!cookieMap['__Secure-1PSID'] || !cookieMap['__Secure-1PSIDTS']) {
|
|
101
|
+
throw new Error('Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).');
|
|
102
|
+
}
|
|
103
|
+
const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
|
|
104
|
+
const editImagePath = resolveInvocationPath(geminiOptions.editImage);
|
|
105
|
+
const outputPath = resolveInvocationPath(geminiOptions.outputPath);
|
|
106
|
+
const attachmentPaths = (runOptions.attachments ?? []).map((attachment) => attachment.path);
|
|
107
|
+
let prompt = runOptions.prompt;
|
|
108
|
+
if (geminiOptions.aspectRatio && (generateImagePath || editImagePath)) {
|
|
109
|
+
prompt = `${prompt} (aspect ratio: ${geminiOptions.aspectRatio})`;
|
|
110
|
+
}
|
|
111
|
+
if (geminiOptions.youtube) {
|
|
112
|
+
prompt = `${prompt}\n\nYouTube video: ${geminiOptions.youtube}`;
|
|
113
|
+
}
|
|
114
|
+
if (generateImagePath && !editImagePath) {
|
|
115
|
+
prompt = `Generate an image: ${prompt}`;
|
|
116
|
+
}
|
|
117
|
+
const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
|
|
118
|
+
let response;
|
|
119
|
+
if (editImagePath) {
|
|
120
|
+
const intro = await runGeminiWebWithFallback({
|
|
121
|
+
prompt: 'Here is an image to edit',
|
|
122
|
+
files: [editImagePath],
|
|
123
|
+
model,
|
|
124
|
+
cookieMap,
|
|
125
|
+
chatMetadata: null,
|
|
126
|
+
});
|
|
127
|
+
const editPrompt = `Use image generation tool to ${prompt}`;
|
|
128
|
+
const out = await runGeminiWebWithFallback({
|
|
129
|
+
prompt: editPrompt,
|
|
130
|
+
files: attachmentPaths,
|
|
131
|
+
model,
|
|
132
|
+
cookieMap,
|
|
133
|
+
chatMetadata: intro.metadata,
|
|
134
|
+
});
|
|
135
|
+
response = {
|
|
136
|
+
text: out.text ?? null,
|
|
137
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
138
|
+
has_images: false,
|
|
139
|
+
image_count: 0,
|
|
140
|
+
};
|
|
141
|
+
const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
|
|
142
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, resolvedOutputPath);
|
|
143
|
+
response.has_images = imageSave.saved;
|
|
144
|
+
response.image_count = imageSave.imageCount;
|
|
145
|
+
if (!imageSave.saved) {
|
|
146
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (generateImagePath) {
|
|
150
|
+
const out = await runGeminiWebWithFallback({
|
|
151
|
+
prompt,
|
|
152
|
+
files: attachmentPaths,
|
|
153
|
+
model,
|
|
154
|
+
cookieMap,
|
|
155
|
+
chatMetadata: null,
|
|
156
|
+
});
|
|
157
|
+
response = {
|
|
158
|
+
text: out.text ?? null,
|
|
159
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
160
|
+
has_images: false,
|
|
161
|
+
image_count: 0,
|
|
162
|
+
};
|
|
163
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, generateImagePath);
|
|
164
|
+
response.has_images = imageSave.saved;
|
|
165
|
+
response.image_count = imageSave.imageCount;
|
|
166
|
+
if (!imageSave.saved) {
|
|
167
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const out = await runGeminiWebWithFallback({
|
|
172
|
+
prompt,
|
|
173
|
+
files: attachmentPaths,
|
|
174
|
+
model,
|
|
175
|
+
cookieMap,
|
|
176
|
+
chatMetadata: null,
|
|
177
|
+
});
|
|
178
|
+
response = {
|
|
179
|
+
text: out.text ?? null,
|
|
180
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
181
|
+
has_images: out.images.length > 0,
|
|
182
|
+
image_count: out.images.length,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const answerText = response.text ?? '';
|
|
186
|
+
let answerMarkdown = answerText;
|
|
187
|
+
if (geminiOptions.showThoughts && response.thoughts) {
|
|
188
|
+
answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
|
|
189
|
+
}
|
|
190
|
+
if (response.has_images && response.image_count > 0) {
|
|
191
|
+
const imagePath = generateImagePath || outputPath || 'generated.png';
|
|
192
|
+
answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
|
|
193
|
+
}
|
|
194
|
+
const tookMs = Date.now() - startTime;
|
|
195
|
+
log?.(`[gemini-web] Completed in ${tookMs}ms`);
|
|
196
|
+
return {
|
|
197
|
+
answerText,
|
|
198
|
+
answerMarkdown,
|
|
199
|
+
tookMs,
|
|
200
|
+
answerTokens: estimateTokenCount(answerText),
|
|
201
|
+
answerChars: answerText.length,
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createGeminiWebExecutor } from './executor.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"scripts": {
|
|
87
87
|
"docs:list": "tsx scripts/docs-list.ts",
|
|
88
88
|
"build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
|
|
89
|
-
"build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const
|
|
89
|
+
"build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
|
|
90
90
|
"start": "pnpm run build && node ./dist/scripts/run-cli.js",
|
|
91
91
|
"oracle": "pnpm start",
|
|
92
92
|
"check": "pnpm run typecheck",
|
package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources
DELETED
|
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>
|
package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -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>
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import UserNotifications
|
|
3
|
-
|
|
4
|
-
let args = CommandLine.arguments
|
|
5
|
-
// Usage: OracleNotifier <title> <message> [soundName]
|
|
6
|
-
if args.count < 3 {
|
|
7
|
-
fputs("usage: OracleNotifier <title> <message> [soundName]\n", stderr)
|
|
8
|
-
exit(1)
|
|
9
|
-
}
|
|
10
|
-
let title = args[1]
|
|
11
|
-
let message = args[2]
|
|
12
|
-
let soundName = args.count >= 4 ? args[3] : "Glass"
|
|
13
|
-
|
|
14
|
-
let center = UNUserNotificationCenter.current()
|
|
15
|
-
let group = DispatchGroup()
|
|
16
|
-
group.enter()
|
|
17
|
-
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
|
18
|
-
if let error = error {
|
|
19
|
-
fputs("auth error: \(error)\n", stderr)
|
|
20
|
-
group.leave()
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
if !granted {
|
|
24
|
-
fputs("authorization not granted\n", stderr)
|
|
25
|
-
group.leave()
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
let content = UNMutableNotificationContent()
|
|
29
|
-
content.title = title
|
|
30
|
-
content.body = message
|
|
31
|
-
if !soundName.isEmpty {
|
|
32
|
-
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName))
|
|
33
|
-
} else {
|
|
34
|
-
content.sound = UNNotificationSound.default
|
|
35
|
-
}
|
|
36
|
-
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
37
|
-
center.add(request) { addError in
|
|
38
|
-
if let addError = addError {
|
|
39
|
-
fputs("add error: \(addError)\n", stderr)
|
|
40
|
-
}
|
|
41
|
-
group.leave()
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
_ = group.wait(timeout: .now() + 2)
|
|
45
|
-
RunLoop.current.run(until: Date().addingTimeInterval(1))
|
|
@@ -1,24 +0,0 @@
|
|
|
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.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
cd "$(dirname "$0")"
|
|
4
|
-
|
|
5
|
-
ICON_SRC=../../assets-oracle-icon.png
|
|
6
|
-
APP=OracleNotifier.app
|
|
7
|
-
CONTENTS="$APP/Contents"
|
|
8
|
-
MACOS="$CONTENTS/MacOS"
|
|
9
|
-
RESOURCES="$CONTENTS/Resources"
|
|
10
|
-
ICONSET=OracleIcon.iconset
|
|
11
|
-
ICNS=OracleIcon.icns
|
|
12
|
-
IDENTITY="${CODESIGN_ID:-Developer ID Application: Peter Steinberger (Y5PE65HELJ)}"
|
|
13
|
-
ZIP="/tmp/OracleNotifierNotarize.zip"
|
|
14
|
-
|
|
15
|
-
NOTARY_KEY_P8="${APP_STORE_CONNECT_API_KEY_P8:-}"
|
|
16
|
-
NOTARY_KEY_ID="${APP_STORE_CONNECT_KEY_ID:-}"
|
|
17
|
-
NOTARY_ISSUER_ID="${APP_STORE_CONNECT_ISSUER_ID:-}"
|
|
18
|
-
DITTO_BIN=${DITTO_BIN:-/usr/bin/ditto}
|
|
19
|
-
|
|
20
|
-
cleanup() {
|
|
21
|
-
rm -f "$ZIP" /tmp/oracle-notifier-api-key.p8
|
|
22
|
-
}
|
|
23
|
-
trap cleanup EXIT
|
|
24
|
-
|
|
25
|
-
rm -rf "$APP" "$ICONSET" "$ICNS"
|
|
26
|
-
mkdir -p "$MACOS" "$RESOURCES"
|
|
27
|
-
|
|
28
|
-
# Build ICNS from PNG
|
|
29
|
-
mkdir "$ICONSET"
|
|
30
|
-
for sz in 16 32 64 128 256 512; do
|
|
31
|
-
sips -z $sz $sz "$ICON_SRC" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
|
|
32
|
-
sips -z $((sz*2)) $((sz*2)) "$ICON_SRC" --out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
|
|
33
|
-
done
|
|
34
|
-
iconutil -c icns --output "$ICNS" "$ICONSET"
|
|
35
|
-
mv "$ICNS" "$RESOURCES/OracleIcon.icns"
|
|
36
|
-
rm -rf "$ICONSET"
|
|
37
|
-
|
|
38
|
-
# Write Info.plist
|
|
39
|
-
cat > "$CONTENTS/Info.plist" <<'PLIST'
|
|
40
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
41
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
42
|
-
<plist version="1.0">
|
|
43
|
-
<dict>
|
|
44
|
-
<key>CFBundleIdentifier</key>
|
|
45
|
-
<string>com.steipete.oracle.notifier</string>
|
|
46
|
-
<key>CFBundleName</key>
|
|
47
|
-
<string>OracleNotifier</string>
|
|
48
|
-
<key>CFBundleDisplayName</key>
|
|
49
|
-
<string>Oracle Notifier</string>
|
|
50
|
-
<key>CFBundleExecutable</key>
|
|
51
|
-
<string>OracleNotifier</string>
|
|
52
|
-
<key>CFBundleIconFile</key>
|
|
53
|
-
<string>OracleIcon</string>
|
|
54
|
-
<key>CFBundlePackageType</key>
|
|
55
|
-
<string>APPL</string>
|
|
56
|
-
<key>LSMinimumSystemVersion</key>
|
|
57
|
-
<string>13.0</string>
|
|
58
|
-
</dict>
|
|
59
|
-
</plist>
|
|
60
|
-
PLIST
|
|
61
|
-
|
|
62
|
-
# Compile Swift helper (arm64)
|
|
63
|
-
swiftc -target arm64-apple-macos13 -o "$MACOS/OracleNotifier" OracleNotifier.swift -framework Foundation -framework UserNotifications
|
|
64
|
-
|
|
65
|
-
echo "Signing with $IDENTITY"
|
|
66
|
-
if ! codesign --force --deep --options runtime --timestamp --sign "$IDENTITY" "$APP"; then
|
|
67
|
-
echo "codesign failed. Set CODESIGN_ID to a valid Developer ID Application certificate." >&2
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
# Notarize if credentials are provided
|
|
72
|
-
if [[ -n "$NOTARY_KEY_P8" && -n "$NOTARY_KEY_ID" && -n "$NOTARY_ISSUER_ID" ]]; then
|
|
73
|
-
echo "$NOTARY_KEY_P8" | sed 's/\\n/\n/g' > /tmp/oracle-notifier-api-key.p8
|
|
74
|
-
echo "Packaging for notarization"
|
|
75
|
-
"$DITTO_BIN" -c -k --keepParent --sequesterRsrc "$APP" "$ZIP"
|
|
76
|
-
|
|
77
|
-
echo "Submitting for notarization"
|
|
78
|
-
xcrun notarytool submit "$ZIP" \
|
|
79
|
-
--key /tmp/oracle-notifier-api-key.p8 \
|
|
80
|
-
--key-id "$NOTARY_KEY_ID" \
|
|
81
|
-
--issuer "$NOTARY_ISSUER_ID" \
|
|
82
|
-
--wait
|
|
83
|
-
|
|
84
|
-
echo "Stapling ticket"
|
|
85
|
-
xcrun stapler staple "$APP"
|
|
86
|
-
xcrun stapler validate "$APP"
|
|
87
|
-
else
|
|
88
|
-
echo "Skipping notarization (APP_STORE_CONNECT_* env vars not set)."
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
spctl -a -t exec -vv "$APP" || true
|
|
92
|
-
|
|
93
|
-
echo "Built $APP"
|