@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,369 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { loadConfig } from '@ottocode/sdk';
3
+ import { getDb } from '@ottocode/database';
4
+ import {
5
+ createSession,
6
+ getLastSession,
7
+ getSessionById,
8
+ } from '../session/manager.ts';
9
+ import {
10
+ selectProviderAndModel,
11
+ type ProviderSelection,
12
+ } from '../provider/selection.ts';
13
+ import { resolveAgentConfig } from '../agent/registry.ts';
14
+ import { dispatchAssistantMessage } from '../message/service.ts';
15
+ import {
16
+ validateProviderModel,
17
+ isProviderAuthorized,
18
+ ensureProviderEnv,
19
+ isProviderId,
20
+ providerEnvVar,
21
+ type ProviderId,
22
+ } from '@ottocode/sdk';
23
+ import { sessions } from '@ottocode/database/schema';
24
+ import { time } from '../debug/index.ts';
25
+
26
+ export class AskServiceError extends Error {
27
+ constructor(
28
+ message: string,
29
+ public status = 400,
30
+ public code = 'ASK_SERVICE_ERROR',
31
+ ) {
32
+ super(message);
33
+ this.name = 'AskServiceError';
34
+ }
35
+
36
+ toJSON() {
37
+ return {
38
+ name: this.name,
39
+ message: this.message,
40
+ code: this.code,
41
+ status: this.status,
42
+ };
43
+ }
44
+ }
45
+
46
+ export type InjectableConfig = {
47
+ provider?: string;
48
+ model?: string;
49
+ apiKey?: string;
50
+ agent?: string;
51
+ };
52
+
53
+ export type InjectableCredentials = Partial<
54
+ Record<ProviderId, { apiKey: string }>
55
+ >;
56
+
57
+ export type AskServerRequest = {
58
+ projectRoot?: string;
59
+ prompt: string;
60
+ agent?: string;
61
+ provider?: string;
62
+ model?: string;
63
+ sessionId?: string;
64
+ last?: boolean;
65
+ jsonMode?: boolean;
66
+ skipFileConfig?: boolean;
67
+ config?: InjectableConfig;
68
+ credentials?: InjectableCredentials;
69
+ agentPrompt?: string;
70
+ tools?: string[];
71
+ };
72
+
73
+ export type AskServerResponse = {
74
+ sessionId: string;
75
+ header: {
76
+ agent?: string;
77
+ provider?: string;
78
+ model?: string;
79
+ sessionId: string;
80
+ };
81
+ provider: ProviderId;
82
+ model: string;
83
+ agent: string;
84
+ assistantMessageId: string;
85
+ message?: { kind: 'created' | 'last'; sessionId: string };
86
+ };
87
+
88
+ type SessionRow =
89
+ typeof import('@ottocode/database/schema').sessions.$inferSelect;
90
+
91
+ export async function handleAskRequest(
92
+ request: AskServerRequest,
93
+ ): Promise<AskServerResponse> {
94
+ try {
95
+ return await processAskRequest(request);
96
+ } catch (err) {
97
+ throw normalizeAskServiceError(err);
98
+ }
99
+ }
100
+
101
+ async function processAskRequest(
102
+ request: AskServerRequest,
103
+ ): Promise<AskServerResponse> {
104
+ const projectRoot = request.projectRoot || process.cwd();
105
+ const configTimer = time('ask:loadConfig+db');
106
+
107
+ let cfg: import('@ottocode/sdk').OttoConfig;
108
+
109
+ if (request.skipFileConfig || request.config) {
110
+ const injectedProvider = (request.config?.provider ||
111
+ request.provider ||
112
+ 'openai') as ProviderId;
113
+ const injectedModel =
114
+ request.config?.model || request.model || 'gpt-4o-mini';
115
+ const injectedAgent = request.config?.agent || request.agent || 'general';
116
+
117
+ cfg = {
118
+ projectRoot,
119
+ defaults: {
120
+ provider: injectedProvider,
121
+ model: injectedModel,
122
+ agent: injectedAgent,
123
+ },
124
+ providers: {
125
+ openai: { enabled: true },
126
+ anthropic: { enabled: true },
127
+ google: { enabled: true },
128
+ openrouter: { enabled: true },
129
+ opencode: { enabled: true },
130
+ setu: { enabled: true },
131
+ },
132
+ paths: {
133
+ dataDir: `${projectRoot}/.otto`,
134
+ dbPath: `${projectRoot}/.otto/otto.sqlite`,
135
+ projectConfigPath: null,
136
+ globalConfigPath: null,
137
+ },
138
+ };
139
+
140
+ if (request.credentials) {
141
+ for (const [provider, creds] of Object.entries(request.credentials)) {
142
+ const envKey =
143
+ providerEnvVar(provider as ProviderId) ??
144
+ `${provider.toUpperCase()}_API_KEY`;
145
+ process.env[envKey] = creds.apiKey;
146
+ }
147
+ } else if (request.config?.apiKey) {
148
+ const envKey =
149
+ providerEnvVar(injectedProvider) ??
150
+ `${injectedProvider.toUpperCase()}_API_KEY`;
151
+ process.env[envKey] = request.config.apiKey;
152
+ }
153
+ } else {
154
+ cfg = await loadConfig(projectRoot);
155
+ }
156
+
157
+ const db = await getDb(cfg.projectRoot);
158
+ configTimer.end();
159
+
160
+ let session: SessionRow | undefined;
161
+ let messageIndicator: AskServerResponse['message'];
162
+
163
+ if (request.sessionId) {
164
+ session = await getSessionById({
165
+ db,
166
+ projectPath: cfg.projectRoot,
167
+ sessionId: request.sessionId,
168
+ });
169
+ if (!session) {
170
+ throw new AskServiceError(`Session not found: ${request.sessionId}`, 404);
171
+ }
172
+ } else if (request.last) {
173
+ session = await getLastSession({ db, projectPath: cfg.projectRoot });
174
+ if (session) {
175
+ messageIndicator = { kind: 'last', sessionId: session.id };
176
+ }
177
+ } else {
178
+ session = undefined;
179
+ }
180
+
181
+ const agentName = (() => {
182
+ if (request.agent) return request.agent;
183
+ if (session?.agent) return session.agent;
184
+ return cfg.defaults.agent;
185
+ })();
186
+
187
+ const agentTimer = time('ask:resolveAgentConfig');
188
+ const agentCfg = request.agentPrompt
189
+ ? {
190
+ name: agentName,
191
+ prompt: request.agentPrompt,
192
+ tools: request.tools ?? ['progress_update', 'finish'],
193
+ provider: isProviderId(request.provider)
194
+ ? (request.provider as ProviderId)
195
+ : undefined,
196
+ model: request.model,
197
+ }
198
+ : await resolveAgentConfig(cfg.projectRoot, agentName);
199
+ agentTimer.end({ agent: agentName });
200
+ const agentProviderDefault = isProviderId(agentCfg.provider)
201
+ ? agentCfg.provider
202
+ : cfg.defaults.provider;
203
+ const agentModelDefault = agentCfg.model ?? cfg.defaults.model;
204
+
205
+ const explicitProvider = isProviderId(request.provider)
206
+ ? (request.provider as ProviderId)
207
+ : undefined;
208
+
209
+ let providerSelection: ProviderSelection;
210
+ const selectTimer = time('ask:selectProviderModel');
211
+ try {
212
+ providerSelection = await selectProviderAndModel({
213
+ cfg,
214
+ agentProviderDefault,
215
+ agentModelDefault,
216
+ explicitProvider,
217
+ explicitModel: request.model,
218
+ skipAuth: Boolean(
219
+ request.skipFileConfig || request.config || request.credentials,
220
+ ),
221
+ });
222
+ selectTimer.end({ provider: providerSelection.provider });
223
+ } catch (err) {
224
+ selectTimer.end();
225
+ throw normalizeAskServiceError(err);
226
+ }
227
+
228
+ if (!session) {
229
+ const createTimer = time('ask:createSession');
230
+ const newSession = await createSession({
231
+ db,
232
+ cfg,
233
+ agent: agentName,
234
+ provider: providerSelection.provider,
235
+ model: providerSelection.model,
236
+ title: null,
237
+ });
238
+ createTimer.end();
239
+ session = newSession;
240
+ messageIndicator = { kind: 'created', sessionId: newSession.id };
241
+ }
242
+ if (!session)
243
+ throw new AskServiceError('Failed to resolve or create session.', 500);
244
+
245
+ const overridesProvided = Boolean(request.provider || request.model);
246
+
247
+ let providerForMessage: ProviderId;
248
+ let modelForMessage: string;
249
+
250
+ if (overridesProvided) {
251
+ providerForMessage = providerSelection.provider;
252
+ modelForMessage = providerSelection.model;
253
+ } else if (session.provider && session.model) {
254
+ const sessionProvider = isProviderId(session.provider)
255
+ ? (session.provider as ProviderId)
256
+ : agentProviderDefault;
257
+ providerForMessage = sessionProvider;
258
+ modelForMessage = session.model;
259
+ } else {
260
+ providerForMessage = providerSelection.provider;
261
+ modelForMessage = providerSelection.model;
262
+ }
263
+
264
+ const providerAuthorized = await isProviderAuthorized(
265
+ cfg,
266
+ providerForMessage,
267
+ );
268
+ let fellBackToSelection = false;
269
+ if (!providerAuthorized) {
270
+ providerForMessage = providerSelection.provider;
271
+ modelForMessage = providerSelection.model;
272
+ fellBackToSelection = true;
273
+ }
274
+ if (
275
+ session &&
276
+ fellBackToSelection &&
277
+ (session.provider !== providerForMessage ||
278
+ session.model !== modelForMessage)
279
+ ) {
280
+ await db
281
+ .update(sessions)
282
+ .set({ provider: providerForMessage, model: modelForMessage })
283
+ .where(eq(sessions.id, session.id));
284
+ session = {
285
+ ...session,
286
+ provider: providerForMessage,
287
+ model: modelForMessage,
288
+ } as SessionRow;
289
+ }
290
+
291
+ validateProviderModel(providerForMessage, modelForMessage);
292
+
293
+ if (!request.skipFileConfig && !request.config && !request.credentials) {
294
+ await ensureProviderEnv(cfg, providerForMessage);
295
+ }
296
+
297
+ const assistantMessage = await dispatchAssistantMessage({
298
+ cfg,
299
+ db,
300
+ session,
301
+ agent: agentName,
302
+ provider: providerForMessage,
303
+ model: modelForMessage,
304
+ content: request.prompt,
305
+ oneShot: !request.sessionId && !request.last,
306
+ });
307
+
308
+ const headerAgent = session.agent ?? agentName;
309
+ const headerProvider = providerForMessage;
310
+ const headerModel = modelForMessage;
311
+
312
+ return {
313
+ sessionId: session.id,
314
+ header: {
315
+ agent: headerAgent,
316
+ provider: headerProvider,
317
+ model: headerModel,
318
+ sessionId: session.id,
319
+ },
320
+ provider: providerForMessage,
321
+ model: modelForMessage,
322
+ agent: agentName,
323
+ assistantMessageId: assistantMessage.assistantMessageId,
324
+ message: messageIndicator,
325
+ };
326
+ }
327
+
328
+ function normalizeAskServiceError(err: unknown): AskServiceError {
329
+ if (err instanceof AskServiceError) return err;
330
+ if (err instanceof Error) {
331
+ const status = inferStatus(err);
332
+ const message = err.message || 'Unknown error';
333
+ return new AskServiceError(message, status);
334
+ }
335
+ return new AskServiceError(String(err ?? 'Unknown error'));
336
+ }
337
+
338
+ export function inferStatus(err: Error): number {
339
+ const anyErr = err as { status?: unknown; code?: unknown };
340
+ if (typeof anyErr.status === 'number') {
341
+ const s = anyErr.status;
342
+ if (Number.isFinite(s) && s >= 400 && s < 600) return s;
343
+ }
344
+ if (typeof anyErr.code === 'number') {
345
+ const s = anyErr.code;
346
+ if (Number.isFinite(s) && s >= 400 && s < 600) return s;
347
+ }
348
+ const derived = deriveStatusFromMessage(err.message || '');
349
+ return derived ?? 400;
350
+ }
351
+
352
+ const STATUS_PATTERNS: Array<{ regex: RegExp; status: number }> = [
353
+ { regex: /not found/i, status: 404 },
354
+ { regex: /missing credentials/i, status: 401 },
355
+ { regex: /unauthorized/i, status: 401 },
356
+ { regex: /not configured/i, status: 401 },
357
+ { regex: /authorized providers/i, status: 401 },
358
+ { regex: /forbidden/i, status: 403 },
359
+ { regex: /timeout/i, status: 504 },
360
+ ];
361
+
362
+ export function deriveStatusFromMessage(message: string): number | undefined {
363
+ const trimmed = message.trim();
364
+ if (!trimmed) return undefined;
365
+ for (const { regex, status } of STATUS_PATTERNS) {
366
+ if (regex.test(trimmed)) return status;
367
+ }
368
+ return undefined;
369
+ }
@@ -0,0 +1,202 @@
1
+ import { getHomeDir } from '@ottocode/sdk';
2
+
3
+ async function detectProjectTooling(
4
+ projectRoot: string,
5
+ ): Promise<{ packageManager?: string; runtime?: string; language?: string }> {
6
+ const { existsSync } = await import('node:fs');
7
+ const { join } = await import('node:path');
8
+ const result: {
9
+ packageManager?: string;
10
+ runtime?: string;
11
+ language?: string;
12
+ } = {};
13
+
14
+ try {
15
+ const pkgPath = join(projectRoot, 'package.json');
16
+ if (existsSync(pkgPath)) {
17
+ const pkg = await Bun.file(pkgPath).json();
18
+ if (pkg.packageManager) {
19
+ const match = String(pkg.packageManager).match(/^(bun|npm|yarn|pnpm)/);
20
+ if (match) result.packageManager = match[1];
21
+ }
22
+ }
23
+ } catch {}
24
+
25
+ if (!result.packageManager) {
26
+ if (
27
+ existsSync(join(projectRoot, 'bun.lockb')) ||
28
+ existsSync(join(projectRoot, 'bun.lock'))
29
+ ) {
30
+ result.packageManager = 'bun';
31
+ } else if (existsSync(join(projectRoot, 'yarn.lock'))) {
32
+ result.packageManager = 'yarn';
33
+ } else if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) {
34
+ result.packageManager = 'pnpm';
35
+ } else if (existsSync(join(projectRoot, 'package-lock.json'))) {
36
+ result.packageManager = 'npm';
37
+ }
38
+ }
39
+
40
+ if (existsSync(join(projectRoot, 'package.json'))) {
41
+ result.runtime = result.packageManager === 'bun' ? 'bun' : 'node';
42
+ } else if (existsSync(join(projectRoot, 'Cargo.toml'))) {
43
+ result.language = 'rust';
44
+ } else if (existsSync(join(projectRoot, 'go.mod'))) {
45
+ result.language = 'go';
46
+ } else if (
47
+ existsSync(join(projectRoot, 'pyproject.toml')) ||
48
+ existsSync(join(projectRoot, 'requirements.txt'))
49
+ ) {
50
+ result.language = 'python';
51
+ } else if (existsSync(join(projectRoot, 'Gemfile'))) {
52
+ result.language = 'ruby';
53
+ }
54
+
55
+ return result;
56
+ }
57
+
58
+ export async function getEnvironmentContext(
59
+ projectRoot: string,
60
+ ): Promise<string> {
61
+ const parts: string[] = [];
62
+
63
+ parts.push(
64
+ 'Here is some useful information about the environment you are running in:',
65
+ );
66
+ parts.push('<env>');
67
+ parts.push(` Working directory: ${projectRoot}`);
68
+
69
+ try {
70
+ const { existsSync } = await import('node:fs');
71
+ const isGitRepo = existsSync(`${projectRoot}/.git`);
72
+ parts.push(` Is directory a git repo: ${isGitRepo ? 'yes' : 'no'}`);
73
+ } catch {
74
+ parts.push(' Is directory a git repo: unknown');
75
+ }
76
+
77
+ parts.push(` Platform: ${process.platform}`);
78
+ parts.push(` Today's date: ${new Date().toDateString()}`);
79
+
80
+ try {
81
+ const tooling = await detectProjectTooling(projectRoot);
82
+ if (tooling.packageManager) {
83
+ parts.push(
84
+ ` Package manager: ${tooling.packageManager} (ALWAYS use this for install/run/build commands)`,
85
+ );
86
+ }
87
+ if (tooling.runtime) {
88
+ parts.push(` Runtime: ${tooling.runtime}`);
89
+ }
90
+ if (tooling.language) {
91
+ parts.push(` Primary language: ${tooling.language}`);
92
+ }
93
+ } catch {}
94
+
95
+ parts.push('</env>');
96
+
97
+ return parts.join('\n');
98
+ }
99
+
100
+ export async function getProjectTree(projectRoot: string): Promise<string> {
101
+ try {
102
+ const { promisify } = await import('node:util');
103
+ const { exec } = await import('node:child_process');
104
+ const execAsync = promisify(exec);
105
+
106
+ const { stdout } = await execAsync(
107
+ 'git ls-files 2>/dev/null || find . -type f -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/dist/*" 2>/dev/null | head -100',
108
+ { cwd: projectRoot, maxBuffer: 1024 * 1024 },
109
+ );
110
+
111
+ if (!stdout.trim()) return '';
112
+
113
+ const files = stdout.trim().split('\n').slice(0, 100);
114
+ return `<project>\n${files.join('\n')}\n</project>`;
115
+ } catch {
116
+ return '';
117
+ }
118
+ }
119
+
120
+ export async function findInstructionFiles(
121
+ projectRoot: string,
122
+ ): Promise<string[]> {
123
+ const { existsSync } = await import('node:fs');
124
+ const { join } = await import('node:path');
125
+ const foundPaths: string[] = [];
126
+
127
+ const localFiles = ['AGENTS.md', 'CLAUDE.md', 'CONTEXT.md'];
128
+ for (const filename of localFiles) {
129
+ let currentDir = projectRoot;
130
+ for (let i = 0; i < 5; i++) {
131
+ const filePath = join(currentDir, filename);
132
+ if (existsSync(filePath)) {
133
+ foundPaths.push(filePath);
134
+ break;
135
+ }
136
+ const parentDir = join(currentDir, '..');
137
+ if (parentDir === currentDir) break;
138
+ currentDir = parentDir;
139
+ }
140
+ }
141
+
142
+ const homeDir = getHomeDir();
143
+ const globalFiles = [
144
+ join(homeDir, '.config', 'otto', 'AGENTS.md'),
145
+ join(homeDir, '.claude', 'CLAUDE.md'),
146
+ ];
147
+
148
+ for (const filePath of globalFiles) {
149
+ if (existsSync(filePath) && !foundPaths.includes(filePath)) {
150
+ foundPaths.push(filePath);
151
+ }
152
+ }
153
+
154
+ return foundPaths;
155
+ }
156
+
157
+ export async function loadInstructionFiles(
158
+ projectRoot: string,
159
+ ): Promise<string> {
160
+ const paths = await findInstructionFiles(projectRoot);
161
+ if (paths.length === 0) return '';
162
+
163
+ const contents: string[] = [];
164
+
165
+ for (const path of paths) {
166
+ try {
167
+ const file = Bun.file(path);
168
+ const content = await file.text();
169
+ if (content.trim()) {
170
+ contents.push(
171
+ `\n--- Custom Instructions from ${path} ---\n${content.trim()}`,
172
+ );
173
+ }
174
+ } catch {}
175
+ }
176
+
177
+ return contents.join('\n');
178
+ }
179
+
180
+ export async function composeEnvironmentAndInstructions(
181
+ projectRoot: string,
182
+ options?: { includeProjectTree?: boolean },
183
+ ): Promise<string> {
184
+ const parts: string[] = [];
185
+
186
+ const envContext = await getEnvironmentContext(projectRoot);
187
+ parts.push(envContext);
188
+
189
+ if (options?.includeProjectTree !== false) {
190
+ const projectTree = await getProjectTree(projectRoot);
191
+ if (projectTree) {
192
+ parts.push(projectTree);
193
+ }
194
+ }
195
+
196
+ const customInstructions = await loadInstructionFiles(projectRoot);
197
+ if (customInstructions) {
198
+ parts.push(customInstructions);
199
+ }
200
+
201
+ return parts.filter(Boolean).join('\n\n');
202
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Legacy debug utilities - now integrated with new logger
3
+ *
4
+ * This file maintains backward compatibility while using the new
5
+ * centralized debug-state and logger modules.
6
+ */
7
+
8
+ import { isDebugEnabled as isDebugEnabledNew } from './state.ts';
9
+ import { time as timeNew, debug as debugNew } from '@ottocode/sdk';
10
+
11
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
12
+
13
+ const SYNONYMS: Record<string, string> = {
14
+ debug: 'log',
15
+ logs: 'log',
16
+ logging: 'log',
17
+ trace: 'log',
18
+ verbose: 'log',
19
+ log: 'log',
20
+ time: 'timing',
21
+ timing: 'timing',
22
+ timings: 'timing',
23
+ perf: 'timing',
24
+ };
25
+
26
+ type DebugConfig = { flags: Set<string> };
27
+
28
+ let cachedConfig: DebugConfig | null = null;
29
+
30
+ function isTruthy(raw: string | undefined): boolean {
31
+ if (!raw) return false;
32
+ const trimmed = raw.trim().toLowerCase();
33
+ if (!trimmed) return false;
34
+ return TRUTHY.has(trimmed) || trimmed === 'all';
35
+ }
36
+
37
+ function normalizeToken(token: string): string {
38
+ const trimmed = token.trim().toLowerCase();
39
+ if (!trimmed) return '';
40
+ if (TRUTHY.has(trimmed) || trimmed === 'all') return 'all';
41
+ return SYNONYMS[trimmed] ?? trimmed;
42
+ }
43
+
44
+ function parseDebugConfig(): DebugConfig {
45
+ const flags = new Set<string>();
46
+ const sources = [process.env.OTTO_DEBUG, process.env.DEBUG_OTTO];
47
+ let sawValue = false;
48
+ for (const raw of sources) {
49
+ if (typeof raw !== 'string') continue;
50
+ const trimmed = raw.trim();
51
+ if (!trimmed) continue;
52
+ sawValue = true;
53
+ const tokens = trimmed.split(/[\s,]+/);
54
+ let matched = false;
55
+ for (const token of tokens) {
56
+ const normalized = normalizeToken(token);
57
+ if (!normalized) continue;
58
+ matched = true;
59
+ flags.add(normalized);
60
+ }
61
+ if (!matched && isTruthy(trimmed)) flags.add('all');
62
+ }
63
+ if (isTruthy(process.env.OTTO_DEBUG_TIMING)) flags.add('timing');
64
+ if (!flags.size && sawValue) flags.add('all');
65
+ return { flags };
66
+ }
67
+
68
+ function getDebugConfig(): DebugConfig {
69
+ if (!cachedConfig) cachedConfig = parseDebugConfig();
70
+ return cachedConfig;
71
+ }
72
+
73
+ /**
74
+ * Check if debug mode is enabled for a specific flag
75
+ * Now uses the centralized debug state
76
+ *
77
+ * @deprecated Use isDebugEnabled from debug-state.ts instead
78
+ */
79
+ export function isDebugEnabled(flag?: string): boolean {
80
+ // Use new centralized debug state for general debug
81
+ if (!flag || flag === 'log') {
82
+ return isDebugEnabledNew();
83
+ }
84
+
85
+ // For specific flags like 'timing', check both new state and legacy env vars
86
+ if (flag === 'timing') {
87
+ // If new debug state is enabled OR timing flag is set
88
+ if (isDebugEnabledNew()) return true;
89
+ }
90
+
91
+ // Legacy flag checking
92
+ const config = getDebugConfig();
93
+ if (config.flags.has('all')) return true;
94
+ if (flag) return config.flags.has(flag);
95
+ return config.flags.has('log');
96
+ }
97
+
98
+ /**
99
+ * Log debug message
100
+ * Now uses the centralized logger
101
+ *
102
+ * @deprecated Use logger.debug from logger.ts instead
103
+ */
104
+ export function debugLog(...args: unknown[]) {
105
+ if (!isDebugEnabled('log')) return;
106
+ debugNew(args.map((arg) => String(arg)).join(' '));
107
+ }
108
+
109
+ /**
110
+ * Create a timer for performance measurement
111
+ * Integrated with centralized logger
112
+ */
113
+ export function time(label: string): {
114
+ end(meta?: Record<string, unknown>): void;
115
+ } {
116
+ return timeNew(label);
117
+ }