starmux 0.12.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 (93) hide show
  1. package/dist/src/cli/mac-mcp-gateway.js +4 -3
  2. package/dist/src/cli/mac-mcp-gateway.js.map +1 -1
  3. package/dist/src/cli/starmux.js +1 -1
  4. package/dist/src/cli/starmux.js.map +1 -1
  5. package/dist/src/runtime-paths.js +3 -3
  6. package/dist/src/runtime-paths.js.map +1 -1
  7. package/dist/vendor/desktop-commander/LICENSE +21 -0
  8. package/dist/vendor/desktop-commander/dist/bootstrap.js +22 -0
  9. package/dist/vendor/desktop-commander/dist/command-manager.js +246 -0
  10. package/dist/vendor/desktop-commander/dist/config-field-definitions.js +37 -0
  11. package/dist/vendor/desktop-commander/dist/config-manager.js +634 -0
  12. package/dist/vendor/desktop-commander/dist/config-recovery.js +81 -0
  13. package/dist/vendor/desktop-commander/dist/config.js +19 -0
  14. package/dist/vendor/desktop-commander/dist/custom-stdio.js +358 -0
  15. package/dist/vendor/desktop-commander/dist/error-handlers.js +15 -0
  16. package/dist/vendor/desktop-commander/dist/handlers/edit-search-handlers.js +6 -0
  17. package/dist/vendor/desktop-commander/dist/handlers/filesystem-handlers.js +450 -0
  18. package/dist/vendor/desktop-commander/dist/handlers/history-handlers.js +67 -0
  19. package/dist/vendor/desktop-commander/dist/handlers/index.js +7 -0
  20. package/dist/vendor/desktop-commander/dist/handlers/process-handlers.js +15 -0
  21. package/dist/vendor/desktop-commander/dist/handlers/search-answers.js +172 -0
  22. package/dist/vendor/desktop-commander/dist/handlers/search-handlers.js +136 -0
  23. package/dist/vendor/desktop-commander/dist/handlers/terminal-handlers.js +35 -0
  24. package/dist/vendor/desktop-commander/dist/index.js +88 -0
  25. package/dist/vendor/desktop-commander/dist/search-manager.js +1262 -0
  26. package/dist/vendor/desktop-commander/dist/server.js +1414 -0
  27. package/dist/vendor/desktop-commander/dist/terminal-manager.js +757 -0
  28. package/dist/vendor/desktop-commander/dist/tools/config.js +211 -0
  29. package/dist/vendor/desktop-commander/dist/tools/edit.js +455 -0
  30. package/dist/vendor/desktop-commander/dist/tools/feedback.js +10 -0
  31. package/dist/vendor/desktop-commander/dist/tools/filesystem.js +1047 -0
  32. package/dist/vendor/desktop-commander/dist/tools/fuzzySearch.js +74 -0
  33. package/dist/vendor/desktop-commander/dist/tools/fuzzySearchCore.js +125 -0
  34. package/dist/vendor/desktop-commander/dist/tools/improved-process-tools.js +726 -0
  35. package/dist/vendor/desktop-commander/dist/tools/mime-types.js +28 -0
  36. package/dist/vendor/desktop-commander/dist/tools/pdf/extract-images.js +131 -0
  37. package/dist/vendor/desktop-commander/dist/tools/pdf/index.js +4 -0
  38. package/dist/vendor/desktop-commander/dist/tools/pdf/lib/pdf2md.js +78 -0
  39. package/dist/vendor/desktop-commander/dist/tools/pdf/manipulations.js +112 -0
  40. package/dist/vendor/desktop-commander/dist/tools/pdf/markdown.js +30 -0
  41. package/dist/vendor/desktop-commander/dist/tools/pdf/utils.js +45 -0
  42. package/dist/vendor/desktop-commander/dist/tools/process.js +90 -0
  43. package/dist/vendor/desktop-commander/dist/tools/prompts.js +259 -0
  44. package/dist/vendor/desktop-commander/dist/tools/schemas.js +242 -0
  45. package/dist/vendor/desktop-commander/dist/tools/usage.js +24 -0
  46. package/dist/vendor/desktop-commander/dist/types.js +1 -0
  47. package/dist/vendor/desktop-commander/dist/ui/config-editor/config-editor-runtime.js +150 -0
  48. package/dist/vendor/desktop-commander/dist/ui/config-editor/index.html +13 -0
  49. package/dist/vendor/desktop-commander/dist/ui/config-editor/styles.css +587 -0
  50. package/dist/vendor/desktop-commander/dist/ui/contracts.js +57 -0
  51. package/dist/vendor/desktop-commander/dist/ui/file-preview/index.html +16 -0
  52. package/dist/vendor/desktop-commander/dist/ui/file-preview/preview-runtime.js +471 -0
  53. package/dist/vendor/desktop-commander/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  54. package/dist/vendor/desktop-commander/dist/ui/file-preview/styles.css +1631 -0
  55. package/dist/vendor/desktop-commander/dist/ui/resources.js +93 -0
  56. package/dist/vendor/desktop-commander/dist/utils/ab-test.js +122 -0
  57. package/dist/vendor/desktop-commander/dist/utils/atomic-write.js +80 -0
  58. package/dist/vendor/desktop-commander/dist/utils/capture.js +40 -0
  59. package/dist/vendor/desktop-commander/dist/utils/dockerPrompt.js +9 -0
  60. package/dist/vendor/desktop-commander/dist/utils/exit-process.js +31 -0
  61. package/dist/vendor/desktop-commander/dist/utils/feature-flags.js +18 -0
  62. package/dist/vendor/desktop-commander/dist/utils/files/base.js +5 -0
  63. package/dist/vendor/desktop-commander/dist/utils/files/binary.js +65 -0
  64. package/dist/vendor/desktop-commander/dist/utils/files/docx.js +694 -0
  65. package/dist/vendor/desktop-commander/dist/utils/files/excel.js +497 -0
  66. package/dist/vendor/desktop-commander/dist/utils/files/factory.js +168 -0
  67. package/dist/vendor/desktop-commander/dist/utils/files/image.js +89 -0
  68. package/dist/vendor/desktop-commander/dist/utils/files/index.js +14 -0
  69. package/dist/vendor/desktop-commander/dist/utils/files/pdf.js +124 -0
  70. package/dist/vendor/desktop-commander/dist/utils/files/text.js +423 -0
  71. package/dist/vendor/desktop-commander/dist/utils/fuzzySearchLogger.js +126 -0
  72. package/dist/vendor/desktop-commander/dist/utils/internal-facts.js +23 -0
  73. package/dist/vendor/desktop-commander/dist/utils/lazy-package.js +44 -0
  74. package/dist/vendor/desktop-commander/dist/utils/lineEndingHandler.js +77 -0
  75. package/dist/vendor/desktop-commander/dist/utils/logger.js +72 -0
  76. package/dist/vendor/desktop-commander/dist/utils/mcp-ui-ab-test.js +62 -0
  77. package/dist/vendor/desktop-commander/dist/utils/open-browser.js +2 -0
  78. package/dist/vendor/desktop-commander/dist/utils/process-detection.js +260 -0
  79. package/dist/vendor/desktop-commander/dist/utils/process-tree.js +223 -0
  80. package/dist/vendor/desktop-commander/dist/utils/rename.js +74 -0
  81. package/dist/vendor/desktop-commander/dist/utils/ripgrep-resolver.js +69 -0
  82. package/dist/vendor/desktop-commander/dist/utils/shell.js +187 -0
  83. package/dist/vendor/desktop-commander/dist/utils/system-info.js +727 -0
  84. package/dist/vendor/desktop-commander/dist/utils/terminal-timestamps.js +18 -0
  85. package/dist/vendor/desktop-commander/dist/utils/toolHistory.js +309 -0
  86. package/dist/vendor/desktop-commander/dist/utils/trackTools.js +55 -0
  87. package/dist/vendor/desktop-commander/dist/utils/unsupportedParams.js +66 -0
  88. package/dist/vendor/desktop-commander/dist/utils/usageTracker.js +225 -0
  89. package/dist/vendor/desktop-commander/dist/utils/welcome-onboarding.js +2 -0
  90. package/dist/vendor/desktop-commander/dist/utils/withTimeout.js +85 -0
  91. package/dist/vendor/desktop-commander/dist/version.js +1 -0
  92. package/dist/vendor/desktop-commander/package.json +15 -0
  93. package/package.json +14 -2
@@ -0,0 +1,18 @@
1
+ import { formatWithOptions } from 'node:util';
2
+ /** Prefix human-readable console output only; stdout also carries MCP JSON-RPC. */
3
+ export function startTerminalTimestamps() {
4
+ const methods = ['log', 'info', 'warn', 'error', 'debug'];
5
+ const originals = methods.map((method) => [method, console[method]]);
6
+ for (const [method, original] of originals) {
7
+ console[method] = (...args) => {
8
+ const time = new Date().toTimeString().slice(0, 8);
9
+ const stream = method === 'warn' || method === 'error' ? process.stderr : process.stdout;
10
+ const message = formatWithOptions({ colors: stream.isTTY === true }, ...args);
11
+ original.call(console, message.split('\n').map((line) => line ? `[${time}] ${line}` : line).join('\n'));
12
+ };
13
+ }
14
+ return () => {
15
+ for (const [method, original] of originals)
16
+ console[method] = original;
17
+ };
18
+ }
@@ -0,0 +1,309 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ // Format timestamp in local timezone for display
5
+ function formatLocalTimestamp(isoTimestamp) {
6
+ const date = new Date(isoTimestamp);
7
+ return date.toLocaleString('en-US', {
8
+ year: 'numeric',
9
+ month: '2-digit',
10
+ day: '2-digit',
11
+ hour: '2-digit',
12
+ minute: '2-digit',
13
+ second: '2-digit',
14
+ hour12: false
15
+ });
16
+ }
17
+ class ToolHistory {
18
+ constructor() {
19
+ this.history = [];
20
+ this.MAX_ENTRIES = 1000;
21
+ /**
22
+ * Cap on the output kept per entry. Entries hold the FULL ServerResult, and
23
+ * get_recent_tool_calls serialises them straight back out, so without a cap
24
+ * the history is unbounded in two compounding ways:
25
+ *
26
+ * - a single large output (a big read_file, a wide list_directory) makes
27
+ * every later history dump that includes it large too, and
28
+ * - any tool whose output happens to CONTAIN a history dump — e.g.
29
+ * `cat`-ing a file a previous dump was written to — nests the whole
30
+ * history inside itself, and each nesting level roughly doubles the JSON
31
+ * escaping. Observed 2026-07-27: 83KB of arguments on disk produced a
32
+ * 1.89MB in-memory dump this way.
33
+ *
34
+ * Excluding more tool names cannot fix that, because the nesting arrives
35
+ * through ordinary tools. Capping the stored output does, and a preview is
36
+ * all this history is for — it is a "what happened recently" aid, not a
37
+ * result cache.
38
+ */
39
+ this.MAX_STORED_OUTPUT_BYTES = 4 * 1024;
40
+ this.MAX_HISTORY_FILE_SIZE_BYTES = 5 * 1024 * 1024;
41
+ // When the file exceeds the cap we trim it down to this target instead of
42
+ // all the way to zero, so a single overflow doesn't cause every subsequent
43
+ // flush to re-trim.
44
+ this.HISTORY_FILE_TRIM_TARGET_BYTES = 4 * 1024 * 1024;
45
+ this.writeQueue = [];
46
+ this.isWriting = false;
47
+ // Store history in same directory as config to keep everything together
48
+ const historyDir = path.join(os.homedir(), '.claude-server-commander');
49
+ // Ensure directory exists
50
+ if (!fs.existsSync(historyDir)) {
51
+ fs.mkdirSync(historyDir, { recursive: true });
52
+ }
53
+ // Use append-only JSONL format (JSON Lines)
54
+ this.historyFile = path.join(historyDir, 'tool-history.jsonl');
55
+ // Load existing history on startup
56
+ this.loadFromDisk();
57
+ // Start async write processor
58
+ this.startWriteProcessor();
59
+ }
60
+ /**
61
+ * Load history from disk (all instances share the same file)
62
+ */
63
+ loadFromDisk() {
64
+ try {
65
+ if (!fs.existsSync(this.historyFile)) {
66
+ return;
67
+ }
68
+ // If the file is over the cap, trim it down before reading so we
69
+ // load a bounded amount.
70
+ this.trimHistoryFileIfTooLarge();
71
+ const content = fs.readFileSync(this.historyFile, 'utf-8');
72
+ const lines = content.trim().split('\n').filter(line => line.trim());
73
+ // Parse each line as JSON
74
+ const records = [];
75
+ for (const line of lines) {
76
+ try {
77
+ records.push(JSON.parse(line));
78
+ }
79
+ catch (e) {
80
+ // Silently skip invalid lines
81
+ }
82
+ }
83
+ // Keep only last 1000 entries, and cap on the way IN as well as on the
84
+ // way out. Entries written before the cap existed are still on disk and
85
+ // are well under the whole-file trim threshold, so without this the cap
86
+ // does nothing for anyone upgrading with an existing history file — i.e.
87
+ // for everyone it was written for.
88
+ this.history = records
89
+ .slice(-this.MAX_ENTRIES)
90
+ .map(record => ({ ...record, output: this.capOutput(record.output) }));
91
+ // If file is getting too large, trim it
92
+ if (lines.length > this.MAX_ENTRIES * 2) {
93
+ this.trimHistoryFile();
94
+ }
95
+ }
96
+ catch (error) {
97
+ // Silently fail
98
+ }
99
+ }
100
+ /**
101
+ * Trim the on-disk history file to stay under the size cap by dropping the
102
+ * oldest entries (lines) until the kept tail fits within the trim target.
103
+ * Returns true only when the file was actually rewritten with a smaller
104
+ * tail, so callers can fall through to their normal path on failure or
105
+ * no-op rather than mutating in-memory state.
106
+ *
107
+ * Always keeps at least the most recent entry, even if a single record
108
+ * exceeds the trim target — there is no useful state below that.
109
+ */
110
+ trimHistoryFileIfTooLarge() {
111
+ let stats;
112
+ try {
113
+ if (!fs.existsSync(this.historyFile)) {
114
+ return false;
115
+ }
116
+ stats = fs.statSync(this.historyFile);
117
+ if (stats.size <= this.MAX_HISTORY_FILE_SIZE_BYTES) {
118
+ return false;
119
+ }
120
+ }
121
+ catch (error) {
122
+ return false;
123
+ }
124
+ try {
125
+ const content = fs.readFileSync(this.historyFile, 'utf-8');
126
+ const lines = content.split('\n').filter(line => line.length > 0);
127
+ if (lines.length === 0) {
128
+ return false;
129
+ }
130
+ // Walk lines from newest to oldest, accumulating bytes (line + '\n'),
131
+ // and keep as many as fit within the trim target. Always keep at
132
+ // least the last line.
133
+ const kept = [];
134
+ let bytes = 0;
135
+ for (let i = lines.length - 1; i >= 0; i--) {
136
+ const lineBytes = Buffer.byteLength(lines[i], 'utf-8') + 1; // +1 for '\n'
137
+ if (kept.length > 0 && bytes + lineBytes > this.HISTORY_FILE_TRIM_TARGET_BYTES) {
138
+ break;
139
+ }
140
+ kept.push(lines[i]);
141
+ bytes += lineBytes;
142
+ }
143
+ kept.reverse();
144
+ fs.writeFileSync(this.historyFile, kept.join('\n') + '\n', 'utf-8');
145
+ return true;
146
+ }
147
+ catch (error) {
148
+ // Trim failed; do not claim the file was changed.
149
+ return false;
150
+ }
151
+ }
152
+ /**
153
+ * Trim history file to prevent it from growing indefinitely
154
+ */
155
+ trimHistoryFile() {
156
+ try {
157
+ // Keep last 1000 entries in memory
158
+ const keepEntries = this.history.slice(-this.MAX_ENTRIES);
159
+ // Write them back
160
+ const lines = keepEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
161
+ fs.writeFileSync(this.historyFile, lines, 'utf-8');
162
+ }
163
+ catch (error) {
164
+ // Silently fail
165
+ }
166
+ }
167
+ /**
168
+ * Async write processor - batches writes to avoid blocking
169
+ */
170
+ startWriteProcessor() {
171
+ this.writeInterval = setInterval(() => {
172
+ if (this.writeQueue.length > 0 && !this.isWriting) {
173
+ this.flushToDisk();
174
+ }
175
+ }, 1000); // Flush every second
176
+ // Prevent interval from keeping process alive during shutdown/tests
177
+ this.writeInterval.unref();
178
+ }
179
+ /**
180
+ * Flush queued writes to disk
181
+ */
182
+ async flushToDisk() {
183
+ if (this.isWriting || this.writeQueue.length === 0)
184
+ return;
185
+ this.isWriting = true;
186
+ const toWrite = [...this.writeQueue];
187
+ this.writeQueue = [];
188
+ try {
189
+ // If the on-disk file has grown past the cap, trim it down to the
190
+ // target size (keeping the most recent entries) before appending.
191
+ // The in-memory cache is unaffected — it is already bounded by
192
+ // MAX_ENTRIES via addCall.
193
+ this.trimHistoryFileIfTooLarge();
194
+ // Append to file (atomic append operation)
195
+ const lines = toWrite.map(entry => JSON.stringify(entry)).join('\n') + '\n';
196
+ fs.appendFileSync(this.historyFile, lines, 'utf-8');
197
+ }
198
+ catch (error) {
199
+ // Put back in queue on failure
200
+ this.writeQueue.unshift(...toWrite);
201
+ }
202
+ finally {
203
+ this.isWriting = false;
204
+ }
205
+ }
206
+ /**
207
+ * Add a tool call to history
208
+ */
209
+ /**
210
+ * Replace an oversized output with a short marker. Keeps the record shape
211
+ * ({ content: [...] }) so readers and formatters need no special case.
212
+ */
213
+ capOutput(output) {
214
+ let size;
215
+ try {
216
+ size = JSON.stringify(output)?.length ?? 0;
217
+ }
218
+ catch {
219
+ // Circular or otherwise unserialisable — it could never be returned to a
220
+ // client anyway, so don't retain it.
221
+ size = Number.POSITIVE_INFINITY;
222
+ }
223
+ if (size <= this.MAX_STORED_OUTPUT_BYTES)
224
+ return output;
225
+ const shown = Number.isFinite(size) ? `${size} bytes` : 'unserialisable';
226
+ return {
227
+ ...output,
228
+ content: [
229
+ {
230
+ type: 'text',
231
+ text: `[output omitted from history: ${shown}, over the ${this.MAX_STORED_OUTPUT_BYTES}-byte cap]`,
232
+ },
233
+ ],
234
+ };
235
+ }
236
+ addCall(toolName, args, output, duration) {
237
+ const record = {
238
+ timestamp: new Date().toISOString(),
239
+ toolName,
240
+ arguments: args,
241
+ output: this.capOutput(output),
242
+ duration
243
+ };
244
+ this.history.push(record);
245
+ // Keep only last 1000 in memory
246
+ if (this.history.length > this.MAX_ENTRIES) {
247
+ this.history.shift();
248
+ }
249
+ // Queue for async write
250
+ this.writeQueue.push(record);
251
+ }
252
+ /**
253
+ * Get recent tool calls with filters
254
+ */
255
+ getRecentCalls(options) {
256
+ let results = [...this.history];
257
+ // Filter by tool name
258
+ if (options.toolName) {
259
+ results = results.filter(r => r.toolName === options.toolName);
260
+ }
261
+ // Filter by timestamp
262
+ if (options.since) {
263
+ const sinceDate = new Date(options.since);
264
+ results = results.filter(r => new Date(r.timestamp) >= sinceDate);
265
+ }
266
+ // Limit results (default 50, max 1000)
267
+ const limit = Math.min(options.maxResults || 50, 1000);
268
+ return results.slice(-limit);
269
+ }
270
+ /**
271
+ * Get recent calls formatted with local timezone
272
+ */
273
+ getRecentCallsFormatted(options) {
274
+ const calls = this.getRecentCalls(options);
275
+ // Format timestamps to local timezone
276
+ return calls.map(call => ({
277
+ ...call,
278
+ timestamp: formatLocalTimestamp(call.timestamp)
279
+ }));
280
+ }
281
+ /**
282
+ * Get current stats
283
+ */
284
+ getStats() {
285
+ return {
286
+ totalEntries: this.history.length,
287
+ oldestEntry: this.history[0]?.timestamp,
288
+ newestEntry: this.history[this.history.length - 1]?.timestamp,
289
+ historyFile: this.historyFile,
290
+ queuedWrites: this.writeQueue.length
291
+ };
292
+ }
293
+ /**
294
+ * Cleanup method - clears interval and flushes pending writes
295
+ * Call this during shutdown or in tests
296
+ */
297
+ async cleanup() {
298
+ // Clear the interval
299
+ if (this.writeInterval) {
300
+ clearInterval(this.writeInterval);
301
+ this.writeInterval = undefined;
302
+ }
303
+ // Flush any remaining writes
304
+ if (this.writeQueue.length > 0) {
305
+ await this.flushToDisk();
306
+ }
307
+ }
308
+ }
309
+ export const toolHistory = new ToolHistory();
@@ -0,0 +1,55 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { TOOL_CALL_FILE, TOOL_CALL_FILE_MAX_SIZE } from '../config.js';
4
+ import { renameWithRetry } from './rename.js';
5
+ // Ensure the directory for the log file exists
6
+ const logDir = path.dirname(TOOL_CALL_FILE);
7
+ await fs.promises.mkdir(logDir, { recursive: true });
8
+ /**
9
+ * Track tool calls and save them to a log file
10
+ * @param toolName Name of the tool being called
11
+ * @param args Arguments passed to the tool (optional)
12
+ */
13
+ export async function trackToolCall(toolName, args) {
14
+ try {
15
+ // Get current timestamp
16
+ const timestamp = new Date().toISOString();
17
+ // Format the log entry
18
+ const logEntry = `${timestamp} | ${toolName.padEnd(20, ' ')}${args ? `\t| Arguments: ${JSON.stringify(args)}` : ''}\n`;
19
+ // Check if file exists and get its size
20
+ let fileSize = 0;
21
+ try {
22
+ const stats = await fs.promises.stat(TOOL_CALL_FILE);
23
+ fileSize = stats.size;
24
+ }
25
+ catch (err) {
26
+ // File doesn't exist yet, size remains 0
27
+ }
28
+ // If file size is 10MB or larger, rotate the log file
29
+ if (fileSize >= TOOL_CALL_FILE_MAX_SIZE) {
30
+ const fileExt = path.extname(TOOL_CALL_FILE);
31
+ const fileBase = path.basename(TOOL_CALL_FILE, fileExt);
32
+ const dirName = path.dirname(TOOL_CALL_FILE);
33
+ // Create a timestamp-based filename for the old log
34
+ const date = new Date();
35
+ const rotateTimestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}_${String(date.getHours()).padStart(2, '0')}-${String(date.getMinutes()).padStart(2, '0')}-${String(date.getSeconds()).padStart(2, '0')}`;
36
+ const newFileName = path.join(dirName, `${fileBase}_${rotateTimestamp}${fileExt}`);
37
+ // Rename the current file
38
+ await renameWithRetry(TOOL_CALL_FILE, newFileName);
39
+ }
40
+ // Append to log file (if file was renamed, this will create a new file)
41
+ await fs.promises.appendFile(TOOL_CALL_FILE, logEntry, 'utf8');
42
+ }
43
+ catch (error) {
44
+ const errorMessage = error instanceof Error ? error.message : String(error);
45
+ const { capture } = await import('./capture.js');
46
+ // Send a final telemetry event noting that the user has opted out
47
+ // This helps us track opt-out rates while respecting the user's choice
48
+ await capture('server_track_tool_call_error', {
49
+ error: errorMessage,
50
+ toolName
51
+ });
52
+ // Don't let logging errors affect the main functionality
53
+ console.error(`Error logging tool call: ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Utility for detecting parameters a caller sent that a tool's Zod schema does
3
+ * not accept.
4
+ *
5
+ * Zod's default `z.object()` silently strips unknown keys, so a model that
6
+ * invents a parameter (e.g. `view_range`) gets a normal-looking response with no
7
+ * signal that its input was ignored. These helpers let the dispatcher surface a
8
+ * corrective warning instead.
9
+ *
10
+ * Detection is top-level only. That is sufficient here because none of the tool
11
+ * arg schemas use a top-level `.passthrough()`/catchall — free-form fields
12
+ * (`options`, `params`, `pdfOptions`) are named keys whose contents are
13
+ * intentionally unvalidated.
14
+ */
15
+ /**
16
+ * Resolve a Zod schema down to the field map of its underlying object schema,
17
+ * unwrapping wrappers (effects/optional/default/nullable/branded) defensively.
18
+ * Returns null when the schema is not (and does not wrap) a plain object schema,
19
+ * in which case we cannot determine the supported parameters and must not warn.
20
+ */
21
+ function getObjectShape(schema) {
22
+ let current = schema;
23
+ for (let i = 0; current && current._def && i < 10; i++) {
24
+ const typeName = current._def.typeName;
25
+ if (typeName === 'ZodObject') {
26
+ const shape = typeof current.shape === 'function' ? current.shape() : current.shape;
27
+ return shape ?? null;
28
+ }
29
+ if (typeName === 'ZodEffects')
30
+ current = current._def.schema;
31
+ else if (typeName === 'ZodOptional' || typeName === 'ZodNullable' || typeName === 'ZodDefault') {
32
+ current = typeof current._def.innerType === 'function' ? current._def.innerType() : current._def.innerType;
33
+ }
34
+ else if (typeName === 'ZodBranded')
35
+ current = current._def.type;
36
+ else
37
+ return null;
38
+ }
39
+ return null;
40
+ }
41
+ /** List the parameter names a tool schema accepts (empty array if it takes none). */
42
+ export function getSupportedParams(schema) {
43
+ const shape = getObjectShape(schema);
44
+ return shape ? Object.keys(shape) : [];
45
+ }
46
+ /**
47
+ * Return the names of top-level keys in `args` that the schema does not accept.
48
+ * Returns [] when args is not a plain object or the schema's params can't be
49
+ * determined (so we never warn on something we can't reason about).
50
+ */
51
+ export function detectUnsupportedParams(args, schema) {
52
+ if (!args || typeof args !== 'object' || Array.isArray(args))
53
+ return [];
54
+ const shape = getObjectShape(schema);
55
+ if (shape === null)
56
+ return [];
57
+ const supported = new Set(Object.keys(shape));
58
+ return Object.keys(args).filter((k) => !supported.has(k));
59
+ }
60
+ /** Build the corrective warning string shown to the model. */
61
+ export function buildUnsupportedParamsWarning(toolName, unsupported, supported) {
62
+ const ignored = unsupported.join(', ');
63
+ const supportedList = supported.length > 0 ? supported.join(', ') : '(none)';
64
+ return `You sent parameters not supported by this tool, which were ignored: ${ignored}. `
65
+ + `Supported parameters for ${toolName}: ${supportedList}.`;
66
+ }
@@ -0,0 +1,225 @@
1
+ import { configManager } from '../config-manager.js';
2
+ const TURN_OFF_FEEDBACK_INSTRUCTION = "*This request disappears after you give feedback or set feedbackGiven=true*";
3
+ // Tool categories mapping
4
+ const TOOL_CATEGORIES = {
5
+ filesystem: ['read_file', 'read_multiple_files', 'write_file', 'create_directory', 'list_directory', 'move_file', 'get_file_info'],
6
+ terminal: ['execute_command', 'read_output', 'force_terminate', 'list_sessions'],
7
+ edit: ['edit_block'],
8
+ search: ['start_search', 'get_more_search_results', 'stop_search', 'list_searches'],
9
+ config: ['get_config', 'set_config_value'],
10
+ process: ['list_processes', 'kill_process']
11
+ };
12
+ // Session timeout (30 minutes of inactivity = new session)
13
+ const SESSION_TIMEOUT = 30 * 60 * 1000;
14
+ class UsageTracker {
15
+ constructor() {
16
+ this.currentSession = null;
17
+ }
18
+ /**
19
+ * Get default usage stats
20
+ */
21
+ getDefaultStats() {
22
+ return {
23
+ filesystemOperations: 0,
24
+ terminalOperations: 0,
25
+ editOperations: 0,
26
+ searchOperations: 0,
27
+ configOperations: 0,
28
+ processOperations: 0,
29
+ totalToolCalls: 0,
30
+ successfulCalls: 0,
31
+ failedCalls: 0,
32
+ toolCounts: {},
33
+ firstUsed: Date.now(),
34
+ lastUsed: Date.now(),
35
+ totalSessions: 0, // Start at 0, incremented on first command
36
+ lastFeedbackPrompt: 0
37
+ };
38
+ }
39
+ /**
40
+ * Get current usage stats from config
41
+ */
42
+ async getStats() {
43
+ // Migrate old nested feedbackGiven to top-level if needed
44
+ const stats = await configManager.getValue('usageStats');
45
+ return stats || this.getDefaultStats();
46
+ }
47
+ /**
48
+ * Determine which category a tool belongs to
49
+ */
50
+ getToolCategory(toolName) {
51
+ for (const [category, tools] of Object.entries(TOOL_CATEGORIES)) {
52
+ if (tools.includes(toolName)) {
53
+ switch (category) {
54
+ case 'filesystem': return 'filesystemOperations';
55
+ case 'terminal': return 'terminalOperations';
56
+ case 'edit': return 'editOperations';
57
+ case 'search': return 'searchOperations';
58
+ case 'config': return 'configOperations';
59
+ case 'process': return 'processOperations';
60
+ }
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Check if we're in a new session
67
+ */
68
+ isNewSession() {
69
+ if (!this.currentSession)
70
+ return true;
71
+ const now = Date.now();
72
+ const timeSinceLastActivity = now - this.currentSession.lastActivity;
73
+ return timeSinceLastActivity > SESSION_TIMEOUT;
74
+ }
75
+ /**
76
+ * Update session tracking
77
+ */
78
+ updateSession() {
79
+ const now = Date.now();
80
+ if (this.isNewSession()) {
81
+ this.currentSession = {
82
+ sessionStart: now,
83
+ lastActivity: now,
84
+ commandsInSession: 1,
85
+ promptedThisSession: false // Reset for new session
86
+ };
87
+ }
88
+ else {
89
+ this.currentSession.lastActivity = now;
90
+ this.currentSession.commandsInSession++;
91
+ }
92
+ }
93
+ /**
94
+ * Track a successful tool call
95
+ */
96
+ async trackSuccess(toolName) {
97
+ this.updateSession();
98
+ const now = Date.now();
99
+ const category = this.getToolCategory(toolName);
100
+ const newSession = this.currentSession?.commandsInSession === 1;
101
+ return await configManager.updateValueNonBlocking('usageStats', (stored) => {
102
+ const defaults = { ...this.getDefaultStats(), firstUsed: now, lastUsed: now };
103
+ const stats = {
104
+ ...defaults,
105
+ ...(stored || {}),
106
+ toolCounts: { ...(stored?.toolCounts || {}) },
107
+ };
108
+ stats.totalToolCalls++;
109
+ stats.successfulCalls++;
110
+ stats.lastUsed = Math.max(stats.lastUsed || 0, now);
111
+ stats.toolCounts[toolName] = (stats.toolCounts[toolName] || 0) + 1;
112
+ if (category)
113
+ stats[category]++;
114
+ if (newSession)
115
+ stats.totalSessions++;
116
+ return stats;
117
+ });
118
+ }
119
+ /**
120
+ * Track a failed tool call
121
+ */
122
+ async trackFailure(toolName) {
123
+ this.updateSession();
124
+ const now = Date.now();
125
+ const category = this.getToolCategory(toolName);
126
+ const newSession = this.currentSession?.commandsInSession === 1;
127
+ return await configManager.updateValueNonBlocking('usageStats', (stored) => {
128
+ const defaults = { ...this.getDefaultStats(), firstUsed: now, lastUsed: now };
129
+ const stats = {
130
+ ...defaults,
131
+ ...(stored || {}),
132
+ toolCounts: { ...(stored?.toolCounts || {}) },
133
+ };
134
+ stats.totalToolCalls++;
135
+ stats.failedCalls++;
136
+ stats.lastUsed = Math.max(stats.lastUsed || 0, now);
137
+ stats.toolCounts[toolName] = (stats.toolCounts[toolName] || 0) + 1;
138
+ if (category)
139
+ stats[category]++;
140
+ if (newSession)
141
+ stats.totalSessions++;
142
+ return stats;
143
+ });
144
+ }
145
+ /**
146
+ * Check if user should be prompted for feedback based on usage patterns
147
+ */
148
+ async shouldPromptForFeedback() { return false; }
149
+ /**
150
+ * Get a random feedback prompt message with strong CTAs and clear actions
151
+ * NEW 2025 Survey - 3 variants for fresh analytics tracking
152
+ */
153
+ async getFeedbackPromptMessage() { return { variant: 'disabled', message: '' }; }
154
+ /**
155
+ * Check if user should be prompted for error feedback
156
+ */
157
+ async shouldPromptForErrorFeedback() { return false; }
158
+ /**
159
+ * Mark that user was prompted for feedback
160
+ */
161
+ async markFeedbackPrompted() {
162
+ const now = Date.now();
163
+ const today = new Date(now).toISOString().split('T')[0];
164
+ if (this.currentSession) {
165
+ this.currentSession.promptedThisSession = true;
166
+ }
167
+ await configManager.updateValueNonBlocking('usageStats', (stored) => {
168
+ const defaults = { ...this.getDefaultStats(), firstUsed: now, lastUsed: now };
169
+ const stats = {
170
+ ...defaults,
171
+ ...(stored || {}),
172
+ toolCounts: { ...(stored?.toolCounts || {}) },
173
+ };
174
+ stats.lastFeedbackPrompt = Math.max(stats.lastFeedbackPrompt || 0, now);
175
+ stats.lastFeedbackPromptDate = today;
176
+ stats.feedbackAttempts = (stats.feedbackAttempts || 0) + 1;
177
+ return stats;
178
+ });
179
+ }
180
+ /**
181
+ * Mark that user has given feedback
182
+ */
183
+ async markFeedbackGiven() {
184
+ // Set top-level config flag
185
+ await configManager.setValue('feedbackGiven', true);
186
+ }
187
+ /**
188
+ * Get usage summary for debugging/admin purposes
189
+ */
190
+ async getUsageSummary() {
191
+ const stats = await this.getStats();
192
+ const now = Date.now();
193
+ const daysSinceFirst = Math.round((now - stats.firstUsed) / (1000 * 60 * 60 * 24));
194
+ const uniqueTools = Object.keys(stats.toolCounts).length;
195
+ const successRate = stats.totalToolCalls > 0 ?
196
+ Math.round((stats.successfulCalls / stats.totalToolCalls) * 100) : 0;
197
+ const topTools = Object.entries(stats.toolCounts)
198
+ .sort(([, a], [, b]) => b - a)
199
+ .slice(0, 5)
200
+ .map(([tool, count]) => `${tool}: ${count}`)
201
+ .join(', ');
202
+ return `📊 **Usage Summary**
203
+ • Total calls: ${stats.totalToolCalls} (${stats.successfulCalls} successful, ${stats.failedCalls} failed)
204
+ • Success rate: ${successRate}%
205
+ • Days using: ${daysSinceFirst}
206
+ • Sessions: ${stats.totalSessions}
207
+ • Unique tools: ${uniqueTools}
208
+ • Most used: ${topTools || 'None'}
209
+ • Feedback given: ${(await configManager.getValue('feedbackGiven')) ? 'Yes' : 'No'}
210
+
211
+ **By Category:**
212
+ • Filesystem: ${stats.filesystemOperations}
213
+ • Terminal: ${stats.terminalOperations}
214
+ • Editing: ${stats.editOperations}
215
+ • Search: ${stats.searchOperations}
216
+ • Config: ${stats.configOperations}
217
+ • Process: ${stats.processOperations}`;
218
+ }
219
+ // Retained for the dormant prompt implementation, which is no longer exposed as an MCP tool.
220
+ async markPromptUsed(promptId, category) {
221
+ console.log(`[PROMPT USAGE] User retrieved prompt: ${promptId} (category: ${category})`);
222
+ }
223
+ }
224
+ // Export singleton instance
225
+ export const usageTracker = new UsageTracker();
@@ -0,0 +1,2 @@
1
+ export async function skipWelcomePageOnboarding() {}
2
+ export async function handleWelcomePageOnboarding() {}