ikie-cli 0.1.27 → 0.1.29

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/agent.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import * as readline from 'node:readline';
3
- import { TOOL_DEFS, SAFE_TOOLS, PLAN_TOOLS, formatToolArgs, executeTool, isRestrictedPath } from './tools.js';
3
+ import { TOOL_DEFS, SAFE_TOOLS, PLAN_TOOLS, formatToolArgs, executeTool, isRestrictedPath, validatePathSafety, validateBashCommand } from './tools.js';
4
4
  import { renderMarkdown, extractThinkTags } from './renderer.js';
5
- import { c, toolLine, permissionPrompt, toolSuccessLine, toolErrorLine, toolOutputBlock, toolDiffBlock, InlineSpinner } from './theme.js';
5
+ import { c, toolLine, toolSuccessLine, toolErrorLine, toolOutputBlock, toolDiffBlock, InlineSpinner, CH, toolMeta } from './theme.js';
6
6
  export function estimateTokens(chars) {
7
7
  return Math.max(1, Math.round(chars / 4));
8
8
  }
@@ -569,6 +569,32 @@ export class Agent {
569
569
  if (name === 'ask_user') {
570
570
  return this.askUser(input);
571
571
  }
572
+ // Safety validation → ask permission on failure
573
+ if (!opts.autoApprove && !this.config.autoApprove) {
574
+ let safetyIssue;
575
+ if (name === 'bash') {
576
+ const cmd = String(input.command ?? '');
577
+ const v = validateBashCommand(cmd);
578
+ if (!v.safe)
579
+ safetyIssue = v.error;
580
+ }
581
+ if (name === 'read_file' || name === 'write_file' || name === 'edit_file' || name === 'list_dir') {
582
+ const p = String(input.path ?? input.cwd ?? '.');
583
+ if (p) {
584
+ const v = validatePathSafety(p);
585
+ if (!v.safe)
586
+ safetyIssue = v.error;
587
+ }
588
+ }
589
+ if (safetyIssue) {
590
+ if (this.sessionDenyList.has(name))
591
+ return `Tool execution denied by user: ${name}`;
592
+ process.stdout.write(`${this.indent}${c.warning('⚠')} ${c.muted(safetyIssue)} ${c.muted('— asking for permission')}\n`);
593
+ const allowed = await this.checkPermission(name, input);
594
+ if (!allowed)
595
+ return `Tool execution denied by user: ${name}`;
596
+ }
597
+ }
572
598
  if (name === 'read_file') {
573
599
  const path = String(input.path ?? '');
574
600
  if (isRestrictedPath(path) && !opts.autoApprove && !this.config.autoApprove && !this.sessionAllowList.has('read_file')) {
@@ -748,9 +774,28 @@ export class Agent {
748
774
  return false;
749
775
  if (this.sessionAllowList.has(toolName))
750
776
  return true;
751
- process.stdout.write(permissionPrompt(toolName, formatToolArgs(toolName, input)));
777
+ const t0 = Date.now();
778
+ const preview = formatToolArgs(toolName, input);
779
+ const { verb, tint } = toolMeta(toolName);
780
+ const makePrompt = (elapsed) => `\n ${tint('●')} ${c.white.bold('permission')} ${c.muted(`(${elapsed}s)`)} ${c.muted('·')} ${c.white(verb)} ${c.dim(preview)}\n` +
781
+ ` ${c.muted('⎿')} ` +
782
+ `${c.success.bold('y')} ${c.muted('allow')} ` +
783
+ `${c.error.bold('n')} ${c.muted('deny')} ` +
784
+ `${c.info.bold('a')} ${c.muted('always')} ` +
785
+ `${c.muted.bold('!')} ${c.muted('never')}\n` +
786
+ ` ${c.muted(CH.arrow)} `;
787
+ process.stdout.write(makePrompt('0.0'));
788
+ const timerUpdate = () => {
789
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
790
+ process.stdout.write(`\x1b[3A\x1b[0J${makePrompt(elapsed)}`);
791
+ };
792
+ const timerInterval = setInterval(timerUpdate, 100);
793
+ const cleanup = () => {
794
+ clearInterval(timerInterval);
795
+ };
752
796
  return new Promise((resolve) => {
753
797
  if (!process.stdin.isTTY) {
798
+ cleanup();
754
799
  process.stdout.write(chalk.dim('(non-interactive, denying)\n'));
755
800
  resolve(false);
756
801
  return;
@@ -765,6 +810,7 @@ export class Agent {
765
810
  process.stdin.resume();
766
811
  }
767
812
  const onData = (data) => {
813
+ cleanup();
768
814
  process.stdin.removeListener('data', onData);
769
815
  // Restore raw mode to what it was (keeps REPL's ESC handler working)
770
816
  if (process.stdin.isTTY) {
@@ -900,6 +946,7 @@ changes they didn't ask for.
900
946
  - \`write_file\`: Create new files or full rewrites
901
947
  - \`edit_file\`: Replace exact strings (preferred for modifications)
902
948
  - \`bash\`: Run shell commands (build, test, git, etc.). Commands ending with & run detached in background.
949
+ **IMPORTANT:** The bash tool is non-interactive — it cannot handle prompts. For commands that ask questions (create-next-app, npm init, etc.), use \`--yes\`, \`-y\`, or pipe through \`yes\` to skip all prompts. For long-running downloads (npx installs), request \`timeout_ms\` up to 300000.
903
950
  - \`list_dir\`: Explore directory structure
904
951
  - \`search_files\`: Find files by glob pattern
905
952
  - \`grep\`: Search file contents by regex
@@ -957,6 +1004,9 @@ changes they didn't ask for.
957
1004
  - \`mcp_stop\`: Stop a running MCP server.
958
1005
  - \`mcp_call\`: Call a tool from a running MCP. Use \`mcp_list\` first to see available tools.
959
1006
  - \`mcp_uninstall\`: Remove an installed MCP (built-in MCPs cannot be uninstalled).
1007
+ - \`mcp_add\`: Add an MCP by specifying the full command directly (Claude/Cline-style). Use when the MCP runs via npx, a script, or any custom command. Example: \`mcp_add(name="magic", commandArgs="npx -y @21st-dev/magic@latest", env={API_KEY: "..."})\`. After adding, you must run \`mcp_start\` to activate it.
1008
+
1009
+ **Recognizing MCP config patterns:** When the user says things like "install this MCP", "claude mcp add", "add this MCP", or pastes a Claude-style MCP config, parse it and use \`mcp_add\`. The format is: \`<any-prefix> mcp add <name> [--scope user] [--env KEY=VALUE ...] -- <command> [args...]\`. Everything after \`--\` is the full command string. Translate this to \`mcp_add\` — do NOT try to run it as a bash command.
960
1010
 
961
1011
  **Built-in MCPs:**
962
1012
  - **filesystem**: Enhanced file operations (read multiple files, directory trees)
package/dist/config.d.ts CHANGED
@@ -28,6 +28,8 @@ export interface IkieConfig {
28
28
  frequencyPenalty: number;
29
29
  theme: string;
30
30
  requestsPerMinute: number;
31
+ /** Whether the first-time onboarding flow has been completed. */
32
+ hasCompletedOnboarding?: boolean;
31
33
  }
32
34
  export declare function ensureHome(): void;
33
35
  export declare function loadConfig(): IkieConfig;
package/dist/index.js CHANGED
@@ -10,9 +10,10 @@ import { buildSystemPrompt, Agent } from './agent.js';
10
10
  import { startREPL } from './repl.js';
11
11
  import { login, logout } from './auth.js';
12
12
  import { c, errorLine } from './theme.js';
13
+ import { runOnboarding, shouldRunOnboarding } from './onboarding.js';
13
14
  const argv = minimist(process.argv.slice(2), {
14
15
  string: ['model', 'rpm'],
15
- boolean: ['help', 'version', 'yes', 'verbose'],
16
+ boolean: ['help', 'version', 'yes', 'verbose', 'onboarding'],
16
17
  alias: { h: 'help', v: 'version', y: 'yes', m: 'model' },
17
18
  });
18
19
  function printUsage() {
@@ -34,6 +35,7 @@ ${c.primary('Options:')}
34
35
  ${c.accent('-y, --yes')} Auto-approve all tool executions
35
36
  ${c.accent('--rpm')} ${c.muted('<number>')} Max model requests per minute (default: 10)
36
37
  ${c.accent('--verbose')} Debug output
38
+ ${c.accent('--onboarding')} Run first-time onboarding again
37
39
  ${c.accent('-h, --help')} Show this help
38
40
  ${c.accent('-v, --version')} Show version
39
41
 
@@ -133,7 +135,7 @@ async function main() {
133
135
  await runSkillsCli(argv._.slice(1).map(String), Boolean(argv.force));
134
136
  process.exit(0);
135
137
  }
136
- const config = loadConfig();
138
+ let config = loadConfig();
137
139
  if (argv.model)
138
140
  config.model = argv.model;
139
141
  if (argv.yes)
@@ -162,6 +164,12 @@ async function main() {
162
164
  // Silently fall back to DEFAULT_MODEL
163
165
  }
164
166
  }
167
+ const oneShot = argv._.length > 0 ? argv._.join(' ') : undefined;
168
+ // Run onboarding on first launch or when --onboarding is passed
169
+ if (argv.onboarding || (!oneShot && shouldRunOnboarding(config))) {
170
+ config = loadConfig(); // reload — onboarding may have saved login
171
+ await runOnboarding(config);
172
+ }
165
173
  const apiKey = getApiKey(config);
166
174
  if (!apiKey) {
167
175
  console.error(`
@@ -191,7 +199,6 @@ ${errorLine('Not signed in.')}
191
199
  console.log();
192
200
  }
193
201
  const agent = new Agent(client, config, systemPrompt);
194
- const oneShot = argv._.length > 0 ? argv._.join(' ') : undefined;
195
202
  await startREPL(agent, config, projectContextStr, oneShot);
196
203
  }
197
204
  main().catch((err) => {
@@ -41,6 +41,17 @@ export declare class MCPManager {
41
41
  error?: string;
42
42
  server?: MCPServer;
43
43
  }>;
44
+ addMCP(config: {
45
+ name: string;
46
+ command: string;
47
+ args: string[];
48
+ env?: Record<string, string>;
49
+ description?: string;
50
+ }): {
51
+ success: boolean;
52
+ error?: string;
53
+ server?: MCPServer;
54
+ };
44
55
  uninstallMCP(name: string): {
45
56
  success: boolean;
46
57
  error?: string;
@@ -125,6 +125,23 @@ export class MCPManager {
125
125
  };
126
126
  }
127
127
  }
128
+ addMCP(config) {
129
+ if (this.registry.servers[config.name]) {
130
+ return { success: false, error: `MCP '${config.name}' already exists` };
131
+ }
132
+ const server = {
133
+ name: config.name,
134
+ command: config.command,
135
+ args: config.args,
136
+ env: config.env,
137
+ description: config.description || 'Custom MCP server',
138
+ enabled: true,
139
+ autoStart: false,
140
+ };
141
+ this.registry.servers[config.name] = server;
142
+ this.saveRegistry();
143
+ return { success: true, server };
144
+ }
128
145
  uninstallMCP(name) {
129
146
  if (!this.registry.servers[name]) {
130
147
  return { success: false, error: `MCP '${name}' not found` };
@@ -0,0 +1,3 @@
1
+ import { type IkieConfig } from './config.js';
2
+ export declare function runOnboarding(config: IkieConfig): Promise<void>;
3
+ export declare function shouldRunOnboarding(config: IkieConfig): boolean;
@@ -0,0 +1,167 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { c, successLine, infoLine, errorLine } from './theme.js';
3
+ import { login } from './auth.js';
4
+ import { saveConfig, isLoggedIn } from './config.js';
5
+ function waitForEnter(message) {
6
+ const msg = message || 'Press Enter to continue...';
7
+ return new Promise((resolve) => {
8
+ const iface = createInterface({ input: process.stdin, output: process.stdout });
9
+ iface.question('\n ' + c.muted('\u25b8') + ' ' + c.dim(msg) + ' ', () => {
10
+ iface.close();
11
+ resolve();
12
+ });
13
+ });
14
+ }
15
+ function askYesNo(question, defaultYes = true) {
16
+ return new Promise((resolve) => {
17
+ const iface = createInterface({ input: process.stdin, output: process.stdout });
18
+ const hint = defaultYes
19
+ ? c.success('Y') + '/' + c.dim('n')
20
+ : c.dim('y') + '/' + c.error('N');
21
+ iface.question('\n ' + c.primary('?') + ' ' + c.white(question) + ' ' + hint + ' ', (answer) => {
22
+ iface.close();
23
+ const a = answer.trim().toLowerCase();
24
+ if (!a)
25
+ resolve(defaultYes);
26
+ else
27
+ resolve(a === 'y' || a === 'yes');
28
+ });
29
+ });
30
+ }
31
+ function displayWelcome() {
32
+ const border = c.primary.bold('\u2550'.repeat(60));
33
+ console.log();
34
+ console.log(c.primary.bold('\u2554') + border + c.primary.bold('\u2557'));
35
+ console.log(c.primary.bold('\u2551') + ' ' + c.primary.bold('\u2551'));
36
+ console.log(c.primary.bold('\u2551') + ' ' + c.accent.bold('Welcome to') + ' ' + c.primary.bold('ikie') + ' ' + c.muted('\u2014 Your AI Pair Programmer') + ' ' + c.primary.bold('\u2551'));
37
+ console.log(c.primary.bold('\u2551') + ' ' + c.muted('Agentic Coding in Your Terminal') + ' ' + c.primary.bold('\u2551'));
38
+ console.log(c.primary.bold('\u2551') + ' ' + c.primary.bold('\u2551'));
39
+ console.log(c.primary.bold('\u255a') + border + c.primary.bold('\u255d'));
40
+ console.log();
41
+ console.log(c.white.bold('Hey there! \u{1F44B}'));
42
+ console.log();
43
+ console.log(c.white("Let's get you set up in just a few steps."));
44
+ console.log();
45
+ }
46
+ async function stepAuthentication(config) {
47
+ console.log();
48
+ console.log(c.primary.bold('Step 1 of 3:') + ' ' + c.white.bold('Sign In'));
49
+ console.log(c.muted('\u2501'.repeat(60)));
50
+ console.log();
51
+ console.log(c.white('To use ikie, you need a free ikie account:'));
52
+ console.log();
53
+ console.log(' ' + c.success('\u2713') + ' Access to powerful AI models');
54
+ console.log(' ' + c.success('\u2713') + ' Web search capabilities');
55
+ console.log(' ' + c.success('\u2713') + ' Session sync across devices');
56
+ console.log(' ' + c.success('\u2713') + ' Weekly free credit limits');
57
+ console.log();
58
+ if (isLoggedIn(config)) {
59
+ console.log(successLine('You are already signed in!'));
60
+ return waitForEnter();
61
+ }
62
+ const shouldLogin = await askYesNo('Would you like to sign in now?', true);
63
+ if (shouldLogin) {
64
+ console.log();
65
+ console.log(c.muted(' Opening login in your browser...'));
66
+ console.log();
67
+ try {
68
+ await login();
69
+ console.log(successLine('Successfully signed in!'));
70
+ }
71
+ catch (err) {
72
+ console.log(errorLine('Login failed: ' + (err instanceof Error ? err.message : String(err))));
73
+ console.log(infoLine('You can sign in later with: ikie login'));
74
+ }
75
+ }
76
+ else {
77
+ console.log(infoLine('You can sign in later with: ikie login'));
78
+ }
79
+ await waitForEnter();
80
+ }
81
+ async function stepFeatureTour() {
82
+ console.log();
83
+ console.log(c.primary.bold('Step 2 of 3:') + ' ' + c.white.bold('What Can ikie Do?'));
84
+ console.log(c.muted('\u2501'.repeat(60)));
85
+ console.log();
86
+ console.log(c.secondary.bold('Tools'));
87
+ console.log(' ' + c.muted('\u2022') + ' Read, write, and edit files');
88
+ console.log(' ' + c.muted('\u2022') + ' Run shell commands');
89
+ console.log(' ' + c.muted('\u2022') + ' Git operations (status, diff, commit, branch)');
90
+ console.log(' ' + c.muted('\u2022') + ' Search the web and fetch URLs');
91
+ console.log(' ' + c.muted('\u2022') + ' Save persistent memory notes');
92
+ console.log();
93
+ console.log(c.secondary.bold('Two Interaction Modes'));
94
+ console.log(' ' + c.muted('\u2022') + ' ' + c.success('Agent mode') + ' \u2014 Full execution, makes changes');
95
+ console.log(' ' + c.muted('\u2022') + ' ' + c.warning('Plan mode') + ' \u2014 Read-only, proposes plans');
96
+ console.log();
97
+ console.log(c.secondary.bold('Extensible'));
98
+ console.log(' ' + c.muted('\u2022') + ' Skills system (reusable instructions)');
99
+ console.log(' ' + c.muted('\u2022') + ' MCP servers (Model Context Protocol)');
100
+ console.log(' ' + c.muted('\u2022') + ' Spawn sub-agents for parallel work');
101
+ console.log();
102
+ await waitForEnter();
103
+ }
104
+ async function stepQuickStart() {
105
+ console.log();
106
+ console.log(c.primary.bold('Step 3 of 3:') + ' ' + c.white.bold('Getting Started'));
107
+ console.log(c.muted('\u2501'.repeat(60)));
108
+ console.log();
109
+ console.log(c.accent('1.') + ' ' + c.white('Ask a question'));
110
+ console.log(' ' + c.dim('ikie "explain this codebase"'));
111
+ console.log();
112
+ console.log(c.accent('2.') + ' ' + c.white('One-shot commands'));
113
+ console.log(' ' + c.dim('ikie "add error handling to api.ts"'));
114
+ console.log();
115
+ console.log(c.accent('3.') + ' ' + c.white('Interactive mode'));
116
+ console.log(' ' + c.dim('ikie'));
117
+ console.log(' ' + c.dim('Chat back and forth with the AI'));
118
+ console.log();
119
+ console.log(c.accent('4.') + ' ' + c.white('Slash commands'));
120
+ console.log(' ' + c.white('/help') + ' \u2014 Show all commands');
121
+ console.log(' ' + c.white('/memory save') + ' \u2014 Save persistent notes');
122
+ console.log(' ' + c.white('/session load') + ' \u2014 Resume previous conversation');
123
+ console.log(' ' + c.white('/plan') + ' \u2014 Read-only planning mode');
124
+ console.log(' ' + c.white('/theme') + ' \u2014 Change color theme');
125
+ console.log();
126
+ console.log(c.white.bold('Pro Tips:'));
127
+ console.log(' ' + c.success('\u2022') + ' Type ' + c.white('!') + ' before a command to run shell directly: ' + c.dim('!ls -la'));
128
+ console.log(' ' + c.success('\u2022') + ' Press ' + c.white('Shift+Tab') + ' to toggle agent/plan modes');
129
+ console.log(' ' + c.success('\u2022') + ' Press ' + c.white('Esc') + ' during execution to cancel');
130
+ console.log();
131
+ await waitForEnter();
132
+ }
133
+ function displayCompletion() {
134
+ console.log();
135
+ console.log(c.success.bold('All Set!'));
136
+ console.log(c.muted('\u2501'.repeat(60)));
137
+ console.log();
138
+ console.log(c.white.bold("You're ready to start coding with ikie!"));
139
+ console.log();
140
+ console.log(c.primary('1.') + ' Type ' + c.accent('ikie') + ' to start interactive mode');
141
+ console.log(c.primary('2.') + ' Type ' + c.accent('/help') + ' to see all commands');
142
+ console.log(c.primary('3.') + ' Start building something amazing!');
143
+ console.log();
144
+ console.log(c.muted('Dashboard:') + ' ' + c.dim('http://140.245.26.210:3000/dashboard'));
145
+ console.log();
146
+ console.log(c.success.bold('Happy coding!'));
147
+ console.log();
148
+ }
149
+ export async function runOnboarding(config) {
150
+ try {
151
+ displayWelcome();
152
+ await waitForEnter();
153
+ await stepAuthentication(config);
154
+ await stepFeatureTour();
155
+ await stepQuickStart();
156
+ displayCompletion();
157
+ saveConfig({ hasCompletedOnboarding: true });
158
+ await waitForEnter('Press Enter to start ikie...');
159
+ }
160
+ catch (err) {
161
+ console.log(errorLine('Onboarding error: ' + (err instanceof Error ? err.message : String(err))));
162
+ saveConfig({ hasCompletedOnboarding: true });
163
+ }
164
+ }
165
+ export function shouldRunOnboarding(config) {
166
+ return !config.hasCompletedOnboarding;
167
+ }
package/dist/repl.js CHANGED
@@ -9,6 +9,7 @@ import { login, logout } from './auth.js';
9
9
  import { join as pathJoin } from 'path';
10
10
  import { deleteSession, listSessions, loadSession, normalizeSessionName, saveSession } from './session.js';
11
11
  import { buildUserContent, formatBytes, loadClipboardImageAttachment, loadImageAttachment, hasClipboardImage } from './attachments.js';
12
+ import { runOnboarding } from './onboarding.js';
12
13
  async function fetchModelsFromServer(config) {
13
14
  const baseUrl = config.baseURL || IKIE_API_BASE;
14
15
  const apiUrl = baseUrl.includes('/api/v1')
@@ -107,6 +108,7 @@ const SLASH_CMDS = [
107
108
  { name: 'plan', desc: 'Switch to plan mode (read-only)' },
108
109
  { name: 'agent', desc: 'Switch to agent mode (full)' },
109
110
  { name: 'compact', desc: 'Summarize conversation to free up context window' },
111
+ { name: 'onboarding', desc: 'Run the first-time onboarding tutorial' },
110
112
  { name: 'login', desc: 'Sign in to ikie account' },
111
113
  { name: 'logout', desc: 'Sign out of ikie account' },
112
114
  { name: 'exit', desc: 'Exit Ikie' },
@@ -182,6 +184,7 @@ ${c.primary.bold('Ikie Commands')}
182
184
  ${c.warning('/plan')} Plan mode — research & propose, no changes
183
185
  ${c.warning('/agent')} Agent mode — full execution (default)
184
186
  ${c.warning('/mode')} Show or set mode ${c.muted('(Shift+Tab toggles)')}
187
+ ${c.warning('/onboarding')} Run first-time onboarding tutorial
185
188
  ${c.warning('/login')} Sign in to ikie account
186
189
  ${c.warning('/logout')} Sign out of ikie account
187
190
  ${c.warning('/exit')} Exit Ikie
@@ -469,6 +472,11 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
469
472
  console.log(`\n ${c.muted('Used automatically when relevant ·')} ${c.warning('/skills show <name>')} ${c.muted('·')} ${c.warning('/skills install <src>')}\n`);
470
473
  return true;
471
474
  }
475
+ case 'onboarding': {
476
+ console.log();
477
+ await runOnboarding(config);
478
+ return true;
479
+ }
472
480
  case 'logout': {
473
481
  if (!isLoggedIn(config)) {
474
482
  console.log(infoLine('Not signed in.'));
package/dist/theme.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.22";
1
+ export declare const VERSION: string;
2
2
  export interface Theme {
3
3
  name: string;
4
4
  description: string;
@@ -41,6 +41,14 @@ export declare function drawBanner(model: string): void;
41
41
  export declare function modeTag(mode: 'agent' | 'plan'): string;
42
42
  /** Circular fill indicator for context usage. */
43
43
  export declare function contextRing(pct: number): string;
44
+ declare const CH: {
45
+ tl: string;
46
+ prompt: string;
47
+ cont: string;
48
+ arrow: string;
49
+ dot: string;
50
+ };
51
+ export { CH };
44
52
  export declare function printPromptHeader(mode?: 'agent' | 'plan'): void;
45
53
  export declare const PROMPT: string;
46
54
  export declare const CONTINUE_PROMPT: string;
@@ -60,6 +68,10 @@ export declare class InlineSpinner {
60
68
  stop(successMessage?: string): void;
61
69
  updateLabel(label: string): void;
62
70
  }
71
+ export declare function toolMeta(rawName: string): {
72
+ verb: string;
73
+ tint: (s: string) => string;
74
+ };
63
75
  export declare function toolLine(name: string, args: string): string;
64
76
  /** Multi-line output block shown after a tool runs. */
65
77
  export declare function toolOutputBlock(result: string, ms: number, indent?: string): string;
@@ -86,4 +98,3 @@ export declare function errorLine(msg: string): string;
86
98
  export declare function warnLine(msg: string): string;
87
99
  export declare function infoLine(msg: string): string;
88
100
  export declare function permissionPrompt(toolName: string, preview: string): string;
89
- export {};
package/dist/theme.js CHANGED
@@ -1,9 +1,20 @@
1
1
  import chalk from 'chalk';
2
2
  import os from 'os';
3
- import { join as pathJoin, basename } from 'path';
3
+ import { join as pathJoin, basename, dirname } from 'path';
4
4
  import { existsSync, readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
5
6
  import { loadConfig, saveConfig } from './config.js';
6
- export const VERSION = '0.1.22';
7
+ function loadVersion() {
8
+ try {
9
+ const pkgPath = pathJoin(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
10
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
11
+ return pkg.version;
12
+ }
13
+ catch {
14
+ return '0.0.0';
15
+ }
16
+ }
17
+ export const VERSION = loadVersion();
7
18
  const IKIE_BANNER = [
8
19
  ' ██╗██╗ ██╗██╗███████╗',
9
20
  ' ██║██║ ██╔╝██║██╔════╝',
@@ -272,6 +283,7 @@ const IS_WIN = process.platform === 'win32';
272
283
  const CH = IS_WIN
273
284
  ? { tl: '+-', prompt: '\\->', cont: '| ', arrow: '>', dot: '●' }
274
285
  : { tl: '╭─', prompt: '╰─❯', cont: '│ ', arrow: '❯', dot: '●' };
286
+ export { CH };
275
287
  export function printPromptHeader(mode = 'agent') {
276
288
  const cwdName = basename(process.cwd()) || '/';
277
289
  const branch = getGitBranchFast();
@@ -342,7 +354,7 @@ export class InlineSpinner {
342
354
  }
343
355
  }
344
356
  // Maps a tool name to a display name + dot color reflecting its effect.
345
- function toolMeta(rawName) {
357
+ export function toolMeta(rawName) {
346
358
  const base = rawName.split(/\s|×/)[0];
347
359
  switch (base) {
348
360
  case 'read_file': return { verb: 'Read', tint: c.info };
package/dist/tools.d.ts CHANGED
@@ -4,4 +4,19 @@ export declare const SAFE_TOOLS: Set<string>;
4
4
  export declare const PLAN_TOOLS: Set<string>;
5
5
  export declare function isRestrictedPath(path: string): boolean;
6
6
  export declare function formatToolArgs(name: string, input: Record<string, unknown>): string;
7
+ /**
8
+ * Validates that a path is safe and within allowed boundaries
9
+ */
10
+ export declare function validatePathSafety(userPath: string): {
11
+ safe: boolean;
12
+ resolved: string;
13
+ error?: string;
14
+ };
15
+ /**
16
+ * Validates bash command for safety
17
+ */
18
+ export declare function validateBashCommand(command: string): {
19
+ safe: boolean;
20
+ error?: string;
21
+ };
7
22
  export declare function executeTool(name: string, input: Record<string, unknown>): Promise<string>;
package/dist/tools.js CHANGED
@@ -401,6 +401,23 @@ export const TOOL_DEFS = [
401
401
  },
402
402
  },
403
403
  },
404
+ {
405
+ type: 'function',
406
+ function: {
407
+ name: 'mcp_add',
408
+ description: 'Add an MCP server by specifying the command directly (Claude/Cline-style). Use this for MCPs that run via npx, a script, or any arbitrary command. Example: mcp_add(name: "magic", description: "21st.dev component MCP", env: {API_KEY: "..."}, commandArgs: "npx -y @21st-dev/magic@latest")',
409
+ parameters: {
410
+ type: 'object',
411
+ properties: {
412
+ name: { type: 'string', description: 'Unique name for this MCP' },
413
+ description: { type: 'string', description: 'Description of what this MCP does' },
414
+ env: { type: 'object', description: 'Environment variables as key-value pairs' },
415
+ commandArgs: { type: 'string', description: 'Full command line to run the MCP server, e.g. "npx -y @21st-dev/magic@latest" or "python server.py"' },
416
+ },
417
+ required: ['name', 'commandArgs'],
418
+ },
419
+ },
420
+ },
404
421
  {
405
422
  type: 'function',
406
423
  function: {
@@ -419,11 +436,11 @@ export const TOOL_DEFS = [
419
436
  // ─── Safe tools (auto-approved) ───────────────────────────────────────────────
420
437
  // spawn_agent is "safe" at the dispatch layer — the tools the sub-agent itself
421
438
  // runs go through their own approval inside the sub-agent loop.
422
- 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']);
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']);
423
440
  // Tools available in PLAN mode — read-only exploration plus delegation/questions.
424
441
  // Everything that mutates the filesystem or runs commands (write_file, edit_file,
425
442
  // bash, memory_write) is intentionally excluded so plan mode can only research.
426
- 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']);
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']);
427
444
  // Paths that may contain secrets, credentials, or system configuration.
428
445
  // Reading these requires explicit user permission even though read_file is normally safe.
429
446
  const RESTRICTED_PATTERNS = [
@@ -515,11 +532,14 @@ export function formatToolArgs(name, input) {
515
532
  /**
516
533
  * Validates that a path is safe and within allowed boundaries
517
534
  */
518
- function validatePathSafety(userPath) {
535
+ export function validatePathSafety(userPath) {
519
536
  try {
520
537
  const resolved = resolve(userPath);
521
538
  const cwd = process.cwd();
522
539
  const rel = relative(cwd, resolved);
540
+ if (rel === '' || rel === '.') {
541
+ return { safe: true, resolved };
542
+ }
523
543
  if (rel.startsWith('..') || resolve(rel) !== rel) {
524
544
  return { safe: false, resolved, error: 'Path traversal detected' };
525
545
  }
@@ -535,11 +555,12 @@ function validatePathSafety(userPath) {
535
555
  /**
536
556
  * Validates bash command for safety
537
557
  */
538
- function validateBashCommand(command) {
558
+ export function validateBashCommand(command) {
539
559
  const trimmed = command.trim();
540
560
  const dangerousPatterns = [
541
561
  { pattern: /\$\(/g, desc: 'command substitution' },
542
- { pattern: />\s*\/(?:dev|etc|proc|sys)\b/g, desc: 'system file access' },
562
+ { pattern: />\s*\/(?:etc|proc|sys)\b/g, desc: 'system file access' },
563
+ { pattern: />>?\s*\/dev\/(?!null(?:\s|$))/g, desc: 'system file access' },
543
564
  ];
544
565
  for (const { pattern, desc } of dangerousPatterns) {
545
566
  if (pattern.test(trimmed)) {
@@ -575,11 +596,7 @@ function sanitizeError(error) {
575
596
  function readFile(input) {
576
597
  if (!input.path || input.path === 'undefined')
577
598
  return 'Error: path is required for read_file';
578
- const pathCheck = validatePathSafety(input.path);
579
- if (!pathCheck.safe) {
580
- return `Error: ${pathCheck.error}`;
581
- }
582
- const abs = pathCheck.resolved;
599
+ const abs = resolve(input.path);
583
600
  if (!existsSync(abs))
584
601
  return `Error: File not found: ${input.path}`;
585
602
  try {
@@ -603,11 +620,7 @@ function writeFile(input) {
603
620
  return 'Error: path is required for write_file';
604
621
  if (input.content == null)
605
622
  return 'Error: content is required for write_file';
606
- const pathCheck = validatePathSafety(input.path);
607
- if (!pathCheck.safe) {
608
- return `Error: ${pathCheck.error}`;
609
- }
610
- const abs = pathCheck.resolved;
623
+ const abs = resolve(input.path);
611
624
  const maxSize = 5 * 1024 * 1024;
612
625
  const contentSize = Buffer.byteLength(input.content, 'utf-8');
613
626
  if (contentSize > maxSize) {
@@ -627,11 +640,7 @@ function writeFile(input) {
627
640
  function editFile(input) {
628
641
  if (!input.path || input.path === 'undefined')
629
642
  return 'Error: path is required for edit_file';
630
- const pathCheck = validatePathSafety(input.path);
631
- if (!pathCheck.safe) {
632
- return `Error: ${pathCheck.error}`;
633
- }
634
- const abs = pathCheck.resolved;
643
+ const abs = resolve(input.path);
635
644
  if (!existsSync(abs))
636
645
  return `Error: File not found: ${input.path}`;
637
646
  try {
@@ -651,17 +660,9 @@ function editFile(input) {
651
660
  async function bash(input) {
652
661
  let cwd = process.cwd();
653
662
  if (input.cwd) {
654
- const cwdCheck = validatePathSafety(input.cwd);
655
- if (!cwdCheck.safe) {
656
- return `Error: ${cwdCheck.error}`;
657
- }
658
- cwd = cwdCheck.resolved;
663
+ cwd = resolve(input.cwd);
659
664
  }
660
665
  const command = input.command.trim();
661
- const validation = validateBashCommand(command);
662
- if (!validation.safe) {
663
- return `Error: ${validation.error}. Command blocked for security.`;
664
- }
665
666
  if (command.endsWith('&')) {
666
667
  const bgCmd = command.slice(0, -1).trim();
667
668
  try {
@@ -682,8 +683,8 @@ async function bash(input) {
682
683
  return `Error starting background process: ${sanitizeError(err)}`;
683
684
  }
684
685
  }
685
- const maxTimeout = 60000;
686
- const timeout = Math.min(input.timeout_ms ?? 30000, maxTimeout);
686
+ const maxTimeout = 300000;
687
+ const timeout = Math.min(input.timeout_ms ?? 60000, maxTimeout);
687
688
  try {
688
689
  const { stdout, stderr } = await execAsync(command, {
689
690
  cwd,
@@ -710,10 +711,7 @@ async function bash(input) {
710
711
  }
711
712
  }
712
713
  function listDir(input) {
713
- const check = validatePathSafety(input.path ?? '.');
714
- if (!check.safe)
715
- return `Error: ${check.error}`;
716
- const root = check.resolved;
714
+ const root = resolve(input.path ?? '.');
717
715
  if (!existsSync(root))
718
716
  return `Error: Not found: ${input.path}`;
719
717
  const maxDepth = input.max_depth ?? 3;
@@ -1181,6 +1179,25 @@ async function mcpUninstall(input) {
1181
1179
  }
1182
1180
  return `✓ Uninstalled MCP "${input.name}".`;
1183
1181
  }
1182
+ async function mcpAdd(input) {
1183
+ const { getMCPManager } = await import('./mcp-manager.js');
1184
+ const manager = getMCPManager();
1185
+ const parts = input.commandArgs.trim().split(/\s+/);
1186
+ const cmd = parts[0];
1187
+ const args = parts.slice(1);
1188
+ const result = manager.addMCP({
1189
+ name: input.name,
1190
+ command: cmd,
1191
+ args,
1192
+ env: input.env,
1193
+ description: input.description,
1194
+ });
1195
+ if (!result.success) {
1196
+ return `Error adding MCP: ${result.error}`;
1197
+ }
1198
+ const envStr = input.env ? `\n Env: ${Object.keys(input.env).join(', ')}` : '';
1199
+ return `✓ Added MCP "${input.name}".\n Command: ${input.commandArgs}${envStr}\n Run mcp_start to activate it.`;
1200
+ }
1184
1201
  // ─── Dispatcher ───────────────────────────────────────────────────────────────
1185
1202
  export async function executeTool(name, input) {
1186
1203
  switch (name) {
@@ -1209,6 +1226,7 @@ export async function executeTool(name, input) {
1209
1226
  case 'mcp_stop': return mcpStop(input);
1210
1227
  case 'mcp_call': return mcpCall(input);
1211
1228
  case 'mcp_uninstall': return mcpUninstall(input);
1229
+ case 'mcp_add': return mcpAdd(input);
1212
1230
  default: return `Unknown tool: ${name}`;
1213
1231
  }
1214
1232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {