@anraktech/sync 0.4.0 → 0.6.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 +149 -42
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4,8 +4,8 @@
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";
8
- import { resolve as resolve2 } from "path";
7
+ import { existsSync as existsSync4, statSync as statSync3 } from "fs";
8
+ import { resolve as resolve3 } from "path";
9
9
  import chalk3 from "chalk";
10
10
 
11
11
  // src/config.ts
@@ -86,7 +86,7 @@ function openBrowser(url) {
86
86
  exec(`${cmd} "${url}"`);
87
87
  }
88
88
  function findFreePort() {
89
- return new Promise((resolve3, reject) => {
89
+ return new Promise((resolve4, reject) => {
90
90
  const srv = createServer();
91
91
  srv.listen(0, () => {
92
92
  const addr = srv.address();
@@ -96,14 +96,14 @@ function findFreePort() {
96
96
  return;
97
97
  }
98
98
  const port = addr.port;
99
- srv.close(() => resolve3(port));
99
+ srv.close(() => resolve4(port));
100
100
  });
101
101
  });
102
102
  }
103
103
  async function browserLogin(apiUrl) {
104
104
  const port = await findFreePort();
105
105
  return new Promise(
106
- (resolve3, reject) => {
106
+ (resolve4, reject) => {
107
107
  let server;
108
108
  const timeout = setTimeout(() => {
109
109
  server?.close();
@@ -150,7 +150,7 @@ async function browserLogin(apiUrl) {
150
150
  const tokens = await resp.json();
151
151
  clearTimeout(timeout);
152
152
  server.close();
153
- resolve3(tokens);
153
+ resolve4(tokens);
154
154
  } catch (err) {
155
155
  clearTimeout(timeout);
156
156
  server.close();
@@ -338,11 +338,11 @@ function persist() {
338
338
  if (cache) saveCache(cache);
339
339
  }
340
340
  function hashFile(filePath) {
341
- return new Promise((resolve3, reject) => {
341
+ return new Promise((resolve4, reject) => {
342
342
  const hash = createHash("sha256");
343
343
  const stream = createReadStream(filePath);
344
344
  stream.on("data", (chunk) => hash.update(chunk));
345
- stream.on("end", () => resolve3(hash.digest("hex")));
345
+ stream.on("end", () => resolve4(hash.digest("hex")));
346
346
  stream.on("error", reject);
347
347
  });
348
348
  }
@@ -416,8 +416,8 @@ function resetCache() {
416
416
 
417
417
  // src/watcher.ts
418
418
  import { watch } from "chokidar";
419
- import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
420
- import { join as join2, relative as relative2, basename as basename4, resolve } from "path";
419
+ import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync3 } from "fs";
420
+ import { join as join3, relative as relative2, basename as basename4, resolve as resolve2 } from "path";
421
421
 
422
422
  // src/uploader.ts
423
423
  import { stat as stat2 } from "fs/promises";
@@ -619,19 +619,73 @@ function sleep(ms) {
619
619
  // src/agent.ts
620
620
  import { createInterface } from "readline/promises";
621
621
  import { stdin, stdout } from "process";
622
+ import { homedir as homedir2, platform } from "os";
623
+ import { resolve, join as join2 } from "path";
624
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
622
625
  import chalk2 from "chalk";
626
+ var HOME = homedir2();
627
+ var IS_MAC = platform() === "darwin";
628
+ var FOLDER_SHORTCUTS = {
629
+ downloads: join2(HOME, "Downloads"),
630
+ download: join2(HOME, "Downloads"),
631
+ desktop: join2(HOME, "Desktop"),
632
+ documents: join2(HOME, "Documents"),
633
+ document: join2(HOME, "Documents"),
634
+ home: HOME,
635
+ "~": HOME
636
+ };
637
+ function normalizePath(folderPath) {
638
+ const trimmed = folderPath.trim();
639
+ if (trimmed.startsWith("~/") || trimmed === "~") {
640
+ return resolve(trimmed.replace(/^~/, HOME));
641
+ }
642
+ const lower = trimmed.toLowerCase();
643
+ if (FOLDER_SHORTCUTS[lower]) {
644
+ return FOLDER_SHORTCUTS[lower];
645
+ }
646
+ const withoutSlash = lower.replace(/^\//, "");
647
+ if (FOLDER_SHORTCUTS[withoutSlash]) {
648
+ return FOLDER_SHORTCUTS[withoutSlash];
649
+ }
650
+ if (trimmed.startsWith("/") || /^[A-Z]:\\/i.test(trimmed)) {
651
+ return resolve(trimmed);
652
+ }
653
+ return resolve(HOME, trimmed);
654
+ }
623
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
+ },
624
678
  {
625
679
  type: "function",
626
680
  function: {
627
681
  name: "scan_folder",
628
- 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.",
629
683
  parameters: {
630
684
  type: "object",
631
685
  properties: {
632
686
  folderPath: {
633
687
  type: "string",
634
- description: "Absolute path to the folder, e.g. /Users/name/Downloads"
688
+ description: "Absolute path to the folder to sync"
635
689
  }
636
690
  },
637
691
  required: ["folderPath"]
@@ -684,21 +738,81 @@ function buildSystemPrompt(config) {
684
738
 
685
739
  Watch folder: ${config.watchFolder}
686
740
  Server: ${config.apiUrl}
741
+ Home directory: ${HOME}
742
+ Platform: ${IS_MAC ? "macOS" : platform()}
687
743
 
688
- 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.
689
745
 
690
746
  Rules:
691
747
  - Be concise. This is a terminal \u2014 1-3 lines unless showing a list.
692
748
  - Do NOT use markdown. Plain text only.
693
- - When user mentions a folder like "downloads" or "desktop", expand to full path (macOS: /Users/<name>/Downloads, Windows: C:\\Users\\<name>\\Downloads).
749
+ - IMPORTANT: When user mentions a folder like "downloads" or "desktop", ALWAYS use the full absolute path. Examples:
750
+ "downloads" or "my downloads" \u2192 ${join2(HOME, "Downloads")}
751
+ "desktop" \u2192 ${join2(HOME, "Desktop")}
752
+ "documents" \u2192 ${join2(HOME, "Documents")}
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.
694
758
  - Call tools to perform actions. Summarize results naturally after.
759
+ - If a tool returns an error, report it clearly to the user.
695
760
  - Do NOT use thinking tags or reasoning tags in your output.`;
696
761
  }
697
762
  async function executeTool(name, args, ctx) {
698
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
+ }
699
812
  case "scan_folder": {
700
- const folderPath = args.folderPath;
701
- if (!folderPath) return JSON.stringify({ error: "Missing folderPath" });
813
+ const rawPath = args.folderPath;
814
+ if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
815
+ const folderPath = normalizePath(rawPath);
702
816
  try {
703
817
  await ctx.scanFolder(folderPath);
704
818
  const stats = getStats();
@@ -800,18 +914,9 @@ async function agentTurn(messages, ctx) {
800
914
  }
801
915
  let textContent = "";
802
916
  const toolCalls = /* @__PURE__ */ new Map();
803
- let isFirstText = true;
804
917
  for await (const { delta } of parseSSE(response)) {
805
918
  if (delta.content) {
806
- if (isFirstText) {
807
- process.stdout.write(" ");
808
- isFirstText = false;
809
- }
810
- const cleaned = delta.content.replace(/<\/?think>/g, "");
811
- if (cleaned) {
812
- process.stdout.write(cleaned);
813
- textContent += cleaned;
814
- }
919
+ textContent += delta.content;
815
920
  }
816
921
  if (delta.tool_calls) {
817
922
  for (const tc of delta.tool_calls) {
@@ -828,8 +933,10 @@ async function agentTurn(messages, ctx) {
828
933
  }
829
934
  }
830
935
  }
936
+ textContent = textContent.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
831
937
  if (textContent) {
832
- process.stdout.write("\n");
938
+ process.stdout.write(` ${textContent}
939
+ `);
833
940
  }
834
941
  if (toolCalls.size === 0) {
835
942
  return textContent;
@@ -918,16 +1025,16 @@ async function scanFolder(config) {
918
1025
  function walk(dir) {
919
1026
  let entries;
920
1027
  try {
921
- entries = readdirSync(dir);
1028
+ entries = readdirSync2(dir);
922
1029
  } catch {
923
1030
  return;
924
1031
  }
925
1032
  for (const entry of entries) {
926
1033
  if (isIgnoredFile(entry)) continue;
927
- const fullPath = join2(dir, entry);
1034
+ const fullPath = join3(dir, entry);
928
1035
  let s;
929
1036
  try {
930
- s = statSync(fullPath);
1037
+ s = statSync2(fullPath);
931
1038
  } catch {
932
1039
  continue;
933
1040
  }
@@ -959,12 +1066,12 @@ async function pushSync(config) {
959
1066
  );
960
1067
  }
961
1068
  async function scanExternalFolder(config, folderPath, cases) {
962
- const absPath = resolve(folderPath);
963
- if (!existsSync2(absPath)) {
1069
+ const absPath = resolve2(folderPath);
1070
+ if (!existsSync3(absPath)) {
964
1071
  log.error(`Folder not found: ${absPath}`);
965
1072
  return;
966
1073
  }
967
- if (!statSync(absPath).isDirectory()) {
1074
+ if (!statSync2(absPath).isDirectory()) {
968
1075
  log.error(`Not a directory: ${absPath}`);
969
1076
  return;
970
1077
  }
@@ -973,16 +1080,16 @@ async function scanExternalFolder(config, folderPath, cases) {
973
1080
  function walk(dir) {
974
1081
  let entries;
975
1082
  try {
976
- entries = readdirSync(dir);
1083
+ entries = readdirSync2(dir);
977
1084
  } catch {
978
1085
  return;
979
1086
  }
980
1087
  for (const entry of entries) {
981
1088
  if (isIgnoredFile(entry)) continue;
982
- const fullPath = join2(dir, entry);
1089
+ const fullPath = join3(dir, entry);
983
1090
  let s;
984
1091
  try {
985
- s = statSync(fullPath);
1092
+ s = statSync2(fullPath);
986
1093
  } catch {
987
1094
  continue;
988
1095
  }
@@ -990,7 +1097,7 @@ async function scanExternalFolder(config, folderPath, cases) {
990
1097
  walk(fullPath);
991
1098
  } else if (s.isFile() && isSupportedFile(entry)) {
992
1099
  fileCount++;
993
- void enqueue(fullPath, join2(absPath, ".."));
1100
+ void enqueue(fullPath, join3(absPath, ".."));
994
1101
  }
995
1102
  }
996
1103
  }
@@ -1107,10 +1214,10 @@ async function startWatching(config) {
1107
1214
  // src/cli.ts
1108
1215
  import { readFileSync as readFileSync2 } from "fs";
1109
1216
  import { fileURLToPath } from "url";
1110
- import { dirname as dirname2, join as join3 } from "path";
1217
+ import { dirname as dirname2, join as join4 } from "path";
1111
1218
  var __filename2 = fileURLToPath(import.meta.url);
1112
1219
  var __dirname2 = dirname2(__filename2);
1113
- var pkg = JSON.parse(readFileSync2(join3(__dirname2, "..", "package.json"), "utf-8"));
1220
+ var pkg = JSON.parse(readFileSync2(join4(__dirname2, "..", "package.json"), "utf-8"));
1114
1221
  var program = new Command();
1115
1222
  program.name("anrak-sync").description("AnrakLegal desktop file sync \u2014 watches local folders, syncs to case management").version(pkg.version);
1116
1223
  program.command("init").description("Set up AnrakLegal Sync (first-time configuration)").option("--password", "Use email/password login instead of browser").action(async (opts) => {
@@ -1151,13 +1258,13 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
1151
1258
  const watchInput = await rl2.question(
1152
1259
  ` Watch folder ${chalk3.dim(`(${defaultFolder})`)}: `
1153
1260
  );
1154
- const watchFolder = resolve2(watchInput || defaultFolder);
1261
+ const watchFolder = resolve3(watchInput || defaultFolder);
1155
1262
  rl2.close();
1156
- if (!existsSync3(watchFolder)) {
1263
+ if (!existsSync4(watchFolder)) {
1157
1264
  log.warn(
1158
1265
  `Folder ${watchFolder} does not exist \u2014 it will be created when you add files`
1159
1266
  );
1160
- } else if (!statSync2(watchFolder).isDirectory()) {
1267
+ } else if (!statSync3(watchFolder).isDirectory()) {
1161
1268
  log.error(`${watchFolder} is not a directory`);
1162
1269
  process.exit(1);
1163
1270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anraktech/sync",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "AnrakLegal desktop file sync agent — watches local folders and syncs to case management",
5
5
  "type": "module",
6
6
  "bin": {