ikie-cli 0.1.39 → 0.1.41

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.js CHANGED
@@ -11,7 +11,7 @@ export const DEFAULT_MODEL = 'kimi-k2p7-code';
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.
13
13
  */
14
- export const IKIE_HOST = process.env.IKIE_HOST ?? 'http://140.245.26.210:3000';
14
+ export const IKIE_HOST = process.env.IKIE_HOST ?? 'http://80.225.207.132:3000';
15
15
  export const IKIE_API_BASE = `${IKIE_HOST}/api/v1`;
16
16
  /**
17
17
  * The port ikie's own host connection uses. A bash command that kills processes
package/dist/index.js CHANGED
@@ -168,7 +168,7 @@ async function main() {
168
168
  const modelsUrl = (config.baseURL ?? IKIE_API_BASE).includes('/api/v1')
169
169
  ? (config.baseURL ?? IKIE_API_BASE).replace('/api/v1', '/api/models')
170
170
  : `${IKIE_HOST}/api/models`;
171
- const res = await fetch(modelsUrl);
171
+ const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(4000) });
172
172
  if (res.ok) {
173
173
  const data = await res.json();
174
174
  const serverDefault = data.models?.find(m => m.is_default);
@@ -201,6 +201,7 @@ ${errorLine('Not signed in.')}
201
201
  const client = new OpenAI({
202
202
  apiKey,
203
203
  baseURL: config.baseURL ?? IKIE_API_BASE,
204
+ timeout: 60000, // 60 seconds timeout to prevent indefinite hangs
204
205
  });
205
206
  const projectCtx = detectProjectContext();
206
207
  const projectContextStr = formatContextForPrompt(projectCtx);
@@ -135,7 +135,7 @@ function displayCompletion() {
135
135
  console.log(c.primary('2.') + ' Type ' + c.accent('/help') + ' to see all commands');
136
136
  console.log(c.primary('3.') + ' Start building something amazing!');
137
137
  console.log();
138
- console.log(c.muted('Dashboard:') + ' ' + c.dim('http://140.245.26.210:3000/dashboard'));
138
+ console.log(c.muted('Dashboard:') + ' ' + c.dim('http://80.225.207.132:3000/dashboard'));
139
139
  console.log();
140
140
  console.log(c.success.bold('Happy coding!'));
141
141
  console.log();
package/dist/repl.js CHANGED
@@ -4,7 +4,7 @@ import { restoreStdinListeners, extractUpstreamError } from './agent.js';
4
4
  import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, promptHeaderText, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, renderSlashMenu, supportsVT, CH, } from './theme.js';
5
5
  import { renderMarkdown } from './renderer.js';
6
6
  import { loadAllMemory } from './memory.js';
7
- import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey } from './config.js';
7
+ import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey, loadConfig } from './config.js';
8
8
  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';
@@ -459,6 +459,12 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
459
459
  }
460
460
  case 'login': {
461
461
  await login();
462
+ const reloaded = loadConfig();
463
+ Object.assign(config, reloaded);
464
+ const freshApiKey = getApiKey(config);
465
+ if (freshApiKey) {
466
+ agent.updateApiKey(freshApiKey);
467
+ }
462
468
  return true;
463
469
  }
464
470
  case 'skills': {
@@ -545,6 +551,12 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
545
551
  case 'onboarding': {
546
552
  console.log();
547
553
  await runOnboarding(config);
554
+ const reloaded = loadConfig();
555
+ Object.assign(config, reloaded);
556
+ const freshApiKey = getApiKey(config);
557
+ if (freshApiKey) {
558
+ agent.updateApiKey(freshApiKey);
559
+ }
548
560
  return true;
549
561
  }
550
562
  case 'mcp': {
@@ -618,6 +630,8 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
618
630
  }
619
631
  else {
620
632
  logout();
633
+ delete config.account;
634
+ agent.updateApiKey('');
621
635
  }
622
636
  return true;
623
637
  }
@@ -1084,6 +1098,36 @@ function notify(message) {
1084
1098
  exec(`powershell -NoProfile -NonInteractive -Command "& {${ps}}"`, () => { });
1085
1099
  }
1086
1100
  }
1101
+ /**
1102
+ * Parse numbered plan steps from an agent's plan-mode response.
1103
+ * Looks for lines like `1. Do something` under a `## Plan` heading.
1104
+ */
1105
+ function parsePlanSteps(conversation) {
1106
+ const lastAssistant = [...conversation].reverse().find(m => m.role === 'assistant');
1107
+ if (!lastAssistant || typeof lastAssistant.content !== 'string')
1108
+ return [];
1109
+ const text = lastAssistant.content;
1110
+ const lines = text.split('\n');
1111
+ const steps = [];
1112
+ let inPlan = false;
1113
+ for (const line of lines) {
1114
+ const t = line.trim();
1115
+ if (/^#{1,3}\s*plan/i.test(t)) {
1116
+ inPlan = true;
1117
+ continue;
1118
+ }
1119
+ if (inPlan) {
1120
+ const m = t.match(/^\d+[.)]\s+(.+)/);
1121
+ if (m) {
1122
+ steps.push(m[1].trim());
1123
+ continue;
1124
+ }
1125
+ if (/^#{1,3}\s/.test(t) || (t === '' && steps.length > 0))
1126
+ break;
1127
+ }
1128
+ }
1129
+ return steps;
1130
+ }
1087
1131
  export async function startREPL(agent, config, projectContext, oneShot) {
1088
1132
  void HISTORY_FILE;
1089
1133
  // Context window size — default 131072 (128k), updated silently from model info.
@@ -1378,8 +1422,11 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1378
1422
  closeMenu();
1379
1423
  const next = agent.getMode() === 'agent' ? 'plan' : 'agent';
1380
1424
  agent.setMode(next);
1425
+ // When supportsVT is true showPrompt prints a tip line between the
1426
+ // header and the prompt, so we need to skip 2 lines to reach the header.
1427
+ const linesUp = supportsVT ? 2 : 1;
1381
1428
  process.stdout.write('\x1b7' + // save cursor (on the input line)
1382
- '\x1b[1A\r\x1b[2K' + // up to the header line, col 0, clear it
1429
+ `\x1b[${linesUp}A\r\x1b[2K` + // up past tip to header, clear it
1383
1430
  promptHeaderText(next) +
1384
1431
  '\x1b8');
1385
1432
  return;
@@ -1474,6 +1521,10 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1474
1521
  }
1475
1522
  else {
1476
1523
  printPromptHeader(agent.getMode());
1524
+ if (supportsVT) {
1525
+ const tip = TIPS[tipIndex++ % TIPS.length];
1526
+ process.stdout.write(` ${c.secondary('Tip:')} ${c.dim(tip)}\n`);
1527
+ }
1477
1528
  const tokens = agent.estimateConversationTokens();
1478
1529
  const pct = tokens / contextWindow;
1479
1530
  const ring = tokens > 0 ? contextRing(Math.min(pct, 1)) : '';
@@ -1487,11 +1538,6 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1487
1538
  currentPrompt = prompt;
1488
1539
  rl.setPrompt(prompt);
1489
1540
  rl.prompt();
1490
- // Rotating tip below the prompt (only on VT-capable terminals)
1491
- if (supportsVT) {
1492
- const tip = TIPS[tipIndex++ % TIPS.length];
1493
- process.stdout.write(`\x1b[s\n${c.dim(' ' + tip)}\x1b[u`);
1494
- }
1495
1541
  }
1496
1542
  };
1497
1543
  rl.on('close', () => {
@@ -1552,6 +1598,12 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1552
1598
  showPrompt();
1553
1599
  return;
1554
1600
  }
1601
+ if (!isLoggedIn(config)) {
1602
+ console.log(errorLine('Not signed in. Run /login to sign in and continue.'));
1603
+ process.stdout.write('\n');
1604
+ showPrompt();
1605
+ return;
1606
+ }
1555
1607
  const agentInput = expandPastedBlocks(fullInput);
1556
1608
  const imagesForTurn = [...imageState.pending];
1557
1609
  const userContent = buildUserContent(agentInput, imagesForTurn);
@@ -1577,8 +1629,9 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1577
1629
  const b = data[0];
1578
1630
  if (b === 0x1b && data.length === 1) {
1579
1631
  if (!abortController.signal.aborted) {
1632
+ process.stdout.write('\r\x1b[2K');
1580
1633
  abortController.abort();
1581
- process.stdout.write(`\n${c.warning(' Cancelled')}\n`);
1634
+ process.stdout.write(` ${c.warning('Cancelled')}\n`);
1582
1635
  }
1583
1636
  }
1584
1637
  else if (b === 0x03) {
@@ -1586,8 +1639,9 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1586
1639
  if (ctrlCCount >= 2)
1587
1640
  process.exit(0);
1588
1641
  if (!abortController.signal.aborted) {
1642
+ process.stdout.write('\r\x1b[2K');
1589
1643
  abortController.abort();
1590
- process.stdout.write(`\n${c.warning(' Cancelled')}\n`);
1644
+ process.stdout.write(` ${c.warning('Cancelled')}\n`);
1591
1645
  }
1592
1646
  setTimeout(() => { ctrlCCount = 0; }, 2000);
1593
1647
  }
@@ -1624,6 +1678,10 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1624
1678
  if (go && !abortController.signal.aborted) {
1625
1679
  agent.setMode('agent');
1626
1680
  process.stdout.write(` ${c.muted('→ switching to')} ${modeTag('agent')} ${c.muted('mode, executing…')}\n`);
1681
+ const planSteps = parsePlanSteps(agent.getConversation());
1682
+ if (planSteps.length > 0) {
1683
+ agent.setPlanSteps(planSteps);
1684
+ }
1627
1685
  await agent.send('Execute the plan you just proposed. Make the changes now.', {
1628
1686
  autoApprove: config.autoApprove,
1629
1687
  signal: abortController.signal,
@@ -0,0 +1,29 @@
1
+ type AgentSession = {
2
+ id: number;
3
+ name: string;
4
+ buffer: string[];
5
+ };
6
+ export declare class SessionManager {
7
+ static activeId: number;
8
+ static nextAgentId: number;
9
+ static sessions: Map<number, AgentSession>;
10
+ static onRedraw?: () => void;
11
+ static init(mainAgentName?: string, onRedrawCallback?: () => void): void;
12
+ static register(name: string): number;
13
+ static remove(id: number): void;
14
+ static getActiveId(): number;
15
+ static setActiveId(id: number, afterRedraw?: () => void): void;
16
+ static switchToNext(afterRedraw?: () => void): void;
17
+ static switchToPrev(afterRedraw?: () => void): void;
18
+ static isActive(agentId: number): boolean;
19
+ /**
20
+ * Append already-rendered lines to an agent's scrollback WITHOUT echoing to
21
+ * stdout. Used by the live task board, which paints its own animation frames
22
+ * directly and only needs the *settled* result recorded for history/redraw.
23
+ */
24
+ static recordHistory(agentId: number, text: string): void;
25
+ static write(agentId: number, text: string): void;
26
+ private static appendToBuffer;
27
+ static redraw(afterRedraw?: () => void): void;
28
+ }
29
+ export {};
@@ -0,0 +1,145 @@
1
+ import chalk from 'chalk';
2
+ export class SessionManager {
3
+ static activeId = 0;
4
+ static nextAgentId = 0;
5
+ static sessions = new Map();
6
+ static onRedraw;
7
+ static init(mainAgentName = 'Main', onRedrawCallback) {
8
+ const id = this.nextAgentId++;
9
+ this.sessions.set(id, { id, name: mainAgentName, buffer: [] });
10
+ this.activeId = id;
11
+ this.onRedraw = onRedrawCallback;
12
+ }
13
+ static register(name) {
14
+ const id = this.nextAgentId++;
15
+ this.sessions.set(id, { id, name, buffer: [] });
16
+ return id;
17
+ }
18
+ static remove(id) {
19
+ if (id !== 0) {
20
+ this.sessions.delete(id);
21
+ if (this.activeId === id) {
22
+ this.setActiveId(0);
23
+ }
24
+ else {
25
+ this.redraw();
26
+ }
27
+ }
28
+ }
29
+ static getActiveId() {
30
+ return this.activeId;
31
+ }
32
+ static setActiveId(id, afterRedraw) {
33
+ if (this.sessions.has(id)) {
34
+ this.activeId = id;
35
+ this.redraw(afterRedraw);
36
+ }
37
+ }
38
+ static switchToNext(afterRedraw) {
39
+ const ids = Array.from(this.sessions.keys());
40
+ const idx = ids.indexOf(this.activeId);
41
+ if (idx >= 0) {
42
+ this.setActiveId(ids[(idx + 1) % ids.length], afterRedraw);
43
+ }
44
+ }
45
+ static switchToPrev(afterRedraw) {
46
+ const ids = Array.from(this.sessions.keys());
47
+ const idx = ids.indexOf(this.activeId);
48
+ if (idx >= 0) {
49
+ this.setActiveId(ids[(idx - 1 + ids.length) % ids.length], afterRedraw);
50
+ }
51
+ }
52
+ static isActive(agentId) {
53
+ return this.activeId === agentId;
54
+ }
55
+ /**
56
+ * Append already-rendered lines to an agent's scrollback WITHOUT echoing to
57
+ * stdout. Used by the live task board, which paints its own animation frames
58
+ * directly and only needs the *settled* result recorded for history/redraw.
59
+ */
60
+ static recordHistory(agentId, text) {
61
+ let session = this.sessions.get(agentId);
62
+ if (!session) {
63
+ session = { id: agentId, name: `Agent ${agentId}`, buffer: [] };
64
+ this.sessions.set(agentId, session);
65
+ }
66
+ this.appendToBuffer(session, text);
67
+ }
68
+ static write(agentId, text) {
69
+ let session = this.sessions.get(agentId);
70
+ if (!session) {
71
+ session = { id: agentId, name: `Agent ${agentId}`, buffer: [] };
72
+ this.sessions.set(agentId, session);
73
+ }
74
+ this.appendToBuffer(session, text);
75
+ if (agentId === this.activeId) {
76
+ process.stdout.write(text);
77
+ }
78
+ }
79
+ static appendToBuffer(session, text) {
80
+ let currentLine = session.buffer.length > 0 ? session.buffer[session.buffer.length - 1] : '';
81
+ for (let i = 0; i < text.length; i++) {
82
+ const char = text[i];
83
+ if (char === '\n') {
84
+ session.buffer[session.buffer.length - 1] = currentLine;
85
+ session.buffer.push('');
86
+ currentLine = '';
87
+ }
88
+ else if (char === '\r') {
89
+ // simple CR behavior: overwrite current line
90
+ // Wait, typically \r\n are together. If we see \r and next is \n, we skip \r.
91
+ if (text[i + 1] === '\n') {
92
+ // ignore \r before \n
93
+ }
94
+ else {
95
+ currentLine = ''; // reset line
96
+ }
97
+ }
98
+ else {
99
+ currentLine += char;
100
+ }
101
+ }
102
+ // Clean up ANSI escape code for "clear line" at start of current line if any
103
+ currentLine = currentLine.replace(/^\x1b\[2K/, '');
104
+ if (session.buffer.length === 0) {
105
+ session.buffer.push(currentLine);
106
+ }
107
+ else {
108
+ session.buffer[session.buffer.length - 1] = currentLine;
109
+ }
110
+ if (session.buffer.length > 1000) {
111
+ session.buffer.splice(0, session.buffer.length - 1000);
112
+ }
113
+ }
114
+ static redraw(afterRedraw) {
115
+ const session = this.sessions.get(this.activeId);
116
+ if (!session)
117
+ return;
118
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
119
+ let out = '';
120
+ out += chalk.bgGray.white('--- AGENTS ---') + '\n';
121
+ const ids = Array.from(this.sessions.keys());
122
+ for (const id of ids) {
123
+ const s = this.sessions.get(id);
124
+ if (id === this.activeId) {
125
+ out += chalk.bgGreen.black(` [${id}: ${s.name}] `) + ' ';
126
+ }
127
+ else {
128
+ out += chalk.bgBlack.gray(` [${id}: ${s.name}] `) + ' ';
129
+ }
130
+ }
131
+ out += '\n' + chalk.dim('(Alt+Right / Alt+Left to switch. Scroll up to read history)\n\n');
132
+ for (const line of session.buffer) {
133
+ out += line + '\n';
134
+ }
135
+ // Remove trailing newline so the cursor stays on the last line for spinners
136
+ if (out.endsWith('\n')) {
137
+ out = out.slice(0, -1);
138
+ }
139
+ process.stdout.write(out);
140
+ if (afterRedraw)
141
+ afterRedraw();
142
+ if (this.onRedraw)
143
+ this.onRedraw();
144
+ }
145
+ }
package/dist/theme.d.ts CHANGED
@@ -34,6 +34,7 @@ export declare const c: {
34
34
  readonly bold: import("chalk").ChalkInstance;
35
35
  readonly dim: import("chalk").ChalkInstance;
36
36
  readonly italic: import("chalk").ChalkInstance;
37
+ readonly strikethrough: import("chalk").ChalkInstance;
37
38
  };
38
39
  export declare function stripAnsi(str: string): string;
39
40
  export declare const BANNER_ROWS = 9;
@@ -70,7 +71,9 @@ export declare class InlineSpinner {
70
71
  private frameIdx;
71
72
  private frames;
72
73
  private visible;
73
- constructor(label?: string, elapsedSince?: number);
74
+ private signal?;
75
+ private onAbort;
76
+ constructor(label?: string, elapsedSince?: number, signal?: AbortSignal);
74
77
  start(): void;
75
78
  private draw;
76
79
  stop(successMessage?: string): void;
package/dist/theme.js CHANGED
@@ -186,6 +186,7 @@ export const c = {
186
186
  get bold() { return chalk.bold; },
187
187
  get dim() { return chalk.dim; },
188
188
  get italic() { return chalk.italic; },
189
+ get strikethrough() { return chalk.strikethrough; },
189
190
  };
190
191
  function truncate(str, max) {
191
192
  if (str.length <= max)
@@ -313,9 +314,22 @@ export class InlineSpinner {
313
314
  frameIdx = 0;
314
315
  frames = ['.', 'o', 'O', 'o'];
315
316
  visible = false;
316
- constructor(label = 'Thinking', elapsedSince) {
317
+ signal;
318
+ onAbort = () => {
319
+ this.stop();
320
+ };
321
+ constructor(label = 'Thinking', elapsedSince, signal) {
317
322
  this.label = label;
318
323
  this.elapsedSince = elapsedSince;
324
+ if (signal) {
325
+ this.signal = signal;
326
+ if (signal.aborted) {
327
+ this.stop();
328
+ }
329
+ else {
330
+ signal.addEventListener('abort', this.onAbort);
331
+ }
332
+ }
319
333
  }
320
334
  start() {
321
335
  if (this.timer || this.delayTimer)
@@ -338,9 +352,14 @@ export class InlineSpinner {
338
352
  const elapsedFrom = this.elapsedSince ?? this.startTime;
339
353
  const elapsed = ((Date.now() - elapsedFrom) / 1000).toFixed(1);
340
354
  const frame = c.primary(this.frames[this.frameIdx]);
341
- process.stdout.write(`\r\x1b[2K ${frame} ${c.muted(this.label)} ${c.accent(`${elapsed}s`)} ${c.muted('· Esc to interrupt')}`);
355
+ const text = `\r\x1b[2K ${frame} ${c.muted(this.label)} ${c.accent(`${elapsed}s`)} ${c.muted('· Esc to interrupt')}`;
356
+ process.stdout.write(text);
342
357
  }
343
358
  stop(successMessage) {
359
+ if (this.signal) {
360
+ this.signal.removeEventListener('abort', this.onAbort);
361
+ this.signal = undefined;
362
+ }
344
363
  if (this.delayTimer) {
345
364
  clearTimeout(this.delayTimer);
346
365
  this.delayTimer = null;
@@ -351,12 +370,26 @@ export class InlineSpinner {
351
370
  }
352
371
  const wasVisible = this.visible;
353
372
  this.visible = false;
373
+ let out = '';
354
374
  if (successMessage) {
355
- process.stdout.write(`\r\x1b[2K ${c.success('ok')} ${c.muted(successMessage)}\n`);
375
+ const elapsedFrom = this.elapsedSince ?? this.startTime;
376
+ const elapsed = ((Date.now() - elapsedFrom) / 1000).toFixed(1);
377
+ // If success message is just "ok" (default behavior) or something else
378
+ if (successMessage === 'ok') {
379
+ out = `\r\x1b[2K ${c.success('✓')} ${c.muted(this.label)} ${c.accent(`${elapsed}s`)}\n`;
380
+ }
381
+ else if (successMessage === 'failed') {
382
+ out = `\r\x1b[2K ${c.error('✗')} ${c.muted(this.label)} ${c.accent(`${elapsed}s`)}\n`;
383
+ }
384
+ else {
385
+ out = `\r\x1b[2K ${c.success('✓')} ${c.muted(successMessage)} ${c.accent(`${elapsed}s`)}\n`;
386
+ }
356
387
  }
357
388
  else if (wasVisible) {
358
- process.stdout.write('\r\x1b[2K');
389
+ out = '\r\x1b[2K';
359
390
  }
391
+ if (out)
392
+ process.stdout.write(out);
360
393
  }
361
394
  updateLabel(label) {
362
395
  this.label = label;
@@ -375,7 +408,6 @@ export function toolMeta(rawName) {
375
408
  case 'grep': return { verb: 'Grep', tint: c.info };
376
409
  case 'search_files': return { verb: 'Search', tint: c.info };
377
410
  case 'memory_write': return { verb: 'Remember', tint: c.secondary };
378
- case 'spawn_agent': return { verb: 'Agent', tint: c.secondary };
379
411
  case 'switch_mode': return { verb: 'SwitchMode', tint: c.warning };
380
412
  case 'ask_user': return { verb: 'Ask', tint: c.info };
381
413
  case 'fetch_url': return { verb: 'Fetch', tint: c.info };
package/dist/tools.d.ts CHANGED
@@ -5,4 +5,4 @@ export declare const SAFE_TOOLS: Set<string>;
5
5
  export declare const PLAN_TOOLS: Set<string>;
6
6
  export declare function isRestrictedPath(path: string): boolean;
7
7
  export declare function formatToolArgs(name: string, input: Record<string, unknown>): string;
8
- export declare function executeTool(name: string, input: Record<string, unknown>): Promise<string>;
8
+ export declare function executeTool(name: string, input: Record<string, unknown>, signal?: AbortSignal): Promise<string>;