@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.
- package/dist/automatedActions/buildFromInitialSpec.md +2 -2
- package/dist/brandExtraction/extract.md +51 -0
- package/dist/headless.js +324 -43
- package/dist/index.js +353 -53
- package/package.json +1 -1
|
@@ -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
|
-
|
|
9
|
+
Get a quick architecture sanity check from `codeSanityCheck` to make sure your approach holds up.
|
|
10
10
|
|
|
11
|
-
|
|
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
|
|
2698
|
+
let path11;
|
|
2699
2699
|
if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
|
|
2700
2700
|
prompt = promptOrOptions.prompt;
|
|
2701
2701
|
existingUrl = promptOrOptions.imageUrl;
|
|
2702
|
-
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
2756
|
+
if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
|
|
2757
2757
|
return;
|
|
2758
2758
|
}
|
|
2759
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4505
|
+
dirty2 = true;
|
|
4506
4506
|
}
|
|
4507
4507
|
}
|
|
4508
|
-
if (
|
|
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/
|
|
5202
|
+
// src/brandExtraction/index.ts
|
|
5203
5203
|
import fs19 from "fs";
|
|
5204
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
5532
|
+
fs20.writeFileSync(
|
|
5258
5533
|
SESSION_FILE,
|
|
5259
5534
|
JSON.stringify({ messages: state.messages }, null, 2),
|
|
5260
5535
|
"utf-8"
|
|
5261
5536
|
);
|
|
5262
|
-
|
|
5537
|
+
log10.info("Session saved", { messageCount: state.messages.length });
|
|
5263
5538
|
} catch (err) {
|
|
5264
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
5698
|
-
|
|
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:
|
|
5984
|
+
startedAt: thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts,
|
|
5708
5985
|
completedAt: event.ts
|
|
5709
5986
|
});
|
|
5710
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|