@mindstudio-ai/remy 0.1.157 → 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,
@@ -5570,7 +5846,8 @@ async function runTurn(params) {
5570
5846
  return;
5571
5847
  }
5572
5848
  const contentBlocks = [];
5573
- let thinkingStartedAt = 0;
5849
+ const thinkingBlockStartTimes = [];
5850
+ let thinkingCompleteCount = 0;
5574
5851
  const toolInputAccumulators = /* @__PURE__ */ new Map();
5575
5852
  let stopReason = "end_turn";
5576
5853
  let subAgentText = "";
@@ -5694,8 +5971,8 @@ async function runTurn(params) {
5694
5971
  break;
5695
5972
  }
5696
5973
  case "thinking":
5697
- if (!thinkingStartedAt) {
5698
- thinkingStartedAt = event.ts;
5974
+ if (event.text === "") {
5975
+ thinkingBlockStartTimes.push(event.ts);
5699
5976
  }
5700
5977
  onEvent({ type: "thinking", text: event.text });
5701
5978
  break;
@@ -5704,10 +5981,10 @@ async function runTurn(params) {
5704
5981
  type: "thinking",
5705
5982
  thinking: event.thinking,
5706
5983
  signature: event.signature,
5707
- startedAt: thinkingStartedAt,
5984
+ startedAt: thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts,
5708
5985
  completedAt: event.ts
5709
5986
  });
5710
- thinkingStartedAt = 0;
5987
+ thinkingCompleteCount++;
5711
5988
  break;
5712
5989
  case "tool_input_delta": {
5713
5990
  const acc = getOrCreateAccumulator2(event.id, event.name);
@@ -5737,7 +6014,7 @@ async function runTurn(params) {
5737
6014
  const tool = getToolByName(event.name);
5738
6015
  const wasStreamed = acc?.started ?? false;
5739
6016
  const isInputStreaming = !!tool?.streaming?.partialInput;
5740
- log9.info("Tool received", {
6017
+ log11.info("Tool received", {
5741
6018
  requestId,
5742
6019
  toolCallId: event.id,
5743
6020
  name: event.name
@@ -5830,7 +6107,7 @@ async function runTurn(params) {
5830
6107
  });
5831
6108
  return;
5832
6109
  }
5833
- log9.info("Tools executing", {
6110
+ log11.info("Tools executing", {
5834
6111
  requestId,
5835
6112
  count: toolCalls.length,
5836
6113
  tools: toolCalls.map((tc) => tc.name)
@@ -5877,7 +6154,7 @@ async function runTurn(params) {
5877
6154
  let result;
5878
6155
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
5879
6156
  saveSession(state);
5880
- log9.info("Waiting for external tool result", {
6157
+ log11.info("Waiting for external tool result", {
5881
6158
  requestId,
5882
6159
  toolCallId: tc.id,
5883
6160
  name: tc.name
@@ -5944,7 +6221,7 @@ async function runTurn(params) {
5944
6221
  if (!tc.input.background) {
5945
6222
  toolRegistry?.unregister(tc.id);
5946
6223
  }
5947
- log9.info("Tool completed", {
6224
+ log11.info("Tool completed", {
5948
6225
  requestId,
5949
6226
  toolCallId: tc.id,
5950
6227
  name: tc.name,
@@ -5958,6 +6235,9 @@ async function runTurn(params) {
5958
6235
  result: r.result,
5959
6236
  isError: r.isError
5960
6237
  });
6238
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
6239
+ triggerBrandExtraction(apiConfig);
6240
+ }
5961
6241
  return r;
5962
6242
  })
5963
6243
  );
@@ -5999,7 +6279,7 @@ async function runTurn(params) {
5999
6279
  }
6000
6280
 
6001
6281
  // src/toolRegistry.ts
6002
- var log10 = createLogger("tool-registry");
6282
+ var log12 = createLogger("tool-registry");
6003
6283
  var ToolRegistry = class {
6004
6284
  entries = /* @__PURE__ */ new Map();
6005
6285
  onEvent;
@@ -6025,7 +6305,7 @@ var ToolRegistry = class {
6025
6305
  if (!entry) {
6026
6306
  return false;
6027
6307
  }
6028
- log10.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6308
+ log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6029
6309
  entry.abortController.abort(mode);
6030
6310
  if (mode === "graceful") {
6031
6311
  const partial = entry.getPartialResult?.() ?? "";
@@ -6058,7 +6338,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6058
6338
  if (!entry) {
6059
6339
  return false;
6060
6340
  }
6061
- log10.info("Tool restarted", { toolCallId: id, name: entry.name });
6341
+ log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6062
6342
  entry.abortController.abort("restart");
6063
6343
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6064
6344
  this.onEvent?.({
@@ -6077,7 +6357,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6077
6357
  import { mkdirSync, existsSync } from "fs";
6078
6358
  import { writeFile } from "fs/promises";
6079
6359
  import { basename, join, extname } from "path";
6080
- var log11 = createLogger("headless:attachments");
6360
+ var log13 = createLogger("headless:attachments");
6081
6361
  var UPLOADS_DIR = "src/.user-uploads";
6082
6362
  function filenameFromUrl(url) {
6083
6363
  try {
@@ -6125,7 +6405,7 @@ async function persistAttachments(attachments) {
6125
6405
  }
6126
6406
  const buffer = Buffer.from(await res.arrayBuffer());
6127
6407
  await writeFile(localPath, buffer);
6128
- log11.info("Attachment saved", {
6408
+ log13.info("Attachment saved", {
6129
6409
  filename: name,
6130
6410
  path: localPath,
6131
6411
  bytes: buffer.length
@@ -6139,7 +6419,7 @@ async function persistAttachments(attachments) {
6139
6419
  if (textRes.ok) {
6140
6420
  extractedTextPath = `${localPath}.txt`;
6141
6421
  await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6142
- log11.info("Extracted text saved", { path: extractedTextPath });
6422
+ log13.info("Extracted text saved", { path: extractedTextPath });
6143
6423
  }
6144
6424
  } catch {
6145
6425
  }
@@ -6326,7 +6606,7 @@ function resolveAction(text) {
6326
6606
  }
6327
6607
 
6328
6608
  // src/headless/index.ts
6329
- var log12 = createLogger("headless");
6609
+ var log14 = createLogger("headless");
6330
6610
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6331
6611
  var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6332
6612
  "promptUser",
@@ -6398,6 +6678,7 @@ var HeadlessSession = class {
6398
6678
  ...this.queueFields()
6399
6679
  });
6400
6680
  }
6681
+ triggerBrandExtraction(this.config);
6401
6682
  this.toolRegistry.onEvent = this.onEvent;
6402
6683
  this.readline = createInterface({ input: process.stdin });
6403
6684
  this.readline.on("line", this.handleStdinLine);
@@ -6497,7 +6778,7 @@ var HeadlessSession = class {
6497
6778
  }
6498
6779
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
6499
6780
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6500
- log12.info("Background complete", {
6781
+ log14.info("Background complete", {
6501
6782
  toolCallId,
6502
6783
  name,
6503
6784
  requestId: this.currentRequestId
@@ -6718,7 +6999,7 @@ var HeadlessSession = class {
6718
6999
  this.turnStart = Date.now();
6719
7000
  const attachments = parsed.attachments;
6720
7001
  if (attachments?.length) {
6721
- log12.info("Message has attachments", {
7002
+ log14.info("Message has attachments", {
6722
7003
  count: attachments.length,
6723
7004
  urls: attachments.map((a) => a.url)
6724
7005
  });
@@ -6735,7 +7016,7 @@ var HeadlessSession = class {
6735
7016
  ${userMessage}` : header;
6736
7017
  }
6737
7018
  } catch (err) {
6738
- log12.warn("Attachment persistence failed", { error: err.message });
7019
+ log14.warn("Attachment persistence failed", { error: err.message });
6739
7020
  }
6740
7021
  }
6741
7022
  let resolved = null;
@@ -6793,7 +7074,7 @@ ${userMessage}` : header;
6793
7074
  error: "Turn ended unexpectedly"
6794
7075
  });
6795
7076
  }
6796
- log12.info("Turn complete", {
7077
+ log14.info("Turn complete", {
6797
7078
  requestId,
6798
7079
  durationMs: Date.now() - this.turnStart
6799
7080
  });
@@ -6805,7 +7086,7 @@ ${userMessage}` : header;
6805
7086
  error: err.message
6806
7087
  });
6807
7088
  }
6808
- log12.warn("Command failed", {
7089
+ log14.warn("Command failed", {
6809
7090
  action: "message",
6810
7091
  requestId,
6811
7092
  error: err.message
@@ -6940,7 +7221,7 @@ ${userMessage}` : header;
6940
7221
  return;
6941
7222
  }
6942
7223
  const { action, requestId } = parsed;
6943
- log12.info("Command received", { action, requestId });
7224
+ log14.info("Command received", { action, requestId });
6944
7225
  if (action === "tool_result" && parsed.id) {
6945
7226
  const id = parsed.id;
6946
7227
  const result = parsed.result ?? "";
@@ -6949,7 +7230,7 @@ ${userMessage}` : header;
6949
7230
  this.pendingTools.delete(id);
6950
7231
  pending.resolve(result);
6951
7232
  } else if (!this.running) {
6952
- log12.info("Late tool_result while idle, dismissing", { id });
7233
+ log14.info("Late tool_result while idle, dismissing", { id });
6953
7234
  this.emit("completed", { success: true }, requestId);
6954
7235
  } else {
6955
7236
  this.earlyResults.set(id, result);