@ottocode/server 0.1.191 → 0.1.193

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.191",
3
+ "version": "0.1.193",
4
4
  "description": "HTTP API server for ottocode",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@ottocode/sdk": "0.1.191",
33
- "@ottocode/database": "0.1.191",
32
+ "@ottocode/sdk": "0.1.193",
33
+ "@ottocode/database": "0.1.193",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -161,6 +161,27 @@ export function registerAuthRoutes(app: Hono) {
161
161
  }
162
162
  });
163
163
 
164
+ app.get('/v1/auth/setu/export', async (c) => {
165
+ try {
166
+ const projectRoot = process.cwd();
167
+ const wallet = await getSetuWallet(projectRoot);
168
+
169
+ if (!wallet) {
170
+ return c.json({ error: 'Setu wallet not configured' }, 404);
171
+ }
172
+
173
+ return c.json({
174
+ success: true,
175
+ publicKey: wallet.publicKey,
176
+ privateKey: wallet.privateKey,
177
+ });
178
+ } catch (error) {
179
+ logger.error('Failed to export Setu wallet', error);
180
+ const errorResponse = serializeError(error);
181
+ return c.json(errorResponse, errorResponse.error.status || 500);
182
+ }
183
+ });
184
+
164
185
  app.post('/v1/auth/:provider', async (c) => {
165
186
  try {
166
187
  const provider = c.req.param('provider') as ProviderId;
@@ -12,6 +12,7 @@ export function registerDefaultsRoute(app: Hono) {
12
12
  provider?: string;
13
13
  model?: string;
14
14
  toolApproval?: 'auto' | 'dangerous' | 'all';
15
+ guidedMode?: boolean;
15
16
  scope?: 'global' | 'local';
16
17
  }>();
17
18
 
@@ -21,12 +22,14 @@ export function registerDefaultsRoute(app: Hono) {
21
22
  provider: string;
22
23
  model: string;
23
24
  toolApproval: 'auto' | 'dangerous' | 'all';
25
+ guidedMode: boolean;
24
26
  }> = {};
25
27
 
26
28
  if (body.agent) updates.agent = body.agent;
27
29
  if (body.provider) updates.provider = body.provider;
28
30
  if (body.model) updates.model = body.model;
29
31
  if (body.toolApproval) updates.toolApproval = body.toolApproval;
32
+ if (body.guidedMode !== undefined) updates.guidedMode = body.guidedMode;
30
33
 
31
34
  await setConfig(scope, updates, projectRoot);
32
35
 
@@ -57,6 +57,7 @@ export function registerMainConfigRoute(app: Hono) {
57
57
  embeddedConfig?.defaults?.toolApproval,
58
58
  cfg.defaults.toolApproval,
59
59
  ) as 'auto' | 'dangerous' | 'all',
60
+ guidedMode: cfg.defaults.guidedMode ?? false,
60
61
  };
61
62
 
62
63
  return c.json({
@@ -105,6 +105,7 @@ export async function setupRunner(opts: RunOpts): Promise<SetupResult> {
105
105
  projectRoot: cfg.projectRoot,
106
106
  agentPrompt,
107
107
  oneShot: opts.oneShot,
108
+ guidedMode: cfg.defaults.guidedMode,
108
109
  spoofPrompt: undefined,
109
110
  includeProjectTree: isFirstMessage,
110
111
  userContext: opts.userContext,
@@ -1,11 +1,12 @@
1
1
  import { hasToolCall, streamText } from 'ai';
2
- import { messageParts } from '@ottocode/database/schema';
2
+ import { messages, messageParts } from '@ottocode/database/schema';
3
3
  import { eq } from 'drizzle-orm';
4
4
  import { publish, subscribe } from '../../events/bus.ts';
5
5
  import { debugLog, time } from '../debug/index.ts';
6
6
  import { toErrorPayload } from '../errors/handling.ts';
7
7
  import {
8
8
  type RunOpts,
9
+ enqueueAssistantRun,
9
10
  setRunning,
10
11
  dequeueJob,
11
12
  cleanupSession,
@@ -293,13 +294,71 @@ async function runAssistant(opts: RunOpts) {
293
294
  await cleanupEmptyTextParts(opts, db);
294
295
  firstToolTimer.end({ seen: firstToolSeen() });
295
296
 
297
+ let streamFinishReason: string | undefined;
298
+ try {
299
+ streamFinishReason = await result.finishReason;
300
+ } catch {
301
+ streamFinishReason = undefined;
302
+ }
303
+
296
304
  debugLog(
297
- `[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}`,
305
+ `[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, finishReason=${streamFinishReason}`,
298
306
  );
299
307
 
300
- if (!_finishObserved && fs) {
308
+ const wasTruncated = streamFinishReason === 'length';
309
+
310
+ const shouldContinue = !_finishObserved && (wasTruncated || fs);
311
+
312
+ if (shouldContinue) {
313
+ debugLog(
314
+ `[RUNNER] WARNING: Stream ended without finish. finishReason=${streamFinishReason}, firstToolSeen=${fs}. Auto-continuing.`,
315
+ );
316
+
317
+ const MAX_CONTINUATIONS = 10;
318
+ const count = opts.continuationCount ?? 0;
319
+ if (count < MAX_CONTINUATIONS) {
320
+ debugLog(
321
+ `[RUNNER] Auto-continuing (${count + 1}/${MAX_CONTINUATIONS})...`,
322
+ );
323
+
324
+ try {
325
+ await completeAssistantMessage({}, opts, db);
326
+ } catch (err) {
327
+ debugLog(
328
+ `[RUNNER] completeAssistantMessage failed before continuation: ${err instanceof Error ? err.message : String(err)}`,
329
+ );
330
+ }
331
+
332
+ const continuationMessageId = crypto.randomUUID();
333
+ await db.insert(messages).values({
334
+ id: continuationMessageId,
335
+ sessionId: opts.sessionId,
336
+ role: 'assistant',
337
+ status: 'pending',
338
+ agent: opts.agent,
339
+ provider: opts.provider,
340
+ model: opts.model,
341
+ createdAt: Date.now(),
342
+ });
343
+
344
+ publish({
345
+ type: 'message.created',
346
+ sessionId: opts.sessionId,
347
+ payload: { id: continuationMessageId, role: 'assistant' },
348
+ });
349
+
350
+ enqueueAssistantRun(
351
+ {
352
+ ...opts,
353
+ assistantMessageId: continuationMessageId,
354
+ continuationCount: count + 1,
355
+ },
356
+ runSessionLoop,
357
+ );
358
+ return;
359
+ }
301
360
  debugLog(
302
- `[RUNNER] WARNING: Stream ended without finish tool being called. Model was mid-execution (tools were used). This is likely an unclean stream termination from the provider.`,
361
+ `[RUNNER] Max continuations (${MAX_CONTINUATIONS}) reached, stopping.`,
303
362
  );
304
363
  }
305
364
  } catch (err) {
@@ -121,12 +121,16 @@ export async function getProjectTree(projectRoot: string): Promise<string> {
121
121
 
122
122
  export async function findInstructionFiles(
123
123
  projectRoot: string,
124
+ options?: { guidedMode?: boolean },
124
125
  ): Promise<string[]> {
125
126
  const { existsSync } = await import('node:fs');
126
127
  const { join } = await import('node:path');
127
128
  const foundPaths: string[] = [];
128
129
 
129
130
  const localFiles = ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md'];
131
+ if (options?.guidedMode) {
132
+ localFiles.push('GUIDED.md');
133
+ }
130
134
  for (const filename of localFiles) {
131
135
  let currentDir = projectRoot;
132
136
  for (let i = 0; i < 5; i++) {
@@ -158,8 +162,9 @@ export async function findInstructionFiles(
158
162
 
159
163
  export async function loadInstructionFiles(
160
164
  projectRoot: string,
165
+ options?: { guidedMode?: boolean },
161
166
  ): Promise<string> {
162
- const paths = await findInstructionFiles(projectRoot);
167
+ const paths = await findInstructionFiles(projectRoot, options);
163
168
  if (paths.length === 0) return '';
164
169
 
165
170
  const contents: string[] = [];
@@ -181,7 +186,7 @@ export async function loadInstructionFiles(
181
186
 
182
187
  export async function composeEnvironmentAndInstructions(
183
188
  projectRoot: string,
184
- options?: { includeProjectTree?: boolean },
189
+ options?: { includeProjectTree?: boolean; guidedMode?: boolean },
185
190
  ): Promise<string> {
186
191
  const parts: string[] = [];
187
192
 
@@ -195,7 +200,9 @@ export async function composeEnvironmentAndInstructions(
195
200
  }
196
201
  }
197
202
 
198
- const customInstructions = await loadInstructionFiles(projectRoot);
203
+ const customInstructions = await loadInstructionFiles(projectRoot, {
204
+ guidedMode: options?.guidedMode,
205
+ });
199
206
  if (customInstructions) {
200
207
  parts.push(customInstructions);
201
208
  }
@@ -8,6 +8,10 @@ import ONESHOT_PROMPT from '@ottocode/sdk/prompts/modes/oneshot.txt' with {
8
8
  type: 'text',
9
9
  };
10
10
  // eslint-disable-next-line @typescript-eslint/consistent-type-imports
11
+ import GUIDED_PROMPT from '@ottocode/sdk/prompts/modes/guided.txt' with {
12
+ type: 'text',
13
+ };
14
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
11
15
  import ANTHROPIC_SPOOF_PROMPT from '@ottocode/sdk/prompts/providers/anthropicSpoof.txt' with {
12
16
  type: 'text',
13
17
  };
@@ -25,6 +29,7 @@ export async function composeSystemPrompt(options: {
25
29
  projectRoot: string;
26
30
  agentPrompt: string;
27
31
  oneShot?: boolean;
32
+ guidedMode?: boolean;
28
33
  spoofPrompt?: string;
29
34
  includeEnvironment?: boolean;
30
35
  includeProjectTree?: boolean;
@@ -79,10 +84,21 @@ export async function composeSystemPrompt(options: {
79
84
  components.push('mode:oneshot');
80
85
  }
81
86
 
87
+ if (options.guidedMode) {
88
+ const guidedBlock = (GUIDED_PROMPT || '').trim();
89
+ if (guidedBlock) {
90
+ parts.push(guidedBlock);
91
+ components.push('mode:guided');
92
+ }
93
+ }
94
+
82
95
  if (options.includeEnvironment !== false) {
83
96
  const envAndInstructions = await composeEnvironmentAndInstructions(
84
97
  options.projectRoot,
85
- { includeProjectTree: options.includeProjectTree },
98
+ {
99
+ includeProjectTree: options.includeProjectTree,
100
+ guidedMode: options.guidedMode,
101
+ },
86
102
  );
87
103
  if (envAndInstructions) {
88
104
  parts.push(envAndInstructions);
@@ -17,6 +17,7 @@ export type RunOpts = {
17
17
  compactionContext?: string;
18
18
  toolApprovalMode?: ToolApprovalMode;
19
19
  compactionRetries?: number;
20
+ continuationCount?: number;
20
21
  };
21
22
 
22
23
  export type QueuedMessage = {