@mindstudio-ai/remy 0.1.145 → 0.1.147

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/index.js CHANGED
@@ -156,6 +156,7 @@ async function* streamChat(params) {
156
156
  const reader = res.body.getReader();
157
157
  const decoder = new TextDecoder();
158
158
  let buffer = "";
159
+ let receivedDone = false;
159
160
  while (true) {
160
161
  let stallTimer;
161
162
  let readResult;
@@ -199,6 +200,7 @@ async function* streamChat(params) {
199
200
  const event = JSON.parse(line.slice(6));
200
201
  if (event.type === "done") {
201
202
  const elapsed = Date.now() - startTime;
203
+ receivedDone = true;
202
204
  log.info("Stream complete", {
203
205
  requestId,
204
206
  ...subAgentId && { subAgentId },
@@ -207,12 +209,27 @@ async function* streamChat(params) {
207
209
  inputTokens: event.usage.inputTokens,
208
210
  outputTokens: event.usage.outputTokens
209
211
  });
212
+ } else if (event.type === "error") {
213
+ log.error("SSE error event", {
214
+ requestId,
215
+ ...subAgentId && { subAgentId },
216
+ error: event.error,
217
+ durationMs: Date.now() - startTime
218
+ });
210
219
  }
211
220
  yield event;
212
221
  } catch {
213
222
  }
214
223
  }
215
224
  }
225
+ if (!receivedDone) {
226
+ log.warn("Stream ended without done event", {
227
+ requestId,
228
+ ...subAgentId && { subAgentId },
229
+ durationMs: Date.now() - startTime,
230
+ remainingBuffer: buffer.slice(0, 200)
231
+ });
232
+ }
216
233
  if (buffer.startsWith("data: ")) {
217
234
  try {
218
235
  yield JSON.parse(buffer.slice(6));
@@ -1541,85 +1558,9 @@ var init_compaction = __esm({
1541
1558
  }
1542
1559
  });
1543
1560
 
1544
- // src/tools/_helpers/sidecar.ts
1545
- function setSidecarBaseUrl(url) {
1546
- baseUrl = url;
1547
- log3.info("Configured", { url });
1548
- }
1549
- function isSidecarConfigured() {
1550
- return baseUrl !== null;
1551
- }
1552
- async function sidecarRequest(endpoint, body = {}, options) {
1553
- if (!baseUrl) {
1554
- throw new Error("Sidecar not available");
1555
- }
1556
- const url = `${baseUrl}${endpoint}`;
1557
- try {
1558
- const res = await fetch(url, {
1559
- method: "POST",
1560
- headers: { "Content-Type": "application/json" },
1561
- body: JSON.stringify(body),
1562
- signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
1563
- });
1564
- if (!res.ok) {
1565
- log3.error("Sidecar error", { endpoint, status: res.status });
1566
- throw new Error(`Sidecar error: ${res.status}`);
1567
- }
1568
- const data = await res.json();
1569
- if (data?.success === false) {
1570
- const code = data.errorCode ? ` [${data.errorCode}]` : "";
1571
- throw new Error(`${data.error || "Unknown error"}${code}`);
1572
- }
1573
- return data;
1574
- } catch (err) {
1575
- if (err.message.startsWith("Sidecar error")) {
1576
- throw err;
1577
- }
1578
- log3.error("Sidecar connection error", { endpoint, error: err.message });
1579
- throw new Error(`Sidecar connection error: ${err.message}`);
1580
- }
1581
- }
1582
- var log3, baseUrl;
1583
- var init_sidecar = __esm({
1584
- "src/tools/_helpers/sidecar.ts"() {
1585
- "use strict";
1586
- init_logger();
1587
- log3 = createLogger("sidecar");
1588
- baseUrl = null;
1589
- }
1590
- });
1591
-
1592
- // src/tools/_helpers/lsp.ts
1593
- async function lspRequest(endpoint, body) {
1594
- return sidecarRequest(endpoint, body);
1595
- }
1596
- var setLspBaseUrl, isLspConfigured;
1597
- var init_lsp = __esm({
1598
- "src/tools/_helpers/lsp.ts"() {
1599
- "use strict";
1600
- init_sidecar();
1601
- setLspBaseUrl = setSidecarBaseUrl;
1602
- isLspConfigured = isSidecarConfigured;
1603
- }
1604
- });
1605
-
1606
1561
  // src/prompt/static/projectContext.ts
1607
1562
  import fs9 from "fs";
1608
1563
  import path4 from "path";
1609
- function loadProjectInstructions() {
1610
- for (const file of AGENT_INSTRUCTION_FILES) {
1611
- try {
1612
- const content = fs9.readFileSync(file, "utf-8").trim();
1613
- if (content) {
1614
- return `
1615
- ## Project Instructions (${file})
1616
- ${content}`;
1617
- }
1618
- } catch {
1619
- }
1620
- }
1621
- return "";
1622
- }
1623
1564
  function loadProjectManifest() {
1624
1565
  try {
1625
1566
  const manifest = fs9.readFileSync("mindstudio.json", "utf-8");
@@ -1735,26 +1676,9 @@ ${listing}
1735
1676
  return "";
1736
1677
  }
1737
1678
  }
1738
- var AGENT_INSTRUCTION_FILES;
1739
1679
  var init_projectContext = __esm({
1740
1680
  "src/prompt/static/projectContext.ts"() {
1741
1681
  "use strict";
1742
- AGENT_INSTRUCTION_FILES = [
1743
- "CLAUDE.md",
1744
- "claude.md",
1745
- ".claude/instructions.md",
1746
- "AGENTS.md",
1747
- "agents.md",
1748
- ".agents.md",
1749
- "COPILOT.md",
1750
- "copilot.md",
1751
- ".copilot-instructions.md",
1752
- ".github/copilot-instructions.md",
1753
- "REMY.md",
1754
- "remy.md",
1755
- ".cursorrules",
1756
- ".cursorules"
1757
- ];
1758
1682
  }
1759
1683
  });
1760
1684
 
@@ -1768,7 +1692,6 @@ function resolveIncludes(template) {
1768
1692
  }
1769
1693
  function buildSystemPrompt(onboardingState, viewContext) {
1770
1694
  const projectContext = [
1771
- loadProjectInstructions(),
1772
1695
  loadProjectManifest(),
1773
1696
  loadSpecFileMetadata(),
1774
1697
  loadProjectFileListing()
@@ -1843,29 +1766,26 @@ Current date: ${now}
1843
1766
  {{compiled/msfm.md}}
1844
1767
  </mindstudio_flavored_markdown_spec_docs>
1845
1768
 
1846
- <project_context>
1847
- ${projectContext}
1848
- </project_context>
1849
-
1850
1769
  <intake_mode_instructions>
1851
- {{static/intake.md}}
1770
+ {{static/intake.md}}
1852
1771
  </intake_mode_instructions>
1853
1772
 
1854
1773
  <spec_authoring_instructions>
1855
- {{static/authoring.md}}
1774
+ {{static/authoring.md}}
1856
1775
  </spec_authoring_instructions>
1857
1776
 
1858
- {{static/team.md}}
1777
+ <team>
1778
+ {{static/team.md}}
1779
+ </team>
1859
1780
 
1860
1781
  <code_authoring_instructions>
1861
1782
  {{static/coding.md}}
1862
- ${isLspConfigured() ? `<typescript_lsp>
1783
+
1784
+ <typescript_lsp>
1863
1785
  {{static/lsp.md}}
1864
- </typescript_lsp>` : ""}
1786
+ </typescript_lsp>
1865
1787
  </code_authoring_instructions>
1866
1788
 
1867
- {{static/instructions.md}}
1868
- ${loadPlanStatus()}
1869
1789
  <conversation_summaries>
1870
1790
  Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
1871
1791
 
@@ -1879,18 +1799,26 @@ New projects progress through four onboarding states. The user might skip this e
1879
1799
  - **initialSpecAuthoring**: Writing and refining the first spec. The user can see it in the editor as it streams in and can give feedback to iterate on it. This phase covers both the initial draft and any back-and-forth refinement before code generation.
1880
1800
  - **initialCodegen**: First code generation from the spec. The agent is generating methods, tables, interfaces, manifest updates, and scenarios. This can take a while and involves heavy tool use. The user sees a full-screen build progress view.
1881
1801
  - **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
1802
+ </project_onboarding>
1803
+
1804
+ {{static/instructions.md}}
1882
1805
 
1883
1806
  <!-- cache_breakpoint -->
1884
1807
 
1885
- <current_project_onboarding_state>
1808
+ <current_project_onboarding_state>
1886
1809
  ${onboardingState ?? "onboardingFinished"}
1887
- </current_project_onboarding_state>
1888
- </project_onboarding>
1810
+ </current_project_onboarding_state>
1811
+
1812
+ <project_context>
1813
+ ${projectContext}
1814
+ </project_context>
1889
1815
 
1890
1816
  <view_context>
1891
1817
  The user is currently in ${viewContext?.mode ?? "code"} mode.
1892
1818
  ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
1893
1819
  </view_context>
1820
+
1821
+ ${loadPlanStatus()}
1894
1822
  `;
1895
1823
  return resolveIncludes(template);
1896
1824
  }
@@ -1898,7 +1826,6 @@ var init_prompt = __esm({
1898
1826
  "src/prompt/index.ts"() {
1899
1827
  "use strict";
1900
1828
  init_assets();
1901
- init_lsp();
1902
1829
  init_projectContext();
1903
1830
  }
1904
1831
  });
@@ -1914,15 +1841,15 @@ function triggerCompaction(state, apiConfig, callbacks) {
1914
1841
  compactConversation(state.messages, apiConfig, system, tools2).then((summaries) => {
1915
1842
  pendingSummaries.push(...summaries);
1916
1843
  callbacks?.onSummariesReady?.();
1917
- log4.info("Compaction complete");
1844
+ log3.info("Compaction complete");
1918
1845
  }).catch((err) => {
1919
1846
  callbacks?.onError?.(err.message || "Compaction failed");
1920
- log4.error("Compaction failed", { error: err.message });
1847
+ log3.error("Compaction failed", { error: err.message });
1921
1848
  }).finally(() => {
1922
1849
  callbacks?.onFinally?.();
1923
1850
  });
1924
1851
  }
1925
- var log4, pendingSummaries;
1852
+ var log3, pendingSummaries;
1926
1853
  var init_trigger = __esm({
1927
1854
  "src/compaction/trigger.ts"() {
1928
1855
  "use strict";
@@ -1930,7 +1857,7 @@ var init_trigger = __esm({
1930
1857
  init_prompt();
1931
1858
  init_tools6();
1932
1859
  init_logger();
1933
- log4 = createLogger("compaction:trigger");
1860
+ log3 = createLogger("compaction:trigger");
1934
1861
  pendingSummaries = [];
1935
1862
  }
1936
1863
  });
@@ -2672,6 +2599,64 @@ var init_editsFinished = __esm({
2672
2599
  }
2673
2600
  });
2674
2601
 
2602
+ // src/tools/_helpers/sidecar.ts
2603
+ function setSidecarBaseUrl(url) {
2604
+ baseUrl = url;
2605
+ log4.info("Configured", { url });
2606
+ }
2607
+ async function sidecarRequest(endpoint, body = {}, options) {
2608
+ if (!baseUrl) {
2609
+ throw new Error("Sidecar not available");
2610
+ }
2611
+ const url = `${baseUrl}${endpoint}`;
2612
+ try {
2613
+ const res = await fetch(url, {
2614
+ method: "POST",
2615
+ headers: { "Content-Type": "application/json" },
2616
+ body: JSON.stringify(body),
2617
+ signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
2618
+ });
2619
+ if (!res.ok) {
2620
+ log4.error("Sidecar error", { endpoint, status: res.status });
2621
+ throw new Error(`Sidecar error: ${res.status}`);
2622
+ }
2623
+ const data = await res.json();
2624
+ if (data?.success === false) {
2625
+ const code = data.errorCode ? ` [${data.errorCode}]` : "";
2626
+ throw new Error(`${data.error || "Unknown error"}${code}`);
2627
+ }
2628
+ return data;
2629
+ } catch (err) {
2630
+ if (err.message.startsWith("Sidecar error")) {
2631
+ throw err;
2632
+ }
2633
+ log4.error("Sidecar connection error", { endpoint, error: err.message });
2634
+ throw new Error(`Sidecar connection error: ${err.message}`);
2635
+ }
2636
+ }
2637
+ var log4, baseUrl;
2638
+ var init_sidecar = __esm({
2639
+ "src/tools/_helpers/sidecar.ts"() {
2640
+ "use strict";
2641
+ init_logger();
2642
+ log4 = createLogger("sidecar");
2643
+ baseUrl = null;
2644
+ }
2645
+ });
2646
+
2647
+ // src/tools/_helpers/lsp.ts
2648
+ async function lspRequest(endpoint, body) {
2649
+ return sidecarRequest(endpoint, body);
2650
+ }
2651
+ var setLspBaseUrl;
2652
+ var init_lsp = __esm({
2653
+ "src/tools/_helpers/lsp.ts"() {
2654
+ "use strict";
2655
+ init_sidecar();
2656
+ setLspBaseUrl = setSidecarBaseUrl;
2657
+ }
2658
+ });
2659
+
2675
2660
  // src/tools/code/lspDiagnostics.ts
2676
2661
  var lspDiagnosticsTool;
2677
2662
  var init_lspDiagnostics = __esm({
@@ -6701,13 +6686,24 @@ function resolveAction(text) {
6701
6686
  }
6702
6687
  }
6703
6688
  let body = readAsset("automatedActions", `${triggerName}.md`);
6689
+ let next;
6690
+ const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
6691
+ if (fmMatch) {
6692
+ const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
6693
+ if (nextMatch) {
6694
+ next = nextMatch[1];
6695
+ }
6696
+ }
6704
6697
  body = body.replace(/^---[\s\S]*?---\s*/, "");
6705
6698
  for (const [key, value] of Object.entries(params)) {
6706
6699
  const str = typeof value === "string" ? value : JSON.stringify(value);
6707
6700
  body = body.replaceAll(`{{${key}}}`, str);
6708
6701
  }
6709
- return `@@automated::${triggerName}@@
6710
- ${body}`;
6702
+ return {
6703
+ message: `@@automated::${triggerName}@@
6704
+ ${body}`,
6705
+ next
6706
+ };
6711
6707
  }
6712
6708
  var NON_ACTION_SENTINELS;
6713
6709
  var init_resolve = __esm({
@@ -6724,7 +6720,15 @@ __export(headless_exports, {
6724
6720
  startHeadless: () => startHeadless
6725
6721
  });
6726
6722
  import { createInterface } from "readline";
6727
- import { writeFileSync, readFileSync, unlinkSync } from "fs";
6723
+ import {
6724
+ writeFileSync,
6725
+ readFileSync,
6726
+ unlinkSync,
6727
+ mkdirSync,
6728
+ existsSync
6729
+ } from "fs";
6730
+ import { writeFile } from "fs/promises";
6731
+ import { basename, join, extname } from "path";
6728
6732
  function emit(event, data, requestId) {
6729
6733
  const payload = { event, ...data };
6730
6734
  if (requestId) {
@@ -6782,6 +6786,7 @@ async function startHeadless(opts = {}) {
6782
6786
  let currentRequestId;
6783
6787
  let completedEmitted = false;
6784
6788
  let turnStart = 0;
6789
+ let pendingNextAction;
6785
6790
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6786
6791
  const pendingTools = /* @__PURE__ */ new Map();
6787
6792
  const earlyResults = /* @__PURE__ */ new Map();
@@ -6932,10 +6937,19 @@ ${xmlParts}
6932
6937
  applyPendingSummaries();
6933
6938
  applyPendingBlockUpdates();
6934
6939
  flushBackgroundQueue();
6940
+ if (pendingNextAction) {
6941
+ const next = pendingNextAction;
6942
+ pendingNextAction = void 0;
6943
+ handleMessage(
6944
+ { action: "message", text: `@@automated::${next}@@` },
6945
+ `chain-${Date.now()}`
6946
+ );
6947
+ }
6935
6948
  }, 0);
6936
6949
  return;
6937
6950
  case "turn_cancelled":
6938
6951
  completedEmitted = true;
6952
+ pendingNextAction = void 0;
6939
6953
  emit("completed", { success: false, error: "cancelled" }, rid);
6940
6954
  return;
6941
6955
  // Streaming events — forward with requestId
@@ -7050,6 +7064,120 @@ ${xmlParts}
7050
7064
  }
7051
7065
  }
7052
7066
  toolRegistry.onEvent = onEvent;
7067
+ const UPLOADS_DIR = "src/.user-uploads";
7068
+ function filenameFromUrl(url) {
7069
+ try {
7070
+ const pathname = new URL(url).pathname;
7071
+ const name = basename(pathname);
7072
+ return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
7073
+ } catch {
7074
+ return `upload-${Date.now()}`;
7075
+ }
7076
+ }
7077
+ function resolveUniqueFilename(name) {
7078
+ if (!existsSync(join(UPLOADS_DIR, name))) {
7079
+ return name;
7080
+ }
7081
+ const ext = extname(name);
7082
+ const base = name.slice(0, name.length - ext.length);
7083
+ let counter = 1;
7084
+ while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
7085
+ counter++;
7086
+ }
7087
+ return `${base}-${counter}${ext}`;
7088
+ }
7089
+ const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
7090
+ ".png",
7091
+ ".jpg",
7092
+ ".jpeg",
7093
+ ".gif",
7094
+ ".webp",
7095
+ ".svg",
7096
+ ".bmp",
7097
+ ".ico",
7098
+ ".tiff",
7099
+ ".tif",
7100
+ ".avif",
7101
+ ".heic",
7102
+ ".heif"
7103
+ ]);
7104
+ function isImageAttachment(att) {
7105
+ const name = att.filename || filenameFromUrl(att.url);
7106
+ return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
7107
+ }
7108
+ async function persistAttachments(attachments) {
7109
+ const nonVoice = attachments.filter((a) => !a.isVoice);
7110
+ if (nonVoice.length === 0) {
7111
+ return { documents: [], images: [] };
7112
+ }
7113
+ mkdirSync(UPLOADS_DIR, { recursive: true });
7114
+ const results = await Promise.allSettled(
7115
+ nonVoice.map(async (att) => {
7116
+ const name = resolveUniqueFilename(
7117
+ att.filename || filenameFromUrl(att.url)
7118
+ );
7119
+ const localPath = join(UPLOADS_DIR, name);
7120
+ const res = await fetch(att.url, {
7121
+ signal: AbortSignal.timeout(3e4)
7122
+ });
7123
+ if (!res.ok) {
7124
+ throw new Error(`HTTP ${res.status} downloading ${att.url}`);
7125
+ }
7126
+ const buffer = Buffer.from(await res.arrayBuffer());
7127
+ await writeFile(localPath, buffer);
7128
+ log11.info("Attachment saved", {
7129
+ filename: name,
7130
+ path: localPath,
7131
+ bytes: buffer.length
7132
+ });
7133
+ let extractedTextPath;
7134
+ if (att.extractedTextUrl) {
7135
+ try {
7136
+ const textRes = await fetch(att.extractedTextUrl, {
7137
+ signal: AbortSignal.timeout(3e4)
7138
+ });
7139
+ if (textRes.ok) {
7140
+ extractedTextPath = `${localPath}.txt`;
7141
+ await writeFile(extractedTextPath, await textRes.text(), "utf-8");
7142
+ log11.info("Extracted text saved", { path: extractedTextPath });
7143
+ }
7144
+ } catch {
7145
+ }
7146
+ }
7147
+ return { filename: name, localPath, extractedTextPath };
7148
+ })
7149
+ );
7150
+ const settled = results.map((r, i) => ({
7151
+ result: r.status === "fulfilled" ? r.value : null,
7152
+ isImage: isImageAttachment(nonVoice[i])
7153
+ }));
7154
+ return {
7155
+ documents: settled.filter((s) => !s.isImage).map((s) => s.result),
7156
+ images: settled.filter((s) => s.isImage).map((s) => s.result)
7157
+ };
7158
+ }
7159
+ function buildUploadHeader(results) {
7160
+ const succeeded = results.filter(Boolean);
7161
+ if (succeeded.length === 0) {
7162
+ return "";
7163
+ }
7164
+ if (succeeded.length === 1) {
7165
+ const r = succeeded[0];
7166
+ const parts = [`[Uploaded file: ${r.localPath}`];
7167
+ if (r.extractedTextPath) {
7168
+ parts.push(`extracted text: ${r.extractedTextPath}`);
7169
+ }
7170
+ return parts.join(" \u2014 ") + "]";
7171
+ }
7172
+ const lines = succeeded.map((r) => {
7173
+ if (r.extractedTextPath) {
7174
+ return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
7175
+ }
7176
+ return `- ${r.localPath}`;
7177
+ });
7178
+ return `[Uploaded files]
7179
+ ${lines.join("\n")}`;
7180
+ }
7053
7181
  async function handleMessage(parsed, requestId) {
7054
7182
  if (running) {
7055
7183
  emit(
@@ -7071,12 +7199,26 @@ ${xmlParts}
7071
7199
  turnStart = Date.now();
7072
7200
  const attachments = parsed.attachments;
7073
7201
  if (attachments?.length) {
7074
- console.warn(
7075
- `[headless] Message has ${attachments.length} attachment(s):`,
7076
- attachments.map((a) => a.url)
7077
- );
7202
+ log11.info("Message has attachments", {
7203
+ count: attachments.length,
7204
+ urls: attachments.map((a) => a.url)
7205
+ });
7078
7206
  }
7079
7207
  let userMessage = parsed.text ?? "";
7208
+ if (attachments?.some((a) => !a.isVoice)) {
7209
+ try {
7210
+ const { documents, images } = await persistAttachments(attachments);
7211
+ const all = [...documents, ...images];
7212
+ const header = buildUploadHeader(all);
7213
+ if (header) {
7214
+ userMessage = userMessage ? `${header}
7215
+
7216
+ ${userMessage}` : header;
7217
+ }
7218
+ } catch (err) {
7219
+ log11.warn("Attachment persistence failed", { error: err.message });
7220
+ }
7221
+ }
7080
7222
  let resolved = null;
7081
7223
  try {
7082
7224
  resolved = resolveAction(userMessage);
@@ -7088,8 +7230,10 @@ ${xmlParts}
7088
7230
  );
7089
7231
  return;
7090
7232
  }
7233
+ pendingNextAction = void 0;
7091
7234
  if (resolved !== null) {
7092
- userMessage = resolved;
7235
+ userMessage = resolved.message;
7236
+ pendingNextAction = resolved.next;
7093
7237
  }
7094
7238
  const isHidden = resolved !== null || !!parsed.hidden;
7095
7239
  const rawText = parsed.text ?? "";
@@ -232,14 +232,16 @@ Slash commands that invoke methods.
232
232
 
233
233
  ```json
234
234
  {
235
- "commands": [
236
- {
237
- "name": "submit-vendor",
238
- "description": "Request a new vendor",
239
- "method": "submit-vendor-request"
240
- }
241
- ],
242
- "loadingMessage": "Processing your request..."
235
+ "discord": {
236
+ "commands": [
237
+ {
238
+ "name": "submit-vendor",
239
+ "description": "Request a new vendor",
240
+ "method": "submit-vendor-request"
241
+ }
242
+ ],
243
+ "loadingMessage": "Processing your request..."
244
+ }
243
245
  }
244
246
  ```
245
247
 
@@ -253,14 +255,16 @@ Bot commands and message handling.
253
255
 
254
256
  ```json
255
257
  {
256
- "commands": [
257
- {
258
- "command": "/submit",
259
- "description": "Submit a vendor request",
260
- "method": "submit-vendor-request"
261
- }
262
- ],
263
- "defaultMethod": "handle-message"
258
+ "telegram": {
259
+ "commands": [
260
+ {
261
+ "command": "/submit",
262
+ "description": "Submit a vendor request",
263
+ "method": "submit-vendor-request"
264
+ }
265
+ ],
266
+ "defaultMethod": "handle-message"
267
+ }
264
268
  }
265
269
  ```
266
270
 
@@ -274,18 +278,20 @@ Scheduled method execution.
274
278
 
275
279
  ```json
276
280
  {
277
- "jobs": [
278
- {
279
- "schedule": "0 9 * * 5",
280
- "method": "process-weekly-payments",
281
- "description": "Process approved invoices every Friday at 9am"
282
- },
283
- {
284
- "schedule": "*/30 * * * *",
285
- "method": "sync-vendor-status",
286
- "description": "Sync vendor statuses every 30 minutes"
287
- }
288
- ]
281
+ "cron": {
282
+ "jobs": [
283
+ {
284
+ "schedule": "0 9 * * 5",
285
+ "method": "process-weekly-payments",
286
+ "description": "Process approved invoices every Friday at 9am"
287
+ },
288
+ {
289
+ "schedule": "*/30 * * * *",
290
+ "method": "sync-vendor-status",
291
+ "description": "Sync vendor statuses every 30 minutes"
292
+ }
293
+ ]
294
+ }
289
295
  }
290
296
  ```
291
297
 
@@ -299,13 +305,15 @@ Inbound HTTP endpoints that invoke methods.
299
305
 
300
306
  ```json
301
307
  {
302
- "endpoints": [
303
- {
304
- "method": "handle-payment-webhook",
305
- "description": "Stripe payment notifications",
306
- "secret": "whk_abc123..."
307
- }
308
- ]
308
+ "webhook": {
309
+ "endpoints": [
310
+ {
311
+ "method": "handle-payment-webhook",
312
+ "description": "Stripe payment notifications",
313
+ "secret": "whk_abc123..."
314
+ }
315
+ ]
316
+ }
309
317
  }
310
318
  ```
311
319
 
@@ -321,7 +329,9 @@ Inbound email triggers.
321
329
 
322
330
  ```json
323
331
  {
324
- "method": "handle-inbound-email"
332
+ "email": {
333
+ "method": "handle-inbound-email"
334
+ }
325
335
  }
326
336
  ```
327
337
 
@@ -335,7 +345,9 @@ Expose methods as AI tools.
335
345
 
336
346
  ```json
337
347
  {
338
- "methods": ["submit-vendor-request", "list-vendors"]
348
+ "mcp": {
349
+ "methods": ["submit-vendor-request", "list-vendors"]
350
+ }
339
351
  }
340
352
  ```
341
353
 
@@ -35,7 +35,7 @@ box-shadow: 0 8px 32px rgba(0,0,0,0.3) for floating depth
35
35
  ~~~
36
36
  ```
37
37
 
38
- When you have image URLs (from the design expert), embed them directly in the spec using markdown image syntax. Write descriptive alt text that captures what the image actually depicts (this helps accessibility and helps the coding agent understand the image without loading it). Use the surrounding prose to explain the design intent — what the image is for, how it should be used in the layout, and why it was chosen.
38
+ When you have image URLs (from the design expert), embed them directly in the spec using markdown image syntax. Write descriptive alt text that captures what the image actually depicts (this helps accessibility and helps the coding agent understand the image without loading it). Use the surrounding prose to explain the design intent — what the image is for, how it should be used in the layout, and why it was chosen. User-uploaded files (images, documents, reference materials) are saved to `src/.user-uploads/` and can be referenced from specs using their disk path.
39
39
 
40
40
  When the design expert provides wireframes, include them directly in the spec for future reference.
41
41