@agentshouse/kit 0.0.1 → 0.1.0-alpha.2
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/LICENSE +21 -0
- package/README.md +1696 -2
- package/bin/connect-linux.sh +127 -0
- package/bin/kit.js +1023 -0
- package/package.json +33 -3
- package/src/acp.js +99 -0
- package/src/adapter-mcp.js +44 -0
- package/src/adapter.js +252 -0
- package/src/agent-logins.js +65 -0
- package/src/agents.js +449 -0
- package/src/apply.js +29 -0
- package/src/attach.js +52 -0
- package/src/binding.js +90 -0
- package/src/blocks.js +156 -0
- package/src/bridge.js +449 -0
- package/src/bytes.js +89 -0
- package/src/channel.js +37 -0
- package/src/command.js +109 -0
- package/src/configuration.js +465 -0
- package/src/connector-login.js +91 -0
- package/src/connector.js +107 -0
- package/src/continuation.js +61 -0
- package/src/contract.js +363 -0
- package/src/conversation-originals.js +143 -0
- package/src/custom/authorization.js +63 -0
- package/src/declaration.js +35 -0
- package/src/delivery.js +191 -0
- package/src/directory.js +102 -0
- package/src/draft.js +89 -0
- package/src/google/calendar.js +177 -0
- package/src/google/declared.js +146 -0
- package/src/google/drive.js +435 -0
- package/src/google/gateway.js +166 -0
- package/src/google/gmail.js +249 -0
- package/src/google/id.js +1 -0
- package/src/google/oauth.js +273 -0
- package/src/google/source.js +25 -0
- package/src/hook.js +376 -0
- package/src/host.js +121 -0
- package/src/house-main.js +79 -0
- package/src/house.js +1085 -0
- package/src/inbound.js +175 -0
- package/src/instance/google.js +1 -0
- package/src/launcher-main.js +19 -0
- package/src/launcher.js +276 -0
- package/src/local-files.js +113 -0
- package/src/login.js +274 -0
- package/src/mcp.js +71 -0
- package/src/migrator.js +248 -0
- package/src/mount.js +582 -0
- package/src/operation.js +20 -0
- package/src/original.js +631 -0
- package/src/peer.js +124 -0
- package/src/plan.js +57 -0
- package/src/question.js +316 -0
- package/src/refusal.js +79 -0
- package/src/repository.js +521 -0
- package/src/session-bin/house +2 -0
- package/src/settlement.js +57 -0
- package/src/setup.js +185 -0
- package/src/source-originals.js +116 -0
- package/src/source.js +286 -0
- package/src/starter-set/grilling/LICENSE +21 -0
- package/src/starter-set/grilling/SKILL.md +30 -0
- package/src/starter-set/grilling/agents/openai.yaml +3 -0
- package/src/starter-set.js +17 -0
- package/src/state.js +157 -0
- package/src/sync-library.js +293 -0
- package/src/sync-state.js +116 -0
- package/src/sync.js +477 -0
- package/src/telegram.js +1045 -0
- package/src/terminal.js +57 -0
- package/src/tmux.js +119 -0
- package/src/worker.js +756 -0
- package/src/workspace.js +500 -0
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
}
|