@ottocode/server 0.1.173

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.
Files changed (111) hide show
  1. package/package.json +42 -0
  2. package/src/events/bus.ts +43 -0
  3. package/src/events/types.ts +32 -0
  4. package/src/index.ts +281 -0
  5. package/src/openapi/helpers.ts +64 -0
  6. package/src/openapi/paths/ask.ts +70 -0
  7. package/src/openapi/paths/config.ts +218 -0
  8. package/src/openapi/paths/files.ts +72 -0
  9. package/src/openapi/paths/git.ts +457 -0
  10. package/src/openapi/paths/messages.ts +92 -0
  11. package/src/openapi/paths/sessions.ts +90 -0
  12. package/src/openapi/paths/setu.ts +154 -0
  13. package/src/openapi/paths/stream.ts +26 -0
  14. package/src/openapi/paths/terminals.ts +226 -0
  15. package/src/openapi/schemas.ts +345 -0
  16. package/src/openapi/spec.ts +49 -0
  17. package/src/presets.ts +85 -0
  18. package/src/routes/ask.ts +113 -0
  19. package/src/routes/auth.ts +592 -0
  20. package/src/routes/branch.ts +106 -0
  21. package/src/routes/config/agents.ts +44 -0
  22. package/src/routes/config/cwd.ts +21 -0
  23. package/src/routes/config/defaults.ts +45 -0
  24. package/src/routes/config/index.ts +16 -0
  25. package/src/routes/config/main.ts +73 -0
  26. package/src/routes/config/models.ts +139 -0
  27. package/src/routes/config/providers.ts +46 -0
  28. package/src/routes/config/utils.ts +120 -0
  29. package/src/routes/files.ts +218 -0
  30. package/src/routes/git/branch.ts +75 -0
  31. package/src/routes/git/commit.ts +209 -0
  32. package/src/routes/git/diff.ts +137 -0
  33. package/src/routes/git/index.ts +18 -0
  34. package/src/routes/git/push.ts +160 -0
  35. package/src/routes/git/schemas.ts +48 -0
  36. package/src/routes/git/staging.ts +208 -0
  37. package/src/routes/git/status.ts +83 -0
  38. package/src/routes/git/types.ts +31 -0
  39. package/src/routes/git/utils.ts +249 -0
  40. package/src/routes/openapi.ts +6 -0
  41. package/src/routes/research.ts +392 -0
  42. package/src/routes/root.ts +5 -0
  43. package/src/routes/session-approval.ts +63 -0
  44. package/src/routes/session-files.ts +387 -0
  45. package/src/routes/session-messages.ts +170 -0
  46. package/src/routes/session-stream.ts +61 -0
  47. package/src/routes/sessions.ts +814 -0
  48. package/src/routes/setu.ts +346 -0
  49. package/src/routes/terminals.ts +227 -0
  50. package/src/runtime/agent/registry.ts +351 -0
  51. package/src/runtime/agent/runner-reasoning.ts +108 -0
  52. package/src/runtime/agent/runner-setup.ts +257 -0
  53. package/src/runtime/agent/runner.ts +375 -0
  54. package/src/runtime/agent-registry.ts +6 -0
  55. package/src/runtime/ask/service.ts +369 -0
  56. package/src/runtime/context/environment.ts +202 -0
  57. package/src/runtime/debug/index.ts +117 -0
  58. package/src/runtime/debug/state.ts +140 -0
  59. package/src/runtime/errors/api-error.ts +192 -0
  60. package/src/runtime/errors/handling.ts +199 -0
  61. package/src/runtime/message/compaction-auto.ts +154 -0
  62. package/src/runtime/message/compaction-context.ts +101 -0
  63. package/src/runtime/message/compaction-detect.ts +26 -0
  64. package/src/runtime/message/compaction-limits.ts +37 -0
  65. package/src/runtime/message/compaction-mark.ts +111 -0
  66. package/src/runtime/message/compaction-prune.ts +75 -0
  67. package/src/runtime/message/compaction.ts +21 -0
  68. package/src/runtime/message/history-builder.ts +266 -0
  69. package/src/runtime/message/service.ts +468 -0
  70. package/src/runtime/message/tool-history-tracker.ts +204 -0
  71. package/src/runtime/prompt/builder.ts +167 -0
  72. package/src/runtime/provider/anthropic.ts +50 -0
  73. package/src/runtime/provider/copilot.ts +12 -0
  74. package/src/runtime/provider/google.ts +8 -0
  75. package/src/runtime/provider/index.ts +60 -0
  76. package/src/runtime/provider/moonshot.ts +8 -0
  77. package/src/runtime/provider/oauth-adapter.ts +237 -0
  78. package/src/runtime/provider/openai.ts +18 -0
  79. package/src/runtime/provider/opencode.ts +7 -0
  80. package/src/runtime/provider/openrouter.ts +7 -0
  81. package/src/runtime/provider/selection.ts +118 -0
  82. package/src/runtime/provider/setu.ts +126 -0
  83. package/src/runtime/provider/zai.ts +16 -0
  84. package/src/runtime/session/branch.ts +280 -0
  85. package/src/runtime/session/db-operations.ts +285 -0
  86. package/src/runtime/session/manager.ts +99 -0
  87. package/src/runtime/session/queue.ts +243 -0
  88. package/src/runtime/stream/abort-handler.ts +65 -0
  89. package/src/runtime/stream/error-handler.ts +371 -0
  90. package/src/runtime/stream/finish-handler.ts +101 -0
  91. package/src/runtime/stream/handlers.ts +5 -0
  92. package/src/runtime/stream/step-finish.ts +93 -0
  93. package/src/runtime/stream/types.ts +25 -0
  94. package/src/runtime/tools/approval.ts +180 -0
  95. package/src/runtime/tools/context.ts +83 -0
  96. package/src/runtime/tools/mapping.ts +154 -0
  97. package/src/runtime/tools/setup.ts +44 -0
  98. package/src/runtime/topup/manager.ts +110 -0
  99. package/src/runtime/utils/cwd.ts +69 -0
  100. package/src/runtime/utils/token.ts +35 -0
  101. package/src/tools/adapter.ts +634 -0
  102. package/src/tools/database/get-parent-session.ts +183 -0
  103. package/src/tools/database/get-session-context.ts +161 -0
  104. package/src/tools/database/index.ts +42 -0
  105. package/src/tools/database/present-session-links.ts +47 -0
  106. package/src/tools/database/query-messages.ts +160 -0
  107. package/src/tools/database/query-sessions.ts +126 -0
  108. package/src/tools/database/search-history.ts +135 -0
  109. package/src/types/sql-imports.d.ts +5 -0
  110. package/sst-env.d.ts +8 -0
  111. package/tsconfig.json +7 -0
@@ -0,0 +1,99 @@
1
+ import { desc, eq } from 'drizzle-orm';
2
+ import type { OttoConfig } from '@ottocode/sdk';
3
+ import type { DB } from '@ottocode/database';
4
+ import { sessions } from '@ottocode/database/schema';
5
+ import {
6
+ validateProviderModel,
7
+ isProviderAuthorized,
8
+ ensureProviderEnv,
9
+ type ProviderId,
10
+ } from '@ottocode/sdk';
11
+ import { publish } from '../../events/bus.ts';
12
+
13
+ type SessionRow = typeof sessions.$inferSelect;
14
+
15
+ type CreateSessionInput = {
16
+ db: DB;
17
+ cfg: OttoConfig;
18
+ agent: string;
19
+ provider: ProviderId;
20
+ model: string;
21
+ title?: string | null;
22
+ };
23
+
24
+ export async function createSession({
25
+ db,
26
+ cfg,
27
+ agent,
28
+ provider,
29
+ model,
30
+ title,
31
+ }: CreateSessionInput): Promise<SessionRow> {
32
+ validateProviderModel(provider, model);
33
+ const authorized = await isProviderAuthorized(cfg, provider);
34
+ if (!authorized) {
35
+ throw new Error(
36
+ `Provider ${provider} is not configured. Run \`otto auth login\` to add credentials.`,
37
+ );
38
+ }
39
+ await ensureProviderEnv(cfg, provider);
40
+ const id = crypto.randomUUID();
41
+ const now = Date.now();
42
+ const row = {
43
+ id,
44
+ title: title ?? null,
45
+ agent,
46
+ provider,
47
+ model,
48
+ projectPath: cfg.projectRoot,
49
+ createdAt: now,
50
+ lastActiveAt: now,
51
+ totalInputTokens: null,
52
+ totalOutputTokens: null,
53
+ totalCachedTokens: null,
54
+ totalCacheCreationTokens: null,
55
+ totalReasoningTokens: null,
56
+ totalToolTimeMs: null,
57
+ toolCountsJson: null,
58
+ currentContextTokens: null,
59
+ };
60
+ await db.insert(sessions).values(row);
61
+ publish({ type: 'session.created', sessionId: id, payload: row });
62
+ return row;
63
+ }
64
+
65
+ type GetSessionInput = {
66
+ db: DB;
67
+ projectPath: string;
68
+ sessionId: string;
69
+ };
70
+
71
+ export async function getSessionById({
72
+ db,
73
+ projectPath,
74
+ sessionId,
75
+ }: GetSessionInput): Promise<SessionRow | undefined> {
76
+ const rows = await db
77
+ .select()
78
+ .from(sessions)
79
+ .where(eq(sessions.id, sessionId));
80
+ if (!rows.length) return undefined;
81
+ const row = rows[0];
82
+ if (row.projectPath !== projectPath) return undefined;
83
+ return row;
84
+ }
85
+
86
+ type GetLastSessionInput = { db: DB; projectPath: string };
87
+
88
+ export async function getLastSession({
89
+ db,
90
+ projectPath,
91
+ }: GetLastSessionInput): Promise<SessionRow | undefined> {
92
+ const rows = await db
93
+ .select()
94
+ .from(sessions)
95
+ .where(eq(sessions.projectPath, projectPath))
96
+ .orderBy(desc(sessions.lastActiveAt), desc(sessions.createdAt))
97
+ .limit(1);
98
+ return rows[0];
99
+ }
@@ -0,0 +1,243 @@
1
+ import type { ProviderName } from '../provider/index.ts';
2
+ import { publish } from '../../events/bus.ts';
3
+ import type { ToolApprovalMode } from '../tools/approval.ts';
4
+
5
+ export type RunOpts = {
6
+ sessionId: string;
7
+ assistantMessageId: string;
8
+ agent: string;
9
+ provider: ProviderName;
10
+ model: string;
11
+ projectRoot: string;
12
+ oneShot?: boolean;
13
+ userContext?: string;
14
+ reasoningText?: boolean;
15
+ abortSignal?: AbortSignal;
16
+ isCompactCommand?: boolean;
17
+ compactionContext?: string;
18
+ toolApprovalMode?: ToolApprovalMode;
19
+ compactionRetries?: number;
20
+ };
21
+
22
+ export type QueuedMessage = {
23
+ messageId: string;
24
+ position: number;
25
+ };
26
+
27
+ type RunnerState = {
28
+ queue: RunOpts[];
29
+ running: boolean;
30
+ currentMessageId: string | null;
31
+ };
32
+
33
+ // Global state for session queues
34
+ const runners = new Map<string, RunnerState>();
35
+
36
+ // Track active abort controllers per MESSAGE (not session)
37
+ const messageAbortControllers = new Map<string, AbortController>();
38
+
39
+ function publishQueueState(sessionId: string) {
40
+ const state = runners.get(sessionId);
41
+ if (!state) return;
42
+
43
+ const queuedMessages: QueuedMessage[] = state.queue.map((opts, index) => ({
44
+ messageId: opts.assistantMessageId,
45
+ position: index,
46
+ }));
47
+
48
+ publish({
49
+ type: 'queue.updated',
50
+ sessionId,
51
+ payload: {
52
+ currentMessageId: state.currentMessageId,
53
+ queuedMessages,
54
+ queueLength: state.queue.length,
55
+ },
56
+ });
57
+ }
58
+
59
+ /**
60
+ * Enqueues an assistant run for a given session.
61
+ * Creates an abort controller per message.
62
+ */
63
+ export function enqueueAssistantRun(
64
+ opts: Omit<RunOpts, 'abortSignal'>,
65
+ processQueueFn: (sessionId: string) => Promise<void>,
66
+ ) {
67
+ const abortController = new AbortController();
68
+ messageAbortControllers.set(opts.assistantMessageId, abortController);
69
+
70
+ const state = runners.get(opts.sessionId) ?? {
71
+ queue: [],
72
+ running: false,
73
+ currentMessageId: null,
74
+ };
75
+ state.queue.push({ ...opts, abortSignal: abortController.signal });
76
+ runners.set(opts.sessionId, state);
77
+
78
+ publishQueueState(opts.sessionId);
79
+
80
+ if (!state.running) void processQueueFn(opts.sessionId);
81
+ }
82
+
83
+ /**
84
+ * Aborts the currently running message for a session.
85
+ * Optionally clears the queue.
86
+ */
87
+ export function abortSession(sessionId: string, clearQueue = false) {
88
+ const state = runners.get(sessionId);
89
+ if (!state) return;
90
+
91
+ // Abort the currently running message
92
+ if (state.currentMessageId) {
93
+ const controller = messageAbortControllers.get(state.currentMessageId);
94
+ if (controller) {
95
+ controller.abort();
96
+ messageAbortControllers.delete(state.currentMessageId);
97
+ }
98
+ }
99
+
100
+ // Optionally clear the queue and abort all queued messages
101
+ if (clearQueue && state.queue.length > 0) {
102
+ for (const opts of state.queue) {
103
+ const controller = messageAbortControllers.get(opts.assistantMessageId);
104
+ if (controller) {
105
+ controller.abort();
106
+ messageAbortControllers.delete(opts.assistantMessageId);
107
+ }
108
+ }
109
+ state.queue = [];
110
+ publishQueueState(sessionId);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Aborts a specific message by its ID.
116
+ * If it's currently running, aborts the stream.
117
+ * If it's queued, removes it from the queue.
118
+ */
119
+ export function abortMessage(
120
+ sessionId: string,
121
+ messageId: string,
122
+ ): { removed: boolean; wasRunning: boolean } {
123
+ const state = runners.get(sessionId);
124
+ if (!state) return { removed: false, wasRunning: false };
125
+
126
+ // Check if this is the currently running message
127
+ if (state.currentMessageId === messageId) {
128
+ const controller = messageAbortControllers.get(messageId);
129
+ if (controller) {
130
+ controller.abort();
131
+ messageAbortControllers.delete(messageId);
132
+ }
133
+ return { removed: true, wasRunning: true };
134
+ }
135
+
136
+ // Check if it's in the queue
137
+ const index = state.queue.findIndex(
138
+ (opts) => opts.assistantMessageId === messageId,
139
+ );
140
+ if (index !== -1) {
141
+ state.queue.splice(index, 1);
142
+ const controller = messageAbortControllers.get(messageId);
143
+ if (controller) {
144
+ controller.abort();
145
+ messageAbortControllers.delete(messageId);
146
+ }
147
+ publishQueueState(sessionId);
148
+ return { removed: true, wasRunning: false };
149
+ }
150
+
151
+ return { removed: false, wasRunning: false };
152
+ }
153
+
154
+ /**
155
+ * Removes a queued message (not the currently running one).
156
+ */
157
+ export function removeFromQueue(sessionId: string, messageId: string): boolean {
158
+ const state = runners.get(sessionId);
159
+ if (!state) return false;
160
+
161
+ // Don't allow removing the currently running message via this function
162
+ if (state.currentMessageId === messageId) {
163
+ return false;
164
+ }
165
+
166
+ const index = state.queue.findIndex(
167
+ (opts) => opts.assistantMessageId === messageId,
168
+ );
169
+ if (index === -1) return false;
170
+
171
+ state.queue.splice(index, 1);
172
+ const controller = messageAbortControllers.get(messageId);
173
+ if (controller) {
174
+ controller.abort();
175
+ messageAbortControllers.delete(messageId);
176
+ }
177
+
178
+ publishQueueState(sessionId);
179
+ return true;
180
+ }
181
+
182
+ /**
183
+ * Gets the current queue state for a session.
184
+ */
185
+ export function getQueueState(sessionId: string): {
186
+ currentMessageId: string | null;
187
+ queuedMessages: QueuedMessage[];
188
+ isRunning: boolean;
189
+ } | null {
190
+ const state = runners.get(sessionId);
191
+ if (!state) return null;
192
+
193
+ return {
194
+ currentMessageId: state.currentMessageId,
195
+ queuedMessages: state.queue.map((opts, index) => ({
196
+ messageId: opts.assistantMessageId,
197
+ position: index,
198
+ })),
199
+ isRunning: state.running,
200
+ };
201
+ }
202
+
203
+ export function getRunnerState(
204
+ sessionId: string,
205
+ ): { queue: RunOpts[]; running: boolean } | undefined {
206
+ return runners.get(sessionId);
207
+ }
208
+
209
+ export function setRunning(sessionId: string, running: boolean) {
210
+ const state = runners.get(sessionId);
211
+ if (state) state.running = running;
212
+ }
213
+
214
+ export function setCurrentMessage(sessionId: string, messageId: string | null) {
215
+ const state = runners.get(sessionId);
216
+ if (state) {
217
+ state.currentMessageId = messageId;
218
+ publishQueueState(sessionId);
219
+ }
220
+ }
221
+
222
+ export function dequeueJob(sessionId: string): RunOpts | undefined {
223
+ const state = runners.get(sessionId);
224
+ const job = state?.queue.shift();
225
+ if (job && state) {
226
+ state.currentMessageId = job.assistantMessageId;
227
+ publishQueueState(sessionId);
228
+ }
229
+ return job;
230
+ }
231
+
232
+ export function cleanupSession(sessionId: string) {
233
+ const state = runners.get(sessionId);
234
+ if (state && state.queue.length === 0 && !state.running) {
235
+ // Clean up any lingering abort controller for current message
236
+ if (state.currentMessageId) {
237
+ messageAbortControllers.delete(state.currentMessageId);
238
+ }
239
+ state.currentMessageId = null;
240
+ runners.delete(sessionId);
241
+ publishQueueState(sessionId);
242
+ }
243
+ }
@@ -0,0 +1,65 @@
1
+ import type { getDb } from '@ottocode/database';
2
+ import { messages, messageParts } from '@ottocode/database/schema';
3
+ import { eq } from 'drizzle-orm';
4
+ import { publish } from '../../events/bus.ts';
5
+ import type { RunOpts } from '../session/queue.ts';
6
+ import type { ToolAdapterContext } from '../../tools/adapter.ts';
7
+ import type { AbortEvent } from './types.ts';
8
+
9
+ export function createAbortHandler(
10
+ opts: RunOpts,
11
+ db: Awaited<ReturnType<typeof getDb>>,
12
+ getStepIndex: () => number,
13
+ sharedCtx: ToolAdapterContext,
14
+ ) {
15
+ return async ({ steps }: AbortEvent) => {
16
+ const stepIndex = getStepIndex();
17
+
18
+ const abortPartId = crypto.randomUUID();
19
+ await db.insert(messageParts).values({
20
+ id: abortPartId,
21
+ messageId: opts.assistantMessageId,
22
+ index: await sharedCtx.nextIndex(),
23
+ stepIndex,
24
+ type: 'error',
25
+ content: JSON.stringify({
26
+ message: 'Generation stopped by user',
27
+ type: 'abort',
28
+ isAborted: true,
29
+ stepsCompleted: steps.length,
30
+ }),
31
+ agent: opts.agent,
32
+ provider: opts.provider,
33
+ model: opts.model,
34
+ startedAt: Date.now(),
35
+ completedAt: Date.now(),
36
+ });
37
+
38
+ await db
39
+ .update(messages)
40
+ .set({
41
+ status: 'error',
42
+ error: 'Generation stopped by user',
43
+ errorType: 'abort',
44
+ errorDetails: JSON.stringify({
45
+ stepsCompleted: steps.length,
46
+ abortedAt: Date.now(),
47
+ }),
48
+ isAborted: true,
49
+ })
50
+ .where(eq(messages.id, opts.assistantMessageId));
51
+
52
+ publish({
53
+ type: 'error',
54
+ sessionId: opts.sessionId,
55
+ payload: {
56
+ messageId: opts.assistantMessageId,
57
+ partId: abortPartId,
58
+ error: 'Generation stopped by user',
59
+ errorType: 'abort',
60
+ isAborted: true,
61
+ stepsCompleted: steps.length,
62
+ },
63
+ });
64
+ };
65
+ }