ikie-cli 0.1.14 → 0.1.16

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/config.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare const CONFIG_FILE: string;
3
3
  export declare const GLOBAL_MEMORY_FILE: string;
4
4
  export declare const SESSIONS_DIR: string;
5
5
  export declare const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
6
- export declare const DEFAULT_MODEL = "accounts/fireworks/models/kimi-k2p7-code";
6
+ export declare const DEFAULT_MODEL = "kimi-k2p7-code";
7
7
  /**
8
8
  * The hosted ikie API (masks the upstream provider behind ik_live_ keys).
9
9
  * Override with IKIE_HOST env var, e.g. for local dev or a custom domain.
package/dist/config.js CHANGED
@@ -6,7 +6,7 @@ export const CONFIG_FILE = join(HOME_DIR, 'config.json');
6
6
  export const GLOBAL_MEMORY_FILE = join(HOME_DIR, 'memory.md');
7
7
  export const SESSIONS_DIR = join(HOME_DIR, 'sessions');
8
8
  export const FIREWORKS_BASE_URL = 'https://api.fireworks.ai/inference/v1';
9
- export const DEFAULT_MODEL = 'accounts/fireworks/models/kimi-k2p7-code';
9
+ export const DEFAULT_MODEL = 'kimi-k2p7-code';
10
10
  /**
11
11
  * The hosted ikie API (masks the upstream provider behind ik_live_ keys).
12
12
  * Override with IKIE_HOST env var, e.g. for local dev or a custom domain.
package/dist/repl.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as readline from 'node:readline';
2
2
  import { execSync, exec } from 'child_process';
3
3
  import { restoreStdinListeners } from './agent.js';
4
- import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, } from './theme.js';
4
+ import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, renderSlashMenu, } from './theme.js';
5
5
  import { renderMarkdown } from './renderer.js';
6
6
  import { loadAllMemory } from './memory.js';
7
7
  import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey } from './config.js';
@@ -34,7 +34,6 @@ async function fetchAndDisplayModels(config) {
34
34
  const defaultLabel = model.is_default ? c.success(' [DEFAULT]') : '';
35
35
  const contextInfo = c.muted(`(${(model.context_window / 1024).toFixed(0)}K context)`);
36
36
  console.log(` ${c.secondary(model.name.padEnd(24))} ${c.white(model.display_name)}${defaultLabel} ${contextInfo}`);
37
- console.log(` ${c.dim(model.provider_model_id)}`);
38
37
  }
39
38
  console.log(`\n${c.muted('To switch models:')}`);
40
39
  console.log(` ${c.accent('/model')} ${c.muted('<name>')}`);
@@ -102,22 +101,36 @@ const SLASH_CMDS = [
102
101
  { name: 'logout', desc: 'Sign out of ikie account' },
103
102
  { name: 'exit', desc: 'Exit Ikie' },
104
103
  ];
105
- function slashCompleter(line) {
104
+ /**
105
+ * Compute live slash-menu items for the current input line.
106
+ * Returns rich items (name/desc/insert) plus the typed fragment to highlight.
107
+ */
108
+ function computeSlashMenu(line) {
106
109
  if (!line.startsWith('/'))
107
- return [[], line];
108
- const parts = line.slice(1).split(/\s+/);
110
+ return { items: [], fragment: '' };
111
+ const afterSlash = line.slice(1);
112
+ const parts = afterSlash.split(/\s+/);
109
113
  const cmd = parts[0]?.toLowerCase() ?? '';
114
+ // Sub-command stage: "/cmd <frag>" where cmd has subs.
110
115
  if (parts.length >= 2) {
111
- const match = SLASH_CMDS.find(c => c.name === cmd);
112
- if (match?.subs) {
113
- const sub = parts[1].toLowerCase();
114
- const hits = match.subs.filter(s => s.name.startsWith(sub)).map(s => `/${cmd} ${s.name}`);
115
- return [hits, line];
116
- }
117
- return [[], line];
116
+ const match = SLASH_CMDS.find(s => s.name === cmd);
117
+ if (!match?.subs)
118
+ return { items: [], fragment: '' };
119
+ const frag = parts[1].toLowerCase();
120
+ const items = match.subs
121
+ .filter(s => s.name.startsWith(frag))
122
+ .map(s => ({ name: `${cmd} ${s.name}`, desc: s.desc, insert: `/${cmd} ${s.name} ` }));
123
+ return { items, fragment: frag };
118
124
  }
119
- const hits = SLASH_CMDS.filter(c => c.name.startsWith(cmd)).map(c => '/' + c.name);
120
- return [hits, line];
125
+ // Command-name stage: "/<frag>".
126
+ const items = SLASH_CMDS
127
+ .filter(s => s.name.startsWith(cmd))
128
+ .map(s => ({
129
+ name: s.name,
130
+ desc: s.desc,
131
+ insert: `/${s.name}${s.subs || s.args ? ' ' : ''}`,
132
+ }));
133
+ return { items, fragment: cmd };
121
134
  }
122
135
  function printHelp() {
123
136
  console.log(`
@@ -732,7 +745,8 @@ function selectModelInteractively(rl, config) {
732
745
  return;
733
746
  }
734
747
  const chosen = models[num - 1];
735
- config.model = chosen.provider_model_id;
748
+ config.model = chosen.name;
749
+ saveConfig({ model: chosen.name });
736
750
  console.log(successLine(`Switched to ${chosen.name} — ${chosen.display_name}`));
737
751
  resolve();
738
752
  });
@@ -847,7 +861,6 @@ export async function startREPL(agent, config, projectContext, oneShot) {
847
861
  terminal: true,
848
862
  historySize: MAX_HISTORY,
849
863
  prompt: PROMPT,
850
- completer: slashCompleter,
851
864
  });
852
865
  let multilineBuffer = '';
853
866
  let busy = false;
@@ -860,6 +873,77 @@ export async function startREPL(agent, config, projectContext, oneShot) {
860
873
  let pasteMode = false;
861
874
  let pasteBuffer = '';
862
875
  const pastedBlocks = new Map();
876
+ // ── Live slash-command menu state ──────────────────────────────────────────
877
+ let currentPrompt = PROMPT; // the prompt string readline is showing
878
+ let menuOpen = false;
879
+ let menuItems = [];
880
+ let menuFragment = '';
881
+ let menuSel = 0;
882
+ let menuRows = 0; // menu lines currently drawn below input
883
+ const inputCol = () => stripAnsi(currentPrompt).length + (rl.cursor ?? 0);
884
+ const eraseMenu = () => {
885
+ if (menuRows === 0)
886
+ return;
887
+ let out = '\r';
888
+ for (let i = 0; i < menuRows; i++)
889
+ out += '\n\x1b[2K';
890
+ out += `\x1b[${menuRows}A\r`;
891
+ const col = inputCol();
892
+ if (col > 0)
893
+ out += `\x1b[${col}C`;
894
+ process.stdout.write(out);
895
+ menuRows = 0;
896
+ };
897
+ const drawMenu = () => {
898
+ eraseMenu();
899
+ if (!menuOpen || menuItems.length === 0)
900
+ return;
901
+ const cols = Math.max(40, process.stdout.columns ?? 80);
902
+ const rows = renderSlashMenu(menuItems, menuSel, menuFragment, cols);
903
+ let out = '\r';
904
+ for (const r of rows)
905
+ out += `\n\x1b[2K${r}`;
906
+ out += `\x1b[${rows.length}A\r`;
907
+ const col = inputCol();
908
+ if (col > 0)
909
+ out += `\x1b[${col}C`;
910
+ process.stdout.write(out);
911
+ menuRows = rows.length;
912
+ };
913
+ const closeMenu = () => {
914
+ if (!menuOpen && menuRows === 0)
915
+ return;
916
+ menuOpen = false;
917
+ eraseMenu();
918
+ };
919
+ const updateMenu = () => {
920
+ const line = rl.line ?? '';
921
+ const { items, fragment } = computeSlashMenu(line);
922
+ // Hide when the sole match is exactly what's typed (nothing left to suggest).
923
+ const exhausted = items.length === 1 && line === `/${items[0].name}`;
924
+ if (items.length && line.startsWith('/') && !exhausted) {
925
+ menuItems = items;
926
+ menuFragment = fragment;
927
+ menuSel = 0;
928
+ menuOpen = true;
929
+ drawMenu();
930
+ }
931
+ else {
932
+ closeMenu();
933
+ }
934
+ };
935
+ const acceptSelection = () => {
936
+ if (!menuOpen || !menuItems[menuSel])
937
+ return;
938
+ const insert = menuItems[menuSel].insert;
939
+ eraseMenu();
940
+ menuOpen = false;
941
+ // Replace the line via public readline API (cursor→end, kill line, write).
942
+ rl.write(null, { ctrl: true, name: 'e' });
943
+ rl.write(null, { ctrl: true, name: 'u' });
944
+ rl.write(insert);
945
+ updateMenu(); // a trailing space may surface a sub-command menu
946
+ };
863
947
  const readlineDataListeners = process.stdin.rawListeners('data').slice();
864
948
  for (const fn of readlineDataListeners) {
865
949
  process.stdin.removeListener('data', fn);
@@ -886,8 +970,52 @@ export async function startREPL(agent, config, projectContext, oneShot) {
886
970
  };
887
971
  const routeInputData = (data) => {
888
972
  const text = data.toString('utf8');
973
+ // ── Live slash-menu navigation (only while the menu is open) ──
974
+ if (menuOpen) {
975
+ if (text === '\x1b[A') {
976
+ menuSel = (menuSel - 1 + menuItems.length) % menuItems.length;
977
+ drawMenu();
978
+ return;
979
+ }
980
+ if (text === '\x1b[B') {
981
+ menuSel = (menuSel + 1) % menuItems.length;
982
+ drawMenu();
983
+ return;
984
+ }
985
+ if (text === '\t') {
986
+ acceptSelection();
987
+ return;
988
+ }
989
+ if (text === '\x1b') {
990
+ closeMenu();
991
+ return;
992
+ }
993
+ if (text === '\r' || text === '\n') {
994
+ const item = menuItems[menuSel];
995
+ if (item) {
996
+ // Fill the highlighted command. If it takes no args/subs (no trailing
997
+ // space) submit it immediately; otherwise keep editing for the arg.
998
+ const submit = !item.insert.endsWith(' ');
999
+ eraseMenu();
1000
+ menuOpen = false;
1001
+ rl.write(null, { ctrl: true, name: 'e' });
1002
+ rl.write(null, { ctrl: true, name: 'u' });
1003
+ rl.write(item.insert);
1004
+ if (submit)
1005
+ forwardToReadline(Buffer.from('\r'));
1006
+ else
1007
+ updateMenu();
1008
+ }
1009
+ else {
1010
+ closeMenu();
1011
+ forwardToReadline(data);
1012
+ }
1013
+ return;
1014
+ }
1015
+ }
889
1016
  // Shift+Tab (CSI Z) → cycle agent⇄plan mode live at the prompt.
890
1017
  if (text === '\x1b[Z') {
1018
+ closeMenu();
891
1019
  const next = agent.getMode() === 'agent' ? 'plan' : 'agent';
892
1020
  agent.setMode(next);
893
1021
  const saved = rl.line || '';
@@ -900,6 +1028,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
900
1028
  }
901
1029
  // Check for Ctrl+V (0x16) - try to paste image from clipboard
902
1030
  if (data.length === 1 && data[0] === 0x16) {
1031
+ closeMenu();
903
1032
  // Check if clipboard has an image
904
1033
  if (hasClipboardImage()) {
905
1034
  try {
@@ -923,6 +1052,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
923
1052
  return;
924
1053
  }
925
1054
  if (!pasteMode && !text.includes('\x1b[200~') && /[\r\n]/.test(text.trim()) && text.length > 3) {
1055
+ closeMenu();
926
1056
  const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
927
1057
  rl.write(normalized);
928
1058
  return;
@@ -946,11 +1076,13 @@ export async function startREPL(agent, config, projectContext, oneShot) {
946
1076
  const start = rest.indexOf('\x1b[200~');
947
1077
  if (start === -1) {
948
1078
  forwardToReadline(Buffer.from(rest, 'utf8'));
1079
+ updateMenu();
949
1080
  return;
950
1081
  }
951
1082
  if (start > 0) {
952
1083
  forwardToReadline(Buffer.from(rest.slice(0, start), 'utf8'));
953
1084
  }
1085
+ closeMenu();
954
1086
  pasteMode = true;
955
1087
  rest = rest.slice(start + '\x1b[200~'.length);
956
1088
  }
@@ -976,7 +1108,9 @@ export async function startREPL(agent, config, projectContext, oneShot) {
976
1108
  return `${expanded}\n\n${attachments.join('\n\n')}`;
977
1109
  };
978
1110
  const showPrompt = () => {
1111
+ closeMenu();
979
1112
  if (multilineBuffer) {
1113
+ currentPrompt = CONTINUE_PROMPT;
980
1114
  rl.setPrompt(CONTINUE_PROMPT);
981
1115
  }
982
1116
  else {
@@ -991,6 +1125,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
991
1125
  const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
992
1126
  process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
993
1127
  }
1128
+ currentPrompt = prompt;
994
1129
  rl.setPrompt(prompt);
995
1130
  }
996
1131
  rl.prompt();
@@ -1013,6 +1148,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1013
1148
  showPrompt();
1014
1149
  });
1015
1150
  const handleLine = async (raw) => {
1151
+ closeMenu();
1016
1152
  const line = raw.trim();
1017
1153
  if (!line) {
1018
1154
  showPrompt();
package/dist/theme.d.ts CHANGED
@@ -74,6 +74,13 @@ interface DiffOpts {
74
74
  export declare function toolDiffBlock(oldStr: string, newStr: string, ms: number, opts?: DiffOpts): string;
75
75
  export declare function toolSuccessLine(ms: number, preview?: string): string;
76
76
  export declare function toolErrorLine(msg: string): string;
77
+ export interface SlashMenuItem {
78
+ name: string;
79
+ desc: string;
80
+ insert: string;
81
+ }
82
+ /** Render the live slash-command dropdown as an array of fully-styled rows. */
83
+ export declare function renderSlashMenu(items: SlashMenuItem[], sel: number, fragment: string, cols: number): string[];
77
84
  export declare function successLine(msg: string): string;
78
85
  export declare function errorLine(msg: string): string;
79
86
  export declare function warnLine(msg: string): string;
package/dist/theme.js CHANGED
@@ -545,6 +545,33 @@ function formatDuration(ms) {
545
545
  const s = ms / 1000;
546
546
  return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`;
547
547
  }
548
+ /** Render the live slash-command dropdown as an array of fully-styled rows. */
549
+ export function renderSlashMenu(items, sel, fragment, cols) {
550
+ if (!items.length)
551
+ return [];
552
+ const nameW = Math.min(22, Math.max(...items.map(i => i.name.length)));
553
+ const frag = fragment.toLowerCase();
554
+ return items.map((item, i) => {
555
+ const selected = i === sel;
556
+ const bar = selected ? c.primary('▌') : ' ';
557
+ // Highlight the matched prefix within the command name.
558
+ let name;
559
+ if (frag && item.name.toLowerCase().startsWith(frag)) {
560
+ const hit = item.name.slice(0, frag.length);
561
+ const rest = item.name.slice(frag.length);
562
+ name = `${c.accent.bold(hit)}${selected ? c.white.bold(rest) : c.white(rest)}`;
563
+ }
564
+ else {
565
+ name = selected ? c.white.bold(item.name) : c.white(item.name);
566
+ }
567
+ const namePadded = name + ' '.repeat(Math.max(0, nameW - item.name.length));
568
+ // Description, clamped to the remaining width.
569
+ const used = 2 /* bar+space */ + 1 + nameW + 2; // leading "/" + gap
570
+ const descMax = Math.max(8, cols - used - 2);
571
+ const desc = item.desc.length > descMax ? item.desc.slice(0, descMax - 1) + '…' : item.desc;
572
+ return `${bar} ${c.primary('/')}${namePadded} ${c.dim(desc)}`;
573
+ });
574
+ }
548
575
  export function successLine(msg) {
549
576
  return ` ${c.success('ok')} ${c.muted(msg)}`;
550
577
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {