@ottocode/sdk 0.1.236 → 0.1.242

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.236",
3
+ "version": "0.1.242",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@ const DEFAULTS: {
36
36
  reasoningText: true,
37
37
  reasoningLevel: 'high',
38
38
  fullWidthContent: true,
39
+ autoCompactThresholdTokens: null,
39
40
  },
40
41
  providers: DEFAULT_PROVIDER_SETTINGS,
41
42
  };
@@ -74,11 +74,12 @@ export async function writeDefaults(
74
74
  agent: string;
75
75
  provider: ProviderId;
76
76
  model: string;
77
- toolApproval: 'auto' | 'dangerous' | 'all';
77
+ toolApproval: 'auto' | 'dangerous' | 'all' | 'yolo';
78
78
  guidedMode: boolean;
79
79
  reasoningText: boolean;
80
80
  reasoningLevel: 'minimal' | 'low' | 'medium' | 'high' | 'max' | 'xhigh';
81
81
  theme: string;
82
+ autoCompactThresholdTokens: number | null;
82
83
  }>,
83
84
  projectRoot?: string,
84
85
  ) {
@@ -0,0 +1,83 @@
1
+ export function normalizeLineEndings(text: string): string {
2
+ return text.replace(/\r\n/g, '\n');
3
+ }
4
+
5
+ export function detectLineEnding(text: string): '\n' | '\r\n' {
6
+ return text.includes('\r\n') ? '\r\n' : '\n';
7
+ }
8
+
9
+ export function convertToLineEnding(
10
+ text: string,
11
+ lineEnding: '\n' | '\r\n',
12
+ ): string {
13
+ if (lineEnding === '\n') return text;
14
+ return text.replace(/\n/g, '\r\n');
15
+ }
16
+
17
+ function countOccurrences(content: string, search: string): number {
18
+ if (!search) return 0;
19
+ let count = 0;
20
+ let start = 0;
21
+ while (true) {
22
+ const index = content.indexOf(search, start);
23
+ if (index === -1) return count;
24
+ count += 1;
25
+ start = index + search.length;
26
+ }
27
+ }
28
+
29
+ export function applyStringEdit(
30
+ content: string,
31
+ oldString: string,
32
+ newString: string,
33
+ replaceAll = false,
34
+ ): { content: string; occurrences: number } {
35
+ if (oldString.length === 0) {
36
+ throw new Error(
37
+ 'oldString must not be empty. Use write to create files or apply_patch for structural insertions.',
38
+ );
39
+ }
40
+ if (oldString === newString) {
41
+ throw new Error(
42
+ 'No changes to apply: oldString and newString are identical.',
43
+ );
44
+ }
45
+
46
+ const lineEnding = detectLineEnding(content);
47
+ const normalizedOld = convertToLineEnding(
48
+ normalizeLineEndings(oldString),
49
+ lineEnding,
50
+ );
51
+ const normalizedNew = convertToLineEnding(
52
+ normalizeLineEndings(newString),
53
+ lineEnding,
54
+ );
55
+
56
+ const occurrences = countOccurrences(content, normalizedOld);
57
+ if (occurrences === 0) {
58
+ throw new Error(
59
+ 'oldString not found in content. Read the file again and copy the exact text, including whitespace.',
60
+ );
61
+ }
62
+ if (occurrences > 1 && !replaceAll) {
63
+ throw new Error(
64
+ 'Found multiple matches for oldString. Provide more surrounding lines to make it unique or set replaceAll to true.',
65
+ );
66
+ }
67
+
68
+ if (replaceAll) {
69
+ return {
70
+ content: content.split(normalizedOld).join(normalizedNew),
71
+ occurrences,
72
+ };
73
+ }
74
+
75
+ const index = content.indexOf(normalizedOld);
76
+ return {
77
+ content:
78
+ content.slice(0, index) +
79
+ normalizedNew +
80
+ content.slice(index + normalizedOld.length),
81
+ occurrences,
82
+ };
83
+ }
@@ -0,0 +1,129 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { tool, type Tool } from 'ai';
3
+ import { z } from 'zod/v3';
4
+ import DESCRIPTION from './edit.txt' with { type: 'text' };
5
+ import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
6
+ import { applyStringEdit } from './edit-shared.ts';
7
+ import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
9
+
10
+ export function buildEditTool(projectRoot: string): {
11
+ name: string;
12
+ tool: Tool;
13
+ } {
14
+ const edit = tool({
15
+ description: DESCRIPTION,
16
+ inputSchema: z.object({
17
+ path: z
18
+ .string()
19
+ .describe(
20
+ 'Relative file path within the project. Absolute paths are not allowed.',
21
+ ),
22
+ oldString: z.string().describe('Exact text to replace'),
23
+ newString: z.string().describe('Replacement text'),
24
+ replaceAll: z
25
+ .boolean()
26
+ .optional()
27
+ .default(false)
28
+ .describe(
29
+ 'Replace every matching occurrence instead of requiring a unique match',
30
+ ),
31
+ }),
32
+ async execute({
33
+ path,
34
+ oldString,
35
+ newString,
36
+ replaceAll = false,
37
+ }: {
38
+ path: string;
39
+ oldString: string;
40
+ newString: string;
41
+ replaceAll?: boolean;
42
+ }): Promise<
43
+ ToolResponse<{
44
+ path: string;
45
+ occurrences: number;
46
+ bytes: number;
47
+ artifact: unknown;
48
+ }>
49
+ > {
50
+ if (!path || path.trim().length === 0) {
51
+ return createToolError(
52
+ 'Missing required parameter: path',
53
+ 'validation',
54
+ {
55
+ parameter: 'path',
56
+ value: path,
57
+ suggestion: 'Provide a relative file path to edit',
58
+ },
59
+ );
60
+ }
61
+ if (isAbsoluteLike(path)) {
62
+ return createToolError(
63
+ `Refusing to edit outside project root: ${path}`,
64
+ 'permission',
65
+ {
66
+ parameter: 'path',
67
+ value: path,
68
+ suggestion: 'Use a relative path within the project',
69
+ },
70
+ );
71
+ }
72
+
73
+ const abs = resolveSafePath(projectRoot, path);
74
+ try {
75
+ await assertFreshRead(projectRoot, abs, path);
76
+ const original = await readFile(abs, 'utf-8');
77
+ const updated = applyStringEdit(
78
+ original,
79
+ oldString,
80
+ newString,
81
+ replaceAll,
82
+ );
83
+ if (updated.content === original) {
84
+ return createToolError('No changes applied.', 'validation', {
85
+ suggestion:
86
+ 'Adjust oldString/newString so the file content actually changes',
87
+ });
88
+ }
89
+
90
+ await writeFile(abs, updated.content, 'utf-8');
91
+ await rememberFileWrite(projectRoot, abs);
92
+ const artifact = await buildWriteArtifact(
93
+ path,
94
+ true,
95
+ original,
96
+ updated.content,
97
+ );
98
+ return {
99
+ ok: true,
100
+ path,
101
+ occurrences: updated.occurrences,
102
+ bytes: updated.content.length,
103
+ artifact,
104
+ };
105
+ } catch (error: unknown) {
106
+ const isEnoent =
107
+ error &&
108
+ typeof error === 'object' &&
109
+ 'code' in error &&
110
+ error.code === 'ENOENT';
111
+ return createToolError(
112
+ isEnoent
113
+ ? `File not found: ${path}`
114
+ : `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
115
+ isEnoent ? 'not_found' : 'execution',
116
+ {
117
+ parameter: 'path',
118
+ value: path,
119
+ suggestion: isEnoent
120
+ ? 'Use read or ls to confirm the file path first'
121
+ : undefined,
122
+ },
123
+ );
124
+ }
125
+ },
126
+ });
127
+
128
+ return { name: 'edit', tool: edit };
129
+ }
@@ -0,0 +1,9 @@
1
+ Replace an exact text block in an existing file.
2
+
3
+ Use this for targeted edits instead of apply_patch whenever possible.
4
+
5
+ Rules:
6
+ - You must read the file first in the current session before editing it.
7
+ - `oldString` must match the file exactly, including whitespace.
8
+ - If `oldString` appears multiple times, provide more context or set `replaceAll: true`.
9
+ - Use `write` for new files and `apply_patch` for structural multi-file diffs.
@@ -1,5 +1,7 @@
1
1
  import type { Tool } from 'ai';
2
+ import { buildEditTool } from './edit.ts';
2
3
  import { buildReadTool } from './read.ts';
4
+ import { buildMultiEditTool } from './multiedit.ts';
3
5
  import { buildWriteTool } from './write.ts';
4
6
  import { buildLsTool } from './ls.ts';
5
7
  import { buildTreeTool } from './tree.ts';
@@ -11,6 +13,8 @@ export function buildFsTools(
11
13
  ): Array<{ name: string; tool: Tool }> {
12
14
  const out: Array<{ name: string; tool: Tool }> = [];
13
15
  out.push(buildReadTool(projectRoot));
16
+ out.push(buildEditTool(projectRoot));
17
+ out.push(buildMultiEditTool(projectRoot));
14
18
  out.push(buildWriteTool(projectRoot));
15
19
  out.push(buildLsTool(projectRoot));
16
20
  out.push(buildTreeTool(projectRoot));
@@ -0,0 +1,137 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { tool, type Tool } from 'ai';
3
+ import { z } from 'zod/v3';
4
+ import DESCRIPTION from './multiedit.txt' with { type: 'text' };
5
+ import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
6
+ import { applyStringEdit } from './edit-shared.ts';
7
+ import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
8
+ import { createToolError, type ToolResponse } from '../../error.ts';
9
+
10
+ const multiEditSchema = z.object({
11
+ path: z
12
+ .string()
13
+ .describe(
14
+ 'Relative file path within the project. Absolute paths are not allowed.',
15
+ ),
16
+ edits: z
17
+ .array(
18
+ z.object({
19
+ oldString: z.string().describe('Exact text to replace'),
20
+ newString: z.string().describe('Replacement text'),
21
+ replaceAll: z
22
+ .boolean()
23
+ .optional()
24
+ .default(false)
25
+ .describe('Replace every matching occurrence for this edit'),
26
+ }),
27
+ )
28
+ .min(1)
29
+ .describe('Edits to apply sequentially to the same file'),
30
+ });
31
+
32
+ export function buildMultiEditTool(projectRoot: string): {
33
+ name: string;
34
+ tool: Tool;
35
+ } {
36
+ const multiedit = tool({
37
+ description: DESCRIPTION,
38
+ inputSchema: multiEditSchema,
39
+ async execute({ path, edits }: z.infer<typeof multiEditSchema>): Promise<
40
+ ToolResponse<{
41
+ path: string;
42
+ editsApplied: number;
43
+ bytes: number;
44
+ artifact: unknown;
45
+ }>
46
+ > {
47
+ if (!path || path.trim().length === 0) {
48
+ return createToolError(
49
+ 'Missing required parameter: path',
50
+ 'validation',
51
+ {
52
+ parameter: 'path',
53
+ value: path,
54
+ suggestion: 'Provide a relative file path to edit',
55
+ },
56
+ );
57
+ }
58
+ if (isAbsoluteLike(path)) {
59
+ return createToolError(
60
+ `Refusing to edit outside project root: ${path}`,
61
+ 'permission',
62
+ {
63
+ parameter: 'path',
64
+ value: path,
65
+ suggestion: 'Use a relative path within the project',
66
+ },
67
+ );
68
+ }
69
+
70
+ const abs = resolveSafePath(projectRoot, path);
71
+ try {
72
+ await assertFreshRead(projectRoot, abs, path);
73
+ const original = await readFile(abs, 'utf-8');
74
+ let nextContent = original;
75
+ for (let i = 0; i < edits.length; i++) {
76
+ const edit = edits[i];
77
+ try {
78
+ nextContent = applyStringEdit(
79
+ nextContent,
80
+ edit.oldString,
81
+ edit.newString,
82
+ edit.replaceAll,
83
+ ).content;
84
+ } catch (error: unknown) {
85
+ throw new Error(
86
+ `Edit ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`,
87
+ );
88
+ }
89
+ }
90
+
91
+ if (nextContent === original) {
92
+ return createToolError('No changes applied.', 'validation', {
93
+ suggestion:
94
+ 'Adjust your edits so the file content actually changes',
95
+ });
96
+ }
97
+
98
+ await writeFile(abs, nextContent, 'utf-8');
99
+ await rememberFileWrite(projectRoot, abs);
100
+ const artifact = await buildWriteArtifact(
101
+ path,
102
+ true,
103
+ original,
104
+ nextContent,
105
+ );
106
+ return {
107
+ ok: true,
108
+ path,
109
+ editsApplied: edits.length,
110
+ bytes: nextContent.length,
111
+ artifact,
112
+ };
113
+ } catch (error: unknown) {
114
+ const isEnoent =
115
+ error &&
116
+ typeof error === 'object' &&
117
+ 'code' in error &&
118
+ error.code === 'ENOENT';
119
+ return createToolError(
120
+ isEnoent
121
+ ? `File not found: ${path}`
122
+ : `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
123
+ isEnoent ? 'not_found' : 'execution',
124
+ {
125
+ parameter: 'path',
126
+ value: path,
127
+ suggestion: isEnoent
128
+ ? 'Use read or ls to confirm the file path first'
129
+ : undefined,
130
+ },
131
+ );
132
+ }
133
+ },
134
+ });
135
+
136
+ return { name: 'multiedit', tool: multiedit };
137
+ }
@@ -0,0 +1,9 @@
1
+ Apply multiple exact text replacements to a single existing file atomically.
2
+
3
+ Use this when you need several edits in one file.
4
+
5
+ Rules:
6
+ - Read the file first before editing.
7
+ - Each edit uses exact `oldString` -> `newString` replacement.
8
+ - All edits must succeed or none are written.
9
+ - Use `replaceAll: true` only when every occurrence should change.
@@ -0,0 +1,72 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { resolve as resolvePath } from 'node:path';
3
+
4
+ type FileStamp = {
5
+ readAt: number;
6
+ mtimeMs?: number;
7
+ ctimeMs?: number;
8
+ size?: number;
9
+ };
10
+
11
+ const readState = new Map<string, Map<string, FileStamp>>();
12
+
13
+ function getProjectState(projectRoot: string): Map<string, FileStamp> {
14
+ const key = resolvePath(projectRoot);
15
+ let state = readState.get(key);
16
+ if (!state) {
17
+ state = new Map<string, FileStamp>();
18
+ readState.set(key, state);
19
+ }
20
+ return state;
21
+ }
22
+
23
+ async function captureFileStamp(absPath: string): Promise<FileStamp> {
24
+ const stats = await stat(absPath);
25
+ return {
26
+ readAt: Date.now(),
27
+ mtimeMs: Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : undefined,
28
+ ctimeMs: Number.isFinite(stats.ctimeMs) ? stats.ctimeMs : undefined,
29
+ size: typeof stats.size === 'number' ? stats.size : undefined,
30
+ };
31
+ }
32
+
33
+ export async function rememberFileRead(
34
+ projectRoot: string,
35
+ absPath: string,
36
+ ): Promise<void> {
37
+ const state = getProjectState(projectRoot);
38
+ state.set(absPath, await captureFileStamp(absPath));
39
+ }
40
+
41
+ export async function rememberFileWrite(
42
+ projectRoot: string,
43
+ absPath: string,
44
+ ): Promise<void> {
45
+ const state = getProjectState(projectRoot);
46
+ state.set(absPath, await captureFileStamp(absPath));
47
+ }
48
+
49
+ export async function assertFreshRead(
50
+ projectRoot: string,
51
+ absPath: string,
52
+ displayPath: string,
53
+ ): Promise<void> {
54
+ const state = getProjectState(projectRoot);
55
+ const previous = state.get(absPath);
56
+ if (!previous) {
57
+ throw new Error(
58
+ `You must read file ${displayPath} before editing it. Use the read tool first.`,
59
+ );
60
+ }
61
+
62
+ const current = await captureFileStamp(absPath);
63
+ const changed =
64
+ current.mtimeMs !== previous.mtimeMs ||
65
+ current.ctimeMs !== previous.ctimeMs ||
66
+ current.size !== previous.size;
67
+ if (!changed) return;
68
+
69
+ throw new Error(
70
+ `File ${displayPath} has changed since it was last read. Read it again before editing.`,
71
+ );
72
+ }
@@ -2,6 +2,7 @@ import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
5
+ import { rememberFileRead } from './read-tracker.ts';
5
6
  import DESCRIPTION from './read.txt' with { type: 'text' };
6
7
  import { createToolError, type ToolResponse } from '../../error.ts';
7
8
 
@@ -108,6 +109,7 @@ export function buildReadTool(projectRoot: string): {
108
109
  try {
109
110
  let content = await readFile(abs, 'utf-8');
110
111
  const indent = detectIndentation(content);
112
+ await rememberFileRead(projectRoot, abs);
111
113
 
112
114
  if (startLine !== undefined && endLine !== undefined) {
113
115
  const lines = content.split('\n');
@@ -8,6 +8,7 @@ import {
8
8
  expandTilde,
9
9
  isAbsoluteLike,
10
10
  } from './util.ts';
11
+ import { rememberFileWrite } from './read-tracker.ts';
11
12
  import DESCRIPTION from './write.txt' with { type: 'text' };
12
13
  import { createToolError, type ToolResponse } from '../../error.ts';
13
14
 
@@ -79,6 +80,7 @@ export function buildWriteTool(projectRoot: string): {
79
80
  existed = true;
80
81
  } catch {}
81
82
  await writeFile(abs, content);
83
+ await rememberFileWrite(projectRoot, abs);
82
84
  const artifact = await buildWriteArtifact(
83
85
  req,
84
86
  existed,
@@ -6,6 +6,6 @@
6
6
  Usage tips:
7
7
  - Use for creating NEW files
8
8
  - Use when replacing >70% of a file's content (almost complete rewrite)
9
- - NEVER use for partial/targeted edits - use apply_patch instead
9
+ - NEVER use for partial/targeted edits - use edit/multiedit first, or apply_patch for structural changes
10
10
  - Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
11
11
  - Prefer idempotent writes by providing the full intended content when you do use write
@@ -12,6 +12,13 @@ You help with coding and build tasks.
12
12
  - Use `terminal(operation: "write", input: "\u0003")` or `terminal(operation: "interrupt")` to stop a process before resorting to `kill`.
13
13
  - Summarize active terminals (purpose, key command, port) in your updates so collaborators know what's running.
14
14
 
15
+ ## Preferred Editing Order
16
+
17
+ - Use `edit` for one exact replacement in an existing file.
18
+ - Use `multiedit` for several exact replacements in the same file.
19
+ - Use `apply_patch` for structural or multi-file changes, file add/delete/rename, or edits that are awkward as exact replacements.
20
+ - Use `write` for new files or near-total rewrites.
21
+
15
22
  ## apply_patch — Mandatory Rules
16
23
 
17
24
  These rules apply EVERY time you use the `apply_patch` tool. Violations cause patch failures.
@@ -211,7 +218,7 @@ Key points:
211
218
  - Wastes output tokens and risks hallucinating unchanged parts
212
219
 
213
220
  ## Never
214
- - Use `write` for partial file edits (use `apply_patch` instead)
221
+ - Use `write` for partial file edits (use `edit`/`multiedit` first, or `apply_patch` for structural diffs)
215
222
  - Make multiple separate `apply_patch` calls for the same file (use multiple hunks with @@ headers instead)
216
223
  - Assume file content remains unchanged between operations
217
- - Use `bash` with `sed`/`awk` for programmatic file editing (use `apply_patch` instead)
224
+ - Use `bash` with `sed`/`awk` for programmatic file editing (use `edit`, `multiedit`, or `apply_patch` instead)
@@ -0,0 +1,24 @@
1
+ You are the init agent.
2
+
3
+ Your sole purpose is to generate or refresh repository agent documentation for future coding agents.
4
+
5
+ ## Core behavior
6
+
7
+ - Inspect the real repository structure before writing anything.
8
+ - Trust code, config, manifests, routes, schemas, and app structure more than existing markdown.
9
+ - Prefer a root `AGENTS.md` that acts as the routing/index doc.
10
+ - Create `.agents/*.md` docs only when they represent meaningfully distinct areas.
11
+ - Prefer a few strong docs over many tiny docs.
12
+ - Keep docs practical: architecture, important paths, workflow rules, and when to consult related docs.
13
+
14
+ ## For monorepos
15
+
16
+ - Detect workspace boundaries and package responsibilities.
17
+ - Root `AGENTS.md` should tell future agents which `.agents` doc to read for tasks involving web, TUI, mobile, server/API, database, SDK/shared packages, or cross-cutting changes.
18
+ - Avoid splitting docs unless the codebase clearly has distinct domains.
19
+
20
+ ## Output expectations
21
+
22
+ - Update existing agent docs when appropriate instead of duplicating them.
23
+ - Mention concrete file paths and package names.
24
+ - Keep the final user-facing summary concise and specific about what was generated.
@@ -21,10 +21,13 @@ You MUST call the `finish` tool at the end of every response to signal completio
21
21
  **IMPORTANT**: Do NOT call `finish` before streaming your response. Always stream your message first, then call `finish`. If you forget to call `finish`, the system will hang and not complete properly.
22
22
 
23
23
  File Editing Best Practices:
24
- - ALWAYS read a file immediately before using apply_patch never patch from memory
25
- - The `read` tool returns an `indentation` field (e.g., "tabs", "2 spaces") — use it to match the file's indentation style in your patch
26
- - Copy context lines CHARACTER-FOR-CHARACTER from the read output never reconstruct from memory
27
- - When making multiple edits to the same file, use multiple `@@` hunks in a single `apply_patch` call
28
- - Never assume file content remains unchanged between separate apply_patch operations
29
- - If a patch fails, read the file AGAIN and copy the exact lines
30
- - If `apply_patch` fails repeatedly on the same file, fall back to `write` only when a full-file rewrite is appropriate
24
+ - Prefer `edit` for one exact replacement in an existing file
25
+ - Prefer `multiedit` for several exact replacements in the same file
26
+ - Use `apply_patch` for structural diffs, file add/delete/rename, or changes that are awkward as exact replacements
27
+ - ALWAYS read a file immediately before using `edit`, `multiedit`, or `apply_patch`
28
+ - For `edit` / `multiedit`, copy the exact text including whitespace from the latest `read` output
29
+ - For `apply_patch`, use the `indentation` field from `read` and copy context lines CHARACTER-FOR-CHARACTER
30
+ - When making multiple edits to the same file with `apply_patch`, use multiple `@@` hunks in a single call
31
+ - Never assume file content remains unchanged between separate edit operations
32
+ - If an edit tool fails, read the file AGAIN and copy the exact lines
33
+ - If `apply_patch` fails repeatedly on the same file, prefer `edit` / `multiedit` when possible and fall back to `write` only for a full-file rewrite