@ottocode/server 0.1.264 → 0.1.266
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 +3 -3
- package/src/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +6 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +68 -264
- package/src/runtime/provider/xai.ts +8 -0
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import { getDb } from '@ottocode/database';
|
|
3
|
+
import { messageParts, messages, sessions } from '@ottocode/database/schema';
|
|
4
|
+
import {
|
|
5
|
+
hasConfiguredProvider,
|
|
6
|
+
loadConfig,
|
|
7
|
+
logger,
|
|
8
|
+
type ProviderId,
|
|
9
|
+
} from '@ottocode/sdk';
|
|
10
|
+
import { and, asc, count, desc, eq } from 'drizzle-orm';
|
|
11
|
+
import { publish } from '../../events/bus.ts';
|
|
12
|
+
import { serializeError } from '../../runtime/errors/api-error.ts';
|
|
13
|
+
|
|
14
|
+
async function loadProjectDb(projectRoot: string) {
|
|
15
|
+
const cfg = await loadConfig(projectRoot);
|
|
16
|
+
const db = await getDb(cfg.projectRoot);
|
|
17
|
+
return { cfg, db };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function buildResearchContext(
|
|
21
|
+
db: Awaited<ReturnType<typeof getDb>>,
|
|
22
|
+
researchSessionId: string,
|
|
23
|
+
) {
|
|
24
|
+
const researchMessages = await db
|
|
25
|
+
.select({
|
|
26
|
+
id: messages.id,
|
|
27
|
+
role: messages.role,
|
|
28
|
+
createdAt: messages.createdAt,
|
|
29
|
+
})
|
|
30
|
+
.from(messages)
|
|
31
|
+
.where(eq(messages.sessionId, researchSessionId))
|
|
32
|
+
.orderBy(asc(messages.createdAt));
|
|
33
|
+
|
|
34
|
+
let contextContent = '';
|
|
35
|
+
for (const msg of researchMessages) {
|
|
36
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
37
|
+
const parts = await db
|
|
38
|
+
.select({ type: messageParts.type, content: messageParts.content })
|
|
39
|
+
.from(messageParts)
|
|
40
|
+
.where(eq(messageParts.messageId, msg.id))
|
|
41
|
+
.orderBy(asc(messageParts.index));
|
|
42
|
+
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
if (part.type === 'text' && part.content) {
|
|
45
|
+
contextContent += `[${msg.role}]: ${part.content}\n\n`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return contextContent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function listResearchSessions(c: Context) {
|
|
55
|
+
const parentId = c.req.param('parentId');
|
|
56
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
57
|
+
const { cfg, db } = await loadProjectDb(projectRoot);
|
|
58
|
+
|
|
59
|
+
const parentRows = await db
|
|
60
|
+
.select()
|
|
61
|
+
.from(sessions)
|
|
62
|
+
.where(eq(sessions.id, parentId))
|
|
63
|
+
.limit(1);
|
|
64
|
+
|
|
65
|
+
if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
|
|
66
|
+
return c.json({ error: 'Parent session not found' }, 404);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const researchRows = await db
|
|
70
|
+
.select({
|
|
71
|
+
id: sessions.id,
|
|
72
|
+
title: sessions.title,
|
|
73
|
+
createdAt: sessions.createdAt,
|
|
74
|
+
lastActiveAt: sessions.lastActiveAt,
|
|
75
|
+
provider: sessions.provider,
|
|
76
|
+
model: sessions.model,
|
|
77
|
+
totalInputTokens: sessions.totalInputTokens,
|
|
78
|
+
totalOutputTokens: sessions.totalOutputTokens,
|
|
79
|
+
totalCachedTokens: sessions.totalCachedTokens,
|
|
80
|
+
totalCacheCreationTokens: sessions.totalCacheCreationTokens,
|
|
81
|
+
})
|
|
82
|
+
.from(sessions)
|
|
83
|
+
.where(
|
|
84
|
+
and(
|
|
85
|
+
eq(sessions.parentSessionId, parentId),
|
|
86
|
+
eq(sessions.sessionType, 'research'),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
.orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt));
|
|
90
|
+
|
|
91
|
+
const sessionsWithCounts = await Promise.all(
|
|
92
|
+
researchRows.map(async (row) => {
|
|
93
|
+
const msgCount = await db
|
|
94
|
+
.select({ count: count() })
|
|
95
|
+
.from(messages)
|
|
96
|
+
.where(eq(messages.sessionId, row.id));
|
|
97
|
+
return { ...row, messageCount: msgCount[0]?.count ?? 0 };
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return c.json({ sessions: sessionsWithCounts });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function createResearchSession(c: Context) {
|
|
105
|
+
const parentId = c.req.param('parentId');
|
|
106
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
107
|
+
const { cfg, db } = await loadProjectDb(projectRoot);
|
|
108
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
109
|
+
string,
|
|
110
|
+
unknown
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
const parentRows = await db
|
|
114
|
+
.select()
|
|
115
|
+
.from(sessions)
|
|
116
|
+
.where(eq(sessions.id, parentId))
|
|
117
|
+
.limit(1);
|
|
118
|
+
|
|
119
|
+
if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
|
|
120
|
+
return c.json({ error: 'Parent session not found' }, 404);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parent = parentRows[0];
|
|
124
|
+
const providerCandidate =
|
|
125
|
+
typeof body.provider === 'string' ? body.provider : undefined;
|
|
126
|
+
const provider: ProviderId =
|
|
127
|
+
providerCandidate && hasConfiguredProvider(cfg, providerCandidate)
|
|
128
|
+
? providerCandidate
|
|
129
|
+
: (parent.provider as ProviderId);
|
|
130
|
+
const modelCandidate =
|
|
131
|
+
typeof body.model === 'string' ? body.model.trim() : undefined;
|
|
132
|
+
const model = modelCandidate?.length ? modelCandidate : parent.model;
|
|
133
|
+
const id = crypto.randomUUID();
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const title = typeof body.title === 'string' ? body.title : null;
|
|
136
|
+
|
|
137
|
+
const row = {
|
|
138
|
+
id,
|
|
139
|
+
title,
|
|
140
|
+
agent: 'research',
|
|
141
|
+
provider,
|
|
142
|
+
model,
|
|
143
|
+
projectPath: cfg.projectRoot,
|
|
144
|
+
createdAt: now,
|
|
145
|
+
lastActiveAt: now,
|
|
146
|
+
parentSessionId: parentId,
|
|
147
|
+
sessionType: 'research',
|
|
148
|
+
totalInputTokens: null,
|
|
149
|
+
totalOutputTokens: null,
|
|
150
|
+
totalCachedTokens: null,
|
|
151
|
+
totalCacheCreationTokens: null,
|
|
152
|
+
totalReasoningTokens: null,
|
|
153
|
+
totalToolTimeMs: null,
|
|
154
|
+
toolCountsJson: null,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await db.insert(sessions).values(row);
|
|
159
|
+
publish({ type: 'session.created', sessionId: id, payload: row });
|
|
160
|
+
return c.json({ session: row, parentSessionId: parentId }, 201);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
logger.error('Failed to create research session', err);
|
|
163
|
+
const errorResponse = serializeError(err);
|
|
164
|
+
return c.json(errorResponse, errorResponse.error.status || 400);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function deleteResearchSession(c: Context) {
|
|
169
|
+
const researchId = c.req.param('researchId');
|
|
170
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
171
|
+
const { cfg, db } = await loadProjectDb(projectRoot);
|
|
172
|
+
|
|
173
|
+
const rows = await db
|
|
174
|
+
.select()
|
|
175
|
+
.from(sessions)
|
|
176
|
+
.where(eq(sessions.id, researchId))
|
|
177
|
+
.limit(1);
|
|
178
|
+
|
|
179
|
+
if (!rows.length) return c.json({ error: 'Research session not found' }, 404);
|
|
180
|
+
|
|
181
|
+
const session = rows[0];
|
|
182
|
+
if (session.projectPath !== cfg.projectRoot) {
|
|
183
|
+
return c.json({ error: 'Research session not found in this project' }, 404);
|
|
184
|
+
}
|
|
185
|
+
if (session.sessionType !== 'research') {
|
|
186
|
+
return c.json({ error: 'Session is not a research session' }, 400);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await db.delete(sessions).where(eq(sessions.id, researchId));
|
|
190
|
+
publish({
|
|
191
|
+
type: 'session.deleted',
|
|
192
|
+
sessionId: researchId,
|
|
193
|
+
payload: { id: researchId },
|
|
194
|
+
});
|
|
195
|
+
return c.json({ success: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function injectResearchContext(c: Context) {
|
|
199
|
+
const parentId = c.req.param('parentId');
|
|
200
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
201
|
+
const { cfg, db } = await loadProjectDb(projectRoot);
|
|
202
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
203
|
+
string,
|
|
204
|
+
unknown
|
|
205
|
+
>;
|
|
206
|
+
const researchSessionId =
|
|
207
|
+
typeof body.researchSessionId === 'string' ? body.researchSessionId : '';
|
|
208
|
+
const label =
|
|
209
|
+
typeof body.label === 'string' ? body.label : 'Research context';
|
|
210
|
+
|
|
211
|
+
if (!researchSessionId)
|
|
212
|
+
return c.json({ error: 'researchSessionId is required' }, 400);
|
|
213
|
+
|
|
214
|
+
const [parentRows, researchRows] = await Promise.all([
|
|
215
|
+
db.select().from(sessions).where(eq(sessions.id, parentId)).limit(1),
|
|
216
|
+
db
|
|
217
|
+
.select()
|
|
218
|
+
.from(sessions)
|
|
219
|
+
.where(eq(sessions.id, researchSessionId))
|
|
220
|
+
.limit(1),
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
if (!parentRows.length || parentRows[0].projectPath !== cfg.projectRoot) {
|
|
224
|
+
return c.json({ error: 'Parent session not found' }, 404);
|
|
225
|
+
}
|
|
226
|
+
if (!researchRows.length || researchRows[0].sessionType !== 'research') {
|
|
227
|
+
return c.json({ error: 'Research session not found' }, 404);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const contextContent = await buildResearchContext(db, researchSessionId);
|
|
231
|
+
const injectedContext = `<research-context from="${researchSessionId}" label="${label}" injected-at="${new Date().toISOString()}">\n${contextContent}</research-context>`;
|
|
232
|
+
|
|
233
|
+
return c.json({
|
|
234
|
+
content: injectedContext,
|
|
235
|
+
label,
|
|
236
|
+
sessionId: researchSessionId,
|
|
237
|
+
parentSessionId: parentId,
|
|
238
|
+
tokenEstimate: Math.ceil(injectedContext.length / 4),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function exportResearchSession(c: Context) {
|
|
243
|
+
const researchId = c.req.param('researchId');
|
|
244
|
+
const projectRoot = c.req.query('project') || process.cwd();
|
|
245
|
+
const { cfg, db } = await loadProjectDb(projectRoot);
|
|
246
|
+
const body = (await c.req.json().catch(() => ({}))) as Record<
|
|
247
|
+
string,
|
|
248
|
+
unknown
|
|
249
|
+
>;
|
|
250
|
+
|
|
251
|
+
const researchRows = await db
|
|
252
|
+
.select()
|
|
253
|
+
.from(sessions)
|
|
254
|
+
.where(eq(sessions.id, researchId))
|
|
255
|
+
.limit(1);
|
|
256
|
+
|
|
257
|
+
if (!researchRows.length || researchRows[0].sessionType !== 'research') {
|
|
258
|
+
return c.json({ error: 'Research session not found' }, 404);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const researchSession = researchRows[0];
|
|
262
|
+
if (researchSession.projectPath !== cfg.projectRoot) {
|
|
263
|
+
return c.json({ error: 'Research session not in this project' }, 404);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const providerCandidate =
|
|
267
|
+
typeof body.provider === 'string' ? body.provider : undefined;
|
|
268
|
+
const provider: ProviderId =
|
|
269
|
+
providerCandidate && hasConfiguredProvider(cfg, providerCandidate)
|
|
270
|
+
? providerCandidate
|
|
271
|
+
: cfg.defaults.provider;
|
|
272
|
+
const modelCandidate =
|
|
273
|
+
typeof body.model === 'string' ? body.model.trim() : undefined;
|
|
274
|
+
const model = modelCandidate?.length ? modelCandidate : cfg.defaults.model;
|
|
275
|
+
const agentCandidate =
|
|
276
|
+
typeof body.agent === 'string' ? body.agent.trim() : undefined;
|
|
277
|
+
const agent = agentCandidate?.length ? agentCandidate : cfg.defaults.agent;
|
|
278
|
+
const contextContent = await buildResearchContext(db, researchId);
|
|
279
|
+
const injectedContext = `<research-context from="${researchId}" exported-at="${new Date().toISOString()}">\n${contextContent}</research-context>`;
|
|
280
|
+
const newSessionId = crypto.randomUUID();
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
|
|
283
|
+
await db.insert(sessions).values({
|
|
284
|
+
id: newSessionId,
|
|
285
|
+
title: researchSession.title ? `From: ${researchSession.title}` : null,
|
|
286
|
+
agent,
|
|
287
|
+
provider,
|
|
288
|
+
model,
|
|
289
|
+
projectPath: cfg.projectRoot,
|
|
290
|
+
createdAt: now,
|
|
291
|
+
lastActiveAt: now,
|
|
292
|
+
parentSessionId: null,
|
|
293
|
+
sessionType: 'main',
|
|
294
|
+
totalInputTokens: null,
|
|
295
|
+
totalOutputTokens: null,
|
|
296
|
+
totalCachedTokens: null,
|
|
297
|
+
totalCacheCreationTokens: null,
|
|
298
|
+
totalReasoningTokens: null,
|
|
299
|
+
totalToolTimeMs: null,
|
|
300
|
+
toolCountsJson: null,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const msgId = crypto.randomUUID();
|
|
304
|
+
const partId = crypto.randomUUID();
|
|
305
|
+
await db.insert(messages).values({
|
|
306
|
+
id: msgId,
|
|
307
|
+
sessionId: newSessionId,
|
|
308
|
+
role: 'system',
|
|
309
|
+
status: 'complete',
|
|
310
|
+
agent,
|
|
311
|
+
provider,
|
|
312
|
+
model,
|
|
313
|
+
createdAt: now,
|
|
314
|
+
completedAt: now,
|
|
315
|
+
});
|
|
316
|
+
await db.insert(messageParts).values({
|
|
317
|
+
id: partId,
|
|
318
|
+
messageId: msgId,
|
|
319
|
+
index: 0,
|
|
320
|
+
type: 'text',
|
|
321
|
+
content: injectedContext,
|
|
322
|
+
agent,
|
|
323
|
+
provider,
|
|
324
|
+
model,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
publish({
|
|
328
|
+
type: 'session.created',
|
|
329
|
+
sessionId: newSessionId,
|
|
330
|
+
payload: { id: newSessionId },
|
|
331
|
+
});
|
|
332
|
+
const newSession = await db
|
|
333
|
+
.select()
|
|
334
|
+
.from(sessions)
|
|
335
|
+
.where(eq(sessions.id, newSessionId))
|
|
336
|
+
.limit(1);
|
|
337
|
+
|
|
338
|
+
return c.json({ newSession: newSession[0], injectedContext }, 201);
|
|
339
|
+
}
|