@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
@@ -0,0 +1,273 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { spawn } from 'node:child_process';
3
+ import { z } from 'zod/v3';
4
+ import DESCRIPTION from './shell.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 ShellResult = ToolResponse<{
45
+ exitCode: number;
46
+ stdout: string;
47
+ stderr: string;
48
+ }>;
49
+
50
+ type ShellStreamChunk =
51
+ | {
52
+ channel: 'output';
53
+ delta: string;
54
+ }
55
+ | {
56
+ result: ShellResult;
57
+ };
58
+
59
+ const shellInputSchema = z
60
+ .object({
61
+ cmd: z
62
+ .string()
63
+ .describe('Non-interactive shell command to run (bash -c <cmd>)'),
64
+ cwd: z
65
+ .string()
66
+ .default('.')
67
+ .describe('Working directory relative to project root'),
68
+ allowNonZeroExit: z
69
+ .boolean()
70
+ .optional()
71
+ .default(false)
72
+ .describe('If true, do not throw on non-zero exit'),
73
+ timeout: z
74
+ .number()
75
+ .optional()
76
+ .default(300000)
77
+ .describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
78
+ })
79
+ .strict();
80
+
81
+ type ShellInput = z.infer<typeof shellInputSchema>;
82
+
83
+ type ShellToolFactory = (definition: {
84
+ description: string;
85
+ inputSchema: typeof shellInputSchema;
86
+ execute(
87
+ input: ShellInput,
88
+ options?: { abortSignal?: AbortSignal },
89
+ ): AsyncIterable<ShellStreamChunk> | ShellResult;
90
+ }) => Tool;
91
+
92
+ export function buildShellTool(projectRoot: string): {
93
+ name: string;
94
+ tool: Tool;
95
+ } {
96
+ const createTool = tool as unknown as ShellToolFactory;
97
+ const shell = createTool({
98
+ description: DESCRIPTION,
99
+ inputSchema: shellInputSchema,
100
+ execute(
101
+ { cmd, cwd, allowNonZeroExit, timeout = 300000 }: ShellInput,
102
+ options?: { abortSignal?: AbortSignal },
103
+ ): AsyncIterable<ShellStreamChunk> | ShellResult {
104
+ const abortSignal = options?.abortSignal;
105
+
106
+ if (abortSignal?.aborted) {
107
+ return createToolError('Command aborted before execution', 'abort', {
108
+ cmd,
109
+ });
110
+ }
111
+
112
+ const absCwd = resolveSafePath(projectRoot, cwd || '.');
113
+ const finalCmd = injectCoAuthorIntoGitCommit(cmd);
114
+
115
+ const proc = spawn(finalCmd, {
116
+ cwd: absCwd,
117
+ shell: true,
118
+ stdio: ['ignore', 'pipe', 'pipe'],
119
+ env: { ...process.env, PATH: getAugmentedPath() },
120
+ detached: true,
121
+ });
122
+
123
+ let stdout = '';
124
+ let stderr = '';
125
+ let didTimeout = false;
126
+ let didAbort = false;
127
+ let settled = false;
128
+ let done = false;
129
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
130
+ const queue: ShellStreamChunk[] = [];
131
+ let notify: (() => void) | null = null;
132
+
133
+ const wake = () => {
134
+ if (!notify) return;
135
+ notify();
136
+ notify = null;
137
+ };
138
+
139
+ const pushDelta = (text: string) => {
140
+ if (!text) return;
141
+ queue.push({ channel: 'output', delta: text });
142
+ wake();
143
+ };
144
+
145
+ const settle = (result: ShellResult) => {
146
+ if (settled) return;
147
+ settled = true;
148
+ if (timeoutId) clearTimeout(timeoutId);
149
+ if (abortSignal) {
150
+ abortSignal.removeEventListener('abort', onAbort);
151
+ }
152
+ queue.push({ result });
153
+ done = true;
154
+ wake();
155
+ };
156
+
157
+ const onAbort = () => {
158
+ if (settled) return;
159
+ didAbort = true;
160
+ if (proc.pid) killProcessTree(proc.pid);
161
+ else proc.kill('SIGTERM');
162
+ };
163
+
164
+ if (abortSignal) {
165
+ abortSignal.addEventListener('abort', onAbort, { once: true });
166
+ }
167
+
168
+ if (timeout > 0) {
169
+ timeoutId = setTimeout(() => {
170
+ didTimeout = true;
171
+ if (proc.pid) killProcessTree(proc.pid);
172
+ else proc.kill();
173
+ }, timeout);
174
+ }
175
+
176
+ proc.stdout?.on('data', (chunk) => {
177
+ const text = chunk.toString();
178
+ stdout += text;
179
+ pushDelta(text);
180
+ });
181
+
182
+ proc.stderr?.on('data', (chunk) => {
183
+ const text = chunk.toString();
184
+ stderr += text;
185
+ pushDelta(text);
186
+ });
187
+
188
+ proc.on('close', (exitCode) => {
189
+ if (didAbort) {
190
+ settle(
191
+ createToolError(`Command aborted by user: ${cmd}`, 'abort', {
192
+ cmd,
193
+ stdout,
194
+ stderr,
195
+ }),
196
+ );
197
+ return;
198
+ }
199
+
200
+ if (didTimeout) {
201
+ settle(
202
+ createToolError(
203
+ `Command timed out after ${timeout}ms: ${cmd}`,
204
+ 'timeout',
205
+ {
206
+ parameter: 'timeout',
207
+ value: timeout,
208
+ stdout,
209
+ stderr,
210
+ suggestion: 'Increase timeout or optimize the command',
211
+ },
212
+ ),
213
+ );
214
+ return;
215
+ }
216
+
217
+ if (exitCode !== 0 && !allowNonZeroExit) {
218
+ const errorDetail = stderr.trim() || stdout.trim() || '';
219
+ const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
220
+ settle(
221
+ createToolError(errorMsg, 'execution', {
222
+ exitCode,
223
+ stdout,
224
+ stderr,
225
+ cmd,
226
+ suggestion: 'Check command syntax or use allowNonZeroExit: true',
227
+ }),
228
+ );
229
+ return;
230
+ }
231
+
232
+ settle({
233
+ ok: true,
234
+ exitCode: exitCode ?? 0,
235
+ stdout,
236
+ stderr,
237
+ });
238
+ });
239
+
240
+ proc.on('error', (err) => {
241
+ settle(
242
+ createToolError(
243
+ `Command execution failed: ${err.message}`,
244
+ 'execution',
245
+ {
246
+ cmd,
247
+ originalError: err.message,
248
+ },
249
+ ),
250
+ );
251
+ });
252
+
253
+ const stream = async function* (): AsyncGenerator<ShellStreamChunk> {
254
+ while (!done || queue.length > 0) {
255
+ if (queue.length === 0) {
256
+ await new Promise<void>((resolve) => {
257
+ notify = resolve;
258
+ });
259
+ }
260
+ while (queue.length > 0) {
261
+ const chunk = queue.shift();
262
+ if (chunk) yield chunk;
263
+ }
264
+ }
265
+ };
266
+
267
+ return stream();
268
+ },
269
+ }) as unknown as Tool;
270
+ return { name: 'shell', tool: shell };
271
+ }
272
+
273
+ export const buildBashTool = buildShellTool;
@@ -0,0 +1,13 @@
1
+ - Execute a non-interactive shell command using `bash -lc`
2
+ - Returns `stdout`, `stderr`, and `exitCode`
3
+ - `cwd` is relative to the project root and sandboxed within it
4
+
5
+ **Use `shell` for one-off, non-interactive commands.** These may be short-lived checks or long-running commands that finish on their own and do not require stdin. For commands that need interactive input, a TTY, or persistence across turns, use the `terminal` tool instead.
6
+
7
+ ## Usage tips
8
+
9
+ - Chain commands with `&&` to fail-fast.
10
+ - For long outputs, redirect to a file and `read` it back.
11
+ - For long-running non-interactive commands, set an appropriate `timeout` and ensure the command exits on its own.
12
+ - Batch independent checks (e.g. `git status && git diff`) in parallel tool calls rather than sequential shell chains when you need results separately.
13
+ - Never use `shell` with `sed`/`awk` for programmatic file editing — use the dedicated file-editing tools instead.
@@ -1,4 +1,4 @@
1
- - Manage persistent terminals for long-running processes (dev servers, watchers, build processes)
1
+ - Manage interactive and persistent terminals (REPLs, prompts, dev servers, watchers)
2
2
  - Returns terminal information and output
3
3
  - Supports creating, reading, writing, listing, and killing terminals
4
4
 
@@ -7,7 +7,7 @@
7
7
  ### start
8
8
  - Spawns a new persistent terminal (interactive shell by default)
9
9
  - Returns terminal ID for future operations
10
- - Use for processes that need to stay alive (dev servers, watchers, logs)
10
+ - Use for commands that need stdin, a TTY, interactive input, or persistence across turns
11
11
  - Before starting, call `terminal(operation: "list")` to see if a matching service is already running
12
12
  - Provide a clear `purpose` or `title` (e.g. "web dev server port 9100") so humans can recognize it
13
13
  - Parameters:
@@ -51,22 +51,25 @@
51
51
  - Parameters:
52
52
  - terminalId (required): Terminal ID to kill
53
53
 
54
- ## When to Use Terminal vs Bash
54
+ ## When to Use Terminal vs Shell
55
55
 
56
56
  ### Use terminal for:
57
+ - Interactive commands that need stdin or a TTY: REPLs, prompts, shells, debuggers
57
58
  - Dev servers: npm run dev, bun dev
58
59
  - File watchers: bun test --watch, nodemon
59
60
  - Build watchers: bun build --watch
60
61
  - Log tailing: tail -f logs/app.log
61
62
  - Background services: docker compose up
62
63
  - Any process that needs to stay alive and produce continuous output
64
+ - Any process you need to read from or write to across turns
63
65
 
64
- ### Use bash for:
66
+ ### Use shell for:
65
67
  - Status checks: git status, ls, ps
66
68
  - One-off commands: mkdir, rm, curl
67
69
  - Quick scripts: bun run build, git commit
68
70
  - File operations: cat, sed, awk
69
- - Short-lived commands with immediate output
71
+ - Non-interactive long-running commands that finish on their own (set an appropriate timeout)
72
+ - Any command that does not need stdin, a TTY, or follow-up interaction
70
73
 
71
74
  ## Example Workflow
72
75
 
@@ -83,7 +86,7 @@
83
86
 
84
87
  ## Notes
85
88
 
86
- - Terminals persist across multiple LLM turns (unlike bash commands)
89
+ - Terminals persist across multiple LLM turns (unlike shell commands)
87
90
  - Maximum 10 terminals per session
88
91
  - Exited terminals auto-cleanup after 5 minutes
89
92
  - Output is buffered (last 500 lines kept)
@@ -4,7 +4,7 @@ import { finishTool } from './builtin/finish.ts';
4
4
  import { buildFsTools } from './builtin/fs/index.ts';
5
5
  import { buildGitTools } from './builtin/git.ts';
6
6
  import { progressUpdateTool } from './builtin/progress.ts';
7
- import { buildBashTool } from './builtin/bash.ts';
7
+ import { buildShellTool } from './builtin/shell.ts';
8
8
  import { buildRipgrepTool } from './builtin/ripgrep.ts';
9
9
  import { buildGlobTool } from './builtin/glob.ts';
10
10
  import { buildApplyPatchTool } from './builtin/patch.ts';
@@ -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 shell = buildShellTool(projectRoot);
151
+ tools.set(shell.name, shell.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
 
@@ -19,13 +18,13 @@ Pick the right tool for the job (each tool's description has its full contract):
19
18
 
20
19
  After making changes:
21
20
 
22
- 1. Run project-specific build/lint/test commands with `bash` (check `package.json`, `README.md`, or `AGENTS.md` for the right command).
21
+ 1. Run project-specific build/lint/test commands with `shell` (check `package.json`, `README.md`, or `AGENTS.md` for the right command).
23
22
  2. Review diffs with `git_status` / `git_diff`.
24
23
  3. Do NOT commit unless the user explicitly asks. It is very important to only commit when asked.
25
24
 
26
25
  ## Terminal tool — when to use
27
26
 
28
- - Prefer `terminal` over `bash` for long-lived processes (dev servers, watchers, log tailing). `bash` is for one-off commands with immediate output.
27
+ - Prefer `terminal` over `shell` for interactive or persistent processes (dev servers, watchers, log tailing). `shell` is for one-off non-interactive commands.
29
28
  - List existing terminals before starting a new one; reuse when possible to avoid duplicate services.
30
29
  - Give each terminal a clear `purpose` / `title` (e.g. "web dev server 9100").
31
30
  - Mention active terminals (purpose, command, port) in your responses so humans know what's running.