@ottocode/server 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/server",
3
- "version": "0.1.234",
3
+ "version": "0.1.236",
4
4
  "description": "HTTP API server for ottocode",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -49,8 +49,8 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@ottocode/sdk": "0.1.234",
53
- "@ottocode/database": "0.1.234",
52
+ "@ottocode/sdk": "0.1.236",
53
+ "@ottocode/database": "0.1.236",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.3.6"
@@ -234,6 +234,100 @@ export const configPaths = {
234
234
  },
235
235
  },
236
236
  },
237
+ '/v1/config/debug': {
238
+ get: {
239
+ tags: ['config'],
240
+ operationId: 'getDebugConfig',
241
+ summary: 'Get debug configuration',
242
+ responses: {
243
+ 200: {
244
+ description: 'OK',
245
+ content: {
246
+ 'application/json': {
247
+ schema: {
248
+ type: 'object',
249
+ properties: {
250
+ enabled: { type: 'boolean' },
251
+ scopes: {
252
+ type: 'array',
253
+ items: { type: 'string' },
254
+ },
255
+ logPath: { type: 'string' },
256
+ sessionsDir: { type: 'string' },
257
+ debugDir: { type: 'string' },
258
+ },
259
+ required: [
260
+ 'enabled',
261
+ 'scopes',
262
+ 'logPath',
263
+ 'sessionsDir',
264
+ 'debugDir',
265
+ ],
266
+ },
267
+ },
268
+ },
269
+ },
270
+ },
271
+ },
272
+ patch: {
273
+ tags: ['config'],
274
+ operationId: 'updateDebugConfig',
275
+ summary: 'Update debug configuration',
276
+ requestBody: {
277
+ required: true,
278
+ content: {
279
+ 'application/json': {
280
+ schema: {
281
+ type: 'object',
282
+ properties: {
283
+ enabled: { type: 'boolean' },
284
+ scopes: {
285
+ type: 'array',
286
+ items: { type: 'string' },
287
+ },
288
+ },
289
+ },
290
+ },
291
+ },
292
+ },
293
+ responses: {
294
+ 200: {
295
+ description: 'OK',
296
+ content: {
297
+ 'application/json': {
298
+ schema: {
299
+ type: 'object',
300
+ properties: {
301
+ success: { type: 'boolean' },
302
+ debug: {
303
+ type: 'object',
304
+ properties: {
305
+ enabled: { type: 'boolean' },
306
+ scopes: {
307
+ type: 'array',
308
+ items: { type: 'string' },
309
+ },
310
+ logPath: { type: 'string' },
311
+ sessionsDir: { type: 'string' },
312
+ debugDir: { type: 'string' },
313
+ },
314
+ required: [
315
+ 'enabled',
316
+ 'scopes',
317
+ 'logPath',
318
+ 'sessionsDir',
319
+ 'debugDir',
320
+ ],
321
+ },
322
+ },
323
+ required: ['success', 'debug'],
324
+ },
325
+ },
326
+ },
327
+ },
328
+ },
329
+ },
330
+ },
237
331
  '/v1/config/models': {
238
332
  get: {
239
333
  tags: ['config'],
@@ -0,0 +1,39 @@
1
+ import type { Hono } from 'hono';
2
+ import { logger, readDebugConfig, writeDebugConfig } from '@ottocode/sdk';
3
+ import { serializeError } from '../../runtime/errors/api-error.ts';
4
+
5
+ export function registerDebugConfigRoute(app: Hono) {
6
+ app.get('/v1/config/debug', async (c) => {
7
+ try {
8
+ const debug = await readDebugConfig();
9
+ return c.json(debug);
10
+ } catch (error) {
11
+ logger.error('Failed to load debug config', error);
12
+ const errorResponse = serializeError(error);
13
+ return c.json(errorResponse, errorResponse.error.status || 500);
14
+ }
15
+ });
16
+
17
+ app.patch('/v1/config/debug', async (c) => {
18
+ try {
19
+ const body = await c.req.json<{
20
+ enabled?: boolean;
21
+ scopes?: string[];
22
+ }>();
23
+
24
+ await writeDebugConfig({
25
+ enabled: body.enabled,
26
+ scopes: Array.isArray(body.scopes)
27
+ ? body.scopes.map((scope) => scope.trim()).filter(Boolean)
28
+ : body.scopes,
29
+ });
30
+
31
+ const debug = await readDebugConfig();
32
+ return c.json({ success: true, debug });
33
+ } catch (error) {
34
+ logger.error('Failed to update debug config', error);
35
+ const errorResponse = serializeError(error);
36
+ return c.json(errorResponse, errorResponse.error.status || 500);
37
+ }
38
+ });
39
+ }
@@ -5,6 +5,7 @@ import { registerAgentsRoute } from './agents.ts';
5
5
  import { registerProvidersRoute } from './providers.ts';
6
6
  import { registerModelsRoutes } from './models.ts';
7
7
  import { registerDefaultsRoute } from './defaults.ts';
8
+ import { registerDebugConfigRoute } from './debug.ts';
8
9
 
9
10
  export function registerConfigRoutes(app: Hono) {
10
11
  registerCwdRoute(app);
@@ -13,4 +14,5 @@ export function registerConfigRoutes(app: Hono) {
13
14
  registerProvidersRoute(app);
14
15
  registerModelsRoutes(app);
15
16
  registerDefaultsRoute(app);
17
+ registerDebugConfigRoute(app);
16
18
  }
@@ -9,7 +9,6 @@ import { readdir } from 'node:fs/promises';
9
9
  import { join } from 'node:path';
10
10
  import type { EmbeddedAppConfig } from '../../index.ts';
11
11
  import type { OttoConfig } from '@ottocode/sdk';
12
- import { logger } from '@ottocode/sdk';
13
12
  import { loadAgentsConfig } from '../../runtime/agent/registry.ts';
14
13
 
15
14
  export async function isProviderAuthorizedHybrid(
@@ -83,11 +82,7 @@ export async function discoverAllAgents(
83
82
  agentSet.add(agentName);
84
83
  }
85
84
  }
86
- } catch (err) {
87
- logger.debug('Failed to load agents.json', {
88
- error: err instanceof Error ? err.message : String(err),
89
- });
90
- }
85
+ } catch {}
91
86
 
92
87
  try {
93
88
  const localAgentsPath = join(projectRoot, '.otto', 'agents');
@@ -100,11 +95,7 @@ export async function discoverAllAgents(
100
95
  }
101
96
  }
102
97
  }
103
- } catch (err) {
104
- logger.debug('Failed to read local agents directory', {
105
- error: err instanceof Error ? err.message : String(err),
106
- });
107
- }
98
+ } catch {}
108
99
 
109
100
  try {
110
101
  const globalAgentsPath = getGlobalAgentsDir();
@@ -117,11 +108,7 @@ export async function discoverAllAgents(
117
108
  }
118
109
  }
119
110
  }
120
- } catch (err) {
121
- logger.debug('Failed to read global agents directory', {
122
- error: err instanceof Error ? err.message : String(err),
123
- });
124
- }
111
+ } catch {}
125
112
 
126
113
  return Array.from(agentSet).sort();
127
114
  }
@@ -10,7 +10,6 @@ import { sessions } from '@ottocode/database/schema';
10
10
  import { gitCommitSchema, gitGenerateCommitMessageSchema } from './schemas.ts';
11
11
  import { validateAndGetGitRoot, parseGitStatus } from './utils.ts';
12
12
  import { resolveModel } from '../../runtime/provider/index.ts';
13
- import { debugLog } from '../../runtime/debug/index.ts';
14
13
  import {
15
14
  detectOAuth,
16
15
  adaptSimpleCall,
@@ -168,7 +167,6 @@ Commit message:`;
168
167
  });
169
168
 
170
169
  if (adapted.forceStream) {
171
- debugLog('[COMMIT] Using streamText for OpenAI OAuth');
172
170
  const result = streamText({
173
171
  model,
174
172
  system: adapted.system,
@@ -180,7 +178,6 @@ Commit message:`;
180
178
  text += chunk;
181
179
  }
182
180
  const message = text.trim();
183
- debugLog(`[COMMIT] OAuth result: "${message.slice(0, 80)}..."`);
184
181
  return c.json({ status: 'ok', data: { message } });
185
182
  }
186
183
 
@@ -18,9 +18,7 @@ export function registerTerminalsRoutes(
18
18
 
19
19
  app.post('/v1/terminals', async (c) => {
20
20
  try {
21
- logger.debug('POST /v1/terminals called');
22
21
  const body = await c.req.json();
23
- logger.debug('Creating terminal request received', body);
24
22
  const { command, args, purpose, cwd, title } = body;
25
23
 
26
24
  if (!command || !purpose) {
@@ -36,13 +34,6 @@ export function registerTerminalsRoutes(
36
34
  }
37
35
  const resolvedCwd = cwd || process.cwd();
38
36
 
39
- logger.debug('Creating terminal', {
40
- command: resolvedCommand,
41
- args,
42
- purpose,
43
- cwd: resolvedCwd,
44
- });
45
-
46
37
  const terminal = terminalManager.create({
47
38
  command: resolvedCommand,
48
39
  args: args || [],
@@ -52,8 +43,6 @@ export function registerTerminalsRoutes(
52
43
  title,
53
44
  });
54
45
 
55
- logger.debug('Terminal created successfully', { id: terminal.id });
56
-
57
46
  return c.json({
58
47
  terminalId: terminal.id,
59
48
  pid: terminal.pid,
@@ -80,25 +69,18 @@ export function registerTerminalsRoutes(
80
69
 
81
70
  const handleTerminalOutput = async (c: Context) => {
82
71
  const id = c.req.param('id');
83
- logger.debug('SSE client connecting to terminal', { id });
84
72
  const terminal = terminalManager.get(id);
85
73
 
86
74
  if (!terminal) {
87
- logger.debug('SSE terminal not found', { id });
88
75
  return c.json({ error: 'Terminal not found' }, 404);
89
76
  }
90
77
 
91
78
  const activeTerminal = terminal;
92
79
 
93
80
  return streamSSE(c, async (stream) => {
94
- logger.debug('SSE stream started for terminal', { id });
95
81
  const skipHistory = c.req.query('skipHistory') === 'true';
96
82
  if (!skipHistory) {
97
83
  const history = activeTerminal.read();
98
- logger.debug('SSE sending terminal history', {
99
- id,
100
- lines: history.length,
101
- });
102
84
  for (const line of history) {
103
85
  await stream.write(
104
86
  `data: ${JSON.stringify({ type: 'data', line })}\n\n`,
@@ -155,9 +137,6 @@ export function registerTerminalsRoutes(
155
137
  }
156
138
 
157
139
  function onAbort() {
158
- logger.debug('SSE client disconnected from terminal', {
159
- id: activeTerminal.id,
160
- });
161
140
  stream.close();
162
141
  finish();
163
142
  }
@@ -47,8 +47,6 @@ export function registerTunnelRoutes(app: Hono) {
47
47
  port = getServerPort() || 9100;
48
48
  }
49
49
 
50
- logger.debug('Starting tunnel on port:', port);
51
-
52
50
  // Kill any stale tunnel processes first
53
51
  await killStaleTunnels();
54
52
 
@@ -60,7 +58,6 @@ export function registerTunnelRoutes(app: Hono) {
60
58
 
61
59
  const url = await activeTunnel.start(port, (msg) => {
62
60
  progressMessage = msg;
63
- logger.debug('Tunnel progress', { message: msg });
64
61
  });
65
62
 
66
63
  tunnelUrl = url;
@@ -109,8 +106,6 @@ export function registerTunnelRoutes(app: Hono) {
109
106
  tunnelError = null;
110
107
  progressMessage = null;
111
108
 
112
- logger.debug('External tunnel registered:', url);
113
-
114
109
  return c.json({
115
110
  ok: true,
116
111
  url: tunnelUrl,
@@ -1,5 +1,4 @@
1
1
  import type { Tool } from 'ai';
2
- import { debugLog } from '../debug/index.ts';
3
2
 
4
3
  export interface MCPPrepareStepState {
5
4
  mcpToolsRecord: Record<string, Tool>;
@@ -57,12 +56,7 @@ export function buildPrepareStep(state: MCPPrepareStepState) {
57
56
  }
58
57
 
59
58
  const activeTools = [...state.baseToolNames, ...state.loadedMCPTools];
60
-
61
- if (state.loadedMCPTools.size > 0) {
62
- debugLog(
63
- `[MCP prepareStep] step=${stepNumber}, active MCP tools: ${[...state.loadedMCPTools].join(', ')}`,
64
- );
65
- }
59
+ void stepNumber;
66
60
 
67
61
  return { activeTools };
68
62
  };
@@ -1,5 +1,4 @@
1
1
  import { getGlobalAgentsJsonPath, getGlobalAgentsDir } from '@ottocode/sdk';
2
- import { debugLog } from '../debug/index.ts';
3
2
  import type { ProviderName } from '@ottocode/sdk';
4
3
  import { catalog } from '@ottocode/sdk';
5
4
  // Embed default agent prompts; only user overrides read from disk.
@@ -334,13 +333,7 @@ export async function resolveAgentConfig(
334
333
  const deduped = Array.from(new Set([...tools, ...baseToolSet]));
335
334
  const provider = normalizeProvider(entry?.provider);
336
335
  const model = normalizeModel(entry?.model);
337
- debugLog(`[agent] ${name} prompt source: ${promptSource}`);
338
- debugLog(
339
- `[agent] ${name} prompt summary: ${JSON.stringify({
340
- length: prompt.length,
341
- lines: prompt.split('\n').length,
342
- })}`,
343
- );
336
+ void promptSource;
344
337
  return {
345
338
  name,
346
339
  prompt,
@@ -1,9 +1,11 @@
1
- import { loadConfig } from '@ottocode/sdk';
1
+ import { loadConfig, logger, getSessionSystemPromptPath } from '@ottocode/sdk';
2
2
  import { wrapLanguageModel } from 'ai';
3
3
  import { devToolsMiddleware } from '@ai-sdk/devtools';
4
4
  import { getDb } from '@ottocode/database';
5
5
  import { sessions } from '@ottocode/database/schema';
6
6
  import { eq } from 'drizzle-orm';
7
+ import { mkdir } from 'node:fs/promises';
8
+ import { dirname } from 'node:path';
7
9
  import { resolveModel } from '../provider/index.ts';
8
10
  import { resolveAgentConfig } from './registry.ts';
9
11
  import { composeSystemPrompt } from '../prompt/builder.ts';
@@ -11,7 +13,7 @@ import { discoverProjectTools } from '@ottocode/sdk';
11
13
  import type { Tool } from 'ai';
12
14
  import { adaptTools } from '../../tools/adapter.ts';
13
15
  import { buildDatabaseTools } from '../../tools/database/index.ts';
14
- import { debugLog, time } from '../debug/index.ts';
16
+ import { time } from '../debug/index.ts';
15
17
  import { isDevtoolsEnabled } from '../debug/state.ts';
16
18
  import { buildHistoryMessages } from '../message/history-builder.ts';
17
19
  import { getMaxOutputTokens } from '../utils/token.ts';
@@ -87,7 +89,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
87
89
  const historyTimer = time('runner:buildHistory');
88
90
  let history: Awaited<ReturnType<typeof buildHistoryMessages>>;
89
91
  if (opts.isCompactCommand && opts.compactionContext) {
90
- debugLog('[RUNNER] Using minimal history for /compact command');
91
92
  history = [];
92
93
  } else {
93
94
  history = await buildHistoryMessages(
@@ -104,34 +105,14 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
104
105
  .where(eq(sessions.id, opts.sessionId))
105
106
  .limit(1);
106
107
  const contextSummary = sessionRows[0]?.contextSummary ?? undefined;
107
- if (contextSummary) {
108
- debugLog(
109
- `[RUNNER] Using context summary from compaction (${contextSummary.length} chars)`,
110
- );
111
- }
112
108
 
113
109
  const isFirstMessage = !history.some((m) => m.role === 'assistant');
114
110
 
115
- debugLog(`[RUNNER] isFirstMessage: ${isFirstMessage}`);
116
- debugLog(`[RUNNER] userContext provided: ${opts.userContext ? 'YES' : 'NO'}`);
117
- if (opts.userContext) {
118
- debugLog(
119
- `[RUNNER] userContext value: ${opts.userContext.substring(0, 100)}${opts.userContext.length > 100 ? '...' : ''}`,
120
- );
121
- }
122
-
123
111
  const systemTimer = time('runner:composeSystemPrompt');
124
112
  const { getAuth } = await import('@ottocode/sdk');
125
113
  const auth = await getAuth(opts.provider, cfg.projectRoot);
126
114
  const oauth = detectOAuth(opts.provider, auth);
127
115
 
128
- debugLog(
129
- `[RUNNER] needsSpoof (OAuth): ${oauth.needsSpoof}, isOpenAIOAuth: ${oauth.isOpenAIOAuth}`,
130
- );
131
- debugLog(
132
- `[RUNNER] spoofPrompt: ${oauth.spoofPrompt ? `present (${opts.provider})` : 'none'}`,
133
- );
134
-
135
116
  const composed = await composeSystemPrompt({
136
117
  provider: opts.provider,
137
118
  model: opts.model,
@@ -154,16 +135,78 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
154
135
 
155
136
  const { system } = adapted;
156
137
  const { systemComponents, additionalSystemMessages } = adapted;
138
+ const openAIProviderOptions = adapted.providerOptions.openai as
139
+ | Record<string, unknown>
140
+ | undefined;
141
+ const openAIInstructions =
142
+ typeof openAIProviderOptions?.instructions === 'string'
143
+ ? openAIProviderOptions.instructions
144
+ : '';
145
+ const effectiveSystemPrompt = system || openAIInstructions || composed.prompt;
146
+ const promptMode = oauth.isOpenAIOAuth
147
+ ? 'openai-oauth'
148
+ : oauth.needsSpoof
149
+ ? 'spoof'
150
+ : 'standard';
157
151
  systemTimer.end();
158
- debugLog(
159
- `[system] summary: ${JSON.stringify({
160
- components: systemComponents,
161
- length: system.length,
162
- })}`,
163
- );
152
+ logger.debug('[prompt] system prompt assembled', {
153
+ sessionId: opts.sessionId,
154
+ messageId: opts.assistantMessageId,
155
+ agent: opts.agent,
156
+ provider: opts.provider,
157
+ model: opts.model,
158
+ promptMode,
159
+ components: systemComponents,
160
+ systemLength: effectiveSystemPrompt.length,
161
+ historyMessages: history.length,
162
+ additionalSystemMessages: additionalSystemMessages.length,
163
+ isFirstMessage,
164
+ isOpenAIOAuth: oauth.isOpenAIOAuth,
165
+ needsSpoof: oauth.needsSpoof,
166
+ });
167
+ logger.debug('[prompt] detailed prompt context', {
168
+ sessionId: opts.sessionId,
169
+ messageId: opts.assistantMessageId,
170
+ debugDetail: true,
171
+ agentPromptLength: agentPrompt.length,
172
+ contextSummaryLength: contextSummary?.length ?? 0,
173
+ userContextLength: opts.userContext?.length ?? 0,
174
+ oneShot: Boolean(opts.oneShot),
175
+ guidedMode: Boolean(cfg.defaults.guidedMode),
176
+ isOpenAIOAuth: oauth.isOpenAIOAuth,
177
+ needsSpoof: oauth.needsSpoof,
178
+ promptMode,
179
+ rawSystemLength: system.length,
180
+ openAIInstructionsLength: openAIInstructions.length,
181
+ effectiveSystemPromptLength: effectiveSystemPrompt.length,
182
+ systemComponents,
183
+ additionalSystemMessageRoles: additionalSystemMessages.map(
184
+ (message) => message.role,
185
+ ),
186
+ });
187
+ if (effectiveSystemPrompt) {
188
+ const systemPromptPath = getSessionSystemPromptPath(opts.sessionId);
189
+ try {
190
+ await mkdir(dirname(systemPromptPath), { recursive: true });
191
+ await Bun.write(systemPromptPath, effectiveSystemPrompt);
192
+ logger.debug('[prompt] wrote system prompt file', {
193
+ sessionId: opts.sessionId,
194
+ messageId: opts.assistantMessageId,
195
+ path: systemPromptPath,
196
+ debugDetail: true,
197
+ promptMode,
198
+ effectiveSystemPromptLength: effectiveSystemPrompt.length,
199
+ });
200
+ } catch (error) {
201
+ logger.warn('[prompt] failed to write system prompt file', {
202
+ sessionId: opts.sessionId,
203
+ messageId: opts.assistantMessageId,
204
+ error: error instanceof Error ? error.message : String(error),
205
+ });
206
+ }
207
+ }
164
208
 
165
209
  if (opts.isCompactCommand && opts.compactionContext) {
166
- debugLog('[RUNNER] Injecting compaction context for /compact command');
167
210
  const compactPrompt = getCompactionSystemPrompt();
168
211
  additionalSystemMessages.push({
169
212
  role: 'system',
@@ -188,9 +231,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
188
231
  for (const dt of dbTools) {
189
232
  discovered.tools.push(dt);
190
233
  }
191
- debugLog(
192
- `[tools] Added ${dbTools.length} database tools for research agent (parent: ${parentSessionId ?? 'none'})`,
193
- );
194
234
  }
195
235
 
196
236
  toolsTimer.end({
@@ -200,12 +240,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
200
240
  const gated = allTools.filter(
201
241
  (tool) => allowedNames.has(tool.name) || tool.name === 'load_mcp_tools',
202
242
  );
203
- debugLog(
204
- `[tools] ${gated.length} gated tools, ${Object.keys(mcpToolsRecord).length} lazy MCP tools`,
205
- );
206
-
207
- debugLog(`[RUNNER] About to create model with provider: ${opts.provider}`);
208
- debugLog(`[RUNNER] About to create model ID: ${opts.model}`);
209
243
 
210
244
  const model = await resolveModel(opts.provider, opts.model, cfg, {
211
245
  sessionId: opts.sessionId,
@@ -218,12 +252,8 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
218
252
  middleware: devToolsMiddleware(),
219
253
  })
220
254
  : model;
221
- debugLog(
222
- `[RUNNER] Model created: ${JSON.stringify({ id: model.modelId, provider: model.provider })}`,
223
- );
224
255
 
225
256
  const maxOutputTokens = adapted.maxOutputTokens;
226
- debugLog(`[RUNNER] maxOutputTokens for ${opts.model}: ${maxOutputTokens}`);
227
257
 
228
258
  const { sharedCtx, firstToolTimer, firstToolSeen } = await setupToolContext(
229
259
  opts,
@@ -253,12 +283,6 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
253
283
  });
254
284
  mergeProviderOptions(providerOptions, reasoningConfig.providerOptions);
255
285
  effectiveMaxOutputTokens = reasoningConfig.effectiveMaxOutputTokens;
256
- debugLog(
257
- `[RUNNER] reasoning enabled for ${opts.provider}/${opts.model}: ${reasoningConfig.enabled}, level: ${opts.reasoningLevel ?? 'default'}`,
258
- );
259
- debugLog(
260
- `[RUNNER] final providerOptions: ${JSON.stringify(providerOptions)}`,
261
- );
262
286
 
263
287
  return {
264
288
  cfg,