@anraktech/sync 0.5.0 → 0.7.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 +184 -30
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,9 +4,9 @@
4
4
  import { Command } from "commander";
5
5
  import { createInterface as createInterface2 } from "readline/promises";
6
6
  import { stdin as stdin2, stdout as stdout2 } from "process";
7
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
7
+ import { existsSync as existsSync4, statSync as statSync3 } from "fs";
8
8
  import { resolve as resolve3 } from "path";
9
- import chalk3 from "chalk";
9
+ import chalk4 from "chalk";
10
10
 
11
11
  // src/config.ts
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -416,7 +416,7 @@ function resetCache() {
416
416
 
417
417
  // src/watcher.ts
418
418
  import { watch } from "chokidar";
419
- import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
419
+ import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync3 } from "fs";
420
420
  import { join as join3, relative as relative2, basename as basename4, resolve as resolve2 } from "path";
421
421
 
422
422
  // src/uploader.ts
@@ -621,6 +621,7 @@ import { createInterface } from "readline/promises";
621
621
  import { stdin, stdout } from "process";
622
622
  import { homedir as homedir2, platform } from "os";
623
623
  import { resolve, join as join2 } from "path";
624
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
624
625
  import chalk2 from "chalk";
625
626
  var HOME = homedir2();
626
627
  var IS_MAC = platform() === "darwin";
@@ -652,17 +653,39 @@ function normalizePath(folderPath) {
652
653
  return resolve(HOME, trimmed);
653
654
  }
654
655
  var TOOLS = [
656
+ {
657
+ type: "function",
658
+ function: {
659
+ name: "browse_folder",
660
+ description: "List files and subfolders in a local directory. Shows file names, sizes, types, and whether they are syncable. Use FIRST when user wants to see what's in a folder before syncing.",
661
+ parameters: {
662
+ type: "object",
663
+ properties: {
664
+ folderPath: {
665
+ type: "string",
666
+ description: "Absolute path to the folder to browse"
667
+ },
668
+ filter: {
669
+ type: "string",
670
+ enum: ["all", "syncable", "folders"],
671
+ description: "Filter: 'all' shows everything, 'syncable' shows only supported file types (PDF, DOCX, etc.), 'folders' shows only subfolders. Default: all"
672
+ }
673
+ },
674
+ required: ["folderPath"]
675
+ }
676
+ }
677
+ },
655
678
  {
656
679
  type: "function",
657
680
  function: {
658
681
  name: "scan_folder",
659
- description: "Scan a local folder on the user's computer and sync all supported files (PDF, DOCX, etc.) to a matching legal case. Use when user asks to scan, sync, upload, or look at a specific folder.",
682
+ description: "Sync all supported files from a local folder to a matching legal case. Use when user explicitly asks to sync, upload, or push files. If unsure what's in the folder, use browse_folder first.",
660
683
  parameters: {
661
684
  type: "object",
662
685
  properties: {
663
686
  folderPath: {
664
687
  type: "string",
665
- description: "Absolute path to the folder, e.g. /Users/name/Downloads"
688
+ description: "Absolute path to the folder to sync"
666
689
  }
667
690
  },
668
691
  required: ["folderPath"]
@@ -718,7 +741,7 @@ Server: ${config.apiUrl}
718
741
  Home directory: ${HOME}
719
742
  Platform: ${IS_MAC ? "macOS" : platform()}
720
743
 
721
- You can scan folders, list cases, show sync status, and more using your tools.
744
+ You can browse local folders, scan & sync files, list cases, show sync status, and more.
722
745
 
723
746
  Rules:
724
747
  - Be concise. This is a terminal \u2014 1-3 lines unless showing a list.
@@ -728,12 +751,64 @@ Rules:
728
751
  "desktop" \u2192 ${join2(HOME, "Desktop")}
729
752
  "documents" \u2192 ${join2(HOME, "Documents")}
730
753
  "~/SomeFolder" \u2192 ${HOME}/SomeFolder
754
+ - When user says "look at" or "check" a folder, use browse_folder FIRST to show what's there.
755
+ - When user says "sync" or "upload", use scan_folder to actually sync.
756
+ - When browsing, highlight which files are syncable (PDF, DOCX, XLSX, etc.) vs not.
757
+ - Present file lists in a clean table/list format with names and sizes.
731
758
  - Call tools to perform actions. Summarize results naturally after.
732
759
  - If a tool returns an error, report it clearly to the user.
733
760
  - Do NOT use thinking tags or reasoning tags in your output.`;
734
761
  }
735
762
  async function executeTool(name, args, ctx) {
736
763
  switch (name) {
764
+ case "browse_folder": {
765
+ const rawPath = args.folderPath;
766
+ if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
767
+ const folderPath = normalizePath(rawPath);
768
+ if (!existsSync2(folderPath)) {
769
+ return JSON.stringify({ error: `Folder not found: ${folderPath}` });
770
+ }
771
+ try {
772
+ const s = statSync(folderPath);
773
+ if (!s.isDirectory()) {
774
+ return JSON.stringify({ error: `Not a directory: ${folderPath}` });
775
+ }
776
+ } catch {
777
+ return JSON.stringify({ error: `Cannot access: ${folderPath}` });
778
+ }
779
+ const filter = args.filter || "all";
780
+ const entries = readdirSync(folderPath);
781
+ const items = [];
782
+ for (const entry of entries) {
783
+ if (isIgnoredFile(entry)) continue;
784
+ const fullPath = join2(folderPath, entry);
785
+ let st;
786
+ try {
787
+ st = statSync(fullPath);
788
+ } catch {
789
+ continue;
790
+ }
791
+ if (st.isDirectory()) {
792
+ if (filter !== "syncable") {
793
+ items.push({ name: entry, type: "folder" });
794
+ }
795
+ } else if (st.isFile()) {
796
+ if (filter === "folders") continue;
797
+ const syncable = isSupportedFile(entry);
798
+ if (filter === "syncable" && !syncable) continue;
799
+ const sizeKB = Math.round(st.size / 1024);
800
+ const size = sizeKB > 1024 ? `${(sizeKB / 1024).toFixed(1)} MB` : `${sizeKB} KB`;
801
+ items.push({ name: entry, type: "file", size, syncable });
802
+ }
803
+ }
804
+ return JSON.stringify({
805
+ folder: folderPath,
806
+ totalItems: items.length,
807
+ items: items.slice(0, 50),
808
+ // Cap at 50 to avoid token explosion
809
+ truncated: items.length > 50
810
+ });
811
+ }
737
812
  case "scan_folder": {
738
813
  const rawPath = args.folderPath;
739
814
  if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
@@ -950,7 +1025,7 @@ async function scanFolder(config) {
950
1025
  function walk(dir) {
951
1026
  let entries;
952
1027
  try {
953
- entries = readdirSync(dir);
1028
+ entries = readdirSync2(dir);
954
1029
  } catch {
955
1030
  return;
956
1031
  }
@@ -959,7 +1034,7 @@ async function scanFolder(config) {
959
1034
  const fullPath = join3(dir, entry);
960
1035
  let s;
961
1036
  try {
962
- s = statSync(fullPath);
1037
+ s = statSync2(fullPath);
963
1038
  } catch {
964
1039
  continue;
965
1040
  }
@@ -992,11 +1067,11 @@ async function pushSync(config) {
992
1067
  }
993
1068
  async function scanExternalFolder(config, folderPath, cases) {
994
1069
  const absPath = resolve2(folderPath);
995
- if (!existsSync2(absPath)) {
1070
+ if (!existsSync3(absPath)) {
996
1071
  log.error(`Folder not found: ${absPath}`);
997
1072
  return;
998
1073
  }
999
- if (!statSync(absPath).isDirectory()) {
1074
+ if (!statSync2(absPath).isDirectory()) {
1000
1075
  log.error(`Not a directory: ${absPath}`);
1001
1076
  return;
1002
1077
  }
@@ -1005,7 +1080,7 @@ async function scanExternalFolder(config, folderPath, cases) {
1005
1080
  function walk(dir) {
1006
1081
  let entries;
1007
1082
  try {
1008
- entries = readdirSync(dir);
1083
+ entries = readdirSync2(dir);
1009
1084
  } catch {
1010
1085
  return;
1011
1086
  }
@@ -1014,7 +1089,7 @@ async function scanExternalFolder(config, folderPath, cases) {
1014
1089
  const fullPath = join3(dir, entry);
1015
1090
  let s;
1016
1091
  try {
1017
- s = statSync(fullPath);
1092
+ s = statSync2(fullPath);
1018
1093
  } catch {
1019
1094
  continue;
1020
1095
  }
@@ -1136,6 +1211,84 @@ async function startWatching(config) {
1136
1211
  process.on("SIGTERM", shutdown);
1137
1212
  }
1138
1213
 
1214
+ // src/updater.ts
1215
+ import { execFileSync } from "child_process";
1216
+ import chalk3 from "chalk";
1217
+ var PACKAGE_NAME = "@anraktech/sync";
1218
+ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
1219
+ var updateCache = { lastCheck: 0, latestVersion: null };
1220
+ function compareVersions(current, latest) {
1221
+ const a = current.split(".").map(Number);
1222
+ const b = latest.split(".").map(Number);
1223
+ for (let i = 0; i < 3; i++) {
1224
+ if ((b[i] ?? 0) > (a[i] ?? 0)) return 1;
1225
+ if ((b[i] ?? 0) < (a[i] ?? 0)) return -1;
1226
+ }
1227
+ return 0;
1228
+ }
1229
+ async function fetchLatestVersion() {
1230
+ try {
1231
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
1232
+ signal: AbortSignal.timeout(5e3)
1233
+ });
1234
+ if (!res.ok) return null;
1235
+ const data = await res.json();
1236
+ return data.version ?? null;
1237
+ } catch {
1238
+ return null;
1239
+ }
1240
+ }
1241
+ function performUpdate(currentVersion, latestVersion) {
1242
+ console.log("");
1243
+ console.log(
1244
+ chalk3.dim(` Update available: ${currentVersion} \u2192 `) + chalk3.green(latestVersion)
1245
+ );
1246
+ console.log(chalk3.dim(" Updating..."));
1247
+ try {
1248
+ execFileSync("npm", ["install", "-g", `${PACKAGE_NAME}@${latestVersion}`], {
1249
+ stdio: "pipe",
1250
+ timeout: 6e4
1251
+ });
1252
+ console.log(chalk3.green(" Updated successfully. Restart to use the new version."));
1253
+ console.log("");
1254
+ return true;
1255
+ } catch {
1256
+ try {
1257
+ execFileSync("npm", ["install", "-g", `${PACKAGE_NAME}@${latestVersion}`, "--prefix", process.env.HOME + "/.npm-global"], {
1258
+ stdio: "pipe",
1259
+ timeout: 6e4
1260
+ });
1261
+ console.log(chalk3.green(" Updated successfully. Restart to use the new version."));
1262
+ console.log("");
1263
+ return true;
1264
+ } catch {
1265
+ console.log(
1266
+ chalk3.dim(" Auto-update failed. Run manually: ") + chalk3.cyan(`npm install -g ${PACKAGE_NAME}`)
1267
+ );
1268
+ console.log("");
1269
+ return false;
1270
+ }
1271
+ }
1272
+ }
1273
+ async function checkForUpdates(currentVersion) {
1274
+ try {
1275
+ const now = Date.now();
1276
+ if (now - updateCache.lastCheck < CHECK_INTERVAL_MS && updateCache.latestVersion) {
1277
+ if (compareVersions(currentVersion, updateCache.latestVersion) > 0) {
1278
+ performUpdate(currentVersion, updateCache.latestVersion);
1279
+ }
1280
+ return;
1281
+ }
1282
+ const latestVersion = await fetchLatestVersion();
1283
+ updateCache = { lastCheck: now, latestVersion };
1284
+ if (!latestVersion) return;
1285
+ if (compareVersions(currentVersion, latestVersion) > 0) {
1286
+ performUpdate(currentVersion, latestVersion);
1287
+ }
1288
+ } catch {
1289
+ }
1290
+ }
1291
+
1139
1292
  // src/cli.ts
1140
1293
  import { readFileSync as readFileSync2 } from "fs";
1141
1294
  import { fileURLToPath } from "url";
@@ -1143,14 +1296,15 @@ import { dirname as dirname2, join as join4 } from "path";
1143
1296
  var __filename2 = fileURLToPath(import.meta.url);
1144
1297
  var __dirname2 = dirname2(__filename2);
1145
1298
  var pkg = JSON.parse(readFileSync2(join4(__dirname2, "..", "package.json"), "utf-8"));
1299
+ await checkForUpdates(pkg.version);
1146
1300
  var program = new Command();
1147
1301
  program.name("anrak-sync").description("AnrakLegal desktop file sync \u2014 watches local folders, syncs to case management").version(pkg.version);
1148
1302
  program.command("init").description("Set up AnrakLegal Sync (first-time configuration)").option("--password", "Use email/password login instead of browser").action(async (opts) => {
1149
1303
  const rl = createInterface2({ input: stdin2, output: stdout2 });
1150
1304
  try {
1151
- console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Setup\n"));
1305
+ console.log(chalk4.bold.blue("\n AnrakLegal Sync \u2014 Setup\n"));
1152
1306
  const apiUrl = await rl.question(
1153
- ` AnrakLegal URL ${chalk3.dim("(https://anrak.legal)")}: `
1307
+ ` AnrakLegal URL ${chalk4.dim("(https://anrak.legal)")}: `
1154
1308
  ) || "https://anrak.legal";
1155
1309
  log.info("Connecting to server...");
1156
1310
  const serverConfig = await fetchServerConfig(apiUrl);
@@ -1181,15 +1335,15 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
1181
1335
  const rl2 = createInterface2({ input: stdin2, output: stdout2 });
1182
1336
  const defaultFolder = process.platform === "win32" ? "C:\\Cases" : `${process.env.HOME}/Cases`;
1183
1337
  const watchInput = await rl2.question(
1184
- ` Watch folder ${chalk3.dim(`(${defaultFolder})`)}: `
1338
+ ` Watch folder ${chalk4.dim(`(${defaultFolder})`)}: `
1185
1339
  );
1186
1340
  const watchFolder = resolve3(watchInput || defaultFolder);
1187
1341
  rl2.close();
1188
- if (!existsSync3(watchFolder)) {
1342
+ if (!existsSync4(watchFolder)) {
1189
1343
  log.warn(
1190
1344
  `Folder ${watchFolder} does not exist \u2014 it will be created when you add files`
1191
1345
  );
1192
- } else if (!statSync2(watchFolder).isDirectory()) {
1346
+ } else if (!statSync3(watchFolder).isDirectory()) {
1193
1347
  log.error(`${watchFolder} is not a directory`);
1194
1348
  process.exit(1);
1195
1349
  }
@@ -1207,7 +1361,7 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
1207
1361
  log.info(`Config saved to ${getConfigDir()}`);
1208
1362
  log.info(`Watching: ${watchFolder}`);
1209
1363
  console.log(
1210
- chalk3.dim("\n Run `anrak-sync start` to begin syncing\n")
1364
+ chalk4.dim("\n Run `anrak-sync start` to begin syncing\n")
1211
1365
  );
1212
1366
  } catch (err) {
1213
1367
  log.error(err instanceof Error ? err.message : String(err));
@@ -1242,7 +1396,7 @@ program.command("login").description("Re-authenticate with AnrakLegal").option("
1242
1396
  });
1243
1397
  program.command("start").description("Start watching for file changes and syncing").action(async () => {
1244
1398
  const config = requireConfig();
1245
- console.log(chalk3.bold.blue("\n AnrakLegal Sync\n"));
1399
+ console.log(chalk4.bold.blue("\n AnrakLegal Sync\n"));
1246
1400
  log.info(`Watching: ${config.watchFolder}`);
1247
1401
  log.info(`Server: ${config.apiUrl}`);
1248
1402
  console.log("");
@@ -1255,7 +1409,7 @@ program.command("start").description("Start watching for file changes and syncin
1255
1409
  });
1256
1410
  program.command("push").description("One-time sync \u2014 upload all new/changed files, then exit").action(async () => {
1257
1411
  const config = requireConfig();
1258
- console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Push\n"));
1412
+ console.log(chalk4.bold.blue("\n AnrakLegal Sync \u2014 Push\n"));
1259
1413
  log.info(`Folder: ${config.watchFolder}`);
1260
1414
  log.info(`Server: ${config.apiUrl}`);
1261
1415
  console.log("");
@@ -1269,24 +1423,24 @@ program.command("push").description("One-time sync \u2014 upload all new/changed
1269
1423
  program.command("status").description("Show sync status").action(async () => {
1270
1424
  const config = requireConfig();
1271
1425
  const stats = getStats();
1272
- console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Status\n"));
1426
+ console.log(chalk4.bold.blue("\n AnrakLegal Sync \u2014 Status\n"));
1273
1427
  console.log(` Server: ${config.apiUrl}`);
1274
1428
  console.log(` Watch folder: ${config.watchFolder}`);
1275
1429
  console.log(` Config: ${getConfigDir()}`);
1276
1430
  console.log("");
1277
1431
  console.log(` Files tracked: ${stats.totalFiles}`);
1278
- console.log(` Synced: ${chalk3.green(stats.synced)}`);
1279
- console.log(` Pending: ${chalk3.yellow(stats.pending)}`);
1280
- console.log(` Errors: ${chalk3.red(stats.errors)}`);
1432
+ console.log(` Synced: ${chalk4.green(stats.synced)}`);
1433
+ console.log(` Pending: ${chalk4.yellow(stats.pending)}`);
1434
+ console.log(` Errors: ${chalk4.red(stats.errors)}`);
1281
1435
  console.log(` Mapped folders: ${stats.mappedFolders}`);
1282
1436
  try {
1283
1437
  const cases = await listCases(config);
1284
1438
  console.log(`
1285
1439
  Server cases: ${cases.length}`);
1286
- console.log(` Auth: ${chalk3.green("valid")}`);
1440
+ console.log(` Auth: ${chalk4.green("valid")}`);
1287
1441
  } catch {
1288
1442
  console.log(`
1289
- Auth: ${chalk3.red("expired \u2014 run anrak-sync login")}`);
1443
+ Auth: ${chalk4.red("expired \u2014 run anrak-sync login")}`);
1290
1444
  }
1291
1445
  console.log("");
1292
1446
  });
@@ -1294,13 +1448,13 @@ program.command("map").description("Show folder-to-case mappings").action(async
1294
1448
  const config = requireConfig();
1295
1449
  const mappings = getAllMappings();
1296
1450
  const entries = Object.entries(mappings);
1297
- console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Mappings\n"));
1451
+ console.log(chalk4.bold.blue("\n AnrakLegal Sync \u2014 Mappings\n"));
1298
1452
  if (entries.length === 0) {
1299
1453
  log.info("No mappings yet. Run `anrak-sync push` or `anrak-sync start` to create them.");
1300
1454
  } else {
1301
1455
  for (const [folder, mapping] of entries) {
1302
1456
  console.log(
1303
- ` ${chalk3.cyan(folder)} -> ${mapping.caseNumber} (${chalk3.dim(mapping.caseName)})`
1457
+ ` ${chalk4.cyan(folder)} -> ${mapping.caseNumber} (${chalk4.dim(mapping.caseName)})`
1304
1458
  );
1305
1459
  }
1306
1460
  }
@@ -1309,9 +1463,9 @@ program.command("map").description("Show folder-to-case mappings").action(async
1309
1463
  const mappedIds = new Set(entries.map(([, m]) => m.caseId));
1310
1464
  const unmapped = cases.filter((c) => !mappedIds.has(c.id));
1311
1465
  if (unmapped.length > 0) {
1312
- console.log(chalk3.dim("\n Unmapped server cases:"));
1466
+ console.log(chalk4.dim("\n Unmapped server cases:"));
1313
1467
  for (const c of unmapped) {
1314
- console.log(` ${chalk3.dim(c.caseNumber)} ${chalk3.dim(c.caseName)}`);
1468
+ console.log(` ${chalk4.dim(c.caseNumber)} ${chalk4.dim(c.caseName)}`);
1315
1469
  }
1316
1470
  }
1317
1471
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anraktech/sync",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "AnrakLegal desktop file sync agent — watches local folders and syncs to case management",
5
5
  "type": "module",
6
6
  "bin": {