@andrewting19/oracle 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +300 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1480 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +127 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1386 -0
  14. package/dist/scripts/test-browser.js +106 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1085 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
  20. package/dist/src/browser/actions/attachments.js +1939 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +549 -0
  23. package/dist/src/browser/actions/navigation.js +451 -0
  24. package/dist/src/browser/actions/promptComposer.js +487 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +346 -0
  28. package/dist/src/browser/config.js +105 -0
  29. package/dist/src/browser/constants.js +75 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1826 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +46 -0
  37. package/dist/src/browser/profileState.js +285 -0
  38. package/dist/src/browser/prompt.js +182 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/providerDomFlow.js +17 -0
  41. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  42. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  43. package/dist/src/browser/providers/index.js +2 -0
  44. package/dist/src/browser/reattach.js +189 -0
  45. package/dist/src/browser/reattachHelpers.js +387 -0
  46. package/dist/src/browser/sessionRunner.js +131 -0
  47. package/dist/src/browser/types.js +1 -0
  48. package/dist/src/browser/utils.js +122 -0
  49. package/dist/src/browserMode.js +1 -0
  50. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  51. package/dist/src/cli/bridge/client.js +81 -0
  52. package/dist/src/cli/bridge/codexConfig.js +43 -0
  53. package/dist/src/cli/bridge/doctor.js +115 -0
  54. package/dist/src/cli/bridge/host.js +261 -0
  55. package/dist/src/cli/browserConfig.js +293 -0
  56. package/dist/src/cli/browserDefaults.js +82 -0
  57. package/dist/src/cli/bundleWarnings.js +9 -0
  58. package/dist/src/cli/clipboard.js +10 -0
  59. package/dist/src/cli/detach.js +14 -0
  60. package/dist/src/cli/dryRun.js +109 -0
  61. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  62. package/dist/src/cli/engine.js +41 -0
  63. package/dist/src/cli/errorUtils.js +9 -0
  64. package/dist/src/cli/fileSize.js +11 -0
  65. package/dist/src/cli/format.js +13 -0
  66. package/dist/src/cli/help.js +77 -0
  67. package/dist/src/cli/hiddenAliases.js +22 -0
  68. package/dist/src/cli/markdownBundle.js +21 -0
  69. package/dist/src/cli/markdownRenderer.js +97 -0
  70. package/dist/src/cli/notifier.js +316 -0
  71. package/dist/src/cli/options.js +305 -0
  72. package/dist/src/cli/oscUtils.js +2 -0
  73. package/dist/src/cli/promptRequirement.js +17 -0
  74. package/dist/src/cli/renderFlags.js +9 -0
  75. package/dist/src/cli/renderOutput.js +26 -0
  76. package/dist/src/cli/rootAlias.js +30 -0
  77. package/dist/src/cli/runOptions.js +90 -0
  78. package/dist/src/cli/sessionCommand.js +121 -0
  79. package/dist/src/cli/sessionDisplay.js +670 -0
  80. package/dist/src/cli/sessionLineage.js +60 -0
  81. package/dist/src/cli/sessionRunner.js +630 -0
  82. package/dist/src/cli/sessionTable.js +96 -0
  83. package/dist/src/cli/tagline.js +255 -0
  84. package/dist/src/cli/tui/index.js +499 -0
  85. package/dist/src/cli/writeOutputPath.js +21 -0
  86. package/dist/src/config.js +26 -0
  87. package/dist/src/gemini-web/browserSessionManager.js +81 -0
  88. package/dist/src/gemini-web/client.js +339 -0
  89. package/dist/src/gemini-web/executionClients.js +1 -0
  90. package/dist/src/gemini-web/executionMode.js +16 -0
  91. package/dist/src/gemini-web/executor.js +443 -0
  92. package/dist/src/gemini-web/index.js +1 -0
  93. package/dist/src/gemini-web/types.js +1 -0
  94. package/dist/src/heartbeat.js +43 -0
  95. package/dist/src/mcp/server.js +40 -0
  96. package/dist/src/mcp/tools/consult.js +307 -0
  97. package/dist/src/mcp/tools/sessionResources.js +75 -0
  98. package/dist/src/mcp/tools/sessions.js +114 -0
  99. package/dist/src/mcp/types.js +22 -0
  100. package/dist/src/mcp/utils.js +45 -0
  101. package/dist/src/oracle/background.js +141 -0
  102. package/dist/src/oracle/claude.js +107 -0
  103. package/dist/src/oracle/client.js +235 -0
  104. package/dist/src/oracle/config.js +192 -0
  105. package/dist/src/oracle/errors.js +132 -0
  106. package/dist/src/oracle/files.js +402 -0
  107. package/dist/src/oracle/finishLine.js +34 -0
  108. package/dist/src/oracle/format.js +30 -0
  109. package/dist/src/oracle/fsAdapter.js +10 -0
  110. package/dist/src/oracle/gemini.js +194 -0
  111. package/dist/src/oracle/logging.js +36 -0
  112. package/dist/src/oracle/markdown.js +46 -0
  113. package/dist/src/oracle/modelResolver.js +183 -0
  114. package/dist/src/oracle/multiModelRunner.js +153 -0
  115. package/dist/src/oracle/oscProgress.js +24 -0
  116. package/dist/src/oracle/promptAssembly.js +16 -0
  117. package/dist/src/oracle/request.js +58 -0
  118. package/dist/src/oracle/run.js +628 -0
  119. package/dist/src/oracle/runUtils.js +34 -0
  120. package/dist/src/oracle/tokenEstimate.js +37 -0
  121. package/dist/src/oracle/tokenStats.js +39 -0
  122. package/dist/src/oracle/tokenStringifier.js +24 -0
  123. package/dist/src/oracle/types.js +1 -0
  124. package/dist/src/oracle.js +12 -0
  125. package/dist/src/oracleHome.js +13 -0
  126. package/dist/src/remote/client.js +129 -0
  127. package/dist/src/remote/health.js +113 -0
  128. package/dist/src/remote/remoteServiceConfig.js +31 -0
  129. package/dist/src/remote/server.js +544 -0
  130. package/dist/src/remote/types.js +1 -0
  131. package/dist/src/sessionManager.js +643 -0
  132. package/dist/src/sessionStore.js +56 -0
  133. package/dist/src/version.js +39 -0
  134. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  135. package/dist/vendor/oracle-notifier/README.md +26 -0
  136. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  137. package/package.json +120 -0
  138. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  139. package/vendor/oracle-notifier/README.md +26 -0
  140. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,499 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import kleur from "kleur";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import fs from "node:fs/promises";
7
+ import { DEFAULT_MODEL, MODEL_CONFIGS, } from "../../oracle.js";
8
+ import { renderMarkdownAnsi } from "../markdownRenderer.js";
9
+ import { sessionStore, pruneOldSessions } from "../../sessionStore.js";
10
+ import { performSessionRun } from "../sessionRunner.js";
11
+ import { MAX_RENDER_BYTES, trimBeforeFirstAnswer } from "../sessionDisplay.js";
12
+ import { formatSessionTableHeader, formatSessionTableRow } from "../sessionTable.js";
13
+ import { buildBrowserConfig, resolveBrowserModelLabel } from "../browserConfig.js";
14
+ import { resolveNotificationSettings } from "../notifier.js";
15
+ import { loadUserConfig } from "../../config.js";
16
+ import { resolveConfiguredMaxFileSizeBytes } from "../fileSize.js";
17
+ import { formatTokenCount } from "../../oracle/runUtils.js";
18
+ const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
19
+ const dim = (text) => (isTty() ? kleur.dim(text) : text);
20
+ const RECENT_WINDOW_HOURS = 24;
21
+ const PAGE_SIZE = 10;
22
+ export async function launchTui({ version, printIntro = true }) {
23
+ const userConfig = (await loadUserConfig()).config;
24
+ const rich = isTty();
25
+ let pagingFailures = 0;
26
+ let exitMessageShown = false;
27
+ if (printIntro) {
28
+ if (rich) {
29
+ console.log(chalk.bold("🧿 oracle"), `${version}`, dim("— Whispering your tokens to the silicon sage"));
30
+ }
31
+ else {
32
+ console.log(`🧿 oracle ${version} — Whispering your tokens to the silicon sage`);
33
+ }
34
+ }
35
+ console.log("");
36
+ let showingOlder = false;
37
+ for (;;) {
38
+ const { recent, older, olderTotal } = await fetchSessionBuckets();
39
+ const choices = [];
40
+ const headerLabel = formatSessionTableHeader(isTty());
41
+ // Start with a selectable row so focus never lands on a separator
42
+ choices.push({ name: chalk.bold.green("ask oracle"), value: "__ask__" });
43
+ if (!showingOlder) {
44
+ if (recent.length > 0) {
45
+ choices.push(new inquirer.Separator(headerLabel));
46
+ choices.push(...recent.map(toSessionChoice));
47
+ }
48
+ else if (older.length > 0) {
49
+ // No recent entries; show first page of older.
50
+ choices.push(new inquirer.Separator(headerLabel));
51
+ choices.push(...older.slice(0, PAGE_SIZE).map(toSessionChoice));
52
+ }
53
+ }
54
+ else if (older.length > 0) {
55
+ choices.push(new inquirer.Separator(headerLabel));
56
+ choices.push(...older.map(toSessionChoice));
57
+ }
58
+ choices.push(new inquirer.Separator(" "));
59
+ choices.push(new inquirer.Separator("Actions"));
60
+ choices.push({ name: chalk.bold.green("ask oracle"), value: "__ask__" });
61
+ if (!showingOlder && olderTotal > 0) {
62
+ choices.push({ name: "Older page", value: "__older__" });
63
+ }
64
+ else {
65
+ choices.push({ name: "Newer (recent)", value: "__reset__" });
66
+ }
67
+ choices.push({ name: "Exit", value: "__exit__" });
68
+ const selection = await new Promise((resolve) => {
69
+ const prompt = inquirer.prompt([
70
+ {
71
+ name: "selection",
72
+ type: "select",
73
+ message: "Select a session or action",
74
+ choices,
75
+ pageSize: 16,
76
+ loop: false,
77
+ },
78
+ ]);
79
+ prompt
80
+ .then(({ selection: answer }) => resolve(answer))
81
+ .catch((error) => {
82
+ pagingFailures += 1;
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ if (message.includes("SIGINT") || message.includes("force closed the prompt")) {
85
+ console.log(chalk.green("🧿 Closing the book. See you next prompt."));
86
+ exitMessageShown = true;
87
+ resolve("__exit__");
88
+ return;
89
+ }
90
+ console.error(chalk.red("Paging failed; returning to recent list."), message);
91
+ if (message.includes("setRawMode") || message.includes("EIO") || pagingFailures >= 3) {
92
+ console.error(chalk.red("Terminal input unavailable; exiting TUI."), dim("Try `stty sane` then rerun oracle, or use `oracle recent`."));
93
+ resolve("__exit__");
94
+ return;
95
+ }
96
+ resolve("__reset__");
97
+ });
98
+ });
99
+ if (process.env.ORACLE_DEBUG_TUI === "1") {
100
+ console.error(`[tui] selection=${JSON.stringify(selection)}`);
101
+ }
102
+ pagingFailures = 0;
103
+ if (selection === "__exit__") {
104
+ if (!exitMessageShown) {
105
+ console.log(chalk.green("🧿 Closing the book. See you next prompt."));
106
+ }
107
+ return;
108
+ }
109
+ if (selection === "__ask__") {
110
+ await askOracleFlow(version, userConfig);
111
+ continue;
112
+ }
113
+ if (selection === "__older__") {
114
+ showingOlder = true;
115
+ continue;
116
+ }
117
+ if (selection === "__reset__") {
118
+ showingOlder = false;
119
+ continue;
120
+ }
121
+ await showSessionDetail(selection);
122
+ }
123
+ }
124
+ async function fetchSessionBuckets() {
125
+ const all = await sessionStore.listSessions();
126
+ const cutoff = Date.now() - RECENT_WINDOW_HOURS * 60 * 60 * 1000;
127
+ const recent = all
128
+ .filter((meta) => new Date(meta.createdAt).getTime() >= cutoff)
129
+ .slice(0, PAGE_SIZE);
130
+ const olderAll = all.filter((meta) => new Date(meta.createdAt).getTime() < cutoff);
131
+ const older = olderAll.slice(0, PAGE_SIZE);
132
+ const hasMoreOlder = olderAll.length > PAGE_SIZE;
133
+ if (recent.length === 0 && older.length === 0 && olderAll.length > 0) {
134
+ // No recent entries; fall back to top 10 overall.
135
+ return {
136
+ recent: olderAll.slice(0, PAGE_SIZE),
137
+ older: [],
138
+ hasMoreOlder: olderAll.length > PAGE_SIZE,
139
+ olderTotal: olderAll.length,
140
+ };
141
+ }
142
+ return { recent, older, hasMoreOlder, olderTotal: olderAll.length };
143
+ }
144
+ function toSessionChoice(meta) {
145
+ return {
146
+ name: formatSessionTableRow(meta, { rich: isTty() }),
147
+ value: meta.id,
148
+ };
149
+ }
150
+ async function showSessionDetail(sessionId) {
151
+ for (;;) {
152
+ const meta = await readSessionMetadataSafe(sessionId);
153
+ if (!meta) {
154
+ console.log(chalk.red(`No session found with ID ${sessionId}`));
155
+ return;
156
+ }
157
+ console.clear();
158
+ printSessionHeader(meta);
159
+ if (meta.models && meta.models.length > 0) {
160
+ printModelSummaries(meta.models);
161
+ }
162
+ const prompt = await readStoredPrompt(sessionId);
163
+ if (prompt) {
164
+ console.log(chalk.bold("Prompt:"));
165
+ console.log(renderMarkdownAnsi(prompt));
166
+ console.log(dim("---"));
167
+ }
168
+ const logPath = await getSessionLogPath(sessionId);
169
+ if (logPath) {
170
+ console.log(dim(`Log file: ${logPath}`));
171
+ }
172
+ console.log("");
173
+ await renderSessionLog(sessionId);
174
+ const isRunning = meta.status === "running";
175
+ const modelActions = meta.models?.map((run) => ({
176
+ name: `View ${run.model} log (${run.status})`,
177
+ value: `log:${run.model}`,
178
+ })) ?? [];
179
+ const actions = [
180
+ { name: "View combined log", value: "log:__all__" },
181
+ ...modelActions,
182
+ ...(isRunning ? [{ name: "Refresh", value: "refresh" }] : []),
183
+ { name: "Back", value: "back" },
184
+ ];
185
+ let next;
186
+ try {
187
+ ({ next } = await inquirer.prompt([
188
+ {
189
+ name: "next",
190
+ type: "select",
191
+ message: "Actions",
192
+ choices: actions,
193
+ },
194
+ ]));
195
+ }
196
+ catch (error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ if (message.includes("SIGINT") || message.includes("force closed the prompt")) {
199
+ console.log(chalk.green("🧿 Closing the book. See you next prompt."));
200
+ return;
201
+ }
202
+ console.error(chalk.red("Paging failed; returning to session list."), message);
203
+ return;
204
+ }
205
+ if (next === "back") {
206
+ return;
207
+ }
208
+ if (next === "refresh") {
209
+ continue;
210
+ }
211
+ if (next.startsWith("log:")) {
212
+ const [, target] = next.split(":");
213
+ await renderSessionLog(sessionId, target === "__all__" ? undefined : target);
214
+ }
215
+ }
216
+ }
217
+ async function renderSessionLog(sessionId, model) {
218
+ const raw = model
219
+ ? await sessionStore.readModelLog(sessionId, model)
220
+ : await sessionStore.readLog(sessionId);
221
+ const headerLabel = model ? `Log (${model})` : "Log";
222
+ console.log(chalk.bold(headerLabel));
223
+ const text = trimBeforeFirstAnswer(raw);
224
+ const size = Buffer.byteLength(text, "utf8");
225
+ if (size > MAX_RENDER_BYTES) {
226
+ console.log(chalk.yellow(`Log is large (${size.toLocaleString()} bytes). Rendering raw text; open the log file for full context.`));
227
+ process.stdout.write(text);
228
+ console.log("");
229
+ return;
230
+ }
231
+ if (!text.trim()) {
232
+ console.log(dim("(log is empty)"));
233
+ console.log("");
234
+ return;
235
+ }
236
+ process.stdout.write(renderMarkdownAnsi(text));
237
+ console.log("");
238
+ }
239
+ async function getSessionLogPath(sessionId) {
240
+ try {
241
+ const paths = await sessionStore.getPaths(sessionId);
242
+ return paths.log;
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ function printSessionHeader(meta) {
249
+ console.log(chalk.bold(`Session ${chalk.cyan(meta.id)}`));
250
+ console.log(`${chalk.white("Status:")} ${meta.status}`);
251
+ console.log(`${chalk.white("Created:")} ${meta.createdAt}`);
252
+ if (meta.model) {
253
+ console.log(`${chalk.white("Model:")} ${meta.model}`);
254
+ }
255
+ const mode = meta.mode ?? meta.options?.mode;
256
+ if (mode) {
257
+ console.log(`${chalk.white("Mode:")} ${mode}`);
258
+ }
259
+ if (meta.errorMessage) {
260
+ console.log(chalk.red(`Error: ${meta.errorMessage}`));
261
+ }
262
+ }
263
+ function printModelSummaries(models) {
264
+ if (models.length === 0) {
265
+ return;
266
+ }
267
+ console.log(chalk.bold("Models:"));
268
+ for (const run of models) {
269
+ const usage = run.usage
270
+ ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
271
+ : "";
272
+ console.log(` - ${chalk.cyan(run.model)} — ${run.status}${usage}`);
273
+ }
274
+ console.log("");
275
+ }
276
+ async function askOracleFlow(version, userConfig) {
277
+ const modelChoices = Object.keys(MODEL_CONFIGS);
278
+ const hasApiKey = Boolean(process.env.OPENAI_API_KEY);
279
+ const initialMode = hasApiKey ? "api" : "browser";
280
+ const preferredMode = userConfig.engine ?? initialMode;
281
+ const wizardQuestions = [
282
+ {
283
+ name: "promptInput",
284
+ type: "input",
285
+ message: "Paste your prompt text or a path to a file (leave blank to cancel):",
286
+ },
287
+ ...(hasApiKey
288
+ ? [
289
+ {
290
+ name: "mode",
291
+ type: "select",
292
+ message: "Engine",
293
+ default: preferredMode,
294
+ choices: [
295
+ { name: "API", value: "api" },
296
+ { name: "Browser", value: "browser" },
297
+ ],
298
+ },
299
+ ]
300
+ : [
301
+ {
302
+ name: "mode",
303
+ type: "select",
304
+ message: "Engine",
305
+ default: preferredMode,
306
+ choices: [{ name: "Browser", value: "browser" }],
307
+ },
308
+ ]),
309
+ {
310
+ name: "slug",
311
+ type: "input",
312
+ message: "Optional slug (3–5 words, leave blank for auto):",
313
+ },
314
+ {
315
+ name: "model",
316
+ type: "select",
317
+ message: "Model",
318
+ default: DEFAULT_MODEL,
319
+ choices: modelChoices,
320
+ },
321
+ {
322
+ name: "models",
323
+ type: "checkbox",
324
+ message: "Additional API models to fan out to (optional)",
325
+ choices: modelChoices,
326
+ when: (ans) => ans.mode === "api",
327
+ filter: (values) => Array.isArray(values)
328
+ ? values
329
+ .map((entry) => entry.trim())
330
+ .filter((entry) => modelChoices.includes(entry))
331
+ : [],
332
+ },
333
+ {
334
+ name: "files",
335
+ type: "input",
336
+ message: "Files or globs to attach (comma-separated, optional):",
337
+ filter: (value) => value
338
+ .split(",")
339
+ .map((entry) => entry.trim())
340
+ .filter(Boolean),
341
+ },
342
+ {
343
+ name: "chromeProfile",
344
+ type: "input",
345
+ message: "Chrome profile to reuse cookies from:",
346
+ default: "Default",
347
+ when: (ans) => ans.mode === "browser",
348
+ },
349
+ {
350
+ name: "chromeCookiePath",
351
+ type: "input",
352
+ message: "Cookie DB path (Chromium/Edge, optional):",
353
+ when: (ans) => ans.mode === "browser",
354
+ },
355
+ {
356
+ name: "hideWindow",
357
+ type: "confirm",
358
+ message: "Hide Chrome window (macOS headful only)?",
359
+ default: false,
360
+ when: (ans) => ans.mode === "browser",
361
+ },
362
+ {
363
+ name: "keepBrowser",
364
+ type: "confirm",
365
+ message: "Keep browser open after completion?",
366
+ default: false,
367
+ when: (ans) => ans.mode === "browser",
368
+ },
369
+ ];
370
+ const answers = await inquirer.prompt(wizardQuestions);
371
+ const mode = (answers.mode ?? initialMode);
372
+ const prompt = await resolvePromptInput(answers.promptInput);
373
+ if (!prompt.trim()) {
374
+ console.log(chalk.yellow("Cancelled."));
375
+ return;
376
+ }
377
+ const promptWithSuffix = userConfig.promptSuffix
378
+ ? `${prompt.trim()}\n${userConfig.promptSuffix}`
379
+ : prompt;
380
+ await sessionStore.ensureStorage();
381
+ await pruneOldSessions(userConfig.sessionRetentionHours, (message) => console.log(chalk.dim(message)));
382
+ const normalizedMultiModels = Array.isArray(answers.models) && answers.models.length > 0
383
+ ? Array.from(new Set([answers.model, ...answers.models].filter((entry) => modelChoices.includes(entry))))
384
+ : [answers.model];
385
+ const runOptions = {
386
+ prompt: promptWithSuffix,
387
+ model: answers.model,
388
+ file: answers.files,
389
+ maxFileSizeBytes: resolveConfiguredMaxFileSizeBytes(userConfig, process.env),
390
+ models: normalizedMultiModels.length > 1 ? normalizedMultiModels : undefined,
391
+ slug: answers.slug,
392
+ filesReport: false,
393
+ maxInput: undefined,
394
+ maxOutput: undefined,
395
+ system: undefined,
396
+ silent: false,
397
+ search: undefined,
398
+ preview: false,
399
+ previewMode: undefined,
400
+ apiKey: undefined,
401
+ sessionId: undefined,
402
+ verbose: false,
403
+ heartbeatIntervalMs: undefined,
404
+ browserAttachments: "auto",
405
+ browserInlineFiles: false,
406
+ browserBundleFiles: false,
407
+ background: undefined,
408
+ };
409
+ const browserConfig = mode === "browser"
410
+ ? await buildBrowserConfig({
411
+ browserChromeProfile: answers.chromeProfile,
412
+ browserCookiePath: answers.chromeCookiePath,
413
+ browserHideWindow: answers.hideWindow,
414
+ browserKeepBrowser: answers.keepBrowser,
415
+ browserModelLabel: resolveBrowserModelLabel(undefined, answers.model),
416
+ model: answers.model,
417
+ })
418
+ : undefined;
419
+ const notifications = resolveNotificationSettings({
420
+ cliNotify: undefined,
421
+ cliNotifySound: undefined,
422
+ env: process.env,
423
+ config: userConfig.notify,
424
+ });
425
+ const sessionMeta = await sessionStore.createSession({
426
+ ...runOptions,
427
+ mode,
428
+ browserConfig,
429
+ waitPreference: true,
430
+ }, process.cwd(), notifications);
431
+ const { logLine, writeChunk, stream } = sessionStore.createLogWriter(sessionMeta.id);
432
+ const combinedLog = (message) => {
433
+ if (message) {
434
+ console.log(message);
435
+ logLine(message);
436
+ }
437
+ };
438
+ // Write streamed chunks to the session log; stdout handling is owned by runOracle.
439
+ const combinedWrite = (chunk) => {
440
+ writeChunk(chunk);
441
+ return true;
442
+ };
443
+ console.log(chalk.bold(`Session ${sessionMeta.id} starting...`));
444
+ console.log(dim(`Log path: ${path.join(os.homedir(), ".oracle", "sessions", sessionMeta.id, "output.log")}`));
445
+ try {
446
+ await performSessionRun({
447
+ sessionMeta,
448
+ runOptions: { ...runOptions, sessionId: sessionMeta.id },
449
+ mode,
450
+ browserConfig,
451
+ cwd: process.cwd(),
452
+ log: combinedLog,
453
+ write: combinedWrite,
454
+ version,
455
+ notifications,
456
+ });
457
+ console.log(chalk.green(`Session ${sessionMeta.id} completed.`));
458
+ }
459
+ catch (error) {
460
+ const message = error instanceof Error ? error.message : String(error);
461
+ console.log(chalk.red(`Session ${sessionMeta.id} failed: ${message}`));
462
+ }
463
+ finally {
464
+ stream.end();
465
+ }
466
+ }
467
+ const readSessionMetadataSafe = (sessionId) => sessionStore.readSession(sessionId);
468
+ async function resolvePromptInput(rawInput) {
469
+ const trimmed = rawInput.trim();
470
+ if (!trimmed) {
471
+ return trimmed;
472
+ }
473
+ const asPath = path.resolve(process.cwd(), trimmed);
474
+ try {
475
+ const stats = await fs.stat(asPath);
476
+ if (stats.isFile()) {
477
+ const contents = await fs.readFile(asPath, "utf8");
478
+ return contents;
479
+ }
480
+ }
481
+ catch {
482
+ // not a file; fall through
483
+ }
484
+ return trimmed;
485
+ }
486
+ async function readStoredPrompt(sessionId) {
487
+ const request = await sessionStore.readRequest(sessionId);
488
+ if (request?.prompt && request.prompt.trim().length > 0) {
489
+ return request.prompt;
490
+ }
491
+ const meta = await sessionStore.readSession(sessionId);
492
+ if (meta?.options?.prompt && meta.options.prompt.trim().length > 0) {
493
+ return meta.options.prompt;
494
+ }
495
+ return null;
496
+ }
497
+ // Exported for testing
498
+ export { askOracleFlow, showSessionDetail };
499
+ export { resolveSessionCost as resolveCost } from "../sessionTable.js";
@@ -0,0 +1,21 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { sessionStore } from "../sessionStore.js";
4
+ export function resolveOutputPath(input, cwd) {
5
+ if (!input || input.trim().length === 0) {
6
+ return undefined;
7
+ }
8
+ const expanded = input.startsWith("~/") ? path.join(os.homedir(), input.slice(2)) : input;
9
+ if (expanded === "-" || expanded === "/dev/stdout") {
10
+ return expanded;
11
+ }
12
+ const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
13
+ const sessionsDir = sessionStore.sessionsDir();
14
+ const normalizedSessionsDir = path.resolve(sessionsDir);
15
+ const normalizedTarget = path.resolve(absolute);
16
+ if (normalizedTarget === normalizedSessionsDir ||
17
+ normalizedTarget.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
18
+ throw new Error(`Refusing to write output inside session storage (${normalizedSessionsDir}). Choose another path.`);
19
+ }
20
+ return absolute;
21
+ }
@@ -0,0 +1,26 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import JSON5 from "json5";
4
+ import { getOracleHomeDir } from "./oracleHome.js";
5
+ function resolveConfigPath() {
6
+ return path.join(getOracleHomeDir(), "config.json");
7
+ }
8
+ export async function loadUserConfig() {
9
+ const CONFIG_PATH = resolveConfigPath();
10
+ try {
11
+ const raw = await fs.readFile(CONFIG_PATH, "utf8");
12
+ const parsed = JSON5.parse(raw);
13
+ return { config: parsed ?? {}, path: CONFIG_PATH, loaded: true };
14
+ }
15
+ catch (error) {
16
+ const code = error.code;
17
+ if (code === "ENOENT") {
18
+ return { config: {}, path: CONFIG_PATH, loaded: false };
19
+ }
20
+ console.warn(`Failed to read ${CONFIG_PATH}: ${error instanceof Error ? error.message : String(error)}`);
21
+ return { config: {}, path: CONFIG_PATH, loaded: false };
22
+ }
23
+ }
24
+ export function configPath() {
25
+ return resolveConfigPath();
26
+ }
@@ -0,0 +1,81 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { launchChrome, connectWithNewTab, closeTab } from "../browser/chromeLifecycle.js";
5
+ import { resolveBrowserConfig } from "../browser/config.js";
6
+ import { readDevToolsPort, writeDevToolsActivePort, writeChromePid, cleanupStaleProfileState, verifyDevToolsReachable, } from "../browser/profileState.js";
7
+ export async function openGeminiBrowserSession(input) {
8
+ const { browserConfig, keepBrowserDefault, purpose, log } = input;
9
+ const profileDir = browserConfig?.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile");
10
+ await mkdir(profileDir, { recursive: true });
11
+ const resolvedConfig = resolveBrowserConfig({
12
+ ...browserConfig,
13
+ manualLogin: true,
14
+ manualLoginProfileDir: profileDir,
15
+ keepBrowser: browserConfig?.keepBrowser ?? keepBrowserDefault,
16
+ });
17
+ const keepBrowser = Boolean(resolvedConfig.keepBrowser);
18
+ let port = await readDevToolsPort(profileDir);
19
+ let launchedChrome = null;
20
+ let chromeWasLaunched = false;
21
+ if (port) {
22
+ const probe = await verifyDevToolsReachable({ port });
23
+ if (!probe.ok) {
24
+ log?.(`[gemini-web] Stale DevTools port ${port}; launching fresh Chrome for ${purpose}.`);
25
+ await cleanupStaleProfileState(profileDir, log, { lockRemovalMode: "if_oracle_pid_dead" });
26
+ port = null;
27
+ }
28
+ }
29
+ if (!port) {
30
+ log?.(`[gemini-web] Launching Chrome for ${purpose}.`);
31
+ launchedChrome = await launchChrome(resolvedConfig, profileDir, log ?? (() => { }));
32
+ port = launchedChrome.port;
33
+ chromeWasLaunched = true;
34
+ await writeDevToolsActivePort(profileDir, port);
35
+ if (launchedChrome.pid) {
36
+ await writeChromePid(profileDir, launchedChrome.pid);
37
+ }
38
+ }
39
+ else {
40
+ log?.(`[gemini-web] Reusing Chrome on port ${port} for ${purpose}.`);
41
+ }
42
+ const connection = await connectWithNewTab(port, log ?? (() => { }), undefined);
43
+ const client = connection.client;
44
+ const targetId = connection.targetId;
45
+ const close = async () => {
46
+ if (keepBrowser) {
47
+ try {
48
+ await client.close();
49
+ }
50
+ catch {
51
+ /* ignore */
52
+ }
53
+ return;
54
+ }
55
+ if (targetId && port) {
56
+ await closeTab(port, targetId, log ?? (() => { })).catch(() => undefined);
57
+ }
58
+ try {
59
+ await client.close();
60
+ }
61
+ catch {
62
+ /* ignore */
63
+ }
64
+ if (chromeWasLaunched && launchedChrome) {
65
+ try {
66
+ launchedChrome.kill();
67
+ }
68
+ catch {
69
+ /* ignore */
70
+ }
71
+ await cleanupStaleProfileState(profileDir, log, { lockRemovalMode: "never" }).catch(() => undefined);
72
+ }
73
+ };
74
+ return {
75
+ profileDir,
76
+ port,
77
+ client,
78
+ targetId: targetId ?? undefined,
79
+ close,
80
+ };
81
+ }