@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.
- package/package.json +42 -0
- package/src/events/bus.ts +43 -0
- package/src/events/types.ts +32 -0
- package/src/index.ts +281 -0
- package/src/openapi/helpers.ts +64 -0
- package/src/openapi/paths/ask.ts +70 -0
- package/src/openapi/paths/config.ts +218 -0
- package/src/openapi/paths/files.ts +72 -0
- package/src/openapi/paths/git.ts +457 -0
- package/src/openapi/paths/messages.ts +92 -0
- package/src/openapi/paths/sessions.ts +90 -0
- package/src/openapi/paths/setu.ts +154 -0
- package/src/openapi/paths/stream.ts +26 -0
- package/src/openapi/paths/terminals.ts +226 -0
- package/src/openapi/schemas.ts +345 -0
- package/src/openapi/spec.ts +49 -0
- package/src/presets.ts +85 -0
- package/src/routes/ask.ts +113 -0
- package/src/routes/auth.ts +592 -0
- package/src/routes/branch.ts +106 -0
- package/src/routes/config/agents.ts +44 -0
- package/src/routes/config/cwd.ts +21 -0
- package/src/routes/config/defaults.ts +45 -0
- package/src/routes/config/index.ts +16 -0
- package/src/routes/config/main.ts +73 -0
- package/src/routes/config/models.ts +139 -0
- package/src/routes/config/providers.ts +46 -0
- package/src/routes/config/utils.ts +120 -0
- package/src/routes/files.ts +218 -0
- package/src/routes/git/branch.ts +75 -0
- package/src/routes/git/commit.ts +209 -0
- package/src/routes/git/diff.ts +137 -0
- package/src/routes/git/index.ts +18 -0
- package/src/routes/git/push.ts +160 -0
- package/src/routes/git/schemas.ts +48 -0
- package/src/routes/git/staging.ts +208 -0
- package/src/routes/git/status.ts +83 -0
- package/src/routes/git/types.ts +31 -0
- package/src/routes/git/utils.ts +249 -0
- package/src/routes/openapi.ts +6 -0
- package/src/routes/research.ts +392 -0
- package/src/routes/root.ts +5 -0
- package/src/routes/session-approval.ts +63 -0
- package/src/routes/session-files.ts +387 -0
- package/src/routes/session-messages.ts +170 -0
- package/src/routes/session-stream.ts +61 -0
- package/src/routes/sessions.ts +814 -0
- package/src/routes/setu.ts +346 -0
- package/src/routes/terminals.ts +227 -0
- package/src/runtime/agent/registry.ts +351 -0
- package/src/runtime/agent/runner-reasoning.ts +108 -0
- package/src/runtime/agent/runner-setup.ts +257 -0
- package/src/runtime/agent/runner.ts +375 -0
- package/src/runtime/agent-registry.ts +6 -0
- package/src/runtime/ask/service.ts +369 -0
- package/src/runtime/context/environment.ts +202 -0
- package/src/runtime/debug/index.ts +117 -0
- package/src/runtime/debug/state.ts +140 -0
- package/src/runtime/errors/api-error.ts +192 -0
- package/src/runtime/errors/handling.ts +199 -0
- package/src/runtime/message/compaction-auto.ts +154 -0
- package/src/runtime/message/compaction-context.ts +101 -0
- package/src/runtime/message/compaction-detect.ts +26 -0
- package/src/runtime/message/compaction-limits.ts +37 -0
- package/src/runtime/message/compaction-mark.ts +111 -0
- package/src/runtime/message/compaction-prune.ts +75 -0
- package/src/runtime/message/compaction.ts +21 -0
- package/src/runtime/message/history-builder.ts +266 -0
- package/src/runtime/message/service.ts +468 -0
- package/src/runtime/message/tool-history-tracker.ts +204 -0
- package/src/runtime/prompt/builder.ts +167 -0
- package/src/runtime/provider/anthropic.ts +50 -0
- package/src/runtime/provider/copilot.ts +12 -0
- package/src/runtime/provider/google.ts +8 -0
- package/src/runtime/provider/index.ts +60 -0
- package/src/runtime/provider/moonshot.ts +8 -0
- package/src/runtime/provider/oauth-adapter.ts +237 -0
- package/src/runtime/provider/openai.ts +18 -0
- package/src/runtime/provider/opencode.ts +7 -0
- package/src/runtime/provider/openrouter.ts +7 -0
- package/src/runtime/provider/selection.ts +118 -0
- package/src/runtime/provider/setu.ts +126 -0
- package/src/runtime/provider/zai.ts +16 -0
- package/src/runtime/session/branch.ts +280 -0
- package/src/runtime/session/db-operations.ts +285 -0
- package/src/runtime/session/manager.ts +99 -0
- package/src/runtime/session/queue.ts +243 -0
- package/src/runtime/stream/abort-handler.ts +65 -0
- package/src/runtime/stream/error-handler.ts +371 -0
- package/src/runtime/stream/finish-handler.ts +101 -0
- package/src/runtime/stream/handlers.ts +5 -0
- package/src/runtime/stream/step-finish.ts +93 -0
- package/src/runtime/stream/types.ts +25 -0
- package/src/runtime/tools/approval.ts +180 -0
- package/src/runtime/tools/context.ts +83 -0
- package/src/runtime/tools/mapping.ts +154 -0
- package/src/runtime/tools/setup.ts +44 -0
- package/src/runtime/topup/manager.ts +110 -0
- package/src/runtime/utils/cwd.ts +69 -0
- package/src/runtime/utils/token.ts +35 -0
- package/src/tools/adapter.ts +634 -0
- package/src/tools/database/get-parent-session.ts +183 -0
- package/src/tools/database/get-session-context.ts +161 -0
- package/src/tools/database/index.ts +42 -0
- package/src/tools/database/present-session-links.ts +47 -0
- package/src/tools/database/query-messages.ts +160 -0
- package/src/tools/database/query-sessions.ts +126 -0
- package/src/tools/database/search-history.ts +135 -0
- package/src/types/sql-imports.d.ts +5 -0
- package/sst-env.d.ts +8 -0
- 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
|
+
}
|