@ottocode/server 0.1.210 → 0.1.212
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/events/types.ts +1 -0
- package/src/routes/files.ts +72 -18
- package/src/routes/git/commit.ts +9 -3
- package/src/routes/sessions.ts +12 -2
- package/src/runtime/agent/runner-setup.ts +5 -1
- package/src/runtime/agent/runner.ts +7 -1
- package/src/runtime/message/history-builder.ts +5 -2
- package/src/runtime/message/service.ts +8 -2
- package/src/runtime/stream/error-handler.ts +14 -2
- package/src/runtime/tools/setup.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.212",
|
|
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.
|
|
53
|
-
"@ottocode/database": "0.1.
|
|
52
|
+
"@ottocode/sdk": "0.1.212",
|
|
53
|
+
"@ottocode/database": "0.1.212",
|
|
54
54
|
"drizzle-orm": "^0.44.5",
|
|
55
55
|
"hono": "^4.9.9",
|
|
56
56
|
"zod": "^4.1.8"
|
package/src/events/types.ts
CHANGED
package/src/routes/files.ts
CHANGED
|
@@ -10,14 +10,7 @@ import { resolveBinary } from '@ottocode/sdk/tools/bin-manager';
|
|
|
10
10
|
|
|
11
11
|
const execAsync = promisify(exec);
|
|
12
12
|
|
|
13
|
-
const EXCLUDED_FILES = new Set([
|
|
14
|
-
'.DS_Store',
|
|
15
|
-
'bun.lockb',
|
|
16
|
-
'.env',
|
|
17
|
-
'.env.local',
|
|
18
|
-
'.env.production',
|
|
19
|
-
'.env.development',
|
|
20
|
-
]);
|
|
13
|
+
const EXCLUDED_FILES = new Set(['.DS_Store', 'bun.lockb']);
|
|
21
14
|
|
|
22
15
|
const EXCLUDED_DIRS = new Set([
|
|
23
16
|
'node_modules',
|
|
@@ -35,6 +28,13 @@ const EXCLUDED_DIRS = new Set([
|
|
|
35
28
|
'.cache',
|
|
36
29
|
'__pycache__',
|
|
37
30
|
'.tsbuildinfo',
|
|
31
|
+
'target',
|
|
32
|
+
'.cargo',
|
|
33
|
+
'.rustup',
|
|
34
|
+
'vendor',
|
|
35
|
+
'.gradle',
|
|
36
|
+
'.idea',
|
|
37
|
+
'.vscode',
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
40
|
function shouldExcludeFile(name: string): boolean {
|
|
@@ -48,11 +48,18 @@ function shouldExcludeDir(name: string): boolean {
|
|
|
48
48
|
async function listFilesWithRg(
|
|
49
49
|
projectRoot: string,
|
|
50
50
|
limit: number,
|
|
51
|
+
includeIgnored = false,
|
|
51
52
|
): Promise<{ files: string[]; truncated: boolean }> {
|
|
52
53
|
const rgBin = await resolveBinary('rg');
|
|
53
54
|
|
|
54
55
|
return new Promise((resolve) => {
|
|
55
|
-
const args = ['--files', '--hidden', '--glob', '
|
|
56
|
+
const args = ['--files', '--hidden', '--glob', '!.*/', '--sort', 'path'];
|
|
57
|
+
if (includeIgnored) {
|
|
58
|
+
args.push('--no-ignore');
|
|
59
|
+
}
|
|
60
|
+
for (const dir of EXCLUDED_DIRS) {
|
|
61
|
+
args.push('--glob', `!**/${dir}/`);
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
const proc = spawn(rgBin, args, { cwd: projectRoot });
|
|
58
65
|
let stdout = '';
|
|
@@ -232,14 +239,42 @@ async function getChangedFiles(
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
|
|
242
|
+
async function getGitIgnoredFiles(
|
|
243
|
+
projectRoot: string,
|
|
244
|
+
files: string[],
|
|
245
|
+
): Promise<Set<string>> {
|
|
246
|
+
if (files.length === 0) return new Set();
|
|
247
|
+
try {
|
|
248
|
+
return new Promise((resolve) => {
|
|
249
|
+
const proc = spawn('git', ['check-ignore', '--stdin'], {
|
|
250
|
+
cwd: projectRoot,
|
|
251
|
+
});
|
|
252
|
+
let stdout = '';
|
|
253
|
+
proc.stdout.on('data', (data) => {
|
|
254
|
+
stdout += data.toString();
|
|
255
|
+
});
|
|
256
|
+
proc.on('close', () => {
|
|
257
|
+
resolve(new Set(stdout.split('\n').filter(Boolean)));
|
|
258
|
+
});
|
|
259
|
+
proc.on('error', () => {
|
|
260
|
+
resolve(new Set());
|
|
261
|
+
});
|
|
262
|
+
proc.stdin.write(files.join('\n'));
|
|
263
|
+
proc.stdin.end();
|
|
264
|
+
});
|
|
265
|
+
} catch (_err) {
|
|
266
|
+
return new Set();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
235
270
|
export function registerFilesRoutes(app: Hono) {
|
|
236
271
|
app.get('/v1/files', async (c) => {
|
|
237
272
|
try {
|
|
238
273
|
const projectRoot = c.req.query('project') || process.cwd();
|
|
239
274
|
const maxDepth = Number.parseInt(c.req.query('maxDepth') || '10', 10);
|
|
240
|
-
const limit = Number.parseInt(c.req.query('limit') || '
|
|
275
|
+
const limit = Number.parseInt(c.req.query('limit') || '10000', 10);
|
|
241
276
|
|
|
242
|
-
let result = await listFilesWithRg(projectRoot, limit);
|
|
277
|
+
let result = await listFilesWithRg(projectRoot, limit, true);
|
|
243
278
|
|
|
244
279
|
if (result.files.length === 0) {
|
|
245
280
|
const gitignorePatterns = await parseGitignore(projectRoot);
|
|
@@ -254,9 +289,15 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
254
289
|
);
|
|
255
290
|
}
|
|
256
291
|
|
|
257
|
-
const changedFiles = await
|
|
292
|
+
const [changedFiles, ignoredFiles] = await Promise.all([
|
|
293
|
+
getChangedFiles(projectRoot),
|
|
294
|
+
getGitIgnoredFiles(projectRoot, result.files),
|
|
295
|
+
]);
|
|
258
296
|
|
|
259
297
|
result.files.sort((a, b) => {
|
|
298
|
+
const aIgnored = ignoredFiles.has(a);
|
|
299
|
+
const bIgnored = ignoredFiles.has(b);
|
|
300
|
+
if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
|
|
260
301
|
const aChanged = changedFiles.has(a);
|
|
261
302
|
const bChanged = changedFiles.has(b);
|
|
262
303
|
if (aChanged && !bChanged) return -1;
|
|
@@ -266,6 +307,7 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
266
307
|
|
|
267
308
|
return c.json({
|
|
268
309
|
files: result.files,
|
|
310
|
+
ignoredFiles: Array.from(ignoredFiles),
|
|
269
311
|
changedFiles: Array.from(changedFiles.entries()).map(
|
|
270
312
|
([path, status]) => ({
|
|
271
313
|
path,
|
|
@@ -293,25 +335,37 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
293
335
|
name: string;
|
|
294
336
|
path: string;
|
|
295
337
|
type: 'file' | 'directory';
|
|
338
|
+
gitignored?: boolean;
|
|
296
339
|
}> = [];
|
|
297
340
|
|
|
298
341
|
for (const entry of entries) {
|
|
299
|
-
if (entry.name.startsWith('.') && entry.name !== '.otto') continue;
|
|
300
342
|
const relPath = relative(projectRoot, join(targetDir, entry.name));
|
|
301
343
|
|
|
302
344
|
if (entry.isDirectory()) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
345
|
+
const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
|
|
346
|
+
items.push({
|
|
347
|
+
name: entry.name,
|
|
348
|
+
path: relPath,
|
|
349
|
+
type: 'directory',
|
|
350
|
+
gitignored: ignored || undefined,
|
|
351
|
+
});
|
|
306
352
|
} else if (entry.isFile()) {
|
|
307
353
|
if (shouldExcludeFile(entry.name)) continue;
|
|
308
|
-
|
|
309
|
-
items.push({
|
|
354
|
+
const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
|
|
355
|
+
items.push({
|
|
356
|
+
name: entry.name,
|
|
357
|
+
path: relPath,
|
|
358
|
+
type: 'file',
|
|
359
|
+
gitignored: ignored || undefined,
|
|
360
|
+
});
|
|
310
361
|
}
|
|
311
362
|
}
|
|
312
363
|
|
|
313
364
|
items.sort((a, b) => {
|
|
314
365
|
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
366
|
+
const aIgnored = a.gitignored ?? false;
|
|
367
|
+
const bIgnored = b.gitignored ?? false;
|
|
368
|
+
if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
|
|
315
369
|
return a.name.localeCompare(b.name);
|
|
316
370
|
});
|
|
317
371
|
|
package/src/routes/git/commit.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
detectOAuth,
|
|
16
16
|
adaptSimpleCall,
|
|
17
17
|
} from '../../runtime/provider/oauth-adapter.ts';
|
|
18
|
+
import { appendCoAuthorTrailer } from '@ottocode/sdk';
|
|
18
19
|
|
|
19
20
|
const execFileAsync = promisify(execFile);
|
|
20
21
|
|
|
@@ -36,9 +37,14 @@ export function registerCommitRoutes(app: Hono) {
|
|
|
36
37
|
|
|
37
38
|
const { gitRoot } = validation;
|
|
38
39
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const fullMessage = appendCoAuthorTrailer(message);
|
|
41
|
+
const { stdout } = await execFileAsync(
|
|
42
|
+
'git',
|
|
43
|
+
['commit', '-m', fullMessage],
|
|
44
|
+
{
|
|
45
|
+
cwd: gitRoot,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
42
48
|
|
|
43
49
|
return c.json({
|
|
44
50
|
status: 'ok',
|
package/src/routes/sessions.ts
CHANGED
|
@@ -383,7 +383,18 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
383
383
|
// Delete message parts first (foreign key constraint)
|
|
384
384
|
await db
|
|
385
385
|
.delete(messageParts)
|
|
386
|
-
.where(
|
|
386
|
+
.where(
|
|
387
|
+
and(
|
|
388
|
+
eq(messageParts.messageId, messageId),
|
|
389
|
+
or(
|
|
390
|
+
eq(messageParts.type, 'error'),
|
|
391
|
+
and(
|
|
392
|
+
eq(messageParts.type, 'tool_call'),
|
|
393
|
+
eq(messageParts.toolName, 'finish'),
|
|
394
|
+
),
|
|
395
|
+
),
|
|
396
|
+
),
|
|
397
|
+
);
|
|
387
398
|
// Delete message
|
|
388
399
|
await db.delete(messages).where(eq(messages.id, messageId));
|
|
389
400
|
|
|
@@ -758,7 +769,6 @@ export function registerSessionsRoutes(app: Hono) {
|
|
|
758
769
|
return c.json({ error: 'Session not found' }, 404);
|
|
759
770
|
}
|
|
760
771
|
|
|
761
|
-
// Delete only error parts - preserve valid text/tool content
|
|
762
772
|
await db
|
|
763
773
|
.delete(messageParts)
|
|
764
774
|
.where(
|
|
@@ -63,7 +63,11 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
|
|
|
63
63
|
debugLog('[RUNNER] Using minimal history for /compact command');
|
|
64
64
|
history = [];
|
|
65
65
|
} else {
|
|
66
|
-
history = await buildHistoryMessages(
|
|
66
|
+
history = await buildHistoryMessages(
|
|
67
|
+
db,
|
|
68
|
+
opts.sessionId,
|
|
69
|
+
opts.assistantMessageId,
|
|
70
|
+
);
|
|
67
71
|
}
|
|
68
72
|
historyTimer.end({ messages: history.length });
|
|
69
73
|
|
|
@@ -454,7 +454,13 @@ async function runAssistant(opts: RunOpts) {
|
|
|
454
454
|
publish({
|
|
455
455
|
type: 'message.created',
|
|
456
456
|
sessionId: opts.sessionId,
|
|
457
|
-
payload: {
|
|
457
|
+
payload: {
|
|
458
|
+
id: continuationMessageId,
|
|
459
|
+
role: 'assistant',
|
|
460
|
+
agent: opts.agent,
|
|
461
|
+
provider: opts.provider,
|
|
462
|
+
model: opts.model,
|
|
463
|
+
},
|
|
458
464
|
});
|
|
459
465
|
|
|
460
466
|
enqueueAssistantRun(
|
|
@@ -12,6 +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
16
|
): Promise<ModelMessage[]> {
|
|
16
17
|
const rows = await db
|
|
17
18
|
.select()
|
|
@@ -26,10 +27,12 @@ export async function buildHistoryMessages(
|
|
|
26
27
|
if (
|
|
27
28
|
m.role === 'assistant' &&
|
|
28
29
|
m.status !== 'complete' &&
|
|
29
|
-
m.status !== 'completed'
|
|
30
|
+
m.status !== 'completed' &&
|
|
31
|
+
m.status !== 'error' &&
|
|
32
|
+
m.id !== currentMessageId
|
|
30
33
|
) {
|
|
31
34
|
debugLog(
|
|
32
|
-
`[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status}
|
|
35
|
+
`[buildHistoryMessages] Skipping assistant message ${m.id} with status ${m.status}`,
|
|
33
36
|
);
|
|
34
37
|
logPendingToolParts(db, m.id);
|
|
35
38
|
continue;
|
|
@@ -124,7 +124,7 @@ export async function dispatchAssistantMessage(
|
|
|
124
124
|
publish({
|
|
125
125
|
type: 'message.created',
|
|
126
126
|
sessionId,
|
|
127
|
-
payload: { id: userMessageId, role: 'user' },
|
|
127
|
+
payload: { id: userMessageId, role: 'user', agent, provider, model },
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
const assistantMessageId = crypto.randomUUID();
|
|
@@ -141,7 +141,13 @@ export async function dispatchAssistantMessage(
|
|
|
141
141
|
publish({
|
|
142
142
|
type: 'message.created',
|
|
143
143
|
sessionId,
|
|
144
|
-
payload: {
|
|
144
|
+
payload: {
|
|
145
|
+
id: assistantMessageId,
|
|
146
|
+
role: 'assistant',
|
|
147
|
+
agent,
|
|
148
|
+
provider,
|
|
149
|
+
model,
|
|
150
|
+
},
|
|
145
151
|
});
|
|
146
152
|
|
|
147
153
|
debugLog(
|
|
@@ -216,7 +216,13 @@ export function createErrorHandler(
|
|
|
216
216
|
publish({
|
|
217
217
|
type: 'message.created',
|
|
218
218
|
sessionId: opts.sessionId,
|
|
219
|
-
payload: {
|
|
219
|
+
payload: {
|
|
220
|
+
id: compactMessageId,
|
|
221
|
+
role: 'assistant',
|
|
222
|
+
agent: opts.agent,
|
|
223
|
+
provider: opts.provider,
|
|
224
|
+
model: opts.model,
|
|
225
|
+
},
|
|
220
226
|
});
|
|
221
227
|
|
|
222
228
|
let compactionSucceeded = false;
|
|
@@ -288,7 +294,13 @@ export function createErrorHandler(
|
|
|
288
294
|
publish({
|
|
289
295
|
type: 'message.created',
|
|
290
296
|
sessionId: opts.sessionId,
|
|
291
|
-
payload: {
|
|
297
|
+
payload: {
|
|
298
|
+
id: retryMessageId,
|
|
299
|
+
role: 'assistant',
|
|
300
|
+
agent: opts.agent,
|
|
301
|
+
provider: opts.provider,
|
|
302
|
+
model: opts.model,
|
|
303
|
+
},
|
|
292
304
|
});
|
|
293
305
|
|
|
294
306
|
enqueueAssistantRun(
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { getDb } from '@ottocode/database';
|
|
2
|
+
import { messageParts } from '@ottocode/database/schema';
|
|
3
|
+
import { eq, desc } from 'drizzle-orm';
|
|
2
4
|
import { time } from '../debug/index.ts';
|
|
3
5
|
import type { ToolAdapterContext } from '../../tools/adapter.ts';
|
|
4
6
|
import type { RunOpts } from '../session/queue.ts';
|
|
@@ -16,8 +18,13 @@ export async function setupToolContext(
|
|
|
16
18
|
const firstToolTimer = time('runner:first-tool-call');
|
|
17
19
|
let firstToolSeen = false;
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const existingParts = await db
|
|
22
|
+
.select({ index: messageParts.index })
|
|
23
|
+
.from(messageParts)
|
|
24
|
+
.where(eq(messageParts.messageId, opts.assistantMessageId))
|
|
25
|
+
.orderBy(desc(messageParts.index))
|
|
26
|
+
.limit(1);
|
|
27
|
+
let currentIndex = existingParts.length > 0 ? existingParts[0].index + 1 : 0;
|
|
21
28
|
const nextIndex = () => currentIndex++;
|
|
22
29
|
|
|
23
30
|
const sharedCtx: RunnerToolContext = {
|