@c4t4/heyamigo 0.8.15 → 0.9.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.
@@ -0,0 +1,72 @@
1
+ // Channel-agnostic address shape. Inbound, outbound, async, browser,
2
+ // and crons all carry an Address string. Sender worker parses the
3
+ // channel prefix and dispatches to the matching ChannelAdapter.
4
+ //
5
+ // Serialized form (the wire shape stored in DB columns):
6
+ // wa:dm:17867@s.whatsapp.net
7
+ // wa:group:120363@g.us
8
+ // tg:dm:user_12345
9
+ // tg:group:-100123456
10
+ // system:cron:42
11
+ //
12
+ // First two segments are well-known. The third is the platform-native
13
+ // external id, kept verbatim — easier to debug, lossless round-trip.
14
+ export function formatAddress(addr) {
15
+ return `${addr.channel}:${addr.scope}:${addr.externalId}`;
16
+ }
17
+ export function parseAddress(s) {
18
+ const idx1 = s.indexOf(':');
19
+ const idx2 = s.indexOf(':', idx1 + 1);
20
+ if (idx1 < 0 || idx2 < 0) {
21
+ throw new Error(`bad address (need channel:scope:external_id): ${s}`);
22
+ }
23
+ const channel = s.slice(0, idx1);
24
+ const scope = s.slice(idx1 + 1, idx2);
25
+ const externalId = s.slice(idx2 + 1);
26
+ if (!externalId)
27
+ throw new Error(`bad address (empty external id): ${s}`);
28
+ return { channel, scope, externalId };
29
+ }
30
+ // Convert a raw Baileys JID into our address form. JID suffixes:
31
+ // @s.whatsapp.net → wa:dm
32
+ // @g.us → wa:group
33
+ // @lid → wa:dm (LID identities; canonicalized to wa:dm)
34
+ // @newsletter → wa:group (broadcast/channel, treat as group-like)
35
+ //
36
+ // The full JID stays in externalId so it round-trips losslessly.
37
+ export function jidToAddress(jid) {
38
+ if (jid.endsWith('@g.us')) {
39
+ return { channel: 'wa', scope: 'group', externalId: jid };
40
+ }
41
+ if (jid.endsWith('@newsletter')) {
42
+ return { channel: 'wa', scope: 'group', externalId: jid };
43
+ }
44
+ if (jid.endsWith('@s.whatsapp.net') || jid.endsWith('@lid')) {
45
+ return { channel: 'wa', scope: 'dm', externalId: jid };
46
+ }
47
+ // Unknown WA jid shape — preserve verbatim, default to DM.
48
+ return { channel: 'wa', scope: 'dm', externalId: jid };
49
+ }
50
+ // Reverse: pull the platform-native id back out. For WA this is the
51
+ // JID; ChannelAdapter implementations use it directly.
52
+ export function addressToExternalId(addr) {
53
+ const a = typeof addr === 'string' ? parseAddress(addr) : addr;
54
+ return a.externalId;
55
+ }
56
+ // Convenience predicates.
57
+ export function isGroup(addr) {
58
+ const a = typeof addr === 'string' ? parseAddress(addr) : addr;
59
+ return a.scope === 'group';
60
+ }
61
+ export function isDm(addr) {
62
+ const a = typeof addr === 'string' ? parseAddress(addr) : addr;
63
+ return a.scope === 'dm';
64
+ }
65
+ // System addresses for bot-internal flows (cron-fired self-prompts,
66
+ // task-spawned messages without an originating chat).
67
+ export function systemCronAddress(cronId) {
68
+ return `system:cron:${cronId}`;
69
+ }
70
+ export function systemTaskAddress(taskId) {
71
+ return `system:task:${taskId}`;
72
+ }
@@ -0,0 +1,74 @@
1
+ // Schema drift detector. Runs after migrations succeed. Compares the
2
+ // live database's table set against the set drizzle's schema.ts
3
+ // declares, refuses to start on mismatch.
4
+ //
5
+ // What this catches:
6
+ // - "Forgot to run `drizzle-kit generate` after editing schema.ts"
7
+ // → migration didn't include the new table; drift detected.
8
+ // - "Someone ran `ALTER TABLE` directly in prod" → extra columns or
9
+ // missing columns vs. schema.ts.
10
+ //
11
+ // What this does NOT catch (intentionally — keeps the check simple
12
+ // and predictable):
13
+ // - Column type changes that SQLite stored compatibly.
14
+ // - Index differences (drizzle doesn't always emit identical CREATE
15
+ // INDEX text; comparing index DDL is fragile).
16
+ // - Drift in non-drizzle tables (e.g. __drizzle_migrations itself).
17
+ //
18
+ // If we ever need stricter checking, add it here behind a config flag.
19
+ // Start strict-but-narrow; loosen if it bites.
20
+ import { getTableConfig, SQLiteTable } from 'drizzle-orm/sqlite-core';
21
+ import { logger } from '../logger.js';
22
+ import * as schema from './schema.js';
23
+ export class SchemaDriftError extends Error {
24
+ diffs;
25
+ constructor(diffs) {
26
+ super(`schema drift detected:\n - ${diffs.join('\n - ')}`);
27
+ this.diffs = diffs;
28
+ this.name = 'SchemaDriftError';
29
+ }
30
+ }
31
+ export function checkSchemaDrift(db) {
32
+ const diffs = [];
33
+ // Discover declared tables by walking the schema module's exports.
34
+ // SQLiteTable is a real class; instanceof is the cleanest discriminator.
35
+ const declared = Object.values(schema)
36
+ .filter((v) => v instanceof SQLiteTable)
37
+ .map(t => getTableConfig(t));
38
+ const liveTables = db
39
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%'")
40
+ .all();
41
+ const liveTableNames = new Set(liveTables.map(r => r.name));
42
+ for (const t of declared) {
43
+ if (!liveTableNames.has(t.name)) {
44
+ diffs.push(`missing table: ${t.name}`);
45
+ continue;
46
+ }
47
+ const liveCols = db
48
+ .prepare(`PRAGMA table_info(${t.name})`)
49
+ .all();
50
+ const liveColNames = new Set(liveCols.map(c => c.name));
51
+ const declaredColNames = new Set(t.columns.map(c => c.name));
52
+ for (const c of t.columns) {
53
+ if (!liveColNames.has(c.name)) {
54
+ diffs.push(`${t.name}: missing column "${c.name}"`);
55
+ }
56
+ }
57
+ for (const name of liveColNames) {
58
+ if (!declaredColNames.has(name)) {
59
+ diffs.push(`${t.name}: unexpected column "${name}" (drift)`);
60
+ }
61
+ }
62
+ }
63
+ // Unexpected tables (drift from out-of-band CREATE TABLE)
64
+ for (const name of liveTableNames) {
65
+ if (!declared.some(t => t.name === name)) {
66
+ diffs.push(`unexpected table "${name}" (drift)`);
67
+ }
68
+ }
69
+ if (diffs.length > 0) {
70
+ logger.fatal({ diffs }, 'schema drift detected; bot refuses to start');
71
+ throw new SchemaDriftError(diffs);
72
+ }
73
+ logger.debug({ tables: declared.map(t => t.name) }, 'schema check passed');
74
+ }
@@ -0,0 +1,44 @@
1
+ // Process-wide SQLite handle. Initialized once at boot in src/index.ts
2
+ // (and in any other entry point that needs DB access, like `setup`).
3
+ // Workers and other modules get the handle via `getDb()` — never open
4
+ // their own.
5
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
6
+ import { resolve } from 'path';
7
+ import * as schema from './schema.js';
8
+ import { runMigrations } from './migrate.js';
9
+ import { checkSchemaDrift } from './check.js';
10
+ let rawDb = null;
11
+ let ormDb = null;
12
+ export function dbPath() {
13
+ return resolve(process.cwd(), 'storage', 'heyamigo.db');
14
+ }
15
+ // Run migrations + drift check + open the singleton. Call once per
16
+ // process at boot. Idempotent — subsequent calls return the existing
17
+ // handle without re-migrating.
18
+ export function initDb() {
19
+ if (ormDb)
20
+ return ormDb;
21
+ const path = dbPath();
22
+ rawDb = runMigrations(path);
23
+ checkSchemaDrift(rawDb);
24
+ ormDb = drizzle(rawDb, { schema });
25
+ return ormDb;
26
+ }
27
+ export function getDb() {
28
+ if (!ormDb)
29
+ throw new Error('db not initialized; call initDb() at boot');
30
+ return ormDb;
31
+ }
32
+ export function getRawDb() {
33
+ if (!rawDb)
34
+ throw new Error('db not initialized; call initDb() at boot');
35
+ return rawDb;
36
+ }
37
+ // Used by graceful shutdown.
38
+ export function closeDb() {
39
+ if (rawDb) {
40
+ rawDb.close();
41
+ rawDb = null;
42
+ ormDb = null;
43
+ }
44
+ }
@@ -0,0 +1,113 @@
1
+ // Migration runner. Called once at boot from src/index.ts before any
2
+ // worker spins up. Order: pre-migration backup → drizzle migrator →
3
+ // drift check (in src/db/check.ts). If anything throws, the bot
4
+ // refuses to start.
5
+ import Database from 'better-sqlite3';
6
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
7
+ import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
8
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
9
+ import { dirname, resolve } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { logger } from '../logger.js';
12
+ // Resolve `migrations/` relative to the package install, not cwd. When
13
+ // installed via npm, the bot runs from the user's project dir but the
14
+ // migration SQL files ship inside @c4t4/heyamigo. From src/db/migrate.ts:
15
+ // dist/db/migrate.js ← __filename
16
+ // dist/db/ ← dirname
17
+ // dist/ ← ../
18
+ // <pkg root>/ ← ../../
19
+ // <pkg root>/migrations
20
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
21
+ const MIGRATIONS_FOLDER = resolve(PKG_ROOT, 'migrations');
22
+ const BACKUP_DIR_NAME = 'backups';
23
+ const KEEP_PRE_MIGRATION_BACKUPS = 10;
24
+ // VACUUM INTO is atomic and produces a fully consistent copy even
25
+ // while the DB is being written to. Trivial insurance before any
26
+ // schema change. Skip if no pending migrations to avoid noise on
27
+ // no-op boots.
28
+ function preMigrationBackup(dbPath) {
29
+ // Cheap check: open the DB, ask the migrator what would happen.
30
+ // We can't easily query "what's pending" without invoking drizzle's
31
+ // internals, so instead we check whether our migrations folder has
32
+ // more entries than the drizzle tracking table claims.
33
+ const sqlFiles = existsSync(MIGRATIONS_FOLDER)
34
+ ? readdirSync(MIGRATIONS_FOLDER).filter(f => f.endsWith('.sql'))
35
+ : [];
36
+ if (sqlFiles.length === 0)
37
+ return null;
38
+ let appliedCount = 0;
39
+ if (existsSync(dbPath)) {
40
+ const probe = new Database(dbPath, { readonly: true, fileMustExist: true });
41
+ try {
42
+ const row = probe
43
+ .prepare("SELECT count(*) AS n FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'")
44
+ .get();
45
+ if (row.n > 0) {
46
+ const counted = probe
47
+ .prepare('SELECT count(*) AS n FROM __drizzle_migrations')
48
+ .get();
49
+ appliedCount = counted.n;
50
+ }
51
+ }
52
+ finally {
53
+ probe.close();
54
+ }
55
+ }
56
+ if (appliedCount >= sqlFiles.length)
57
+ return null; // up to date, nothing to back up for
58
+ const backupDir = resolve(dirname(dbPath), BACKUP_DIR_NAME);
59
+ mkdirSync(backupDir, { recursive: true });
60
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
61
+ const backupPath = resolve(backupDir, `pre-migration-${ts}.db`);
62
+ if (existsSync(dbPath)) {
63
+ const tmp = new Database(dbPath, { readonly: true, fileMustExist: true });
64
+ try {
65
+ tmp.exec(`VACUUM INTO '${backupPath.replace(/'/g, "''")}'`);
66
+ }
67
+ finally {
68
+ tmp.close();
69
+ }
70
+ logger.info({ backupPath }, 'pre-migration backup written');
71
+ }
72
+ else {
73
+ // No DB yet — nothing to back up. Boot will create it fresh.
74
+ return null;
75
+ }
76
+ rotatePreMigrationBackups(backupDir);
77
+ return backupPath;
78
+ }
79
+ function rotatePreMigrationBackups(backupDir) {
80
+ const files = readdirSync(backupDir)
81
+ .filter(f => f.startsWith('pre-migration-') && f.endsWith('.db'))
82
+ .map(f => ({ name: f, path: resolve(backupDir, f), mtime: statSync(resolve(backupDir, f)).mtimeMs }))
83
+ .sort((a, b) => b.mtime - a.mtime);
84
+ const toDelete = files.slice(KEEP_PRE_MIGRATION_BACKUPS);
85
+ for (const f of toDelete) {
86
+ try {
87
+ unlinkSync(f.path);
88
+ }
89
+ catch (err) {
90
+ logger.warn({ err, file: f.name }, 'failed to delete old backup');
91
+ }
92
+ }
93
+ }
94
+ export function runMigrations(dbPath) {
95
+ mkdirSync(dirname(dbPath), { recursive: true });
96
+ const backupPath = preMigrationBackup(dbPath);
97
+ const db = new Database(dbPath);
98
+ db.pragma('journal_mode = WAL'); // required for litestream
99
+ db.pragma('foreign_keys = ON');
100
+ const drizzleDb = drizzle(db);
101
+ try {
102
+ migrate(drizzleDb, { migrationsFolder: MIGRATIONS_FOLDER });
103
+ }
104
+ catch (err) {
105
+ logger.fatal({ err, backupPath }, 'migration failed; refusing to start. restore the pre-migration backup if needed.');
106
+ db.close();
107
+ throw err;
108
+ }
109
+ if (backupPath) {
110
+ logger.info('migrations applied successfully');
111
+ }
112
+ return db;
113
+ }
@@ -0,0 +1,53 @@
1
+ // Schema source of truth. Every DDL change goes here first, then
2
+ // `npx drizzle-kit generate` produces a SQL migration in migrations/.
3
+ // Direct ALTER/CREATE/DROP outside this flow is forbidden — see the
4
+ // "Cardinal rule" section in refactor.md.
5
+ import { sqliteTable, text, integer, primaryKey, index } from 'drizzle-orm/sqlite-core';
6
+ // ──────────────────────────────────────────────────────────────────
7
+ // Identity
8
+ // ──────────────────────────────────────────────────────────────────
9
+ // Canonical humans the bot knows about. One row per real person; same
10
+ // person on multiple channels = multiple rows in `identities`, one row
11
+ // here.
12
+ export const persons = sqliteTable('persons', {
13
+ id: text('id').primaryKey(),
14
+ displayName: text('display_name'),
15
+ timezone: text('timezone'),
16
+ createdAt: integer('created_at').notNull(),
17
+ });
18
+ // Channel-addressable identities resolving to a person. Address is
19
+ // channel-prefixed: wa:dm:..., wa:group:..., tg:dm:..., etc.
20
+ export const identities = sqliteTable('identities', {
21
+ personId: text('person_id').notNull().references(() => persons.id),
22
+ address: text('address').notNull().unique(),
23
+ addedAt: integer('added_at').notNull(),
24
+ }, t => ({
25
+ pk: primaryKey({ columns: [t.personId, t.address] }),
26
+ }));
27
+ // ──────────────────────────────────────────────────────────────────
28
+ // Runtime control
29
+ // ──────────────────────────────────────────────────────────────────
30
+ // Worker registry. Every worker (chat, async, browser, sender, memory,
31
+ // orchestrator) inserts a row at startup and updates last_seen as a
32
+ // heartbeat. Orchestrator uses this for liveness detection and
33
+ // graceful shutdown drain (see refactor.md §Workers).
34
+ export const workers = sqliteTable('workers', {
35
+ id: text('id').primaryKey(), // `${hostname}-${pid}-${slot}`
36
+ kind: text('kind').notNull(), // 'chat'|'async'|'browser'|'sender'|'memory'|'orchestrator'
37
+ status: text('status').notNull(), // 'idle'|'busy'|'draining'|'dead'
38
+ currentJob: text('current_job'), // 'queue:id' if busy
39
+ lastSeen: integer('last_seen').notNull(),
40
+ startedAt: integer('started_at').notNull(),
41
+ }, t => ({
42
+ byKindStatus: index('workers_by_kind_status').on(t.kind, t.status),
43
+ byLastSeen: index('workers_by_last_seen').on(t.lastSeen),
44
+ }));
45
+ // Bot-wide control signals. Insert a row to request shutdown/pause/
46
+ // reload; orchestrator picks it up on its next tick. Single-key, so
47
+ // using PK on key gives us upsert semantics.
48
+ export const control = sqliteTable('control', {
49
+ key: text('key').primaryKey(), // 'shutdown'|'pause'|'reload_config'
50
+ value: text('value'),
51
+ requestedBy: text('requested_by'),
52
+ requestedAt: integer('requested_at').notNull(),
53
+ });
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { closeDb, initDb } from './db/index.js';
1
2
  import { attachIncoming } from './gateway/incoming.js';
2
3
  import { handleReply } from './gateway/outgoing.js';
3
4
  import { logger } from './logger.js';
@@ -6,6 +7,10 @@ import { replayPending } from './queue/queue.js';
6
7
  import { startSocket } from './wa/socket.js';
7
8
  async function main() {
8
9
  logger.info('heyamigo starting');
10
+ // Migrations + drift check first; refuses to start on schema mismatch.
11
+ // Additive — flat-file storage (sessions.json, memory files,
12
+ // access.json) still authoritative until later phases swap them.
13
+ initDb();
9
14
  startScheduler();
10
15
  await startSocket((sock) => {
11
16
  attachIncoming(sock);
@@ -18,10 +23,12 @@ async function main() {
18
23
  }
19
24
  process.on('SIGINT', () => {
20
25
  logger.info('SIGINT received, shutting down');
26
+ closeDb();
21
27
  process.exit(0);
22
28
  });
23
29
  process.on('SIGTERM', () => {
24
30
  logger.info('SIGTERM received, shutting down');
31
+ closeDb();
25
32
  process.exit(0);
26
33
  });
27
34
  main().catch((err) => {
@@ -0,0 +1,34 @@
1
+ CREATE TABLE `control` (
2
+ `key` text PRIMARY KEY NOT NULL,
3
+ `value` text,
4
+ `requested_by` text,
5
+ `requested_at` integer NOT NULL
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE `identities` (
9
+ `person_id` text NOT NULL,
10
+ `address` text NOT NULL,
11
+ `added_at` integer NOT NULL,
12
+ PRIMARY KEY(`person_id`, `address`),
13
+ FOREIGN KEY (`person_id`) REFERENCES `persons`(`id`) ON UPDATE no action ON DELETE no action
14
+ );
15
+ --> statement-breakpoint
16
+ CREATE UNIQUE INDEX `identities_address_unique` ON `identities` (`address`);--> statement-breakpoint
17
+ CREATE TABLE `persons` (
18
+ `id` text PRIMARY KEY NOT NULL,
19
+ `display_name` text,
20
+ `timezone` text,
21
+ `created_at` integer NOT NULL
22
+ );
23
+ --> statement-breakpoint
24
+ CREATE TABLE `workers` (
25
+ `id` text PRIMARY KEY NOT NULL,
26
+ `kind` text NOT NULL,
27
+ `status` text NOT NULL,
28
+ `current_job` text,
29
+ `last_seen` integer NOT NULL,
30
+ `started_at` integer NOT NULL
31
+ );
32
+ --> statement-breakpoint
33
+ CREATE INDEX `workers_by_kind_status` ON `workers` (`kind`,`status`);--> statement-breakpoint
34
+ CREATE INDEX `workers_by_last_seen` ON `workers` (`last_seen`);
@@ -0,0 +1,223 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "16f29db1-6a21-41fb-b7e2-933ea7384dad",
5
+ "prevId": "00000000-0000-0000-0000-000000000000",
6
+ "tables": {
7
+ "control": {
8
+ "name": "control",
9
+ "columns": {
10
+ "key": {
11
+ "name": "key",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "value": {
18
+ "name": "value",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": false,
22
+ "autoincrement": false
23
+ },
24
+ "requested_by": {
25
+ "name": "requested_by",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": false,
29
+ "autoincrement": false
30
+ },
31
+ "requested_at": {
32
+ "name": "requested_at",
33
+ "type": "integer",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ }
38
+ },
39
+ "indexes": {},
40
+ "foreignKeys": {},
41
+ "compositePrimaryKeys": {},
42
+ "uniqueConstraints": {},
43
+ "checkConstraints": {}
44
+ },
45
+ "identities": {
46
+ "name": "identities",
47
+ "columns": {
48
+ "person_id": {
49
+ "name": "person_id",
50
+ "type": "text",
51
+ "primaryKey": false,
52
+ "notNull": true,
53
+ "autoincrement": false
54
+ },
55
+ "address": {
56
+ "name": "address",
57
+ "type": "text",
58
+ "primaryKey": false,
59
+ "notNull": true,
60
+ "autoincrement": false
61
+ },
62
+ "added_at": {
63
+ "name": "added_at",
64
+ "type": "integer",
65
+ "primaryKey": false,
66
+ "notNull": true,
67
+ "autoincrement": false
68
+ }
69
+ },
70
+ "indexes": {
71
+ "identities_address_unique": {
72
+ "name": "identities_address_unique",
73
+ "columns": [
74
+ "address"
75
+ ],
76
+ "isUnique": true
77
+ }
78
+ },
79
+ "foreignKeys": {
80
+ "identities_person_id_persons_id_fk": {
81
+ "name": "identities_person_id_persons_id_fk",
82
+ "tableFrom": "identities",
83
+ "tableTo": "persons",
84
+ "columnsFrom": [
85
+ "person_id"
86
+ ],
87
+ "columnsTo": [
88
+ "id"
89
+ ],
90
+ "onDelete": "no action",
91
+ "onUpdate": "no action"
92
+ }
93
+ },
94
+ "compositePrimaryKeys": {
95
+ "identities_person_id_address_pk": {
96
+ "columns": [
97
+ "person_id",
98
+ "address"
99
+ ],
100
+ "name": "identities_person_id_address_pk"
101
+ }
102
+ },
103
+ "uniqueConstraints": {},
104
+ "checkConstraints": {}
105
+ },
106
+ "persons": {
107
+ "name": "persons",
108
+ "columns": {
109
+ "id": {
110
+ "name": "id",
111
+ "type": "text",
112
+ "primaryKey": true,
113
+ "notNull": true,
114
+ "autoincrement": false
115
+ },
116
+ "display_name": {
117
+ "name": "display_name",
118
+ "type": "text",
119
+ "primaryKey": false,
120
+ "notNull": false,
121
+ "autoincrement": false
122
+ },
123
+ "timezone": {
124
+ "name": "timezone",
125
+ "type": "text",
126
+ "primaryKey": false,
127
+ "notNull": false,
128
+ "autoincrement": false
129
+ },
130
+ "created_at": {
131
+ "name": "created_at",
132
+ "type": "integer",
133
+ "primaryKey": false,
134
+ "notNull": true,
135
+ "autoincrement": false
136
+ }
137
+ },
138
+ "indexes": {},
139
+ "foreignKeys": {},
140
+ "compositePrimaryKeys": {},
141
+ "uniqueConstraints": {},
142
+ "checkConstraints": {}
143
+ },
144
+ "workers": {
145
+ "name": "workers",
146
+ "columns": {
147
+ "id": {
148
+ "name": "id",
149
+ "type": "text",
150
+ "primaryKey": true,
151
+ "notNull": true,
152
+ "autoincrement": false
153
+ },
154
+ "kind": {
155
+ "name": "kind",
156
+ "type": "text",
157
+ "primaryKey": false,
158
+ "notNull": true,
159
+ "autoincrement": false
160
+ },
161
+ "status": {
162
+ "name": "status",
163
+ "type": "text",
164
+ "primaryKey": false,
165
+ "notNull": true,
166
+ "autoincrement": false
167
+ },
168
+ "current_job": {
169
+ "name": "current_job",
170
+ "type": "text",
171
+ "primaryKey": false,
172
+ "notNull": false,
173
+ "autoincrement": false
174
+ },
175
+ "last_seen": {
176
+ "name": "last_seen",
177
+ "type": "integer",
178
+ "primaryKey": false,
179
+ "notNull": true,
180
+ "autoincrement": false
181
+ },
182
+ "started_at": {
183
+ "name": "started_at",
184
+ "type": "integer",
185
+ "primaryKey": false,
186
+ "notNull": true,
187
+ "autoincrement": false
188
+ }
189
+ },
190
+ "indexes": {
191
+ "workers_by_kind_status": {
192
+ "name": "workers_by_kind_status",
193
+ "columns": [
194
+ "kind",
195
+ "status"
196
+ ],
197
+ "isUnique": false
198
+ },
199
+ "workers_by_last_seen": {
200
+ "name": "workers_by_last_seen",
201
+ "columns": [
202
+ "last_seen"
203
+ ],
204
+ "isUnique": false
205
+ }
206
+ },
207
+ "foreignKeys": {},
208
+ "compositePrimaryKeys": {},
209
+ "uniqueConstraints": {},
210
+ "checkConstraints": {}
211
+ }
212
+ },
213
+ "views": {},
214
+ "enums": {},
215
+ "_meta": {
216
+ "schemas": {},
217
+ "tables": {},
218
+ "columns": {}
219
+ },
220
+ "internal": {
221
+ "indexes": {}
222
+ }
223
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1779668680508,
9
+ "tag": "0000_phase0_identity_control",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.8.15",
3
+ "version": "0.9.0",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist/",
12
+ "migrations/",
12
13
  "config/config.example.json",
13
14
  "config/access.example.json",
14
15
  "config/personalities/",
@@ -52,15 +53,19 @@
52
53
  "@clack/prompts": "^1.2.0",
53
54
  "@hapi/boom": "^10.0.1",
54
55
  "baileys": "7.0.0-rc.9",
56
+ "better-sqlite3": "^12.10.0",
55
57
  "commander": "^14.0.3",
58
+ "drizzle-orm": "^0.45.2",
56
59
  "fastq": "^1.17.1",
57
60
  "pino": "^9.3.2",
58
61
  "qrcode": "^1.5.4",
59
62
  "zod": "^3.23.8"
60
63
  },
61
64
  "devDependencies": {
65
+ "@types/better-sqlite3": "^7.6.13",
62
66
  "@types/node": "^20.14.10",
63
67
  "@types/qrcode": "^1.5.5",
68
+ "drizzle-kit": "^0.31.10",
64
69
  "tsx": "^4.16.2",
65
70
  "typescript": "^5.5.3"
66
71
  }