ikie-cli 0.1.21 → 0.1.22
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 +2 -0
- package/dist/agent.js +124 -21
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +2 -1
- package/dist/tools.d.ts +1 -0
- package/dist/tools.js +46 -1
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
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, PLAN_TOOLS, formatToolArgs, executeTool } from './tools.js';
|
|
3
|
+
import { TOOL_DEFS, SAFE_TOOLS, PLAN_TOOLS, formatToolArgs, executeTool, isRestrictedPath } from './tools.js';
|
|
4
4
|
import { renderMarkdown, extractThinkTags } from './renderer.js';
|
|
5
5
|
import { c, toolLine, permissionPrompt, toolSuccessLine, toolErrorLine, toolOutputBlock, toolDiffBlock, InlineSpinner } from './theme.js';
|
|
6
6
|
export function estimateTokens(chars) {
|
|
@@ -277,7 +277,7 @@ export class Agent {
|
|
|
277
277
|
this.activeTurnStats.toolCalls += group.length;
|
|
278
278
|
const summary = this.formatGroupSummary(group[0].name, inputs);
|
|
279
279
|
process.stdout.write(`\n${this.indent}${toolLine(`${group[0].name} ×${group.length}`, summary).trimStart()}\n`);
|
|
280
|
-
if (!opts.autoApprove && !this.config.autoApprove && !SAFE_TOOLS.has(group[0].name)) {
|
|
280
|
+
if (!opts.autoApprove && !this.config.autoApprove && !SAFE_TOOLS.has(group[0].name) && group[0].name !== 'switch_mode') {
|
|
281
281
|
const allowed = await this.checkPermission(group[0].name, inputs[0]);
|
|
282
282
|
if (!allowed) {
|
|
283
283
|
for (const tc of group) {
|
|
@@ -291,26 +291,48 @@ export class Agent {
|
|
|
291
291
|
}
|
|
292
292
|
const t0 = Date.now();
|
|
293
293
|
let errors = 0;
|
|
294
|
+
// Separate subagents so they can run in parallel; keep other tools sequential
|
|
295
|
+
// to avoid races on file mutations.
|
|
296
|
+
const spawnIndices = [];
|
|
297
|
+
const otherIndices = [];
|
|
294
298
|
for (let i = 0; i < group.length; i++) {
|
|
299
|
+
if (group[i].name === 'spawn_agent')
|
|
300
|
+
spawnIndices.push(i);
|
|
301
|
+
else
|
|
302
|
+
otherIndices.push(i);
|
|
303
|
+
}
|
|
304
|
+
const results = new Map();
|
|
305
|
+
if (spawnIndices.length > 0) {
|
|
306
|
+
const spawnResults = await Promise.all(spawnIndices.map(i => this.runSubagent(inputs[i], opts)));
|
|
307
|
+
spawnIndices.forEach((idx, i) => results.set(idx, spawnResults[i]));
|
|
308
|
+
}
|
|
309
|
+
for (const i of otherIndices) {
|
|
295
310
|
if (opts.signal?.aborted)
|
|
296
311
|
break;
|
|
297
312
|
const tc = group[i];
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (result.startsWith('Error'))
|
|
306
|
-
errors++;
|
|
307
|
-
this.recordChangedFile(tc.name, inputs[i], result);
|
|
308
|
-
this.conversation.push({ role: 'tool', tool_call_id: tc.id, content: result });
|
|
313
|
+
try {
|
|
314
|
+
if (tc.name === 'read_file' && isRestrictedPath(String(inputs[i].path ?? ''))) {
|
|
315
|
+
const allowed = await this.checkPermission('read_file', inputs[i]);
|
|
316
|
+
if (!allowed) {
|
|
317
|
+
results.set(i, `Tool execution denied by user: read_file ${inputs[i].path}`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
309
320
|
}
|
|
310
|
-
|
|
321
|
+
const result = await executeTool(tc.name, inputs[i]);
|
|
322
|
+
if (result.startsWith('Error'))
|
|
311
323
|
errors++;
|
|
312
|
-
|
|
313
|
-
|
|
324
|
+
this.recordChangedFile(tc.name, inputs[i], result);
|
|
325
|
+
results.set(i, result);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
errors++;
|
|
329
|
+
results.set(i, `Tool error: ${err}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (let i = 0; i < group.length; i++) {
|
|
333
|
+
const result = results.get(i);
|
|
334
|
+
if (result !== undefined) {
|
|
335
|
+
this.conversation.push({ role: 'tool', tool_call_id: group[i].id, content: result });
|
|
314
336
|
}
|
|
315
337
|
}
|
|
316
338
|
const ms = Date.now() - t0;
|
|
@@ -338,12 +360,19 @@ export class Agent {
|
|
|
338
360
|
let tools = this.depth >= 1
|
|
339
361
|
? TOOL_DEFS.filter(t => t.function.name !== 'spawn_agent')
|
|
340
362
|
: TOOL_DEFS;
|
|
363
|
+
// Always include switch_mode so the agent can request a mode change.
|
|
364
|
+
const switchModeTool = TOOL_DEFS.find(t => t.function.name === 'switch_mode');
|
|
341
365
|
// Plan mode: only offer read-only tools, and steer toward proposing a plan.
|
|
342
366
|
let systemContent = this.systemPrompt;
|
|
343
367
|
if (this.mode === 'plan') {
|
|
344
|
-
tools = tools.filter(t => PLAN_TOOLS.has(t.function.name));
|
|
368
|
+
tools = tools.filter(t => PLAN_TOOLS.has(t.function.name) || t.function.name === 'switch_mode');
|
|
369
|
+
if (switchModeTool && !tools.includes(switchModeTool))
|
|
370
|
+
tools.push(switchModeTool);
|
|
345
371
|
systemContent += PLAN_MODE_ADDENDUM;
|
|
346
372
|
}
|
|
373
|
+
else if (switchModeTool && !tools.includes(switchModeTool)) {
|
|
374
|
+
tools.push(switchModeTool);
|
|
375
|
+
}
|
|
347
376
|
return {
|
|
348
377
|
model: this.config.model,
|
|
349
378
|
max_tokens: this.config.maxTokens,
|
|
@@ -528,12 +557,26 @@ export class Agent {
|
|
|
528
557
|
}
|
|
529
558
|
// ── Tool execution ────────────────────────────────────────────────────────
|
|
530
559
|
async handleToolCall(name, id, input, opts) {
|
|
560
|
+
if (name === 'switch_mode') {
|
|
561
|
+
return this.handleSwitchMode(input);
|
|
562
|
+
}
|
|
531
563
|
if (name === 'spawn_agent') {
|
|
532
564
|
return this.runSubagent(input, opts);
|
|
533
565
|
}
|
|
534
566
|
if (name === 'ask_user') {
|
|
535
567
|
return this.askUser(input);
|
|
536
568
|
}
|
|
569
|
+
if (name === 'read_file') {
|
|
570
|
+
const path = String(input.path ?? '');
|
|
571
|
+
if (isRestrictedPath(path) && !opts.autoApprove && !this.config.autoApprove && !this.sessionAllowList.has('read_file')) {
|
|
572
|
+
if (this.sessionDenyList.has('read_file'))
|
|
573
|
+
return `Tool execution denied by user: read_file ${path}`;
|
|
574
|
+
process.stdout.write(`${this.indent}${c.warning('⚠')} ${c.muted('Restricted file')} ${c.white(path)} ${c.muted('— asking for permission')}\n`);
|
|
575
|
+
const allowed = await this.checkPermission('read_file', input);
|
|
576
|
+
if (!allowed)
|
|
577
|
+
return `Tool execution denied by user: read_file ${path}`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
537
580
|
if (!opts.autoApprove && !this.config.autoApprove && !SAFE_TOOLS.has(name)) {
|
|
538
581
|
const allowed = await this.checkPermission(name, input);
|
|
539
582
|
if (!allowed)
|
|
@@ -563,6 +606,63 @@ export class Agent {
|
|
|
563
606
|
return msg;
|
|
564
607
|
}
|
|
565
608
|
}
|
|
609
|
+
async handleSwitchMode(input) {
|
|
610
|
+
if (this.depth > 0) {
|
|
611
|
+
return 'Error: subagents cannot switch mode. Return your findings and let the main agent decide.';
|
|
612
|
+
}
|
|
613
|
+
const mode = input.mode;
|
|
614
|
+
const reason = (input.reason ?? '').trim();
|
|
615
|
+
if (!mode || (mode !== 'plan' && mode !== 'agent')) {
|
|
616
|
+
return 'Error: switch_mode requires mode "plan" or "agent".';
|
|
617
|
+
}
|
|
618
|
+
if (mode === this.mode) {
|
|
619
|
+
return `Already in ${mode} mode.`;
|
|
620
|
+
}
|
|
621
|
+
const allowed = await this.requestModeSwitch(mode, reason);
|
|
622
|
+
if (!allowed) {
|
|
623
|
+
return `Mode switch to ${mode} denied by user.`;
|
|
624
|
+
}
|
|
625
|
+
this.setMode(mode);
|
|
626
|
+
return `Switched to ${mode} mode.`;
|
|
627
|
+
}
|
|
628
|
+
async requestModeSwitch(mode, reason) {
|
|
629
|
+
process.stdout.write(`\n ${c.primary('◆')} ${c.white.bold('mode switch')} ${c.muted('·')} ${c.white(`to ${mode}`)}\n` +
|
|
630
|
+
` ${c.muted('reason:')} ${c.dim(reason)}\n` +
|
|
631
|
+
` ${c.muted('⎿')} ${c.success.bold('y')} ${c.muted('allow')} ${c.error.bold('n')} ${c.muted('deny')}\n` +
|
|
632
|
+
` ${c.muted('❯')} `);
|
|
633
|
+
return new Promise((resolve) => {
|
|
634
|
+
if (!process.stdin.isTTY) {
|
|
635
|
+
process.stdout.write(chalk.dim('(non-interactive, denying)\n'));
|
|
636
|
+
resolve(false);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const wasRaw = process.stdin.isRaw ?? false;
|
|
640
|
+
const savedDataListeners = process.stdin.rawListeners('data').slice();
|
|
641
|
+
const savedKeypressListeners = process.stdin.rawListeners('keypress').slice();
|
|
642
|
+
process.stdin.removeAllListeners('data');
|
|
643
|
+
process.stdin.removeAllListeners('keypress');
|
|
644
|
+
if (process.stdin.isTTY) {
|
|
645
|
+
process.stdin.setRawMode(true);
|
|
646
|
+
process.stdin.resume();
|
|
647
|
+
}
|
|
648
|
+
const onData = (data) => {
|
|
649
|
+
process.stdin.removeListener('data', onData);
|
|
650
|
+
if (process.stdin.isTTY)
|
|
651
|
+
process.stdin.setRawMode(wasRaw);
|
|
652
|
+
restoreStdinListeners(savedDataListeners, savedKeypressListeners);
|
|
653
|
+
const key = data.toString().toLowerCase();
|
|
654
|
+
if (key === 'y' || key === '\r' || key === '\n') {
|
|
655
|
+
process.stdout.write(chalk.green('y\n'));
|
|
656
|
+
resolve(true);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
process.stdout.write(chalk.red('n\n'));
|
|
660
|
+
resolve(false);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
process.stdin.on('data', onData);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
566
666
|
async askUser(input) {
|
|
567
667
|
const question = (input.question ?? '').trim();
|
|
568
668
|
if (!question)
|
|
@@ -823,14 +923,17 @@ changes they didn't ask for.
|
|
|
823
923
|
- **Trust fetched page content over snippet summaries** — snippets can be stale; the live page is authoritative.
|
|
824
924
|
- **Never state a version, date, or fact as definitive if your search results conflict** — say what
|
|
825
925
|
the most recent source says and link it.
|
|
826
|
-
- \`ask_user\`: Ask the user a clarifying question when you need more info to proceed.
|
|
827
|
-
The user's answer is returned as the tool result. Use sparingly — only when genuinely
|
|
828
|
-
unsure. Don't ask for confirmation on safe operations.
|
|
829
926
|
- \`spawn_agent\`: Delegate a self-contained subtask to a focused sub-agent. Use this
|
|
830
927
|
to parallelize or isolate work — e.g. "investigate how auth is implemented and report
|
|
831
928
|
back", or "write and run tests for module X". The sub-agent has the same tools (except
|
|
832
929
|
it cannot spawn further sub-agents) and returns a summary. Give it a clear, complete
|
|
833
|
-
\`task\` and any needed \`context\`, since it does not see this conversation
|
|
930
|
+
\`task\` and any needed \`context\`, since it does not see this conversation.
|
|
931
|
+
- \`switch_mode\`: Request permission to switch between plan and agent mode. Use when the current
|
|
932
|
+
mode is not sufficient for what you need to do next. The user must approve the switch.
|
|
933
|
+
- \`ask_user\`: Ask the user a clarifying question when you need more info to proceed.
|
|
934
|
+
The user's answer is returned as the tool result. Use sparingly — only when genuinely
|
|
935
|
+
unsure. Don't ask for confirmation on safe operations.
|
|
936
|
+
`,
|
|
834
937
|
];
|
|
835
938
|
if (projectContext)
|
|
836
939
|
parts.push(`## Project Context\n${projectContext}`);
|
package/dist/theme.d.ts
CHANGED
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.22';
|
|
7
7
|
const IKIE_BANNER = [
|
|
8
8
|
' ██╗██╗ ██╗██╗███████╗',
|
|
9
9
|
' ██║██║ ██╔╝██║██╔════╝',
|
|
@@ -354,6 +354,7 @@ function toolMeta(rawName) {
|
|
|
354
354
|
case 'search_files': return { verb: 'Search', tint: c.info };
|
|
355
355
|
case 'memory_write': return { verb: 'Remember', tint: c.secondary };
|
|
356
356
|
case 'spawn_agent': return { verb: 'Agent', tint: c.secondary };
|
|
357
|
+
case 'switch_mode': return { verb: 'SwitchMode', tint: c.warning };
|
|
357
358
|
case 'ask_user': return { verb: 'Ask', tint: c.info };
|
|
358
359
|
case 'fetch_url': return { verb: 'Fetch', tint: c.info };
|
|
359
360
|
case 'web_search': return { verb: 'WebSearch', tint: c.info };
|
package/dist/tools.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ 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
4
|
export declare const PLAN_TOOLS: Set<string>;
|
|
5
|
+
export declare function isRestrictedPath(path: string): boolean;
|
|
5
6
|
export declare function formatToolArgs(name: string, input: Record<string, unknown>): string;
|
|
6
7
|
export declare function executeTool(name: string, input: Record<string, unknown>): Promise<string>;
|
package/dist/tools.js
CHANGED
|
@@ -156,7 +156,7 @@ export const TOOL_DEFS = [
|
|
|
156
156
|
type: 'function',
|
|
157
157
|
function: {
|
|
158
158
|
name: 'spawn_agent',
|
|
159
|
-
description: 'Delegate a self-contained subtask to a focused, autonomous sub-agent that has the same tools (it cannot spawn further sub-agents). The sub-agent does NOT see the current conversation, so the task and context must be self-contained. It returns a concise summary of what it did. Use for isolating research or parallelizable chunks of work.',
|
|
159
|
+
description: 'Delegate a self-contained subtask to a focused, autonomous sub-agent that has the same tools (it cannot spawn further sub-agents). The sub-agent does NOT see the current conversation, so the task and context must be self-contained. It returns a concise summary of what it did. Use for isolating research or parallelizable chunks of work. Multiple subagents can run in parallel.',
|
|
160
160
|
parameters: {
|
|
161
161
|
type: 'object',
|
|
162
162
|
properties: {
|
|
@@ -167,6 +167,21 @@ export const TOOL_DEFS = [
|
|
|
167
167
|
},
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
|
+
{
|
|
171
|
+
type: 'function',
|
|
172
|
+
function: {
|
|
173
|
+
name: 'switch_mode',
|
|
174
|
+
description: 'Request permission to switch between plan and agent mode. Use when the current mode is not sufficient — for example, when you are in plan mode and need to make changes, or when you are in agent mode and want to step back to read-only planning. The user must approve the switch.',
|
|
175
|
+
parameters: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {
|
|
178
|
+
mode: { type: 'string', enum: ['plan', 'agent'], description: 'The mode to switch to.' },
|
|
179
|
+
reason: { type: 'string', description: 'Brief reason why the switch is needed.' },
|
|
180
|
+
},
|
|
181
|
+
required: ['mode', 'reason'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
170
185
|
{
|
|
171
186
|
type: 'function',
|
|
172
187
|
function: {
|
|
@@ -279,6 +294,35 @@ export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'gre
|
|
|
279
294
|
// Everything that mutates the filesystem or runs commands (write_file, edit_file,
|
|
280
295
|
// bash, memory_write) is intentionally excluded so plan mode can only research.
|
|
281
296
|
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch']);
|
|
297
|
+
// Paths that may contain secrets, credentials, or system configuration.
|
|
298
|
+
// Reading these requires explicit user permission even though read_file is normally safe.
|
|
299
|
+
const RESTRICTED_PATTERNS = [
|
|
300
|
+
/^\.env(\.|$)/i, // .env, .env.local, .env.production, etc.
|
|
301
|
+
/\/(\.env(\.|$)|\.env$)/i, // any directory's .env files
|
|
302
|
+
/\b\.ssh\b/i, // .ssh directories
|
|
303
|
+
/\b\.aws\b/i, // AWS credentials/config
|
|
304
|
+
/\b\.docker\b/i, // Docker config
|
|
305
|
+
/\b\.npmrc\b/i, // npm auth tokens
|
|
306
|
+
/\b\.pypirc\b/i, // PyPI credentials
|
|
307
|
+
/\b\.git-credentials\b/i, // git credentials
|
|
308
|
+
/\bid_(rsa|ed25519|ecdsa|dsa)\b/i, // SSH private keys
|
|
309
|
+
/\b.*\.pem\b/i, // PEM certs/keys
|
|
310
|
+
/\b.*\.key\b/i, // key files
|
|
311
|
+
/\b.*\.p12\b/i, /\b.*\.pfx\b/i, // PKCS#12 bundles
|
|
312
|
+
/\b\.bashrc\b/i, /\b\.zshrc\b/i, /\b\.profile\b/i, /\b\.bash_profile\b/i,
|
|
313
|
+
/\b\.netrc\b/i, /\b\.pgpass\b/i, /\b\.mylogin\.cnf\b/i,
|
|
314
|
+
/\/etc\b/i, // system config
|
|
315
|
+
/\/proc\b/i, /\/sys\b/i, // Linux system dirs
|
|
316
|
+
/\/var\/(run|spool|lib|db)\b/i, // system state
|
|
317
|
+
/\bshadow\b/i, /\bpasswd\b/i, /\bhtpasswd\b/i, /\bsudoers\b/i,
|
|
318
|
+
];
|
|
319
|
+
export function isRestrictedPath(path) {
|
|
320
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
321
|
+
const basename = normalized.split('/').pop() ?? '';
|
|
322
|
+
if (RESTRICTED_PATTERNS.some(p => p.test(basename)))
|
|
323
|
+
return true;
|
|
324
|
+
return RESTRICTED_PATTERNS.some(p => p.test(normalized));
|
|
325
|
+
}
|
|
282
326
|
// ─── Display helpers ──────────────────────────────────────────────────────────
|
|
283
327
|
export function formatToolArgs(name, input) {
|
|
284
328
|
const p = (v) => v != null && v !== 'undefined' ? String(v) : '(missing)';
|
|
@@ -779,6 +823,7 @@ function formatSearchResults(query, results) {
|
|
|
779
823
|
export async function executeTool(name, input) {
|
|
780
824
|
switch (name) {
|
|
781
825
|
case 'read_file': return readFile(input);
|
|
826
|
+
case 'switch_mode': return 'Error: switch_mode is handled by the agent, not the tool executor.';
|
|
782
827
|
case 'write_file': return writeFile(input);
|
|
783
828
|
case 'edit_file': return editFile(input);
|
|
784
829
|
case 'bash': return bash(input);
|