@mindstudio-ai/remy 0.1.158 → 0.1.160

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,
@@ -5572,6 +5848,7 @@ async function runTurn(params) {
5572
5848
  const contentBlocks = [];
5573
5849
  const thinkingBlockStartTimes = [];
5574
5850
  let thinkingCompleteCount = 0;
5851
+ let textBlockOpen = false;
5575
5852
  const toolInputAccumulators = /* @__PURE__ */ new Map();
5576
5853
  let stopReason = "end_turn";
5577
5854
  let subAgentText = "";
@@ -5682,7 +5959,7 @@ async function runTurn(params) {
5682
5959
  switch (event.type) {
5683
5960
  case "text": {
5684
5961
  const lastBlock = contentBlocks.at(-1);
5685
- if (lastBlock?.type === "text") {
5962
+ if (lastBlock?.type === "text" && textBlockOpen) {
5686
5963
  lastBlock.text += event.text;
5687
5964
  } else {
5688
5965
  contentBlocks.push({
@@ -5691,12 +5968,14 @@ async function runTurn(params) {
5691
5968
  startedAt: event.ts
5692
5969
  });
5693
5970
  }
5971
+ textBlockOpen = true;
5694
5972
  onEvent({ type: "text", text: event.text });
5695
5973
  break;
5696
5974
  }
5697
5975
  case "thinking":
5698
5976
  if (event.text === "") {
5699
5977
  thinkingBlockStartTimes.push(event.ts);
5978
+ textBlockOpen = false;
5700
5979
  }
5701
5980
  onEvent({ type: "thinking", text: event.text });
5702
5981
  break;
@@ -5738,7 +6017,7 @@ async function runTurn(params) {
5738
6017
  const tool = getToolByName(event.name);
5739
6018
  const wasStreamed = acc?.started ?? false;
5740
6019
  const isInputStreaming = !!tool?.streaming?.partialInput;
5741
- log9.info("Tool received", {
6020
+ log11.info("Tool received", {
5742
6021
  requestId,
5743
6022
  toolCallId: event.id,
5744
6023
  name: event.name
@@ -5831,7 +6110,7 @@ async function runTurn(params) {
5831
6110
  });
5832
6111
  return;
5833
6112
  }
5834
- log9.info("Tools executing", {
6113
+ log11.info("Tools executing", {
5835
6114
  requestId,
5836
6115
  count: toolCalls.length,
5837
6116
  tools: toolCalls.map((tc) => tc.name)
@@ -5878,7 +6157,7 @@ async function runTurn(params) {
5878
6157
  let result;
5879
6158
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
5880
6159
  saveSession(state);
5881
- log9.info("Waiting for external tool result", {
6160
+ log11.info("Waiting for external tool result", {
5882
6161
  requestId,
5883
6162
  toolCallId: tc.id,
5884
6163
  name: tc.name
@@ -5945,7 +6224,7 @@ async function runTurn(params) {
5945
6224
  if (!tc.input.background) {
5946
6225
  toolRegistry?.unregister(tc.id);
5947
6226
  }
5948
- log9.info("Tool completed", {
6227
+ log11.info("Tool completed", {
5949
6228
  requestId,
5950
6229
  toolCallId: tc.id,
5951
6230
  name: tc.name,
@@ -5959,6 +6238,9 @@ async function runTurn(params) {
5959
6238
  result: r.result,
5960
6239
  isError: r.isError
5961
6240
  });
6241
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
6242
+ triggerBrandExtraction(apiConfig);
6243
+ }
5962
6244
  return r;
5963
6245
  })
5964
6246
  );
@@ -6000,7 +6282,7 @@ async function runTurn(params) {
6000
6282
  }
6001
6283
 
6002
6284
  // src/toolRegistry.ts
6003
- var log10 = createLogger("tool-registry");
6285
+ var log12 = createLogger("tool-registry");
6004
6286
  var ToolRegistry = class {
6005
6287
  entries = /* @__PURE__ */ new Map();
6006
6288
  onEvent;
@@ -6026,7 +6308,7 @@ var ToolRegistry = class {
6026
6308
  if (!entry) {
6027
6309
  return false;
6028
6310
  }
6029
- log10.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6311
+ log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6030
6312
  entry.abortController.abort(mode);
6031
6313
  if (mode === "graceful") {
6032
6314
  const partial = entry.getPartialResult?.() ?? "";
@@ -6059,7 +6341,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6059
6341
  if (!entry) {
6060
6342
  return false;
6061
6343
  }
6062
- log10.info("Tool restarted", { toolCallId: id, name: entry.name });
6344
+ log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6063
6345
  entry.abortController.abort("restart");
6064
6346
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6065
6347
  this.onEvent?.({
@@ -6078,7 +6360,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6078
6360
  import { mkdirSync, existsSync } from "fs";
6079
6361
  import { writeFile } from "fs/promises";
6080
6362
  import { basename, join, extname } from "path";
6081
- var log11 = createLogger("headless:attachments");
6363
+ var log13 = createLogger("headless:attachments");
6082
6364
  var UPLOADS_DIR = "src/.user-uploads";
6083
6365
  function filenameFromUrl(url) {
6084
6366
  try {
@@ -6126,7 +6408,7 @@ async function persistAttachments(attachments) {
6126
6408
  }
6127
6409
  const buffer = Buffer.from(await res.arrayBuffer());
6128
6410
  await writeFile(localPath, buffer);
6129
- log11.info("Attachment saved", {
6411
+ log13.info("Attachment saved", {
6130
6412
  filename: name,
6131
6413
  path: localPath,
6132
6414
  bytes: buffer.length
@@ -6140,7 +6422,7 @@ async function persistAttachments(attachments) {
6140
6422
  if (textRes.ok) {
6141
6423
  extractedTextPath = `${localPath}.txt`;
6142
6424
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6143
- log11.info("Extracted text saved", { path: extractedTextPath });
6425
+ log13.info("Extracted text saved", { path: extractedTextPath });
6144
6426
  }
6145
6427
  } catch {
6146
6428
  }
@@ -6327,7 +6609,7 @@ function resolveAction(text) {
6327
6609
  }
6328
6610
 
6329
6611
  // src/headless/index.ts
6330
- var log12 = createLogger("headless");
6612
+ var log14 = createLogger("headless");
6331
6613
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6332
6614
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6333
6615
  "promptUser",
@@ -6399,6 +6681,7 @@ var HeadlessSession = class {
6399
6681
  ...this.queueFields()
6400
6682
  });
6401
6683
  }
6684
+ triggerBrandExtraction(this.config);
6402
6685
  this.toolRegistry.onEvent = this.onEvent;
6403
6686
  this.readline = createInterface({ input: process.stdin });
6404
6687
  this.readline.on("line", this.handleStdinLine);
@@ -6498,7 +6781,7 @@ var HeadlessSession = class {
6498
6781
  }
6499
6782
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
6500
6783
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6501
- log12.info("Background complete", {
6784
+ log14.info("Background complete", {
6502
6785
  toolCallId,
6503
6786
  name,
6504
6787
  requestId: this.currentRequestId
@@ -6719,7 +7002,7 @@ var HeadlessSession = class {
6719
7002
  this.turnStart = Date.now();
6720
7003
  const attachments = parsed.attachments;
6721
7004
  if (attachments?.length) {
6722
- log12.info("Message has attachments", {
7005
+ log14.info("Message has attachments", {
6723
7006
  count: attachments.length,
6724
7007
  urls: attachments.map((a) => a.url)
6725
7008
  });
@@ -6736,7 +7019,7 @@ var HeadlessSession = class {
6736
7019
  ${userMessage}` : header;
6737
7020
  }
6738
7021
  } catch (err) {
6739
- log12.warn("Attachment persistence failed", { error: err.message });
7022
+ log14.warn("Attachment persistence failed", { error: err.message });
6740
7023
  }
6741
7024
  }
6742
7025
  let resolved = null;
@@ -6794,7 +7077,7 @@ ${userMessage}` : header;
6794
7077
  error: "Turn ended unexpectedly"
6795
7078
  });
6796
7079
  }
6797
- log12.info("Turn complete", {
7080
+ log14.info("Turn complete", {
6798
7081
  requestId,
6799
7082
  durationMs: Date.now() - this.turnStart
6800
7083
  });
@@ -6806,7 +7089,7 @@ ${userMessage}` : header;
6806
7089
  error: err.message
6807
7090
  });
6808
7091
  }
6809
- log12.warn("Command failed", {
7092
+ log14.warn("Command failed", {
6810
7093
  action: "message",
6811
7094
  requestId,
6812
7095
  error: err.message
@@ -6941,7 +7224,7 @@ ${userMessage}` : header;
6941
7224
  return;
6942
7225
  }
6943
7226
  const { action, requestId } = parsed;
6944
- log12.info("Command received", { action, requestId });
7227
+ log14.info("Command received", { action, requestId });
6945
7228
  if (action === "tool_result" && parsed.id) {
6946
7229
  const id = parsed.id;
6947
7230
  const result = parsed.result ?? "";
@@ -6950,7 +7233,7 @@ ${userMessage}` : header;
6950
7233
  this.pendingTools.delete(id);
6951
7234
  pending.resolve(result);
6952
7235
  } else if (!this.running) {
6953
- log12.info("Late tool_result while idle, dismissing", { id });
7236
+ log14.info("Late tool_result while idle, dismissing", { id });
6954
7237
  this.emit("completed", { success: true }, requestId);
6955
7238
  } else {
6956
7239
  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,
@@ -6155,6 +6447,7 @@ async function runTurn(params) {
6155
6447
  const contentBlocks = [];
6156
6448
  const thinkingBlockStartTimes = [];
6157
6449
  let thinkingCompleteCount = 0;
6450
+ let textBlockOpen = false;
6158
6451
  const toolInputAccumulators = /* @__PURE__ */ new Map();
6159
6452
  let stopReason = "end_turn";
6160
6453
  let subAgentText = "";
@@ -6265,7 +6558,7 @@ async function runTurn(params) {
6265
6558
  switch (event.type) {
6266
6559
  case "text": {
6267
6560
  const lastBlock = contentBlocks.at(-1);
6268
- if (lastBlock?.type === "text") {
6561
+ if (lastBlock?.type === "text" && textBlockOpen) {
6269
6562
  lastBlock.text += event.text;
6270
6563
  } else {
6271
6564
  contentBlocks.push({
@@ -6274,12 +6567,14 @@ async function runTurn(params) {
6274
6567
  startedAt: event.ts
6275
6568
  });
6276
6569
  }
6570
+ textBlockOpen = true;
6277
6571
  onEvent({ type: "text", text: event.text });
6278
6572
  break;
6279
6573
  }
6280
6574
  case "thinking":
6281
6575
  if (event.text === "") {
6282
6576
  thinkingBlockStartTimes.push(event.ts);
6577
+ textBlockOpen = false;
6283
6578
  }
6284
6579
  onEvent({ type: "thinking", text: event.text });
6285
6580
  break;
@@ -6321,7 +6616,7 @@ async function runTurn(params) {
6321
6616
  const tool = getToolByName(event.name);
6322
6617
  const wasStreamed = acc?.started ?? false;
6323
6618
  const isInputStreaming = !!tool?.streaming?.partialInput;
6324
- log8.info("Tool received", {
6619
+ log10.info("Tool received", {
6325
6620
  requestId,
6326
6621
  toolCallId: event.id,
6327
6622
  name: event.name
@@ -6414,7 +6709,7 @@ async function runTurn(params) {
6414
6709
  });
6415
6710
  return;
6416
6711
  }
6417
- log8.info("Tools executing", {
6712
+ log10.info("Tools executing", {
6418
6713
  requestId,
6419
6714
  count: toolCalls.length,
6420
6715
  tools: toolCalls.map((tc) => tc.name)
@@ -6461,7 +6756,7 @@ async function runTurn(params) {
6461
6756
  let result;
6462
6757
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
6463
6758
  saveSession(state);
6464
- log8.info("Waiting for external tool result", {
6759
+ log10.info("Waiting for external tool result", {
6465
6760
  requestId,
6466
6761
  toolCallId: tc.id,
6467
6762
  name: tc.name
@@ -6528,7 +6823,7 @@ async function runTurn(params) {
6528
6823
  if (!tc.input.background) {
6529
6824
  toolRegistry?.unregister(tc.id);
6530
6825
  }
6531
- log8.info("Tool completed", {
6826
+ log10.info("Tool completed", {
6532
6827
  requestId,
6533
6828
  toolCallId: tc.id,
6534
6829
  name: tc.name,
@@ -6542,6 +6837,9 @@ async function runTurn(params) {
6542
6837
  result: r.result,
6543
6838
  isError: r.isError
6544
6839
  });
6840
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
6841
+ triggerBrandExtraction(apiConfig);
6842
+ }
6545
6843
  return r;
6546
6844
  })
6547
6845
  );
@@ -6581,7 +6879,7 @@ async function runTurn(params) {
6581
6879
  }
6582
6880
  }
6583
6881
  }
6584
- var log8, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
6882
+ var log10, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
6585
6883
  var init_agent = __esm({
6586
6884
  "src/agent.ts"() {
6587
6885
  "use strict";
@@ -6595,7 +6893,9 @@ var init_agent = __esm({
6595
6893
  init_cleanMessages();
6596
6894
  init_tools6();
6597
6895
  init_sentinel();
6598
- log8 = createLogger("agent");
6896
+ init_trigger2();
6897
+ log10 = createLogger("agent");
6898
+ BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
6599
6899
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
6600
6900
  "promptUser",
6601
6901
  "setProjectOnboardingState",
@@ -6616,16 +6916,16 @@ var init_agent = __esm({
6616
6916
  });
6617
6917
 
6618
6918
  // src/config.ts
6619
- import fs19 from "fs";
6620
- import path9 from "path";
6919
+ import fs20 from "fs";
6920
+ import path10 from "path";
6621
6921
  import os from "os";
6622
6922
  function loadConfigFile() {
6623
6923
  try {
6624
- const raw = fs19.readFileSync(CONFIG_PATH, "utf-8");
6625
- log9.debug("Loaded config file", { path: CONFIG_PATH });
6924
+ const raw = fs20.readFileSync(CONFIG_PATH, "utf-8");
6925
+ log11.debug("Loaded config file", { path: CONFIG_PATH });
6626
6926
  return JSON.parse(raw);
6627
6927
  } catch (err) {
6628
- log9.debug("No config file found", {
6928
+ log11.debug("No config file found", {
6629
6929
  path: CONFIG_PATH,
6630
6930
  error: err.message
6631
6931
  });
@@ -6639,26 +6939,26 @@ function resolveConfig(flags2) {
6639
6939
  const apiKey = flags2?.apiKey || process.env.MINDSTUDIO_API_KEY || env?.apiKey || "";
6640
6940
  const baseUrl2 = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
6641
6941
  if (!apiKey) {
6642
- log9.error("No API key found");
6942
+ log11.error("No API key found");
6643
6943
  throw new Error(
6644
6944
  "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
6645
6945
  );
6646
6946
  }
6647
6947
  const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
6648
- log9.info("Config resolved", {
6948
+ log11.info("Config resolved", {
6649
6949
  baseUrl: baseUrl2,
6650
6950
  keySource,
6651
6951
  environment: activeEnv
6652
6952
  });
6653
6953
  return { apiKey, baseUrl: baseUrl2 };
6654
6954
  }
6655
- var log9, CONFIG_PATH, DEFAULT_BASE_URL;
6955
+ var log11, CONFIG_PATH, DEFAULT_BASE_URL;
6656
6956
  var init_config = __esm({
6657
6957
  "src/config.ts"() {
6658
6958
  "use strict";
6659
6959
  init_logger();
6660
- log9 = createLogger("config");
6661
- CONFIG_PATH = path9.join(
6960
+ log11 = createLogger("config");
6961
+ CONFIG_PATH = path10.join(
6662
6962
  os.homedir(),
6663
6963
  ".mindstudio-local-tunnel",
6664
6964
  "config.json"
@@ -6668,12 +6968,12 @@ var init_config = __esm({
6668
6968
  });
6669
6969
 
6670
6970
  // src/toolRegistry.ts
6671
- var log10, ToolRegistry;
6971
+ var log12, ToolRegistry;
6672
6972
  var init_toolRegistry = __esm({
6673
6973
  "src/toolRegistry.ts"() {
6674
6974
  "use strict";
6675
6975
  init_logger();
6676
- log10 = createLogger("tool-registry");
6976
+ log12 = createLogger("tool-registry");
6677
6977
  ToolRegistry = class {
6678
6978
  entries = /* @__PURE__ */ new Map();
6679
6979
  onEvent;
@@ -6699,7 +6999,7 @@ var init_toolRegistry = __esm({
6699
6999
  if (!entry) {
6700
7000
  return false;
6701
7001
  }
6702
- log10.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
7002
+ log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6703
7003
  entry.abortController.abort(mode);
6704
7004
  if (mode === "graceful") {
6705
7005
  const partial = entry.getPartialResult?.() ?? "";
@@ -6732,7 +7032,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6732
7032
  if (!entry) {
6733
7033
  return false;
6734
7034
  }
6735
- log10.info("Tool restarted", { toolCallId: id, name: entry.name });
7035
+ log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6736
7036
  entry.abortController.abort("restart");
6737
7037
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6738
7038
  this.onEvent?.({
@@ -6798,7 +7098,7 @@ async function persistAttachments(attachments) {
6798
7098
  }
6799
7099
  const buffer = Buffer.from(await res.arrayBuffer());
6800
7100
  await writeFile(localPath, buffer);
6801
- log11.info("Attachment saved", {
7101
+ log13.info("Attachment saved", {
6802
7102
  filename: name,
6803
7103
  path: localPath,
6804
7104
  bytes: buffer.length
@@ -6812,7 +7112,7 @@ async function persistAttachments(attachments) {
6812
7112
  if (textRes.ok) {
6813
7113
  extractedTextPath = `${localPath}.txt`;
6814
7114
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6815
- log11.info("Extracted text saved", { path: extractedTextPath });
7115
+ log13.info("Extracted text saved", { path: extractedTextPath });
6816
7116
  }
6817
7117
  } catch {
6818
7118
  }
@@ -6857,12 +7157,12 @@ function buildUploadHeader(results) {
6857
7157
  return `[Uploaded files]
6858
7158
  ${lines.join("\n")}`;
6859
7159
  }
6860
- var log11, UPLOADS_DIR, IMAGE_EXTENSIONS;
7160
+ var log13, UPLOADS_DIR, IMAGE_EXTENSIONS;
6861
7161
  var init_attachments = __esm({
6862
7162
  "src/headless/attachments.ts"() {
6863
7163
  "use strict";
6864
7164
  init_logger();
6865
- log11 = createLogger("headless:attachments");
7165
+ log13 = createLogger("headless:attachments");
6866
7166
  UPLOADS_DIR = "src/.user-uploads";
6867
7167
  IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
6868
7168
  }
@@ -7041,7 +7341,7 @@ __export(headless_exports, {
7041
7341
  HeadlessSession: () => HeadlessSession
7042
7342
  });
7043
7343
  import { createInterface } from "readline";
7044
- var log12, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
7344
+ var log14, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
7045
7345
  var init_headless = __esm({
7046
7346
  "src/headless/index.ts"() {
7047
7347
  "use strict";
@@ -7050,6 +7350,7 @@ var init_headless = __esm({
7050
7350
  init_prompt();
7051
7351
  init_trigger();
7052
7352
  init_compaction();
7353
+ init_trigger2();
7053
7354
  init_lsp();
7054
7355
  init_agent();
7055
7356
  init_session();
@@ -7060,7 +7361,7 @@ var init_headless = __esm({
7060
7361
  init_messageQueue();
7061
7362
  init_resolve();
7062
7363
  init_sentinel();
7063
- log12 = createLogger("headless");
7364
+ log14 = createLogger("headless");
7064
7365
  EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
7065
7366
  USER_FACING_TOOLS = /* @__PURE__ */ new Set([
7066
7367
  "promptUser",
@@ -7132,6 +7433,7 @@ var init_headless = __esm({
7132
7433
  ...this.queueFields()
7133
7434
  });
7134
7435
  }
7436
+ triggerBrandExtraction(this.config);
7135
7437
  this.toolRegistry.onEvent = this.onEvent;
7136
7438
  this.readline = createInterface({ input: process.stdin });
7137
7439
  this.readline.on("line", this.handleStdinLine);
@@ -7231,7 +7533,7 @@ var init_headless = __esm({
7231
7533
  }
7232
7534
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
7233
7535
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
7234
- log12.info("Background complete", {
7536
+ log14.info("Background complete", {
7235
7537
  toolCallId,
7236
7538
  name,
7237
7539
  requestId: this.currentRequestId
@@ -7452,7 +7754,7 @@ var init_headless = __esm({
7452
7754
  this.turnStart = Date.now();
7453
7755
  const attachments = parsed.attachments;
7454
7756
  if (attachments?.length) {
7455
- log12.info("Message has attachments", {
7757
+ log14.info("Message has attachments", {
7456
7758
  count: attachments.length,
7457
7759
  urls: attachments.map((a) => a.url)
7458
7760
  });
@@ -7469,7 +7771,7 @@ var init_headless = __esm({
7469
7771
  ${userMessage}` : header;
7470
7772
  }
7471
7773
  } catch (err) {
7472
- log12.warn("Attachment persistence failed", { error: err.message });
7774
+ log14.warn("Attachment persistence failed", { error: err.message });
7473
7775
  }
7474
7776
  }
7475
7777
  let resolved = null;
@@ -7527,7 +7829,7 @@ ${userMessage}` : header;
7527
7829
  error: "Turn ended unexpectedly"
7528
7830
  });
7529
7831
  }
7530
- log12.info("Turn complete", {
7832
+ log14.info("Turn complete", {
7531
7833
  requestId,
7532
7834
  durationMs: Date.now() - this.turnStart
7533
7835
  });
@@ -7539,7 +7841,7 @@ ${userMessage}` : header;
7539
7841
  error: err.message
7540
7842
  });
7541
7843
  }
7542
- log12.warn("Command failed", {
7844
+ log14.warn("Command failed", {
7543
7845
  action: "message",
7544
7846
  requestId,
7545
7847
  error: err.message
@@ -7674,7 +7976,7 @@ ${userMessage}` : header;
7674
7976
  return;
7675
7977
  }
7676
7978
  const { action, requestId } = parsed;
7677
- log12.info("Command received", { action, requestId });
7979
+ log14.info("Command received", { action, requestId });
7678
7980
  if (action === "tool_result" && parsed.id) {
7679
7981
  const id = parsed.id;
7680
7982
  const result = parsed.result ?? "";
@@ -7683,7 +7985,7 @@ ${userMessage}` : header;
7683
7985
  this.pendingTools.delete(id);
7684
7986
  pending.resolve(result);
7685
7987
  } else if (!this.running) {
7686
- log12.info("Late tool_result while idle, dismissing", { id });
7988
+ log14.info("Late tool_result while idle, dismissing", { id });
7687
7989
  this.emit("completed", { success: true }, requestId);
7688
7990
  } else {
7689
7991
  this.earlyResults.set(id, result);
@@ -7811,8 +8113,8 @@ ${userMessage}` : header;
7811
8113
  // src/index.tsx
7812
8114
  import { render } from "ink";
7813
8115
  import os2 from "os";
7814
- import fs20 from "fs";
7815
- import path10 from "path";
8116
+ import fs21 from "fs";
8117
+ import path11 from "path";
7816
8118
 
7817
8119
  // src/tui/App.tsx
7818
8120
  import { useState as useState2, useCallback, useRef } from "react";
@@ -8130,8 +8432,8 @@ for (let i = 0; i < args.length; i++) {
8130
8432
  var startupLog = createLogger("startup");
8131
8433
  function printDebugInfo(config) {
8132
8434
  const pkg = JSON.parse(
8133
- fs20.readFileSync(
8134
- path10.join(import.meta.dirname, "..", "package.json"),
8435
+ fs21.readFileSync(
8436
+ path11.join(import.meta.dirname, "..", "package.json"),
8135
8437
  "utf-8"
8136
8438
  )
8137
8439
  );
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.160",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",