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/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
- // ─── OpenAI-format Tool Definitions ──────────────────────────────────────────
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: ['command'],
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
- // spawn_agent is "safe" at the dispatch layer the tools the sub-agent itself
369
- // runs go through their own approval inside the sub-agent loop.
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', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search', 'git_status', 'git_diff', 'git_log', 'git_branch', 'use_skill', 'install_skill', 'remove_skill', 'mcp_list']);
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
- const command = input.command.trim();
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
- try {
598
- const { stdout, stderr } = await execAsync(command, {
599
- cwd,
600
- timeout,
601
- maxBuffer: 2 * 1024 * 1024,
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
- const parts = [];
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
- process.stdout.write('\n');
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
- process.stdout.write(`${indent}${line}\n`);
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
- process.stdout.write(`${indent}${line}\n`);
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
- process.stdout.write(`${indent}... +${hiddenLines} lines\n`);
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
- const child = spawn(isWindows ? 'cmd.exe' : 'bash', isWindows ? ['/d', '/s', '/c', command] : ['-c', command], { cwd, stdio: 'inherit' });
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.39",
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/anomalyco/opencode/issues",
27
+ "bugs": "https://github.com/ikie-cli/ikie/issues",
28
28
  "scripts": {
29
29
  "build": "tsc",
30
30
  "dev": "tsx src/index.ts",