@steipete/oracle 0.11.1 → 0.12.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 +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +53 -15
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +41 -7
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/sessionRunner.js +72 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -11
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +1 -0
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +5 -1
- package/package.json +3 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const DEFAULT_DOC_PATHS = [
|
|
4
|
+
"README.md",
|
|
5
|
+
"docs/index.md",
|
|
6
|
+
"docs/agents.md",
|
|
7
|
+
"docs/sessions.md",
|
|
8
|
+
"docs/spec.md",
|
|
9
|
+
"docs/cli-reference.md",
|
|
10
|
+
];
|
|
11
|
+
const FLAG_RE = /(^|[\s`([{|,])(--[a-z][a-z0-9-]*)(?=$|[\s`)[\].,;:|=<>}])/g;
|
|
12
|
+
const SLASH_FLAG_RE = /--[a-z][a-z0-9-]*(?:\/(?:--[a-z][a-z0-9-]*|-[a-z][a-z0-9-]*))+/g;
|
|
13
|
+
const ROOT_ONLY_SECTIONS = new Set(["Core consult flags"]);
|
|
14
|
+
export async function checkDocsFlags({ command, cwd = process.cwd(), paths, }) {
|
|
15
|
+
const availableFlags = collectCommanderFlags(command);
|
|
16
|
+
const rootFlags = collectCommanderFlags(command, { recursive: false });
|
|
17
|
+
const commandFlags = collectCommandFlags(command);
|
|
18
|
+
const docPaths = await resolveDocPaths(cwd, paths);
|
|
19
|
+
const issues = [];
|
|
20
|
+
const checkedFlags = new Set();
|
|
21
|
+
for (const docPath of docPaths) {
|
|
22
|
+
const body = await fs.readFile(path.resolve(cwd, docPath), "utf8");
|
|
23
|
+
for (const reference of extractMarkdownFlagReferences(body)) {
|
|
24
|
+
const { command: commandPath, flag, section } = reference;
|
|
25
|
+
checkedFlags.add(flag);
|
|
26
|
+
const scopedFlags = commandPath
|
|
27
|
+
? (commandFlags.get(commandPath) ?? availableFlags)
|
|
28
|
+
: section && ROOT_ONLY_SECTIONS.has(section)
|
|
29
|
+
? rootFlags
|
|
30
|
+
: availableFlags;
|
|
31
|
+
if (!scopedFlags.has(flag)) {
|
|
32
|
+
issues.push({ file: docPath, flag, section, command: commandPath });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
checkedFiles: docPaths,
|
|
38
|
+
checkedFlags: [...checkedFlags].sort(),
|
|
39
|
+
issues: issues.sort((a, b) => a.file.localeCompare(b.file) || a.flag.localeCompare(b.flag)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function printDocsCheckResult(result, log = console.log) {
|
|
43
|
+
if (result.issues.length === 0) {
|
|
44
|
+
log(`Docs/help check: ok (${result.checkedFlags.length} flags, ${result.checkedFiles.length} files)`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
log("Docs/help drift:");
|
|
48
|
+
for (const issue of result.issues) {
|
|
49
|
+
const scopes = [issue.section, issue.command].filter(Boolean);
|
|
50
|
+
const scope = scopes.length > 0 ? ` (${scopes.join(", ")})` : "";
|
|
51
|
+
log(`- ${issue.file}${scope} mentions ${issue.flag}, but CLI help does not expose ${issue.flag}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function collectCommanderFlags(command, options) {
|
|
55
|
+
const flags = new Set(["--help", "--version"]);
|
|
56
|
+
for (const option of command.options) {
|
|
57
|
+
for (const flag of extractOptionFlags(option.flags)) {
|
|
58
|
+
flags.add(flag);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (options?.recursive === false) {
|
|
62
|
+
return flags;
|
|
63
|
+
}
|
|
64
|
+
for (const subcommand of command.commands) {
|
|
65
|
+
for (const flag of collectCommanderFlags(subcommand)) {
|
|
66
|
+
flags.add(flag);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return flags;
|
|
70
|
+
}
|
|
71
|
+
function collectCommandFlags(command, pathParts = ["oracle"]) {
|
|
72
|
+
const flags = new Map();
|
|
73
|
+
flags.set(pathParts.join(" "), collectCommanderFlags(command, { recursive: false }));
|
|
74
|
+
for (const subcommand of command.commands) {
|
|
75
|
+
for (const [path, subcommandFlags] of collectCommandFlags(subcommand, [
|
|
76
|
+
...pathParts,
|
|
77
|
+
subcommand.name(),
|
|
78
|
+
])) {
|
|
79
|
+
flags.set(path, subcommandFlags);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return flags;
|
|
83
|
+
}
|
|
84
|
+
export function extractMarkdownFlags(markdown) {
|
|
85
|
+
return [
|
|
86
|
+
...new Set(extractMarkdownFlagReferences(markdown).map((reference) => reference.flag)),
|
|
87
|
+
].sort();
|
|
88
|
+
}
|
|
89
|
+
function extractMarkdownFlagReferences(markdown) {
|
|
90
|
+
const references = [];
|
|
91
|
+
let section;
|
|
92
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
93
|
+
const heading = line.match(/^##+\s+(.+?)\s*$/);
|
|
94
|
+
if (heading) {
|
|
95
|
+
section = heading[1];
|
|
96
|
+
}
|
|
97
|
+
const commandPath = extractOracleCommandPath(line);
|
|
98
|
+
const lineFlags = new Set();
|
|
99
|
+
for (const match of line.matchAll(FLAG_RE)) {
|
|
100
|
+
const flag = match[2];
|
|
101
|
+
if (!flag || flag.endsWith("-")) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
lineFlags.add(flag);
|
|
105
|
+
}
|
|
106
|
+
for (const flag of expandSlashFlagReferences(line)) {
|
|
107
|
+
lineFlags.add(flag);
|
|
108
|
+
}
|
|
109
|
+
for (const flag of lineFlags) {
|
|
110
|
+
references.push({ flag, section, command: commandPath });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return references.sort((a, b) => a.flag.localeCompare(b.flag) || (a.section ?? "").localeCompare(b.section ?? ""));
|
|
114
|
+
}
|
|
115
|
+
function extractOracleCommandPath(line) {
|
|
116
|
+
const trimmed = line.trim().replace(/^[$>]\s+/, "");
|
|
117
|
+
if (!trimmed.startsWith("oracle ") && !trimmed.startsWith("npx ")) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const tokens = trimmed.split(/\s+/);
|
|
121
|
+
let oracleIndex = tokens[0] === "oracle" ? 0 : -1;
|
|
122
|
+
if (oracleIndex === -1) {
|
|
123
|
+
oracleIndex = tokens.findIndex((token) => token === "@steipete/oracle");
|
|
124
|
+
}
|
|
125
|
+
if (oracleIndex === -1) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const pathParts = ["oracle"];
|
|
129
|
+
for (const token of tokens.slice(oracleIndex + 1)) {
|
|
130
|
+
if (token.startsWith("-") || !/^[a-z][a-z0-9-]*$/.test(token)) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
pathParts.push(token);
|
|
134
|
+
}
|
|
135
|
+
return pathParts.join(" ");
|
|
136
|
+
}
|
|
137
|
+
function extractOptionFlags(flagsText) {
|
|
138
|
+
const flags = new Set();
|
|
139
|
+
for (const match of flagsText.matchAll(/--\[no-\]([a-z][a-z0-9-]*)/g)) {
|
|
140
|
+
flags.add(`--${match[1]}`);
|
|
141
|
+
flags.add(`--no-${match[1]}`);
|
|
142
|
+
}
|
|
143
|
+
for (const match of flagsText.matchAll(/--[a-z][a-z0-9-]*/g)) {
|
|
144
|
+
flags.add(match[0]);
|
|
145
|
+
}
|
|
146
|
+
return [...flags];
|
|
147
|
+
}
|
|
148
|
+
function expandSlashFlagReferences(line) {
|
|
149
|
+
const flags = [];
|
|
150
|
+
for (const match of line.matchAll(SLASH_FLAG_RE)) {
|
|
151
|
+
const parts = match[0].split("/");
|
|
152
|
+
const base = parts[0];
|
|
153
|
+
const basePrefix = base.slice(0, base.lastIndexOf("-") + 1);
|
|
154
|
+
flags.push(base);
|
|
155
|
+
for (const part of parts.slice(1)) {
|
|
156
|
+
if (part.startsWith("--")) {
|
|
157
|
+
flags.push(part);
|
|
158
|
+
}
|
|
159
|
+
else if (part.startsWith("-") && basePrefix) {
|
|
160
|
+
flags.push(`${basePrefix}${part.slice(1)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return flags;
|
|
165
|
+
}
|
|
166
|
+
async function resolveDocPaths(cwd, paths) {
|
|
167
|
+
const candidates = paths && paths.length > 0 ? paths : DEFAULT_DOC_PATHS;
|
|
168
|
+
const existing = [];
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
try {
|
|
171
|
+
const stat = await fs.stat(path.resolve(cwd, candidate));
|
|
172
|
+
if (stat.isFile()) {
|
|
173
|
+
existing.push(candidate);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
if (paths && paths.length > 0) {
|
|
178
|
+
throw new Error(`Docs check path not found: ${candidate}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (existing.length === 0) {
|
|
183
|
+
throw new Error("No docs found to check. Run from the repo root or pass --docs-path <file>.");
|
|
184
|
+
}
|
|
185
|
+
return existing;
|
|
186
|
+
}
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -12,21 +12,28 @@ export function defaultWaitPreference(model, engine) {
|
|
|
12
12
|
* Precedence:
|
|
13
13
|
* 1) Legacy --browser flag forces browser.
|
|
14
14
|
* 2) Explicit --engine value.
|
|
15
|
-
* 3)
|
|
16
|
-
* 4)
|
|
15
|
+
* 3) Explicit API provider routing flags force API.
|
|
16
|
+
* 4) ORACLE_ENGINE environment override (api|browser).
|
|
17
|
+
* 5) API environment decides: api when set, otherwise browser.
|
|
17
18
|
*/
|
|
18
|
-
export function resolveEngine({ engine, browserFlag, env, }) {
|
|
19
|
+
export function resolveEngine({ engine, browserFlag, apiProviderRequested, env, }) {
|
|
19
20
|
if (browserFlag) {
|
|
20
21
|
return "browser";
|
|
21
22
|
}
|
|
22
23
|
if (engine) {
|
|
23
24
|
return engine;
|
|
24
25
|
}
|
|
26
|
+
if (apiProviderRequested) {
|
|
27
|
+
return "api";
|
|
28
|
+
}
|
|
25
29
|
const envEngine = normalizeEngineMode(env.ORACLE_ENGINE);
|
|
26
30
|
if (envEngine) {
|
|
27
31
|
return envEngine;
|
|
28
32
|
}
|
|
29
|
-
return env
|
|
33
|
+
return hasApiEnvironment(env) ? "api" : "browser";
|
|
34
|
+
}
|
|
35
|
+
function hasApiEnvironment(env) {
|
|
36
|
+
return Boolean(env.OPENAI_API_KEY || env.OPENROUTER_API_KEY);
|
|
30
37
|
}
|
|
31
38
|
function normalizeEngineMode(raw) {
|
|
32
39
|
if (typeof raw !== "string") {
|
package/dist/src/cli/options.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { InvalidArgumentError } from "commander";
|
|
2
|
-
import { parseDuration } from "../
|
|
2
|
+
import { parseDuration } from "../duration.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fg from "fast-glob";
|
|
5
|
-
import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle.js";
|
|
5
|
+
import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle/config.js";
|
|
6
6
|
export function collectPaths(value, previous = []) {
|
|
7
7
|
if (!value) {
|
|
8
8
|
return previous;
|
|
@@ -140,11 +140,17 @@ export function parseTimeoutOption(value) {
|
|
|
140
140
|
const normalized = value.trim().toLowerCase();
|
|
141
141
|
if (normalized === "auto")
|
|
142
142
|
return "auto";
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
if (/^[0-9]+(?:\.[0-9]+)?$/.test(normalized)) {
|
|
144
|
+
const parsed = Number.parseFloat(normalized);
|
|
145
|
+
if (parsed > 0) {
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
146
148
|
}
|
|
147
|
-
|
|
149
|
+
const parsedMs = parseDuration(normalized, Number.NaN);
|
|
150
|
+
if (!Number.isFinite(parsedMs) || parsedMs <= 0) {
|
|
151
|
+
throw new InvalidArgumentError('Timeout must be a positive number of seconds, a duration like "10m", or "auto".');
|
|
152
|
+
}
|
|
153
|
+
return parsedMs / 1000;
|
|
148
154
|
}
|
|
149
155
|
export function parseDurationOption(value, label) {
|
|
150
156
|
if (value == null)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { performance } from "node:perf_hooks";
|
|
4
|
+
const SECRET_VALUE_FLAGS = new Set([
|
|
5
|
+
"--api-key",
|
|
6
|
+
"--browser-follow-up",
|
|
7
|
+
"--browser-inline-cookies",
|
|
8
|
+
"--browser-inline-cookies-file",
|
|
9
|
+
"--message",
|
|
10
|
+
"--prompt",
|
|
11
|
+
"--remote-token",
|
|
12
|
+
"--token",
|
|
13
|
+
"-p",
|
|
14
|
+
]);
|
|
15
|
+
const VALUE_FLAGS = new Set([
|
|
16
|
+
"--aspect",
|
|
17
|
+
"--azure-api-version",
|
|
18
|
+
"--azure-deployment",
|
|
19
|
+
"--azure-endpoint",
|
|
20
|
+
"--base-url",
|
|
21
|
+
"--browser-archive",
|
|
22
|
+
"--browser-attachments",
|
|
23
|
+
"--browser-auto-reattach-delay",
|
|
24
|
+
"--browser-auto-reattach-interval",
|
|
25
|
+
"--browser-auto-reattach-timeout",
|
|
26
|
+
"--browser-bundle-format",
|
|
27
|
+
"--browser-cookie-names",
|
|
28
|
+
"--browser-cookie-path",
|
|
29
|
+
"--browser-cookie-wait",
|
|
30
|
+
"--browser-input-timeout",
|
|
31
|
+
"--browser-max-concurrent-tabs",
|
|
32
|
+
"--browser-model-strategy",
|
|
33
|
+
"--browser-port",
|
|
34
|
+
"--browser-profile-lock-timeout",
|
|
35
|
+
"--browser-recheck-delay",
|
|
36
|
+
"--browser-recheck-timeout",
|
|
37
|
+
"--browser-research",
|
|
38
|
+
"--browser-reuse-wait",
|
|
39
|
+
"--browser-tab",
|
|
40
|
+
"--browser-timeout",
|
|
41
|
+
"--browser-url",
|
|
42
|
+
"--chatgpt-url",
|
|
43
|
+
"--engine",
|
|
44
|
+
"--followup",
|
|
45
|
+
"--followup-model",
|
|
46
|
+
"--heartbeat",
|
|
47
|
+
"--http-timeout",
|
|
48
|
+
"--max-file-size-bytes",
|
|
49
|
+
"--model",
|
|
50
|
+
"--models",
|
|
51
|
+
"--output",
|
|
52
|
+
"--partial",
|
|
53
|
+
"--perf-trace-path",
|
|
54
|
+
"--provider",
|
|
55
|
+
"--remote-chrome",
|
|
56
|
+
"--remote-host",
|
|
57
|
+
"--slug",
|
|
58
|
+
"--timeout",
|
|
59
|
+
"--write-output",
|
|
60
|
+
"--youtube",
|
|
61
|
+
"--zombie-timeout",
|
|
62
|
+
"-e",
|
|
63
|
+
"-m",
|
|
64
|
+
"-s",
|
|
65
|
+
]);
|
|
66
|
+
export function isTraceValueFlag(flag) {
|
|
67
|
+
return SECRET_VALUE_FLAGS.has(flag) || VALUE_FLAGS.has(flag);
|
|
68
|
+
}
|
|
69
|
+
class DisabledPerfTrace {
|
|
70
|
+
mark() { }
|
|
71
|
+
wrapFirstOutput() { }
|
|
72
|
+
flush() { }
|
|
73
|
+
}
|
|
74
|
+
class FilePerfTrace {
|
|
75
|
+
outputPath;
|
|
76
|
+
options;
|
|
77
|
+
events = [];
|
|
78
|
+
wrapped = false;
|
|
79
|
+
firstOutput = false;
|
|
80
|
+
flushed = false;
|
|
81
|
+
constructor(outputPath, options) {
|
|
82
|
+
this.outputPath = outputPath;
|
|
83
|
+
this.options = options;
|
|
84
|
+
}
|
|
85
|
+
mark(name, data) {
|
|
86
|
+
this.events.push({
|
|
87
|
+
name,
|
|
88
|
+
ms: Number(performance.now().toFixed(3)),
|
|
89
|
+
data,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
wrapFirstOutput() {
|
|
93
|
+
if (this.wrapped)
|
|
94
|
+
return;
|
|
95
|
+
this.wrapped = true;
|
|
96
|
+
const wrap = (stream) => {
|
|
97
|
+
const original = stream.write.bind(stream);
|
|
98
|
+
stream.write = ((...args) => {
|
|
99
|
+
if (!this.firstOutput) {
|
|
100
|
+
this.firstOutput = true;
|
|
101
|
+
this.mark("first-output", {
|
|
102
|
+
stream: stream === process.stderr ? "stderr" : "stdout",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return original(...args);
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
wrap(process.stdout);
|
|
109
|
+
wrap(process.stderr);
|
|
110
|
+
}
|
|
111
|
+
flush(exitCode) {
|
|
112
|
+
if (this.flushed)
|
|
113
|
+
return;
|
|
114
|
+
this.flushed = true;
|
|
115
|
+
this.mark("exit", { exitCode: exitCode ?? 0 });
|
|
116
|
+
const payload = {
|
|
117
|
+
version: this.options.version,
|
|
118
|
+
argv: sanitizeTraceArgv(this.options.argv),
|
|
119
|
+
cwd: this.options.cwd ?? process.cwd(),
|
|
120
|
+
pid: process.pid,
|
|
121
|
+
node: process.version,
|
|
122
|
+
timeOrigin: performance.timeOrigin,
|
|
123
|
+
totalMs: Number(performance.now().toFixed(3)),
|
|
124
|
+
events: this.events,
|
|
125
|
+
};
|
|
126
|
+
writeFileSync(this.outputPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function createPerfTrace(options) {
|
|
130
|
+
const envValue = process.env.ORACLE_PERF_TRACE;
|
|
131
|
+
const optionValue = options.value;
|
|
132
|
+
if (!optionValue && !envValue) {
|
|
133
|
+
return new DisabledPerfTrace();
|
|
134
|
+
}
|
|
135
|
+
const rawValue = typeof optionValue === "string" ? optionValue : envValue;
|
|
136
|
+
const outputPath = rawValue && rawValue !== "1" && rawValue !== "true"
|
|
137
|
+
? path.resolve(options.cwd ?? process.cwd(), rawValue)
|
|
138
|
+
: path.join(options.cwd ?? process.cwd(), `.oracle-perf-${new Date().toISOString().replace(/[:.]/g, "-")}-${process.pid}.json`);
|
|
139
|
+
const trace = new FilePerfTrace(outputPath, options);
|
|
140
|
+
trace.wrapFirstOutput();
|
|
141
|
+
trace.mark("cli-module-ready");
|
|
142
|
+
return trace;
|
|
143
|
+
}
|
|
144
|
+
export function deriveDetachedPerfTraceEnv(value, sessionId) {
|
|
145
|
+
const trimmed = value?.trim();
|
|
146
|
+
if (!trimmed || trimmed === "1" || trimmed.toLowerCase() === "true")
|
|
147
|
+
return value;
|
|
148
|
+
const safeSessionId = sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
149
|
+
const lastSlash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
|
|
150
|
+
const lastDot = trimmed.lastIndexOf(".");
|
|
151
|
+
if (lastDot > lastSlash) {
|
|
152
|
+
return `${trimmed.slice(0, lastDot)}.${safeSessionId}${trimmed.slice(lastDot)}`;
|
|
153
|
+
}
|
|
154
|
+
return `${trimmed}.${safeSessionId}.json`;
|
|
155
|
+
}
|
|
156
|
+
export function resolveDetachedPerfTraceEnv(cliValue, envValue, sessionId) {
|
|
157
|
+
if (typeof cliValue === "string") {
|
|
158
|
+
return deriveDetachedPerfTraceEnv(cliValue, sessionId);
|
|
159
|
+
}
|
|
160
|
+
if (cliValue === true) {
|
|
161
|
+
return "1";
|
|
162
|
+
}
|
|
163
|
+
return deriveDetachedPerfTraceEnv(envValue, sessionId);
|
|
164
|
+
}
|
|
165
|
+
export function buildDetachedPerfTraceEnv(env, cliValue, sessionId) {
|
|
166
|
+
const nextEnv = { ...env };
|
|
167
|
+
const traceValue = resolveDetachedPerfTraceEnv(cliValue, env.ORACLE_PERF_TRACE, sessionId);
|
|
168
|
+
if (traceValue) {
|
|
169
|
+
nextEnv.ORACLE_PERF_TRACE = traceValue;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
delete nextEnv.ORACLE_PERF_TRACE;
|
|
173
|
+
}
|
|
174
|
+
return nextEnv;
|
|
175
|
+
}
|
|
176
|
+
export function sanitizeTraceArgv(argv) {
|
|
177
|
+
const sanitized = [];
|
|
178
|
+
let redactNext = false;
|
|
179
|
+
let valueNext = false;
|
|
180
|
+
let afterDoubleDash = false;
|
|
181
|
+
for (const arg of argv) {
|
|
182
|
+
if (afterDoubleDash) {
|
|
183
|
+
sanitized.push("[redacted-positional]");
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (arg === "--") {
|
|
187
|
+
sanitized.push(arg);
|
|
188
|
+
afterDoubleDash = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (redactNext) {
|
|
192
|
+
sanitized.push("[redacted]");
|
|
193
|
+
redactNext = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (valueNext) {
|
|
197
|
+
sanitized.push(redactPotentialSecret(arg));
|
|
198
|
+
valueNext = false;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const equalsIndex = arg.indexOf("=");
|
|
202
|
+
const flag = equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
|
|
203
|
+
if (equalsIndex >= 0 && SECRET_VALUE_FLAGS.has(flag)) {
|
|
204
|
+
sanitized.push(`${flag}=[redacted]`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (arg.startsWith("-p") && arg.length > 2) {
|
|
208
|
+
sanitized.push("-p[redacted]");
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (equalsIndex >= 0) {
|
|
212
|
+
sanitized.push(`${flag}=${redactPotentialSecret(arg.slice(equalsIndex + 1))}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (SECRET_VALUE_FLAGS.has(arg)) {
|
|
216
|
+
sanitized.push(arg);
|
|
217
|
+
redactNext = true;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
221
|
+
sanitized.push(arg);
|
|
222
|
+
valueNext = true;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!arg.startsWith("-")) {
|
|
226
|
+
sanitized.push("[redacted-positional]");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
sanitized.push(arg);
|
|
230
|
+
}
|
|
231
|
+
return sanitized;
|
|
232
|
+
}
|
|
233
|
+
function redactPotentialSecret(value) {
|
|
234
|
+
return value
|
|
235
|
+
.replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [redacted]")
|
|
236
|
+
.replace(/:\/\/([^:/?#\s]+):([^@/?#\s]+)@/g, "://$1:[redacted]@")
|
|
237
|
+
.replace(/([?&](?:access_)?token=)[^&#\s]+/gi, "$1[redacted]")
|
|
238
|
+
.replace(/([?&](?:api[-_]?key|auth|authorization|password|secret)=)[^&#\s]+/gi, "$1[redacted]")
|
|
239
|
+
.replace(/\bsk-(?:ant-|or-)?[A-Za-z0-9_-]{8,}\b/g, "sk-...[redacted]")
|
|
240
|
+
.replace(/\bxai-[A-Za-z0-9_-]{8,}\b/g, "xai-...[redacted]")
|
|
241
|
+
.replace(/\bAIza[0-9A-Za-z_-]{8,}\b/g, "AIza...[redacted]");
|
|
242
|
+
}
|
|
@@ -10,6 +10,8 @@ export function shouldRequirePrompt(rawArgs, options) {
|
|
|
10
10
|
options.execSession ||
|
|
11
11
|
options.status ||
|
|
12
12
|
options.debugHelp ||
|
|
13
|
+
options.route ||
|
|
14
|
+
options.preflight ||
|
|
13
15
|
firstArg === "status" ||
|
|
14
16
|
firstArg === "session");
|
|
15
17
|
const requiresPrompt = options.renderMarkdown || Boolean(options.preview) || Boolean(options.dryRun) || !bypassPrompt;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { DEFAULT_MODEL } from "../oracle/config.js";
|
|
3
|
+
import { resolveApiModel } from "./options.js";
|
|
4
|
+
import { loadUserConfig } from "../config.js";
|
|
5
|
+
import { buildProviderRoutePlan } from "../oracle/providerRoutePlan.js";
|
|
6
|
+
export async function runProviderDoctor(options) {
|
|
7
|
+
if (!options.providers) {
|
|
8
|
+
console.log("Run `oracle doctor --providers` to inspect API provider readiness.");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const { config: userConfig } = await loadUserConfig();
|
|
12
|
+
const providerMode = resolveProviderMode(options);
|
|
13
|
+
const azure = resolveAzureOptions(options, userConfig);
|
|
14
|
+
const models = resolveModels(options, userConfig);
|
|
15
|
+
const plans = models.map((model) => buildProviderRoutePlan({
|
|
16
|
+
model,
|
|
17
|
+
providerMode,
|
|
18
|
+
azure,
|
|
19
|
+
baseUrl: options.baseUrl ?? userConfig.apiBaseUrl,
|
|
20
|
+
env: process.env,
|
|
21
|
+
}));
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify({ providers: plans }, null, 2));
|
|
24
|
+
process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
printProviderPlans(plans);
|
|
28
|
+
process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
|
|
29
|
+
}
|
|
30
|
+
export function printProviderPlans(plans, { title = "Provider readiness" } = {}) {
|
|
31
|
+
console.log(chalk.bold(title));
|
|
32
|
+
console.log("");
|
|
33
|
+
for (const plan of plans) {
|
|
34
|
+
const status = plan.ok ? chalk.green("ok") : chalk.red("not ready");
|
|
35
|
+
console.log(`${plan.model}: ${status}`);
|
|
36
|
+
console.log(chalk.dim(` provider: ${plan.providerLabel}`));
|
|
37
|
+
console.log(chalk.dim(` base: ${plan.base || "(none)"}`));
|
|
38
|
+
console.log(chalk.dim(` key: ${plan.keyPreview}`));
|
|
39
|
+
if (plan.isAzureOpenAI || plan.azureDeploymentName) {
|
|
40
|
+
console.log(chalk.dim(` azure deployment: ${plan.azureDeploymentName ?? "none"}`));
|
|
41
|
+
}
|
|
42
|
+
if (plan.azureNote) {
|
|
43
|
+
console.log(chalk.dim(` azure: ${plan.azureNote}`));
|
|
44
|
+
}
|
|
45
|
+
if (plan.error) {
|
|
46
|
+
console.log(chalk.dim(` error: ${plan.error}`));
|
|
47
|
+
}
|
|
48
|
+
console.log("");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveModels(options, userConfig) {
|
|
52
|
+
const entries = Array.isArray(options.models) && options.models.length > 0
|
|
53
|
+
? options.models
|
|
54
|
+
: typeof options.models === "string" && options.models.trim().length > 0
|
|
55
|
+
? options.models
|
|
56
|
+
.split(",")
|
|
57
|
+
.map((entry) => entry.trim())
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
: [options.model ?? userConfig.model ?? DEFAULT_MODEL];
|
|
60
|
+
return Array.from(new Set(entries.map((entry) => resolveApiModel(entry))));
|
|
61
|
+
}
|
|
62
|
+
function resolveProviderMode(options) {
|
|
63
|
+
const provider = options.provider ?? "auto";
|
|
64
|
+
if (provider === "azure" && options.azure === false) {
|
|
65
|
+
throw new Error("--provider azure cannot be combined with --no-azure.");
|
|
66
|
+
}
|
|
67
|
+
if (options.azure === false) {
|
|
68
|
+
return "openai";
|
|
69
|
+
}
|
|
70
|
+
return provider;
|
|
71
|
+
}
|
|
72
|
+
function resolveAzureOptions(options, userConfig) {
|
|
73
|
+
const endpoint = firstNonEmpty(options.azureEndpoint, process.env.AZURE_OPENAI_ENDPOINT, userConfig.azure?.endpoint);
|
|
74
|
+
if (!endpoint) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
endpoint,
|
|
79
|
+
deployment: firstNonEmpty(options.azureDeployment, process.env.AZURE_OPENAI_DEPLOYMENT, userConfig.azure?.deployment),
|
|
80
|
+
apiVersion: firstNonEmpty(options.azureApiVersion, process.env.AZURE_OPENAI_API_VERSION, userConfig.azure?.apiVersion),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function firstNonEmpty(...values) {
|
|
84
|
+
return values.find((value) => value?.trim());
|
|
85
|
+
}
|