@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/client.d.ts CHANGED
@@ -5,6 +5,7 @@ import { Spaces } from "./resources/spaces.js";
5
5
  import { Documents, Jobs, Sources } from "./resources/ingestion.js";
6
6
  import { Conflicts, Entities, Graph } from "./resources/knowledge.js";
7
7
  import { Conversations } from "./resources/conversations.js";
8
+ import { Google } from "./resources/google.js";
8
9
  import { Integrations } from "./resources/integrations.js";
9
10
  import { Health } from "./resources/health.js";
10
11
  import { Agent } from "./resources/agent.js";
@@ -32,6 +33,8 @@ export declare class PersistMemory {
32
33
  readonly conflicts: Conflicts;
33
34
  readonly conversations: Conversations;
34
35
  readonly integrations: Integrations;
36
+ /** Drive, mail and contacts on the user's connected Google account. */
37
+ readonly google: Google;
35
38
  readonly health: Health;
36
39
  readonly agent: Agent;
37
40
  constructor(options: ClientOptions);
package/dist/http.d.ts CHANGED
@@ -64,6 +64,18 @@ interface InternalRequest {
64
64
  readonly path: string;
65
65
  readonly query?: QueryParams;
66
66
  readonly body?: unknown;
67
+ /**
68
+ * Bytes, for the endpoints that take a file.
69
+ *
70
+ * Separate from `body` rather than sniffed out of it: `JSON.stringify` of a
71
+ * `Uint8Array` produces `{"0":137,"1":80,...}`, which is a valid request and
72
+ * a corrupt file - and the failure shows up as "this PDF is not a PDF" a
73
+ * long way from the line that caused it.
74
+ */
75
+ readonly rawBody?: Uint8Array;
76
+ readonly contentType?: string;
77
+ /** Bytes back, for a download. JSON is parsed; a file is not. */
78
+ readonly rawResponse?: boolean;
67
79
  readonly options?: RequestOptions;
68
80
  }
69
81
  export declare class HttpClient {
@@ -80,6 +92,14 @@ export declare class HttpClient {
80
92
  toJSON(): Record<string, unknown>;
81
93
  get<T>(path: string, query?: QueryParams, options?: RequestOptions): Promise<T>;
82
94
  post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
95
+ /** POST with a file as the body. The type describes the bytes, not JSON. */
96
+ postBytes<T>(path: string, bytes: Uint8Array, contentType: string, query?: QueryParams, options?: RequestOptions): Promise<T>;
97
+ /** GET that returns bytes rather than JSON, for downloading a file. */
98
+ getBytes(path: string, query?: QueryParams, options?: RequestOptions): Promise<{
99
+ bytes: Uint8Array;
100
+ contentType: string;
101
+ filename?: string;
102
+ }>;
83
103
  patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
84
104
  delete<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
85
105
  }
package/dist/index.cjs CHANGED
@@ -279,6 +279,27 @@ var HttpClient = class {
279
279
  ...options ? { options } : {}
280
280
  });
281
281
  }
282
+ /** POST with a file as the body. The type describes the bytes, not JSON. */
283
+ async postBytes(path, bytes, contentType, query, options) {
284
+ return this.#request({
285
+ method: "POST",
286
+ path,
287
+ rawBody: bytes,
288
+ contentType,
289
+ ...query ? { query } : {},
290
+ ...options ? { options } : {}
291
+ });
292
+ }
293
+ /** GET that returns bytes rather than JSON, for downloading a file. */
294
+ async getBytes(path, query, options) {
295
+ return this.#request({
296
+ method: "GET",
297
+ path,
298
+ rawResponse: true,
299
+ ...query ? { query } : {},
300
+ ...options ? { options } : {}
301
+ });
302
+ }
282
303
  async patch(path, body, options) {
283
304
  return this.#request({
284
305
  method: "PATCH",
@@ -331,11 +352,20 @@ var HttpClient = class {
331
352
  this.#fetch(url, {
332
353
  method: request.method,
333
354
  headers: this.#headers(request),
334
- ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
355
+ ...request.rawBody !== void 0 ? { body: request.rawBody } : request.body !== void 0 ? { body: JSON.stringify(request.body) } : {},
335
356
  signal: deadline.signal
336
357
  }),
337
358
  deadline.signal
338
359
  );
360
+ if (request.rawResponse && response.ok) {
361
+ const disposition = response.headers.get("content-disposition") ?? "";
362
+ const named = /filename="([^"]+)"/.exec(disposition)?.[1];
363
+ return {
364
+ bytes: new Uint8Array(await response.arrayBuffer()),
365
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
366
+ ...named ? { filename: named } : {}
367
+ };
368
+ }
339
369
  const payload = await readBody(response);
340
370
  if (!response.ok) throw errorFromResponse(response.status, payload, response.headers);
341
371
  return payload;
@@ -356,9 +386,11 @@ var HttpClient = class {
356
386
  return {
357
387
  // The only place the key is ever read.
358
388
  authorization: `Bearer ${this.#apiKey}`,
359
- accept: "application/json",
389
+ // A download route answers with the file's own type, so `*/*` rather
390
+ // than a promise to accept only JSON that the server would have to break.
391
+ accept: request.rawResponse ? "*/*" : "application/json",
360
392
  "user-agent": this.#userAgent,
361
- ...request.body !== void 0 ? { "content-type": "application/json" } : {},
393
+ ...request.rawBody !== void 0 ? { "content-type": request.contentType ?? "application/octet-stream" } : request.body !== void 0 ? { "content-type": "application/json" } : {},
362
394
  ...request.options?.idempotencyKey ? { "idempotency-key": request.options.idempotencyKey } : {}
363
395
  };
364
396
  }
@@ -670,17 +702,22 @@ var Spaces = class {
670
702
  /**
671
703
  * The memories filed in a Space.
672
704
  *
673
- * This endpoint answers `{ data, pagination: { limit } }` with no cursor: it
674
- * returns the first `limit` members and stops. Wrapped in a `Paginated`
675
- * anyway so it reads like every other list, and it simply yields one page -
676
- * a caller who needs more should filter `memories.list` by `spaceIds`, which
677
- * is the endpoint that actually pages.
705
+ * The cursor is PASSED. This fetch used to ignore the paginator's cursor
706
+ * on the stale belief that the endpoint had none the server has minted
707
+ * `pagination.nextCursor` since it started paging, and its own comment
708
+ * says "both SDKs iterate by reading pagination". Ignoring it meant every
709
+ * page request was identical: the loop guard saw a non-advancing fetch and
710
+ * stopped silently, so `all()` returned the first page twice and dropped
711
+ * everything after it — duplicated AND truncated data, with no error.
678
712
  */
679
713
  memories(id, params = {}, options) {
680
714
  return new Paginated(
681
- () => this.#http.get(
715
+ (cursor) => this.#http.get(
682
716
  `/api/v1/spaces/${encodeURIComponent(id)}/memories`,
683
- { ...params.limit !== void 0 ? { limit: params.limit } : {} },
717
+ {
718
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
719
+ ...cursor !== void 0 ? { cursor } : {}
720
+ },
684
721
  options
685
722
  )
686
723
  );
@@ -706,6 +743,94 @@ var Spaces = class {
706
743
  options
707
744
  );
708
745
  }
746
+ /* ----------------------- who else can see it ----------------------- */
747
+ /**
748
+ * Who can see this Space, including invitations nobody has accepted.
749
+ *
750
+ * A DIFFERENT EDGE from `memories()` next door, and the difference is worth
751
+ * holding on to: that one maps a MEMORY to a Space, this one maps a PERSON
752
+ * to a Space. The server keeps them in two tables with two names for exactly
753
+ * that reason.
754
+ *
755
+ * Read `acceptedAt` before you render a row. An invitation grants nothing
756
+ * until it is accepted, so a list that draws invited and accepted people the
757
+ * same way tells its user somebody is reading their memories when nobody is.
758
+ *
759
+ * Paginated like every other list here. A Space has a handful of
760
+ * collaborators rather than thousands, so this will usually be one page -
761
+ * which costs a caller nothing and means the shape does not change if a
762
+ * Space ever has an organisation on it.
763
+ */
764
+ collaborators(id, params = {}, options) {
765
+ return new Paginated(
766
+ (cursor) => this.#http.get(
767
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
768
+ {
769
+ ...params.limit !== void 0 ? { limit: params.limit } : {},
770
+ ...cursor !== void 0 ? { cursor } : {}
771
+ },
772
+ options
773
+ )
774
+ );
775
+ }
776
+ /**
777
+ * Offers somebody sight of a Space. Answers with the invitation.
778
+ *
779
+ * AN OFFER, NOT A GRANT, and the returned `acceptedAt` will be absent to
780
+ * prove it. The recipient has to accept before they can see anything, which
781
+ * is the property that keeps "nothing enters your memory without you" true
782
+ * even when somebody else starts the sharing. Do not tell your user their
783
+ * Space "has been shared" on the strength of a 2xx here.
784
+ *
785
+ * WHAT THEY GET IS THE WHOLE SPACE: every memory already filed in it and
786
+ * every memory that lands in it afterwards. There is no narrower grant, and
787
+ * `role` does not make one - it decides what they may do BESIDES read.
788
+ *
789
+ * Worth an idempotency key when a person is behind it. A double-clicked
790
+ * "share" is two invitations to the same address, and the second one is a
791
+ * second email arriving at somebody who has already been asked.
792
+ */
793
+ async share(id, params, options) {
794
+ return this.#http.post(
795
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
796
+ params,
797
+ options
798
+ );
799
+ }
800
+ /**
801
+ * Ends somebody's access, or withdraws an invitation they never accepted.
802
+ *
803
+ * Nothing was ever copied into their account - a collaborator SEES the
804
+ * owner's memories rather than holding a duplicate - so this is one write
805
+ * and not a cascade, and there is no orphaned copy left behind.
806
+ *
807
+ * A body on a DELETE, matching `removeMemories` above. The alternative is an
808
+ * address in a path segment, where every `.`, `+` and `@` is a chance for a
809
+ * proxy or a router to normalise somebody else's email into the one that
810
+ * gets revoked.
811
+ */
812
+ async unshare(id, email, options) {
813
+ return this.#http.delete(
814
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
815
+ { email },
816
+ options
817
+ );
818
+ }
819
+ /**
820
+ * Changes what an existing collaborator may do. Never invites anybody.
821
+ *
822
+ * The quiet one. Moving somebody from `viewer` to `owner` sends no
823
+ * invitation and needs no acceptance, and afterwards they can share the
824
+ * Space onward and revoke the person who promoted them. Show your user what
825
+ * `owner` means before you send this, not after.
826
+ */
827
+ async setRole(id, params, options) {
828
+ return this.#http.patch(
829
+ `/api/v1/sharing/spaces/${encodeURIComponent(id)}/collaborators`,
830
+ params,
831
+ options
832
+ );
833
+ }
709
834
  };
710
835
 
711
836
  // src/resources/ingestion.ts
@@ -950,6 +1075,121 @@ var Conversations = class {
950
1075
  }
951
1076
  };
952
1077
 
1078
+ // src/resources/google.ts
1079
+ var Google = class {
1080
+ #http;
1081
+ constructor(http) {
1082
+ this.#http = http;
1083
+ }
1084
+ /** Files by name, newest first. Omit the query for recently changed ones. */
1085
+ async searchDrive(params = {}, options) {
1086
+ return this.#http.get(
1087
+ "/api/v1/google/drive/files",
1088
+ {
1089
+ ...params.query !== void 0 ? { query: params.query } : {},
1090
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1091
+ },
1092
+ options
1093
+ );
1094
+ }
1095
+ async getDriveFile(fileId, options) {
1096
+ return this.#http.get(
1097
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}`,
1098
+ void 0,
1099
+ options
1100
+ );
1101
+ }
1102
+ /**
1103
+ * The bytes of a Drive file.
1104
+ *
1105
+ * A Google Doc, Sheet or Slide holds no bytes of its own and is exported on
1106
+ * the way - a document as PDF, a spreadsheet as CSV - so `filename` comes
1107
+ * back describing what it BECAME. Writing it under the id instead produces a
1108
+ * file nothing will open.
1109
+ */
1110
+ async downloadDriveFile(fileId, options) {
1111
+ return this.#http.getBytes(
1112
+ `/api/v1/google/drive/files/${encodeURIComponent(fileId)}/content`,
1113
+ void 0,
1114
+ options
1115
+ );
1116
+ }
1117
+ /**
1118
+ * Writes a file into the user's Drive.
1119
+ *
1120
+ * Needs one of the Drive write permissions on their connection. A read-only
1121
+ * grant is refused by Google, and the error names the missing permission
1122
+ * rather than reporting a failed upload - one is fixed with a checkbox and
1123
+ * the other sends somebody looking for a bug.
1124
+ */
1125
+ async saveToDrive(params, options) {
1126
+ return this.#http.postBytes(
1127
+ "/api/v1/google/drive/files",
1128
+ params.bytes,
1129
+ params.contentType ?? "application/octet-stream",
1130
+ {
1131
+ name: params.name,
1132
+ ...params.folderId !== void 0 ? { folderId: params.folderId } : {}
1133
+ },
1134
+ options
1135
+ );
1136
+ }
1137
+ /**
1138
+ * Recent messages - senders, subjects and a one-line preview, never bodies.
1139
+ *
1140
+ * `query` is Gmail's own syntax passed through as written: `from:priya`,
1141
+ * `has:attachment`, `newer_than:7d`. It selects within the connected mailbox
1142
+ * and cannot reach another one.
1143
+ */
1144
+ async searchMail(params = {}, options) {
1145
+ return this.#http.get(
1146
+ "/api/v1/google/mail",
1147
+ {
1148
+ ...params.query !== void 0 ? { query: params.query } : {},
1149
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1150
+ },
1151
+ options
1152
+ );
1153
+ }
1154
+ /** One message, with its body and the names of what is attached. */
1155
+ async readMail(messageId, options) {
1156
+ return this.#http.get(
1157
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}`,
1158
+ void 0,
1159
+ options
1160
+ );
1161
+ }
1162
+ /**
1163
+ * The bytes of one attachment.
1164
+ *
1165
+ * Separate from `readMail` so listing a mailbox never drags attachments
1166
+ * across the network: a message with a 40 MB deck should not cost 40 MB to
1167
+ * summarise.
1168
+ */
1169
+ async downloadAttachment(messageId, attachmentId, options) {
1170
+ return this.#http.getBytes(
1171
+ `/api/v1/google/mail/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`,
1172
+ void 0,
1173
+ options
1174
+ );
1175
+ }
1176
+ /** Sends as the connected account. Needs the send permission. */
1177
+ async sendMail(params, options) {
1178
+ return this.#http.post("/api/v1/google/mail/send", params, options);
1179
+ }
1180
+ /** People in the user's contacts. Omit the query to list them. */
1181
+ async contacts(params = {}, options) {
1182
+ return this.#http.get(
1183
+ "/api/v1/google/contacts",
1184
+ {
1185
+ ...params.query !== void 0 ? { query: params.query } : {},
1186
+ ...params.limit !== void 0 ? { limit: params.limit } : {}
1187
+ },
1188
+ options
1189
+ );
1190
+ }
1191
+ };
1192
+
953
1193
  // src/resources/integrations.ts
954
1194
  var Integrations = class {
955
1195
  #http;
@@ -1087,6 +1327,8 @@ var PersistMemory = class {
1087
1327
  conflicts;
1088
1328
  conversations;
1089
1329
  integrations;
1330
+ /** Drive, mail and contacts on the user's connected Google account. */
1331
+ google;
1090
1332
  health;
1091
1333
  agent;
1092
1334
  #http;
@@ -1103,6 +1345,7 @@ var PersistMemory = class {
1103
1345
  this.conflicts = new Conflicts(this.#http);
1104
1346
  this.conversations = new Conversations(this.#http);
1105
1347
  this.integrations = new Integrations(this.#http);
1348
+ this.google = new Google(this.#http);
1106
1349
  this.health = new Health(this.#http);
1107
1350
  this.agent = new Agent(this.#http);
1108
1351
  }