@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 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 sliced = allLines.slice(startIdx, endIdxExclusive);
2076
- const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
2077
- let result = numbered;
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 + sliced.length;
2080
- if (displayStart > 1 || displayEnd < totalLines) {
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 isBrandRelevant(filePath) {
6804
- if (filePath === path12.join("src", "app.md")) {
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 = readSafe("mindstudio.json");
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 = readSafe("mindstudio.json");
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({ path: filePath, content });
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 sliced = allLines.slice(startIdx, endIdxExclusive);
2825
- const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
2826
- let result = numbered;
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 + sliced.length;
2829
- if (displayStart > 1 || displayEnd < totalLines) {
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 isBrandRelevant(filePath) {
7449
- if (filePath === path10.join("src", "app.md")) {
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 = readSafe("mindstudio.json");
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 = readSafe("mindstudio.json");
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({ path: filePath, content });
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.251",
3
+ "version": "0.1.252",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",