@ottocode/sdk 0.1.235 → 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.235",
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
  }
@@ -1,7 +1,34 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { getGlobalConfigPath } from '../../../config/src/paths.ts';
3
+
4
+ type DebugSettings = {
5
+ debugEnabled?: boolean;
6
+ debugScopes?: unknown;
7
+ };
8
+
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 {};
16
+ }
17
+ }
18
+
1
19
  export function isDebugEnabled(): boolean {
2
- return false;
20
+ return readDebugSettings().debugEnabled === true;
3
21
  }
4
22
 
5
23
  export function isTraceEnabled(): boolean {
6
24
  return false;
7
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
+ );
34
+ }
@@ -1,9 +1,22 @@
1
1
  import { appendFileSync, mkdirSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
- import { isDebugEnabled, isTraceEnabled } from './debug.ts';
3
+ import { isDebugEnabled, isTraceEnabled, getDebugScopes } from './debug.ts';
4
+ import {
5
+ getGlobalDebugLogPath,
6
+ getSessionDebugDetailsLogPath,
7
+ getSessionDebugLogPath,
8
+ } from '../../../config/src/paths.ts';
4
9
 
5
10
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
6
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
+
7
20
  function safeHasMeta(
8
21
  meta?: Record<string, unknown>,
9
22
  ): meta is Record<string, unknown> {
@@ -11,18 +24,90 @@ function safeHasMeta(
11
24
  }
12
25
 
13
26
  function getDebugLogFilePath(): string | undefined {
14
- return 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]);
15
57
  }
16
58
 
17
59
  function serializeLogMeta(meta?: Record<string, unknown>): string {
18
60
  if (!safeHasMeta(meta)) return '';
19
61
  try {
20
- return ` ${JSON.stringify(meta)}`;
62
+ const sanitized = { ...meta };
63
+ delete sanitized.debugDetail;
64
+ return Object.keys(sanitized).length ? ` ${JSON.stringify(sanitized)}` : '';
21
65
  } catch {
22
66
  return ' [unserializable-meta]';
23
67
  }
24
68
  }
25
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
+
26
111
  function writeLogLine(line: string, meta?: Record<string, unknown>) {
27
112
  const suffix = serializeLogMeta(meta);
28
113
  const fullLine = `${new Date().toISOString()} ${line}${suffix}`;
@@ -37,32 +122,44 @@ function writeLogLine(line: string, meta?: Record<string, unknown>) {
37
122
  }
38
123
  }
39
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
142
+ }
143
+ }
144
+
40
145
  return fullLine;
41
146
  }
42
147
 
43
148
  export function debug(message: string, meta?: Record<string, unknown>): void {
44
- if (!isDebugEnabled()) return;
149
+ if (!shouldWriteDebugLog(message)) return;
45
150
  try {
46
151
  const line = writeLogLine(`[debug] ${message}`, meta);
47
- if (safeHasMeta(meta)) {
48
- console.log(line, meta);
49
- } else {
50
- console.log(line);
51
- }
152
+ printLine('debug', line, meta);
52
153
  } catch {
53
154
  // ignore logging errors
54
155
  }
55
156
  }
56
157
 
57
158
  export function info(message: string, meta?: Record<string, unknown>): void {
58
- if (!isDebugEnabled() && !isTraceEnabled()) return;
159
+ if (!shouldWriteDebugLog(message) && !isTraceEnabled()) return;
59
160
  try {
60
161
  const line = writeLogLine(`[info] ${message}`, meta);
61
- if (safeHasMeta(meta)) {
62
- console.log(line, meta);
63
- } else {
64
- console.log(line);
65
- }
162
+ printLine('info', line, meta);
66
163
  } catch {
67
164
  // ignore logging errors
68
165
  }
@@ -71,11 +168,7 @@ export function info(message: string, meta?: Record<string, unknown>): void {
71
168
  export function warn(message: string, meta?: Record<string, unknown>): void {
72
169
  try {
73
170
  const line = writeLogLine(`[warn] ${message}`, meta);
74
- if (safeHasMeta(meta)) {
75
- console.warn(line, meta);
76
- } else {
77
- console.warn(line);
78
- }
171
+ printLine('warn', line, meta);
79
172
  } catch {
80
173
  // ignore logging errors
81
174
  }
@@ -127,10 +220,10 @@ export function error(
127
220
 
128
221
  if (safeHasMeta(logMeta)) {
129
222
  const line = writeLogLine(`[error] ${message}`, logMeta);
130
- console.error(line, logMeta);
223
+ printLine('error', line, logMeta);
131
224
  } else {
132
225
  const line = writeLogLine(`[error] ${message}`);
133
- console.error(line);
226
+ printLine('error', line);
134
227
  }
135
228
  } catch (logErr) {
136
229
  try {
@@ -177,11 +270,7 @@ export function time(label: string): Timer {
177
270
  `[timing] ${label} ${duration.toFixed(1)}ms`,
178
271
  meta,
179
272
  );
180
- if (safeHasMeta(meta)) {
181
- console.log(base, meta);
182
- } else {
183
- console.log(base);
184
- }
273
+ printLine('info', base, meta);
185
274
  } catch {
186
275
  // ignore timing log errors
187
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,13 +3,6 @@ export function isDebugEnabled(flag?: string): boolean {
3
3
  return false;
4
4
  }
5
5
 
6
- function nowMs(): number {
7
- const perf = (globalThis as { performance?: { now?: () => number } })
8
- .performance;
9
- if (perf && typeof perf.now === 'function') return perf.now();
10
- return Date.now();
11
- }
12
-
13
6
  type Timer = { end(meta?: Record<string, unknown>): void };
14
7
 
15
8
  export function time(label: string): Timer {
@@ -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
  };