@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.
- package/package.json +7 -2
- package/src/config/src/index.ts +5 -0
- package/src/config/src/manager.ts +106 -30
- package/src/core/src/providers/resolver.ts +28 -1
- package/src/core/src/tools/builtin/bash.ts +1 -266
- package/src/core/src/tools/builtin/fs/edit-shared.ts +1 -1
- package/src/core/src/tools/builtin/fs/edit.txt +2 -2
- package/src/core/src/tools/builtin/fs/write.txt +1 -1
- package/src/core/src/tools/builtin/shell.ts +273 -0
- package/src/core/src/tools/builtin/shell.txt +13 -0
- package/src/core/src/tools/builtin/terminal.txt +9 -6
- package/src/core/src/tools/loader.ts +134 -82
- package/src/index.ts +33 -0
- package/src/prompts/src/agents/build.txt +5 -6
- package/src/prompts/src/modes/guided.txt +2 -2
- package/src/prompts/src/providers/anthropic.txt +2 -2
- package/src/prompts/src/providers/default.txt +2 -2
- package/src/prompts/src/providers/glm.txt +2 -2
- package/src/prompts/src/providers/google.txt +9 -9
- package/src/prompts/src/providers/moonshot.txt +2 -2
- package/src/prompts/src/providers/openai.txt +3 -3
- package/src/prompts/src/providers.ts +15 -0
- package/src/providers/src/authorization.ts +26 -1
- package/src/providers/src/catalog-manual.ts +21 -6
- package/src/providers/src/catalog-merged.ts +2 -2
- package/src/providers/src/catalog.ts +10462 -10283
- package/src/providers/src/env.ts +10 -5
- package/src/providers/src/index.ts +26 -0
- package/src/providers/src/oauth-models.ts +1 -0
- package/src/providers/src/ollama-discovery.ts +149 -0
- package/src/providers/src/pricing.ts +3 -0
- package/src/providers/src/registry.ts +258 -0
- package/src/providers/src/utils.ts +10 -3
- package/src/providers/src/validate.ts +63 -2
- package/src/skills/index.ts +3 -0
- package/src/skills/tool.ts +28 -36
- package/src/types/src/config.ts +34 -8
- package/src/types/src/index.ts +4 -0
- package/src/types/src/provider.ts +33 -3
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
tools
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- `
|
|
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 `
|
|
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 `
|
|
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.
|