@absolutejs/voice 0.0.22-beta.127 → 0.0.22-beta.129

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.
@@ -0,0 +1,27 @@
1
+ import type { RealtimeAdapter } from './types';
2
+ export type OpenAIRealtimeModel = 'gpt-realtime' | 'gpt-realtime-mini' | 'gpt-4o-realtime-preview' | 'gpt-4o-mini-realtime-preview' | (string & {});
3
+ export type OpenAIRealtimeVoice = 'alloy' | 'ash' | 'ballad' | 'cedar' | 'coral' | 'echo' | 'marin' | 'sage' | 'shimmer' | 'verse' | {
4
+ id: string;
5
+ } | (string & {});
6
+ export type OpenAIRealtimeTranscriptionModel = 'gpt-4o-mini-transcribe' | 'gpt-4o-transcribe' | 'whisper-1' | (string & {});
7
+ export type OpenAIRealtimeNoiseReduction = 'near_field' | 'far_field';
8
+ export type OpenAIRealtimeResponseMode = 'audio' | 'text';
9
+ export type OpenAIRealtimeAdapterOptions = {
10
+ apiKey: string;
11
+ autoCommitSilenceMs?: number;
12
+ baseUrl?: string;
13
+ emitResponseTranscripts?: boolean;
14
+ inputTranscriptionLanguage?: string;
15
+ inputTranscriptionModel?: OpenAIRealtimeTranscriptionModel | null;
16
+ inputTranscriptionPrompt?: string;
17
+ instructions?: string;
18
+ maxOutputTokens?: number | 'inf';
19
+ model?: OpenAIRealtimeModel;
20
+ noiseReduction?: OpenAIRealtimeNoiseReduction;
21
+ responseMode?: OpenAIRealtimeResponseMode;
22
+ speed?: number;
23
+ temperature?: number;
24
+ voice?: OpenAIRealtimeVoice;
25
+ webSocket?: typeof WebSocket;
26
+ };
27
+ export declare const createOpenAIRealtimeAdapter: (options: OpenAIRealtimeAdapterOptions) => RealtimeAdapter;
@@ -2567,6 +2567,17 @@ var serverMessageToAction = (message) => {
2567
2567
  transcript: message.transcript,
2568
2568
  type: "partial"
2569
2569
  };
2570
+ case "replay":
2571
+ return {
2572
+ assistantTexts: message.assistantTexts,
2573
+ call: message.call,
2574
+ partial: message.partial,
2575
+ scenarioId: message.scenarioId,
2576
+ sessionId: message.sessionId,
2577
+ status: message.status,
2578
+ turns: message.turns,
2579
+ type: "replay"
2580
+ };
2570
2581
  case "session":
2571
2582
  return {
2572
2583
  sessionId: message.sessionId,
@@ -2631,6 +2642,7 @@ var isVoiceServerMessage = (value) => {
2631
2642
  case "final":
2632
2643
  case "partial":
2633
2644
  case "pong":
2645
+ case "replay":
2634
2646
  case "session":
2635
2647
  case "turn":
2636
2648
  return true;
@@ -2895,6 +2907,20 @@ var createVoiceStreamStore = () => {
2895
2907
  partial: action.transcript.text
2896
2908
  };
2897
2909
  break;
2910
+ case "replay":
2911
+ state = {
2912
+ ...state,
2913
+ assistantTexts: [...action.assistantTexts],
2914
+ call: action.call ?? null,
2915
+ error: null,
2916
+ isConnected: action.status === "active",
2917
+ partial: action.partial,
2918
+ scenarioId: action.scenarioId ?? state.scenarioId,
2919
+ sessionId: action.sessionId,
2920
+ status: action.status,
2921
+ turns: [...action.turns]
2922
+ };
2923
+ break;
2898
2924
  case "session":
2899
2925
  state = {
2900
2926
  ...state,
@@ -865,6 +865,17 @@ var serverMessageToAction = (message) => {
865
865
  transcript: message.transcript,
866
866
  type: "partial"
867
867
  };
868
+ case "replay":
869
+ return {
870
+ assistantTexts: message.assistantTexts,
871
+ call: message.call,
872
+ partial: message.partial,
873
+ scenarioId: message.scenarioId,
874
+ sessionId: message.sessionId,
875
+ status: message.status,
876
+ turns: message.turns,
877
+ type: "replay"
878
+ };
868
879
  case "session":
869
880
  return {
870
881
  sessionId: message.sessionId,
@@ -929,6 +940,7 @@ var isVoiceServerMessage = (value) => {
929
940
  case "final":
930
941
  case "partial":
931
942
  case "pong":
943
+ case "replay":
932
944
  case "session":
933
945
  case "turn":
934
946
  return true;
@@ -1193,6 +1205,20 @@ var createVoiceStreamStore = () => {
1193
1205
  partial: action.transcript.text
1194
1206
  };
1195
1207
  break;
1208
+ case "replay":
1209
+ state = {
1210
+ ...state,
1211
+ assistantTexts: [...action.assistantTexts],
1212
+ call: action.call ?? null,
1213
+ error: null,
1214
+ isConnected: action.status === "active",
1215
+ partial: action.partial,
1216
+ scenarioId: action.scenarioId ?? state.scenarioId,
1217
+ sessionId: action.sessionId,
1218
+ status: action.status,
1219
+ turns: [...action.turns]
1220
+ };
1221
+ break;
1196
1222
  case "session":
1197
1223
  state = {
1198
1224
  ...state,
@@ -2,7 +2,7 @@ import { Elysia } from 'elysia';
2
2
  import type { VoiceTelephonySetupStatus, VoiceTelephonySmokeCheck, VoiceTelephonySmokeReport } from './contract';
3
3
  import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions } from '../telephonyOutcome';
4
4
  import { type VoiceCallReviewArtifact, type VoiceCallReviewConfig } from '../testing/review';
5
- import type { AudioFormat, VoiceLogger, VoicePluginConfig, VoiceSessionRecord, VoiceServerMessage } from '../types';
5
+ import type { AudioFormat, STTAdapter, VoiceLogger, VoicePluginConfig, VoiceSessionRecord, VoiceServerMessage } from '../types';
6
6
  type TwilioMediaPayload = {
7
7
  chunk?: string;
8
8
  payload: string;
@@ -78,7 +78,7 @@ export type TwilioMediaStreamSocket = {
78
78
  close: (code?: number, reason?: string) => void | Promise<void>;
79
79
  send: (data: string) => void | Promise<void>;
80
80
  };
81
- export type TwilioMediaStreamBridgeOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Omit<VoicePluginConfig<TContext, TSession, TResult>, 'htmx' | 'path'> & {
81
+ export type TwilioMediaStreamBridgeOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Omit<VoicePluginConfig<TContext, TSession, TResult>, 'htmx' | 'path' | 'stt'> & {
82
82
  clearOnInboundMedia?: boolean;
83
83
  context: TContext;
84
84
  logger?: VoiceLogger;
@@ -97,6 +97,7 @@ export type TwilioMediaStreamBridgeOptions<TContext = unknown, TSession extends
97
97
  };
98
98
  scenarioId?: string;
99
99
  sessionId?: string;
100
+ stt: STTAdapter;
100
101
  };
101
102
  export type TwilioMediaStreamBridge = {
102
103
  close: (reason?: string) => Promise<void>;
@@ -2126,6 +2126,17 @@ var serverMessageToAction = (message) => {
2126
2126
  transcript: message.transcript,
2127
2127
  type: "partial"
2128
2128
  };
2129
+ case "replay":
2130
+ return {
2131
+ assistantTexts: message.assistantTexts,
2132
+ call: message.call,
2133
+ partial: message.partial,
2134
+ scenarioId: message.scenarioId,
2135
+ sessionId: message.sessionId,
2136
+ status: message.status,
2137
+ turns: message.turns,
2138
+ type: "replay"
2139
+ };
2129
2140
  case "session":
2130
2141
  return {
2131
2142
  sessionId: message.sessionId,
@@ -2190,6 +2201,7 @@ var isVoiceServerMessage = (value) => {
2190
2201
  case "final":
2191
2202
  case "partial":
2192
2203
  case "pong":
2204
+ case "replay":
2193
2205
  case "session":
2194
2206
  case "turn":
2195
2207
  return true;
@@ -2454,6 +2466,20 @@ var createVoiceStreamStore = () => {
2454
2466
  partial: action.transcript.text
2455
2467
  };
2456
2468
  break;
2469
+ case "replay":
2470
+ state = {
2471
+ ...state,
2472
+ assistantTexts: [...action.assistantTexts],
2473
+ call: action.call ?? null,
2474
+ error: null,
2475
+ isConnected: action.status === "active",
2476
+ partial: action.partial,
2477
+ scenarioId: action.scenarioId ?? state.scenarioId,
2478
+ sessionId: action.sessionId,
2479
+ status: action.status,
2480
+ turns: [...action.turns]
2481
+ };
2482
+ break;
2457
2483
  case "session":
2458
2484
  state = {
2459
2485
  ...state,
@@ -5033,6 +5059,12 @@ var DEFAULT_FORMAT = {
5033
5059
  encoding: "pcm_s16le",
5034
5060
  sampleRateHz: 16000
5035
5061
  };
5062
+ var DEFAULT_REALTIME_FORMAT = {
5063
+ channels: 1,
5064
+ container: "raw",
5065
+ encoding: "pcm_s16le",
5066
+ sampleRateHz: 24000
5067
+ };
5036
5068
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
5037
5069
  var createEmptyCurrentTurn = () => ({
5038
5070
  finalText: "",
@@ -5310,6 +5342,18 @@ var createVoiceSession = (options) => {
5310
5342
  type: "call_lifecycle"
5311
5343
  });
5312
5344
  };
5345
+ const sendReplay = async (session) => {
5346
+ await send({
5347
+ assistantTexts: session.turns.flatMap((turn) => turn.assistantText ? [turn.assistantText] : []),
5348
+ call: session.call,
5349
+ partial: session.currentTurn.partialText,
5350
+ scenarioId: session.scenarioId,
5351
+ sessionId: options.id,
5352
+ status: session.status,
5353
+ turns: session.turns,
5354
+ type: "replay"
5355
+ });
5356
+ };
5313
5357
  const runHandoff = async (input) => {
5314
5358
  const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
5315
5359
  action: input.action,
@@ -5413,6 +5457,23 @@ var createVoiceSession = (options) => {
5413
5457
  });
5414
5458
  }
5415
5459
  };
5460
+ const sendAssistantAudio = async (chunk, input) => {
5461
+ const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
5462
+ await send({
5463
+ chunkBase64: encodeBase64(normalizedChunk),
5464
+ format: input.format,
5465
+ receivedAt: input.receivedAt,
5466
+ turnId: activeTTSTurnId,
5467
+ type: "audio"
5468
+ });
5469
+ if (activeTTSTurnId) {
5470
+ await appendTurnLatencyStage({
5471
+ at: input.receivedAt,
5472
+ stage: "assistant_audio_received",
5473
+ turnId: activeTTSTurnId
5474
+ });
5475
+ }
5476
+ };
5416
5477
  const scheduleTurnCommit = (delayMs, reason, reset = true) => {
5417
5478
  if (!reset && silenceTimer) {
5418
5479
  return;
@@ -6114,8 +6175,12 @@ var createVoiceSession = (options) => {
6114
6175
  if (sttSession) {
6115
6176
  return sttSession;
6116
6177
  }
6117
- const openedSession = await options.stt.open({
6118
- format: DEFAULT_FORMAT,
6178
+ const inputAdapter = options.realtime ?? options.stt;
6179
+ if (!inputAdapter) {
6180
+ throw new Error("Voice session requires either an stt or realtime adapter.");
6181
+ }
6182
+ const openedSession = await inputAdapter.open({
6183
+ format: options.realtime ? options.realtimeInputFormat ?? DEFAULT_REALTIME_FORMAT : DEFAULT_FORMAT,
6119
6184
  languageStrategy: options.languageStrategy,
6120
6185
  lexicon,
6121
6186
  phraseHints,
@@ -6150,6 +6215,16 @@ var createVoiceSession = (options) => {
6150
6215
  openedSession.on("close", (event) => {
6151
6216
  runAdapterEvent("adapter.close", () => handleClose(event));
6152
6217
  });
6218
+ if (options.realtime) {
6219
+ openedSession.on("audio", ({ chunk, format, receivedAt }) => {
6220
+ runAdapterEvent("adapter.audio", async () => {
6221
+ await sendAssistantAudio(chunk, {
6222
+ format,
6223
+ receivedAt
6224
+ });
6225
+ });
6226
+ });
6227
+ }
6153
6228
  return openedSession;
6154
6229
  };
6155
6230
  const ensureTTSSession = async () => {
@@ -6174,21 +6249,10 @@ var createVoiceSession = (options) => {
6174
6249
  if (ttsSession !== openedSession) {
6175
6250
  return;
6176
6251
  }
6177
- const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
6178
- await send({
6179
- chunkBase64: encodeBase64(normalizedChunk),
6252
+ await sendAssistantAudio(chunk, {
6180
6253
  format,
6181
- receivedAt,
6182
- turnId: activeTTSTurnId,
6183
- type: "audio"
6254
+ receivedAt
6184
6255
  });
6185
- if (activeTTSTurnId) {
6186
- await appendTurnLatencyStage({
6187
- at: receivedAt,
6188
- stage: "assistant_audio_received",
6189
- turnId: activeTTSTurnId
6190
- });
6191
- }
6192
6256
  });
6193
6257
  });
6194
6258
  openedSession.on("error", (event) => {
@@ -6267,7 +6331,8 @@ var createVoiceSession = (options) => {
6267
6331
  await appendTrace({
6268
6332
  payload: {
6269
6333
  text: output.assistantText,
6270
- ttsConfigured: Boolean(options.tts)
6334
+ ttsConfigured: Boolean(options.tts),
6335
+ realtimeConfigured: Boolean(options.realtime)
6271
6336
  },
6272
6337
  session,
6273
6338
  turnId: turn.id,
@@ -6299,9 +6364,35 @@ var createVoiceSession = (options) => {
6299
6364
  turnId: turn.id,
6300
6365
  type: "turn.assistant"
6301
6366
  });
6367
+ } else if (options.realtime) {
6368
+ const activeRealtimeSession = await ensureAdapter();
6369
+ const realtimeStartedAt = Date.now();
6370
+ activeTTSTurnId = turn.id;
6371
+ await appendTurnLatencyStage({
6372
+ at: realtimeStartedAt,
6373
+ session,
6374
+ stage: "tts_send_started",
6375
+ turnId: turn.id
6376
+ });
6377
+ await activeRealtimeSession.send(output.assistantText);
6378
+ await appendTurnLatencyStage({
6379
+ session,
6380
+ stage: "tts_send_completed",
6381
+ turnId: turn.id
6382
+ });
6383
+ await appendTrace({
6384
+ payload: {
6385
+ elapsedMs: Date.now() - realtimeStartedAt,
6386
+ mode: "realtime",
6387
+ status: "sent"
6388
+ },
6389
+ session,
6390
+ turnId: turn.id,
6391
+ type: "turn.assistant"
6392
+ });
6302
6393
  }
6303
6394
  } catch (error) {
6304
- logger.warn("voice tts send failed", {
6395
+ logger.warn("voice assistant audio send failed", {
6305
6396
  error: toError(error).message,
6306
6397
  sessionId: options.id,
6307
6398
  turnId: turn.id
@@ -6309,7 +6400,7 @@ var createVoiceSession = (options) => {
6309
6400
  await appendTrace({
6310
6401
  payload: {
6311
6402
  error: toError(error).message,
6312
- status: "tts-send-failed"
6403
+ status: options.realtime ? "realtime-send-failed" : "tts-send-failed"
6313
6404
  },
6314
6405
  session,
6315
6406
  turnId: turn.id,
@@ -6514,7 +6605,7 @@ var createVoiceSession = (options) => {
6514
6605
  turn,
6515
6606
  type: "turn"
6516
6607
  });
6517
- if (options.sttLifecycle === "turn-scoped") {
6608
+ if (options.stt && options.sttLifecycle === "turn-scoped") {
6518
6609
  await closeAdapter("turn-commit");
6519
6610
  }
6520
6611
  await completeTurn(updatedSession, turn);
@@ -6577,6 +6668,7 @@ var createVoiceSession = (options) => {
6577
6668
  scenarioId: session.scenarioId,
6578
6669
  type: "session"
6579
6670
  });
6671
+ await sendReplay(session);
6580
6672
  if (shouldFireOnSession) {
6581
6673
  await options.route.onCallStart?.({
6582
6674
  api,
@@ -9600,7 +9692,7 @@ var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenc
9600
9692
  };
9601
9693
  };
9602
9694
  // src/testing/tts.ts
9603
- var DEFAULT_REALTIME_FORMAT = {
9695
+ var DEFAULT_REALTIME_FORMAT2 = {
9604
9696
  channels: 1,
9605
9697
  container: "raw",
9606
9698
  encoding: "pcm_s16le",
@@ -9659,7 +9751,7 @@ var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
9659
9751
  let audioDurationMs = 0;
9660
9752
  let audioChunkCount = 0;
9661
9753
  const session = adapter.kind === "realtime" ? await adapter.open({
9662
- format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT,
9754
+ format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT2,
9663
9755
  sessionId: `tts-benchmark:${fixture.id}`,
9664
9756
  ...openOptions ?? {}
9665
9757
  }) : await adapter.open({
package/dist/types.d.ts CHANGED
@@ -616,9 +616,11 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
616
616
  lexicon?: VoiceLexiconEntry[] | VoiceLexiconResolver<TContext>;
617
617
  phraseHints?: VoicePhraseHint[] | VoicePhraseHintResolver<TContext>;
618
618
  preset?: VoiceRuntimePreset;
619
- stt: STTAdapter;
619
+ stt?: STTAdapter;
620
620
  sttFallback?: VoiceSTTFallbackConfig;
621
621
  sttLifecycle?: VoiceSTTLifecycle;
622
+ realtime?: RealtimeAdapter;
623
+ realtimeInputFormat?: AudioFormat;
622
624
  tts?: TTSAdapter;
623
625
  session: VoiceSessionStore<NoInfer<TSession>>;
624
626
  reconnect?: VoiceReconnectConfig;
@@ -635,7 +637,9 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
635
637
  id: string;
636
638
  context: TContext;
637
639
  socket: VoiceSocket;
638
- stt: STTAdapter;
640
+ stt?: STTAdapter;
641
+ realtime?: RealtimeAdapter;
642
+ realtimeInputFormat?: AudioFormat;
639
643
  tts?: TTSAdapter;
640
644
  languageStrategy?: VoiceLanguageStrategy;
641
645
  lexicon?: VoiceLexiconEntry[];
@@ -682,6 +686,16 @@ export type VoiceServerSessionMessage = {
682
686
  status: VoiceSessionStatus;
683
687
  scenarioId?: string;
684
688
  };
689
+ export type VoiceServerReplayMessage<TResult = unknown> = {
690
+ type: 'replay';
691
+ assistantTexts: string[];
692
+ call?: VoiceCallLifecycleState;
693
+ partial: string;
694
+ scenarioId?: string;
695
+ sessionId: string;
696
+ status: VoiceSessionStatus;
697
+ turns: VoiceTurnRecord<TResult>[];
698
+ };
685
699
  export type VoiceServerPartialMessage = {
686
700
  type: 'partial';
687
701
  transcript: Transcript;
@@ -723,7 +737,7 @@ export type VoiceServerErrorMessage = {
723
737
  export type VoiceServerPongMessage = {
724
738
  type: 'pong';
725
739
  };
726
- export type VoiceServerMessage<TResult = unknown> = VoiceServerSessionMessage | VoiceServerPartialMessage | VoiceServerFinalMessage | VoiceServerTurnMessage<TResult> | VoiceServerAssistantMessage | VoiceServerAudioMessage | VoiceServerCallLifecycleMessage | VoiceServerCompleteMessage | VoiceServerErrorMessage | VoiceServerPongMessage;
740
+ export type VoiceServerMessage<TResult = unknown> = VoiceServerSessionMessage | VoiceServerReplayMessage<TResult> | VoiceServerPartialMessage | VoiceServerFinalMessage | VoiceServerTurnMessage<TResult> | VoiceServerAssistantMessage | VoiceServerAudioMessage | VoiceServerCallLifecycleMessage | VoiceServerCompleteMessage | VoiceServerErrorMessage | VoiceServerPongMessage;
727
741
  export type VoiceConnectionOptions = {
728
742
  protocols?: string[];
729
743
  scenarioId?: string;
@@ -974,6 +988,15 @@ export type VoiceStoreAction<TResult = unknown> = {
974
988
  sessionId: string;
975
989
  scenarioId?: string;
976
990
  status: VoiceSessionStatus;
991
+ } | {
992
+ type: 'replay';
993
+ assistantTexts: string[];
994
+ call?: VoiceCallLifecycleState;
995
+ partial: string;
996
+ scenarioId?: string;
997
+ sessionId: string;
998
+ status: VoiceSessionStatus;
999
+ turns: VoiceTurnRecord<TResult>[];
977
1000
  } | {
978
1001
  type: 'call_lifecycle';
979
1002
  event: VoiceCallLifecycleEvent;
package/dist/vue/index.js CHANGED
@@ -2338,6 +2338,17 @@ var serverMessageToAction = (message) => {
2338
2338
  transcript: message.transcript,
2339
2339
  type: "partial"
2340
2340
  };
2341
+ case "replay":
2342
+ return {
2343
+ assistantTexts: message.assistantTexts,
2344
+ call: message.call,
2345
+ partial: message.partial,
2346
+ scenarioId: message.scenarioId,
2347
+ sessionId: message.sessionId,
2348
+ status: message.status,
2349
+ turns: message.turns,
2350
+ type: "replay"
2351
+ };
2341
2352
  case "session":
2342
2353
  return {
2343
2354
  sessionId: message.sessionId,
@@ -2402,6 +2413,7 @@ var isVoiceServerMessage = (value) => {
2402
2413
  case "final":
2403
2414
  case "partial":
2404
2415
  case "pong":
2416
+ case "replay":
2405
2417
  case "session":
2406
2418
  case "turn":
2407
2419
  return true;
@@ -2666,6 +2678,20 @@ var createVoiceStreamStore = () => {
2666
2678
  partial: action.transcript.text
2667
2679
  };
2668
2680
  break;
2681
+ case "replay":
2682
+ state = {
2683
+ ...state,
2684
+ assistantTexts: [...action.assistantTexts],
2685
+ call: action.call ?? null,
2686
+ error: null,
2687
+ isConnected: action.status === "active",
2688
+ partial: action.partial,
2689
+ scenarioId: action.scenarioId ?? state.scenarioId,
2690
+ sessionId: action.sessionId,
2691
+ status: action.status,
2692
+ turns: [...action.turns]
2693
+ };
2694
+ break;
2669
2695
  case "session":
2670
2696
  state = {
2671
2697
  ...state,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.127",
3
+ "version": "0.0.22-beta.129",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",