@anraktech/sync 0.10.0 → 0.12.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 +331 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -619,7 +619,7 @@ function sleep(ms) {
|
|
|
619
619
|
import { createInterface } from "readline/promises";
|
|
620
620
|
import { stdin, stdout } from "process";
|
|
621
621
|
import { homedir as homedir2, platform } from "os";
|
|
622
|
-
import { resolve, join as join2, dirname as dirname2 } from "path";
|
|
622
|
+
import { resolve, join as join2, dirname as dirname2, basename as basename4 } from "path";
|
|
623
623
|
import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
|
|
624
624
|
import { fileURLToPath } from "url";
|
|
625
625
|
import chalk2 from "chalk";
|
|
@@ -663,11 +663,57 @@ function normalizePath(folderPath) {
|
|
|
663
663
|
return resolve(HOME, trimmed);
|
|
664
664
|
}
|
|
665
665
|
var TOOLS = [
|
|
666
|
+
{
|
|
667
|
+
type: "function",
|
|
668
|
+
function: {
|
|
669
|
+
name: "search_files",
|
|
670
|
+
description: "Search for files by name across one or more directories. Supports partial matching and glob patterns. Returns full paths, sizes, and whether files are syncable. Use this when user asks to find a specific file.",
|
|
671
|
+
parameters: {
|
|
672
|
+
type: "object",
|
|
673
|
+
properties: {
|
|
674
|
+
query: {
|
|
675
|
+
type: "string",
|
|
676
|
+
description: "Search query \u2014 filename or partial name to match (case-insensitive). Examples: 'rental agreement', 'Black & White', '.pdf'"
|
|
677
|
+
},
|
|
678
|
+
searchPath: {
|
|
679
|
+
type: "string",
|
|
680
|
+
description: "Directory to search in. Defaults to home directory. Use absolute paths."
|
|
681
|
+
},
|
|
682
|
+
maxDepth: {
|
|
683
|
+
type: "number",
|
|
684
|
+
description: "Maximum directory depth to search. Default: 3. Increase for deeper searches."
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
required: ["query"]
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
type: "function",
|
|
693
|
+
function: {
|
|
694
|
+
name: "upload_to_case",
|
|
695
|
+
description: "Upload a specific file to a specific legal case. Use when the user wants to upload a particular file to a particular case. Requires the full file path and case identifier (case number or name).",
|
|
696
|
+
parameters: {
|
|
697
|
+
type: "object",
|
|
698
|
+
properties: {
|
|
699
|
+
filePath: {
|
|
700
|
+
type: "string",
|
|
701
|
+
description: "Absolute path to the file to upload"
|
|
702
|
+
},
|
|
703
|
+
caseIdentifier: {
|
|
704
|
+
type: "string",
|
|
705
|
+
description: "Case number (e.g. 'CASE-001') or case name to upload to. Will fuzzy-match against available cases."
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
required: ["filePath", "caseIdentifier"]
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
},
|
|
666
712
|
{
|
|
667
713
|
type: "function",
|
|
668
714
|
function: {
|
|
669
715
|
name: "browse_folder",
|
|
670
|
-
description: "List files and subfolders in a local directory. Shows file names, sizes, types, and whether they are syncable. Use
|
|
716
|
+
description: "List files and subfolders in a local directory. Shows file names, sizes, types, and whether they are syncable. Use when user wants to see what's in a folder. For finding a specific file, use search_files instead.",
|
|
671
717
|
parameters: {
|
|
672
718
|
type: "object",
|
|
673
719
|
properties: {
|
|
@@ -679,6 +725,10 @@ var TOOLS = [
|
|
|
679
725
|
type: "string",
|
|
680
726
|
enum: ["all", "syncable", "folders"],
|
|
681
727
|
description: "Filter: 'all' shows everything, 'syncable' shows only supported file types (PDF, DOCX, etc.), 'folders' shows only subfolders. Default: all"
|
|
728
|
+
},
|
|
729
|
+
nameFilter: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Optional: filter items by name (case-insensitive substring match). Use to narrow down large folders."
|
|
682
732
|
}
|
|
683
733
|
},
|
|
684
734
|
required: ["folderPath"]
|
|
@@ -689,7 +739,7 @@ var TOOLS = [
|
|
|
689
739
|
type: "function",
|
|
690
740
|
function: {
|
|
691
741
|
name: "scan_folder",
|
|
692
|
-
description: "Sync
|
|
742
|
+
description: "Sync ALL supported files from a local folder to a matching legal case. Use ONLY when user explicitly asks to sync an entire folder. For uploading a single file, use upload_to_case instead.",
|
|
693
743
|
parameters: {
|
|
694
744
|
type: "object",
|
|
695
745
|
properties: {
|
|
@@ -761,8 +811,10 @@ Rules:
|
|
|
761
811
|
"desktop" \u2192 ${join2(HOME, "Desktop")}
|
|
762
812
|
"documents" \u2192 ${join2(HOME, "Documents")}
|
|
763
813
|
"~/SomeFolder" \u2192 ${HOME}/SomeFolder
|
|
764
|
-
- When user
|
|
765
|
-
- When user
|
|
814
|
+
- FINDING FILES: When user asks to find or locate a specific file, ALWAYS use search_files. NEVER browse a folder hoping to find it.
|
|
815
|
+
- UPLOADING: When user wants to upload a specific file to a case, use upload_to_case (NOT scan_folder). scan_folder uploads EVERYTHING.
|
|
816
|
+
- When user says "look at" or "check" a folder, use browse_folder to show what's there.
|
|
817
|
+
- When user says "sync all" or "sync this folder", use scan_folder.
|
|
766
818
|
- When browsing, highlight which files are syncable (PDF, DOCX, XLSX, etc.) vs not.
|
|
767
819
|
- Present file lists in a clean table/list format with names and sizes.
|
|
768
820
|
- Call tools to perform actions. Summarize results naturally after.
|
|
@@ -771,6 +823,117 @@ Rules:
|
|
|
771
823
|
}
|
|
772
824
|
async function executeTool(name, args, ctx) {
|
|
773
825
|
switch (name) {
|
|
826
|
+
case "search_files": {
|
|
827
|
+
let searchDir2 = function(dir, depth) {
|
|
828
|
+
if (depth > maxDepth || results.length >= 20) return;
|
|
829
|
+
let entries;
|
|
830
|
+
try {
|
|
831
|
+
entries = readdirSync(dir);
|
|
832
|
+
} catch {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
for (const entry of entries) {
|
|
836
|
+
if (results.length >= 20) break;
|
|
837
|
+
if (isIgnoredFile(entry)) continue;
|
|
838
|
+
const fullPath = join2(dir, entry);
|
|
839
|
+
let st;
|
|
840
|
+
try {
|
|
841
|
+
st = statSync(fullPath);
|
|
842
|
+
} catch {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (st.isDirectory()) {
|
|
846
|
+
searchDir2(fullPath, depth + 1);
|
|
847
|
+
} else if (st.isFile() && entry.toLowerCase().includes(queryLower)) {
|
|
848
|
+
const sizeKB = Math.round(st.size / 1024);
|
|
849
|
+
const size = sizeKB > 1024 ? `${(sizeKB / 1024).toFixed(1)} MB` : `${sizeKB} KB`;
|
|
850
|
+
results.push({
|
|
851
|
+
name: entry,
|
|
852
|
+
path: fullPath,
|
|
853
|
+
size,
|
|
854
|
+
syncable: isSupportedFile(entry)
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
var searchDir = searchDir2;
|
|
860
|
+
const query = (args.query || "").trim();
|
|
861
|
+
if (!query) return JSON.stringify({ error: "Missing search query" });
|
|
862
|
+
const rawSearch = args.searchPath || HOME;
|
|
863
|
+
const searchPath = normalizePath(rawSearch);
|
|
864
|
+
const maxDepth = Math.min(args.maxDepth || 3, 6);
|
|
865
|
+
if (!existsSync2(searchPath) || !statSync(searchPath).isDirectory()) {
|
|
866
|
+
return JSON.stringify({ error: `Search path not found: ${searchPath}` });
|
|
867
|
+
}
|
|
868
|
+
const queryLower = query.toLowerCase();
|
|
869
|
+
const results = [];
|
|
870
|
+
searchDir2(searchPath, 0);
|
|
871
|
+
return JSON.stringify({
|
|
872
|
+
query,
|
|
873
|
+
searchPath,
|
|
874
|
+
found: results.length,
|
|
875
|
+
maxResults: 20,
|
|
876
|
+
results
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
case "upload_to_case": {
|
|
880
|
+
const rawPath = args.filePath;
|
|
881
|
+
const caseId = args.caseIdentifier;
|
|
882
|
+
if (!rawPath) return JSON.stringify({ error: "Missing filePath" });
|
|
883
|
+
if (!caseId) return JSON.stringify({ error: "Missing caseIdentifier" });
|
|
884
|
+
const filePath = normalizePath(rawPath);
|
|
885
|
+
if (!existsSync2(filePath)) {
|
|
886
|
+
return JSON.stringify({ error: `File not found: ${filePath}` });
|
|
887
|
+
}
|
|
888
|
+
const fileStat = statSync(filePath);
|
|
889
|
+
if (!fileStat.isFile()) {
|
|
890
|
+
return JSON.stringify({ error: `Not a file: ${filePath}` });
|
|
891
|
+
}
|
|
892
|
+
if (fileStat.size > 50 * 1024 * 1024) {
|
|
893
|
+
return JSON.stringify({ error: `File exceeds 50MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)}MB)` });
|
|
894
|
+
}
|
|
895
|
+
if (!isSupportedFile(basename4(filePath))) {
|
|
896
|
+
return JSON.stringify({ error: `Unsupported file type: ${basename4(filePath)}. Supported: PDF, DOCX, XLSX, PPTX, TXT, images` });
|
|
897
|
+
}
|
|
898
|
+
await ctx.refreshCases();
|
|
899
|
+
const cases = ctx.getCases();
|
|
900
|
+
const searchLower = caseId.toLowerCase();
|
|
901
|
+
let matchedCase = cases.find(
|
|
902
|
+
(c) => c.caseNumber.toLowerCase() === searchLower || c.caseName.toLowerCase() === searchLower
|
|
903
|
+
);
|
|
904
|
+
if (!matchedCase) {
|
|
905
|
+
matchedCase = cases.find(
|
|
906
|
+
(c) => c.caseNumber.toLowerCase().includes(searchLower) || c.caseName.toLowerCase().includes(searchLower)
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (!matchedCase) {
|
|
910
|
+
return JSON.stringify({
|
|
911
|
+
error: `No case matching "${caseId}". Available cases: ${cases.map((c) => `${c.caseNumber} (${c.caseName})`).join(", ")}`
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
log.file("uploading", `${basename4(filePath)} -> ${matchedCase.caseName}`);
|
|
916
|
+
const result = await uploadFile(ctx.config, matchedCase.id, filePath);
|
|
917
|
+
if (result.success && result.linkedDocumentIds.length > 0) {
|
|
918
|
+
const hash = await hashFile(filePath);
|
|
919
|
+
markSynced(filePath, hash, fileStat.size, matchedCase.id, result.linkedDocumentIds[0]);
|
|
920
|
+
log.success(`Uploaded ${basename4(filePath)} to ${matchedCase.caseName}`);
|
|
921
|
+
return JSON.stringify({
|
|
922
|
+
success: true,
|
|
923
|
+
filename: basename4(filePath),
|
|
924
|
+
caseName: matchedCase.caseName,
|
|
925
|
+
caseNumber: matchedCase.caseNumber,
|
|
926
|
+
documentId: result.linkedDocumentIds[0]
|
|
927
|
+
});
|
|
928
|
+
} else {
|
|
929
|
+
return JSON.stringify({
|
|
930
|
+
error: `Upload completed but no document was linked. The file may be a duplicate.`
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
} catch (err) {
|
|
934
|
+
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
774
937
|
case "browse_folder": {
|
|
775
938
|
const rawPath = args.folderPath;
|
|
776
939
|
if (!rawPath) return JSON.stringify({ error: "Missing folderPath" });
|
|
@@ -787,10 +950,12 @@ async function executeTool(name, args, ctx) {
|
|
|
787
950
|
return JSON.stringify({ error: `Cannot access: ${folderPath}` });
|
|
788
951
|
}
|
|
789
952
|
const filter = args.filter || "all";
|
|
953
|
+
const nameFilter = (args.nameFilter || "").toLowerCase();
|
|
790
954
|
const entries = readdirSync(folderPath);
|
|
791
955
|
const items = [];
|
|
792
956
|
for (const entry of entries) {
|
|
793
957
|
if (isIgnoredFile(entry)) continue;
|
|
958
|
+
if (nameFilter && !entry.toLowerCase().includes(nameFilter)) continue;
|
|
794
959
|
const fullPath = join2(folderPath, entry);
|
|
795
960
|
let st;
|
|
796
961
|
try {
|
|
@@ -814,9 +979,9 @@ async function executeTool(name, args, ctx) {
|
|
|
814
979
|
return JSON.stringify({
|
|
815
980
|
folder: folderPath,
|
|
816
981
|
totalItems: items.length,
|
|
817
|
-
items: items.slice(0,
|
|
818
|
-
//
|
|
819
|
-
truncated: items.length >
|
|
982
|
+
items: items.slice(0, 100),
|
|
983
|
+
// Increased from 50 to 100
|
|
984
|
+
truncated: items.length > 100
|
|
820
985
|
});
|
|
821
986
|
}
|
|
822
987
|
case "scan_folder": {
|
|
@@ -1019,12 +1184,152 @@ function startAIAgent(ctx) {
|
|
|
1019
1184
|
console.log(row(`${chalk2.dim("Status")} ${statusLine}`));
|
|
1020
1185
|
}
|
|
1021
1186
|
console.log(empty);
|
|
1022
|
-
console.log(row(chalk2.dim("Type naturally
|
|
1023
|
-
console.log(row(chalk2.dim(
|
|
1187
|
+
console.log(row(chalk2.dim("Type naturally, or use / commands.")));
|
|
1188
|
+
console.log(row(chalk2.dim("Type /help to see all commands.")));
|
|
1024
1189
|
console.log(empty);
|
|
1025
1190
|
console.log(bot);
|
|
1026
1191
|
console.log("");
|
|
1027
1192
|
const PROMPT = `${chalk2.bold.blue("\u276F")} `;
|
|
1193
|
+
const slashCommands = {
|
|
1194
|
+
help: {
|
|
1195
|
+
description: "Show all commands",
|
|
1196
|
+
handler: () => {
|
|
1197
|
+
const W2 = Math.min(process.stdout.columns || 60, 60);
|
|
1198
|
+
console.log("");
|
|
1199
|
+
console.log(chalk2.bold(" Commands"));
|
|
1200
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(W2 - 4)));
|
|
1201
|
+
for (const [cmd, { description }] of Object.entries(slashCommands)) {
|
|
1202
|
+
const label = chalk2.cyan(`/${cmd}`);
|
|
1203
|
+
const dots = chalk2.dim(".".repeat(Math.max(2, 18 - cmd.length)));
|
|
1204
|
+
console.log(` ${label} ${dots} ${chalk2.dim(description)}`);
|
|
1205
|
+
}
|
|
1206
|
+
console.log("");
|
|
1207
|
+
console.log(chalk2.dim(" Or just type naturally \u2014 the AI understands plain English."));
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
cases: {
|
|
1211
|
+
description: "List your legal cases",
|
|
1212
|
+
handler: async () => {
|
|
1213
|
+
try {
|
|
1214
|
+
await ctx.refreshCases();
|
|
1215
|
+
const cases = ctx.getCases();
|
|
1216
|
+
if (cases.length === 0) {
|
|
1217
|
+
console.log(chalk2.dim(" No cases found."));
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
console.log("");
|
|
1221
|
+
console.log(chalk2.bold(` ${cases.length} case${cases.length !== 1 ? "s" : ""}`));
|
|
1222
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(40)));
|
|
1223
|
+
for (const c of cases) {
|
|
1224
|
+
const docs = c.documents?.length ?? 0;
|
|
1225
|
+
console.log(` ${chalk2.cyan(c.caseNumber)} ${c.caseName} ${chalk2.dim(`${docs} docs`)}`);
|
|
1226
|
+
}
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
console.log(chalk2.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
status: {
|
|
1233
|
+
description: "Show sync status",
|
|
1234
|
+
handler: () => {
|
|
1235
|
+
const s = getStats();
|
|
1236
|
+
const q = queueSize();
|
|
1237
|
+
console.log("");
|
|
1238
|
+
console.log(chalk2.bold(" Sync Status"));
|
|
1239
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(30)));
|
|
1240
|
+
console.log(` ${chalk2.dim("Files tracked")} ${s.totalFiles}`);
|
|
1241
|
+
console.log(` ${chalk2.dim("Synced")} ${chalk2.green(s.synced)}`);
|
|
1242
|
+
console.log(` ${chalk2.dim("Pending")} ${chalk2.yellow(s.pending)}`);
|
|
1243
|
+
console.log(` ${chalk2.dim("Errors")} ${s.errors > 0 ? chalk2.red(s.errors) : s.errors}`);
|
|
1244
|
+
console.log(` ${chalk2.dim("Queue")} ${q}`);
|
|
1245
|
+
console.log(` ${chalk2.dim("Mapped folders")} ${s.mappedFolders}`);
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
folder: {
|
|
1249
|
+
description: "Show watch folder path",
|
|
1250
|
+
handler: () => {
|
|
1251
|
+
console.log(` ${chalk2.dim("Watch folder")} ${ctx.config.watchFolder}`);
|
|
1252
|
+
console.log(` ${chalk2.dim("Config dir")} ${getConfigDir()}`);
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
mappings: {
|
|
1256
|
+
description: "Show folder \u2192 case mappings",
|
|
1257
|
+
handler: () => {
|
|
1258
|
+
const mappings = getAllMappings();
|
|
1259
|
+
const entries = Object.entries(mappings);
|
|
1260
|
+
if (entries.length === 0) {
|
|
1261
|
+
console.log(chalk2.dim(" No mappings yet. Sync a folder to create one."));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
console.log("");
|
|
1265
|
+
console.log(chalk2.bold(` ${entries.length} mapping${entries.length !== 1 ? "s" : ""}`));
|
|
1266
|
+
console.log(chalk2.dim(" " + "\u2500".repeat(40)));
|
|
1267
|
+
for (const [folder, m] of entries) {
|
|
1268
|
+
console.log(` ${chalk2.cyan(folder)} \u2192 ${m.caseNumber} ${chalk2.dim(`(${m.caseName})`)}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
login: {
|
|
1273
|
+
description: "Re-authenticate with AnrakLegal",
|
|
1274
|
+
handler: async () => {
|
|
1275
|
+
try {
|
|
1276
|
+
log.info("Opening browser for login...");
|
|
1277
|
+
const tokens = await browserLogin(ctx.config.apiUrl);
|
|
1278
|
+
ctx.config.accessToken = tokens.accessToken;
|
|
1279
|
+
ctx.config.refreshToken = tokens.refreshToken;
|
|
1280
|
+
saveConfig(ctx.config);
|
|
1281
|
+
log.success("Logged in successfully");
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
console.log(chalk2.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
logout: {
|
|
1288
|
+
description: "Clear stored credentials",
|
|
1289
|
+
handler: () => {
|
|
1290
|
+
ctx.config.accessToken = "";
|
|
1291
|
+
ctx.config.refreshToken = "";
|
|
1292
|
+
saveConfig(ctx.config);
|
|
1293
|
+
log.success("Logged out. Run /login to re-authenticate.");
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
sync: {
|
|
1297
|
+
description: "Re-scan watch folder and sync",
|
|
1298
|
+
handler: async () => {
|
|
1299
|
+
try {
|
|
1300
|
+
await ctx.triggerScan();
|
|
1301
|
+
const s = getStats();
|
|
1302
|
+
if (s.pending === 0) {
|
|
1303
|
+
log.success("Everything up to date");
|
|
1304
|
+
} else {
|
|
1305
|
+
log.info(`${s.pending} file${s.pending !== 1 ? "s" : ""} pending sync`);
|
|
1306
|
+
}
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
console.log(chalk2.red(` Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
clear: {
|
|
1313
|
+
description: "Clear conversation history",
|
|
1314
|
+
handler: () => {
|
|
1315
|
+
history.length = 1;
|
|
1316
|
+
console.log(chalk2.dim(" Conversation cleared."));
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
reset: {
|
|
1320
|
+
description: "Clear sync cache (re-uploads on next sync)",
|
|
1321
|
+
handler: () => {
|
|
1322
|
+
resetCache();
|
|
1323
|
+
log.success("Cache cleared. Run /sync to re-sync.");
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1326
|
+
quit: {
|
|
1327
|
+
description: "Exit AnrakLegal Sync",
|
|
1328
|
+
handler: () => {
|
|
1329
|
+
process.emit("SIGINT");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1028
1333
|
async function promptLoop() {
|
|
1029
1334
|
while (true) {
|
|
1030
1335
|
let input;
|
|
@@ -1035,15 +1340,26 @@ function startAIAgent(ctx) {
|
|
|
1035
1340
|
}
|
|
1036
1341
|
const trimmed = input.trim();
|
|
1037
1342
|
if (!trimmed) continue;
|
|
1343
|
+
if (trimmed.startsWith("/")) {
|
|
1344
|
+
const cmd = trimmed.slice(1).toLowerCase().split(/\s+/)[0];
|
|
1345
|
+
if (slashCommands[cmd]) {
|
|
1346
|
+
await slashCommands[cmd].handler();
|
|
1347
|
+
console.log("");
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const matches = Object.keys(slashCommands).filter((k) => k.startsWith(cmd));
|
|
1351
|
+
if (matches.length > 0) {
|
|
1352
|
+
console.log(chalk2.dim(` Did you mean: ${matches.map((m) => chalk2.cyan(`/${m}`)).join(", ")}?`));
|
|
1353
|
+
} else {
|
|
1354
|
+
console.log(chalk2.dim(` Unknown command. Type ${chalk2.cyan("/help")} for available commands.`));
|
|
1355
|
+
}
|
|
1356
|
+
console.log("");
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1038
1359
|
if (/^(quit|exit|q)$/i.test(trimmed)) {
|
|
1039
1360
|
process.emit("SIGINT");
|
|
1040
1361
|
return;
|
|
1041
1362
|
}
|
|
1042
|
-
if (/^(clear|reset|new)$/i.test(trimmed)) {
|
|
1043
|
-
history.length = 1;
|
|
1044
|
-
console.log(chalk2.dim(" Conversation cleared.\n"));
|
|
1045
|
-
continue;
|
|
1046
|
-
}
|
|
1047
1363
|
history.push({ role: "user", content: trimmed });
|
|
1048
1364
|
while (history.length > 21) {
|
|
1049
1365
|
history.splice(1, 1);
|