@ottocode/server 0.1.215 → 0.1.217

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.215",
3
+ "version": "0.1.217",
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.215",
53
- "@ottocode/database": "0.1.215",
52
+ "@ottocode/sdk": "0.1.217",
53
+ "@ottocode/database": "0.1.217",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.3.6"
@@ -178,6 +178,7 @@ export const configPaths = {
178
178
  agent: { type: 'string' },
179
179
  provider: { type: 'string' },
180
180
  model: { type: 'string' },
181
+ reasoningText: { type: 'boolean' },
181
182
  scope: {
182
183
  type: 'string',
183
184
  enum: ['global', 'local'],
@@ -203,6 +204,7 @@ export const configPaths = {
203
204
  agent: { type: 'string' },
204
205
  provider: { type: 'string' },
205
206
  model: { type: 'string' },
207
+ reasoningText: { type: 'boolean' },
206
208
  },
207
209
  required: ['agent', 'provider', 'model'],
208
210
  },
@@ -67,6 +67,11 @@ export const messagesPaths = {
67
67
  description:
68
68
  'Optional user-provided context to include in the system prompt.',
69
69
  },
70
+ reasoningText: {
71
+ type: 'boolean',
72
+ description:
73
+ 'Enable extended thinking / reasoning for models that support it.',
74
+ },
70
75
  },
71
76
  },
72
77
  },
@@ -6,15 +6,37 @@ export const sessionsPaths = {
6
6
  tags: ['sessions'],
7
7
  operationId: 'listSessions',
8
8
  summary: 'List sessions',
9
- parameters: [projectQueryParam()],
9
+ parameters: [
10
+ projectQueryParam(),
11
+ {
12
+ in: 'query',
13
+ name: 'limit',
14
+ schema: { type: 'integer', default: 50, minimum: 1, maximum: 200 },
15
+ description: 'Maximum number of sessions to return',
16
+ },
17
+ {
18
+ in: 'query',
19
+ name: 'offset',
20
+ schema: { type: 'integer', default: 0, minimum: 0 },
21
+ description: 'Offset for pagination',
22
+ },
23
+ ],
10
24
  responses: {
11
25
  200: {
12
26
  description: 'OK',
13
27
  content: {
14
28
  'application/json': {
15
29
  schema: {
16
- type: 'array',
17
- items: { $ref: '#/components/schemas/Session' },
30
+ type: 'object',
31
+ properties: {
32
+ items: {
33
+ type: 'array',
34
+ items: { $ref: '#/components/schemas/Session' },
35
+ },
36
+ hasMore: { type: 'boolean' },
37
+ nextOffset: { type: 'integer', nullable: true },
38
+ },
39
+ required: ['items', 'hasMore', 'nextOffset'],
18
40
  },
19
41
  },
20
42
  },
@@ -55,6 +77,79 @@ export const sessionsPaths = {
55
77
  },
56
78
  },
57
79
  },
80
+ '/v1/sessions/{sessionId}': {
81
+ patch: {
82
+ tags: ['sessions'],
83
+ operationId: 'updateSession',
84
+ summary: 'Update session preferences',
85
+ parameters: [
86
+ {
87
+ in: 'path',
88
+ name: 'sessionId',
89
+ required: true,
90
+ schema: { type: 'string' },
91
+ },
92
+ projectQueryParam(),
93
+ ],
94
+ requestBody: {
95
+ required: true,
96
+ content: {
97
+ 'application/json': {
98
+ schema: {
99
+ type: 'object',
100
+ properties: {
101
+ title: { type: 'string' },
102
+ agent: { type: 'string' },
103
+ provider: { $ref: '#/components/schemas/Provider' },
104
+ model: { type: 'string' },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ },
110
+ responses: {
111
+ 200: {
112
+ description: 'OK',
113
+ content: {
114
+ 'application/json': {
115
+ schema: { $ref: '#/components/schemas/Session' },
116
+ },
117
+ },
118
+ },
119
+ 400: errorResponse(),
120
+ 404: errorResponse(),
121
+ },
122
+ },
123
+ delete: {
124
+ tags: ['sessions'],
125
+ operationId: 'deleteSession',
126
+ summary: 'Delete a session',
127
+ parameters: [
128
+ {
129
+ in: 'path',
130
+ name: 'sessionId',
131
+ required: true,
132
+ schema: { type: 'string' },
133
+ },
134
+ projectQueryParam(),
135
+ ],
136
+ responses: {
137
+ 200: {
138
+ description: 'OK',
139
+ content: {
140
+ 'application/json': {
141
+ schema: {
142
+ type: 'object',
143
+ properties: { success: { type: 'boolean' } },
144
+ required: ['success'],
145
+ },
146
+ },
147
+ },
148
+ },
149
+ 404: errorResponse(),
150
+ },
151
+ },
152
+ },
58
153
  '/v1/sessions/{sessionId}/abort': {
59
154
  delete: {
60
155
  tags: ['sessions'],
@@ -5,7 +5,7 @@ export const setuPaths = {
5
5
  operationId: 'getSetuBalance',
6
6
  summary: 'Get Setu account balance',
7
7
  description:
8
- 'Returns wallet balance, total spent, total topups, and request count',
8
+ 'Returns wallet balance, subscription, account info, limits, and usage data',
9
9
  responses: {
10
10
  200: {
11
11
  description: 'OK',
@@ -19,6 +19,44 @@ export const setuPaths = {
19
19
  totalSpent: { type: 'number' },
20
20
  totalTopups: { type: 'number' },
21
21
  requestCount: { type: 'number' },
22
+ scope: { type: 'string', enum: ['wallet', 'account'] },
23
+ payg: {
24
+ type: 'object',
25
+ properties: {
26
+ walletBalanceUsd: { type: 'number' },
27
+ accountBalanceUsd: { type: 'number' },
28
+ rawPoolUsd: { type: 'number' },
29
+ effectiveSpendableUsd: { type: 'number' },
30
+ },
31
+ },
32
+ limits: {
33
+ type: 'object',
34
+ nullable: true,
35
+ properties: {
36
+ enabled: { type: 'boolean' },
37
+ dailyLimitUsd: { type: 'number', nullable: true },
38
+ dailySpentUsd: { type: 'number' },
39
+ dailyRemainingUsd: { type: 'number', nullable: true },
40
+ monthlyLimitUsd: { type: 'number', nullable: true },
41
+ monthlySpentUsd: { type: 'number' },
42
+ monthlyRemainingUsd: { type: 'number', nullable: true },
43
+ capRemainingUsd: { type: 'number', nullable: true },
44
+ },
45
+ },
46
+ subscription: {
47
+ type: 'object',
48
+ nullable: true,
49
+ properties: {
50
+ active: { type: 'boolean' },
51
+ tierId: { type: 'string' },
52
+ tierName: { type: 'string' },
53
+ creditsIncluded: { type: 'number' },
54
+ creditsUsed: { type: 'number' },
55
+ creditsRemaining: { type: 'number' },
56
+ periodStart: { type: 'string' },
57
+ periodEnd: { type: 'string' },
58
+ },
59
+ },
22
60
  },
23
61
  required: [
24
62
  'walletAddress',
@@ -93,6 +93,90 @@ export const skillsPaths = {
93
93
  },
94
94
  },
95
95
  },
96
+ '/v1/skills/{name}/files': {
97
+ get: {
98
+ tags: ['config'],
99
+ operationId: 'listSkillFiles',
100
+ summary: 'List files in a skill directory',
101
+ parameters: [
102
+ {
103
+ in: 'path',
104
+ name: 'name',
105
+ required: true,
106
+ schema: { type: 'string' },
107
+ },
108
+ projectQueryParam(),
109
+ ],
110
+ responses: {
111
+ 200: {
112
+ description: 'OK',
113
+ content: {
114
+ 'application/json': {
115
+ schema: {
116
+ type: 'object',
117
+ properties: {
118
+ files: {
119
+ type: 'array',
120
+ items: {
121
+ type: 'object',
122
+ properties: {
123
+ relativePath: { type: 'string' },
124
+ size: { type: 'number' },
125
+ },
126
+ required: ['relativePath', 'size'],
127
+ },
128
+ },
129
+ },
130
+ required: ['files'],
131
+ },
132
+ },
133
+ },
134
+ },
135
+ 500: errorResponse(),
136
+ },
137
+ },
138
+ },
139
+ '/v1/skills/{name}/files/{filePath}': {
140
+ get: {
141
+ tags: ['config'],
142
+ operationId: 'getSkillFile',
143
+ summary: 'Read a specific file from a skill directory',
144
+ parameters: [
145
+ {
146
+ in: 'path',
147
+ name: 'name',
148
+ required: true,
149
+ schema: { type: 'string' },
150
+ },
151
+ {
152
+ in: 'path',
153
+ name: 'filePath',
154
+ required: true,
155
+ schema: { type: 'string' },
156
+ },
157
+ projectQueryParam(),
158
+ ],
159
+ responses: {
160
+ 200: {
161
+ description: 'OK',
162
+ content: {
163
+ 'application/json': {
164
+ schema: {
165
+ type: 'object',
166
+ properties: {
167
+ content: { type: 'string' },
168
+ path: { type: 'string' },
169
+ },
170
+ required: ['content', 'path'],
171
+ },
172
+ },
173
+ },
174
+ },
175
+ 404: errorResponse(),
176
+ 500: errorResponse(),
177
+ },
178
+ },
179
+ },
96
180
  '/v1/skills/validate': {
97
181
  post: {
98
182
  tags: ['config'],
@@ -194,6 +194,7 @@ export const schemas = {
194
194
  agent: { type: 'string' },
195
195
  provider: { $ref: '#/components/schemas/Provider' },
196
196
  model: { type: 'string' },
197
+ reasoningText: { type: 'boolean' },
197
198
  },
198
199
  required: ['agent', 'provider', 'model'],
199
200
  },
@@ -13,6 +13,8 @@ export function registerDefaultsRoute(app: Hono) {
13
13
  model?: string;
14
14
  toolApproval?: 'auto' | 'dangerous' | 'all';
15
15
  guidedMode?: boolean;
16
+ reasoningText?: boolean;
17
+ theme?: string;
16
18
  scope?: 'global' | 'local';
17
19
  }>();
18
20
 
@@ -23,6 +25,8 @@ export function registerDefaultsRoute(app: Hono) {
23
25
  model: string;
24
26
  toolApproval: 'auto' | 'dangerous' | 'all';
25
27
  guidedMode: boolean;
28
+ reasoningText: boolean;
29
+ theme: string;
26
30
  }> = {};
27
31
 
28
32
  if (body.agent) updates.agent = body.agent;
@@ -30,6 +34,9 @@ export function registerDefaultsRoute(app: Hono) {
30
34
  if (body.model) updates.model = body.model;
31
35
  if (body.toolApproval) updates.toolApproval = body.toolApproval;
32
36
  if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
37
+ if (body.reasoningText !== undefined)
38
+ updates.reasoningText = body.reasoningText;
39
+ if (body.theme) updates.theme = body.theme;
33
40
 
34
41
  await setConfig(scope, updates, projectRoot);
35
42
 
@@ -58,6 +58,8 @@ export function registerMainConfigRoute(app: Hono) {
58
58
  cfg.defaults.toolApproval,
59
59
  ) as 'auto' | 'dangerous' | 'all',
60
60
  guidedMode: cfg.defaults.guidedMode ?? false,
61
+ reasoningText: cfg.defaults.reasoningText ?? true,
62
+ theme: cfg.defaults.theme,
61
63
  };
62
64
 
63
65
  return c.json({
@@ -122,7 +122,8 @@ export function registerSessionMessagesRoutes(app: Hono) {
122
122
  typeOf: typeof userContext,
123
123
  });
124
124
 
125
- const reasoning = body?.reasoningText === true;
125
+ const reasoning =
126
+ body?.reasoningText ?? cfg.defaults.reasoningText ?? false;
126
127
 
127
128
  // Validate model capabilities if tools are allowed for this agent
128
129
  const wantsToolCalls = true; // agent toolset may be non-empty
@@ -140,11 +140,16 @@ export function registerSessionsRoutes(app: Hono) {
140
140
  agent?: string;
141
141
  provider?: string;
142
142
  model?: string;
143
+ title?: string | null;
143
144
  lastActiveAt?: number;
144
145
  } = {
145
146
  lastActiveAt: Date.now(),
146
147
  };
147
148
 
149
+ if (typeof body.title === 'string') {
150
+ updates.title = body.title.trim() || null;
151
+ }
152
+
148
153
  // Validate agent if provided
149
154
  if (typeof body.agent === 'string') {
150
155
  const agentName = body.agent.trim();
@@ -2,6 +2,8 @@ import type { Hono } from 'hono';
2
2
  import {
3
3
  discoverSkills,
4
4
  loadSkill,
5
+ loadSkillFile,
6
+ discoverSkillFiles,
5
7
  findGitRoot,
6
8
  validateSkillName,
7
9
  parseSkillFile,
@@ -61,6 +63,51 @@ export function registerSkillsRoutes(app: Hono) {
61
63
  });
62
64
 
63
65
  app.post('/v1/skills/validate', async (c) => {
66
+ app.get('/v1/skills/:name/files', async (c) => {
67
+ try {
68
+ const name = c.req.param('name');
69
+ const projectRoot = c.req.query('project') || process.cwd();
70
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
71
+ await discoverSkills(projectRoot, repoRoot);
72
+
73
+ const files = await discoverSkillFiles(name);
74
+ return c.json({ files });
75
+ } catch (error) {
76
+ logger.error('Failed to list skill files', error);
77
+ const errorResponse = serializeError(error);
78
+ return c.json(
79
+ errorResponse,
80
+ (errorResponse.error.status || 500) as 500,
81
+ );
82
+ }
83
+ });
84
+
85
+ app.get('/v1/skills/:name/files/*', async (c) => {
86
+ try {
87
+ const name = c.req.param('name');
88
+ const filePath = c.req.path.replace(`/v1/skills/${name}/files/`, '');
89
+ const projectRoot = c.req.query('project') || process.cwd();
90
+ const repoRoot = (await findGitRoot(projectRoot)) ?? projectRoot;
91
+ await discoverSkills(projectRoot, repoRoot);
92
+
93
+ const result = await loadSkillFile(name, filePath);
94
+ if (!result) {
95
+ return c.json(
96
+ { error: `File '${filePath}' not found in skill '${name}'` },
97
+ 404,
98
+ );
99
+ }
100
+ return c.json({ content: result.content, path: result.resolvedPath });
101
+ } catch (error) {
102
+ logger.error('Failed to load skill file', error);
103
+ const errorResponse = serializeError(error);
104
+ return c.json(
105
+ errorResponse,
106
+ (errorResponse.error.status || 500) as 500,
107
+ );
108
+ }
109
+ });
110
+
64
111
  try {
65
112
  const body = await c.req.json<{ content: string; path?: string }>();
66
113
  if (!body.content) {
@@ -12,7 +12,7 @@ import { ToolHistoryTracker } from './tool-history-tracker.ts';
12
12
  export async function buildHistoryMessages(
13
13
  db: Awaited<ReturnType<typeof getDb>>,
14
14
  sessionId: string,
15
- currentMessageId?: string,
15
+ _currentMessageId?: string,
16
16
  ): Promise<ModelMessage[]> {
17
17
  const rows = await db
18
18
  .select()
@@ -24,26 +24,30 @@ export async function buildHistoryMessages(
24
24
  const toolHistory = new ToolHistoryTracker();
25
25
 
26
26
  for (const m of rows) {
27
+ const parts = await db
28
+ .select()
29
+ .from(messageParts)
30
+ .where(eq(messageParts.messageId, m.id))
31
+ .orderBy(asc(messageParts.index));
32
+
27
33
  if (
28
34
  m.role === 'assistant' &&
29
35
  m.status !== 'complete' &&
30
36
  m.status !== 'completed' &&
31
- m.status !== 'error' &&
32
- m.id !== currentMessageId
37
+ m.status !== 'error'
33
38
  ) {
39
+ if (parts.length === 0) {
40
+ debugLog(
41
+ `[buildHistoryMessages] Skipping empty assistant message ${m.id} with status ${m.status}`,
42
+ );
43
+ continue;
44
+ }
45
+
34
46
  debugLog(
35
- `[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status}`,
47
+ `[buildHistoryMessages] Including non-complete assistant message ${m.id} (status: ${m.status}) with ${parts.length} parts to preserve context`,
36
48
  );
37
- logPendingToolParts(db, m.id);
38
- continue;
39
49
  }
40
50
 
41
- const parts = await db
42
- .select()
43
- .from(messageParts)
44
- .where(eq(messageParts.messageId, m.id))
45
- .orderBy(asc(messageParts.index));
46
-
47
51
  if (m.role === 'user') {
48
52
  const uparts: UIMessage['parts'] = [];
49
53
  for (const p of parts) {
@@ -123,7 +127,6 @@ export async function buildHistoryMessages(
123
127
  if (t) assistantParts.push({ type: 'text', text: t });
124
128
  } catch {}
125
129
  } else if (p.type === 'tool_call') {
126
- // Skip compacted tool calls entirely
127
130
  if (p.compactedAt) continue;
128
131
 
129
132
  try {
@@ -141,7 +144,6 @@ export async function buildHistoryMessages(
141
144
  }
142
145
  } catch {}
143
146
  } else if (p.type === 'tool_result') {
144
- // Skip compacted tool results entirely
145
147
  if (p.compactedAt) continue;
146
148
 
147
149
  try {
@@ -159,7 +161,6 @@ export async function buildHistoryMessages(
159
161
  }
160
162
  } catch {}
161
163
  }
162
- // Skip error parts in history
163
164
  }
164
165
 
165
166
  const toolResultsById = new Map(
@@ -167,14 +168,12 @@ export async function buildHistoryMessages(
167
168
  );
168
169
 
169
170
  for (const call of toolCalls) {
170
- // Skip finish tool from history - it's internal loop control
171
171
  if (call.name === 'finish') continue;
172
172
 
173
173
  const toolType = `tool-${call.name}` as `tool-${string}`;
174
174
  let result = toolResultsById.get(call.callId);
175
175
 
176
176
  if (!result) {
177
- // Synthesize a result for incomplete tool calls to preserve history
178
177
  debugLog(
179
178
  `[buildHistoryMessages] Synthesizing error result for incomplete tool call ${call.name}#${call.callId}`,
180
179
  );
@@ -221,7 +220,7 @@ export async function buildHistoryMessages(
221
220
  return await convertToModelMessages(ui);
222
221
  }
223
222
 
224
- async function logPendingToolParts(
223
+ async function _logPendingToolParts(
225
224
  db: Awaited<ReturnType<typeof getDb>>,
226
225
  messageId: string,
227
226
  ) {
@@ -99,7 +99,7 @@ export function toClaudeCodeName(canonical: string): string {
99
99
  }
100
100
  // Default: convert snake_case to PascalCase
101
101
  return canonical
102
- .split('_')
102
+ .split(/[\s_]+/)
103
103
  .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
104
104
  .join('');
105
105
  }