vigthoria-cli 1.6.20 → 1.6.22

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.
@@ -217,6 +217,13 @@ export declare class APIClient {
217
217
  private getVigFlowAccessToken;
218
218
  private getVigFlowHeaders;
219
219
  private withVigFlow;
220
+ /**
221
+ * Build the correct sub-path for VigFlow endpoints.
222
+ * Local servers (e.g. localhost:5060) need `/api/…` prefix.
223
+ * The remote gateway URL already ends with `/api/vigflow`, so appending
224
+ * another `/api/…` would double the prefix and cause 404s.
225
+ */
226
+ private vigFlowEndpoint;
220
227
  listVigFlowTemplates(options?: {
221
228
  category?: string;
222
229
  search?: string;
@@ -339,6 +346,11 @@ export declare class APIClient {
339
346
  }[];
340
347
  suggestions: string[];
341
348
  }>;
349
+ /**
350
+ * Lightweight client-side heuristic scan: catches common code smells
351
+ * AND logic/arithmetic bugs so review never returns "score 30, no issues".
352
+ */
353
+ private heuristicCodeIssues;
342
354
  fixCode(code: string, language: string, fixType: string): Promise<{
343
355
  fixed: string;
344
356
  changes: {
package/dist/utils/api.js CHANGED
@@ -307,13 +307,19 @@ class APIClient {
307
307
  }
308
308
  getVigFlowBaseUrls() {
309
309
  const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
310
+ // Put the remote gateway first, since local VigFlow servers are
311
+ // rarely running for end-user CLI installations. This avoids
312
+ // wasting connection-attempt time on 127.0.0.1 and hitting the
313
+ // remote gateway only after the local attempts have already
314
+ // errored — which surfaces as a confusing "last error" 404 in
315
+ // some setups.
310
316
  const urls = [
311
317
  process.env.VIGTHORIA_VIGFLOW_URL,
312
318
  process.env.VIGFLOW_URL,
313
319
  process.env.WORKFLOW_BUILDER_URL,
320
+ `${configuredApiUrl}/api/vigflow`,
314
321
  'http://127.0.0.1:5060',
315
322
  'http://127.0.0.1:5050',
316
- `${configuredApiUrl}/api/vigflow`,
317
323
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
318
324
  return [...new Set(urls)];
319
325
  }
@@ -793,6 +799,19 @@ class APIClient {
793
799
  }
794
800
  throw lastError || new Error(`No VigFlow backend available for ${operation}.`);
795
801
  }
802
+ /**
803
+ * Build the correct sub-path for VigFlow endpoints.
804
+ * Local servers (e.g. localhost:5060) need `/api/…` prefix.
805
+ * The remote gateway URL already ends with `/api/vigflow`, so appending
806
+ * another `/api/…` would double the prefix and cause 404s.
807
+ */
808
+ vigFlowEndpoint(baseUrl, subPath) {
809
+ if (/\/api\/vigflow\/?$/i.test(baseUrl)) {
810
+ // Remote gateway – subPath like '/templates' is enough
811
+ return `${baseUrl.replace(/\/$/, '')}${subPath}`;
812
+ }
813
+ return `${baseUrl}/api${subPath}`;
814
+ }
796
815
  async listVigFlowTemplates(options = {}) {
797
816
  return this.withVigFlow('list templates', async (baseUrl, headers) => {
798
817
  const query = new URLSearchParams();
@@ -802,7 +821,7 @@ class APIClient {
802
821
  if (options.search) {
803
822
  query.set('search', options.search);
804
823
  }
805
- const url = `${baseUrl}/api/templates${query.size > 0 ? `?${query.toString()}` : ''}`;
824
+ const url = `${this.vigFlowEndpoint(baseUrl, '/templates')}${query.size > 0 ? `?${query.toString()}` : ''}`;
806
825
  const response = await axios_1.default.get(url, {
807
826
  headers,
808
827
  timeout: 30000,
@@ -813,7 +832,7 @@ class APIClient {
813
832
  }
814
833
  async listVigFlowWorkflows() {
815
834
  return this.withVigFlow('list workflows', async (baseUrl, headers) => {
816
- const response = await axios_1.default.get(`${baseUrl}/api/workflows`, {
835
+ const response = await axios_1.default.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
817
836
  headers,
818
837
  timeout: 30000,
819
838
  });
@@ -865,7 +884,7 @@ class APIClient {
865
884
  }
866
885
  async useVigFlowTemplate(templateId, options = {}) {
867
886
  return this.withVigFlow('use template', async (baseUrl, headers) => {
868
- const response = await axios_1.default.post(`${baseUrl}/api/templates/${encodeURIComponent(templateId)}/use`, {
887
+ const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
869
888
  name: options.name,
870
889
  variables: options.variables || {},
871
890
  }, {
@@ -881,7 +900,7 @@ class APIClient {
881
900
  }
882
901
  async runVigFlowWorkflow(workflowId, options = {}) {
883
902
  return this.withVigFlow('run workflow', async (baseUrl, headers) => {
884
- const response = await axios_1.default.post(`${baseUrl}/api/executions/run/${encodeURIComponent(workflowId)}`, {
903
+ const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
885
904
  data: options.data || {},
886
905
  options: options.executionOptions || {},
887
906
  }, {
@@ -897,7 +916,7 @@ class APIClient {
897
916
  }
898
917
  async getVigFlowExecutionStatus(executionId) {
899
918
  return this.withVigFlow('execution status', async (baseUrl, headers) => {
900
- const response = await axios_1.default.get(`${baseUrl}/api/executions/${encodeURIComponent(executionId)}`, {
919
+ const response = await axios_1.default.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
901
920
  headers,
902
921
  timeout: 30000,
903
922
  });
@@ -918,6 +937,11 @@ class APIClient {
918
937
  const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
919
938
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
920
939
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
940
+ // When the server cannot directly access the workspace (e.g. Windows
941
+ // client), use the local path as a hint and flag that the workspace
942
+ // files are provided inline in localWorkspaceSummary.workspaceFiles.
943
+ const effectiveWorkspacePath = serverWorkspacePath || localWorkspacePath || null;
944
+ const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
921
945
  const payload = {
922
946
  workspace: resolvedContext.workspace || null,
923
947
  activeFile: resolvedContext.activeFile || null,
@@ -931,12 +955,16 @@ class APIClient {
931
955
  executionSurface: resolvedContext.executionSurface || 'cli',
932
956
  clientSurface: resolvedContext.clientSurface || 'cli',
933
957
  localMachineCapable: resolvedContext.localMachineCapable !== false,
934
- workspacePath: serverWorkspacePath || null,
935
- projectPath: serverWorkspacePath || null,
936
- targetPath: serverWorkspacePath || null,
958
+ workspacePath: effectiveWorkspacePath,
959
+ projectPath: effectiveWorkspacePath,
960
+ targetPath: effectiveWorkspacePath,
937
961
  localWorkspacePath: localWorkspacePath || null,
938
962
  localWorkspaceName: localWorkspacePath ? path_1.default.basename(localWorkspacePath) : null,
939
963
  localWorkspaceSummary,
964
+ // Signal to the server that the workspace filesystem is not locally
965
+ // accessible — it must hydrate a temp directory from the provided
966
+ // workspaceFiles before the agent starts using tools.
967
+ workspaceHydrationRequired: needsHydration,
940
968
  contextId: resolvedContext.contextId,
941
969
  traceId: resolvedContext.traceId,
942
970
  mcpContextId: resolvedContext.mcpContextId || null,
@@ -2302,11 +2330,11 @@ document.addEventListener('DOMContentLoaded', () => {
2302
2330
  return result.message;
2303
2331
  }
2304
2332
  if (Array.isArray(data?.events)) {
2305
- const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string');
2333
+ const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
2306
2334
  if (completionEvent) {
2307
2335
  return completionEvent.summary;
2308
2336
  }
2309
- const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string');
2337
+ const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string' && event.content.trim());
2310
2338
  if (messageEvent) {
2311
2339
  return messageEvent.content;
2312
2340
  }
@@ -2333,7 +2361,7 @@ document.addEventListener('DOMContentLoaded', () => {
2333
2361
  const toolResults = [];
2334
2362
  const filesRead = [];
2335
2363
  const filesWritten = [];
2336
- let lastAssistantText = '';
2364
+ const assistantFragments = [];
2337
2365
  for (const event of events) {
2338
2366
  if (!event)
2339
2367
  continue;
@@ -2352,16 +2380,22 @@ document.addEventListener('DOMContentLoaded', () => {
2352
2380
  }
2353
2381
  }
2354
2382
  if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
2355
- lastAssistantText = event.content.trim();
2383
+ assistantFragments.push(event.content.trim());
2356
2384
  }
2357
2385
  // Some servers emit 'text' events for incremental assistant text
2358
2386
  if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
2359
- lastAssistantText = event.content.trim();
2387
+ assistantFragments.push(event.content.trim());
2388
+ }
2389
+ // Some servers emit content_block_delta for streamed text
2390
+ if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
2391
+ assistantFragments.push(event.delta.text.trim());
2360
2392
  }
2361
2393
  }
2362
- // Prefer the last assistant text the model emitted
2363
- if (lastAssistantText.length > 20) {
2364
- return lastAssistantText;
2394
+ // Concatenate ALL assistant text fragments in order — keeps full
2395
+ // multi-turn reasoning instead of only the last fragment.
2396
+ const fullAssistantText = assistantFragments.join('\n\n').trim();
2397
+ if (fullAssistantText.length > 20) {
2398
+ return fullAssistantText;
2365
2399
  }
2366
2400
  // Otherwise build a summary from tool evidence
2367
2401
  const sections = [];
@@ -2722,6 +2756,13 @@ document.addEventListener('DOMContentLoaded', () => {
2722
2756
  throw new Error(errors.join(' | '));
2723
2757
  }
2724
2758
  formatOperatorResponse(data = {}) {
2759
+ // If the server returned a direct answer field, prefer it (for lookup tasks)
2760
+ if (typeof data.answer === 'string' && data.answer.trim()) {
2761
+ return data.answer.trim();
2762
+ }
2763
+ if (typeof data.result === 'string' && data.result.trim()) {
2764
+ return data.result.trim();
2765
+ }
2725
2766
  const lines = [];
2726
2767
  if (data.summary) {
2727
2768
  lines.push(String(data.summary).trim());
@@ -3200,13 +3241,19 @@ document.addEventListener('DOMContentLoaded', () => {
3200
3241
  // Prepend a forceful scope-enforcement instruction so the model
3201
3242
  // doesn't expand a small task into an oversized glossy page.
3202
3243
  const scopedPrompt = [
3203
- 'IMPORTANT — SCOPE CONSTRAINTS:',
3204
- '1. Follow the user\'s scope literally. Output ONLY what was requested.',
3205
- '2. If the user says "tiny", "small", "simple", or "minimal", produce ≤ 50 lines of code.',
3206
- '3. Do NOT add: hero sections, animations, gradients, Google Fonts, Font Awesome, responsive breakpoints, or landing-page patterns UNLESS the user explicitly requests them.',
3207
- '4. Do NOT add external CDN links or dependencies unless the user asks for them.',
3208
- '5. Prefer inline styles or a small <style> block. No large CSS frameworks.',
3209
- '6. Return raw code only no markdown fences, no explanations.',
3244
+ 'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
3245
+ '1. Output ONLY what the user explicitly asked for. Nothing more.',
3246
+ '2. If the prompt is 15 words, produce ≤ 80 lines of code maximum.',
3247
+ '3. If the user says "tiny", "small", "simple", "minimal", or "basic", produce 50 lines.',
3248
+ '4. NEVER add any of these unless the user EXPLICITLY requests them:',
3249
+ ' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
3250
+ ' - CSS animations, gradients, neon effects, glass-morphism, particles',
3251
+ ' - Google Fonts, Font Awesome, external CDN links, icon libraries',
3252
+ ' - Responsive breakpoints, media queries (unless asked)',
3253
+ ' - Multiple pages or components when one was requested',
3254
+ '5. Prefer inline styles or a small <style> block. No CSS frameworks.',
3255
+ '6. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
3256
+ '7. Match the complexity of the request: a "hello world" is 1-5 lines, a "button" is 5-15 lines.',
3210
3257
  '',
3211
3258
  prompt,
3212
3259
  ].join('\n');
@@ -3243,22 +3290,109 @@ document.addEventListener('DOMContentLoaded', () => {
3243
3290
  const response = await this.client.post('/api/ai/review', {
3244
3291
  code,
3245
3292
  language,
3293
+ instructions: [
3294
+ 'Return concrete, line-specific issues with severity.',
3295
+ 'Every issue MUST reference a line number.',
3296
+ 'If the score is below 50, you MUST list at least 2 specific issues.',
3297
+ 'Prioritize REAL BUGS over style issues:',
3298
+ '- Wrong arithmetic operators (+ instead of -, * instead of /, etc.)',
3299
+ '- Logic errors (function named "add" using subtraction, wrong comparisons)',
3300
+ '- Off-by-one errors, incorrect return values',
3301
+ '- Type mismatches, null/undefined access',
3302
+ 'Only report style issues (console.log, naming) AFTER listing all real bugs.',
3303
+ ].join(' '),
3246
3304
  });
3247
3305
  const raw = response.data ?? {};
3248
3306
  const score = typeof raw.score === 'number' ? raw.score : 0;
3249
3307
  const issues = Array.isArray(raw.issues) ? raw.issues : [];
3250
3308
  const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
3251
- // Prevent contradictory output: low score but zero issues
3309
+ // Always run client-side heuristics and merge any findings the
3310
+ // server missed. This ensures arithmetic/logic bugs are surfaced
3311
+ // even when the server only reports style issues like console.log.
3312
+ const heuristic = this.heuristicCodeIssues(code, language);
3313
+ for (const h of heuristic) {
3314
+ // Avoid duplicating issues the server already reported on the same line
3315
+ const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
3316
+ if (!isDuplicate) {
3317
+ issues.push(h);
3318
+ }
3319
+ }
3320
+ // Sort: errors first, then warnings, then info
3321
+ const severityOrder = { error: 0, warning: 1, info: 2 };
3322
+ issues.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
3323
+ // Prevent contradictory output: low score but zero issues.
3252
3324
  if (score < 50 && issues.length === 0) {
3253
3325
  issues.push({
3254
3326
  type: 'quality',
3255
- line: 0,
3256
- message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. This typically means the review model returned incomplete data. Consider re-running the review or inspecting the file manually.`,
3327
+ line: 1,
3328
+ message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
3257
3329
  severity: 'warning',
3258
3330
  });
3259
3331
  }
3260
3332
  return { score, issues, suggestions };
3261
3333
  }
3334
+ /**
3335
+ * Lightweight client-side heuristic scan: catches common code smells
3336
+ * AND logic/arithmetic bugs so review never returns "score 30, no issues".
3337
+ */
3338
+ heuristicCodeIssues(code, language) {
3339
+ const issues = [];
3340
+ const lines = code.split('\n');
3341
+ for (let i = 0; i < lines.length; i++) {
3342
+ const line = lines[i];
3343
+ const lineNum = i + 1;
3344
+ // console.log left in production code
3345
+ if (/\bconsole\.(log|debug|info)\b/.test(line) && !/\/\//.test(line.slice(0, line.indexOf('console')))) {
3346
+ issues.push({ type: 'quality', line: lineNum, message: 'console.log/debug statement — remove or replace with proper logging.', severity: 'warning' });
3347
+ }
3348
+ // TODO/FIXME/HACK comments
3349
+ if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line)) {
3350
+ issues.push({ type: 'maintainability', line: lineNum, message: `Unresolved ${line.match(/\b(TODO|FIXME|HACK|XXX)\b/)?.[0]} comment.`, severity: 'info' });
3351
+ }
3352
+ // Empty catch blocks
3353
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line)) {
3354
+ issues.push({ type: 'error-handling', line: lineNum, message: 'Empty catch block — errors are silently swallowed.', severity: 'warning' });
3355
+ }
3356
+ // Very long lines (> 200 chars)
3357
+ if (line.length > 200) {
3358
+ issues.push({ type: 'style', line: lineNum, message: `Line exceeds 200 characters (${line.length}).`, severity: 'info' });
3359
+ }
3360
+ // Arithmetic / logic bugs: function named 'add' but using subtraction
3361
+ if (/\breturn\s+\w+\s*-\s*\w+/.test(line)) {
3362
+ // Check if the enclosing function name implies addition
3363
+ for (let j = i; j >= Math.max(0, i - 5); j--) {
3364
+ if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
3365
+ issues.push({ type: 'logic', line: lineNum, message: 'Subtraction operator in a function named "add" — likely should be addition (+).', severity: 'error' });
3366
+ break;
3367
+ }
3368
+ }
3369
+ }
3370
+ // Multiplication where addition is expected
3371
+ if (/\breturn\s+\w+\s*\*\s*\w+/.test(line)) {
3372
+ for (let j = i; j >= Math.max(0, i - 5); j--) {
3373
+ if (/function\s+add\b/i.test(lines[j]) || /\badd\s*[=(]/.test(lines[j])) {
3374
+ issues.push({ type: 'logic', line: lineNum, message: 'Multiplication operator in a function named "add" — likely should be addition (+).', severity: 'error' });
3375
+ break;
3376
+ }
3377
+ }
3378
+ }
3379
+ // Division where subtraction/addition is expected in subtract
3380
+ if (/\breturn\s+\w+\s*\+\s*\w+/.test(line)) {
3381
+ for (let j = i; j >= Math.max(0, i - 5); j--) {
3382
+ if (/function\s+subtract\b/i.test(lines[j]) || /\bsubtract\s*[=(]/.test(lines[j])) {
3383
+ issues.push({ type: 'logic', line: lineNum, message: 'Addition operator in a function named "subtract" — likely should be subtraction (-).', severity: 'error' });
3384
+ break;
3385
+ }
3386
+ }
3387
+ }
3388
+ // Off-by-one: comparing with === where <= or >= may be needed
3389
+ // (not implemented yet — would require more complex AST analysis)
3390
+ // Limit to 15 heuristic issues
3391
+ if (issues.length >= 15)
3392
+ break;
3393
+ }
3394
+ return issues;
3395
+ }
3262
3396
  async fixCode(code, language, fixType) {
3263
3397
  // Client-side syntax pre-check: detect obvious errors and include
3264
3398
  // them in the request so the model has concrete signals.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Vigthoria CLI → DevTools Bridge Telemetry Client
3
+ *
4
+ * Connects the local CLI to the remote bridge server in "commando" mode,
5
+ * streaming real-time activity (commands, tool calls, model responses,
6
+ * file edits, errors) and receiving admin-issued commands.
7
+ *
8
+ * Design principles:
9
+ * - Fire-and-forget: never blocks the CLI main flow
10
+ * - Auto-reconnects with exponential back-off
11
+ * - Opt-in via --bridge <url> flag
12
+ * - Sensitive data (API keys, tokens) is never transmitted
13
+ */
14
+ export type TelemetryEventType = 'cli:start' | 'cli:command' | 'cli:prompt' | 'cli:model-response' | 'cli:tool-call' | 'cli:tool-result' | 'cli:file-edit' | 'cli:error' | 'cli:end' | 'cli:mode-change' | 'cli:heartbeat';
15
+ export interface TelemetryEvent {
16
+ type: TelemetryEventType;
17
+ payload: Record<string, unknown>;
18
+ ts: number;
19
+ clientId: string;
20
+ }
21
+ export interface AdminCommand {
22
+ type: 'admin:command';
23
+ action: string;
24
+ params?: Record<string, unknown>;
25
+ requestId?: string;
26
+ }
27
+ export interface BridgeClientOptions {
28
+ bridgeUrl: string;
29
+ apiKey?: string;
30
+ machineLabel?: string;
31
+ onAdminCommand?: (cmd: AdminCommand) => void;
32
+ }
33
+ /** Get the active bridge client (may be null if --bridge was not used). */
34
+ export declare function getBridgeClient(): BridgeClient | null;
35
+ export declare class BridgeClient {
36
+ private ws;
37
+ private url;
38
+ private apiKey;
39
+ private machineLabel;
40
+ private clientId;
41
+ private connected;
42
+ private reconnectTimer;
43
+ private heartbeatTimer;
44
+ private queue;
45
+ private maxQueueSize;
46
+ private reconnectDelay;
47
+ private destroyed;
48
+ private onAdminCommand?;
49
+ constructor(opts: BridgeClientOptions);
50
+ connect(): Promise<void>;
51
+ destroy(): void;
52
+ get isConnected(): boolean;
53
+ /** CLI session started (command, flags, cwd). */
54
+ emitStart(data: {
55
+ command: string;
56
+ flags: Record<string, unknown>;
57
+ cwd: string;
58
+ }): void;
59
+ /** User entered a prompt / message. */
60
+ emitPrompt(data: {
61
+ prompt: string;
62
+ mode: string;
63
+ model: string;
64
+ }): void;
65
+ /** Model response received (summary only, not full content). */
66
+ emitModelResponse(data: {
67
+ model: string;
68
+ chars: number;
69
+ hasToolCalls: boolean;
70
+ preview: string;
71
+ }): void;
72
+ /** Tool is being called. */
73
+ emitToolCall(data: {
74
+ tool: string;
75
+ args: Record<string, string>;
76
+ }): void;
77
+ /** Tool finished executing. */
78
+ emitToolResult(data: {
79
+ tool: string;
80
+ success: boolean;
81
+ preview: string;
82
+ }): void;
83
+ /** File was written or edited. */
84
+ emitFileEdit(data: {
85
+ file: string;
86
+ action: 'write' | 'edit';
87
+ linesChanged: number;
88
+ }): void;
89
+ /** Error occurred. */
90
+ emitError(data: {
91
+ message: string;
92
+ code?: string;
93
+ }): void;
94
+ /** Mode changed (agent / operator / chat). */
95
+ emitModeChange(data: {
96
+ mode: string;
97
+ model: string;
98
+ }): void;
99
+ /** Session ended. */
100
+ emitEnd(data: {
101
+ reason: string;
102
+ }): void;
103
+ private emit;
104
+ private sendRaw;
105
+ private bufferEvent;
106
+ private flushQueue;
107
+ private scheduleReconnect;
108
+ private startHeartbeat;
109
+ private stopHeartbeat;
110
+ }