@agentshouse/kit 0.1.0-alpha.9 → 0.2.1-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 (93) hide show
  1. package/README.md +14 -1740
  2. package/dist/acp.js +120 -0
  3. package/dist/agents.js +90 -0
  4. package/dist/api.js +45 -0
  5. package/dist/blocks.js +51 -0
  6. package/dist/bridge.js +124 -0
  7. package/dist/clis.js +110 -0
  8. package/dist/conversations.js +538 -0
  9. package/dist/files.js +21 -0
  10. package/dist/home.js +24 -0
  11. package/dist/house-main.js +92 -0
  12. package/dist/instructions.js +28 -0
  13. package/dist/kit-main.js +16 -0
  14. package/dist/login.js +112 -0
  15. package/dist/resident.js +66 -0
  16. package/dist/secret-input.js +16 -0
  17. package/dist/sign-in.js +83 -0
  18. package/dist/stream.js +47 -0
  19. package/package.json +34 -10
  20. package/bin/connect-linux.sh +0 -134
  21. package/bin/kit.js +0 -1023
  22. package/bootstrap.json +0 -17
  23. package/src/acp.js +0 -99
  24. package/src/adapter-mcp.js +0 -44
  25. package/src/adapter.js +0 -252
  26. package/src/agent-logins.js +0 -65
  27. package/src/agents.js +0 -504
  28. package/src/apply.js +0 -29
  29. package/src/attach.js +0 -52
  30. package/src/binding.js +0 -90
  31. package/src/blocks.js +0 -156
  32. package/src/bridge.js +0 -449
  33. package/src/bytes.js +0 -89
  34. package/src/channel.js +0 -37
  35. package/src/command.js +0 -109
  36. package/src/configuration.js +0 -465
  37. package/src/connector-login.js +0 -91
  38. package/src/connector.js +0 -107
  39. package/src/continuation.js +0 -61
  40. package/src/contract.js +0 -363
  41. package/src/conversation-originals.js +0 -143
  42. package/src/custom/authorization.js +0 -63
  43. package/src/declaration.js +0 -35
  44. package/src/delivery.js +0 -191
  45. package/src/directory.js +0 -102
  46. package/src/draft.js +0 -89
  47. package/src/google/calendar.js +0 -177
  48. package/src/google/declared.js +0 -146
  49. package/src/google/drive.js +0 -435
  50. package/src/google/gateway.js +0 -166
  51. package/src/google/gmail.js +0 -249
  52. package/src/google/id.js +0 -1
  53. package/src/google/oauth.js +0 -273
  54. package/src/google/source.js +0 -25
  55. package/src/hook.js +0 -376
  56. package/src/host.js +0 -121
  57. package/src/house-main.js +0 -79
  58. package/src/house.js +0 -1090
  59. package/src/inbound.js +0 -175
  60. package/src/instance/google.js +0 -1
  61. package/src/kit-work-stream.js +0 -84
  62. package/src/launcher-main.js +0 -19
  63. package/src/launcher.js +0 -276
  64. package/src/local-files.js +0 -113
  65. package/src/login.js +0 -274
  66. package/src/mcp.js +0 -71
  67. package/src/migrator.js +0 -248
  68. package/src/mount.js +0 -582
  69. package/src/operation.js +0 -20
  70. package/src/original.js +0 -631
  71. package/src/peer.js +0 -124
  72. package/src/plan.js +0 -57
  73. package/src/question.js +0 -316
  74. package/src/refusal.js +0 -79
  75. package/src/repository.js +0 -521
  76. package/src/session-bin/house +0 -2
  77. package/src/settlement.js +0 -57
  78. package/src/setup.js +0 -190
  79. package/src/source-originals.js +0 -116
  80. package/src/source.js +0 -286
  81. package/src/starter-set/grilling/LICENSE +0 -21
  82. package/src/starter-set/grilling/SKILL.md +0 -30
  83. package/src/starter-set/grilling/agents/openai.yaml +0 -3
  84. package/src/starter-set.js +0 -17
  85. package/src/state.js +0 -157
  86. package/src/sync-library.js +0 -293
  87. package/src/sync-state.js +0 -120
  88. package/src/sync.js +0 -482
  89. package/src/telegram.js +0 -1045
  90. package/src/terminal.js +0 -57
  91. package/src/tmux.js +0 -119
  92. package/src/worker.js +0 -925
  93. package/src/workspace.js +0 -500
@@ -1,63 +0,0 @@
1
- import { resolveEnvironment, runCommand } from "../command.js";
2
- import { AUTHORIZATION_OUTPUT_MAX_BYTES, KIT_CONTRACT_VERSION } from "../contract.js";
3
- import { KitRefusal } from "../refusal.js";
4
-
5
- function authorizationRequest(instance, mode) {
6
- return `${JSON.stringify({
7
- contract: KIT_CONTRACT_VERSION,
8
- action: "authorization",
9
- mode,
10
- instance: instance.id,
11
- })}\n`;
12
- }
13
-
14
- function authorizationAnswer(stdout) {
15
- let answer;
16
- try {
17
- answer = JSON.parse(stdout);
18
- } catch {
19
- throw new KitRefusal("kit_connector_authorization_invalid");
20
- }
21
- if (
22
- typeof answer !== "object" ||
23
- answer === null ||
24
- Array.isArray(answer) ||
25
- Object.keys(answer).length !== 1 ||
26
- typeof answer.authorized !== "boolean"
27
- ) {
28
- throw new KitRefusal("kit_connector_authorization_invalid");
29
- }
30
- return answer.authorized;
31
- }
32
-
33
- async function serviceAuthorization(instance, service, mode, context) {
34
- const connector = service.connector;
35
- const run = await runCommand({
36
- command: connector.command,
37
- environment: await resolveEnvironment(connector),
38
- stdin: authorizationRequest(instance, mode),
39
- timeoutMs: connector.timeoutMs,
40
- outputMaxBytes: AUTHORIZATION_OUTPUT_MAX_BYTES,
41
- signal: context.signal,
42
- });
43
- if (run.status !== 0) {
44
- if (mode === "status") return false;
45
- throw new KitRefusal("kit_connector_authorization_failed", `${run.status}`);
46
- }
47
- return authorizationAnswer(run.stdout);
48
- }
49
-
50
- export async function customAuthorizationStatus(instance, context) {
51
- for (const service of instance.services) {
52
- if (!(await serviceAuthorization(instance, service, "status", context))) return false;
53
- }
54
- return true;
55
- }
56
-
57
- export async function authorizeCustom(instance, context) {
58
- for (const service of instance.services) {
59
- if (await serviceAuthorization(instance, service, "status", context)) continue;
60
- if (!(await serviceAuthorization(instance, service, "login", context))) return false;
61
- }
62
- return true;
63
- }
@@ -1,35 +0,0 @@
1
- import { KitRefusal } from "./refusal.js";
2
-
3
- export function declarationReader(code) {
4
- const refuse = (declaration) => {
5
- throw new KitRefusal(code, declaration);
6
- };
7
- const plain = (value, declaration) => {
8
- if (typeof value !== "object" || value === null || Array.isArray(value)) refuse(declaration);
9
- return value;
10
- };
11
- const closed = (value, fields, declaration) => {
12
- plain(value, declaration);
13
- for (const field of Object.keys(value)) {
14
- if (!fields.includes(field)) refuse(`${declaration}.${field}`);
15
- }
16
- return value;
17
- };
18
- const text = (value, declaration, maximum) => {
19
- if (typeof value !== "string" || value.length === 0 || value.length > maximum) {
20
- refuse(declaration);
21
- }
22
- return value;
23
- };
24
- const optionalText = (value, declaration, maximum) =>
25
- value === undefined ? null : text(value, declaration, maximum);
26
- const positiveCount = (value, declaration, maximum = Number.MAX_SAFE_INTEGER) => {
27
- if (!Number.isInteger(value) || value < 1 || value > maximum) refuse(declaration);
28
- return value;
29
- };
30
- const filled = (value, declaration) => {
31
- if (!Array.isArray(value) || value.length === 0) refuse(declaration);
32
- return value;
33
- };
34
- return { refuse, plain, closed, text, optionalText, positiveCount, filled };
35
- }
package/src/delivery.js DELETED
@@ -1,191 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { KIT_CAPTURE_BATCH_MAX } from "./contract.js";
3
-
4
- const DAY_MS = 24 * 60 * 60 * 1000;
5
-
6
- const RETRY_CODES = new Set([
7
- "kit_house_unavailable",
8
- "environment_control_lease_active",
9
- "rate_limited",
10
- "service_mode_write_halted",
11
- ]);
12
-
13
- const FAILURE_CATEGORIES = {
14
- kit_command_timeout: "provider",
15
- kit_command_cancelled: "provider",
16
- kit_command_output_exceeded: "provider",
17
- kit_command_unavailable: "provider",
18
- kit_connector_failed: "provider",
19
- kit_connector_line_invalid: "provider",
20
- kit_connector_limit_exceeded: "provider",
21
- kit_item_invalid: "provider",
22
- kit_bound_exceeded: "provider",
23
- dedup_key_conflict: "provider",
24
- kit_secret_unresolved: "config",
25
- kit_house_credential_refused: "config",
26
- kit_configuration_invalid: "config",
27
- kit_instance_unknown: "config",
28
- kit_state_name_invalid: "config",
29
- kit_upgrade_required: "config",
30
- kit_declaration_invalid: "config",
31
- kit_source_not_found: "config",
32
- kit_source_mismatch: "config",
33
- kit_route_unknown: "config",
34
- room_archived: "config",
35
- room_deleting: "config",
36
- kit_room_not_writable: "authority",
37
- operation_denied: "authority",
38
- kit_credential_rejected: "authority",
39
- rate_limited: "house",
40
- service_mode_write_halted: "house",
41
- storage_full: "house",
42
- kit_capture_rejected: "house",
43
- kit_lease_rejected: "house",
44
- environment_control_lease_active: "house",
45
- environment_control_lease_rejected: "authority",
46
- kit_apply_rejected: "house",
47
- kit_house_unavailable: "network",
48
- google_authorization_required: "authority",
49
- google_authority_revoked: "authority",
50
- google_scope_revoked: "authority",
51
- google_authorization_rejected: "authority",
52
- google_authorization_cancelled: "authority",
53
- google_authorization_unreadable: "config",
54
- google_provider_unavailable: "provider",
55
- google_provider_response_invalid: "provider",
56
- google_checkpoint_invalid: "provider",
57
- google_service_unsupported: "config",
58
- google_oauth_client_unconfigured: "config",
59
- google_drive_selection_exceeded: "config",
60
- google_drive_item_missing: "provider",
61
- };
62
-
63
- export function fileDescriptor(file) {
64
- const { path: _path, ...descriptor } = file;
65
- return descriptor;
66
- }
67
-
68
- function revisionAttachments(attachments) {
69
- return attachments.map((attachment) =>
70
- attachment.file === undefined ? attachment : { ...attachment, file: fileDescriptor(attachment.file) },
71
- );
72
- }
73
-
74
- export function acceptedVersion(key, position, file) {
75
- const identity = createHash("sha256")
76
- .update(JSON.stringify([key, position, file.bytes, file.sha256, file.media_type ?? null]))
77
- .digest();
78
- identity[6] = (identity[6] & 0x0f) | 0x80;
79
- identity[8] = (identity[8] & 0x3f) | 0x80;
80
- const hex = identity.subarray(0, 16).toString("hex");
81
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
82
- }
83
-
84
- function deliveredAttachments(key, attachments) {
85
- return attachments.map((attachment, position) => {
86
- if (attachment.file === undefined) return attachment;
87
- const descriptor = fileDescriptor(attachment.file);
88
- return {
89
- original: { version: acceptedVersion(key, position, descriptor), ...descriptor },
90
- ...(attachment.name === undefined ? {} : { name: attachment.name }),
91
- };
92
- });
93
- }
94
-
95
- export function deliveryKey(binding, item) {
96
- const revisionIdentity =
97
- item.nativeVersion ??
98
- (item.providerModifiedAt === null || item.providerModifiedAt === undefined
99
- ? null
100
- : [
101
- item.providerModifiedAt,
102
- createHash("sha256")
103
- .update(
104
- JSON.stringify([
105
- item.container ?? null,
106
- item.body,
107
- item.author,
108
- item.url,
109
- revisionAttachments(item.attachments),
110
- ]),
111
- )
112
- .digest("hex"),
113
- ]);
114
- return createHash("sha256")
115
- .update(
116
- JSON.stringify([
117
- binding.sourceRef,
118
- binding.service,
119
- item.namespace,
120
- item.nativeObjectId,
121
- revisionIdentity,
122
- ]),
123
- )
124
- .digest("hex");
125
- }
126
-
127
- export function deliveredItem(binding, item) {
128
- const key = deliveryKey(binding, item);
129
- return {
130
- dedup_key: key,
131
- observed_at: item.observedAt,
132
- provider: item.namespace,
133
- container: item.container ?? `${item.namespace}/${item.nativeObjectId}`,
134
- native_object_id: item.nativeObjectId,
135
- ...(item.nativeVersion === null ? {} : { native_version: item.nativeVersion }),
136
- ...(item.providerModifiedAt === null || item.providerModifiedAt === undefined
137
- ? {}
138
- : { provider_modified: item.providerModifiedAt }),
139
- body: item.body,
140
- ...(item.author === null ? {} : { author: item.author }),
141
- ...(item.url === null ? {} : { url: item.url }),
142
- ...(item.attachments.length === 0
143
- ? {}
144
- : { attachments: deliveredAttachments(key, item.attachments) }),
145
- };
146
- }
147
-
148
- export function newCheckpoint(backfill, now) {
149
- if (backfill.kind === "all") return { cursor: null, backfill: null };
150
- if (backfill.kind === "days") {
151
- return {
152
- cursor: null,
153
- backfill: {
154
- kind: "days",
155
- floor: new Date(now.getTime() - backfill.bound * DAY_MS).toISOString(),
156
- },
157
- };
158
- }
159
- return { cursor: null, backfill: { kind: "count", remaining: backfill.bound } };
160
- }
161
-
162
- export function itemLimit(checkpoint) {
163
- return checkpoint.backfill?.kind === "count"
164
- ? Math.min(KIT_CAPTURE_BATCH_MAX, checkpoint.backfill.remaining)
165
- : KIT_CAPTURE_BATCH_MAX;
166
- }
167
-
168
- export function admittedItems(checkpoint, items) {
169
- if (checkpoint.backfill?.kind !== "days") return items;
170
- const floor = Date.parse(checkpoint.backfill.floor);
171
- return items.filter((item) => Date.parse(item.observedAt) >= floor);
172
- }
173
-
174
- export function advancedCheckpoint(checkpoint, cursor, delivered) {
175
- if (checkpoint.backfill?.kind !== "count") return { cursor, backfill: checkpoint.backfill };
176
- const remaining = Math.max(0, checkpoint.backfill.remaining - delivered);
177
- return { cursor, backfill: remaining === 0 ? null : { kind: "count", remaining } };
178
- }
179
-
180
- export function stallOf(code, houseRetry = null) {
181
- if (houseRetry !== null) return { code, disposition: houseRetry ? "retry" : "blocked", category: "house" };
182
- return { code, disposition: RETRY_CODES.has(code) ? "retry" : "blocked" };
183
- }
184
-
185
- export function healthOfRun(stall) {
186
- if (stall === null) return { state: "ok", category: null };
187
- return {
188
- state: stall.disposition === "retry" ? "retrying" : "failed",
189
- category: stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal",
190
- };
191
- }
package/src/directory.js DELETED
@@ -1,102 +0,0 @@
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";
5
- import { LAUNCH_DIRECTORY_BASE } from "./contract.js";
6
- import { KitRefusal } from "./refusal.js";
7
- import { seedStarterSet } from "./starter-set.js";
8
-
9
- export function launchBase(dependencies = {}) {
10
- return dependencies.launchBase ?? LAUNCH_DIRECTORY_BASE;
11
- }
12
-
13
- function defaultDirectory(base, agentId) {
14
- if (typeof agentId !== "string" || agentId.length === 0) return null;
15
- const expected = resolve(base, agentId);
16
- return dirname(expected) === resolve(base) ? expected : null;
17
- }
18
-
19
- async function made(path) {
20
- try {
21
- await mkdir(path, { recursive: true });
22
- return null;
23
- } catch (error) {
24
- return error.code ?? "unknown";
25
- }
26
- }
27
-
28
- export async function prepareLaunchBase(base) {
29
- const refused = await made(base);
30
- if (refused !== null) {
31
- throw new KitRefusal(
32
- "kit_launch_base_unprepared",
33
- `${base} could not be created (${refused}); create that absolute directory and give this installation write access to it`,
34
- );
35
- }
36
- }
37
-
38
- function unavailable(path, cause) {
39
- return new KitRefusal("kit_launch_directory_unavailable", `${path} ${cause}`);
40
- }
41
-
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
- }
61
- }
62
-
63
- function within(base, selected) {
64
- const path = relative(base, selected);
65
- return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
66
- }
67
-
68
- export async function readyLaunchDirectory(selected, base, agentId = null, options = {}) {
69
- if (typeof selected !== "string" || !isAbsolute(selected)) {
70
- throw unavailable(selected, "is not an absolute directory path");
71
- }
72
- const path = resolve(selected);
73
- if (options.container && !within(resolve(base), path)) {
74
- throw unavailable(selected, `resolves outside ${base}`);
75
- }
76
- if (path === defaultDirectory(base, agentId) && !(await isDirectory(path))) {
77
- await seedDefaultDirectory(selected, base, path);
78
- }
79
- let described;
80
- try {
81
- described = await stat(path);
82
- } catch (error) {
83
- throw unavailable(selected, `is unavailable (${error.code ?? "unknown"})`);
84
- }
85
- if (!described.isDirectory()) throw unavailable(selected, "is not a directory");
86
- try {
87
- await access(path, constants.R_OK | constants.X_OK);
88
- } catch (error) {
89
- throw unavailable(selected, `is not accessible (${error.code ?? "unknown"})`);
90
- }
91
- if (options.container) {
92
- let mounted;
93
- let actual;
94
- try {
95
- mounted = await realpath(base);
96
- actual = await realpath(path);
97
- } catch (error) {
98
- throw unavailable(selected, `could not be resolved (${error.code ?? "unknown"})`);
99
- }
100
- if (!within(mounted, actual)) throw unavailable(selected, `resolves outside ${base}`);
101
- }
102
- }
package/src/draft.js DELETED
@@ -1,89 +0,0 @@
1
- import { assistantBlocks, blocksText, withinAssistantBound } from "./blocks.js";
2
-
3
- const DRAFT_INTERVAL_MS = 500;
4
-
5
- export function observedAssistantDrafts(options) {
6
- const intervalMs = options.intervalMs ?? DRAFT_INTERVAL_MS;
7
- const now = options.now ?? (() => Date.now());
8
- let timer = null;
9
- let sentAt = null;
10
- let sent = null;
11
- let latest = null;
12
-
13
- function write() {
14
- timer = null;
15
- if (latest === null || latest === sent) return;
16
- sent = latest;
17
- sentAt = now();
18
- options.send(sent);
19
- }
20
-
21
- function cancel() {
22
- if (timer === null) return;
23
- clearTimeout(timer);
24
- timer = null;
25
- }
26
-
27
- return {
28
- observe(view) {
29
- if (typeof view !== "string" || view === "") return;
30
- latest = view;
31
- if (timer !== null) return;
32
- const wait = sentAt === null ? 0 : sentAt + intervalMs - now();
33
- if (wait <= 0) {
34
- write();
35
- return;
36
- }
37
- timer = setTimeout(write, wait);
38
- timer.unref?.();
39
- },
40
- complete(view) {
41
- if (typeof view !== "string" || view === "") return;
42
- latest = view;
43
- cancel();
44
- write();
45
- },
46
- close: cancel,
47
- };
48
- }
49
-
50
- export function openAssistantDrafts(options) {
51
- let pending = null;
52
- let sending = null;
53
- let sent = null;
54
- let closed = false;
55
-
56
- function pump() {
57
- if (sending !== null || pending === null) return;
58
- const view = pending;
59
- pending = null;
60
- sent = view.text;
61
- let started;
62
- try {
63
- started = Promise.resolve(options.send(view.blocks));
64
- } catch {
65
- started = Promise.resolve();
66
- }
67
- sending = started
68
- .catch(() => undefined)
69
- .finally(() => {
70
- sending = null;
71
- pump();
72
- });
73
- }
74
-
75
- return {
76
- update(text) {
77
- if (closed || typeof text !== "string" || text === "") return;
78
- if (text === sent || text === pending?.text) return;
79
- const blocks = assistantBlocks(text);
80
- if (blocks === null || !withinAssistantBound(blocksText(blocks))) return;
81
- pending = { text, blocks };
82
- pump();
83
- },
84
- close() {
85
- closed = true;
86
- pump();
87
- },
88
- };
89
- }
@@ -1,177 +0,0 @@
1
- import { ITEM_BODY_MAX_BYTES } from "../contract.js";
2
- import { KitRefusal } from "../refusal.js";
3
- import { googleJson } from "./gateway.js";
4
- import { GOOGLE_SCOPES } from "./oauth.js";
5
-
6
- const CALENDAR_ORIGIN = "https://www.googleapis.com";
7
-
8
- function cursorOf(value, calendars) {
9
- if (value === null) return { calendar: 0, pageToken: null, syncTokens: {} };
10
- let cursor;
11
- try {
12
- cursor = JSON.parse(value);
13
- } catch {
14
- throw new KitRefusal("google_checkpoint_invalid", "calendar");
15
- }
16
- if (
17
- typeof cursor !== "object" ||
18
- cursor === null ||
19
- !Number.isInteger(cursor.calendar) ||
20
- cursor.calendar < 0 ||
21
- cursor.calendar >= calendars.length ||
22
- (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
- )
29
- ) {
30
- throw new KitRefusal("google_checkpoint_invalid", "calendar");
31
- }
32
- return cursor;
33
- }
34
-
35
- function eventBody(event) {
36
- return [event.summary, event.description, event.location]
37
- .filter((value) => typeof value === "string" && value.length > 0)
38
- .join("\n\n");
39
- }
40
-
41
- function authorOf(event) {
42
- if (typeof event.organizer?.email !== "string") return null;
43
- return typeof event.organizer.displayName === "string"
44
- ? `${event.organizer.displayName} <${event.organizer.email}>`
45
- : event.organizer.email;
46
- }
47
-
48
- function normalizedEvent(event, calendar) {
49
- if (
50
- typeof event?.id !== "string" ||
51
- typeof event.etag !== "string" ||
52
- typeof event.updated !== "string" ||
53
- Number.isNaN(Date.parse(event.updated)) ||
54
- (event.status !== undefined && typeof event.status !== "string") ||
55
- (event.htmlLink !== undefined && typeof event.htmlLink !== "string")
56
- ) {
57
- throw new KitRefusal("google_provider_response_invalid", "calendar event");
58
- }
59
- if (event.status === "cancelled") throw new KitRefusal("google_calendar_item_cancelled");
60
- const body = eventBody(event);
61
- if (Buffer.byteLength(body, "utf8") > ITEM_BODY_MAX_BYTES) {
62
- throw new KitRefusal("google_calendar_content_exceeded");
63
- }
64
- return {
65
- namespace: "google/calendar",
66
- nativeObjectId: event.id,
67
- nativeVersion: event.etag,
68
- providerModifiedAt: new Date(event.updated).toISOString(),
69
- observedAt: new Date(event.updated).toISOString(),
70
- body,
71
- author: authorOf(event),
72
- url: event.htmlLink ?? null,
73
- attachments: [],
74
- container: `google/calendar/${calendar}`,
75
- };
76
- }
77
-
78
- function pageUrl(calendar, cursor, limit, backfillFloor) {
79
- const url = new URL(
80
- `/calendar/v3/calendars/${encodeURIComponent(calendar)}/events`,
81
- CALENDAR_ORIGIN,
82
- );
83
- url.searchParams.set("maxResults", `${limit}`);
84
- url.searchParams.set("singleEvents", "true");
85
- url.searchParams.set("showDeleted", "true");
86
- 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);
89
- else if (backfillFloor !== null && backfillFloor !== undefined) {
90
- url.searchParams.set("timeMin", backfillFloor);
91
- }
92
- return url.toString();
93
- }
94
-
95
- 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
- };
106
- let answer;
107
- try {
108
- answer = await googleJson(
109
- {
110
- ...common,
111
- goneCode: "google_calendar_cursor_expired",
112
- url: pageUrl(calendar, cursor, options.limit, options.backfillFloor),
113
- },
114
- dependencies,
115
- );
116
- } catch (error) {
117
- if (!(error instanceof KitRefusal) || error.code !== "google_calendar_cursor_expired") {
118
- throw error;
119
- }
120
- const syncTokens = { ...cursor.syncTokens };
121
- delete syncTokens[calendar];
122
- cursor = { calendar: cursor.calendar, pageToken: null, syncTokens };
123
- answer = await googleJson(
124
- { ...common, url: pageUrl(calendar, cursor, options.limit, options.backfillFloor) },
125
- dependencies,
126
- );
127
- }
128
- const items = answer.items ?? [];
129
- if (
130
- !Array.isArray(items) ||
131
- (answer.nextPageToken !== undefined && typeof answer.nextPageToken !== "string") ||
132
- (answer.nextSyncToken !== undefined && typeof answer.nextSyncToken !== "string")
133
- ) {
134
- throw new KitRefusal("google_provider_response_invalid", "calendar page");
135
- }
136
- let next;
137
- if (answer.nextPageToken !== undefined) {
138
- next = { calendar: cursor.calendar, pageToken: answer.nextPageToken, syncTokens: cursor.syncTokens };
139
- } else {
140
- if (typeof answer.nextSyncToken !== "string") {
141
- throw new KitRefusal("google_provider_response_invalid", "calendar sync token");
142
- }
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
- };
149
- }
150
- const normalized = [];
151
- const dropped = [];
152
- for (const event of items) {
153
- try {
154
- normalized.push(normalizedEvent(event, calendar));
155
- } catch (error) {
156
- if (
157
- error instanceof KitRefusal &&
158
- ["google_calendar_item_cancelled", "google_calendar_content_exceeded"].includes(error.code)
159
- ) {
160
- dropped.push({
161
- namespace: "google/calendar",
162
- nativeObjectId: event.id,
163
- nativeVersion: event.etag,
164
- code: error.code,
165
- });
166
- } else {
167
- throw error;
168
- }
169
- }
170
- }
171
- return {
172
- items: normalized,
173
- dropped,
174
- cursor: JSON.stringify(next),
175
- more: answer.nextPageToken !== undefined || cursor.calendar + 1 < calendars.length,
176
- };
177
- }