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 +13 -5
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +20 -1
- package/dist/auth.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -7
- package/dist/index.js +4 -23
- package/dist/repl.js +3 -3
- package/dist/theme.js +2 -0
- package/dist/tools.js +146 -2
- package/package.json +1 -1
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
|
-
-
|
|
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
|
-
#
|
|
26
|
-
|
|
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
|
-
-
|
|
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
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
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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', '
|
|
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
|
|
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 ??
|
|
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,
|
|
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 ||
|
|
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 ??
|
|
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
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(/ /gi, ' ')
|
|
523
|
+
.replace(/&/gi, '&')
|
|
524
|
+
.replace(/</gi, '<')
|
|
525
|
+
.replace(/>/gi, '>')
|
|
526
|
+
.replace(/"/gi, '"')
|
|
527
|
+
.replace(/'|'/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
|
}
|