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.
@@ -32,6 +32,7 @@ declare function createReadFileTool(options: ReadFileToolOptions): ai.Tool<{
32
32
 
33
33
  interface WriteFileToolOptions {
34
34
  workingDirectory: string;
35
+ sessionId: string;
35
36
  }
36
37
  declare function createWriteFileTool(options: WriteFileToolOptions): ai.Tool<{
37
38
  mode: "full" | "str_replace";
@@ -37,7 +37,6 @@ async function isTmuxAvailable() {
37
37
  try {
38
38
  const { stdout } = await execAsync("tmux -V");
39
39
  tmuxAvailableCache = true;
40
- console.log(`[tmux] Available: ${stdout.trim()}`);
41
40
  return true;
42
41
  } catch (error) {
43
42
  tmuxAvailableCache = false;
@@ -609,131 +608,16 @@ Use this to understand existing code, check file contents, or gather context.`,
609
608
  // src/tools/write-file.ts
610
609
  import { tool as tool3 } from "ai";
611
610
  import { z as z3 } from "zod";
612
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
613
- import { resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2, dirname } from "path";
614
- import { existsSync as existsSync3 } from "fs";
615
- var writeFileInputSchema = z3.object({
616
- path: z3.string().describe("The path to the file. Can be relative to working directory or absolute."),
617
- mode: z3.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
618
- content: z3.string().optional().describe('For "full" mode: The complete content to write to the file'),
619
- old_string: z3.string().optional().describe('For "str_replace" mode: The exact string to find and replace'),
620
- new_string: z3.string().optional().describe('For "str_replace" mode: The string to replace old_string with')
621
- });
622
- function createWriteFileTool(options) {
623
- return tool3({
624
- description: `Write content to a file. Supports two modes:
625
- 1. "full" - Write the entire file content (creates new file or replaces existing)
626
- 2. "str_replace" - Replace a specific string in an existing file (for precise edits)
627
-
628
- For str_replace mode:
629
- - Provide the exact string to find (old_string) and its replacement (new_string)
630
- - The old_string must match EXACTLY (including whitespace and indentation)
631
- - Only the first occurrence is replaced
632
- - Use this for surgical edits to existing code
633
-
634
- For full mode:
635
- - Provide the complete file content
636
- - Creates parent directories if they don't exist
637
- - Use this for new files or complete rewrites
638
-
639
- Working directory: ${options.workingDirectory}`,
640
- inputSchema: writeFileInputSchema,
641
- execute: async ({ path, mode, content, old_string, new_string }) => {
642
- try {
643
- const absolutePath = isAbsolute2(path) ? path : resolve2(options.workingDirectory, path);
644
- const relativePath = relative2(options.workingDirectory, absolutePath);
645
- if (relativePath.startsWith("..") && !isAbsolute2(path)) {
646
- return {
647
- success: false,
648
- error: "Path escapes the working directory. Use an absolute path if intentional."
649
- };
650
- }
651
- if (mode === "full") {
652
- if (content === void 0) {
653
- return {
654
- success: false,
655
- error: 'Content is required for "full" mode'
656
- };
657
- }
658
- const dir = dirname(absolutePath);
659
- if (!existsSync3(dir)) {
660
- await mkdir2(dir, { recursive: true });
661
- }
662
- const existed = existsSync3(absolutePath);
663
- await writeFile2(absolutePath, content, "utf-8");
664
- return {
665
- success: true,
666
- path: absolutePath,
667
- relativePath: relative2(options.workingDirectory, absolutePath),
668
- mode: "full",
669
- action: existed ? "replaced" : "created",
670
- bytesWritten: Buffer.byteLength(content, "utf-8"),
671
- lineCount: content.split("\n").length
672
- };
673
- } else if (mode === "str_replace") {
674
- if (old_string === void 0 || new_string === void 0) {
675
- return {
676
- success: false,
677
- error: 'Both old_string and new_string are required for "str_replace" mode'
678
- };
679
- }
680
- if (!existsSync3(absolutePath)) {
681
- return {
682
- success: false,
683
- error: `File not found: ${path}. Use "full" mode to create new files.`
684
- };
685
- }
686
- const currentContent = await readFile3(absolutePath, "utf-8");
687
- if (!currentContent.includes(old_string)) {
688
- const lines = currentContent.split("\n");
689
- const preview = lines.slice(0, 20).join("\n");
690
- return {
691
- success: false,
692
- error: "old_string not found in file. The string must match EXACTLY including whitespace.",
693
- hint: "Check for differences in indentation, line endings, or invisible characters.",
694
- filePreview: lines.length > 20 ? `${preview}
695
- ... (${lines.length - 20} more lines)` : preview
696
- };
697
- }
698
- const occurrences = currentContent.split(old_string).length - 1;
699
- if (occurrences > 1) {
700
- return {
701
- success: false,
702
- error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
703
- hint: "Include surrounding lines or more specific content in old_string."
704
- };
705
- }
706
- const newContent = currentContent.replace(old_string, new_string);
707
- await writeFile2(absolutePath, newContent, "utf-8");
708
- const oldLines = old_string.split("\n").length;
709
- const newLines = new_string.split("\n").length;
710
- return {
711
- success: true,
712
- path: absolutePath,
713
- relativePath: relative2(options.workingDirectory, absolutePath),
714
- mode: "str_replace",
715
- linesRemoved: oldLines,
716
- linesAdded: newLines,
717
- lineDelta: newLines - oldLines
718
- };
719
- }
720
- return {
721
- success: false,
722
- error: `Invalid mode: ${mode}`
723
- };
724
- } catch (error) {
725
- return {
726
- success: false,
727
- error: error.message
728
- };
729
- }
730
- }
731
- });
732
- }
611
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
612
+ import { resolve as resolve3, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
613
+ import { existsSync as existsSync4 } from "fs";
733
614
 
734
- // src/tools/todo.ts
735
- import { tool as tool4 } from "ai";
736
- import { z as z4 } from "zod";
615
+ // src/checkpoints/index.ts
616
+ import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
617
+ import { existsSync as existsSync3 } from "fs";
618
+ import { resolve as resolve2, relative as relative2, dirname } from "path";
619
+ import { exec as exec3 } from "child_process";
620
+ import { promisify as promisify3 } from "util";
737
621
 
738
622
  // src/db/index.ts
739
623
  import Database from "better-sqlite3";
@@ -819,6 +703,28 @@ var activeStreams = sqliteTable("active_streams", {
819
703
  createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
820
704
  finishedAt: integer("finished_at", { mode: "timestamp" })
821
705
  });
706
+ var checkpoints = sqliteTable("checkpoints", {
707
+ id: text("id").primaryKey(),
708
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
709
+ // The message sequence number this checkpoint was created BEFORE
710
+ // (i.e., the state before this user message was processed)
711
+ messageSequence: integer("message_sequence").notNull(),
712
+ // Optional git commit hash if in a git repo
713
+ gitHead: text("git_head"),
714
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
715
+ });
716
+ var fileBackups = sqliteTable("file_backups", {
717
+ id: text("id").primaryKey(),
718
+ checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
719
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
720
+ // Relative path from working directory
721
+ filePath: text("file_path").notNull(),
722
+ // Original content (null means file didn't exist before)
723
+ originalContent: text("original_content"),
724
+ // Whether the file existed before this checkpoint
725
+ existed: integer("existed", { mode: "boolean" }).notNull().default(true),
726
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
727
+ });
822
728
 
823
729
  // src/db/index.ts
824
730
  var db = null;
@@ -891,8 +797,240 @@ var skillQueries = {
891
797
  return !!result;
892
798
  }
893
799
  };
800
+ var fileBackupQueries = {
801
+ create(data) {
802
+ const id = nanoid2();
803
+ const result = getDb().insert(fileBackups).values({
804
+ id,
805
+ checkpointId: data.checkpointId,
806
+ sessionId: data.sessionId,
807
+ filePath: data.filePath,
808
+ originalContent: data.originalContent,
809
+ existed: data.existed,
810
+ createdAt: /* @__PURE__ */ new Date()
811
+ }).returning().get();
812
+ return result;
813
+ },
814
+ getByCheckpoint(checkpointId) {
815
+ return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
816
+ },
817
+ getBySession(sessionId) {
818
+ return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
819
+ },
820
+ /**
821
+ * Get all file backups from a given checkpoint sequence onwards (inclusive)
822
+ * (Used when reverting - need to restore these files)
823
+ *
824
+ * When reverting to checkpoint X, we need backups from checkpoint X and all later ones
825
+ * because checkpoint X's backups represent the state BEFORE processing message X.
826
+ */
827
+ getFromSequence(sessionId, messageSequence) {
828
+ const checkpointsFrom = getDb().select().from(checkpoints).where(
829
+ and(
830
+ eq(checkpoints.sessionId, sessionId),
831
+ sql`message_sequence >= ${messageSequence}`
832
+ )
833
+ ).all();
834
+ if (checkpointsFrom.length === 0) {
835
+ return [];
836
+ }
837
+ const checkpointIds = checkpointsFrom.map((c) => c.id);
838
+ const allBackups = [];
839
+ for (const cpId of checkpointIds) {
840
+ const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
841
+ allBackups.push(...backups);
842
+ }
843
+ return allBackups;
844
+ },
845
+ /**
846
+ * Check if a file already has a backup in the current checkpoint
847
+ */
848
+ hasBackup(checkpointId, filePath) {
849
+ const result = getDb().select().from(fileBackups).where(
850
+ and(
851
+ eq(fileBackups.checkpointId, checkpointId),
852
+ eq(fileBackups.filePath, filePath)
853
+ )
854
+ ).get();
855
+ return !!result;
856
+ },
857
+ deleteBySession(sessionId) {
858
+ const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
859
+ return result.changes;
860
+ }
861
+ };
862
+
863
+ // src/checkpoints/index.ts
864
+ var execAsync3 = promisify3(exec3);
865
+ var activeManagers = /* @__PURE__ */ new Map();
866
+ function getCheckpointManager(sessionId, workingDirectory) {
867
+ let manager = activeManagers.get(sessionId);
868
+ if (!manager) {
869
+ manager = {
870
+ sessionId,
871
+ workingDirectory,
872
+ currentCheckpointId: null
873
+ };
874
+ activeManagers.set(sessionId, manager);
875
+ }
876
+ return manager;
877
+ }
878
+ async function backupFile(sessionId, workingDirectory, filePath) {
879
+ const manager = getCheckpointManager(sessionId, workingDirectory);
880
+ if (!manager.currentCheckpointId) {
881
+ console.warn("[checkpoint] No active checkpoint, skipping file backup");
882
+ return null;
883
+ }
884
+ const absolutePath = resolve2(workingDirectory, filePath);
885
+ const relativePath = relative2(workingDirectory, absolutePath);
886
+ if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
887
+ return null;
888
+ }
889
+ let originalContent = null;
890
+ let existed = false;
891
+ if (existsSync3(absolutePath)) {
892
+ try {
893
+ originalContent = await readFile3(absolutePath, "utf-8");
894
+ existed = true;
895
+ } catch (error) {
896
+ console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
897
+ }
898
+ }
899
+ const backup = fileBackupQueries.create({
900
+ checkpointId: manager.currentCheckpointId,
901
+ sessionId,
902
+ filePath: relativePath,
903
+ originalContent,
904
+ existed
905
+ });
906
+ return backup;
907
+ }
908
+
909
+ // src/tools/write-file.ts
910
+ var writeFileInputSchema = z3.object({
911
+ path: z3.string().describe("The path to the file. Can be relative to working directory or absolute."),
912
+ mode: z3.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
913
+ content: z3.string().optional().describe('For "full" mode: The complete content to write to the file'),
914
+ old_string: z3.string().optional().describe('For "str_replace" mode: The exact string to find and replace'),
915
+ new_string: z3.string().optional().describe('For "str_replace" mode: The string to replace old_string with')
916
+ });
917
+ function createWriteFileTool(options) {
918
+ return tool3({
919
+ description: `Write content to a file. Supports two modes:
920
+ 1. "full" - Write the entire file content (creates new file or replaces existing)
921
+ 2. "str_replace" - Replace a specific string in an existing file (for precise edits)
922
+
923
+ For str_replace mode:
924
+ - Provide the exact string to find (old_string) and its replacement (new_string)
925
+ - The old_string must match EXACTLY (including whitespace and indentation)
926
+ - Only the first occurrence is replaced
927
+ - Use this for surgical edits to existing code
928
+
929
+ For full mode:
930
+ - Provide the complete file content
931
+ - Creates parent directories if they don't exist
932
+ - Use this for new files or complete rewrites
933
+
934
+ Working directory: ${options.workingDirectory}`,
935
+ inputSchema: writeFileInputSchema,
936
+ execute: async ({ path, mode, content, old_string, new_string }) => {
937
+ try {
938
+ const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
939
+ const relativePath = relative3(options.workingDirectory, absolutePath);
940
+ if (relativePath.startsWith("..") && !isAbsolute2(path)) {
941
+ return {
942
+ success: false,
943
+ error: "Path escapes the working directory. Use an absolute path if intentional."
944
+ };
945
+ }
946
+ if (mode === "full") {
947
+ if (content === void 0) {
948
+ return {
949
+ success: false,
950
+ error: 'Content is required for "full" mode'
951
+ };
952
+ }
953
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
954
+ const dir = dirname2(absolutePath);
955
+ if (!existsSync4(dir)) {
956
+ await mkdir3(dir, { recursive: true });
957
+ }
958
+ const existed = existsSync4(absolutePath);
959
+ await writeFile3(absolutePath, content, "utf-8");
960
+ return {
961
+ success: true,
962
+ path: absolutePath,
963
+ relativePath: relative3(options.workingDirectory, absolutePath),
964
+ mode: "full",
965
+ action: existed ? "replaced" : "created",
966
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
967
+ lineCount: content.split("\n").length
968
+ };
969
+ } else if (mode === "str_replace") {
970
+ if (old_string === void 0 || new_string === void 0) {
971
+ return {
972
+ success: false,
973
+ error: 'Both old_string and new_string are required for "str_replace" mode'
974
+ };
975
+ }
976
+ if (!existsSync4(absolutePath)) {
977
+ return {
978
+ success: false,
979
+ error: `File not found: ${path}. Use "full" mode to create new files.`
980
+ };
981
+ }
982
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
983
+ const currentContent = await readFile4(absolutePath, "utf-8");
984
+ if (!currentContent.includes(old_string)) {
985
+ const lines = currentContent.split("\n");
986
+ const preview = lines.slice(0, 20).join("\n");
987
+ return {
988
+ success: false,
989
+ error: "old_string not found in file. The string must match EXACTLY including whitespace.",
990
+ hint: "Check for differences in indentation, line endings, or invisible characters.",
991
+ filePreview: lines.length > 20 ? `${preview}
992
+ ... (${lines.length - 20} more lines)` : preview
993
+ };
994
+ }
995
+ const occurrences = currentContent.split(old_string).length - 1;
996
+ if (occurrences > 1) {
997
+ return {
998
+ success: false,
999
+ error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
1000
+ hint: "Include surrounding lines or more specific content in old_string."
1001
+ };
1002
+ }
1003
+ const newContent = currentContent.replace(old_string, new_string);
1004
+ await writeFile3(absolutePath, newContent, "utf-8");
1005
+ const oldLines = old_string.split("\n").length;
1006
+ const newLines = new_string.split("\n").length;
1007
+ return {
1008
+ success: true,
1009
+ path: absolutePath,
1010
+ relativePath: relative3(options.workingDirectory, absolutePath),
1011
+ mode: "str_replace",
1012
+ linesRemoved: oldLines,
1013
+ linesAdded: newLines,
1014
+ lineDelta: newLines - oldLines
1015
+ };
1016
+ }
1017
+ return {
1018
+ success: false,
1019
+ error: `Invalid mode: ${mode}`
1020
+ };
1021
+ } catch (error) {
1022
+ return {
1023
+ success: false,
1024
+ error: error.message
1025
+ };
1026
+ }
1027
+ }
1028
+ });
1029
+ }
894
1030
 
895
1031
  // src/tools/todo.ts
1032
+ import { tool as tool4 } from "ai";
1033
+ import { z as z4 } from "zod";
896
1034
  var todoInputSchema = z4.object({
897
1035
  action: z4.enum(["add", "list", "mark", "clear"]).describe("The action to perform on the todo list"),
898
1036
  items: z4.array(
@@ -1020,9 +1158,9 @@ import { tool as tool5 } from "ai";
1020
1158
  import { z as z6 } from "zod";
1021
1159
 
1022
1160
  // src/skills/index.ts
1023
- import { readFile as readFile4, readdir } from "fs/promises";
1024
- import { resolve as resolve3, basename, extname } from "path";
1025
- import { existsSync as existsSync4 } from "fs";
1161
+ import { readFile as readFile5, readdir } from "fs/promises";
1162
+ import { resolve as resolve4, basename, extname } from "path";
1163
+ import { existsSync as existsSync5 } from "fs";
1026
1164
 
1027
1165
  // src/config/types.ts
1028
1166
  import { z as z5 } from "zod";
@@ -1108,15 +1246,15 @@ function getSkillNameFromPath(filePath) {
1108
1246
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1109
1247
  }
1110
1248
  async function loadSkillsFromDirectory(directory) {
1111
- if (!existsSync4(directory)) {
1249
+ if (!existsSync5(directory)) {
1112
1250
  return [];
1113
1251
  }
1114
1252
  const skills = [];
1115
1253
  const files = await readdir(directory);
1116
1254
  for (const file of files) {
1117
1255
  if (!file.endsWith(".md")) continue;
1118
- const filePath = resolve3(directory, file);
1119
- const content = await readFile4(filePath, "utf-8");
1256
+ const filePath = resolve4(directory, file);
1257
+ const content = await readFile5(filePath, "utf-8");
1120
1258
  const parsed = parseSkillFrontmatter(content);
1121
1259
  if (parsed) {
1122
1260
  skills.push({
@@ -1158,7 +1296,7 @@ async function loadSkillContent(skillName, directories) {
1158
1296
  if (!skill) {
1159
1297
  return null;
1160
1298
  }
1161
- const content = await readFile4(skill.filePath, "utf-8");
1299
+ const content = await readFile5(skill.filePath, "utf-8");
1162
1300
  const parsed = parseSkillFrontmatter(content);
1163
1301
  return {
1164
1302
  ...skill,
@@ -1269,7 +1407,8 @@ function createTools(options) {
1269
1407
  workingDirectory: options.workingDirectory
1270
1408
  }),
1271
1409
  write_file: createWriteFileTool({
1272
- workingDirectory: options.workingDirectory
1410
+ workingDirectory: options.workingDirectory,
1411
+ sessionId: options.sessionId
1273
1412
  }),
1274
1413
  todo: createTodoTool({
1275
1414
  sessionId: options.sessionId