starmux 0.11.0 → 0.13.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.
Files changed (136) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +169 -97
  3. package/SECURITY.md +3 -3
  4. package/dist/src/access-control.js +23 -20
  5. package/dist/src/access-control.js.map +1 -1
  6. package/dist/src/access-dashboard.js +8 -2
  7. package/dist/src/access-dashboard.js.map +1 -1
  8. package/dist/src/cli/instances.js +87 -14
  9. package/dist/src/cli/instances.js.map +1 -1
  10. package/dist/src/cli/mac-mcp-gateway.js +4 -3
  11. package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
  12. package/dist/src/cli/starmux-status.js +122 -0
  13. package/dist/src/cli/starmux-status.js.map +1 -0
  14. package/dist/src/cli/starmux.js +222 -49
  15. package/dist/src/cli/starmux.js.map +1 -1
  16. package/dist/src/deny-dashboard.js +2 -2
  17. package/dist/src/deny-dashboard.js.map +1 -1
  18. package/dist/src/instance-runtime.js +11 -2
  19. package/dist/src/instance-runtime.js.map +1 -1
  20. package/dist/src/instance-store.js +3 -1
  21. package/dist/src/instance-store.js.map +1 -1
  22. package/dist/src/ops-dashboard.js +4 -4
  23. package/dist/src/ops-dashboard.js.map +1 -1
  24. package/dist/src/permissions-dashboard.js +2 -2
  25. package/dist/src/permissions-dashboard.js.map +1 -1
  26. package/dist/src/runtime-paths.js +3 -3
  27. package/dist/src/runtime-paths.js.map +1 -1
  28. package/dist/src/starmux-migration.js +1 -1
  29. package/dist/src/starmux-migration.js.map +1 -1
  30. package/dist/vendor/desktop-commander/LICENSE +21 -0
  31. package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
  32. package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
  33. package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
  34. package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
  35. package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
  36. package/dist/vendor/desktop-commander/dist/config.js +19 -0
  37. package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
  38. package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
  39. package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
  40. package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
  41. package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
  42. package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
  43. package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
  44. package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
  45. package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
  46. package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
  47. package/dist/vendor/desktop-commander/dist/index.js +88 -0
  48. package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
  49. package/dist/vendor/desktop-commander/dist/server.js +1414 -0
  50. package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
  51. package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
  52. package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
  53. package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
  54. package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
  55. package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
  56. package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
  57. package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
  58. package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
  59. package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
  60. package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
  61. package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
  62. package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
  63. package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
  64. package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
  65. package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
  66. package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
  67. package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
  68. package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
  69. package/dist/vendor/desktop-commander/dist/types.js +1 -0
  70. package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
  71. package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
  72. package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
  73. package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
  74. package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
  75. package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
  76. package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  77. package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
  78. package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
  79. package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
  80. package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
  81. package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
  82. package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
  83. package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
  84. package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
  85. package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
  86. package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
  87. package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
  88. package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
  89. package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
  90. package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
  91. package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
  92. package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
  93. package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
  94. package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
  95. package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
  96. package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
  97. package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
  98. package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
  99. package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
  100. package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
  101. package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
  102. package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
  103. package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
  104. package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
  105. package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
  106. package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
  107. package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
  108. package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
  109. package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
  110. package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
  111. package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
  112. package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
  113. package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
  114. package/dist/vendor/desktop-commander/dist/version.js +1 -0
  115. package/dist/vendor/desktop-commander/package.json +15 -0
  116. package/docs/ACCESS_CONTROL.md +5 -5
  117. package/docs/CLIENTS.md +11 -19
  118. package/docs/CONNECT_CHATGPT.md +4 -6
  119. package/docs/INSTALL.md +9 -5
  120. package/docs/INSTANCES.md +5 -2
  121. package/docs/OAUTH.md +6 -14
  122. package/docs/PLATFORMS.md +4 -4
  123. package/docs/README.md +2 -11
  124. package/docs/REMOTE_SETUP.md +13 -13
  125. package/docs/SECURITY.md +5 -5
  126. package/docs/assets/starmux-architecture.svg +315 -0
  127. package/package.json +14 -2
  128. package/docs/ARCHITECTURE.md +0 -51
  129. package/docs/MIGRATION.md +0 -37
  130. package/docs/RELEASE_STATUS.md +0 -31
  131. package/docs/TYPESCRIPT_MIGRATION.md +0 -56
  132. package/docs/VALIDATION_OAUTH.md +0 -79
  133. package/docs/VALIDATION_TUNNEL.md +0 -39
  134. package/docs/VALIDATION_WORKBUDDY.md +0 -59
  135. package/docs/assets/mcp-connection-route.svg +0 -104
  136. package/docs/assets/mcp-gateway-simple.svg +0 -62
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Image file handler
3
+ * Handles reading image files and converting to base64
4
+ */
5
+ import fs from "fs/promises";
6
+ const SVG_MIME_TYPE = 'image/svg+xml';
7
+ /**
8
+ * Whether content of this MIME type is answered as an image: every image type
9
+ * but SVG, which is text (XML), read and written as text. Only the file preview
10
+ * widget draws an SVG as an image (svgAsImage). Files and URLs both decide here.
11
+ */
12
+ export function isImageAnswer(mimeType, svgAsImage = false) {
13
+ const type = mimeType.toLowerCase().split(';')[0].trim();
14
+ return type.startsWith('image/') && (svgAsImage || type !== SVG_MIME_TYPE);
15
+ }
16
+ /**
17
+ * Image file handler implementation
18
+ * Supports: PNG, JPEG, GIF, WebP, BMP; an SVG only for the file preview widget
19
+ * (isImageAnswer), everyone else reads and writes it as text.
20
+ */
21
+ export class ImageFileHandler {
22
+ canHandle(path, options) {
23
+ return isImageAnswer(this.getMimeType(path), options?.svgAsImage);
24
+ }
25
+ async read(path, options) {
26
+ // Images are always read in full, ignoring offset and length
27
+ const buffer = await fs.readFile(path, { signal: options?.signal });
28
+ const content = buffer.toString('base64');
29
+ const mimeType = this.getMimeType(path);
30
+ return {
31
+ content,
32
+ mimeType,
33
+ metadata: {
34
+ isImage: true
35
+ }
36
+ };
37
+ }
38
+ async write(path, content, mode) {
39
+ // An image can't take text at its end: writing the content would replace the file
40
+ if (mode === 'append') {
41
+ throw new Error('Image append not supported.');
42
+ }
43
+ // If content is base64 string, convert to buffer
44
+ if (typeof content === 'string') {
45
+ const buffer = Buffer.from(content, 'base64');
46
+ await fs.writeFile(path, buffer);
47
+ }
48
+ else {
49
+ await fs.writeFile(path, content);
50
+ }
51
+ }
52
+ async getInfo(path) {
53
+ const stats = await fs.stat(path);
54
+ return {
55
+ size: stats.size,
56
+ created: stats.birthtime,
57
+ modified: stats.mtime,
58
+ accessed: stats.atime,
59
+ isDirectory: stats.isDirectory(),
60
+ isFile: stats.isFile(),
61
+ permissions: stats.mode.toString(8).slice(-3),
62
+ fileType: 'image',
63
+ metadata: {
64
+ isImage: true
65
+ }
66
+ };
67
+ }
68
+ /**
69
+ * Get MIME type for image based on file extension
70
+ */
71
+ getMimeType(path) {
72
+ const lowerPath = path.toLowerCase();
73
+ for (const [ext, mimeType] of Object.entries(ImageFileHandler.IMAGE_MIME_TYPES)) {
74
+ if (lowerPath.endsWith(ext)) {
75
+ return mimeType;
76
+ }
77
+ }
78
+ return 'application/octet-stream'; // Fallback
79
+ }
80
+ }
81
+ ImageFileHandler.IMAGE_MIME_TYPES = {
82
+ '.png': 'image/png',
83
+ '.jpg': 'image/jpeg',
84
+ '.jpeg': 'image/jpeg',
85
+ '.gif': 'image/gif',
86
+ '.webp': 'image/webp',
87
+ '.bmp': 'image/bmp',
88
+ '.svg': 'image/svg+xml'
89
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * File handling system
3
+ * Exports all file handlers, interfaces, and utilities
4
+ */
5
+ // Base interfaces and types
6
+ export * from './base.js';
7
+ // Factory function
8
+ export { getFileHandler, isExcelFile, isImageFile, preloadFileSupport, stillLoadingError } from './factory.js';
9
+ // File handlers
10
+ export { TextFileHandler } from './text.js';
11
+ export { ImageFileHandler, isImageAnswer } from './image.js';
12
+ export { BinaryFileHandler } from './binary.js';
13
+ export { ExcelFileHandler, exceljsPackage } from './excel.js';
14
+ export { pizzipPackage } from './docx.js';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * PDF File Handler
3
+ * Implements FileHandler interface for PDF documents
4
+ */
5
+ import fs from 'fs/promises';
6
+ import { parsePdfToMarkdown, parseMarkdownToPdf, editPdf } from '../../tools/pdf/index.js';
7
+ /**
8
+ * File handler for PDF documents
9
+ * Extracts text and images, supports page-based pagination
10
+ */
11
+ export class PdfFileHandler {
12
+ constructor() {
13
+ this.extensions = ['.pdf'];
14
+ }
15
+ /**
16
+ * Check if this handler can handle the given file
17
+ */
18
+ canHandle(path) {
19
+ const ext = path.toLowerCase();
20
+ return this.extensions.some(e => ext.endsWith(e));
21
+ }
22
+ /**
23
+ * Read PDF content - extracts text as markdown with images
24
+ */
25
+ async read(path, options) {
26
+ const { offset = 0, length } = options ?? {};
27
+ try {
28
+ // Use existing PDF parser
29
+ // With no length, read from offset to the last page (all pages from offset 0)
30
+ // Note: offset defaults to 0 if undefined.
31
+ const pdfResult = await parsePdfToMarkdown(path, { offset, length: length ?? Infinity });
32
+ return {
33
+ content: '', // Main content is in metadata.pages
34
+ mimeType: 'application/pdf',
35
+ metadata: {
36
+ isPdf: true,
37
+ author: pdfResult.metadata.author,
38
+ title: pdfResult.metadata.title,
39
+ totalPages: pdfResult.metadata.totalPages,
40
+ pages: pdfResult.pages
41
+ }
42
+ };
43
+ }
44
+ catch (error) {
45
+ const errorMessage = error instanceof Error ? error.message : String(error);
46
+ return {
47
+ content: `Error reading PDF: ${errorMessage}`,
48
+ mimeType: 'text/plain',
49
+ metadata: {
50
+ error: true,
51
+ errorMessage
52
+ }
53
+ };
54
+ }
55
+ }
56
+ /**
57
+ * Write PDF - creates from markdown or operations
58
+ */
59
+ async write(path, content, mode) {
60
+ // A PDF can't take text at its end: rendering the content would replace the file
61
+ if (mode === 'append') {
62
+ throw new Error('PDF append not supported. Use write_pdf to modify existing PDF files.');
63
+ }
64
+ // If content is string, treat as markdown to convert
65
+ if (typeof content === 'string') {
66
+ const pdfBuffer = await parseMarkdownToPdf(content);
67
+ await fs.writeFile(path, pdfBuffer);
68
+ }
69
+ else if (Array.isArray(content)) {
70
+ // Array of operations - use editPdf
71
+ const resultBuffer = await editPdf(path, content);
72
+ await fs.writeFile(path, resultBuffer);
73
+ }
74
+ else {
75
+ throw new Error('PDF write requires markdown string or array of operations');
76
+ }
77
+ }
78
+ /**
79
+ * Edit PDF by range/operations
80
+ */
81
+ async editRange(path, range, content, options) {
82
+ // For PDF, range editing isn't directly supported
83
+ // Could interpret range as page numbers in future
84
+ // The output path and inserted PDFs are checked as write_pdf checks them.
85
+ // Errors reach edit_block (as the Excel handler's do), which answers with them.
86
+ // Imported when used: tools/filesystem.js loads this handler through the file handler factory
87
+ const { validatePdfOperationPaths } = await import('../../tools/filesystem.js');
88
+ const targetPath = await validatePdfOperationPaths(path, content, options?.outputPath);
89
+ const resultBuffer = await editPdf(path, content);
90
+ await fs.writeFile(targetPath, resultBuffer);
91
+ return { success: true, editsApplied: 1 };
92
+ }
93
+ /**
94
+ * Get PDF file information
95
+ */
96
+ async getInfo(path) {
97
+ const stats = await fs.stat(path);
98
+ // Get basic PDF metadata
99
+ let metadata = { isPdf: true };
100
+ try {
101
+ const pdfResult = await parsePdfToMarkdown(path, { offset: 0, length: 0 }); // Just metadata
102
+ metadata = {
103
+ isPdf: true,
104
+ title: pdfResult.metadata.title,
105
+ author: pdfResult.metadata.author,
106
+ totalPages: pdfResult.metadata.totalPages
107
+ };
108
+ }
109
+ catch {
110
+ // If we can't parse, just return basic info
111
+ }
112
+ return {
113
+ size: stats.size,
114
+ created: stats.birthtime,
115
+ modified: stats.mtime,
116
+ accessed: stats.atime,
117
+ isDirectory: false,
118
+ isFile: true,
119
+ permissions: (stats.mode & 0o777).toString(8),
120
+ fileType: 'binary',
121
+ metadata
122
+ };
123
+ }
124
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Text file handler
3
+ * Handles reading, writing, and editing text files
4
+ *
5
+ * Binary detection is handled at the factory level (factory.ts) using isBinaryFile.
6
+ * This handler only receives files that have been confirmed as text.
7
+ *
8
+ * TECHNICAL DEBT:
9
+ * This handler is missing editRange() - text search/replace logic currently lives in
10
+ * src/tools/edit.ts (performSearchReplace function) instead of here.
11
+ *
12
+ * For architectural consistency with ExcelFileHandler.editRange(), the fuzzy
13
+ * search/replace logic should be moved here. See comment in src/tools/edit.ts.
14
+ */
15
+ import fs from "fs/promises";
16
+ import { once } from 'events';
17
+ import { createReadStream } from 'fs';
18
+ import { createInterface } from 'readline';
19
+ // TODO: Centralize these constants with filesystem.ts to avoid silent drift
20
+ // These duplicate concepts from filesystem.ts and should be moved to a shared
21
+ // constants module (e.g., src/utils/files/constants.ts) during reorganization
22
+ const FILE_SIZE_LIMITS = {
23
+ LARGE_FILE_THRESHOLD: 10 * 1024 * 1024, // 10MB
24
+ LINE_COUNT_LIMIT: 10 * 1024 * 1024, // 10MB for line counting
25
+ };
26
+ const READ_PERFORMANCE_THRESHOLDS = {
27
+ SMALL_READ_THRESHOLD: 100, // For very small reads
28
+ DEEP_OFFSET_THRESHOLD: 1000, // Past this line, find where it starts first
29
+ CHUNK_SIZE: 8192, // 8KB chunks for reverse reading
30
+ SCAN_CHUNK_SIZE: 64 * 1024, // 64KB chunks for counting line breaks forward
31
+ };
32
+ const LINE_FEED = 0x0a;
33
+ const CARRIAGE_RETURN = 0x0d;
34
+ /**
35
+ * The line breaks read_file reads by (readline's): LF, CRLF as one break, and
36
+ * a lone CR. True when a line ends between two adjacent code units: the bytes
37
+ * of UTF-8 text (an LF or CR byte is never part of another character) or a
38
+ * string's characters. `after` is -1 at the end of the text.
39
+ */
40
+ function lineBreakBetween(before, after) {
41
+ return before === LINE_FEED || (before === CARRIAGE_RETURN && after !== LINE_FEED);
42
+ }
43
+ function throwIfAborted(signal) {
44
+ if (signal?.aborted) {
45
+ const err = new Error('Read aborted');
46
+ err.code = 'ABORT_ERR';
47
+ throw err;
48
+ }
49
+ }
50
+ /**
51
+ * Text file handler implementation
52
+ * Binary detection is done at the factory level - this handler assumes file is text
53
+ */
54
+ export class TextFileHandler {
55
+ canHandle(_path) {
56
+ // Text handler accepts all files that pass the factory's binary check
57
+ // The factory routes binary files to BinaryFileHandler before reaching here
58
+ return true;
59
+ }
60
+ async detectTextEncoding(filePath) {
61
+ const fd = await fs.open(filePath, 'r');
62
+ try {
63
+ const bom = Buffer.alloc(2);
64
+ const { bytesRead } = await fd.read(bom, 0, bom.length, 0);
65
+ if (bytesRead === 2 && bom[0] === 0xff && bom[1] === 0xfe) {
66
+ return 'utf16le';
67
+ }
68
+ return 'utf8';
69
+ }
70
+ finally {
71
+ await fd.close();
72
+ }
73
+ }
74
+ async read(filePath, options) {
75
+ const offset = options?.offset ?? 0;
76
+ const length = options?.length ?? 1000; // Default from config
77
+ const includeStatusMessage = options?.includeStatusMessage ?? true;
78
+ // Binary detection is done at factory level - just read as text
79
+ return this.readFileWithSmartPositioning(filePath, offset, length, 'text/plain', includeStatusMessage, options?.signal);
80
+ }
81
+ async write(path, content, mode = 'rewrite') {
82
+ if (mode === 'append') {
83
+ await fs.appendFile(path, content);
84
+ }
85
+ else {
86
+ await fs.writeFile(path, content);
87
+ }
88
+ }
89
+ async getInfo(path) {
90
+ const stats = await fs.stat(path);
91
+ const info = {
92
+ size: stats.size,
93
+ created: stats.birthtime,
94
+ modified: stats.mtime,
95
+ accessed: stats.atime,
96
+ isDirectory: stats.isDirectory(),
97
+ isFile: stats.isFile(),
98
+ permissions: stats.mode.toString(8).slice(-3),
99
+ fileType: 'text',
100
+ metadata: {}
101
+ };
102
+ // For text files that aren't too large, count lines
103
+ if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
104
+ try {
105
+ const encoding = await this.detectTextEncoding(path);
106
+ let content = await fs.readFile(path, encoding);
107
+ if (encoding === 'utf16le' && content.charCodeAt(0) === 0xfeff) {
108
+ content = content.slice(1);
109
+ }
110
+ const lineCount = TextFileHandler.countLines(content);
111
+ info.metadata.lineCount = lineCount;
112
+ }
113
+ catch (error) {
114
+ // If reading fails, skip line count
115
+ }
116
+ }
117
+ return info;
118
+ }
119
+ // ========================================================================
120
+ // Private Helper Methods (extracted from filesystem.ts)
121
+ // ========================================================================
122
+ /**
123
+ * Count lines in text content the way read_file reads them (lineBreakBetween).
124
+ * A line break at the very end ends the last line; it doesn't start another.
125
+ * Made static and public for use by other modules (e.g., writeFile telemetry in filesystem.ts)
126
+ */
127
+ static countLines(content) {
128
+ if (content === '')
129
+ return 0;
130
+ let lines = 1;
131
+ for (let i = 1; i < content.length; i++) {
132
+ if (lineBreakBetween(content.charCodeAt(i - 1), content.charCodeAt(i)))
133
+ lines++;
134
+ }
135
+ return lines;
136
+ }
137
+ /**
138
+ * Get file line count (for files under size limit)
139
+ */
140
+ async getFileLineCount(filePath, signal, encoding = 'utf8') {
141
+ try {
142
+ const stats = await fs.stat(filePath);
143
+ if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
144
+ let content = await fs.readFile(filePath, { encoding, signal });
145
+ if (encoding === 'utf16le' && content.charCodeAt(0) === 0xfeff) {
146
+ content = content.slice(1);
147
+ }
148
+ return TextFileHandler.countLines(content);
149
+ }
150
+ }
151
+ catch (error) {
152
+ // If we can't read the file, return undefined
153
+ }
154
+ return undefined;
155
+ }
156
+ /**
157
+ * Generate enhanced status message
158
+ */
159
+ generateEnhancedStatusMessage(readLines, offset, totalLines, isNegativeOffset = false) {
160
+ if (isNegativeOffset) {
161
+ if (totalLines !== undefined) {
162
+ return `[Reading last ${readLines} lines (total: ${totalLines} lines)]`;
163
+ }
164
+ else {
165
+ return `[Reading last ${readLines} lines]`;
166
+ }
167
+ }
168
+ else {
169
+ if (totalLines !== undefined) {
170
+ const endLine = offset + readLines;
171
+ const remainingLines = Math.max(0, totalLines - endLine);
172
+ if (offset === 0) {
173
+ return `[Reading ${readLines} lines from start (total: ${totalLines} lines, ${remainingLines} remaining)]`;
174
+ }
175
+ else {
176
+ return `[Reading ${readLines} lines from line ${offset} (total: ${totalLines} lines, ${remainingLines} remaining)]`;
177
+ }
178
+ }
179
+ else {
180
+ if (offset === 0) {
181
+ return `[Reading ${readLines} lines from start]`;
182
+ }
183
+ else {
184
+ return `[Reading ${readLines} lines from line ${offset}]`;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ /**
190
+ * Split text into lines while preserving line endings
191
+ * Made static and public for use by other modules (e.g., readFileInternal in filesystem.ts)
192
+ */
193
+ static splitLinesPreservingEndings(content) {
194
+ if (!content)
195
+ return [''];
196
+ const lines = [];
197
+ let currentLine = '';
198
+ for (let i = 0; i < content.length; i++) {
199
+ const char = content[i];
200
+ currentLine += char;
201
+ if (char === '\n') {
202
+ lines.push(currentLine);
203
+ currentLine = '';
204
+ }
205
+ else if (char === '\r') {
206
+ if (i + 1 < content.length && content[i + 1] === '\n') {
207
+ currentLine += content[i + 1];
208
+ i++;
209
+ }
210
+ lines.push(currentLine);
211
+ currentLine = '';
212
+ }
213
+ }
214
+ if (currentLine) {
215
+ lines.push(currentLine);
216
+ }
217
+ return lines;
218
+ }
219
+ /**
220
+ * Read file with smart positioning for optimal performance
221
+ */
222
+ async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true, signal) {
223
+ const stats = await fs.stat(filePath);
224
+ const fileSize = stats.size;
225
+ const encoding = await this.detectTextEncoding(filePath);
226
+ const totalLines = await this.getFileLineCount(filePath, signal, encoding);
227
+ // UTF-16LE needs character-aware streaming. Skip the two-byte BOM and keep
228
+ // the same line pagination/status semantics as ordinary text reads.
229
+ if (encoding === 'utf16le') {
230
+ if (offset < 0) {
231
+ return await this.readFromEndWithReadline(filePath, Math.abs(offset), mimeType, includeStatusMessage, totalLines, signal, 'utf16le', 2);
232
+ }
233
+ return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal, 'utf16le', 2);
234
+ }
235
+ // For negative offsets (tail behavior), use reverse reading
236
+ if (offset < 0) {
237
+ const requestedLines = Math.abs(offset);
238
+ if (fileSize > FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD &&
239
+ requestedLines <= READ_PERFORMANCE_THRESHOLDS.SMALL_READ_THRESHOLD) {
240
+ return await this.readLastNLinesReverse(filePath, requestedLines, mimeType, includeStatusMessage, totalLines, signal);
241
+ }
242
+ else {
243
+ return await this.readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage, totalLines, signal);
244
+ }
245
+ }
246
+ // For positive offsets
247
+ else {
248
+ if (fileSize < FILE_SIZE_LIMITS.LARGE_FILE_THRESHOLD || offset === 0) {
249
+ return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal);
250
+ }
251
+ else {
252
+ if (offset > READ_PERFORMANCE_THRESHOLDS.DEEP_OFFSET_THRESHOLD) {
253
+ return await this.readFromDeepOffset(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal);
254
+ }
255
+ else {
256
+ return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ /**
262
+ * Read last N lines efficiently: find where they start by reading the file
263
+ * backwards, then read them from there as readline does for smaller files
264
+ */
265
+ async readLastNLinesReverse(filePath, n, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
266
+ const start = await this.findLastLinesStart(filePath, n, signal);
267
+ return this.readFromEndWithReadline(filePath, n, mimeType, includeStatusMessage, fileTotalLines, signal, undefined, start);
268
+ }
269
+ /**
270
+ * Byte position where the last `n` lines start, found by reading 8 KB chunks
271
+ * backwards and finding line breaks by lineBreakBetween. A line break at the
272
+ * very end ends the last line instead of starting another.
273
+ */
274
+ async findLastLinesStart(filePath, n, signal) {
275
+ const fd = await fs.open(filePath, 'r');
276
+ try {
277
+ const { size } = await fd.stat();
278
+ const buffer = Buffer.alloc(READ_PERFORMANCE_THRESHOLDS.CHUNK_SIZE);
279
+ let position = size;
280
+ let breaks = 0;
281
+ let after = -1; // the byte after buffer[i]; -1 at the end of the file
282
+ while (position > 0) {
283
+ throwIfAborted(signal);
284
+ const readSize = Math.min(buffer.length, position);
285
+ position -= readSize;
286
+ await fd.read(buffer, 0, readSize, position);
287
+ for (let i = readSize - 1; i >= 0; i--) {
288
+ const byte = buffer[i];
289
+ if (after !== -1 && lineBreakBetween(byte, after) && ++breaks === n) {
290
+ return position + i + 1;
291
+ }
292
+ after = byte;
293
+ }
294
+ }
295
+ return 0;
296
+ }
297
+ finally {
298
+ await fd.close();
299
+ }
300
+ }
301
+ /**
302
+ * Run `readLines` on a readline over `filePath`, then close the file before
303
+ * returning, whether the read finished, stopped early or failed. rl.close()
304
+ * alone closes the line reader, not the file stream under it, so a read
305
+ * that stopped early kept the file open until the process exited.
306
+ */
307
+ async withLineReader(filePath, options, readLines) {
308
+ const input = createReadStream(filePath, options);
309
+ // Settles once the file is closed: on 'close', or on 'error', which a
310
+ // file stream emits after closing its file
311
+ const closed = once(input, 'close').catch(() => { });
312
+ const rl = createInterface({
313
+ input,
314
+ crlfDelay: Infinity
315
+ });
316
+ try {
317
+ await readLines(rl);
318
+ }
319
+ finally {
320
+ rl.close();
321
+ input.destroy();
322
+ await closed;
323
+ }
324
+ }
325
+ /**
326
+ * Read from end using readline with circular buffer
327
+ */
328
+ async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines, signal, encoding, start) {
329
+ const buffer = new Array(requestedLines);
330
+ let bufferIndex = 0;
331
+ let totalLines = 0;
332
+ await this.withLineReader(filePath, { signal, encoding, start }, async (rl) => {
333
+ for await (const line of rl) {
334
+ buffer[bufferIndex] = line;
335
+ bufferIndex = (bufferIndex + 1) % requestedLines;
336
+ totalLines++;
337
+ }
338
+ });
339
+ let result;
340
+ if (totalLines >= requestedLines) {
341
+ result = [
342
+ ...buffer.slice(bufferIndex),
343
+ ...buffer.slice(0, bufferIndex)
344
+ ].filter(line => line !== undefined);
345
+ }
346
+ else {
347
+ result = buffer.slice(0, totalLines);
348
+ }
349
+ const content = includeStatusMessage
350
+ ? `${this.generateEnhancedStatusMessage(result.length, -requestedLines, fileTotalLines, true)}\n\n${result.join('\n')}`
351
+ : result.join('\n');
352
+ return { content, mimeType, metadata: {} };
353
+ }
354
+ /**
355
+ * Read from start/middle using readline. With `start`, reading begins at
356
+ * that byte, where line `startLine` begins.
357
+ */
358
+ async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal, encoding, start, startLine = 0) {
359
+ const result = [];
360
+ let lineNumber = startLine;
361
+ await this.withLineReader(filePath, { signal, encoding, start }, async (rl) => {
362
+ for await (const line of rl) {
363
+ if (lineNumber >= offset && result.length < length) {
364
+ result.push(line);
365
+ }
366
+ if (result.length >= length)
367
+ break;
368
+ lineNumber++;
369
+ }
370
+ });
371
+ if (includeStatusMessage) {
372
+ const statusMessage = this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
373
+ const content = `${statusMessage}\n\n${result.join('\n')}`;
374
+ return { content, mimeType, metadata: {} };
375
+ }
376
+ else {
377
+ const content = result.join('\n');
378
+ return { content, mimeType, metadata: {} };
379
+ }
380
+ }
381
+ /**
382
+ * Read from a deep offset in a very large file: find the byte where line
383
+ * `offset` starts, then read from there with readline
384
+ */
385
+ async readFromDeepOffset(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
386
+ const start = await this.findLineStart(filePath, offset, signal);
387
+ return this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines, signal, undefined, start, offset);
388
+ }
389
+ /**
390
+ * Byte position where line `line` (0-based) starts, or the file size when
391
+ * the file has fewer lines. Counts line breaks by lineBreakBetween in raw
392
+ * bytes, which is much faster than reading lines.
393
+ */
394
+ async findLineStart(filePath, line, signal) {
395
+ if (line <= 0)
396
+ return 0;
397
+ const fd = await fs.open(filePath, 'r');
398
+ try {
399
+ const buffer = Buffer.alloc(READ_PERFORMANCE_THRESHOLDS.SCAN_CHUNK_SIZE);
400
+ let position = 0;
401
+ let breaks = 0;
402
+ let previous = -1;
403
+ for (;;) {
404
+ throwIfAborted(signal);
405
+ const { bytesRead } = await fd.read(buffer, 0, buffer.length, position);
406
+ if (bytesRead === 0)
407
+ return position;
408
+ for (let i = 0; i < bytesRead; i++) {
409
+ const byte = buffer[i];
410
+ // A line break before this byte: the next line starts here
411
+ if (lineBreakBetween(previous, byte) && ++breaks === line) {
412
+ return position + i;
413
+ }
414
+ previous = byte;
415
+ }
416
+ position += bytesRead;
417
+ }
418
+ }
419
+ finally {
420
+ await fd.close();
421
+ }
422
+ }
423
+ }