ikie-cli 0.1.33 → 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 +26 -9
- package/dist/agent.d.ts +44 -0
- package/dist/agent.js +372 -120
- 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 +297 -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 +115 -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({
|
|
@@ -1009,6 +1163,8 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1009
1163
|
let menuFragment = '';
|
|
1010
1164
|
let menuSel = 0;
|
|
1011
1165
|
let menuRows = 0; // menu lines currently drawn below input
|
|
1166
|
+
// ── Interactive picker state (arrow-key selectors) ──────────────────────────
|
|
1167
|
+
let pickerRows = 0;
|
|
1012
1168
|
const inputCol = () => stripAnsi(currentPrompt).length + (rl.cursor ?? 0);
|
|
1013
1169
|
const eraseMenu = () => {
|
|
1014
1170
|
if (menuRows === 0)
|
|
@@ -1039,6 +1195,40 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1039
1195
|
process.stdout.write(out);
|
|
1040
1196
|
menuRows = rows.length;
|
|
1041
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;
|
|
1042
1232
|
const closeMenu = () => {
|
|
1043
1233
|
if (!menuOpen && menuRows === 0)
|
|
1044
1234
|
return;
|
|
@@ -1129,18 +1319,23 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1129
1319
|
if (text === '\r' || text === '\n') {
|
|
1130
1320
|
const item = menuItems[menuSel];
|
|
1131
1321
|
if (item) {
|
|
1132
|
-
|
|
1133
|
-
// space) submit it immediately; otherwise keep editing for the arg.
|
|
1134
|
-
const submit = !item.insert.endsWith(' ');
|
|
1322
|
+
const line = rl.line ?? '';
|
|
1135
1323
|
eraseMenu();
|
|
1136
1324
|
menuOpen = false;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
+
}
|
|
1144
1339
|
}
|
|
1145
1340
|
else {
|
|
1146
1341
|
closeMenu();
|
|
@@ -1149,6 +1344,33 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1149
1344
|
return;
|
|
1150
1345
|
}
|
|
1151
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
|
+
}
|
|
1152
1374
|
// Shift+Tab (CSI Z) → cycle agent⇄plan mode live, updating ONLY the mode
|
|
1153
1375
|
// word in the header line directly above the prompt. No new line, no
|
|
1154
1376
|
// reprinted prompt — the cursor and whatever the user has typed stay put.
|
|
@@ -1248,6 +1470,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1248
1470
|
if (multilineBuffer) {
|
|
1249
1471
|
currentPrompt = CONTINUE_PROMPT;
|
|
1250
1472
|
rl.setPrompt(CONTINUE_PROMPT);
|
|
1473
|
+
rl.prompt();
|
|
1251
1474
|
}
|
|
1252
1475
|
else {
|
|
1253
1476
|
printPromptHeader(agent.getMode());
|
|
@@ -1263,8 +1486,11 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1263
1486
|
}
|
|
1264
1487
|
currentPrompt = prompt;
|
|
1265
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`);
|
|
1266
1493
|
}
|
|
1267
|
-
rl.prompt();
|
|
1268
1494
|
};
|
|
1269
1495
|
rl.on('close', () => {
|
|
1270
1496
|
setBracketedPaste(false);
|
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
|