@agentshouse/kit 0.0.1 → 0.1.0-alpha.1

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1696 -2
  3. package/bin/connect-linux.sh +127 -0
  4. package/bin/kit.js +1023 -0
  5. package/package.json +33 -3
  6. package/src/acp.js +99 -0
  7. package/src/adapter-mcp.js +44 -0
  8. package/src/adapter.js +252 -0
  9. package/src/agent-logins.js +65 -0
  10. package/src/agents.js +449 -0
  11. package/src/apply.js +29 -0
  12. package/src/attach.js +52 -0
  13. package/src/binding.js +90 -0
  14. package/src/blocks.js +156 -0
  15. package/src/bridge.js +449 -0
  16. package/src/bytes.js +89 -0
  17. package/src/channel.js +37 -0
  18. package/src/command.js +109 -0
  19. package/src/configuration.js +465 -0
  20. package/src/connector-login.js +91 -0
  21. package/src/connector.js +107 -0
  22. package/src/continuation.js +61 -0
  23. package/src/contract.js +363 -0
  24. package/src/conversation-originals.js +143 -0
  25. package/src/custom/authorization.js +63 -0
  26. package/src/declaration.js +35 -0
  27. package/src/delivery.js +191 -0
  28. package/src/directory.js +102 -0
  29. package/src/draft.js +89 -0
  30. package/src/google/calendar.js +177 -0
  31. package/src/google/declared.js +146 -0
  32. package/src/google/drive.js +435 -0
  33. package/src/google/gateway.js +166 -0
  34. package/src/google/gmail.js +249 -0
  35. package/src/google/id.js +1 -0
  36. package/src/google/oauth.js +273 -0
  37. package/src/google/source.js +25 -0
  38. package/src/hook.js +376 -0
  39. package/src/host.js +121 -0
  40. package/src/house-main.js +79 -0
  41. package/src/house.js +1085 -0
  42. package/src/inbound.js +175 -0
  43. package/src/instance/google.js +1 -0
  44. package/src/launcher-main.js +19 -0
  45. package/src/launcher.js +276 -0
  46. package/src/local-files.js +113 -0
  47. package/src/login.js +274 -0
  48. package/src/mcp.js +71 -0
  49. package/src/migrator.js +248 -0
  50. package/src/mount.js +582 -0
  51. package/src/operation.js +20 -0
  52. package/src/original.js +631 -0
  53. package/src/peer.js +124 -0
  54. package/src/plan.js +57 -0
  55. package/src/question.js +316 -0
  56. package/src/refusal.js +79 -0
  57. package/src/repository.js +521 -0
  58. package/src/session-bin/house +2 -0
  59. package/src/settlement.js +57 -0
  60. package/src/setup.js +185 -0
  61. package/src/source-originals.js +116 -0
  62. package/src/source.js +286 -0
  63. package/src/starter-set/grilling/LICENSE +21 -0
  64. package/src/starter-set/grilling/SKILL.md +30 -0
  65. package/src/starter-set/grilling/agents/openai.yaml +3 -0
  66. package/src/starter-set.js +17 -0
  67. package/src/state.js +157 -0
  68. package/src/sync-library.js +293 -0
  69. package/src/sync-state.js +116 -0
  70. package/src/sync.js +477 -0
  71. package/src/telegram.js +1045 -0
  72. package/src/terminal.js +57 -0
  73. package/src/tmux.js +119 -0
  74. package/src/worker.js +756 -0
  75. package/src/workspace.js +500 -0
@@ -0,0 +1,146 @@
1
+ import { ITEM_BODY_MAX_BYTES } from "../contract.js";
2
+ import { KitRefusal } from "../refusal.js";
3
+ import { googleJson } from "./gateway.js";
4
+
5
+ function valueAt(value, path) {
6
+ let current = value;
7
+ for (const segment of path.split(".")) {
8
+ if (current === null || current === undefined || typeof current !== "object") return undefined;
9
+ current = current[segment];
10
+ }
11
+ return current;
12
+ }
13
+
14
+ function requiredText(value, declaration) {
15
+ if (typeof value !== "string" || value.length === 0) {
16
+ throw new KitRefusal("google_provider_response_invalid", declaration);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ function optionalText(value, declaration) {
22
+ if (value === undefined || value === null) return null;
23
+ return requiredText(value, declaration);
24
+ }
25
+
26
+ function cursorOf(value) {
27
+ if (value === null) return { pageToken: null, checkpoint: null };
28
+ let cursor;
29
+ try {
30
+ cursor = JSON.parse(value);
31
+ } catch {
32
+ throw new KitRefusal("google_checkpoint_invalid", "declared service");
33
+ }
34
+ if (
35
+ typeof cursor !== "object" ||
36
+ cursor === null ||
37
+ (cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
38
+ (cursor.checkpoint !== null && typeof cursor.checkpoint !== "string")
39
+ ) {
40
+ throw new KitRefusal("google_checkpoint_invalid", "declared service");
41
+ }
42
+ return cursor;
43
+ }
44
+
45
+ function composedBody(item, paths) {
46
+ const parts = paths.map((path) => valueAt(item, path));
47
+ if (parts.some((part) => !["string", "number", "boolean"].includes(typeof part))) {
48
+ throw new KitRefusal("google_provider_response_invalid", "declared body");
49
+ }
50
+ const body = parts.map((part) => `${part}`).join("\n\n");
51
+ if (Buffer.byteLength(body, "utf8") > ITEM_BODY_MAX_BYTES) {
52
+ throw new KitRefusal("google_provider_response_invalid", "declared body");
53
+ }
54
+ return body;
55
+ }
56
+
57
+ function normalizedItem(service, item) {
58
+ const fields = service.declaration.fields;
59
+ const observedAt = requiredText(valueAt(item, fields.observedAt), "declared observed time");
60
+ if (Number.isNaN(Date.parse(observedAt))) {
61
+ throw new KitRefusal("google_provider_response_invalid", "declared observed time");
62
+ }
63
+ const container = requiredText(valueAt(item, fields.container), "declared container");
64
+ const nativeVersion =
65
+ fields.nativeVersion === null
66
+ ? null
67
+ : optionalText(valueAt(item, fields.nativeVersion), "declared native version");
68
+ const providerModifiedAt =
69
+ fields.providerModifiedAt === null
70
+ ? null
71
+ : optionalText(
72
+ valueAt(item, fields.providerModifiedAt),
73
+ "declared provider modification time",
74
+ );
75
+ if (providerModifiedAt !== null && Number.isNaN(Date.parse(providerModifiedAt))) {
76
+ throw new KitRefusal("google_provider_response_invalid", "declared provider modification time");
77
+ }
78
+ return {
79
+ namespace: `google/${service.service}`,
80
+ nativeObjectId: requiredText(valueAt(item, fields.nativeId), "declared native id"),
81
+ nativeVersion,
82
+ providerModifiedAt:
83
+ providerModifiedAt === null ? null : new Date(providerModifiedAt).toISOString(),
84
+ observedAt: new Date(observedAt).toISOString(),
85
+ body: composedBody(item, fields.body),
86
+ author:
87
+ fields.author === null
88
+ ? null
89
+ : optionalText(valueAt(item, fields.author), "declared author"),
90
+ url:
91
+ fields.url === null ? null : optionalText(valueAt(item, fields.url), "declared url"),
92
+ attachments: [],
93
+ container: `google/${service.service}/${container}`,
94
+ };
95
+ }
96
+
97
+ export async function declaredPage(options, dependencies = {}) {
98
+ const declared = options.service.declaration;
99
+ const cursor = cursorOf(options.cursor);
100
+ const url = new URL(declared.path, `https://${declared.host}`);
101
+ for (const [name, value] of Object.entries(declared.query)) url.searchParams.set(name, value);
102
+ url.searchParams.set(declared.pageSize, `${options.limit}`);
103
+ if (cursor.pageToken !== null) {
104
+ url.searchParams.set(declared.pageToken.request, cursor.pageToken);
105
+ }
106
+ if (cursor.checkpoint !== null) {
107
+ url.searchParams.set(declared.pageToken.checkpointRequest, cursor.checkpoint);
108
+ }
109
+ const answer = await googleJson(
110
+ {
111
+ directory: options.directory,
112
+ instance: options.instance,
113
+ client: options.client,
114
+ requiredScopes: declared.scopes,
115
+ url: url.toString(),
116
+ signal: options.signal,
117
+ },
118
+ dependencies,
119
+ );
120
+ const entries = valueAt(answer, declared.items);
121
+ if (!Array.isArray(entries) || entries.length > options.limit) {
122
+ throw new KitRefusal("google_provider_response_invalid", "declared items");
123
+ }
124
+ const pageToken = valueAt(answer, declared.pageToken.response);
125
+ let next;
126
+ if (pageToken !== undefined && pageToken !== null) {
127
+ next = {
128
+ pageToken: requiredText(pageToken, "declared page token"),
129
+ checkpoint: cursor.checkpoint,
130
+ };
131
+ } else {
132
+ next = {
133
+ pageToken: null,
134
+ checkpoint: requiredText(
135
+ valueAt(answer, declared.pageToken.checkpointResponse),
136
+ "declared checkpoint",
137
+ ),
138
+ };
139
+ }
140
+ return {
141
+ items: entries.map((entry) => normalizedItem(options.service, entry)),
142
+ dropped: [],
143
+ cursor: JSON.stringify(next),
144
+ more: pageToken !== undefined && pageToken !== null,
145
+ };
146
+ }
@@ -0,0 +1,435 @@
1
+ import { ITEM_BODY_MAX_BYTES } from "../contract.js";
2
+ import { KitRefusal } from "../refusal.js";
3
+ import { googleBoundedText, googleJson } from "./gateway.js";
4
+ import { GOOGLE_DRIVE_ID } from "./id.js";
5
+ import { GOOGLE_SCOPES } from "./oauth.js";
6
+
7
+ const DRIVE_ORIGIN = "https://www.googleapis.com";
8
+ const FOLDER_MIME = "application/vnd.google-apps.folder";
9
+ const GOOGLE_MIME_PREFIX = "application/vnd.google-apps.";
10
+ const GOOGLE_EXPORT_MIMES = new Map([
11
+ [`${GOOGLE_MIME_PREFIX}document`, "text/plain"],
12
+ [`${GOOGLE_MIME_PREFIX}spreadsheet`, "text/csv"],
13
+ [`${GOOGLE_MIME_PREFIX}presentation`, "text/plain"],
14
+ [`${GOOGLE_MIME_PREFIX}drawing`, "image/svg+xml"],
15
+ [`${GOOGLE_MIME_PREFIX}script`, `${GOOGLE_MIME_PREFIX}script+json`],
16
+ ]);
17
+ const TEXT_MIMES = new Set([
18
+ "application/json",
19
+ "application/xml",
20
+ "application/xhtml+xml",
21
+ "image/svg+xml",
22
+ ]);
23
+ const FILE_FIELDS = "id,name,mimeType,modifiedTime,version,parents,webViewLink,owners,trashed";
24
+ const FOLDER_FRONTIER_MAX = 4096;
25
+
26
+ function selectionsOf(selection) {
27
+ return [
28
+ ...selection.files.map((id) => ({ kind: "file", id })),
29
+ ...selection.folders.map((id) => ({ kind: "folder", id })),
30
+ ];
31
+ }
32
+
33
+ function cursorOf(value, selections) {
34
+ if (value === null) return null;
35
+ let cursor;
36
+ try {
37
+ cursor = JSON.parse(value);
38
+ } catch {
39
+ throw new KitRefusal("google_checkpoint_invalid", "drive");
40
+ }
41
+ const validSnapshot =
42
+ cursor?.phase === "snapshot" &&
43
+ Number.isInteger(cursor.selection) &&
44
+ cursor.selection >= 0 &&
45
+ cursor.selection < selections.length &&
46
+ (cursor.pageToken === null || typeof cursor.pageToken === "string") &&
47
+ typeof cursor.changeToken === "string" &&
48
+ Array.isArray(cursor.folders) &&
49
+ cursor.folders.every(
50
+ (folder) => typeof folder === "string" && GOOGLE_DRIVE_ID.test(folder),
51
+ ) &&
52
+ Array.isArray(cursor.visited) &&
53
+ cursor.visited.every(
54
+ (folder) => typeof folder === "string" && GOOGLE_DRIVE_ID.test(folder),
55
+ ) &&
56
+ new Set([...cursor.folders, ...cursor.visited]).size ===
57
+ cursor.folders.length + cursor.visited.length &&
58
+ cursor.folders.length + cursor.visited.length <= FOLDER_FRONTIER_MAX &&
59
+ (selections[cursor.selection].kind === "folder"
60
+ ? cursor.folders.length > 0
61
+ : cursor.folders.length === 0 && cursor.visited.length === 0);
62
+ const validChanges =
63
+ cursor?.phase === "changes" &&
64
+ cursor.selection === 0 &&
65
+ typeof cursor.pageToken === "string";
66
+ if (!validSnapshot && !validChanges) throw new KitRefusal("google_checkpoint_invalid", "drive");
67
+ return cursor;
68
+ }
69
+
70
+ function common(options) {
71
+ return {
72
+ directory: options.directory,
73
+ instance: options.instance,
74
+ client: options.client,
75
+ requiredScopes: [GOOGLE_SCOPES.drive],
76
+ signal: options.signal,
77
+ };
78
+ }
79
+
80
+ function fileUrl(id, query = {}) {
81
+ const url = new URL(`/drive/v3/files/${encodeURIComponent(id)}`, DRIVE_ORIGIN);
82
+ for (const [name, value] of Object.entries(query)) url.searchParams.set(name, value);
83
+ return url.toString();
84
+ }
85
+
86
+ function fileMetadata(value) {
87
+ if (
88
+ typeof value?.id !== "string" ||
89
+ !GOOGLE_DRIVE_ID.test(value.id) ||
90
+ typeof value.name !== "string" ||
91
+ typeof value.mimeType !== "string" ||
92
+ (value.parents !== undefined &&
93
+ (!Array.isArray(value.parents) ||
94
+ value.parents.some(
95
+ (parent) => typeof parent !== "string" || !GOOGLE_DRIVE_ID.test(parent),
96
+ )))
97
+ ) {
98
+ throw new KitRefusal("google_provider_response_invalid", "drive file");
99
+ }
100
+ return value;
101
+ }
102
+
103
+ async function parentPath(options, file, dependencies, cache) {
104
+ const names = [];
105
+ const identifiers = [];
106
+ const seen = new Set();
107
+ let parent = file.parents?.[0] ?? null;
108
+ while (parent !== null && parent !== "root") {
109
+ if (seen.has(parent)) throw new KitRefusal("google_provider_response_invalid", "drive parents");
110
+ seen.add(parent);
111
+ let metadata = cache.get(parent);
112
+ if (metadata === undefined) {
113
+ metadata = fileMetadata(
114
+ await googleJson(
115
+ {
116
+ ...common(options),
117
+ url: fileUrl(parent, { fields: FILE_FIELDS }),
118
+ notFoundCode: "google_drive_item_missing",
119
+ },
120
+ dependencies,
121
+ ),
122
+ );
123
+ cache.set(parent, metadata);
124
+ }
125
+ if (metadata.mimeType !== FOLDER_MIME) {
126
+ throw new KitRefusal("google_provider_response_invalid", "drive parent");
127
+ }
128
+ names.unshift(metadata.name);
129
+ identifiers.unshift(metadata.id);
130
+ parent = metadata.parents?.[0] ?? null;
131
+ }
132
+ return { names, identifiers };
133
+ }
134
+
135
+ async function contentOf(options, file, dependencies) {
136
+ const exported = GOOGLE_EXPORT_MIMES.get(file.mimeType);
137
+ if (
138
+ exported === undefined &&
139
+ !file.mimeType.startsWith("text/") &&
140
+ !TEXT_MIMES.has(file.mimeType)
141
+ ) {
142
+ throw new KitRefusal("google_drive_content_unsupported");
143
+ }
144
+ const url =
145
+ exported === undefined
146
+ ? fileUrl(file.id, { alt: "media" })
147
+ : `${DRIVE_ORIGIN}/drive/v3/files/${encodeURIComponent(file.id)}/export?mimeType=${encodeURIComponent(exported)}`;
148
+ try {
149
+ return await googleBoundedText(
150
+ { ...common(options), url, notFoundCode: "google_drive_item_missing" },
151
+ ITEM_BODY_MAX_BYTES,
152
+ dependencies,
153
+ );
154
+ } catch (error) {
155
+ if (error instanceof KitRefusal && error.code === "google_content_exceeded") {
156
+ throw new KitRefusal("google_drive_content_exceeded");
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ function authorOf(file) {
163
+ const owner = Array.isArray(file.owners) ? file.owners[0] : null;
164
+ if (typeof owner?.emailAddress !== "string") return null;
165
+ return typeof owner.displayName === "string"
166
+ ? `${owner.displayName} <${owner.emailAddress}>`
167
+ : owner.emailAddress;
168
+ }
169
+
170
+ async function normalizedFile(options, file, dependencies, cache) {
171
+ const metadata = fileMetadata(file);
172
+ if (
173
+ metadata.mimeType === FOLDER_MIME ||
174
+ typeof metadata.modifiedTime !== "string" ||
175
+ Number.isNaN(Date.parse(metadata.modifiedTime)) ||
176
+ (metadata.version !== undefined && typeof metadata.version !== "string") ||
177
+ (metadata.webViewLink !== undefined && typeof metadata.webViewLink !== "string")
178
+ ) {
179
+ throw new KitRefusal("google_provider_response_invalid", "drive content");
180
+ }
181
+ const body = await contentOf(options, metadata, dependencies);
182
+ const folders = await parentPath(options, metadata, dependencies, cache);
183
+ return {
184
+ namespace: "google/drive",
185
+ nativeObjectId: metadata.id,
186
+ nativeVersion: metadata.version ?? metadata.modifiedTime,
187
+ providerModifiedAt: new Date(metadata.modifiedTime).toISOString(),
188
+ observedAt: new Date(metadata.modifiedTime).toISOString(),
189
+ body,
190
+ author: authorOf(metadata),
191
+ url: metadata.webViewLink ?? null,
192
+ attachments: [],
193
+ container: `google/drive${folders.names.length === 0 ? "" : `/${folders.names.join("/")}`}`,
194
+ };
195
+ }
196
+
197
+ function droppedFile(file, code) {
198
+ return {
199
+ namespace: "google/drive",
200
+ nativeObjectId: file.id,
201
+ nativeVersion: file.version ?? file.modifiedTime ?? null,
202
+ code,
203
+ };
204
+ }
205
+
206
+ async function normalizeFiles(options, files, dependencies, cache) {
207
+ const items = [];
208
+ const dropped = [];
209
+ for (const file of files) {
210
+ if (file.mimeType === FOLDER_MIME) continue;
211
+ try {
212
+ items.push(await normalizedFile(options, file, dependencies, cache));
213
+ } catch (error) {
214
+ if (
215
+ error instanceof KitRefusal &&
216
+ [
217
+ "google_drive_content_unsupported",
218
+ "google_drive_content_exceeded",
219
+ "google_drive_item_missing",
220
+ ].includes(error.code)
221
+ ) {
222
+ dropped.push(droppedFile(file, error.code));
223
+ } else {
224
+ throw error;
225
+ }
226
+ }
227
+ }
228
+ return { items, dropped };
229
+ }
230
+
231
+ async function startPageToken(options, dependencies) {
232
+ const answer = await googleJson(
233
+ { ...common(options), url: `${DRIVE_ORIGIN}/drive/v3/changes/startPageToken` },
234
+ dependencies,
235
+ );
236
+ if (typeof answer.startPageToken !== "string") {
237
+ throw new KitRefusal("google_provider_response_invalid", "drive start page token");
238
+ }
239
+ return answer.startPageToken;
240
+ }
241
+
242
+ async function snapshotPage(options, cursor, selections, dependencies) {
243
+ const selected = selections[cursor.selection];
244
+ let files;
245
+ const dropped = [];
246
+ let nextPageToken = null;
247
+ if (selected.kind === "file") {
248
+ try {
249
+ files = [
250
+ fileMetadata(
251
+ await googleJson(
252
+ {
253
+ ...common(options),
254
+ url: fileUrl(selected.id, { fields: FILE_FIELDS }),
255
+ notFoundCode: "google_drive_item_missing",
256
+ },
257
+ dependencies,
258
+ ),
259
+ ),
260
+ ];
261
+ } catch (error) {
262
+ if (!(error instanceof KitRefusal) || error.code !== "google_drive_item_missing") throw error;
263
+ files = [];
264
+ dropped.push({
265
+ namespace: "google/drive",
266
+ nativeObjectId: selected.id,
267
+ nativeVersion: null,
268
+ code: error.code,
269
+ });
270
+ }
271
+ } else {
272
+ const folder = cursor.folders[0];
273
+ const url = new URL("/drive/v3/files", DRIVE_ORIGIN);
274
+ url.searchParams.set("q", `'${folder}' in parents and trashed = false`);
275
+ url.searchParams.set("pageSize", `${options.limit}`);
276
+ url.searchParams.set("fields", `nextPageToken,files(${FILE_FIELDS})`);
277
+ if (cursor.pageToken !== null) url.searchParams.set("pageToken", cursor.pageToken);
278
+ const answer = await googleJson({ ...common(options), url: url.toString() }, dependencies);
279
+ if (
280
+ !Array.isArray(answer.files) ||
281
+ (answer.nextPageToken !== undefined && typeof answer.nextPageToken !== "string")
282
+ ) {
283
+ throw new KitRefusal("google_provider_response_invalid", "drive files");
284
+ }
285
+ files = answer.files.map(fileMetadata);
286
+ nextPageToken = answer.nextPageToken ?? null;
287
+ }
288
+ const cache = new Map();
289
+ const normalized = await normalizeFiles(options, files, dependencies, cache);
290
+ const knownFolders = new Set([...cursor.folders, ...cursor.visited]);
291
+ const discoveredFolders = [
292
+ ...new Set(
293
+ files
294
+ .filter((file) => file.mimeType === FOLDER_MIME && !knownFolders.has(file.id))
295
+ .map((file) => file.id),
296
+ ),
297
+ ];
298
+ if (knownFolders.size + discoveredFolders.length > FOLDER_FRONTIER_MAX) {
299
+ throw new KitRefusal("google_drive_selection_exceeded");
300
+ }
301
+ let next;
302
+ if (nextPageToken !== null) {
303
+ next = {
304
+ phase: "snapshot",
305
+ selection: cursor.selection,
306
+ pageToken: nextPageToken,
307
+ changeToken: cursor.changeToken,
308
+ folders: [...cursor.folders, ...discoveredFolders],
309
+ visited: cursor.visited,
310
+ };
311
+ } else if (
312
+ selected.kind === "folder" &&
313
+ cursor.folders.length - 1 + discoveredFolders.length > 0
314
+ ) {
315
+ next = {
316
+ phase: "snapshot",
317
+ selection: cursor.selection,
318
+ pageToken: null,
319
+ changeToken: cursor.changeToken,
320
+ folders: [...cursor.folders.slice(1), ...discoveredFolders],
321
+ visited: [...cursor.visited, cursor.folders[0]],
322
+ };
323
+ } else if (cursor.selection + 1 < selections.length) {
324
+ const following = selections[cursor.selection + 1];
325
+ next = {
326
+ phase: "snapshot",
327
+ selection: cursor.selection + 1,
328
+ pageToken: null,
329
+ changeToken: cursor.changeToken,
330
+ folders: following.kind === "folder" ? [following.id] : [],
331
+ visited: [],
332
+ };
333
+ } else {
334
+ next = { phase: "changes", selection: 0, pageToken: cursor.changeToken };
335
+ }
336
+ return {
337
+ items: normalized.items,
338
+ dropped: [...dropped, ...normalized.dropped],
339
+ cursor: JSON.stringify(next),
340
+ more: true,
341
+ };
342
+ }
343
+
344
+ async function selectedChange(options, file, dependencies, cache) {
345
+ if (options.selection.files.includes(file.id)) return true;
346
+ const folders = await parentPath(options, file, dependencies, cache);
347
+ return folders.identifiers.some((folder) => options.selection.folders.includes(folder));
348
+ }
349
+
350
+ async function changesPage(options, cursor, dependencies) {
351
+ const url = new URL("/drive/v3/changes", DRIVE_ORIGIN);
352
+ url.searchParams.set("pageToken", cursor.pageToken);
353
+ url.searchParams.set("pageSize", `${options.limit}`);
354
+ url.searchParams.set("includeRemoved", "false");
355
+ url.searchParams.set("fields", `nextPageToken,newStartPageToken,changes(fileId,removed,file(${FILE_FIELDS}))`);
356
+ const answer = await googleJson(
357
+ {
358
+ ...common(options),
359
+ url: url.toString(),
360
+ goneCode: "google_drive_cursor_expired",
361
+ },
362
+ dependencies,
363
+ );
364
+ if (
365
+ !Array.isArray(answer.changes) ||
366
+ (answer.nextPageToken !== undefined && typeof answer.nextPageToken !== "string") ||
367
+ (answer.newStartPageToken !== undefined && typeof answer.newStartPageToken !== "string")
368
+ ) {
369
+ throw new KitRefusal("google_provider_response_invalid", "drive changes");
370
+ }
371
+ const files = answer.changes
372
+ .filter((change) => change?.removed !== true && change?.file !== undefined)
373
+ .map((change) => fileMetadata(change.file));
374
+ const cache = new Map();
375
+ const selected = [];
376
+ const dropped = [];
377
+ for (const file of files) {
378
+ if (file.mimeType === FOLDER_MIME) continue;
379
+ try {
380
+ if (await selectedChange(options, file, dependencies, cache)) selected.push(file);
381
+ } catch (error) {
382
+ if (!(error instanceof KitRefusal) || error.code !== "google_drive_item_missing") throw error;
383
+ dropped.push(droppedFile(file, error.code));
384
+ }
385
+ }
386
+ const normalized = await normalizeFiles(options, selected, dependencies, cache);
387
+ const pageToken = answer.nextPageToken ?? answer.newStartPageToken;
388
+ if (typeof pageToken !== "string") {
389
+ throw new KitRefusal("google_provider_response_invalid", "drive change token");
390
+ }
391
+ return {
392
+ items: normalized.items,
393
+ dropped: [...dropped, ...normalized.dropped],
394
+ cursor: JSON.stringify({ phase: "changes", selection: 0, pageToken }),
395
+ more: answer.nextPageToken !== undefined,
396
+ };
397
+ }
398
+
399
+ export async function drivePage(options, dependencies = {}) {
400
+ const selections = selectionsOf(options.selection);
401
+ const stored = cursorOf(options.cursor, selections);
402
+ const cursor =
403
+ stored ?? {
404
+ phase: "snapshot",
405
+ selection: 0,
406
+ pageToken: null,
407
+ changeToken: await startPageToken(options, dependencies),
408
+ folders: selections[0].kind === "folder" ? [selections[0].id] : [],
409
+ visited: [],
410
+ };
411
+ if (cursor.phase === "snapshot") {
412
+ return snapshotPage(options, cursor, selections, dependencies);
413
+ }
414
+ try {
415
+ return await changesPage(options, cursor, dependencies);
416
+ } catch (error) {
417
+ if (!(error instanceof KitRefusal) || error.code !== "google_drive_cursor_expired") {
418
+ throw error;
419
+ }
420
+ const first = selections[0];
421
+ return snapshotPage(
422
+ options,
423
+ {
424
+ phase: "snapshot",
425
+ selection: 0,
426
+ pageToken: null,
427
+ changeToken: await startPageToken(options, dependencies),
428
+ folders: first.kind === "folder" ? [first.id] : [],
429
+ visited: [],
430
+ },
431
+ selections,
432
+ dependencies,
433
+ );
434
+ }
435
+ }