@lakitu/sdk 0.1.0

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 (111) hide show
  1. package/README.md +166 -0
  2. package/convex/_generated/api.d.ts +45 -0
  3. package/convex/_generated/api.js +23 -0
  4. package/convex/_generated/dataModel.d.ts +58 -0
  5. package/convex/_generated/server.d.ts +143 -0
  6. package/convex/_generated/server.js +93 -0
  7. package/convex/cloud/CLAUDE.md +238 -0
  8. package/convex/cloud/_generated/api.ts +84 -0
  9. package/convex/cloud/_generated/component.ts +861 -0
  10. package/convex/cloud/_generated/dataModel.ts +60 -0
  11. package/convex/cloud/_generated/server.ts +156 -0
  12. package/convex/cloud/convex.config.ts +16 -0
  13. package/convex/cloud/index.ts +29 -0
  14. package/convex/cloud/intentSchema/generate.ts +447 -0
  15. package/convex/cloud/intentSchema/index.ts +16 -0
  16. package/convex/cloud/intentSchema/types.ts +418 -0
  17. package/convex/cloud/ksaPolicy.ts +554 -0
  18. package/convex/cloud/mail.ts +92 -0
  19. package/convex/cloud/schema.ts +322 -0
  20. package/convex/cloud/utils/kanbanContext.ts +229 -0
  21. package/convex/cloud/workflows/agentBoard.ts +451 -0
  22. package/convex/cloud/workflows/agentPrompt.ts +272 -0
  23. package/convex/cloud/workflows/agentThread.ts +374 -0
  24. package/convex/cloud/workflows/compileSandbox.ts +146 -0
  25. package/convex/cloud/workflows/crudBoard.ts +217 -0
  26. package/convex/cloud/workflows/crudKSAs.ts +262 -0
  27. package/convex/cloud/workflows/crudLorobeads.ts +371 -0
  28. package/convex/cloud/workflows/crudSkills.ts +205 -0
  29. package/convex/cloud/workflows/crudThreads.ts +708 -0
  30. package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
  31. package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
  32. package/convex/sandbox/README.md +90 -0
  33. package/convex/sandbox/_generated/api.d.ts +2934 -0
  34. package/convex/sandbox/_generated/api.js +23 -0
  35. package/convex/sandbox/_generated/dataModel.d.ts +60 -0
  36. package/convex/sandbox/_generated/server.d.ts +143 -0
  37. package/convex/sandbox/_generated/server.js +93 -0
  38. package/convex/sandbox/actions/bash.ts +130 -0
  39. package/convex/sandbox/actions/browser.ts +282 -0
  40. package/convex/sandbox/actions/file.ts +336 -0
  41. package/convex/sandbox/actions/lsp.ts +325 -0
  42. package/convex/sandbox/actions/pdf.ts +119 -0
  43. package/convex/sandbox/agent/codeExecLoop.ts +535 -0
  44. package/convex/sandbox/agent/decisions.ts +284 -0
  45. package/convex/sandbox/agent/index.ts +515 -0
  46. package/convex/sandbox/agent/subagents.ts +651 -0
  47. package/convex/sandbox/brandResearch/index.ts +417 -0
  48. package/convex/sandbox/context/index.ts +7 -0
  49. package/convex/sandbox/context/session.ts +402 -0
  50. package/convex/sandbox/convex.config.ts +17 -0
  51. package/convex/sandbox/index.ts +51 -0
  52. package/convex/sandbox/nodeActions/codeExec.ts +130 -0
  53. package/convex/sandbox/planning/beads.ts +187 -0
  54. package/convex/sandbox/planning/index.ts +8 -0
  55. package/convex/sandbox/planning/sync.ts +194 -0
  56. package/convex/sandbox/prompts/codeExec.ts +852 -0
  57. package/convex/sandbox/prompts/modes.ts +231 -0
  58. package/convex/sandbox/prompts/system.ts +142 -0
  59. package/convex/sandbox/schema.ts +510 -0
  60. package/convex/sandbox/state/artifacts.ts +99 -0
  61. package/convex/sandbox/state/checkpoints.ts +341 -0
  62. package/convex/sandbox/state/files.ts +383 -0
  63. package/convex/sandbox/state/index.ts +10 -0
  64. package/convex/sandbox/state/verification.actions.ts +268 -0
  65. package/convex/sandbox/state/verification.ts +101 -0
  66. package/convex/sandbox/tsconfig.json +25 -0
  67. package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
  68. package/dist/cli/commands/build.d.ts +19 -0
  69. package/dist/cli/commands/build.d.ts.map +1 -0
  70. package/dist/cli/commands/build.js +223 -0
  71. package/dist/cli/commands/init.d.ts +16 -0
  72. package/dist/cli/commands/init.d.ts.map +1 -0
  73. package/dist/cli/commands/init.js +148 -0
  74. package/dist/cli/commands/publish.d.ts +12 -0
  75. package/dist/cli/commands/publish.d.ts.map +1 -0
  76. package/dist/cli/commands/publish.js +33 -0
  77. package/dist/cli/index.d.ts +14 -0
  78. package/dist/cli/index.d.ts.map +1 -0
  79. package/dist/cli/index.js +40 -0
  80. package/dist/sdk/builders.d.ts +104 -0
  81. package/dist/sdk/builders.d.ts.map +1 -0
  82. package/dist/sdk/builders.js +214 -0
  83. package/dist/sdk/index.d.ts +29 -0
  84. package/dist/sdk/index.d.ts.map +1 -0
  85. package/dist/sdk/index.js +38 -0
  86. package/dist/sdk/types.d.ts +107 -0
  87. package/dist/sdk/types.d.ts.map +1 -0
  88. package/dist/sdk/types.js +6 -0
  89. package/ksa/README.md +263 -0
  90. package/ksa/_generated/REFERENCE.md +2954 -0
  91. package/ksa/_generated/registry.ts +257 -0
  92. package/ksa/_shared/configReader.ts +302 -0
  93. package/ksa/_shared/configSchemas.ts +649 -0
  94. package/ksa/_shared/gateway.ts +175 -0
  95. package/ksa/_shared/ksaBehaviors.ts +411 -0
  96. package/ksa/_shared/ksaProxy.ts +248 -0
  97. package/ksa/_shared/localDb.ts +302 -0
  98. package/ksa/index.ts +134 -0
  99. package/package.json +93 -0
  100. package/runtime/browser/agent-browser.ts +330 -0
  101. package/runtime/entrypoint.ts +194 -0
  102. package/runtime/lsp/manager.ts +366 -0
  103. package/runtime/pdf/pdf-generator.ts +50 -0
  104. package/runtime/pdf/renderer.ts +357 -0
  105. package/runtime/pdf/schema.ts +97 -0
  106. package/runtime/services/file-watcher.ts +191 -0
  107. package/template/build.ts +307 -0
  108. package/template/e2b/Dockerfile +69 -0
  109. package/template/e2b/e2b.toml +13 -0
  110. package/template/e2b/prebuild.sh +68 -0
  111. package/template/e2b/start.sh +14 -0
@@ -0,0 +1,282 @@
1
+ "use node";
2
+
3
+ /**
4
+ * Browser Actions
5
+ *
6
+ * Internal actions for browser automation using agent-browser CLI.
7
+ */
8
+
9
+ import { internalAction } from "../_generated/server";
10
+ import { v } from "convex/values";
11
+
12
+ /**
13
+ * Navigate to a URL
14
+ */
15
+ export const open = internalAction({
16
+ args: {
17
+ url: v.string(),
18
+ },
19
+ handler: async (ctx, args) => {
20
+ try {
21
+ const { execSync } = await import("child_process");
22
+
23
+ execSync(`agent-browser open "${args.url}"`, {
24
+ encoding: "utf8",
25
+ timeout: 30000,
26
+ env: {
27
+ ...process.env,
28
+ HOME: "/home/user",
29
+ },
30
+ });
31
+
32
+ return { success: true, url: args.url };
33
+ } catch (error: any) {
34
+ return { success: false, error: error.message };
35
+ }
36
+ },
37
+ });
38
+
39
+ /**
40
+ * Get page snapshot with interactive elements
41
+ */
42
+ export const snapshot = internalAction({
43
+ args: {
44
+ interactive: v.optional(v.boolean()),
45
+ },
46
+ handler: async (ctx, args) => {
47
+ try {
48
+ const { execSync } = await import("child_process");
49
+
50
+ const cmdArgs = ["snapshot"];
51
+ if (args.interactive !== false) {
52
+ cmdArgs.push("--interactive");
53
+ }
54
+
55
+ const result = execSync(`agent-browser ${cmdArgs.join(" ")}`, {
56
+ encoding: "utf8",
57
+ timeout: 15000,
58
+ env: {
59
+ ...process.env,
60
+ HOME: "/home/user",
61
+ },
62
+ });
63
+
64
+ // Parse snapshot output
65
+ const snapshot = parseSnapshot(result);
66
+ return { success: true, ...snapshot };
67
+ } catch (error: any) {
68
+ return { success: false, error: error.message, elements: [] };
69
+ }
70
+ },
71
+ });
72
+
73
+ /**
74
+ * Click an element by ref
75
+ */
76
+ export const click = internalAction({
77
+ args: {
78
+ ref: v.string(),
79
+ },
80
+ handler: async (ctx, args) => {
81
+ try {
82
+ const { execSync } = await import("child_process");
83
+
84
+ execSync(`agent-browser click "${args.ref}"`, {
85
+ encoding: "utf8",
86
+ timeout: 10000,
87
+ env: {
88
+ ...process.env,
89
+ HOME: "/home/user",
90
+ },
91
+ });
92
+
93
+ return { success: true };
94
+ } catch (error: any) {
95
+ return { success: false, error: error.message };
96
+ }
97
+ },
98
+ });
99
+
100
+ /**
101
+ * Type text into focused element
102
+ */
103
+ export const type = internalAction({
104
+ args: {
105
+ text: v.string(),
106
+ },
107
+ handler: async (ctx, args) => {
108
+ try {
109
+ const { execSync } = await import("child_process");
110
+
111
+ // Escape text for shell
112
+ const escaped = args.text.replace(/"/g, '\\"');
113
+
114
+ execSync(`agent-browser type "${escaped}"`, {
115
+ encoding: "utf8",
116
+ timeout: 10000,
117
+ env: {
118
+ ...process.env,
119
+ HOME: "/home/user",
120
+ },
121
+ });
122
+
123
+ return { success: true };
124
+ } catch (error: any) {
125
+ return { success: false, error: error.message };
126
+ }
127
+ },
128
+ });
129
+
130
+ /**
131
+ * Press a keyboard key
132
+ */
133
+ export const press = internalAction({
134
+ args: {
135
+ key: v.string(),
136
+ },
137
+ handler: async (ctx, args) => {
138
+ try {
139
+ const { execSync } = await import("child_process");
140
+
141
+ execSync(`agent-browser press "${args.key}"`, {
142
+ encoding: "utf8",
143
+ timeout: 5000,
144
+ env: {
145
+ ...process.env,
146
+ HOME: "/home/user",
147
+ },
148
+ });
149
+
150
+ return { success: true };
151
+ } catch (error: any) {
152
+ return { success: false, error: error.message };
153
+ }
154
+ },
155
+ });
156
+
157
+ /**
158
+ * Scroll the page
159
+ */
160
+ export const scroll = internalAction({
161
+ args: {
162
+ direction: v.union(
163
+ v.literal("up"),
164
+ v.literal("down"),
165
+ v.literal("top"),
166
+ v.literal("bottom")
167
+ ),
168
+ },
169
+ handler: async (ctx, args) => {
170
+ try {
171
+ const { execSync } = await import("child_process");
172
+
173
+ execSync(`agent-browser scroll ${args.direction}`, {
174
+ encoding: "utf8",
175
+ timeout: 5000,
176
+ env: {
177
+ ...process.env,
178
+ HOME: "/home/user",
179
+ },
180
+ });
181
+
182
+ return { success: true };
183
+ } catch (error: any) {
184
+ return { success: false, error: error.message };
185
+ }
186
+ },
187
+ });
188
+
189
+ /**
190
+ * Take a screenshot
191
+ */
192
+ export const screenshot = internalAction({
193
+ args: {},
194
+ handler: async (ctx) => {
195
+ try {
196
+ const { execSync } = await import("child_process");
197
+
198
+ const result = execSync(`agent-browser screenshot --format base64`, {
199
+ encoding: "utf8",
200
+ timeout: 10000,
201
+ maxBuffer: 50 * 1024 * 1024,
202
+ env: {
203
+ ...process.env,
204
+ HOME: "/home/user",
205
+ },
206
+ });
207
+
208
+ return { success: true, screenshot: result.trim() };
209
+ } catch (error: any) {
210
+ return { success: false, error: error.message };
211
+ }
212
+ },
213
+ });
214
+
215
+ /**
216
+ * Close browser session
217
+ */
218
+ export const close = internalAction({
219
+ args: {},
220
+ handler: async (ctx) => {
221
+ try {
222
+ const { execSync } = await import("child_process");
223
+
224
+ execSync("agent-browser close", {
225
+ encoding: "utf8",
226
+ timeout: 5000,
227
+ env: {
228
+ ...process.env,
229
+ HOME: "/home/user",
230
+ },
231
+ });
232
+
233
+ return { success: true };
234
+ } catch (error: any) {
235
+ // Ignore errors on close
236
+ return { success: true };
237
+ }
238
+ },
239
+ });
240
+
241
+ // ============================================
242
+ // Helpers
243
+ // ============================================
244
+
245
+ function parseSnapshot(output: string): {
246
+ url: string;
247
+ title: string;
248
+ elements: Array<{
249
+ ref: string;
250
+ tag: string;
251
+ text?: string;
252
+ }>;
253
+ } {
254
+ const lines = output.split("\n");
255
+ const elements: Array<{ ref: string; tag: string; text?: string }> = [];
256
+ let url = "";
257
+ let title = "";
258
+
259
+ for (const line of lines) {
260
+ if (line.startsWith("URL:")) {
261
+ url = line.slice(4).trim();
262
+ continue;
263
+ }
264
+
265
+ if (line.startsWith("Title:")) {
266
+ title = line.slice(6).trim();
267
+ continue;
268
+ }
269
+
270
+ // Parse element refs like "@e1 button[Login]"
271
+ const refMatch = line.match(/^(@e\d+)\s+(\w+)(?:\[(.+?)\])?/);
272
+ if (refMatch) {
273
+ elements.push({
274
+ ref: refMatch[1],
275
+ tag: refMatch[2],
276
+ text: refMatch[3],
277
+ });
278
+ }
279
+ }
280
+
281
+ return { url, title, elements };
282
+ }
@@ -0,0 +1,336 @@
1
+ "use node";
2
+
3
+ /**
4
+ * File Actions
5
+ *
6
+ * Internal actions for file system operations.
7
+ * These run in Node.js context and can use fs, child_process, etc.
8
+ */
9
+
10
+ import { internalAction } from "../_generated/server";
11
+ import { v } from "convex/values";
12
+
13
+ // ============================================
14
+ // Read File
15
+ // ============================================
16
+
17
+ export const readFile = internalAction({
18
+ args: {
19
+ path: v.string(),
20
+ encoding: v.optional(v.string()),
21
+ },
22
+ handler: async (ctx, args) => {
23
+ const { readFile: fsReadFile, stat } = await import("fs/promises");
24
+
25
+ try {
26
+ const encoding = (args.encoding || "utf8") as BufferEncoding;
27
+ const content = await fsReadFile(args.path, encoding);
28
+ const stats = await stat(args.path);
29
+
30
+ return {
31
+ success: true,
32
+ content,
33
+ path: args.path,
34
+ size: stats.size,
35
+ };
36
+ } catch (error: any) {
37
+ return {
38
+ success: false,
39
+ error: error.message,
40
+ path: args.path,
41
+ };
42
+ }
43
+ },
44
+ });
45
+
46
+ // ============================================
47
+ // Write File
48
+ // ============================================
49
+
50
+ export const writeFile = internalAction({
51
+ args: {
52
+ path: v.string(),
53
+ content: v.string(),
54
+ createDirs: v.optional(v.boolean()),
55
+ },
56
+ handler: async (ctx, args) => {
57
+ const { writeFile: fsWriteFile, mkdir, stat } = await import("fs/promises");
58
+
59
+ try {
60
+ // Check if file exists
61
+ try {
62
+ await stat(args.path);
63
+ return {
64
+ success: false,
65
+ error: "File already exists. Use editFile to modify existing files.",
66
+ path: args.path,
67
+ };
68
+ } catch {
69
+ // File doesn't exist, good to proceed
70
+ }
71
+
72
+ // Create parent directories if needed
73
+ if (args.createDirs !== false) {
74
+ const dir = args.path.substring(0, args.path.lastIndexOf("/"));
75
+ if (dir) {
76
+ await mkdir(dir, { recursive: true });
77
+ }
78
+ }
79
+
80
+ // Write file
81
+ await fsWriteFile(args.path, args.content, "utf8");
82
+
83
+ return {
84
+ success: true,
85
+ path: args.path,
86
+ size: args.content.length,
87
+ };
88
+ } catch (error: any) {
89
+ return {
90
+ success: false,
91
+ error: error.message,
92
+ path: args.path,
93
+ };
94
+ }
95
+ },
96
+ });
97
+
98
+ // ============================================
99
+ // Edit File
100
+ // ============================================
101
+
102
+ export const editFile = internalAction({
103
+ args: {
104
+ path: v.string(),
105
+ oldContent: v.string(),
106
+ newContent: v.string(),
107
+ },
108
+ handler: async (ctx, args) => {
109
+ const { readFile: fsReadFile, writeFile: fsWriteFile } = await import("fs/promises");
110
+
111
+ try {
112
+ // Read current content
113
+ const currentContent = await fsReadFile(args.path, "utf8");
114
+
115
+ // Validate precondition
116
+ if (!currentContent.includes(args.oldContent)) {
117
+ return {
118
+ success: false,
119
+ error: "Precondition failed: old_content not found in file.",
120
+ path: args.path,
121
+ hint: "Read the file first to get current content",
122
+ };
123
+ }
124
+
125
+ // Apply edit
126
+ const updatedContent = currentContent.replace(args.oldContent, args.newContent);
127
+
128
+ // Generate simple diff
129
+ const diff = generateDiff(currentContent, updatedContent, args.path);
130
+
131
+ // Write updated content
132
+ await fsWriteFile(args.path, updatedContent, "utf8");
133
+
134
+ return {
135
+ success: true,
136
+ path: args.path,
137
+ diff,
138
+ previousContent: currentContent,
139
+ newContent: updatedContent,
140
+ };
141
+ } catch (error: any) {
142
+ return {
143
+ success: false,
144
+ error: error.message,
145
+ path: args.path,
146
+ };
147
+ }
148
+ },
149
+ });
150
+
151
+ // ============================================
152
+ // Glob Files
153
+ // ============================================
154
+
155
+ export const globFiles = internalAction({
156
+ args: {
157
+ pattern: v.string(),
158
+ cwd: v.optional(v.string()),
159
+ maxResults: v.optional(v.number()),
160
+ },
161
+ handler: async (ctx, args) => {
162
+ try {
163
+ const { execSync } = await import("child_process");
164
+ const cwd = args.cwd || "/home/user/workspace";
165
+ const max = args.maxResults || 100;
166
+
167
+ // Use shell find with pattern matching
168
+ // Convert glob pattern to find-compatible pattern
169
+ const pattern = args.pattern;
170
+ let cmd: string;
171
+
172
+ if (pattern.includes("**")) {
173
+ // Recursive glob - use find with -name
174
+ const name = pattern.split("/").pop() || "*";
175
+ cmd = `find "${cwd}" -type f -name "${name}" 2>/dev/null | head -${max + 1}`;
176
+ } else if (pattern.includes("*")) {
177
+ // Simple glob - use find with -name
178
+ cmd = `find "${cwd}" -maxdepth 1 -type f -name "${pattern}" 2>/dev/null | head -${max + 1}`;
179
+ } else {
180
+ // Exact path
181
+ cmd = `find "${cwd}" -type f -path "*${pattern}" 2>/dev/null | head -${max + 1}`;
182
+ }
183
+
184
+ const output = execSync(cmd, {
185
+ encoding: "utf8",
186
+ cwd,
187
+ maxBuffer: 10 * 1024 * 1024,
188
+ }).trim();
189
+
190
+ const files = output ? output.split("\n").filter(Boolean) : [];
191
+
192
+ return {
193
+ success: true,
194
+ files: files.slice(0, max),
195
+ count: files.length,
196
+ truncated: files.length > max,
197
+ };
198
+ } catch (error: any) {
199
+ return {
200
+ success: false,
201
+ error: error.message,
202
+ };
203
+ }
204
+ },
205
+ });
206
+
207
+ // ============================================
208
+ // Grep Files
209
+ // ============================================
210
+
211
+ export const grepFiles = internalAction({
212
+ args: {
213
+ pattern: v.string(),
214
+ path: v.optional(v.string()),
215
+ fileGlob: v.optional(v.string()),
216
+ maxMatches: v.optional(v.number()),
217
+ },
218
+ handler: async (ctx, args) => {
219
+ try {
220
+ const { execSync } = await import("child_process");
221
+ const searchPath = args.path || "/home/user/workspace";
222
+ const max = args.maxMatches || 50;
223
+
224
+ let cmd = `grep -rn "${args.pattern}" "${searchPath}"`;
225
+ if (args.fileGlob) {
226
+ cmd += ` --include="${args.fileGlob}"`;
227
+ }
228
+ cmd += ` 2>/dev/null | head -${max}`;
229
+
230
+ const output = execSync(cmd, {
231
+ encoding: "utf8",
232
+ maxBuffer: 10 * 1024 * 1024,
233
+ }).trim();
234
+
235
+ const matches = output
236
+ .split("\n")
237
+ .filter(Boolean)
238
+ .map((line) => {
239
+ const [filePath, lineNum, ...rest] = line.split(":");
240
+ return {
241
+ file: filePath,
242
+ line: parseInt(lineNum, 10),
243
+ content: rest.join(":").trim(),
244
+ };
245
+ });
246
+
247
+ return {
248
+ success: true,
249
+ matches,
250
+ count: matches.length,
251
+ };
252
+ } catch (error: any) {
253
+ // grep returns exit code 1 if no matches
254
+ if (error.status === 1) {
255
+ return {
256
+ success: true,
257
+ matches: [],
258
+ count: 0,
259
+ };
260
+ }
261
+ return {
262
+ success: false,
263
+ error: error.message,
264
+ };
265
+ }
266
+ },
267
+ });
268
+
269
+ // ============================================
270
+ // List Directory
271
+ // ============================================
272
+
273
+ export const listDir = internalAction({
274
+ args: {
275
+ path: v.string(),
276
+ showHidden: v.optional(v.boolean()),
277
+ },
278
+ handler: async (ctx, args) => {
279
+ const { readdir } = await import("fs/promises");
280
+
281
+ try {
282
+ const entries = await readdir(args.path, { withFileTypes: true });
283
+
284
+ const items = entries
285
+ .filter((e) => args.showHidden || !e.name.startsWith("."))
286
+ .map((e) => ({
287
+ name: e.name,
288
+ type: e.isDirectory() ? "directory" : "file",
289
+ path: `${args.path}/${e.name}`,
290
+ }));
291
+
292
+ return {
293
+ success: true,
294
+ items,
295
+ count: items.length,
296
+ };
297
+ } catch (error: any) {
298
+ return {
299
+ success: false,
300
+ error: error.message,
301
+ };
302
+ }
303
+ },
304
+ });
305
+
306
+ // ============================================
307
+ // Helpers
308
+ // ============================================
309
+
310
+ function generateDiff(oldContent: string, newContent: string, path: string): string {
311
+ const oldLines = oldContent.split("\n");
312
+ const newLines = newContent.split("\n");
313
+
314
+ let diff = `--- a/${path}\n+++ b/${path}\n`;
315
+
316
+ // Simple line-by-line diff
317
+ const maxLines = Math.max(oldLines.length, newLines.length);
318
+ for (let i = 0; i < maxLines; i++) {
319
+ const oldLine = oldLines[i];
320
+ const newLine = newLines[i];
321
+
322
+ if (oldLine === newLine) {
323
+ continue; // Skip unchanged lines for brevity
324
+ }
325
+
326
+ if (oldLine !== undefined && newLine !== undefined && oldLine !== newLine) {
327
+ diff += `-${oldLine}\n+${newLine}\n`;
328
+ } else if (oldLine !== undefined) {
329
+ diff += `-${oldLine}\n`;
330
+ } else if (newLine !== undefined) {
331
+ diff += `+${newLine}\n`;
332
+ }
333
+ }
334
+
335
+ return diff;
336
+ }