amai 0.0.4 → 0.0.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.
package/dist/cli.cjs CHANGED
@@ -9,6 +9,7 @@ var promises = require('fs/promises');
9
9
  var path9 = require('path');
10
10
  var fs3 = require('fs');
11
11
  var os2 = require('os');
12
+ var crypto = require('crypto');
12
13
  var child_process = require('child_process');
13
14
  var util = require('util');
14
15
  var hono = require('hono');
@@ -612,15 +613,133 @@ function calculateDiffStats(oldContent, newContent) {
612
613
  }
613
614
  return { linesAdded, linesRemoved };
614
615
  }
615
-
616
- // src/tools/apply-patch.ts
616
+ var CheckpointStore = class {
617
+ checkpoints = /* @__PURE__ */ new Map();
618
+ fileCheckpoints = /* @__PURE__ */ new Map();
619
+ // filePath -> checkpointIds
620
+ /**
621
+ * Compute SHA-256 hash of content
622
+ */
623
+ computeHash(content) {
624
+ return crypto.createHash("sha256").update(content, "utf8").digest("hex");
625
+ }
626
+ /**
627
+ * Create a new checkpoint before an edit operation
628
+ */
629
+ createCheckpoint(id, filePath, beforeContent, afterContent) {
630
+ const checkpoint = {
631
+ id,
632
+ filePath,
633
+ beforeContent,
634
+ afterContent,
635
+ beforeHash: this.computeHash(beforeContent),
636
+ afterHash: this.computeHash(afterContent),
637
+ timestamp: Date.now()
638
+ };
639
+ this.checkpoints.set(id, checkpoint);
640
+ const fileCheckpointIds = this.fileCheckpoints.get(filePath) || [];
641
+ fileCheckpointIds.push(id);
642
+ this.fileCheckpoints.set(filePath, fileCheckpointIds);
643
+ return checkpoint;
644
+ }
645
+ /**
646
+ * Get a checkpoint by ID
647
+ */
648
+ getCheckpoint(id) {
649
+ return this.checkpoints.get(id);
650
+ }
651
+ /**
652
+ * Get all checkpoints for a file (ordered by timestamp)
653
+ */
654
+ getCheckpointsForFile(filePath) {
655
+ const ids = this.fileCheckpoints.get(filePath) || [];
656
+ return ids.map((id) => this.checkpoints.get(id)).filter((cp) => cp !== void 0).sort((a, b) => a.timestamp - b.timestamp);
657
+ }
658
+ /**
659
+ * Verify if current file content matches expected state
660
+ * Returns true if safe to revert
661
+ */
662
+ verifyFileState(checkpointId, currentContent) {
663
+ const checkpoint = this.checkpoints.get(checkpointId);
664
+ if (!checkpoint) {
665
+ return {
666
+ safe: false,
667
+ reason: "Checkpoint not found"
668
+ };
669
+ }
670
+ const currentHash = this.computeHash(currentContent);
671
+ if (currentHash === checkpoint.afterHash) {
672
+ return {
673
+ safe: true,
674
+ checkpoint,
675
+ currentHash
676
+ };
677
+ }
678
+ if (currentHash === checkpoint.beforeHash) {
679
+ return {
680
+ safe: false,
681
+ reason: "File appears to already be reverted",
682
+ checkpoint,
683
+ currentHash
684
+ };
685
+ }
686
+ return {
687
+ safe: false,
688
+ reason: "File was modified after this edit. Current content does not match expected state.",
689
+ checkpoint,
690
+ currentHash
691
+ };
692
+ }
693
+ /**
694
+ * Remove a checkpoint after successful revert or accept
695
+ */
696
+ removeCheckpoint(id) {
697
+ const checkpoint = this.checkpoints.get(id);
698
+ if (!checkpoint) return false;
699
+ this.checkpoints.delete(id);
700
+ const fileCheckpointIds = this.fileCheckpoints.get(checkpoint.filePath);
701
+ if (fileCheckpointIds) {
702
+ const filtered = fileCheckpointIds.filter((cpId) => cpId !== id);
703
+ if (filtered.length === 0) {
704
+ this.fileCheckpoints.delete(checkpoint.filePath);
705
+ } else {
706
+ this.fileCheckpoints.set(checkpoint.filePath, filtered);
707
+ }
708
+ }
709
+ return true;
710
+ }
711
+ /**
712
+ * Get all checkpoints (for debugging/listing)
713
+ */
714
+ getAllCheckpoints() {
715
+ return Array.from(this.checkpoints.values()).sort((a, b) => b.timestamp - a.timestamp);
716
+ }
717
+ /**
718
+ * Clear all checkpoints (for cleanup)
719
+ */
720
+ clear() {
721
+ this.checkpoints.clear();
722
+ this.fileCheckpoints.clear();
723
+ }
724
+ /**
725
+ * Get statistics
726
+ */
727
+ getStats() {
728
+ return {
729
+ totalCheckpoints: this.checkpoints.size,
730
+ filesTracked: this.fileCheckpoints.size
731
+ };
732
+ }
733
+ };
734
+ var checkpointStore = new CheckpointStore();
617
735
  zod.z.object({
618
736
  file_path: zod.z.string().describe("The path to the file you want to search and replace in. You can use either a relative path in the workspace or an absolute path. If an absolute path is provided, it will be preserved as is"),
619
737
  new_string: zod.z.string().describe("The edited text to replace the old_string (must be different from the old_string)"),
620
- old_string: zod.z.string().describe("The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)")
738
+ old_string: zod.z.string().describe("The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)"),
739
+ toolCallId: zod.z.string().optional().describe("Optional tool call ID for checkpoint tracking")
621
740
  });
622
741
  var apply_patch = async function(input, projectCwd) {
623
- const { file_path, new_string, old_string } = input;
742
+ const { file_path, new_string, old_string, toolCallId } = input;
624
743
  try {
625
744
  if (!file_path) {
626
745
  return {
@@ -695,6 +814,13 @@ var apply_patch = async function(input, projectCwd) {
695
814
  };
696
815
  }
697
816
  const newContent = fileContent.replace(old_string, new_string);
817
+ const checkpointId = toolCallId || crypto.randomUUID();
818
+ const checkpoint = checkpointStore.createCheckpoint(
819
+ checkpointId,
820
+ absolute_file_path,
821
+ fileContent,
822
+ newContent
823
+ );
698
824
  try {
699
825
  await promises.writeFile(absolute_file_path, newContent, "utf-8");
700
826
  const diffStats = calculateDiffStats(fileContent, newContent);
@@ -704,9 +830,14 @@ var apply_patch = async function(input, projectCwd) {
704
830
  new_string,
705
831
  linesAdded: diffStats.linesAdded,
706
832
  linesRemoved: diffStats.linesRemoved,
707
- message: `Successfully replaced string in file: ${file_path}`
833
+ message: `Successfully replaced string in file: ${file_path}`,
834
+ // Include checkpoint info for frontend
835
+ checkpointId: checkpoint.id,
836
+ beforeHash: checkpoint.beforeHash,
837
+ afterHash: checkpoint.afterHash
708
838
  };
709
839
  } catch (error) {
840
+ checkpointStore.removeCheckpoint(checkpointId);
710
841
  return {
711
842
  success: false,
712
843
  message: `Failed to write to file: ${file_path}`,
@@ -724,10 +855,11 @@ var apply_patch = async function(input, projectCwd) {
724
855
  zod.z.object({
725
856
  target_file: zod.z.string().describe("The relative path to the file to modify. The tool will create any directories in the path that don't exist"),
726
857
  content: zod.z.string().describe("The content to write to the file"),
727
- providedNewFile: zod.z.boolean().describe("The new file content to write to the file").optional()
858
+ providedNewFile: zod.z.boolean().describe("The new file content to write to the file").optional(),
859
+ toolCallId: zod.z.string().optional().describe("Optional tool call ID for checkpoint tracking")
728
860
  });
729
861
  var editFiles = async function(input, projectCwd) {
730
- const { target_file, content, providedNewFile } = input;
862
+ const { target_file, content, providedNewFile, toolCallId } = input;
731
863
  try {
732
864
  if (projectCwd) {
733
865
  const validation = validatePath(target_file, projectCwd);
@@ -759,7 +891,19 @@ var editFiles = async function(input, projectCwd) {
759
891
  isNewFile = true;
760
892
  }
761
893
  }
762
- await fs3__default.default.promises.writeFile(filePath, content);
894
+ const checkpointId = toolCallId || crypto.randomUUID();
895
+ const checkpoint = checkpointStore.createCheckpoint(
896
+ checkpointId,
897
+ filePath,
898
+ existingContent,
899
+ content
900
+ );
901
+ try {
902
+ await fs3__default.default.promises.writeFile(filePath, content);
903
+ } catch (writeError) {
904
+ checkpointStore.removeCheckpoint(checkpointId);
905
+ throw writeError;
906
+ }
763
907
  const diffStats = calculateDiffStats(existingContent, content);
764
908
  if (isNewFile) {
765
909
  return {
@@ -769,7 +913,11 @@ var editFiles = async function(input, projectCwd) {
769
913
  new_string: content,
770
914
  message: `Created new file: ${target_file}`,
771
915
  linesAdded: diffStats.linesAdded,
772
- linesRemoved: diffStats.linesRemoved
916
+ linesRemoved: diffStats.linesRemoved,
917
+ // Include checkpoint info for frontend
918
+ checkpointId: checkpoint.id,
919
+ beforeHash: checkpoint.beforeHash,
920
+ afterHash: checkpoint.afterHash
773
921
  };
774
922
  } else {
775
923
  return {
@@ -779,7 +927,11 @@ var editFiles = async function(input, projectCwd) {
779
927
  new_string: content,
780
928
  message: `Modified file: ${target_file}`,
781
929
  linesAdded: diffStats.linesAdded,
782
- linesRemoved: diffStats.linesRemoved
930
+ linesRemoved: diffStats.linesRemoved,
931
+ // Include checkpoint info for frontend
932
+ checkpointId: checkpoint.id,
933
+ beforeHash: checkpoint.beforeHash,
934
+ afterHash: checkpoint.afterHash
783
935
  };
784
936
  }
785
937
  } catch (error) {
@@ -1335,7 +1487,15 @@ var startHttpServer = (connection) => {
1335
1487
  });
1336
1488
  app.post("/revert", async (c) => {
1337
1489
  try {
1338
- const { filePath, oldString, newString, projectCwd } = await c.req.json();
1490
+ const {
1491
+ filePath,
1492
+ oldString,
1493
+ newString,
1494
+ projectCwd,
1495
+ checkpointId,
1496
+ expectedAfterHash,
1497
+ force = false
1498
+ } = await c.req.json();
1339
1499
  if (!filePath || oldString === void 0) {
1340
1500
  return c.json({ error: "filePath and oldString required" }, 400);
1341
1501
  }
@@ -1359,25 +1519,127 @@ var startHttpServer = (connection) => {
1359
1519
  }
1360
1520
  return c.json({ error: `Failed to read file: ${error.message}` }, 500);
1361
1521
  }
1522
+ if (checkpointId) {
1523
+ const verification = checkpointStore.verifyFileState(checkpointId, currentContent);
1524
+ if (!verification.safe && !force) {
1525
+ return c.json({
1526
+ success: false,
1527
+ conflict: true,
1528
+ error: verification.reason,
1529
+ currentHash: verification.currentHash,
1530
+ expectedHash: verification.checkpoint?.afterHash,
1531
+ checkpointId
1532
+ }, 409);
1533
+ }
1534
+ if (verification.checkpoint) {
1535
+ try {
1536
+ await promises.writeFile(resolved, verification.checkpoint.beforeContent, "utf-8");
1537
+ checkpointStore.removeCheckpoint(checkpointId);
1538
+ return c.json({ success: true, usedCheckpoint: true });
1539
+ } catch (writeError) {
1540
+ return c.json({ error: `Failed to write file: ${writeError.message}` }, 500);
1541
+ }
1542
+ }
1543
+ }
1544
+ if (expectedAfterHash && !force) {
1545
+ const currentHash = checkpointStore.computeHash(currentContent);
1546
+ if (currentHash !== expectedAfterHash) {
1547
+ return c.json({
1548
+ success: false,
1549
+ conflict: true,
1550
+ error: "File was modified after this edit. Current content does not match expected state.",
1551
+ currentHash,
1552
+ expectedHash: expectedAfterHash
1553
+ }, 409);
1554
+ }
1555
+ }
1362
1556
  let finalContent;
1363
1557
  if (newString && newString !== oldString) {
1364
1558
  if (!currentContent.includes(newString)) {
1365
- return c.json({ error: "Cannot revert: the new content is not found in the current file. The file may have been modified." }, 400);
1559
+ return c.json({
1560
+ success: false,
1561
+ conflict: true,
1562
+ error: "Cannot revert: the new content is not found in the current file. The file may have been modified."
1563
+ }, 409);
1366
1564
  }
1367
1565
  const occurrences = currentContent.split(newString).length - 1;
1368
1566
  if (occurrences > 1) {
1369
- return c.json({ error: "Cannot revert: the new content appears multiple times in the file" }, 400);
1567
+ return c.json({
1568
+ success: false,
1569
+ conflict: true,
1570
+ error: "Cannot revert: the new content appears multiple times in the file"
1571
+ }, 409);
1370
1572
  }
1371
1573
  finalContent = currentContent.replace(newString, oldString);
1372
1574
  } else {
1373
1575
  finalContent = oldString;
1374
1576
  }
1375
1577
  await promises.writeFile(resolved, finalContent, "utf-8");
1578
+ if (checkpointId) {
1579
+ checkpointStore.removeCheckpoint(checkpointId);
1580
+ }
1376
1581
  return c.json({ success: true });
1377
1582
  } catch (error) {
1378
1583
  return c.json({ error: error.message }, 500);
1379
1584
  }
1380
1585
  });
1586
+ app.post("/revert/force", async (c) => {
1587
+ try {
1588
+ const { filePath, checkpointId, projectCwd } = await c.req.json();
1589
+ if (!checkpointId) {
1590
+ return c.json({ error: "checkpointId is required for force revert" }, 400);
1591
+ }
1592
+ const checkpoint = checkpointStore.getCheckpoint(checkpointId);
1593
+ if (!checkpoint) {
1594
+ return c.json({ error: "Checkpoint not found" }, 404);
1595
+ }
1596
+ let resolved;
1597
+ if (projectCwd) {
1598
+ resolved = path9__default.default.isAbsolute(filePath || checkpoint.filePath) ? filePath || checkpoint.filePath : path9__default.default.resolve(projectCwd, filePath || checkpoint.filePath);
1599
+ const normalizedResolved = path9__default.default.normalize(resolved);
1600
+ const normalizedCwd = path9__default.default.normalize(projectCwd);
1601
+ if (!normalizedResolved.startsWith(normalizedCwd)) {
1602
+ return c.json({ error: "Path is outside project directory" }, 403);
1603
+ }
1604
+ } else {
1605
+ resolved = checkpoint.filePath;
1606
+ }
1607
+ try {
1608
+ await promises.writeFile(resolved, checkpoint.beforeContent, "utf-8");
1609
+ checkpointStore.removeCheckpoint(checkpointId);
1610
+ return c.json({ success: true, forced: true });
1611
+ } catch (writeError) {
1612
+ return c.json({ error: `Failed to write file: ${writeError.message}` }, 500);
1613
+ }
1614
+ } catch (error) {
1615
+ return c.json({ error: error.message }, 500);
1616
+ }
1617
+ });
1618
+ app.get("/checkpoints/:checkpointId", (c) => {
1619
+ const checkpointId = c.req.param("checkpointId");
1620
+ const checkpoint = checkpointStore.getCheckpoint(checkpointId);
1621
+ if (!checkpoint) {
1622
+ return c.json({ error: "Checkpoint not found" }, 404);
1623
+ }
1624
+ return c.json({
1625
+ id: checkpoint.id,
1626
+ filePath: checkpoint.filePath,
1627
+ beforeHash: checkpoint.beforeHash,
1628
+ afterHash: checkpoint.afterHash,
1629
+ timestamp: checkpoint.timestamp
1630
+ });
1631
+ });
1632
+ app.get("/checkpoints", (c) => {
1633
+ const stats = checkpointStore.getStats();
1634
+ const checkpoints = checkpointStore.getAllCheckpoints().map((cp) => ({
1635
+ id: cp.id,
1636
+ filePath: cp.filePath,
1637
+ beforeHash: cp.beforeHash,
1638
+ afterHash: cp.afterHash,
1639
+ timestamp: cp.timestamp
1640
+ }));
1641
+ return c.json({ stats, checkpoints });
1642
+ });
1381
1643
  app.get("/projects", (c) => {
1382
1644
  const projects = projectRegistry.list();
1383
1645
  return c.json({ projects });
@@ -1895,7 +2157,7 @@ function getDaemonPid() {
1895
2157
  return null;
1896
2158
  }
1897
2159
  }
1898
- var VERSION = "0.0.4";
2160
+ var VERSION = "0.0.5";
1899
2161
  var PROJECT_DIR = process.cwd();
1900
2162
  function promptUser(question) {
1901
2163
  const rl = readline__default.default.createInterface({