ikie-cli 0.1.3 → 0.1.4

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
@@ -7,6 +7,8 @@ export interface AgentOptions {
7
7
  signal?: AbortSignal;
8
8
  startedAt?: number;
9
9
  }
10
+ /** Plan = read-only research + propose; Agent = full execution. */
11
+ export type AgentMode = 'agent' | 'plan';
10
12
  export interface AgentTurnStats {
11
13
  modelCalls: number;
12
14
  toolCalls: number;
@@ -37,11 +39,14 @@ export declare class Agent {
37
39
  private activeTurnStats;
38
40
  private activeChangedFiles;
39
41
  private lastTurnStats;
42
+ private mode;
40
43
  constructor(client: OpenAI, config: IkieConfig, systemPrompt: string, depth?: number);
41
44
  clearConversation(): void;
42
45
  getConversation(): ChatCompletionMessageParam[];
43
46
  setConversation(messages: ChatCompletionMessageParam[]): void;
44
47
  getLastTurnStats(): AgentTurnStats;
48
+ getMode(): AgentMode;
49
+ setMode(mode: AgentMode): void;
45
50
  send(userMessage: string | UserContentPart[], opts?: AgentOptions): Promise<void>;
46
51
  private recordChangedFile;
47
52
  private groupToolCalls;
@@ -59,4 +64,5 @@ export declare class Agent {
59
64
  private checkPermission;
60
65
  }
61
66
  export declare const SUBAGENT_FRAMING = "You are a focused sub-agent spawned by Ikie to autonomously complete ONE specific task.\nWork independently \u2014 do not ask the user questions. Use your tools to gather what you\nneed, do the work, and verify it. When finished, your FINAL message must be a concise\nsummary of what you did and any key results (paths changed, findings, answers). That\nsummary is the only thing returned to the main agent, so make it self-contained.";
67
+ export declare const PLAN_MODE_ADDENDUM = "\n\n## PLAN MODE (read-only)\nYou are currently in **plan mode**. You have ONLY read-only tools (read_file,\nlist_dir, search_files, grep, spawn_agent, ask_user). You CANNOT write files, edit\nfiles, run shell commands, or change anything \u2014 those tools are unavailable and any\nattempt will be blocked.\n\nYour job is to investigate and produce a clear, actionable plan:\n- Explore the relevant files and understand the current state before proposing anything.\n- Do NOT describe changes as if you already made them. You haven't.\n- End your response with a concise plan: a short **## Plan** heading followed by\n numbered steps. Name the specific files to change and what to change in each. Call\n out risks, assumptions, or open questions.\n- Keep it tight \u2014 enough to execute from, not an essay.\n\nAfter you present the plan, the user will be asked whether to execute it. On approval,\nyou'll be switched to agent mode and asked to carry out exactly this plan.";
62
68
  export declare function buildSystemPrompt(projectContext: string, memoryContext: string): string;
package/dist/agent.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import * as readline from 'node:readline';
3
- import { TOOL_DEFS, SAFE_TOOLS, formatToolArgs, executeTool } from './tools.js';
3
+ import { TOOL_DEFS, SAFE_TOOLS, PLAN_TOOLS, formatToolArgs, executeTool } from './tools.js';
4
4
  import { renderMarkdown, extractThinkTags } from './renderer.js';
5
5
  import { c, toolLine, permissionPrompt, toolSuccessLine, toolErrorLine, InlineSpinner } from './theme.js';
6
6
  export function estimateTokens(chars) {
@@ -62,6 +62,7 @@ export class Agent {
62
62
  activeTurnStats = null;
63
63
  activeChangedFiles = new Set();
64
64
  lastTurnStats = { modelCalls: 0, toolCalls: 0, filesChanged: 0 };
65
+ mode = 'agent';
65
66
  constructor(client, config, systemPrompt, depth = 0) {
66
67
  this.client = client;
67
68
  this.config = config;
@@ -81,6 +82,12 @@ export class Agent {
81
82
  getLastTurnStats() {
82
83
  return { ...this.lastTurnStats };
83
84
  }
85
+ getMode() {
86
+ return this.mode;
87
+ }
88
+ setMode(mode) {
89
+ this.mode = mode;
90
+ }
84
91
  async send(userMessage, opts = {}) {
85
92
  this.activeTurnStats = { modelCalls: 0, toolCalls: 0, filesChanged: 0 };
86
93
  this.activeChangedFiles = new Set();
@@ -170,6 +177,21 @@ export class Agent {
170
177
  return {};
171
178
  }
172
179
  });
180
+ // Plan mode is read-only. The model normally isn't even offered mutating
181
+ // tools (see buildParams), but refuse here too as defense-in-depth.
182
+ if (this.mode === 'plan' && !PLAN_TOOLS.has(group[0].name)) {
183
+ if (this.activeTurnStats)
184
+ this.activeTurnStats.toolCalls += group.length;
185
+ process.stdout.write(`\n${this.indent}${toolLine(group[0].name, formatToolArgs(group[0].name, inputs[0])).trimStart()}\n`);
186
+ process.stdout.write(`${this.indent}${toolErrorLine('blocked · plan mode is read-only')}\n`);
187
+ for (const tc of group) {
188
+ this.conversation.push({
189
+ role: 'tool', tool_call_id: tc.id,
190
+ content: 'Blocked: plan mode is read-only. Do not attempt changes — propose a plan instead. The user can switch to agent mode to apply it.',
191
+ });
192
+ }
193
+ continue;
194
+ }
173
195
  if (group.length === 1) {
174
196
  if (this.activeTurnStats)
175
197
  this.activeTurnStats.toolCalls++;
@@ -220,8 +242,8 @@ export class Agent {
220
242
  }
221
243
  const ms = Date.now() - t0;
222
244
  const lineStr = errors === 0
223
- ? toolSuccessLine(ms, `Executed ${group.length} operations`)
224
- : toolErrorLine(`${errors} of ${group.length} operations failed`);
245
+ ? toolSuccessLine(ms, `${group.length} operations`)
246
+ : toolErrorLine(`${errors} of ${group.length} operations`);
225
247
  process.stdout.write(`${this.indent}${lineStr}\n`);
226
248
  }
227
249
  }
@@ -240,9 +262,15 @@ export class Agent {
240
262
  }
241
263
  }
242
264
  buildParams() {
243
- const tools = this.depth >= 1
265
+ let tools = this.depth >= 1
244
266
  ? TOOL_DEFS.filter(t => t.function.name !== 'spawn_agent')
245
267
  : TOOL_DEFS;
268
+ // Plan mode: only offer read-only tools, and steer toward proposing a plan.
269
+ let systemContent = this.systemPrompt;
270
+ if (this.mode === 'plan') {
271
+ tools = tools.filter(t => PLAN_TOOLS.has(t.function.name));
272
+ systemContent += PLAN_MODE_ADDENDUM;
273
+ }
246
274
  return {
247
275
  model: this.config.model,
248
276
  max_tokens: this.config.maxTokens,
@@ -252,7 +280,7 @@ export class Agent {
252
280
  presence_penalty: this.config.presencePenalty,
253
281
  frequency_penalty: this.config.frequencyPenalty,
254
282
  messages: [
255
- { role: 'system', content: this.systemPrompt },
283
+ { role: 'system', content: systemContent },
256
284
  ...this.conversation,
257
285
  ],
258
286
  tools,
@@ -587,9 +615,29 @@ Work independently — do not ask the user questions. Use your tools to gather w
587
615
  need, do the work, and verify it. When finished, your FINAL message must be a concise
588
616
  summary of what you did and any key results (paths changed, findings, answers). That
589
617
  summary is the only thing returned to the main agent, so make it self-contained.`;
618
+ // Appended to the system prompt while in PLAN mode. The model is given only
619
+ // read-only tools (see buildParams); this steers it to research and propose.
620
+ export const PLAN_MODE_ADDENDUM = `
621
+
622
+ ## PLAN MODE (read-only)
623
+ You are currently in **plan mode**. You have ONLY read-only tools (read_file,
624
+ list_dir, search_files, grep, spawn_agent, ask_user). You CANNOT write files, edit
625
+ files, run shell commands, or change anything — those tools are unavailable and any
626
+ attempt will be blocked.
627
+
628
+ Your job is to investigate and produce a clear, actionable plan:
629
+ - Explore the relevant files and understand the current state before proposing anything.
630
+ - Do NOT describe changes as if you already made them. You haven't.
631
+ - End your response with a concise plan: a short **## Plan** heading followed by
632
+ numbered steps. Name the specific files to change and what to change in each. Call
633
+ out risks, assumptions, or open questions.
634
+ - Keep it tight — enough to execute from, not an essay.
635
+
636
+ After you present the plan, the user will be asked whether to execute it. On approval,
637
+ you'll be switched to agent mode and asked to carry out exactly this plan.`;
590
638
  export function buildSystemPrompt(projectContext, memoryContext) {
591
639
  const parts = [
592
- `You are Ikie, an expert agentic coding assistant running in the terminal.
640
+ `You are Ikie, an elite agentic software engineer running in the terminal.
593
641
 
594
642
  ## Identity
595
643
  Your name is Ikie. If asked what you are, who made you, or what model powers you,
@@ -598,42 +646,64 @@ Claude, ChatGPT, GPT, Gemini, Llama, or any other named model or company's assis
598
646
  Do not speculate about your underlying model.
599
647
 
600
648
  You help developers write, debug, understand, and refactor code. You work autonomously
601
- using your tools to accomplish tasks. Be direct, concise, and practical.
649
+ using your tools to accomplish tasks. Be direct, concise, and pragmatic. Optimize for
650
+ the user's actual goal, not the literal letter of the request — but never overstep into
651
+ changes they didn't ask for.
602
652
 
603
653
  ## Response Formatting
604
654
  - Use **markdown formatting** in all your responses for better readability
605
- - Use **bold** for emphasis: **important text**
606
- - Use *italics* for secondary emphasis: *note*
607
- - Use \`inline code\` for: variable names, file names, short code snippets, commands
608
- - Use code blocks with language tags for multi-line code (triple backticks)
609
- - Use bullet points (-, *, +) or numbered lists for steps and options
610
- - Use headers (##, ###) to organize longer responses
611
- - Use tables for structured data comparisons
612
- - Keep responses **concise but well-formatted** - the terminal renderer supports rich markdown
655
+ - Use **bold** for key takeaways, \`inline code\` for identifiers, file names, commands
656
+ - Use fenced code blocks with language tags for multi-line code
657
+ - Use bullet/numbered lists for steps and options; headers (##, ###) to organize long replies
658
+ - Use tables for structured comparisons
659
+ - Keep responses **concise but well-formatted** lead with the answer, then detail.
660
+ Don't narrate routine tool calls; show results. The terminal renders rich markdown.
613
661
 
614
662
  ## Working Style
615
663
  - ALWAYS provide complete, valid arguments to tools. Never omit required fields like \`path\`.
616
- - When creating files, use the FULL file path (relative or absolute) — not just a filename.
617
- - Read files before editing them
618
- - Prefer \`edit_file\` over \`write_file\` for modifications (surgical edits > full rewrites)
619
- - Run tests or sanity checks after making meaningful changes
620
- - For complex tasks, break them down and tackle one step at a time
621
- - Acknowledge errors and self-correct; don't pretend failures didn't happen
622
- - When writing large files, write the COMPLETE content. Never truncate or use placeholders.
623
- - For long-running servers/processes, use \`nohup cmd &\` or \`setsid cmd\` so they survive after the session.
664
+ - When creating files, use the FULL file path — not just a filename.
665
+ - Read files before editing them. Prefer \`edit_file\` (surgical) over \`write_file\` (full rewrite).
666
+ - When writing files, write the COMPLETE content. Never truncate or leave \`// ...\` placeholders.
667
+ - For complex tasks, break them down and tackle one step at a time.
668
+ - Acknowledge errors and self-correct; never pretend a failure didn't happen.
669
+ - For long-running servers/processes, use \`nohup cmd &\` or \`setsid cmd\` so they survive the session.
670
+
671
+ ## Engineering Principles
672
+ - **Match the codebase.** Before writing code, infer the project's conventions — language,
673
+ framework, formatting, naming, error handling, file layout — and follow them. Check
674
+ imports/neighboring files for the libraries actually in use; never assume a dependency exists.
675
+ - **Smallest correct change.** Solve the problem at its root, not with a patch over a symptom.
676
+ Avoid unrelated refactors and scope creep. Don't reformat code you aren't changing.
677
+ - **Type safety & correctness.** Honor existing types; avoid \`any\`/unsafe casts. Handle the
678
+ error and edge cases (empty, null, boundary, concurrency), not just the happy path.
679
+ - **No secrets in code.** Never hardcode keys, tokens, or credentials. Read from env/config.
680
+ Never log or echo secrets. Validate and sanitize all external input.
681
+ - **Clarity over cleverness.** Write code a teammate can read. Name things well. Add comments
682
+ only for non-obvious *why*, not to restate the *what*.
683
+ - **Leave it green.** Don't introduce dead code, unused imports, or commented-out blocks.
684
+
685
+ ## Modern Design (when building UI / frontend)
686
+ - **Aesthetic by default.** Produce clean, modern, professional interfaces — generous
687
+ whitespace, clear visual hierarchy, consistent spacing scale, restrained palette.
688
+ - **Design tokens, not magic numbers.** Reuse the project's theme/variables (Tailwind config,
689
+ CSS custom properties, design system). Don't scatter ad-hoc hex colors or pixel values.
690
+ - **Responsive & accessible.** Mobile-first; fluid layouts. Semantic HTML, proper
691
+ labels/alt text, keyboard focus states, sufficient contrast (WCAG AA), respect reduced-motion.
692
+ - **Layout with flex/grid.** Use flexbox and CSS grid for fluid, mobile-first layouts.
693
+ - **Polish the details.** Hover/focus/active/disabled states, loading and empty states, smooth
694
+ but subtle transitions, sensible defaults. Avoid layout shift.
695
+ - **Reuse before you build.** Prefer existing components and utilities over new one-offs.
624
696
 
625
697
  ## Approach
626
- - Understand before acting: read the relevant files and explore the structure
627
- instead of guessing. A few targeted reads beat one wrong edit.
628
- - For multi-step work, form a short plan, then execute it step by step and adapt
629
- as you learn. Keep momentum — don't stall on decisions you can reverse later.
630
- - Verify your work: after edits, re-read the changed regions and run the build,
631
- tests, or linter. Fix what you broke before you call a task done.
698
+ - Understand before acting: read the relevant files and explore the structure instead of
699
+ guessing. A few targeted reads beat one wrong edit.
700
+ - For multi-step work, form a short plan, then execute it step by step and adapt as you learn.
701
+ Keep momentum — don't stall on decisions you can reverse later.
702
+ - Verify your work: after edits, re-read the changed regions and run the build, tests, or
703
+ linter. Fix what you broke before you call a task done.
632
704
  - Delegate isolated or parallelizable investigation to \`spawn_agent\` to stay focused.
633
- - **Format your responses beautifully**: Use markdown syntax consistently - headers, lists,
634
- code blocks, bold/italics. The terminal has excellent markdown rendering.
635
- - Be concise but clear: explain what you did and why in well-formatted points.
636
- Show code/results rather than narrating. Use \`ask_user\` only when genuinely blocked.
705
+ - Be concise but clear: explain what you did and why in well-formatted points. Show
706
+ code/results rather than narrating. Use \`ask_user\` only when genuinely blocked.
637
707
  - Never leave a task half-finished or claim a success you have not verified.
638
708
 
639
709
  ## Tools Available
package/dist/index.js CHANGED
@@ -40,7 +40,8 @@ ${c.primary('Environment:')}
40
40
  ${c.muted('IKIE_API_KEY')} Alternative key env var
41
41
 
42
42
  ${c.primary('In-session commands:')}
43
- ${c.muted('/help /clear /memory /session /context /model /rpm /exit')}
43
+ ${c.muted('/help /clear /memory /session /plan /agent /usage /model /exit')}
44
+ ${c.muted('Shift+Tab Toggle plan ⇄ agent mode')}
44
45
  ${c.muted('!<cmd> Run shell command directly')}
45
46
  ${c.muted('Ctrl+V Paste image from clipboard')}
46
47
  `);
package/dist/repl.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as readline from 'node:readline';
2
2
  import { execSync } from 'child_process';
3
3
  import { restoreStdinListeners } from './agent.js';
4
- import { c, PROMPT, CONTINUE_PROMPT, printPromptHeader, 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, } from './theme.js';
5
5
  import { renderMarkdown } from './renderer.js';
6
6
  import { loadAllMemory } from './memory.js';
7
- import { HOME_DIR, saveConfig, DEFAULT_MODEL, FIREWORKS_BASE_URL, IKIE_HOST, isLoggedIn } from './config.js';
7
+ import { HOME_DIR, saveConfig, DEFAULT_MODEL, FIREWORKS_BASE_URL, IKIE_HOST, isLoggedIn, getApiKey } 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';
@@ -63,10 +63,9 @@ const SLASH_CMDS = [
63
63
  },
64
64
  {
65
65
  name: 'session',
66
- desc: 'Save/load chat sessions',
66
+ desc: 'Load/manage chat sessions',
67
67
  subs: [
68
68
  { name: 'list', desc: 'List sessions' },
69
- { name: 'save', desc: 'Save current session' },
70
69
  { name: 'load', desc: 'Load a session' },
71
70
  { name: 'new', desc: 'Start fresh session' },
72
71
  { name: 'delete', desc: 'Delete a session' },
@@ -94,6 +93,10 @@ const SLASH_CMDS = [
94
93
  { name: 'theme', desc: 'Change visual theme', args: '[name]' },
95
94
  { name: 'rpm', desc: 'Set request limit', args: '[number]' },
96
95
  { name: 'tokens', desc: 'Token estimate' },
96
+ { name: 'usage', desc: 'Show account usage & credit' },
97
+ { name: 'mode', desc: 'Show/set mode (plan|agent)', args: '[plan|agent]' },
98
+ { name: 'plan', desc: 'Switch to plan mode (read-only)' },
99
+ { name: 'agent', desc: 'Switch to agent mode (full)' },
97
100
  { name: 'login', desc: 'Sign in to ikie account' },
98
101
  { name: 'logout', desc: 'Sign out of ikie account' },
99
102
  { name: 'exit', desc: 'Exit Ikie' },
@@ -128,9 +131,8 @@ ${c.primary.bold('Ikie Commands')}
128
131
  ${c.warning('/image list')} List queued images
129
132
  ${c.warning('/image clear')} Clear queued images
130
133
  ${c.warning('/session list')} List saved sessions
131
- ${c.warning('/session save')} Save current chat
132
134
  ${c.warning('/session load')} Load saved chat
133
- ${c.warning('/session new')} Start a fresh chat
135
+ ${c.warning('/session new')} Start a fresh chat (auto-saved)
134
136
  ${c.warning('/context')} Show current project context
135
137
  ${c.warning('/models')} List available models
136
138
  ${c.warning('/model <name>')} Switch AI model
@@ -148,6 +150,10 @@ ${c.primary.bold('Ikie Commands')}
148
150
  ${c.warning('/theme')} Pick a theme interactively
149
151
  ${c.warning('/rpm [number]')} Show or set request limit
150
152
  ${c.warning('/tokens')} Show conversation token estimate
153
+ ${c.warning('/usage')} Show account usage & credit balance
154
+ ${c.warning('/plan')} Plan mode — research & propose, no changes
155
+ ${c.warning('/agent')} Agent mode — full execution (default)
156
+ ${c.warning('/mode')} Show or set mode ${c.muted('(Shift+Tab toggles)')}
151
157
  ${c.warning('/login')} Sign in to ikie account
152
158
  ${c.warning('/logout')} Sign out of ikie account
153
159
  ${c.warning('/exit')} Exit Ikie
@@ -258,7 +264,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
258
264
  if (sub === 'list' || sub === 'ls') {
259
265
  const sessions = listSessions();
260
266
  if (!sessions.length) {
261
- console.log(infoLine('No saved sessions yet. Use /session save <name>.'));
267
+ console.log(infoLine('No saved sessions yet. Every chat is saved automatically.'));
262
268
  return true;
263
269
  }
264
270
  console.log(`\n${c.primary.bold('Sessions')}`);
@@ -268,13 +274,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
268
274
  const model = s.model ? ` ${c.muted(s.model.split('/').pop() ?? s.model)}` : '';
269
275
  console.log(` ${active} ${c.secondary(s.name.padEnd(24))} ${c.muted(String(s.messageCount).padStart(3) + ' msgs')} ${c.dim(when)}${model}`);
270
276
  }
271
- return true;
272
- }
273
- if (sub === 'save') {
274
- const name = normalizeSessionName(nameArg || sessionState.activeName);
275
- const saved = saveSession(name, config.model, agent.getConversation());
276
- sessionState.activeName = saved.name;
277
- console.log(successLine(`Session saved as "${saved.name}".`));
277
+ console.log(`\n ${c.muted('Resume with')} ${c.warning('/session load <name>')}\n`);
278
278
  return true;
279
279
  }
280
280
  if (sub === 'load') {
@@ -297,8 +297,8 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
297
297
  }
298
298
  if (sub === 'new') {
299
299
  agent.clearConversation();
300
- sessionState.activeName = nameArg ? normalizeSessionName(nameArg) : undefined;
301
- console.log(successLine(sessionState.activeName ? `Started new session "${sessionState.activeName}".` : 'Started a fresh session.'));
300
+ sessionState.activeName = nameArg ? normalizeSessionName(nameArg) : normalizeSessionName();
301
+ console.log(successLine(`Started new session "${sessionState.activeName}" (auto-saved).`));
302
302
  return true;
303
303
  }
304
304
  if (sub === 'delete' || sub === 'rm') {
@@ -318,7 +318,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
318
318
  }
319
319
  return true;
320
320
  }
321
- console.log(errorLine('Usage: /session list|save [name]|load <name>|new [name]|delete <name>'));
321
+ console.log(errorLine('Usage: /session list|load <name>|new [name]|delete <name>'));
322
322
  return true;
323
323
  }
324
324
  case 'context':
@@ -530,6 +530,56 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
530
530
  console.log(infoLine(`~${Math.round(approxTokens).toLocaleString()} tokens in conversation`));
531
531
  return true;
532
532
  }
533
+ case 'plan': {
534
+ agent.setMode('plan');
535
+ console.log(successLine(`Switched to ${modeTag('plan')} mode — read-only. Ikie will research and propose a plan.`));
536
+ return true;
537
+ }
538
+ case 'agent': {
539
+ agent.setMode('agent');
540
+ console.log(successLine(`Switched to ${modeTag('agent')} mode — full execution.`));
541
+ return true;
542
+ }
543
+ case 'mode': {
544
+ const want = (args[0] ?? '').toLowerCase();
545
+ if (want === 'plan' || want === 'agent') {
546
+ agent.setMode(want);
547
+ console.log(successLine(`Mode set to ${modeTag(want)}.`));
548
+ }
549
+ else if (want) {
550
+ console.log(errorLine('Usage: /mode [plan|agent]'));
551
+ }
552
+ else {
553
+ console.log(infoLine(`Current mode: ${modeTag(agent.getMode())} ${c.muted('· Shift+Tab to toggle')}`));
554
+ }
555
+ return true;
556
+ }
557
+ case 'usage': {
558
+ if (!isLoggedIn(config)) {
559
+ console.log(infoLine('Not signed in. Run /login to connect your ikie account.'));
560
+ return true;
561
+ }
562
+ const apiKey = getApiKey(config);
563
+ if (!apiKey) {
564
+ console.log(errorLine('No API key found. Run /login again.'));
565
+ return true;
566
+ }
567
+ try {
568
+ const res = await fetch(`${IKIE_HOST}/api/usage`, {
569
+ headers: { Authorization: `Bearer ${apiKey}` },
570
+ });
571
+ if (!res.ok) {
572
+ console.log(errorLine(`Couldn't fetch usage (HTTP ${res.status}).`));
573
+ return true;
574
+ }
575
+ const data = (await res.json());
576
+ printUsage(data);
577
+ }
578
+ catch (err) {
579
+ console.log(errorLine(`Couldn't reach ikie at ${IKIE_HOST}: ${err instanceof Error ? err.message : String(err)}`));
580
+ }
581
+ return true;
582
+ }
533
583
  case 'exit':
534
584
  case 'quit':
535
585
  case 'q':
@@ -540,6 +590,43 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
540
590
  return true;
541
591
  }
542
592
  }
593
+ function fmtUsd(n) {
594
+ if (n === 0)
595
+ return '$0.00';
596
+ if (n < 0.01)
597
+ return `$${n.toFixed(6)}`;
598
+ return `$${n.toFixed(2)}`;
599
+ }
600
+ function fmtTokens(n) {
601
+ if (n < 1000)
602
+ return String(n);
603
+ if (n < 1_000_000)
604
+ return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
605
+ return (n / 1_000_000).toFixed(2).replace(/\.00$/, '') + 'M';
606
+ }
607
+ function printUsage(data) {
608
+ const s = data.stats;
609
+ const row = (label, value) => console.log(` ${c.secondary(label.padEnd(18))} ${c.white(value)}`);
610
+ console.log(`\n${c.primary.bold('Account Usage')}\n`);
611
+ row('Credit balance', c.success.bold(fmtUsd(data.balance)).toString());
612
+ row('Total spent', fmtUsd(s.totalCost));
613
+ console.log();
614
+ row('Requests · 30d', s.requests30d.toLocaleString());
615
+ row('Requests · 24h', s.requests24h.toLocaleString());
616
+ row('Requests · all', s.totalRequests.toLocaleString());
617
+ console.log();
618
+ row('Total tokens', fmtTokens(s.totalTokens));
619
+ row('Read / Write', `${fmtTokens(s.readTokens)} / ${fmtTokens(s.writeTokens)}`);
620
+ row('Cached tokens', fmtTokens(s.cachedTokens));
621
+ console.log();
622
+ row('Avg latency', s.avgLatencyMs ? `${s.avgLatencyMs}ms` : '—');
623
+ row('Cache hit rate', `${(s.cacheHitRate * 100).toFixed(1)}%`);
624
+ if (data.model) {
625
+ row('Model', data.model.name);
626
+ row('Context window', `${data.model.context_window.toLocaleString()} tokens`);
627
+ }
628
+ console.log(`\n ${c.muted('Full dashboard:')} ${c.info(`${IKIE_HOST}/dashboard`)}\n`);
629
+ }
543
630
  function promptLine(prompt) {
544
631
  return new Promise((resolve) => {
545
632
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -549,6 +636,30 @@ function promptLine(prompt) {
549
636
  });
550
637
  });
551
638
  }
639
+ /**
640
+ * After a plan-mode turn, ask whether to execute the proposed plan.
641
+ * Reads a single keypress. Assumes stdin is already in raw mode (it is, during
642
+ * a turn). Returns true on y / Enter, false otherwise.
643
+ */
644
+ function confirmExecutePlan() {
645
+ return new Promise((resolve) => {
646
+ if (!process.stdin.isTTY) {
647
+ resolve(false);
648
+ return;
649
+ }
650
+ process.stdout.write(`\n ${c.primary('▸')} ${c.white.bold('Plan ready.')} ${c.muted('Execute it now?')} ` +
651
+ `${c.success.bold('y')} ${c.muted('yes')} ${c.error.bold('n')} ${c.muted('keep planning')}\n` +
652
+ ` ${c.muted('❯')} `);
653
+ const onKey = (d) => {
654
+ process.stdin.removeListener('data', onKey);
655
+ const k = d.toString().toLowerCase();
656
+ const yes = k === 'y' || k === '\r' || k === '\n';
657
+ process.stdout.write(yes ? c.success('y\n') : c.muted('n\n'));
658
+ resolve(yes);
659
+ };
660
+ process.stdin.on('data', onKey);
661
+ });
662
+ }
552
663
  function printGoodbye(sessionState, agent, config) {
553
664
  const msgs = agent.getConversation();
554
665
  const hasConversation = msgs.length > 0;
@@ -562,15 +673,11 @@ function printGoodbye(sessionState, agent, config) {
562
673
  }
563
674
  }
564
675
  console.log(`\n${c.primary.bold('Goodbye!')}\n`);
565
- if (sessionState.activeName) {
566
- console.log(` ${c.muted('Your session')} ${c.secondary.bold(sessionState.activeName)} ${c.muted('has been saved.')}`);
567
- console.log(`\n ${c.muted('To continue:')}`);
568
- console.log(` ${c.accent('ikie')} ${c.warning(`/session load ${sessionState.activeName}`)}\n`);
569
- }
570
- else if (hasConversation) {
571
- console.log(` ${c.warning('\u26A0')} ${c.muted('Your current conversation was not saved.')}`);
572
- console.log(`\n ${c.muted('To save next time:')}`);
573
- console.log(` ${c.warning('/session save')} ${c.muted('<name>')}\n`);
676
+ if (sessionState.activeName && hasConversation) {
677
+ console.log(` ${c.muted('Your session')} ${c.secondary.bold(sessionState.activeName)} ${c.muted('was saved.')}`);
678
+ console.log(`\n ${c.muted('To pick up where you left off:')}`);
679
+ console.log(` ${c.accent('ikie')} ${c.warning(`/session load ${sessionState.activeName}`)}`);
680
+ console.log(`\n ${c.muted('Or list everything with')} ${c.warning('/session list')}\n`);
574
681
  }
575
682
  console.log(` ${c.muted('Happy coding!')}\n`);
576
683
  }
@@ -677,7 +784,11 @@ function selectThemeInteractively(rl, config) {
677
784
  }
678
785
  export async function startREPL(agent, config, projectContext, oneShot) {
679
786
  void HISTORY_FILE;
680
- if (oneShot) {
787
+ // A `/`-prefixed launch arg (e.g. `ikie /session load foo`) is a slash
788
+ // COMMAND, not a prompt — run it after setup, then stay interactive.
789
+ // A plain launch arg is a one-shot prompt: run it once and exit.
790
+ const initialCommand = oneShot && oneShot.trim().startsWith('/') ? oneShot.trim() : undefined;
791
+ if (oneShot && !initialCommand) {
681
792
  try {
682
793
  await agent.send(oneShot, { autoApprove: config.autoApprove });
683
794
  }
@@ -699,7 +810,9 @@ export async function startREPL(agent, config, projectContext, oneShot) {
699
810
  let multilineBuffer = '';
700
811
  let busy = false;
701
812
  let ctrlCCount = 0;
702
- const sessionState = {};
813
+ // Every session auto-saves. Start with a generated name so there's always
814
+ // somewhere to persist to; /session load can resume it later.
815
+ const sessionState = { activeName: normalizeSessionName() };
703
816
  const imageState = { nextId: 1, pending: [] };
704
817
  let pasteSeq = 0;
705
818
  let pasteMode = false;
@@ -731,6 +844,18 @@ export async function startREPL(agent, config, projectContext, oneShot) {
731
844
  };
732
845
  const routeInputData = (data) => {
733
846
  const text = data.toString('utf8');
847
+ // Shift+Tab (CSI Z) → cycle agent⇄plan mode live at the prompt.
848
+ if (text === '\x1b[Z') {
849
+ const next = agent.getMode() === 'agent' ? 'plan' : 'agent';
850
+ agent.setMode(next);
851
+ const saved = rl.line || '';
852
+ process.stdout.write('\r\x1b[2K'); // clear the current prompt line
853
+ process.stdout.write(` ${c.muted('▸')} ${modeTag(next)} ${c.muted('mode')}\n`);
854
+ printPromptHeader(next);
855
+ rl.setPrompt(PROMPT);
856
+ process.stdout.write(rl.getPrompt() + saved);
857
+ return;
858
+ }
734
859
  // Check for Ctrl+V (0x16) - try to paste image from clipboard
735
860
  if (data.length === 1 && data[0] === 0x16) {
736
861
  // Check if clipboard has an image
@@ -813,7 +938,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
813
938
  rl.setPrompt(CONTINUE_PROMPT);
814
939
  }
815
940
  else {
816
- printPromptHeader();
941
+ printPromptHeader(agent.getMode());
817
942
  if (imageState.pending.length) {
818
943
  const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
819
944
  process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
@@ -883,8 +1008,10 @@ export async function startREPL(agent, config, projectContext, oneShot) {
883
1008
  const userContent = buildUserContent(agentInput, imagesForTurn);
884
1009
  imageState.pending = [];
885
1010
  let restoredImages = false;
1011
+ // Clear the (now-submitted) prompt line. The agent's InlineSpinner provides
1012
+ // the "Working… · Esc to interrupt" feedback and clears itself when done, so
1013
+ // we don't print a static line here (it would never get cleared).
886
1014
  process.stdout.write('\r\x1b[2K');
887
- process.stdout.write(`${c.muted(' Working. Press Esc to interrupt.')}\n`);
888
1015
  busy = true;
889
1016
  rl.pause();
890
1017
  const taskStartedAt = Date.now();
@@ -926,6 +1053,24 @@ export async function startREPL(agent, config, projectContext, oneShot) {
926
1053
  if (sessionState.activeName && !abortController.signal.aborted) {
927
1054
  saveSession(sessionState.activeName, config.model, agent.getConversation());
928
1055
  }
1056
+ // Plan mode: a plan was just proposed — offer to execute it.
1057
+ if (agent.getMode() === 'plan' && !abortController.signal.aborted) {
1058
+ process.stdin.removeListener('data', cancelHandler);
1059
+ const go = await confirmExecutePlan();
1060
+ process.stdin.on('data', cancelHandler);
1061
+ if (go && !abortController.signal.aborted) {
1062
+ agent.setMode('agent');
1063
+ process.stdout.write(` ${c.muted('→ switching to')} ${modeTag('agent')} ${c.muted('mode, executing…')}\n`);
1064
+ await agent.send('Execute the plan you just proposed. Make the changes now.', {
1065
+ autoApprove: config.autoApprove,
1066
+ signal: abortController.signal,
1067
+ startedAt: taskStartedAt,
1068
+ });
1069
+ if (sessionState.activeName && !abortController.signal.aborted) {
1070
+ saveSession(sessionState.activeName, config.model, agent.getConversation());
1071
+ }
1072
+ }
1073
+ }
929
1074
  }
930
1075
  catch (err) {
931
1076
  if (!abortController.signal.aborted) {
@@ -944,13 +1089,16 @@ export async function startREPL(agent, config, projectContext, oneShot) {
944
1089
  }
945
1090
  const elapsed = formatElapsed(Date.now() - taskStartedAt);
946
1091
  process.stdin.removeListener('data', cancelHandler);
947
- if (process.stdin.isTTY)
948
- process.stdin.setRawMode(false);
949
1092
  process.stdin.pause();
950
1093
  restoreStdinListeners(savedDataListeners, savedKeypressListeners);
951
1094
  busy = false;
952
1095
  ctrlCCount = 0;
953
1096
  rl.resume();
1097
+ // readline was created with terminal:true and owns echo while in raw mode.
1098
+ // Re-enable raw mode (the turn's cancel handler may have toggled it) so the
1099
+ // TTY doesn't also echo input — otherwise every line is echoed twice.
1100
+ if (process.stdin.isTTY)
1101
+ process.stdin.setRawMode(true);
954
1102
  const status = abortController.signal.aborted
955
1103
  ? c.warning(formatTaskTimeline(agent, elapsed, 'cancelled'))
956
1104
  : taskFailed
@@ -964,5 +1112,15 @@ export async function startREPL(agent, config, projectContext, oneShot) {
964
1112
  rl.on('line', (raw) => {
965
1113
  chain = chain.then(() => handleLine(raw));
966
1114
  });
1115
+ // If launched with a slash command (e.g. `ikie /session load foo`), run it
1116
+ // once now, then hand control to the user with the result already applied.
1117
+ if (initialCommand) {
1118
+ try {
1119
+ await handleSlashCommand(initialCommand, agent, config, projectContext, rl, sessionState, imageState);
1120
+ }
1121
+ catch (err) {
1122
+ console.error(errorLine(err instanceof Error ? err.message : String(err)));
1123
+ }
1124
+ }
967
1125
  showPrompt();
968
1126
  }
package/dist/theme.d.ts CHANGED
@@ -38,7 +38,8 @@ export declare const c: {
38
38
  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
- export declare function printPromptHeader(): void;
41
+ export declare function modeTag(mode: 'agent' | 'plan'): string;
42
+ export declare function printPromptHeader(mode?: 'agent' | 'plan'): void;
42
43
  export declare const PROMPT: string;
43
44
  export declare const CONTINUE_PROMPT: string;
44
45
  export declare class InlineSpinner {
package/dist/theme.js CHANGED
@@ -248,22 +248,24 @@ export function drawBanner(model) {
248
248
  '',
249
249
  `${c.muted('/help')} ${c.muted('·')} ${c.muted('/theme')} ${c.muted('·')} ${c.muted('Esc cancels running work')}`,
250
250
  ];
251
- process.stdout.write(c.muted('╭' + '─'.repeat(innerWidth) + '╮') + '\n');
251
+ process.stdout.write('\n');
252
252
  for (let i = 0; i < Math.max(art.length, meta.length); i++) {
253
253
  const artLine = art[i] ?? '';
254
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`);
255
+ const metaLine = meta[i] ? ` ${meta[i]}` : '';
256
+ process.stdout.write(` ${artColored}${metaLine}\n`);
258
257
  }
259
- process.stdout.write(c.muted('╰' + '─'.repeat(innerWidth) + '╯') + '\n');
258
+ process.stdout.write('\n');
260
259
  }
261
- export function printPromptHeader() {
260
+ export function modeTag(mode) {
261
+ return mode === 'plan' ? c.warning.bold('plan') : c.success('agent');
262
+ }
263
+ export function printPromptHeader(mode = 'agent') {
262
264
  const cwdName = basename(process.cwd()) || '/';
263
265
  const branch = getGitBranchFast();
264
266
  const gitSegment = branch ? ` ${c.muted('on')} ${c.secondary(branch)}` : '';
265
267
  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`);
268
+ process.stdout.write(`\n${c.primary('╭─')} ${c.primary.bold('ikie')} ${c.muted('·')} ${modeTag(mode)}${gitSegment}${themeSegment} ${c.muted('in')} ${c.accent(cwdName)}\n`);
267
269
  }
268
270
  export const PROMPT = c.primary('╰─❯ ');
269
271
  export const CONTINUE_PROMPT = c.primary('│ ');
@@ -326,21 +328,45 @@ export class InlineSpinner {
326
328
  this.draw();
327
329
  }
328
330
  }
331
+ // Maps a tool name to a clean verb + a dot color reflecting its effect.
332
+ // Read-only tools are calm (info), mutating tools warn, exec/agent stand out.
333
+ function toolMeta(rawName) {
334
+ const base = rawName.split(/\s|×/)[0];
335
+ switch (base) {
336
+ case 'read_file': return { verb: 'read', tint: c.info };
337
+ case 'write_file': return { verb: 'write', tint: c.warning };
338
+ case 'edit_file': return { verb: 'edit', tint: c.warning };
339
+ case 'bash': return { verb: 'run', tint: c.accent };
340
+ case 'list_dir': return { verb: 'list', tint: c.info };
341
+ case 'grep': return { verb: 'grep', tint: c.info };
342
+ case 'search_files': return { verb: 'find', tint: c.info };
343
+ case 'memory_write': return { verb: 'memory', tint: c.secondary };
344
+ case 'spawn_agent': return { verb: 'agent', tint: c.secondary };
345
+ case 'ask_user': return { verb: 'ask', tint: c.info };
346
+ default: return { verb: base, tint: c.primary };
347
+ }
348
+ }
329
349
  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(')')}`;
350
+ const { verb, tint } = toolMeta(name);
351
+ // Surface a "×N" batch suffix as a subtle count badge.
352
+ const countMatch = name.match(/×(\d+)/);
353
+ const badge = countMatch ? ` ${c.muted(`×${countMatch[1]}`)}` : '';
354
+ const label = c.white.bold(verb.padEnd(6));
355
+ return `${tint('')} ${label}${badge} ${c.dim(args)}`;
337
356
  }
338
357
  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}`;
358
+ const time = c.muted(formatDuration(ms));
359
+ const tail = preview ? ` ${c.dim(preview)}` : '';
360
+ return ` ${c.muted('⎿')} ${time}${tail}`;
341
361
  }
342
362
  export function toolErrorLine(msg) {
343
- return `${c.primary('╰─')} ${c.error('err')} ${c.error(msg)}`;
363
+ return ` ${c.muted('')} ${c.error('failed')} ${c.error(msg)}`;
364
+ }
365
+ function formatDuration(ms) {
366
+ if (ms < 1000)
367
+ return `${ms}ms`;
368
+ const s = ms / 1000;
369
+ return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`;
344
370
  }
345
371
  export function successLine(msg) {
346
372
  return ` ${c.success('ok')} ${c.muted(msg)}`;
@@ -355,11 +381,12 @@ export function infoLine(msg) {
355
381
  return ` ${c.info('info')} ${c.muted(msg)}`;
356
382
  }
357
383
  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('╰─❯')} `);
384
+ const { verb, tint } = toolMeta(toolName);
385
+ return (`\n ${tint('●')} ${c.white.bold('permission')} ${c.muted('·')} ${c.white(verb)} ${c.dim(preview)}\n` +
386
+ ` ${c.muted('')} ` +
387
+ `${c.success.bold('y')} ${c.muted('allow')} ` +
388
+ `${c.error.bold('n')} ${c.muted('deny')} ` +
389
+ `${c.info.bold('a')} ${c.muted('always')} ` +
390
+ `${c.muted.bold('!')} ${c.muted('never')}\n` +
391
+ ` ${c.muted('❯')} `);
365
392
  }
package/dist/tools.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type OpenAI from 'openai';
2
2
  export declare const TOOL_DEFS: OpenAI.Chat.ChatCompletionTool[];
3
3
  export declare const SAFE_TOOLS: Set<string>;
4
+ export declare const PLAN_TOOLS: Set<string>;
4
5
  export declare function formatToolArgs(name: string, input: Record<string, unknown>): string;
5
6
  export declare function executeTool(name: string, input: Record<string, unknown>): Promise<string>;
package/dist/tools.js CHANGED
@@ -172,6 +172,10 @@ export const TOOL_DEFS = [
172
172
  // spawn_agent is "safe" at the dispatch layer — the tools the sub-agent itself
173
173
  // runs go through their own approval inside the sub-agent loop.
174
174
  export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user']);
175
+ // Tools available in PLAN mode — read-only exploration plus delegation/questions.
176
+ // Everything that mutates the filesystem or runs commands (write_file, edit_file,
177
+ // bash, memory_write) is intentionally excluded so plan mode can only research.
178
+ export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user']);
175
179
  // ─── Display helpers ──────────────────────────────────────────────────────────
176
180
  export function formatToolArgs(name, input) {
177
181
  const p = (v) => v != null && v !== 'undefined' ? String(v) : '(missing)';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {