@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.
- package/README.md +172 -158
- package/bin/connect-linux.sh +15 -4
- package/bin/kit.js +48 -46
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/adapter-mcp.js +23 -1
- package/src/bridge.js +4 -0
- package/src/configuration.js +13 -209
- package/src/connector-login.js +3 -84
- package/src/contract.js +4 -0
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +346 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/house.js +43 -5
- package/src/how-we-work.md +25 -0
- package/src/kit-work-stream.js +1 -1
- package/src/launcher.js +11 -4
- package/src/mcp.js +5 -0
- package/src/setup.js +8 -14
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +51 -26
- package/src/state.js +19 -1
- package/src/tasks.js +72 -0
- package/src/worker.js +46 -68
- package/src/working-copy.js +1 -15
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /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,
|
|
3
|
-
import {
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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)
|
|
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);
|
package/src/google/calendar.js
CHANGED
|
@@ -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
|
|
9
|
-
if (value === null) return {
|
|
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.
|
|
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
|
|
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/${
|
|
67
|
+
container: `google/calendar/${CALENDAR}`,
|
|
75
68
|
};
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
...
|
|
111
|
+
...shared,
|
|
111
112
|
goneCode: "google_calendar_cursor_expired",
|
|
112
|
-
url: pageUrl(
|
|
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
|
-
|
|
121
|
-
delete syncTokens[calendar];
|
|
122
|
-
cursor = { calendar: cursor.calendar, pageToken: null, syncTokens };
|
|
121
|
+
cursor = { pageToken: null, syncToken: null };
|
|
123
122
|
answer = await googleJson(
|
|
124
|
-
{ ...
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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
|
|
169
|
+
more: answer.nextPageToken !== undefined,
|
|
176
170
|
};
|
|
177
171
|
}
|
package/src/google/drive.js
CHANGED
|
@@ -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 =
|
|
24
|
-
|
|
22
|
+
const FILE_FIELDS =
|
|
23
|
+
"id,name,mimeType,modifiedTime,version,parents,webViewLink,owners,ownedByMe,trashed";
|
|
25
24
|
|
|
26
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
375
|
-
const
|
|
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:
|
|
394
|
-
cursor: JSON.stringify({ phase: "changes",
|
|
278
|
+
dropped: normalized.dropped,
|
|
279
|
+
cursor: JSON.stringify({ phase: "changes", pageToken }),
|
|
395
280
|
more: answer.nextPageToken !== undefined,
|
|
396
281
|
};
|
|
397
282
|
}
|
|
398
283
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
}
|