@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.
package/dist/index.js ADDED
@@ -0,0 +1,3164 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/logger.ts
13
+ import fs from "fs";
14
+ function timestamp() {
15
+ return (/* @__PURE__ */ new Date()).toISOString();
16
+ }
17
+ function truncateValues(obj) {
18
+ const result = {};
19
+ for (const [key, value] of Object.entries(obj)) {
20
+ if (typeof value === "string" && value.length > MAX_VALUE_LENGTH) {
21
+ result[key] = value.slice(0, MAX_VALUE_LENGTH) + `... (${value.length} chars)`;
22
+ } else if (Array.isArray(value) && value.length > 5) {
23
+ result[key] = `[${value.length} items]`;
24
+ } else {
25
+ result[key] = value;
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ function write(level, msg, data) {
31
+ if (LEVELS[level] > currentLevel) {
32
+ return;
33
+ }
34
+ const parts = [`[${timestamp()}]`, level.toUpperCase().padEnd(5), msg];
35
+ if (data) {
36
+ parts.push(JSON.stringify(truncateValues(data)));
37
+ }
38
+ writeFn(parts.join(" "));
39
+ }
40
+ function initLoggerHeadless(level = "info") {
41
+ currentLevel = LEVELS[level];
42
+ writeFn = (line) => {
43
+ process.stderr.write(line + "\n");
44
+ };
45
+ }
46
+ function initLoggerInteractive(level = "error") {
47
+ currentLevel = LEVELS[level];
48
+ let fd = null;
49
+ writeFn = (line) => {
50
+ try {
51
+ if (fd === null) {
52
+ fd = fs.openSync(".remy-debug.log", "a");
53
+ }
54
+ fs.writeSync(fd, line + "\n");
55
+ } catch {
56
+ }
57
+ };
58
+ }
59
+ var LEVELS, currentLevel, writeFn, MAX_VALUE_LENGTH, log;
60
+ var init_logger = __esm({
61
+ "src/logger.ts"() {
62
+ "use strict";
63
+ LEVELS = {
64
+ error: 0,
65
+ warn: 1,
66
+ info: 2,
67
+ debug: 3
68
+ };
69
+ currentLevel = LEVELS.error;
70
+ writeFn = () => {
71
+ };
72
+ MAX_VALUE_LENGTH = 200;
73
+ log = {
74
+ error(msg, data) {
75
+ write("error", msg, data);
76
+ },
77
+ warn(msg, data) {
78
+ write("warn", msg, data);
79
+ },
80
+ info(msg, data) {
81
+ write("info", msg, data);
82
+ },
83
+ debug(msg, data) {
84
+ write("debug", msg, data);
85
+ }
86
+ };
87
+ }
88
+ });
89
+
90
+ // src/api.ts
91
+ async function* streamChat(params) {
92
+ const { baseUrl, apiKey, signal, ...body } = params;
93
+ const url = `${baseUrl}/_internal/v2/agent/chat`;
94
+ const startTime = Date.now();
95
+ const messagesWithAttachments = body.messages.filter(
96
+ (m) => m.attachments && m.attachments.length > 0
97
+ );
98
+ log.info("POST agent/chat", {
99
+ url,
100
+ model: body.model,
101
+ messageCount: body.messages.length,
102
+ toolCount: body.tools.length,
103
+ ...messagesWithAttachments.length > 0 && {
104
+ attachments: messagesWithAttachments.map((m) => ({
105
+ role: m.role,
106
+ attachmentCount: m.attachments.length,
107
+ urls: m.attachments.map((a) => a.url)
108
+ }))
109
+ }
110
+ });
111
+ let res;
112
+ try {
113
+ res = await fetch(url, {
114
+ method: "POST",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ Authorization: `Bearer ${apiKey}`
118
+ },
119
+ body: JSON.stringify(body),
120
+ signal
121
+ });
122
+ } catch (err) {
123
+ if (signal?.aborted) {
124
+ log.info("Request aborted by signal");
125
+ throw err;
126
+ }
127
+ log.error("Network error", { error: err.message });
128
+ yield { type: "error", error: `Network error: ${err.message}` };
129
+ return;
130
+ }
131
+ const ttfb = Date.now() - startTime;
132
+ log.info(`Response ${res.status}`, { ttfb: `${ttfb}ms` });
133
+ if (!res.ok) {
134
+ let errorMessage = `HTTP ${res.status}`;
135
+ try {
136
+ const body2 = await res.json();
137
+ if (body2.error) {
138
+ errorMessage = body2.error;
139
+ }
140
+ if (body2.errorMessage) {
141
+ errorMessage = body2.errorMessage;
142
+ }
143
+ } catch {
144
+ }
145
+ log.error("API error", { status: res.status, error: errorMessage });
146
+ yield { type: "error", error: errorMessage };
147
+ return;
148
+ }
149
+ const reader = res.body.getReader();
150
+ const decoder = new TextDecoder();
151
+ let buffer = "";
152
+ while (true) {
153
+ const { done, value } = await reader.read();
154
+ if (done) {
155
+ break;
156
+ }
157
+ buffer += decoder.decode(value, { stream: true });
158
+ const lines = buffer.split("\n");
159
+ buffer = lines.pop() ?? "";
160
+ for (const line of lines) {
161
+ if (!line.startsWith("data: ")) {
162
+ continue;
163
+ }
164
+ try {
165
+ const event = JSON.parse(line.slice(6));
166
+ if (event.type === "done") {
167
+ const elapsed = Date.now() - startTime;
168
+ log.info("Stream complete", {
169
+ elapsed: `${elapsed}ms`,
170
+ stopReason: event.stopReason,
171
+ inputTokens: event.usage.inputTokens,
172
+ outputTokens: event.usage.outputTokens
173
+ });
174
+ }
175
+ yield event;
176
+ } catch {
177
+ }
178
+ }
179
+ }
180
+ if (buffer.startsWith("data: ")) {
181
+ try {
182
+ yield JSON.parse(buffer.slice(6));
183
+ } catch {
184
+ }
185
+ }
186
+ }
187
+ var init_api = __esm({
188
+ "src/api.ts"() {
189
+ "use strict";
190
+ init_logger();
191
+ }
192
+ });
193
+
194
+ // src/tools/spec/_helpers.ts
195
+ function parseHeadings(content) {
196
+ const lines = content.split("\n");
197
+ const headings = [];
198
+ for (let i = 0; i < lines.length; i++) {
199
+ const match = lines[i].match(HEADING_RE);
200
+ if (match) {
201
+ headings.push({
202
+ level: match[1].length,
203
+ text: match[2].trim(),
204
+ startLine: i,
205
+ contentStart: i + 1,
206
+ contentEnd: lines.length
207
+ // placeholder — resolved below
208
+ });
209
+ }
210
+ }
211
+ for (let i = 0; i < headings.length; i++) {
212
+ const current = headings[i];
213
+ let end = lines.length;
214
+ for (let j = i + 1; j < headings.length; j++) {
215
+ if (headings[j].level <= current.level) {
216
+ end = headings[j].startLine;
217
+ break;
218
+ }
219
+ }
220
+ current.contentEnd = end;
221
+ }
222
+ return headings;
223
+ }
224
+ function resolveHeadingPath(content, headingPath) {
225
+ const lines = content.split("\n");
226
+ const headings = parseHeadings(content);
227
+ if (headingPath === "") {
228
+ const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
229
+ return {
230
+ startLine: 0,
231
+ contentStart: 0,
232
+ contentEnd: firstHeadingLine
233
+ };
234
+ }
235
+ const segments = headingPath.split(">").map((s) => s.trim());
236
+ let searchStart = 0;
237
+ let searchEnd = lines.length;
238
+ let resolved = null;
239
+ for (let si = 0; si < segments.length; si++) {
240
+ const segment = segments[si].toLowerCase();
241
+ const candidates = headings.filter(
242
+ (h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
243
+ );
244
+ if (candidates.length === 0) {
245
+ const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
246
+ const searchedPath = segments.slice(0, si + 1).join(" > ");
247
+ throw new Error(
248
+ `Heading not found: "${searchedPath}"
249
+
250
+ Available headings:
251
+ ${available || "(none)"}`
252
+ );
253
+ }
254
+ resolved = candidates[0];
255
+ searchStart = resolved.contentStart;
256
+ searchEnd = resolved.contentEnd;
257
+ }
258
+ return {
259
+ startLine: resolved.startLine,
260
+ contentStart: resolved.contentStart,
261
+ contentEnd: resolved.contentEnd
262
+ };
263
+ }
264
+ function validateSpecPath(filePath) {
265
+ if (!filePath.startsWith("src/")) {
266
+ throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
267
+ }
268
+ return filePath;
269
+ }
270
+ function getHeadingTree(content) {
271
+ const headings = parseHeadings(content);
272
+ if (headings.length === 0) {
273
+ return "(no headings)";
274
+ }
275
+ return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
276
+ }
277
+ var HEADING_RE;
278
+ var init_helpers = __esm({
279
+ "src/tools/spec/_helpers.ts"() {
280
+ "use strict";
281
+ HEADING_RE = /^(#{1,6})\s+(.+)$/;
282
+ }
283
+ });
284
+
285
+ // src/tools/spec/readSpec.ts
286
+ import fs2 from "fs/promises";
287
+ var DEFAULT_MAX_LINES, readSpecTool;
288
+ var init_readSpec = __esm({
289
+ "src/tools/spec/readSpec.ts"() {
290
+ "use strict";
291
+ init_helpers();
292
+ DEFAULT_MAX_LINES = 500;
293
+ readSpecTool = {
294
+ definition: {
295
+ name: "readSpec",
296
+ 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).",
297
+ inputSchema: {
298
+ type: "object",
299
+ properties: {
300
+ path: {
301
+ type: "string",
302
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
303
+ },
304
+ offset: {
305
+ type: "number",
306
+ description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
307
+ },
308
+ maxLines: {
309
+ type: "number",
310
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
311
+ }
312
+ },
313
+ required: ["path"]
314
+ }
315
+ },
316
+ async execute(input) {
317
+ try {
318
+ validateSpecPath(input.path);
319
+ } catch (err) {
320
+ return `Error: ${err.message}`;
321
+ }
322
+ try {
323
+ const content = await fs2.readFile(input.path, "utf-8");
324
+ const allLines = content.split("\n");
325
+ const totalLines = allLines.length;
326
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
327
+ let startIdx;
328
+ if (input.offset && input.offset < 0) {
329
+ startIdx = Math.max(0, totalLines + input.offset);
330
+ } else {
331
+ startIdx = Math.max(0, (input.offset || 1) - 1);
332
+ }
333
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
334
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
335
+ let result = numbered;
336
+ const endLine = startIdx + sliced.length;
337
+ const displayStart = startIdx + 1;
338
+ if (endLine < totalLines) {
339
+ result += `
340
+
341
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
342
+ }
343
+ return result;
344
+ } catch (err) {
345
+ return `Error reading file: ${err.message}`;
346
+ }
347
+ }
348
+ };
349
+ }
350
+ });
351
+
352
+ // src/tools/_helpers/diff.ts
353
+ function unifiedDiff(filePath, oldText, newText) {
354
+ const oldLines = oldText.split("\n");
355
+ const newLines = newText.split("\n");
356
+ let firstDiff = 0;
357
+ while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
358
+ firstDiff++;
359
+ }
360
+ let oldEnd = oldLines.length - 1;
361
+ let newEnd = newLines.length - 1;
362
+ while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
363
+ oldEnd--;
364
+ newEnd--;
365
+ }
366
+ const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
367
+ const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
368
+ const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
369
+ const lines = [];
370
+ lines.push(`--- ${filePath}`);
371
+ lines.push(`+++ ${filePath}`);
372
+ lines.push(
373
+ `@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
374
+ );
375
+ for (let i = ctxStart; i < firstDiff; i++) {
376
+ lines.push(` ${oldLines[i]}`);
377
+ }
378
+ for (let i = firstDiff; i <= oldEnd; i++) {
379
+ lines.push(`-${oldLines[i]}`);
380
+ }
381
+ for (let i = firstDiff; i <= newEnd; i++) {
382
+ lines.push(`+${newLines[i]}`);
383
+ }
384
+ for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
385
+ lines.push(` ${oldLines[i]}`);
386
+ }
387
+ return lines.join("\n");
388
+ }
389
+ var CONTEXT_LINES;
390
+ var init_diff = __esm({
391
+ "src/tools/_helpers/diff.ts"() {
392
+ "use strict";
393
+ CONTEXT_LINES = 3;
394
+ }
395
+ });
396
+
397
+ // src/tools/spec/writeSpec.ts
398
+ import fs3 from "fs/promises";
399
+ import path from "path";
400
+ var writeSpecTool;
401
+ var init_writeSpec = __esm({
402
+ "src/tools/spec/writeSpec.ts"() {
403
+ "use strict";
404
+ init_helpers();
405
+ init_diff();
406
+ writeSpecTool = {
407
+ definition: {
408
+ name: "writeSpec",
409
+ 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.",
410
+ inputSchema: {
411
+ type: "object",
412
+ properties: {
413
+ path: {
414
+ type: "string",
415
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
416
+ },
417
+ content: {
418
+ type: "string",
419
+ description: "The full MSFM markdown content to write."
420
+ }
421
+ },
422
+ required: ["path", "content"]
423
+ }
424
+ },
425
+ streaming: {
426
+ transform: async (partial) => {
427
+ const oldContent = await fs3.readFile(partial.path, "utf-8").catch(() => "");
428
+ const lineCount = partial.content.split("\n").length;
429
+ return `Writing ${partial.path} (${lineCount} lines)
430
+ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
431
+ }
432
+ },
433
+ async execute(input) {
434
+ try {
435
+ validateSpecPath(input.path);
436
+ } catch (err) {
437
+ return `Error: ${err.message}`;
438
+ }
439
+ try {
440
+ await fs3.mkdir(path.dirname(input.path), { recursive: true });
441
+ let oldContent = null;
442
+ try {
443
+ oldContent = await fs3.readFile(input.path, "utf-8");
444
+ } catch {
445
+ }
446
+ await fs3.writeFile(input.path, input.content, "utf-8");
447
+ const lineCount = input.content.split("\n").length;
448
+ const label = oldContent !== null ? "Wrote" : "Created";
449
+ return `${label} ${input.path} (${lineCount} lines)
450
+ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
451
+ } catch (err) {
452
+ return `Error writing file: ${err.message}`;
453
+ }
454
+ }
455
+ };
456
+ }
457
+ });
458
+
459
+ // src/tools/spec/editSpec.ts
460
+ import fs4 from "fs/promises";
461
+ var editSpecTool;
462
+ var init_editSpec = __esm({
463
+ "src/tools/spec/editSpec.ts"() {
464
+ "use strict";
465
+ init_helpers();
466
+ init_diff();
467
+ editSpecTool = {
468
+ definition: {
469
+ name: "editSpec",
470
+ 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.',
471
+ inputSchema: {
472
+ type: "object",
473
+ properties: {
474
+ path: {
475
+ type: "string",
476
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
477
+ },
478
+ edits: {
479
+ type: "array",
480
+ items: {
481
+ type: "object",
482
+ properties: {
483
+ heading: {
484
+ type: "string",
485
+ description: 'Heading path using " > " to separate nesting levels (e.g., "Vendors > Approval Flow"). Empty string targets the preamble (content before the first heading).'
486
+ },
487
+ operation: {
488
+ type: "string",
489
+ enum: ["replace", "insert_after", "insert_before", "delete"],
490
+ 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."
491
+ },
492
+ content: {
493
+ type: "string",
494
+ description: "MSFM markdown content for replace/insert operations. Not needed for delete."
495
+ }
496
+ },
497
+ required: ["heading", "operation"]
498
+ },
499
+ description: "Array of edits to apply in order."
500
+ }
501
+ },
502
+ required: ["path", "edits"]
503
+ }
504
+ },
505
+ async execute(input) {
506
+ try {
507
+ validateSpecPath(input.path);
508
+ } catch (err) {
509
+ return `Error: ${err.message}`;
510
+ }
511
+ let originalContent;
512
+ try {
513
+ originalContent = await fs4.readFile(input.path, "utf-8");
514
+ } catch (err) {
515
+ return `Error reading file: ${err.message}`;
516
+ }
517
+ let content = originalContent;
518
+ for (const edit of input.edits) {
519
+ let range;
520
+ try {
521
+ range = resolveHeadingPath(content, edit.heading);
522
+ } catch (err) {
523
+ const tree = getHeadingTree(content);
524
+ return `Error: ${err.message}
525
+
526
+ Document structure:
527
+ ${tree}`;
528
+ }
529
+ const lines = content.split("\n");
530
+ switch (edit.operation) {
531
+ case "replace": {
532
+ if (edit.content == null) {
533
+ return 'Error: "content" is required for replace operations.';
534
+ }
535
+ const contentLines = edit.content.split("\n");
536
+ lines.splice(
537
+ range.contentStart,
538
+ range.contentEnd - range.contentStart,
539
+ ...contentLines
540
+ );
541
+ break;
542
+ }
543
+ case "insert_after": {
544
+ if (edit.content == null) {
545
+ return 'Error: "content" is required for insert_after operations.';
546
+ }
547
+ const contentLines = edit.content.split("\n");
548
+ lines.splice(range.contentEnd, 0, ...contentLines);
549
+ break;
550
+ }
551
+ case "insert_before": {
552
+ if (edit.content == null) {
553
+ return 'Error: "content" is required for insert_before operations.';
554
+ }
555
+ const contentLines = edit.content.split("\n");
556
+ lines.splice(range.startLine, 0, ...contentLines);
557
+ break;
558
+ }
559
+ case "delete": {
560
+ lines.splice(range.startLine, range.contentEnd - range.startLine);
561
+ break;
562
+ }
563
+ default:
564
+ return `Error: Unknown operation "${edit.operation}". Use replace, insert_after, insert_before, or delete.`;
565
+ }
566
+ content = lines.join("\n");
567
+ }
568
+ try {
569
+ await fs4.writeFile(input.path, content, "utf-8");
570
+ } catch (err) {
571
+ return `Error writing file: ${err.message}`;
572
+ }
573
+ return unifiedDiff(input.path, originalContent, content);
574
+ }
575
+ };
576
+ }
577
+ });
578
+
579
+ // src/tools/spec/listSpecFiles.ts
580
+ import fs5 from "fs/promises";
581
+ import path2 from "path";
582
+ async function listRecursive(dir) {
583
+ const results = [];
584
+ const entries = await fs5.readdir(dir, { withFileTypes: true });
585
+ entries.sort((a, b) => {
586
+ if (a.isDirectory() && !b.isDirectory()) {
587
+ return -1;
588
+ }
589
+ if (!a.isDirectory() && b.isDirectory()) {
590
+ return 1;
591
+ }
592
+ return a.name.localeCompare(b.name);
593
+ });
594
+ for (const entry of entries) {
595
+ const fullPath = path2.join(dir, entry.name);
596
+ if (entry.isDirectory()) {
597
+ results.push(`${fullPath}/`);
598
+ results.push(...await listRecursive(fullPath));
599
+ } else {
600
+ results.push(fullPath);
601
+ }
602
+ }
603
+ return results;
604
+ }
605
+ var listSpecFilesTool;
606
+ var init_listSpecFiles = __esm({
607
+ "src/tools/spec/listSpecFiles.ts"() {
608
+ "use strict";
609
+ listSpecFilesTool = {
610
+ definition: {
611
+ name: "listSpecFiles",
612
+ 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.",
613
+ inputSchema: {
614
+ type: "object",
615
+ properties: {},
616
+ required: []
617
+ }
618
+ },
619
+ async execute() {
620
+ try {
621
+ const entries = await listRecursive("src");
622
+ if (entries.length === 0) {
623
+ return "src/ is empty \u2014 no spec files yet.";
624
+ }
625
+ return entries.join("\n");
626
+ } catch (err) {
627
+ if (err.code === "ENOENT") {
628
+ return "Error: src/ directory does not exist.";
629
+ }
630
+ return `Error listing spec files: ${err.message}`;
631
+ }
632
+ }
633
+ };
634
+ }
635
+ });
636
+
637
+ // src/tools/spec/setViewMode.ts
638
+ var setViewModeTool;
639
+ var init_setViewMode = __esm({
640
+ "src/tools/spec/setViewMode.ts"() {
641
+ "use strict";
642
+ setViewModeTool = {
643
+ definition: {
644
+ name: "setViewMode",
645
+ 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.',
646
+ inputSchema: {
647
+ type: "object",
648
+ properties: {
649
+ mode: {
650
+ type: "string",
651
+ enum: [
652
+ "intake",
653
+ "preview",
654
+ "spec",
655
+ "code",
656
+ "databases",
657
+ "scenarios",
658
+ "logs"
659
+ ],
660
+ description: "The view mode to switch to."
661
+ }
662
+ },
663
+ required: ["mode"]
664
+ }
665
+ },
666
+ async execute() {
667
+ return "View mode updated.";
668
+ }
669
+ };
670
+ }
671
+ });
672
+
673
+ // src/tools/spec/promptUser.ts
674
+ var promptUserTool;
675
+ var init_promptUser = __esm({
676
+ "src/tools/spec/promptUser.ts"() {
677
+ "use strict";
678
+ promptUserTool = {
679
+ definition: {
680
+ name: "promptUser",
681
+ 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.',
682
+ inputSchema: {
683
+ type: "object",
684
+ properties: {
685
+ type: {
686
+ type: "string",
687
+ enum: ["form", "inline"],
688
+ description: "Choose this first, before writing questions. form: full form for structured intake with many questions. inline: compact in-chat display."
689
+ },
690
+ questions: {
691
+ type: "array",
692
+ items: {
693
+ type: "object",
694
+ properties: {
695
+ id: {
696
+ type: "string",
697
+ description: "Unique identifier for this question. Used as the key in the response object."
698
+ },
699
+ question: {
700
+ type: "string",
701
+ description: "The question to ask."
702
+ },
703
+ type: {
704
+ type: "string",
705
+ enum: ["select", "text", "confirm", "file", "color"],
706
+ 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)."
707
+ },
708
+ helpText: {
709
+ type: "string",
710
+ description: "Optional detail rendered below the question as a subtitle."
711
+ },
712
+ required: {
713
+ type: "boolean",
714
+ description: "Whether the user must answer this question. Defaults to false."
715
+ },
716
+ options: {
717
+ type: "array",
718
+ items: {
719
+ oneOf: [
720
+ { type: "string" },
721
+ {
722
+ type: "object",
723
+ properties: {
724
+ label: {
725
+ type: "string",
726
+ description: "The option text."
727
+ },
728
+ description: {
729
+ type: "string",
730
+ description: "Optional detail shown below the label."
731
+ }
732
+ },
733
+ required: ["label"]
734
+ }
735
+ ]
736
+ },
737
+ description: "Options for select type. Each can be a string or { label, description }."
738
+ },
739
+ multiple: {
740
+ type: "boolean",
741
+ description: "For select: allow picking multiple options (returns array). For file: allow multiple uploads (returns array of URLs). Defaults to false."
742
+ },
743
+ allowOther: {
744
+ type: "boolean",
745
+ description: 'For select type: adds an "Other" option with a free-form text input. Defaults to false.'
746
+ },
747
+ format: {
748
+ type: "string",
749
+ enum: ["email", "url", "phone", "number"],
750
+ description: "For text type: adds input validation and mobile keyboard hints."
751
+ },
752
+ placeholder: {
753
+ type: "string",
754
+ description: "For text type: placeholder hint text."
755
+ },
756
+ accept: {
757
+ type: "string",
758
+ 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.'
759
+ }
760
+ },
761
+ required: ["id", "question", "type"]
762
+ },
763
+ description: "One or more questions to present."
764
+ }
765
+ },
766
+ required: ["type", "questions"]
767
+ }
768
+ },
769
+ streaming: {
770
+ partialInput: (partial, lastCount) => {
771
+ const questions = partial.questions;
772
+ if (!Array.isArray(questions) || questions.length === 0) {
773
+ return null;
774
+ }
775
+ const hasType = typeof partial.type === "string";
776
+ if (!hasType && questions.length < 3) {
777
+ return null;
778
+ }
779
+ const confirmed = questions.length > 1 ? questions.slice(0, -1) : [];
780
+ if (confirmed.length <= lastCount) {
781
+ return null;
782
+ }
783
+ return {
784
+ input: {
785
+ ...partial,
786
+ type: partial.type ?? "inline",
787
+ questions: confirmed
788
+ },
789
+ emittedCount: confirmed.length
790
+ };
791
+ }
792
+ },
793
+ async execute(input) {
794
+ const questions = input.questions;
795
+ const lines = questions.map((q) => {
796
+ let line = `- ${q.question}`;
797
+ if (q.type === "select") {
798
+ const opts = (q.options || []).map(
799
+ (o) => typeof o === "string" ? o : o.label
800
+ );
801
+ line += q.multiple ? ` (pick one or more: ${opts.join(" / ")})` : ` (${opts.join(" / ")})`;
802
+ } else if (q.type === "confirm") {
803
+ line += " (yes / no)";
804
+ } else if (q.type === "file") {
805
+ line += " (upload file)";
806
+ } else if (q.type === "color") {
807
+ line += " (pick a color)";
808
+ }
809
+ return line;
810
+ });
811
+ return `Please answer these questions:
812
+ ${lines.join("\n")}`;
813
+ }
814
+ };
815
+ }
816
+ });
817
+
818
+ // src/tools/spec/clearSyncStatus.ts
819
+ var clearSyncStatusTool;
820
+ var init_clearSyncStatus = __esm({
821
+ "src/tools/spec/clearSyncStatus.ts"() {
822
+ "use strict";
823
+ clearSyncStatusTool = {
824
+ definition: {
825
+ name: "clearSyncStatus",
826
+ description: "Clear the sync status flags after syncing spec and code. Call this after finishing a sync operation.",
827
+ inputSchema: {
828
+ type: "object",
829
+ properties: {}
830
+ }
831
+ },
832
+ async execute() {
833
+ return "ok";
834
+ }
835
+ };
836
+ }
837
+ });
838
+
839
+ // src/tools/spec/presentSyncPlan.ts
840
+ var presentSyncPlanTool;
841
+ var init_presentSyncPlan = __esm({
842
+ "src/tools/spec/presentSyncPlan.ts"() {
843
+ "use strict";
844
+ presentSyncPlanTool = {
845
+ definition: {
846
+ name: "presentSyncPlan",
847
+ 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.",
848
+ inputSchema: {
849
+ type: "object",
850
+ properties: {
851
+ content: {
852
+ type: "string",
853
+ description: "Markdown plan describing what changed and what will be updated."
854
+ }
855
+ },
856
+ required: ["content"]
857
+ }
858
+ },
859
+ streaming: {},
860
+ async execute() {
861
+ return "approved";
862
+ }
863
+ };
864
+ }
865
+ });
866
+
867
+ // src/tools/spec/presentPublishPlan.ts
868
+ var presentPublishPlanTool;
869
+ var init_presentPublishPlan = __esm({
870
+ "src/tools/spec/presentPublishPlan.ts"() {
871
+ "use strict";
872
+ presentPublishPlanTool = {
873
+ definition: {
874
+ name: "presentPublishPlan",
875
+ 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.",
876
+ inputSchema: {
877
+ type: "object",
878
+ properties: {
879
+ content: {
880
+ type: "string",
881
+ description: "Markdown changelog describing what changed and what will be deployed."
882
+ }
883
+ },
884
+ required: ["content"]
885
+ }
886
+ },
887
+ streaming: {},
888
+ async execute() {
889
+ return "approved";
890
+ }
891
+ };
892
+ }
893
+ });
894
+
895
+ // src/tools/spec/presentPlan.ts
896
+ var presentPlanTool;
897
+ var init_presentPlan = __esm({
898
+ "src/tools/spec/presentPlan.ts"() {
899
+ "use strict";
900
+ presentPlanTool = {
901
+ definition: {
902
+ name: "presentPlan",
903
+ 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.",
904
+ inputSchema: {
905
+ type: "object",
906
+ properties: {
907
+ content: {
908
+ type: "string",
909
+ description: "Markdown plan describing what you intend to do."
910
+ }
911
+ },
912
+ required: ["content"]
913
+ }
914
+ },
915
+ streaming: {},
916
+ async execute() {
917
+ return "approved";
918
+ }
919
+ };
920
+ }
921
+ });
922
+
923
+ // src/tools/code/readFile.ts
924
+ import fs6 from "fs/promises";
925
+ function isBinary(buffer) {
926
+ const sample = buffer.subarray(0, 8192);
927
+ for (let i = 0; i < sample.length; i++) {
928
+ if (sample[i] === 0) {
929
+ return true;
930
+ }
931
+ }
932
+ return false;
933
+ }
934
+ var DEFAULT_MAX_LINES2, readFileTool;
935
+ var init_readFile = __esm({
936
+ "src/tools/code/readFile.ts"() {
937
+ "use strict";
938
+ DEFAULT_MAX_LINES2 = 500;
939
+ readFileTool = {
940
+ definition: {
941
+ name: "readFile",
942
+ 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).",
943
+ inputSchema: {
944
+ type: "object",
945
+ properties: {
946
+ path: {
947
+ type: "string",
948
+ description: "The file path to read, relative to the project root."
949
+ },
950
+ offset: {
951
+ type: "number",
952
+ 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."
953
+ },
954
+ maxLines: {
955
+ type: "number",
956
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
957
+ }
958
+ },
959
+ required: ["path"]
960
+ }
961
+ },
962
+ async execute(input) {
963
+ try {
964
+ const buffer = await fs6.readFile(input.path);
965
+ if (isBinary(buffer)) {
966
+ const size = buffer.length;
967
+ const unit = size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)}MB` : `${(size / 1024).toFixed(1)}KB`;
968
+ return `Error: ${input.path} appears to be a binary file (${unit}). Use bash to inspect it if needed.`;
969
+ }
970
+ const content = buffer.toString("utf-8");
971
+ const allLines = content.split("\n");
972
+ const totalLines = allLines.length;
973
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
974
+ let startIdx;
975
+ if (input.offset && input.offset < 0) {
976
+ startIdx = Math.max(0, totalLines + input.offset);
977
+ } else {
978
+ startIdx = Math.max(0, (input.offset || 1) - 1);
979
+ }
980
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
981
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
982
+ let result = numbered;
983
+ const endLine = startIdx + sliced.length;
984
+ const displayStart = startIdx + 1;
985
+ if (endLine < totalLines) {
986
+ result += `
987
+
988
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
989
+ }
990
+ return result;
991
+ } catch (err) {
992
+ return `Error reading file: ${err.message}`;
993
+ }
994
+ }
995
+ };
996
+ }
997
+ });
998
+
999
+ // src/tools/code/writeFile.ts
1000
+ import fs7 from "fs/promises";
1001
+ import path3 from "path";
1002
+ var writeFileTool;
1003
+ var init_writeFile = __esm({
1004
+ "src/tools/code/writeFile.ts"() {
1005
+ "use strict";
1006
+ init_diff();
1007
+ writeFileTool = {
1008
+ definition: {
1009
+ name: "writeFile",
1010
+ 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.",
1011
+ inputSchema: {
1012
+ type: "object",
1013
+ properties: {
1014
+ path: {
1015
+ type: "string",
1016
+ description: "The file path to write, relative to the project root."
1017
+ },
1018
+ content: {
1019
+ type: "string",
1020
+ description: "The full content to write to the file."
1021
+ }
1022
+ },
1023
+ required: ["path", "content"]
1024
+ }
1025
+ },
1026
+ streaming: {
1027
+ transform: async (partial) => {
1028
+ const oldContent = await fs7.readFile(partial.path, "utf-8").catch(() => "");
1029
+ const lineCount = partial.content.split("\n").length;
1030
+ return `Writing ${partial.path} (${lineCount} lines)
1031
+ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
1032
+ }
1033
+ },
1034
+ async execute(input) {
1035
+ try {
1036
+ await fs7.mkdir(path3.dirname(input.path), { recursive: true });
1037
+ let oldContent = null;
1038
+ try {
1039
+ oldContent = await fs7.readFile(input.path, "utf-8");
1040
+ } catch {
1041
+ }
1042
+ await fs7.writeFile(input.path, input.content, "utf-8");
1043
+ const lineCount = input.content.split("\n").length;
1044
+ const label = oldContent !== null ? "Wrote" : "Created";
1045
+ return `${label} ${input.path} (${lineCount} lines)
1046
+ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1047
+ } catch (err) {
1048
+ return `Error writing file: ${err.message}`;
1049
+ }
1050
+ }
1051
+ };
1052
+ }
1053
+ });
1054
+
1055
+ // src/tools/code/editFile/_helpers.ts
1056
+ function buildLineOffsets(content) {
1057
+ const offsets = [0];
1058
+ for (let i = 0; i < content.length; i++) {
1059
+ if (content[i] === "\n") {
1060
+ offsets.push(i + 1);
1061
+ }
1062
+ }
1063
+ return offsets;
1064
+ }
1065
+ function lineAtOffset(offsets, charIndex) {
1066
+ let lo = 0;
1067
+ let hi = offsets.length - 1;
1068
+ while (lo < hi) {
1069
+ const mid = lo + hi + 1 >> 1;
1070
+ if (offsets[mid] <= charIndex) {
1071
+ lo = mid;
1072
+ } else {
1073
+ hi = mid - 1;
1074
+ }
1075
+ }
1076
+ return lo + 1;
1077
+ }
1078
+ function findOccurrences(content, searchString) {
1079
+ if (!searchString) {
1080
+ return [];
1081
+ }
1082
+ const offsets = buildLineOffsets(content);
1083
+ const results = [];
1084
+ let pos = 0;
1085
+ while (pos <= content.length - searchString.length) {
1086
+ const idx = content.indexOf(searchString, pos);
1087
+ if (idx === -1) {
1088
+ break;
1089
+ }
1090
+ results.push({ index: idx, line: lineAtOffset(offsets, idx) });
1091
+ pos = idx + 1;
1092
+ }
1093
+ return results;
1094
+ }
1095
+ function flexibleMatch(content, searchString) {
1096
+ const contentLines = content.split("\n");
1097
+ const searchLines = searchString.split("\n").map((l) => l.trimStart());
1098
+ if (searchLines.length === 0) {
1099
+ return null;
1100
+ }
1101
+ const matches = [];
1102
+ for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
1103
+ let allMatch = true;
1104
+ for (let j = 0; j < searchLines.length; j++) {
1105
+ if (contentLines[i + j].trimStart() !== searchLines[j]) {
1106
+ allMatch = false;
1107
+ break;
1108
+ }
1109
+ }
1110
+ if (allMatch) {
1111
+ matches.push(i);
1112
+ }
1113
+ }
1114
+ if (matches.length !== 1) {
1115
+ return null;
1116
+ }
1117
+ const startIdx = matches[0];
1118
+ const matchedText = contentLines.slice(startIdx, startIdx + searchLines.length).join("\n");
1119
+ const offsets = buildLineOffsets(content);
1120
+ return {
1121
+ matchedText,
1122
+ index: offsets[startIdx],
1123
+ line: startIdx + 1
1124
+ // 1-based
1125
+ };
1126
+ }
1127
+ function replaceAt(content, index, oldLength, newString) {
1128
+ return content.slice(0, index) + newString + content.slice(index + oldLength);
1129
+ }
1130
+ function formatOccurrenceError(count, lines, filePath) {
1131
+ 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.`;
1132
+ }
1133
+ var init_helpers2 = __esm({
1134
+ "src/tools/code/editFile/_helpers.ts"() {
1135
+ "use strict";
1136
+ }
1137
+ });
1138
+
1139
+ // src/tools/code/editFile/index.ts
1140
+ import fs8 from "fs/promises";
1141
+ var editFileTool;
1142
+ var init_editFile = __esm({
1143
+ "src/tools/code/editFile/index.ts"() {
1144
+ "use strict";
1145
+ init_diff();
1146
+ init_helpers2();
1147
+ editFileTool = {
1148
+ definition: {
1149
+ name: "editFile",
1150
+ 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.",
1151
+ inputSchema: {
1152
+ type: "object",
1153
+ properties: {
1154
+ path: {
1155
+ type: "string",
1156
+ description: "The file path to edit, relative to the project root."
1157
+ },
1158
+ old_string: {
1159
+ type: "string",
1160
+ description: "The exact string to find and replace. Must be unique in the file unless replace_all is true."
1161
+ },
1162
+ new_string: {
1163
+ type: "string",
1164
+ description: "The replacement string."
1165
+ },
1166
+ replace_all: {
1167
+ type: "boolean",
1168
+ description: "If true, replace every occurrence of old_string in the file. Defaults to false."
1169
+ }
1170
+ },
1171
+ required: ["path", "old_string", "new_string"]
1172
+ }
1173
+ },
1174
+ async execute(input) {
1175
+ try {
1176
+ const content = await fs8.readFile(input.path, "utf-8");
1177
+ const { old_string, new_string, replace_all } = input;
1178
+ const occurrences = findOccurrences(content, old_string);
1179
+ if (replace_all) {
1180
+ if (occurrences.length === 0) {
1181
+ return `Error: old_string not found in ${input.path}.`;
1182
+ }
1183
+ let updated = content;
1184
+ for (let i = occurrences.length - 1; i >= 0; i--) {
1185
+ updated = replaceAt(
1186
+ updated,
1187
+ occurrences[i].index,
1188
+ old_string.length,
1189
+ new_string
1190
+ );
1191
+ }
1192
+ await fs8.writeFile(input.path, updated, "utf-8");
1193
+ return `Replaced ${occurrences.length} occurrence${occurrences.length > 1 ? "s" : ""} in ${input.path}
1194
+ ${unifiedDiff(input.path, content, updated)}`;
1195
+ }
1196
+ if (occurrences.length === 1) {
1197
+ const updated = replaceAt(
1198
+ content,
1199
+ occurrences[0].index,
1200
+ old_string.length,
1201
+ new_string
1202
+ );
1203
+ await fs8.writeFile(input.path, updated, "utf-8");
1204
+ return `Updated ${input.path}
1205
+ ${unifiedDiff(input.path, content, updated)}`;
1206
+ }
1207
+ if (occurrences.length > 1) {
1208
+ const lines = occurrences.map((o) => o.line);
1209
+ return `Error: ${formatOccurrenceError(occurrences.length, lines, input.path)}`;
1210
+ }
1211
+ const flex = flexibleMatch(content, old_string);
1212
+ if (flex) {
1213
+ const updated = replaceAt(
1214
+ content,
1215
+ flex.index,
1216
+ flex.matchedText.length,
1217
+ new_string
1218
+ );
1219
+ await fs8.writeFile(input.path, updated, "utf-8");
1220
+ return `Updated ${input.path} (matched with flexible whitespace at line ${flex.line})
1221
+ ${unifiedDiff(input.path, content, updated)}`;
1222
+ }
1223
+ return `Error: old_string not found in ${input.path}. Make sure you've read the file first and copied the exact text.`;
1224
+ } catch (err) {
1225
+ return `Error editing file: ${err.message}`;
1226
+ }
1227
+ }
1228
+ };
1229
+ }
1230
+ });
1231
+
1232
+ // src/tools/code/bash.ts
1233
+ import { exec } from "child_process";
1234
+ var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINES3, bashTool;
1235
+ var init_bash = __esm({
1236
+ "src/tools/code/bash.ts"() {
1237
+ "use strict";
1238
+ DEFAULT_TIMEOUT_MS = 12e4;
1239
+ DEFAULT_MAX_LINES3 = 500;
1240
+ bashTool = {
1241
+ definition: {
1242
+ name: "bash",
1243
+ 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.",
1244
+ inputSchema: {
1245
+ type: "object",
1246
+ properties: {
1247
+ command: {
1248
+ type: "string",
1249
+ description: "The shell command to execute."
1250
+ },
1251
+ cwd: {
1252
+ type: "string",
1253
+ description: "Working directory to run the command in. Defaults to the project root."
1254
+ },
1255
+ timeout: {
1256
+ type: "number",
1257
+ description: "Timeout in seconds. Defaults to 120. Use higher values for long-running commands like builds or test suites."
1258
+ },
1259
+ maxLines: {
1260
+ type: "number",
1261
+ description: "Maximum number of output lines to return. Defaults to 500. Set to 0 for no limit."
1262
+ }
1263
+ },
1264
+ required: ["command"]
1265
+ }
1266
+ },
1267
+ async execute(input) {
1268
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1269
+ const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1270
+ return new Promise((resolve) => {
1271
+ exec(
1272
+ input.command,
1273
+ {
1274
+ timeout: timeoutMs,
1275
+ maxBuffer: 2 * 1024 * 1024,
1276
+ ...input.cwd ? { cwd: input.cwd } : {},
1277
+ env: { ...process.env, FORCE_COLOR: "1" }
1278
+ },
1279
+ (err, stdout, stderr) => {
1280
+ let result = "";
1281
+ if (stdout) {
1282
+ result += stdout;
1283
+ }
1284
+ if (stderr) {
1285
+ result += (result ? "\n" : "") + stderr;
1286
+ }
1287
+ if (err && !stdout && !stderr) {
1288
+ result = `Error: ${err.message}`;
1289
+ }
1290
+ if (!result) {
1291
+ resolve("(no output)");
1292
+ return;
1293
+ }
1294
+ const lines = result.split("\n");
1295
+ if (lines.length > maxLines) {
1296
+ resolve(
1297
+ lines.slice(0, maxLines).join("\n") + `
1298
+
1299
+ (truncated at ${maxLines} lines of ${lines.length} total \u2014 increase maxLines to see more)`
1300
+ );
1301
+ } else {
1302
+ resolve(result);
1303
+ }
1304
+ }
1305
+ );
1306
+ });
1307
+ }
1308
+ };
1309
+ }
1310
+ });
1311
+
1312
+ // src/tools/code/grep.ts
1313
+ import { exec as exec2 } from "child_process";
1314
+ function formatResults(stdout, max) {
1315
+ const lines = stdout.trim().split("\n");
1316
+ let result = lines.join("\n");
1317
+ if (lines.length >= max) {
1318
+ result += `
1319
+
1320
+ (truncated at ${max} results \u2014 increase maxResults to see more)`;
1321
+ }
1322
+ return result;
1323
+ }
1324
+ var DEFAULT_MAX, grepTool;
1325
+ var init_grep = __esm({
1326
+ "src/tools/code/grep.ts"() {
1327
+ "use strict";
1328
+ DEFAULT_MAX = 50;
1329
+ grepTool = {
1330
+ definition: {
1331
+ name: "grep",
1332
+ 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.",
1333
+ inputSchema: {
1334
+ type: "object",
1335
+ properties: {
1336
+ pattern: {
1337
+ type: "string",
1338
+ description: "The search pattern (regex supported)."
1339
+ },
1340
+ path: {
1341
+ type: "string",
1342
+ description: "Directory or file to search in. Defaults to current directory."
1343
+ },
1344
+ glob: {
1345
+ type: "string",
1346
+ description: 'File glob to filter (e.g., "*.ts"). Only used with ripgrep.'
1347
+ },
1348
+ maxResults: {
1349
+ type: "number",
1350
+ description: "Maximum number of matching lines to return. Defaults to 50. Increase if you need more comprehensive results."
1351
+ }
1352
+ },
1353
+ required: ["pattern"]
1354
+ }
1355
+ },
1356
+ async execute(input) {
1357
+ const searchPath = input.path || ".";
1358
+ const max = input.maxResults || DEFAULT_MAX;
1359
+ const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
1360
+ const escaped = input.pattern.replace(/'/g, "'\\''");
1361
+ const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1362
+ const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1363
+ return new Promise((resolve) => {
1364
+ exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1365
+ if (stdout?.trim()) {
1366
+ resolve(formatResults(stdout, max));
1367
+ return;
1368
+ }
1369
+ exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1370
+ if (grepStdout?.trim()) {
1371
+ resolve(formatResults(grepStdout, max));
1372
+ } else {
1373
+ resolve("No matches found.");
1374
+ }
1375
+ });
1376
+ });
1377
+ });
1378
+ }
1379
+ };
1380
+ }
1381
+ });
1382
+
1383
+ // src/tools/code/glob.ts
1384
+ import fg from "fast-glob";
1385
+ var DEFAULT_MAX2, globTool;
1386
+ var init_glob = __esm({
1387
+ "src/tools/code/glob.ts"() {
1388
+ "use strict";
1389
+ DEFAULT_MAX2 = 200;
1390
+ globTool = {
1391
+ definition: {
1392
+ name: "glob",
1393
+ 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.',
1394
+ inputSchema: {
1395
+ type: "object",
1396
+ properties: {
1397
+ pattern: {
1398
+ type: "string",
1399
+ description: 'Glob pattern (e.g., "**/*.ts", "src/**/*.tsx", "*.json").'
1400
+ },
1401
+ maxResults: {
1402
+ type: "number",
1403
+ description: "Maximum number of file paths to return. Defaults to 200. Increase if you need the complete list."
1404
+ }
1405
+ },
1406
+ required: ["pattern"]
1407
+ }
1408
+ },
1409
+ async execute(input) {
1410
+ try {
1411
+ const max = input.maxResults || DEFAULT_MAX2;
1412
+ const files = await fg(input.pattern, {
1413
+ ignore: ["**/node_modules/**", "**/.git/**"],
1414
+ dot: false
1415
+ });
1416
+ if (files.length === 0) {
1417
+ return "No files found.";
1418
+ }
1419
+ const sorted = files.sort();
1420
+ const truncated = sorted.slice(0, max);
1421
+ let result = truncated.join("\n");
1422
+ if (sorted.length > max) {
1423
+ result += `
1424
+
1425
+ (showing ${max} of ${sorted.length} matches \u2014 increase maxResults to see all)`;
1426
+ }
1427
+ return result;
1428
+ } catch (err) {
1429
+ return `Error: ${err.message}`;
1430
+ }
1431
+ }
1432
+ };
1433
+ }
1434
+ });
1435
+
1436
+ // src/tools/code/listDir.ts
1437
+ import fs9 from "fs/promises";
1438
+ var listDirTool;
1439
+ var init_listDir = __esm({
1440
+ "src/tools/code/listDir.ts"() {
1441
+ "use strict";
1442
+ listDirTool = {
1443
+ definition: {
1444
+ name: "listDir",
1445
+ 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.",
1446
+ inputSchema: {
1447
+ type: "object",
1448
+ properties: {
1449
+ path: {
1450
+ type: "string",
1451
+ description: 'Directory path to list, relative to project root. Defaults to ".".'
1452
+ }
1453
+ }
1454
+ }
1455
+ },
1456
+ async execute(input) {
1457
+ const dirPath = input.path || ".";
1458
+ try {
1459
+ const entries = await fs9.readdir(dirPath, { withFileTypes: true });
1460
+ const lines = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
1461
+ if (a.isDirectory() && !b.isDirectory()) {
1462
+ return -1;
1463
+ }
1464
+ if (!a.isDirectory() && b.isDirectory()) {
1465
+ return 1;
1466
+ }
1467
+ return a.name.localeCompare(b.name);
1468
+ }).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
1469
+ return lines.join("\n") || "(empty directory)";
1470
+ } catch (err) {
1471
+ return `Error listing directory: ${err.message}`;
1472
+ }
1473
+ }
1474
+ };
1475
+ }
1476
+ });
1477
+
1478
+ // src/tools/code/editsFinished.ts
1479
+ var editsFinishedTool;
1480
+ var init_editsFinished = __esm({
1481
+ "src/tools/code/editsFinished.ts"() {
1482
+ "use strict";
1483
+ editsFinishedTool = {
1484
+ definition: {
1485
+ name: "editsFinished",
1486
+ 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.",
1487
+ inputSchema: {
1488
+ type: "object",
1489
+ properties: {},
1490
+ required: []
1491
+ }
1492
+ },
1493
+ async execute() {
1494
+ return "Preview updated.";
1495
+ }
1496
+ };
1497
+ }
1498
+ });
1499
+
1500
+ // src/tools/_helpers/lsp.ts
1501
+ function setLspBaseUrl(url) {
1502
+ lspBaseUrl = url;
1503
+ log.info("LSP configured", { url });
1504
+ }
1505
+ function isLspConfigured() {
1506
+ return lspBaseUrl !== null;
1507
+ }
1508
+ async function lspRequest(endpoint, body) {
1509
+ if (!lspBaseUrl) {
1510
+ throw new Error("LSP not available");
1511
+ }
1512
+ const url = `${lspBaseUrl}${endpoint}`;
1513
+ log.debug("LSP request", { endpoint, body });
1514
+ try {
1515
+ const res = await fetch(url, {
1516
+ method: "POST",
1517
+ headers: { "Content-Type": "application/json" },
1518
+ body: JSON.stringify(body)
1519
+ });
1520
+ if (!res.ok) {
1521
+ log.error("LSP sidecar error", { endpoint, status: res.status });
1522
+ throw new Error(`LSP sidecar error: ${res.status}`);
1523
+ }
1524
+ return res.json();
1525
+ } catch (err) {
1526
+ if (err.message.startsWith("LSP sidecar")) {
1527
+ throw err;
1528
+ }
1529
+ log.error("LSP connection error", { endpoint, error: err.message });
1530
+ throw new Error(`LSP connection error: ${err.message}`);
1531
+ }
1532
+ }
1533
+ var lspBaseUrl;
1534
+ var init_lsp = __esm({
1535
+ "src/tools/_helpers/lsp.ts"() {
1536
+ "use strict";
1537
+ init_logger();
1538
+ lspBaseUrl = null;
1539
+ }
1540
+ });
1541
+
1542
+ // src/tools/code/lspDiagnostics.ts
1543
+ var lspDiagnosticsTool;
1544
+ var init_lspDiagnostics = __esm({
1545
+ "src/tools/code/lspDiagnostics.ts"() {
1546
+ "use strict";
1547
+ init_lsp();
1548
+ lspDiagnosticsTool = {
1549
+ definition: {
1550
+ name: "lspDiagnostics",
1551
+ 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.",
1552
+ inputSchema: {
1553
+ type: "object",
1554
+ properties: {
1555
+ file: {
1556
+ type: "string",
1557
+ description: "File path relative to workspace root."
1558
+ }
1559
+ },
1560
+ required: ["file"]
1561
+ }
1562
+ },
1563
+ async execute(input) {
1564
+ const data = await lspRequest("/diagnostics", { file: input.file });
1565
+ const diags = data.diagnostics || [];
1566
+ if (diags.length === 0) {
1567
+ return "No diagnostics \u2014 file is clean.";
1568
+ }
1569
+ const lines = [];
1570
+ for (const d of diags) {
1571
+ let line = `${d.severity}: ${d.file}:${d.line}:${d.column} \u2014 ${d.message}`;
1572
+ try {
1573
+ const actionsData = await lspRequest("/code-actions", {
1574
+ file: d.file,
1575
+ startLine: d.line,
1576
+ startColumn: d.column,
1577
+ endLine: d.endLine ?? d.line,
1578
+ endColumn: d.endColumn ?? d.column,
1579
+ diagnostics: [d]
1580
+ });
1581
+ const actions = actionsData.actions || [];
1582
+ if (actions.length > 0) {
1583
+ const fixes = actions.map((a) => a.title).join("; ");
1584
+ line += `
1585
+ Quick fixes: ${fixes}`;
1586
+ }
1587
+ } catch {
1588
+ }
1589
+ lines.push(line);
1590
+ }
1591
+ return lines.join("\n");
1592
+ }
1593
+ };
1594
+ }
1595
+ });
1596
+
1597
+ // src/tools/code/restartProcess.ts
1598
+ var restartProcessTool;
1599
+ var init_restartProcess = __esm({
1600
+ "src/tools/code/restartProcess.ts"() {
1601
+ "use strict";
1602
+ init_lsp();
1603
+ restartProcessTool = {
1604
+ definition: {
1605
+ name: "restartProcess",
1606
+ 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.",
1607
+ inputSchema: {
1608
+ type: "object",
1609
+ properties: {
1610
+ name: {
1611
+ type: "string",
1612
+ description: 'Process name to restart. Currently supported: "devServer".'
1613
+ }
1614
+ },
1615
+ required: ["name"]
1616
+ }
1617
+ },
1618
+ async execute(input) {
1619
+ const data = await lspRequest("/restart-process", { name: input.name });
1620
+ if (data.ok) {
1621
+ return `Restarted ${input.name}.`;
1622
+ }
1623
+ return `Error: unexpected response: ${JSON.stringify(data)}`;
1624
+ }
1625
+ };
1626
+ }
1627
+ });
1628
+
1629
+ // src/tools/index.ts
1630
+ function getSpecTools() {
1631
+ return [readSpecTool, writeSpecTool, editSpecTool, listSpecFilesTool];
1632
+ }
1633
+ function getCodeTools() {
1634
+ const tools = [
1635
+ readFileTool,
1636
+ writeFileTool,
1637
+ editFileTool,
1638
+ bashTool,
1639
+ grepTool,
1640
+ globTool,
1641
+ listDirTool,
1642
+ editsFinishedTool
1643
+ ];
1644
+ if (isLspConfigured()) {
1645
+ tools.push(lspDiagnosticsTool, restartProcessTool);
1646
+ }
1647
+ return tools;
1648
+ }
1649
+ function getTools(projectHasCode) {
1650
+ if (projectHasCode) {
1651
+ return [
1652
+ setViewModeTool,
1653
+ promptUserTool,
1654
+ clearSyncStatusTool,
1655
+ presentSyncPlanTool,
1656
+ presentPublishPlanTool,
1657
+ presentPlanTool,
1658
+ ...getSpecTools(),
1659
+ ...getCodeTools()
1660
+ ];
1661
+ }
1662
+ return [
1663
+ setViewModeTool,
1664
+ promptUserTool,
1665
+ clearSyncStatusTool,
1666
+ presentSyncPlanTool,
1667
+ presentPublishPlanTool,
1668
+ ...getSpecTools()
1669
+ ];
1670
+ }
1671
+ function getToolDefinitions(projectHasCode) {
1672
+ return getTools(projectHasCode).map((t) => t.definition);
1673
+ }
1674
+ function getToolByName(name) {
1675
+ const allTools = [
1676
+ setViewModeTool,
1677
+ promptUserTool,
1678
+ clearSyncStatusTool,
1679
+ presentSyncPlanTool,
1680
+ presentPublishPlanTool,
1681
+ presentPlanTool,
1682
+ ...getSpecTools(),
1683
+ ...getCodeTools()
1684
+ ];
1685
+ return allTools.find((t) => t.definition.name === name);
1686
+ }
1687
+ function executeTool(name, input) {
1688
+ const tool = getToolByName(name);
1689
+ if (!tool) {
1690
+ return Promise.resolve(`Error: Unknown tool "${name}"`);
1691
+ }
1692
+ return tool.execute(input);
1693
+ }
1694
+ var init_tools = __esm({
1695
+ "src/tools/index.ts"() {
1696
+ "use strict";
1697
+ init_readSpec();
1698
+ init_writeSpec();
1699
+ init_editSpec();
1700
+ init_listSpecFiles();
1701
+ init_setViewMode();
1702
+ init_promptUser();
1703
+ init_clearSyncStatus();
1704
+ init_presentSyncPlan();
1705
+ init_presentPublishPlan();
1706
+ init_presentPlan();
1707
+ init_readFile();
1708
+ init_writeFile();
1709
+ init_editFile();
1710
+ init_bash();
1711
+ init_grep();
1712
+ init_glob();
1713
+ init_listDir();
1714
+ init_editsFinished();
1715
+ init_lsp();
1716
+ init_lspDiagnostics();
1717
+ init_restartProcess();
1718
+ }
1719
+ });
1720
+
1721
+ // src/session.ts
1722
+ import fs10 from "fs";
1723
+ function loadSession(state) {
1724
+ try {
1725
+ const raw = fs10.readFileSync(SESSION_FILE, "utf-8");
1726
+ const data = JSON.parse(raw);
1727
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
1728
+ state.messages = sanitizeMessages(data.messages);
1729
+ return true;
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ return false;
1734
+ }
1735
+ function sanitizeMessages(messages) {
1736
+ const result = [];
1737
+ for (let i = 0; i < messages.length; i++) {
1738
+ result.push(messages[i]);
1739
+ const msg = messages[i];
1740
+ if (msg.role !== "assistant" || !msg.toolCalls?.length) {
1741
+ continue;
1742
+ }
1743
+ const resultIds = /* @__PURE__ */ new Set();
1744
+ for (let j = i + 1; j < messages.length; j++) {
1745
+ const next = messages[j];
1746
+ if (next.role === "user" && next.toolCallId) {
1747
+ resultIds.add(next.toolCallId);
1748
+ } else {
1749
+ break;
1750
+ }
1751
+ }
1752
+ for (const tc of msg.toolCalls) {
1753
+ if (!resultIds.has(tc.id)) {
1754
+ result.push({
1755
+ role: "user",
1756
+ content: "Error: tool result lost (session recovered)",
1757
+ toolCallId: tc.id,
1758
+ isToolError: true
1759
+ });
1760
+ }
1761
+ }
1762
+ }
1763
+ return result;
1764
+ }
1765
+ function saveSession(state) {
1766
+ try {
1767
+ fs10.writeFileSync(
1768
+ SESSION_FILE,
1769
+ JSON.stringify({ messages: state.messages }, null, 2),
1770
+ "utf-8"
1771
+ );
1772
+ } catch {
1773
+ }
1774
+ }
1775
+ function clearSession(state) {
1776
+ state.messages = [];
1777
+ try {
1778
+ fs10.unlinkSync(SESSION_FILE);
1779
+ } catch {
1780
+ }
1781
+ }
1782
+ var SESSION_FILE;
1783
+ var init_session = __esm({
1784
+ "src/session.ts"() {
1785
+ "use strict";
1786
+ SESSION_FILE = ".remy-session.json";
1787
+ }
1788
+ });
1789
+
1790
+ // src/parsePartialJson.ts
1791
+ function parsePartialJson(jsonString) {
1792
+ const length = jsonString.length;
1793
+ let index = 0;
1794
+ const markPartial = (msg) => {
1795
+ throw new PartialJSON(`${msg} at position ${index}`);
1796
+ };
1797
+ const throwMalformed = (msg) => {
1798
+ throw new MalformedJSON(`${msg} at position ${index}`);
1799
+ };
1800
+ const skipBlank = () => {
1801
+ while (index < length && " \n\r ".includes(jsonString[index])) {
1802
+ index++;
1803
+ }
1804
+ };
1805
+ const parseAny = () => {
1806
+ skipBlank();
1807
+ if (index >= length) {
1808
+ markPartial("Unexpected end of input");
1809
+ }
1810
+ const ch = jsonString[index];
1811
+ if (ch === '"') {
1812
+ return parseStr();
1813
+ }
1814
+ if (ch === "{") {
1815
+ return parseObj();
1816
+ }
1817
+ if (ch === "[") {
1818
+ return parseArr();
1819
+ }
1820
+ if (jsonString.substring(index, index + 4) === "null" || length - index < 4 && "null".startsWith(jsonString.substring(index))) {
1821
+ index += 4;
1822
+ return null;
1823
+ }
1824
+ if (jsonString.substring(index, index + 4) === "true" || length - index < 4 && "true".startsWith(jsonString.substring(index))) {
1825
+ index += 4;
1826
+ return true;
1827
+ }
1828
+ if (jsonString.substring(index, index + 5) === "false" || length - index < 5 && "false".startsWith(jsonString.substring(index))) {
1829
+ index += 5;
1830
+ return false;
1831
+ }
1832
+ return parseNum();
1833
+ };
1834
+ const parseStr = () => {
1835
+ const start = index;
1836
+ let escape = false;
1837
+ index++;
1838
+ while (index < length && (jsonString[index] !== '"' || escape && jsonString[index - 1] === "\\")) {
1839
+ escape = jsonString[index] === "\\" ? !escape : false;
1840
+ index++;
1841
+ }
1842
+ if (jsonString.charAt(index) === '"') {
1843
+ try {
1844
+ return JSON.parse(
1845
+ jsonString.substring(start, ++index - Number(escape))
1846
+ );
1847
+ } catch (e) {
1848
+ return throwMalformed(String(e));
1849
+ }
1850
+ }
1851
+ try {
1852
+ return JSON.parse(
1853
+ jsonString.substring(start, index - Number(escape)) + '"'
1854
+ );
1855
+ } catch {
1856
+ return JSON.parse(
1857
+ jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"'
1858
+ );
1859
+ }
1860
+ };
1861
+ const parseObj = () => {
1862
+ index++;
1863
+ skipBlank();
1864
+ const obj = {};
1865
+ try {
1866
+ while (jsonString[index] !== "}") {
1867
+ skipBlank();
1868
+ if (index >= length) {
1869
+ return obj;
1870
+ }
1871
+ const key = parseStr();
1872
+ skipBlank();
1873
+ index++;
1874
+ try {
1875
+ obj[key] = parseAny();
1876
+ } catch {
1877
+ return obj;
1878
+ }
1879
+ skipBlank();
1880
+ if (jsonString[index] === ",") {
1881
+ index++;
1882
+ }
1883
+ }
1884
+ } catch {
1885
+ return obj;
1886
+ }
1887
+ index++;
1888
+ return obj;
1889
+ };
1890
+ const parseArr = () => {
1891
+ index++;
1892
+ const arr = [];
1893
+ try {
1894
+ while (jsonString[index] !== "]") {
1895
+ arr.push(parseAny());
1896
+ skipBlank();
1897
+ if (jsonString[index] === ",") {
1898
+ index++;
1899
+ }
1900
+ }
1901
+ } catch {
1902
+ return arr;
1903
+ }
1904
+ index++;
1905
+ return arr;
1906
+ };
1907
+ const parseNum = () => {
1908
+ if (index === 0) {
1909
+ if (jsonString === "-") {
1910
+ throwMalformed("Not sure what '-' is");
1911
+ }
1912
+ try {
1913
+ return JSON.parse(jsonString);
1914
+ } catch (e) {
1915
+ try {
1916
+ return JSON.parse(
1917
+ jsonString.substring(0, jsonString.lastIndexOf("e"))
1918
+ );
1919
+ } catch {
1920
+ }
1921
+ throwMalformed(String(e));
1922
+ }
1923
+ }
1924
+ const start = index;
1925
+ if (jsonString[index] === "-") {
1926
+ index++;
1927
+ }
1928
+ while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1) {
1929
+ index++;
1930
+ }
1931
+ try {
1932
+ return JSON.parse(jsonString.substring(start, index));
1933
+ } catch (e) {
1934
+ if (jsonString.substring(start, index) === "-") {
1935
+ markPartial("Not sure what '-' is");
1936
+ }
1937
+ try {
1938
+ return JSON.parse(
1939
+ jsonString.substring(start, jsonString.lastIndexOf("e"))
1940
+ );
1941
+ } catch {
1942
+ throwMalformed(String(e));
1943
+ }
1944
+ }
1945
+ };
1946
+ return parseAny();
1947
+ }
1948
+ var PartialJSON, MalformedJSON;
1949
+ var init_parsePartialJson = __esm({
1950
+ "src/parsePartialJson.ts"() {
1951
+ "use strict";
1952
+ PartialJSON = class extends Error {
1953
+ };
1954
+ MalformedJSON = class extends Error {
1955
+ };
1956
+ }
1957
+ });
1958
+
1959
+ // src/agent.ts
1960
+ function createAgentState() {
1961
+ return { messages: [] };
1962
+ }
1963
+ async function runTurn(params) {
1964
+ const {
1965
+ state,
1966
+ userMessage,
1967
+ attachments,
1968
+ apiConfig,
1969
+ system,
1970
+ model,
1971
+ projectHasCode,
1972
+ signal,
1973
+ onEvent,
1974
+ resolveExternalTool,
1975
+ hidden
1976
+ } = params;
1977
+ const tools = getToolDefinitions(projectHasCode);
1978
+ log.info("Turn started", {
1979
+ messageLength: userMessage.length,
1980
+ toolCount: tools.length,
1981
+ tools: tools.map((t) => t.name),
1982
+ ...attachments && attachments.length > 0 && {
1983
+ attachmentCount: attachments.length,
1984
+ attachmentUrls: attachments.map((a) => a.url)
1985
+ }
1986
+ });
1987
+ onEvent({ type: "turn_started" });
1988
+ const userMsg = { role: "user", content: userMessage };
1989
+ if (hidden) {
1990
+ userMsg.hidden = true;
1991
+ }
1992
+ if (attachments && attachments.length > 0) {
1993
+ userMsg.attachments = attachments;
1994
+ log.debug("Attachments added to user message", {
1995
+ count: attachments.length,
1996
+ urls: attachments.map((a) => a.url)
1997
+ });
1998
+ }
1999
+ state.messages.push(userMsg);
2000
+ while (true) {
2001
+ let getOrCreateAccumulator2 = function(id, name) {
2002
+ let acc = toolInputAccumulators.get(id);
2003
+ if (!acc) {
2004
+ acc = { name, json: "", started: false, lastEmittedCount: 0 };
2005
+ toolInputAccumulators.set(id, acc);
2006
+ }
2007
+ return acc;
2008
+ };
2009
+ var getOrCreateAccumulator = getOrCreateAccumulator2;
2010
+ if (signal?.aborted) {
2011
+ onEvent({ type: "turn_cancelled" });
2012
+ saveSession(state);
2013
+ return;
2014
+ }
2015
+ let assistantText = "";
2016
+ const toolCalls = [];
2017
+ const toolInputAccumulators = /* @__PURE__ */ new Map();
2018
+ let stopReason = "end_turn";
2019
+ async function handlePartialInput(acc, id, name, partial) {
2020
+ const tool = getToolByName(name);
2021
+ if (!tool?.streaming) {
2022
+ return;
2023
+ }
2024
+ const {
2025
+ contentField = "content",
2026
+ transform,
2027
+ partialInput
2028
+ } = tool.streaming;
2029
+ if (partialInput) {
2030
+ const result = partialInput(partial, acc.lastEmittedCount);
2031
+ if (!result) {
2032
+ return;
2033
+ }
2034
+ acc.lastEmittedCount = result.emittedCount;
2035
+ acc.started = true;
2036
+ log.debug("Streaming partial tool_start", {
2037
+ id,
2038
+ name,
2039
+ emittedCount: result.emittedCount
2040
+ });
2041
+ onEvent({
2042
+ type: "tool_start",
2043
+ id,
2044
+ name,
2045
+ input: result.input,
2046
+ partial: true
2047
+ });
2048
+ return;
2049
+ }
2050
+ const content = partial[contentField];
2051
+ if (typeof content !== "string") {
2052
+ return;
2053
+ }
2054
+ if (!acc.started) {
2055
+ acc.started = true;
2056
+ log.debug("Streaming content tool: emitting early tool_start", {
2057
+ id,
2058
+ name
2059
+ });
2060
+ onEvent({ type: "tool_start", id, name, input: partial });
2061
+ }
2062
+ if (transform) {
2063
+ const result = await transform(partial);
2064
+ log.debug("Streaming content tool: emitting tool_input_delta", {
2065
+ id,
2066
+ name,
2067
+ resultLength: result.length
2068
+ });
2069
+ onEvent({ type: "tool_input_delta", id, name, result });
2070
+ } else {
2071
+ log.debug("Streaming content tool: emitting tool_input_delta", {
2072
+ id,
2073
+ name,
2074
+ contentLength: content.length
2075
+ });
2076
+ onEvent({ type: "tool_input_delta", id, name, result: content });
2077
+ }
2078
+ }
2079
+ try {
2080
+ for await (const event of streamChat({
2081
+ ...apiConfig,
2082
+ model,
2083
+ system,
2084
+ messages: state.messages,
2085
+ tools,
2086
+ signal
2087
+ })) {
2088
+ if (signal?.aborted) {
2089
+ break;
2090
+ }
2091
+ switch (event.type) {
2092
+ case "text":
2093
+ assistantText += event.text;
2094
+ onEvent({ type: "text", text: event.text });
2095
+ break;
2096
+ case "thinking":
2097
+ onEvent({ type: "thinking", text: event.text });
2098
+ break;
2099
+ case "tool_input_delta": {
2100
+ const acc = getOrCreateAccumulator2(event.id, event.name);
2101
+ acc.json += event.delta;
2102
+ log.debug("Received tool_input_delta", {
2103
+ id: event.id,
2104
+ name: event.name,
2105
+ deltaLength: event.delta.length,
2106
+ accumulatedLength: acc.json.length
2107
+ });
2108
+ try {
2109
+ const partial = parsePartialJson(acc.json);
2110
+ await handlePartialInput(acc, event.id, event.name, partial);
2111
+ } catch {
2112
+ }
2113
+ break;
2114
+ }
2115
+ case "tool_input_args": {
2116
+ const acc = getOrCreateAccumulator2(event.id, event.name);
2117
+ log.debug("Received tool_input_args", {
2118
+ id: event.id,
2119
+ name: event.name,
2120
+ keys: Object.keys(event.args)
2121
+ });
2122
+ await handlePartialInput(acc, event.id, event.name, event.args);
2123
+ break;
2124
+ }
2125
+ case "tool_use": {
2126
+ toolCalls.push({
2127
+ id: event.id,
2128
+ name: event.name,
2129
+ input: event.input
2130
+ });
2131
+ const acc = toolInputAccumulators.get(event.id);
2132
+ const tool = getToolByName(event.name);
2133
+ const wasStreamed = acc?.started ?? false;
2134
+ const isInputStreaming = !!tool?.streaming?.partialInput;
2135
+ log.debug("Received tool_use", {
2136
+ id: event.id,
2137
+ name: event.name,
2138
+ wasStreamed,
2139
+ isInputStreaming
2140
+ });
2141
+ if (!wasStreamed || isInputStreaming) {
2142
+ onEvent({
2143
+ type: "tool_start",
2144
+ id: event.id,
2145
+ name: event.name,
2146
+ input: event.input
2147
+ });
2148
+ }
2149
+ break;
2150
+ }
2151
+ case "done":
2152
+ stopReason = event.stopReason;
2153
+ break;
2154
+ case "error":
2155
+ onEvent({ type: "error", error: event.error });
2156
+ return;
2157
+ }
2158
+ }
2159
+ } catch (err) {
2160
+ if (signal?.aborted) {
2161
+ } else {
2162
+ throw err;
2163
+ }
2164
+ }
2165
+ if (signal?.aborted) {
2166
+ if (assistantText) {
2167
+ state.messages.push({
2168
+ role: "assistant",
2169
+ content: assistantText + "\n\n(cancelled)",
2170
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0
2171
+ });
2172
+ }
2173
+ onEvent({ type: "turn_cancelled" });
2174
+ saveSession(state);
2175
+ return;
2176
+ }
2177
+ state.messages.push({
2178
+ role: "assistant",
2179
+ content: assistantText,
2180
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0
2181
+ });
2182
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
2183
+ saveSession(state);
2184
+ onEvent({ type: "turn_done" });
2185
+ return;
2186
+ }
2187
+ log.info("Executing tools", {
2188
+ count: toolCalls.length,
2189
+ tools: toolCalls.map((tc) => tc.name)
2190
+ });
2191
+ const results = await Promise.all(
2192
+ toolCalls.map(async (tc) => {
2193
+ if (signal?.aborted) {
2194
+ return {
2195
+ id: tc.id,
2196
+ result: "Error: cancelled",
2197
+ isError: true
2198
+ };
2199
+ }
2200
+ const toolStart = Date.now();
2201
+ try {
2202
+ let result;
2203
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
2204
+ saveSession(state);
2205
+ log.debug("Waiting for external tool result", {
2206
+ name: tc.name,
2207
+ id: tc.id
2208
+ });
2209
+ result = await resolveExternalTool(tc.id, tc.name, tc.input);
2210
+ } else {
2211
+ result = await executeTool(tc.name, tc.input);
2212
+ }
2213
+ const isError = result.startsWith("Error");
2214
+ log.debug("Tool completed", {
2215
+ name: tc.name,
2216
+ elapsed: `${Date.now() - toolStart}ms`,
2217
+ isError,
2218
+ resultLength: result.length
2219
+ });
2220
+ onEvent({
2221
+ type: "tool_done",
2222
+ id: tc.id,
2223
+ name: tc.name,
2224
+ result,
2225
+ isError
2226
+ });
2227
+ return { id: tc.id, result, isError };
2228
+ } catch (err) {
2229
+ const errorMsg = `Error: ${err.message}`;
2230
+ onEvent({
2231
+ type: "tool_done",
2232
+ id: tc.id,
2233
+ name: tc.name,
2234
+ result: errorMsg,
2235
+ isError: true
2236
+ });
2237
+ return { id: tc.id, result: errorMsg, isError: true };
2238
+ }
2239
+ })
2240
+ );
2241
+ for (const r of results) {
2242
+ state.messages.push({
2243
+ role: "user",
2244
+ content: r.result,
2245
+ toolCallId: r.id,
2246
+ isToolError: r.isError
2247
+ });
2248
+ }
2249
+ if (signal?.aborted) {
2250
+ onEvent({ type: "turn_cancelled" });
2251
+ saveSession(state);
2252
+ return;
2253
+ }
2254
+ }
2255
+ }
2256
+ var EXTERNAL_TOOLS;
2257
+ var init_agent = __esm({
2258
+ "src/agent.ts"() {
2259
+ "use strict";
2260
+ init_api();
2261
+ init_tools();
2262
+ init_session();
2263
+ init_logger();
2264
+ init_parsePartialJson();
2265
+ EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
2266
+ "promptUser",
2267
+ "setViewMode",
2268
+ "clearSyncStatus",
2269
+ "presentSyncPlan",
2270
+ "presentPublishPlan",
2271
+ "presentPlan"
2272
+ ]);
2273
+ }
2274
+ });
2275
+
2276
+ // src/prompt/static/projectContext.ts
2277
+ import fs11 from "fs";
2278
+ import path4 from "path";
2279
+ function loadProjectInstructions() {
2280
+ for (const file of AGENT_INSTRUCTION_FILES) {
2281
+ try {
2282
+ const content = fs11.readFileSync(file, "utf-8").trim();
2283
+ if (content) {
2284
+ return `
2285
+ ## Project Instructions (${file})
2286
+ ${content}`;
2287
+ }
2288
+ } catch {
2289
+ }
2290
+ }
2291
+ return "";
2292
+ }
2293
+ function loadProjectManifest() {
2294
+ try {
2295
+ const manifest = fs11.readFileSync("mindstudio.json", "utf-8");
2296
+ return `
2297
+ ## Project Manifest (mindstudio.json)
2298
+ \`\`\`json
2299
+ ${manifest}
2300
+ \`\`\``;
2301
+ } catch {
2302
+ return "";
2303
+ }
2304
+ }
2305
+ function loadSpecFileMetadata() {
2306
+ try {
2307
+ const files = walkMdFiles("src");
2308
+ if (files.length === 0) {
2309
+ return "";
2310
+ }
2311
+ const entries = [];
2312
+ for (const filePath of files) {
2313
+ const { name, description } = parseFrontmatter(filePath);
2314
+ let line = `- ${filePath}`;
2315
+ if (name) {
2316
+ line += ` \u2014 "${name}"`;
2317
+ }
2318
+ if (description) {
2319
+ line += ` \u2014 ${description}`;
2320
+ }
2321
+ entries.push(line);
2322
+ }
2323
+ return `
2324
+ ## Spec Files
2325
+ ${entries.join("\n")}`;
2326
+ } catch {
2327
+ return "";
2328
+ }
2329
+ }
2330
+ function walkMdFiles(dir) {
2331
+ const results = [];
2332
+ try {
2333
+ const entries = fs11.readdirSync(dir, { withFileTypes: true });
2334
+ for (const entry of entries) {
2335
+ const full = path4.join(dir, entry.name);
2336
+ if (entry.isDirectory()) {
2337
+ results.push(...walkMdFiles(full));
2338
+ } else if (entry.name.endsWith(".md")) {
2339
+ results.push(full);
2340
+ }
2341
+ }
2342
+ } catch {
2343
+ }
2344
+ return results.sort();
2345
+ }
2346
+ function parseFrontmatter(filePath) {
2347
+ try {
2348
+ const content = fs11.readFileSync(filePath, "utf-8");
2349
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
2350
+ if (!match) {
2351
+ return { name: "", description: "" };
2352
+ }
2353
+ const fm = match[1];
2354
+ const name = fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "";
2355
+ const description = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
2356
+ return { name, description };
2357
+ } catch {
2358
+ return { name: "", description: "" };
2359
+ }
2360
+ }
2361
+ function loadProjectFileListing() {
2362
+ try {
2363
+ const entries = fs11.readdirSync(".", { withFileTypes: true });
2364
+ const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
2365
+ if (a.isDirectory() && !b.isDirectory()) {
2366
+ return -1;
2367
+ }
2368
+ if (!a.isDirectory() && b.isDirectory()) {
2369
+ return 1;
2370
+ }
2371
+ return a.name.localeCompare(b.name);
2372
+ }).map((e) => e.isDirectory() ? `${e.name}/` : e.name).join("\n");
2373
+ return `
2374
+ ## Project Files
2375
+ \`\`\`
2376
+ ${listing}
2377
+ \`\`\``;
2378
+ } catch {
2379
+ return "";
2380
+ }
2381
+ }
2382
+ var AGENT_INSTRUCTION_FILES;
2383
+ var init_projectContext = __esm({
2384
+ "src/prompt/static/projectContext.ts"() {
2385
+ "use strict";
2386
+ AGENT_INSTRUCTION_FILES = [
2387
+ "CLAUDE.md",
2388
+ "claude.md",
2389
+ ".claude/instructions.md",
2390
+ "AGENTS.md",
2391
+ "agents.md",
2392
+ ".agents.md",
2393
+ "COPILOT.md",
2394
+ "copilot.md",
2395
+ ".copilot-instructions.md",
2396
+ ".github/copilot-instructions.md",
2397
+ "REMY.md",
2398
+ "remy.md",
2399
+ ".cursorrules",
2400
+ ".cursorules"
2401
+ ];
2402
+ }
2403
+ });
2404
+
2405
+ // src/prompt/index.ts
2406
+ import fs12 from "fs";
2407
+ import path5 from "path";
2408
+ function requireFile(filePath) {
2409
+ const full = path5.join(PROMPT_DIR, filePath);
2410
+ try {
2411
+ return fs12.readFileSync(full, "utf-8").trim();
2412
+ } catch {
2413
+ throw new Error(`Required prompt file missing: ${full}`);
2414
+ }
2415
+ }
2416
+ function resolveIncludes(template) {
2417
+ const result = template.replace(
2418
+ /\{\{([^}]+)\}\}/g,
2419
+ (_, filePath) => requireFile(filePath.trim())
2420
+ );
2421
+ return result.replace(/\n{3,}/g, "\n\n").trim();
2422
+ }
2423
+ function buildSystemPrompt(projectHasCode, viewContext) {
2424
+ const projectContext = [
2425
+ loadProjectInstructions(),
2426
+ loadProjectManifest(),
2427
+ loadSpecFileMetadata(),
2428
+ loadProjectFileListing()
2429
+ ].filter(Boolean).join("\n");
2430
+ const now = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
2431
+ dateStyle: "full",
2432
+ timeStyle: "long"
2433
+ });
2434
+ const template = `
2435
+ {{static/identity.md}}
2436
+
2437
+ The current date is ${now}.
2438
+
2439
+ <platform_docs>
2440
+ <platform>
2441
+ {{compiled/platform.md}}
2442
+ </platform>
2443
+
2444
+ <manifest>
2445
+ {{compiled/manifest.md}}
2446
+ </manifest>
2447
+
2448
+ <tables>
2449
+ {{compiled/tables.md}}
2450
+ </tables>
2451
+
2452
+ <methods>
2453
+ {{compiled/methods.md}}
2454
+ </methods>
2455
+
2456
+ <auth>
2457
+ {{compiled/auth.md}}
2458
+ </auth>
2459
+
2460
+ <dev_and_deploy>
2461
+ {{compiled/dev-and-deploy.md}}
2462
+ </dev_and_deploy>
2463
+
2464
+ <design>
2465
+ {{compiled/design.md}}
2466
+ </design>
2467
+
2468
+ <media_cdn>
2469
+ {{compiled/media-cdn.md}}
2470
+ </media_cdn>
2471
+
2472
+ <interfaces>
2473
+ {{compiled/interfaces.md}}
2474
+ </interfaces>
2475
+
2476
+ <scenarios>
2477
+ {{compiled/scenarios.md}}
2478
+ </scenarios>
2479
+ </platform_docs>
2480
+
2481
+ <mindstudio_agent_sdk_docs>
2482
+ {{compiled/sdk-actions.md}}
2483
+ </mindstudio_agent_sdk_docs>
2484
+
2485
+ <mindstudio_flavored_markdown_spec_docs>
2486
+ {{compiled/msfm.md}}
2487
+ </mindstudio_flavored_markdown_spec_docs>
2488
+
2489
+ <project_context>
2490
+ ${projectContext}
2491
+ </project_context>
2492
+
2493
+ ${isLspConfigured() ? `<lsp>
2494
+ {{static/lsp.md}}
2495
+ </lsp>` : ""}
2496
+
2497
+ {{static/intake.md}}
2498
+
2499
+ {{static/authoring.md}}
2500
+
2501
+ {{static/instructions.md}}
2502
+
2503
+ <current_authoring_mode>
2504
+ ${projectHasCode ? "Project has code - keep code and spec in sync." : "Project does not have code yet - focus on writing the spec."}
2505
+ </current_authoring_mode>
2506
+
2507
+ <view_context>
2508
+ The user is currently in ${viewContext?.mode ?? "code"} mode.
2509
+ ${viewContext?.activeFile ? `Active file: ${viewContext.activeFile}` : ""}
2510
+ </view_context>
2511
+ `;
2512
+ return resolveIncludes(template);
2513
+ }
2514
+ var PROMPT_DIR;
2515
+ var init_prompt = __esm({
2516
+ "src/prompt/index.ts"() {
2517
+ "use strict";
2518
+ init_lsp();
2519
+ init_projectContext();
2520
+ PROMPT_DIR = import.meta.dirname ?? path5.dirname(new URL(import.meta.url).pathname);
2521
+ }
2522
+ });
2523
+
2524
+ // src/config.ts
2525
+ import fs13 from "fs";
2526
+ import path6 from "path";
2527
+ import os from "os";
2528
+ function loadConfigFile() {
2529
+ try {
2530
+ const raw = fs13.readFileSync(CONFIG_PATH, "utf-8");
2531
+ log.debug("Loaded config file", { path: CONFIG_PATH });
2532
+ return JSON.parse(raw);
2533
+ } catch (err) {
2534
+ log.debug("No config file found", {
2535
+ path: CONFIG_PATH,
2536
+ error: err.message
2537
+ });
2538
+ return {};
2539
+ }
2540
+ }
2541
+ function resolveConfig(flags2) {
2542
+ const file = loadConfigFile();
2543
+ const activeEnv = file.environment || "prod";
2544
+ const env = file.environments?.[activeEnv];
2545
+ const apiKey = flags2?.apiKey || process.env.MINDSTUDIO_API_KEY || env?.apiKey || "";
2546
+ const baseUrl = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
2547
+ if (!apiKey) {
2548
+ log.error("No API key found");
2549
+ throw new Error(
2550
+ "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
2551
+ );
2552
+ }
2553
+ const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
2554
+ log.info("Config resolved", {
2555
+ baseUrl,
2556
+ keySource,
2557
+ environment: activeEnv
2558
+ });
2559
+ return { apiKey, baseUrl };
2560
+ }
2561
+ var CONFIG_PATH, DEFAULT_BASE_URL;
2562
+ var init_config = __esm({
2563
+ "src/config.ts"() {
2564
+ "use strict";
2565
+ init_logger();
2566
+ CONFIG_PATH = path6.join(
2567
+ os.homedir(),
2568
+ ".mindstudio-local-tunnel",
2569
+ "config.json"
2570
+ );
2571
+ DEFAULT_BASE_URL = "https://api.mindstudio.ai";
2572
+ }
2573
+ });
2574
+
2575
+ // src/headless.ts
2576
+ var headless_exports = {};
2577
+ __export(headless_exports, {
2578
+ startHeadless: () => startHeadless
2579
+ });
2580
+ import { createInterface } from "readline";
2581
+ import fs14 from "fs";
2582
+ import path7 from "path";
2583
+ function loadActionPrompt(name) {
2584
+ return fs14.readFileSync(path7.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
2585
+ }
2586
+ function emit(event, data) {
2587
+ process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
2588
+ }
2589
+ async function startHeadless(opts = {}) {
2590
+ const stderrWrite = (...args2) => {
2591
+ process.stderr.write(args2.map(String).join(" ") + "\n");
2592
+ };
2593
+ console.log = stderrWrite;
2594
+ console.warn = stderrWrite;
2595
+ console.info = stderrWrite;
2596
+ if (opts.lspUrl) {
2597
+ setLspBaseUrl(opts.lspUrl);
2598
+ }
2599
+ const config = resolveConfig({
2600
+ apiKey: opts.apiKey,
2601
+ baseUrl: opts.baseUrl
2602
+ });
2603
+ const state = createAgentState();
2604
+ const resumed = loadSession(state);
2605
+ if (resumed) {
2606
+ emit("session_restored", {
2607
+ messageCount: state.messages.length
2608
+ });
2609
+ }
2610
+ let running = false;
2611
+ let currentAbort = null;
2612
+ const externalToolPromises = /* @__PURE__ */ new Map();
2613
+ function onEvent(e) {
2614
+ switch (e.type) {
2615
+ case "text":
2616
+ emit("text", { text: e.text });
2617
+ break;
2618
+ case "thinking":
2619
+ emit("thinking", { text: e.text });
2620
+ break;
2621
+ case "tool_input_delta":
2622
+ emit("tool_input_delta", { id: e.id, name: e.name, result: e.result });
2623
+ break;
2624
+ case "tool_start": {
2625
+ emit("tool_start", {
2626
+ id: e.id,
2627
+ name: e.name,
2628
+ input: e.input,
2629
+ ...e.partial && { partial: true }
2630
+ });
2631
+ if (!e.partial && !externalToolPromises.has(e.id)) {
2632
+ let resolve;
2633
+ const promise = new Promise((r) => {
2634
+ resolve = r;
2635
+ });
2636
+ externalToolPromises.set(e.id, { promise, resolve });
2637
+ }
2638
+ break;
2639
+ }
2640
+ case "tool_done":
2641
+ emit("tool_done", {
2642
+ id: e.id,
2643
+ name: e.name,
2644
+ result: e.result,
2645
+ isError: e.isError
2646
+ });
2647
+ break;
2648
+ case "turn_started":
2649
+ emit("turn_started");
2650
+ break;
2651
+ case "turn_done":
2652
+ emit("turn_done");
2653
+ break;
2654
+ case "turn_cancelled":
2655
+ emit("turn_cancelled");
2656
+ break;
2657
+ case "error":
2658
+ emit("error", { error: e.error });
2659
+ break;
2660
+ }
2661
+ }
2662
+ function resolveExternalTool(id, _name, _input) {
2663
+ const entry = externalToolPromises.get(id);
2664
+ if (entry) {
2665
+ return entry.promise;
2666
+ }
2667
+ let resolve;
2668
+ const promise = new Promise((r) => {
2669
+ resolve = r;
2670
+ });
2671
+ externalToolPromises.set(id, { promise, resolve });
2672
+ return promise;
2673
+ }
2674
+ const rl = createInterface({ input: process.stdin });
2675
+ rl.on("line", async (line) => {
2676
+ let parsed;
2677
+ try {
2678
+ parsed = JSON.parse(line);
2679
+ } catch {
2680
+ emit("error", { error: "Invalid JSON on stdin" });
2681
+ return;
2682
+ }
2683
+ if (parsed.action === "tool_result" && parsed.id) {
2684
+ const entry = externalToolPromises.get(parsed.id);
2685
+ if (entry) {
2686
+ externalToolPromises.delete(parsed.id);
2687
+ entry.resolve(parsed.result ?? "");
2688
+ }
2689
+ return;
2690
+ }
2691
+ if (parsed.action === "get_history") {
2692
+ emit("history", {
2693
+ messages: state.messages
2694
+ });
2695
+ return;
2696
+ }
2697
+ if (parsed.action === "clear") {
2698
+ clearSession(state);
2699
+ emit("session_cleared");
2700
+ return;
2701
+ }
2702
+ if (parsed.action === "cancel") {
2703
+ if (currentAbort) {
2704
+ currentAbort.abort();
2705
+ }
2706
+ for (const [id, entry] of externalToolPromises) {
2707
+ entry.resolve("Error: cancelled");
2708
+ externalToolPromises.delete(id);
2709
+ }
2710
+ return;
2711
+ }
2712
+ if (parsed.action === "message" && (parsed.text || parsed.runCommand)) {
2713
+ if (running) {
2714
+ emit("error", { error: "Agent is already processing a message" });
2715
+ return;
2716
+ }
2717
+ running = true;
2718
+ currentAbort = new AbortController();
2719
+ if (parsed.attachments?.length) {
2720
+ console.warn(
2721
+ `[headless] Message has ${parsed.attachments.length} attachment(s):`,
2722
+ parsed.attachments.map((a) => a.url)
2723
+ );
2724
+ }
2725
+ let userMessage = parsed.text ?? "";
2726
+ const isCommand = !!parsed.runCommand;
2727
+ if (parsed.runCommand === "sync") {
2728
+ userMessage = loadActionPrompt("sync");
2729
+ } else if (parsed.runCommand === "publish") {
2730
+ userMessage = loadActionPrompt("publish");
2731
+ }
2732
+ const projectHasCode = parsed.projectHasCode ?? true;
2733
+ const system = buildSystemPrompt(projectHasCode, parsed.viewContext);
2734
+ try {
2735
+ await runTurn({
2736
+ state,
2737
+ userMessage,
2738
+ attachments: parsed.attachments,
2739
+ apiConfig: config,
2740
+ system,
2741
+ model: opts.model,
2742
+ projectHasCode,
2743
+ signal: currentAbort.signal,
2744
+ onEvent,
2745
+ resolveExternalTool,
2746
+ hidden: isCommand
2747
+ });
2748
+ } catch (err) {
2749
+ emit("error", { error: err.message });
2750
+ }
2751
+ currentAbort = null;
2752
+ running = false;
2753
+ }
2754
+ });
2755
+ rl.on("close", () => {
2756
+ emit("stopping");
2757
+ emit("stopped");
2758
+ process.exit(0);
2759
+ });
2760
+ function shutdown() {
2761
+ emit("stopping");
2762
+ emit("stopped");
2763
+ process.exit(0);
2764
+ }
2765
+ process.on("SIGTERM", shutdown);
2766
+ process.on("SIGINT", shutdown);
2767
+ emit("ready");
2768
+ }
2769
+ var BASE_DIR, ACTIONS_DIR;
2770
+ var init_headless = __esm({
2771
+ "src/headless.ts"() {
2772
+ "use strict";
2773
+ init_config();
2774
+ init_prompt();
2775
+ init_lsp();
2776
+ init_agent();
2777
+ init_session();
2778
+ BASE_DIR = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
2779
+ ACTIONS_DIR = path7.join(BASE_DIR, "actions");
2780
+ }
2781
+ });
2782
+
2783
+ // src/index.tsx
2784
+ import { render } from "ink";
2785
+ import os2 from "os";
2786
+ import fs15 from "fs";
2787
+ import path8 from "path";
2788
+
2789
+ // src/tui/App.tsx
2790
+ import { useState as useState2, useCallback, useRef } from "react";
2791
+ import { Box as Box4, Text as Text5, useApp, useInput } from "ink";
2792
+
2793
+ // src/tui/InputPrompt.tsx
2794
+ import { useState } from "react";
2795
+ import { Box, Text } from "ink";
2796
+ import TextInput from "ink-text-input";
2797
+ import { jsx, jsxs } from "react/jsx-runtime";
2798
+ function InputPrompt({ onSubmit, disabled }) {
2799
+ const [value, setValue] = useState("");
2800
+ if (disabled) {
2801
+ return null;
2802
+ }
2803
+ return /* @__PURE__ */ jsxs(Box, { children: [
2804
+ /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "> " }),
2805
+ /* @__PURE__ */ jsx(
2806
+ TextInput,
2807
+ {
2808
+ value,
2809
+ onChange: setValue,
2810
+ onSubmit: (v) => {
2811
+ if (v.trim()) {
2812
+ onSubmit(v.trim());
2813
+ setValue("");
2814
+ }
2815
+ }
2816
+ }
2817
+ )
2818
+ ] });
2819
+ }
2820
+
2821
+ // src/tui/MessageList.tsx
2822
+ import { Box as Box3, Text as Text4 } from "ink";
2823
+
2824
+ // src/tui/ToolCall.tsx
2825
+ import { Box as Box2, Text as Text2 } from "ink";
2826
+ import Spinner from "ink-spinner";
2827
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2828
+ function summarizeInput(name, input) {
2829
+ switch (name) {
2830
+ case "readFile":
2831
+ case "writeFile":
2832
+ case "editFile":
2833
+ return input.path || "";
2834
+ case "bash":
2835
+ return input.command || "";
2836
+ case "grep":
2837
+ return input.pattern || "";
2838
+ case "glob":
2839
+ return input.pattern || "";
2840
+ case "listDir":
2841
+ return input.path || ".";
2842
+ default:
2843
+ return JSON.stringify(input).slice(0, 60);
2844
+ }
2845
+ }
2846
+ function summarizeResult(name, result) {
2847
+ if (result.startsWith("Error")) {
2848
+ return result.split("\n")[0];
2849
+ }
2850
+ const lines = result.split("\n").length;
2851
+ switch (name) {
2852
+ case "readFile":
2853
+ return `${lines} lines`;
2854
+ case "writeFile":
2855
+ return result;
2856
+ case "editFile":
2857
+ return result;
2858
+ case "bash":
2859
+ return lines > 3 ? `${lines} lines of output` : result.trim();
2860
+ case "grep":
2861
+ return `${lines} matches`;
2862
+ case "glob":
2863
+ return `${lines} files`;
2864
+ case "listDir":
2865
+ return `${lines} entries`;
2866
+ default:
2867
+ return `${lines} lines`;
2868
+ }
2869
+ }
2870
+ function ToolCall({ name, input, status, result }) {
2871
+ const summary = summarizeInput(name, input);
2872
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
2873
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
2874
+ status === "running" ? /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: /* @__PURE__ */ jsx2(Spinner, { type: "dots" }) }) : status === "error" ? /* @__PURE__ */ jsx2(Text2, { color: "red", children: "\u2717" }) : /* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u27E1" }),
2875
+ /* @__PURE__ */ jsx2(Text2, { children: " " }),
2876
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: name }),
2877
+ summary ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2878
+ " ",
2879
+ summary
2880
+ ] }) : null,
2881
+ result && status !== "running" ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2882
+ " \u2192 ",
2883
+ summarizeResult(name, result)
2884
+ ] }) : null
2885
+ ] });
2886
+ }
2887
+
2888
+ // src/tui/ThinkingBlock.tsx
2889
+ import { Text as Text3 } from "ink";
2890
+ import { jsx as jsx3 } from "react/jsx-runtime";
2891
+ function ThinkingBlock({ text }) {
2892
+ if (!text) {
2893
+ return null;
2894
+ }
2895
+ return /* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: true, children: text });
2896
+ }
2897
+
2898
+ // src/tui/MessageList.tsx
2899
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2900
+ function MessageList({ turns }) {
2901
+ return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", gap: 1, children: turns.map((turn, i) => /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", children: turn.role === "user" ? /* @__PURE__ */ jsxs3(Text4, { children: [
2902
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "> " }),
2903
+ turn.text
2904
+ ] }) : /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
2905
+ turn.thinking ? /* @__PURE__ */ jsx4(ThinkingBlock, { text: turn.thinking }) : null,
2906
+ turn.toolCalls.map((tc) => /* @__PURE__ */ jsx4(
2907
+ ToolCall,
2908
+ {
2909
+ name: tc.name,
2910
+ input: tc.input,
2911
+ status: tc.status,
2912
+ result: tc.result
2913
+ },
2914
+ tc.id
2915
+ )),
2916
+ turn.text ? /* @__PURE__ */ jsx4(Box3, { marginTop: turn.toolCalls.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx4(Text4, { children: turn.text }) }) : null
2917
+ ] }) }, i)) });
2918
+ }
2919
+
2920
+ // src/tui/App.tsx
2921
+ init_agent();
2922
+ init_prompt();
2923
+ init_session();
2924
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2925
+ function App({ apiConfig, model }) {
2926
+ const { exit } = useApp();
2927
+ const [turns, setTurns] = useState2([]);
2928
+ const [isRunning, setIsRunning] = useState2(false);
2929
+ const abortRef = useRef(null);
2930
+ const [agentState] = useState2(() => {
2931
+ const s = createAgentState();
2932
+ loadSession(s);
2933
+ return s;
2934
+ });
2935
+ const [sessionRestored] = useState2(() => agentState.messages.length > 0);
2936
+ const [system] = useState2(() => buildSystemPrompt());
2937
+ useInput((input, key) => {
2938
+ if (key.escape && isRunning && abortRef.current) {
2939
+ abortRef.current.abort();
2940
+ }
2941
+ });
2942
+ const updateAssistant = useCallback(
2943
+ (updater) => {
2944
+ setTurns((prev) => {
2945
+ const copy = [...prev];
2946
+ const last = copy[copy.length - 1];
2947
+ if (last?.role === "assistant") {
2948
+ copy[copy.length - 1] = updater(last);
2949
+ }
2950
+ return copy;
2951
+ });
2952
+ },
2953
+ []
2954
+ );
2955
+ const handleSubmit = useCallback(
2956
+ async (message) => {
2957
+ if (message === "/clear") {
2958
+ clearSession(agentState);
2959
+ setTurns([]);
2960
+ return;
2961
+ }
2962
+ setTurns((prev) => [
2963
+ ...prev,
2964
+ {
2965
+ role: "user",
2966
+ text: message,
2967
+ thinking: "",
2968
+ toolCalls: [],
2969
+ done: true
2970
+ },
2971
+ {
2972
+ role: "assistant",
2973
+ text: "",
2974
+ thinking: "",
2975
+ toolCalls: [],
2976
+ done: false
2977
+ }
2978
+ ]);
2979
+ setIsRunning(true);
2980
+ const abort = new AbortController();
2981
+ abortRef.current = abort;
2982
+ try {
2983
+ await runTurn({
2984
+ state: agentState,
2985
+ userMessage: message,
2986
+ apiConfig,
2987
+ system,
2988
+ model,
2989
+ projectHasCode: true,
2990
+ signal: abort.signal,
2991
+ onEvent: (event) => {
2992
+ switch (event.type) {
2993
+ case "text":
2994
+ updateAssistant((t) => ({
2995
+ ...t,
2996
+ text: t.text + event.text
2997
+ }));
2998
+ break;
2999
+ case "thinking":
3000
+ updateAssistant((t) => ({
3001
+ ...t,
3002
+ thinking: t.thinking + event.text
3003
+ }));
3004
+ break;
3005
+ case "tool_start":
3006
+ updateAssistant((t) => ({
3007
+ ...t,
3008
+ toolCalls: [
3009
+ ...t.toolCalls,
3010
+ {
3011
+ id: event.id,
3012
+ name: event.name,
3013
+ input: event.input,
3014
+ status: "running"
3015
+ }
3016
+ ]
3017
+ }));
3018
+ break;
3019
+ case "tool_done":
3020
+ updateAssistant((t) => ({
3021
+ ...t,
3022
+ toolCalls: t.toolCalls.map(
3023
+ (tc) => tc.id === event.id ? {
3024
+ ...tc,
3025
+ status: event.isError ? "error" : "done",
3026
+ result: event.result
3027
+ } : tc
3028
+ )
3029
+ }));
3030
+ break;
3031
+ case "turn_done":
3032
+ updateAssistant((t) => ({ ...t, done: true }));
3033
+ break;
3034
+ case "turn_cancelled":
3035
+ updateAssistant((t) => ({
3036
+ ...t,
3037
+ text: t.text + "\n(cancelled)",
3038
+ done: true
3039
+ }));
3040
+ break;
3041
+ case "error":
3042
+ updateAssistant((t) => ({
3043
+ ...t,
3044
+ text: t.text + `
3045
+ Error: ${event.error}`,
3046
+ done: true
3047
+ }));
3048
+ break;
3049
+ }
3050
+ }
3051
+ });
3052
+ } catch (err) {
3053
+ updateAssistant((t) => ({
3054
+ ...t,
3055
+ text: t.text + `
3056
+ Error: ${err.message}`,
3057
+ done: true
3058
+ }));
3059
+ }
3060
+ abortRef.current = null;
3061
+ setIsRunning(false);
3062
+ },
3063
+ [agentState, apiConfig, system, model, updateAssistant]
3064
+ );
3065
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
3066
+ /* @__PURE__ */ jsxs4(Text5, { bold: true, color: "magenta", children: [
3067
+ "Remy ",
3068
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "v0.1.0 \u2014 MindStudio coding agent" })
3069
+ ] }),
3070
+ sessionRestored && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
3071
+ "Session restored (",
3072
+ agentState.messages.length,
3073
+ " messages). Delete .remy-session.json to start fresh."
3074
+ ] }),
3075
+ /* @__PURE__ */ jsx5(MessageList, { turns }),
3076
+ isRunning ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Press Escape to cancel" }) : /* @__PURE__ */ jsx5(InputPrompt, { onSubmit: handleSubmit, disabled: isRunning })
3077
+ ] });
3078
+ }
3079
+
3080
+ // src/index.tsx
3081
+ init_config();
3082
+ init_logger();
3083
+ import { jsx as jsx6 } from "react/jsx-runtime";
3084
+ var args = process.argv.slice(2);
3085
+ var flags = {};
3086
+ var headless = false;
3087
+ for (let i = 0; i < args.length; i++) {
3088
+ if (args[i] === "--api-key" && args[i + 1]) {
3089
+ flags.apiKey = args[++i];
3090
+ } else if (args[i] === "--base-url" && args[i + 1]) {
3091
+ flags.baseUrl = args[++i];
3092
+ } else if (args[i] === "--model" && args[i + 1]) {
3093
+ flags.model = args[++i];
3094
+ } else if (args[i] === "--lsp-url" && args[i + 1]) {
3095
+ flags.lspUrl = args[++i];
3096
+ } else if (args[i] === "--log-level" && args[i + 1]) {
3097
+ flags.logLevel = args[++i];
3098
+ } else if (args[i] === "--headless") {
3099
+ headless = true;
3100
+ }
3101
+ }
3102
+ function printDebugInfo(config) {
3103
+ const pkg = JSON.parse(
3104
+ fs15.readFileSync(
3105
+ path8.join(import.meta.dirname, "..", "package.json"),
3106
+ "utf-8"
3107
+ )
3108
+ );
3109
+ const keyPreview = config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : "(none)";
3110
+ console.log("");
3111
+ console.log("remy debug info");
3112
+ console.log("\u2500".repeat(40));
3113
+ console.log(` version: ${pkg.version}`);
3114
+ console.log(` node: ${process.version}`);
3115
+ console.log(` platform: ${os2.platform()} ${os2.arch()}`);
3116
+ console.log(` os: ${os2.type()} ${os2.release()}`);
3117
+ console.log(` cwd: ${process.cwd()}`);
3118
+ console.log(` bin: ${process.argv[1]}`);
3119
+ console.log(` model: ${flags.model || "(default)"}`);
3120
+ console.log(` base url: ${config.baseUrl}`);
3121
+ console.log(` api key: ${keyPreview}`);
3122
+ console.log(
3123
+ ` key source: ${flags.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file"}`
3124
+ );
3125
+ console.log("\u2500".repeat(40));
3126
+ console.log("");
3127
+ }
3128
+ var logLevel = flags.logLevel || void 0;
3129
+ if (headless) {
3130
+ initLoggerHeadless(logLevel);
3131
+ const { startHeadless: startHeadless2 } = await Promise.resolve().then(() => (init_headless(), headless_exports));
3132
+ startHeadless2({
3133
+ apiKey: flags.apiKey,
3134
+ baseUrl: flags.baseUrl,
3135
+ model: flags.model,
3136
+ lspUrl: flags.lspUrl
3137
+ }).catch((err) => {
3138
+ console.error(err.message);
3139
+ process.exit(1);
3140
+ });
3141
+ } else {
3142
+ initLoggerInteractive(logLevel);
3143
+ try {
3144
+ const config = resolveConfig({
3145
+ apiKey: flags.apiKey,
3146
+ baseUrl: flags.baseUrl
3147
+ });
3148
+ printDebugInfo(config);
3149
+ const { waitUntilExit } = render(
3150
+ /* @__PURE__ */ jsx6(
3151
+ App,
3152
+ {
3153
+ apiConfig: { baseUrl: config.baseUrl, apiKey: config.apiKey },
3154
+ model: flags.model
3155
+ }
3156
+ ),
3157
+ { exitOnCtrlC: true }
3158
+ );
3159
+ await waitUntilExit();
3160
+ } catch (err) {
3161
+ console.error(err.message);
3162
+ process.exit(1);
3163
+ }
3164
+ }