botinabox 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as readline from "readline";
5
+ import * as fs from "fs";
6
+ import { execSync } from "child_process";
7
+
8
+ // src/update-check.ts
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
10
+ import { join } from "path";
11
+ import { homedir } from "os";
12
+ var ONE_DAY_MS = 864e5;
13
+ function isNewer(latest, current) {
14
+ const a = latest.split(".").map(Number);
15
+ const b = current.split(".").map(Number);
16
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
17
+ const av = a[i] ?? 0;
18
+ const bv = b[i] ?? 0;
19
+ if (av > bv) return true;
20
+ if (av < bv) return false;
21
+ }
22
+ return false;
23
+ }
24
+ async function checkForUpdate(pkgName, currentVersion) {
25
+ const cacheDir = join(homedir(), `.${pkgName}`);
26
+ const cachePath = join(cacheDir, "update-check.json");
27
+ try {
28
+ if (existsSync(cachePath)) {
29
+ const cached = JSON.parse(readFileSync(cachePath, "utf-8"));
30
+ if (Date.now() - cached.checked < ONE_DAY_MS) {
31
+ return isNewer(cached.latest, currentVersion) ? cached.latest : null;
32
+ }
33
+ }
34
+ } catch {
35
+ }
36
+ const res = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, {
37
+ headers: { accept: "application/json" },
38
+ signal: AbortSignal.timeout(5e3)
39
+ });
40
+ if (!res.ok) return null;
41
+ const data = await res.json();
42
+ const latest = data.version;
43
+ try {
44
+ if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
45
+ writeFileSync(cachePath, JSON.stringify({ latest, checked: Date.now() }));
46
+ } catch {
47
+ }
48
+ return isNewer(latest, currentVersion) ? latest : null;
49
+ }
50
+
51
+ // src/cli.ts
52
+ function getVersion() {
53
+ try {
54
+ const pkgPath = new URL("../package.json", import.meta.url).pathname;
55
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
56
+ return pkg.version;
57
+ } catch {
58
+ return "unknown";
59
+ }
60
+ }
61
+ async function main(args) {
62
+ const [command, subcommand, ...rest] = args;
63
+ const version = getVersion();
64
+ if (version !== "unknown") {
65
+ checkForUpdate("botinabox", version).then((latest) => {
66
+ if (latest) {
67
+ process.on("exit", () => {
68
+ console.log(
69
+ `
70
+ Update available: ${version} \u2192 ${latest} \u2014 run "botinabox update" to upgrade`
71
+ );
72
+ });
73
+ }
74
+ }).catch(() => {
75
+ });
76
+ }
77
+ if (command === "update") {
78
+ await runUpdate(version);
79
+ } else if (command === "--version" || command === "-v") {
80
+ console.log(version);
81
+ } else if (command === "auth" && subcommand === "google") {
82
+ await authGoogle(rest);
83
+ } else {
84
+ console.log("Usage:");
85
+ console.log(" botinabox auth google <account-email> [options]");
86
+ console.log(" botinabox update Upgrade to the latest version");
87
+ console.log(" botinabox --version Print version");
88
+ console.log("");
89
+ console.log("Options:");
90
+ console.log(" --client-id=<id> Google OAuth client ID");
91
+ console.log(" --client-secret=<secret> Google OAuth client secret");
92
+ console.log(" --credentials=<path> Path to Google credentials JSON");
93
+ console.log(" --db=<path> Path to SQLite database (default: ./data/lattice.db)");
94
+ console.log(" --scopes=<scopes> Comma-separated OAuth scopes");
95
+ process.exit(1);
96
+ }
97
+ }
98
+ async function runUpdate(currentVersion) {
99
+ console.log(`Current version: ${currentVersion}`);
100
+ const latest = await checkForUpdate("botinabox", currentVersion);
101
+ if (!latest) {
102
+ console.log("Already up to date.");
103
+ return;
104
+ }
105
+ console.log(`Updating to ${latest}...`);
106
+ try {
107
+ execSync("npm install -g botinabox@latest", { stdio: "inherit" });
108
+ console.log(`Updated botinabox ${currentVersion} \u2192 ${latest}`);
109
+ } catch {
110
+ console.error("Update failed. Try running manually: npm install -g botinabox@latest");
111
+ process.exit(1);
112
+ }
113
+ }
114
+ async function authGoogle(args) {
115
+ const flags = /* @__PURE__ */ new Map();
116
+ let account;
117
+ for (const arg of args) {
118
+ if (arg.startsWith("--")) {
119
+ const [key, ...valParts] = arg.slice(2).split("=");
120
+ flags.set(key, valParts.join("="));
121
+ } else if (!account) {
122
+ account = arg;
123
+ }
124
+ }
125
+ if (!account) {
126
+ console.error("Error: account email is required.");
127
+ console.error(" botinabox auth google <account-email> --client-id=... --client-secret=...");
128
+ process.exit(1);
129
+ }
130
+ let clientId = flags.get("client-id") ?? process.env.GOOGLE_CLIENT_ID;
131
+ let clientSecret = flags.get("client-secret") ?? process.env.GOOGLE_CLIENT_SECRET;
132
+ const credPath = flags.get("credentials");
133
+ if ((!clientId || !clientSecret) && credPath) {
134
+ if (fs.existsSync(credPath)) {
135
+ const creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
136
+ const installed = creds.installed ?? creds.web;
137
+ clientId = clientId ?? installed?.client_id;
138
+ clientSecret = clientSecret ?? installed?.client_secret;
139
+ }
140
+ }
141
+ if (!clientId || !clientSecret) {
142
+ console.error(
143
+ "Error: Google OAuth credentials required.\n Set --client-id and --client-secret, or --credentials=path/to/credentials.json,\n or GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables."
144
+ );
145
+ process.exit(1);
146
+ }
147
+ const defaultScopes = [
148
+ "https://www.googleapis.com/auth/gmail.readonly",
149
+ "https://www.googleapis.com/auth/gmail.send",
150
+ "https://www.googleapis.com/auth/calendar.readonly"
151
+ ];
152
+ const scopes = flags.has("scopes") ? flags.get("scopes").split(",") : defaultScopes;
153
+ const dbPath = flags.get("db") ?? process.env.DATABASE_PATH ?? "./data/lattice.db";
154
+ const { DataStore, defineCoreTables } = await import("./index.js");
155
+ const db = new DataStore({ dbPath });
156
+ defineCoreTables(db);
157
+ await db.init();
158
+ const tokenLoader = async (key) => {
159
+ const rows = await db.query("secrets", { where: { name: key } });
160
+ const row = rows.find((r) => r["deleted_at"] == null);
161
+ return row?.["value"] ?? null;
162
+ };
163
+ const tokenSaver = async (key, value) => {
164
+ const rows = await db.query("secrets", { where: { name: key } });
165
+ const existing = rows.find((r) => r["deleted_at"] == null);
166
+ if (existing) {
167
+ await db.update("secrets", { id: existing["id"] }, {
168
+ value,
169
+ type: "oauth2",
170
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
171
+ });
172
+ } else {
173
+ await db.insert("secrets", {
174
+ name: key,
175
+ type: "oauth2",
176
+ value,
177
+ description: `Google OAuth tokens for ${account}`
178
+ });
179
+ }
180
+ };
181
+ const { GoogleGmailConnector } = await import("./gmail-connector-MNUBRNFM.js");
182
+ const connector = new GoogleGmailConnector({ tokenLoader, tokenSaver });
183
+ connector.config = {
184
+ account,
185
+ oauth: { clientId, clientSecret, redirectUri: "urn:ietf:wg:oauth:2.0:oob" },
186
+ scopes
187
+ };
188
+ const codeProvider = async (authUrl) => {
189
+ console.log(`
190
+ Authorize account: ${account}`);
191
+ console.log(`
192
+ Open this URL in your browser:
193
+
194
+ ${authUrl}
195
+ `);
196
+ const rl = readline.createInterface({
197
+ input: process.stdin,
198
+ output: process.stdout
199
+ });
200
+ return new Promise((resolve) => {
201
+ rl.question("Enter the authorization code: ", (answer) => {
202
+ rl.close();
203
+ resolve(answer.trim());
204
+ });
205
+ });
206
+ };
207
+ const result = await connector.authenticate(codeProvider);
208
+ if (result.success) {
209
+ console.log(`
210
+ Tokens saved for ${account}. Connector is ready.`);
211
+ } else {
212
+ console.error(`
213
+ Authentication failed: ${result.error}`);
214
+ process.exit(1);
215
+ }
216
+ db.close();
217
+ }
218
+ export {
219
+ main
220
+ };
@@ -0,0 +1,79 @@
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
+ interface AuthResult {
38
+ success: boolean;
39
+ account?: string;
40
+ /** URL the user must visit to authorize (for OAuth flows) */
41
+ authUrl?: string;
42
+ error?: string;
43
+ }
44
+ type ConnectorConfig = Record<string, unknown>;
45
+ /**
46
+ * Generic connector interface for external service integrations.
47
+ *
48
+ * Connectors pull and optionally push data to/from external services
49
+ * (Gmail, Calendar, Trello, Jira, Salesforce, etc.). They produce
50
+ * typed records — the consuming application decides where to store them.
51
+ *
52
+ * @typeParam T - The record type this connector produces/consumes.
53
+ */
54
+ interface Connector<T = Record<string, unknown>> {
55
+ readonly id: string;
56
+ readonly meta: ConnectorMeta;
57
+ connect(config: ConnectorConfig): Promise<void>;
58
+ disconnect(): Promise<void>;
59
+ healthCheck(): Promise<{
60
+ ok: boolean;
61
+ account?: string;
62
+ error?: string;
63
+ }>;
64
+ /** Pull records from external source */
65
+ sync(options?: SyncOptions): Promise<SyncResult<T>>;
66
+ /** Push a record to external source (optional) */
67
+ push?(payload: T): Promise<PushResult>;
68
+ /**
69
+ * Run the authentication/authorization flow for this connector.
70
+ * For OAuth connectors, this generates the auth URL and exchanges the code for tokens.
71
+ *
72
+ * @param codeProvider - called with the auth URL; must return the authorization code.
73
+ * For CLI flows, this prints the URL and reads from stdin.
74
+ * For programmatic flows, the caller handles the redirect.
75
+ */
76
+ authenticate?(codeProvider: (authUrl: string) => Promise<string>): Promise<AuthResult>;
77
+ }
78
+
79
+ export type { AuthResult as A, ConnectorConfig as C, PushResult as P, SyncOptions as S, Connector as a, ConnectorMeta as b, SyncResult as c };
@@ -1,4 +1,4 @@
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';
1
+ import { C as ConnectorConfig, a as Connector, b as ConnectorMeta, A as AuthResult, S as SyncOptions, c as SyncResult, P as PushResult } from '../../connector-B4Mj0P1b.js';
2
2
 
3
3
  /** Google connector types. */
4
4
 
@@ -13,11 +13,26 @@ interface GoogleTokens {
13
13
  expiry_date?: number;
14
14
  token_type: string;
15
15
  }
16
+ interface GoogleServiceAccountConfig {
17
+ /** Path to service account key JSON file */
18
+ keyFile?: string;
19
+ /** Inline service account credentials (alternative to keyFile) */
20
+ credentials?: {
21
+ client_email: string;
22
+ private_key: string;
23
+ project_id?: string;
24
+ };
25
+ /** Email of the user to impersonate via domain-wide delegation */
26
+ subject: string;
27
+ }
16
28
  interface GoogleConnectorConfig extends ConnectorConfig {
17
29
  /** Google account email */
18
30
  account: string;
19
- oauth: GoogleOAuthConfig;
20
- scopes: string[];
31
+ /** OAuth2 user auth (requires browser flow) */
32
+ oauth?: GoogleOAuthConfig;
33
+ /** Service account auth (headless, for cloud deployments) */
34
+ serviceAccount?: GoogleServiceAccountConfig;
35
+ scopes?: string[];
21
36
  }
22
37
  interface EmailAddress {
23
38
  name?: string;
@@ -84,6 +99,11 @@ declare function getAuthUrl(client: OAuth2Client, scopes: string[]): string;
84
99
  * Exchange an authorization code for tokens.
85
100
  */
86
101
  declare function exchangeCode(client: OAuth2Client, code: string): Promise<GoogleTokens>;
102
+ /**
103
+ * Create an authenticated client using a service account with
104
+ * domain-wide delegation (impersonation). No browser flow needed.
105
+ */
106
+ declare function createServiceAccountClient(config: GoogleServiceAccountConfig, scopes: string[]): Promise<OAuth2Client>;
87
107
  /**
88
108
  * Load persisted tokens via a generic getter callback.
89
109
  *
@@ -115,21 +135,21 @@ declare function refreshIfNeeded(client: OAuth2Client, tokens: GoogleTokens, sav
115
135
  */
116
136
 
117
137
  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>;
138
+ /** Load persisted tokens for a given account key (OAuth2 flow only). */
139
+ tokenLoader?: (key: string) => Promise<string | null>;
140
+ /** Persist tokens for a given account key (OAuth2 flow only). */
141
+ tokenSaver?: (key: string, value: string) => Promise<void>;
122
142
  }
123
143
  declare class GoogleGmailConnector implements Connector<EmailRecord> {
124
144
  readonly id = "google-gmail";
125
145
  readonly meta: ConnectorMeta;
126
- private tokenLoader;
127
- private tokenSaver;
146
+ private tokenLoader?;
147
+ private tokenSaver?;
128
148
  private client;
129
149
  private config;
130
150
  private tokens;
131
151
  private gmail;
132
- constructor(opts: GmailConnectorOpts);
152
+ constructor(opts?: GmailConnectorOpts);
133
153
  connect(config: GoogleConnectorConfig): Promise<void>;
134
154
  disconnect(): Promise<void>;
135
155
  healthCheck(): Promise<{
@@ -137,6 +157,7 @@ declare class GoogleGmailConnector implements Connector<EmailRecord> {
137
157
  account?: string;
138
158
  error?: string;
139
159
  }>;
160
+ authenticate(codeProvider: (authUrl: string) => Promise<string>): Promise<AuthResult>;
140
161
  sync(options?: SyncOptions): Promise<SyncResult<EmailRecord>>;
141
162
  /** Incremental sync using Gmail history API. */
142
163
  private syncIncremental;
@@ -156,21 +177,21 @@ declare class GoogleGmailConnector implements Connector<EmailRecord> {
156
177
  */
157
178
 
158
179
  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>;
180
+ /** Load persisted tokens for a given account key (OAuth2 flow only). */
181
+ tokenLoader?: (key: string) => Promise<string | null>;
182
+ /** Persist tokens for a given account key (OAuth2 flow only). */
183
+ tokenSaver?: (key: string, value: string) => Promise<void>;
163
184
  }
164
185
  declare class GoogleCalendarConnector implements Connector<CalendarEventRecord> {
165
186
  readonly id = "google-calendar";
166
187
  readonly meta: ConnectorMeta;
167
- private tokenLoader;
168
- private tokenSaver;
188
+ private tokenLoader?;
189
+ private tokenSaver?;
169
190
  private client;
170
191
  private config;
171
192
  private tokens;
172
193
  private calendar;
173
- constructor(opts: CalendarConnectorOpts);
194
+ constructor(opts?: CalendarConnectorOpts);
174
195
  connect(config: GoogleConnectorConfig): Promise<void>;
175
196
  disconnect(): Promise<void>;
176
197
  healthCheck(): Promise<{
@@ -178,6 +199,7 @@ declare class GoogleCalendarConnector implements Connector<CalendarEventRecord>
178
199
  account?: string;
179
200
  error?: string;
180
201
  }>;
202
+ authenticate(codeProvider: (authUrl: string) => Promise<string>): Promise<AuthResult>;
181
203
  sync(options?: SyncOptions): Promise<SyncResult<CalendarEventRecord>>;
182
204
  /** Incremental sync using Calendar syncToken. */
183
205
  private syncIncremental;
@@ -187,4 +209,4 @@ declare class GoogleCalendarConnector implements Connector<CalendarEventRecord>
187
209
  private mapEvent;
188
210
  }
189
211
 
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 };
212
+ export { type CalendarAttendee, type CalendarConnectorOpts, type CalendarEventRecord, type EmailAddress, type EmailRecord, type GmailConnectorOpts, GoogleCalendarConnector, type GoogleConnectorConfig, GoogleGmailConnector, type GoogleOAuthConfig, type GoogleServiceAccountConfig, type GoogleTokens, createOAuth2Client, createServiceAccountClient, exchangeCode, getAuthUrl, loadTokens, refreshIfNeeded, saveTokens };