@blinkdotnew/sdk 2.0.1 → 2.0.3

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.
package/dist/index.d.mts CHANGED
@@ -836,7 +836,7 @@ declare class HttpClient {
836
836
  signal?: AbortSignal;
837
837
  }): Promise<BlinkResponse<any>>;
838
838
  /**
839
- * Stream AI text generation with Vercel AI SDK data stream format
839
+ * Stream AI text generation - uses Vercel AI SDK's pipeUIMessageStreamToResponse (Data Stream Protocol)
840
840
  */
841
841
  streamAiText(prompt: string, options: {
842
842
  model?: string | undefined;
@@ -860,7 +860,7 @@ declare class HttpClient {
860
860
  signal?: AbortSignal;
861
861
  }): Promise<BlinkResponse<any>>;
862
862
  /**
863
- * Stream AI object generation with Vercel AI SDK data stream format
863
+ * Stream AI object generation - uses Vercel AI SDK's pipeTextStreamToResponse
864
864
  */
865
865
  streamAiObject(prompt: string, options: {
866
866
  model?: string | undefined;
@@ -941,10 +941,10 @@ declare class HttpClient {
941
941
  private parseResponse;
942
942
  private handleErrorResponse;
943
943
  /**
944
- * Parse Vercel AI SDK data stream format
945
- * Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
944
+ * Parse Vercel AI SDK v5 Data Stream Protocol (Server-Sent Events)
945
+ * Supports all event types from the UI Message Stream protocol
946
946
  */
947
- private parseDataStream;
947
+ private parseDataStreamProtocol;
948
948
  }
949
949
 
950
950
  /**
package/dist/index.d.ts CHANGED
@@ -836,7 +836,7 @@ declare class HttpClient {
836
836
  signal?: AbortSignal;
837
837
  }): Promise<BlinkResponse<any>>;
838
838
  /**
839
- * Stream AI text generation with Vercel AI SDK data stream format
839
+ * Stream AI text generation - uses Vercel AI SDK's pipeUIMessageStreamToResponse (Data Stream Protocol)
840
840
  */
841
841
  streamAiText(prompt: string, options: {
842
842
  model?: string | undefined;
@@ -860,7 +860,7 @@ declare class HttpClient {
860
860
  signal?: AbortSignal;
861
861
  }): Promise<BlinkResponse<any>>;
862
862
  /**
863
- * Stream AI object generation with Vercel AI SDK data stream format
863
+ * Stream AI object generation - uses Vercel AI SDK's pipeTextStreamToResponse
864
864
  */
865
865
  streamAiObject(prompt: string, options: {
866
866
  model?: string | undefined;
@@ -941,10 +941,10 @@ declare class HttpClient {
941
941
  private parseResponse;
942
942
  private handleErrorResponse;
943
943
  /**
944
- * Parse Vercel AI SDK data stream format
945
- * Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
944
+ * Parse Vercel AI SDK v5 Data Stream Protocol (Server-Sent Events)
945
+ * Supports all event types from the UI Message Stream protocol
946
946
  */
947
- private parseDataStream;
947
+ private parseDataStreamProtocol;
948
948
  }
949
949
 
950
950
  /**
package/dist/index.js CHANGED
@@ -676,7 +676,7 @@ var HttpClient = class {
676
676
  });
677
677
  }
678
678
  /**
679
- * Stream AI text generation with Vercel AI SDK data stream format
679
+ * Stream AI text generation - uses Vercel AI SDK's pipeUIMessageStreamToResponse (Data Stream Protocol)
680
680
  */
681
681
  async streamAiText(prompt, options = {}, onChunk) {
682
682
  const url = this.buildUrl(`/api/ai/${this.projectId}/text`);
@@ -706,7 +706,7 @@ var HttpClient = class {
706
706
  if (!response.body) {
707
707
  throw new BlinkNetworkError("No response body for streaming");
708
708
  }
709
- return this.parseDataStream(response.body, onChunk);
709
+ return this.parseDataStreamProtocol(response.body, onChunk);
710
710
  } catch (error) {
711
711
  if (error instanceof BlinkError) {
712
712
  throw error;
@@ -731,7 +731,7 @@ var HttpClient = class {
731
731
  });
732
732
  }
733
733
  /**
734
- * Stream AI object generation with Vercel AI SDK data stream format
734
+ * Stream AI object generation - uses Vercel AI SDK's pipeTextStreamToResponse
735
735
  */
736
736
  async streamAiObject(prompt, options = {}, onPartial) {
737
737
  const url = this.buildUrl(`/api/ai/${this.projectId}/object`);
@@ -761,7 +761,35 @@ var HttpClient = class {
761
761
  if (!response.body) {
762
762
  throw new BlinkNetworkError("No response body for streaming");
763
763
  }
764
- return this.parseDataStream(response.body, void 0, onPartial);
764
+ const reader = response.body.getReader();
765
+ const decoder = new TextDecoder();
766
+ let buffer = "";
767
+ let latestObject = {};
768
+ try {
769
+ while (true) {
770
+ const { done, value } = await reader.read();
771
+ if (done) break;
772
+ const chunk = decoder.decode(value, { stream: true });
773
+ buffer += chunk;
774
+ try {
775
+ const parsed = JSON.parse(buffer);
776
+ latestObject = parsed;
777
+ if (onPartial) {
778
+ onPartial(parsed);
779
+ }
780
+ } catch {
781
+ }
782
+ }
783
+ if (buffer) {
784
+ try {
785
+ latestObject = JSON.parse(buffer);
786
+ } catch {
787
+ }
788
+ }
789
+ return { object: latestObject };
790
+ } finally {
791
+ reader.releaseLock();
792
+ }
765
793
  } catch (error) {
766
794
  if (error instanceof BlinkError) {
767
795
  throw error;
@@ -924,93 +952,94 @@ var HttpClient = class {
924
952
  }
925
953
  }
926
954
  /**
927
- * Parse Vercel AI SDK data stream format
928
- * Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
955
+ * Parse Vercel AI SDK v5 Data Stream Protocol (Server-Sent Events)
956
+ * Supports all event types from the UI Message Stream protocol
929
957
  */
930
- async parseDataStream(body, onChunk, onPartial) {
958
+ async parseDataStreamProtocol(body, onChunk) {
931
959
  const reader = body.getReader();
932
960
  const decoder = new TextDecoder();
961
+ const finalResult = {
962
+ text: "",
963
+ toolCalls: [],
964
+ toolResults: [],
965
+ sources: [],
966
+ files: [],
967
+ reasoning: []
968
+ };
933
969
  let buffer = "";
934
- let finalResult = {};
935
970
  try {
936
971
  while (true) {
937
972
  const { done, value } = await reader.read();
938
973
  if (done) break;
939
974
  buffer += decoder.decode(value, { stream: true });
940
- const lines = buffer.split(/\r?\n/);
975
+ const lines = buffer.split("\n");
941
976
  buffer = lines.pop() || "";
942
977
  for (const line of lines) {
943
978
  if (!line.trim()) continue;
979
+ if (line === "[DONE]") {
980
+ continue;
981
+ }
982
+ if (!line.startsWith("data: ")) continue;
944
983
  try {
945
- if (line.startsWith("f:")) {
946
- const metadata = JSON.parse(line.slice(2));
947
- finalResult.messageId = metadata.messageId;
948
- } else if (line.startsWith("0:")) {
949
- const textChunk = JSON.parse(line.slice(2));
950
- if (onChunk) {
951
- onChunk(textChunk);
952
- }
953
- finalResult.text = (finalResult.text || "") + textChunk;
954
- } else if (line.startsWith("2:")) {
955
- const data = JSON.parse(line.slice(2));
956
- if (Array.isArray(data) && data.length > 0) {
957
- const item = data[0];
958
- if (typeof item === "string") {
959
- finalResult.status = item;
960
- } else if (typeof item === "object") {
961
- if (onPartial) {
962
- onPartial(item);
963
- }
964
- finalResult.object = item;
984
+ const jsonStr = line.slice(6);
985
+ const part = JSON.parse(jsonStr);
986
+ switch (part.type) {
987
+ case "text-start":
988
+ break;
989
+ case "text-delta":
990
+ if (part.delta) {
991
+ finalResult.text += part.delta;
992
+ if (onChunk) onChunk(part.delta);
965
993
  }
966
- }
967
- } else if (line.startsWith("d:")) {
968
- const metadata = JSON.parse(line.slice(2));
969
- if (metadata.usage) {
970
- finalResult.usage = metadata.usage;
971
- }
972
- if (metadata.finishReason) {
973
- finalResult.finishReason = metadata.finishReason;
974
- }
975
- } else if (line.startsWith("e:")) {
976
- const errorData = JSON.parse(line.slice(2));
977
- finalResult.error = errorData;
978
- }
979
- } catch (error) {
980
- console.warn("Failed to parse stream line:", line, error);
981
- }
982
- }
983
- }
984
- if (buffer.trim()) {
985
- try {
986
- if (buffer.startsWith("0:")) {
987
- const textChunk = JSON.parse(buffer.slice(2));
988
- if (onChunk) {
989
- onChunk(textChunk);
990
- }
991
- finalResult.text = (finalResult.text || "") + textChunk;
992
- } else if (buffer.startsWith("2:")) {
993
- const data = JSON.parse(buffer.slice(2));
994
- if (Array.isArray(data) && data.length > 0) {
995
- const item = data[0];
996
- if (typeof item === "object") {
997
- if (onPartial) {
998
- onPartial(item);
994
+ if (part.textDelta) {
995
+ finalResult.text += part.textDelta;
996
+ if (onChunk) onChunk(part.textDelta);
999
997
  }
1000
- finalResult.object = item;
1001
- }
1002
- }
1003
- } else if (buffer.startsWith("d:")) {
1004
- const metadata = JSON.parse(buffer.slice(2));
1005
- if (metadata.usage) {
1006
- finalResult.usage = metadata.usage;
1007
- }
1008
- if (metadata.finishReason) {
1009
- finalResult.finishReason = metadata.finishReason;
998
+ break;
999
+ case "text-end":
1000
+ break;
1001
+ case "tool-call":
1002
+ finalResult.toolCalls.push({
1003
+ toolCallId: part.toolCallId,
1004
+ toolName: part.toolName,
1005
+ args: part.args
1006
+ });
1007
+ break;
1008
+ case "tool-result":
1009
+ finalResult.toolResults.push({
1010
+ toolCallId: part.toolCallId,
1011
+ toolName: part.toolName,
1012
+ result: part.result
1013
+ });
1014
+ break;
1015
+ case "source-url":
1016
+ finalResult.sources.push({
1017
+ id: part.id,
1018
+ url: part.url,
1019
+ title: part.title
1020
+ });
1021
+ break;
1022
+ case "file":
1023
+ finalResult.files.push(part.file);
1024
+ break;
1025
+ case "reasoning":
1026
+ finalResult.reasoning.push(part.content);
1027
+ break;
1028
+ case "finish":
1029
+ finalResult.finishReason = part.finishReason;
1030
+ finalResult.usage = part.usage;
1031
+ if (part.response) finalResult.response = part.response;
1032
+ break;
1033
+ case "error":
1034
+ finalResult.error = part.error;
1035
+ throw new Error(part.error);
1036
+ case "data":
1037
+ if (!finalResult.customData) finalResult.customData = [];
1038
+ finalResult.customData.push(part.value);
1039
+ break;
1010
1040
  }
1041
+ } catch (e) {
1011
1042
  }
1012
- } catch (error) {
1013
- console.warn("Failed to parse final buffer:", buffer, error);
1014
1043
  }
1015
1044
  }
1016
1045
  return finalResult;
@@ -3727,13 +3756,7 @@ var BlinkAIImpl = class {
3727
3756
  options.prompt || "",
3728
3757
  requestBody
3729
3758
  );
3730
- if (response.data?.result) {
3731
- return response.data.result;
3732
- } else if (response.data?.text) {
3733
- return response.data;
3734
- } else {
3735
- throw new BlinkAIError("Invalid response format: missing text");
3736
- }
3759
+ return response.data;
3737
3760
  } catch (error) {
3738
3761
  if (error instanceof BlinkAIError) {
3739
3762
  throw error;
@@ -3802,9 +3825,14 @@ var BlinkAIImpl = class {
3802
3825
  );
3803
3826
  return {
3804
3827
  text: result.text || "",
3805
- finishReason: "stop",
3828
+ finishReason: result.finishReason || "stop",
3806
3829
  usage: result.usage,
3807
- ...result
3830
+ toolCalls: result.toolCalls,
3831
+ toolResults: result.toolResults,
3832
+ sources: result.sources,
3833
+ files: result.files,
3834
+ reasoningDetails: result.reasoning,
3835
+ response: result.response
3808
3836
  };
3809
3837
  } catch (error) {
3810
3838
  if (error instanceof BlinkAIError) {
@@ -3883,13 +3911,7 @@ var BlinkAIImpl = class {
3883
3911
  signal: options.signal
3884
3912
  }
3885
3913
  );
3886
- if (response.data?.result) {
3887
- return response.data.result;
3888
- } else if (response.data?.object) {
3889
- return response.data;
3890
- } else {
3891
- throw new BlinkAIError("Invalid response format: missing object");
3892
- }
3914
+ return response.data;
3893
3915
  } catch (error) {
3894
3916
  if (error instanceof BlinkAIError) {
3895
3917
  throw error;
@@ -3951,8 +3973,7 @@ var BlinkAIImpl = class {
3951
3973
  return {
3952
3974
  object: result.object || {},
3953
3975
  finishReason: "stop",
3954
- usage: result.usage,
3955
- ...result
3976
+ usage: result.usage
3956
3977
  };
3957
3978
  } catch (error) {
3958
3979
  if (error instanceof BlinkAIError) {
package/dist/index.mjs CHANGED
@@ -674,7 +674,7 @@ var HttpClient = class {
674
674
  });
675
675
  }
676
676
  /**
677
- * Stream AI text generation with Vercel AI SDK data stream format
677
+ * Stream AI text generation - uses Vercel AI SDK's pipeUIMessageStreamToResponse (Data Stream Protocol)
678
678
  */
679
679
  async streamAiText(prompt, options = {}, onChunk) {
680
680
  const url = this.buildUrl(`/api/ai/${this.projectId}/text`);
@@ -704,7 +704,7 @@ var HttpClient = class {
704
704
  if (!response.body) {
705
705
  throw new BlinkNetworkError("No response body for streaming");
706
706
  }
707
- return this.parseDataStream(response.body, onChunk);
707
+ return this.parseDataStreamProtocol(response.body, onChunk);
708
708
  } catch (error) {
709
709
  if (error instanceof BlinkError) {
710
710
  throw error;
@@ -729,7 +729,7 @@ var HttpClient = class {
729
729
  });
730
730
  }
731
731
  /**
732
- * Stream AI object generation with Vercel AI SDK data stream format
732
+ * Stream AI object generation - uses Vercel AI SDK's pipeTextStreamToResponse
733
733
  */
734
734
  async streamAiObject(prompt, options = {}, onPartial) {
735
735
  const url = this.buildUrl(`/api/ai/${this.projectId}/object`);
@@ -759,7 +759,35 @@ var HttpClient = class {
759
759
  if (!response.body) {
760
760
  throw new BlinkNetworkError("No response body for streaming");
761
761
  }
762
- return this.parseDataStream(response.body, void 0, onPartial);
762
+ const reader = response.body.getReader();
763
+ const decoder = new TextDecoder();
764
+ let buffer = "";
765
+ let latestObject = {};
766
+ try {
767
+ while (true) {
768
+ const { done, value } = await reader.read();
769
+ if (done) break;
770
+ const chunk = decoder.decode(value, { stream: true });
771
+ buffer += chunk;
772
+ try {
773
+ const parsed = JSON.parse(buffer);
774
+ latestObject = parsed;
775
+ if (onPartial) {
776
+ onPartial(parsed);
777
+ }
778
+ } catch {
779
+ }
780
+ }
781
+ if (buffer) {
782
+ try {
783
+ latestObject = JSON.parse(buffer);
784
+ } catch {
785
+ }
786
+ }
787
+ return { object: latestObject };
788
+ } finally {
789
+ reader.releaseLock();
790
+ }
763
791
  } catch (error) {
764
792
  if (error instanceof BlinkError) {
765
793
  throw error;
@@ -922,93 +950,94 @@ var HttpClient = class {
922
950
  }
923
951
  }
924
952
  /**
925
- * Parse Vercel AI SDK data stream format
926
- * Handles text chunks (0:"text"), partial objects (2:[...]), and metadata (d:, e:)
953
+ * Parse Vercel AI SDK v5 Data Stream Protocol (Server-Sent Events)
954
+ * Supports all event types from the UI Message Stream protocol
927
955
  */
928
- async parseDataStream(body, onChunk, onPartial) {
956
+ async parseDataStreamProtocol(body, onChunk) {
929
957
  const reader = body.getReader();
930
958
  const decoder = new TextDecoder();
959
+ const finalResult = {
960
+ text: "",
961
+ toolCalls: [],
962
+ toolResults: [],
963
+ sources: [],
964
+ files: [],
965
+ reasoning: []
966
+ };
931
967
  let buffer = "";
932
- let finalResult = {};
933
968
  try {
934
969
  while (true) {
935
970
  const { done, value } = await reader.read();
936
971
  if (done) break;
937
972
  buffer += decoder.decode(value, { stream: true });
938
- const lines = buffer.split(/\r?\n/);
973
+ const lines = buffer.split("\n");
939
974
  buffer = lines.pop() || "";
940
975
  for (const line of lines) {
941
976
  if (!line.trim()) continue;
977
+ if (line === "[DONE]") {
978
+ continue;
979
+ }
980
+ if (!line.startsWith("data: ")) continue;
942
981
  try {
943
- if (line.startsWith("f:")) {
944
- const metadata = JSON.parse(line.slice(2));
945
- finalResult.messageId = metadata.messageId;
946
- } else if (line.startsWith("0:")) {
947
- const textChunk = JSON.parse(line.slice(2));
948
- if (onChunk) {
949
- onChunk(textChunk);
950
- }
951
- finalResult.text = (finalResult.text || "") + textChunk;
952
- } else if (line.startsWith("2:")) {
953
- const data = JSON.parse(line.slice(2));
954
- if (Array.isArray(data) && data.length > 0) {
955
- const item = data[0];
956
- if (typeof item === "string") {
957
- finalResult.status = item;
958
- } else if (typeof item === "object") {
959
- if (onPartial) {
960
- onPartial(item);
961
- }
962
- finalResult.object = item;
982
+ const jsonStr = line.slice(6);
983
+ const part = JSON.parse(jsonStr);
984
+ switch (part.type) {
985
+ case "text-start":
986
+ break;
987
+ case "text-delta":
988
+ if (part.delta) {
989
+ finalResult.text += part.delta;
990
+ if (onChunk) onChunk(part.delta);
963
991
  }
964
- }
965
- } else if (line.startsWith("d:")) {
966
- const metadata = JSON.parse(line.slice(2));
967
- if (metadata.usage) {
968
- finalResult.usage = metadata.usage;
969
- }
970
- if (metadata.finishReason) {
971
- finalResult.finishReason = metadata.finishReason;
972
- }
973
- } else if (line.startsWith("e:")) {
974
- const errorData = JSON.parse(line.slice(2));
975
- finalResult.error = errorData;
976
- }
977
- } catch (error) {
978
- console.warn("Failed to parse stream line:", line, error);
979
- }
980
- }
981
- }
982
- if (buffer.trim()) {
983
- try {
984
- if (buffer.startsWith("0:")) {
985
- const textChunk = JSON.parse(buffer.slice(2));
986
- if (onChunk) {
987
- onChunk(textChunk);
988
- }
989
- finalResult.text = (finalResult.text || "") + textChunk;
990
- } else if (buffer.startsWith("2:")) {
991
- const data = JSON.parse(buffer.slice(2));
992
- if (Array.isArray(data) && data.length > 0) {
993
- const item = data[0];
994
- if (typeof item === "object") {
995
- if (onPartial) {
996
- onPartial(item);
992
+ if (part.textDelta) {
993
+ finalResult.text += part.textDelta;
994
+ if (onChunk) onChunk(part.textDelta);
997
995
  }
998
- finalResult.object = item;
999
- }
1000
- }
1001
- } else if (buffer.startsWith("d:")) {
1002
- const metadata = JSON.parse(buffer.slice(2));
1003
- if (metadata.usage) {
1004
- finalResult.usage = metadata.usage;
1005
- }
1006
- if (metadata.finishReason) {
1007
- finalResult.finishReason = metadata.finishReason;
996
+ break;
997
+ case "text-end":
998
+ break;
999
+ case "tool-call":
1000
+ finalResult.toolCalls.push({
1001
+ toolCallId: part.toolCallId,
1002
+ toolName: part.toolName,
1003
+ args: part.args
1004
+ });
1005
+ break;
1006
+ case "tool-result":
1007
+ finalResult.toolResults.push({
1008
+ toolCallId: part.toolCallId,
1009
+ toolName: part.toolName,
1010
+ result: part.result
1011
+ });
1012
+ break;
1013
+ case "source-url":
1014
+ finalResult.sources.push({
1015
+ id: part.id,
1016
+ url: part.url,
1017
+ title: part.title
1018
+ });
1019
+ break;
1020
+ case "file":
1021
+ finalResult.files.push(part.file);
1022
+ break;
1023
+ case "reasoning":
1024
+ finalResult.reasoning.push(part.content);
1025
+ break;
1026
+ case "finish":
1027
+ finalResult.finishReason = part.finishReason;
1028
+ finalResult.usage = part.usage;
1029
+ if (part.response) finalResult.response = part.response;
1030
+ break;
1031
+ case "error":
1032
+ finalResult.error = part.error;
1033
+ throw new Error(part.error);
1034
+ case "data":
1035
+ if (!finalResult.customData) finalResult.customData = [];
1036
+ finalResult.customData.push(part.value);
1037
+ break;
1008
1038
  }
1039
+ } catch (e) {
1009
1040
  }
1010
- } catch (error) {
1011
- console.warn("Failed to parse final buffer:", buffer, error);
1012
1041
  }
1013
1042
  }
1014
1043
  return finalResult;
@@ -3725,13 +3754,7 @@ var BlinkAIImpl = class {
3725
3754
  options.prompt || "",
3726
3755
  requestBody
3727
3756
  );
3728
- if (response.data?.result) {
3729
- return response.data.result;
3730
- } else if (response.data?.text) {
3731
- return response.data;
3732
- } else {
3733
- throw new BlinkAIError("Invalid response format: missing text");
3734
- }
3757
+ return response.data;
3735
3758
  } catch (error) {
3736
3759
  if (error instanceof BlinkAIError) {
3737
3760
  throw error;
@@ -3800,9 +3823,14 @@ var BlinkAIImpl = class {
3800
3823
  );
3801
3824
  return {
3802
3825
  text: result.text || "",
3803
- finishReason: "stop",
3826
+ finishReason: result.finishReason || "stop",
3804
3827
  usage: result.usage,
3805
- ...result
3828
+ toolCalls: result.toolCalls,
3829
+ toolResults: result.toolResults,
3830
+ sources: result.sources,
3831
+ files: result.files,
3832
+ reasoningDetails: result.reasoning,
3833
+ response: result.response
3806
3834
  };
3807
3835
  } catch (error) {
3808
3836
  if (error instanceof BlinkAIError) {
@@ -3881,13 +3909,7 @@ var BlinkAIImpl = class {
3881
3909
  signal: options.signal
3882
3910
  }
3883
3911
  );
3884
- if (response.data?.result) {
3885
- return response.data.result;
3886
- } else if (response.data?.object) {
3887
- return response.data;
3888
- } else {
3889
- throw new BlinkAIError("Invalid response format: missing object");
3890
- }
3912
+ return response.data;
3891
3913
  } catch (error) {
3892
3914
  if (error instanceof BlinkAIError) {
3893
3915
  throw error;
@@ -3949,8 +3971,7 @@ var BlinkAIImpl = class {
3949
3971
  return {
3950
3972
  object: result.object || {},
3951
3973
  finishReason: "stop",
3952
- usage: result.usage,
3953
- ...result
3974
+ usage: result.usage
3954
3975
  };
3955
3976
  } catch (error) {
3956
3977
  if (error instanceof BlinkAIError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blinkdotnew/sdk",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps",
5
5
  "keywords": [
6
6
  "blink",