@kernel.chat/kbot 3.99.19 → 3.99.21

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,141 @@
1
+ /**
2
+ * HierarchicalPlanner — Phase 1 passthrough.
3
+ *
4
+ * This file lays the pipe for the four-tier hierarchical planner described in
5
+ * ./types.ts. Today it does NOT implement tiers — `planAndExecute` delegates
6
+ * the entire user turn to the existing flat `autonomousExecute` from
7
+ * ../../planner.ts and synthesizes a `PlannerResult` shell around it.
8
+ *
9
+ * Real goal persistence is live: `loadGoal`, `createGoal`, and `setGoal` read
10
+ * and write `~/.kbot/planner/goals/<id>.json` plus the `active.json` pointer.
11
+ * The current PlannerResult.goal is always `null` in this phase because the
12
+ * tier-1 selection/creation loop isn't wired in yet — callers opt into goal
13
+ * tracking explicitly via createGoal/setGoal.
14
+ *
15
+ * Feature flag: `planAndExecute` throws unless `process.env.KBOT_PLANNER ===
16
+ * 'hierarchical'`. Nothing in production calls this class yet.
17
+ */
18
+ import { randomBytes } from 'node:crypto';
19
+ import { autonomousExecute } from '../../planner.js';
20
+ import { defaultStateDir, getActive, readGoal, setActive, writeGoal, } from './persistence.js';
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // ULID-ish id generator.
23
+ // We depend on `ulid` if it's resolvable at runtime, otherwise fall back to a
24
+ // 26-char Crockford-base32 string sourced from crypto.randomBytes. Either way
25
+ // IDs are sortable-ish and collision-resistant enough for per-goal filenames.
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
28
+ function cryptoUlid() {
29
+ // 10 chars of timestamp (ms) + 16 chars of randomness = 26 chars.
30
+ let ts = Date.now();
31
+ const tsChars = [];
32
+ for (let i = 0; i < 10; i++) {
33
+ tsChars.unshift(CROCKFORD[ts % 32]);
34
+ ts = Math.floor(ts / 32);
35
+ }
36
+ const bytes = randomBytes(10);
37
+ const rand = [];
38
+ // 80 bits → 16 base32 chars. Pull 5 bits at a time.
39
+ let acc = 0;
40
+ let accBits = 0;
41
+ for (const b of bytes) {
42
+ acc = (acc << 8) | b;
43
+ accBits += 8;
44
+ while (accBits >= 5) {
45
+ accBits -= 5;
46
+ rand.push(CROCKFORD[(acc >> accBits) & 0x1f]);
47
+ }
48
+ }
49
+ return tsChars.join('') + rand.slice(0, 16).join('');
50
+ }
51
+ export class HierarchicalPlanner {
52
+ stateDir;
53
+ constructor(opts = {}) {
54
+ this.stateDir = opts.stateDir ?? defaultStateDir();
55
+ }
56
+ /**
57
+ * Plan and execute one user turn.
58
+ *
59
+ * Phase 1: feature-flag gated passthrough to autonomousExecute.
60
+ * Phase 2+: replace the body with the Tier 1–4 cascade.
61
+ */
62
+ async planAndExecute(userTurn, ctx) {
63
+ const flag = process.env.KBOT_PLANNER;
64
+ if (flag !== 'hierarchical') {
65
+ throw new Error(`HierarchicalPlanner.planAndExecute is gated behind KBOT_PLANNER=hierarchical ` +
66
+ `(got ${flag ?? 'unset'}). This path is Phase-1 scaffolding only.`);
67
+ }
68
+ const startedAt = Date.now();
69
+ const plan = await autonomousExecute(userTurn, ctx.agentOpts, {
70
+ autoApprove: ctx.autoApprove ?? true,
71
+ });
72
+ const phase = {
73
+ id: cryptoUlid(),
74
+ goalId: '', // no goal linked in Phase 1
75
+ kind: 'other',
76
+ objective: userTurn,
77
+ exitCriteria: [],
78
+ startedAt: new Date(startedAt).toISOString(),
79
+ endedAt: new Date().toISOString(),
80
+ status: plan.status === 'completed' ? 'done' : 'active',
81
+ };
82
+ const steps = plan.steps.map(step => ({ ...step }));
83
+ const action = {
84
+ id: cryptoUlid(),
85
+ phaseId: phase.id,
86
+ userTurn,
87
+ summary: plan.summary,
88
+ steps,
89
+ createdAt: plan.createdAt,
90
+ status: plan.status === 'completed'
91
+ ? 'done'
92
+ : plan.status === 'failed'
93
+ ? 'failed'
94
+ : 'running',
95
+ };
96
+ const metrics = {
97
+ tier1Calls: 0,
98
+ tier2Calls: 0,
99
+ tier3Calls: 1, // autonomousExecute counts as one tier-3 invocation
100
+ tier4Calls: steps.length,
101
+ tokensIn: 0,
102
+ tokensOut: 0,
103
+ wallMs: Date.now() - startedAt,
104
+ };
105
+ // Phase 1 never attaches a goal — that's Phase 2.
106
+ return { goal: null, phase, action, metrics };
107
+ }
108
+ /** Read the currently-active goal from disk, or null if none is set. */
109
+ async loadGoal() {
110
+ return getActive(this.stateDir);
111
+ }
112
+ /**
113
+ * Create a new goal on disk and mark it active.
114
+ * Fields the caller omits are filled with sensible defaults.
115
+ */
116
+ async createGoal(spec = {}) {
117
+ const now = new Date().toISOString();
118
+ const goal = {
119
+ id: spec.id ?? cryptoUlid(),
120
+ title: spec.title ?? '(untitled goal)',
121
+ intent: spec.intent ?? '',
122
+ acceptance: spec.acceptance ?? [],
123
+ createdAt: spec.createdAt ?? now,
124
+ updatedAt: spec.updatedAt ?? now,
125
+ status: spec.status ?? 'active',
126
+ tags: spec.tags,
127
+ };
128
+ await writeGoal(this.stateDir, goal);
129
+ await setActive(this.stateDir, goal.id);
130
+ return goal;
131
+ }
132
+ /** Activate an existing goal by id. Throws if the goal does not exist. */
133
+ async setGoal(goalId) {
134
+ const existing = await readGoal(this.stateDir, goalId);
135
+ if (!existing) {
136
+ throw new Error(`setGoal: goal ${goalId} not found under ${this.stateDir}`);
137
+ }
138
+ await setActive(this.stateDir, goalId);
139
+ }
140
+ }
141
+ //# sourceMappingURL=session-planner.js.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Hierarchical Planner — Type Definitions
3
+ *
4
+ * Four tiers, increasing temporal resolution:
5
+ * Tier 1 SessionGoal days → weeks (Opus, rarely re-planned)
6
+ * Tier 2 Phase hours (Opus, on scope shift)
7
+ * Tier 3 Action minutes (Sonnet, per user turn)
8
+ * Tier 4 ToolCallSpec seconds (Haiku, per tool call)
9
+ *
10
+ * Inspired by Suno's 3-stage transformer (semantic → coarse acoustic → fine
11
+ * acoustic). Coarse intent is stable; fine actuation is cheap and rewritten.
12
+ *
13
+ * See DESIGN.md in this directory for the full rationale, cost model,
14
+ * decision logic, and integration plan. This file is types-only — no imports
15
+ * from heavy runtime modules, no implementation.
16
+ */
17
+ import type { PlanStep } from '../../planner.js';
18
+ /** Long-lived user objective that spans many turns and possibly many sessions. */
19
+ export interface SessionGoal {
20
+ /** Stable identifier (uuid). Persists across sessions. */
21
+ id: string;
22
+ /** Short title: "ship hierarchical planner v1". */
23
+ title: string;
24
+ /** 1–3 sentence rationale: what success looks like. */
25
+ intent: string;
26
+ /** Acceptance criteria — bullet list the user would agree closes the goal. */
27
+ acceptance: string[];
28
+ /** ISO timestamps. */
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ /** Lifecycle. `paused` goals stay on disk but don't get re-planned. */
32
+ status: 'active' | 'paused' | 'completed' | 'abandoned';
33
+ /** Free-form tags: repo name, domain, user-supplied label. */
34
+ tags?: string[];
35
+ }
36
+ /** Coarse mode the agent is currently operating in. */
37
+ export type PhaseKind = 'explore' | 'build' | 'debug' | 'review' | 'write' | 'refactor' | 'deploy' | 'other';
38
+ /** A contiguous stretch of work under one mode, toward one milestone. */
39
+ export interface Phase {
40
+ id: string;
41
+ /** Parent goal. */
42
+ goalId: string;
43
+ kind: PhaseKind;
44
+ /** What this phase commits to producing. */
45
+ objective: string;
46
+ /** Exit criteria — when `kind` should flip or phase should close. */
47
+ exitCriteria: string[];
48
+ /** Files or subsystems scoped in. */
49
+ scope?: string[];
50
+ /** ISO timestamps. */
51
+ startedAt: string;
52
+ endedAt?: string;
53
+ status: 'active' | 'done' | 'aborted';
54
+ }
55
+ /**
56
+ * One action = one user turn's plan. Steps are the existing `PlanStep`s so we
57
+ * stay compatible with `planner.ts#executePlan`.
58
+ */
59
+ export interface ActionStep extends PlanStep {
60
+ }
61
+ export interface Action {
62
+ id: string;
63
+ phaseId: string;
64
+ /** Verbatim user turn that triggered this action. */
65
+ userTurn: string;
66
+ /** One-sentence plan. */
67
+ summary: string;
68
+ /** Ordered steps; each step is a PlanStep-compatible record. */
69
+ steps: ActionStep[];
70
+ /** Agents this action expects to consult (from learned-router). */
71
+ expectedAgents?: string[];
72
+ createdAt: string;
73
+ status: 'pending' | 'running' | 'done' | 'failed';
74
+ }
75
+ /** Coarse hazard class used to gate permissions and verdict logic. */
76
+ export type SideEffectClass = 'pure' | 'read' | 'write' | 'exec' | 'network' | 'destructive' | 'external';
77
+ /** The final low-level tool call. Haiku fills this in. */
78
+ export interface ToolCallSpec {
79
+ id: string;
80
+ actionId: string;
81
+ stepId: number;
82
+ tool: string;
83
+ args: Record<string, unknown>;
84
+ sideEffect: SideEffectClass;
85
+ /** Optional prediction of what the tool should return on success. */
86
+ expectedOutcome?: string;
87
+ /** Hard ceiling in ms; falls back to pipeline default when absent. */
88
+ timeoutMs?: number;
89
+ }
90
+ export type VerdictDecision = 'continue' | 'revise-action' | 'revise-phase' | 'revise-goal' | 'abort';
91
+ /** Emitted after every tool call; consumed by the up-delegation ladder. */
92
+ export interface TierVerdict {
93
+ decision: VerdictDecision;
94
+ tier: 'tool' | 'action' | 'phase' | 'goal';
95
+ reason: string;
96
+ /** Evidence: tool error, failed assertion, diff summary. */
97
+ evidence?: string;
98
+ }
99
+ export interface TurnMetrics {
100
+ tier1Calls: number;
101
+ tier2Calls: number;
102
+ tier3Calls: number;
103
+ tier4Calls: number;
104
+ tokensIn: number;
105
+ tokensOut: number;
106
+ wallMs: number;
107
+ }
108
+ /** Top-level return of `HierarchicalPlanner.planTurn`. */
109
+ export interface PlannerResult {
110
+ goal: SessionGoal;
111
+ phase: Phase;
112
+ action: Action;
113
+ verdicts: TierVerdict[];
114
+ metrics: TurnMetrics;
115
+ }
116
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Hierarchical Planner — Type Definitions
3
+ *
4
+ * Four tiers, increasing temporal resolution:
5
+ * Tier 1 SessionGoal days → weeks (Opus, rarely re-planned)
6
+ * Tier 2 Phase hours (Opus, on scope shift)
7
+ * Tier 3 Action minutes (Sonnet, per user turn)
8
+ * Tier 4 ToolCallSpec seconds (Haiku, per tool call)
9
+ *
10
+ * Inspired by Suno's 3-stage transformer (semantic → coarse acoustic → fine
11
+ * acoustic). Coarse intent is stable; fine actuation is cheap and rewritten.
12
+ *
13
+ * See DESIGN.md in this directory for the full rationale, cost model,
14
+ * decision logic, and integration plan. This file is types-only — no imports
15
+ * from heavy runtime modules, no implementation.
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=types.js.map
@@ -57,6 +57,40 @@ export declare function truncationMiddleware(maxSize?: number): ToolMiddleware;
57
57
  * Emits tool_call_start and tool_call_end events.
58
58
  */
59
59
  export declare function telemetryMiddleware(emit: (event: string, data: any) => void): ToolMiddleware;
60
+ /**
61
+ * Thresholds for outcome classification.
62
+ * Exported so downstream code (training, analytics) can mirror them.
63
+ */
64
+ export declare const OUTCOME_EMPTY_THRESHOLD = 5;
65
+ export declare const OUTCOME_LARGE_THRESHOLD = 10240;
66
+ /**
67
+ * Classify a tool execution outcome from its ToolContext.
68
+ *
69
+ * Priority:
70
+ * 1. timeout — aborted with timeout reason
71
+ * 2. error — ctx.error is set (or aborted for any other reason)
72
+ * 3. empty — result present but shorter than OUTCOME_EMPTY_THRESHOLD chars
73
+ * 4. large — result byte length > OUTCOME_LARGE_THRESHOLD
74
+ * 5. success — anything else
75
+ */
76
+ export declare function classifyOutcome(ctx: ToolContext): 'success' | 'error' | 'timeout' | 'empty' | 'large';
77
+ /**
78
+ * Observer middleware — writes an observation to ~/.kbot/observer/session.jsonl.
79
+ *
80
+ * Records the three fields that cannot be backfilled for action-token training:
81
+ * - durationMs: wall-clock time of tool execution
82
+ * - outcome: success | error | timeout | empty | large
83
+ * - resultSize: byte length of the serialized result
84
+ *
85
+ * Emits schema v2 events. Backward compatible — consumers that don't know
86
+ * about the new fields will ignore them (the tokenizer has fallbacks).
87
+ *
88
+ * Place this as the OUTERMOST middleware so duration captures the true
89
+ * wall-clock of the full pipeline (including timeout, truncation, fallback).
90
+ */
91
+ export declare function observerMiddleware(sessionId: string, options?: {
92
+ enabled?: () => boolean;
93
+ }): ToolMiddleware;
60
94
  /**
61
95
  * Execution middleware — the actual tool call.
62
96
  * This should be the last middleware in the pipeline.
@@ -100,7 +134,7 @@ export declare function fallbackMiddleware(rules: FallbackRule[], execute: (name
100
134
  export declare function resourceAwareMiddleware(): ToolMiddleware;
101
135
  /**
102
136
  * Create the default pipeline with the standard middleware stack.
103
- * Order: telemetry? → permission → hooks → resource → metrics → timeout → truncation → fallback? → execution
137
+ * Order: observer? → telemetry? → permission → hooks → resource → metrics → timeout → truncation → fallback? → execution
104
138
  */
105
139
  export declare function createDefaultPipeline(deps: {
106
140
  checkPermission: (name: string, args: any) => Promise<boolean>;
@@ -116,5 +150,9 @@ export declare function createDefaultPipeline(deps: {
116
150
  recordMetrics: (name: string, duration: number, error?: string) => void;
117
151
  emit?: (event: string, data: any) => void;
118
152
  fallbackRules?: FallbackRule[];
153
+ /** If set, observerMiddleware is added as the outermost layer and writes to ~/.kbot/observer/session.jsonl. */
154
+ observerSessionId?: string;
155
+ /** Runtime gate for observer writes. */
156
+ observerEnabled?: () => boolean;
119
157
  }): ToolPipeline;
120
158
  //# sourceMappingURL=tool-pipeline.d.ts.map
@@ -162,6 +162,111 @@ export function telemetryMiddleware(emit) {
162
162
  });
163
163
  };
164
164
  }
165
+ // ── Outcome classification ──
166
+ /**
167
+ * Thresholds for outcome classification.
168
+ * Exported so downstream code (training, analytics) can mirror them.
169
+ */
170
+ export const OUTCOME_EMPTY_THRESHOLD = 5; // result under this many chars → "empty"
171
+ export const OUTCOME_LARGE_THRESHOLD = 10_240; // result over this many bytes → "large"
172
+ /**
173
+ * Classify a tool execution outcome from its ToolContext.
174
+ *
175
+ * Priority:
176
+ * 1. timeout — aborted with timeout reason
177
+ * 2. error — ctx.error is set (or aborted for any other reason)
178
+ * 3. empty — result present but shorter than OUTCOME_EMPTY_THRESHOLD chars
179
+ * 4. large — result byte length > OUTCOME_LARGE_THRESHOLD
180
+ * 5. success — anything else
181
+ */
182
+ export function classifyOutcome(ctx) {
183
+ // Timeout takes precedence over generic error because the error string is set too.
184
+ if (ctx.aborted && /timed?\s*out|timeout/i.test(ctx.abortReason ?? ctx.error ?? '')) {
185
+ return 'timeout';
186
+ }
187
+ if (ctx.error || ctx.aborted)
188
+ return 'error';
189
+ const result = ctx.result ?? '';
190
+ if (result.length < OUTCOME_EMPTY_THRESHOLD)
191
+ return 'empty';
192
+ if (Buffer.byteLength(result, 'utf8') > OUTCOME_LARGE_THRESHOLD)
193
+ return 'large';
194
+ return 'success';
195
+ }
196
+ /**
197
+ * Observer middleware — writes an observation to ~/.kbot/observer/session.jsonl.
198
+ *
199
+ * Records the three fields that cannot be backfilled for action-token training:
200
+ * - durationMs: wall-clock time of tool execution
201
+ * - outcome: success | error | timeout | empty | large
202
+ * - resultSize: byte length of the serialized result
203
+ *
204
+ * Emits schema v2 events. Backward compatible — consumers that don't know
205
+ * about the new fields will ignore them (the tokenizer has fallbacks).
206
+ *
207
+ * Place this as the OUTERMOST middleware so duration captures the true
208
+ * wall-clock of the full pipeline (including timeout, truncation, fallback).
209
+ */
210
+ export function observerMiddleware(sessionId, options = {}) {
211
+ return async (ctx, next) => {
212
+ const start = Date.now();
213
+ let threw = undefined;
214
+ try {
215
+ await next();
216
+ }
217
+ catch (err) {
218
+ // Capture so we can still log; rethrow after.
219
+ threw = err;
220
+ if (!ctx.error)
221
+ ctx.error = err instanceof Error ? err.message : String(err);
222
+ }
223
+ const durationMs = Date.now() - start;
224
+ // Allow runtime opt-out (e.g. user disabled observer).
225
+ if (options.enabled && !options.enabled()) {
226
+ if (threw)
227
+ throw threw;
228
+ return;
229
+ }
230
+ const result = ctx.result ?? '';
231
+ const resultSize = Buffer.byteLength(result, 'utf8');
232
+ const outcome = classifyOutcome(ctx);
233
+ try {
234
+ const { recordObservation } = await import('./observer.js');
235
+ // Extract a few "safe" arg fields for indexing — mirror what agent.ts does.
236
+ const a = (ctx.toolArgs ?? {});
237
+ const args = {};
238
+ if (typeof a.file_path === 'string')
239
+ args.file_path = a.file_path;
240
+ else if (typeof a.path === 'string')
241
+ args.path = a.path;
242
+ if (typeof a.command === 'string')
243
+ args.command = a.command.slice(0, 200);
244
+ if (typeof a.pattern === 'string')
245
+ args.pattern = a.pattern;
246
+ if (typeof a.url === 'string')
247
+ args.url = a.url;
248
+ if (typeof a.query === 'string')
249
+ args.query = a.query;
250
+ recordObservation({
251
+ schema: 2,
252
+ ts: new Date(start).toISOString(),
253
+ tool: ctx.toolName,
254
+ args,
255
+ result_length: result.length,
256
+ session: sessionId,
257
+ error: Boolean(ctx.error),
258
+ durationMs,
259
+ outcome,
260
+ resultSize,
261
+ });
262
+ }
263
+ catch {
264
+ /* observer is non-critical — never break tool execution */
265
+ }
266
+ if (threw)
267
+ throw threw;
268
+ };
269
+ }
165
270
  /**
166
271
  * Execution middleware — the actual tool call.
167
272
  * This should be the last middleware in the pipeline.
@@ -313,10 +418,13 @@ export function resourceAwareMiddleware() {
313
418
  }
314
419
  /**
315
420
  * Create the default pipeline with the standard middleware stack.
316
- * Order: telemetry? → permission → hooks → resource → metrics → timeout → truncation → fallback? → execution
421
+ * Order: observer? → telemetry? → permission → hooks → resource → metrics → timeout → truncation → fallback? → execution
317
422
  */
318
423
  export function createDefaultPipeline(deps) {
319
424
  const pipeline = new ToolPipeline();
425
+ if (deps.observerSessionId) {
426
+ pipeline.use(observerMiddleware(deps.observerSessionId, { enabled: deps.observerEnabled }));
427
+ }
320
428
  if (deps.emit) {
321
429
  pipeline.use(telemetryMiddleware(deps.emit));
322
430
  }
@@ -0,0 +1,2 @@
1
+ export declare function registerAbletonListenTool(): void;
2
+ //# sourceMappingURL=ableton-listen.d.ts.map
@@ -0,0 +1,126 @@
1
+ // ableton_listen — subscribe to real-time LOM events via kbot-control.
2
+ //
3
+ // AbletonOSC has no listener API. kbot-control implements listeners using
4
+ // native Max LiveAPI property callbacks, streamed over the TCP JSON-RPC
5
+ // connection as "notify" messages. This tool exposes that capability to
6
+ // the agent so it can subscribe to beat, playing_position, parameter
7
+ // changes, etc., and pull a buffered history for inspection.
8
+ import { registerTool } from './index.js';
9
+ import { KbotControlClient } from '../integrations/kbot-control-client.js';
10
+ // Per-path ring buffer of recent events so the agent can poll history.
11
+ const MAX_PER_PATH = 100;
12
+ const history = new Map();
13
+ const subscriptions = new Map();
14
+ function recordEvent(path, value) {
15
+ let buf = history.get(path);
16
+ if (!buf) {
17
+ buf = [];
18
+ history.set(path, buf);
19
+ }
20
+ buf.push({ path, value, at: Date.now() });
21
+ if (buf.length > MAX_PER_PATH)
22
+ buf.shift();
23
+ }
24
+ export function registerAbletonListenTool() {
25
+ registerTool({
26
+ name: 'ableton_listen',
27
+ description: 'Subscribe to real-time Ableton events via kbot-control.amxd. ' +
28
+ 'Subscriptions stream over the persistent TCP connection and the tool buffers the last 100 events per path. ' +
29
+ 'Use action="subscribe" with a LOM-ish path (e.g. "song.is_playing", "song.tempo", "tracks[0].output_meter_left"), ' +
30
+ 'then action="history" to pull what came in. action="list" shows active subscriptions. ' +
31
+ 'Requires kbot-control.amxd loaded in Ableton.',
32
+ parameters: {
33
+ action: {
34
+ type: 'string',
35
+ description: '"subscribe" | "unsubscribe" | "list" | "history" | "clear"',
36
+ required: true,
37
+ },
38
+ path: {
39
+ type: 'string',
40
+ description: 'LOM path for subscribe/unsubscribe/history. Examples: "song.is_playing", "song.tempo", "song.current_song_time", "tracks[0].mute".',
41
+ },
42
+ limit: {
43
+ type: 'number',
44
+ description: 'For "history": max events to return (default 25).',
45
+ },
46
+ },
47
+ tier: 'free',
48
+ timeout: 15_000,
49
+ async execute(args) {
50
+ const action = String(args.action || '').toLowerCase();
51
+ const path = args.path !== undefined ? String(args.path) : '';
52
+ try {
53
+ switch (action) {
54
+ case 'subscribe': {
55
+ if (!path)
56
+ return 'Error: subscribe needs a path.';
57
+ if (subscriptions.has(path))
58
+ return `Already subscribed to ${path}`;
59
+ const handler = (value) => recordEvent(path, value);
60
+ await KbotControlClient.get().subscribe(path, handler);
61
+ subscriptions.set(path, handler);
62
+ return `Subscribed to \`${path}\`. Events will be recorded; call action="history" to pull them.`;
63
+ }
64
+ case 'unsubscribe': {
65
+ if (!path)
66
+ return 'Error: unsubscribe needs a path.';
67
+ const handler = subscriptions.get(path);
68
+ if (!handler)
69
+ return `Not subscribed to ${path}`;
70
+ await KbotControlClient.get().unsubscribe(path, handler);
71
+ subscriptions.delete(path);
72
+ return `Unsubscribed from ${path}`;
73
+ }
74
+ case 'list': {
75
+ if (subscriptions.size === 0)
76
+ return 'No active subscriptions.';
77
+ const lines = ['## Active subscriptions', ''];
78
+ for (const p of subscriptions.keys()) {
79
+ const buf = history.get(p);
80
+ lines.push(`- \`${p}\` — ${buf ? buf.length : 0} events buffered`);
81
+ }
82
+ return lines.join('\n');
83
+ }
84
+ case 'history': {
85
+ if (!path) {
86
+ // Return summary of all paths
87
+ if (history.size === 0)
88
+ return 'No events recorded yet.';
89
+ const lines = ['## Event history summary', ''];
90
+ for (const [p, buf] of history.entries()) {
91
+ const last = buf[buf.length - 1];
92
+ lines.push(`- \`${p}\`: ${buf.length} events, last value = ${JSON.stringify(last?.value)}`);
93
+ }
94
+ return lines.join('\n');
95
+ }
96
+ const buf = history.get(path);
97
+ if (!buf || buf.length === 0)
98
+ return `No events recorded for \`${path}\`.`;
99
+ const limit = Number(args.limit) || 25;
100
+ const recent = buf.slice(-limit);
101
+ const lines = [`## \`${path}\` — last ${recent.length} events`, ''];
102
+ for (const ev of recent) {
103
+ const dt = new Date(ev.at).toISOString().slice(11, 23);
104
+ lines.push(`- ${dt} → ${JSON.stringify(ev.value)}`);
105
+ }
106
+ return lines.join('\n');
107
+ }
108
+ case 'clear': {
109
+ if (path) {
110
+ history.delete(path);
111
+ return `Cleared history for ${path}`;
112
+ }
113
+ history.clear();
114
+ return 'Cleared all history';
115
+ }
116
+ default:
117
+ return `Unknown action "${action}". Options: subscribe, unsubscribe, list, history, clear`;
118
+ }
119
+ }
120
+ catch (e) {
121
+ return `ableton_listen error: ${e.message}`;
122
+ }
123
+ },
124
+ });
125
+ }
126
+ //# sourceMappingURL=ableton-listen.js.map