@mindstudio-ai/remy 0.1.195 → 0.1.197

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/headless.js CHANGED
@@ -647,375 +647,215 @@ async function generateBackgroundAck(params) {
647
647
  }
648
648
  }
649
649
 
650
- // src/usageLedger.ts
651
- import fs5 from "fs";
652
- var LEDGER_FILE = ".logs/usage.ndjson";
653
- var fd = null;
654
- function nanoToDollars(nano) {
655
- return typeof nano === "number" ? nano / 1e9 : void 0;
656
- }
657
- function recordUsage(entry) {
658
- try {
659
- if (fd === null) {
660
- fs5.mkdirSync(".logs", { recursive: true });
661
- fd = fs5.openSync(LEDGER_FILE, "a");
650
+ // src/tools/spec/readSpec.ts
651
+ import fs5 from "fs/promises";
652
+
653
+ // src/tools/spec/_helpers.ts
654
+ var HEADING_RE = /^(#{1,6})\s+(.+)$/;
655
+ function parseHeadings(content) {
656
+ const lines = content.split("\n");
657
+ const headings = [];
658
+ for (let i = 0; i < lines.length; i++) {
659
+ const match = lines[i].match(HEADING_RE);
660
+ if (match) {
661
+ headings.push({
662
+ level: match[1].length,
663
+ text: match[2].trim(),
664
+ startLine: i,
665
+ contentStart: i + 1,
666
+ contentEnd: lines.length
667
+ // placeholder — resolved below
668
+ });
662
669
  }
663
- fs5.writeSync(fd, JSON.stringify(entry) + "\n");
664
- } catch {
665
670
  }
671
+ for (let i = 0; i < headings.length; i++) {
672
+ const current = headings[i];
673
+ let end = lines.length;
674
+ for (let j = i + 1; j < headings.length; j++) {
675
+ if (headings[j].level <= current.level) {
676
+ end = headings[j].startLine;
677
+ break;
678
+ }
679
+ }
680
+ current.contentEnd = end;
681
+ }
682
+ return headings;
666
683
  }
667
-
668
- // src/compaction/index.ts
669
- var log3 = createLogger("compaction");
670
- var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
671
- var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
672
- var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
673
- async function compactConversation(messages, apiConfig, system, tools2, model) {
674
- const endIndex = findSafeInsertionPoint(messages);
675
- const summaries = [];
676
- const tasks = [];
677
- const conversationMessages = getConversationMessagesForSummary(
678
- messages,
679
- endIndex
680
- );
681
- if (conversationMessages.length > 0) {
682
- tasks.push(
683
- generateSummary(
684
- apiConfig,
685
- "conversation",
686
- CONVERSATION_SUMMARY_PROMPT,
687
- conversationMessages,
688
- system,
689
- tools2,
690
- model
691
- ).then((text) => {
692
- if (text) {
693
- summaries.push({ name: "conversation", text });
694
- }
695
- })
696
- );
684
+ function resolveHeadingPath(content, headingPath) {
685
+ const lines = content.split("\n");
686
+ const headings = parseHeadings(content);
687
+ if (headingPath === "") {
688
+ const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
689
+ return {
690
+ startLine: 0,
691
+ contentStart: 0,
692
+ contentEnd: firstHeadingLine
693
+ };
697
694
  }
698
- for (const name of SUMMARIZABLE_SUBAGENTS) {
699
- const subagentMessages = getSubAgentMessagesForSummary(
700
- messages,
701
- name,
702
- endIndex
695
+ const segments = headingPath.split(">").map((s) => s.trim());
696
+ let searchStart = 0;
697
+ let searchEnd = lines.length;
698
+ let resolved = null;
699
+ for (let si = 0; si < segments.length; si++) {
700
+ const segment = segments[si].toLowerCase();
701
+ const candidates = headings.filter(
702
+ (h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
703
703
  );
704
- if (subagentMessages.length > 0) {
705
- tasks.push(
706
- generateSummary(
707
- apiConfig,
708
- name,
709
- SUBAGENT_SUMMARY_PROMPT,
710
- subagentMessages,
711
- system,
712
- tools2,
713
- model
714
- ).then((text) => {
715
- if (text) {
716
- summaries.push({ name, text });
717
- }
718
- })
704
+ if (candidates.length === 0) {
705
+ const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
706
+ const searchedPath = segments.slice(0, si + 1).join(" > ");
707
+ throw new Error(
708
+ `Heading not found: "${searchedPath}"
709
+
710
+ Available headings:
711
+ ${available || "(none)"}`
719
712
  );
720
713
  }
714
+ resolved = candidates[0];
715
+ searchStart = resolved.contentStart;
716
+ searchEnd = resolved.contentEnd;
721
717
  }
722
- await Promise.all(tasks);
723
- const checkpointMessages = summaries.map((s) => ({
724
- role: "user",
725
- hidden: true,
726
- content: [
727
- {
728
- type: "summary",
729
- name: s.name,
730
- text: s.text,
731
- startedAt: Date.now()
732
- }
733
- ]
734
- }));
735
- log3.info("Compaction complete", { summaries: summaries.length });
736
- return checkpointMessages;
718
+ return {
719
+ startLine: resolved.startLine,
720
+ contentStart: resolved.contentStart,
721
+ contentEnd: resolved.contentEnd
722
+ };
737
723
  }
738
- function findSafeInsertionPoint(messages) {
739
- let idx = messages.length;
740
- while (idx > 0) {
741
- const msg = messages[idx - 1];
742
- if (msg.role === "user" && msg.toolCallId) {
743
- idx--;
744
- } else {
745
- break;
746
- }
747
- }
748
- if (idx < messages.length && idx > 0) {
749
- const msg = messages[idx - 1];
750
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
751
- const hasToolUse = msg.content.some(
752
- (b) => b.type === "tool"
753
- );
754
- if (hasToolUse) {
755
- idx--;
756
- }
757
- }
724
+ function validateSpecPath(filePath) {
725
+ if (!filePath.startsWith("src/")) {
726
+ throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
758
727
  }
759
- return idx;
728
+ return filePath;
760
729
  }
761
- function getConversationMessagesForSummary(messages, endIndex) {
762
- let startIdx = 0;
763
- for (let i = endIndex - 1; i >= 0; i--) {
764
- const msg = messages[i];
765
- if (!Array.isArray(msg.content)) {
766
- continue;
767
- }
768
- for (const block of msg.content) {
769
- if (block.type === "summary" && block.name === "conversation") {
770
- startIdx = i + 1;
771
- break;
772
- }
773
- }
774
- if (startIdx > 0) {
775
- break;
776
- }
730
+ function getHeadingTree(content) {
731
+ const headings = parseHeadings(content);
732
+ if (headings.length === 0) {
733
+ return "(no headings)";
777
734
  }
778
- return messages.slice(startIdx, endIndex);
735
+ return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
779
736
  }
780
- function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
781
- let checkpointIdx = -1;
782
- for (let i = endIndex - 1; i >= 0; i--) {
783
- const msg = messages[i];
784
- if (!Array.isArray(msg.content)) {
785
- continue;
737
+
738
+ // src/tools/spec/readSpec.ts
739
+ var DEFAULT_MAX_LINES = 500;
740
+ var readSpecTool = {
741
+ clearable: true,
742
+ definition: {
743
+ name: "readSpec",
744
+ description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
745
+ inputSchema: {
746
+ type: "object",
747
+ properties: {
748
+ path: {
749
+ type: "string",
750
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
751
+ },
752
+ offset: {
753
+ type: "number",
754
+ description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
755
+ },
756
+ maxLines: {
757
+ type: "number",
758
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
759
+ }
760
+ },
761
+ required: ["path"]
786
762
  }
787
- for (const block of msg.content) {
788
- if (block.type === "summary" && block.name === subAgentName) {
789
- checkpointIdx = i;
790
- break;
791
- }
763
+ },
764
+ async execute(input) {
765
+ try {
766
+ validateSpecPath(input.path);
767
+ } catch (err) {
768
+ return `Error: ${err.message}`;
792
769
  }
793
- if (checkpointIdx !== -1) {
794
- break;
770
+ try {
771
+ const content = await fs5.readFile(input.path, "utf-8");
772
+ const allLines = content.split("\n");
773
+ const totalLines = allLines.length;
774
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
775
+ let startIdx;
776
+ if (input.offset && input.offset < 0) {
777
+ startIdx = Math.max(0, totalLines + input.offset);
778
+ } else {
779
+ startIdx = Math.max(0, (input.offset || 1) - 1);
780
+ }
781
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
782
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
783
+ let result = numbered;
784
+ const endLine = startIdx + sliced.length;
785
+ const displayStart = startIdx + 1;
786
+ if (endLine < totalLines) {
787
+ result += `
788
+
789
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
790
+ }
791
+ return result;
792
+ } catch (err) {
793
+ return `Error reading file: ${err.message}`;
795
794
  }
796
795
  }
797
- const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
798
- const collected = [];
799
- for (let i = startIdx; i < endIndex; i++) {
800
- const msg = messages[i];
801
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
802
- continue;
803
- }
804
- for (const block of msg.content) {
805
- if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
806
- collected.push(...block.subAgentMessages);
807
- }
808
- }
809
- }
810
- return collected;
811
- }
812
- function serializeForSummary(messages) {
813
- return messages.map((msg) => {
814
- if (typeof msg.content === "string") {
815
- return `[${msg.role}]: ${msg.content}`;
816
- }
817
- if (!Array.isArray(msg.content)) {
818
- return `[${msg.role}]: (empty)`;
819
- }
820
- const blocks = msg.content;
821
- const parts = [];
822
- for (const block of blocks) {
823
- if (block.type === "text") {
824
- parts.push(block.text);
825
- } else if (block.type === "tool") {
826
- parts.push(
827
- `[tool: ${block.name}(${JSON.stringify(block.input).slice(0, 200)})] \u2192 ${(block.result ?? "").slice(0, 500)}`
828
- );
829
- }
830
- }
831
- return `[${msg.role}]: ${parts.join("\n")}`;
832
- }).join("\n\n");
833
- }
834
- var CHUNK_CHAR_LIMIT = 24e5;
835
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
836
- const serialized = serializeForSummary(messagesToSummarize);
837
- if (!serialized.trim()) {
838
- return null;
839
- }
840
- if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
841
- const mid = Math.floor(messagesToSummarize.length / 2);
842
- log3.info("Chunking summary", {
843
- name,
844
- messageCount: messagesToSummarize.length,
845
- serializedLength: serialized.length
846
- });
847
- const [first, second] = await Promise.all([
848
- generateSummary(
849
- apiConfig,
850
- `${name} [pt1]`,
851
- compactionPrompt,
852
- messagesToSummarize.slice(0, mid),
853
- mainSystem,
854
- mainTools,
855
- model
856
- ),
857
- generateSummary(
858
- apiConfig,
859
- `${name} [pt2]`,
860
- compactionPrompt,
861
- messagesToSummarize.slice(mid),
862
- mainSystem,
863
- mainTools,
864
- model
865
- )
866
- ]);
867
- const parts = [first, second].filter((p) => !!p);
868
- return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
869
- }
870
- log3.info("Generating summary", {
871
- name,
872
- messageCount: messagesToSummarize.length,
873
- cacheReuse: !!mainSystem
874
- });
875
- let summaryText = "";
876
- const useMainCache = !!mainSystem;
877
- const system = useMainCache ? mainSystem : compactionPrompt;
878
- const tools2 = [];
879
- const userContent = useMainCache ? `${compactionPrompt}
880
-
881
- ---
882
-
883
- Conversation to summarize:
884
-
885
- ${serialized}` : serialized;
886
- const iterStart = Date.now();
887
- for await (const event of streamChat({
888
- ...apiConfig,
889
- model,
890
- subAgentId: "conversationSummarizer",
891
- system,
892
- messages: [{ role: "user", content: userContent }],
893
- tools: tools2
894
- })) {
895
- if (event.type === "text") {
896
- summaryText += event.text;
897
- } else if (event.type === "done") {
898
- recordUsage({
899
- ts: Date.now(),
900
- agentName: "conversationSummarizer",
901
- modelId: event.modelId,
902
- inputTokens: event.usage.inputTokens,
903
- outputTokens: event.usage.outputTokens,
904
- cacheCreationTokens: event.usage.cacheCreationTokens,
905
- cacheReadTokens: event.usage.cacheReadTokens,
906
- cost: nanoToDollars(event.cost),
907
- billingEvents: event.billingEvents,
908
- durationMs: Date.now() - iterStart,
909
- toolNames: []
910
- });
911
- } else if (event.type === "error") {
912
- log3.error("Summary generation failed", { name, error: event.error });
913
- return null;
914
- }
915
- }
916
- if (!summaryText.trim()) {
917
- log3.warn("Empty summary generated", { name });
918
- return null;
919
- }
920
- log3.info("Summary generated", { name, summaryLength: summaryText.length });
921
- return summaryText.trim();
922
- }
796
+ };
923
797
 
924
- // src/tools/spec/readSpec.ts
798
+ // src/tools/spec/writeSpec.ts
925
799
  import fs6 from "fs/promises";
800
+ import path4 from "path";
926
801
 
927
- // src/tools/spec/_helpers.ts
928
- var HEADING_RE = /^(#{1,6})\s+(.+)$/;
929
- function parseHeadings(content) {
930
- const lines = content.split("\n");
931
- const headings = [];
932
- for (let i = 0; i < lines.length; i++) {
933
- const match = lines[i].match(HEADING_RE);
934
- if (match) {
935
- headings.push({
936
- level: match[1].length,
937
- text: match[2].trim(),
938
- startLine: i,
939
- contentStart: i + 1,
940
- contentEnd: lines.length
941
- // placeholder — resolved below
942
- });
943
- }
802
+ // src/tools/_helpers/diff.ts
803
+ var CONTEXT_LINES = 3;
804
+ function unifiedDiff(filePath, oldText, newText) {
805
+ const oldLines = oldText ? oldText.split("\n") : [];
806
+ const newLines = newText ? newText.split("\n") : [];
807
+ let firstDiff = 0;
808
+ while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
809
+ firstDiff++;
944
810
  }
945
- for (let i = 0; i < headings.length; i++) {
946
- const current = headings[i];
947
- let end = lines.length;
948
- for (let j = i + 1; j < headings.length; j++) {
949
- if (headings[j].level <= current.level) {
950
- end = headings[j].startLine;
951
- break;
952
- }
953
- }
954
- current.contentEnd = end;
811
+ let oldEnd = oldLines.length - 1;
812
+ let newEnd = newLines.length - 1;
813
+ while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
814
+ oldEnd--;
815
+ newEnd--;
955
816
  }
956
- return headings;
957
- }
958
- function resolveHeadingPath(content, headingPath) {
959
- const lines = content.split("\n");
960
- const headings = parseHeadings(content);
961
- if (headingPath === "") {
962
- const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
963
- return {
964
- startLine: 0,
965
- contentStart: 0,
966
- contentEnd: firstHeadingLine
967
- };
817
+ const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
818
+ const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
819
+ const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
820
+ const lines = [];
821
+ lines.push(`--- ${filePath}`);
822
+ lines.push(`+++ ${filePath}`);
823
+ lines.push(
824
+ `@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
825
+ );
826
+ for (let i = ctxStart; i < firstDiff; i++) {
827
+ lines.push(` ${oldLines[i]}`);
968
828
  }
969
- const segments = headingPath.split(">").map((s) => s.trim());
970
- let searchStart = 0;
971
- let searchEnd = lines.length;
972
- let resolved = null;
973
- for (let si = 0; si < segments.length; si++) {
974
- const segment = segments[si].toLowerCase();
975
- const candidates = headings.filter(
976
- (h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
977
- );
978
- if (candidates.length === 0) {
979
- const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
980
- const searchedPath = segments.slice(0, si + 1).join(" > ");
981
- throw new Error(
982
- `Heading not found: "${searchedPath}"
983
-
984
- Available headings:
985
- ${available || "(none)"}`
986
- );
987
- }
988
- resolved = candidates[0];
989
- searchStart = resolved.contentStart;
990
- searchEnd = resolved.contentEnd;
829
+ for (let i = firstDiff; i <= oldEnd; i++) {
830
+ lines.push(`-${oldLines[i]}`);
991
831
  }
992
- return {
993
- startLine: resolved.startLine,
994
- contentStart: resolved.contentStart,
995
- contentEnd: resolved.contentEnd
996
- };
997
- }
998
- function validateSpecPath(filePath) {
999
- if (!filePath.startsWith("src/")) {
1000
- throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
832
+ for (let i = firstDiff; i <= newEnd; i++) {
833
+ lines.push(`+${newLines[i]}`);
1001
834
  }
1002
- return filePath;
1003
- }
1004
- function getHeadingTree(content) {
1005
- const headings = parseHeadings(content);
1006
- if (headings.length === 0) {
1007
- return "(no headings)";
835
+ for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
836
+ lines.push(` ${oldLines[i]}`);
1008
837
  }
1009
- return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
838
+ return lines.join("\n");
1010
839
  }
1011
840
 
1012
- // src/tools/spec/readSpec.ts
1013
- var DEFAULT_MAX_LINES = 500;
1014
- var readSpecTool = {
841
+ // src/tools/_helpers/fileLock.ts
842
+ var locks = /* @__PURE__ */ new Map();
843
+ function acquireFileLock(filePath) {
844
+ let release;
845
+ const next = new Promise((res) => {
846
+ release = res;
847
+ });
848
+ const wait = locks.get(filePath) ?? Promise.resolve();
849
+ locks.set(filePath, next);
850
+ return wait.then(() => release);
851
+ }
852
+
853
+ // src/tools/spec/writeSpec.ts
854
+ var writeSpecTool = {
1015
855
  clearable: true,
1016
856
  definition: {
1017
- name: "readSpec",
1018
- description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
857
+ name: "writeSpec",
858
+ description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
1019
859
  inputSchema: {
1020
860
  type: "object",
1021
861
  properties: {
@@ -1023,131 +863,17 @@ var readSpecTool = {
1023
863
  type: "string",
1024
864
  description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
1025
865
  },
1026
- offset: {
1027
- type: "number",
1028
- description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
1029
- },
1030
- maxLines: {
1031
- type: "number",
1032
- description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
866
+ content: {
867
+ type: "string",
868
+ description: "The full MSFM markdown content to write."
1033
869
  }
1034
870
  },
1035
- required: ["path"]
1036
- }
1037
- },
1038
- async execute(input) {
1039
- try {
1040
- validateSpecPath(input.path);
1041
- } catch (err) {
1042
- return `Error: ${err.message}`;
1043
- }
1044
- try {
1045
- const content = await fs6.readFile(input.path, "utf-8");
1046
- const allLines = content.split("\n");
1047
- const totalLines = allLines.length;
1048
- const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
1049
- let startIdx;
1050
- if (input.offset && input.offset < 0) {
1051
- startIdx = Math.max(0, totalLines + input.offset);
1052
- } else {
1053
- startIdx = Math.max(0, (input.offset || 1) - 1);
1054
- }
1055
- const sliced = allLines.slice(startIdx, startIdx + maxLines);
1056
- const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
1057
- let result = numbered;
1058
- const endLine = startIdx + sliced.length;
1059
- const displayStart = startIdx + 1;
1060
- if (endLine < totalLines) {
1061
- result += `
1062
-
1063
- (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
1064
- }
1065
- return result;
1066
- } catch (err) {
1067
- return `Error reading file: ${err.message}`;
1068
- }
1069
- }
1070
- };
1071
-
1072
- // src/tools/spec/writeSpec.ts
1073
- import fs7 from "fs/promises";
1074
- import path4 from "path";
1075
-
1076
- // src/tools/_helpers/diff.ts
1077
- var CONTEXT_LINES = 3;
1078
- function unifiedDiff(filePath, oldText, newText) {
1079
- const oldLines = oldText ? oldText.split("\n") : [];
1080
- const newLines = newText ? newText.split("\n") : [];
1081
- let firstDiff = 0;
1082
- while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
1083
- firstDiff++;
1084
- }
1085
- let oldEnd = oldLines.length - 1;
1086
- let newEnd = newLines.length - 1;
1087
- while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
1088
- oldEnd--;
1089
- newEnd--;
1090
- }
1091
- const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
1092
- const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
1093
- const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
1094
- const lines = [];
1095
- lines.push(`--- ${filePath}`);
1096
- lines.push(`+++ ${filePath}`);
1097
- lines.push(
1098
- `@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
1099
- );
1100
- for (let i = ctxStart; i < firstDiff; i++) {
1101
- lines.push(` ${oldLines[i]}`);
1102
- }
1103
- for (let i = firstDiff; i <= oldEnd; i++) {
1104
- lines.push(`-${oldLines[i]}`);
1105
- }
1106
- for (let i = firstDiff; i <= newEnd; i++) {
1107
- lines.push(`+${newLines[i]}`);
1108
- }
1109
- for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
1110
- lines.push(` ${oldLines[i]}`);
1111
- }
1112
- return lines.join("\n");
1113
- }
1114
-
1115
- // src/tools/_helpers/fileLock.ts
1116
- var locks = /* @__PURE__ */ new Map();
1117
- function acquireFileLock(filePath) {
1118
- let release;
1119
- const next = new Promise((res) => {
1120
- release = res;
1121
- });
1122
- const wait = locks.get(filePath) ?? Promise.resolve();
1123
- locks.set(filePath, next);
1124
- return wait.then(() => release);
1125
- }
1126
-
1127
- // src/tools/spec/writeSpec.ts
1128
- var writeSpecTool = {
1129
- clearable: true,
1130
- definition: {
1131
- name: "writeSpec",
1132
- description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
1133
- inputSchema: {
1134
- type: "object",
1135
- properties: {
1136
- path: {
1137
- type: "string",
1138
- description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
1139
- },
1140
- content: {
1141
- type: "string",
1142
- description: "The full MSFM markdown content to write."
1143
- }
1144
- },
1145
- required: ["path", "content"]
871
+ required: ["path", "content"]
1146
872
  }
1147
873
  },
1148
874
  streaming: {
1149
875
  transform: async (partial) => {
1150
- const oldContent = await fs7.readFile(partial.path, "utf-8").catch(() => "");
876
+ const oldContent = await fs6.readFile(partial.path, "utf-8").catch(() => "");
1151
877
  const lineCount = partial.content.split("\n").length;
1152
878
  return `Writing ${partial.path} (${lineCount} lines)
1153
879
  ${unifiedDiff(partial.path, oldContent, partial.content)}`;
@@ -1161,13 +887,13 @@ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
1161
887
  }
1162
888
  const release = await acquireFileLock(input.path);
1163
889
  try {
1164
- await fs7.mkdir(path4.dirname(input.path), { recursive: true });
890
+ await fs6.mkdir(path4.dirname(input.path), { recursive: true });
1165
891
  let oldContent = null;
1166
892
  try {
1167
- oldContent = await fs7.readFile(input.path, "utf-8");
893
+ oldContent = await fs6.readFile(input.path, "utf-8");
1168
894
  } catch {
1169
895
  }
1170
- await fs7.writeFile(input.path, input.content, "utf-8");
896
+ await fs6.writeFile(input.path, input.content, "utf-8");
1171
897
  const lineCount = input.content.split("\n").length;
1172
898
  const label = oldContent !== null ? "Wrote" : "Created";
1173
899
  return `${label} ${input.path} (${lineCount} lines)
@@ -1181,7 +907,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1181
907
  };
1182
908
 
1183
909
  // src/tools/spec/editSpec.ts
1184
- import fs8 from "fs/promises";
910
+ import fs7 from "fs/promises";
1185
911
  var editSpecTool = {
1186
912
  clearable: true,
1187
913
  definition: {
@@ -1231,7 +957,7 @@ var editSpecTool = {
1231
957
  try {
1232
958
  let originalContent;
1233
959
  try {
1234
- originalContent = await fs8.readFile(input.path, "utf-8");
960
+ originalContent = await fs7.readFile(input.path, "utf-8");
1235
961
  } catch (err) {
1236
962
  return `Error reading file: ${err.message}`;
1237
963
  }
@@ -1287,7 +1013,7 @@ ${tree}`;
1287
1013
  content = lines.join("\n");
1288
1014
  }
1289
1015
  try {
1290
- await fs8.writeFile(input.path, content, "utf-8");
1016
+ await fs7.writeFile(input.path, content, "utf-8");
1291
1017
  } catch (err) {
1292
1018
  return `Error writing file: ${err.message}`;
1293
1019
  }
@@ -1299,7 +1025,7 @@ ${tree}`;
1299
1025
  };
1300
1026
 
1301
1027
  // src/tools/spec/listSpecFiles.ts
1302
- import fs9 from "fs/promises";
1028
+ import fs8 from "fs/promises";
1303
1029
  import path5 from "path";
1304
1030
  var listSpecFilesTool = {
1305
1031
  clearable: false,
@@ -1329,7 +1055,7 @@ var listSpecFilesTool = {
1329
1055
  };
1330
1056
  async function listRecursive(dir) {
1331
1057
  const results = [];
1332
- const entries = await fs9.readdir(dir, { withFileTypes: true });
1058
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1333
1059
  entries.sort((a, b) => {
1334
1060
  if (a.isDirectory() && !b.isDirectory()) {
1335
1061
  return -1;
@@ -1375,7 +1101,7 @@ var presentPublishPlanTool = {
1375
1101
  };
1376
1102
 
1377
1103
  // src/tools/spec/writePlan.ts
1378
- import fs10 from "fs/promises";
1104
+ import fs9 from "fs/promises";
1379
1105
  var PLAN_FILE = ".remy-plan.md";
1380
1106
  var writePlanTool = {
1381
1107
  clearable: false,
@@ -1400,13 +1126,13 @@ status: pending
1400
1126
  ---
1401
1127
 
1402
1128
  ${content}`;
1403
- await fs10.writeFile(PLAN_FILE, file, "utf-8");
1129
+ await fs9.writeFile(PLAN_FILE, file, "utf-8");
1404
1130
  return "Plan written to .remy-plan.md. Waiting for user approval.";
1405
1131
  }
1406
1132
  };
1407
1133
 
1408
1134
  // src/tools/spec/updatePlanStatus.ts
1409
- import fs11 from "fs/promises";
1135
+ import fs10 from "fs/promises";
1410
1136
  var PLAN_FILE2 = ".remy-plan.md";
1411
1137
  var updatePlanStatusTool = {
1412
1138
  clearable: false,
@@ -1429,15 +1155,15 @@ var updatePlanStatusTool = {
1429
1155
  const status = input.status;
1430
1156
  let content;
1431
1157
  try {
1432
- content = await fs11.readFile(PLAN_FILE2, "utf-8");
1158
+ content = await fs10.readFile(PLAN_FILE2, "utf-8");
1433
1159
  } catch {
1434
1160
  return "No plan file found.";
1435
1161
  }
1436
1162
  if (status === "rejected") {
1437
- await fs11.unlink(PLAN_FILE2);
1163
+ await fs10.unlink(PLAN_FILE2);
1438
1164
  return "Plan rejected and removed.";
1439
1165
  }
1440
- await fs11.writeFile(
1166
+ await fs10.writeFile(
1441
1167
  PLAN_FILE2,
1442
1168
  content.replace(/^status:\s*\w+/m, `status: ${status}`),
1443
1169
  "utf-8"
@@ -1743,6 +1469,24 @@ var askMindStudioSdkTool = {
1743
1469
  }
1744
1470
  };
1745
1471
 
1472
+ // src/usageLedger.ts
1473
+ import fs11 from "fs";
1474
+ var LEDGER_FILE = ".logs/usage.ndjson";
1475
+ var fd = null;
1476
+ function nanoToDollars(nano) {
1477
+ return typeof nano === "number" ? nano / 1e9 : void 0;
1478
+ }
1479
+ function recordUsage(entry) {
1480
+ try {
1481
+ if (fd === null) {
1482
+ fs11.mkdirSync(".logs", { recursive: true });
1483
+ fd = fs11.openSync(LEDGER_FILE, "a");
1484
+ }
1485
+ fs11.writeSync(fd, JSON.stringify(entry) + "\n");
1486
+ } catch {
1487
+ }
1488
+ }
1489
+
1746
1490
  // src/subagents/common/runMindstudioCli.ts
1747
1491
  function stripFlags(args) {
1748
1492
  const out = [];
@@ -2584,11 +2328,11 @@ var editsFinishedTool = {
2584
2328
  };
2585
2329
 
2586
2330
  // src/tools/_helpers/sidecar.ts
2587
- var log4 = createLogger("sidecar");
2331
+ var log3 = createLogger("sidecar");
2588
2332
  var baseUrl = null;
2589
2333
  function setSidecarBaseUrl(url) {
2590
2334
  baseUrl = url;
2591
- log4.info("Configured", { url });
2335
+ log3.info("Configured", { url });
2592
2336
  }
2593
2337
  async function sidecarRequest(endpoint, body = {}, options) {
2594
2338
  if (!baseUrl) {
@@ -2603,7 +2347,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
2603
2347
  signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
2604
2348
  });
2605
2349
  if (!res.ok) {
2606
- log4.error("Sidecar error", { endpoint, status: res.status });
2350
+ log3.error("Sidecar error", { endpoint, status: res.status });
2607
2351
  throw new Error(`Sidecar error: ${res.status}`);
2608
2352
  }
2609
2353
  const data = await res.json();
@@ -2616,7 +2360,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
2616
2360
  if (err.message.startsWith("Sidecar error")) {
2617
2361
  throw err;
2618
2362
  }
2619
- log4.error("Sidecar connection error", { endpoint, error: err.message });
2363
+ log3.error("Sidecar connection error", { endpoint, error: err.message });
2620
2364
  throw new Error(`Sidecar connection error: ${err.message}`);
2621
2365
  }
2622
2366
  }
@@ -2907,7 +2651,7 @@ function acquireBrowserLock() {
2907
2651
  }
2908
2652
 
2909
2653
  // src/toolRegistry.ts
2910
- var log5 = createLogger("tool-registry");
2654
+ var log4 = createLogger("tool-registry");
2911
2655
  var USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
2912
2656
  var ToolRegistry = class {
2913
2657
  entries = /* @__PURE__ */ new Map();
@@ -2934,7 +2678,7 @@ var ToolRegistry = class {
2934
2678
  if (!entry) {
2935
2679
  return false;
2936
2680
  }
2937
- log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
2681
+ log4.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
2938
2682
  entry.abortController.abort(mode);
2939
2683
  if (mode === "graceful") {
2940
2684
  const partial = entry.getPartialResult?.() ?? "";
@@ -2967,7 +2711,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
2967
2711
  if (!entry) {
2968
2712
  return false;
2969
2713
  }
2970
- log5.info("Tool restarted", { toolCallId: id, name: entry.name });
2714
+ log4.info("Tool restarted", { toolCallId: id, name: entry.name });
2971
2715
  entry.abortController.abort("restart");
2972
2716
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
2973
2717
  this.onEvent?.({
@@ -3196,15 +2940,6 @@ ${content}` : attachmentHeader;
3196
2940
  const blocks = msg.content;
3197
2941
  const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3198
2942
  const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
3199
- const thinking = blocks.filter(
3200
- (b) => b.type === "thinking" || b.type === "redacted_thinking"
3201
- ).map(
3202
- (b) => b.type === "thinking" ? {
3203
- type: "thinking",
3204
- thinking: b.thinking,
3205
- signature: b.signature
3206
- } : { type: "redacted_thinking", data: b.data }
3207
- );
3208
2943
  const cleaned2 = {
3209
2944
  role: msg.role,
3210
2945
  content: text
@@ -3212,9 +2947,6 @@ ${content}` : attachmentHeader;
3212
2947
  if (toolCalls.length > 0) {
3213
2948
  cleaned2.toolCalls = toolCalls;
3214
2949
  }
3215
- if (thinking.length > 0) {
3216
- cleaned2.thinking = thinking;
3217
- }
3218
2950
  if (msg.providerMetadata) {
3219
2951
  cleaned2.providerMetadata = msg.providerMetadata;
3220
2952
  }
@@ -3227,7 +2959,7 @@ ${content}` : attachmentHeader;
3227
2959
  }
3228
2960
 
3229
2961
  // src/subagents/runner.ts
3230
- var log6 = createLogger("sub-agent");
2962
+ var log5 = createLogger("sub-agent");
3231
2963
  async function runSubAgent(config) {
3232
2964
  const {
3233
2965
  system,
@@ -3254,7 +2986,7 @@ async function runSubAgent(config) {
3254
2986
  const signal = background ? bgAbort.signal : parentSignal;
3255
2987
  const agentName = subAgentId || "sub-agent";
3256
2988
  const runStart = Date.now();
3257
- log6.info("Sub-agent started", { requestId, parentToolId, agentName });
2989
+ log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3258
2990
  const emit = (e) => {
3259
2991
  onEvent({ ...e, parentToolId });
3260
2992
  };
@@ -3471,7 +3203,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3471
3203
  ...hasArtifacts ? { artifacts } : {}
3472
3204
  };
3473
3205
  }
3474
- log6.info("Tools executing", {
3206
+ log5.info("Tools executing", {
3475
3207
  requestId,
3476
3208
  parentToolId,
3477
3209
  count: toolCalls.length,
@@ -3548,7 +3280,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3548
3280
  run2(tc.input);
3549
3281
  const r = await resultPromise;
3550
3282
  toolRegistry?.unregister(tc.id);
3551
- log6.info("Tool completed", {
3283
+ log5.info("Tool completed", {
3552
3284
  requestId,
3553
3285
  parentToolId,
3554
3286
  toolCallId: tc.id,
@@ -3599,7 +3331,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3599
3331
  const wrapRun = async () => {
3600
3332
  try {
3601
3333
  const result = await run();
3602
- log6.info("Sub-agent complete", {
3334
+ log5.info("Sub-agent complete", {
3603
3335
  requestId,
3604
3336
  parentToolId,
3605
3337
  agentName,
@@ -3608,7 +3340,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3608
3340
  });
3609
3341
  return result;
3610
3342
  } catch (err) {
3611
- log6.warn("Sub-agent error", {
3343
+ log5.warn("Sub-agent error", {
3612
3344
  requestId,
3613
3345
  parentToolId,
3614
3346
  agentName,
@@ -3620,7 +3352,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3620
3352
  if (!background) {
3621
3353
  return wrapRun();
3622
3354
  }
3623
- log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3355
+ log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3624
3356
  toolRegistry?.register({
3625
3357
  id: parentToolId,
3626
3358
  name: agentName,
@@ -3686,7 +3418,7 @@ var BROWSER_TOOLS = [
3686
3418
  {
3687
3419
  clearable: true,
3688
3420
  name: "browserCommand",
3689
- description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) also return a `recordingUrl` \u2014 an rrweb session recording for visual replay. Timeout: 120s.",
3421
+ description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) also return a `recording` object \u2014 one chunk of a continuous per-session rrweb recording that the viewer stitches into a single replay (not a standalone per-call clip). Timeout: 120s.",
3690
3422
  inputSchema: {
3691
3423
  type: "object",
3692
3424
  properties: {
@@ -3904,7 +3636,7 @@ function resolveModel(surfaceId, models, fallback) {
3904
3636
  }
3905
3637
 
3906
3638
  // src/subagents/browserAutomation/index.ts
3907
- var log7 = createLogger("browser-automation");
3639
+ var log6 = createLogger("browser-automation");
3908
3640
  async function runBrowserAutomation(task, context, opts) {
3909
3641
  const release = await acquireBrowserLock();
3910
3642
  try {
@@ -4003,7 +3735,7 @@ async function runBrowserAutomation(task, context, opts) {
4003
3735
  }
4004
3736
  }
4005
3737
  } catch {
4006
- log7.debug("Failed to parse batch analysis result", {
3738
+ log6.debug("Failed to parse batch analysis result", {
4007
3739
  batchResult
4008
3740
  });
4009
3741
  }
@@ -5537,158 +5269,454 @@ var SANITY_CHECK_TOOLS = [
5537
5269
  required: ["command"]
5538
5270
  }
5539
5271
  }
5540
- ];
5541
-
5542
- // src/subagents/codeSanityCheck/index.ts
5543
- var BASE_PROMPT3 = readAsset("subagents/codeSanityCheck", "prompt.md");
5544
- var codeSanityCheckTool = {
5545
- clearable: false,
5546
- definition: {
5547
- name: "codeSanityCheck",
5548
- description: 'Quick sanity check on an approach before building. Reviews architecture, package choices, and flags potential issues. Usually responds with "looks good." Occasionally catches something important. Readonly \u2014 can search the web and read code but cannot modify anything.',
5549
- inputSchema: {
5550
- type: "object",
5551
- properties: {
5552
- task: {
5553
- type: "string",
5554
- description: "What you're about to build and how. Include the plan, packages you intend to use, and any architectural decisions you've made."
5272
+ ];
5273
+
5274
+ // src/subagents/codeSanityCheck/index.ts
5275
+ var BASE_PROMPT3 = readAsset("subagents/codeSanityCheck", "prompt.md");
5276
+ var codeSanityCheckTool = {
5277
+ clearable: false,
5278
+ definition: {
5279
+ name: "codeSanityCheck",
5280
+ description: 'Quick sanity check on an approach before building. Reviews architecture, package choices, and flags potential issues. Usually responds with "looks good." Occasionally catches something important. Readonly \u2014 can search the web and read code but cannot modify anything.',
5281
+ inputSchema: {
5282
+ type: "object",
5283
+ properties: {
5284
+ task: {
5285
+ type: "string",
5286
+ description: "What you're about to build and how. Include the plan, packages you intend to use, and any architectural decisions you've made."
5287
+ }
5288
+ },
5289
+ required: ["task"]
5290
+ }
5291
+ },
5292
+ async execute(input, context) {
5293
+ if (!context) {
5294
+ return "Error: code sanity check requires execution context";
5295
+ }
5296
+ const specIndex = loadSpecIndex();
5297
+ const parts = [BASE_PROMPT3, loadPlatformBrief()];
5298
+ parts.push("<!-- cache_breakpoint -->");
5299
+ if (specIndex) {
5300
+ parts.push(specIndex);
5301
+ }
5302
+ const system = parts.join("\n\n");
5303
+ const result = await runSubAgent({
5304
+ system,
5305
+ task: input.task,
5306
+ tools: SANITY_CHECK_TOOLS,
5307
+ externalTools: /* @__PURE__ */ new Set(),
5308
+ executeTool: (name, toolInput) => executeTool(name, toolInput, context),
5309
+ apiConfig: context.apiConfig,
5310
+ model: resolveModel("codeSanityCheck", context.models, context.model),
5311
+ subAgentId: "codeSanityCheck",
5312
+ signal: context.signal,
5313
+ parentToolId: context.toolCallId,
5314
+ requestId: context.requestId,
5315
+ onEvent: context.onEvent,
5316
+ resolveExternalTool: context.resolveExternalTool,
5317
+ toolRegistry: context.toolRegistry
5318
+ });
5319
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
5320
+ return result.text;
5321
+ }
5322
+ };
5323
+
5324
+ // src/tools/common/scrapeWebUrl.ts
5325
+ var scrapeWebUrlTool = {
5326
+ clearable: false,
5327
+ definition: {
5328
+ name: "scrapeWebUrl",
5329
+ description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
5330
+ inputSchema: {
5331
+ type: "object",
5332
+ properties: {
5333
+ url: {
5334
+ type: "string",
5335
+ description: "The URL to fetch."
5336
+ },
5337
+ screenshot: {
5338
+ type: "boolean",
5339
+ description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
5340
+ }
5341
+ },
5342
+ required: ["url"]
5343
+ }
5344
+ },
5345
+ async execute(input, context) {
5346
+ const url = input.url;
5347
+ const screenshot = input.screenshot;
5348
+ const pageOptions = { onlyMainContent: true };
5349
+ if (screenshot) {
5350
+ pageOptions.screenshot = true;
5351
+ }
5352
+ return runMindstudioCli(
5353
+ [
5354
+ "scrape-url",
5355
+ "--url",
5356
+ url,
5357
+ "--page-options",
5358
+ JSON.stringify(pageOptions)
5359
+ ],
5360
+ { onLog: context?.onLog }
5361
+ );
5362
+ }
5363
+ };
5364
+
5365
+ // src/tools/index.ts
5366
+ function deriveContext(parent, toolCallId) {
5367
+ return { ...parent, toolCallId };
5368
+ }
5369
+ var ALL_TOOLS = [
5370
+ // Common
5371
+ setProjectOnboardingStateTool,
5372
+ promptUserTool,
5373
+ confirmDestructiveActionTool,
5374
+ askMindStudioSdkTool,
5375
+ scrapeWebUrlTool,
5376
+ searchGoogleTool,
5377
+ setProjectMetadataTool,
5378
+ designExpertTool,
5379
+ productVisionTool,
5380
+ codeSanityCheckTool,
5381
+ compactConversationTool,
5382
+ // Post-onboarding
5383
+ presentPublishPlanTool,
5384
+ writePlanTool,
5385
+ updatePlanStatusTool,
5386
+ // Spec
5387
+ readSpecTool,
5388
+ writeSpecTool,
5389
+ editSpecTool,
5390
+ listSpecFilesTool,
5391
+ // Code
5392
+ readFileTool,
5393
+ writeFileTool,
5394
+ editFileTool,
5395
+ bashTool,
5396
+ grepTool,
5397
+ globTool,
5398
+ listDirTool,
5399
+ editsFinishedTool,
5400
+ runScenarioTool,
5401
+ runMethodTool,
5402
+ queryDatabaseTool,
5403
+ screenshotTool,
5404
+ browserAutomationTool,
5405
+ // LSP
5406
+ lspDiagnosticsTool,
5407
+ restartProcessTool
5408
+ ];
5409
+ var CLEARABLE_TOOLS = new Set(
5410
+ ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
5411
+ );
5412
+ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
5413
+ "visualDesignExpert",
5414
+ "productVision",
5415
+ "codeSanityCheck",
5416
+ "runAutomatedBrowserTest",
5417
+ "askMindStudioSdk"
5418
+ ]);
5419
+ function getToolDefinitions(_onboardingState) {
5420
+ return ALL_TOOLS.map((t) => t.definition);
5421
+ }
5422
+ function getToolByName(name) {
5423
+ return ALL_TOOLS.find((t) => t.definition.name === name);
5424
+ }
5425
+ function executeTool(name, input, context) {
5426
+ const tool = getToolByName(name);
5427
+ if (!tool) {
5428
+ return Promise.resolve(`Error: Unknown tool "${name}"`);
5429
+ }
5430
+ return tool.execute(input, context);
5431
+ }
5432
+
5433
+ // src/compaction/index.ts
5434
+ var log7 = createLogger("compaction");
5435
+ var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
5436
+ var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
5437
+ var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
5438
+ async function compactConversation(messages, apiConfig, system, tools2, model) {
5439
+ const endIndex = findSafeInsertionPoint(messages);
5440
+ const summaries = [];
5441
+ const tasks = [];
5442
+ const conversationMessages = getConversationMessagesForSummary(
5443
+ messages,
5444
+ endIndex
5445
+ );
5446
+ if (conversationMessages.length > 0) {
5447
+ tasks.push(
5448
+ generateSummary(
5449
+ apiConfig,
5450
+ "conversation",
5451
+ CONVERSATION_SUMMARY_PROMPT,
5452
+ conversationMessages,
5453
+ system,
5454
+ tools2,
5455
+ model
5456
+ ).then((text) => {
5457
+ if (text) {
5458
+ summaries.push({ name: "conversation", text });
5459
+ }
5460
+ })
5461
+ );
5462
+ }
5463
+ for (const name of SUMMARIZABLE_SUBAGENTS) {
5464
+ const subagentMessages = getSubAgentMessagesForSummary(
5465
+ messages,
5466
+ name,
5467
+ endIndex
5468
+ );
5469
+ if (subagentMessages.length > 0) {
5470
+ tasks.push(
5471
+ generateSummary(
5472
+ apiConfig,
5473
+ name,
5474
+ SUBAGENT_SUMMARY_PROMPT,
5475
+ subagentMessages,
5476
+ system,
5477
+ tools2,
5478
+ model
5479
+ ).then((text) => {
5480
+ if (text) {
5481
+ summaries.push({ name, text });
5482
+ }
5483
+ })
5484
+ );
5485
+ }
5486
+ }
5487
+ await Promise.all(tasks);
5488
+ const checkpointMessages = summaries.map((s) => ({
5489
+ role: "user",
5490
+ hidden: true,
5491
+ content: [
5492
+ {
5493
+ type: "summary",
5494
+ name: s.name,
5495
+ text: s.text,
5496
+ startedAt: Date.now()
5497
+ }
5498
+ ]
5499
+ }));
5500
+ log7.info("Compaction complete", { summaries: summaries.length });
5501
+ return checkpointMessages;
5502
+ }
5503
+ function findSafeInsertionPoint(messages) {
5504
+ let idx = messages.length;
5505
+ while (idx > 0) {
5506
+ const msg = messages[idx - 1];
5507
+ if (msg.role === "user" && msg.toolCallId) {
5508
+ idx--;
5509
+ } else {
5510
+ break;
5511
+ }
5512
+ }
5513
+ if (idx < messages.length && idx > 0) {
5514
+ const msg = messages[idx - 1];
5515
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
5516
+ const hasToolUse = msg.content.some(
5517
+ (b) => b.type === "tool"
5518
+ );
5519
+ if (hasToolUse) {
5520
+ idx--;
5521
+ }
5522
+ }
5523
+ }
5524
+ return idx;
5525
+ }
5526
+ function getConversationMessagesForSummary(messages, endIndex) {
5527
+ let startIdx = 0;
5528
+ for (let i = endIndex - 1; i >= 0; i--) {
5529
+ const msg = messages[i];
5530
+ if (!Array.isArray(msg.content)) {
5531
+ continue;
5532
+ }
5533
+ for (const block of msg.content) {
5534
+ if (block.type === "summary" && block.name === "conversation") {
5535
+ startIdx = i + 1;
5536
+ break;
5537
+ }
5538
+ }
5539
+ if (startIdx > 0) {
5540
+ break;
5541
+ }
5542
+ }
5543
+ return messages.slice(startIdx, endIndex);
5544
+ }
5545
+ function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
5546
+ let checkpointIdx = -1;
5547
+ for (let i = endIndex - 1; i >= 0; i--) {
5548
+ const msg = messages[i];
5549
+ if (!Array.isArray(msg.content)) {
5550
+ continue;
5551
+ }
5552
+ for (const block of msg.content) {
5553
+ if (block.type === "summary" && block.name === subAgentName) {
5554
+ checkpointIdx = i;
5555
+ break;
5556
+ }
5557
+ }
5558
+ if (checkpointIdx !== -1) {
5559
+ break;
5560
+ }
5561
+ }
5562
+ const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
5563
+ const collected = [];
5564
+ for (let i = startIdx; i < endIndex; i++) {
5565
+ const msg = messages[i];
5566
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
5567
+ continue;
5568
+ }
5569
+ for (const block of msg.content) {
5570
+ if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
5571
+ collected.push(...block.subAgentMessages);
5572
+ }
5573
+ }
5574
+ }
5575
+ return collected;
5576
+ }
5577
+ function serializeForSummary(messages) {
5578
+ const toolNameById = /* @__PURE__ */ new Map();
5579
+ for (const msg of messages) {
5580
+ if (!Array.isArray(msg.content)) {
5581
+ continue;
5582
+ }
5583
+ for (const block of msg.content) {
5584
+ if (block.type === "tool") {
5585
+ toolNameById.set(block.id, block.name);
5586
+ }
5587
+ }
5588
+ }
5589
+ const lines = [];
5590
+ for (const msg of messages) {
5591
+ if (msg.role === "user" && msg.toolCallId) {
5592
+ const toolName = toolNameById.get(msg.toolCallId);
5593
+ if (toolName && SUBAGENT_TOOL_NAMES.has(toolName)) {
5594
+ const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.filter(
5595
+ (b) => b.type === "text"
5596
+ ).map((b) => b.text).join("\n") : "";
5597
+ if (content.trim()) {
5598
+ lines.push(`[${toolName} result]: ${content}`);
5555
5599
  }
5556
- },
5557
- required: ["task"]
5600
+ }
5601
+ continue;
5558
5602
  }
5559
- },
5560
- async execute(input, context) {
5561
- if (!context) {
5562
- return "Error: code sanity check requires execution context";
5603
+ if (typeof msg.content === "string") {
5604
+ if (msg.content.trim()) {
5605
+ lines.push(`[${msg.role}]: ${msg.content}`);
5606
+ }
5607
+ continue;
5563
5608
  }
5564
- const specIndex = loadSpecIndex();
5565
- const parts = [BASE_PROMPT3, loadPlatformBrief()];
5566
- parts.push("<!-- cache_breakpoint -->");
5567
- if (specIndex) {
5568
- parts.push(specIndex);
5609
+ if (!Array.isArray(msg.content)) {
5610
+ continue;
5569
5611
  }
5570
- const system = parts.join("\n\n");
5571
- const result = await runSubAgent({
5572
- system,
5573
- task: input.task,
5574
- tools: SANITY_CHECK_TOOLS,
5575
- externalTools: /* @__PURE__ */ new Set(),
5576
- executeTool: (name, toolInput) => executeTool(name, toolInput, context),
5577
- apiConfig: context.apiConfig,
5578
- model: resolveModel("codeSanityCheck", context.models, context.model),
5579
- subAgentId: "codeSanityCheck",
5580
- signal: context.signal,
5581
- parentToolId: context.toolCallId,
5582
- requestId: context.requestId,
5583
- onEvent: context.onEvent,
5584
- resolveExternalTool: context.resolveExternalTool,
5585
- toolRegistry: context.toolRegistry
5612
+ const blocks = msg.content;
5613
+ const parts = [];
5614
+ let toolCount = 0;
5615
+ for (const block of blocks) {
5616
+ if (block.type === "text") {
5617
+ parts.push(block.text);
5618
+ } else if (block.type === "tool") {
5619
+ toolCount++;
5620
+ }
5621
+ }
5622
+ if (toolCount > 0) {
5623
+ parts.push(`[used ${toolCount} tool${toolCount === 1 ? "" : "s"}]`);
5624
+ }
5625
+ const body = parts.join("\n").trim();
5626
+ if (body) {
5627
+ lines.push(`[${msg.role}]: ${body}`);
5628
+ }
5629
+ }
5630
+ return lines.join("\n\n");
5631
+ }
5632
+ var CHUNK_CHAR_LIMIT = 24e5;
5633
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
5634
+ const serialized = serializeForSummary(messagesToSummarize);
5635
+ if (!serialized.trim()) {
5636
+ return null;
5637
+ }
5638
+ if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
5639
+ const mid = Math.floor(messagesToSummarize.length / 2);
5640
+ log7.info("Chunking summary", {
5641
+ name,
5642
+ messageCount: messagesToSummarize.length,
5643
+ serializedLength: serialized.length
5586
5644
  });
5587
- context.subAgentMessages?.set(context.toolCallId, result.messages);
5588
- return result.text;
5645
+ const [first, second] = await Promise.all([
5646
+ generateSummary(
5647
+ apiConfig,
5648
+ `${name} [pt1]`,
5649
+ compactionPrompt,
5650
+ messagesToSummarize.slice(0, mid),
5651
+ mainSystem,
5652
+ mainTools,
5653
+ model
5654
+ ),
5655
+ generateSummary(
5656
+ apiConfig,
5657
+ `${name} [pt2]`,
5658
+ compactionPrompt,
5659
+ messagesToSummarize.slice(mid),
5660
+ mainSystem,
5661
+ mainTools,
5662
+ model
5663
+ )
5664
+ ]);
5665
+ const parts = [first, second].filter((p) => !!p);
5666
+ return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
5589
5667
  }
5590
- };
5668
+ log7.info("Generating summary", {
5669
+ name,
5670
+ messageCount: messagesToSummarize.length,
5671
+ cacheReuse: !!mainSystem
5672
+ });
5673
+ let summaryText = "";
5674
+ const useMainCache = !!mainSystem;
5675
+ const system = useMainCache ? mainSystem : compactionPrompt;
5676
+ const tools2 = [];
5677
+ const userContent = useMainCache ? `${compactionPrompt}
5591
5678
 
5592
- // src/tools/common/scrapeWebUrl.ts
5593
- var scrapeWebUrlTool = {
5594
- clearable: false,
5595
- definition: {
5596
- name: "scrapeWebUrl",
5597
- description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
5598
- inputSchema: {
5599
- type: "object",
5600
- properties: {
5601
- url: {
5602
- type: "string",
5603
- description: "The URL to fetch."
5604
- },
5605
- screenshot: {
5606
- type: "boolean",
5607
- description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
5608
- }
5609
- },
5610
- required: ["url"]
5611
- }
5612
- },
5613
- async execute(input, context) {
5614
- const url = input.url;
5615
- const screenshot = input.screenshot;
5616
- const pageOptions = { onlyMainContent: true };
5617
- if (screenshot) {
5618
- pageOptions.screenshot = true;
5679
+ ---
5680
+
5681
+ Conversation to summarize:
5682
+
5683
+ ${serialized}` : serialized;
5684
+ const iterStart = Date.now();
5685
+ for await (const event of streamChat({
5686
+ ...apiConfig,
5687
+ model,
5688
+ subAgentId: "conversationSummarizer",
5689
+ system,
5690
+ messages: [{ role: "user", content: userContent }],
5691
+ tools: tools2
5692
+ })) {
5693
+ if (event.type === "text") {
5694
+ summaryText += event.text;
5695
+ } else if (event.type === "done") {
5696
+ recordUsage({
5697
+ ts: Date.now(),
5698
+ agentName: "conversationSummarizer",
5699
+ modelId: event.modelId,
5700
+ inputTokens: event.usage.inputTokens,
5701
+ outputTokens: event.usage.outputTokens,
5702
+ cacheCreationTokens: event.usage.cacheCreationTokens,
5703
+ cacheReadTokens: event.usage.cacheReadTokens,
5704
+ cost: nanoToDollars(event.cost),
5705
+ billingEvents: event.billingEvents,
5706
+ durationMs: Date.now() - iterStart,
5707
+ toolNames: []
5708
+ });
5709
+ } else if (event.type === "error") {
5710
+ log7.error("Summary generation failed", { name, error: event.error });
5711
+ return null;
5619
5712
  }
5620
- return runMindstudioCli(
5621
- [
5622
- "scrape-url",
5623
- "--url",
5624
- url,
5625
- "--page-options",
5626
- JSON.stringify(pageOptions)
5627
- ],
5628
- { onLog: context?.onLog }
5629
- );
5630
5713
  }
5631
- };
5632
-
5633
- // src/tools/index.ts
5634
- function deriveContext(parent, toolCallId) {
5635
- return { ...parent, toolCallId };
5636
- }
5637
- var ALL_TOOLS = [
5638
- // Common
5639
- setProjectOnboardingStateTool,
5640
- promptUserTool,
5641
- confirmDestructiveActionTool,
5642
- askMindStudioSdkTool,
5643
- scrapeWebUrlTool,
5644
- searchGoogleTool,
5645
- setProjectMetadataTool,
5646
- designExpertTool,
5647
- productVisionTool,
5648
- codeSanityCheckTool,
5649
- compactConversationTool,
5650
- // Post-onboarding
5651
- presentPublishPlanTool,
5652
- writePlanTool,
5653
- updatePlanStatusTool,
5654
- // Spec
5655
- readSpecTool,
5656
- writeSpecTool,
5657
- editSpecTool,
5658
- listSpecFilesTool,
5659
- // Code
5660
- readFileTool,
5661
- writeFileTool,
5662
- editFileTool,
5663
- bashTool,
5664
- grepTool,
5665
- globTool,
5666
- listDirTool,
5667
- editsFinishedTool,
5668
- runScenarioTool,
5669
- runMethodTool,
5670
- queryDatabaseTool,
5671
- screenshotTool,
5672
- browserAutomationTool,
5673
- // LSP
5674
- lspDiagnosticsTool,
5675
- restartProcessTool
5676
- ];
5677
- var CLEARABLE_TOOLS = new Set(
5678
- ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
5679
- );
5680
- function getToolDefinitions(_onboardingState) {
5681
- return ALL_TOOLS.map((t) => t.definition);
5682
- }
5683
- function getToolByName(name) {
5684
- return ALL_TOOLS.find((t) => t.definition.name === name);
5685
- }
5686
- function executeTool(name, input, context) {
5687
- const tool = getToolByName(name);
5688
- if (!tool) {
5689
- return Promise.resolve(`Error: Unknown tool "${name}"`);
5714
+ if (!summaryText.trim()) {
5715
+ log7.warn("Empty summary generated", { name });
5716
+ return null;
5690
5717
  }
5691
- return tool.execute(input, context);
5718
+ log7.info("Summary generated", { name, summaryLength: summaryText.length });
5719
+ return summaryText.trim();
5692
5720
  }
5693
5721
 
5694
5722
  // src/compaction/trigger.ts
@@ -7827,13 +7855,11 @@ var HeadlessSession = class {
7827
7855
  clearSession(this.state);
7828
7856
  return {};
7829
7857
  }
7830
- /** Archive the current session and seed a fresh one with the given
7831
- * per-agent model overrides. Models are immutable for the life of a
7832
- * session this is the only way to change them. Omitting `models`
7833
- * (or sending an empty object) resets to "use server defaults for
7834
- * every agent". */
7835
- handleNewSession(models) {
7836
- clearSession(this.state);
7858
+ /** Change per-agent model picks without clearing history. Takes effect on
7859
+ * the next turn the model is resolved live, per LLM call, from
7860
+ * `state.models`. Omitting `models` (or sending an empty object) resets
7861
+ * every agent to "use server defaults". */
7862
+ handleChangeModels(models) {
7837
7863
  this.state.models = models && Object.keys(models).length > 0 ? models : void 0;
7838
7864
  saveSession(this.state);
7839
7865
  return {
@@ -7930,12 +7956,23 @@ var HeadlessSession = class {
7930
7956
  );
7931
7957
  return;
7932
7958
  }
7933
- if (action === "newSession") {
7959
+ if (action === "changeModels") {
7960
+ if (this.running) {
7961
+ this.emit(
7962
+ "completed",
7963
+ {
7964
+ success: false,
7965
+ error: "cannot change models while a turn is running"
7966
+ },
7967
+ requestId
7968
+ );
7969
+ return;
7970
+ }
7934
7971
  const models = parsed.models;
7935
7972
  this.dispatchSimple(
7936
7973
  requestId,
7937
- "session_cleared",
7938
- () => this.handleNewSession(models)
7974
+ "models_changed",
7975
+ () => this.handleChangeModels(models)
7939
7976
  );
7940
7977
  return;
7941
7978
  }