ikie-cli 0.1.1 → 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 +6 -0
- package/dist/agent.js +128 -30
- package/dist/attachments.js +6 -6
- package/dist/index.js +2 -1
- package/dist/repl.js +213 -35
- package/dist/theme.d.ts +3 -2
- package/dist/theme.js +52 -25
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +13 -4
- package/package.json +1 -1
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,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
import { TOOL_DEFS, SAFE_TOOLS, PLAN_TOOLS, formatToolArgs, executeTool } from './tools.js';
|
|
3
4
|
import { renderMarkdown, extractThinkTags } from './renderer.js';
|
|
4
5
|
import { c, toolLine, permissionPrompt, toolSuccessLine, toolErrorLine, InlineSpinner } from './theme.js';
|
|
5
6
|
export function estimateTokens(chars) {
|
|
@@ -61,6 +62,7 @@ export class Agent {
|
|
|
61
62
|
activeTurnStats = null;
|
|
62
63
|
activeChangedFiles = new Set();
|
|
63
64
|
lastTurnStats = { modelCalls: 0, toolCalls: 0, filesChanged: 0 };
|
|
65
|
+
mode = 'agent';
|
|
64
66
|
constructor(client, config, systemPrompt, depth = 0) {
|
|
65
67
|
this.client = client;
|
|
66
68
|
this.config = config;
|
|
@@ -80,6 +82,12 @@ export class Agent {
|
|
|
80
82
|
getLastTurnStats() {
|
|
81
83
|
return { ...this.lastTurnStats };
|
|
82
84
|
}
|
|
85
|
+
getMode() {
|
|
86
|
+
return this.mode;
|
|
87
|
+
}
|
|
88
|
+
setMode(mode) {
|
|
89
|
+
this.mode = mode;
|
|
90
|
+
}
|
|
83
91
|
async send(userMessage, opts = {}) {
|
|
84
92
|
this.activeTurnStats = { modelCalls: 0, toolCalls: 0, filesChanged: 0 };
|
|
85
93
|
this.activeChangedFiles = new Set();
|
|
@@ -169,6 +177,21 @@ export class Agent {
|
|
|
169
177
|
return {};
|
|
170
178
|
}
|
|
171
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
|
+
}
|
|
172
195
|
if (group.length === 1) {
|
|
173
196
|
if (this.activeTurnStats)
|
|
174
197
|
this.activeTurnStats.toolCalls++;
|
|
@@ -219,8 +242,8 @@ export class Agent {
|
|
|
219
242
|
}
|
|
220
243
|
const ms = Date.now() - t0;
|
|
221
244
|
const lineStr = errors === 0
|
|
222
|
-
? toolSuccessLine(ms,
|
|
223
|
-
: toolErrorLine(`${errors} of ${group.length} operations
|
|
245
|
+
? toolSuccessLine(ms, `${group.length} operations`)
|
|
246
|
+
: toolErrorLine(`${errors} of ${group.length} operations`);
|
|
224
247
|
process.stdout.write(`${this.indent}${lineStr}\n`);
|
|
225
248
|
}
|
|
226
249
|
}
|
|
@@ -239,9 +262,15 @@ export class Agent {
|
|
|
239
262
|
}
|
|
240
263
|
}
|
|
241
264
|
buildParams() {
|
|
242
|
-
|
|
265
|
+
let tools = this.depth >= 1
|
|
243
266
|
? TOOL_DEFS.filter(t => t.function.name !== 'spawn_agent')
|
|
244
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
|
+
}
|
|
245
274
|
return {
|
|
246
275
|
model: this.config.model,
|
|
247
276
|
max_tokens: this.config.maxTokens,
|
|
@@ -251,7 +280,7 @@ export class Agent {
|
|
|
251
280
|
presence_penalty: this.config.presencePenalty,
|
|
252
281
|
frequency_penalty: this.config.frequencyPenalty,
|
|
253
282
|
messages: [
|
|
254
|
-
{ role: 'system', content:
|
|
283
|
+
{ role: 'system', content: systemContent },
|
|
255
284
|
...this.conversation,
|
|
256
285
|
],
|
|
257
286
|
tools,
|
|
@@ -452,14 +481,13 @@ export class Agent {
|
|
|
452
481
|
const question = (input.question ?? '').trim();
|
|
453
482
|
if (!question)
|
|
454
483
|
return 'Error: ask_user requires a question.';
|
|
455
|
-
process.stdout.write(`\n${this.indent}${c.info('[?]')} ${c.white.bold(question)}\n` +
|
|
456
|
-
`${this.indent}${c.primary('╰─❯')} `);
|
|
457
484
|
return new Promise((resolve) => {
|
|
458
485
|
if (!process.stdin.isTTY) {
|
|
459
486
|
process.stdout.write(chalk.dim('(non-interactive, skipping)\n'));
|
|
460
487
|
resolve('(no answer — non-interactive mode)');
|
|
461
488
|
return;
|
|
462
489
|
}
|
|
490
|
+
// Save current stdin state
|
|
463
491
|
const savedDataListeners = process.stdin.rawListeners('data').slice();
|
|
464
492
|
const savedKeypressListeners = process.stdin.rawListeners('keypress').slice();
|
|
465
493
|
process.stdin.removeAllListeners('data');
|
|
@@ -467,16 +495,27 @@ export class Agent {
|
|
|
467
495
|
const wasRaw = process.stdin.isRaw ?? false;
|
|
468
496
|
if (wasRaw)
|
|
469
497
|
process.stdin.setRawMode(false);
|
|
470
|
-
|
|
471
|
-
const rl = readline.createInterface({
|
|
472
|
-
|
|
498
|
+
// Create readline interface
|
|
499
|
+
const rl = readline.createInterface({
|
|
500
|
+
input: process.stdin,
|
|
501
|
+
output: process.stdout,
|
|
502
|
+
terminal: false // Let us handle the prompt ourselves
|
|
503
|
+
});
|
|
504
|
+
// Display the question with custom formatting
|
|
505
|
+
process.stdout.write(`\n${this.indent}${c.info('[?]')} ${c.white.bold(question)}\n` +
|
|
506
|
+
`${this.indent}${c.primary('╰─❯')} `);
|
|
507
|
+
// Wait for user input
|
|
508
|
+
rl.once('line', (answer) => {
|
|
473
509
|
rl.close();
|
|
510
|
+
// Restore stdin state
|
|
474
511
|
if (wasRaw)
|
|
475
512
|
process.stdin.setRawMode(true);
|
|
476
513
|
restoreStdinListeners(savedDataListeners, savedKeypressListeners);
|
|
477
514
|
const trimmed = answer.trim();
|
|
478
515
|
resolve(trimmed || '(no answer)');
|
|
479
516
|
});
|
|
517
|
+
// Explicitly resume stdin to ensure it's listening
|
|
518
|
+
process.stdin.resume();
|
|
480
519
|
});
|
|
481
520
|
}
|
|
482
521
|
// ── Subagent ──────────────────────────────────────────────────────────────
|
|
@@ -532,12 +571,16 @@ export class Agent {
|
|
|
532
571
|
const savedKeypressListeners = process.stdin.rawListeners('keypress').slice();
|
|
533
572
|
process.stdin.removeAllListeners('data');
|
|
534
573
|
process.stdin.removeAllListeners('keypress');
|
|
535
|
-
process.stdin.
|
|
536
|
-
|
|
574
|
+
if (process.stdin.isTTY) {
|
|
575
|
+
process.stdin.setRawMode(true);
|
|
576
|
+
process.stdin.resume();
|
|
577
|
+
}
|
|
537
578
|
const onData = (data) => {
|
|
538
579
|
process.stdin.removeListener('data', onData);
|
|
539
580
|
// Restore raw mode to what it was (keeps REPL's ESC handler working)
|
|
540
|
-
process.stdin.
|
|
581
|
+
if (process.stdin.isTTY) {
|
|
582
|
+
process.stdin.setRawMode(wasRaw);
|
|
583
|
+
}
|
|
541
584
|
restoreStdinListeners(savedDataListeners, savedKeypressListeners);
|
|
542
585
|
// Only pause if nobody else was listening (no REPL ESC handler)
|
|
543
586
|
if (!savedDataListeners.length) {
|
|
@@ -572,9 +615,29 @@ Work independently — do not ask the user questions. Use your tools to gather w
|
|
|
572
615
|
need, do the work, and verify it. When finished, your FINAL message must be a concise
|
|
573
616
|
summary of what you did and any key results (paths changed, findings, answers). That
|
|
574
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.`;
|
|
575
638
|
export function buildSystemPrompt(projectContext, memoryContext) {
|
|
576
639
|
const parts = [
|
|
577
|
-
`You are Ikie, an
|
|
640
|
+
`You are Ikie, an elite agentic software engineer running in the terminal.
|
|
578
641
|
|
|
579
642
|
## Identity
|
|
580
643
|
Your name is Ikie. If asked what you are, who made you, or what model powers you,
|
|
@@ -583,28 +646,63 @@ Claude, ChatGPT, GPT, Gemini, Llama, or any other named model or company's assis
|
|
|
583
646
|
Do not speculate about your underlying model.
|
|
584
647
|
|
|
585
648
|
You help developers write, debug, understand, and refactor code. You work autonomously
|
|
586
|
-
using your tools to accomplish tasks. Be direct, concise, and
|
|
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.
|
|
652
|
+
|
|
653
|
+
## Response Formatting
|
|
654
|
+
- Use **markdown formatting** in all your responses for better readability
|
|
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.
|
|
587
661
|
|
|
588
662
|
## Working Style
|
|
589
663
|
- ALWAYS provide complete, valid arguments to tools. Never omit required fields like \`path\`.
|
|
590
|
-
- When creating files, use the FULL file path
|
|
591
|
-
- Read files before editing them
|
|
592
|
-
-
|
|
593
|
-
-
|
|
594
|
-
-
|
|
595
|
-
-
|
|
596
|
-
|
|
597
|
-
|
|
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.
|
|
598
696
|
|
|
599
697
|
## Approach
|
|
600
|
-
- Understand before acting: read the relevant files and explore the structure
|
|
601
|
-
|
|
602
|
-
- For multi-step work, form a short plan, then execute it step by step and adapt
|
|
603
|
-
|
|
604
|
-
- Verify your work: after edits, re-read the changed regions and run the build,
|
|
605
|
-
|
|
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.
|
|
606
704
|
- Delegate isolated or parallelizable investigation to \`spawn_agent\` to stay focused.
|
|
607
|
-
- Be concise: explain what you did and why in
|
|
705
|
+
- Be concise but clear: explain what you did and why in well-formatted points. Show
|
|
608
706
|
code/results rather than narrating. Use \`ask_user\` only when genuinely blocked.
|
|
609
707
|
- Never leave a task half-finished or claim a success you have not verified.
|
|
610
708
|
|
package/dist/attachments.js
CHANGED
|
@@ -127,12 +127,12 @@ function loadClipboardImageMacOS(outPath, id) {
|
|
|
127
127
|
function loadClipboardImageLinux(outPath, id) {
|
|
128
128
|
// Try xclip first (X11)
|
|
129
129
|
let result = spawnSync('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], {
|
|
130
|
-
encoding: '
|
|
130
|
+
encoding: 'buffer',
|
|
131
131
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
132
132
|
});
|
|
133
|
-
if (result.status === 0 && result.stdout) {
|
|
133
|
+
if (result.status === 0 && Buffer.isBuffer(result.stdout) && result.stdout.length > 0) {
|
|
134
134
|
try {
|
|
135
|
-
writeFileSync(outPath, result.stdout
|
|
135
|
+
writeFileSync(outPath, result.stdout);
|
|
136
136
|
if (existsSync(outPath) && statSync(outPath).size > 0) {
|
|
137
137
|
return loadImageAttachment(outPath, id);
|
|
138
138
|
}
|
|
@@ -141,12 +141,12 @@ function loadClipboardImageLinux(outPath, id) {
|
|
|
141
141
|
}
|
|
142
142
|
// Try wl-paste for Wayland
|
|
143
143
|
result = spawnSync('wl-paste', ['--type', 'image/png'], {
|
|
144
|
-
encoding: '
|
|
144
|
+
encoding: 'buffer',
|
|
145
145
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
146
146
|
});
|
|
147
|
-
if (result.status === 0 && result.stdout) {
|
|
147
|
+
if (result.status === 0 && Buffer.isBuffer(result.stdout) && result.stdout.length > 0) {
|
|
148
148
|
try {
|
|
149
|
-
writeFileSync(outPath, result.stdout
|
|
149
|
+
writeFileSync(outPath, result.stdout);
|
|
150
150
|
if (existsSync(outPath) && statSync(outPath).size > 0) {
|
|
151
151
|
return loadImageAttachment(outPath, id);
|
|
152
152
|
}
|
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 /
|
|
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,11 @@
|
|
|
1
|
-
import readline from 'readline';
|
|
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 } from './config.js';
|
|
7
|
+
import { HOME_DIR, saveConfig, DEFAULT_MODEL, FIREWORKS_BASE_URL, IKIE_HOST, isLoggedIn, getApiKey } from './config.js';
|
|
8
|
+
import { login, logout } from './auth.js';
|
|
8
9
|
import { join as pathJoin } from 'path';
|
|
9
10
|
import { deleteSession, listSessions, loadSession, normalizeSessionName, saveSession } from './session.js';
|
|
10
11
|
import { buildUserContent, formatBytes, loadClipboardImageAttachment, loadImageAttachment, hasClipboardImage } from './attachments.js';
|
|
@@ -62,10 +63,9 @@ const SLASH_CMDS = [
|
|
|
62
63
|
},
|
|
63
64
|
{
|
|
64
65
|
name: 'session',
|
|
65
|
-
desc: '
|
|
66
|
+
desc: 'Load/manage chat sessions',
|
|
66
67
|
subs: [
|
|
67
68
|
{ name: 'list', desc: 'List sessions' },
|
|
68
|
-
{ name: 'save', desc: 'Save current session' },
|
|
69
69
|
{ name: 'load', desc: 'Load a session' },
|
|
70
70
|
{ name: 'new', desc: 'Start fresh session' },
|
|
71
71
|
{ name: 'delete', desc: 'Delete a session' },
|
|
@@ -93,6 +93,12 @@ const SLASH_CMDS = [
|
|
|
93
93
|
{ name: 'theme', desc: 'Change visual theme', args: '[name]' },
|
|
94
94
|
{ name: 'rpm', desc: 'Set request limit', args: '[number]' },
|
|
95
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)' },
|
|
100
|
+
{ name: 'login', desc: 'Sign in to ikie account' },
|
|
101
|
+
{ name: 'logout', desc: 'Sign out of ikie account' },
|
|
96
102
|
{ name: 'exit', desc: 'Exit Ikie' },
|
|
97
103
|
];
|
|
98
104
|
function slashCompleter(line) {
|
|
@@ -125,9 +131,8 @@ ${c.primary.bold('Ikie Commands')}
|
|
|
125
131
|
${c.warning('/image list')} List queued images
|
|
126
132
|
${c.warning('/image clear')} Clear queued images
|
|
127
133
|
${c.warning('/session list')} List saved sessions
|
|
128
|
-
${c.warning('/session save')} Save current chat
|
|
129
134
|
${c.warning('/session load')} Load saved chat
|
|
130
|
-
${c.warning('/session new')} Start a fresh chat
|
|
135
|
+
${c.warning('/session new')} Start a fresh chat (auto-saved)
|
|
131
136
|
${c.warning('/context')} Show current project context
|
|
132
137
|
${c.warning('/models')} List available models
|
|
133
138
|
${c.warning('/model <name>')} Switch AI model
|
|
@@ -145,6 +150,12 @@ ${c.primary.bold('Ikie Commands')}
|
|
|
145
150
|
${c.warning('/theme')} Pick a theme interactively
|
|
146
151
|
${c.warning('/rpm [number]')} Show or set request limit
|
|
147
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)')}
|
|
157
|
+
${c.warning('/login')} Sign in to ikie account
|
|
158
|
+
${c.warning('/logout')} Sign out of ikie account
|
|
148
159
|
${c.warning('/exit')} Exit Ikie
|
|
149
160
|
|
|
150
161
|
${c.primary.bold('Shortcuts')}
|
|
@@ -253,7 +264,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
253
264
|
if (sub === 'list' || sub === 'ls') {
|
|
254
265
|
const sessions = listSessions();
|
|
255
266
|
if (!sessions.length) {
|
|
256
|
-
console.log(infoLine('No saved sessions yet.
|
|
267
|
+
console.log(infoLine('No saved sessions yet. Every chat is saved automatically.'));
|
|
257
268
|
return true;
|
|
258
269
|
}
|
|
259
270
|
console.log(`\n${c.primary.bold('Sessions')}`);
|
|
@@ -263,13 +274,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
263
274
|
const model = s.model ? ` ${c.muted(s.model.split('/').pop() ?? s.model)}` : '';
|
|
264
275
|
console.log(` ${active} ${c.secondary(s.name.padEnd(24))} ${c.muted(String(s.messageCount).padStart(3) + ' msgs')} ${c.dim(when)}${model}`);
|
|
265
276
|
}
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
if (sub === 'save') {
|
|
269
|
-
const name = normalizeSessionName(nameArg || sessionState.activeName);
|
|
270
|
-
const saved = saveSession(name, config.model, agent.getConversation());
|
|
271
|
-
sessionState.activeName = saved.name;
|
|
272
|
-
console.log(successLine(`Session saved as "${saved.name}".`));
|
|
277
|
+
console.log(`\n ${c.muted('Resume with')} ${c.warning('/session load <name>')}\n`);
|
|
273
278
|
return true;
|
|
274
279
|
}
|
|
275
280
|
if (sub === 'load') {
|
|
@@ -292,8 +297,8 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
292
297
|
}
|
|
293
298
|
if (sub === 'new') {
|
|
294
299
|
agent.clearConversation();
|
|
295
|
-
sessionState.activeName = nameArg ? normalizeSessionName(nameArg) :
|
|
296
|
-
console.log(successLine(
|
|
300
|
+
sessionState.activeName = nameArg ? normalizeSessionName(nameArg) : normalizeSessionName();
|
|
301
|
+
console.log(successLine(`Started new session "${sessionState.activeName}" (auto-saved).`));
|
|
297
302
|
return true;
|
|
298
303
|
}
|
|
299
304
|
if (sub === 'delete' || sub === 'rm') {
|
|
@@ -313,7 +318,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
313
318
|
}
|
|
314
319
|
return true;
|
|
315
320
|
}
|
|
316
|
-
console.log(errorLine('Usage: /session list|
|
|
321
|
+
console.log(errorLine('Usage: /session list|load <name>|new [name]|delete <name>'));
|
|
317
322
|
return true;
|
|
318
323
|
}
|
|
319
324
|
case 'context':
|
|
@@ -340,6 +345,19 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
340
345
|
}
|
|
341
346
|
return true;
|
|
342
347
|
}
|
|
348
|
+
case 'login': {
|
|
349
|
+
await login();
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
case 'logout': {
|
|
353
|
+
if (!isLoggedIn(config)) {
|
|
354
|
+
console.log(infoLine('Not signed in.'));
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
logout();
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
343
361
|
case 'settings': {
|
|
344
362
|
const sub = (args[0] ?? 'show').toLowerCase();
|
|
345
363
|
const value = args.slice(1).join(' ').trim();
|
|
@@ -512,6 +530,56 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
512
530
|
console.log(infoLine(`~${Math.round(approxTokens).toLocaleString()} tokens in conversation`));
|
|
513
531
|
return true;
|
|
514
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
|
+
}
|
|
515
583
|
case 'exit':
|
|
516
584
|
case 'quit':
|
|
517
585
|
case 'q':
|
|
@@ -522,6 +590,43 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
|
|
|
522
590
|
return true;
|
|
523
591
|
}
|
|
524
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
|
+
}
|
|
525
630
|
function promptLine(prompt) {
|
|
526
631
|
return new Promise((resolve) => {
|
|
527
632
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -531,6 +636,30 @@ function promptLine(prompt) {
|
|
|
531
636
|
});
|
|
532
637
|
});
|
|
533
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
|
+
}
|
|
534
663
|
function printGoodbye(sessionState, agent, config) {
|
|
535
664
|
const msgs = agent.getConversation();
|
|
536
665
|
const hasConversation = msgs.length > 0;
|
|
@@ -544,15 +673,11 @@ function printGoodbye(sessionState, agent, config) {
|
|
|
544
673
|
}
|
|
545
674
|
}
|
|
546
675
|
console.log(`\n${c.primary.bold('Goodbye!')}\n`);
|
|
547
|
-
if (sessionState.activeName) {
|
|
548
|
-
console.log(` ${c.muted('Your session')} ${c.secondary.bold(sessionState.activeName)} ${c.muted('
|
|
549
|
-
console.log(`\n ${c.muted('To
|
|
550
|
-
console.log(` ${c.accent('ikie')} ${c.warning(`/session load ${sessionState.activeName}`)}
|
|
551
|
-
|
|
552
|
-
else if (hasConversation) {
|
|
553
|
-
console.log(` ${c.warning('\u26A0')} ${c.muted('Your current conversation was not saved.')}`);
|
|
554
|
-
console.log(`\n ${c.muted('To save next time:')}`);
|
|
555
|
-
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`);
|
|
556
681
|
}
|
|
557
682
|
console.log(` ${c.muted('Happy coding!')}\n`);
|
|
558
683
|
}
|
|
@@ -659,7 +784,11 @@ function selectThemeInteractively(rl, config) {
|
|
|
659
784
|
}
|
|
660
785
|
export async function startREPL(agent, config, projectContext, oneShot) {
|
|
661
786
|
void HISTORY_FILE;
|
|
662
|
-
|
|
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) {
|
|
663
792
|
try {
|
|
664
793
|
await agent.send(oneShot, { autoApprove: config.autoApprove });
|
|
665
794
|
}
|
|
@@ -681,7 +810,9 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
681
810
|
let multilineBuffer = '';
|
|
682
811
|
let busy = false;
|
|
683
812
|
let ctrlCCount = 0;
|
|
684
|
-
|
|
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() };
|
|
685
816
|
const imageState = { nextId: 1, pending: [] };
|
|
686
817
|
let pasteSeq = 0;
|
|
687
818
|
let pasteMode = false;
|
|
@@ -713,6 +844,18 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
713
844
|
};
|
|
714
845
|
const routeInputData = (data) => {
|
|
715
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
|
+
}
|
|
716
859
|
// Check for Ctrl+V (0x16) - try to paste image from clipboard
|
|
717
860
|
if (data.length === 1 && data[0] === 0x16) {
|
|
718
861
|
// Check if clipboard has an image
|
|
@@ -738,7 +881,8 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
738
881
|
return;
|
|
739
882
|
}
|
|
740
883
|
if (!pasteMode && !text.includes('\x1b[200~') && /[\r\n]/.test(text.trim()) && text.length > 3) {
|
|
741
|
-
|
|
884
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
885
|
+
rl.write(normalized);
|
|
742
886
|
return;
|
|
743
887
|
}
|
|
744
888
|
let rest = text;
|
|
@@ -750,7 +894,8 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
750
894
|
return;
|
|
751
895
|
}
|
|
752
896
|
pasteBuffer += rest.slice(0, end);
|
|
753
|
-
|
|
897
|
+
const normalized = pasteBuffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
898
|
+
rl.write(normalized);
|
|
754
899
|
pasteBuffer = '';
|
|
755
900
|
pasteMode = false;
|
|
756
901
|
rest = rest.slice(end + '\x1b[201~'.length);
|
|
@@ -793,7 +938,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
793
938
|
rl.setPrompt(CONTINUE_PROMPT);
|
|
794
939
|
}
|
|
795
940
|
else {
|
|
796
|
-
printPromptHeader();
|
|
941
|
+
printPromptHeader(agent.getMode());
|
|
797
942
|
if (imageState.pending.length) {
|
|
798
943
|
const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
|
|
799
944
|
process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
|
|
@@ -863,8 +1008,10 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
863
1008
|
const userContent = buildUserContent(agentInput, imagesForTurn);
|
|
864
1009
|
imageState.pending = [];
|
|
865
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).
|
|
866
1014
|
process.stdout.write('\r\x1b[2K');
|
|
867
|
-
process.stdout.write(`${c.muted(' Working. Press Esc to interrupt.')}\n`);
|
|
868
1015
|
busy = true;
|
|
869
1016
|
rl.pause();
|
|
870
1017
|
const taskStartedAt = Date.now();
|
|
@@ -906,6 +1053,24 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
906
1053
|
if (sessionState.activeName && !abortController.signal.aborted) {
|
|
907
1054
|
saveSession(sessionState.activeName, config.model, agent.getConversation());
|
|
908
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
|
+
}
|
|
909
1074
|
}
|
|
910
1075
|
catch (err) {
|
|
911
1076
|
if (!abortController.signal.aborted) {
|
|
@@ -924,13 +1089,16 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
924
1089
|
}
|
|
925
1090
|
const elapsed = formatElapsed(Date.now() - taskStartedAt);
|
|
926
1091
|
process.stdin.removeListener('data', cancelHandler);
|
|
927
|
-
if (process.stdin.isTTY)
|
|
928
|
-
process.stdin.setRawMode(false);
|
|
929
1092
|
process.stdin.pause();
|
|
930
1093
|
restoreStdinListeners(savedDataListeners, savedKeypressListeners);
|
|
931
1094
|
busy = false;
|
|
932
1095
|
ctrlCCount = 0;
|
|
933
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);
|
|
934
1102
|
const status = abortController.signal.aborted
|
|
935
1103
|
? c.warning(formatTaskTimeline(agent, elapsed, 'cancelled'))
|
|
936
1104
|
: taskFailed
|
|
@@ -944,5 +1112,15 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
944
1112
|
rl.on('line', (raw) => {
|
|
945
1113
|
chain = chain.then(() => handleLine(raw));
|
|
946
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
|
+
}
|
|
947
1125
|
showPrompt();
|
|
948
1126
|
}
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.1";
|
|
2
2
|
export interface Theme {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
@@ -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
|
|
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
|
@@ -3,7 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import { join as pathJoin, basename } from 'path';
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
5
|
import { loadConfig, saveConfig } from './config.js';
|
|
6
|
-
export const VERSION = '0.1.
|
|
6
|
+
export const VERSION = '0.1.1';
|
|
7
7
|
const IKIE_BANNER = [
|
|
8
8
|
' ██╗██╗ ██╗██╗███████╗',
|
|
9
9
|
' ██║██║ ██╔╝██║██╔════╝',
|
|
@@ -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(
|
|
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] ? `
|
|
256
|
-
|
|
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(
|
|
258
|
+
process.stdout.write('\n');
|
|
260
259
|
}
|
|
261
|
-
export function
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
340
|
-
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
` ${c.
|
|
361
|
-
`${c.
|
|
362
|
-
`${c.
|
|
363
|
-
`${c.
|
|
364
|
-
|
|
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)';
|
|
@@ -451,15 +455,20 @@ function globToRegExp(pattern) {
|
|
|
451
455
|
.replace(/\u0000/g, '.*');
|
|
452
456
|
return new RegExp(`(^|/)${escaped}$`, 'i');
|
|
453
457
|
}
|
|
454
|
-
function memoryWrite(input) {
|
|
458
|
+
async function memoryWrite(input) {
|
|
455
459
|
const scope = input.scope ?? 'project';
|
|
456
|
-
|
|
460
|
+
try {
|
|
461
|
+
const { appendProjectMemory, appendGlobalMemory } = await import('./memory.js');
|
|
457
462
|
if (scope === 'global')
|
|
458
463
|
appendGlobalMemory(input.content);
|
|
459
464
|
else
|
|
460
465
|
appendProjectMemory(input.content);
|
|
461
|
-
|
|
462
|
-
|
|
466
|
+
return `Saved to ${scope} memory.`;
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
470
|
+
return `Error saving to ${scope} memory: ${msg}`;
|
|
471
|
+
}
|
|
463
472
|
}
|
|
464
473
|
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
|
465
474
|
export async function executeTool(name, input) {
|