@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,72 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import { promises as fs } from 'node:fs';
4
+ import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
5
+ import DESCRIPTION from './ls.txt' with { type: 'text' };
6
+ import { toIgnoredBasenames } from '../ignore.ts';
7
+ import { createToolError, type ToolResponse } from '../../error.ts';
8
+
9
+ export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
10
+ const ls = tool({
11
+ description: DESCRIPTION,
12
+ inputSchema: z.object({
13
+ path: z
14
+ .string()
15
+ .default('.')
16
+ .describe(
17
+ "Directory path. Relative to project root by default; absolute ('/...') and home ('~/...') paths are allowed.",
18
+ ),
19
+ ignore: z
20
+ .array(z.string())
21
+ .optional()
22
+ .describe('List of directory names/globs to ignore'),
23
+ }),
24
+ async execute({
25
+ path,
26
+ ignore,
27
+ }: {
28
+ path: string;
29
+ ignore?: string[];
30
+ }): Promise<
31
+ ToolResponse<{
32
+ path: string;
33
+ entries: Array<{ name: string; type: string }>;
34
+ }>
35
+ > {
36
+ const req = expandTilde(path || '.');
37
+ const abs = isAbsoluteLike(req)
38
+ ? req
39
+ : resolveSafePath(projectRoot, req || '.');
40
+ const ignored = toIgnoredBasenames(ignore);
41
+
42
+ try {
43
+ const dirents = await fs.readdir(abs, { withFileTypes: true });
44
+ const entries = dirents
45
+ .filter((d) => !String(d.name).startsWith('.'))
46
+ .map((d) => ({
47
+ name: String(d.name),
48
+ type: d.isDirectory() ? 'dir' : 'file',
49
+ }))
50
+ .filter((entry) => !(entry.type === 'dir' && ignored.has(entry.name)))
51
+ .sort((a, b) => a.name.localeCompare(b.name));
52
+ return { ok: true, path: req, entries };
53
+ } catch (error: unknown) {
54
+ const err = error as { code?: string; message?: string };
55
+ const message = err.message || 'ls failed';
56
+ return createToolError(
57
+ `ls failed for ${req}: ${message}`,
58
+ err.code === 'ENOENT' ? 'not_found' : 'execution',
59
+ {
60
+ parameter: 'path',
61
+ value: req,
62
+ suggestion:
63
+ err.code === 'ENOENT'
64
+ ? 'Check if the directory exists'
65
+ : 'Check if the directory exists and is accessible',
66
+ },
67
+ );
68
+ }
69
+ },
70
+ });
71
+ return { name: 'ls', tool: ls };
72
+ }
@@ -0,0 +1,8 @@
1
+ - Lists files and directories in a given path (non-recursive)
2
+ - Accepts absolute ('/...'), home ('~/...'), or project-relative paths
3
+ - Hides common build and cache folders by default (node_modules, dist, .git, etc.)
4
+ - Optional ignore patterns allow further filtering of directory names
5
+
6
+ Usage tips:
7
+ - Prefer the Glob tool for pattern-based discovery and Grep for content search
8
+ - Use the Tree tool for a hierarchical view with limited depth
@@ -0,0 +1,17 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import DESCRIPTION from './pwd.txt' with { type: 'text' };
4
+
5
+ // description imported above
6
+
7
+ export function buildPwdTool(): { name: string; tool: Tool } {
8
+ const pwd = tool({
9
+ description: DESCRIPTION,
10
+ inputSchema: z.object({}).optional(),
11
+ async execute() {
12
+ // Actual cwd resolution is handled in the adapter; this is a placeholder schema
13
+ return { cwd: '.' };
14
+ },
15
+ });
16
+ return { name: 'pwd', tool: pwd };
17
+ }
@@ -0,0 +1,5 @@
1
+ - Return the current working directory (relative to the project root)
2
+ - The runner maintains a per-session cwd; this tool reports it
3
+
4
+ Usage tip:
5
+ - Combine with the CD tool to navigate within the workspace
@@ -0,0 +1,119 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
5
+ import DESCRIPTION from './read.txt' with { type: 'text' };
6
+ import { createToolError, type ToolResponse } from '../../error.ts';
7
+
8
+ const embeddedTextAssets: Record<string, string> = {};
9
+
10
+ export function buildReadTool(projectRoot: string): {
11
+ name: string;
12
+ tool: Tool;
13
+ } {
14
+ const read = tool({
15
+ description: DESCRIPTION,
16
+ inputSchema: z.object({
17
+ path: z
18
+ .string()
19
+ .describe(
20
+ "File path. Relative to project root by default; absolute ('/...') and home ('~/...') paths are allowed.",
21
+ ),
22
+ startLine: z
23
+ .number()
24
+ .int()
25
+ .min(1)
26
+ .optional()
27
+ .describe(
28
+ 'Starting line number (1-indexed). If provided, only reads lines from startLine to endLine.',
29
+ ),
30
+ endLine: z
31
+ .number()
32
+ .int()
33
+ .min(1)
34
+ .optional()
35
+ .describe(
36
+ 'Ending line number (1-indexed, inclusive). Required if startLine is provided.',
37
+ ),
38
+ }),
39
+ async execute({
40
+ path,
41
+ startLine,
42
+ endLine,
43
+ }: {
44
+ path: string;
45
+ startLine?: number;
46
+ endLine?: number;
47
+ }): Promise<
48
+ ToolResponse<{
49
+ path: string;
50
+ content: string;
51
+ size: number;
52
+ lineRange?: string;
53
+ totalLines?: number;
54
+ }>
55
+ > {
56
+ if (!path || path.trim().length === 0) {
57
+ return createToolError(
58
+ 'Missing required parameter: path',
59
+ 'validation',
60
+ {
61
+ parameter: 'path',
62
+ value: path,
63
+ suggestion: 'Provide a file path to read',
64
+ },
65
+ );
66
+ }
67
+
68
+ const req = expandTilde(path);
69
+ const abs = isAbsoluteLike(req) ? req : resolveSafePath(projectRoot, req);
70
+
71
+ try {
72
+ let content = await readFile(abs, 'utf-8');
73
+
74
+ if (startLine !== undefined && endLine !== undefined) {
75
+ const lines = content.split('\n');
76
+ const start = Math.max(1, startLine) - 1;
77
+ const end = Math.min(lines.length, endLine);
78
+ const selectedLines = lines.slice(start, end);
79
+ content = selectedLines.join('\n');
80
+ return {
81
+ ok: true,
82
+ path: req,
83
+ content,
84
+ size: content.length,
85
+ lineRange: `@${startLine}-${endLine}`,
86
+ totalLines: lines.length,
87
+ };
88
+ }
89
+
90
+ return { ok: true, path: req, content, size: content.length };
91
+ } catch (error: unknown) {
92
+ const embedded = embeddedTextAssets[req];
93
+ if (embedded) {
94
+ const content = await readFile(embedded, 'utf-8');
95
+ return { ok: true, path: req, content, size: content.length };
96
+ }
97
+ const isEnoent =
98
+ error &&
99
+ typeof error === 'object' &&
100
+ 'code' in error &&
101
+ error.code === 'ENOENT';
102
+ return createToolError(
103
+ isEnoent
104
+ ? `File not found: ${req}`
105
+ : `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
106
+ isEnoent ? 'not_found' : 'execution',
107
+ {
108
+ parameter: 'path',
109
+ value: req,
110
+ suggestion: isEnoent
111
+ ? 'Use ls or tree to find available files'
112
+ : undefined,
113
+ },
114
+ );
115
+ }
116
+ },
117
+ });
118
+ return { name: 'read', tool: read };
119
+ }
@@ -0,0 +1,8 @@
1
+ - Read a text file from the workspace
2
+ - Accepts absolute ('/...'), home ('~/...'), or project-relative paths
3
+ - Returns file text and size in bytes
4
+ - May serve embedded text assets for some paths (internal optimization)
5
+
6
+ Usage tips:
7
+ - Prefer relative project paths when possible (more portable)
8
+ - For large files or searches, use the Grep or Ripgrep tool
@@ -0,0 +1,149 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import { promises as fs } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
6
+ import DESCRIPTION from './tree.txt' with { type: 'text' };
7
+ import { toIgnoredBasenames } from '../ignore.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
9
+
10
+ async function walkTree(
11
+ dir: string,
12
+ ignored: Set<string>,
13
+ maxDepth: number | null,
14
+ currentDepth: number,
15
+ prefix: string,
16
+ ): Promise<{ lines: string[]; dirs: number; files: number }> {
17
+ let dirs = 0;
18
+ let files = 0;
19
+ const lines: string[] = [];
20
+
21
+ if (maxDepth !== null && currentDepth >= maxDepth)
22
+ return { lines, dirs, files };
23
+
24
+ try {
25
+ const rawEntries = await fs.readdir(dir, { withFileTypes: true });
26
+ const entries = rawEntries.map((e) => ({
27
+ name: String(e.name),
28
+ isDir: e.isDirectory(),
29
+ }));
30
+
31
+ const filtered = entries
32
+ .filter((e) => !e.name.startsWith('.'))
33
+ .filter((e) => !(e.isDir && ignored.has(e.name)))
34
+ .sort((a, b) => {
35
+ if (a.isDir && !b.isDir) return -1;
36
+ if (!a.isDir && b.isDir) return 1;
37
+ return a.name.localeCompare(b.name);
38
+ });
39
+
40
+ for (let i = 0; i < filtered.length; i++) {
41
+ const entry = filtered[i];
42
+ const isLast = i === filtered.length - 1;
43
+ const connector = isLast ? '└── ' : '├── ';
44
+ const childPrefix = isLast ? ' ' : '│ ';
45
+
46
+ if (entry.isDir) {
47
+ dirs++;
48
+ lines.push(`${prefix}${connector}${entry.name}`);
49
+ const sub = await walkTree(
50
+ join(dir, entry.name),
51
+ ignored,
52
+ maxDepth,
53
+ currentDepth + 1,
54
+ `${prefix}${childPrefix}`,
55
+ );
56
+ lines.push(...sub.lines);
57
+ dirs += sub.dirs;
58
+ files += sub.files;
59
+ } else {
60
+ files++;
61
+ let lineCount = '';
62
+ try {
63
+ const content = await fs.readFile(join(dir, entry.name), 'utf-8');
64
+ const count = content.split('\n').length;
65
+ lineCount = ` (${count} lines)`;
66
+ } catch {}
67
+ lines.push(`${prefix}${connector}${entry.name}${lineCount}`);
68
+ }
69
+ }
70
+ } catch {
71
+ return { lines, dirs, files };
72
+ }
73
+
74
+ return { lines, dirs, files };
75
+ }
76
+
77
+ export function buildTreeTool(projectRoot: string): {
78
+ name: string;
79
+ tool: Tool;
80
+ } {
81
+ const tree = tool({
82
+ description: DESCRIPTION,
83
+ inputSchema: z.object({
84
+ path: z.string().default('.'),
85
+ depth: z
86
+ .number()
87
+ .int()
88
+ .min(1)
89
+ .max(20)
90
+ .optional()
91
+ .describe('Optional depth limit (defaults to full depth).'),
92
+ ignore: z
93
+ .array(z.string())
94
+ .optional()
95
+ .describe('List of directory names/globs to ignore'),
96
+ }),
97
+ async execute({
98
+ path,
99
+ depth,
100
+ ignore,
101
+ }: {
102
+ path: string;
103
+ depth?: number;
104
+ ignore?: string[];
105
+ }): Promise<
106
+ ToolResponse<{ path: string; depth: number | null; tree: string }>
107
+ > {
108
+ const req = expandTilde(path || '.');
109
+ const start = isAbsoluteLike(req)
110
+ ? req
111
+ : resolveSafePath(projectRoot, req || '.');
112
+ const ignored = toIgnoredBasenames(ignore);
113
+
114
+ try {
115
+ await fs.access(start);
116
+ } catch {
117
+ return createToolError(
118
+ `tree failed for ${req}: directory not found`,
119
+ 'not_found',
120
+ {
121
+ parameter: 'path',
122
+ value: req,
123
+ suggestion: 'Check if the directory exists',
124
+ },
125
+ );
126
+ }
127
+
128
+ try {
129
+ const result = await walkTree(start, ignored, depth ?? null, 0, '');
130
+ const header = '.';
131
+ const summary = `\n${result.dirs} director${result.dirs === 1 ? 'y' : 'ies'}, ${result.files} file${result.files === 1 ? '' : 's'}`;
132
+ const output = [header, ...result.lines, summary].join('\n');
133
+ return { ok: true, path: req, depth: depth ?? null, tree: output };
134
+ } catch (error: unknown) {
135
+ const err = error as { message?: string };
136
+ return createToolError(
137
+ `tree failed for ${req}: ${err.message || 'unknown error'}`,
138
+ 'execution',
139
+ {
140
+ parameter: 'path',
141
+ value: req,
142
+ suggestion: 'Check if the directory exists and is accessible',
143
+ },
144
+ );
145
+ }
146
+ },
147
+ });
148
+ return { name: 'tree', tool: tree };
149
+ }
@@ -0,0 +1,11 @@
1
+ - Render a directory tree from a starting path with line counts for each file
2
+ - Accepts absolute, home, or project-relative paths
3
+ - Skips common build/cache folders (node_modules, dist, .git, etc.) by default
4
+ - Depth is capped to avoid excessive output (1–5)
5
+ - Each file shows its line count (e.g. "index.ts (42 lines)")
6
+
7
+ Usage tips:
8
+ - Use the LS tool for a flat listing of one directory
9
+ - Use the Glob and Grep tools for file pattern and content searches respectively
10
+ - Pay attention to line counts: files over 500 lines are large — prefer reading specific line ranges (startLine/endLine) or use Grep to find relevant sections instead of reading the entire file
11
+ - Files over 2000 lines can consume significant context — always use targeted reads or search tools for these
@@ -0,0 +1,95 @@
1
+ import { createTwoFilesPatch } from 'diff';
2
+ import { resolve as resolvePath } from 'node:path';
3
+
4
+ function normalizeForComparison(value: string) {
5
+ const withForwardSlashes = value.replace(/\\/g, '/');
6
+ return process.platform === 'win32'
7
+ ? withForwardSlashes.toLowerCase()
8
+ : withForwardSlashes;
9
+ }
10
+
11
+ export function resolveSafePath(projectRoot: string, p: string) {
12
+ const root = resolvePath(projectRoot);
13
+ const target = resolvePath(root, p || '.');
14
+ const rootNorm = (() => {
15
+ const normalized = normalizeForComparison(root);
16
+ if (normalized === '/') return '/';
17
+ return normalized.replace(/[\\/]+$/, '');
18
+ })();
19
+ const targetNorm = normalizeForComparison(target);
20
+ const rootWithSlash = rootNorm === '/' ? '/' : `${rootNorm}/`;
21
+ const inProject =
22
+ targetNorm === rootNorm || targetNorm.startsWith(rootWithSlash);
23
+ if (!inProject) throw new Error(`Path escapes project root: ${p}`);
24
+ return target;
25
+ }
26
+
27
+ export function expandTilde(p: string): string {
28
+ const home = process.env.HOME || process.env.USERPROFILE || '';
29
+ if (!home) return p;
30
+ if (p === '~') return home;
31
+ if (p.startsWith('~/')) return `${home}/${p.slice(2)}`;
32
+ return p;
33
+ }
34
+
35
+ export function isAbsoluteLike(p: string): boolean {
36
+ return p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
37
+ }
38
+
39
+ export async function buildWriteArtifact(
40
+ relPath: string,
41
+ existed: boolean,
42
+ oldText: string,
43
+ newText: string,
44
+ ) {
45
+ let patch = '';
46
+ try {
47
+ patch = createTwoFilesPatch(
48
+ `a/${relPath}`,
49
+ `b/${relPath}`,
50
+ String(oldText ?? ''),
51
+ String(newText ?? ''),
52
+ '',
53
+ '',
54
+ { context: 3 },
55
+ );
56
+ } catch {}
57
+ if (!patch || !patch.trim().length) {
58
+ const header = existed ? 'Update File' : 'Add File';
59
+ const oldLines = String(oldText ?? '').split('\n');
60
+ const newLines = String(newText ?? '').split('\n');
61
+ const lines: string[] = [];
62
+ lines.push('*** Begin Patch');
63
+ lines.push(`*** ${header}: ${relPath}`);
64
+ lines.push('@@');
65
+ if (existed) for (const l of oldLines) lines.push(`-${l}`);
66
+ for (const l of newLines) lines.push(`+${l}`);
67
+ lines.push('*** End Patch');
68
+ patch = lines.join('\n');
69
+ }
70
+ const { additions, deletions } = summarizePatchCounts(patch);
71
+ return {
72
+ kind: 'file_diff',
73
+ patch,
74
+ summary: { files: 1, additions, deletions },
75
+ } as const;
76
+ }
77
+
78
+ export function summarizePatchCounts(patch: string): {
79
+ additions: number;
80
+ deletions: number;
81
+ } {
82
+ let adds = 0;
83
+ let dels = 0;
84
+ for (const line of String(patch || '').split('\n')) {
85
+ if (
86
+ line.startsWith('+++') ||
87
+ line.startsWith('---') ||
88
+ line.startsWith('diff ')
89
+ )
90
+ continue;
91
+ if (line.startsWith('+')) adds += 1;
92
+ else if (line.startsWith('-')) dels += 1;
93
+ }
94
+ return { additions: adds, deletions: dels };
95
+ }
@@ -0,0 +1,106 @@
1
+ import { tool, type Tool } from 'ai';
2
+ import { z } from 'zod/v3';
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import {
5
+ buildWriteArtifact,
6
+ resolveSafePath,
7
+ expandTilde,
8
+ isAbsoluteLike,
9
+ } from './util.ts';
10
+ import DESCRIPTION from './write.txt' with { type: 'text' };
11
+ import { createToolError, type ToolResponse } from '../../error.ts';
12
+
13
+ export function buildWriteTool(projectRoot: string): {
14
+ name: string;
15
+ tool: Tool;
16
+ } {
17
+ const write = tool({
18
+ description: DESCRIPTION,
19
+ inputSchema: z.object({
20
+ path: z
21
+ .string()
22
+ .describe(
23
+ 'Relative file path within the project. Writes outside the project are not allowed.',
24
+ ),
25
+ content: z.string().describe('Text content to write'),
26
+ createDirs: z.boolean().optional().default(true),
27
+ }),
28
+ async execute({
29
+ path,
30
+ content,
31
+ createDirs,
32
+ }: {
33
+ path: string;
34
+ content: string;
35
+ createDirs?: boolean;
36
+ }): Promise<
37
+ ToolResponse<{
38
+ path: string;
39
+ bytes: number;
40
+ artifact: unknown;
41
+ }>
42
+ > {
43
+ if (!path || path.trim().length === 0) {
44
+ return createToolError(
45
+ 'Missing required parameter: path',
46
+ 'validation',
47
+ {
48
+ parameter: 'path',
49
+ value: path,
50
+ suggestion: 'Provide a file path to write',
51
+ },
52
+ );
53
+ }
54
+
55
+ const req = expandTilde(path);
56
+ if (isAbsoluteLike(req)) {
57
+ return createToolError(
58
+ `Refusing to write outside project root: ${req}. Use a relative path within the project.`,
59
+ 'permission',
60
+ {
61
+ parameter: 'path',
62
+ value: req,
63
+ suggestion: 'Use a relative path within the project',
64
+ },
65
+ );
66
+ }
67
+ const abs = resolveSafePath(projectRoot, req);
68
+
69
+ try {
70
+ if (createDirs) {
71
+ const dirPath = abs.slice(0, abs.lastIndexOf('/'));
72
+ await mkdir(dirPath, { recursive: true });
73
+ }
74
+ let existed = false;
75
+ let oldText = '';
76
+ try {
77
+ oldText = await readFile(abs, 'utf-8');
78
+ existed = true;
79
+ } catch {}
80
+ await writeFile(abs, content);
81
+ const artifact = await buildWriteArtifact(
82
+ req,
83
+ existed,
84
+ oldText,
85
+ content,
86
+ );
87
+ return {
88
+ ok: true,
89
+ path: req,
90
+ bytes: content.length,
91
+ artifact,
92
+ };
93
+ } catch (error: unknown) {
94
+ return createToolError(
95
+ `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
96
+ 'execution',
97
+ {
98
+ parameter: 'path',
99
+ value: req,
100
+ },
101
+ );
102
+ }
103
+ },
104
+ });
105
+ return { name: 'write', tool: write };
106
+ }
@@ -0,0 +1,11 @@
1
+ - Write text to a file in the workspace
2
+ - Creates the file if it does not exist (when allowed)
3
+ - Only writes within the project root; absolute paths are rejected
4
+ - Returns a compact patch artifact summarizing the change
5
+
6
+ Usage tips:
7
+ - Use for creating NEW files
8
+ - Use when replacing >70% of a file's content (almost complete rewrite)
9
+ - NEVER use for partial/targeted edits - use apply_patch or edit instead
10
+ - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
11
+ - Prefer idempotent writes by providing the full intended content when you do use write
@@ -0,0 +1,6 @@
1
+ - Create a git commit with the provided message
2
+ - Supports `amend` and `signoff` flags
3
+
4
+ Usage tips:
5
+ - Only commit after explicit user approval
6
+ - Consider including scope and brief rationale in the message
@@ -0,0 +1,5 @@
1
+ - Show git diff
2
+ - By default shows staged changes; set `all=true` for full working tree diff
3
+
4
+ Usage tips:
5
+ - Use with `git_status` to preview changes before committing
@@ -0,0 +1,5 @@
1
+ - Show git status in porcelain v1 format summary
2
+ - Reports staged and unstaged counts and a capped raw list
3
+
4
+ Usage tips:
5
+ - Use before committing to understand working tree changes