@ottocode/server 0.1.235 → 0.1.237

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.235",
3
+ "version": "0.1.237",
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.235",
53
- "@ottocode/database": "0.1.235",
52
+ "@ottocode/sdk": "0.1.237",
53
+ "@ottocode/database": "0.1.237",
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'],
package/src/presets.ts CHANGED
@@ -54,6 +54,8 @@ export const BUILTIN_AGENTS = {
54
54
  */
55
55
  export const BUILTIN_TOOLS = [
56
56
  'read',
57
+ 'edit',
58
+ 'multiedit',
57
59
  'write',
58
60
  'ls',
59
61
  'tree',
@@ -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(
@@ -6,7 +6,16 @@ import { eq, and, inArray } from 'drizzle-orm';
6
6
  import { serializeError } from '../runtime/errors/api-error.ts';
7
7
  import { logger } from '@ottocode/sdk';
8
8
 
9
- const FILE_EDIT_TOOLS = ['Write', 'ApplyPatch', 'write', 'apply_patch'];
9
+ const FILE_EDIT_TOOLS = [
10
+ 'Write',
11
+ 'Edit',
12
+ 'MultiEdit',
13
+ 'ApplyPatch',
14
+ 'write',
15
+ 'edit',
16
+ 'multiedit',
17
+ 'apply_patch',
18
+ ];
10
19
 
11
20
  interface FileOperation {
12
21
  path: string;
@@ -62,7 +71,7 @@ function extractFilePathFromToolCall(
62
71
 
63
72
  const name = toolName.toLowerCase();
64
73
 
65
- if (name === 'write') {
74
+ if (name === 'write' || name === 'edit' || name === 'multiedit') {
66
75
  if (args && typeof args.path === 'string') return args.path;
67
76
  if (typeof c.path === 'string') return c.path;
68
77
  }
@@ -189,6 +198,13 @@ function extractDataFromToolResult(
189
198
  patch = (args?.patch as string | undefined) ?? c.patch;
190
199
  }
191
200
 
201
+ if (
202
+ (name === 'edit' || name === 'multiedit') &&
203
+ typeof c.result?.artifact?.patch === 'string'
204
+ ) {
205
+ patch = c.result.artifact.patch;
206
+ }
207
+
192
208
  if (name === 'write') {
193
209
  writeContent = args?.content as string | undefined;
194
210
  }
@@ -213,6 +229,7 @@ function extractDataFromToolResult(
213
229
  function getOperationType(toolName: string): 'write' | 'patch' | 'create' {
214
230
  const name = toolName.toLowerCase();
215
231
  if (name === 'write') return 'write';
232
+ if (name === 'edit' || name === 'multiedit') return 'patch';
216
233
  if (name === 'applypatch' || name === 'apply_patch') return 'patch';
217
234
  return 'write';
218
235
  }
@@ -116,6 +116,8 @@ const baseToolSet = ['progress_update', 'finish', 'skill'] as const;
116
116
  const defaultToolExtras: Record<string, string[]> = {
117
117
  build: [
118
118
  'read',
119
+ 'edit',
120
+ 'multiedit',
119
121
  'write',
120
122
  'ls',
121
123
  'tree',
@@ -131,6 +133,8 @@ const defaultToolExtras: Record<string, string[]> = {
131
133
  plan: ['read', 'ls', 'tree', 'ripgrep', 'update_todos', 'websearch'],
132
134
  general: [
133
135
  'read',
136
+ 'edit',
137
+ 'multiedit',
134
138
  'write',
135
139
  'ls',
136
140
  'tree',
@@ -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';
@@ -133,7 +135,76 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
133
135
 
134
136
  const { system } = adapted;
135
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';
136
151
  systemTimer.end();
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
+ }
137
208
 
138
209
  if (opts.isCompactCommand && opts.compactionContext) {
139
210
  const compactPrompt = getCompactionSystemPrompt();
@@ -51,7 +51,12 @@ export {
51
51
  getRunnerState,
52
52
  } from '../session/queue.ts';
53
53
 
54
- const DEFAULT_TRACED_TOOL_INPUTS = new Set(['write', 'apply_patch']);
54
+ const DEFAULT_TRACED_TOOL_INPUTS = new Set([
55
+ 'write',
56
+ 'edit',
57
+ 'multiedit',
58
+ 'apply_patch',
59
+ ]);
55
60
 
56
61
  function shouldTraceToolInput(name: string): boolean {
57
62
  void DEFAULT_TRACED_TOOL_INPUTS;
@@ -9,6 +9,7 @@ import { runSessionLoop } from '../agent/runner.ts';
9
9
  import { resolveModel } from '../provider/index.ts';
10
10
  import {
11
11
  getFastModelForAuth,
12
+ logger,
12
13
  type ProviderId,
13
14
  type ReasoningLevel,
14
15
  } from '@ottocode/sdk';
@@ -61,6 +62,14 @@ export async function dispatchAssistantMessage(
61
62
  const sessionId = session.id;
62
63
  const now = Date.now();
63
64
  const userMessageId = crypto.randomUUID();
65
+ logger.debug('[agent] dispatching assistant message', {
66
+ sessionId,
67
+ agent,
68
+ provider,
69
+ model,
70
+ oneShot: Boolean(oneShot),
71
+ hasUserContext: Boolean(userContext),
72
+ });
64
73
 
65
74
  await db.insert(messages).values({
66
75
  id: userMessageId,
@@ -194,6 +203,14 @@ export async function dispatchAssistantMessage(
194
203
  },
195
204
  runSessionLoop,
196
205
  );
206
+ logger.debug('[agent] assistant run enqueued', {
207
+ sessionId,
208
+ assistantMessageId,
209
+ agent,
210
+ provider,
211
+ model,
212
+ isCompactCommand: isCompact,
213
+ });
197
214
 
198
215
  void touchSessionLastActive({ db, sessionId });
199
216
 
@@ -4,6 +4,8 @@ export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
4
4
 
5
5
  export const DANGEROUS_TOOLS = new Set([
6
6
  'bash',
7
+ 'edit',
8
+ 'multiedit',
7
9
  'write',
8
10
  'apply_patch',
9
11
  'terminal',
@@ -17,6 +17,8 @@ export type ToolNamingConvention = 'canonical' | 'claude-code';
17
17
  export const CANONICAL_TO_PASCAL: Record<string, string> = {
18
18
  // File system operations
19
19
  read: 'Read',
20
+ edit: 'Edit',
21
+ multiedit: 'MultiEdit',
20
22
  write: 'Write',
21
23
  ls: 'Ls',
22
24
  tree: 'Tree',
@@ -55,6 +57,8 @@ export const CANONICAL_TO_PASCAL: Record<string, string> = {
55
57
  export const PASCAL_TO_CANONICAL: Record<string, string> = {
56
58
  // File system operations
57
59
  Read: 'read',
60
+ Edit: 'edit',
61
+ MultiEdit: 'multiedit',
58
62
  Write: 'write',
59
63
  Ls: 'ls',
60
64
  Tree: 'tree',
@@ -2,7 +2,7 @@ import type { Tool } from 'ai';
2
2
  import { messageParts, sessions } from '@ottocode/database/schema';
3
3
  import { eq } from 'drizzle-orm';
4
4
  import { publish } from '../events/bus.ts';
5
- import type { DiscoveredTool } from '@ottocode/sdk';
5
+ import { logger, type DiscoveredTool } from '@ottocode/sdk';
6
6
  import { getCwd, setCwd, joinRelative } from '../runtime/utils/cwd.ts';
7
7
  import type {
8
8
  ToolAdapterContext,
@@ -59,7 +59,12 @@ function extractToolCallId(options: unknown): string | undefined {
59
59
  return (options as { toolCallId?: string } | undefined)?.toolCallId;
60
60
  }
61
61
 
62
- const DEFAULT_TRACED_TOOL_INPUTS = new Set(['write', 'apply_patch']);
62
+ const DEFAULT_TRACED_TOOL_INPUTS = new Set([
63
+ 'write',
64
+ 'edit',
65
+ 'multiedit',
66
+ 'apply_patch',
67
+ ]);
63
68
 
64
69
  function shouldTraceToolInput(name: string): boolean {
65
70
  void DEFAULT_TRACED_TOOL_INPUTS;
@@ -312,6 +317,13 @@ export function adaptTools(
312
317
  messageId: ctx.messageId,
313
318
  },
314
319
  });
320
+ logger.debug(`[tools] call ${name}`, {
321
+ sessionId: ctx.sessionId,
322
+ messageId: ctx.messageId,
323
+ toolName: name,
324
+ callId,
325
+ stepIndex: ctx.stepIndex,
326
+ });
315
327
  // Persist synchronously to maintain correct ordering
316
328
  try {
317
329
  const index = await ctx.nextIndex();
@@ -689,6 +701,13 @@ export function adaptTools(
689
701
  sessionId: ctx.sessionId,
690
702
  payload: { ...contentObj, stepIndex: stepIndexForEvent },
691
703
  });
704
+ logger.debug(`[tools] result ${name}`, {
705
+ sessionId: ctx.sessionId,
706
+ messageId: ctx.messageId,
707
+ toolName: name,
708
+ callId,
709
+ stepIndex: stepIndexForEvent,
710
+ });
692
711
  if (name === 'update_todos') {
693
712
  try {
694
713
  const resultValue = (contentObj as { result?: unknown })