@anraktech/sync 0.13.1 → 0.15.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 +387 -81
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
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 {
|
|
8
|
-
import { resolve as
|
|
7
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
8
|
+
import { resolve as resolve4 } from "path";
|
|
9
|
+
import { homedir as homedir3 } from "os";
|
|
9
10
|
import chalk4 from "chalk";
|
|
10
11
|
|
|
11
12
|
// src/config.ts
|
|
12
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
13
14
|
import { homedir } from "os";
|
|
14
|
-
import { join } from "path";
|
|
15
|
+
import { join, resolve } from "path";
|
|
15
16
|
|
|
16
17
|
// src/logger.ts
|
|
17
18
|
import chalk from "chalk";
|
|
@@ -31,11 +32,29 @@ var log = {
|
|
|
31
32
|
var CONFIG_DIR = join(homedir(), ".anrak-sync");
|
|
32
33
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
33
34
|
var CACHE_FILE = join(CONFIG_DIR, "cache.json");
|
|
35
|
+
var HOME = homedir();
|
|
36
|
+
var SHORTCUTS = {
|
|
37
|
+
downloads: join(HOME, "Downloads"),
|
|
38
|
+
download: join(HOME, "Downloads"),
|
|
39
|
+
desktop: join(HOME, "Desktop"),
|
|
40
|
+
documents: join(HOME, "Documents"),
|
|
41
|
+
document: join(HOME, "Documents")
|
|
42
|
+
};
|
|
43
|
+
function normalizePath(p) {
|
|
44
|
+
if (!p) return p;
|
|
45
|
+
if (p.startsWith("~/") || p === "~") return resolve(p.replace(/^~/, HOME));
|
|
46
|
+
const lower = p.toLowerCase().replace(/^\//, "");
|
|
47
|
+
if (SHORTCUTS[lower]) return SHORTCUTS[lower];
|
|
48
|
+
if (p.startsWith("/") && !existsSync(p) && SHORTCUTS[p.slice(1).toLowerCase()]) {
|
|
49
|
+
return SHORTCUTS[p.slice(1).toLowerCase()];
|
|
50
|
+
}
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
34
53
|
function getWatchFolders(config) {
|
|
35
54
|
const folders = /* @__PURE__ */ new Set();
|
|
36
|
-
if (config.watchFolder) folders.add(config.watchFolder);
|
|
55
|
+
if (config.watchFolder) folders.add(normalizePath(config.watchFolder));
|
|
37
56
|
if (config.watchFolders) {
|
|
38
|
-
for (const f of config.watchFolders) folders.add(f);
|
|
57
|
+
for (const f of config.watchFolders) folders.add(normalizePath(f));
|
|
39
58
|
}
|
|
40
59
|
return [...folders];
|
|
41
60
|
}
|
|
@@ -115,7 +134,7 @@ function openBrowser(url) {
|
|
|
115
134
|
exec(`${cmd} "${url}"`);
|
|
116
135
|
}
|
|
117
136
|
function findFreePort() {
|
|
118
|
-
return new Promise((
|
|
137
|
+
return new Promise((resolve5, reject) => {
|
|
119
138
|
const srv = createServer();
|
|
120
139
|
srv.listen(0, () => {
|
|
121
140
|
const addr = srv.address();
|
|
@@ -125,14 +144,14 @@ function findFreePort() {
|
|
|
125
144
|
return;
|
|
126
145
|
}
|
|
127
146
|
const port = addr.port;
|
|
128
|
-
srv.close(() =>
|
|
147
|
+
srv.close(() => resolve5(port));
|
|
129
148
|
});
|
|
130
149
|
});
|
|
131
150
|
}
|
|
132
151
|
async function browserLogin(apiUrl) {
|
|
133
152
|
const port = await findFreePort();
|
|
134
153
|
return new Promise(
|
|
135
|
-
(
|
|
154
|
+
(resolve5, reject) => {
|
|
136
155
|
let server;
|
|
137
156
|
const timeout = setTimeout(() => {
|
|
138
157
|
server?.close();
|
|
@@ -179,7 +198,7 @@ async function browserLogin(apiUrl) {
|
|
|
179
198
|
const tokens = await resp.json();
|
|
180
199
|
clearTimeout(timeout);
|
|
181
200
|
server.close();
|
|
182
|
-
|
|
201
|
+
resolve5(tokens);
|
|
183
202
|
} catch (err) {
|
|
184
203
|
clearTimeout(timeout);
|
|
185
204
|
server.close();
|
|
@@ -367,11 +386,11 @@ function persist() {
|
|
|
367
386
|
if (cache) saveCache(cache);
|
|
368
387
|
}
|
|
369
388
|
function hashFile(filePath) {
|
|
370
|
-
return new Promise((
|
|
389
|
+
return new Promise((resolve5, reject) => {
|
|
371
390
|
const hash = createHash("sha256");
|
|
372
391
|
const stream = createReadStream(filePath);
|
|
373
392
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
374
|
-
stream.on("end", () =>
|
|
393
|
+
stream.on("end", () => resolve5(hash.digest("hex")));
|
|
375
394
|
stream.on("error", reject);
|
|
376
395
|
});
|
|
377
396
|
}
|
|
@@ -446,7 +465,7 @@ function resetCache() {
|
|
|
446
465
|
// src/watcher.ts
|
|
447
466
|
import { watch } from "chokidar";
|
|
448
467
|
import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync3 } from "fs";
|
|
449
|
-
import { join as join3, relative as relative2, basename as basename5, resolve as
|
|
468
|
+
import { join as join3, relative as relative2, basename as basename5, resolve as resolve3 } from "path";
|
|
450
469
|
|
|
451
470
|
// src/uploader.ts
|
|
452
471
|
import { stat as stat2 } from "fs/promises";
|
|
@@ -454,6 +473,40 @@ import { basename as basename3, dirname, relative } from "path";
|
|
|
454
473
|
|
|
455
474
|
// src/mapper.ts
|
|
456
475
|
import { basename as basename2 } from "path";
|
|
476
|
+
|
|
477
|
+
// src/events.ts
|
|
478
|
+
var pendingFiles = [];
|
|
479
|
+
var recentMappings = [];
|
|
480
|
+
var MAX_RECENT = 20;
|
|
481
|
+
function addRecentMapping(msg) {
|
|
482
|
+
recentMappings.push(msg);
|
|
483
|
+
if (recentMappings.length > MAX_RECENT) {
|
|
484
|
+
recentMappings.shift();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function addPendingFile(file) {
|
|
488
|
+
if (!pendingFiles.some((f) => f.filePath === file.filePath)) {
|
|
489
|
+
pendingFiles.push(file);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function removePendingFile(filePath) {
|
|
493
|
+
const idx = pendingFiles.findIndex((f) => f.filePath === filePath);
|
|
494
|
+
if (idx >= 0) {
|
|
495
|
+
return pendingFiles.splice(idx, 1)[0];
|
|
496
|
+
}
|
|
497
|
+
return void 0;
|
|
498
|
+
}
|
|
499
|
+
function removePendingByFolder(folderName) {
|
|
500
|
+
const removed = [];
|
|
501
|
+
for (let i = pendingFiles.length - 1; i >= 0; i--) {
|
|
502
|
+
if (pendingFiles[i].folderName === folderName) {
|
|
503
|
+
removed.push(pendingFiles.splice(i, 1)[0]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return removed;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/mapper.ts
|
|
457
510
|
function normalize(s) {
|
|
458
511
|
return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
459
512
|
}
|
|
@@ -506,7 +559,7 @@ function deriveCaseInfo(folderName) {
|
|
|
506
559
|
caseName: sanitized
|
|
507
560
|
};
|
|
508
561
|
}
|
|
509
|
-
async function resolveFolder(config, folderPath, cases) {
|
|
562
|
+
async function resolveFolder(config, folderPath, cases, options) {
|
|
510
563
|
const folderName = basename2(folderPath);
|
|
511
564
|
const cached = getFolderMapping(folderName);
|
|
512
565
|
if (cached) {
|
|
@@ -522,6 +575,7 @@ async function resolveFolder(config, folderPath, cases) {
|
|
|
522
575
|
if (match) {
|
|
523
576
|
log.success(`Mapped "${folderName}" -> ${match.caseNumber} (${match.caseName})`);
|
|
524
577
|
setFolderMapping(folderName, match.id, match.caseName, match.caseNumber);
|
|
578
|
+
addRecentMapping(`Mapped "${folderName}" \u2192 ${match.caseNumber} (${match.caseName})`);
|
|
525
579
|
return {
|
|
526
580
|
caseId: match.id,
|
|
527
581
|
caseName: match.caseName,
|
|
@@ -529,12 +583,63 @@ async function resolveFolder(config, folderPath, cases) {
|
|
|
529
583
|
created: false
|
|
530
584
|
};
|
|
531
585
|
}
|
|
586
|
+
if (options?.smartMapper && allCases.length > 0) {
|
|
587
|
+
try {
|
|
588
|
+
const smartResult = await options.smartMapper(
|
|
589
|
+
folderName,
|
|
590
|
+
options.fileName || folderName,
|
|
591
|
+
allCases
|
|
592
|
+
);
|
|
593
|
+
if (smartResult) {
|
|
594
|
+
if (smartResult.confidence === "high") {
|
|
595
|
+
log.success(`AI mapped "${folderName}" \u2192 ${smartResult.caseNumber} (${smartResult.reason})`);
|
|
596
|
+
setFolderMapping(folderName, smartResult.caseId, smartResult.caseName, smartResult.caseNumber);
|
|
597
|
+
addRecentMapping(
|
|
598
|
+
`AI mapped "${options.fileName || folderName}" \u2192 ${smartResult.caseNumber} (${smartResult.caseName})`
|
|
599
|
+
);
|
|
600
|
+
return {
|
|
601
|
+
caseId: smartResult.caseId,
|
|
602
|
+
caseName: smartResult.caseName,
|
|
603
|
+
caseNumber: smartResult.caseNumber,
|
|
604
|
+
created: false
|
|
605
|
+
};
|
|
606
|
+
} else {
|
|
607
|
+
log.info(
|
|
608
|
+
`AI unsure about "${folderName}" \u2014 suggests ${smartResult.caseNumber} (${smartResult.reason})`
|
|
609
|
+
);
|
|
610
|
+
addPendingFile({
|
|
611
|
+
filePath: "",
|
|
612
|
+
// Set by uploader
|
|
613
|
+
folderName,
|
|
614
|
+
suggestion: {
|
|
615
|
+
caseName: smartResult.caseName,
|
|
616
|
+
caseNumber: smartResult.caseNumber,
|
|
617
|
+
reason: smartResult.reason
|
|
618
|
+
},
|
|
619
|
+
addedAt: Date.now()
|
|
620
|
+
});
|
|
621
|
+
addRecentMapping(
|
|
622
|
+
`Unsure: "${options.fileName || folderName}" in "${folderName}" \u2014 AI suggests ${smartResult.caseNumber} but needs confirmation`
|
|
623
|
+
);
|
|
624
|
+
return {
|
|
625
|
+
caseId: "",
|
|
626
|
+
caseName: "",
|
|
627
|
+
caseNumber: "",
|
|
628
|
+
created: false,
|
|
629
|
+
needsInput: true
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
532
636
|
const { caseNumber, caseName } = deriveCaseInfo(folderName);
|
|
533
637
|
log.info(`No match for "${folderName}" \u2014 creating case ${caseNumber}`);
|
|
534
638
|
try {
|
|
535
639
|
const newCase = await createCase(config, caseNumber, caseName);
|
|
536
640
|
setFolderMapping(folderName, newCase.id, newCase.caseName, newCase.caseNumber);
|
|
537
641
|
log.success(`Created case ${newCase.caseNumber} (${newCase.caseName})`);
|
|
642
|
+
addRecentMapping(`Created new case "${newCase.caseNumber}" for folder "${folderName}"`);
|
|
538
643
|
return {
|
|
539
644
|
caseId: newCase.id,
|
|
540
645
|
caseName: newCase.caseName,
|
|
@@ -583,7 +688,7 @@ async function enqueue(filePath, watchFolder) {
|
|
|
583
688
|
queue.push({ filePath, folderPath, retries: 0 });
|
|
584
689
|
log.file("queued", relative(watchFolder, filePath));
|
|
585
690
|
}
|
|
586
|
-
async function processQueue(config, cases) {
|
|
691
|
+
async function processQueue(config, cases, options) {
|
|
587
692
|
if (processing) return { uploaded: 0, failed: 0 };
|
|
588
693
|
processing = true;
|
|
589
694
|
let uploaded = 0;
|
|
@@ -596,11 +701,29 @@ async function processQueue(config, cases) {
|
|
|
596
701
|
await sleep(RATE_LIMIT_INTERVAL_MS - elapsed);
|
|
597
702
|
}
|
|
598
703
|
try {
|
|
599
|
-
const
|
|
704
|
+
const resolved = await resolveFolder(
|
|
600
705
|
config,
|
|
601
706
|
item.folderPath,
|
|
602
|
-
cases
|
|
707
|
+
cases,
|
|
708
|
+
{ smartMapper: options?.smartMapper, fileName: filename }
|
|
603
709
|
);
|
|
710
|
+
if (resolved.needsInput) {
|
|
711
|
+
const pending = pendingFiles.find(
|
|
712
|
+
(p) => p.folderName === basename3(item.folderPath) && !p.filePath
|
|
713
|
+
);
|
|
714
|
+
if (pending) {
|
|
715
|
+
pending.filePath = item.filePath;
|
|
716
|
+
} else {
|
|
717
|
+
addPendingFile({
|
|
718
|
+
filePath: item.filePath,
|
|
719
|
+
folderName: basename3(item.folderPath),
|
|
720
|
+
addedAt: Date.now()
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
log.info(`Waiting for mapping: ${filename}`);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const { caseId, caseName } = resolved;
|
|
604
727
|
const hash = await hashFile(item.filePath);
|
|
605
728
|
const s = await stat2(item.filePath);
|
|
606
729
|
markPending(item.filePath, hash, s.size, caseId);
|
|
@@ -649,7 +772,7 @@ function sleep(ms) {
|
|
|
649
772
|
import { createInterface } from "readline/promises";
|
|
650
773
|
import { stdin, stdout } from "process";
|
|
651
774
|
import { homedir as homedir2, platform } from "os";
|
|
652
|
-
import { resolve, join as join2, dirname as dirname2, basename as basename4 } from "path";
|
|
775
|
+
import { resolve as resolve2, join as join2, dirname as dirname2, basename as basename4 } from "path";
|
|
653
776
|
import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
|
|
654
777
|
import { fileURLToPath } from "url";
|
|
655
778
|
import chalk2 from "chalk";
|
|
@@ -663,21 +786,21 @@ var PKG_VERSION = (() => {
|
|
|
663
786
|
return "0.0.0";
|
|
664
787
|
}
|
|
665
788
|
})();
|
|
666
|
-
var
|
|
789
|
+
var HOME2 = homedir2();
|
|
667
790
|
var IS_MAC = platform() === "darwin";
|
|
668
791
|
var FOLDER_SHORTCUTS = {
|
|
669
|
-
downloads: join2(
|
|
670
|
-
download: join2(
|
|
671
|
-
desktop: join2(
|
|
672
|
-
documents: join2(
|
|
673
|
-
document: join2(
|
|
674
|
-
home:
|
|
675
|
-
"~":
|
|
792
|
+
downloads: join2(HOME2, "Downloads"),
|
|
793
|
+
download: join2(HOME2, "Downloads"),
|
|
794
|
+
desktop: join2(HOME2, "Desktop"),
|
|
795
|
+
documents: join2(HOME2, "Documents"),
|
|
796
|
+
document: join2(HOME2, "Documents"),
|
|
797
|
+
home: HOME2,
|
|
798
|
+
"~": HOME2
|
|
676
799
|
};
|
|
677
|
-
function
|
|
800
|
+
function normalizePath2(folderPath) {
|
|
678
801
|
const trimmed = folderPath.trim();
|
|
679
802
|
if (trimmed.startsWith("~/") || trimmed === "~") {
|
|
680
|
-
return
|
|
803
|
+
return resolve2(trimmed.replace(/^~/, HOME2));
|
|
681
804
|
}
|
|
682
805
|
const lower = trimmed.toLowerCase();
|
|
683
806
|
if (FOLDER_SHORTCUTS[lower]) {
|
|
@@ -688,9 +811,9 @@ function normalizePath(folderPath) {
|
|
|
688
811
|
return FOLDER_SHORTCUTS[withoutSlash];
|
|
689
812
|
}
|
|
690
813
|
if (trimmed.startsWith("/") || /^[A-Z]:\\/i.test(trimmed)) {
|
|
691
|
-
return
|
|
814
|
+
return resolve2(trimmed);
|
|
692
815
|
}
|
|
693
|
-
return
|
|
816
|
+
return resolve2(HOME2, trimmed);
|
|
694
817
|
}
|
|
695
818
|
var TOOLS = [
|
|
696
819
|
{
|
|
@@ -845,28 +968,42 @@ var TOOLS = [
|
|
|
845
968
|
}
|
|
846
969
|
}
|
|
847
970
|
];
|
|
848
|
-
function buildSystemPrompt(config) {
|
|
971
|
+
function buildSystemPrompt(config, onboarding) {
|
|
849
972
|
const folders = getWatchFolders(config);
|
|
850
973
|
const folderList = folders.map((f) => ` - ${f}`).join("\n");
|
|
974
|
+
const onboardingBlock = onboarding ? `
|
|
975
|
+
ONBOARDING MODE \u2014 This is the user's first time using AnrakLegal Sync. You just finished setup.
|
|
976
|
+
Your FIRST response must:
|
|
977
|
+
1. Welcome them warmly (1 line).
|
|
978
|
+
2. Ask which folders on their computer contain legal files they'd like to sync. Give examples like:
|
|
979
|
+
- Downloads (${join2(HOME2, "Downloads")}) \u2014 where they download court orders, filings
|
|
980
|
+
- Desktop (${join2(HOME2, "Desktop")}) \u2014 quick-access files
|
|
981
|
+
- Documents (${join2(HOME2, "Documents")}) \u2014 organized case folders
|
|
982
|
+
- A custom folder path
|
|
983
|
+
3. Tell them they can name multiple folders at once, e.g. "downloads and desktop"
|
|
984
|
+
4. After the user responds, use manage_watch_folders to add each folder they mention.
|
|
985
|
+
5. Once folders are set, confirm the setup and let them know you're now watching for new files.
|
|
986
|
+
Keep it conversational and simple \u2014 they are lawyers, not developers.
|
|
987
|
+
` : "";
|
|
851
988
|
return `You are AnrakLegal Sync, a terminal assistant that helps lawyers sync local files to their case management system.
|
|
852
989
|
|
|
853
990
|
Watch folders:
|
|
854
991
|
${folderList}
|
|
855
992
|
Server: ${config.apiUrl}
|
|
856
|
-
Home directory: ${
|
|
993
|
+
Home directory: ${HOME2}
|
|
857
994
|
Platform: ${IS_MAC ? "macOS" : platform()}
|
|
858
995
|
|
|
859
996
|
You can browse local folders, scan & sync files, list cases, show sync status, manage watch folders, and more.
|
|
860
|
-
|
|
997
|
+
${onboardingBlock}
|
|
861
998
|
Rules:
|
|
862
999
|
- Be concise. This is a terminal \u2014 1-3 lines unless showing a list.
|
|
863
1000
|
- Do NOT use markdown. Plain text only.
|
|
864
1001
|
- WATCH FOLDERS: When user asks to "watch", "add", or "also sync" a folder, use manage_watch_folders with action "add". When they say "stop watching" or "remove" a folder, use action "remove".
|
|
865
1002
|
- IMPORTANT: When user mentions a folder like "downloads" or "desktop", ALWAYS use the full absolute path. Examples:
|
|
866
|
-
"downloads" or "my downloads" \u2192 ${join2(
|
|
867
|
-
"desktop" \u2192 ${join2(
|
|
868
|
-
"documents" \u2192 ${join2(
|
|
869
|
-
"~/SomeFolder" \u2192 ${
|
|
1003
|
+
"downloads" or "my downloads" \u2192 ${join2(HOME2, "Downloads")}
|
|
1004
|
+
"desktop" \u2192 ${join2(HOME2, "Desktop")}
|
|
1005
|
+
"documents" \u2192 ${join2(HOME2, "Documents")}
|
|
1006
|
+
"~/SomeFolder" \u2192 ${HOME2}/SomeFolder
|
|
870
1007
|
- FINDING FILES: When user asks to find or locate a specific file, ALWAYS use search_files. NEVER browse a folder hoping to find it.
|
|
871
1008
|
- UPLOADING: When user wants to upload a specific file to a case, use upload_to_case (NOT scan_folder). scan_folder uploads EVERYTHING.
|
|
872
1009
|
- When user says "look at" or "check" a folder, use browse_folder to show what's there.
|
|
@@ -875,7 +1012,19 @@ Rules:
|
|
|
875
1012
|
- Present file lists in a clean table/list format with names and sizes.
|
|
876
1013
|
- Call tools to perform actions. Summarize results naturally after.
|
|
877
1014
|
- If a tool returns an error, report it clearly to the user.
|
|
878
|
-
-
|
|
1015
|
+
- PENDING MAPPINGS: When there are files waiting for case assignment, proactively ask the user which case to map them to. Use upload_to_case to upload once the user confirms.
|
|
1016
|
+
- Do NOT use thinking tags or reasoning tags in your output.
|
|
1017
|
+
${pendingFiles.length > 0 ? `
|
|
1018
|
+
FILES WAITING FOR MAPPING (ask user which case):
|
|
1019
|
+
${pendingFiles.map((f) => {
|
|
1020
|
+
const name = basename4(f.filePath || "unknown");
|
|
1021
|
+
const sug = f.suggestion ? ` \u2014 AI suggests: ${f.suggestion.caseName} (${f.suggestion.reason})` : "";
|
|
1022
|
+
return `- ${name} in folder "${f.folderName}"${sug}`;
|
|
1023
|
+
}).join("\n")}
|
|
1024
|
+
` : ""}${recentMappings.length > 0 ? `
|
|
1025
|
+
Recent mapping activity:
|
|
1026
|
+
${recentMappings.slice(-5).map((m) => `- ${m}`).join("\n")}
|
|
1027
|
+
` : ""}`;
|
|
879
1028
|
}
|
|
880
1029
|
async function executeTool(name, args, ctx) {
|
|
881
1030
|
switch (name) {
|
|
@@ -915,8 +1064,8 @@ async function executeTool(name, args, ctx) {
|
|
|
915
1064
|
var searchDir = searchDir2;
|
|
916
1065
|
const query = (args.query || "").trim();
|
|
917
1066
|
if (!query) return JSON.stringify({ error: "Missing search query" });
|
|
918
|
-
const rawSearch = args.searchPath ||
|
|
919
|
-
const searchPath =
|
|
1067
|
+
const rawSearch = args.searchPath || HOME2;
|
|
1068
|
+
const searchPath = normalizePath2(rawSearch);
|
|
920
1069
|
const maxDepth = Math.min(args.maxDepth || 3, 6);
|
|
921
1070
|
if (!existsSync2(searchPath) || !statSync(searchPath).isDirectory()) {
|
|
922
1071
|
return JSON.stringify({ error: `Search path not found: ${searchPath}` });
|
|
@@ -937,7 +1086,7 @@ async function executeTool(name, args, ctx) {
|
|
|
937
1086
|
const caseId = args.caseIdentifier;
|
|
938
1087
|
if (!rawPath) return JSON.stringify({ error: "Missing filePath" });
|
|
939
1088
|
if (!caseId) return JSON.stringify({ error: "Missing caseIdentifier" });
|
|
940
|
-
const filePath =
|
|
1089
|
+
const filePath = normalizePath2(rawPath);
|
|
941
1090
|
if (!existsSync2(filePath)) {
|
|
942
1091
|
return JSON.stringify({ error: `File not found: ${filePath}` });
|
|
943
1092
|
}
|
|
@@ -974,6 +1123,28 @@ async function executeTool(name, args, ctx) {
|
|
|
974
1123
|
const hash = await hashFile(filePath);
|
|
975
1124
|
markSynced(filePath, hash, fileStat.size, matchedCase.id, result.linkedDocumentIds[0]);
|
|
976
1125
|
log.success(`Uploaded ${basename4(filePath)} to ${matchedCase.caseName}`);
|
|
1126
|
+
removePendingFile(filePath);
|
|
1127
|
+
const parentFolder = dirname2(filePath);
|
|
1128
|
+
const folderName = basename4(parentFolder);
|
|
1129
|
+
if (folderName && matchedCase) {
|
|
1130
|
+
setFolderMapping(folderName, matchedCase.id, matchedCase.caseName, matchedCase.caseNumber);
|
|
1131
|
+
const sameFolderPending = removePendingByFolder(folderName);
|
|
1132
|
+
for (const pf of sameFolderPending) {
|
|
1133
|
+
if (pf.filePath && pf.filePath !== filePath) {
|
|
1134
|
+
try {
|
|
1135
|
+
const pfResult = await uploadFile(ctx.config, matchedCase.id, pf.filePath);
|
|
1136
|
+
if (pfResult.success && pfResult.linkedDocumentIds.length > 0) {
|
|
1137
|
+
const pfHash = await hashFile(pf.filePath);
|
|
1138
|
+
const pfStat = statSync(pf.filePath);
|
|
1139
|
+
markSynced(pf.filePath, pfHash, pfStat.size, matchedCase.id, pfResult.linkedDocumentIds[0]);
|
|
1140
|
+
log.success(`Also uploaded ${basename4(pf.filePath)} to ${matchedCase.caseName}`);
|
|
1141
|
+
}
|
|
1142
|
+
} catch {
|
|
1143
|
+
log.warn(`Failed to upload pending file: ${basename4(pf.filePath)}`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
977
1148
|
return JSON.stringify({
|
|
978
1149
|
success: true,
|
|
979
1150
|
filename: basename4(filePath),
|
|
@@ -993,7 +1164,7 @@ async function executeTool(name, args, ctx) {
|
|
|
993
1164
|
case "browse_folder": {
|
|
994
1165
|
const rawPath = args.folderPath;
|
|
995
1166
|
if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
|
|
996
|
-
const folderPath =
|
|
1167
|
+
const folderPath = normalizePath2(rawPath);
|
|
997
1168
|
if (!existsSync2(folderPath)) {
|
|
998
1169
|
return JSON.stringify({ error: `Folder not found: ${folderPath}` });
|
|
999
1170
|
}
|
|
@@ -1043,7 +1214,7 @@ async function executeTool(name, args, ctx) {
|
|
|
1043
1214
|
case "scan_folder": {
|
|
1044
1215
|
const rawPath = args.folderPath;
|
|
1045
1216
|
if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
|
|
1046
|
-
const folderPath =
|
|
1217
|
+
const folderPath = normalizePath2(rawPath);
|
|
1047
1218
|
try {
|
|
1048
1219
|
await ctx.scanFolder(folderPath);
|
|
1049
1220
|
const stats = getStats();
|
|
@@ -1115,7 +1286,7 @@ async function executeTool(name, args, ctx) {
|
|
|
1115
1286
|
}
|
|
1116
1287
|
if (action === "add") {
|
|
1117
1288
|
if (!rawFolder) return JSON.stringify({ error: "Missing folderPath" });
|
|
1118
|
-
const folderPath =
|
|
1289
|
+
const folderPath = normalizePath2(rawFolder);
|
|
1119
1290
|
if (!ctx.addWatchFolder) {
|
|
1120
1291
|
return JSON.stringify({ error: "Watch folder management not available" });
|
|
1121
1292
|
}
|
|
@@ -1127,7 +1298,7 @@ async function executeTool(name, args, ctx) {
|
|
|
1127
1298
|
}
|
|
1128
1299
|
if (action === "remove") {
|
|
1129
1300
|
if (!rawFolder) return JSON.stringify({ error: "Missing folderPath" });
|
|
1130
|
-
const folderPath =
|
|
1301
|
+
const folderPath = normalizePath2(rawFolder);
|
|
1131
1302
|
if (!ctx.removeWatchFolder) {
|
|
1132
1303
|
return JSON.stringify({ error: "Watch folder management not available" });
|
|
1133
1304
|
}
|
|
@@ -1238,13 +1409,13 @@ async function agentTurn(messages, ctx) {
|
|
|
1238
1409
|
}
|
|
1239
1410
|
function startAIAgent(ctx) {
|
|
1240
1411
|
const history = [
|
|
1241
|
-
{ role: "system", content: buildSystemPrompt(ctx.config) }
|
|
1412
|
+
{ role: "system", content: buildSystemPrompt(ctx.config, ctx.onboarding) }
|
|
1242
1413
|
];
|
|
1243
1414
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
1244
1415
|
const W = Math.min(process.stdout.columns || 60, 60);
|
|
1245
1416
|
const allFolders = getWatchFolders(ctx.config);
|
|
1246
1417
|
const folderDisplays = allFolders.map(
|
|
1247
|
-
(f) => f.startsWith(
|
|
1418
|
+
(f) => f.startsWith(HOME2) ? "~" + f.slice(HOME2.length) : f
|
|
1248
1419
|
);
|
|
1249
1420
|
const stats = ctx.bootStats;
|
|
1250
1421
|
const pad = (s, len) => {
|
|
@@ -1290,7 +1461,11 @@ function startAIAgent(ctx) {
|
|
|
1290
1461
|
console.log(row(`${chalk2.dim("Status")} ${statusLine}`));
|
|
1291
1462
|
}
|
|
1292
1463
|
console.log(empty);
|
|
1293
|
-
|
|
1464
|
+
if (ctx.onboarding) {
|
|
1465
|
+
console.log(row(chalk2.dim("First-time setup \u2014 the AI will guide you.")));
|
|
1466
|
+
} else {
|
|
1467
|
+
console.log(row(chalk2.dim("Type naturally, or press / for commands.")));
|
|
1468
|
+
}
|
|
1294
1469
|
console.log(empty);
|
|
1295
1470
|
console.log(bot);
|
|
1296
1471
|
console.log("");
|
|
@@ -1361,7 +1536,7 @@ function startAIAgent(ctx) {
|
|
|
1361
1536
|
console.log(chalk2.dim(" " + "\u2500".repeat(40)));
|
|
1362
1537
|
for (let i = 0; i < folders.length; i++) {
|
|
1363
1538
|
const f = folders[i];
|
|
1364
|
-
const display = f.startsWith(
|
|
1539
|
+
const display = f.startsWith(HOME2) ? "~" + f.slice(HOME2.length) : f;
|
|
1365
1540
|
const tag = f === ctx.config.watchFolder ? chalk2.cyan(" (primary)") : "";
|
|
1366
1541
|
console.log(` ${chalk2.dim(`${i + 1}.`)} ${display}${tag}`);
|
|
1367
1542
|
}
|
|
@@ -1379,14 +1554,14 @@ function startAIAgent(ctx) {
|
|
|
1379
1554
|
console.log(chalk2.red(" Usage: /folder add <path>"));
|
|
1380
1555
|
return;
|
|
1381
1556
|
}
|
|
1382
|
-
const absPath =
|
|
1557
|
+
const absPath = normalizePath2(folderArg);
|
|
1383
1558
|
if (!ctx.addWatchFolder) {
|
|
1384
1559
|
console.log(chalk2.red(" Watch folder management not available."));
|
|
1385
1560
|
return;
|
|
1386
1561
|
}
|
|
1387
1562
|
const result = await ctx.addWatchFolder(absPath);
|
|
1388
1563
|
if (result.added) {
|
|
1389
|
-
const display = (result.path || absPath).startsWith(
|
|
1564
|
+
const display = (result.path || absPath).startsWith(HOME2) ? "~" + (result.path || absPath).slice(HOME2.length) : result.path || absPath;
|
|
1390
1565
|
console.log(chalk2.green(` Added: ${display}`));
|
|
1391
1566
|
} else {
|
|
1392
1567
|
console.log(chalk2.yellow(` ${result.error || "Could not add folder"}`));
|
|
@@ -1396,14 +1571,14 @@ function startAIAgent(ctx) {
|
|
|
1396
1571
|
console.log(chalk2.red(" Usage: /folder remove <path>"));
|
|
1397
1572
|
return;
|
|
1398
1573
|
}
|
|
1399
|
-
const absPath =
|
|
1574
|
+
const absPath = normalizePath2(folderArg);
|
|
1400
1575
|
if (!ctx.removeWatchFolder) {
|
|
1401
1576
|
console.log(chalk2.red(" Watch folder management not available."));
|
|
1402
1577
|
return;
|
|
1403
1578
|
}
|
|
1404
1579
|
const result = ctx.removeWatchFolder(absPath);
|
|
1405
1580
|
if (result.removed) {
|
|
1406
|
-
const display = absPath.startsWith(
|
|
1581
|
+
const display = absPath.startsWith(HOME2) ? "~" + absPath.slice(HOME2.length) : absPath;
|
|
1407
1582
|
console.log(chalk2.green(` Removed: ${display}`));
|
|
1408
1583
|
} else {
|
|
1409
1584
|
console.log(chalk2.yellow(` ${result.error || "Could not remove folder"}`));
|
|
@@ -1413,14 +1588,14 @@ function startAIAgent(ctx) {
|
|
|
1413
1588
|
console.log(chalk2.red(" Usage: /folder set <path>"));
|
|
1414
1589
|
return;
|
|
1415
1590
|
}
|
|
1416
|
-
const absPath =
|
|
1591
|
+
const absPath = normalizePath2(folderArg);
|
|
1417
1592
|
if (!existsSync2(absPath) || !statSync(absPath).isDirectory()) {
|
|
1418
1593
|
console.log(chalk2.red(` Not a valid directory: ${absPath}`));
|
|
1419
1594
|
return;
|
|
1420
1595
|
}
|
|
1421
1596
|
ctx.config.watchFolder = absPath;
|
|
1422
1597
|
saveConfig(ctx.config);
|
|
1423
|
-
const display = absPath.startsWith(
|
|
1598
|
+
const display = absPath.startsWith(HOME2) ? "~" + absPath.slice(HOME2.length) : absPath;
|
|
1424
1599
|
console.log(chalk2.green(` Primary folder set to: ${display}`));
|
|
1425
1600
|
} else {
|
|
1426
1601
|
console.log(chalk2.dim(" Usage: /folder [list|add|remove|set] <path>"));
|
|
@@ -1506,8 +1681,41 @@ function startAIAgent(ctx) {
|
|
|
1506
1681
|
}
|
|
1507
1682
|
};
|
|
1508
1683
|
let currentSlashArgs = "";
|
|
1684
|
+
let lastPendingCount = 0;
|
|
1685
|
+
let onboardingDone = !ctx.onboarding;
|
|
1509
1686
|
async function promptLoop() {
|
|
1687
|
+
if (!onboardingDone) {
|
|
1688
|
+
onboardingDone = true;
|
|
1689
|
+
history.push({ role: "user", content: "Hi, I just installed AnrakLegal Sync. Help me set up my watch folders." });
|
|
1690
|
+
try {
|
|
1691
|
+
const response = await agentTurn([...history], ctx);
|
|
1692
|
+
if (response) {
|
|
1693
|
+
history.push({ role: "assistant", content: response });
|
|
1694
|
+
}
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1697
|
+
console.log(chalk2.red(` Error: ${msg}`));
|
|
1698
|
+
}
|
|
1699
|
+
console.log("");
|
|
1700
|
+
}
|
|
1510
1701
|
while (true) {
|
|
1702
|
+
if (pendingFiles.length > 0 && pendingFiles.length !== lastPendingCount) {
|
|
1703
|
+
lastPendingCount = pendingFiles.length;
|
|
1704
|
+
console.log("");
|
|
1705
|
+
console.log(chalk2.yellow(` ${pendingFiles.length} file(s) need case mapping:`));
|
|
1706
|
+
for (const f of pendingFiles) {
|
|
1707
|
+
const name = basename4(f.filePath || "unknown");
|
|
1708
|
+
if (f.suggestion) {
|
|
1709
|
+
console.log(
|
|
1710
|
+
` ${chalk2.cyan(name)} ${chalk2.dim("in")} ${f.folderName} ${chalk2.dim("\u2014 AI suggests:")} ${chalk2.green(f.suggestion.caseName)}`
|
|
1711
|
+
);
|
|
1712
|
+
} else {
|
|
1713
|
+
console.log(` ${chalk2.cyan(name)} ${chalk2.dim("in")} ${f.folderName} ${chalk2.dim("\u2014 no match found")}`);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
console.log(chalk2.dim(" Tell me which case, or say 'yes' to accept suggestions."));
|
|
1717
|
+
console.log("");
|
|
1718
|
+
}
|
|
1511
1719
|
let input;
|
|
1512
1720
|
try {
|
|
1513
1721
|
input = await rl.question(PROMPT);
|
|
@@ -1558,6 +1766,7 @@ function startAIAgent(ctx) {
|
|
|
1558
1766
|
while (history.length > 21) {
|
|
1559
1767
|
history.splice(1, 1);
|
|
1560
1768
|
}
|
|
1769
|
+
history[0] = { role: "system", content: buildSystemPrompt(ctx.config, false) };
|
|
1561
1770
|
try {
|
|
1562
1771
|
const response = await agentTurn([...history], ctx);
|
|
1563
1772
|
if (response) {
|
|
@@ -1573,6 +1782,92 @@ function startAIAgent(ctx) {
|
|
|
1573
1782
|
void promptLoop();
|
|
1574
1783
|
}
|
|
1575
1784
|
|
|
1785
|
+
// src/smart-mapper.ts
|
|
1786
|
+
async function smartMapFolder(config, folderName, fileName, cases) {
|
|
1787
|
+
if (cases.length === 0) return null;
|
|
1788
|
+
let token;
|
|
1789
|
+
try {
|
|
1790
|
+
token = await getAccessToken(config);
|
|
1791
|
+
} catch {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
const caseList = cases.map((c, i) => `${i + 1}. ${c.caseNumber}: ${c.caseName}`).join("\n");
|
|
1795
|
+
const messages = [
|
|
1796
|
+
{
|
|
1797
|
+
role: "system",
|
|
1798
|
+
content: "You are a legal file organizer. Match files to legal cases. Respond ONLY with a JSON object. No markdown, no explanation outside JSON."
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
role: "user",
|
|
1802
|
+
content: `Match this file to one of the cases below.
|
|
1803
|
+
|
|
1804
|
+
File: ${fileName}
|
|
1805
|
+
Folder: ${folderName}
|
|
1806
|
+
|
|
1807
|
+
Cases:
|
|
1808
|
+
${caseList}
|
|
1809
|
+
|
|
1810
|
+
Respond with ONLY valid JSON:
|
|
1811
|
+
If a case matches: {"caseIndex": <number 1-${cases.length}>, "confidence": "high" or "low", "reason": "<10 words max>"}
|
|
1812
|
+
If no case matches: {"caseIndex": null, "confidence": "none", "reason": "<why>"}`
|
|
1813
|
+
}
|
|
1814
|
+
];
|
|
1815
|
+
try {
|
|
1816
|
+
const response = await fetch(`${config.apiUrl}/api/sync/chat`, {
|
|
1817
|
+
method: "POST",
|
|
1818
|
+
headers: {
|
|
1819
|
+
Authorization: `Bearer ${token}`,
|
|
1820
|
+
"Content-Type": "application/json"
|
|
1821
|
+
},
|
|
1822
|
+
body: JSON.stringify({ messages }),
|
|
1823
|
+
signal: AbortSignal.timeout(15e3)
|
|
1824
|
+
});
|
|
1825
|
+
if (!response.ok) return null;
|
|
1826
|
+
let text = "";
|
|
1827
|
+
const reader = response.body.getReader();
|
|
1828
|
+
const decoder = new TextDecoder();
|
|
1829
|
+
while (true) {
|
|
1830
|
+
const { done, value } = await reader.read();
|
|
1831
|
+
if (done) break;
|
|
1832
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1833
|
+
for (const line of chunk.split("\n")) {
|
|
1834
|
+
const trimmed = line.trim();
|
|
1835
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
1836
|
+
const data = trimmed.slice(6);
|
|
1837
|
+
if (data === "[DONE]") continue;
|
|
1838
|
+
try {
|
|
1839
|
+
const parsed = JSON.parse(data);
|
|
1840
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
1841
|
+
if (content) text += content;
|
|
1842
|
+
} catch {
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
text = text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
1847
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
1848
|
+
if (!jsonMatch) return null;
|
|
1849
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
1850
|
+
if (result.caseIndex == null || result.confidence === "none") {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
const idx = result.caseIndex - 1;
|
|
1854
|
+
if (idx < 0 || idx >= cases.length) return null;
|
|
1855
|
+
const matched = cases[idx];
|
|
1856
|
+
return {
|
|
1857
|
+
caseId: matched.id,
|
|
1858
|
+
caseName: matched.caseName,
|
|
1859
|
+
caseNumber: matched.caseNumber,
|
|
1860
|
+
confidence: result.confidence === "high" ? "high" : "low",
|
|
1861
|
+
reason: result.reason || ""
|
|
1862
|
+
};
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
log.debug(
|
|
1865
|
+
`Smart mapper error: ${err instanceof Error ? err.message : String(err)}`
|
|
1866
|
+
);
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1576
1871
|
// src/watcher.ts
|
|
1577
1872
|
async function scanFolder(config) {
|
|
1578
1873
|
const folders = getWatchFolders(config);
|
|
@@ -1625,7 +1920,7 @@ async function pushSync(config) {
|
|
|
1625
1920
|
);
|
|
1626
1921
|
}
|
|
1627
1922
|
async function scanExternalFolder(config, folderPath, cases) {
|
|
1628
|
-
const absPath =
|
|
1923
|
+
const absPath = resolve3(folderPath);
|
|
1629
1924
|
if (!existsSync3(absPath)) {
|
|
1630
1925
|
log.error(`Folder not found: ${absPath}`);
|
|
1631
1926
|
return;
|
|
@@ -1671,12 +1966,13 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
1671
1966
|
log.success("Everything up to date");
|
|
1672
1967
|
}
|
|
1673
1968
|
}
|
|
1674
|
-
async function startWatching(config) {
|
|
1969
|
+
async function startWatching(config, opts) {
|
|
1675
1970
|
const folders = getWatchFolders(config);
|
|
1971
|
+
const smartMapper = async (folderName, fileName, caseList) => smartMapFolder(config, folderName, fileName, caseList);
|
|
1676
1972
|
let cases = await listCases(config);
|
|
1677
1973
|
const { scanned, queued } = await scanFolder(config);
|
|
1678
1974
|
if (queued > 0) {
|
|
1679
|
-
const result = await processQueue(config, cases);
|
|
1975
|
+
const result = await processQueue(config, cases, { smartMapper });
|
|
1680
1976
|
if (result.failed > 0) {
|
|
1681
1977
|
log.warn(`Initial sync: ${result.uploaded} uploaded, ${result.failed} failed`);
|
|
1682
1978
|
}
|
|
@@ -1693,7 +1989,7 @@ async function startWatching(config) {
|
|
|
1693
1989
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1694
1990
|
debounceTimer = setTimeout(async () => {
|
|
1695
1991
|
if (queueSize() > 0) {
|
|
1696
|
-
const result = await processQueue(config, cases);
|
|
1992
|
+
const result = await processQueue(config, cases, { smartMapper });
|
|
1697
1993
|
if (result.uploaded > 0 || result.failed > 0) {
|
|
1698
1994
|
log.info(
|
|
1699
1995
|
`Batch: ${result.uploaded} uploaded, ${result.failed} failed`
|
|
@@ -1743,6 +2039,7 @@ async function startWatching(config) {
|
|
|
1743
2039
|
startAIAgent({
|
|
1744
2040
|
config,
|
|
1745
2041
|
bootStats: { cases: cases.length, scanned, queued },
|
|
2042
|
+
onboarding: opts?.onboarding,
|
|
1746
2043
|
getCases: () => cases,
|
|
1747
2044
|
refreshCases: async () => {
|
|
1748
2045
|
cases = await listCases(config);
|
|
@@ -1753,7 +2050,7 @@ async function startWatching(config) {
|
|
|
1753
2050
|
const result = await scanFolder(config);
|
|
1754
2051
|
log.info(`Scanned ${result.scanned} files, ${result.queued} need syncing`);
|
|
1755
2052
|
if (result.queued > 0) {
|
|
1756
|
-
const syncResult = await processQueue(config, cases);
|
|
2053
|
+
const syncResult = await processQueue(config, cases, { smartMapper });
|
|
1757
2054
|
log.info(`Synced: ${syncResult.uploaded} uploaded, ${syncResult.failed} failed`);
|
|
1758
2055
|
}
|
|
1759
2056
|
},
|
|
@@ -1761,7 +2058,7 @@ async function startWatching(config) {
|
|
|
1761
2058
|
await scanExternalFolder(config, folderPath, cases);
|
|
1762
2059
|
},
|
|
1763
2060
|
addWatchFolder: async (folderPath) => {
|
|
1764
|
-
const absPath =
|
|
2061
|
+
const absPath = resolve3(folderPath);
|
|
1765
2062
|
if (!existsSync3(absPath) || !statSync2(absPath).isDirectory()) {
|
|
1766
2063
|
return { added: false, error: `Not a valid directory: ${absPath}` };
|
|
1767
2064
|
}
|
|
@@ -1774,7 +2071,7 @@ async function startWatching(config) {
|
|
|
1774
2071
|
return { added: true, path: absPath };
|
|
1775
2072
|
},
|
|
1776
2073
|
removeWatchFolder: (folderPath) => {
|
|
1777
|
-
const absPath =
|
|
2074
|
+
const absPath = resolve3(folderPath);
|
|
1778
2075
|
const result = removeWatchFolder(config, absPath);
|
|
1779
2076
|
if (result.removed) {
|
|
1780
2077
|
const w = watchers.get(absPath);
|
|
@@ -1875,13 +2172,34 @@ async function checkForUpdates(currentVersion) {
|
|
|
1875
2172
|
}
|
|
1876
2173
|
|
|
1877
2174
|
// src/cli.ts
|
|
1878
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
1879
2175
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1880
2176
|
import { dirname as dirname3, join as join4 } from "path";
|
|
1881
2177
|
var __filename3 = fileURLToPath2(import.meta.url);
|
|
1882
2178
|
var __dirname3 = dirname3(__filename3);
|
|
1883
2179
|
var pkg = JSON.parse(readFileSync3(join4(__dirname3, "..", "package.json"), "utf-8"));
|
|
1884
2180
|
await checkForUpdates(pkg.version);
|
|
2181
|
+
var HOME3 = homedir3();
|
|
2182
|
+
var FOLDER_SHORTCUTS2 = {
|
|
2183
|
+
downloads: join4(HOME3, "Downloads"),
|
|
2184
|
+
download: join4(HOME3, "Downloads"),
|
|
2185
|
+
desktop: join4(HOME3, "Desktop"),
|
|
2186
|
+
documents: join4(HOME3, "Documents"),
|
|
2187
|
+
document: join4(HOME3, "Documents"),
|
|
2188
|
+
home: HOME3,
|
|
2189
|
+
"~": HOME3
|
|
2190
|
+
};
|
|
2191
|
+
function normalizeFolderPath(input) {
|
|
2192
|
+
const trimmed = input.trim();
|
|
2193
|
+
if (!trimmed) return trimmed;
|
|
2194
|
+
if (trimmed.startsWith("~/") || trimmed === "~") {
|
|
2195
|
+
return resolve4(trimmed.replace(/^~/, HOME3));
|
|
2196
|
+
}
|
|
2197
|
+
const lower = trimmed.toLowerCase();
|
|
2198
|
+
if (FOLDER_SHORTCUTS2[lower]) return FOLDER_SHORTCUTS2[lower];
|
|
2199
|
+
const withoutSlash = lower.replace(/^\//, "");
|
|
2200
|
+
if (FOLDER_SHORTCUTS2[withoutSlash]) return FOLDER_SHORTCUTS2[withoutSlash];
|
|
2201
|
+
return resolve4(trimmed);
|
|
2202
|
+
}
|
|
1885
2203
|
var program = new Command();
|
|
1886
2204
|
program.name("anrak-sync").description("AnrakLegal desktop file sync \u2014 watches local folders, syncs to case management").version(pkg.version);
|
|
1887
2205
|
program.command("init").description("Set up AnrakLegal Sync (first-time configuration)").option("--password", "Use email/password login instead of browser").action(async (opts) => {
|
|
@@ -1919,37 +2237,25 @@ program.command("init").description("Set up AnrakLegal Sync (first-time configur
|
|
|
1919
2237
|
tokens = await browserLogin(apiUrl);
|
|
1920
2238
|
}
|
|
1921
2239
|
log.success("Authenticated");
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
` Watch folder ${chalk4.dim(`(${defaultFolder})`)}: `
|
|
1926
|
-
);
|
|
1927
|
-
const watchFolder = resolve3(watchInput || defaultFolder);
|
|
1928
|
-
rl2.close();
|
|
1929
|
-
if (!existsSync4(watchFolder)) {
|
|
1930
|
-
log.warn(
|
|
1931
|
-
`Folder ${watchFolder} does not exist \u2014 it will be created when you add files`
|
|
1932
|
-
);
|
|
1933
|
-
} else if (!statSync3(watchFolder).isDirectory()) {
|
|
1934
|
-
log.error(`${watchFolder} is not a directory`);
|
|
1935
|
-
process.exit(1);
|
|
2240
|
+
try {
|
|
2241
|
+
rl.close();
|
|
2242
|
+
} catch {
|
|
1936
2243
|
}
|
|
2244
|
+
const defaultFolder = normalizeFolderPath("downloads");
|
|
1937
2245
|
const config = {
|
|
1938
2246
|
apiUrl,
|
|
1939
2247
|
supabaseUrl: serverConfig.supabaseUrl,
|
|
1940
2248
|
supabaseAnonKey: serverConfig.supabaseAnonKey,
|
|
1941
2249
|
accessToken: tokens.accessToken,
|
|
1942
2250
|
refreshToken: tokens.refreshToken,
|
|
1943
|
-
watchFolder
|
|
2251
|
+
watchFolder: defaultFolder
|
|
1944
2252
|
};
|
|
1945
2253
|
saveConfig(config);
|
|
1946
2254
|
console.log("");
|
|
1947
2255
|
log.success("Setup complete!");
|
|
1948
2256
|
log.info(`Config saved to ${getConfigDir()}`);
|
|
1949
|
-
log
|
|
1950
|
-
|
|
1951
|
-
chalk4.dim("\n Run `anrak-sync start` to begin syncing\n")
|
|
1952
|
-
);
|
|
2257
|
+
console.log("");
|
|
2258
|
+
await startWatching(config, { onboarding: true });
|
|
1953
2259
|
} catch (err) {
|
|
1954
2260
|
log.error(err instanceof Error ? err.message : String(err));
|
|
1955
2261
|
process.exit(1);
|