@hasna/todos 0.11.7 → 0.11.8
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 +10539 -8920
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrate.d.ts +14 -0
- package/dist/db/pg-migrate.d.ts.map +1 -0
- package/dist/db/templates.d.ts +11 -0
- package/dist/db/templates.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9685 -8
- package/dist/mcp/index.js +1984 -442
- package/dist/server/index.js +87 -2
- package/package.json +2 -2
package/dist/mcp/index.js
CHANGED
|
@@ -40,6 +40,64 @@ var __export = (target, all) => {
|
|
|
40
40
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
41
41
|
|
|
42
42
|
// node_modules/@hasna/cloud/dist/index.js
|
|
43
|
+
var exports_dist = {};
|
|
44
|
+
__export(exports_dist, {
|
|
45
|
+
translateSql: () => translateSql,
|
|
46
|
+
translateParams: () => translateParams,
|
|
47
|
+
translateDdl: () => translateDdl,
|
|
48
|
+
syncPush: () => syncPush,
|
|
49
|
+
syncPull: () => syncPull,
|
|
50
|
+
storeConflicts: () => storeConflicts,
|
|
51
|
+
setupAutoSync: () => setupAutoSync,
|
|
52
|
+
sendFeedback: () => sendFeedback,
|
|
53
|
+
saveFeedback: () => saveFeedback,
|
|
54
|
+
saveCloudConfig: () => saveCloudConfig,
|
|
55
|
+
runScheduledSync: () => runScheduledSync,
|
|
56
|
+
resolveConflicts: () => resolveConflicts,
|
|
57
|
+
resolveConflict: () => resolveConflict,
|
|
58
|
+
resetSyncMeta: () => resetSyncMeta,
|
|
59
|
+
resetAllSyncMeta: () => resetAllSyncMeta,
|
|
60
|
+
removeSyncSchedule: () => removeSyncSchedule,
|
|
61
|
+
registerSyncSchedule: () => registerSyncSchedule,
|
|
62
|
+
registerCloudTools: () => registerCloudTools,
|
|
63
|
+
registerCloudCommands: () => registerCloudCommands,
|
|
64
|
+
purgeResolvedConflicts: () => purgeResolvedConflicts,
|
|
65
|
+
parseInterval: () => parseInterval,
|
|
66
|
+
minutesToCron: () => minutesToCron,
|
|
67
|
+
migrateDotfile: () => migrateDotfile,
|
|
68
|
+
listSqliteTables: () => listSqliteTables,
|
|
69
|
+
listPgTables: () => listPgTables,
|
|
70
|
+
listFeedback: () => listFeedback,
|
|
71
|
+
listConflicts: () => listConflicts,
|
|
72
|
+
incrementalSyncPush: () => incrementalSyncPush,
|
|
73
|
+
incrementalSyncPull: () => incrementalSyncPull,
|
|
74
|
+
hasLegacyDotfile: () => hasLegacyDotfile,
|
|
75
|
+
getWinningData: () => getWinningData,
|
|
76
|
+
getSyncScheduleStatus: () => getSyncScheduleStatus,
|
|
77
|
+
getSyncMetaForTable: () => getSyncMetaForTable,
|
|
78
|
+
getSyncMetaAll: () => getSyncMetaAll,
|
|
79
|
+
getHasnaDir: () => getHasnaDir,
|
|
80
|
+
getDbPath: () => getDbPath,
|
|
81
|
+
getDataDir: () => getDataDir,
|
|
82
|
+
getConnectionString: () => getConnectionString,
|
|
83
|
+
getConflict: () => getConflict,
|
|
84
|
+
getConfigPath: () => getConfigPath,
|
|
85
|
+
getConfigDir: () => getConfigDir,
|
|
86
|
+
getCloudConfig: () => getCloudConfig,
|
|
87
|
+
getAutoSyncConfig: () => getAutoSyncConfig,
|
|
88
|
+
ensureSyncMetaTable: () => ensureSyncMetaTable,
|
|
89
|
+
ensureFeedbackTable: () => ensureFeedbackTable,
|
|
90
|
+
ensureConflictsTable: () => ensureConflictsTable,
|
|
91
|
+
enableAutoSync: () => enableAutoSync,
|
|
92
|
+
discoverSyncableServices: () => discoverSyncableServices,
|
|
93
|
+
detectConflicts: () => detectConflicts,
|
|
94
|
+
createDatabase: () => createDatabase,
|
|
95
|
+
SyncProgressTracker: () => SyncProgressTracker,
|
|
96
|
+
SqliteAdapter: () => SqliteAdapter,
|
|
97
|
+
PgAdapterAsync: () => PgAdapterAsync,
|
|
98
|
+
PgAdapter: () => PgAdapter,
|
|
99
|
+
CloudConfigSchema: () => CloudConfigSchema
|
|
100
|
+
});
|
|
43
101
|
import { createRequire } from "module";
|
|
44
102
|
import { Database } from "bun:sqlite";
|
|
45
103
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
@@ -54,8 +112,12 @@ import {
|
|
|
54
112
|
import { homedir } from "os";
|
|
55
113
|
import { join, relative } from "path";
|
|
56
114
|
import { hostname } from "os";
|
|
115
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
57
116
|
import { homedir as homedir3 } from "os";
|
|
58
117
|
import { join as join3 } from "path";
|
|
118
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
|
|
119
|
+
import { join as join4 } from "path";
|
|
120
|
+
import { join as join5, dirname } from "path";
|
|
59
121
|
function __accessProp2(key) {
|
|
60
122
|
return this[key];
|
|
61
123
|
}
|
|
@@ -96,6 +158,17 @@ function sqliteToPostgres(sql) {
|
|
|
96
158
|
out = out.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
|
|
97
159
|
return out;
|
|
98
160
|
}
|
|
161
|
+
function translateDdl(ddl, dialect) {
|
|
162
|
+
if (dialect === "sqlite")
|
|
163
|
+
return ddl;
|
|
164
|
+
let out = ddl;
|
|
165
|
+
out = out.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/gi, "BIGSERIAL PRIMARY KEY");
|
|
166
|
+
out = out.replace(/\bAUTOINCREMENT\b/gi, "");
|
|
167
|
+
out = out.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
|
|
168
|
+
out = out.replace(/\bBLOB\b/gi, "BYTEA");
|
|
169
|
+
out = sqliteToPostgres(out);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
99
172
|
|
|
100
173
|
class SqliteAdapter {
|
|
101
174
|
db;
|
|
@@ -919,6 +992,45 @@ function getDbPath(serviceName) {
|
|
|
919
992
|
const dir = getDataDir(serviceName);
|
|
920
993
|
return join(dir, `${serviceName}.db`);
|
|
921
994
|
}
|
|
995
|
+
function migrateDotfile(serviceName) {
|
|
996
|
+
const legacyDir = join(homedir(), `.${serviceName}`);
|
|
997
|
+
const newDir = join(HASNA_DIR, serviceName);
|
|
998
|
+
if (!existsSync(legacyDir))
|
|
999
|
+
return [];
|
|
1000
|
+
if (existsSync(newDir))
|
|
1001
|
+
return [];
|
|
1002
|
+
mkdirSync(newDir, { recursive: true });
|
|
1003
|
+
const migrated = [];
|
|
1004
|
+
copyDirRecursive(legacyDir, newDir, legacyDir, migrated);
|
|
1005
|
+
return migrated;
|
|
1006
|
+
}
|
|
1007
|
+
function copyDirRecursive(src, dest, root, migrated) {
|
|
1008
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
1009
|
+
for (const entry of entries) {
|
|
1010
|
+
const srcPath = join(src, entry.name);
|
|
1011
|
+
const destPath = join(dest, entry.name);
|
|
1012
|
+
if (entry.isDirectory()) {
|
|
1013
|
+
mkdirSync(destPath, { recursive: true });
|
|
1014
|
+
copyDirRecursive(srcPath, destPath, root, migrated);
|
|
1015
|
+
} else {
|
|
1016
|
+
copyFileSync(srcPath, destPath);
|
|
1017
|
+
migrated.push(relative(root, srcPath));
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function hasLegacyDotfile(serviceName) {
|
|
1022
|
+
return existsSync(join(homedir(), `.${serviceName}`));
|
|
1023
|
+
}
|
|
1024
|
+
function getHasnaDir() {
|
|
1025
|
+
mkdirSync(HASNA_DIR, { recursive: true });
|
|
1026
|
+
return HASNA_DIR;
|
|
1027
|
+
}
|
|
1028
|
+
function getConfigDir() {
|
|
1029
|
+
return CONFIG_DIR;
|
|
1030
|
+
}
|
|
1031
|
+
function getConfigPath() {
|
|
1032
|
+
return CONFIG_PATH;
|
|
1033
|
+
}
|
|
922
1034
|
function getCloudConfig() {
|
|
923
1035
|
if (!existsSync2(CONFIG_PATH)) {
|
|
924
1036
|
return CloudConfigSchema.parse({});
|
|
@@ -930,6 +1042,11 @@ function getCloudConfig() {
|
|
|
930
1042
|
return CloudConfigSchema.parse({});
|
|
931
1043
|
}
|
|
932
1044
|
}
|
|
1045
|
+
function saveCloudConfig(config) {
|
|
1046
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
1047
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
1048
|
+
`, "utf-8");
|
|
1049
|
+
}
|
|
933
1050
|
function getConnectionString(dbName) {
|
|
934
1051
|
const config = getCloudConfig();
|
|
935
1052
|
const { host, port, username, password_env, ssl } = config.rds;
|
|
@@ -1079,42 +1196,100 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
1079
1196
|
primaryKey: pkOption
|
|
1080
1197
|
} = options;
|
|
1081
1198
|
const results = [];
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const result = {
|
|
1085
|
-
table,
|
|
1086
|
-
rowsRead: 0,
|
|
1087
|
-
rowsWritten: 0,
|
|
1088
|
-
rowsSkipped: 0,
|
|
1089
|
-
errors: []
|
|
1090
|
-
};
|
|
1199
|
+
const sqliteTarget = !isAsyncAdapter(target) ? target : null;
|
|
1200
|
+
if (sqliteTarget) {
|
|
1091
1201
|
try {
|
|
1092
|
-
|
|
1202
|
+
sqliteTarget.exec("PRAGMA foreign_keys = OFF");
|
|
1203
|
+
} catch {}
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
for (let i = 0;i < tables.length; i++) {
|
|
1207
|
+
const table = tables[i];
|
|
1208
|
+
const result = {
|
|
1093
1209
|
table,
|
|
1094
|
-
phase: "reading",
|
|
1095
1210
|
rowsRead: 0,
|
|
1096
1211
|
rowsWritten: 0,
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
result.rowsRead = rows.length;
|
|
1102
|
-
if (rows.length === 0) {
|
|
1212
|
+
rowsSkipped: 0,
|
|
1213
|
+
errors: []
|
|
1214
|
+
};
|
|
1215
|
+
try {
|
|
1103
1216
|
onProgress?.({
|
|
1104
1217
|
table,
|
|
1105
|
-
phase: "
|
|
1218
|
+
phase: "reading",
|
|
1106
1219
|
rowsRead: 0,
|
|
1107
1220
|
rowsWritten: 0,
|
|
1108
1221
|
totalTables: tables.length,
|
|
1109
1222
|
currentTableIndex: i
|
|
1110
1223
|
});
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1224
|
+
const rows = await readAll(source, `SELECT * FROM "${table}"`);
|
|
1225
|
+
result.rowsRead = rows.length;
|
|
1226
|
+
if (rows.length === 0) {
|
|
1227
|
+
onProgress?.({
|
|
1228
|
+
table,
|
|
1229
|
+
phase: "done",
|
|
1230
|
+
rowsRead: 0,
|
|
1231
|
+
rowsWritten: 0,
|
|
1232
|
+
totalTables: tables.length,
|
|
1233
|
+
currentTableIndex: i
|
|
1234
|
+
});
|
|
1235
|
+
results.push(result);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
|
|
1239
|
+
const sourceColumns = Object.keys(rows[0]);
|
|
1240
|
+
let targetColumns = null;
|
|
1241
|
+
if (!isAsyncAdapter(target)) {
|
|
1242
|
+
try {
|
|
1243
|
+
const colInfo = target.all(`PRAGMA table_info("${table}")`);
|
|
1244
|
+
targetColumns = new Set(colInfo.map((c) => c.name));
|
|
1245
|
+
} catch {}
|
|
1246
|
+
} else {
|
|
1247
|
+
try {
|
|
1248
|
+
const colInfo = await target.all(`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = '${table}'`);
|
|
1249
|
+
targetColumns = new Set(colInfo.map((c) => c.column_name));
|
|
1250
|
+
} catch {}
|
|
1251
|
+
}
|
|
1252
|
+
const columns = targetColumns ? sourceColumns.filter((c) => targetColumns.has(c)) : sourceColumns;
|
|
1253
|
+
if (pkColumns.length === 0) {
|
|
1254
|
+
result.errors.push(`Table "${table}" has no primary key \u2014 inserting without conflict handling`);
|
|
1255
|
+
onProgress?.({
|
|
1256
|
+
table,
|
|
1257
|
+
phase: "writing",
|
|
1258
|
+
rowsRead: result.rowsRead,
|
|
1259
|
+
rowsWritten: 0,
|
|
1260
|
+
totalTables: tables.length,
|
|
1261
|
+
currentTableIndex: i
|
|
1262
|
+
});
|
|
1263
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
1264
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
1265
|
+
try {
|
|
1266
|
+
if (isAsyncAdapter(target)) {
|
|
1267
|
+
await batchInsertPg(target, table, columns, batch);
|
|
1268
|
+
} else {
|
|
1269
|
+
batchInsertSqlite(target, table, columns, batch);
|
|
1270
|
+
}
|
|
1271
|
+
result.rowsWritten += batch.length;
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
onProgress?.({
|
|
1277
|
+
table,
|
|
1278
|
+
phase: "done",
|
|
1279
|
+
rowsRead: result.rowsRead,
|
|
1280
|
+
rowsWritten: result.rowsWritten,
|
|
1281
|
+
totalTables: tables.length,
|
|
1282
|
+
currentTableIndex: i
|
|
1283
|
+
});
|
|
1284
|
+
results.push(result);
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
1288
|
+
if (missingPks.length > 0) {
|
|
1289
|
+
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} \u2014 skipping`);
|
|
1290
|
+
results.push(result);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1118
1293
|
onProgress?.({
|
|
1119
1294
|
table,
|
|
1120
1295
|
phase: "writing",
|
|
@@ -1123,18 +1298,27 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
1123
1298
|
totalTables: tables.length,
|
|
1124
1299
|
currentTableIndex: i
|
|
1125
1300
|
});
|
|
1301
|
+
const updateCols = columns.filter((c) => !pkColumns.includes(c));
|
|
1126
1302
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
1127
1303
|
const batch = rows.slice(offset, offset + batchSize);
|
|
1128
1304
|
try {
|
|
1129
1305
|
if (isAsyncAdapter(target)) {
|
|
1130
|
-
await
|
|
1306
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
1131
1307
|
} else {
|
|
1132
|
-
|
|
1308
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
1133
1309
|
}
|
|
1134
1310
|
result.rowsWritten += batch.length;
|
|
1135
1311
|
} catch (err) {
|
|
1136
1312
|
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
1137
1313
|
}
|
|
1314
|
+
onProgress?.({
|
|
1315
|
+
table,
|
|
1316
|
+
phase: "writing",
|
|
1317
|
+
rowsRead: result.rowsRead,
|
|
1318
|
+
rowsWritten: result.rowsWritten,
|
|
1319
|
+
totalTables: tables.length,
|
|
1320
|
+
currentTableIndex: i
|
|
1321
|
+
});
|
|
1138
1322
|
}
|
|
1139
1323
|
onProgress?.({
|
|
1140
1324
|
table,
|
|
@@ -1144,57 +1328,27 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
1144
1328
|
totalTables: tables.length,
|
|
1145
1329
|
currentTableIndex: i
|
|
1146
1330
|
});
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
}
|
|
1150
|
-
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
1151
|
-
if (missingPks.length > 0) {
|
|
1152
|
-
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} \u2014 skipping`);
|
|
1153
|
-
results.push(result);
|
|
1154
|
-
continue;
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
result.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
1155
1333
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1170
|
-
} else {
|
|
1171
|
-
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
1334
|
+
results.push(result);
|
|
1335
|
+
}
|
|
1336
|
+
} finally {
|
|
1337
|
+
if (sqliteTarget) {
|
|
1338
|
+
try {
|
|
1339
|
+
sqliteTarget.exec("PRAGMA foreign_keys = ON");
|
|
1340
|
+
} catch {}
|
|
1341
|
+
try {
|
|
1342
|
+
const violations = sqliteTarget.all("PRAGMA foreign_key_check");
|
|
1343
|
+
if (violations.length > 0) {
|
|
1344
|
+
const tables2 = [...new Set(violations.map((v) => v.table))];
|
|
1345
|
+
const msg = `FK integrity check: ${violations.length} violation(s) in table(s): ${tables2.join(", ")}`;
|
|
1346
|
+
if (results.length > 0) {
|
|
1347
|
+
results[results.length - 1].errors.push(msg);
|
|
1172
1348
|
}
|
|
1173
|
-
result.rowsWritten += batch.length;
|
|
1174
|
-
} catch (err) {
|
|
1175
|
-
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
1176
1349
|
}
|
|
1177
|
-
|
|
1178
|
-
table,
|
|
1179
|
-
phase: "writing",
|
|
1180
|
-
rowsRead: result.rowsRead,
|
|
1181
|
-
rowsWritten: result.rowsWritten,
|
|
1182
|
-
totalTables: tables.length,
|
|
1183
|
-
currentTableIndex: i
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
onProgress?.({
|
|
1187
|
-
table,
|
|
1188
|
-
phase: "done",
|
|
1189
|
-
rowsRead: result.rowsRead,
|
|
1190
|
-
rowsWritten: result.rowsWritten,
|
|
1191
|
-
totalTables: tables.length,
|
|
1192
|
-
currentTableIndex: i
|
|
1193
|
-
});
|
|
1194
|
-
} catch (err) {
|
|
1195
|
-
result.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
1350
|
+
} catch {}
|
|
1196
1351
|
}
|
|
1197
|
-
results.push(result);
|
|
1198
1352
|
}
|
|
1199
1353
|
return results;
|
|
1200
1354
|
}
|
|
@@ -1222,7 +1376,7 @@ function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batc
|
|
|
1222
1376
|
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
1223
1377
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
1224
1378
|
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
1225
|
-
const params = batch.flatMap((row) => columns.map((c) => row[c]
|
|
1379
|
+
const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
|
|
1226
1380
|
target.run(sql, ...params);
|
|
1227
1381
|
}
|
|
1228
1382
|
async function batchInsertPg(target, table, columns, batch) {
|
|
@@ -1243,9 +1397,22 @@ function batchInsertSqlite(target, table, columns, batch) {
|
|
|
1243
1397
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
1244
1398
|
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
1245
1399
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
1246
|
-
const params = batch.flatMap((row) => columns.map((c) => row[c]
|
|
1400
|
+
const params = batch.flatMap((row) => columns.map((c) => coerceForSqlite(row[c])));
|
|
1247
1401
|
target.run(sql, ...params);
|
|
1248
1402
|
}
|
|
1403
|
+
function coerceForSqlite(value) {
|
|
1404
|
+
if (value === null || value === undefined)
|
|
1405
|
+
return null;
|
|
1406
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
1407
|
+
return value;
|
|
1408
|
+
if (value instanceof Date)
|
|
1409
|
+
return value.toISOString();
|
|
1410
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
1411
|
+
return value;
|
|
1412
|
+
if (typeof value === "object")
|
|
1413
|
+
return JSON.stringify(value);
|
|
1414
|
+
return String(value);
|
|
1415
|
+
}
|
|
1249
1416
|
function isAsyncAdapter(adapter) {
|
|
1250
1417
|
return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
|
|
1251
1418
|
}
|
|
@@ -1313,6 +1480,10 @@ async function sendFeedback(feedback, db) {
|
|
|
1313
1480
|
return { sent: false, id, error: errorMsg };
|
|
1314
1481
|
}
|
|
1315
1482
|
}
|
|
1483
|
+
function listFeedback(db) {
|
|
1484
|
+
ensureFeedbackTable(db);
|
|
1485
|
+
return db.all(`SELECT id, service, version, message, email, machine_id, created_at FROM feedback ORDER BY created_at DESC`);
|
|
1486
|
+
}
|
|
1316
1487
|
|
|
1317
1488
|
class SyncProgressTracker {
|
|
1318
1489
|
db;
|
|
@@ -1433,105 +1604,749 @@ class SyncProgressTracker {
|
|
|
1433
1604
|
}
|
|
1434
1605
|
}
|
|
1435
1606
|
}
|
|
1436
|
-
function
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1607
|
+
function detectConflicts(local, remote, table, primaryKey = "id", conflictColumn = "updated_at") {
|
|
1608
|
+
const conflicts = [];
|
|
1609
|
+
const remoteMap = new Map;
|
|
1610
|
+
for (const row of remote) {
|
|
1611
|
+
const key = String(row[primaryKey]);
|
|
1612
|
+
remoteMap.set(key, row);
|
|
1613
|
+
}
|
|
1614
|
+
for (const localRow of local) {
|
|
1615
|
+
const key = String(localRow[primaryKey]);
|
|
1616
|
+
const remoteRow = remoteMap.get(key);
|
|
1617
|
+
if (!remoteRow)
|
|
1618
|
+
continue;
|
|
1619
|
+
const localTs = localRow[conflictColumn];
|
|
1620
|
+
const remoteTs = remoteRow[conflictColumn];
|
|
1621
|
+
if (localTs !== remoteTs) {
|
|
1622
|
+
conflicts.push({
|
|
1623
|
+
table,
|
|
1624
|
+
row_id: key,
|
|
1625
|
+
local_updated_at: String(localTs ?? ""),
|
|
1626
|
+
remote_updated_at: String(remoteTs ?? ""),
|
|
1627
|
+
local_data: { ...localRow },
|
|
1628
|
+
remote_data: { ...remoteRow },
|
|
1629
|
+
resolved: false
|
|
1630
|
+
});
|
|
1453
1631
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1632
|
+
}
|
|
1633
|
+
return conflicts;
|
|
1634
|
+
}
|
|
1635
|
+
function resolveConflicts(conflicts, strategy = "newest-wins") {
|
|
1636
|
+
return conflicts.map((conflict) => {
|
|
1637
|
+
const resolved = { ...conflict, resolved: true, resolution: strategy };
|
|
1638
|
+
switch (strategy) {
|
|
1639
|
+
case "local-wins":
|
|
1640
|
+
break;
|
|
1641
|
+
case "remote-wins":
|
|
1642
|
+
break;
|
|
1643
|
+
case "newest-wins": {
|
|
1644
|
+
const localTime = new Date(conflict.local_updated_at).getTime();
|
|
1645
|
+
const remoteTime = new Date(conflict.remote_updated_at).getTime();
|
|
1646
|
+
if (remoteTime > localTime) {
|
|
1647
|
+
resolved.resolution = "newest-wins";
|
|
1648
|
+
} else {
|
|
1649
|
+
resolved.resolution = "newest-wins";
|
|
1650
|
+
}
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1468
1653
|
}
|
|
1469
|
-
|
|
1470
|
-
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
1471
|
-
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
1472
|
-
const results = await syncPush(local, cloud, { tables: tableList });
|
|
1473
|
-
local.close();
|
|
1474
|
-
await cloud.close();
|
|
1475
|
-
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
1476
|
-
return {
|
|
1477
|
-
content: [{ type: "text", text: `Pushed ${total} rows across ${tableList.length} table(s).` }]
|
|
1478
|
-
};
|
|
1654
|
+
return resolved;
|
|
1479
1655
|
});
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1656
|
+
}
|
|
1657
|
+
function getWinningData(conflict) {
|
|
1658
|
+
if (!conflict.resolved || !conflict.resolution) {
|
|
1659
|
+
throw new Error(`Conflict for row ${conflict.row_id} is not resolved`);
|
|
1660
|
+
}
|
|
1661
|
+
switch (conflict.resolution) {
|
|
1662
|
+
case "local-wins":
|
|
1663
|
+
return conflict.local_data;
|
|
1664
|
+
case "remote-wins":
|
|
1665
|
+
return conflict.remote_data;
|
|
1666
|
+
case "newest-wins": {
|
|
1667
|
+
const localTime = new Date(conflict.local_updated_at).getTime();
|
|
1668
|
+
const remoteTime = new Date(conflict.remote_updated_at).getTime();
|
|
1669
|
+
return remoteTime >= localTime ? conflict.remote_data : conflict.local_data;
|
|
1670
|
+
}
|
|
1671
|
+
case "manual":
|
|
1672
|
+
return conflict.local_data;
|
|
1673
|
+
default:
|
|
1674
|
+
return conflict.local_data;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function ensureConflictsTable(db) {
|
|
1678
|
+
db.exec(`
|
|
1679
|
+
CREATE TABLE IF NOT EXISTS _sync_conflicts (
|
|
1680
|
+
id TEXT PRIMARY KEY,
|
|
1681
|
+
table_name TEXT,
|
|
1682
|
+
row_id TEXT,
|
|
1683
|
+
local_data TEXT,
|
|
1684
|
+
remote_data TEXT,
|
|
1685
|
+
local_updated_at TEXT,
|
|
1686
|
+
remote_updated_at TEXT,
|
|
1687
|
+
resolution TEXT,
|
|
1688
|
+
resolved_at TEXT,
|
|
1689
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
1690
|
+
)
|
|
1691
|
+
`);
|
|
1692
|
+
}
|
|
1693
|
+
function storeConflicts(db, conflicts) {
|
|
1694
|
+
ensureConflictsTable(db);
|
|
1695
|
+
for (const conflict of conflicts) {
|
|
1696
|
+
const id = `${conflict.table}:${conflict.row_id}:${Date.now()}`;
|
|
1697
|
+
db.run(`INSERT INTO _sync_conflicts (id, table_name, row_id, local_data, remote_data, local_updated_at, remote_updated_at, resolution, resolved_at)
|
|
1698
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, conflict.table, conflict.row_id, JSON.stringify(conflict.local_data), JSON.stringify(conflict.remote_data), conflict.local_updated_at, conflict.remote_updated_at, conflict.resolution ?? null, conflict.resolved ? new Date().toISOString() : null);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function listConflicts(db, opts) {
|
|
1702
|
+
ensureConflictsTable(db);
|
|
1703
|
+
let sql = `SELECT * FROM _sync_conflicts WHERE 1=1`;
|
|
1704
|
+
const params = [];
|
|
1705
|
+
if (opts?.resolved !== undefined) {
|
|
1706
|
+
if (opts.resolved) {
|
|
1707
|
+
sql += ` AND resolution IS NOT NULL AND resolved_at IS NOT NULL`;
|
|
1497
1708
|
} else {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1709
|
+
sql += ` AND (resolution IS NULL OR resolved_at IS NULL)`;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (opts?.table) {
|
|
1713
|
+
sql += ` AND table_name = ?`;
|
|
1714
|
+
params.push(opts.table);
|
|
1715
|
+
}
|
|
1716
|
+
sql += ` ORDER BY created_at DESC`;
|
|
1717
|
+
return db.all(sql, ...params);
|
|
1718
|
+
}
|
|
1719
|
+
function resolveConflict(db, conflictId, strategy) {
|
|
1720
|
+
ensureConflictsTable(db);
|
|
1721
|
+
const row = db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
|
|
1722
|
+
if (!row)
|
|
1723
|
+
return null;
|
|
1724
|
+
db.run(`UPDATE _sync_conflicts SET resolution = ?, resolved_at = datetime('now') WHERE id = ?`, strategy, conflictId);
|
|
1725
|
+
return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
|
|
1726
|
+
}
|
|
1727
|
+
function getConflict(db, conflictId) {
|
|
1728
|
+
ensureConflictsTable(db);
|
|
1729
|
+
return db.get(`SELECT * FROM _sync_conflicts WHERE id = ?`, conflictId);
|
|
1730
|
+
}
|
|
1731
|
+
function purgeResolvedConflicts(db) {
|
|
1732
|
+
ensureConflictsTable(db);
|
|
1733
|
+
const result = db.run(`DELETE FROM _sync_conflicts WHERE resolution IS NOT NULL AND resolved_at IS NOT NULL`);
|
|
1734
|
+
return result.changes;
|
|
1735
|
+
}
|
|
1736
|
+
function ensureSyncMetaTable(db) {
|
|
1737
|
+
db.exec(SYNC_META_TABLE_SQL);
|
|
1738
|
+
}
|
|
1739
|
+
function getSyncMeta(db, table) {
|
|
1740
|
+
ensureSyncMetaTable(db);
|
|
1741
|
+
return db.get(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta WHERE table_name = ?`, table) ?? null;
|
|
1742
|
+
}
|
|
1743
|
+
function upsertSyncMeta(db, meta) {
|
|
1744
|
+
ensureSyncMetaTable(db);
|
|
1745
|
+
const existing = db.get(`SELECT table_name FROM _sync_meta WHERE table_name = ?`, meta.table_name);
|
|
1746
|
+
if (existing) {
|
|
1747
|
+
db.run(`UPDATE _sync_meta SET last_synced_at = ?, last_synced_row_count = ?, direction = ? WHERE table_name = ?`, meta.last_synced_at, meta.last_synced_row_count, meta.direction, meta.table_name);
|
|
1748
|
+
} else {
|
|
1749
|
+
db.run(`INSERT INTO _sync_meta (table_name, last_synced_at, last_synced_row_count, direction) VALUES (?, ?, ?, ?)`, meta.table_name, meta.last_synced_at, meta.last_synced_row_count, meta.direction);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function transferRows(source, target, table, rows, options) {
|
|
1753
|
+
const { primaryKey = "id", conflictColumn = "updated_at" } = options;
|
|
1754
|
+
let written = 0;
|
|
1755
|
+
let skipped = 0;
|
|
1756
|
+
const errors2 = [];
|
|
1757
|
+
if (rows.length === 0)
|
|
1758
|
+
return { written, skipped, errors: errors2 };
|
|
1759
|
+
const columns = Object.keys(rows[0]);
|
|
1760
|
+
const hasConflictCol = columns.includes(conflictColumn);
|
|
1761
|
+
const hasPrimaryKey = columns.includes(primaryKey);
|
|
1762
|
+
if (!hasPrimaryKey) {
|
|
1763
|
+
errors2.push(`Table "${table}" has no "${primaryKey}" column -- skipping`);
|
|
1764
|
+
return { written, skipped, errors: errors2 };
|
|
1765
|
+
}
|
|
1766
|
+
for (const row of rows) {
|
|
1767
|
+
try {
|
|
1768
|
+
const existing = target.get(`SELECT "${primaryKey}"${hasConflictCol ? `, "${conflictColumn}"` : ""} FROM "${table}" WHERE "${primaryKey}" = ?`, row[primaryKey]);
|
|
1769
|
+
if (existing) {
|
|
1770
|
+
if (hasConflictCol && existing[conflictColumn] && row[conflictColumn]) {
|
|
1771
|
+
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
1772
|
+
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
1773
|
+
if (existingTime >= incomingTime) {
|
|
1774
|
+
skipped++;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
1779
|
+
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
1780
|
+
values.push(row[primaryKey]);
|
|
1781
|
+
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
1782
|
+
} else {
|
|
1783
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
1784
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
1785
|
+
const values = columns.map((c) => row[c]);
|
|
1786
|
+
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
1509
1787
|
}
|
|
1788
|
+
written++;
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
errors2.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
1510
1791
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1792
|
+
}
|
|
1793
|
+
return { written, skipped, errors: errors2 };
|
|
1794
|
+
}
|
|
1795
|
+
function incrementalSyncPush(local, remote, tables, options = {}) {
|
|
1796
|
+
const { conflictColumn = "updated_at", batchSize = 500 } = options;
|
|
1797
|
+
const results = [];
|
|
1798
|
+
ensureSyncMetaTable(local);
|
|
1799
|
+
for (const table of tables) {
|
|
1800
|
+
const stat = {
|
|
1801
|
+
table,
|
|
1802
|
+
total_rows: 0,
|
|
1803
|
+
synced_rows: 0,
|
|
1804
|
+
skipped_rows: 0,
|
|
1805
|
+
errors: [],
|
|
1806
|
+
first_sync: false
|
|
1517
1807
|
};
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
type: "text",
|
|
1530
|
-
text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
|
|
1808
|
+
try {
|
|
1809
|
+
const countResult = local.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
1810
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
1811
|
+
const meta = getSyncMeta(local, table);
|
|
1812
|
+
let rows;
|
|
1813
|
+
if (meta?.last_synced_at) {
|
|
1814
|
+
try {
|
|
1815
|
+
rows = local.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
1816
|
+
} catch {
|
|
1817
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
1818
|
+
stat.first_sync = true;
|
|
1531
1819
|
}
|
|
1532
|
-
|
|
1820
|
+
} else {
|
|
1821
|
+
rows = local.all(`SELECT * FROM "${table}"`);
|
|
1822
|
+
stat.first_sync = true;
|
|
1823
|
+
}
|
|
1824
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
1825
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
1826
|
+
const result = transferRows(local, remote, table, batch, options);
|
|
1827
|
+
stat.synced_rows += result.written;
|
|
1828
|
+
stat.skipped_rows += result.skipped;
|
|
1829
|
+
stat.errors.push(...result.errors);
|
|
1830
|
+
}
|
|
1831
|
+
if (rows.length === 0) {
|
|
1832
|
+
stat.skipped_rows = stat.total_rows;
|
|
1833
|
+
}
|
|
1834
|
+
const now = new Date().toISOString();
|
|
1835
|
+
upsertSyncMeta(local, {
|
|
1836
|
+
table_name: table,
|
|
1837
|
+
last_synced_at: now,
|
|
1838
|
+
last_synced_row_count: stat.synced_rows,
|
|
1839
|
+
direction: "push"
|
|
1840
|
+
});
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
1843
|
+
}
|
|
1844
|
+
results.push(stat);
|
|
1845
|
+
}
|
|
1846
|
+
return results;
|
|
1847
|
+
}
|
|
1848
|
+
function incrementalSyncPull(remote, local, tables, options = {}) {
|
|
1849
|
+
const { conflictColumn = "updated_at", batchSize = 500 } = options;
|
|
1850
|
+
const results = [];
|
|
1851
|
+
ensureSyncMetaTable(local);
|
|
1852
|
+
for (const table of tables) {
|
|
1853
|
+
const stat = {
|
|
1854
|
+
table,
|
|
1855
|
+
total_rows: 0,
|
|
1856
|
+
synced_rows: 0,
|
|
1857
|
+
skipped_rows: 0,
|
|
1858
|
+
errors: [],
|
|
1859
|
+
first_sync: false
|
|
1533
1860
|
};
|
|
1534
|
-
|
|
1861
|
+
try {
|
|
1862
|
+
const countResult = remote.get(`SELECT COUNT(*) as cnt FROM "${table}"`);
|
|
1863
|
+
stat.total_rows = countResult?.cnt ?? 0;
|
|
1864
|
+
const meta = getSyncMeta(local, table);
|
|
1865
|
+
let rows;
|
|
1866
|
+
if (meta?.last_synced_at) {
|
|
1867
|
+
try {
|
|
1868
|
+
rows = remote.all(`SELECT * FROM "${table}" WHERE "${conflictColumn}" > ?`, meta.last_synced_at);
|
|
1869
|
+
} catch {
|
|
1870
|
+
rows = remote.all(`SELECT * FROM "${table}"`);
|
|
1871
|
+
stat.first_sync = true;
|
|
1872
|
+
}
|
|
1873
|
+
} else {
|
|
1874
|
+
rows = remote.all(`SELECT * FROM "${table}"`);
|
|
1875
|
+
stat.first_sync = true;
|
|
1876
|
+
}
|
|
1877
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
1878
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
1879
|
+
const result = transferRows(remote, local, table, batch, options);
|
|
1880
|
+
stat.synced_rows += result.written;
|
|
1881
|
+
stat.skipped_rows += result.skipped;
|
|
1882
|
+
stat.errors.push(...result.errors);
|
|
1883
|
+
}
|
|
1884
|
+
if (rows.length === 0) {
|
|
1885
|
+
stat.skipped_rows = stat.total_rows;
|
|
1886
|
+
}
|
|
1887
|
+
const now = new Date().toISOString();
|
|
1888
|
+
upsertSyncMeta(local, {
|
|
1889
|
+
table_name: table,
|
|
1890
|
+
last_synced_at: now,
|
|
1891
|
+
last_synced_row_count: stat.synced_rows,
|
|
1892
|
+
direction: "pull"
|
|
1893
|
+
});
|
|
1894
|
+
} catch (err) {
|
|
1895
|
+
stat.errors.push(`Table "${table}": ${err?.message ?? String(err)}`);
|
|
1896
|
+
}
|
|
1897
|
+
results.push(stat);
|
|
1898
|
+
}
|
|
1899
|
+
return results;
|
|
1900
|
+
}
|
|
1901
|
+
function getSyncMetaAll(db) {
|
|
1902
|
+
ensureSyncMetaTable(db);
|
|
1903
|
+
return db.all(`SELECT table_name, last_synced_at, last_synced_row_count, direction FROM _sync_meta ORDER BY table_name`);
|
|
1904
|
+
}
|
|
1905
|
+
function getSyncMetaForTable(db, table) {
|
|
1906
|
+
return getSyncMeta(db, table);
|
|
1907
|
+
}
|
|
1908
|
+
function resetSyncMeta(db, table) {
|
|
1909
|
+
ensureSyncMetaTable(db);
|
|
1910
|
+
db.run(`DELETE FROM _sync_meta WHERE table_name = ?`, table);
|
|
1911
|
+
}
|
|
1912
|
+
function resetAllSyncMeta(db) {
|
|
1913
|
+
ensureSyncMetaTable(db);
|
|
1914
|
+
db.run(`DELETE FROM _sync_meta`);
|
|
1915
|
+
}
|
|
1916
|
+
function getAutoSyncConfig() {
|
|
1917
|
+
try {
|
|
1918
|
+
if (!existsSync3(AUTO_SYNC_CONFIG_PATH)) {
|
|
1919
|
+
return { ...DEFAULT_AUTO_SYNC_CONFIG };
|
|
1920
|
+
}
|
|
1921
|
+
const raw = JSON.parse(readFileSync2(AUTO_SYNC_CONFIG_PATH, "utf-8"));
|
|
1922
|
+
return {
|
|
1923
|
+
auto_sync_on_start: typeof raw.auto_sync_on_start === "boolean" ? raw.auto_sync_on_start : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_start,
|
|
1924
|
+
auto_sync_on_stop: typeof raw.auto_sync_on_stop === "boolean" ? raw.auto_sync_on_stop : DEFAULT_AUTO_SYNC_CONFIG.auto_sync_on_stop
|
|
1925
|
+
};
|
|
1926
|
+
} catch {
|
|
1927
|
+
return { ...DEFAULT_AUTO_SYNC_CONFIG };
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
function executeAutoSync(event, local, remote, tables) {
|
|
1931
|
+
const direction = event === "start" ? "pull" : "push";
|
|
1932
|
+
const result = {
|
|
1933
|
+
event,
|
|
1934
|
+
direction,
|
|
1935
|
+
success: false,
|
|
1936
|
+
tables_synced: 0,
|
|
1937
|
+
total_rows_synced: 0,
|
|
1938
|
+
errors: []
|
|
1939
|
+
};
|
|
1940
|
+
try {
|
|
1941
|
+
const stats = direction === "pull" ? incrementalSyncPull(remote, local, tables) : incrementalSyncPush(local, remote, tables);
|
|
1942
|
+
for (const s of stats) {
|
|
1943
|
+
if (s.errors.length === 0) {
|
|
1944
|
+
result.tables_synced++;
|
|
1945
|
+
}
|
|
1946
|
+
result.total_rows_synced += s.synced_rows;
|
|
1947
|
+
result.errors.push(...s.errors);
|
|
1948
|
+
}
|
|
1949
|
+
result.success = result.errors.length === 0;
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
result.errors.push(err?.message ?? String(err));
|
|
1952
|
+
}
|
|
1953
|
+
return result;
|
|
1954
|
+
}
|
|
1955
|
+
function installSignalHandlers() {
|
|
1956
|
+
if (signalHandlersInstalled)
|
|
1957
|
+
return;
|
|
1958
|
+
signalHandlersInstalled = true;
|
|
1959
|
+
const handleExit = () => {
|
|
1960
|
+
for (const fn of cleanupHandlers) {
|
|
1961
|
+
try {
|
|
1962
|
+
fn();
|
|
1963
|
+
} catch {}
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
process.on("SIGTERM", () => {
|
|
1967
|
+
handleExit();
|
|
1968
|
+
process.exit(0);
|
|
1969
|
+
});
|
|
1970
|
+
process.on("SIGINT", () => {
|
|
1971
|
+
handleExit();
|
|
1972
|
+
process.exit(0);
|
|
1973
|
+
});
|
|
1974
|
+
process.on("beforeExit", () => {
|
|
1975
|
+
handleExit();
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function setupAutoSync(serviceName, server, local, remote, tables) {
|
|
1979
|
+
const config = getAutoSyncConfig();
|
|
1980
|
+
const cloudConfig = getCloudConfig();
|
|
1981
|
+
const isSyncEnabled = cloudConfig.mode === "hybrid" || cloudConfig.mode === "cloud";
|
|
1982
|
+
const syncOnStart = () => {
|
|
1983
|
+
if (!config.auto_sync_on_start || !isSyncEnabled)
|
|
1984
|
+
return null;
|
|
1985
|
+
return executeAutoSync("start", local, remote, tables);
|
|
1986
|
+
};
|
|
1987
|
+
const syncOnStop = () => {
|
|
1988
|
+
if (!config.auto_sync_on_stop || !isSyncEnabled)
|
|
1989
|
+
return null;
|
|
1990
|
+
return executeAutoSync("stop", local, remote, tables);
|
|
1991
|
+
};
|
|
1992
|
+
if (server && typeof server.onconnect === "function") {
|
|
1993
|
+
const origOnConnect = server.onconnect;
|
|
1994
|
+
server.onconnect = (...args) => {
|
|
1995
|
+
syncOnStart();
|
|
1996
|
+
return origOnConnect.apply(server, args);
|
|
1997
|
+
};
|
|
1998
|
+
} else if (server && typeof server.on === "function") {
|
|
1999
|
+
server.on("connect", () => {
|
|
2000
|
+
syncOnStart();
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
if (server && typeof server.ondisconnect === "function") {
|
|
2004
|
+
const origOnDisconnect = server.ondisconnect;
|
|
2005
|
+
server.ondisconnect = (...args) => {
|
|
2006
|
+
syncOnStop();
|
|
2007
|
+
return origOnDisconnect.apply(server, args);
|
|
2008
|
+
};
|
|
2009
|
+
} else if (server && typeof server.on === "function") {
|
|
2010
|
+
server.on("disconnect", () => {
|
|
2011
|
+
syncOnStop();
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
installSignalHandlers();
|
|
2015
|
+
cleanupHandlers.push(() => {
|
|
2016
|
+
syncOnStop();
|
|
2017
|
+
});
|
|
2018
|
+
return { syncOnStart, syncOnStop, config };
|
|
2019
|
+
}
|
|
2020
|
+
function enableAutoSync(serviceName, mcpServer, local, remote, tables) {
|
|
2021
|
+
setupAutoSync(serviceName, mcpServer, local, remote, tables);
|
|
2022
|
+
}
|
|
2023
|
+
function discoverSyncableServices() {
|
|
2024
|
+
const hasnaDir = getHasnaDir();
|
|
2025
|
+
const services = [];
|
|
2026
|
+
try {
|
|
2027
|
+
const entries = readdirSync2(hasnaDir, { withFileTypes: true });
|
|
2028
|
+
for (const entry of entries) {
|
|
2029
|
+
if (!entry.isDirectory())
|
|
2030
|
+
continue;
|
|
2031
|
+
const dbPath = join4(hasnaDir, entry.name, `${entry.name}.db`);
|
|
2032
|
+
if (existsSync4(dbPath)) {
|
|
2033
|
+
services.push(entry.name);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
} catch {}
|
|
2037
|
+
return services;
|
|
2038
|
+
}
|
|
2039
|
+
async function runScheduledSync() {
|
|
2040
|
+
const config = getCloudConfig();
|
|
2041
|
+
if (config.mode === "local")
|
|
2042
|
+
return [];
|
|
2043
|
+
const services = discoverSyncableServices();
|
|
2044
|
+
const results = [];
|
|
2045
|
+
let remote = null;
|
|
2046
|
+
for (const service of services) {
|
|
2047
|
+
const result = {
|
|
2048
|
+
service,
|
|
2049
|
+
tables_synced: 0,
|
|
2050
|
+
total_rows_synced: 0,
|
|
2051
|
+
errors: []
|
|
2052
|
+
};
|
|
2053
|
+
try {
|
|
2054
|
+
const dbPath = join4(getDataDir(service), `${service}.db`);
|
|
2055
|
+
if (!existsSync4(dbPath)) {
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
const local = new SqliteAdapter(dbPath);
|
|
2059
|
+
const tables = listSqliteTables(local).filter((t) => !t.startsWith("_") && !t.startsWith("sqlite_"));
|
|
2060
|
+
if (tables.length === 0) {
|
|
2061
|
+
local.close();
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const connStr = getConnectionString(service);
|
|
2066
|
+
remote = new PgAdapterAsync(connStr);
|
|
2067
|
+
} catch (err) {
|
|
2068
|
+
result.errors.push(`Connection failed: ${err?.message ?? String(err)}`);
|
|
2069
|
+
local.close();
|
|
2070
|
+
results.push(result);
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
const stats = incrementalSyncPush(local, remote, tables);
|
|
2074
|
+
for (const s of stats) {
|
|
2075
|
+
if (s.errors.length === 0) {
|
|
2076
|
+
result.tables_synced++;
|
|
2077
|
+
}
|
|
2078
|
+
result.total_rows_synced += s.synced_rows;
|
|
2079
|
+
result.errors.push(...s.errors);
|
|
2080
|
+
}
|
|
2081
|
+
local.close();
|
|
2082
|
+
await remote.close();
|
|
2083
|
+
remote = null;
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
result.errors.push(err?.message ?? String(err));
|
|
2086
|
+
}
|
|
2087
|
+
results.push(result);
|
|
2088
|
+
}
|
|
2089
|
+
if (remote) {
|
|
2090
|
+
try {
|
|
2091
|
+
await remote.close();
|
|
2092
|
+
} catch {}
|
|
2093
|
+
}
|
|
2094
|
+
return results;
|
|
2095
|
+
}
|
|
2096
|
+
function getWorkerPath() {
|
|
2097
|
+
const dir = typeof import.meta.dir === "string" ? import.meta.dir : dirname(import.meta.url.replace("file://", ""));
|
|
2098
|
+
const tsPath = join5(dir, "scheduled-sync.ts");
|
|
2099
|
+
const jsPath = join5(dir, "scheduled-sync.js");
|
|
2100
|
+
try {
|
|
2101
|
+
const { existsSync: existsSync5 } = __require("fs");
|
|
2102
|
+
if (existsSync5(tsPath))
|
|
2103
|
+
return tsPath;
|
|
2104
|
+
} catch {}
|
|
2105
|
+
return jsPath;
|
|
2106
|
+
}
|
|
2107
|
+
function parseInterval(input) {
|
|
2108
|
+
const trimmed = input.trim().toLowerCase();
|
|
2109
|
+
const hourMatch = trimmed.match(/^(\d+)\s*h$/);
|
|
2110
|
+
if (hourMatch) {
|
|
2111
|
+
const hours = parseInt(hourMatch[1], 10);
|
|
2112
|
+
if (hours <= 0) {
|
|
2113
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
2114
|
+
}
|
|
2115
|
+
return hours * 60;
|
|
2116
|
+
}
|
|
2117
|
+
const minMatch = trimmed.match(/^(\d+)\s*m$/);
|
|
2118
|
+
if (minMatch) {
|
|
2119
|
+
const mins = parseInt(minMatch[1], 10);
|
|
2120
|
+
if (mins <= 0) {
|
|
2121
|
+
throw new Error(`Invalid interval "${input}". Value must be greater than 0.`);
|
|
2122
|
+
}
|
|
2123
|
+
return mins;
|
|
2124
|
+
}
|
|
2125
|
+
const plain = parseInt(trimmed, 10);
|
|
2126
|
+
if (!isNaN(plain) && plain > 0) {
|
|
2127
|
+
return plain;
|
|
2128
|
+
}
|
|
2129
|
+
throw new Error(`Invalid interval "${input}". Use formats like: 5m, 10m, 1h, or a plain number of minutes.`);
|
|
2130
|
+
}
|
|
2131
|
+
function minutesToCron(minutes) {
|
|
2132
|
+
if (minutes <= 0) {
|
|
2133
|
+
throw new Error("Interval must be greater than 0 minutes.");
|
|
2134
|
+
}
|
|
2135
|
+
if (minutes < 60) {
|
|
2136
|
+
return `*/${minutes} * * * *`;
|
|
2137
|
+
}
|
|
2138
|
+
const hours = Math.floor(minutes / 60);
|
|
2139
|
+
const remainderMins = minutes % 60;
|
|
2140
|
+
if (remainderMins === 0 && hours <= 24) {
|
|
2141
|
+
return `0 */${hours} * * *`;
|
|
2142
|
+
}
|
|
2143
|
+
return `*/${minutes} * * * *`;
|
|
2144
|
+
}
|
|
2145
|
+
async function registerSyncSchedule(intervalMinutes) {
|
|
2146
|
+
if (intervalMinutes <= 0) {
|
|
2147
|
+
throw new Error("Interval must be a positive number of minutes.");
|
|
2148
|
+
}
|
|
2149
|
+
const cronExpr = minutesToCron(intervalMinutes);
|
|
2150
|
+
const workerPath = getWorkerPath();
|
|
2151
|
+
await Bun.cron(workerPath, cronExpr, CRON_TITLE);
|
|
2152
|
+
const config = getCloudConfig();
|
|
2153
|
+
config.sync.schedule_minutes = intervalMinutes;
|
|
2154
|
+
saveCloudConfig(config);
|
|
2155
|
+
}
|
|
2156
|
+
async function removeSyncSchedule() {
|
|
2157
|
+
await Bun.cron.remove(CRON_TITLE);
|
|
2158
|
+
const config = getCloudConfig();
|
|
2159
|
+
config.sync.schedule_minutes = 0;
|
|
2160
|
+
saveCloudConfig(config);
|
|
2161
|
+
}
|
|
2162
|
+
function getSyncScheduleStatus() {
|
|
2163
|
+
const config = getCloudConfig();
|
|
2164
|
+
const minutes = config.sync.schedule_minutes;
|
|
2165
|
+
const registered = minutes > 0;
|
|
2166
|
+
return {
|
|
2167
|
+
registered,
|
|
2168
|
+
schedule_minutes: minutes,
|
|
2169
|
+
cron_expression: registered ? minutesToCron(minutes) : null
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function registerCloudTools(server, serviceName) {
|
|
2173
|
+
server.tool(`${serviceName}_cloud_status`, "Show cloud configuration and connection health", {}, async () => {
|
|
2174
|
+
const config = getCloudConfig();
|
|
2175
|
+
const lines = [
|
|
2176
|
+
`Mode: ${config.mode}`,
|
|
2177
|
+
`Service: ${serviceName}`,
|
|
2178
|
+
`RDS Host: ${config.rds.host || "(not configured)"}`
|
|
2179
|
+
];
|
|
2180
|
+
if (config.rds.host && config.rds.username) {
|
|
2181
|
+
try {
|
|
2182
|
+
const pg2 = new PgAdapterAsync(getConnectionString("postgres"));
|
|
2183
|
+
await pg2.get("SELECT 1 as ok");
|
|
2184
|
+
lines.push("PostgreSQL: connected");
|
|
2185
|
+
await pg2.close();
|
|
2186
|
+
} catch (err) {
|
|
2187
|
+
lines.push(`PostgreSQL: failed \u2014 ${err?.message}`);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return { content: [{ type: "text", text: lines.join(`
|
|
2191
|
+
`) }] };
|
|
2192
|
+
});
|
|
2193
|
+
server.tool(`${serviceName}_cloud_push`, "Push local data to cloud PostgreSQL", {
|
|
2194
|
+
tables: exports_external2.string().optional().describe("Comma-separated table names (default: all)")
|
|
2195
|
+
}, async ({ tables: tablesStr }) => {
|
|
2196
|
+
const config = getCloudConfig();
|
|
2197
|
+
if (config.mode === "local") {
|
|
2198
|
+
return {
|
|
2199
|
+
content: [
|
|
2200
|
+
{ type: "text", text: "Error: cloud mode not configured." }
|
|
2201
|
+
],
|
|
2202
|
+
isError: true
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
2206
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
2207
|
+
const tableList = tablesStr ? tablesStr.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
2208
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
2209
|
+
local.close();
|
|
2210
|
+
await cloud.close();
|
|
2211
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2212
|
+
return {
|
|
2213
|
+
content: [{ type: "text", text: `Pushed ${total} rows across ${tableList.length} table(s).` }]
|
|
2214
|
+
};
|
|
2215
|
+
});
|
|
2216
|
+
server.tool(`${serviceName}_cloud_pull`, "Pull cloud PostgreSQL data to local", {
|
|
2217
|
+
tables: exports_external2.string().optional().describe("Comma-separated table names (default: all)")
|
|
2218
|
+
}, async ({ tables: tablesStr }) => {
|
|
2219
|
+
const config = getCloudConfig();
|
|
2220
|
+
if (config.mode === "local") {
|
|
2221
|
+
return {
|
|
2222
|
+
content: [
|
|
2223
|
+
{ type: "text", text: "Error: cloud mode not configured." }
|
|
2224
|
+
],
|
|
2225
|
+
isError: true
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
2229
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
2230
|
+
let tableList;
|
|
2231
|
+
if (tablesStr) {
|
|
2232
|
+
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
2233
|
+
} else {
|
|
2234
|
+
try {
|
|
2235
|
+
tableList = await listPgTables(cloud);
|
|
2236
|
+
} catch {
|
|
2237
|
+
local.close();
|
|
2238
|
+
await cloud.close();
|
|
2239
|
+
return {
|
|
2240
|
+
content: [
|
|
2241
|
+
{ type: "text", text: "Error: failed to list cloud tables." }
|
|
2242
|
+
],
|
|
2243
|
+
isError: true
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
2248
|
+
local.close();
|
|
2249
|
+
await cloud.close();
|
|
2250
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2251
|
+
return {
|
|
2252
|
+
content: [{ type: "text", text: `Pulled ${total} rows across ${tableList.length} table(s).` }]
|
|
2253
|
+
};
|
|
2254
|
+
});
|
|
2255
|
+
server.tool(`${serviceName}_cloud_feedback`, "Send feedback for this service", {
|
|
2256
|
+
message: exports_external2.string().describe("Feedback message"),
|
|
2257
|
+
email: exports_external2.string().optional().describe("Contact email")
|
|
2258
|
+
}, async ({ message, email }) => {
|
|
2259
|
+
const db = createDatabase({ service: "cloud" });
|
|
2260
|
+
const result = await sendFeedback({ service: serviceName, message, email }, db);
|
|
2261
|
+
db.close();
|
|
2262
|
+
return {
|
|
2263
|
+
content: [
|
|
2264
|
+
{
|
|
2265
|
+
type: "text",
|
|
2266
|
+
text: result.sent ? `Feedback sent (id: ${result.id})` : `Saved locally (id: ${result.id}): ${result.error}`
|
|
2267
|
+
}
|
|
2268
|
+
]
|
|
2269
|
+
};
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
function registerCloudCommands(program, serviceName) {
|
|
2273
|
+
const cloudCmd = program.command("cloud").description("Cloud sync and feedback commands");
|
|
2274
|
+
cloudCmd.command("status").description("Show cloud config and connection health").action(async () => {
|
|
2275
|
+
const config = getCloudConfig();
|
|
2276
|
+
console.log("Mode:", config.mode);
|
|
2277
|
+
console.log("RDS Host:", config.rds.host || "(not configured)");
|
|
2278
|
+
console.log("Service:", serviceName);
|
|
2279
|
+
if (config.rds.host && config.rds.username) {
|
|
2280
|
+
try {
|
|
2281
|
+
const connStr = getConnectionString("postgres");
|
|
2282
|
+
const pg2 = new PgAdapterAsync(connStr);
|
|
2283
|
+
await pg2.get("SELECT 1 as ok");
|
|
2284
|
+
console.log("PostgreSQL: connected");
|
|
2285
|
+
await pg2.close();
|
|
2286
|
+
} catch (err) {
|
|
2287
|
+
console.log("PostgreSQL: connection failed \u2014", err?.message);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
cloudCmd.command("push").description("Push local data to cloud").option("--tables <tables>", "Comma-separated table names").action(async (opts) => {
|
|
2292
|
+
const config = getCloudConfig();
|
|
2293
|
+
if (config.mode === "local") {
|
|
2294
|
+
console.error("Error: mode is 'local'. Run `cloud setup` first.");
|
|
2295
|
+
process.exit(1);
|
|
2296
|
+
}
|
|
2297
|
+
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
2298
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
2299
|
+
const tables = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables(local);
|
|
2300
|
+
const results = await syncPush(local, cloud, {
|
|
2301
|
+
tables,
|
|
2302
|
+
onProgress: (p) => {
|
|
2303
|
+
if (p.phase === "done") {
|
|
2304
|
+
console.log(` ${p.table}: ${p.rowsWritten} rows pushed`);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
local.close();
|
|
2309
|
+
await cloud.close();
|
|
2310
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2311
|
+
console.log(`Done. ${total} rows pushed.`);
|
|
2312
|
+
});
|
|
2313
|
+
cloudCmd.command("pull").description("Pull cloud data to local").option("--tables <tables>", "Comma-separated table names").action(async (opts) => {
|
|
2314
|
+
const config = getCloudConfig();
|
|
2315
|
+
if (config.mode === "local") {
|
|
2316
|
+
console.error("Error: mode is 'local'. Run `cloud setup` first.");
|
|
2317
|
+
process.exit(1);
|
|
2318
|
+
}
|
|
2319
|
+
const local = new SqliteAdapter(getDbPath(serviceName));
|
|
2320
|
+
const cloud = new PgAdapterAsync(getConnectionString(serviceName));
|
|
2321
|
+
let tables;
|
|
2322
|
+
if (opts.tables) {
|
|
2323
|
+
tables = opts.tables.split(",").map((t) => t.trim());
|
|
2324
|
+
} else {
|
|
2325
|
+
tables = await listPgTables(cloud);
|
|
2326
|
+
}
|
|
2327
|
+
const results = await syncPull(cloud, local, {
|
|
2328
|
+
tables,
|
|
2329
|
+
onProgress: (p) => {
|
|
2330
|
+
if (p.phase === "done") {
|
|
2331
|
+
console.log(` ${p.table}: ${p.rowsWritten} rows pulled`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
local.close();
|
|
2336
|
+
await cloud.close();
|
|
2337
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2338
|
+
console.log(`Done. ${total} rows pulled.`);
|
|
2339
|
+
});
|
|
2340
|
+
cloudCmd.command("feedback").description("Send feedback").requiredOption("--message <msg>", "Feedback message").option("--email <email>", "Contact email").action(async (opts) => {
|
|
2341
|
+
const db = createDatabase({ service: "cloud" });
|
|
2342
|
+
const result = await sendFeedback({ service: serviceName, message: opts.message, email: opts.email }, db);
|
|
2343
|
+
db.close();
|
|
2344
|
+
if (result.sent) {
|
|
2345
|
+
console.log(`Feedback sent (id: ${result.id})`);
|
|
2346
|
+
} else {
|
|
2347
|
+
console.log(`Feedback saved locally (id: ${result.id}): ${result.error}`);
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
1535
2350
|
}
|
|
1536
2351
|
var __create, __getProtoOf, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node, __toESMCache_esm, __toESM = (mod, isNodeMode, target) => {
|
|
1537
2352
|
var canCache = mod != null && typeof mod === "object";
|
|
@@ -1785,7 +2600,13 @@ CREATE TABLE IF NOT EXISTS feedback (
|
|
|
1785
2600
|
email TEXT DEFAULT '',
|
|
1786
2601
|
machine_id TEXT DEFAULT '',
|
|
1787
2602
|
created_at TEXT DEFAULT (datetime('now'))
|
|
1788
|
-
)`,
|
|
2603
|
+
)`, SYNC_META_TABLE_SQL = `
|
|
2604
|
+
CREATE TABLE IF NOT EXISTS _sync_meta (
|
|
2605
|
+
table_name TEXT PRIMARY KEY,
|
|
2606
|
+
last_synced_at TEXT,
|
|
2607
|
+
last_synced_row_count INTEGER DEFAULT 0,
|
|
2608
|
+
direction TEXT DEFAULT 'push'
|
|
2609
|
+
)`, AUTO_SYNC_CONFIG_PATH, DEFAULT_AUTO_SYNC_CONFIG, cleanupHandlers, signalHandlersInstalled = false, CRON_TITLE = "hasna-cloud-sync";
|
|
1789
2610
|
var init_dist = __esm(() => {
|
|
1790
2611
|
__create = Object.create;
|
|
1791
2612
|
__getProtoOf = Object.getPrototypeOf;
|
|
@@ -9724,11 +10545,19 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
|
|
|
9724
10545
|
}).default({}),
|
|
9725
10546
|
mode: exports_external2.enum(["local", "cloud", "hybrid"]).default("local"),
|
|
9726
10547
|
auto_sync_interval_minutes: exports_external2.number().default(0),
|
|
9727
|
-
feedback_endpoint: exports_external2.string().default("https://feedback.hasna.com/api/v1/feedback")
|
|
10548
|
+
feedback_endpoint: exports_external2.string().default("https://feedback.hasna.com/api/v1/feedback"),
|
|
10549
|
+
sync: exports_external2.object({
|
|
10550
|
+
schedule_minutes: exports_external2.number().default(0)
|
|
10551
|
+
}).default({})
|
|
9728
10552
|
});
|
|
9729
10553
|
CONFIG_DIR = join2(homedir2(), ".hasna", "cloud");
|
|
9730
10554
|
CONFIG_PATH = join2(CONFIG_DIR, "config.json");
|
|
9731
10555
|
AUTO_SYNC_CONFIG_PATH = join3(homedir3(), ".hasna", "cloud", "config.json");
|
|
10556
|
+
DEFAULT_AUTO_SYNC_CONFIG = {
|
|
10557
|
+
auto_sync_on_start: true,
|
|
10558
|
+
auto_sync_on_stop: true
|
|
10559
|
+
};
|
|
10560
|
+
cleanupHandlers = [];
|
|
9732
10561
|
});
|
|
9733
10562
|
|
|
9734
10563
|
// src/types/index.ts
|
|
@@ -9851,18 +10680,18 @@ __export(exports_database, {
|
|
|
9851
10680
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
9852
10681
|
});
|
|
9853
10682
|
import { Database as Database2 } from "bun:sqlite";
|
|
9854
|
-
import { existsSync as
|
|
9855
|
-
import { dirname, join as
|
|
10683
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
10684
|
+
import { dirname as dirname2, join as join6, resolve } from "path";
|
|
9856
10685
|
function isInMemoryDb(path) {
|
|
9857
10686
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
9858
10687
|
}
|
|
9859
10688
|
function findNearestTodosDb(startDir) {
|
|
9860
10689
|
let dir = resolve(startDir);
|
|
9861
10690
|
while (true) {
|
|
9862
|
-
const candidate =
|
|
9863
|
-
if (
|
|
10691
|
+
const candidate = join6(dir, ".todos", "todos.db");
|
|
10692
|
+
if (existsSync5(candidate))
|
|
9864
10693
|
return candidate;
|
|
9865
|
-
const parent =
|
|
10694
|
+
const parent = dirname2(dir);
|
|
9866
10695
|
if (parent === dir)
|
|
9867
10696
|
break;
|
|
9868
10697
|
dir = parent;
|
|
@@ -9872,9 +10701,9 @@ function findNearestTodosDb(startDir) {
|
|
|
9872
10701
|
function findGitRoot(startDir) {
|
|
9873
10702
|
let dir = resolve(startDir);
|
|
9874
10703
|
while (true) {
|
|
9875
|
-
if (
|
|
10704
|
+
if (existsSync5(join6(dir, ".git")))
|
|
9876
10705
|
return dir;
|
|
9877
|
-
const parent =
|
|
10706
|
+
const parent = dirname2(dir);
|
|
9878
10707
|
if (parent === dir)
|
|
9879
10708
|
break;
|
|
9880
10709
|
dir = parent;
|
|
@@ -9895,13 +10724,13 @@ function getDbPath2() {
|
|
|
9895
10724
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
9896
10725
|
const gitRoot = findGitRoot(cwd);
|
|
9897
10726
|
if (gitRoot) {
|
|
9898
|
-
return
|
|
10727
|
+
return join6(gitRoot, ".todos", "todos.db");
|
|
9899
10728
|
}
|
|
9900
10729
|
}
|
|
9901
10730
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
9902
|
-
const newPath =
|
|
9903
|
-
const legacyPath =
|
|
9904
|
-
if (!
|
|
10731
|
+
const newPath = join6(home, ".hasna", "todos", "todos.db");
|
|
10732
|
+
const legacyPath = join6(home, ".todos", "todos.db");
|
|
10733
|
+
if (!existsSync5(newPath) && existsSync5(legacyPath)) {
|
|
9905
10734
|
return legacyPath;
|
|
9906
10735
|
}
|
|
9907
10736
|
return newPath;
|
|
@@ -9909,8 +10738,8 @@ function getDbPath2() {
|
|
|
9909
10738
|
function ensureDir(filePath) {
|
|
9910
10739
|
if (isInMemoryDb(filePath))
|
|
9911
10740
|
return;
|
|
9912
|
-
const dir =
|
|
9913
|
-
if (!
|
|
10741
|
+
const dir = dirname2(resolve(filePath));
|
|
10742
|
+
if (!existsSync5(dir)) {
|
|
9914
10743
|
mkdirSync3(dir, { recursive: true });
|
|
9915
10744
|
}
|
|
9916
10745
|
}
|
|
@@ -10877,20 +11706,20 @@ var init_projects = __esm(() => {
|
|
|
10877
11706
|
});
|
|
10878
11707
|
|
|
10879
11708
|
// src/lib/sync-utils.ts
|
|
10880
|
-
import { existsSync as
|
|
10881
|
-
import { join as
|
|
11709
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
11710
|
+
import { join as join8 } from "path";
|
|
10882
11711
|
function ensureDir2(dir) {
|
|
10883
|
-
if (!
|
|
11712
|
+
if (!existsSync7(dir))
|
|
10884
11713
|
mkdirSync5(dir, { recursive: true });
|
|
10885
11714
|
}
|
|
10886
11715
|
function listJsonFiles(dir) {
|
|
10887
|
-
if (!
|
|
11716
|
+
if (!existsSync7(dir))
|
|
10888
11717
|
return [];
|
|
10889
|
-
return
|
|
11718
|
+
return readdirSync3(dir).filter((f) => f.endsWith(".json"));
|
|
10890
11719
|
}
|
|
10891
11720
|
function readJsonFile(path) {
|
|
10892
11721
|
try {
|
|
10893
|
-
return JSON.parse(
|
|
11722
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
10894
11723
|
} catch {
|
|
10895
11724
|
return null;
|
|
10896
11725
|
}
|
|
@@ -10900,14 +11729,14 @@ function writeJsonFile(path, data) {
|
|
|
10900
11729
|
`);
|
|
10901
11730
|
}
|
|
10902
11731
|
function readHighWaterMark(dir) {
|
|
10903
|
-
const path =
|
|
10904
|
-
if (!
|
|
11732
|
+
const path = join8(dir, ".highwatermark");
|
|
11733
|
+
if (!existsSync7(path))
|
|
10905
11734
|
return 1;
|
|
10906
|
-
const val = parseInt(
|
|
11735
|
+
const val = parseInt(readFileSync3(path, "utf-8").trim(), 10);
|
|
10907
11736
|
return isNaN(val) ? 1 : val;
|
|
10908
11737
|
}
|
|
10909
11738
|
function writeHighWaterMark(dir, value) {
|
|
10910
|
-
writeFileSync2(
|
|
11739
|
+
writeFileSync2(join8(dir, ".highwatermark"), String(value));
|
|
10911
11740
|
}
|
|
10912
11741
|
function getFileMtimeMs(path) {
|
|
10913
11742
|
try {
|
|
@@ -10933,18 +11762,18 @@ var init_sync_utils = __esm(() => {
|
|
|
10933
11762
|
});
|
|
10934
11763
|
|
|
10935
11764
|
// src/lib/config.ts
|
|
10936
|
-
import { existsSync as
|
|
10937
|
-
import { join as
|
|
11765
|
+
import { existsSync as existsSync8 } from "fs";
|
|
11766
|
+
import { join as join9 } from "path";
|
|
10938
11767
|
function getTodosGlobalDir() {
|
|
10939
11768
|
const home = process.env["HOME"] || HOME;
|
|
10940
|
-
const newDir =
|
|
10941
|
-
const legacyDir =
|
|
10942
|
-
if (!
|
|
11769
|
+
const newDir = join9(home, ".hasna", "todos");
|
|
11770
|
+
const legacyDir = join9(home, ".todos");
|
|
11771
|
+
if (!existsSync8(newDir) && existsSync8(legacyDir))
|
|
10943
11772
|
return legacyDir;
|
|
10944
11773
|
return newDir;
|
|
10945
11774
|
}
|
|
10946
|
-
function
|
|
10947
|
-
return
|
|
11775
|
+
function getConfigPath2() {
|
|
11776
|
+
return join9(getTodosGlobalDir(), "config.json");
|
|
10948
11777
|
}
|
|
10949
11778
|
function normalizeAgent(agent) {
|
|
10950
11779
|
return agent.trim().toLowerCase();
|
|
@@ -10952,11 +11781,11 @@ function normalizeAgent(agent) {
|
|
|
10952
11781
|
function loadConfig() {
|
|
10953
11782
|
if (cached)
|
|
10954
11783
|
return cached;
|
|
10955
|
-
if (!
|
|
11784
|
+
if (!existsSync8(getConfigPath2())) {
|
|
10956
11785
|
cached = {};
|
|
10957
11786
|
return cached;
|
|
10958
11787
|
}
|
|
10959
|
-
const config = readJsonFile(
|
|
11788
|
+
const config = readJsonFile(getConfigPath2()) || {};
|
|
10960
11789
|
if (typeof config.sync_agents === "string") {
|
|
10961
11790
|
config.sync_agents = config.sync_agents.split(",").map((a) => a.trim()).filter(Boolean);
|
|
10962
11791
|
}
|
|
@@ -11268,6 +12097,7 @@ var init_webhooks = __esm(() => {
|
|
|
11268
12097
|
// src/db/templates.ts
|
|
11269
12098
|
var exports_templates = {};
|
|
11270
12099
|
__export(exports_templates, {
|
|
12100
|
+
updateTemplate: () => updateTemplate,
|
|
11271
12101
|
taskFromTemplate: () => taskFromTemplate,
|
|
11272
12102
|
listTemplates: () => listTemplates,
|
|
11273
12103
|
getTemplate: () => getTemplate,
|
|
@@ -11282,6 +12112,9 @@ function rowToTemplate(row) {
|
|
|
11282
12112
|
priority: row.priority || "medium"
|
|
11283
12113
|
};
|
|
11284
12114
|
}
|
|
12115
|
+
function resolveTemplateId(id, d) {
|
|
12116
|
+
return resolvePartialId(d, "task_templates", id);
|
|
12117
|
+
}
|
|
11285
12118
|
function createTemplate(input, db) {
|
|
11286
12119
|
const d = db || getDatabase();
|
|
11287
12120
|
const id = uuid();
|
|
@@ -11302,7 +12135,10 @@ function createTemplate(input, db) {
|
|
|
11302
12135
|
}
|
|
11303
12136
|
function getTemplate(id, db) {
|
|
11304
12137
|
const d = db || getDatabase();
|
|
11305
|
-
const
|
|
12138
|
+
const resolved = resolveTemplateId(id, d);
|
|
12139
|
+
if (!resolved)
|
|
12140
|
+
return null;
|
|
12141
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
11306
12142
|
return row ? rowToTemplate(row) : null;
|
|
11307
12143
|
}
|
|
11308
12144
|
function listTemplates(db) {
|
|
@@ -11311,15 +12147,63 @@ function listTemplates(db) {
|
|
|
11311
12147
|
}
|
|
11312
12148
|
function deleteTemplate(id, db) {
|
|
11313
12149
|
const d = db || getDatabase();
|
|
11314
|
-
|
|
12150
|
+
const resolved = resolveTemplateId(id, d);
|
|
12151
|
+
if (!resolved)
|
|
12152
|
+
return false;
|
|
12153
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
11315
12154
|
}
|
|
11316
|
-
function
|
|
11317
|
-
const
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
|
|
12155
|
+
function updateTemplate(id, updates, db) {
|
|
12156
|
+
const d = db || getDatabase();
|
|
12157
|
+
const resolved = resolveTemplateId(id, d);
|
|
12158
|
+
if (!resolved)
|
|
12159
|
+
return null;
|
|
12160
|
+
const sets = [];
|
|
12161
|
+
const values = [];
|
|
12162
|
+
if (updates.name !== undefined) {
|
|
12163
|
+
sets.push("name = ?");
|
|
12164
|
+
values.push(updates.name);
|
|
12165
|
+
}
|
|
12166
|
+
if (updates.title_pattern !== undefined) {
|
|
12167
|
+
sets.push("title_pattern = ?");
|
|
12168
|
+
values.push(updates.title_pattern);
|
|
12169
|
+
}
|
|
12170
|
+
if (updates.description !== undefined) {
|
|
12171
|
+
sets.push("description = ?");
|
|
12172
|
+
values.push(updates.description);
|
|
12173
|
+
}
|
|
12174
|
+
if (updates.priority !== undefined) {
|
|
12175
|
+
sets.push("priority = ?");
|
|
12176
|
+
values.push(updates.priority);
|
|
12177
|
+
}
|
|
12178
|
+
if (updates.tags !== undefined) {
|
|
12179
|
+
sets.push("tags = ?");
|
|
12180
|
+
values.push(JSON.stringify(updates.tags));
|
|
12181
|
+
}
|
|
12182
|
+
if (updates.project_id !== undefined) {
|
|
12183
|
+
sets.push("project_id = ?");
|
|
12184
|
+
values.push(updates.project_id);
|
|
12185
|
+
}
|
|
12186
|
+
if (updates.plan_id !== undefined) {
|
|
12187
|
+
sets.push("plan_id = ?");
|
|
12188
|
+
values.push(updates.plan_id);
|
|
12189
|
+
}
|
|
12190
|
+
if (updates.metadata !== undefined) {
|
|
12191
|
+
sets.push("metadata = ?");
|
|
12192
|
+
values.push(JSON.stringify(updates.metadata));
|
|
12193
|
+
}
|
|
12194
|
+
if (sets.length === 0)
|
|
12195
|
+
return getTemplate(resolved, d);
|
|
12196
|
+
values.push(resolved);
|
|
12197
|
+
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
12198
|
+
return getTemplate(resolved, d);
|
|
12199
|
+
}
|
|
12200
|
+
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
12201
|
+
const t = getTemplate(templateId, db);
|
|
12202
|
+
if (!t)
|
|
12203
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
12204
|
+
return {
|
|
12205
|
+
title: overrides.title || t.title_pattern,
|
|
12206
|
+
description: overrides.description ?? t.description ?? undefined,
|
|
11323
12207
|
priority: overrides.priority ?? t.priority,
|
|
11324
12208
|
tags: overrides.tags ?? t.tags,
|
|
11325
12209
|
project_id: overrides.project_id ?? t.project_id ?? undefined,
|
|
@@ -14358,210 +15242,795 @@ var init_agent_metrics = __esm(() => {
|
|
|
14358
15242
|
init_database();
|
|
14359
15243
|
});
|
|
14360
15244
|
|
|
14361
|
-
// src/lib/extract.ts
|
|
14362
|
-
var exports_extract = {};
|
|
14363
|
-
__export(exports_extract, {
|
|
14364
|
-
tagToPriority: () => tagToPriority,
|
|
14365
|
-
extractTodos: () => extractTodos,
|
|
14366
|
-
extractFromSource: () => extractFromSource,
|
|
14367
|
-
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
15245
|
+
// src/lib/extract.ts
|
|
15246
|
+
var exports_extract = {};
|
|
15247
|
+
__export(exports_extract, {
|
|
15248
|
+
tagToPriority: () => tagToPriority,
|
|
15249
|
+
extractTodos: () => extractTodos,
|
|
15250
|
+
extractFromSource: () => extractFromSource,
|
|
15251
|
+
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
15252
|
+
});
|
|
15253
|
+
import { readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
15254
|
+
import { relative as relative2, resolve as resolve2, join as join12 } from "path";
|
|
15255
|
+
function tagToPriority(tag) {
|
|
15256
|
+
switch (tag) {
|
|
15257
|
+
case "BUG":
|
|
15258
|
+
case "FIXME":
|
|
15259
|
+
return "high";
|
|
15260
|
+
case "HACK":
|
|
15261
|
+
case "XXX":
|
|
15262
|
+
return "medium";
|
|
15263
|
+
case "TODO":
|
|
15264
|
+
return "medium";
|
|
15265
|
+
case "NOTE":
|
|
15266
|
+
return "low";
|
|
15267
|
+
}
|
|
15268
|
+
}
|
|
15269
|
+
function buildTagRegex(tags) {
|
|
15270
|
+
const tagPattern = tags.join("|");
|
|
15271
|
+
return new RegExp(`(?:^|\\s)(?:\\/\\/|\\/\\*|#|\\*|--|;;|%|<!--|\\{-)\\s*(?:@?)(${tagPattern})\\s*[:(]?\\s*(.*)$`, "i");
|
|
15272
|
+
}
|
|
15273
|
+
function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
|
|
15274
|
+
const regex = buildTagRegex(tags);
|
|
15275
|
+
const results = [];
|
|
15276
|
+
const lines = source.split(`
|
|
15277
|
+
`);
|
|
15278
|
+
for (let i = 0;i < lines.length; i++) {
|
|
15279
|
+
const line = lines[i];
|
|
15280
|
+
const match = line.match(regex);
|
|
15281
|
+
if (match) {
|
|
15282
|
+
const tag = match[1].toUpperCase();
|
|
15283
|
+
let message = match[2].trim();
|
|
15284
|
+
message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
|
|
15285
|
+
if (message) {
|
|
15286
|
+
results.push({
|
|
15287
|
+
tag,
|
|
15288
|
+
message,
|
|
15289
|
+
file: filePath,
|
|
15290
|
+
line: i + 1,
|
|
15291
|
+
raw: line
|
|
15292
|
+
});
|
|
15293
|
+
}
|
|
15294
|
+
}
|
|
15295
|
+
}
|
|
15296
|
+
return results;
|
|
15297
|
+
}
|
|
15298
|
+
function collectFiles(basePath, extensions) {
|
|
15299
|
+
const stat = statSync2(basePath);
|
|
15300
|
+
if (stat.isFile()) {
|
|
15301
|
+
return [basePath];
|
|
15302
|
+
}
|
|
15303
|
+
const glob = new Bun.Glob("**/*");
|
|
15304
|
+
const files = [];
|
|
15305
|
+
for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
|
|
15306
|
+
const parts = entry.split("/");
|
|
15307
|
+
if (parts.some((p) => SKIP_DIRS.has(p)))
|
|
15308
|
+
continue;
|
|
15309
|
+
const dotIdx = entry.lastIndexOf(".");
|
|
15310
|
+
if (dotIdx === -1)
|
|
15311
|
+
continue;
|
|
15312
|
+
const ext = entry.slice(dotIdx);
|
|
15313
|
+
if (!extensions.has(ext))
|
|
15314
|
+
continue;
|
|
15315
|
+
files.push(entry);
|
|
15316
|
+
}
|
|
15317
|
+
return files.sort();
|
|
15318
|
+
}
|
|
15319
|
+
function extractTodos(options, db) {
|
|
15320
|
+
const basePath = resolve2(options.path);
|
|
15321
|
+
const tags = options.patterns || [...EXTRACT_TAGS];
|
|
15322
|
+
const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
|
|
15323
|
+
const files = collectFiles(basePath, extensions);
|
|
15324
|
+
const allComments = [];
|
|
15325
|
+
for (const file of files) {
|
|
15326
|
+
const fullPath = statSync2(basePath).isFile() ? basePath : join12(basePath, file);
|
|
15327
|
+
try {
|
|
15328
|
+
const source = readFileSync6(fullPath, "utf-8");
|
|
15329
|
+
const relPath = statSync2(basePath).isFile() ? relative2(resolve2(basePath, ".."), fullPath) : file;
|
|
15330
|
+
const comments = extractFromSource(source, relPath, tags);
|
|
15331
|
+
allComments.push(...comments);
|
|
15332
|
+
} catch {}
|
|
15333
|
+
}
|
|
15334
|
+
if (options.dry_run) {
|
|
15335
|
+
return { comments: allComments, tasks: [], skipped: 0 };
|
|
15336
|
+
}
|
|
15337
|
+
const tasks = [];
|
|
15338
|
+
let skipped = 0;
|
|
15339
|
+
const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
|
|
15340
|
+
const existingKeys = new Set;
|
|
15341
|
+
for (const t of existingTasks) {
|
|
15342
|
+
const meta = t.metadata;
|
|
15343
|
+
if (meta?.["source_file"] && meta?.["source_line"]) {
|
|
15344
|
+
existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
|
|
15345
|
+
}
|
|
15346
|
+
}
|
|
15347
|
+
for (const comment of allComments) {
|
|
15348
|
+
const dedupKey = `${comment.file}:${comment.line}`;
|
|
15349
|
+
if (existingKeys.has(dedupKey)) {
|
|
15350
|
+
skipped++;
|
|
15351
|
+
continue;
|
|
15352
|
+
}
|
|
15353
|
+
const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
|
|
15354
|
+
const task = createTask({
|
|
15355
|
+
title: `[${comment.tag}] ${comment.message}`,
|
|
15356
|
+
description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
|
|
15357
|
+
\`\`\`
|
|
15358
|
+
${comment.raw.trim()}
|
|
15359
|
+
\`\`\``,
|
|
15360
|
+
priority: tagToPriority(comment.tag),
|
|
15361
|
+
project_id: options.project_id,
|
|
15362
|
+
task_list_id: options.task_list_id,
|
|
15363
|
+
assigned_to: options.assigned_to,
|
|
15364
|
+
agent_id: options.agent_id,
|
|
15365
|
+
tags: taskTags,
|
|
15366
|
+
metadata: {
|
|
15367
|
+
source: "code_comment",
|
|
15368
|
+
comment_type: comment.tag,
|
|
15369
|
+
source_file: comment.file,
|
|
15370
|
+
source_line: comment.line
|
|
15371
|
+
}
|
|
15372
|
+
}, db);
|
|
15373
|
+
addTaskFile({
|
|
15374
|
+
task_id: task.id,
|
|
15375
|
+
path: comment.file,
|
|
15376
|
+
note: `Line ${comment.line}: ${comment.tag} comment`
|
|
15377
|
+
}, db);
|
|
15378
|
+
tasks.push(task);
|
|
15379
|
+
existingKeys.add(dedupKey);
|
|
15380
|
+
}
|
|
15381
|
+
return { comments: allComments, tasks, skipped };
|
|
15382
|
+
}
|
|
15383
|
+
var EXTRACT_TAGS, DEFAULT_EXTENSIONS, SKIP_DIRS;
|
|
15384
|
+
var init_extract = __esm(() => {
|
|
15385
|
+
init_tasks();
|
|
15386
|
+
init_task_files();
|
|
15387
|
+
EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
15388
|
+
DEFAULT_EXTENSIONS = new Set([
|
|
15389
|
+
".ts",
|
|
15390
|
+
".tsx",
|
|
15391
|
+
".js",
|
|
15392
|
+
".jsx",
|
|
15393
|
+
".mjs",
|
|
15394
|
+
".cjs",
|
|
15395
|
+
".py",
|
|
15396
|
+
".rb",
|
|
15397
|
+
".go",
|
|
15398
|
+
".rs",
|
|
15399
|
+
".c",
|
|
15400
|
+
".cpp",
|
|
15401
|
+
".h",
|
|
15402
|
+
".hpp",
|
|
15403
|
+
".java",
|
|
15404
|
+
".kt",
|
|
15405
|
+
".swift",
|
|
15406
|
+
".cs",
|
|
15407
|
+
".php",
|
|
15408
|
+
".sh",
|
|
15409
|
+
".bash",
|
|
15410
|
+
".zsh",
|
|
15411
|
+
".lua",
|
|
15412
|
+
".sql",
|
|
15413
|
+
".r",
|
|
15414
|
+
".R",
|
|
15415
|
+
".yaml",
|
|
15416
|
+
".yml",
|
|
15417
|
+
".toml",
|
|
15418
|
+
".css",
|
|
15419
|
+
".scss",
|
|
15420
|
+
".less",
|
|
15421
|
+
".vue",
|
|
15422
|
+
".svelte",
|
|
15423
|
+
".ex",
|
|
15424
|
+
".exs",
|
|
15425
|
+
".erl",
|
|
15426
|
+
".hs",
|
|
15427
|
+
".ml",
|
|
15428
|
+
".mli",
|
|
15429
|
+
".clj",
|
|
15430
|
+
".cljs"
|
|
15431
|
+
]);
|
|
15432
|
+
SKIP_DIRS = new Set([
|
|
15433
|
+
"node_modules",
|
|
15434
|
+
".git",
|
|
15435
|
+
"dist",
|
|
15436
|
+
"build",
|
|
15437
|
+
"out",
|
|
15438
|
+
".next",
|
|
15439
|
+
".turbo",
|
|
15440
|
+
"coverage",
|
|
15441
|
+
"__pycache__",
|
|
15442
|
+
".venv",
|
|
15443
|
+
"venv",
|
|
15444
|
+
"vendor",
|
|
15445
|
+
"target",
|
|
15446
|
+
".cache",
|
|
15447
|
+
".parcel-cache"
|
|
15448
|
+
]);
|
|
15449
|
+
});
|
|
15450
|
+
|
|
15451
|
+
// src/db/pg-migrations.ts
|
|
15452
|
+
var PG_MIGRATIONS;
|
|
15453
|
+
var init_pg_migrations = __esm(() => {
|
|
15454
|
+
PG_MIGRATIONS = [
|
|
15455
|
+
`
|
|
15456
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
15457
|
+
id TEXT PRIMARY KEY,
|
|
15458
|
+
name TEXT NOT NULL,
|
|
15459
|
+
path TEXT UNIQUE NOT NULL,
|
|
15460
|
+
description TEXT,
|
|
15461
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15462
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15463
|
+
);
|
|
15464
|
+
|
|
15465
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
15466
|
+
id TEXT PRIMARY KEY,
|
|
15467
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
15468
|
+
parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15469
|
+
title TEXT NOT NULL,
|
|
15470
|
+
description TEXT,
|
|
15471
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
|
15472
|
+
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
|
|
15473
|
+
agent_id TEXT,
|
|
15474
|
+
assigned_to TEXT,
|
|
15475
|
+
session_id TEXT,
|
|
15476
|
+
working_dir TEXT,
|
|
15477
|
+
tags TEXT DEFAULT '[]',
|
|
15478
|
+
metadata TEXT DEFAULT '{}',
|
|
15479
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
15480
|
+
locked_by TEXT,
|
|
15481
|
+
locked_at TEXT,
|
|
15482
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15483
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15484
|
+
completed_at TEXT
|
|
15485
|
+
);
|
|
15486
|
+
|
|
15487
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
15488
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15489
|
+
depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15490
|
+
PRIMARY KEY (task_id, depends_on),
|
|
15491
|
+
CHECK (task_id != depends_on)
|
|
15492
|
+
);
|
|
15493
|
+
|
|
15494
|
+
CREATE TABLE IF NOT EXISTS task_comments (
|
|
15495
|
+
id TEXT PRIMARY KEY,
|
|
15496
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15497
|
+
agent_id TEXT,
|
|
15498
|
+
session_id TEXT,
|
|
15499
|
+
content TEXT NOT NULL,
|
|
15500
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15501
|
+
);
|
|
15502
|
+
|
|
15503
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
15504
|
+
id TEXT PRIMARY KEY,
|
|
15505
|
+
agent_id TEXT,
|
|
15506
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
15507
|
+
working_dir TEXT,
|
|
15508
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15509
|
+
last_activity TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15510
|
+
metadata TEXT DEFAULT '{}'
|
|
15511
|
+
);
|
|
15512
|
+
|
|
15513
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
|
|
15514
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
|
|
15515
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
15516
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
|
|
15517
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
|
|
15518
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(agent_id);
|
|
15519
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
15520
|
+
CREATE INDEX IF NOT EXISTS idx_comments_task ON task_comments(task_id);
|
|
15521
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
|
15522
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
15523
|
+
|
|
15524
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
15525
|
+
id INTEGER PRIMARY KEY,
|
|
15526
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15527
|
+
);
|
|
15528
|
+
|
|
15529
|
+
INSERT INTO _migrations (id) VALUES (1) ON CONFLICT DO NOTHING;
|
|
15530
|
+
`,
|
|
15531
|
+
`
|
|
15532
|
+
ALTER TABLE projects ADD COLUMN IF NOT EXISTS task_list_id TEXT;
|
|
15533
|
+
INSERT INTO _migrations (id) VALUES (2) ON CONFLICT DO NOTHING;
|
|
15534
|
+
`,
|
|
15535
|
+
`
|
|
15536
|
+
CREATE TABLE IF NOT EXISTS task_tags (
|
|
15537
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15538
|
+
tag TEXT NOT NULL,
|
|
15539
|
+
PRIMARY KEY (task_id, tag)
|
|
15540
|
+
);
|
|
15541
|
+
CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag);
|
|
15542
|
+
CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id);
|
|
15543
|
+
|
|
15544
|
+
INSERT INTO _migrations (id) VALUES (3) ON CONFLICT DO NOTHING;
|
|
15545
|
+
`,
|
|
15546
|
+
`
|
|
15547
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
15548
|
+
id TEXT PRIMARY KEY,
|
|
15549
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
15550
|
+
name TEXT NOT NULL,
|
|
15551
|
+
description TEXT,
|
|
15552
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
15553
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15554
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15555
|
+
);
|
|
15556
|
+
CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id);
|
|
15557
|
+
CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status);
|
|
15558
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL;
|
|
15559
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id);
|
|
15560
|
+
INSERT INTO _migrations (id) VALUES (4) ON CONFLICT DO NOTHING;
|
|
15561
|
+
`,
|
|
15562
|
+
`
|
|
15563
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
15564
|
+
id TEXT PRIMARY KEY,
|
|
15565
|
+
name TEXT NOT NULL UNIQUE,
|
|
15566
|
+
description TEXT,
|
|
15567
|
+
metadata TEXT DEFAULT '{}',
|
|
15568
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15569
|
+
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15570
|
+
);
|
|
15571
|
+
CREATE INDEX IF NOT EXISTS idx_agents_name ON agents(name);
|
|
15572
|
+
|
|
15573
|
+
CREATE TABLE IF NOT EXISTS task_lists (
|
|
15574
|
+
id TEXT PRIMARY KEY,
|
|
15575
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
15576
|
+
slug TEXT NOT NULL,
|
|
15577
|
+
name TEXT NOT NULL,
|
|
15578
|
+
description TEXT,
|
|
15579
|
+
metadata TEXT DEFAULT '{}',
|
|
15580
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15581
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15582
|
+
UNIQUE(project_id, slug)
|
|
15583
|
+
);
|
|
15584
|
+
CREATE INDEX IF NOT EXISTS idx_task_lists_project ON task_lists(project_id);
|
|
15585
|
+
CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug);
|
|
15586
|
+
|
|
15587
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
|
|
15588
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id);
|
|
15589
|
+
|
|
15590
|
+
INSERT INTO _migrations (id) VALUES (5) ON CONFLICT DO NOTHING;
|
|
15591
|
+
`,
|
|
15592
|
+
`
|
|
15593
|
+
ALTER TABLE projects ADD COLUMN IF NOT EXISTS task_prefix TEXT;
|
|
15594
|
+
ALTER TABLE projects ADD COLUMN IF NOT EXISTS task_counter INTEGER NOT NULL DEFAULT 0;
|
|
15595
|
+
|
|
15596
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS short_id TEXT;
|
|
15597
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL;
|
|
15598
|
+
|
|
15599
|
+
INSERT INTO _migrations (id) VALUES (6) ON CONFLICT DO NOTHING;
|
|
15600
|
+
`,
|
|
15601
|
+
`
|
|
15602
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS due_at TEXT;
|
|
15603
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at);
|
|
15604
|
+
INSERT INTO _migrations (id) VALUES (7) ON CONFLICT DO NOTHING;
|
|
15605
|
+
`,
|
|
15606
|
+
`
|
|
15607
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'agent';
|
|
15608
|
+
INSERT INTO _migrations (id) VALUES (8) ON CONFLICT DO NOTHING;
|
|
15609
|
+
`,
|
|
15610
|
+
`
|
|
15611
|
+
ALTER TABLE plans ADD COLUMN IF NOT EXISTS task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL;
|
|
15612
|
+
ALTER TABLE plans ADD COLUMN IF NOT EXISTS agent_id TEXT;
|
|
15613
|
+
CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id);
|
|
15614
|
+
CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id);
|
|
15615
|
+
INSERT INTO _migrations (id) VALUES (9) ON CONFLICT DO NOTHING;
|
|
15616
|
+
`,
|
|
15617
|
+
`
|
|
15618
|
+
CREATE TABLE IF NOT EXISTS task_history (
|
|
15619
|
+
id TEXT PRIMARY KEY,
|
|
15620
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15621
|
+
action TEXT NOT NULL,
|
|
15622
|
+
field TEXT,
|
|
15623
|
+
old_value TEXT,
|
|
15624
|
+
new_value TEXT,
|
|
15625
|
+
agent_id TEXT,
|
|
15626
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15627
|
+
);
|
|
15628
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id);
|
|
15629
|
+
CREATE INDEX IF NOT EXISTS idx_task_history_agent ON task_history(agent_id);
|
|
15630
|
+
|
|
15631
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
15632
|
+
id TEXT PRIMARY KEY,
|
|
15633
|
+
url TEXT NOT NULL,
|
|
15634
|
+
events TEXT NOT NULL DEFAULT '[]',
|
|
15635
|
+
secret TEXT,
|
|
15636
|
+
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
15637
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15638
|
+
);
|
|
15639
|
+
|
|
15640
|
+
CREATE TABLE IF NOT EXISTS task_templates (
|
|
15641
|
+
id TEXT PRIMARY KEY,
|
|
15642
|
+
name TEXT NOT NULL,
|
|
15643
|
+
title_pattern TEXT NOT NULL,
|
|
15644
|
+
description TEXT,
|
|
15645
|
+
priority TEXT DEFAULT 'medium',
|
|
15646
|
+
tags TEXT DEFAULT '[]',
|
|
15647
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
15648
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
15649
|
+
metadata TEXT DEFAULT '{}',
|
|
15650
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15651
|
+
);
|
|
15652
|
+
|
|
15653
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS estimated_minutes INTEGER;
|
|
15654
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS requires_approval BOOLEAN NOT NULL DEFAULT FALSE;
|
|
15655
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS approved_by TEXT;
|
|
15656
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS approved_at TEXT;
|
|
15657
|
+
|
|
15658
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS permissions TEXT DEFAULT '["*"]';
|
|
15659
|
+
|
|
15660
|
+
INSERT INTO _migrations (id) VALUES (10) ON CONFLICT DO NOTHING;
|
|
15661
|
+
`,
|
|
15662
|
+
`
|
|
15663
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS reports_to TEXT;
|
|
15664
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS title TEXT;
|
|
15665
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS level TEXT;
|
|
15666
|
+
INSERT INTO _migrations (id) VALUES (11) ON CONFLICT DO NOTHING;
|
|
15667
|
+
`,
|
|
15668
|
+
`
|
|
15669
|
+
CREATE TABLE IF NOT EXISTS orgs (
|
|
15670
|
+
id TEXT PRIMARY KEY,
|
|
15671
|
+
name TEXT NOT NULL UNIQUE,
|
|
15672
|
+
description TEXT,
|
|
15673
|
+
metadata TEXT DEFAULT '{}',
|
|
15674
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15675
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15676
|
+
);
|
|
15677
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
|
|
15678
|
+
ALTER TABLE projects ADD COLUMN IF NOT EXISTS org_id TEXT REFERENCES orgs(id) ON DELETE SET NULL;
|
|
15679
|
+
INSERT INTO _migrations (id) VALUES (12) ON CONFLICT DO NOTHING;
|
|
15680
|
+
`,
|
|
15681
|
+
`
|
|
15682
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS recurrence_rule TEXT;
|
|
15683
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS recurrence_parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
|
|
15684
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_parent ON tasks(recurrence_parent_id);
|
|
15685
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_recurrence_rule ON tasks(recurrence_rule) WHERE recurrence_rule IS NOT NULL;
|
|
15686
|
+
INSERT INTO _migrations (id) VALUES (13) ON CONFLICT DO NOTHING;
|
|
15687
|
+
`,
|
|
15688
|
+
`
|
|
15689
|
+
ALTER TABLE task_comments ADD COLUMN IF NOT EXISTS type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note'));
|
|
15690
|
+
ALTER TABLE task_comments ADD COLUMN IF NOT EXISTS progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100));
|
|
15691
|
+
INSERT INTO _migrations (id) VALUES (14) ON CONFLICT DO NOTHING;
|
|
15692
|
+
`,
|
|
15693
|
+
`
|
|
15694
|
+
-- PostgreSQL uses tsvector/tsquery instead of FTS5
|
|
15695
|
+
-- Full-text search can be done with to_tsvector/to_tsquery on tasks table directly
|
|
15696
|
+
-- No virtual table needed; add a generated tsvector column instead
|
|
15697
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS search_vector tsvector;
|
|
15698
|
+
|
|
15699
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_search ON tasks USING GIN(search_vector);
|
|
15700
|
+
|
|
15701
|
+
-- Function to update search vector
|
|
15702
|
+
CREATE OR REPLACE FUNCTION tasks_search_vector_update() RETURNS trigger AS $$
|
|
15703
|
+
BEGIN
|
|
15704
|
+
NEW.search_vector :=
|
|
15705
|
+
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
|
|
15706
|
+
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') ||
|
|
15707
|
+
setweight(to_tsvector('english', COALESCE(NEW.tags, '')), 'C');
|
|
15708
|
+
RETURN NEW;
|
|
15709
|
+
END;
|
|
15710
|
+
$$ LANGUAGE plpgsql;
|
|
15711
|
+
|
|
15712
|
+
DROP TRIGGER IF EXISTS tasks_search_vector_trigger ON tasks;
|
|
15713
|
+
CREATE TRIGGER tasks_search_vector_trigger
|
|
15714
|
+
BEFORE INSERT OR UPDATE OF title, description, tags ON tasks
|
|
15715
|
+
FOR EACH ROW EXECUTE FUNCTION tasks_search_vector_update();
|
|
15716
|
+
|
|
15717
|
+
-- Backfill existing rows
|
|
15718
|
+
UPDATE tasks SET search_vector =
|
|
15719
|
+
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
|
|
15720
|
+
setweight(to_tsvector('english', COALESCE(description, '')), 'B') ||
|
|
15721
|
+
setweight(to_tsvector('english', COALESCE(tags, '')), 'C')
|
|
15722
|
+
WHERE search_vector IS NULL;
|
|
15723
|
+
|
|
15724
|
+
INSERT INTO _migrations (id) VALUES (15) ON CONFLICT DO NOTHING;
|
|
15725
|
+
`,
|
|
15726
|
+
`
|
|
15727
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS spawns_template_id TEXT REFERENCES task_templates(id) ON DELETE SET NULL;
|
|
15728
|
+
INSERT INTO _migrations (id) VALUES (16) ON CONFLICT DO NOTHING;
|
|
15729
|
+
`,
|
|
15730
|
+
`
|
|
15731
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS session_id TEXT;
|
|
15732
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS working_dir TEXT;
|
|
15733
|
+
INSERT INTO _migrations (id) VALUES (17) ON CONFLICT DO NOTHING;
|
|
15734
|
+
`,
|
|
15735
|
+
`
|
|
15736
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS confidence DOUBLE PRECISION;
|
|
15737
|
+
INSERT INTO _migrations (id) VALUES (18) ON CONFLICT DO NOTHING;
|
|
15738
|
+
`,
|
|
15739
|
+
`
|
|
15740
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS reason TEXT;
|
|
15741
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS spawned_from_session TEXT;
|
|
15742
|
+
INSERT INTO _migrations (id) VALUES (19) ON CONFLICT DO NOTHING;
|
|
15743
|
+
`,
|
|
15744
|
+
`
|
|
15745
|
+
CREATE TABLE IF NOT EXISTS handoffs (
|
|
15746
|
+
id TEXT PRIMARY KEY,
|
|
15747
|
+
agent_id TEXT,
|
|
15748
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
15749
|
+
summary TEXT NOT NULL,
|
|
15750
|
+
completed TEXT,
|
|
15751
|
+
in_progress TEXT,
|
|
15752
|
+
blockers TEXT,
|
|
15753
|
+
next_steps TEXT,
|
|
15754
|
+
created_at TIMESTAMPTZ NOT NULL
|
|
15755
|
+
);
|
|
15756
|
+
INSERT INTO _migrations (id) VALUES (20) ON CONFLICT DO NOTHING;
|
|
15757
|
+
`,
|
|
15758
|
+
`
|
|
15759
|
+
CREATE TABLE IF NOT EXISTS task_checklists (
|
|
15760
|
+
id TEXT PRIMARY KEY,
|
|
15761
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15762
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
15763
|
+
text TEXT NOT NULL,
|
|
15764
|
+
checked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
15765
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15766
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15767
|
+
);
|
|
15768
|
+
CREATE INDEX IF NOT EXISTS idx_task_checklists_task ON task_checklists(task_id);
|
|
15769
|
+
INSERT INTO _migrations (id) VALUES (21) ON CONFLICT DO NOTHING;
|
|
15770
|
+
`,
|
|
15771
|
+
`
|
|
15772
|
+
CREATE TABLE IF NOT EXISTS project_sources (
|
|
15773
|
+
id TEXT PRIMARY KEY,
|
|
15774
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
15775
|
+
type TEXT NOT NULL,
|
|
15776
|
+
name TEXT NOT NULL,
|
|
15777
|
+
uri TEXT NOT NULL,
|
|
15778
|
+
description TEXT,
|
|
15779
|
+
metadata TEXT DEFAULT '{}',
|
|
15780
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15781
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15782
|
+
);
|
|
15783
|
+
CREATE INDEX IF NOT EXISTS idx_project_sources_project ON project_sources(project_id);
|
|
15784
|
+
CREATE INDEX IF NOT EXISTS idx_project_sources_type ON project_sources(type);
|
|
15785
|
+
INSERT INTO _migrations (id) VALUES (22) ON CONFLICT DO NOTHING;
|
|
15786
|
+
`,
|
|
15787
|
+
`
|
|
15788
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT;
|
|
15789
|
+
INSERT INTO _migrations (id) VALUES (23) ON CONFLICT DO NOTHING;
|
|
15790
|
+
`,
|
|
15791
|
+
`
|
|
15792
|
+
CREATE TABLE IF NOT EXISTS resource_locks (
|
|
15793
|
+
resource_type TEXT NOT NULL,
|
|
15794
|
+
resource_id TEXT NOT NULL,
|
|
15795
|
+
agent_id TEXT NOT NULL,
|
|
15796
|
+
lock_type TEXT NOT NULL DEFAULT 'advisory',
|
|
15797
|
+
locked_at TIMESTAMPTZ NOT NULL,
|
|
15798
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
15799
|
+
UNIQUE(resource_type, resource_id, lock_type)
|
|
15800
|
+
);
|
|
15801
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_type_id ON resource_locks(resource_type, resource_id);
|
|
15802
|
+
CREATE INDEX IF NOT EXISTS idx_resource_locks_agent ON resource_locks(agent_id);
|
|
15803
|
+
INSERT INTO _migrations (id) VALUES (24) ON CONFLICT DO NOTHING;
|
|
15804
|
+
`,
|
|
15805
|
+
`
|
|
15806
|
+
CREATE TABLE IF NOT EXISTS task_files (
|
|
15807
|
+
id TEXT PRIMARY KEY,
|
|
15808
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15809
|
+
path TEXT NOT NULL,
|
|
15810
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
15811
|
+
agent_id TEXT,
|
|
15812
|
+
note TEXT,
|
|
15813
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15814
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15815
|
+
);
|
|
15816
|
+
CREATE INDEX IF NOT EXISTS idx_task_files_task ON task_files(task_id);
|
|
15817
|
+
CREATE INDEX IF NOT EXISTS idx_task_files_path ON task_files(path);
|
|
15818
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_files_task_path ON task_files(task_id, path);
|
|
15819
|
+
INSERT INTO _migrations (id) VALUES (25) ON CONFLICT DO NOTHING;
|
|
15820
|
+
`,
|
|
15821
|
+
`
|
|
15822
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS assigned_by TEXT;
|
|
15823
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS assigned_from_project TEXT;
|
|
15824
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_by ON tasks(assigned_by);
|
|
15825
|
+
INSERT INTO _migrations (id) VALUES (26) ON CONFLICT DO NOTHING;
|
|
15826
|
+
`,
|
|
15827
|
+
`
|
|
15828
|
+
CREATE TABLE IF NOT EXISTS task_relationships (
|
|
15829
|
+
id TEXT PRIMARY KEY,
|
|
15830
|
+
source_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15831
|
+
target_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15832
|
+
relationship_type TEXT NOT NULL CHECK(relationship_type IN ('related_to', 'conflicts_with', 'similar_to', 'duplicates', 'supersedes', 'modifies_same_file')),
|
|
15833
|
+
metadata TEXT DEFAULT '{}',
|
|
15834
|
+
created_by TEXT,
|
|
15835
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15836
|
+
CHECK (source_task_id != target_task_id)
|
|
15837
|
+
);
|
|
15838
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_source ON task_relationships(source_task_id);
|
|
15839
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_target ON task_relationships(target_task_id);
|
|
15840
|
+
CREATE INDEX IF NOT EXISTS idx_task_rel_type ON task_relationships(relationship_type);
|
|
15841
|
+
INSERT INTO _migrations (id) VALUES (27) ON CONFLICT DO NOTHING;
|
|
15842
|
+
`,
|
|
15843
|
+
`
|
|
15844
|
+
CREATE TABLE IF NOT EXISTS kg_edges (
|
|
15845
|
+
id TEXT PRIMARY KEY,
|
|
15846
|
+
source_id TEXT NOT NULL,
|
|
15847
|
+
source_type TEXT NOT NULL,
|
|
15848
|
+
target_id TEXT NOT NULL,
|
|
15849
|
+
target_type TEXT NOT NULL,
|
|
15850
|
+
relation_type TEXT NOT NULL,
|
|
15851
|
+
weight DOUBLE PRECISION NOT NULL DEFAULT 1.0,
|
|
15852
|
+
metadata TEXT DEFAULT '{}',
|
|
15853
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15854
|
+
UNIQUE(source_id, source_type, target_id, target_type, relation_type)
|
|
15855
|
+
);
|
|
15856
|
+
CREATE INDEX IF NOT EXISTS idx_kg_source ON kg_edges(source_id, source_type);
|
|
15857
|
+
CREATE INDEX IF NOT EXISTS idx_kg_target ON kg_edges(target_id, target_type);
|
|
15858
|
+
CREATE INDEX IF NOT EXISTS idx_kg_relation ON kg_edges(relation_type);
|
|
15859
|
+
INSERT INTO _migrations (id) VALUES (28) ON CONFLICT DO NOTHING;
|
|
15860
|
+
`,
|
|
15861
|
+
`
|
|
15862
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS capabilities TEXT DEFAULT '[]';
|
|
15863
|
+
INSERT INTO _migrations (id) VALUES (29) ON CONFLICT DO NOTHING;
|
|
15864
|
+
`,
|
|
15865
|
+
`
|
|
15866
|
+
ALTER TABLE agents ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
|
|
15867
|
+
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
15868
|
+
INSERT INTO _migrations (id) VALUES (30) ON CONFLICT DO NOTHING;
|
|
15869
|
+
`,
|
|
15870
|
+
`
|
|
15871
|
+
CREATE TABLE IF NOT EXISTS project_agent_roles (
|
|
15872
|
+
id TEXT PRIMARY KEY,
|
|
15873
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
15874
|
+
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
15875
|
+
role TEXT NOT NULL,
|
|
15876
|
+
is_lead BOOLEAN NOT NULL DEFAULT FALSE,
|
|
15877
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15878
|
+
UNIQUE(project_id, agent_id, role)
|
|
15879
|
+
);
|
|
15880
|
+
CREATE INDEX IF NOT EXISTS idx_project_agent_roles_project ON project_agent_roles(project_id);
|
|
15881
|
+
CREATE INDEX IF NOT EXISTS idx_project_agent_roles_agent ON project_agent_roles(agent_id);
|
|
15882
|
+
INSERT INTO _migrations (id) VALUES (31) ON CONFLICT DO NOTHING;
|
|
15883
|
+
`,
|
|
15884
|
+
`
|
|
15885
|
+
CREATE TABLE IF NOT EXISTS task_commits (
|
|
15886
|
+
id TEXT PRIMARY KEY,
|
|
15887
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15888
|
+
sha TEXT NOT NULL,
|
|
15889
|
+
message TEXT,
|
|
15890
|
+
author TEXT,
|
|
15891
|
+
files_changed TEXT,
|
|
15892
|
+
committed_at TEXT,
|
|
15893
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15894
|
+
UNIQUE(task_id, sha)
|
|
15895
|
+
);
|
|
15896
|
+
CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id);
|
|
15897
|
+
CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha);
|
|
15898
|
+
INSERT INTO _migrations (id) VALUES (32) ON CONFLICT DO NOTHING;
|
|
15899
|
+
`,
|
|
15900
|
+
`
|
|
15901
|
+
CREATE TABLE IF NOT EXISTS file_locks (
|
|
15902
|
+
id TEXT PRIMARY KEY,
|
|
15903
|
+
path TEXT NOT NULL UNIQUE,
|
|
15904
|
+
agent_id TEXT NOT NULL,
|
|
15905
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
15906
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
15907
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15908
|
+
);
|
|
15909
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
|
|
15910
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
15911
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
15912
|
+
INSERT INTO _migrations (id) VALUES (33) ON CONFLICT DO NOTHING;
|
|
15913
|
+
`,
|
|
15914
|
+
`
|
|
15915
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS started_at TEXT;
|
|
15916
|
+
INSERT INTO _migrations (id) VALUES (34) ON CONFLICT DO NOTHING;
|
|
15917
|
+
`,
|
|
15918
|
+
`
|
|
15919
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS task_type TEXT;
|
|
15920
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
|
|
15921
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS cost_tokens INTEGER DEFAULT 0;
|
|
15922
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS cost_usd DOUBLE PRECISION DEFAULT 0;
|
|
15923
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS delegated_from TEXT;
|
|
15924
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS delegation_depth INTEGER DEFAULT 0;
|
|
15925
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS retry_count INTEGER DEFAULT 0;
|
|
15926
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS max_retries INTEGER DEFAULT 3;
|
|
15927
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS retry_after TEXT;
|
|
15928
|
+
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS sla_minutes INTEGER;
|
|
15929
|
+
|
|
15930
|
+
CREATE TABLE IF NOT EXISTS task_traces (
|
|
15931
|
+
id TEXT PRIMARY KEY,
|
|
15932
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
15933
|
+
agent_id TEXT,
|
|
15934
|
+
trace_type TEXT NOT NULL CHECK(trace_type IN ('tool_call','llm_call','error','handoff','custom')),
|
|
15935
|
+
name TEXT,
|
|
15936
|
+
input_summary TEXT,
|
|
15937
|
+
output_summary TEXT,
|
|
15938
|
+
duration_ms INTEGER,
|
|
15939
|
+
tokens INTEGER,
|
|
15940
|
+
cost_usd DOUBLE PRECISION,
|
|
15941
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15942
|
+
);
|
|
15943
|
+
CREATE INDEX IF NOT EXISTS idx_task_traces_task ON task_traces(task_id);
|
|
15944
|
+
CREATE INDEX IF NOT EXISTS idx_task_traces_agent ON task_traces(agent_id);
|
|
15945
|
+
|
|
15946
|
+
CREATE TABLE IF NOT EXISTS context_snapshots (
|
|
15947
|
+
id TEXT PRIMARY KEY,
|
|
15948
|
+
agent_id TEXT,
|
|
15949
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
15950
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
15951
|
+
snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
|
|
15952
|
+
plan_summary TEXT,
|
|
15953
|
+
files_open TEXT DEFAULT '[]',
|
|
15954
|
+
attempts TEXT DEFAULT '[]',
|
|
15955
|
+
blockers TEXT DEFAULT '[]',
|
|
15956
|
+
next_steps TEXT,
|
|
15957
|
+
metadata TEXT DEFAULT '{}',
|
|
15958
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15959
|
+
);
|
|
15960
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id);
|
|
15961
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id);
|
|
15962
|
+
|
|
15963
|
+
CREATE TABLE IF NOT EXISTS agent_budgets (
|
|
15964
|
+
agent_id TEXT PRIMARY KEY,
|
|
15965
|
+
max_concurrent INTEGER DEFAULT 5,
|
|
15966
|
+
max_cost_usd DOUBLE PRECISION,
|
|
15967
|
+
max_task_minutes INTEGER,
|
|
15968
|
+
period_hours INTEGER DEFAULT 24,
|
|
15969
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
15970
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15971
|
+
);
|
|
15972
|
+
|
|
15973
|
+
INSERT INTO _migrations (id) VALUES (35) ON CONFLICT DO NOTHING;
|
|
15974
|
+
`,
|
|
15975
|
+
`
|
|
15976
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
15977
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
15978
|
+
message TEXT NOT NULL,
|
|
15979
|
+
email TEXT,
|
|
15980
|
+
category TEXT DEFAULT 'general',
|
|
15981
|
+
version TEXT,
|
|
15982
|
+
machine_id TEXT,
|
|
15983
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
15984
|
+
);
|
|
15985
|
+
|
|
15986
|
+
INSERT INTO _migrations (id) VALUES (36) ON CONFLICT DO NOTHING;
|
|
15987
|
+
`
|
|
15988
|
+
];
|
|
15989
|
+
});
|
|
15990
|
+
|
|
15991
|
+
// src/db/pg-migrate.ts
|
|
15992
|
+
var exports_pg_migrate = {};
|
|
15993
|
+
__export(exports_pg_migrate, {
|
|
15994
|
+
applyPgMigrations: () => applyPgMigrations
|
|
14368
15995
|
});
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
14389
|
-
function extractFromSource(source, filePath, tags = [...EXTRACT_TAGS]) {
|
|
14390
|
-
const regex = buildTagRegex(tags);
|
|
14391
|
-
const results = [];
|
|
14392
|
-
const lines = source.split(`
|
|
14393
|
-
`);
|
|
14394
|
-
for (let i = 0;i < lines.length; i++) {
|
|
14395
|
-
const line = lines[i];
|
|
14396
|
-
const match = line.match(regex);
|
|
14397
|
-
if (match) {
|
|
14398
|
-
const tag = match[1].toUpperCase();
|
|
14399
|
-
let message = match[2].trim();
|
|
14400
|
-
message = message.replace(/\s*\*\/\s*$/, "").replace(/\s*-->\s*$/, "").replace(/\s*-\}\s*$/, "").trim();
|
|
14401
|
-
if (message) {
|
|
14402
|
-
results.push({
|
|
14403
|
-
tag,
|
|
14404
|
-
message,
|
|
14405
|
-
file: filePath,
|
|
14406
|
-
line: i + 1,
|
|
14407
|
-
raw: line
|
|
14408
|
-
});
|
|
15996
|
+
async function applyPgMigrations(connectionString) {
|
|
15997
|
+
const pg = new PgAdapterAsync(connectionString);
|
|
15998
|
+
const result = {
|
|
15999
|
+
applied: [],
|
|
16000
|
+
alreadyApplied: [],
|
|
16001
|
+
errors: [],
|
|
16002
|
+
totalMigrations: PG_MIGRATIONS.length
|
|
16003
|
+
};
|
|
16004
|
+
try {
|
|
16005
|
+
await pg.run(`CREATE TABLE IF NOT EXISTS _pg_migrations (
|
|
16006
|
+
id SERIAL PRIMARY KEY,
|
|
16007
|
+
version INT UNIQUE NOT NULL,
|
|
16008
|
+
applied_at TIMESTAMPTZ DEFAULT NOW()
|
|
16009
|
+
)`);
|
|
16010
|
+
const applied = await pg.all("SELECT version FROM _pg_migrations ORDER BY version");
|
|
16011
|
+
const appliedSet = new Set(applied.map((r) => r.version));
|
|
16012
|
+
for (let i = 0;i < PG_MIGRATIONS.length; i++) {
|
|
16013
|
+
if (appliedSet.has(i)) {
|
|
16014
|
+
result.alreadyApplied.push(i);
|
|
16015
|
+
continue;
|
|
14409
16016
|
}
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
return [basePath];
|
|
14418
|
-
}
|
|
14419
|
-
const glob = new Bun.Glob("**/*");
|
|
14420
|
-
const files = [];
|
|
14421
|
-
for (const entry of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
|
|
14422
|
-
const parts = entry.split("/");
|
|
14423
|
-
if (parts.some((p) => SKIP_DIRS.has(p)))
|
|
14424
|
-
continue;
|
|
14425
|
-
const dotIdx = entry.lastIndexOf(".");
|
|
14426
|
-
if (dotIdx === -1)
|
|
14427
|
-
continue;
|
|
14428
|
-
const ext = entry.slice(dotIdx);
|
|
14429
|
-
if (!extensions.has(ext))
|
|
14430
|
-
continue;
|
|
14431
|
-
files.push(entry);
|
|
14432
|
-
}
|
|
14433
|
-
return files.sort();
|
|
14434
|
-
}
|
|
14435
|
-
function extractTodos(options, db) {
|
|
14436
|
-
const basePath = resolve2(options.path);
|
|
14437
|
-
const tags = options.patterns || [...EXTRACT_TAGS];
|
|
14438
|
-
const extensions = options.extensions ? new Set(options.extensions.map((e) => e.startsWith(".") ? e : `.${e}`)) : DEFAULT_EXTENSIONS;
|
|
14439
|
-
const files = collectFiles(basePath, extensions);
|
|
14440
|
-
const allComments = [];
|
|
14441
|
-
for (const file of files) {
|
|
14442
|
-
const fullPath = statSync2(basePath).isFile() ? basePath : join10(basePath, file);
|
|
14443
|
-
try {
|
|
14444
|
-
const source = readFileSync4(fullPath, "utf-8");
|
|
14445
|
-
const relPath = statSync2(basePath).isFile() ? relative2(resolve2(basePath, ".."), fullPath) : file;
|
|
14446
|
-
const comments = extractFromSource(source, relPath, tags);
|
|
14447
|
-
allComments.push(...comments);
|
|
14448
|
-
} catch {}
|
|
14449
|
-
}
|
|
14450
|
-
if (options.dry_run) {
|
|
14451
|
-
return { comments: allComments, tasks: [], skipped: 0 };
|
|
14452
|
-
}
|
|
14453
|
-
const tasks = [];
|
|
14454
|
-
let skipped = 0;
|
|
14455
|
-
const existingTasks = options.project_id ? listTasks({ project_id: options.project_id, tags: ["extracted"] }, db) : listTasks({ tags: ["extracted"] }, db);
|
|
14456
|
-
const existingKeys = new Set;
|
|
14457
|
-
for (const t of existingTasks) {
|
|
14458
|
-
const meta = t.metadata;
|
|
14459
|
-
if (meta?.["source_file"] && meta?.["source_line"]) {
|
|
14460
|
-
existingKeys.add(`${meta["source_file"]}:${meta["source_line"]}`);
|
|
14461
|
-
}
|
|
14462
|
-
}
|
|
14463
|
-
for (const comment of allComments) {
|
|
14464
|
-
const dedupKey = `${comment.file}:${comment.line}`;
|
|
14465
|
-
if (existingKeys.has(dedupKey)) {
|
|
14466
|
-
skipped++;
|
|
14467
|
-
continue;
|
|
14468
|
-
}
|
|
14469
|
-
const taskTags = ["extracted", comment.tag.toLowerCase(), ...options.tags || []];
|
|
14470
|
-
const task = createTask({
|
|
14471
|
-
title: `[${comment.tag}] ${comment.message}`,
|
|
14472
|
-
description: `Extracted from code comment in \`${comment.file}\` at line ${comment.line}:
|
|
14473
|
-
\`\`\`
|
|
14474
|
-
${comment.raw.trim()}
|
|
14475
|
-
\`\`\``,
|
|
14476
|
-
priority: tagToPriority(comment.tag),
|
|
14477
|
-
project_id: options.project_id,
|
|
14478
|
-
task_list_id: options.task_list_id,
|
|
14479
|
-
assigned_to: options.assigned_to,
|
|
14480
|
-
agent_id: options.agent_id,
|
|
14481
|
-
tags: taskTags,
|
|
14482
|
-
metadata: {
|
|
14483
|
-
source: "code_comment",
|
|
14484
|
-
comment_type: comment.tag,
|
|
14485
|
-
source_file: comment.file,
|
|
14486
|
-
source_line: comment.line
|
|
16017
|
+
try {
|
|
16018
|
+
await pg.exec(PG_MIGRATIONS[i]);
|
|
16019
|
+
await pg.run("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", i);
|
|
16020
|
+
result.applied.push(i);
|
|
16021
|
+
} catch (err) {
|
|
16022
|
+
result.errors.push(`Migration ${i}: ${err?.message ?? String(err)}`);
|
|
16023
|
+
break;
|
|
14487
16024
|
}
|
|
14488
|
-
}
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
path: comment.file,
|
|
14492
|
-
note: `Line ${comment.line}: ${comment.tag} comment`
|
|
14493
|
-
}, db);
|
|
14494
|
-
tasks.push(task);
|
|
14495
|
-
existingKeys.add(dedupKey);
|
|
16025
|
+
}
|
|
16026
|
+
} finally {
|
|
16027
|
+
await pg.close();
|
|
14496
16028
|
}
|
|
14497
|
-
return
|
|
16029
|
+
return result;
|
|
14498
16030
|
}
|
|
14499
|
-
var
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
init_task_files();
|
|
14503
|
-
EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
14504
|
-
DEFAULT_EXTENSIONS = new Set([
|
|
14505
|
-
".ts",
|
|
14506
|
-
".tsx",
|
|
14507
|
-
".js",
|
|
14508
|
-
".jsx",
|
|
14509
|
-
".mjs",
|
|
14510
|
-
".cjs",
|
|
14511
|
-
".py",
|
|
14512
|
-
".rb",
|
|
14513
|
-
".go",
|
|
14514
|
-
".rs",
|
|
14515
|
-
".c",
|
|
14516
|
-
".cpp",
|
|
14517
|
-
".h",
|
|
14518
|
-
".hpp",
|
|
14519
|
-
".java",
|
|
14520
|
-
".kt",
|
|
14521
|
-
".swift",
|
|
14522
|
-
".cs",
|
|
14523
|
-
".php",
|
|
14524
|
-
".sh",
|
|
14525
|
-
".bash",
|
|
14526
|
-
".zsh",
|
|
14527
|
-
".lua",
|
|
14528
|
-
".sql",
|
|
14529
|
-
".r",
|
|
14530
|
-
".R",
|
|
14531
|
-
".yaml",
|
|
14532
|
-
".yml",
|
|
14533
|
-
".toml",
|
|
14534
|
-
".css",
|
|
14535
|
-
".scss",
|
|
14536
|
-
".less",
|
|
14537
|
-
".vue",
|
|
14538
|
-
".svelte",
|
|
14539
|
-
".ex",
|
|
14540
|
-
".exs",
|
|
14541
|
-
".erl",
|
|
14542
|
-
".hs",
|
|
14543
|
-
".ml",
|
|
14544
|
-
".mli",
|
|
14545
|
-
".clj",
|
|
14546
|
-
".cljs"
|
|
14547
|
-
]);
|
|
14548
|
-
SKIP_DIRS = new Set([
|
|
14549
|
-
"node_modules",
|
|
14550
|
-
".git",
|
|
14551
|
-
"dist",
|
|
14552
|
-
"build",
|
|
14553
|
-
"out",
|
|
14554
|
-
".next",
|
|
14555
|
-
".turbo",
|
|
14556
|
-
"coverage",
|
|
14557
|
-
"__pycache__",
|
|
14558
|
-
".venv",
|
|
14559
|
-
"venv",
|
|
14560
|
-
"vendor",
|
|
14561
|
-
"target",
|
|
14562
|
-
".cache",
|
|
14563
|
-
".parcel-cache"
|
|
14564
|
-
]);
|
|
16031
|
+
var init_pg_migrate = __esm(() => {
|
|
16032
|
+
init_dist();
|
|
16033
|
+
init_pg_migrations();
|
|
14565
16034
|
});
|
|
14566
16035
|
|
|
14567
16036
|
// src/mcp/index.ts
|
|
@@ -18827,16 +20296,16 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
18827
20296
|
init_tasks();
|
|
18828
20297
|
init_config();
|
|
18829
20298
|
init_sync_utils();
|
|
18830
|
-
import { existsSync as
|
|
18831
|
-
import { join as
|
|
20299
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, readdirSync as readdirSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
20300
|
+
import { join as join10 } from "path";
|
|
18832
20301
|
function getTaskListDir(taskListId) {
|
|
18833
|
-
return
|
|
20302
|
+
return join10(HOME, ".claude", "tasks", taskListId);
|
|
18834
20303
|
}
|
|
18835
20304
|
function readClaudeTask(dir, filename) {
|
|
18836
|
-
return readJsonFile(
|
|
20305
|
+
return readJsonFile(join10(dir, filename));
|
|
18837
20306
|
}
|
|
18838
20307
|
function writeClaudeTask(dir, task) {
|
|
18839
|
-
writeJsonFile(
|
|
20308
|
+
writeJsonFile(join10(dir, `${task.id}.json`), task);
|
|
18840
20309
|
}
|
|
18841
20310
|
function toClaudeStatus(status) {
|
|
18842
20311
|
if (status === "pending" || status === "in_progress" || status === "completed") {
|
|
@@ -18848,14 +20317,14 @@ function toSqliteStatus(status) {
|
|
|
18848
20317
|
return status;
|
|
18849
20318
|
}
|
|
18850
20319
|
function readPrefixCounter(dir) {
|
|
18851
|
-
const path =
|
|
18852
|
-
if (!
|
|
20320
|
+
const path = join10(dir, ".prefix-counter");
|
|
20321
|
+
if (!existsSync9(path))
|
|
18853
20322
|
return 0;
|
|
18854
|
-
const val = parseInt(
|
|
20323
|
+
const val = parseInt(readFileSync5(path, "utf-8").trim(), 10);
|
|
18855
20324
|
return isNaN(val) ? 0 : val;
|
|
18856
20325
|
}
|
|
18857
20326
|
function writePrefixCounter(dir, value) {
|
|
18858
|
-
writeFileSync3(
|
|
20327
|
+
writeFileSync3(join10(dir, ".prefix-counter"), String(value));
|
|
18859
20328
|
}
|
|
18860
20329
|
function formatPrefixedSubject(title, prefix, counter) {
|
|
18861
20330
|
const padded = String(counter).padStart(5, "0");
|
|
@@ -18882,7 +20351,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
|
18882
20351
|
}
|
|
18883
20352
|
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
18884
20353
|
const dir = getTaskListDir(taskListId);
|
|
18885
|
-
if (!
|
|
20354
|
+
if (!existsSync9(dir))
|
|
18886
20355
|
ensureDir2(dir);
|
|
18887
20356
|
const filter = {};
|
|
18888
20357
|
if (projectId)
|
|
@@ -18891,7 +20360,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
18891
20360
|
const existingByTodosId = new Map;
|
|
18892
20361
|
const files = listJsonFiles(dir);
|
|
18893
20362
|
for (const f of files) {
|
|
18894
|
-
const path =
|
|
20363
|
+
const path = join10(dir, f);
|
|
18895
20364
|
const ct = readClaudeTask(dir, f);
|
|
18896
20365
|
if (ct?.metadata?.["todos_id"]) {
|
|
18897
20366
|
existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
|
|
@@ -18978,10 +20447,10 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
18978
20447
|
}
|
|
18979
20448
|
function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
18980
20449
|
const dir = getTaskListDir(taskListId);
|
|
18981
|
-
if (!
|
|
20450
|
+
if (!existsSync9(dir)) {
|
|
18982
20451
|
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
18983
20452
|
}
|
|
18984
|
-
const files =
|
|
20453
|
+
const files = readdirSync5(dir).filter((f) => f.endsWith(".json"));
|
|
18985
20454
|
let pulled = 0;
|
|
18986
20455
|
const errors2 = [];
|
|
18987
20456
|
const prefer = options.prefer || "remote";
|
|
@@ -18998,7 +20467,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
18998
20467
|
}
|
|
18999
20468
|
for (const f of files) {
|
|
19000
20469
|
try {
|
|
19001
|
-
const filePath =
|
|
20470
|
+
const filePath = join10(dir, f);
|
|
19002
20471
|
const ct = readClaudeTask(dir, f);
|
|
19003
20472
|
if (!ct)
|
|
19004
20473
|
continue;
|
|
@@ -19069,27 +20538,27 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
|
|
|
19069
20538
|
init_tasks();
|
|
19070
20539
|
init_sync_utils();
|
|
19071
20540
|
init_config();
|
|
19072
|
-
import { existsSync as
|
|
19073
|
-
import { join as
|
|
20541
|
+
import { existsSync as existsSync10 } from "fs";
|
|
20542
|
+
import { join as join11 } from "path";
|
|
19074
20543
|
function getTodosGlobalDir2() {
|
|
19075
|
-
const newDir =
|
|
19076
|
-
const legacyDir =
|
|
19077
|
-
if (!
|
|
20544
|
+
const newDir = join11(HOME, ".hasna", "todos");
|
|
20545
|
+
const legacyDir = join11(HOME, ".todos");
|
|
20546
|
+
if (!existsSync10(newDir) && existsSync10(legacyDir))
|
|
19078
20547
|
return legacyDir;
|
|
19079
20548
|
return newDir;
|
|
19080
20549
|
}
|
|
19081
20550
|
function agentBaseDir(agent) {
|
|
19082
20551
|
const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
|
|
19083
|
-
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] ||
|
|
20552
|
+
return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join11(getTodosGlobalDir2(), "agents");
|
|
19084
20553
|
}
|
|
19085
20554
|
function getTaskListDir2(agent, taskListId) {
|
|
19086
|
-
return
|
|
20555
|
+
return join11(agentBaseDir(agent), agent, taskListId);
|
|
19087
20556
|
}
|
|
19088
20557
|
function readAgentTask(dir, filename) {
|
|
19089
|
-
return readJsonFile(
|
|
20558
|
+
return readJsonFile(join11(dir, filename));
|
|
19090
20559
|
}
|
|
19091
20560
|
function writeAgentTask(dir, task) {
|
|
19092
|
-
writeJsonFile(
|
|
20561
|
+
writeJsonFile(join11(dir, `${task.id}.json`), task);
|
|
19093
20562
|
}
|
|
19094
20563
|
function taskToAgentTask(task, externalId, existingMeta) {
|
|
19095
20564
|
return {
|
|
@@ -19114,7 +20583,7 @@ function metadataKey(agent) {
|
|
|
19114
20583
|
}
|
|
19115
20584
|
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
19116
20585
|
const dir = getTaskListDir2(agent, taskListId);
|
|
19117
|
-
if (!
|
|
20586
|
+
if (!existsSync10(dir))
|
|
19118
20587
|
ensureDir2(dir);
|
|
19119
20588
|
const filter = {};
|
|
19120
20589
|
if (projectId)
|
|
@@ -19123,7 +20592,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
19123
20592
|
const existingByTodosId = new Map;
|
|
19124
20593
|
const files = listJsonFiles(dir);
|
|
19125
20594
|
for (const f of files) {
|
|
19126
|
-
const path =
|
|
20595
|
+
const path = join11(dir, f);
|
|
19127
20596
|
const at = readAgentTask(dir, f);
|
|
19128
20597
|
if (at?.metadata?.["todos_id"]) {
|
|
19129
20598
|
existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
|
|
@@ -19197,7 +20666,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
19197
20666
|
}
|
|
19198
20667
|
function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
19199
20668
|
const dir = getTaskListDir2(agent, taskListId);
|
|
19200
|
-
if (!
|
|
20669
|
+
if (!existsSync10(dir)) {
|
|
19201
20670
|
return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
|
|
19202
20671
|
}
|
|
19203
20672
|
const files = listJsonFiles(dir);
|
|
@@ -19216,7 +20685,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
|
19216
20685
|
}
|
|
19217
20686
|
for (const f of files) {
|
|
19218
20687
|
try {
|
|
19219
|
-
const filePath =
|
|
20688
|
+
const filePath = join11(dir, f);
|
|
19220
20689
|
const at = readAgentTask(dir, f);
|
|
19221
20690
|
if (!at)
|
|
19222
20691
|
continue;
|
|
@@ -19359,14 +20828,14 @@ init_config();
|
|
|
19359
20828
|
init_database();
|
|
19360
20829
|
init_checklists();
|
|
19361
20830
|
init_types();
|
|
19362
|
-
import { readFileSync as
|
|
19363
|
-
import { join as
|
|
20831
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
20832
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
19364
20833
|
import { fileURLToPath } from "url";
|
|
19365
20834
|
function getMcpVersion() {
|
|
19366
20835
|
try {
|
|
19367
|
-
const __dir =
|
|
19368
|
-
const pkgPath =
|
|
19369
|
-
return JSON.parse(
|
|
20836
|
+
const __dir = dirname3(fileURLToPath(import.meta.url));
|
|
20837
|
+
const pkgPath = join13(__dir, "..", "package.json");
|
|
20838
|
+
return JSON.parse(readFileSync7(pkgPath, "utf-8")).version || "0.0.0";
|
|
19370
20839
|
} catch {
|
|
19371
20840
|
return "0.0.0";
|
|
19372
20841
|
}
|
|
@@ -19402,6 +20871,7 @@ var STANDARD_EXCLUDED = new Set([
|
|
|
19402
20871
|
"list_templates",
|
|
19403
20872
|
"create_task_from_template",
|
|
19404
20873
|
"delete_template",
|
|
20874
|
+
"update_template",
|
|
19405
20875
|
"approve_task"
|
|
19406
20876
|
]);
|
|
19407
20877
|
function shouldRegisterTool(name) {
|
|
@@ -21223,7 +22693,8 @@ if (shouldRegisterTool("create_task_from_template")) {
|
|
|
21223
22693
|
}, async (params) => {
|
|
21224
22694
|
try {
|
|
21225
22695
|
const { taskFromTemplate: taskFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
21226
|
-
const
|
|
22696
|
+
const resolvedTemplateId = resolveId(params.template_id, "task_templates");
|
|
22697
|
+
const input = taskFromTemplate2(resolvedTemplateId, {
|
|
21227
22698
|
title: params.title,
|
|
21228
22699
|
description: params.description,
|
|
21229
22700
|
priority: params.priority,
|
|
@@ -21242,13 +22713,37 @@ if (shouldRegisterTool("delete_template")) {
|
|
|
21242
22713
|
server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
|
|
21243
22714
|
try {
|
|
21244
22715
|
const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
21245
|
-
const
|
|
22716
|
+
const resolvedId = resolveId(id, "task_templates");
|
|
22717
|
+
const deleted = deleteTemplate2(resolvedId);
|
|
21246
22718
|
return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
|
|
21247
22719
|
} catch (e) {
|
|
21248
22720
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
21249
22721
|
}
|
|
21250
22722
|
});
|
|
21251
22723
|
}
|
|
22724
|
+
if (shouldRegisterTool("update_template")) {
|
|
22725
|
+
server.tool("update_template", "Update a task template's name, title pattern, description, priority, tags, or other fields.", {
|
|
22726
|
+
id: exports_external.string(),
|
|
22727
|
+
name: exports_external.string().optional(),
|
|
22728
|
+
title_pattern: exports_external.string().optional(),
|
|
22729
|
+
description: exports_external.string().optional(),
|
|
22730
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
22731
|
+
tags: exports_external.array(exports_external.string()).optional(),
|
|
22732
|
+
project_id: exports_external.string().optional(),
|
|
22733
|
+
plan_id: exports_external.string().optional()
|
|
22734
|
+
}, async ({ id, ...updates }) => {
|
|
22735
|
+
try {
|
|
22736
|
+
const { updateTemplate: updateTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
22737
|
+
const resolvedId = resolveId(id, "task_templates");
|
|
22738
|
+
const t = updateTemplate2(resolvedId, updates);
|
|
22739
|
+
if (!t)
|
|
22740
|
+
return { content: [{ type: "text", text: `Template not found: ${id}` }], isError: true };
|
|
22741
|
+
return { content: [{ type: "text", text: `Template updated: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}" | ${t.priority}` }] };
|
|
22742
|
+
} catch (e) {
|
|
22743
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
22744
|
+
}
|
|
22745
|
+
});
|
|
22746
|
+
}
|
|
21252
22747
|
if (shouldRegisterTool("approve_task")) {
|
|
21253
22748
|
server.tool("approve_task", "Approve a task with requires_approval=true.", {
|
|
21254
22749
|
id: exports_external.string(),
|
|
@@ -22365,6 +23860,7 @@ if (shouldRegisterTool("search_tools")) {
|
|
|
22365
23860
|
"list_templates",
|
|
22366
23861
|
"create_task_from_template",
|
|
22367
23862
|
"delete_template",
|
|
23863
|
+
"update_template",
|
|
22368
23864
|
"bulk_update_tasks",
|
|
22369
23865
|
"bulk_create_tasks",
|
|
22370
23866
|
"get_task_stats",
|
|
@@ -22601,6 +24097,9 @@ if (shouldRegisterTool("describe_tools")) {
|
|
|
22601
24097
|
delete_template: `Delete a task template.
|
|
22602
24098
|
Params: id(string, req)
|
|
22603
24099
|
Example: {id: 'a1b2c3d4'}`,
|
|
24100
|
+
update_template: `Update a task template's name, title pattern, or other fields.
|
|
24101
|
+
Params: id(string, req), name(string), title_pattern(string), description(string), priority(low|medium|high|critical), tags(string[]), project_id(string), plan_id(string)
|
|
24102
|
+
Example: {id: 'a1b2c3d4', name: 'Renamed Template', priority: 'critical'}`,
|
|
22604
24103
|
get_active_work: `See all in-progress tasks and who is working on them.
|
|
22605
24104
|
Params: project_id(string, optional), task_list_id(string, optional)
|
|
22606
24105
|
Example: {project_id: 'a1b2c3d4'}`,
|
|
@@ -23411,6 +24910,49 @@ server.resource("task-lists", "todos://task-lists", { description: "All task lis
|
|
|
23411
24910
|
const lists = listTaskLists();
|
|
23412
24911
|
return { contents: [{ uri: "todos://task-lists", text: JSON.stringify(lists, null, 2), mimeType: "application/json" }] };
|
|
23413
24912
|
});
|
|
24913
|
+
if (shouldRegisterTool("migrate_pg")) {
|
|
24914
|
+
server.tool("migrate_pg", "Apply PostgreSQL schema migrations to the configured RDS instance", {
|
|
24915
|
+
connection_string: exports_external.string().optional().describe("PostgreSQL connection string (overrides cloud config)")
|
|
24916
|
+
}, async ({ connection_string }) => {
|
|
24917
|
+
try {
|
|
24918
|
+
let connStr;
|
|
24919
|
+
if (connection_string) {
|
|
24920
|
+
connStr = connection_string;
|
|
24921
|
+
} else {
|
|
24922
|
+
const { getConnectionString: getConnectionString2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
24923
|
+
connStr = getConnectionString2("todos");
|
|
24924
|
+
}
|
|
24925
|
+
const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
|
|
24926
|
+
const result = await applyPgMigrations2(connStr);
|
|
24927
|
+
const lines = [];
|
|
24928
|
+
if (result.applied.length > 0) {
|
|
24929
|
+
lines.push(`Applied ${result.applied.length} migration(s): ${result.applied.join(", ")}`);
|
|
24930
|
+
}
|
|
24931
|
+
if (result.alreadyApplied.length > 0) {
|
|
24932
|
+
lines.push(`Already applied: ${result.alreadyApplied.length} migration(s)`);
|
|
24933
|
+
}
|
|
24934
|
+
if (result.errors.length > 0) {
|
|
24935
|
+
lines.push(`Errors:
|
|
24936
|
+
${result.errors.join(`
|
|
24937
|
+
`)}`);
|
|
24938
|
+
}
|
|
24939
|
+
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
24940
|
+
lines.push("Schema is up to date.");
|
|
24941
|
+
}
|
|
24942
|
+
lines.push(`Total migrations: ${result.totalMigrations}`);
|
|
24943
|
+
return {
|
|
24944
|
+
content: [{ type: "text", text: lines.join(`
|
|
24945
|
+
`) }],
|
|
24946
|
+
isError: result.errors.length > 0
|
|
24947
|
+
};
|
|
24948
|
+
} catch (e) {
|
|
24949
|
+
return {
|
|
24950
|
+
content: [{ type: "text", text: `Migration failed: ${e?.message ?? String(e)}` }],
|
|
24951
|
+
isError: true
|
|
24952
|
+
};
|
|
24953
|
+
}
|
|
24954
|
+
});
|
|
24955
|
+
}
|
|
23414
24956
|
registerCloudTools(server, "todos");
|
|
23415
24957
|
async function main() {
|
|
23416
24958
|
const transport = new StdioServerTransport;
|