@ottocode/sdk 0.1.228 → 0.1.231

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/sdk",
3
- "version": "0.1.228",
3
+ "version": "0.1.231",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -34,6 +34,8 @@ const DEFAULTS: {
34
34
  toolApproval: 'auto',
35
35
  guidedMode: false,
36
36
  reasoningText: true,
37
+ reasoningLevel: 'high',
38
+ fullWidthContent: true,
37
39
  },
38
40
  providers: DEFAULT_PROVIDER_SETTINGS,
39
41
  };
@@ -66,6 +66,7 @@ export async function writeDefaults(
66
66
  toolApproval: 'auto' | 'dangerous' | 'all';
67
67
  guidedMode: boolean;
68
68
  reasoningText: boolean;
69
+ reasoningLevel: 'minimal' | 'low' | 'medium' | 'high' | 'max' | 'xhigh';
69
70
  theme: string;
70
71
  }>,
71
72
  projectRoot?: string,
@@ -115,9 +115,15 @@ export { isDebugEnabled, isTraceEnabled } from './utils/debug.ts';
115
115
  export {
116
116
  MCPClientWrapper,
117
117
  MCPServerManager,
118
+ COPILOT_MCP_SCOPE,
118
119
  convertMCPToolsToAISDK,
120
+ getCopilotMCPOAuthKey,
121
+ getStoredCopilotMCPToken,
119
122
  getMCPManager,
123
+ hasCopilotMCPScopes,
120
124
  initializeMCP,
125
+ isGitHubCopilotUrl,
126
+ isStoredCopilotMCPAuthenticated,
121
127
  shutdownMCP,
122
128
  loadMCPConfig,
123
129
  addMCPServerToConfig,
@@ -0,0 +1,104 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { MCPScope } from './types.ts';
3
+ import type { OAuthCredentialStore } from './oauth/store.ts';
4
+
5
+ export const GITHUB_COPILOT_HOSTS = [
6
+ 'api.githubcopilot.com',
7
+ 'copilot-proxy.githubusercontent.com',
8
+ ];
9
+
10
+ export const COPILOT_MCP_REQUIRED_SCOPES = [
11
+ 'repo',
12
+ 'read:org',
13
+ 'gist',
14
+ 'notifications',
15
+ 'read:project',
16
+ 'security_events',
17
+ ];
18
+
19
+ export const COPILOT_MCP_SCOPE =
20
+ 'repo read:org read:packages gist notifications read:project security_events';
21
+
22
+ /**
23
+ * Returns whether a URL points at a GitHub Copilot-backed MCP endpoint.
24
+ */
25
+ export function isGitHubCopilotUrl(url?: string): boolean {
26
+ if (!url) return false;
27
+ try {
28
+ const parsed = new URL(url);
29
+ return GITHUB_COPILOT_HOSTS.some(
30
+ (host) =>
31
+ parsed.hostname === host || parsed.hostname.endsWith(`.${host}`),
32
+ );
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Returns whether the stored scope string satisfies the GitHub Copilot MCP requirements.
40
+ */
41
+ export function hasCopilotMCPScopes(scopes?: string): boolean {
42
+ if (!scopes) return false;
43
+ const granted = scopes.split(/[\s,]+/).filter(Boolean);
44
+ return COPILOT_MCP_REQUIRED_SCOPES.every((scope) => granted.includes(scope));
45
+ }
46
+
47
+ /**
48
+ * Builds the MCP OAuth store key for a GitHub Copilot MCP server.
49
+ */
50
+ export function getCopilotMCPOAuthKey(
51
+ serverName: string,
52
+ scope: MCPScope = 'global',
53
+ projectRoot?: string,
54
+ ): string {
55
+ if (scope === 'project' && projectRoot) {
56
+ const hash = createHash('sha256')
57
+ .update(projectRoot)
58
+ .digest('hex')
59
+ .slice(0, 8);
60
+ return `${serverName}_proj_${hash}`;
61
+ }
62
+ return serverName;
63
+ }
64
+
65
+ /**
66
+ * Loads the stored GitHub Copilot MCP bearer token for a server.
67
+ */
68
+ export async function getStoredCopilotMCPToken(
69
+ store: OAuthCredentialStore,
70
+ serverName: string,
71
+ scope: MCPScope = 'global',
72
+ projectRoot?: string,
73
+ ): Promise<{ token: string | null; needsReauth: boolean; scopes?: string }> {
74
+ const tokens = await store.loadTokens(
75
+ getCopilotMCPOAuthKey(serverName, scope, projectRoot),
76
+ );
77
+ const token = tokens?.access_token ?? null;
78
+ if (!token) {
79
+ return { token: null, needsReauth: true };
80
+ }
81
+ return {
82
+ token,
83
+ needsReauth: !hasCopilotMCPScopes(tokens?.scope),
84
+ scopes: tokens?.scope,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Returns whether the stored GitHub Copilot MCP credentials are usable.
90
+ */
91
+ export async function isStoredCopilotMCPAuthenticated(
92
+ store: OAuthCredentialStore,
93
+ serverName: string,
94
+ scope: MCPScope = 'global',
95
+ projectRoot?: string,
96
+ ): Promise<boolean> {
97
+ const { token, needsReauth } = await getStoredCopilotMCPToken(
98
+ store,
99
+ serverName,
100
+ scope,
101
+ projectRoot,
102
+ );
103
+ return !!token && !needsReauth;
104
+ }
@@ -13,6 +13,15 @@ export { MCPServerManager } from './server-manager.ts';
13
13
 
14
14
  export { convertMCPToolsToAISDK } from './tools.ts';
15
15
 
16
+ export {
17
+ COPILOT_MCP_SCOPE,
18
+ getCopilotMCPOAuthKey,
19
+ getStoredCopilotMCPToken,
20
+ hasCopilotMCPScopes,
21
+ isGitHubCopilotUrl,
22
+ isStoredCopilotMCPAuthenticated,
23
+ } from './copilot-auth.ts';
24
+
16
25
  export {
17
26
  getMCPToolBriefs,
18
27
  buildLoadMCPToolsTool,
@@ -2,66 +2,12 @@ import { MCPClientWrapper, type MCPToolInfo } from './client.ts';
2
2
  import type { MCPServerConfig, MCPServerStatus } from './types.ts';
3
3
  import { OAuthCredentialStore } from './oauth/store.ts';
4
4
  import { OttoOAuthProvider } from './oauth/provider.ts';
5
- import { createHash } from 'node:crypto';
6
- import { getAuth } from '../../../auth/src/index.ts';
7
-
8
- const GITHUB_COPILOT_HOSTS = [
9
- 'api.githubcopilot.com',
10
- 'copilot-proxy.githubusercontent.com',
11
- ];
12
-
13
- function isGitHubCopilotUrl(url?: string): boolean {
14
- if (!url) return false;
15
- try {
16
- const parsed = new URL(url);
17
- return GITHUB_COPILOT_HOSTS.some(
18
- (h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`),
19
- );
20
- } catch {
21
- return false;
22
- }
23
- }
24
-
25
- const COPILOT_MCP_REQUIRED_SCOPES = [
26
- 'repo',
27
- 'read:org',
28
- 'gist',
29
- 'notifications',
30
- 'read:project',
31
- 'security_events',
32
- ];
33
-
34
- function hasMCPScopes(scopes?: string): boolean {
35
- if (!scopes) return false;
36
- const granted = scopes.split(/[\s,]+/).filter(Boolean);
37
- return COPILOT_MCP_REQUIRED_SCOPES.every((s) => granted.includes(s));
38
- }
39
-
40
- async function getCopilotToken(): Promise<string | null> {
41
- try {
42
- const auth = await getAuth('copilot');
43
- if (auth?.type === 'oauth' && auth.refresh) {
44
- return auth.refresh;
45
- }
46
- } catch {}
47
- return null;
48
- }
49
-
50
- async function getCopilotMCPToken(): Promise<{
51
- token: string | null;
52
- needsReauth: boolean;
53
- }> {
54
- try {
55
- const auth = await getAuth('copilot');
56
- if (auth?.type === 'oauth' && auth.refresh) {
57
- if (!hasMCPScopes(auth.scopes)) {
58
- return { token: auth.refresh, needsReauth: true };
59
- }
60
- return { token: auth.refresh, needsReauth: false };
61
- }
62
- } catch {}
63
- return { token: null, needsReauth: true };
64
- }
5
+ import {
6
+ getCopilotMCPOAuthKey,
7
+ getStoredCopilotMCPToken,
8
+ isGitHubCopilotUrl,
9
+ isStoredCopilotMCPAuthenticated,
10
+ } from './copilot-auth.ts';
65
11
 
66
12
  type IndexedTool = {
67
13
  server: string;
@@ -88,14 +34,11 @@ export class MCPServerManager {
88
34
 
89
35
  private oauthKey(serverName: string): string {
90
36
  const scope = this.serverScopes.get(serverName);
91
- if (scope === 'project' && this.projectRoot) {
92
- const hash = createHash('sha256')
93
- .update(this.projectRoot)
94
- .digest('hex')
95
- .slice(0, 8);
96
- return `${serverName}_proj_${hash}`;
97
- }
98
- return serverName;
37
+ return getCopilotMCPOAuthKey(
38
+ serverName,
39
+ scope,
40
+ this.projectRoot ?? undefined,
41
+ );
99
42
  }
100
43
 
101
44
  async startServers(configs: MCPServerConfig[]): Promise<void> {
@@ -115,7 +58,12 @@ export class MCPServerManager {
115
58
 
116
59
  if (transport !== 'stdio') {
117
60
  if (isGitHubCopilotUrl(config.url)) {
118
- const { token, needsReauth } = await getCopilotMCPToken();
61
+ const { token, needsReauth } = await getStoredCopilotMCPToken(
62
+ this.oauthStore,
63
+ config.name,
64
+ config.scope ?? 'global',
65
+ this.projectRoot ?? undefined,
66
+ );
119
67
  if (token && !needsReauth) {
120
68
  config = {
121
69
  ...config,
@@ -159,7 +107,7 @@ export class MCPServerManager {
159
107
  return;
160
108
  }
161
109
  console.error(
162
- `[mcp] GitHub Copilot MCP server "${config.name}" requires authentication. Run \`otto auth login copilot\` or \`otto mcp auth ${config.name}\`.`,
110
+ `[mcp] GitHub Copilot MCP server "${config.name}" requires authentication. Run \`otto mcp auth ${config.name}\`.`,
163
111
  );
164
112
  this.clients.set(config.name, client);
165
113
  return;
@@ -296,7 +244,12 @@ export class MCPServerManager {
296
244
  const config = client.serverConfig;
297
245
  let authenticated = false;
298
246
  if (isGitHubCopilotUrl(config.url)) {
299
- authenticated = !!(await getCopilotToken());
247
+ authenticated = await isStoredCopilotMCPAuthenticated(
248
+ this.oauthStore,
249
+ name,
250
+ config.scope ?? 'global',
251
+ this.projectRoot ?? undefined,
252
+ ).catch(() => false);
300
253
  } else {
301
254
  const key = this.oauthKey(name);
302
255
  authenticated = await this.oauthStore
@@ -334,8 +287,13 @@ export class MCPServerManager {
334
287
  if (transport === 'stdio') return null;
335
288
 
336
289
  if (isGitHubCopilotUrl(config.url)) {
337
- const token = await getCopilotToken();
338
- if (token) {
290
+ const { token, needsReauth } = await getStoredCopilotMCPToken(
291
+ this.oauthStore,
292
+ config.name,
293
+ config.scope ?? 'global',
294
+ this.projectRoot ?? undefined,
295
+ );
296
+ if (token && !needsReauth) {
339
297
  const authedConfig = {
340
298
  ...config,
341
299
  headers: {
@@ -472,8 +430,19 @@ export class MCPServerManager {
472
430
  ): Promise<{ authenticated: boolean; expiresAt?: number }> {
473
431
  const client = this.clients.get(name);
474
432
  if (client && isGitHubCopilotUrl(client.serverConfig.url)) {
475
- const token = await getCopilotToken();
476
- return { authenticated: !!token };
433
+ const key = this.oauthKey(name);
434
+ const tokens = await this.oauthStore.loadTokens(key);
435
+ return {
436
+ authenticated:
437
+ !!tokens?.access_token &&
438
+ (await isStoredCopilotMCPAuthenticated(
439
+ this.oauthStore,
440
+ name,
441
+ client.serverConfig.scope ?? 'global',
442
+ this.projectRoot ?? undefined,
443
+ )),
444
+ expiresAt: tokens?.expires_at,
445
+ };
477
446
  }
478
447
  const key = this.oauthKey(name);
479
448
  const tokens = await this.oauthStore.loadTokens(key);
@@ -1,20 +1,15 @@
1
- // @ts-expect-error Bun file asset import
2
1
  import darwinArm64 from 'bun-pty/rust-pty/target/release/librust_pty_arm64.dylib' with {
3
2
  type: 'file',
4
3
  };
5
- // @ts-expect-error Bun file asset import
6
4
  import darwinX64 from 'bun-pty/rust-pty/target/release/librust_pty.dylib' with {
7
5
  type: 'file',
8
6
  };
9
- // @ts-expect-error Bun file asset import
10
7
  import linuxArm64 from 'bun-pty/rust-pty/target/release/librust_pty_arm64.so' with {
11
8
  type: 'file',
12
9
  };
13
- // @ts-expect-error Bun file asset import
14
10
  import linuxX64 from 'bun-pty/rust-pty/target/release/librust_pty.so' with {
15
11
  type: 'file',
16
12
  };
17
- // @ts-expect-error Bun file asset import
18
13
  import windowsDll from 'bun-pty/rust-pty/target/release/rust_pty.dll' with {
19
14
  type: 'file',
20
15
  };
@@ -1,9 +1,9 @@
1
1
  import { tool, type Tool } from 'ai';
2
- import { z } from 'zod/v3';
3
2
  import { spawn } from 'node:child_process';
3
+ import { z } from 'zod/v3';
4
4
  import DESCRIPTION from './bash.txt' with { type: 'text' };
5
- import { createToolError, type ToolResponse } from '../error.ts';
6
5
  import { getAugmentedPath } from '../bin-manager.ts';
6
+ import { createToolError, type ToolResponse } from '../error.ts';
7
7
  import { injectCoAuthorIntoGitCommit } from './git-identity.ts';
8
8
 
9
9
  function normalizePath(p: string) {
@@ -41,10 +41,26 @@ function killProcessTree(pid: number) {
41
41
  }
42
42
  }
43
43
 
44
+ type BashResult = ToolResponse<{
45
+ exitCode: number;
46
+ stdout: string;
47
+ stderr: string;
48
+ }>;
49
+
50
+ type BashStreamChunk =
51
+ | {
52
+ channel: 'output';
53
+ delta: string;
54
+ }
55
+ | {
56
+ result: BashResult;
57
+ };
58
+
44
59
  export function buildBashTool(projectRoot: string): {
45
60
  name: string;
46
61
  tool: Tool;
47
62
  } {
63
+ // biome-ignore lint/suspicious/noExplicitAny: AI SDK tool typings do not model async-iterable execute results yet.
48
64
  const bash = tool({
49
65
  description: DESCRIPTION,
50
66
  inputSchema: z
@@ -66,7 +82,7 @@ export function buildBashTool(projectRoot: string): {
66
82
  .describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
67
83
  })
68
84
  .strict(),
69
- async execute(
85
+ execute(
70
86
  {
71
87
  cmd,
72
88
  cwd,
@@ -79,13 +95,7 @@ export function buildBashTool(projectRoot: string): {
79
95
  timeout?: number;
80
96
  },
81
97
  options?: { abortSignal?: AbortSignal },
82
- ): Promise<
83
- ToolResponse<{
84
- exitCode: number;
85
- stdout: string;
86
- stderr: string;
87
- }>
88
- > {
98
+ ): AsyncIterable<BashStreamChunk> | BashResult {
89
99
  const abortSignal = options?.abortSignal;
90
100
 
91
101
  if (abortSignal?.aborted) {
@@ -95,126 +105,162 @@ export function buildBashTool(projectRoot: string): {
95
105
  }
96
106
 
97
107
  const absCwd = resolveSafePath(projectRoot, cwd || '.');
98
-
99
108
  const finalCmd = injectCoAuthorIntoGitCommit(cmd);
100
109
 
101
- return new Promise((resolve) => {
102
- const proc = spawn(finalCmd, {
103
- cwd: absCwd,
104
- shell: true,
105
- stdio: ['ignore', 'pipe', 'pipe'],
106
- env: { ...process.env, PATH: getAugmentedPath() },
107
- detached: true,
108
- });
110
+ const proc = spawn(finalCmd, {
111
+ cwd: absCwd,
112
+ shell: true,
113
+ stdio: ['ignore', 'pipe', 'pipe'],
114
+ env: { ...process.env, PATH: getAugmentedPath() },
115
+ detached: true,
116
+ });
109
117
 
110
- let stdout = '';
111
- let stderr = '';
112
- let didTimeout = false;
113
- let didAbort = false;
114
- let settled = false;
115
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
116
-
117
- const settle = (
118
- result: ToolResponse<{
119
- exitCode: number;
120
- stdout: string;
121
- stderr: string;
122
- }>,
123
- ) => {
124
- if (settled) return;
125
- settled = true;
126
- if (timeoutId) clearTimeout(timeoutId);
127
- if (abortSignal) {
128
- abortSignal.removeEventListener('abort', onAbort);
129
- }
130
- resolve(result);
131
- };
118
+ let stdout = '';
119
+ let stderr = '';
120
+ let didTimeout = false;
121
+ let didAbort = false;
122
+ let settled = false;
123
+ let done = false;
124
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
125
+ const queue: BashStreamChunk[] = [];
126
+ let notify: (() => void) | null = null;
132
127
 
133
- const onAbort = () => {
134
- if (settled) return;
135
- didAbort = true;
136
- if (proc.pid) killProcessTree(proc.pid);
137
- else proc.kill('SIGTERM');
138
- };
128
+ const wake = () => {
129
+ if (!notify) return;
130
+ notify();
131
+ notify = null;
132
+ };
133
+
134
+ const pushDelta = (text: string) => {
135
+ if (!text) return;
136
+ queue.push({ channel: 'output', delta: text });
137
+ wake();
138
+ };
139
139
 
140
+ const settle = (result: BashResult) => {
141
+ if (settled) return;
142
+ settled = true;
143
+ if (timeoutId) clearTimeout(timeoutId);
140
144
  if (abortSignal) {
141
- abortSignal.addEventListener('abort', onAbort, { once: true });
145
+ abortSignal.removeEventListener('abort', onAbort);
142
146
  }
147
+ queue.push({ result });
148
+ done = true;
149
+ wake();
150
+ };
143
151
 
144
- if (timeout > 0) {
145
- timeoutId = setTimeout(() => {
146
- didTimeout = true;
147
- if (proc.pid) killProcessTree(proc.pid);
148
- else proc.kill();
149
- }, timeout);
150
- }
152
+ const onAbort = () => {
153
+ if (settled) return;
154
+ didAbort = true;
155
+ if (proc.pid) killProcessTree(proc.pid);
156
+ else proc.kill('SIGTERM');
157
+ };
151
158
 
152
- proc.stdout?.on('data', (chunk) => {
153
- stdout += chunk.toString();
154
- });
159
+ if (abortSignal) {
160
+ abortSignal.addEventListener('abort', onAbort, { once: true });
161
+ }
155
162
 
156
- proc.stderr?.on('data', (chunk) => {
157
- stderr += chunk.toString();
158
- });
163
+ if (timeout > 0) {
164
+ timeoutId = setTimeout(() => {
165
+ didTimeout = true;
166
+ if (proc.pid) killProcessTree(proc.pid);
167
+ else proc.kill();
168
+ }, timeout);
169
+ }
159
170
 
160
- proc.on('close', (exitCode) => {
161
- if (didAbort) {
162
- settle(
163
- createToolError(`Command aborted by user: ${cmd}`, 'abort', {
164
- cmd,
165
- stdout,
166
- stderr,
167
- }),
168
- );
169
- } else if (didTimeout) {
170
- settle(
171
- createToolError(
172
- `Command timed out after ${timeout}ms: ${cmd}`,
173
- 'timeout',
174
- {
175
- parameter: 'timeout',
176
- value: timeout,
177
- suggestion: 'Increase timeout or optimize the command',
178
- },
179
- ),
180
- );
181
- } else if (exitCode !== 0 && !allowNonZeroExit) {
182
- const errorDetail = stderr.trim() || stdout.trim() || '';
183
- const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
184
- settle(
185
- createToolError(errorMsg, 'execution', {
186
- exitCode,
187
- stdout,
188
- stderr,
189
- cmd,
190
- suggestion:
191
- 'Check command syntax or use allowNonZeroExit: true',
192
- }),
193
- );
194
- } else {
195
- settle({
196
- ok: true,
197
- exitCode: exitCode ?? 0,
171
+ proc.stdout?.on('data', (chunk) => {
172
+ const text = chunk.toString();
173
+ stdout += text;
174
+ pushDelta(text);
175
+ });
176
+
177
+ proc.stderr?.on('data', (chunk) => {
178
+ const text = chunk.toString();
179
+ stderr += text;
180
+ pushDelta(text);
181
+ });
182
+
183
+ proc.on('close', (exitCode) => {
184
+ if (didAbort) {
185
+ settle(
186
+ createToolError(`Command aborted by user: ${cmd}`, 'abort', {
187
+ cmd,
198
188
  stdout,
199
189
  stderr,
200
- });
201
- }
202
- });
190
+ }),
191
+ );
192
+ return;
193
+ }
203
194
 
204
- proc.on('error', (err) => {
195
+ if (didTimeout) {
205
196
  settle(
206
197
  createToolError(
207
- `Command execution failed: ${err.message}`,
208
- 'execution',
198
+ `Command timed out after ${timeout}ms: ${cmd}`,
199
+ 'timeout',
209
200
  {
210
- cmd,
211
- originalError: err.message,
201
+ parameter: 'timeout',
202
+ value: timeout,
203
+ stdout,
204
+ stderr,
205
+ suggestion: 'Increase timeout or optimize the command',
212
206
  },
213
207
  ),
214
208
  );
209
+ return;
210
+ }
211
+
212
+ if (exitCode !== 0 && !allowNonZeroExit) {
213
+ const errorDetail = stderr.trim() || stdout.trim() || '';
214
+ const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
215
+ settle(
216
+ createToolError(errorMsg, 'execution', {
217
+ exitCode,
218
+ stdout,
219
+ stderr,
220
+ cmd,
221
+ suggestion: 'Check command syntax or use allowNonZeroExit: true',
222
+ }),
223
+ );
224
+ return;
225
+ }
226
+
227
+ settle({
228
+ ok: true,
229
+ exitCode: exitCode ?? 0,
230
+ stdout,
231
+ stderr,
215
232
  });
216
233
  });
234
+
235
+ proc.on('error', (err) => {
236
+ settle(
237
+ createToolError(
238
+ `Command execution failed: ${err.message}`,
239
+ 'execution',
240
+ {
241
+ cmd,
242
+ originalError: err.message,
243
+ },
244
+ ),
245
+ );
246
+ });
247
+
248
+ const stream = async function* (): AsyncGenerator<BashStreamChunk> {
249
+ while (!done || queue.length > 0) {
250
+ if (queue.length === 0) {
251
+ await new Promise<void>((resolve) => {
252
+ notify = resolve;
253
+ });
254
+ }
255
+ while (queue.length > 0) {
256
+ const chunk = queue.shift();
257
+ if (chunk) yield chunk;
258
+ }
259
+ }
260
+ };
261
+
262
+ return stream();
217
263
  },
218
- });
264
+ } as any);
219
265
  return { name: 'bash', tool: bash };
220
266
  }
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export type {
29
29
  DefaultConfig,
30
30
  PathConfig,
31
31
  OttoConfig,
32
+ ReasoningLevel,
32
33
  } from './types/src/index.ts';
33
34
 
34
35
  // =======================
@@ -361,9 +362,15 @@ export type { TunnelConnection, TunnelEvents } from './tunnel/index.ts';
361
362
  export {
362
363
  MCPClientWrapper,
363
364
  MCPServerManager,
365
+ COPILOT_MCP_SCOPE,
364
366
  convertMCPToolsToAISDK,
367
+ getCopilotMCPOAuthKey,
368
+ getStoredCopilotMCPToken,
365
369
  getMCPManager,
370
+ hasCopilotMCPScopes,
366
371
  initializeMCP,
372
+ isGitHubCopilotUrl,
373
+ isStoredCopilotMCPAuthenticated,
367
374
  shutdownMCP,
368
375
  loadMCPConfig,
369
376
  addMCPServerToConfig,
@@ -49,6 +49,25 @@ async function readIfExists(path: string): Promise<string | undefined> {
49
49
  return undefined;
50
50
  }
51
51
 
52
+ function getPromptOverridePaths(args: {
53
+ projectRoot: string;
54
+ provider: string;
55
+ modelId?: string;
56
+ }): { modelPaths: string[]; providerPaths: string[] } {
57
+ const { projectRoot, provider, modelId } = args;
58
+ const modelPaths: string[] = [];
59
+ const providerPaths: string[] = [];
60
+
61
+ if (modelId) {
62
+ const sanitized = sanitizeModelId(modelId);
63
+ modelPaths.push(`${projectRoot}/.otto/prompts/models/${sanitized}.txt`);
64
+ }
65
+
66
+ providerPaths.push(`${projectRoot}/.otto/prompts/providers/${provider}.txt`);
67
+
68
+ return { modelPaths, providerPaths };
69
+ }
70
+
52
71
  export type ProviderPromptResult = {
53
72
  prompt: string;
54
73
  resolvedType: string;
@@ -57,21 +76,37 @@ export type ProviderPromptResult = {
57
76
  export async function providerBasePrompt(
58
77
  provider: string,
59
78
  modelId: string | undefined,
60
- _projectRoot: string,
79
+ projectRoot: string,
61
80
  ): Promise<ProviderPromptResult> {
62
81
  const id = String(provider || '').toLowerCase();
82
+ const { modelPaths, providerPaths } = getPromptOverridePaths({
83
+ projectRoot,
84
+ provider: id,
85
+ modelId,
86
+ });
63
87
 
64
88
  if (modelId) {
65
89
  const sanitized = sanitizeModelId(modelId);
66
- const modelPath = `src/prompts/models/${sanitized}.txt`;
67
- const modelText = await readIfExists(modelPath);
68
- if (modelText) {
90
+ for (const modelPath of modelPaths) {
91
+ const modelText = await readIfExists(modelPath);
92
+ if (!modelText) continue;
69
93
  const promptType = `model:${sanitized}`;
70
- debugLog(`[provider] prompt: ${promptType} (${modelText.length} chars)`);
94
+ debugLog(
95
+ `[provider] prompt: ${promptType} (${modelText.length} chars) from ${modelPath}`,
96
+ );
71
97
  return { prompt: modelText, resolvedType: promptType };
72
98
  }
73
99
  }
74
100
 
101
+ for (const providerPath of providerPaths) {
102
+ const providerText = await readIfExists(providerPath);
103
+ if (!providerText) continue;
104
+ debugLog(
105
+ `[provider] prompt: custom:${id} (${providerText.length} chars) from ${providerPath}`,
106
+ );
107
+ return { prompt: providerText, resolvedType: `custom:${id}` };
108
+ }
109
+
75
110
  if (isProviderId(id) && modelId) {
76
111
  const info = getModelInfo(id, modelId);
77
112
  if (info?.ownedBy) {
@@ -119,13 +154,6 @@ export async function providerBasePrompt(
119
154
  return { prompt: result, resolvedType: 'glm' };
120
155
  }
121
156
 
122
- const providerPath = `src/prompts/providers/${id}.txt`;
123
- const providerText = await readIfExists(providerPath);
124
- if (providerText) {
125
- debugLog(`[provider] prompt: custom:${id} (${providerText.length} chars)`);
126
- return { prompt: providerText, resolvedType: `custom:${id}` };
127
- }
128
-
129
157
  const result = PROVIDER_DEFAULT.trim();
130
158
  debugLog(`[provider] prompt: default (${result.length} chars)`);
131
159
  return { prompt: result, resolvedType: 'default' };
@@ -14,6 +14,7 @@ const OWNER_NPM: Record<ModelOwner, string> = {
14
14
  openai: '@ai-sdk/openai',
15
15
  anthropic: '@ai-sdk/anthropic',
16
16
  google: '@ai-sdk/google',
17
+ openrouter: '@openrouter/ai-sdk-provider',
17
18
  xai: '@ai-sdk/xai',
18
19
  moonshot: '@ai-sdk/openai-compatible',
19
20
  zai: '@ai-sdk/openai-compatible',
@@ -762,7 +762,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
762
762
  ownedBy: 'openai',
763
763
  label: 'GPT-5.4',
764
764
  modalities: {
765
- input: ['text', 'image'],
765
+ input: ['text', 'image', 'pdf'],
766
766
  output: ['text'],
767
767
  },
768
768
  toolCall: true,
@@ -2323,6 +2323,57 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
2323
2323
  output: 64000,
2324
2324
  },
2325
2325
  },
2326
+ {
2327
+ id: 'gemini-3.1-flash-image-preview',
2328
+ ownedBy: 'google',
2329
+ label: 'Gemini 3.1 Flash Image (Preview)',
2330
+ modalities: {
2331
+ input: ['text', 'image', 'pdf'],
2332
+ output: ['text', 'image'],
2333
+ },
2334
+ toolCall: false,
2335
+ reasoningText: true,
2336
+ attachment: true,
2337
+ temperature: true,
2338
+ knowledge: '2025-01',
2339
+ releaseDate: '2026-02-26',
2340
+ lastUpdated: '2026-02-26',
2341
+ openWeights: false,
2342
+ cost: {
2343
+ input: 0.25,
2344
+ output: 60,
2345
+ },
2346
+ limit: {
2347
+ context: 131072,
2348
+ output: 32768,
2349
+ },
2350
+ },
2351
+ {
2352
+ id: 'gemini-3.1-flash-lite-preview',
2353
+ ownedBy: 'google',
2354
+ label: 'Gemini 3.1 Flash Lite Preview',
2355
+ modalities: {
2356
+ input: ['text', 'image', 'video', 'audio', 'pdf'],
2357
+ output: ['text'],
2358
+ },
2359
+ toolCall: true,
2360
+ reasoningText: true,
2361
+ attachment: true,
2362
+ temperature: true,
2363
+ knowledge: '2025-01',
2364
+ releaseDate: '2026-03-03',
2365
+ lastUpdated: '2026-03-03',
2366
+ openWeights: false,
2367
+ cost: {
2368
+ input: 0.5,
2369
+ output: 3,
2370
+ cacheRead: 0.05,
2371
+ },
2372
+ limit: {
2373
+ context: 1048576,
2374
+ output: 65536,
2375
+ },
2376
+ },
2326
2377
  {
2327
2378
  id: 'gemini-3.1-pro-preview',
2328
2379
  ownedBy: 'google',
@@ -3940,6 +3991,78 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
3940
3991
  output: 2000,
3941
3992
  },
3942
3993
  },
3994
+ {
3995
+ id: 'inception/mercury',
3996
+ label: 'Mercury',
3997
+ modalities: {
3998
+ input: ['text'],
3999
+ output: ['text'],
4000
+ },
4001
+ toolCall: true,
4002
+ reasoningText: false,
4003
+ attachment: false,
4004
+ temperature: true,
4005
+ releaseDate: '2025-06-26',
4006
+ lastUpdated: '2025-06-26',
4007
+ openWeights: false,
4008
+ cost: {
4009
+ input: 0.25,
4010
+ output: 0.75,
4011
+ cacheRead: 0.025,
4012
+ },
4013
+ limit: {
4014
+ context: 128000,
4015
+ output: 32000,
4016
+ },
4017
+ },
4018
+ {
4019
+ id: 'inception/mercury-2',
4020
+ label: 'Mercury 2',
4021
+ modalities: {
4022
+ input: ['text'],
4023
+ output: ['text'],
4024
+ },
4025
+ toolCall: true,
4026
+ reasoningText: true,
4027
+ attachment: false,
4028
+ temperature: true,
4029
+ releaseDate: '2026-03-04',
4030
+ lastUpdated: '2026-03-04',
4031
+ openWeights: false,
4032
+ cost: {
4033
+ input: 0.25,
4034
+ output: 0.75,
4035
+ cacheRead: 0.025,
4036
+ },
4037
+ limit: {
4038
+ context: 128000,
4039
+ output: 50000,
4040
+ },
4041
+ },
4042
+ {
4043
+ id: 'inception/mercury-coder',
4044
+ label: 'Mercury Coder',
4045
+ modalities: {
4046
+ input: ['text'],
4047
+ output: ['text'],
4048
+ },
4049
+ toolCall: true,
4050
+ reasoningText: false,
4051
+ attachment: false,
4052
+ temperature: true,
4053
+ releaseDate: '2025-04-30',
4054
+ lastUpdated: '2025-04-30',
4055
+ openWeights: false,
4056
+ cost: {
4057
+ input: 0.25,
4058
+ output: 0.75,
4059
+ cacheRead: 0.025,
4060
+ },
4061
+ limit: {
4062
+ context: 128000,
4063
+ output: 32000,
4064
+ },
4065
+ },
3943
4066
  {
3944
4067
  id: 'kwaipilot/kat-coder-pro:free',
3945
4068
  label: 'Kat Coder Pro (free)',
@@ -5743,6 +5866,77 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
5743
5866
  output: 50000,
5744
5867
  },
5745
5868
  },
5869
+ {
5870
+ id: 'openrouter/free',
5871
+ label: 'Free Models Router',
5872
+ modalities: {
5873
+ input: ['text', 'image'],
5874
+ output: ['text'],
5875
+ },
5876
+ toolCall: true,
5877
+ reasoningText: true,
5878
+ attachment: true,
5879
+ temperature: true,
5880
+ releaseDate: '2026-02-01',
5881
+ lastUpdated: '2026-02-01',
5882
+ openWeights: false,
5883
+ cost: {
5884
+ input: 0,
5885
+ output: 0,
5886
+ },
5887
+ limit: {
5888
+ context: 200000,
5889
+ output: 8000,
5890
+ },
5891
+ },
5892
+ {
5893
+ id: 'openrouter/healer-alpha',
5894
+ label: 'Healer Alpha',
5895
+ modalities: {
5896
+ input: ['text', 'image', 'audio', 'pdf'],
5897
+ output: ['text'],
5898
+ },
5899
+ toolCall: true,
5900
+ reasoningText: true,
5901
+ attachment: true,
5902
+ temperature: true,
5903
+ knowledge: '2026-03-11',
5904
+ releaseDate: '2026-03-11',
5905
+ lastUpdated: '2026-03-11',
5906
+ openWeights: false,
5907
+ cost: {
5908
+ input: 0,
5909
+ output: 0,
5910
+ },
5911
+ limit: {
5912
+ context: 262144,
5913
+ output: 64000,
5914
+ },
5915
+ },
5916
+ {
5917
+ id: 'openrouter/hunter-alpha',
5918
+ label: 'Hunter Alpha',
5919
+ modalities: {
5920
+ input: ['text', 'image', 'pdf'],
5921
+ output: ['text'],
5922
+ },
5923
+ toolCall: true,
5924
+ reasoningText: true,
5925
+ attachment: true,
5926
+ temperature: true,
5927
+ knowledge: '2026-03-11',
5928
+ releaseDate: '2026-03-11',
5929
+ lastUpdated: '2026-03-11',
5930
+ openWeights: false,
5931
+ cost: {
5932
+ input: 0,
5933
+ output: 0,
5934
+ },
5935
+ limit: {
5936
+ context: 1048576,
5937
+ output: 64000,
5938
+ },
5939
+ },
5746
5940
  {
5747
5941
  id: 'openrouter/sherlock-dash-alpha',
5748
5942
  label: 'Sherlock Dash Alpha',
@@ -8199,6 +8393,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
8199
8393
  output: 262144,
8200
8394
  },
8201
8395
  },
8396
+ {
8397
+ id: 'mimo-v2-flash-free',
8398
+ label: 'MiMo V2 Flash Free',
8399
+ modalities: {
8400
+ input: ['text'],
8401
+ output: ['text'],
8402
+ },
8403
+ toolCall: true,
8404
+ reasoningText: true,
8405
+ attachment: false,
8406
+ temperature: true,
8407
+ knowledge: '2024-12',
8408
+ releaseDate: '2025-12-16',
8409
+ lastUpdated: '2025-12-16',
8410
+ openWeights: true,
8411
+ cost: {
8412
+ input: 0,
8413
+ output: 0,
8414
+ cacheRead: 0,
8415
+ },
8416
+ limit: {
8417
+ context: 262144,
8418
+ output: 65536,
8419
+ },
8420
+ },
8202
8421
  {
8203
8422
  id: 'minimax-m2.1',
8204
8423
  ownedBy: 'minimax',
@@ -8309,6 +8528,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
8309
8528
  npm: '@ai-sdk/anthropic',
8310
8529
  },
8311
8530
  },
8531
+ {
8532
+ id: 'nemotron-3-super-free',
8533
+ label: 'Nemotron 3 Super Free',
8534
+ modalities: {
8535
+ input: ['text'],
8536
+ output: ['text'],
8537
+ },
8538
+ toolCall: true,
8539
+ reasoningText: true,
8540
+ attachment: false,
8541
+ temperature: true,
8542
+ knowledge: '2026-02',
8543
+ releaseDate: '2026-03-11',
8544
+ lastUpdated: '2026-03-11',
8545
+ openWeights: true,
8546
+ cost: {
8547
+ input: 0,
8548
+ output: 0,
8549
+ cacheRead: 0,
8550
+ },
8551
+ limit: {
8552
+ context: 1000000,
8553
+ output: 128000,
8554
+ },
8555
+ },
8312
8556
  {
8313
8557
  id: 'qwen3-coder',
8314
8558
  label: 'Qwen3 Coder',
@@ -8593,7 +8837,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
8593
8837
  temperature: true,
8594
8838
  releaseDate: '2026-02-11',
8595
8839
  lastUpdated: '2026-02-11',
8596
- openWeights: false,
8840
+ openWeights: true,
8597
8841
  cost: {
8598
8842
  input: 1,
8599
8843
  output: 3.2,
@@ -8868,7 +9112,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
8868
9112
  temperature: true,
8869
9113
  releaseDate: '2026-02-11',
8870
9114
  lastUpdated: '2026-02-11',
8871
- openWeights: false,
9115
+ openWeights: true,
8872
9116
  cost: {
8873
9117
  input: 0,
8874
9118
  output: 0,
@@ -9661,7 +9905,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
9661
9905
  output: 0,
9662
9906
  },
9663
9907
  limit: {
9664
- context: 128000,
9908
+ context: 264000,
9665
9909
  output: 64000,
9666
9910
  },
9667
9911
  },
@@ -9686,7 +9930,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
9686
9930
  output: 0,
9687
9931
  },
9688
9932
  limit: {
9689
- context: 272000,
9933
+ context: 400000,
9690
9934
  output: 128000,
9691
9935
  },
9692
9936
  },
@@ -4,6 +4,7 @@ import { getModelNpmBinding } from './utils.ts';
4
4
 
5
5
  export type OpenRouterProviderConfig = {
6
6
  apiKey?: string;
7
+ baseURL?: string;
7
8
  };
8
9
 
9
10
  function isAnthropicModel(model: string): boolean {
@@ -129,6 +129,7 @@ const OWNER_TO_FAMILY: Record<ModelOwner, UnderlyingProviderKey> = {
129
129
  openai: 'openai',
130
130
  anthropic: 'anthropic',
131
131
  google: 'google',
132
+ openrouter: 'openai-compatible',
132
133
  xai: 'openai',
133
134
  moonshot: 'moonshot',
134
135
  zai: 'glm',
package/src/tunnel/qr.ts CHANGED
@@ -1,4 +1,3 @@
1
- // @ts-expect-error No bundled types for qrcode-terminal in all workspace builds
2
1
  import qrcode from 'qrcode-terminal';
3
2
 
4
3
  export function generateQRCode(data: string): Promise<string> {
@@ -9,6 +9,13 @@ export type Scope = 'global' | 'local';
9
9
  * Default settings for the CLI
10
10
  */
11
11
  export type ToolApprovalMode = 'auto' | 'dangerous' | 'all';
12
+ export type ReasoningLevel =
13
+ | 'minimal'
14
+ | 'low'
15
+ | 'medium'
16
+ | 'high'
17
+ | 'max'
18
+ | 'xhigh';
12
19
 
13
20
  export type DefaultConfig = {
14
21
  agent: string;
@@ -17,7 +24,9 @@ export type DefaultConfig = {
17
24
  toolApproval?: ToolApprovalMode;
18
25
  guidedMode?: boolean;
19
26
  reasoningText?: boolean;
27
+ reasoningLevel?: ReasoningLevel;
20
28
  theme?: string;
29
+ fullWidthContent?: boolean;
21
30
  };
22
31
 
23
32
  export type ProviderSettings = Record<
@@ -19,4 +19,5 @@ export type {
19
19
  ProviderSettings,
20
20
  OttoConfig,
21
21
  ToolApprovalMode,
22
+ ReasoningLevel,
22
23
  } from './config';
@@ -33,6 +33,7 @@ export type ModelOwner =
33
33
  | 'openai'
34
34
  | 'anthropic'
35
35
  | 'google'
36
+ | 'openrouter'
36
37
  | 'xai'
37
38
  | 'moonshot'
38
39
  | 'zai'