kiku-agent 1.3.0 → 1.3.1
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 +7 -7
- package/package.json +6 -1
- package/server/agent/agentCore.js +2 -2
- package/server/agent/catalogEntityResolver.js +8 -0
- package/server/agent/cli.js +4 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/prompts.js +1 -1
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/apiRouter.js +7 -11
- package/server/routes/agent.js +55 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
4
|
+
|
|
5
|
+
export function parseIPv4(host) {
|
|
6
|
+
if (typeof host !== 'string' || !host) return null;
|
|
7
|
+
if (host.includes(':')) return null;
|
|
8
|
+
|
|
9
|
+
const parts = host.split('.');
|
|
10
|
+
if (parts.length > 4 || parts.length === 0) return null;
|
|
11
|
+
|
|
12
|
+
const numbers = [];
|
|
13
|
+
for (let i = 0; i < parts.length; i++) {
|
|
14
|
+
const part = parts[i];
|
|
15
|
+
if (!part) return null;
|
|
16
|
+
let val;
|
|
17
|
+
if (/^0x[0-9a-f]+$/i.test(part)) {
|
|
18
|
+
val = parseInt(part, 16);
|
|
19
|
+
} else if (/^0[0-7]+$/.test(part)) {
|
|
20
|
+
val = parseInt(part, 8);
|
|
21
|
+
} else if (/^\d+$/.test(part)) {
|
|
22
|
+
val = parseInt(part, 10);
|
|
23
|
+
} else {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (isNaN(val) || val < 0) return null;
|
|
27
|
+
numbers.push(val);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let a, b, c, d;
|
|
31
|
+
if (numbers.length === 1) {
|
|
32
|
+
const n = numbers[0];
|
|
33
|
+
if (n > 0xffffffff) return null;
|
|
34
|
+
a = (n >>> 24) & 255;
|
|
35
|
+
b = (n >>> 16) & 255;
|
|
36
|
+
c = (n >>> 8) & 255;
|
|
37
|
+
d = n & 255;
|
|
38
|
+
} else if (numbers.length === 2) {
|
|
39
|
+
if (numbers[0] > 255 || numbers[1] > 0xffffff) return null;
|
|
40
|
+
a = numbers[0];
|
|
41
|
+
b = (numbers[1] >>> 16) & 255;
|
|
42
|
+
c = (numbers[1] >>> 8) & 255;
|
|
43
|
+
d = numbers[1] & 255;
|
|
44
|
+
} else if (numbers.length === 3) {
|
|
45
|
+
if (numbers[0] > 255 || numbers[1] > 255 || numbers[2] > 0xffff) return null;
|
|
46
|
+
a = numbers[0];
|
|
47
|
+
b = numbers[1];
|
|
48
|
+
c = (numbers[2] >>> 8) & 255;
|
|
49
|
+
d = numbers[2] & 255;
|
|
50
|
+
} else if (numbers.length === 4) {
|
|
51
|
+
if (numbers.some(n => n > 255)) return null;
|
|
52
|
+
[a, b, c, d] = numbers;
|
|
53
|
+
} else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${a}.${b}.${c}.${d}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isPrivateIPv4(ip) {
|
|
61
|
+
const parsed = parseIPv4(ip) || (typeof ip === 'string' ? ip : '');
|
|
62
|
+
const parts = parsed.split('.').map(Number);
|
|
63
|
+
if (parts.length !== 4 || parts.some(p => isNaN(p) || p < 0 || p > 255)) return false;
|
|
64
|
+
|
|
65
|
+
if (parts[0] === 127) return true;
|
|
66
|
+
if (parts[0] === 10) return true;
|
|
67
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
68
|
+
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
69
|
+
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
70
|
+
if (parts[0] === 0) return true;
|
|
71
|
+
if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true;
|
|
72
|
+
if (parts[0] === 198 && (parts[1] === 18 || parts[1] === 19)) return true;
|
|
73
|
+
if (parts[0] >= 224) return true;
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isPrivateIPv6(ip) {
|
|
79
|
+
const clean = ip.toLowerCase().trim();
|
|
80
|
+
|
|
81
|
+
if (clean === '::1' || clean === '0:0:0:0:0:0:0:1' || clean === '::') return true;
|
|
82
|
+
|
|
83
|
+
if (/^fe[89ab]/i.test(clean)) return true;
|
|
84
|
+
|
|
85
|
+
if (/^f[cd]/i.test(clean)) return true;
|
|
86
|
+
|
|
87
|
+
const mapped = clean.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
88
|
+
if (mapped && isPrivateIPv4(mapped[1])) return true;
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isIpAddress(host) {
|
|
94
|
+
if (!host || typeof host !== 'string') return false;
|
|
95
|
+
return Boolean(parseIPv4(host)) || host.includes(':');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DNS_CACHE = new Map();
|
|
99
|
+
const DNS_CACHE_TTL_MS = 60_000;
|
|
100
|
+
const DNS_CACHE_FAIL_TTL_MS = 10_000;
|
|
101
|
+
const DNS_CACHE_MAX_ENTRIES = 1000;
|
|
102
|
+
|
|
103
|
+
function setDnsCacheEntry(hostname, entry) {
|
|
104
|
+
if (!DNS_CACHE.has(hostname) && DNS_CACHE.size >= DNS_CACHE_MAX_ENTRIES) {
|
|
105
|
+
const oldestKey = DNS_CACHE.keys().next().value;
|
|
106
|
+
if (oldestKey !== undefined) DNS_CACHE.delete(oldestKey);
|
|
107
|
+
}
|
|
108
|
+
DNS_CACHE.set(hostname, entry);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function clearDnsCache() {
|
|
112
|
+
DNS_CACHE.clear();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const httpKeepAliveAgent = new http.Agent({ keepAlive: true, maxSockets: 20 });
|
|
116
|
+
const httpsKeepAliveAgent = new https.Agent({ keepAlive: true, maxSockets: 20 });
|
|
117
|
+
|
|
118
|
+
export async function assertSafeUrlHostname(hostname) {
|
|
119
|
+
if (!hostname || typeof hostname !== 'string') {
|
|
120
|
+
return { safe: false, reason: 'Missing hostname' };
|
|
121
|
+
}
|
|
122
|
+
const clean = hostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
123
|
+
|
|
124
|
+
if (clean === 'localhost' || clean.endsWith('.localhost') || clean.includes('metadata.google') || clean.includes('169.254.169.254')) {
|
|
125
|
+
return { safe: false, reason: `Blocked hostname: ${clean}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parsedIp = parseIPv4(clean);
|
|
129
|
+
if (parsedIp) {
|
|
130
|
+
if (isPrivateIPv4(parsedIp)) {
|
|
131
|
+
return { safe: false, reason: `Target is private/internal IP: ${clean}` };
|
|
132
|
+
}
|
|
133
|
+
return { safe: true, pinnedIp: parsedIp };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (isIpAddress(clean)) {
|
|
137
|
+
if (isPrivateIPv6(clean)) {
|
|
138
|
+
return { safe: false, reason: `Target is private/internal IP: ${clean}` };
|
|
139
|
+
}
|
|
140
|
+
return { safe: true, pinnedIp: clean };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cached = DNS_CACHE.get(clean);
|
|
144
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
145
|
+
return { safe: cached.safe, pinnedIp: cached.pinnedIp, reason: cached.reason };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const addresses = await dnsLookup(clean, { all: true });
|
|
150
|
+
if (!addresses || addresses.length === 0) {
|
|
151
|
+
const res = { safe: false, reason: `Unable to resolve DNS for hostname "${clean}"` };
|
|
152
|
+
setDnsCacheEntry(clean, { ...res, expiresAt: Date.now() + DNS_CACHE_FAIL_TTL_MS });
|
|
153
|
+
return res;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const { address } of addresses) {
|
|
157
|
+
if (isPrivateIPv4(address) || isPrivateIPv6(address)) {
|
|
158
|
+
const res = { safe: false, reason: `Hostname "${clean}" resolved to private IP: ${address}` };
|
|
159
|
+
setDnsCacheEntry(clean, { ...res, expiresAt: Date.now() + DNS_CACHE_FAIL_TTL_MS });
|
|
160
|
+
return res;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const res = { safe: true, pinnedIp: addresses[0].address };
|
|
165
|
+
setDnsCacheEntry(clean, { ...res, expiresAt: Date.now() + DNS_CACHE_TTL_MS });
|
|
166
|
+
return res;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
const res = { safe: false, reason: `DNS lookup failed for "${clean}": ${err.message}` };
|
|
169
|
+
setDnsCacheEntry(clean, { ...res, expiresAt: Date.now() + DNS_CACHE_FAIL_TTL_MS });
|
|
170
|
+
return res;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
export async function safeFetchWithPinning(inputUrl, options = {}, maxRedirects = 3) {
|
|
176
|
+
let currentUrl = String(inputUrl || '');
|
|
177
|
+
let currentOptions = { ...options };
|
|
178
|
+
|
|
179
|
+
for (let redirects = 0; redirects <= maxRedirects; redirects++) {
|
|
180
|
+
let parsedUrl;
|
|
181
|
+
try {
|
|
182
|
+
parsedUrl = new URL(currentUrl);
|
|
183
|
+
} catch {
|
|
184
|
+
throw new Error(`Invalid URL format: "${currentUrl}"`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
188
|
+
throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
192
|
+
const ssrfCheck = await assertSafeUrlHostname(hostname);
|
|
193
|
+
if (!ssrfCheck.safe) {
|
|
194
|
+
throw new Error(`SSRF Guard rejected: ${ssrfCheck.reason}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const targetIp = ssrfCheck.pinnedIp;
|
|
198
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
199
|
+
const client = isHttps ? https : http;
|
|
200
|
+
const port = parsedUrl.port ? Number(parsedUrl.port) : (isHttps ? 443 : 80);
|
|
201
|
+
const method = String(currentOptions.method || 'GET').toUpperCase();
|
|
202
|
+
|
|
203
|
+
const headers = { ...(currentOptions.headers || {}) };
|
|
204
|
+
if (!headers.host && !headers.Host) {
|
|
205
|
+
headers.Host = parsedUrl.host;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const agent = currentOptions.agent || (isHttps ? httpsKeepAliveAgent : httpKeepAliveAgent);
|
|
209
|
+
|
|
210
|
+
const response = await new Promise((resolve, reject) => {
|
|
211
|
+
let req = null;
|
|
212
|
+
try {
|
|
213
|
+
req = client.request({
|
|
214
|
+
host: targetIp,
|
|
215
|
+
port,
|
|
216
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
217
|
+
method,
|
|
218
|
+
headers,
|
|
219
|
+
agent,
|
|
220
|
+
servername: isHttps ? hostname : undefined,
|
|
221
|
+
signal: currentOptions.signal,
|
|
222
|
+
timeout: currentOptions.timeoutMs || 15000
|
|
223
|
+
}, res => {
|
|
224
|
+
const chunks = [];
|
|
225
|
+
let bytesReceived = 0;
|
|
226
|
+
const maxBytes = currentOptions.maxBytes || 5_000_000;
|
|
227
|
+
|
|
228
|
+
res.on('data', chunk => {
|
|
229
|
+
bytesReceived += chunk.length;
|
|
230
|
+
if (bytesReceived <= maxBytes) {
|
|
231
|
+
chunks.push(chunk);
|
|
232
|
+
} else {
|
|
233
|
+
res.destroy();
|
|
234
|
+
req.destroy();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
res.on('end', () => {
|
|
239
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
240
|
+
const responseHeaders = new Headers();
|
|
241
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
242
|
+
if (Array.isArray(v)) {
|
|
243
|
+
v.forEach(val => responseHeaders.append(k, val));
|
|
244
|
+
} else if (v !== undefined) {
|
|
245
|
+
responseHeaders.set(k, v);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
resolve({
|
|
250
|
+
status: res.statusCode || 200,
|
|
251
|
+
statusText: res.statusMessage || '',
|
|
252
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
253
|
+
headers: responseHeaders,
|
|
254
|
+
url: parsedUrl.href,
|
|
255
|
+
text: async () => bodyBuffer.toString('utf8'),
|
|
256
|
+
json: async () => JSON.parse(bodyBuffer.toString('utf8')),
|
|
257
|
+
buffer: async () => bodyBuffer
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
req.on('error', reject);
|
|
263
|
+
req.on('timeout', () => {
|
|
264
|
+
req.destroy(new Error(`Request to ${hostname} (${targetIp}) timed out`));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (currentOptions.body) {
|
|
268
|
+
req.write(currentOptions.body);
|
|
269
|
+
}
|
|
270
|
+
req.end();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
reject(err);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
277
|
+
if (!REDIRECT_STATUSES.has(response.status)) {
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const location = response.headers.get('location');
|
|
282
|
+
if (!location) return response;
|
|
283
|
+
if (redirects === maxRedirects) throw new Error('Too many redirects');
|
|
284
|
+
|
|
285
|
+
currentUrl = new URL(location, parsedUrl).href;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error('Too many redirects');
|
|
289
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class SearchTelemetry {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.reset();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
reset() {
|
|
7
|
+
this.totalSearches = 0;
|
|
8
|
+
this.fallbackTriggers = 0;
|
|
9
|
+
this.zeroResults = 0;
|
|
10
|
+
this.latencies = [];
|
|
11
|
+
this.circuitBreakerTrips = {
|
|
12
|
+
duckduckgo: 0,
|
|
13
|
+
searxng: 0,
|
|
14
|
+
jina: 0
|
|
15
|
+
};
|
|
16
|
+
this.searchesByEngine = {
|
|
17
|
+
duckduckgo: 0,
|
|
18
|
+
searxng: 0,
|
|
19
|
+
jina: 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
recordSearch({ latencyMs = 0, fallbackOccurred = false, zeroResult = false, engine = null } = {}) {
|
|
24
|
+
this.totalSearches++;
|
|
25
|
+
if (fallbackOccurred) this.fallbackTriggers++;
|
|
26
|
+
if (zeroResult) this.zeroResults++;
|
|
27
|
+
if (Number.isFinite(latencyMs) && latencyMs >= 0) {
|
|
28
|
+
this.latencies.push(latencyMs);
|
|
29
|
+
if (this.latencies.length > 2000) {
|
|
30
|
+
this.latencies = this.latencies.slice(-1000);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (engine && this.searchesByEngine[engine] !== undefined) {
|
|
34
|
+
this.searchesByEngine[engine]++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
recordCircuitTrip(engine) {
|
|
39
|
+
const key = String(engine || '').toLowerCase();
|
|
40
|
+
if (this.circuitBreakerTrips[key] !== undefined) {
|
|
41
|
+
this.circuitBreakerTrips[key]++;
|
|
42
|
+
} else {
|
|
43
|
+
this.circuitBreakerTrips[key] = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
calculateP95Latency() {
|
|
48
|
+
if (this.latencies.length === 0) return 0;
|
|
49
|
+
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
50
|
+
const index = Math.ceil(sorted.length * 0.95) - 1;
|
|
51
|
+
return sorted[Math.max(0, index)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getMetrics() {
|
|
55
|
+
const total = this.totalSearches || 1;
|
|
56
|
+
const fallbackRate = (this.fallbackTriggers / total);
|
|
57
|
+
const zeroRate = (this.zeroResults / total);
|
|
58
|
+
const p95 = this.calculateP95Latency();
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
totalSearches: this.totalSearches,
|
|
62
|
+
fallbackTriggerRate: Number(fallbackRate.toFixed(4)),
|
|
63
|
+
zeroResultRate: Number(zeroRate.toFixed(4)),
|
|
64
|
+
searchLatencyP95Ms: p95,
|
|
65
|
+
circuitBreakerTrips: { ...this.circuitBreakerTrips },
|
|
66
|
+
searchesByEngine: { ...this.searchesByEngine }
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const defaultTelemetry = new SearchTelemetry();
|
package/server/apiRouter.js
CHANGED
|
@@ -13,6 +13,8 @@ import { timelineData } from '../src/data/timelineData.js';
|
|
|
13
13
|
import { PACKAGE_VERSION } from './agent/version.js';
|
|
14
14
|
import { QUIZ_CORRECT_ANSWERS } from './agent/quizData.js';
|
|
15
15
|
import { parseEstimateRequest } from './agent/vramService.js';
|
|
16
|
+
import { toolsSchema } from './agent/tools.js';
|
|
17
|
+
import { toolDefinitions as mcpToolDefinitions } from './mcp/tools.js';
|
|
16
18
|
|
|
17
19
|
function loadEnv() {
|
|
18
20
|
try {
|
|
@@ -2081,7 +2083,7 @@ function renderApiDocsHtml() {
|
|
|
2081
2083
|
Kiku AI Agent CLI & MCP Server
|
|
2082
2084
|
</h2>
|
|
2083
2085
|
<p class="text-xs sm:text-sm text-stone-600 dark:text-stone-300 mt-1 max-w-3xl">
|
|
2084
|
-
Need an autonomous AI assistant or want to connect Ju Jingyi's knowledge base to Cursor, Claude Desktop, or VS Code? Use the official <strong>kiku-agent</strong> CLI & <strong>kiku-mcp</strong> server with
|
|
2086
|
+
Need an autonomous AI assistant or want to connect Ju Jingyi's knowledge base to Cursor, Claude Desktop, or VS Code? Use the official <strong>kiku-agent</strong> CLI & <strong>kiku-mcp</strong> server with ${toolsSchema.length} validated tools, local LM Studio (<code class="font-mono text-xs">google/gemma-4-e4b</code>) or cloud LLMs, and multi-agent debate.
|
|
2085
2087
|
</p>
|
|
2086
2088
|
</div>
|
|
2087
2089
|
</div>
|
|
@@ -2091,12 +2093,6 @@ function renderApiDocsHtml() {
|
|
|
2091
2093
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.981 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"/></svg>
|
|
2092
2094
|
<span>View on npm</span>
|
|
2093
2095
|
</a>
|
|
2094
|
-
<a href="/chat" class="px-4 py-2 rounded-xl bg-stone-100 hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700 text-stone-800 dark:text-stone-100 font-bold text-xs border border-stone-300 dark:border-stone-700 flex items-center gap-1.5 transition-all active:scale-95">
|
|
2095
|
-
<span>💬 Web Chat</span>
|
|
2096
|
-
</a>
|
|
2097
|
-
<a href="https://github.com/ThemeHackers/jujingyi_kikuuu" target="_blank" rel="noopener noreferrer" class="px-4 py-2 rounded-xl bg-stone-100 hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700 text-stone-800 dark:text-stone-100 font-bold text-xs border border-stone-300 dark:border-stone-700 flex items-center gap-1.5 transition-all active:scale-95">
|
|
2098
|
-
<span>GitHub</span>
|
|
2099
|
-
</a>
|
|
2100
2096
|
</div>
|
|
2101
2097
|
</div>
|
|
2102
2098
|
|
|
@@ -2109,7 +2105,7 @@ function renderApiDocsHtml() {
|
|
|
2109
2105
|
<span class="w-2.5 h-2.5 rounded-full bg-rose-500 animate-pulse"></span>
|
|
2110
2106
|
<h3 class="font-bold text-stone-900 dark:text-white text-base">Terminal AI Agent (CLI)</h3>
|
|
2111
2107
|
</div>
|
|
2112
|
-
<span class="font-mono text-xs px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-950 text-rose-700 dark:text-rose-300 border border-rose-200 dark:border-rose-800/60 font-semibold"
|
|
2108
|
+
<span class="font-mono text-xs px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-950 text-rose-700 dark:text-rose-300 border border-rose-200 dark:border-rose-800/60 font-semibold">${toolsSchema.length} Tools</span>
|
|
2113
2109
|
</div>
|
|
2114
2110
|
<p class="text-xs text-stone-600 dark:text-stone-300 leading-relaxed">
|
|
2115
2111
|
Autonomous ReAct agent with dynamic multiline input, live thinking reasoning streams, local/cloud LLM switching, background research jobs, YouTube music streaming, and multi-agent debate.
|
|
@@ -2151,7 +2147,7 @@ function renderApiDocsHtml() {
|
|
|
2151
2147
|
<span class="w-2.5 h-2.5 rounded-full bg-purple-500 animate-pulse"></span>
|
|
2152
2148
|
<h3 class="font-bold text-stone-900 dark:text-white text-base">Model Context Protocol (MCP)</h3>
|
|
2153
2149
|
</div>
|
|
2154
|
-
<span class="font-mono text-xs px-2 py-0.5 rounded bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800/60 font-semibold"
|
|
2150
|
+
<span class="font-mono text-xs px-2 py-0.5 rounded bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800/60 font-semibold">${mcpToolDefinitions.length} MCP Tools</span>
|
|
2155
2151
|
</div>
|
|
2156
2152
|
<p class="text-xs text-stone-600 dark:text-stone-300 leading-relaxed">
|
|
2157
2153
|
Connect Cursor, Claude Desktop, VS Code, or Antigravity directly to Ju Jingyi's full knowledge base using stdio or Streamable HTTP.
|
|
@@ -2181,11 +2177,11 @@ function renderApiDocsHtml() {
|
|
|
2181
2177
|
"mcpServers": {
|
|
2182
2178
|
"kiku": {
|
|
2183
2179
|
"command": "npx",
|
|
2184
|
-
"args": ["-y", "kiku-agent", "kiku-mcp"]
|
|
2180
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"]
|
|
2185
2181
|
}
|
|
2186
2182
|
}
|
|
2187
2183
|
}</code></pre>
|
|
2188
|
-
<button type="button" onclick="copySnippet('{\\n \\\"mcpServers\\\": {\\n \\\"kiku\\\": {\\n \\\"command\\\": \\\"npx\\\",\\n \\\"args\\\": [\\\"-y\\\", \\\"kiku-agent\\\", \\\"kiku-mcp\\\"]\\n }\\n }\\n}', this)" class="absolute top-2 right-2 text-[10px] px-2 py-0.5 rounded bg-purple-800/80 hover:bg-purple-700 text-white cursor-pointer">Copy</button>
|
|
2184
|
+
<button type="button" onclick="copySnippet('{\\n \\\"mcpServers\\\": {\\n \\\"kiku\\\": {\\n \\\"command\\\": \\\"npx\\\",\\n \\\"args\\\": [\\\"-y\\\", \\\"-p\\\", \\\"kiku-agent\\\", \\\"kiku-mcp\\\"]\\n }\\n }\\n}', this)" class="absolute top-2 right-2 text-[10px] px-2 py-0.5 rounded bg-purple-800/80 hover:bg-purple-700 text-white cursor-pointer">Copy</button>
|
|
2189
2185
|
</div>
|
|
2190
2186
|
</div>
|
|
2191
2187
|
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { KikuAgent } from '../agent/agentCore.js';
|
|
2
|
+
import { AgentMemory } from '../agent/memory.js';
|
|
3
|
+
import { PUBLIC_REMOTE_TOOL_NAMES } from '../agent/publicTools.js';
|
|
4
|
+
|
|
5
|
+
export function getSharedAgent(options = {}) {
|
|
6
|
+
return new KikuAgent({
|
|
7
|
+
provider: 'lmstudio',
|
|
8
|
+
apiKey: '',
|
|
9
|
+
...options,
|
|
10
|
+
memory: new AgentMemory({ persistence: false }),
|
|
11
|
+
sessionPersistence: false,
|
|
12
|
+
allowedToolNames: PUBLIC_REMOTE_TOOL_NAMES,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function handleAgentChatStream(req, res, queryPrompt, agentOptions = {}) {
|
|
17
|
+
res.statusCode = 200;
|
|
18
|
+
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
19
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
20
|
+
res.setHeader('Connection', 'keep-alive');
|
|
21
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
22
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
23
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
24
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
25
|
+
|
|
26
|
+
const sendEvent = (event, data) => {
|
|
27
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const agent = getSharedAgent(agentOptions);
|
|
31
|
+
await agent.ready();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
sendEvent('start', { prompt: queryPrompt, timestamp: Date.now() });
|
|
35
|
+
|
|
36
|
+
await agent.ask(queryPrompt, {
|
|
37
|
+
onReasoningStart: () => sendEvent('thought_start', {}),
|
|
38
|
+
onReasoningDelta: (chunk) => sendEvent('thought_delta', { chunk }),
|
|
39
|
+
onReasoningEnd: () => sendEvent('thought_end', {}),
|
|
40
|
+
onToolStart: (name, args) => sendEvent('tool_start', { name, args }),
|
|
41
|
+
onToolResult: (name, result, durationMs) => sendEvent('tool_result', { name, result, durationMs }),
|
|
42
|
+
onTokenDelta: (chunk) => sendEvent('token', { chunk }),
|
|
43
|
+
onFinalAnswer: (answer, meta) => sendEvent('done', { answer, meta }),
|
|
44
|
+
onError: (err) => {
|
|
45
|
+
console.error('Agent stream error:', err);
|
|
46
|
+
sendEvent('error', { error: process.env.NODE_ENV === 'development' ? err.message : 'Agent stream execution encountered an issue.' });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Agent chat stream error:', error);
|
|
51
|
+
sendEvent('error', { error: process.env.NODE_ENV === 'development' ? error.message : 'Agent stream execution failed.' });
|
|
52
|
+
} finally {
|
|
53
|
+
res.end();
|
|
54
|
+
}
|
|
55
|
+
}
|