@ottocode/sdk 0.1.234 → 0.1.236
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/package.json +1 -1
- package/src/config/src/manager.ts +55 -0
- package/src/config/src/paths.ts +27 -0
- package/src/core/src/terminals/manager.ts +0 -14
- package/src/core/src/tools/loader.ts +1 -4
- package/src/core/src/utils/debug.ts +23 -29
- package/src/core/src/utils/logger.ts +155 -26
- package/src/index.ts +9 -1
- package/src/prompts/src/base.txt +6 -0
- package/src/prompts/src/debug.ts +4 -93
- package/src/prompts/src/providers.ts +0 -19
- package/src/providers/src/openai-oauth-client.ts +255 -7
- package/src/skills/loader.ts +2 -12
- package/src/types/src/config.ts +2 -0
package/package.json
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
getGlobalConfigDir,
|
|
11
11
|
getGlobalConfigPath,
|
|
12
|
+
getGlobalDebugDir,
|
|
13
|
+
getGlobalDebugLogPath,
|
|
14
|
+
getGlobalDebugSessionsDir,
|
|
12
15
|
getLocalDataDir,
|
|
13
16
|
joinPath,
|
|
14
17
|
} from './paths.ts';
|
|
@@ -20,6 +23,14 @@ import {
|
|
|
20
23
|
|
|
21
24
|
export type Scope = 'global' | 'local';
|
|
22
25
|
|
|
26
|
+
export type DebugConfig = {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
scopes: string[];
|
|
29
|
+
logPath: string;
|
|
30
|
+
sessionsDir: string;
|
|
31
|
+
debugDir: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
23
34
|
export async function read(projectRoot?: string) {
|
|
24
35
|
const cfg = await loadConfig(projectRoot);
|
|
25
36
|
const auth = await getAllAuth(projectRoot);
|
|
@@ -111,6 +122,50 @@ export async function writeDefaults(
|
|
|
111
122
|
await Bun.write(globalPath, JSON.stringify(next, null, 2));
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
export async function readDebugConfig(
|
|
126
|
+
projectRoot?: string,
|
|
127
|
+
): Promise<DebugConfig> {
|
|
128
|
+
const cfg = await loadConfig(projectRoot);
|
|
129
|
+
return {
|
|
130
|
+
enabled: cfg.debugEnabled === true,
|
|
131
|
+
scopes: Array.isArray(cfg.debugScopes)
|
|
132
|
+
? cfg.debugScopes.filter(
|
|
133
|
+
(scope): scope is string =>
|
|
134
|
+
typeof scope === 'string' && scope.trim().length > 0,
|
|
135
|
+
)
|
|
136
|
+
: [],
|
|
137
|
+
logPath: getGlobalDebugLogPath(),
|
|
138
|
+
sessionsDir: getGlobalDebugSessionsDir(),
|
|
139
|
+
debugDir: getGlobalDebugDir(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function writeDebugConfig(
|
|
144
|
+
updates: Partial<{
|
|
145
|
+
enabled: boolean;
|
|
146
|
+
scopes: string[];
|
|
147
|
+
}>,
|
|
148
|
+
) {
|
|
149
|
+
const globalPath = getGlobalConfigPath();
|
|
150
|
+
const existing = await readJsonFile(globalPath);
|
|
151
|
+
const next: Record<string, unknown> = { ...(existing ?? {}) };
|
|
152
|
+
|
|
153
|
+
if (updates.enabled !== undefined) {
|
|
154
|
+
next.debugEnabled = updates.enabled;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (updates.scopes !== undefined) {
|
|
158
|
+
next.debugScopes = updates.scopes;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const base = getGlobalConfigDir();
|
|
162
|
+
try {
|
|
163
|
+
const { promises: fs } = await import('node:fs');
|
|
164
|
+
await fs.mkdir(base, { recursive: true }).catch(() => {});
|
|
165
|
+
} catch {}
|
|
166
|
+
await Bun.write(globalPath, JSON.stringify(next, null, 2));
|
|
167
|
+
}
|
|
168
|
+
|
|
114
169
|
async function readJsonFile(
|
|
115
170
|
filePath: string,
|
|
116
171
|
): Promise<Record<string, unknown> | undefined> {
|
package/src/config/src/paths.ts
CHANGED
|
@@ -79,6 +79,33 @@ export function getGlobalCommandsDir(): string {
|
|
|
79
79
|
return joinPath(getGlobalConfigDir(), 'commands');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function getGlobalDebugDir(): string {
|
|
83
|
+
return joinPath(getGlobalConfigDir(), 'debug');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getGlobalDebugLogPath(): string {
|
|
87
|
+
return joinPath(getGlobalDebugDir(), 'latest.log');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getGlobalDebugSessionsDir(): string {
|
|
91
|
+
return joinPath(getGlobalDebugDir(), 'sessions');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getSessionDebugLogPath(sessionId: string): string {
|
|
95
|
+
return joinPath(getGlobalDebugSessionsDir(), `${sessionId}.log`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getSessionDebugDetailsLogPath(sessionId: string): string {
|
|
99
|
+
return joinPath(getGlobalDebugSessionsDir(), `${sessionId}.details.log`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getSessionSystemPromptPath(sessionId: string): string {
|
|
103
|
+
return joinPath(
|
|
104
|
+
getGlobalDebugSessionsDir(),
|
|
105
|
+
`${sessionId}.system-prompt.txt`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
export function getLocalDataDir(projectRoot: string): string {
|
|
83
110
|
return joinPath(projectRoot, '.otto');
|
|
84
111
|
}
|
|
@@ -29,14 +29,6 @@ export class TerminalManager {
|
|
|
29
29
|
const id = this.generateId();
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
|
-
logger.debug('TerminalManager: creating terminal', {
|
|
33
|
-
id,
|
|
34
|
-
command: options.command,
|
|
35
|
-
args: options.args,
|
|
36
|
-
cwd: options.cwd,
|
|
37
|
-
purpose: options.purpose,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
32
|
const ptyOptions: PtyOptions = {
|
|
41
33
|
name: 'xterm-256color',
|
|
42
34
|
cols: 80,
|
|
@@ -52,10 +44,6 @@ export class TerminalManager {
|
|
|
52
44
|
|
|
53
45
|
const pty = spawnPty(options.command, options.args || [], ptyOptions);
|
|
54
46
|
|
|
55
|
-
logger.debug('TerminalManager: PTY created', {
|
|
56
|
-
pid: pty.pid,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
47
|
const terminal = new Terminal(id, pty, options);
|
|
60
48
|
|
|
61
49
|
terminal.onExit((_exitCode) => {
|
|
@@ -68,8 +56,6 @@ export class TerminalManager {
|
|
|
68
56
|
|
|
69
57
|
this.terminals.set(id, terminal);
|
|
70
58
|
|
|
71
|
-
logger.debug('TerminalManager: terminal added to map', { id });
|
|
72
|
-
|
|
73
59
|
return terminal;
|
|
74
60
|
} catch (error) {
|
|
75
61
|
logger.error('TerminalManager: failed to create terminal', error);
|
|
@@ -179,10 +179,7 @@ export async function discoverProjectTools(
|
|
|
179
179
|
try {
|
|
180
180
|
const plugin = await loadPlugin(absPath, folder, projectRoot);
|
|
181
181
|
if (plugin) tools.set(plugin.name, plugin.tool);
|
|
182
|
-
} catch
|
|
183
|
-
if (process.env.OTTO_DEBUG_TOOLS === '1')
|
|
184
|
-
console.error('Failed to load tool', absPath, err);
|
|
185
|
-
}
|
|
182
|
+
} catch {}
|
|
186
183
|
}
|
|
187
184
|
}
|
|
188
185
|
// Fallback: manual directory scan
|
|
@@ -1,40 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { getGlobalConfigPath } from '../../../config/src/paths.ts';
|
|
2
3
|
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
type DebugSettings = {
|
|
5
|
+
debugEnabled?: boolean;
|
|
6
|
+
debugScopes?: unknown;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function envEnabled(keys: string[]): boolean {
|
|
16
|
-
for (const key of keys) {
|
|
17
|
-
const raw = typeof process !== 'undefined' ? process.env?.[key] : undefined;
|
|
18
|
-
if (!raw) continue;
|
|
19
|
-
const trimmed = raw.trim().toLowerCase();
|
|
20
|
-
if (!trimmed) continue;
|
|
21
|
-
if (TRUTHY.has(trimmed) || trimmed === 'all') return true;
|
|
9
|
+
function readDebugSettings(): DebugSettings {
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(getGlobalConfigPath(), 'utf-8');
|
|
12
|
+
const parsed = JSON.parse(raw) as DebugSettings;
|
|
13
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
14
|
+
} catch {
|
|
15
|
+
return {};
|
|
22
16
|
}
|
|
23
|
-
return false;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
export function isDebugEnabled(): boolean {
|
|
27
|
-
|
|
28
|
-
if (typeof globalFlag === 'boolean') {
|
|
29
|
-
return globalFlag;
|
|
30
|
-
}
|
|
31
|
-
return envEnabled(['OTTO_DEBUG', 'DEBUG_OTTO']);
|
|
20
|
+
return readDebugSettings().debugEnabled === true;
|
|
32
21
|
}
|
|
33
22
|
|
|
34
23
|
export function isTraceEnabled(): boolean {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDebugScopes(): string[] {
|
|
28
|
+
const scopes = readDebugSettings().debugScopes;
|
|
29
|
+
if (!Array.isArray(scopes)) return [];
|
|
30
|
+
return scopes.filter(
|
|
31
|
+
(scope): scope is string =>
|
|
32
|
+
typeof scope === 'string' && scope.trim().length > 0,
|
|
33
|
+
);
|
|
40
34
|
}
|
|
@@ -1,34 +1,165 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { isDebugEnabled, isTraceEnabled, getDebugScopes } from './debug.ts';
|
|
4
|
+
import {
|
|
5
|
+
getGlobalDebugLogPath,
|
|
6
|
+
getSessionDebugDetailsLogPath,
|
|
7
|
+
getSessionDebugLogPath,
|
|
8
|
+
} from '../../../config/src/paths.ts';
|
|
2
9
|
|
|
3
10
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
4
11
|
|
|
12
|
+
const ANSI_RESET = '\x1b[0m';
|
|
13
|
+
const ANSI_DIM = '\x1b[2m';
|
|
14
|
+
const ANSI_CYAN = '\x1b[36m';
|
|
15
|
+
const ANSI_BLUE = '\x1b[34m';
|
|
16
|
+
const ANSI_GREEN = '\x1b[32m';
|
|
17
|
+
const ANSI_YELLOW = '\x1b[33m';
|
|
18
|
+
const ANSI_RED = '\x1b[31m';
|
|
19
|
+
|
|
5
20
|
function safeHasMeta(
|
|
6
21
|
meta?: Record<string, unknown>,
|
|
7
22
|
): meta is Record<string, unknown> {
|
|
8
23
|
return Boolean(meta && Object.keys(meta).length);
|
|
9
24
|
}
|
|
10
25
|
|
|
11
|
-
|
|
12
|
-
if (!isDebugEnabled()) return;
|
|
26
|
+
function getDebugLogFilePath(): string | undefined {
|
|
27
|
+
if (!isDebugEnabled()) return undefined;
|
|
28
|
+
return getGlobalDebugLogPath();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSessionLogFilePath(
|
|
32
|
+
meta?: Record<string, unknown>,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
if (!isDebugEnabled()) return undefined;
|
|
35
|
+
if (meta?.debugDetail === true) return undefined;
|
|
36
|
+
const sessionId = meta?.sessionId;
|
|
37
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return undefined;
|
|
38
|
+
return getSessionDebugLogPath(sessionId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSessionDetailsLogFilePath(
|
|
42
|
+
meta?: Record<string, unknown>,
|
|
43
|
+
): string | undefined {
|
|
44
|
+
if (!isDebugEnabled()) return undefined;
|
|
45
|
+
const sessionId = meta?.sessionId;
|
|
46
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return undefined;
|
|
47
|
+
return getSessionDebugDetailsLogPath(sessionId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shouldWriteDebugLog(message: string): boolean {
|
|
51
|
+
if (!isDebugEnabled()) return false;
|
|
52
|
+
const scopes = getDebugScopes();
|
|
53
|
+
if (!scopes.length) return true;
|
|
54
|
+
const match = message.match(/^\[([^\]]+)\]/);
|
|
55
|
+
if (!match?.[1]) return true;
|
|
56
|
+
return scopes.includes(match[1]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function serializeLogMeta(meta?: Record<string, unknown>): string {
|
|
60
|
+
if (!safeHasMeta(meta)) return '';
|
|
13
61
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
62
|
+
const sanitized = { ...meta };
|
|
63
|
+
delete sanitized.debugDetail;
|
|
64
|
+
return Object.keys(sanitized).length ? ` ${JSON.stringify(sanitized)}` : '';
|
|
65
|
+
} catch {
|
|
66
|
+
return ' [unserializable-meta]';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function colorizeLine(line: string, level: LogLevel): string {
|
|
71
|
+
const levelColor =
|
|
72
|
+
level === 'debug'
|
|
73
|
+
? ANSI_CYAN
|
|
74
|
+
: level === 'info'
|
|
75
|
+
? ANSI_BLUE
|
|
76
|
+
: level === 'warn'
|
|
77
|
+
? ANSI_YELLOW
|
|
78
|
+
: ANSI_RED;
|
|
79
|
+
const scopeMatch = line.match(
|
|
80
|
+
/\[(debug|info|warn|error|timing)\]\s+\[([^\]]+)\]/i,
|
|
81
|
+
);
|
|
82
|
+
if (!scopeMatch) {
|
|
83
|
+
return `${levelColor}${line}${ANSI_RESET}`;
|
|
84
|
+
}
|
|
85
|
+
const rest = line.slice(24);
|
|
86
|
+
return `${ANSI_DIM}${line.slice(0, 24)}${ANSI_RESET}${rest
|
|
87
|
+
.replace(scopeMatch[1], `${levelColor}${scopeMatch[1]}${ANSI_RESET}`)
|
|
88
|
+
.replace(
|
|
89
|
+
`[${scopeMatch[2]}]`,
|
|
90
|
+
`${ANSI_GREEN}[${scopeMatch[2]}]${ANSI_RESET}`,
|
|
91
|
+
)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printLine(
|
|
95
|
+
level: LogLevel,
|
|
96
|
+
line: string,
|
|
97
|
+
meta?: Record<string, unknown>,
|
|
98
|
+
) {
|
|
99
|
+
const colored = colorizeLine(line, level);
|
|
100
|
+
if (safeHasMeta(meta)) {
|
|
101
|
+
if (level === 'warn') console.warn(colored, meta);
|
|
102
|
+
else if (level === 'error') console.error(colored, meta);
|
|
103
|
+
else console.log(colored, meta);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (level === 'warn') console.warn(colored);
|
|
107
|
+
else if (level === 'error') console.error(colored);
|
|
108
|
+
else console.log(colored);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeLogLine(line: string, meta?: Record<string, unknown>) {
|
|
112
|
+
const suffix = serializeLogMeta(meta);
|
|
113
|
+
const fullLine = `${new Date().toISOString()} ${line}${suffix}`;
|
|
114
|
+
const logFile = getDebugLogFilePath();
|
|
115
|
+
|
|
116
|
+
if (logFile) {
|
|
117
|
+
try {
|
|
118
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
119
|
+
appendFileSync(logFile, `${fullLine}\n`, 'utf-8');
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore file logging errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sessionLogFile = getSessionLogFilePath(meta);
|
|
126
|
+
if (sessionLogFile) {
|
|
127
|
+
try {
|
|
128
|
+
mkdirSync(dirname(sessionLogFile), { recursive: true });
|
|
129
|
+
appendFileSync(sessionLogFile, `${fullLine}\n`, 'utf-8');
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore file logging errors
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sessionDetailsLogFile = getSessionDetailsLogFilePath(meta);
|
|
136
|
+
if (sessionDetailsLogFile) {
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(dirname(sessionDetailsLogFile), { recursive: true });
|
|
139
|
+
appendFileSync(sessionDetailsLogFile, `${fullLine}\n`, 'utf-8');
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore file logging errors
|
|
18
142
|
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return fullLine;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function debug(message: string, meta?: Record<string, unknown>): void {
|
|
149
|
+
if (!shouldWriteDebugLog(message)) return;
|
|
150
|
+
try {
|
|
151
|
+
const line = writeLogLine(`[debug] ${message}`, meta);
|
|
152
|
+
printLine('debug', line, meta);
|
|
19
153
|
} catch {
|
|
20
154
|
// ignore logging errors
|
|
21
155
|
}
|
|
22
156
|
}
|
|
23
157
|
|
|
24
158
|
export function info(message: string, meta?: Record<string, unknown>): void {
|
|
25
|
-
if (!
|
|
159
|
+
if (!shouldWriteDebugLog(message) && !isTraceEnabled()) return;
|
|
26
160
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} else {
|
|
30
|
-
console.log(`[info] ${message}`);
|
|
31
|
-
}
|
|
161
|
+
const line = writeLogLine(`[info] ${message}`, meta);
|
|
162
|
+
printLine('info', line, meta);
|
|
32
163
|
} catch {
|
|
33
164
|
// ignore logging errors
|
|
34
165
|
}
|
|
@@ -36,11 +167,8 @@ export function info(message: string, meta?: Record<string, unknown>): void {
|
|
|
36
167
|
|
|
37
168
|
export function warn(message: string, meta?: Record<string, unknown>): void {
|
|
38
169
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} else {
|
|
42
|
-
console.warn(`[warn] ${message}`);
|
|
43
|
-
}
|
|
170
|
+
const line = writeLogLine(`[warn] ${message}`, meta);
|
|
171
|
+
printLine('warn', line, meta);
|
|
44
172
|
} catch {
|
|
45
173
|
// ignore logging errors
|
|
46
174
|
}
|
|
@@ -91,9 +219,11 @@ export function error(
|
|
|
91
219
|
}
|
|
92
220
|
|
|
93
221
|
if (safeHasMeta(logMeta)) {
|
|
94
|
-
|
|
222
|
+
const line = writeLogLine(`[error] ${message}`, logMeta);
|
|
223
|
+
printLine('error', line, logMeta);
|
|
95
224
|
} else {
|
|
96
|
-
|
|
225
|
+
const line = writeLogLine(`[error] ${message}`);
|
|
226
|
+
printLine('error', line);
|
|
97
227
|
}
|
|
98
228
|
} catch (logErr) {
|
|
99
229
|
try {
|
|
@@ -136,12 +266,11 @@ export function time(label: string): Timer {
|
|
|
136
266
|
finished = true;
|
|
137
267
|
const duration = nowMs() - start;
|
|
138
268
|
try {
|
|
139
|
-
const base =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
269
|
+
const base = writeLogLine(
|
|
270
|
+
`[timing] ${label} ${duration.toFixed(1)}ms`,
|
|
271
|
+
meta,
|
|
272
|
+
);
|
|
273
|
+
printLine('info', base, meta);
|
|
145
274
|
} catch {
|
|
146
275
|
// ignore timing log errors
|
|
147
276
|
}
|
package/src/index.ts
CHANGED
|
@@ -177,6 +177,12 @@ export {
|
|
|
177
177
|
getGlobalAgentsDir,
|
|
178
178
|
getGlobalToolsDir,
|
|
179
179
|
getGlobalCommandsDir,
|
|
180
|
+
getGlobalDebugDir,
|
|
181
|
+
getGlobalDebugLogPath,
|
|
182
|
+
getGlobalDebugSessionsDir,
|
|
183
|
+
getSessionDebugLogPath,
|
|
184
|
+
getSessionDebugDetailsLogPath,
|
|
185
|
+
getSessionSystemPromptPath,
|
|
180
186
|
getSecureAuthPath,
|
|
181
187
|
getSecureBaseDir,
|
|
182
188
|
getSecureOAuthDir,
|
|
@@ -187,12 +193,14 @@ export {
|
|
|
187
193
|
isAuthorized,
|
|
188
194
|
ensureEnv,
|
|
189
195
|
writeDefaults as setConfig,
|
|
196
|
+
readDebugConfig,
|
|
197
|
+
writeDebugConfig,
|
|
190
198
|
writeAuth,
|
|
191
199
|
removeAuth as removeConfig,
|
|
192
200
|
getOnboardingComplete,
|
|
193
201
|
setOnboardingComplete,
|
|
194
202
|
} from './config/src/manager.ts';
|
|
195
|
-
export type { Scope } from './config/src/manager.ts';
|
|
203
|
+
export type { Scope, DebugConfig } from './config/src/manager.ts';
|
|
196
204
|
|
|
197
205
|
// =======================
|
|
198
206
|
// Prompts (from internal prompts module)
|
package/src/prompts/src/base.txt
CHANGED
|
@@ -3,6 +3,12 @@ You are a helpful, concise assistant.
|
|
|
3
3
|
- Do not print pseudo tool calls like `call:tool{}`; invoke tools directly.
|
|
4
4
|
- Use sensible default filenames when needed.
|
|
5
5
|
- Prefer minimal, precise outputs and actionable steps.
|
|
6
|
+
- Keep user-facing responses short, scannable, and suitable for a terminal UI.
|
|
7
|
+
- Default to a concise teammate tone unless the user explicitly asks for detail.
|
|
8
|
+
- Avoid long retrospective narratives of every step you took.
|
|
9
|
+
- Do not restate tool outputs unless they are necessary for the user.
|
|
10
|
+
- Final responses should usually be 3-6 short bullets or a few short paragraphs.
|
|
11
|
+
- Focus on outcome, key files, verification, and only the most relevant next step.
|
|
6
12
|
|
|
7
13
|
## Finish Tool - CRITICAL
|
|
8
14
|
|
package/src/prompts/src/debug.ts
CHANGED
|
@@ -1,104 +1,15 @@
|
|
|
1
|
-
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
2
|
-
|
|
3
|
-
const SYNONYMS: Record<string, string> = {
|
|
4
|
-
debug: 'log',
|
|
5
|
-
logs: 'log',
|
|
6
|
-
logging: 'log',
|
|
7
|
-
trace: 'log',
|
|
8
|
-
verbose: 'log',
|
|
9
|
-
log: 'log',
|
|
10
|
-
time: 'timing',
|
|
11
|
-
timing: 'timing',
|
|
12
|
-
timings: 'timing',
|
|
13
|
-
perf: 'timing',
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type DebugConfig = { flags: Set<string> };
|
|
17
|
-
|
|
18
|
-
let cachedConfig: DebugConfig | null = null;
|
|
19
|
-
|
|
20
|
-
function isTruthy(raw: string | undefined): boolean {
|
|
21
|
-
if (!raw) return false;
|
|
22
|
-
const trimmed = raw.trim().toLowerCase();
|
|
23
|
-
if (!trimmed) return false;
|
|
24
|
-
return TRUTHY.has(trimmed) || trimmed === 'all';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeToken(token: string): string {
|
|
28
|
-
const trimmed = token.trim().toLowerCase();
|
|
29
|
-
if (!trimmed) return '';
|
|
30
|
-
if (TRUTHY.has(trimmed) || trimmed === 'all') return 'all';
|
|
31
|
-
return SYNONYMS[trimmed] ?? trimmed;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseDebugConfig(): DebugConfig {
|
|
35
|
-
const flags = new Set<string>();
|
|
36
|
-
const sources = [process.env.OTTO_DEBUG, process.env.DEBUG_OTTO];
|
|
37
|
-
let sawValue = false;
|
|
38
|
-
for (const raw of sources) {
|
|
39
|
-
if (typeof raw !== 'string') continue;
|
|
40
|
-
const trimmed = raw.trim();
|
|
41
|
-
if (!trimmed) continue;
|
|
42
|
-
sawValue = true;
|
|
43
|
-
const tokens = trimmed.split(/[\s,]+/);
|
|
44
|
-
let matched = false;
|
|
45
|
-
for (const token of tokens) {
|
|
46
|
-
const normalized = normalizeToken(token);
|
|
47
|
-
if (!normalized) continue;
|
|
48
|
-
matched = true;
|
|
49
|
-
flags.add(normalized);
|
|
50
|
-
}
|
|
51
|
-
if (!matched && isTruthy(trimmed)) flags.add('all');
|
|
52
|
-
}
|
|
53
|
-
if (isTruthy(process.env.OTTO_DEBUG_TIMING)) flags.add('timing');
|
|
54
|
-
if (!flags.size && sawValue) flags.add('all');
|
|
55
|
-
return { flags };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function getDebugConfig(): DebugConfig {
|
|
59
|
-
if (!cachedConfig) cachedConfig = parseDebugConfig();
|
|
60
|
-
return cachedConfig;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
1
|
export function isDebugEnabled(flag?: string): boolean {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (flag) return config.flags.has(flag);
|
|
67
|
-
return config.flags.has('log');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function debugLog(...args: unknown[]) {
|
|
71
|
-
if (!isDebugEnabled('log')) return;
|
|
72
|
-
try {
|
|
73
|
-
console.log('[debug]', ...args);
|
|
74
|
-
} catch {}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function nowMs(): number {
|
|
78
|
-
const perf = (globalThis as { performance?: { now?: () => number } })
|
|
79
|
-
.performance;
|
|
80
|
-
if (perf && typeof perf.now === 'function') return perf.now();
|
|
81
|
-
return Date.now();
|
|
2
|
+
void flag;
|
|
3
|
+
return false;
|
|
82
4
|
}
|
|
83
5
|
|
|
84
6
|
type Timer = { end(meta?: Record<string, unknown>): void };
|
|
85
7
|
|
|
86
8
|
export function time(label: string): Timer {
|
|
87
|
-
|
|
88
|
-
return { end() {} };
|
|
89
|
-
}
|
|
90
|
-
const start = nowMs();
|
|
91
|
-
let finished = false;
|
|
9
|
+
void label;
|
|
92
10
|
return {
|
|
93
11
|
end(meta?: Record<string, unknown>) {
|
|
94
|
-
|
|
95
|
-
finished = true;
|
|
96
|
-
const duration = nowMs() - start;
|
|
97
|
-
try {
|
|
98
|
-
const line = `[timing] ${label} ${duration.toFixed(1)}ms`;
|
|
99
|
-
if (meta && Object.keys(meta).length) console.log(line, meta);
|
|
100
|
-
else console.log(line);
|
|
101
|
-
} catch {}
|
|
12
|
+
void meta;
|
|
102
13
|
},
|
|
103
14
|
};
|
|
104
15
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { debugLog } from './debug.ts';
|
|
2
1
|
import {
|
|
3
2
|
getModelFamily,
|
|
4
3
|
getModelInfo,
|
|
@@ -91,9 +90,6 @@ export async function providerBasePrompt(
|
|
|
91
90
|
const modelText = await readIfExists(modelPath);
|
|
92
91
|
if (!modelText) continue;
|
|
93
92
|
const promptType = `model:${sanitized}`;
|
|
94
|
-
debugLog(
|
|
95
|
-
`[provider] prompt: ${promptType} (${modelText.length} chars) from ${modelPath}`,
|
|
96
|
-
);
|
|
97
93
|
return { prompt: modelText, resolvedType: promptType };
|
|
98
94
|
}
|
|
99
95
|
}
|
|
@@ -101,9 +97,6 @@ export async function providerBasePrompt(
|
|
|
101
97
|
for (const providerPath of providerPaths) {
|
|
102
98
|
const providerText = await readIfExists(providerPath);
|
|
103
99
|
if (!providerText) continue;
|
|
104
|
-
debugLog(
|
|
105
|
-
`[provider] prompt: custom:${id} (${providerText.length} chars) from ${providerPath}`,
|
|
106
|
-
);
|
|
107
100
|
return { prompt: providerText, resolvedType: `custom:${id}` };
|
|
108
101
|
}
|
|
109
102
|
|
|
@@ -112,49 +105,37 @@ export async function providerBasePrompt(
|
|
|
112
105
|
if (info?.ownedBy) {
|
|
113
106
|
const family = getModelFamily(id, modelId);
|
|
114
107
|
const result = promptForFamily(family);
|
|
115
|
-
debugLog(
|
|
116
|
-
`[provider] prompt: ownedBy:${info.ownedBy} (via ${id}/${modelId}, ${result.length} chars)`,
|
|
117
|
-
);
|
|
118
108
|
return { prompt: result, resolvedType: family ?? info.ownedBy };
|
|
119
109
|
}
|
|
120
110
|
|
|
121
111
|
const family = getModelFamily(id, modelId);
|
|
122
112
|
if (family) {
|
|
123
113
|
const result = promptForFamily(family);
|
|
124
|
-
debugLog(
|
|
125
|
-
`[provider] prompt: family:${family} (via ${id}/${modelId}, ${result.length} chars)`,
|
|
126
|
-
);
|
|
127
114
|
return { prompt: result, resolvedType: family };
|
|
128
115
|
}
|
|
129
116
|
}
|
|
130
117
|
|
|
131
118
|
if (id === 'openai') {
|
|
132
119
|
const result = PROVIDER_OPENAI.trim();
|
|
133
|
-
debugLog(`[provider] prompt: openai (${result.length} chars)`);
|
|
134
120
|
return { prompt: result, resolvedType: 'openai' };
|
|
135
121
|
}
|
|
136
122
|
if (id === 'anthropic') {
|
|
137
123
|
const result = PROVIDER_ANTHROPIC.trim();
|
|
138
|
-
debugLog(`[provider] prompt: anthropic (${result.length} chars)`);
|
|
139
124
|
return { prompt: result, resolvedType: 'anthropic' };
|
|
140
125
|
}
|
|
141
126
|
if (id === 'google') {
|
|
142
127
|
const result = PROVIDER_GOOGLE.trim();
|
|
143
|
-
debugLog(`[provider] prompt: google (${result.length} chars)`);
|
|
144
128
|
return { prompt: result, resolvedType: 'google' };
|
|
145
129
|
}
|
|
146
130
|
if (id === 'moonshot') {
|
|
147
131
|
const result = PROVIDER_MOONSHOT.trim();
|
|
148
|
-
debugLog(`[provider] prompt: moonshot (${result.length} chars)`);
|
|
149
132
|
return { prompt: result, resolvedType: 'moonshot' };
|
|
150
133
|
}
|
|
151
134
|
if (id === 'zai' || id === 'zai-coding') {
|
|
152
135
|
const result = PROVIDER_GLM.trim();
|
|
153
|
-
debugLog(`[provider] prompt: glm (${result.length} chars)`);
|
|
154
136
|
return { prompt: result, resolvedType: 'glm' };
|
|
155
137
|
}
|
|
156
138
|
|
|
157
139
|
const result = PROVIDER_DEFAULT.trim();
|
|
158
|
-
debugLog(`[provider] prompt: default (${result.length} chars)`);
|
|
159
140
|
return { prompt: result, resolvedType: 'default' };
|
|
160
141
|
}
|
|
@@ -2,6 +2,10 @@ import { createOpenAI } from '@ai-sdk/openai';
|
|
|
2
2
|
import type { OAuth } from '../../types/src/index.ts';
|
|
3
3
|
import { refreshOpenAIToken } from '../../auth/src/openai-oauth.ts';
|
|
4
4
|
import { setAuth, getAuth } from '../../auth/src/index.ts';
|
|
5
|
+
import {
|
|
6
|
+
debug as loggerDebug,
|
|
7
|
+
warn as loggerWarn,
|
|
8
|
+
} from '../../core/src/utils/logger.ts';
|
|
5
9
|
import os from 'node:os';
|
|
6
10
|
|
|
7
11
|
const CODEX_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
@@ -11,12 +15,48 @@ const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
|
11
15
|
const TOKEN_REFRESH_MAX_RETRIES = 2;
|
|
12
16
|
const TOKEN_REFRESH_RETRY_DELAY_MS = 1000;
|
|
13
17
|
|
|
18
|
+
type OpenAIOAuthSessionState = {
|
|
19
|
+
responseId?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
status?: string;
|
|
22
|
+
incompleteReason?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const openAIOAuthSessionState = new Map<string, OpenAIOAuthSessionState>();
|
|
26
|
+
|
|
14
27
|
export type OpenAIOAuthConfig = {
|
|
15
28
|
oauth: OAuth;
|
|
16
29
|
projectRoot?: string;
|
|
17
30
|
sessionId?: string;
|
|
18
31
|
};
|
|
19
32
|
|
|
33
|
+
function shouldDebugOpenAIOAuth() {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function logOpenAIOAuth(message: string) {
|
|
38
|
+
if (shouldDebugOpenAIOAuth()) {
|
|
39
|
+
loggerDebug(`[openai-oauth] ${message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldUsePreviousResponseId() {
|
|
44
|
+
return process.env.OTTO_OPENAI_OAUTH_PREVIOUS_RESPONSE_ID === '1';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearOpenAIOAuthSessionState(sessionId?: string) {
|
|
48
|
+
if (sessionId) {
|
|
49
|
+
openAIOAuthSessionState.delete(sessionId);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
openAIOAuthSessionState.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getOpenAIOAuthSessionState(sessionId: string) {
|
|
56
|
+
const state = openAIOAuthSessionState.get(sessionId);
|
|
57
|
+
return state ? { ...state } : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
20
60
|
function sleep(ms: number) {
|
|
21
61
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
62
|
}
|
|
@@ -65,7 +105,7 @@ async function ensureValidToken(
|
|
|
65
105
|
accountId: updated.accountId,
|
|
66
106
|
};
|
|
67
107
|
} catch {
|
|
68
|
-
|
|
108
|
+
loggerWarn(
|
|
69
109
|
'[openai-oauth] Token refresh failed after retries, falling back to current token',
|
|
70
110
|
);
|
|
71
111
|
return { oauth, access: oauth.access, accountId: oauth.accountId };
|
|
@@ -83,6 +123,187 @@ function rewriteUrl(url: string): string {
|
|
|
83
123
|
return url;
|
|
84
124
|
}
|
|
85
125
|
|
|
126
|
+
function readSessionState(sessionId?: string) {
|
|
127
|
+
if (!sessionId) return undefined;
|
|
128
|
+
return openAIOAuthSessionState.get(sessionId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeSessionState(sessionId: string, next: OpenAIOAuthSessionState) {
|
|
132
|
+
openAIOAuthSessionState.set(sessionId, next);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function rewriteRequestBody(
|
|
136
|
+
body: string,
|
|
137
|
+
sessionId?: string,
|
|
138
|
+
): { body: string; previousResponseId?: string; model?: string } {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
141
|
+
const model = typeof parsed.model === 'string' ? parsed.model : undefined;
|
|
142
|
+
if (!sessionId) {
|
|
143
|
+
return { body, model };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const prior = readSessionState(sessionId);
|
|
147
|
+
if (
|
|
148
|
+
prior?.responseId &&
|
|
149
|
+
!parsed.previous_response_id &&
|
|
150
|
+
(!prior.model || !model || prior.model === model)
|
|
151
|
+
) {
|
|
152
|
+
if (!shouldUsePreviousResponseId()) {
|
|
153
|
+
logOpenAIOAuth(
|
|
154
|
+
`not injecting previous_response_id=${prior.responseId} for session=${sessionId} model=${model ?? 'unknown'} because Codex HTTP backend rejects it; enable OTTO_OPENAI_OAUTH_PREVIOUS_RESPONSE_ID=1 only for validation`,
|
|
155
|
+
);
|
|
156
|
+
return { body, model };
|
|
157
|
+
}
|
|
158
|
+
parsed.previous_response_id = prior.responseId;
|
|
159
|
+
logOpenAIOAuth(
|
|
160
|
+
`injecting previous_response_id=${prior.responseId} for session=${sessionId} model=${model ?? 'unknown'}`,
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
body: JSON.stringify(parsed),
|
|
164
|
+
previousResponseId: prior.responseId,
|
|
165
|
+
model,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { body, model };
|
|
170
|
+
} catch {
|
|
171
|
+
return { body };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function previewText(value: unknown, maxLength = 240): string | undefined {
|
|
176
|
+
if (typeof value !== 'string') return undefined;
|
|
177
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
178
|
+
if (!normalized) return undefined;
|
|
179
|
+
return normalized.length > maxLength
|
|
180
|
+
? `${normalized.slice(0, maxLength)}…`
|
|
181
|
+
: normalized;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function summarizeRequestBody(body: string): string {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
187
|
+
const input = Array.isArray(parsed.input) ? parsed.input : [];
|
|
188
|
+
const systemMessages = input.filter((item) => {
|
|
189
|
+
if (!item || typeof item !== 'object') return false;
|
|
190
|
+
const role = (item as Record<string, unknown>).role;
|
|
191
|
+
return role === 'system';
|
|
192
|
+
});
|
|
193
|
+
const systemPreview = previewText(
|
|
194
|
+
(systemMessages[0] as Record<string, unknown> | undefined)?.content,
|
|
195
|
+
);
|
|
196
|
+
const instructionsPreview = previewText(parsed.instructions);
|
|
197
|
+
return [
|
|
198
|
+
`model=${typeof parsed.model === 'string' ? parsed.model : 'unknown'}`,
|
|
199
|
+
`instructionsPresent=${typeof parsed.instructions === 'string'}`,
|
|
200
|
+
`instructionsPreview=${instructionsPreview ?? 'none'}`,
|
|
201
|
+
`inputCount=${input.length}`,
|
|
202
|
+
`systemMessageCount=${systemMessages.length}`,
|
|
203
|
+
`firstSystemPreview=${systemPreview ?? 'none'}`,
|
|
204
|
+
`previousResponseId=${typeof parsed.previous_response_id === 'string' ? parsed.previous_response_id : 'none'}`,
|
|
205
|
+
].join(' ');
|
|
206
|
+
} catch {
|
|
207
|
+
return 'unparseable-body';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function trackResponseEvent(data: string, sessionId?: string) {
|
|
212
|
+
if (!sessionId) return;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const parsed = JSON.parse(data) as Record<string, unknown>;
|
|
216
|
+
const type = typeof parsed.type === 'string' ? parsed.type : undefined;
|
|
217
|
+
const response =
|
|
218
|
+
parsed.response && typeof parsed.response === 'object'
|
|
219
|
+
? (parsed.response as Record<string, unknown>)
|
|
220
|
+
: undefined;
|
|
221
|
+
const responseId =
|
|
222
|
+
typeof response?.id === 'string'
|
|
223
|
+
? response.id
|
|
224
|
+
: typeof parsed.response_id === 'string'
|
|
225
|
+
? parsed.response_id
|
|
226
|
+
: undefined;
|
|
227
|
+
const responseModel =
|
|
228
|
+
typeof response?.model === 'string' ? response.model : undefined;
|
|
229
|
+
const responseStatus =
|
|
230
|
+
typeof response?.status === 'string' ? response.status : undefined;
|
|
231
|
+
const incompleteReason =
|
|
232
|
+
response?.incomplete_details &&
|
|
233
|
+
typeof response.incomplete_details === 'object' &&
|
|
234
|
+
typeof (response.incomplete_details as Record<string, unknown>).reason ===
|
|
235
|
+
'string'
|
|
236
|
+
? ((response.incomplete_details as Record<string, unknown>)
|
|
237
|
+
.reason as string)
|
|
238
|
+
: undefined;
|
|
239
|
+
|
|
240
|
+
if (responseId) {
|
|
241
|
+
const prior = readSessionState(sessionId);
|
|
242
|
+
writeSessionState(sessionId, {
|
|
243
|
+
responseId,
|
|
244
|
+
model: responseModel ?? prior?.model,
|
|
245
|
+
status: responseStatus ?? type,
|
|
246
|
+
incompleteReason,
|
|
247
|
+
});
|
|
248
|
+
logOpenAIOAuth(
|
|
249
|
+
`tracked response event type=${type ?? 'unknown'} responseId=${responseId} session=${sessionId} status=${responseStatus ?? 'unknown'} incompleteReason=${incompleteReason ?? 'none'}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// ignore non-JSON data chunks
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function trackResponsesStream(
|
|
258
|
+
response: Response,
|
|
259
|
+
sessionId?: string,
|
|
260
|
+
): Response {
|
|
261
|
+
if (!response.body || !sessionId) {
|
|
262
|
+
return response;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const decoder = new TextDecoder();
|
|
266
|
+
const encoder = new TextEncoder();
|
|
267
|
+
let buffer = '';
|
|
268
|
+
|
|
269
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
270
|
+
transform(chunk, controller) {
|
|
271
|
+
buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
|
|
272
|
+
let boundary = buffer.indexOf('\n\n');
|
|
273
|
+
while (boundary !== -1) {
|
|
274
|
+
const rawEvent = buffer.slice(0, boundary);
|
|
275
|
+
buffer = buffer.slice(boundary + 2);
|
|
276
|
+
|
|
277
|
+
const dataLines: string[] = [];
|
|
278
|
+
for (const line of rawEvent.split('\n')) {
|
|
279
|
+
if (line.startsWith('data:')) {
|
|
280
|
+
dataLines.push(line.slice('data:'.length).trimStart());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const data = dataLines.join('\n');
|
|
284
|
+
if (data && data !== '[DONE]') {
|
|
285
|
+
trackResponseEvent(data, sessionId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
controller.enqueue(encoder.encode(`${rawEvent}\n\n`));
|
|
289
|
+
boundary = buffer.indexOf('\n\n');
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
flush(controller) {
|
|
293
|
+
buffer += decoder.decode().replace(/\r\n/g, '\n');
|
|
294
|
+
if (buffer.length > 0) {
|
|
295
|
+
controller.enqueue(encoder.encode(buffer));
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return new Response(response.body.pipeThrough(transform), {
|
|
301
|
+
status: response.status,
|
|
302
|
+
statusText: response.statusText,
|
|
303
|
+
headers: response.headers,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
86
307
|
function buildHeaders(
|
|
87
308
|
init: RequestInit | undefined,
|
|
88
309
|
accessToken: string,
|
|
@@ -124,20 +345,44 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
|
|
|
124
345
|
? input.href
|
|
125
346
|
: input.url;
|
|
126
347
|
const targetUrl = rewriteUrl(originalUrl);
|
|
348
|
+
const isResponsesRequest = targetUrl === CODEX_RESPONSES_URL;
|
|
349
|
+
let requestInit = init;
|
|
350
|
+
let requestModel: string | undefined;
|
|
351
|
+
if (isResponsesRequest && typeof init?.body === 'string') {
|
|
352
|
+
const rewritten = rewriteRequestBody(init.body, config.sessionId);
|
|
353
|
+
requestModel = rewritten.model;
|
|
354
|
+
requestInit =
|
|
355
|
+
rewritten.body !== init.body ? { ...init, body: rewritten.body } : init;
|
|
356
|
+
logOpenAIOAuth(
|
|
357
|
+
`request payload summary: ${summarizeRequestBody(requestInit?.body && typeof requestInit.body === 'string' ? requestInit.body : init.body)}`,
|
|
358
|
+
);
|
|
359
|
+
if (config.sessionId && requestModel) {
|
|
360
|
+
const prior = readSessionState(config.sessionId);
|
|
361
|
+
writeSessionState(config.sessionId, {
|
|
362
|
+
responseId: prior?.responseId,
|
|
363
|
+
model: requestModel,
|
|
364
|
+
status: prior?.status,
|
|
365
|
+
incompleteReason: prior?.incompleteReason,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
127
369
|
|
|
128
370
|
const headers = buildHeaders(
|
|
129
|
-
|
|
371
|
+
requestInit,
|
|
130
372
|
validated.access,
|
|
131
373
|
validated.accountId,
|
|
132
374
|
config.sessionId,
|
|
133
375
|
);
|
|
134
376
|
|
|
135
377
|
const response = await fetch(targetUrl, {
|
|
136
|
-
...
|
|
378
|
+
...requestInit,
|
|
137
379
|
headers,
|
|
138
380
|
// @ts-expect-error Bun-specific fetch option
|
|
139
381
|
timeout: false,
|
|
140
382
|
});
|
|
383
|
+
const trackedResponse = isResponsesRequest
|
|
384
|
+
? trackResponsesStream(response, config.sessionId)
|
|
385
|
+
: response;
|
|
141
386
|
|
|
142
387
|
if (response.status === 401) {
|
|
143
388
|
try {
|
|
@@ -155,18 +400,21 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
|
|
|
155
400
|
}
|
|
156
401
|
|
|
157
402
|
const retryHeaders = buildHeaders(
|
|
158
|
-
|
|
403
|
+
requestInit,
|
|
159
404
|
currentOAuth.access,
|
|
160
405
|
currentOAuth.accountId,
|
|
161
406
|
config.sessionId,
|
|
162
407
|
);
|
|
163
408
|
|
|
164
|
-
|
|
165
|
-
...
|
|
409
|
+
const retryResponse = await fetch(targetUrl, {
|
|
410
|
+
...requestInit,
|
|
166
411
|
headers: retryHeaders,
|
|
167
412
|
// @ts-expect-error Bun-specific fetch option
|
|
168
413
|
timeout: false,
|
|
169
414
|
});
|
|
415
|
+
return isResponsesRequest
|
|
416
|
+
? trackResponsesStream(retryResponse, config.sessionId)
|
|
417
|
+
: retryResponse;
|
|
170
418
|
} catch {
|
|
171
419
|
console.error(
|
|
172
420
|
'[openai-oauth] 401 retry failed, returning original 401 response',
|
|
@@ -175,7 +423,7 @@ export function createOpenAIOAuthFetch(config: OpenAIOAuthConfig) {
|
|
|
175
423
|
}
|
|
176
424
|
}
|
|
177
425
|
|
|
178
|
-
return
|
|
426
|
+
return trackedResponse;
|
|
179
427
|
};
|
|
180
428
|
|
|
181
429
|
return customFetch as typeof fetch;
|
package/src/skills/loader.ts
CHANGED
|
@@ -198,20 +198,10 @@ async function loadSkillsFromDir(
|
|
|
198
198
|
const skill = parseSkillFile(content, filePath, scope);
|
|
199
199
|
|
|
200
200
|
const dirName = dirname(filePath).split(/[\\/]/).pop();
|
|
201
|
-
|
|
202
|
-
if (process.env.OTTO_DEBUG === '1') {
|
|
203
|
-
console.warn(
|
|
204
|
-
`Skill name '${skill.metadata.name}' doesn't match directory '${dirName}' in ${filePath}`,
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
201
|
+
void dirName;
|
|
208
202
|
|
|
209
203
|
skills.set(skill.metadata.name, skill);
|
|
210
|
-
} catch
|
|
211
|
-
if (process.env.OTTO_DEBUG === '1') {
|
|
212
|
-
console.error(`Failed to load skill from ${filePath}:`, err);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
204
|
+
} catch {}
|
|
215
205
|
}
|
|
216
206
|
}
|
|
217
207
|
|