@anraktech/sync 0.3.0 → 0.5.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 +122 -236
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { createInterface as
|
|
6
|
-
import { stdin as
|
|
5
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
6
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
7
7
|
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
8
|
-
import { resolve as
|
|
9
|
-
import
|
|
8
|
+
import { resolve as resolve3 } from "path";
|
|
9
|
+
import chalk3 from "chalk";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -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
|
}
|
|
@@ -417,10 +417,7 @@ function resetCache() {
|
|
|
417
417
|
// src/watcher.ts
|
|
418
418
|
import { watch } from "chokidar";
|
|
419
419
|
import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
|
|
420
|
-
import { join as
|
|
421
|
-
import { createInterface as createInterface2 } from "readline/promises";
|
|
422
|
-
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
423
|
-
import chalk3 from "chalk";
|
|
420
|
+
import { join as join3, relative as relative2, basename as basename4, resolve as resolve2 } from "path";
|
|
424
421
|
|
|
425
422
|
// src/uploader.ts
|
|
426
423
|
import { stat as stat2 } from "fs/promises";
|
|
@@ -622,19 +619,50 @@ function sleep(ms) {
|
|
|
622
619
|
// src/agent.ts
|
|
623
620
|
import { createInterface } from "readline/promises";
|
|
624
621
|
import { stdin, stdout } from "process";
|
|
622
|
+
import { homedir as homedir2, platform } from "os";
|
|
623
|
+
import { resolve, join as join2 } from "path";
|
|
625
624
|
import chalk2 from "chalk";
|
|
625
|
+
var HOME = homedir2();
|
|
626
|
+
var IS_MAC = platform() === "darwin";
|
|
627
|
+
var FOLDER_SHORTCUTS = {
|
|
628
|
+
downloads: join2(HOME, "Downloads"),
|
|
629
|
+
download: join2(HOME, "Downloads"),
|
|
630
|
+
desktop: join2(HOME, "Desktop"),
|
|
631
|
+
documents: join2(HOME, "Documents"),
|
|
632
|
+
document: join2(HOME, "Documents"),
|
|
633
|
+
home: HOME,
|
|
634
|
+
"~": HOME
|
|
635
|
+
};
|
|
636
|
+
function normalizePath(folderPath) {
|
|
637
|
+
const trimmed = folderPath.trim();
|
|
638
|
+
if (trimmed.startsWith("~/") || trimmed === "~") {
|
|
639
|
+
return resolve(trimmed.replace(/^~/, HOME));
|
|
640
|
+
}
|
|
641
|
+
const lower = trimmed.toLowerCase();
|
|
642
|
+
if (FOLDER_SHORTCUTS[lower]) {
|
|
643
|
+
return FOLDER_SHORTCUTS[lower];
|
|
644
|
+
}
|
|
645
|
+
const withoutSlash = lower.replace(/^\//, "");
|
|
646
|
+
if (FOLDER_SHORTCUTS[withoutSlash]) {
|
|
647
|
+
return FOLDER_SHORTCUTS[withoutSlash];
|
|
648
|
+
}
|
|
649
|
+
if (trimmed.startsWith("/") || /^[A-Z]:\\/i.test(trimmed)) {
|
|
650
|
+
return resolve(trimmed);
|
|
651
|
+
}
|
|
652
|
+
return resolve(HOME, trimmed);
|
|
653
|
+
}
|
|
626
654
|
var TOOLS = [
|
|
627
655
|
{
|
|
628
656
|
type: "function",
|
|
629
657
|
function: {
|
|
630
658
|
name: "scan_folder",
|
|
631
|
-
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
|
|
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.",
|
|
632
660
|
parameters: {
|
|
633
661
|
type: "object",
|
|
634
662
|
properties: {
|
|
635
663
|
folderPath: {
|
|
636
664
|
type: "string",
|
|
637
|
-
description: "Absolute path to the folder
|
|
665
|
+
description: "Absolute path to the folder, e.g. /Users/name/Downloads"
|
|
638
666
|
}
|
|
639
667
|
},
|
|
640
668
|
required: ["folderPath"]
|
|
@@ -645,7 +673,7 @@ var TOOLS = [
|
|
|
645
673
|
type: "function",
|
|
646
674
|
function: {
|
|
647
675
|
name: "list_cases",
|
|
648
|
-
description: "List all legal cases on the
|
|
676
|
+
description: "List all legal cases on the server with case numbers, names, and document counts.",
|
|
649
677
|
parameters: { type: "object", properties: {} }
|
|
650
678
|
}
|
|
651
679
|
},
|
|
@@ -653,7 +681,7 @@ var TOOLS = [
|
|
|
653
681
|
type: "function",
|
|
654
682
|
function: {
|
|
655
683
|
name: "get_status",
|
|
656
|
-
description: "Show sync
|
|
684
|
+
description: "Show sync stats: files tracked, synced, pending, errors, queue size.",
|
|
657
685
|
parameters: { type: "object", properties: {} }
|
|
658
686
|
}
|
|
659
687
|
},
|
|
@@ -661,7 +689,7 @@ var TOOLS = [
|
|
|
661
689
|
type: "function",
|
|
662
690
|
function: {
|
|
663
691
|
name: "get_mappings",
|
|
664
|
-
description: "Show
|
|
692
|
+
description: "Show which local folders are mapped to which legal cases.",
|
|
665
693
|
parameters: { type: "object", properties: {} }
|
|
666
694
|
}
|
|
667
695
|
},
|
|
@@ -669,7 +697,7 @@ var TOOLS = [
|
|
|
669
697
|
type: "function",
|
|
670
698
|
function: {
|
|
671
699
|
name: "refresh_cases",
|
|
672
|
-
description: "Refresh the case list from the server
|
|
700
|
+
description: "Refresh the case list from the server.",
|
|
673
701
|
parameters: { type: "object", properties: {} }
|
|
674
702
|
}
|
|
675
703
|
},
|
|
@@ -677,37 +705,39 @@ var TOOLS = [
|
|
|
677
705
|
type: "function",
|
|
678
706
|
function: {
|
|
679
707
|
name: "rescan_watch_folder",
|
|
680
|
-
description: "Re-scan the
|
|
708
|
+
description: "Re-scan the watch folder for new or changed files and sync them.",
|
|
681
709
|
parameters: { type: "object", properties: {} }
|
|
682
710
|
}
|
|
683
711
|
}
|
|
684
712
|
];
|
|
685
713
|
function buildSystemPrompt(config) {
|
|
686
|
-
return `You are AnrakLegal Sync
|
|
687
|
-
You help lawyers sync local files to their AnrakLegal case management system.
|
|
714
|
+
return `You are AnrakLegal Sync, a terminal assistant that helps lawyers sync local files to their case management system.
|
|
688
715
|
|
|
689
|
-
|
|
716
|
+
Watch folder: ${config.watchFolder}
|
|
690
717
|
Server: ${config.apiUrl}
|
|
718
|
+
Home directory: ${HOME}
|
|
719
|
+
Platform: ${IS_MAC ? "macOS" : platform()}
|
|
691
720
|
|
|
692
|
-
You can
|
|
693
|
-
- Scan any folder on the user's computer and sync files to cases
|
|
694
|
-
- List cases on the server
|
|
695
|
-
- Show sync status and folder-to-case mappings
|
|
696
|
-
- Re-scan the watch folder for changes
|
|
697
|
-
- Refresh the case list
|
|
698
|
-
|
|
699
|
-
When the user asks you to do something, call the appropriate tool. After getting tool results, summarize them conversationally and concisely \u2014 this is a terminal, keep it brief.
|
|
721
|
+
You can scan folders, list cases, show sync status, and more using your tools.
|
|
700
722
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
Do NOT use markdown
|
|
723
|
+
Rules:
|
|
724
|
+
- Be concise. This is a terminal \u2014 1-3 lines unless showing a list.
|
|
725
|
+
- Do NOT use markdown. Plain text only.
|
|
726
|
+
- IMPORTANT: When user mentions a folder like "downloads" or "desktop", ALWAYS use the full absolute path. Examples:
|
|
727
|
+
"downloads" or "my downloads" \u2192 ${join2(HOME, "Downloads")}
|
|
728
|
+
"desktop" \u2192 ${join2(HOME, "Desktop")}
|
|
729
|
+
"documents" \u2192 ${join2(HOME, "Documents")}
|
|
730
|
+
"~/SomeFolder" \u2192 ${HOME}/SomeFolder
|
|
731
|
+
- Call tools to perform actions. Summarize results naturally after.
|
|
732
|
+
- If a tool returns an error, report it clearly to the user.
|
|
733
|
+
- Do NOT use thinking tags or reasoning tags in your output.`;
|
|
704
734
|
}
|
|
705
735
|
async function executeTool(name, args, ctx) {
|
|
706
736
|
switch (name) {
|
|
707
737
|
case "scan_folder": {
|
|
708
|
-
const
|
|
709
|
-
if (!
|
|
710
|
-
|
|
738
|
+
const rawPath = args.folderPath;
|
|
739
|
+
if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
|
|
740
|
+
const folderPath = normalizePath(rawPath);
|
|
711
741
|
try {
|
|
712
742
|
await ctx.scanFolder(folderPath);
|
|
713
743
|
const stats = getStats();
|
|
@@ -720,9 +750,7 @@ async function executeTool(name, args, ctx) {
|
|
|
720
750
|
errors: stats.errors
|
|
721
751
|
});
|
|
722
752
|
} catch (err) {
|
|
723
|
-
return JSON.stringify({
|
|
724
|
-
error: err instanceof Error ? err.message : String(err)
|
|
725
|
-
});
|
|
753
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
|
|
726
754
|
}
|
|
727
755
|
}
|
|
728
756
|
case "list_cases": {
|
|
@@ -753,13 +781,9 @@ async function executeTool(name, args, ctx) {
|
|
|
753
781
|
}
|
|
754
782
|
case "refresh_cases": {
|
|
755
783
|
await ctx.refreshCases();
|
|
756
|
-
return JSON.stringify({
|
|
757
|
-
success: true,
|
|
758
|
-
caseCount: ctx.getCases().length
|
|
759
|
-
});
|
|
784
|
+
return JSON.stringify({ success: true, caseCount: ctx.getCases().length });
|
|
760
785
|
}
|
|
761
786
|
case "rescan_watch_folder": {
|
|
762
|
-
log.dim(`Re-scanning ${ctx.config.watchFolder}...`);
|
|
763
787
|
await ctx.triggerScan();
|
|
764
788
|
const stats = getStats();
|
|
765
789
|
return JSON.stringify({
|
|
@@ -792,42 +816,31 @@ async function* parseSSE(response) {
|
|
|
792
816
|
const parsed = JSON.parse(data);
|
|
793
817
|
const choice = parsed.choices?.[0];
|
|
794
818
|
if (choice?.delta) {
|
|
795
|
-
yield {
|
|
796
|
-
delta: choice.delta,
|
|
797
|
-
finishReason: choice.finish_reason ?? null
|
|
798
|
-
};
|
|
819
|
+
yield { delta: choice.delta, finishReason: choice.finish_reason ?? null };
|
|
799
820
|
}
|
|
800
821
|
} catch {
|
|
801
822
|
}
|
|
802
823
|
}
|
|
803
824
|
}
|
|
804
825
|
}
|
|
805
|
-
var MODEL = "qwen/qwen3-8b";
|
|
806
826
|
async function agentTurn(messages, ctx) {
|
|
807
|
-
const
|
|
827
|
+
const token = await getAccessToken(ctx.config);
|
|
828
|
+
const response = await fetch(`${ctx.config.apiUrl}/api/sync/chat`, {
|
|
808
829
|
method: "POST",
|
|
809
830
|
headers: {
|
|
810
|
-
Authorization: `Bearer ${
|
|
811
|
-
"Content-Type": "application/json"
|
|
812
|
-
"HTTP-Referer": "https://anrak.legal",
|
|
813
|
-
"X-Title": "AnrakLegal Sync CLI"
|
|
831
|
+
Authorization: `Bearer ${token}`,
|
|
832
|
+
"Content-Type": "application/json"
|
|
814
833
|
},
|
|
815
|
-
body: JSON.stringify({
|
|
816
|
-
model: MODEL,
|
|
817
|
-
messages,
|
|
818
|
-
tools: TOOLS,
|
|
819
|
-
stream: true
|
|
820
|
-
})
|
|
834
|
+
body: JSON.stringify({ messages, tools: TOOLS })
|
|
821
835
|
});
|
|
822
836
|
if (!response.ok) {
|
|
823
837
|
const body = await response.text().catch(() => "");
|
|
824
|
-
throw new Error(`
|
|
838
|
+
throw new Error(`Server error (${response.status}): ${body.slice(0, 200)}`);
|
|
825
839
|
}
|
|
826
840
|
let textContent = "";
|
|
827
841
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
828
842
|
for await (const { delta } of parseSSE(response)) {
|
|
829
843
|
if (delta.content) {
|
|
830
|
-
process.stdout.write(delta.content);
|
|
831
844
|
textContent += delta.content;
|
|
832
845
|
}
|
|
833
846
|
if (delta.tool_calls) {
|
|
@@ -845,8 +858,10 @@ async function agentTurn(messages, ctx) {
|
|
|
845
858
|
}
|
|
846
859
|
}
|
|
847
860
|
}
|
|
861
|
+
textContent = textContent.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
848
862
|
if (textContent) {
|
|
849
|
-
process.stdout.write(
|
|
863
|
+
process.stdout.write(` ${textContent}
|
|
864
|
+
`);
|
|
850
865
|
}
|
|
851
866
|
if (toolCalls.size === 0) {
|
|
852
867
|
return textContent;
|
|
@@ -862,29 +877,33 @@ async function agentTurn(messages, ctx) {
|
|
|
862
877
|
};
|
|
863
878
|
messages.push(assistantMsg);
|
|
864
879
|
for (const tc of toolCalls.values()) {
|
|
880
|
+
process.stdout.write(chalk2.dim(` \u27F3 ${tc.name}...
|
|
881
|
+
`));
|
|
865
882
|
let args = {};
|
|
866
883
|
try {
|
|
867
884
|
args = JSON.parse(tc.args || "{}");
|
|
868
885
|
} catch {
|
|
869
886
|
}
|
|
870
887
|
const result = await executeTool(tc.name, args, ctx);
|
|
871
|
-
messages.push({
|
|
872
|
-
role: "tool",
|
|
873
|
-
content: result,
|
|
874
|
-
tool_call_id: tc.id
|
|
875
|
-
});
|
|
888
|
+
messages.push({ role: "tool", content: result, tool_call_id: tc.id });
|
|
876
889
|
}
|
|
877
890
|
return agentTurn(messages, ctx);
|
|
878
891
|
}
|
|
879
892
|
function startAIAgent(ctx) {
|
|
880
|
-
const PROMPT = chalk2.blue("anrak> ");
|
|
881
893
|
const history = [
|
|
882
894
|
{ role: "system", content: buildSystemPrompt(ctx.config) }
|
|
883
895
|
];
|
|
884
896
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
885
897
|
console.log("");
|
|
886
|
-
log.
|
|
887
|
-
log.dim(
|
|
898
|
+
console.log(chalk2.bold(" AnrakLegal Sync"));
|
|
899
|
+
console.log(chalk2.dim(` Watching ${ctx.config.watchFolder}`));
|
|
900
|
+
console.log(chalk2.dim(` Connected to ${ctx.config.apiUrl}`));
|
|
901
|
+
console.log("");
|
|
902
|
+
console.log(
|
|
903
|
+
chalk2.dim(" Ask me anything \u2014 scan folders, check cases, show status.")
|
|
904
|
+
);
|
|
905
|
+
console.log(chalk2.dim(' Type "quit" to exit.\n'));
|
|
906
|
+
const PROMPT = `${chalk2.bold.blue("\u276F")} `;
|
|
888
907
|
async function promptLoop() {
|
|
889
908
|
while (true) {
|
|
890
909
|
let input;
|
|
@@ -901,7 +920,7 @@ function startAIAgent(ctx) {
|
|
|
901
920
|
}
|
|
902
921
|
if (/^(clear|reset|new)$/i.test(trimmed)) {
|
|
903
922
|
history.length = 1;
|
|
904
|
-
log.
|
|
923
|
+
console.log(chalk2.dim(" Conversation cleared.\n"));
|
|
905
924
|
continue;
|
|
906
925
|
}
|
|
907
926
|
history.push({ role: "user", content: trimmed });
|
|
@@ -915,10 +934,7 @@ function startAIAgent(ctx) {
|
|
|
915
934
|
}
|
|
916
935
|
} catch (err) {
|
|
917
936
|
const msg = err instanceof Error ? err.message : String(err);
|
|
918
|
-
log.
|
|
919
|
-
if (msg.includes("401") || msg.includes("403")) {
|
|
920
|
-
log.warn("Check your OpenRouter API key (OPENROUTER_API_KEY env var)");
|
|
921
|
-
}
|
|
937
|
+
console.log(chalk2.red(` Error: ${msg}`));
|
|
922
938
|
}
|
|
923
939
|
console.log("");
|
|
924
940
|
}
|
|
@@ -940,7 +956,7 @@ async function scanFolder(config) {
|
|
|
940
956
|
}
|
|
941
957
|
for (const entry of entries) {
|
|
942
958
|
if (isIgnoredFile(entry)) continue;
|
|
943
|
-
const fullPath =
|
|
959
|
+
const fullPath = join3(dir, entry);
|
|
944
960
|
let s;
|
|
945
961
|
try {
|
|
946
962
|
s = statSync(fullPath);
|
|
@@ -975,7 +991,7 @@ async function pushSync(config) {
|
|
|
975
991
|
);
|
|
976
992
|
}
|
|
977
993
|
async function scanExternalFolder(config, folderPath, cases) {
|
|
978
|
-
const absPath =
|
|
994
|
+
const absPath = resolve2(folderPath);
|
|
979
995
|
if (!existsSync2(absPath)) {
|
|
980
996
|
log.error(`Folder not found: ${absPath}`);
|
|
981
997
|
return;
|
|
@@ -995,7 +1011,7 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
995
1011
|
}
|
|
996
1012
|
for (const entry of entries) {
|
|
997
1013
|
if (isIgnoredFile(entry)) continue;
|
|
998
|
-
const fullPath =
|
|
1014
|
+
const fullPath = join3(dir, entry);
|
|
999
1015
|
let s;
|
|
1000
1016
|
try {
|
|
1001
1017
|
s = statSync(fullPath);
|
|
@@ -1006,7 +1022,7 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
1006
1022
|
walk(fullPath);
|
|
1007
1023
|
} else if (s.isFile() && isSupportedFile(entry)) {
|
|
1008
1024
|
fileCount++;
|
|
1009
|
-
void enqueue(fullPath,
|
|
1025
|
+
void enqueue(fullPath, join3(absPath, ".."));
|
|
1010
1026
|
}
|
|
1011
1027
|
}
|
|
1012
1028
|
}
|
|
@@ -1021,129 +1037,6 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
1021
1037
|
log.success("Everything up to date");
|
|
1022
1038
|
}
|
|
1023
1039
|
}
|
|
1024
|
-
function startInteractiveAgent(config, getCases, refreshCases, triggerScan) {
|
|
1025
|
-
const PROMPT = chalk3.blue("anrak> ");
|
|
1026
|
-
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1027
|
-
function showHelp() {
|
|
1028
|
-
console.log("");
|
|
1029
|
-
console.log(chalk3.bold(" Available commands:"));
|
|
1030
|
-
console.log("");
|
|
1031
|
-
console.log(` ${chalk3.cyan("scan <folder>")} Scan a folder and sync its files to a case`);
|
|
1032
|
-
console.log(` ${chalk3.cyan("rescan")} Re-scan the watch folder for new files`);
|
|
1033
|
-
console.log(` ${chalk3.cyan("cases")} List cases on the server`);
|
|
1034
|
-
console.log(` ${chalk3.cyan("status")} Show sync statistics`);
|
|
1035
|
-
console.log(` ${chalk3.cyan("mappings")} Show folder \u2192 case mappings`);
|
|
1036
|
-
console.log(` ${chalk3.cyan("refresh")} Refresh cases from server`);
|
|
1037
|
-
console.log(` ${chalk3.cyan("help")} Show this help`);
|
|
1038
|
-
console.log(` ${chalk3.cyan("quit")} Stop syncing and exit`);
|
|
1039
|
-
console.log("");
|
|
1040
|
-
}
|
|
1041
|
-
async function handleCommand(input) {
|
|
1042
|
-
const trimmed = input.trim();
|
|
1043
|
-
if (!trimmed) return;
|
|
1044
|
-
const [cmd, ...args] = trimmed.split(/\s+/);
|
|
1045
|
-
const arg = args.join(" ");
|
|
1046
|
-
switch (cmd.toLowerCase()) {
|
|
1047
|
-
case "scan": {
|
|
1048
|
-
if (!arg) {
|
|
1049
|
-
log.warn("Usage: scan <folder path>");
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
await scanExternalFolder(config, arg, getCases());
|
|
1053
|
-
break;
|
|
1054
|
-
}
|
|
1055
|
-
case "rescan": {
|
|
1056
|
-
await triggerScan();
|
|
1057
|
-
break;
|
|
1058
|
-
}
|
|
1059
|
-
case "cases": {
|
|
1060
|
-
await refreshCases();
|
|
1061
|
-
const cases = getCases();
|
|
1062
|
-
if (cases.length === 0) {
|
|
1063
|
-
log.info("No cases found on server");
|
|
1064
|
-
} else {
|
|
1065
|
-
console.log("");
|
|
1066
|
-
for (const c of cases) {
|
|
1067
|
-
const docCount = c.documents?.length ?? 0;
|
|
1068
|
-
console.log(
|
|
1069
|
-
` ${chalk3.cyan(c.caseNumber)} ${c.caseName} ${chalk3.dim(`(${docCount} docs)`)}`
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
console.log("");
|
|
1073
|
-
}
|
|
1074
|
-
break;
|
|
1075
|
-
}
|
|
1076
|
-
case "status": {
|
|
1077
|
-
const stats = getStats();
|
|
1078
|
-
console.log("");
|
|
1079
|
-
console.log(` ${chalk3.bold("Watch folder:")} ${config.watchFolder}`);
|
|
1080
|
-
console.log(` ${chalk3.bold("Files tracked:")} ${stats.totalFiles}`);
|
|
1081
|
-
console.log(` ${chalk3.bold("Synced:")} ${chalk3.green(String(stats.synced))}`);
|
|
1082
|
-
console.log(` ${chalk3.bold("Pending:")} ${chalk3.yellow(String(stats.pending))}`);
|
|
1083
|
-
console.log(` ${chalk3.bold("Errors:")} ${chalk3.red(String(stats.errors))}`);
|
|
1084
|
-
console.log(` ${chalk3.bold("Mapped folders:")} ${stats.mappedFolders}`);
|
|
1085
|
-
console.log(` ${chalk3.bold("Queue:")} ${queueSize()}`);
|
|
1086
|
-
console.log("");
|
|
1087
|
-
break;
|
|
1088
|
-
}
|
|
1089
|
-
case "mappings":
|
|
1090
|
-
case "map": {
|
|
1091
|
-
const mappings = getAllMappings();
|
|
1092
|
-
const entries = Object.entries(mappings);
|
|
1093
|
-
if (entries.length === 0) {
|
|
1094
|
-
log.info("No folder mappings yet");
|
|
1095
|
-
} else {
|
|
1096
|
-
console.log("");
|
|
1097
|
-
for (const [folder, m] of entries) {
|
|
1098
|
-
console.log(
|
|
1099
|
-
` ${chalk3.cyan(folder)} \u2192 ${m.caseNumber} ${chalk3.dim(`(${m.caseName})`)}`
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
console.log("");
|
|
1103
|
-
}
|
|
1104
|
-
break;
|
|
1105
|
-
}
|
|
1106
|
-
case "refresh": {
|
|
1107
|
-
log.info("Refreshing cases from server...");
|
|
1108
|
-
await refreshCases();
|
|
1109
|
-
log.success(`Found ${getCases().length} case(s)`);
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
case "help":
|
|
1113
|
-
case "?": {
|
|
1114
|
-
showHelp();
|
|
1115
|
-
break;
|
|
1116
|
-
}
|
|
1117
|
-
case "quit":
|
|
1118
|
-
case "exit":
|
|
1119
|
-
case "q": {
|
|
1120
|
-
process.emit("SIGINT");
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
default: {
|
|
1124
|
-
log.warn(`Unknown command: ${cmd}. Type ${chalk3.cyan("help")} for available commands.`);
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
showHelp();
|
|
1130
|
-
async function promptLoop() {
|
|
1131
|
-
while (true) {
|
|
1132
|
-
let input;
|
|
1133
|
-
try {
|
|
1134
|
-
input = await rl.question(PROMPT);
|
|
1135
|
-
} catch {
|
|
1136
|
-
break;
|
|
1137
|
-
}
|
|
1138
|
-
try {
|
|
1139
|
-
await handleCommand(input);
|
|
1140
|
-
} catch (err) {
|
|
1141
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
void promptLoop();
|
|
1146
|
-
}
|
|
1147
1040
|
async function startWatching(config) {
|
|
1148
1041
|
const folder = config.watchFolder;
|
|
1149
1042
|
log.info(`Scanning ${folder}...`);
|
|
@@ -1211,8 +1104,7 @@ async function startWatching(config) {
|
|
|
1211
1104
|
watcher.on("error", (err) => {
|
|
1212
1105
|
log.error(`Watcher error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1213
1106
|
});
|
|
1214
|
-
|
|
1215
|
-
const agentContext = {
|
|
1107
|
+
startAIAgent({
|
|
1216
1108
|
config,
|
|
1217
1109
|
getCases: () => cases,
|
|
1218
1110
|
refreshCases: async () => {
|
|
@@ -1230,13 +1122,7 @@ async function startWatching(config) {
|
|
|
1230
1122
|
scanFolder: async (folderPath) => {
|
|
1231
1123
|
await scanExternalFolder(config, folderPath, cases);
|
|
1232
1124
|
}
|
|
1233
|
-
};
|
|
1234
|
-
if (apiKey) {
|
|
1235
|
-
startAIAgent({ ...agentContext, apiKey });
|
|
1236
|
-
} else {
|
|
1237
|
-
log.dim("Tip: Set OPENROUTER_API_KEY for AI-powered natural language mode\n");
|
|
1238
|
-
startInteractiveAgent(config, agentContext.getCases, agentContext.refreshCases, agentContext.triggerScan);
|
|
1239
|
-
}
|
|
1125
|
+
});
|
|
1240
1126
|
const shutdown = () => {
|
|
1241
1127
|
log.info("Shutting down...");
|
|
1242
1128
|
clearInterval(refreshInterval);
|
|
@@ -1253,18 +1139,18 @@ async function startWatching(config) {
|
|
|
1253
1139
|
// src/cli.ts
|
|
1254
1140
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1255
1141
|
import { fileURLToPath } from "url";
|
|
1256
|
-
import { dirname as dirname2, join as
|
|
1142
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1257
1143
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
1258
1144
|
var __dirname2 = dirname2(__filename2);
|
|
1259
|
-
var pkg = JSON.parse(readFileSync2(
|
|
1145
|
+
var pkg = JSON.parse(readFileSync2(join4(__dirname2, "..", "package.json"), "utf-8"));
|
|
1260
1146
|
var program = new Command();
|
|
1261
1147
|
program.name("anrak-sync").description("AnrakLegal desktop file sync \u2014 watches local folders, syncs to case management").version(pkg.version);
|
|
1262
1148
|
program.command("init").description("Set up AnrakLegal Sync (first-time configuration)").option("--password", "Use email/password login instead of browser").action(async (opts) => {
|
|
1263
|
-
const rl =
|
|
1149
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1264
1150
|
try {
|
|
1265
|
-
console.log(
|
|
1151
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Setup\n"));
|
|
1266
1152
|
const apiUrl = await rl.question(
|
|
1267
|
-
` AnrakLegal URL ${
|
|
1153
|
+
` AnrakLegal URL ${chalk3.dim("(https://anrak.legal)")}: `
|
|
1268
1154
|
) || "https://anrak.legal";
|
|
1269
1155
|
log.info("Connecting to server...");
|
|
1270
1156
|
const serverConfig = await fetchServerConfig(apiUrl);
|
|
@@ -1292,12 +1178,12 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
|
|
|
1292
1178
|
tokens = await browserLogin(apiUrl);
|
|
1293
1179
|
}
|
|
1294
1180
|
log.success("Authenticated");
|
|
1295
|
-
const rl2 =
|
|
1181
|
+
const rl2 = createInterface2({ input: stdin2, output: stdout2 });
|
|
1296
1182
|
const defaultFolder = process.platform === "win32" ? "C:\\Cases" : `${process.env.HOME}/Cases`;
|
|
1297
1183
|
const watchInput = await rl2.question(
|
|
1298
|
-
` Watch folder ${
|
|
1184
|
+
` Watch folder ${chalk3.dim(`(${defaultFolder})`)}: `
|
|
1299
1185
|
);
|
|
1300
|
-
const watchFolder =
|
|
1186
|
+
const watchFolder = resolve3(watchInput || defaultFolder);
|
|
1301
1187
|
rl2.close();
|
|
1302
1188
|
if (!existsSync3(watchFolder)) {
|
|
1303
1189
|
log.warn(
|
|
@@ -1321,7 +1207,7 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
|
|
|
1321
1207
|
log.info(`Config saved to ${getConfigDir()}`);
|
|
1322
1208
|
log.info(`Watching: ${watchFolder}`);
|
|
1323
1209
|
console.log(
|
|
1324
|
-
|
|
1210
|
+
chalk3.dim("\n Run `anrak-sync start` to begin syncing\n")
|
|
1325
1211
|
);
|
|
1326
1212
|
} catch (err) {
|
|
1327
1213
|
log.error(err instanceof Error ? err.message : String(err));
|
|
@@ -1333,7 +1219,7 @@ program.command("login").description("Re-authenticate with AnrakLegal").option("
|
|
|
1333
1219
|
try {
|
|
1334
1220
|
let tokens;
|
|
1335
1221
|
if (opts.password) {
|
|
1336
|
-
const rl =
|
|
1222
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
1337
1223
|
try {
|
|
1338
1224
|
const email = await rl.question(" Email: ");
|
|
1339
1225
|
const password = await rl.question(" Password: ");
|
|
@@ -1356,7 +1242,7 @@ program.command("login").description("Re-authenticate with AnrakLegal").option("
|
|
|
1356
1242
|
});
|
|
1357
1243
|
program.command("start").description("Start watching for file changes and syncing").action(async () => {
|
|
1358
1244
|
const config = requireConfig();
|
|
1359
|
-
console.log(
|
|
1245
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync\n"));
|
|
1360
1246
|
log.info(`Watching: ${config.watchFolder}`);
|
|
1361
1247
|
log.info(`Server: ${config.apiUrl}`);
|
|
1362
1248
|
console.log("");
|
|
@@ -1369,7 +1255,7 @@ program.command("start").description("Start watching for file changes and syncin
|
|
|
1369
1255
|
});
|
|
1370
1256
|
program.command("push").description("One-time sync \u2014 upload all new/changed files, then exit").action(async () => {
|
|
1371
1257
|
const config = requireConfig();
|
|
1372
|
-
console.log(
|
|
1258
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Push\n"));
|
|
1373
1259
|
log.info(`Folder: ${config.watchFolder}`);
|
|
1374
1260
|
log.info(`Server: ${config.apiUrl}`);
|
|
1375
1261
|
console.log("");
|
|
@@ -1383,24 +1269,24 @@ program.command("push").description("One-time sync \u2014 upload all new/changed
|
|
|
1383
1269
|
program.command("status").description("Show sync status").action(async () => {
|
|
1384
1270
|
const config = requireConfig();
|
|
1385
1271
|
const stats = getStats();
|
|
1386
|
-
console.log(
|
|
1272
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Status\n"));
|
|
1387
1273
|
console.log(` Server: ${config.apiUrl}`);
|
|
1388
1274
|
console.log(` Watch folder: ${config.watchFolder}`);
|
|
1389
1275
|
console.log(` Config: ${getConfigDir()}`);
|
|
1390
1276
|
console.log("");
|
|
1391
1277
|
console.log(` Files tracked: ${stats.totalFiles}`);
|
|
1392
|
-
console.log(` Synced: ${
|
|
1393
|
-
console.log(` Pending: ${
|
|
1394
|
-
console.log(` Errors: ${
|
|
1278
|
+
console.log(` Synced: ${chalk3.green(stats.synced)}`);
|
|
1279
|
+
console.log(` Pending: ${chalk3.yellow(stats.pending)}`);
|
|
1280
|
+
console.log(` Errors: ${chalk3.red(stats.errors)}`);
|
|
1395
1281
|
console.log(` Mapped folders: ${stats.mappedFolders}`);
|
|
1396
1282
|
try {
|
|
1397
1283
|
const cases = await listCases(config);
|
|
1398
1284
|
console.log(`
|
|
1399
1285
|
Server cases: ${cases.length}`);
|
|
1400
|
-
console.log(` Auth: ${
|
|
1286
|
+
console.log(` Auth: ${chalk3.green("valid")}`);
|
|
1401
1287
|
} catch {
|
|
1402
1288
|
console.log(`
|
|
1403
|
-
Auth: ${
|
|
1289
|
+
Auth: ${chalk3.red("expired \u2014 run anrak-sync login")}`);
|
|
1404
1290
|
}
|
|
1405
1291
|
console.log("");
|
|
1406
1292
|
});
|
|
@@ -1408,13 +1294,13 @@ program.command("map").description("Show folder-to-case mappings").action(async
|
|
|
1408
1294
|
const config = requireConfig();
|
|
1409
1295
|
const mappings = getAllMappings();
|
|
1410
1296
|
const entries = Object.entries(mappings);
|
|
1411
|
-
console.log(
|
|
1297
|
+
console.log(chalk3.bold.blue("\n AnrakLegal Sync \u2014 Mappings\n"));
|
|
1412
1298
|
if (entries.length === 0) {
|
|
1413
1299
|
log.info("No mappings yet. Run `anrak-sync push` or `anrak-sync start` to create them.");
|
|
1414
1300
|
} else {
|
|
1415
1301
|
for (const [folder, mapping] of entries) {
|
|
1416
1302
|
console.log(
|
|
1417
|
-
` ${
|
|
1303
|
+
` ${chalk3.cyan(folder)} -> ${mapping.caseNumber} (${chalk3.dim(mapping.caseName)})`
|
|
1418
1304
|
);
|
|
1419
1305
|
}
|
|
1420
1306
|
}
|
|
@@ -1423,9 +1309,9 @@ program.command("map").description("Show folder-to-case mappings").action(async
|
|
|
1423
1309
|
const mappedIds = new Set(entries.map(([, m]) => m.caseId));
|
|
1424
1310
|
const unmapped = cases.filter((c) => !mappedIds.has(c.id));
|
|
1425
1311
|
if (unmapped.length > 0) {
|
|
1426
|
-
console.log(
|
|
1312
|
+
console.log(chalk3.dim("\n Unmapped server cases:"));
|
|
1427
1313
|
for (const c of unmapped) {
|
|
1428
|
-
console.log(` ${
|
|
1314
|
+
console.log(` ${chalk3.dim(c.caseNumber)} ${chalk3.dim(c.caseName)}`);
|
|
1429
1315
|
}
|
|
1430
1316
|
}
|
|
1431
1317
|
} catch {
|