@anraktech/sync 0.2.1 → 0.4.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 +302 -131
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -418,9 +418,6 @@ function resetCache() {
|
|
|
418
418
|
import { watch } from "chokidar";
|
|
419
419
|
import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
|
|
420
420
|
import { join as join2, relative as relative2, basename as basename4, resolve } from "path";
|
|
421
|
-
import { createInterface } from "readline/promises";
|
|
422
|
-
import { stdin, stdout } from "process";
|
|
423
|
-
import chalk2 from "chalk";
|
|
424
421
|
|
|
425
422
|
// src/uploader.ts
|
|
426
423
|
import { stat as stat2 } from "fs/promises";
|
|
@@ -619,6 +616,300 @@ function sleep(ms) {
|
|
|
619
616
|
return new Promise((r) => setTimeout(r, ms));
|
|
620
617
|
}
|
|
621
618
|
|
|
619
|
+
// src/agent.ts
|
|
620
|
+
import { createInterface } from "readline/promises";
|
|
621
|
+
import { stdin, stdout } from "process";
|
|
622
|
+
import chalk2 from "chalk";
|
|
623
|
+
var TOOLS = [
|
|
624
|
+
{
|
|
625
|
+
type: "function",
|
|
626
|
+
function: {
|
|
627
|
+
name: "scan_folder",
|
|
628
|
+
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.",
|
|
629
|
+
parameters: {
|
|
630
|
+
type: "object",
|
|
631
|
+
properties: {
|
|
632
|
+
folderPath: {
|
|
633
|
+
type: "string",
|
|
634
|
+
description: "Absolute path to the folder, e.g. /Users/name/Downloads"
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
required: ["folderPath"]
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
type: "function",
|
|
643
|
+
function: {
|
|
644
|
+
name: "list_cases",
|
|
645
|
+
description: "List all legal cases on the server with case numbers, names, and document counts.",
|
|
646
|
+
parameters: { type: "object", properties: {} }
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
type: "function",
|
|
651
|
+
function: {
|
|
652
|
+
name: "get_status",
|
|
653
|
+
description: "Show sync stats: files tracked, synced, pending, errors, queue size.",
|
|
654
|
+
parameters: { type: "object", properties: {} }
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
type: "function",
|
|
659
|
+
function: {
|
|
660
|
+
name: "get_mappings",
|
|
661
|
+
description: "Show which local folders are mapped to which legal cases.",
|
|
662
|
+
parameters: { type: "object", properties: {} }
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
type: "function",
|
|
667
|
+
function: {
|
|
668
|
+
name: "refresh_cases",
|
|
669
|
+
description: "Refresh the case list from the server.",
|
|
670
|
+
parameters: { type: "object", properties: {} }
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
type: "function",
|
|
675
|
+
function: {
|
|
676
|
+
name: "rescan_watch_folder",
|
|
677
|
+
description: "Re-scan the watch folder for new or changed files and sync them.",
|
|
678
|
+
parameters: { type: "object", properties: {} }
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
];
|
|
682
|
+
function buildSystemPrompt(config) {
|
|
683
|
+
return `You are AnrakLegal Sync, a terminal assistant that helps lawyers sync local files to their case management system.
|
|
684
|
+
|
|
685
|
+
Watch folder: ${config.watchFolder}
|
|
686
|
+
Server: ${config.apiUrl}
|
|
687
|
+
|
|
688
|
+
You can scan folders, list cases, show sync status, and more using your tools.
|
|
689
|
+
|
|
690
|
+
Rules:
|
|
691
|
+
- Be concise. This is a terminal \u2014 1-3 lines unless showing a list.
|
|
692
|
+
- Do NOT use markdown. Plain text only.
|
|
693
|
+
- When user mentions a folder like "downloads" or "desktop", expand to full path (macOS: /Users/<name>/Downloads, Windows: C:\\Users\\<name>\\Downloads).
|
|
694
|
+
- Call tools to perform actions. Summarize results naturally after.
|
|
695
|
+
- Do NOT use thinking tags or reasoning tags in your output.`;
|
|
696
|
+
}
|
|
697
|
+
async function executeTool(name, args, ctx) {
|
|
698
|
+
switch (name) {
|
|
699
|
+
case "scan_folder": {
|
|
700
|
+
const folderPath = args.folderPath;
|
|
701
|
+
if (!folderPath) return JSON.stringify({ error: "Missing folderPath" });
|
|
702
|
+
try {
|
|
703
|
+
await ctx.scanFolder(folderPath);
|
|
704
|
+
const stats = getStats();
|
|
705
|
+
return JSON.stringify({
|
|
706
|
+
success: true,
|
|
707
|
+
folderPath,
|
|
708
|
+
totalFiles: stats.totalFiles,
|
|
709
|
+
synced: stats.synced,
|
|
710
|
+
pending: stats.pending,
|
|
711
|
+
errors: stats.errors
|
|
712
|
+
});
|
|
713
|
+
} catch (err) {
|
|
714
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
case "list_cases": {
|
|
718
|
+
await ctx.refreshCases();
|
|
719
|
+
const cases = ctx.getCases();
|
|
720
|
+
return JSON.stringify(
|
|
721
|
+
cases.map((c) => ({
|
|
722
|
+
caseNumber: c.caseNumber,
|
|
723
|
+
caseName: c.caseName,
|
|
724
|
+
status: c.status,
|
|
725
|
+
documents: c.documents?.length ?? 0
|
|
726
|
+
}))
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
case "get_status": {
|
|
730
|
+
const stats = getStats();
|
|
731
|
+
return JSON.stringify({ ...stats, queueSize: queueSize() });
|
|
732
|
+
}
|
|
733
|
+
case "get_mappings": {
|
|
734
|
+
const mappings = getAllMappings();
|
|
735
|
+
return JSON.stringify(
|
|
736
|
+
Object.entries(mappings).map(([folder, m]) => ({
|
|
737
|
+
folder,
|
|
738
|
+
caseNumber: m.caseNumber,
|
|
739
|
+
caseName: m.caseName
|
|
740
|
+
}))
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
case "refresh_cases": {
|
|
744
|
+
await ctx.refreshCases();
|
|
745
|
+
return JSON.stringify({ success: true, caseCount: ctx.getCases().length });
|
|
746
|
+
}
|
|
747
|
+
case "rescan_watch_folder": {
|
|
748
|
+
await ctx.triggerScan();
|
|
749
|
+
const stats = getStats();
|
|
750
|
+
return JSON.stringify({
|
|
751
|
+
success: true,
|
|
752
|
+
totalFiles: stats.totalFiles,
|
|
753
|
+
synced: stats.synced,
|
|
754
|
+
pending: stats.pending
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
default:
|
|
758
|
+
return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function* parseSSE(response) {
|
|
762
|
+
const reader = response.body.getReader();
|
|
763
|
+
const decoder = new TextDecoder();
|
|
764
|
+
let buffer = "";
|
|
765
|
+
while (true) {
|
|
766
|
+
const { done, value } = await reader.read();
|
|
767
|
+
if (done) break;
|
|
768
|
+
buffer += decoder.decode(value, { stream: true });
|
|
769
|
+
const lines = buffer.split("\n");
|
|
770
|
+
buffer = lines.pop();
|
|
771
|
+
for (const line of lines) {
|
|
772
|
+
const trimmed = line.trim();
|
|
773
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
774
|
+
const data = trimmed.slice(6);
|
|
775
|
+
if (data === "[DONE]") return;
|
|
776
|
+
try {
|
|
777
|
+
const parsed = JSON.parse(data);
|
|
778
|
+
const choice = parsed.choices?.[0];
|
|
779
|
+
if (choice?.delta) {
|
|
780
|
+
yield { delta: choice.delta, finishReason: choice.finish_reason ?? null };
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async function agentTurn(messages, ctx) {
|
|
788
|
+
const token = await getAccessToken(ctx.config);
|
|
789
|
+
const response = await fetch(`${ctx.config.apiUrl}/api/sync/chat`, {
|
|
790
|
+
method: "POST",
|
|
791
|
+
headers: {
|
|
792
|
+
Authorization: `Bearer ${token}`,
|
|
793
|
+
"Content-Type": "application/json"
|
|
794
|
+
},
|
|
795
|
+
body: JSON.stringify({ messages, tools: TOOLS })
|
|
796
|
+
});
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
const body = await response.text().catch(() => "");
|
|
799
|
+
throw new Error(`Server error (${response.status}): ${body.slice(0, 200)}`);
|
|
800
|
+
}
|
|
801
|
+
let textContent = "";
|
|
802
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
803
|
+
let isFirstText = true;
|
|
804
|
+
for await (const { delta } of parseSSE(response)) {
|
|
805
|
+
if (delta.content) {
|
|
806
|
+
if (isFirstText) {
|
|
807
|
+
process.stdout.write(" ");
|
|
808
|
+
isFirstText = false;
|
|
809
|
+
}
|
|
810
|
+
const cleaned = delta.content.replace(/<\/?think>/g, "");
|
|
811
|
+
if (cleaned) {
|
|
812
|
+
process.stdout.write(cleaned);
|
|
813
|
+
textContent += cleaned;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (delta.tool_calls) {
|
|
817
|
+
for (const tc of delta.tool_calls) {
|
|
818
|
+
const existing = toolCalls.get(tc.index);
|
|
819
|
+
if (existing) {
|
|
820
|
+
if (tc.function?.arguments) existing.args += tc.function.arguments;
|
|
821
|
+
} else {
|
|
822
|
+
toolCalls.set(tc.index, {
|
|
823
|
+
id: tc.id ?? `call_${tc.index}`,
|
|
824
|
+
name: tc.function?.name ?? "",
|
|
825
|
+
args: tc.function?.arguments ?? ""
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (textContent) {
|
|
832
|
+
process.stdout.write("\n");
|
|
833
|
+
}
|
|
834
|
+
if (toolCalls.size === 0) {
|
|
835
|
+
return textContent;
|
|
836
|
+
}
|
|
837
|
+
const assistantMsg = {
|
|
838
|
+
role: "assistant",
|
|
839
|
+
content: textContent || null,
|
|
840
|
+
tool_calls: Array.from(toolCalls.values()).map((tc) => ({
|
|
841
|
+
id: tc.id,
|
|
842
|
+
type: "function",
|
|
843
|
+
function: { name: tc.name, arguments: tc.args }
|
|
844
|
+
}))
|
|
845
|
+
};
|
|
846
|
+
messages.push(assistantMsg);
|
|
847
|
+
for (const tc of toolCalls.values()) {
|
|
848
|
+
process.stdout.write(chalk2.dim(` \u27F3 ${tc.name}...
|
|
849
|
+
`));
|
|
850
|
+
let args = {};
|
|
851
|
+
try {
|
|
852
|
+
args = JSON.parse(tc.args || "{}");
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
const result = await executeTool(tc.name, args, ctx);
|
|
856
|
+
messages.push({ role: "tool", content: result, tool_call_id: tc.id });
|
|
857
|
+
}
|
|
858
|
+
return agentTurn(messages, ctx);
|
|
859
|
+
}
|
|
860
|
+
function startAIAgent(ctx) {
|
|
861
|
+
const history = [
|
|
862
|
+
{ role: "system", content: buildSystemPrompt(ctx.config) }
|
|
863
|
+
];
|
|
864
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
865
|
+
console.log("");
|
|
866
|
+
console.log(chalk2.bold(" AnrakLegal Sync"));
|
|
867
|
+
console.log(chalk2.dim(` Watching ${ctx.config.watchFolder}`));
|
|
868
|
+
console.log(chalk2.dim(` Connected to ${ctx.config.apiUrl}`));
|
|
869
|
+
console.log("");
|
|
870
|
+
console.log(
|
|
871
|
+
chalk2.dim(" Ask me anything \u2014 scan folders, check cases, show status.")
|
|
872
|
+
);
|
|
873
|
+
console.log(chalk2.dim(' Type "quit" to exit.\n'));
|
|
874
|
+
const PROMPT = `${chalk2.bold.blue("\u276F")} `;
|
|
875
|
+
async function promptLoop() {
|
|
876
|
+
while (true) {
|
|
877
|
+
let input;
|
|
878
|
+
try {
|
|
879
|
+
input = await rl.question(PROMPT);
|
|
880
|
+
} catch {
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
const trimmed = input.trim();
|
|
884
|
+
if (!trimmed) continue;
|
|
885
|
+
if (/^(quit|exit|q)$/i.test(trimmed)) {
|
|
886
|
+
process.emit("SIGINT");
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (/^(clear|reset|new)$/i.test(trimmed)) {
|
|
890
|
+
history.length = 1;
|
|
891
|
+
console.log(chalk2.dim(" Conversation cleared.\n"));
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
history.push({ role: "user", content: trimmed });
|
|
895
|
+
while (history.length > 21) {
|
|
896
|
+
history.splice(1, 1);
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const response = await agentTurn([...history], ctx);
|
|
900
|
+
if (response) {
|
|
901
|
+
history.push({ role: "assistant", content: response });
|
|
902
|
+
}
|
|
903
|
+
} catch (err) {
|
|
904
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
905
|
+
console.log(chalk2.red(` Error: ${msg}`));
|
|
906
|
+
}
|
|
907
|
+
console.log("");
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
void promptLoop();
|
|
911
|
+
}
|
|
912
|
+
|
|
622
913
|
// src/watcher.ts
|
|
623
914
|
async function scanFolder(config) {
|
|
624
915
|
const folder = config.watchFolder;
|
|
@@ -714,129 +1005,6 @@ async function scanExternalFolder(config, folderPath, cases) {
|
|
|
714
1005
|
log.success("Everything up to date");
|
|
715
1006
|
}
|
|
716
1007
|
}
|
|
717
|
-
function startInteractiveAgent(config, getCases, refreshCases, triggerScan) {
|
|
718
|
-
const PROMPT = chalk2.blue("anrak> ");
|
|
719
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
720
|
-
function showHelp() {
|
|
721
|
-
console.log("");
|
|
722
|
-
console.log(chalk2.bold(" Available commands:"));
|
|
723
|
-
console.log("");
|
|
724
|
-
console.log(` ${chalk2.cyan("scan <folder>")} Scan a folder and sync its files to a case`);
|
|
725
|
-
console.log(` ${chalk2.cyan("rescan")} Re-scan the watch folder for new files`);
|
|
726
|
-
console.log(` ${chalk2.cyan("cases")} List cases on the server`);
|
|
727
|
-
console.log(` ${chalk2.cyan("status")} Show sync statistics`);
|
|
728
|
-
console.log(` ${chalk2.cyan("mappings")} Show folder \u2192 case mappings`);
|
|
729
|
-
console.log(` ${chalk2.cyan("refresh")} Refresh cases from server`);
|
|
730
|
-
console.log(` ${chalk2.cyan("help")} Show this help`);
|
|
731
|
-
console.log(` ${chalk2.cyan("quit")} Stop syncing and exit`);
|
|
732
|
-
console.log("");
|
|
733
|
-
}
|
|
734
|
-
async function handleCommand(input) {
|
|
735
|
-
const trimmed = input.trim();
|
|
736
|
-
if (!trimmed) return;
|
|
737
|
-
const [cmd, ...args] = trimmed.split(/\s+/);
|
|
738
|
-
const arg = args.join(" ");
|
|
739
|
-
switch (cmd.toLowerCase()) {
|
|
740
|
-
case "scan": {
|
|
741
|
-
if (!arg) {
|
|
742
|
-
log.warn("Usage: scan <folder path>");
|
|
743
|
-
break;
|
|
744
|
-
}
|
|
745
|
-
await scanExternalFolder(config, arg, getCases());
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
case "rescan": {
|
|
749
|
-
await triggerScan();
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
case "cases": {
|
|
753
|
-
await refreshCases();
|
|
754
|
-
const cases = getCases();
|
|
755
|
-
if (cases.length === 0) {
|
|
756
|
-
log.info("No cases found on server");
|
|
757
|
-
} else {
|
|
758
|
-
console.log("");
|
|
759
|
-
for (const c of cases) {
|
|
760
|
-
const docCount = c.documents?.length ?? 0;
|
|
761
|
-
console.log(
|
|
762
|
-
` ${chalk2.cyan(c.caseNumber)} ${c.caseName} ${chalk2.dim(`(${docCount} docs)`)}`
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
console.log("");
|
|
766
|
-
}
|
|
767
|
-
break;
|
|
768
|
-
}
|
|
769
|
-
case "status": {
|
|
770
|
-
const stats = getStats();
|
|
771
|
-
console.log("");
|
|
772
|
-
console.log(` ${chalk2.bold("Watch folder:")} ${config.watchFolder}`);
|
|
773
|
-
console.log(` ${chalk2.bold("Files tracked:")} ${stats.totalFiles}`);
|
|
774
|
-
console.log(` ${chalk2.bold("Synced:")} ${chalk2.green(String(stats.synced))}`);
|
|
775
|
-
console.log(` ${chalk2.bold("Pending:")} ${chalk2.yellow(String(stats.pending))}`);
|
|
776
|
-
console.log(` ${chalk2.bold("Errors:")} ${chalk2.red(String(stats.errors))}`);
|
|
777
|
-
console.log(` ${chalk2.bold("Mapped folders:")} ${stats.mappedFolders}`);
|
|
778
|
-
console.log(` ${chalk2.bold("Queue:")} ${queueSize()}`);
|
|
779
|
-
console.log("");
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
case "mappings":
|
|
783
|
-
case "map": {
|
|
784
|
-
const mappings = getAllMappings();
|
|
785
|
-
const entries = Object.entries(mappings);
|
|
786
|
-
if (entries.length === 0) {
|
|
787
|
-
log.info("No folder mappings yet");
|
|
788
|
-
} else {
|
|
789
|
-
console.log("");
|
|
790
|
-
for (const [folder, m] of entries) {
|
|
791
|
-
console.log(
|
|
792
|
-
` ${chalk2.cyan(folder)} \u2192 ${m.caseNumber} ${chalk2.dim(`(${m.caseName})`)}`
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
console.log("");
|
|
796
|
-
}
|
|
797
|
-
break;
|
|
798
|
-
}
|
|
799
|
-
case "refresh": {
|
|
800
|
-
log.info("Refreshing cases from server...");
|
|
801
|
-
await refreshCases();
|
|
802
|
-
log.success(`Found ${getCases().length} case(s)`);
|
|
803
|
-
break;
|
|
804
|
-
}
|
|
805
|
-
case "help":
|
|
806
|
-
case "?": {
|
|
807
|
-
showHelp();
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
case "quit":
|
|
811
|
-
case "exit":
|
|
812
|
-
case "q": {
|
|
813
|
-
process.emit("SIGINT");
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
default: {
|
|
817
|
-
log.warn(`Unknown command: ${cmd}. Type ${chalk2.cyan("help")} for available commands.`);
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
showHelp();
|
|
823
|
-
async function promptLoop() {
|
|
824
|
-
while (true) {
|
|
825
|
-
let input;
|
|
826
|
-
try {
|
|
827
|
-
input = await rl.question(PROMPT);
|
|
828
|
-
} catch {
|
|
829
|
-
break;
|
|
830
|
-
}
|
|
831
|
-
try {
|
|
832
|
-
await handleCommand(input);
|
|
833
|
-
} catch (err) {
|
|
834
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
void promptLoop();
|
|
839
|
-
}
|
|
840
1008
|
async function startWatching(config) {
|
|
841
1009
|
const folder = config.watchFolder;
|
|
842
1010
|
log.info(`Scanning ${folder}...`);
|
|
@@ -904,13 +1072,13 @@ async function startWatching(config) {
|
|
|
904
1072
|
watcher.on("error", (err) => {
|
|
905
1073
|
log.error(`Watcher error: ${err instanceof Error ? err.message : String(err)}`);
|
|
906
1074
|
});
|
|
907
|
-
|
|
1075
|
+
startAIAgent({
|
|
908
1076
|
config,
|
|
909
|
-
() => cases,
|
|
910
|
-
async () => {
|
|
1077
|
+
getCases: () => cases,
|
|
1078
|
+
refreshCases: async () => {
|
|
911
1079
|
cases = await listCases(config);
|
|
912
1080
|
},
|
|
913
|
-
async () => {
|
|
1081
|
+
triggerScan: async () => {
|
|
914
1082
|
log.info(`Re-scanning ${folder}...`);
|
|
915
1083
|
const result = await scanFolder(config);
|
|
916
1084
|
log.info(`Scanned ${result.scanned} files, ${result.queued} need syncing`);
|
|
@@ -918,8 +1086,11 @@ async function startWatching(config) {
|
|
|
918
1086
|
const syncResult = await processQueue(config, cases);
|
|
919
1087
|
log.info(`Synced: ${syncResult.uploaded} uploaded, ${syncResult.failed} failed`);
|
|
920
1088
|
}
|
|
1089
|
+
},
|
|
1090
|
+
scanFolder: async (folderPath) => {
|
|
1091
|
+
await scanExternalFolder(config, folderPath, cases);
|
|
921
1092
|
}
|
|
922
|
-
);
|
|
1093
|
+
});
|
|
923
1094
|
const shutdown = () => {
|
|
924
1095
|
log.info("Shutting down...");
|
|
925
1096
|
clearInterval(refreshInterval);
|