ikie-cli 0.1.0

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.
@@ -0,0 +1,21 @@
1
+ import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions.js';
2
+ export interface SavedSession {
3
+ name: string;
4
+ model: string;
5
+ cwd: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ messages: ChatCompletionMessageParam[];
9
+ }
10
+ export interface SessionSummary {
11
+ name: string;
12
+ updatedAt: string;
13
+ messageCount: number;
14
+ model?: string;
15
+ }
16
+ export declare function normalizeSessionName(name?: string): string;
17
+ export declare function sessionPath(name: string): string;
18
+ export declare function saveSession(name: string, model: string, messages: ChatCompletionMessageParam[]): SavedSession;
19
+ export declare function loadSession(name: string): SavedSession;
20
+ export declare function deleteSession(name: string): void;
21
+ export declare function listSessions(): SessionSummary[];
@@ -0,0 +1,85 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { SESSIONS_DIR, ensureHome } from './config.js';
4
+ function safeName(name) {
5
+ return name
6
+ .trim()
7
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
8
+ .replace(/^-+|-+$/g, '')
9
+ .slice(0, 80);
10
+ }
11
+ export function normalizeSessionName(name) {
12
+ const cleaned = safeName(name || '');
13
+ if (cleaned)
14
+ return cleaned;
15
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
16
+ return `session-${stamp}`;
17
+ }
18
+ export function sessionPath(name) {
19
+ ensureHome();
20
+ return join(SESSIONS_DIR, `${normalizeSessionName(name)}.json`);
21
+ }
22
+ export function saveSession(name, model, messages) {
23
+ ensureHome();
24
+ const normalized = normalizeSessionName(name);
25
+ const path = sessionPath(normalized);
26
+ const now = new Date().toISOString();
27
+ let createdAt = now;
28
+ if (existsSync(path)) {
29
+ try {
30
+ const existing = JSON.parse(readFileSync(path, 'utf-8'));
31
+ if (existing.createdAt)
32
+ createdAt = existing.createdAt;
33
+ }
34
+ catch { }
35
+ }
36
+ const data = {
37
+ name: normalized,
38
+ model,
39
+ cwd: process.cwd(),
40
+ createdAt,
41
+ updatedAt: now,
42
+ messages,
43
+ };
44
+ writeFileSync(path, JSON.stringify(data, null, 2));
45
+ return data;
46
+ }
47
+ export function loadSession(name) {
48
+ const path = sessionPath(name);
49
+ if (!existsSync(path))
50
+ throw new Error(`Session not found: ${normalizeSessionName(name)}`);
51
+ return JSON.parse(readFileSync(path, 'utf-8'));
52
+ }
53
+ export function deleteSession(name) {
54
+ const path = sessionPath(name);
55
+ if (!existsSync(path))
56
+ throw new Error(`Session not found: ${normalizeSessionName(name)}`);
57
+ unlinkSync(path);
58
+ }
59
+ export function listSessions() {
60
+ ensureHome();
61
+ if (!existsSync(SESSIONS_DIR))
62
+ mkdirSync(SESSIONS_DIR, { recursive: true });
63
+ return readdirSync(SESSIONS_DIR)
64
+ .filter(file => file.endsWith('.json'))
65
+ .map(file => {
66
+ const path = join(SESSIONS_DIR, file);
67
+ try {
68
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
69
+ return {
70
+ name: data.name ?? file.replace(/\.json$/, ''),
71
+ updatedAt: data.updatedAt ?? statSync(path).mtime.toISOString(),
72
+ messageCount: data.messages?.length ?? 0,
73
+ model: data.model,
74
+ };
75
+ }
76
+ catch {
77
+ return {
78
+ name: file.replace(/\.json$/, ''),
79
+ updatedAt: statSync(path).mtime.toISOString(),
80
+ messageCount: 0,
81
+ };
82
+ }
83
+ })
84
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
85
+ }
@@ -0,0 +1,66 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export interface Theme {
3
+ name: string;
4
+ description: string;
5
+ colors: {
6
+ primary: string;
7
+ secondary: string;
8
+ accent: string;
9
+ success: string;
10
+ error: string;
11
+ warning: string;
12
+ info: string;
13
+ muted: string;
14
+ bgBottomBar: string;
15
+ fgBottomBar: string;
16
+ };
17
+ bannerArt?: string[];
18
+ bannerColors?: string[];
19
+ }
20
+ export declare const THEMES: Record<string, Theme>;
21
+ export declare let activeTheme: Theme;
22
+ export declare function getTheme(): Theme;
23
+ export declare function setTheme(name: string): boolean;
24
+ export declare const c: {
25
+ readonly primary: import("chalk").ChalkInstance;
26
+ readonly secondary: import("chalk").ChalkInstance;
27
+ readonly accent: import("chalk").ChalkInstance;
28
+ readonly success: import("chalk").ChalkInstance;
29
+ readonly error: import("chalk").ChalkInstance;
30
+ readonly warning: import("chalk").ChalkInstance;
31
+ readonly info: import("chalk").ChalkInstance;
32
+ readonly muted: import("chalk").ChalkInstance;
33
+ readonly white: import("chalk").ChalkInstance;
34
+ readonly bold: import("chalk").ChalkInstance;
35
+ readonly dim: import("chalk").ChalkInstance;
36
+ readonly italic: import("chalk").ChalkInstance;
37
+ };
38
+ export declare function stripAnsi(str: string): string;
39
+ export declare const BANNER_ROWS = 9;
40
+ export declare function drawBanner(model: string): void;
41
+ export declare function printPromptHeader(): void;
42
+ export declare const PROMPT: string;
43
+ export declare const CONTINUE_PROMPT: string;
44
+ export declare class InlineSpinner {
45
+ private timer;
46
+ private delayTimer;
47
+ private startTime;
48
+ private elapsedSince?;
49
+ private label;
50
+ private frameIdx;
51
+ private frames;
52
+ private visible;
53
+ constructor(label?: string, elapsedSince?: number);
54
+ start(): void;
55
+ private draw;
56
+ stop(successMessage?: string): void;
57
+ updateLabel(label: string): void;
58
+ }
59
+ export declare function toolLine(name: string, args: string): string;
60
+ export declare function toolSuccessLine(ms: number, preview?: string): string;
61
+ export declare function toolErrorLine(msg: string): string;
62
+ export declare function successLine(msg: string): string;
63
+ export declare function errorLine(msg: string): string;
64
+ export declare function warnLine(msg: string): string;
65
+ export declare function infoLine(msg: string): string;
66
+ export declare function permissionPrompt(toolName: string, preview: string): string;
package/dist/theme.js ADDED
@@ -0,0 +1,365 @@
1
+ import chalk from 'chalk';
2
+ import os from 'os';
3
+ import { join as pathJoin, basename } from 'path';
4
+ import { existsSync, readFileSync } from 'fs';
5
+ import { loadConfig, saveConfig } from './config.js';
6
+ export const VERSION = '0.1.0';
7
+ const IKIE_BANNER = [
8
+ ' ██╗██╗ ██╗██╗███████╗',
9
+ ' ██║██║ ██╔╝██║██╔════╝',
10
+ ' ██║█████╔╝ ██║█████╗ ',
11
+ ' ██║██╔═██╗ ██║██╔══╝ ',
12
+ ' ██║██║ ██╗██║███████╗',
13
+ ' ╚═╝╚═╝ ╚═╝╚═╝╚══════╝',
14
+ ];
15
+ export const THEMES = {
16
+ nebula: {
17
+ name: 'nebula',
18
+ description: 'Deep violet with cool blue accents',
19
+ colors: {
20
+ primary: '#8B5CF6',
21
+ secondary: '#60A5FA',
22
+ accent: '#C4B5FD',
23
+ success: '#22C55E',
24
+ error: '#F87171',
25
+ warning: '#FBBF24',
26
+ info: '#38BDF8',
27
+ muted: '#6B7280',
28
+ bgBottomBar: '#171526',
29
+ fgBottomBar: '#C4B5FD',
30
+ },
31
+ bannerArt: IKIE_BANNER,
32
+ bannerColors: ['#7C3AED', '#8B5CF6', '#A78BFA', '#C4B5FD'],
33
+ },
34
+ cyberpunk: {
35
+ name: 'cyberpunk',
36
+ description: 'Neon magenta, cyan, and yellow',
37
+ colors: {
38
+ primary: '#FF007F',
39
+ secondary: '#00D7FF',
40
+ accent: '#FFE600',
41
+ success: '#39FF14',
42
+ error: '#FF3131',
43
+ warning: '#FF8A00',
44
+ info: '#8B5CF6',
45
+ muted: '#5A5A72',
46
+ bgBottomBar: '#140A24',
47
+ fgBottomBar: '#00D7FF',
48
+ },
49
+ bannerArt: IKIE_BANNER,
50
+ bannerColors: ['#FF007F', '#00D7FF', '#FFE600', '#FF007F'],
51
+ },
52
+ dracula: {
53
+ name: 'dracula',
54
+ description: 'Soft purple, pink, and cyan',
55
+ colors: {
56
+ primary: '#BD93F9',
57
+ secondary: '#FF79C6',
58
+ accent: '#8BE9FD',
59
+ success: '#50FA7B',
60
+ error: '#FF5555',
61
+ warning: '#FFB86C',
62
+ info: '#6272A4',
63
+ muted: '#6272A4',
64
+ bgBottomBar: '#282A36',
65
+ fgBottomBar: '#BD93F9',
66
+ },
67
+ bannerArt: IKIE_BANNER,
68
+ bannerColors: ['#BD93F9', '#FF79C6', '#8BE9FD', '#50FA7B'],
69
+ },
70
+ forest: {
71
+ name: 'forest',
72
+ description: 'Emerald, teal, and warm gold',
73
+ colors: {
74
+ primary: '#10B981',
75
+ secondary: '#2DD4BF',
76
+ accent: '#F59E0B',
77
+ success: '#34D399',
78
+ error: '#EF4444',
79
+ warning: '#F59E0B',
80
+ info: '#06B6D4',
81
+ muted: '#4B5563',
82
+ bgBottomBar: '#063A31',
83
+ fgBottomBar: '#34D399',
84
+ },
85
+ bannerArt: IKIE_BANNER,
86
+ bannerColors: ['#047857', '#10B981', '#2DD4BF', '#F59E0B'],
87
+ },
88
+ slate: {
89
+ name: 'slate',
90
+ description: 'Low-noise neutral terminal palette',
91
+ colors: {
92
+ primary: '#E2E8F0',
93
+ secondary: '#94A3B8',
94
+ accent: '#38BDF8',
95
+ success: '#22C55E',
96
+ error: '#EF4444',
97
+ warning: '#F59E0B',
98
+ info: '#60A5FA',
99
+ muted: '#64748B',
100
+ bgBottomBar: '#0F172A',
101
+ fgBottomBar: '#E2E8F0',
102
+ },
103
+ bannerArt: IKIE_BANNER,
104
+ bannerColors: ['#F8FAFC', '#CBD5E1', '#94A3B8', '#38BDF8'],
105
+ },
106
+ amber: {
107
+ name: 'amber',
108
+ description: 'Warm amber with restrained contrast',
109
+ colors: {
110
+ primary: '#FFB000',
111
+ secondary: '#F97316',
112
+ accent: '#FDE68A',
113
+ success: '#84CC16',
114
+ error: '#FF5F00',
115
+ warning: '#FFC83B',
116
+ info: '#38BDF8',
117
+ muted: '#8A6A28',
118
+ bgBottomBar: '#241700',
119
+ fgBottomBar: '#FFB000',
120
+ },
121
+ bannerArt: IKIE_BANNER,
122
+ bannerColors: ['#FFB000', '#FFC83B', '#F97316', '#FDE68A'],
123
+ },
124
+ aurora: {
125
+ name: 'aurora',
126
+ description: 'Nord-inspired blue, green, and rose',
127
+ colors: {
128
+ primary: '#88C0D0',
129
+ secondary: '#A3BE8C',
130
+ accent: '#B48EAD',
131
+ success: '#A3BE8C',
132
+ error: '#BF616A',
133
+ warning: '#EBCB8B',
134
+ info: '#81A1C1',
135
+ muted: '#4C566A',
136
+ bgBottomBar: '#2E3440',
137
+ fgBottomBar: '#88C0D0',
138
+ },
139
+ bannerArt: IKIE_BANNER,
140
+ bannerColors: ['#88C0D0', '#8FBCBB', '#A3BE8C', '#B48EAD'],
141
+ },
142
+ };
143
+ let activeThemeName = 'nebula';
144
+ try {
145
+ const cfg = loadConfig();
146
+ if (cfg?.theme)
147
+ activeThemeName = cfg.theme;
148
+ }
149
+ catch { }
150
+ export let activeTheme = THEMES[activeThemeName] || THEMES.nebula;
151
+ export function getTheme() {
152
+ return activeTheme;
153
+ }
154
+ export function setTheme(name) {
155
+ if (!THEMES[name])
156
+ return false;
157
+ activeThemeName = name;
158
+ activeTheme = THEMES[name];
159
+ try {
160
+ saveConfig({ theme: name });
161
+ }
162
+ catch { }
163
+ return true;
164
+ }
165
+ export const c = {
166
+ get primary() { return chalk.hex(activeTheme.colors.primary); },
167
+ get secondary() { return chalk.hex(activeTheme.colors.secondary); },
168
+ get accent() { return chalk.hex(activeTheme.colors.accent); },
169
+ get success() { return chalk.hex(activeTheme.colors.success); },
170
+ get error() { return chalk.hex(activeTheme.colors.error); },
171
+ get warning() { return chalk.hex(activeTheme.colors.warning); },
172
+ get info() { return chalk.hex(activeTheme.colors.info); },
173
+ get muted() { return chalk.hex(activeTheme.colors.muted); },
174
+ get white() { return chalk.white; },
175
+ get bold() { return chalk.bold; },
176
+ get dim() { return chalk.dim; },
177
+ get italic() { return chalk.italic; },
178
+ };
179
+ function truncate(str, max) {
180
+ if (str.length <= max)
181
+ return str;
182
+ return str.slice(0, Math.max(0, max - 3)) + '...';
183
+ }
184
+ function formatModel(model) {
185
+ const parts = model.split('/');
186
+ return parts[parts.length - 1] || model;
187
+ }
188
+ export function stripAnsi(str) {
189
+ return str.replace(/\x1B\[[0-9;]*m/g, '');
190
+ }
191
+ function visibleLength(str) {
192
+ return stripAnsi(str).length;
193
+ }
194
+ function padVisible(str, width) {
195
+ return str + ' '.repeat(Math.max(0, width - visibleLength(str)));
196
+ }
197
+ function getGitBranchFast() {
198
+ try {
199
+ let dir = process.cwd();
200
+ for (let i = 0; i < 4; i++) {
201
+ const gitDir = pathJoin(dir, '.git');
202
+ if (existsSync(gitDir)) {
203
+ const headPath = pathJoin(gitDir, 'HEAD');
204
+ if (existsSync(headPath)) {
205
+ const headContent = readFileSync(headPath, 'utf-8').trim();
206
+ if (headContent.startsWith('ref: ')) {
207
+ return headContent.slice(5).split('/').slice(2).join('/');
208
+ }
209
+ return headContent.slice(0, 7);
210
+ }
211
+ }
212
+ const parent = pathJoin(dir, '..');
213
+ if (parent === dir)
214
+ break;
215
+ dir = parent;
216
+ }
217
+ }
218
+ catch { }
219
+ return undefined;
220
+ }
221
+ function getSystemStats() {
222
+ try {
223
+ const platform = os.platform();
224
+ const osName = platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'macOS' : platform === 'linux' ? 'Linux' : platform;
225
+ const totalMemGB = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
226
+ const usedMemGB = ((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024).toFixed(1);
227
+ return `${osName} | Node ${process.version} | Mem ${usedMemGB}/${totalMemGB} GB`;
228
+ }
229
+ catch {
230
+ return `Node ${process.version}`;
231
+ }
232
+ }
233
+ export const BANNER_ROWS = 9;
234
+ export function drawBanner(model) {
235
+ const cols = Math.max(58, Math.min(process.stdout.columns ?? 80, 110));
236
+ const modelName = formatModel(model);
237
+ const innerWidth = cols - 4;
238
+ const cwd = truncate(process.cwd(), Math.min(52, Math.max(20, innerWidth - 36)));
239
+ const branch = getGitBranchFast();
240
+ const art = activeTheme.bannerArt ?? IKIE_BANNER;
241
+ const colors = activeTheme.bannerColors ?? [activeTheme.colors.primary];
242
+ const artWidth = Math.max(...art.map(line => line.length));
243
+ const meta = [
244
+ `${c.accent.bold('ikie')} ${c.muted('·')} ${c.accent(`v${VERSION}`)} ${c.muted('·')} ${c.secondary(modelName)}`,
245
+ `${c.muted('cwd')} ${c.white(cwd)}`,
246
+ branch ? `${c.muted('git')} ${c.secondary(branch)}` : `${c.muted('theme')} ${c.secondary(activeTheme.name)}`,
247
+ c.dim(getSystemStats()),
248
+ '',
249
+ `${c.muted('/help')} ${c.muted('·')} ${c.muted('/theme')} ${c.muted('·')} ${c.muted('Esc cancels running work')}`,
250
+ ];
251
+ process.stdout.write(c.muted('╭' + '─'.repeat(innerWidth) + '╮') + '\n');
252
+ for (let i = 0; i < Math.max(art.length, meta.length); i++) {
253
+ const artLine = art[i] ?? '';
254
+ const artColored = chalk.hex(colors[i % colors.length]).bold(padVisible(artLine, artWidth));
255
+ const metaLine = meta[i] ? ` ${c.muted('│')} ${meta[i]}` : '';
256
+ const row = `${artColored}${metaLine}`;
257
+ process.stdout.write(`${c.muted('│')} ${padVisible(row, innerWidth - 1)}${c.muted('│')}\n`);
258
+ }
259
+ process.stdout.write(c.muted('╰' + '─'.repeat(innerWidth) + '╯') + '\n');
260
+ }
261
+ export function printPromptHeader() {
262
+ const cwdName = basename(process.cwd()) || '/';
263
+ const branch = getGitBranchFast();
264
+ const gitSegment = branch ? ` ${c.muted('on')} ${c.secondary(branch)}` : '';
265
+ const themeSegment = ` ${c.muted('theme')} ${c.secondary(activeTheme.name)}`;
266
+ process.stdout.write(`\n${c.primary('╭─')} ${c.primary.bold('ikie')}${gitSegment}${themeSegment} ${c.muted('in')} ${c.accent(cwdName)}\n`);
267
+ }
268
+ export const PROMPT = c.primary('╰─❯ ');
269
+ export const CONTINUE_PROMPT = c.primary('│ ');
270
+ export class InlineSpinner {
271
+ timer = null;
272
+ delayTimer = null;
273
+ startTime = 0;
274
+ elapsedSince;
275
+ label;
276
+ frameIdx = 0;
277
+ frames = ['.', 'o', 'O', 'o'];
278
+ visible = false;
279
+ constructor(label = 'Thinking', elapsedSince) {
280
+ this.label = label;
281
+ this.elapsedSince = elapsedSince;
282
+ }
283
+ start() {
284
+ if (this.timer || this.delayTimer)
285
+ return;
286
+ this.startTime = Date.now();
287
+ this.frameIdx = 0;
288
+ this.delayTimer = setTimeout(() => {
289
+ this.delayTimer = null;
290
+ this.visible = true;
291
+ this.draw();
292
+ this.timer = setInterval(() => {
293
+ this.frameIdx = (this.frameIdx + 1) % this.frames.length;
294
+ this.draw();
295
+ }, 100);
296
+ }, 250);
297
+ }
298
+ draw() {
299
+ if (!this.visible)
300
+ return;
301
+ const elapsedFrom = this.elapsedSince ?? this.startTime;
302
+ const elapsed = ((Date.now() - elapsedFrom) / 1000).toFixed(1);
303
+ const frame = c.primary(this.frames[this.frameIdx]);
304
+ process.stdout.write(`\r\x1b[2K ${frame} ${c.muted(this.label)} ${c.accent(`${elapsed}s`)} ${c.muted('· Esc to interrupt')}`);
305
+ }
306
+ stop(successMessage) {
307
+ if (this.delayTimer) {
308
+ clearTimeout(this.delayTimer);
309
+ this.delayTimer = null;
310
+ }
311
+ if (this.timer) {
312
+ clearInterval(this.timer);
313
+ this.timer = null;
314
+ }
315
+ const wasVisible = this.visible;
316
+ this.visible = false;
317
+ if (successMessage) {
318
+ process.stdout.write(`\r\x1b[2K ${c.success('ok')} ${c.muted(successMessage)}\n`);
319
+ }
320
+ else if (wasVisible) {
321
+ process.stdout.write('\r\x1b[2K');
322
+ }
323
+ }
324
+ updateLabel(label) {
325
+ this.label = label;
326
+ this.draw();
327
+ }
328
+ }
329
+ export function toolLine(name, args) {
330
+ const tag = name === 'read_file' ? 'READ' :
331
+ name === 'write_file' ? 'WRITE' :
332
+ name === 'edit_file' ? 'EDIT' :
333
+ name === 'bash' ? 'EXEC' :
334
+ name === 'list_dir' ? 'LIST' :
335
+ name === 'grep' || name === 'search_files' ? 'FIND' : 'TOOL';
336
+ return `${c.primary('╭─')} ${c.accent.bold(`[${tag}]`)} ${c.warning.bold(name)}${c.muted('(')}${c.accent(args)}${c.muted(')')}`;
337
+ }
338
+ export function toolSuccessLine(ms, preview) {
339
+ const tail = preview ? ` ${c.muted('│')} ${c.dim(preview)}` : '';
340
+ return `${c.primary('╰─')} ${c.success('ok')} ${c.muted(`${ms}ms`)}${tail}`;
341
+ }
342
+ export function toolErrorLine(msg) {
343
+ return `${c.primary('╰─')} ${c.error('err')} ${c.error(msg)}`;
344
+ }
345
+ export function successLine(msg) {
346
+ return ` ${c.success('ok')} ${c.muted(msg)}`;
347
+ }
348
+ export function errorLine(msg) {
349
+ return ` ${c.error('err')} ${msg}`;
350
+ }
351
+ export function warnLine(msg) {
352
+ return ` ${c.warning('warn')} ${msg}`;
353
+ }
354
+ export function infoLine(msg) {
355
+ return ` ${c.info('info')} ${c.muted(msg)}`;
356
+ }
357
+ export function permissionPrompt(toolName, preview) {
358
+ return (`\n ${c.primary('╭─')} ${c.warning.bold('permission')} ${c.white.bold('Allow')} ${c.warning.bold(toolName)}${c.muted('?')}\n` +
359
+ ` ${c.primary('│')} ${c.muted(preview)}\n` +
360
+ ` ${c.primary('│')} ${c.muted('[')}${c.success.bold('y')}${c.muted(']')} allow ` +
361
+ `${c.muted('[')}${c.error.bold('n')}${c.muted(']')} deny ` +
362
+ `${c.muted('[')}${c.info.bold('a')}${c.muted(']')} always allow ` +
363
+ `${c.muted('[')}${c.muted.bold('!')}${c.muted(']')} always deny\n` +
364
+ ` ${c.primary('╰─❯')} `);
365
+ }
@@ -0,0 +1,5 @@
1
+ import type OpenAI from 'openai';
2
+ export declare const TOOL_DEFS: OpenAI.Chat.ChatCompletionTool[];
3
+ export declare const SAFE_TOOLS: Set<string>;
4
+ export declare function formatToolArgs(name: string, input: Record<string, unknown>): string;
5
+ export declare function executeTool(name: string, input: Record<string, unknown>): Promise<string>;