@ottocode/sdk 0.1.245 → 0.1.247

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 (40) hide show
  1. package/package.json +7 -2
  2. package/src/config/src/index.ts +5 -0
  3. package/src/config/src/manager.ts +106 -30
  4. package/src/core/src/providers/resolver.ts +28 -1
  5. package/src/core/src/tools/builtin/bash.ts +1 -266
  6. package/src/core/src/tools/builtin/fs/edit-shared.ts +1 -1
  7. package/src/core/src/tools/builtin/fs/edit.txt +2 -2
  8. package/src/core/src/tools/builtin/fs/write.txt +1 -1
  9. package/src/core/src/tools/builtin/shell.ts +273 -0
  10. package/src/core/src/tools/builtin/shell.txt +13 -0
  11. package/src/core/src/tools/builtin/terminal.txt +9 -6
  12. package/src/core/src/tools/loader.ts +134 -82
  13. package/src/index.ts +33 -0
  14. package/src/prompts/src/agents/build.txt +5 -6
  15. package/src/prompts/src/modes/guided.txt +2 -2
  16. package/src/prompts/src/providers/anthropic.txt +2 -2
  17. package/src/prompts/src/providers/default.txt +2 -2
  18. package/src/prompts/src/providers/glm.txt +2 -2
  19. package/src/prompts/src/providers/google.txt +9 -9
  20. package/src/prompts/src/providers/moonshot.txt +2 -2
  21. package/src/prompts/src/providers/openai.txt +3 -3
  22. package/src/prompts/src/providers.ts +15 -0
  23. package/src/providers/src/authorization.ts +26 -1
  24. package/src/providers/src/catalog-manual.ts +21 -6
  25. package/src/providers/src/catalog-merged.ts +2 -2
  26. package/src/providers/src/catalog.ts +10462 -10283
  27. package/src/providers/src/env.ts +10 -5
  28. package/src/providers/src/index.ts +26 -0
  29. package/src/providers/src/oauth-models.ts +1 -0
  30. package/src/providers/src/ollama-discovery.ts +149 -0
  31. package/src/providers/src/pricing.ts +3 -0
  32. package/src/providers/src/registry.ts +258 -0
  33. package/src/providers/src/utils.ts +10 -3
  34. package/src/providers/src/validate.ts +63 -2
  35. package/src/skills/index.ts +3 -0
  36. package/src/skills/tool.ts +28 -36
  37. package/src/types/src/config.ts +34 -8
  38. package/src/types/src/index.ts +4 -0
  39. package/src/types/src/provider.ts +33 -3
  40. package/src/core/src/tools/builtin/bash.txt +0 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.245",
3
+ "version": "0.1.247",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -37,6 +37,10 @@
37
37
  "import": "./src/core/src/tools/builtin/bash.ts",
38
38
  "types": "./src/core/src/tools/builtin/bash.ts"
39
39
  },
40
+ "./tools/builtin/shell": {
41
+ "import": "./src/core/src/tools/builtin/shell.ts",
42
+ "types": "./src/core/src/tools/builtin/shell.ts"
43
+ },
40
44
  "./tools/builtin/finish": {
41
45
  "import": "./src/core/src/tools/builtin/finish.ts",
42
46
  "types": "./src/core/src/tools/builtin/finish.ts"
@@ -97,9 +101,10 @@
97
101
  "@modelcontextprotocol/sdk": "^1.12",
98
102
  "@openauthjs/openauth": "^0.4.3",
99
103
  "@openrouter/ai-sdk-provider": "^1.2.0",
100
- "@ottorouter/ai-sdk": "0.2.0",
104
+ "@ottorouter/ai-sdk": "0.2.1",
101
105
  "@solana/web3.js": "^1.98.0",
102
106
  "ai": "^6.0.0",
107
+ "ai-sdk-ollama": "^3.8.3",
103
108
  "bs58": "^6.0.0",
104
109
  "bun-pty": "^0.3.2",
105
110
  "diff": "^8.0.2",
@@ -13,6 +13,7 @@ const DEFAULT_PROVIDER_SETTINGS: OttoConfig['providers'] = {
13
13
  openai: { enabled: false },
14
14
  anthropic: { enabled: false },
15
15
  google: { enabled: false },
16
+ 'ollama-cloud': { enabled: false, baseURL: 'https://ollama.com' },
16
17
  openrouter: { enabled: false },
17
18
  opencode: { enabled: false },
18
19
  copilot: { enabled: false },
@@ -64,6 +65,7 @@ export async function loadConfig(
64
65
  projectRoot,
65
66
  defaults: merged.defaults as OttoConfig['defaults'],
66
67
  providers: merged.providers as OttoConfig['providers'],
68
+ skills: merged.skills as OttoConfig['skills'],
67
69
  paths: {
68
70
  dataDir,
69
71
  dbPath,
@@ -129,6 +131,9 @@ export {
129
131
  isAuthorized,
130
132
  ensureEnv,
131
133
  writeDefaults,
134
+ writeProviderSettings,
135
+ removeProviderSettings,
136
+ writeSkillSettings,
132
137
  writeAuth,
133
138
  removeAuth,
134
139
  } from './manager.ts';
@@ -6,6 +6,10 @@ import {
6
6
  type ProviderId,
7
7
  type AuthInfo,
8
8
  } from '../../auth/src/index.ts';
9
+ import type {
10
+ ProviderSettingsEntry,
11
+ SkillSettings,
12
+ } from '../../types/src/index.ts';
9
13
  import {
10
14
  getGlobalConfigDir,
11
15
  getGlobalConfigPath,
@@ -83,30 +87,8 @@ export async function writeDefaults(
83
87
  }>,
84
88
  projectRoot?: string,
85
89
  ) {
86
- const root = projectRoot ? String(projectRoot) : process.cwd();
87
-
88
- if (scope === 'local') {
89
- const localDir = getLocalDataDir(root);
90
- const localPath = joinPath(localDir, 'config.json');
91
- const existing = await readJsonFile(localPath);
92
- const prevDefaults =
93
- existing && typeof existing.defaults === 'object'
94
- ? (existing.defaults as Record<string, unknown>)
95
- : {};
96
- const next = {
97
- ...existing,
98
- defaults: { ...prevDefaults, ...updates },
99
- };
100
- try {
101
- const { promises: fs } = await import('node:fs');
102
- await fs.mkdir(localDir, { recursive: true }).catch(() => {});
103
- } catch {}
104
- await Bun.write(localPath, JSON.stringify(next, null, 2));
105
- return;
106
- }
107
-
108
- const globalPath = getGlobalConfigPath();
109
- const existing = await readJsonFile(globalPath);
90
+ const filePath = getConfigFilePath(scope, projectRoot);
91
+ const existing = await readJsonFile(filePath);
110
92
  const prevDefaults =
111
93
  existing && typeof existing.defaults === 'object'
112
94
  ? (existing.defaults as Record<string, unknown>)
@@ -115,12 +97,82 @@ export async function writeDefaults(
115
97
  ...existing,
116
98
  defaults: { ...prevDefaults, ...updates },
117
99
  };
118
- const base = getGlobalConfigDir();
119
- try {
120
- const { promises: fs } = await import('node:fs');
121
- await fs.mkdir(base, { recursive: true }).catch(() => {});
122
- } catch {}
123
- await Bun.write(globalPath, JSON.stringify(next, null, 2));
100
+ await writeConfigFile(filePath, next);
101
+ }
102
+
103
+ /**
104
+ * Persist provider settings for a built-in or custom provider entry.
105
+ */
106
+ export async function writeProviderSettings(
107
+ scope: Scope,
108
+ provider: string,
109
+ updates: ProviderSettingsEntry,
110
+ projectRoot?: string,
111
+ ) {
112
+ const filePath = getConfigFilePath(scope, projectRoot);
113
+ const existing = await readJsonFile(filePath);
114
+ const prevProviders =
115
+ existing && typeof existing.providers === 'object'
116
+ ? (existing.providers as Record<string, unknown>)
117
+ : {};
118
+ const previousEntry =
119
+ prevProviders[provider] && typeof prevProviders[provider] === 'object'
120
+ ? (prevProviders[provider] as Record<string, unknown>)
121
+ : {};
122
+ const next = {
123
+ ...existing,
124
+ providers: {
125
+ ...prevProviders,
126
+ [provider]: { ...previousEntry, ...updates },
127
+ },
128
+ };
129
+ await writeConfigFile(filePath, next);
130
+ }
131
+
132
+ /**
133
+ * Remove a provider override or custom provider entry from config.
134
+ */
135
+ export async function removeProviderSettings(
136
+ scope: Scope,
137
+ provider: string,
138
+ projectRoot?: string,
139
+ ) {
140
+ const filePath = getConfigFilePath(scope, projectRoot);
141
+ const existing = await readJsonFile(filePath);
142
+ if (!existing || typeof existing.providers !== 'object') return;
143
+ const providers = { ...(existing.providers as Record<string, unknown>) };
144
+ delete providers[provider];
145
+ const next = { ...existing, providers };
146
+ await writeConfigFile(filePath, next);
147
+ }
148
+
149
+ export async function writeSkillSettings(
150
+ scope: Scope,
151
+ updates: SkillSettings,
152
+ projectRoot?: string,
153
+ ) {
154
+ const filePath = getConfigFilePath(scope, projectRoot);
155
+ const existing = await readJsonFile(filePath);
156
+ const prevSkills =
157
+ existing && typeof existing.skills === 'object'
158
+ ? (existing.skills as Record<string, unknown>)
159
+ : {};
160
+ const prevItems =
161
+ prevSkills.items && typeof prevSkills.items === 'object'
162
+ ? (prevSkills.items as Record<string, unknown>)
163
+ : {};
164
+ const next = {
165
+ ...existing,
166
+ skills: {
167
+ ...prevSkills,
168
+ ...updates,
169
+ items: {
170
+ ...prevItems,
171
+ ...(updates.items ?? {}),
172
+ },
173
+ },
174
+ };
175
+ await writeConfigFile(filePath, next);
124
176
  }
125
177
 
126
178
  export async function readDebugConfig(
@@ -183,6 +235,30 @@ async function readJsonFile(
183
235
  }
184
236
  }
185
237
 
238
+ function getConfigFilePath(scope: Scope, projectRoot?: string): string {
239
+ const root = projectRoot ? String(projectRoot) : process.cwd();
240
+ if (scope === 'local') {
241
+ const localDir = getLocalDataDir(root);
242
+ return joinPath(localDir, 'config.json');
243
+ }
244
+ return getGlobalConfigPath();
245
+ }
246
+
247
+ async function writeConfigFile(
248
+ filePath: string,
249
+ value: Record<string, unknown>,
250
+ ) {
251
+ const base =
252
+ filePath === getGlobalConfigPath()
253
+ ? getGlobalConfigDir()
254
+ : filePath.slice(0, Math.max(0, filePath.lastIndexOf('/')));
255
+ try {
256
+ const { promises: fs } = await import('node:fs');
257
+ await fs.mkdir(base, { recursive: true }).catch(() => {});
258
+ } catch {}
259
+ await Bun.write(filePath, JSON.stringify(value, null, 2));
260
+ }
261
+
186
262
  export async function writeAuth(
187
263
  provider: ProviderId,
188
264
  info: AuthInfo,
@@ -1,12 +1,14 @@
1
1
  import { openai, createOpenAI } from '@ai-sdk/openai';
2
2
  import { anthropic, createAnthropic } from '@ai-sdk/anthropic';
3
3
  import { google, createGoogleGenerativeAI } from '@ai-sdk/google';
4
+ import { createOllama } from 'ai-sdk-ollama';
4
5
  import { createOpenRouter } from '@openrouter/ai-sdk-provider';
5
6
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
6
7
  import {
7
8
  catalog,
8
9
  createOttoRouterModel,
9
10
  createOpenAIOAuthModel,
11
+ normalizeOllamaBaseURL,
10
12
  } from '../../../providers/src/index.ts';
11
13
  import { createCopilotModel } from '../../../providers/src/copilot-client.ts';
12
14
  import type { OAuth } from '../../../types/src/index.ts';
@@ -32,13 +34,15 @@ export type ProviderName =
32
34
  | 'openai'
33
35
  | 'anthropic'
34
36
  | 'google'
37
+ | 'ollama-cloud'
35
38
  | 'openrouter'
36
39
  | 'opencode'
37
40
  | 'copilot'
38
41
  | 'ottorouter'
39
42
  | 'zai'
40
43
  | 'zai-coding'
41
- | 'moonshot';
44
+ | 'moonshot'
45
+ | 'minimax';
42
46
 
43
47
  export type ModelConfig = {
44
48
  apiKey?: string;
@@ -96,6 +100,20 @@ export async function resolveModel(
96
100
  return google(model);
97
101
  }
98
102
 
103
+ if (provider === 'ollama-cloud') {
104
+ const entry = catalog[provider];
105
+ const apiKey = config.apiKey || process.env.OLLAMA_API_KEY || '';
106
+ const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined;
107
+ const baseURL = normalizeOllamaBaseURL(
108
+ config.baseURL || entry?.api || 'https://ollama.com',
109
+ );
110
+ const instance = createOllama({
111
+ baseURL,
112
+ headers,
113
+ });
114
+ return instance(model);
115
+ }
116
+
99
117
  if (provider === 'openrouter') {
100
118
  const apiKey = config.apiKey || process.env.OPENROUTER_API_KEY || '';
101
119
  const openrouter = createOpenRouter({ apiKey });
@@ -233,6 +251,15 @@ export async function resolveModel(
233
251
  return instance(model);
234
252
  }
235
253
 
254
+ if (provider === 'minimax') {
255
+ const entry = catalog[provider];
256
+ const apiKey = config.apiKey || process.env.MINIMAX_API_KEY || '';
257
+ const baseURL =
258
+ config.baseURL || entry?.api || 'https://api.minimax.io/anthropic/v1';
259
+ const instance = createAnthropic({ apiKey, baseURL });
260
+ return instance(model);
261
+ }
262
+
236
263
  throw new Error(`Unsupported provider: ${provider}`);
237
264
  }
238
265
 
@@ -1,266 +1 @@
1
- import { tool, type Tool } from 'ai';
2
- import { spawn } from 'node:child_process';
3
- import { z } from 'zod/v3';
4
- import DESCRIPTION from './bash.txt' with { type: 'text' };
5
- import { getAugmentedPath } from '../bin-manager.ts';
6
- import { createToolError, type ToolResponse } from '../error.ts';
7
- import { injectCoAuthorIntoGitCommit } from './git-identity.ts';
8
-
9
- function normalizePath(p: string) {
10
- const normalized = p.replace(/\\/g, '/');
11
- const driveMatch = normalized.match(/^([A-Za-z]):\//);
12
- const drivePrefix = driveMatch ? `${driveMatch[1]}:` : '';
13
- const rest = driveMatch ? normalized.slice(2) : normalized;
14
- const parts = rest.split('/');
15
- const stack: string[] = [];
16
- for (const part of parts) {
17
- if (!part || part === '.') continue;
18
- if (part === '..') stack.pop();
19
- else stack.push(part);
20
- }
21
- if (drivePrefix) return `${drivePrefix}/${stack.join('/')}`;
22
- return `/${stack.join('/')}`;
23
- }
24
-
25
- function resolveSafePath(projectRoot: string, p: string) {
26
- const root = normalizePath(projectRoot);
27
- const abs = normalizePath(`${root}/${p || '.'}`);
28
- if (!(abs === root || abs.startsWith(`${root}/`))) {
29
- throw new Error(`cwd escapes project root: ${p}`);
30
- }
31
- return abs;
32
- }
33
-
34
- function killProcessTree(pid: number) {
35
- try {
36
- process.kill(-pid, 'SIGTERM');
37
- } catch {
38
- try {
39
- process.kill(pid, 'SIGTERM');
40
- } catch {}
41
- }
42
- }
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
-
59
- export function buildBashTool(projectRoot: string): {
60
- name: string;
61
- tool: Tool;
62
- } {
63
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK tool typings do not model async-iterable execute results yet.
64
- const bash = tool({
65
- description: DESCRIPTION,
66
- inputSchema: z
67
- .object({
68
- cmd: z.string().describe('Shell command to run (bash -c <cmd>)'),
69
- cwd: z
70
- .string()
71
- .default('.')
72
- .describe('Working directory relative to project root'),
73
- allowNonZeroExit: z
74
- .boolean()
75
- .optional()
76
- .default(false)
77
- .describe('If true, do not throw on non-zero exit'),
78
- timeout: z
79
- .number()
80
- .optional()
81
- .default(300000)
82
- .describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
83
- })
84
- .strict(),
85
- execute(
86
- {
87
- cmd,
88
- cwd,
89
- allowNonZeroExit,
90
- timeout = 300000,
91
- }: {
92
- cmd: string;
93
- cwd?: string;
94
- allowNonZeroExit?: boolean;
95
- timeout?: number;
96
- },
97
- options?: { abortSignal?: AbortSignal },
98
- ): AsyncIterable<BashStreamChunk> | BashResult {
99
- const abortSignal = options?.abortSignal;
100
-
101
- if (abortSignal?.aborted) {
102
- return createToolError('Command aborted before execution', 'abort', {
103
- cmd,
104
- });
105
- }
106
-
107
- const absCwd = resolveSafePath(projectRoot, cwd || '.');
108
- const finalCmd = injectCoAuthorIntoGitCommit(cmd);
109
-
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
- });
117
-
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;
127
-
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
-
140
- const settle = (result: BashResult) => {
141
- if (settled) return;
142
- settled = true;
143
- if (timeoutId) clearTimeout(timeoutId);
144
- if (abortSignal) {
145
- abortSignal.removeEventListener('abort', onAbort);
146
- }
147
- queue.push({ result });
148
- done = true;
149
- wake();
150
- };
151
-
152
- const onAbort = () => {
153
- if (settled) return;
154
- didAbort = true;
155
- if (proc.pid) killProcessTree(proc.pid);
156
- else proc.kill('SIGTERM');
157
- };
158
-
159
- if (abortSignal) {
160
- abortSignal.addEventListener('abort', onAbort, { once: true });
161
- }
162
-
163
- if (timeout > 0) {
164
- timeoutId = setTimeout(() => {
165
- didTimeout = true;
166
- if (proc.pid) killProcessTree(proc.pid);
167
- else proc.kill();
168
- }, timeout);
169
- }
170
-
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,
188
- stdout,
189
- stderr,
190
- }),
191
- );
192
- return;
193
- }
194
-
195
- if (didTimeout) {
196
- settle(
197
- createToolError(
198
- `Command timed out after ${timeout}ms: ${cmd}`,
199
- 'timeout',
200
- {
201
- parameter: 'timeout',
202
- value: timeout,
203
- stdout,
204
- stderr,
205
- suggestion: 'Increase timeout or optimize the command',
206
- },
207
- ),
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,
232
- });
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();
263
- },
264
- } as any);
265
- return { name: 'bash', tool: bash };
266
- }
1
+ export { buildShellTool, buildShellTool as buildBashTool } from './shell.ts';
@@ -34,7 +34,7 @@ export function applyStringEdit(
34
34
  ): { content: string; occurrences: number } {
35
35
  if (oldString.length === 0) {
36
36
  throw new Error(
37
- 'oldString must not be empty. Use write to create files or apply_patch for structural insertions.',
37
+ 'oldString must not be empty. Use write to create files or a structural editing tool for larger insertions.',
38
38
  );
39
39
  }
40
40
  if (oldString === newString) {
@@ -1,9 +1,9 @@
1
1
  Replace an exact text block in an existing file.
2
2
 
3
- Use this for targeted edits instead of apply_patch whenever possible.
3
+ Use this for targeted edits instead of structural patch-style editing whenever possible.
4
4
 
5
5
  Rules:
6
6
  - You must read the file first in the current session before editing it.
7
7
  - `oldString` must match the file exactly, including whitespace.
8
8
  - If `oldString` appears multiple times, provide more context or set `replaceAll: true`.
9
- - Use `write` for new files and `apply_patch` for structural multi-file diffs.
9
+ - Use `write` for new files and a structural editing tool for multi-file diffs when that capability exists.
@@ -6,6 +6,6 @@
6
6
  Usage tips:
7
7
  - Use for creating NEW files
8
8
  - Use when replacing >70% of a file's content (almost complete rewrite)
9
- - NEVER use for partial/targeted edits - use edit/multiedit first, or apply_patch for structural changes
9
+ - NEVER use for partial/targeted edits - use the dedicated exact-editing tools first, or a structural editing tool for larger changes when available
10
10
  - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
11
11
  - Prefer idempotent writes by providing the full intended content when you do use write