@hasna/brains 0.0.26 → 0.0.28
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 +829 -157
- package/dist/index.js +33 -35
- package/dist/mcp/index.js +53 -57
- package/dist/server/index.js +36 -38
- package/package.json +3 -2
package/dist/cli/index.js
CHANGED
|
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
993
993
|
this._exitCallback = (err) => {
|
|
994
994
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
995
995
|
throw err;
|
|
996
|
-
}
|
|
996
|
+
}
|
|
997
997
|
};
|
|
998
998
|
}
|
|
999
999
|
return this;
|
|
@@ -2137,8 +2137,8 @@ __export(exports_dist, {
|
|
|
2137
2137
|
ensureConflictsTable: () => ensureConflictsTable,
|
|
2138
2138
|
ensureAllPgDatabases: () => ensureAllPgDatabases,
|
|
2139
2139
|
enableAutoSync: () => enableAutoSync,
|
|
2140
|
-
discoverSyncableServicesV2: () =>
|
|
2141
|
-
discoverSyncableServices: () =>
|
|
2140
|
+
discoverSyncableServicesV2: () => discoverSyncableServices2,
|
|
2141
|
+
discoverSyncableServices: () => discoverSyncableServices,
|
|
2142
2142
|
discoverServices: () => discoverServices,
|
|
2143
2143
|
detectConflicts: () => detectConflicts,
|
|
2144
2144
|
createDatabase: () => createDatabase,
|
|
@@ -2154,28 +2154,28 @@ __export(exports_dist, {
|
|
|
2154
2154
|
import { createRequire } from "module";
|
|
2155
2155
|
import { Database as Database2 } from "bun:sqlite";
|
|
2156
2156
|
import {
|
|
2157
|
-
existsSync,
|
|
2157
|
+
existsSync as existsSync2,
|
|
2158
2158
|
mkdirSync,
|
|
2159
2159
|
readdirSync,
|
|
2160
2160
|
copyFileSync
|
|
2161
2161
|
} from "fs";
|
|
2162
|
-
import { homedir } from "os";
|
|
2163
|
-
import { join, relative } from "path";
|
|
2164
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
2165
2162
|
import { homedir as homedir2 } from "os";
|
|
2166
|
-
import { join as join2 } from "path";
|
|
2167
|
-
import {
|
|
2168
|
-
import {
|
|
2169
|
-
import {
|
|
2163
|
+
import { join as join2, relative } from "path";
|
|
2164
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
2165
|
+
import { homedir as homedir22 } from "os";
|
|
2166
|
+
import { join as join22 } from "path";
|
|
2167
|
+
import { readdirSync as readdirSync3, existsSync as existsSync6 } from "fs";
|
|
2168
|
+
import { join as join6 } from "path";
|
|
2169
|
+
import { homedir as homedir5 } from "os";
|
|
2170
2170
|
import { hostname } from "os";
|
|
2171
|
-
import { existsSync as
|
|
2172
|
-
import { homedir as
|
|
2171
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2172
|
+
import { homedir as homedir3 } from "os";
|
|
2173
|
+
import { join as join3 } from "path";
|
|
2174
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
2173
2175
|
import { join as join4 } from "path";
|
|
2174
|
-
import {
|
|
2175
|
-
import {
|
|
2176
|
-
import {
|
|
2177
|
-
import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2178
|
-
import { homedir as homedir5, platform } from "os";
|
|
2176
|
+
import { join as join5, dirname } from "path";
|
|
2177
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2178
|
+
import { homedir as homedir4, platform } from "os";
|
|
2179
2179
|
function __accessProp2(key) {
|
|
2180
2180
|
return this[key];
|
|
2181
2181
|
}
|
|
@@ -3057,20 +3057,20 @@ function custom(check, _params = {}, fatal) {
|
|
|
3057
3057
|
return ZodAny.create();
|
|
3058
3058
|
}
|
|
3059
3059
|
function getDataDir(serviceName) {
|
|
3060
|
-
const dir =
|
|
3060
|
+
const dir = join2(HASNA_DIR, serviceName);
|
|
3061
3061
|
mkdirSync(dir, { recursive: true });
|
|
3062
3062
|
return dir;
|
|
3063
3063
|
}
|
|
3064
3064
|
function getDbPath(serviceName) {
|
|
3065
3065
|
const dir = getDataDir(serviceName);
|
|
3066
|
-
return
|
|
3066
|
+
return join2(dir, `${serviceName}.db`);
|
|
3067
3067
|
}
|
|
3068
3068
|
function migrateDotfile(serviceName) {
|
|
3069
|
-
const legacyDir =
|
|
3070
|
-
const newDir =
|
|
3071
|
-
if (!
|
|
3069
|
+
const legacyDir = join2(homedir2(), `.${serviceName}`);
|
|
3070
|
+
const newDir = join2(HASNA_DIR, serviceName);
|
|
3071
|
+
if (!existsSync2(legacyDir))
|
|
3072
3072
|
return [];
|
|
3073
|
-
if (
|
|
3073
|
+
if (existsSync2(newDir))
|
|
3074
3074
|
return [];
|
|
3075
3075
|
mkdirSync(newDir, { recursive: true });
|
|
3076
3076
|
const migrated = [];
|
|
@@ -3080,8 +3080,8 @@ function migrateDotfile(serviceName) {
|
|
|
3080
3080
|
function copyDirRecursive(src, dest, root, migrated) {
|
|
3081
3081
|
const entries = readdirSync(src, { withFileTypes: true });
|
|
3082
3082
|
for (const entry of entries) {
|
|
3083
|
-
const srcPath =
|
|
3084
|
-
const destPath =
|
|
3083
|
+
const srcPath = join2(src, entry.name);
|
|
3084
|
+
const destPath = join2(dest, entry.name);
|
|
3085
3085
|
if (entry.isDirectory()) {
|
|
3086
3086
|
mkdirSync(destPath, { recursive: true });
|
|
3087
3087
|
copyDirRecursive(srcPath, destPath, root, migrated);
|
|
@@ -3092,7 +3092,7 @@ function copyDirRecursive(src, dest, root, migrated) {
|
|
|
3092
3092
|
}
|
|
3093
3093
|
}
|
|
3094
3094
|
function hasLegacyDotfile(serviceName) {
|
|
3095
|
-
return
|
|
3095
|
+
return existsSync2(join2(homedir2(), `.${serviceName}`));
|
|
3096
3096
|
}
|
|
3097
3097
|
function getHasnaDir() {
|
|
3098
3098
|
mkdirSync(HASNA_DIR, { recursive: true });
|
|
@@ -3105,7 +3105,7 @@ function getConfigPath() {
|
|
|
3105
3105
|
return CONFIG_PATH;
|
|
3106
3106
|
}
|
|
3107
3107
|
function getCloudConfig() {
|
|
3108
|
-
if (!
|
|
3108
|
+
if (!existsSync22(CONFIG_PATH)) {
|
|
3109
3109
|
return CloudConfigSchema.parse({});
|
|
3110
3110
|
}
|
|
3111
3111
|
try {
|
|
@@ -3147,11 +3147,11 @@ function isSyncExcludedTable(table) {
|
|
|
3147
3147
|
return SYNC_EXCLUDED_TABLE_PATTERNS.some((p) => p.test(table));
|
|
3148
3148
|
}
|
|
3149
3149
|
function discoverServices() {
|
|
3150
|
-
const dataDir =
|
|
3151
|
-
if (!
|
|
3150
|
+
const dataDir = join6(homedir5(), ".hasna");
|
|
3151
|
+
if (!existsSync6(dataDir))
|
|
3152
3152
|
return [];
|
|
3153
3153
|
try {
|
|
3154
|
-
const entries =
|
|
3154
|
+
const entries = readdirSync3(dataDir, { withFileTypes: true });
|
|
3155
3155
|
return entries.filter((e) => {
|
|
3156
3156
|
if (!e.isDirectory())
|
|
3157
3157
|
return false;
|
|
@@ -3163,30 +3163,30 @@ function discoverServices() {
|
|
|
3163
3163
|
return [];
|
|
3164
3164
|
}
|
|
3165
3165
|
}
|
|
3166
|
-
function
|
|
3166
|
+
function discoverSyncableServices2() {
|
|
3167
3167
|
const local = discoverServices();
|
|
3168
3168
|
const pgSet = new Set(KNOWN_PG_SERVICES);
|
|
3169
3169
|
return local.filter((s) => pgSet.has(s));
|
|
3170
3170
|
}
|
|
3171
3171
|
function getServiceDbPath(service) {
|
|
3172
|
-
const dataDir =
|
|
3173
|
-
if (!
|
|
3172
|
+
const dataDir = join6(homedir5(), ".hasna", service);
|
|
3173
|
+
if (!existsSync6(dataDir))
|
|
3174
3174
|
return null;
|
|
3175
3175
|
const candidates = [
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3176
|
+
join6(dataDir, `${service}.db`),
|
|
3177
|
+
join6(dataDir, "data.db"),
|
|
3178
|
+
join6(dataDir, "database.db")
|
|
3179
3179
|
];
|
|
3180
3180
|
try {
|
|
3181
|
-
const files =
|
|
3181
|
+
const files = readdirSync3(dataDir);
|
|
3182
3182
|
for (const f of files) {
|
|
3183
3183
|
if (f.endsWith(".db") && !f.endsWith("-wal") && !f.endsWith("-shm")) {
|
|
3184
|
-
candidates.push(
|
|
3184
|
+
candidates.push(join6(dataDir, f));
|
|
3185
3185
|
}
|
|
3186
3186
|
}
|
|
3187
3187
|
} catch {}
|
|
3188
3188
|
for (const p of candidates) {
|
|
3189
|
-
if (
|
|
3189
|
+
if (existsSync6(p))
|
|
3190
3190
|
return p;
|
|
3191
3191
|
}
|
|
3192
3192
|
return null;
|
|
@@ -3493,9 +3493,9 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
3493
3493
|
const batch = rows.slice(offset, offset + batchSize);
|
|
3494
3494
|
try {
|
|
3495
3495
|
if (isAsyncAdapter(target)) {
|
|
3496
|
-
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch
|
|
3496
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
3497
3497
|
} else {
|
|
3498
|
-
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch
|
|
3498
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
3499
3499
|
}
|
|
3500
3500
|
result.rowsWritten += batch.length;
|
|
3501
3501
|
} catch (err) {
|
|
@@ -3542,7 +3542,7 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
3542
3542
|
}
|
|
3543
3543
|
return results;
|
|
3544
3544
|
}
|
|
3545
|
-
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch
|
|
3545
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
3546
3546
|
if (batch.length === 0)
|
|
3547
3547
|
return;
|
|
3548
3548
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -3552,22 +3552,20 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, ba
|
|
|
3552
3552
|
}).join(", ");
|
|
3553
3553
|
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
3554
3554
|
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
3555
|
-
const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
|
|
3556
3555
|
const sql2 = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
3557
|
-
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}
|
|
3556
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
3558
3557
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
3559
3558
|
await target.run(sql2, ...params);
|
|
3560
3559
|
}
|
|
3561
|
-
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch
|
|
3560
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
|
|
3562
3561
|
if (batch.length === 0)
|
|
3563
3562
|
return;
|
|
3564
3563
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
3565
3564
|
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
3566
3565
|
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
3567
3566
|
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
3568
|
-
const whereClause = conflictColumn && updateCols.includes(conflictColumn) ? ` WHERE "${table}"."${conflictColumn}" IS NULL OR EXCLUDED."${conflictColumn}" >= "${table}"."${conflictColumn}"` : "";
|
|
3569
3567
|
const sql2 = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
3570
|
-
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}
|
|
3568
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
3571
3569
|
const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
|
|
3572
3570
|
target.run(sql2, ...params);
|
|
3573
3571
|
}
|
|
@@ -3626,17 +3624,17 @@ function ensureFeedbackTable(db) {
|
|
|
3626
3624
|
function saveFeedback(db, feedback) {
|
|
3627
3625
|
ensureFeedbackTable(db);
|
|
3628
3626
|
const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
3629
|
-
const
|
|
3627
|
+
const now2 = new Date().toISOString();
|
|
3630
3628
|
const machineId = feedback.machine_id ?? hostname();
|
|
3631
3629
|
db.run(`INSERT INTO feedback (id, service, version, message, email, machine_id, created_at)
|
|
3632
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ??
|
|
3630
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, feedback.service, feedback.version ?? "", feedback.message, feedback.email ?? "", machineId, feedback.created_at ?? now2);
|
|
3633
3631
|
return id;
|
|
3634
3632
|
}
|
|
3635
3633
|
async function sendFeedback(feedback, db) {
|
|
3636
3634
|
const config = getCloudConfig();
|
|
3637
3635
|
const id = feedback.id ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
3638
3636
|
const machineId = feedback.machine_id ?? hostname();
|
|
3639
|
-
const
|
|
3637
|
+
const now2 = new Date().toISOString();
|
|
3640
3638
|
const payload = {
|
|
3641
3639
|
id,
|
|
3642
3640
|
service: feedback.service,
|
|
@@ -3644,7 +3642,7 @@ async function sendFeedback(feedback, db) {
|
|
|
3644
3642
|
message: feedback.message,
|
|
3645
3643
|
email: feedback.email ?? "",
|
|
3646
3644
|
machine_id: machineId,
|
|
3647
|
-
created_at: feedback.created_at ??
|
|
3645
|
+
created_at: feedback.created_at ?? now2
|
|
3648
3646
|
};
|
|
3649
3647
|
try {
|
|
3650
3648
|
const res = await fetch(config.feedback_endpoint, {
|
|
@@ -3700,8 +3698,8 @@ class SyncProgressTracker {
|
|
|
3700
3698
|
}
|
|
3701
3699
|
start(table, total, direction) {
|
|
3702
3700
|
const resumed = this.canResume(table);
|
|
3703
|
-
const
|
|
3704
|
-
this.startTimes.set(table,
|
|
3701
|
+
const now2 = Date.now();
|
|
3702
|
+
this.startTimes.set(table, now2);
|
|
3705
3703
|
const status = resumed ? "resumed" : "in_progress";
|
|
3706
3704
|
const info = {
|
|
3707
3705
|
table,
|
|
@@ -4023,10 +4021,10 @@ function incrementalSyncPush(local, remote, tables, options = {}) {
|
|
|
4023
4021
|
if (rows.length === 0) {
|
|
4024
4022
|
stat.skipped_rows = stat.total_rows;
|
|
4025
4023
|
}
|
|
4026
|
-
const
|
|
4024
|
+
const now2 = new Date().toISOString();
|
|
4027
4025
|
upsertSyncMeta(local, {
|
|
4028
4026
|
table_name: table,
|
|
4029
|
-
last_synced_at:
|
|
4027
|
+
last_synced_at: now2,
|
|
4030
4028
|
last_synced_row_count: stat.synced_rows,
|
|
4031
4029
|
direction: "push"
|
|
4032
4030
|
});
|
|
@@ -4076,10 +4074,10 @@ function incrementalSyncPull(remote, local, tables, options = {}) {
|
|
|
4076
4074
|
if (rows.length === 0) {
|
|
4077
4075
|
stat.skipped_rows = stat.total_rows;
|
|
4078
4076
|
}
|
|
4079
|
-
const
|
|
4077
|
+
const now2 = new Date().toISOString();
|
|
4080
4078
|
upsertSyncMeta(local, {
|
|
4081
4079
|
table_name: table,
|
|
4082
|
-
last_synced_at:
|
|
4080
|
+
last_synced_at: now2,
|
|
4083
4081
|
last_synced_row_count: stat.synced_rows,
|
|
4084
4082
|
direction: "pull"
|
|
4085
4083
|
});
|
|
@@ -4107,7 +4105,7 @@ function resetAllSyncMeta(db) {
|
|
|
4107
4105
|
}
|
|
4108
4106
|
function getAutoSyncConfig() {
|
|
4109
4107
|
try {
|
|
4110
|
-
if (!
|
|
4108
|
+
if (!existsSync3(AUTO_SYNC_CONFIG_PATH)) {
|
|
4111
4109
|
return { ...DEFAULT_AUTO_SYNC_CONFIG };
|
|
4112
4110
|
}
|
|
4113
4111
|
const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
|
|
@@ -4119,7 +4117,7 @@ function getAutoSyncConfig() {
|
|
|
4119
4117
|
return { ...DEFAULT_AUTO_SYNC_CONFIG };
|
|
4120
4118
|
}
|
|
4121
4119
|
}
|
|
4122
|
-
|
|
4120
|
+
function executeAutoSync(event, local, remote, tables) {
|
|
4123
4121
|
const direction = event === "start" ? "pull" : "push";
|
|
4124
4122
|
const result = {
|
|
4125
4123
|
event,
|
|
@@ -4129,31 +4127,18 @@ async function executeAutoSync(event, serviceName, local, tables) {
|
|
|
4129
4127
|
total_rows_synced: 0,
|
|
4130
4128
|
errors: []
|
|
4131
4129
|
};
|
|
4132
|
-
let remote = null;
|
|
4133
4130
|
try {
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
if (syncTables.length === 0) {
|
|
4138
|
-
result.success = true;
|
|
4139
|
-
return result;
|
|
4140
|
-
}
|
|
4141
|
-
const results = direction === "pull" ? await syncPull(remote, local, { tables: syncTables }) : await syncPush(local, remote, { tables: syncTables });
|
|
4142
|
-
for (const r of results) {
|
|
4143
|
-
if (r.errors.length === 0)
|
|
4131
|
+
const stats = direction === "pull" ? incrementalSyncPull(remote, local, tables) : incrementalSyncPush(local, remote, tables);
|
|
4132
|
+
for (const s of stats) {
|
|
4133
|
+
if (s.errors.length === 0) {
|
|
4144
4134
|
result.tables_synced++;
|
|
4145
|
-
|
|
4146
|
-
result.
|
|
4135
|
+
}
|
|
4136
|
+
result.total_rows_synced += s.synced_rows;
|
|
4137
|
+
result.errors.push(...s.errors);
|
|
4147
4138
|
}
|
|
4148
4139
|
result.success = result.errors.length === 0;
|
|
4149
4140
|
} catch (err) {
|
|
4150
4141
|
result.errors.push(err?.message ?? String(err));
|
|
4151
|
-
} finally {
|
|
4152
|
-
if (remote) {
|
|
4153
|
-
try {
|
|
4154
|
-
await remote.close();
|
|
4155
|
-
} catch {}
|
|
4156
|
-
}
|
|
4157
4142
|
}
|
|
4158
4143
|
return result;
|
|
4159
4144
|
}
|
|
@@ -4161,76 +4146,80 @@ function installSignalHandlers() {
|
|
|
4161
4146
|
if (signalHandlersInstalled)
|
|
4162
4147
|
return;
|
|
4163
4148
|
signalHandlersInstalled = true;
|
|
4164
|
-
const handleExit =
|
|
4149
|
+
const handleExit = () => {
|
|
4165
4150
|
for (const fn of cleanupHandlers) {
|
|
4166
4151
|
try {
|
|
4167
|
-
|
|
4152
|
+
fn();
|
|
4168
4153
|
} catch {}
|
|
4169
4154
|
}
|
|
4170
4155
|
};
|
|
4171
|
-
process.on("SIGTERM",
|
|
4172
|
-
|
|
4156
|
+
process.on("SIGTERM", () => {
|
|
4157
|
+
handleExit();
|
|
4173
4158
|
process.exit(0);
|
|
4174
4159
|
});
|
|
4175
|
-
process.on("SIGINT",
|
|
4176
|
-
|
|
4160
|
+
process.on("SIGINT", () => {
|
|
4161
|
+
handleExit();
|
|
4177
4162
|
process.exit(0);
|
|
4178
4163
|
});
|
|
4179
|
-
process.on("beforeExit",
|
|
4180
|
-
|
|
4164
|
+
process.on("beforeExit", () => {
|
|
4165
|
+
handleExit();
|
|
4181
4166
|
});
|
|
4182
4167
|
}
|
|
4183
4168
|
function setupAutoSync(serviceName, server, local, remote, tables) {
|
|
4184
4169
|
const config = getAutoSyncConfig();
|
|
4185
4170
|
const cloudConfig = getCloudConfig();
|
|
4186
4171
|
const isSyncEnabled = cloudConfig.mode === "hybrid" || cloudConfig.mode === "cloud";
|
|
4187
|
-
const syncOnStart =
|
|
4172
|
+
const syncOnStart = () => {
|
|
4188
4173
|
if (!config.auto_sync_on_start || !isSyncEnabled)
|
|
4189
4174
|
return null;
|
|
4190
|
-
return executeAutoSync("start",
|
|
4175
|
+
return executeAutoSync("start", local, remote, tables);
|
|
4191
4176
|
};
|
|
4192
|
-
const syncOnStop =
|
|
4177
|
+
const syncOnStop = () => {
|
|
4193
4178
|
if (!config.auto_sync_on_stop || !isSyncEnabled)
|
|
4194
4179
|
return null;
|
|
4195
|
-
return executeAutoSync("stop",
|
|
4180
|
+
return executeAutoSync("stop", local, remote, tables);
|
|
4196
4181
|
};
|
|
4197
4182
|
if (server && typeof server.onconnect === "function") {
|
|
4198
4183
|
const origOnConnect = server.onconnect;
|
|
4199
|
-
server.onconnect =
|
|
4200
|
-
|
|
4184
|
+
server.onconnect = (...args) => {
|
|
4185
|
+
syncOnStart();
|
|
4201
4186
|
return origOnConnect.apply(server, args);
|
|
4202
4187
|
};
|
|
4203
4188
|
} else if (server && typeof server.on === "function") {
|
|
4204
|
-
server.on("connect", () =>
|
|
4189
|
+
server.on("connect", () => {
|
|
4190
|
+
syncOnStart();
|
|
4191
|
+
});
|
|
4205
4192
|
}
|
|
4206
4193
|
if (server && typeof server.ondisconnect === "function") {
|
|
4207
4194
|
const origOnDisconnect = server.ondisconnect;
|
|
4208
|
-
server.ondisconnect =
|
|
4209
|
-
|
|
4195
|
+
server.ondisconnect = (...args) => {
|
|
4196
|
+
syncOnStop();
|
|
4210
4197
|
return origOnDisconnect.apply(server, args);
|
|
4211
4198
|
};
|
|
4212
4199
|
} else if (server && typeof server.on === "function") {
|
|
4213
|
-
server.on("disconnect", () =>
|
|
4200
|
+
server.on("disconnect", () => {
|
|
4201
|
+
syncOnStop();
|
|
4202
|
+
});
|
|
4214
4203
|
}
|
|
4215
4204
|
installSignalHandlers();
|
|
4216
|
-
cleanupHandlers.push(
|
|
4217
|
-
|
|
4205
|
+
cleanupHandlers.push(() => {
|
|
4206
|
+
syncOnStop();
|
|
4218
4207
|
});
|
|
4219
4208
|
return { syncOnStart, syncOnStop, config };
|
|
4220
4209
|
}
|
|
4221
4210
|
function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
|
|
4222
4211
|
setupAutoSync(serviceName, mcpServer, local, remote, tables);
|
|
4223
4212
|
}
|
|
4224
|
-
function
|
|
4213
|
+
function discoverSyncableServices() {
|
|
4225
4214
|
const hasnaDir = getHasnaDir();
|
|
4226
4215
|
const services = [];
|
|
4227
4216
|
try {
|
|
4228
|
-
const entries =
|
|
4217
|
+
const entries = readdirSync2(hasnaDir, { withFileTypes: true });
|
|
4229
4218
|
for (const entry of entries) {
|
|
4230
4219
|
if (!entry.isDirectory())
|
|
4231
4220
|
continue;
|
|
4232
|
-
const dbPath =
|
|
4233
|
-
if (
|
|
4221
|
+
const dbPath = join4(hasnaDir, entry.name, `${entry.name}.db`);
|
|
4222
|
+
if (existsSync4(dbPath)) {
|
|
4234
4223
|
services.push(entry.name);
|
|
4235
4224
|
}
|
|
4236
4225
|
}
|
|
@@ -4241,7 +4230,7 @@ async function runScheduledSync() {
|
|
|
4241
4230
|
const config = getCloudConfig();
|
|
4242
4231
|
if (config.mode === "local")
|
|
4243
4232
|
return [];
|
|
4244
|
-
const services =
|
|
4233
|
+
const services = discoverSyncableServices();
|
|
4245
4234
|
const results = [];
|
|
4246
4235
|
let remote = null;
|
|
4247
4236
|
for (const service of services) {
|
|
@@ -4252,8 +4241,8 @@ async function runScheduledSync() {
|
|
|
4252
4241
|
errors: []
|
|
4253
4242
|
};
|
|
4254
4243
|
try {
|
|
4255
|
-
const dbPath =
|
|
4256
|
-
if (!
|
|
4244
|
+
const dbPath = join4(getDataDir(service), `${service}.db`);
|
|
4245
|
+
if (!existsSync4(dbPath)) {
|
|
4257
4246
|
continue;
|
|
4258
4247
|
}
|
|
4259
4248
|
const local = new SqliteAdapter(dbPath);
|
|
@@ -4334,34 +4323,34 @@ function minutesToCron(minutes) {
|
|
|
4334
4323
|
}
|
|
4335
4324
|
function getWorkerPath() {
|
|
4336
4325
|
const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
|
|
4337
|
-
const tsPath =
|
|
4338
|
-
const jsPath =
|
|
4326
|
+
const tsPath = join5(dir, "scheduled-sync.ts");
|
|
4327
|
+
const jsPath = join5(dir, "scheduled-sync.js");
|
|
4339
4328
|
try {
|
|
4340
|
-
if (
|
|
4329
|
+
if (existsSync5(tsPath))
|
|
4341
4330
|
return tsPath;
|
|
4342
4331
|
} catch {}
|
|
4343
4332
|
return jsPath;
|
|
4344
4333
|
}
|
|
4345
4334
|
function getBunPath() {
|
|
4346
4335
|
const candidates = [
|
|
4347
|
-
|
|
4336
|
+
join5(homedir4(), ".bun", "bin", "bun"),
|
|
4348
4337
|
"/usr/local/bin/bun",
|
|
4349
4338
|
"/usr/bin/bun"
|
|
4350
4339
|
];
|
|
4351
4340
|
for (const p of candidates) {
|
|
4352
|
-
if (
|
|
4341
|
+
if (existsSync5(p))
|
|
4353
4342
|
return p;
|
|
4354
4343
|
}
|
|
4355
4344
|
return "bun";
|
|
4356
4345
|
}
|
|
4357
4346
|
function getLaunchdPlistPath() {
|
|
4358
|
-
return
|
|
4347
|
+
return join5(homedir4(), "Library", "LaunchAgents", `com.hasna.cloud-sync.plist`);
|
|
4359
4348
|
}
|
|
4360
4349
|
function createLaunchdPlist(intervalMinutes) {
|
|
4361
4350
|
const workerPath = getWorkerPath();
|
|
4362
4351
|
const bunPath = getBunPath();
|
|
4363
|
-
const logPath =
|
|
4364
|
-
const errorLogPath =
|
|
4352
|
+
const logPath = join5(CONFIG_DIR2, "sync.log");
|
|
4353
|
+
const errorLogPath = join5(CONFIG_DIR2, "sync-error.log");
|
|
4365
4354
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
4366
4355
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4367
4356
|
<plist version="1.0">
|
|
@@ -4387,7 +4376,7 @@ function createLaunchdPlist(intervalMinutes) {
|
|
|
4387
4376
|
<key>PATH</key>
|
|
4388
4377
|
<string>${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}</string>
|
|
4389
4378
|
<key>HOME</key>
|
|
4390
|
-
<string>${
|
|
4379
|
+
<string>${homedir4()}</string>
|
|
4391
4380
|
</dict>
|
|
4392
4381
|
</dict>
|
|
4393
4382
|
</plist>`;
|
|
@@ -4412,7 +4401,7 @@ async function removeLaunchd() {
|
|
|
4412
4401
|
} catch {}
|
|
4413
4402
|
}
|
|
4414
4403
|
function getSystemdDir() {
|
|
4415
|
-
return
|
|
4404
|
+
return join5(homedir4(), ".config", "systemd", "user");
|
|
4416
4405
|
}
|
|
4417
4406
|
function createSystemdService() {
|
|
4418
4407
|
const workerPath = getWorkerPath();
|
|
@@ -4424,7 +4413,7 @@ After=network.target
|
|
|
4424
4413
|
[Service]
|
|
4425
4414
|
Type=oneshot
|
|
4426
4415
|
ExecStart=${bunPath} run ${workerPath}
|
|
4427
|
-
Environment=HOME=${
|
|
4416
|
+
Environment=HOME=${homedir4()}
|
|
4428
4417
|
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
4429
4418
|
|
|
4430
4419
|
[Install]
|
|
@@ -4447,8 +4436,8 @@ WantedBy=timers.target
|
|
|
4447
4436
|
async function registerSystemd(intervalMinutes) {
|
|
4448
4437
|
const dir = getSystemdDir();
|
|
4449
4438
|
mkdirSync3(dir, { recursive: true });
|
|
4450
|
-
writeFileSync2(
|
|
4451
|
-
writeFileSync2(
|
|
4439
|
+
writeFileSync2(join5(dir, `${SERVICE_NAME}.service`), createSystemdService());
|
|
4440
|
+
writeFileSync2(join5(dir, `${SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
|
|
4452
4441
|
await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
|
|
4453
4442
|
await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SERVICE_NAME}.timer`]).exited;
|
|
4454
4443
|
}
|
|
@@ -4458,10 +4447,10 @@ async function removeSystemd() {
|
|
|
4458
4447
|
} catch {}
|
|
4459
4448
|
const dir = getSystemdDir();
|
|
4460
4449
|
try {
|
|
4461
|
-
unlinkSync(
|
|
4450
|
+
unlinkSync(join5(dir, `${SERVICE_NAME}.service`));
|
|
4462
4451
|
} catch {}
|
|
4463
4452
|
try {
|
|
4464
|
-
unlinkSync(
|
|
4453
|
+
unlinkSync(join5(dir, `${SERVICE_NAME}.timer`));
|
|
4465
4454
|
} catch {}
|
|
4466
4455
|
try {
|
|
4467
4456
|
await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
|
|
@@ -4498,9 +4487,9 @@ function getSyncScheduleStatus() {
|
|
|
4498
4487
|
let mechanism = "none";
|
|
4499
4488
|
if (registered) {
|
|
4500
4489
|
if (platform() === "darwin") {
|
|
4501
|
-
mechanism =
|
|
4490
|
+
mechanism = existsSync5(getLaunchdPlistPath()) ? "launchd" : "none";
|
|
4502
4491
|
} else {
|
|
4503
|
-
mechanism =
|
|
4492
|
+
mechanism = existsSync5(join5(getSystemdDir(), `${SERVICE_NAME}.timer`)) ? "systemd" : "none";
|
|
4504
4493
|
}
|
|
4505
4494
|
}
|
|
4506
4495
|
return {
|
|
@@ -13045,7 +13034,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13045
13034
|
init_external();
|
|
13046
13035
|
});
|
|
13047
13036
|
init_dotfile = __esm2(() => {
|
|
13048
|
-
HASNA_DIR =
|
|
13037
|
+
HASNA_DIR = join2(homedir2(), ".hasna");
|
|
13049
13038
|
});
|
|
13050
13039
|
exports_config = {};
|
|
13051
13040
|
__export2(exports_config, {
|
|
@@ -13076,14 +13065,14 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13076
13065
|
schedule_minutes: exports_external.number().default(0)
|
|
13077
13066
|
}).default({})
|
|
13078
13067
|
});
|
|
13079
|
-
CONFIG_DIR =
|
|
13080
|
-
CONFIG_PATH =
|
|
13068
|
+
CONFIG_DIR = join22(homedir22(), ".hasna", "cloud");
|
|
13069
|
+
CONFIG_PATH = join22(CONFIG_DIR, "config.json");
|
|
13081
13070
|
});
|
|
13082
13071
|
exports_discover = {};
|
|
13083
13072
|
__export2(exports_discover, {
|
|
13084
13073
|
isSyncExcludedTable: () => isSyncExcludedTable,
|
|
13085
13074
|
getServiceDbPath: () => getServiceDbPath,
|
|
13086
|
-
discoverSyncableServices: () =>
|
|
13075
|
+
discoverSyncableServices: () => discoverSyncableServices2,
|
|
13087
13076
|
discoverServices: () => discoverServices,
|
|
13088
13077
|
SYNC_EXCLUDED_TABLE_PATTERNS: () => SYNC_EXCLUDED_TABLE_PATTERNS,
|
|
13089
13078
|
KNOWN_PG_SERVICES: () => KNOWN_PG_SERVICES
|
|
@@ -13138,10 +13127,8 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13138
13127
|
init_config();
|
|
13139
13128
|
init_config();
|
|
13140
13129
|
init_dotfile();
|
|
13141
|
-
init_adapter();
|
|
13142
13130
|
init_config();
|
|
13143
|
-
|
|
13144
|
-
AUTO_SYNC_CONFIG_PATH = join4(homedir4(), ".hasna", "cloud", "config.json");
|
|
13131
|
+
AUTO_SYNC_CONFIG_PATH = join3(homedir3(), ".hasna", "cloud", "config.json");
|
|
13145
13132
|
DEFAULT_AUTO_SYNC_CONFIG = {
|
|
13146
13133
|
auto_sync_on_start: true,
|
|
13147
13134
|
auto_sync_on_stop: true
|
|
@@ -13151,7 +13138,7 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
13151
13138
|
init_adapter();
|
|
13152
13139
|
init_dotfile();
|
|
13153
13140
|
init_config();
|
|
13154
|
-
CONFIG_DIR2 =
|
|
13141
|
+
CONFIG_DIR2 = join5(homedir4(), ".hasna", "cloud");
|
|
13155
13142
|
init_adapter();
|
|
13156
13143
|
init_config();
|
|
13157
13144
|
init_discover();
|
|
@@ -13433,7 +13420,7 @@ var exports_sessions2 = {};
|
|
|
13433
13420
|
__export(exports_sessions2, {
|
|
13434
13421
|
gatherFromSessions: () => gatherFromSessions
|
|
13435
13422
|
});
|
|
13436
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
13423
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
13437
13424
|
import { existsSync as existsSync10 } from "fs";
|
|
13438
13425
|
import { join as join12 } from "path";
|
|
13439
13426
|
import { homedir as homedir11 } from "os";
|
|
@@ -13467,7 +13454,7 @@ async function gatherFromSessions(options = {}) {
|
|
|
13467
13454
|
if (fileStat && fileStat.mtime < options.since)
|
|
13468
13455
|
continue;
|
|
13469
13456
|
}
|
|
13470
|
-
const content = await
|
|
13457
|
+
const content = await readFile2(filePath, "utf-8").catch(() => "");
|
|
13471
13458
|
if (!content.trim())
|
|
13472
13459
|
continue;
|
|
13473
13460
|
const lines = content.trim().split(`
|
|
@@ -13603,6 +13590,690 @@ var init_pg_migrate = __esm(() => {
|
|
|
13603
13590
|
init_pg_migrations();
|
|
13604
13591
|
});
|
|
13605
13592
|
|
|
13593
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
13594
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
13595
|
+
import { existsSync } from "fs";
|
|
13596
|
+
import { homedir } from "os";
|
|
13597
|
+
import { join } from "path";
|
|
13598
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
13599
|
+
import { randomUUID } from "crypto";
|
|
13600
|
+
import { spawn } from "child_process";
|
|
13601
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
13602
|
+
function getPathValue(input, path) {
|
|
13603
|
+
return path.split(".").reduce((value, part) => {
|
|
13604
|
+
if (value && typeof value === "object" && part in value) {
|
|
13605
|
+
return value[part];
|
|
13606
|
+
}
|
|
13607
|
+
return;
|
|
13608
|
+
}, input);
|
|
13609
|
+
}
|
|
13610
|
+
function wildcardToRegExp(pattern) {
|
|
13611
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
13612
|
+
return new RegExp(`^${escaped}$`);
|
|
13613
|
+
}
|
|
13614
|
+
function matchString(value, matcher) {
|
|
13615
|
+
if (matcher === undefined)
|
|
13616
|
+
return true;
|
|
13617
|
+
if (value === undefined)
|
|
13618
|
+
return false;
|
|
13619
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
13620
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
13621
|
+
}
|
|
13622
|
+
function matchRecord(input, matcher) {
|
|
13623
|
+
if (!matcher)
|
|
13624
|
+
return true;
|
|
13625
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
13626
|
+
const actual = getPathValue(input, path);
|
|
13627
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
13628
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
13629
|
+
}
|
|
13630
|
+
return actual === expected;
|
|
13631
|
+
});
|
|
13632
|
+
}
|
|
13633
|
+
function eventMatchesFilter(event, filter) {
|
|
13634
|
+
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);
|
|
13635
|
+
}
|
|
13636
|
+
function channelMatchesEvent(channel, event) {
|
|
13637
|
+
if (!channel.enabled)
|
|
13638
|
+
return false;
|
|
13639
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
13640
|
+
return true;
|
|
13641
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
13642
|
+
}
|
|
13643
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
13644
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
13645
|
+
function getEventsDataDir(override) {
|
|
13646
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
13647
|
+
}
|
|
13648
|
+
|
|
13649
|
+
class JsonEventsStore {
|
|
13650
|
+
dataDir;
|
|
13651
|
+
channelsPath;
|
|
13652
|
+
eventsPath;
|
|
13653
|
+
deliveriesPath;
|
|
13654
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
13655
|
+
this.dataDir = dataDir;
|
|
13656
|
+
this.channelsPath = join(dataDir, "channels.json");
|
|
13657
|
+
this.eventsPath = join(dataDir, "events.json");
|
|
13658
|
+
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
13659
|
+
}
|
|
13660
|
+
async init() {
|
|
13661
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
13662
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
13663
|
+
return;
|
|
13664
|
+
});
|
|
13665
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
13666
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
13667
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
13668
|
+
}
|
|
13669
|
+
async addChannel(channel) {
|
|
13670
|
+
await this.init();
|
|
13671
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
13672
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
13673
|
+
if (index >= 0) {
|
|
13674
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
13675
|
+
} else {
|
|
13676
|
+
channels.push(channel);
|
|
13677
|
+
}
|
|
13678
|
+
await this.writeJson(this.channelsPath, channels);
|
|
13679
|
+
return index >= 0 ? channels[index] : channel;
|
|
13680
|
+
}
|
|
13681
|
+
async listChannels() {
|
|
13682
|
+
await this.init();
|
|
13683
|
+
return this.readJson(this.channelsPath, []);
|
|
13684
|
+
}
|
|
13685
|
+
async getChannel(id) {
|
|
13686
|
+
const channels = await this.listChannels();
|
|
13687
|
+
return channels.find((channel) => channel.id === id);
|
|
13688
|
+
}
|
|
13689
|
+
async removeChannel(id) {
|
|
13690
|
+
await this.init();
|
|
13691
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
13692
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
13693
|
+
await this.writeJson(this.channelsPath, next);
|
|
13694
|
+
return next.length !== channels.length;
|
|
13695
|
+
}
|
|
13696
|
+
async appendEvent(event) {
|
|
13697
|
+
await this.init();
|
|
13698
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
13699
|
+
events.push(event);
|
|
13700
|
+
await this.writeJson(this.eventsPath, events);
|
|
13701
|
+
return event;
|
|
13702
|
+
}
|
|
13703
|
+
async listEvents() {
|
|
13704
|
+
await this.init();
|
|
13705
|
+
return this.readJson(this.eventsPath, []);
|
|
13706
|
+
}
|
|
13707
|
+
async findEventByIdentity(identity) {
|
|
13708
|
+
const events = await this.listEvents();
|
|
13709
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
13710
|
+
}
|
|
13711
|
+
async appendDelivery(result) {
|
|
13712
|
+
await this.init();
|
|
13713
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
13714
|
+
deliveries.push(result);
|
|
13715
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
13716
|
+
return result;
|
|
13717
|
+
}
|
|
13718
|
+
async listDeliveries() {
|
|
13719
|
+
await this.init();
|
|
13720
|
+
return this.readJson(this.deliveriesPath, []);
|
|
13721
|
+
}
|
|
13722
|
+
async exportData() {
|
|
13723
|
+
return {
|
|
13724
|
+
channels: await this.listChannels(),
|
|
13725
|
+
events: await this.listEvents(),
|
|
13726
|
+
deliveries: await this.listDeliveries()
|
|
13727
|
+
};
|
|
13728
|
+
}
|
|
13729
|
+
async ensureArrayFile(path) {
|
|
13730
|
+
if (!existsSync(path)) {
|
|
13731
|
+
await writeFile(path, `[]
|
|
13732
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
13733
|
+
}
|
|
13734
|
+
await chmod(path, 384).catch(() => {
|
|
13735
|
+
return;
|
|
13736
|
+
});
|
|
13737
|
+
}
|
|
13738
|
+
async readJson(path, fallback) {
|
|
13739
|
+
try {
|
|
13740
|
+
const raw = await readFile(path, "utf-8");
|
|
13741
|
+
if (!raw.trim())
|
|
13742
|
+
return fallback;
|
|
13743
|
+
return JSON.parse(raw);
|
|
13744
|
+
} catch (error) {
|
|
13745
|
+
if (error.code === "ENOENT")
|
|
13746
|
+
return fallback;
|
|
13747
|
+
throw error;
|
|
13748
|
+
}
|
|
13749
|
+
}
|
|
13750
|
+
async writeJson(path, value) {
|
|
13751
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
13752
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
13753
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
13754
|
+
await rename(tempPath, path);
|
|
13755
|
+
await chmod(path, 384).catch(() => {
|
|
13756
|
+
return;
|
|
13757
|
+
});
|
|
13758
|
+
}
|
|
13759
|
+
}
|
|
13760
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
13761
|
+
function buildSignatureBase(timestamp, body) {
|
|
13762
|
+
return `${timestamp}.${body}`;
|
|
13763
|
+
}
|
|
13764
|
+
function signPayload(secret, timestamp, body) {
|
|
13765
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
13766
|
+
return `sha256=${digest}`;
|
|
13767
|
+
}
|
|
13768
|
+
function now() {
|
|
13769
|
+
return new Date().toISOString();
|
|
13770
|
+
}
|
|
13771
|
+
function truncate(value, max = 4096) {
|
|
13772
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
13773
|
+
}
|
|
13774
|
+
function buildWebhookRequest(event, channel) {
|
|
13775
|
+
if (!channel.webhook)
|
|
13776
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13777
|
+
const body = JSON.stringify(event);
|
|
13778
|
+
const timestamp = event.time;
|
|
13779
|
+
const headers = {
|
|
13780
|
+
"Content-Type": "application/json",
|
|
13781
|
+
"User-Agent": "@hasna/events",
|
|
13782
|
+
"X-Hasna-Event-Id": event.id,
|
|
13783
|
+
"X-Hasna-Event-Type": event.type,
|
|
13784
|
+
"X-Hasna-Timestamp": timestamp,
|
|
13785
|
+
...channel.webhook.headers
|
|
13786
|
+
};
|
|
13787
|
+
if (channel.webhook.secret) {
|
|
13788
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
13789
|
+
}
|
|
13790
|
+
return { body, headers };
|
|
13791
|
+
}
|
|
13792
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
13793
|
+
if (!channel.webhook)
|
|
13794
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13795
|
+
const startedAt = now();
|
|
13796
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
13797
|
+
const controller = new AbortController;
|
|
13798
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
13799
|
+
try {
|
|
13800
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
13801
|
+
method: "POST",
|
|
13802
|
+
headers,
|
|
13803
|
+
body,
|
|
13804
|
+
signal: controller.signal
|
|
13805
|
+
});
|
|
13806
|
+
const responseBody = truncate(await response.text());
|
|
13807
|
+
return {
|
|
13808
|
+
attempt: 1,
|
|
13809
|
+
status: response.ok ? "success" : "failed",
|
|
13810
|
+
startedAt,
|
|
13811
|
+
completedAt: now(),
|
|
13812
|
+
responseStatus: response.status,
|
|
13813
|
+
responseBody,
|
|
13814
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
13815
|
+
};
|
|
13816
|
+
} catch (error) {
|
|
13817
|
+
return {
|
|
13818
|
+
attempt: 1,
|
|
13819
|
+
status: "failed",
|
|
13820
|
+
startedAt,
|
|
13821
|
+
completedAt: now(),
|
|
13822
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13823
|
+
};
|
|
13824
|
+
} finally {
|
|
13825
|
+
clearTimeout(timeout);
|
|
13826
|
+
}
|
|
13827
|
+
}
|
|
13828
|
+
async function dispatchCommand(event, channel) {
|
|
13829
|
+
if (!channel.command)
|
|
13830
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
13831
|
+
const startedAt = now();
|
|
13832
|
+
const eventJson = JSON.stringify(event);
|
|
13833
|
+
const env = {
|
|
13834
|
+
...process.env,
|
|
13835
|
+
...channel.command.env,
|
|
13836
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
13837
|
+
HASNA_EVENT_ID: event.id,
|
|
13838
|
+
HASNA_EVENT_TYPE: event.type,
|
|
13839
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
13840
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
13841
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
13842
|
+
HASNA_EVENT_TIME: event.time,
|
|
13843
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
13844
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
13845
|
+
HASNA_EVENT_JSON: eventJson
|
|
13846
|
+
};
|
|
13847
|
+
return new Promise((resolve) => {
|
|
13848
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
13849
|
+
cwd: channel.command.cwd,
|
|
13850
|
+
env,
|
|
13851
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13852
|
+
});
|
|
13853
|
+
let stdout = "";
|
|
13854
|
+
let stderr = "";
|
|
13855
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
13856
|
+
child.stdin.end(eventJson);
|
|
13857
|
+
child.stdout.on("data", (chunk) => {
|
|
13858
|
+
stdout += chunk.toString();
|
|
13859
|
+
});
|
|
13860
|
+
child.stderr.on("data", (chunk) => {
|
|
13861
|
+
stderr += chunk.toString();
|
|
13862
|
+
});
|
|
13863
|
+
child.on("error", (error) => {
|
|
13864
|
+
clearTimeout(timeout);
|
|
13865
|
+
resolve({
|
|
13866
|
+
attempt: 1,
|
|
13867
|
+
status: "failed",
|
|
13868
|
+
startedAt,
|
|
13869
|
+
completedAt: now(),
|
|
13870
|
+
stdout: truncate(stdout),
|
|
13871
|
+
stderr: truncate(stderr),
|
|
13872
|
+
error: error.message
|
|
13873
|
+
});
|
|
13874
|
+
});
|
|
13875
|
+
child.on("close", (code, signal) => {
|
|
13876
|
+
clearTimeout(timeout);
|
|
13877
|
+
const success = code === 0;
|
|
13878
|
+
resolve({
|
|
13879
|
+
attempt: 1,
|
|
13880
|
+
status: success ? "success" : "failed",
|
|
13881
|
+
startedAt,
|
|
13882
|
+
completedAt: now(),
|
|
13883
|
+
stdout: truncate(stdout),
|
|
13884
|
+
stderr: truncate(stderr),
|
|
13885
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
13886
|
+
});
|
|
13887
|
+
});
|
|
13888
|
+
});
|
|
13889
|
+
}
|
|
13890
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
13891
|
+
if (channel.transport === "webhook")
|
|
13892
|
+
return dispatchWebhook(event, channel, options);
|
|
13893
|
+
if (channel.transport === "command")
|
|
13894
|
+
return dispatchCommand(event, channel);
|
|
13895
|
+
return {
|
|
13896
|
+
attempt: 1,
|
|
13897
|
+
status: "skipped",
|
|
13898
|
+
startedAt: now(),
|
|
13899
|
+
completedAt: now(),
|
|
13900
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
13901
|
+
};
|
|
13902
|
+
}
|
|
13903
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
13904
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
13905
|
+
return {
|
|
13906
|
+
id: randomUUID(),
|
|
13907
|
+
eventId: event.id,
|
|
13908
|
+
channelId: channel.id,
|
|
13909
|
+
transport: channel.transport,
|
|
13910
|
+
status,
|
|
13911
|
+
attempts,
|
|
13912
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
13913
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
13914
|
+
};
|
|
13915
|
+
}
|
|
13916
|
+
function createEvent(input) {
|
|
13917
|
+
return {
|
|
13918
|
+
id: input.id ?? randomUUID2(),
|
|
13919
|
+
source: input.source,
|
|
13920
|
+
type: input.type,
|
|
13921
|
+
time: normalizeTime(input.time),
|
|
13922
|
+
subject: input.subject,
|
|
13923
|
+
severity: input.severity ?? "info",
|
|
13924
|
+
data: input.data ?? {},
|
|
13925
|
+
message: input.message,
|
|
13926
|
+
dedupeKey: input.dedupeKey,
|
|
13927
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
13928
|
+
metadata: input.metadata ?? {}
|
|
13929
|
+
};
|
|
13930
|
+
}
|
|
13931
|
+
|
|
13932
|
+
class EventsClient {
|
|
13933
|
+
store;
|
|
13934
|
+
redactors;
|
|
13935
|
+
transportOptions;
|
|
13936
|
+
constructor(options = {}) {
|
|
13937
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
13938
|
+
this.redactors = options.redactors ?? [];
|
|
13939
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
13940
|
+
}
|
|
13941
|
+
async addChannel(input) {
|
|
13942
|
+
const timestamp = new Date().toISOString();
|
|
13943
|
+
return this.store.addChannel({
|
|
13944
|
+
...input,
|
|
13945
|
+
createdAt: input.createdAt ?? timestamp,
|
|
13946
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
13947
|
+
});
|
|
13948
|
+
}
|
|
13949
|
+
async listChannels() {
|
|
13950
|
+
return this.store.listChannels();
|
|
13951
|
+
}
|
|
13952
|
+
async removeChannel(id) {
|
|
13953
|
+
return this.store.removeChannel(id);
|
|
13954
|
+
}
|
|
13955
|
+
async emit(input, options = {}) {
|
|
13956
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
13957
|
+
if (options.dedupe !== false) {
|
|
13958
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
13959
|
+
if (existing) {
|
|
13960
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
13961
|
+
}
|
|
13962
|
+
}
|
|
13963
|
+
await this.store.appendEvent(event);
|
|
13964
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
13965
|
+
return { event, deliveries, deduped: false };
|
|
13966
|
+
}
|
|
13967
|
+
async listEvents() {
|
|
13968
|
+
return this.store.listEvents();
|
|
13969
|
+
}
|
|
13970
|
+
async listDeliveries() {
|
|
13971
|
+
return this.store.listDeliveries();
|
|
13972
|
+
}
|
|
13973
|
+
async deliver(event) {
|
|
13974
|
+
const channels = await this.store.listChannels();
|
|
13975
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
13976
|
+
const deliveries = [];
|
|
13977
|
+
for (const channel of selected) {
|
|
13978
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13979
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13980
|
+
await this.store.appendDelivery(result);
|
|
13981
|
+
deliveries.push(result);
|
|
13982
|
+
}
|
|
13983
|
+
return deliveries;
|
|
13984
|
+
}
|
|
13985
|
+
async testChannel(id, input = {}) {
|
|
13986
|
+
const channel = await this.store.getChannel(id);
|
|
13987
|
+
if (!channel)
|
|
13988
|
+
throw new Error(`Channel not found: ${id}`);
|
|
13989
|
+
const event = createEvent({
|
|
13990
|
+
source: input.source ?? "hasna.events",
|
|
13991
|
+
type: input.type ?? "events.test",
|
|
13992
|
+
subject: input.subject ?? id,
|
|
13993
|
+
severity: input.severity ?? "info",
|
|
13994
|
+
data: input.data ?? { test: true },
|
|
13995
|
+
message: input.message ?? "Hasna events test delivery",
|
|
13996
|
+
dedupeKey: input.dedupeKey,
|
|
13997
|
+
schemaVersion: input.schemaVersion,
|
|
13998
|
+
metadata: input.metadata,
|
|
13999
|
+
time: input.time,
|
|
14000
|
+
id: input.id
|
|
14001
|
+
});
|
|
14002
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
14003
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
14004
|
+
await this.store.appendDelivery(result);
|
|
14005
|
+
return result;
|
|
14006
|
+
}
|
|
14007
|
+
async replay(options = {}) {
|
|
14008
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
14009
|
+
if (options.eventId && event.id !== options.eventId)
|
|
14010
|
+
return false;
|
|
14011
|
+
if (options.source && event.source !== options.source)
|
|
14012
|
+
return false;
|
|
14013
|
+
if (options.type && event.type !== options.type)
|
|
14014
|
+
return false;
|
|
14015
|
+
return true;
|
|
14016
|
+
});
|
|
14017
|
+
if (options.dryRun)
|
|
14018
|
+
return { events, deliveries: [] };
|
|
14019
|
+
const deliveries = [];
|
|
14020
|
+
for (const event of events) {
|
|
14021
|
+
deliveries.push(...await this.deliver(event));
|
|
14022
|
+
}
|
|
14023
|
+
return { events, deliveries };
|
|
14024
|
+
}
|
|
14025
|
+
async applyRedaction(event, channel) {
|
|
14026
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
14027
|
+
for (const redactor of this.redactors) {
|
|
14028
|
+
next = await redactor(next, channel);
|
|
14029
|
+
}
|
|
14030
|
+
return next;
|
|
14031
|
+
}
|
|
14032
|
+
async deliverWithRetry(event, channel) {
|
|
14033
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
14034
|
+
const attempts = [];
|
|
14035
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
14036
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
14037
|
+
attempt.attempt = index + 1;
|
|
14038
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
14039
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
14040
|
+
}
|
|
14041
|
+
attempts.push(attempt);
|
|
14042
|
+
if (attempt.status !== "failed")
|
|
14043
|
+
break;
|
|
14044
|
+
if (attempt.nextBackoffMs)
|
|
14045
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
14046
|
+
}
|
|
14047
|
+
return createDeliveryResult(event, channel, attempts);
|
|
14048
|
+
}
|
|
14049
|
+
}
|
|
14050
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
14051
|
+
if (paths.length === 0)
|
|
14052
|
+
return event;
|
|
14053
|
+
const copy = structuredClone(event);
|
|
14054
|
+
for (const path of paths) {
|
|
14055
|
+
setPath(copy, path, replacement);
|
|
14056
|
+
}
|
|
14057
|
+
return copy;
|
|
14058
|
+
}
|
|
14059
|
+
function sanitizeChannelForOutput(channel) {
|
|
14060
|
+
const copy = structuredClone(channel);
|
|
14061
|
+
if (copy.webhook?.secret)
|
|
14062
|
+
copy.webhook.secret = "[REDACTED]";
|
|
14063
|
+
if (copy.command?.env) {
|
|
14064
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
14065
|
+
}
|
|
14066
|
+
return copy;
|
|
14067
|
+
}
|
|
14068
|
+
function sanitizeChannelsForOutput(channels) {
|
|
14069
|
+
return channels.map(sanitizeChannelForOutput);
|
|
14070
|
+
}
|
|
14071
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
14072
|
+
return redactValue(event, replacement);
|
|
14073
|
+
}
|
|
14074
|
+
function shouldRedactKey(key) {
|
|
14075
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
14076
|
+
}
|
|
14077
|
+
function redactValue(value, replacement) {
|
|
14078
|
+
if (Array.isArray(value))
|
|
14079
|
+
return value.map((item) => redactValue(item, replacement));
|
|
14080
|
+
if (!value || typeof value !== "object")
|
|
14081
|
+
return value;
|
|
14082
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
14083
|
+
key,
|
|
14084
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
14085
|
+
]));
|
|
14086
|
+
}
|
|
14087
|
+
function setPath(input, path, replacement) {
|
|
14088
|
+
const parts = path.split(".");
|
|
14089
|
+
let cursor = input;
|
|
14090
|
+
for (const part of parts.slice(0, -1)) {
|
|
14091
|
+
const next = cursor[part];
|
|
14092
|
+
if (!next || typeof next !== "object")
|
|
14093
|
+
return;
|
|
14094
|
+
cursor = next;
|
|
14095
|
+
}
|
|
14096
|
+
const last = parts.at(-1);
|
|
14097
|
+
if (last && last in cursor)
|
|
14098
|
+
cursor[last] = replacement;
|
|
14099
|
+
}
|
|
14100
|
+
function normalizeTime(value) {
|
|
14101
|
+
if (!value)
|
|
14102
|
+
return new Date().toISOString();
|
|
14103
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
14104
|
+
}
|
|
14105
|
+
function normalizeRetryPolicy(policy) {
|
|
14106
|
+
return {
|
|
14107
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
14108
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
14109
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
14110
|
+
};
|
|
14111
|
+
}
|
|
14112
|
+
function parseJsonObject(value, fallback) {
|
|
14113
|
+
if (!value)
|
|
14114
|
+
return fallback;
|
|
14115
|
+
const parsed = JSON.parse(value);
|
|
14116
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14117
|
+
throw new Error("Expected a JSON object");
|
|
14118
|
+
}
|
|
14119
|
+
return parsed;
|
|
14120
|
+
}
|
|
14121
|
+
function parseHeaders(values) {
|
|
14122
|
+
if (!values?.length)
|
|
14123
|
+
return;
|
|
14124
|
+
const headers = {};
|
|
14125
|
+
for (const value of values) {
|
|
14126
|
+
const separator = value.indexOf("=");
|
|
14127
|
+
if (separator === -1)
|
|
14128
|
+
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
14129
|
+
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
14130
|
+
}
|
|
14131
|
+
return headers;
|
|
14132
|
+
}
|
|
14133
|
+
function parseFilter(options) {
|
|
14134
|
+
const filter2 = {};
|
|
14135
|
+
if (options.source)
|
|
14136
|
+
filter2.source = options.source;
|
|
14137
|
+
if (options.type)
|
|
14138
|
+
filter2.type = options.type;
|
|
14139
|
+
if (options.subject)
|
|
14140
|
+
filter2.subject = options.subject;
|
|
14141
|
+
if (options.severity)
|
|
14142
|
+
filter2.severity = options.severity;
|
|
14143
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
14144
|
+
}
|
|
14145
|
+
function createClient(options) {
|
|
14146
|
+
if (options.createClient)
|
|
14147
|
+
return options.createClient();
|
|
14148
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
14149
|
+
}
|
|
14150
|
+
function print(value, json, text) {
|
|
14151
|
+
if (json)
|
|
14152
|
+
console.log(JSON.stringify(value, null, 2));
|
|
14153
|
+
else
|
|
14154
|
+
console.log(text);
|
|
14155
|
+
}
|
|
14156
|
+
function hasJsonOption(options) {
|
|
14157
|
+
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
14158
|
+
}
|
|
14159
|
+
function wantsJson(actionOptions, command) {
|
|
14160
|
+
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
14161
|
+
}
|
|
14162
|
+
function registerWebhookCommands(program, options) {
|
|
14163
|
+
const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
14164
|
+
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) => {
|
|
14165
|
+
const timestamp = new Date().toISOString();
|
|
14166
|
+
const channel = {
|
|
14167
|
+
id: actionOptions.id,
|
|
14168
|
+
name: actionOptions.name,
|
|
14169
|
+
enabled: !actionOptions.disabled,
|
|
14170
|
+
transport: actionOptions.transport,
|
|
14171
|
+
filters: parseFilter(actionOptions),
|
|
14172
|
+
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
14173
|
+
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
14174
|
+
createdAt: timestamp,
|
|
14175
|
+
updatedAt: timestamp
|
|
14176
|
+
};
|
|
14177
|
+
if (actionOptions.transport === "webhook") {
|
|
14178
|
+
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
14179
|
+
} else if (actionOptions.transport === "command") {
|
|
14180
|
+
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
14181
|
+
} else {
|
|
14182
|
+
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
14183
|
+
}
|
|
14184
|
+
const saved = await createClient(options).addChannel(channel);
|
|
14185
|
+
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
14186
|
+
});
|
|
14187
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
14188
|
+
const channels = await createClient(options).listChannels();
|
|
14189
|
+
if (wantsJson(actionOptions, command)) {
|
|
14190
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
14191
|
+
return;
|
|
14192
|
+
}
|
|
14193
|
+
if (!channels.length) {
|
|
14194
|
+
console.log("No channels configured.");
|
|
14195
|
+
return;
|
|
14196
|
+
}
|
|
14197
|
+
for (const channel of channels) {
|
|
14198
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
14199
|
+
}
|
|
14200
|
+
});
|
|
14201
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
14202
|
+
const removed = await createClient(options).removeChannel(id);
|
|
14203
|
+
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
14204
|
+
});
|
|
14205
|
+
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) => {
|
|
14206
|
+
const result = await createClient(options).testChannel(id, {
|
|
14207
|
+
source: options.source,
|
|
14208
|
+
type: actionOptions.type,
|
|
14209
|
+
subject: actionOptions.subject ?? id,
|
|
14210
|
+
message: actionOptions.message,
|
|
14211
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
14212
|
+
});
|
|
14213
|
+
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
14214
|
+
});
|
|
14215
|
+
return webhooks;
|
|
14216
|
+
}
|
|
14217
|
+
function registerEventCommands(program, options) {
|
|
14218
|
+
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
14219
|
+
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) => {
|
|
14220
|
+
const result = await createClient(options).emit({
|
|
14221
|
+
source: actionOptions.source ?? options.source,
|
|
14222
|
+
type,
|
|
14223
|
+
subject: actionOptions.subject,
|
|
14224
|
+
severity: actionOptions.severity,
|
|
14225
|
+
message: actionOptions.message,
|
|
14226
|
+
dedupeKey: actionOptions.dedupeKey,
|
|
14227
|
+
data: parseJsonObject(actionOptions.data, {}),
|
|
14228
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
14229
|
+
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
14230
|
+
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
14231
|
+
});
|
|
14232
|
+
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) => {
|
|
14233
|
+
let rows = await createClient(options).listEvents();
|
|
14234
|
+
if (actionOptions.source)
|
|
14235
|
+
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
14236
|
+
if (actionOptions.type)
|
|
14237
|
+
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
14238
|
+
if (actionOptions.limit)
|
|
14239
|
+
rows = rows.slice(-actionOptions.limit);
|
|
14240
|
+
if (wantsJson(actionOptions, command)) {
|
|
14241
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
14242
|
+
return;
|
|
14243
|
+
}
|
|
14244
|
+
if (!rows.length) {
|
|
14245
|
+
console.log("No events recorded.");
|
|
14246
|
+
return;
|
|
14247
|
+
}
|
|
14248
|
+
for (const event of rows)
|
|
14249
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
14250
|
+
});
|
|
14251
|
+
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) => {
|
|
14252
|
+
const result = await createClient(options).replay({
|
|
14253
|
+
eventId: actionOptions.id,
|
|
14254
|
+
source: actionOptions.source,
|
|
14255
|
+
type: actionOptions.type,
|
|
14256
|
+
dryRun: actionOptions.dryRun
|
|
14257
|
+
});
|
|
14258
|
+
print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
14259
|
+
});
|
|
14260
|
+
return events;
|
|
14261
|
+
}
|
|
14262
|
+
function registerEventsCommands(program, options) {
|
|
14263
|
+
registerWebhookCommands(program, options);
|
|
14264
|
+
registerEventCommands(program, options);
|
|
14265
|
+
}
|
|
14266
|
+
function parseNumber(value) {
|
|
14267
|
+
const parsed = Number(value);
|
|
14268
|
+
if (!Number.isFinite(parsed))
|
|
14269
|
+
throw new Error(`Expected a number, got ${value}`);
|
|
14270
|
+
return parsed;
|
|
14271
|
+
}
|
|
14272
|
+
function collectValues(value, previous) {
|
|
14273
|
+
previous.push(value);
|
|
14274
|
+
return previous;
|
|
14275
|
+
}
|
|
14276
|
+
|
|
13606
14277
|
// node_modules/commander/esm.mjs
|
|
13607
14278
|
var import__ = __toESM(require_commander(), 1);
|
|
13608
14279
|
var {
|
|
@@ -14198,7 +14869,7 @@ function sql(strings, ...params) {
|
|
|
14198
14869
|
return new SQL([new StringChunk(str)]);
|
|
14199
14870
|
}
|
|
14200
14871
|
sql2.raw = raw;
|
|
14201
|
-
function
|
|
14872
|
+
function join2(chunks, separator) {
|
|
14202
14873
|
const result = [];
|
|
14203
14874
|
for (const [i, chunk] of chunks.entries()) {
|
|
14204
14875
|
if (i > 0 && separator !== undefined) {
|
|
@@ -14208,7 +14879,7 @@ function sql(strings, ...params) {
|
|
|
14208
14879
|
}
|
|
14209
14880
|
return new SQL(result);
|
|
14210
14881
|
}
|
|
14211
|
-
sql2.join =
|
|
14882
|
+
sql2.join = join2;
|
|
14212
14883
|
function identifier(value) {
|
|
14213
14884
|
return new Name(value);
|
|
14214
14885
|
}
|
|
@@ -16254,7 +16925,7 @@ class SQLiteSelectQueryBuilderBase extends TypedQueryBuilder {
|
|
|
16254
16925
|
return (table, on) => {
|
|
16255
16926
|
const baseTableName = this.tableName;
|
|
16256
16927
|
const tableName = getTableLikeName(table);
|
|
16257
|
-
if (typeof tableName === "string" && this.config.joins?.some((
|
|
16928
|
+
if (typeof tableName === "string" && this.config.joins?.some((join2) => join2.alias === tableName)) {
|
|
16258
16929
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
16259
16930
|
}
|
|
16260
16931
|
if (!this.isPartialSelect) {
|
|
@@ -16650,7 +17321,7 @@ class SQLiteUpdateBase extends QueryPromise {
|
|
|
16650
17321
|
createJoin(joinType) {
|
|
16651
17322
|
return (table, on) => {
|
|
16652
17323
|
const tableName = getTableLikeName(table);
|
|
16653
|
-
if (typeof tableName === "string" && this.config.joins.some((
|
|
17324
|
+
if (typeof tableName === "string" && this.config.joins.some((join2) => join2.alias === tableName)) {
|
|
16654
17325
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
16655
17326
|
}
|
|
16656
17327
|
if (typeof on === "function") {
|
|
@@ -17457,7 +18128,7 @@ function printInfo(message) {
|
|
|
17457
18128
|
}
|
|
17458
18129
|
|
|
17459
18130
|
// src/cli/commands/models.ts
|
|
17460
|
-
import { randomUUID } from "crypto";
|
|
18131
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
17461
18132
|
|
|
17462
18133
|
// node_modules/openai/internal/qs/formats.mjs
|
|
17463
18134
|
var default_format = "RFC3986";
|
|
@@ -23397,8 +24068,8 @@ function registerModelsCommands(program2) {
|
|
|
23397
24068
|
printInfo(`Model already tracked as: ${existing.id}`);
|
|
23398
24069
|
return;
|
|
23399
24070
|
}
|
|
23400
|
-
const modelId =
|
|
23401
|
-
const
|
|
24071
|
+
const modelId = randomUUID3();
|
|
24072
|
+
const now2 = Date.now();
|
|
23402
24073
|
const name = opts.name ?? result.fineTunedModel ?? `imported-${jobId}`;
|
|
23403
24074
|
await db.insert(fineTunedModels).values({
|
|
23404
24075
|
id: modelId,
|
|
@@ -23407,15 +24078,15 @@ function registerModelsCommands(program2) {
|
|
|
23407
24078
|
baseModel: result.baseModel ?? "unknown",
|
|
23408
24079
|
status: result.status,
|
|
23409
24080
|
fineTuneJobId: jobId,
|
|
23410
|
-
createdAt:
|
|
23411
|
-
updatedAt:
|
|
24081
|
+
createdAt: now2,
|
|
24082
|
+
updatedAt: now2
|
|
23412
24083
|
});
|
|
23413
24084
|
await db.insert(trainingJobs).values({
|
|
23414
|
-
id:
|
|
24085
|
+
id: randomUUID3(),
|
|
23415
24086
|
modelId,
|
|
23416
24087
|
provider: opts.provider,
|
|
23417
24088
|
status: result.status,
|
|
23418
|
-
startedAt:
|
|
24089
|
+
startedAt: now2
|
|
23419
24090
|
});
|
|
23420
24091
|
printSuccess("Model imported successfully.");
|
|
23421
24092
|
console.log();
|
|
@@ -23434,7 +24105,7 @@ function registerModelsCommands(program2) {
|
|
|
23434
24105
|
}
|
|
23435
24106
|
|
|
23436
24107
|
// src/cli/commands/finetune.ts
|
|
23437
|
-
import { randomUUID as
|
|
24108
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
23438
24109
|
import { existsSync as existsSync9 } from "fs";
|
|
23439
24110
|
function registerFinetuneCommands(program2) {
|
|
23440
24111
|
const finetuneCmd = program2.command("finetune").description("Manage fine-tuning jobs");
|
|
@@ -23476,8 +24147,8 @@ function registerFinetuneCommands(program2) {
|
|
|
23476
24147
|
({ jobId, status: jobStatus } = await tl.createFineTuneJob(fileId, opts.baseModel, opts.name));
|
|
23477
24148
|
}
|
|
23478
24149
|
const db = getDb();
|
|
23479
|
-
const modelId =
|
|
23480
|
-
const
|
|
24150
|
+
const modelId = randomUUID5();
|
|
24151
|
+
const now2 = Date.now();
|
|
23481
24152
|
await db.insert(fineTunedModels).values({
|
|
23482
24153
|
id: modelId,
|
|
23483
24154
|
name: opts.name,
|
|
@@ -23485,16 +24156,16 @@ function registerFinetuneCommands(program2) {
|
|
|
23485
24156
|
baseModel: opts.baseModel,
|
|
23486
24157
|
status: "running",
|
|
23487
24158
|
fineTuneJobId: jobId,
|
|
23488
|
-
createdAt:
|
|
23489
|
-
updatedAt:
|
|
24159
|
+
createdAt: now2,
|
|
24160
|
+
updatedAt: now2
|
|
23490
24161
|
});
|
|
23491
|
-
const trainingJobId =
|
|
24162
|
+
const trainingJobId = randomUUID5();
|
|
23492
24163
|
await db.insert(trainingJobs).values({
|
|
23493
24164
|
id: trainingJobId,
|
|
23494
24165
|
modelId,
|
|
23495
24166
|
provider: opts.provider,
|
|
23496
24167
|
status: jobStatus,
|
|
23497
|
-
startedAt:
|
|
24168
|
+
startedAt: now2
|
|
23498
24169
|
});
|
|
23499
24170
|
printSuccess(`Fine-tune job started!`);
|
|
23500
24171
|
console.log();
|
|
@@ -23628,7 +24299,7 @@ function registerFinetuneCommands(program2) {
|
|
|
23628
24299
|
}
|
|
23629
24300
|
|
|
23630
24301
|
// src/cli/commands/data.ts
|
|
23631
|
-
import { randomUUID as
|
|
24302
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
23632
24303
|
import { readFileSync as readFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
23633
24304
|
import { dirname as dirname3, join as join13 } from "path";
|
|
23634
24305
|
import { homedir as homedir12 } from "os";
|
|
@@ -23649,7 +24320,7 @@ function registerDataCommands(program2) {
|
|
|
23649
24320
|
try {
|
|
23650
24321
|
mkdirSync6(opts.output, { recursive: true });
|
|
23651
24322
|
const sources = opts.source === "all" ? ["todos", "mementos", "conversations", "sessions"] : [opts.source];
|
|
23652
|
-
const
|
|
24323
|
+
const now2 = Date.now();
|
|
23653
24324
|
const db = getDb();
|
|
23654
24325
|
const gathererMap = {
|
|
23655
24326
|
todos: (o) => Promise.resolve().then(() => (init_todos(), exports_todos)).then((m) => m.gatherFromTodos(o)),
|
|
@@ -23672,17 +24343,17 @@ function registerDataCommands(program2) {
|
|
|
23672
24343
|
printInfo(` No examples found in ${source}.`);
|
|
23673
24344
|
continue;
|
|
23674
24345
|
}
|
|
23675
|
-
const fileName = `${source}-${
|
|
24346
|
+
const fileName = `${source}-${now2}.jsonl`;
|
|
23676
24347
|
const filePath = join13(opts.output, fileName);
|
|
23677
24348
|
writeFileSync5(filePath, examples.map((e) => JSON.stringify(e)).join(`
|
|
23678
24349
|
`) + `
|
|
23679
24350
|
`, "utf8");
|
|
23680
24351
|
await db.insert(trainingDatasets).values({
|
|
23681
|
-
id:
|
|
24352
|
+
id: randomUUID6(),
|
|
23682
24353
|
source,
|
|
23683
24354
|
filePath,
|
|
23684
24355
|
exampleCount: count,
|
|
23685
|
-
createdAt:
|
|
24356
|
+
createdAt: now2
|
|
23686
24357
|
});
|
|
23687
24358
|
printSuccess(` \u2713 ${count} examples \u2192 ${filePath}`);
|
|
23688
24359
|
totalExamples += count;
|
|
@@ -23768,7 +24439,7 @@ function registerDataCommands(program2) {
|
|
|
23768
24439
|
`, "utf8");
|
|
23769
24440
|
const db = getDb();
|
|
23770
24441
|
await db.insert(trainingDatasets).values({
|
|
23771
|
-
id:
|
|
24442
|
+
id: randomUUID6(),
|
|
23772
24443
|
source: "mixed",
|
|
23773
24444
|
filePath: opts.output,
|
|
23774
24445
|
exampleCount: finalLines.length,
|
|
@@ -24229,4 +24900,5 @@ feedbackCmd.command("list").description("List locally saved feedback").option("-
|
|
|
24229
24900
|
]));
|
|
24230
24901
|
});
|
|
24231
24902
|
registerCloudCommands2(program2);
|
|
24903
|
+
registerEventsCommands(program2, { source: "brains" });
|
|
24232
24904
|
program2.parse();
|