@mindstudio-ai/remy 0.1.212 → 0.1.214
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/automatedActions/publish.md +1 -1
- package/dist/headless.js +153 -232
- package/dist/index.js +157 -236
- package/dist/prompt/static/authoring.md +1 -1
- package/dist/prompt/static/coding.md +3 -1
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ The user wants to deploy their app. Pushing to the `main` branch triggers a prod
|
|
|
9
9
|
Review the current state of the working tree — what has changed since the last commit, what's been committed since the last push, and the overall shape of recent work. Write a user-friendly changelog with `presentPublishPlan` — summarize what changed in plain language ("added vendor approval workflow", "fixed invoice totals", "updated the dashboard layout"). Reference specific code or file paths only when it helps clarity. This is what the user will see before deploying.
|
|
10
10
|
|
|
11
11
|
If approved:
|
|
12
|
-
- Stage and commit any uncommitted changes with a clean, descriptive commit message
|
|
12
|
+
- Stage and commit any uncommitted changes with a clean, descriptive commit message. If the committed work resolves any open issues (`mindstudio-prod issues`), reference them in the commit message with a closing keyword — `fixes #42`, `closes #7` — so the deploy closes them automatically once it goes live.
|
|
13
13
|
- Push to main
|
|
14
14
|
- Use `mindstudio-prod releases status --wait` to poll the build until it completes. Let the user know it's deploying, then report back when it's live.
|
|
15
15
|
- Once deployed, offer to help with next steps. This includes technical steps likesetting up a custom domain (`mindstudio-prod domains`), checking for errors (`mindstudio-prod requests stats`), seeding production data (`mindstudio-prod db`), managing env vars/secrets, or anything else they need for launch. It also includes going above and beyond and helping holistically. If it's the initial deploy, offer to help create collateral to announce the launch (e.g., an image for sharing on social media, text copy for a post, etc); if it's a meaningful incremental update, an annoucement post or something similar - go above and beyond here to help the user see that you care about the product from end-to-end, not just writing code! They will be appreciative, grateful, and pleased with your creativity here. Refer to the design guidance in the spec for how to talk about the product, and consider consulting the design expert to generate images or other marketing collateral.
|
package/dist/headless.js
CHANGED
|
@@ -724,88 +724,23 @@ ${loadPlanStatus()}
|
|
|
724
724
|
import fs5 from "fs/promises";
|
|
725
725
|
|
|
726
726
|
// src/tools/spec/_helpers.ts
|
|
727
|
-
var HEADING_RE = /^(#{1,6})\s+(.+)$/;
|
|
728
|
-
function parseHeadings(content) {
|
|
729
|
-
const lines = content.split("\n");
|
|
730
|
-
const headings = [];
|
|
731
|
-
for (let i = 0; i < lines.length; i++) {
|
|
732
|
-
const match = lines[i].match(HEADING_RE);
|
|
733
|
-
if (match) {
|
|
734
|
-
headings.push({
|
|
735
|
-
level: match[1].length,
|
|
736
|
-
text: match[2].trim(),
|
|
737
|
-
startLine: i,
|
|
738
|
-
contentStart: i + 1,
|
|
739
|
-
contentEnd: lines.length
|
|
740
|
-
// placeholder — resolved below
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
for (let i = 0; i < headings.length; i++) {
|
|
745
|
-
const current = headings[i];
|
|
746
|
-
let end = lines.length;
|
|
747
|
-
for (let j = i + 1; j < headings.length; j++) {
|
|
748
|
-
if (headings[j].level <= current.level) {
|
|
749
|
-
end = headings[j].startLine;
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
current.contentEnd = end;
|
|
754
|
-
}
|
|
755
|
-
return headings;
|
|
756
|
-
}
|
|
757
|
-
function resolveHeadingPath(content, headingPath) {
|
|
758
|
-
const lines = content.split("\n");
|
|
759
|
-
const headings = parseHeadings(content);
|
|
760
|
-
if (headingPath === "") {
|
|
761
|
-
const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
|
|
762
|
-
return {
|
|
763
|
-
startLine: 0,
|
|
764
|
-
contentStart: 0,
|
|
765
|
-
contentEnd: firstHeadingLine
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
const segments = headingPath.split(">").map((s) => s.trim());
|
|
769
|
-
let searchStart = 0;
|
|
770
|
-
let searchEnd = lines.length;
|
|
771
|
-
let resolved = null;
|
|
772
|
-
for (let si = 0; si < segments.length; si++) {
|
|
773
|
-
const segment = segments[si].toLowerCase();
|
|
774
|
-
const candidates = headings.filter(
|
|
775
|
-
(h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
|
|
776
|
-
);
|
|
777
|
-
if (candidates.length === 0) {
|
|
778
|
-
const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
|
|
779
|
-
const searchedPath = segments.slice(0, si + 1).join(" > ");
|
|
780
|
-
throw new Error(
|
|
781
|
-
`Heading not found: "${searchedPath}"
|
|
782
|
-
|
|
783
|
-
Available headings:
|
|
784
|
-
${available || "(none)"}`
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
resolved = candidates[0];
|
|
788
|
-
searchStart = resolved.contentStart;
|
|
789
|
-
searchEnd = resolved.contentEnd;
|
|
790
|
-
}
|
|
791
|
-
return {
|
|
792
|
-
startLine: resolved.startLine,
|
|
793
|
-
contentStart: resolved.contentStart,
|
|
794
|
-
contentEnd: resolved.contentEnd
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
727
|
function validateSpecPath(filePath) {
|
|
798
728
|
if (!filePath.startsWith("src/")) {
|
|
799
729
|
throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
|
|
800
730
|
}
|
|
801
731
|
return filePath;
|
|
802
732
|
}
|
|
803
|
-
function
|
|
804
|
-
const
|
|
805
|
-
if (
|
|
806
|
-
return
|
|
733
|
+
function extractFrontmatter(content) {
|
|
734
|
+
const lines = content.split("\n");
|
|
735
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
736
|
+
return null;
|
|
807
737
|
}
|
|
808
|
-
|
|
738
|
+
for (let i = 1; i < lines.length; i++) {
|
|
739
|
+
if (lines[i].trim() === "---") {
|
|
740
|
+
return lines.slice(0, i + 1).join("\n");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
809
744
|
}
|
|
810
745
|
|
|
811
746
|
// src/tools/spec/readSpec.ts
|
|
@@ -981,11 +916,92 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
|
|
|
981
916
|
|
|
982
917
|
// src/tools/spec/editSpec.ts
|
|
983
918
|
import fs7 from "fs/promises";
|
|
919
|
+
|
|
920
|
+
// src/tools/code/editFile/_helpers.ts
|
|
921
|
+
function buildLineOffsets(content) {
|
|
922
|
+
const offsets = [0];
|
|
923
|
+
for (let i = 0; i < content.length; i++) {
|
|
924
|
+
if (content[i] === "\n") {
|
|
925
|
+
offsets.push(i + 1);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return offsets;
|
|
929
|
+
}
|
|
930
|
+
function lineAtOffset(offsets, charIndex) {
|
|
931
|
+
let lo = 0;
|
|
932
|
+
let hi = offsets.length - 1;
|
|
933
|
+
while (lo < hi) {
|
|
934
|
+
const mid = lo + hi + 1 >> 1;
|
|
935
|
+
if (offsets[mid] <= charIndex) {
|
|
936
|
+
lo = mid;
|
|
937
|
+
} else {
|
|
938
|
+
hi = mid - 1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return lo + 1;
|
|
942
|
+
}
|
|
943
|
+
function findOccurrences(content, searchString) {
|
|
944
|
+
if (!searchString) {
|
|
945
|
+
return [];
|
|
946
|
+
}
|
|
947
|
+
const offsets = buildLineOffsets(content);
|
|
948
|
+
const results = [];
|
|
949
|
+
let pos = 0;
|
|
950
|
+
while (pos <= content.length - searchString.length) {
|
|
951
|
+
const idx = content.indexOf(searchString, pos);
|
|
952
|
+
if (idx === -1) {
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
results.push({ index: idx, line: lineAtOffset(offsets, idx) });
|
|
956
|
+
pos = idx + 1;
|
|
957
|
+
}
|
|
958
|
+
return results;
|
|
959
|
+
}
|
|
960
|
+
function flexibleMatch(content, searchString) {
|
|
961
|
+
const contentLines = content.split("\n");
|
|
962
|
+
const searchLines = searchString.split("\n").map((l) => l.trimStart());
|
|
963
|
+
if (searchLines.length === 0) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
const matches = [];
|
|
967
|
+
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
968
|
+
let allMatch = true;
|
|
969
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
970
|
+
if (contentLines[i + j].trimStart() !== searchLines[j]) {
|
|
971
|
+
allMatch = false;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (allMatch) {
|
|
976
|
+
matches.push(i);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (matches.length !== 1) {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
const startIdx = matches[0];
|
|
983
|
+
const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
|
|
984
|
+
const offsets = buildLineOffsets(content);
|
|
985
|
+
return {
|
|
986
|
+
matchedText,
|
|
987
|
+
index: offsets[startIdx],
|
|
988
|
+
line: startIdx + 1
|
|
989
|
+
// 1-based
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function replaceAt(content, index, oldLength, newString) {
|
|
993
|
+
return content.slice(0, index) + newString + content.slice(index + oldLength);
|
|
994
|
+
}
|
|
995
|
+
function formatOccurrenceError(count, lines, filePath) {
|
|
996
|
+
return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/tools/spec/editSpec.ts
|
|
984
1000
|
var editSpecTool = {
|
|
985
1001
|
clearable: true,
|
|
986
1002
|
definition: {
|
|
987
1003
|
name: "editSpec",
|
|
988
|
-
description:
|
|
1004
|
+
description: "Make a targeted find/replace edit to a spec file (src/*.md). old_string must appear exactly once (minor indentation differences are handled automatically); set replace_all to true to replace every occurrence. Read the file with readSpec first so you match the exact text, and include the full enclosing structure rather than an inner fragment. The file's YAML frontmatter (the leading --- \u2026 --- block, which holds required fields like name) is protected \u2014 an edit that would remove or malform it is refused. For a full rewrite, use writeSpec.",
|
|
989
1005
|
inputSchema: {
|
|
990
1006
|
type: "object",
|
|
991
1007
|
properties: {
|
|
@@ -993,31 +1009,20 @@ var editSpecTool = {
|
|
|
993
1009
|
type: "string",
|
|
994
1010
|
description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
|
|
995
1011
|
},
|
|
996
|
-
|
|
997
|
-
type: "
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
enum: ["replace", "insert_after", "insert_before", "delete"],
|
|
1008
|
-
description: "replace: swap content under this heading (keeps the heading line). insert_after: add content after this section. insert_before: add content before this heading. delete: remove this heading and all its content."
|
|
1009
|
-
},
|
|
1010
|
-
content: {
|
|
1011
|
-
type: "string",
|
|
1012
|
-
description: "MSFM markdown content for replace/insert operations. Not needed for delete."
|
|
1013
|
-
}
|
|
1014
|
-
},
|
|
1015
|
-
required: ["heading", "operation"]
|
|
1016
|
-
},
|
|
1017
|
-
description: "Array of edits to apply in order."
|
|
1012
|
+
old_string: {
|
|
1013
|
+
type: "string",
|
|
1014
|
+
description: "The exact string to find and replace. Must be unique in the file unless replace_all is true."
|
|
1015
|
+
},
|
|
1016
|
+
new_string: {
|
|
1017
|
+
type: "string",
|
|
1018
|
+
description: "The replacement string."
|
|
1019
|
+
},
|
|
1020
|
+
replace_all: {
|
|
1021
|
+
type: "boolean",
|
|
1022
|
+
description: "If true, replace every occurrence of old_string in the file. Defaults to false."
|
|
1018
1023
|
}
|
|
1019
1024
|
},
|
|
1020
|
-
required: ["path", "
|
|
1025
|
+
required: ["path", "old_string", "new_string"]
|
|
1021
1026
|
}
|
|
1022
1027
|
},
|
|
1023
1028
|
async execute(input) {
|
|
@@ -1028,69 +1033,66 @@ var editSpecTool = {
|
|
|
1028
1033
|
}
|
|
1029
1034
|
const release = await acquireFileLock(input.path);
|
|
1030
1035
|
try {
|
|
1031
|
-
let
|
|
1036
|
+
let content;
|
|
1032
1037
|
try {
|
|
1033
|
-
|
|
1038
|
+
content = await fs7.readFile(input.path, "utf-8");
|
|
1034
1039
|
} catch (err) {
|
|
1035
1040
|
return `Error reading file: ${err.message}`;
|
|
1036
1041
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
return `Error: ${err.message}
|
|
1045
|
-
|
|
1046
|
-
Document structure:
|
|
1047
|
-
${tree}`;
|
|
1042
|
+
const { old_string, new_string, replace_all } = input;
|
|
1043
|
+
const occurrences = findOccurrences(content, old_string);
|
|
1044
|
+
let updated;
|
|
1045
|
+
let notePrefix;
|
|
1046
|
+
if (replace_all) {
|
|
1047
|
+
if (occurrences.length === 0) {
|
|
1048
|
+
return `Error: old_string not found in ${input.path}.`;
|
|
1048
1049
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
lines.splice(range.startLine, 0, ...contentLines);
|
|
1077
|
-
break;
|
|
1078
|
-
}
|
|
1079
|
-
case "delete": {
|
|
1080
|
-
lines.splice(range.startLine, range.contentEnd - range.startLine);
|
|
1081
|
-
break;
|
|
1082
|
-
}
|
|
1083
|
-
default:
|
|
1084
|
-
return `Error: Unknown operation "${edit.operation}". Use replace, insert_after, insert_before, or delete.`;
|
|
1050
|
+
updated = content;
|
|
1051
|
+
for (let i = occurrences.length - 1; i >= 0; i--) {
|
|
1052
|
+
updated = replaceAt(
|
|
1053
|
+
updated,
|
|
1054
|
+
occurrences[i].index,
|
|
1055
|
+
old_string.length,
|
|
1056
|
+
new_string
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
notePrefix = `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
|
|
1060
|
+
`;
|
|
1061
|
+
} else if (occurrences.length === 1) {
|
|
1062
|
+
updated = replaceAt(
|
|
1063
|
+
content,
|
|
1064
|
+
occurrences[0].index,
|
|
1065
|
+
old_string.length,
|
|
1066
|
+
new_string
|
|
1067
|
+
);
|
|
1068
|
+
notePrefix = `Updated ${input.path}
|
|
1069
|
+
`;
|
|
1070
|
+
} else if (occurrences.length > 1) {
|
|
1071
|
+
const lines = occurrences.map((o) => o.line);
|
|
1072
|
+
return `Error: ${formatOccurrenceError(occurrences.length, lines, input.path)}`;
|
|
1073
|
+
} else {
|
|
1074
|
+
const flex = flexibleMatch(content, old_string);
|
|
1075
|
+
if (!flex) {
|
|
1076
|
+
return `Error: old_string not found in ${input.path}. Make sure you've read the file first and copied the exact text.`;
|
|
1085
1077
|
}
|
|
1086
|
-
|
|
1078
|
+
updated = replaceAt(
|
|
1079
|
+
content,
|
|
1080
|
+
flex.index,
|
|
1081
|
+
flex.matchedText.length,
|
|
1082
|
+
new_string
|
|
1083
|
+
);
|
|
1084
|
+
notePrefix = `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
|
|
1085
|
+
`;
|
|
1086
|
+
}
|
|
1087
|
+
if (extractFrontmatter(content) !== null && extractFrontmatter(updated) === null) {
|
|
1088
|
+
return `Error: that edit would remove or malform the spec's YAML frontmatter (the leading \`--- \u2026 ---\` block, which holds required fields like \`name\`). Narrow old_string to the body content you meant to change and leave the frontmatter block intact.`;
|
|
1087
1089
|
}
|
|
1088
1090
|
try {
|
|
1089
|
-
await fs7.writeFile(input.path,
|
|
1091
|
+
await fs7.writeFile(input.path, updated, "utf-8");
|
|
1090
1092
|
} catch (err) {
|
|
1091
1093
|
return `Error writing file: ${err.message}`;
|
|
1092
1094
|
}
|
|
1093
|
-
return unifiedDiff(input.path,
|
|
1095
|
+
return `${notePrefix}${unifiedDiff(input.path, content, updated)}`;
|
|
1094
1096
|
} finally {
|
|
1095
1097
|
release();
|
|
1096
1098
|
}
|
|
@@ -1868,87 +1870,6 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
|
|
|
1868
1870
|
|
|
1869
1871
|
// src/tools/code/editFile/index.ts
|
|
1870
1872
|
import fs14 from "fs/promises";
|
|
1871
|
-
|
|
1872
|
-
// src/tools/code/editFile/_helpers.ts
|
|
1873
|
-
function buildLineOffsets(content) {
|
|
1874
|
-
const offsets = [0];
|
|
1875
|
-
for (let i = 0; i < content.length; i++) {
|
|
1876
|
-
if (content[i] === "\n") {
|
|
1877
|
-
offsets.push(i + 1);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
return offsets;
|
|
1881
|
-
}
|
|
1882
|
-
function lineAtOffset(offsets, charIndex) {
|
|
1883
|
-
let lo = 0;
|
|
1884
|
-
let hi = offsets.length - 1;
|
|
1885
|
-
while (lo < hi) {
|
|
1886
|
-
const mid = lo + hi + 1 >> 1;
|
|
1887
|
-
if (offsets[mid] <= charIndex) {
|
|
1888
|
-
lo = mid;
|
|
1889
|
-
} else {
|
|
1890
|
-
hi = mid - 1;
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
return lo + 1;
|
|
1894
|
-
}
|
|
1895
|
-
function findOccurrences(content, searchString) {
|
|
1896
|
-
if (!searchString) {
|
|
1897
|
-
return [];
|
|
1898
|
-
}
|
|
1899
|
-
const offsets = buildLineOffsets(content);
|
|
1900
|
-
const results = [];
|
|
1901
|
-
let pos = 0;
|
|
1902
|
-
while (pos <= content.length - searchString.length) {
|
|
1903
|
-
const idx = content.indexOf(searchString, pos);
|
|
1904
|
-
if (idx === -1) {
|
|
1905
|
-
break;
|
|
1906
|
-
}
|
|
1907
|
-
results.push({ index: idx, line: lineAtOffset(offsets, idx) });
|
|
1908
|
-
pos = idx + 1;
|
|
1909
|
-
}
|
|
1910
|
-
return results;
|
|
1911
|
-
}
|
|
1912
|
-
function flexibleMatch(content, searchString) {
|
|
1913
|
-
const contentLines = content.split("\n");
|
|
1914
|
-
const searchLines = searchString.split("\n").map((l) => l.trimStart());
|
|
1915
|
-
if (searchLines.length === 0) {
|
|
1916
|
-
return null;
|
|
1917
|
-
}
|
|
1918
|
-
const matches = [];
|
|
1919
|
-
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
1920
|
-
let allMatch = true;
|
|
1921
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
1922
|
-
if (contentLines[i + j].trimStart() !== searchLines[j]) {
|
|
1923
|
-
allMatch = false;
|
|
1924
|
-
break;
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
if (allMatch) {
|
|
1928
|
-
matches.push(i);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
if (matches.length !== 1) {
|
|
1932
|
-
return null;
|
|
1933
|
-
}
|
|
1934
|
-
const startIdx = matches[0];
|
|
1935
|
-
const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
|
|
1936
|
-
const offsets = buildLineOffsets(content);
|
|
1937
|
-
return {
|
|
1938
|
-
matchedText,
|
|
1939
|
-
index: offsets[startIdx],
|
|
1940
|
-
line: startIdx + 1
|
|
1941
|
-
// 1-based
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
function replaceAt(content, index, oldLength, newString) {
|
|
1945
|
-
return content.slice(0, index) + newString + content.slice(index + oldLength);
|
|
1946
|
-
}
|
|
1947
|
-
function formatOccurrenceError(count, lines, filePath) {
|
|
1948
|
-
return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
// src/tools/code/editFile/index.ts
|
|
1952
1873
|
var editFileTool = {
|
|
1953
1874
|
clearable: true,
|
|
1954
1875
|
definition: {
|
package/dist/index.js
CHANGED
|
@@ -369,93 +369,27 @@ var init_api = __esm({
|
|
|
369
369
|
});
|
|
370
370
|
|
|
371
371
|
// src/tools/spec/_helpers.ts
|
|
372
|
-
function parseHeadings(content) {
|
|
373
|
-
const lines = content.split("\n");
|
|
374
|
-
const headings = [];
|
|
375
|
-
for (let i = 0; i < lines.length; i++) {
|
|
376
|
-
const match = lines[i].match(HEADING_RE);
|
|
377
|
-
if (match) {
|
|
378
|
-
headings.push({
|
|
379
|
-
level: match[1].length,
|
|
380
|
-
text: match[2].trim(),
|
|
381
|
-
startLine: i,
|
|
382
|
-
contentStart: i + 1,
|
|
383
|
-
contentEnd: lines.length
|
|
384
|
-
// placeholder — resolved below
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
for (let i = 0; i < headings.length; i++) {
|
|
389
|
-
const current = headings[i];
|
|
390
|
-
let end = lines.length;
|
|
391
|
-
for (let j = i + 1; j < headings.length; j++) {
|
|
392
|
-
if (headings[j].level <= current.level) {
|
|
393
|
-
end = headings[j].startLine;
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
current.contentEnd = end;
|
|
398
|
-
}
|
|
399
|
-
return headings;
|
|
400
|
-
}
|
|
401
|
-
function resolveHeadingPath(content, headingPath) {
|
|
402
|
-
const lines = content.split("\n");
|
|
403
|
-
const headings = parseHeadings(content);
|
|
404
|
-
if (headingPath === "") {
|
|
405
|
-
const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
|
|
406
|
-
return {
|
|
407
|
-
startLine: 0,
|
|
408
|
-
contentStart: 0,
|
|
409
|
-
contentEnd: firstHeadingLine
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
const segments = headingPath.split(">").map((s) => s.trim());
|
|
413
|
-
let searchStart = 0;
|
|
414
|
-
let searchEnd = lines.length;
|
|
415
|
-
let resolved = null;
|
|
416
|
-
for (let si = 0; si < segments.length; si++) {
|
|
417
|
-
const segment = segments[si].toLowerCase();
|
|
418
|
-
const candidates = headings.filter(
|
|
419
|
-
(h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
|
|
420
|
-
);
|
|
421
|
-
if (candidates.length === 0) {
|
|
422
|
-
const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
|
|
423
|
-
const searchedPath = segments.slice(0, si + 1).join(" > ");
|
|
424
|
-
throw new Error(
|
|
425
|
-
`Heading not found: "${searchedPath}"
|
|
426
|
-
|
|
427
|
-
Available headings:
|
|
428
|
-
${available || "(none)"}`
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
resolved = candidates[0];
|
|
432
|
-
searchStart = resolved.contentStart;
|
|
433
|
-
searchEnd = resolved.contentEnd;
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
startLine: resolved.startLine,
|
|
437
|
-
contentStart: resolved.contentStart,
|
|
438
|
-
contentEnd: resolved.contentEnd
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
372
|
function validateSpecPath(filePath) {
|
|
442
373
|
if (!filePath.startsWith("src/")) {
|
|
443
374
|
throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
|
|
444
375
|
}
|
|
445
376
|
return filePath;
|
|
446
377
|
}
|
|
447
|
-
function
|
|
448
|
-
const
|
|
449
|
-
if (
|
|
450
|
-
return
|
|
378
|
+
function extractFrontmatter(content) {
|
|
379
|
+
const lines = content.split("\n");
|
|
380
|
+
if (lines.length === 0 || lines[0].trim() !== "---") {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
for (let i = 1; i < lines.length; i++) {
|
|
384
|
+
if (lines[i].trim() === "---") {
|
|
385
|
+
return lines.slice(0, i + 1).join("\n");
|
|
386
|
+
}
|
|
451
387
|
}
|
|
452
|
-
return
|
|
388
|
+
return null;
|
|
453
389
|
}
|
|
454
|
-
var HEADING_RE;
|
|
455
390
|
var init_helpers = __esm({
|
|
456
391
|
"src/tools/spec/_helpers.ts"() {
|
|
457
392
|
"use strict";
|
|
458
|
-
HEADING_RE = /^(#{1,6})\s+(.+)$/;
|
|
459
393
|
}
|
|
460
394
|
});
|
|
461
395
|
|
|
@@ -657,6 +591,90 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
|
|
|
657
591
|
}
|
|
658
592
|
});
|
|
659
593
|
|
|
594
|
+
// src/tools/code/editFile/_helpers.ts
|
|
595
|
+
function buildLineOffsets(content) {
|
|
596
|
+
const offsets = [0];
|
|
597
|
+
for (let i = 0; i < content.length; i++) {
|
|
598
|
+
if (content[i] === "\n") {
|
|
599
|
+
offsets.push(i + 1);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return offsets;
|
|
603
|
+
}
|
|
604
|
+
function lineAtOffset(offsets, charIndex) {
|
|
605
|
+
let lo = 0;
|
|
606
|
+
let hi = offsets.length - 1;
|
|
607
|
+
while (lo < hi) {
|
|
608
|
+
const mid = lo + hi + 1 >> 1;
|
|
609
|
+
if (offsets[mid] <= charIndex) {
|
|
610
|
+
lo = mid;
|
|
611
|
+
} else {
|
|
612
|
+
hi = mid - 1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return lo + 1;
|
|
616
|
+
}
|
|
617
|
+
function findOccurrences(content, searchString) {
|
|
618
|
+
if (!searchString) {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
const offsets = buildLineOffsets(content);
|
|
622
|
+
const results = [];
|
|
623
|
+
let pos = 0;
|
|
624
|
+
while (pos <= content.length - searchString.length) {
|
|
625
|
+
const idx = content.indexOf(searchString, pos);
|
|
626
|
+
if (idx === -1) {
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
results.push({ index: idx, line: lineAtOffset(offsets, idx) });
|
|
630
|
+
pos = idx + 1;
|
|
631
|
+
}
|
|
632
|
+
return results;
|
|
633
|
+
}
|
|
634
|
+
function flexibleMatch(content, searchString) {
|
|
635
|
+
const contentLines = content.split("\n");
|
|
636
|
+
const searchLines = searchString.split("\n").map((l) => l.trimStart());
|
|
637
|
+
if (searchLines.length === 0) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
const matches = [];
|
|
641
|
+
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
642
|
+
let allMatch = true;
|
|
643
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
644
|
+
if (contentLines[i + j].trimStart() !== searchLines[j]) {
|
|
645
|
+
allMatch = false;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (allMatch) {
|
|
650
|
+
matches.push(i);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (matches.length !== 1) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
const startIdx = matches[0];
|
|
657
|
+
const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
|
|
658
|
+
const offsets = buildLineOffsets(content);
|
|
659
|
+
return {
|
|
660
|
+
matchedText,
|
|
661
|
+
index: offsets[startIdx],
|
|
662
|
+
line: startIdx + 1
|
|
663
|
+
// 1-based
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function replaceAt(content, index, oldLength, newString) {
|
|
667
|
+
return content.slice(0, index) + newString + content.slice(index + oldLength);
|
|
668
|
+
}
|
|
669
|
+
function formatOccurrenceError(count, lines, filePath) {
|
|
670
|
+
return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
|
|
671
|
+
}
|
|
672
|
+
var init_helpers2 = __esm({
|
|
673
|
+
"src/tools/code/editFile/_helpers.ts"() {
|
|
674
|
+
"use strict";
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
660
678
|
// src/tools/spec/editSpec.ts
|
|
661
679
|
import fs4 from "fs/promises";
|
|
662
680
|
var editSpecTool;
|
|
@@ -664,13 +682,14 @@ var init_editSpec = __esm({
|
|
|
664
682
|
"src/tools/spec/editSpec.ts"() {
|
|
665
683
|
"use strict";
|
|
666
684
|
init_helpers();
|
|
685
|
+
init_helpers2();
|
|
667
686
|
init_diff();
|
|
668
687
|
init_fileLock();
|
|
669
688
|
editSpecTool = {
|
|
670
689
|
clearable: true,
|
|
671
690
|
definition: {
|
|
672
691
|
name: "editSpec",
|
|
673
|
-
description:
|
|
692
|
+
description: "Make a targeted find/replace edit to a spec file (src/*.md). old_string must appear exactly once (minor indentation differences are handled automatically); set replace_all to true to replace every occurrence. Read the file with readSpec first so you match the exact text, and include the full enclosing structure rather than an inner fragment. The file's YAML frontmatter (the leading --- \u2026 --- block, which holds required fields like name) is protected \u2014 an edit that would remove or malform it is refused. For a full rewrite, use writeSpec.",
|
|
674
693
|
inputSchema: {
|
|
675
694
|
type: "object",
|
|
676
695
|
properties: {
|
|
@@ -678,31 +697,20 @@ var init_editSpec = __esm({
|
|
|
678
697
|
type: "string",
|
|
679
698
|
description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
|
|
680
699
|
},
|
|
681
|
-
|
|
682
|
-
type: "
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
enum: ["replace", "insert_after", "insert_before", "delete"],
|
|
693
|
-
description: "replace: swap content under this heading (keeps the heading line). insert_after: add content after this section. insert_before: add content before this heading. delete: remove this heading and all its content."
|
|
694
|
-
},
|
|
695
|
-
content: {
|
|
696
|
-
type: "string",
|
|
697
|
-
description: "MSFM markdown content for replace/insert operations. Not needed for delete."
|
|
698
|
-
}
|
|
699
|
-
},
|
|
700
|
-
required: ["heading", "operation"]
|
|
701
|
-
},
|
|
702
|
-
description: "Array of edits to apply in order."
|
|
700
|
+
old_string: {
|
|
701
|
+
type: "string",
|
|
702
|
+
description: "The exact string to find and replace. Must be unique in the file unless replace_all is true."
|
|
703
|
+
},
|
|
704
|
+
new_string: {
|
|
705
|
+
type: "string",
|
|
706
|
+
description: "The replacement string."
|
|
707
|
+
},
|
|
708
|
+
replace_all: {
|
|
709
|
+
type: "boolean",
|
|
710
|
+
description: "If true, replace every occurrence of old_string in the file. Defaults to false."
|
|
703
711
|
}
|
|
704
712
|
},
|
|
705
|
-
required: ["path", "
|
|
713
|
+
required: ["path", "old_string", "new_string"]
|
|
706
714
|
}
|
|
707
715
|
},
|
|
708
716
|
async execute(input) {
|
|
@@ -713,69 +721,66 @@ var init_editSpec = __esm({
|
|
|
713
721
|
}
|
|
714
722
|
const release = await acquireFileLock(input.path);
|
|
715
723
|
try {
|
|
716
|
-
let
|
|
724
|
+
let content;
|
|
717
725
|
try {
|
|
718
|
-
|
|
726
|
+
content = await fs4.readFile(input.path, "utf-8");
|
|
719
727
|
} catch (err) {
|
|
720
728
|
return `Error reading file: ${err.message}`;
|
|
721
729
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
return `Error: ${err.message}
|
|
730
|
-
|
|
731
|
-
Document structure:
|
|
732
|
-
${tree}`;
|
|
730
|
+
const { old_string, new_string, replace_all } = input;
|
|
731
|
+
const occurrences = findOccurrences(content, old_string);
|
|
732
|
+
let updated;
|
|
733
|
+
let notePrefix;
|
|
734
|
+
if (replace_all) {
|
|
735
|
+
if (occurrences.length === 0) {
|
|
736
|
+
return `Error: old_string not found in ${input.path}.`;
|
|
733
737
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
lines.splice(range.startLine, 0, ...contentLines);
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
case "delete": {
|
|
765
|
-
lines.splice(range.startLine, range.contentEnd - range.startLine);
|
|
766
|
-
break;
|
|
767
|
-
}
|
|
768
|
-
default:
|
|
769
|
-
return `Error: Unknown operation "${edit.operation}". Use replace, insert_after, insert_before, or delete.`;
|
|
738
|
+
updated = content;
|
|
739
|
+
for (let i = occurrences.length - 1; i >= 0; i--) {
|
|
740
|
+
updated = replaceAt(
|
|
741
|
+
updated,
|
|
742
|
+
occurrences[i].index,
|
|
743
|
+
old_string.length,
|
|
744
|
+
new_string
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
notePrefix = `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
|
|
748
|
+
`;
|
|
749
|
+
} else if (occurrences.length === 1) {
|
|
750
|
+
updated = replaceAt(
|
|
751
|
+
content,
|
|
752
|
+
occurrences[0].index,
|
|
753
|
+
old_string.length,
|
|
754
|
+
new_string
|
|
755
|
+
);
|
|
756
|
+
notePrefix = `Updated ${input.path}
|
|
757
|
+
`;
|
|
758
|
+
} else if (occurrences.length > 1) {
|
|
759
|
+
const lines = occurrences.map((o) => o.line);
|
|
760
|
+
return `Error: ${formatOccurrenceError(occurrences.length, lines, input.path)}`;
|
|
761
|
+
} else {
|
|
762
|
+
const flex = flexibleMatch(content, old_string);
|
|
763
|
+
if (!flex) {
|
|
764
|
+
return `Error: old_string not found in ${input.path}. Make sure you've read the file first and copied the exact text.`;
|
|
770
765
|
}
|
|
771
|
-
|
|
766
|
+
updated = replaceAt(
|
|
767
|
+
content,
|
|
768
|
+
flex.index,
|
|
769
|
+
flex.matchedText.length,
|
|
770
|
+
new_string
|
|
771
|
+
);
|
|
772
|
+
notePrefix = `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
|
|
773
|
+
`;
|
|
774
|
+
}
|
|
775
|
+
if (extractFrontmatter(content) !== null && extractFrontmatter(updated) === null) {
|
|
776
|
+
return `Error: that edit would remove or malform the spec's YAML frontmatter (the leading \`--- \u2026 ---\` block, which holds required fields like \`name\`). Narrow old_string to the body content you meant to change and leave the frontmatter block intact.`;
|
|
772
777
|
}
|
|
773
778
|
try {
|
|
774
|
-
await fs4.writeFile(input.path,
|
|
779
|
+
await fs4.writeFile(input.path, updated, "utf-8");
|
|
775
780
|
} catch (err) {
|
|
776
781
|
return `Error writing file: ${err.message}`;
|
|
777
782
|
}
|
|
778
|
-
return unifiedDiff(input.path,
|
|
783
|
+
return `${notePrefix}${unifiedDiff(input.path, content, updated)}`;
|
|
779
784
|
} finally {
|
|
780
785
|
release();
|
|
781
786
|
}
|
|
@@ -2485,90 +2490,6 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
|
|
|
2485
2490
|
}
|
|
2486
2491
|
});
|
|
2487
2492
|
|
|
2488
|
-
// src/tools/code/editFile/_helpers.ts
|
|
2489
|
-
function buildLineOffsets(content) {
|
|
2490
|
-
const offsets = [0];
|
|
2491
|
-
for (let i = 0; i < content.length; i++) {
|
|
2492
|
-
if (content[i] === "\n") {
|
|
2493
|
-
offsets.push(i + 1);
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
return offsets;
|
|
2497
|
-
}
|
|
2498
|
-
function lineAtOffset(offsets, charIndex) {
|
|
2499
|
-
let lo = 0;
|
|
2500
|
-
let hi = offsets.length - 1;
|
|
2501
|
-
while (lo < hi) {
|
|
2502
|
-
const mid = lo + hi + 1 >> 1;
|
|
2503
|
-
if (offsets[mid] <= charIndex) {
|
|
2504
|
-
lo = mid;
|
|
2505
|
-
} else {
|
|
2506
|
-
hi = mid - 1;
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
return lo + 1;
|
|
2510
|
-
}
|
|
2511
|
-
function findOccurrences(content, searchString) {
|
|
2512
|
-
if (!searchString) {
|
|
2513
|
-
return [];
|
|
2514
|
-
}
|
|
2515
|
-
const offsets = buildLineOffsets(content);
|
|
2516
|
-
const results = [];
|
|
2517
|
-
let pos = 0;
|
|
2518
|
-
while (pos <= content.length - searchString.length) {
|
|
2519
|
-
const idx = content.indexOf(searchString, pos);
|
|
2520
|
-
if (idx === -1) {
|
|
2521
|
-
break;
|
|
2522
|
-
}
|
|
2523
|
-
results.push({ index: idx, line: lineAtOffset(offsets, idx) });
|
|
2524
|
-
pos = idx + 1;
|
|
2525
|
-
}
|
|
2526
|
-
return results;
|
|
2527
|
-
}
|
|
2528
|
-
function flexibleMatch(content, searchString) {
|
|
2529
|
-
const contentLines = content.split("\n");
|
|
2530
|
-
const searchLines = searchString.split("\n").map((l) => l.trimStart());
|
|
2531
|
-
if (searchLines.length === 0) {
|
|
2532
|
-
return null;
|
|
2533
|
-
}
|
|
2534
|
-
const matches = [];
|
|
2535
|
-
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
2536
|
-
let allMatch = true;
|
|
2537
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
2538
|
-
if (contentLines[i + j].trimStart() !== searchLines[j]) {
|
|
2539
|
-
allMatch = false;
|
|
2540
|
-
break;
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
if (allMatch) {
|
|
2544
|
-
matches.push(i);
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
if (matches.length !== 1) {
|
|
2548
|
-
return null;
|
|
2549
|
-
}
|
|
2550
|
-
const startIdx = matches[0];
|
|
2551
|
-
const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
|
|
2552
|
-
const offsets = buildLineOffsets(content);
|
|
2553
|
-
return {
|
|
2554
|
-
matchedText,
|
|
2555
|
-
index: offsets[startIdx],
|
|
2556
|
-
line: startIdx + 1
|
|
2557
|
-
// 1-based
|
|
2558
|
-
};
|
|
2559
|
-
}
|
|
2560
|
-
function replaceAt(content, index, oldLength, newString) {
|
|
2561
|
-
return content.slice(0, index) + newString + content.slice(index + oldLength);
|
|
2562
|
-
}
|
|
2563
|
-
function formatOccurrenceError(count, lines, filePath) {
|
|
2564
|
-
return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
|
|
2565
|
-
}
|
|
2566
|
-
var init_helpers2 = __esm({
|
|
2567
|
-
"src/tools/code/editFile/_helpers.ts"() {
|
|
2568
|
-
"use strict";
|
|
2569
|
-
}
|
|
2570
|
-
});
|
|
2571
|
-
|
|
2572
2493
|
// src/tools/code/editFile/index.ts
|
|
2573
2494
|
import fs13 from "fs/promises";
|
|
2574
2495
|
var editFileTool;
|
|
@@ -90,5 +90,5 @@ When generated code exists in `dist/`, you have both spec tools and code tools.
|
|
|
90
90
|
**Key principle: spec and code stay in sync.**
|
|
91
91
|
- When editing the spec, also update the affected code in the same turn.
|
|
92
92
|
- When the user asks for a code change that represents a behavioral change, also update the spec.
|
|
93
|
-
- Spec tools
|
|
93
|
+
- Spec tools work on `src/` files: `readSpec` to read (do this before editing), `editSpec` for targeted find/replace edits (same `old_string`/`new_string` model as `editFile`), `writeSpec` for a full-file write, `listSpecFiles` to see what exists.
|
|
94
94
|
- Code tools (`readFile`, `writeFile`, `editFile`, etc.) work on `dist/` and other project files.
|
|
@@ -36,6 +36,8 @@ For counting visitor or funnel activity (signups, page interactions, checkout co
|
|
|
36
36
|
|
|
37
37
|
When writing `db` filter predicates that reference outer-scope values (`input.*`, `auth.*`, foreign keys collected earlier, etc.), use the bindings form so the filter compiles to SQL — see `tables.md` "Filter Predicates" for patterns and the inline-comment convention.
|
|
38
38
|
|
|
39
|
+
The SDK also includes a `reportIssue` method that can file bug reports on the app's platform-level issue tracker, if the user wants to build something to allow their users to report bugs in their app.
|
|
40
|
+
|
|
39
41
|
### Auth
|
|
40
42
|
- Not every app needs auth, and even for apps that do need auth, not every screen needs auth. Think intentionally about places where auth is required. Don't make auth be the first thing a user sees - that's jarring. Only show auth at intuitive and natural moments in the user's journey - be thoughtful about how to implement auth in the UI.
|
|
41
43
|
- Frontend interfaces are always untrusted. Always enforce auth in backend methods. Use frontend auth and role information as a hint to conditionally show/hide UI to make the experience pleasant and seamless for users depending on their state, but remember to always use backend methods for gating data that is conditional on auth.
|
|
@@ -79,6 +81,6 @@ You have access to the `mindstudio` CLI, which exposes every SDK action as a com
|
|
|
79
81
|
### Production App Management
|
|
80
82
|
You have access to `mindstudio-prod`, a CLI for managing the user's production app. Use it via your bash tool. All output is JSON. Run `mindstudio-prod --help` or `mindstudio-prod <command> --help` to discover usage and available options.
|
|
81
83
|
|
|
82
|
-
Available commands: `requests` (server-side request logs, error rates, latency), `crashes` (frontend browser errors — grouped issues + drill-down to individual events), `analytics` (traffic, top pages/referrers/geo, AI-referral attribution, live counters), `releases` (deploy status, history), `domains` (custom subdomains and fully custom domains), `users` (list, set roles), `db` (query production sql db), `data` (live db operations like lift-from-dev), `methods` (list, invoke), `secrets` (list, get, set, delete).
|
|
84
|
+
Available commands: `requests` (server-side request logs, error rates, latency), `crashes` (frontend browser errors — grouped issues + drill-down to individual events), `analytics` (traffic, top pages/referrers/geo, AI-referral attribution, live counters), `releases` (deploy status, history), `domains` (custom subdomains and fully custom domains), `users` (list, set roles), `db` (query production sql db), `data` (live db operations like lift-from-dev), `methods` (list, invoke), `secrets` (list, get, set, delete), `issues` (reference and manage externally-reported bugs and issues. Do not use this to track work you are doing with the user - only read from it and resolve issues if the user asks for help fixing a bug from the issues tracker).
|
|
83
85
|
|
|
84
86
|
Use when the user asks about production behavior (server errors via `requests`, browser crashes via `crashes`, traffic/engagement via `analytics`), wants to manage their live app (domains, users, roles), needs to seed or query production data, or wants to check release status.
|