ikie-cli 0.1.32 → 0.1.34

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/skills.js CHANGED
@@ -11,16 +11,38 @@ function parseFrontmatter(raw) {
11
11
  if (!m)
12
12
  return { meta: {}, body: text.trim() };
13
13
  const meta = {};
14
- for (const line of m[1].split(/\r?\n/)) {
14
+ const lines = m[1].split(/\r?\n/);
15
+ let i = 0;
16
+ while (i < lines.length) {
17
+ const line = lines[i];
15
18
  const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
16
- if (!kv)
19
+ if (!kv) {
20
+ i++;
17
21
  continue;
22
+ }
23
+ const key = kv[1].toLowerCase();
18
24
  let value = kv[2].trim();
25
+ // YAML list continuation: values that end without content may start a block list.
26
+ if (value === '') {
27
+ const listValues = [];
28
+ i++;
29
+ while (i < lines.length && /^\s*-\s+/.test(lines[i])) {
30
+ listValues.push(lines[i].replace(/^\s*-\s+/, '').trim());
31
+ i++;
32
+ }
33
+ if (listValues.length) {
34
+ meta[key] = listValues.join(', ');
35
+ continue;
36
+ }
37
+ meta[key] = '';
38
+ continue;
39
+ }
19
40
  // Strip matching surrounding quotes.
20
41
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
21
42
  value = value.slice(1, -1);
22
43
  }
23
- meta[kv[1].toLowerCase()] = value;
44
+ meta[key] = value;
45
+ i++;
24
46
  }
25
47
  return { meta, body: (m[2] ?? '').trim() };
26
48
  }
@@ -87,6 +109,14 @@ export function discoverSkills(cwd = process.cwd()) {
87
109
  const key = name.toLowerCase();
88
110
  if (!name || byName.has(key))
89
111
  continue;
112
+ const allowedTools = parseAllowedTools(meta['allowed-tools'] || meta.allowedtools || '');
113
+ const disallowedTools = parseAllowedTools(meta['disallowed-tools'] || meta.disallowedtools || '');
114
+ const disableModelInvocation = truthy(meta['disable-model-invocation'] || meta.disablemodelinvocation);
115
+ const userInvocable = truthy(meta['user-invocable'] || meta.userinvocable);
116
+ const whenToUse = (meta['when_to_use'] || meta['when-to-use'] || meta.whentouse || '').trim();
117
+ // Skills marked disable-model-invocation are not offered to the model.
118
+ if (disableModelInvocation)
119
+ continue;
90
120
  byName.set(key, {
91
121
  name,
92
122
  description,
@@ -95,6 +125,11 @@ export function discoverSkills(cwd = process.cwd()) {
95
125
  file,
96
126
  source: root.source,
97
127
  origin: root.origin,
128
+ allowedTools,
129
+ disallowedTools,
130
+ disableModelInvocation,
131
+ userInvocable,
132
+ whenToUse,
98
133
  });
99
134
  }
100
135
  }
@@ -141,20 +176,62 @@ export function listSkillFiles(skill) {
141
176
  walk(skill.dir, '');
142
177
  return out.sort();
143
178
  }
179
+ function truthy(v) {
180
+ return /^(true|yes|1|on)$/i.test((v ?? '').trim());
181
+ }
182
+ function parseAllowedTools(raw) {
183
+ const v = raw.trim();
184
+ if (!v)
185
+ return undefined;
186
+ return v.split(/[,\s]+/).map(t => t.trim()).filter(Boolean);
187
+ }
144
188
  /**
145
189
  * Render the skill catalog for the system prompt: each skill's name +
146
190
  * description, so the model knows what exists and when to reach for one.
147
191
  * Returns '' when no skills are installed (so the section is omitted).
192
+ * Skips skills marked disable-model-invocation.
148
193
  */
149
194
  export function formatSkillsForPrompt(skills) {
150
- if (!skills.length)
195
+ const visible = skills.filter(s => !s.disableModelInvocation);
196
+ if (!visible.length)
151
197
  return '';
152
- const lines = skills.map(s => {
198
+ const lines = visible.map(s => {
153
199
  const desc = s.description || '(no description)';
154
- return `- **${s.name}** — ${desc}`;
200
+ const when = s.whenToUse ? ` when to use: ${s.whenToUse}` : '';
201
+ return `- **${s.name}** — ${desc}${when}`;
155
202
  });
156
203
  return lines.join('\n');
157
204
  }
205
+ /**
206
+ * Map Claude-style allowed-tools tokens to ikie tool names.
207
+ * Strips arguments, lowercases, and maps known tokens.
208
+ */
209
+ export function mapAllowedTools(tokens) {
210
+ if (!tokens?.length)
211
+ return [];
212
+ const map = {
213
+ read: 'read_file',
214
+ write: 'write_file',
215
+ edit: 'edit_file',
216
+ bash: 'bash',
217
+ grep: 'grep',
218
+ glob: 'search_files',
219
+ };
220
+ const out = [];
221
+ for (const raw of tokens) {
222
+ const token = raw.replace(/\([^)]*\)/g, '').trim().toLowerCase();
223
+ if (!token)
224
+ continue;
225
+ if (token.startsWith('mcp__')) {
226
+ out.push(token);
227
+ continue;
228
+ }
229
+ const mapped = map[token] ?? token;
230
+ if (!out.includes(mapped))
231
+ out.push(mapped);
232
+ }
233
+ return out;
234
+ }
158
235
  /**
159
236
  * Build the full text returned to the model when it loads a skill via
160
237
  * `use_skill`: the instructions plus a manifest of bundled files and the
package/dist/theme.d.ts CHANGED
@@ -80,7 +80,7 @@ export declare function toolMeta(rawName: string): {
80
80
  };
81
81
  export declare function toolLine(name: string, args: string): string;
82
82
  /** Multi-line output block shown after a tool runs. */
83
- export declare function toolOutputBlock(result: string, ms: number, indent?: string): string;
83
+ export declare function toolOutputBlock(result: string, ms: number, indent?: string, savePath?: string): string;
84
84
  interface DiffOpts {
85
85
  path?: string;
86
86
  indent?: string;
package/dist/theme.js CHANGED
@@ -397,12 +397,16 @@ export function toolLine(name, args) {
397
397
  return `${tint('●')} ${c.white.bold(verb + badge)}${c.dim('(')}${c.muted(args)}${c.dim(')')}`;
398
398
  }
399
399
  /** Multi-line output block shown after a tool runs. */
400
- export function toolOutputBlock(result, ms, indent = ' ') {
400
+ export function toolOutputBlock(result, ms, indent = ' ', savePath) {
401
401
  const time = c.muted(formatDuration(ms));
402
+ // Skip display if output was already streamed
403
+ if (result.includes('__STREAMED__')) {
404
+ return `${indent}${c.muted('⎿')} ${time}`;
405
+ }
402
406
  const lines = result.split('\n').filter(l => l.trim() !== '');
403
407
  if (!lines.length)
404
408
  return `${indent}${c.muted('⎿')} ${time}`;
405
- const MAX = 5;
409
+ const MAX = 10;
406
410
  const shown = lines.slice(0, MAX);
407
411
  const hidden = lines.length - MAX;
408
412
  const cont = indent + ' ';
@@ -412,7 +416,13 @@ export function toolOutputBlock(result, ms, indent = ' ') {
412
416
  out.push(`${cont}${c.dim(clampLine(shown[i]))}`);
413
417
  }
414
418
  if (hidden > 0) {
415
- out.push(`${cont}${c.muted(`… +${hidden} lines`)}`);
419
+ const outputId = Math.random().toString(36).substring(7);
420
+ const savedOutputs = global;
421
+ if (!savedOutputs._ikieOutputs)
422
+ savedOutputs._ikieOutputs = new Map();
423
+ savedOutputs._ikieOutputs.set(outputId, result);
424
+ out.push(`${cont}${c.muted(`… +${hidden} lines`)} ${c.dim('│')} ${c.info('Full output available')}`);
425
+ out.push(`${cont}${c.muted('└─')} ${c.accent('ikie')} ${c.muted('show-output')} ${c.secondary(outputId)} ${c.dim('# View full output')}`);
416
426
  }
417
427
  return out.join('\n');
418
428
  }
@@ -528,7 +538,14 @@ export function toolDiffBlock(oldStr, newStr, ms, opts = {}) {
528
538
  }
529
539
  }
530
540
  const out = [];
531
- out.push(`${indent}${c.muted('⎿')} ${time} ${c.dim(summary)}`);
541
+ // Enhanced header with file path
542
+ if (opts.path) {
543
+ out.push(`${indent}${c.muted('⎿')} ${time} ${c.info('📝')} ${c.white.bold(opts.path)}`);
544
+ out.push(`${indent} ${c.dim(summary)}`);
545
+ }
546
+ else {
547
+ out.push(`${indent}${c.muted('⎿')} ${time} ${c.dim(summary)}`);
548
+ }
532
549
  const MAX = 16;
533
550
  const codeWidth = cols - indent.length - gw - 4; // gutter + " x " marker + space
534
551
  const render = rowsRaw.slice(0, MAX);
package/dist/tools.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type OpenAI from 'openai';
2
2
  export declare const TOOL_DEFS: OpenAI.Chat.ChatCompletionTool[];
3
+ export declare function getMcpToolDefs(): OpenAI.Chat.ChatCompletionTool[];
3
4
  export declare const SAFE_TOOLS: Set<string>;
4
5
  export declare const PLAN_TOOLS: Set<string>;
5
6
  export declare function isRestrictedPath(path: string): boolean;
package/dist/tools.js CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSy
3
3
  import { dirname, join, resolve } from 'path';
4
4
  import { promisify } from 'util';
5
5
  import { glob } from 'glob';
6
+ import { getMcpManager, parseClaudeMcpAddCommand } from './mcp-manager.js';
6
7
  const execAsync = promisify(exec);
7
8
  // ─── OpenAI-format Tool Definitions ──────────────────────────────────────────
8
9
  export const TOOL_DEFS = [
@@ -64,6 +65,7 @@ export const TOOL_DEFS = [
64
65
  command: { type: 'string', description: 'Shell command' },
65
66
  timeout_ms: { type: 'number', description: 'Timeout ms (default 30000)' },
66
67
  cwd: { type: 'string', description: 'Working directory' },
68
+ interactive: { type: 'boolean', description: 'Set true for commands that need an interactive terminal — ones that show arrow-key menus or prompts that cannot be skipped with flags (e.g. `shadcn init`, `create-next-app`, `npm init` without -y). The command is connected to the real terminal so the USER answers the prompts directly. Output is shown live, not captured, so the result only reports the exit status — read any files it creates afterward.' },
67
69
  },
68
70
  required: ['command'],
69
71
  },
@@ -328,28 +330,11 @@ export const TOOL_DEFS = [
328
330
  },
329
331
  },
330
332
  },
331
- {
332
- type: 'function',
333
- function: {
334
- name: 'mcp_install',
335
- description: 'Install a new MCP (Model Context Protocol) server to extend capabilities. MCPs provide specialized tools like GitHub API, database access, browser automation, etc.',
336
- parameters: {
337
- type: 'object',
338
- properties: {
339
- name: { type: 'string', description: 'Unique name for this MCP' },
340
- source: { type: 'string', description: 'Source: npm package (npm:package-name), git URL, or local path' },
341
- description: { type: 'string', description: 'Description of what this MCP does' },
342
- autoStart: { type: 'boolean', description: 'Start automatically on ikie launch (default: false)' },
343
- },
344
- required: ['name', 'source'],
345
- },
346
- },
347
- },
348
333
  {
349
334
  type: 'function',
350
335
  function: {
351
336
  name: 'mcp_list',
352
- description: 'List all installed MCP servers and their available tools.',
337
+ description: 'List all configured MCP servers and their available tools.',
353
338
  parameters: {
354
339
  type: 'object',
355
340
  properties: {},
@@ -357,50 +342,6 @@ export const TOOL_DEFS = [
357
342
  },
358
343
  },
359
344
  },
360
- {
361
- type: 'function',
362
- function: {
363
- name: 'mcp_start',
364
- description: 'Start an installed MCP server to make its tools available.',
365
- parameters: {
366
- type: 'object',
367
- properties: {
368
- name: { type: 'string', description: 'Name of the MCP server to start' },
369
- },
370
- required: ['name'],
371
- },
372
- },
373
- },
374
- {
375
- type: 'function',
376
- function: {
377
- name: 'mcp_stop',
378
- description: 'Stop a running MCP server.',
379
- parameters: {
380
- type: 'object',
381
- properties: {
382
- name: { type: 'string', description: 'Name of the MCP server to stop' },
383
- },
384
- required: ['name'],
385
- },
386
- },
387
- },
388
- {
389
- type: 'function',
390
- function: {
391
- name: 'mcp_call',
392
- description: 'Call a tool from a running MCP server. Use mcp_list first to see available tools and their parameters.',
393
- parameters: {
394
- type: 'object',
395
- properties: {
396
- server: { type: 'string', description: 'MCP server name' },
397
- tool: { type: 'string', description: 'Tool name from the MCP server' },
398
- arguments: { type: 'object', description: 'Arguments to pass to the tool' },
399
- },
400
- required: ['server', 'tool', 'arguments'],
401
- },
402
- },
403
- },
404
345
  {
405
346
  type: 'function',
406
347
  function: {
@@ -418,29 +359,20 @@ export const TOOL_DEFS = [
418
359
  },
419
360
  },
420
361
  },
421
- {
422
- type: 'function',
423
- function: {
424
- name: 'mcp_uninstall',
425
- description: 'Uninstall an MCP server (cannot uninstall built-in MCPs).',
426
- parameters: {
427
- type: 'object',
428
- properties: {
429
- name: { type: 'string', description: 'Name of the MCP server to uninstall' },
430
- },
431
- required: ['name'],
432
- },
433
- },
434
- },
435
362
  ];
363
+ // ─── MCP first-class tool definitions ─────────────────────────────────────────
364
+ export function getMcpToolDefs() {
365
+ return getMcpManager().getToolDefsSync();
366
+ }
436
367
  // ─── Safe tools (auto-approved) ───────────────────────────────────────────────
437
368
  // spawn_agent is "safe" at the dispatch layer — the tools the sub-agent itself
438
369
  // runs go through their own approval inside the sub-agent loop.
439
- export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list', 'mcp_start', 'mcp_stop', 'mcp_call', 'mcp_add']);
370
+ export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list']);
440
371
  // Tools available in PLAN mode — read-only exploration plus delegation/questions.
441
372
  // Everything that mutates the filesystem or runs commands (write_file, edit_file,
442
373
  // bash, memory_write) is intentionally excluded so plan mode can only research.
443
- export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list', 'mcp_call', 'mcp_add']);
374
+ // MCP tools are also excluded because we cannot prove they are read-only.
375
+ export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list']);
444
376
  // Paths that may contain secrets, credentials, or system configuration.
445
377
  // Reading these requires explicit user permission even though read_file is normally safe.
446
378
  const RESTRICTED_PATTERNS = [
@@ -524,8 +456,17 @@ export function formatToolArgs(name, input) {
524
456
  return `"${p(input.source)}"${input.force ? ' --force' : ''}`;
525
457
  case 'remove_skill':
526
458
  return input.name ? `"${p(input.name)}"` : '(list installed)';
527
- default:
459
+ case 'mcp_list':
460
+ return '(status)';
461
+ case 'mcp_add':
462
+ return `"${p(input.name)}"`;
463
+ default: {
464
+ if (name.startsWith('mcp__')) {
465
+ const parts = name.split('__');
466
+ return `${parts[1] ?? '?'}.${parts[2] ?? '?'}`;
467
+ }
528
468
  return JSON.stringify(input).slice(0, 80);
469
+ }
529
470
  }
530
471
  }
531
472
  // ─── Security Validation Functions ───────────────────────────────────────────
@@ -623,6 +564,9 @@ async function bash(input) {
623
564
  cwd = resolve(input.cwd);
624
565
  }
625
566
  const command = input.command.trim();
567
+ if (input.interactive) {
568
+ return bashInteractive(command, cwd);
569
+ }
626
570
  if (command.endsWith('&')) {
627
571
  const bgCmd = command.slice(0, -1).trim();
628
572
  try {
@@ -645,6 +589,11 @@ async function bash(input) {
645
589
  }
646
590
  const maxTimeout = 300000;
647
591
  const timeout = Math.min(input.timeout_ms ?? 60000, maxTimeout);
592
+ // For build commands or when stream is enabled, use streaming output
593
+ const shouldStream = input.stream || /\b(build|compile|test|deploy|install)\b/i.test(command);
594
+ if (shouldStream) {
595
+ return bashStreaming(command, cwd, timeout);
596
+ }
648
597
  try {
649
598
  const { stdout, stderr } = await execAsync(command, {
650
599
  cwd,
@@ -670,6 +619,126 @@ async function bash(input) {
670
619
  return `Exit ${e.code ?? 1}\n${parts.join('\n')}`;
671
620
  }
672
621
  }
622
+ function bashStreaming(command, cwd, timeout) {
623
+ return new Promise((resolve, reject) => {
624
+ const isWindows = process.platform === 'win32';
625
+ const child = spawn(isWindows ? 'cmd.exe' : 'bash', isWindows ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, timeout });
626
+ let stdout = '';
627
+ let stderr = '';
628
+ const indent = ' ';
629
+ const MAX_LINES_SHOWN = 3;
630
+ let linesShown = 0;
631
+ let totalLines = 0;
632
+ // Print a blank line before streaming output
633
+ process.stdout.write('\n');
634
+ // Stream stdout line by line - show only first few lines
635
+ child.stdout?.on('data', (data) => {
636
+ const text = data.toString();
637
+ stdout += text;
638
+ const lines = text.split('\n').filter(l => l.trim());
639
+ for (const line of lines) {
640
+ totalLines++;
641
+ if (linesShown < MAX_LINES_SHOWN) {
642
+ process.stdout.write(`${indent}${line}\n`);
643
+ linesShown++;
644
+ }
645
+ }
646
+ });
647
+ child.stderr?.on('data', (data) => {
648
+ const text = data.toString();
649
+ stderr += text;
650
+ const lines = text.split('\n').filter(l => l.trim());
651
+ for (const line of lines) {
652
+ totalLines++;
653
+ if (linesShown < MAX_LINES_SHOWN) {
654
+ process.stdout.write(`${indent}${line}\n`);
655
+ linesShown++;
656
+ }
657
+ }
658
+ });
659
+ child.on('error', (err) => {
660
+ reject(new Error(`Failed to execute command: ${sanitizeError(err)}`));
661
+ });
662
+ child.on('exit', (code) => {
663
+ // Show truncation message if there are hidden lines
664
+ const hiddenLines = totalLines - linesShown;
665
+ if (hiddenLines > 0) {
666
+ process.stdout.write(`${indent}... +${hiddenLines} lines\n`);
667
+ }
668
+ const parts = [];
669
+ if (stdout.trim())
670
+ parts.push(stdout.trim());
671
+ if (stderr.trim())
672
+ parts.push(`[stderr]\n${stderr.trim()}`);
673
+ // Return special marker to prevent re-display
674
+ const output = parts.join('\n') || '(no output)';
675
+ if (code !== 0) {
676
+ resolve(`Exit ${code}\n__STREAMED__\n${output}`);
677
+ }
678
+ else {
679
+ resolve(`__STREAMED__\n${output}`);
680
+ }
681
+ });
682
+ });
683
+ }
684
+ /**
685
+ * Runs a command attached to the real terminal (stdio: 'inherit') so the USER
686
+ * can answer interactive prompts (arrow-key menus, y/N questions) that the
687
+ * non-interactive path cannot handle. Temporarily detaches the REPL's own stdin
688
+ * listeners and raw mode while the child owns the terminal, then restores them.
689
+ * Output is not captured — only the exit status is reported back to the agent.
690
+ */
691
+ function bashInteractive(command, cwd) {
692
+ return new Promise((resolvePromise) => {
693
+ const stdin = process.stdin;
694
+ const isTTY = Boolean(stdin.isTTY);
695
+ const wasRaw = isTTY ? Boolean(stdin.isRaw) : false;
696
+ // Save and detach whatever the REPL has on stdin (cancel handler, etc.) so
697
+ // the child receives keystrokes directly.
698
+ const saved = isTTY ? stdin.rawListeners('data').slice() : [];
699
+ for (const l of saved)
700
+ stdin.removeListener('data', l);
701
+ const restore = () => {
702
+ if (isTTY) {
703
+ try {
704
+ stdin.setRawMode(wasRaw);
705
+ }
706
+ catch { /* ignore */ }
707
+ if (process.stdout.isTTY)
708
+ process.stdout.write('\x1b[?2004h'); // re-arm bracketed paste
709
+ }
710
+ for (const l of saved)
711
+ stdin.on('data', l);
712
+ };
713
+ if (isTTY) {
714
+ try {
715
+ stdin.setRawMode(false);
716
+ }
717
+ catch { /* ignore */ }
718
+ if (process.stdout.isTTY)
719
+ process.stdout.write('\x1b[?2004l'); // disable bracketed paste for the child
720
+ }
721
+ process.stdout.write('\n');
722
+ try {
723
+ const isWindows = process.platform === 'win32';
724
+ const child = spawn(isWindows ? 'cmd.exe' : 'bash', isWindows ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, stdio: 'inherit' });
725
+ child.on('error', (err) => {
726
+ restore();
727
+ resolvePromise(`Error running interactive command: ${sanitizeError(err)}`);
728
+ });
729
+ child.on('exit', (code) => {
730
+ restore();
731
+ resolvePromise(code === 0
732
+ ? '(interactive command completed — output was shown to the user; read any created/changed files to see the result)'
733
+ : `Exit ${code ?? 1} (interactive command)`);
734
+ });
735
+ }
736
+ catch (err) {
737
+ restore();
738
+ resolvePromise(`Error running interactive command: ${sanitizeError(err)}`);
739
+ }
740
+ });
741
+ }
673
742
  function listDir(input) {
674
743
  const root = resolve(input.path ?? '.');
675
744
  if (!existsSync(root))
@@ -1065,98 +1134,40 @@ async function removeSkill(input) {
1065
1134
  return removed ? `Removed skill "${name}".` : `No skill named "${name}" found.`;
1066
1135
  }
1067
1136
  // ─── MCP Functions ────────────────────────────────────────────────────────────
1068
- async function mcpInstall(input) {
1069
- const { getMCPManager } = await import('./mcp-manager.js');
1070
- const manager = getMCPManager();
1071
- const result = await manager.installMCP({
1072
- name: input.name,
1073
- source: input.source,
1074
- description: input.description,
1075
- autoStart: input.autoStart,
1076
- });
1077
- if (!result.success) {
1078
- return `Error installing MCP: ${result.error}`;
1079
- }
1080
- return `✓ Installed MCP "${input.name}".\nRun mcp_start to activate it.`;
1081
- }
1082
1137
  async function mcpList() {
1083
- const { getMCPManager } = await import('./mcp-manager.js');
1084
- const manager = getMCPManager();
1085
- const mcps = manager.listMCPs();
1086
- if (!mcps.length) {
1087
- return 'No MCPs installed. Use mcp_install to add MCP servers.';
1138
+ const servers = getMcpManager().listServers();
1139
+ if (!servers.length) {
1140
+ return 'No MCP servers configured. Use mcp_add or create a .mcp.json file.';
1088
1141
  }
1089
- const lines = ['Available MCPs:\n'];
1090
- for (const mcp of mcps) {
1091
- const status = mcp.running ? '🟢 Running' : mcp.enabled ? '⚪ Stopped' : '⚫ Disabled';
1092
- const builtIn = mcp.builtIn ? ' [Built-in]' : '';
1093
- lines.push(`${status} ${mcp.name}${builtIn}`);
1094
- lines.push(` ${mcp.description}`);
1095
- if (mcp.running && mcp.tools) {
1096
- lines.push(` Tools: ${mcp.tools.map(t => t.name).join(', ')}`);
1142
+ const lines = ['MCP servers:\n'];
1143
+ for (const s of servers) {
1144
+ const status = s.connected ? '🟢 connected' : s.enabled ? '⚪ disabled/not connected' : '⚫ disabled';
1145
+ lines.push(`${status} ${s.name}`);
1146
+ if (s.error)
1147
+ lines.push(` error: ${s.error}`);
1148
+ if (s.tools.length) {
1149
+ lines.push(` tools: ${s.tools.map(t => t.name).join(', ')}`);
1097
1150
  }
1098
1151
  lines.push('');
1099
1152
  }
1100
1153
  return lines.join('\n');
1101
1154
  }
1102
- async function mcpStart(input) {
1103
- const { getMCPManager } = await import('./mcp-manager.js');
1104
- const manager = getMCPManager();
1105
- const result = await manager.startMCP(input.name);
1106
- if (!result.success) {
1107
- return `Error starting MCP: ${result.error}`;
1108
- }
1109
- const toolCount = result.tools?.length || 0;
1110
- const toolNames = result.tools?.map(t => t.name).join(', ') || 'none';
1111
- return `✓ Started MCP "${input.name}".\nAvailable tools (${toolCount}): ${toolNames}`;
1112
- }
1113
- async function mcpStop(input) {
1114
- const { getMCPManager } = await import('./mcp-manager.js');
1115
- const manager = getMCPManager();
1116
- const result = manager.stopMCP(input.name);
1117
- if (!result.success) {
1118
- return `Error stopping MCP: ${result.error}`;
1155
+ async function mcpAdd(input) {
1156
+ const parsed = parseClaudeMcpAddCommand(`mcp add ${input.name} ${input.commandArgs}`);
1157
+ if ('error' in parsed) {
1158
+ return `Error adding MCP: ${parsed.error}`;
1119
1159
  }
1120
- return `✓ Stopped MCP "${input.name}".`;
1121
- }
1122
- async function mcpCall(input) {
1123
- const { getMCPManager } = await import('./mcp-manager.js');
1124
- const manager = getMCPManager();
1125
- const result = await manager.callMCPTool(input.server, input.tool, input.arguments);
1126
- if (!result.success) {
1127
- return `Error calling MCP tool: ${result.error}`;
1160
+ if (parsed.name !== input.name) {
1161
+ return `Error adding MCP: name mismatch`;
1128
1162
  }
1129
- return typeof result.result === 'string'
1130
- ? result.result
1131
- : JSON.stringify(result.result, null, 2);
1132
- }
1133
- async function mcpUninstall(input) {
1134
- const { getMCPManager } = await import('./mcp-manager.js');
1135
- const manager = getMCPManager();
1136
- const result = manager.uninstallMCP(input.name);
1137
- if (!result.success) {
1138
- return `Error uninstalling MCP: ${result.error}`;
1163
+ const entry = { ...parsed.entry, description: input.description, env: input.env ?? parsed.entry.env };
1164
+ try {
1165
+ await getMcpManager().addServer(input.name, entry, 'user');
1166
+ return `✓ Added MCP "${input.name}" and connected.\n Command: ${input.commandArgs}`;
1139
1167
  }
1140
- return `✓ Uninstalled MCP "${input.name}".`;
1141
- }
1142
- async function mcpAdd(input) {
1143
- const { getMCPManager } = await import('./mcp-manager.js');
1144
- const manager = getMCPManager();
1145
- const parts = input.commandArgs.trim().split(/\s+/);
1146
- const cmd = parts[0];
1147
- const args = parts.slice(1);
1148
- const result = manager.addMCP({
1149
- name: input.name,
1150
- command: cmd,
1151
- args,
1152
- env: input.env,
1153
- description: input.description,
1154
- });
1155
- if (!result.success) {
1156
- return `Error adding MCP: ${result.error}`;
1168
+ catch (err) {
1169
+ return `Error adding MCP: ${err instanceof Error ? err.message : String(err)}`;
1157
1170
  }
1158
- const envStr = input.env ? `\n Env: ${Object.keys(input.env).join(', ')}` : '';
1159
- return `✓ Added MCP "${input.name}".\n Command: ${input.commandArgs}${envStr}\n Run mcp_start to activate it.`;
1160
1171
  }
1161
1172
  // ─── Dispatcher ───────────────────────────────────────────────────────────────
1162
1173
  export async function executeTool(name, input) {
@@ -1180,13 +1191,13 @@ export async function executeTool(name, input) {
1180
1191
  case 'use_skill': return useSkill(input);
1181
1192
  case 'install_skill': return installSkill(input);
1182
1193
  case 'remove_skill': return removeSkill(input);
1183
- case 'mcp_install': return mcpInstall(input);
1184
1194
  case 'mcp_list': return mcpList();
1185
- case 'mcp_start': return mcpStart(input);
1186
- case 'mcp_stop': return mcpStop(input);
1187
- case 'mcp_call': return mcpCall(input);
1188
- case 'mcp_uninstall': return mcpUninstall(input);
1189
1195
  case 'mcp_add': return mcpAdd(input);
1190
- default: return `Unknown tool: ${name}`;
1196
+ default: {
1197
+ if (name.startsWith('mcp__')) {
1198
+ return getMcpManager().callTool(name, input);
1199
+ }
1200
+ return `Unknown tool: ${name}`;
1201
+ }
1191
1202
  }
1192
1203
  }
package/dist/tree.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ interface TreeOptions {
2
+ maxDepth?: number;
3
+ showHidden?: boolean;
4
+ showSize?: boolean;
5
+ exclude?: string[];
6
+ currentDepth?: number;
7
+ }
8
+ /**
9
+ * Generate a visual file tree
10
+ */
11
+ export declare function generateTree(rootPath: string, options?: TreeOptions): string;
12
+ /**
13
+ * Parse tree command arguments
14
+ */
15
+ export declare function parseTreeArgs(args: string[]): {
16
+ path: string;
17
+ options: TreeOptions;
18
+ };
19
+ export {};