@mindstudio-ai/remy 0.1.250 → 0.1.252
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 +305 -239
- package/dist/index.js +332 -262
- package/dist/prompt/compiled/files.md +95 -0
- package/dist/subagents/codeSanityCheck/prompt.md +2 -0
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -828,6 +828,10 @@ Current date: ${now}
|
|
|
828
828
|
{{compiled/media-cdn.md}}
|
|
829
829
|
</media_cdn>
|
|
830
830
|
|
|
831
|
+
<app_files>
|
|
832
|
+
{{compiled/files.md}}
|
|
833
|
+
</app_files>
|
|
834
|
+
|
|
831
835
|
<interfaces>
|
|
832
836
|
{{compiled/interfaces.md}}
|
|
833
837
|
</interfaces>
|
|
@@ -2002,6 +2006,7 @@ var compactConversationTool = {
|
|
|
2002
2006
|
// src/tools/code/readFile.ts
|
|
2003
2007
|
import fs12 from "fs/promises";
|
|
2004
2008
|
var DEFAULT_WINDOW = 500;
|
|
2009
|
+
var MAX_BYTES = 64 * 1024;
|
|
2005
2010
|
function isBinary(buffer) {
|
|
2006
2011
|
const sample = buffer.subarray(0, 8192);
|
|
2007
2012
|
for (let i = 0; i < sample.length; i++) {
|
|
@@ -2015,7 +2020,7 @@ var readFileTool = {
|
|
|
2015
2020
|
clearable: true,
|
|
2016
2021
|
definition: {
|
|
2017
2022
|
name: "readFile",
|
|
2018
|
-
description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. By default returns the first 500 lines. To read a specific range, pass startLine and endLine (1-indexed, inclusive) \u2014 e.g. to read lines 253\u2013343, pass startLine: 253, endLine: 343. To read the end of a file or log, pass tail (the number of lines from the end). Line numbers in the output correspond to what editFile expects. For a large file, locate the relevant section first (symbols or grep), then read just that range.",
|
|
2023
|
+
description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. By default returns the first 500 lines, and at most 64KB \u2014 a file with very wide lines (a CSV, a minified bundle) comes back short of 500 lines, so read a narrower range or grep rather than paging through it. To read a specific range, pass startLine and endLine (1-indexed, inclusive) \u2014 e.g. to read lines 253\u2013343, pass startLine: 253, endLine: 343. To read the end of a file or log, pass tail (the number of lines from the end). Line numbers in the output correspond to what editFile expects. For a large file, locate the relevant section first (symbols or grep), then read just that range.",
|
|
2019
2024
|
inputSchema: {
|
|
2020
2025
|
type: "object",
|
|
2021
2026
|
properties: {
|
|
@@ -2072,12 +2077,35 @@ var readFileTool = {
|
|
|
2072
2077
|
endIdxExclusive = Math.min(startIdx + DEFAULT_WINDOW, totalLines);
|
|
2073
2078
|
}
|
|
2074
2079
|
}
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2077
|
-
let
|
|
2080
|
+
const numberedLines = allLines.slice(startIdx, endIdxExclusive).map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`);
|
|
2081
|
+
let kept = numberedLines;
|
|
2082
|
+
let byteTruncated = false;
|
|
2083
|
+
if (Buffer.byteLength(numberedLines.join("\n"), "utf-8") > MAX_BYTES) {
|
|
2084
|
+
byteTruncated = true;
|
|
2085
|
+
kept = [];
|
|
2086
|
+
let used = 0;
|
|
2087
|
+
for (const line of numberedLines) {
|
|
2088
|
+
const cost = Buffer.byteLength(line, "utf-8") + (kept.length > 0 ? 1 : 0);
|
|
2089
|
+
if (used + cost > MAX_BYTES) {
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
kept.push(line);
|
|
2093
|
+
used += cost;
|
|
2094
|
+
}
|
|
2095
|
+
if (kept.length === 0) {
|
|
2096
|
+
kept = [
|
|
2097
|
+
Buffer.from(numberedLines[0], "utf-8").subarray(0, MAX_BYTES).toString("utf-8")
|
|
2098
|
+
];
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
let result = kept.join("\n");
|
|
2078
2102
|
const displayStart = startIdx + 1;
|
|
2079
|
-
const displayEnd = startIdx +
|
|
2080
|
-
if (
|
|
2103
|
+
const displayEnd = startIdx + kept.length;
|
|
2104
|
+
if (byteTruncated) {
|
|
2105
|
+
result += `
|
|
2106
|
+
|
|
2107
|
+
(showing lines ${displayStart}\u2013${displayEnd} of ${totalLines}, truncated at ${(MAX_BYTES / 1024).toFixed(0)}KB \u2014 this file's lines are wide. Read a narrower range with startLine/endLine, or grep for what you need, instead of paging through it.)`;
|
|
2108
|
+
} else if (displayStart > 1 || displayEnd < totalLines) {
|
|
2081
2109
|
result += `
|
|
2082
2110
|
|
|
2083
2111
|
(showing lines ${displayStart}\u2013${displayEnd} of ${totalLines} \u2014 pass startLine/endLine to read a different range)`;
|
|
@@ -3766,6 +3794,17 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3766
3794
|
return { text: ack, messages: [], backgrounded: true };
|
|
3767
3795
|
}
|
|
3768
3796
|
|
|
3797
|
+
// src/subagents/common/tools.ts
|
|
3798
|
+
var COMMON_READ_TOOLS = [
|
|
3799
|
+
readFileTool.definition,
|
|
3800
|
+
listDirTool.definition,
|
|
3801
|
+
grepTool.definition,
|
|
3802
|
+
globTool.definition
|
|
3803
|
+
];
|
|
3804
|
+
var COMMON_READ_TOOL_NAMES = new Set(
|
|
3805
|
+
COMMON_READ_TOOLS.map((t) => t.name)
|
|
3806
|
+
);
|
|
3807
|
+
|
|
3769
3808
|
// src/subagents/browserAutomation/tools.ts
|
|
3770
3809
|
var BROWSER_TOOLS = [
|
|
3771
3810
|
{
|
|
@@ -3909,23 +3948,193 @@ var BROWSER_TOOLS = [
|
|
|
3909
3948
|
}
|
|
3910
3949
|
}
|
|
3911
3950
|
}
|
|
3912
|
-
}
|
|
3951
|
+
},
|
|
3952
|
+
// Read tools so the QA agent can pull full spec detail on demand — the spec
|
|
3953
|
+
// context in its prompt is a lightweight index (see prompt.ts) that points
|
|
3954
|
+
// here. Routed to the global executeTool in index.ts, mirroring specSync.
|
|
3955
|
+
...COMMON_READ_TOOLS,
|
|
3956
|
+
readSpecTool.definition
|
|
3913
3957
|
];
|
|
3914
3958
|
var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
|
|
3915
3959
|
|
|
3916
|
-
// src/subagents/
|
|
3960
|
+
// src/subagents/common/context.ts
|
|
3917
3961
|
import fs16 from "fs";
|
|
3918
|
-
|
|
3919
|
-
function
|
|
3962
|
+
import path9 from "path";
|
|
3963
|
+
function walkMdFiles2(dir, skip) {
|
|
3964
|
+
const files = [];
|
|
3920
3965
|
try {
|
|
3921
|
-
const
|
|
3922
|
-
|
|
3966
|
+
for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
|
|
3967
|
+
const full = path9.join(dir, entry.name);
|
|
3968
|
+
if (entry.isDirectory()) {
|
|
3969
|
+
if (!skip?.has(entry.name)) {
|
|
3970
|
+
files.push(...walkMdFiles2(full, skip));
|
|
3971
|
+
}
|
|
3972
|
+
} else if (entry.name.endsWith(".md")) {
|
|
3973
|
+
files.push(full);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
} catch {
|
|
3977
|
+
}
|
|
3978
|
+
return files.sort();
|
|
3979
|
+
}
|
|
3980
|
+
function parseFrontmatter2(filePath) {
|
|
3981
|
+
try {
|
|
3982
|
+
const content = fs16.readFileSync(filePath, "utf-8");
|
|
3983
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
3984
|
+
if (!match) {
|
|
3985
|
+
return {};
|
|
3986
|
+
}
|
|
3987
|
+
const fm = {};
|
|
3988
|
+
for (const line of match[1].split("\n")) {
|
|
3989
|
+
const sep = line.indexOf(":");
|
|
3990
|
+
if (sep > 0) {
|
|
3991
|
+
const key = line.slice(0, sep).trim();
|
|
3992
|
+
const val = line.slice(sep + 1).trim();
|
|
3993
|
+
fm[key] = val;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
return fm;
|
|
3997
|
+
} catch {
|
|
3998
|
+
return {};
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
function loadSpecIndex() {
|
|
4002
|
+
const files = walkMdFiles2("src", /* @__PURE__ */ new Set(["roadmap"]));
|
|
4003
|
+
if (files.length === 0) {
|
|
4004
|
+
return "";
|
|
4005
|
+
}
|
|
4006
|
+
const lines = files.map((f) => {
|
|
4007
|
+
const fm = parseFrontmatter2(f);
|
|
4008
|
+
let line = `- ${f}`;
|
|
4009
|
+
if (fm.name) {
|
|
4010
|
+
line += ` \u2014 "${fm.name}"`;
|
|
4011
|
+
}
|
|
4012
|
+
if (fm.description) {
|
|
4013
|
+
line += ` \u2014 ${fm.description}`;
|
|
4014
|
+
}
|
|
4015
|
+
return line;
|
|
4016
|
+
});
|
|
4017
|
+
return `<spec_files>
|
|
4018
|
+
## Project Spec Files
|
|
4019
|
+
Use readFile to access full contents.
|
|
3923
4020
|
|
|
3924
|
-
|
|
4021
|
+
${lines.join("\n")}
|
|
4022
|
+
</spec_files>`;
|
|
4023
|
+
}
|
|
4024
|
+
function loadRoadmapIndex() {
|
|
4025
|
+
const parts = [];
|
|
4026
|
+
try {
|
|
4027
|
+
const indexJson = JSON.parse(
|
|
4028
|
+
fs16.readFileSync("src/roadmap/index.json", "utf-8")
|
|
4029
|
+
);
|
|
4030
|
+
if (indexJson.lanes?.length > 0) {
|
|
4031
|
+
const laneLines = indexJson.lanes.map(
|
|
4032
|
+
(l) => `- **${l.name}**: ${l.narrative || ""} (${l.items?.length || 0} items)`
|
|
4033
|
+
);
|
|
4034
|
+
parts.push(`### Lanes
|
|
4035
|
+
${laneLines.join("\n")}`);
|
|
4036
|
+
}
|
|
4037
|
+
if (indexJson.standalone?.length > 0) {
|
|
4038
|
+
parts.push(
|
|
4039
|
+
`### Standalone
|
|
4040
|
+
${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
} catch {
|
|
4044
|
+
}
|
|
4045
|
+
const files = walkMdFiles2("src/roadmap");
|
|
4046
|
+
if (files.length > 0) {
|
|
4047
|
+
const lines = files.map((f) => {
|
|
4048
|
+
const fm = parseFrontmatter2(f);
|
|
4049
|
+
let line = `- ${f}`;
|
|
4050
|
+
if (fm.name) {
|
|
4051
|
+
line += ` \u2014 "${fm.name}"`;
|
|
4052
|
+
}
|
|
4053
|
+
if (fm.status) {
|
|
4054
|
+
line += ` (${fm.status})`;
|
|
4055
|
+
}
|
|
4056
|
+
if (fm.description) {
|
|
4057
|
+
line += ` \u2014 ${fm.description}`;
|
|
4058
|
+
}
|
|
4059
|
+
return line;
|
|
4060
|
+
});
|
|
4061
|
+
parts.push(`### Items
|
|
4062
|
+
${lines.join("\n")}`);
|
|
4063
|
+
}
|
|
4064
|
+
if (parts.length === 0) {
|
|
4065
|
+
return "";
|
|
4066
|
+
}
|
|
4067
|
+
return `<current_roadmap>
|
|
4068
|
+
## Roadmap
|
|
4069
|
+
Use readFile to access full contents.
|
|
4070
|
+
|
|
4071
|
+
${parts.join("\n\n")}
|
|
4072
|
+
</current_roadmap>`;
|
|
4073
|
+
}
|
|
4074
|
+
function loadPlatformBrief() {
|
|
4075
|
+
return `<platform_brief>
|
|
4076
|
+
## What is a Remy app?
|
|
4077
|
+
|
|
4078
|
+
A Remy app is a managed full-stack TypeScript project with three layers: a spec (natural language in src/), a backend contract (methods, tables, roles in dist/), and one or more interfaces (web, API, bots, cron, etc.). The spec is the source of truth; code is derived from it.
|
|
4079
|
+
|
|
4080
|
+
This is a capable, stable platform used in production by 100k+ users. Build with confidence \u2014 you're building production-grade apps, not fragile prototypes.
|
|
4081
|
+
|
|
4082
|
+
## What people build
|
|
4083
|
+
|
|
4084
|
+
- Business tools \u2014 client portals, approval workflows, admin panels with role-based access
|
|
4085
|
+
- AI-powered apps \u2014 document processors, image/video tools, content generators, conversational agents that take actions
|
|
4086
|
+
- Full-stack web apps \u2014 social platforms, membership sites, marketplaces, booking systems, community hubs \u2014 multi-user apps with auth, data, UI
|
|
4087
|
+
- Automations with no UI \u2014 cron jobs, webhook handlers, email processors, data sync pipelines
|
|
4088
|
+
- Marketing & launch pages \u2014 landing pages, waitlist pages with referral mechanics, product sites with scroll animations
|
|
4089
|
+
- Agent tools \u2014 MCP tool servers for AI assistants
|
|
4090
|
+
- Creative/interactive projects \u2014 browser games with p5.js or Three.js, interactive visualizations, generative art, portfolio sites
|
|
4091
|
+
- API services \u2014 backend logic exposed as REST endpoints
|
|
4092
|
+
- Simple static sites \u2014 no backend needed, just a web interface with a build step
|
|
4093
|
+
|
|
4094
|
+
An app can be any combination of these.
|
|
4095
|
+
|
|
4096
|
+
## Interfaces
|
|
4097
|
+
|
|
4098
|
+
Each interface type invokes the same backend methods. Methods don't know which interface called them.
|
|
4099
|
+
|
|
4100
|
+
- Web \u2014 any TypeScript project with a build command. Framework-agnostic (React, Vue, Svelte, vanilla, anything). The frontend SDK provides typed RPC to backend methods.
|
|
4101
|
+
- API \u2014 auto-generated REST endpoints for every method
|
|
4102
|
+
- Cron \u2014 scheduled jobs on a configurable interval
|
|
4103
|
+
- Webhook \u2014 HTTP endpoints that trigger methods
|
|
4104
|
+
- Email \u2014 inbound email processing
|
|
4105
|
+
- MCP \u2014 tool servers for AI assistants
|
|
4106
|
+
- Agent \u2014 conversational LLM interface with tool access to backend methods
|
|
4107
|
+
|
|
4108
|
+
## Backend
|
|
4109
|
+
|
|
4110
|
+
TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
|
|
4111
|
+
|
|
4112
|
+
- Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
|
|
4113
|
+
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email-code, sms-code) and cookie sessions. API key auth for programmatic access. No OAuth, no social login (no Apple, Google, Facebook, or GitHub sign-in). Backend methods use auth.requireRole() for access control.
|
|
4114
|
+
- Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
|
|
4115
|
+
- Git-native deployment. Push to default branch to deploy.
|
|
4116
|
+
|
|
4117
|
+
## MindStudio SDK
|
|
4118
|
+
|
|
4119
|
+
The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (OpenAI, Anthropic, Google, Meta, Mistral, and more) and 1000+ integrations (email, SMS, Slack, HubSpot, Google Workspace, web scraping, image/video generation, media processing, and much more) with zero configuration \u2014 credentials are handled automatically in the execution environment. No API keys needed. This SDK is robust and battle-tested in production.
|
|
3925
4120
|
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
4121
|
+
## What Remy apps are NOT good for
|
|
4122
|
+
|
|
4123
|
+
- Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
|
|
4124
|
+
- Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
|
|
4125
|
+
</platform_brief>`;
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
// src/subagents/browserAutomation/prompt.ts
|
|
4129
|
+
var BASE_PROMPT = readAsset("subagents/browserAutomation", "prompt.md");
|
|
4130
|
+
function getBrowserAutomationPrompt() {
|
|
4131
|
+
try {
|
|
4132
|
+
const specIndex = loadSpecIndex();
|
|
4133
|
+
const parts = [BASE_PROMPT, "<!-- cache_breakpoint -->"];
|
|
4134
|
+
if (specIndex) {
|
|
4135
|
+
parts.push(specIndex);
|
|
4136
|
+
}
|
|
4137
|
+
return parts.join("\n\n");
|
|
3929
4138
|
} catch {
|
|
3930
4139
|
return BASE_PROMPT;
|
|
3931
4140
|
}
|
|
@@ -3982,6 +4191,9 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
3982
4191
|
return `Error taking screenshot: ${err.message}`;
|
|
3983
4192
|
}
|
|
3984
4193
|
}
|
|
4194
|
+
if (COMMON_READ_TOOL_NAMES.has(name) || name === readSpecTool.definition.name) {
|
|
4195
|
+
return executeTool(name, _input, context);
|
|
4196
|
+
}
|
|
3985
4197
|
return `Error: unknown local tool "${name}"`;
|
|
3986
4198
|
},
|
|
3987
4199
|
apiConfig: context.apiConfig,
|
|
@@ -4190,17 +4402,6 @@ var screenshotTool = {
|
|
|
4190
4402
|
}
|
|
4191
4403
|
};
|
|
4192
4404
|
|
|
4193
|
-
// src/subagents/common/tools.ts
|
|
4194
|
-
var COMMON_READ_TOOLS = [
|
|
4195
|
-
readFileTool.definition,
|
|
4196
|
-
listDirTool.definition,
|
|
4197
|
-
grepTool.definition,
|
|
4198
|
-
globTool.definition
|
|
4199
|
-
];
|
|
4200
|
-
var COMMON_READ_TOOL_NAMES = new Set(
|
|
4201
|
-
COMMON_READ_TOOLS.map((t) => t.name)
|
|
4202
|
-
);
|
|
4203
|
-
|
|
4204
4405
|
// src/subagents/designExpert/tools/searchGoogle.ts
|
|
4205
4406
|
var searchGoogle_exports = {};
|
|
4206
4407
|
__export(searchGoogle_exports, {
|
|
@@ -4802,174 +5003,6 @@ __export(polishCopy_exports, {
|
|
|
4802
5003
|
execute: () => execute8
|
|
4803
5004
|
});
|
|
4804
5005
|
|
|
4805
|
-
// src/subagents/common/context.ts
|
|
4806
|
-
import fs17 from "fs";
|
|
4807
|
-
import path9 from "path";
|
|
4808
|
-
function walkMdFiles2(dir, skip) {
|
|
4809
|
-
const files = [];
|
|
4810
|
-
try {
|
|
4811
|
-
for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
|
|
4812
|
-
const full = path9.join(dir, entry.name);
|
|
4813
|
-
if (entry.isDirectory()) {
|
|
4814
|
-
if (!skip?.has(entry.name)) {
|
|
4815
|
-
files.push(...walkMdFiles2(full, skip));
|
|
4816
|
-
}
|
|
4817
|
-
} else if (entry.name.endsWith(".md")) {
|
|
4818
|
-
files.push(full);
|
|
4819
|
-
}
|
|
4820
|
-
}
|
|
4821
|
-
} catch {
|
|
4822
|
-
}
|
|
4823
|
-
return files.sort();
|
|
4824
|
-
}
|
|
4825
|
-
function parseFrontmatter2(filePath) {
|
|
4826
|
-
try {
|
|
4827
|
-
const content = fs17.readFileSync(filePath, "utf-8");
|
|
4828
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
4829
|
-
if (!match) {
|
|
4830
|
-
return {};
|
|
4831
|
-
}
|
|
4832
|
-
const fm = {};
|
|
4833
|
-
for (const line of match[1].split("\n")) {
|
|
4834
|
-
const sep = line.indexOf(":");
|
|
4835
|
-
if (sep > 0) {
|
|
4836
|
-
const key = line.slice(0, sep).trim();
|
|
4837
|
-
const val = line.slice(sep + 1).trim();
|
|
4838
|
-
fm[key] = val;
|
|
4839
|
-
}
|
|
4840
|
-
}
|
|
4841
|
-
return fm;
|
|
4842
|
-
} catch {
|
|
4843
|
-
return {};
|
|
4844
|
-
}
|
|
4845
|
-
}
|
|
4846
|
-
function loadSpecIndex() {
|
|
4847
|
-
const files = walkMdFiles2("src", /* @__PURE__ */ new Set(["roadmap"]));
|
|
4848
|
-
if (files.length === 0) {
|
|
4849
|
-
return "";
|
|
4850
|
-
}
|
|
4851
|
-
const lines = files.map((f) => {
|
|
4852
|
-
const fm = parseFrontmatter2(f);
|
|
4853
|
-
let line = `- ${f}`;
|
|
4854
|
-
if (fm.name) {
|
|
4855
|
-
line += ` \u2014 "${fm.name}"`;
|
|
4856
|
-
}
|
|
4857
|
-
if (fm.description) {
|
|
4858
|
-
line += ` \u2014 ${fm.description}`;
|
|
4859
|
-
}
|
|
4860
|
-
return line;
|
|
4861
|
-
});
|
|
4862
|
-
return `<spec_files>
|
|
4863
|
-
## Project Spec Files
|
|
4864
|
-
Use readFile to access full contents.
|
|
4865
|
-
|
|
4866
|
-
${lines.join("\n")}
|
|
4867
|
-
</spec_files>`;
|
|
4868
|
-
}
|
|
4869
|
-
function loadRoadmapIndex() {
|
|
4870
|
-
const parts = [];
|
|
4871
|
-
try {
|
|
4872
|
-
const indexJson = JSON.parse(
|
|
4873
|
-
fs17.readFileSync("src/roadmap/index.json", "utf-8")
|
|
4874
|
-
);
|
|
4875
|
-
if (indexJson.lanes?.length > 0) {
|
|
4876
|
-
const laneLines = indexJson.lanes.map(
|
|
4877
|
-
(l) => `- **${l.name}**: ${l.narrative || ""} (${l.items?.length || 0} items)`
|
|
4878
|
-
);
|
|
4879
|
-
parts.push(`### Lanes
|
|
4880
|
-
${laneLines.join("\n")}`);
|
|
4881
|
-
}
|
|
4882
|
-
if (indexJson.standalone?.length > 0) {
|
|
4883
|
-
parts.push(
|
|
4884
|
-
`### Standalone
|
|
4885
|
-
${indexJson.standalone.map((s) => `- ${s}`).join("\n")}`
|
|
4886
|
-
);
|
|
4887
|
-
}
|
|
4888
|
-
} catch {
|
|
4889
|
-
}
|
|
4890
|
-
const files = walkMdFiles2("src/roadmap");
|
|
4891
|
-
if (files.length > 0) {
|
|
4892
|
-
const lines = files.map((f) => {
|
|
4893
|
-
const fm = parseFrontmatter2(f);
|
|
4894
|
-
let line = `- ${f}`;
|
|
4895
|
-
if (fm.name) {
|
|
4896
|
-
line += ` \u2014 "${fm.name}"`;
|
|
4897
|
-
}
|
|
4898
|
-
if (fm.status) {
|
|
4899
|
-
line += ` (${fm.status})`;
|
|
4900
|
-
}
|
|
4901
|
-
if (fm.description) {
|
|
4902
|
-
line += ` \u2014 ${fm.description}`;
|
|
4903
|
-
}
|
|
4904
|
-
return line;
|
|
4905
|
-
});
|
|
4906
|
-
parts.push(`### Items
|
|
4907
|
-
${lines.join("\n")}`);
|
|
4908
|
-
}
|
|
4909
|
-
if (parts.length === 0) {
|
|
4910
|
-
return "";
|
|
4911
|
-
}
|
|
4912
|
-
return `<current_roadmap>
|
|
4913
|
-
## Roadmap
|
|
4914
|
-
Use readFile to access full contents.
|
|
4915
|
-
|
|
4916
|
-
${parts.join("\n\n")}
|
|
4917
|
-
</current_roadmap>`;
|
|
4918
|
-
}
|
|
4919
|
-
function loadPlatformBrief() {
|
|
4920
|
-
return `<platform_brief>
|
|
4921
|
-
## What is a Remy app?
|
|
4922
|
-
|
|
4923
|
-
A Remy app is a managed full-stack TypeScript project with three layers: a spec (natural language in src/), a backend contract (methods, tables, roles in dist/), and one or more interfaces (web, API, bots, cron, etc.). The spec is the source of truth; code is derived from it.
|
|
4924
|
-
|
|
4925
|
-
This is a capable, stable platform used in production by 100k+ users. Build with confidence \u2014 you're building production-grade apps, not fragile prototypes.
|
|
4926
|
-
|
|
4927
|
-
## What people build
|
|
4928
|
-
|
|
4929
|
-
- Business tools \u2014 client portals, approval workflows, admin panels with role-based access
|
|
4930
|
-
- AI-powered apps \u2014 document processors, image/video tools, content generators, conversational agents that take actions
|
|
4931
|
-
- Full-stack web apps \u2014 social platforms, membership sites, marketplaces, booking systems, community hubs \u2014 multi-user apps with auth, data, UI
|
|
4932
|
-
- Automations with no UI \u2014 cron jobs, webhook handlers, email processors, data sync pipelines
|
|
4933
|
-
- Marketing & launch pages \u2014 landing pages, waitlist pages with referral mechanics, product sites with scroll animations
|
|
4934
|
-
- Agent tools \u2014 MCP tool servers for AI assistants
|
|
4935
|
-
- Creative/interactive projects \u2014 browser games with p5.js or Three.js, interactive visualizations, generative art, portfolio sites
|
|
4936
|
-
- API services \u2014 backend logic exposed as REST endpoints
|
|
4937
|
-
- Simple static sites \u2014 no backend needed, just a web interface with a build step
|
|
4938
|
-
|
|
4939
|
-
An app can be any combination of these.
|
|
4940
|
-
|
|
4941
|
-
## Interfaces
|
|
4942
|
-
|
|
4943
|
-
Each interface type invokes the same backend methods. Methods don't know which interface called them.
|
|
4944
|
-
|
|
4945
|
-
- Web \u2014 any TypeScript project with a build command. Framework-agnostic (React, Vue, Svelte, vanilla, anything). The frontend SDK provides typed RPC to backend methods.
|
|
4946
|
-
- API \u2014 auto-generated REST endpoints for every method
|
|
4947
|
-
- Cron \u2014 scheduled jobs on a configurable interval
|
|
4948
|
-
- Webhook \u2014 HTTP endpoints that trigger methods
|
|
4949
|
-
- Email \u2014 inbound email processing
|
|
4950
|
-
- MCP \u2014 tool servers for AI assistants
|
|
4951
|
-
- Agent \u2014 conversational LLM interface with tool access to backend methods
|
|
4952
|
-
|
|
4953
|
-
## Backend
|
|
4954
|
-
|
|
4955
|
-
TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
|
|
4956
|
-
|
|
4957
|
-
- Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
|
|
4958
|
-
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email-code, sms-code) and cookie sessions. API key auth for programmatic access. No OAuth, no social login (no Apple, Google, Facebook, or GitHub sign-in). Backend methods use auth.requireRole() for access control.
|
|
4959
|
-
- Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
|
|
4960
|
-
- Git-native deployment. Push to default branch to deploy.
|
|
4961
|
-
|
|
4962
|
-
## MindStudio SDK
|
|
4963
|
-
|
|
4964
|
-
The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (OpenAI, Anthropic, Google, Meta, Mistral, and more) and 1000+ integrations (email, SMS, Slack, HubSpot, Google Workspace, web scraping, image/video generation, media processing, and much more) with zero configuration \u2014 credentials are handled automatically in the execution environment. No API keys needed. This SDK is robust and battle-tested in production.
|
|
4965
|
-
|
|
4966
|
-
## What Remy apps are NOT good for
|
|
4967
|
-
|
|
4968
|
-
- Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
|
|
4969
|
-
- Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
|
|
4970
|
-
</platform_brief>`;
|
|
4971
|
-
}
|
|
4972
|
-
|
|
4973
5006
|
// src/subagents/copyEditor/tools.ts
|
|
4974
5007
|
var COPY_EDITOR_TOOLS = [...COMMON_READ_TOOLS];
|
|
4975
5008
|
|
|
@@ -5068,7 +5101,7 @@ async function executeDesignExpertTool(name, input, context, toolCallId, onLog)
|
|
|
5068
5101
|
}
|
|
5069
5102
|
|
|
5070
5103
|
// src/subagents/designExpert/data/sampleCache.ts
|
|
5071
|
-
import
|
|
5104
|
+
import fs17 from "fs";
|
|
5072
5105
|
var SAMPLE_FILE = ".remy-design-sample.json";
|
|
5073
5106
|
var cached2 = null;
|
|
5074
5107
|
function generateIndices(poolSize, sampleSize) {
|
|
@@ -5082,14 +5115,14 @@ function generateIndices(poolSize, sampleSize) {
|
|
|
5082
5115
|
}
|
|
5083
5116
|
function load() {
|
|
5084
5117
|
try {
|
|
5085
|
-
return JSON.parse(
|
|
5118
|
+
return JSON.parse(fs17.readFileSync(SAMPLE_FILE, "utf-8"));
|
|
5086
5119
|
} catch {
|
|
5087
5120
|
return null;
|
|
5088
5121
|
}
|
|
5089
5122
|
}
|
|
5090
5123
|
function save(indices) {
|
|
5091
5124
|
try {
|
|
5092
|
-
|
|
5125
|
+
fs17.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
|
|
5093
5126
|
} catch {
|
|
5094
5127
|
}
|
|
5095
5128
|
}
|
|
@@ -5451,7 +5484,7 @@ var VISION_TOOLS = [
|
|
|
5451
5484
|
];
|
|
5452
5485
|
|
|
5453
5486
|
// src/subagents/productVision/executor.ts
|
|
5454
|
-
import
|
|
5487
|
+
import fs18 from "fs";
|
|
5455
5488
|
import path10 from "path";
|
|
5456
5489
|
var ROADMAP_DIR = "src/roadmap";
|
|
5457
5490
|
var PITCH_DECK_SHELL = readAsset(
|
|
@@ -5466,13 +5499,13 @@ async function executeVisionTool(name, input, context) {
|
|
|
5466
5499
|
case "writeFile": {
|
|
5467
5500
|
const filePath = resolve(input.path);
|
|
5468
5501
|
try {
|
|
5469
|
-
|
|
5502
|
+
fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
|
|
5470
5503
|
let oldContent = null;
|
|
5471
5504
|
try {
|
|
5472
|
-
oldContent =
|
|
5505
|
+
oldContent = fs18.readFileSync(filePath, "utf-8");
|
|
5473
5506
|
} catch {
|
|
5474
5507
|
}
|
|
5475
|
-
|
|
5508
|
+
fs18.writeFileSync(filePath, input.content, "utf-8");
|
|
5476
5509
|
const lineCount = input.content.split("\n").length;
|
|
5477
5510
|
const label = oldContent !== null ? "Wrote" : "Created";
|
|
5478
5511
|
return `${label} ${filePath} (${lineCount} lines)
|
|
@@ -5484,11 +5517,11 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
|
|
|
5484
5517
|
case "deleteFile": {
|
|
5485
5518
|
const filePath = resolve(input.path);
|
|
5486
5519
|
try {
|
|
5487
|
-
if (!
|
|
5520
|
+
if (!fs18.existsSync(filePath)) {
|
|
5488
5521
|
return `Error: ${filePath} does not exist`;
|
|
5489
5522
|
}
|
|
5490
|
-
const oldContent =
|
|
5491
|
-
|
|
5523
|
+
const oldContent = fs18.readFileSync(filePath, "utf-8");
|
|
5524
|
+
fs18.unlinkSync(filePath);
|
|
5492
5525
|
return `Deleted ${filePath}
|
|
5493
5526
|
${unifiedDiff(filePath, oldContent, "")}`;
|
|
5494
5527
|
} catch (err) {
|
|
@@ -5501,9 +5534,9 @@ ${unifiedDiff(filePath, oldContent, "")}`;
|
|
|
5501
5534
|
}
|
|
5502
5535
|
const filePath = resolve("pitch.html");
|
|
5503
5536
|
try {
|
|
5504
|
-
|
|
5505
|
-
const exists =
|
|
5506
|
-
const before = exists ?
|
|
5537
|
+
fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
|
|
5538
|
+
const exists = fs18.existsSync(filePath);
|
|
5539
|
+
const before = exists ? fs18.statSync(filePath).mtimeMs : null;
|
|
5507
5540
|
const delivery = exists ? `### Your deliverable
|
|
5508
5541
|
The pitch deck already exists at \`${filePath}\`. Read it, then update it for the new <pitch_content>, keeping the presentation scaffolding intact \u2014 change only what needs to change.
|
|
5509
5542
|
|
|
@@ -5533,11 +5566,11 @@ Maintain the bones of the presentation scaffolding. Always keep the progress bar
|
|
|
5533
5566
|
${delivery}`;
|
|
5534
5567
|
const result = await runDesignExpertRender({ task }, context);
|
|
5535
5568
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
5536
|
-
if (!
|
|
5569
|
+
if (!fs18.existsSync(filePath)) {
|
|
5537
5570
|
return `Error: the design expert did not write ${filePath}. Its reply was:
|
|
5538
5571
|
${result.text}`;
|
|
5539
5572
|
}
|
|
5540
|
-
if (before !== null &&
|
|
5573
|
+
if (before !== null && fs18.statSync(filePath).mtimeMs === before) {
|
|
5541
5574
|
return `Error: the pitch deck at ${filePath} was not modified. The design expert's reply was:
|
|
5542
5575
|
${result.text}`;
|
|
5543
5576
|
}
|
|
@@ -5871,7 +5904,7 @@ var scrapeWebUrlTool = {
|
|
|
5871
5904
|
};
|
|
5872
5905
|
|
|
5873
5906
|
// src/tools/spec/writeBuildOverview.ts
|
|
5874
|
-
import
|
|
5907
|
+
import fs19 from "fs";
|
|
5875
5908
|
var OVERVIEW_FILE = "src/overview.html";
|
|
5876
5909
|
var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
|
|
5877
5910
|
|
|
@@ -5951,7 +5984,7 @@ var buildOverviewTool = {
|
|
|
5951
5984
|
if (!content) {
|
|
5952
5985
|
return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
|
|
5953
5986
|
}
|
|
5954
|
-
const exists =
|
|
5987
|
+
const exists = fs19.existsSync(OVERVIEW_FILE);
|
|
5955
5988
|
const task = `<overview_copy>${content}</overview_copy>
|
|
5956
5989
|
|
|
5957
5990
|
${DESIGN_BRIEF}
|
|
@@ -5969,7 +6002,7 @@ ${exists ? refreshDelivery() : initialDelivery()}`;
|
|
|
5969
6002
|
}
|
|
5970
6003
|
const result = await runDesignExpertRender({ task }, context);
|
|
5971
6004
|
context.subAgentMessages?.set(context.toolCallId, result.messages);
|
|
5972
|
-
if (!
|
|
6005
|
+
if (!fs19.existsSync(OVERVIEW_FILE)) {
|
|
5973
6006
|
return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
|
|
5974
6007
|
${result.text}`;
|
|
5975
6008
|
}
|
|
@@ -6393,7 +6426,7 @@ Write the summary of the conversation above, following your instructions.`;
|
|
|
6393
6426
|
}
|
|
6394
6427
|
|
|
6395
6428
|
// src/session.ts
|
|
6396
|
-
import
|
|
6429
|
+
import fs20 from "fs";
|
|
6397
6430
|
import path11 from "path";
|
|
6398
6431
|
var log9 = createLogger("session");
|
|
6399
6432
|
var SESSION_FILE = ".remy-session.json";
|
|
@@ -6412,7 +6445,7 @@ var ARCHIVE_MSG_CACHE_MAX = 3;
|
|
|
6412
6445
|
function loadSession(state) {
|
|
6413
6446
|
pruneArchives();
|
|
6414
6447
|
try {
|
|
6415
|
-
const raw =
|
|
6448
|
+
const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
|
|
6416
6449
|
const data = JSON.parse(raw);
|
|
6417
6450
|
if (data.models && typeof data.models === "object") {
|
|
6418
6451
|
state.models = data.models;
|
|
@@ -6485,19 +6518,19 @@ function buildPayload(state) {
|
|
|
6485
6518
|
return payload;
|
|
6486
6519
|
}
|
|
6487
6520
|
function archiveMessages(messages, label, models) {
|
|
6488
|
-
|
|
6521
|
+
fs20.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
6489
6522
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6490
6523
|
const count = messages.length;
|
|
6491
6524
|
let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.c${count}.json`);
|
|
6492
6525
|
let n = 1;
|
|
6493
|
-
while (
|
|
6526
|
+
while (fs20.existsSync(dest)) {
|
|
6494
6527
|
dest = path11.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.c${count}.json`);
|
|
6495
6528
|
}
|
|
6496
6529
|
const payload = { messages };
|
|
6497
6530
|
if (models && Object.keys(models).length > 0) {
|
|
6498
6531
|
payload.models = models;
|
|
6499
6532
|
}
|
|
6500
|
-
|
|
6533
|
+
fs20.writeFileSync(dest, JSON.stringify(payload), "utf-8");
|
|
6501
6534
|
archiveCountCache.set(path11.basename(dest), count);
|
|
6502
6535
|
log9.info("Session archived", { label, dest, messageCount: count });
|
|
6503
6536
|
pruneArchives();
|
|
@@ -6505,13 +6538,13 @@ function archiveMessages(messages, label, models) {
|
|
|
6505
6538
|
}
|
|
6506
6539
|
function pruneArchives() {
|
|
6507
6540
|
try {
|
|
6508
|
-
const entries =
|
|
6541
|
+
const entries = fs20.readdirSync(ARCHIVE_DIR).filter((name) => ARCHIVE_NAME_RE.test(name));
|
|
6509
6542
|
if (entries.length <= 1) {
|
|
6510
6543
|
return;
|
|
6511
6544
|
}
|
|
6512
6545
|
const archives = entries.map((name) => ({
|
|
6513
6546
|
name,
|
|
6514
|
-
size:
|
|
6547
|
+
size: fs20.statSync(path11.join(ARCHIVE_DIR, name)).size
|
|
6515
6548
|
})).sort(
|
|
6516
6549
|
(a, b) => archiveSortKey(b.name).localeCompare(archiveSortKey(a.name))
|
|
6517
6550
|
);
|
|
@@ -6529,7 +6562,7 @@ function pruneArchives() {
|
|
|
6529
6562
|
let freed = 0;
|
|
6530
6563
|
for (let i = cut; i < archives.length; i++) {
|
|
6531
6564
|
try {
|
|
6532
|
-
|
|
6565
|
+
fs20.unlinkSync(path11.join(ARCHIVE_DIR, archives[i].name));
|
|
6533
6566
|
freed += archives[i].size;
|
|
6534
6567
|
removed++;
|
|
6535
6568
|
} catch {
|
|
@@ -6553,7 +6586,7 @@ function parseArchive(name) {
|
|
|
6553
6586
|
return cached3;
|
|
6554
6587
|
}
|
|
6555
6588
|
try {
|
|
6556
|
-
const raw =
|
|
6589
|
+
const raw = fs20.readFileSync(path11.join(ARCHIVE_DIR, name), "utf-8");
|
|
6557
6590
|
const data = JSON.parse(raw);
|
|
6558
6591
|
const messages = Array.isArray(data?.messages) ? data.messages : [];
|
|
6559
6592
|
archiveCountCache.set(name, messages.length);
|
|
@@ -6591,7 +6624,7 @@ function readArchiveMessages(name) {
|
|
|
6591
6624
|
function listConversationArchives() {
|
|
6592
6625
|
let names;
|
|
6593
6626
|
try {
|
|
6594
|
-
names =
|
|
6627
|
+
names = fs20.readdirSync(ARCHIVE_DIR).filter((n) => ARCHIVE_NAME_RE.test(n));
|
|
6595
6628
|
} catch {
|
|
6596
6629
|
return { slots: [], archivedCount: 0 };
|
|
6597
6630
|
}
|
|
@@ -6702,7 +6735,7 @@ function saveSession(state) {
|
|
|
6702
6735
|
if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
|
|
6703
6736
|
serialized = JSON.stringify(buildPayload(state));
|
|
6704
6737
|
}
|
|
6705
|
-
|
|
6738
|
+
fs20.writeFileSync(SESSION_FILE, serialized, "utf-8");
|
|
6706
6739
|
log9.info("Session saved", { messageCount: state.messages.length });
|
|
6707
6740
|
} catch (err) {
|
|
6708
6741
|
log9.warn("Session save failed", { error: err.message });
|
|
@@ -6718,8 +6751,8 @@ function clearSession(state) {
|
|
|
6718
6751
|
}
|
|
6719
6752
|
state.messages = [];
|
|
6720
6753
|
try {
|
|
6721
|
-
if (
|
|
6722
|
-
|
|
6754
|
+
if (fs20.existsSync(SESSION_FILE)) {
|
|
6755
|
+
fs20.unlinkSync(SESSION_FILE);
|
|
6723
6756
|
}
|
|
6724
6757
|
} catch (err) {
|
|
6725
6758
|
log9.warn("Session clear: could not remove live file", {
|
|
@@ -6771,7 +6804,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
|
|
|
6771
6804
|
}
|
|
6772
6805
|
|
|
6773
6806
|
// src/brandExtraction/index.ts
|
|
6774
|
-
import
|
|
6807
|
+
import fs21 from "fs";
|
|
6775
6808
|
import path12 from "path";
|
|
6776
6809
|
import { createHash } from "crypto";
|
|
6777
6810
|
var log11 = createLogger("brandExtraction");
|
|
@@ -6795,13 +6828,16 @@ async function runExtraction(apiConfig, model) {
|
|
|
6795
6828
|
log11.info("Brand persisted", { inputHash });
|
|
6796
6829
|
return brand;
|
|
6797
6830
|
}
|
|
6798
|
-
function
|
|
6799
|
-
if (filePath
|
|
6831
|
+
function isDedicatedBrandFile(filePath) {
|
|
6832
|
+
if (filePath.split(path12.sep).includes("@brand")) {
|
|
6800
6833
|
return true;
|
|
6801
6834
|
}
|
|
6802
6835
|
const { type } = parseFrontmatter3(filePath);
|
|
6803
6836
|
return type.startsWith("design/color") || type.startsWith("design/typography");
|
|
6804
6837
|
}
|
|
6838
|
+
function isBrandRelevant(filePath) {
|
|
6839
|
+
return filePath === path12.join("src", "app.md") || isDedicatedBrandFile(filePath);
|
|
6840
|
+
}
|
|
6805
6841
|
function computeInputHash() {
|
|
6806
6842
|
const entries = [];
|
|
6807
6843
|
for (const filePath of walkMdFiles3("src")) {
|
|
@@ -6809,7 +6845,7 @@ function computeInputHash() {
|
|
|
6809
6845
|
entries.push({ path: filePath, content: readSafe(filePath) });
|
|
6810
6846
|
}
|
|
6811
6847
|
}
|
|
6812
|
-
const manifest =
|
|
6848
|
+
const manifest = readBrandManifest();
|
|
6813
6849
|
if (manifest) {
|
|
6814
6850
|
entries.push({ path: "mindstudio.json", content: manifest });
|
|
6815
6851
|
}
|
|
@@ -6822,7 +6858,25 @@ function sha256(input) {
|
|
|
6822
6858
|
}
|
|
6823
6859
|
function readSafe(filePath) {
|
|
6824
6860
|
try {
|
|
6825
|
-
return
|
|
6861
|
+
return fs21.readFileSync(filePath, "utf-8");
|
|
6862
|
+
} catch {
|
|
6863
|
+
return "";
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
function readBrandManifest() {
|
|
6867
|
+
const raw = readSafe("mindstudio.json");
|
|
6868
|
+
if (!raw) {
|
|
6869
|
+
return "";
|
|
6870
|
+
}
|
|
6871
|
+
try {
|
|
6872
|
+
const parsed = JSON.parse(raw);
|
|
6873
|
+
const projected = {};
|
|
6874
|
+
for (const key of ["name", "description", "iconUrl"]) {
|
|
6875
|
+
if (parsed[key] !== void 0) {
|
|
6876
|
+
projected[key] = parsed[key];
|
|
6877
|
+
}
|
|
6878
|
+
}
|
|
6879
|
+
return Object.keys(projected).length > 0 ? JSON.stringify(projected, null, 2) : "";
|
|
6826
6880
|
} catch {
|
|
6827
6881
|
return "";
|
|
6828
6882
|
}
|
|
@@ -6830,7 +6884,7 @@ function readSafe(filePath) {
|
|
|
6830
6884
|
function walkMdFiles3(dir) {
|
|
6831
6885
|
const results = [];
|
|
6832
6886
|
try {
|
|
6833
|
-
const entries =
|
|
6887
|
+
const entries = fs21.readdirSync(dir, { withFileTypes: true });
|
|
6834
6888
|
for (const entry of entries) {
|
|
6835
6889
|
const full = path12.join(dir, entry.name);
|
|
6836
6890
|
if (entry.isDirectory()) {
|
|
@@ -6845,7 +6899,7 @@ function walkMdFiles3(dir) {
|
|
|
6845
6899
|
}
|
|
6846
6900
|
function parseFrontmatter3(filePath) {
|
|
6847
6901
|
try {
|
|
6848
|
-
const content =
|
|
6902
|
+
const content = fs21.readFileSync(filePath, "utf-8");
|
|
6849
6903
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6850
6904
|
if (!match) {
|
|
6851
6905
|
return { type: "" };
|
|
@@ -6908,6 +6962,7 @@ async function extractBrand(apiConfig, model) {
|
|
|
6908
6962
|
}
|
|
6909
6963
|
return validateBrand(parsed);
|
|
6910
6964
|
}
|
|
6965
|
+
var HEAD_SLICE_CHARS = 2e3;
|
|
6911
6966
|
var BRAND_CORPUS_CHAR_LIMIT = 24e5;
|
|
6912
6967
|
function buildCorpus() {
|
|
6913
6968
|
const all = walkMdFiles3("src");
|
|
@@ -6916,14 +6971,17 @@ function buildCorpus() {
|
|
|
6916
6971
|
...all.filter((f) => !isBrandRelevant(f))
|
|
6917
6972
|
];
|
|
6918
6973
|
const files = [];
|
|
6919
|
-
const manifest =
|
|
6974
|
+
const manifest = readBrandManifest();
|
|
6920
6975
|
if (manifest) {
|
|
6921
6976
|
files.push({ path: "mindstudio.json", content: manifest });
|
|
6922
6977
|
}
|
|
6923
6978
|
for (const filePath of ordered) {
|
|
6924
6979
|
const content = readSafe(filePath);
|
|
6925
6980
|
if (content) {
|
|
6926
|
-
files.push({
|
|
6981
|
+
files.push({
|
|
6982
|
+
path: filePath,
|
|
6983
|
+
content: isDedicatedBrandFile(filePath) ? content : headSlice(content)
|
|
6984
|
+
});
|
|
6927
6985
|
}
|
|
6928
6986
|
}
|
|
6929
6987
|
const sep = "\n\n---\n\n";
|
|
@@ -6945,6 +7003,14 @@ ${content}`;
|
|
|
6945
7003
|
}
|
|
6946
7004
|
return sections.join(sep);
|
|
6947
7005
|
}
|
|
7006
|
+
function headSlice(content) {
|
|
7007
|
+
if (content.length <= HEAD_SLICE_CHARS) {
|
|
7008
|
+
return content;
|
|
7009
|
+
}
|
|
7010
|
+
return content.slice(0, HEAD_SLICE_CHARS) + `
|
|
7011
|
+
|
|
7012
|
+
(head slice of a ${(content.length / 1024).toFixed(0)}KB spec \u2014 not a dedicated brand file, so only its opening is included)`;
|
|
7013
|
+
}
|
|
6948
7014
|
function parseJsonResponse(text) {
|
|
6949
7015
|
const trimmed = text.trim();
|
|
6950
7016
|
const fenceMatch = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/);
|
|
@@ -7043,14 +7109,14 @@ function pickFont(raw) {
|
|
|
7043
7109
|
}
|
|
7044
7110
|
function persistBrand(brand, inputHash) {
|
|
7045
7111
|
const tmp = `${BRAND_FILE}.tmp`;
|
|
7046
|
-
|
|
7047
|
-
|
|
7112
|
+
fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
|
|
7113
|
+
fs21.renameSync(tmp, BRAND_FILE);
|
|
7048
7114
|
const cache = { inputHash, generatedAt: Date.now() };
|
|
7049
|
-
|
|
7115
|
+
fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
7050
7116
|
}
|
|
7051
7117
|
function readCache() {
|
|
7052
7118
|
try {
|
|
7053
|
-
const raw =
|
|
7119
|
+
const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
|
|
7054
7120
|
const parsed = JSON.parse(raw);
|
|
7055
7121
|
if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
|
|
7056
7122
|
return parsed;
|