@mindstudio-ai/remy 0.1.0

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.
@@ -0,0 +1,2515 @@
1
+ // src/headless.ts
2
+ import { createInterface } from "readline";
3
+ import fs14 from "fs";
4
+ import path7 from "path";
5
+
6
+ // src/config.ts
7
+ import fs2 from "fs";
8
+ import path from "path";
9
+ import os from "os";
10
+
11
+ // src/logger.ts
12
+ import fs from "fs";
13
+ var LEVELS = {
14
+ error: 0,
15
+ warn: 1,
16
+ info: 2,
17
+ debug: 3
18
+ };
19
+ var currentLevel = LEVELS.error;
20
+ var writeFn = () => {
21
+ };
22
+ function timestamp() {
23
+ return (/* @__PURE__ */ new Date()).toISOString();
24
+ }
25
+ var MAX_VALUE_LENGTH = 200;
26
+ function truncateValues(obj) {
27
+ const result = {};
28
+ for (const [key, value] of Object.entries(obj)) {
29
+ if (typeof value === "string" && value.length > MAX_VALUE_LENGTH) {
30
+ result[key] = value.slice(0, MAX_VALUE_LENGTH) + `... (${value.length} chars)`;
31
+ } else if (Array.isArray(value) && value.length > 5) {
32
+ result[key] = `[${value.length} items]`;
33
+ } else {
34
+ result[key] = value;
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ function write(level, msg, data) {
40
+ if (LEVELS[level] > currentLevel) {
41
+ return;
42
+ }
43
+ const parts = [`[${timestamp()}]`, level.toUpperCase().padEnd(5), msg];
44
+ if (data) {
45
+ parts.push(JSON.stringify(truncateValues(data)));
46
+ }
47
+ writeFn(parts.join(" "));
48
+ }
49
+ var log = {
50
+ error(msg, data) {
51
+ write("error", msg, data);
52
+ },
53
+ warn(msg, data) {
54
+ write("warn", msg, data);
55
+ },
56
+ info(msg, data) {
57
+ write("info", msg, data);
58
+ },
59
+ debug(msg, data) {
60
+ write("debug", msg, data);
61
+ }
62
+ };
63
+
64
+ // src/config.ts
65
+ var CONFIG_PATH = path.join(
66
+ os.homedir(),
67
+ ".mindstudio-local-tunnel",
68
+ "config.json"
69
+ );
70
+ var DEFAULT_BASE_URL = "https://api.mindstudio.ai";
71
+ function loadConfigFile() {
72
+ try {
73
+ const raw = fs2.readFileSync(CONFIG_PATH, "utf-8");
74
+ log.debug("Loaded config file", { path: CONFIG_PATH });
75
+ return JSON.parse(raw);
76
+ } catch (err) {
77
+ log.debug("No config file found", {
78
+ path: CONFIG_PATH,
79
+ error: err.message
80
+ });
81
+ return {};
82
+ }
83
+ }
84
+ function resolveConfig(flags) {
85
+ const file = loadConfigFile();
86
+ const activeEnv = file.environment || "prod";
87
+ const env = file.environments?.[activeEnv];
88
+ const apiKey = flags?.apiKey || process.env.MINDSTUDIO_API_KEY || env?.apiKey || "";
89
+ const baseUrl = flags?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
90
+ if (!apiKey) {
91
+ log.error("No API key found");
92
+ throw new Error(
93
+ "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
94
+ );
95
+ }
96
+ const keySource = flags?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
97
+ log.info("Config resolved", {
98
+ baseUrl,
99
+ keySource,
100
+ environment: activeEnv
101
+ });
102
+ return { apiKey, baseUrl };
103
+ }
104
+
105
+ // src/prompt/index.ts
106
+ import fs4 from "fs";
107
+ import path3 from "path";
108
+
109
+ // src/tools/_helpers/lsp.ts
110
+ var lspBaseUrl = null;
111
+ function setLspBaseUrl(url) {
112
+ lspBaseUrl = url;
113
+ log.info("LSP configured", { url });
114
+ }
115
+ function isLspConfigured() {
116
+ return lspBaseUrl !== null;
117
+ }
118
+ async function lspRequest(endpoint, body) {
119
+ if (!lspBaseUrl) {
120
+ throw new Error("LSP not available");
121
+ }
122
+ const url = `${lspBaseUrl}${endpoint}`;
123
+ log.debug("LSP request", { endpoint, body });
124
+ try {
125
+ const res = await fetch(url, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify(body)
129
+ });
130
+ if (!res.ok) {
131
+ log.error("LSP sidecar error", { endpoint, status: res.status });
132
+ throw new Error(`LSP sidecar error: ${res.status}`);
133
+ }
134
+ return res.json();
135
+ } catch (err) {
136
+ if (err.message.startsWith("LSP sidecar")) {
137
+ throw err;
138
+ }
139
+ log.error("LSP connection error", { endpoint, error: err.message });
140
+ throw new Error(`LSP connection error: ${err.message}`);
141
+ }
142
+ }
143
+
144
+ // src/prompt/static/projectContext.ts
145
+ import fs3 from "fs";
146
+ import path2 from "path";
147
+ var AGENT_INSTRUCTION_FILES = [
148
+ "CLAUDE.md",
149
+ "claude.md",
150
+ ".claude/instructions.md",
151
+ "AGENTS.md",
152
+ "agents.md",
153
+ ".agents.md",
154
+ "COPILOT.md",
155
+ "copilot.md",
156
+ ".copilot-instructions.md",
157
+ ".github/copilot-instructions.md",
158
+ "REMY.md",
159
+ "remy.md",
160
+ ".cursorrules",
161
+ ".cursorules"
162
+ ];
163
+ function loadProjectInstructions() {
164
+ for (const file of AGENT_INSTRUCTION_FILES) {
165
+ try {
166
+ const content = fs3.readFileSync(file, "utf-8").trim();
167
+ if (content) {
168
+ return `
169
+ ## Project Instructions (${file})
170
+ ${content}`;
171
+ }
172
+ } catch {
173
+ }
174
+ }
175
+ return "";
176
+ }
177
+ function loadProjectManifest() {
178
+ try {
179
+ const manifest = fs3.readFileSync("mindstudio.json", "utf-8");
180
+ return `
181
+ ## Project Manifest (mindstudio.json)
182
+ \`\`\`json
183
+ ${manifest}
184
+ \`\`\``;
185
+ } catch {
186
+ return "";
187
+ }
188
+ }
189
+ function loadSpecFileMetadata() {
190
+ try {
191
+ const files = walkMdFiles("src");
192
+ if (files.length === 0) {
193
+ return "";
194
+ }
195
+ const entries = [];
196
+ for (const filePath of files) {
197
+ const { name, description } = parseFrontmatter(filePath);
198
+ let line = `- ${filePath}`;
199
+ if (name) {
200
+ line += ` \u2014 "${name}"`;
201
+ }
202
+ if (description) {
203
+ line += ` \u2014 ${description}`;
204
+ }
205
+ entries.push(line);
206
+ }
207
+ return `
208
+ ## Spec Files
209
+ ${entries.join("\n")}`;
210
+ } catch {
211
+ return "";
212
+ }
213
+ }
214
+ function walkMdFiles(dir) {
215
+ const results = [];
216
+ try {
217
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
218
+ for (const entry of entries) {
219
+ const full = path2.join(dir, entry.name);
220
+ if (entry.isDirectory()) {
221
+ results.push(...walkMdFiles(full));
222
+ } else if (entry.name.endsWith(".md")) {
223
+ results.push(full);
224
+ }
225
+ }
226
+ } catch {
227
+ }
228
+ return results.sort();
229
+ }
230
+ function parseFrontmatter(filePath) {
231
+ try {
232
+ const content = fs3.readFileSync(filePath, "utf-8");
233
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
234
+ if (!match) {
235
+ return { name: "", description: "" };
236
+ }
237
+ const fm = match[1];
238
+ const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
239
+ const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
240
+ return { name, description };
241
+ } catch {
242
+ return { name: "", description: "" };
243
+ }
244
+ }
245
+ function loadProjectFileListing() {
246
+ try {
247
+ const entries = fs3.readdirSync(".", { withFileTypes: true });
248
+ const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
249
+ if (a.isDirectory() && !b.isDirectory()) {
250
+ return -1;
251
+ }
252
+ if (!a.isDirectory() && b.isDirectory()) {
253
+ return 1;
254
+ }
255
+ return a.name.localeCompare(b.name);
256
+ }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
257
+ return `
258
+ ## Project Files
259
+ \`\`\`
260
+ ${listing}
261
+ \`\`\``;
262
+ } catch {
263
+ return "";
264
+ }
265
+ }
266
+
267
+ // src/prompt/index.ts
268
+ var PROMPT_DIR = import.meta.dirname ?? path3.dirname(new URL(import.meta.url).pathname);
269
+ function requireFile(filePath) {
270
+ const full = path3.join(PROMPT_DIR, filePath);
271
+ try {
272
+ return fs4.readFileSync(full, "utf-8").trim();
273
+ } catch {
274
+ throw new Error(`Required prompt file missing: ${full}`);
275
+ }
276
+ }
277
+ function resolveIncludes(template) {
278
+ const result = template.replace(
279
+ /\{\{([^}]+)\}\}/g,
280
+ (_, filePath) => requireFile(filePath.trim())
281
+ );
282
+ return result.replace(/\n{3,}/g, "\n\n").trim();
283
+ }
284
+ function buildSystemPrompt(projectHasCode, viewContext) {
285
+ const projectContext = [
286
+ loadProjectInstructions(),
287
+ loadProjectManifest(),
288
+ loadSpecFileMetadata(),
289
+ loadProjectFileListing()
290
+ ].filter(Boolean).join("\n");
291
+ const now = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
292
+ dateStyle: "full",
293
+ timeStyle: "long"
294
+ });
295
+ const template = `
296
+ {{static/identity.md}}
297
+
298
+ The current date is ${now}.
299
+
300
+ <platform_docs>
301
+ <platform>
302
+ {{compiled/platform.md}}
303
+ </platform>
304
+
305
+ <manifest>
306
+ {{compiled/manifest.md}}
307
+ </manifest>
308
+
309
+ <tables>
310
+ {{compiled/tables.md}}
311
+ </tables>
312
+
313
+ <methods>
314
+ {{compiled/methods.md}}
315
+ </methods>
316
+
317
+ <auth>
318
+ {{compiled/auth.md}}
319
+ </auth>
320
+
321
+ <dev_and_deploy>
322
+ {{compiled/dev-and-deploy.md}}
323
+ </dev_and_deploy>
324
+
325
+ <design>
326
+ {{compiled/design.md}}
327
+ </design>
328
+
329
+ <media_cdn>
330
+ {{compiled/media-cdn.md}}
331
+ </media_cdn>
332
+
333
+ <interfaces>
334
+ {{compiled/interfaces.md}}
335
+ </interfaces>
336
+
337
+ <scenarios>
338
+ {{compiled/scenarios.md}}
339
+ </scenarios>
340
+ </platform_docs>
341
+
342
+ <mindstudio_agent_sdk_docs>
343
+ {{compiled/sdk-actions.md}}
344
+ </mindstudio_agent_sdk_docs>
345
+
346
+ <mindstudio_flavored_markdown_spec_docs>
347
+ {{compiled/msfm.md}}
348
+ </mindstudio_flavored_markdown_spec_docs>
349
+
350
+ <project_context>
351
+ ${projectContext}
352
+ </project_context>
353
+
354
+ ${isLspConfigured() ? `<lsp>
355
+ {{static/lsp.md}}
356
+ </lsp>` : ""}
357
+
358
+ {{static/intake.md}}
359
+
360
+ {{static/authoring.md}}
361
+
362
+ {{static/instructions.md}}
363
+
364
+ <current_authoring_mode>
365
+ ${projectHasCode ? "Project has code - keep code and spec in sync." : "Project does not have code yet - focus on writing the spec."}
366
+ </current_authoring_mode>
367
+
368
+ <view_context>
369
+ The user is currently in ${viewContext?.mode ?? "code"} mode.
370
+ ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
371
+ </view_context>
372
+ `;
373
+ return resolveIncludes(template);
374
+ }
375
+
376
+ // src/api.ts
377
+ async function* streamChat(params) {
378
+ const { baseUrl, apiKey, signal, ...body } = params;
379
+ const url = `${baseUrl}/_internal/v2/agent/chat`;
380
+ const startTime = Date.now();
381
+ const messagesWithAttachments = body.messages.filter(
382
+ (m) => m.attachments && m.attachments.length > 0
383
+ );
384
+ log.info("POST agent/chat", {
385
+ url,
386
+ model: body.model,
387
+ messageCount: body.messages.length,
388
+ toolCount: body.tools.length,
389
+ ...messagesWithAttachments.length > 0 && {
390
+ attachments: messagesWithAttachments.map((m) => ({
391
+ role: m.role,
392
+ attachmentCount: m.attachments.length,
393
+ urls: m.attachments.map((a) => a.url)
394
+ }))
395
+ }
396
+ });
397
+ let res;
398
+ try {
399
+ res = await fetch(url, {
400
+ method: "POST",
401
+ headers: {
402
+ "Content-Type": "application/json",
403
+ Authorization: `Bearer ${apiKey}`
404
+ },
405
+ body: JSON.stringify(body),
406
+ signal
407
+ });
408
+ } catch (err) {
409
+ if (signal?.aborted) {
410
+ log.info("Request aborted by signal");
411
+ throw err;
412
+ }
413
+ log.error("Network error", { error: err.message });
414
+ yield { type: "error", error: `Network error: ${err.message}` };
415
+ return;
416
+ }
417
+ const ttfb = Date.now() - startTime;
418
+ log.info(`Response ${res.status}`, { ttfb: `${ttfb}ms` });
419
+ if (!res.ok) {
420
+ let errorMessage = `HTTP ${res.status}`;
421
+ try {
422
+ const body2 = await res.json();
423
+ if (body2.error) {
424
+ errorMessage = body2.error;
425
+ }
426
+ if (body2.errorMessage) {
427
+ errorMessage = body2.errorMessage;
428
+ }
429
+ } catch {
430
+ }
431
+ log.error("API error", { status: res.status, error: errorMessage });
432
+ yield { type: "error", error: errorMessage };
433
+ return;
434
+ }
435
+ const reader = res.body.getReader();
436
+ const decoder = new TextDecoder();
437
+ let buffer = "";
438
+ while (true) {
439
+ const { done, value } = await reader.read();
440
+ if (done) {
441
+ break;
442
+ }
443
+ buffer += decoder.decode(value, { stream: true });
444
+ const lines = buffer.split("\n");
445
+ buffer = lines.pop() ?? "";
446
+ for (const line of lines) {
447
+ if (!line.startsWith("data: ")) {
448
+ continue;
449
+ }
450
+ try {
451
+ const event = JSON.parse(line.slice(6));
452
+ if (event.type === "done") {
453
+ const elapsed = Date.now() - startTime;
454
+ log.info("Stream complete", {
455
+ elapsed: `${elapsed}ms`,
456
+ stopReason: event.stopReason,
457
+ inputTokens: event.usage.inputTokens,
458
+ outputTokens: event.usage.outputTokens
459
+ });
460
+ }
461
+ yield event;
462
+ } catch {
463
+ }
464
+ }
465
+ }
466
+ if (buffer.startsWith("data: ")) {
467
+ try {
468
+ yield JSON.parse(buffer.slice(6));
469
+ } catch {
470
+ }
471
+ }
472
+ }
473
+
474
+ // src/tools/spec/readSpec.ts
475
+ import fs5 from "fs/promises";
476
+
477
+ // src/tools/spec/_helpers.ts
478
+ var HEADING_RE = /^(#{1,6})\s+(.+)$/;
479
+ function parseHeadings(content) {
480
+ const lines = content.split("\n");
481
+ const headings = [];
482
+ for (let i = 0; i < lines.length; i++) {
483
+ const match = lines[i].match(HEADING_RE);
484
+ if (match) {
485
+ headings.push({
486
+ level: match[1].length,
487
+ text: match[2].trim(),
488
+ startLine: i,
489
+ contentStart: i + 1,
490
+ contentEnd: lines.length
491
+ // placeholder — resolved below
492
+ });
493
+ }
494
+ }
495
+ for (let i = 0; i < headings.length; i++) {
496
+ const current = headings[i];
497
+ let end = lines.length;
498
+ for (let j = i + 1; j < headings.length; j++) {
499
+ if (headings[j].level <= current.level) {
500
+ end = headings[j].startLine;
501
+ break;
502
+ }
503
+ }
504
+ current.contentEnd = end;
505
+ }
506
+ return headings;
507
+ }
508
+ function resolveHeadingPath(content, headingPath) {
509
+ const lines = content.split("\n");
510
+ const headings = parseHeadings(content);
511
+ if (headingPath === "") {
512
+ const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
513
+ return {
514
+ startLine: 0,
515
+ contentStart: 0,
516
+ contentEnd: firstHeadingLine
517
+ };
518
+ }
519
+ const segments = headingPath.split(">").map((s) => s.trim());
520
+ let searchStart = 0;
521
+ let searchEnd = lines.length;
522
+ let resolved = null;
523
+ for (let si = 0; si < segments.length; si++) {
524
+ const segment = segments[si].toLowerCase();
525
+ const candidates = headings.filter(
526
+ (h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
527
+ );
528
+ if (candidates.length === 0) {
529
+ const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
530
+ const searchedPath = segments.slice(0, si + 1).join(" > ");
531
+ throw new Error(
532
+ `Heading not found: "${searchedPath}"
533
+
534
+ Available headings:
535
+ ${available || "(none)"}`
536
+ );
537
+ }
538
+ resolved = candidates[0];
539
+ searchStart = resolved.contentStart;
540
+ searchEnd = resolved.contentEnd;
541
+ }
542
+ return {
543
+ startLine: resolved.startLine,
544
+ contentStart: resolved.contentStart,
545
+ contentEnd: resolved.contentEnd
546
+ };
547
+ }
548
+ function validateSpecPath(filePath) {
549
+ if (!filePath.startsWith("src/")) {
550
+ throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
551
+ }
552
+ return filePath;
553
+ }
554
+ function getHeadingTree(content) {
555
+ const headings = parseHeadings(content);
556
+ if (headings.length === 0) {
557
+ return "(no headings)";
558
+ }
559
+ return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
560
+ }
561
+
562
+ // src/tools/spec/readSpec.ts
563
+ var DEFAULT_MAX_LINES = 500;
564
+ var readSpecTool = {
565
+ definition: {
566
+ name: "readSpec",
567
+ description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
568
+ inputSchema: {
569
+ type: "object",
570
+ properties: {
571
+ path: {
572
+ type: "string",
573
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
574
+ },
575
+ offset: {
576
+ type: "number",
577
+ description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
578
+ },
579
+ maxLines: {
580
+ type: "number",
581
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
582
+ }
583
+ },
584
+ required: ["path"]
585
+ }
586
+ },
587
+ async execute(input) {
588
+ try {
589
+ validateSpecPath(input.path);
590
+ } catch (err) {
591
+ return `Error: ${err.message}`;
592
+ }
593
+ try {
594
+ const content = await fs5.readFile(input.path, "utf-8");
595
+ const allLines = content.split("\n");
596
+ const totalLines = allLines.length;
597
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
598
+ let startIdx;
599
+ if (input.offset && input.offset < 0) {
600
+ startIdx = Math.max(0, totalLines + input.offset);
601
+ } else {
602
+ startIdx = Math.max(0, (input.offset || 1) - 1);
603
+ }
604
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
605
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
606
+ let result = numbered;
607
+ const endLine = startIdx + sliced.length;
608
+ const displayStart = startIdx + 1;
609
+ if (endLine < totalLines) {
610
+ result += `
611
+
612
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
613
+ }
614
+ return result;
615
+ } catch (err) {
616
+ return `Error reading file: ${err.message}`;
617
+ }
618
+ }
619
+ };
620
+
621
+ // src/tools/spec/writeSpec.ts
622
+ import fs6 from "fs/promises";
623
+ import path4 from "path";
624
+
625
+ // src/tools/_helpers/diff.ts
626
+ var CONTEXT_LINES = 3;
627
+ function unifiedDiff(filePath, oldText, newText) {
628
+ const oldLines = oldText.split("\n");
629
+ const newLines = newText.split("\n");
630
+ let firstDiff = 0;
631
+ while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
632
+ firstDiff++;
633
+ }
634
+ let oldEnd = oldLines.length - 1;
635
+ let newEnd = newLines.length - 1;
636
+ while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
637
+ oldEnd--;
638
+ newEnd--;
639
+ }
640
+ const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
641
+ const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
642
+ const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
643
+ const lines = [];
644
+ lines.push(`--- ${filePath}`);
645
+ lines.push(`+++ ${filePath}`);
646
+ lines.push(
647
+ `@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
648
+ );
649
+ for (let i = ctxStart; i < firstDiff; i++) {
650
+ lines.push(` ${oldLines[i]}`);
651
+ }
652
+ for (let i = firstDiff; i <= oldEnd; i++) {
653
+ lines.push(`-${oldLines[i]}`);
654
+ }
655
+ for (let i = firstDiff; i <= newEnd; i++) {
656
+ lines.push(`+${newLines[i]}`);
657
+ }
658
+ for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
659
+ lines.push(` ${oldLines[i]}`);
660
+ }
661
+ return lines.join("\n");
662
+ }
663
+
664
+ // src/tools/spec/writeSpec.ts
665
+ var writeSpecTool = {
666
+ definition: {
667
+ name: "writeSpec",
668
+ description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
669
+ inputSchema: {
670
+ type: "object",
671
+ properties: {
672
+ path: {
673
+ type: "string",
674
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
675
+ },
676
+ content: {
677
+ type: "string",
678
+ description: "The full MSFM markdown content to write."
679
+ }
680
+ },
681
+ required: ["path", "content"]
682
+ }
683
+ },
684
+ streaming: {
685
+ transform: async (partial) => {
686
+ const oldContent = await fs6.readFile(partial.path, "utf-8").catch(() => "");
687
+ const lineCount = partial.content.split("\n").length;
688
+ return `Writing ${partial.path} (${lineCount} lines)
689
+ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
690
+ }
691
+ },
692
+ async execute(input) {
693
+ try {
694
+ validateSpecPath(input.path);
695
+ } catch (err) {
696
+ return `Error: ${err.message}`;
697
+ }
698
+ try {
699
+ await fs6.mkdir(path4.dirname(input.path), { recursive: true });
700
+ let oldContent = null;
701
+ try {
702
+ oldContent = await fs6.readFile(input.path, "utf-8");
703
+ } catch {
704
+ }
705
+ await fs6.writeFile(input.path, input.content, "utf-8");
706
+ const lineCount = input.content.split("\n").length;
707
+ const label = oldContent !== null ? "Wrote" : "Created";
708
+ return `${label} ${input.path} (${lineCount} lines)
709
+ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
710
+ } catch (err) {
711
+ return `Error writing file: ${err.message}`;
712
+ }
713
+ }
714
+ };
715
+
716
+ // src/tools/spec/editSpec.ts
717
+ import fs7 from "fs/promises";
718
+ var editSpecTool = {
719
+ definition: {
720
+ name: "editSpec",
721
+ description: 'Make targeted edits to a spec file by heading path. This is the primary tool for modifying existing specs. Each edit targets a section by its heading hierarchy (e.g., "Vendors > Approval Flow") and applies an operation. Multiple edits are applied in order.',
722
+ inputSchema: {
723
+ type: "object",
724
+ properties: {
725
+ path: {
726
+ type: "string",
727
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
728
+ },
729
+ edits: {
730
+ type: "array",
731
+ items: {
732
+ type: "object",
733
+ properties: {
734
+ heading: {
735
+ type: "string",
736
+ description: 'Heading path using " > " to separate nesting levels (e.g., "Vendors > Approval Flow"). Empty string targets the preamble (content before the first heading).'
737
+ },
738
+ operation: {
739
+ type: "string",
740
+ enum: ["replace", "insert_after", "insert_before", "delete"],
741
+ description: "replace: swap content under this heading (keeps the heading line). insert_after: add content after this section. insert_before: add content before this heading. delete: remove this heading and all its content."
742
+ },
743
+ content: {
744
+ type: "string",
745
+ description: "MSFM markdown content for replace/insert operations. Not needed for delete."
746
+ }
747
+ },
748
+ required: ["heading", "operation"]
749
+ },
750
+ description: "Array of edits to apply in order."
751
+ }
752
+ },
753
+ required: ["path", "edits"]
754
+ }
755
+ },
756
+ async execute(input) {
757
+ try {
758
+ validateSpecPath(input.path);
759
+ } catch (err) {
760
+ return `Error: ${err.message}`;
761
+ }
762
+ let originalContent;
763
+ try {
764
+ originalContent = await fs7.readFile(input.path, "utf-8");
765
+ } catch (err) {
766
+ return `Error reading file: ${err.message}`;
767
+ }
768
+ let content = originalContent;
769
+ for (const edit of input.edits) {
770
+ let range;
771
+ try {
772
+ range = resolveHeadingPath(content, edit.heading);
773
+ } catch (err) {
774
+ const tree = getHeadingTree(content);
775
+ return `Error: ${err.message}
776
+
777
+ Document structure:
778
+ ${tree}`;
779
+ }
780
+ const lines = content.split("\n");
781
+ switch (edit.operation) {
782
+ case "replace": {
783
+ if (edit.content == null) {
784
+ return 'Error: "content" is required for replace operations.';
785
+ }
786
+ const contentLines = edit.content.split("\n");
787
+ lines.splice(
788
+ range.contentStart,
789
+ range.contentEnd - range.contentStart,
790
+ ...contentLines
791
+ );
792
+ break;
793
+ }
794
+ case "insert_after": {
795
+ if (edit.content == null) {
796
+ return 'Error: "content" is required for insert_after operations.';
797
+ }
798
+ const contentLines = edit.content.split("\n");
799
+ lines.splice(range.contentEnd, 0, ...contentLines);
800
+ break;
801
+ }
802
+ case "insert_before": {
803
+ if (edit.content == null) {
804
+ return 'Error: "content" is required for insert_before operations.';
805
+ }
806
+ const contentLines = edit.content.split("\n");
807
+ lines.splice(range.startLine, 0, ...contentLines);
808
+ break;
809
+ }
810
+ case "delete": {
811
+ lines.splice(range.startLine, range.contentEnd - range.startLine);
812
+ break;
813
+ }
814
+ default:
815
+ return `Error: Unknown operation "${edit.operation}". Use replace, insert_after, insert_before, or delete.`;
816
+ }
817
+ content = lines.join("\n");
818
+ }
819
+ try {
820
+ await fs7.writeFile(input.path, content, "utf-8");
821
+ } catch (err) {
822
+ return `Error writing file: ${err.message}`;
823
+ }
824
+ return unifiedDiff(input.path, originalContent, content);
825
+ }
826
+ };
827
+
828
+ // src/tools/spec/listSpecFiles.ts
829
+ import fs8 from "fs/promises";
830
+ import path5 from "path";
831
+ var listSpecFilesTool = {
832
+ definition: {
833
+ name: "listSpecFiles",
834
+ description: "List all files in the src/ directory (spec files, brand guidelines, interface specs, references). Use this to understand what spec files exist before reading or editing them.",
835
+ inputSchema: {
836
+ type: "object",
837
+ properties: {},
838
+ required: []
839
+ }
840
+ },
841
+ async execute() {
842
+ try {
843
+ const entries = await listRecursive("src");
844
+ if (entries.length === 0) {
845
+ return "src/ is empty \u2014 no spec files yet.";
846
+ }
847
+ return entries.join("\n");
848
+ } catch (err) {
849
+ if (err.code === "ENOENT") {
850
+ return "Error: src/ directory does not exist.";
851
+ }
852
+ return `Error listing spec files: ${err.message}`;
853
+ }
854
+ }
855
+ };
856
+ async function listRecursive(dir) {
857
+ const results = [];
858
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
859
+ entries.sort((a, b) => {
860
+ if (a.isDirectory() && !b.isDirectory()) {
861
+ return -1;
862
+ }
863
+ if (!a.isDirectory() && b.isDirectory()) {
864
+ return 1;
865
+ }
866
+ return a.name.localeCompare(b.name);
867
+ });
868
+ for (const entry of entries) {
869
+ const fullPath = path5.join(dir, entry.name);
870
+ if (entry.isDirectory()) {
871
+ results.push(`${fullPath}/`);
872
+ results.push(...await listRecursive(fullPath));
873
+ } else {
874
+ results.push(fullPath);
875
+ }
876
+ }
877
+ return results;
878
+ }
879
+
880
+ // src/tools/spec/setViewMode.ts
881
+ var setViewModeTool = {
882
+ definition: {
883
+ name: "setViewMode",
884
+ description: 'Switch the IDE view mode. Use this to navigate the user to the right context. When transitioning from intake to spec, write the first spec file BEFORE calling this \u2014 the user needs something to see when the spec editor opens. Switch to "code" during code generation, then to "preview" when done so the user sees the result.',
885
+ inputSchema: {
886
+ type: "object",
887
+ properties: {
888
+ mode: {
889
+ type: "string",
890
+ enum: [
891
+ "intake",
892
+ "preview",
893
+ "spec",
894
+ "code",
895
+ "databases",
896
+ "scenarios",
897
+ "logs"
898
+ ],
899
+ description: "The view mode to switch to."
900
+ }
901
+ },
902
+ required: ["mode"]
903
+ }
904
+ },
905
+ async execute() {
906
+ return "View mode updated.";
907
+ }
908
+ };
909
+
910
+ // src/tools/spec/promptUser.ts
911
+ var promptUserTool = {
912
+ definition: {
913
+ name: "promptUser",
914
+ description: 'Ask the user structured questions. Choose type first: "form" for structured intake (5+ questions, takes over screen), "inline" for quick clarifications or confirmations. Blocks until the user responds. Result contains `_dismissed: true` if the user dismisses without answering.',
915
+ inputSchema: {
916
+ type: "object",
917
+ properties: {
918
+ type: {
919
+ type: "string",
920
+ enum: ["form", "inline"],
921
+ description: "Choose this first, before writing questions. form: full form for structured intake with many questions. inline: compact in-chat display."
922
+ },
923
+ questions: {
924
+ type: "array",
925
+ items: {
926
+ type: "object",
927
+ properties: {
928
+ id: {
929
+ type: "string",
930
+ description: "Unique identifier for this question. Used as the key in the response object."
931
+ },
932
+ question: {
933
+ type: "string",
934
+ description: "The question to ask."
935
+ },
936
+ type: {
937
+ type: "string",
938
+ enum: ["select", "text", "confirm", "file", "color"],
939
+ description: "select: pick from options. text: free-form input. confirm: yes/no. file: file/image upload \u2014 returns CDN URL(s) that can be referenced directly or curled onto disk. color: color picker (returns hex)."
940
+ },
941
+ helpText: {
942
+ type: "string",
943
+ description: "Optional detail rendered below the question as a subtitle."
944
+ },
945
+ required: {
946
+ type: "boolean",
947
+ description: "Whether the user must answer this question. Defaults to false."
948
+ },
949
+ options: {
950
+ type: "array",
951
+ items: {
952
+ oneOf: [
953
+ { type: "string" },
954
+ {
955
+ type: "object",
956
+ properties: {
957
+ label: {
958
+ type: "string",
959
+ description: "The option text."
960
+ },
961
+ description: {
962
+ type: "string",
963
+ description: "Optional detail shown below the label."
964
+ }
965
+ },
966
+ required: ["label"]
967
+ }
968
+ ]
969
+ },
970
+ description: "Options for select type. Each can be a string or { label, description }."
971
+ },
972
+ multiple: {
973
+ type: "boolean",
974
+ description: "For select: allow picking multiple options (returns array). For file: allow multiple uploads (returns array of URLs). Defaults to false."
975
+ },
976
+ allowOther: {
977
+ type: "boolean",
978
+ description: 'For select type: adds an "Other" option with a free-form text input. Defaults to false.'
979
+ },
980
+ format: {
981
+ type: "string",
982
+ enum: ["email", "url", "phone", "number"],
983
+ description: "For text type: adds input validation and mobile keyboard hints."
984
+ },
985
+ placeholder: {
986
+ type: "string",
987
+ description: "For text type: placeholder hint text."
988
+ },
989
+ accept: {
990
+ type: "string",
991
+ description: 'For file type: comma-separated mime types, like HTML input accept (e.g. "image/*", "image/*,video/*", "application/pdf"). Omit to accept all file types.'
992
+ }
993
+ },
994
+ required: ["id", "question", "type"]
995
+ },
996
+ description: "One or more questions to present."
997
+ }
998
+ },
999
+ required: ["type", "questions"]
1000
+ }
1001
+ },
1002
+ streaming: {
1003
+ partialInput: (partial, lastCount) => {
1004
+ const questions = partial.questions;
1005
+ if (!Array.isArray(questions) || questions.length === 0) {
1006
+ return null;
1007
+ }
1008
+ const hasType = typeof partial.type === "string";
1009
+ if (!hasType && questions.length < 3) {
1010
+ return null;
1011
+ }
1012
+ const confirmed = questions.length > 1 ? questions.slice(0, -1) : [];
1013
+ if (confirmed.length <= lastCount) {
1014
+ return null;
1015
+ }
1016
+ return {
1017
+ input: {
1018
+ ...partial,
1019
+ type: partial.type ?? "inline",
1020
+ questions: confirmed
1021
+ },
1022
+ emittedCount: confirmed.length
1023
+ };
1024
+ }
1025
+ },
1026
+ async execute(input) {
1027
+ const questions = input.questions;
1028
+ const lines = questions.map((q) => {
1029
+ let line = `- ${q.question}`;
1030
+ if (q.type === "select") {
1031
+ const opts = (q.options || []).map(
1032
+ (o) => typeof o === "string" ? o : o.label
1033
+ );
1034
+ line += q.multiple ? ` (pick one or more: ${opts.join(" / ")})` : ` (${opts.join(" / ")})`;
1035
+ } else if (q.type === "confirm") {
1036
+ line += " (yes / no)";
1037
+ } else if (q.type === "file") {
1038
+ line += " (upload file)";
1039
+ } else if (q.type === "color") {
1040
+ line += " (pick a color)";
1041
+ }
1042
+ return line;
1043
+ });
1044
+ return `Please answer these questions:
1045
+ ${lines.join("\n")}`;
1046
+ }
1047
+ };
1048
+
1049
+ // src/tools/spec/clearSyncStatus.ts
1050
+ var clearSyncStatusTool = {
1051
+ definition: {
1052
+ name: "clearSyncStatus",
1053
+ description: "Clear the sync status flags after syncing spec and code. Call this after finishing a sync operation.",
1054
+ inputSchema: {
1055
+ type: "object",
1056
+ properties: {}
1057
+ }
1058
+ },
1059
+ async execute() {
1060
+ return "ok";
1061
+ }
1062
+ };
1063
+
1064
+ // src/tools/spec/presentSyncPlan.ts
1065
+ var presentSyncPlanTool = {
1066
+ definition: {
1067
+ name: "presentSyncPlan",
1068
+ description: "Present a structured sync plan to the user for approval. Write a clear markdown summary of what changed and what you intend to update. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE making any sync edits.",
1069
+ inputSchema: {
1070
+ type: "object",
1071
+ properties: {
1072
+ content: {
1073
+ type: "string",
1074
+ description: "Markdown plan describing what changed and what will be updated."
1075
+ }
1076
+ },
1077
+ required: ["content"]
1078
+ }
1079
+ },
1080
+ streaming: {},
1081
+ async execute() {
1082
+ return "approved";
1083
+ }
1084
+ };
1085
+
1086
+ // src/tools/spec/presentPublishPlan.ts
1087
+ var presentPublishPlanTool = {
1088
+ definition: {
1089
+ name: "presentPublishPlan",
1090
+ description: "Present a publish changelog to the user for approval. Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
1091
+ inputSchema: {
1092
+ type: "object",
1093
+ properties: {
1094
+ content: {
1095
+ type: "string",
1096
+ description: "Markdown changelog describing what changed and what will be deployed."
1097
+ }
1098
+ },
1099
+ required: ["content"]
1100
+ }
1101
+ },
1102
+ streaming: {},
1103
+ async execute() {
1104
+ return "approved";
1105
+ }
1106
+ };
1107
+
1108
+ // src/tools/spec/presentPlan.ts
1109
+ var presentPlanTool = {
1110
+ definition: {
1111
+ name: "presentPlan",
1112
+ description: "Present an implementation plan for user approval before making changes. Use this only for large, multi-step changes or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. If the user rejects with feedback, revise and present again.",
1113
+ inputSchema: {
1114
+ type: "object",
1115
+ properties: {
1116
+ content: {
1117
+ type: "string",
1118
+ description: "Markdown plan describing what you intend to do."
1119
+ }
1120
+ },
1121
+ required: ["content"]
1122
+ }
1123
+ },
1124
+ streaming: {},
1125
+ async execute() {
1126
+ return "approved";
1127
+ }
1128
+ };
1129
+
1130
+ // src/tools/code/readFile.ts
1131
+ import fs9 from "fs/promises";
1132
+ var DEFAULT_MAX_LINES2 = 500;
1133
+ function isBinary(buffer) {
1134
+ const sample = buffer.subarray(0, 8192);
1135
+ for (let i = 0; i < sample.length; i++) {
1136
+ if (sample[i] === 0) {
1137
+ return true;
1138
+ }
1139
+ }
1140
+ return false;
1141
+ }
1142
+ var readFileTool = {
1143
+ definition: {
1144
+ name: "readFile",
1145
+ description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. For large files, consider using symbols first to identify the relevant section, then use offset and maxLines to read just that section. Line numbers in the output correspond to what editFile expects. Defaults to first 500 lines. Use a negative offset to read from the end of the file (e.g., offset: -50 reads the last 50 lines).",
1146
+ inputSchema: {
1147
+ type: "object",
1148
+ properties: {
1149
+ path: {
1150
+ type: "string",
1151
+ description: "The file path to read, relative to the project root."
1152
+ },
1153
+ offset: {
1154
+ type: "number",
1155
+ description: "Line number to start reading from (1-indexed). Use a negative number to read from the end (e.g., -50 reads the last 50 lines). Defaults to 1."
1156
+ },
1157
+ maxLines: {
1158
+ type: "number",
1159
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
1160
+ }
1161
+ },
1162
+ required: ["path"]
1163
+ }
1164
+ },
1165
+ async execute(input) {
1166
+ try {
1167
+ const buffer = await fs9.readFile(input.path);
1168
+ if (isBinary(buffer)) {
1169
+ const size = buffer.length;
1170
+ const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
1171
+ return `Error: ${input.path} appears to be a binary file (${unit}). Use bash to inspect it if needed.`;
1172
+ }
1173
+ const content = buffer.toString("utf-8");
1174
+ const allLines = content.split("\n");
1175
+ const totalLines = allLines.length;
1176
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
1177
+ let startIdx;
1178
+ if (input.offset && input.offset < 0) {
1179
+ startIdx = Math.max(0, totalLines + input.offset);
1180
+ } else {
1181
+ startIdx = Math.max(0, (input.offset || 1) - 1);
1182
+ }
1183
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
1184
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
1185
+ let result = numbered;
1186
+ const endLine = startIdx + sliced.length;
1187
+ const displayStart = startIdx + 1;
1188
+ if (endLine < totalLines) {
1189
+ result += `
1190
+
1191
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
1192
+ }
1193
+ return result;
1194
+ } catch (err) {
1195
+ return `Error reading file: ${err.message}`;
1196
+ }
1197
+ }
1198
+ };
1199
+
1200
+ // src/tools/code/writeFile.ts
1201
+ import fs10 from "fs/promises";
1202
+ import path6 from "path";
1203
+ var writeFileTool = {
1204
+ definition: {
1205
+ name: "writeFile",
1206
+ description: "Create a new file or completely overwrite an existing one. Parent directories are created automatically. Use this for new files or full rewrites. For targeted changes to existing files, use editFile instead \u2014 it preserves the parts you don't want to change and avoids errors from forgetting to include unchanged code.",
1207
+ inputSchema: {
1208
+ type: "object",
1209
+ properties: {
1210
+ path: {
1211
+ type: "string",
1212
+ description: "The file path to write, relative to the project root."
1213
+ },
1214
+ content: {
1215
+ type: "string",
1216
+ description: "The full content to write to the file."
1217
+ }
1218
+ },
1219
+ required: ["path", "content"]
1220
+ }
1221
+ },
1222
+ streaming: {
1223
+ transform: async (partial) => {
1224
+ const oldContent = await fs10.readFile(partial.path, "utf-8").catch(() => "");
1225
+ const lineCount = partial.content.split("\n").length;
1226
+ return `Writing ${partial.path} (${lineCount} lines)
1227
+ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
1228
+ }
1229
+ },
1230
+ async execute(input) {
1231
+ try {
1232
+ await fs10.mkdir(path6.dirname(input.path), { recursive: true });
1233
+ let oldContent = null;
1234
+ try {
1235
+ oldContent = await fs10.readFile(input.path, "utf-8");
1236
+ } catch {
1237
+ }
1238
+ await fs10.writeFile(input.path, input.content, "utf-8");
1239
+ const lineCount = input.content.split("\n").length;
1240
+ const label = oldContent !== null ? "Wrote" : "Created";
1241
+ return `${label} ${input.path} (${lineCount} lines)
1242
+ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1243
+ } catch (err) {
1244
+ return `Error writing file: ${err.message}`;
1245
+ }
1246
+ }
1247
+ };
1248
+
1249
+ // src/tools/code/editFile/index.ts
1250
+ import fs11 from "fs/promises";
1251
+
1252
+ // src/tools/code/editFile/_helpers.ts
1253
+ function buildLineOffsets(content) {
1254
+ const offsets = [0];
1255
+ for (let i = 0; i < content.length; i++) {
1256
+ if (content[i] === "\n") {
1257
+ offsets.push(i + 1);
1258
+ }
1259
+ }
1260
+ return offsets;
1261
+ }
1262
+ function lineAtOffset(offsets, charIndex) {
1263
+ let lo = 0;
1264
+ let hi = offsets.length - 1;
1265
+ while (lo < hi) {
1266
+ const mid = lo + hi + 1 >> 1;
1267
+ if (offsets[mid] <= charIndex) {
1268
+ lo = mid;
1269
+ } else {
1270
+ hi = mid - 1;
1271
+ }
1272
+ }
1273
+ return lo + 1;
1274
+ }
1275
+ function findOccurrences(content, searchString) {
1276
+ if (!searchString) {
1277
+ return [];
1278
+ }
1279
+ const offsets = buildLineOffsets(content);
1280
+ const results = [];
1281
+ let pos = 0;
1282
+ while (pos <= content.length - searchString.length) {
1283
+ const idx = content.indexOf(searchString, pos);
1284
+ if (idx === -1) {
1285
+ break;
1286
+ }
1287
+ results.push({ index: idx, line: lineAtOffset(offsets, idx) });
1288
+ pos = idx + 1;
1289
+ }
1290
+ return results;
1291
+ }
1292
+ function flexibleMatch(content, searchString) {
1293
+ const contentLines = content.split("\n");
1294
+ const searchLines = searchString.split("\n").map((l) => l.trimStart());
1295
+ if (searchLines.length === 0) {
1296
+ return null;
1297
+ }
1298
+ const matches = [];
1299
+ for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
1300
+ let allMatch = true;
1301
+ for (let j = 0; j < searchLines.length; j++) {
1302
+ if (contentLines[i + j].trimStart() !== searchLines[j]) {
1303
+ allMatch = false;
1304
+ break;
1305
+ }
1306
+ }
1307
+ if (allMatch) {
1308
+ matches.push(i);
1309
+ }
1310
+ }
1311
+ if (matches.length !== 1) {
1312
+ return null;
1313
+ }
1314
+ const startIdx = matches[0];
1315
+ const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
1316
+ const offsets = buildLineOffsets(content);
1317
+ return {
1318
+ matchedText,
1319
+ index: offsets[startIdx],
1320
+ line: startIdx + 1
1321
+ // 1-based
1322
+ };
1323
+ }
1324
+ function replaceAt(content, index, oldLength, newString) {
1325
+ return content.slice(0, index) + newString + content.slice(index + oldLength);
1326
+ }
1327
+ function formatOccurrenceError(count, lines, filePath) {
1328
+ return `old_string found ${count} times in ${filePath} (at lines ${lines.join(", ")}) \u2014 must be unique. Include more surrounding context to disambiguate, or use replace_all to replace every occurrence.`;
1329
+ }
1330
+
1331
+ // src/tools/code/editFile/index.ts
1332
+ var editFileTool = {
1333
+ definition: {
1334
+ name: "editFile",
1335
+ description: "Replace a string in a file. old_string must appear exactly once (minor indentation differences are handled automatically). Set replace_all to true to replace every occurrence at once. For bulk mechanical substitutions (renaming a variable, swapping colors), prefer replace_all. Always read the file first so you know the exact text to match.",
1336
+ inputSchema: {
1337
+ type: "object",
1338
+ properties: {
1339
+ path: {
1340
+ type: "string",
1341
+ description: "The file path to edit, relative to the project root."
1342
+ },
1343
+ old_string: {
1344
+ type: "string",
1345
+ description: "The exact string to find and replace. Must be unique in the file unless replace_all is true."
1346
+ },
1347
+ new_string: {
1348
+ type: "string",
1349
+ description: "The replacement string."
1350
+ },
1351
+ replace_all: {
1352
+ type: "boolean",
1353
+ description: "If true, replace every occurrence of old_string in the file. Defaults to false."
1354
+ }
1355
+ },
1356
+ required: ["path", "old_string", "new_string"]
1357
+ }
1358
+ },
1359
+ async execute(input) {
1360
+ try {
1361
+ const content = await fs11.readFile(input.path, "utf-8");
1362
+ const { old_string, new_string, replace_all } = input;
1363
+ const occurrences = findOccurrences(content, old_string);
1364
+ if (replace_all) {
1365
+ if (occurrences.length === 0) {
1366
+ return `Error: old_string not found in ${input.path}.`;
1367
+ }
1368
+ let updated = content;
1369
+ for (let i = occurrences.length - 1; i >= 0; i--) {
1370
+ updated = replaceAt(
1371
+ updated,
1372
+ occurrences[i].index,
1373
+ old_string.length,
1374
+ new_string
1375
+ );
1376
+ }
1377
+ await fs11.writeFile(input.path, updated, "utf-8");
1378
+ return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
1379
+ ${unifiedDiff(input.path, content, updated)}`;
1380
+ }
1381
+ if (occurrences.length === 1) {
1382
+ const updated = replaceAt(
1383
+ content,
1384
+ occurrences[0].index,
1385
+ old_string.length,
1386
+ new_string
1387
+ );
1388
+ await fs11.writeFile(input.path, updated, "utf-8");
1389
+ return `Updated ${input.path}
1390
+ ${unifiedDiff(input.path, content, updated)}`;
1391
+ }
1392
+ if (occurrences.length > 1) {
1393
+ const lines = occurrences.map((o) => o.line);
1394
+ return `Error: ${formatOccurrenceError(occurrences.length, lines, input.path)}`;
1395
+ }
1396
+ const flex = flexibleMatch(content, old_string);
1397
+ if (flex) {
1398
+ const updated = replaceAt(
1399
+ content,
1400
+ flex.index,
1401
+ flex.matchedText.length,
1402
+ new_string
1403
+ );
1404
+ await fs11.writeFile(input.path, updated, "utf-8");
1405
+ return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
1406
+ ${unifiedDiff(input.path, content, updated)}`;
1407
+ }
1408
+ return `Error: old_string not found in ${input.path}. Make sure you've read the file first and copied the exact text.`;
1409
+ } catch (err) {
1410
+ return `Error editing file: ${err.message}`;
1411
+ }
1412
+ }
1413
+ };
1414
+
1415
+ // src/tools/code/bash.ts
1416
+ import { exec } from "child_process";
1417
+ var DEFAULT_TIMEOUT_MS = 12e4;
1418
+ var DEFAULT_MAX_LINES3 = 500;
1419
+ var bashTool = {
1420
+ definition: {
1421
+ name: "bash",
1422
+ description: "Run a shell command and return stdout + stderr. 120-second timeout by default (configurable). Use for: npm install/build/test, git operations, tsc --noEmit, or any CLI tool. Prefer dedicated tools over bash when available (use grep instead of bash + rg, readFile instead of bash + cat). Output is truncated to 500 lines by default.",
1423
+ inputSchema: {
1424
+ type: "object",
1425
+ properties: {
1426
+ command: {
1427
+ type: "string",
1428
+ description: "The shell command to execute."
1429
+ },
1430
+ cwd: {
1431
+ type: "string",
1432
+ description: "Working directory to run the command in. Defaults to the project root."
1433
+ },
1434
+ timeout: {
1435
+ type: "number",
1436
+ description: "Timeout in seconds. Defaults to 120. Use higher values for long-running commands like builds or test suites."
1437
+ },
1438
+ maxLines: {
1439
+ type: "number",
1440
+ description: "Maximum number of output lines to return. Defaults to 500. Set to 0 for no limit."
1441
+ }
1442
+ },
1443
+ required: ["command"]
1444
+ }
1445
+ },
1446
+ async execute(input) {
1447
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1448
+ const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1449
+ return new Promise((resolve) => {
1450
+ exec(
1451
+ input.command,
1452
+ {
1453
+ timeout: timeoutMs,
1454
+ maxBuffer: 2 * 1024 * 1024,
1455
+ ...input.cwd ? { cwd: input.cwd } : {},
1456
+ env: { ...process.env, FORCE_COLOR: "1" }
1457
+ },
1458
+ (err, stdout, stderr) => {
1459
+ let result = "";
1460
+ if (stdout) {
1461
+ result += stdout;
1462
+ }
1463
+ if (stderr) {
1464
+ result += (result ? "\n" : "") + stderr;
1465
+ }
1466
+ if (err && !stdout && !stderr) {
1467
+ result = `Error: ${err.message}`;
1468
+ }
1469
+ if (!result) {
1470
+ resolve("(no output)");
1471
+ return;
1472
+ }
1473
+ const lines = result.split("\n");
1474
+ if (lines.length > maxLines) {
1475
+ resolve(
1476
+ lines.slice(0, maxLines).join("\n") + `
1477
+
1478
+ (truncated at ${maxLines} lines of ${lines.length} total \u2014 increase maxLines to see more)`
1479
+ );
1480
+ } else {
1481
+ resolve(result);
1482
+ }
1483
+ }
1484
+ );
1485
+ });
1486
+ }
1487
+ };
1488
+
1489
+ // src/tools/code/grep.ts
1490
+ import { exec as exec2 } from "child_process";
1491
+ var DEFAULT_MAX = 50;
1492
+ function formatResults(stdout, max) {
1493
+ const lines = stdout.trim().split("\n");
1494
+ let result = lines.join("\n");
1495
+ if (lines.length >= max) {
1496
+ result += `
1497
+
1498
+ (truncated at ${max} results \u2014 increase maxResults to see more)`;
1499
+ }
1500
+ return result;
1501
+ }
1502
+ var grepTool = {
1503
+ definition: {
1504
+ name: "grep",
1505
+ description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers (default 50 results). Use this to find where something is used, locate function definitions, or search for patterns across the codebase. For finding a symbol's definition precisely, prefer the definition tool if LSP is available. Automatically excludes node_modules and .git.",
1506
+ inputSchema: {
1507
+ type: "object",
1508
+ properties: {
1509
+ pattern: {
1510
+ type: "string",
1511
+ description: "The search pattern (regex supported)."
1512
+ },
1513
+ path: {
1514
+ type: "string",
1515
+ description: "Directory or file to search in. Defaults to current directory."
1516
+ },
1517
+ glob: {
1518
+ type: "string",
1519
+ description: 'File glob to filter (e.g., "*.ts"). Only used with ripgrep.'
1520
+ },
1521
+ maxResults: {
1522
+ type: "number",
1523
+ description: "Maximum number of matching lines to return. Defaults to 50. Increase if you need more comprehensive results."
1524
+ }
1525
+ },
1526
+ required: ["pattern"]
1527
+ }
1528
+ },
1529
+ async execute(input) {
1530
+ const searchPath = input.path || ".";
1531
+ const max = input.maxResults || DEFAULT_MAX;
1532
+ const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
1533
+ const escaped = input.pattern.replace(/'/g, "'\\''");
1534
+ const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1535
+ const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1536
+ return new Promise((resolve) => {
1537
+ exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1538
+ if (stdout?.trim()) {
1539
+ resolve(formatResults(stdout, max));
1540
+ return;
1541
+ }
1542
+ exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1543
+ if (grepStdout?.trim()) {
1544
+ resolve(formatResults(grepStdout, max));
1545
+ } else {
1546
+ resolve("No matches found.");
1547
+ }
1548
+ });
1549
+ });
1550
+ });
1551
+ }
1552
+ };
1553
+
1554
+ // src/tools/code/glob.ts
1555
+ import fg from "fast-glob";
1556
+ var DEFAULT_MAX2 = 200;
1557
+ var globTool = {
1558
+ definition: {
1559
+ name: "glob",
1560
+ description: 'Find files matching a glob pattern. Returns matching file paths sorted alphabetically (default 200 results). Use this to discover project structure, find files by name or extension, or check if a file exists. Common patterns: "**/*.ts" (all TypeScript files), "src/**/*.tsx" (React components in src), "*.json" (root-level JSON files). Automatically excludes node_modules and .git.',
1561
+ inputSchema: {
1562
+ type: "object",
1563
+ properties: {
1564
+ pattern: {
1565
+ type: "string",
1566
+ description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.tsx", "*.json").'
1567
+ },
1568
+ maxResults: {
1569
+ type: "number",
1570
+ description: "Maximum number of file paths to return. Defaults to 200. Increase if you need the complete list."
1571
+ }
1572
+ },
1573
+ required: ["pattern"]
1574
+ }
1575
+ },
1576
+ async execute(input) {
1577
+ try {
1578
+ const max = input.maxResults || DEFAULT_MAX2;
1579
+ const files = await fg(input.pattern, {
1580
+ ignore: ["**/node_modules/**", "**/.git/**"],
1581
+ dot: false
1582
+ });
1583
+ if (files.length === 0) {
1584
+ return "No files found.";
1585
+ }
1586
+ const sorted = files.sort();
1587
+ const truncated = sorted.slice(0, max);
1588
+ let result = truncated.join("\n");
1589
+ if (sorted.length > max) {
1590
+ result += `
1591
+
1592
+ (showing ${max} of ${sorted.length} matches \u2014 increase maxResults to see all)`;
1593
+ }
1594
+ return result;
1595
+ } catch (err) {
1596
+ return `Error: ${err.message}`;
1597
+ }
1598
+ }
1599
+ };
1600
+
1601
+ // src/tools/code/listDir.ts
1602
+ import fs12 from "fs/promises";
1603
+ var listDirTool = {
1604
+ definition: {
1605
+ name: "listDir",
1606
+ description: "List the contents of a directory. Shows entries with / suffix for directories, sorted directories-first then alphabetically. Use this for a quick overview of a directory's contents. For finding files across the whole project, use glob instead.",
1607
+ inputSchema: {
1608
+ type: "object",
1609
+ properties: {
1610
+ path: {
1611
+ type: "string",
1612
+ description: 'Directory path to list, relative to project root. Defaults to ".".'
1613
+ }
1614
+ }
1615
+ }
1616
+ },
1617
+ async execute(input) {
1618
+ const dirPath = input.path || ".";
1619
+ try {
1620
+ const entries = await fs12.readdir(dirPath, { withFileTypes: true });
1621
+ const lines = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
1622
+ if (a.isDirectory() && !b.isDirectory()) {
1623
+ return -1;
1624
+ }
1625
+ if (!a.isDirectory() && b.isDirectory()) {
1626
+ return 1;
1627
+ }
1628
+ return a.name.localeCompare(b.name);
1629
+ }).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
1630
+ return lines.join("\n") || "(empty directory)";
1631
+ } catch (err) {
1632
+ return `Error listing directory: ${err.message}`;
1633
+ }
1634
+ }
1635
+ };
1636
+
1637
+ // src/tools/code/editsFinished.ts
1638
+ var editsFinishedTool = {
1639
+ definition: {
1640
+ name: "editsFinished",
1641
+ description: "Signal that file edits are complete. Call this after you finish writing/editing files so the live preview updates cleanly. The preview is paused while you edit to avoid showing broken intermediate states \u2014 this unpauses it. If you forget to call this, the preview updates when your turn ends.",
1642
+ inputSchema: {
1643
+ type: "object",
1644
+ properties: {},
1645
+ required: []
1646
+ }
1647
+ },
1648
+ async execute() {
1649
+ return "Preview updated.";
1650
+ }
1651
+ };
1652
+
1653
+ // src/tools/code/lspDiagnostics.ts
1654
+ var lspDiagnosticsTool = {
1655
+ definition: {
1656
+ name: "lspDiagnostics",
1657
+ description: "Get TypeScript diagnostics (type errors, warnings) for a file, with suggested fixes when available. Use this after editing a file to check for errors.",
1658
+ inputSchema: {
1659
+ type: "object",
1660
+ properties: {
1661
+ file: {
1662
+ type: "string",
1663
+ description: "File path relative to workspace root."
1664
+ }
1665
+ },
1666
+ required: ["file"]
1667
+ }
1668
+ },
1669
+ async execute(input) {
1670
+ const data = await lspRequest("/diagnostics", { file: input.file });
1671
+ const diags = data.diagnostics || [];
1672
+ if (diags.length === 0) {
1673
+ return "No diagnostics \u2014 file is clean.";
1674
+ }
1675
+ const lines = [];
1676
+ for (const d of diags) {
1677
+ let line = `${d.severity}: ${d.file}:${d.line}:${d.column} \u2014 ${d.message}`;
1678
+ try {
1679
+ const actionsData = await lspRequest("/code-actions", {
1680
+ file: d.file,
1681
+ startLine: d.line,
1682
+ startColumn: d.column,
1683
+ endLine: d.endLine ?? d.line,
1684
+ endColumn: d.endColumn ?? d.column,
1685
+ diagnostics: [d]
1686
+ });
1687
+ const actions = actionsData.actions || [];
1688
+ if (actions.length > 0) {
1689
+ const fixes = actions.map((a) => a.title).join("; ");
1690
+ line += `
1691
+ Quick fixes: ${fixes}`;
1692
+ }
1693
+ } catch {
1694
+ }
1695
+ lines.push(line);
1696
+ }
1697
+ return lines.join("\n");
1698
+ }
1699
+ };
1700
+
1701
+ // src/tools/code/restartProcess.ts
1702
+ var restartProcessTool = {
1703
+ definition: {
1704
+ name: "restartProcess",
1705
+ description: "Restart a managed sandbox process. Use this after running npm install or changing package.json to restart the dev server so it picks up new dependencies.",
1706
+ inputSchema: {
1707
+ type: "object",
1708
+ properties: {
1709
+ name: {
1710
+ type: "string",
1711
+ description: 'Process name to restart. Currently supported: "devServer".'
1712
+ }
1713
+ },
1714
+ required: ["name"]
1715
+ }
1716
+ },
1717
+ async execute(input) {
1718
+ const data = await lspRequest("/restart-process", { name: input.name });
1719
+ if (data.ok) {
1720
+ return `Restarted ${input.name}.`;
1721
+ }
1722
+ return `Error: unexpected response: ${JSON.stringify(data)}`;
1723
+ }
1724
+ };
1725
+
1726
+ // src/tools/index.ts
1727
+ function getSpecTools() {
1728
+ return [readSpecTool, writeSpecTool, editSpecTool, listSpecFilesTool];
1729
+ }
1730
+ function getCodeTools() {
1731
+ const tools = [
1732
+ readFileTool,
1733
+ writeFileTool,
1734
+ editFileTool,
1735
+ bashTool,
1736
+ grepTool,
1737
+ globTool,
1738
+ listDirTool,
1739
+ editsFinishedTool
1740
+ ];
1741
+ if (isLspConfigured()) {
1742
+ tools.push(lspDiagnosticsTool, restartProcessTool);
1743
+ }
1744
+ return tools;
1745
+ }
1746
+ function getTools(projectHasCode) {
1747
+ if (projectHasCode) {
1748
+ return [
1749
+ setViewModeTool,
1750
+ promptUserTool,
1751
+ clearSyncStatusTool,
1752
+ presentSyncPlanTool,
1753
+ presentPublishPlanTool,
1754
+ presentPlanTool,
1755
+ ...getSpecTools(),
1756
+ ...getCodeTools()
1757
+ ];
1758
+ }
1759
+ return [
1760
+ setViewModeTool,
1761
+ promptUserTool,
1762
+ clearSyncStatusTool,
1763
+ presentSyncPlanTool,
1764
+ presentPublishPlanTool,
1765
+ ...getSpecTools()
1766
+ ];
1767
+ }
1768
+ function getToolDefinitions(projectHasCode) {
1769
+ return getTools(projectHasCode).map((t) => t.definition);
1770
+ }
1771
+ function getToolByName(name) {
1772
+ const allTools = [
1773
+ setViewModeTool,
1774
+ promptUserTool,
1775
+ clearSyncStatusTool,
1776
+ presentSyncPlanTool,
1777
+ presentPublishPlanTool,
1778
+ presentPlanTool,
1779
+ ...getSpecTools(),
1780
+ ...getCodeTools()
1781
+ ];
1782
+ return allTools.find((t) => t.definition.name === name);
1783
+ }
1784
+ function executeTool(name, input) {
1785
+ const tool = getToolByName(name);
1786
+ if (!tool) {
1787
+ return Promise.resolve(`Error: Unknown tool "${name}"`);
1788
+ }
1789
+ return tool.execute(input);
1790
+ }
1791
+
1792
+ // src/session.ts
1793
+ import fs13 from "fs";
1794
+ var SESSION_FILE = ".remy-session.json";
1795
+ function loadSession(state) {
1796
+ try {
1797
+ const raw = fs13.readFileSync(SESSION_FILE, "utf-8");
1798
+ const data = JSON.parse(raw);
1799
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
1800
+ state.messages = sanitizeMessages(data.messages);
1801
+ return true;
1802
+ }
1803
+ } catch {
1804
+ }
1805
+ return false;
1806
+ }
1807
+ function sanitizeMessages(messages) {
1808
+ const result = [];
1809
+ for (let i = 0; i < messages.length; i++) {
1810
+ result.push(messages[i]);
1811
+ const msg = messages[i];
1812
+ if (msg.role !== "assistant" || !msg.toolCalls?.length) {
1813
+ continue;
1814
+ }
1815
+ const resultIds = /* @__PURE__ */ new Set();
1816
+ for (let j = i + 1; j < messages.length; j++) {
1817
+ const next = messages[j];
1818
+ if (next.role === "user" && next.toolCallId) {
1819
+ resultIds.add(next.toolCallId);
1820
+ } else {
1821
+ break;
1822
+ }
1823
+ }
1824
+ for (const tc of msg.toolCalls) {
1825
+ if (!resultIds.has(tc.id)) {
1826
+ result.push({
1827
+ role: "user",
1828
+ content: "Error: tool result lost (session recovered)",
1829
+ toolCallId: tc.id,
1830
+ isToolError: true
1831
+ });
1832
+ }
1833
+ }
1834
+ }
1835
+ return result;
1836
+ }
1837
+ function saveSession(state) {
1838
+ try {
1839
+ fs13.writeFileSync(
1840
+ SESSION_FILE,
1841
+ JSON.stringify({ messages: state.messages }, null, 2),
1842
+ "utf-8"
1843
+ );
1844
+ } catch {
1845
+ }
1846
+ }
1847
+ function clearSession(state) {
1848
+ state.messages = [];
1849
+ try {
1850
+ fs13.unlinkSync(SESSION_FILE);
1851
+ } catch {
1852
+ }
1853
+ }
1854
+
1855
+ // src/parsePartialJson.ts
1856
+ var PartialJSON = class extends Error {
1857
+ };
1858
+ var MalformedJSON = class extends Error {
1859
+ };
1860
+ function parsePartialJson(jsonString) {
1861
+ const length = jsonString.length;
1862
+ let index = 0;
1863
+ const markPartial = (msg) => {
1864
+ throw new PartialJSON(`${msg} at position ${index}`);
1865
+ };
1866
+ const throwMalformed = (msg) => {
1867
+ throw new MalformedJSON(`${msg} at position ${index}`);
1868
+ };
1869
+ const skipBlank = () => {
1870
+ while (index < length && " \n\r ".includes(jsonString[index])) {
1871
+ index++;
1872
+ }
1873
+ };
1874
+ const parseAny = () => {
1875
+ skipBlank();
1876
+ if (index >= length) {
1877
+ markPartial("Unexpected end of input");
1878
+ }
1879
+ const ch = jsonString[index];
1880
+ if (ch === '"') {
1881
+ return parseStr();
1882
+ }
1883
+ if (ch === "{") {
1884
+ return parseObj();
1885
+ }
1886
+ if (ch === "[") {
1887
+ return parseArr();
1888
+ }
1889
+ if (jsonString.substring(index, index + 4) === "null" || length - index < 4 && "null".startsWith(jsonString.substring(index))) {
1890
+ index += 4;
1891
+ return null;
1892
+ }
1893
+ if (jsonString.substring(index, index + 4) === "true" || length - index < 4 && "true".startsWith(jsonString.substring(index))) {
1894
+ index += 4;
1895
+ return true;
1896
+ }
1897
+ if (jsonString.substring(index, index + 5) === "false" || length - index < 5 && "false".startsWith(jsonString.substring(index))) {
1898
+ index += 5;
1899
+ return false;
1900
+ }
1901
+ return parseNum();
1902
+ };
1903
+ const parseStr = () => {
1904
+ const start = index;
1905
+ let escape = false;
1906
+ index++;
1907
+ while (index < length && (jsonString[index] !== '"' || escape && jsonString[index - 1] === "\\")) {
1908
+ escape = jsonString[index] === "\\" ? !escape : false;
1909
+ index++;
1910
+ }
1911
+ if (jsonString.charAt(index) === '"') {
1912
+ try {
1913
+ return JSON.parse(
1914
+ jsonString.substring(start, ++index - Number(escape))
1915
+ );
1916
+ } catch (e) {
1917
+ return throwMalformed(String(e));
1918
+ }
1919
+ }
1920
+ try {
1921
+ return JSON.parse(
1922
+ jsonString.substring(start, index - Number(escape)) + '"'
1923
+ );
1924
+ } catch {
1925
+ return JSON.parse(
1926
+ jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"'
1927
+ );
1928
+ }
1929
+ };
1930
+ const parseObj = () => {
1931
+ index++;
1932
+ skipBlank();
1933
+ const obj = {};
1934
+ try {
1935
+ while (jsonString[index] !== "}") {
1936
+ skipBlank();
1937
+ if (index >= length) {
1938
+ return obj;
1939
+ }
1940
+ const key = parseStr();
1941
+ skipBlank();
1942
+ index++;
1943
+ try {
1944
+ obj[key] = parseAny();
1945
+ } catch {
1946
+ return obj;
1947
+ }
1948
+ skipBlank();
1949
+ if (jsonString[index] === ",") {
1950
+ index++;
1951
+ }
1952
+ }
1953
+ } catch {
1954
+ return obj;
1955
+ }
1956
+ index++;
1957
+ return obj;
1958
+ };
1959
+ const parseArr = () => {
1960
+ index++;
1961
+ const arr = [];
1962
+ try {
1963
+ while (jsonString[index] !== "]") {
1964
+ arr.push(parseAny());
1965
+ skipBlank();
1966
+ if (jsonString[index] === ",") {
1967
+ index++;
1968
+ }
1969
+ }
1970
+ } catch {
1971
+ return arr;
1972
+ }
1973
+ index++;
1974
+ return arr;
1975
+ };
1976
+ const parseNum = () => {
1977
+ if (index === 0) {
1978
+ if (jsonString === "-") {
1979
+ throwMalformed("Not sure what '-' is");
1980
+ }
1981
+ try {
1982
+ return JSON.parse(jsonString);
1983
+ } catch (e) {
1984
+ try {
1985
+ return JSON.parse(
1986
+ jsonString.substring(0, jsonString.lastIndexOf("e"))
1987
+ );
1988
+ } catch {
1989
+ }
1990
+ throwMalformed(String(e));
1991
+ }
1992
+ }
1993
+ const start = index;
1994
+ if (jsonString[index] === "-") {
1995
+ index++;
1996
+ }
1997
+ while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1) {
1998
+ index++;
1999
+ }
2000
+ try {
2001
+ return JSON.parse(jsonString.substring(start, index));
2002
+ } catch (e) {
2003
+ if (jsonString.substring(start, index) === "-") {
2004
+ markPartial("Not sure what '-' is");
2005
+ }
2006
+ try {
2007
+ return JSON.parse(
2008
+ jsonString.substring(start, jsonString.lastIndexOf("e"))
2009
+ );
2010
+ } catch {
2011
+ throwMalformed(String(e));
2012
+ }
2013
+ }
2014
+ };
2015
+ return parseAny();
2016
+ }
2017
+
2018
+ // src/agent.ts
2019
+ var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
2020
+ "promptUser",
2021
+ "setViewMode",
2022
+ "clearSyncStatus",
2023
+ "presentSyncPlan",
2024
+ "presentPublishPlan",
2025
+ "presentPlan"
2026
+ ]);
2027
+ function createAgentState() {
2028
+ return { messages: [] };
2029
+ }
2030
+ async function runTurn(params) {
2031
+ const {
2032
+ state,
2033
+ userMessage,
2034
+ attachments,
2035
+ apiConfig,
2036
+ system,
2037
+ model,
2038
+ projectHasCode,
2039
+ signal,
2040
+ onEvent,
2041
+ resolveExternalTool,
2042
+ hidden
2043
+ } = params;
2044
+ const tools = getToolDefinitions(projectHasCode);
2045
+ log.info("Turn started", {
2046
+ messageLength: userMessage.length,
2047
+ toolCount: tools.length,
2048
+ tools: tools.map((t) => t.name),
2049
+ ...attachments && attachments.length > 0 && {
2050
+ attachmentCount: attachments.length,
2051
+ attachmentUrls: attachments.map((a) => a.url)
2052
+ }
2053
+ });
2054
+ onEvent({ type: "turn_started" });
2055
+ const userMsg = { role: "user", content: userMessage };
2056
+ if (hidden) {
2057
+ userMsg.hidden = true;
2058
+ }
2059
+ if (attachments && attachments.length > 0) {
2060
+ userMsg.attachments = attachments;
2061
+ log.debug("Attachments added to user message", {
2062
+ count: attachments.length,
2063
+ urls: attachments.map((a) => a.url)
2064
+ });
2065
+ }
2066
+ state.messages.push(userMsg);
2067
+ while (true) {
2068
+ let getOrCreateAccumulator2 = function(id, name) {
2069
+ let acc = toolInputAccumulators.get(id);
2070
+ if (!acc) {
2071
+ acc = { name, json: "", started: false, lastEmittedCount: 0 };
2072
+ toolInputAccumulators.set(id, acc);
2073
+ }
2074
+ return acc;
2075
+ };
2076
+ var getOrCreateAccumulator = getOrCreateAccumulator2;
2077
+ if (signal?.aborted) {
2078
+ onEvent({ type: "turn_cancelled" });
2079
+ saveSession(state);
2080
+ return;
2081
+ }
2082
+ let assistantText = "";
2083
+ const toolCalls = [];
2084
+ const toolInputAccumulators = /* @__PURE__ */ new Map();
2085
+ let stopReason = "end_turn";
2086
+ async function handlePartialInput(acc, id, name, partial) {
2087
+ const tool = getToolByName(name);
2088
+ if (!tool?.streaming) {
2089
+ return;
2090
+ }
2091
+ const {
2092
+ contentField = "content",
2093
+ transform,
2094
+ partialInput
2095
+ } = tool.streaming;
2096
+ if (partialInput) {
2097
+ const result = partialInput(partial, acc.lastEmittedCount);
2098
+ if (!result) {
2099
+ return;
2100
+ }
2101
+ acc.lastEmittedCount = result.emittedCount;
2102
+ acc.started = true;
2103
+ log.debug("Streaming partial tool_start", {
2104
+ id,
2105
+ name,
2106
+ emittedCount: result.emittedCount
2107
+ });
2108
+ onEvent({
2109
+ type: "tool_start",
2110
+ id,
2111
+ name,
2112
+ input: result.input,
2113
+ partial: true
2114
+ });
2115
+ return;
2116
+ }
2117
+ const content = partial[contentField];
2118
+ if (typeof content !== "string") {
2119
+ return;
2120
+ }
2121
+ if (!acc.started) {
2122
+ acc.started = true;
2123
+ log.debug("Streaming content tool: emitting early tool_start", {
2124
+ id,
2125
+ name
2126
+ });
2127
+ onEvent({ type: "tool_start", id, name, input: partial });
2128
+ }
2129
+ if (transform) {
2130
+ const result = await transform(partial);
2131
+ log.debug("Streaming content tool: emitting tool_input_delta", {
2132
+ id,
2133
+ name,
2134
+ resultLength: result.length
2135
+ });
2136
+ onEvent({ type: "tool_input_delta", id, name, result });
2137
+ } else {
2138
+ log.debug("Streaming content tool: emitting tool_input_delta", {
2139
+ id,
2140
+ name,
2141
+ contentLength: content.length
2142
+ });
2143
+ onEvent({ type: "tool_input_delta", id, name, result: content });
2144
+ }
2145
+ }
2146
+ try {
2147
+ for await (const event of streamChat({
2148
+ ...apiConfig,
2149
+ model,
2150
+ system,
2151
+ messages: state.messages,
2152
+ tools,
2153
+ signal
2154
+ })) {
2155
+ if (signal?.aborted) {
2156
+ break;
2157
+ }
2158
+ switch (event.type) {
2159
+ case "text":
2160
+ assistantText += event.text;
2161
+ onEvent({ type: "text", text: event.text });
2162
+ break;
2163
+ case "thinking":
2164
+ onEvent({ type: "thinking", text: event.text });
2165
+ break;
2166
+ case "tool_input_delta": {
2167
+ const acc = getOrCreateAccumulator2(event.id, event.name);
2168
+ acc.json += event.delta;
2169
+ log.debug("Received tool_input_delta", {
2170
+ id: event.id,
2171
+ name: event.name,
2172
+ deltaLength: event.delta.length,
2173
+ accumulatedLength: acc.json.length
2174
+ });
2175
+ try {
2176
+ const partial = parsePartialJson(acc.json);
2177
+ await handlePartialInput(acc, event.id, event.name, partial);
2178
+ } catch {
2179
+ }
2180
+ break;
2181
+ }
2182
+ case "tool_input_args": {
2183
+ const acc = getOrCreateAccumulator2(event.id, event.name);
2184
+ log.debug("Received tool_input_args", {
2185
+ id: event.id,
2186
+ name: event.name,
2187
+ keys: Object.keys(event.args)
2188
+ });
2189
+ await handlePartialInput(acc, event.id, event.name, event.args);
2190
+ break;
2191
+ }
2192
+ case "tool_use": {
2193
+ toolCalls.push({
2194
+ id: event.id,
2195
+ name: event.name,
2196
+ input: event.input
2197
+ });
2198
+ const acc = toolInputAccumulators.get(event.id);
2199
+ const tool = getToolByName(event.name);
2200
+ const wasStreamed = acc?.started ?? false;
2201
+ const isInputStreaming = !!tool?.streaming?.partialInput;
2202
+ log.debug("Received tool_use", {
2203
+ id: event.id,
2204
+ name: event.name,
2205
+ wasStreamed,
2206
+ isInputStreaming
2207
+ });
2208
+ if (!wasStreamed || isInputStreaming) {
2209
+ onEvent({
2210
+ type: "tool_start",
2211
+ id: event.id,
2212
+ name: event.name,
2213
+ input: event.input
2214
+ });
2215
+ }
2216
+ break;
2217
+ }
2218
+ case "done":
2219
+ stopReason = event.stopReason;
2220
+ break;
2221
+ case "error":
2222
+ onEvent({ type: "error", error: event.error });
2223
+ return;
2224
+ }
2225
+ }
2226
+ } catch (err) {
2227
+ if (signal?.aborted) {
2228
+ } else {
2229
+ throw err;
2230
+ }
2231
+ }
2232
+ if (signal?.aborted) {
2233
+ if (assistantText) {
2234
+ state.messages.push({
2235
+ role: "assistant",
2236
+ content: assistantText + "\n\n(cancelled)",
2237
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0
2238
+ });
2239
+ }
2240
+ onEvent({ type: "turn_cancelled" });
2241
+ saveSession(state);
2242
+ return;
2243
+ }
2244
+ state.messages.push({
2245
+ role: "assistant",
2246
+ content: assistantText,
2247
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0
2248
+ });
2249
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
2250
+ saveSession(state);
2251
+ onEvent({ type: "turn_done" });
2252
+ return;
2253
+ }
2254
+ log.info("Executing tools", {
2255
+ count: toolCalls.length,
2256
+ tools: toolCalls.map((tc) => tc.name)
2257
+ });
2258
+ const results = await Promise.all(
2259
+ toolCalls.map(async (tc) => {
2260
+ if (signal?.aborted) {
2261
+ return {
2262
+ id: tc.id,
2263
+ result: "Error: cancelled",
2264
+ isError: true
2265
+ };
2266
+ }
2267
+ const toolStart = Date.now();
2268
+ try {
2269
+ let result;
2270
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
2271
+ saveSession(state);
2272
+ log.debug("Waiting for external tool result", {
2273
+ name: tc.name,
2274
+ id: tc.id
2275
+ });
2276
+ result = await resolveExternalTool(tc.id, tc.name, tc.input);
2277
+ } else {
2278
+ result = await executeTool(tc.name, tc.input);
2279
+ }
2280
+ const isError = result.startsWith("Error");
2281
+ log.debug("Tool completed", {
2282
+ name: tc.name,
2283
+ elapsed: `${Date.now() - toolStart}ms`,
2284
+ isError,
2285
+ resultLength: result.length
2286
+ });
2287
+ onEvent({
2288
+ type: "tool_done",
2289
+ id: tc.id,
2290
+ name: tc.name,
2291
+ result,
2292
+ isError
2293
+ });
2294
+ return { id: tc.id, result, isError };
2295
+ } catch (err) {
2296
+ const errorMsg = `Error: ${err.message}`;
2297
+ onEvent({
2298
+ type: "tool_done",
2299
+ id: tc.id,
2300
+ name: tc.name,
2301
+ result: errorMsg,
2302
+ isError: true
2303
+ });
2304
+ return { id: tc.id, result: errorMsg, isError: true };
2305
+ }
2306
+ })
2307
+ );
2308
+ for (const r of results) {
2309
+ state.messages.push({
2310
+ role: "user",
2311
+ content: r.result,
2312
+ toolCallId: r.id,
2313
+ isToolError: r.isError
2314
+ });
2315
+ }
2316
+ if (signal?.aborted) {
2317
+ onEvent({ type: "turn_cancelled" });
2318
+ saveSession(state);
2319
+ return;
2320
+ }
2321
+ }
2322
+ }
2323
+
2324
+ // src/headless.ts
2325
+ var BASE_DIR = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
2326
+ var ACTIONS_DIR = path7.join(BASE_DIR, "actions");
2327
+ function loadActionPrompt(name) {
2328
+ return fs14.readFileSync(path7.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
2329
+ }
2330
+ function emit(event, data) {
2331
+ process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
2332
+ }
2333
+ async function startHeadless(opts = {}) {
2334
+ const stderrWrite = (...args) => {
2335
+ process.stderr.write(args.map(String).join(" ") + "\n");
2336
+ };
2337
+ console.log = stderrWrite;
2338
+ console.warn = stderrWrite;
2339
+ console.info = stderrWrite;
2340
+ if (opts.lspUrl) {
2341
+ setLspBaseUrl(opts.lspUrl);
2342
+ }
2343
+ const config = resolveConfig({
2344
+ apiKey: opts.apiKey,
2345
+ baseUrl: opts.baseUrl
2346
+ });
2347
+ const state = createAgentState();
2348
+ const resumed = loadSession(state);
2349
+ if (resumed) {
2350
+ emit("session_restored", {
2351
+ messageCount: state.messages.length
2352
+ });
2353
+ }
2354
+ let running = false;
2355
+ let currentAbort = null;
2356
+ const externalToolPromises = /* @__PURE__ */ new Map();
2357
+ function onEvent(e) {
2358
+ switch (e.type) {
2359
+ case "text":
2360
+ emit("text", { text: e.text });
2361
+ break;
2362
+ case "thinking":
2363
+ emit("thinking", { text: e.text });
2364
+ break;
2365
+ case "tool_input_delta":
2366
+ emit("tool_input_delta", { id: e.id, name: e.name, result: e.result });
2367
+ break;
2368
+ case "tool_start": {
2369
+ emit("tool_start", {
2370
+ id: e.id,
2371
+ name: e.name,
2372
+ input: e.input,
2373
+ ...e.partial && { partial: true }
2374
+ });
2375
+ if (!e.partial && !externalToolPromises.has(e.id)) {
2376
+ let resolve;
2377
+ const promise = new Promise((r) => {
2378
+ resolve = r;
2379
+ });
2380
+ externalToolPromises.set(e.id, { promise, resolve });
2381
+ }
2382
+ break;
2383
+ }
2384
+ case "tool_done":
2385
+ emit("tool_done", {
2386
+ id: e.id,
2387
+ name: e.name,
2388
+ result: e.result,
2389
+ isError: e.isError
2390
+ });
2391
+ break;
2392
+ case "turn_started":
2393
+ emit("turn_started");
2394
+ break;
2395
+ case "turn_done":
2396
+ emit("turn_done");
2397
+ break;
2398
+ case "turn_cancelled":
2399
+ emit("turn_cancelled");
2400
+ break;
2401
+ case "error":
2402
+ emit("error", { error: e.error });
2403
+ break;
2404
+ }
2405
+ }
2406
+ function resolveExternalTool(id, _name, _input) {
2407
+ const entry = externalToolPromises.get(id);
2408
+ if (entry) {
2409
+ return entry.promise;
2410
+ }
2411
+ let resolve;
2412
+ const promise = new Promise((r) => {
2413
+ resolve = r;
2414
+ });
2415
+ externalToolPromises.set(id, { promise, resolve });
2416
+ return promise;
2417
+ }
2418
+ const rl = createInterface({ input: process.stdin });
2419
+ rl.on("line", async (line) => {
2420
+ let parsed;
2421
+ try {
2422
+ parsed = JSON.parse(line);
2423
+ } catch {
2424
+ emit("error", { error: "Invalid JSON on stdin" });
2425
+ return;
2426
+ }
2427
+ if (parsed.action === "tool_result" && parsed.id) {
2428
+ const entry = externalToolPromises.get(parsed.id);
2429
+ if (entry) {
2430
+ externalToolPromises.delete(parsed.id);
2431
+ entry.resolve(parsed.result ?? "");
2432
+ }
2433
+ return;
2434
+ }
2435
+ if (parsed.action === "get_history") {
2436
+ emit("history", {
2437
+ messages: state.messages
2438
+ });
2439
+ return;
2440
+ }
2441
+ if (parsed.action === "clear") {
2442
+ clearSession(state);
2443
+ emit("session_cleared");
2444
+ return;
2445
+ }
2446
+ if (parsed.action === "cancel") {
2447
+ if (currentAbort) {
2448
+ currentAbort.abort();
2449
+ }
2450
+ for (const [id, entry] of externalToolPromises) {
2451
+ entry.resolve("Error: cancelled");
2452
+ externalToolPromises.delete(id);
2453
+ }
2454
+ return;
2455
+ }
2456
+ if (parsed.action === "message" && (parsed.text || parsed.runCommand)) {
2457
+ if (running) {
2458
+ emit("error", { error: "Agent is already processing a message" });
2459
+ return;
2460
+ }
2461
+ running = true;
2462
+ currentAbort = new AbortController();
2463
+ if (parsed.attachments?.length) {
2464
+ console.warn(
2465
+ `[headless] Message has ${parsed.attachments.length} attachment(s):`,
2466
+ parsed.attachments.map((a) => a.url)
2467
+ );
2468
+ }
2469
+ let userMessage = parsed.text ?? "";
2470
+ const isCommand = !!parsed.runCommand;
2471
+ if (parsed.runCommand === "sync") {
2472
+ userMessage = loadActionPrompt("sync");
2473
+ } else if (parsed.runCommand === "publish") {
2474
+ userMessage = loadActionPrompt("publish");
2475
+ }
2476
+ const projectHasCode = parsed.projectHasCode ?? true;
2477
+ const system = buildSystemPrompt(projectHasCode, parsed.viewContext);
2478
+ try {
2479
+ await runTurn({
2480
+ state,
2481
+ userMessage,
2482
+ attachments: parsed.attachments,
2483
+ apiConfig: config,
2484
+ system,
2485
+ model: opts.model,
2486
+ projectHasCode,
2487
+ signal: currentAbort.signal,
2488
+ onEvent,
2489
+ resolveExternalTool,
2490
+ hidden: isCommand
2491
+ });
2492
+ } catch (err) {
2493
+ emit("error", { error: err.message });
2494
+ }
2495
+ currentAbort = null;
2496
+ running = false;
2497
+ }
2498
+ });
2499
+ rl.on("close", () => {
2500
+ emit("stopping");
2501
+ emit("stopped");
2502
+ process.exit(0);
2503
+ });
2504
+ function shutdown() {
2505
+ emit("stopping");
2506
+ emit("stopped");
2507
+ process.exit(0);
2508
+ }
2509
+ process.on("SIGTERM", shutdown);
2510
+ process.on("SIGINT", shutdown);
2511
+ emit("ready");
2512
+ }
2513
+ export {
2514
+ startHeadless
2515
+ };