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 +5 -0
- package/dist/agent.js +52 -0
- package/dist/repl.js +41 -2
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +6 -0
- package/package.json +1 -1
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(
|
|
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();
|