@love-moon/conductor-sdk 0.2.17 → 0.2.19

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.
@@ -74,6 +74,7 @@ export declare class BackendApiClient {
74
74
  id?: string;
75
75
  projectId: string;
76
76
  title: string;
77
+ status?: string;
77
78
  backendType?: string;
78
79
  sessionId?: string | null;
79
80
  sessionFilePath?: string | null;
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@ import { ConductorConfig } from './config/index.js';
3
3
  import { MessageRouter } from './message/index.js';
4
4
  import { DownstreamCursorStore, DurableUpstreamOutboxStore } from './outbox/index.js';
5
5
  import { SessionDiskStore, SessionManager } from './session/index.js';
6
- import { ConductorWebSocketClient } from './ws/index.js';
6
+ import { ConductorWebSocketClient, WebSocketConnectedEvent, WebSocketDisconnectEvent } from './ws/index.js';
7
7
  type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'updateTask' | 'commitSdkMessage' | 'commitTaskStatusUpdate' | 'commitAgentCommandAck' | 'commitTaskStopAck' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
8
8
  type RealtimeClientLike = Pick<ConductorWebSocketClient, 'registerHandler' | 'connect' | 'disconnect' | 'sendJson'>;
9
9
  export interface ConductorClientConnectOptions {
@@ -21,10 +21,8 @@ export interface ConductorClientConnectOptions {
21
21
  upstreamOutbox?: DurableUpstreamOutboxStore;
22
22
  downstreamCursorStore?: DownstreamCursorStore;
23
23
  agentHost?: string;
24
- onConnected?: (event: {
25
- isReconnect: boolean;
26
- }) => void;
27
- onDisconnected?: () => void;
24
+ onConnected?: (event: WebSocketConnectedEvent) => void;
25
+ onDisconnected?: (event: WebSocketDisconnectEvent) => void;
28
26
  onStopTask?: (event: StopTaskEvent) => Promise<void> | void;
29
27
  }
30
28
  interface ConductorClientInit {
package/dist/client.js CHANGED
@@ -5,7 +5,7 @@ import { getPlanLimitMessageFromError } from './limits/index.js';
5
5
  import { MessageRouter } from './message/index.js';
6
6
  import { DownstreamCursorStore, DurableUpstreamOutboxStore, normalizeDownstreamCommandCursor, } from './outbox/index.js';
7
7
  import { SessionDiskStore, SessionManager, currentHostname } from './session/index.js';
8
- import { ConductorWebSocketClient } from './ws/index.js';
8
+ import { ConductorWebSocketClient, } from './ws/index.js';
9
9
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10
10
  export class ConductorClient {
11
11
  config;
@@ -131,6 +131,7 @@ export class ConductorClient {
131
131
  id: taskId,
132
132
  projectId,
133
133
  title,
134
+ status: 'running',
134
135
  backendType,
135
136
  sessionId: logicalSessionId,
136
137
  sessionFilePath: explicitSessionFilePath,
@@ -841,6 +842,8 @@ function formatMessagesResponse(messages) {
841
842
  role: msg.role,
842
843
  content: msg.content,
843
844
  ack_token: msg.ackToken,
845
+ metadata: msg.metadata ?? undefined,
846
+ attachments: msg.attachments?.length ? msg.attachments : undefined,
844
847
  created_at: msg.createdAt.toISOString(),
845
848
  })),
846
849
  next_ack_token: messages.length ? messages[messages.length - 1].ackToken ?? null : null,
@@ -16,6 +16,8 @@ export declare class MessageRouter {
16
16
  private resolveMessageId;
17
17
  private coerceRole;
18
18
  private coerceContent;
19
+ private coerceMetadata;
20
+ private coerceAttachments;
19
21
  private formatActionContent;
20
22
  private ensureSession;
21
23
  }
@@ -26,6 +26,8 @@ export class MessageRouter {
26
26
  role: this.coerceRole(data.role, 'user'),
27
27
  content: this.coerceContent(data.content),
28
28
  ackToken: data.ack_token ? String(data.ack_token) : null,
29
+ metadata: this.coerceMetadata(data.metadata),
30
+ attachments: this.coerceAttachments(data.attachments),
29
31
  });
30
32
  await this.notify(taskId);
31
33
  }
@@ -36,6 +38,8 @@ export class MessageRouter {
36
38
  role: this.coerceRole(data.role, 'action'),
37
39
  content: this.formatActionContent(data),
38
40
  ackToken: data.ack_token ? String(data.ack_token) : null,
41
+ metadata: this.coerceMetadata(data.metadata),
42
+ attachments: this.coerceAttachments(data.attachments),
39
43
  });
40
44
  await this.notify(taskId);
41
45
  }
@@ -92,6 +96,20 @@ export class MessageRouter {
92
96
  return String(content);
93
97
  }
94
98
  }
99
+ coerceMetadata(metadata) {
100
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
101
+ return null;
102
+ }
103
+ return { ...metadata };
104
+ }
105
+ coerceAttachments(attachments) {
106
+ if (!Array.isArray(attachments)) {
107
+ return [];
108
+ }
109
+ return attachments
110
+ .filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
111
+ .map((entry) => ({ ...entry }));
112
+ }
95
113
  formatActionContent(data) {
96
114
  if (typeof data.content === 'string' && data.content.trim()) {
97
115
  return data.content;
@@ -1,8 +1,11 @@
1
+ type JsonRecord = Record<string, unknown>;
1
2
  export interface MessageInput {
2
3
  messageId: string;
3
4
  role: string;
4
5
  content: string;
5
6
  ackToken?: string | null;
7
+ metadata?: JsonRecord | null;
8
+ attachments?: JsonRecord[];
6
9
  }
7
10
  export declare class MessageRecord {
8
11
  readonly messageId: string;
@@ -10,6 +13,8 @@ export declare class MessageRecord {
10
13
  readonly content: string;
11
14
  readonly createdAt: Date;
12
15
  readonly ackToken?: string | null;
16
+ readonly metadata?: JsonRecord | null;
17
+ readonly attachments: JsonRecord[];
13
18
  constructor(init: MessageInput);
14
19
  }
15
20
  export declare class SessionState {
@@ -37,3 +42,4 @@ export declare class SessionManager {
37
42
  endSession(taskId: string): Promise<void>;
38
43
  private ensureEvent;
39
44
  }
45
+ export {};
@@ -1,15 +1,33 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ function normalizeJsonRecord(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
4
+ return null;
5
+ }
6
+ return { ...value };
7
+ }
8
+ function normalizeJsonRecordArray(value) {
9
+ if (!Array.isArray(value)) {
10
+ return [];
11
+ }
12
+ return value
13
+ .filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
14
+ .map((entry) => ({ ...entry }));
15
+ }
2
16
  export class MessageRecord {
3
17
  messageId;
4
18
  role;
5
19
  content;
6
20
  createdAt;
7
21
  ackToken;
22
+ metadata;
23
+ attachments;
8
24
  constructor(init) {
9
25
  this.messageId = init.messageId;
10
26
  this.role = init.role;
11
27
  this.content = init.content;
12
28
  this.ackToken = init.ackToken;
29
+ this.metadata = normalizeJsonRecord(init.metadata);
30
+ this.attachments = normalizeJsonRecordArray(init.attachments);
13
31
  this.createdAt = new Date();
14
32
  }
15
33
  }
@@ -1,9 +1,17 @@
1
1
  import { ConductorConfig } from '../config/index.js';
2
2
  export type WebSocketHandler = (payload: Record<string, any>) => Promise<void> | void;
3
+ export interface WebSocketCloseInfo {
4
+ code?: number | null;
5
+ reason?: string | null;
6
+ }
3
7
  export interface WebSocketLike {
4
8
  send(data: string): Promise<void> | void;
5
9
  ping(): Promise<void> | void;
6
10
  close(): Promise<void> | void;
11
+ terminate?(): Promise<void> | void;
12
+ onPong?(handler: () => void): void;
13
+ onCloseInfo?(handler: (info: WebSocketCloseInfo) => void): void;
14
+ onErrorInfo?(handler: (error: unknown) => void): void;
7
15
  closed?: boolean;
8
16
  [Symbol.asyncIterator](): AsyncIterableIterator<string>;
9
17
  }
@@ -11,16 +19,35 @@ export interface ConnectOptions {
11
19
  headers: Record<string, string>;
12
20
  }
13
21
  export type ConnectImpl = (url: string, options: ConnectOptions) => Promise<WebSocketLike>;
22
+ export interface WebSocketConnectedEvent {
23
+ isReconnect: boolean;
24
+ connectedAt: number;
25
+ }
26
+ export interface WebSocketPongEvent {
27
+ at: number;
28
+ latencyMs: number | null;
29
+ }
30
+ export interface WebSocketDisconnectEvent {
31
+ reason: string;
32
+ disconnectedAt: number;
33
+ connectedAt: number | null;
34
+ closeCode: number | null;
35
+ closeReason: string | null;
36
+ socketError: string | null;
37
+ missedPongs: number;
38
+ lastPingAt: number | null;
39
+ lastPongAt: number | null;
40
+ lastMessageAt: number | null;
41
+ }
14
42
  export interface WebSocketClientOptions {
15
43
  reconnectDelay?: number;
16
44
  heartbeatInterval?: number;
17
45
  extraHeaders?: Record<string, string>;
18
46
  connectImpl?: ConnectImpl;
19
47
  hostName?: string;
20
- onConnected?: (event: {
21
- isReconnect: boolean;
22
- }) => void;
23
- onDisconnected?: () => void;
48
+ onConnected?: (event: WebSocketConnectedEvent) => void;
49
+ onDisconnected?: (event: WebSocketDisconnectEvent) => void;
50
+ onPong?: (event: WebSocketPongEvent) => void;
24
51
  onReconnected?: () => void;
25
52
  }
26
53
  export declare class ConductorWebSocketClient {
@@ -31,10 +58,12 @@ export declare class ConductorWebSocketClient {
31
58
  private readonly connectImpl;
32
59
  private readonly onConnected?;
33
60
  private readonly onDisconnected?;
61
+ private readonly onPong?;
34
62
  private readonly onReconnected?;
35
63
  private readonly handlers;
36
64
  private readonly extraHeaders;
37
65
  private conn;
66
+ private runtime;
38
67
  private stop;
39
68
  private listenTask;
40
69
  private heartbeatTask;
@@ -45,6 +74,7 @@ export declare class ConductorWebSocketClient {
45
74
  registerHandler(handler: WebSocketHandler): void;
46
75
  connect(): Promise<void>;
47
76
  disconnect(): Promise<void>;
77
+ forceReconnect(reason?: string): Promise<void>;
48
78
  sendJson(payload: Record<string, any>): Promise<void>;
49
79
  private ensureConnection;
50
80
  private openConnection;
@@ -59,4 +89,11 @@ export declare class ConductorWebSocketClient {
59
89
  private isConnectionClosed;
60
90
  private sendWithReconnect;
61
91
  private isNotOpenError;
92
+ private createRuntime;
93
+ private attachConnectionObservers;
94
+ private getRuntime;
95
+ private markDisconnectReason;
96
+ private buildDisconnectEvent;
97
+ private closeConnection;
98
+ private terminateConnection;
62
99
  }
package/dist/ws/client.js CHANGED
@@ -16,10 +16,12 @@ export class ConductorWebSocketClient {
16
16
  connectImpl;
17
17
  onConnected;
18
18
  onDisconnected;
19
+ onPong;
19
20
  onReconnected;
20
21
  handlers = [];
21
22
  extraHeaders;
22
23
  conn = null;
24
+ runtime = null;
23
25
  stop = false;
24
26
  listenTask = null;
25
27
  heartbeatTask = null;
@@ -38,6 +40,7 @@ export class ConductorWebSocketClient {
38
40
  this.connectImpl = options.connectImpl ?? defaultConnectImpl;
39
41
  this.onConnected = options.onConnected;
40
42
  this.onDisconnected = options.onDisconnected;
43
+ this.onPong = options.onPong;
41
44
  this.onReconnected = options.onReconnected;
42
45
  }
43
46
  registerHandler(handler) {
@@ -60,9 +63,20 @@ export class ConductorWebSocketClient {
60
63
  this.heartbeatTask = null;
61
64
  }
62
65
  if (this.conn && !this.isConnectionClosed(this.conn)) {
63
- await this.conn.close();
66
+ this.markDisconnectReason(this.conn, 'manual_disconnect');
67
+ await this.closeConnection(this.conn);
64
68
  }
65
69
  this.conn = null;
70
+ this.runtime = null;
71
+ }
72
+ async forceReconnect(reason = 'manual_reconnect') {
73
+ const conn = this.conn;
74
+ if (!conn || this.isConnectionClosed(conn)) {
75
+ await this.openConnection(true);
76
+ return;
77
+ }
78
+ this.markDisconnectReason(conn, reason);
79
+ await this.terminateConnection(conn);
66
80
  }
67
81
  async sendJson(payload) {
68
82
  await this.ensureConnection();
@@ -83,15 +97,19 @@ export class ConductorWebSocketClient {
83
97
  while (!this.stop) {
84
98
  try {
85
99
  const headers = { Authorization: `Bearer ${this.token}`, ...this.extraHeaders };
86
- this.conn = await this.connectImpl(this.url, { headers });
100
+ const conn = await this.connectImpl(this.url, { headers });
101
+ const runtime = this.createRuntime(conn);
102
+ this.attachConnectionObservers(runtime);
103
+ this.conn = conn;
104
+ this.runtime = runtime;
87
105
  const isReconnect = this.hasConnectedAtLeastOnce;
88
106
  this.hasConnectedAtLeastOnce = true;
89
- this.notifyConnected(isReconnect);
107
+ this.notifyConnected({ isReconnect, connectedAt: runtime.connectedAt });
90
108
  if (isReconnect) {
91
109
  this.notifyReconnected();
92
110
  }
93
- this.listenTask = this.listenLoop(this.conn);
94
- this.heartbeatTask = this.heartbeatLoop(this.conn);
111
+ this.listenTask = this.listenLoop(conn);
112
+ this.heartbeatTask = this.heartbeatLoop(conn);
95
113
  return;
96
114
  }
97
115
  catch (error) {
@@ -124,10 +142,26 @@ export class ConductorWebSocketClient {
124
142
  try {
125
143
  while (!this.stop && !this.isConnectionClosed(conn)) {
126
144
  await wait(this.heartbeatInterval, this.waitController.signal);
145
+ if (this.stop || conn !== this.conn) {
146
+ return;
147
+ }
148
+ const runtime = this.getRuntime(conn);
149
+ if (!runtime) {
150
+ return;
151
+ }
152
+ if (runtime.supportsPongTracking && runtime.waitingForPong) {
153
+ runtime.missedPongs += 1;
154
+ runtime.disconnectReason = 'pong_timeout';
155
+ await this.terminateConnection(conn);
156
+ break;
157
+ }
158
+ runtime.lastPingAt = Date.now();
159
+ runtime.waitingForPong = runtime.supportsPongTracking;
127
160
  try {
128
161
  await conn.ping();
129
162
  }
130
163
  catch {
164
+ this.markDisconnectReason(conn, 'ping_failed');
131
165
  break;
132
166
  }
133
167
  }
@@ -140,11 +174,17 @@ export class ConductorWebSocketClient {
140
174
  if (this.stop || conn !== this.conn) {
141
175
  return;
142
176
  }
177
+ const runtime = this.getRuntime(conn);
143
178
  this.conn = null;
144
- this.notifyDisconnected();
179
+ this.runtime = null;
180
+ this.notifyDisconnected(this.buildDisconnectEvent(runtime));
145
181
  await this.openConnection(true);
146
182
  }
147
183
  async dispatch(message) {
184
+ const now = Date.now();
185
+ if (this.runtime) {
186
+ this.runtime.lastMessageAt = now;
187
+ }
148
188
  let payload;
149
189
  try {
150
190
  payload = JSON.parse(message);
@@ -159,12 +199,12 @@ export class ConductorWebSocketClient {
159
199
  }
160
200
  }
161
201
  }
162
- notifyConnected(isReconnect) {
202
+ notifyConnected(event) {
163
203
  if (!this.onConnected) {
164
204
  return;
165
205
  }
166
206
  try {
167
- this.onConnected({ isReconnect });
207
+ this.onConnected(event);
168
208
  }
169
209
  catch {
170
210
  // Swallow callback errors to avoid impacting reconnect behavior.
@@ -181,12 +221,12 @@ export class ConductorWebSocketClient {
181
221
  // Swallow callback errors to avoid impacting reconnect behavior.
182
222
  }
183
223
  }
184
- notifyDisconnected() {
224
+ notifyDisconnected(event) {
185
225
  if (!this.onDisconnected) {
186
226
  return;
187
227
  }
188
228
  try {
189
- this.onDisconnected();
229
+ this.onDisconnected(event);
190
230
  }
191
231
  catch {
192
232
  // Swallow callback errors to avoid impacting reconnect behavior.
@@ -222,8 +262,10 @@ export class ConductorWebSocketClient {
222
262
  }
223
263
  attemptedReconnect = true;
224
264
  if (this.conn === conn) {
265
+ this.markDisconnectReason(conn, 'send_reconnect');
225
266
  this.conn = null;
226
- this.notifyDisconnected();
267
+ this.notifyDisconnected(this.buildDisconnectEvent(this.getRuntime(conn)));
268
+ this.runtime = null;
227
269
  }
228
270
  await this.openConnection(true);
229
271
  }
@@ -235,6 +277,100 @@ export class ConductorWebSocketClient {
235
277
  const message = error instanceof Error ? error.message : String(error);
236
278
  return message.toLowerCase().includes('websocket is not open');
237
279
  }
280
+ createRuntime(conn) {
281
+ const connectedAt = Date.now();
282
+ return {
283
+ conn,
284
+ connectedAt,
285
+ supportsPongTracking: typeof conn.onPong === 'function',
286
+ lastMessageAt: null,
287
+ lastPingAt: null,
288
+ lastPongAt: connectedAt,
289
+ waitingForPong: false,
290
+ missedPongs: 0,
291
+ disconnectReason: null,
292
+ closeCode: null,
293
+ closeReason: null,
294
+ socketError: null,
295
+ };
296
+ }
297
+ attachConnectionObservers(runtime) {
298
+ runtime.conn.onPong?.(() => {
299
+ const activeRuntime = this.getRuntime(runtime.conn);
300
+ if (!activeRuntime) {
301
+ return;
302
+ }
303
+ const at = Date.now();
304
+ const latencyMs = activeRuntime.lastPingAt ? at - activeRuntime.lastPingAt : null;
305
+ activeRuntime.lastPongAt = at;
306
+ activeRuntime.waitingForPong = false;
307
+ activeRuntime.missedPongs = 0;
308
+ if (this.onPong) {
309
+ try {
310
+ this.onPong({ at, latencyMs });
311
+ }
312
+ catch {
313
+ // Swallow callback errors to avoid impacting reconnect behavior.
314
+ }
315
+ }
316
+ });
317
+ runtime.conn.onCloseInfo?.((info) => {
318
+ const activeRuntime = this.getRuntime(runtime.conn);
319
+ if (!activeRuntime) {
320
+ return;
321
+ }
322
+ activeRuntime.closeCode = typeof info.code === 'number' ? info.code : null;
323
+ activeRuntime.closeReason = normalizeOptionalString(info.reason);
324
+ });
325
+ runtime.conn.onErrorInfo?.((error) => {
326
+ const activeRuntime = this.getRuntime(runtime.conn);
327
+ if (!activeRuntime) {
328
+ return;
329
+ }
330
+ activeRuntime.socketError = error instanceof Error ? error.message : String(error);
331
+ if (!activeRuntime.disconnectReason) {
332
+ activeRuntime.disconnectReason = 'socket_error';
333
+ }
334
+ });
335
+ }
336
+ getRuntime(conn) {
337
+ if (!this.runtime || this.runtime.conn !== conn) {
338
+ return null;
339
+ }
340
+ return this.runtime;
341
+ }
342
+ markDisconnectReason(conn, reason) {
343
+ const runtime = this.getRuntime(conn);
344
+ if (runtime && !runtime.disconnectReason) {
345
+ runtime.disconnectReason = reason;
346
+ }
347
+ }
348
+ buildDisconnectEvent(runtime) {
349
+ return {
350
+ reason: runtime?.disconnectReason || 'connection_lost',
351
+ disconnectedAt: Date.now(),
352
+ connectedAt: runtime?.connectedAt ?? null,
353
+ closeCode: runtime?.closeCode ?? null,
354
+ closeReason: runtime?.closeReason ?? null,
355
+ socketError: runtime?.socketError ?? null,
356
+ missedPongs: runtime?.missedPongs ?? 0,
357
+ lastPingAt: runtime?.lastPingAt ?? null,
358
+ lastPongAt: runtime?.lastPongAt ?? null,
359
+ lastMessageAt: runtime?.lastMessageAt ?? null,
360
+ };
361
+ }
362
+ async closeConnection(conn) {
363
+ if (typeof conn.close === 'function') {
364
+ await conn.close();
365
+ }
366
+ }
367
+ async terminateConnection(conn) {
368
+ if (typeof conn.terminate === 'function') {
369
+ await conn.terminate();
370
+ return;
371
+ }
372
+ await this.closeConnection(conn);
373
+ }
238
374
  }
239
375
  async function wait(ms, signal) {
240
376
  if (!signal) {
@@ -276,16 +412,34 @@ class WsAdapter {
276
412
  ws;
277
413
  queue = [];
278
414
  waiters = [];
415
+ pongHandlers = new Set();
416
+ closeInfoHandlers = new Set();
417
+ errorHandlers = new Set();
279
418
  closed = false;
280
419
  constructor(ws) {
281
420
  this.ws = ws;
282
421
  ws.on('message', (data) => this.enqueue(data.toString()));
283
- ws.on('close', () => {
422
+ ws.on('pong', () => {
423
+ for (const handler of this.pongHandlers) {
424
+ handler();
425
+ }
426
+ });
427
+ ws.on('close', (code, reason) => {
284
428
  this.closed = true;
429
+ const closeInfo = {
430
+ code,
431
+ reason: normalizeOptionalString(reason?.toString()),
432
+ };
433
+ for (const handler of this.closeInfoHandlers) {
434
+ handler(closeInfo);
435
+ }
285
436
  this.enqueue(null);
286
437
  });
287
- ws.on('error', () => {
438
+ ws.on('error', (error) => {
288
439
  this.closed = true;
440
+ for (const handler of this.errorHandlers) {
441
+ handler(error);
442
+ }
289
443
  this.enqueue(null);
290
444
  });
291
445
  }
@@ -323,6 +477,21 @@ class WsAdapter {
323
477
  this.ws.close();
324
478
  });
325
479
  }
480
+ terminate() {
481
+ if (this.closed) {
482
+ return;
483
+ }
484
+ this.ws.terminate();
485
+ }
486
+ onPong(handler) {
487
+ this.pongHandlers.add(handler);
488
+ }
489
+ onCloseInfo(handler) {
490
+ this.closeInfoHandlers.add(handler);
491
+ }
492
+ onErrorInfo(handler) {
493
+ this.errorHandlers.add(handler);
494
+ }
326
495
  async *[Symbol.asyncIterator]() {
327
496
  while (true) {
328
497
  const value = await this.nextValue();
@@ -351,3 +520,10 @@ class WsAdapter {
351
520
  }
352
521
  }
353
522
  }
523
+ function normalizeOptionalString(value) {
524
+ if (typeof value !== 'string') {
525
+ return null;
526
+ }
527
+ const trimmed = value.trim();
528
+ return trimmed || null;
529
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,5 +27,5 @@
27
27
  "typescript": "^5.6.3",
28
28
  "vitest": "^2.1.4"
29
29
  },
30
- "gitCommitId": "c2654e1"
30
+ "gitCommitId": "346e048"
31
31
  }