copilot-liku-cli 0.0.4 → 0.0.8

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.
Files changed (46) hide show
  1. package/QUICKSTART.md +24 -0
  2. package/README.md +85 -33
  3. package/package.json +23 -14
  4. package/scripts/postinstall.js +63 -0
  5. package/src/cli/commands/window.js +66 -0
  6. package/src/main/agents/base-agent.js +15 -7
  7. package/src/main/agents/builder.js +211 -0
  8. package/src/main/agents/index.js +7 -4
  9. package/src/main/agents/orchestrator.js +13 -0
  10. package/src/main/agents/producer.js +891 -0
  11. package/src/main/agents/researcher.js +78 -0
  12. package/src/main/agents/state-manager.js +134 -2
  13. package/src/main/agents/verifier.js +201 -0
  14. package/src/main/ai-service.js +349 -35
  15. package/src/main/index.js +680 -110
  16. package/src/main/inspect-service.js +24 -1
  17. package/src/main/python-bridge.js +395 -0
  18. package/src/main/system-automation.js +849 -131
  19. package/src/main/ui-automation/core/ui-provider.js +99 -0
  20. package/src/main/ui-automation/core/uia-host.js +214 -0
  21. package/src/main/ui-automation/index.js +30 -0
  22. package/src/main/ui-automation/interactions/element-click.js +6 -6
  23. package/src/main/ui-automation/interactions/high-level.js +28 -6
  24. package/src/main/ui-automation/interactions/index.js +21 -0
  25. package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
  26. package/src/main/ui-automation/window/index.js +6 -0
  27. package/src/main/ui-automation/window/manager.js +173 -26
  28. package/src/main/ui-watcher.js +401 -58
  29. package/src/main/visual-awareness.js +18 -1
  30. package/src/native/windows-uia/Program.cs +89 -0
  31. package/src/native/windows-uia/build.ps1 +24 -0
  32. package/src/native/windows-uia-dotnet/Program.cs +920 -0
  33. package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
  34. package/src/native/windows-uia-dotnet/build.ps1 +24 -0
  35. package/src/renderer/chat/chat.js +915 -671
  36. package/src/renderer/chat/index.html +2 -4
  37. package/src/renderer/chat/preload.js +8 -1
  38. package/src/renderer/overlay/overlay.js +157 -8
  39. package/src/renderer/overlay/preload.js +4 -0
  40. package/src/shared/inspect-types.js +82 -6
  41. package/ARCHITECTURE.md +0 -411
  42. package/CONFIGURATION.md +0 -302
  43. package/CONTRIBUTING.md +0 -225
  44. package/ELECTRON_README.md +0 -121
  45. package/PROJECT_STATUS.md +0 -229
  46. package/TESTING.md +0 -274
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  const { BaseAgent, AgentRole, AgentCapabilities } = require('./base-agent');
15
+ const { PythonBridge } = require('../python-bridge');
15
16
  const fs = require('fs');
16
17
  const path = require('path');
17
18
 
@@ -41,6 +42,9 @@ class ResearcherAgent extends BaseAgent {
41
42
  this.researchCache = new Map();
42
43
  this.cacheMaxAge = options.cacheMaxAge || 3600000; // 1 hour
43
44
  this.sourceCredibility = new Map();
45
+
46
+ // PythonBridge for genre intelligence (lazy init via shared singleton)
47
+ this.pythonBridge = null;
44
48
  }
45
49
 
46
50
  getSystemPrompt() {
@@ -506,6 +510,80 @@ Provide comprehensive findings with:
506
510
  this.researchCache.clear();
507
511
  this.sourceCredibility.clear();
508
512
  }
513
+
514
+ // ===== Genre Intelligence Methods (Sprint 3 — Task 3.4) =====
515
+
516
+ /**
517
+ * Lazily initialise and start the shared PythonBridge.
518
+ * @returns {Promise<PythonBridge>}
519
+ */
520
+ async ensurePythonBridge() {
521
+ if (!this.pythonBridge) {
522
+ this.pythonBridge = PythonBridge.getShared();
523
+ }
524
+ if (!this.pythonBridge.isRunning) {
525
+ this.log('info', 'Starting PythonBridge for genre intelligence');
526
+ await this.pythonBridge.start();
527
+ }
528
+ return this.pythonBridge;
529
+ }
530
+
531
+ /**
532
+ * Look up the 10-dimensional DNA vector for a given genre.
533
+ *
534
+ * Results are cached in ``researchCache`` to avoid repeated RPCs.
535
+ *
536
+ * @param {string} genre Genre identifier (e.g. "trap_soul").
537
+ * @returns {Promise<object>} { genre, found, vector, dimensions }
538
+ */
539
+ async queryGenreDNA(genre) {
540
+ // Check cache first
541
+ const cacheKey = `genre_dna::${genre}`;
542
+ const cached = this.researchCache.get(cacheKey);
543
+ if (cached && (Date.now() - cached.timestamp) < this.cacheMaxAge) {
544
+ this.log('info', 'Returning cached genre DNA', { genre });
545
+ return { ...cached.result, fromCache: true };
546
+ }
547
+
548
+ await this.ensurePythonBridge();
549
+ this.log('info', 'Querying genre DNA', { genre });
550
+
551
+ const result = await this.pythonBridge.call('genre_dna_lookup', { genre });
552
+
553
+ // Cache the result
554
+ this.researchCache.set(cacheKey, {
555
+ result,
556
+ timestamp: Date.now(),
557
+ });
558
+
559
+ return result;
560
+ }
561
+
562
+ /**
563
+ * Blend multiple genre DNA vectors with weights.
564
+ *
565
+ * @param {Array<{genre: string, weight: number}>} genres
566
+ * @returns {Promise<object>} { vector, sources, description, suggested_tempo, dimensions }
567
+ */
568
+ async blendGenres(genres) {
569
+ await this.ensurePythonBridge();
570
+ this.log('info', 'Blending genres', { count: genres.length });
571
+
572
+ const result = await this.pythonBridge.call('genre_blend', { genres });
573
+ return result;
574
+ }
575
+
576
+ /**
577
+ * Stop and release the PythonBridge.
578
+ * @returns {Promise<void>}
579
+ */
580
+ async disposePythonBridge() {
581
+ if (this.pythonBridge) {
582
+ this.log('info', 'Disposing PythonBridge');
583
+ await this.pythonBridge.stop();
584
+ this.pythonBridge = null;
585
+ }
586
+ }
509
587
  }
510
588
 
511
589
  module.exports = { ResearcherAgent };
@@ -9,6 +9,7 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
  const { nowIso, nowFilenameSafe } = require('../utils/time');
12
+ const { PythonBridge } = require('../python-bridge');
12
13
 
13
14
  class AgentStateManager {
14
15
  constructor(statePath = null) {
@@ -48,7 +49,10 @@ class AgentStateManager {
48
49
  purpose: null,
49
50
  parentSessionId: null
50
51
  },
51
- checkpoints: []
52
+ checkpoints: [],
53
+ sessionGraph: null,
54
+ generations: [],
55
+ lastSync: null
52
56
  };
53
57
  }
54
58
 
@@ -69,6 +73,13 @@ class AgentStateManager {
69
73
  state.schemaVersion = 2;
70
74
  state.version = '1.1.0';
71
75
  }
76
+ if (!state.schemaVersion || state.schemaVersion < 3) {
77
+ state.sessionGraph = state.sessionGraph || null;
78
+ state.generations = state.generations || [];
79
+ state.lastSync = state.lastSync || null;
80
+ state.schemaVersion = 3;
81
+ state.version = '1.2.0';
82
+ }
72
83
  return state;
73
84
  }
74
85
 
@@ -335,10 +346,131 @@ class AgentStateManager {
335
346
  purpose: null,
336
347
  parentSessionId: null
337
348
  },
338
- checkpoints: []
349
+ checkpoints: [],
350
+ sessionGraph: null,
351
+ generations: [],
352
+ lastSync: null
339
353
  };
340
354
  this._saveState();
341
355
  }
356
+
357
+ // ===== SessionGraph Integration =====
358
+
359
+ /**
360
+ * Fetch the current SessionGraph from the Python backend.
361
+ * Caches locally in state for offline access.
362
+ * @returns {Promise<object|null>} The SessionGraph dict or null
363
+ */
364
+ async fetchSessionGraph() {
365
+ try {
366
+ const bridge = PythonBridge.getShared();
367
+ const graph = await bridge.call('session_state', {});
368
+ this.state.sessionGraph = graph;
369
+ this.state.lastSync = nowIso();
370
+ this._saveState();
371
+ return graph;
372
+ } catch (error) {
373
+ console.warn(`[StateManager] Failed to fetch SessionGraph: ${error.message}`);
374
+ return this.state.sessionGraph || null;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Get the cached SessionGraph (no network call).
380
+ * @returns {object|null}
381
+ */
382
+ getCachedSessionGraph() {
383
+ return this.state.sessionGraph || null;
384
+ }
385
+
386
+ /**
387
+ * Get summary of the session graph (track count, section count, etc.)
388
+ * @returns {object} Summary stats
389
+ */
390
+ getSessionSummary() {
391
+ const graph = this.state.sessionGraph;
392
+ if (!graph) {
393
+ return { available: false };
394
+ }
395
+
396
+ const tracks = graph.tracks || [];
397
+ const sections = graph.sections || [];
398
+ const totalBars = sections.reduce((sum, s) => sum + (s.bars || s.length_bars || 0), 0);
399
+ const hasMidi = tracks.some(t => (t.clips || []).some(c => c.midi_path || c.midi));
400
+ const hasAudio = tracks.some(t => (t.clips || []).some(c => c.audio_path || c.audio));
401
+
402
+ return {
403
+ available: true,
404
+ session_id: graph.session_id || null,
405
+ bpm: graph.bpm || null,
406
+ key: graph.key || null,
407
+ genre: graph.genre || null,
408
+ trackCount: tracks.length,
409
+ sectionCount: sections.length,
410
+ totalBars,
411
+ hasMidi,
412
+ hasAudio
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Record a generation event in state with the resulting SessionGraph.
418
+ * @param {string} prompt - The original user prompt
419
+ * @param {object} result - The GenerationResult from generate_sync
420
+ * @param {object} sessionGraph - The SessionGraph from session_state
421
+ */
422
+ recordGeneration(prompt, result, sessionGraph) {
423
+ if (!this.state.generations) {
424
+ this.state.generations = [];
425
+ }
426
+
427
+ this.state.generations.push({
428
+ timestamp: nowIso(),
429
+ prompt,
430
+ result: {
431
+ success: result?.success ?? null,
432
+ session_id: result?.session_id ?? null,
433
+ tracks: result?.tracks ?? [],
434
+ error: result?.error ?? null
435
+ },
436
+ sessionGraph: sessionGraph || null
437
+ });
438
+
439
+ // Keep only last 10 generations
440
+ if (this.state.generations.length > 10) {
441
+ this.state.generations = this.state.generations.slice(-10);
442
+ }
443
+
444
+ this._saveState();
445
+ }
446
+
447
+ /**
448
+ * Get history of past generations.
449
+ * @param {number} [limit=5]
450
+ * @returns {Array}
451
+ */
452
+ getGenerationHistory(limit = 5) {
453
+ return (this.state.generations || []).slice(-limit);
454
+ }
455
+
456
+ /**
457
+ * Sync session state between Python and Electron.
458
+ * Fetches graph, records in state, returns summary.
459
+ * @returns {Promise<object>} { synced: true/false, summary, timestamp }
460
+ */
461
+ async syncSessionState() {
462
+ const timestamp = nowIso();
463
+ try {
464
+ const graph = await this.fetchSessionGraph();
465
+ if (!graph) {
466
+ return { synced: false, summary: null, timestamp, error: 'No graph returned' };
467
+ }
468
+ const summary = this.getSessionSummary();
469
+ return { synced: true, summary, timestamp };
470
+ } catch (error) {
471
+ return { synced: false, summary: null, timestamp, error: error.message };
472
+ }
473
+ }
342
474
  }
343
475
 
344
476
  module.exports = { AgentStateManager };
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  const { BaseAgent, AgentRole, AgentCapabilities } = require('./base-agent');
14
+ const { PythonBridge } = require('../python-bridge');
14
15
 
15
16
  class VerifierAgent extends BaseAgent {
16
17
  constructor(options = {}) {
@@ -33,6 +34,9 @@ class VerifierAgent extends BaseAgent {
33
34
  this.verificationResults = [];
34
35
  this.currentPhase = null;
35
36
  this.verdict = null;
37
+
38
+ // PythonBridge for music quality critics (lazy init via shared singleton)
39
+ this.pythonBridge = null;
36
40
  }
37
41
 
38
42
  getSystemPrompt() {
@@ -447,6 +451,203 @@ Always structure your response as:
447
451
  this.currentPhase = null;
448
452
  this.verdict = null;
449
453
  }
454
+
455
+ // ===== Music Quality Verification (Sprint 3 — Task 3.3) =====
456
+
457
+ /**
458
+ * Lazily initialise and start the shared PythonBridge.
459
+ * @returns {Promise<PythonBridge>}
460
+ */
461
+ async ensurePythonBridge() {
462
+ if (!this.pythonBridge) {
463
+ this.pythonBridge = PythonBridge.getShared();
464
+ }
465
+ if (!this.pythonBridge.isRunning) {
466
+ const alive = await this.pythonBridge.isAlive();
467
+ if (!alive) {
468
+ this.log('info', 'Starting PythonBridge for music critics');
469
+ await this.pythonBridge.start();
470
+ } else {
471
+ this.log('info', 'PythonBridge connected to existing server');
472
+ }
473
+ }
474
+ return this.pythonBridge;
475
+ }
476
+
477
+ /**
478
+ * Run VLC / BKAS / ADC quality-gate critics on a MIDI file.
479
+ *
480
+ * @param {string} midiPath Path to the MIDI file.
481
+ * @param {string} [genre] Genre identifier for context-aware eval.
482
+ * @param {object} [analysisData] Pre-extracted analysis data (voicings, bass_notes, etc.)
483
+ * @returns {Promise<{passed: boolean, metrics: Array, report: object}>}
484
+ */
485
+ async runMusicCritics(midiPath, genre, analysisData = {}) {
486
+ await this.ensurePythonBridge();
487
+ this.log('info', 'Running music critics', { midiPath, genre });
488
+
489
+ const hasAnalysisData = analysisData && Object.keys(analysisData).length > 0;
490
+ const method = hasAnalysisData ? 'run_critics' : 'run_critics_midi';
491
+ const report = await this.pythonBridge.call(method, {
492
+ midi_path: midiPath,
493
+ genre,
494
+ ...analysisData,
495
+ });
496
+
497
+ // Record proof entries for each metric
498
+ if (report && Array.isArray(report.metrics)) {
499
+ for (const metric of report.metrics) {
500
+ this.addStructuredProof({
501
+ type: 'music-critic',
502
+ criticName: metric.name,
503
+ value: metric.value,
504
+ threshold: metric.threshold,
505
+ passed: metric.passed,
506
+ midiPath,
507
+ });
508
+ }
509
+ }
510
+
511
+ this.addProof(
512
+ 'music-critics-overall',
513
+ report.overall_passed ? 'PASS' : 'FAIL',
514
+ midiPath
515
+ );
516
+
517
+ return {
518
+ passed: report.overall_passed,
519
+ metrics: report.metrics,
520
+ report,
521
+ };
522
+ }
523
+
524
+ /**
525
+ * Preflight gate for score plans before generation.
526
+ *
527
+ * Combines deterministic checks with a premium-model verifier pass.
528
+ * Returns pass/fail plus issues and recommendations.
529
+ */
530
+ async preflightScorePlanGate(scorePlan, context = {}) {
531
+ const issues = [];
532
+ const recommendations = [];
533
+
534
+ const required = ['schema_version', 'prompt', 'bpm', 'key', 'mode', 'sections', 'tracks'];
535
+ for (const key of required) {
536
+ if (scorePlan?.[key] === undefined || scorePlan?.[key] === null) {
537
+ issues.push(`Missing required field: ${key}`);
538
+ }
539
+ }
540
+
541
+ if (scorePlan?.schema_version !== 'score_plan_v1') {
542
+ issues.push('schema_version must be score_plan_v1');
543
+ }
544
+
545
+ if (typeof scorePlan?.bpm !== 'number' || Number.isNaN(scorePlan.bpm) || scorePlan.bpm < 30 || scorePlan.bpm > 220) {
546
+ issues.push('bpm out of valid range [30,220]');
547
+ }
548
+
549
+ if (!Array.isArray(scorePlan?.sections) || scorePlan.sections.length < 1) {
550
+ issues.push('sections must be a non-empty array');
551
+ }
552
+
553
+ if (!Array.isArray(scorePlan?.tracks) || scorePlan.tracks.length < 1) {
554
+ issues.push('tracks must be a non-empty array');
555
+ }
556
+
557
+ if (issues.length > 0) {
558
+ recommendations.push('Fix schema/shape issues before generation');
559
+ }
560
+
561
+ let modelReview = null;
562
+ const verifierModel = context.model || 'claude-sonnet-4.5';
563
+ try {
564
+ const reviewPrompt = `You are a strict music plan verifier. Evaluate this score plan for generation risk and musical coherence.
565
+
566
+ Return JSON only with fields:
567
+ {
568
+ "passed": boolean,
569
+ "issues": string[],
570
+ "recommendations": string[]
571
+ }
572
+
573
+ Prompt context:
574
+ ${context.prompt || ''}
575
+
576
+ Score plan:
577
+ ${JSON.stringify(scorePlan, null, 2)}`;
578
+
579
+ const review = await this.chat(reviewPrompt, { model: verifierModel });
580
+ const text = (review?.text || '').trim();
581
+ const jsonText = text.startsWith('{') ? text : (text.match(/\{[\s\S]*\}/)?.[0] || '{}');
582
+ modelReview = JSON.parse(jsonText);
583
+ } catch (error) {
584
+ modelReview = {
585
+ passed: true,
586
+ issues: [],
587
+ recommendations: [`Model verifier unavailable: ${error.message}`]
588
+ };
589
+ }
590
+
591
+ if (Array.isArray(modelReview?.issues)) {
592
+ issues.push(...modelReview.issues);
593
+ }
594
+ if (Array.isArray(modelReview?.recommendations)) {
595
+ recommendations.push(...modelReview.recommendations);
596
+ }
597
+
598
+ const passed = issues.length === 0 && modelReview?.passed !== false;
599
+
600
+ this.addStructuredProof({
601
+ type: 'score-plan-preflight',
602
+ passed,
603
+ verifierModel,
604
+ issuesCount: issues.length,
605
+ recommendationsCount: recommendations.length
606
+ });
607
+
608
+ return {
609
+ passed,
610
+ verifierModel,
611
+ issues,
612
+ recommendations,
613
+ modelReview
614
+ };
615
+ }
616
+
617
+ /**
618
+ * Analyze rendered audio against target genre expectations.
619
+ */
620
+ async analyzeRenderedOutput(audioPath, genre = 'pop') {
621
+ await this.ensurePythonBridge();
622
+ this.log('info', 'Analyzing rendered output', { audioPath, genre });
623
+
624
+ const report = await this.pythonBridge.call('analyze_output', {
625
+ audio_path: audioPath,
626
+ genre,
627
+ });
628
+
629
+ this.addStructuredProof({
630
+ type: 'output-analysis',
631
+ audioPath,
632
+ genre,
633
+ passed: !!report?.passed,
634
+ genreMatchScore: report?.genre_match_score,
635
+ });
636
+
637
+ return report;
638
+ }
639
+
640
+ /**
641
+ * Stop and release the PythonBridge.
642
+ * @returns {Promise<void>}
643
+ */
644
+ async disposePythonBridge() {
645
+ if (this.pythonBridge) {
646
+ this.log('info', 'Disposing PythonBridge');
647
+ await this.pythonBridge.stop();
648
+ this.pythonBridge = null;
649
+ }
650
+ }
450
651
  }
451
652
 
452
653
  module.exports = { VerifierAgent };