@agentshouse/kit 0.1.0-alpha.42 → 0.1.0-alpha.44
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 +2 -2
- package/bootstrap.json +5 -5
- package/docs/configuration.md +7 -5
- package/docs/google.md +9 -0
- package/package.json +1 -1
- package/src/connector.js +5 -0
- package/src/contract.js +1 -1
- package/src/delivery.js +2 -1
- package/src/google/calendar.js +39 -10
- package/src/google/drive.js +53 -39
- package/src/google/gmail.js +89 -2
- package/src/workspace.js +7 -0
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ The Skill set Kit installs is Matt Pocock's
|
|
|
13
13
|
Linux (`amd64`, `arm64`) or macOS (Apple silicon, Intel):
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
16
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.44/connect-linux.sh)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Windows 11 (`amd64`), in PowerShell:
|
|
20
20
|
|
|
21
21
|
```powershell
|
|
22
|
-
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
22
|
+
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.44/connect-windows.ps1)))
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The command sets up Docker if it is missing, creates `~/.house-kit` (Kit home;
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.44",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.44/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.44/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"windows": {
|
|
17
|
-
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
17
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.44/connect-windows.ps1)))",
|
|
18
18
|
"asset": {
|
|
19
19
|
"name": "connect-windows.ps1",
|
|
20
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.44/connect-windows.ps1"
|
|
21
21
|
},
|
|
22
22
|
"workspace": {
|
|
23
23
|
"argument": "--workspace",
|
package/docs/configuration.md
CHANGED
|
@@ -76,7 +76,7 @@ It must finish within `timeout_ms` (at most 120000).
|
|
|
76
76
|
Kit writes one request line to its standard input:
|
|
77
77
|
|
|
78
78
|
```json
|
|
79
|
-
{ "contract": "
|
|
79
|
+
{ "contract": "4", "instance": "acme", "cursor": null, "limit": 100 }
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
The command prints NDJSON: `item` lines, then at most one `cursor` line.
|
|
@@ -87,10 +87,12 @@ The command prints NDJSON: `item` lines, then at most one `cursor` line.
|
|
|
87
87
|
{"type":"cursor","cursor":"page-2"}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
`namespace`, `native_object_id`, `observed_at` and `body` are required.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
`namespace`, `native_object_id`, `observed_at` and `body` are required. The
|
|
91
|
+
optional `container` and `name` are the folder path and file name the item
|
|
92
|
+
reads at below its Source in House. An attachment is a `url`, or a local `file`
|
|
93
|
+
with its exact `path`, `bytes`, `sha256` and optional `media_type`. A malformed
|
|
94
|
+
line, a non-zero exit or a timeout rejects the whole run and nothing is
|
|
95
|
+
delivered.
|
|
94
96
|
|
|
95
97
|
`backfill` bounds the first capture: `{ "days": 30 }`, `{ "count": 500 }` or
|
|
96
98
|
`{ "all": true }`.
|
package/docs/google.md
CHANGED
|
@@ -10,6 +10,15 @@ Each service captures, read-only:
|
|
|
10
10
|
- Calendar: the account's primary calendar.
|
|
11
11
|
- Drive: the files the account owns.
|
|
12
12
|
|
|
13
|
+
Each capture is a file below its Source in House:
|
|
14
|
+
|
|
15
|
+
- Gmail: `<label>/<thread subject>/<YYYY-MM-DD>-<subject>.md`. The label is the
|
|
16
|
+
message's first own label by name, else `INBOX`, `SENT` or `DRAFT`, else
|
|
17
|
+
`All Mail`.
|
|
18
|
+
- Drive: `<folder path>/<title>.md`.
|
|
19
|
+
- Calendar: `<calendar>/<YYYY-MM>/<YYYY-MM-DD>-<event title>.md`, by the
|
|
20
|
+
event's start day.
|
|
21
|
+
|
|
13
22
|
## Signing in
|
|
14
23
|
|
|
15
24
|
Kit ships no Google OAuth client. Each account signs in with your own:
|
package/package.json
CHANGED
package/src/connector.js
CHANGED
|
@@ -21,6 +21,8 @@ const ITEM_FIELDS = [
|
|
|
21
21
|
"author",
|
|
22
22
|
"url",
|
|
23
23
|
"attachments",
|
|
24
|
+
"container",
|
|
25
|
+
"name",
|
|
24
26
|
];
|
|
25
27
|
const ATTACHMENT_FIELDS = ["url", "file", "name"];
|
|
26
28
|
const FILE_FIELDS = ["path", "bytes", "sha256", "media_type"];
|
|
@@ -60,6 +62,7 @@ function itemOf(line) {
|
|
|
60
62
|
if (typeof line.body !== "string" || Buffer.byteLength(line.body, "utf8") > ITEM_BODY_MAX_BYTES) {
|
|
61
63
|
refuse("body");
|
|
62
64
|
}
|
|
65
|
+
const name = optionalText(line.name, "name", Number.MAX_SAFE_INTEGER);
|
|
63
66
|
return {
|
|
64
67
|
namespace,
|
|
65
68
|
nativeObjectId: text(line.native_object_id, "native_object_id", 512),
|
|
@@ -69,6 +72,8 @@ function itemOf(line) {
|
|
|
69
72
|
author: optionalText(line.author, "author", 512),
|
|
70
73
|
url: optionalText(line.url, "url", 2048),
|
|
71
74
|
attachments: attachmentsOf(line.attachments),
|
|
75
|
+
container: optionalText(line.container, "container", Number.MAX_SAFE_INTEGER),
|
|
76
|
+
claimed: name === null ? {} : { name },
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
|
package/src/contract.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
|
|
2
2
|
|
|
3
|
-
export const KIT_CONTRACT_VERSION = "
|
|
3
|
+
export const KIT_CONTRACT_VERSION = "4";
|
|
4
4
|
export const KIT_CAPTURE_BATCH_MAX = 100;
|
|
5
5
|
export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
|
|
6
6
|
export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
|
package/src/delivery.js
CHANGED
|
@@ -128,7 +128,8 @@ export function deliveredItem(binding, item) {
|
|
|
128
128
|
dedup_key: key,
|
|
129
129
|
observed_at: item.observedAt,
|
|
130
130
|
provider: item.namespace,
|
|
131
|
-
|
|
131
|
+
...(item.container === null ? {} : { container: item.container }),
|
|
132
|
+
...(Object.keys(item.claimed).length === 0 ? {} : { claimed: item.claimed }),
|
|
132
133
|
native_object_id: item.nativeObjectId,
|
|
133
134
|
...(item.nativeVersion === null ? {} : { native_version: item.nativeVersion }),
|
|
134
135
|
...(item.providerModifiedAt === null || item.providerModifiedAt === undefined
|
package/src/google/calendar.js
CHANGED
|
@@ -5,6 +5,7 @@ import { GOOGLE_SCOPES } from "./oauth.js";
|
|
|
5
5
|
|
|
6
6
|
const CALENDAR_ORIGIN = "https://www.googleapis.com";
|
|
7
7
|
const CALENDAR = "primary";
|
|
8
|
+
const DAY = /^\d{4}-\d{2}-\d{2}/;
|
|
8
9
|
|
|
9
10
|
function cursorOf(value) {
|
|
10
11
|
if (value === null) return { pageToken: null, syncToken: null };
|
|
@@ -31,29 +32,56 @@ function eventBody(event) {
|
|
|
31
32
|
.join("\n\n");
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
-
if (typeof
|
|
36
|
-
return typeof
|
|
37
|
-
? `${event.organizer.displayName} <${event.organizer.email}>`
|
|
38
|
-
: event.organizer.email;
|
|
35
|
+
function person(entry) {
|
|
36
|
+
if (typeof entry?.email !== "string") return null;
|
|
37
|
+
return typeof entry.displayName === "string" ? `${entry.displayName} <${entry.email}>` : entry.email;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
function
|
|
40
|
+
function momentOf(value) {
|
|
41
|
+
if (typeof value?.dateTime === "string") return value.dateTime;
|
|
42
|
+
return typeof value?.date === "string" ? value.date : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function claimedEvent(event, start) {
|
|
46
|
+
const day = start.slice(0, 10);
|
|
47
|
+
const end = momentOf(event.end);
|
|
48
|
+
const attendees = (event.attendees ?? []).map(person).filter((attendee) => attendee !== null);
|
|
49
|
+
const titled = typeof event.summary === "string" && event.summary.length > 0;
|
|
50
|
+
return {
|
|
51
|
+
name: titled ? `${day}-${event.summary}.md` : `${day}.md`,
|
|
52
|
+
start,
|
|
53
|
+
...(end === null ? {} : { end }),
|
|
54
|
+
...(attendees.length === 0 ? {} : { attendees }),
|
|
55
|
+
...(typeof event.location === "string" && event.location.length > 0
|
|
56
|
+
? { location: event.location }
|
|
57
|
+
: {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizedEvent(event, calendar) {
|
|
42
62
|
if (
|
|
43
63
|
typeof event?.id !== "string" ||
|
|
44
64
|
typeof event.etag !== "string" ||
|
|
45
65
|
typeof event.updated !== "string" ||
|
|
46
66
|
Number.isNaN(Date.parse(event.updated)) ||
|
|
47
67
|
(event.status !== undefined && typeof event.status !== "string") ||
|
|
48
|
-
(event.htmlLink !== undefined && typeof event.htmlLink !== "string")
|
|
68
|
+
(event.htmlLink !== undefined && typeof event.htmlLink !== "string") ||
|
|
69
|
+
(event.attendees !== undefined && !Array.isArray(event.attendees))
|
|
49
70
|
) {
|
|
50
71
|
throw new KitRefusal("google_provider_response_invalid", "calendar event");
|
|
51
72
|
}
|
|
52
73
|
if (event.status === "cancelled") throw new KitRefusal("google_calendar_item_cancelled");
|
|
74
|
+
const start = momentOf(event.start);
|
|
75
|
+
if (start === null || !DAY.test(start)) {
|
|
76
|
+
throw new KitRefusal("google_provider_response_invalid", "calendar event start");
|
|
77
|
+
}
|
|
53
78
|
const body = eventBody(event);
|
|
54
79
|
if (Buffer.byteLength(body, "utf8") > ITEM_BODY_MAX_BYTES) {
|
|
55
80
|
throw new KitRefusal("google_calendar_content_exceeded");
|
|
56
81
|
}
|
|
82
|
+
if (typeof calendar !== "string") {
|
|
83
|
+
throw new KitRefusal("google_provider_response_invalid", "calendar summary");
|
|
84
|
+
}
|
|
57
85
|
return {
|
|
58
86
|
namespace: "google/calendar",
|
|
59
87
|
nativeObjectId: event.id,
|
|
@@ -61,10 +89,11 @@ function normalizedEvent(event) {
|
|
|
61
89
|
providerModifiedAt: new Date(event.updated).toISOString(),
|
|
62
90
|
observedAt: new Date(event.updated).toISOString(),
|
|
63
91
|
body,
|
|
64
|
-
author:
|
|
92
|
+
author: person(event.organizer),
|
|
65
93
|
url: event.htmlLink ?? null,
|
|
66
94
|
attachments: [],
|
|
67
|
-
container:
|
|
95
|
+
container: `${calendar}/${start.slice(0, 7)}`,
|
|
96
|
+
claimed: claimedEvent(event, start),
|
|
68
97
|
};
|
|
69
98
|
}
|
|
70
99
|
|
|
@@ -145,7 +174,7 @@ export async function calendarPage(options, dependencies = {}) {
|
|
|
145
174
|
const dropped = [];
|
|
146
175
|
for (const event of items) {
|
|
147
176
|
try {
|
|
148
|
-
normalized.push(normalizedEvent(event));
|
|
177
|
+
normalized.push(normalizedEvent(event, answer.summary));
|
|
149
178
|
} catch (error) {
|
|
150
179
|
if (
|
|
151
180
|
error instanceof KitRefusal &&
|
package/src/google/drive.js
CHANGED
|
@@ -67,34 +67,51 @@ function fileMetadata(value) {
|
|
|
67
67
|
return value;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
async function
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let metadata = cache.get(parent);
|
|
78
|
-
if (metadata === undefined) {
|
|
79
|
-
metadata = fileMetadata(
|
|
80
|
-
await googleJson(
|
|
81
|
-
{
|
|
82
|
-
...common(options),
|
|
83
|
-
url: fileUrl(parent, { fields: FILE_FIELDS }),
|
|
84
|
-
notFoundCode: "google_drive_item_missing",
|
|
85
|
-
},
|
|
86
|
-
dependencies,
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
cache.set(parent, metadata);
|
|
90
|
-
}
|
|
91
|
-
if (metadata.mimeType !== FOLDER_MIME) {
|
|
92
|
-
throw new KitRefusal("google_provider_response_invalid", "drive parent");
|
|
93
|
-
}
|
|
94
|
-
names.unshift(metadata.name);
|
|
95
|
-
parent = metadata.parents?.[0] ?? null;
|
|
70
|
+
async function rootId(options, dependencies) {
|
|
71
|
+
const root = await googleJson(
|
|
72
|
+
{ ...common(options), url: fileUrl("root", { fields: "id" }) },
|
|
73
|
+
dependencies,
|
|
74
|
+
);
|
|
75
|
+
if (typeof root.id !== "string") {
|
|
76
|
+
throw new KitRefusal("google_provider_response_invalid", "drive root");
|
|
96
77
|
}
|
|
97
|
-
return
|
|
78
|
+
return root.id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function folderPaths(options, dependencies) {
|
|
82
|
+
const folders = new Map();
|
|
83
|
+
let root = null;
|
|
84
|
+
return async (file) => {
|
|
85
|
+
const names = [];
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
let parent = file.parents?.[0] ?? null;
|
|
88
|
+
if (parent === null || parent === "root") return names;
|
|
89
|
+
root ??= await rootId(options, dependencies);
|
|
90
|
+
while (parent !== null && parent !== "root" && parent !== root) {
|
|
91
|
+
if (seen.has(parent)) throw new KitRefusal("google_provider_response_invalid", "drive parents");
|
|
92
|
+
seen.add(parent);
|
|
93
|
+
let metadata = folders.get(parent);
|
|
94
|
+
if (metadata === undefined) {
|
|
95
|
+
metadata = fileMetadata(
|
|
96
|
+
await googleJson(
|
|
97
|
+
{
|
|
98
|
+
...common(options),
|
|
99
|
+
url: fileUrl(parent, { fields: FILE_FIELDS }),
|
|
100
|
+
notFoundCode: "google_drive_item_missing",
|
|
101
|
+
},
|
|
102
|
+
dependencies,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
folders.set(parent, metadata);
|
|
106
|
+
}
|
|
107
|
+
if (metadata.mimeType !== FOLDER_MIME) {
|
|
108
|
+
throw new KitRefusal("google_provider_response_invalid", "drive parent");
|
|
109
|
+
}
|
|
110
|
+
names.unshift(metadata.name);
|
|
111
|
+
parent = metadata.parents?.[0] ?? null;
|
|
112
|
+
}
|
|
113
|
+
return names;
|
|
114
|
+
};
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
async function contentOf(options, file, dependencies) {
|
|
@@ -132,7 +149,7 @@ function authorOf(file) {
|
|
|
132
149
|
: owner.emailAddress;
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
async function normalizedFile(options, file, dependencies,
|
|
152
|
+
async function normalizedFile(options, file, dependencies, pathOf) {
|
|
136
153
|
const metadata = fileMetadata(file);
|
|
137
154
|
if (
|
|
138
155
|
metadata.mimeType === FOLDER_MIME ||
|
|
@@ -144,7 +161,7 @@ async function normalizedFile(options, file, dependencies, cache) {
|
|
|
144
161
|
throw new KitRefusal("google_provider_response_invalid", "drive content");
|
|
145
162
|
}
|
|
146
163
|
const body = await contentOf(options, metadata, dependencies);
|
|
147
|
-
const folders = await
|
|
164
|
+
const folders = await pathOf(metadata);
|
|
148
165
|
return {
|
|
149
166
|
namespace: "google/drive",
|
|
150
167
|
nativeObjectId: metadata.id,
|
|
@@ -155,7 +172,8 @@ async function normalizedFile(options, file, dependencies, cache) {
|
|
|
155
172
|
author: authorOf(metadata),
|
|
156
173
|
url: metadata.webViewLink ?? null,
|
|
157
174
|
attachments: [],
|
|
158
|
-
container:
|
|
175
|
+
container: folders.length === 0 ? null : folders.join("/"),
|
|
176
|
+
claimed: { name: `${metadata.name}.md` },
|
|
159
177
|
};
|
|
160
178
|
}
|
|
161
179
|
|
|
@@ -168,13 +186,14 @@ function droppedFile(file, code) {
|
|
|
168
186
|
};
|
|
169
187
|
}
|
|
170
188
|
|
|
171
|
-
async function normalizeFiles(options, files, dependencies
|
|
189
|
+
async function normalizeFiles(options, files, dependencies) {
|
|
190
|
+
const pathOf = folderPaths(options, dependencies);
|
|
172
191
|
const items = [];
|
|
173
192
|
const dropped = [];
|
|
174
193
|
for (const file of files) {
|
|
175
194
|
if (file.mimeType === FOLDER_MIME) continue;
|
|
176
195
|
try {
|
|
177
|
-
items.push(await normalizedFile(options, file, dependencies,
|
|
196
|
+
items.push(await normalizedFile(options, file, dependencies, pathOf));
|
|
178
197
|
} catch (error) {
|
|
179
198
|
if (
|
|
180
199
|
error instanceof KitRefusal &&
|
|
@@ -225,12 +244,7 @@ async function snapshotPage(options, cursor, dependencies) {
|
|
|
225
244
|
) {
|
|
226
245
|
throw new KitRefusal("google_provider_response_invalid", "drive files");
|
|
227
246
|
}
|
|
228
|
-
const normalized = await normalizeFiles(
|
|
229
|
-
options,
|
|
230
|
-
answer.files.map(fileMetadata),
|
|
231
|
-
dependencies,
|
|
232
|
-
new Map(),
|
|
233
|
-
);
|
|
247
|
+
const normalized = await normalizeFiles(options, answer.files.map(fileMetadata), dependencies);
|
|
234
248
|
const next =
|
|
235
249
|
answer.nextPageToken === undefined
|
|
236
250
|
? { phase: "changes", pageToken: cursor.changeToken }
|
|
@@ -268,7 +282,7 @@ async function changesPage(options, cursor, dependencies) {
|
|
|
268
282
|
.filter((change) => change?.removed !== true && change?.file !== undefined)
|
|
269
283
|
.map((change) => fileMetadata(change.file))
|
|
270
284
|
.filter((file) => file.ownedByMe === true && file.trashed !== true);
|
|
271
|
-
const normalized = await normalizeFiles(options, files, dependencies
|
|
285
|
+
const normalized = await normalizeFiles(options, files, dependencies);
|
|
272
286
|
const pageToken = answer.nextPageToken ?? answer.newStartPageToken;
|
|
273
287
|
if (typeof pageToken !== "string") {
|
|
274
288
|
throw new KitRefusal("google_provider_response_invalid", "drive change token");
|
package/src/google/gmail.js
CHANGED
|
@@ -5,6 +5,9 @@ import { GOOGLE_SCOPES } from "./oauth.js";
|
|
|
5
5
|
|
|
6
6
|
const GMAIL_ORIGIN = "https://gmail.googleapis.com";
|
|
7
7
|
const EXCLUDED_LABELS = ["SPAM", "TRASH"];
|
|
8
|
+
const PLACES = ["INBOX", "SENT", "DRAFT"];
|
|
9
|
+
const ARCHIVED = "All Mail";
|
|
10
|
+
const CLAIMED_HEADERS = ["from", "to", "cc", "subject"];
|
|
8
11
|
|
|
9
12
|
function cursorOf(value) {
|
|
10
13
|
if (value === null) return { mode: "messages", pageToken: null, historyId: null };
|
|
@@ -54,6 +57,7 @@ function header(message, name) {
|
|
|
54
57
|
function normalizedMessage(message) {
|
|
55
58
|
if (
|
|
56
59
|
typeof message?.id !== "string" ||
|
|
60
|
+
typeof message.threadId !== "string" ||
|
|
57
61
|
typeof message.historyId !== "string" ||
|
|
58
62
|
!/^\d+$/.test(message.historyId) ||
|
|
59
63
|
typeof message.internalDate !== "string" ||
|
|
@@ -85,7 +89,87 @@ function normalizedMessage(message) {
|
|
|
85
89
|
author,
|
|
86
90
|
url: `https://mail.google.com/mail/u/0/#all/${encodeURIComponent(message.id)}`,
|
|
87
91
|
attachments: [],
|
|
88
|
-
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function claimedMessage(message, observedAt) {
|
|
96
|
+
const day = observedAt.slice(0, 10);
|
|
97
|
+
const subject = header(message, "subject");
|
|
98
|
+
const claimed = { name: subject === null || subject === "" ? `${day}.md` : `${day}-${subject}.md` };
|
|
99
|
+
for (const field of CLAIMED_HEADERS) {
|
|
100
|
+
const value = header(message, field);
|
|
101
|
+
if (value !== null) claimed[field] = value;
|
|
102
|
+
}
|
|
103
|
+
return claimed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function labelOf(labelIds, labels) {
|
|
107
|
+
const [user] = labelIds
|
|
108
|
+
.map((id) => labels.get(id))
|
|
109
|
+
.filter((label) => label?.type === "user")
|
|
110
|
+
.map((label) => label.name)
|
|
111
|
+
.sort();
|
|
112
|
+
return user ?? PLACES.find((id) => labelIds.includes(id)) ?? ARCHIVED;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function labelsOf(shared, dependencies) {
|
|
116
|
+
const answer = await googleJson(
|
|
117
|
+
{ ...shared, url: `${GMAIL_ORIGIN}/gmail/v1/users/me/labels` },
|
|
118
|
+
dependencies,
|
|
119
|
+
);
|
|
120
|
+
if (
|
|
121
|
+
!Array.isArray(answer.labels) ||
|
|
122
|
+
answer.labels.some(
|
|
123
|
+
(label) =>
|
|
124
|
+
typeof label?.id !== "string" ||
|
|
125
|
+
typeof label.name !== "string" ||
|
|
126
|
+
typeof label.type !== "string",
|
|
127
|
+
)
|
|
128
|
+
) {
|
|
129
|
+
throw new KitRefusal("google_provider_response_invalid", "gmail labels");
|
|
130
|
+
}
|
|
131
|
+
return new Map(answer.labels.map((label) => [label.id, label]));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function threadSubject(shared, threadId, dependencies) {
|
|
135
|
+
const url = new URL(`/gmail/v1/users/me/threads/${encodeURIComponent(threadId)}`, GMAIL_ORIGIN);
|
|
136
|
+
url.searchParams.set("format", "metadata");
|
|
137
|
+
url.searchParams.set("metadataHeaders", "Subject");
|
|
138
|
+
const thread = await googleJson(
|
|
139
|
+
{ ...shared, url: url.toString(), notFoundCode: "google_gmail_item_missing" },
|
|
140
|
+
dependencies,
|
|
141
|
+
);
|
|
142
|
+
if (
|
|
143
|
+
!Array.isArray(thread.messages) ||
|
|
144
|
+
thread.messages.length === 0 ||
|
|
145
|
+
thread.messages.some(
|
|
146
|
+
(message) => typeof message?.internalDate !== "string" || !/^\d+$/.test(message.internalDate),
|
|
147
|
+
)
|
|
148
|
+
) {
|
|
149
|
+
throw new KitRefusal("google_provider_response_invalid", "gmail thread");
|
|
150
|
+
}
|
|
151
|
+
const [opening] = [...thread.messages].sort(
|
|
152
|
+
(left, right) => Number(left.internalDate) - Number(right.internalDate),
|
|
153
|
+
);
|
|
154
|
+
const subject = header(opening, "subject");
|
|
155
|
+
return subject === "" ? null : subject;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function placement(shared, dependencies) {
|
|
159
|
+
let labels = null;
|
|
160
|
+
const subjects = new Map();
|
|
161
|
+
return async (message, item) => {
|
|
162
|
+
labels ??= await labelsOf(shared, dependencies);
|
|
163
|
+
if (!subjects.has(message.threadId)) {
|
|
164
|
+
subjects.set(message.threadId, await threadSubject(shared, message.threadId, dependencies));
|
|
165
|
+
}
|
|
166
|
+
const label = labelOf(message.labelIds, labels);
|
|
167
|
+
const subject = subjects.get(message.threadId);
|
|
168
|
+
return {
|
|
169
|
+
...item,
|
|
170
|
+
container: subject === null ? label : `${label}/${subject}`,
|
|
171
|
+
claimed: claimedMessage(message, item.observedAt),
|
|
172
|
+
};
|
|
89
173
|
};
|
|
90
174
|
}
|
|
91
175
|
|
|
@@ -202,6 +286,7 @@ export async function gmailPage(options, dependencies = {}) {
|
|
|
202
286
|
throw new KitRefusal("google_provider_response_invalid", "gmail cursor");
|
|
203
287
|
}
|
|
204
288
|
const identifiers = referencesOf(answer, cursor.mode);
|
|
289
|
+
const placed = placement(shared, dependencies);
|
|
205
290
|
const items = [];
|
|
206
291
|
const dropped = [];
|
|
207
292
|
for (const identifier of identifiers) {
|
|
@@ -216,7 +301,9 @@ export async function gmailPage(options, dependencies = {}) {
|
|
|
216
301
|
dependencies,
|
|
217
302
|
);
|
|
218
303
|
const normalized = normalizedMessage(detail);
|
|
219
|
-
if (!detail.labelIds.some((label) => EXCLUDED_LABELS.includes(label)))
|
|
304
|
+
if (!detail.labelIds.some((label) => EXCLUDED_LABELS.includes(label))) {
|
|
305
|
+
items.push(await placed(detail, normalized));
|
|
306
|
+
}
|
|
220
307
|
} catch (error) {
|
|
221
308
|
if (
|
|
222
309
|
error instanceof KitRefusal &&
|
package/src/workspace.js
CHANGED
|
@@ -201,6 +201,13 @@ export async function workspacePrerequisites(host, image) {
|
|
|
201
201
|
if (version.status !== 0 || version.stdout.trim() === "") {
|
|
202
202
|
return { supported: false, refusal: "the container runtime daemon did not answer a version" };
|
|
203
203
|
}
|
|
204
|
+
const swap = await docker(host, ["info", "--format", "{{.SwapLimit}}"]);
|
|
205
|
+
if (swap.status !== 0 || swap.stdout.trim() !== "true") {
|
|
206
|
+
return {
|
|
207
|
+
supported: false,
|
|
208
|
+
refusal: "the container runtime cannot enforce the workspace memory and swap ceiling",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
204
211
|
const formatter = await docker(host, [
|
|
205
212
|
"run",
|
|
206
213
|
"--rm",
|