@mindstudio-ai/remy 0.1.184 → 0.1.186

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/headless.js CHANGED
@@ -2902,6 +2902,82 @@ function acquireBrowserLock() {
2902
2902
  return wait.then(() => release);
2903
2903
  }
2904
2904
 
2905
+ // src/toolRegistry.ts
2906
+ var log5 = createLogger("tool-registry");
2907
+ var USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
2908
+ var ToolRegistry = class {
2909
+ entries = /* @__PURE__ */ new Map();
2910
+ onEvent;
2911
+ register(entry) {
2912
+ this.entries.set(entry.id, entry);
2913
+ }
2914
+ unregister(id) {
2915
+ this.entries.delete(id);
2916
+ }
2917
+ get(id) {
2918
+ return this.entries.get(id);
2919
+ }
2920
+ /**
2921
+ * Stop a running tool.
2922
+ *
2923
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
2924
+ * - hard: abort and settle with a generic error
2925
+ *
2926
+ * Returns true if the tool was found and stopped.
2927
+ */
2928
+ stop(id, mode) {
2929
+ const entry = this.entries.get(id);
2930
+ if (!entry) {
2931
+ return false;
2932
+ }
2933
+ log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
2934
+ entry.abortController.abort(mode);
2935
+ if (mode === "graceful") {
2936
+ const partial = entry.getPartialResult?.() ?? "";
2937
+ const result = partial ? `[INTERRUPTED]
2938
+
2939
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
2940
+ entry.settle(result, false);
2941
+ } else {
2942
+ entry.settle(USER_CANCELLED_RESULT, true);
2943
+ }
2944
+ this.onEvent?.({
2945
+ type: "tool_stopped",
2946
+ id: entry.id,
2947
+ name: entry.name,
2948
+ mode,
2949
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
2950
+ });
2951
+ this.entries.delete(id);
2952
+ return true;
2953
+ }
2954
+ /**
2955
+ * Restart a running tool with the same or patched input.
2956
+ * The original controllable promise stays pending and settles
2957
+ * when the new execution finishes.
2958
+ *
2959
+ * Returns true if the tool was found and restarted.
2960
+ */
2961
+ restart(id, patchedInput) {
2962
+ const entry = this.entries.get(id);
2963
+ if (!entry) {
2964
+ return false;
2965
+ }
2966
+ log5.info("Tool restarted", { toolCallId: id, name: entry.name });
2967
+ entry.abortController.abort("restart");
2968
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
2969
+ this.onEvent?.({
2970
+ type: "tool_restarted",
2971
+ id: entry.id,
2972
+ name: entry.name,
2973
+ input: newInput,
2974
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
2975
+ });
2976
+ entry.rerun(newInput);
2977
+ return true;
2978
+ }
2979
+ };
2980
+
2905
2981
  // src/statusWatcher.ts
2906
2982
  function startStatusWatcher(config) {
2907
2983
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
@@ -3147,7 +3223,7 @@ ${content}` : attachmentHeader;
3147
3223
  }
3148
3224
 
3149
3225
  // src/subagents/runner.ts
3150
- var log5 = createLogger("sub-agent");
3226
+ var log6 = createLogger("sub-agent");
3151
3227
  async function runSubAgent(config) {
3152
3228
  const {
3153
3229
  system,
@@ -3174,7 +3250,7 @@ async function runSubAgent(config) {
3174
3250
  const signal = background ? bgAbort.signal : parentSignal;
3175
3251
  const agentName = subAgentId || "sub-agent";
3176
3252
  const runStart = Date.now();
3177
- log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3253
+ log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3178
3254
  const emit = (e) => {
3179
3255
  onEvent({ ...e, parentToolId });
3180
3256
  };
@@ -3209,7 +3285,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3209
3285
  messages: thisInvocation()
3210
3286
  };
3211
3287
  }
3212
- return { text: "Error: cancelled", messages: thisInvocation() };
3288
+ return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3213
3289
  }
3214
3290
  let lastToolResult = "";
3215
3291
  while (true) {
@@ -3391,7 +3467,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3391
3467
  ...hasArtifacts ? { artifacts } : {}
3392
3468
  };
3393
3469
  }
3394
- log5.info("Tools executing", {
3470
+ log6.info("Tools executing", {
3395
3471
  requestId,
3396
3472
  parentToolId,
3397
3473
  count: toolCalls.length,
@@ -3401,7 +3477,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3401
3477
  const results = await Promise.all(
3402
3478
  toolCalls.map(async (tc) => {
3403
3479
  if (signal?.aborted) {
3404
- return { id: tc.id, result: "Error: cancelled", isError: true };
3480
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
3405
3481
  }
3406
3482
  let settle;
3407
3483
  const resultPromise = new Promise((res) => {
@@ -3468,7 +3544,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3468
3544
  run2(tc.input);
3469
3545
  const r = await resultPromise;
3470
3546
  toolRegistry?.unregister(tc.id);
3471
- log5.info("Tool completed", {
3547
+ log6.info("Tool completed", {
3472
3548
  requestId,
3473
3549
  parentToolId,
3474
3550
  toolCallId: tc.id,
@@ -3519,7 +3595,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3519
3595
  const wrapRun = async () => {
3520
3596
  try {
3521
3597
  const result = await run();
3522
- log5.info("Sub-agent complete", {
3598
+ log6.info("Sub-agent complete", {
3523
3599
  requestId,
3524
3600
  parentToolId,
3525
3601
  agentName,
@@ -3528,7 +3604,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3528
3604
  });
3529
3605
  return result;
3530
3606
  } catch (err) {
3531
- log5.warn("Sub-agent error", {
3607
+ log6.warn("Sub-agent error", {
3532
3608
  requestId,
3533
3609
  parentToolId,
3534
3610
  agentName,
@@ -3540,7 +3616,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3540
3616
  if (!background) {
3541
3617
  return wrapRun();
3542
3618
  }
3543
- log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3619
+ log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3544
3620
  toolRegistry?.register({
3545
3621
  id: parentToolId,
3546
3622
  name: agentName,
@@ -3722,14 +3798,14 @@ ${appSpec}
3722
3798
  // src/models/surfaces.ts
3723
3799
  var MODEL_SURFACES = {
3724
3800
  parent: {
3725
- default: "claude-4-7-opus",
3801
+ default: "claude-4-8-opus",
3726
3802
  label: "Remy",
3727
3803
  description: "The main Remy agent you chat with about your product. Writes code and manages delegation to other agents.",
3728
3804
  modelType: "text",
3729
3805
  userPickable: true
3730
3806
  },
3731
3807
  visualDesignExpert: {
3732
- default: "claude-4-7-opus",
3808
+ default: "claude-4-8-opus",
3733
3809
  label: "Design Agent",
3734
3810
  description: "Designs your product's interfaces, including components, layouts, typography, color, and visual identity.",
3735
3811
  modelType: "text",
@@ -3796,6 +3872,7 @@ var MODEL_SURFACES = {
3796
3872
  };
3797
3873
  var ALLOWED_MODELS_BY_TYPE = {
3798
3874
  text: [
3875
+ "claude-4-8-opus",
3799
3876
  "claude-4-7-opus",
3800
3877
  "claude-4-6-opus",
3801
3878
  "claude-4-6-sonnet",
@@ -3813,7 +3890,7 @@ function resolveModel(surfaceId, models, fallback) {
3813
3890
  }
3814
3891
 
3815
3892
  // src/subagents/browserAutomation/index.ts
3816
- var log6 = createLogger("browser-automation");
3893
+ var log7 = createLogger("browser-automation");
3817
3894
  async function runBrowserAutomation(task, context) {
3818
3895
  const release = await acquireBrowserLock();
3819
3896
  try {
@@ -3905,7 +3982,7 @@ async function runBrowserAutomation(task, context) {
3905
3982
  }
3906
3983
  }
3907
3984
  } catch {
3908
- log6.debug("Failed to parse batch analysis result", {
3985
+ log7.debug("Failed to parse batch analysis result", {
3909
3986
  batchResult
3910
3987
  });
3911
3988
  }
@@ -5572,7 +5649,7 @@ function executeTool(name, input, context) {
5572
5649
  }
5573
5650
 
5574
5651
  // src/compaction/trigger.ts
5575
- var log7 = createLogger("compaction:trigger");
5652
+ var log8 = createLogger("compaction:trigger");
5576
5653
  var pendingSummaries = [];
5577
5654
  var inflightCompaction = null;
5578
5655
  function getPendingSummaries() {
@@ -5599,11 +5676,11 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5599
5676
  ).then((summaries) => {
5600
5677
  pendingSummaries.push(...summaries);
5601
5678
  listener?.({ type: "complete", requestId });
5602
- log7.info("Compaction complete");
5679
+ log8.info("Compaction complete");
5603
5680
  }).catch((err) => {
5604
5681
  const message = err.message || "Compaction failed";
5605
5682
  listener?.({ type: "complete", error: message, requestId });
5606
- log7.error("Compaction failed", { error: message });
5683
+ log8.error("Compaction failed", { error: message });
5607
5684
  throw err;
5608
5685
  }).finally(() => {
5609
5686
  inflightCompaction = null;
@@ -5615,7 +5692,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5615
5692
  import fs20 from "fs";
5616
5693
  import path10 from "path";
5617
5694
  import { createHash } from "crypto";
5618
- var log8 = createLogger("brandExtraction");
5695
+ var log9 = createLogger("brandExtraction");
5619
5696
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
5620
5697
  var BRAND_FILE = ".remy-brand.json";
5621
5698
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -5623,17 +5700,17 @@ async function runExtraction(apiConfig, model) {
5623
5700
  const inputHash = computeInputHash();
5624
5701
  const cached2 = readCache();
5625
5702
  if (cached2 && cached2.inputHash === inputHash) {
5626
- log8.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
5703
+ log9.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
5627
5704
  return null;
5628
5705
  }
5629
- log8.info("Extracting brand", { inputHash });
5706
+ log9.info("Extracting brand", { inputHash });
5630
5707
  const brand = await extractBrand(apiConfig, model);
5631
5708
  if (!brand) {
5632
- log8.warn("Brand extraction failed \u2014 leaving cache untouched");
5709
+ log9.warn("Brand extraction failed \u2014 leaving cache untouched");
5633
5710
  return null;
5634
5711
  }
5635
5712
  persistBrand(brand, inputHash);
5636
- log8.info("Brand persisted", { inputHash });
5713
+ log9.info("Brand persisted", { inputHash });
5637
5714
  return brand;
5638
5715
  }
5639
5716
  function computeInputHash() {
@@ -5699,7 +5776,7 @@ function parseFrontmatter3(filePath) {
5699
5776
  async function extractBrand(apiConfig, model) {
5700
5777
  const corpus = buildCorpus();
5701
5778
  if (!corpus.trim()) {
5702
- log8.debug("No spec corpus \u2014 emitting empty brand");
5779
+ log9.debug("No spec corpus \u2014 emitting empty brand");
5703
5780
  return { version: 1 };
5704
5781
  }
5705
5782
  let responseText = "";
@@ -5730,17 +5807,17 @@ async function extractBrand(apiConfig, model) {
5730
5807
  toolNames: []
5731
5808
  });
5732
5809
  } else if (event.type === "error") {
5733
- log8.error("Brand extraction stream error", { error: event.error });
5810
+ log9.error("Brand extraction stream error", { error: event.error });
5734
5811
  return null;
5735
5812
  }
5736
5813
  }
5737
5814
  } catch (err) {
5738
- log8.error("Brand extraction threw", { error: err?.message });
5815
+ log9.error("Brand extraction threw", { error: err?.message });
5739
5816
  return null;
5740
5817
  }
5741
5818
  const parsed = parseJsonResponse(responseText);
5742
5819
  if (!parsed) {
5743
- log8.warn("Brand extraction returned unparseable JSON", {
5820
+ log9.warn("Brand extraction returned unparseable JSON", {
5744
5821
  preview: responseText.slice(0, 200)
5745
5822
  });
5746
5823
  return null;
@@ -5882,7 +5959,7 @@ function readCache() {
5882
5959
  }
5883
5960
 
5884
5961
  // src/brandExtraction/trigger.ts
5885
- var log9 = createLogger("brandExtraction:trigger");
5962
+ var log10 = createLogger("brandExtraction:trigger");
5886
5963
  var inflight = false;
5887
5964
  var dirty = false;
5888
5965
  function triggerBrandExtraction(apiConfig, model) {
@@ -5892,7 +5969,7 @@ function triggerBrandExtraction(apiConfig, model) {
5892
5969
  }
5893
5970
  inflight = true;
5894
5971
  void runExtraction(apiConfig, model).catch((err) => {
5895
- log9.error("Brand extraction failed", { error: err?.message });
5972
+ log10.error("Brand extraction failed", { error: err?.message });
5896
5973
  }).finally(() => {
5897
5974
  inflight = false;
5898
5975
  if (dirty) {
@@ -5905,7 +5982,7 @@ function triggerBrandExtraction(apiConfig, model) {
5905
5982
  // src/session.ts
5906
5983
  import fs21 from "fs";
5907
5984
  import path11 from "path";
5908
- var log10 = createLogger("session");
5985
+ var log11 = createLogger("session");
5909
5986
  var SESSION_FILE = ".remy-session.json";
5910
5987
  var ARCHIVE_DIR = ".logs/sessions";
5911
5988
  function loadSession(state) {
@@ -5917,7 +5994,7 @@ function loadSession(state) {
5917
5994
  }
5918
5995
  if (Array.isArray(data.messages) && data.messages.length > 0) {
5919
5996
  state.messages = sanitizeMessages(data.messages);
5920
- log10.info("Session loaded", {
5997
+ log11.info("Session loaded", {
5921
5998
  messageCount: state.messages.length,
5922
5999
  ...state.models && { models: state.models }
5923
6000
  });
@@ -5970,9 +6047,9 @@ function saveSession(state) {
5970
6047
  payload.models = state.models;
5971
6048
  }
5972
6049
  fs21.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
5973
- log10.info("Session saved", { messageCount: state.messages.length });
6050
+ log11.info("Session saved", { messageCount: state.messages.length });
5974
6051
  } catch (err) {
5975
- log10.warn("Session save failed", { error: err.message });
6052
+ log11.warn("Session save failed", { error: err.message });
5976
6053
  }
5977
6054
  }
5978
6055
  function clearSession(state) {
@@ -5983,10 +6060,10 @@ function clearSession(state) {
5983
6060
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5984
6061
  const dest = path11.join(ARCHIVE_DIR, `cleared-${ts}.json`);
5985
6062
  fs21.renameSync(SESSION_FILE, dest);
5986
- log10.info("Session archived on clear", { dest });
6063
+ log11.info("Session archived on clear", { dest });
5987
6064
  }
5988
6065
  } catch (err) {
5989
- log10.warn("Session archive on clear failed, deleting instead", {
6066
+ log11.warn("Session archive on clear failed, deleting instead", {
5990
6067
  error: err.message
5991
6068
  });
5992
6069
  try {
@@ -6190,7 +6267,7 @@ function friendlyError(raw) {
6190
6267
  }
6191
6268
 
6192
6269
  // src/agent.ts
6193
- var log11 = createLogger("agent");
6270
+ var log12 = createLogger("agent");
6194
6271
  var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
6195
6272
  function getTextContent(blocks) {
6196
6273
  return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
@@ -6239,7 +6316,7 @@ async function runTurn(params) {
6239
6316
  } = params;
6240
6317
  const tools2 = getToolDefinitions(onboardingState);
6241
6318
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
6242
- log11.info("Turn started", {
6319
+ log12.info("Turn started", {
6243
6320
  requestId,
6244
6321
  model,
6245
6322
  toolCount: tools2.length,
@@ -6492,7 +6569,7 @@ async function runTurn(params) {
6492
6569
  const tool = getToolByName(event.name);
6493
6570
  const wasStreamed = acc?.started ?? false;
6494
6571
  const isInputStreaming = !!tool?.streaming?.partialInput;
6495
- log11.info("Tool received", {
6572
+ log12.info("Tool received", {
6496
6573
  requestId,
6497
6574
  toolCallId: event.id,
6498
6575
  name: event.name
@@ -6606,7 +6683,7 @@ async function runTurn(params) {
6606
6683
  });
6607
6684
  return;
6608
6685
  }
6609
- log11.info("Tools executing", {
6686
+ log12.info("Tools executing", {
6610
6687
  requestId,
6611
6688
  count: toolCalls.length,
6612
6689
  tools: toolCalls.map((tc) => tc.name)
@@ -6626,7 +6703,7 @@ async function runTurn(params) {
6626
6703
  const results = await Promise.all(
6627
6704
  toolCalls.map(async (tc) => {
6628
6705
  if (signal?.aborted) {
6629
- return { id: tc.id, result: "Error: cancelled", isError: true };
6706
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
6630
6707
  }
6631
6708
  const toolStart = Date.now();
6632
6709
  let settle;
@@ -6645,7 +6722,7 @@ async function runTurn(params) {
6645
6722
  };
6646
6723
  const cascadeAbort = () => {
6647
6724
  toolAbort.abort();
6648
- safeSettle("Error: cancelled", true);
6725
+ safeSettle(USER_CANCELLED_RESULT, true);
6649
6726
  };
6650
6727
  signal?.addEventListener("abort", cascadeAbort, { once: true });
6651
6728
  const run = async (input) => {
@@ -6653,7 +6730,7 @@ async function runTurn(params) {
6653
6730
  let result;
6654
6731
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
6655
6732
  saveSession(state);
6656
- log11.info("Waiting for external tool result", {
6733
+ log12.info("Waiting for external tool result", {
6657
6734
  requestId,
6658
6735
  toolCallId: tc.id,
6659
6736
  name: tc.name
@@ -6721,7 +6798,7 @@ async function runTurn(params) {
6721
6798
  if (!tc.input.background) {
6722
6799
  toolRegistry?.unregister(tc.id);
6723
6800
  }
6724
- log11.info("Tool completed", {
6801
+ log12.info("Tool completed", {
6725
6802
  requestId,
6726
6803
  toolCallId: tc.id,
6727
6804
  name: tc.name,
@@ -6781,81 +6858,6 @@ async function runTurn(params) {
6781
6858
  }
6782
6859
  }
6783
6860
 
6784
- // src/toolRegistry.ts
6785
- var log12 = createLogger("tool-registry");
6786
- var ToolRegistry = class {
6787
- entries = /* @__PURE__ */ new Map();
6788
- onEvent;
6789
- register(entry) {
6790
- this.entries.set(entry.id, entry);
6791
- }
6792
- unregister(id) {
6793
- this.entries.delete(id);
6794
- }
6795
- get(id) {
6796
- return this.entries.get(id);
6797
- }
6798
- /**
6799
- * Stop a running tool.
6800
- *
6801
- * - graceful: abort and settle with [INTERRUPTED] + partial result
6802
- * - hard: abort and settle with a generic error
6803
- *
6804
- * Returns true if the tool was found and stopped.
6805
- */
6806
- stop(id, mode) {
6807
- const entry = this.entries.get(id);
6808
- if (!entry) {
6809
- return false;
6810
- }
6811
- log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6812
- entry.abortController.abort(mode);
6813
- if (mode === "graceful") {
6814
- const partial = entry.getPartialResult?.() ?? "";
6815
- const result = partial ? `[INTERRUPTED]
6816
-
6817
- ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6818
- entry.settle(result, false);
6819
- } else {
6820
- entry.settle("Error: tool was cancelled", true);
6821
- }
6822
- this.onEvent?.({
6823
- type: "tool_stopped",
6824
- id: entry.id,
6825
- name: entry.name,
6826
- mode,
6827
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
6828
- });
6829
- this.entries.delete(id);
6830
- return true;
6831
- }
6832
- /**
6833
- * Restart a running tool with the same or patched input.
6834
- * The original controllable promise stays pending and settles
6835
- * when the new execution finishes.
6836
- *
6837
- * Returns true if the tool was found and restarted.
6838
- */
6839
- restart(id, patchedInput) {
6840
- const entry = this.entries.get(id);
6841
- if (!entry) {
6842
- return false;
6843
- }
6844
- log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6845
- entry.abortController.abort("restart");
6846
- const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6847
- this.onEvent?.({
6848
- type: "tool_restarted",
6849
- id: entry.id,
6850
- name: entry.name,
6851
- input: newInput,
6852
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
6853
- });
6854
- entry.rerun(newInput);
6855
- return true;
6856
- }
6857
- };
6858
-
6859
6861
  // src/headless/attachments.ts
6860
6862
  import { mkdirSync, existsSync } from "fs";
6861
6863
  import { writeFile } from "fs/promises";
@@ -7804,7 +7806,7 @@ var HeadlessSession = class {
7804
7806
  }
7805
7807
  for (const [id, pending] of this.pendingTools) {
7806
7808
  clearTimeout(pending.timeout);
7807
- pending.resolve("Error: cancelled");
7809
+ pending.resolve(USER_CANCELLED_RESULT);
7808
7810
  this.pendingTools.delete(id);
7809
7811
  }
7810
7812
  return this.queue.drain();
package/dist/index.js CHANGED
@@ -2034,14 +2034,14 @@ var init_surfaces = __esm({
2034
2034
  "use strict";
2035
2035
  MODEL_SURFACES = {
2036
2036
  parent: {
2037
- default: "claude-4-7-opus",
2037
+ default: "claude-4-8-opus",
2038
2038
  label: "Remy",
2039
2039
  description: "The main Remy agent you chat with about your product. Writes code and manages delegation to other agents.",
2040
2040
  modelType: "text",
2041
2041
  userPickable: true
2042
2042
  },
2043
2043
  visualDesignExpert: {
2044
- default: "claude-4-7-opus",
2044
+ default: "claude-4-8-opus",
2045
2045
  label: "Design Agent",
2046
2046
  description: "Designs your product's interfaces, including components, layouts, typography, color, and visual identity.",
2047
2047
  modelType: "text",
@@ -2108,6 +2108,7 @@ var init_surfaces = __esm({
2108
2108
  };
2109
2109
  ALLOWED_MODELS_BY_TYPE = {
2110
2110
  text: [
2111
+ "claude-4-8-opus",
2111
2112
  "claude-4-7-opus",
2112
2113
  "claude-4-6-opus",
2113
2114
  "claude-4-6-sonnet",
@@ -3319,6 +3320,89 @@ var init_browserLock = __esm({
3319
3320
  }
3320
3321
  });
3321
3322
 
3323
+ // src/toolRegistry.ts
3324
+ var log5, USER_CANCELLED_RESULT, ToolRegistry;
3325
+ var init_toolRegistry = __esm({
3326
+ "src/toolRegistry.ts"() {
3327
+ "use strict";
3328
+ init_logger();
3329
+ log5 = createLogger("tool-registry");
3330
+ USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
3331
+ ToolRegistry = class {
3332
+ entries = /* @__PURE__ */ new Map();
3333
+ onEvent;
3334
+ register(entry) {
3335
+ this.entries.set(entry.id, entry);
3336
+ }
3337
+ unregister(id) {
3338
+ this.entries.delete(id);
3339
+ }
3340
+ get(id) {
3341
+ return this.entries.get(id);
3342
+ }
3343
+ /**
3344
+ * Stop a running tool.
3345
+ *
3346
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
3347
+ * - hard: abort and settle with a generic error
3348
+ *
3349
+ * Returns true if the tool was found and stopped.
3350
+ */
3351
+ stop(id, mode) {
3352
+ const entry = this.entries.get(id);
3353
+ if (!entry) {
3354
+ return false;
3355
+ }
3356
+ log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
3357
+ entry.abortController.abort(mode);
3358
+ if (mode === "graceful") {
3359
+ const partial = entry.getPartialResult?.() ?? "";
3360
+ const result = partial ? `[INTERRUPTED]
3361
+
3362
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3363
+ entry.settle(result, false);
3364
+ } else {
3365
+ entry.settle(USER_CANCELLED_RESULT, true);
3366
+ }
3367
+ this.onEvent?.({
3368
+ type: "tool_stopped",
3369
+ id: entry.id,
3370
+ name: entry.name,
3371
+ mode,
3372
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
3373
+ });
3374
+ this.entries.delete(id);
3375
+ return true;
3376
+ }
3377
+ /**
3378
+ * Restart a running tool with the same or patched input.
3379
+ * The original controllable promise stays pending and settles
3380
+ * when the new execution finishes.
3381
+ *
3382
+ * Returns true if the tool was found and restarted.
3383
+ */
3384
+ restart(id, patchedInput) {
3385
+ const entry = this.entries.get(id);
3386
+ if (!entry) {
3387
+ return false;
3388
+ }
3389
+ log5.info("Tool restarted", { toolCallId: id, name: entry.name });
3390
+ entry.abortController.abort("restart");
3391
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
3392
+ this.onEvent?.({
3393
+ type: "tool_restarted",
3394
+ id: entry.id,
3395
+ name: entry.name,
3396
+ input: newInput,
3397
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
3398
+ });
3399
+ entry.rerun(newInput);
3400
+ return true;
3401
+ }
3402
+ };
3403
+ }
3404
+ });
3405
+
3322
3406
  // src/statusWatcher.ts
3323
3407
  function startStatusWatcher(config) {
3324
3408
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
@@ -3606,7 +3690,7 @@ async function runSubAgent(config) {
3606
3690
  const signal = background ? bgAbort.signal : parentSignal;
3607
3691
  const agentName = subAgentId || "sub-agent";
3608
3692
  const runStart = Date.now();
3609
- log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3693
+ log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3610
3694
  const emit = (e) => {
3611
3695
  onEvent({ ...e, parentToolId });
3612
3696
  };
@@ -3641,7 +3725,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3641
3725
  messages: thisInvocation()
3642
3726
  };
3643
3727
  }
3644
- return { text: "Error: cancelled", messages: thisInvocation() };
3728
+ return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3645
3729
  }
3646
3730
  let lastToolResult = "";
3647
3731
  while (true) {
@@ -3823,7 +3907,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3823
3907
  ...hasArtifacts ? { artifacts } : {}
3824
3908
  };
3825
3909
  }
3826
- log5.info("Tools executing", {
3910
+ log6.info("Tools executing", {
3827
3911
  requestId,
3828
3912
  parentToolId,
3829
3913
  count: toolCalls.length,
@@ -3833,7 +3917,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3833
3917
  const results = await Promise.all(
3834
3918
  toolCalls.map(async (tc) => {
3835
3919
  if (signal?.aborted) {
3836
- return { id: tc.id, result: "Error: cancelled", isError: true };
3920
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
3837
3921
  }
3838
3922
  let settle;
3839
3923
  const resultPromise = new Promise((res) => {
@@ -3900,7 +3984,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3900
3984
  run2(tc.input);
3901
3985
  const r = await resultPromise;
3902
3986
  toolRegistry?.unregister(tc.id);
3903
- log5.info("Tool completed", {
3987
+ log6.info("Tool completed", {
3904
3988
  requestId,
3905
3989
  parentToolId,
3906
3990
  toolCallId: tc.id,
@@ -3951,7 +4035,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3951
4035
  const wrapRun = async () => {
3952
4036
  try {
3953
4037
  const result = await run();
3954
- log5.info("Sub-agent complete", {
4038
+ log6.info("Sub-agent complete", {
3955
4039
  requestId,
3956
4040
  parentToolId,
3957
4041
  agentName,
@@ -3960,7 +4044,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3960
4044
  });
3961
4045
  return result;
3962
4046
  } catch (err) {
3963
- log5.warn("Sub-agent error", {
4047
+ log6.warn("Sub-agent error", {
3964
4048
  requestId,
3965
4049
  parentToolId,
3966
4050
  agentName,
@@ -3972,7 +4056,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3972
4056
  if (!background) {
3973
4057
  return wrapRun();
3974
4058
  }
3975
- log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
4059
+ log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3976
4060
  toolRegistry?.register({
3977
4061
  id: parentToolId,
3978
4062
  name: agentName,
@@ -3999,16 +4083,17 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3999
4083
  });
4000
4084
  return { text: ack, messages: [], backgrounded: true };
4001
4085
  }
4002
- var log5;
4086
+ var log6;
4003
4087
  var init_runner = __esm({
4004
4088
  "src/subagents/runner.ts"() {
4005
4089
  "use strict";
4006
4090
  init_api();
4007
4091
  init_logger();
4008
4092
  init_usageLedger();
4093
+ init_toolRegistry();
4009
4094
  init_statusWatcher();
4010
4095
  init_cleanMessages();
4011
- log5 = createLogger("sub-agent");
4096
+ log6 = createLogger("sub-agent");
4012
4097
  }
4013
4098
  });
4014
4099
 
@@ -4268,7 +4353,7 @@ async function runBrowserAutomation(task, context) {
4268
4353
  }
4269
4354
  }
4270
4355
  } catch {
4271
- log6.debug("Failed to parse batch analysis result", {
4356
+ log7.debug("Failed to parse batch analysis result", {
4272
4357
  batchResult
4273
4358
  });
4274
4359
  }
@@ -4292,7 +4377,7 @@ async function runBrowserAutomation(task, context) {
4292
4377
  release();
4293
4378
  }
4294
4379
  }
4295
- var log6, browserAutomationTool;
4380
+ var log7, browserAutomationTool;
4296
4381
  var init_browserAutomation = __esm({
4297
4382
  "src/subagents/browserAutomation/index.ts"() {
4298
4383
  "use strict";
@@ -4305,7 +4390,7 @@ var init_browserAutomation = __esm({
4305
4390
  init_runMindstudioCli();
4306
4391
  init_surfaces();
4307
4392
  init_logger();
4308
- log6 = createLogger("browser-automation");
4393
+ log7 = createLogger("browser-automation");
4309
4394
  browserAutomationTool = {
4310
4395
  clearable: true,
4311
4396
  definition: {
@@ -6236,7 +6321,7 @@ function loadSession(state) {
6236
6321
  }
6237
6322
  if (Array.isArray(data.messages) && data.messages.length > 0) {
6238
6323
  state.messages = sanitizeMessages(data.messages);
6239
- log7.info("Session loaded", {
6324
+ log8.info("Session loaded", {
6240
6325
  messageCount: state.messages.length,
6241
6326
  ...state.models && { models: state.models }
6242
6327
  });
@@ -6289,9 +6374,9 @@ function saveSession(state) {
6289
6374
  payload.models = state.models;
6290
6375
  }
6291
6376
  fs19.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
6292
- log7.info("Session saved", { messageCount: state.messages.length });
6377
+ log8.info("Session saved", { messageCount: state.messages.length });
6293
6378
  } catch (err) {
6294
- log7.warn("Session save failed", { error: err.message });
6379
+ log8.warn("Session save failed", { error: err.message });
6295
6380
  }
6296
6381
  }
6297
6382
  function clearSession(state) {
@@ -6302,10 +6387,10 @@ function clearSession(state) {
6302
6387
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6303
6388
  const dest = path9.join(ARCHIVE_DIR, `cleared-${ts}.json`);
6304
6389
  fs19.renameSync(SESSION_FILE, dest);
6305
- log7.info("Session archived on clear", { dest });
6390
+ log8.info("Session archived on clear", { dest });
6306
6391
  }
6307
6392
  } catch (err) {
6308
- log7.warn("Session archive on clear failed, deleting instead", {
6393
+ log8.warn("Session archive on clear failed, deleting instead", {
6309
6394
  error: err.message
6310
6395
  });
6311
6396
  try {
@@ -6314,12 +6399,12 @@ function clearSession(state) {
6314
6399
  }
6315
6400
  }
6316
6401
  }
6317
- var log7, SESSION_FILE, ARCHIVE_DIR;
6402
+ var log8, SESSION_FILE, ARCHIVE_DIR;
6318
6403
  var init_session = __esm({
6319
6404
  "src/session.ts"() {
6320
6405
  "use strict";
6321
6406
  init_logger();
6322
- log7 = createLogger("session");
6407
+ log8 = createLogger("session");
6323
6408
  SESSION_FILE = ".remy-session.json";
6324
6409
  ARCHIVE_DIR = ".logs/sessions";
6325
6410
  }
@@ -6538,17 +6623,17 @@ async function runExtraction(apiConfig, model) {
6538
6623
  const inputHash = computeInputHash();
6539
6624
  const cached2 = readCache();
6540
6625
  if (cached2 && cached2.inputHash === inputHash) {
6541
- log8.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6626
+ log9.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6542
6627
  return null;
6543
6628
  }
6544
- log8.info("Extracting brand", { inputHash });
6629
+ log9.info("Extracting brand", { inputHash });
6545
6630
  const brand = await extractBrand(apiConfig, model);
6546
6631
  if (!brand) {
6547
- log8.warn("Brand extraction failed \u2014 leaving cache untouched");
6632
+ log9.warn("Brand extraction failed \u2014 leaving cache untouched");
6548
6633
  return null;
6549
6634
  }
6550
6635
  persistBrand(brand, inputHash);
6551
- log8.info("Brand persisted", { inputHash });
6636
+ log9.info("Brand persisted", { inputHash });
6552
6637
  return brand;
6553
6638
  }
6554
6639
  function computeInputHash() {
@@ -6614,7 +6699,7 @@ function parseFrontmatter3(filePath) {
6614
6699
  async function extractBrand(apiConfig, model) {
6615
6700
  const corpus = buildCorpus();
6616
6701
  if (!corpus.trim()) {
6617
- log8.debug("No spec corpus \u2014 emitting empty brand");
6702
+ log9.debug("No spec corpus \u2014 emitting empty brand");
6618
6703
  return { version: 1 };
6619
6704
  }
6620
6705
  let responseText = "";
@@ -6645,17 +6730,17 @@ async function extractBrand(apiConfig, model) {
6645
6730
  toolNames: []
6646
6731
  });
6647
6732
  } else if (event.type === "error") {
6648
- log8.error("Brand extraction stream error", { error: event.error });
6733
+ log9.error("Brand extraction stream error", { error: event.error });
6649
6734
  return null;
6650
6735
  }
6651
6736
  }
6652
6737
  } catch (err) {
6653
- log8.error("Brand extraction threw", { error: err?.message });
6738
+ log9.error("Brand extraction threw", { error: err?.message });
6654
6739
  return null;
6655
6740
  }
6656
6741
  const parsed = parseJsonResponse(responseText);
6657
6742
  if (!parsed) {
6658
- log8.warn("Brand extraction returned unparseable JSON", {
6743
+ log9.warn("Brand extraction returned unparseable JSON", {
6659
6744
  preview: responseText.slice(0, 200)
6660
6745
  });
6661
6746
  return null;
@@ -6795,7 +6880,7 @@ function readCache() {
6795
6880
  return null;
6796
6881
  }
6797
6882
  }
6798
- var log8, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE;
6883
+ var log9, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE;
6799
6884
  var init_brandExtraction = __esm({
6800
6885
  "src/brandExtraction/index.ts"() {
6801
6886
  "use strict";
@@ -6803,7 +6888,7 @@ var init_brandExtraction = __esm({
6803
6888
  init_assets();
6804
6889
  init_logger();
6805
6890
  init_usageLedger();
6806
- log8 = createLogger("brandExtraction");
6891
+ log9 = createLogger("brandExtraction");
6807
6892
  EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6808
6893
  BRAND_FILE = ".remy-brand.json";
6809
6894
  CACHE_FILE = ".remy-brand.cache.json";
@@ -6818,7 +6903,7 @@ function triggerBrandExtraction(apiConfig, model) {
6818
6903
  }
6819
6904
  inflight = true;
6820
6905
  void runExtraction(apiConfig, model).catch((err) => {
6821
- log9.error("Brand extraction failed", { error: err?.message });
6906
+ log10.error("Brand extraction failed", { error: err?.message });
6822
6907
  }).finally(() => {
6823
6908
  inflight = false;
6824
6909
  if (dirty) {
@@ -6827,13 +6912,13 @@ function triggerBrandExtraction(apiConfig, model) {
6827
6912
  }
6828
6913
  });
6829
6914
  }
6830
- var log9, inflight, dirty;
6915
+ var log10, inflight, dirty;
6831
6916
  var init_trigger2 = __esm({
6832
6917
  "src/brandExtraction/trigger.ts"() {
6833
6918
  "use strict";
6834
6919
  init_brandExtraction();
6835
6920
  init_logger();
6836
- log9 = createLogger("brandExtraction:trigger");
6921
+ log10 = createLogger("brandExtraction:trigger");
6837
6922
  inflight = false;
6838
6923
  dirty = false;
6839
6924
  }
@@ -6871,7 +6956,7 @@ async function runTurn(params) {
6871
6956
  } = params;
6872
6957
  const tools2 = getToolDefinitions(onboardingState);
6873
6958
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
6874
- log10.info("Turn started", {
6959
+ log11.info("Turn started", {
6875
6960
  requestId,
6876
6961
  model,
6877
6962
  toolCount: tools2.length,
@@ -7124,7 +7209,7 @@ async function runTurn(params) {
7124
7209
  const tool = getToolByName(event.name);
7125
7210
  const wasStreamed = acc?.started ?? false;
7126
7211
  const isInputStreaming = !!tool?.streaming?.partialInput;
7127
- log10.info("Tool received", {
7212
+ log11.info("Tool received", {
7128
7213
  requestId,
7129
7214
  toolCallId: event.id,
7130
7215
  name: event.name
@@ -7238,7 +7323,7 @@ async function runTurn(params) {
7238
7323
  });
7239
7324
  return;
7240
7325
  }
7241
- log10.info("Tools executing", {
7326
+ log11.info("Tools executing", {
7242
7327
  requestId,
7243
7328
  count: toolCalls.length,
7244
7329
  tools: toolCalls.map((tc) => tc.name)
@@ -7258,7 +7343,7 @@ async function runTurn(params) {
7258
7343
  const results = await Promise.all(
7259
7344
  toolCalls.map(async (tc) => {
7260
7345
  if (signal?.aborted) {
7261
- return { id: tc.id, result: "Error: cancelled", isError: true };
7346
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
7262
7347
  }
7263
7348
  const toolStart = Date.now();
7264
7349
  let settle;
@@ -7277,7 +7362,7 @@ async function runTurn(params) {
7277
7362
  };
7278
7363
  const cascadeAbort = () => {
7279
7364
  toolAbort.abort();
7280
- safeSettle("Error: cancelled", true);
7365
+ safeSettle(USER_CANCELLED_RESULT, true);
7281
7366
  };
7282
7367
  signal?.addEventListener("abort", cascadeAbort, { once: true });
7283
7368
  const run = async (input) => {
@@ -7285,7 +7370,7 @@ async function runTurn(params) {
7285
7370
  let result;
7286
7371
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
7287
7372
  saveSession(state);
7288
- log10.info("Waiting for external tool result", {
7373
+ log11.info("Waiting for external tool result", {
7289
7374
  requestId,
7290
7375
  toolCallId: tc.id,
7291
7376
  name: tc.name
@@ -7353,7 +7438,7 @@ async function runTurn(params) {
7353
7438
  if (!tc.input.background) {
7354
7439
  toolRegistry?.unregister(tc.id);
7355
7440
  }
7356
- log10.info("Tool completed", {
7441
+ log11.info("Tool completed", {
7357
7442
  requestId,
7358
7443
  toolCallId: tc.id,
7359
7444
  name: tc.name,
@@ -7412,7 +7497,7 @@ async function runTurn(params) {
7412
7497
  }
7413
7498
  }
7414
7499
  }
7415
- var log10, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
7500
+ var log11, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
7416
7501
  var init_agent = __esm({
7417
7502
  "src/agent.ts"() {
7418
7503
  "use strict";
@@ -7429,7 +7514,8 @@ var init_agent = __esm({
7429
7514
  init_sentinel();
7430
7515
  init_trigger2();
7431
7516
  init_surfaces();
7432
- log10 = createLogger("agent");
7517
+ init_toolRegistry();
7518
+ log11 = createLogger("agent");
7433
7519
  BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
7434
7520
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
7435
7521
  "promptUser",
@@ -7457,10 +7543,10 @@ import os from "os";
7457
7543
  function loadConfigFile() {
7458
7544
  try {
7459
7545
  const raw = fs21.readFileSync(CONFIG_PATH, "utf-8");
7460
- log11.debug("Loaded config file", { path: CONFIG_PATH });
7546
+ log12.debug("Loaded config file", { path: CONFIG_PATH });
7461
7547
  return JSON.parse(raw);
7462
7548
  } catch (err) {
7463
- log11.debug("No config file found", {
7549
+ log12.debug("No config file found", {
7464
7550
  path: CONFIG_PATH,
7465
7551
  error: err.message
7466
7552
  });
@@ -7475,13 +7561,13 @@ function resolveConfig(flags2) {
7475
7561
  const baseUrl2 = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
7476
7562
  const appId = process.env.MINDSTUDIO_APP_ID || void 0;
7477
7563
  if (!apiKey) {
7478
- log11.error("No API key found");
7564
+ log12.error("No API key found");
7479
7565
  throw new Error(
7480
7566
  "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
7481
7567
  );
7482
7568
  }
7483
7569
  const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
7484
- log11.info("Config resolved", {
7570
+ log12.info("Config resolved", {
7485
7571
  baseUrl: baseUrl2,
7486
7572
  keySource,
7487
7573
  environment: activeEnv,
@@ -7489,12 +7575,12 @@ function resolveConfig(flags2) {
7489
7575
  });
7490
7576
  return { apiKey, baseUrl: baseUrl2, appId };
7491
7577
  }
7492
- var log11, CONFIG_PATH, DEFAULT_BASE_URL;
7578
+ var log12, CONFIG_PATH, DEFAULT_BASE_URL;
7493
7579
  var init_config = __esm({
7494
7580
  "src/config.ts"() {
7495
7581
  "use strict";
7496
7582
  init_logger();
7497
- log11 = createLogger("config");
7583
+ log12 = createLogger("config");
7498
7584
  CONFIG_PATH = path11.join(
7499
7585
  os.homedir(),
7500
7586
  ".mindstudio-local-tunnel",
@@ -7504,88 +7590,6 @@ var init_config = __esm({
7504
7590
  }
7505
7591
  });
7506
7592
 
7507
- // src/toolRegistry.ts
7508
- var log12, ToolRegistry;
7509
- var init_toolRegistry = __esm({
7510
- "src/toolRegistry.ts"() {
7511
- "use strict";
7512
- init_logger();
7513
- log12 = createLogger("tool-registry");
7514
- ToolRegistry = class {
7515
- entries = /* @__PURE__ */ new Map();
7516
- onEvent;
7517
- register(entry) {
7518
- this.entries.set(entry.id, entry);
7519
- }
7520
- unregister(id) {
7521
- this.entries.delete(id);
7522
- }
7523
- get(id) {
7524
- return this.entries.get(id);
7525
- }
7526
- /**
7527
- * Stop a running tool.
7528
- *
7529
- * - graceful: abort and settle with [INTERRUPTED] + partial result
7530
- * - hard: abort and settle with a generic error
7531
- *
7532
- * Returns true if the tool was found and stopped.
7533
- */
7534
- stop(id, mode) {
7535
- const entry = this.entries.get(id);
7536
- if (!entry) {
7537
- return false;
7538
- }
7539
- log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
7540
- entry.abortController.abort(mode);
7541
- if (mode === "graceful") {
7542
- const partial = entry.getPartialResult?.() ?? "";
7543
- const result = partial ? `[INTERRUPTED]
7544
-
7545
- ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
7546
- entry.settle(result, false);
7547
- } else {
7548
- entry.settle("Error: tool was cancelled", true);
7549
- }
7550
- this.onEvent?.({
7551
- type: "tool_stopped",
7552
- id: entry.id,
7553
- name: entry.name,
7554
- mode,
7555
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
7556
- });
7557
- this.entries.delete(id);
7558
- return true;
7559
- }
7560
- /**
7561
- * Restart a running tool with the same or patched input.
7562
- * The original controllable promise stays pending and settles
7563
- * when the new execution finishes.
7564
- *
7565
- * Returns true if the tool was found and restarted.
7566
- */
7567
- restart(id, patchedInput) {
7568
- const entry = this.entries.get(id);
7569
- if (!entry) {
7570
- return false;
7571
- }
7572
- log12.info("Tool restarted", { toolCallId: id, name: entry.name });
7573
- entry.abortController.abort("restart");
7574
- const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
7575
- this.onEvent?.({
7576
- type: "tool_restarted",
7577
- id: entry.id,
7578
- name: entry.name,
7579
- input: newInput,
7580
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
7581
- });
7582
- entry.rerun(newInput);
7583
- return true;
7584
- }
7585
- };
7586
- }
7587
- });
7588
-
7589
7593
  // src/headless/attachments.ts
7590
7594
  import { mkdirSync, existsSync } from "fs";
7591
7595
  import { writeFile } from "fs/promises";
@@ -8593,7 +8597,7 @@ var init_headless = __esm({
8593
8597
  }
8594
8598
  for (const [id, pending] of this.pendingTools) {
8595
8599
  clearTimeout(pending.timeout);
8596
- pending.resolve("Error: cancelled");
8600
+ pending.resolve(USER_CANCELLED_RESULT);
8597
8601
  this.pendingTools.delete(id);
8598
8602
  }
8599
8603
  return this.queue.drain();
@@ -344,7 +344,19 @@ Accepts any HTTP method. The method receives `{ method, headers, query, body }`
344
344
 
345
345
  ## Email
346
346
 
347
- Inbound email triggers. An app can register one inbound address that routes all inbound emails to one method.
347
+ Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method.
348
+
349
+ ### Address tiers
350
+
351
+ Three tiers, all delivered to the same handler method. The new tiers are catchall (no localpart registration); the legacy tier is specific-localpart and frozen for new apps.
352
+
353
+ | Tier | Address | How it's set up |
354
+ |---|---|---|
355
+ | Platform subdomain (default) | `*@<custom_subdomain>.madewithremy.com` | Automatic the moment the app has a `custom_subdomain` set. Every address on that subdomain delivers to the handler. |
356
+ | Custom domain | `*@<their-domain>` | The user adds a domain in the dashboard's email-domains settings and points one MX record at `mx.msagent.ai`. Not something the agent provisions. |
357
+ | Legacy `mindstudio-hooks.com` | `<name>@mindstudio-hooks.com` | Existing apps only — frozen for new apps. Don't recommend it; treat as read-only history. |
358
+
359
+ Because the new tiers are catchall, `to` carries an arbitrary localpart. Methods that need to branch on it should read `input.to` (e.g. `if (input.to.startsWith('support@')) ...`).
348
360
 
349
361
  ### Config (`interface.json`)
350
362
 
@@ -357,27 +369,27 @@ Inbound email triggers. An app can register one inbound address that routes all
357
369
  }
358
370
  ```
359
371
 
360
- `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com` wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before the method runs.
361
-
362
- Address pattern: `{custom-name}@mindstudio-hooks.com`.
372
+ `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com` wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before the method runs (silently — the sender isn't bounced). Matching is case-insensitive. The same list applies uniformly across all three address tiers.
363
373
 
364
374
  ### Input shape
365
375
 
366
376
  ```ts
367
377
  {
368
- to: string; // resolved from the SMTP envelope
378
+ to: string; // full recipient address; localpart is arbitrary on catchall tiers
369
379
  from: string; // bare address, extracted from "Name <a@b>" form
370
380
  subject: string; // 'No Subject' if missing
371
- message: string; // plain text body; 'No Body' if neither text nor html was sent
381
+ message: string; // plain text body, falls back to HTML if text is missing; 'No Body' if neither was sent
372
382
  html: string; // HTML body, or '' when text-only
373
383
  attachments: string[]; // CDN URLs — already uploaded by the platform
374
384
  }
375
385
  ```
376
386
 
377
- ### Attachments
387
+ ### Attachments and size limits
378
388
 
379
389
  `attachments[]` is an array of CDN URLs — the platform has already received and uploaded the files. Fetch them server-side via the URL when you need the bytes; pass them through as URLs to UI or downstream services.
380
390
 
391
+ Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by the platform before the method runs.
392
+
381
393
  ### Auth
382
394
 
383
395
  Methods invoked through this interface run with `auth.roles: ['system']` (see the system-roles section above). They have no user session and can't impersonate. Use `auth.requireRole('system')` to gate methods that should only be reachable via email.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.184",
3
+ "version": "0.1.186",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",