@kernel.chat/kbot 3.97.0 → 3.97.4

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 CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  <p align="center">
4
4
  <strong>kbot</strong><br>
5
- Open-source terminal AI agent. 764+ tools. 35 agents. 20 providers. Dreams, learns, watches your system, controls your phone. $0 local.
5
+ Open-source terminal AI agent. 787+ tools. 35 agents. 20 providers. Dreams, learns, watches your system, controls your phone. $0 local.
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo.gif" alt="kbot demo" width="700">
9
+ <img src="https://raw.githubusercontent.com/isaacsight/kernel/main/tools/video-assets/demo-quick.gif" alt="kbot demo" width="700">
10
10
  </p>
11
11
 
12
12
  <p align="center">
@@ -31,7 +31,7 @@ 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).
34
- - **764+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, music production, iPhone control, and more.
34
+ - **787+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, music production, iPhone control, and more.
35
35
  - **Programmatic SDK** — use kbot as a library in your own apps.
36
36
  - **MCP server built in** — plug kbot into Claude Code, Cursor, VS Code, Zed, or Neovim as a tool provider.
37
37
 
@@ -142,7 +142,7 @@ Checks security, documentation, code quality, CI/CD, community health, and DevOp
142
142
  |---|---|---|---|---|---|
143
143
  | AI providers | 20 | 1 | 1 | 6 | 75+ |
144
144
  | Specialist agents | 35 | 0 | 0 | 0 | 0 |
145
- | Built-in tools | 764+ | ~20 | ~15 | ~10 | ~15 |
145
+ | Built-in tools | 787+ | ~20 | ~15 | ~10 | ~15 |
146
146
  | Science tools | 114 | 0 | 0 | 0 | 0 |
147
147
  | Memory system | 7-tier bidirectional | File-based | No | No | No |
148
148
  | Dream engine | Yes ($0 local) | Cloud API | No | No | No |
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 · 360+ tools · 20 providers · MIT licensed')}`);
105
+ console.log(` ${chalk.dim('35 specialist agents · 787+ tools · 20 providers · MIT licensed')}`);
106
106
  console.log();
107
107
  process.exit(0);
108
108
  });
@@ -129,4 +129,36 @@ export declare class IntelligenceCoordinator {
129
129
  export declare function getCoordinator(): IntelligenceCoordinator;
130
130
  /** Register coordinator tools with the kbot tool registry */
131
131
  export declare function registerCoordinatorTools(): void;
132
+ export interface Task {
133
+ id: string;
134
+ goal: string;
135
+ status: 'pending' | 'running' | 'done' | 'failed';
136
+ agent: string;
137
+ dependencies: string[];
138
+ result?: string;
139
+ error?: string;
140
+ }
141
+ export interface CoordinatorPlan {
142
+ id: string;
143
+ goal: string;
144
+ tasks: Task[];
145
+ createdAt: string;
146
+ completedAt?: string;
147
+ status: 'planning' | 'executing' | 'done' | 'failed';
148
+ }
149
+ /**
150
+ * Decompose a high-level goal into ordered sub-tasks with dependencies.
151
+ * Uses the LLM (via runAgent) to break the goal into 2-6 concrete tasks.
152
+ */
153
+ export declare function decompose(goal: string): Promise<CoordinatorPlan>;
154
+ /**
155
+ * Execute a plan's tasks in dependency order (sequential for now).
156
+ * Tasks whose dependencies are all 'done' are eligible to run.
157
+ */
158
+ export declare function execute(plan: CoordinatorPlan): Promise<CoordinatorPlan>;
159
+ /**
160
+ * Convenience: decompose a goal, execute the plan, and synthesize results.
161
+ * Returns a human-readable summary of what was accomplished.
162
+ */
163
+ export declare function coordinate(goal: string): Promise<string>;
132
164
  //# sourceMappingURL=coordinator.d.ts.map
@@ -8,6 +8,7 @@
8
8
  import { homedir } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
11
+ import chalk from 'chalk';
11
12
  // ── Lazy module loaders (dynamic imports to break circular deps) ──
12
13
  async function getLearning() { return import('./learning.js'); }
13
14
  async function getConfidence() { return import('./confidence.js'); }
@@ -673,10 +674,166 @@ export function registerCoordinatorTools() {
673
674
  return JSON.stringify(result, null, 2);
674
675
  },
675
676
  });
677
+ registerTool({
678
+ name: 'coordinator_orchestrate',
679
+ description: 'Decompose a goal into sub-tasks, assign specialist agents, execute in dependency order, and synthesize results',
680
+ parameters: { goal: { type: 'string', description: 'The high-level goal to orchestrate', required: true } },
681
+ tier: 'free',
682
+ execute: async (args) => coordinate(args.goal),
683
+ });
676
684
  }).catch(() => {
677
685
  // tools/index.js not available — skip registration
678
686
  });
679
687
  }
688
+ // ── Goal Decomposition & Multi-Agent Orchestration ──
689
+ //
690
+ // Takes a high-level goal and:
691
+ // 1. Decomposes it into ordered sub-tasks via LLM
692
+ // 2. Assigns each sub-task to the best specialist agent
693
+ // 3. Manages dependencies between sub-tasks
694
+ // 4. Synthesizes results into a final output
695
+ // 5. Learns from the execution for next time
696
+ const COORD_AMETHYST = typeof chalk.hex === 'function' ? chalk.hex('#6B5B95') : ((s) => s);
697
+ const DECOMPOSE_PROMPT = `You are a task decomposition engine. Break the goal into 2-6 concrete sub-tasks.
698
+ Output ONLY valid JSON: {"tasks":[{"id":"t1","goal":"...","agent":"agent_id","dependencies":[]}]}
699
+ Agents: kernel (general), coder (code/debug), researcher (research), writer (docs), analyst (strategy), guardian (security), infrastructure (devops).
700
+ Rules: each task independently verifiable, use dependencies for ordering, assign best agent, one objective per task.`;
701
+ /**
702
+ * Decompose a high-level goal into ordered sub-tasks with dependencies.
703
+ * Uses the LLM (via runAgent) to break the goal into 2-6 concrete tasks.
704
+ */
705
+ export async function decompose(goal) {
706
+ const planId = shortId();
707
+ const plan = {
708
+ id: planId,
709
+ goal,
710
+ tasks: [],
711
+ createdAt: new Date().toISOString(),
712
+ status: 'planning',
713
+ };
714
+ process.stderr.write(` ${COORD_AMETHYST('◆ decompose')} ${chalk.dim(goal.slice(0, 80))}${goal.length > 80 ? '...' : ''}\n`);
715
+ try {
716
+ // Lazy import to avoid circular dependency
717
+ const { runAgent } = await import('./agent.js');
718
+ const response = await runAgent(`${DECOMPOSE_PROMPT}\n\nGoal: ${goal}\n\nOutput JSON:`, { agent: 'kernel', skipPlanner: true, sessionId: `coord-${planId}` });
719
+ // Parse JSON from response
720
+ const jsonMatch = response.content.match(/\{[\s\S]*\}/);
721
+ if (jsonMatch) {
722
+ const parsed = JSON.parse(jsonMatch[0]);
723
+ plan.tasks = (parsed.tasks || []).slice(0, 6).map(t => ({
724
+ id: t.id || shortId(),
725
+ goal: t.goal,
726
+ status: 'pending',
727
+ agent: t.agent || 'kernel',
728
+ dependencies: t.dependencies || [],
729
+ }));
730
+ }
731
+ }
732
+ catch (err) {
733
+ process.stderr.write(` ${chalk.red('✗')} decomposition failed: ${err instanceof Error ? err.message : String(err)}\n`);
734
+ }
735
+ // Fallback: if decomposition produced nothing, create a single task
736
+ if (plan.tasks.length === 0) {
737
+ plan.tasks = [{
738
+ id: 't1',
739
+ goal,
740
+ status: 'pending',
741
+ agent: 'kernel',
742
+ dependencies: [],
743
+ }];
744
+ }
745
+ plan.status = 'executing';
746
+ for (const task of plan.tasks) {
747
+ const deps = task.dependencies.length > 0 ? chalk.dim(` -> ${task.dependencies.join(',')}`) : '';
748
+ process.stderr.write(` ${chalk.dim('│')} ${chalk.cyan(task.id)} ${task.goal} ${chalk.magenta(`@${task.agent}`)}${deps}\n`);
749
+ }
750
+ return plan;
751
+ }
752
+ /**
753
+ * Execute a plan's tasks in dependency order (sequential for now).
754
+ * Tasks whose dependencies are all 'done' are eligible to run.
755
+ */
756
+ export async function execute(plan) {
757
+ const { runAgent } = await import('./agent.js');
758
+ const completed = new Set();
759
+ process.stderr.write(`\n ${COORD_AMETHYST('◆ execute')} ${plan.tasks.length} tasks\n`);
760
+ while (completed.size < plan.tasks.length) {
761
+ // Find tasks whose dependencies are all satisfied
762
+ const ready = plan.tasks.filter(t => t.status === 'pending' &&
763
+ t.dependencies.every(dep => completed.has(dep)));
764
+ if (ready.length === 0) {
765
+ // Remaining tasks have unmet deps — mark them failed
766
+ for (const t of plan.tasks) {
767
+ if (t.status === 'pending') {
768
+ t.status = 'failed';
769
+ t.error = 'Unmet dependencies (earlier tasks failed)';
770
+ completed.add(t.id);
771
+ }
772
+ }
773
+ break;
774
+ }
775
+ // Execute ready tasks sequentially (parallel execution can come later)
776
+ for (const task of ready) {
777
+ task.status = 'running';
778
+ process.stderr.write(` ${chalk.dim('├')} ${chalk.yellow('●')} ${task.id}: ${task.goal}\n`);
779
+ // Gather dependency results as context
780
+ const depCtx = task.dependencies
781
+ .map(id => { const d = plan.tasks.find(t => t.id === id); return d?.result ? `[${id}]: ${d.result.slice(0, 500)}` : ''; })
782
+ .filter(Boolean).join('\n');
783
+ const prompt = `You are executing a sub-task of a larger plan.${depCtx ? `\n\nPrevious results:\n${depCtx}` : ''}\n\nYour task: ${task.goal}\n\nExecute this now.`;
784
+ try {
785
+ const response = await runAgent(prompt, {
786
+ agent: task.agent,
787
+ skipPlanner: true,
788
+ sessionId: `coord-${plan.id}-${task.id}`,
789
+ });
790
+ task.result = response.content;
791
+ task.status = 'done';
792
+ process.stderr.write(` ${chalk.dim('│')} ${chalk.green('✓')} ${task.id} done\n`);
793
+ }
794
+ catch (err) {
795
+ task.status = 'failed';
796
+ task.error = err instanceof Error ? err.message : String(err);
797
+ process.stderr.write(` ${chalk.dim('│')} ${chalk.red('✗')} ${task.id} failed: ${task.error}\n`);
798
+ }
799
+ completed.add(task.id);
800
+ }
801
+ }
802
+ // Determine final plan status
803
+ const failed = plan.tasks.filter(t => t.status === 'failed');
804
+ plan.status = failed.length === 0 ? 'done' : 'failed';
805
+ plan.completedAt = new Date().toISOString();
806
+ const done = plan.tasks.filter(t => t.status === 'done').length;
807
+ process.stderr.write(` ${chalk.dim('└')} ${done}/${plan.tasks.length} tasks succeeded\n`);
808
+ // Record outcome for learning
809
+ try {
810
+ const coord = getCoordinator();
811
+ if (plan.status === 'done')
812
+ coord.addGoal(plan.goal, 0.7).status = 'completed';
813
+ const learning = await getLearning();
814
+ learning.learnFromExchange(`[coordinator] ${plan.goal}`, `${done}/${plan.tasks.length} done`, plan.tasks.map(t => t.agent));
815
+ }
816
+ catch { /* non-critical */ }
817
+ return plan;
818
+ }
819
+ /**
820
+ * Convenience: decompose a goal, execute the plan, and synthesize results.
821
+ * Returns a human-readable summary of what was accomplished.
822
+ */
823
+ export async function coordinate(goal) {
824
+ const plan = await decompose(goal);
825
+ const executed = await execute(plan);
826
+ // Synthesize results
827
+ const results = executed.tasks
828
+ .filter(t => t.status === 'done' && t.result)
829
+ .map(t => `## ${t.goal}\n${t.result}`)
830
+ .join('\n\n');
831
+ const failed = executed.tasks.filter(t => t.status === 'failed');
832
+ const failSummary = failed.length > 0
833
+ ? `\n\n---\n${failed.length} task(s) failed:\n${failed.map(t => `- ${t.goal}: ${t.error}`).join('\n')}`
834
+ : '';
835
+ return results + failSummary;
836
+ }
680
837
  // Auto-register tools when this module is imported
681
838
  registerCoordinatorTools();
682
839
  //# sourceMappingURL=coordinator.js.map
package/dist/doctor.js CHANGED
@@ -518,11 +518,12 @@ export async function runDoctor() {
518
518
  // ── Formatter ──
519
519
  // Color palette matching ui.ts
520
520
  const useColor = !process.env.NO_COLOR && process.stdout.isTTY !== false;
521
- const GREEN = useColor ? chalk.hex('#4ADE80') : chalk;
522
- const RED = useColor ? chalk.hex('#F87171') : chalk;
523
- const YELLOW = useColor ? chalk.hex('#FBBF24') : chalk;
521
+ const hex = typeof chalk.hex === 'function' ? (c) => chalk.hex(c) : () => ((s) => s);
522
+ const GREEN = useColor ? hex('#4ADE80') : ((s) => s);
523
+ const RED = useColor ? hex('#F87171') : ((s) => s);
524
+ const YELLOW = useColor ? hex('#FBBF24') : ((s) => s);
524
525
  const DIM = useColor ? chalk.dim : ((s) => s);
525
- const ACCENT = useColor ? chalk.hex('#A78BFA') : chalk;
526
+ const ACCENT = useColor ? hex('#A78BFA') : ((s) => s);
526
527
  const STATUS_ICON = {
527
528
  pass: GREEN('✓'),
528
529
  warn: YELLOW('!'),
package/dist/share.js CHANGED
@@ -25,7 +25,7 @@ export function formatShareMarkdown(turns, meta) {
25
25
  const lines = [
26
26
  `# ${name}`,
27
27
  '',
28
- `> Generated with [kbot](${KBOT_URL}) — 25 specialist agents, 290+ tools, 20 AI providers`,
28
+ `> Generated with [kbot](${KBOT_URL}) — 35 specialist agents, 787+ tools, 20 AI providers`,
29
29
  `> Agent: \`${agent}\` | Date: ${date}`,
30
30
  '',
31
31
  '---',
package/dist/streaming.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // The thinking display shows the AI's reasoning process in real-time,
8
8
  // then the final response renders normally.
9
9
  import chalk from 'chalk';
10
- const ACCENT_DIM = chalk.hex('#7C6CB0');
10
+ const ACCENT_DIM = typeof chalk.hex === 'function' ? chalk.hex('#7C6CB0') : chalk.dim;
11
11
  const THINKING_COLOR = chalk.dim.italic;
12
12
  /** Max accumulated content size during streaming (5MB) to prevent OOM */
13
13
  const MAX_STREAM_CONTENT = 5 * 1024 * 1024;
@@ -11,10 +11,12 @@
11
11
  // ableton_mixer — snapshot levels, batch set, sends
12
12
  // ableton_create_progression — chord progressions → MIDI in clips
13
13
  // ableton_session_info — full session state snapshot
14
+ // ableton_audio_analysis — real-time audio level meters (track + master RMS)
14
15
  // ableton_knowledge — deep Ableton knowledge base queries (registered in ableton-knowledge.ts)
15
16
  //
16
17
  // Requires: AbletonOSC loaded in Ableton Live (Preferences → Link/Tempo/MIDI → Control Surface)
17
18
  import { registerTool } from './index.js';
19
+ import { execSync } from 'node:child_process';
18
20
  import { ensureAbleton, formatAbletonError } from '../integrations/ableton-osc.js';
19
21
  import { parseProgression, voiceChord, arpeggiate, NAMED_PROGRESSIONS, RHYTHM_PATTERNS, GENRE_DRUM_PATTERNS, noteNameToMidi, midiToNoteName, } from './music-theory.js';
20
22
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -769,52 +771,89 @@ export function registerAbletonTools() {
769
771
  // ─── 10. Load Plugin ──────────────────────────────────────────────────
770
772
  registerTool({
771
773
  name: 'ableton_load_plugin',
772
- description: 'Load any instrument or plugin onto a track by name — native (Operator, Wavetable, Drift) or third-party VST/AU (Serum 2, Vital, Kontakt). Searches Ableton\'s browser and loads onto the selected track.',
774
+ description: 'Load any instrument or plugin onto a track by name — native (Operator, Wavetable, Drift) or third-party VST/AU (Serum 2, Vital, Kontakt). Tries OSC first, then falls back to AppleScript browser automation on macOS.',
773
775
  parameters: {
774
776
  track: { type: 'number', description: 'Track number (1-based)', required: true },
775
777
  plugin: { type: 'string', description: 'Plugin name to search for (e.g. "Serum 2", "Operator", "Wavetable")', required: true },
776
778
  manufacturer: { type: 'string', description: 'Manufacturer name for VST/AU (e.g. "Xfer Records", "Native Instruments"). Optional — helps narrow search.' },
779
+ skip_results: { type: 'number', description: 'Number of Down arrow presses before selecting (to skip past FX/presets). Default: 1' },
777
780
  },
778
781
  tier: 'free',
779
- timeout: 15_000,
782
+ timeout: 20_000,
780
783
  async execute(args) {
781
784
  const t = userTrack(args.track);
782
785
  const plugin = String(args.plugin);
783
786
  const manufacturer = args.manufacturer ? String(args.manufacturer) : '';
787
+ const skipResults = Number(args.skip_results) || 1;
788
+ // ── Attempt 1: OSC (fast, reliable for native instruments) ──
784
789
  try {
785
790
  const osc = await ensureAbleton();
786
- if (manufacturer) {
787
- const result = await osc.query('/live/kbot/load_plugin', t, manufacturer, plugin);
788
- const status = extractArgs(result);
789
- if (status[0] === 'ok') {
790
- return `Loaded **${status[1]}** on track ${args.track}`;
791
- }
792
- return `Could not find "${manufacturer} ${plugin}": ${status.join(', ')}`;
793
- }
794
- // Try load_plugin first — _deep_plugin_search correctly handles native
795
- // instruments by returning the first loadable child of matching browser folders.
796
- // This works for both native (Drum Rack, Operator, Wavetable) and third-party plugins.
797
- let result = await osc.query('/live/kbot/load_plugin', t, plugin, '');
798
- let status = extractArgs(result);
799
- if (status[0] === 'ok') {
800
- return `Loaded **${status[1]}** on track ${args.track}`;
801
- }
802
- // Fallback: try load_device (uses _search_tree, less reliable for native instruments)
803
- result = await osc.query('/live/kbot/load_device', t, plugin);
804
- status = extractArgs(result);
805
- if (status[0] === 'ok') {
806
- return `Loaded **${status[1]}** on track ${args.track}`;
791
+ // Select the target track first so the plugin loads there
792
+ osc.send('/live/song/set/current_track', t);
793
+ await new Promise(r => setTimeout(r, 200));
794
+ // Try native load_device endpoint (works for Ableton built-in instruments)
795
+ const nativeResult = await osc.query('/live/track/load/device', t, plugin);
796
+ const nativeStatus = extractArgs(nativeResult);
797
+ if (nativeStatus.length > 0 && String(nativeStatus[0]) !== 'error') {
798
+ return `Loaded **${plugin}** on track ${args.track} (via OSC)`;
807
799
  }
808
- // Final fallback: try plugins category with name as both manufacturer and plugin
809
- result = await osc.query('/live/kbot/load_plugin', t, plugin, plugin);
810
- status = extractArgs(result);
811
- if (status[0] === 'ok') {
812
- return `Loaded **${status[1]}** on track ${args.track}`;
800
+ }
801
+ catch {
802
+ // OSC failed — fall through to AppleScript
803
+ }
804
+ // ── Attempt 2: AppleScript browser automation (macOS only) ──
805
+ // Uses Ableton's built-in browser search: Cmd+F → type name → arrow down → Return
806
+ // Proven approach from ZENOLOGY loading session
807
+ if (process.platform !== 'darwin') {
808
+ return `Plugin "${plugin}" could not be loaded via OSC. AppleScript fallback is macOS-only.`;
809
+ }
810
+ try {
811
+ const searchTerm = manufacturer ? `${manufacturer} ${plugin}` : plugin;
812
+ // Build AppleScript: activate Ableton, open browser search, type plugin name, select, load
813
+ const downArrows = Array(skipResults).fill('key code 125').join('\ndelay 0.3\n'); // 125 = Down arrow
814
+ const script = `
815
+ tell application "Ableton Live 12"
816
+ activate
817
+ end tell
818
+ delay 0.5
819
+ tell application "System Events"
820
+ tell process "Ableton Live 12"
821
+ -- Open browser search: Cmd+F (View > Search in Browser)
822
+ keystroke "f" using command down
823
+ delay 0.8
824
+ -- Clear any existing search text and type plugin name
825
+ keystroke "a" using command down
826
+ delay 0.1
827
+ keystroke "${searchTerm.replace(/"/g, '\\"')}"
828
+ delay 1.0
829
+ -- Navigate down to the result (skip past categories/folders)
830
+ ${downArrows}
831
+ delay 0.3
832
+ -- Press Return to load the selected item onto the current track
833
+ key code 36
834
+ delay 0.5
835
+ end tell
836
+ end tell
837
+ return "ok"
838
+ `;
839
+ const lines = script.split('\n').filter(l => l.trim());
840
+ const escapedArgs = lines.map(l => `-e '${l.replace(/'/g, "'\\''")}'`).join(' ');
841
+ const result = execSync(`osascript ${escapedArgs}`, {
842
+ encoding: 'utf-8',
843
+ timeout: 15_000,
844
+ stdio: ['pipe', 'pipe', 'pipe'],
845
+ }).trim();
846
+ if (result === 'ok') {
847
+ return `Loaded **${plugin}** on track ${args.track} (via AppleScript browser search)`;
813
848
  }
814
- return `Plugin "${plugin}" not found in Ableton's browser. Check the name and try again.`;
849
+ return `AppleScript returned unexpected result: ${result}`;
815
850
  }
816
851
  catch (err) {
817
- return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
852
+ const msg = err.message;
853
+ if (msg.includes('not allowed') || msg.includes('assistive')) {
854
+ return `AppleScript failed — Accessibility permission required.\n\nGrant permission in System Settings > Privacy & Security > Accessibility for your terminal app.`;
855
+ }
856
+ return `Failed to load "${plugin}": OSC endpoint not available, AppleScript fallback failed.\n\nError: ${msg}`;
818
857
  }
819
858
  },
820
859
  });
@@ -964,21 +1003,19 @@ export function registerAbletonTools() {
964
1003
  if (args.name) {
965
1004
  osc.send('/live/track/set/name', newTrackIdx, String(args.name));
966
1005
  }
967
- // Load instrument if specified — use load_plugin first (handles native + third-party)
1006
+ // Load instrument if specified — try native OSC first, then load_plugin tool handles fallbacks
968
1007
  if (args.instrument) {
969
- const manufacturer = args.manufacturer ? String(args.manufacturer) : '';
970
- if (manufacturer) {
971
- await osc.query('/live/kbot/load_plugin', newTrackIdx, manufacturer, String(args.instrument));
972
- }
973
- else {
974
- // Try load_plugin first (works for native instruments via _deep_plugin_search fallback)
975
- const result = await osc.query('/live/kbot/load_plugin', newTrackIdx, String(args.instrument), '');
976
- const status = extractArgs(result);
977
- if (status[0] !== 'ok') {
978
- // Fallback to load_device
979
- await osc.query('/live/kbot/load_device', newTrackIdx, String(args.instrument));
1008
+ try {
1009
+ const loadResult = await osc.query('/live/track/load/device', newTrackIdx, String(args.instrument));
1010
+ const loadStatus = extractArgs(loadResult);
1011
+ if (loadStatus.length === 0 || String(loadStatus[0]) === 'error') {
1012
+ // Native load failed — OSC doesn't have this device. The user can use
1013
+ // ableton_load_plugin separately which has AppleScript fallback.
980
1014
  }
981
1015
  }
1016
+ catch {
1017
+ // Timeout or connection error — device may still have loaded, continue
1018
+ }
982
1019
  }
983
1020
  return `Created ${trackType} track **${args.name || 'Track ' + (newTrackIdx + 1)}**${args.instrument ? ' with ' + args.instrument : ''} (track ${newTrackIdx + 1})`;
984
1021
  }
@@ -1035,5 +1072,102 @@ export function registerAbletonTools() {
1035
1072
  }
1036
1073
  },
1037
1074
  });
1075
+ // ─── 15. Audio Analysis ──────────────────────────────────────────────
1076
+ registerTool({
1077
+ name: 'ableton_audio_analysis',
1078
+ description: 'Get real-time audio level meters from Ableton Live — track output levels (L/R RMS), master output, and peak detection. Use this to hear what is playing, check if a track has signal, or monitor the mix.',
1079
+ parameters: {
1080
+ track: { type: 'number', description: 'Track number to analyze (1-based). Omit to get master output only.' },
1081
+ all_tracks: { type: 'boolean', description: 'If true, read levels for all tracks plus master. Default: false' },
1082
+ },
1083
+ tier: 'free',
1084
+ timeout: 10_000,
1085
+ async execute(args) {
1086
+ try {
1087
+ const osc = await ensureAbleton();
1088
+ const lines = ['## Audio Levels', ''];
1089
+ // Helper to read a meter value with error handling
1090
+ async function readMeter(address, ...oscArgs) {
1091
+ try {
1092
+ const result = await osc.query(address, ...oscArgs);
1093
+ const vals = extractArgs(result);
1094
+ // Meter values are typically floats 0.0 - 1.0 (or higher for clipping)
1095
+ return typeof vals[vals.length - 1] === 'number' ? vals[vals.length - 1] : 0;
1096
+ }
1097
+ catch {
1098
+ return -1; // timeout = no response
1099
+ }
1100
+ }
1101
+ // Meter bar visualization
1102
+ function meterBar(level) {
1103
+ if (level < 0)
1104
+ return '[-no signal-]';
1105
+ const db = level > 0 ? 20 * Math.log10(level) : -Infinity;
1106
+ const dbStr = db === -Infinity ? '-inf' : db.toFixed(1);
1107
+ const barLen = Math.min(20, Math.max(0, Math.round(level * 20)));
1108
+ const bar = '\u2588'.repeat(barLen) + '\u2591'.repeat(20 - barLen);
1109
+ return `[${bar}] ${dbStr} dB`;
1110
+ }
1111
+ if (args.all_tracks) {
1112
+ // Read all track levels
1113
+ const countResult = await osc.query('/live/song/get/num_tracks');
1114
+ const numTracks = Number(extractArgs(countResult)[0]) || 0;
1115
+ for (let t = 0; t < numTracks - 1; t++) { // -1 to skip master return
1116
+ const nameResult = await osc.query('/live/track/get/name', t);
1117
+ const name = extractArgs(nameResult)[1] || `Track ${t + 1}`;
1118
+ const left = await readMeter('/live/track/get/output_meter_left', t);
1119
+ const right = await readMeter('/live/track/get/output_meter_right', t);
1120
+ const avg = left >= 0 && right >= 0 ? (left + right) / 2 : Math.max(left, right);
1121
+ lines.push(`**${displayTrack(t)}. ${name}**: ${meterBar(avg)}`);
1122
+ if (left >= 0 && right >= 0 && Math.abs(left - right) > 0.05) {
1123
+ lines.push(` L: ${meterBar(left)} | R: ${meterBar(right)}`);
1124
+ }
1125
+ }
1126
+ lines.push('');
1127
+ }
1128
+ else if (args.track) {
1129
+ // Read specific track
1130
+ const t = userTrack(args.track);
1131
+ const nameResult = await osc.query('/live/track/get/name', t);
1132
+ const name = extractArgs(nameResult)[1] || `Track ${args.track}`;
1133
+ const left = await readMeter('/live/track/get/output_meter_left', t);
1134
+ const right = await readMeter('/live/track/get/output_meter_right', t);
1135
+ lines.push(`**Track ${args.track} (${name})**`);
1136
+ lines.push(` Left: ${meterBar(left)}`);
1137
+ lines.push(` Right: ${meterBar(right)}`);
1138
+ // Check if signal is present
1139
+ const avg = left >= 0 && right >= 0 ? (left + right) / 2 : Math.max(left, right);
1140
+ if (avg <= 0) {
1141
+ lines.push('');
1142
+ lines.push('No signal detected. Check: is the track armed? Is transport playing? Does the track have clips?');
1143
+ }
1144
+ else if (avg > 1.0) {
1145
+ lines.push('');
1146
+ lines.push('**Warning**: Signal is clipping! Reduce track volume.');
1147
+ }
1148
+ lines.push('');
1149
+ }
1150
+ // Always show master output
1151
+ const masterLeft = await readMeter('/live/master/get/output_meter_left');
1152
+ const masterRight = await readMeter('/live/master/get/output_meter_right');
1153
+ lines.push('**Master Output**');
1154
+ lines.push(` Left: ${meterBar(masterLeft)}`);
1155
+ lines.push(` Right: ${meterBar(masterRight)}`);
1156
+ const masterAvg = masterLeft >= 0 && masterRight >= 0 ? (masterLeft + masterRight) / 2 : Math.max(masterLeft, masterRight);
1157
+ if (masterAvg <= 0) {
1158
+ lines.push('');
1159
+ lines.push('No audio on master output. Is transport playing?');
1160
+ }
1161
+ else if (masterAvg > 0.9) {
1162
+ lines.push('');
1163
+ lines.push('**Loud!** Master is near clipping. Consider reducing levels.');
1164
+ }
1165
+ return lines.join('\n');
1166
+ }
1167
+ catch (err) {
1168
+ return `Ableton connection failed: ${err.message}\n\n${formatAbletonError()}`;
1169
+ }
1170
+ },
1171
+ });
1038
1172
  }
1039
1173
  //# sourceMappingURL=ableton.js.map
@@ -415,7 +415,7 @@ function formatAuditReport(result) {
415
415
  // Badge
416
416
  const badgeColor = pct >= 80 ? 'brightgreen' : pct >= 60 ? 'yellow' : 'red';
417
417
  const badgeUrl = `https://img.shields.io/badge/kbot_audit-${result.grade}_(${pct}%25)-${badgeColor}`;
418
- lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[![kbot audit: ${result.grade}](${badgeUrl})](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents, 686+ tools, 20 AI providers*`, `*Install: \`npm install -g @kernel.chat/kbot\` | Audit any repo: \`kbot audit owner/repo\`*`);
418
+ lines.push('---', '', '### Add this badge to your README', '', '```markdown', `[![kbot audit: ${result.grade}](${badgeUrl})](https://www.npmjs.com/package/@kernel.chat/kbot)`, '```', '', `*Audited by [kbot](https://www.npmjs.com/package/@kernel.chat/kbot) — 35 specialist agents, 787+ tools, 20 AI providers*`, `*Install: \`npm install -g @kernel.chat/kbot\` | Audit any repo: \`kbot audit owner/repo\`*`);
419
419
  return lines.join('\n');
420
420
  }
421
421
  /** Generate a compact one-line summary for social sharing */
@@ -546,7 +546,7 @@ function formatAuditTerminal(result) {
546
546
  // Install CTA
547
547
  lines.push(chalk.hex(DIM)(' Audited by ') +
548
548
  chalk.hex(VIOLET).bold('kbot') +
549
- chalk.hex(DIM)(' \u2014 35 specialist agents, 686+ tools, 20 AI providers'));
549
+ chalk.hex(DIM)(' \u2014 35 specialist agents, 787+ tools, 20 AI providers'));
550
550
  lines.push(chalk.hex(DIM)(' Install: ') +
551
551
  chalk.hex(WHITE)('npm install -g @kernel.chat/kbot') +
552
552
  chalk.hex(DIM)(' | Audit any repo: ') +