sessioncast-cli 2.2.2 → 2.3.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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LlmService = void 0;
4
4
  const child_process_1 = require("child_process");
5
+ const readline_1 = require("readline");
5
6
  class LlmService {
6
7
  constructor(config) {
7
8
  this.config = config || {
@@ -37,6 +38,12 @@ class LlmService {
37
38
  return await this.callAnthropic(actualModel, messages || [], temperature, maxTokens);
38
39
  case 'claude-code':
39
40
  return await this.callClaudeCode(actualModel, messages || [], maxTokens);
41
+ case 'codex':
42
+ return await this.callCodex(actualModel, messages || [], maxTokens);
43
+ case 'gemini':
44
+ return await this.callGemini(actualModel, messages || [], temperature, maxTokens);
45
+ case 'cursor':
46
+ return await this.callCursor(actualModel, messages || [], maxTokens);
40
47
  default:
41
48
  return {
42
49
  id: '',
@@ -65,6 +72,42 @@ class LlmService {
65
72
  };
66
73
  }
67
74
  }
75
+ async chatStream(model, messages, temperature, maxTokens, onChunk) {
76
+ if (!this.config.enabled || !onChunk) {
77
+ return this.chat(model, messages, temperature, maxTokens);
78
+ }
79
+ const provider = this.config.provider || 'ollama';
80
+ const actualModel = model || this.config.model;
81
+ try {
82
+ switch (provider.toLowerCase()) {
83
+ case 'claude-code':
84
+ return await this.callClaudeCodeStream(actualModel, messages || [], maxTokens, onChunk);
85
+ case 'ollama':
86
+ return await this.callOllamaStream(actualModel, messages || [], temperature, maxTokens, onChunk);
87
+ case 'openai':
88
+ return await this.callOpenAiStream(actualModel, messages || [], temperature, maxTokens, onChunk);
89
+ case 'anthropic':
90
+ return await this.callAnthropicStream(actualModel, messages || [], temperature, maxTokens, onChunk);
91
+ default:
92
+ // For providers without streaming, fall back to non-streaming
93
+ const result = await this.chat(model, messages, temperature, maxTokens);
94
+ if (result.choices?.[0]?.message?.content) {
95
+ onChunk({ content: result.choices[0].message.content, done: true });
96
+ }
97
+ return result;
98
+ }
99
+ }
100
+ catch (error) {
101
+ return {
102
+ id: '',
103
+ object: 'error',
104
+ created: Date.now(),
105
+ model: '',
106
+ choices: [],
107
+ error: { message: error.message, type: 'internal_error' }
108
+ };
109
+ }
110
+ }
68
111
  async callOllama(model, messages, temperature, maxTokens) {
69
112
  const baseUrl = this.config.baseUrl || 'http://localhost:11434';
70
113
  const requestBody = {
@@ -328,6 +371,551 @@ class LlmService {
328
371
  });
329
372
  });
330
373
  }
374
+ async callCodex(model, messages, maxTokens) {
375
+ const parts = [];
376
+ for (const msg of messages) {
377
+ if (msg.role === 'system') {
378
+ parts.push(`[System] ${msg.content}`);
379
+ }
380
+ else if (msg.role === 'user') {
381
+ parts.push(msg.content);
382
+ }
383
+ else if (msg.role === 'assistant') {
384
+ parts.push(`[Assistant] ${msg.content}`);
385
+ }
386
+ }
387
+ const prompt = parts.join('\n\n');
388
+ const args = ['exec', '--json'];
389
+ if (model && model !== 'default') {
390
+ args.push('-c', `model="${model}"`);
391
+ }
392
+ return new Promise((resolve) => {
393
+ const proc = (0, child_process_1.spawn)('codex', args, {
394
+ stdio: ['pipe', 'pipe', 'pipe']
395
+ });
396
+ proc.stdin.write(prompt);
397
+ proc.stdin.end();
398
+ let stdout = '';
399
+ let stderr = '';
400
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
401
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
402
+ const timer = setTimeout(() => {
403
+ proc.kill();
404
+ resolve({
405
+ id: '',
406
+ object: 'error',
407
+ created: Math.floor(Date.now() / 1000),
408
+ model,
409
+ choices: [],
410
+ error: { message: 'Codex CLI timed out after 120s', type: 'timeout' }
411
+ });
412
+ }, 120000);
413
+ proc.on('close', (code) => {
414
+ clearTimeout(timer);
415
+ if (code !== 0) {
416
+ resolve({
417
+ id: '',
418
+ object: 'error',
419
+ created: Math.floor(Date.now() / 1000),
420
+ model,
421
+ choices: [],
422
+ error: {
423
+ message: `Codex CLI exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`,
424
+ type: 'internal_error'
425
+ }
426
+ });
427
+ return;
428
+ }
429
+ try {
430
+ // codex exec --json outputs JSONL events, find the last assistant message
431
+ const lines = stdout.trim().split('\n');
432
+ let content = '';
433
+ let usage = { input_tokens: 0, output_tokens: 0 };
434
+ for (const line of lines) {
435
+ try {
436
+ const event = JSON.parse(line);
437
+ if (event.type === 'message' && event.role === 'assistant') {
438
+ content = typeof event.content === 'string'
439
+ ? event.content
440
+ : (event.content || []).filter((c) => c.type === 'output_text').map((c) => c.text).join('');
441
+ }
442
+ if (event.usage) {
443
+ usage = event.usage;
444
+ }
445
+ }
446
+ catch { /* skip non-JSON lines */ }
447
+ }
448
+ resolve({
449
+ id: `codex-${Math.random().toString(36).substring(2, 10)}`,
450
+ object: 'chat.completion',
451
+ created: Math.floor(Date.now() / 1000),
452
+ model: model || 'codex',
453
+ choices: [{
454
+ index: 0,
455
+ message: { role: 'assistant', content: content || stdout.trim() },
456
+ finish_reason: 'stop'
457
+ }],
458
+ usage: {
459
+ prompt_tokens: usage.input_tokens || 0,
460
+ completion_tokens: usage.output_tokens || 0,
461
+ total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
462
+ }
463
+ });
464
+ }
465
+ catch {
466
+ resolve({
467
+ id: `codex-${Math.random().toString(36).substring(2, 10)}`,
468
+ object: 'chat.completion',
469
+ created: Math.floor(Date.now() / 1000),
470
+ model: model || 'codex',
471
+ choices: [{
472
+ index: 0,
473
+ message: { role: 'assistant', content: stdout.trim() },
474
+ finish_reason: 'stop'
475
+ }],
476
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
477
+ });
478
+ }
479
+ });
480
+ });
481
+ }
482
+ async callGemini(model, messages, temperature, maxTokens) {
483
+ const apiKey = this.config.apiKey;
484
+ if (!apiKey) {
485
+ return {
486
+ id: '',
487
+ object: 'error',
488
+ created: Date.now(),
489
+ model: '',
490
+ choices: [],
491
+ error: {
492
+ message: 'Gemini API key not configured',
493
+ type: 'configuration_error'
494
+ }
495
+ };
496
+ }
497
+ const actualModel = model || 'gemini-2.0-flash';
498
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${actualModel}:generateContent?key=${apiKey}`;
499
+ // Separate system message from conversation
500
+ let systemInstruction;
501
+ const contents = [];
502
+ for (const msg of messages) {
503
+ if (msg.role === 'system') {
504
+ systemInstruction = msg.content;
505
+ }
506
+ else {
507
+ contents.push({
508
+ role: msg.role === 'assistant' ? 'model' : 'user',
509
+ parts: [{ text: msg.content }]
510
+ });
511
+ }
512
+ }
513
+ const requestBody = { contents };
514
+ if (systemInstruction) {
515
+ requestBody.systemInstruction = { parts: [{ text: systemInstruction }] };
516
+ }
517
+ const generationConfig = {};
518
+ if (maxTokens)
519
+ generationConfig.maxOutputTokens = maxTokens;
520
+ if (temperature !== undefined)
521
+ generationConfig.temperature = temperature;
522
+ if (Object.keys(generationConfig).length > 0) {
523
+ requestBody.generationConfig = generationConfig;
524
+ }
525
+ const response = await fetch(url, {
526
+ method: 'POST',
527
+ headers: { 'Content-Type': 'application/json' },
528
+ body: JSON.stringify(requestBody)
529
+ });
530
+ if (!response.ok) {
531
+ const errorBody = await response.text();
532
+ throw new Error(`Gemini returned status ${response.status}: ${errorBody}`);
533
+ }
534
+ const geminiResponse = await response.json();
535
+ return this.convertGeminiToOpenAiFormat(geminiResponse, actualModel);
536
+ }
537
+ convertGeminiToOpenAiFormat(geminiResponse, model) {
538
+ const candidate = (geminiResponse.candidates || [])[0] || {};
539
+ const content = (candidate.content?.parts || [])
540
+ .map((p) => p.text || '')
541
+ .join('');
542
+ const usage = geminiResponse.usageMetadata || {};
543
+ return {
544
+ id: `gemini-${Math.random().toString(36).substring(2, 10)}`,
545
+ object: 'chat.completion',
546
+ created: Math.floor(Date.now() / 1000),
547
+ model,
548
+ choices: [{
549
+ index: 0,
550
+ message: { role: 'assistant', content },
551
+ finish_reason: candidate.finishReason === 'STOP' ? 'stop' : (candidate.finishReason || 'stop')
552
+ }],
553
+ usage: {
554
+ prompt_tokens: usage.promptTokenCount || 0,
555
+ completion_tokens: usage.candidatesTokenCount || 0,
556
+ total_tokens: usage.totalTokenCount || 0
557
+ }
558
+ };
559
+ }
560
+ async callCursor(model, messages, maxTokens) {
561
+ const parts = [];
562
+ for (const msg of messages) {
563
+ if (msg.role === 'system') {
564
+ parts.push(`[System] ${msg.content}`);
565
+ }
566
+ else if (msg.role === 'user') {
567
+ parts.push(msg.content);
568
+ }
569
+ else if (msg.role === 'assistant') {
570
+ parts.push(`[Assistant] ${msg.content}`);
571
+ }
572
+ }
573
+ const prompt = parts.join('\n\n');
574
+ const args = ['-p', '--output-format', 'json'];
575
+ if (model && model !== 'default') {
576
+ args.push('--model', model);
577
+ }
578
+ return new Promise((resolve) => {
579
+ const proc = (0, child_process_1.spawn)('cursor', args, {
580
+ stdio: ['pipe', 'pipe', 'pipe']
581
+ });
582
+ proc.stdin.write(prompt);
583
+ proc.stdin.end();
584
+ let stdout = '';
585
+ let stderr = '';
586
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
587
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
588
+ const timer = setTimeout(() => {
589
+ proc.kill();
590
+ resolve({
591
+ id: '',
592
+ object: 'error',
593
+ created: Math.floor(Date.now() / 1000),
594
+ model,
595
+ choices: [],
596
+ error: { message: 'Cursor CLI timed out after 120s', type: 'timeout' }
597
+ });
598
+ }, 120000);
599
+ proc.on('close', (code) => {
600
+ clearTimeout(timer);
601
+ if (code !== 0) {
602
+ resolve({
603
+ id: '',
604
+ object: 'error',
605
+ created: Math.floor(Date.now() / 1000),
606
+ model,
607
+ choices: [],
608
+ error: {
609
+ message: `Cursor CLI exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`,
610
+ type: 'internal_error'
611
+ }
612
+ });
613
+ return;
614
+ }
615
+ try {
616
+ const events = JSON.parse(stdout);
617
+ const resultEvent = Array.isArray(events)
618
+ ? events.find((e) => e.type === 'result')
619
+ : events;
620
+ const content = resultEvent?.result || '';
621
+ const usage = resultEvent?.usage || {};
622
+ resolve({
623
+ id: `cursor-${Math.random().toString(36).substring(2, 10)}`,
624
+ object: 'chat.completion',
625
+ created: Math.floor(Date.now() / 1000),
626
+ model: model || 'cursor',
627
+ choices: [{
628
+ index: 0,
629
+ message: { role: 'assistant', content },
630
+ finish_reason: 'stop'
631
+ }],
632
+ usage: {
633
+ prompt_tokens: usage.input_tokens || 0,
634
+ completion_tokens: usage.output_tokens || 0,
635
+ total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
636
+ }
637
+ });
638
+ }
639
+ catch {
640
+ resolve({
641
+ id: `cursor-${Math.random().toString(36).substring(2, 10)}`,
642
+ object: 'chat.completion',
643
+ created: Math.floor(Date.now() / 1000),
644
+ model: model || 'cursor',
645
+ choices: [{
646
+ index: 0,
647
+ message: { role: 'assistant', content: stdout.trim() },
648
+ finish_reason: 'stop'
649
+ }],
650
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
651
+ });
652
+ }
653
+ });
654
+ });
655
+ }
656
+ async callClaudeCodeStream(model, messages, maxTokens, onChunk) {
657
+ const parts = [];
658
+ for (const msg of messages) {
659
+ if (msg.role === 'system')
660
+ parts.push(`[System] ${msg.content}`);
661
+ else if (msg.role === 'user')
662
+ parts.push(msg.content);
663
+ else if (msg.role === 'assistant')
664
+ parts.push(`[Assistant] ${msg.content}`);
665
+ }
666
+ const prompt = parts.join('\n\n');
667
+ const args = ['-p', '--output-format', 'stream-json', '--no-session-persistence'];
668
+ if (model && model !== 'default') {
669
+ args.push('--model', model);
670
+ }
671
+ return new Promise((resolve) => {
672
+ const env = { ...process.env };
673
+ delete env.CLAUDECODE;
674
+ delete env.CLAUDE_CODE;
675
+ const proc = (0, child_process_1.spawn)('claude', args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
676
+ proc.stdin.write(prompt);
677
+ proc.stdin.end();
678
+ let fullContent = '';
679
+ let lastResult = null;
680
+ let stderr = '';
681
+ const rl = (0, readline_1.createInterface)({ input: proc.stdout });
682
+ rl.on('line', (line) => {
683
+ try {
684
+ const event = JSON.parse(line);
685
+ if (event.type === 'assistant' && event.message) {
686
+ // Extract text content from assistant message
687
+ const text = typeof event.message === 'string'
688
+ ? event.message
689
+ : (event.message.content || [])
690
+ .filter((c) => c.type === 'text')
691
+ .map((c) => c.text)
692
+ .join('');
693
+ if (text) {
694
+ fullContent += text;
695
+ onChunk({ content: text, done: false });
696
+ }
697
+ }
698
+ else if (event.type === 'content_block_delta') {
699
+ const delta = event.delta?.text || '';
700
+ if (delta) {
701
+ fullContent += delta;
702
+ onChunk({ content: delta, done: false });
703
+ }
704
+ }
705
+ else if (event.type === 'result') {
706
+ lastResult = event;
707
+ if (!fullContent && event.result) {
708
+ fullContent = event.result;
709
+ }
710
+ }
711
+ }
712
+ catch { /* skip non-JSON lines */ }
713
+ });
714
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
715
+ const timer = setTimeout(() => {
716
+ proc.kill();
717
+ onChunk({ content: '', done: true });
718
+ resolve({
719
+ id: '', object: 'error', created: Math.floor(Date.now() / 1000), model,
720
+ choices: [], error: { message: 'Claude Code timed out after 120s', type: 'timeout' }
721
+ });
722
+ }, 120000);
723
+ proc.on('close', (code) => {
724
+ clearTimeout(timer);
725
+ rl.close();
726
+ onChunk({ content: '', done: true });
727
+ const usage = lastResult?.usage || {};
728
+ resolve({
729
+ id: lastResult?.session_id || `claude-${Math.random().toString(36).substring(2, 10)}`,
730
+ object: 'chat.completion',
731
+ created: Math.floor(Date.now() / 1000),
732
+ model: model || 'claude-code',
733
+ choices: [{
734
+ index: 0,
735
+ message: { role: 'assistant', content: fullContent || lastResult?.result || '' },
736
+ finish_reason: 'stop'
737
+ }],
738
+ usage: {
739
+ prompt_tokens: usage.input_tokens || 0,
740
+ completion_tokens: usage.output_tokens || 0,
741
+ total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
742
+ }
743
+ });
744
+ });
745
+ });
746
+ }
747
+ async callOllamaStream(model, messages, temperature, maxTokens, onChunk) {
748
+ const baseUrl = this.config.baseUrl || 'http://localhost:11434';
749
+ const requestBody = { model, messages, stream: true };
750
+ const options = {};
751
+ if (temperature !== undefined)
752
+ options.temperature = temperature;
753
+ if (maxTokens !== undefined)
754
+ options.num_predict = maxTokens;
755
+ if (Object.keys(options).length > 0)
756
+ requestBody.options = options;
757
+ const response = await fetch(`${baseUrl}/api/chat`, {
758
+ method: 'POST',
759
+ headers: { 'Content-Type': 'application/json' },
760
+ body: JSON.stringify(requestBody)
761
+ });
762
+ if (!response.ok)
763
+ throw new Error(`Ollama returned status ${response.status}`);
764
+ let fullContent = '';
765
+ const reader = response.body?.getReader();
766
+ const decoder = new TextDecoder();
767
+ if (reader) {
768
+ let buffer = '';
769
+ while (true) {
770
+ const { done, value } = await reader.read();
771
+ if (done)
772
+ break;
773
+ buffer += decoder.decode(value, { stream: true });
774
+ const lines = buffer.split('\n');
775
+ buffer = lines.pop() || '';
776
+ for (const line of lines) {
777
+ if (!line.trim())
778
+ continue;
779
+ try {
780
+ const event = JSON.parse(line);
781
+ if (event.message?.content) {
782
+ fullContent += event.message.content;
783
+ onChunk({ content: event.message.content, done: false });
784
+ }
785
+ }
786
+ catch { /* skip */ }
787
+ }
788
+ }
789
+ }
790
+ onChunk({ content: '', done: true });
791
+ return {
792
+ id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`,
793
+ object: 'chat.completion',
794
+ created: Math.floor(Date.now() / 1000),
795
+ model,
796
+ choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
797
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
798
+ };
799
+ }
800
+ async callOpenAiStream(model, messages, temperature, maxTokens, onChunk) {
801
+ const baseUrl = this.config.baseUrl || 'https://api.openai.com/v1';
802
+ const apiKey = this.config.apiKey;
803
+ if (!apiKey)
804
+ throw new Error('OpenAI API key not configured');
805
+ const requestBody = { model, messages, stream: true };
806
+ if (temperature !== undefined)
807
+ requestBody.temperature = temperature;
808
+ if (maxTokens !== undefined)
809
+ requestBody.max_tokens = maxTokens;
810
+ const response = await fetch(`${baseUrl}/chat/completions`, {
811
+ method: 'POST',
812
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
813
+ body: JSON.stringify(requestBody)
814
+ });
815
+ if (!response.ok)
816
+ throw new Error(`OpenAI returned status ${response.status}`);
817
+ let fullContent = '';
818
+ const reader = response.body?.getReader();
819
+ const decoder = new TextDecoder();
820
+ if (reader) {
821
+ let buffer = '';
822
+ while (true) {
823
+ const { done, value } = await reader.read();
824
+ if (done)
825
+ break;
826
+ buffer += decoder.decode(value, { stream: true });
827
+ const lines = buffer.split('\n');
828
+ buffer = lines.pop() || '';
829
+ for (const line of lines) {
830
+ if (!line.startsWith('data: ') || line === 'data: [DONE]')
831
+ continue;
832
+ try {
833
+ const event = JSON.parse(line.slice(6));
834
+ const delta = event.choices?.[0]?.delta?.content || '';
835
+ if (delta) {
836
+ fullContent += delta;
837
+ onChunk({ content: delta, done: false });
838
+ }
839
+ }
840
+ catch { /* skip */ }
841
+ }
842
+ }
843
+ }
844
+ onChunk({ content: '', done: true });
845
+ return {
846
+ id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`,
847
+ object: 'chat.completion',
848
+ created: Math.floor(Date.now() / 1000),
849
+ model,
850
+ choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
851
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
852
+ };
853
+ }
854
+ async callAnthropicStream(model, messages, temperature, maxTokens, onChunk) {
855
+ const baseUrl = this.config.baseUrl || 'https://api.anthropic.com';
856
+ const apiKey = this.config.apiKey;
857
+ if (!apiKey)
858
+ throw new Error('Anthropic API key not configured');
859
+ let systemPrompt;
860
+ const chatMessages = [];
861
+ for (const msg of messages) {
862
+ if (msg.role === 'system')
863
+ systemPrompt = msg.content;
864
+ else
865
+ chatMessages.push({ role: msg.role, content: msg.content });
866
+ }
867
+ const requestBody = { model, messages: chatMessages, max_tokens: maxTokens || 1024, stream: true };
868
+ if (systemPrompt)
869
+ requestBody.system = systemPrompt;
870
+ if (temperature !== undefined)
871
+ requestBody.temperature = temperature;
872
+ const response = await fetch(`${baseUrl}/v1/messages`, {
873
+ method: 'POST',
874
+ headers: {
875
+ 'Content-Type': 'application/json',
876
+ 'x-api-key': apiKey,
877
+ 'anthropic-version': '2023-06-01'
878
+ },
879
+ body: JSON.stringify(requestBody)
880
+ });
881
+ if (!response.ok)
882
+ throw new Error(`Anthropic returned status ${response.status}`);
883
+ let fullContent = '';
884
+ const reader = response.body?.getReader();
885
+ const decoder = new TextDecoder();
886
+ if (reader) {
887
+ let buffer = '';
888
+ while (true) {
889
+ const { done, value } = await reader.read();
890
+ if (done)
891
+ break;
892
+ buffer += decoder.decode(value, { stream: true });
893
+ const lines = buffer.split('\n');
894
+ buffer = lines.pop() || '';
895
+ for (const line of lines) {
896
+ if (!line.startsWith('data: '))
897
+ continue;
898
+ try {
899
+ const event = JSON.parse(line.slice(6));
900
+ if (event.type === 'content_block_delta' && event.delta?.text) {
901
+ fullContent += event.delta.text;
902
+ onChunk({ content: event.delta.text, done: false });
903
+ }
904
+ }
905
+ catch { /* skip */ }
906
+ }
907
+ }
908
+ }
909
+ onChunk({ content: '', done: true });
910
+ return {
911
+ id: `msg-${Math.random().toString(36).substring(2, 10)}`,
912
+ object: 'chat.completion',
913
+ created: Math.floor(Date.now() / 1000),
914
+ model,
915
+ choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
916
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
917
+ };
918
+ }
331
919
  convertAnthropicToOpenAiFormat(anthropicResponse, model) {
332
920
  const content = (anthropicResponse.content || [])
333
921
  .filter((block) => block.type === 'text')
@@ -6,11 +6,17 @@ export declare class AgentRunner {
6
6
  private scanTimer;
7
7
  private running;
8
8
  constructor(config: AgentConfig);
9
- static loadConfig(configPath?: string): AgentConfig;
9
+ /**
10
+ * Fetch optimal relay URL from Platform API using agent token.
11
+ * Returns null on failure (caller should fall back to default).
12
+ */
13
+ private static fetchRelayUrlForAgent;
14
+ static loadConfig(configPath?: string): Promise<AgentConfig>;
10
15
  start(): Promise<void>;
11
16
  private scanAndUpdateSessions;
12
17
  private startSessionHandler;
13
18
  private stopSessionHandler;
14
19
  private createTmuxSession;
20
+ private fetchEncKey;
15
21
  stop(): void;
16
22
  }