ikie-cli 0.1.27 → 0.1.28
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 +53 -3
- package/dist/config.d.ts +2 -0
- package/dist/index.js +10 -3
- package/dist/mcp-manager.d.ts +11 -0
- package/dist/mcp-manager.js +17 -0
- package/dist/onboarding.d.ts +3 -0
- package/dist/onboarding.js +162 -0
- package/dist/repl.js +8 -0
- package/dist/theme.d.ts +13 -2
- package/dist/theme.js +15 -3
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +53 -35
- package/package.json +1 -1
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,
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
package/dist/mcp-manager.d.ts
CHANGED
|
@@ -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;
|
package/dist/mcp-manager.js
CHANGED
|
@@ -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,162 @@
|
|
|
1
|
+
import { c, successLine, infoLine, errorLine } from './theme.js';
|
|
2
|
+
import { login } from './auth.js';
|
|
3
|
+
import { saveConfig, isLoggedIn } from './config.js';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
function waitForEnter(message = 'Press Enter to continue...') {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const iface = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
iface.question(`\n ${c.muted('▸')} ${c.dim(message)} `, () => { iface.close(); resolve(); });
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function askYesNo(question, defaultYes = true) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const iface = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
const hint = defaultYes ? `${c.success('Y')}/${c.dim('n')}` : `${c.dim('y')}/${c.error('N')}`;
|
|
15
|
+
iface.question(`\n ${c.primary('?')} ${c.white(question)} ${hint} `, (answer) => {
|
|
16
|
+
iface.close();
|
|
17
|
+
const a = answer.trim().toLowerCase();
|
|
18
|
+
if (!a)
|
|
19
|
+
resolve(defaultYes);
|
|
20
|
+
else
|
|
21
|
+
resolve(a === 'y' || a === 'yes');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function displayWelcome() {
|
|
26
|
+
console.log(`
|
|
27
|
+
${c.primary.bold('╔═══════════════════════════════════════════════════════════════╗')}
|
|
28
|
+
${c.primary.bold('║')} ${c.primary.bold('║')}
|
|
29
|
+
${c.primary.bold('║')} ${c.accent.bold('Welcome to')} ${c.primary.bold('ikie')} ${c.muted('— Your AI Pair Programmer')} ${c.primary.bold('║')}
|
|
30
|
+
${c.primary.bold('║')} ${c.muted('Agentic Coding in Your Terminal')} ${c.primary.bold('║')}
|
|
31
|
+
${c.primary.bold('║')} ${c.primary.bold('║')}
|
|
32
|
+
${c.primary.bold('╚═══════════════════════════════════════════════════════════════╝')}
|
|
33
|
+
|
|
34
|
+
${c.white.bold("Hey there! 👋")}
|
|
35
|
+
|
|
36
|
+
${c.white("Let's get you set up in just a few steps.")}
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
async function stepAuthentication(config) {
|
|
40
|
+
console.log(`
|
|
41
|
+
|
|
42
|
+
${c.primary.bold('Step 1 of 3:')} ${c.white.bold('Sign In')}
|
|
43
|
+
${c.muted('\u2501'.repeat(60))}
|
|
44
|
+
|
|
45
|
+
${c.white('To use ikie, you need a free ikie account:')}
|
|
46
|
+
|
|
47
|
+
${c.success('✓')} Access to powerful AI models
|
|
48
|
+
${c.success('✓')} Web search capabilities
|
|
49
|
+
${c.success('✓')} Session sync across devices
|
|
50
|
+
${c.success('✓')} Weekly free credit limits
|
|
51
|
+
`);
|
|
52
|
+
if (isLoggedIn(config)) {
|
|
53
|
+
console.log(successLine('You are already signed in!'));
|
|
54
|
+
return waitForEnter();
|
|
55
|
+
}
|
|
56
|
+
const shouldLogin = await askYesNo('Would you like to sign in now?', true);
|
|
57
|
+
if (shouldLogin) {
|
|
58
|
+
console.log(`\n${c.muted(' Opening login in your browser...')}\n`);
|
|
59
|
+
try {
|
|
60
|
+
await login();
|
|
61
|
+
console.log(successLine('Successfully signed in!'));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.log(errorLine(`Login failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
65
|
+
console.log(infoLine('You can sign in later with: ikie login'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(infoLine('You can sign in later with: ikie login'));
|
|
70
|
+
}
|
|
71
|
+
await waitForEnter();
|
|
72
|
+
}
|
|
73
|
+
async function stepFeatureTour() {
|
|
74
|
+
console.log(`
|
|
75
|
+
|
|
76
|
+
${c.primary.bold('Step 2 of 3:')} ${c.white.bold('What Can ikie Do?')}
|
|
77
|
+
${c.muted('\u2501'.repeat(60))}
|
|
78
|
+
|
|
79
|
+
${c.secondary.bold('Tools')}
|
|
80
|
+
${c.muted('•')} Read, write, and edit files
|
|
81
|
+
${c.muted('•')} Run shell commands
|
|
82
|
+
${c.muted('•')} Git operations (status, diff, commit, branch)
|
|
83
|
+
${c.muted('•')} Search the web and fetch URLs
|
|
84
|
+
${c.muted('•')} Save persistent memory notes
|
|
85
|
+
|
|
86
|
+
${c.secondary.bold('Two Interaction Modes')}
|
|
87
|
+
${c.muted('•')} ${c.success('Agent mode')} — Full execution, makes changes
|
|
88
|
+
${c.muted('•')} ${c.warning('Plan mode')} — Read-only, proposes plans
|
|
89
|
+
|
|
90
|
+
${c.secondary.bold('Extensible')}
|
|
91
|
+
${c.muted('•')} Skills system (reusable instructions)
|
|
92
|
+
${c.muted('•')} MCP servers (Model Context Protocol)
|
|
93
|
+
${c.muted('•')} Spawn sub-agents for parallel work
|
|
94
|
+
`);
|
|
95
|
+
await waitForEnter();
|
|
96
|
+
}
|
|
97
|
+
async function stepQuickStart() {
|
|
98
|
+
console.log(`
|
|
99
|
+
|
|
100
|
+
${c.primary.bold('Step 3 of 3:')} ${c.white.bold('Getting Started')}
|
|
101
|
+
${c.muted('\u2501'.repeat(60))}
|
|
102
|
+
|
|
103
|
+
${c.accent('1.')} ${c.white('Ask a question')}
|
|
104
|
+
${c.dim('ikie "explain this codebase"')}
|
|
105
|
+
|
|
106
|
+
${c.accent('2.')} ${c.white('One-shot commands')}
|
|
107
|
+
${c.dim('ikie "add error handling to api.ts"')}
|
|
108
|
+
|
|
109
|
+
${c.accent('3.')} ${c.white('Interactive mode')}
|
|
110
|
+
${c.dim('ikie')}
|
|
111
|
+
${c.dim('Chat back and forth with the AI')}
|
|
112
|
+
|
|
113
|
+
${c.accent('4.')} ${c.white('Slash commands')}
|
|
114
|
+
${c.dim('/help')} — Show all commands
|
|
115
|
+
${c.dim('/memory save')} — Save persistent notes
|
|
116
|
+
${c.dim('/session load')} — Resume previous conversation
|
|
117
|
+
${c.dim('/plan')} — Read-only planning mode
|
|
118
|
+
${c.dim('/theme')} — Change color theme
|
|
119
|
+
|
|
120
|
+
${c.white.bold('Pro Tips:')}
|
|
121
|
+
${c.success('•')} Type ${c.white('!')} before a command to run shell directly: ${c.dim('!ls -la')}
|
|
122
|
+
${c.success('•')} Press ${c.white('Shift+Tab')} to toggle agent/plan modes
|
|
123
|
+
${c.success('•')} Press ${c.white('Esc')} during execution to cancel
|
|
124
|
+
`);
|
|
125
|
+
await waitForEnter();
|
|
126
|
+
}
|
|
127
|
+
function displayCompletion() {
|
|
128
|
+
console.log(`
|
|
129
|
+
|
|
130
|
+
${c.success.bold('All Set!')}
|
|
131
|
+
${c.muted('\u2501'.repeat(60))}
|
|
132
|
+
|
|
133
|
+
${c.white.bold("You're ready to start coding with ikie!")}
|
|
134
|
+
|
|
135
|
+
${c.primary('1.')} Type ${c.accent('ikie')} to start interactive mode
|
|
136
|
+
${c.primary('2.')} Type ${c.accent('/help')} to see all commands
|
|
137
|
+
${c.primary('3.')} Start building something amazing!
|
|
138
|
+
|
|
139
|
+
${c.muted('Dashboard:')} ${c.dim('http://140.245.26.210:3000/dashboard')}
|
|
140
|
+
|
|
141
|
+
${c.success.bold('Happy coding!')}
|
|
142
|
+
`);
|
|
143
|
+
}
|
|
144
|
+
export async function runOnboarding(config) {
|
|
145
|
+
try {
|
|
146
|
+
displayWelcome();
|
|
147
|
+
await waitForEnter();
|
|
148
|
+
await stepAuthentication(config);
|
|
149
|
+
await stepFeatureTour();
|
|
150
|
+
await stepQuickStart();
|
|
151
|
+
displayCompletion();
|
|
152
|
+
saveConfig({ hasCompletedOnboarding: true });
|
|
153
|
+
await waitForEnter('Press Enter to start ikie...');
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.log(errorLine(`Onboarding error: ${err instanceof Error ? err.message : String(err)}`));
|
|
157
|
+
saveConfig({ hasCompletedOnboarding: true });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function shouldRunOnboarding(config) {
|
|
161
|
+
return !config.hasCompletedOnboarding;
|
|
162
|
+
}
|
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
|
|
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
|
-
|
|
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*\/(?:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
686
|
-
const timeout = Math.min(input.timeout_ms ??
|
|
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
|
|
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
|
}
|