sparkecoder 0.1.4 → 0.1.5

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.
@@ -1,6 +1,6 @@
1
1
  import 'ai';
2
- import '../schema-cUDLVN-b.js';
3
- export { A as Agent, a as AgentOptions, b as AgentRunOptions, c as AgentStreamResult, C as ContextManager, d as buildSystemPrompt } from '../index-DkR9Ln_7.js';
2
+ import '../schema-CkrIadxa.js';
3
+ export { A as Agent, a as AgentOptions, b as AgentRunOptions, c as AgentStreamResult, C as ContextManager, d as buildSystemPrompt } from '../index-Btr542-G.js';
4
4
  import '../bash-CGAqW7HR.js';
5
5
  import 'drizzle-orm/sqlite-core';
6
6
  import 'zod';
@@ -93,6 +93,28 @@ var activeStreams = sqliteTable("active_streams", {
93
93
  createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
94
94
  finishedAt: integer("finished_at", { mode: "timestamp" })
95
95
  });
96
+ var checkpoints = sqliteTable("checkpoints", {
97
+ id: text("id").primaryKey(),
98
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
99
+ // The message sequence number this checkpoint was created BEFORE
100
+ // (i.e., the state before this user message was processed)
101
+ messageSequence: integer("message_sequence").notNull(),
102
+ // Optional git commit hash if in a git repo
103
+ gitHead: text("git_head"),
104
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
105
+ });
106
+ var fileBackups = sqliteTable("file_backups", {
107
+ id: text("id").primaryKey(),
108
+ checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
109
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
110
+ // Relative path from working directory
111
+ filePath: text("file_path").notNull(),
112
+ // Original content (null means file didn't exist before)
113
+ originalContent: text("original_content"),
114
+ // Whether the file existed before this checkpoint
115
+ existed: integer("existed", { mode: "boolean" }).notNull().default(true),
116
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
117
+ });
96
118
 
97
119
  // src/db/index.ts
98
120
  var db = null;
@@ -202,6 +224,19 @@ var messageQueries = {
202
224
  deleteBySession(sessionId) {
203
225
  const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
204
226
  return result.changes;
227
+ },
228
+ /**
229
+ * Delete all messages with sequence >= the given sequence number
230
+ * (Used when reverting to a checkpoint)
231
+ */
232
+ deleteFromSequence(sessionId, fromSequence) {
233
+ const result = getDb().delete(messages).where(
234
+ and(
235
+ eq(messages.sessionId, sessionId),
236
+ sql`sequence >= ${fromSequence}`
237
+ )
238
+ ).run();
239
+ return result.changes;
205
240
  }
206
241
  };
207
242
  var toolExecutionQueries = {
@@ -245,6 +280,19 @@ var toolExecutionQueries = {
245
280
  },
246
281
  getBySession(sessionId) {
247
282
  return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
283
+ },
284
+ /**
285
+ * Delete all tool executions after a given timestamp
286
+ * (Used when reverting to a checkpoint)
287
+ */
288
+ deleteAfterTime(sessionId, afterTime) {
289
+ const result = getDb().delete(toolExecutions).where(
290
+ and(
291
+ eq(toolExecutions.sessionId, sessionId),
292
+ sql`started_at > ${afterTime.getTime()}`
293
+ )
294
+ ).run();
295
+ return result.changes;
248
296
  }
249
297
  };
250
298
  var todoQueries = {
@@ -310,6 +358,68 @@ var skillQueries = {
310
358
  return !!result;
311
359
  }
312
360
  };
361
+ var fileBackupQueries = {
362
+ create(data) {
363
+ const id = nanoid();
364
+ const result = getDb().insert(fileBackups).values({
365
+ id,
366
+ checkpointId: data.checkpointId,
367
+ sessionId: data.sessionId,
368
+ filePath: data.filePath,
369
+ originalContent: data.originalContent,
370
+ existed: data.existed,
371
+ createdAt: /* @__PURE__ */ new Date()
372
+ }).returning().get();
373
+ return result;
374
+ },
375
+ getByCheckpoint(checkpointId) {
376
+ return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
377
+ },
378
+ getBySession(sessionId) {
379
+ return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
380
+ },
381
+ /**
382
+ * Get all file backups from a given checkpoint sequence onwards (inclusive)
383
+ * (Used when reverting - need to restore these files)
384
+ *
385
+ * When reverting to checkpoint X, we need backups from checkpoint X and all later ones
386
+ * because checkpoint X's backups represent the state BEFORE processing message X.
387
+ */
388
+ getFromSequence(sessionId, messageSequence) {
389
+ const checkpointsFrom = getDb().select().from(checkpoints).where(
390
+ and(
391
+ eq(checkpoints.sessionId, sessionId),
392
+ sql`message_sequence >= ${messageSequence}`
393
+ )
394
+ ).all();
395
+ if (checkpointsFrom.length === 0) {
396
+ return [];
397
+ }
398
+ const checkpointIds = checkpointsFrom.map((c) => c.id);
399
+ const allBackups = [];
400
+ for (const cpId of checkpointIds) {
401
+ const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
402
+ allBackups.push(...backups);
403
+ }
404
+ return allBackups;
405
+ },
406
+ /**
407
+ * Check if a file already has a backup in the current checkpoint
408
+ */
409
+ hasBackup(checkpointId, filePath) {
410
+ const result = getDb().select().from(fileBackups).where(
411
+ and(
412
+ eq(fileBackups.checkpointId, checkpointId),
413
+ eq(fileBackups.filePath, filePath)
414
+ )
415
+ ).get();
416
+ return !!result;
417
+ },
418
+ deleteBySession(sessionId) {
419
+ const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
420
+ return result.changes;
421
+ }
422
+ };
313
423
 
314
424
  // src/config/index.ts
315
425
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
@@ -445,7 +555,6 @@ async function isTmuxAvailable() {
445
555
  try {
446
556
  const { stdout } = await execAsync("tmux -V");
447
557
  tmuxAvailableCache = true;
448
- console.log(`[tmux] Available: ${stdout.trim()}`);
449
558
  return true;
450
559
  } catch (error) {
451
560
  tmuxAvailableCache = false;
@@ -1017,9 +1126,62 @@ Use this to understand existing code, check file contents, or gather context.`,
1017
1126
  // src/tools/write-file.ts
1018
1127
  import { tool as tool3 } from "ai";
1019
1128
  import { z as z4 } from "zod";
1020
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1021
- import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
1129
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1130
+ import { resolve as resolve4, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname3 } from "path";
1131
+ import { existsSync as existsSync5 } from "fs";
1132
+
1133
+ // src/checkpoints/index.ts
1134
+ import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1022
1135
  import { existsSync as existsSync4 } from "fs";
1136
+ import { resolve as resolve3, relative as relative2, dirname as dirname2 } from "path";
1137
+ import { exec as exec3 } from "child_process";
1138
+ import { promisify as promisify3 } from "util";
1139
+ var execAsync3 = promisify3(exec3);
1140
+ var activeManagers = /* @__PURE__ */ new Map();
1141
+ function getCheckpointManager(sessionId, workingDirectory) {
1142
+ let manager = activeManagers.get(sessionId);
1143
+ if (!manager) {
1144
+ manager = {
1145
+ sessionId,
1146
+ workingDirectory,
1147
+ currentCheckpointId: null
1148
+ };
1149
+ activeManagers.set(sessionId, manager);
1150
+ }
1151
+ return manager;
1152
+ }
1153
+ async function backupFile(sessionId, workingDirectory, filePath) {
1154
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1155
+ if (!manager.currentCheckpointId) {
1156
+ console.warn("[checkpoint] No active checkpoint, skipping file backup");
1157
+ return null;
1158
+ }
1159
+ const absolutePath = resolve3(workingDirectory, filePath);
1160
+ const relativePath = relative2(workingDirectory, absolutePath);
1161
+ if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
1162
+ return null;
1163
+ }
1164
+ let originalContent = null;
1165
+ let existed = false;
1166
+ if (existsSync4(absolutePath)) {
1167
+ try {
1168
+ originalContent = await readFile3(absolutePath, "utf-8");
1169
+ existed = true;
1170
+ } catch (error) {
1171
+ console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
1172
+ }
1173
+ }
1174
+ const backup = fileBackupQueries.create({
1175
+ checkpointId: manager.currentCheckpointId,
1176
+ sessionId,
1177
+ filePath: relativePath,
1178
+ originalContent,
1179
+ existed
1180
+ });
1181
+ return backup;
1182
+ }
1183
+
1184
+ // src/tools/write-file.ts
1023
1185
  var writeFileInputSchema = z4.object({
1024
1186
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
1025
1187
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -1048,8 +1210,8 @@ Working directory: ${options.workingDirectory}`,
1048
1210
  inputSchema: writeFileInputSchema,
1049
1211
  execute: async ({ path, mode, content, old_string, new_string }) => {
1050
1212
  try {
1051
- const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
1052
- const relativePath = relative2(options.workingDirectory, absolutePath);
1213
+ const absolutePath = isAbsolute2(path) ? path : resolve4(options.workingDirectory, path);
1214
+ const relativePath = relative3(options.workingDirectory, absolutePath);
1053
1215
  if (relativePath.startsWith("..") && !isAbsolute2(path)) {
1054
1216
  return {
1055
1217
  success: false,
@@ -1063,16 +1225,17 @@ Working directory: ${options.workingDirectory}`,
1063
1225
  error: 'Content is required for "full" mode'
1064
1226
  };
1065
1227
  }
1066
- const dir = dirname2(absolutePath);
1067
- if (!existsSync4(dir)) {
1068
- await mkdir2(dir, { recursive: true });
1228
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1229
+ const dir = dirname3(absolutePath);
1230
+ if (!existsSync5(dir)) {
1231
+ await mkdir3(dir, { recursive: true });
1069
1232
  }
1070
- const existed = existsSync4(absolutePath);
1071
- await writeFile2(absolutePath, content, "utf-8");
1233
+ const existed = existsSync5(absolutePath);
1234
+ await writeFile3(absolutePath, content, "utf-8");
1072
1235
  return {
1073
1236
  success: true,
1074
1237
  path: absolutePath,
1075
- relativePath: relative2(options.workingDirectory, absolutePath),
1238
+ relativePath: relative3(options.workingDirectory, absolutePath),
1076
1239
  mode: "full",
1077
1240
  action: existed ? "replaced" : "created",
1078
1241
  bytesWritten: Buffer.byteLength(content, "utf-8"),
@@ -1085,13 +1248,14 @@ Working directory: ${options.workingDirectory}`,
1085
1248
  error: 'Both old_string and new_string are required for "str_replace" mode'
1086
1249
  };
1087
1250
  }
1088
- if (!existsSync4(absolutePath)) {
1251
+ if (!existsSync5(absolutePath)) {
1089
1252
  return {
1090
1253
  success: false,
1091
1254
  error: `File not found: ${path}. Use "full" mode to create new files.`
1092
1255
  };
1093
1256
  }
1094
- const currentContent = await readFile3(absolutePath, "utf-8");
1257
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1258
+ const currentContent = await readFile4(absolutePath, "utf-8");
1095
1259
  if (!currentContent.includes(old_string)) {
1096
1260
  const lines = currentContent.split("\n");
1097
1261
  const preview = lines.slice(0, 20).join("\n");
@@ -1112,13 +1276,13 @@ Working directory: ${options.workingDirectory}`,
1112
1276
  };
1113
1277
  }
1114
1278
  const newContent = currentContent.replace(old_string, new_string);
1115
- await writeFile2(absolutePath, newContent, "utf-8");
1279
+ await writeFile3(absolutePath, newContent, "utf-8");
1116
1280
  const oldLines = old_string.split("\n").length;
1117
1281
  const newLines = new_string.split("\n").length;
1118
1282
  return {
1119
1283
  success: true,
1120
1284
  path: absolutePath,
1121
- relativePath: relative2(options.workingDirectory, absolutePath),
1285
+ relativePath: relative3(options.workingDirectory, absolutePath),
1122
1286
  mode: "str_replace",
1123
1287
  linesRemoved: oldLines,
1124
1288
  linesAdded: newLines,
@@ -1269,9 +1433,9 @@ import { tool as tool5 } from "ai";
1269
1433
  import { z as z6 } from "zod";
1270
1434
 
1271
1435
  // src/skills/index.ts
1272
- import { readFile as readFile4, readdir } from "fs/promises";
1273
- import { resolve as resolve4, basename, extname } from "path";
1274
- import { existsSync as existsSync5 } from "fs";
1436
+ import { readFile as readFile5, readdir } from "fs/promises";
1437
+ import { resolve as resolve5, basename, extname } from "path";
1438
+ import { existsSync as existsSync6 } from "fs";
1275
1439
  function parseSkillFrontmatter(content) {
1276
1440
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1277
1441
  if (!frontmatterMatch) {
@@ -1302,15 +1466,15 @@ function getSkillNameFromPath(filePath) {
1302
1466
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1303
1467
  }
1304
1468
  async function loadSkillsFromDirectory(directory) {
1305
- if (!existsSync5(directory)) {
1469
+ if (!existsSync6(directory)) {
1306
1470
  return [];
1307
1471
  }
1308
1472
  const skills = [];
1309
1473
  const files = await readdir(directory);
1310
1474
  for (const file of files) {
1311
1475
  if (!file.endsWith(".md")) continue;
1312
- const filePath = resolve4(directory, file);
1313
- const content = await readFile4(filePath, "utf-8");
1476
+ const filePath = resolve5(directory, file);
1477
+ const content = await readFile5(filePath, "utf-8");
1314
1478
  const parsed = parseSkillFrontmatter(content);
1315
1479
  if (parsed) {
1316
1480
  skills.push({
@@ -1352,7 +1516,7 @@ async function loadSkillContent(skillName, directories) {
1352
1516
  if (!skill) {
1353
1517
  return null;
1354
1518
  }
1355
- const content = await readFile4(skill.filePath, "utf-8");
1519
+ const content = await readFile5(skill.filePath, "utf-8");
1356
1520
  const parsed = parseSkillFrontmatter(content);
1357
1521
  return {
1358
1522
  ...skill,
@@ -1463,7 +1627,8 @@ function createTools(options) {
1463
1627
  workingDirectory: options.workingDirectory
1464
1628
  }),
1465
1629
  write_file: createWriteFileTool({
1466
- workingDirectory: options.workingDirectory
1630
+ workingDirectory: options.workingDirectory,
1631
+ sessionId: options.sessionId
1467
1632
  }),
1468
1633
  todo: createTodoTool({
1469
1634
  sessionId: options.sessionId
@@ -1521,6 +1686,9 @@ You have access to powerful tools for:
1521
1686
  - **todo**: Manage your task list to track progress on complex operations
1522
1687
  - **load_skill**: Load specialized knowledge documents for specific tasks
1523
1688
 
1689
+
1690
+ IMPORTANT: If you have zero context of where you are working, always explore it first to understand the structure before doing things for the user.
1691
+
1524
1692
  Use the TODO tool to manage your task list to track progress on complex operations. Always ask the user what they want to do specifically before doing it, and make a plan.
1525
1693
  Step 1 of the plan should be researching files and understanding the components/structure of what you're working on (if you don't already have context), then after u have done that, plan out the rest of the tasks u need to do.
1526
1694
  You can clear the todo and restart it, and do multiple things inside of one session.
@@ -1957,8 +2125,8 @@ var Agent = class _Agent {
1957
2125
  this.pendingApprovals.set(toolCallId, execution);
1958
2126
  options.onApprovalRequired?.(execution);
1959
2127
  sessionQueries.updateStatus(this.session.id, "waiting");
1960
- const approved = await new Promise((resolve5) => {
1961
- approvalResolvers.set(toolCallId, { resolve: resolve5, sessionId: this.session.id });
2128
+ const approved = await new Promise((resolve6) => {
2129
+ approvalResolvers.set(toolCallId, { resolve: resolve6, sessionId: this.session.id });
1962
2130
  });
1963
2131
  const resolverData = approvalResolvers.get(toolCallId);
1964
2132
  approvalResolvers.delete(toolCallId);