@ottocode/sdk 0.1.204 → 0.1.206

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.204",
3
+ "version": "0.1.206",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@ export type { ProviderId, ModelInfo } from '../../types/src/index.ts';
30
30
  // Tools
31
31
  // =======================
32
32
  export { discoverProjectTools } from './tools/loader';
33
- export type { DiscoveredTool } from './tools/loader';
33
+ export type { DiscoveredTool, DiscoverResult } from './tools/loader';
34
34
  export { setTerminalManager, getTerminalManager } from './tools/loader';
35
35
 
36
36
  // Tool error handling utilities
@@ -166,15 +166,18 @@ export class MCPClientWrapper {
166
166
  args: Record<string, unknown>,
167
167
  ): Promise<unknown> {
168
168
  const result = await this.client.callTool({ name, arguments: args });
169
+ const images = extractImages(result.content);
169
170
  if (result.isError) {
170
171
  return {
171
172
  ok: false,
172
173
  error: formatContent(result.content),
174
+ ...(images.length > 0 && { images }),
173
175
  };
174
176
  }
175
177
  return {
176
178
  ok: true,
177
179
  result: formatContent(result.content),
180
+ ...(images.length > 0 && { images }),
178
181
  };
179
182
  }
180
183
 
@@ -218,12 +221,34 @@ function formatContent(content: unknown): string {
218
221
  if (item && typeof item === 'object' && 'text' in item) {
219
222
  parts.push(String(item.text));
220
223
  } else if (item && typeof item === 'object' && 'data' in item) {
221
- parts.push(
222
- `[binary data: ${(item as { mimeType?: string }).mimeType ?? 'unknown'}]`,
223
- );
224
+ const mimeType = (item as { mimeType?: string }).mimeType ?? 'unknown';
225
+ if (mimeType.startsWith('image/')) {
226
+ parts.push(`[image: ${mimeType}]`);
227
+ } else {
228
+ parts.push(`[binary data: ${mimeType}]`);
229
+ }
224
230
  } else {
225
231
  parts.push(JSON.stringify(item));
226
232
  }
227
233
  }
228
234
  return parts.join('\n');
229
235
  }
236
+
237
+ function extractImages(
238
+ content: unknown,
239
+ ): Array<{ data: string; mimeType: string }> {
240
+ if (!Array.isArray(content)) return [];
241
+ const images: Array<{ data: string; mimeType: string }> = [];
242
+ for (const item of content) {
243
+ if (item && typeof item === 'object' && 'data' in item) {
244
+ const mimeType = (item as { mimeType?: string }).mimeType ?? 'unknown';
245
+ if (mimeType.startsWith('image/')) {
246
+ images.push({
247
+ data: String((item as { data: unknown }).data),
248
+ mimeType,
249
+ });
250
+ }
251
+ }
252
+ }
253
+ return images;
254
+ }
@@ -13,6 +13,14 @@ export { MCPServerManager } from './server-manager.ts';
13
13
 
14
14
  export { convertMCPToolsToAISDK } from './tools.ts';
15
15
 
16
+ export {
17
+ getMCPToolBriefs,
18
+ buildLoadMCPToolsTool,
19
+ getMCPToolsRecord,
20
+ buildMCPToolCatalogDescription,
21
+ type MCPToolBrief,
22
+ } from './lazy-tools.ts';
23
+
16
24
  export {
17
25
  getMCPManager,
18
26
  initializeMCP,
@@ -0,0 +1,89 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import type { MCPServerManager } from './server-manager.ts';
4
+ import { convertMCPToolsToAISDK } from './tools.ts';
5
+
6
+ export type MCPToolBrief = {
7
+ name: string;
8
+ server: string;
9
+ description: string;
10
+ };
11
+
12
+ export function getMCPToolBriefs(manager: MCPServerManager): MCPToolBrief[] {
13
+ return manager.getTools().map(({ name, server, tool: t }) => ({
14
+ name,
15
+ server,
16
+ description: t.description ?? `MCP tool: ${t.name}`,
17
+ }));
18
+ }
19
+
20
+ export function buildMCPToolCatalogDescription(briefs: MCPToolBrief[]): string {
21
+ if (briefs.length === 0) return 'No MCP tools available.';
22
+ const grouped = new Map<string, MCPToolBrief[]>();
23
+ for (const b of briefs) {
24
+ const list = grouped.get(b.server) ?? [];
25
+ list.push(b);
26
+ grouped.set(b.server, list);
27
+ }
28
+ const lines: string[] = [];
29
+ for (const [server, tools] of grouped) {
30
+ lines.push(`[${server}]`);
31
+ for (const t of tools) {
32
+ lines.push(` ${t.name}: ${t.description.slice(0, 120)}`);
33
+ }
34
+ }
35
+ return lines.join('\n');
36
+ }
37
+
38
+ export function buildLoadMCPToolsTool(briefs: MCPToolBrief[]): {
39
+ name: string;
40
+ tool: Tool;
41
+ } {
42
+ const catalog = buildMCPToolCatalogDescription(briefs);
43
+ const validNames = new Set(briefs.map((b) => b.name));
44
+
45
+ return {
46
+ name: 'load_mcp_tools',
47
+ tool: tool({
48
+ description: `Load MCP tools by name so they become available for use in the next step. Call this with the tool names you need before using them.\n\nAvailable MCP tools:\n${catalog}`,
49
+ inputSchema: z.object({
50
+ tools: z
51
+ .array(z.string())
52
+ .describe(
53
+ 'Array of MCP tool names to load (e.g. ["chrome__click", "chrome__screenshot"])',
54
+ ),
55
+ }),
56
+ execute: async ({ tools: requested }) => {
57
+ const loaded: string[] = [];
58
+ const notFound: string[] = [];
59
+ for (const name of requested) {
60
+ if (validNames.has(name)) {
61
+ loaded.push(name);
62
+ } else {
63
+ notFound.push(name);
64
+ }
65
+ }
66
+ return {
67
+ ok: true,
68
+ loaded,
69
+ ...(notFound.length > 0 ? { notFound } : {}),
70
+ message:
71
+ loaded.length > 0
72
+ ? `Loaded ${loaded.length} tool(s). They are now available for use.`
73
+ : 'No valid tools to load.',
74
+ };
75
+ },
76
+ }),
77
+ };
78
+ }
79
+
80
+ export function getMCPToolsRecord(
81
+ manager: MCPServerManager,
82
+ ): Record<string, Tool> {
83
+ const mcpTools = convertMCPToolsToAISDK(manager);
84
+ const record: Record<string, Tool> = {};
85
+ for (const { name, tool: t } of mcpTools) {
86
+ record[name] = t;
87
+ }
88
+ return record;
89
+ }
@@ -220,6 +220,7 @@ export class MCPServerManager {
220
220
 
221
221
  this.serverScopes.set(config.name, config.scope ?? 'global');
222
222
  const key = this.oauthKey(config.name);
223
+ await this.oauthStore.clearServer(key);
223
224
  const provider = new OttoOAuthProvider(key, this.oauthStore, {
224
225
  clientId: config.oauth?.clientId,
225
226
  callbackPort: config.oauth?.callbackPort,
@@ -244,6 +245,7 @@ export class MCPServerManager {
244
245
 
245
246
  if (provider.pendingAuthUrl) {
246
247
  this.pendingAuth.set(config.name, provider.pendingAuthUrl);
248
+ this.waitForAuthAndReconnect(config.name, provider);
247
249
  return provider.pendingAuthUrl;
248
250
  }
249
251
  return null;
@@ -295,6 +297,30 @@ export class MCPServerManager {
295
297
  await this.stopServer(name);
296
298
  }
297
299
 
300
+ async clearAuthData(
301
+ name: string,
302
+ scope?: 'global' | 'project',
303
+ projectRoot?: string,
304
+ ): Promise<void> {
305
+ const provider = this.authProviders.get(name);
306
+ if (provider) {
307
+ await provider.clearCredentials();
308
+ provider.cleanup();
309
+ }
310
+ this.authProviders.delete(name);
311
+ if (scope) {
312
+ this.serverScopes.set(name, scope);
313
+ }
314
+ if (projectRoot) {
315
+ this.projectRoot = projectRoot;
316
+ }
317
+ const key = this.oauthKey(name);
318
+ await this.oauthStore.clearServer(key);
319
+ if (key !== name) {
320
+ await this.oauthStore.clearServer(name);
321
+ }
322
+ }
323
+
298
324
  async getAuthStatus(
299
325
  name: string,
300
326
  ): Promise<{ authenticated: boolean; expiresAt?: number }> {
@@ -1,7 +1,15 @@
1
1
  import { tool, type Tool } from 'ai';
2
+ import type { ToolResultOutput } from '@ai-sdk/provider-utils';
2
3
  import { z } from 'zod/v3';
3
4
  import type { MCPServerManager } from './server-manager.ts';
4
5
 
6
+ type MCPToolResult = {
7
+ ok: boolean;
8
+ result?: string;
9
+ error?: string;
10
+ images?: Array<{ data: string; mimeType: string }>;
11
+ };
12
+
5
13
  export function convertMCPToolsToAISDK(
6
14
  manager: MCPServerManager,
7
15
  ): Array<{ name: string; tool: Tool }> {
@@ -16,13 +24,38 @@ export function convertMCPToolsToAISDK(
16
24
  ) as z.ZodObject<z.ZodRawShape>,
17
25
  async execute(args: Record<string, unknown>) {
18
26
  try {
19
- return await manager.callTool(name, args);
27
+ return (await manager.callTool(name, args)) as MCPToolResult;
20
28
  } catch (err) {
21
29
  return {
22
30
  ok: false,
23
31
  error: err instanceof Error ? err.message : String(err),
24
- };
32
+ } satisfies MCPToolResult;
33
+ }
34
+ },
35
+ toModelOutput({ output }): ToolResultOutput {
36
+ const result = output as MCPToolResult;
37
+ if (result.images && result.images.length > 0) {
38
+ const parts: Array<
39
+ | { type: 'text'; text: string }
40
+ | { type: 'image-data'; data: string; mediaType: string }
41
+ > = [];
42
+ const text = result.ok ? result.result : result.error;
43
+ if (text) {
44
+ parts.push({ type: 'text', text });
45
+ }
46
+ for (const img of result.images) {
47
+ parts.push({
48
+ type: 'image-data',
49
+ data: img.data,
50
+ mediaType: img.mimeType,
51
+ });
52
+ }
53
+ return { type: 'content', value: parts } as ToolResultOutput;
25
54
  }
55
+ return {
56
+ type: 'json',
57
+ value: result as unknown as import('@ai-sdk/provider').JSONValue,
58
+ };
26
59
  },
27
60
  }),
28
61
  }));
@@ -16,7 +16,12 @@ import { buildTerminalTool } from './builtin/terminal.ts';
16
16
  import type { TerminalManager } from '../terminals/index.ts';
17
17
  import { initializeSkills, buildSkillTool } from '../../../skills/index.ts';
18
18
  import { getMCPManager } from '../mcp/index.ts';
19
- import { convertMCPToolsToAISDK } from '../mcp/tools.ts';
19
+ import {
20
+ getMCPToolBriefs,
21
+ buildLoadMCPToolsTool,
22
+ getMCPToolsRecord,
23
+ type MCPToolBrief,
24
+ } from '../mcp/lazy-tools.ts';
20
25
  import fg from 'fast-glob';
21
26
  import { dirname, isAbsolute, join } from 'node:path';
22
27
  import { pathToFileURL } from 'node:url';
@@ -25,6 +30,11 @@ import { spawn as nodeSpawn } from 'node:child_process';
25
30
 
26
31
  export type DiscoveredTool = { name: string; tool: Tool };
27
32
 
33
+ export type DiscoverResult = {
34
+ tools: DiscoveredTool[];
35
+ mcpToolsRecord: Record<string, Tool>;
36
+ };
37
+
28
38
  type PluginParameter = {
29
39
  type: 'string' | 'number' | 'boolean';
30
40
  description?: string;
@@ -108,7 +118,7 @@ export function getTerminalManager(): TerminalManager | null {
108
118
  export async function discoverProjectTools(
109
119
  projectRoot: string,
110
120
  globalConfigDir?: string,
111
- ): Promise<DiscoveredTool[]> {
121
+ ): Promise<DiscoverResult> {
112
122
  const tools = new Map<string, Tool>();
113
123
  for (const { name, tool } of buildFsTools(projectRoot)) tools.set(name, tool);
114
124
  for (const { name, tool } of buildGitTools(projectRoot))
@@ -148,10 +158,14 @@ export async function discoverProjectTools(
148
158
  tools.set(skillTool.name, skillTool.tool);
149
159
 
150
160
  const mcpManager = getMCPManager();
161
+ let mcpToolsRecord: Record<string, Tool> = {};
162
+ let mcpBriefs: MCPToolBrief[] = [];
151
163
  if (mcpManager?.started) {
152
- const mcpTools = convertMCPToolsToAISDK(mcpManager);
153
- for (const { name, tool } of mcpTools) {
154
- tools.set(name, tool);
164
+ mcpBriefs = getMCPToolBriefs(mcpManager);
165
+ if (mcpBriefs.length > 0) {
166
+ mcpToolsRecord = getMCPToolsRecord(mcpManager);
167
+ const loadTool = buildLoadMCPToolsTool(mcpBriefs);
168
+ tools.set(loadTool.name, loadTool.tool);
155
169
  }
156
170
  }
157
171
 
@@ -210,7 +224,10 @@ export async function discoverProjectTools(
210
224
 
211
225
  await loadFromBase(globalConfigDir);
212
226
  await loadFromBase(join(projectRoot, '.otto'));
213
- return Array.from(tools.entries()).map(([name, tool]) => ({ name, tool }));
227
+ return {
228
+ tools: Array.from(tools.entries()).map(([name, tool]) => ({ name, tool })),
229
+ mcpToolsRecord,
230
+ };
214
231
  }
215
232
 
216
233
  async function loadPlugin(
package/src/index.ts CHANGED
@@ -218,7 +218,7 @@ export type { ProviderName, ModelConfig } from './core/src/index.ts';
218
218
 
219
219
  // Tools
220
220
  export { discoverProjectTools } from './core/src/index.ts';
221
- export type { DiscoveredTool } from './core/src/index.ts';
221
+ export type { DiscoveredTool, DiscoverResult } from './core/src/index.ts';
222
222
  export { setTerminalManager, getTerminalManager } from './core/src/index.ts';
223
223
  export { buildFsTools } from './core/src/index.ts';
224
224
  export { buildGitTools } from './core/src/index.ts';
@@ -78,6 +78,7 @@ export type SetuProviderOptions = {
78
78
  promptCacheKey?: string;
79
79
  promptCacheRetention?: 'in_memory' | '24h';
80
80
  topupApprovalMode?: 'auto' | 'approval';
81
+ autoPayThresholdUsd?: number;
81
82
  };
82
83
 
83
84
  export type SetuAuth = {
@@ -163,6 +164,7 @@ export function createSetuFetch(
163
164
  const promptCacheKey = options.promptCacheKey;
164
165
  const promptCacheRetention = options.promptCacheRetention;
165
166
  const topupApprovalMode = options.topupApprovalMode ?? 'auto';
167
+ const autoPayThresholdUsd = options.autoPayThresholdUsd ?? 0;
166
168
 
167
169
  const baseFetch = globalThis.fetch.bind(globalThis);
168
170
 
@@ -232,39 +234,62 @@ export function createSetuFetch(
232
234
  const amountUsd =
233
235
  parseInt(requirement.maxAmountRequired, 10) / 1_000_000;
234
236
 
235
- if (topupApprovalMode === 'approval' && callbacks.onPaymentApproval) {
237
+ let walletUsdcBalance = 0;
238
+ if (autoPayThresholdUsd > 0) {
239
+ walletUsdcBalance = await getWalletUsdcBalance(walletAddress, rpcURL);
240
+ }
241
+
242
+ const canAutoPay =
243
+ autoPayThresholdUsd > 0 && walletUsdcBalance >= autoPayThresholdUsd;
244
+
245
+ const requestApproval = async () => {
246
+ if (!callbacks.onPaymentApproval) return;
236
247
  const approval = await callbacks.onPaymentApproval({
237
248
  amountUsd,
238
- currentBalance: 0,
249
+ currentBalance: walletUsdcBalance,
239
250
  });
240
-
241
251
  if (approval === 'cancel') {
242
252
  callbacks.onPaymentError?.('Payment cancelled by user');
243
253
  throw new Error('Setu: payment cancelled by user');
244
254
  }
245
-
246
255
  if (approval === 'fiat') {
247
256
  const err = new Error('Setu: fiat payment selected');
248
257
  (err as Error & { code: string }).code = 'SETU_FIAT_SELECTED';
249
258
  throw err;
250
259
  }
260
+ };
261
+
262
+ if (!canAutoPay && topupApprovalMode === 'approval') {
263
+ await requestApproval();
251
264
  }
252
265
 
253
- callbacks.onPaymentRequired?.(amountUsd, 0);
254
-
255
- const outcome = await handlePayment({
256
- requirement,
257
- keypair,
258
- rpcURL,
259
- baseURL,
260
- baseFetch,
261
- buildWalletHeaders,
262
- maxAttempts: remainingPayments,
263
- callbacks,
264
- });
265
-
266
- const newTotal = currentAttempts + outcome.attemptsUsed;
267
- globalPaymentAttempts.set(walletAddress, newTotal);
266
+ callbacks.onPaymentRequired?.(amountUsd, walletUsdcBalance);
267
+
268
+ const doPayment = async () => {
269
+ const outcome = await handlePayment({
270
+ requirement,
271
+ keypair,
272
+ rpcURL,
273
+ baseURL,
274
+ baseFetch,
275
+ buildWalletHeaders,
276
+ maxAttempts: remainingPayments,
277
+ callbacks,
278
+ });
279
+ const newTotal = currentAttempts + outcome.attemptsUsed;
280
+ globalPaymentAttempts.set(walletAddress, newTotal);
281
+ };
282
+
283
+ if (canAutoPay) {
284
+ try {
285
+ await doPayment();
286
+ } catch (_autoPayErr) {
287
+ await requestApproval();
288
+ await doPayment();
289
+ }
290
+ } else {
291
+ await doPayment();
292
+ }
268
293
  } finally {
269
294
  releaseLock();
270
295
  }
@@ -638,6 +663,48 @@ export function getPublicKeyFromPrivate(privateKey: string): string | null {
638
663
  }
639
664
  }
640
665
 
666
+ async function getWalletUsdcBalance(
667
+ walletAddress: string,
668
+ rpcUrl: string,
669
+ ): Promise<number> {
670
+ try {
671
+ const usdcMint = rpcUrl.includes('devnet')
672
+ ? USDC_MINT_DEVNET
673
+ : USDC_MINT_MAINNET;
674
+ const response = await fetch(rpcUrl, {
675
+ method: 'POST',
676
+ headers: { 'Content-Type': 'application/json' },
677
+ body: JSON.stringify({
678
+ jsonrpc: '2.0',
679
+ id: 1,
680
+ method: 'getTokenAccountsByOwner',
681
+ params: [walletAddress, { mint: usdcMint }, { encoding: 'jsonParsed' }],
682
+ }),
683
+ });
684
+ if (!response.ok) return 0;
685
+ const data = (await response.json()) as {
686
+ result?: {
687
+ value?: Array<{
688
+ account: {
689
+ data: {
690
+ parsed: {
691
+ info: { tokenAmount: { uiAmount: number } };
692
+ };
693
+ };
694
+ };
695
+ }>;
696
+ };
697
+ };
698
+ let total = 0;
699
+ for (const acct of data.result?.value ?? []) {
700
+ total += acct.account.data.parsed.info.tokenAmount.uiAmount ?? 0;
701
+ }
702
+ return total;
703
+ } catch {
704
+ return 0;
705
+ }
706
+ }
707
+
641
708
  const USDC_MINT_MAINNET = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
642
709
  const USDC_MINT_DEVNET = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
643
710