@kernel.chat/kbot 3.99.35 → 4.0.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 +15 -6
- package/dist/agent-protocol.js +5 -0
- package/dist/cache-warmth.d.ts +25 -0
- package/dist/cache-warmth.js +131 -0
- package/dist/cli.js +6 -6
- package/dist/plugin-sdk.d.ts +23 -2
- package/dist/plugin-sdk.js +118 -8
- package/dist/plugins.js +15 -1
- package/dist/streaming.js +18 -0
- package/dist/tools/browser.js +1 -0
- package/dist/tools/computer.js +6 -0
- package/dist/tools/containers.js +1 -0
- package/dist/tools/creative.js +5 -0
- package/dist/tools/deploy.js +1 -0
- package/dist/tools/emergent.js +1 -0
- package/dist/tools/gamedev.js +16 -0
- package/dist/tools/hacker-toolkit.js +6 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +16 -80
- package/dist/tools/kbot-browser.js +1 -0
- package/dist/tools/quality.js +1 -0
- package/dist/tools/redblue.js +4 -0
- package/dist/tools/security-brain.js +3 -0
- package/dist/tools/security-hunt.js +1 -0
- package/dist/tools/security.js +6 -0
- package/dist/tools/test-runner.js +1 -0
- package/dist/tools/threat-intel.js +2 -0
- package/dist/ui.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<strong>kbot</strong><br>
|
|
5
|
-
Open-source terminal AI agent.
|
|
5
|
+
Open-source terminal AI agent. 100+ specialist skills. 35 specialist agents. 20 providers. Dreams, learns, watches your system, controls your phone. $0 local.
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
@@ -31,10 +31,17 @@ Most terminal AI agents lock you into one provider, one model, one way of workin
|
|
|
31
31
|
- **Runs fully offline** — Embedded llama.cpp, Ollama, LM Studio, or Jan. $0, fully private.
|
|
32
32
|
- **Learns your patterns** — Bayesian skill ratings + pattern extraction. Gets faster over time.
|
|
33
33
|
- **35 specialist agents** — auto-routes your request to the right expert (coder, researcher, writer, guardian, quant, and 30 more). Run any agent manually: `kbot --agent <id> "<prompt>"`. List them: `kbot agents`.
|
|
34
|
-
- **
|
|
34
|
+
- **100+ specialist skills** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, music production, iPhone control, and more.
|
|
35
|
+
- **v4.0 evidence-based curation** — went from 670 skills to ~100. Every kept skill has telemetry, agent reference, or test coverage. Everything else moved to plugins.
|
|
35
36
|
- **Programmatic SDK** — use kbot as a library in your own apps.
|
|
36
37
|
- **MCP server built in** — plug kbot into Claude Code, Cursor, VS Code, Zed, or Neovim as a tool provider.
|
|
37
38
|
|
|
39
|
+
## Benchmarks
|
|
40
|
+
|
|
41
|
+
Methodology-explicit comparison vs other CLI agents → [BENCHMARKS.md](./BENCHMARKS.md). TL;DR: kbot beats Aider (4.4×) and OpenCode (5.7×) on cold start; loses to Claude Code, Codex, and jcode on raw boot but wins on cost-per-task (BYOK + Ollama fallback), vertical depth (Ableton/security/computer-use/channels), and offline availability (~70% of representative tasks).
|
|
42
|
+
|
|
43
|
+
Using jcode? Wire kbot in as an MCP backend → [templates/jcode-integration.md](./templates/jcode-integration.md).
|
|
44
|
+
|
|
38
45
|
## Use with Claude Code / Cursor / Zed
|
|
39
46
|
|
|
40
47
|
kbot is designed to compound with your existing AI editor, not replace it. One command wires everything up — MCP server config + a Claude Code skill that pre-authorizes the integration so safety filters don't refuse legitimate kbot calls.
|
|
@@ -165,8 +172,8 @@ Checks security, documentation, code quality, CI/CD, community health, and DevOp
|
|
|
165
172
|
|---|---|---|---|---|---|
|
|
166
173
|
| AI providers | 20 | 1 | 1 | 6 | 75+ |
|
|
167
174
|
| Specialist agents | 35 | 0 | 0 | 0 | 0 |
|
|
168
|
-
| Built-in
|
|
169
|
-
| Science
|
|
175
|
+
| Built-in skills | 100+ | ~20 | ~15 | ~10 | ~15 |
|
|
176
|
+
| Science skills | included | 0 | 0 | 0 | 0 |
|
|
170
177
|
| Memory system | 7-tier bidirectional | File-based | No | No | No |
|
|
171
178
|
| Dream engine | Yes ($0 local) | Cloud API | No | No | No |
|
|
172
179
|
| Service watchdog | Yes | No | No | No | No |
|
|
@@ -228,7 +235,9 @@ kbot auto-routes to the right agent for each task. Or pick one with `--agent <na
|
|
|
228
235
|
| **Domain** | infrastructure, quant, investigator, oracle, chronist, sage, communicator, adapter |
|
|
229
236
|
| **Presets** | claude-code, cursor, copilot, creative, developer |
|
|
230
237
|
|
|
231
|
-
##
|
|
238
|
+
## 100+ Specialist Skills
|
|
239
|
+
|
|
240
|
+
As of v4.0, kbot ships ~100 curated skills (down from 670 — every kept skill has telemetry, agent reference, or test coverage). The rest are available as plugins.
|
|
232
241
|
|
|
233
242
|
| Category | Examples |
|
|
234
243
|
|----------|---------|
|
|
@@ -320,7 +329,7 @@ graph TD
|
|
|
320
329
|
D -->|Multi-step| F[Autonomous Planner]
|
|
321
330
|
E --> G[Provider API + Tool Loop]
|
|
322
331
|
F --> G
|
|
323
|
-
G --> H{
|
|
332
|
+
G --> H{100+ Skills}
|
|
324
333
|
H --> I[File ops, bash, git, GitHub, search, deploy, DB, game dev...]
|
|
325
334
|
G --> J[Learning Engine]
|
|
326
335
|
J --> K[Patterns + Solutions + User Profile]
|
package/dist/agent-protocol.js
CHANGED
|
@@ -391,6 +391,7 @@ export function getTrustReport() {
|
|
|
391
391
|
export function registerAgentProtocolTools() {
|
|
392
392
|
registerTool({
|
|
393
393
|
name: 'agent_handoff',
|
|
394
|
+
deprecated: true,
|
|
394
395
|
description: 'Create a handoff to transfer work to another agent. Includes context, artifacts, and priority. The receiving agent can accept or reject. Use this when a task is better suited for a different specialist.',
|
|
395
396
|
parameters: {
|
|
396
397
|
from: { type: 'string', description: 'Agent ID initiating the handoff', required: true },
|
|
@@ -430,6 +431,7 @@ export function registerAgentProtocolTools() {
|
|
|
430
431
|
});
|
|
431
432
|
registerTool({
|
|
432
433
|
name: 'blackboard_write',
|
|
434
|
+
deprecated: true,
|
|
433
435
|
description: 'Write to the shared agent blackboard (working memory). Any agent can write facts, hypotheses, decisions, artifacts, or questions. Other agents can read these to coordinate without direct communication.',
|
|
434
436
|
parameters: {
|
|
435
437
|
key: { type: 'string', description: 'Key to write (e.g., "architecture_decision", "security_finding")', required: true },
|
|
@@ -464,6 +466,7 @@ export function registerAgentProtocolTools() {
|
|
|
464
466
|
});
|
|
465
467
|
registerTool({
|
|
466
468
|
name: 'blackboard_read',
|
|
469
|
+
deprecated: true,
|
|
467
470
|
description: 'Read from the shared agent blackboard. Query a specific key or list all entries filtered by type. Use this to see what other agents have written and coordinate work.',
|
|
468
471
|
parameters: {
|
|
469
472
|
key: { type: 'string', description: 'Specific key to read. If omitted, returns all entries.' },
|
|
@@ -504,6 +507,7 @@ export function registerAgentProtocolTools() {
|
|
|
504
507
|
});
|
|
505
508
|
registerTool({
|
|
506
509
|
name: 'agent_propose',
|
|
510
|
+
deprecated: true,
|
|
507
511
|
description: 'Propose an approach for multi-agent negotiation. Other agents can vote agree/disagree/abstain. Use resolve to determine the outcome. Ties are broken by trust scores.',
|
|
508
512
|
parameters: {
|
|
509
513
|
action: { type: 'string', description: 'Action: propose, vote, resolve, or status', required: true },
|
|
@@ -589,6 +593,7 @@ export function registerAgentProtocolTools() {
|
|
|
589
593
|
});
|
|
590
594
|
registerTool({
|
|
591
595
|
name: 'agent_trust',
|
|
596
|
+
deprecated: true,
|
|
592
597
|
description: 'Check or update trust scores for agents. Trust is asymmetric: success adds 0.05, failure subtracts 0.10. Scores persist across sessions in ~/.kbot/trust.json.',
|
|
593
598
|
parameters: {
|
|
594
599
|
action: { type: 'string', description: 'Action: check, update, best, or report', required: true },
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Anthropic prompt cache TTL — 5 minutes */
|
|
2
|
+
export declare const CACHE_TTL_MS: number;
|
|
3
|
+
/** Hash a system prompt to a short stable key */
|
|
4
|
+
export declare function hashPrompt(text: string): string;
|
|
5
|
+
/** Reset in-memory cache (test hook) */
|
|
6
|
+
export declare function _resetCacheWarmthCache(): void;
|
|
7
|
+
/** Record a successful API call's timestamp */
|
|
8
|
+
export declare function recordCacheCall(model: string, promptHash: string, now?: number): void;
|
|
9
|
+
export interface CacheWarmthCheck {
|
|
10
|
+
warm: boolean;
|
|
11
|
+
ageMs?: number;
|
|
12
|
+
estimatedExtraCostUSD?: number;
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Check whether the prompt cache is still warm for (model, promptHash).
|
|
17
|
+
* Returns warm=true if no prior call OR within TTL. Returns warm=false
|
|
18
|
+
* with a chalk.yellow message when cold AND we haven't warned for this
|
|
19
|
+
* specific cold-event yet.
|
|
20
|
+
*
|
|
21
|
+
* @param costPerMTokInput USD per million input tokens (from auth.ts)
|
|
22
|
+
* @param promptTokenEstimate rough token count (e.g. text.length / 4)
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkCacheWarmth(model: string, promptHash: string, costPerMTokInput: number, promptTokenEstimate: number, now?: number): CacheWarmthCheck;
|
|
25
|
+
//# sourceMappingURL=cache-warmth.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// kbot Cache Warmth — Anthropic prompt cache TTL warning
|
|
2
|
+
//
|
|
3
|
+
// Anthropic's prompt cache has a 5-minute TTL. If the next API call lands
|
|
4
|
+
// after the cache expired, the user pays full input-token price instead
|
|
5
|
+
// of the cached price. This module tracks per-(model, prompt-hash) call
|
|
6
|
+
// timestamps and warns once per cold event.
|
|
7
|
+
//
|
|
8
|
+
// State persists at ~/.kbot/cache-warmth.json (atomic tmp+rename writes).
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
/** Anthropic prompt cache TTL — 5 minutes */
|
|
15
|
+
export const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
16
|
+
/** State file location — overridable via KBOT_CACHE_WARMTH_PATH (test hook) */
|
|
17
|
+
function statePath() {
|
|
18
|
+
return process.env.KBOT_CACHE_WARMTH_PATH || join(homedir(), '.kbot', 'cache-warmth.json');
|
|
19
|
+
}
|
|
20
|
+
/** Hash a system prompt to a short stable key */
|
|
21
|
+
export function hashPrompt(text) {
|
|
22
|
+
return createHash('md5').update(text).digest('hex').slice(0, 16);
|
|
23
|
+
}
|
|
24
|
+
let cached;
|
|
25
|
+
function emptyState() {
|
|
26
|
+
return { lastCall: {}, warnedColdEvents: {} };
|
|
27
|
+
}
|
|
28
|
+
function loadState() {
|
|
29
|
+
if (cached)
|
|
30
|
+
return cached;
|
|
31
|
+
try {
|
|
32
|
+
const path = statePath();
|
|
33
|
+
if (!existsSync(path)) {
|
|
34
|
+
cached = emptyState();
|
|
35
|
+
return cached;
|
|
36
|
+
}
|
|
37
|
+
const raw = readFileSync(path, 'utf8');
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
cached = {
|
|
40
|
+
lastCall: parsed.lastCall || {},
|
|
41
|
+
warnedColdEvents: parsed.warnedColdEvents || {},
|
|
42
|
+
};
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
cached = emptyState();
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function saveState(state) {
|
|
51
|
+
try {
|
|
52
|
+
const path = statePath();
|
|
53
|
+
const dir = dirname(path);
|
|
54
|
+
if (!existsSync(dir))
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
57
|
+
writeFileSync(tmp, JSON.stringify(state), 'utf8');
|
|
58
|
+
renameSync(tmp, path);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Non-fatal — state is best-effort
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Reset in-memory cache (test hook) */
|
|
65
|
+
export function _resetCacheWarmthCache() {
|
|
66
|
+
cached = undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Build the composite key */
|
|
69
|
+
function key(model, promptHash) {
|
|
70
|
+
return `${model}::${promptHash}`;
|
|
71
|
+
}
|
|
72
|
+
/** Record a successful API call's timestamp */
|
|
73
|
+
export function recordCacheCall(model, promptHash, now = Date.now()) {
|
|
74
|
+
const state = loadState();
|
|
75
|
+
state.lastCall[key(model, promptHash)] = now;
|
|
76
|
+
saveState(state);
|
|
77
|
+
}
|
|
78
|
+
/** Format ms as "Nm Ss" */
|
|
79
|
+
function formatAge(ms) {
|
|
80
|
+
const totalSec = Math.floor(ms / 1000);
|
|
81
|
+
const m = Math.floor(totalSec / 60);
|
|
82
|
+
const s = totalSec % 60;
|
|
83
|
+
return `${m}m ${s}s`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check whether the prompt cache is still warm for (model, promptHash).
|
|
87
|
+
* Returns warm=true if no prior call OR within TTL. Returns warm=false
|
|
88
|
+
* with a chalk.yellow message when cold AND we haven't warned for this
|
|
89
|
+
* specific cold-event yet.
|
|
90
|
+
*
|
|
91
|
+
* @param costPerMTokInput USD per million input tokens (from auth.ts)
|
|
92
|
+
* @param promptTokenEstimate rough token count (e.g. text.length / 4)
|
|
93
|
+
*/
|
|
94
|
+
export function checkCacheWarmth(model, promptHash, costPerMTokInput, promptTokenEstimate, now = Date.now()) {
|
|
95
|
+
if (process.env.KBOT_CACHE_WARMTH_WARN === 'off') {
|
|
96
|
+
return { warm: true };
|
|
97
|
+
}
|
|
98
|
+
const state = loadState();
|
|
99
|
+
const k = key(model, promptHash);
|
|
100
|
+
const last = state.lastCall[k];
|
|
101
|
+
// First call ever for this (model, prompt) — cache wasn't expected to exist
|
|
102
|
+
if (!last)
|
|
103
|
+
return { warm: true };
|
|
104
|
+
const ageMs = now - last;
|
|
105
|
+
if (ageMs <= CACHE_TTL_MS) {
|
|
106
|
+
return { warm: true, ageMs };
|
|
107
|
+
}
|
|
108
|
+
// Cold — but only warn once per cold-event (keyed on the prior lastCall ts)
|
|
109
|
+
const warned = state.warnedColdEvents[k] || [];
|
|
110
|
+
if (warned.includes(last)) {
|
|
111
|
+
return { warm: false, ageMs };
|
|
112
|
+
}
|
|
113
|
+
// Cost estimate: cached reads are ~10% of full input price; the cold
|
|
114
|
+
// call pays roughly 90% extra vs. the warm path it would have hit.
|
|
115
|
+
// We report the full input cost as the "extra" — a conservative upper
|
|
116
|
+
// bound that matches what the user actually pays for these tokens.
|
|
117
|
+
const extraUSD = (costPerMTokInput * promptTokenEstimate) / 1_000_000;
|
|
118
|
+
// Persist that we've warned so subsequent calls in the same cold-event
|
|
119
|
+
// (e.g. a tool loop) don't re-warn until a fresh warm window opens.
|
|
120
|
+
warned.push(last);
|
|
121
|
+
// Keep the list bounded
|
|
122
|
+
if (warned.length > 32)
|
|
123
|
+
warned.splice(0, warned.length - 32);
|
|
124
|
+
state.warnedColdEvents[k] = warned;
|
|
125
|
+
saveState(state);
|
|
126
|
+
const message = chalk.yellow(`[kbot] Anthropic prompt cache likely cold — last call was ${formatAge(ageMs)} ago (TTL is 5m). ` +
|
|
127
|
+
`This call will pay full input price (~$${extraUSD.toFixed(2)} more). ` +
|
|
128
|
+
`Run kbot doctor cache for tips.`);
|
|
129
|
+
return { warm: false, ageMs, estimatedExtraCostUSD: extraUSD, message };
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=cache-warmth.js.map
|
package/dist/cli.js
CHANGED
|
@@ -102,7 +102,7 @@ async function main() {
|
|
|
102
102
|
console.log(` ${chalk.cyan('https://github.com/isaacsight/kernel/issues')} ${chalk.dim('Bug reports')}`);
|
|
103
103
|
console.log(` ${chalk.cyan('support@kernel.chat')} ${chalk.dim('Email (AI-assisted replies)')}`);
|
|
104
104
|
console.log();
|
|
105
|
-
console.log(` ${chalk.dim('35 specialist agents ·
|
|
105
|
+
console.log(` ${chalk.dim('35 specialist agents · 100+ skills · 20 providers · MIT licensed')}`);
|
|
106
106
|
console.log();
|
|
107
107
|
process.exit(0);
|
|
108
108
|
});
|
|
@@ -582,10 +582,10 @@ async function main() {
|
|
|
582
582
|
// ── Discovery Agent ──
|
|
583
583
|
const discoveryCmd = program
|
|
584
584
|
.command('discovery')
|
|
585
|
-
.description('
|
|
585
|
+
.description('Background outreach agent — finds conversations, drafts responses, posts for you');
|
|
586
586
|
discoveryCmd
|
|
587
587
|
.command('start')
|
|
588
|
-
.description('Start the discovery loop — scans HN, GitHub, Reddit and posts
|
|
588
|
+
.description('Start the discovery loop — scans HN, GitHub, Reddit and posts in the background')
|
|
589
589
|
.option('--dry-run', 'Find and draft but don\'t post')
|
|
590
590
|
.option('--interval <minutes>', 'Poll interval in minutes', '60')
|
|
591
591
|
.option('--model <model>', 'Ollama model for analysis', 'qwen2.5-coder:32b')
|
|
@@ -4202,7 +4202,7 @@ async function main() {
|
|
|
4202
4202
|
const models = await listOllamaModels();
|
|
4203
4203
|
if (models.length > 0)
|
|
4204
4204
|
printInfo(`${models.length} models available. Using: ${ollamaModel || PROVIDERS.ollama.defaultModel}`);
|
|
4205
|
-
printInfo('
|
|
4205
|
+
printInfo('100+ skills ready. Type your prompt or press Enter for interactive mode.');
|
|
4206
4206
|
}
|
|
4207
4207
|
else {
|
|
4208
4208
|
printError(`Cannot reach Ollama at ${ollamaHost}. Is it running?`);
|
|
@@ -4696,7 +4696,7 @@ async function byokFlow() {
|
|
|
4696
4696
|
console.log();
|
|
4697
4697
|
printSuccess(`BYOK mode enabled — ${providerConfig.name}`);
|
|
4698
4698
|
printInfo('You pay the provider directly. No message limits. No restrictions.');
|
|
4699
|
-
printInfo('All
|
|
4699
|
+
printInfo('All 100+ skills + 35 specialist agents + learning system = yours.');
|
|
4700
4700
|
console.log();
|
|
4701
4701
|
printSuccess('Ready. Run `kbot` to start.');
|
|
4702
4702
|
}
|
|
@@ -4996,7 +4996,7 @@ async function startRepl(agentOpts, context, tier, byokActive = false, localActi
|
|
|
4996
4996
|
const suggestions = await detectProjectSuggestions();
|
|
4997
4997
|
console.log();
|
|
4998
4998
|
console.log(chalk.dim(' ┌─────────────────────────────────────────────────┐'));
|
|
4999
|
-
console.log(chalk.dim(' │') + chalk.bold(' 35 agents.
|
|
4999
|
+
console.log(chalk.dim(' │') + chalk.bold(' 35 agents. 100+ skills. Just say what you need. ') + chalk.dim(' │'));
|
|
5000
5000
|
console.log(chalk.dim(' │ │'));
|
|
5001
5001
|
if (suggestions.length > 0) {
|
|
5002
5002
|
for (const s of suggestions.slice(0, 4)) {
|
package/dist/plugin-sdk.d.ts
CHANGED
|
@@ -35,6 +35,17 @@ export interface PluginConfig {
|
|
|
35
35
|
enabled: string[];
|
|
36
36
|
disabled: string[];
|
|
37
37
|
}
|
|
38
|
+
export interface LoadPluginsOptions {
|
|
39
|
+
/** Override the plugin directory (used by tests). Defaults to ~/.kbot/plugins. */
|
|
40
|
+
pluginsDir?: string;
|
|
41
|
+
/** Override the integrity manifest path. Defaults to ~/.kbot/plugins.json. */
|
|
42
|
+
manifestPath?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Override the integrity-disabled flag. Defaults to reading
|
|
45
|
+
* `process.env.KBOT_PLUGIN_INTEGRITY === 'off'`.
|
|
46
|
+
*/
|
|
47
|
+
integrityDisabled?: boolean;
|
|
48
|
+
}
|
|
38
49
|
export interface SDKPluginManifest {
|
|
39
50
|
name: string;
|
|
40
51
|
version: string;
|
|
@@ -53,8 +64,18 @@ export interface SDKPluginManifest {
|
|
|
53
64
|
/**
|
|
54
65
|
* Load all installed and enabled SDK plugins.
|
|
55
66
|
* Called at startup after the legacy plugins.ts loadPlugins() runs.
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
*
|
|
68
|
+
* Integrity contract (mirrors plugins.ts):
|
|
69
|
+
* - Reads the same manifest at `~/.kbot/plugins.json` (override via
|
|
70
|
+
* `KBOT_PLUGIN_MANIFEST` or `opts.manifestPath`).
|
|
71
|
+
* - If the manifest exists, only plugins whose `name` appears in
|
|
72
|
+
* `result.verified` are imported. Drift → throws `IntegrityError`.
|
|
73
|
+
* - If the manifest is missing, falls back to back-compat behaviour: all
|
|
74
|
+
* discovered, enabled plugins are imported (with a yellow info note).
|
|
75
|
+
* - `KBOT_PLUGIN_INTEGRITY=off` skips verification entirely with a loud
|
|
76
|
+
* yellow warning.
|
|
77
|
+
*/
|
|
78
|
+
export declare function loadPlugins(verbose?: boolean, opts?: LoadPluginsOptions): Promise<SDKPluginManifest[]>;
|
|
58
79
|
/**
|
|
59
80
|
* Scaffold a new plugin with a full project structure.
|
|
60
81
|
* Creates ~/.kbot/plugins/<name>/ with index.ts and package.json.
|
package/dist/plugin-sdk.js
CHANGED
|
@@ -21,11 +21,32 @@ import { join, basename, resolve } from 'node:path';
|
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { pathToFileURL } from 'node:url';
|
|
23
23
|
import { execSync } from 'node:child_process';
|
|
24
|
+
import chalk from 'chalk';
|
|
24
25
|
import { registerTool } from './tools/index.js';
|
|
26
|
+
import { IntegrityError, verifyAllPlugins, enforce, } from './plugins-integrity.js';
|
|
25
27
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
26
28
|
const KBOT_DIR = join(homedir(), '.kbot');
|
|
27
29
|
const PLUGINS_DIR = join(KBOT_DIR, 'plugins');
|
|
30
|
+
// NOTE: PLUGINS_CONFIG (per-plugin enable/disable list) shares the path
|
|
31
|
+
// `~/.kbot/plugins.json` with the integrity manifest used by plugins.ts. This
|
|
32
|
+
// is a pre-existing collision in the SDK loader — the enable/disable JSON has
|
|
33
|
+
// shape `{ enabled, disabled }`, while the integrity manifest has shape
|
|
34
|
+
// `{ schemaVersion, plugins }`. In practice users running with the integrity
|
|
35
|
+
// manifest must override the SDK config path, or the integrity manifest path,
|
|
36
|
+
// via `KBOT_PLUGIN_MANIFEST`. The fix is out of scope for the integrity
|
|
37
|
+
// wiring; flagged here so we do not mask the collision in tests.
|
|
28
38
|
const PLUGINS_CONFIG = join(KBOT_DIR, 'plugins.json');
|
|
39
|
+
/**
|
|
40
|
+
* Default integrity manifest path. Mirrors plugins.ts so a single manifest
|
|
41
|
+
* covers both drop-in `.js` plugins (handled by plugins.ts) and SDK-style
|
|
42
|
+
* directory plugins (handled here). The integrity manifest's `path` field is
|
|
43
|
+
* resolved relative to `~/.kbot/plugins/`, so:
|
|
44
|
+
* - `hello.js` → simple drop-in plugin
|
|
45
|
+
* - `my-tool/index.js` → SDK-style packaged plugin
|
|
46
|
+
* are both expressible in one manifest. Override via `KBOT_PLUGIN_MANIFEST`
|
|
47
|
+
* or the `manifestPath` option to `loadPlugins`.
|
|
48
|
+
*/
|
|
49
|
+
const DEFAULT_MANIFEST_PATH = join(homedir(), '.kbot', 'plugins.json');
|
|
29
50
|
// ── State ────────────────────────────────────────────────────────────────
|
|
30
51
|
const loadedSDKPlugins = new Map();
|
|
31
52
|
const registeredCommands = new Map();
|
|
@@ -219,19 +240,22 @@ function registerPluginHooks(plugin) {
|
|
|
219
240
|
/**
|
|
220
241
|
* Discover local plugins in ~/.kbot/plugins/<name>/ directories.
|
|
221
242
|
* Each directory should contain an index.ts or index.js file.
|
|
243
|
+
*
|
|
244
|
+
* Pass `pluginsDir` to scan a custom location (used by tests). Defaults to
|
|
245
|
+
* the module-level `PLUGINS_DIR` (`~/.kbot/plugins`).
|
|
222
246
|
*/
|
|
223
|
-
function discoverLocalPlugins() {
|
|
224
|
-
ensureDir(
|
|
247
|
+
function discoverLocalPlugins(pluginsDir = PLUGINS_DIR) {
|
|
248
|
+
ensureDir(pluginsDir);
|
|
225
249
|
const results = [];
|
|
226
250
|
let entries;
|
|
227
251
|
try {
|
|
228
|
-
entries = readdirSync(
|
|
252
|
+
entries = readdirSync(pluginsDir);
|
|
229
253
|
}
|
|
230
254
|
catch {
|
|
231
255
|
return results;
|
|
232
256
|
}
|
|
233
257
|
for (const entry of entries) {
|
|
234
|
-
const dirPath = join(
|
|
258
|
+
const dirPath = join(pluginsDir, entry);
|
|
235
259
|
try {
|
|
236
260
|
const stat = statSync(dirPath);
|
|
237
261
|
if (!stat.isDirectory())
|
|
@@ -328,16 +352,91 @@ function discoverNpmPlugins() {
|
|
|
328
352
|
}
|
|
329
353
|
return results;
|
|
330
354
|
}
|
|
355
|
+
// ── Integrity Gate ───────────────────────────────────────────────────────
|
|
356
|
+
/**
|
|
357
|
+
* Run the integrity manifest gate before loading any SDK plugin module.
|
|
358
|
+
*
|
|
359
|
+
* Mirrors `plugins.ts`'s `runIntegrityGate` so the two loaders enforce the
|
|
360
|
+
* same fail-closed contract against the same manifest at `~/.kbot/plugins.json`.
|
|
361
|
+
*
|
|
362
|
+
* Behaviour:
|
|
363
|
+
* - If KBOT_PLUGIN_INTEGRITY=off (or `integrityDisabled === true`):
|
|
364
|
+
* emit a yellow warning and return `null` (verification skipped, all
|
|
365
|
+
* discovered plugins are eligible to load).
|
|
366
|
+
* - If the manifest file does not exist: emit a yellow info note and
|
|
367
|
+
* return `null` (back-compat — manifest is optional today).
|
|
368
|
+
* - If the manifest exists and verifies: return the `VerifyAllResult` so
|
|
369
|
+
* the caller can restrict loads to `result.verified`.
|
|
370
|
+
* - If the manifest exists and any plugin fails: throw `IntegrityError`
|
|
371
|
+
* (loader refuses to import any SDK plugin this session).
|
|
372
|
+
*/
|
|
373
|
+
async function runIntegrityGate(manifestPath, pluginsDir, integrityDisabled) {
|
|
374
|
+
if (integrityDisabled) {
|
|
375
|
+
console.error(chalk.yellow(` ⚠ KBOT_PLUGIN_INTEGRITY=off — skipping integrity check for SDK plugins (NOT FOR PRODUCTION). ` +
|
|
376
|
+
`Plugins under ${pluginsDir} will load without hash checking.`));
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (!existsSync(manifestPath)) {
|
|
380
|
+
console.error(chalk.yellow(` ⚠ no plugin manifest at ${manifestPath} — plugin SDK loaded without integrity verification. ` +
|
|
381
|
+
`Create one to pin plugins by SHA-256 (see PLUGINS_INTEGRITY.md).`));
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const result = await verifyAllPlugins(manifestPath, pluginsDir);
|
|
386
|
+
if (result.failed.length > 0) {
|
|
387
|
+
const lines = result.failed
|
|
388
|
+
.map((f) => ` - ${f.name}: ${f.reason}`)
|
|
389
|
+
.join('\n');
|
|
390
|
+
console.error(chalk.red(` ✗ Plugin integrity check failed for ${result.failed.length} SDK plugin(s):\n${lines}\n` +
|
|
391
|
+
` Manifest: ${manifestPath}\n` +
|
|
392
|
+
` To refresh hashes, recompute SHA-256 for each plugin entry and update the manifest. ` +
|
|
393
|
+
`Set KBOT_PLUGIN_INTEGRITY=off ONLY for local dev; never in production.`));
|
|
394
|
+
enforce(result); // throws IntegrityError
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
if (err instanceof IntegrityError)
|
|
400
|
+
throw err;
|
|
401
|
+
// loadManifest threw (malformed JSON, schema violation, etc.) — fail closed.
|
|
402
|
+
console.error(chalk.red(` ✗ Failed to load plugin integrity manifest at ${manifestPath}: ${err.message}`));
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
331
406
|
// ── Public API ───────────────────────────────────────────────────────────
|
|
332
407
|
/**
|
|
333
408
|
* Load all installed and enabled SDK plugins.
|
|
334
409
|
* Called at startup after the legacy plugins.ts loadPlugins() runs.
|
|
410
|
+
*
|
|
411
|
+
* Integrity contract (mirrors plugins.ts):
|
|
412
|
+
* - Reads the same manifest at `~/.kbot/plugins.json` (override via
|
|
413
|
+
* `KBOT_PLUGIN_MANIFEST` or `opts.manifestPath`).
|
|
414
|
+
* - If the manifest exists, only plugins whose `name` appears in
|
|
415
|
+
* `result.verified` are imported. Drift → throws `IntegrityError`.
|
|
416
|
+
* - If the manifest is missing, falls back to back-compat behaviour: all
|
|
417
|
+
* discovered, enabled plugins are imported (with a yellow info note).
|
|
418
|
+
* - `KBOT_PLUGIN_INTEGRITY=off` skips verification entirely with a loud
|
|
419
|
+
* yellow warning.
|
|
335
420
|
*/
|
|
336
|
-
export async function loadPlugins(verbose = false) {
|
|
421
|
+
export async function loadPlugins(verbose = false, opts = {}) {
|
|
422
|
+
const pluginsDir = opts.pluginsDir ?? PLUGINS_DIR;
|
|
423
|
+
const manifestPath = opts.manifestPath ?? process.env.KBOT_PLUGIN_MANIFEST ?? DEFAULT_MANIFEST_PATH;
|
|
424
|
+
const integrityDisabled = opts.integrityDisabled ?? process.env.KBOT_PLUGIN_INTEGRITY === 'off';
|
|
337
425
|
const manifests = [];
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
const
|
|
426
|
+
// Integrity gate — runs BEFORE any SDK plugin file is imported. Throws
|
|
427
|
+
// IntegrityError on drift unless KBOT_PLUGIN_INTEGRITY=off.
|
|
428
|
+
const integrity = await runIntegrityGate(manifestPath, pluginsDir, integrityDisabled);
|
|
429
|
+
// When a manifest verified successfully, restrict loads to verified names.
|
|
430
|
+
// When the manifest is missing or integrity is disabled, allow every file.
|
|
431
|
+
const verifiedNames = integrity
|
|
432
|
+
? new Set(integrity.verified)
|
|
433
|
+
: null;
|
|
434
|
+
// Discover from both sources. NPM plugins live in global node_modules and
|
|
435
|
+
// are not (yet) covered by the integrity manifest's relative-path scheme;
|
|
436
|
+
// when integrity is enforced, only local plugins listed in the manifest
|
|
437
|
+
// load. NPM plugins are skipped entirely under enforcement.
|
|
438
|
+
const localPlugins = discoverLocalPlugins(pluginsDir);
|
|
439
|
+
const npmPlugins = verifiedNames ? [] : discoverNpmPlugins();
|
|
341
440
|
const allDiscovered = [
|
|
342
441
|
...localPlugins.map(p => ({ ...p, source: 'local' })),
|
|
343
442
|
...npmPlugins.map(p => ({ ...p, source: 'npm' })),
|
|
@@ -361,6 +460,17 @@ export async function loadPlugins(verbose = false) {
|
|
|
361
460
|
manifests.push(manifest);
|
|
362
461
|
continue;
|
|
363
462
|
}
|
|
463
|
+
// When manifest verification ran, only load plugins whose name appears in
|
|
464
|
+
// the verified set. Discovered plugins not declared in the manifest are
|
|
465
|
+
// skipped (they could not have passed verification).
|
|
466
|
+
if (verifiedNames && !verifiedNames.has(name)) {
|
|
467
|
+
if (verbose) {
|
|
468
|
+
console.error(chalk.yellow(` ⚠ [SDK] Skipping ${name} — not declared in plugin manifest`));
|
|
469
|
+
}
|
|
470
|
+
manifest.error = 'not declared in plugin integrity manifest';
|
|
471
|
+
manifests.push(manifest);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
364
474
|
try {
|
|
365
475
|
const plugin = await importPlugin(entryPath);
|
|
366
476
|
manifest.version = plugin.version;
|
package/dist/plugins.js
CHANGED
|
@@ -107,7 +107,21 @@ export async function loadPlugins(verbose = false, opts = {}) {
|
|
|
107
107
|
const verifiedNames = integrity
|
|
108
108
|
? new Set(integrity.verified)
|
|
109
109
|
: null;
|
|
110
|
-
|
|
110
|
+
// Top-level plugin files
|
|
111
|
+
const topLevel = readdirSync(pluginsDir).filter(f => PLUGIN_EXTENSIONS.some(ext => f.endsWith(ext)));
|
|
112
|
+
// Forged subdirectory (created by forge.ts at runtime). v3.99.31 and earlier
|
|
113
|
+
// persisted forged tools here without the loader scanning for them; fixed in v4.0.
|
|
114
|
+
let forgedFiles = [];
|
|
115
|
+
try {
|
|
116
|
+
const forgedDir = join(pluginsDir, 'forged');
|
|
117
|
+
forgedFiles = readdirSync(forgedDir)
|
|
118
|
+
.filter(f => PLUGIN_EXTENSIONS.some(ext => f.endsWith(ext)))
|
|
119
|
+
.map(f => `forged/${f}`);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// forged/ doesn't exist — nothing to load
|
|
123
|
+
}
|
|
124
|
+
const files = [...topLevel, ...forgedFiles];
|
|
111
125
|
if (files.length === 0)
|
|
112
126
|
return [];
|
|
113
127
|
for (const file of files) {
|
package/dist/streaming.js
CHANGED
|
@@ -67,6 +67,24 @@ export async function streamAnthropicResponse(apiKey, apiUrl, model, system, mes
|
|
|
67
67
|
}
|
|
68
68
|
if (tools && tools.length > 0)
|
|
69
69
|
body.tools = tools;
|
|
70
|
+
// Anthropic prompt-cache TTL warning (jcode borrow). Warn once per (model,
|
|
71
|
+
// prompt-hash) cold event when the cache likely expired since last call.
|
|
72
|
+
if (apiUrl.includes('anthropic') && system && process.env.KBOT_CACHE_WARMTH_WARN !== 'off') {
|
|
73
|
+
try {
|
|
74
|
+
const { hashPrompt, checkCacheWarmth, recordCacheCall } = await import('./cache-warmth.js');
|
|
75
|
+
const promptHash = hashPrompt(system);
|
|
76
|
+
const inputCostPerMTok = model.includes('opus') ? 15 : model.includes('haiku') ? 0.8 : 3;
|
|
77
|
+
const promptTokenEstimate = Math.ceil(system.length / 4);
|
|
78
|
+
const check = checkCacheWarmth(model, promptHash, inputCostPerMTok, promptTokenEstimate);
|
|
79
|
+
if (!check.warm && check.message) {
|
|
80
|
+
console.warn((await import('chalk')).default.yellow(check.message));
|
|
81
|
+
}
|
|
82
|
+
recordCacheCall(model, promptHash);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// never let warning logic break the API call
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
let res;
|
|
71
89
|
let lastError;
|
|
72
90
|
for (let attempt = 0; attempt <= MAX_STREAM_RETRIES; attempt++) {
|
package/dist/tools/browser.js
CHANGED
package/dist/tools/computer.js
CHANGED
|
@@ -253,6 +253,7 @@ export function registerComputerTools() {
|
|
|
253
253
|
// ── Permission & lock check ──
|
|
254
254
|
registerTool({
|
|
255
255
|
name: 'computer_check',
|
|
256
|
+
deprecated: true,
|
|
256
257
|
description: 'Check computer use permissions and acquire the session lock. Call this before any other computer use tool. Returns permission status and any required setup steps.',
|
|
257
258
|
parameters: {},
|
|
258
259
|
tier: 'free',
|
|
@@ -277,6 +278,7 @@ export function registerComputerTools() {
|
|
|
277
278
|
// ── App approval ──
|
|
278
279
|
registerTool({
|
|
279
280
|
name: 'app_approve',
|
|
281
|
+
deprecated: true,
|
|
280
282
|
description: 'Approve an app for computer use in this session. Must be called before interacting with an app. Shows a warning for sensitive apps (terminals, Finder, System Settings).',
|
|
281
283
|
parameters: {
|
|
282
284
|
app: { type: 'string', description: 'App name (e.g., "Safari", "Finder", "Xcode")', required: true },
|
|
@@ -456,6 +458,7 @@ export function registerComputerTools() {
|
|
|
456
458
|
// ── Mouse click ──
|
|
457
459
|
registerTool({
|
|
458
460
|
name: 'mouse_click',
|
|
461
|
+
deprecated: true,
|
|
459
462
|
description: 'Click at specific screen coordinates.',
|
|
460
463
|
parameters: {
|
|
461
464
|
x: { type: 'number', description: 'X coordinate', required: true },
|
|
@@ -873,6 +876,7 @@ export function registerComputerTools() {
|
|
|
873
876
|
});
|
|
874
877
|
registerTool({
|
|
875
878
|
name: 'window_resize',
|
|
879
|
+
deprecated: true,
|
|
876
880
|
description: 'Resize a window of a specific app.',
|
|
877
881
|
parameters: {
|
|
878
882
|
app: { type: 'string', description: 'App name', required: true },
|
|
@@ -926,6 +930,7 @@ export function registerComputerTools() {
|
|
|
926
930
|
});
|
|
927
931
|
registerTool({
|
|
928
932
|
name: 'window_move',
|
|
933
|
+
deprecated: true,
|
|
929
934
|
description: 'Move a window to specific screen coordinates.',
|
|
930
935
|
parameters: {
|
|
931
936
|
app: { type: 'string', description: 'App name', required: true },
|
|
@@ -1080,6 +1085,7 @@ export function registerComputerTools() {
|
|
|
1080
1085
|
// ── Release lock ──
|
|
1081
1086
|
registerTool({
|
|
1082
1087
|
name: 'computer_release',
|
|
1088
|
+
deprecated: true,
|
|
1083
1089
|
description: 'Release the computer use lock and end the session. Call when done with computer use.',
|
|
1084
1090
|
parameters: {},
|
|
1085
1091
|
tier: 'free',
|
package/dist/tools/containers.js
CHANGED
|
@@ -428,6 +428,7 @@ export function registerContainerTools() {
|
|
|
428
428
|
// ── License Check ─────────────────────────────────────────────────
|
|
429
429
|
registerTool({
|
|
430
430
|
name: 'license_check',
|
|
431
|
+
deprecated: true,
|
|
431
432
|
description: 'Check license compatibility across a project\'s dependency tree.',
|
|
432
433
|
parameters: {
|
|
433
434
|
path: { type: 'string', description: 'Project directory path', required: true },
|