@kernel.chat/kbot 4.1.0 → 4.1.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.
@@ -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
@@ -10,6 +10,7 @@ import { workspaceAgentTools } from './workspace-agent-tools.js';
10
10
  import { computerCoordinatorTools } from './computer-coordinator-tools.js';
11
11
  import { SECURITY_AGENT_TOOLS } from './security-agent-tools.js';
12
12
  import { anthropicManagedAgentTools } from './anthropic-managed-agents-tools.js';
13
+ import { forecastSummaryTool } from './forecast-summary.js';
13
14
  function adaptCoordinatorTool(t) {
14
15
  const props = t.inputSchema.properties ?? {};
15
16
  const required = new Set(t.inputSchema.required ?? []);
@@ -80,6 +81,7 @@ export function registerSwarm2026Tools() {
80
81
  registerTool(fileLibraryListTool);
81
82
  registerTool(fileLibrarySearchTool);
82
83
  registerTool(fileLibraryGetTool);
84
+ registerTool(forecastSummaryTool);
83
85
  for (const t of workspaceAgentTools)
84
86
  registerTool(t);
85
87
  for (const t of anthropicManagedAgentTools)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Open-source terminal AI agent. 100+ specialist skills, 35 specialist agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT. v4.0 — evidence-based curation.",
5
5
  "type": "module",
6
6
  "repository": {