@hasna/contacts 0.6.18 → 0.6.20
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/index.js +831 -155
- package/dist/index.js +67 -69
- package/dist/mcp/index.js +70 -79
- package/dist/server/index.js +80 -89
- package/package.json +3 -2
package/dist/cli/index.js
CHANGED
|
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1011
1011
|
this._exitCallback = (err) => {
|
|
1012
1012
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1013
1013
|
throw err;
|
|
1014
|
-
}
|
|
1014
|
+
}
|
|
1015
1015
|
};
|
|
1016
1016
|
}
|
|
1017
1017
|
return this;
|
|
@@ -2103,24 +2103,24 @@ var require_commander = __commonJS((exports) => {
|
|
|
2103
2103
|
import { createRequire } from "module";
|
|
2104
2104
|
import { Database } from "bun:sqlite";
|
|
2105
2105
|
import {
|
|
2106
|
-
existsSync,
|
|
2106
|
+
existsSync as existsSync2,
|
|
2107
2107
|
mkdirSync,
|
|
2108
2108
|
readdirSync,
|
|
2109
2109
|
copyFileSync
|
|
2110
2110
|
} from "fs";
|
|
2111
|
-
import { homedir } from "os";
|
|
2112
|
-
import { join, relative } from "path";
|
|
2113
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
2114
2111
|
import { homedir as homedir2 } from "os";
|
|
2115
|
-
import { join as join2 } from "path";
|
|
2116
|
-
import {
|
|
2117
|
-
import {
|
|
2118
|
-
import {
|
|
2112
|
+
import { join as join2, relative } from "path";
|
|
2113
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
2114
|
+
import { homedir as homedir22 } from "os";
|
|
2115
|
+
import { join as join22 } from "path";
|
|
2116
|
+
import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
|
|
2117
|
+
import { join as join6 } from "path";
|
|
2118
|
+
import { homedir as homedir5 } from "os";
|
|
2119
2119
|
import { hostname } from "os";
|
|
2120
|
-
import { homedir as
|
|
2121
|
-
import { join as
|
|
2122
|
-
import { join as
|
|
2123
|
-
import { homedir as
|
|
2120
|
+
import { homedir as homedir3 } from "os";
|
|
2121
|
+
import { join as join3 } from "path";
|
|
2122
|
+
import { join as join5, dirname } from "path";
|
|
2123
|
+
import { homedir as homedir4, platform } from "os";
|
|
2124
2124
|
function __accessProp2(key) {
|
|
2125
2125
|
return this[key];
|
|
2126
2126
|
}
|
|
@@ -2991,13 +2991,13 @@ function custom(check, _params = {}, fatal) {
|
|
|
2991
2991
|
return ZodAny.create();
|
|
2992
2992
|
}
|
|
2993
2993
|
function getDataDir(serviceName) {
|
|
2994
|
-
const dir =
|
|
2994
|
+
const dir = join2(HASNA_DIR, serviceName);
|
|
2995
2995
|
mkdirSync(dir, { recursive: true });
|
|
2996
2996
|
return dir;
|
|
2997
2997
|
}
|
|
2998
2998
|
function getDbPath(serviceName) {
|
|
2999
2999
|
const dir = getDataDir(serviceName);
|
|
3000
|
-
return
|
|
3000
|
+
return join2(dir, `${serviceName}.db`);
|
|
3001
3001
|
}
|
|
3002
3002
|
function getConfigDir() {
|
|
3003
3003
|
return CONFIG_DIR;
|
|
@@ -3006,7 +3006,7 @@ function getConfigPath() {
|
|
|
3006
3006
|
return CONFIG_PATH;
|
|
3007
3007
|
}
|
|
3008
3008
|
function getCloudConfig() {
|
|
3009
|
-
if (!
|
|
3009
|
+
if (!existsSync22(CONFIG_PATH)) {
|
|
3010
3010
|
return CloudConfigSchema.parse({});
|
|
3011
3011
|
}
|
|
3012
3012
|
try {
|
|
@@ -3048,11 +3048,11 @@ function isSyncExcludedTable(table) {
|
|
|
3048
3048
|
return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
|
|
3049
3049
|
}
|
|
3050
3050
|
function discoverServices() {
|
|
3051
|
-
const dataDir =
|
|
3052
|
-
if (!
|
|
3051
|
+
const dataDir = join6(homedir5(), ".hasna");
|
|
3052
|
+
if (!existsSync6(dataDir))
|
|
3053
3053
|
return [];
|
|
3054
3054
|
try {
|
|
3055
|
-
const entries =
|
|
3055
|
+
const entries = readdirSync3(dataDir, { withFileTypes: true });
|
|
3056
3056
|
return entries.filter((e) => {
|
|
3057
3057
|
if (!e.isDirectory())
|
|
3058
3058
|
return false;
|
|
@@ -3064,30 +3064,30 @@ function discoverServices() {
|
|
|
3064
3064
|
return [];
|
|
3065
3065
|
}
|
|
3066
3066
|
}
|
|
3067
|
-
function
|
|
3067
|
+
function discoverSyncableServices2() {
|
|
3068
3068
|
const local = discoverServices();
|
|
3069
3069
|
const pgSet = new Set(KNOWN_PG_SERVICES);
|
|
3070
3070
|
return local.filter((s) => pgSet.has(s));
|
|
3071
3071
|
}
|
|
3072
3072
|
function getServiceDbPath(service) {
|
|
3073
|
-
const dataDir =
|
|
3074
|
-
if (!
|
|
3073
|
+
const dataDir = join6(homedir5(), ".hasna", service);
|
|
3074
|
+
if (!existsSync6(dataDir))
|
|
3075
3075
|
return null;
|
|
3076
3076
|
const candidates = [
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3077
|
+
join6(dataDir, `${service}.db`),
|
|
3078
|
+
join6(dataDir, "data.db"),
|
|
3079
|
+
join6(dataDir, "database.db")
|
|
3080
3080
|
];
|
|
3081
3081
|
try {
|
|
3082
|
-
const files =
|
|
3082
|
+
const files = readdirSync3(dataDir);
|
|
3083
3083
|
for (const f of files) {
|
|
3084
3084
|
if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
|
|
3085
|
-
candidates.push(
|
|
3085
|
+
candidates.push(join6(dataDir, f));
|
|
3086
3086
|
}
|
|
3087
3087
|
}
|
|
3088
3088
|
} catch {}
|
|
3089
3089
|
for (const p of candidates) {
|
|
3090
|
-
if (
|
|
3090
|
+
if (existsSync6(p))
|
|
3091
3091
|
return p;
|
|
3092
3092
|
}
|
|
3093
3093
|
return null;
|
|
@@ -3394,9 +3394,9 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
3394
3394
|
const batch = rows.slice(offset, offset + batchSize);
|
|
3395
3395
|
try {
|
|
3396
3396
|
if (isAsyncAdapter(target)) {
|
|
3397
|
-
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch
|
|
3397
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
3398
3398
|
} else {
|
|
3399
|
-
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch
|
|
3399
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
3400
3400
|
}
|
|
3401
3401
|
result.rowsWritten += batch.length;
|
|
3402
3402
|
} catch (err) {
|
|
@@ -3443,7 +3443,7 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
3443
3443
|
}
|
|
3444
3444
|
return results;
|
|
3445
3445
|
}
|
|
3446
|
-
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch
|
|
3446
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
3447
3447
|
if (batch.length === 0)
|
|
3448
3448
|
return;
|
|
3449
3449
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -3453,22 +3453,20 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, ba
|
|
|
3453
3453
|
}).join(", ");
|
|
3454
3454
|
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
3455
3455
|
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
3456
|
-
const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
|
|
3457
3456
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
3458
|
-
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}
|
|
3457
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
3459
3458
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
3460
3459
|
await target.run(sql, ...params);
|
|
3461
3460
|
}
|
|
3462
|
-
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch
|
|
3461
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
|
|
3463
3462
|
if (batch.length === 0)
|
|
3464
3463
|
return;
|
|
3465
3464
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
3466
3465
|
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
3467
3466
|
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
3468
3467
|
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
3469
|
-
const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
|
|
3470
3468
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
3471
|
-
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}
|
|
3469
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
3472
3470
|
const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
|
|
3473
3471
|
target.run(sql, ...params);
|
|
3474
3472
|
}
|
|
@@ -3527,17 +3525,17 @@ function ensureFeedbackTable(db) {
|
|
|
3527
3525
|
function saveFeedback(db, feedback) {
|
|
3528
3526
|
ensureFeedbackTable(db);
|
|
3529
3527
|
const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
3530
|
-
const
|
|
3528
|
+
const now2 = new Date().toISOString();
|
|
3531
3529
|
const machineId = feedback.machine_id ?? hostname();
|
|
3532
3530
|
db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
|
|
3533
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ??
|
|
3531
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now2);
|
|
3534
3532
|
return id;
|
|
3535
3533
|
}
|
|
3536
3534
|
async function sendFeedback(feedback, db) {
|
|
3537
3535
|
const config = getCloudConfig();
|
|
3538
3536
|
const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
3539
3537
|
const machineId = feedback.machine_id ?? hostname();
|
|
3540
|
-
const
|
|
3538
|
+
const now2 = new Date().toISOString();
|
|
3541
3539
|
const payload = {
|
|
3542
3540
|
id,
|
|
3543
3541
|
service: feedback.service,
|
|
@@ -3545,7 +3543,7 @@ async function sendFeedback(feedback, db) {
|
|
|
3545
3543
|
message: feedback.message,
|
|
3546
3544
|
email: feedback.email ?? "",
|
|
3547
3545
|
machine_id: machineId,
|
|
3548
|
-
created_at: feedback.created_at ??
|
|
3546
|
+
created_at: feedback.created_at ?? now2
|
|
3549
3547
|
};
|
|
3550
3548
|
try {
|
|
3551
3549
|
const res = await fetch(config.feedback_endpoint, {
|
|
@@ -3597,8 +3595,8 @@ class SyncProgressTracker {
|
|
|
3597
3595
|
}
|
|
3598
3596
|
start(table, total, direction) {
|
|
3599
3597
|
const resumed = this.canResume(table);
|
|
3600
|
-
const
|
|
3601
|
-
this.startTimes.set(table,
|
|
3598
|
+
const now2 = Date.now();
|
|
3599
|
+
this.startTimes.set(table, now2);
|
|
3602
3600
|
const status = resumed ? "resumed" : "in_progress";
|
|
3603
3601
|
const info = {
|
|
3604
3602
|
table,
|
|
@@ -3693,7 +3691,7 @@ class SyncProgressTracker {
|
|
|
3693
3691
|
}
|
|
3694
3692
|
}
|
|
3695
3693
|
}
|
|
3696
|
-
function registerCloudTools(server, serviceName
|
|
3694
|
+
function registerCloudTools(server, serviceName) {
|
|
3697
3695
|
server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
|
|
3698
3696
|
const config = getCloudConfig();
|
|
3699
3697
|
const lines = [
|
|
@@ -3726,13 +3724,8 @@ function registerCloudTools(server, serviceName, opts = {}) {
|
|
|
3726
3724
|
isError: true
|
|
3727
3725
|
};
|
|
3728
3726
|
}
|
|
3729
|
-
const local = new SqliteAdapter(
|
|
3727
|
+
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
3730
3728
|
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
3731
|
-
if (opts.migrations?.length) {
|
|
3732
|
-
for (const sql of opts.migrations) {
|
|
3733
|
-
await cloud.run(sql);
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
3729
|
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
3737
3730
|
const results = await syncPush(local, cloud, { tables: tableList });
|
|
3738
3731
|
local.close();
|
|
@@ -3754,7 +3747,7 @@ function registerCloudTools(server, serviceName, opts = {}) {
|
|
|
3754
3747
|
isError: true
|
|
3755
3748
|
};
|
|
3756
3749
|
}
|
|
3757
|
-
const local = new SqliteAdapter(
|
|
3750
|
+
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
3758
3751
|
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
3759
3752
|
let tableList;
|
|
3760
3753
|
if (tablesStr) {
|
|
@@ -12100,7 +12093,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
12100
12093
|
init_external();
|
|
12101
12094
|
});
|
|
12102
12095
|
init_dotfile = __esm2(() => {
|
|
12103
|
-
HASNA_DIR =
|
|
12096
|
+
HASNA_DIR = join2(homedir2(), ".hasna");
|
|
12104
12097
|
});
|
|
12105
12098
|
exports_config = {};
|
|
12106
12099
|
__export2(exports_config, {
|
|
@@ -12131,14 +12124,14 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
12131
12124
|
schedule_minutes: exports_external.number().default(0)
|
|
12132
12125
|
}).default({})
|
|
12133
12126
|
});
|
|
12134
|
-
CONFIG_DIR =
|
|
12135
|
-
CONFIG_PATH =
|
|
12127
|
+
CONFIG_DIR = join22(homedir22(), ".hasna", "cloud");
|
|
12128
|
+
CONFIG_PATH = join22(CONFIG_DIR, "config.json");
|
|
12136
12129
|
});
|
|
12137
12130
|
exports_discover = {};
|
|
12138
12131
|
__export2(exports_discover, {
|
|
12139
12132
|
isSyncExcludedTable: () => isSyncExcludedTable,
|
|
12140
12133
|
getServiceDbPath: () => getServiceDbPath,
|
|
12141
|
-
discoverSyncableServices: () =>
|
|
12134
|
+
discoverSyncableServices: () => discoverSyncableServices2,
|
|
12142
12135
|
discoverServices: () => discoverServices,
|
|
12143
12136
|
SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
|
|
12144
12137
|
KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
|
|
@@ -12193,15 +12186,13 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
12193
12186
|
init_config();
|
|
12194
12187
|
init_config();
|
|
12195
12188
|
init_dotfile();
|
|
12196
|
-
init_adapter();
|
|
12197
12189
|
init_config();
|
|
12198
|
-
|
|
12199
|
-
AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
|
|
12190
|
+
AUTO_SYNC_CONFIG_PATH = join3(homedir3(), ".hasna", "cloud", "config.json");
|
|
12200
12191
|
init_config();
|
|
12201
12192
|
init_adapter();
|
|
12202
12193
|
init_dotfile();
|
|
12203
12194
|
init_config();
|
|
12204
|
-
CONFIG_DIR2 =
|
|
12195
|
+
CONFIG_DIR2 = join5(homedir4(), ".hasna", "cloud");
|
|
12205
12196
|
init_adapter();
|
|
12206
12197
|
init_config();
|
|
12207
12198
|
init_discover();
|
|
@@ -12248,23 +12239,23 @@ var exports_database = {};
|
|
|
12248
12239
|
__export(exports_database, {
|
|
12249
12240
|
uuid: () => uuid,
|
|
12250
12241
|
resetDatabase: () => resetDatabase,
|
|
12251
|
-
now: () =>
|
|
12242
|
+
now: () => now2,
|
|
12252
12243
|
getDbPath: () => getDbPath2,
|
|
12253
12244
|
getDatabase: () => getDatabase,
|
|
12254
12245
|
getDataDir: () => getDataDir2
|
|
12255
12246
|
});
|
|
12256
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
12257
|
-
import { dirname as dirname2, join as
|
|
12247
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync } from "fs";
|
|
12248
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
12258
12249
|
function getDataDir2() {
|
|
12259
12250
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
12260
|
-
const newDir =
|
|
12261
|
-
const oldDir =
|
|
12262
|
-
if (
|
|
12251
|
+
const newDir = join4(home, ".hasna", "contacts");
|
|
12252
|
+
const oldDir = join4(home, ".contacts");
|
|
12253
|
+
if (existsSync3(oldDir) && !existsSync3(newDir)) {
|
|
12263
12254
|
mkdirSync3(newDir, { recursive: true });
|
|
12264
|
-
for (const file of
|
|
12265
|
-
const oldPath =
|
|
12255
|
+
for (const file of readdirSync2(oldDir)) {
|
|
12256
|
+
const oldPath = join4(oldDir, file);
|
|
12266
12257
|
if (statSync(oldPath).isFile()) {
|
|
12267
|
-
copyFileSync2(oldPath,
|
|
12258
|
+
copyFileSync2(oldPath, join4(newDir, file));
|
|
12268
12259
|
}
|
|
12269
12260
|
}
|
|
12270
12261
|
}
|
|
@@ -12276,13 +12267,13 @@ function getDbPath2() {
|
|
|
12276
12267
|
return process.env["HASNA_CONTACTS_DB_PATH"];
|
|
12277
12268
|
if (process.env["CONTACTS_DB_PATH"])
|
|
12278
12269
|
return process.env["CONTACTS_DB_PATH"];
|
|
12279
|
-
return
|
|
12270
|
+
return join4(getDataDir2(), "contacts.db");
|
|
12280
12271
|
}
|
|
12281
12272
|
function ensureDir(filePath) {
|
|
12282
12273
|
if (filePath === ":memory:")
|
|
12283
12274
|
return;
|
|
12284
12275
|
const dir = dirname2(resolve(filePath));
|
|
12285
|
-
if (!
|
|
12276
|
+
if (!existsSync3(dir))
|
|
12286
12277
|
mkdirSync3(dir, { recursive: true });
|
|
12287
12278
|
}
|
|
12288
12279
|
function getDatabase(path) {
|
|
@@ -12306,7 +12297,7 @@ function resetDatabase() {
|
|
|
12306
12297
|
function uuid() {
|
|
12307
12298
|
return crypto.randomUUID();
|
|
12308
12299
|
}
|
|
12309
|
-
function
|
|
12300
|
+
function now2() {
|
|
12310
12301
|
return new Date().toISOString();
|
|
12311
12302
|
}
|
|
12312
12303
|
function runMigrations(db) {
|
|
@@ -13004,7 +12995,7 @@ function loadContactDetails(db, contact) {
|
|
|
13004
12995
|
function createContact(input, db) {
|
|
13005
12996
|
const d = db || getDatabase();
|
|
13006
12997
|
const id = uuid();
|
|
13007
|
-
const timestamp =
|
|
12998
|
+
const timestamp = now2();
|
|
13008
12999
|
const firstName = input.first_name ?? "";
|
|
13009
13000
|
const lastName = input.last_name ?? "";
|
|
13010
13001
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
@@ -13150,7 +13141,7 @@ function updateContact(id, input, db) {
|
|
|
13150
13141
|
if (!existing)
|
|
13151
13142
|
throw new ContactNotFoundError(id);
|
|
13152
13143
|
const setClauses = ["updated_at = ?"];
|
|
13153
|
-
const params = [
|
|
13144
|
+
const params = [now2()];
|
|
13154
13145
|
if (input.first_name !== undefined) {
|
|
13155
13146
|
setClauses.push("first_name = ?");
|
|
13156
13147
|
params.push(input.first_name);
|
|
@@ -13344,7 +13335,7 @@ function mergeContacts(keepId, mergeId, db) {
|
|
|
13344
13335
|
d.run(`UPDATE activity_log SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
13345
13336
|
d.run(`DELETE FROM contacts WHERE id = ?`, [mergeId]);
|
|
13346
13337
|
const updates = ["updated_at = ?"];
|
|
13347
|
-
const params = [
|
|
13338
|
+
const params = [now2()];
|
|
13348
13339
|
if (!keepRow.notes && mergeRow.notes) {
|
|
13349
13340
|
updates.push("notes = ?");
|
|
13350
13341
|
params.push(mergeRow.notes);
|
|
@@ -13418,7 +13409,7 @@ function archiveContact(id, db) {
|
|
|
13418
13409
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
13419
13410
|
if (!row)
|
|
13420
13411
|
throw new ContactNotFoundError(id);
|
|
13421
|
-
d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [
|
|
13412
|
+
d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [now2(), id]);
|
|
13422
13413
|
logActivity(d, { contact_id: id, action: "contact.archived", details: `Archived contact: ${row.display_name}` });
|
|
13423
13414
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
13424
13415
|
return loadContactDetails(d, rowToContact(updated));
|
|
@@ -13428,7 +13419,7 @@ function unarchiveContact(id, db) {
|
|
|
13428
13419
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
13429
13420
|
if (!row)
|
|
13430
13421
|
throw new ContactNotFoundError(id);
|
|
13431
|
-
d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [
|
|
13422
|
+
d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [now2(), id]);
|
|
13432
13423
|
logActivity(d, { contact_id: id, action: "contact.unarchived", details: `Unarchived contact: ${row.display_name}` });
|
|
13433
13424
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
13434
13425
|
return loadContactDetails(d, rowToContact(updated));
|
|
@@ -13475,7 +13466,7 @@ function autoLinkContactToCompany(contactId, db) {
|
|
|
13475
13466
|
const companyRow = d.query(`SELECT id FROM companies WHERE domain = ? LIMIT 1`).get(domain);
|
|
13476
13467
|
if (!companyRow)
|
|
13477
13468
|
return null;
|
|
13478
|
-
d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id,
|
|
13469
|
+
d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id, now2(), contactId]);
|
|
13479
13470
|
logActivity(d, { contact_id: contactId, action: "contact.auto_linked", details: `Auto-linked to company via email domain: ${domain}` });
|
|
13480
13471
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
13481
13472
|
return loadContactDetails(d, rowToContact(updated));
|
|
@@ -13595,7 +13586,7 @@ function loadCompanyDetails(db, company) {
|
|
|
13595
13586
|
function createCompany(input, db) {
|
|
13596
13587
|
const d = db || getDatabase();
|
|
13597
13588
|
const id = uuid();
|
|
13598
|
-
const timestamp =
|
|
13589
|
+
const timestamp = now2();
|
|
13599
13590
|
d.run(`INSERT INTO companies (id, name, domain, logo_url, description, industry, size, founded_year, notes, custom_fields, is_owned_entity, entity_type, created_at, updated_at)
|
|
13600
13591
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
13601
13592
|
id,
|
|
@@ -13684,7 +13675,7 @@ function updateCompany(id, input, db) {
|
|
|
13684
13675
|
if (!existing)
|
|
13685
13676
|
throw new CompanyNotFoundError(id);
|
|
13686
13677
|
const setClauses = ["updated_at = ?"];
|
|
13687
|
-
const params = [
|
|
13678
|
+
const params = [now2()];
|
|
13688
13679
|
if (input.name !== undefined) {
|
|
13689
13680
|
setClauses.push("name = ?");
|
|
13690
13681
|
params.push(input.name);
|
|
@@ -13782,7 +13773,7 @@ function archiveCompany(id, db) {
|
|
|
13782
13773
|
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
13783
13774
|
if (!row)
|
|
13784
13775
|
throw new CompanyNotFoundError(id);
|
|
13785
|
-
d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [
|
|
13776
|
+
d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [now2(), id]);
|
|
13786
13777
|
logActivity(d, { company_id: id, action: "company.archived", details: `Archived company: ${row.name}` });
|
|
13787
13778
|
const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
13788
13779
|
return loadCompanyDetails(d, rowToCompany2(updated));
|
|
@@ -13792,7 +13783,7 @@ function unarchiveCompany(id, db) {
|
|
|
13792
13783
|
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
13793
13784
|
if (!row)
|
|
13794
13785
|
throw new CompanyNotFoundError(id);
|
|
13795
|
-
d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [
|
|
13786
|
+
d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [now2(), id]);
|
|
13796
13787
|
logActivity(d, { company_id: id, action: "company.unarchived", details: `Unarchived company: ${row.name}` });
|
|
13797
13788
|
const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
13798
13789
|
return loadCompanyDetails(d, rowToCompany2(updated));
|
|
@@ -13948,7 +13939,7 @@ var init_tags = __esm(() => {
|
|
|
13948
13939
|
// src/db/groups.ts
|
|
13949
13940
|
function createGroup(db, input) {
|
|
13950
13941
|
const id = uuid();
|
|
13951
|
-
db.query(`INSERT INTO groups(id, name, description, project_id, created_at, updated_at) VALUES(?,?,?,?,?,?)`).run(id, input.name, input.description ?? null, input.project_id ?? null,
|
|
13942
|
+
db.query(`INSERT INTO groups(id, name, description, project_id, created_at, updated_at) VALUES(?,?,?,?,?,?)`).run(id, input.name, input.description ?? null, input.project_id ?? null, now2(), now2());
|
|
13952
13943
|
return getGroup(db, id);
|
|
13953
13944
|
}
|
|
13954
13945
|
function getGroup(db, id) {
|
|
@@ -13978,7 +13969,7 @@ function updateGroup(db, id, input) {
|
|
|
13978
13969
|
vals.push(input.project_id ?? null);
|
|
13979
13970
|
}
|
|
13980
13971
|
fields.push("updated_at = ?");
|
|
13981
|
-
vals.push(
|
|
13972
|
+
vals.push(now2());
|
|
13982
13973
|
vals.push(id);
|
|
13983
13974
|
db.query(`UPDATE groups SET ${fields.join(", ")} WHERE id = ?`).run(...vals);
|
|
13984
13975
|
return getGroup(db, id);
|
|
@@ -14568,10 +14559,10 @@ __export(exports_images, {
|
|
|
14568
14559
|
getImageAsBase64: () => getImageAsBase64,
|
|
14569
14560
|
deleteImage: () => deleteImage
|
|
14570
14561
|
});
|
|
14571
|
-
import { existsSync as
|
|
14572
|
-
import { join as
|
|
14562
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, copyFileSync as copyFileSync3, unlinkSync, readdirSync as readdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
14563
|
+
import { join as join9, extname, basename } from "path";
|
|
14573
14564
|
function ensureImagesDir() {
|
|
14574
|
-
if (!
|
|
14565
|
+
if (!existsSync5(IMAGES_DIR))
|
|
14575
14566
|
mkdirSync6(IMAGES_DIR, { recursive: true });
|
|
14576
14567
|
}
|
|
14577
14568
|
function getImagesDir() {
|
|
@@ -14586,17 +14577,17 @@ function saveImage(entityId, source, options) {
|
|
|
14586
14577
|
const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
|
|
14587
14578
|
const data = Buffer.from(base64Match[2], "base64");
|
|
14588
14579
|
const filename2 = `${entityId}.${ext2}`;
|
|
14589
|
-
writeFileSync3(
|
|
14580
|
+
writeFileSync3(join9(IMAGES_DIR, filename2), data);
|
|
14590
14581
|
return filename2;
|
|
14591
14582
|
}
|
|
14592
|
-
if (!
|
|
14583
|
+
if (!existsSync5(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
|
|
14593
14584
|
const ext2 = options?.format || "jpg";
|
|
14594
14585
|
const data = Buffer.from(source.trim(), "base64");
|
|
14595
14586
|
const filename2 = `${entityId}.${ext2}`;
|
|
14596
|
-
writeFileSync3(
|
|
14587
|
+
writeFileSync3(join9(IMAGES_DIR, filename2), data);
|
|
14597
14588
|
return filename2;
|
|
14598
14589
|
}
|
|
14599
|
-
if (!
|
|
14590
|
+
if (!existsSync5(source)) {
|
|
14600
14591
|
throw new Error(`Image file not found: ${source}`);
|
|
14601
14592
|
}
|
|
14602
14593
|
const ext = extname(source).slice(1).toLowerCase() || "jpg";
|
|
@@ -14605,14 +14596,14 @@ function saveImage(entityId, source, options) {
|
|
|
14605
14596
|
throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
|
|
14606
14597
|
}
|
|
14607
14598
|
const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
14608
|
-
copyFileSync3(source,
|
|
14599
|
+
copyFileSync3(source, join9(IMAGES_DIR, filename));
|
|
14609
14600
|
return filename;
|
|
14610
14601
|
}
|
|
14611
14602
|
function getImagePath(entityId) {
|
|
14612
14603
|
ensureImagesDir();
|
|
14613
|
-
const files =
|
|
14604
|
+
const files = readdirSync4(IMAGES_DIR);
|
|
14614
14605
|
const match = files.find((f) => f.startsWith(`${entityId}.`));
|
|
14615
|
-
return match ?
|
|
14606
|
+
return match ? join9(IMAGES_DIR, match) : null;
|
|
14616
14607
|
}
|
|
14617
14608
|
function getImageAsBase64(entityId) {
|
|
14618
14609
|
const path = getImagePath(entityId);
|
|
@@ -14625,11 +14616,11 @@ function getImageAsBase64(entityId) {
|
|
|
14625
14616
|
}
|
|
14626
14617
|
function deleteImage(entityId) {
|
|
14627
14618
|
ensureImagesDir();
|
|
14628
|
-
const files =
|
|
14619
|
+
const files = readdirSync4(IMAGES_DIR);
|
|
14629
14620
|
let deleted = false;
|
|
14630
14621
|
for (const f of files) {
|
|
14631
14622
|
if (f.startsWith(`${entityId}.`)) {
|
|
14632
|
-
unlinkSync(
|
|
14623
|
+
unlinkSync(join9(IMAGES_DIR, f));
|
|
14633
14624
|
deleted = true;
|
|
14634
14625
|
}
|
|
14635
14626
|
}
|
|
@@ -14637,17 +14628,17 @@ function deleteImage(entityId) {
|
|
|
14637
14628
|
}
|
|
14638
14629
|
function listImages() {
|
|
14639
14630
|
ensureImagesDir();
|
|
14640
|
-
const files =
|
|
14631
|
+
const files = readdirSync4(IMAGES_DIR).filter((f) => !f.startsWith("."));
|
|
14641
14632
|
return files.map((f) => ({
|
|
14642
14633
|
entity_id: basename(f, extname(f)),
|
|
14643
14634
|
filename: f,
|
|
14644
|
-
path:
|
|
14635
|
+
path: join9(IMAGES_DIR, f)
|
|
14645
14636
|
}));
|
|
14646
14637
|
}
|
|
14647
14638
|
var IMAGES_DIR;
|
|
14648
14639
|
var init_images = __esm(() => {
|
|
14649
14640
|
init_database();
|
|
14650
|
-
IMAGES_DIR =
|
|
14641
|
+
IMAGES_DIR = join9(getDataDir2(), "images");
|
|
14651
14642
|
});
|
|
14652
14643
|
|
|
14653
14644
|
// src/mcp/http.ts
|
|
@@ -14699,8 +14690,8 @@ var DEFAULT_MCP_HTTP_PORT = 8809, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME
|
|
|
14699
14690
|
var init_http = () => {};
|
|
14700
14691
|
|
|
14701
14692
|
// src/lib/connector.ts
|
|
14702
|
-
import { join as
|
|
14703
|
-
import { existsSync as
|
|
14693
|
+
import { join as join10 } from "path";
|
|
14694
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
14704
14695
|
import { homedir as homedir6 } from "os";
|
|
14705
14696
|
async function runConnector(name, args, opts = {}) {
|
|
14706
14697
|
const binary = `connect-${name}`;
|
|
@@ -14751,12 +14742,12 @@ async function runConnector(name, args, opts = {}) {
|
|
|
14751
14742
|
}
|
|
14752
14743
|
function getConnectorTokenPath(name, profile = "default") {
|
|
14753
14744
|
const bases = [
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14745
|
+
join10(homedir6(), ".connectors", `connect-${name}`, "profiles", profile, "tokens.json"),
|
|
14746
|
+
join10(homedir6(), ".connect", `connect-${name}`, "profiles", profile, "tokens.json"),
|
|
14747
|
+
join10(homedir6(), ".connect", `connect-${name}`, "tokens.json")
|
|
14757
14748
|
];
|
|
14758
14749
|
for (const p of bases) {
|
|
14759
|
-
if (
|
|
14750
|
+
if (existsSync7(p))
|
|
14760
14751
|
return p;
|
|
14761
14752
|
}
|
|
14762
14753
|
return null;
|
|
@@ -15749,7 +15740,7 @@ function rowToOrgMember(row) {
|
|
|
15749
15740
|
function addOrgMember(input, db) {
|
|
15750
15741
|
const d = db || getDatabase();
|
|
15751
15742
|
const id = uuid();
|
|
15752
|
-
const timestamp =
|
|
15743
|
+
const timestamp = now2();
|
|
15753
15744
|
d.run(`INSERT INTO org_members (id, company_id, contact_id, title, specialization, office_phone, response_sla_hours, notes, created_at, updated_at)
|
|
15754
15745
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
15755
15746
|
id,
|
|
@@ -15773,7 +15764,7 @@ function listOrgMembers(companyId, db) {
|
|
|
15773
15764
|
function updateOrgMember(id, input, db) {
|
|
15774
15765
|
const d = db || getDatabase();
|
|
15775
15766
|
const setClauses = ["updated_at = ?"];
|
|
15776
|
-
const params = [
|
|
15767
|
+
const params = [now2()];
|
|
15777
15768
|
if ("title" in input) {
|
|
15778
15769
|
setClauses.push("title = ?");
|
|
15779
15770
|
params.push(input.title ?? null);
|
|
@@ -15920,7 +15911,7 @@ function rowToContactTask(row) {
|
|
|
15920
15911
|
function createContactTask(input, db) {
|
|
15921
15912
|
const d = db || getDatabase();
|
|
15922
15913
|
const id = uuid();
|
|
15923
|
-
const timestamp =
|
|
15914
|
+
const timestamp = now2();
|
|
15924
15915
|
d.run(`INSERT INTO contact_tasks
|
|
15925
15916
|
(id, title, description, contact_id, assigned_by, deadline, status, priority,
|
|
15926
15917
|
entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
|
|
@@ -15968,7 +15959,7 @@ function listContactTasks(opts = {}, db) {
|
|
|
15968
15959
|
function updateContactTask(id, input, db) {
|
|
15969
15960
|
const d = db || getDatabase();
|
|
15970
15961
|
const setClauses = ["updated_at = ?"];
|
|
15971
|
-
const params = [
|
|
15962
|
+
const params = [now2()];
|
|
15972
15963
|
if (input.title !== undefined) {
|
|
15973
15964
|
setClauses.push("title = ?");
|
|
15974
15965
|
params.push(input.title);
|
|
@@ -16066,7 +16057,7 @@ function rowToApplication(row) {
|
|
|
16066
16057
|
function createApplication(input, db) {
|
|
16067
16058
|
const d = db || getDatabase();
|
|
16068
16059
|
const id = uuid();
|
|
16069
|
-
const timestamp =
|
|
16060
|
+
const timestamp = now2();
|
|
16070
16061
|
d.run(`INSERT INTO applications
|
|
16071
16062
|
(id, program_name, provider_company_id, type, value_usd, applicant_contact_id, primary_contact_id,
|
|
16072
16063
|
status, submitted_date, decision_date, follow_up_date, notes, method, form_url, metadata, created_at, updated_at)
|
|
@@ -16118,7 +16109,7 @@ function listApplications(opts = {}, db) {
|
|
|
16118
16109
|
function updateApplication(id, input, db) {
|
|
16119
16110
|
const d = db || getDatabase();
|
|
16120
16111
|
const setClauses = ["updated_at = ?"];
|
|
16121
|
-
const params = [
|
|
16112
|
+
const params = [now2()];
|
|
16122
16113
|
if (input.program_name !== undefined) {
|
|
16123
16114
|
setClauses.push("program_name = ?");
|
|
16124
16115
|
params.push(input.program_name);
|
|
@@ -16199,9 +16190,9 @@ __export(exports_upcoming, {
|
|
|
16199
16190
|
function getUpcomingItems(days = 7, db) {
|
|
16200
16191
|
const _db2 = db || getDatabase();
|
|
16201
16192
|
const items = [];
|
|
16202
|
-
const
|
|
16203
|
-
const future = new Date(
|
|
16204
|
-
const todayStr =
|
|
16193
|
+
const now4 = new Date;
|
|
16194
|
+
const future = new Date(now4.getTime() + days * 86400000);
|
|
16195
|
+
const todayStr = now4.toISOString().slice(0, 10);
|
|
16205
16196
|
const futureStr = future.toISOString().slice(0, 10);
|
|
16206
16197
|
const followUps = _db2.query(`SELECT c.id, c.display_name, c.follow_up_at FROM contacts c WHERE c.follow_up_at IS NOT NULL AND c.follow_up_at <= ? AND c.do_not_contact = 0`).all(futureStr);
|
|
16207
16198
|
for (const r of followUps) {
|
|
@@ -16222,8 +16213,8 @@ function getUpcomingItems(days = 7, db) {
|
|
|
16222
16213
|
const contacts = _db2.query(`SELECT id, display_name, birthday FROM contacts WHERE birthday IS NOT NULL AND do_not_contact = 0`).all();
|
|
16223
16214
|
for (const c of contacts) {
|
|
16224
16215
|
const bday = new Date(c.birthday);
|
|
16225
|
-
const thisYear = new Date(
|
|
16226
|
-
const nextBday = thisYear >=
|
|
16216
|
+
const thisYear = new Date(now4.getFullYear(), bday.getMonth(), bday.getDate());
|
|
16217
|
+
const nextBday = thisYear >= now4 ? thisYear : new Date(now4.getFullYear() + 1, bday.getMonth(), bday.getDate());
|
|
16227
16218
|
const nextStr = nextBday.toISOString().slice(0, 10);
|
|
16228
16219
|
if (nextStr <= futureStr) {
|
|
16229
16220
|
items.push({ date: nextStr, type: "birthday", contact_id: c.id, contact_name: c.display_name, title: `Birthday: ${c.display_name}`, urgency: nextStr === todayStr ? "today" : "upcoming" });
|
|
@@ -16364,7 +16355,7 @@ function rowToDeal(row) {
|
|
|
16364
16355
|
function createDeal(input, db) {
|
|
16365
16356
|
const d = db || getDatabase();
|
|
16366
16357
|
const id = uuid();
|
|
16367
|
-
const timestamp =
|
|
16358
|
+
const timestamp = now2();
|
|
16368
16359
|
d.run(`INSERT INTO deals (id, title, contact_id, company_id, stage, value_usd, currency, close_date, notes, created_at, updated_at)
|
|
16369
16360
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
16370
16361
|
id,
|
|
@@ -16412,7 +16403,7 @@ function updateDeal(id, input, db) {
|
|
|
16412
16403
|
if (!existing)
|
|
16413
16404
|
return null;
|
|
16414
16405
|
const setClauses = ["updated_at = ?"];
|
|
16415
|
-
const params = [
|
|
16406
|
+
const params = [now2()];
|
|
16416
16407
|
if (input.title !== undefined) {
|
|
16417
16408
|
setClauses.push("title = ?");
|
|
16418
16409
|
params.push(input.title);
|
|
@@ -16500,7 +16491,7 @@ function rowToEvent(row) {
|
|
|
16500
16491
|
function logEvent(input, db) {
|
|
16501
16492
|
const d = db || getDatabase();
|
|
16502
16493
|
const id = uuid();
|
|
16503
|
-
const timestamp =
|
|
16494
|
+
const timestamp = now2();
|
|
16504
16495
|
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
16505
16496
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
16506
16497
|
id,
|
|
@@ -17212,7 +17203,7 @@ __export(exports_field_history, {
|
|
|
17212
17203
|
});
|
|
17213
17204
|
function recordFieldChange(contactId, fieldName, oldValue, newValue, source, createdBy, db) {
|
|
17214
17205
|
const _db2 = db || getDatabase();
|
|
17215
|
-
_db2.query(`INSERT INTO contact_field_history(id,contact_id,field_name,old_value,new_value,valid_from,source,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)`).run(uuid(), contactId, fieldName, oldValue != null ? String(oldValue) : null, newValue != null ? String(newValue) : null,
|
|
17206
|
+
_db2.query(`INSERT INTO contact_field_history(id,contact_id,field_name,old_value,new_value,valid_from,source,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)`).run(uuid(), contactId, fieldName, oldValue != null ? String(oldValue) : null, newValue != null ? String(newValue) : null, now2(), source || null, createdBy || null, now2());
|
|
17216
17207
|
}
|
|
17217
17208
|
function getFieldHistory(contactId, fieldName, db) {
|
|
17218
17209
|
const _db2 = db || getDatabase();
|
|
@@ -17245,7 +17236,7 @@ function addJobEntry(contactId, input, db) {
|
|
|
17245
17236
|
_db2.query(`UPDATE job_history SET is_current=0, end_date=COALESCE(end_date,?) WHERE contact_id=? AND is_current=1`).run(new Date().toISOString().slice(0, 10), contactId);
|
|
17246
17237
|
}
|
|
17247
17238
|
const id = uuid();
|
|
17248
|
-
_db2.query(`INSERT INTO job_history(id,contact_id,company_id,company_name,title,start_date,end_date,is_current,inferred,source,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.company_id || null, input.company_name, input.title || null, input.start_date || null, input.end_date || null, input.is_current ? 1 : 0, input.inferred ? 1 : 0, input.source || null,
|
|
17239
|
+
_db2.query(`INSERT INTO job_history(id,contact_id,company_id,company_name,title,start_date,end_date,is_current,inferred,source,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.company_id || null, input.company_name, input.title || null, input.start_date || null, input.end_date || null, input.is_current ? 1 : 0, input.inferred ? 1 : 0, input.source || null, now2());
|
|
17249
17240
|
return rowToJob(_db2.query(`SELECT * FROM job_history WHERE id=?`).get(id));
|
|
17250
17241
|
}
|
|
17251
17242
|
function getJobHistory(contactId, db) {
|
|
@@ -17272,7 +17263,7 @@ function rowToLearning(r) {
|
|
|
17272
17263
|
function saveLearning(contactId, input, db) {
|
|
17273
17264
|
const _db2 = db || getDatabase();
|
|
17274
17265
|
const id = uuid();
|
|
17275
|
-
_db2.query(`INSERT INTO contact_learnings(id,contact_id,content,type,confidence,importance,learned_by,session_id,visibility,tags,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.content, input.type || "fact", input.confidence ?? 70, input.importance ?? 5, input.learned_by || null, input.session_id || null, input.visibility || "shared", JSON.stringify(input.tags || []),
|
|
17266
|
+
_db2.query(`INSERT INTO contact_learnings(id,contact_id,content,type,confidence,importance,learned_by,session_id,visibility,tags,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.content, input.type || "fact", input.confidence ?? 70, input.importance ?? 5, input.learned_by || null, input.session_id || null, input.visibility || "shared", JSON.stringify(input.tags || []), now2(), now2());
|
|
17276
17267
|
return rowToLearning(_db2.query(`SELECT * FROM contact_learnings WHERE id=?`).get(id));
|
|
17277
17268
|
}
|
|
17278
17269
|
function getLearnings(contactId, opts = {}, db) {
|
|
@@ -17311,12 +17302,12 @@ function searchLearnings(query, opts = {}, db) {
|
|
|
17311
17302
|
}
|
|
17312
17303
|
function confirmLearning(learningId, _agentName, db) {
|
|
17313
17304
|
const _db2 = db || getDatabase();
|
|
17314
|
-
_db2.query(`UPDATE contact_learnings SET confirmed_count=confirmed_count+1, confidence=MIN(100,confidence+10), updated_at=? WHERE id=?`).run(
|
|
17305
|
+
_db2.query(`UPDATE contact_learnings SET confirmed_count=confirmed_count+1, confidence=MIN(100,confidence+10), updated_at=? WHERE id=?`).run(now2(), learningId);
|
|
17315
17306
|
}
|
|
17316
17307
|
function decayLearnings(db) {
|
|
17317
17308
|
const _db2 = db || getDatabase();
|
|
17318
17309
|
const cutoff = new Date(Date.now() - 30 * 86400000).toISOString();
|
|
17319
|
-
const result = _db2.query(`UPDATE contact_learnings SET confidence=MAX(10,confidence-5), updated_at=? WHERE confirmed_count=0 AND created_at<? AND confidence>10`).run(
|
|
17310
|
+
const result = _db2.query(`UPDATE contact_learnings SET confidence=MAX(10,confidence-5), updated_at=? WHERE confirmed_count=0 AND created_at<? AND confidence>10`).run(now2(), cutoff);
|
|
17320
17311
|
return result.changes || 0;
|
|
17321
17312
|
}
|
|
17322
17313
|
function deleteLearning(learningId, db) {
|
|
@@ -17336,7 +17327,7 @@ function acquireLock(contactId, agentName, ttlSeconds = 300, reason, sessionId,
|
|
|
17336
17327
|
return { acquired: false, held_by: existing.agent_name, lock: existing };
|
|
17337
17328
|
const id = uuid();
|
|
17338
17329
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
17339
|
-
_db2.query(`INSERT INTO contact_locks(id,contact_id,agent_name,reason,acquired_at,expires_at,session_id) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, agentName, reason || null,
|
|
17330
|
+
_db2.query(`INSERT INTO contact_locks(id,contact_id,agent_name,reason,acquired_at,expires_at,session_id) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, agentName, reason || null, now2(), expiresAt, sessionId || null);
|
|
17340
17331
|
return {
|
|
17341
17332
|
acquired: true,
|
|
17342
17333
|
lock: _db2.query(`SELECT * FROM contact_locks WHERE id=?`).get(id)
|
|
@@ -17354,11 +17345,11 @@ function checkLock(contactId, db) {
|
|
|
17354
17345
|
}
|
|
17355
17346
|
function cleanExpiredLocks(db) {
|
|
17356
17347
|
const _db2 = db || getDatabase();
|
|
17357
|
-
_db2.query(`DELETE FROM contact_locks WHERE expires_at<?`).run(
|
|
17348
|
+
_db2.query(`DELETE FROM contact_locks WHERE expires_at<?`).run(now2());
|
|
17358
17349
|
}
|
|
17359
17350
|
function logAgentActivity(contactId, agentName, action, details, sessionId, db) {
|
|
17360
17351
|
const _db2 = db || getDatabase();
|
|
17361
|
-
_db2.query(`INSERT INTO contact_agent_activity(id,contact_id,agent_name,action,details,session_id,created_at) VALUES(?,?,?,?,?,?,?)`).run(uuid(), contactId, agentName, action, details || null, sessionId || null,
|
|
17352
|
+
_db2.query(`INSERT INTO contact_agent_activity(id,contact_id,agent_name,action,details,session_id,created_at) VALUES(?,?,?,?,?,?,?)`).run(uuid(), contactId, agentName, action, details || null, sessionId || null, now2());
|
|
17362
17353
|
}
|
|
17363
17354
|
function getAgentActivity(contactId, limit = 20, db) {
|
|
17364
17355
|
const _db2 = db || getDatabase();
|
|
@@ -17443,7 +17434,7 @@ __export(exports_identity, {
|
|
|
17443
17434
|
function addIdentity(contactId, system, externalId, externalUrl, confidence = "inferred", db) {
|
|
17444
17435
|
const _db2 = db || getDatabase();
|
|
17445
17436
|
const id = uuid();
|
|
17446
|
-
_db2.query(`INSERT OR REPLACE INTO contact_identities(id,contact_id,system,external_id,external_url,confidence,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, system, externalId, externalUrl || null, confidence,
|
|
17437
|
+
_db2.query(`INSERT OR REPLACE INTO contact_identities(id,contact_id,system,external_id,external_url,confidence,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, system, externalId, externalUrl || null, confidence, now2());
|
|
17447
17438
|
return _db2.query(`SELECT * FROM contact_identities WHERE id=?`).get(id);
|
|
17448
17439
|
}
|
|
17449
17440
|
function resolveIdentity(system, externalId, db) {
|
|
@@ -17542,7 +17533,7 @@ async function embedContact(contactId, db) {
|
|
|
17542
17533
|
const tokens = tokenize(text);
|
|
17543
17534
|
const tfidf = buildTfIdf(tokens);
|
|
17544
17535
|
const embedding = JSON.stringify(Array.from(tfidf.entries()).sort((a, b) => b[1] - a[1]).slice(0, 100));
|
|
17545
|
-
_db2.query(`INSERT OR REPLACE INTO contact_embeddings(contact_id, embedding, model, embedded_text, created_at, updated_at) VALUES(?,?,'tfidf',?,?,?)`).run(contactId, embedding, text.slice(0, 500),
|
|
17536
|
+
_db2.query(`INSERT OR REPLACE INTO contact_embeddings(contact_id, embedding, model, embedded_text, created_at, updated_at) VALUES(?,?,'tfidf',?,?,?)`).run(contactId, embedding, text.slice(0, 500), now2(), now2());
|
|
17546
17537
|
}
|
|
17547
17538
|
async function embedAllContacts(db) {
|
|
17548
17539
|
const _db2 = db || getDatabase();
|
|
@@ -17907,9 +17898,9 @@ function getStaleContacts(threshold = 40, db) {
|
|
|
17907
17898
|
function markFieldVerified(contactId, fieldName, source, db) {
|
|
17908
17899
|
const _db2 = db || getDatabase();
|
|
17909
17900
|
try {
|
|
17910
|
-
_db2.query(`INSERT OR REPLACE INTO field_verifications(contact_id,field_name,verified_at,source) VALUES(?,?,?,?)`).run(contactId, fieldName,
|
|
17901
|
+
_db2.query(`INSERT OR REPLACE INTO field_verifications(contact_id,field_name,verified_at,source) VALUES(?,?,?,?)`).run(contactId, fieldName, now2(), source || null);
|
|
17911
17902
|
} catch {
|
|
17912
|
-
_db2.query(`INSERT INTO activity_log(id,contact_id,action,details,created_at) VALUES(?,?,?,?,?)`).run(crypto.randomUUID(), contactId, "field.verified", JSON.stringify({ field_name: fieldName, source }),
|
|
17903
|
+
_db2.query(`INSERT INTO activity_log(id,contact_id,action,details,created_at) VALUES(?,?,?,?,?)`).run(crypto.randomUUID(), contactId, "field.verified", JSON.stringify({ field_name: fieldName, source }), now2());
|
|
17913
17904
|
}
|
|
17914
17905
|
}
|
|
17915
17906
|
var SCORED_FIELDS;
|
|
@@ -17930,7 +17921,7 @@ __export(exports_org_chart, {
|
|
|
17930
17921
|
function addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false, db) {
|
|
17931
17922
|
const _db2 = db || getDatabase();
|
|
17932
17923
|
const id = uuid();
|
|
17933
|
-
_db2.query(`INSERT OR IGNORE INTO org_chart_edges(id,company_id,contact_a_id,contact_b_id,edge_type,inferred,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, companyId, contactAId, contactBId, edgeType, inferred ? 1 : 0,
|
|
17924
|
+
_db2.query(`INSERT OR IGNORE INTO org_chart_edges(id,company_id,contact_a_id,contact_b_id,edge_type,inferred,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, companyId, contactAId, contactBId, edgeType, inferred ? 1 : 0, now2());
|
|
17934
17925
|
return _db2.query(`SELECT * FROM org_chart_edges WHERE company_id=? AND contact_a_id=? AND contact_b_id=? AND edge_type=?`).get(companyId, contactAId, contactBId, edgeType);
|
|
17935
17926
|
}
|
|
17936
17927
|
function listOrgChart(companyId, db) {
|
|
@@ -17940,7 +17931,7 @@ function listOrgChart(companyId, db) {
|
|
|
17940
17931
|
function setDealContactRole(dealId, contactId, accountRole, db) {
|
|
17941
17932
|
const _db2 = db || getDatabase();
|
|
17942
17933
|
const id = uuid();
|
|
17943
|
-
_db2.query(`INSERT OR REPLACE INTO deal_contact_roles(id,deal_id,contact_id,account_role,created_at) VALUES(?,?,?,?,?)`).run(id, dealId, contactId, accountRole,
|
|
17934
|
+
_db2.query(`INSERT OR REPLACE INTO deal_contact_roles(id,deal_id,contact_id,account_role,created_at) VALUES(?,?,?,?,?)`).run(id, dealId, contactId, accountRole, now2());
|
|
17944
17935
|
return _db2.query(`SELECT * FROM deal_contact_roles WHERE deal_id=? AND contact_id=?`).get(dealId, contactId);
|
|
17945
17936
|
}
|
|
17946
17937
|
function getDealTeam(dealId, db) {
|
|
@@ -17987,7 +17978,7 @@ __export(exports_vault, {
|
|
|
17987
17978
|
decrypt: () => decrypt
|
|
17988
17979
|
});
|
|
17989
17980
|
import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
17990
|
-
import { join as
|
|
17981
|
+
import { join as join11 } from "path";
|
|
17991
17982
|
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
|
|
17992
17983
|
function deriveKey(passphrase, salt) {
|
|
17993
17984
|
return pbkdf2Sync(passphrase, salt, 1e5, 32, "sha512");
|
|
@@ -18098,7 +18089,7 @@ function storeFile(sourcePath, entityId) {
|
|
|
18098
18089
|
if (!existsSync9(DOCUMENTS_DIR))
|
|
18099
18090
|
mkdirSync7(DOCUMENTS_DIR, { recursive: true });
|
|
18100
18091
|
const ext = sourcePath.split(".").pop() || "bin";
|
|
18101
|
-
const destPath =
|
|
18092
|
+
const destPath = join11(DOCUMENTS_DIR, `${entityId}.${ext}`);
|
|
18102
18093
|
const data = readFileSync5(sourcePath);
|
|
18103
18094
|
writeFileSync4(destPath, data);
|
|
18104
18095
|
return destPath;
|
|
@@ -18106,10 +18097,10 @@ function storeFile(sourcePath, entityId) {
|
|
|
18106
18097
|
function getDocumentFilePath(entityId) {
|
|
18107
18098
|
if (!existsSync9(DOCUMENTS_DIR))
|
|
18108
18099
|
return null;
|
|
18109
|
-
const { readdirSync:
|
|
18110
|
-
const files =
|
|
18100
|
+
const { readdirSync: readdirSync5 } = __require("fs");
|
|
18101
|
+
const files = readdirSync5(DOCUMENTS_DIR);
|
|
18111
18102
|
const match = files.find((f) => f.startsWith(`${entityId}.`) && !f.endsWith(".enc"));
|
|
18112
|
-
return match ?
|
|
18103
|
+
return match ? join11(DOCUMENTS_DIR, match) : null;
|
|
18113
18104
|
}
|
|
18114
18105
|
function decryptFile(encPath) {
|
|
18115
18106
|
const key = requireVault();
|
|
@@ -18130,9 +18121,9 @@ var VAULT_DIR, VAULT_CONFIG, VAULT_SESSION, DOCUMENTS_DIR, SESSION_TTL_MS, _deri
|
|
|
18130
18121
|
var init_vault = __esm(() => {
|
|
18131
18122
|
init_database();
|
|
18132
18123
|
VAULT_DIR = getDataDir2();
|
|
18133
|
-
VAULT_CONFIG =
|
|
18134
|
-
VAULT_SESSION =
|
|
18135
|
-
DOCUMENTS_DIR =
|
|
18124
|
+
VAULT_CONFIG = join11(VAULT_DIR, "vault.json");
|
|
18125
|
+
VAULT_SESSION = join11(VAULT_DIR, ".vault-session");
|
|
18126
|
+
DOCUMENTS_DIR = join11(VAULT_DIR, "documents");
|
|
18136
18127
|
SESSION_TTL_MS = 30 * 60 * 1000;
|
|
18137
18128
|
});
|
|
18138
18129
|
|
|
@@ -18155,7 +18146,7 @@ function addDocument(input, db) {
|
|
|
18155
18146
|
if (input.file_path) {
|
|
18156
18147
|
filePath = storeFile(input.file_path, id);
|
|
18157
18148
|
}
|
|
18158
|
-
_db2.query(`INSERT INTO contact_documents (id, contact_id, doc_type, label, encrypted_value, iv, encrypted_file_path, metadata, expires_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(id, input.contact_id, input.doc_type, input.label ?? null, ciphertext, iv, filePath, JSON.stringify(input.metadata || {}), input.expires_at ?? null,
|
|
18149
|
+
_db2.query(`INSERT INTO contact_documents (id, contact_id, doc_type, label, encrypted_value, iv, encrypted_file_path, metadata, expires_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(id, input.contact_id, input.doc_type, input.label ?? null, ciphertext, iv, filePath, JSON.stringify(input.metadata || {}), input.expires_at ?? null, now2(), now2());
|
|
18159
18150
|
return getDocument(id, _db2);
|
|
18160
18151
|
}
|
|
18161
18152
|
function getDocument(id, db) {
|
|
@@ -18290,13 +18281,13 @@ function setHealthData(contactId, input, db) {
|
|
|
18290
18281
|
}
|
|
18291
18282
|
if (sets.length) {
|
|
18292
18283
|
sets.push("updated_at = ?");
|
|
18293
|
-
params.push(
|
|
18284
|
+
params.push(now2());
|
|
18294
18285
|
params.push(contactId);
|
|
18295
18286
|
_db2.query(`UPDATE contact_health SET ${sets.join(", ")} WHERE contact_id = ?`).run(...params);
|
|
18296
18287
|
}
|
|
18297
18288
|
} else {
|
|
18298
18289
|
const id = uuid();
|
|
18299
|
-
_db2.query(`INSERT INTO contact_health (id, contact_id, blood_type, allergies, medical_conditions, medications, emergency_contacts, health_insurance_provider, health_insurance_id, primary_physician, primary_physician_phone, organ_donor, notes, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.blood_type ?? null, JSON.stringify(input.allergies || []), JSON.stringify(input.medical_conditions || []), JSON.stringify(input.medications || []), JSON.stringify(input.emergency_contacts || []), input.health_insurance_provider ?? null, input.health_insurance_id ?? null, input.primary_physician ?? null, input.primary_physician_phone ?? null, input.organ_donor ? 1 : 0, input.notes ?? null,
|
|
18290
|
+
_db2.query(`INSERT INTO contact_health (id, contact_id, blood_type, allergies, medical_conditions, medications, emergency_contacts, health_insurance_provider, health_insurance_id, primary_physician, primary_physician_phone, organ_donor, notes, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.blood_type ?? null, JSON.stringify(input.allergies || []), JSON.stringify(input.medical_conditions || []), JSON.stringify(input.medications || []), JSON.stringify(input.emergency_contacts || []), input.health_insurance_provider ?? null, input.health_insurance_id ?? null, input.primary_physician ?? null, input.primary_physician_phone ?? null, input.organ_donor ? 1 : 0, input.notes ?? null, now2(), now2());
|
|
18300
18291
|
}
|
|
18301
18292
|
return getHealthData(contactId, _db2);
|
|
18302
18293
|
}
|
|
@@ -24627,12 +24618,12 @@ var init_register_tools = __esm(() => {
|
|
|
24627
24618
|
|
|
24628
24619
|
// src/mcp/index.ts
|
|
24629
24620
|
import { readFileSync as readFileSync7 } from "fs";
|
|
24630
|
-
import { join as
|
|
24621
|
+
import { join as join12 } from "path";
|
|
24631
24622
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24632
24623
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24633
24624
|
function getServerVersion() {
|
|
24634
24625
|
try {
|
|
24635
|
-
const packageJsonPath =
|
|
24626
|
+
const packageJsonPath = join12(import.meta.dir, "..", "..", "package.json");
|
|
24636
24627
|
const pkg = JSON.parse(readFileSync7(packageJsonPath, "utf8"));
|
|
24637
24628
|
return pkg.version ?? "0.0.0";
|
|
24638
24629
|
} catch {
|
|
@@ -24679,7 +24670,7 @@ __export(exports_serve, {
|
|
|
24679
24670
|
startServer: () => startServer
|
|
24680
24671
|
});
|
|
24681
24672
|
import { existsSync as existsSync12 } from "fs";
|
|
24682
|
-
import { join as
|
|
24673
|
+
import { join as join13 } from "path";
|
|
24683
24674
|
function json3(data, status = 200) {
|
|
24684
24675
|
return new Response(JSON.stringify(data), {
|
|
24685
24676
|
status,
|
|
@@ -24925,7 +24916,7 @@ async function handleImages(req, _url, segments) {
|
|
|
24925
24916
|
return apiError("No image file in form data");
|
|
24926
24917
|
const ext = file.name?.split(".").pop() || "jpg";
|
|
24927
24918
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
24928
|
-
const tmpPath =
|
|
24919
|
+
const tmpPath = join13(getImagesDir(), `_upload_${entityId}.${ext}`);
|
|
24929
24920
|
const { writeFileSync: wfs } = await import("fs");
|
|
24930
24921
|
wfs(tmpPath, buffer);
|
|
24931
24922
|
try {
|
|
@@ -25012,8 +25003,8 @@ function startServer(port) {
|
|
|
25012
25003
|
response = apiError("Not found", 404);
|
|
25013
25004
|
}
|
|
25014
25005
|
} else {
|
|
25015
|
-
const filePath =
|
|
25016
|
-
response = serveStaticFile(filePath) ?? serveStaticFile(
|
|
25006
|
+
const filePath = join13(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
|
|
25007
|
+
response = serveStaticFile(filePath) ?? serveStaticFile(join13(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
|
|
25017
25008
|
}
|
|
25018
25009
|
} catch (err) {
|
|
25019
25010
|
console.error("Request error:", err);
|
|
@@ -25037,9 +25028,693 @@ var init_serve = __esm(() => {
|
|
|
25037
25028
|
init_images();
|
|
25038
25029
|
init_http();
|
|
25039
25030
|
init_mcp();
|
|
25040
|
-
DASHBOARD_DIST =
|
|
25031
|
+
DASHBOARD_DIST = join13(import.meta.dir, "../../dashboard/dist");
|
|
25041
25032
|
});
|
|
25042
25033
|
|
|
25034
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
25035
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
25036
|
+
import { existsSync } from "fs";
|
|
25037
|
+
import { homedir } from "os";
|
|
25038
|
+
import { join } from "path";
|
|
25039
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
25040
|
+
import { randomUUID } from "crypto";
|
|
25041
|
+
import { spawn } from "child_process";
|
|
25042
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
25043
|
+
function getPathValue(input, path) {
|
|
25044
|
+
return path.split(".").reduce((value, part) => {
|
|
25045
|
+
if (value && typeof value === "object" && part in value) {
|
|
25046
|
+
return value[part];
|
|
25047
|
+
}
|
|
25048
|
+
return;
|
|
25049
|
+
}, input);
|
|
25050
|
+
}
|
|
25051
|
+
function wildcardToRegExp(pattern) {
|
|
25052
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
25053
|
+
return new RegExp(`^${escaped}$`);
|
|
25054
|
+
}
|
|
25055
|
+
function matchString(value, matcher) {
|
|
25056
|
+
if (matcher === undefined)
|
|
25057
|
+
return true;
|
|
25058
|
+
if (value === undefined)
|
|
25059
|
+
return false;
|
|
25060
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
25061
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
25062
|
+
}
|
|
25063
|
+
function matchRecord(input, matcher) {
|
|
25064
|
+
if (!matcher)
|
|
25065
|
+
return true;
|
|
25066
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
25067
|
+
const actual = getPathValue(input, path);
|
|
25068
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
25069
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
25070
|
+
}
|
|
25071
|
+
return actual === expected;
|
|
25072
|
+
});
|
|
25073
|
+
}
|
|
25074
|
+
function eventMatchesFilter(event, filter) {
|
|
25075
|
+
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
25076
|
+
}
|
|
25077
|
+
function channelMatchesEvent(channel, event) {
|
|
25078
|
+
if (!channel.enabled)
|
|
25079
|
+
return false;
|
|
25080
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
25081
|
+
return true;
|
|
25082
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
25083
|
+
}
|
|
25084
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
25085
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
25086
|
+
function getEventsDataDir(override) {
|
|
25087
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
25088
|
+
}
|
|
25089
|
+
|
|
25090
|
+
class JsonEventsStore {
|
|
25091
|
+
dataDir;
|
|
25092
|
+
channelsPath;
|
|
25093
|
+
eventsPath;
|
|
25094
|
+
deliveriesPath;
|
|
25095
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
25096
|
+
this.dataDir = dataDir;
|
|
25097
|
+
this.channelsPath = join(dataDir, "channels.json");
|
|
25098
|
+
this.eventsPath = join(dataDir, "events.json");
|
|
25099
|
+
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
25100
|
+
}
|
|
25101
|
+
async init() {
|
|
25102
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
25103
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
25104
|
+
return;
|
|
25105
|
+
});
|
|
25106
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
25107
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
25108
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
25109
|
+
}
|
|
25110
|
+
async addChannel(channel) {
|
|
25111
|
+
await this.init();
|
|
25112
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
25113
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
25114
|
+
if (index >= 0) {
|
|
25115
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
25116
|
+
} else {
|
|
25117
|
+
channels.push(channel);
|
|
25118
|
+
}
|
|
25119
|
+
await this.writeJson(this.channelsPath, channels);
|
|
25120
|
+
return index >= 0 ? channels[index] : channel;
|
|
25121
|
+
}
|
|
25122
|
+
async listChannels() {
|
|
25123
|
+
await this.init();
|
|
25124
|
+
return this.readJson(this.channelsPath, []);
|
|
25125
|
+
}
|
|
25126
|
+
async getChannel(id) {
|
|
25127
|
+
const channels = await this.listChannels();
|
|
25128
|
+
return channels.find((channel) => channel.id === id);
|
|
25129
|
+
}
|
|
25130
|
+
async removeChannel(id) {
|
|
25131
|
+
await this.init();
|
|
25132
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
25133
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
25134
|
+
await this.writeJson(this.channelsPath, next);
|
|
25135
|
+
return next.length !== channels.length;
|
|
25136
|
+
}
|
|
25137
|
+
async appendEvent(event) {
|
|
25138
|
+
await this.init();
|
|
25139
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
25140
|
+
events.push(event);
|
|
25141
|
+
await this.writeJson(this.eventsPath, events);
|
|
25142
|
+
return event;
|
|
25143
|
+
}
|
|
25144
|
+
async listEvents() {
|
|
25145
|
+
await this.init();
|
|
25146
|
+
return this.readJson(this.eventsPath, []);
|
|
25147
|
+
}
|
|
25148
|
+
async findEventByIdentity(identity) {
|
|
25149
|
+
const events = await this.listEvents();
|
|
25150
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
25151
|
+
}
|
|
25152
|
+
async appendDelivery(result) {
|
|
25153
|
+
await this.init();
|
|
25154
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
25155
|
+
deliveries.push(result);
|
|
25156
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
25157
|
+
return result;
|
|
25158
|
+
}
|
|
25159
|
+
async listDeliveries() {
|
|
25160
|
+
await this.init();
|
|
25161
|
+
return this.readJson(this.deliveriesPath, []);
|
|
25162
|
+
}
|
|
25163
|
+
async exportData() {
|
|
25164
|
+
return {
|
|
25165
|
+
channels: await this.listChannels(),
|
|
25166
|
+
events: await this.listEvents(),
|
|
25167
|
+
deliveries: await this.listDeliveries()
|
|
25168
|
+
};
|
|
25169
|
+
}
|
|
25170
|
+
async ensureArrayFile(path) {
|
|
25171
|
+
if (!existsSync(path)) {
|
|
25172
|
+
await writeFile(path, `[]
|
|
25173
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
25174
|
+
}
|
|
25175
|
+
await chmod(path, 384).catch(() => {
|
|
25176
|
+
return;
|
|
25177
|
+
});
|
|
25178
|
+
}
|
|
25179
|
+
async readJson(path, fallback) {
|
|
25180
|
+
try {
|
|
25181
|
+
const raw = await readFile(path, "utf-8");
|
|
25182
|
+
if (!raw.trim())
|
|
25183
|
+
return fallback;
|
|
25184
|
+
return JSON.parse(raw);
|
|
25185
|
+
} catch (error) {
|
|
25186
|
+
if (error.code === "ENOENT")
|
|
25187
|
+
return fallback;
|
|
25188
|
+
throw error;
|
|
25189
|
+
}
|
|
25190
|
+
}
|
|
25191
|
+
async writeJson(path, value) {
|
|
25192
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
25193
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
25194
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
25195
|
+
await rename(tempPath, path);
|
|
25196
|
+
await chmod(path, 384).catch(() => {
|
|
25197
|
+
return;
|
|
25198
|
+
});
|
|
25199
|
+
}
|
|
25200
|
+
}
|
|
25201
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
25202
|
+
function buildSignatureBase(timestamp, body) {
|
|
25203
|
+
return `${timestamp}.${body}`;
|
|
25204
|
+
}
|
|
25205
|
+
function signPayload(secret, timestamp, body) {
|
|
25206
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
25207
|
+
return `sha256=${digest}`;
|
|
25208
|
+
}
|
|
25209
|
+
function now() {
|
|
25210
|
+
return new Date().toISOString();
|
|
25211
|
+
}
|
|
25212
|
+
function truncate(value, max = 4096) {
|
|
25213
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
25214
|
+
}
|
|
25215
|
+
function buildWebhookRequest(event, channel) {
|
|
25216
|
+
if (!channel.webhook)
|
|
25217
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
25218
|
+
const body = JSON.stringify(event);
|
|
25219
|
+
const timestamp = event.time;
|
|
25220
|
+
const headers = {
|
|
25221
|
+
"Content-Type": "application/json",
|
|
25222
|
+
"User-Agent": "@hasna/events",
|
|
25223
|
+
"X-Hasna-Event-Id": event.id,
|
|
25224
|
+
"X-Hasna-Event-Type": event.type,
|
|
25225
|
+
"X-Hasna-Timestamp": timestamp,
|
|
25226
|
+
...channel.webhook.headers
|
|
25227
|
+
};
|
|
25228
|
+
if (channel.webhook.secret) {
|
|
25229
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
25230
|
+
}
|
|
25231
|
+
return { body, headers };
|
|
25232
|
+
}
|
|
25233
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
25234
|
+
if (!channel.webhook)
|
|
25235
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
25236
|
+
const startedAt = now();
|
|
25237
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
25238
|
+
const controller = new AbortController;
|
|
25239
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
25240
|
+
try {
|
|
25241
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
25242
|
+
method: "POST",
|
|
25243
|
+
headers,
|
|
25244
|
+
body,
|
|
25245
|
+
signal: controller.signal
|
|
25246
|
+
});
|
|
25247
|
+
const responseBody = truncate(await response.text());
|
|
25248
|
+
return {
|
|
25249
|
+
attempt: 1,
|
|
25250
|
+
status: response.ok ? "success" : "failed",
|
|
25251
|
+
startedAt,
|
|
25252
|
+
completedAt: now(),
|
|
25253
|
+
responseStatus: response.status,
|
|
25254
|
+
responseBody,
|
|
25255
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
25256
|
+
};
|
|
25257
|
+
} catch (error) {
|
|
25258
|
+
return {
|
|
25259
|
+
attempt: 1,
|
|
25260
|
+
status: "failed",
|
|
25261
|
+
startedAt,
|
|
25262
|
+
completedAt: now(),
|
|
25263
|
+
error: error instanceof Error ? error.message : String(error)
|
|
25264
|
+
};
|
|
25265
|
+
} finally {
|
|
25266
|
+
clearTimeout(timeout);
|
|
25267
|
+
}
|
|
25268
|
+
}
|
|
25269
|
+
async function dispatchCommand(event, channel) {
|
|
25270
|
+
if (!channel.command)
|
|
25271
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
25272
|
+
const startedAt = now();
|
|
25273
|
+
const eventJson = JSON.stringify(event);
|
|
25274
|
+
const env = {
|
|
25275
|
+
...process.env,
|
|
25276
|
+
...channel.command.env,
|
|
25277
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
25278
|
+
HASNA_EVENT_ID: event.id,
|
|
25279
|
+
HASNA_EVENT_TYPE: event.type,
|
|
25280
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
25281
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
25282
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
25283
|
+
HASNA_EVENT_TIME: event.time,
|
|
25284
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
25285
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
25286
|
+
HASNA_EVENT_JSON: eventJson
|
|
25287
|
+
};
|
|
25288
|
+
return new Promise((resolve) => {
|
|
25289
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
25290
|
+
cwd: channel.command.cwd,
|
|
25291
|
+
env,
|
|
25292
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
25293
|
+
});
|
|
25294
|
+
let stdout = "";
|
|
25295
|
+
let stderr = "";
|
|
25296
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
25297
|
+
child.stdin.end(eventJson);
|
|
25298
|
+
child.stdout.on("data", (chunk) => {
|
|
25299
|
+
stdout += chunk.toString();
|
|
25300
|
+
});
|
|
25301
|
+
child.stderr.on("data", (chunk) => {
|
|
25302
|
+
stderr += chunk.toString();
|
|
25303
|
+
});
|
|
25304
|
+
child.on("error", (error) => {
|
|
25305
|
+
clearTimeout(timeout);
|
|
25306
|
+
resolve({
|
|
25307
|
+
attempt: 1,
|
|
25308
|
+
status: "failed",
|
|
25309
|
+
startedAt,
|
|
25310
|
+
completedAt: now(),
|
|
25311
|
+
stdout: truncate(stdout),
|
|
25312
|
+
stderr: truncate(stderr),
|
|
25313
|
+
error: error.message
|
|
25314
|
+
});
|
|
25315
|
+
});
|
|
25316
|
+
child.on("close", (code, signal) => {
|
|
25317
|
+
clearTimeout(timeout);
|
|
25318
|
+
const success = code === 0;
|
|
25319
|
+
resolve({
|
|
25320
|
+
attempt: 1,
|
|
25321
|
+
status: success ? "success" : "failed",
|
|
25322
|
+
startedAt,
|
|
25323
|
+
completedAt: now(),
|
|
25324
|
+
stdout: truncate(stdout),
|
|
25325
|
+
stderr: truncate(stderr),
|
|
25326
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
25327
|
+
});
|
|
25328
|
+
});
|
|
25329
|
+
});
|
|
25330
|
+
}
|
|
25331
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
25332
|
+
if (channel.transport === "webhook")
|
|
25333
|
+
return dispatchWebhook(event, channel, options);
|
|
25334
|
+
if (channel.transport === "command")
|
|
25335
|
+
return dispatchCommand(event, channel);
|
|
25336
|
+
return {
|
|
25337
|
+
attempt: 1,
|
|
25338
|
+
status: "skipped",
|
|
25339
|
+
startedAt: now(),
|
|
25340
|
+
completedAt: now(),
|
|
25341
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
25342
|
+
};
|
|
25343
|
+
}
|
|
25344
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
25345
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
25346
|
+
return {
|
|
25347
|
+
id: randomUUID(),
|
|
25348
|
+
eventId: event.id,
|
|
25349
|
+
channelId: channel.id,
|
|
25350
|
+
transport: channel.transport,
|
|
25351
|
+
status,
|
|
25352
|
+
attempts,
|
|
25353
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
25354
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
25355
|
+
};
|
|
25356
|
+
}
|
|
25357
|
+
function createEvent(input) {
|
|
25358
|
+
return {
|
|
25359
|
+
id: input.id ?? randomUUID2(),
|
|
25360
|
+
source: input.source,
|
|
25361
|
+
type: input.type,
|
|
25362
|
+
time: normalizeTime(input.time),
|
|
25363
|
+
subject: input.subject,
|
|
25364
|
+
severity: input.severity ?? "info",
|
|
25365
|
+
data: input.data ?? {},
|
|
25366
|
+
message: input.message,
|
|
25367
|
+
dedupeKey: input.dedupeKey,
|
|
25368
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
25369
|
+
metadata: input.metadata ?? {}
|
|
25370
|
+
};
|
|
25371
|
+
}
|
|
25372
|
+
|
|
25373
|
+
class EventsClient {
|
|
25374
|
+
store;
|
|
25375
|
+
redactors;
|
|
25376
|
+
transportOptions;
|
|
25377
|
+
constructor(options = {}) {
|
|
25378
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
25379
|
+
this.redactors = options.redactors ?? [];
|
|
25380
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
25381
|
+
}
|
|
25382
|
+
async addChannel(input) {
|
|
25383
|
+
const timestamp = new Date().toISOString();
|
|
25384
|
+
return this.store.addChannel({
|
|
25385
|
+
...input,
|
|
25386
|
+
createdAt: input.createdAt ?? timestamp,
|
|
25387
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
25388
|
+
});
|
|
25389
|
+
}
|
|
25390
|
+
async listChannels() {
|
|
25391
|
+
return this.store.listChannels();
|
|
25392
|
+
}
|
|
25393
|
+
async removeChannel(id) {
|
|
25394
|
+
return this.store.removeChannel(id);
|
|
25395
|
+
}
|
|
25396
|
+
async emit(input, options = {}) {
|
|
25397
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
25398
|
+
if (options.dedupe !== false) {
|
|
25399
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
25400
|
+
if (existing) {
|
|
25401
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
25402
|
+
}
|
|
25403
|
+
}
|
|
25404
|
+
await this.store.appendEvent(event);
|
|
25405
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
25406
|
+
return { event, deliveries, deduped: false };
|
|
25407
|
+
}
|
|
25408
|
+
async listEvents() {
|
|
25409
|
+
return this.store.listEvents();
|
|
25410
|
+
}
|
|
25411
|
+
async listDeliveries() {
|
|
25412
|
+
return this.store.listDeliveries();
|
|
25413
|
+
}
|
|
25414
|
+
async deliver(event) {
|
|
25415
|
+
const channels = await this.store.listChannels();
|
|
25416
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
25417
|
+
const deliveries = [];
|
|
25418
|
+
for (const channel of selected) {
|
|
25419
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
25420
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
25421
|
+
await this.store.appendDelivery(result);
|
|
25422
|
+
deliveries.push(result);
|
|
25423
|
+
}
|
|
25424
|
+
return deliveries;
|
|
25425
|
+
}
|
|
25426
|
+
async testChannel(id, input = {}) {
|
|
25427
|
+
const channel = await this.store.getChannel(id);
|
|
25428
|
+
if (!channel)
|
|
25429
|
+
throw new Error(`Channel not found: ${id}`);
|
|
25430
|
+
const event = createEvent({
|
|
25431
|
+
source: input.source ?? "hasna.events",
|
|
25432
|
+
type: input.type ?? "events.test",
|
|
25433
|
+
subject: input.subject ?? id,
|
|
25434
|
+
severity: input.severity ?? "info",
|
|
25435
|
+
data: input.data ?? { test: true },
|
|
25436
|
+
message: input.message ?? "Hasna events test delivery",
|
|
25437
|
+
dedupeKey: input.dedupeKey,
|
|
25438
|
+
schemaVersion: input.schemaVersion,
|
|
25439
|
+
metadata: input.metadata,
|
|
25440
|
+
time: input.time,
|
|
25441
|
+
id: input.id
|
|
25442
|
+
});
|
|
25443
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
25444
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
25445
|
+
await this.store.appendDelivery(result);
|
|
25446
|
+
return result;
|
|
25447
|
+
}
|
|
25448
|
+
async replay(options = {}) {
|
|
25449
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
25450
|
+
if (options.eventId && event.id !== options.eventId)
|
|
25451
|
+
return false;
|
|
25452
|
+
if (options.source && event.source !== options.source)
|
|
25453
|
+
return false;
|
|
25454
|
+
if (options.type && event.type !== options.type)
|
|
25455
|
+
return false;
|
|
25456
|
+
return true;
|
|
25457
|
+
});
|
|
25458
|
+
if (options.dryRun)
|
|
25459
|
+
return { events, deliveries: [] };
|
|
25460
|
+
const deliveries = [];
|
|
25461
|
+
for (const event of events) {
|
|
25462
|
+
deliveries.push(...await this.deliver(event));
|
|
25463
|
+
}
|
|
25464
|
+
return { events, deliveries };
|
|
25465
|
+
}
|
|
25466
|
+
async applyRedaction(event, channel) {
|
|
25467
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
25468
|
+
for (const redactor of this.redactors) {
|
|
25469
|
+
next = await redactor(next, channel);
|
|
25470
|
+
}
|
|
25471
|
+
return next;
|
|
25472
|
+
}
|
|
25473
|
+
async deliverWithRetry(event, channel) {
|
|
25474
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
25475
|
+
const attempts = [];
|
|
25476
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
25477
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
25478
|
+
attempt.attempt = index + 1;
|
|
25479
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
25480
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
25481
|
+
}
|
|
25482
|
+
attempts.push(attempt);
|
|
25483
|
+
if (attempt.status !== "failed")
|
|
25484
|
+
break;
|
|
25485
|
+
if (attempt.nextBackoffMs)
|
|
25486
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
25487
|
+
}
|
|
25488
|
+
return createDeliveryResult(event, channel, attempts);
|
|
25489
|
+
}
|
|
25490
|
+
}
|
|
25491
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
25492
|
+
if (paths.length === 0)
|
|
25493
|
+
return event;
|
|
25494
|
+
const copy = structuredClone(event);
|
|
25495
|
+
for (const path of paths) {
|
|
25496
|
+
setPath(copy, path, replacement);
|
|
25497
|
+
}
|
|
25498
|
+
return copy;
|
|
25499
|
+
}
|
|
25500
|
+
function sanitizeChannelForOutput(channel) {
|
|
25501
|
+
const copy = structuredClone(channel);
|
|
25502
|
+
if (copy.webhook?.secret)
|
|
25503
|
+
copy.webhook.secret = "[REDACTED]";
|
|
25504
|
+
if (copy.command?.env) {
|
|
25505
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
25506
|
+
}
|
|
25507
|
+
return copy;
|
|
25508
|
+
}
|
|
25509
|
+
function sanitizeChannelsForOutput(channels) {
|
|
25510
|
+
return channels.map(sanitizeChannelForOutput);
|
|
25511
|
+
}
|
|
25512
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
25513
|
+
return redactValue(event, replacement);
|
|
25514
|
+
}
|
|
25515
|
+
function shouldRedactKey(key) {
|
|
25516
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
25517
|
+
}
|
|
25518
|
+
function redactValue(value, replacement) {
|
|
25519
|
+
if (Array.isArray(value))
|
|
25520
|
+
return value.map((item) => redactValue(item, replacement));
|
|
25521
|
+
if (!value || typeof value !== "object")
|
|
25522
|
+
return value;
|
|
25523
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
25524
|
+
key,
|
|
25525
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
25526
|
+
]));
|
|
25527
|
+
}
|
|
25528
|
+
function setPath(input, path, replacement) {
|
|
25529
|
+
const parts = path.split(".");
|
|
25530
|
+
let cursor = input;
|
|
25531
|
+
for (const part of parts.slice(0, -1)) {
|
|
25532
|
+
const next = cursor[part];
|
|
25533
|
+
if (!next || typeof next !== "object")
|
|
25534
|
+
return;
|
|
25535
|
+
cursor = next;
|
|
25536
|
+
}
|
|
25537
|
+
const last = parts.at(-1);
|
|
25538
|
+
if (last && last in cursor)
|
|
25539
|
+
cursor[last] = replacement;
|
|
25540
|
+
}
|
|
25541
|
+
function normalizeTime(value) {
|
|
25542
|
+
if (!value)
|
|
25543
|
+
return new Date().toISOString();
|
|
25544
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
25545
|
+
}
|
|
25546
|
+
function normalizeRetryPolicy(policy) {
|
|
25547
|
+
return {
|
|
25548
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
25549
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
25550
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
25551
|
+
};
|
|
25552
|
+
}
|
|
25553
|
+
function parseJsonObject(value, fallback) {
|
|
25554
|
+
if (!value)
|
|
25555
|
+
return fallback;
|
|
25556
|
+
const parsed = JSON.parse(value);
|
|
25557
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25558
|
+
throw new Error("Expected a JSON object");
|
|
25559
|
+
}
|
|
25560
|
+
return parsed;
|
|
25561
|
+
}
|
|
25562
|
+
function parseHeaders(values) {
|
|
25563
|
+
if (!values?.length)
|
|
25564
|
+
return;
|
|
25565
|
+
const headers = {};
|
|
25566
|
+
for (const value of values) {
|
|
25567
|
+
const separator = value.indexOf("=");
|
|
25568
|
+
if (separator === -1)
|
|
25569
|
+
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
25570
|
+
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
25571
|
+
}
|
|
25572
|
+
return headers;
|
|
25573
|
+
}
|
|
25574
|
+
function parseFilter(options) {
|
|
25575
|
+
const filter2 = {};
|
|
25576
|
+
if (options.source)
|
|
25577
|
+
filter2.source = options.source;
|
|
25578
|
+
if (options.type)
|
|
25579
|
+
filter2.type = options.type;
|
|
25580
|
+
if (options.subject)
|
|
25581
|
+
filter2.subject = options.subject;
|
|
25582
|
+
if (options.severity)
|
|
25583
|
+
filter2.severity = options.severity;
|
|
25584
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
25585
|
+
}
|
|
25586
|
+
function createClient(options) {
|
|
25587
|
+
if (options.createClient)
|
|
25588
|
+
return options.createClient();
|
|
25589
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
25590
|
+
}
|
|
25591
|
+
function print(value, json, text) {
|
|
25592
|
+
if (json)
|
|
25593
|
+
console.log(JSON.stringify(value, null, 2));
|
|
25594
|
+
else
|
|
25595
|
+
console.log(text);
|
|
25596
|
+
}
|
|
25597
|
+
function hasJsonOption(options) {
|
|
25598
|
+
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
25599
|
+
}
|
|
25600
|
+
function wantsJson(actionOptions, command) {
|
|
25601
|
+
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
25602
|
+
}
|
|
25603
|
+
function registerWebhookCommands(program, options) {
|
|
25604
|
+
const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
25605
|
+
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
|
|
25606
|
+
const timestamp = new Date().toISOString();
|
|
25607
|
+
const channel = {
|
|
25608
|
+
id: actionOptions.id,
|
|
25609
|
+
name: actionOptions.name,
|
|
25610
|
+
enabled: !actionOptions.disabled,
|
|
25611
|
+
transport: actionOptions.transport,
|
|
25612
|
+
filters: parseFilter(actionOptions),
|
|
25613
|
+
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
25614
|
+
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
25615
|
+
createdAt: timestamp,
|
|
25616
|
+
updatedAt: timestamp
|
|
25617
|
+
};
|
|
25618
|
+
if (actionOptions.transport === "webhook") {
|
|
25619
|
+
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
25620
|
+
} else if (actionOptions.transport === "command") {
|
|
25621
|
+
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
25622
|
+
} else {
|
|
25623
|
+
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
25624
|
+
}
|
|
25625
|
+
const saved = await createClient(options).addChannel(channel);
|
|
25626
|
+
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
25627
|
+
});
|
|
25628
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
25629
|
+
const channels = await createClient(options).listChannels();
|
|
25630
|
+
if (wantsJson(actionOptions, command)) {
|
|
25631
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
25632
|
+
return;
|
|
25633
|
+
}
|
|
25634
|
+
if (!channels.length) {
|
|
25635
|
+
console.log("No channels configured.");
|
|
25636
|
+
return;
|
|
25637
|
+
}
|
|
25638
|
+
for (const channel of channels) {
|
|
25639
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
25640
|
+
}
|
|
25641
|
+
});
|
|
25642
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
25643
|
+
const removed = await createClient(options).removeChannel(id);
|
|
25644
|
+
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
25645
|
+
});
|
|
25646
|
+
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
25647
|
+
const result = await createClient(options).testChannel(id, {
|
|
25648
|
+
source: options.source,
|
|
25649
|
+
type: actionOptions.type,
|
|
25650
|
+
subject: actionOptions.subject ?? id,
|
|
25651
|
+
message: actionOptions.message,
|
|
25652
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
25653
|
+
});
|
|
25654
|
+
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
25655
|
+
});
|
|
25656
|
+
return webhooks;
|
|
25657
|
+
}
|
|
25658
|
+
function registerEventCommands(program, options) {
|
|
25659
|
+
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
25660
|
+
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
|
|
25661
|
+
const result = await createClient(options).emit({
|
|
25662
|
+
source: actionOptions.source ?? options.source,
|
|
25663
|
+
type,
|
|
25664
|
+
subject: actionOptions.subject,
|
|
25665
|
+
severity: actionOptions.severity,
|
|
25666
|
+
message: actionOptions.message,
|
|
25667
|
+
dedupeKey: actionOptions.dedupeKey,
|
|
25668
|
+
data: parseJsonObject(actionOptions.data, {}),
|
|
25669
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
25670
|
+
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
25671
|
+
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
25672
|
+
});
|
|
25673
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
25674
|
+
let rows = await createClient(options).listEvents();
|
|
25675
|
+
if (actionOptions.source)
|
|
25676
|
+
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
25677
|
+
if (actionOptions.type)
|
|
25678
|
+
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
25679
|
+
if (actionOptions.limit)
|
|
25680
|
+
rows = rows.slice(-actionOptions.limit);
|
|
25681
|
+
if (wantsJson(actionOptions, command)) {
|
|
25682
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
25683
|
+
return;
|
|
25684
|
+
}
|
|
25685
|
+
if (!rows.length) {
|
|
25686
|
+
console.log("No events recorded.");
|
|
25687
|
+
return;
|
|
25688
|
+
}
|
|
25689
|
+
for (const event of rows)
|
|
25690
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
25691
|
+
});
|
|
25692
|
+
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
25693
|
+
const result = await createClient(options).replay({
|
|
25694
|
+
eventId: actionOptions.id,
|
|
25695
|
+
source: actionOptions.source,
|
|
25696
|
+
type: actionOptions.type,
|
|
25697
|
+
dryRun: actionOptions.dryRun
|
|
25698
|
+
});
|
|
25699
|
+
print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
25700
|
+
});
|
|
25701
|
+
return events;
|
|
25702
|
+
}
|
|
25703
|
+
function registerEventsCommands(program, options) {
|
|
25704
|
+
registerWebhookCommands(program, options);
|
|
25705
|
+
registerEventCommands(program, options);
|
|
25706
|
+
}
|
|
25707
|
+
function parseNumber(value) {
|
|
25708
|
+
const parsed = Number(value);
|
|
25709
|
+
if (!Number.isFinite(parsed))
|
|
25710
|
+
throw new Error(`Expected a number, got ${value}`);
|
|
25711
|
+
return parsed;
|
|
25712
|
+
}
|
|
25713
|
+
function collectValues(value, previous) {
|
|
25714
|
+
previous.push(value);
|
|
25715
|
+
return previous;
|
|
25716
|
+
}
|
|
25717
|
+
|
|
25043
25718
|
// node_modules/commander/esm.mjs
|
|
25044
25719
|
var import__ = __toESM(require_commander(), 1);
|
|
25045
25720
|
var {
|
|
@@ -25069,12 +25744,12 @@ import chalk2 from "chalk";
|
|
|
25069
25744
|
|
|
25070
25745
|
// src/lib/config.ts
|
|
25071
25746
|
init_database();
|
|
25072
|
-
import { existsSync as
|
|
25747
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
25073
25748
|
import { join as join7 } from "path";
|
|
25074
25749
|
var CONFIG_DIR3 = getDataDir2();
|
|
25075
25750
|
var CONFIG_FILE = join7(CONFIG_DIR3, "config.json");
|
|
25076
25751
|
function readConfig() {
|
|
25077
|
-
if (!
|
|
25752
|
+
if (!existsSync4(CONFIG_FILE))
|
|
25078
25753
|
return {};
|
|
25079
25754
|
try {
|
|
25080
25755
|
return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
|
|
@@ -25085,7 +25760,7 @@ function readConfig() {
|
|
|
25085
25760
|
|
|
25086
25761
|
// src/cli/commands/core.tsx
|
|
25087
25762
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync13, copyFileSync as copyFileSync4, statSync as statSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync6 } from "fs";
|
|
25088
|
-
import { extname as extname3, join as
|
|
25763
|
+
import { extname as extname3, join as join14 } from "path";
|
|
25089
25764
|
|
|
25090
25765
|
// src/cli/utils.tsx
|
|
25091
25766
|
import chalk from "chalk";
|
|
@@ -25859,7 +26534,7 @@ Group not found: ${groupId}
|
|
|
25859
26534
|
});
|
|
25860
26535
|
program2.command("backup").description("Backup the contacts database").option("--output <path>", "Output path").option("--list", "List existing backups").action((opts) => {
|
|
25861
26536
|
const { getDataDir: getDataDir3 } = (init_database(), __toCommonJS(exports_database));
|
|
25862
|
-
const backupDir =
|
|
26537
|
+
const backupDir = join14(getDataDir3(), "backups");
|
|
25863
26538
|
if (opts.list) {
|
|
25864
26539
|
if (!existsSync13(backupDir)) {
|
|
25865
26540
|
console.log(chalk2.gray(`
|
|
@@ -25878,7 +26553,7 @@ No backups found.
|
|
|
25878
26553
|
Existing Backups:
|
|
25879
26554
|
`));
|
|
25880
26555
|
for (const f of files) {
|
|
25881
|
-
const filePath =
|
|
26556
|
+
const filePath = join14(backupDir, f);
|
|
25882
26557
|
const size2 = statSync2(filePath).size;
|
|
25883
26558
|
const mtime = statSync2(filePath).mtime.toISOString().slice(0, 19).replace("T", " ");
|
|
25884
26559
|
console.log(` ${chalk2.cyan(f)} ${chalk2.gray(`${(size2 / 1024).toFixed(1)} KB ${mtime}`)}`);
|
|
@@ -25895,7 +26570,7 @@ Database not found: ${src}
|
|
|
25895
26570
|
}
|
|
25896
26571
|
mkdirSync8(backupDir, { recursive: true });
|
|
25897
26572
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
25898
|
-
const dest = opts.output ||
|
|
26573
|
+
const dest = opts.output || join14(backupDir, `contacts-${ts}.db`);
|
|
25899
26574
|
copyFileSync4(src, dest);
|
|
25900
26575
|
const size = statSync2(dest).size;
|
|
25901
26576
|
console.log(chalk2.green(`
|
|
@@ -27577,4 +28252,5 @@ registerCoreCommands(program);
|
|
|
27577
28252
|
registerCrmCommands(program);
|
|
27578
28253
|
registerAdvancedCommands(program);
|
|
27579
28254
|
registerCloudCommands(program, "contacts");
|
|
28255
|
+
registerEventsCommands(program, { source: "contacts", eventsCommandName: "event-bus" });
|
|
27580
28256
|
program.parse(process.argv);
|