botinabox 0.3.1 → 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 +195 -1
- package/dist/index.js +255 -2
- 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;
|
|
@@ -466,6 +473,8 @@ declare class ChannelRegistry {
|
|
|
466
473
|
* Run health checks on all registered adapters.
|
|
467
474
|
*/
|
|
468
475
|
healthCheck(): Promise<Record<string, HealthStatus>>;
|
|
476
|
+
/** Check if an adapter is registered. */
|
|
477
|
+
has(id: string): boolean;
|
|
469
478
|
/** Get an adapter by ID. */
|
|
470
479
|
get(id: string): ChannelAdapter | undefined;
|
|
471
480
|
/** List all registered adapters. */
|
|
@@ -550,6 +559,7 @@ type EntitySource = {
|
|
|
550
559
|
remoteKey: string;
|
|
551
560
|
remoteTable: string;
|
|
552
561
|
filters?: Filter[];
|
|
562
|
+
softDelete?: boolean;
|
|
553
563
|
orderBy?: string;
|
|
554
564
|
limit?: number;
|
|
555
565
|
} | {
|
|
@@ -986,6 +996,69 @@ declare const CORE_MIGRATIONS: Array<{
|
|
|
986
996
|
*/
|
|
987
997
|
declare function defineCoreEntityContexts(db: DataStore): void;
|
|
988
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Options for domain table generation.
|
|
1001
|
+
* Enable/disable optional tables based on your app's needs.
|
|
1002
|
+
*/
|
|
1003
|
+
interface DomainSchemaOptions {
|
|
1004
|
+
/** Include client + invoice tables (default: true) */
|
|
1005
|
+
clients?: boolean;
|
|
1006
|
+
/** Include repository table (default: true) */
|
|
1007
|
+
repositories?: boolean;
|
|
1008
|
+
/** Include file table (default: true) */
|
|
1009
|
+
files?: boolean;
|
|
1010
|
+
/** Include channel table (default: true) */
|
|
1011
|
+
channels?: boolean;
|
|
1012
|
+
/** Include rule table + junction tables (default: true) */
|
|
1013
|
+
rules?: boolean;
|
|
1014
|
+
/** Include event audit log (default: true) */
|
|
1015
|
+
events?: boolean;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Define standard domain tables that most multi-agent apps need.
|
|
1019
|
+
* Call after defineCoreTables() and before db.init().
|
|
1020
|
+
*
|
|
1021
|
+
* Provides: org, project, + optional client, invoice, repository,
|
|
1022
|
+
* file, channel, rule, event tables with appropriate junction tables.
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* ```ts
|
|
1026
|
+
* defineCoreTables(db);
|
|
1027
|
+
* defineDomainTables(db); // all tables
|
|
1028
|
+
* defineDomainTables(db, { clients: false }); // skip client/invoice
|
|
1029
|
+
* ```
|
|
1030
|
+
*/
|
|
1031
|
+
declare function defineDomainTables(db: DataStore, options?: DomainSchemaOptions): void;
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Options for domain entity context generation.
|
|
1035
|
+
* Match the options used in defineDomainTables().
|
|
1036
|
+
*/
|
|
1037
|
+
interface DomainEntityContextOptions {
|
|
1038
|
+
clients?: boolean;
|
|
1039
|
+
repositories?: boolean;
|
|
1040
|
+
files?: boolean;
|
|
1041
|
+
channels?: boolean;
|
|
1042
|
+
rules?: boolean;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Define entity context rendering for standard domain tables.
|
|
1046
|
+
* Call after defineDomainTables() and defineCoreTables().
|
|
1047
|
+
*
|
|
1048
|
+
* Renders per-entity directories with context files for:
|
|
1049
|
+
* org, project, + optional client, file, channel entities.
|
|
1050
|
+
* Also adds PROJECTS.md, RULES.md, SKILLS.md, REPOS.md to agent context.
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```ts
|
|
1054
|
+
* defineCoreTables(db);
|
|
1055
|
+
* defineDomainTables(db);
|
|
1056
|
+
* defineCoreEntityContexts(db); // agents, users, skills
|
|
1057
|
+
* defineDomainEntityContexts(db); // org, project, client, file, channel
|
|
1058
|
+
* ```
|
|
1059
|
+
*/
|
|
1060
|
+
declare function defineDomainEntityContexts(db: DataStore, options?: DomainEntityContextOptions): void;
|
|
1061
|
+
|
|
989
1062
|
interface SanitizerOptions {
|
|
990
1063
|
fieldLengthLimits?: Record<string, number>;
|
|
991
1064
|
truncateSuffix?: string;
|
|
@@ -1047,6 +1120,20 @@ declare class AuditEmitter {
|
|
|
1047
1120
|
emit(event: AuditEvent): void;
|
|
1048
1121
|
}
|
|
1049
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Build a clean environment for spawned subprocesses.
|
|
1125
|
+
* Strips secrets and passes only safe system variables.
|
|
1126
|
+
* Used by the CLI execution adapter when spawning agent processes.
|
|
1127
|
+
*/
|
|
1128
|
+
/**
|
|
1129
|
+
* Build a filtered environment for subprocess execution.
|
|
1130
|
+
* Only passes explicitly allowed variables — all secrets are stripped.
|
|
1131
|
+
*
|
|
1132
|
+
* @param allowedKeys - Additional keys to allow beyond the defaults
|
|
1133
|
+
* @param inject - Extra key-value pairs to inject into the env
|
|
1134
|
+
*/
|
|
1135
|
+
declare function buildProcessEnv(allowedKeys?: string[], inject?: Record<string, string>): Record<string, string>;
|
|
1136
|
+
|
|
1050
1137
|
interface PackageUpdate {
|
|
1051
1138
|
name: string;
|
|
1052
1139
|
installedVersion: string;
|
|
@@ -1233,6 +1320,11 @@ declare class NdjsonLogger {
|
|
|
1233
1320
|
close(): void;
|
|
1234
1321
|
}
|
|
1235
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
|
+
*/
|
|
1236
1328
|
declare class HeartbeatScheduler {
|
|
1237
1329
|
private wakeupQueue;
|
|
1238
1330
|
private hooks;
|
|
@@ -1245,6 +1337,76 @@ declare class HeartbeatScheduler {
|
|
|
1245
1337
|
stop(): void;
|
|
1246
1338
|
}
|
|
1247
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
|
+
|
|
1248
1410
|
/**
|
|
1249
1411
|
* Creates a config revision record with before/after snapshots.
|
|
1250
1412
|
* Note: uses config_revisions table with config_yaml storing JSON of {agentId, before, after}.
|
|
@@ -1373,4 +1535,36 @@ declare class SecretStore {
|
|
|
1373
1535
|
private _toMeta;
|
|
1374
1536
|
}
|
|
1375
1537
|
|
|
1376
|
-
|
|
1538
|
+
/**
|
|
1539
|
+
* Parse Claude CLI NDJSON (stream-json) output into structured results.
|
|
1540
|
+
* Used by the CLI execution adapter to extract session info, costs,
|
|
1541
|
+
* token usage, and text output from Claude CLI subprocess output.
|
|
1542
|
+
*/
|
|
1543
|
+
interface ParsedStream {
|
|
1544
|
+
sessionId: string | null;
|
|
1545
|
+
model: string | null;
|
|
1546
|
+
costUsd: number | null;
|
|
1547
|
+
usage: UsageSummary | null;
|
|
1548
|
+
summary: string;
|
|
1549
|
+
isError: boolean;
|
|
1550
|
+
errorMessage: string | null;
|
|
1551
|
+
stopReason: string | null;
|
|
1552
|
+
}
|
|
1553
|
+
interface UsageSummary {
|
|
1554
|
+
inputTokens: number;
|
|
1555
|
+
cachedInputTokens: number;
|
|
1556
|
+
outputTokens: number;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Parse Claude CLI NDJSON output into a structured result.
|
|
1560
|
+
* Handles init, assistant, and result event types.
|
|
1561
|
+
*/
|
|
1562
|
+
declare function parseClaudeStream(stdout: string): ParsedStream;
|
|
1563
|
+
/** Check if the run stopped due to max turns. */
|
|
1564
|
+
declare function isMaxTurns(parsed: ParsedStream): boolean;
|
|
1565
|
+
/** Check if Claude CLI requires login. */
|
|
1566
|
+
declare function isLoginRequired(stdout: string): boolean;
|
|
1567
|
+
/** Rewrite local image paths to prevent CLI auto-embedding as vision content. */
|
|
1568
|
+
declare function deactivateLocalImagePaths(prompt: string): string;
|
|
1569
|
+
|
|
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
|
@@ -585,6 +585,10 @@ var ChannelRegistry = class {
|
|
|
585
585
|
}
|
|
586
586
|
return results;
|
|
587
587
|
}
|
|
588
|
+
/** Check if an adapter is registered. */
|
|
589
|
+
has(id) {
|
|
590
|
+
return this.adapters.has(id);
|
|
591
|
+
}
|
|
588
592
|
/** Get an adapter by ID. */
|
|
589
593
|
get(id) {
|
|
590
594
|
return this.adapters.get(id)?.adapter;
|
|
@@ -923,6 +927,9 @@ var NotificationQueue = class {
|
|
|
923
927
|
* Returns the notification ID.
|
|
924
928
|
*/
|
|
925
929
|
async enqueue(channel, recipient, payload) {
|
|
930
|
+
if (!this.channelRegistry.has(channel)) {
|
|
931
|
+
throw new Error(`No registered adapter for channel: ${channel}`);
|
|
932
|
+
}
|
|
926
933
|
const row = await this.db.insert("notifications", {
|
|
927
934
|
channel,
|
|
928
935
|
recipient_id: recipient,
|
|
@@ -1573,6 +1580,29 @@ function defineCoreTables(db) {
|
|
|
1573
1580
|
"FOREIGN KEY (user_id) REFERENCES users(id)"
|
|
1574
1581
|
]
|
|
1575
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
|
+
});
|
|
1576
1606
|
db.define("secrets", {
|
|
1577
1607
|
columns: {
|
|
1578
1608
|
id: "TEXT PRIMARY KEY",
|
|
@@ -1611,6 +1641,34 @@ var CORE_MIGRATIONS = [
|
|
|
1611
1641
|
{
|
|
1612
1642
|
version: "003_runs_cost_index",
|
|
1613
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`
|
|
1614
1672
|
}
|
|
1615
1673
|
];
|
|
1616
1674
|
|
|
@@ -2767,8 +2825,9 @@ var TaskQueue = class {
|
|
|
2767
2825
|
)
|
|
2768
2826
|
)
|
|
2769
2827
|
});
|
|
2770
|
-
|
|
2771
|
-
|
|
2828
|
+
const taskId = row["id"];
|
|
2829
|
+
void this.hooks.emit("task.created", { taskId, title: task.title });
|
|
2830
|
+
return taskId;
|
|
2772
2831
|
}
|
|
2773
2832
|
async update(id, changes) {
|
|
2774
2833
|
await this.db.update("tasks", { id }, {
|
|
@@ -2905,6 +2964,11 @@ var RunManager = class {
|
|
|
2905
2964
|
}
|
|
2906
2965
|
}
|
|
2907
2966
|
} else {
|
|
2967
|
+
await this.db.update("tasks", { id: taskId }, {
|
|
2968
|
+
status: "done",
|
|
2969
|
+
result: result.output,
|
|
2970
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2971
|
+
});
|
|
2908
2972
|
const task = await this.db.get("tasks", { id: taskId });
|
|
2909
2973
|
if (task && task["followup_agent_id"]) {
|
|
2910
2974
|
const chainDepth = (task["chain_depth"] ?? 0) + 1;
|
|
@@ -3374,6 +3438,194 @@ var HeartbeatScheduler = class {
|
|
|
3374
3438
|
}
|
|
3375
3439
|
};
|
|
3376
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
|
+
|
|
3377
3629
|
// src/core/orchestrator/adapters/api-adapter.ts
|
|
3378
3630
|
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
3379
3631
|
|
|
@@ -3950,6 +4202,7 @@ export {
|
|
|
3950
4202
|
ProviderRegistry,
|
|
3951
4203
|
RUN_STATUSES,
|
|
3952
4204
|
RunManager,
|
|
4205
|
+
Scheduler,
|
|
3953
4206
|
SecretStore,
|
|
3954
4207
|
SessionKey,
|
|
3955
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",
|