@ottocode/sdk 0.1.245 → 0.1.246

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 (34) hide show
  1. package/package.json +3 -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 +2 -2
  6. package/src/core/src/tools/builtin/bash.txt +1 -1
  7. package/src/core/src/tools/builtin/fs/edit-shared.ts +1 -1
  8. package/src/core/src/tools/builtin/fs/edit.txt +2 -2
  9. package/src/core/src/tools/builtin/fs/write.txt +1 -1
  10. package/src/core/src/tools/loader.ts +133 -81
  11. package/src/index.ts +33 -0
  12. package/src/prompts/src/agents/build.txt +3 -4
  13. package/src/prompts/src/providers/default.txt +1 -1
  14. package/src/prompts/src/providers/glm.txt +1 -1
  15. package/src/prompts/src/providers/google.txt +2 -2
  16. package/src/prompts/src/providers/moonshot.txt +1 -1
  17. package/src/prompts/src/providers/openai.txt +3 -3
  18. package/src/prompts/src/providers.ts +15 -0
  19. package/src/providers/src/authorization.ts +26 -1
  20. package/src/providers/src/catalog-manual.ts +21 -6
  21. package/src/providers/src/catalog-merged.ts +2 -2
  22. package/src/providers/src/catalog.ts +10284 -10283
  23. package/src/providers/src/env.ts +10 -5
  24. package/src/providers/src/index.ts +26 -0
  25. package/src/providers/src/ollama-discovery.ts +149 -0
  26. package/src/providers/src/pricing.ts +3 -0
  27. package/src/providers/src/registry.ts +258 -0
  28. package/src/providers/src/utils.ts +10 -3
  29. package/src/providers/src/validate.ts +63 -2
  30. package/src/skills/index.ts +3 -0
  31. package/src/skills/tool.ts +28 -36
  32. package/src/types/src/config.ts +34 -8
  33. package/src/types/src/index.ts +4 -0
  34. package/src/types/src/provider.ts +33 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.245",
3
+ "version": "0.1.246",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -97,9 +97,10 @@
97
97
  "@modelcontextprotocol/sdk": "^1.12",
98
98
  "@openauthjs/openauth": "^0.4.3",
99
99
  "@openrouter/ai-sdk-provider": "^1.2.0",
100
- "@ottorouter/ai-sdk": "0.2.0",
100
+ "@ottorouter/ai-sdk": "0.2.1",
101
101
  "@solana/web3.js": "^1.98.0",
102
102
  "ai": "^6.0.0",
103
+ "ai-sdk-ollama": "^3.8.3",
103
104
  "bs58": "^6.0.0",
104
105
  "bun-pty": "^0.3.2",
105
106
  "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
 
@@ -60,9 +60,9 @@ export function buildBashTool(projectRoot: string): {
60
60
  name: string;
61
61
  tool: Tool;
62
62
  } {
63
- // biome-ignore lint/suspicious/noExplicitAny: AI SDK tool typings do not model async-iterable execute results yet.
64
63
  const bash = tool({
65
64
  description: DESCRIPTION,
65
+
66
66
  inputSchema: z
67
67
  .object({
68
68
  cmd: z.string().describe('Shell command to run (bash -c <cmd>)'),
@@ -261,6 +261,6 @@ export function buildBashTool(projectRoot: string): {
261
261
 
262
262
  return stream();
263
263
  },
264
- } as any);
264
+ }) as unknown as Tool;
265
265
  return { name: 'bash', tool: bash };
266
266
  }
@@ -9,4 +9,4 @@
9
9
  - Chain commands with `&&` to fail-fast.
10
10
  - For long outputs, redirect to a file and `read` it back.
11
11
  - Batch independent checks (e.g. `git status && git diff`) in parallel tool calls rather than sequential bash chains when you need results separately.
12
- - Never use `bash` with `sed`/`awk` for programmatic file editing — use `edit`, `multiedit`, or `apply_patch` instead.
12
+ - Never use `bash` with `sed`/`awk` for programmatic file editing — use the dedicated file-editing tools instead.
@@ -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
@@ -12,7 +12,11 @@ import { updateTodosTool } from './builtin/todos.ts';
12
12
  import { buildWebSearchTool } from './builtin/websearch.ts';
13
13
  import { buildTerminalTool } from './builtin/terminal.ts';
14
14
  import type { TerminalManager } from '../terminals/index.ts';
15
- import { initializeSkills, buildSkillTool } from '../../../skills/index.ts';
15
+ import {
16
+ initializeSkills,
17
+ buildSkillTool,
18
+ setSkillSettings,
19
+ } from '../../../skills/index.ts';
16
20
  import { getMCPManager } from '../mcp/index.ts';
17
21
  import {
18
22
  getMCPToolBriefs,
@@ -104,6 +108,7 @@ type FsHelpers = {
104
108
  const pluginPatterns = ['tools/*/tool.js', 'tools/*/tool.mjs'];
105
109
 
106
110
  let globalTerminalManager: TerminalManager | null = null;
111
+ const staticToolDiscoveryCache = new Map<string, Promise<DiscoveredTool[]>>();
107
112
 
108
113
  export function setTerminalManager(manager: TerminalManager): void {
109
114
  globalTerminalManager = manager;
@@ -113,42 +118,141 @@ export function getTerminalManager(): TerminalManager | null {
113
118
  return globalTerminalManager;
114
119
  }
115
120
 
121
+ function getStaticToolDiscoveryCacheKey(
122
+ projectRoot: string,
123
+ globalConfigDir?: string,
124
+ ): string {
125
+ return `${projectRoot}::${globalConfigDir ?? ''}`;
126
+ }
127
+
128
+ async function discoverStaticProjectTools(
129
+ projectRoot: string,
130
+ globalConfigDir?: string,
131
+ skillSettings?: {
132
+ enabled?: boolean;
133
+ items?: Record<string, { enabled?: boolean }>;
134
+ },
135
+ ): Promise<DiscoveredTool[]> {
136
+ setSkillSettings(skillSettings);
137
+ const cacheKey = getStaticToolDiscoveryCacheKey(projectRoot, globalConfigDir);
138
+ const cached = staticToolDiscoveryCache.get(cacheKey);
139
+ if (cached) return cached;
140
+
141
+ const discoveryPromise = (async () => {
142
+ const tools = new Map<string, Tool>();
143
+ for (const { name, tool } of buildFsTools(projectRoot))
144
+ tools.set(name, tool);
145
+ for (const { name, tool } of buildGitTools(projectRoot))
146
+ tools.set(name, tool);
147
+ // Built-ins
148
+ tools.set('finish', finishTool);
149
+ tools.set('progress_update', progressUpdateTool);
150
+ const bash = buildBashTool(projectRoot);
151
+ tools.set(bash.name, bash.tool);
152
+ // Search
153
+ const rg = buildRipgrepTool(projectRoot);
154
+ tools.set(rg.name, rg.tool);
155
+ const glob = buildGlobTool(projectRoot);
156
+ tools.set(glob.name, glob.tool);
157
+ // Patch/apply
158
+ const ap = buildApplyPatchTool(projectRoot);
159
+ tools.set(ap.name, ap.tool);
160
+ // Todo tracking
161
+ tools.set('update_todos', updateTodosTool);
162
+ // Web search
163
+ const ws = buildWebSearchTool();
164
+ tools.set(ws.name, ws.tool);
165
+ // Skills
166
+ await initializeSkills(projectRoot);
167
+ const skillTool = buildSkillTool();
168
+ tools.set(skillTool.name, skillTool.tool);
169
+
170
+ async function loadFromBase(base: string | null | undefined) {
171
+ if (!base) return;
172
+ try {
173
+ await fs.readdir(base);
174
+ } catch {
175
+ return;
176
+ }
177
+ for (const pattern of pluginPatterns) {
178
+ const files = await fg(pattern, { cwd: base, absolute: false });
179
+ for (const rel of files) {
180
+ const match = rel.match(/^tools\/([^/]+)\/tool\.(m?js)$/);
181
+ if (!match || !match[1]) continue;
182
+ const folder = match[1];
183
+ const absPath = join(base, rel).replace(/\\/g, '/');
184
+ try {
185
+ const plugin = await loadPlugin(absPath, folder, projectRoot);
186
+ if (plugin) tools.set(plugin.name, plugin.tool);
187
+ } catch {}
188
+ }
189
+ }
190
+ // Fallback: manual directory scan
191
+ try {
192
+ const toolsDir = join(base, 'tools');
193
+ const entries = await fs.readdir(toolsDir).catch(() => [] as string[]);
194
+ for (const folder of entries) {
195
+ const js = join(toolsDir, folder, 'tool.js');
196
+ const mjs = join(toolsDir, folder, 'tool.mjs');
197
+ const candidate = await fs
198
+ .stat(js)
199
+ .then(() => js)
200
+ .catch(
201
+ async () =>
202
+ await fs
203
+ .stat(mjs)
204
+ .then(() => mjs)
205
+ .catch(() => null),
206
+ );
207
+ if (!candidate) continue;
208
+ try {
209
+ const plugin = await loadPlugin(
210
+ candidate.replace(/\\/g, '/'),
211
+ folder,
212
+ projectRoot,
213
+ );
214
+ if (plugin) tools.set(plugin.name, plugin.tool);
215
+ } catch {}
216
+ }
217
+ } catch {}
218
+ }
219
+
220
+ await loadFromBase(globalConfigDir);
221
+ await loadFromBase(join(projectRoot, '.otto'));
222
+ return Array.from(tools.entries()).map(([name, tool]) => ({ name, tool }));
223
+ })();
224
+
225
+ staticToolDiscoveryCache.set(cacheKey, discoveryPromise);
226
+ try {
227
+ return await discoveryPromise;
228
+ } catch (error) {
229
+ staticToolDiscoveryCache.delete(cacheKey);
230
+ throw error;
231
+ }
232
+ }
233
+
116
234
  export async function discoverProjectTools(
117
235
  projectRoot: string,
118
236
  globalConfigDir?: string,
237
+ skillSettings?: {
238
+ enabled?: boolean;
239
+ items?: Record<string, { enabled?: boolean }>;
240
+ },
119
241
  ): Promise<DiscoverResult> {
120
- const tools = new Map<string, Tool>();
121
- for (const { name, tool } of buildFsTools(projectRoot)) tools.set(name, tool);
122
- for (const { name, tool } of buildGitTools(projectRoot))
123
- tools.set(name, tool);
124
- // Built-ins
125
- tools.set('finish', finishTool);
126
- tools.set('progress_update', progressUpdateTool);
127
- const bash = buildBashTool(projectRoot);
128
- tools.set(bash.name, bash.tool);
129
- // Search
130
- const rg = buildRipgrepTool(projectRoot);
131
- tools.set(rg.name, rg.tool);
132
- const glob = buildGlobTool(projectRoot);
133
- tools.set(glob.name, glob.tool);
134
- // Patch/apply
135
- const ap = buildApplyPatchTool(projectRoot);
136
- tools.set(ap.name, ap.tool);
137
- // Todo tracking
138
- tools.set('update_todos', updateTodosTool);
139
- // Web search
140
- const ws = buildWebSearchTool();
141
- tools.set(ws.name, ws.tool);
142
- // Terminal (if manager is available)
242
+ setSkillSettings(skillSettings);
243
+ const staticTools = await discoverStaticProjectTools(
244
+ projectRoot,
245
+ globalConfigDir,
246
+ skillSettings,
247
+ );
248
+ const tools = new Map<string, Tool>(
249
+ staticTools.map(({ name, tool }) => [name, tool]),
250
+ );
251
+
143
252
  if (globalTerminalManager) {
144
253
  const term = buildTerminalTool(projectRoot, globalTerminalManager);
145
254
  tools.set(term.name, term.tool);
146
255
  }
147
- // Skills
148
- // Always reinitialize to ensure skills are discovered for the current project
149
- await initializeSkills(projectRoot);
150
- const skillTool = buildSkillTool();
151
- tools.set(skillTool.name, skillTool.tool);
152
256
 
153
257
  const mcpManager = getMCPManager();
154
258
  let mcpToolsRecord: Record<string, Tool> = {};
@@ -162,58 +266,6 @@ export async function discoverProjectTools(
162
266
  }
163
267
  }
164
268
 
165
- async function loadFromBase(base: string | null | undefined) {
166
- if (!base) return;
167
- try {
168
- await fs.readdir(base);
169
- } catch {
170
- return;
171
- }
172
- for (const pattern of pluginPatterns) {
173
- const files = await fg(pattern, { cwd: base, absolute: false });
174
- for (const rel of files) {
175
- const match = rel.match(/^tools\/([^/]+)\/tool\.(m?js)$/);
176
- if (!match || !match[1]) continue;
177
- const folder = match[1];
178
- const absPath = join(base, rel).replace(/\\/g, '/');
179
- try {
180
- const plugin = await loadPlugin(absPath, folder, projectRoot);
181
- if (plugin) tools.set(plugin.name, plugin.tool);
182
- } catch {}
183
- }
184
- }
185
- // Fallback: manual directory scan
186
- try {
187
- const toolsDir = join(base, 'tools');
188
- const entries = await fs.readdir(toolsDir).catch(() => [] as string[]);
189
- for (const folder of entries) {
190
- const js = join(toolsDir, folder, 'tool.js');
191
- const mjs = join(toolsDir, folder, 'tool.mjs');
192
- const candidate = await fs
193
- .stat(js)
194
- .then(() => js)
195
- .catch(
196
- async () =>
197
- await fs
198
- .stat(mjs)
199
- .then(() => mjs)
200
- .catch(() => null),
201
- );
202
- if (!candidate) continue;
203
- try {
204
- const plugin = await loadPlugin(
205
- candidate.replace(/\\/g, '/'),
206
- folder,
207
- projectRoot,
208
- );
209
- if (plugin) tools.set(plugin.name, plugin.tool);
210
- } catch {}
211
- }
212
- } catch {}
213
- }
214
-
215
- await loadFromBase(globalConfigDir);
216
- await loadFromBase(join(projectRoot, '.otto'));
217
269
  return {
218
270
  tools: Array.from(tools.entries()).map(([name, tool]) => ({ name, tool })),
219
271
  mcpToolsRecord,
package/src/index.ts CHANGED
@@ -14,7 +14,10 @@
14
14
  // =======================
15
15
  // Provider types
16
16
  export type {
17
+ BuiltInProviderId,
17
18
  ProviderId,
19
+ ProviderCompatibility,
20
+ ProviderPromptFamily,
18
21
  ModelOwner,
19
22
  ModelInfo,
20
23
  ModelProviderBinding,
@@ -28,6 +31,7 @@ export type { ApiAuth, OAuth, AuthInfo, AuthFile } from './types/src/index.ts';
28
31
  export type {
29
32
  DefaultConfig,
30
33
  PathConfig,
34
+ ProviderSettingsEntry,
31
35
  OttoConfig,
32
36
  ReasoningLevel,
33
37
  } from './types/src/index.ts';
@@ -51,6 +55,29 @@ export {
51
55
  modelSupportsReasoning,
52
56
  } from './providers/src/index.ts';
53
57
  export type { UnderlyingProviderKey } from './providers/src/index.ts';
58
+ export {
59
+ discoverOllamaModels,
60
+ normalizeOllamaBaseURL,
61
+ } from './providers/src/index.ts';
62
+ export type {
63
+ DiscoverOllamaOptions,
64
+ DiscoverOllamaResult,
65
+ } from './providers/src/index.ts';
66
+ export {
67
+ isBuiltInProviderId,
68
+ getProviderSettings,
69
+ getProviderDefinition,
70
+ hasConfiguredProvider,
71
+ getConfiguredProviderIds,
72
+ getConfiguredProviderModels,
73
+ getConfiguredProviderDefaultModel,
74
+ providerAllowsAnyModel,
75
+ hasConfiguredModel,
76
+ getConfiguredProviderFamily,
77
+ getConfiguredProviderEnvVar,
78
+ getConfiguredProviderApiKey,
79
+ } from './providers/src/index.ts';
80
+ export type { ResolvedProviderDefinition } from './providers/src/index.ts';
54
81
  export {
55
82
  isProviderAuthorized,
56
83
  ensureProviderEnv,
@@ -193,6 +220,9 @@ export {
193
220
  isAuthorized,
194
221
  ensureEnv,
195
222
  writeDefaults as setConfig,
223
+ writeProviderSettings,
224
+ removeProviderSettings,
225
+ writeSkillSettings,
196
226
  readDebugConfig,
197
227
  writeDebugConfig,
198
228
  writeAuth,
@@ -339,8 +369,11 @@ export {
339
369
  export {
340
370
  initializeSkills,
341
371
  getDiscoveredSkills,
372
+ setSkillSettings,
373
+ filterDiscoveredSkills,
342
374
  isSkillsInitialized,
343
375
  buildSkillTool,
376
+ summarizeDescription,
344
377
  rebuildSkillDescription,
345
378
  } from './skills/index.ts';
346
379
 
@@ -8,10 +8,9 @@ You help with coding and build tasks.
8
8
 
9
9
  Pick the right tool for the job (each tool's description has its full contract):
10
10
 
11
- - `edit` one exact replacement in an existing file.
12
- - `multiedit` several exact replacements in the same file.
13
- - `apply_patch` structural diffs, file add/delete/rename, or multi-file changes awkward as exact replacements.
14
- - `write` — NEW files only, or >70% full-file rewrites. Never for targeted edits.
11
+ - Use the exact-replacement editing tools available to you for targeted changes in existing files.
12
+ - Use patch-style editing for structural diffs, file add/delete/rename, or multi-file changes when that capability is available.
13
+ - Use `write` only for NEW files or >70% full-file rewrites. Never use it for targeted edits.
15
14
 
16
15
  **Always read a file immediately before editing it.** Memory and earlier context are not reliable — the file may have changed.
17
16
 
@@ -26,7 +26,7 @@ You are a coding agent running in otto, a terminal-based coding assistant. Preci
26
26
 
27
27
  1. Understand — use `glob`, `ripgrep`, `tree`, `read` to map the code. Batch independent searches.
28
28
  2. Plan — use `update_todos` for multi-step work. Mark one step `in_progress` at a time.
29
- 3. Implement — prefer `edit` / `multiedit` for targeted changes; use `apply_patch` for structural or multi-file edits; use `write` only for new files or near-total rewrites.
29
+ 3. Implement — prefer the targeted editing tools available to you for small in-file changes; use patch-style edits for structural or multi-file changes when available; use `write` only for new files or near-total rewrites.
30
30
  4. Verify — run project-specific build/lint/test commands via `bash`. Check `README.md` / `AGENTS.md` for the right command.
31
31
  5. Review — `git_status` / `git_diff`. Do NOT commit unless asked.
32
32