@mindstudio-ai/remy 0.1.158 → 0.1.159

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.
@@ -6,9 +6,9 @@
6
6
  This is the code generation phase. The spec is written. Build everything now in three phases: planning, coding, and verifying. Execute each phase in order in a single turn.
7
7
 
8
8
  ## Planning
9
- Think about your approach and then get a quick sanity check from `codeSanityCheck` to make sure you aren't missing anything.
9
+ Get a quick architecture sanity check from `codeSanityCheck` to make sure your approach holds up.
10
10
 
11
- If you are building a web frontend, consult `visualDesignExpert` for guidance and ideas on specific component design, UI patterns, and interactions - it has access to a deep repository of design inspiration and will be able to give you great ideas to work with while building. Don't ask it to design full screens - focus on specific components, moments, and concepts where its ideas can be additive and transformative, you already have the basic design and layout guidance from the spec.
11
+ Then bring in `visualDesignExpert` before writing any frontend code. Walk it through the screens and the key interactions the moments where the user does something or sees something land — and ask for direction on the specifics that make a real product feel alive: motion, micro-interactions, hover and focus states, empty and loading states, the components and moments that need extra texture. The spec defines the brand and the rough layout. The designer fills in the texture between them, and that texture is what separates an app that lands from one that feels generic. This pass is the highest-leverage thing you do before writing code.
12
12
 
13
13
  Use your remy-notes.md file to make a checklist of the work that needs to be done. Don't store implementation details in it - it is soley for keeping track of tasks.
14
14
 
@@ -0,0 +1,51 @@
1
+ You extract a structured `AppBrand` JSON object from the spec files of a MindStudio app project.
2
+
3
+ Your output is read by a frontend renderer that uses the brand to style internal documents (implementation plans, sync plans, publish plans) with a "letterhead" treatment — a wordmark, accent color, paper-tone background, and branded fonts. Every field is optional. The renderer falls back to generic styling when a field is missing or invalid.
4
+
5
+ ## Output format
6
+
7
+ Reply with **exactly one** fenced ```json block. No prose before or after.
8
+
9
+ The object must match this shape:
10
+
11
+ ```ts
12
+ type AppBrand = {
13
+ version: 1;
14
+ name?: string;
15
+ tagline?: string;
16
+ logoUrl?: string;
17
+ colors?: {
18
+ background?: string;
19
+ text?: string;
20
+ heading?: string;
21
+ accent?: string;
22
+ muted?: string;
23
+ };
24
+ typography?: {
25
+ body?: { family: string; stylesheet?: string; fileUrl?: string };
26
+ heading?: { family: string; stylesheet?: string; fileUrl?: string };
27
+ };
28
+ };
29
+ ```
30
+
31
+ Hex (`#RRGGBB`, `#RGB`) is preferred for colors; any valid CSS color string is acceptable.
32
+
33
+ ## Rules
34
+
35
+ - **Omit any field you can't extract confidently.** Partial output is correct. Do not invent.
36
+ - **`version` is always `1`.**
37
+ - **`name`**: the wordmark text — the app's display name as it would appear at the top of a document. Pull from the manifest, the main spec, or wherever the app's name is stated. Do NOT guess from the file paths.
38
+ - **`tagline`**: a short subtitle if the spec has one. One sentence or a short phrase. Omit if there isn't one — do not invent a tagline from the description.
39
+ - **`logoUrl`**: the app's logo image URL if the spec has one. Omit if there isn't one.
40
+ - **`colors.background`** is **paper**, not brand. It tints the page behind body text. If the brand only defines saturated/vivid colors and no calm surface tone, **omit this field** so the renderer falls back to neutral. A 3-5% saturation tint of a brand color is acceptable; a fully saturated brand color is not.
41
+ - **`colors.text`** is body text — usually near-black or near-white depending on background. **`colors.heading`** is often the same as text. **`colors.accent`** is for links and small flair. **`colors.muted`** is for captions and secondary text — omit if the spec doesn't define it; the renderer derives it from `text` automatically.
42
+ - **`typography.body`**: applied to paragraphs, lists, tables. **Body type must stay readable.** If the brand font is decorative, display-only, or script (e.g., a hand-lettered logo font), **omit `body`** and only set `heading`.
43
+ - **`typography.heading`**: applied to h1-h6 only. Decorative fonts are fine here.
44
+ - **Do NOT include a `mono` field.** Code blocks stay generic mono for legibility.
45
+ - For `BrandFont.stylesheet` and `BrandFont.fileUrl`: only emit a URL that **appears verbatim in the spec content** you were given. Do not fabricate Google Fonts URLs from a family name. If neither URL is present in the spec, omit both — the renderer assumes the family is a system font or already loaded.
46
+
47
+ If the project has no spec files yet, or the spec contains no brand information, output:
48
+
49
+ ```json
50
+ { "version": 1 }
51
+ ```
package/dist/headless.js CHANGED
@@ -2695,11 +2695,11 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2695
2695
  let prompt;
2696
2696
  let existingUrl;
2697
2697
  let onLog;
2698
- let path10;
2698
+ let path11;
2699
2699
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2700
2700
  prompt = promptOrOptions.prompt;
2701
2701
  existingUrl = promptOrOptions.imageUrl;
2702
- path10 = promptOrOptions.path;
2702
+ path11 = promptOrOptions.path;
2703
2703
  onLog = promptOrOptions.onLog;
2704
2704
  } else {
2705
2705
  prompt = promptOrOptions;
@@ -2711,7 +2711,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2711
2711
  } else {
2712
2712
  const ssResult = await sidecarRequest(
2713
2713
  "/screenshot-full-page",
2714
- path10 ? { path: path10 } : void 0,
2714
+ path11 ? { path: path11 } : void 0,
2715
2715
  { timeout: 12e4 }
2716
2716
  );
2717
2717
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -2748,15 +2748,15 @@ function acquireBrowserLock() {
2748
2748
  // src/statusWatcher.ts
2749
2749
  function startStatusWatcher(config) {
2750
2750
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
2751
- let inflight = false;
2751
+ let inflight2 = false;
2752
2752
  let stopped = false;
2753
2753
  let pauseCount = 0;
2754
2754
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
2755
2755
  async function tick() {
2756
- if (stopped || signal?.aborted || inflight || pauseCount > 0) {
2756
+ if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
2757
2757
  return;
2758
2758
  }
2759
- inflight = true;
2759
+ inflight2 = true;
2760
2760
  try {
2761
2761
  const context = getContext();
2762
2762
  if (!context) {
@@ -2784,7 +2784,7 @@ function startStatusWatcher(config) {
2784
2784
  onStatus(data.label);
2785
2785
  } catch {
2786
2786
  } finally {
2787
- inflight = false;
2787
+ inflight2 = false;
2788
2788
  }
2789
2789
  }
2790
2790
  const timer = setInterval(tick, interval);
@@ -4497,15 +4497,15 @@ function getSampleIndices(pools, sizes) {
4497
4497
  }
4498
4498
  const loaded = load();
4499
4499
  if (loaded) {
4500
- let dirty = false;
4500
+ let dirty2 = false;
4501
4501
  for (const key of ["uiInspiration", "designReferences", "fonts"]) {
4502
4502
  const before = loaded[key].length;
4503
4503
  loaded[key] = loaded[key].filter((i) => i < pools[key]);
4504
4504
  if (loaded[key].length < before) {
4505
- dirty = true;
4505
+ dirty2 = true;
4506
4506
  }
4507
4507
  }
4508
- if (dirty) {
4508
+ if (dirty2) {
4509
4509
  save(loaded);
4510
4510
  }
4511
4511
  cached = loaded;
@@ -5199,17 +5199,292 @@ function triggerCompaction(state, apiConfig, callbacks) {
5199
5199
  });
5200
5200
  }
5201
5201
 
5202
- // src/session.ts
5202
+ // src/brandExtraction/index.ts
5203
5203
  import fs19 from "fs";
5204
- var log8 = createLogger("session");
5204
+ import path10 from "path";
5205
+ import { createHash } from "crypto";
5206
+ var log8 = createLogger("brandExtraction");
5207
+ var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
5208
+ var BRAND_FILE = ".remy-brand.json";
5209
+ var CACHE_FILE = ".remy-brand.cache.json";
5210
+ async function runExtraction(apiConfig) {
5211
+ const inputHash = computeInputHash();
5212
+ const cached2 = readCache();
5213
+ if (cached2 && cached2.inputHash === inputHash) {
5214
+ log8.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
5215
+ return null;
5216
+ }
5217
+ log8.info("Extracting brand", { inputHash });
5218
+ const brand = await extractBrand(apiConfig);
5219
+ if (!brand) {
5220
+ log8.warn("Brand extraction failed \u2014 leaving cache untouched");
5221
+ return null;
5222
+ }
5223
+ persistBrand(brand, inputHash);
5224
+ log8.info("Brand persisted", { inputHash });
5225
+ return brand;
5226
+ }
5227
+ function computeInputHash() {
5228
+ const entries = [];
5229
+ for (const filePath of walkMdFiles3("src")) {
5230
+ if (filePath === path10.join("src", "app.md")) {
5231
+ entries.push({ path: filePath, content: readSafe(filePath) });
5232
+ continue;
5233
+ }
5234
+ const fm = parseFrontmatter3(filePath);
5235
+ if (fm.type.startsWith("design/color") || fm.type.startsWith("design/typography")) {
5236
+ entries.push({ path: filePath, content: readSafe(filePath) });
5237
+ }
5238
+ }
5239
+ const manifest = readSafe("mindstudio.json");
5240
+ if (manifest) {
5241
+ entries.push({ path: "mindstudio.json", content: manifest });
5242
+ }
5243
+ entries.sort((a, b) => a.path.localeCompare(b.path));
5244
+ const fingerprint = entries.map((e) => `${e.path}:${sha256(e.content)}`).join("\n");
5245
+ return sha256(fingerprint);
5246
+ }
5247
+ function sha256(input) {
5248
+ return createHash("sha256").update(input).digest("hex");
5249
+ }
5250
+ function readSafe(filePath) {
5251
+ try {
5252
+ return fs19.readFileSync(filePath, "utf-8");
5253
+ } catch {
5254
+ return "";
5255
+ }
5256
+ }
5257
+ function walkMdFiles3(dir) {
5258
+ const results = [];
5259
+ try {
5260
+ const entries = fs19.readdirSync(dir, { withFileTypes: true });
5261
+ for (const entry of entries) {
5262
+ const full = path10.join(dir, entry.name);
5263
+ if (entry.isDirectory()) {
5264
+ results.push(...walkMdFiles3(full));
5265
+ } else if (entry.name.endsWith(".md")) {
5266
+ results.push(full);
5267
+ }
5268
+ }
5269
+ } catch {
5270
+ }
5271
+ return results.sort();
5272
+ }
5273
+ function parseFrontmatter3(filePath) {
5274
+ try {
5275
+ const content = fs19.readFileSync(filePath, "utf-8");
5276
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
5277
+ if (!match) {
5278
+ return { type: "" };
5279
+ }
5280
+ const fm = match[1];
5281
+ const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
5282
+ return { type };
5283
+ } catch {
5284
+ return { type: "" };
5285
+ }
5286
+ }
5287
+ async function extractBrand(apiConfig) {
5288
+ const corpus = buildCorpus();
5289
+ if (!corpus.trim()) {
5290
+ log8.debug("No spec corpus \u2014 emitting empty brand");
5291
+ return { version: 1 };
5292
+ }
5293
+ let responseText = "";
5294
+ try {
5295
+ for await (const event of streamChat({
5296
+ ...apiConfig,
5297
+ subAgentId: "brandExtractor",
5298
+ system: EXTRACT_PROMPT,
5299
+ messages: [{ role: "user", content: corpus }],
5300
+ tools: []
5301
+ })) {
5302
+ if (event.type === "text") {
5303
+ responseText += event.text;
5304
+ } else if (event.type === "error") {
5305
+ log8.error("Brand extraction stream error", { error: event.error });
5306
+ return null;
5307
+ }
5308
+ }
5309
+ } catch (err) {
5310
+ log8.error("Brand extraction threw", { error: err?.message });
5311
+ return null;
5312
+ }
5313
+ const parsed = parseJsonResponse(responseText);
5314
+ if (!parsed) {
5315
+ log8.warn("Brand extraction returned unparseable JSON", {
5316
+ preview: responseText.slice(0, 200)
5317
+ });
5318
+ return null;
5319
+ }
5320
+ return validateBrand(parsed);
5321
+ }
5322
+ function buildCorpus() {
5323
+ const sections = [];
5324
+ const manifest = readSafe("mindstudio.json");
5325
+ if (manifest) {
5326
+ sections.push(`## File: mindstudio.json
5327
+
5328
+ ${manifest}`);
5329
+ }
5330
+ for (const filePath of walkMdFiles3("src")) {
5331
+ const content = readSafe(filePath);
5332
+ if (content) {
5333
+ sections.push(`## File: ${filePath}
5334
+
5335
+ ${content}`);
5336
+ }
5337
+ }
5338
+ return sections.join("\n\n---\n\n");
5339
+ }
5340
+ function parseJsonResponse(text) {
5341
+ const trimmed = text.trim();
5342
+ const fenceMatch = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/);
5343
+ const candidate = fenceMatch ? fenceMatch[1] : trimmed;
5344
+ try {
5345
+ return JSON.parse(candidate);
5346
+ } catch {
5347
+ const braceMatch = candidate.match(/\{[\s\S]*\}/);
5348
+ if (braceMatch) {
5349
+ try {
5350
+ return JSON.parse(braceMatch[0]);
5351
+ } catch {
5352
+ return null;
5353
+ }
5354
+ }
5355
+ return null;
5356
+ }
5357
+ }
5358
+ function validateBrand(raw) {
5359
+ if (!raw || typeof raw !== "object") {
5360
+ return null;
5361
+ }
5362
+ const obj = raw;
5363
+ const out = { version: 1 };
5364
+ if (typeof obj.name === "string" && obj.name.trim()) {
5365
+ out.name = obj.name.trim();
5366
+ }
5367
+ if (typeof obj.tagline === "string" && obj.tagline.trim()) {
5368
+ out.tagline = obj.tagline.trim();
5369
+ }
5370
+ if (typeof obj.logoUrl === "string" && obj.logoUrl.trim()) {
5371
+ out.logoUrl = obj.logoUrl.trim();
5372
+ }
5373
+ const colors = pickColors(obj.colors);
5374
+ if (colors) {
5375
+ out.colors = colors;
5376
+ }
5377
+ const typography = pickTypography(obj.typography);
5378
+ if (typography) {
5379
+ out.typography = typography;
5380
+ }
5381
+ return out;
5382
+ }
5383
+ function pickColors(raw) {
5384
+ if (!raw || typeof raw !== "object") {
5385
+ return void 0;
5386
+ }
5387
+ const c = raw;
5388
+ const out = {};
5389
+ for (const key of [
5390
+ "background",
5391
+ "text",
5392
+ "heading",
5393
+ "accent",
5394
+ "muted"
5395
+ ]) {
5396
+ const v = c[key];
5397
+ if (typeof v === "string" && v.trim()) {
5398
+ out[key] = v.trim();
5399
+ }
5400
+ }
5401
+ return Object.keys(out).length > 0 ? out : void 0;
5402
+ }
5403
+ function pickTypography(raw) {
5404
+ if (!raw || typeof raw !== "object") {
5405
+ return void 0;
5406
+ }
5407
+ const t = raw;
5408
+ const out = {};
5409
+ const body = pickFont(t.body);
5410
+ if (body) {
5411
+ out.body = body;
5412
+ }
5413
+ const heading = pickFont(t.heading);
5414
+ if (heading) {
5415
+ out.heading = heading;
5416
+ }
5417
+ return Object.keys(out).length > 0 ? out : void 0;
5418
+ }
5419
+ function pickFont(raw) {
5420
+ if (!raw || typeof raw !== "object") {
5421
+ return void 0;
5422
+ }
5423
+ const f = raw;
5424
+ if (typeof f.family !== "string" || !f.family.trim()) {
5425
+ return void 0;
5426
+ }
5427
+ const out = { family: f.family.trim() };
5428
+ if (typeof f.stylesheet === "string" && f.stylesheet.trim()) {
5429
+ out.stylesheet = f.stylesheet.trim();
5430
+ }
5431
+ if (typeof f.fileUrl === "string" && f.fileUrl.trim()) {
5432
+ out.fileUrl = f.fileUrl.trim();
5433
+ }
5434
+ return out;
5435
+ }
5436
+ function persistBrand(brand, inputHash) {
5437
+ const tmp = `${BRAND_FILE}.tmp`;
5438
+ fs19.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
5439
+ fs19.renameSync(tmp, BRAND_FILE);
5440
+ const cache = { inputHash, generatedAt: Date.now() };
5441
+ fs19.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
5442
+ }
5443
+ function readCache() {
5444
+ try {
5445
+ const raw = fs19.readFileSync(CACHE_FILE, "utf-8");
5446
+ const parsed = JSON.parse(raw);
5447
+ if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
5448
+ return parsed;
5449
+ }
5450
+ return null;
5451
+ } catch {
5452
+ return null;
5453
+ }
5454
+ }
5455
+
5456
+ // src/brandExtraction/trigger.ts
5457
+ var log9 = createLogger("brandExtraction:trigger");
5458
+ var inflight = false;
5459
+ var dirty = false;
5460
+ function triggerBrandExtraction(apiConfig) {
5461
+ if (inflight) {
5462
+ dirty = true;
5463
+ return;
5464
+ }
5465
+ inflight = true;
5466
+ void runExtraction(apiConfig).catch((err) => {
5467
+ log9.error("Brand extraction failed", { error: err?.message });
5468
+ }).finally(() => {
5469
+ inflight = false;
5470
+ if (dirty) {
5471
+ dirty = false;
5472
+ triggerBrandExtraction(apiConfig);
5473
+ }
5474
+ });
5475
+ }
5476
+
5477
+ // src/session.ts
5478
+ import fs20 from "fs";
5479
+ var log10 = createLogger("session");
5205
5480
  var SESSION_FILE = ".remy-session.json";
5206
5481
  function loadSession(state) {
5207
5482
  try {
5208
- const raw = fs19.readFileSync(SESSION_FILE, "utf-8");
5483
+ const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
5209
5484
  const data = JSON.parse(raw);
5210
5485
  if (Array.isArray(data.messages) && data.messages.length > 0) {
5211
5486
  state.messages = sanitizeMessages(data.messages);
5212
- log8.info("Session loaded", { messageCount: state.messages.length });
5487
+ log10.info("Session loaded", { messageCount: state.messages.length });
5213
5488
  return true;
5214
5489
  }
5215
5490
  } catch {
@@ -5254,20 +5529,20 @@ function sanitizeMessages(messages) {
5254
5529
  }
5255
5530
  function saveSession(state) {
5256
5531
  try {
5257
- fs19.writeFileSync(
5532
+ fs20.writeFileSync(
5258
5533
  SESSION_FILE,
5259
5534
  JSON.stringify({ messages: state.messages }, null, 2),
5260
5535
  "utf-8"
5261
5536
  );
5262
- log8.info("Session saved", { messageCount: state.messages.length });
5537
+ log10.info("Session saved", { messageCount: state.messages.length });
5263
5538
  } catch (err) {
5264
- log8.warn("Session save failed", { error: err.message });
5539
+ log10.warn("Session save failed", { error: err.message });
5265
5540
  }
5266
5541
  }
5267
5542
  function clearSession(state) {
5268
5543
  state.messages = [];
5269
5544
  try {
5270
- fs19.unlinkSync(SESSION_FILE);
5545
+ fs20.unlinkSync(SESSION_FILE);
5271
5546
  } catch {
5272
5547
  }
5273
5548
  }
@@ -5462,7 +5737,8 @@ function friendlyError(raw) {
5462
5737
  }
5463
5738
 
5464
5739
  // src/agent.ts
5465
- var log9 = createLogger("agent");
5740
+ var log11 = createLogger("agent");
5741
+ var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
5466
5742
  function getTextContent(blocks) {
5467
5743
  return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
5468
5744
  }
@@ -5509,7 +5785,7 @@ async function runTurn(params) {
5509
5785
  } = params;
5510
5786
  const tools2 = getToolDefinitions(onboardingState);
5511
5787
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
5512
- log9.info("Turn started", {
5788
+ log11.info("Turn started", {
5513
5789
  requestId,
5514
5790
  model,
5515
5791
  toolCount: tools2.length,
@@ -5738,7 +6014,7 @@ async function runTurn(params) {
5738
6014
  const tool = getToolByName(event.name);
5739
6015
  const wasStreamed = acc?.started ?? false;
5740
6016
  const isInputStreaming = !!tool?.streaming?.partialInput;
5741
- log9.info("Tool received", {
6017
+ log11.info("Tool received", {
5742
6018
  requestId,
5743
6019
  toolCallId: event.id,
5744
6020
  name: event.name
@@ -5831,7 +6107,7 @@ async function runTurn(params) {
5831
6107
  });
5832
6108
  return;
5833
6109
  }
5834
- log9.info("Tools executing", {
6110
+ log11.info("Tools executing", {
5835
6111
  requestId,
5836
6112
  count: toolCalls.length,
5837
6113
  tools: toolCalls.map((tc) => tc.name)
@@ -5878,7 +6154,7 @@ async function runTurn(params) {
5878
6154
  let result;
5879
6155
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
5880
6156
  saveSession(state);
5881
- log9.info("Waiting for external tool result", {
6157
+ log11.info("Waiting for external tool result", {
5882
6158
  requestId,
5883
6159
  toolCallId: tc.id,
5884
6160
  name: tc.name
@@ -5945,7 +6221,7 @@ async function runTurn(params) {
5945
6221
  if (!tc.input.background) {
5946
6222
  toolRegistry?.unregister(tc.id);
5947
6223
  }
5948
- log9.info("Tool completed", {
6224
+ log11.info("Tool completed", {
5949
6225
  requestId,
5950
6226
  toolCallId: tc.id,
5951
6227
  name: tc.name,
@@ -5959,6 +6235,9 @@ async function runTurn(params) {
5959
6235
  result: r.result,
5960
6236
  isError: r.isError
5961
6237
  });
6238
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
6239
+ triggerBrandExtraction(apiConfig);
6240
+ }
5962
6241
  return r;
5963
6242
  })
5964
6243
  );
@@ -6000,7 +6279,7 @@ async function runTurn(params) {
6000
6279
  }
6001
6280
 
6002
6281
  // src/toolRegistry.ts
6003
- var log10 = createLogger("tool-registry");
6282
+ var log12 = createLogger("tool-registry");
6004
6283
  var ToolRegistry = class {
6005
6284
  entries = /* @__PURE__ */ new Map();
6006
6285
  onEvent;
@@ -6026,7 +6305,7 @@ var ToolRegistry = class {
6026
6305
  if (!entry) {
6027
6306
  return false;
6028
6307
  }
6029
- log10.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6308
+ log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6030
6309
  entry.abortController.abort(mode);
6031
6310
  if (mode === "graceful") {
6032
6311
  const partial = entry.getPartialResult?.() ?? "";
@@ -6059,7 +6338,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6059
6338
  if (!entry) {
6060
6339
  return false;
6061
6340
  }
6062
- log10.info("Tool restarted", { toolCallId: id, name: entry.name });
6341
+ log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6063
6342
  entry.abortController.abort("restart");
6064
6343
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6065
6344
  this.onEvent?.({
@@ -6078,7 +6357,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6078
6357
  import { mkdirSync, existsSync } from "fs";
6079
6358
  import { writeFile } from "fs/promises";
6080
6359
  import { basename, join, extname } from "path";
6081
- var log11 = createLogger("headless:attachments");
6360
+ var log13 = createLogger("headless:attachments");
6082
6361
  var UPLOADS_DIR = "src/.user-uploads";
6083
6362
  function filenameFromUrl(url) {
6084
6363
  try {
@@ -6126,7 +6405,7 @@ async function persistAttachments(attachments) {
6126
6405
  }
6127
6406
  const buffer = Buffer.from(await res.arrayBuffer());
6128
6407
  await writeFile(localPath, buffer);
6129
- log11.info("Attachment saved", {
6408
+ log13.info("Attachment saved", {
6130
6409
  filename: name,
6131
6410
  path: localPath,
6132
6411
  bytes: buffer.length
@@ -6140,7 +6419,7 @@ async function persistAttachments(attachments) {
6140
6419
  if (textRes.ok) {
6141
6420
  extractedTextPath = `${localPath}.txt`;
6142
6421
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6143
- log11.info("Extracted text saved", { path: extractedTextPath });
6422
+ log13.info("Extracted text saved", { path: extractedTextPath });
6144
6423
  }
6145
6424
  } catch {
6146
6425
  }
@@ -6327,7 +6606,7 @@ function resolveAction(text) {
6327
6606
  }
6328
6607
 
6329
6608
  // src/headless/index.ts
6330
- var log12 = createLogger("headless");
6609
+ var log14 = createLogger("headless");
6331
6610
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6332
6611
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6333
6612
  "promptUser",
@@ -6399,6 +6678,7 @@ var HeadlessSession = class {
6399
6678
  ...this.queueFields()
6400
6679
  });
6401
6680
  }
6681
+ triggerBrandExtraction(this.config);
6402
6682
  this.toolRegistry.onEvent = this.onEvent;
6403
6683
  this.readline = createInterface({ input: process.stdin });
6404
6684
  this.readline.on("line", this.handleStdinLine);
@@ -6498,7 +6778,7 @@ var HeadlessSession = class {
6498
6778
  }
6499
6779
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
6500
6780
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6501
- log12.info("Background complete", {
6781
+ log14.info("Background complete", {
6502
6782
  toolCallId,
6503
6783
  name,
6504
6784
  requestId: this.currentRequestId
@@ -6719,7 +6999,7 @@ var HeadlessSession = class {
6719
6999
  this.turnStart = Date.now();
6720
7000
  const attachments = parsed.attachments;
6721
7001
  if (attachments?.length) {
6722
- log12.info("Message has attachments", {
7002
+ log14.info("Message has attachments", {
6723
7003
  count: attachments.length,
6724
7004
  urls: attachments.map((a) => a.url)
6725
7005
  });
@@ -6736,7 +7016,7 @@ var HeadlessSession = class {
6736
7016
  ${userMessage}` : header;
6737
7017
  }
6738
7018
  } catch (err) {
6739
- log12.warn("Attachment persistence failed", { error: err.message });
7019
+ log14.warn("Attachment persistence failed", { error: err.message });
6740
7020
  }
6741
7021
  }
6742
7022
  let resolved = null;
@@ -6794,7 +7074,7 @@ ${userMessage}` : header;
6794
7074
  error: "Turn ended unexpectedly"
6795
7075
  });
6796
7076
  }
6797
- log12.info("Turn complete", {
7077
+ log14.info("Turn complete", {
6798
7078
  requestId,
6799
7079
  durationMs: Date.now() - this.turnStart
6800
7080
  });
@@ -6806,7 +7086,7 @@ ${userMessage}` : header;
6806
7086
  error: err.message
6807
7087
  });
6808
7088
  }
6809
- log12.warn("Command failed", {
7089
+ log14.warn("Command failed", {
6810
7090
  action: "message",
6811
7091
  requestId,
6812
7092
  error: err.message
@@ -6941,7 +7221,7 @@ ${userMessage}` : header;
6941
7221
  return;
6942
7222
  }
6943
7223
  const { action, requestId } = parsed;
6944
- log12.info("Command received", { action, requestId });
7224
+ log14.info("Command received", { action, requestId });
6945
7225
  if (action === "tool_result" && parsed.id) {
6946
7226
  const id = parsed.id;
6947
7227
  const result = parsed.result ?? "";
@@ -6950,7 +7230,7 @@ ${userMessage}` : header;
6950
7230
  this.pendingTools.delete(id);
6951
7231
  pending.resolve(result);
6952
7232
  } else if (!this.running) {
6953
- log12.info("Late tool_result while idle, dismissing", { id });
7233
+ log14.info("Late tool_result while idle, dismissing", { id });
6954
7234
  this.emit("completed", { success: true }, requestId);
6955
7235
  } else {
6956
7236
  this.earlyResults.set(id, result);
package/dist/index.js CHANGED
@@ -2962,11 +2962,11 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2962
2962
  let prompt;
2963
2963
  let existingUrl;
2964
2964
  let onLog;
2965
- let path11;
2965
+ let path12;
2966
2966
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2967
2967
  prompt = promptOrOptions.prompt;
2968
2968
  existingUrl = promptOrOptions.imageUrl;
2969
- path11 = promptOrOptions.path;
2969
+ path12 = promptOrOptions.path;
2970
2970
  onLog = promptOrOptions.onLog;
2971
2971
  } else {
2972
2972
  prompt = promptOrOptions;
@@ -2978,7 +2978,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2978
2978
  } else {
2979
2979
  const ssResult = await sidecarRequest(
2980
2980
  "/screenshot-full-page",
2981
- path11 ? { path: path11 } : void 0,
2981
+ path12 ? { path: path12 } : void 0,
2982
2982
  { timeout: 12e4 }
2983
2983
  );
2984
2984
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -3033,15 +3033,15 @@ var init_browserLock = __esm({
3033
3033
  // src/statusWatcher.ts
3034
3034
  function startStatusWatcher(config) {
3035
3035
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
3036
- let inflight = false;
3036
+ let inflight2 = false;
3037
3037
  let stopped = false;
3038
3038
  let pauseCount = 0;
3039
3039
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3040
3040
  async function tick() {
3041
- if (stopped || signal?.aborted || inflight || pauseCount > 0) {
3041
+ if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
3042
3042
  return;
3043
3043
  }
3044
- inflight = true;
3044
+ inflight2 = true;
3045
3045
  try {
3046
3046
  const context = getContext();
3047
3047
  if (!context) {
@@ -3069,7 +3069,7 @@ function startStatusWatcher(config) {
3069
3069
  onStatus(data.label);
3070
3070
  } catch {
3071
3071
  } finally {
3072
- inflight = false;
3072
+ inflight2 = false;
3073
3073
  }
3074
3074
  }
3075
3075
  const timer = setInterval(tick, interval);
@@ -4936,15 +4936,15 @@ function getSampleIndices(pools, sizes) {
4936
4936
  }
4937
4937
  const loaded = load();
4938
4938
  if (loaded) {
4939
- let dirty = false;
4939
+ let dirty2 = false;
4940
4940
  for (const key of ["uiInspiration", "designReferences", "fonts"]) {
4941
4941
  const before = loaded[key].length;
4942
4942
  loaded[key] = loaded[key].filter((i) => i < pools[key]);
4943
4943
  if (loaded[key].length < before) {
4944
- dirty = true;
4944
+ dirty2 = true;
4945
4945
  }
4946
4946
  }
4947
- if (dirty) {
4947
+ if (dirty2) {
4948
4948
  save(loaded);
4949
4949
  }
4950
4950
  cached = loaded;
@@ -6061,6 +6061,298 @@ var init_errors = __esm({
6061
6061
  }
6062
6062
  });
6063
6063
 
6064
+ // src/brandExtraction/index.ts
6065
+ import fs19 from "fs";
6066
+ import path9 from "path";
6067
+ import { createHash } from "crypto";
6068
+ async function runExtraction(apiConfig) {
6069
+ const inputHash = computeInputHash();
6070
+ const cached2 = readCache();
6071
+ if (cached2 && cached2.inputHash === inputHash) {
6072
+ log8.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6073
+ return null;
6074
+ }
6075
+ log8.info("Extracting brand", { inputHash });
6076
+ const brand = await extractBrand(apiConfig);
6077
+ if (!brand) {
6078
+ log8.warn("Brand extraction failed \u2014 leaving cache untouched");
6079
+ return null;
6080
+ }
6081
+ persistBrand(brand, inputHash);
6082
+ log8.info("Brand persisted", { inputHash });
6083
+ return brand;
6084
+ }
6085
+ function computeInputHash() {
6086
+ const entries = [];
6087
+ for (const filePath of walkMdFiles3("src")) {
6088
+ if (filePath === path9.join("src", "app.md")) {
6089
+ entries.push({ path: filePath, content: readSafe(filePath) });
6090
+ continue;
6091
+ }
6092
+ const fm = parseFrontmatter3(filePath);
6093
+ if (fm.type.startsWith("design/color") || fm.type.startsWith("design/typography")) {
6094
+ entries.push({ path: filePath, content: readSafe(filePath) });
6095
+ }
6096
+ }
6097
+ const manifest = readSafe("mindstudio.json");
6098
+ if (manifest) {
6099
+ entries.push({ path: "mindstudio.json", content: manifest });
6100
+ }
6101
+ entries.sort((a, b) => a.path.localeCompare(b.path));
6102
+ const fingerprint = entries.map((e) => `${e.path}:${sha256(e.content)}`).join("\n");
6103
+ return sha256(fingerprint);
6104
+ }
6105
+ function sha256(input) {
6106
+ return createHash("sha256").update(input).digest("hex");
6107
+ }
6108
+ function readSafe(filePath) {
6109
+ try {
6110
+ return fs19.readFileSync(filePath, "utf-8");
6111
+ } catch {
6112
+ return "";
6113
+ }
6114
+ }
6115
+ function walkMdFiles3(dir) {
6116
+ const results = [];
6117
+ try {
6118
+ const entries = fs19.readdirSync(dir, { withFileTypes: true });
6119
+ for (const entry of entries) {
6120
+ const full = path9.join(dir, entry.name);
6121
+ if (entry.isDirectory()) {
6122
+ results.push(...walkMdFiles3(full));
6123
+ } else if (entry.name.endsWith(".md")) {
6124
+ results.push(full);
6125
+ }
6126
+ }
6127
+ } catch {
6128
+ }
6129
+ return results.sort();
6130
+ }
6131
+ function parseFrontmatter3(filePath) {
6132
+ try {
6133
+ const content = fs19.readFileSync(filePath, "utf-8");
6134
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6135
+ if (!match) {
6136
+ return { type: "" };
6137
+ }
6138
+ const fm = match[1];
6139
+ const type = fm.match(/^type:\s*(.+)$/m)?.[1]?.trim() ?? "";
6140
+ return { type };
6141
+ } catch {
6142
+ return { type: "" };
6143
+ }
6144
+ }
6145
+ async function extractBrand(apiConfig) {
6146
+ const corpus = buildCorpus();
6147
+ if (!corpus.trim()) {
6148
+ log8.debug("No spec corpus \u2014 emitting empty brand");
6149
+ return { version: 1 };
6150
+ }
6151
+ let responseText = "";
6152
+ try {
6153
+ for await (const event of streamChat({
6154
+ ...apiConfig,
6155
+ subAgentId: "brandExtractor",
6156
+ system: EXTRACT_PROMPT,
6157
+ messages: [{ role: "user", content: corpus }],
6158
+ tools: []
6159
+ })) {
6160
+ if (event.type === "text") {
6161
+ responseText += event.text;
6162
+ } else if (event.type === "error") {
6163
+ log8.error("Brand extraction stream error", { error: event.error });
6164
+ return null;
6165
+ }
6166
+ }
6167
+ } catch (err) {
6168
+ log8.error("Brand extraction threw", { error: err?.message });
6169
+ return null;
6170
+ }
6171
+ const parsed = parseJsonResponse(responseText);
6172
+ if (!parsed) {
6173
+ log8.warn("Brand extraction returned unparseable JSON", {
6174
+ preview: responseText.slice(0, 200)
6175
+ });
6176
+ return null;
6177
+ }
6178
+ return validateBrand(parsed);
6179
+ }
6180
+ function buildCorpus() {
6181
+ const sections = [];
6182
+ const manifest = readSafe("mindstudio.json");
6183
+ if (manifest) {
6184
+ sections.push(`## File: mindstudio.json
6185
+
6186
+ ${manifest}`);
6187
+ }
6188
+ for (const filePath of walkMdFiles3("src")) {
6189
+ const content = readSafe(filePath);
6190
+ if (content) {
6191
+ sections.push(`## File: ${filePath}
6192
+
6193
+ ${content}`);
6194
+ }
6195
+ }
6196
+ return sections.join("\n\n---\n\n");
6197
+ }
6198
+ function parseJsonResponse(text) {
6199
+ const trimmed = text.trim();
6200
+ const fenceMatch = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/);
6201
+ const candidate = fenceMatch ? fenceMatch[1] : trimmed;
6202
+ try {
6203
+ return JSON.parse(candidate);
6204
+ } catch {
6205
+ const braceMatch = candidate.match(/\{[\s\S]*\}/);
6206
+ if (braceMatch) {
6207
+ try {
6208
+ return JSON.parse(braceMatch[0]);
6209
+ } catch {
6210
+ return null;
6211
+ }
6212
+ }
6213
+ return null;
6214
+ }
6215
+ }
6216
+ function validateBrand(raw) {
6217
+ if (!raw || typeof raw !== "object") {
6218
+ return null;
6219
+ }
6220
+ const obj = raw;
6221
+ const out = { version: 1 };
6222
+ if (typeof obj.name === "string" && obj.name.trim()) {
6223
+ out.name = obj.name.trim();
6224
+ }
6225
+ if (typeof obj.tagline === "string" && obj.tagline.trim()) {
6226
+ out.tagline = obj.tagline.trim();
6227
+ }
6228
+ if (typeof obj.logoUrl === "string" && obj.logoUrl.trim()) {
6229
+ out.logoUrl = obj.logoUrl.trim();
6230
+ }
6231
+ const colors = pickColors(obj.colors);
6232
+ if (colors) {
6233
+ out.colors = colors;
6234
+ }
6235
+ const typography = pickTypography(obj.typography);
6236
+ if (typography) {
6237
+ out.typography = typography;
6238
+ }
6239
+ return out;
6240
+ }
6241
+ function pickColors(raw) {
6242
+ if (!raw || typeof raw !== "object") {
6243
+ return void 0;
6244
+ }
6245
+ const c = raw;
6246
+ const out = {};
6247
+ for (const key of [
6248
+ "background",
6249
+ "text",
6250
+ "heading",
6251
+ "accent",
6252
+ "muted"
6253
+ ]) {
6254
+ const v = c[key];
6255
+ if (typeof v === "string" && v.trim()) {
6256
+ out[key] = v.trim();
6257
+ }
6258
+ }
6259
+ return Object.keys(out).length > 0 ? out : void 0;
6260
+ }
6261
+ function pickTypography(raw) {
6262
+ if (!raw || typeof raw !== "object") {
6263
+ return void 0;
6264
+ }
6265
+ const t = raw;
6266
+ const out = {};
6267
+ const body = pickFont(t.body);
6268
+ if (body) {
6269
+ out.body = body;
6270
+ }
6271
+ const heading = pickFont(t.heading);
6272
+ if (heading) {
6273
+ out.heading = heading;
6274
+ }
6275
+ return Object.keys(out).length > 0 ? out : void 0;
6276
+ }
6277
+ function pickFont(raw) {
6278
+ if (!raw || typeof raw !== "object") {
6279
+ return void 0;
6280
+ }
6281
+ const f = raw;
6282
+ if (typeof f.family !== "string" || !f.family.trim()) {
6283
+ return void 0;
6284
+ }
6285
+ const out = { family: f.family.trim() };
6286
+ if (typeof f.stylesheet === "string" && f.stylesheet.trim()) {
6287
+ out.stylesheet = f.stylesheet.trim();
6288
+ }
6289
+ if (typeof f.fileUrl === "string" && f.fileUrl.trim()) {
6290
+ out.fileUrl = f.fileUrl.trim();
6291
+ }
6292
+ return out;
6293
+ }
6294
+ function persistBrand(brand, inputHash) {
6295
+ const tmp = `${BRAND_FILE}.tmp`;
6296
+ fs19.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6297
+ fs19.renameSync(tmp, BRAND_FILE);
6298
+ const cache = { inputHash, generatedAt: Date.now() };
6299
+ fs19.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6300
+ }
6301
+ function readCache() {
6302
+ try {
6303
+ const raw = fs19.readFileSync(CACHE_FILE, "utf-8");
6304
+ const parsed = JSON.parse(raw);
6305
+ if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
6306
+ return parsed;
6307
+ }
6308
+ return null;
6309
+ } catch {
6310
+ return null;
6311
+ }
6312
+ }
6313
+ var log8, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE;
6314
+ var init_brandExtraction = __esm({
6315
+ "src/brandExtraction/index.ts"() {
6316
+ "use strict";
6317
+ init_api();
6318
+ init_assets();
6319
+ init_logger();
6320
+ log8 = createLogger("brandExtraction");
6321
+ EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6322
+ BRAND_FILE = ".remy-brand.json";
6323
+ CACHE_FILE = ".remy-brand.cache.json";
6324
+ }
6325
+ });
6326
+
6327
+ // src/brandExtraction/trigger.ts
6328
+ function triggerBrandExtraction(apiConfig) {
6329
+ if (inflight) {
6330
+ dirty = true;
6331
+ return;
6332
+ }
6333
+ inflight = true;
6334
+ void runExtraction(apiConfig).catch((err) => {
6335
+ log9.error("Brand extraction failed", { error: err?.message });
6336
+ }).finally(() => {
6337
+ inflight = false;
6338
+ if (dirty) {
6339
+ dirty = false;
6340
+ triggerBrandExtraction(apiConfig);
6341
+ }
6342
+ });
6343
+ }
6344
+ var log9, inflight, dirty;
6345
+ var init_trigger2 = __esm({
6346
+ "src/brandExtraction/trigger.ts"() {
6347
+ "use strict";
6348
+ init_brandExtraction();
6349
+ init_logger();
6350
+ log9 = createLogger("brandExtraction:trigger");
6351
+ inflight = false;
6352
+ dirty = false;
6353
+ }
6354
+ });
6355
+
6064
6356
  // src/agent.ts
6065
6357
  function getTextContent(blocks) {
6066
6358
  return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
@@ -6092,7 +6384,7 @@ async function runTurn(params) {
6092
6384
  } = params;
6093
6385
  const tools2 = getToolDefinitions(onboardingState);
6094
6386
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
6095
- log8.info("Turn started", {
6387
+ log10.info("Turn started", {
6096
6388
  requestId,
6097
6389
  model,
6098
6390
  toolCount: tools2.length,
@@ -6321,7 +6613,7 @@ async function runTurn(params) {
6321
6613
  const tool = getToolByName(event.name);
6322
6614
  const wasStreamed = acc?.started ?? false;
6323
6615
  const isInputStreaming = !!tool?.streaming?.partialInput;
6324
- log8.info("Tool received", {
6616
+ log10.info("Tool received", {
6325
6617
  requestId,
6326
6618
  toolCallId: event.id,
6327
6619
  name: event.name
@@ -6414,7 +6706,7 @@ async function runTurn(params) {
6414
6706
  });
6415
6707
  return;
6416
6708
  }
6417
- log8.info("Tools executing", {
6709
+ log10.info("Tools executing", {
6418
6710
  requestId,
6419
6711
  count: toolCalls.length,
6420
6712
  tools: toolCalls.map((tc) => tc.name)
@@ -6461,7 +6753,7 @@ async function runTurn(params) {
6461
6753
  let result;
6462
6754
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
6463
6755
  saveSession(state);
6464
- log8.info("Waiting for external tool result", {
6756
+ log10.info("Waiting for external tool result", {
6465
6757
  requestId,
6466
6758
  toolCallId: tc.id,
6467
6759
  name: tc.name
@@ -6528,7 +6820,7 @@ async function runTurn(params) {
6528
6820
  if (!tc.input.background) {
6529
6821
  toolRegistry?.unregister(tc.id);
6530
6822
  }
6531
- log8.info("Tool completed", {
6823
+ log10.info("Tool completed", {
6532
6824
  requestId,
6533
6825
  toolCallId: tc.id,
6534
6826
  name: tc.name,
@@ -6542,6 +6834,9 @@ async function runTurn(params) {
6542
6834
  result: r.result,
6543
6835
  isError: r.isError
6544
6836
  });
6837
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
6838
+ triggerBrandExtraction(apiConfig);
6839
+ }
6545
6840
  return r;
6546
6841
  })
6547
6842
  );
@@ -6581,7 +6876,7 @@ async function runTurn(params) {
6581
6876
  }
6582
6877
  }
6583
6878
  }
6584
- var log8, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
6879
+ var log10, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
6585
6880
  var init_agent = __esm({
6586
6881
  "src/agent.ts"() {
6587
6882
  "use strict";
@@ -6595,7 +6890,9 @@ var init_agent = __esm({
6595
6890
  init_cleanMessages();
6596
6891
  init_tools6();
6597
6892
  init_sentinel();
6598
- log8 = createLogger("agent");
6893
+ init_trigger2();
6894
+ log10 = createLogger("agent");
6895
+ BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
6599
6896
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
6600
6897
  "promptUser",
6601
6898
  "setProjectOnboardingState",
@@ -6616,16 +6913,16 @@ var init_agent = __esm({
6616
6913
  });
6617
6914
 
6618
6915
  // src/config.ts
6619
- import fs19 from "fs";
6620
- import path9 from "path";
6916
+ import fs20 from "fs";
6917
+ import path10 from "path";
6621
6918
  import os from "os";
6622
6919
  function loadConfigFile() {
6623
6920
  try {
6624
- const raw = fs19.readFileSync(CONFIG_PATH, "utf-8");
6625
- log9.debug("Loaded config file", { path: CONFIG_PATH });
6921
+ const raw = fs20.readFileSync(CONFIG_PATH, "utf-8");
6922
+ log11.debug("Loaded config file", { path: CONFIG_PATH });
6626
6923
  return JSON.parse(raw);
6627
6924
  } catch (err) {
6628
- log9.debug("No config file found", {
6925
+ log11.debug("No config file found", {
6629
6926
  path: CONFIG_PATH,
6630
6927
  error: err.message
6631
6928
  });
@@ -6639,26 +6936,26 @@ function resolveConfig(flags2) {
6639
6936
  const apiKey = flags2?.apiKey || process.env.MINDSTUDIO_API_KEY || env?.apiKey || "";
6640
6937
  const baseUrl2 = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
6641
6938
  if (!apiKey) {
6642
- log9.error("No API key found");
6939
+ log11.error("No API key found");
6643
6940
  throw new Error(
6644
6941
  "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
6645
6942
  );
6646
6943
  }
6647
6944
  const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
6648
- log9.info("Config resolved", {
6945
+ log11.info("Config resolved", {
6649
6946
  baseUrl: baseUrl2,
6650
6947
  keySource,
6651
6948
  environment: activeEnv
6652
6949
  });
6653
6950
  return { apiKey, baseUrl: baseUrl2 };
6654
6951
  }
6655
- var log9, CONFIG_PATH, DEFAULT_BASE_URL;
6952
+ var log11, CONFIG_PATH, DEFAULT_BASE_URL;
6656
6953
  var init_config = __esm({
6657
6954
  "src/config.ts"() {
6658
6955
  "use strict";
6659
6956
  init_logger();
6660
- log9 = createLogger("config");
6661
- CONFIG_PATH = path9.join(
6957
+ log11 = createLogger("config");
6958
+ CONFIG_PATH = path10.join(
6662
6959
  os.homedir(),
6663
6960
  ".mindstudio-local-tunnel",
6664
6961
  "config.json"
@@ -6668,12 +6965,12 @@ var init_config = __esm({
6668
6965
  });
6669
6966
 
6670
6967
  // src/toolRegistry.ts
6671
- var log10, ToolRegistry;
6968
+ var log12, ToolRegistry;
6672
6969
  var init_toolRegistry = __esm({
6673
6970
  "src/toolRegistry.ts"() {
6674
6971
  "use strict";
6675
6972
  init_logger();
6676
- log10 = createLogger("tool-registry");
6973
+ log12 = createLogger("tool-registry");
6677
6974
  ToolRegistry = class {
6678
6975
  entries = /* @__PURE__ */ new Map();
6679
6976
  onEvent;
@@ -6699,7 +6996,7 @@ var init_toolRegistry = __esm({
6699
6996
  if (!entry) {
6700
6997
  return false;
6701
6998
  }
6702
- log10.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6999
+ log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6703
7000
  entry.abortController.abort(mode);
6704
7001
  if (mode === "graceful") {
6705
7002
  const partial = entry.getPartialResult?.() ?? "";
@@ -6732,7 +7029,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6732
7029
  if (!entry) {
6733
7030
  return false;
6734
7031
  }
6735
- log10.info("Tool restarted", { toolCallId: id, name: entry.name });
7032
+ log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6736
7033
  entry.abortController.abort("restart");
6737
7034
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6738
7035
  this.onEvent?.({
@@ -6798,7 +7095,7 @@ async function persistAttachments(attachments) {
6798
7095
  }
6799
7096
  const buffer = Buffer.from(await res.arrayBuffer());
6800
7097
  await writeFile(localPath, buffer);
6801
- log11.info("Attachment saved", {
7098
+ log13.info("Attachment saved", {
6802
7099
  filename: name,
6803
7100
  path: localPath,
6804
7101
  bytes: buffer.length
@@ -6812,7 +7109,7 @@ async function persistAttachments(attachments) {
6812
7109
  if (textRes.ok) {
6813
7110
  extractedTextPath = `${localPath}.txt`;
6814
7111
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6815
- log11.info("Extracted text saved", { path: extractedTextPath });
7112
+ log13.info("Extracted text saved", { path: extractedTextPath });
6816
7113
  }
6817
7114
  } catch {
6818
7115
  }
@@ -6857,12 +7154,12 @@ function buildUploadHeader(results) {
6857
7154
  return `[Uploaded files]
6858
7155
  ${lines.join("\n")}`;
6859
7156
  }
6860
- var log11, UPLOADS_DIR, IMAGE_EXTENSIONS;
7157
+ var log13, UPLOADS_DIR, IMAGE_EXTENSIONS;
6861
7158
  var init_attachments = __esm({
6862
7159
  "src/headless/attachments.ts"() {
6863
7160
  "use strict";
6864
7161
  init_logger();
6865
- log11 = createLogger("headless:attachments");
7162
+ log13 = createLogger("headless:attachments");
6866
7163
  UPLOADS_DIR = "src/.user-uploads";
6867
7164
  IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
6868
7165
  }
@@ -7041,7 +7338,7 @@ __export(headless_exports, {
7041
7338
  HeadlessSession: () => HeadlessSession
7042
7339
  });
7043
7340
  import { createInterface } from "readline";
7044
- var log12, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
7341
+ var log14, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
7045
7342
  var init_headless = __esm({
7046
7343
  "src/headless/index.ts"() {
7047
7344
  "use strict";
@@ -7050,6 +7347,7 @@ var init_headless = __esm({
7050
7347
  init_prompt();
7051
7348
  init_trigger();
7052
7349
  init_compaction();
7350
+ init_trigger2();
7053
7351
  init_lsp();
7054
7352
  init_agent();
7055
7353
  init_session();
@@ -7060,7 +7358,7 @@ var init_headless = __esm({
7060
7358
  init_messageQueue();
7061
7359
  init_resolve();
7062
7360
  init_sentinel();
7063
- log12 = createLogger("headless");
7361
+ log14 = createLogger("headless");
7064
7362
  EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
7065
7363
  USER_FACING_TOOLS = /* @__PURE__ */ new Set([
7066
7364
  "promptUser",
@@ -7132,6 +7430,7 @@ var init_headless = __esm({
7132
7430
  ...this.queueFields()
7133
7431
  });
7134
7432
  }
7433
+ triggerBrandExtraction(this.config);
7135
7434
  this.toolRegistry.onEvent = this.onEvent;
7136
7435
  this.readline = createInterface({ input: process.stdin });
7137
7436
  this.readline.on("line", this.handleStdinLine);
@@ -7231,7 +7530,7 @@ var init_headless = __esm({
7231
7530
  }
7232
7531
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
7233
7532
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
7234
- log12.info("Background complete", {
7533
+ log14.info("Background complete", {
7235
7534
  toolCallId,
7236
7535
  name,
7237
7536
  requestId: this.currentRequestId
@@ -7452,7 +7751,7 @@ var init_headless = __esm({
7452
7751
  this.turnStart = Date.now();
7453
7752
  const attachments = parsed.attachments;
7454
7753
  if (attachments?.length) {
7455
- log12.info("Message has attachments", {
7754
+ log14.info("Message has attachments", {
7456
7755
  count: attachments.length,
7457
7756
  urls: attachments.map((a) => a.url)
7458
7757
  });
@@ -7469,7 +7768,7 @@ var init_headless = __esm({
7469
7768
  ${userMessage}` : header;
7470
7769
  }
7471
7770
  } catch (err) {
7472
- log12.warn("Attachment persistence failed", { error: err.message });
7771
+ log14.warn("Attachment persistence failed", { error: err.message });
7473
7772
  }
7474
7773
  }
7475
7774
  let resolved = null;
@@ -7527,7 +7826,7 @@ ${userMessage}` : header;
7527
7826
  error: "Turn ended unexpectedly"
7528
7827
  });
7529
7828
  }
7530
- log12.info("Turn complete", {
7829
+ log14.info("Turn complete", {
7531
7830
  requestId,
7532
7831
  durationMs: Date.now() - this.turnStart
7533
7832
  });
@@ -7539,7 +7838,7 @@ ${userMessage}` : header;
7539
7838
  error: err.message
7540
7839
  });
7541
7840
  }
7542
- log12.warn("Command failed", {
7841
+ log14.warn("Command failed", {
7543
7842
  action: "message",
7544
7843
  requestId,
7545
7844
  error: err.message
@@ -7674,7 +7973,7 @@ ${userMessage}` : header;
7674
7973
  return;
7675
7974
  }
7676
7975
  const { action, requestId } = parsed;
7677
- log12.info("Command received", { action, requestId });
7976
+ log14.info("Command received", { action, requestId });
7678
7977
  if (action === "tool_result" && parsed.id) {
7679
7978
  const id = parsed.id;
7680
7979
  const result = parsed.result ?? "";
@@ -7683,7 +7982,7 @@ ${userMessage}` : header;
7683
7982
  this.pendingTools.delete(id);
7684
7983
  pending.resolve(result);
7685
7984
  } else if (!this.running) {
7686
- log12.info("Late tool_result while idle, dismissing", { id });
7985
+ log14.info("Late tool_result while idle, dismissing", { id });
7687
7986
  this.emit("completed", { success: true }, requestId);
7688
7987
  } else {
7689
7988
  this.earlyResults.set(id, result);
@@ -7811,8 +8110,8 @@ ${userMessage}` : header;
7811
8110
  // src/index.tsx
7812
8111
  import { render } from "ink";
7813
8112
  import os2 from "os";
7814
- import fs20 from "fs";
7815
- import path10 from "path";
8113
+ import fs21 from "fs";
8114
+ import path11 from "path";
7816
8115
 
7817
8116
  // src/tui/App.tsx
7818
8117
  import { useState as useState2, useCallback, useRef } from "react";
@@ -8130,8 +8429,8 @@ for (let i = 0; i < args.length; i++) {
8130
8429
  var startupLog = createLogger("startup");
8131
8430
  function printDebugInfo(config) {
8132
8431
  const pkg = JSON.parse(
8133
- fs20.readFileSync(
8134
- path10.join(import.meta.dirname, "..", "package.json"),
8432
+ fs21.readFileSync(
8433
+ path11.join(import.meta.dirname, "..", "package.json"),
8135
8434
  "utf-8"
8136
8435
  )
8137
8436
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.158",
3
+ "version": "0.1.159",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",