@kernel.chat/kbot 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/agent-sdk/from-agent-sdk.d.ts +28 -0
- package/dist/adapters/agent-sdk/from-agent-sdk.js +107 -0
- package/dist/adapters/agent-sdk/index.d.ts +4 -0
- package/dist/adapters/agent-sdk/index.js +14 -0
- package/dist/adapters/agent-sdk/to-agent-sdk.d.ts +13 -0
- package/dist/adapters/agent-sdk/to-agent-sdk.js +73 -0
- package/dist/adapters/agent-sdk/types.d.ts +41 -0
- package/dist/adapters/agent-sdk/types.js +9 -0
- package/dist/cli.js +15 -1
- package/dist/permissions.d.ts +17 -0
- package/dist/permissions.js +48 -0
- package/dist/tool-pipeline.js +11 -0
- package/dist/tools/forecast-summary.d.ts +25 -0
- package/dist/tools/forecast-summary.js +204 -0
- package/dist/tools/security-audit-local.d.ts +47 -0
- package/dist/tools/security-audit-local.js +363 -0
- package/dist/tools/stream-overlay.d.ts +9 -0
- package/dist/tools/stream-overlay.js +167 -134
- package/dist/tools/stream-renderer.js +10 -1
- package/dist/tools/swarm-2026-04.js +4 -0
- package/package.json +1 -1
- package/skills/ai-engineering/full-stack-mastery/SKILL.md +174 -0
- package/skills/security-audit/dependency-audit/SKILL.md +102 -0
- package/skills/security-audit/local-vulnerability-hunt/SKILL.md +127 -0
- package/skills/security-audit/secrets-leak-scan/SKILL.md +109 -0
- package/skills/security-audit/threat-model-quickdraw/SKILL.md +107 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../tools/index.js';
|
|
2
|
+
import type { AgentSdkExecutableTool, AgentSdkTool } from './types.js';
|
|
3
|
+
export interface FromAgentSdkOptions {
|
|
4
|
+
/** kbot tier to assign to the imported tool. Default 'free'. */
|
|
5
|
+
tier?: ToolDefinition['tier'];
|
|
6
|
+
/** Custom timeout (ms). */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Max result size (bytes). */
|
|
9
|
+
maxResultSize?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Fallback executor when the source tool ships no handler. Useful when the
|
|
12
|
+
* caller routes execution elsewhere (e.g., back through the Anthropic SDK).
|
|
13
|
+
*/
|
|
14
|
+
fallbackExecutor?: (toolName: string, args: Record<string, unknown>) => Promise<string> | string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Convert a non-executable Agent SDK tool definition into a kbot ToolDefinition.
|
|
18
|
+
* Because the source has no handler, an executor must be supplied via opts.fallbackExecutor
|
|
19
|
+
* — otherwise the resulting tool will return a structured "no handler" error string when invoked.
|
|
20
|
+
*/
|
|
21
|
+
export declare function fromAgentSdkTool(tool: AgentSdkTool, opts?: FromAgentSdkOptions): ToolDefinition;
|
|
22
|
+
/**
|
|
23
|
+
* Convert an executable Agent SDK tool (schema + handler) into a kbot ToolDefinition.
|
|
24
|
+
* Preferred over fromAgentSdkTool() when the caller has the implementation in process.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fromAgentSdkExecutableTool(tool: AgentSdkExecutableTool, opts?: Omit<FromAgentSdkOptions, 'fallbackExecutor'>): ToolDefinition;
|
|
27
|
+
export declare function fromAgentSdkTools(tools: AgentSdkTool[], opts?: FromAgentSdkOptions): ToolDefinition[];
|
|
28
|
+
//# sourceMappingURL=from-agent-sdk.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Anthropic Agent SDK tool → kbot ToolDefinition
|
|
2
|
+
//
|
|
3
|
+
// Lets a kbot host import third-party Agent SDK tools (or hand-written
|
|
4
|
+
// schema/handler pairs) and register them in the kbot tool registry. Schema
|
|
5
|
+
// is downconverted from JSON Schema to kbot's flatter parameter shape.
|
|
6
|
+
//
|
|
7
|
+
// Inverse of to-agent-sdk.ts. Round-trip is lossy in general (JSON Schema is
|
|
8
|
+
// strictly richer than kbot's shape), but is stable for the param shapes
|
|
9
|
+
// that kbot itself uses.
|
|
10
|
+
const JSON_TO_KBOT = {
|
|
11
|
+
string: 'string',
|
|
12
|
+
number: 'number',
|
|
13
|
+
integer: 'integer',
|
|
14
|
+
boolean: 'boolean',
|
|
15
|
+
object: 'object',
|
|
16
|
+
array: 'array',
|
|
17
|
+
null: 'null',
|
|
18
|
+
};
|
|
19
|
+
function pickType(t) {
|
|
20
|
+
if (typeof t === 'string')
|
|
21
|
+
return JSON_TO_KBOT[t] ?? 'string';
|
|
22
|
+
if (Array.isArray(t)) {
|
|
23
|
+
// Pick the first non-null type to keep parity with kbot's single-type shape.
|
|
24
|
+
const first = t.find((x) => x !== 'null');
|
|
25
|
+
return first ? JSON_TO_KBOT[first] ?? 'string' : 'string';
|
|
26
|
+
}
|
|
27
|
+
return 'string';
|
|
28
|
+
}
|
|
29
|
+
function mapProperty(p, required) {
|
|
30
|
+
const out = {
|
|
31
|
+
type: pickType(p.type),
|
|
32
|
+
description: typeof p.description === 'string' ? p.description : '',
|
|
33
|
+
required,
|
|
34
|
+
};
|
|
35
|
+
if (p.default !== undefined)
|
|
36
|
+
out.default = p.default;
|
|
37
|
+
if (p.items !== undefined)
|
|
38
|
+
out.items = p.items;
|
|
39
|
+
if (p.properties)
|
|
40
|
+
out.properties = p.properties;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function buildParameters(schema) {
|
|
44
|
+
if (!schema || typeof schema !== 'object')
|
|
45
|
+
return {};
|
|
46
|
+
const required = new Set(schema.required ?? []);
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
49
|
+
out[name] = mapProperty(prop, required.has(name));
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convert a non-executable Agent SDK tool definition into a kbot ToolDefinition.
|
|
55
|
+
* Because the source has no handler, an executor must be supplied via opts.fallbackExecutor
|
|
56
|
+
* — otherwise the resulting tool will return a structured "no handler" error string when invoked.
|
|
57
|
+
*/
|
|
58
|
+
export function fromAgentSdkTool(tool, opts = {}) {
|
|
59
|
+
const fallback = opts.fallbackExecutor;
|
|
60
|
+
return {
|
|
61
|
+
name: tool.name,
|
|
62
|
+
description: tool.description,
|
|
63
|
+
parameters: buildParameters(tool.input_schema),
|
|
64
|
+
tier: opts.tier ?? 'free',
|
|
65
|
+
timeout: opts.timeout,
|
|
66
|
+
maxResultSize: opts.maxResultSize,
|
|
67
|
+
async execute(args) {
|
|
68
|
+
if (fallback) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await fallback(tool.name, args);
|
|
71
|
+
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return `Error: ${e.message}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return `Error: tool "${tool.name}" was imported without a handler. Provide opts.fallbackExecutor when calling fromAgentSdkTool().`;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Convert an executable Agent SDK tool (schema + handler) into a kbot ToolDefinition.
|
|
83
|
+
* Preferred over fromAgentSdkTool() when the caller has the implementation in process.
|
|
84
|
+
*/
|
|
85
|
+
export function fromAgentSdkExecutableTool(tool, opts = {}) {
|
|
86
|
+
return {
|
|
87
|
+
name: tool.name,
|
|
88
|
+
description: tool.description,
|
|
89
|
+
parameters: buildParameters(tool.input_schema),
|
|
90
|
+
tier: opts.tier ?? 'free',
|
|
91
|
+
timeout: opts.timeout,
|
|
92
|
+
maxResultSize: opts.maxResultSize,
|
|
93
|
+
async execute(args) {
|
|
94
|
+
try {
|
|
95
|
+
const result = await tool.handler(args);
|
|
96
|
+
return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
return `Error: ${e.message}`;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function fromAgentSdkTools(tools, opts = {}) {
|
|
105
|
+
return tools.map((t) => fromAgentSdkTool(t, opts));
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=from-agent-sdk.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { AgentSdkTool, AgentSdkExecutableTool, AgentSdkInputSchema, JsonSchemaProperty, JsonSchemaType, } from './types.js';
|
|
2
|
+
export { toAgentSdkTool, toAgentSdkTools, type ToAgentSdkOptions, } from './to-agent-sdk.js';
|
|
3
|
+
export { fromAgentSdkTool, fromAgentSdkExecutableTool, fromAgentSdkTools, type FromAgentSdkOptions, } from './from-agent-sdk.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Anthropic Agent SDK adapter — bidirectional schema translation between
|
|
2
|
+
// kbot's ToolDefinition surface and the Agent SDK / Messages API tool surface.
|
|
3
|
+
//
|
|
4
|
+
// kbot stays provider-agnostic — this adapter takes no runtime dependency
|
|
5
|
+
// on @anthropic-ai/sdk. It only translates schemas and (for the from-side)
|
|
6
|
+
// wraps optional executors. Round-trip kbot → Agent SDK → kbot is stable
|
|
7
|
+
// for the parameter shapes kbot itself uses.
|
|
8
|
+
//
|
|
9
|
+
// Background: with the Anthropic Agent SDK opening to external developers
|
|
10
|
+
// in May 2026, kbot tools can now be advertised to that ecosystem and
|
|
11
|
+
// vice-versa — without coupling the registry to one provider's runtime.
|
|
12
|
+
export { toAgentSdkTool, toAgentSdkTools, } from './to-agent-sdk.js';
|
|
13
|
+
export { fromAgentSdkTool, fromAgentSdkExecutableTool, fromAgentSdkTools, } from './from-agent-sdk.js';
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../tools/index.js';
|
|
2
|
+
import type { AgentSdkTool } from './types.js';
|
|
3
|
+
export interface ToAgentSdkOptions {
|
|
4
|
+
/** If true (default), kbot tool names are passed through unchanged. */
|
|
5
|
+
preserveName?: boolean;
|
|
6
|
+
/** Optional rename hook. Wins over preserveName. */
|
|
7
|
+
renameTool?: (name: string) => string;
|
|
8
|
+
/** If true, pass `additionalProperties: false` on the input schema. Default true. */
|
|
9
|
+
strict?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function toAgentSdkTool(tool: ToolDefinition, opts?: ToAgentSdkOptions): AgentSdkTool;
|
|
12
|
+
export declare function toAgentSdkTools(tools: ToolDefinition[], opts?: ToAgentSdkOptions): AgentSdkTool[];
|
|
13
|
+
//# sourceMappingURL=to-agent-sdk.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// kbot ToolDefinition → Anthropic Agent SDK tool
|
|
2
|
+
//
|
|
3
|
+
// One-way schema translation. The Agent SDK delivers tool_use blocks and
|
|
4
|
+
// expects the host to route them to a handler — that routing belongs in the
|
|
5
|
+
// caller's agent loop, not in the adapter. This file only produces the
|
|
6
|
+
// schema half so kbot tools can be advertised to the Agent SDK / Messages
|
|
7
|
+
// API without taking a runtime dependency on @anthropic-ai/sdk.
|
|
8
|
+
const KBOT_TYPE_TO_JSON = {
|
|
9
|
+
string: 'string',
|
|
10
|
+
number: 'number',
|
|
11
|
+
integer: 'integer',
|
|
12
|
+
boolean: 'boolean',
|
|
13
|
+
object: 'object',
|
|
14
|
+
array: 'array',
|
|
15
|
+
null: 'null',
|
|
16
|
+
};
|
|
17
|
+
function mapType(t) {
|
|
18
|
+
const lc = t.toLowerCase();
|
|
19
|
+
return KBOT_TYPE_TO_JSON[lc] ?? 'string';
|
|
20
|
+
}
|
|
21
|
+
function mapParameter(p) {
|
|
22
|
+
const out = {
|
|
23
|
+
type: mapType(p.type),
|
|
24
|
+
description: p.description,
|
|
25
|
+
};
|
|
26
|
+
if (p.default !== undefined)
|
|
27
|
+
out.default = p.default;
|
|
28
|
+
if (p.items)
|
|
29
|
+
out.items = p.items;
|
|
30
|
+
if (p.properties) {
|
|
31
|
+
const nested = {};
|
|
32
|
+
for (const [k, v] of Object.entries(p.properties)) {
|
|
33
|
+
// Best-effort: nested properties in kbot params are loosely typed.
|
|
34
|
+
const vv = v;
|
|
35
|
+
nested[k] = {
|
|
36
|
+
type: vv.type ? mapType(vv.type) : 'string',
|
|
37
|
+
description: vv.description ?? '',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
out.properties = nested;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
export function toAgentSdkTool(tool, opts = {}) {
|
|
45
|
+
const properties = {};
|
|
46
|
+
const required = [];
|
|
47
|
+
for (const [name, param] of Object.entries(tool.parameters)) {
|
|
48
|
+
properties[name] = mapParameter(param);
|
|
49
|
+
if (param.required)
|
|
50
|
+
required.push(name);
|
|
51
|
+
}
|
|
52
|
+
const input_schema = {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties,
|
|
55
|
+
additionalProperties: opts.strict === false ? true : false,
|
|
56
|
+
};
|
|
57
|
+
if (required.length > 0)
|
|
58
|
+
input_schema.required = required;
|
|
59
|
+
const name = opts.renameTool
|
|
60
|
+
? opts.renameTool(tool.name)
|
|
61
|
+
: opts.preserveName === false
|
|
62
|
+
? tool.name.replace(/[^A-Za-z0-9_-]/g, '_')
|
|
63
|
+
: tool.name;
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
description: tool.description,
|
|
67
|
+
input_schema,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function toAgentSdkTools(tools, opts = {}) {
|
|
71
|
+
return tools.map((t) => toAgentSdkTool(t, opts));
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=to-agent-sdk.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type JsonSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
|
|
2
|
+
export interface JsonSchemaProperty {
|
|
3
|
+
type?: JsonSchemaType | JsonSchemaType[];
|
|
4
|
+
description?: string;
|
|
5
|
+
enum?: unknown[];
|
|
6
|
+
items?: JsonSchemaProperty | Record<string, unknown>;
|
|
7
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
8
|
+
required?: string[];
|
|
9
|
+
default?: unknown;
|
|
10
|
+
[k: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface AgentSdkInputSchema {
|
|
13
|
+
type: 'object';
|
|
14
|
+
properties: Record<string, JsonSchemaProperty>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
additionalProperties?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The on-the-wire tool definition the Agent SDK and the Messages API both
|
|
20
|
+
* accept. Names are snake_case by convention; the SDK does not enforce.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentSdkTool {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
input_schema: AgentSdkInputSchema;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Optional executable companion. The SDK delivers `tool_use` blocks; the
|
|
29
|
+
* caller is responsible for routing them to a handler. `AgentSdkExecutableTool`
|
|
30
|
+
* couples the schema with a handler so the from-adapter can hand back something
|
|
31
|
+
* the kbot registry can run.
|
|
32
|
+
*/
|
|
33
|
+
export interface AgentSdkExecutableTool extends AgentSdkTool {
|
|
34
|
+
/**
|
|
35
|
+
* Handler may return a string (passed through) or arbitrary JSON-serializable
|
|
36
|
+
* value (stringified by the adapter). Mirrors what real Agent SDK handlers
|
|
37
|
+
* return in practice.
|
|
38
|
+
*/
|
|
39
|
+
handler: (input: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Anthropic Agent SDK tool surface — minimal type model.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the Tool shape used by @anthropic-ai/sdk and the Agent SDK
|
|
4
|
+
// without taking a runtime dependency. kbot stays provider-agnostic;
|
|
5
|
+
// this adapter only ever speaks JSON Schema and JSON.
|
|
6
|
+
//
|
|
7
|
+
// Reference: https://docs.anthropic.com/en/api/messages#tools
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
package/dist/cli.js
CHANGED
|
@@ -56,6 +56,7 @@ async function main() {
|
|
|
56
56
|
.option('--lite', 'Lightweight mode — skip heavy tools (auto-enabled on Replit)')
|
|
57
57
|
.option('--safe', 'Confirm destructive operations')
|
|
58
58
|
.option('--strict', 'Confirm ALL operations')
|
|
59
|
+
.option('--persona <id>', 'Scope tool access to a persona (researcher, coder, computer-use)')
|
|
59
60
|
.option('--ollama-launch', 'Auto-configure for ollama launch (sets Ollama as provider)')
|
|
60
61
|
.argument('[prompt...]', 'One-shot prompt')
|
|
61
62
|
.helpOption('-h, --help', 'display help for command')
|
|
@@ -4369,7 +4370,7 @@ async function main() {
|
|
|
4369
4370
|
}
|
|
4370
4371
|
// Permission mode: autonomous by default, users opt-in to confirmations
|
|
4371
4372
|
{
|
|
4372
|
-
const { setPermissionMode } = await import('./permissions.js');
|
|
4373
|
+
const { setPermissionMode, setActivePersona } = await import('./permissions.js');
|
|
4373
4374
|
if (opts.yes) {
|
|
4374
4375
|
// --yes / -y: skip all confirmations (for scripts & CI)
|
|
4375
4376
|
setPermissionMode('permissive');
|
|
@@ -4384,6 +4385,19 @@ async function main() {
|
|
|
4384
4385
|
// DEFAULT: permissive — kbot acts autonomously, no confirmation prompts
|
|
4385
4386
|
setPermissionMode('permissive');
|
|
4386
4387
|
}
|
|
4388
|
+
// Persona scoping (v4.2.0). Optional. Falls back to env var.
|
|
4389
|
+
const personaId = opts.persona || process.env.KBOT_PERSONA || null;
|
|
4390
|
+
if (personaId) {
|
|
4391
|
+
try {
|
|
4392
|
+
setActivePersona(personaId);
|
|
4393
|
+
if (!opts.quiet && !opts.pipe)
|
|
4394
|
+
printInfo(`Persona: ${personaId}`);
|
|
4395
|
+
}
|
|
4396
|
+
catch (err) {
|
|
4397
|
+
printError(err.message);
|
|
4398
|
+
process.exit(1);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4387
4401
|
}
|
|
4388
4402
|
// Register built-in agents (hacker, operator, dreamer) so --agent flag works
|
|
4389
4403
|
registerBuiltinAgents();
|
package/dist/permissions.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
import { type Persona } from './futures/persona/index.js';
|
|
1
2
|
export type PermissionMode = 'permissive' | 'normal' | 'strict';
|
|
3
|
+
/**
|
|
4
|
+
* Set (or clear) the active persona by id. Pass null to disable persona checking.
|
|
5
|
+
* Throws if id is not found in PERSONA_REGISTRY.
|
|
6
|
+
*
|
|
7
|
+
* v4.2.0 wires the futures/persona substrate into the live permissions chain.
|
|
8
|
+
* When a persona is set, every checkPermission() call runs canInvoke() FIRST;
|
|
9
|
+
* if the persona denies, the tool is blocked before the destructive-op prompt.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setActivePersona(id: string | null): void;
|
|
12
|
+
/** Get the currently active persona, or null if none. */
|
|
13
|
+
export declare function getActivePersona(): Persona | null;
|
|
14
|
+
/**
|
|
15
|
+
* Check the active persona (if any) against a tool invocation.
|
|
16
|
+
* Returns the denial reason string if denied, or null if allowed (or no persona set).
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkPersonaScope(toolName: string, args: Record<string, unknown>): string | null;
|
|
2
19
|
/** Set the permission mode */
|
|
3
20
|
export declare function setPermissionMode(mode: PermissionMode): void;
|
|
4
21
|
/** Get the current permission mode */
|
package/dist/permissions.js
CHANGED
|
@@ -13,7 +13,46 @@
|
|
|
13
13
|
// 'strict' — confirm all file writes and tool calls
|
|
14
14
|
import { createInterface } from 'node:readline';
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
+
import { canInvoke, PERSONA_REGISTRY, } from './futures/persona/index.js';
|
|
16
17
|
let currentMode = 'normal';
|
|
18
|
+
/** Active persona for this CLI run. null = no persona-scoping (default). */
|
|
19
|
+
let activePersona = null;
|
|
20
|
+
/**
|
|
21
|
+
* Set (or clear) the active persona by id. Pass null to disable persona checking.
|
|
22
|
+
* Throws if id is not found in PERSONA_REGISTRY.
|
|
23
|
+
*
|
|
24
|
+
* v4.2.0 wires the futures/persona substrate into the live permissions chain.
|
|
25
|
+
* When a persona is set, every checkPermission() call runs canInvoke() FIRST;
|
|
26
|
+
* if the persona denies, the tool is blocked before the destructive-op prompt.
|
|
27
|
+
*/
|
|
28
|
+
export function setActivePersona(id) {
|
|
29
|
+
if (id === null) {
|
|
30
|
+
activePersona = null;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const persona = PERSONA_REGISTRY[id];
|
|
34
|
+
if (!persona) {
|
|
35
|
+
const known = Object.keys(PERSONA_REGISTRY).join(', ');
|
|
36
|
+
throw new Error(`unknown persona "${id}". Known personas: ${known}`);
|
|
37
|
+
}
|
|
38
|
+
activePersona = persona;
|
|
39
|
+
}
|
|
40
|
+
/** Get the currently active persona, or null if none. */
|
|
41
|
+
export function getActivePersona() {
|
|
42
|
+
return activePersona;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check the active persona (if any) against a tool invocation.
|
|
46
|
+
* Returns the denial reason string if denied, or null if allowed (or no persona set).
|
|
47
|
+
*/
|
|
48
|
+
export function checkPersonaScope(toolName, args) {
|
|
49
|
+
if (!activePersona)
|
|
50
|
+
return null;
|
|
51
|
+
const verdict = canInvoke(activePersona, toolName, args);
|
|
52
|
+
if (verdict.allowed)
|
|
53
|
+
return null;
|
|
54
|
+
return `Persona '${activePersona.id}' denies '${toolName}': ${verdict.reason ?? 'permission denied'}`;
|
|
55
|
+
}
|
|
17
56
|
/** Patterns that always require confirmation in normal mode */
|
|
18
57
|
const DESTRUCTIVE_PATTERNS = [
|
|
19
58
|
{ pattern: /^git\s+push/i, reason: 'Pushes code to remote — visible to others' },
|
|
@@ -119,6 +158,15 @@ export async function confirmToolCall(toolName, args, reason) {
|
|
|
119
158
|
* Used as middleware in the tool execution pipeline.
|
|
120
159
|
*/
|
|
121
160
|
export async function checkPermission(toolName, args) {
|
|
161
|
+
// Persona check fires BEFORE the destructive-op prompt. If a persona is
|
|
162
|
+
// set and denies the tool, fail fast — no confirmation prompt, no retry.
|
|
163
|
+
const personaDenial = checkPersonaScope(toolName, args);
|
|
164
|
+
if (personaDenial) {
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(` ${chalk.red('✗')} ${personaDenial}`);
|
|
167
|
+
console.log();
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
122
170
|
const reason = needsConfirmation(toolName, args);
|
|
123
171
|
if (!reason)
|
|
124
172
|
return true;
|
package/dist/tool-pipeline.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// pipeline.use(metricsMiddleware(recordMetrics))
|
|
12
12
|
// pipeline.use(executionMiddleware(executeTool))
|
|
13
13
|
// await pipeline.execute(ctx)
|
|
14
|
+
import { checkPersonaScope } from './permissions.js';
|
|
14
15
|
export class ToolPipeline {
|
|
15
16
|
middleware = [];
|
|
16
17
|
/** Append middleware to the end of the pipeline */
|
|
@@ -56,6 +57,16 @@ export class ToolPipeline {
|
|
|
56
57
|
*/
|
|
57
58
|
export function permissionMiddleware(checkPermission) {
|
|
58
59
|
return async (ctx, next) => {
|
|
60
|
+
// v4.2.0: persona scope check fires BEFORE the destructive-op
|
|
61
|
+
// confirmation. A persona denial blocks the tool with a clear reason
|
|
62
|
+
// and is non-interactive — no prompt, no retry.
|
|
63
|
+
const personaDenial = checkPersonaScope(ctx.toolName, ctx.toolArgs);
|
|
64
|
+
if (personaDenial) {
|
|
65
|
+
ctx.aborted = true;
|
|
66
|
+
ctx.abortReason = personaDenial;
|
|
67
|
+
ctx.error = personaDenial;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
59
70
|
const allowed = await checkPermission(ctx.toolName, ctx.toolArgs);
|
|
60
71
|
if (!allowed) {
|
|
61
72
|
ctx.aborted = true;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ToolDefinition } from './index.js';
|
|
2
|
+
import { type GrowthMetrics } from './foundation-engines.js';
|
|
3
|
+
import type { Horizon, Signal } from '../futures/forecast/types.js';
|
|
4
|
+
interface HistoryRecord {
|
|
5
|
+
ts: number;
|
|
6
|
+
signals: Partial<Record<keyof GrowthMetrics, number>>;
|
|
7
|
+
}
|
|
8
|
+
/** Read history file. Skips malformed lines. Returns chronological order. */
|
|
9
|
+
export declare function readHistory(path?: string): HistoryRecord[];
|
|
10
|
+
/** Append a record, cap to MAX_HISTORY entries. Atomic via tmp + rename. */
|
|
11
|
+
export declare function appendHistory(rec: HistoryRecord, path?: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Convert a list of HistoryRecord into Signal[] keyed by metric name.
|
|
14
|
+
* Only emits metrics that appear in at least one record.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildSignals(history: HistoryRecord[]): Signal[];
|
|
17
|
+
/** Run the full forecast pipeline, given a horizon and an optional clock. */
|
|
18
|
+
export declare function runForecastSummary(opts?: {
|
|
19
|
+
horizon?: Horizon;
|
|
20
|
+
now?: number;
|
|
21
|
+
metricsOverride?: GrowthMetrics | null;
|
|
22
|
+
}): string;
|
|
23
|
+
export declare const forecastSummaryTool: ToolDefinition;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=forecast-summary.d.ts.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// forecast-summary — wires the v5 futures/forecast substrate into a kbot tool.
|
|
2
|
+
//
|
|
3
|
+
// Reads kbot's current growth metrics (the same surface growth_summary
|
|
4
|
+
// produces), persists a rolling-window JSONL history at
|
|
5
|
+
// ~/.kbot/growth/history.jsonl, and projects each metric forward via
|
|
6
|
+
// synthesizeForecasts(). Returns markdown narrative + structured JSON.
|
|
7
|
+
//
|
|
8
|
+
// First substrate-to-product hop for the v4.2 forecast module. Opt-in only;
|
|
9
|
+
// does NOT modify growth_summary.
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { loadGrowth } from './foundation-engines.js';
|
|
14
|
+
import { synthesizeForecasts, narrative } from '../futures/forecast/index.js';
|
|
15
|
+
import { clampHorizon, signalHistory, } from '../futures/forecast/projection.js';
|
|
16
|
+
const HORIZONS = new Set(['1d', '7d', '30d', '90d']);
|
|
17
|
+
const MAX_HISTORY = 90;
|
|
18
|
+
const MIN_SAMPLES = 5;
|
|
19
|
+
/** Override for tests; falls back to ~/.kbot/growth. */
|
|
20
|
+
function historyDir() {
|
|
21
|
+
return process.env.KBOT_GROWTH_HISTORY_DIR ?? join(homedir(), '.kbot', 'growth');
|
|
22
|
+
}
|
|
23
|
+
function historyPath() {
|
|
24
|
+
return join(historyDir(), 'history.jsonl');
|
|
25
|
+
}
|
|
26
|
+
/** Read history file. Skips malformed lines. Returns chronological order. */
|
|
27
|
+
export function readHistory(path = historyPath()) {
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return [];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
const rec = JSON.parse(trimmed);
|
|
37
|
+
if (typeof rec.ts === 'number' && rec.signals && typeof rec.signals === 'object') {
|
|
38
|
+
out.push(rec);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// skip malformed
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
out.sort((a, b) => a.ts - b.ts);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** Append a record, cap to MAX_HISTORY entries. Atomic via tmp + rename. */
|
|
49
|
+
export function appendHistory(rec, path = historyPath()) {
|
|
50
|
+
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
52
|
+
const existing = readHistory(path);
|
|
53
|
+
existing.push(rec);
|
|
54
|
+
// Keep only the last MAX_HISTORY records
|
|
55
|
+
const kept = existing.slice(-MAX_HISTORY);
|
|
56
|
+
const body = kept.map((r) => JSON.stringify(r)).join('\n') + '\n';
|
|
57
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
58
|
+
writeFileSync(tmp, body, 'utf8');
|
|
59
|
+
renameSync(tmp, path);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Convert a list of HistoryRecord into Signal[] keyed by metric name.
|
|
63
|
+
* Only emits metrics that appear in at least one record.
|
|
64
|
+
*/
|
|
65
|
+
export function buildSignals(history) {
|
|
66
|
+
const byMetric = new Map();
|
|
67
|
+
for (const rec of history) {
|
|
68
|
+
for (const [name, v] of Object.entries(rec.signals)) {
|
|
69
|
+
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
70
|
+
continue;
|
|
71
|
+
const arr = byMetric.get(name) ?? [];
|
|
72
|
+
arr.push({ ts: rec.ts, value: v });
|
|
73
|
+
byMetric.set(name, arr);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...byMetric.entries()].map(([name, values]) => ({ name, values }));
|
|
77
|
+
}
|
|
78
|
+
/** Read current growth metrics. Used to capture a fresh sample. */
|
|
79
|
+
function currentMetrics() {
|
|
80
|
+
const g = loadGrowth();
|
|
81
|
+
if (!g)
|
|
82
|
+
return null;
|
|
83
|
+
return { ...g.metrics };
|
|
84
|
+
}
|
|
85
|
+
function pickMetricsForHistory(m) {
|
|
86
|
+
// Persist every metric — the tool decides what to project at read time.
|
|
87
|
+
return {
|
|
88
|
+
npmDownloads: m.npmDownloads,
|
|
89
|
+
githubStars: m.githubStars,
|
|
90
|
+
totalUsers: m.totalUsers,
|
|
91
|
+
totalMessages: m.totalMessages,
|
|
92
|
+
totalStreams: m.totalStreams,
|
|
93
|
+
totalStreamMinutes: m.totalStreamMinutes,
|
|
94
|
+
toolsBuilt: m.toolsBuilt,
|
|
95
|
+
factsLearned: m.factsLearned,
|
|
96
|
+
dreamsDreamed: m.dreamsDreamed,
|
|
97
|
+
techniquesDiscovered: m.techniquesDiscovered,
|
|
98
|
+
worldBlocksPlaced: m.worldBlocksPlaced,
|
|
99
|
+
versionsShipped: m.versionsShipped,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function bootstrapMessage(sample) {
|
|
103
|
+
return [
|
|
104
|
+
`# forecast_summary — bootstrapping`,
|
|
105
|
+
``,
|
|
106
|
+
`forecast_summary needs at least ${MIN_SAMPLES} historical samples to project.`,
|
|
107
|
+
`This is sample ${sample} of ${MIN_SAMPLES}. Re-run periodically to seed the history.`,
|
|
108
|
+
``,
|
|
109
|
+
`History path: \`${historyPath()}\``,
|
|
110
|
+
].join('\n');
|
|
111
|
+
}
|
|
112
|
+
function markdownTable(forecasts) {
|
|
113
|
+
if (forecasts.length === 0)
|
|
114
|
+
return '';
|
|
115
|
+
const lines = [];
|
|
116
|
+
lines.push('| signal | direction | point | range | confidence | method |');
|
|
117
|
+
lines.push('|---|---|---|---|---|---|');
|
|
118
|
+
for (const f of forecasts) {
|
|
119
|
+
const dir = f.trend.kind === 'flat' ? 'flat' : f.trend.slope > 0 ? 'up' : 'down';
|
|
120
|
+
const point = formatNum(f.pointEstimate);
|
|
121
|
+
const range = `${formatNum(f.lowerBound)} – ${formatNum(f.upperBound)}`;
|
|
122
|
+
const conf = `${(f.confidence * 100).toFixed(0)}%`;
|
|
123
|
+
lines.push(`| ${f.signal} | ${dir} | ${point} | ${range} | ${conf} | ${f.method} |`);
|
|
124
|
+
}
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
function formatNum(n) {
|
|
128
|
+
if (!Number.isFinite(n))
|
|
129
|
+
return '—';
|
|
130
|
+
const abs = Math.abs(n);
|
|
131
|
+
if (abs >= 1_000_000)
|
|
132
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
133
|
+
if (abs >= 1_000)
|
|
134
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
135
|
+
if (abs >= 10)
|
|
136
|
+
return n.toFixed(0);
|
|
137
|
+
if (abs >= 1)
|
|
138
|
+
return n.toFixed(1);
|
|
139
|
+
return n.toFixed(2);
|
|
140
|
+
}
|
|
141
|
+
/** Run the full forecast pipeline, given a horizon and an optional clock. */
|
|
142
|
+
export function runForecastSummary(opts = {}) {
|
|
143
|
+
const horizon = opts.horizon && HORIZONS.has(opts.horizon) ? opts.horizon : '30d';
|
|
144
|
+
const now = opts.now ?? Date.now();
|
|
145
|
+
const metrics = opts.metricsOverride === undefined ? currentMetrics() : opts.metricsOverride;
|
|
146
|
+
const path = historyPath();
|
|
147
|
+
// Capture a new sample if we have growth metrics to sample from.
|
|
148
|
+
if (metrics) {
|
|
149
|
+
appendHistory({ ts: now, signals: pickMetricsForHistory(metrics) }, path);
|
|
150
|
+
}
|
|
151
|
+
const history = readHistory(path);
|
|
152
|
+
if (history.length < MIN_SAMPLES) {
|
|
153
|
+
return bootstrapMessage(history.length);
|
|
154
|
+
}
|
|
155
|
+
const allSignals = buildSignals(history);
|
|
156
|
+
// Identify signals too short for the requested horizon → skip + note.
|
|
157
|
+
const skipped = [];
|
|
158
|
+
const usable = [];
|
|
159
|
+
for (const sig of allSignals) {
|
|
160
|
+
if (clampHorizon(horizon, signalHistory(sig))) {
|
|
161
|
+
usable.push(sig);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
skipped.push(sig.name);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const forecasts = synthesizeForecasts(usable, horizon);
|
|
168
|
+
const summary = narrative(forecasts);
|
|
169
|
+
const md = [];
|
|
170
|
+
md.push(`# forecast_summary — horizon ${horizon}`);
|
|
171
|
+
md.push('');
|
|
172
|
+
md.push(summary);
|
|
173
|
+
md.push('');
|
|
174
|
+
if (forecasts.length > 0) {
|
|
175
|
+
md.push(markdownTable(forecasts));
|
|
176
|
+
md.push('');
|
|
177
|
+
}
|
|
178
|
+
if (skipped.length > 0) {
|
|
179
|
+
md.push(`> Skipped (history shorter than ${horizon}): ${skipped.join(', ')}`);
|
|
180
|
+
md.push('');
|
|
181
|
+
}
|
|
182
|
+
md.push('```json');
|
|
183
|
+
md.push(JSON.stringify({ horizon, forecasts, skipped, samples: history.length }, null, 2));
|
|
184
|
+
md.push('```');
|
|
185
|
+
return md.join('\n');
|
|
186
|
+
}
|
|
187
|
+
export const forecastSummaryTool = {
|
|
188
|
+
name: 'forecast_summary',
|
|
189
|
+
description: 'Project kbot growth signals (npm downloads, GitHub stars, users, etc.) forward at the given horizon. Wraps the futures/forecast substrate. Returns a markdown narrative + structured JSON. Opt-in; does not run automatically.',
|
|
190
|
+
parameters: {
|
|
191
|
+
horizon: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'Projection horizon: 1d, 7d, 30d, or 90d (default 30d).',
|
|
194
|
+
required: false,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
tier: 'free',
|
|
198
|
+
async execute(args) {
|
|
199
|
+
const raw = typeof args.horizon === 'string' ? args.horizon : '30d';
|
|
200
|
+
const horizon = HORIZONS.has(raw) ? raw : '30d';
|
|
201
|
+
return runForecastSummary({ horizon });
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=forecast-summary.js.map
|