@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.
Files changed (2) hide show
  1. package/dist/cli.js +221 -53
  2. 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 folder: ${config.watchFolder}
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 watchDisplay = ctx.config.watchFolder.startsWith(HOME) ? "~" + ctx.config.watchFolder.slice(HOME.length) : ctx.config.watchFolder;
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
- console.log(row(`${chalk2.dim("Folder")} ${watchDisplay}`));
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 use / commands.")));
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: "Show watch folder path",
1250
- handler: () => {
1251
- console.log(` ${chalk2.dim("Watch folder")} ${ctx.config.watchFolder}`);
1252
- console.log(` ${chalk2.dim("Config dir")} ${getConfigDir()}`);
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 cmd = trimmed.slice(1).toLowerCase().split(/\s+/)[0];
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("/help")} for available commands.`));
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 folder = config.watchFolder;
1515
+ const folders = getWatchFolders(config);
1385
1516
  let scanned = 0;
1386
- let queued = 0;
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, folder).then(() => queued++);
1537
+ void enqueue(fullPath, watchRoot);
1408
1538
  }
1409
1539
  }
1410
1540
  }
1411
- walk(folder);
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 folder = config.watchFolder;
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 watcher = watch(folder, {
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
- watcher.on("add", async (path) => {
1521
- const filename = basename5(path);
1522
- if (!isSupportedFile(filename) || isIgnoredFile(filename)) return;
1523
- log.file("detected", relative2(folder, path));
1524
- await enqueue(path, folder);
1525
- scheduleProcess();
1526
- });
1527
- watcher.on("change", async (path) => {
1528
- const filename = basename5(path);
1529
- if (!isSupportedFile(filename) || isIgnoredFile(filename)) return;
1530
- log.file("changed", relative2(folder, path));
1531
- await enqueue(path, folder);
1532
- scheduleProcess();
1533
- });
1534
- watcher.on("unlink", (path) => {
1535
- log.file("deleted", relative2(folder, path));
1536
- });
1537
- watcher.on("error", (err) => {
1538
- log.error(`Watcher error: ${err instanceof Error ? err.message : String(err)}`);
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
- log.info(`Re-scanning ${folder}...`);
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
- watcher.close().then(() => {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anraktech/sync",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "AnrakLegal desktop file sync agent — watches local folders and syncs to case management",
5
5
  "type": "module",
6
6
  "bin": {