@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.
- package/dist/cli.js +173 -8
- 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
|
|
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
|
|
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
|
|
765
|
-
- When user
|
|
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,
|
|
818
|
-
//
|
|
819
|
-
truncated: items.length >
|
|
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": {
|