ikie-cli 0.1.39 → 0.1.41
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 +9 -11
- package/dist/agent.js +138 -181
- package/dist/config.js +1 -1
- package/dist/index.js +2 -1
- package/dist/onboarding.js +1 -1
- package/dist/repl.js +67 -9
- package/dist/session-manager.d.ts +29 -0
- package/dist/session-manager.js +145 -0
- package/dist/theme.d.ts +4 -1
- package/dist/theme.js +37 -5
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +249 -68
- package/package.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -5,7 +5,130 @@ import { promisify } from 'util';
|
|
|
5
5
|
import { glob } from 'glob';
|
|
6
6
|
import { getMcpManager, parseClaudeMcpAddCommand } from './mcp-manager.js';
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
|
-
|
|
8
|
+
const sessions = new Map();
|
|
9
|
+
let sidAcc = 0;
|
|
10
|
+
const sessionId = () => `s_${Date.now().toString(36)}_${(++sidAcc).toString(36)}`;
|
|
11
|
+
function closeSession(id) {
|
|
12
|
+
const s = sessions.get(id);
|
|
13
|
+
if (s && !s.done) {
|
|
14
|
+
s.done = true;
|
|
15
|
+
try {
|
|
16
|
+
s.child.kill();
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
}
|
|
20
|
+
sessions.delete(id);
|
|
21
|
+
}
|
|
22
|
+
// GC stale sessions every 30s
|
|
23
|
+
setInterval(() => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [id, s] of sessions)
|
|
26
|
+
if (s.done || now - s.at > 300_000)
|
|
27
|
+
closeSession(id);
|
|
28
|
+
}, 30_000).unref();
|
|
29
|
+
/**
|
|
30
|
+
* Spawn a command with pipes. If it exits before `graceMs`, resolve normally
|
|
31
|
+
* (no session). If it stays alive, stash the session and return a tagged
|
|
32
|
+
* result so the agent can continue sending input.
|
|
33
|
+
*/
|
|
34
|
+
async function trySession(command, cwd, graceMs, signal) {
|
|
35
|
+
const isWin = process.platform === 'win32';
|
|
36
|
+
const child = spawn(isWin ? 'cmd.exe' : 'bash', isWin ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
37
|
+
let out = '';
|
|
38
|
+
let exited = false;
|
|
39
|
+
let exitCode = null;
|
|
40
|
+
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
41
|
+
child.stderr.on('data', (d) => { out += d.toString(); });
|
|
42
|
+
const exitPromise = new Promise(r => {
|
|
43
|
+
child.on('exit', (c) => { exited = true; exitCode = c; r(); });
|
|
44
|
+
child.on('error', () => { exited = true; exitCode = 1; r(); });
|
|
45
|
+
});
|
|
46
|
+
// Abort during initial wait kills the child.
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
child.kill();
|
|
49
|
+
return 'Interrupted.';
|
|
50
|
+
}
|
|
51
|
+
const abortKill = () => { try {
|
|
52
|
+
child.kill();
|
|
53
|
+
}
|
|
54
|
+
catch { } };
|
|
55
|
+
if (signal)
|
|
56
|
+
signal.addEventListener('abort', abortKill, { once: true });
|
|
57
|
+
// Wait grace period — if the process finishes, return output directly.
|
|
58
|
+
await Promise.race([
|
|
59
|
+
exitPromise,
|
|
60
|
+
new Promise(r => setTimeout(r, graceMs)),
|
|
61
|
+
]);
|
|
62
|
+
if (signal)
|
|
63
|
+
signal.removeEventListener('abort', abortKill);
|
|
64
|
+
if (exited) {
|
|
65
|
+
const parts = [];
|
|
66
|
+
if (out.trim())
|
|
67
|
+
parts.push(out.trim());
|
|
68
|
+
const output = parts.join('\n') || '(no output)';
|
|
69
|
+
return exitCode === 0 ? output : `Exit ${exitCode ?? 1}\n${output}`;
|
|
70
|
+
}
|
|
71
|
+
// Still alive — create a session.
|
|
72
|
+
const id = sessionId();
|
|
73
|
+
sessions.set(id, { child, lastOutput: out, at: Date.now(), done: false });
|
|
74
|
+
// Also kill the session on abort.
|
|
75
|
+
if (signal) {
|
|
76
|
+
signal.addEventListener('abort', () => closeSession(id), { once: true });
|
|
77
|
+
}
|
|
78
|
+
// Continue accumulating output into lastOutput.
|
|
79
|
+
child.stdout.on('data', (d) => {
|
|
80
|
+
const s = sessions.get(id);
|
|
81
|
+
if (s)
|
|
82
|
+
s.lastOutput += d.toString();
|
|
83
|
+
});
|
|
84
|
+
child.stderr.on('data', (d) => {
|
|
85
|
+
const s = sessions.get(id);
|
|
86
|
+
if (s)
|
|
87
|
+
s.lastOutput += d.toString();
|
|
88
|
+
});
|
|
89
|
+
child.on('exit', () => {
|
|
90
|
+
const s = sessions.get(id);
|
|
91
|
+
if (s)
|
|
92
|
+
s.done = true;
|
|
93
|
+
});
|
|
94
|
+
return `[session ${id}]\n${out.trimEnd() || '(awaiting input)'}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Send input to an active session and wait up to `waitMs` for more output.
|
|
98
|
+
* Returns new output accumulated since the last call.
|
|
99
|
+
*/
|
|
100
|
+
async function continueSession(id, stdin, waitMs, signal) {
|
|
101
|
+
const s = sessions.get(id);
|
|
102
|
+
if (!s)
|
|
103
|
+
return '[session not found]';
|
|
104
|
+
if (s.done)
|
|
105
|
+
return s.lastOutput || '(process already exited)';
|
|
106
|
+
const beforeLen = s.lastOutput.length;
|
|
107
|
+
s.child.stdin.write(stdin);
|
|
108
|
+
s.child.stdin.write('\n');
|
|
109
|
+
// Abort mid-wait → kill the session.
|
|
110
|
+
if (signal?.aborted) {
|
|
111
|
+
closeSession(id);
|
|
112
|
+
return 'Interrupted.';
|
|
113
|
+
}
|
|
114
|
+
const abortKill = () => closeSession(id);
|
|
115
|
+
if (signal)
|
|
116
|
+
signal.addEventListener('abort', abortKill, { once: true });
|
|
117
|
+
// Wait for exit or timeout and collect whatever output came in.
|
|
118
|
+
await Promise.race([
|
|
119
|
+
new Promise(r => s.child.once('exit', () => r())),
|
|
120
|
+
new Promise(r => setTimeout(r, waitMs)),
|
|
121
|
+
]);
|
|
122
|
+
if (signal)
|
|
123
|
+
signal.removeEventListener('abort', abortKill);
|
|
124
|
+
s.at = Date.now();
|
|
125
|
+
const newPart = s.lastOutput.slice(beforeLen).trim();
|
|
126
|
+
if (s.done) {
|
|
127
|
+
closeSession(id);
|
|
128
|
+
return newPart || '(process exited)';
|
|
129
|
+
}
|
|
130
|
+
return newPart || '(no new output)';
|
|
131
|
+
}
|
|
9
132
|
export const TOOL_DEFS = [
|
|
10
133
|
{
|
|
11
134
|
type: 'function',
|
|
@@ -62,12 +185,15 @@ export const TOOL_DEFS = [
|
|
|
62
185
|
parameters: {
|
|
63
186
|
type: 'object',
|
|
64
187
|
properties: {
|
|
65
|
-
command: { type: 'string', description: 'Shell command' },
|
|
188
|
+
command: { type: 'string', description: 'Shell command. Omit when continuing an interactive session (provide session_id instead).' },
|
|
66
189
|
timeout_ms: { type: 'number', description: 'Timeout ms (default 30000)' },
|
|
67
190
|
cwd: { type: 'string', description: 'Working directory' },
|
|
68
191
|
interactive: { type: 'boolean', description: 'Set true for commands that need an interactive terminal — ones that show arrow-key menus or prompts that cannot be skipped with flags (e.g. `shadcn init`, `create-next-app`, `npm init` without -y). The command is connected to the real terminal so the USER answers the prompts directly. Output is shown live, not captured, so the result only reports the exit status — read any files it creates afterward.' },
|
|
192
|
+
allow_interaction: { type: 'boolean', description: 'Set true to let the AGENT handle interactive prompts programmatically. If the command prompts for input (stays alive >2s), a session is created — the result includes a session_id and the prompt text. Send input by calling bash with the same session_id + stdin. If the command runs to completion without prompting, it returns normally with no session.' },
|
|
193
|
+
session_id: { type: 'string', description: 'Session ID from an `allow_interaction` bash call. Provide to continue answering prompts — set `stdin` to send input, or omit `stdin` to poll for more output.' },
|
|
194
|
+
stdin: { type: 'string', description: 'Input to send to an active session\'s stdin. Only valid when `session_id` is set. The input is sent followed by a newline.' },
|
|
69
195
|
},
|
|
70
|
-
required: [
|
|
196
|
+
required: [],
|
|
71
197
|
},
|
|
72
198
|
},
|
|
73
199
|
},
|
|
@@ -154,21 +280,6 @@ export const TOOL_DEFS = [
|
|
|
154
280
|
},
|
|
155
281
|
},
|
|
156
282
|
},
|
|
157
|
-
{
|
|
158
|
-
type: 'function',
|
|
159
|
-
function: {
|
|
160
|
-
name: 'spawn_agent',
|
|
161
|
-
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.',
|
|
162
|
-
parameters: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: {
|
|
165
|
-
task: { type: 'string', description: 'A clear, complete description of the task for the sub-agent to accomplish.' },
|
|
166
|
-
context: { type: 'string', description: 'Optional background the sub-agent needs (file paths, constraints, prior findings).' },
|
|
167
|
-
},
|
|
168
|
-
required: ['task'],
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
283
|
{
|
|
173
284
|
type: 'function',
|
|
174
285
|
function: {
|
|
@@ -365,14 +476,12 @@ export function getMcpToolDefs() {
|
|
|
365
476
|
return getMcpManager().getToolDefsSync();
|
|
366
477
|
}
|
|
367
478
|
// ─── Safe tools (auto-approved) ───────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list']);
|
|
371
|
-
// Tools available in PLAN mode — read-only exploration plus delegation/questions.
|
|
479
|
+
export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list']);
|
|
480
|
+
// Tools available in PLAN mode — read-only exploration plus questions.
|
|
372
481
|
// Everything that mutates the filesystem or runs commands (write_file, edit_file,
|
|
373
482
|
// bash, memory_write) is intentionally excluded so plan mode can only research.
|
|
374
483
|
// MCP tools are also excluded because we cannot prove they are read-only.
|
|
375
|
-
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', '
|
|
484
|
+
export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list']);
|
|
376
485
|
// Paths that may contain secrets, credentials, or system configuration.
|
|
377
486
|
// Reading these requires explicit user permission even though read_file is normally safe.
|
|
378
487
|
const RESTRICTED_PATTERNS = [
|
|
@@ -428,10 +537,6 @@ export function formatToolArgs(name, input) {
|
|
|
428
537
|
const q = String(input.question ?? '');
|
|
429
538
|
return `"${q.length > 56 ? q.slice(0, 56) + '…' : q}"`;
|
|
430
539
|
}
|
|
431
|
-
case 'spawn_agent': {
|
|
432
|
-
const task = String(input.task ?? '');
|
|
433
|
-
return `"${task.length > 56 ? task.slice(0, 56) + '…' : task}"`;
|
|
434
|
-
}
|
|
435
540
|
case 'git_status':
|
|
436
541
|
return input.path ? `"${p(input.path)}"` : '(cwd)';
|
|
437
542
|
case 'git_diff':
|
|
@@ -558,14 +663,24 @@ function editFile(input) {
|
|
|
558
663
|
return `Error: ${sanitizeError(e)}`;
|
|
559
664
|
}
|
|
560
665
|
}
|
|
561
|
-
async function bash(input) {
|
|
666
|
+
async function bash(input, signal) {
|
|
562
667
|
let cwd = process.cwd();
|
|
563
668
|
if (input.cwd) {
|
|
564
669
|
cwd = resolve(input.cwd);
|
|
565
670
|
}
|
|
566
|
-
|
|
671
|
+
// Session continuation: send stdin to an active session.
|
|
672
|
+
if (input.session_id) {
|
|
673
|
+
return continueSession(input.session_id, input.stdin ?? '', 5000, signal);
|
|
674
|
+
}
|
|
675
|
+
const command = (input.command ?? '').trim();
|
|
676
|
+
if (!command)
|
|
677
|
+
return 'Error: command is required for bash';
|
|
567
678
|
if (input.interactive) {
|
|
568
|
-
return bashInteractive(command, cwd);
|
|
679
|
+
return bashInteractive(command, cwd, signal);
|
|
680
|
+
}
|
|
681
|
+
// Agent-controlled interactive session.
|
|
682
|
+
if (input.allow_interaction) {
|
|
683
|
+
return trySession(command, cwd, 2500, signal);
|
|
569
684
|
}
|
|
570
685
|
if (command.endsWith('&')) {
|
|
571
686
|
const bgCmd = command.slice(0, -1).trim();
|
|
@@ -592,34 +707,48 @@ async function bash(input) {
|
|
|
592
707
|
// For build commands or when stream is enabled, use streaming output
|
|
593
708
|
const shouldStream = input.stream || /\b(build|compile|test|deploy|install)\b/i.test(command);
|
|
594
709
|
if (shouldStream) {
|
|
595
|
-
return bashStreaming(command, cwd, timeout);
|
|
710
|
+
return bashStreaming(command, cwd, timeout, signal);
|
|
596
711
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
712
|
+
return bashSpawn(command, cwd, timeout, signal);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Spawn a command, capture output, and kill it on abort signal.
|
|
716
|
+
*/
|
|
717
|
+
function bashSpawn(command, cwd, timeout, signal) {
|
|
718
|
+
return new Promise((resolve) => {
|
|
719
|
+
const isWindows = process.platform === 'win32';
|
|
720
|
+
const child = spawn(isWindows ? 'cmd.exe' : 'bash', isWindows ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, timeout });
|
|
721
|
+
let stdout = '';
|
|
722
|
+
let stderr = '';
|
|
723
|
+
child.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
724
|
+
child.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
725
|
+
const kill = () => { try {
|
|
726
|
+
child.kill();
|
|
727
|
+
}
|
|
728
|
+
catch { /* ignore */ } };
|
|
729
|
+
if (signal) {
|
|
730
|
+
if (signal.aborted) {
|
|
731
|
+
kill();
|
|
732
|
+
resolve('Interrupted.');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
signal.addEventListener('abort', kill, { once: true });
|
|
736
|
+
}
|
|
737
|
+
child.on('error', () => { });
|
|
738
|
+
child.on('exit', (code) => {
|
|
739
|
+
if (signal)
|
|
740
|
+
signal.removeEventListener('abort', kill);
|
|
741
|
+
const parts = [];
|
|
742
|
+
if (stdout.trim())
|
|
743
|
+
parts.push(stdout.trim());
|
|
744
|
+
if (stderr.trim())
|
|
745
|
+
parts.push(`[stderr]\n${stderr.trim()}`);
|
|
746
|
+
const output = parts.join('\n') || '(no output)';
|
|
747
|
+
resolve(code === 0 ? output : `Exit ${code ?? 1}\n${output}`);
|
|
602
748
|
});
|
|
603
|
-
|
|
604
|
-
if (stdout.trim())
|
|
605
|
-
parts.push(stdout.trim());
|
|
606
|
-
if (stderr.trim())
|
|
607
|
-
parts.push(`[stderr]\n${stderr.trim()}`);
|
|
608
|
-
return parts.join('\n') || '(no output)';
|
|
609
|
-
}
|
|
610
|
-
catch (err) {
|
|
611
|
-
const e = err;
|
|
612
|
-
const parts = [];
|
|
613
|
-
if (e.stdout?.trim())
|
|
614
|
-
parts.push(e.stdout.trim());
|
|
615
|
-
if (e.stderr?.trim())
|
|
616
|
-
parts.push(`[stderr]\n${e.stderr.trim()}`);
|
|
617
|
-
if (!parts.length)
|
|
618
|
-
parts.push(sanitizeError(e.message ?? 'Command failed'));
|
|
619
|
-
return `Exit ${e.code ?? 1}\n${parts.join('\n')}`;
|
|
620
|
-
}
|
|
749
|
+
});
|
|
621
750
|
}
|
|
622
|
-
function bashStreaming(command, cwd, timeout) {
|
|
751
|
+
function bashStreaming(command, cwd, timeout, signal) {
|
|
623
752
|
return new Promise((resolve, reject) => {
|
|
624
753
|
const isWindows = process.platform === 'win32';
|
|
625
754
|
const child = spawn(isWindows ? 'cmd.exe' : 'bash', isWindows ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, timeout });
|
|
@@ -629,8 +758,25 @@ function bashStreaming(command, cwd, timeout) {
|
|
|
629
758
|
const MAX_LINES_SHOWN = 3;
|
|
630
759
|
let linesShown = 0;
|
|
631
760
|
let totalLines = 0;
|
|
761
|
+
const kill = () => { try {
|
|
762
|
+
child.kill();
|
|
763
|
+
}
|
|
764
|
+
catch { /* ignore */ } };
|
|
765
|
+
if (signal) {
|
|
766
|
+
if (signal.aborted) {
|
|
767
|
+
kill();
|
|
768
|
+
resolve('Interrupted.');
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
signal.addEventListener('abort', kill, { once: true });
|
|
772
|
+
}
|
|
773
|
+
const cleanup = () => { if (signal)
|
|
774
|
+
signal.removeEventListener('abort', kill); };
|
|
632
775
|
// Print a blank line before streaming output
|
|
633
|
-
|
|
776
|
+
const print = (text) => {
|
|
777
|
+
process.stdout.write(text);
|
|
778
|
+
};
|
|
779
|
+
print('\n');
|
|
634
780
|
// Stream stdout line by line - show only first few lines
|
|
635
781
|
child.stdout?.on('data', (data) => {
|
|
636
782
|
const text = data.toString();
|
|
@@ -639,7 +785,7 @@ function bashStreaming(command, cwd, timeout) {
|
|
|
639
785
|
for (const line of lines) {
|
|
640
786
|
totalLines++;
|
|
641
787
|
if (linesShown < MAX_LINES_SHOWN) {
|
|
642
|
-
|
|
788
|
+
print(`${indent}${line}\n`);
|
|
643
789
|
linesShown++;
|
|
644
790
|
}
|
|
645
791
|
}
|
|
@@ -651,19 +797,21 @@ function bashStreaming(command, cwd, timeout) {
|
|
|
651
797
|
for (const line of lines) {
|
|
652
798
|
totalLines++;
|
|
653
799
|
if (linesShown < MAX_LINES_SHOWN) {
|
|
654
|
-
|
|
800
|
+
print(`${indent}${line}\n`);
|
|
655
801
|
linesShown++;
|
|
656
802
|
}
|
|
657
803
|
}
|
|
658
804
|
});
|
|
659
805
|
child.on('error', (err) => {
|
|
806
|
+
cleanup();
|
|
660
807
|
reject(new Error(`Failed to execute command: ${sanitizeError(err)}`));
|
|
661
808
|
});
|
|
662
809
|
child.on('exit', (code) => {
|
|
810
|
+
cleanup();
|
|
663
811
|
// Show truncation message if there are hidden lines
|
|
664
812
|
const hiddenLines = totalLines - linesShown;
|
|
665
813
|
if (hiddenLines > 0) {
|
|
666
|
-
|
|
814
|
+
print(`${indent}... +${hiddenLines} lines\n`);
|
|
667
815
|
}
|
|
668
816
|
const parts = [];
|
|
669
817
|
if (stdout.trim())
|
|
@@ -688,7 +836,7 @@ function bashStreaming(command, cwd, timeout) {
|
|
|
688
836
|
* listeners and raw mode while the child owns the terminal, then restores them.
|
|
689
837
|
* Output is not captured — only the exit status is reported back to the agent.
|
|
690
838
|
*/
|
|
691
|
-
function bashInteractive(command, cwd) {
|
|
839
|
+
function bashInteractive(command, cwd, signal) {
|
|
692
840
|
return new Promise((resolvePromise) => {
|
|
693
841
|
const stdin = process.stdin;
|
|
694
842
|
const isTTY = Boolean(stdin.isTTY);
|
|
@@ -719,14 +867,29 @@ function bashInteractive(command, cwd) {
|
|
|
719
867
|
process.stdout.write('\x1b[?2004l'); // disable bracketed paste for the child
|
|
720
868
|
}
|
|
721
869
|
process.stdout.write('\n');
|
|
870
|
+
let child;
|
|
871
|
+
const kill = () => { try {
|
|
872
|
+
child?.kill();
|
|
873
|
+
}
|
|
874
|
+
catch { /* ignore */ } };
|
|
875
|
+
if (signal) {
|
|
876
|
+
if (signal.aborted) {
|
|
877
|
+
restore();
|
|
878
|
+
resolvePromise('Interrupted.');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
signal.addEventListener('abort', () => { kill(); restore(); }, { once: true });
|
|
882
|
+
}
|
|
722
883
|
try {
|
|
723
884
|
const isWindows = process.platform === 'win32';
|
|
724
|
-
|
|
885
|
+
child = spawn(isWindows ? 'cmd.exe' : 'bash', isWindows ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, stdio: 'inherit' });
|
|
725
886
|
child.on('error', (err) => {
|
|
887
|
+
signal?.removeEventListener('abort', kill);
|
|
726
888
|
restore();
|
|
727
889
|
resolvePromise(`Error running interactive command: ${sanitizeError(err)}`);
|
|
728
890
|
});
|
|
729
891
|
child.on('exit', (code) => {
|
|
892
|
+
signal?.removeEventListener('abort', kill);
|
|
730
893
|
restore();
|
|
731
894
|
resolvePromise(code === 0
|
|
732
895
|
? '(interactive command completed — output was shown to the user; read any created/changed files to see the result)'
|
|
@@ -734,6 +897,7 @@ function bashInteractive(command, cwd) {
|
|
|
734
897
|
});
|
|
735
898
|
}
|
|
736
899
|
catch (err) {
|
|
900
|
+
signal?.removeEventListener('abort', kill);
|
|
737
901
|
restore();
|
|
738
902
|
resolvePromise(`Error running interactive command: ${sanitizeError(err)}`);
|
|
739
903
|
}
|
|
@@ -1005,7 +1169,7 @@ function htmlToText(html) {
|
|
|
1005
1169
|
.replace(/\n{3,}/g, '\n\n')
|
|
1006
1170
|
.trim();
|
|
1007
1171
|
}
|
|
1008
|
-
async function fetchUrl(input) {
|
|
1172
|
+
async function fetchUrl(input, signal) {
|
|
1009
1173
|
let url = (input.url ?? '').trim();
|
|
1010
1174
|
if (!url || url === 'undefined')
|
|
1011
1175
|
return 'Error: url is required for fetch_url';
|
|
@@ -1014,6 +1178,13 @@ async function fetchUrl(input) {
|
|
|
1014
1178
|
const maxChars = input.max_chars && input.max_chars > 0 ? input.max_chars : 10000;
|
|
1015
1179
|
const controller = new AbortController();
|
|
1016
1180
|
const timer = setTimeout(() => controller.abort(), 15000);
|
|
1181
|
+
if (signal) {
|
|
1182
|
+
if (signal.aborted) {
|
|
1183
|
+
clearTimeout(timer);
|
|
1184
|
+
return 'Interrupted.';
|
|
1185
|
+
}
|
|
1186
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1187
|
+
}
|
|
1017
1188
|
try {
|
|
1018
1189
|
const res = await fetch(url, {
|
|
1019
1190
|
redirect: 'follow',
|
|
@@ -1038,15 +1209,18 @@ async function fetchUrl(input) {
|
|
|
1038
1209
|
}
|
|
1039
1210
|
catch (err) {
|
|
1040
1211
|
const e = err;
|
|
1041
|
-
if (e.name === 'AbortError')
|
|
1212
|
+
if (e.name === 'AbortError') {
|
|
1213
|
+
if (signal?.aborted)
|
|
1214
|
+
return 'Interrupted.';
|
|
1042
1215
|
return `Error: request to ${url} timed out after 15s`;
|
|
1216
|
+
}
|
|
1043
1217
|
return `Error fetching ${url}: ${sanitizeError(e)}`;
|
|
1044
1218
|
}
|
|
1045
1219
|
finally {
|
|
1046
1220
|
clearTimeout(timer);
|
|
1047
1221
|
}
|
|
1048
1222
|
}
|
|
1049
|
-
async function webSearch(input) {
|
|
1223
|
+
async function webSearch(input, signal) {
|
|
1050
1224
|
const query = (input.query ?? '').trim();
|
|
1051
1225
|
if (!query || query === 'undefined')
|
|
1052
1226
|
return 'Error: query is required for web_search';
|
|
@@ -1056,11 +1230,18 @@ async function webSearch(input) {
|
|
|
1056
1230
|
if (!apiKey) {
|
|
1057
1231
|
return 'web_search requires an ikie account. Sign in with `ikie login` — search is included with your account.';
|
|
1058
1232
|
}
|
|
1233
|
+
const controller = new AbortController();
|
|
1234
|
+
if (signal) {
|
|
1235
|
+
if (signal.aborted)
|
|
1236
|
+
return 'Interrupted.';
|
|
1237
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
1238
|
+
}
|
|
1059
1239
|
try {
|
|
1060
1240
|
const u = new URL(`${IKIE_API_BASE}/search`);
|
|
1061
1241
|
u.searchParams.set('q', query);
|
|
1062
1242
|
u.searchParams.set('count', String(count));
|
|
1063
1243
|
const res = await fetch(u, {
|
|
1244
|
+
signal: controller.signal,
|
|
1064
1245
|
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
|
|
1065
1246
|
});
|
|
1066
1247
|
if (!res.ok) {
|
|
@@ -1170,13 +1351,13 @@ async function mcpAdd(input) {
|
|
|
1170
1351
|
}
|
|
1171
1352
|
}
|
|
1172
1353
|
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
|
1173
|
-
export async function executeTool(name, input) {
|
|
1354
|
+
export async function executeTool(name, input, signal) {
|
|
1174
1355
|
switch (name) {
|
|
1175
1356
|
case 'read_file': return readFile(input);
|
|
1176
1357
|
case 'switch_mode': return 'Error: switch_mode is handled by the agent, not the tool executor.';
|
|
1177
1358
|
case 'write_file': return writeFile(input);
|
|
1178
1359
|
case 'edit_file': return editFile(input);
|
|
1179
|
-
case 'bash': return bash(input);
|
|
1360
|
+
case 'bash': return bash(input, signal);
|
|
1180
1361
|
case 'list_dir': return listDir(input);
|
|
1181
1362
|
case 'search_files': return searchFiles(input);
|
|
1182
1363
|
case 'grep': return grepFiles(input);
|
|
@@ -1186,8 +1367,8 @@ export async function executeTool(name, input) {
|
|
|
1186
1367
|
case 'git_log': return gitLog(input);
|
|
1187
1368
|
case 'git_commit': return gitCommit(input);
|
|
1188
1369
|
case 'git_branch': return gitBranch(input);
|
|
1189
|
-
case 'fetch_url': return fetchUrl(input);
|
|
1190
|
-
case 'web_search': return webSearch(input);
|
|
1370
|
+
case 'fetch_url': return fetchUrl(input, signal);
|
|
1371
|
+
case 'web_search': return webSearch(input, signal);
|
|
1191
1372
|
case 'use_skill': return useSkill(input);
|
|
1192
1373
|
case 'install_skill': return installSkill(input);
|
|
1193
1374
|
case 'remove_skill': return removeSkill(input);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ikie-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "Agentic coding CLI — your terminal AI pair programmer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"url": "git+https://github.com/ikie-cli/ikie.git"
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://ikie-cli.com",
|
|
27
|
-
"bugs": "https://github.com/
|
|
27
|
+
"bugs": "https://github.com/ikie-cli/ikie/issues",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsc",
|
|
30
30
|
"dev": "tsx src/index.ts",
|