@ottocode/sdk 0.1.233 → 0.1.235
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/core/src/terminals/manager.ts +0 -14
- package/src/core/src/tools/loader.ts +1 -4
- package/src/core/src/utils/debug.ts +2 -35
- package/src/core/src/utils/logger.ts +49 -9
- package/src/prompts/src/base.txt +6 -0
- package/src/prompts/src/debug.ts +4 -86
- package/src/prompts/src/providers.ts +0 -19
- package/src/providers/src/openai-oauth-client.ts +255 -7
- package/src/skills/loader.ts +6 -12
package/package.json
CHANGED
|
@@ -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,7 @@
|
|
|
1
|
-
const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
|
|
2
|
-
|
|
3
|
-
type GlobalDebugFlags = {
|
|
4
|
-
__OTTO_DEBUG_ENABLED__?: boolean;
|
|
5
|
-
__OTTO_TRACE_ENABLED__?: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
function readGlobalFlag(
|
|
9
|
-
key: '__OTTO_DEBUG_ENABLED__' | '__OTTO_TRACE_ENABLED__',
|
|
10
|
-
) {
|
|
11
|
-
const globalState = globalThis as GlobalDebugFlags;
|
|
12
|
-
return globalState[key];
|
|
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;
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
1
|
export function isDebugEnabled(): boolean {
|
|
27
|
-
|
|
28
|
-
if (typeof globalFlag === 'boolean') {
|
|
29
|
-
return globalFlag;
|
|
30
|
-
}
|
|
31
|
-
return envEnabled(['OTTO_DEBUG', 'DEBUG_OTTO']);
|
|
2
|
+
return false;
|
|
32
3
|
}
|
|
33
4
|
|
|
34
5
|
export function isTraceEnabled(): boolean {
|
|
35
|
-
|
|
36
|
-
if (typeof globalFlag === 'boolean') {
|
|
37
|
-
return Boolean(globalFlag) && isDebugEnabled();
|
|
38
|
-
}
|
|
39
|
-
return envEnabled(['OTTO_TRACE', 'TRACE_OTTO']) && isDebugEnabled();
|
|
6
|
+
return false;
|
|
40
7
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
1
3
|
import { isDebugEnabled, isTraceEnabled } from './debug.ts';
|
|
2
4
|
|
|
3
5
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
@@ -8,13 +10,44 @@ function safeHasMeta(
|
|
|
8
10
|
return Boolean(meta && Object.keys(meta).length);
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
function getDebugLogFilePath(): string | undefined {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function serializeLogMeta(meta?: Record<string, unknown>): string {
|
|
18
|
+
if (!safeHasMeta(meta)) return '';
|
|
19
|
+
try {
|
|
20
|
+
return ` ${JSON.stringify(meta)}`;
|
|
21
|
+
} catch {
|
|
22
|
+
return ' [unserializable-meta]';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeLogLine(line: string, meta?: Record<string, unknown>) {
|
|
27
|
+
const suffix = serializeLogMeta(meta);
|
|
28
|
+
const fullLine = `${new Date().toISOString()} ${line}${suffix}`;
|
|
29
|
+
const logFile = getDebugLogFilePath();
|
|
30
|
+
|
|
31
|
+
if (logFile) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
34
|
+
appendFileSync(logFile, `${fullLine}\n`, 'utf-8');
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore file logging errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fullLine;
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
export function debug(message: string, meta?: Record<string, unknown>): void {
|
|
12
44
|
if (!isDebugEnabled()) return;
|
|
13
45
|
try {
|
|
46
|
+
const line = writeLogLine(`[debug] ${message}`, meta);
|
|
14
47
|
if (safeHasMeta(meta)) {
|
|
15
|
-
console.log(
|
|
48
|
+
console.log(line, meta);
|
|
16
49
|
} else {
|
|
17
|
-
console.log(
|
|
50
|
+
console.log(line);
|
|
18
51
|
}
|
|
19
52
|
} catch {
|
|
20
53
|
// ignore logging errors
|
|
@@ -24,10 +57,11 @@ export function debug(message: string, meta?: Record<string, unknown>): void {
|
|
|
24
57
|
export function info(message: string, meta?: Record<string, unknown>): void {
|
|
25
58
|
if (!isDebugEnabled() && !isTraceEnabled()) return;
|
|
26
59
|
try {
|
|
60
|
+
const line = writeLogLine(`[info] ${message}`, meta);
|
|
27
61
|
if (safeHasMeta(meta)) {
|
|
28
|
-
console.log(
|
|
62
|
+
console.log(line, meta);
|
|
29
63
|
} else {
|
|
30
|
-
console.log(
|
|
64
|
+
console.log(line);
|
|
31
65
|
}
|
|
32
66
|
} catch {
|
|
33
67
|
// ignore logging errors
|
|
@@ -36,10 +70,11 @@ export function info(message: string, meta?: Record<string, unknown>): void {
|
|
|
36
70
|
|
|
37
71
|
export function warn(message: string, meta?: Record<string, unknown>): void {
|
|
38
72
|
try {
|
|
73
|
+
const line = writeLogLine(`[warn] ${message}`, meta);
|
|
39
74
|
if (safeHasMeta(meta)) {
|
|
40
|
-
console.warn(
|
|
75
|
+
console.warn(line, meta);
|
|
41
76
|
} else {
|
|
42
|
-
console.warn(
|
|
77
|
+
console.warn(line);
|
|
43
78
|
}
|
|
44
79
|
} catch {
|
|
45
80
|
// ignore logging errors
|
|
@@ -91,9 +126,11 @@ export function error(
|
|
|
91
126
|
}
|
|
92
127
|
|
|
93
128
|
if (safeHasMeta(logMeta)) {
|
|
94
|
-
|
|
129
|
+
const line = writeLogLine(`[error] ${message}`, logMeta);
|
|
130
|
+
console.error(line, logMeta);
|
|
95
131
|
} else {
|
|
96
|
-
|
|
132
|
+
const line = writeLogLine(`[error] ${message}`);
|
|
133
|
+
console.error(line);
|
|
97
134
|
}
|
|
98
135
|
} catch (logErr) {
|
|
99
136
|
try {
|
|
@@ -136,7 +173,10 @@ export function time(label: string): Timer {
|
|
|
136
173
|
finished = true;
|
|
137
174
|
const duration = nowMs() - start;
|
|
138
175
|
try {
|
|
139
|
-
const base =
|
|
176
|
+
const base = writeLogLine(
|
|
177
|
+
`[timing] ${label} ${duration.toFixed(1)}ms`,
|
|
178
|
+
meta,
|
|
179
|
+
);
|
|
140
180
|
if (safeHasMeta(meta)) {
|
|
141
181
|
console.log(base, meta);
|
|
142
182
|
} else {
|
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,77 +1,6 @@
|
|
|
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 {}
|
|
2
|
+
void flag;
|
|
3
|
+
return false;
|
|
75
4
|
}
|
|
76
5
|
|
|
77
6
|
function nowMs(): number {
|
|
@@ -84,21 +13,10 @@ function nowMs(): number {
|
|
|
84
13
|
type Timer = { end(meta?: Record<string, unknown>): void };
|
|
85
14
|
|
|
86
15
|
export function time(label: string): Timer {
|
|
87
|
-
|
|
88
|
-
return { end() {} };
|
|
89
|
-
}
|
|
90
|
-
const start = nowMs();
|
|
91
|
-
let finished = false;
|
|
16
|
+
void label;
|
|
92
17
|
return {
|
|
93
18
|
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 {}
|
|
19
|
+
void meta;
|
|
102
20
|
},
|
|
103
21
|
};
|
|
104
22
|
}
|
|
@@ -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
|
@@ -14,6 +14,8 @@ const skillCache = new Map<string, SkillDefinition>();
|
|
|
14
14
|
|
|
15
15
|
const SKILL_DIRS = [
|
|
16
16
|
'.otto/skills',
|
|
17
|
+
'.agents/skills',
|
|
18
|
+
'.agenst/skills',
|
|
17
19
|
'.claude/skills',
|
|
18
20
|
'.opencode/skills',
|
|
19
21
|
'.codex/skills',
|
|
@@ -53,6 +55,8 @@ export async function discoverSkills(
|
|
|
53
55
|
|
|
54
56
|
const globalDirs = [
|
|
55
57
|
join(getGlobalConfigDir(), 'skills'),
|
|
58
|
+
join(home, '.agents/skills'),
|
|
59
|
+
join(home, '.agenst/skills'),
|
|
56
60
|
join(home, '.claude/skills'),
|
|
57
61
|
join(home, '.config/opencode/skills'),
|
|
58
62
|
join(home, '.codex/skills'),
|
|
@@ -194,20 +198,10 @@ async function loadSkillsFromDir(
|
|
|
194
198
|
const skill = parseSkillFile(content, filePath, scope);
|
|
195
199
|
|
|
196
200
|
const dirName = dirname(filePath).split(/[\\/]/).pop();
|
|
197
|
-
|
|
198
|
-
if (process.env.OTTO_DEBUG === '1') {
|
|
199
|
-
console.warn(
|
|
200
|
-
`Skill name '${skill.metadata.name}' doesn't match directory '${dirName}' in ${filePath}`,
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
201
|
+
void dirName;
|
|
204
202
|
|
|
205
203
|
skills.set(skill.metadata.name, skill);
|
|
206
|
-
} catch
|
|
207
|
-
if (process.env.OTTO_DEBUG === '1') {
|
|
208
|
-
console.error(`Failed to load skill from ${filePath}:`, err);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
204
|
+
} catch {}
|
|
211
205
|
}
|
|
212
206
|
}
|
|
213
207
|
|