@chibitek/chibi 0.1.172 → 0.1.174
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/dist/assets/{abnfDiagram-O67JEVCF-CGoW52MW.js → abnfDiagram-O67JEVCF-BVDndX1g.js} +1 -1
- package/dist/assets/{arc-DEkKLw4F.js → arc-WduS6wYB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-Cyi6MYs6.js → architectureDiagram-NJMV4G6O-CYK9LkCf.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CddA0wa_.js → blockDiagram-BEXU5L5S-BMj-aPW9.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Dg8q9n90.js → c4Diagram-YGBWAQC7-cXYIStgL.js} +1 -1
- package/dist/assets/channel-TratrwmM.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BSMub34c.js → chunk-2Q5K7J3B-CZ_nCppA.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW--Xc7Up5y.js → chunk-3FUC2YCW-C3wRAbtM.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-B9Ae4f30.js → chunk-5VM5RSS4-CMvgOXJP.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dh1To9G3.js → chunk-GTNCS2PH-9wBuLQzU.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-C_5eiV2n.js → chunk-GWA4HPMP-BpJMmfAi.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-5wQuPsks.js → chunk-JWPE2WC7-BnM06ABv.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BGXS05ur.js → chunk-NETBCI7D-CjjiiXiT.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ClIMhmD5.js → chunk-WEXAMYUT-D3heHHZs.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CcKs11-L.js → chunk-XXDRQBXY-n_P41ZlM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BCiLckGU.js → classDiagram-v2-NBCMYWYE-Dz60Ek-6.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-JyAqT-ND.js → cose-bilkent-JH36ORCC-BCt6vBi9.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BznovZj0.js → cynefin-OW5HDTMX-U0uq4EAQ.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BDRrTIEB.js → cynefinDiagram-VND7K2PF-CaQX0eUJ.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BbWguhLj.js → dagre-6A5THRUB-DlCMESYH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BMk1KTBr.js → diagram-22UHCM2B-Cl1xmlTD.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-ChnEP2in.js → diagram-3UASUU5V-Cus3CPg7.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-C78imnTV.js → diagram-ATOU4E4O-i06rMvae.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-Dekx02f_.js → diagram-CDSNMT55-CUM5_heN.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-D2__fPwe.js → diagram-MLGK6HIB-B5NKRruo.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-C9l7MZm7.js → diagram-MPIPVDR6-CAumAcGl.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D4x0h3XT.js → ebnfDiagram-ZINNZB2B-B19d7d8l.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-twChz5io.js → elk-276RUBZZ-NJEpfRWi.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BXEKWWMq.js → erDiagram-OPXOYQCR-BExk6CHc.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Ct-XrJMr.js → flowDiagram-KWPJA3E3-ClExQ_p0.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-Bn92lxCs.js → ganttDiagram-FUAMR5RP-1MeM9sSl.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dvfjo7cn.js → gitGraphDiagram-X574FWY7-Tw9Dudpa.js} +1 -1
- package/dist/assets/{index-DvneZE6Z.js → index-KYHldywn.js} +140 -140
- package/dist/assets/{index-CarzpTfN.js → index-qsorLr2x.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-MJ14OyNX.js → infoDiagram-VRGFBTTK-ChU2tjvl.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-nf9xhTNX.js → ishikawaDiagram-OU5B5YK6-BeTbrX2Q.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DqfK6X2e.js → journeyDiagram-ZHPQQLJL-B7ijaWBD.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CjUR1QYT.js → kanban-definition-PNTS6WVX-JBmK2F1r.js} +1 -1
- package/dist/assets/{linear-dPqGsZwL.js → linear-B7FXe3Ue.js} +1 -1
- package/dist/assets/{mermaid.core-zL0DLPAI.js → mermaid.core-BPlfZpqv.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-C1-Jqg3_.js → mindmap-definition-NLK3R4M7-CmT3S5QR.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CFlvuAHC.js → pegDiagram-GJSIUBJH-CFFHIcdQ.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BvyT0IgF.js → pieDiagram-5QR66LMP-yTiPmXRV.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-bg7JeR2c.js → quadrantDiagram-O4NWA36T-Cw8u7esm.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-dF2Nqim8.js → railroadDiagram-XR7U4H2S-BfKggsOW.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwVu_Or6.js → requirementDiagram-PLB6GJNP-Cf3n79jF.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-wBC3-mRy.js → sankeyDiagram-IPEJSGJF-B2NkSZig.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-Bs9lHMf-.js → sequenceDiagram-PO4LG4MO-BfFYiYK3.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-d6YiGOIQ.js → sizeCapture-INFHLROL-CZ_2nC3f.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK--f6dscuE.js → stateDiagram-v2-GCMORJYK-B9jwzMXf.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-CQpzBoAc.js → swimlanes-2SLR337P-Bxhafu77.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWNGCaH3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DpoyUyqt.js → timeline-definition-EJHVYXUP-Dle5SJls.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-Dg7mR3GW.js → usecaseDiagram-POWQR4AR-CgRTu9EO.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CoEfTL7k.js → vennDiagram-UO4OBE2U-D7m_1z6B.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-24tIY01C.js → wardleyDiagram-VNRHLVJA-7-TzmX4L.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-CsoqIRqn.js → xychartDiagram-PMCCYNJV-BVXXdviS.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/index.js +2797 -1875
- package/dist-server/openmausbot.js +64 -2
- package/dist-server/pair-cli.js +64 -2
- package/dist-server/server/builtin-chibi.js +1 -1
- package/dist-server/server/dictation.js +8 -0
- package/dist-server/server/drivers/claude.js +4 -4
- package/dist-server/server/engine-tiers-view.js +49 -0
- package/dist-server/server/engine-tiers.js +37 -55
- package/dist-server/server/harness-import/engine-policy.js +6 -1
- package/dist-server/server/index.js +12 -13
- package/dist-server/server/oauth-account-core.js +153 -0
- package/dist-server/server/oauth-account-tools.js +7 -348
- package/dist-server/server/oauth-calendar-tools.js +551 -0
- package/dist-server/server/personal-bot.js +0 -26
- package/dist-server/server/remote-desktop.js +139 -7
- package/dist-server/server/routes/engine-tiers.js +12 -1
- package/dist-server/server/subscription-bots.js +2 -21
- package/dist-server/server/turn-resources.js +28 -0
- package/dist-server/server/workspace-backup-archive.js +138 -0
- package/dist-server/server/workspace-backup-http.js +50 -8
- package/dist-server/server/workspace-backup-private.js +83 -0
- package/dist-server/server/workspace-backup-secrets.js +90 -0
- package/dist-server/server/workspace-backup.js +171 -235
- package/dist-server/server/workspace-restore-paths.js +186 -0
- package/dist-server/shared/oauth-providers.js +2 -1
- package/dist-server/shared/provider-accounts.js +29 -0
- package/package.json +1 -1
- package/dist/assets/channel-Dda8O6gd.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C1ko4o6t.js +0 -8
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// What every account tool shares: the account and call types, argument
|
|
2
|
+
// basics, and one authenticated request to Google or Microsoft Graph.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { OAUTH_PROVIDERS } from "../shared/oauth-providers.js";
|
|
5
|
+
const HTTP_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_RESULT_CHARS = 48_000;
|
|
7
|
+
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
export const MAX_DOWNLOAD_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
export const READS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
10
|
+
export const CREATES = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
11
|
+
/** Sends mail or invitations to other people: cannot be taken back. */
|
|
12
|
+
export const SENDS = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
13
|
+
export const accountArg = { type: "string", description: "Email address of the account to use. Optional only when this bot has exactly one account of this provider." };
|
|
14
|
+
export const EMAIL = /^[^\s@<>,;"]+@[^\s@<>,;"]+\.[^\s@<>,;"]+$/;
|
|
15
|
+
export const noNewlines = (value) => !/[\r\n]/.test(value);
|
|
16
|
+
export const base = z.object({ account: z.string().max(320).optional() });
|
|
17
|
+
export class ToolInputError extends Error {
|
|
18
|
+
}
|
|
19
|
+
export function bounded(text, limit = MAX_RESULT_CHARS) {
|
|
20
|
+
return text.length <= limit ? text : `${text.slice(0, limit)}\n[truncated: ask for less]`;
|
|
21
|
+
}
|
|
22
|
+
export function addresses(value, field) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return [];
|
|
25
|
+
const list = (Array.isArray(value) ? value : value.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
26
|
+
if (list.length > 50)
|
|
27
|
+
throw new ToolInputError(`${field}: at most 50 addresses`);
|
|
28
|
+
for (const item of list) {
|
|
29
|
+
if (!EMAIL.test(item))
|
|
30
|
+
throw new ToolInputError(`${field}: "${item.slice(0, 80)}" is not an email address`);
|
|
31
|
+
}
|
|
32
|
+
return list;
|
|
33
|
+
}
|
|
34
|
+
export class ProviderApiError extends Error {
|
|
35
|
+
status;
|
|
36
|
+
constructor(status, message) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.status = status;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function readLimited(response, limit) {
|
|
42
|
+
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
43
|
+
if (declared > limit) {
|
|
44
|
+
await response.body?.cancel().catch(() => { });
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (!response.body)
|
|
48
|
+
return Buffer.alloc(0);
|
|
49
|
+
const reader = response.body.getReader();
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let size = 0;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const { done, value } = await reader.read();
|
|
54
|
+
if (done)
|
|
55
|
+
break;
|
|
56
|
+
size += value.byteLength;
|
|
57
|
+
if (size > limit) {
|
|
58
|
+
await reader.cancel().catch(() => { });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
chunks.push(value);
|
|
62
|
+
}
|
|
63
|
+
return Buffer.concat(chunks);
|
|
64
|
+
}
|
|
65
|
+
export function providerErrorMessage(body, status) {
|
|
66
|
+
const error = body?.error;
|
|
67
|
+
const message = typeof error === "string"
|
|
68
|
+
? error
|
|
69
|
+
: error && typeof error === "object" && typeof error.message === "string"
|
|
70
|
+
? error.message
|
|
71
|
+
: "";
|
|
72
|
+
return (message || `request failed with HTTP ${status}`).split(/\r?\n/)[0].slice(0, 300);
|
|
73
|
+
}
|
|
74
|
+
export async function call(ctx, account, url, init) {
|
|
75
|
+
const fetcher = ctx.fetch ?? fetch;
|
|
76
|
+
const { expect, ...request } = init;
|
|
77
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
78
|
+
const token = await ctx.accessToken(account.id);
|
|
79
|
+
const headers = new Headers(request.headers);
|
|
80
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
81
|
+
if (!headers.has("accept") && expect === "json")
|
|
82
|
+
headers.set("accept", "application/json");
|
|
83
|
+
const response = await fetcher(url, { ...request, headers, signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) });
|
|
84
|
+
if (response.status === 401 && attempt === 0) {
|
|
85
|
+
await response.body?.cancel().catch(() => { });
|
|
86
|
+
ctx.invalidate(account.id);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const raw = await readLimited(response, 64 * 1024);
|
|
91
|
+
let body = null;
|
|
92
|
+
try {
|
|
93
|
+
body = raw ? JSON.parse(raw.toString("utf8")) : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
body = null;
|
|
97
|
+
}
|
|
98
|
+
const message = providerErrorMessage(body, response.status);
|
|
99
|
+
if (response.status === 403)
|
|
100
|
+
throw new ProviderApiError(403, `${account.email} does not allow this: ${message}`);
|
|
101
|
+
if (response.status === 404)
|
|
102
|
+
throw new ProviderApiError(404, `Not found in ${account.email}: ${message}`);
|
|
103
|
+
throw new ProviderApiError(response.status, `${OAUTH_PROVIDERS[account.provider].label} refused the request for ${account.email}: ${message}`);
|
|
104
|
+
}
|
|
105
|
+
if (expect === "none") {
|
|
106
|
+
await response.body?.cancel().catch(() => { });
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const bytes = await readLimited(response, expect === "json" ? MAX_JSON_BYTES : MAX_DOWNLOAD_BYTES);
|
|
110
|
+
if (expect === "bytes")
|
|
111
|
+
return bytes;
|
|
112
|
+
if (!bytes)
|
|
113
|
+
throw new ProviderApiError(502, "The response was too large");
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new ProviderApiError(502, "The service returned an unreadable response");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new ProviderApiError(401, `${account.email} needs to sign in again: open Settings → Accounts and connect it again.`);
|
|
122
|
+
}
|
|
123
|
+
export function parse(schema, value, what) {
|
|
124
|
+
const parsed = schema.safeParse(value);
|
|
125
|
+
if (!parsed.success)
|
|
126
|
+
throw new ProviderApiError(502, `Unexpected ${what} response`);
|
|
127
|
+
return parsed.data;
|
|
128
|
+
}
|
|
129
|
+
export function json(value) {
|
|
130
|
+
return JSON.stringify(value, null, 1);
|
|
131
|
+
}
|
|
132
|
+
export function htmlToText(html) {
|
|
133
|
+
return html
|
|
134
|
+
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, "")
|
|
135
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
136
|
+
.replace(/<\/(p|div|tr|li|h\d)>/gi, "\n")
|
|
137
|
+
.replace(/<[^>]+>/g, "")
|
|
138
|
+
.replace(/ /g, " ")
|
|
139
|
+
.replace(/&/g, "&")
|
|
140
|
+
.replace(/</g, "<")
|
|
141
|
+
.replace(/>/g, ">")
|
|
142
|
+
.replace(/"/g, '"')
|
|
143
|
+
.replace(/'/g, "'")
|
|
144
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
145
|
+
.trim();
|
|
146
|
+
}
|
|
147
|
+
export const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
148
|
+
export const graphRecipient = z.object({ emailAddress: z.object({ address: z.string().optional(), name: z.string().optional() }).optional() });
|
|
149
|
+
export function recipient(value) {
|
|
150
|
+
const address = value?.emailAddress?.address;
|
|
151
|
+
const name = value?.emailAddress?.name;
|
|
152
|
+
return address ? (name && name !== address ? `${name} <${address}>` : address) : undefined;
|
|
153
|
+
}
|
|
@@ -4,29 +4,15 @@
|
|
|
4
4
|
// after the internal route has re-checked that this bot may use it.
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { OAUTH_PROVIDERS, } from "../shared/oauth-providers.js";
|
|
7
|
+
import { CREATES, GRAPH, MAX_DOWNLOAD_BYTES, READS, SENDS, ToolInputError, accountArg, addresses, base, bounded, call, graphRecipient, htmlToText, json, noNewlines, parse, recipient, } from "./oauth-account-core.js";
|
|
7
8
|
import { familyGranted, missingFamilies } from "./oauth-accounts.js";
|
|
9
|
+
import { CALENDAR_RUNNERS, GOOGLE_CALENDAR_TOOLS, OUTLOOK_CALENDAR_TOOLS } from "./oauth-calendar-tools.js";
|
|
8
10
|
import { officeKind, officeText } from "./office-text.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
export { ToolInputError } from "./oauth-account-core.js";
|
|
12
|
+
export { graphDateTime } from "./oauth-calendar-tools.js";
|
|
11
13
|
const MAX_BODY_CHARS = 20_000;
|
|
12
14
|
const MAX_FILE_TEXT_CHARS = 40_000;
|
|
13
|
-
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
|
14
|
-
const MAX_DOWNLOAD_BYTES = 8 * 1024 * 1024;
|
|
15
|
-
const READS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
16
|
-
const CREATES = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
17
|
-
/** Sends mail or invitations to other people: cannot be taken back. */
|
|
18
|
-
const SENDS = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
|
|
19
|
-
const accountArg = { type: "string", description: "Email address of the account to use. Optional only when this bot has exactly one account of this provider." };
|
|
20
15
|
const emailList = z.union([z.string(), z.array(z.string()).max(50)]);
|
|
21
|
-
const EMAIL = /^[^\s@<>,;"]+@[^\s@<>,;"]+\.[^\s@<>,;"]+$/;
|
|
22
|
-
const noNewlines = (value) => !/[\r\n]/.test(value);
|
|
23
|
-
function isoInstant(value) {
|
|
24
|
-
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value));
|
|
25
|
-
}
|
|
26
|
-
function isoDateOrTime(value) {
|
|
27
|
-
return /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value);
|
|
28
|
-
}
|
|
29
|
-
const base = z.object({ account: z.string().max(320).optional() });
|
|
30
16
|
const searchArgs = base.extend({ q: z.string().min(1).max(500), max: z.number().int().min(1).max(25).optional() }).strict();
|
|
31
17
|
const idArgs = (key) => base.extend({ [key]: z.string().min(1).max(512).regex(/^[A-Za-z0-9!_.=+\-/~:]+$/, "not a valid id") }).strict();
|
|
32
18
|
const draftArgs = base.extend({
|
|
@@ -35,22 +21,6 @@ const draftArgs = base.extend({
|
|
|
35
21
|
body: z.string().max(200_000),
|
|
36
22
|
cc: emailList.optional(),
|
|
37
23
|
}).strict();
|
|
38
|
-
const listEventsArgs = base.extend({
|
|
39
|
-
timeMin: z.string().refine(isoInstant, "timeMin must be an ISO 8601 date-time with a time zone, e.g. 2026-10-01T00:00:00Z"),
|
|
40
|
-
timeMax: z.string().refine(isoInstant, "timeMax must be an ISO 8601 date-time with a time zone, e.g. 2026-10-08T00:00:00Z"),
|
|
41
|
-
calendarId: z.string().min(1).max(320).optional(),
|
|
42
|
-
max: z.number().int().min(1).max(100).optional(),
|
|
43
|
-
}).strict();
|
|
44
|
-
const createEventArgs = base.extend({
|
|
45
|
-
summary: z.string().min(1).max(1_000).refine(noNewlines, "summary must be one line"),
|
|
46
|
-
start: z.string().refine(isoDateOrTime, "start must be YYYY-MM-DD or an ISO 8601 date-time"),
|
|
47
|
-
end: z.string().refine(isoDateOrTime, "end must be YYYY-MM-DD or an ISO 8601 date-time"),
|
|
48
|
-
timeZone: z.string().min(1).max(64).regex(/^[A-Za-z0-9_+\-/ ]+$/).optional(),
|
|
49
|
-
description: z.string().max(8_000).optional(),
|
|
50
|
-
location: z.string().max(1_000).optional(),
|
|
51
|
-
attendees: z.array(z.string()).max(50).optional(),
|
|
52
|
-
calendarId: z.string().min(1).max(320).optional(),
|
|
53
|
-
}).strict();
|
|
54
24
|
const listAccountsArgs = z.object({}).strict();
|
|
55
25
|
const searchSchema = (what) => ({
|
|
56
26
|
type: "object",
|
|
@@ -71,50 +41,20 @@ const draftSchema = {
|
|
|
71
41
|
required: ["to", "subject", "body"],
|
|
72
42
|
additionalProperties: false,
|
|
73
43
|
};
|
|
74
|
-
const listEventsSchema = (withCalendar) => ({
|
|
75
|
-
type: "object",
|
|
76
|
-
properties: {
|
|
77
|
-
account: accountArg,
|
|
78
|
-
timeMin: { type: "string", description: "Start of the window, ISO 8601 with a time zone (e.g. 2026-10-01T00:00:00Z)." },
|
|
79
|
-
timeMax: { type: "string", description: "End of the window, ISO 8601 with a time zone." },
|
|
80
|
-
...(withCalendar ? { calendarId: { type: "string", description: "Calendar id; defaults to the primary calendar." } } : {}),
|
|
81
|
-
max: { type: "integer", minimum: 1, maximum: 100, description: "Most events to return (default 25)." },
|
|
82
|
-
},
|
|
83
|
-
required: ["timeMin", "timeMax"],
|
|
84
|
-
additionalProperties: false,
|
|
85
|
-
});
|
|
86
|
-
const createEventSchema = (withCalendar) => ({
|
|
87
|
-
type: "object",
|
|
88
|
-
properties: {
|
|
89
|
-
account: accountArg,
|
|
90
|
-
summary: { type: "string", description: "Event title." },
|
|
91
|
-
start: { type: "string", description: "YYYY-MM-DD for an all-day event, or an ISO 8601 date-time. Without an offset, pass timeZone." },
|
|
92
|
-
end: { type: "string", description: "Same format as start. For all-day events this is the day after the last day." },
|
|
93
|
-
timeZone: { type: "string", description: "IANA time zone such as America/Los_Angeles, used when start/end have no offset." },
|
|
94
|
-
description: { type: "string" },
|
|
95
|
-
location: { type: "string" },
|
|
96
|
-
attendees: { type: "array", items: { type: "string" }, description: "Guest email addresses. They receive an invitation." },
|
|
97
|
-
...(withCalendar ? { calendarId: { type: "string", description: "Calendar id; defaults to the primary calendar." } } : {}),
|
|
98
|
-
},
|
|
99
|
-
required: ["summary", "start", "end"],
|
|
100
|
-
additionalProperties: false,
|
|
101
|
-
});
|
|
102
44
|
export const ACCOUNT_TOOLS = [
|
|
103
45
|
{ name: "list_accounts", provider: null, description: "List the Google and Microsoft accounts this bot may use, by email.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, annotations: READS, args: listAccountsArgs },
|
|
104
46
|
{ name: "gmail_search", provider: "google", family: "gmail", description: "Search Gmail with Gmail search syntax (e.g. from:alex newer_than:7d). Returns message ids, senders, subjects, dates and snippets.", inputSchema: searchSchema("Gmail search query."), annotations: READS, args: searchArgs },
|
|
105
47
|
{ name: "gmail_read", provider: "google", family: "gmail", description: "Read one Gmail message by id: headers, plain-text body and attachment names.", inputSchema: idSchema("id", "Message id from gmail_search."), annotations: READS, args: idArgs("id") },
|
|
106
48
|
{ name: "gmail_create_draft", provider: "google", family: "gmailSend", description: "Create a plain-text Gmail draft. Nothing is sent until gmail_send_draft.", inputSchema: draftSchema, annotations: CREATES, args: draftArgs },
|
|
107
49
|
{ name: "gmail_send_draft", provider: "google", family: "gmailSend", description: "Send an existing Gmail draft by its draft id.", inputSchema: idSchema("draftId", "Draft id from gmail_create_draft."), annotations: SENDS, args: idArgs("draftId") },
|
|
108
|
-
|
|
109
|
-
{ name: "calendar_create_event", provider: "google", family: "calendar", description: "Create a Google Calendar event. Attendees receive an invitation.", inputSchema: createEventSchema(true), annotations: SENDS, args: createEventArgs },
|
|
50
|
+
...GOOGLE_CALENDAR_TOOLS,
|
|
110
51
|
{ name: "drive_search", provider: "google", family: "drive", description: "Search Google Drive files by words in their name or content.", inputSchema: searchSchema("Words to look for."), annotations: READS, args: searchArgs },
|
|
111
52
|
{ name: "drive_read", provider: "google", family: "drive", description: "Read a Google Drive file by id: text for Docs, Sheets (CSV), Slides, Office and text files; details and a link otherwise.", inputSchema: idSchema("fileId", "File id from drive_search."), annotations: READS, args: idArgs("fileId") },
|
|
112
53
|
{ name: "outlook_search", provider: "microsoft", family: "outlook", description: "Search Outlook mail by words (sender, subject, body). Returns message ids, senders, subjects, dates and previews.", inputSchema: searchSchema("Words to search for."), annotations: READS, args: searchArgs },
|
|
113
54
|
{ name: "outlook_read", provider: "microsoft", family: "outlook", description: "Read one Outlook message by id: headers and plain-text body.", inputSchema: idSchema("id", "Message id from outlook_search."), annotations: READS, args: idArgs("id") },
|
|
114
55
|
{ name: "outlook_create_draft", provider: "microsoft", family: "outlook", description: "Create a plain-text Outlook draft. Nothing is sent until outlook_send_draft.", inputSchema: draftSchema, annotations: CREATES, args: draftArgs },
|
|
115
56
|
{ name: "outlook_send_draft", provider: "microsoft", family: "outlook", description: "Send an existing Outlook draft by its id.", inputSchema: idSchema("draftId", "Draft id from outlook_create_draft."), annotations: SENDS, args: idArgs("draftId") },
|
|
116
|
-
|
|
117
|
-
{ name: "outlook_calendar_create_event", provider: "microsoft", family: "outlookCalendar", description: "Create an Outlook calendar event. Attendees receive an invitation.", inputSchema: createEventSchema(false), annotations: SENDS, args: createEventArgs.omit({ calendarId: true }) },
|
|
57
|
+
...OUTLOOK_CALENDAR_TOOLS,
|
|
118
58
|
{ name: "onedrive_search", provider: "microsoft", family: "onedrive", description: "Search OneDrive files by words in their name or content.", inputSchema: searchSchema("Words to look for."), annotations: READS, args: searchArgs },
|
|
119
59
|
{ name: "onedrive_read", provider: "microsoft", family: "onedrive", description: "Read a OneDrive file by id: text for small text and Office files, details and a link otherwise.", inputSchema: idSchema("fileId", "File id from onedrive_search."), annotations: READS, args: idArgs("fileId") },
|
|
120
60
|
];
|
|
@@ -129,11 +69,6 @@ export function accountToolList(providers) {
|
|
|
129
69
|
export function isReadOnlyAccountTool(name) {
|
|
130
70
|
return TOOL_BY_NAME.get(name)?.annotations.readOnlyHint === true;
|
|
131
71
|
}
|
|
132
|
-
export class ToolInputError extends Error {
|
|
133
|
-
}
|
|
134
|
-
function bounded(text, limit = MAX_RESULT_CHARS) {
|
|
135
|
-
return text.length <= limit ? text : `${text.slice(0, limit)}\n[truncated: ask for less]`;
|
|
136
|
-
}
|
|
137
72
|
function describeIssues(error) {
|
|
138
73
|
return error.issues.slice(0, 5).map((issue) => `${issue.path.join(".") || "arguments"}: ${issue.message}`).join("; ");
|
|
139
74
|
}
|
|
@@ -154,134 +89,9 @@ export function resolveAccount(provider, allowed, requested) {
|
|
|
154
89
|
return candidates[0];
|
|
155
90
|
throw new ToolInputError(`Pass account: this bot has more than one ${label} account (${candidates.map((account) => account.email).join(", ")}).`);
|
|
156
91
|
}
|
|
157
|
-
function addresses(value, field) {
|
|
158
|
-
if (value === undefined)
|
|
159
|
-
return [];
|
|
160
|
-
const list = (Array.isArray(value) ? value : value.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
161
|
-
if (list.length > 50)
|
|
162
|
-
throw new ToolInputError(`${field}: at most 50 addresses`);
|
|
163
|
-
for (const item of list) {
|
|
164
|
-
if (!EMAIL.test(item))
|
|
165
|
-
throw new ToolInputError(`${field}: "${item.slice(0, 80)}" is not an email address`);
|
|
166
|
-
}
|
|
167
|
-
return list;
|
|
168
|
-
}
|
|
169
|
-
class ProviderApiError extends Error {
|
|
170
|
-
status;
|
|
171
|
-
constructor(status, message) {
|
|
172
|
-
super(message);
|
|
173
|
-
this.status = status;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
async function readLimited(response, limit) {
|
|
177
|
-
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
178
|
-
if (declared > limit) {
|
|
179
|
-
await response.body?.cancel().catch(() => { });
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
if (!response.body)
|
|
183
|
-
return Buffer.alloc(0);
|
|
184
|
-
const reader = response.body.getReader();
|
|
185
|
-
const chunks = [];
|
|
186
|
-
let size = 0;
|
|
187
|
-
for (;;) {
|
|
188
|
-
const { done, value } = await reader.read();
|
|
189
|
-
if (done)
|
|
190
|
-
break;
|
|
191
|
-
size += value.byteLength;
|
|
192
|
-
if (size > limit) {
|
|
193
|
-
await reader.cancel().catch(() => { });
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
chunks.push(value);
|
|
197
|
-
}
|
|
198
|
-
return Buffer.concat(chunks);
|
|
199
|
-
}
|
|
200
|
-
function providerErrorMessage(body, status) {
|
|
201
|
-
const error = body?.error;
|
|
202
|
-
const message = typeof error === "string"
|
|
203
|
-
? error
|
|
204
|
-
: error && typeof error === "object" && typeof error.message === "string"
|
|
205
|
-
? error.message
|
|
206
|
-
: "";
|
|
207
|
-
return (message || `request failed with HTTP ${status}`).split(/\r?\n/)[0].slice(0, 300);
|
|
208
|
-
}
|
|
209
|
-
async function call(ctx, account, url, init) {
|
|
210
|
-
const fetcher = ctx.fetch ?? fetch;
|
|
211
|
-
const { expect, ...request } = init;
|
|
212
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
213
|
-
const token = await ctx.accessToken(account.id);
|
|
214
|
-
const headers = new Headers(request.headers);
|
|
215
|
-
headers.set("authorization", `Bearer ${token}`);
|
|
216
|
-
if (!headers.has("accept") && expect === "json")
|
|
217
|
-
headers.set("accept", "application/json");
|
|
218
|
-
const response = await fetcher(url, { ...request, headers, signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) });
|
|
219
|
-
if (response.status === 401 && attempt === 0) {
|
|
220
|
-
await response.body?.cancel().catch(() => { });
|
|
221
|
-
ctx.invalidate(account.id);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
if (!response.ok) {
|
|
225
|
-
const raw = await readLimited(response, 64 * 1024);
|
|
226
|
-
let body = null;
|
|
227
|
-
try {
|
|
228
|
-
body = raw ? JSON.parse(raw.toString("utf8")) : null;
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
body = null;
|
|
232
|
-
}
|
|
233
|
-
const message = providerErrorMessage(body, response.status);
|
|
234
|
-
if (response.status === 403)
|
|
235
|
-
throw new ProviderApiError(403, `${account.email} does not allow this: ${message}`);
|
|
236
|
-
if (response.status === 404)
|
|
237
|
-
throw new ProviderApiError(404, `Not found in ${account.email}: ${message}`);
|
|
238
|
-
throw new ProviderApiError(response.status, `${OAUTH_PROVIDERS[account.provider].label} refused the request for ${account.email}: ${message}`);
|
|
239
|
-
}
|
|
240
|
-
if (expect === "none") {
|
|
241
|
-
await response.body?.cancel().catch(() => { });
|
|
242
|
-
return undefined;
|
|
243
|
-
}
|
|
244
|
-
const bytes = await readLimited(response, expect === "json" ? MAX_JSON_BYTES : MAX_DOWNLOAD_BYTES);
|
|
245
|
-
if (expect === "bytes")
|
|
246
|
-
return bytes;
|
|
247
|
-
if (!bytes)
|
|
248
|
-
throw new ProviderApiError(502, "The response was too large");
|
|
249
|
-
try {
|
|
250
|
-
return JSON.parse(bytes.toString("utf8"));
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
throw new ProviderApiError(502, "The service returned an unreadable response");
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
throw new ProviderApiError(401, `${account.email} needs to sign in again: open Settings → Accounts and connect it again.`);
|
|
257
|
-
}
|
|
258
|
-
function parse(schema, value, what) {
|
|
259
|
-
const parsed = schema.safeParse(value);
|
|
260
|
-
if (!parsed.success)
|
|
261
|
-
throw new ProviderApiError(502, `Unexpected ${what} response`);
|
|
262
|
-
return parsed.data;
|
|
263
|
-
}
|
|
264
|
-
function json(value) {
|
|
265
|
-
return JSON.stringify(value, null, 1);
|
|
266
|
-
}
|
|
267
92
|
function decodeBase64Url(data) {
|
|
268
93
|
return data ? Buffer.from(data, "base64url").toString("utf8") : "";
|
|
269
94
|
}
|
|
270
|
-
function htmlToText(html) {
|
|
271
|
-
return html
|
|
272
|
-
.replace(/<(script|style)[\s\S]*?<\/\1>/gi, "")
|
|
273
|
-
.replace(/<br\s*\/?>/gi, "\n")
|
|
274
|
-
.replace(/<\/(p|div|tr|li|h\d)>/gi, "\n")
|
|
275
|
-
.replace(/<[^>]+>/g, "")
|
|
276
|
-
.replace(/ /g, " ")
|
|
277
|
-
.replace(/&/g, "&")
|
|
278
|
-
.replace(/</g, "<")
|
|
279
|
-
.replace(/>/g, ">")
|
|
280
|
-
.replace(/"/g, '"')
|
|
281
|
-
.replace(/'/g, "'")
|
|
282
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
283
|
-
.trim();
|
|
284
|
-
}
|
|
285
95
|
function encodeHeader(value) {
|
|
286
96
|
return /^[\x20-\x7e]*$/.test(value) ? value : `=?UTF-8?B?${Buffer.from(value, "utf8").toString("base64")}?=`;
|
|
287
97
|
}
|
|
@@ -317,7 +127,6 @@ function readable(name, mimeType) {
|
|
|
317
127
|
}
|
|
318
128
|
// ── Google ────────────────────────────────────────────────────────────────
|
|
319
129
|
const GMAIL = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
320
|
-
const CALENDAR = "https://www.googleapis.com/calendar/v3";
|
|
321
130
|
const DRIVE = "https://www.googleapis.com/drive/v3";
|
|
322
131
|
const gmailPartSchema = z.lazy(() => z.object({
|
|
323
132
|
mimeType: z.string().optional(),
|
|
@@ -405,69 +214,6 @@ async function gmailSendDraft(ctx, account, args) {
|
|
|
405
214
|
const sent = parse(z.object({ id: z.string(), threadId: z.string().optional() }), await call(ctx, account, `${GMAIL}/drafts/send`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: args.draftId }), expect: "json" }), "Gmail send");
|
|
406
215
|
return json({ account: account.email, sent: true, messageId: sent.id, threadId: sent.threadId });
|
|
407
216
|
}
|
|
408
|
-
const googleEventSchema = z.object({
|
|
409
|
-
id: z.string(),
|
|
410
|
-
summary: z.string().optional(),
|
|
411
|
-
description: z.string().optional(),
|
|
412
|
-
location: z.string().optional(),
|
|
413
|
-
status: z.string().optional(),
|
|
414
|
-
htmlLink: z.string().optional(),
|
|
415
|
-
start: z.object({ dateTime: z.string().optional(), date: z.string().optional(), timeZone: z.string().optional() }).optional(),
|
|
416
|
-
end: z.object({ dateTime: z.string().optional(), date: z.string().optional(), timeZone: z.string().optional() }).optional(),
|
|
417
|
-
organizer: z.object({ email: z.string().optional() }).optional(),
|
|
418
|
-
attendees: z.array(z.object({ email: z.string().optional(), responseStatus: z.string().optional() })).optional(),
|
|
419
|
-
});
|
|
420
|
-
function googleEvent(event) {
|
|
421
|
-
return {
|
|
422
|
-
id: event.id,
|
|
423
|
-
summary: event.summary,
|
|
424
|
-
start: event.start?.dateTime ?? event.start?.date,
|
|
425
|
-
end: event.end?.dateTime ?? event.end?.date,
|
|
426
|
-
location: event.location,
|
|
427
|
-
organizer: event.organizer?.email,
|
|
428
|
-
attendees: event.attendees?.map((attendee) => `${attendee.email ?? ""}${attendee.responseStatus ? ` (${attendee.responseStatus})` : ""}`),
|
|
429
|
-
description: event.description ? bounded(event.description, 2_000) : undefined,
|
|
430
|
-
link: event.htmlLink,
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
async function calendarListEvents(ctx, account, args) {
|
|
434
|
-
const calendarId = encodeURIComponent(args.calendarId ?? "primary");
|
|
435
|
-
const query = new URLSearchParams({ timeMin: args.timeMin, timeMax: args.timeMax, singleEvents: "true", orderBy: "startTime", maxResults: String(args.max ?? 25) });
|
|
436
|
-
const list = parse(z.object({ items: z.array(googleEventSchema).optional(), timeZone: z.string().optional() }), await call(ctx, account, `${CALENDAR}/calendars/${calendarId}/events?${query}`, { expect: "json" }), "Calendar");
|
|
437
|
-
return json({ account: account.email, timeZone: list.timeZone, events: (list.items ?? []).map(googleEvent) });
|
|
438
|
-
}
|
|
439
|
-
function eventTime(value, timeZone, field) {
|
|
440
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
441
|
-
return { allDay: true, date: value };
|
|
442
|
-
const hasOffset = /(?:Z|[+-]\d{2}:\d{2})$/.test(value);
|
|
443
|
-
if (!hasOffset && !timeZone)
|
|
444
|
-
throw new ToolInputError(`${field}: add a UTC offset (e.g. 2026-10-01T15:00:00-07:00) or pass timeZone`);
|
|
445
|
-
return { allDay: false, dateTime: value, hasOffset };
|
|
446
|
-
}
|
|
447
|
-
async function calendarCreateEvent(ctx, account, args) {
|
|
448
|
-
const start = eventTime(args.start, args.timeZone, "start");
|
|
449
|
-
const end = eventTime(args.end, args.timeZone, "end");
|
|
450
|
-
if (start.allDay !== end.allDay)
|
|
451
|
-
throw new ToolInputError("start and end must both be dates or both be date-times");
|
|
452
|
-
const attendees = addresses(args.attendees, "attendees");
|
|
453
|
-
const time = (value) => value.allDay
|
|
454
|
-
? { date: value.date }
|
|
455
|
-
: { dateTime: value.dateTime, ...(args.timeZone ? { timeZone: args.timeZone } : {}) };
|
|
456
|
-
const body = {
|
|
457
|
-
summary: args.summary,
|
|
458
|
-
...(args.description ? { description: args.description } : {}),
|
|
459
|
-
...(args.location ? { location: args.location } : {}),
|
|
460
|
-
start: time(start),
|
|
461
|
-
end: time(end),
|
|
462
|
-
...(attendees.length ? { attendees: attendees.map((email) => ({ email })) } : {}),
|
|
463
|
-
};
|
|
464
|
-
const calendarId = encodeURIComponent(args.calendarId ?? "primary");
|
|
465
|
-
const query = new URLSearchParams({ sendUpdates: attendees.length ? "all" : "none" });
|
|
466
|
-
const created = parse(googleEventSchema, await call(ctx, account, `${CALENDAR}/calendars/${calendarId}/events?${query}`, {
|
|
467
|
-
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), expect: "json",
|
|
468
|
-
}), "Calendar");
|
|
469
|
-
return json({ account: account.email, created: googleEvent(created) });
|
|
470
|
-
}
|
|
471
217
|
const driveFileSchema = z.object({
|
|
472
218
|
id: z.string(),
|
|
473
219
|
name: z.string().optional(),
|
|
@@ -516,8 +262,6 @@ async function driveRead(ctx, account, args) {
|
|
|
516
262
|
: { ...details, text: bounded(text, MAX_FILE_TEXT_CHARS) });
|
|
517
263
|
}
|
|
518
264
|
// ── Microsoft Graph ───────────────────────────────────────────────────────
|
|
519
|
-
const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
520
|
-
const graphRecipient = z.object({ emailAddress: z.object({ address: z.string().optional(), name: z.string().optional() }).optional() });
|
|
521
265
|
const graphMessageSchema = z.object({
|
|
522
266
|
id: z.string(),
|
|
523
267
|
conversationId: z.string().optional(),
|
|
@@ -531,11 +275,6 @@ const graphMessageSchema = z.object({
|
|
|
531
275
|
hasAttachments: z.boolean().optional(),
|
|
532
276
|
body: z.object({ contentType: z.string().optional(), content: z.string().optional() }).optional(),
|
|
533
277
|
});
|
|
534
|
-
function recipient(value) {
|
|
535
|
-
const address = value?.emailAddress?.address;
|
|
536
|
-
const name = value?.emailAddress?.name;
|
|
537
|
-
return address ? (name && name !== address ? `${name} <${address}>` : address) : undefined;
|
|
538
|
-
}
|
|
539
278
|
async function outlookSearch(ctx, account, args) {
|
|
540
279
|
const query = new URLSearchParams({
|
|
541
280
|
$search: `"${args.q.replace(/["\\]/g, " ")}"`,
|
|
@@ -598,83 +337,6 @@ async function outlookSendDraft(ctx, account, args) {
|
|
|
598
337
|
await call(ctx, account, `${GRAPH}/me/messages/${encodeURIComponent(args.draftId)}/send`, { method: "POST", expect: "none" });
|
|
599
338
|
return json({ account: account.email, sent: true, draftId: args.draftId });
|
|
600
339
|
}
|
|
601
|
-
const graphTime = z.object({ dateTime: z.string().optional(), timeZone: z.string().optional() });
|
|
602
|
-
const graphEventSchema = z.object({
|
|
603
|
-
id: z.string(),
|
|
604
|
-
subject: z.string().nullable().optional(),
|
|
605
|
-
start: graphTime.optional(),
|
|
606
|
-
end: graphTime.optional(),
|
|
607
|
-
isAllDay: z.boolean().optional(),
|
|
608
|
-
location: z.object({ displayName: z.string().optional() }).nullable().optional(),
|
|
609
|
-
organizer: graphRecipient.nullable().optional(),
|
|
610
|
-
attendees: z.array(graphRecipient.extend({ status: z.object({ response: z.string().optional() }).optional() })).optional(),
|
|
611
|
-
webLink: z.string().optional(),
|
|
612
|
-
bodyPreview: z.string().optional(),
|
|
613
|
-
});
|
|
614
|
-
function graphEvent(event) {
|
|
615
|
-
return {
|
|
616
|
-
id: event.id,
|
|
617
|
-
summary: event.subject,
|
|
618
|
-
start: event.start?.dateTime ? `${event.start.dateTime}${event.start.timeZone === "UTC" ? "Z" : ` (${event.start.timeZone ?? ""})`}` : undefined,
|
|
619
|
-
end: event.end?.dateTime ? `${event.end.dateTime}${event.end.timeZone === "UTC" ? "Z" : ` (${event.end.timeZone ?? ""})`}` : undefined,
|
|
620
|
-
allDay: event.isAllDay,
|
|
621
|
-
location: event.location?.displayName || undefined,
|
|
622
|
-
organizer: recipient(event.organizer),
|
|
623
|
-
attendees: event.attendees?.map((attendee) => `${recipient(attendee) ?? ""}${attendee.status?.response ? ` (${attendee.status.response})` : ""}`),
|
|
624
|
-
preview: event.bodyPreview ? bounded(event.bodyPreview, 1_000) : undefined,
|
|
625
|
-
link: event.webLink,
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
async function outlookListEvents(ctx, account, args) {
|
|
629
|
-
const query = new URLSearchParams({
|
|
630
|
-
startDateTime: new Date(args.timeMin).toISOString(),
|
|
631
|
-
endDateTime: new Date(args.timeMax).toISOString(),
|
|
632
|
-
$top: String(args.max ?? 25),
|
|
633
|
-
$orderby: "start/dateTime",
|
|
634
|
-
$select: "id,subject,start,end,isAllDay,location,organizer,attendees,webLink,bodyPreview",
|
|
635
|
-
});
|
|
636
|
-
const list = parse(z.object({ value: z.array(graphEventSchema) }), await call(ctx, account, `${GRAPH}/me/calendarView?${query}`, {
|
|
637
|
-
headers: { prefer: 'outlook.timezone="UTC"' }, expect: "json",
|
|
638
|
-
}), "Outlook calendar");
|
|
639
|
-
return json({ account: account.email, timeZone: "UTC", events: list.value.map(graphEvent) });
|
|
640
|
-
}
|
|
641
|
-
/** Graph wants a wall-clock time plus a zone: a time with an offset is
|
|
642
|
-
* converted to UTC, one without keeps the zone the bot named. */
|
|
643
|
-
export function graphDateTime(value, timeZone, field) {
|
|
644
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
645
|
-
return { dateTime: `${value}T00:00:00`, timeZone: timeZone ?? "UTC", allDay: true };
|
|
646
|
-
if (/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
|
|
647
|
-
const parsed = Date.parse(value);
|
|
648
|
-
if (!Number.isFinite(parsed))
|
|
649
|
-
throw new ToolInputError(`${field}: not a valid date-time`);
|
|
650
|
-
return { dateTime: new Date(parsed).toISOString().replace(/\.\d{3}Z$/, ""), timeZone: "UTC", allDay: false };
|
|
651
|
-
}
|
|
652
|
-
if (!timeZone)
|
|
653
|
-
throw new ToolInputError(`${field}: add a UTC offset (e.g. 2026-10-01T15:00:00-07:00) or pass timeZone`);
|
|
654
|
-
return { dateTime: value, timeZone, allDay: false };
|
|
655
|
-
}
|
|
656
|
-
async function outlookCreateEvent(ctx, account, args) {
|
|
657
|
-
const start = graphDateTime(args.start, args.timeZone, "start");
|
|
658
|
-
const end = graphDateTime(args.end, args.timeZone, "end");
|
|
659
|
-
if (start.allDay !== end.allDay)
|
|
660
|
-
throw new ToolInputError("start and end must both be dates or both be date-times");
|
|
661
|
-
const attendees = addresses(args.attendees, "attendees");
|
|
662
|
-
const created = parse(graphEventSchema, await call(ctx, account, `${GRAPH}/me/events`, {
|
|
663
|
-
method: "POST",
|
|
664
|
-
headers: { "content-type": "application/json", prefer: 'outlook.timezone="UTC"' },
|
|
665
|
-
body: JSON.stringify({
|
|
666
|
-
subject: args.summary,
|
|
667
|
-
...(args.description ? { body: { contentType: "Text", content: args.description } } : {}),
|
|
668
|
-
start: { dateTime: start.dateTime, timeZone: start.timeZone },
|
|
669
|
-
end: { dateTime: end.dateTime, timeZone: end.timeZone },
|
|
670
|
-
...(start.allDay ? { isAllDay: true } : {}),
|
|
671
|
-
...(args.location ? { location: { displayName: args.location } } : {}),
|
|
672
|
-
...(attendees.length ? { attendees: attendees.map((address) => ({ emailAddress: { address }, type: "required" })) } : {}),
|
|
673
|
-
}),
|
|
674
|
-
expect: "json",
|
|
675
|
-
}), "Outlook calendar");
|
|
676
|
-
return json({ account: account.email, created: graphEvent(created) });
|
|
677
|
-
}
|
|
678
340
|
const driveItemSchema = z.object({
|
|
679
341
|
id: z.string(),
|
|
680
342
|
name: z.string().optional(),
|
|
@@ -713,16 +375,13 @@ const RUNNERS = {
|
|
|
713
375
|
gmail_read: gmailRead,
|
|
714
376
|
gmail_create_draft: gmailCreateDraft,
|
|
715
377
|
gmail_send_draft: gmailSendDraft,
|
|
716
|
-
|
|
717
|
-
calendar_create_event: calendarCreateEvent,
|
|
378
|
+
...CALENDAR_RUNNERS,
|
|
718
379
|
drive_search: driveSearch,
|
|
719
380
|
drive_read: driveRead,
|
|
720
381
|
outlook_search: outlookSearch,
|
|
721
382
|
outlook_read: outlookRead,
|
|
722
383
|
outlook_create_draft: outlookCreateDraft,
|
|
723
384
|
outlook_send_draft: outlookSendDraft,
|
|
724
|
-
outlook_calendar_list_events: outlookListEvents,
|
|
725
|
-
outlook_calendar_create_event: outlookCreateEvent,
|
|
726
385
|
onedrive_search: onedriveSearch,
|
|
727
386
|
onedrive_read: onedriveRead,
|
|
728
387
|
};
|