@persistmemory/sdk 0.1.2 → 0.3.0

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.js CHANGED
@@ -238,6 +238,27 @@ var HttpClient = class {
238
238
  ...options ? { options } : {}
239
239
  });
240
240
  }
241
+ /** POST with a file as the body. The type describes the bytes, not JSON. */
242
+ async postBytes(path, bytes, contentType, query, options) {
243
+ return this.#request({
244
+ method: "POST",
245
+ path,
246
+ rawBody: bytes,
247
+ contentType,
248
+ ...query ? { query } : {},
249
+ ...options ? { options } : {}
250
+ });
251
+ }
252
+ /** GET that returns bytes rather than JSON, for downloading a file. */
253
+ async getBytes(path, query, options) {
254
+ return this.#request({
255
+ method: "GET",
256
+ path,
257
+ rawResponse: true,
258
+ ...query ? { query } : {},
259
+ ...options ? { options } : {}
260
+ });
261
+ }
241
262
  async patch(path, body, options) {
242
263
  return this.#request({
243
264
  method: "PATCH",
@@ -290,11 +311,20 @@ var HttpClient = class {
290
311
  this.#fetch(url, {
291
312
  method: request.method,
292
313
  headers: this.#headers(request),
293
- ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
314
+ ...request.rawBody !== void 0 ? { body: request.rawBody } : request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
294
315
  signal: deadline.signal
295
316
  }),
296
317
  deadline.signal
297
318
  );
319
+ if (request.rawResponse && response.ok) {
320
+ const disposition = response.headers.get("content-disposition") ?? "";
321
+ const named = /filename="([^"]+)"/.exec(disposition)?.[1];
322
+ return {
323
+ bytes: new Uint8Array(await response.arrayBuffer()),
324
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
325
+ ...named ? { filename: named } : {}
326
+ };
327
+ }
298
328
  const payload = await readBody(response);
299
329
  if (!response.ok) throw errorFromResponse(response.status, payload, response.headers);
300
330
  return payload;
@@ -315,9 +345,11 @@ var HttpClient = class {
315
345
  return {
316
346
  // The only place the key is ever read.
317
347
  authorization: `Bearer ${this.#apiKey}`,
318
- accept: "application/json",
348
+ // A download route answers with the file's own type, so `*/*` rather
349
+ // than a promise to accept only JSON that the server would have to break.
350
+ accept: request.rawResponse ? "*/*" : "application/json",
319
351
  "user-agent": this.#userAgent,
320
- ...request.body !== void 0 ? { "content-type": "application/json" } : {},
352
+ ...request.rawBody !== void 0 ? { "content-type": request.contentType ?? "application/octet-stream" } : request.body !== void 0 ? { "content-type": "application/json" } : {},
321
353
  ...request.options?.idempotencyKey ? { "idempotency-key": request.options.idempotencyKey } : {}
322
354
  };
323
355
  }
@@ -629,17 +661,22 @@ var Spaces = class {
629
661
  /**
630
662
  * The memories filed in a Space.
631
663
  *
632
- * This endpoint answers `{ data, pagination: { limit } }` with no cursor: it
633
- * returns the first `limit` members and stops. Wrapped in a `Paginated`
634
- * anyway so it reads like every other list, and it simply yields one page -
635
- * a caller who needs more should filter `memories.list` by `spaceIds`, which
636
- * is the endpoint that actually pages.
664
+ * The cursor is PASSED. This fetch used to ignore the paginator's cursor
665
+ * on the stale belief that the endpoint had none the server has minted
666
+ * `pagination.nextCursor` since it started paging, and its own comment
667
+ * says "both SDKs iterate by reading pagination". Ignoring it meant every
668
+ * page request was identical: the loop guard saw a non-advancing fetch and
669
+ * stopped silently, so `all()` returned the first page twice and dropped
670
+ * everything after it — duplicated AND truncated data, with no error.
637
671
  */
638
672
  memories(id, params = {}, options) {
639
673
  return new Paginated(
640
- () => this.#http.get(
674
+ (cursor) => this.#http.get(
641
675
  `/api/v1/spaces/${encodeURIComponent(id)}/memories`,
642
- { ...params.limit !== void 0 ? { limit: params.limit } : {} },
676
+ {
677
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
678
+ ...cursor !== void 0 ? { cursor } : {}
679
+ },
643
680
  options
644
681
  )
645
682
  );
@@ -665,6 +702,94 @@ var Spaces = class {
665
702
  options
666
703
  );
667
704
  }
705
+ /* ----------------------- who else can see it ----------------------- */
706
+ /**
707
+ * Who can see this Space, including invitations nobody has accepted.
708
+ *
709
+ * A DIFFERENT EDGE from `memories()` next door, and the difference is worth
710
+ * holding on to: that one maps a MEMORY to a Space, this one maps a PERSON
711
+ * to a Space. The server keeps them in two tables with two names for exactly
712
+ * that reason.
713
+ *
714
+ * Read `acceptedAt` before you render a row. An invitation grants nothing
715
+ * until it is accepted, so a list that draws invited and accepted people the
716
+ * same way tells its user somebody is reading their memories when nobody is.
717
+ *
718
+ * Paginated like every other list here. A Space has a handful of
719
+ * collaborators rather than thousands, so this will usually be one page -
720
+ * which costs a caller nothing and means the shape does not change if a
721
+ * Space ever has an organisation on it.
722
+ */
723
+ collaborators(id, params = {}, options) {
724
+ return new Paginated(
725
+ (cursor) => this.#http.get(
726
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
727
+ {
728
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
729
+ ...cursor !== void 0 ? { cursor } : {}
730
+ },
731
+ options
732
+ )
733
+ );
734
+ }
735
+ /**
736
+ * Offers somebody sight of a Space. Answers with the invitation.
737
+ *
738
+ * AN OFFER, NOT A GRANT, and the returned `acceptedAt` will be absent to
739
+ * prove it. The recipient has to accept before they can see anything, which
740
+ * is the property that keeps "nothing enters your memory without you" true
741
+ * even when somebody else starts the sharing. Do not tell your user their
742
+ * Space "has been shared" on the strength of a 2xx here.
743
+ *
744
+ * WHAT THEY GET IS THE WHOLE SPACE: every memory already filed in it and
745
+ * every memory that lands in it afterwards. There is no narrower grant, and
746
+ * `role` does not make one - it decides what they may do BESIDES read.
747
+ *
748
+ * Worth an idempotency key when a person is behind it. A double-clicked
749
+ * "share" is two invitations to the same address, and the second one is a
750
+ * second email arriving at somebody who has already been asked.
751
+ */
752
+ async share(id, params, options) {
753
+ return this.#http.post(
754
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
755
+ params,
756
+ options
757
+ );
758
+ }
759
+ /**
760
+ * Ends somebody's access, or withdraws an invitation they never accepted.
761
+ *
762
+ * Nothing was ever copied into their account - a collaborator SEES the
763
+ * owner's memories rather than holding a duplicate - so this is one write
764
+ * and not a cascade, and there is no orphaned copy left behind.
765
+ *
766
+ * A body on a DELETE, matching `removeMemories` above. The alternative is an
767
+ * address in a path segment, where every `.`, `+` and `@` is a chance for a
768
+ * proxy or a router to normalise somebody else's email into the one that
769
+ * gets revoked.
770
+ */
771
+ async unshare(id, email, options) {
772
+ return this.#http.delete(
773
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
774
+ { email },
775
+ options
776
+ );
777
+ }
778
+ /**
779
+ * Changes what an existing collaborator may do. Never invites anybody.
780
+ *
781
+ * The quiet one. Moving somebody from `viewer` to `owner` sends no
782
+ * invitation and needs no acceptance, and afterwards they can share the
783
+ * Space onward and revoke the person who promoted them. Show your user what
784
+ * `owner` means before you send this, not after.
785
+ */
786
+ async setRole(id, params, options) {
787
+ return this.#http.patch(
788
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
789
+ params,
790
+ options
791
+ );
792
+ }
668
793
  };
669
794
 
670
795
  // src/resources/ingestion.ts
@@ -909,6 +1034,121 @@ var Conversations = class {
909
1034
  }
910
1035
  };
911
1036
 
1037
+ // src/resources/google.ts
1038
+ var Google = class {
1039
+ #http;
1040
+ constructor(http) {
1041
+ this.#http = http;
1042
+ }
1043
+ /** Files by name, newest first. Omit the query for recently changed ones. */
1044
+ async searchDrive(params = {}, options) {
1045
+ return this.#http.get(
1046
+ "/api/v1/google/drive/files",
1047
+ {
1048
+ ...params.query !== void 0 ? { query: params.query } : {},
1049
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1050
+ },
1051
+ options
1052
+ );
1053
+ }
1054
+ async getDriveFile(fileId, options) {
1055
+ return this.#http.get(
1056
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}`,
1057
+ void 0,
1058
+ options
1059
+ );
1060
+ }
1061
+ /**
1062
+ * The bytes of a Drive file.
1063
+ *
1064
+ * A Google Doc, Sheet or Slide holds no bytes of its own and is exported on
1065
+ * the way - a document as PDF, a spreadsheet as CSV - so `filename` comes
1066
+ * back describing what it BECAME. Writing it under the id instead produces a
1067
+ * file nothing will open.
1068
+ */
1069
+ async downloadDriveFile(fileId, options) {
1070
+ return this.#http.getBytes(
1071
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`,
1072
+ void 0,
1073
+ options
1074
+ );
1075
+ }
1076
+ /**
1077
+ * Writes a file into the user's Drive.
1078
+ *
1079
+ * Needs one of the Drive write permissions on their connection. A read-only
1080
+ * grant is refused by Google, and the error names the missing permission
1081
+ * rather than reporting a failed upload - one is fixed with a checkbox and
1082
+ * the other sends somebody looking for a bug.
1083
+ */
1084
+ async saveToDrive(params, options) {
1085
+ return this.#http.postBytes(
1086
+ "/api/v1/google/drive/files",
1087
+ params.bytes,
1088
+ params.contentType ?? "application/octet-stream",
1089
+ {
1090
+ name: params.name,
1091
+ ...params.folderId !== void 0 ? { folderId: params.folderId } : {}
1092
+ },
1093
+ options
1094
+ );
1095
+ }
1096
+ /**
1097
+ * Recent messages - senders, subjects and a one-line preview, never bodies.
1098
+ *
1099
+ * `query` is Gmail's own syntax passed through as written: `from:priya`,
1100
+ * `has:attachment`, `newer_than:7d`. It selects within the connected mailbox
1101
+ * and cannot reach another one.
1102
+ */
1103
+ async searchMail(params = {}, options) {
1104
+ return this.#http.get(
1105
+ "/api/v1/google/mail",
1106
+ {
1107
+ ...params.query !== void 0 ? { query: params.query } : {},
1108
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1109
+ },
1110
+ options
1111
+ );
1112
+ }
1113
+ /** One message, with its body and the names of what is attached. */
1114
+ async readMail(messageId, options) {
1115
+ return this.#http.get(
1116
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}`,
1117
+ void 0,
1118
+ options
1119
+ );
1120
+ }
1121
+ /**
1122
+ * The bytes of one attachment.
1123
+ *
1124
+ * Separate from `readMail` so listing a mailbox never drags attachments
1125
+ * across the network: a message with a 40 MB deck should not cost 40 MB to
1126
+ * summarise.
1127
+ */
1128
+ async downloadAttachment(messageId, attachmentId, options) {
1129
+ return this.#http.getBytes(
1130
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`,
1131
+ void 0,
1132
+ options
1133
+ );
1134
+ }
1135
+ /** Sends as the connected account. Needs the send permission. */
1136
+ async sendMail(params, options) {
1137
+ return this.#http.post("/api/v1/google/mail/send", params, options);
1138
+ }
1139
+ /** People in the user's contacts. Omit the query to list them. */
1140
+ async contacts(params = {}, options) {
1141
+ return this.#http.get(
1142
+ "/api/v1/google/contacts",
1143
+ {
1144
+ ...params.query !== void 0 ? { query: params.query } : {},
1145
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1146
+ },
1147
+ options
1148
+ );
1149
+ }
1150
+ };
1151
+
912
1152
  // src/resources/integrations.ts
913
1153
  var Integrations = class {
914
1154
  #http;
@@ -1046,6 +1286,8 @@ var PersistMemory = class {
1046
1286
  conflicts;
1047
1287
  conversations;
1048
1288
  integrations;
1289
+ /** Drive, mail and contacts on the user's connected Google account. */
1290
+ google;
1049
1291
  health;
1050
1292
  agent;
1051
1293
  #http;
@@ -1062,6 +1304,7 @@ var PersistMemory = class {
1062
1304
  this.conflicts = new Conflicts(this.#http);
1063
1305
  this.conversations = new Conversations(this.#http);
1064
1306
  this.integrations = new Integrations(this.#http);
1307
+ this.google = new Google(this.#http);
1065
1308
  this.health = new Health(this.#http);
1066
1309
  this.agent = new Agent(this.#http);
1067
1310
  }