@anraktech/sync 0.10.0 → 0.11.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 +173 -8
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -619,7 +619,7 @@ function sleep(ms) {
619
619
  import { createInterface } from "readline/promises";
620
620
  import { stdin, stdout } from "process";
621
621
  import { homedir as homedir2, platform } from "os";
622
- import { resolve, join as join2, dirname as dirname2 } from "path";
622
+ import { resolve, join as join2, dirname as dirname2, basename as basename4 } from "path";
623
623
  import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
624
624
  import { fileURLToPath } from "url";
625
625
  import chalk2 from "chalk";
@@ -663,11 +663,57 @@ function normalizePath(folderPath) {
663
663
  return resolve(HOME, trimmed);
664
664
  }
665
665
  var TOOLS = [
666
+ {
667
+ type: "function",
668
+ function: {
669
+ name: "search_files",
670
+ description: "Search for files by name across one or more directories. Supports partial matching and glob patterns. Returns full paths, sizes, and whether files are syncable. Use this when user asks to find a specific file.",
671
+ parameters: {
672
+ type: "object",
673
+ properties: {
674
+ query: {
675
+ type: "string",
676
+ description: "Search query \u2014 filename or partial name to match (case-insensitive). Examples: 'rental agreement', 'Black & White', '.pdf'"
677
+ },
678
+ searchPath: {
679
+ type: "string",
680
+ description: "Directory to search in. Defaults to home directory. Use absolute paths."
681
+ },
682
+ maxDepth: {
683
+ type: "number",
684
+ description: "Maximum directory depth to search. Default: 3. Increase for deeper searches."
685
+ }
686
+ },
687
+ required: ["query"]
688
+ }
689
+ }
690
+ },
691
+ {
692
+ type: "function",
693
+ function: {
694
+ name: "upload_to_case",
695
+ description: "Upload a specific file to a specific legal case. Use when the user wants to upload a particular file to a particular case. Requires the full file path and case identifier (case number or name).",
696
+ parameters: {
697
+ type: "object",
698
+ properties: {
699
+ filePath: {
700
+ type: "string",
701
+ description: "Absolute path to the file to upload"
702
+ },
703
+ caseIdentifier: {
704
+ type: "string",
705
+ description: "Case number (e.g. 'CASE-001') or case name to upload to. Will fuzzy-match against available cases."
706
+ }
707
+ },
708
+ required: ["filePath", "caseIdentifier"]
709
+ }
710
+ }
711
+ },
666
712
  {
667
713
  type: "function",
668
714
  function: {
669
715
  name: "browse_folder",
670
- 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.",
716
+ description: "List files and subfolders in a local directory. Shows file names, sizes, types, and whether they are syncable. Use when user wants to see what's in a folder. For finding a specific file, use search_files instead.",
671
717
  parameters: {
672
718
  type: "object",
673
719
  properties: {
@@ -679,6 +725,10 @@ var TOOLS = [
679
725
  type: "string",
680
726
  enum: ["all", "syncable", "folders"],
681
727
  description: "Filter: 'all' shows everything, 'syncable' shows only supported file types (PDF, DOCX, etc.), 'folders' shows only subfolders. Default: all"
728
+ },
729
+ nameFilter: {
730
+ type: "string",
731
+ description: "Optional: filter items by name (case-insensitive substring match). Use to narrow down large folders."
682
732
  }
683
733
  },
684
734
  required: ["folderPath"]
@@ -689,7 +739,7 @@ var TOOLS = [
689
739
  type: "function",
690
740
  function: {
691
741
  name: "scan_folder",
692
- 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.",
742
+ description: "Sync ALL supported files from a local folder to a matching legal case. Use ONLY when user explicitly asks to sync an entire folder. For uploading a single file, use upload_to_case instead.",
693
743
  parameters: {
694
744
  type: "object",
695
745
  properties: {
@@ -761,8 +811,10 @@ Rules:
761
811
  "desktop" \u2192 ${join2(HOME, "Desktop")}
762
812
  "documents" \u2192 ${join2(HOME, "Documents")}
763
813
  "~/SomeFolder" \u2192 ${HOME}/SomeFolder
764
- - When user says "look at" or "check" a folder, use browse_folder FIRST to show what's there.
765
- - When user says "sync" or "upload", use scan_folder to actually sync.
814
+ - FINDING FILES: When user asks to find or locate a specific file, ALWAYS use search_files. NEVER browse a folder hoping to find it.
815
+ - UPLOADING: When user wants to upload a specific file to a case, use upload_to_case (NOT scan_folder). scan_folder uploads EVERYTHING.
816
+ - When user says "look at" or "check" a folder, use browse_folder to show what's there.
817
+ - When user says "sync all" or "sync this folder", use scan_folder.
766
818
  - When browsing, highlight which files are syncable (PDF, DOCX, XLSX, etc.) vs not.
767
819
  - Present file lists in a clean table/list format with names and sizes.
768
820
  - Call tools to perform actions. Summarize results naturally after.
@@ -771,6 +823,117 @@ Rules:
771
823
  }
772
824
  async function executeTool(name, args, ctx) {
773
825
  switch (name) {
826
+ case "search_files": {
827
+ let searchDir2 = function(dir, depth) {
828
+ if (depth > maxDepth || results.length >= 20) return;
829
+ let entries;
830
+ try {
831
+ entries = readdirSync(dir);
832
+ } catch {
833
+ return;
834
+ }
835
+ for (const entry of entries) {
836
+ if (results.length >= 20) break;
837
+ if (isIgnoredFile(entry)) continue;
838
+ const fullPath = join2(dir, entry);
839
+ let st;
840
+ try {
841
+ st = statSync(fullPath);
842
+ } catch {
843
+ continue;
844
+ }
845
+ if (st.isDirectory()) {
846
+ searchDir2(fullPath, depth + 1);
847
+ } else if (st.isFile() && entry.toLowerCase().includes(queryLower)) {
848
+ const sizeKB = Math.round(st.size / 1024);
849
+ const size = sizeKB > 1024 ? `${(sizeKB / 1024).toFixed(1)} MB` : `${sizeKB} KB`;
850
+ results.push({
851
+ name: entry,
852
+ path: fullPath,
853
+ size,
854
+ syncable: isSupportedFile(entry)
855
+ });
856
+ }
857
+ }
858
+ };
859
+ var searchDir = searchDir2;
860
+ const query = (args.query || "").trim();
861
+ if (!query) return JSON.stringify({ error: "Missing search query" });
862
+ const rawSearch = args.searchPath || HOME;
863
+ const searchPath = normalizePath(rawSearch);
864
+ const maxDepth = Math.min(args.maxDepth || 3, 6);
865
+ if (!existsSync2(searchPath) || !statSync(searchPath).isDirectory()) {
866
+ return JSON.stringify({ error: `Search path not found: ${searchPath}` });
867
+ }
868
+ const queryLower = query.toLowerCase();
869
+ const results = [];
870
+ searchDir2(searchPath, 0);
871
+ return JSON.stringify({
872
+ query,
873
+ searchPath,
874
+ found: results.length,
875
+ maxResults: 20,
876
+ results
877
+ });
878
+ }
879
+ case "upload_to_case": {
880
+ const rawPath = args.filePath;
881
+ const caseId = args.caseIdentifier;
882
+ if (!rawPath) return JSON.stringify({ error: "Missing filePath" });
883
+ if (!caseId) return JSON.stringify({ error: "Missing caseIdentifier" });
884
+ const filePath = normalizePath(rawPath);
885
+ if (!existsSync2(filePath)) {
886
+ return JSON.stringify({ error: `File not found: ${filePath}` });
887
+ }
888
+ const fileStat = statSync(filePath);
889
+ if (!fileStat.isFile()) {
890
+ return JSON.stringify({ error: `Not a file: ${filePath}` });
891
+ }
892
+ if (fileStat.size > 50 * 1024 * 1024) {
893
+ return JSON.stringify({ error: `File exceeds 50MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)}MB)` });
894
+ }
895
+ if (!isSupportedFile(basename4(filePath))) {
896
+ return JSON.stringify({ error: `Unsupported file type: ${basename4(filePath)}. Supported: PDF, DOCX, XLSX, PPTX, TXT, images` });
897
+ }
898
+ await ctx.refreshCases();
899
+ const cases = ctx.getCases();
900
+ const searchLower = caseId.toLowerCase();
901
+ let matchedCase = cases.find(
902
+ (c) => c.caseNumber.toLowerCase() === searchLower || c.caseName.toLowerCase() === searchLower
903
+ );
904
+ if (!matchedCase) {
905
+ matchedCase = cases.find(
906
+ (c) => c.caseNumber.toLowerCase().includes(searchLower) || c.caseName.toLowerCase().includes(searchLower)
907
+ );
908
+ }
909
+ if (!matchedCase) {
910
+ return JSON.stringify({
911
+ error: `No case matching "${caseId}". Available cases: ${cases.map((c) => `${c.caseNumber} (${c.caseName})`).join(", ")}`
912
+ });
913
+ }
914
+ try {
915
+ log.file("uploading", `${basename4(filePath)} -> ${matchedCase.caseName}`);
916
+ const result = await uploadFile(ctx.config, matchedCase.id, filePath);
917
+ if (result.success && result.linkedDocumentIds.length > 0) {
918
+ const hash = await hashFile(filePath);
919
+ markSynced(filePath, hash, fileStat.size, matchedCase.id, result.linkedDocumentIds[0]);
920
+ log.success(`Uploaded ${basename4(filePath)} to ${matchedCase.caseName}`);
921
+ return JSON.stringify({
922
+ success: true,
923
+ filename: basename4(filePath),
924
+ caseName: matchedCase.caseName,
925
+ caseNumber: matchedCase.caseNumber,
926
+ documentId: result.linkedDocumentIds[0]
927
+ });
928
+ } else {
929
+ return JSON.stringify({
930
+ error: `Upload completed but no document was linked. The file may be a duplicate.`
931
+ });
932
+ }
933
+ } catch (err) {
934
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
935
+ }
936
+ }
774
937
  case "browse_folder": {
775
938
  const rawPath = args.folderPath;
776
939
  if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
@@ -787,10 +950,12 @@ async function executeTool(name, args, ctx) {
787
950
  return JSON.stringify({ error: `Cannot access: ${folderPath}` });
788
951
  }
789
952
  const filter = args.filter || "all";
953
+ const nameFilter = (args.nameFilter || "").toLowerCase();
790
954
  const entries = readdirSync(folderPath);
791
955
  const items = [];
792
956
  for (const entry of entries) {
793
957
  if (isIgnoredFile(entry)) continue;
958
+ if (nameFilter && !entry.toLowerCase().includes(nameFilter)) continue;
794
959
  const fullPath = join2(folderPath, entry);
795
960
  let st;
796
961
  try {
@@ -814,9 +979,9 @@ async function executeTool(name, args, ctx) {
814
979
  return JSON.stringify({
815
980
  folder: folderPath,
816
981
  totalItems: items.length,
817
- items: items.slice(0, 50),
818
- // Cap at 50 to avoid token explosion
819
- truncated: items.length > 50
982
+ items: items.slice(0, 100),
983
+ // Increased from 50 to 100
984
+ truncated: items.length > 100
820
985
  });
821
986
  }
822
987
  case "scan_folder": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anraktech/sync",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "AnrakLegal desktop file sync agent — watches local folders and syncs to case management",
5
5
  "type": "module",
6
6
  "bin": {