@anraktech/sync 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +221 -53
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,36 @@ var log = {
|
|
|
31
31
|
var CONFIG_DIR = join(homedir(), ".anrak-sync");
|
|
32
32
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
33
33
|
var CACHE_FILE = join(CONFIG_DIR, "cache.json");
|
|
34
|
+
function getWatchFolders(config) {
|
|
35
|
+
const folders = /* @__PURE__ */ new Set();
|
|
36
|
+
if (config.watchFolder) folders.add(config.watchFolder);
|
|
37
|
+
if (config.watchFolders) {
|
|
38
|
+
for (const f of config.watchFolders) folders.add(f);
|
|
39
|
+
}
|
|
40
|
+
return [...folders];
|
|
41
|
+
}
|
|
42
|
+
function addWatchFolder(config, folder) {
|
|
43
|
+
const existing = getWatchFolders(config);
|
|
44
|
+
if (existing.includes(folder)) return false;
|
|
45
|
+
if (!config.watchFolders) config.watchFolders = [];
|
|
46
|
+
config.watchFolders.push(folder);
|
|
47
|
+
saveConfig(config);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
function removeWatchFolder(config, folder) {
|
|
51
|
+
const all = getWatchFolders(config);
|
|
52
|
+
if (all.length <= 1) return { removed: false, error: "Cannot remove the only watch folder." };
|
|
53
|
+
if (!all.includes(folder)) return { removed: false, error: `Folder not found: ${folder}` };
|
|
54
|
+
if (config.watchFolder === folder) {
|
|
55
|
+
const remaining = all.filter((f) => f !== folder);
|
|
56
|
+
config.watchFolder = remaining[0];
|
|
57
|
+
config.watchFolders = remaining.slice(1);
|
|
58
|
+
} else {
|
|
59
|
+
config.watchFolders = (config.watchFolders || []).filter((f) => f !== folder);
|
|
60
|
+
}
|
|
61
|
+
saveConfig(config);
|
|
62
|
+
return { removed: true };
|
|
63
|
+
}
|
|
34
64
|
function ensureDir() {
|
|
35
65
|
if (!existsSync(CONFIG_DIR)) {
|
|
36
66
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -794,9 +824,12 @@ var TOOLS = [
|
|
|
794
824
|
}
|
|
795
825
|
];
|
|
796
826
|
function buildSystemPrompt(config) {
|
|
827
|
+
const folders = getWatchFolders(config);
|
|
828
|
+
const folderList = folders.map((f) => ` - ${f}`).join("\n");
|
|
797
829
|
return `You are AnrakLegal Sync, a terminal assistant that helps lawyers sync local files to their case management system.
|
|
798
830
|
|
|
799
|
-
Watch
|
|
831
|
+
Watch folders:
|
|
832
|
+
${folderList}
|
|
800
833
|
Server: ${config.apiUrl}
|
|
801
834
|
Home directory: ${HOME}
|
|
802
835
|
Platform: ${IS_MAC ? "macOS" : platform()}
|
|
@@ -1146,7 +1179,10 @@ function startAIAgent(ctx) {
|
|
|
1146
1179
|
];
|
|
1147
1180
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
1148
1181
|
const W = Math.min(process.stdout.columns || 60, 60);
|
|
1149
|
-
const
|
|
1182
|
+
const allFolders = getWatchFolders(ctx.config);
|
|
1183
|
+
const folderDisplays = allFolders.map(
|
|
1184
|
+
(f) => f.startsWith(HOME) ? "~" + f.slice(HOME.length) : f
|
|
1185
|
+
);
|
|
1150
1186
|
const stats = ctx.bootStats;
|
|
1151
1187
|
const pad = (s, len) => {
|
|
1152
1188
|
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -1178,14 +1214,20 @@ function startAIAgent(ctx) {
|
|
|
1178
1214
|
console.log("");
|
|
1179
1215
|
console.log(top);
|
|
1180
1216
|
console.log(empty);
|
|
1181
|
-
|
|
1217
|
+
if (folderDisplays.length === 1) {
|
|
1218
|
+
console.log(row(`${chalk2.dim("Folder")} ${folderDisplays[0]}`));
|
|
1219
|
+
} else {
|
|
1220
|
+
console.log(row(`${chalk2.dim("Folders")} ${chalk2.cyan(String(folderDisplays.length))} watched`));
|
|
1221
|
+
for (const fd of folderDisplays) {
|
|
1222
|
+
console.log(row(`${chalk2.dim(" \xB7")} ${fd}`));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1182
1225
|
console.log(row(`${chalk2.dim("Server")} ${ctx.config.apiUrl}`));
|
|
1183
1226
|
if (statusLine) {
|
|
1184
1227
|
console.log(row(`${chalk2.dim("Status")} ${statusLine}`));
|
|
1185
1228
|
}
|
|
1186
1229
|
console.log(empty);
|
|
1187
|
-
console.log(row(chalk2.dim("Type naturally, or
|
|
1188
|
-
console.log(row(chalk2.dim("Type /help to see all commands.")));
|
|
1230
|
+
console.log(row(chalk2.dim("Type naturally, or press / for commands.")));
|
|
1189
1231
|
console.log(empty);
|
|
1190
1232
|
console.log(bot);
|
|
1191
1233
|
console.log("");
|
|
@@ -1246,10 +1288,80 @@ function startAIAgent(ctx) {
|
|
|
1246
1288
|
}
|
|
1247
1289
|
},
|
|
1248
1290
|
folder: {
|
|
1249
|
-
description: "
|
|
1250
|
-
handler: () => {
|
|
1251
|
-
|
|
1252
|
-
|
|
1291
|
+
description: "Manage watch folders (add/remove/list)",
|
|
1292
|
+
handler: async () => {
|
|
1293
|
+
const sub = currentSlashArgs.trim();
|
|
1294
|
+
if (!sub || sub === "list") {
|
|
1295
|
+
const folders = getWatchFolders(ctx.config);
|
|
1296
|
+
console.log("");
|
|
1297
|
+
console.log(chalk2.bold(` ${folders.length} watch folder${folders.length !== 1 ? "s" : ""}`));
|
|
1298
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(40)));
|
|
1299
|
+
for (let i = 0; i < folders.length; i++) {
|
|
1300
|
+
const f = folders[i];
|
|
1301
|
+
const display = f.startsWith(HOME) ? "~" + f.slice(HOME.length) : f;
|
|
1302
|
+
const tag = f === ctx.config.watchFolder ? chalk2.cyan(" (primary)") : "";
|
|
1303
|
+
console.log(` ${chalk2.dim(`${i + 1}.`)} ${display}${tag}`);
|
|
1304
|
+
}
|
|
1305
|
+
console.log("");
|
|
1306
|
+
console.log(chalk2.dim(" /folder add <path> Add a new watch folder"));
|
|
1307
|
+
console.log(chalk2.dim(" /folder remove <path> Stop watching a folder"));
|
|
1308
|
+
console.log(chalk2.dim(" /folder set <path> Set primary watch folder"));
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const parts = sub.split(/\s+/);
|
|
1312
|
+
const action = parts[0].toLowerCase();
|
|
1313
|
+
const folderArg = parts.slice(1).join(" ");
|
|
1314
|
+
if (action === "add") {
|
|
1315
|
+
if (!folderArg) {
|
|
1316
|
+
console.log(chalk2.red(" Usage: /folder add <path>"));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const absPath = normalizePath(folderArg);
|
|
1320
|
+
if (!ctx.addWatchFolder) {
|
|
1321
|
+
console.log(chalk2.red(" Watch folder management not available."));
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const result = await ctx.addWatchFolder(absPath);
|
|
1325
|
+
if (result.added) {
|
|
1326
|
+
const display = (result.path || absPath).startsWith(HOME) ? "~" + (result.path || absPath).slice(HOME.length) : result.path || absPath;
|
|
1327
|
+
console.log(chalk2.green(` Added: ${display}`));
|
|
1328
|
+
} else {
|
|
1329
|
+
console.log(chalk2.yellow(` ${result.error || "Could not add folder"}`));
|
|
1330
|
+
}
|
|
1331
|
+
} else if (action === "remove" || action === "rm") {
|
|
1332
|
+
if (!folderArg) {
|
|
1333
|
+
console.log(chalk2.red(" Usage: /folder remove <path>"));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const absPath = normalizePath(folderArg);
|
|
1337
|
+
if (!ctx.removeWatchFolder) {
|
|
1338
|
+
console.log(chalk2.red(" Watch folder management not available."));
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const result = ctx.removeWatchFolder(absPath);
|
|
1342
|
+
if (result.removed) {
|
|
1343
|
+
const display = absPath.startsWith(HOME) ? "~" + absPath.slice(HOME.length) : absPath;
|
|
1344
|
+
console.log(chalk2.green(` Removed: ${display}`));
|
|
1345
|
+
} else {
|
|
1346
|
+
console.log(chalk2.yellow(` ${result.error || "Could not remove folder"}`));
|
|
1347
|
+
}
|
|
1348
|
+
} else if (action === "set") {
|
|
1349
|
+
if (!folderArg) {
|
|
1350
|
+
console.log(chalk2.red(" Usage: /folder set <path>"));
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const absPath = normalizePath(folderArg);
|
|
1354
|
+
if (!existsSync2(absPath) || !statSync(absPath).isDirectory()) {
|
|
1355
|
+
console.log(chalk2.red(` Not a valid directory: ${absPath}`));
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
ctx.config.watchFolder = absPath;
|
|
1359
|
+
saveConfig(ctx.config);
|
|
1360
|
+
const display = absPath.startsWith(HOME) ? "~" + absPath.slice(HOME.length) : absPath;
|
|
1361
|
+
console.log(chalk2.green(` Primary folder set to: ${display}`));
|
|
1362
|
+
} else {
|
|
1363
|
+
console.log(chalk2.dim(" Usage: /folder [list|add|remove|set] <path>"));
|
|
1364
|
+
}
|
|
1253
1365
|
}
|
|
1254
1366
|
},
|
|
1255
1367
|
mappings: {
|
|
@@ -1330,6 +1442,7 @@ function startAIAgent(ctx) {
|
|
|
1330
1442
|
}
|
|
1331
1443
|
}
|
|
1332
1444
|
};
|
|
1445
|
+
let currentSlashArgs = "";
|
|
1333
1446
|
async function promptLoop() {
|
|
1334
1447
|
while (true) {
|
|
1335
1448
|
let input;
|
|
@@ -1341,9 +1454,27 @@ function startAIAgent(ctx) {
|
|
|
1341
1454
|
const trimmed = input.trim();
|
|
1342
1455
|
if (!trimmed) continue;
|
|
1343
1456
|
if (trimmed.startsWith("/")) {
|
|
1344
|
-
const
|
|
1457
|
+
const withoutSlash = trimmed.slice(1);
|
|
1458
|
+
const parts = withoutSlash.split(/\s+/);
|
|
1459
|
+
const cmd = (parts[0] || "").toLowerCase();
|
|
1460
|
+
if (!cmd) {
|
|
1461
|
+
console.log("");
|
|
1462
|
+
console.log(chalk2.bold(" Commands"));
|
|
1463
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(Math.min(process.stdout.columns || 60, 60) - 4)));
|
|
1464
|
+
for (const [name, { description }] of Object.entries(slashCommands)) {
|
|
1465
|
+
const label = chalk2.cyan(`/${name}`);
|
|
1466
|
+
const dots = chalk2.dim(".".repeat(Math.max(2, 18 - name.length)));
|
|
1467
|
+
console.log(` ${label} ${dots} ${chalk2.dim(description)}`);
|
|
1468
|
+
}
|
|
1469
|
+
console.log("");
|
|
1470
|
+
console.log(chalk2.dim(" Or just type naturally \u2014 the AI understands plain English."));
|
|
1471
|
+
console.log("");
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1345
1474
|
if (slashCommands[cmd]) {
|
|
1475
|
+
currentSlashArgs = parts.slice(1).join(" ");
|
|
1346
1476
|
await slashCommands[cmd].handler();
|
|
1477
|
+
currentSlashArgs = "";
|
|
1347
1478
|
console.log("");
|
|
1348
1479
|
continue;
|
|
1349
1480
|
}
|
|
@@ -1351,7 +1482,7 @@ function startAIAgent(ctx) {
|
|
|
1351
1482
|
if (matches.length > 0) {
|
|
1352
1483
|
console.log(chalk2.dim(` Did you mean: ${matches.map((m) => chalk2.cyan(`/${m}`)).join(", ")}?`));
|
|
1353
1484
|
} else {
|
|
1354
|
-
console.log(chalk2.dim(` Unknown command. Type ${chalk2.cyan("/
|
|
1485
|
+
console.log(chalk2.dim(` Unknown command. Type ${chalk2.cyan("/")} for available commands.`));
|
|
1355
1486
|
}
|
|
1356
1487
|
console.log("");
|
|
1357
1488
|
continue;
|
|
@@ -1381,10 +1512,9 @@ function startAIAgent(ctx) {
|
|
|
1381
1512
|
|
|
1382
1513
|
// src/watcher.ts
|
|
1383
1514
|
async function scanFolder(config) {
|
|
1384
|
-
const
|
|
1515
|
+
const folders = getWatchFolders(config);
|
|
1385
1516
|
let scanned = 0;
|
|
1386
|
-
|
|
1387
|
-
function walk(dir) {
|
|
1517
|
+
function walk(dir, watchRoot) {
|
|
1388
1518
|
let entries;
|
|
1389
1519
|
try {
|
|
1390
1520
|
entries = readdirSync2(dir);
|
|
@@ -1401,14 +1531,18 @@ async function scanFolder(config) {
|
|
|
1401
1531
|
continue;
|
|
1402
1532
|
}
|
|
1403
1533
|
if (s.isDirectory()) {
|
|
1404
|
-
walk(fullPath);
|
|
1534
|
+
walk(fullPath, watchRoot);
|
|
1405
1535
|
} else if (s.isFile() && isSupportedFile(entry)) {
|
|
1406
1536
|
scanned++;
|
|
1407
|
-
void enqueue(fullPath,
|
|
1537
|
+
void enqueue(fullPath, watchRoot);
|
|
1408
1538
|
}
|
|
1409
1539
|
}
|
|
1410
1540
|
}
|
|
1411
|
-
|
|
1541
|
+
for (const folder of folders) {
|
|
1542
|
+
if (existsSync3(folder)) {
|
|
1543
|
+
walk(folder, folder);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1412
1546
|
await new Promise((r) => setTimeout(r, 500));
|
|
1413
1547
|
return { scanned, queued: queueSize() };
|
|
1414
1548
|
}
|
|
@@ -1475,7 +1609,7 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
1475
1609
|
}
|
|
1476
1610
|
}
|
|
1477
1611
|
async function startWatching(config) {
|
|
1478
|
-
const
|
|
1612
|
+
const folders = getWatchFolders(config);
|
|
1479
1613
|
let cases = await listCases(config);
|
|
1480
1614
|
const { scanned, queued } = await scanFolder(config);
|
|
1481
1615
|
if (queued > 0) {
|
|
@@ -1490,19 +1624,7 @@ async function startWatching(config) {
|
|
|
1490
1624
|
} catch {
|
|
1491
1625
|
}
|
|
1492
1626
|
}, 5 * 60 * 1e3);
|
|
1493
|
-
const
|
|
1494
|
-
ignored: /(^|[\/\\])(\.|~\$|Thumbs\.db|desktop\.ini)/,
|
|
1495
|
-
persistent: true,
|
|
1496
|
-
ignoreInitial: true,
|
|
1497
|
-
// We already scanned
|
|
1498
|
-
awaitWriteFinish: {
|
|
1499
|
-
stabilityThreshold: 2e3,
|
|
1500
|
-
// Wait 2s after last change
|
|
1501
|
-
pollInterval: 500
|
|
1502
|
-
},
|
|
1503
|
-
depth: 5
|
|
1504
|
-
// Max folder depth
|
|
1505
|
-
});
|
|
1627
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
1506
1628
|
let debounceTimer = null;
|
|
1507
1629
|
function scheduleProcess() {
|
|
1508
1630
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -1517,26 +1639,44 @@ async function startWatching(config) {
|
|
|
1517
1639
|
}
|
|
1518
1640
|
}, 3e3);
|
|
1519
1641
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1642
|
+
function createWatcher(folder) {
|
|
1643
|
+
if (watchers.has(folder)) return;
|
|
1644
|
+
const w = watch(folder, {
|
|
1645
|
+
ignored: /(^|[\/\\])(\.|~\$|Thumbs\.db|desktop\.ini)/,
|
|
1646
|
+
persistent: true,
|
|
1647
|
+
ignoreInitial: true,
|
|
1648
|
+
awaitWriteFinish: {
|
|
1649
|
+
stabilityThreshold: 2e3,
|
|
1650
|
+
pollInterval: 500
|
|
1651
|
+
},
|
|
1652
|
+
depth: 5
|
|
1653
|
+
});
|
|
1654
|
+
w.on("add", async (path) => {
|
|
1655
|
+
const filename = basename5(path);
|
|
1656
|
+
if (!isSupportedFile(filename) || isIgnoredFile(filename)) return;
|
|
1657
|
+
log.file("detected", relative2(folder, path));
|
|
1658
|
+
await enqueue(path, folder);
|
|
1659
|
+
scheduleProcess();
|
|
1660
|
+
});
|
|
1661
|
+
w.on("change", async (path) => {
|
|
1662
|
+
const filename = basename5(path);
|
|
1663
|
+
if (!isSupportedFile(filename) || isIgnoredFile(filename)) return;
|
|
1664
|
+
log.file("changed", relative2(folder, path));
|
|
1665
|
+
await enqueue(path, folder);
|
|
1666
|
+
scheduleProcess();
|
|
1667
|
+
});
|
|
1668
|
+
w.on("unlink", (path) => {
|
|
1669
|
+
log.file("deleted", relative2(folder, path));
|
|
1670
|
+
});
|
|
1671
|
+
w.on("error", (err) => {
|
|
1672
|
+
log.error(`Watcher error (${basename5(folder)}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1673
|
+
});
|
|
1674
|
+
watchers.set(folder, w);
|
|
1675
|
+
log.debug(`Watching: ${folder}`);
|
|
1676
|
+
}
|
|
1677
|
+
for (const folder of folders) {
|
|
1678
|
+
createWatcher(folder);
|
|
1679
|
+
}
|
|
1540
1680
|
startAIAgent({
|
|
1541
1681
|
config,
|
|
1542
1682
|
bootStats: { cases: cases.length, scanned, queued },
|
|
@@ -1545,7 +1685,8 @@ async function startWatching(config) {
|
|
|
1545
1685
|
cases = await listCases(config);
|
|
1546
1686
|
},
|
|
1547
1687
|
triggerScan: async () => {
|
|
1548
|
-
|
|
1688
|
+
const allFolders = getWatchFolders(config);
|
|
1689
|
+
log.info(`Re-scanning ${allFolders.length} folder(s)...`);
|
|
1549
1690
|
const result = await scanFolder(config);
|
|
1550
1691
|
log.info(`Scanned ${result.scanned} files, ${result.queued} need syncing`);
|
|
1551
1692
|
if (result.queued > 0) {
|
|
@@ -1555,13 +1696,40 @@ async function startWatching(config) {
|
|
|
1555
1696
|
},
|
|
1556
1697
|
scanFolder: async (folderPath) => {
|
|
1557
1698
|
await scanExternalFolder(config, folderPath, cases);
|
|
1699
|
+
},
|
|
1700
|
+
addWatchFolder: async (folderPath) => {
|
|
1701
|
+
const absPath = resolve2(folderPath);
|
|
1702
|
+
if (!existsSync3(absPath) || !statSync2(absPath).isDirectory()) {
|
|
1703
|
+
return { added: false, error: `Not a valid directory: ${absPath}` };
|
|
1704
|
+
}
|
|
1705
|
+
const added = addWatchFolder(config, absPath);
|
|
1706
|
+
if (!added) {
|
|
1707
|
+
return { added: false, error: "Folder already being watched" };
|
|
1708
|
+
}
|
|
1709
|
+
createWatcher(absPath);
|
|
1710
|
+
log.success(`Now watching: ${absPath}`);
|
|
1711
|
+
return { added: true, path: absPath };
|
|
1712
|
+
},
|
|
1713
|
+
removeWatchFolder: (folderPath) => {
|
|
1714
|
+
const absPath = resolve2(folderPath);
|
|
1715
|
+
const result = removeWatchFolder(config, absPath);
|
|
1716
|
+
if (result.removed) {
|
|
1717
|
+
const w = watchers.get(absPath);
|
|
1718
|
+
if (w) {
|
|
1719
|
+
void w.close();
|
|
1720
|
+
watchers.delete(absPath);
|
|
1721
|
+
}
|
|
1722
|
+
log.success(`Stopped watching: ${absPath}`);
|
|
1723
|
+
}
|
|
1724
|
+
return result;
|
|
1558
1725
|
}
|
|
1559
1726
|
});
|
|
1560
1727
|
const shutdown = () => {
|
|
1561
1728
|
log.info("Shutting down...");
|
|
1562
1729
|
clearInterval(refreshInterval);
|
|
1563
1730
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1564
|
-
|
|
1731
|
+
const closePromises = [...watchers.values()].map((w) => w.close());
|
|
1732
|
+
Promise.all(closePromises).then(() => {
|
|
1565
1733
|
log.success("Stopped");
|
|
1566
1734
|
process.exit(0);
|
|
1567
1735
|
});
|