min-agent 0.1.5 → 0.1.7

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.
@@ -41,6 +41,7 @@ export class ThinkingBodySplitter {
41
41
  drain(isFinal) {
42
42
  let display = "";
43
43
  let thinking = "";
44
+ let hadThinking = false;
44
45
  while (this.buf.length > 0) {
45
46
  const open = findFirstOpen(this.buf);
46
47
  if (!open) {
@@ -62,7 +63,11 @@ export class ThinkingBodySplitter {
62
63
  break;
63
64
  }
64
65
  if (open.index > 0) {
65
- display += this.buf.slice(0, open.index);
66
+ // Strip trailing newlines before thinking block
67
+ let pre = this.buf.slice(0, open.index);
68
+ pre = pre.replace(/\n+$/, "");
69
+ if (pre)
70
+ display += pre;
66
71
  this.buf = this.buf.slice(open.index);
67
72
  }
68
73
  const low = lower(this.buf);
@@ -81,8 +86,16 @@ export class ThinkingBodySplitter {
81
86
  }
82
87
  const inner = this.buf.slice(afterOpen, closeRel);
83
88
  thinking += inner;
89
+ hadThinking = true;
84
90
  this.buf = this.buf.slice(closeRel + open.tag.close.length);
85
91
  }
92
+ // Strip leading newlines from display that follow a thinking block
93
+ if (hadThinking && display.length === 0 && this.buf.startsWith("\n")) {
94
+ // Will be handled on next feed
95
+ }
96
+ if (hadThinking) {
97
+ display = display.replace(/^\n+/, "");
98
+ }
86
99
  return { display, thinking };
87
100
  }
88
101
  }
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { runAgent, runChat } from "./agent.js";
2
2
  import { loadMcpConfig, saveMcpConfig, checkMcpServer, checkAllMcpServers, formatMcpServerBinding, } from "./mcp.js";
3
3
  import { discoverSkills, getSkills } from "./skills.js";
4
4
  import { loadMemories, addMemory, deleteMemory, searchMemories } from "./memory.js";
5
- import { runSetup, isConfigured, loadConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
5
+ import { runSetup, isConfigured, loadConfig, saveConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
6
6
  import { setAutoApprove } from "./confirm.js";
7
7
  import { existsSync, writeFileSync, mkdirSync } from "fs";
8
8
  import path from "path";
@@ -207,6 +207,19 @@ async function main() {
207
207
  }
208
208
  case "mcp": {
209
209
  const subcommand = args[1];
210
+ if (subcommand === "-h" || subcommand === "--help" || !subcommand) {
211
+ console.log(`Usage: min-agent mcp <command>
212
+
213
+ Commands:
214
+ list List configured MCP servers (name + status)
215
+ info <name> Show details of a server
216
+ add <name> ... Add a new server (stdio or remote)
217
+ remove <name> Remove a server
218
+ enable <name> Enable a disabled server
219
+ disable <name> Disable a server
220
+ check Test connectivity of all servers`);
221
+ process.exit(0);
222
+ }
210
223
  switch (subcommand) {
211
224
  case "add": {
212
225
  const name = args[2];
@@ -275,15 +288,43 @@ async function main() {
275
288
  const servers = Object.entries(config.mcpServers);
276
289
  if (servers.length === 0) {
277
290
  console.log("No MCP servers configured.");
278
- console.log("Add one with: min-agent mcp add <name> <command...> or --url <https://...>");
291
+ console.log("Add one with: min-agent mcp add <name> <command...>");
279
292
  }
280
293
  else {
281
294
  console.log("MCP Servers:");
282
295
  for (const [name, cfg] of servers) {
283
- const status = cfg.enabled === false ? " (disabled)" : "";
284
- console.log(` ${name}: ${formatMcpServerBinding(cfg)}${status}`);
296
+ const status = cfg.enabled === false ? "\x1b[90mdisabled\x1b[0m" : "\x1b[32menabled\x1b[0m";
297
+ console.log(` ${name} ${status}`);
285
298
  }
299
+ console.log("\nUse: min-agent mcp info <name> for details");
300
+ }
301
+ break;
302
+ }
303
+ case "info": {
304
+ const name = args[2];
305
+ if (!name) {
306
+ console.error("Usage: min-agent mcp info <name>");
307
+ process.exit(1);
286
308
  }
309
+ const config = loadMcpConfig();
310
+ const cfg = config.mcpServers[name];
311
+ if (!cfg) {
312
+ console.error(`MCP server "${name}" not found`);
313
+ process.exit(1);
314
+ }
315
+ console.log(`Name: ${name}`);
316
+ console.log(`Status: ${cfg.enabled === false ? "disabled" : "enabled"}`);
317
+ console.log(`Binding: ${formatMcpServerBinding(cfg)}`);
318
+ if (cfg.command)
319
+ console.log(`Command: ${Array.isArray(cfg.command) ? cfg.command.join(" ") : `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim()}`);
320
+ if (cfg.url)
321
+ console.log(`URL: ${cfg.url}`);
322
+ if (cfg.token)
323
+ console.log(`Token: ***`);
324
+ if (cfg.environment)
325
+ console.log(`Env: ${Object.keys(cfg.environment).join(", ")}`);
326
+ if (cfg.timeout)
327
+ console.log(`Timeout: ${cfg.timeout}ms`);
287
328
  break;
288
329
  }
289
330
  case "check": {
@@ -317,8 +358,44 @@ async function main() {
317
358
  }
318
359
  break;
319
360
  }
361
+ case "enable": {
362
+ const names = args.slice(2);
363
+ if (names.length === 0) {
364
+ console.error("Usage: min-agent mcp enable <name...>");
365
+ process.exit(1);
366
+ }
367
+ const config = loadMcpConfig();
368
+ for (const name of names) {
369
+ if (!config.mcpServers[name]) {
370
+ console.error(`MCP server "${name}" not found`);
371
+ continue;
372
+ }
373
+ config.mcpServers[name].enabled = true;
374
+ console.log(`✓ MCP server "${name}" enabled`);
375
+ }
376
+ saveMcpConfig(config);
377
+ break;
378
+ }
379
+ case "disable": {
380
+ const names = args.slice(2);
381
+ if (names.length === 0) {
382
+ console.error("Usage: min-agent mcp disable <name...>");
383
+ process.exit(1);
384
+ }
385
+ const config = loadMcpConfig();
386
+ for (const name of names) {
387
+ if (!config.mcpServers[name]) {
388
+ console.error(`MCP server "${name}" not found`);
389
+ continue;
390
+ }
391
+ config.mcpServers[name].enabled = false;
392
+ console.log(`✓ MCP server "${name}" disabled`);
393
+ }
394
+ saveMcpConfig(config);
395
+ break;
396
+ }
320
397
  default:
321
- console.error("Usage: min-agent mcp [add|remove|list|check]");
398
+ console.error("Usage: min-agent mcp -h");
322
399
  process.exit(1);
323
400
  }
324
401
  break;
@@ -410,35 +487,89 @@ async function main() {
410
487
  }
411
488
  case "skills": {
412
489
  const subcommand = args[1];
490
+ if (subcommand === "-h" || subcommand === "--help" || !subcommand) {
491
+ console.log(`Usage: min-agent skills <command>
492
+
493
+ Commands:
494
+ list List skills (name + status)
495
+ info <name> Show details of a skill
496
+ enable <name> Enable a disabled skill
497
+ disable <name> Disable a skill`);
498
+ process.exit(0);
499
+ }
413
500
  switch (subcommand) {
501
+ case "info": {
502
+ const name = args[2];
503
+ if (!name) {
504
+ console.error("Usage: min-agent skills info <name>");
505
+ process.exit(1);
506
+ }
507
+ discoverSkills({ silent: true });
508
+ const { getSkill } = await import("./skills.js");
509
+ const skill = getSkill(name);
510
+ if (!skill) {
511
+ console.error(`Skill "${name}" not found`);
512
+ process.exit(1);
513
+ }
514
+ const config = loadConfig();
515
+ const isDisabled = (config.disabledSkills ?? []).includes(name);
516
+ console.log(`Name: ${skill.name}`);
517
+ console.log(`Status: ${isDisabled ? "disabled" : "enabled"}`);
518
+ console.log(`Description: ${skill.description}`);
519
+ console.log(`Location: ${skill.location}`);
520
+ break;
521
+ }
522
+ case "enable": {
523
+ const names = args.slice(2);
524
+ if (names.length === 0) {
525
+ console.error("Usage: min-agent skills enable <name...>");
526
+ process.exit(1);
527
+ }
528
+ const config = loadConfig();
529
+ config.disabledSkills = (config.disabledSkills ?? []).filter((s) => !names.includes(s));
530
+ saveConfig(config);
531
+ for (const name of names)
532
+ console.log(`✓ Skill "${name}" enabled`);
533
+ break;
534
+ }
535
+ case "disable": {
536
+ const names = args.slice(2);
537
+ if (names.length === 0) {
538
+ console.error("Usage: min-agent skills disable <name...>");
539
+ process.exit(1);
540
+ }
541
+ const config = loadConfig();
542
+ const disabled = config.disabledSkills ?? [];
543
+ for (const name of names) {
544
+ if (!disabled.includes(name))
545
+ disabled.push(name);
546
+ console.log(`✓ Skill "${name}" disabled`);
547
+ }
548
+ config.disabledSkills = disabled;
549
+ saveConfig(config);
550
+ break;
551
+ }
414
552
  case "list": {
415
- discoverSkills();
553
+ discoverSkills({ silent: true });
416
554
  const skills = getSkills();
417
- if (skills.length === 0) {
418
- console.log("No skills found.");
419
- console.log("Add skills by creating SKILL.md files in:");
420
- console.log(" ~/.agents/skills/<name>/SKILL.md (global, all projects)");
421
- console.log(" .min-agent/skills/<name>/SKILL.md");
422
- console.log(" .opencode/skills/<name>/SKILL.md");
423
- console.log("");
424
- console.log("SKILL.md format:");
425
- console.log(" ---");
426
- console.log(" name: my-skill");
427
- console.log(" description: What this skill does");
428
- console.log(" ---");
429
- console.log(" # Instructions content...");
555
+ const config = loadConfig();
556
+ const disabledNames = config.disabledSkills ?? [];
557
+ if (skills.length === 0 && disabledNames.length === 0) {
558
+ console.log("No skills found. Add SKILL.md files in .min-agent/skills/<name>/");
559
+ break;
430
560
  }
431
- else {
432
- console.log("Available Skills:");
433
- for (const skill of skills) {
434
- console.log(` ${skill.name}: ${skill.description}`);
435
- console.log(` ${skill.location}`);
436
- }
561
+ console.log("Skills:");
562
+ for (const skill of skills) {
563
+ console.log(` ${skill.name} \x1b[32menabled\x1b[0m`);
564
+ }
565
+ for (const n of disabledNames) {
566
+ console.log(` ${n} \x1b[90mdisabled\x1b[0m`);
437
567
  }
568
+ console.log("\nUse: min-agent skills info <name> for details");
438
569
  break;
439
570
  }
440
571
  default:
441
- console.error("Usage: min-agent skills [list]");
572
+ console.error("Usage: min-agent skills -h");
442
573
  process.exit(1);
443
574
  }
444
575
  break;
package/dist/confirm.js CHANGED
@@ -1,4 +1,3 @@
1
- import readline from "readline";
2
1
  let autoApprove = false;
3
2
  export function setAutoApprove(value) {
4
3
  autoApprove = value;
@@ -10,12 +9,22 @@ export function isAutoApprove() {
10
9
  export async function confirm(message) {
11
10
  if (autoApprove)
12
11
  return true;
13
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ process.stdout.write(`\n\x1b[33m⚠ ${message} [y/N] \x1b[0m`);
14
13
  return new Promise((resolve) => {
15
- rl.question(`\x1b[33m⚠ ${message} [y/N] \x1b[0m`, (answer) => {
16
- rl.close();
17
- resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
18
- });
14
+ const wasRaw = process.stdin.isRaw;
15
+ if (process.stdin.isTTY)
16
+ process.stdin.setRawMode(true);
17
+ const onData = (buf) => {
18
+ const ch = buf.toString();
19
+ process.stdin.removeListener("data", onData);
20
+ if (process.stdin.isTTY)
21
+ process.stdin.setRawMode(wasRaw ?? false);
22
+ // Echo the character and newline
23
+ process.stdout.write(ch === "\r" || ch === "\n" ? "\n" : `${ch}\n`);
24
+ const answer = ch.trim().toLowerCase();
25
+ resolve(answer === "y");
26
+ };
27
+ process.stdin.on("data", onData);
19
28
  });
20
29
  }
21
30
  /** Check if a shell command is potentially dangerous */
package/dist/markdown.js CHANGED
@@ -37,6 +37,8 @@ export class MarkdownRenderer {
37
37
  buffer = "";
38
38
  inCodeBlock = false;
39
39
  codeLang = "";
40
+ tableRows = [];
41
+ inTable = false;
40
42
  /** Process a text delta and return formatted output */
41
43
  write(text) {
42
44
  this.buffer += text;
@@ -49,19 +51,161 @@ export class MarkdownRenderer {
49
51
  break;
50
52
  const line = this.buffer.slice(0, nlIdx);
51
53
  this.buffer = this.buffer.slice(nlIdx + 1);
54
+ // Table handling: collect rows, render when table ends
55
+ if (this.isTableRow(line)) {
56
+ if (this.isTableSeparator(line)) {
57
+ this.inTable = true;
58
+ continue;
59
+ }
60
+ this.inTable = true;
61
+ this.tableRows.push(this.parseTableRow(line));
62
+ continue;
63
+ }
64
+ // Table just ended — flush it
65
+ if (this.inTable) {
66
+ output += this.renderTable(c);
67
+ this.tableRows = [];
68
+ this.inTable = false;
69
+ }
52
70
  output += this.formatLine(line, c) + "\n";
53
71
  }
72
+ // Stream partial line immediately for real-time feel
73
+ // Hold back only if it could be start of table or code fence
74
+ if (!this.inTable && !this.inCodeBlock && this.buffer.length > 0) {
75
+ if (!this.buffer.startsWith("|") && !this.buffer.startsWith("`")) {
76
+ const partial = this.buffer;
77
+ this.buffer = "";
78
+ output += this.formatInline(partial, c);
79
+ }
80
+ }
81
+ // If we're in a table and buffer starts with "|", hold it (waiting for \n)
82
+ // If we're in a table and buffer does NOT start with "|", the table ended mid-stream
83
+ if (this.inTable && this.buffer.length > 0 && !this.buffer.startsWith("|")) {
84
+ output += this.renderTable(c);
85
+ this.tableRows = [];
86
+ this.inTable = false;
87
+ // Now output the non-table buffer content
88
+ if (this.buffer.length > 0 && !this.buffer.startsWith("`")) {
89
+ const partial = this.buffer;
90
+ this.buffer = "";
91
+ output += this.formatInline(partial, c);
92
+ }
93
+ }
54
94
  return output;
55
95
  }
56
96
  /** Flush remaining buffer */
57
97
  flush() {
58
- if (!this.buffer)
59
- return "";
60
98
  const c = useColor() ? C : Z;
61
- const out = this.formatLine(this.buffer, c);
99
+ let out = "";
100
+ // If buffer has a pending table row, add it
101
+ if (this.inTable && this.buffer.length > 0) {
102
+ if (this.isTableRow(this.buffer)) {
103
+ if (!this.isTableSeparator(this.buffer)) {
104
+ this.tableRows.push(this.parseTableRow(this.buffer));
105
+ }
106
+ this.buffer = "";
107
+ }
108
+ }
109
+ // Flush pending table
110
+ if (this.inTable && this.tableRows.length > 0) {
111
+ out += this.renderTable(c);
112
+ this.tableRows = [];
113
+ this.inTable = false;
114
+ }
115
+ if (!this.buffer)
116
+ return out;
117
+ out += this.formatLine(this.buffer, c);
62
118
  this.buffer = "";
63
119
  return out;
64
120
  }
121
+ isTableRow(line) {
122
+ const trimmed = line.trim();
123
+ return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|", 1);
124
+ }
125
+ isTableSeparator(line) {
126
+ return /^\s*\|[\s:]*-+[\s:|-]*\|\s*$/.test(line);
127
+ }
128
+ parseTableRow(line) {
129
+ return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
130
+ }
131
+ /** Get display width of a string (CJK/emoji chars = 2, others = 1) */
132
+ displayWidth(str) {
133
+ let width = 0;
134
+ for (const ch of str) {
135
+ const code = ch.codePointAt(0) ?? 0;
136
+ if (
137
+ // CJK
138
+ (code >= 0x1100 && code <= 0x115f) ||
139
+ (code >= 0x2e80 && code <= 0x303e) ||
140
+ (code >= 0x3040 && code <= 0x33bf) ||
141
+ (code >= 0x3400 && code <= 0x4dbf) ||
142
+ (code >= 0x4e00 && code <= 0x9fff) ||
143
+ (code >= 0xa000 && code <= 0xa4cf) ||
144
+ (code >= 0xac00 && code <= 0xd7af) ||
145
+ (code >= 0xf900 && code <= 0xfaff) ||
146
+ (code >= 0xfe30 && code <= 0xfe6f) ||
147
+ (code >= 0xff01 && code <= 0xff60) ||
148
+ (code >= 0xffe0 && code <= 0xffe6) ||
149
+ (code >= 0x20000 && code <= 0x2fffd) ||
150
+ (code >= 0x30000 && code <= 0x3fffd) ||
151
+ // Emoji
152
+ (code >= 0x1f300 && code <= 0x1f9ff) || // Misc Symbols, Emoticons, Dingbats, etc.
153
+ (code >= 0x1fa00 && code <= 0x1faff) || // Chess, Extended-A
154
+ (code >= 0x2600 && code <= 0x27bf) || // Misc Symbols, Dingbats
155
+ (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors (skip width)
156
+ (code >= 0x200d && code <= 0x200d) || // ZWJ (skip width)
157
+ (code >= 0x1f1e0 && code <= 0x1f1ff) // Regional Indicators (flags)
158
+ ) {
159
+ // Variation selectors and ZWJ are zero-width joiners
160
+ if ((code >= 0xfe00 && code <= 0xfe0f) || code === 0x200d) {
161
+ width += 0;
162
+ }
163
+ else {
164
+ width += 2;
165
+ }
166
+ }
167
+ else {
168
+ width += 1;
169
+ }
170
+ }
171
+ return width;
172
+ }
173
+ /** Pad string to target display width */
174
+ padToWidth(str, targetWidth) {
175
+ const currentWidth = this.displayWidth(str);
176
+ const padding = targetWidth - currentWidth;
177
+ return padding > 0 ? str + " ".repeat(padding) : str;
178
+ }
179
+ renderTable(c) {
180
+ if (this.tableRows.length === 0)
181
+ return "";
182
+ // Calculate column widths based on display width (CJK-aware)
183
+ const colCount = Math.max(...this.tableRows.map((r) => r.length));
184
+ const widths = Array(colCount).fill(0);
185
+ for (const row of this.tableRows) {
186
+ for (let i = 0; i < row.length; i++) {
187
+ widths[i] = Math.max(widths[i], this.displayWidth(row[i] ?? ""));
188
+ }
189
+ }
190
+ const lines = [];
191
+ const top = `${c.dim}┌${widths.map((w) => "─".repeat(w + 2)).join("┬")}┐${c.reset}`;
192
+ const mid = `${c.dim}├${widths.map((w) => "─".repeat(w + 2)).join("┼")}┤${c.reset}`;
193
+ const bot = `${c.dim}└${widths.map((w) => "─".repeat(w + 2)).join("┴")}┘${c.reset}`;
194
+ lines.push(top);
195
+ for (let r = 0; r < this.tableRows.length; r++) {
196
+ const row = this.tableRows[r];
197
+ const cells = widths.map((w, i) => {
198
+ const cell = row[i] ?? "";
199
+ const padded = this.padToWidth(cell, w);
200
+ return r === 0 ? `${c.bold}${padded}${c.reset}` : padded;
201
+ });
202
+ lines.push(`${c.dim}│${c.reset} ${cells.join(` ${c.dim}│${c.reset} `)} ${c.dim}│${c.reset}`);
203
+ if (r === 0)
204
+ lines.push(mid);
205
+ }
206
+ lines.push(bot);
207
+ return lines.join("\n") + "\n";
208
+ }
65
209
  formatInline(line, c) {
66
210
  // Split by inline code spans; format outside segments only
67
211
  const parts = line.split(/(`[^`]*`)/g);
package/dist/mcp.js CHANGED
@@ -7,24 +7,47 @@ import { tool, jsonSchema } from "ai";
7
7
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
8
8
  import path from "path";
9
9
  import { truncateToolOutput } from "./tool-output.js";
10
+ import { getConfigDir } from "./config.js";
10
11
  const DEFAULT_TIMEOUT = 30000;
11
12
  function getMcpConfigPath() {
12
- return path.join(process.cwd(), ".min-agent", "mcp.json");
13
+ // Check project-local first, then global
14
+ const local = path.join(process.cwd(), ".min-agent", "mcp.json");
15
+ if (existsSync(local))
16
+ return local;
17
+ return path.join(getConfigDir(), "mcp.json");
18
+ }
19
+ function getMcpConfigWritePath() {
20
+ // Write to project-local if it exists, otherwise global
21
+ const local = path.join(process.cwd(), ".min-agent", "mcp.json");
22
+ if (existsSync(path.dirname(local)))
23
+ return local;
24
+ return path.join(getConfigDir(), "mcp.json");
13
25
  }
14
26
  let connectedServers = {};
15
27
  export function loadMcpConfig() {
16
- const configPath = getMcpConfigPath();
17
- if (!existsSync(configPath))
18
- return { mcpServers: {} };
19
- try {
20
- return JSON.parse(readFileSync(configPath, "utf-8"));
28
+ const globalPath = path.join(getConfigDir(), "mcp.json");
29
+ const localPath = path.join(process.cwd(), ".min-agent", "mcp.json");
30
+ let config = { mcpServers: {} };
31
+ // Load global first
32
+ if (existsSync(globalPath)) {
33
+ try {
34
+ const global = JSON.parse(readFileSync(globalPath, "utf-8"));
35
+ config.mcpServers = { ...config.mcpServers, ...global.mcpServers };
36
+ }
37
+ catch { }
21
38
  }
22
- catch {
23
- return { mcpServers: {} };
39
+ // Local overrides global
40
+ if (existsSync(localPath) && localPath !== globalPath) {
41
+ try {
42
+ const local = JSON.parse(readFileSync(localPath, "utf-8"));
43
+ config.mcpServers = { ...config.mcpServers, ...local.mcpServers };
44
+ }
45
+ catch { }
24
46
  }
47
+ return config;
25
48
  }
26
49
  export function saveMcpConfig(config) {
27
- const configPath = getMcpConfigPath();
50
+ const configPath = path.join(getConfigDir(), "mcp.json");
28
51
  const dir = path.dirname(configPath);
29
52
  mkdirSync(dir, { recursive: true });
30
53
  writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
@@ -43,7 +66,21 @@ function buildRemoteRequestInit(config) {
43
66
  return { headers };
44
67
  }
45
68
  async function openStdioMcpServer(name, config) {
46
- const [cmd, ...args] = config.command ?? [];
69
+ // Support both formats:
70
+ // { command: ["uvx", "mcp-server-time"] } — min-agent native
71
+ // { command: "uvx", args: ["mcp-server-time"] } — opencode/claude style
72
+ let cmd;
73
+ let args;
74
+ if (Array.isArray(config.command)) {
75
+ [cmd, ...args] = config.command;
76
+ }
77
+ else if (typeof config.command === "string") {
78
+ cmd = config.command;
79
+ args = config.args ?? [];
80
+ }
81
+ else {
82
+ throw new Error(`MCP "${name}" has empty command`);
83
+ }
47
84
  if (!cmd) {
48
85
  throw new Error(`MCP "${name}" has empty command`);
49
86
  }
@@ -131,13 +168,18 @@ export function formatMcpServerBinding(config) {
131
168
  const mode = config.remoteTransport ?? "auto";
132
169
  return `${config.url} [remote:${mode}]`;
133
170
  }
134
- return (config.command ?? []).join(" ");
171
+ const cmd = Array.isArray(config.command) ? config.command.join(" ") : `${config.command ?? ""} ${(config.args ?? []).join(" ")}`.trim();
172
+ return cmd || "(no command)";
135
173
  }
136
174
  export async function connectMcpServer(name, config) {
137
175
  if (config.enabled === false)
138
176
  return null;
177
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT;
139
178
  try {
140
- const server = await openMcpServer(name, config);
179
+ const server = await Promise.race([
180
+ openMcpServer(name, config),
181
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`connection timed out after ${timeout}ms`)), timeout)),
182
+ ]);
141
183
  console.log(`\x1b[90m MCP "${name}" connected (${server.tools.length} tools)\x1b[0m`);
142
184
  return server;
143
185
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Paste handler for large text input.
3
+ *
4
+ * When the user pastes large content (multi-line or >150 chars),
5
+ * shows a collapsed summary in the terminal but preserves the full
6
+ * text for sending to the model.
7
+ *
8
+ * Based on opencode's paste handling in the TUI prompt component.
9
+ */
10
+ const PASTE_LINE_THRESHOLD = 3;
11
+ const PASTE_CHAR_THRESHOLD = 150;
12
+ const PREVIEW_LINES = 3;
13
+ /**
14
+ * Process input text and detect if it's a large paste.
15
+ * Returns the full text plus display metadata.
16
+ */
17
+ export function processPastedInput(text) {
18
+ const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
19
+ if (lineCount < PASTE_LINE_THRESHOLD && text.length <= PASTE_CHAR_THRESHOLD) {
20
+ return { fullText: text, isLargePaste: false };
21
+ }
22
+ const lines = text.split("\n");
23
+ const preview = lines.slice(0, PREVIEW_LINES).join("\n");
24
+ const remaining = lineCount - PREVIEW_LINES;
25
+ return {
26
+ fullText: text,
27
+ isLargePaste: true,
28
+ summary: remaining > 0
29
+ ? `${preview}\n\x1b[90m ... (${remaining} more lines, ~${text.length} chars total)\x1b[0m`
30
+ : `\x1b[90m[Pasted ${text.length} chars]\x1b[0m`,
31
+ };
32
+ }
33
+ /**
34
+ * Print paste feedback to the user.
35
+ */
36
+ export function printPasteFeedback(result) {
37
+ if (!result.isLargePaste)
38
+ return;
39
+ const lineCount = (result.fullText.match(/\n/g)?.length ?? 0) + 1;
40
+ console.log(`\x1b[90m 📋 Pasted ~${lineCount} lines (${result.fullText.length} chars)\x1b[0m`);
41
+ }