@agentshouse/kit 0.1.0-alpha.25 → 0.1.0-alpha.27

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 (117) hide show
  1. package/README.md +172 -158
  2. package/bin/connect-linux.sh +15 -4
  3. package/bin/kit.js +48 -46
  4. package/bootstrap.json +3 -3
  5. package/package.json +1 -1
  6. package/src/adapter-mcp.js +23 -1
  7. package/src/bridge.js +4 -0
  8. package/src/configuration.js +13 -209
  9. package/src/connector-login.js +3 -84
  10. package/src/contract.js +4 -0
  11. package/src/delivery.js +4 -7
  12. package/src/directory.js +6 -26
  13. package/src/google/calendar.js +39 -45
  14. package/src/google/drive.js +61 -186
  15. package/src/google/entries.js +346 -0
  16. package/src/google/gateway.js +3 -6
  17. package/src/google/gmail.js +33 -23
  18. package/src/google/oauth.js +106 -66
  19. package/src/google/source.js +21 -23
  20. package/src/house.js +43 -5
  21. package/src/how-we-work.md +25 -0
  22. package/src/kit-work-stream.js +1 -1
  23. package/src/launcher.js +11 -4
  24. package/src/mcp.js +5 -0
  25. package/src/setup.js +8 -14
  26. package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
  27. package/src/skill-set/ask-matt/SKILL.md +91 -0
  28. package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
  29. package/src/skill-set/code-review/LICENSE +21 -0
  30. package/src/skill-set/code-review/SKILL.md +89 -0
  31. package/src/skill-set/code-review/agents/openai.yaml +3 -0
  32. package/src/skill-set/codebase-design/DEEPENING.md +37 -0
  33. package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
  34. package/src/skill-set/codebase-design/LICENSE +21 -0
  35. package/src/skill-set/codebase-design/SKILL.md +116 -0
  36. package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
  37. package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
  38. package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
  39. package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
  40. package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
  41. package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
  42. package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
  43. package/src/skill-set/domain-modeling/LICENSE +21 -0
  44. package/src/skill-set/domain-modeling/SKILL.md +76 -0
  45. package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
  46. package/src/skill-set/grill-me/LICENSE +21 -0
  47. package/src/skill-set/grill-me/SKILL.md +8 -0
  48. package/src/skill-set/grill-me/agents/openai.yaml +4 -0
  49. package/src/skill-set/grill-with-docs/LICENSE +21 -0
  50. package/src/skill-set/grill-with-docs/SKILL.md +8 -0
  51. package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
  52. package/src/skill-set/grilling/LICENSE +21 -0
  53. package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
  54. package/src/skill-set/handoff/LICENSE +21 -0
  55. package/src/skill-set/handoff/SKILL.md +17 -0
  56. package/src/skill-set/handoff/agents/openai.yaml +4 -0
  57. package/src/skill-set/implement/LICENSE +21 -0
  58. package/src/skill-set/implement/SKILL.md +16 -0
  59. package/src/skill-set/implement/agents/openai.yaml +4 -0
  60. package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
  61. package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
  62. package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
  63. package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
  64. package/src/skill-set/prototype/LICENSE +21 -0
  65. package/src/skill-set/prototype/LOGIC.md +67 -0
  66. package/src/skill-set/prototype/SKILL.md +28 -0
  67. package/src/skill-set/prototype/UI.md +112 -0
  68. package/src/skill-set/prototype/agents/openai.yaml +3 -0
  69. package/src/skill-set/research/LICENSE +21 -0
  70. package/src/skill-set/research/SKILL.md +14 -0
  71. package/src/skill-set/research/agents/openai.yaml +3 -0
  72. package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
  73. package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
  74. package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
  75. package/src/skill-set/tdd/LICENSE +21 -0
  76. package/src/skill-set/tdd/SKILL.md +40 -0
  77. package/src/skill-set/tdd/agents/openai.yaml +3 -0
  78. package/src/skill-set/tdd/mocking.md +59 -0
  79. package/src/skill-set/tdd/tests.md +77 -0
  80. package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
  81. package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
  82. package/src/skill-set/teach/LICENSE +21 -0
  83. package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
  84. package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
  85. package/src/skill-set/teach/SKILL.md +141 -0
  86. package/src/skill-set/teach/agents/openai.yaml +4 -0
  87. package/src/skill-set/to-questionnaire/LICENSE +21 -0
  88. package/src/skill-set/to-questionnaire/SKILL.md +55 -0
  89. package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
  90. package/src/skill-set/to-spec/LICENSE +21 -0
  91. package/src/skill-set/to-spec/SKILL.md +76 -0
  92. package/src/skill-set/to-spec/agents/openai.yaml +4 -0
  93. package/src/skill-set/to-tickets/LICENSE +21 -0
  94. package/src/skill-set/to-tickets/SKILL.md +106 -0
  95. package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
  96. package/src/skill-set/wait-what/LICENSE +21 -0
  97. package/src/skill-set/wait-what/SKILL.md +8 -0
  98. package/src/skill-set/wait-what/agents/openai.yaml +4 -0
  99. package/src/skill-set/wayfinder/LICENSE +21 -0
  100. package/src/skill-set/wayfinder/SKILL.md +129 -0
  101. package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
  102. package/src/skill-set/writing-for-agents/LICENSE +21 -0
  103. package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
  104. package/src/skill-set/writing-for-agents/SKILL.md +83 -0
  105. package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
  106. package/src/skill-set.js +85 -0
  107. package/src/source.js +51 -26
  108. package/src/state.js +19 -1
  109. package/src/tasks.js +72 -0
  110. package/src/worker.js +46 -68
  111. package/src/working-copy.js +1 -15
  112. package/src/google/declared.js +0 -146
  113. package/src/google/id.js +0 -1
  114. package/src/instance/google.js +0 -1
  115. package/src/starter-set.js +0 -17
  116. /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
  117. /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/directory.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { constants } from "node:fs";
2
- import { access, mkdir, realpath, rename, rm, stat } from "node:fs/promises";
3
- import { randomUUID } from "node:crypto";
4
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { access, mkdir, realpath, stat } from "node:fs/promises";
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
4
  import { LAUNCH_DIRECTORY_BASE } from "./contract.js";
6
5
  import { KitRefusal } from "./refusal.js";
7
- import { seedStarterSet } from "./starter-set.js";
8
6
 
9
7
  export function launchBase(dependencies = {}) {
10
8
  return dependencies.launchBase ?? LAUNCH_DIRECTORY_BASE;
@@ -39,25 +37,9 @@ function unavailable(path, cause) {
39
37
  return new KitRefusal("kit_launch_directory_unavailable", `${path} ${cause}`);
40
38
  }
41
39
 
42
- async function isDirectory(path) {
43
- try {
44
- return (await stat(path)).isDirectory();
45
- } catch {
46
- return false;
47
- }
48
- }
49
-
50
- async function seedDefaultDirectory(selected, base, path) {
51
- const staging = join(base, `.${randomUUID()}.staging`);
52
- try {
53
- await seedStarterSet(staging);
54
- await rename(staging, path);
55
- } catch (error) {
56
- await rm(staging, { recursive: true, force: true }).catch(() => undefined);
57
- if (!(await isDirectory(path))) {
58
- throw unavailable(selected, `could not be prepared (${error.code ?? "unknown"})`);
59
- }
60
- }
40
+ async function createDefaultDirectory(selected, path) {
41
+ const refused = await made(path);
42
+ if (refused !== null) throw unavailable(selected, `could not be created (${refused})`);
61
43
  }
62
44
 
63
45
  function within(base, selected) {
@@ -73,9 +55,7 @@ export async function readyLaunchDirectory(selected, base, agentId = null, optio
73
55
  if (options.container && !within(resolve(base), path)) {
74
56
  throw unavailable(selected, `resolves outside ${base}`);
75
57
  }
76
- if (path === defaultDirectory(base, agentId) && !(await isDirectory(path))) {
77
- await seedDefaultDirectory(selected, base, path);
78
- }
58
+ if (path === defaultDirectory(base, agentId)) await createDefaultDirectory(selected, path);
79
59
  let described;
80
60
  try {
81
61
  described = await stat(path);
@@ -4,9 +4,10 @@ import { googleJson } from "./gateway.js";
4
4
  import { GOOGLE_SCOPES } from "./oauth.js";
5
5
 
6
6
  const CALENDAR_ORIGIN = "https://www.googleapis.com";
7
+ const CALENDAR = "primary";
7
8
 
8
- function cursorOf(value, calendars) {
9
- if (value === null) return { calendar: 0, pageToken: null, syncTokens: {} };
9
+ function cursorOf(value) {
10
+ if (value === null) return { pageToken: null, syncToken: null };
10
11
  let cursor;
11
12
  try {
12
13
  cursor = JSON.parse(value);
@@ -16,16 +17,8 @@ function cursorOf(value, calendars) {
16
17
  if (
17
18
  typeof cursor !== "object" ||
18
19
  cursor === null ||
19
- !Number.isInteger(cursor.calendar) ||
20
- cursor.calendar < 0 ||
21
- cursor.calendar >= calendars.length ||
22
20
  (cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
23
- typeof cursor.syncTokens !== "object" ||
24
- cursor.syncTokens === null ||
25
- Array.isArray(cursor.syncTokens) ||
26
- Object.entries(cursor.syncTokens).some(
27
- ([calendar, token]) => !calendars.includes(calendar) || typeof token !== "string",
28
- )
21
+ (cursor.syncToken !== null && typeof cursor.syncToken !== "string")
29
22
  ) {
30
23
  throw new KitRefusal("google_checkpoint_invalid", "calendar");
31
24
  }
@@ -45,7 +38,7 @@ function authorOf(event) {
45
38
  : event.organizer.email;
46
39
  }
47
40
 
48
- function normalizedEvent(event, calendar) {
41
+ function normalizedEvent(event) {
49
42
  if (
50
43
  typeof event?.id !== "string" ||
51
44
  typeof event.etag !== "string" ||
@@ -71,45 +64,53 @@ function normalizedEvent(event, calendar) {
71
64
  author: authorOf(event),
72
65
  url: event.htmlLink ?? null,
73
66
  attachments: [],
74
- container: `google/calendar/${calendar}`,
67
+ container: `google/calendar/${CALENDAR}`,
75
68
  };
76
69
  }
77
70
 
78
- function pageUrl(calendar, cursor, limit, backfillFloor) {
79
- const url = new URL(
80
- `/calendar/v3/calendars/${encodeURIComponent(calendar)}/events`,
81
- CALENDAR_ORIGIN,
82
- );
71
+ function common(options) {
72
+ return {
73
+ directory: options.directory,
74
+ account: options.account,
75
+ requiredScopes: [GOOGLE_SCOPES.calendar],
76
+ signal: options.signal,
77
+ };
78
+ }
79
+
80
+ function pageUrl(cursor, limit, backfillFloor) {
81
+ const url = new URL(`/calendar/v3/calendars/${CALENDAR}/events`, CALENDAR_ORIGIN);
83
82
  url.searchParams.set("maxResults", `${limit}`);
84
83
  url.searchParams.set("singleEvents", "true");
85
84
  url.searchParams.set("showDeleted", "true");
86
85
  if (cursor.pageToken !== null) url.searchParams.set("pageToken", cursor.pageToken);
87
- const syncToken = cursor.syncTokens[calendar];
88
- if (syncToken !== undefined) url.searchParams.set("syncToken", syncToken);
86
+ if (cursor.syncToken !== null) url.searchParams.set("syncToken", cursor.syncToken);
89
87
  else if (backfillFloor !== null && backfillFloor !== undefined) {
90
88
  url.searchParams.set("timeMin", backfillFloor);
91
89
  }
92
90
  return url.toString();
93
91
  }
94
92
 
93
+ export async function calendarEmail(options, dependencies = {}) {
94
+ const calendar = await googleJson(
95
+ { ...common(options), url: `${CALENDAR_ORIGIN}/calendar/v3/calendars/${CALENDAR}` },
96
+ dependencies,
97
+ );
98
+ if (typeof calendar.id !== "string") {
99
+ throw new KitRefusal("google_provider_response_invalid", "calendar");
100
+ }
101
+ return calendar.id;
102
+ }
103
+
95
104
  export async function calendarPage(options, dependencies = {}) {
96
- const calendars = options.selection.calendars;
97
- let cursor = cursorOf(options.cursor, calendars);
98
- const calendar = calendars[cursor.calendar];
99
- const common = {
100
- directory: options.directory,
101
- instance: options.instance,
102
- client: options.client,
103
- requiredScopes: [GOOGLE_SCOPES.calendar],
104
- signal: options.signal,
105
- };
105
+ let cursor = cursorOf(options.cursor);
106
+ const shared = common(options);
106
107
  let answer;
107
108
  try {
108
109
  answer = await googleJson(
109
110
  {
110
- ...common,
111
+ ...shared,
111
112
  goneCode: "google_calendar_cursor_expired",
112
- url: pageUrl(calendar, cursor, options.limit, options.backfillFloor),
113
+ url: pageUrl(cursor, options.limit, options.backfillFloor),
113
114
  },
114
115
  dependencies,
115
116
  );
@@ -117,11 +118,9 @@ export async function calendarPage(options, dependencies = {}) {
117
118
  if (!(error instanceof KitRefusal) || error.code !== "google_calendar_cursor_expired") {
118
119
  throw error;
119
120
  }
120
- const syncTokens = { ...cursor.syncTokens };
121
- delete syncTokens[calendar];
122
- cursor = { calendar: cursor.calendar, pageToken: null, syncTokens };
121
+ cursor = { pageToken: null, syncToken: null };
123
122
  answer = await googleJson(
124
- { ...common, url: pageUrl(calendar, cursor, options.limit, options.backfillFloor) },
123
+ { ...shared, url: pageUrl(cursor, options.limit, options.backfillFloor) },
125
124
  dependencies,
126
125
  );
127
126
  }
@@ -135,23 +134,18 @@ export async function calendarPage(options, dependencies = {}) {
135
134
  }
136
135
  let next;
137
136
  if (answer.nextPageToken !== undefined) {
138
- next = { calendar: cursor.calendar, pageToken: answer.nextPageToken, syncTokens: cursor.syncTokens };
137
+ next = { pageToken: answer.nextPageToken, syncToken: cursor.syncToken };
139
138
  } else {
140
139
  if (typeof answer.nextSyncToken !== "string") {
141
140
  throw new KitRefusal("google_provider_response_invalid", "calendar sync token");
142
141
  }
143
- const syncTokens = { ...cursor.syncTokens, [calendar]: answer.nextSyncToken };
144
- next = {
145
- calendar: cursor.calendar + 1 === calendars.length ? 0 : cursor.calendar + 1,
146
- pageToken: null,
147
- syncTokens,
148
- };
142
+ next = { pageToken: null, syncToken: answer.nextSyncToken };
149
143
  }
150
144
  const normalized = [];
151
145
  const dropped = [];
152
146
  for (const event of items) {
153
147
  try {
154
- normalized.push(normalizedEvent(event, calendar));
148
+ normalized.push(normalizedEvent(event));
155
149
  } catch (error) {
156
150
  if (
157
151
  error instanceof KitRefusal &&
@@ -172,6 +166,6 @@ export async function calendarPage(options, dependencies = {}) {
172
166
  items: normalized,
173
167
  dropped,
174
168
  cursor: JSON.stringify(next),
175
- more: answer.nextPageToken !== undefined || cursor.calendar + 1 < calendars.length,
169
+ more: answer.nextPageToken !== undefined,
176
170
  };
177
171
  }
@@ -1,7 +1,6 @@
1
1
  import { ITEM_BODY_MAX_BYTES } from "../contract.js";
2
2
  import { KitRefusal } from "../refusal.js";
3
3
  import { googleBoundedText, googleJson } from "./gateway.js";
4
- import { GOOGLE_DRIVE_ID } from "./id.js";
5
4
  import { GOOGLE_SCOPES } from "./oauth.js";
6
5
 
7
6
  const DRIVE_ORIGIN = "https://www.googleapis.com";
@@ -20,17 +19,10 @@ const TEXT_MIMES = new Set([
20
19
  "application/xhtml+xml",
21
20
  "image/svg+xml",
22
21
  ]);
23
- const FILE_FIELDS = "id,name,mimeType,modifiedTime,version,parents,webViewLink,owners,trashed";
24
- const FOLDER_FRONTIER_MAX = 4096;
22
+ const FILE_FIELDS =
23
+ "id,name,mimeType,modifiedTime,version,parents,webViewLink,owners,ownedByMe,trashed";
25
24
 
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) {
25
+ function cursorOf(value) {
34
26
  if (value === null) return null;
35
27
  let cursor;
36
28
  try {
@@ -40,29 +32,9 @@ function cursorOf(value, selections) {
40
32
  }
41
33
  const validSnapshot =
42
34
  cursor?.phase === "snapshot" &&
43
- Number.isInteger(cursor.selection) &&
44
- cursor.selection >= 0 &&
45
- cursor.selection < selections.length &&
46
35
  (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";
36
+ typeof cursor.changeToken === "string";
37
+ const validChanges = cursor?.phase === "changes" && typeof cursor.pageToken === "string";
66
38
  if (!validSnapshot && !validChanges) throw new KitRefusal("google_checkpoint_invalid", "drive");
67
39
  return cursor;
68
40
  }
@@ -70,8 +42,7 @@ function cursorOf(value, selections) {
70
42
  function common(options) {
71
43
  return {
72
44
  directory: options.directory,
73
- instance: options.instance,
74
- client: options.client,
45
+ account: options.account,
75
46
  requiredScopes: [GOOGLE_SCOPES.drive],
76
47
  signal: options.signal,
77
48
  };
@@ -86,14 +57,10 @@ function fileUrl(id, query = {}) {
86
57
  function fileMetadata(value) {
87
58
  if (
88
59
  typeof value?.id !== "string" ||
89
- !GOOGLE_DRIVE_ID.test(value.id) ||
90
60
  typeof value.name !== "string" ||
91
61
  typeof value.mimeType !== "string" ||
92
62
  (value.parents !== undefined &&
93
- (!Array.isArray(value.parents) ||
94
- value.parents.some(
95
- (parent) => typeof parent !== "string" || !GOOGLE_DRIVE_ID.test(parent),
96
- )))
63
+ (!Array.isArray(value.parents) || value.parents.some((parent) => typeof parent !== "string")))
97
64
  ) {
98
65
  throw new KitRefusal("google_provider_response_invalid", "drive file");
99
66
  }
@@ -102,7 +69,6 @@ function fileMetadata(value) {
102
69
 
103
70
  async function parentPath(options, file, dependencies, cache) {
104
71
  const names = [];
105
- const identifiers = [];
106
72
  const seen = new Set();
107
73
  let parent = file.parents?.[0] ?? null;
108
74
  while (parent !== null && parent !== "root") {
@@ -126,10 +92,9 @@ async function parentPath(options, file, dependencies, cache) {
126
92
  throw new KitRefusal("google_provider_response_invalid", "drive parent");
127
93
  }
128
94
  names.unshift(metadata.name);
129
- identifiers.unshift(metadata.id);
130
95
  parent = metadata.parents?.[0] ?? null;
131
96
  }
132
- return { names, identifiers };
97
+ return names;
133
98
  }
134
99
 
135
100
  async function contentOf(options, file, dependencies) {
@@ -190,7 +155,7 @@ async function normalizedFile(options, file, dependencies, cache) {
190
155
  author: authorOf(metadata),
191
156
  url: metadata.webViewLink ?? null,
192
157
  attachments: [],
193
- container: `google/drive${folders.names.length === 0 ? "" : `/${folders.names.join("/")}`}`,
158
+ container: `google/drive${folders.length === 0 ? "" : `/${folders.join("/")}`}`,
194
159
  };
195
160
  }
196
161
 
@@ -239,114 +204,45 @@ async function startPageToken(options, dependencies) {
239
204
  return answer.startPageToken;
240
205
  }
241
206
 
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");
207
+ function snapshotQuery(backfillFloor) {
208
+ const clauses = ["'me' in owners", "trashed = false", `mimeType != '${FOLDER_MIME}'`];
209
+ if (backfillFloor !== null && backfillFloor !== undefined) {
210
+ clauses.push(`modifiedTime >= '${backfillFloor}'`);
300
211
  }
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
212
+ return clauses.join(" and ");
213
+ }
214
+
215
+ async function snapshotPage(options, cursor, dependencies) {
216
+ const url = new URL("/drive/v3/files", DRIVE_ORIGIN);
217
+ url.searchParams.set("q", snapshotQuery(options.backfillFloor));
218
+ url.searchParams.set("pageSize", `${options.limit}`);
219
+ url.searchParams.set("fields", `nextPageToken,files(${FILE_FIELDS})`);
220
+ if (cursor.pageToken !== null) url.searchParams.set("pageToken", cursor.pageToken);
221
+ const answer = await googleJson({ ...common(options), url: url.toString() }, dependencies);
222
+ if (
223
+ !Array.isArray(answer.files) ||
224
+ (answer.nextPageToken !== undefined && typeof answer.nextPageToken !== "string")
314
225
  ) {
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 };
226
+ throw new KitRefusal("google_provider_response_invalid", "drive files");
335
227
  }
228
+ const normalized = await normalizeFiles(
229
+ options,
230
+ answer.files.map(fileMetadata),
231
+ dependencies,
232
+ new Map(),
233
+ );
234
+ const next =
235
+ answer.nextPageToken === undefined
236
+ ? { phase: "changes", pageToken: cursor.changeToken }
237
+ : { phase: "snapshot", pageToken: answer.nextPageToken, changeToken: cursor.changeToken };
336
238
  return {
337
239
  items: normalized.items,
338
- dropped: [...dropped, ...normalized.dropped],
240
+ dropped: normalized.dropped,
339
241
  cursor: JSON.stringify(next),
340
242
  more: true,
341
243
  };
342
244
  }
343
245
 
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
246
  async function changesPage(options, cursor, dependencies) {
351
247
  const url = new URL("/drive/v3/changes", DRIVE_ORIGIN);
352
248
  url.searchParams.set("pageToken", cursor.pageToken);
@@ -370,66 +266,45 @@ async function changesPage(options, cursor, dependencies) {
370
266
  }
371
267
  const files = answer.changes
372
268
  .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);
269
+ .map((change) => fileMetadata(change.file))
270
+ .filter((file) => file.ownedByMe === true && file.trashed !== true);
271
+ const normalized = await normalizeFiles(options, files, dependencies, new Map());
387
272
  const pageToken = answer.nextPageToken ?? answer.newStartPageToken;
388
273
  if (typeof pageToken !== "string") {
389
274
  throw new KitRefusal("google_provider_response_invalid", "drive change token");
390
275
  }
391
276
  return {
392
277
  items: normalized.items,
393
- dropped: [...dropped, ...normalized.dropped],
394
- cursor: JSON.stringify({ phase: "changes", selection: 0, pageToken }),
278
+ dropped: normalized.dropped,
279
+ cursor: JSON.stringify({ phase: "changes", pageToken }),
395
280
  more: answer.nextPageToken !== undefined,
396
281
  };
397
282
  }
398
283
 
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);
284
+ async function openedSnapshot(options, dependencies) {
285
+ return { phase: "snapshot", pageToken: null, changeToken: await startPageToken(options, dependencies) };
286
+ }
287
+
288
+ export async function driveEmail(options, dependencies = {}) {
289
+ const about = await googleJson(
290
+ { ...common(options), url: `${DRIVE_ORIGIN}/drive/v3/about?fields=user(emailAddress)` },
291
+ dependencies,
292
+ );
293
+ if (typeof about.user?.emailAddress !== "string") {
294
+ throw new KitRefusal("google_provider_response_invalid", "drive about");
413
295
  }
296
+ return about.user.emailAddress;
297
+ }
298
+
299
+ export async function drivePage(options, dependencies = {}) {
300
+ const cursor = cursorOf(options.cursor) ?? (await openedSnapshot(options, dependencies));
301
+ if (cursor.phase === "snapshot") return snapshotPage(options, cursor, dependencies);
414
302
  try {
415
303
  return await changesPage(options, cursor, dependencies);
416
304
  } catch (error) {
417
305
  if (!(error instanceof KitRefusal) || error.code !== "google_drive_cursor_expired") {
418
306
  throw error;
419
307
  }
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
- );
308
+ return snapshotPage(options, await openedSnapshot(options, dependencies), dependencies);
434
309
  }
435
310
  }