botinabox 0.4.0 → 0.5.0
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/CHANGELOG.md +18 -0
- package/dist/connector-DDahQw-2.d.ts +63 -0
- package/dist/connectors/google/index.d.ts +190 -0
- package/dist/connectors/google/index.js +487 -0
- package/dist/index.d.ts +83 -1
- package/dist/index.js +240 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.5.0] — 2026-04-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Connector interface** — Generic `Connector<T>` abstraction for external service integrations (Gmail, Calendar, Trello, Jira, Salesforce, etc.). Pull-based `sync()` returns typed records; optional `push()` writes back. Connectors produce data — consumers decide where to store it. New `connectors` config key in `BotConfig`.
|
|
14
|
+
- **Google connectors** — `GoogleGmailConnector` and `GoogleCalendarConnector` implementing `Connector<EmailRecord>` and `Connector<CalendarEventRecord>`. Incremental sync (Gmail historyId, Calendar syncToken), full sync with pagination, email sending via `push()`. OAuth2 helpers with callback-based token persistence. Exported from `botinabox/google`. `googleapis` as optional peer dependency.
|
|
15
|
+
- **Scheduler** — Database-backed `Scheduler` class with `schedules` core table. Supports recurring (cron expressions via `cron-parser`) and one-time schedules. Hook-based actions: when a schedule fires, it emits the configured action as a hook event. Methods: `register()`, `update()`, `unregister()`, `list()`, `tick()`.
|
|
16
|
+
- **`schedules` core table** — id, name, type (recurring/one_time), cron, run_at, timezone, enabled, action, action_config, last_fired_at, next_fire_at.
|
|
17
|
+
|
|
18
|
+
### Deprecated
|
|
19
|
+
|
|
20
|
+
- **HeartbeatScheduler** — Replaced by `Scheduler`. HeartbeatScheduler uses in-memory `setInterval` which loses state on restart. Kept for backward compatibility but marked `@deprecated`.
|
|
21
|
+
|
|
22
|
+
### Dependencies
|
|
23
|
+
|
|
24
|
+
- Added `cron-parser` ^4.9.0.
|
|
25
|
+
- Added `googleapis` >=140.0.0 as optional peer dependency.
|
|
26
|
+
|
|
9
27
|
## [0.3.0] — 2026-04-03
|
|
10
28
|
|
|
11
29
|
### Added
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Connector types — generic external service integrations. */
|
|
2
|
+
interface ConnectorMeta {
|
|
3
|
+
displayName: string;
|
|
4
|
+
/** Provider identifier, e.g. "google", "trello", "jira", "salesforce" */
|
|
5
|
+
provider: string;
|
|
6
|
+
/** Data type this connector handles, e.g. "email", "calendar", "board", "crm" */
|
|
7
|
+
dataType: string;
|
|
8
|
+
}
|
|
9
|
+
interface SyncOptions {
|
|
10
|
+
/** Only sync records after this ISO 8601 timestamp */
|
|
11
|
+
since?: string;
|
|
12
|
+
/** Provider-specific incremental sync token */
|
|
13
|
+
cursor?: string;
|
|
14
|
+
/** Maximum number of records to fetch */
|
|
15
|
+
limit?: number;
|
|
16
|
+
/** Provider-specific query filters */
|
|
17
|
+
filters?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
interface SyncResult<T = Record<string, unknown>> {
|
|
20
|
+
/** Typed records produced by the connector — consumer decides where to store */
|
|
21
|
+
records: T[];
|
|
22
|
+
/** Next incremental sync token (persist for future calls) */
|
|
23
|
+
cursor?: string;
|
|
24
|
+
/** Whether more records are available (pagination) */
|
|
25
|
+
hasMore: boolean;
|
|
26
|
+
/** Errors encountered during sync (non-fatal per-record failures) */
|
|
27
|
+
errors: Array<{
|
|
28
|
+
id?: string;
|
|
29
|
+
error: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
interface PushResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
externalId?: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
type ConnectorConfig = Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Generic connector interface for external service integrations.
|
|
40
|
+
*
|
|
41
|
+
* Connectors pull and optionally push data to/from external services
|
|
42
|
+
* (Gmail, Calendar, Trello, Jira, Salesforce, etc.). They produce
|
|
43
|
+
* typed records — the consuming application decides where to store them.
|
|
44
|
+
*
|
|
45
|
+
* @typeParam T - The record type this connector produces/consumes.
|
|
46
|
+
*/
|
|
47
|
+
interface Connector<T = Record<string, unknown>> {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly meta: ConnectorMeta;
|
|
50
|
+
connect(config: ConnectorConfig): Promise<void>;
|
|
51
|
+
disconnect(): Promise<void>;
|
|
52
|
+
healthCheck(): Promise<{
|
|
53
|
+
ok: boolean;
|
|
54
|
+
account?: string;
|
|
55
|
+
error?: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** Pull records from external source */
|
|
58
|
+
sync(options?: SyncOptions): Promise<SyncResult<T>>;
|
|
59
|
+
/** Push a record to external source (optional) */
|
|
60
|
+
push?(payload: T): Promise<PushResult>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type { ConnectorConfig as C, PushResult as P, SyncOptions as S, Connector as a, ConnectorMeta as b, SyncResult as c };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { C as ConnectorConfig, a as Connector, b as ConnectorMeta, S as SyncOptions, c as SyncResult, P as PushResult } from '../../connector-DDahQw-2.js';
|
|
2
|
+
|
|
3
|
+
/** Google connector types. */
|
|
4
|
+
|
|
5
|
+
interface GoogleOAuthConfig {
|
|
6
|
+
clientId: string;
|
|
7
|
+
clientSecret: string;
|
|
8
|
+
redirectUri: string;
|
|
9
|
+
}
|
|
10
|
+
interface GoogleTokens {
|
|
11
|
+
access_token: string;
|
|
12
|
+
refresh_token?: string;
|
|
13
|
+
expiry_date?: number;
|
|
14
|
+
token_type: string;
|
|
15
|
+
}
|
|
16
|
+
interface GoogleConnectorConfig extends ConnectorConfig {
|
|
17
|
+
/** Google account email */
|
|
18
|
+
account: string;
|
|
19
|
+
oauth: GoogleOAuthConfig;
|
|
20
|
+
scopes: string[];
|
|
21
|
+
}
|
|
22
|
+
interface EmailAddress {
|
|
23
|
+
name?: string;
|
|
24
|
+
email: string;
|
|
25
|
+
}
|
|
26
|
+
interface EmailRecord {
|
|
27
|
+
gmailId: string;
|
|
28
|
+
threadId: string;
|
|
29
|
+
account: string;
|
|
30
|
+
subject: string;
|
|
31
|
+
from: EmailAddress;
|
|
32
|
+
to: EmailAddress[];
|
|
33
|
+
cc: EmailAddress[];
|
|
34
|
+
bcc: EmailAddress[];
|
|
35
|
+
/** ISO 8601 timestamp */
|
|
36
|
+
date: string;
|
|
37
|
+
snippet: string;
|
|
38
|
+
body?: string;
|
|
39
|
+
labels: string[];
|
|
40
|
+
isRead: boolean;
|
|
41
|
+
}
|
|
42
|
+
interface CalendarAttendee {
|
|
43
|
+
email: string;
|
|
44
|
+
displayName?: string;
|
|
45
|
+
responseStatus?: string;
|
|
46
|
+
}
|
|
47
|
+
interface CalendarEventRecord {
|
|
48
|
+
googleEventId: string;
|
|
49
|
+
calendarId: string;
|
|
50
|
+
account: string;
|
|
51
|
+
title: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
location?: string;
|
|
54
|
+
/** ISO 8601 timestamp */
|
|
55
|
+
startAt: string;
|
|
56
|
+
/** ISO 8601 timestamp */
|
|
57
|
+
endAt: string;
|
|
58
|
+
allDay: boolean;
|
|
59
|
+
timezone?: string;
|
|
60
|
+
status: string;
|
|
61
|
+
organizerEmail: string;
|
|
62
|
+
attendees: CalendarAttendee[];
|
|
63
|
+
recurrence?: string[];
|
|
64
|
+
htmlLink?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Google OAuth2 helpers.
|
|
69
|
+
*
|
|
70
|
+
* Uses dynamic `import('googleapis')` so the package is only required
|
|
71
|
+
* at runtime by consumers who actually use the Google connectors.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
type OAuth2Client = any;
|
|
75
|
+
/**
|
|
76
|
+
* Create a Google OAuth2 client from app credentials.
|
|
77
|
+
*/
|
|
78
|
+
declare function createOAuth2Client(config: GoogleOAuthConfig): Promise<OAuth2Client>;
|
|
79
|
+
/**
|
|
80
|
+
* Generate the consent screen URL for the given scopes.
|
|
81
|
+
*/
|
|
82
|
+
declare function getAuthUrl(client: OAuth2Client, scopes: string[]): string;
|
|
83
|
+
/**
|
|
84
|
+
* Exchange an authorization code for tokens.
|
|
85
|
+
*/
|
|
86
|
+
declare function exchangeCode(client: OAuth2Client, code: string): Promise<GoogleTokens>;
|
|
87
|
+
/**
|
|
88
|
+
* Load persisted tokens via a generic getter callback.
|
|
89
|
+
*
|
|
90
|
+
* @param getter Reads a string value by key (e.g. SecretStore.get)
|
|
91
|
+
* @param accountKey Unique key prefix for this account
|
|
92
|
+
*/
|
|
93
|
+
declare function loadTokens(getter: (key: string) => Promise<string | null>, accountKey: string): Promise<GoogleTokens | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Persist tokens via a generic setter callback.
|
|
96
|
+
*
|
|
97
|
+
* @param setter Writes a string value by key (e.g. SecretStore.set)
|
|
98
|
+
* @param accountKey Unique key prefix for this account
|
|
99
|
+
* @param tokens Tokens to persist
|
|
100
|
+
*/
|
|
101
|
+
declare function saveTokens(setter: (key: string, value: string) => Promise<void>, accountKey: string, tokens: GoogleTokens): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Refresh the access token if it has expired (or is about to within 60 s).
|
|
104
|
+
*
|
|
105
|
+
* If the token was refreshed and a `saver` callback is provided, the new
|
|
106
|
+
* tokens are persisted automatically.
|
|
107
|
+
*/
|
|
108
|
+
declare function refreshIfNeeded(client: OAuth2Client, tokens: GoogleTokens, saver?: (tokens: GoogleTokens) => Promise<void>): Promise<GoogleTokens>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Gmail connector — pulls email metadata and optionally sends mail.
|
|
112
|
+
*
|
|
113
|
+
* Produces `EmailRecord` objects. Does NOT write to any database table;
|
|
114
|
+
* the consuming application decides how to store records.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
interface GmailConnectorOpts {
|
|
118
|
+
/** Load persisted tokens for a given account key. */
|
|
119
|
+
tokenLoader: (key: string) => Promise<string | null>;
|
|
120
|
+
/** Persist tokens for a given account key. */
|
|
121
|
+
tokenSaver: (key: string, value: string) => Promise<void>;
|
|
122
|
+
}
|
|
123
|
+
declare class GoogleGmailConnector implements Connector<EmailRecord> {
|
|
124
|
+
readonly id = "google-gmail";
|
|
125
|
+
readonly meta: ConnectorMeta;
|
|
126
|
+
private tokenLoader;
|
|
127
|
+
private tokenSaver;
|
|
128
|
+
private client;
|
|
129
|
+
private config;
|
|
130
|
+
private tokens;
|
|
131
|
+
private gmail;
|
|
132
|
+
constructor(opts: GmailConnectorOpts);
|
|
133
|
+
connect(config: GoogleConnectorConfig): Promise<void>;
|
|
134
|
+
disconnect(): Promise<void>;
|
|
135
|
+
healthCheck(): Promise<{
|
|
136
|
+
ok: boolean;
|
|
137
|
+
account?: string;
|
|
138
|
+
error?: string;
|
|
139
|
+
}>;
|
|
140
|
+
sync(options?: SyncOptions): Promise<SyncResult<EmailRecord>>;
|
|
141
|
+
/** Incremental sync using Gmail history API. */
|
|
142
|
+
private syncIncremental;
|
|
143
|
+
/** Full sync — list messages and fetch each one. */
|
|
144
|
+
private syncFull;
|
|
145
|
+
push(payload: EmailRecord): Promise<PushResult>;
|
|
146
|
+
private ensureConnected;
|
|
147
|
+
/** Fetch a single message by ID and parse into an EmailRecord. */
|
|
148
|
+
private fetchMessage;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Google Calendar connector — pulls calendar events.
|
|
153
|
+
*
|
|
154
|
+
* Produces `CalendarEventRecord` objects. Does NOT write to any database
|
|
155
|
+
* table; the consuming application decides how to store records.
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
interface CalendarConnectorOpts {
|
|
159
|
+
/** Load persisted tokens for a given account key. */
|
|
160
|
+
tokenLoader: (key: string) => Promise<string | null>;
|
|
161
|
+
/** Persist tokens for a given account key. */
|
|
162
|
+
tokenSaver: (key: string, value: string) => Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
declare class GoogleCalendarConnector implements Connector<CalendarEventRecord> {
|
|
165
|
+
readonly id = "google-calendar";
|
|
166
|
+
readonly meta: ConnectorMeta;
|
|
167
|
+
private tokenLoader;
|
|
168
|
+
private tokenSaver;
|
|
169
|
+
private client;
|
|
170
|
+
private config;
|
|
171
|
+
private tokens;
|
|
172
|
+
private calendar;
|
|
173
|
+
constructor(opts: CalendarConnectorOpts);
|
|
174
|
+
connect(config: GoogleConnectorConfig): Promise<void>;
|
|
175
|
+
disconnect(): Promise<void>;
|
|
176
|
+
healthCheck(): Promise<{
|
|
177
|
+
ok: boolean;
|
|
178
|
+
account?: string;
|
|
179
|
+
error?: string;
|
|
180
|
+
}>;
|
|
181
|
+
sync(options?: SyncOptions): Promise<SyncResult<CalendarEventRecord>>;
|
|
182
|
+
/** Incremental sync using Calendar syncToken. */
|
|
183
|
+
private syncIncremental;
|
|
184
|
+
/** Full sync using timeMin. */
|
|
185
|
+
private syncFull;
|
|
186
|
+
private ensureConnected;
|
|
187
|
+
private mapEvent;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { type CalendarAttendee, type CalendarConnectorOpts, type CalendarEventRecord, type EmailAddress, type EmailRecord, type GmailConnectorOpts, GoogleCalendarConnector, type GoogleConnectorConfig, GoogleGmailConnector, type GoogleOAuthConfig, type GoogleTokens, createOAuth2Client, exchangeCode, getAuthUrl, loadTokens, refreshIfNeeded, saveTokens };
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// src/connectors/google/oauth.ts
|
|
2
|
+
var _google;
|
|
3
|
+
async function getGoogle() {
|
|
4
|
+
if (!_google) {
|
|
5
|
+
try {
|
|
6
|
+
const mod = await import("googleapis");
|
|
7
|
+
_google = mod.google;
|
|
8
|
+
} catch {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"googleapis is required for Google connectors. Install it: npm install googleapis"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return _google;
|
|
15
|
+
}
|
|
16
|
+
async function createOAuth2Client(config) {
|
|
17
|
+
const google = await getGoogle();
|
|
18
|
+
return new google.auth.OAuth2(
|
|
19
|
+
config.clientId,
|
|
20
|
+
config.clientSecret,
|
|
21
|
+
config.redirectUri
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function getAuthUrl(client, scopes) {
|
|
25
|
+
return client.generateAuthUrl({
|
|
26
|
+
access_type: "offline",
|
|
27
|
+
prompt: "consent",
|
|
28
|
+
scope: scopes
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function exchangeCode(client, code) {
|
|
32
|
+
const { tokens } = await client.getToken(code);
|
|
33
|
+
return tokens;
|
|
34
|
+
}
|
|
35
|
+
async function loadTokens(getter, accountKey) {
|
|
36
|
+
const raw = await getter(`google_tokens:${accountKey}`);
|
|
37
|
+
if (!raw) return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function saveTokens(setter, accountKey, tokens) {
|
|
45
|
+
await setter(`google_tokens:${accountKey}`, JSON.stringify(tokens));
|
|
46
|
+
}
|
|
47
|
+
async function refreshIfNeeded(client, tokens, saver) {
|
|
48
|
+
const buffer = 6e4;
|
|
49
|
+
const isExpired = tokens.expiry_date != null && Date.now() >= tokens.expiry_date - buffer;
|
|
50
|
+
if (!isExpired) return tokens;
|
|
51
|
+
client.setCredentials(tokens);
|
|
52
|
+
const { credentials } = await client.refreshAccessToken();
|
|
53
|
+
const refreshed = {
|
|
54
|
+
access_token: credentials.access_token,
|
|
55
|
+
refresh_token: credentials.refresh_token ?? tokens.refresh_token,
|
|
56
|
+
expiry_date: credentials.expiry_date ?? void 0,
|
|
57
|
+
token_type: credentials.token_type ?? "Bearer"
|
|
58
|
+
};
|
|
59
|
+
if (saver) {
|
|
60
|
+
await saver(refreshed);
|
|
61
|
+
}
|
|
62
|
+
return refreshed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/connectors/google/gmail-connector.ts
|
|
66
|
+
var GoogleGmailConnector = class {
|
|
67
|
+
id = "google-gmail";
|
|
68
|
+
meta = {
|
|
69
|
+
displayName: "Google Gmail",
|
|
70
|
+
provider: "google",
|
|
71
|
+
dataType: "email"
|
|
72
|
+
};
|
|
73
|
+
tokenLoader;
|
|
74
|
+
tokenSaver;
|
|
75
|
+
client = null;
|
|
76
|
+
config = null;
|
|
77
|
+
tokens = null;
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
gmail = null;
|
|
80
|
+
constructor(opts) {
|
|
81
|
+
this.tokenLoader = opts.tokenLoader;
|
|
82
|
+
this.tokenSaver = opts.tokenSaver;
|
|
83
|
+
}
|
|
84
|
+
// ── Lifecycle ──────────────────────────────────────────────────
|
|
85
|
+
async connect(config) {
|
|
86
|
+
this.config = config;
|
|
87
|
+
this.client = await createOAuth2Client(config.oauth);
|
|
88
|
+
this.tokens = await loadTokens(this.tokenLoader, config.account);
|
|
89
|
+
if (!this.tokens) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`No stored tokens for account ${config.account}. Complete the OAuth flow first.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
this.tokens = await refreshIfNeeded(
|
|
95
|
+
this.client,
|
|
96
|
+
this.tokens,
|
|
97
|
+
async (t) => saveTokens(this.tokenSaver, config.account, t)
|
|
98
|
+
);
|
|
99
|
+
this.client.setCredentials(this.tokens);
|
|
100
|
+
const { google } = await import("googleapis");
|
|
101
|
+
this.gmail = google.gmail({ version: "v1", auth: this.client });
|
|
102
|
+
}
|
|
103
|
+
async disconnect() {
|
|
104
|
+
this.client = null;
|
|
105
|
+
this.gmail = null;
|
|
106
|
+
this.tokens = null;
|
|
107
|
+
this.config = null;
|
|
108
|
+
}
|
|
109
|
+
async healthCheck() {
|
|
110
|
+
try {
|
|
111
|
+
this.ensureConnected();
|
|
112
|
+
const res = await this.gmail.users.getProfile({ userId: "me" });
|
|
113
|
+
return { ok: true, account: res.data.emailAddress };
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { ok: false, error: errorMessage(err) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ── Sync ───────────────────────────────────────────────────────
|
|
119
|
+
async sync(options) {
|
|
120
|
+
this.ensureConnected();
|
|
121
|
+
if (options?.cursor) {
|
|
122
|
+
return this.syncIncremental(options.cursor, options.limit);
|
|
123
|
+
}
|
|
124
|
+
return this.syncFull(options);
|
|
125
|
+
}
|
|
126
|
+
/** Incremental sync using Gmail history API. */
|
|
127
|
+
async syncIncremental(startHistoryId, limit) {
|
|
128
|
+
const records = [];
|
|
129
|
+
const errors = [];
|
|
130
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
131
|
+
let pageToken;
|
|
132
|
+
let latestHistoryId = startHistoryId;
|
|
133
|
+
do {
|
|
134
|
+
const res = await this.gmail.users.history.list({
|
|
135
|
+
userId: "me",
|
|
136
|
+
startHistoryId,
|
|
137
|
+
historyTypes: ["messageAdded"],
|
|
138
|
+
...pageToken ? { pageToken } : {}
|
|
139
|
+
});
|
|
140
|
+
latestHistoryId = res.data.historyId ?? latestHistoryId;
|
|
141
|
+
const histories = res.data.history ?? [];
|
|
142
|
+
for (const h of histories) {
|
|
143
|
+
for (const added of h.messagesAdded ?? []) {
|
|
144
|
+
const msgId = added.message?.id;
|
|
145
|
+
if (!msgId || seenIds.has(msgId)) continue;
|
|
146
|
+
seenIds.add(msgId);
|
|
147
|
+
try {
|
|
148
|
+
const record = await this.fetchMessage(msgId);
|
|
149
|
+
records.push(record);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
errors.push({ id: msgId, error: errorMessage(err) });
|
|
152
|
+
}
|
|
153
|
+
if (limit && records.length >= limit) {
|
|
154
|
+
return { records, cursor: latestHistoryId, hasMore: true, errors };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
pageToken = res.data.nextPageToken ?? void 0;
|
|
159
|
+
} while (pageToken);
|
|
160
|
+
return { records, cursor: latestHistoryId, hasMore: false, errors };
|
|
161
|
+
}
|
|
162
|
+
/** Full sync — list messages and fetch each one. */
|
|
163
|
+
async syncFull(options) {
|
|
164
|
+
const records = [];
|
|
165
|
+
const errors = [];
|
|
166
|
+
const maxResults = options?.limit ?? 100;
|
|
167
|
+
let query = "";
|
|
168
|
+
if (options?.since) {
|
|
169
|
+
const epoch = Math.floor(new Date(options.since).getTime() / 1e3);
|
|
170
|
+
query = `after:${epoch}`;
|
|
171
|
+
}
|
|
172
|
+
if (options?.filters?.q) {
|
|
173
|
+
query = query ? `${query} ${options.filters.q}` : String(options.filters.q);
|
|
174
|
+
}
|
|
175
|
+
let pageToken;
|
|
176
|
+
let collected = 0;
|
|
177
|
+
do {
|
|
178
|
+
const res = await this.gmail.users.messages.list({
|
|
179
|
+
userId: "me",
|
|
180
|
+
maxResults: Math.min(maxResults - collected, 100),
|
|
181
|
+
...query ? { q: query } : {},
|
|
182
|
+
...pageToken ? { pageToken } : {}
|
|
183
|
+
});
|
|
184
|
+
const messages = res.data.messages ?? [];
|
|
185
|
+
for (const msg of messages) {
|
|
186
|
+
try {
|
|
187
|
+
const record = await this.fetchMessage(msg.id);
|
|
188
|
+
records.push(record);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
errors.push({ id: msg.id, error: errorMessage(err) });
|
|
191
|
+
}
|
|
192
|
+
collected++;
|
|
193
|
+
if (collected >= maxResults) break;
|
|
194
|
+
}
|
|
195
|
+
pageToken = res.data.nextPageToken ?? void 0;
|
|
196
|
+
} while (pageToken && collected < maxResults);
|
|
197
|
+
const profile = await this.gmail.users.getProfile({ userId: "me" });
|
|
198
|
+
const cursor = profile.data.historyId ?? void 0;
|
|
199
|
+
return {
|
|
200
|
+
records,
|
|
201
|
+
cursor,
|
|
202
|
+
hasMore: !!pageToken,
|
|
203
|
+
errors
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// ── Push (send email) ─────────────────────────────────────────
|
|
207
|
+
async push(payload) {
|
|
208
|
+
this.ensureConnected();
|
|
209
|
+
try {
|
|
210
|
+
const toHeader = payload.to.map(formatAddress).join(", ");
|
|
211
|
+
const ccHeader = payload.cc.length ? `Cc: ${payload.cc.map(formatAddress).join(", ")}\r
|
|
212
|
+
` : "";
|
|
213
|
+
const bccHeader = payload.bcc.length ? `Bcc: ${payload.bcc.map(formatAddress).join(", ")}\r
|
|
214
|
+
` : "";
|
|
215
|
+
const mime = [
|
|
216
|
+
`To: ${toHeader}\r
|
|
217
|
+
`,
|
|
218
|
+
ccHeader,
|
|
219
|
+
bccHeader,
|
|
220
|
+
`Subject: ${payload.subject}\r
|
|
221
|
+
`,
|
|
222
|
+
`Content-Type: text/plain; charset="UTF-8"\r
|
|
223
|
+
`,
|
|
224
|
+
`\r
|
|
225
|
+
`,
|
|
226
|
+
payload.body ?? ""
|
|
227
|
+
].join("");
|
|
228
|
+
const encoded = Buffer.from(mime).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
229
|
+
const res = await this.gmail.users.messages.send({
|
|
230
|
+
userId: "me",
|
|
231
|
+
requestBody: { raw: encoded }
|
|
232
|
+
});
|
|
233
|
+
return { success: true, externalId: res.data.id };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return { success: false, error: errorMessage(err) };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ── Internals ─────────────────────────────────────────────────
|
|
239
|
+
ensureConnected() {
|
|
240
|
+
if (!this.gmail || !this.config) {
|
|
241
|
+
throw new Error("GoogleGmailConnector is not connected. Call connect() first.");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Fetch a single message by ID and parse into an EmailRecord. */
|
|
245
|
+
async fetchMessage(messageId) {
|
|
246
|
+
const res = await this.gmail.users.messages.get({
|
|
247
|
+
userId: "me",
|
|
248
|
+
id: messageId,
|
|
249
|
+
format: "metadata",
|
|
250
|
+
metadataHeaders: ["From", "To", "Cc", "Bcc", "Subject", "Date"]
|
|
251
|
+
});
|
|
252
|
+
const msg = res.data;
|
|
253
|
+
const headers = msg.payload?.headers ?? [];
|
|
254
|
+
const getHeader = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? "";
|
|
255
|
+
return {
|
|
256
|
+
gmailId: msg.id,
|
|
257
|
+
threadId: msg.threadId,
|
|
258
|
+
account: this.config.account,
|
|
259
|
+
subject: getHeader("Subject"),
|
|
260
|
+
from: parseAddress(getHeader("From")),
|
|
261
|
+
to: parseAddressList(getHeader("To")),
|
|
262
|
+
cc: parseAddressList(getHeader("Cc")),
|
|
263
|
+
bcc: parseAddressList(getHeader("Bcc")),
|
|
264
|
+
date: new Date(getHeader("Date")).toISOString(),
|
|
265
|
+
snippet: msg.snippet ?? "",
|
|
266
|
+
labels: msg.labelIds ?? [],
|
|
267
|
+
isRead: !(msg.labelIds ?? []).includes("UNREAD")
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function parseAddress(raw) {
|
|
272
|
+
const match = raw.match(/^(.+?)\s*<([^>]+)>$/);
|
|
273
|
+
if (match) {
|
|
274
|
+
return { name: match[1].replace(/^["']|["']$/g, "").trim(), email: match[2] };
|
|
275
|
+
}
|
|
276
|
+
return { email: raw.trim() };
|
|
277
|
+
}
|
|
278
|
+
function parseAddressList(raw) {
|
|
279
|
+
if (!raw.trim()) return [];
|
|
280
|
+
const results = [];
|
|
281
|
+
const parts = raw.split(/,(?=(?:[^<]*<[^>]*>)*[^>]*$)/);
|
|
282
|
+
for (const part of parts) {
|
|
283
|
+
const trimmed = part.trim();
|
|
284
|
+
if (trimmed) results.push(parseAddress(trimmed));
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
function formatAddress(addr) {
|
|
289
|
+
return addr.name ? `${addr.name} <${addr.email}>` : addr.email;
|
|
290
|
+
}
|
|
291
|
+
function errorMessage(err) {
|
|
292
|
+
return err instanceof Error ? err.message : String(err);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/connectors/google/calendar-connector.ts
|
|
296
|
+
var GoogleCalendarConnector = class {
|
|
297
|
+
id = "google-calendar";
|
|
298
|
+
meta = {
|
|
299
|
+
displayName: "Google Calendar",
|
|
300
|
+
provider: "google",
|
|
301
|
+
dataType: "calendar"
|
|
302
|
+
};
|
|
303
|
+
tokenLoader;
|
|
304
|
+
tokenSaver;
|
|
305
|
+
client = null;
|
|
306
|
+
config = null;
|
|
307
|
+
tokens = null;
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
309
|
+
calendar = null;
|
|
310
|
+
constructor(opts) {
|
|
311
|
+
this.tokenLoader = opts.tokenLoader;
|
|
312
|
+
this.tokenSaver = opts.tokenSaver;
|
|
313
|
+
}
|
|
314
|
+
// ── Lifecycle ──────────────────────────────────────────────────
|
|
315
|
+
async connect(config) {
|
|
316
|
+
this.config = config;
|
|
317
|
+
this.client = await createOAuth2Client(config.oauth);
|
|
318
|
+
this.tokens = await loadTokens(this.tokenLoader, config.account);
|
|
319
|
+
if (!this.tokens) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`No stored tokens for account ${config.account}. Complete the OAuth flow first.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
this.tokens = await refreshIfNeeded(
|
|
325
|
+
this.client,
|
|
326
|
+
this.tokens,
|
|
327
|
+
async (t) => saveTokens(this.tokenSaver, config.account, t)
|
|
328
|
+
);
|
|
329
|
+
this.client.setCredentials(this.tokens);
|
|
330
|
+
const { google } = await import("googleapis");
|
|
331
|
+
this.calendar = google.calendar({ version: "v3", auth: this.client });
|
|
332
|
+
}
|
|
333
|
+
async disconnect() {
|
|
334
|
+
this.client = null;
|
|
335
|
+
this.calendar = null;
|
|
336
|
+
this.tokens = null;
|
|
337
|
+
this.config = null;
|
|
338
|
+
}
|
|
339
|
+
async healthCheck() {
|
|
340
|
+
try {
|
|
341
|
+
this.ensureConnected();
|
|
342
|
+
const res = await this.calendar.calendarList.list({ maxResults: 1 });
|
|
343
|
+
const primary = (res.data.items ?? []).find(
|
|
344
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
345
|
+
(c) => c.primary
|
|
346
|
+
);
|
|
347
|
+
return { ok: true, account: primary?.id ?? this.config.account };
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return { ok: false, error: errorMessage2(err) };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ── Sync ───────────────────────────────────────────────────────
|
|
353
|
+
async sync(options) {
|
|
354
|
+
this.ensureConnected();
|
|
355
|
+
if (options?.cursor) {
|
|
356
|
+
return this.syncIncremental(options.cursor, options);
|
|
357
|
+
}
|
|
358
|
+
return this.syncFull(options);
|
|
359
|
+
}
|
|
360
|
+
/** Incremental sync using Calendar syncToken. */
|
|
361
|
+
async syncIncremental(syncToken, options) {
|
|
362
|
+
const calendarId = options?.filters?.calendarId ?? "primary";
|
|
363
|
+
const records = [];
|
|
364
|
+
const errors = [];
|
|
365
|
+
let pageToken;
|
|
366
|
+
let nextSyncToken;
|
|
367
|
+
try {
|
|
368
|
+
do {
|
|
369
|
+
const res = await this.calendar.events.list({
|
|
370
|
+
calendarId,
|
|
371
|
+
syncToken,
|
|
372
|
+
...pageToken ? { pageToken } : {},
|
|
373
|
+
maxResults: options?.limit ? Math.min(options.limit - records.length, 250) : 250
|
|
374
|
+
});
|
|
375
|
+
for (const event of res.data.items ?? []) {
|
|
376
|
+
try {
|
|
377
|
+
records.push(this.mapEvent(event, calendarId));
|
|
378
|
+
} catch (err) {
|
|
379
|
+
errors.push({ id: event.id, error: errorMessage2(err) });
|
|
380
|
+
}
|
|
381
|
+
if (options?.limit && records.length >= options.limit) break;
|
|
382
|
+
}
|
|
383
|
+
pageToken = res.data.nextPageToken ?? void 0;
|
|
384
|
+
nextSyncToken = res.data.nextSyncToken ?? void 0;
|
|
385
|
+
} while (pageToken && (!options?.limit || records.length < options.limit));
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (err?.code === 410) {
|
|
388
|
+
return this.syncFull(options);
|
|
389
|
+
}
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
records,
|
|
394
|
+
cursor: nextSyncToken,
|
|
395
|
+
hasMore: !!pageToken,
|
|
396
|
+
errors
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/** Full sync using timeMin. */
|
|
400
|
+
async syncFull(options) {
|
|
401
|
+
const calendarId = options?.filters?.calendarId ?? "primary";
|
|
402
|
+
const records = [];
|
|
403
|
+
const errors = [];
|
|
404
|
+
const maxResults = options?.limit ?? 250;
|
|
405
|
+
const timeMin = options?.since ? new Date(options.since).toISOString() : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString();
|
|
406
|
+
let pageToken;
|
|
407
|
+
let nextSyncToken;
|
|
408
|
+
do {
|
|
409
|
+
const res = await this.calendar.events.list({
|
|
410
|
+
calendarId,
|
|
411
|
+
timeMin,
|
|
412
|
+
singleEvents: true,
|
|
413
|
+
orderBy: "startTime",
|
|
414
|
+
maxResults: Math.min(maxResults - records.length, 250),
|
|
415
|
+
...pageToken ? { pageToken } : {}
|
|
416
|
+
});
|
|
417
|
+
for (const event of res.data.items ?? []) {
|
|
418
|
+
try {
|
|
419
|
+
records.push(this.mapEvent(event, calendarId));
|
|
420
|
+
} catch (err) {
|
|
421
|
+
errors.push({ id: event.id, error: errorMessage2(err) });
|
|
422
|
+
}
|
|
423
|
+
if (records.length >= maxResults) break;
|
|
424
|
+
}
|
|
425
|
+
pageToken = res.data.nextPageToken ?? void 0;
|
|
426
|
+
nextSyncToken = res.data.nextSyncToken ?? void 0;
|
|
427
|
+
} while (pageToken && records.length < maxResults);
|
|
428
|
+
return {
|
|
429
|
+
records,
|
|
430
|
+
cursor: nextSyncToken,
|
|
431
|
+
hasMore: !!pageToken,
|
|
432
|
+
errors
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// ── Internals ─────────────────────────────────────────────────
|
|
436
|
+
ensureConnected() {
|
|
437
|
+
if (!this.calendar || !this.config) {
|
|
438
|
+
throw new Error("GoogleCalendarConnector is not connected. Call connect() first.");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
442
|
+
mapEvent(event, calendarId) {
|
|
443
|
+
const start = event.start ?? {};
|
|
444
|
+
const end = event.end ?? {};
|
|
445
|
+
const allDay = !!start.date;
|
|
446
|
+
const startAt = allDay ? new Date(start.date).toISOString() : new Date(start.dateTime).toISOString();
|
|
447
|
+
const endAt = allDay ? new Date(end.date).toISOString() : new Date(end.dateTime).toISOString();
|
|
448
|
+
const attendees = (event.attendees ?? []).map(
|
|
449
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
450
|
+
(a) => ({
|
|
451
|
+
email: a.email,
|
|
452
|
+
displayName: a.displayName,
|
|
453
|
+
responseStatus: a.responseStatus
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
return {
|
|
457
|
+
googleEventId: event.id,
|
|
458
|
+
calendarId,
|
|
459
|
+
account: this.config.account,
|
|
460
|
+
title: event.summary ?? "(No title)",
|
|
461
|
+
description: event.description ?? void 0,
|
|
462
|
+
location: event.location ?? void 0,
|
|
463
|
+
startAt,
|
|
464
|
+
endAt,
|
|
465
|
+
allDay,
|
|
466
|
+
timezone: start.timeZone ?? void 0,
|
|
467
|
+
status: event.status ?? "confirmed",
|
|
468
|
+
organizerEmail: event.organizer?.email ?? "",
|
|
469
|
+
attendees,
|
|
470
|
+
recurrence: event.recurrence ?? void 0,
|
|
471
|
+
htmlLink: event.htmlLink ?? void 0
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
function errorMessage2(err) {
|
|
476
|
+
return err instanceof Error ? err.message : String(err);
|
|
477
|
+
}
|
|
478
|
+
export {
|
|
479
|
+
GoogleCalendarConnector,
|
|
480
|
+
GoogleGmailConnector,
|
|
481
|
+
createOAuth2Client,
|
|
482
|
+
exchangeCode,
|
|
483
|
+
getAuthUrl,
|
|
484
|
+
loadTokens,
|
|
485
|
+
refreshIfNeeded,
|
|
486
|
+
saveTokens
|
|
487
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { C as ChannelAdapter, H as HealthStatus, I as InboundMessage } from './c
|
|
|
2
2
|
export { A as Attachment, a as ChannelCapabilities, b as ChannelConfig, c as ChannelMeta, d as ChatType, F as FormattingMode, O as OutboundPayload, S as SendResult } from './channel-06G0vbIn.js';
|
|
3
3
|
import { T as TokenUsage, L as LLMProvider, M as ModelInfo, R as ResolvedModel, C as ChatMessage } from './provider-qqJYv9nv.js';
|
|
4
4
|
export { a as ChatParams, b as ChatResult, c as ContentBlock, d as ToolDefinition, e as ToolUse } from './provider-qqJYv9nv.js';
|
|
5
|
+
import { C as ConnectorConfig } from './connector-DDahQw-2.js';
|
|
6
|
+
export { a as Connector, b as ConnectorMeta, P as PushResult, S as SyncOptions, c as SyncResult } from './connector-DDahQw-2.js';
|
|
5
7
|
import * as better_sqlite3 from 'better-sqlite3';
|
|
6
8
|
|
|
7
9
|
/** Execution adapter types — Story 1.5 / 3.4 / 3.5 */
|
|
@@ -191,6 +193,11 @@ interface BotConfig {
|
|
|
191
193
|
enabled: boolean;
|
|
192
194
|
accounts?: Record<string, unknown>;
|
|
193
195
|
} & Record<string, unknown>>;
|
|
196
|
+
connectors?: Record<string, {
|
|
197
|
+
enabled: boolean;
|
|
198
|
+
provider: string;
|
|
199
|
+
accounts?: Record<string, ConnectorConfig>;
|
|
200
|
+
} & Record<string, unknown>>;
|
|
194
201
|
agents: AgentConfig[];
|
|
195
202
|
providers: Record<string, {
|
|
196
203
|
enabled: boolean;
|
|
@@ -1313,6 +1320,11 @@ declare class NdjsonLogger {
|
|
|
1313
1320
|
close(): void;
|
|
1314
1321
|
}
|
|
1315
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* @deprecated Use {@link Scheduler} from `botinabox` instead.
|
|
1325
|
+
* HeartbeatScheduler uses in-memory setInterval which loses state on restart.
|
|
1326
|
+
* The Scheduler class uses database-backed schedules with cron expressions.
|
|
1327
|
+
*/
|
|
1316
1328
|
declare class HeartbeatScheduler {
|
|
1317
1329
|
private wakeupQueue;
|
|
1318
1330
|
private hooks;
|
|
@@ -1325,6 +1337,76 @@ declare class HeartbeatScheduler {
|
|
|
1325
1337
|
stop(): void;
|
|
1326
1338
|
}
|
|
1327
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* Scheduler — database-backed job scheduling with cron expressions.
|
|
1342
|
+
*
|
|
1343
|
+
* Supports one-time and recurring schedules. When a schedule fires,
|
|
1344
|
+
* it emits the schedule's `action` as a hook event with the
|
|
1345
|
+
* `action_config` payload. Consumers subscribe to handle the action.
|
|
1346
|
+
*
|
|
1347
|
+
* Replaces HeartbeatScheduler for recurring use cases.
|
|
1348
|
+
*/
|
|
1349
|
+
|
|
1350
|
+
interface ScheduleDef {
|
|
1351
|
+
name: string;
|
|
1352
|
+
description?: string;
|
|
1353
|
+
/** Cron expression for recurring schedules */
|
|
1354
|
+
cron?: string;
|
|
1355
|
+
/** ISO 8601 datetime for one-time schedules */
|
|
1356
|
+
runAt?: string;
|
|
1357
|
+
/** Hook event name to emit when fired */
|
|
1358
|
+
action: string;
|
|
1359
|
+
/** JSON-serializable payload passed to the hook */
|
|
1360
|
+
actionConfig?: Record<string, unknown>;
|
|
1361
|
+
timezone?: string;
|
|
1362
|
+
}
|
|
1363
|
+
interface Schedule {
|
|
1364
|
+
id: string;
|
|
1365
|
+
name: string;
|
|
1366
|
+
description: string | null;
|
|
1367
|
+
type: "one_time" | "recurring";
|
|
1368
|
+
cron: string | null;
|
|
1369
|
+
run_at: string | null;
|
|
1370
|
+
timezone: string;
|
|
1371
|
+
enabled: number;
|
|
1372
|
+
action: string;
|
|
1373
|
+
action_config: string;
|
|
1374
|
+
last_fired_at: string | null;
|
|
1375
|
+
next_fire_at: string | null;
|
|
1376
|
+
created_at: string;
|
|
1377
|
+
updated_at: string;
|
|
1378
|
+
deleted_at: string | null;
|
|
1379
|
+
}
|
|
1380
|
+
declare class Scheduler {
|
|
1381
|
+
private db;
|
|
1382
|
+
private hooks;
|
|
1383
|
+
private timer;
|
|
1384
|
+
constructor(db: DataStore, hooks: HookBus);
|
|
1385
|
+
/**
|
|
1386
|
+
* Start the scheduler. Computes initial next_fire_at for schedules
|
|
1387
|
+
* that don't have one, then polls for due schedules.
|
|
1388
|
+
*/
|
|
1389
|
+
start(pollIntervalMs?: number): Promise<void>;
|
|
1390
|
+
stop(): void;
|
|
1391
|
+
/** Check for and fire due schedules. */
|
|
1392
|
+
tick(): Promise<void>;
|
|
1393
|
+
/** Register a new schedule. */
|
|
1394
|
+
register(def: ScheduleDef): Promise<string>;
|
|
1395
|
+
/** Update an existing schedule. */
|
|
1396
|
+
update(id: string, changes: Partial<Pick<ScheduleDef, "name" | "cron" | "runAt" | "action" | "actionConfig" | "timezone" | "description">> & {
|
|
1397
|
+
enabled?: boolean;
|
|
1398
|
+
}): Promise<void>;
|
|
1399
|
+
/** Soft-delete a schedule. */
|
|
1400
|
+
unregister(id: string): Promise<void>;
|
|
1401
|
+
/** List schedules, optionally filtered. */
|
|
1402
|
+
list(filter?: {
|
|
1403
|
+
enabled?: boolean;
|
|
1404
|
+
action?: string;
|
|
1405
|
+
}): Promise<Schedule[]>;
|
|
1406
|
+
/** Compute next_fire_at for any enabled schedule missing it. */
|
|
1407
|
+
private initializeNextFireTimes;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1328
1410
|
/**
|
|
1329
1411
|
* Creates a config revision record with before/after snapshots.
|
|
1330
1412
|
* Note: uses config_revisions table with config_yaml storing JSON of {agentId, before, after}.
|
|
@@ -1485,4 +1567,4 @@ declare function isLoginRequired(stdout: string): boolean;
|
|
|
1485
1567
|
/** Rewrite local image paths to prevent CLI auto-embedding as vision content. */
|
|
1486
1568
|
declare function deactivateLocalImagePaths(prompt: string): string;
|
|
1487
1569
|
|
|
1488
|
-
export { AGENT_STATUSES, type AgentConfig, type AgentDefinition, type AgentFilter, type AgentRecord, AgentRegistry, type AgentStatus, ApiExecutionAdapter, AuditEmitter, type AuditEvent, BackupManager, type BotConfig, type BudgetCheck, type BudgetConfig, BudgetController, CORE_MIGRATIONS, ChannelAdapter, ChannelRegistry, ChannelRegistryError, ChatMessage, ChatSessionManager, CliExecutionAdapter, type ColumnValidator, ColumnValidatorImpl, type ConfigLoadError, type ConfigLoadResult, DEFAULTS, DEFAULT_CONFIG, type DataConfig, DataStore, DataStoreError, type DomainEntityContextOptions, type DomainSchemaOptions, EVENTS, type EntityColumnDef, type EntityConfig, type EntityContextDef, type EntityFileSpec, type EntitySource, type ExecutionAdapter, type Filter, HealthStatus, HeartbeatScheduler, HookBus, type HookHandler, type HookOptions, type HookRegistration, InboundMessage, LLMProvider, MAX_CHAIN_DEPTH, MessagePipeline, type ModelConfig, ModelInfo, ModelRouter, NdjsonLogger, NotificationQueue, type PackageMigration, type PackageUpdate, type ParsedStream, type PkLookup, ProviderRegistry, type QueryOptions, RUN_STATUSES, type RelationDef, type RenderConfig, ResolvedModel, type RetryPolicy, type Row, type RunContext, RunManager, type RunResult, type RunStatus, type SanitizerOptions, type SchemaError, type SecretInput, type SecretMeta, SecretStore, type SecurityConfig, type SeedItem, SessionKey, SessionManager, type SqliteAdapter, type StepRef, TASK_STATUSES, type TableDefinition, type TableInfoRow, type TaskDefinition, TaskQueue, type TaskRecord, type TaskStatus, TokenUsage, type Unsubscribe, UpdateChecker, type UpdateConfig, UpdateManager, type UpdateManifest, type UsageSummary, type User, type UserInput, UserRegistry, WakeupQueue, type WorkflowConfigEntry, type WorkflowDefinition$1 as WorkflowDefinition, WorkflowEngine, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowStep$1 as WorkflowStep, type WorkflowStepConfig, type WorkflowTrigger, _resetConfig, areDependenciesMet, buildAgentBindings, buildChainOrigin, buildProcessEnv, checkAllowlist, checkChainDepth, checkMentionGate, chunkText, classifyUpdate, compareVersions, createConfigRevision, deactivateLocalImagePaths, defineCoreEntityContexts, defineCoreTables, defineDomainEntityContexts, defineDomainTables, detectCycle, discoverChannels, discoverProviders, formatText, getConfig, initConfig, interpolate, interpolateEnv, isLoginRequired, isMaxTurns, loadConfig, parseClaudeStream, parseVersion, runPackageMigrations, sanitize, topologicalSort, validateConfig };
|
|
1570
|
+
export { AGENT_STATUSES, type AgentConfig, type AgentDefinition, type AgentFilter, type AgentRecord, AgentRegistry, type AgentStatus, ApiExecutionAdapter, AuditEmitter, type AuditEvent, BackupManager, type BotConfig, type BudgetCheck, type BudgetConfig, BudgetController, CORE_MIGRATIONS, ChannelAdapter, ChannelRegistry, ChannelRegistryError, ChatMessage, ChatSessionManager, CliExecutionAdapter, type ColumnValidator, ColumnValidatorImpl, type ConfigLoadError, type ConfigLoadResult, ConnectorConfig, DEFAULTS, DEFAULT_CONFIG, type DataConfig, DataStore, DataStoreError, type DomainEntityContextOptions, type DomainSchemaOptions, EVENTS, type EntityColumnDef, type EntityConfig, type EntityContextDef, type EntityFileSpec, type EntitySource, type ExecutionAdapter, type Filter, HealthStatus, HeartbeatScheduler, HookBus, type HookHandler, type HookOptions, type HookRegistration, InboundMessage, LLMProvider, MAX_CHAIN_DEPTH, MessagePipeline, type ModelConfig, ModelInfo, ModelRouter, NdjsonLogger, NotificationQueue, type PackageMigration, type PackageUpdate, type ParsedStream, type PkLookup, ProviderRegistry, type QueryOptions, RUN_STATUSES, type RelationDef, type RenderConfig, ResolvedModel, type RetryPolicy, type Row, type RunContext, RunManager, type RunResult, type RunStatus, type SanitizerOptions, type Schedule, type ScheduleDef, Scheduler, type SchemaError, type SecretInput, type SecretMeta, SecretStore, type SecurityConfig, type SeedItem, SessionKey, SessionManager, type SqliteAdapter, type StepRef, TASK_STATUSES, type TableDefinition, type TableInfoRow, type TaskDefinition, TaskQueue, type TaskRecord, type TaskStatus, TokenUsage, type Unsubscribe, UpdateChecker, type UpdateConfig, UpdateManager, type UpdateManifest, type UsageSummary, type User, type UserInput, UserRegistry, WakeupQueue, type WorkflowConfigEntry, type WorkflowDefinition$1 as WorkflowDefinition, WorkflowEngine, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowStep$1 as WorkflowStep, type WorkflowStepConfig, type WorkflowTrigger, _resetConfig, areDependenciesMet, buildAgentBindings, buildChainOrigin, buildProcessEnv, checkAllowlist, checkChainDepth, checkMentionGate, chunkText, classifyUpdate, compareVersions, createConfigRevision, deactivateLocalImagePaths, defineCoreEntityContexts, defineCoreTables, defineDomainEntityContexts, defineDomainTables, detectCycle, discoverChannels, discoverProviders, formatText, getConfig, initConfig, interpolate, interpolateEnv, isLoginRequired, isMaxTurns, loadConfig, parseClaudeStream, parseVersion, runPackageMigrations, sanitize, topologicalSort, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -1580,6 +1580,29 @@ function defineCoreTables(db) {
|
|
|
1580
1580
|
"FOREIGN KEY (user_id) REFERENCES users(id)"
|
|
1581
1581
|
]
|
|
1582
1582
|
});
|
|
1583
|
+
db.define("schedules", {
|
|
1584
|
+
columns: {
|
|
1585
|
+
id: "TEXT PRIMARY KEY",
|
|
1586
|
+
name: "TEXT NOT NULL",
|
|
1587
|
+
description: "TEXT",
|
|
1588
|
+
type: "TEXT NOT NULL DEFAULT 'recurring'",
|
|
1589
|
+
cron: "TEXT",
|
|
1590
|
+
run_at: "TEXT",
|
|
1591
|
+
timezone: "TEXT DEFAULT 'UTC'",
|
|
1592
|
+
enabled: "INTEGER NOT NULL DEFAULT 1",
|
|
1593
|
+
action: "TEXT NOT NULL",
|
|
1594
|
+
action_config: "TEXT DEFAULT '{}'",
|
|
1595
|
+
last_fired_at: "TEXT",
|
|
1596
|
+
next_fire_at: "TEXT",
|
|
1597
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
1598
|
+
updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
1599
|
+
deleted_at: "TEXT"
|
|
1600
|
+
},
|
|
1601
|
+
tableConstraints: [
|
|
1602
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_name ON schedules(name) WHERE deleted_at IS NULL",
|
|
1603
|
+
"CREATE INDEX IF NOT EXISTS idx_schedules_next ON schedules(enabled, next_fire_at) WHERE deleted_at IS NULL"
|
|
1604
|
+
]
|
|
1605
|
+
});
|
|
1583
1606
|
db.define("secrets", {
|
|
1584
1607
|
columns: {
|
|
1585
1608
|
id: "TEXT PRIMARY KEY",
|
|
@@ -1618,6 +1641,34 @@ var CORE_MIGRATIONS = [
|
|
|
1618
1641
|
{
|
|
1619
1642
|
version: "003_runs_cost_index",
|
|
1620
1643
|
sql: `CREATE INDEX IF NOT EXISTS idx_runs_cost ON runs(agent_id, completed_at) WHERE cost_cents > 0;`
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
version: "004_schedules_table",
|
|
1647
|
+
sql: `CREATE TABLE IF NOT EXISTS schedules (
|
|
1648
|
+
id TEXT PRIMARY KEY,
|
|
1649
|
+
name TEXT NOT NULL,
|
|
1650
|
+
description TEXT,
|
|
1651
|
+
type TEXT NOT NULL DEFAULT 'recurring',
|
|
1652
|
+
cron TEXT,
|
|
1653
|
+
run_at TEXT,
|
|
1654
|
+
timezone TEXT DEFAULT 'UTC',
|
|
1655
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
1656
|
+
action TEXT NOT NULL,
|
|
1657
|
+
action_config TEXT DEFAULT '{}',
|
|
1658
|
+
last_fired_at TEXT,
|
|
1659
|
+
next_fire_at TEXT,
|
|
1660
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
1661
|
+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
1662
|
+
deleted_at TEXT
|
|
1663
|
+
)`
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
version: "005_schedules_name_index",
|
|
1667
|
+
sql: `CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_name ON schedules(name) WHERE deleted_at IS NULL`
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
version: "006_schedules_next_index",
|
|
1671
|
+
sql: `CREATE INDEX IF NOT EXISTS idx_schedules_next ON schedules(enabled, next_fire_at) WHERE deleted_at IS NULL`
|
|
1621
1672
|
}
|
|
1622
1673
|
];
|
|
1623
1674
|
|
|
@@ -3387,6 +3438,194 @@ var HeartbeatScheduler = class {
|
|
|
3387
3438
|
}
|
|
3388
3439
|
};
|
|
3389
3440
|
|
|
3441
|
+
// src/core/orchestrator/scheduler.ts
|
|
3442
|
+
import { parseExpression } from "cron-parser";
|
|
3443
|
+
import { v4 as uuid } from "uuid";
|
|
3444
|
+
function computeNextFire(cron, timezone, after) {
|
|
3445
|
+
const interval = parseExpression(cron, {
|
|
3446
|
+
currentDate: after ?? /* @__PURE__ */ new Date(),
|
|
3447
|
+
tz: timezone
|
|
3448
|
+
});
|
|
3449
|
+
return interval.next().toISOString();
|
|
3450
|
+
}
|
|
3451
|
+
var Scheduler = class {
|
|
3452
|
+
constructor(db, hooks) {
|
|
3453
|
+
this.db = db;
|
|
3454
|
+
this.hooks = hooks;
|
|
3455
|
+
}
|
|
3456
|
+
db;
|
|
3457
|
+
hooks;
|
|
3458
|
+
timer = null;
|
|
3459
|
+
/**
|
|
3460
|
+
* Start the scheduler. Computes initial next_fire_at for schedules
|
|
3461
|
+
* that don't have one, then polls for due schedules.
|
|
3462
|
+
*/
|
|
3463
|
+
async start(pollIntervalMs = 3e4) {
|
|
3464
|
+
await this.initializeNextFireTimes();
|
|
3465
|
+
this.timer = setInterval(() => {
|
|
3466
|
+
void this.tick();
|
|
3467
|
+
}, pollIntervalMs);
|
|
3468
|
+
void this.tick();
|
|
3469
|
+
}
|
|
3470
|
+
stop() {
|
|
3471
|
+
if (this.timer) {
|
|
3472
|
+
clearInterval(this.timer);
|
|
3473
|
+
this.timer = null;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
/** Check for and fire due schedules. */
|
|
3477
|
+
async tick() {
|
|
3478
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3479
|
+
const schedules = (await this.db.query("schedules", {
|
|
3480
|
+
where: { enabled: 1 }
|
|
3481
|
+
})).filter(
|
|
3482
|
+
(s) => s["deleted_at"] == null && s["next_fire_at"] != null && s["next_fire_at"] <= now
|
|
3483
|
+
);
|
|
3484
|
+
for (const schedule of schedules) {
|
|
3485
|
+
try {
|
|
3486
|
+
const config = JSON.parse(schedule.action_config || "{}");
|
|
3487
|
+
await this.hooks.emit(schedule.action, {
|
|
3488
|
+
schedule_id: schedule.id,
|
|
3489
|
+
schedule_name: schedule.name,
|
|
3490
|
+
...config
|
|
3491
|
+
});
|
|
3492
|
+
await this.hooks.emit("schedule.fired", {
|
|
3493
|
+
schedule_id: schedule.id,
|
|
3494
|
+
schedule_name: schedule.name,
|
|
3495
|
+
action: schedule.action,
|
|
3496
|
+
fired_at: now
|
|
3497
|
+
});
|
|
3498
|
+
if (schedule.type === "recurring" && schedule.cron) {
|
|
3499
|
+
const nextFire = computeNextFire(
|
|
3500
|
+
schedule.cron,
|
|
3501
|
+
schedule.timezone,
|
|
3502
|
+
/* @__PURE__ */ new Date()
|
|
3503
|
+
);
|
|
3504
|
+
await this.db.update(
|
|
3505
|
+
"schedules",
|
|
3506
|
+
{ id: schedule.id },
|
|
3507
|
+
{
|
|
3508
|
+
last_fired_at: now,
|
|
3509
|
+
next_fire_at: nextFire,
|
|
3510
|
+
updated_at: now
|
|
3511
|
+
}
|
|
3512
|
+
);
|
|
3513
|
+
} else {
|
|
3514
|
+
await this.db.update(
|
|
3515
|
+
"schedules",
|
|
3516
|
+
{ id: schedule.id },
|
|
3517
|
+
{
|
|
3518
|
+
last_fired_at: now,
|
|
3519
|
+
next_fire_at: null,
|
|
3520
|
+
enabled: 0,
|
|
3521
|
+
updated_at: now
|
|
3522
|
+
}
|
|
3523
|
+
);
|
|
3524
|
+
}
|
|
3525
|
+
} catch (err) {
|
|
3526
|
+
console.error(
|
|
3527
|
+
`[Scheduler] Error firing schedule "${schedule.name}":`,
|
|
3528
|
+
err
|
|
3529
|
+
);
|
|
3530
|
+
await this.hooks.emit("schedule.error", {
|
|
3531
|
+
schedule_id: schedule.id,
|
|
3532
|
+
schedule_name: schedule.name,
|
|
3533
|
+
error: String(err)
|
|
3534
|
+
});
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
/** Register a new schedule. */
|
|
3539
|
+
async register(def) {
|
|
3540
|
+
const id = uuid();
|
|
3541
|
+
const type = def.cron ? "recurring" : "one_time";
|
|
3542
|
+
const timezone = def.timezone ?? "UTC";
|
|
3543
|
+
let nextFire = null;
|
|
3544
|
+
if (def.cron) {
|
|
3545
|
+
nextFire = computeNextFire(def.cron, timezone);
|
|
3546
|
+
} else if (def.runAt) {
|
|
3547
|
+
nextFire = new Date(def.runAt).toISOString();
|
|
3548
|
+
}
|
|
3549
|
+
await this.db.insert("schedules", {
|
|
3550
|
+
id,
|
|
3551
|
+
name: def.name,
|
|
3552
|
+
description: def.description ?? null,
|
|
3553
|
+
type,
|
|
3554
|
+
cron: def.cron ?? null,
|
|
3555
|
+
run_at: def.runAt ?? null,
|
|
3556
|
+
timezone,
|
|
3557
|
+
enabled: 1,
|
|
3558
|
+
action: def.action,
|
|
3559
|
+
action_config: JSON.stringify(def.actionConfig ?? {}),
|
|
3560
|
+
next_fire_at: nextFire
|
|
3561
|
+
});
|
|
3562
|
+
return id;
|
|
3563
|
+
}
|
|
3564
|
+
/** Update an existing schedule. */
|
|
3565
|
+
async update(id, changes) {
|
|
3566
|
+
const row = {
|
|
3567
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3568
|
+
};
|
|
3569
|
+
if (changes.name !== void 0) row["name"] = changes.name;
|
|
3570
|
+
if (changes.description !== void 0) row["description"] = changes.description;
|
|
3571
|
+
if (changes.action !== void 0) row["action"] = changes.action;
|
|
3572
|
+
if (changes.actionConfig !== void 0)
|
|
3573
|
+
row["action_config"] = JSON.stringify(changes.actionConfig);
|
|
3574
|
+
if (changes.enabled !== void 0) row["enabled"] = changes.enabled ? 1 : 0;
|
|
3575
|
+
if (changes.cron !== void 0) {
|
|
3576
|
+
row["cron"] = changes.cron;
|
|
3577
|
+
row["type"] = "recurring";
|
|
3578
|
+
row["run_at"] = null;
|
|
3579
|
+
row["next_fire_at"] = computeNextFire(
|
|
3580
|
+
changes.cron,
|
|
3581
|
+
changes.timezone ?? "UTC"
|
|
3582
|
+
);
|
|
3583
|
+
} else if (changes.runAt !== void 0) {
|
|
3584
|
+
row["run_at"] = changes.runAt;
|
|
3585
|
+
row["type"] = "one_time";
|
|
3586
|
+
row["cron"] = null;
|
|
3587
|
+
row["next_fire_at"] = new Date(changes.runAt).toISOString();
|
|
3588
|
+
}
|
|
3589
|
+
if (changes.timezone !== void 0) row["timezone"] = changes.timezone;
|
|
3590
|
+
await this.db.update("schedules", { id }, row);
|
|
3591
|
+
}
|
|
3592
|
+
/** Soft-delete a schedule. */
|
|
3593
|
+
async unregister(id) {
|
|
3594
|
+
await this.db.update("schedules", { id }, {
|
|
3595
|
+
enabled: 0,
|
|
3596
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3597
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
/** List schedules, optionally filtered. */
|
|
3601
|
+
async list(filter) {
|
|
3602
|
+
const where = {};
|
|
3603
|
+
if (filter?.enabled !== void 0) where["enabled"] = filter.enabled ? 1 : 0;
|
|
3604
|
+
if (filter?.action !== void 0) where["action"] = filter.action;
|
|
3605
|
+
return (await this.db.query("schedules", { where })).filter((s) => s["deleted_at"] == null);
|
|
3606
|
+
}
|
|
3607
|
+
/** Compute next_fire_at for any enabled schedule missing it. */
|
|
3608
|
+
async initializeNextFireTimes() {
|
|
3609
|
+
const schedules = (await this.db.query("schedules", { where: { enabled: 1 } })).filter(
|
|
3610
|
+
(s) => s["deleted_at"] == null && s["next_fire_at"] == null
|
|
3611
|
+
);
|
|
3612
|
+
for (const s of schedules) {
|
|
3613
|
+
let nextFire = null;
|
|
3614
|
+
if (s.type === "recurring" && s.cron) {
|
|
3615
|
+
nextFire = computeNextFire(s.cron, s.timezone);
|
|
3616
|
+
} else if (s.type === "one_time" && s.run_at) {
|
|
3617
|
+
nextFire = new Date(s.run_at).toISOString();
|
|
3618
|
+
}
|
|
3619
|
+
if (nextFire) {
|
|
3620
|
+
await this.db.update("schedules", { id: s.id }, {
|
|
3621
|
+
next_fire_at: nextFire,
|
|
3622
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
};
|
|
3628
|
+
|
|
3390
3629
|
// src/core/orchestrator/adapters/api-adapter.ts
|
|
3391
3630
|
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
3392
3631
|
|
|
@@ -3963,6 +4202,7 @@ export {
|
|
|
3963
4202
|
ProviderRegistry,
|
|
3964
4203
|
RUN_STATUSES,
|
|
3965
4204
|
RunManager,
|
|
4205
|
+
Scheduler,
|
|
3966
4206
|
SecretStore,
|
|
3967
4207
|
SessionKey,
|
|
3968
4208
|
SessionManager,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botinabox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Bot in a Box — framework for building multi-agent bots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./webhook": {
|
|
34
34
|
"import": "./dist/channels/webhook/index.js",
|
|
35
35
|
"types": "./dist/channels/webhook/index.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./google": {
|
|
38
|
+
"import": "./dist/connectors/google/index.js",
|
|
39
|
+
"types": "./dist/connectors/google/index.d.ts"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"bin": {
|
|
@@ -46,12 +50,14 @@
|
|
|
46
50
|
"dependencies": {
|
|
47
51
|
"@types/uuid": "^10.0.0",
|
|
48
52
|
"ajv": "^8.17.1",
|
|
53
|
+
"cron-parser": "^4.9.0",
|
|
49
54
|
"latticesql": "^0.18.0",
|
|
50
55
|
"uuid": "^13.0.0",
|
|
51
56
|
"yaml": "^2.7.0"
|
|
52
57
|
},
|
|
53
58
|
"peerDependencies": {
|
|
54
59
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
60
|
+
"googleapis": ">=140.0.0",
|
|
55
61
|
"openai": "^4.104.0"
|
|
56
62
|
},
|
|
57
63
|
"peerDependenciesMeta": {
|
|
@@ -60,12 +66,16 @@
|
|
|
60
66
|
},
|
|
61
67
|
"openai": {
|
|
62
68
|
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"googleapis": {
|
|
71
|
+
"optional": true
|
|
63
72
|
}
|
|
64
73
|
},
|
|
65
74
|
"devDependencies": {
|
|
66
75
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
67
76
|
"@types/better-sqlite3": "^7.6.12",
|
|
68
77
|
"@types/node": "^22.10.0",
|
|
78
|
+
"googleapis": "^171.4.0",
|
|
69
79
|
"openai": "^4.104.0",
|
|
70
80
|
"tsup": "^8.3.5",
|
|
71
81
|
"typescript": "^5.7.2",
|