@anraktech/sync 0.13.0 → 0.14.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 +316 -9
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -454,6 +454,40 @@ import { basename as basename3, dirname, relative } from "path";
|
|
|
454
454
|
|
|
455
455
|
// src/mapper.ts
|
|
456
456
|
import { basename as basename2 } from "path";
|
|
457
|
+
|
|
458
|
+
// src/events.ts
|
|
459
|
+
var pendingFiles = [];
|
|
460
|
+
var recentMappings = [];
|
|
461
|
+
var MAX_RECENT = 20;
|
|
462
|
+
function addRecentMapping(msg) {
|
|
463
|
+
recentMappings.push(msg);
|
|
464
|
+
if (recentMappings.length > MAX_RECENT) {
|
|
465
|
+
recentMappings.shift();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function addPendingFile(file) {
|
|
469
|
+
if (!pendingFiles.some((f) => f.filePath === file.filePath)) {
|
|
470
|
+
pendingFiles.push(file);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function removePendingFile(filePath) {
|
|
474
|
+
const idx = pendingFiles.findIndex((f) => f.filePath === filePath);
|
|
475
|
+
if (idx >= 0) {
|
|
476
|
+
return pendingFiles.splice(idx, 1)[0];
|
|
477
|
+
}
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
function removePendingByFolder(folderName) {
|
|
481
|
+
const removed = [];
|
|
482
|
+
for (let i = pendingFiles.length - 1; i >= 0; i--) {
|
|
483
|
+
if (pendingFiles[i].folderName === folderName) {
|
|
484
|
+
removed.push(pendingFiles.splice(i, 1)[0]);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return removed;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/mapper.ts
|
|
457
491
|
function normalize(s) {
|
|
458
492
|
return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
459
493
|
}
|
|
@@ -506,7 +540,7 @@ function deriveCaseInfo(folderName) {
|
|
|
506
540
|
caseName: sanitized
|
|
507
541
|
};
|
|
508
542
|
}
|
|
509
|
-
async function resolveFolder(config, folderPath, cases) {
|
|
543
|
+
async function resolveFolder(config, folderPath, cases, options) {
|
|
510
544
|
const folderName = basename2(folderPath);
|
|
511
545
|
const cached = getFolderMapping(folderName);
|
|
512
546
|
if (cached) {
|
|
@@ -522,6 +556,7 @@ async function resolveFolder(config, folderPath, cases) {
|
|
|
522
556
|
if (match) {
|
|
523
557
|
log.success(`Mapped "${folderName}" -> ${match.caseNumber} (${match.caseName})`);
|
|
524
558
|
setFolderMapping(folderName, match.id, match.caseName, match.caseNumber);
|
|
559
|
+
addRecentMapping(`Mapped "${folderName}" \u2192 ${match.caseNumber} (${match.caseName})`);
|
|
525
560
|
return {
|
|
526
561
|
caseId: match.id,
|
|
527
562
|
caseName: match.caseName,
|
|
@@ -529,12 +564,63 @@ async function resolveFolder(config, folderPath, cases) {
|
|
|
529
564
|
created: false
|
|
530
565
|
};
|
|
531
566
|
}
|
|
567
|
+
if (options?.smartMapper && allCases.length > 0) {
|
|
568
|
+
try {
|
|
569
|
+
const smartResult = await options.smartMapper(
|
|
570
|
+
folderName,
|
|
571
|
+
options.fileName || folderName,
|
|
572
|
+
allCases
|
|
573
|
+
);
|
|
574
|
+
if (smartResult) {
|
|
575
|
+
if (smartResult.confidence === "high") {
|
|
576
|
+
log.success(`AI mapped "${folderName}" \u2192 ${smartResult.caseNumber} (${smartResult.reason})`);
|
|
577
|
+
setFolderMapping(folderName, smartResult.caseId, smartResult.caseName, smartResult.caseNumber);
|
|
578
|
+
addRecentMapping(
|
|
579
|
+
`AI mapped "${options.fileName || folderName}" \u2192 ${smartResult.caseNumber} (${smartResult.caseName})`
|
|
580
|
+
);
|
|
581
|
+
return {
|
|
582
|
+
caseId: smartResult.caseId,
|
|
583
|
+
caseName: smartResult.caseName,
|
|
584
|
+
caseNumber: smartResult.caseNumber,
|
|
585
|
+
created: false
|
|
586
|
+
};
|
|
587
|
+
} else {
|
|
588
|
+
log.info(
|
|
589
|
+
`AI unsure about "${folderName}" \u2014 suggests ${smartResult.caseNumber} (${smartResult.reason})`
|
|
590
|
+
);
|
|
591
|
+
addPendingFile({
|
|
592
|
+
filePath: "",
|
|
593
|
+
// Set by uploader
|
|
594
|
+
folderName,
|
|
595
|
+
suggestion: {
|
|
596
|
+
caseName: smartResult.caseName,
|
|
597
|
+
caseNumber: smartResult.caseNumber,
|
|
598
|
+
reason: smartResult.reason
|
|
599
|
+
},
|
|
600
|
+
addedAt: Date.now()
|
|
601
|
+
});
|
|
602
|
+
addRecentMapping(
|
|
603
|
+
`Unsure: "${options.fileName || folderName}" in "${folderName}" \u2014 AI suggests ${smartResult.caseNumber} but needs confirmation`
|
|
604
|
+
);
|
|
605
|
+
return {
|
|
606
|
+
caseId: "",
|
|
607
|
+
caseName: "",
|
|
608
|
+
caseNumber: "",
|
|
609
|
+
created: false,
|
|
610
|
+
needsInput: true
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
532
617
|
const { caseNumber, caseName } = deriveCaseInfo(folderName);
|
|
533
618
|
log.info(`No match for "${folderName}" \u2014 creating case ${caseNumber}`);
|
|
534
619
|
try {
|
|
535
620
|
const newCase = await createCase(config, caseNumber, caseName);
|
|
536
621
|
setFolderMapping(folderName, newCase.id, newCase.caseName, newCase.caseNumber);
|
|
537
622
|
log.success(`Created case ${newCase.caseNumber} (${newCase.caseName})`);
|
|
623
|
+
addRecentMapping(`Created new case "${newCase.caseNumber}" for folder "${folderName}"`);
|
|
538
624
|
return {
|
|
539
625
|
caseId: newCase.id,
|
|
540
626
|
caseName: newCase.caseName,
|
|
@@ -583,7 +669,7 @@ async function enqueue(filePath, watchFolder) {
|
|
|
583
669
|
queue.push({ filePath, folderPath, retries: 0 });
|
|
584
670
|
log.file("queued", relative(watchFolder, filePath));
|
|
585
671
|
}
|
|
586
|
-
async function processQueue(config, cases) {
|
|
672
|
+
async function processQueue(config, cases, options) {
|
|
587
673
|
if (processing) return { uploaded: 0, failed: 0 };
|
|
588
674
|
processing = true;
|
|
589
675
|
let uploaded = 0;
|
|
@@ -596,11 +682,29 @@ async function processQueue(config, cases) {
|
|
|
596
682
|
await sleep(RATE_LIMIT_INTERVAL_MS - elapsed);
|
|
597
683
|
}
|
|
598
684
|
try {
|
|
599
|
-
const
|
|
685
|
+
const resolved = await resolveFolder(
|
|
600
686
|
config,
|
|
601
687
|
item.folderPath,
|
|
602
|
-
cases
|
|
688
|
+
cases,
|
|
689
|
+
{ smartMapper: options?.smartMapper, fileName: filename }
|
|
603
690
|
);
|
|
691
|
+
if (resolved.needsInput) {
|
|
692
|
+
const pending = pendingFiles.find(
|
|
693
|
+
(p) => p.folderName === basename3(item.folderPath) && !p.filePath
|
|
694
|
+
);
|
|
695
|
+
if (pending) {
|
|
696
|
+
pending.filePath = item.filePath;
|
|
697
|
+
} else {
|
|
698
|
+
addPendingFile({
|
|
699
|
+
filePath: item.filePath,
|
|
700
|
+
folderName: basename3(item.folderPath),
|
|
701
|
+
addedAt: Date.now()
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
log.info(`Waiting for mapping: ${filename}`);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const { caseId, caseName } = resolved;
|
|
604
708
|
const hash = await hashFile(item.filePath);
|
|
605
709
|
const s = await stat2(item.filePath);
|
|
606
710
|
markPending(item.filePath, hash, s.size, caseId);
|
|
@@ -821,6 +925,28 @@ var TOOLS = [
|
|
|
821
925
|
description: "Re-scan the watch folder for new or changed files and sync them.",
|
|
822
926
|
parameters: { type: "object", properties: {} }
|
|
823
927
|
}
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
type: "function",
|
|
931
|
+
function: {
|
|
932
|
+
name: "manage_watch_folders",
|
|
933
|
+
description: "Add, remove, or list watch folders. Use when user wants to watch a new folder, stop watching a folder, or see which folders are being watched.",
|
|
934
|
+
parameters: {
|
|
935
|
+
type: "object",
|
|
936
|
+
properties: {
|
|
937
|
+
action: {
|
|
938
|
+
type: "string",
|
|
939
|
+
enum: ["add", "remove", "list"],
|
|
940
|
+
description: "Action to perform: 'add' a new folder, 'remove' an existing one, or 'list' all watched folders."
|
|
941
|
+
},
|
|
942
|
+
folderPath: {
|
|
943
|
+
type: "string",
|
|
944
|
+
description: "Absolute path to the folder (required for add/remove). Use full absolute paths like /Users/name/Desktop."
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
required: ["action"]
|
|
948
|
+
}
|
|
949
|
+
}
|
|
824
950
|
}
|
|
825
951
|
];
|
|
826
952
|
function buildSystemPrompt(config) {
|
|
@@ -834,11 +960,12 @@ Server: ${config.apiUrl}
|
|
|
834
960
|
Home directory: ${HOME}
|
|
835
961
|
Platform: ${IS_MAC ? "macOS" : platform()}
|
|
836
962
|
|
|
837
|
-
You can browse local folders, scan & sync files, list cases, show sync status, and more.
|
|
963
|
+
You can browse local folders, scan & sync files, list cases, show sync status, manage watch folders, and more.
|
|
838
964
|
|
|
839
965
|
Rules:
|
|
840
966
|
- Be concise. This is a terminal \u2014 1-3 lines unless showing a list.
|
|
841
967
|
- Do NOT use markdown. Plain text only.
|
|
968
|
+
- 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".
|
|
842
969
|
- IMPORTANT: When user mentions a folder like "downloads" or "desktop", ALWAYS use the full absolute path. Examples:
|
|
843
970
|
"downloads" or "my downloads" \u2192 ${join2(HOME, "Downloads")}
|
|
844
971
|
"desktop" \u2192 ${join2(HOME, "Desktop")}
|
|
@@ -852,7 +979,19 @@ Rules:
|
|
|
852
979
|
- Present file lists in a clean table/list format with names and sizes.
|
|
853
980
|
- Call tools to perform actions. Summarize results naturally after.
|
|
854
981
|
- If a tool returns an error, report it clearly to the user.
|
|
855
|
-
-
|
|
982
|
+
- 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.
|
|
983
|
+
- Do NOT use thinking tags or reasoning tags in your output.
|
|
984
|
+
${pendingFiles.length > 0 ? `
|
|
985
|
+
FILES WAITING FOR MAPPING (ask user which case):
|
|
986
|
+
${pendingFiles.map((f) => {
|
|
987
|
+
const name = basename4(f.filePath || "unknown");
|
|
988
|
+
const sug = f.suggestion ? ` \u2014 AI suggests: ${f.suggestion.caseName} (${f.suggestion.reason})` : "";
|
|
989
|
+
return `- ${name} in folder "${f.folderName}"${sug}`;
|
|
990
|
+
}).join("\n")}
|
|
991
|
+
` : ""}${recentMappings.length > 0 ? `
|
|
992
|
+
Recent mapping activity:
|
|
993
|
+
${recentMappings.slice(-5).map((m) => `- ${m}`).join("\n")}
|
|
994
|
+
` : ""}`;
|
|
856
995
|
}
|
|
857
996
|
async function executeTool(name, args, ctx) {
|
|
858
997
|
switch (name) {
|
|
@@ -951,6 +1090,28 @@ async function executeTool(name, args, ctx) {
|
|
|
951
1090
|
const hash = await hashFile(filePath);
|
|
952
1091
|
markSynced(filePath, hash, fileStat.size, matchedCase.id, result.linkedDocumentIds[0]);
|
|
953
1092
|
log.success(`Uploaded ${basename4(filePath)} to ${matchedCase.caseName}`);
|
|
1093
|
+
removePendingFile(filePath);
|
|
1094
|
+
const parentFolder = dirname2(filePath);
|
|
1095
|
+
const folderName = basename4(parentFolder);
|
|
1096
|
+
if (folderName && matchedCase) {
|
|
1097
|
+
setFolderMapping(folderName, matchedCase.id, matchedCase.caseName, matchedCase.caseNumber);
|
|
1098
|
+
const sameFolderPending = removePendingByFolder(folderName);
|
|
1099
|
+
for (const pf of sameFolderPending) {
|
|
1100
|
+
if (pf.filePath && pf.filePath !== filePath) {
|
|
1101
|
+
try {
|
|
1102
|
+
const pfResult = await uploadFile(ctx.config, matchedCase.id, pf.filePath);
|
|
1103
|
+
if (pfResult.success && pfResult.linkedDocumentIds.length > 0) {
|
|
1104
|
+
const pfHash = await hashFile(pf.filePath);
|
|
1105
|
+
const pfStat = statSync(pf.filePath);
|
|
1106
|
+
markSynced(pf.filePath, pfHash, pfStat.size, matchedCase.id, pfResult.linkedDocumentIds[0]);
|
|
1107
|
+
log.success(`Also uploaded ${basename4(pf.filePath)} to ${matchedCase.caseName}`);
|
|
1108
|
+
}
|
|
1109
|
+
} catch {
|
|
1110
|
+
log.warn(`Failed to upload pending file: ${basename4(pf.filePath)}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
954
1115
|
return JSON.stringify({
|
|
955
1116
|
success: true,
|
|
956
1117
|
filename: basename4(filePath),
|
|
@@ -1076,6 +1237,46 @@ async function executeTool(name, args, ctx) {
|
|
|
1076
1237
|
pending: stats.pending
|
|
1077
1238
|
});
|
|
1078
1239
|
}
|
|
1240
|
+
case "manage_watch_folders": {
|
|
1241
|
+
const action = args.action;
|
|
1242
|
+
const rawFolder = args.folderPath;
|
|
1243
|
+
if (action === "list") {
|
|
1244
|
+
const folders = getWatchFolders(ctx.config);
|
|
1245
|
+
return JSON.stringify({
|
|
1246
|
+
folders: folders.map((f, i) => ({
|
|
1247
|
+
path: f,
|
|
1248
|
+
primary: f === ctx.config.watchFolder,
|
|
1249
|
+
index: i + 1
|
|
1250
|
+
})),
|
|
1251
|
+
total: folders.length
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
if (action === "add") {
|
|
1255
|
+
if (!rawFolder) return JSON.stringify({ error: "Missing folderPath" });
|
|
1256
|
+
const folderPath = normalizePath(rawFolder);
|
|
1257
|
+
if (!ctx.addWatchFolder) {
|
|
1258
|
+
return JSON.stringify({ error: "Watch folder management not available" });
|
|
1259
|
+
}
|
|
1260
|
+
const result = await ctx.addWatchFolder(folderPath);
|
|
1261
|
+
if (result.added) {
|
|
1262
|
+
return JSON.stringify({ success: true, added: result.path || folderPath, total: getWatchFolders(ctx.config).length });
|
|
1263
|
+
}
|
|
1264
|
+
return JSON.stringify({ error: result.error || "Could not add folder" });
|
|
1265
|
+
}
|
|
1266
|
+
if (action === "remove") {
|
|
1267
|
+
if (!rawFolder) return JSON.stringify({ error: "Missing folderPath" });
|
|
1268
|
+
const folderPath = normalizePath(rawFolder);
|
|
1269
|
+
if (!ctx.removeWatchFolder) {
|
|
1270
|
+
return JSON.stringify({ error: "Watch folder management not available" });
|
|
1271
|
+
}
|
|
1272
|
+
const result = ctx.removeWatchFolder(folderPath);
|
|
1273
|
+
if (result.removed) {
|
|
1274
|
+
return JSON.stringify({ success: true, removed: folderPath, total: getWatchFolders(ctx.config).length });
|
|
1275
|
+
}
|
|
1276
|
+
return JSON.stringify({ error: result.error || "Could not remove folder" });
|
|
1277
|
+
}
|
|
1278
|
+
return JSON.stringify({ error: `Unknown action: ${action}. Use add, remove, or list.` });
|
|
1279
|
+
}
|
|
1079
1280
|
default:
|
|
1080
1281
|
return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
1081
1282
|
}
|
|
@@ -1443,8 +1644,26 @@ function startAIAgent(ctx) {
|
|
|
1443
1644
|
}
|
|
1444
1645
|
};
|
|
1445
1646
|
let currentSlashArgs = "";
|
|
1647
|
+
let lastPendingCount = 0;
|
|
1446
1648
|
async function promptLoop() {
|
|
1447
1649
|
while (true) {
|
|
1650
|
+
if (pendingFiles.length > 0 && pendingFiles.length !== lastPendingCount) {
|
|
1651
|
+
lastPendingCount = pendingFiles.length;
|
|
1652
|
+
console.log("");
|
|
1653
|
+
console.log(chalk2.yellow(` ${pendingFiles.length} file(s) need case mapping:`));
|
|
1654
|
+
for (const f of pendingFiles) {
|
|
1655
|
+
const name = basename4(f.filePath || "unknown");
|
|
1656
|
+
if (f.suggestion) {
|
|
1657
|
+
console.log(
|
|
1658
|
+
` ${chalk2.cyan(name)} ${chalk2.dim("in")} ${f.folderName} ${chalk2.dim("\u2014 AI suggests:")} ${chalk2.green(f.suggestion.caseName)}`
|
|
1659
|
+
);
|
|
1660
|
+
} else {
|
|
1661
|
+
console.log(` ${chalk2.cyan(name)} ${chalk2.dim("in")} ${f.folderName} ${chalk2.dim("\u2014 no match found")}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
console.log(chalk2.dim(" Tell me which case, or say 'yes' to accept suggestions."));
|
|
1665
|
+
console.log("");
|
|
1666
|
+
}
|
|
1448
1667
|
let input;
|
|
1449
1668
|
try {
|
|
1450
1669
|
input = await rl.question(PROMPT);
|
|
@@ -1495,6 +1714,7 @@ function startAIAgent(ctx) {
|
|
|
1495
1714
|
while (history.length > 21) {
|
|
1496
1715
|
history.splice(1, 1);
|
|
1497
1716
|
}
|
|
1717
|
+
history[0] = { role: "system", content: buildSystemPrompt(ctx.config) };
|
|
1498
1718
|
try {
|
|
1499
1719
|
const response = await agentTurn([...history], ctx);
|
|
1500
1720
|
if (response) {
|
|
@@ -1510,6 +1730,92 @@ function startAIAgent(ctx) {
|
|
|
1510
1730
|
void promptLoop();
|
|
1511
1731
|
}
|
|
1512
1732
|
|
|
1733
|
+
// src/smart-mapper.ts
|
|
1734
|
+
async function smartMapFolder(config, folderName, fileName, cases) {
|
|
1735
|
+
if (cases.length === 0) return null;
|
|
1736
|
+
let token;
|
|
1737
|
+
try {
|
|
1738
|
+
token = await getAccessToken(config);
|
|
1739
|
+
} catch {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
const caseList = cases.map((c, i) => `${i + 1}. ${c.caseNumber}: ${c.caseName}`).join("\n");
|
|
1743
|
+
const messages = [
|
|
1744
|
+
{
|
|
1745
|
+
role: "system",
|
|
1746
|
+
content: "You are a legal file organizer. Match files to legal cases. Respond ONLY with a JSON object. No markdown, no explanation outside JSON."
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
role: "user",
|
|
1750
|
+
content: `Match this file to one of the cases below.
|
|
1751
|
+
|
|
1752
|
+
File: ${fileName}
|
|
1753
|
+
Folder: ${folderName}
|
|
1754
|
+
|
|
1755
|
+
Cases:
|
|
1756
|
+
${caseList}
|
|
1757
|
+
|
|
1758
|
+
Respond with ONLY valid JSON:
|
|
1759
|
+
If a case matches: {"caseIndex": <number 1-${cases.length}>, "confidence": "high" or "low", "reason": "<10 words max>"}
|
|
1760
|
+
If no case matches: {"caseIndex": null, "confidence": "none", "reason": "<why>"}`
|
|
1761
|
+
}
|
|
1762
|
+
];
|
|
1763
|
+
try {
|
|
1764
|
+
const response = await fetch(`${config.apiUrl}/api/sync/chat`, {
|
|
1765
|
+
method: "POST",
|
|
1766
|
+
headers: {
|
|
1767
|
+
Authorization: `Bearer ${token}`,
|
|
1768
|
+
"Content-Type": "application/json"
|
|
1769
|
+
},
|
|
1770
|
+
body: JSON.stringify({ messages }),
|
|
1771
|
+
signal: AbortSignal.timeout(15e3)
|
|
1772
|
+
});
|
|
1773
|
+
if (!response.ok) return null;
|
|
1774
|
+
let text = "";
|
|
1775
|
+
const reader = response.body.getReader();
|
|
1776
|
+
const decoder = new TextDecoder();
|
|
1777
|
+
while (true) {
|
|
1778
|
+
const { done, value } = await reader.read();
|
|
1779
|
+
if (done) break;
|
|
1780
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1781
|
+
for (const line of chunk.split("\n")) {
|
|
1782
|
+
const trimmed = line.trim();
|
|
1783
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
1784
|
+
const data = trimmed.slice(6);
|
|
1785
|
+
if (data === "[DONE]") continue;
|
|
1786
|
+
try {
|
|
1787
|
+
const parsed = JSON.parse(data);
|
|
1788
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
1789
|
+
if (content) text += content;
|
|
1790
|
+
} catch {
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
text = text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
1795
|
+
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
1796
|
+
if (!jsonMatch) return null;
|
|
1797
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
1798
|
+
if (result.caseIndex == null || result.confidence === "none") {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
const idx = result.caseIndex - 1;
|
|
1802
|
+
if (idx < 0 || idx >= cases.length) return null;
|
|
1803
|
+
const matched = cases[idx];
|
|
1804
|
+
return {
|
|
1805
|
+
caseId: matched.id,
|
|
1806
|
+
caseName: matched.caseName,
|
|
1807
|
+
caseNumber: matched.caseNumber,
|
|
1808
|
+
confidence: result.confidence === "high" ? "high" : "low",
|
|
1809
|
+
reason: result.reason || ""
|
|
1810
|
+
};
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
log.debug(
|
|
1813
|
+
`Smart mapper error: ${err instanceof Error ? err.message : String(err)}`
|
|
1814
|
+
);
|
|
1815
|
+
return null;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1513
1819
|
// src/watcher.ts
|
|
1514
1820
|
async function scanFolder(config) {
|
|
1515
1821
|
const folders = getWatchFolders(config);
|
|
@@ -1610,10 +1916,11 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
1610
1916
|
}
|
|
1611
1917
|
async function startWatching(config) {
|
|
1612
1918
|
const folders = getWatchFolders(config);
|
|
1919
|
+
const smartMapper = async (folderName, fileName, caseList) => smartMapFolder(config, folderName, fileName, caseList);
|
|
1613
1920
|
let cases = await listCases(config);
|
|
1614
1921
|
const { scanned, queued } = await scanFolder(config);
|
|
1615
1922
|
if (queued > 0) {
|
|
1616
|
-
const result = await processQueue(config, cases);
|
|
1923
|
+
const result = await processQueue(config, cases, { smartMapper });
|
|
1617
1924
|
if (result.failed > 0) {
|
|
1618
1925
|
log.warn(`Initial sync: ${result.uploaded} uploaded, ${result.failed} failed`);
|
|
1619
1926
|
}
|
|
@@ -1630,7 +1937,7 @@ async function startWatching(config) {
|
|
|
1630
1937
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1631
1938
|
debounceTimer = setTimeout(async () => {
|
|
1632
1939
|
if (queueSize() > 0) {
|
|
1633
|
-
const result = await processQueue(config, cases);
|
|
1940
|
+
const result = await processQueue(config, cases, { smartMapper });
|
|
1634
1941
|
if (result.uploaded > 0 || result.failed > 0) {
|
|
1635
1942
|
log.info(
|
|
1636
1943
|
`Batch: ${result.uploaded} uploaded, ${result.failed} failed`
|
|
@@ -1690,7 +1997,7 @@ async function startWatching(config) {
|
|
|
1690
1997
|
const result = await scanFolder(config);
|
|
1691
1998
|
log.info(`Scanned ${result.scanned} files, ${result.queued} need syncing`);
|
|
1692
1999
|
if (result.queued > 0) {
|
|
1693
|
-
const syncResult = await processQueue(config, cases);
|
|
2000
|
+
const syncResult = await processQueue(config, cases, { smartMapper });
|
|
1694
2001
|
log.info(`Synced: ${syncResult.uploaded} uploaded, ${syncResult.failed} failed`);
|
|
1695
2002
|
}
|
|
1696
2003
|
},
|