ikie-cli 0.1.10 → 0.1.12

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.d.ts CHANGED
@@ -45,6 +45,11 @@ export declare class Agent {
45
45
  getConversation(): ChatCompletionMessageParam[];
46
46
  setConversation(messages: ChatCompletionMessageParam[]): void;
47
47
  getLastTurnStats(): AgentTurnStats;
48
+ estimateConversationTokens(): number;
49
+ compact(): Promise<{
50
+ before: number;
51
+ after: number;
52
+ }>;
48
53
  getMode(): AgentMode;
49
54
  setMode(mode: AgentMode): void;
50
55
  send(userMessage: string | UserContentPart[], opts?: AgentOptions): Promise<void>;
package/dist/agent.js CHANGED
@@ -89,6 +89,58 @@ export class Agent {
89
89
  getLastTurnStats() {
90
90
  return { ...this.lastTurnStats };
91
91
  }
92
+ estimateConversationTokens() {
93
+ let chars = 0;
94
+ for (const msg of this.conversation) {
95
+ if (typeof msg.content === 'string') {
96
+ chars += msg.content.length;
97
+ }
98
+ else if (Array.isArray(msg.content)) {
99
+ for (const part of msg.content) {
100
+ if (part.text)
101
+ chars += part.text.length;
102
+ }
103
+ }
104
+ }
105
+ return estimateTokens(chars);
106
+ }
107
+ async compact() {
108
+ if (!this.conversation.length)
109
+ return { before: 0, after: 0 };
110
+ const before = this.estimateConversationTokens();
111
+ const res = await this.client.chat.completions.create({
112
+ model: this.config.model,
113
+ max_tokens: 4096,
114
+ messages: [
115
+ {
116
+ role: 'system',
117
+ content: 'You are a conversation summarizer. Produce a dense, complete summary.',
118
+ },
119
+ ...this.conversation,
120
+ {
121
+ role: 'user',
122
+ content: 'Summarize this entire conversation into a concise context document. '
123
+ + 'Include: what was requested, what was done (files created/modified with exact paths), '
124
+ + 'decisions made, current state of work, and anything still pending. '
125
+ + 'Preserve all technical details, file paths, code decisions, and key outputs. '
126
+ + 'This summary will replace the full conversation history.',
127
+ },
128
+ ],
129
+ });
130
+ const summary = res.choices[0]?.message?.content ?? '(no summary)';
131
+ this.conversation = [
132
+ {
133
+ role: 'user',
134
+ content: `[Conversation compacted — previous context summary below]\n\n${summary}`,
135
+ },
136
+ {
137
+ role: 'assistant',
138
+ content: 'Got it. I have the full context from our previous conversation and will continue seamlessly.',
139
+ },
140
+ ];
141
+ const after = this.estimateConversationTokens();
142
+ return { before, after };
143
+ }
92
144
  getMode() {
93
145
  return this.mode;
94
146
  }
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, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, } from './theme.js';
4
+ import { c, PROMPT, CONTINUE_PROMPT, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, } 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';
@@ -97,6 +97,7 @@ const SLASH_CMDS = [
97
97
  { name: 'mode', desc: 'Show/set mode (plan|agent)', args: '[plan|agent]' },
98
98
  { name: 'plan', desc: 'Switch to plan mode (read-only)' },
99
99
  { name: 'agent', desc: 'Switch to agent mode (full)' },
100
+ { name: 'compact', desc: 'Summarize conversation to free up context window' },
100
101
  { name: 'login', desc: 'Sign in to ikie account' },
101
102
  { name: 'logout', desc: 'Sign out of ikie account' },
102
103
  { name: 'exit', desc: 'Exit Ikie' },
@@ -345,6 +346,17 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
345
346
  }
346
347
  return true;
347
348
  }
349
+ case 'compact': {
350
+ if (!agent.getConversation().length) {
351
+ console.log(infoLine('Nothing to compact.'));
352
+ return true;
353
+ }
354
+ console.log(c.muted(' Compacting conversation…'));
355
+ const { before, after } = await agent.compact();
356
+ const freed = before - after;
357
+ console.log(successLine(`Compacted — freed ~${freed.toLocaleString()} tokens (${before.toLocaleString()} → ${after.toLocaleString()})`));
358
+ return true;
359
+ }
348
360
  case 'login': {
349
361
  await login();
350
362
  return true;
@@ -807,6 +819,13 @@ function notify(message) {
807
819
  }
808
820
  export async function startREPL(agent, config, projectContext, oneShot) {
809
821
  void HISTORY_FILE;
822
+ // Context window size — default 131072 (128k), updated silently from model info.
823
+ let contextWindow = 131072;
824
+ fetchModelsFromServer(config).then(models => {
825
+ const current = models.find(m => m.name === config.model) ?? models.find(m => m.is_default);
826
+ if (current?.context_window)
827
+ contextWindow = current.context_window;
828
+ }).catch(() => { });
810
829
  // A `/`-prefixed launch arg (e.g. `ikie /session load foo`) is a slash
811
830
  // COMMAND, not a prompt — run it after setup, then stay interactive.
812
831
  // A plain launch arg is a one-shot prompt: run it once and exit.
@@ -962,11 +981,17 @@ export async function startREPL(agent, config, projectContext, oneShot) {
962
981
  }
963
982
  else {
964
983
  printPromptHeader(agent.getMode());
984
+ const tokens = agent.estimateConversationTokens();
985
+ const pct = tokens / contextWindow;
986
+ const ring = tokens > 0 ? contextRing(Math.min(pct, 1)) : '';
987
+ const prompt = ring
988
+ ? `${c.primary('╰─')} ${ring} ${c.primary('❯ ')}`
989
+ : PROMPT;
965
990
  if (imageState.pending.length) {
966
991
  const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
967
992
  process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
968
993
  }
969
- rl.setPrompt(PROMPT);
994
+ rl.setPrompt(prompt);
970
995
  }
971
996
  rl.prompt();
972
997
  };
@@ -1076,6 +1101,20 @@ export async function startREPL(agent, config, projectContext, oneShot) {
1076
1101
  if (sessionState.activeName && !abortController.signal.aborted) {
1077
1102
  saveSession(sessionState.activeName, config.model, agent.getConversation());
1078
1103
  }
1104
+ // Auto-compact when context exceeds 85% of window.
1105
+ if (!abortController.signal.aborted) {
1106
+ const pct = agent.estimateConversationTokens() / contextWindow;
1107
+ if (pct >= 0.85) {
1108
+ process.stdout.write(`\n ${c.warning('◕')} ${c.muted('Context at')} ${c.warning(`${Math.round(pct * 100)}%`)}${c.muted(' — auto-compacting…')}\n`);
1109
+ try {
1110
+ const { before, after } = await agent.compact();
1111
+ process.stdout.write(` ${c.success('○')} ${c.muted(`Freed ~${(before - after).toLocaleString()} tokens`)}\n`);
1112
+ }
1113
+ catch {
1114
+ process.stdout.write(` ${c.muted('Could not auto-compact — use /compact manually')}\n`);
1115
+ }
1116
+ }
1117
+ }
1079
1118
  // Plan mode: a plan was just proposed — offer to execute it.
1080
1119
  if (agent.getMode() === 'plan' && !abortController.signal.aborted) {
1081
1120
  process.stdin.removeListener('data', cancelHandler);
package/dist/theme.d.ts CHANGED
@@ -39,6 +39,8 @@ export declare function stripAnsi(str: string): string;
39
39
  export declare const BANNER_ROWS = 9;
40
40
  export declare function drawBanner(model: string): void;
41
41
  export declare function modeTag(mode: 'agent' | 'plan'): string;
42
+ /** Circular fill indicator for context usage. */
43
+ export declare function contextRing(pct: number): string;
42
44
  export declare function printPromptHeader(mode?: 'agent' | 'plan'): void;
43
45
  export declare const PROMPT: string;
44
46
  export declare const CONTINUE_PROMPT: string;
package/dist/theme.js CHANGED
@@ -262,6 +262,12 @@ export function drawBanner(model) {
262
262
  export function modeTag(mode) {
263
263
  return mode === 'plan' ? c.warning.bold('plan') : c.success('agent');
264
264
  }
265
+ /** Circular fill indicator for context usage. */
266
+ export function contextRing(pct) {
267
+ const char = pct < 0.2 ? '○' : pct < 0.4 ? '◔' : pct < 0.6 ? '◑' : pct < 0.8 ? '◕' : '●';
268
+ const color = pct < 0.7 ? c.success : pct < 0.85 ? c.warning : c.error;
269
+ return `${color(char)} ${color(`${Math.round(pct * 100)}%`)}`;
270
+ }
265
271
  export function printPromptHeader(mode = 'agent') {
266
272
  const cwdName = basename(process.cwd()) || '/';
267
273
  const branch = getGitBranchFast();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {