@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.
- package/dist/cli.js +149 -42
- 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
|
|
8
|
-
import { resolve as
|
|
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((
|
|
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(() =>
|
|
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
|
-
(
|
|
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
|
-
|
|
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((
|
|
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", () =>
|
|
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
|
|
420
|
-
import { join as
|
|
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: "
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
701
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
1034
|
+
const fullPath = join3(dir, entry);
|
|
928
1035
|
let s;
|
|
929
1036
|
try {
|
|
930
|
-
s =
|
|
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 =
|
|
963
|
-
if (!
|
|
1069
|
+
const absPath = resolve2(folderPath);
|
|
1070
|
+
if (!existsSync3(absPath)) {
|
|
964
1071
|
log.error(`Folder not found: ${absPath}`);
|
|
965
1072
|
return;
|
|
966
1073
|
}
|
|
967
|
-
if (!
|
|
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 =
|
|
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 =
|
|
1089
|
+
const fullPath = join3(dir, entry);
|
|
983
1090
|
let s;
|
|
984
1091
|
try {
|
|
985
|
-
s =
|
|
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,
|
|
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
|
|
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(
|
|
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 =
|
|
1261
|
+
const watchFolder = resolve3(watchInput || defaultFolder);
|
|
1155
1262
|
rl2.close();
|
|
1156
|
-
if (!
|
|
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 (!
|
|
1267
|
+
} else if (!statSync3(watchFolder).isDirectory()) {
|
|
1161
1268
|
log.error(`${watchFolder} is not a directory`);
|
|
1162
1269
|
process.exit(1);
|
|
1163
1270
|
}
|