ikie-cli 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
5
  ```bash
6
6
  npm install -g ikie-cli
7
+ ikie login
7
8
  ikie "write a rust web server"
8
9
  ```
9
10
 
10
11
  ## Features
11
12
 
12
- - Streaming AI chat completions via Fireworks AI (default: Kimi K2)
13
- - 10 built-in tools: read/write/edit files, bash, search, grep, memory, agent spawn
13
+ - 12 built-in tools: read/write/edit files, bash, search, grep, memory, agent spawn, web fetch & search
14
14
  - Interactive REPL with slash commands, markdown rendering, and session management
15
15
  - One-shot mode: `ikie "your prompt"`
16
16
  - Image paste support (Ctrl+V)
@@ -22,14 +22,22 @@ ikie "write a rust web server"
22
22
  # Install
23
23
  npm install -g ikie
24
24
 
25
- # Set your API key
26
- export FIREWORKS_API_KEY=fw_...
25
+ # Sign in to your ikie account
26
+ ikie login
27
27
 
28
28
  # Start coding
29
29
  ikie "refactor this file and add tests"
30
30
  ```
31
31
 
32
+ ## Web access
33
+
34
+ ikie can read pages and search the web:
35
+
36
+ - **`fetch_url`** — read any page or URL (HTML is stripped to plain text).
37
+ - **`web_search`** — search the web and get back titles, URLs, and snippets. Powered by the ikie
38
+ server — no separate search API key needed; your ikie login is enough.
39
+
32
40
  ## Requirements
33
41
 
34
42
  - Node.js 20+
35
- - A Fireworks AI API key (or any OpenAI-compatible endpoint via `--base-url`)
43
+ - An ikie account sign in with `ikie login`
package/dist/agent.d.ts CHANGED
@@ -64,5 +64,5 @@ export declare class Agent {
64
64
  private checkPermission;
65
65
  }
66
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.";
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, fetch_url, web_search, 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.";
68
68
  export declare function buildSystemPrompt(projectContext: string, memoryContext: string): string;
package/dist/agent.js CHANGED
@@ -47,6 +47,8 @@ function toolPhaseLabel(name) {
47
47
  case 'list_dir': return 'Listing directory';
48
48
  case 'search_files': return 'Searching';
49
49
  case 'grep': return 'Searching';
50
+ case 'fetch_url': return 'Fetching';
51
+ case 'web_search': return 'Searching web';
50
52
  default: return `Preparing ${name}`;
51
53
  }
52
54
  }
@@ -621,7 +623,7 @@ export const PLAN_MODE_ADDENDUM = `
621
623
 
622
624
  ## PLAN MODE (read-only)
623
625
  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
626
+ list_dir, search_files, grep, fetch_url, web_search, spawn_agent, ask_user). You CANNOT write files, edit
625
627
  files, run shell commands, or change anything — those tools are unavailable and any
626
628
  attempt will be blocked.
627
629
 
@@ -715,6 +717,23 @@ changes they didn't ask for.
715
717
  - \`search_files\`: Find files by glob pattern
716
718
  - \`grep\`: Search file contents by regex
717
719
  - \`memory_write\`: Persist important notes across sessions
720
+ - \`fetch_url\`: Fetch a web page or URL and return its readable text (HTML is stripped to plain text).
721
+ Use it to read docs, articles, or API responses when you have a URL.
722
+ - \`web_search\`: Search the web for a query and get back titles, URLs, and snippets. Works
723
+ automatically for logged-in ikie users — no separate API key needed. Combine with \`fetch_url\`
724
+ to read a promising result in full.
725
+
726
+ ## Using web_search correctly
727
+ - **Always search when the user asks about anything current** — versions, prices, news, docs,
728
+ release dates. Never answer from memory alone for time-sensitive facts.
729
+ - **Check dates in snippets.** Results are ordered by relevance, not recency. If snippets show
730
+ conflicting versions or dates, fetch the most promising URL to get the ground truth.
731
+ - **For "latest X" queries**, include the current year in your search (e.g. "latest Node.js version 2026")
732
+ to surface recent results over older cached pages.
733
+ - **If a fetch returns 403/blocked**, try a different URL from the search results — don't give up.
734
+ - **Trust fetched page content over snippet summaries** — snippets can be stale; the live page is authoritative.
735
+ - **Never state a version, date, or fact as definitive if your search results conflict** — say what
736
+ the most recent source says and link it.
718
737
  - \`ask_user\`: Ask the user a clarifying question when you need more info to proceed.
719
738
  The user's answer is returned as the tool result. Use sparingly — only when genuinely
720
739
  unsure. Don't ask for confirmation on safe operations.
package/dist/auth.js CHANGED
@@ -85,5 +85,5 @@ export async function login() {
85
85
  }
86
86
  export function logout() {
87
87
  clearLogin();
88
- console.log(successLine('Logged out. ikie will use your local API key / Fireworks again.'));
88
+ console.log(successLine('Logged out. Run `ikie login` to sign back in.'));
89
89
  }
package/dist/config.d.ts CHANGED
@@ -37,5 +37,5 @@ export declare function getApiKey(cfg: IkieConfig): string | undefined;
37
37
  export declare function isLoggedIn(cfg: IkieConfig): boolean;
38
38
  /** Persist the hosted account login (ik_live_ key from `ikie login`). */
39
39
  export declare function saveLogin(apiKey: string): void;
40
- /** Clear the hosted account login and revert to the default base URL. */
40
+ /** Clear the hosted account login. */
41
41
  export declare function clearLogin(): void;
package/dist/config.js CHANGED
@@ -17,7 +17,7 @@ const DEFAULTS = {
17
17
  model: DEFAULT_MODEL,
18
18
  maxTokens: 32768,
19
19
  autoApprove: false,
20
- baseURL: FIREWORKS_BASE_URL,
20
+ baseURL: IKIE_API_BASE,
21
21
  temperature: 0.6,
22
22
  topP: 0.95,
23
23
  topK: 40,
@@ -47,10 +47,7 @@ export function saveConfig(patch) {
47
47
  writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...patch }, null, 2));
48
48
  }
49
49
  export function getApiKey(cfg) {
50
- return (process.env.FIREWORKS_API_KEY ??
51
- process.env.IKIE_API_KEY ??
52
- cfg.account?.apiKey ??
53
- cfg.apiKey);
50
+ return cfg.account?.apiKey;
54
51
  }
55
52
  /** True when signed into a hosted ikie account. */
56
53
  export function isLoggedIn(cfg) {
@@ -60,10 +57,9 @@ export function isLoggedIn(cfg) {
60
57
  export function saveLogin(apiKey) {
61
58
  saveConfig({ account: { apiKey, loggedInAt: Date.now() }, baseURL: IKIE_API_BASE });
62
59
  }
63
- /** Clear the hosted account login and revert to the default base URL. */
60
+ /** Clear the hosted account login. */
64
61
  export function clearLogin() {
65
62
  const current = loadConfig();
66
63
  delete current.account;
67
- current.baseURL = FIREWORKS_BASE_URL;
68
64
  writeFileSync(CONFIG_FILE, JSON.stringify(current, null, 2));
69
65
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import process from 'process';
3
3
  import OpenAI from 'openai';
4
4
  import minimist from 'minimist';
5
- import { loadConfig, getApiKey, FIREWORKS_BASE_URL, DEFAULT_MODEL, isLoggedIn, IKIE_API_BASE } from './config.js';
5
+ import { loadConfig, getApiKey, IKIE_API_BASE, DEFAULT_MODEL } from './config.js';
6
6
  import { detectProjectContext, formatContextForPrompt } from './context.js';
7
7
  import { loadAllMemory, formatMemoryForPrompt } from './memory.js';
8
8
  import { buildSystemPrompt, Agent } from './agent.js';
@@ -10,7 +10,7 @@ import { startREPL } from './repl.js';
10
10
  import { login, logout } from './auth.js';
11
11
  import { c, errorLine } from './theme.js';
12
12
  const argv = minimist(process.argv.slice(2), {
13
- string: ['model', 'api-key', 'base-url', 'rpm'],
13
+ string: ['model', 'rpm'],
14
14
  boolean: ['help', 'version', 'yes', 'verbose'],
15
15
  alias: { h: 'help', v: 'version', y: 'yes', m: 'model' },
16
16
  });
@@ -28,17 +28,11 @@ ${c.primary('Usage:')}
28
28
  ${c.primary('Options:')}
29
29
  ${c.accent('-m, --model')} ${c.muted('<id>')} Model ID (default: ${c.dim(DEFAULT_MODEL)})
30
30
  ${c.accent('-y, --yes')} Auto-approve all tool executions
31
- ${c.accent('--api-key')} ${c.muted('<key>')} Fireworks API key
32
- ${c.accent('--base-url')} ${c.muted('<url>')} API base URL (default: Fireworks AI)
33
31
  ${c.accent('--rpm')} ${c.muted('<number>')} Max model requests per minute (default: 10)
34
32
  ${c.accent('--verbose')} Debug output
35
33
  ${c.accent('-h, --help')} Show this help
36
34
  ${c.accent('-v, --version')} Show version
37
35
 
38
- ${c.primary('Environment:')}
39
- ${c.muted('FIREWORKS_API_KEY')} Fireworks AI API key
40
- ${c.muted('IKIE_API_KEY')} Alternative key env var
41
-
42
36
  ${c.primary('In-session commands:')}
43
37
  ${c.muted('/help /clear /memory /session /plan /agent /usage /model /exit')}
44
38
  ${c.muted('Shift+Tab Toggle plan ⇄ agent mode')}
@@ -79,37 +73,24 @@ async function main() {
79
73
  config.model = argv.model;
80
74
  if (argv.yes)
81
75
  config.autoApprove = true;
82
- if (argv['api-key'])
83
- config.apiKey = argv['api-key'];
84
- if (argv['base-url'])
85
- config.baseURL = argv['base-url'];
86
76
  if (argv.rpm) {
87
77
  const rpm = Number(argv.rpm);
88
78
  if (Number.isFinite(rpm) && rpm > 0)
89
79
  config.requestsPerMinute = Math.floor(rpm);
90
80
  }
91
- // When signed into a hosted account, default to the ikie API endpoint
92
- // unless the user explicitly overrode it on the CLI.
93
- if (isLoggedIn(config) && !argv['base-url']) {
94
- config.baseURL = IKIE_API_BASE;
95
- }
96
81
  const apiKey = getApiKey(config);
97
82
  if (!apiKey) {
98
83
  console.error(`
99
84
  ${errorLine('Not signed in.')}
100
85
 
101
- Sign in to your ikie account (recommended):
86
+ Sign in to continue:
102
87
  ${c.accent('ikie login')}
103
-
104
- Or bring your own Fireworks key:
105
- ${c.accent('export FIREWORKS_API_KEY=fw_...')}
106
- ${c.accent('ikie --api-key fw_...')}
107
88
  `);
108
89
  process.exit(1);
109
90
  }
110
91
  const client = new OpenAI({
111
92
  apiKey,
112
- baseURL: config.baseURL ?? FIREWORKS_BASE_URL,
93
+ baseURL: config.baseURL ?? IKIE_API_BASE,
113
94
  });
114
95
  const projectCtx = detectProjectContext();
115
96
  const projectContextStr = formatContextForPrompt(projectCtx);
package/dist/repl.js CHANGED
@@ -4,13 +4,13 @@ import { restoreStdinListeners } from './agent.js';
4
4
  import { c, PROMPT, CONTINUE_PROMPT, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, } from './theme.js';
5
5
  import { renderMarkdown } from './renderer.js';
6
6
  import { loadAllMemory } from './memory.js';
7
- import { HOME_DIR, saveConfig, DEFAULT_MODEL, FIREWORKS_BASE_URL, IKIE_HOST, isLoggedIn, getApiKey } from './config.js';
7
+ import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey } from './config.js';
8
8
  import { login, logout } from './auth.js';
9
9
  import { join as pathJoin } from 'path';
10
10
  import { deleteSession, listSessions, loadSession, normalizeSessionName, saveSession } from './session.js';
11
11
  import { buildUserContent, formatBytes, loadClipboardImageAttachment, loadImageAttachment, hasClipboardImage } from './attachments.js';
12
12
  async function fetchModelsFromServer(config) {
13
- const baseUrl = config.baseURL || FIREWORKS_BASE_URL;
13
+ const baseUrl = config.baseURL || IKIE_API_BASE;
14
14
  const apiUrl = baseUrl.includes('/api/v1')
15
15
  ? baseUrl.replace('/api/v1', '/api/models')
16
16
  : `${IKIE_HOST}/api/models`;
@@ -371,7 +371,7 @@ async function handleSlashCommand(input, agent, config, projectContext, rl, sess
371
371
  console.log(` ${c.secondary('Auto Approve:')} ${c.white(config.autoApprove ? 'Yes' : 'No')}`);
372
372
  console.log(` ${c.secondary('Requests/min:')} ${c.white(config.requestsPerMinute)}`);
373
373
  console.log(` ${c.secondary('Theme:')} ${c.white(config.theme)}`);
374
- console.log(` ${c.secondary('Base URL:')} ${c.white(config.baseURL ?? FIREWORKS_BASE_URL)}`);
374
+ console.log(` ${c.secondary('Base URL:')} ${c.white(config.baseURL ?? IKIE_API_BASE)}`);
375
375
  console.log(`\n ${c.muted('Use')} ${c.warning('/settings <option> <value>')} ${c.muted('to change settings')}`);
376
376
  console.log(` ${c.muted('Options:')} model, temperature, max-tokens, top-p, top-k, auto-approve, rpm, theme`);
377
377
  return true;
package/dist/theme.js CHANGED
@@ -182,6 +182,8 @@ function truncate(str, max) {
182
182
  return str.slice(0, Math.max(0, max - 3)) + '...';
183
183
  }
184
184
  function formatModel(model) {
185
+ if (!model)
186
+ return 'none';
185
187
  const parts = model.split('/');
186
188
  return parts[parts.length - 1] || model;
187
189
  }
package/dist/tools.js CHANGED
@@ -167,15 +167,45 @@ export const TOOL_DEFS = [
167
167
  },
168
168
  },
169
169
  },
170
+ {
171
+ type: 'function',
172
+ function: {
173
+ name: 'fetch_url',
174
+ description: 'Fetch a web page (or any URL) over HTTP(S) and return its readable text content. HTML is stripped to plain text; JSON and plain-text responses are returned as-is. Use this to read documentation, articles, or API responses from the web.',
175
+ parameters: {
176
+ type: 'object',
177
+ properties: {
178
+ url: { type: 'string', description: 'The URL to fetch. If no scheme is given, https:// is assumed.' },
179
+ max_chars: { type: 'number', description: 'Maximum characters to return (default 10000). The result is truncated past this.' },
180
+ },
181
+ required: ['url'],
182
+ },
183
+ },
184
+ },
185
+ {
186
+ type: 'function',
187
+ function: {
188
+ name: 'web_search',
189
+ description: 'Search the web and return a list of results (title, URL, snippet). Requires a search API key configured in the environment (BRAVE_API_KEY, or GOOGLE_API_KEY + GOOGLE_CX). If no key is configured the tool reports that and returns no results — pair it with fetch_url to read a result page in full.',
190
+ parameters: {
191
+ type: 'object',
192
+ properties: {
193
+ query: { type: 'string', description: 'The search query.' },
194
+ count: { type: 'number', description: 'Number of results to return (default 5, max 10).' },
195
+ },
196
+ required: ['query'],
197
+ },
198
+ },
199
+ },
170
200
  ];
171
201
  // ─── Safe tools (auto-approved) ───────────────────────────────────────────────
172
202
  // spawn_agent is "safe" at the dispatch layer — the tools the sub-agent itself
173
203
  // runs go through their own approval inside the sub-agent loop.
174
- export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user']);
204
+ export const SAFE_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'memory_write', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search']);
175
205
  // Tools available in PLAN mode — read-only exploration plus delegation/questions.
176
206
  // Everything that mutates the filesystem or runs commands (write_file, edit_file,
177
207
  // 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']);
208
+ export const PLAN_TOOLS = new Set(['read_file', 'list_dir', 'search_files', 'grep', 'spawn_agent', 'ask_user', 'fetch_url', 'web_search']);
179
209
  // ─── Display helpers ──────────────────────────────────────────────────────────
180
210
  export function formatToolArgs(name, input) {
181
211
  const p = (v) => v != null && v !== 'undefined' ? String(v) : '(missing)';
@@ -206,6 +236,12 @@ export function formatToolArgs(name, input) {
206
236
  const task = String(input.task ?? '');
207
237
  return `"${task.length > 56 ? task.slice(0, 56) + '…' : task}"`;
208
238
  }
239
+ case 'fetch_url':
240
+ return `"${p(input.url)}"`;
241
+ case 'web_search': {
242
+ const q = String(input.query ?? '');
243
+ return `"${q.length > 56 ? q.slice(0, 56) + '…' : q}"`;
244
+ }
209
245
  default:
210
246
  return JSON.stringify(input).slice(0, 80);
211
247
  }
@@ -470,6 +506,112 @@ async function memoryWrite(input) {
470
506
  return `Error saving to ${scope} memory: ${msg}`;
471
507
  }
472
508
  }
509
+ // ─── Web ──────────────────────────────────────────────────────────────────────
510
+ function htmlToText(html) {
511
+ return html
512
+ // Drop non-content blocks entirely.
513
+ .replace(/<!--[\s\S]*?-->/g, '')
514
+ .replace(/<(script|style|noscript|head|svg)[\s\S]*?<\/\1>/gi, '')
515
+ // Turn block-level boundaries into newlines so text doesn't run together.
516
+ .replace(/<\/(p|div|section|article|header|footer|li|tr|h[1-6]|ul|ol|table|blockquote)>/gi, '\n')
517
+ .replace(/<br\s*\/?>/gi, '\n')
518
+ .replace(/<li[^>]*>/gi, '\n• ')
519
+ // Strip all remaining tags.
520
+ .replace(/<[^>]+>/g, '')
521
+ // Decode the handful of entities that actually matter.
522
+ .replace(/&nbsp;/gi, ' ')
523
+ .replace(/&amp;/gi, '&')
524
+ .replace(/&lt;/gi, '<')
525
+ .replace(/&gt;/gi, '>')
526
+ .replace(/&quot;/gi, '"')
527
+ .replace(/&#39;|&apos;/gi, "'")
528
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
529
+ // Collapse whitespace and excess blank lines.
530
+ .replace(/[ \t]+/g, ' ')
531
+ .replace(/\n[ \t]+/g, '\n')
532
+ .replace(/\n{3,}/g, '\n\n')
533
+ .trim();
534
+ }
535
+ async function fetchUrl(input) {
536
+ let url = (input.url ?? '').trim();
537
+ if (!url || url === 'undefined')
538
+ return 'Error: url is required for fetch_url';
539
+ if (!/^[a-z]+:\/\//i.test(url))
540
+ url = `https://${url}`;
541
+ const maxChars = input.max_chars && input.max_chars > 0 ? input.max_chars : 10000;
542
+ const controller = new AbortController();
543
+ const timer = setTimeout(() => controller.abort(), 15000);
544
+ try {
545
+ const res = await fetch(url, {
546
+ redirect: 'follow',
547
+ signal: controller.signal,
548
+ headers: {
549
+ 'User-Agent': 'Mozilla/5.0 (compatible; IkieBot/1.0; +https://ikie.dev)',
550
+ Accept: 'text/html,application/xhtml+xml,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5',
551
+ },
552
+ });
553
+ if (!res.ok)
554
+ return `Error: HTTP ${res.status} ${res.statusText} for ${url}`;
555
+ const contentType = res.headers.get('content-type') ?? '';
556
+ const body = await res.text();
557
+ const text = /text\/html|application\/xhtml/i.test(contentType) ? htmlToText(body) : body.trim();
558
+ const header = `URL: ${res.url}\nContent-Type: ${contentType || 'unknown'}\n\n`;
559
+ if (!text)
560
+ return `${header}(empty response)`;
561
+ if (text.length > maxChars) {
562
+ return `${header}${text.slice(0, maxChars)}\n\n…[truncated ${text.length - maxChars} more chars]`;
563
+ }
564
+ return `${header}${text}`;
565
+ }
566
+ catch (err) {
567
+ const e = err;
568
+ if (e.name === 'AbortError')
569
+ return `Error: request to ${url} timed out after 15s`;
570
+ return `Error fetching ${url}: ${e.message ?? err}`;
571
+ }
572
+ finally {
573
+ clearTimeout(timer);
574
+ }
575
+ }
576
+ async function webSearch(input) {
577
+ const query = (input.query ?? '').trim();
578
+ if (!query || query === 'undefined')
579
+ return 'Error: query is required for web_search';
580
+ const count = Math.min(Math.max(input.count ?? 5, 1), 10);
581
+ // Load account key at call time (same pattern as memoryWrite's dynamic import).
582
+ const { loadConfig, getApiKey, IKIE_API_BASE } = await import('./config.js');
583
+ const apiKey = getApiKey(loadConfig());
584
+ if (!apiKey) {
585
+ return 'web_search requires an ikie account. Sign in with `ikie login` — search is included with your account.';
586
+ }
587
+ try {
588
+ const u = new URL(`${IKIE_API_BASE}/search`);
589
+ u.searchParams.set('q', query);
590
+ u.searchParams.set('count', String(count));
591
+ const res = await fetch(u, {
592
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
593
+ });
594
+ if (!res.ok) {
595
+ const body = await res.text().catch(() => res.statusText);
596
+ return `Error: web_search HTTP ${res.status}: ${body}`;
597
+ }
598
+ const data = await res.json();
599
+ return formatSearchResults(query, data.results ?? []);
600
+ }
601
+ catch (err) {
602
+ const e = err;
603
+ return `Error during web_search: ${e.message ?? err}`;
604
+ }
605
+ }
606
+ function formatSearchResults(query, results) {
607
+ if (!results.length)
608
+ return `No results for "${query}".`;
609
+ const lines = results.map((r, i) => {
610
+ const snippet = (r.snippet ?? '').replace(/\s+/g, ' ').trim();
611
+ return `${i + 1}. ${r.title ?? '(untitled)'}\n ${r.url ?? ''}${snippet ? `\n ${snippet}` : ''}`;
612
+ });
613
+ return `Results for "${query}":\n\n${lines.join('\n\n')}`;
614
+ }
473
615
  // ─── Dispatcher ───────────────────────────────────────────────────────────────
474
616
  export async function executeTool(name, input) {
475
617
  switch (name) {
@@ -481,6 +623,8 @@ export async function executeTool(name, input) {
481
623
  case 'search_files': return searchFiles(input);
482
624
  case 'grep': return grepFiles(input);
483
625
  case 'memory_write': return memoryWrite(input);
626
+ case 'fetch_url': return fetchUrl(input);
627
+ case 'web_search': return webSearch(input);
484
628
  default: return `Unknown tool: ${name}`;
485
629
  }
486
630
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {