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/README.md +185 -22
- package/dist/agent.d.ts +44 -0
- package/dist/agent.js +386 -128
- package/dist/config.d.ts +8 -0
- package/dist/config.js +4 -0
- package/dist/index.js +36 -1
- package/dist/mcp-manager.d.ts +75 -89
- package/dist/mcp-manager.js +710 -304
- package/dist/repl.js +315 -71
- package/dist/skills.d.ts +16 -0
- package/dist/skills.js +83 -6
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +21 -4
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +177 -166
- package/dist/tree.d.ts +19 -0
- package/dist/tree.js +266 -0
- package/package.json +2 -1
package/dist/repl.js
CHANGED
|
@@ -10,6 +10,8 @@ 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
12
|
import { runOnboarding } from './onboarding.js';
|
|
13
|
+
import { getMcpManager, parseClaudeMcpAddCommand } from './mcp-manager.js';
|
|
14
|
+
import { generateTree, parseTreeArgs } from './tree.js';
|
|
13
15
|
async function fetchModelsFromServer(config) {
|
|
14
16
|
const baseUrl = config.baseURL || IKIE_API_BASE;
|
|
15
17
|
const apiUrl = baseUrl.includes('/api/v1')
|
|
@@ -72,6 +74,11 @@ const SLASH_CMDS = [
|
|
|
72
74
|
],
|
|
73
75
|
},
|
|
74
76
|
{ name: 'context', desc: 'Show project context' },
|
|
77
|
+
{
|
|
78
|
+
name: 'tree',
|
|
79
|
+
desc: 'Show file tree',
|
|
80
|
+
args: '[path] [-a] [-s] [-d depth]',
|
|
81
|
+
},
|
|
75
82
|
{
|
|
76
83
|
name: 'models',
|
|
77
84
|
desc: 'List available models',
|
|
@@ -109,10 +116,51 @@ const SLASH_CMDS = [
|
|
|
109
116
|
{ name: 'agent', desc: 'Switch to agent mode (full)' },
|
|
110
117
|
{ name: 'compact', desc: 'Summarize conversation to free up context window' },
|
|
111
118
|
{ name: 'onboarding', desc: 'Run the first-time onboarding tutorial' },
|
|
119
|
+
{
|
|
120
|
+
name: 'mcp',
|
|
121
|
+
desc: 'MCP servers',
|
|
122
|
+
args: '[add|remove|reconnect|list]',
|
|
123
|
+
subs: [
|
|
124
|
+
{ name: 'add', desc: 'Add a server from a claude mcp add line' },
|
|
125
|
+
{ name: 'remove', desc: 'Remove a server by name' },
|
|
126
|
+
{ name: 'reconnect', desc: 'Reconnect a server by name' },
|
|
127
|
+
{ name: 'list', desc: 'List configured servers' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
112
130
|
{ name: 'login', desc: 'Sign in to ikie account' },
|
|
113
131
|
{ name: 'logout', desc: 'Sign out of ikie account' },
|
|
114
132
|
{ name: 'exit', desc: 'Exit Ikie' },
|
|
115
133
|
];
|
|
134
|
+
// ── Interactive picker state (arrow-key selectors like /theme) ──
|
|
135
|
+
let pickerActive = false;
|
|
136
|
+
let pickerItems = [];
|
|
137
|
+
let pickerSel = 0;
|
|
138
|
+
let pickerResolve = null;
|
|
139
|
+
let drawPickerFn = null;
|
|
140
|
+
let pickerTitle = '';
|
|
141
|
+
let pickerActiveValue = null;
|
|
142
|
+
// ── Rotating tips shown below the prompt ─────────────────────────
|
|
143
|
+
const TIPS = [
|
|
144
|
+
'Use /theme to change the color theme',
|
|
145
|
+
'Type /model to switch the AI model',
|
|
146
|
+
'Use /help to see all commands',
|
|
147
|
+
'Press Esc to cancel a running task',
|
|
148
|
+
'Use /session to manage chat sessions',
|
|
149
|
+
'Type /clear to reset the conversation',
|
|
150
|
+
'Use /settings to adjust your preferences',
|
|
151
|
+
'Type /usage to check your API usage',
|
|
152
|
+
'Use /context to see project context',
|
|
153
|
+
'Use /image to attach images',
|
|
154
|
+
'Type /tokens for token estimates',
|
|
155
|
+
'Use /mode to switch between plan and agent mode',
|
|
156
|
+
'Type /compact to summarize the conversation',
|
|
157
|
+
'Use /memory to save notes',
|
|
158
|
+
'Type /exit to quit ikie',
|
|
159
|
+
'Use /skills to manage skills',
|
|
160
|
+
'Type /tree to show the file tree',
|
|
161
|
+
'Use /rpm to set the requests-per-minute limit',
|
|
162
|
+
];
|
|
163
|
+
let tipIndex = Date.now(); // start at a pseudo-random position
|
|
116
164
|
/**
|
|
117
165
|
* Compute live slash-menu items for the current input line.
|
|
118
166
|
* Returns rich items (name/desc/insert) plus the typed fragment to highlight.
|
|
@@ -137,6 +185,13 @@ function computeSlashMenu(line) {
|
|
|
137
185
|
// Command-name stage: "/<frag>".
|
|
138
186
|
const items = SLASH_CMDS
|
|
139
187
|
.filter(s => s.name.startsWith(cmd))
|
|
188
|
+
.sort((a, b) => {
|
|
189
|
+
if (a.name === cmd)
|
|
190
|
+
return -1;
|
|
191
|
+
if (b.name === cmd)
|
|
192
|
+
return 1;
|
|
193
|
+
return 0;
|
|
194
|
+
})
|
|
140
195
|
.map(s => ({
|
|
141
196
|
name: s.name,
|
|
142
197
|
desc: s.desc,
|
|
@@ -160,6 +215,7 @@ ${c.primary.bold('Ikie Commands')}
|
|
|
160
215
|
${c.warning('/session load')} Load saved chat
|
|
161
216
|
${c.warning('/session new')} Start a fresh chat (auto-saved)
|
|
162
217
|
${c.warning('/context')} Show current project context
|
|
218
|
+
${c.warning('/tree [path]')} Show file tree structure
|
|
163
219
|
${c.warning('/models')} List available models
|
|
164
220
|
${c.warning('/model <name>')} Switch AI model
|
|
165
221
|
${c.warning('/settings')} View all settings
|
|
@@ -181,6 +237,10 @@ ${c.primary.bold('Ikie Commands')}
|
|
|
181
237
|
${c.warning('/skills show <name>')} Show a skill's instructions
|
|
182
238
|
${c.warning('/skills install <src>')} Install a skill (git URL or path)
|
|
183
239
|
${c.warning('/skills remove <name>')} Remove an installed skill
|
|
240
|
+
${c.warning('/mcp')} List MCP servers and tools
|
|
241
|
+
${c.warning('/mcp add <line>')} Add a server from a claude mcp add line
|
|
242
|
+
${c.warning('/mcp remove <name>')} Remove a server by name
|
|
243
|
+
${c.warning('/mcp reconnect <name>')} Reconnect a server by name
|
|
184
244
|
${c.warning('/plan')} Plan mode — research & propose, no changes
|
|
185
245
|
${c.warning('/agent')} Agent mode — full execution (default)
|
|
186
246
|
${c.warning('/mode')} Show or set mode ${c.muted('(Shift+Tab toggles)')}
|
|
@@ -356,6 +416,15 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
356
416
|
console.log(`\n${c.primary.bold('Project Context')}\n`);
|
|
357
417
|
console.log(renderMarkdown(projectContext));
|
|
358
418
|
return true;
|
|
419
|
+
case 'tree': {
|
|
420
|
+
const { path, options } = parseTreeArgs(args);
|
|
421
|
+
console.log();
|
|
422
|
+
console.log(generateTree(path, options));
|
|
423
|
+
console.log();
|
|
424
|
+
console.log(c.muted(' Usage: /tree [path] [-a all] [-s size] [-d depth]'));
|
|
425
|
+
console.log(c.muted(' Example: /tree src -a -d 4'));
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
359
428
|
case 'models': {
|
|
360
429
|
const sub = args[0]?.toLowerCase();
|
|
361
430
|
if (sub === 'refresh' || !sub || sub === 'list') {
|
|
@@ -370,7 +439,8 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
370
439
|
}
|
|
371
440
|
else {
|
|
372
441
|
config.model = model;
|
|
373
|
-
|
|
442
|
+
config.modelPinned = true;
|
|
443
|
+
saveConfig({ model, modelPinned: true });
|
|
374
444
|
console.log(successLine(`Default model set to: ${model}`));
|
|
375
445
|
console.log(infoLine('Persisted to settings — will be used for all new sessions.'));
|
|
376
446
|
}
|
|
@@ -477,6 +547,71 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
477
547
|
await runOnboarding(config);
|
|
478
548
|
return true;
|
|
479
549
|
}
|
|
550
|
+
case 'mcp': {
|
|
551
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
552
|
+
const rest = args.slice(1).join(' ').trim();
|
|
553
|
+
const manager = getMcpManager();
|
|
554
|
+
if (sub === 'add') {
|
|
555
|
+
if (!rest) {
|
|
556
|
+
console.log(errorLine('Usage: /mcp add <claude mcp add line>'));
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
const parsed = parseClaudeMcpAddCommand(rest);
|
|
560
|
+
if ('error' in parsed) {
|
|
561
|
+
console.log(errorLine(parsed.error));
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
await manager.addServer(parsed.name, parsed.entry, 'project');
|
|
566
|
+
console.log(successLine(`Added MCP server "${parsed.name}" and connected.`));
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
570
|
+
}
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
if (sub === 'remove' || sub === 'rm') {
|
|
574
|
+
if (!rest) {
|
|
575
|
+
console.log(errorLine('Usage: /mcp remove <name>'));
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
const removed = manager.removeServer(rest, 'project');
|
|
579
|
+
console.log(removed ? successLine(`Removed MCP server "${rest}".`) : errorLine(`No MCP server named "${rest}" in project scope.`));
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
if (sub === 'reconnect') {
|
|
583
|
+
if (!rest) {
|
|
584
|
+
console.log(errorLine('Usage: /mcp reconnect <name>'));
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
await manager.reconnectServer(rest);
|
|
589
|
+
console.log(successLine(`Reconnected MCP server "${rest}".`));
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
console.log(errorLine(err instanceof Error ? err.message : String(err)));
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
const servers = manager.listServers();
|
|
597
|
+
if (!servers.length) {
|
|
598
|
+
console.log(infoLine('No MCP servers configured.'));
|
|
599
|
+
console.log(`\n ${c.muted('Add one with')} ${c.warning('/mcp add <claude mcp add line>')}`);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
console.log(`\n${c.primary.bold('MCP Servers')}\n`);
|
|
603
|
+
for (const s of servers) {
|
|
604
|
+
const status = s.connected ? c.success('●') : s.enabled ? c.warning('○') : c.error('○');
|
|
605
|
+
console.log(` ${status} ${c.white.bold(s.name)} ${s.connected ? c.success('connected') : s.enabled ? c.muted('not connected') : c.error('disabled')}`);
|
|
606
|
+
if (s.error)
|
|
607
|
+
console.log(` ${c.error(s.error)}`);
|
|
608
|
+
if (s.tools.length) {
|
|
609
|
+
console.log(` ${c.muted('tools:')} ${s.tools.map(t => t.name).join(', ')}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
console.log();
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
480
615
|
case 'logout': {
|
|
481
616
|
if (!isLoggedIn(config)) {
|
|
482
617
|
console.log(infoLine('Not signed in.'));
|
|
@@ -515,7 +650,8 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
515
650
|
return true;
|
|
516
651
|
}
|
|
517
652
|
config.model = value;
|
|
518
|
-
|
|
653
|
+
config.modelPinned = true;
|
|
654
|
+
saveConfig({ model: value, modelPinned: true });
|
|
519
655
|
console.log(successLine(`Default model changed to: ${value}`));
|
|
520
656
|
return true;
|
|
521
657
|
}
|
|
@@ -598,6 +734,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
598
734
|
}
|
|
599
735
|
if (sub === 'reset') {
|
|
600
736
|
config.model = DEFAULT_MODEL;
|
|
737
|
+
config.modelPinned = false;
|
|
601
738
|
config.maxTokens = 32768;
|
|
602
739
|
config.temperature = 0.6;
|
|
603
740
|
config.topP = 0.95;
|
|
@@ -606,6 +743,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
606
743
|
config.requestsPerMinute = 10;
|
|
607
744
|
config.theme = 'nebula';
|
|
608
745
|
saveConfig({
|
|
746
|
+
modelPinned: false,
|
|
609
747
|
maxTokens: 32768,
|
|
610
748
|
temperature: 0.6,
|
|
611
749
|
topP: 0.95,
|
|
@@ -848,43 +986,52 @@ function formatTaskTimeline(agent, elapsed, state) {
|
|
|
848
986
|
const tail = state === 'cancelled' ? `cancelled after ${elapsed}` : `${state} in ${elapsed}`;
|
|
849
987
|
return `${modelLabel} · ${toolLabel}${filesLabel} · ${tail}`;
|
|
850
988
|
}
|
|
989
|
+
function printTaskBreakdown(agent, elapsedMs, tokensUsed) {
|
|
990
|
+
const stats = agent.getLastTurnStats();
|
|
991
|
+
const totalSec = (elapsedMs / 1000).toFixed(1);
|
|
992
|
+
console.log(`\n${c.primary('┌')} ${c.white.bold('Task Summary')}`);
|
|
993
|
+
console.log(`${c.primary('│')} ${c.muted('Total time:')} ${c.accent(totalSec + 's')}`);
|
|
994
|
+
console.log(`${c.primary('│')} ${c.muted('AI calls:')} ${c.secondary(stats.modelCalls.toString())} ${c.dim('(~' + tokensUsed.toLocaleString() + ' tokens)')}`);
|
|
995
|
+
console.log(`${c.primary('│')} ${c.muted('Tools executed:')} ${c.secondary(stats.toolCalls.toString())}`);
|
|
996
|
+
if (stats.filesChanged > 0) {
|
|
997
|
+
console.log(`${c.primary('│')} ${c.muted('Files changed:')} ${c.warning(stats.filesChanged.toString())}`);
|
|
998
|
+
}
|
|
999
|
+
// Rough cost estimate (assuming ~$0.003 per 1K tokens for Claude Sonnet)
|
|
1000
|
+
const estimatedCost = (tokensUsed / 1000) * 0.003;
|
|
1001
|
+
if (estimatedCost > 0.001) {
|
|
1002
|
+
console.log(`${c.primary('│')} ${c.muted('Est. cost:')} ${c.dim('~$' + estimatedCost.toFixed(4))}`);
|
|
1003
|
+
}
|
|
1004
|
+
console.log(`${c.primary('└')}${'─'.repeat(40)}\n`);
|
|
1005
|
+
}
|
|
851
1006
|
function selectModelInteractively(rl, config) {
|
|
852
1007
|
return new Promise((resolve) => {
|
|
853
|
-
const models = [];
|
|
854
|
-
const prompt = () => {
|
|
855
|
-
rl.question(c.muted('Enter number (or 0 to cancel): '), (answer) => {
|
|
856
|
-
const num = parseInt(answer, 10);
|
|
857
|
-
if (isNaN(num) || num < 0 || num > models.length) {
|
|
858
|
-
prompt();
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (num === 0) {
|
|
862
|
-
console.log(infoLine('Model selection cancelled.'));
|
|
863
|
-
resolve();
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const chosen = models[num - 1];
|
|
867
|
-
config.model = chosen.name;
|
|
868
|
-
saveConfig({ model: chosen.name });
|
|
869
|
-
console.log(successLine(`Switched to ${chosen.name} — ${chosen.display_name}`));
|
|
870
|
-
resolve();
|
|
871
|
-
});
|
|
872
|
-
};
|
|
873
|
-
console.log(`\n ${c.accent.bold('Select a Model')}`);
|
|
874
1008
|
fetchModelsFromServer(config).then((fetched) => {
|
|
875
|
-
|
|
876
|
-
if (models.length === 0) {
|
|
1009
|
+
if (fetched.length === 0) {
|
|
877
1010
|
console.log(` ${c.error('No models available.')}`);
|
|
878
1011
|
resolve();
|
|
879
1012
|
return;
|
|
880
1013
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1014
|
+
pickerTitle = 'Select a Model';
|
|
1015
|
+
pickerActiveValue = config.model;
|
|
1016
|
+
pickerItems = fetched.map(m => ({
|
|
1017
|
+
name: m.name,
|
|
1018
|
+
desc: `${m.display_name} (${(m.context_window / 1024).toFixed(0)}K)${m.is_default ? ' [DEFAULT]' : ''}`,
|
|
1019
|
+
}));
|
|
1020
|
+
pickerSel = Math.max(0, fetched.findIndex(m => m.name === config.model));
|
|
1021
|
+
pickerActive = true;
|
|
1022
|
+
pickerResolve = (chosen) => {
|
|
1023
|
+
if (chosen) {
|
|
1024
|
+
config.model = chosen;
|
|
1025
|
+
config.modelPinned = true;
|
|
1026
|
+
saveConfig({ model: chosen, modelPinned: true });
|
|
1027
|
+
console.log(successLine(`Switched to ${chosen}`));
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
console.log(infoLine('Model selection cancelled.'));
|
|
1031
|
+
}
|
|
1032
|
+
resolve();
|
|
1033
|
+
};
|
|
1034
|
+
drawPickerFn?.();
|
|
888
1035
|
}).catch(() => {
|
|
889
1036
|
console.log(` ${c.error('Failed to load models.')}`);
|
|
890
1037
|
resolve();
|
|
@@ -894,37 +1041,24 @@ function selectModelInteractively(rl, config) {
|
|
|
894
1041
|
function selectThemeInteractively(rl, config) {
|
|
895
1042
|
return new Promise((resolve) => {
|
|
896
1043
|
const themesList = Object.keys(THEMES);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
if (num === 0) {
|
|
907
|
-
console.log(infoLine('Theme selection cancelled.'));
|
|
908
|
-
resolve();
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
const chosen = themesList[num - 1];
|
|
1044
|
+
pickerTitle = 'Select a Theme';
|
|
1045
|
+
pickerActiveValue = config.theme;
|
|
1046
|
+
pickerItems = themesList.map(name => ({ name, desc: THEMES[name].description }));
|
|
1047
|
+
pickerSel = Math.max(0, themesList.indexOf(config.theme));
|
|
1048
|
+
pickerActive = true;
|
|
1049
|
+
pickerResolve = (chosen) => {
|
|
1050
|
+
if (chosen) {
|
|
912
1051
|
setTheme(chosen);
|
|
913
1052
|
config.theme = chosen;
|
|
914
1053
|
drawBanner(config.model);
|
|
915
1054
|
console.log(successLine(`Theme switched to "${chosen}".`));
|
|
916
|
-
|
|
917
|
-
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
console.log(infoLine('Theme selection cancelled.'));
|
|
1058
|
+
}
|
|
1059
|
+
resolve();
|
|
918
1060
|
};
|
|
919
|
-
|
|
920
|
-
themesList.forEach((name, i) => {
|
|
921
|
-
const theme = THEMES[name];
|
|
922
|
-
const num = c.primary((i + 1).toString().padStart(2));
|
|
923
|
-
const active = i === currentIndex ? ` ${c.success('[ACTIVE]')}` : '';
|
|
924
|
-
console.log(` ${num}) ${c.bold(name.padEnd(12))} ${c.muted('-')} ${c.dim(theme.description)}${active}`);
|
|
925
|
-
});
|
|
926
|
-
console.log(` ${c.muted(' 0)')} Cancel`);
|
|
927
|
-
prompt();
|
|
1061
|
+
drawPickerFn?.();
|
|
928
1062
|
});
|
|
929
1063
|
}
|
|
930
1064
|
// Notify the user when a long task finishes (> 10s). Terminal bell always;
|
|
@@ -964,14 +1098,34 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
964
1098
|
// A plain launch arg is a one-shot prompt: run it once and exit.
|
|
965
1099
|
const initialCommand = oneShot && oneShot.trim().startsWith('/') ? oneShot.trim() : undefined;
|
|
966
1100
|
if (oneShot && !initialCommand) {
|
|
1101
|
+
const abortController = new AbortController();
|
|
1102
|
+
// Set up Esc/Ctrl-C handler for one-shot mode too.
|
|
1103
|
+
if (process.stdin.isTTY) {
|
|
1104
|
+
const wasRaw = process.stdin.isRaw ?? false;
|
|
1105
|
+
if (!wasRaw)
|
|
1106
|
+
process.stdin.setRawMode(true);
|
|
1107
|
+
process.stdin.resume();
|
|
1108
|
+
const cancelHandler = (data) => {
|
|
1109
|
+
const b = data[0];
|
|
1110
|
+
if ((b === 0x1b && data.length === 1) || b === 0x03) {
|
|
1111
|
+
if (!abortController.signal.aborted) {
|
|
1112
|
+
abortController.abort();
|
|
1113
|
+
process.stdout.write(`\n${c.warning(' Cancelled')}\n`);
|
|
1114
|
+
}
|
|
1115
|
+
if (b === 0x03)
|
|
1116
|
+
process.exit(0);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
process.stdin.on('data', cancelHandler);
|
|
1120
|
+
}
|
|
967
1121
|
try {
|
|
968
|
-
await agent.send(oneShot, { autoApprove: config.autoApprove });
|
|
1122
|
+
await agent.send(oneShot, { autoApprove: config.autoApprove, signal: abortController.signal });
|
|
1123
|
+
process.exit(0);
|
|
969
1124
|
}
|
|
970
1125
|
catch (err) {
|
|
971
1126
|
console.error(errorLine(`Agent error: ${err}`));
|
|
972
1127
|
process.exit(1);
|
|
973
1128
|
}
|
|
974
|
-
return;
|
|
975
1129
|
}
|
|
976
1130
|
drawBanner(config.model);
|
|
977
1131
|
const rl = readline.createInterface({
|
|
@@ -981,6 +1135,16 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
981
1135
|
historySize: MAX_HISTORY,
|
|
982
1136
|
prompt: PROMPT,
|
|
983
1137
|
});
|
|
1138
|
+
// Enable bracketed-paste mode so the terminal wraps pasted text in
|
|
1139
|
+
// \x1b[200~ … \x1b[201~ markers. This lets a multi-chunk paste (a long
|
|
1140
|
+
// paragraph the TTY delivers across several reads) be coalesced into ONE
|
|
1141
|
+
// paste instead of being split into several. Disabled again on exit.
|
|
1142
|
+
const setBracketedPaste = (on) => {
|
|
1143
|
+
if (process.stdout.isTTY)
|
|
1144
|
+
process.stdout.write(on ? '\x1b[?2004h' : '\x1b[?2004l');
|
|
1145
|
+
};
|
|
1146
|
+
setBracketedPaste(true);
|
|
1147
|
+
process.on('exit', () => setBracketedPaste(false));
|
|
984
1148
|
let multilineBuffer = '';
|
|
985
1149
|
let busy = false;
|
|
986
1150
|
let ctrlCCount = 0;
|
|
@@ -999,6 +1163,8 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
999
1163
|
let menuFragment = '';
|
|
1000
1164
|
let menuSel = 0;
|
|
1001
1165
|
let menuRows = 0; // menu lines currently drawn below input
|
|
1166
|
+
// ── Interactive picker state (arrow-key selectors) ──────────────────────────
|
|
1167
|
+
let pickerRows = 0;
|
|
1002
1168
|
const inputCol = () => stripAnsi(currentPrompt).length + (rl.cursor ?? 0);
|
|
1003
1169
|
const eraseMenu = () => {
|
|
1004
1170
|
if (menuRows === 0)
|
|
@@ -1029,6 +1195,40 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1029
1195
|
process.stdout.write(out);
|
|
1030
1196
|
menuRows = rows.length;
|
|
1031
1197
|
};
|
|
1198
|
+
const erasePicker = () => {
|
|
1199
|
+
if (pickerRows === 0)
|
|
1200
|
+
return;
|
|
1201
|
+
let out = '\r';
|
|
1202
|
+
for (let i = 0; i < pickerRows; i++)
|
|
1203
|
+
out += '\n\x1b[2K';
|
|
1204
|
+
out += `\x1b[${pickerRows}A\r`;
|
|
1205
|
+
process.stdout.write(out);
|
|
1206
|
+
pickerRows = 0;
|
|
1207
|
+
};
|
|
1208
|
+
const drawPicker = () => {
|
|
1209
|
+
erasePicker();
|
|
1210
|
+
if (!pickerActive || pickerItems.length === 0)
|
|
1211
|
+
return;
|
|
1212
|
+
const rows = [];
|
|
1213
|
+
rows.push(` ${c.accent.bold(pickerTitle)} ${c.muted('(↑↓ · Enter · Esc)')}`);
|
|
1214
|
+
rows.push('');
|
|
1215
|
+
pickerItems.forEach((item, i) => {
|
|
1216
|
+
const sel = i === pickerSel;
|
|
1217
|
+
const active = item.name === pickerActiveValue;
|
|
1218
|
+
const bullet = sel ? c.primary('●') : c.muted('○');
|
|
1219
|
+
const name = sel ? c.white.bold(item.name) : c.white(item.name);
|
|
1220
|
+
const tag = active ? ` ${c.success('[ACTIVE]')}` : '';
|
|
1221
|
+
const desc = item.desc ? ` ${c.dim(item.desc)}` : '';
|
|
1222
|
+
rows.push(` ${bullet} ${name}${tag}${desc}`);
|
|
1223
|
+
});
|
|
1224
|
+
let out = '\r';
|
|
1225
|
+
for (const r of rows)
|
|
1226
|
+
out += `\n${r}`;
|
|
1227
|
+
out += `\x1b[${rows.length}A\r`;
|
|
1228
|
+
process.stdout.write(out);
|
|
1229
|
+
pickerRows = rows.length;
|
|
1230
|
+
};
|
|
1231
|
+
drawPickerFn = drawPicker;
|
|
1032
1232
|
const closeMenu = () => {
|
|
1033
1233
|
if (!menuOpen && menuRows === 0)
|
|
1034
1234
|
return;
|
|
@@ -1083,6 +1283,13 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1083
1283
|
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
1084
1284
|
if (!normalized)
|
|
1085
1285
|
return;
|
|
1286
|
+
// Short, single-line pastes (a word, a path, a URL) belong inline as if
|
|
1287
|
+
// typed — only collapse multi-line or long pastes into a [Pasted #N] block.
|
|
1288
|
+
if (!normalized.includes('\n') && normalized.length <= 200) {
|
|
1289
|
+
forwardToReadline(Buffer.from(normalized, 'utf8'));
|
|
1290
|
+
updateMenu();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1086
1293
|
const token = `[Pasted #${++pasteSeq}: ${pasteSummary(normalized)}]`;
|
|
1087
1294
|
pastedBlocks.set(token, normalized);
|
|
1088
1295
|
rl.write(token);
|
|
@@ -1112,18 +1319,23 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1112
1319
|
if (text === '\r' || text === '\n') {
|
|
1113
1320
|
const item = menuItems[menuSel];
|
|
1114
1321
|
if (item) {
|
|
1115
|
-
|
|
1116
|
-
// space) submit it immediately; otherwise keep editing for the arg.
|
|
1117
|
-
const submit = !item.insert.endsWith(' ');
|
|
1322
|
+
const line = rl.line ?? '';
|
|
1118
1323
|
eraseMenu();
|
|
1119
1324
|
menuOpen = false;
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1325
|
+
// Line already exactly matches the selected command — submit directly.
|
|
1326
|
+
if (line === `/${item.name}`) {
|
|
1327
|
+
forwardToReadline(data);
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
const submit = !item.insert.endsWith(' ');
|
|
1331
|
+
rl.write(null, { ctrl: true, name: 'e' });
|
|
1332
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
1333
|
+
rl.write(item.insert);
|
|
1334
|
+
if (submit)
|
|
1335
|
+
forwardToReadline(Buffer.from('\r'));
|
|
1336
|
+
else
|
|
1337
|
+
updateMenu();
|
|
1338
|
+
}
|
|
1127
1339
|
}
|
|
1128
1340
|
else {
|
|
1129
1341
|
closeMenu();
|
|
@@ -1132,6 +1344,33 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1132
1344
|
return;
|
|
1133
1345
|
}
|
|
1134
1346
|
}
|
|
1347
|
+
// ── Interactive picker (arrow-key selection like /theme) ──
|
|
1348
|
+
if (pickerActive) {
|
|
1349
|
+
if (text === '\x1b[A') {
|
|
1350
|
+
pickerSel = (pickerSel - 1 + pickerItems.length) % pickerItems.length;
|
|
1351
|
+
drawPicker();
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (text === '\x1b[B') {
|
|
1355
|
+
pickerSel = (pickerSel + 1) % pickerItems.length;
|
|
1356
|
+
drawPicker();
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (text === '\r' || text === '\n') {
|
|
1360
|
+
const picked = pickerItems[pickerSel].name;
|
|
1361
|
+
pickerActive = false;
|
|
1362
|
+
erasePicker();
|
|
1363
|
+
pickerResolve?.(picked);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (text === '\x1b') {
|
|
1367
|
+
pickerActive = false;
|
|
1368
|
+
erasePicker();
|
|
1369
|
+
pickerResolve?.(null);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
return; // eat all other input while picker is active
|
|
1373
|
+
}
|
|
1135
1374
|
// Shift+Tab (CSI Z) → cycle agent⇄plan mode live, updating ONLY the mode
|
|
1136
1375
|
// word in the header line directly above the prompt. No new line, no
|
|
1137
1376
|
// reprinted prompt — the cursor and whatever the user has typed stay put.
|
|
@@ -1231,6 +1470,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1231
1470
|
if (multilineBuffer) {
|
|
1232
1471
|
currentPrompt = CONTINUE_PROMPT;
|
|
1233
1472
|
rl.setPrompt(CONTINUE_PROMPT);
|
|
1473
|
+
rl.prompt();
|
|
1234
1474
|
}
|
|
1235
1475
|
else {
|
|
1236
1476
|
printPromptHeader(agent.getMode());
|
|
@@ -1246,10 +1486,14 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1246
1486
|
}
|
|
1247
1487
|
currentPrompt = prompt;
|
|
1248
1488
|
rl.setPrompt(prompt);
|
|
1489
|
+
rl.prompt();
|
|
1490
|
+
// Rotating tip below the prompt
|
|
1491
|
+
const tip = TIPS[tipIndex++ % TIPS.length];
|
|
1492
|
+
process.stdout.write(`\x1b[s\n${c.dim(' ' + tip)}\x1b[u`);
|
|
1249
1493
|
}
|
|
1250
|
-
rl.prompt();
|
|
1251
1494
|
};
|
|
1252
1495
|
rl.on('close', () => {
|
|
1496
|
+
setBracketedPaste(false);
|
|
1253
1497
|
printGoodbye(sessionState, agent, config);
|
|
1254
1498
|
process.exit(0);
|
|
1255
1499
|
});
|
package/dist/skills.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export interface Skill {
|
|
|
22
22
|
source: 'project' | 'user';
|
|
23
23
|
/** The roots this skill was discovered under, e.g. ".ikie" or ".claude". */
|
|
24
24
|
origin: string;
|
|
25
|
+
/** Tools the skill pre-approves for the session. */
|
|
26
|
+
allowedTools?: string[];
|
|
27
|
+
/** Tools the skill explicitly forbids. */
|
|
28
|
+
disallowedTools?: string[];
|
|
29
|
+
/** If true, do not show the skill in the catalog or offer it to the model. */
|
|
30
|
+
disableModelInvocation?: boolean;
|
|
31
|
+
/** If true, the user can invoke the skill directly (e.g. via /skill-name). */
|
|
32
|
+
userInvocable?: boolean;
|
|
33
|
+
/** Extra guidance on when the skill should be used. */
|
|
34
|
+
whenToUse?: string;
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
37
|
* Discover all available skills across project and user roots.
|
|
@@ -37,8 +47,14 @@ export declare function listSkillFiles(skill: Skill): string[];
|
|
|
37
47
|
* Render the skill catalog for the system prompt: each skill's name +
|
|
38
48
|
* description, so the model knows what exists and when to reach for one.
|
|
39
49
|
* Returns '' when no skills are installed (so the section is omitted).
|
|
50
|
+
* Skips skills marked disable-model-invocation.
|
|
40
51
|
*/
|
|
41
52
|
export declare function formatSkillsForPrompt(skills: Skill[]): string;
|
|
53
|
+
/**
|
|
54
|
+
* Map Claude-style allowed-tools tokens to ikie tool names.
|
|
55
|
+
* Strips arguments, lowercases, and maps known tokens.
|
|
56
|
+
*/
|
|
57
|
+
export declare function mapAllowedTools(tokens: string[] | undefined): string[];
|
|
42
58
|
/**
|
|
43
59
|
* Build the full text returned to the model when it loads a skill via
|
|
44
60
|
* `use_skill`: the instructions plus a manifest of bundled files and the
|