@ottocode/sdk 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 (125) hide show
  1. package/README.md +338 -0
  2. package/package.json +128 -0
  3. package/src/agent/types.ts +19 -0
  4. package/src/auth/src/copilot-oauth.ts +190 -0
  5. package/src/auth/src/index.ts +100 -0
  6. package/src/auth/src/oauth.ts +234 -0
  7. package/src/auth/src/openai-oauth.ts +394 -0
  8. package/src/auth/src/wallet.ts +51 -0
  9. package/src/browser.ts +32 -0
  10. package/src/config/src/index.ts +110 -0
  11. package/src/config/src/manager.ts +181 -0
  12. package/src/config/src/paths.ts +98 -0
  13. package/src/core/src/errors.ts +102 -0
  14. package/src/core/src/index.ts +108 -0
  15. package/src/core/src/providers/resolver.ts +244 -0
  16. package/src/core/src/streaming/artifacts.ts +41 -0
  17. package/src/core/src/terminals/bun-pty.ts +13 -0
  18. package/src/core/src/terminals/circular-buffer.ts +30 -0
  19. package/src/core/src/terminals/ensure-bun-pty.ts +70 -0
  20. package/src/core/src/terminals/index.ts +8 -0
  21. package/src/core/src/terminals/manager.ts +158 -0
  22. package/src/core/src/terminals/rust-libs.ts +30 -0
  23. package/src/core/src/terminals/terminal.ts +132 -0
  24. package/src/core/src/tools/bin-manager.ts +250 -0
  25. package/src/core/src/tools/builtin/bash.ts +155 -0
  26. package/src/core/src/tools/builtin/bash.txt +7 -0
  27. package/src/core/src/tools/builtin/file-cache.ts +39 -0
  28. package/src/core/src/tools/builtin/finish.ts +12 -0
  29. package/src/core/src/tools/builtin/finish.txt +10 -0
  30. package/src/core/src/tools/builtin/fs/cd.ts +19 -0
  31. package/src/core/src/tools/builtin/fs/cd.txt +5 -0
  32. package/src/core/src/tools/builtin/fs/index.ts +20 -0
  33. package/src/core/src/tools/builtin/fs/ls.ts +72 -0
  34. package/src/core/src/tools/builtin/fs/ls.txt +8 -0
  35. package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
  36. package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
  37. package/src/core/src/tools/builtin/fs/read.ts +119 -0
  38. package/src/core/src/tools/builtin/fs/read.txt +8 -0
  39. package/src/core/src/tools/builtin/fs/tree.ts +149 -0
  40. package/src/core/src/tools/builtin/fs/tree.txt +11 -0
  41. package/src/core/src/tools/builtin/fs/util.ts +95 -0
  42. package/src/core/src/tools/builtin/fs/write.ts +106 -0
  43. package/src/core/src/tools/builtin/fs/write.txt +11 -0
  44. package/src/core/src/tools/builtin/git.commit.txt +6 -0
  45. package/src/core/src/tools/builtin/git.diff.txt +5 -0
  46. package/src/core/src/tools/builtin/git.status.txt +5 -0
  47. package/src/core/src/tools/builtin/git.ts +151 -0
  48. package/src/core/src/tools/builtin/glob.ts +128 -0
  49. package/src/core/src/tools/builtin/glob.txt +10 -0
  50. package/src/core/src/tools/builtin/grep.ts +136 -0
  51. package/src/core/src/tools/builtin/grep.txt +9 -0
  52. package/src/core/src/tools/builtin/ignore.ts +45 -0
  53. package/src/core/src/tools/builtin/patch/apply.ts +546 -0
  54. package/src/core/src/tools/builtin/patch/constants.ts +5 -0
  55. package/src/core/src/tools/builtin/patch/normalize.ts +31 -0
  56. package/src/core/src/tools/builtin/patch/parse-enveloped.ts +209 -0
  57. package/src/core/src/tools/builtin/patch/parse-unified.ts +231 -0
  58. package/src/core/src/tools/builtin/patch/parse.ts +28 -0
  59. package/src/core/src/tools/builtin/patch/text.ts +23 -0
  60. package/src/core/src/tools/builtin/patch/types.ts +82 -0
  61. package/src/core/src/tools/builtin/patch.ts +167 -0
  62. package/src/core/src/tools/builtin/patch.txt +207 -0
  63. package/src/core/src/tools/builtin/progress.ts +55 -0
  64. package/src/core/src/tools/builtin/progress.txt +7 -0
  65. package/src/core/src/tools/builtin/ripgrep.ts +125 -0
  66. package/src/core/src/tools/builtin/ripgrep.txt +7 -0
  67. package/src/core/src/tools/builtin/terminal.ts +300 -0
  68. package/src/core/src/tools/builtin/terminal.txt +93 -0
  69. package/src/core/src/tools/builtin/todos.ts +66 -0
  70. package/src/core/src/tools/builtin/todos.txt +7 -0
  71. package/src/core/src/tools/builtin/websearch.ts +250 -0
  72. package/src/core/src/tools/builtin/websearch.txt +12 -0
  73. package/src/core/src/tools/error.ts +67 -0
  74. package/src/core/src/tools/loader.ts +421 -0
  75. package/src/core/src/types/index.ts +11 -0
  76. package/src/core/src/types/types.ts +4 -0
  77. package/src/core/src/utils/ansi.ts +27 -0
  78. package/src/core/src/utils/debug.ts +40 -0
  79. package/src/core/src/utils/logger.ts +150 -0
  80. package/src/index.ts +313 -0
  81. package/src/prompts/src/agents/build.txt +89 -0
  82. package/src/prompts/src/agents/general.txt +15 -0
  83. package/src/prompts/src/agents/plan.txt +10 -0
  84. package/src/prompts/src/agents/research.txt +50 -0
  85. package/src/prompts/src/base.txt +24 -0
  86. package/src/prompts/src/debug.ts +104 -0
  87. package/src/prompts/src/index.ts +1 -0
  88. package/src/prompts/src/modes/oneshot.txt +9 -0
  89. package/src/prompts/src/providers/anthropic.txt +247 -0
  90. package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
  91. package/src/prompts/src/providers/default.txt +466 -0
  92. package/src/prompts/src/providers/google.txt +230 -0
  93. package/src/prompts/src/providers/moonshot.txt +24 -0
  94. package/src/prompts/src/providers/openai.txt +414 -0
  95. package/src/prompts/src/providers.ts +143 -0
  96. package/src/providers/src/anthropic-caching.ts +202 -0
  97. package/src/providers/src/anthropic-oauth-client.ts +157 -0
  98. package/src/providers/src/authorization.ts +17 -0
  99. package/src/providers/src/catalog-manual.ts +135 -0
  100. package/src/providers/src/catalog-merged.ts +9 -0
  101. package/src/providers/src/catalog.ts +8329 -0
  102. package/src/providers/src/copilot-client.ts +39 -0
  103. package/src/providers/src/env.ts +31 -0
  104. package/src/providers/src/google-client.ts +16 -0
  105. package/src/providers/src/index.ts +75 -0
  106. package/src/providers/src/moonshot-client.ts +25 -0
  107. package/src/providers/src/oauth-models.ts +39 -0
  108. package/src/providers/src/openai-oauth-client.ts +108 -0
  109. package/src/providers/src/opencode-client.ts +64 -0
  110. package/src/providers/src/openrouter-client.ts +31 -0
  111. package/src/providers/src/pricing.ts +178 -0
  112. package/src/providers/src/setu-client.ts +643 -0
  113. package/src/providers/src/utils.ts +210 -0
  114. package/src/providers/src/validate.ts +39 -0
  115. package/src/providers/src/zai-client.ts +47 -0
  116. package/src/skills/index.ts +34 -0
  117. package/src/skills/loader.ts +152 -0
  118. package/src/skills/parser.ts +108 -0
  119. package/src/skills/tool.ts +87 -0
  120. package/src/skills/types.ts +41 -0
  121. package/src/skills/validator.ts +110 -0
  122. package/src/types/src/auth.ts +33 -0
  123. package/src/types/src/config.ts +36 -0
  124. package/src/types/src/index.ts +20 -0
  125. package/src/types/src/provider.ts +71 -0
@@ -0,0 +1,394 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes, createHash } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+
5
+ const OPENAI_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
6
+ const OPENAI_ISSUER = 'https://auth.openai.com';
7
+ const OPENAI_CALLBACK_PORT = 1455;
8
+
9
+ function generatePKCE() {
10
+ const verifier = randomBytes(32)
11
+ .toString('base64')
12
+ .replace(/\+/g, '-')
13
+ .replace(/\//g, '_')
14
+ .replace(/=/g, '');
15
+
16
+ const challenge = createHash('sha256')
17
+ .update(verifier)
18
+ .digest('base64')
19
+ .replace(/\+/g, '-')
20
+ .replace(/\//g, '_')
21
+ .replace(/=/g, '');
22
+
23
+ return { verifier, challenge };
24
+ }
25
+
26
+ function generateState() {
27
+ return randomBytes(32)
28
+ .toString('base64')
29
+ .replace(/\+/g, '-')
30
+ .replace(/\//g, '_')
31
+ .replace(/=/g, '');
32
+ }
33
+
34
+ async function openBrowser(url: string) {
35
+ const platform = process.platform;
36
+ let command: string;
37
+
38
+ switch (platform) {
39
+ case 'darwin':
40
+ command = `open "${url}"`;
41
+ break;
42
+ case 'win32':
43
+ command = `start "${url}"`;
44
+ break;
45
+ default:
46
+ command = `xdg-open "${url}"`;
47
+ break;
48
+ }
49
+
50
+ return new Promise<void>((resolve, reject) => {
51
+ const child = spawn(command, [], { shell: true });
52
+ child.on('error', reject);
53
+ child.on('exit', (code) => {
54
+ if (code === 0) resolve();
55
+ else reject(new Error(`Failed to open browser (exit code ${code})`));
56
+ });
57
+ });
58
+ }
59
+
60
+ export type OpenAIOAuthResult = {
61
+ url: string;
62
+ verifier: string;
63
+ waitForCallback: () => Promise<string>;
64
+ close: () => void;
65
+ };
66
+
67
+ export async function authorizeOpenAI(): Promise<OpenAIOAuthResult> {
68
+ const pkce = generatePKCE();
69
+ const state = generateState();
70
+ const redirectUri = `http://localhost:${OPENAI_CALLBACK_PORT}/auth/callback`;
71
+
72
+ const params = new URLSearchParams({
73
+ response_type: 'code',
74
+ client_id: OPENAI_CLIENT_ID,
75
+ redirect_uri: redirectUri,
76
+ scope: 'openid profile email offline_access',
77
+ code_challenge: pkce.challenge,
78
+ code_challenge_method: 'S256',
79
+ id_token_add_organizations: 'true',
80
+ codex_cli_simplified_flow: 'true',
81
+ state: state,
82
+ });
83
+
84
+ const authUrl = `${OPENAI_ISSUER}/oauth/authorize?${params.toString()}`;
85
+
86
+ let resolveCallback: (code: string) => void;
87
+ let rejectCallback: (error: Error) => void;
88
+ const callbackPromise = new Promise<string>((resolve, reject) => {
89
+ resolveCallback = resolve;
90
+ rejectCallback = reject;
91
+ });
92
+
93
+ const server = createServer((req, res) => {
94
+ const reqUrl = new URL(
95
+ req.url || '/',
96
+ `http://localhost:${OPENAI_CALLBACK_PORT}`,
97
+ );
98
+
99
+ if (reqUrl.pathname === '/auth/callback') {
100
+ const code = reqUrl.searchParams.get('code');
101
+ const returnedState = reqUrl.searchParams.get('state');
102
+ const error = reqUrl.searchParams.get('error');
103
+
104
+ if (error) {
105
+ res.writeHead(400, { 'Content-Type': 'text/html' });
106
+ res.end(
107
+ `<html><body><h1>Authentication Failed</h1><p>${error}</p></body></html>`,
108
+ );
109
+ rejectCallback(new Error(`OAuth error: ${error}`));
110
+ return;
111
+ }
112
+
113
+ if (returnedState !== state) {
114
+ res.writeHead(400, { 'Content-Type': 'text/html' });
115
+ res.end(
116
+ '<html><body><h1>Invalid State</h1><p>State mismatch. Please try again.</p></body></html>',
117
+ );
118
+ rejectCallback(new Error('State mismatch'));
119
+ return;
120
+ }
121
+
122
+ if (code) {
123
+ res.writeHead(200, { 'Content-Type': 'text/html' });
124
+ res.end(`
125
+ <html>
126
+ <head>
127
+ <title>otto - Authentication Successful</title>
128
+ <style>
129
+ body {
130
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
131
+ display: flex;
132
+ justify-content: center;
133
+ align-items: center;
134
+ height: 100vh;
135
+ margin: 0;
136
+ background: linear-gradient(135deg, #10a37f 0%, #1a7f5a 100%);
137
+ color: white;
138
+ }
139
+ .container {
140
+ text-align: center;
141
+ padding: 2rem;
142
+ background: rgba(255,255,255,0.1);
143
+ border-radius: 16px;
144
+ backdrop-filter: blur(10px);
145
+ }
146
+ .checkmark { font-size: 4rem; margin-bottom: 1rem; }
147
+ h1 { margin: 0 0 0.5rem 0; }
148
+ p { margin: 0; opacity: 0.9; }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <div class="container">
153
+ <div class="checkmark">✓</div>
154
+ <h1>Connected!</h1>
155
+ <p>You can close this window.</p>
156
+ </div>
157
+ <script>
158
+ // Delay to allow server to complete token exchange
159
+ setTimeout(() => {
160
+ if (window.opener) {
161
+ window.opener.postMessage({ type: 'oauth-success', provider: 'openai' }, '*');
162
+ }
163
+ setTimeout(() => window.close(), 500);
164
+ }, 1500);
165
+ </script>
166
+ </body>
167
+ </html>
168
+ `);
169
+ resolveCallback(code);
170
+ } else {
171
+ res.writeHead(400, { 'Content-Type': 'text/html' });
172
+ res.end('<html><body><h1>Missing Code</h1></body></html>');
173
+ rejectCallback(new Error('No authorization code received'));
174
+ }
175
+ } else {
176
+ res.writeHead(404);
177
+ res.end('Not found');
178
+ }
179
+ });
180
+
181
+ await new Promise<void>((resolve, reject) => {
182
+ server.on('error', (err: NodeJS.ErrnoException) => {
183
+ if (err.code === 'EADDRINUSE') {
184
+ reject(
185
+ new Error(
186
+ `Port ${OPENAI_CALLBACK_PORT} is already in use. Make sure no other OAuth flow is running (including the official Codex CLI).`,
187
+ ),
188
+ );
189
+ } else {
190
+ reject(err);
191
+ }
192
+ });
193
+ server.listen(OPENAI_CALLBACK_PORT, '127.0.0.1', () => resolve());
194
+ });
195
+
196
+ return {
197
+ url: authUrl,
198
+ verifier: pkce.verifier,
199
+ waitForCallback: () => callbackPromise,
200
+ close: () => {
201
+ server.close();
202
+ },
203
+ };
204
+ }
205
+
206
+ export async function exchangeOpenAI(code: string, verifier: string) {
207
+ const redirectUri = `http://localhost:${OPENAI_CALLBACK_PORT}/auth/callback`;
208
+
209
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
210
+ method: 'POST',
211
+ headers: {
212
+ 'Content-Type': 'application/x-www-form-urlencoded',
213
+ },
214
+ body: new URLSearchParams({
215
+ grant_type: 'authorization_code',
216
+ code,
217
+ redirect_uri: redirectUri,
218
+ client_id: OPENAI_CLIENT_ID,
219
+ code_verifier: verifier,
220
+ }).toString(),
221
+ });
222
+
223
+ if (!response.ok) {
224
+ const error = await response.text();
225
+ throw new Error(`Token exchange failed: ${error}`);
226
+ }
227
+
228
+ const json = (await response.json()) as {
229
+ id_token: string;
230
+ access_token: string;
231
+ refresh_token: string;
232
+ expires_in?: number;
233
+ };
234
+
235
+ let accountId: string | undefined;
236
+ try {
237
+ const payload = JSON.parse(
238
+ Buffer.from(json.access_token.split('.')[1], 'base64').toString(),
239
+ );
240
+ accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id;
241
+ } catch {}
242
+
243
+ return {
244
+ idToken: json.id_token,
245
+ access: json.access_token,
246
+ refresh: json.refresh_token,
247
+ expires: Date.now() + (json.expires_in || 3600) * 1000,
248
+ accountId,
249
+ };
250
+ }
251
+
252
+ export async function refreshOpenAIToken(refreshToken: string) {
253
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
254
+ method: 'POST',
255
+ headers: {
256
+ 'Content-Type': 'application/json',
257
+ },
258
+ body: JSON.stringify({
259
+ client_id: OPENAI_CLIENT_ID,
260
+ grant_type: 'refresh_token',
261
+ refresh_token: refreshToken,
262
+ scope: 'openid profile email',
263
+ }),
264
+ });
265
+
266
+ if (!response.ok) {
267
+ throw new Error('Failed to refresh OpenAI token');
268
+ }
269
+
270
+ const json = (await response.json()) as {
271
+ id_token?: string;
272
+ access_token?: string;
273
+ refresh_token?: string;
274
+ expires_in?: number;
275
+ };
276
+
277
+ return {
278
+ idToken: json.id_token,
279
+ access: json.access_token || '',
280
+ refresh: json.refresh_token || refreshToken,
281
+ expires: Date.now() + (json.expires_in || 3600) * 1000,
282
+ };
283
+ }
284
+
285
+ export async function openOpenAIAuthUrl(url: string) {
286
+ try {
287
+ await openBrowser(url);
288
+ return true;
289
+ } catch {
290
+ return false;
291
+ }
292
+ }
293
+
294
+ export async function obtainOpenAIApiKey(idToken: string): Promise<string> {
295
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
296
+ method: 'POST',
297
+ headers: {
298
+ 'Content-Type': 'application/x-www-form-urlencoded',
299
+ },
300
+ body: new URLSearchParams({
301
+ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
302
+ client_id: OPENAI_CLIENT_ID,
303
+ requested_token: 'openai-api-key',
304
+ subject_token: idToken,
305
+ subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
306
+ }).toString(),
307
+ });
308
+
309
+ if (!response.ok) {
310
+ const error = await response.text();
311
+ throw new Error(`API key exchange failed: ${error}`);
312
+ }
313
+
314
+ const json = (await response.json()) as {
315
+ access_token: string;
316
+ };
317
+
318
+ return json.access_token;
319
+ }
320
+
321
+ export function authorizeOpenAIWeb(redirectUri: string): {
322
+ url: string;
323
+ verifier: string;
324
+ state: string;
325
+ } {
326
+ const pkce = generatePKCE();
327
+ const state = generateState();
328
+
329
+ const params = new URLSearchParams({
330
+ response_type: 'code',
331
+ client_id: OPENAI_CLIENT_ID,
332
+ redirect_uri: redirectUri,
333
+ scope: 'openid profile email offline_access',
334
+ code_challenge: pkce.challenge,
335
+ code_challenge_method: 'S256',
336
+ id_token_add_organizations: 'true',
337
+ codex_cli_simplified_flow: 'true',
338
+ state: state,
339
+ });
340
+
341
+ return {
342
+ url: `${OPENAI_ISSUER}/oauth/authorize?${params.toString()}`,
343
+ verifier: pkce.verifier,
344
+ state,
345
+ };
346
+ }
347
+
348
+ export async function exchangeOpenAIWeb(
349
+ code: string,
350
+ verifier: string,
351
+ redirectUri: string,
352
+ ) {
353
+ const response = await fetch(`${OPENAI_ISSUER}/oauth/token`, {
354
+ method: 'POST',
355
+ headers: {
356
+ 'Content-Type': 'application/x-www-form-urlencoded',
357
+ },
358
+ body: new URLSearchParams({
359
+ grant_type: 'authorization_code',
360
+ code,
361
+ redirect_uri: redirectUri,
362
+ client_id: OPENAI_CLIENT_ID,
363
+ code_verifier: verifier,
364
+ }).toString(),
365
+ });
366
+
367
+ if (!response.ok) {
368
+ const error = await response.text();
369
+ throw new Error(`Token exchange failed: ${error}`);
370
+ }
371
+
372
+ const json = (await response.json()) as {
373
+ id_token: string;
374
+ access_token: string;
375
+ refresh_token: string;
376
+ expires_in?: number;
377
+ };
378
+
379
+ let accountId: string | undefined;
380
+ try {
381
+ const payload = JSON.parse(
382
+ Buffer.from(json.access_token.split('.')[1], 'base64').toString(),
383
+ );
384
+ accountId = payload['https://api.openai.com/auth']?.chatgpt_account_id;
385
+ } catch {}
386
+
387
+ return {
388
+ idToken: json.id_token,
389
+ access: json.access_token,
390
+ refresh: json.refresh_token,
391
+ expires: Date.now() + (json.expires_in || 3600) * 1000,
392
+ accountId,
393
+ };
394
+ }
@@ -0,0 +1,51 @@
1
+ import { Keypair } from '@solana/web3.js';
2
+ import bs58 from 'bs58';
3
+ import { getAuth, setAuth } from './index.ts';
4
+
5
+ export interface WalletInfo {
6
+ publicKey: string;
7
+ privateKey: string;
8
+ }
9
+
10
+ export function generateWallet(): WalletInfo {
11
+ const keypair = Keypair.generate();
12
+ return {
13
+ privateKey: bs58.encode(keypair.secretKey),
14
+ publicKey: keypair.publicKey.toBase58(),
15
+ };
16
+ }
17
+
18
+ export function importWallet(privateKey: string): WalletInfo {
19
+ const privateKeyBytes = bs58.decode(privateKey);
20
+ const keypair = Keypair.fromSecretKey(privateKeyBytes);
21
+ return {
22
+ privateKey,
23
+ publicKey: keypair.publicKey.toBase58(),
24
+ };
25
+ }
26
+
27
+ export async function getSetuWallet(
28
+ projectRoot?: string,
29
+ ): Promise<WalletInfo | null> {
30
+ const auth = await getAuth('setu', projectRoot);
31
+ if (auth?.type === 'wallet' && auth.secret) {
32
+ return importWallet(auth.secret);
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export async function ensureSetuWallet(
38
+ projectRoot?: string,
39
+ ): Promise<WalletInfo> {
40
+ const existing = await getSetuWallet(projectRoot);
41
+ if (existing) return existing;
42
+
43
+ const wallet = generateWallet();
44
+ await setAuth(
45
+ 'setu',
46
+ { type: 'wallet', secret: wallet.privateKey },
47
+ projectRoot,
48
+ 'global',
49
+ );
50
+ return wallet;
51
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,32 @@
1
+ // ============================================================================
2
+ // @ottocode/sdk/browser - Browser-safe exports
3
+ // ============================================================================
4
+ // This module exports only browser-compatible code (no Bun, no Node.js APIs).
5
+ // Use this entry point for web applications like web-sdk.
6
+ //
7
+ // Usage:
8
+ // import { estimateModelCostUsd, type ProviderId } from '@ottocode/sdk/browser';
9
+ // ============================================================================
10
+
11
+ // Types (pure TypeScript, no runtime dependencies)
12
+ export type {
13
+ ProviderId,
14
+ ModelInfo,
15
+ ModelProviderBinding,
16
+ ProviderCatalogEntry,
17
+ } from './types/src/index.ts';
18
+
19
+ // Pricing utilities (pure TypeScript)
20
+ export { estimateModelCostUsd } from './providers/src/pricing.ts';
21
+
22
+ // Catalog data (pure TypeScript)
23
+ export { catalog } from './providers/src/catalog-merged.ts';
24
+
25
+ // Provider utilities (pure TypeScript - imported directly to avoid pulling in Bun deps)
26
+ export {
27
+ isProviderId,
28
+ providerIds,
29
+ defaultModelFor,
30
+ hasModel,
31
+ getModelInfo,
32
+ } from './providers/src/utils.ts';
@@ -0,0 +1,110 @@
1
+ import {
2
+ getGlobalConfigPath,
3
+ getLocalDataDir,
4
+ ensureDir,
5
+ fileExists,
6
+ joinPath,
7
+ } from './paths.ts';
8
+ import type { OttoConfig } from '../../types/src/index.ts';
9
+
10
+ export type { OttoConfig } from '../../types/src/index.ts';
11
+
12
+ const DEFAULTS: { defaults: OttoConfig['defaults'] } = {
13
+ defaults: {
14
+ agent: 'build',
15
+ provider: 'setu',
16
+ model: 'kimi-k2.5',
17
+ toolApproval: 'auto',
18
+ },
19
+ };
20
+
21
+ export async function loadConfig(
22
+ projectRootInput?: string,
23
+ ): Promise<OttoConfig> {
24
+ const projectRoot = projectRootInput
25
+ ? String(projectRootInput)
26
+ : process.cwd();
27
+
28
+ const dataDir = getLocalDataDir(projectRoot);
29
+ const dbPath = joinPath(dataDir, 'otto.sqlite');
30
+ const projectConfigPath = joinPath(dataDir, 'config.json');
31
+ const globalConfigPath = getGlobalConfigPath();
32
+
33
+ const projectCfg = await readJsonOptional(projectConfigPath);
34
+ const globalCfg = await readJsonOptional(globalConfigPath);
35
+
36
+ const merged = deepMerge(DEFAULTS, globalCfg, projectCfg);
37
+
38
+ await ensureDir(dataDir);
39
+
40
+ return {
41
+ projectRoot,
42
+ defaults: merged.defaults as OttoConfig['defaults'],
43
+ paths: {
44
+ dataDir,
45
+ dbPath,
46
+ projectConfigPath: (await fileExists(projectConfigPath))
47
+ ? projectConfigPath
48
+ : null,
49
+ globalConfigPath: (await fileExists(globalConfigPath))
50
+ ? globalConfigPath
51
+ : null,
52
+ },
53
+ } satisfies OttoConfig;
54
+ }
55
+
56
+ type JsonObject = Record<string, unknown>;
57
+
58
+ async function readJsonOptional(file: string): Promise<JsonObject | undefined> {
59
+ const f = Bun.file(file);
60
+ if (!(await f.exists())) return undefined;
61
+ try {
62
+ const buf = await f.text();
63
+ const parsed = JSON.parse(buf);
64
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
65
+ return parsed as JsonObject;
66
+ }
67
+ return undefined;
68
+ } catch {
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ function deepMerge<T extends JsonObject>(
74
+ ...objects: Array<JsonObject | undefined>
75
+ ): T {
76
+ const result: JsonObject = {};
77
+ for (const obj of objects) {
78
+ if (!obj) continue;
79
+ mergeInto(result, obj);
80
+ }
81
+ return result as T;
82
+ }
83
+
84
+ function mergeInto(target: JsonObject, source: JsonObject): JsonObject {
85
+ for (const key of Object.keys(source)) {
86
+ const sv = source[key];
87
+ const tv = target[key];
88
+ if (sv && typeof sv === 'object' && !Array.isArray(sv)) {
89
+ const svObj = sv as JsonObject;
90
+ const nextTarget =
91
+ tv && typeof tv === 'object' && !Array.isArray(tv)
92
+ ? (tv as JsonObject)
93
+ : {};
94
+ target[key] = mergeInto(nextTarget, svObj);
95
+ } else {
96
+ target[key] = sv;
97
+ }
98
+ }
99
+ return target;
100
+ }
101
+
102
+ export type { Scope } from './manager.ts';
103
+ export {
104
+ read,
105
+ isAuthorized,
106
+ ensureEnv,
107
+ writeDefaults,
108
+ writeAuth,
109
+ removeAuth,
110
+ } from './manager.ts';