sparkecoder 0.1.4 → 0.1.6
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/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +193 -25
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +937 -495
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +53 -2
- package/dist/db/index.js +184 -0
- package/dist/db/index.js.map +1 -1
- package/dist/{index-DkR9Ln_7.d.ts → index-BeKylVnB.d.ts} +5 -5
- package/dist/index.d.ts +68 -5
- package/dist/index.js +864 -463
- package/dist/index.js.map +1 -1
- package/dist/{schema-cUDLVN-b.d.ts → schema-CkrIadxa.d.ts} +246 -2
- package/dist/server/index.js +858 -465
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +272 -133
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
|
613
|
-
import { resolve as
|
|
614
|
-
import { existsSync as
|
|
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/
|
|
735
|
-
import {
|
|
736
|
-
import {
|
|
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
|
|
1024
|
-
import { resolve as
|
|
1025
|
-
import { existsSync as
|
|
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 (!
|
|
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 =
|
|
1119
|
-
const content = await
|
|
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
|
|
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
|