ikie-cli 0.1.10 → 0.1.11
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 +38 -2
- package/dist/theme.d.ts +3 -1
- package/dist/theme.js +16 -2
- 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.
|
|
@@ -961,7 +980,10 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
961
980
|
rl.setPrompt(CONTINUE_PROMPT);
|
|
962
981
|
}
|
|
963
982
|
else {
|
|
964
|
-
|
|
983
|
+
const tokens = agent.estimateConversationTokens();
|
|
984
|
+
const pct = tokens / contextWindow;
|
|
985
|
+
const ring = tokens > 0 ? contextRing(Math.min(pct, 1)) : '';
|
|
986
|
+
printPromptHeader(agent.getMode(), ring);
|
|
965
987
|
if (imageState.pending.length) {
|
|
966
988
|
const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
|
|
967
989
|
process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
|
|
@@ -1076,6 +1098,20 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1076
1098
|
if (sessionState.activeName && !abortController.signal.aborted) {
|
|
1077
1099
|
saveSession(sessionState.activeName, config.model, agent.getConversation());
|
|
1078
1100
|
}
|
|
1101
|
+
// Auto-compact when context exceeds 85% of window.
|
|
1102
|
+
if (!abortController.signal.aborted) {
|
|
1103
|
+
const pct = agent.estimateConversationTokens() / contextWindow;
|
|
1104
|
+
if (pct >= 0.85) {
|
|
1105
|
+
process.stdout.write(`\n ${c.warning('◕')} ${c.muted('Context at')} ${c.warning(`${Math.round(pct * 100)}%`)}${c.muted(' — auto-compacting…')}\n`);
|
|
1106
|
+
try {
|
|
1107
|
+
const { before, after } = await agent.compact();
|
|
1108
|
+
process.stdout.write(` ${c.success('○')} ${c.muted(`Freed ~${(before - after).toLocaleString()} tokens`)}\n`);
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
process.stdout.write(` ${c.muted('Could not auto-compact — use /compact manually')}\n`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1079
1115
|
// Plan mode: a plan was just proposed — offer to execute it.
|
|
1080
1116
|
if (agent.getMode() === 'plan' && !abortController.signal.aborted) {
|
|
1081
1117
|
process.stdin.removeListener('data', cancelHandler);
|
package/dist/theme.d.ts
CHANGED
|
@@ -39,7 +39,9 @@ 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
|
-
|
|
42
|
+
/** Circular fill indicator for context usage. */
|
|
43
|
+
export declare function contextRing(pct: number): string;
|
|
44
|
+
export declare function printPromptHeader(mode?: 'agent' | 'plan', ring?: string): void;
|
|
43
45
|
export declare const PROMPT: string;
|
|
44
46
|
export declare const CONTINUE_PROMPT: string;
|
|
45
47
|
export declare class InlineSpinner {
|
package/dist/theme.js
CHANGED
|
@@ -262,12 +262,26 @@ 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
|
-
|
|
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
|
+
}
|
|
271
|
+
export function printPromptHeader(mode = 'agent', ring = '') {
|
|
266
272
|
const cwdName = basename(process.cwd()) || '/';
|
|
267
273
|
const branch = getGitBranchFast();
|
|
268
274
|
const gitSegment = branch ? ` ${c.muted('on')} ${c.secondary(branch)}` : '';
|
|
269
275
|
const themeSegment = ` ${c.muted('theme')} ${c.secondary(activeTheme.name)}`;
|
|
270
|
-
|
|
276
|
+
const left = `${c.primary('╭─')} ${c.primary.bold('ikie')} ${c.muted('·')} ${modeTag(mode)}${gitSegment}${themeSegment} ${c.muted('in')} ${c.accent(cwdName)}`;
|
|
277
|
+
if (ring) {
|
|
278
|
+
const cols = process.stdout.columns ?? 80;
|
|
279
|
+
const pad = Math.max(1, cols - stripAnsi(left).length - stripAnsi(ring).length - 1);
|
|
280
|
+
process.stdout.write(`\n${left}${' '.repeat(pad)}${ring}\n`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
process.stdout.write(`\n${left}\n`);
|
|
284
|
+
}
|
|
271
285
|
}
|
|
272
286
|
export const PROMPT = c.primary('╰─❯ ');
|
|
273
287
|
export const CONTINUE_PROMPT = c.primary('│ ');
|