@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.234",
3
+ "version": "0.1.236",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -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> {
@@ -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 (err) {
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
- const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
1
+ import { readFileSync } from 'node:fs';
2
+ import { getGlobalConfigPath } from '../../../config/src/paths.ts';
2
3
 
3
- type GlobalDebugFlags = {
4
- __OTTO_DEBUG_ENABLED__?: boolean;
5
- __OTTO_TRACE_ENABLED__?: boolean;
4
+ type DebugSettings = {
5
+ debugEnabled?: boolean;
6
+ debugScopes?: unknown;
6
7
  };
7
8
 
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;
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
- const globalFlag = readGlobalFlag('__OTTO_DEBUG_ENABLED__');
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
- const globalFlag = readGlobalFlag('__OTTO_TRACE_ENABLED__');
36
- if (typeof globalFlag === 'boolean') {
37
- return Boolean(globalFlag) && isDebugEnabled();
38
- }
39
- return envEnabled(['OTTO_TRACE', 'TRACE_OTTO']) && isDebugEnabled();
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 { isDebugEnabled, isTraceEnabled } from './debug.ts';
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
- export function debug(message: string, meta?: Record<string, unknown>): void {
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
- if (safeHasMeta(meta)) {
15
- console.log(`[debug] ${message}`, meta);
16
- } else {
17
- console.log(`[debug] ${message}`);
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 (!isDebugEnabled() && !isTraceEnabled()) return;
159
+ if (!shouldWriteDebugLog(message) && !isTraceEnabled()) return;
26
160
  try {
27
- if (safeHasMeta(meta)) {
28
- console.log(`[info] ${message}`, meta);
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
- if (safeHasMeta(meta)) {
40
- console.warn(`[warn] ${message}`, meta);
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
- console.error(`[error] ${message}`, logMeta);
222
+ const line = writeLogLine(`[error] ${message}`, logMeta);
223
+ printLine('error', line, logMeta);
95
224
  } else {
96
- console.error(`[error] ${message}`);
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 = `[timing] ${label} ${duration.toFixed(1)}ms`;
140
- if (safeHasMeta(meta)) {
141
- console.log(base, meta);
142
- } else {
143
- console.log(base);
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)
@@ -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
 
@@ -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
- const config = getDebugConfig();
65
- if (config.flags.has('all')) return true;
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
- if (!isDebugEnabled('timing')) {
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
- if (finished) return;
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
- console.error(
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
- init,
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
- ...init,
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
- init,
403
+ requestInit,
159
404
  currentOAuth.access,
160
405
  currentOAuth.accountId,
161
406
  config.sessionId,
162
407
  );
163
408
 
164
- return fetch(targetUrl, {
165
- ...init,
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 response;
426
+ return trackedResponse;
179
427
  };
180
428
 
181
429
  return customFetch as typeof fetch;
@@ -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
- if (dirName !== skill.metadata.name) {
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 (err) {
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
 
@@ -55,5 +55,7 @@ export type OttoConfig = {
55
55
  defaults: DefaultConfig;
56
56
  providers: ProviderSettings;
57
57
  paths: PathConfig;
58
+ debugEnabled?: boolean;
59
+ debugScopes?: string[];
58
60
  onboardingComplete?: boolean;
59
61
  };