@agi-cli/sdk 0.1.81 → 0.1.83

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.
@@ -0,0 +1,209 @@
1
+ import {
2
+ PATCH_ADD_PREFIX,
3
+ PATCH_BEGIN_MARKER,
4
+ PATCH_DELETE_PREFIX,
5
+ PATCH_END_MARKER,
6
+ PATCH_UPDATE_PREFIX,
7
+ } from './constants.ts';
8
+ import type {
9
+ PatchAddOperation,
10
+ PatchDeleteOperation,
11
+ PatchHunk,
12
+ PatchHunkLine,
13
+ PatchOperation,
14
+ PatchUpdateOperation,
15
+ } from './types.ts';
16
+
17
+ function parseDirectivePath(line: string, prefix: string): string {
18
+ const filePath = line.slice(prefix.length).trim();
19
+ if (!filePath) {
20
+ throw new Error(`Missing file path for directive: ${line}`);
21
+ }
22
+ if (filePath.startsWith('/') || filePath.includes('..')) {
23
+ throw new Error('Patch file paths must be relative to the project root.');
24
+ }
25
+ return filePath;
26
+ }
27
+
28
+ function parseHunkHeader(raw: string) {
29
+ const match = raw.match(
30
+ /^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
31
+ );
32
+ if (match) {
33
+ const [, oldStart, oldCount, newStart, newCount, context] = match;
34
+ return {
35
+ oldStart: Number.parseInt(oldStart, 10),
36
+ oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
37
+ newStart: Number.parseInt(newStart, 10),
38
+ newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
39
+ context: context?.trim() || undefined,
40
+ };
41
+ }
42
+ const context = raw.replace(/^@@/, '').trim();
43
+ return context ? { context } : {};
44
+ }
45
+
46
+ export function parseEnvelopedPatch(patch: string): PatchOperation[] {
47
+ const normalized = patch.replace(/\r\n/g, '\n');
48
+ const lines = normalized.split('\n');
49
+ const operations: PatchOperation[] = [];
50
+
51
+ type Builder =
52
+ | (PatchAddOperation & { kind: 'add' })
53
+ | (PatchDeleteOperation & { kind: 'delete' })
54
+ | (PatchUpdateOperation & {
55
+ kind: 'update';
56
+ currentHunk: PatchHunk | null;
57
+ });
58
+
59
+ let builder: Builder | null = null;
60
+ let inside = false;
61
+ let encounteredEnd = false;
62
+
63
+ const flushBuilder = () => {
64
+ if (!builder) return;
65
+ if (builder.kind === 'update') {
66
+ if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
67
+ builder.hunks.pop();
68
+ }
69
+ if (builder.hunks.length === 0) {
70
+ throw new Error(
71
+ `Update for ${builder.filePath} does not contain any diff hunks.`,
72
+ );
73
+ }
74
+ operations.push({
75
+ kind: 'update',
76
+ filePath: builder.filePath,
77
+ hunks: builder.hunks.map((hunk) => ({
78
+ header: { ...hunk.header },
79
+ lines: hunk.lines.map((line) => ({ ...line })),
80
+ })),
81
+ });
82
+ } else if (builder.kind === 'add') {
83
+ operations.push({
84
+ kind: 'add',
85
+ filePath: builder.filePath,
86
+ lines: [...builder.lines],
87
+ });
88
+ } else {
89
+ operations.push({ kind: 'delete', filePath: builder.filePath });
90
+ }
91
+ builder = null;
92
+ };
93
+
94
+ for (let i = 0; i < lines.length; i++) {
95
+ const line = lines[i];
96
+
97
+ if (!inside) {
98
+ if (line.trim() === '') continue;
99
+ if (line.startsWith(PATCH_BEGIN_MARKER)) {
100
+ inside = true;
101
+ continue;
102
+ }
103
+ throw new Error(
104
+ 'Patch must start with "*** Begin Patch" and use the enveloped patch format.',
105
+ );
106
+ }
107
+
108
+ if (line.startsWith(PATCH_BEGIN_MARKER)) {
109
+ throw new Error('Nested "*** Begin Patch" markers are not supported.');
110
+ }
111
+
112
+ if (line.startsWith(PATCH_END_MARKER)) {
113
+ flushBuilder();
114
+ encounteredEnd = true;
115
+ const remaining = lines.slice(i + 1).find((rest) => rest.trim() !== '');
116
+ if (remaining) {
117
+ throw new Error(
118
+ 'Unexpected content found after "*** End Patch" marker.',
119
+ );
120
+ }
121
+ break;
122
+ }
123
+
124
+ if (line.startsWith(PATCH_ADD_PREFIX)) {
125
+ flushBuilder();
126
+ builder = {
127
+ kind: 'add',
128
+ filePath: parseDirectivePath(line, PATCH_ADD_PREFIX),
129
+ lines: [],
130
+ };
131
+ continue;
132
+ }
133
+
134
+ if (line.startsWith(PATCH_UPDATE_PREFIX)) {
135
+ flushBuilder();
136
+ builder = {
137
+ kind: 'update',
138
+ filePath: parseDirectivePath(line, PATCH_UPDATE_PREFIX),
139
+ hunks: [],
140
+ currentHunk: null,
141
+ };
142
+ continue;
143
+ }
144
+
145
+ if (line.startsWith(PATCH_DELETE_PREFIX)) {
146
+ flushBuilder();
147
+ builder = {
148
+ kind: 'delete',
149
+ filePath: parseDirectivePath(line, PATCH_DELETE_PREFIX),
150
+ };
151
+ continue;
152
+ }
153
+
154
+ if (!builder) {
155
+ if (line.trim() === '') continue;
156
+ throw new Error(`Unexpected content in patch: "${line}"`);
157
+ }
158
+
159
+ if (builder.kind === 'add') {
160
+ builder.lines.push(line.startsWith('+') ? line.slice(1) : line);
161
+ continue;
162
+ }
163
+
164
+ if (builder.kind === 'delete') {
165
+ if (line.trim() !== '') {
166
+ throw new Error(
167
+ `Delete directive for ${builder.filePath} should not contain additional lines.`,
168
+ );
169
+ }
170
+ continue;
171
+ }
172
+
173
+ if (line.startsWith('@@')) {
174
+ const hunk: PatchHunk = { header: parseHunkHeader(line), lines: [] };
175
+ builder.hunks.push(hunk);
176
+ builder.currentHunk = hunk;
177
+ continue;
178
+ }
179
+
180
+ if (!builder.currentHunk) {
181
+ const fallback: PatchHunk = { header: {}, lines: [] };
182
+ builder.hunks.push(fallback);
183
+ builder.currentHunk = fallback;
184
+ }
185
+
186
+ const hunk = builder.currentHunk;
187
+ const prefix = line[0];
188
+ const createLine = (kind: PatchHunkLine['kind'], content: string) => ({
189
+ kind,
190
+ content,
191
+ });
192
+
193
+ if (prefix === '+') {
194
+ hunk.lines.push(createLine('add', line.slice(1)));
195
+ } else if (prefix === '-') {
196
+ hunk.lines.push(createLine('remove', line.slice(1)));
197
+ } else if (prefix === ' ') {
198
+ hunk.lines.push(createLine('context', line.slice(1)));
199
+ } else {
200
+ hunk.lines.push(createLine('context', line));
201
+ }
202
+ }
203
+
204
+ if (!encounteredEnd) {
205
+ throw new Error('Missing "*** End Patch" marker.');
206
+ }
207
+
208
+ return operations;
209
+ }
@@ -0,0 +1,231 @@
1
+ import type {
2
+ PatchAddOperation,
3
+ PatchDeleteOperation,
4
+ PatchHunk,
5
+ PatchHunkLine,
6
+ PatchOperation,
7
+ PatchUpdateOperation,
8
+ } from './types.ts';
9
+
10
+ function stripPath(raw: string): string | null {
11
+ let trimmed = raw.trim();
12
+ if (!trimmed || trimmed === '/dev/null') return null;
13
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
14
+ trimmed = trimmed.slice(1, -1).replace(/\\"/g, '"');
15
+ }
16
+ if (trimmed.startsWith('a/') || trimmed.startsWith('b/')) {
17
+ trimmed = trimmed.slice(2);
18
+ }
19
+ if (trimmed.startsWith('./')) {
20
+ trimmed = trimmed.slice(2);
21
+ }
22
+ return trimmed || null;
23
+ }
24
+
25
+ function parseHunkHeader(raw: string) {
26
+ const match = raw.match(
27
+ /^@@\s*-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s*@@(?:\s*(.*))?$/,
28
+ );
29
+ if (match) {
30
+ const [, oldStart, oldCount, newStart, newCount, context] = match;
31
+ return {
32
+ oldStart: Number.parseInt(oldStart, 10),
33
+ oldLines: oldCount ? Number.parseInt(oldCount, 10) : undefined,
34
+ newStart: Number.parseInt(newStart, 10),
35
+ newLines: newCount ? Number.parseInt(newCount, 10) : undefined,
36
+ context: context?.trim() || undefined,
37
+ };
38
+ }
39
+ const context = raw.replace(/^@@/, '').trim();
40
+ return context ? { context } : {};
41
+ }
42
+
43
+ function shouldIgnoreMetadata(line: string) {
44
+ const trimmed = line.trim();
45
+ if (trimmed === '') return true;
46
+ if (trimmed === '\') return true;
47
+ const prefixes = [
48
+ 'diff --git',
49
+ 'index ',
50
+ 'similarity index',
51
+ 'dissimilarity index',
52
+ 'rename from',
53
+ 'rename to',
54
+ 'copy from',
55
+ 'copy to',
56
+ 'new file mode',
57
+ 'deleted file mode',
58
+ 'old mode',
59
+ 'new mode',
60
+ 'Binary files',
61
+ ];
62
+ return prefixes.some((prefix) => trimmed.startsWith(prefix));
63
+ }
64
+
65
+ export function parseUnifiedPatch(patch: string): PatchOperation[] {
66
+ const normalized = patch.replace(/\r\n/g, '\n');
67
+ const lines = normalized.split('\n');
68
+ const operations: PatchOperation[] = [];
69
+
70
+ type Builder =
71
+ | (PatchAddOperation & { kind: 'add' })
72
+ | (PatchDeleteOperation & { kind: 'delete' })
73
+ | (PatchUpdateOperation & {
74
+ kind: 'update';
75
+ currentHunk: PatchHunk | null;
76
+ });
77
+
78
+ let builder: Builder | null = null;
79
+
80
+ const flush = () => {
81
+ if (!builder) return;
82
+ if (builder.kind === 'update') {
83
+ if (builder.currentHunk && builder.currentHunk.lines.length === 0) {
84
+ builder.hunks.pop();
85
+ }
86
+ if (builder.hunks.length === 0) {
87
+ throw new Error(
88
+ `Update for ${builder.filePath} does not contain any diff hunks.`,
89
+ );
90
+ }
91
+ operations.push({
92
+ kind: 'update',
93
+ filePath: builder.filePath,
94
+ hunks: builder.hunks.map((hunk) => ({
95
+ header: { ...hunk.header },
96
+ lines: hunk.lines.map((line) => ({ ...line })),
97
+ })),
98
+ });
99
+ } else if (builder.kind === 'add') {
100
+ operations.push({
101
+ kind: 'add',
102
+ filePath: builder.filePath,
103
+ lines: [...builder.lines],
104
+ });
105
+ } else {
106
+ operations.push({ kind: 'delete', filePath: builder.filePath });
107
+ }
108
+ builder = null;
109
+ };
110
+
111
+ for (let i = 0; i < lines.length; i++) {
112
+ const line = lines[i];
113
+
114
+ if (line.startsWith('--- ')) {
115
+ const oldPathRaw = line.slice(4);
116
+ const next = lines[i + 1];
117
+ if (!next?.startsWith('+++ ')) {
118
+ throw new Error(
119
+ 'Invalid unified diff: expected "+++ <path>" after "--- <path>"',
120
+ );
121
+ }
122
+ const newPathRaw = next.slice(4);
123
+ i += 1;
124
+
125
+ flush();
126
+
127
+ const oldPath = stripPath(oldPathRaw);
128
+ const newPath = stripPath(newPathRaw);
129
+
130
+ if (!oldPath && !newPath) {
131
+ throw new Error(
132
+ 'Invalid unified diff: missing file paths after ---/+++ headers.',
133
+ );
134
+ }
135
+
136
+ if (!oldPath) {
137
+ if (!newPath) {
138
+ throw new Error(
139
+ 'Invalid unified diff: missing target path for added file.',
140
+ );
141
+ }
142
+ builder = {
143
+ kind: 'add',
144
+ filePath: newPath,
145
+ lines: [],
146
+ };
147
+ continue;
148
+ }
149
+
150
+ if (!newPath) {
151
+ builder = {
152
+ kind: 'delete',
153
+ filePath: oldPath,
154
+ };
155
+ continue;
156
+ }
157
+
158
+ if (oldPath !== newPath) {
159
+ throw new Error(
160
+ `Renames are not supported in apply_patch. Old path: ${oldPath}, new path: ${newPath}`,
161
+ );
162
+ }
163
+
164
+ builder = {
165
+ kind: 'update',
166
+ filePath: newPath,
167
+ hunks: [],
168
+ currentHunk: null,
169
+ };
170
+ continue;
171
+ }
172
+
173
+ if (!builder) {
174
+ if (shouldIgnoreMetadata(line)) continue;
175
+ if (line.trim() === '') continue;
176
+ throw new Error(`Unrecognized content in patch: "${line}"`);
177
+ }
178
+
179
+ if (builder.kind === 'add') {
180
+ if (shouldIgnoreMetadata(line) || line.startsWith('@@')) continue;
181
+ if (line.startsWith('+')) {
182
+ builder.lines.push(line.slice(1));
183
+ }
184
+ continue;
185
+ }
186
+
187
+ if (builder.kind === 'delete') {
188
+ continue;
189
+ }
190
+
191
+ if (shouldIgnoreMetadata(line)) continue;
192
+
193
+ if (line.startsWith('@@')) {
194
+ const hunk: PatchHunk = { header: parseHunkHeader(line), lines: [] };
195
+ builder.hunks.push(hunk);
196
+ builder.currentHunk = hunk;
197
+ continue;
198
+ }
199
+
200
+ if (!builder.currentHunk) {
201
+ const fallback: PatchHunk = { header: {}, lines: [] };
202
+ builder.hunks.push(fallback);
203
+ builder.currentHunk = fallback;
204
+ }
205
+
206
+ const hunk = builder.currentHunk;
207
+ const prefix = line[0];
208
+ const getLine = (kind: PatchHunkLine['kind'], content: string) => ({
209
+ kind,
210
+ content,
211
+ });
212
+
213
+ if (prefix === '+') {
214
+ hunk.lines.push(getLine('add', line.slice(1)));
215
+ } else if (prefix === '-') {
216
+ hunk.lines.push(getLine('remove', line.slice(1)));
217
+ } else if (prefix === ' ') {
218
+ hunk.lines.push(getLine('context', line.slice(1)));
219
+ } else {
220
+ hunk.lines.push(getLine('context', line));
221
+ }
222
+ }
223
+
224
+ flush();
225
+
226
+ if (operations.length === 0) {
227
+ throw new Error('No operations found in unified diff.');
228
+ }
229
+
230
+ return operations;
231
+ }
@@ -0,0 +1,28 @@
1
+ import { PATCH_BEGIN_MARKER } from './constants.ts';
2
+ import { parseEnvelopedPatch } from './parse-enveloped.ts';
3
+ import { parseUnifiedPatch } from './parse-unified.ts';
4
+ import type { PatchOperation } from './types.ts';
5
+
6
+ export type PatchFormat = 'enveloped' | 'unified';
7
+
8
+ export function parsePatchInput(patch: string): {
9
+ format: PatchFormat;
10
+ operations: PatchOperation[];
11
+ } {
12
+ const trimmed = patch.trim();
13
+ if (!trimmed) {
14
+ throw new Error('Patch content is empty.');
15
+ }
16
+
17
+ if (trimmed.includes(PATCH_BEGIN_MARKER)) {
18
+ return {
19
+ format: 'enveloped',
20
+ operations: parseEnvelopedPatch(patch),
21
+ };
22
+ }
23
+
24
+ return {
25
+ format: 'unified',
26
+ operations: parseUnifiedPatch(patch),
27
+ };
28
+ }
@@ -0,0 +1,23 @@
1
+ export function splitLines(value: string): {
2
+ lines: string[];
3
+ newline: string;
4
+ } {
5
+ const newline = value.includes('\r\n') ? '\r\n' : '\n';
6
+ const normalized = newline === '\n' ? value : value.replace(/\r\n/g, '\n');
7
+ const parts = normalized.split('\n');
8
+ if (parts.length > 0 && parts[parts.length - 1] === '') {
9
+ parts.pop();
10
+ }
11
+ return { lines: parts, newline };
12
+ }
13
+
14
+ export function joinLines(lines: string[], newline: string): string {
15
+ const base = lines.join('\n');
16
+ return newline === '\n' ? base : base.replace(/\n/g, newline);
17
+ }
18
+
19
+ export function ensureTrailingNewline(lines: string[]) {
20
+ if (lines.length === 0 || lines[lines.length - 1] !== '') {
21
+ lines.push('');
22
+ }
23
+ }
@@ -0,0 +1,82 @@
1
+ export type PatchOperationKind = 'add' | 'update' | 'delete';
2
+
3
+ export interface PatchHunkLine {
4
+ kind: 'context' | 'add' | 'remove';
5
+ content: string;
6
+ }
7
+
8
+ export interface PatchHunkHeader {
9
+ oldStart?: number;
10
+ oldLines?: number;
11
+ newStart?: number;
12
+ newLines?: number;
13
+ context?: string;
14
+ }
15
+
16
+ export interface PatchHunk {
17
+ header: PatchHunkHeader;
18
+ lines: PatchHunkLine[];
19
+ }
20
+
21
+ export interface PatchAddOperation {
22
+ kind: 'add';
23
+ filePath: string;
24
+ lines: string[];
25
+ }
26
+
27
+ export interface PatchDeleteOperation {
28
+ kind: 'delete';
29
+ filePath: string;
30
+ }
31
+
32
+ export interface PatchUpdateOperation {
33
+ kind: 'update';
34
+ filePath: string;
35
+ hunks: PatchHunk[];
36
+ }
37
+
38
+ export type PatchOperation =
39
+ | PatchAddOperation
40
+ | PatchDeleteOperation
41
+ | PatchUpdateOperation;
42
+
43
+ export interface AppliedPatchHunk {
44
+ header: PatchHunkHeader;
45
+ lines: PatchHunkLine[];
46
+ oldStart: number;
47
+ oldLines: number;
48
+ newStart: number;
49
+ newLines: number;
50
+ additions: number;
51
+ deletions: number;
52
+ }
53
+
54
+ export interface AppliedPatchOperation {
55
+ kind: PatchOperationKind;
56
+ filePath: string;
57
+ stats: {
58
+ additions: number;
59
+ deletions: number;
60
+ };
61
+ hunks: AppliedPatchHunk[];
62
+ }
63
+
64
+ export interface PatchSummary {
65
+ files: number;
66
+ additions: number;
67
+ deletions: number;
68
+ }
69
+
70
+ export interface PatchApplicationResult {
71
+ operations: AppliedPatchOperation[];
72
+ summary: PatchSummary;
73
+ normalizedPatch: string;
74
+ rejected: RejectedPatch[];
75
+ }
76
+
77
+ export interface RejectedPatch {
78
+ kind: PatchOperationKind;
79
+ filePath: string;
80
+ reason: string;
81
+ operation: PatchOperation;
82
+ }