@ottocode/sdk 0.1.173

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 (125) hide show
  1. package/README.md +338 -0
  2. package/package.json +128 -0
  3. package/src/agent/types.ts +19 -0
  4. package/src/auth/src/copilot-oauth.ts +190 -0
  5. package/src/auth/src/index.ts +100 -0
  6. package/src/auth/src/oauth.ts +234 -0
  7. package/src/auth/src/openai-oauth.ts +394 -0
  8. package/src/auth/src/wallet.ts +51 -0
  9. package/src/browser.ts +32 -0
  10. package/src/config/src/index.ts +110 -0
  11. package/src/config/src/manager.ts +181 -0
  12. package/src/config/src/paths.ts +98 -0
  13. package/src/core/src/errors.ts +102 -0
  14. package/src/core/src/index.ts +108 -0
  15. package/src/core/src/providers/resolver.ts +244 -0
  16. package/src/core/src/streaming/artifacts.ts +41 -0
  17. package/src/core/src/terminals/bun-pty.ts +13 -0
  18. package/src/core/src/terminals/circular-buffer.ts +30 -0
  19. package/src/core/src/terminals/ensure-bun-pty.ts +70 -0
  20. package/src/core/src/terminals/index.ts +8 -0
  21. package/src/core/src/terminals/manager.ts +158 -0
  22. package/src/core/src/terminals/rust-libs.ts +30 -0
  23. package/src/core/src/terminals/terminal.ts +132 -0
  24. package/src/core/src/tools/bin-manager.ts +250 -0
  25. package/src/core/src/tools/builtin/bash.ts +155 -0
  26. package/src/core/src/tools/builtin/bash.txt +7 -0
  27. package/src/core/src/tools/builtin/file-cache.ts +39 -0
  28. package/src/core/src/tools/builtin/finish.ts +12 -0
  29. package/src/core/src/tools/builtin/finish.txt +10 -0
  30. package/src/core/src/tools/builtin/fs/cd.ts +19 -0
  31. package/src/core/src/tools/builtin/fs/cd.txt +5 -0
  32. package/src/core/src/tools/builtin/fs/index.ts +20 -0
  33. package/src/core/src/tools/builtin/fs/ls.ts +72 -0
  34. package/src/core/src/tools/builtin/fs/ls.txt +8 -0
  35. package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
  36. package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
  37. package/src/core/src/tools/builtin/fs/read.ts +119 -0
  38. package/src/core/src/tools/builtin/fs/read.txt +8 -0
  39. package/src/core/src/tools/builtin/fs/tree.ts +149 -0
  40. package/src/core/src/tools/builtin/fs/tree.txt +11 -0
  41. package/src/core/src/tools/builtin/fs/util.ts +95 -0
  42. package/src/core/src/tools/builtin/fs/write.ts +106 -0
  43. package/src/core/src/tools/builtin/fs/write.txt +11 -0
  44. package/src/core/src/tools/builtin/git.commit.txt +6 -0
  45. package/src/core/src/tools/builtin/git.diff.txt +5 -0
  46. package/src/core/src/tools/builtin/git.status.txt +5 -0
  47. package/src/core/src/tools/builtin/git.ts +151 -0
  48. package/src/core/src/tools/builtin/glob.ts +128 -0
  49. package/src/core/src/tools/builtin/glob.txt +10 -0
  50. package/src/core/src/tools/builtin/grep.ts +136 -0
  51. package/src/core/src/tools/builtin/grep.txt +9 -0
  52. package/src/core/src/tools/builtin/ignore.ts +45 -0
  53. package/src/core/src/tools/builtin/patch/apply.ts +546 -0
  54. package/src/core/src/tools/builtin/patch/constants.ts +5 -0
  55. package/src/core/src/tools/builtin/patch/normalize.ts +31 -0
  56. package/src/core/src/tools/builtin/patch/parse-enveloped.ts +209 -0
  57. package/src/core/src/tools/builtin/patch/parse-unified.ts +231 -0
  58. package/src/core/src/tools/builtin/patch/parse.ts +28 -0
  59. package/src/core/src/tools/builtin/patch/text.ts +23 -0
  60. package/src/core/src/tools/builtin/patch/types.ts +82 -0
  61. package/src/core/src/tools/builtin/patch.ts +167 -0
  62. package/src/core/src/tools/builtin/patch.txt +207 -0
  63. package/src/core/src/tools/builtin/progress.ts +55 -0
  64. package/src/core/src/tools/builtin/progress.txt +7 -0
  65. package/src/core/src/tools/builtin/ripgrep.ts +125 -0
  66. package/src/core/src/tools/builtin/ripgrep.txt +7 -0
  67. package/src/core/src/tools/builtin/terminal.ts +300 -0
  68. package/src/core/src/tools/builtin/terminal.txt +93 -0
  69. package/src/core/src/tools/builtin/todos.ts +66 -0
  70. package/src/core/src/tools/builtin/todos.txt +7 -0
  71. package/src/core/src/tools/builtin/websearch.ts +250 -0
  72. package/src/core/src/tools/builtin/websearch.txt +12 -0
  73. package/src/core/src/tools/error.ts +67 -0
  74. package/src/core/src/tools/loader.ts +421 -0
  75. package/src/core/src/types/index.ts +11 -0
  76. package/src/core/src/types/types.ts +4 -0
  77. package/src/core/src/utils/ansi.ts +27 -0
  78. package/src/core/src/utils/debug.ts +40 -0
  79. package/src/core/src/utils/logger.ts +150 -0
  80. package/src/index.ts +313 -0
  81. package/src/prompts/src/agents/build.txt +89 -0
  82. package/src/prompts/src/agents/general.txt +15 -0
  83. package/src/prompts/src/agents/plan.txt +10 -0
  84. package/src/prompts/src/agents/research.txt +50 -0
  85. package/src/prompts/src/base.txt +24 -0
  86. package/src/prompts/src/debug.ts +104 -0
  87. package/src/prompts/src/index.ts +1 -0
  88. package/src/prompts/src/modes/oneshot.txt +9 -0
  89. package/src/prompts/src/providers/anthropic.txt +247 -0
  90. package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
  91. package/src/prompts/src/providers/default.txt +466 -0
  92. package/src/prompts/src/providers/google.txt +230 -0
  93. package/src/prompts/src/providers/moonshot.txt +24 -0
  94. package/src/prompts/src/providers/openai.txt +414 -0
  95. package/src/prompts/src/providers.ts +143 -0
  96. package/src/providers/src/anthropic-caching.ts +202 -0
  97. package/src/providers/src/anthropic-oauth-client.ts +157 -0
  98. package/src/providers/src/authorization.ts +17 -0
  99. package/src/providers/src/catalog-manual.ts +135 -0
  100. package/src/providers/src/catalog-merged.ts +9 -0
  101. package/src/providers/src/catalog.ts +8329 -0
  102. package/src/providers/src/copilot-client.ts +39 -0
  103. package/src/providers/src/env.ts +31 -0
  104. package/src/providers/src/google-client.ts +16 -0
  105. package/src/providers/src/index.ts +75 -0
  106. package/src/providers/src/moonshot-client.ts +25 -0
  107. package/src/providers/src/oauth-models.ts +39 -0
  108. package/src/providers/src/openai-oauth-client.ts +108 -0
  109. package/src/providers/src/opencode-client.ts +64 -0
  110. package/src/providers/src/openrouter-client.ts +31 -0
  111. package/src/providers/src/pricing.ts +178 -0
  112. package/src/providers/src/setu-client.ts +643 -0
  113. package/src/providers/src/utils.ts +210 -0
  114. package/src/providers/src/validate.ts +39 -0
  115. package/src/providers/src/zai-client.ts +47 -0
  116. package/src/skills/index.ts +34 -0
  117. package/src/skills/loader.ts +152 -0
  118. package/src/skills/parser.ts +108 -0
  119. package/src/skills/tool.ts +87 -0
  120. package/src/skills/types.ts +41 -0
  121. package/src/skills/validator.ts +110 -0
  122. package/src/types/src/auth.ts +33 -0
  123. package/src/types/src/config.ts +36 -0
  124. package/src/types/src/index.ts +20 -0
  125. package/src/types/src/provider.ts +71 -0
@@ -0,0 +1,12 @@
1
+ - Search the web or fetch content from URLs
2
+ - Use `query` to search the web and get a list of results with titles, URLs, and snippets
3
+ - Use `url` to fetch and read the content of a specific webpage
4
+ - Returns cleaned, text-based content (HTML tags are stripped)
5
+ - Cannot be used for both search and URL fetch in the same call
6
+
7
+ Usage tips:
8
+ - For research: use `query` to find relevant pages, then `url` to read specific ones
9
+ - Search returns up to 10 results with titles, URLs, and snippets
10
+ - URL fetching works for text-based content (HTML, JSON, XML, plain text)
11
+ - Content is automatically truncated if it exceeds maxLength (default 50,000 chars)
12
+ - Use this to gather current information, read documentation, or verify facts
@@ -0,0 +1,67 @@
1
+ export type ToolErrorType =
2
+ | 'validation'
3
+ | 'not_found'
4
+ | 'permission'
5
+ | 'execution'
6
+ | 'timeout'
7
+ | 'unsupported';
8
+
9
+ export type ToolErrorResponse = {
10
+ ok: false;
11
+ error: string;
12
+ errorType?: ToolErrorType;
13
+ details?: {
14
+ parameter?: string;
15
+ value?: unknown;
16
+ constraint?: string;
17
+ suggestion?: string;
18
+ [key: string]: unknown;
19
+ };
20
+ stack?: string;
21
+ };
22
+
23
+ export type ToolSuccessResponse<T = unknown> = {
24
+ ok: true;
25
+ } & T;
26
+
27
+ export type ToolResponse<T = unknown> =
28
+ | ToolSuccessResponse<T>
29
+ | ToolErrorResponse;
30
+
31
+ export function isToolError(result: unknown): result is ToolErrorResponse {
32
+ if (!result || typeof result !== 'object') return false;
33
+ const obj = result as Record<string, unknown>;
34
+ return obj.ok === false || 'error' in obj || obj.success === false;
35
+ }
36
+
37
+ export function extractToolError(
38
+ result: unknown,
39
+ topLevelError?: string,
40
+ ): string | undefined {
41
+ if (topLevelError?.trim()) return topLevelError.trim();
42
+ if (!result || typeof result !== 'object') return undefined;
43
+
44
+ const obj = result as Record<string, unknown>;
45
+ const keys = ['error', 'stderr', 'message', 'detail', 'details', 'reason'];
46
+ for (const key of keys) {
47
+ const value = obj[key];
48
+ if (typeof value === 'string') {
49
+ const trimmed = value.trim();
50
+ if (trimmed.length) return trimmed;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ export function createToolError(
57
+ error: string,
58
+ errorType?: ToolErrorType,
59
+ details?: ToolErrorResponse['details'],
60
+ ): ToolErrorResponse {
61
+ return {
62
+ ok: false,
63
+ error,
64
+ errorType,
65
+ details,
66
+ };
67
+ }
@@ -0,0 +1,421 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import { finishTool } from './builtin/finish.ts';
4
+ import { buildFsTools } from './builtin/fs/index.ts';
5
+ import { buildGitTools } from './builtin/git.ts';
6
+ import { progressUpdateTool } from './builtin/progress.ts';
7
+ import { buildBashTool } from './builtin/bash.ts';
8
+ import { buildRipgrepTool } from './builtin/ripgrep.ts';
9
+ import { buildGrepTool } from './builtin/grep.ts';
10
+ import { buildGlobTool } from './builtin/glob.ts';
11
+ import { buildApplyPatchTool } from './builtin/patch.ts';
12
+ import { updateTodosTool } from './builtin/todos.ts';
13
+ import { buildWebSearchTool } from './builtin/websearch.ts';
14
+ import { buildTerminalTool } from './builtin/terminal.ts';
15
+ import type { TerminalManager } from '../terminals/index.ts';
16
+ import { initializeSkills, buildSkillTool } from '../../../skills/index.ts';
17
+ import fg from 'fast-glob';
18
+ import { dirname, isAbsolute, join } from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
20
+ import { promises as fs } from 'node:fs';
21
+ import { spawn as nodeSpawn } from 'node:child_process';
22
+
23
+ export type DiscoveredTool = { name: string; tool: Tool };
24
+
25
+ type PluginParameter = {
26
+ type: 'string' | 'number' | 'boolean';
27
+ description?: string;
28
+ default?: string | number | boolean;
29
+ enum?: string[];
30
+ optional?: boolean;
31
+ };
32
+
33
+ type PluginDescriptor = {
34
+ name?: string;
35
+ description?: string;
36
+ parameters?: Record<string, PluginParameter>;
37
+ execute?: PluginExecutor;
38
+ run?: PluginExecutor;
39
+ handler?: PluginExecutor;
40
+ setup?: (context: PluginContext) => unknown | Promise<unknown>;
41
+ onInit?: (context: PluginContext) => unknown | Promise<unknown>;
42
+ };
43
+
44
+ type PluginExecutor = (args: PluginExecuteArgs) => unknown | Promise<unknown>;
45
+
46
+ type PluginExecuteArgs = {
47
+ input: Record<string, unknown>;
48
+ project: string;
49
+ projectRoot: string;
50
+ directory: string;
51
+ worktree: string;
52
+ exec: ExecFn;
53
+ run: ExecFn;
54
+ $: TemplateExecFn;
55
+ fs: FsHelpers;
56
+ env: Record<string, string>;
57
+ context: PluginContext;
58
+ };
59
+
60
+ type PluginContext = {
61
+ project: string;
62
+ projectRoot: string;
63
+ directory: string;
64
+ worktree: string;
65
+ toolDir: string;
66
+ };
67
+
68
+ type ExecFn = (
69
+ command: string,
70
+ args?: string[] | ExecOptions,
71
+ options?: ExecOptions,
72
+ ) => Promise<ExecResult>;
73
+
74
+ type TemplateExecFn = (
75
+ strings: TemplateStringsArray,
76
+ ...values: unknown[]
77
+ ) => Promise<ExecResult>;
78
+
79
+ type ExecOptions = {
80
+ cwd?: string;
81
+ env?: Record<string, string>;
82
+ allowNonZeroExit?: boolean;
83
+ };
84
+
85
+ type ExecResult = { exitCode: number; stdout: string; stderr: string };
86
+
87
+ type FsHelpers = {
88
+ readFile: (path: string, encoding?: BufferEncoding) => Promise<string>;
89
+ writeFile: (path: string, content: string) => Promise<void>;
90
+ exists: (path: string) => Promise<boolean>;
91
+ };
92
+
93
+ const pluginPatterns = ['tools/*/tool.js', 'tools/*/tool.mjs'];
94
+
95
+ let globalTerminalManager: TerminalManager | null = null;
96
+
97
+ export function setTerminalManager(manager: TerminalManager): void {
98
+ globalTerminalManager = manager;
99
+ }
100
+
101
+ export function getTerminalManager(): TerminalManager | null {
102
+ return globalTerminalManager;
103
+ }
104
+
105
+ export async function discoverProjectTools(
106
+ projectRoot: string,
107
+ globalConfigDir?: string,
108
+ ): Promise<DiscoveredTool[]> {
109
+ const tools = new Map<string, Tool>();
110
+ for (const { name, tool } of buildFsTools(projectRoot)) tools.set(name, tool);
111
+ for (const { name, tool } of buildGitTools(projectRoot))
112
+ tools.set(name, tool);
113
+ // Built-ins
114
+ tools.set('finish', finishTool);
115
+ tools.set('progress_update', progressUpdateTool);
116
+ const bash = buildBashTool(projectRoot);
117
+ tools.set(bash.name, bash.tool);
118
+ // Search
119
+ const rg = buildRipgrepTool(projectRoot);
120
+ tools.set(rg.name, rg.tool);
121
+ const grep = buildGrepTool(projectRoot);
122
+ tools.set(grep.name, grep.tool);
123
+ const glob = buildGlobTool(projectRoot);
124
+ tools.set(glob.name, glob.tool);
125
+ // Patch/apply
126
+ const ap = buildApplyPatchTool(projectRoot);
127
+ tools.set(ap.name, ap.tool);
128
+ // Todo tracking
129
+ tools.set('update_todos', updateTodosTool);
130
+ // Web search
131
+ const ws = buildWebSearchTool();
132
+ tools.set(ws.name, ws.tool);
133
+ // Terminal (if manager is available)
134
+ if (globalTerminalManager) {
135
+ const term = buildTerminalTool(projectRoot, globalTerminalManager);
136
+ tools.set(term.name, term.tool);
137
+ }
138
+ // Skills
139
+ // Always reinitialize to ensure skills are discovered for the current project
140
+ await initializeSkills(projectRoot);
141
+ const skillTool = buildSkillTool();
142
+ tools.set(skillTool.name, skillTool.tool);
143
+
144
+ async function loadFromBase(base: string | null | undefined) {
145
+ if (!base) return;
146
+ try {
147
+ await fs.readdir(base);
148
+ } catch {
149
+ return;
150
+ }
151
+ for (const pattern of pluginPatterns) {
152
+ const files = await fg(pattern, { cwd: base, absolute: false });
153
+ for (const rel of files) {
154
+ const match = rel.match(/^tools\/([^/]+)\/tool\.(m?js)$/);
155
+ if (!match || !match[1]) continue;
156
+ const folder = match[1];
157
+ const absPath = join(base, rel).replace(/\\/g, '/');
158
+ try {
159
+ const plugin = await loadPlugin(absPath, folder, projectRoot);
160
+ if (plugin) tools.set(plugin.name, plugin.tool);
161
+ } catch (err) {
162
+ if (process.env.OTTO_DEBUG_TOOLS === '1')
163
+ console.error('Failed to load tool', absPath, err);
164
+ }
165
+ }
166
+ }
167
+ // Fallback: manual directory scan
168
+ try {
169
+ const toolsDir = join(base, 'tools');
170
+ const entries = await fs.readdir(toolsDir).catch(() => [] as string[]);
171
+ for (const folder of entries) {
172
+ const js = join(toolsDir, folder, 'tool.js');
173
+ const mjs = join(toolsDir, folder, 'tool.mjs');
174
+ const candidate = await fs
175
+ .stat(js)
176
+ .then(() => js)
177
+ .catch(
178
+ async () =>
179
+ await fs
180
+ .stat(mjs)
181
+ .then(() => mjs)
182
+ .catch(() => null),
183
+ );
184
+ if (!candidate) continue;
185
+ try {
186
+ const plugin = await loadPlugin(
187
+ candidate.replace(/\\/g, '/'),
188
+ folder,
189
+ projectRoot,
190
+ );
191
+ if (plugin) tools.set(plugin.name, plugin.tool);
192
+ } catch {}
193
+ }
194
+ } catch {}
195
+ }
196
+
197
+ await loadFromBase(globalConfigDir);
198
+ await loadFromBase(join(projectRoot, '.otto'));
199
+ return Array.from(tools.entries()).map(([name, tool]) => ({ name, tool }));
200
+ }
201
+
202
+ async function loadPlugin(
203
+ absPath: string,
204
+ folder: string,
205
+ projectRoot: string,
206
+ ): Promise<DiscoveredTool | null> {
207
+ const mod = await import(`${pathToFileURL(absPath).href}?t=${Date.now()}`);
208
+ const candidate = resolveExport(mod);
209
+ if (!candidate) throw new Error('No plugin export found');
210
+
211
+ const context: PluginContext = {
212
+ project: projectRoot,
213
+ projectRoot,
214
+ directory: projectRoot,
215
+ worktree: projectRoot,
216
+ toolDir: absPath.slice(0, absPath.lastIndexOf('/')),
217
+ };
218
+
219
+ let descriptor: PluginDescriptor | null | undefined;
220
+ if (typeof candidate === 'function') descriptor = await candidate(context);
221
+ else descriptor = candidate;
222
+ if (!descriptor || typeof descriptor !== 'object')
223
+ throw new Error('Plugin must return an object descriptor');
224
+
225
+ if (typeof descriptor.setup === 'function') await descriptor.setup(context);
226
+ if (typeof descriptor.onInit === 'function') await descriptor.onInit(context);
227
+
228
+ const name = sanitizeName(descriptor.name ?? folder);
229
+ const description = descriptor.description ?? `Custom tool ${name}`;
230
+ const parameters = descriptor.parameters ?? {};
231
+ const inputSchema = createInputSchema(parameters);
232
+ const executor = resolveExecutor(descriptor);
233
+
234
+ const helpersFactory = createHelpers(projectRoot, context.toolDir);
235
+
236
+ const wrapped = tool({
237
+ description,
238
+ inputSchema,
239
+ async execute(input) {
240
+ const helpers = helpersFactory();
241
+ const result = await executor({
242
+ input: input as Record<string, unknown>,
243
+ project: helpers.context.project,
244
+ projectRoot: helpers.context.projectRoot,
245
+ directory: helpers.context.directory,
246
+ worktree: helpers.context.worktree,
247
+ exec: helpers.exec,
248
+ run: helpers.exec,
249
+ $: helpers.templateExec,
250
+ fs: helpers.fs,
251
+ env: helpers.env,
252
+ context: helpers.context,
253
+ });
254
+ return result ?? { ok: true };
255
+ },
256
+ });
257
+
258
+ return { name, tool: wrapped };
259
+ }
260
+
261
+ function resolveExport(mod: Record<string, unknown>) {
262
+ if (mod.default) return mod.default;
263
+ if (mod.tool) return mod.tool;
264
+ if (mod.plugin) return mod.plugin;
265
+ if (mod.Tool) return mod.Tool;
266
+ const values = Object.values(mod);
267
+ return values.find(
268
+ (value) => typeof value === 'function' || typeof value === 'object',
269
+ );
270
+ }
271
+
272
+ function resolveExecutor(descriptor: PluginDescriptor): PluginExecutor {
273
+ const fn = descriptor.execute ?? descriptor.run ?? descriptor.handler;
274
+ if (typeof fn !== 'function')
275
+ throw new Error('Plugin must provide an execute/run/handler function');
276
+ return fn;
277
+ }
278
+
279
+ function sanitizeName(name: string) {
280
+ const cleaned = name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 128);
281
+ return cleaned || 'tool';
282
+ }
283
+
284
+ function createInputSchema(parameters: Record<string, PluginParameter>) {
285
+ const shape: Record<string, z.ZodTypeAny> = {};
286
+ for (const [key, def] of Object.entries(parameters)) {
287
+ let schema: z.ZodTypeAny;
288
+ if (def.type === 'string') {
289
+ const values = def.enum;
290
+ schema = values?.length
291
+ ? z.enum(values as [string, ...string[]])
292
+ : z.string();
293
+ } else if (def.type === 'number') schema = z.number();
294
+ else schema = z.boolean();
295
+ if (def.description) schema = schema.describe(def.description);
296
+ if (def.default !== undefined)
297
+ schema = schema.default(def.default as never);
298
+ else if (def.optional) schema = schema.optional();
299
+ shape[key] = schema;
300
+ }
301
+ return Object.keys(shape).length ? z.object(shape).strict() : z.object({});
302
+ }
303
+
304
+ function createHelpers(projectRoot: string, toolDir: string) {
305
+ return () => {
306
+ const exec = createExec(projectRoot);
307
+ const fsHelpers = createFsHelpers(projectRoot);
308
+ const context: PluginContext = {
309
+ project: projectRoot,
310
+ projectRoot,
311
+ directory: projectRoot,
312
+ worktree: projectRoot,
313
+ toolDir,
314
+ };
315
+ const env: Record<string, string> = {};
316
+ for (const [key, value] of Object.entries(process.env))
317
+ if (typeof value === 'string') env[key] = value;
318
+ const templateExec: TemplateExecFn = (strings, ...values) => {
319
+ const commandLine = strings.reduce((acc, part, index) => {
320
+ const value = index < values.length ? String(values[index]) : '';
321
+ return acc + part + value;
322
+ }, '');
323
+ const pieces = commandLine.trim().split(/\s+/).filter(Boolean);
324
+ if (pieces.length === 0)
325
+ throw new Error('Empty command passed to template executor');
326
+ const firstPiece = pieces[0];
327
+ if (!firstPiece)
328
+ throw new Error('Empty command passed to template executor');
329
+ return exec(firstPiece, pieces.slice(1));
330
+ };
331
+ return {
332
+ exec,
333
+ fs: fsHelpers,
334
+ env,
335
+ templateExec,
336
+ context,
337
+ };
338
+ };
339
+ }
340
+
341
+ function createExec(projectRoot: string): ExecFn {
342
+ return async (
343
+ command: string,
344
+ argsOrOptions?: string[] | ExecOptions,
345
+ maybeOptions?: ExecOptions,
346
+ ) => {
347
+ let args: string[] = [];
348
+ let options: ExecOptions = {};
349
+ if (Array.isArray(argsOrOptions)) {
350
+ args = argsOrOptions;
351
+ options = maybeOptions ?? {};
352
+ } else if (argsOrOptions) options = argsOrOptions;
353
+
354
+ const cwd = options.cwd
355
+ ? resolveWithinProject(projectRoot, options.cwd)
356
+ : projectRoot;
357
+ const env: Record<string, string> = {};
358
+ for (const [key, value] of Object.entries(process.env))
359
+ if (typeof value === 'string') env[key] = value;
360
+ if (options.env)
361
+ for (const [key, value] of Object.entries(options.env)) env[key] = value;
362
+
363
+ const proc = nodeSpawn(command, args, {
364
+ cwd,
365
+ env,
366
+ stdio: ['ignore', 'pipe', 'pipe'],
367
+ });
368
+
369
+ const stdoutChunks: Buffer[] = [];
370
+ const stderrChunks: Buffer[] = [];
371
+
372
+ proc.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
373
+ proc.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
374
+
375
+ const exitCode = await new Promise<number>((resolve, reject) => {
376
+ proc.on('exit', (code) => resolve(code ?? 0));
377
+ proc.on('error', reject);
378
+ });
379
+
380
+ const stdout = Buffer.concat(stdoutChunks).toString('utf-8');
381
+ const stderr = Buffer.concat(stderrChunks).toString('utf-8');
382
+ if (exitCode !== 0 && !options.allowNonZeroExit) {
383
+ const message = stderr.trim() || stdout.trim() || `${command} failed`;
384
+ throw new Error(`${command} exited with code ${exitCode}: ${message}`);
385
+ }
386
+ return { exitCode, stdout, stderr };
387
+ };
388
+ }
389
+
390
+ function createFsHelpers(projectRoot: string): FsHelpers {
391
+ return {
392
+ async readFile(path: string, encoding: BufferEncoding = 'utf-8') {
393
+ const abs = resolveWithinProject(projectRoot, path);
394
+ return fs.readFile(abs, { encoding });
395
+ },
396
+ async writeFile(path: string, content: string) {
397
+ const abs = resolveWithinProject(projectRoot, path);
398
+ await fs.mkdir(dirname(abs), { recursive: true });
399
+ await fs.writeFile(abs, content, 'utf-8');
400
+ },
401
+ async exists(path: string) {
402
+ const abs = resolveWithinProject(projectRoot, path);
403
+ try {
404
+ await fs.access(abs);
405
+ return true;
406
+ } catch {
407
+ return false;
408
+ }
409
+ },
410
+ };
411
+ }
412
+
413
+ function resolveWithinProject(projectRoot: string, target: string) {
414
+ if (!target) return projectRoot;
415
+ if (target.startsWith('~/')) {
416
+ const home = process.env.HOME || process.env.USERPROFILE || '';
417
+ return join(home, target.slice(2));
418
+ }
419
+ if (isAbsolute(target)) return target;
420
+ return join(projectRoot, target);
421
+ }
@@ -0,0 +1,11 @@
1
+ export type ExecutionContext = {
2
+ projectRoot: string;
3
+ workingDir?: string;
4
+ env?: Record<string, string>;
5
+ };
6
+
7
+ export type ToolResult = {
8
+ success: boolean;
9
+ output?: string;
10
+ error?: string;
11
+ };
@@ -0,0 +1,4 @@
1
+ export interface ToolContext {
2
+ projectRoot: string;
3
+ // Consider adding db, logger etc. when integrated
4
+ }
@@ -0,0 +1,27 @@
1
+ export function stripAnsi(input: string): string {
2
+ let result = '';
3
+ for (let i = 0; i < input.length; i += 1) {
4
+ const ch = input[i];
5
+ if (ch === '\u001B' || ch === '\u009B') {
6
+ // Skip CSI sequences until we hit a terminating byte (A-Z or a-z)
7
+ i += 1;
8
+ while (i < input.length) {
9
+ const code = input[i];
10
+ if (
11
+ code &&
12
+ ((code >= '@' && code <= 'Z') || (code >= 'a' && code <= 'z'))
13
+ ) {
14
+ break;
15
+ }
16
+ i += 1;
17
+ }
18
+ } else {
19
+ result += ch;
20
+ }
21
+ }
22
+ return result;
23
+ }
24
+
25
+ export function normalizeTerminalLine(line: string): string {
26
+ return stripAnsi(line).replace(/\r/g, '');
27
+ }
@@ -0,0 +1,40 @@
1
+ const TRUTHY = new Set(['1', 'true', 'yes', 'on']);
2
+
3
+ type GlobalDebugFlags = {
4
+ __OTTO_DEBUG_ENABLED__?: boolean;
5
+ __OTTO_TRACE_ENABLED__?: boolean;
6
+ };
7
+
8
+ function readGlobalFlag(
9
+ key: '__OTTO_DEBUG_ENABLED__' | '__OTTO_TRACE_ENABLED__',
10
+ ) {
11
+ const globalState = globalThis as GlobalDebugFlags;
12
+ return globalState[key];
13
+ }
14
+
15
+ function envEnabled(keys: string[]): boolean {
16
+ for (const key of keys) {
17
+ const raw = typeof process !== 'undefined' ? process.env?.[key] : undefined;
18
+ if (!raw) continue;
19
+ const trimmed = raw.trim().toLowerCase();
20
+ if (!trimmed) continue;
21
+ if (TRUTHY.has(trimmed) || trimmed === 'all') return true;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ export function isDebugEnabled(): boolean {
27
+ const globalFlag = readGlobalFlag('__OTTO_DEBUG_ENABLED__');
28
+ if (typeof globalFlag === 'boolean') {
29
+ return globalFlag;
30
+ }
31
+ return envEnabled(['OTTO_DEBUG', 'DEBUG_OTTO']);
32
+ }
33
+
34
+ export function isTraceEnabled(): boolean {
35
+ const globalFlag = readGlobalFlag('__OTTO_TRACE_ENABLED__');
36
+ if (typeof globalFlag === 'boolean') {
37
+ return Boolean(globalFlag) && isDebugEnabled();
38
+ }
39
+ return envEnabled(['OTTO_TRACE', 'TRACE_OTTO']) && isDebugEnabled();
40
+ }