@mindstudio-ai/remy 0.1.251 → 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 +72 -11
- package/dist/index.js +74 -13
- 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)`;
|
|
@@ -6800,13 +6828,16 @@ async function runExtraction(apiConfig, model) {
|
|
|
6800
6828
|
log11.info("Brand persisted", { inputHash });
|
|
6801
6829
|
return brand;
|
|
6802
6830
|
}
|
|
6803
|
-
function
|
|
6804
|
-
if (filePath
|
|
6831
|
+
function isDedicatedBrandFile(filePath) {
|
|
6832
|
+
if (filePath.split(path12.sep).includes("@brand")) {
|
|
6805
6833
|
return true;
|
|
6806
6834
|
}
|
|
6807
6835
|
const { type } = parseFrontmatter3(filePath);
|
|
6808
6836
|
return type.startsWith("design/color") || type.startsWith("design/typography");
|
|
6809
6837
|
}
|
|
6838
|
+
function isBrandRelevant(filePath) {
|
|
6839
|
+
return filePath === path12.join("src", "app.md") || isDedicatedBrandFile(filePath);
|
|
6840
|
+
}
|
|
6810
6841
|
function computeInputHash() {
|
|
6811
6842
|
const entries = [];
|
|
6812
6843
|
for (const filePath of walkMdFiles3("src")) {
|
|
@@ -6814,7 +6845,7 @@ function computeInputHash() {
|
|
|
6814
6845
|
entries.push({ path: filePath, content: readSafe(filePath) });
|
|
6815
6846
|
}
|
|
6816
6847
|
}
|
|
6817
|
-
const manifest =
|
|
6848
|
+
const manifest = readBrandManifest();
|
|
6818
6849
|
if (manifest) {
|
|
6819
6850
|
entries.push({ path: "mindstudio.json", content: manifest });
|
|
6820
6851
|
}
|
|
@@ -6832,6 +6863,24 @@ function readSafe(filePath) {
|
|
|
6832
6863
|
return "";
|
|
6833
6864
|
}
|
|
6834
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) : "";
|
|
6880
|
+
} catch {
|
|
6881
|
+
return "";
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6835
6884
|
function walkMdFiles3(dir) {
|
|
6836
6885
|
const results = [];
|
|
6837
6886
|
try {
|
|
@@ -6913,6 +6962,7 @@ async function extractBrand(apiConfig, model) {
|
|
|
6913
6962
|
}
|
|
6914
6963
|
return validateBrand(parsed);
|
|
6915
6964
|
}
|
|
6965
|
+
var HEAD_SLICE_CHARS = 2e3;
|
|
6916
6966
|
var BRAND_CORPUS_CHAR_LIMIT = 24e5;
|
|
6917
6967
|
function buildCorpus() {
|
|
6918
6968
|
const all = walkMdFiles3("src");
|
|
@@ -6921,14 +6971,17 @@ function buildCorpus() {
|
|
|
6921
6971
|
...all.filter((f) => !isBrandRelevant(f))
|
|
6922
6972
|
];
|
|
6923
6973
|
const files = [];
|
|
6924
|
-
const manifest =
|
|
6974
|
+
const manifest = readBrandManifest();
|
|
6925
6975
|
if (manifest) {
|
|
6926
6976
|
files.push({ path: "mindstudio.json", content: manifest });
|
|
6927
6977
|
}
|
|
6928
6978
|
for (const filePath of ordered) {
|
|
6929
6979
|
const content = readSafe(filePath);
|
|
6930
6980
|
if (content) {
|
|
6931
|
-
files.push({
|
|
6981
|
+
files.push({
|
|
6982
|
+
path: filePath,
|
|
6983
|
+
content: isDedicatedBrandFile(filePath) ? content : headSlice(content)
|
|
6984
|
+
});
|
|
6932
6985
|
}
|
|
6933
6986
|
}
|
|
6934
6987
|
const sep = "\n\n---\n\n";
|
|
@@ -6950,6 +7003,14 @@ ${content}`;
|
|
|
6950
7003
|
}
|
|
6951
7004
|
return sections.join(sep);
|
|
6952
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
|
+
}
|
|
6953
7014
|
function parseJsonResponse(text) {
|
|
6954
7015
|
const trimmed = text.trim();
|
|
6955
7016
|
const fenceMatch = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/);
|
package/dist/index.js
CHANGED
|
@@ -2755,16 +2755,17 @@ function isBinary(buffer) {
|
|
|
2755
2755
|
}
|
|
2756
2756
|
return false;
|
|
2757
2757
|
}
|
|
2758
|
-
var DEFAULT_WINDOW, readFileTool;
|
|
2758
|
+
var DEFAULT_WINDOW, MAX_BYTES, readFileTool;
|
|
2759
2759
|
var init_readFile = __esm({
|
|
2760
2760
|
"src/tools/code/readFile.ts"() {
|
|
2761
2761
|
"use strict";
|
|
2762
2762
|
DEFAULT_WINDOW = 500;
|
|
2763
|
+
MAX_BYTES = 64 * 1024;
|
|
2763
2764
|
readFileTool = {
|
|
2764
2765
|
clearable: true,
|
|
2765
2766
|
definition: {
|
|
2766
2767
|
name: "readFile",
|
|
2767
|
-
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.",
|
|
2768
|
+
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.",
|
|
2768
2769
|
inputSchema: {
|
|
2769
2770
|
type: "object",
|
|
2770
2771
|
properties: {
|
|
@@ -2821,12 +2822,35 @@ var init_readFile = __esm({
|
|
|
2821
2822
|
endIdxExclusive = Math.min(startIdx + DEFAULT_WINDOW, totalLines);
|
|
2822
2823
|
}
|
|
2823
2824
|
}
|
|
2824
|
-
const
|
|
2825
|
-
|
|
2826
|
-
let
|
|
2825
|
+
const numberedLines = allLines.slice(startIdx, endIdxExclusive).map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`);
|
|
2826
|
+
let kept = numberedLines;
|
|
2827
|
+
let byteTruncated = false;
|
|
2828
|
+
if (Buffer.byteLength(numberedLines.join("\n"), "utf-8") > MAX_BYTES) {
|
|
2829
|
+
byteTruncated = true;
|
|
2830
|
+
kept = [];
|
|
2831
|
+
let used = 0;
|
|
2832
|
+
for (const line of numberedLines) {
|
|
2833
|
+
const cost = Buffer.byteLength(line, "utf-8") + (kept.length > 0 ? 1 : 0);
|
|
2834
|
+
if (used + cost > MAX_BYTES) {
|
|
2835
|
+
break;
|
|
2836
|
+
}
|
|
2837
|
+
kept.push(line);
|
|
2838
|
+
used += cost;
|
|
2839
|
+
}
|
|
2840
|
+
if (kept.length === 0) {
|
|
2841
|
+
kept = [
|
|
2842
|
+
Buffer.from(numberedLines[0], "utf-8").subarray(0, MAX_BYTES).toString("utf-8")
|
|
2843
|
+
];
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
let result = kept.join("\n");
|
|
2827
2847
|
const displayStart = startIdx + 1;
|
|
2828
|
-
const displayEnd = startIdx +
|
|
2829
|
-
if (
|
|
2848
|
+
const displayEnd = startIdx + kept.length;
|
|
2849
|
+
if (byteTruncated) {
|
|
2850
|
+
result += `
|
|
2851
|
+
|
|
2852
|
+
(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.)`;
|
|
2853
|
+
} else if (displayStart > 1 || displayEnd < totalLines) {
|
|
2830
2854
|
result += `
|
|
2831
2855
|
|
|
2832
2856
|
(showing lines ${displayStart}\u2013${displayEnd} of ${totalLines} \u2014 pass startLine/endLine to read a different range)`;
|
|
@@ -7445,13 +7469,16 @@ async function runExtraction(apiConfig, model) {
|
|
|
7445
7469
|
log10.info("Brand persisted", { inputHash });
|
|
7446
7470
|
return brand;
|
|
7447
7471
|
}
|
|
7448
|
-
function
|
|
7449
|
-
if (filePath
|
|
7472
|
+
function isDedicatedBrandFile(filePath) {
|
|
7473
|
+
if (filePath.split(path10.sep).includes("@brand")) {
|
|
7450
7474
|
return true;
|
|
7451
7475
|
}
|
|
7452
7476
|
const { type } = parseFrontmatter2(filePath);
|
|
7453
7477
|
return type.startsWith("design/color") || type.startsWith("design/typography");
|
|
7454
7478
|
}
|
|
7479
|
+
function isBrandRelevant(filePath) {
|
|
7480
|
+
return filePath === path10.join("src", "app.md") || isDedicatedBrandFile(filePath);
|
|
7481
|
+
}
|
|
7455
7482
|
function computeInputHash() {
|
|
7456
7483
|
const entries = [];
|
|
7457
7484
|
for (const filePath of walkMdFiles2("src")) {
|
|
@@ -7459,7 +7486,7 @@ function computeInputHash() {
|
|
|
7459
7486
|
entries.push({ path: filePath, content: readSafe(filePath) });
|
|
7460
7487
|
}
|
|
7461
7488
|
}
|
|
7462
|
-
const manifest =
|
|
7489
|
+
const manifest = readBrandManifest();
|
|
7463
7490
|
if (manifest) {
|
|
7464
7491
|
entries.push({ path: "mindstudio.json", content: manifest });
|
|
7465
7492
|
}
|
|
@@ -7477,6 +7504,24 @@ function readSafe(filePath) {
|
|
|
7477
7504
|
return "";
|
|
7478
7505
|
}
|
|
7479
7506
|
}
|
|
7507
|
+
function readBrandManifest() {
|
|
7508
|
+
const raw = readSafe("mindstudio.json");
|
|
7509
|
+
if (!raw) {
|
|
7510
|
+
return "";
|
|
7511
|
+
}
|
|
7512
|
+
try {
|
|
7513
|
+
const parsed = JSON.parse(raw);
|
|
7514
|
+
const projected = {};
|
|
7515
|
+
for (const key of ["name", "description", "iconUrl"]) {
|
|
7516
|
+
if (parsed[key] !== void 0) {
|
|
7517
|
+
projected[key] = parsed[key];
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
7520
|
+
return Object.keys(projected).length > 0 ? JSON.stringify(projected, null, 2) : "";
|
|
7521
|
+
} catch {
|
|
7522
|
+
return "";
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7480
7525
|
function walkMdFiles2(dir) {
|
|
7481
7526
|
const results = [];
|
|
7482
7527
|
try {
|
|
@@ -7565,14 +7610,17 @@ function buildCorpus() {
|
|
|
7565
7610
|
...all.filter((f) => !isBrandRelevant(f))
|
|
7566
7611
|
];
|
|
7567
7612
|
const files = [];
|
|
7568
|
-
const manifest =
|
|
7613
|
+
const manifest = readBrandManifest();
|
|
7569
7614
|
if (manifest) {
|
|
7570
7615
|
files.push({ path: "mindstudio.json", content: manifest });
|
|
7571
7616
|
}
|
|
7572
7617
|
for (const filePath of ordered) {
|
|
7573
7618
|
const content = readSafe(filePath);
|
|
7574
7619
|
if (content) {
|
|
7575
|
-
files.push({
|
|
7620
|
+
files.push({
|
|
7621
|
+
path: filePath,
|
|
7622
|
+
content: isDedicatedBrandFile(filePath) ? content : headSlice(content)
|
|
7623
|
+
});
|
|
7576
7624
|
}
|
|
7577
7625
|
}
|
|
7578
7626
|
const sep = "\n\n---\n\n";
|
|
@@ -7594,6 +7642,14 @@ ${content}`;
|
|
|
7594
7642
|
}
|
|
7595
7643
|
return sections.join(sep);
|
|
7596
7644
|
}
|
|
7645
|
+
function headSlice(content) {
|
|
7646
|
+
if (content.length <= HEAD_SLICE_CHARS) {
|
|
7647
|
+
return content;
|
|
7648
|
+
}
|
|
7649
|
+
return content.slice(0, HEAD_SLICE_CHARS) + `
|
|
7650
|
+
|
|
7651
|
+
(head slice of a ${(content.length / 1024).toFixed(0)}KB spec \u2014 not a dedicated brand file, so only its opening is included)`;
|
|
7652
|
+
}
|
|
7597
7653
|
function parseJsonResponse(text) {
|
|
7598
7654
|
const trimmed = text.trim();
|
|
7599
7655
|
const fenceMatch = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/);
|
|
@@ -7709,7 +7765,7 @@ function readCache() {
|
|
|
7709
7765
|
return null;
|
|
7710
7766
|
}
|
|
7711
7767
|
}
|
|
7712
|
-
var log10, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE, BRAND_CORPUS_CHAR_LIMIT;
|
|
7768
|
+
var log10, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE, HEAD_SLICE_CHARS, BRAND_CORPUS_CHAR_LIMIT;
|
|
7713
7769
|
var init_brandExtraction = __esm({
|
|
7714
7770
|
"src/brandExtraction/index.ts"() {
|
|
7715
7771
|
"use strict";
|
|
@@ -7721,6 +7777,7 @@ var init_brandExtraction = __esm({
|
|
|
7721
7777
|
EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
|
|
7722
7778
|
BRAND_FILE = ".remy-brand.json";
|
|
7723
7779
|
CACHE_FILE = ".remy-brand.cache.json";
|
|
7780
|
+
HEAD_SLICE_CHARS = 2e3;
|
|
7724
7781
|
BRAND_CORPUS_CHAR_LIMIT = 24e5;
|
|
7725
7782
|
}
|
|
7726
7783
|
});
|
|
@@ -8600,6 +8657,10 @@ Current date: ${now}
|
|
|
8600
8657
|
{{compiled/media-cdn.md}}
|
|
8601
8658
|
</media_cdn>
|
|
8602
8659
|
|
|
8660
|
+
<app_files>
|
|
8661
|
+
{{compiled/files.md}}
|
|
8662
|
+
</app_files>
|
|
8663
|
+
|
|
8603
8664
|
<interfaces>
|
|
8604
8665
|
{{compiled/interfaces.md}}
|
|
8605
8666
|
</interfaces>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Files & Storage
|
|
2
|
+
|
|
3
|
+
Per-app blob storage — the twin of `db` (`db` stores rows; `files` stores files: user uploads,
|
|
4
|
+
generated documents, images, marketing assets). **Private by default.** Files serve on the app's own
|
|
5
|
+
domain.
|
|
6
|
+
|
|
7
|
+
**File stores are always live — there is no dev copy.** Every `put`/`delete`/overwrite hits
|
|
8
|
+
production storage immediately and irreversibly. And unlike the database, **scenarios never reset file
|
|
9
|
+
stores** — a scenario truncates DB tables but leaves files untouched, so files are not a "clean slate"
|
|
10
|
+
you can re-seed, and orphaned files accumulate across runs. Delete deliberately.
|
|
11
|
+
|
|
12
|
+
## Defining a store
|
|
13
|
+
|
|
14
|
+
Like `db.defineTable`, define at module scope and import into methods. Access is pinned at define
|
|
15
|
+
time.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { files } from '@mindstudio-ai/agent';
|
|
19
|
+
|
|
20
|
+
export const Uploads = files.defineStore('uploads'); // private (default)
|
|
21
|
+
export const Assets = files.defineStore('assets', { access: 'public' }); // world-readable + CDN
|
|
22
|
+
// optional upload policy: files.defineStore('uploads', { maxSize, contentTypes })
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Store names: lowercase `[a-z0-9_-]`, ≤ 64 chars. Keys are paths within the store (`reports/q1.pdf`).
|
|
26
|
+
|
|
27
|
+
## Backend (`@mindstudio-ai/agent`)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Uploads } from './files/uploads';
|
|
31
|
+
|
|
32
|
+
// Store bytes the backend produced; hand file.url to the frontend.
|
|
33
|
+
const file = await Reports.put(pdfBuffer, { contentType: 'application/pdf', filename: 'q1.pdf' });
|
|
34
|
+
file.url; // stable, on-domain URL for <img>/<a>/fetch (private → app-session-authed)
|
|
35
|
+
|
|
36
|
+
const bytes = await Uploads.get(key); // Buffer — backend-side read (parse it, feed a model)
|
|
37
|
+
await Uploads.head(key); // metadata; .exists(key) → boolean
|
|
38
|
+
const { files, cursor } = await Uploads.list({ prefix: 'reports/', limit: 100 });
|
|
39
|
+
await Uploads.delete(key);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `put(content, { key?, contentType?, filename?, contentAddressed? })` → `StoredFile`
|
|
43
|
+
(`{ key, url, size?, contentType?, updatedAt?, shareUrl() }`). Omit `key` → UUID;
|
|
44
|
+
`contentAddressed: true` → a `<sha256>.<ext>` key (immutable/idempotent, for baked-in public assets).
|
|
45
|
+
- **`file.url`** is a plain relative string — don't await it. To display a user *their own* file, hand
|
|
46
|
+
them `file.url`; don't `get()` the bytes and stream them yourself.
|
|
47
|
+
- **`await file.shareUrl({ expiresIn })`** → an absolute signed link that works with **no session**
|
|
48
|
+
(email / cross-site embed). Private stores only; default 24h.
|
|
49
|
+
|
|
50
|
+
## User uploads (client-direct — bytes never go through the backend)
|
|
51
|
+
|
|
52
|
+
Backend mints a token; the browser uploads straight to storage:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// backend method
|
|
56
|
+
export async function getUploadSlot(input: { filename: string; contentType: string }) {
|
|
57
|
+
return Uploads.createUploadToken({ contentType: input.contentType, maxSize: 25 * 1024 * 1024 });
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
```typescript
|
|
61
|
+
// frontend (@mindstudio-ai/interface)
|
|
62
|
+
import { createClient, platform } from '@mindstudio-ai/interface';
|
|
63
|
+
const api = createClient();
|
|
64
|
+
const token = await api.getUploadSlot({ filename: file.name, contentType: file.type });
|
|
65
|
+
const { key, url } = await platform.upload(token, file, { onProgress: (f) => setProgress(f) });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Public assets + image resizing
|
|
69
|
+
|
|
70
|
+
Public files are world-readable, on the app's domain, and **images resize via query params**
|
|
71
|
+
(`?w=&h=&fit=&crop=&fm=&dpr=&q=&blur=&sharpen=` — same vocabulary as the image CDN; set `dpr=2/3` for
|
|
72
|
+
retina). Request the size you need rather than CSS-scaling a full-res original.
|
|
73
|
+
|
|
74
|
+
Lightweight-config pattern: a public store + a stable `key` is a file the frontend can `fetch` with no
|
|
75
|
+
DB hit and the backend can overwrite (`Config.put(json, { key: 'config/latest.json' })`).
|
|
76
|
+
|
|
77
|
+
## Build-time / marketing assets
|
|
78
|
+
|
|
79
|
+
Need an image on the site (hero, logo, OG image)? **Never commit binaries to the repo** — it bloats
|
|
80
|
+
git. Upload once and embed the returned URL:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
mindstudio-prod files put --public ./hero.jpg # → { url, key } — content-addressed, immutable
|
|
84
|
+
```
|
|
85
|
+
Write that URL into your JSX/HTML. Also: `files list`, `files rm --store … --key …` (`--help` for flags).
|
|
86
|
+
|
|
87
|
+
## When public vs private
|
|
88
|
+
|
|
89
|
+
- **Private (default):** user uploads, generated docs, anything not world-readable. Reads are authed
|
|
90
|
+
(the app session) or a short-lived `shareUrl`.
|
|
91
|
+
- **Public:** marketing images, resizable media, config the frontend reads. Deliberate `access:
|
|
92
|
+
'public'`.
|
|
93
|
+
|
|
94
|
+
Per-user access is the app's job — key files per user (`{userId}/…`) and hand each user only their own
|
|
95
|
+
URLs; the platform authorizes at the app level, not per file.
|
|
@@ -88,6 +88,8 @@ When a plan includes multiple screens/API calls, always note this item for the d
|
|
|
88
88
|
|
|
89
89
|
- **Hardcoded credentials.** If the plan or code contains API keys, tokens, or connection strings inline, flag it — these should be `process.env` secrets managed via the dashboard. Also flag if the plan uses `process.env` for something the MindStudio SDK already handles (AI model keys, email/SMS sending, etc.).
|
|
90
90
|
|
|
91
|
+
- **Don't use `agent.uploadFile()` from @mindstudio-ai/agent SDK** — that's the legacy v1 public CDN. Use `files`.
|
|
92
|
+
|
|
91
93
|
### Other things to note
|
|
92
94
|
|
|
93
95
|
If you get a whiff of any of the following, make a note for the developer:
|