@agenticmail/core 0.9.24 → 0.9.26
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/chunk-J6JINNJ3.js +724 -0
- package/dist/index.cjs +1145 -929
- package/dist/index.d.cts +94 -9
- package/dist/index.d.ts +94 -9
- package/dist/index.js +179 -698
- package/dist/skills-RE3S767B.js +23 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -705,6 +705,746 @@ export default {
|
|
|
705
705
|
}
|
|
706
706
|
});
|
|
707
707
|
|
|
708
|
+
// src/memory/text-search.ts
|
|
709
|
+
function stem(word) {
|
|
710
|
+
if (word.length < 3) return word;
|
|
711
|
+
let stemmed = word;
|
|
712
|
+
for (const [pattern, replacement, minLen] of STEM_RULES) {
|
|
713
|
+
if (stemmed.length >= minLen && pattern.test(stemmed)) {
|
|
714
|
+
stemmed = stemmed.replace(pattern, replacement);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
|
|
719
|
+
stemmed = stemmed.slice(0, -1);
|
|
720
|
+
}
|
|
721
|
+
return stemmed;
|
|
722
|
+
}
|
|
723
|
+
function tokenize(text) {
|
|
724
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
|
|
725
|
+
}
|
|
726
|
+
var BM25_K1, BM25_B, FIELD_WEIGHT_TITLE, FIELD_WEIGHT_TAGS, FIELD_WEIGHT_CONTENT, PREFIX_MATCH_PENALTY, STOP_WORDS, STEM_RULES, DOUBLE_CONSONANT, MemorySearchIndex;
|
|
727
|
+
var init_text_search = __esm({
|
|
728
|
+
"src/memory/text-search.ts"() {
|
|
729
|
+
"use strict";
|
|
730
|
+
BM25_K1 = 1.2;
|
|
731
|
+
BM25_B = 0.75;
|
|
732
|
+
FIELD_WEIGHT_TITLE = 3;
|
|
733
|
+
FIELD_WEIGHT_TAGS = 2;
|
|
734
|
+
FIELD_WEIGHT_CONTENT = 1;
|
|
735
|
+
PREFIX_MATCH_PENALTY = 0.7;
|
|
736
|
+
STOP_WORDS = /* @__PURE__ */ new Set([
|
|
737
|
+
"a",
|
|
738
|
+
"about",
|
|
739
|
+
"above",
|
|
740
|
+
"after",
|
|
741
|
+
"again",
|
|
742
|
+
"against",
|
|
743
|
+
"all",
|
|
744
|
+
"am",
|
|
745
|
+
"an",
|
|
746
|
+
"and",
|
|
747
|
+
"any",
|
|
748
|
+
"are",
|
|
749
|
+
"as",
|
|
750
|
+
"at",
|
|
751
|
+
"be",
|
|
752
|
+
"because",
|
|
753
|
+
"been",
|
|
754
|
+
"before",
|
|
755
|
+
"being",
|
|
756
|
+
"below",
|
|
757
|
+
"between",
|
|
758
|
+
"both",
|
|
759
|
+
"but",
|
|
760
|
+
"by",
|
|
761
|
+
"can",
|
|
762
|
+
"could",
|
|
763
|
+
"did",
|
|
764
|
+
"do",
|
|
765
|
+
"does",
|
|
766
|
+
"doing",
|
|
767
|
+
"down",
|
|
768
|
+
"during",
|
|
769
|
+
"each",
|
|
770
|
+
"either",
|
|
771
|
+
"every",
|
|
772
|
+
"few",
|
|
773
|
+
"for",
|
|
774
|
+
"from",
|
|
775
|
+
"further",
|
|
776
|
+
"get",
|
|
777
|
+
"got",
|
|
778
|
+
"had",
|
|
779
|
+
"has",
|
|
780
|
+
"have",
|
|
781
|
+
"having",
|
|
782
|
+
"he",
|
|
783
|
+
"her",
|
|
784
|
+
"here",
|
|
785
|
+
"hers",
|
|
786
|
+
"herself",
|
|
787
|
+
"him",
|
|
788
|
+
"himself",
|
|
789
|
+
"his",
|
|
790
|
+
"how",
|
|
791
|
+
"i",
|
|
792
|
+
"if",
|
|
793
|
+
"in",
|
|
794
|
+
"into",
|
|
795
|
+
"is",
|
|
796
|
+
"it",
|
|
797
|
+
"its",
|
|
798
|
+
"itself",
|
|
799
|
+
"just",
|
|
800
|
+
"may",
|
|
801
|
+
"me",
|
|
802
|
+
"might",
|
|
803
|
+
"more",
|
|
804
|
+
"most",
|
|
805
|
+
"must",
|
|
806
|
+
"my",
|
|
807
|
+
"myself",
|
|
808
|
+
"neither",
|
|
809
|
+
"no",
|
|
810
|
+
"nor",
|
|
811
|
+
"not",
|
|
812
|
+
"now",
|
|
813
|
+
"of",
|
|
814
|
+
"off",
|
|
815
|
+
"on",
|
|
816
|
+
"once",
|
|
817
|
+
"only",
|
|
818
|
+
"or",
|
|
819
|
+
"other",
|
|
820
|
+
"ought",
|
|
821
|
+
"our",
|
|
822
|
+
"ours",
|
|
823
|
+
"ourselves",
|
|
824
|
+
"out",
|
|
825
|
+
"over",
|
|
826
|
+
"own",
|
|
827
|
+
"same",
|
|
828
|
+
"shall",
|
|
829
|
+
"she",
|
|
830
|
+
"should",
|
|
831
|
+
"so",
|
|
832
|
+
"some",
|
|
833
|
+
"such",
|
|
834
|
+
"than",
|
|
835
|
+
"that",
|
|
836
|
+
"the",
|
|
837
|
+
"their",
|
|
838
|
+
"theirs",
|
|
839
|
+
"them",
|
|
840
|
+
"themselves",
|
|
841
|
+
"then",
|
|
842
|
+
"there",
|
|
843
|
+
"these",
|
|
844
|
+
"they",
|
|
845
|
+
"this",
|
|
846
|
+
"those",
|
|
847
|
+
"through",
|
|
848
|
+
"to",
|
|
849
|
+
"too",
|
|
850
|
+
"under",
|
|
851
|
+
"until",
|
|
852
|
+
"up",
|
|
853
|
+
"us",
|
|
854
|
+
"very",
|
|
855
|
+
"was",
|
|
856
|
+
"we",
|
|
857
|
+
"were",
|
|
858
|
+
"what",
|
|
859
|
+
"when",
|
|
860
|
+
"where",
|
|
861
|
+
"which",
|
|
862
|
+
"while",
|
|
863
|
+
"who",
|
|
864
|
+
"whom",
|
|
865
|
+
"why",
|
|
866
|
+
"will",
|
|
867
|
+
"with",
|
|
868
|
+
"would",
|
|
869
|
+
"yet",
|
|
870
|
+
"you",
|
|
871
|
+
"your",
|
|
872
|
+
"yours",
|
|
873
|
+
"yourself",
|
|
874
|
+
"yourselves"
|
|
875
|
+
]);
|
|
876
|
+
STEM_RULES = [
|
|
877
|
+
// Step 1: plurals and past participles
|
|
878
|
+
[/ies$/, "i", 3],
|
|
879
|
+
// policies → polici,eries → eri
|
|
880
|
+
[/sses$/, "ss", 4],
|
|
881
|
+
// addresses → address
|
|
882
|
+
[/([^s])s$/, "$1", 3],
|
|
883
|
+
// items → item, but not "ss"
|
|
884
|
+
[/eed$/, "ee", 4],
|
|
885
|
+
// agreed → agree
|
|
886
|
+
[/ed$/, "", 3],
|
|
887
|
+
// configured → configur, but min length 3
|
|
888
|
+
[/ing$/, "", 4],
|
|
889
|
+
// running → runn → run (handled below)
|
|
890
|
+
// Step 2: derivational suffixes
|
|
891
|
+
[/ational$/, "ate", 6],
|
|
892
|
+
// relational → relate
|
|
893
|
+
[/tion$/, "t", 5],
|
|
894
|
+
// adoption → adopt
|
|
895
|
+
[/ness$/, "", 5],
|
|
896
|
+
// awareness → aware
|
|
897
|
+
[/ment$/, "", 5],
|
|
898
|
+
// deployment → deploy
|
|
899
|
+
[/able$/, "", 5],
|
|
900
|
+
// configurable → configur
|
|
901
|
+
[/ible$/, "", 5],
|
|
902
|
+
// accessible → access
|
|
903
|
+
[/ful$/, "", 5],
|
|
904
|
+
// powerful → power
|
|
905
|
+
[/ous$/, "", 5],
|
|
906
|
+
// dangerous → danger
|
|
907
|
+
[/ive$/, "", 5],
|
|
908
|
+
// interactive → interact
|
|
909
|
+
[/ize$/, "", 4],
|
|
910
|
+
// normalize → normal
|
|
911
|
+
[/ise$/, "", 4],
|
|
912
|
+
// organise → organ
|
|
913
|
+
[/ally$/, "", 5],
|
|
914
|
+
// automatically → automat
|
|
915
|
+
[/ly$/, "", 4],
|
|
916
|
+
// quickly → quick
|
|
917
|
+
[/er$/, "", 4]
|
|
918
|
+
// handler → handl
|
|
919
|
+
];
|
|
920
|
+
DOUBLE_CONSONANT = /([^aeiou])\1$/;
|
|
921
|
+
MemorySearchIndex = class {
|
|
922
|
+
/** Posting lists: stemmed term → Set of memory IDs containing it */
|
|
923
|
+
postings = /* @__PURE__ */ new Map();
|
|
924
|
+
/** Per-document metadata for BM25 scoring */
|
|
925
|
+
docs = /* @__PURE__ */ new Map();
|
|
926
|
+
/** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
|
|
927
|
+
idf = /* @__PURE__ */ new Map();
|
|
928
|
+
idfStale = true;
|
|
929
|
+
/** 3-character prefix map for prefix matching: prefix → Set of full stems */
|
|
930
|
+
prefixMap = /* @__PURE__ */ new Map();
|
|
931
|
+
/** Total weighted document length (for computing average) */
|
|
932
|
+
totalWeightedLen = 0;
|
|
933
|
+
get docCount() {
|
|
934
|
+
return this.docs.size;
|
|
935
|
+
}
|
|
936
|
+
get avgDocLen() {
|
|
937
|
+
return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Index a memory entry. Extracts stems from title, content, and tags
|
|
941
|
+
* with field-specific weighting and builds posting lists.
|
|
942
|
+
*/
|
|
943
|
+
addDocument(id, entry) {
|
|
944
|
+
if (this.docs.has(id)) this.removeDocument(id);
|
|
945
|
+
const titleTokens = tokenize(entry.title);
|
|
946
|
+
const contentTokens = tokenize(entry.content);
|
|
947
|
+
const tagTokens = entry.tags.flatMap((t) => tokenize(t));
|
|
948
|
+
const weightedTf = /* @__PURE__ */ new Map();
|
|
949
|
+
for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
|
|
950
|
+
for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
|
|
951
|
+
for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
|
|
952
|
+
const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
|
|
953
|
+
const allStems = /* @__PURE__ */ new Set();
|
|
954
|
+
for (const t of weightedTf.keys()) allStems.add(t);
|
|
955
|
+
const stemSequence = [...titleTokens, ...contentTokens];
|
|
956
|
+
const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
|
|
957
|
+
this.docs.set(id, docRecord);
|
|
958
|
+
this.totalWeightedLen += weightedLen;
|
|
959
|
+
for (const term of allStems) {
|
|
960
|
+
let posting = this.postings.get(term);
|
|
961
|
+
if (!posting) {
|
|
962
|
+
posting = /* @__PURE__ */ new Set();
|
|
963
|
+
this.postings.set(term, posting);
|
|
964
|
+
}
|
|
965
|
+
posting.add(id);
|
|
966
|
+
if (term.length >= 3) {
|
|
967
|
+
const prefix = term.slice(0, 3);
|
|
968
|
+
let prefixSet = this.prefixMap.get(prefix);
|
|
969
|
+
if (!prefixSet) {
|
|
970
|
+
prefixSet = /* @__PURE__ */ new Set();
|
|
971
|
+
this.prefixMap.set(prefix, prefixSet);
|
|
972
|
+
}
|
|
973
|
+
prefixSet.add(term);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
this.idfStale = true;
|
|
977
|
+
}
|
|
978
|
+
/** Remove a document from the index. */
|
|
979
|
+
removeDocument(id) {
|
|
980
|
+
const doc = this.docs.get(id);
|
|
981
|
+
if (!doc) return;
|
|
982
|
+
this.totalWeightedLen -= doc.weightedLen;
|
|
983
|
+
this.docs.delete(id);
|
|
984
|
+
for (const term of doc.allStems) {
|
|
985
|
+
const posting = this.postings.get(term);
|
|
986
|
+
if (posting) {
|
|
987
|
+
posting.delete(id);
|
|
988
|
+
if (posting.size === 0) {
|
|
989
|
+
this.postings.delete(term);
|
|
990
|
+
if (term.length >= 3) {
|
|
991
|
+
const prefixSet = this.prefixMap.get(term.slice(0, 3));
|
|
992
|
+
if (prefixSet) {
|
|
993
|
+
prefixSet.delete(term);
|
|
994
|
+
if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
this.idfStale = true;
|
|
1001
|
+
}
|
|
1002
|
+
/** Recompute IDF values for all terms. Called lazily before search. */
|
|
1003
|
+
refreshIdf() {
|
|
1004
|
+
if (!this.idfStale) return;
|
|
1005
|
+
const N = this.docs.size;
|
|
1006
|
+
this.idf.clear();
|
|
1007
|
+
for (const [term, posting] of this.postings) {
|
|
1008
|
+
const df = posting.size;
|
|
1009
|
+
this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
|
|
1010
|
+
}
|
|
1011
|
+
this.idfStale = false;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Expand query terms with prefix matches.
|
|
1015
|
+
* "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
|
|
1016
|
+
*/
|
|
1017
|
+
expandQueryTerms(queryStems) {
|
|
1018
|
+
const expanded = /* @__PURE__ */ new Map();
|
|
1019
|
+
for (const qs of queryStems) {
|
|
1020
|
+
if (this.postings.has(qs)) {
|
|
1021
|
+
expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
|
|
1022
|
+
}
|
|
1023
|
+
if (qs.length >= 3) {
|
|
1024
|
+
const prefix = qs.slice(0, 3);
|
|
1025
|
+
const candidates = this.prefixMap.get(prefix);
|
|
1026
|
+
if (candidates) {
|
|
1027
|
+
for (const candidate of candidates) {
|
|
1028
|
+
if (candidate !== qs && candidate.startsWith(qs)) {
|
|
1029
|
+
expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return expanded;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Compute bigram proximity boost: if two query terms appear adjacent
|
|
1039
|
+
* in the document's stem sequence, boost the score.
|
|
1040
|
+
*/
|
|
1041
|
+
bigramProximityBoost(docId, queryStems) {
|
|
1042
|
+
if (queryStems.length < 2) return 0;
|
|
1043
|
+
const doc = this.docs.get(docId);
|
|
1044
|
+
if (!doc || doc.stemSequence.length < 2) return 0;
|
|
1045
|
+
let boost = 0;
|
|
1046
|
+
const seq = doc.stemSequence;
|
|
1047
|
+
const querySet = new Set(queryStems);
|
|
1048
|
+
for (let i = 0; i < seq.length - 1; i++) {
|
|
1049
|
+
if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
|
|
1050
|
+
boost += 0.5;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return Math.min(boost, 2);
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Search the index for documents matching a query.
|
|
1057
|
+
* Returns scored results sorted by BM25F relevance.
|
|
1058
|
+
*
|
|
1059
|
+
* @param query - Raw query string
|
|
1060
|
+
* @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
|
|
1061
|
+
* @returns Array of { id, score } sorted by descending score
|
|
1062
|
+
*/
|
|
1063
|
+
search(query, candidateIds) {
|
|
1064
|
+
const queryStems = tokenize(query);
|
|
1065
|
+
if (queryStems.length === 0) return [];
|
|
1066
|
+
this.refreshIdf();
|
|
1067
|
+
const expandedTerms = this.expandQueryTerms(queryStems);
|
|
1068
|
+
if (expandedTerms.size === 0) return [];
|
|
1069
|
+
const avgDl = this.avgDocLen;
|
|
1070
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1071
|
+
for (const term of expandedTerms.keys()) {
|
|
1072
|
+
const posting = this.postings.get(term);
|
|
1073
|
+
if (posting) {
|
|
1074
|
+
for (const docId of posting) {
|
|
1075
|
+
if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const results = [];
|
|
1080
|
+
for (const docId of candidates) {
|
|
1081
|
+
const doc = this.docs.get(docId);
|
|
1082
|
+
if (!doc) continue;
|
|
1083
|
+
let score = 0;
|
|
1084
|
+
for (const [term, weight] of expandedTerms) {
|
|
1085
|
+
const tf = doc.weightedTf.get(term) || 0;
|
|
1086
|
+
if (tf === 0) continue;
|
|
1087
|
+
const termIdf = this.idf.get(term) || 0;
|
|
1088
|
+
const numerator = tf * (BM25_K1 + 1);
|
|
1089
|
+
const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
|
|
1090
|
+
score += termIdf * (numerator / denominator) * weight;
|
|
1091
|
+
}
|
|
1092
|
+
score += this.bigramProximityBoost(docId, queryStems);
|
|
1093
|
+
if (score > 0) results.push({ id: docId, score });
|
|
1094
|
+
}
|
|
1095
|
+
results.sort((a, b) => b.score - a.score);
|
|
1096
|
+
return results;
|
|
1097
|
+
}
|
|
1098
|
+
/** Check if a document exists in the index. */
|
|
1099
|
+
has(id) {
|
|
1100
|
+
return this.docs.has(id);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// src/skills/registry.ts
|
|
1107
|
+
function builtInDir() {
|
|
1108
|
+
const here = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(import_meta2.url));
|
|
1109
|
+
return (0, import_node_path5.join)(here, "built-in");
|
|
1110
|
+
}
|
|
1111
|
+
function userDir() {
|
|
1112
|
+
const dir2 = (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".agenticmail", "skills");
|
|
1113
|
+
try {
|
|
1114
|
+
if (!(0, import_node_fs4.existsSync)(dir2)) (0, import_node_fs4.mkdirSync)(dir2, { recursive: true });
|
|
1115
|
+
} catch {
|
|
1116
|
+
}
|
|
1117
|
+
return dir2;
|
|
1118
|
+
}
|
|
1119
|
+
function skillToIndexDoc(s) {
|
|
1120
|
+
const contentParts = [
|
|
1121
|
+
s.description,
|
|
1122
|
+
s.context?.when_to_use ?? "",
|
|
1123
|
+
...s.principles ?? [],
|
|
1124
|
+
...Object.values(s.phrases ?? {}),
|
|
1125
|
+
...(s.tactics ?? []).flatMap((t) => [t.name, t.when, t.script]),
|
|
1126
|
+
...s.success_signals ?? [],
|
|
1127
|
+
...s.failure_signals ?? []
|
|
1128
|
+
];
|
|
1129
|
+
return {
|
|
1130
|
+
title: s.name,
|
|
1131
|
+
// Include category as a tag for free — a query of "negotiation"
|
|
1132
|
+
// hits both literal `negotiation`-category skills AND skills
|
|
1133
|
+
// that tagged themselves "negotiation".
|
|
1134
|
+
tags: [...s.tags ?? [], s.category],
|
|
1135
|
+
content: contentParts.filter(Boolean).join(" ")
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function loadAllSkillsFromDisk() {
|
|
1139
|
+
const all = /* @__PURE__ */ new Map();
|
|
1140
|
+
const dirs = [builtInDir(), userDir()];
|
|
1141
|
+
for (const dir2 of dirs) {
|
|
1142
|
+
if (!(0, import_node_fs4.existsSync)(dir2)) continue;
|
|
1143
|
+
let entries;
|
|
1144
|
+
try {
|
|
1145
|
+
entries = (0, import_node_fs4.readdirSync)(dir2);
|
|
1146
|
+
} catch {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
for (const entry of entries) {
|
|
1150
|
+
if (!entry.endsWith(".json")) continue;
|
|
1151
|
+
const fullPath = (0, import_node_path5.join)(dir2, entry);
|
|
1152
|
+
try {
|
|
1153
|
+
const st = (0, import_node_fs4.statSync)(fullPath);
|
|
1154
|
+
if (!st.isFile()) continue;
|
|
1155
|
+
const raw = (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
|
|
1156
|
+
const parsed = JSON.parse(raw);
|
|
1157
|
+
const errors = validateSkill(parsed);
|
|
1158
|
+
if (errors.length > 0) {
|
|
1159
|
+
console.warn(`[skills] ${entry} invalid, skipping: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
all.set(parsed.id, parsed);
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
console.warn(`[skills] could not load ${fullPath}: ${err.message}`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return all;
|
|
1169
|
+
}
|
|
1170
|
+
function ensureLoaded() {
|
|
1171
|
+
const now = Date.now();
|
|
1172
|
+
if (cache.byId && cache.index && now - cache.ts < CACHE_TTL_MS) {
|
|
1173
|
+
return { byId: cache.byId, index: cache.index };
|
|
1174
|
+
}
|
|
1175
|
+
const fresh = loadAllSkillsFromDisk();
|
|
1176
|
+
const index = new MemorySearchIndex();
|
|
1177
|
+
for (const [id, skill] of fresh) {
|
|
1178
|
+
try {
|
|
1179
|
+
index.addDocument(id, skillToIndexDoc(skill));
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
cache.byId = fresh;
|
|
1184
|
+
cache.index = index;
|
|
1185
|
+
cache.ts = now;
|
|
1186
|
+
return { byId: fresh, index };
|
|
1187
|
+
}
|
|
1188
|
+
function invalidateSkillCache() {
|
|
1189
|
+
cache.byId = null;
|
|
1190
|
+
cache.index = null;
|
|
1191
|
+
cache.ts = 0;
|
|
1192
|
+
}
|
|
1193
|
+
function validateSkill(s) {
|
|
1194
|
+
const errs = [];
|
|
1195
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) {
|
|
1196
|
+
return [{ path: "$", message: "skill must be a JSON object" }];
|
|
1197
|
+
}
|
|
1198
|
+
const sk = s;
|
|
1199
|
+
const requireString = (key, minLen = 1) => {
|
|
1200
|
+
const v = sk[key];
|
|
1201
|
+
if (typeof v !== "string" || v.length < minLen) {
|
|
1202
|
+
errs.push({ path: key, message: `must be a non-empty string` });
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
const requireArray = (key, minLen = 1) => {
|
|
1206
|
+
const v = sk[key];
|
|
1207
|
+
if (!Array.isArray(v) || v.length < minLen) {
|
|
1208
|
+
errs.push({ path: key, message: `must be a non-empty array` });
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
requireString("id");
|
|
1212
|
+
if (typeof sk.id === "string" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(sk.id)) {
|
|
1213
|
+
errs.push({ path: "id", message: "must be lowercase-hyphenated slug (a-z, 0-9, -)" });
|
|
1214
|
+
}
|
|
1215
|
+
requireString("name");
|
|
1216
|
+
requireString("version");
|
|
1217
|
+
requireString("description");
|
|
1218
|
+
requireString("category");
|
|
1219
|
+
const validCategories = [
|
|
1220
|
+
"negotiation",
|
|
1221
|
+
"customer-service",
|
|
1222
|
+
"reservations",
|
|
1223
|
+
"medical-admin",
|
|
1224
|
+
"legal-admin",
|
|
1225
|
+
"finance-admin",
|
|
1226
|
+
"real-estate",
|
|
1227
|
+
"travel",
|
|
1228
|
+
"subscription",
|
|
1229
|
+
"home-services",
|
|
1230
|
+
"social",
|
|
1231
|
+
"civic",
|
|
1232
|
+
"employment",
|
|
1233
|
+
"debt-collection",
|
|
1234
|
+
"other"
|
|
1235
|
+
];
|
|
1236
|
+
if (typeof sk.category === "string" && !validCategories.includes(sk.category)) {
|
|
1237
|
+
errs.push({ path: "category", message: `unknown category "${sk.category}"; must be one of: ${validCategories.join(", ")}` });
|
|
1238
|
+
}
|
|
1239
|
+
if (!Array.isArray(sk.tags)) errs.push({ path: "tags", message: "must be an array of strings" });
|
|
1240
|
+
if (sk.disclaimer !== null && typeof sk.disclaimer !== "string") {
|
|
1241
|
+
errs.push({ path: "disclaimer", message: "must be a string or null" });
|
|
1242
|
+
}
|
|
1243
|
+
if (!sk.context || typeof sk.context !== "object") {
|
|
1244
|
+
errs.push({ path: "context", message: "must be an object" });
|
|
1245
|
+
} else {
|
|
1246
|
+
const ctx = sk.context;
|
|
1247
|
+
if (typeof ctx.when_to_use !== "string") errs.push({ path: "context.when_to_use", message: "must be a string" });
|
|
1248
|
+
if (!Array.isArray(ctx.preconditions)) errs.push({ path: "context.preconditions", message: "must be an array" });
|
|
1249
|
+
if (typeof ctx.estimated_call_duration_minutes !== "number") errs.push({ path: "context.estimated_call_duration_minutes", message: "must be a number" });
|
|
1250
|
+
}
|
|
1251
|
+
requireArray("principles", 2);
|
|
1252
|
+
if (!sk.phrases || typeof sk.phrases !== "object") errs.push({ path: "phrases", message: "must be an object of {key: phrase}" });
|
|
1253
|
+
if (!Array.isArray(sk.tactics) || sk.tactics.length === 0) {
|
|
1254
|
+
errs.push({ path: "tactics", message: "must be a non-empty array" });
|
|
1255
|
+
} else {
|
|
1256
|
+
sk.tactics.forEach((t, i) => {
|
|
1257
|
+
if (!t || typeof t !== "object") {
|
|
1258
|
+
errs.push({ path: `tactics[${i}]`, message: "must be an object" });
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const tactic = t;
|
|
1262
|
+
if (typeof tactic.name !== "string") errs.push({ path: `tactics[${i}].name`, message: "must be a string" });
|
|
1263
|
+
if (typeof tactic.when !== "string") errs.push({ path: `tactics[${i}].when`, message: "must be a string" });
|
|
1264
|
+
if (typeof tactic.script !== "string" || tactic.script.length < 5) {
|
|
1265
|
+
errs.push({ path: `tactics[${i}].script`, message: "must be a non-empty string (>= 5 chars)" });
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
requireArray("boundaries", 1);
|
|
1270
|
+
if (!Array.isArray(sk.success_signals)) errs.push({ path: "success_signals", message: "must be an array" });
|
|
1271
|
+
if (!Array.isArray(sk.failure_signals)) errs.push({ path: "failure_signals", message: "must be an array" });
|
|
1272
|
+
if (!sk.exit_strategy || typeof sk.exit_strategy !== "object") {
|
|
1273
|
+
errs.push({ path: "exit_strategy", message: "must be an object" });
|
|
1274
|
+
} else {
|
|
1275
|
+
const xs = sk.exit_strategy;
|
|
1276
|
+
if (typeof xs.on_success !== "string") errs.push({ path: "exit_strategy.on_success", message: "must be a string" });
|
|
1277
|
+
if (typeof xs.on_failure !== "string") errs.push({ path: "exit_strategy.on_failure", message: "must be a string" });
|
|
1278
|
+
}
|
|
1279
|
+
if (!Array.isArray(sk.required_user_info)) errs.push({ path: "required_user_info", message: "must be an array" });
|
|
1280
|
+
if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
|
|
1281
|
+
return errs;
|
|
1282
|
+
}
|
|
1283
|
+
function summarize(s) {
|
|
1284
|
+
return {
|
|
1285
|
+
id: s.id,
|
|
1286
|
+
name: s.name,
|
|
1287
|
+
category: s.category,
|
|
1288
|
+
tags: s.tags,
|
|
1289
|
+
description: s.description,
|
|
1290
|
+
version: s.version,
|
|
1291
|
+
disclaimer_required: !!s.disclaimer,
|
|
1292
|
+
estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function listSkills(opts = {}) {
|
|
1296
|
+
const all = Array.from(ensureLoaded().byId.values());
|
|
1297
|
+
const filtered = all.filter((s) => {
|
|
1298
|
+
if (opts.category && s.category !== opts.category) return false;
|
|
1299
|
+
if (opts.tag && !s.tags.includes(opts.tag.toLowerCase())) return false;
|
|
1300
|
+
return true;
|
|
1301
|
+
});
|
|
1302
|
+
return filtered.sort((a, b) => a.name.localeCompare(b.name)).map(summarize);
|
|
1303
|
+
}
|
|
1304
|
+
function searchSkills(query, limit = 20) {
|
|
1305
|
+
const q = query.trim();
|
|
1306
|
+
if (!q) return [];
|
|
1307
|
+
const { byId, index } = ensureLoaded();
|
|
1308
|
+
const ranked = index.search(q);
|
|
1309
|
+
if (ranked.length === 0) {
|
|
1310
|
+
const qLow = q.toLowerCase();
|
|
1311
|
+
const fallback = [];
|
|
1312
|
+
for (const s of byId.values()) {
|
|
1313
|
+
if (s.id.toLowerCase().includes(qLow) || s.name.toLowerCase().includes(qLow) || s.tags.some((t) => t.toLowerCase().includes(qLow))) {
|
|
1314
|
+
fallback.push(summarize(s));
|
|
1315
|
+
if (fallback.length >= limit) break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return fallback;
|
|
1319
|
+
}
|
|
1320
|
+
const out = [];
|
|
1321
|
+
for (const { id } of ranked) {
|
|
1322
|
+
const skill = byId.get(id);
|
|
1323
|
+
if (!skill) continue;
|
|
1324
|
+
out.push(summarize(skill));
|
|
1325
|
+
if (out.length >= limit) break;
|
|
1326
|
+
}
|
|
1327
|
+
return out;
|
|
1328
|
+
}
|
|
1329
|
+
function loadSkill(id) {
|
|
1330
|
+
return ensureLoaded().byId.get(id) ?? null;
|
|
1331
|
+
}
|
|
1332
|
+
function saveUserSkill(skill) {
|
|
1333
|
+
const errors = validateSkill(skill);
|
|
1334
|
+
if (errors.length > 0) {
|
|
1335
|
+
throw new Error(`skill validation failed: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
1336
|
+
}
|
|
1337
|
+
const dir2 = userDir();
|
|
1338
|
+
const path2 = (0, import_node_path5.join)(dir2, `${skill.id}.json`);
|
|
1339
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1340
|
+
const out = {
|
|
1341
|
+
...skill,
|
|
1342
|
+
created_at: skill.created_at ?? now,
|
|
1343
|
+
updated_at: now
|
|
1344
|
+
};
|
|
1345
|
+
(0, import_node_fs4.writeFileSync)(path2, JSON.stringify(out, null, 2), "utf-8");
|
|
1346
|
+
invalidateSkillCache();
|
|
1347
|
+
return { path: path2 };
|
|
1348
|
+
}
|
|
1349
|
+
function skillFilename(id) {
|
|
1350
|
+
return `${(0, import_node_path5.basename)(id)}.json`;
|
|
1351
|
+
}
|
|
1352
|
+
function userSkillsDir() {
|
|
1353
|
+
return userDir();
|
|
1354
|
+
}
|
|
1355
|
+
var import_node_fs4, import_node_path5, import_node_url, import_node_os4, import_meta2, cache, CACHE_TTL_MS;
|
|
1356
|
+
var init_registry = __esm({
|
|
1357
|
+
"src/skills/registry.ts"() {
|
|
1358
|
+
"use strict";
|
|
1359
|
+
import_node_fs4 = require("fs");
|
|
1360
|
+
import_node_path5 = require("path");
|
|
1361
|
+
import_node_url = require("url");
|
|
1362
|
+
import_node_os4 = require("os");
|
|
1363
|
+
init_text_search();
|
|
1364
|
+
import_meta2 = {};
|
|
1365
|
+
cache = { ts: 0, byId: null, index: null };
|
|
1366
|
+
CACHE_TTL_MS = 5e3;
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// src/skills/index.ts
|
|
1371
|
+
var skills_exports = {};
|
|
1372
|
+
__export(skills_exports, {
|
|
1373
|
+
invalidateSkillCache: () => invalidateSkillCache,
|
|
1374
|
+
listSkills: () => listSkills,
|
|
1375
|
+
loadSkill: () => loadSkill,
|
|
1376
|
+
renderSkillAsPrompt: () => renderSkillAsPrompt,
|
|
1377
|
+
saveUserSkill: () => saveUserSkill,
|
|
1378
|
+
searchSkills: () => searchSkills,
|
|
1379
|
+
skillFilename: () => skillFilename,
|
|
1380
|
+
userSkillsDir: () => userSkillsDir,
|
|
1381
|
+
validateSkill: () => validateSkill
|
|
1382
|
+
});
|
|
1383
|
+
function renderSkillAsPrompt(skill) {
|
|
1384
|
+
const lines = [];
|
|
1385
|
+
lines.push(`=== SKILL LOADED: ${skill.name} (v${skill.version}) ===`);
|
|
1386
|
+
lines.push(`Category: ${skill.category} Tags: ${skill.tags.join(", ")}`);
|
|
1387
|
+
lines.push("");
|
|
1388
|
+
lines.push(skill.description);
|
|
1389
|
+
lines.push("");
|
|
1390
|
+
if (skill.disclaimer) {
|
|
1391
|
+
lines.push("REQUIRED DISCLAIMER (recite at start of the substantive turn):");
|
|
1392
|
+
lines.push(` "${skill.disclaimer}"`);
|
|
1393
|
+
lines.push("");
|
|
1394
|
+
}
|
|
1395
|
+
lines.push("WHEN TO USE THIS:");
|
|
1396
|
+
lines.push(` ${skill.context.when_to_use}`);
|
|
1397
|
+
if (skill.context.preconditions.length > 0) {
|
|
1398
|
+
lines.push("Preconditions:");
|
|
1399
|
+
for (const p of skill.context.preconditions) lines.push(` - ${p}`);
|
|
1400
|
+
}
|
|
1401
|
+
lines.push("");
|
|
1402
|
+
lines.push("PRINCIPLES:");
|
|
1403
|
+
for (const p of skill.principles) lines.push(` - ${p}`);
|
|
1404
|
+
lines.push("");
|
|
1405
|
+
if (Object.keys(skill.phrases).length > 0) {
|
|
1406
|
+
lines.push("PHRASES (paraphrase to match your voice; keep the structural move):");
|
|
1407
|
+
for (const [k, v] of Object.entries(skill.phrases)) lines.push(` [${k}] "${v}"`);
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
}
|
|
1410
|
+
if (skill.tactics.length > 0) {
|
|
1411
|
+
lines.push("TACTICS (try in order; fall back to next on failure):");
|
|
1412
|
+
skill.tactics.forEach((t, i) => {
|
|
1413
|
+
lines.push(` ${i + 1}. ${t.name}`);
|
|
1414
|
+
lines.push(` When: ${t.when}`);
|
|
1415
|
+
lines.push(` Script: "${t.script}"`);
|
|
1416
|
+
});
|
|
1417
|
+
lines.push("");
|
|
1418
|
+
}
|
|
1419
|
+
if (skill.boundaries.length > 0) {
|
|
1420
|
+
lines.push("HARD BOUNDARIES \u2014 do not cross:");
|
|
1421
|
+
for (const b of skill.boundaries) lines.push(` - ${b}`);
|
|
1422
|
+
lines.push("");
|
|
1423
|
+
}
|
|
1424
|
+
lines.push("SUCCESS SIGNALS:");
|
|
1425
|
+
for (const s of skill.success_signals) lines.push(` - ${s}`);
|
|
1426
|
+
lines.push("");
|
|
1427
|
+
lines.push("FAILURE SIGNALS \u2014 when these appear, escalate or exit:");
|
|
1428
|
+
for (const s of skill.failure_signals) lines.push(` - ${s}`);
|
|
1429
|
+
lines.push("");
|
|
1430
|
+
lines.push("EXIT:");
|
|
1431
|
+
lines.push(` On success: ${skill.exit_strategy.on_success}`);
|
|
1432
|
+
lines.push(` On failure: ${skill.exit_strategy.on_failure}`);
|
|
1433
|
+
if (skill.exit_strategy.follow_ups && skill.exit_strategy.follow_ups.length > 0) {
|
|
1434
|
+
lines.push(" Follow-ups (after the call):");
|
|
1435
|
+
for (const f of skill.exit_strategy.follow_ups) lines.push(` - ${f}`);
|
|
1436
|
+
}
|
|
1437
|
+
lines.push("");
|
|
1438
|
+
lines.push("=== END SKILL ===");
|
|
1439
|
+
return lines.join("\n");
|
|
1440
|
+
}
|
|
1441
|
+
var init_skills = __esm({
|
|
1442
|
+
"src/skills/index.ts"() {
|
|
1443
|
+
"use strict";
|
|
1444
|
+
init_registry();
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
|
|
708
1448
|
// src/index.ts
|
|
709
1449
|
var index_exports = {};
|
|
710
1450
|
__export(index_exports, {
|
|
@@ -736,6 +1476,7 @@ __export(index_exports, {
|
|
|
736
1476
|
GET_DATETIME_TOOL: () => GET_DATETIME_TOOL,
|
|
737
1477
|
GatewayManager: () => GatewayManager,
|
|
738
1478
|
InboxWatcher: () => InboxWatcher,
|
|
1479
|
+
LOAD_SKILL_TOOL: () => LOAD_SKILL_TOOL,
|
|
739
1480
|
MEMORY_CATEGORIES: () => MEMORY_CATEGORIES,
|
|
740
1481
|
MailReceiver: () => MailReceiver,
|
|
741
1482
|
MailSender: () => MailSender,
|
|
@@ -772,6 +1513,7 @@ __export(index_exports, {
|
|
|
772
1513
|
RelayBridge: () => RelayBridge,
|
|
773
1514
|
RelayGateway: () => RelayGateway,
|
|
774
1515
|
SEARCH_EMAIL_TOOL: () => SEARCH_EMAIL_TOOL,
|
|
1516
|
+
SEARCH_SKILLS_TOOL: () => SEARCH_SKILLS_TOOL,
|
|
775
1517
|
SPAM_THRESHOLD: () => SPAM_THRESHOLD,
|
|
776
1518
|
ServiceManager: () => ServiceManager,
|
|
777
1519
|
SetupManager: () => SetupManager,
|
|
@@ -8003,7 +8745,8 @@ function buildPhoneTransportConfig(input) {
|
|
|
8003
8745
|
}
|
|
8004
8746
|
}
|
|
8005
8747
|
const capabilities = Array.isArray(input.capabilities) ? input.capabilities.filter((item) => typeof item === "string" && ["sms", "call_control", "realtime_media", "recording_supported"].includes(item)) : ["call_control"];
|
|
8006
|
-
const
|
|
8748
|
+
const defaultRegions = isTwilio ? ["WORLD"] : ["EU"];
|
|
8749
|
+
const supportedRegions = Array.isArray(input.supportedRegions) ? input.supportedRegions.filter((item) => typeof item === "string" && ["AT", "DE", "EU", "WORLD"].includes(item)) : defaultRegions;
|
|
8007
8750
|
const config = {
|
|
8008
8751
|
provider,
|
|
8009
8752
|
phoneNumber,
|
|
@@ -8013,7 +8756,7 @@ function buildPhoneTransportConfig(input) {
|
|
|
8013
8756
|
webhookSecret,
|
|
8014
8757
|
apiUrl: apiUrl || void 0,
|
|
8015
8758
|
capabilities: Array.from(/* @__PURE__ */ new Set(["call_control", ...capabilities])),
|
|
8016
|
-
supportedRegions: supportedRegions.length ? Array.from(new Set(supportedRegions)) :
|
|
8759
|
+
supportedRegions: supportedRegions.length ? Array.from(new Set(supportedRegions)) : defaultRegions,
|
|
8017
8760
|
configuredAt: input.configuredAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
8018
8761
|
};
|
|
8019
8762
|
const validation = validatePhoneTransportProfile(config);
|
|
@@ -9669,12 +10412,46 @@ var SEARCH_EMAIL_TOOL = {
|
|
|
9669
10412
|
additionalProperties: false
|
|
9670
10413
|
}
|
|
9671
10414
|
};
|
|
10415
|
+
var SEARCH_SKILLS_TOOL = {
|
|
10416
|
+
type: "function",
|
|
10417
|
+
name: "search_skills",
|
|
10418
|
+
description: "Search your skill library for a playbook that fits the situation you just hit on this call (billing dispute, debt collector tactics, reservation deadlock, etc). Returns ranked summaries \u2014 pick the best match and pass its id to load_skill. Fast.",
|
|
10419
|
+
parameters: {
|
|
10420
|
+
type: "object",
|
|
10421
|
+
properties: {
|
|
10422
|
+
query: {
|
|
10423
|
+
type: "string",
|
|
10424
|
+
description: 'Plain-language description of the situation, e.g. "rep insists on a commitment date", "the restaurant is fully booked", "I need to dispute a recurring charge after cancellation".'
|
|
10425
|
+
}
|
|
10426
|
+
},
|
|
10427
|
+
required: ["query"],
|
|
10428
|
+
additionalProperties: false
|
|
10429
|
+
}
|
|
10430
|
+
};
|
|
10431
|
+
var LOAD_SKILL_TOOL = {
|
|
10432
|
+
type: "function",
|
|
10433
|
+
name: "load_skill",
|
|
10434
|
+
description: 'Load a skill playbook by id into your context for the rest of this call. The playbook (principles, scripted phrases, ordered tactics, hard boundaries, exit strategy) grounds your next turns. Always call search_skills first to find the right id. Before calling, say "hold on one moment" \u2014 loading is briefer than `ask_operator` but takes a beat.',
|
|
10435
|
+
parameters: {
|
|
10436
|
+
type: "object",
|
|
10437
|
+
properties: {
|
|
10438
|
+
id: {
|
|
10439
|
+
type: "string",
|
|
10440
|
+
description: 'Skill id (lowercase-hyphenated), e.g. "negotiate-bill-reduction". Get it from search_skills.'
|
|
10441
|
+
}
|
|
10442
|
+
},
|
|
10443
|
+
required: ["id"],
|
|
10444
|
+
additionalProperties: false
|
|
10445
|
+
}
|
|
10446
|
+
};
|
|
9672
10447
|
var REALTIME_TOOL_DEFINITIONS = {
|
|
9673
10448
|
ask_operator: ASK_OPERATOR_TOOL,
|
|
9674
10449
|
web_search: WEB_SEARCH_TOOL,
|
|
9675
10450
|
recall_memory: RECALL_MEMORY_TOOL,
|
|
9676
10451
|
get_datetime: GET_DATETIME_TOOL,
|
|
9677
|
-
search_email: SEARCH_EMAIL_TOOL
|
|
10452
|
+
search_email: SEARCH_EMAIL_TOOL,
|
|
10453
|
+
search_skills: SEARCH_SKILLS_TOOL,
|
|
10454
|
+
load_skill: LOAD_SKILL_TOOL
|
|
9678
10455
|
};
|
|
9679
10456
|
function buildRealtimeToolGuidance(tools) {
|
|
9680
10457
|
if (tools.length === 0) return "";
|
|
@@ -9693,6 +10470,16 @@ function buildRealtimeToolGuidance(tools) {
|
|
|
9693
10470
|
'The lookup tools (web_search, recall_memory, get_datetime, search_email) return in seconds \u2014 a brief "one moment" is plenty; no long hold is needed for these.'
|
|
9694
10471
|
);
|
|
9695
10472
|
}
|
|
10473
|
+
if (names.has("search_skills") && names.has("load_skill")) {
|
|
10474
|
+
lines.push(
|
|
10475
|
+
`Your SKILL LIBRARY contains playbooks for specific real-world phone situations \u2014 bill negotiation, debt-collector handling, restaurant booking, dispute filing, etc. Each playbook is a complete set of principles, scripted phrases, ordered tactics, boundaries, and exit strategy for that one situation. When you find yourself on the call without a clear next move \u2014 the rep brought up something you do not know how to handle, the conversation reached a stage that needs a specific tactic \u2014 load a skill instead of improvising:
|
|
10476
|
+
1. Tell the caller you need a moment: "Hold on one moment \u2014 let me check something."
|
|
10477
|
+
2. Call search_skills with a one-line description of the situation.
|
|
10478
|
+
3. Call load_skill with the id of the best match.
|
|
10479
|
+
4. Resume the call grounded in the playbook the load returned. Follow the playbook's tactic order, use its scripted phrases (paraphrased to match your voice), respect its hard boundaries, watch for its success / failure signals.
|
|
10480
|
+
A skill's rendered playbook is now part of your instructions for the rest of the call. You can load a second skill if a new situation comes up \u2014 but the model keeps a max of two loaded; a third load drops the oldest. Pick skills deliberately.`
|
|
10481
|
+
);
|
|
10482
|
+
}
|
|
9696
10483
|
return lines.join("\n");
|
|
9697
10484
|
}
|
|
9698
10485
|
function toolErrorText(err) {
|
|
@@ -9878,6 +10665,7 @@ var REALTIME_AUDIO_SAMPLE_RATE = 24e3;
|
|
|
9878
10665
|
var REALTIME_MAX_AUDIO_FRAME_BASE64 = 256 * 1024;
|
|
9879
10666
|
var MAX_PENDING_AUDIO_FRAMES = 200;
|
|
9880
10667
|
var REALTIME_TOOL_CALL_TIMEOUT_MS = 6 * 6e4;
|
|
10668
|
+
var MAX_LOADED_SKILLS = 2;
|
|
9881
10669
|
var MAX_IN_FLIGHT_TOOL_CALLS = 8;
|
|
9882
10670
|
var DEFAULT_PERSONA = "You are a helpful, professional voice assistant making a phone call on behalf of your operator. Speak naturally and concisely, the way a person would on a real call. Listen carefully, do not talk over the other party, and keep each turn short. Never invent facts; if you do not know something, say so. Do not reveal that you are an AI unless you are asked directly.";
|
|
9883
10671
|
function buildRealtimeInstructions(opts) {
|
|
@@ -9969,6 +10757,26 @@ var RealtimeVoiceBridge = class {
|
|
|
9969
10757
|
toolCallNames = /* @__PURE__ */ new Map();
|
|
9970
10758
|
/** `call_id`s whose tool call is currently executing. */
|
|
9971
10759
|
inFlightToolCalls = /* @__PURE__ */ new Set();
|
|
10760
|
+
/**
|
|
10761
|
+
* Mid-call skills loaded into the session so far, FIFO. Earliest at
|
|
10762
|
+
* index 0; cap at {@link MAX_LOADED_SKILLS}. When a (cap+1)th skill
|
|
10763
|
+
* is loaded the oldest one drops out — the model can't usefully
|
|
10764
|
+
* hold five playbooks in working memory at once, so we keep the
|
|
10765
|
+
* working set narrow on purpose.
|
|
10766
|
+
*/
|
|
10767
|
+
loadedSkills = [];
|
|
10768
|
+
/**
|
|
10769
|
+
* The original `instructions` string from the session.update sent at
|
|
10770
|
+
* open. We keep a private copy because every mid-call skill load
|
|
10771
|
+
* issues a fresh `session.update` whose `instructions` is built as:
|
|
10772
|
+
*
|
|
10773
|
+
* baseInstructions + "\n\n" + renderedSkill1 + "\n\n" + renderedSkill2 …
|
|
10774
|
+
*
|
|
10775
|
+
* Without this snapshot, successive loads would compound — the second
|
|
10776
|
+
* load would see "base + skill1" as the base and append skill2 to
|
|
10777
|
+
* THAT, eventually drifting unboundedly.
|
|
10778
|
+
*/
|
|
10779
|
+
baseInstructions = "";
|
|
9972
10780
|
constructor(opts) {
|
|
9973
10781
|
const carrier = opts.carrier ?? opts.elks;
|
|
9974
10782
|
if (!carrier) {
|
|
@@ -10005,6 +10813,10 @@ var RealtimeVoiceBridge = class {
|
|
|
10005
10813
|
handleOpenAIOpen() {
|
|
10006
10814
|
if (this.ended || this.openaiReady) return;
|
|
10007
10815
|
this.openaiReady = true;
|
|
10816
|
+
const sess = this.sessionConfig?.session;
|
|
10817
|
+
if (sess && typeof sess.instructions === "string") {
|
|
10818
|
+
this.baseInstructions = sess.instructions;
|
|
10819
|
+
}
|
|
10008
10820
|
this.safeSend(this.openai, this.sessionConfig);
|
|
10009
10821
|
this.safeSend(this.openai, { type: "response.create" });
|
|
10010
10822
|
for (const audio of this.pendingAudio.splice(0)) {
|
|
@@ -10015,6 +10827,74 @@ var RealtimeVoiceBridge = class {
|
|
|
10015
10827
|
handleOpenAIClose() {
|
|
10016
10828
|
this.end("openai-closed");
|
|
10017
10829
|
}
|
|
10830
|
+
/**
|
|
10831
|
+
* Load a skill playbook into the live OpenAI Realtime session for
|
|
10832
|
+
* the rest of the call.
|
|
10833
|
+
*
|
|
10834
|
+
* Mechanics:
|
|
10835
|
+
* 1. Resolve the skill JSON via the skills registry (file on disk).
|
|
10836
|
+
* 2. Append the rendered skill text to the agent's working
|
|
10837
|
+
* instructions and re-send a `session.update` carrying ONLY
|
|
10838
|
+
* the new `instructions` field. The OpenAI Realtime API
|
|
10839
|
+
* supports partial session.update — we don't have to re-send
|
|
10840
|
+
* audio config, tools, voice, etc.
|
|
10841
|
+
* 3. Track which skills are loaded so we (a) FIFO-evict the
|
|
10842
|
+
* oldest when the cap is hit and (b) include every still-
|
|
10843
|
+
* loaded skill in the next composed instructions.
|
|
10844
|
+
* 4. Emit a transcript marker so the mission record shows the
|
|
10845
|
+
* adaptation ("[skill loaded: Negotiate a Bill Reduction
|
|
10846
|
+
* v1.0.0]"). Useful for post-call review and for the build
|
|
10847
|
+
* farm's telemetry on which skills actually got reached for.
|
|
10848
|
+
*
|
|
10849
|
+
* Returns an object the {@link load_skill} tool handler can serialise
|
|
10850
|
+
* back to the model: `ok: true` plus the skill name + version on
|
|
10851
|
+
* success, `ok: false` plus a short reason on failure (unknown id,
|
|
10852
|
+
* call ended, registry I/O error). Never throws — a buggy registry
|
|
10853
|
+
* or a missing file must not crash the bridge mid-call.
|
|
10854
|
+
*
|
|
10855
|
+
* Phase 2 of the skill library (`docs/skill-library-plan.md`).
|
|
10856
|
+
*/
|
|
10857
|
+
async loadSkillIntoSession(skillId) {
|
|
10858
|
+
if (this.ended) return { ok: false, message: "Call has already ended; cannot load a skill now." };
|
|
10859
|
+
if (!this.openaiReady) return { ok: false, message: "Session is not ready yet; try again in a moment." };
|
|
10860
|
+
if (this.loadedSkills.some((s) => s.id === skillId)) {
|
|
10861
|
+
const existing = this.loadedSkills.find((s) => s.id === skillId);
|
|
10862
|
+
return { ok: true, message: `Skill "${skillId}" is already loaded.`, name: skillId, version: existing.version };
|
|
10863
|
+
}
|
|
10864
|
+
let loadSkill2;
|
|
10865
|
+
let renderSkillAsPrompt2;
|
|
10866
|
+
try {
|
|
10867
|
+
({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await Promise.resolve().then(() => (init_skills(), skills_exports)));
|
|
10868
|
+
} catch (err) {
|
|
10869
|
+
return { ok: false, message: `Skill registry unavailable: ${errorText(err)}` };
|
|
10870
|
+
}
|
|
10871
|
+
const skill = loadSkill2(skillId);
|
|
10872
|
+
if (!skill) {
|
|
10873
|
+
return { ok: false, message: `No skill found with id "${skillId}". Call search_skills first to find the right id.` };
|
|
10874
|
+
}
|
|
10875
|
+
const rendered = renderSkillAsPrompt2(skill);
|
|
10876
|
+
while (this.loadedSkills.length >= MAX_LOADED_SKILLS) {
|
|
10877
|
+
const dropped = this.loadedSkills.shift();
|
|
10878
|
+
if (dropped) {
|
|
10879
|
+
this.emitTranscript("system", `[skill unloaded for working-memory limit: ${dropped.id} v${dropped.version}]`);
|
|
10880
|
+
}
|
|
10881
|
+
}
|
|
10882
|
+
this.loadedSkills.push({ id: skill.id, version: skill.version, renderedPrompt: rendered });
|
|
10883
|
+
const composed = [
|
|
10884
|
+
this.baseInstructions,
|
|
10885
|
+
...this.loadedSkills.map((s) => s.renderedPrompt)
|
|
10886
|
+
].filter((s) => s && s.length > 0).join("\n\n");
|
|
10887
|
+
this.safeSend(this.openai, {
|
|
10888
|
+
type: "session.update",
|
|
10889
|
+
session: { instructions: composed }
|
|
10890
|
+
});
|
|
10891
|
+
this.emitTranscript("system", `[skill loaded: ${skill.name} v${skill.version}]`);
|
|
10892
|
+
return { ok: true, message: `Loaded skill: ${skill.name} (v${skill.version})`, name: skill.name, version: skill.version };
|
|
10893
|
+
}
|
|
10894
|
+
/** The list of skills currently loaded into the session (FIFO-ordered). */
|
|
10895
|
+
get loadedSkillIds() {
|
|
10896
|
+
return this.loadedSkills.map((s) => s.id);
|
|
10897
|
+
}
|
|
10018
10898
|
/** Call when the OpenAI socket errors. */
|
|
10019
10899
|
handleOpenAIError(err) {
|
|
10020
10900
|
this.emitTranscript("system", `OpenAI Realtime error: ${errorText(err)}`);
|
|
@@ -10475,7 +11355,7 @@ try {
|
|
|
10475
11355
|
}
|
|
10476
11356
|
|
|
10477
11357
|
// src/util/safe-path.ts
|
|
10478
|
-
var
|
|
11358
|
+
var import_node_path6 = require("path");
|
|
10479
11359
|
var PathTraversalError = class extends Error {
|
|
10480
11360
|
constructor(baseDir, parts) {
|
|
10481
11361
|
super(
|
|
@@ -10498,14 +11378,14 @@ function safeJoin(baseDir, ...partsAndOpts) {
|
|
|
10498
11378
|
}
|
|
10499
11379
|
if (!opts.allowAbsolute) {
|
|
10500
11380
|
for (const part of parts) {
|
|
10501
|
-
if ((0,
|
|
11381
|
+
if ((0, import_node_path6.isAbsolute)(part)) {
|
|
10502
11382
|
throw new PathTraversalError(baseDir, parts);
|
|
10503
11383
|
}
|
|
10504
11384
|
}
|
|
10505
11385
|
}
|
|
10506
|
-
const resolvedBase = (0,
|
|
10507
|
-
const resolved = (0,
|
|
10508
|
-
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase +
|
|
11386
|
+
const resolvedBase = (0, import_node_path6.resolve)(baseDir);
|
|
11387
|
+
const resolved = (0, import_node_path6.resolve)(resolvedBase, ...parts);
|
|
11388
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + import_node_path6.sep)) {
|
|
10509
11389
|
throw new PathTraversalError(baseDir, parts);
|
|
10510
11390
|
}
|
|
10511
11391
|
return resolved;
|
|
@@ -10519,9 +11399,9 @@ function tryJoin(baseDir, ...parts) {
|
|
|
10519
11399
|
}
|
|
10520
11400
|
}
|
|
10521
11401
|
function assertWithinBase(baseDir, candidate) {
|
|
10522
|
-
const resolvedBase = (0,
|
|
10523
|
-
const resolvedCandidate = (0,
|
|
10524
|
-
if (resolvedCandidate !== resolvedBase && !resolvedCandidate.startsWith(resolvedBase +
|
|
11402
|
+
const resolvedBase = (0, import_node_path6.resolve)(baseDir);
|
|
11403
|
+
const resolvedCandidate = (0, import_node_path6.resolve)(candidate);
|
|
11404
|
+
if (resolvedCandidate !== resolvedBase && !resolvedCandidate.startsWith(resolvedBase + import_node_path6.sep)) {
|
|
10525
11405
|
throw new PathTraversalError(baseDir, [candidate]);
|
|
10526
11406
|
}
|
|
10527
11407
|
return resolvedCandidate;
|
|
@@ -10574,19 +11454,19 @@ function redactObject(input, _depth = 0) {
|
|
|
10574
11454
|
}
|
|
10575
11455
|
|
|
10576
11456
|
// src/operator-prefs.ts
|
|
10577
|
-
var
|
|
10578
|
-
var
|
|
10579
|
-
var
|
|
11457
|
+
var import_node_fs5 = require("fs");
|
|
11458
|
+
var import_node_os5 = require("os");
|
|
11459
|
+
var import_node_path7 = require("path");
|
|
10580
11460
|
function dir() {
|
|
10581
|
-
return (0,
|
|
11461
|
+
return (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".agenticmail");
|
|
10582
11462
|
}
|
|
10583
11463
|
function path() {
|
|
10584
|
-
return (0,
|
|
11464
|
+
return (0, import_node_path7.join)(dir(), "operator-prefs.json");
|
|
10585
11465
|
}
|
|
10586
11466
|
function readFile() {
|
|
10587
|
-
if (!(0,
|
|
11467
|
+
if (!(0, import_node_fs5.existsSync)(path())) return { version: 1 };
|
|
10588
11468
|
try {
|
|
10589
|
-
const raw = (0,
|
|
11469
|
+
const raw = (0, import_node_fs5.readFileSync)(path(), "utf-8");
|
|
10590
11470
|
if (!raw.trim()) return { version: 1 };
|
|
10591
11471
|
const parsed = JSON.parse(raw);
|
|
10592
11472
|
return { version: 1, operatorEmail: typeof parsed.operatorEmail === "string" ? parsed.operatorEmail : void 0 };
|
|
@@ -10597,10 +11477,10 @@ function readFile() {
|
|
|
10597
11477
|
function writeFile(shape) {
|
|
10598
11478
|
const d = dir();
|
|
10599
11479
|
const p = path();
|
|
10600
|
-
if (!(0,
|
|
11480
|
+
if (!(0, import_node_fs5.existsSync)(d)) (0, import_node_fs5.mkdirSync)(d, { recursive: true });
|
|
10601
11481
|
const tmp = `${p}.agenticmail-tmp-${process.pid}`;
|
|
10602
|
-
(0,
|
|
10603
|
-
(0,
|
|
11482
|
+
(0, import_node_fs5.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
|
|
11483
|
+
(0, import_node_fs5.renameSync)(tmp, p);
|
|
10604
11484
|
}
|
|
10605
11485
|
function getOperatorEmail() {
|
|
10606
11486
|
const shape = readFile();
|
|
@@ -10629,21 +11509,21 @@ function operatorPrefsStoragePath() {
|
|
|
10629
11509
|
}
|
|
10630
11510
|
|
|
10631
11511
|
// src/host-sessions.ts
|
|
10632
|
-
var
|
|
10633
|
-
var
|
|
10634
|
-
var
|
|
11512
|
+
var import_node_fs6 = require("fs");
|
|
11513
|
+
var import_node_path8 = require("path");
|
|
11514
|
+
var import_node_os6 = require("os");
|
|
10635
11515
|
function storageDir() {
|
|
10636
|
-
return (0,
|
|
11516
|
+
return (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".agenticmail");
|
|
10637
11517
|
}
|
|
10638
11518
|
function storagePath() {
|
|
10639
|
-
return (0,
|
|
11519
|
+
return (0, import_node_path8.join)(storageDir(), "host-sessions.json");
|
|
10640
11520
|
}
|
|
10641
11521
|
var DEFAULT_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10642
11522
|
function readFile2() {
|
|
10643
11523
|
const p = storagePath();
|
|
10644
|
-
if (!(0,
|
|
11524
|
+
if (!(0, import_node_fs6.existsSync)(p)) return { version: 1, sessions: {} };
|
|
10645
11525
|
try {
|
|
10646
|
-
const raw = (0,
|
|
11526
|
+
const raw = (0, import_node_fs6.readFileSync)(p, "utf-8");
|
|
10647
11527
|
if (!raw.trim()) return { version: 1, sessions: {} };
|
|
10648
11528
|
const parsed = JSON.parse(raw);
|
|
10649
11529
|
return {
|
|
@@ -10657,10 +11537,10 @@ function readFile2() {
|
|
|
10657
11537
|
function writeFile2(shape) {
|
|
10658
11538
|
const dir2 = storageDir();
|
|
10659
11539
|
const p = storagePath();
|
|
10660
|
-
if (!(0,
|
|
11540
|
+
if (!(0, import_node_fs6.existsSync)(dir2)) (0, import_node_fs6.mkdirSync)(dir2, { recursive: true });
|
|
10661
11541
|
const tmp = `${p}.agenticmail-tmp-${process.pid}`;
|
|
10662
|
-
(0,
|
|
10663
|
-
(0,
|
|
11542
|
+
(0, import_node_fs6.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
|
|
11543
|
+
(0, import_node_fs6.renameSync)(tmp, p);
|
|
10664
11544
|
}
|
|
10665
11545
|
function saveHostSession(host, session) {
|
|
10666
11546
|
if (!session.sessionId) return;
|
|
@@ -10835,15 +11715,15 @@ function buildApiUrl(baseOrigin, pathAndQuery) {
|
|
|
10835
11715
|
|
|
10836
11716
|
// src/setup/index.ts
|
|
10837
11717
|
var import_node_crypto6 = require("crypto");
|
|
10838
|
-
var
|
|
10839
|
-
var
|
|
10840
|
-
var
|
|
11718
|
+
var import_node_fs10 = require("fs");
|
|
11719
|
+
var import_node_path12 = require("path");
|
|
11720
|
+
var import_node_os10 = require("os");
|
|
10841
11721
|
|
|
10842
11722
|
// src/setup/deps.ts
|
|
10843
11723
|
var import_node_child_process2 = require("child_process");
|
|
10844
|
-
var
|
|
10845
|
-
var
|
|
10846
|
-
var
|
|
11724
|
+
var import_node_fs7 = require("fs");
|
|
11725
|
+
var import_node_path9 = require("path");
|
|
11726
|
+
var import_node_os7 = require("os");
|
|
10847
11727
|
var DependencyChecker = class {
|
|
10848
11728
|
async checkAll() {
|
|
10849
11729
|
return Promise.all([
|
|
@@ -10893,8 +11773,8 @@ var DependencyChecker = class {
|
|
|
10893
11773
|
}
|
|
10894
11774
|
}
|
|
10895
11775
|
async checkCloudflared() {
|
|
10896
|
-
const binPath = (0,
|
|
10897
|
-
if ((0,
|
|
11776
|
+
const binPath = (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".agenticmail", "bin", "cloudflared");
|
|
11777
|
+
if ((0, import_node_fs7.existsSync)(binPath)) {
|
|
10898
11778
|
let version;
|
|
10899
11779
|
try {
|
|
10900
11780
|
const output = (0, import_node_child_process2.execFileSync)(binPath, ["--version"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
@@ -10916,10 +11796,10 @@ var DependencyChecker = class {
|
|
|
10916
11796
|
|
|
10917
11797
|
// src/setup/installer.ts
|
|
10918
11798
|
var import_node_child_process3 = require("child_process");
|
|
10919
|
-
var
|
|
11799
|
+
var import_node_fs8 = require("fs");
|
|
10920
11800
|
var import_promises2 = require("fs/promises");
|
|
10921
|
-
var
|
|
10922
|
-
var
|
|
11801
|
+
var import_node_path10 = require("path");
|
|
11802
|
+
var import_node_os8 = require("os");
|
|
10923
11803
|
function runShellWithRollingOutput(cmd, opts = {}) {
|
|
10924
11804
|
const maxLines = opts.maxLines ?? 20;
|
|
10925
11805
|
const timeout = opts.timeout ?? 3e5;
|
|
@@ -11029,7 +11909,7 @@ var DependencyInstaller = class {
|
|
|
11029
11909
|
*/
|
|
11030
11910
|
async installDocker() {
|
|
11031
11911
|
if (this.isDockerReady()) return;
|
|
11032
|
-
const os = (0,
|
|
11912
|
+
const os = (0, import_node_os8.platform)();
|
|
11033
11913
|
if (os === "darwin") {
|
|
11034
11914
|
await this.installDockerMac();
|
|
11035
11915
|
} else if (os === "linux") {
|
|
@@ -11095,15 +11975,15 @@ var DependencyInstaller = class {
|
|
|
11095
11975
|
try {
|
|
11096
11976
|
const composeBin = (0, import_node_child_process3.execFileSync)("which", ["docker-compose"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
11097
11977
|
if (!composeBin) return;
|
|
11098
|
-
const pluginDir = (0,
|
|
11099
|
-
const pluginPath = (0,
|
|
11100
|
-
if ((0,
|
|
11978
|
+
const pluginDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".docker", "cli-plugins");
|
|
11979
|
+
const pluginPath = (0, import_node_path10.join)(pluginDir, "docker-compose");
|
|
11980
|
+
if ((0, import_node_fs8.existsSync)(pluginPath)) return;
|
|
11101
11981
|
try {
|
|
11102
|
-
(0,
|
|
11982
|
+
(0, import_node_fs8.mkdirSync)(pluginDir, { recursive: true });
|
|
11103
11983
|
} catch {
|
|
11104
11984
|
}
|
|
11105
11985
|
try {
|
|
11106
|
-
(0,
|
|
11986
|
+
(0, import_node_fs8.symlinkSync)(composeBin, pluginPath);
|
|
11107
11987
|
} catch {
|
|
11108
11988
|
}
|
|
11109
11989
|
} catch {
|
|
@@ -11162,9 +12042,9 @@ var DependencyInstaller = class {
|
|
|
11162
12042
|
return;
|
|
11163
12043
|
}
|
|
11164
12044
|
this.onProgress("__progress__:5:Installing Docker Engine...");
|
|
11165
|
-
const tmpDir = (0,
|
|
12045
|
+
const tmpDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "tmp");
|
|
11166
12046
|
await (0, import_promises2.mkdir)(tmpDir, { recursive: true });
|
|
11167
|
-
const scriptPath = (0,
|
|
12047
|
+
const scriptPath = (0, import_node_path10.join)(tmpDir, "install-docker.sh");
|
|
11168
12048
|
const dlResult = await runShellWithRollingOutput(
|
|
11169
12049
|
`curl -fsSL https://get.docker.com -o "${scriptPath}" && sudo sh "${scriptPath}"`,
|
|
11170
12050
|
{ timeout: 3e5 }
|
|
@@ -11232,8 +12112,8 @@ var DependencyInstaller = class {
|
|
|
11232
12112
|
}
|
|
11233
12113
|
try {
|
|
11234
12114
|
const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
11235
|
-
const dockerExe = (0,
|
|
11236
|
-
if ((0,
|
|
12115
|
+
const dockerExe = (0, import_node_path10.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
|
|
12116
|
+
if ((0, import_node_fs8.existsSync)(dockerExe)) {
|
|
11237
12117
|
(0, import_node_child_process3.execSync)(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
|
|
11238
12118
|
}
|
|
11239
12119
|
} catch {
|
|
@@ -11283,8 +12163,8 @@ var DependencyInstaller = class {
|
|
|
11283
12163
|
this.onProgress("__progress__:70:Docker Desktop installed. Starting...");
|
|
11284
12164
|
try {
|
|
11285
12165
|
const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
11286
|
-
const dockerExe = (0,
|
|
11287
|
-
if ((0,
|
|
12166
|
+
const dockerExe = (0, import_node_path10.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
|
|
12167
|
+
if ((0, import_node_fs8.existsSync)(dockerExe)) {
|
|
11288
12168
|
(0, import_node_child_process3.execSync)(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
|
|
11289
12169
|
}
|
|
11290
12170
|
} catch {
|
|
@@ -11321,12 +12201,12 @@ var DependencyInstaller = class {
|
|
|
11321
12201
|
* Start the Stalwart mail server Docker container.
|
|
11322
12202
|
*/
|
|
11323
12203
|
async startStalwart(composePath) {
|
|
11324
|
-
if (!(0,
|
|
12204
|
+
if (!(0, import_node_fs8.existsSync)(composePath)) {
|
|
11325
12205
|
throw new Error(`docker-compose.yml not found at: ${composePath}`);
|
|
11326
12206
|
}
|
|
11327
12207
|
if (!this.isDockerReady()) {
|
|
11328
12208
|
this.onProgress("Starting Docker...");
|
|
11329
|
-
const os = (0,
|
|
12209
|
+
const os = (0, import_node_os8.platform)();
|
|
11330
12210
|
if (os === "darwin") {
|
|
11331
12211
|
await this.startColima();
|
|
11332
12212
|
} else if (os === "win32") {
|
|
@@ -11344,7 +12224,7 @@ var DependencyInstaller = class {
|
|
|
11344
12224
|
}
|
|
11345
12225
|
}
|
|
11346
12226
|
this.onProgress("__progress__:10:Pulling mail server image...");
|
|
11347
|
-
if ((0,
|
|
12227
|
+
if ((0, import_node_os8.platform)() === "darwin") this.linkComposePlugin();
|
|
11348
12228
|
let composeResult = await runSilent("docker", ["compose", "-f", composePath, "up", "-d"], { timeout: 12e4 });
|
|
11349
12229
|
if (composeResult.exitCode !== 0 && hasCommand("docker-compose")) {
|
|
11350
12230
|
composeResult = await runSilent("docker-compose", ["-f", composePath, "up", "-d"], { timeout: 12e4 });
|
|
@@ -11379,22 +12259,22 @@ var DependencyInstaller = class {
|
|
|
11379
12259
|
* Returns the path to the installed binary.
|
|
11380
12260
|
*/
|
|
11381
12261
|
async installCloudflared() {
|
|
11382
|
-
const os = (0,
|
|
11383
|
-
const binDir = (0,
|
|
12262
|
+
const os = (0, import_node_os8.platform)();
|
|
12263
|
+
const binDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "bin");
|
|
11384
12264
|
const binName = os === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
11385
|
-
const binPath = (0,
|
|
11386
|
-
if ((0,
|
|
12265
|
+
const binPath = (0, import_node_path10.join)(binDir, binName);
|
|
12266
|
+
if ((0, import_node_fs8.existsSync)(binPath)) {
|
|
11387
12267
|
return binPath;
|
|
11388
12268
|
}
|
|
11389
12269
|
try {
|
|
11390
12270
|
const whichCmd = os === "win32" ? "where" : "which";
|
|
11391
12271
|
const sysPath = (0, import_node_child_process3.execFileSync)(whichCmd, ["cloudflared"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split("\n")[0];
|
|
11392
|
-
if (sysPath && (0,
|
|
12272
|
+
if (sysPath && (0, import_node_fs8.existsSync)(sysPath)) return sysPath;
|
|
11393
12273
|
} catch {
|
|
11394
12274
|
}
|
|
11395
12275
|
this.onProgress("Downloading cloudflared...");
|
|
11396
12276
|
await (0, import_promises2.mkdir)(binDir, { recursive: true });
|
|
11397
|
-
const cpu = (0,
|
|
12277
|
+
const cpu = (0, import_node_os8.arch)();
|
|
11398
12278
|
const archName = cpu === "arm64" ? "arm64" : "amd64";
|
|
11399
12279
|
let downloadUrl;
|
|
11400
12280
|
if (os === "darwin") {
|
|
@@ -11412,7 +12292,7 @@ var DependencyInstaller = class {
|
|
|
11412
12292
|
}
|
|
11413
12293
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
11414
12294
|
if (os === "darwin") {
|
|
11415
|
-
const tgzPath = (0,
|
|
12295
|
+
const tgzPath = (0, import_node_path10.join)(binDir, "cloudflared.tgz");
|
|
11416
12296
|
await (0, import_promises2.writeFile)(tgzPath, buffer);
|
|
11417
12297
|
try {
|
|
11418
12298
|
(0, import_node_child_process3.execFileSync)("tar", ["-xzf", tgzPath, "-C", binDir, "cloudflared"], { timeout: 15e3, stdio: "ignore" });
|
|
@@ -11429,7 +12309,7 @@ var DependencyInstaller = class {
|
|
|
11429
12309
|
if (os !== "win32") await (0, import_promises2.chmod)(tmpPath, 493);
|
|
11430
12310
|
await (0, import_promises2.rename)(tmpPath, binPath);
|
|
11431
12311
|
}
|
|
11432
|
-
if (!(0,
|
|
12312
|
+
if (!(0, import_node_fs8.existsSync)(binPath)) {
|
|
11433
12313
|
throw new Error("cloudflared download succeeded but binary not found after extraction");
|
|
11434
12314
|
}
|
|
11435
12315
|
this.onProgress("cloudflared installed");
|
|
@@ -11447,23 +12327,23 @@ var DependencyInstaller = class {
|
|
|
11447
12327
|
|
|
11448
12328
|
// src/setup/service.ts
|
|
11449
12329
|
var import_node_child_process4 = require("child_process");
|
|
11450
|
-
var
|
|
11451
|
-
var
|
|
11452
|
-
var
|
|
12330
|
+
var import_node_fs9 = require("fs");
|
|
12331
|
+
var import_node_path11 = require("path");
|
|
12332
|
+
var import_node_os9 = require("os");
|
|
11453
12333
|
var import_node_module2 = require("module");
|
|
11454
|
-
var
|
|
12334
|
+
var import_meta3 = {};
|
|
11455
12335
|
var PLIST_LABEL = "com.agenticmail.server";
|
|
11456
12336
|
var SYSTEMD_UNIT = "agenticmail.service";
|
|
11457
12337
|
var ServiceManager = class {
|
|
11458
|
-
os = (0,
|
|
12338
|
+
os = (0, import_node_os9.platform)();
|
|
11459
12339
|
/**
|
|
11460
12340
|
* Get the path to the service file.
|
|
11461
12341
|
*/
|
|
11462
12342
|
getServicePath() {
|
|
11463
12343
|
if (this.os === "darwin") {
|
|
11464
|
-
return (0,
|
|
12344
|
+
return (0, import_node_path11.join)((0, import_node_os9.homedir)(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
11465
12345
|
} else {
|
|
11466
|
-
return (0,
|
|
12346
|
+
return (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
11467
12347
|
}
|
|
11468
12348
|
}
|
|
11469
12349
|
/**
|
|
@@ -11495,42 +12375,42 @@ var ServiceManager = class {
|
|
|
11495
12375
|
*/
|
|
11496
12376
|
getApiEntryPath() {
|
|
11497
12377
|
try {
|
|
11498
|
-
const req = (0, import_node_module2.createRequire)(
|
|
12378
|
+
const req = (0, import_node_module2.createRequire)(import_meta3.url);
|
|
11499
12379
|
const resolved = req.resolve("@agenticmail/api");
|
|
11500
|
-
if ((0,
|
|
12380
|
+
if ((0, import_node_fs9.existsSync)(resolved)) return resolved;
|
|
11501
12381
|
} catch {
|
|
11502
12382
|
}
|
|
11503
12383
|
const parentPackages = [
|
|
11504
|
-
(0,
|
|
12384
|
+
(0, import_node_path11.join)("@agenticmail", "cli"),
|
|
11505
12385
|
// current scoped package
|
|
11506
12386
|
"agenticmail"
|
|
11507
12387
|
// legacy unscoped package
|
|
11508
12388
|
];
|
|
11509
12389
|
const baseDirs = [
|
|
11510
12390
|
// user-local install
|
|
11511
|
-
(0,
|
|
12391
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules")
|
|
11512
12392
|
];
|
|
11513
12393
|
try {
|
|
11514
12394
|
const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
11515
|
-
baseDirs.push((0,
|
|
11516
|
-
baseDirs.push((0,
|
|
12395
|
+
baseDirs.push((0, import_node_path11.join)(prefix, "lib", "node_modules"));
|
|
12396
|
+
baseDirs.push((0, import_node_path11.join)(prefix, "node_modules"));
|
|
11517
12397
|
} catch {
|
|
11518
12398
|
}
|
|
11519
12399
|
baseDirs.push("/opt/homebrew/lib/node_modules");
|
|
11520
12400
|
baseDirs.push("/usr/local/lib/node_modules");
|
|
11521
12401
|
for (const base of baseDirs) {
|
|
11522
|
-
const sibling = (0,
|
|
11523
|
-
if ((0,
|
|
12402
|
+
const sibling = (0, import_node_path11.join)(base, "@agenticmail", "api", "dist", "index.js");
|
|
12403
|
+
if ((0, import_node_fs9.existsSync)(sibling)) return sibling;
|
|
11524
12404
|
for (const parent of parentPackages) {
|
|
11525
|
-
const nested = (0,
|
|
11526
|
-
if ((0,
|
|
12405
|
+
const nested = (0, import_node_path11.join)(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
|
|
12406
|
+
if ((0, import_node_fs9.existsSync)(nested)) return nested;
|
|
11527
12407
|
}
|
|
11528
12408
|
}
|
|
11529
|
-
const dataDir = (0,
|
|
11530
|
-
const entryCache = (0,
|
|
11531
|
-
if ((0,
|
|
11532
|
-
const cached = (0,
|
|
11533
|
-
if (cached && (0,
|
|
12409
|
+
const dataDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
|
|
12410
|
+
const entryCache = (0, import_node_path11.join)(dataDir, "api-entry.path");
|
|
12411
|
+
if ((0, import_node_fs9.existsSync)(entryCache)) {
|
|
12412
|
+
const cached = (0, import_node_fs9.readFileSync)(entryCache, "utf-8").trim();
|
|
12413
|
+
if (cached && (0, import_node_fs9.existsSync)(cached)) return cached;
|
|
11534
12414
|
}
|
|
11535
12415
|
throw new Error("Could not find @agenticmail/api entry point. Run `agenticmail start` first to populate the cache.");
|
|
11536
12416
|
}
|
|
@@ -11538,9 +12418,9 @@ var ServiceManager = class {
|
|
|
11538
12418
|
* Cache the API entry path so the service can find it later.
|
|
11539
12419
|
*/
|
|
11540
12420
|
cacheApiEntryPath(entryPath) {
|
|
11541
|
-
const dataDir = (0,
|
|
11542
|
-
if (!(0,
|
|
11543
|
-
(0,
|
|
12421
|
+
const dataDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
|
|
12422
|
+
if (!(0, import_node_fs9.existsSync)(dataDir)) (0, import_node_fs9.mkdirSync)(dataDir, { recursive: true });
|
|
12423
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path11.join)(dataDir, "api-entry.path"), entryPath);
|
|
11544
12424
|
}
|
|
11545
12425
|
/**
|
|
11546
12426
|
* Get the current package version.
|
|
@@ -11551,38 +12431,38 @@ var ServiceManager = class {
|
|
|
11551
12431
|
*/
|
|
11552
12432
|
getVersion() {
|
|
11553
12433
|
try {
|
|
11554
|
-
const req = (0, import_node_module2.createRequire)(
|
|
12434
|
+
const req = (0, import_node_module2.createRequire)(import_meta3.url);
|
|
11555
12435
|
const pkgJson = req.resolve("@agenticmail/cli/package.json");
|
|
11556
|
-
if ((0,
|
|
11557
|
-
const pkg = JSON.parse((0,
|
|
12436
|
+
if ((0, import_node_fs9.existsSync)(pkgJson)) {
|
|
12437
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(pkgJson, "utf-8"));
|
|
11558
12438
|
if (pkg.version) return pkg.version;
|
|
11559
12439
|
}
|
|
11560
12440
|
} catch {
|
|
11561
12441
|
}
|
|
11562
12442
|
try {
|
|
11563
12443
|
const apiEntry = this.getApiEntryPath();
|
|
11564
|
-
const apiPkg = (0,
|
|
11565
|
-
if ((0,
|
|
11566
|
-
const pkg = JSON.parse((0,
|
|
12444
|
+
const apiPkg = (0, import_node_path11.join)(apiEntry, "..", "..", "package.json");
|
|
12445
|
+
if ((0, import_node_fs9.existsSync)(apiPkg)) {
|
|
12446
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(apiPkg, "utf-8"));
|
|
11567
12447
|
if (pkg.version) return pkg.version;
|
|
11568
12448
|
}
|
|
11569
12449
|
} catch {
|
|
11570
12450
|
}
|
|
11571
12451
|
const candidates = [
|
|
11572
|
-
(0,
|
|
11573
|
-
(0,
|
|
11574
|
-
(0,
|
|
12452
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules", "@agenticmail", "cli", "package.json"),
|
|
12453
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules", "agenticmail", "package.json"),
|
|
12454
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "package-version.json")
|
|
11575
12455
|
];
|
|
11576
12456
|
try {
|
|
11577
12457
|
const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
11578
|
-
candidates.push((0,
|
|
11579
|
-
candidates.push((0,
|
|
12458
|
+
candidates.push((0, import_node_path11.join)(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
|
|
12459
|
+
candidates.push((0, import_node_path11.join)(prefix, "lib", "node_modules", "agenticmail", "package.json"));
|
|
11580
12460
|
} catch {
|
|
11581
12461
|
}
|
|
11582
12462
|
for (const p of candidates) {
|
|
11583
12463
|
try {
|
|
11584
|
-
if ((0,
|
|
11585
|
-
const pkg = JSON.parse((0,
|
|
12464
|
+
if ((0, import_node_fs9.existsSync)(p)) {
|
|
12465
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(p, "utf-8"));
|
|
11586
12466
|
if (pkg.version) return pkg.version;
|
|
11587
12467
|
}
|
|
11588
12468
|
} catch {
|
|
@@ -11595,9 +12475,9 @@ var ServiceManager = class {
|
|
|
11595
12475
|
* This ensures AgenticMail doesn't fail on boot when Docker is still loading.
|
|
11596
12476
|
*/
|
|
11597
12477
|
generateStartScript(nodePath, apiEntry) {
|
|
11598
|
-
const scriptPath = (0,
|
|
11599
|
-
const scriptDir = (0,
|
|
11600
|
-
if (!(0,
|
|
12478
|
+
const scriptPath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin", "start-server.sh");
|
|
12479
|
+
const scriptDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin");
|
|
12480
|
+
if (!(0, import_node_fs9.existsSync)(scriptDir)) (0, import_node_fs9.mkdirSync)(scriptDir, { recursive: true });
|
|
11601
12481
|
const script = [
|
|
11602
12482
|
"#!/bin/bash",
|
|
11603
12483
|
"# AgenticMail auto-start script",
|
|
@@ -11648,7 +12528,7 @@ var ServiceManager = class {
|
|
|
11648
12528
|
`log "Starting API server: ${nodePath} ${apiEntry}"`,
|
|
11649
12529
|
`exec "${nodePath}" "${apiEntry}"`
|
|
11650
12530
|
].join("\n") + "\n";
|
|
11651
|
-
(0,
|
|
12531
|
+
(0, import_node_fs9.writeFileSync)(scriptPath, script, { mode: 493 });
|
|
11652
12532
|
return scriptPath;
|
|
11653
12533
|
}
|
|
11654
12534
|
/**
|
|
@@ -11661,9 +12541,9 @@ var ServiceManager = class {
|
|
|
11661
12541
|
* - Service version tracking in env vars
|
|
11662
12542
|
*/
|
|
11663
12543
|
generatePlist(nodePath, apiEntry, configPath) {
|
|
11664
|
-
const config = JSON.parse((0,
|
|
11665
|
-
const logDir = (0,
|
|
11666
|
-
if (!(0,
|
|
12544
|
+
const config = JSON.parse((0, import_node_fs9.readFileSync)(configPath, "utf-8"));
|
|
12545
|
+
const logDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "logs");
|
|
12546
|
+
if (!(0, import_node_fs9.existsSync)(logDir)) (0, import_node_fs9.mkdirSync)(logDir, { recursive: true });
|
|
11667
12547
|
const version = this.getVersion();
|
|
11668
12548
|
const startScript = this.generateStartScript(nodePath, apiEntry);
|
|
11669
12549
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -11684,9 +12564,9 @@ var ServiceManager = class {
|
|
|
11684
12564
|
<key>EnvironmentVariables</key>
|
|
11685
12565
|
<dict>
|
|
11686
12566
|
<key>HOME</key>
|
|
11687
|
-
<string>${(0,
|
|
12567
|
+
<string>${(0, import_node_os9.homedir)()}</string>
|
|
11688
12568
|
<key>AGENTICMAIL_DATA_DIR</key>
|
|
11689
|
-
<string>${config.dataDir || (0,
|
|
12569
|
+
<string>${config.dataDir || (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail")}</string>
|
|
11690
12570
|
<key>PATH</key>
|
|
11691
12571
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
11692
12572
|
<key>AGENTICMAIL_SERVICE_VERSION</key>
|
|
@@ -11739,8 +12619,8 @@ var ServiceManager = class {
|
|
|
11739
12619
|
* - Proper dependency ordering
|
|
11740
12620
|
*/
|
|
11741
12621
|
generateSystemdUnit(nodePath, apiEntry, configPath) {
|
|
11742
|
-
const config = JSON.parse((0,
|
|
11743
|
-
const dataDir = config.dataDir || (0,
|
|
12622
|
+
const config = JSON.parse((0, import_node_fs9.readFileSync)(configPath, "utf-8"));
|
|
12623
|
+
const dataDir = config.dataDir || (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
|
|
11744
12624
|
const version = this.getVersion();
|
|
11745
12625
|
const startScript = this.generateStartScript(nodePath, apiEntry);
|
|
11746
12626
|
return `[Unit]
|
|
@@ -11757,7 +12637,7 @@ Restart=always
|
|
|
11757
12637
|
RestartSec=15
|
|
11758
12638
|
TimeoutStartSec=660
|
|
11759
12639
|
LimitNOFILE=8192
|
|
11760
|
-
Environment=HOME=${(0,
|
|
12640
|
+
Environment=HOME=${(0, import_node_os9.homedir)()}
|
|
11761
12641
|
Environment=AGENTICMAIL_DATA_DIR=${dataDir}
|
|
11762
12642
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin
|
|
11763
12643
|
Environment=AGENTICMAIL_SERVICE_VERSION=${version}
|
|
@@ -11770,8 +12650,8 @@ WantedBy=default.target
|
|
|
11770
12650
|
* Install the auto-start service.
|
|
11771
12651
|
*/
|
|
11772
12652
|
install() {
|
|
11773
|
-
const configPath = (0,
|
|
11774
|
-
if (!(0,
|
|
12653
|
+
const configPath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "config.json");
|
|
12654
|
+
if (!(0, import_node_fs9.existsSync)(configPath)) {
|
|
11775
12655
|
return { installed: false, message: "Config not found. Run agenticmail setup first." };
|
|
11776
12656
|
}
|
|
11777
12657
|
const nodePath = this.getNodePath();
|
|
@@ -11783,17 +12663,17 @@ WantedBy=default.target
|
|
|
11783
12663
|
}
|
|
11784
12664
|
const servicePath = this.getServicePath();
|
|
11785
12665
|
if (this.os === "darwin") {
|
|
11786
|
-
const dir2 = (0,
|
|
11787
|
-
if (!(0,
|
|
11788
|
-
if ((0,
|
|
12666
|
+
const dir2 = (0, import_node_path11.join)((0, import_node_os9.homedir)(), "Library", "LaunchAgents");
|
|
12667
|
+
if (!(0, import_node_fs9.existsSync)(dir2)) (0, import_node_fs9.mkdirSync)(dir2, { recursive: true });
|
|
12668
|
+
if ((0, import_node_fs9.existsSync)(servicePath)) {
|
|
11789
12669
|
try {
|
|
11790
12670
|
(0, import_node_child_process4.execFileSync)("launchctl", ["unload", servicePath], { timeout: 1e4, stdio: "ignore" });
|
|
11791
12671
|
} catch {
|
|
11792
12672
|
}
|
|
11793
12673
|
}
|
|
11794
12674
|
const plist = this.generatePlist(nodePath, apiEntry, configPath);
|
|
11795
|
-
(0,
|
|
11796
|
-
(0,
|
|
12675
|
+
(0, import_node_fs9.writeFileSync)(servicePath, plist);
|
|
12676
|
+
(0, import_node_fs9.chmodSync)(servicePath, 384);
|
|
11797
12677
|
try {
|
|
11798
12678
|
(0, import_node_child_process4.execFileSync)("launchctl", ["load", servicePath], { timeout: 1e4, stdio: "ignore" });
|
|
11799
12679
|
} catch (err) {
|
|
@@ -11801,11 +12681,11 @@ WantedBy=default.target
|
|
|
11801
12681
|
}
|
|
11802
12682
|
return { installed: true, message: `Service installed at ${servicePath}` };
|
|
11803
12683
|
} else if (this.os === "linux") {
|
|
11804
|
-
const dir2 = (0,
|
|
11805
|
-
if (!(0,
|
|
12684
|
+
const dir2 = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".config", "systemd", "user");
|
|
12685
|
+
if (!(0, import_node_fs9.existsSync)(dir2)) (0, import_node_fs9.mkdirSync)(dir2, { recursive: true });
|
|
11806
12686
|
const unit = this.generateSystemdUnit(nodePath, apiEntry, configPath);
|
|
11807
|
-
(0,
|
|
11808
|
-
(0,
|
|
12687
|
+
(0, import_node_fs9.writeFileSync)(servicePath, unit);
|
|
12688
|
+
(0, import_node_fs9.chmodSync)(servicePath, 384);
|
|
11809
12689
|
try {
|
|
11810
12690
|
(0, import_node_child_process4.execFileSync)("systemctl", ["--user", "daemon-reload"], { timeout: 1e4, stdio: "ignore" });
|
|
11811
12691
|
(0, import_node_child_process4.execFileSync)("systemctl", ["--user", "enable", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
|
|
@@ -11827,7 +12707,7 @@ WantedBy=default.target
|
|
|
11827
12707
|
*/
|
|
11828
12708
|
uninstall() {
|
|
11829
12709
|
const servicePath = this.getServicePath();
|
|
11830
|
-
if (!(0,
|
|
12710
|
+
if (!(0, import_node_fs9.existsSync)(servicePath)) {
|
|
11831
12711
|
return { removed: false, message: "Service is not installed." };
|
|
11832
12712
|
}
|
|
11833
12713
|
if (this.os === "darwin") {
|
|
@@ -11836,7 +12716,7 @@ WantedBy=default.target
|
|
|
11836
12716
|
} catch {
|
|
11837
12717
|
}
|
|
11838
12718
|
try {
|
|
11839
|
-
(0,
|
|
12719
|
+
(0, import_node_fs9.unlinkSync)(servicePath);
|
|
11840
12720
|
} catch {
|
|
11841
12721
|
}
|
|
11842
12722
|
return { removed: true, message: "Service removed." };
|
|
@@ -11847,7 +12727,7 @@ WantedBy=default.target
|
|
|
11847
12727
|
} catch {
|
|
11848
12728
|
}
|
|
11849
12729
|
try {
|
|
11850
|
-
(0,
|
|
12730
|
+
(0, import_node_fs9.unlinkSync)(servicePath);
|
|
11851
12731
|
} catch {
|
|
11852
12732
|
}
|
|
11853
12733
|
try {
|
|
@@ -11865,7 +12745,7 @@ WantedBy=default.target
|
|
|
11865
12745
|
status() {
|
|
11866
12746
|
const servicePath = this.getServicePath();
|
|
11867
12747
|
const plat = this.os === "darwin" ? "launchd" : this.os === "linux" ? "systemd" : "unsupported";
|
|
11868
|
-
const installed = (0,
|
|
12748
|
+
const installed = (0, import_node_fs9.existsSync)(servicePath);
|
|
11869
12749
|
let running = false;
|
|
11870
12750
|
if (installed) {
|
|
11871
12751
|
if (this.os === "darwin") {
|
|
@@ -11920,34 +12800,34 @@ WantedBy=default.target
|
|
|
11920
12800
|
needsRepair() {
|
|
11921
12801
|
if (this.os !== "darwin" && this.os !== "linux") return null;
|
|
11922
12802
|
const servicePath = this.getServicePath();
|
|
11923
|
-
if (!(0,
|
|
12803
|
+
if (!(0, import_node_fs9.existsSync)(servicePath)) return null;
|
|
11924
12804
|
let serviceContent = "";
|
|
11925
12805
|
try {
|
|
11926
|
-
serviceContent = (0,
|
|
12806
|
+
serviceContent = (0, import_node_fs9.readFileSync)(servicePath, "utf-8");
|
|
11927
12807
|
} catch {
|
|
11928
12808
|
return { reason: "Service file unreadable" };
|
|
11929
12809
|
}
|
|
11930
|
-
const startScript = (0,
|
|
12810
|
+
const startScript = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin", "start-server.sh");
|
|
11931
12811
|
if (serviceContent.includes(startScript)) {
|
|
11932
|
-
if (!(0,
|
|
12812
|
+
if (!(0, import_node_fs9.existsSync)(startScript)) {
|
|
11933
12813
|
return { reason: "start-server.sh is missing" };
|
|
11934
12814
|
}
|
|
11935
12815
|
let scriptContent = "";
|
|
11936
12816
|
try {
|
|
11937
|
-
scriptContent = (0,
|
|
12817
|
+
scriptContent = (0, import_node_fs9.readFileSync)(startScript, "utf-8");
|
|
11938
12818
|
} catch {
|
|
11939
12819
|
return { reason: "start-server.sh unreadable" };
|
|
11940
12820
|
}
|
|
11941
12821
|
const apiPathMatch = scriptContent.match(/(\/[^"\s]+@agenticmail\/api\/dist\/index\.js)/);
|
|
11942
12822
|
if (apiPathMatch) {
|
|
11943
12823
|
const referenced = apiPathMatch[1];
|
|
11944
|
-
if (!(0,
|
|
12824
|
+
if (!(0, import_node_fs9.existsSync)(referenced)) {
|
|
11945
12825
|
return { reason: `start-server.sh references missing path: ${referenced}` };
|
|
11946
12826
|
}
|
|
11947
12827
|
}
|
|
11948
12828
|
if (/node_modules\/agenticmail\/(?!.*@agenticmail\/cli)/.test(scriptContent)) {
|
|
11949
12829
|
const stale = /(\S*node_modules\/agenticmail\/\S*)/.exec(scriptContent)?.[1];
|
|
11950
|
-
if (stale && !(0,
|
|
12830
|
+
if (stale && !(0, import_node_fs9.existsSync)(stale)) {
|
|
11951
12831
|
return { reason: `start-server.sh references legacy unscoped path: ${stale}` };
|
|
11952
12832
|
}
|
|
11953
12833
|
}
|
|
@@ -11960,11 +12840,11 @@ WantedBy=default.target
|
|
|
11960
12840
|
return { reason: `Service version drift (current CLI is v${currentVersion})` };
|
|
11961
12841
|
}
|
|
11962
12842
|
}
|
|
11963
|
-
const entryCache = (0,
|
|
11964
|
-
if ((0,
|
|
12843
|
+
const entryCache = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "api-entry.path");
|
|
12844
|
+
if ((0, import_node_fs9.existsSync)(entryCache)) {
|
|
11965
12845
|
try {
|
|
11966
|
-
const cached = (0,
|
|
11967
|
-
if (cached && !(0,
|
|
12846
|
+
const cached = (0, import_node_fs9.readFileSync)(entryCache, "utf-8").trim();
|
|
12847
|
+
if (cached && !(0, import_node_fs9.existsSync)(cached)) {
|
|
11968
12848
|
return { reason: `Cached API entry path no longer exists: ${cached}` };
|
|
11969
12849
|
}
|
|
11970
12850
|
} catch {
|
|
@@ -12015,13 +12895,13 @@ var SetupManager = class {
|
|
|
12015
12895
|
* falls back to monorepo location.
|
|
12016
12896
|
*/
|
|
12017
12897
|
getComposePath() {
|
|
12018
|
-
const standalonePath = (0,
|
|
12019
|
-
if ((0,
|
|
12898
|
+
const standalonePath = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail", "docker-compose.yml");
|
|
12899
|
+
if ((0, import_node_fs10.existsSync)(standalonePath)) return standalonePath;
|
|
12020
12900
|
const cwd = process.cwd();
|
|
12021
|
-
const candidates = [cwd, (0,
|
|
12901
|
+
const candidates = [cwd, (0, import_node_path12.join)(cwd, "..")];
|
|
12022
12902
|
for (const dir2 of candidates) {
|
|
12023
|
-
const p = (0,
|
|
12024
|
-
if ((0,
|
|
12903
|
+
const p = (0, import_node_path12.join)(dir2, "docker-compose.yml");
|
|
12904
|
+
if ((0, import_node_fs10.existsSync)(p)) return p;
|
|
12025
12905
|
}
|
|
12026
12906
|
return standalonePath;
|
|
12027
12907
|
}
|
|
@@ -12031,19 +12911,19 @@ var SetupManager = class {
|
|
|
12031
12911
|
* Always regenerates Docker files to keep passwords in sync.
|
|
12032
12912
|
*/
|
|
12033
12913
|
initConfig() {
|
|
12034
|
-
const dataDir = (0,
|
|
12035
|
-
const configPath = (0,
|
|
12036
|
-
const envPath = (0,
|
|
12037
|
-
if ((0,
|
|
12914
|
+
const dataDir = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail");
|
|
12915
|
+
const configPath = (0, import_node_path12.join)(dataDir, "config.json");
|
|
12916
|
+
const envPath = (0, import_node_path12.join)(dataDir, ".env");
|
|
12917
|
+
if ((0, import_node_fs10.existsSync)(configPath)) {
|
|
12038
12918
|
try {
|
|
12039
|
-
const existing = JSON.parse((0,
|
|
12919
|
+
const existing = JSON.parse((0, import_node_fs10.readFileSync)(configPath, "utf-8"));
|
|
12040
12920
|
this.generateDockerFiles(existing);
|
|
12041
12921
|
return { configPath, envPath, config: existing, isNew: false };
|
|
12042
12922
|
} catch {
|
|
12043
12923
|
}
|
|
12044
12924
|
}
|
|
12045
|
-
if (!(0,
|
|
12046
|
-
(0,
|
|
12925
|
+
if (!(0, import_node_fs10.existsSync)(dataDir)) {
|
|
12926
|
+
(0, import_node_fs10.mkdirSync)(dataDir, { recursive: true });
|
|
12047
12927
|
}
|
|
12048
12928
|
const masterKey = `mk_${(0, import_node_crypto6.randomBytes)(24).toString("hex")}`;
|
|
12049
12929
|
const stalwartPassword = (0, import_node_crypto6.randomBytes)(16).toString("hex");
|
|
@@ -12059,8 +12939,8 @@ var SetupManager = class {
|
|
|
12059
12939
|
api: { port: 3829, host: "127.0.0.1" },
|
|
12060
12940
|
dataDir
|
|
12061
12941
|
};
|
|
12062
|
-
(0,
|
|
12063
|
-
(0,
|
|
12942
|
+
(0, import_node_fs10.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
12943
|
+
(0, import_node_fs10.chmodSync)(configPath, 384);
|
|
12064
12944
|
const envContent = `# Auto-generated by agenticmail setup
|
|
12065
12945
|
STALWART_ADMIN_USER=admin
|
|
12066
12946
|
STALWART_ADMIN_PASSWORD=${stalwartPassword}
|
|
@@ -12075,8 +12955,8 @@ SMTP_PORT=587
|
|
|
12075
12955
|
IMAP_HOST=localhost
|
|
12076
12956
|
IMAP_PORT=143
|
|
12077
12957
|
`;
|
|
12078
|
-
(0,
|
|
12079
|
-
(0,
|
|
12958
|
+
(0, import_node_fs10.writeFileSync)(envPath, envContent);
|
|
12959
|
+
(0, import_node_fs10.chmodSync)(envPath, 384);
|
|
12080
12960
|
this.generateDockerFiles(config);
|
|
12081
12961
|
return { configPath, envPath, config, isNew: true };
|
|
12082
12962
|
}
|
|
@@ -12085,13 +12965,13 @@ IMAP_PORT=143
|
|
|
12085
12965
|
* with the correct admin password from config.
|
|
12086
12966
|
*/
|
|
12087
12967
|
generateDockerFiles(config) {
|
|
12088
|
-
const dataDir = config.dataDir || (0,
|
|
12089
|
-
if (!(0,
|
|
12090
|
-
(0,
|
|
12968
|
+
const dataDir = config.dataDir || (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail");
|
|
12969
|
+
if (!(0, import_node_fs10.existsSync)(dataDir)) {
|
|
12970
|
+
(0, import_node_fs10.mkdirSync)(dataDir, { recursive: true });
|
|
12091
12971
|
}
|
|
12092
12972
|
const password = config.stalwart?.adminPassword || "changeme";
|
|
12093
|
-
const composePath = (0,
|
|
12094
|
-
(0,
|
|
12973
|
+
const composePath = (0, import_node_path12.join)(dataDir, "docker-compose.yml");
|
|
12974
|
+
(0, import_node_fs10.writeFileSync)(composePath, `services:
|
|
12095
12975
|
stalwart:
|
|
12096
12976
|
# Pinned to v0.15.5 \u2014 Stalwart 0.16+ moved its config to JSON
|
|
12097
12977
|
# at /etc/stalwart/config.json (hardcoded into the container
|
|
@@ -12120,9 +13000,9 @@ IMAP_PORT=143
|
|
|
12120
13000
|
volumes:
|
|
12121
13001
|
stalwart-data:
|
|
12122
13002
|
`);
|
|
12123
|
-
(0,
|
|
12124
|
-
const tomlPath = (0,
|
|
12125
|
-
(0,
|
|
13003
|
+
(0, import_node_fs10.chmodSync)(composePath, 384);
|
|
13004
|
+
const tomlPath = (0, import_node_path12.join)(dataDir, "stalwart.toml");
|
|
13005
|
+
(0, import_node_fs10.writeFileSync)(tomlPath, `# Stalwart Mail Server \u2014 AgenticMail Configuration
|
|
12126
13006
|
|
|
12127
13007
|
[server]
|
|
12128
13008
|
hostname = "localhost"
|
|
@@ -12172,27 +13052,27 @@ enable = true
|
|
|
12172
13052
|
user = "admin"
|
|
12173
13053
|
secret = "${password}"
|
|
12174
13054
|
`);
|
|
12175
|
-
(0,
|
|
13055
|
+
(0, import_node_fs10.chmodSync)(tomlPath, 384);
|
|
12176
13056
|
}
|
|
12177
13057
|
/**
|
|
12178
13058
|
* Check if config has already been initialized.
|
|
12179
13059
|
*/
|
|
12180
13060
|
isInitialized() {
|
|
12181
|
-
const configPath = (0,
|
|
12182
|
-
return (0,
|
|
13061
|
+
const configPath = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail", "config.json");
|
|
13062
|
+
return (0, import_node_fs10.existsSync)(configPath);
|
|
12183
13063
|
}
|
|
12184
13064
|
};
|
|
12185
13065
|
|
|
12186
13066
|
// src/media/manager.ts
|
|
12187
13067
|
var import_node_child_process6 = require("child_process");
|
|
12188
13068
|
var import_node_util = require("util");
|
|
12189
|
-
var
|
|
12190
|
-
var
|
|
13069
|
+
var import_node_fs12 = require("fs");
|
|
13070
|
+
var import_node_path13 = require("path");
|
|
12191
13071
|
|
|
12192
13072
|
// src/media/binaries.ts
|
|
12193
13073
|
var import_node_child_process5 = require("child_process");
|
|
12194
|
-
var
|
|
12195
|
-
var
|
|
13074
|
+
var import_node_fs11 = require("fs");
|
|
13075
|
+
var import_meta4 = {};
|
|
12196
13076
|
var BINARY_SPECS = {
|
|
12197
13077
|
ffmpeg: {
|
|
12198
13078
|
binary: "ffmpeg",
|
|
@@ -12265,7 +13145,7 @@ function probeCommand(command, spec) {
|
|
|
12265
13145
|
}
|
|
12266
13146
|
function detectEdgeTts(spec) {
|
|
12267
13147
|
try {
|
|
12268
|
-
const resolved =
|
|
13148
|
+
const resolved = import_meta4.resolve?.("node-edge-tts");
|
|
12269
13149
|
if (resolved) {
|
|
12270
13150
|
return {
|
|
12271
13151
|
binary: "edge-tts",
|
|
@@ -12332,7 +13212,7 @@ function requireWhisperModel(modelPath) {
|
|
|
12332
13212
|
"A whisper.cpp model file is required (whisperModel option). Download one, e.g. ggml-base.en.bin, from https://huggingface.co/ggerganov/whisper.cpp and pass its absolute path."
|
|
12333
13213
|
);
|
|
12334
13214
|
}
|
|
12335
|
-
if (!(0,
|
|
13215
|
+
if (!(0, import_node_fs11.existsSync)(modelPath)) {
|
|
12336
13216
|
throw new Error(`whisper model file not found: ${modelPath}`);
|
|
12337
13217
|
}
|
|
12338
13218
|
return modelPath;
|
|
@@ -12384,7 +13264,7 @@ function validateInputPath(path2, label = "input") {
|
|
|
12384
13264
|
`${label} file path may not start with "-" \u2014 pass an absolute path so it cannot be parsed as a command flag`
|
|
12385
13265
|
);
|
|
12386
13266
|
}
|
|
12387
|
-
if (!(0,
|
|
13267
|
+
if (!(0, import_node_fs12.existsSync)(path2)) {
|
|
12388
13268
|
throw new Error(`${label} file not found: ${path2}`);
|
|
12389
13269
|
}
|
|
12390
13270
|
return path2;
|
|
@@ -12406,32 +13286,32 @@ var MediaManager = class {
|
|
|
12406
13286
|
if (options.outputDir) {
|
|
12407
13287
|
this.outputDir = options.outputDir;
|
|
12408
13288
|
} else if (options.dataDir) {
|
|
12409
|
-
this.outputDir = (0,
|
|
13289
|
+
this.outputDir = (0, import_node_path13.join)(options.dataDir, "media");
|
|
12410
13290
|
} else {
|
|
12411
13291
|
const tmp = process.env.TMPDIR || process.env.TEMP || "/tmp";
|
|
12412
|
-
this.outputDir = (0,
|
|
13292
|
+
this.outputDir = (0, import_node_path13.join)(tmp, "agenticmail-media");
|
|
12413
13293
|
}
|
|
12414
13294
|
}
|
|
12415
13295
|
/** Ensure the output directory exists; returns it. */
|
|
12416
13296
|
ensureOutputDir() {
|
|
12417
|
-
if (!(0,
|
|
12418
|
-
(0,
|
|
13297
|
+
if (!(0, import_node_fs12.existsSync)(this.outputDir)) {
|
|
13298
|
+
(0, import_node_fs12.mkdirSync)(this.outputDir, { recursive: true });
|
|
12419
13299
|
}
|
|
12420
13300
|
return this.outputDir;
|
|
12421
13301
|
}
|
|
12422
13302
|
/** Build an output path inside the managed output dir. */
|
|
12423
13303
|
outPath(prefix, ext) {
|
|
12424
|
-
return (0,
|
|
13304
|
+
return (0, import_node_path13.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}.${ext}`);
|
|
12425
13305
|
}
|
|
12426
13306
|
/** Build a sub-directory inside the managed output dir. */
|
|
12427
13307
|
outDir(prefix) {
|
|
12428
|
-
const dir2 = (0,
|
|
12429
|
-
(0,
|
|
13308
|
+
const dir2 = (0, import_node_path13.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
13309
|
+
(0, import_node_fs12.mkdirSync)(dir2, { recursive: true });
|
|
12430
13310
|
return dir2;
|
|
12431
13311
|
}
|
|
12432
13312
|
/** Stat a produced file into a {@link MediaFileResult} envelope. */
|
|
12433
13313
|
fileResult(path2, extra = {}) {
|
|
12434
|
-
const stat = (0,
|
|
13314
|
+
const stat = (0, import_node_fs12.statSync)(path2);
|
|
12435
13315
|
return { ok: true, filePath: path2, sizeBytes: stat.size, ...extra };
|
|
12436
13316
|
}
|
|
12437
13317
|
// ─── binary invocation helpers (execFile, arg arrays, no shell) ────
|
|
@@ -12545,7 +13425,7 @@ var MediaManager = class {
|
|
|
12545
13425
|
/** Edit an image with ImageMagick. */
|
|
12546
13426
|
async imageEdit(opts) {
|
|
12547
13427
|
const input = validateInputPath(opts.input);
|
|
12548
|
-
const ext = safeExtension(opts.format, (0,
|
|
13428
|
+
const ext = safeExtension(opts.format, (0, import_node_path13.extname)(input).slice(1) || "png");
|
|
12549
13429
|
const out = this.outPath("img", ext);
|
|
12550
13430
|
switch (opts.action) {
|
|
12551
13431
|
case "resize": {
|
|
@@ -12632,7 +13512,7 @@ var MediaManager = class {
|
|
|
12632
13512
|
switch (opts.action) {
|
|
12633
13513
|
case "trim": {
|
|
12634
13514
|
const input = validateInputPath(opts.input);
|
|
12635
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13515
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12636
13516
|
const a = ["-i", input];
|
|
12637
13517
|
if (opts.start) a.push("-ss", String(opts.start));
|
|
12638
13518
|
if (opts.end) a.push("-to", String(opts.end));
|
|
@@ -12653,8 +13533,8 @@ var MediaManager = class {
|
|
|
12653
13533
|
if (files.length < 2) throw new Error("At least 2 files are required for merge");
|
|
12654
13534
|
files.forEach((f, i) => validateInputPath(f, `files[${i}]`));
|
|
12655
13535
|
const listFile = this.outPath("concat", "txt");
|
|
12656
|
-
(0,
|
|
12657
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13536
|
+
(0, import_node_fs12.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
|
|
13537
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(files[0]).slice(1) || "mp3"));
|
|
12658
13538
|
try {
|
|
12659
13539
|
await this.ffmpeg(["-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", "-y", out]);
|
|
12660
13540
|
} finally {
|
|
@@ -12665,7 +13545,7 @@ var MediaManager = class {
|
|
|
12665
13545
|
case "volume": {
|
|
12666
13546
|
const input = validateInputPath(opts.input);
|
|
12667
13547
|
if (!opts.volume) throw new Error('volume is required (e.g. "1.5" or "10dB")');
|
|
12668
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13548
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12669
13549
|
await this.ffmpeg(["-i", input, "-af", `volume=${opts.volume}`, "-y", out]);
|
|
12670
13550
|
return this.fileResult(out);
|
|
12671
13551
|
}
|
|
@@ -12673,7 +13553,7 @@ var MediaManager = class {
|
|
|
12673
13553
|
const input = validateInputPath(opts.input);
|
|
12674
13554
|
const factor = clampNumber(opts.speedFactor, 0.5, 100, 0);
|
|
12675
13555
|
if (!factor) throw new Error("speedFactor is required for speed");
|
|
12676
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13556
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12677
13557
|
await this.ffmpeg(["-i", input, "-af", `atempo=${factor}`, "-y", out]);
|
|
12678
13558
|
return this.fileResult(out);
|
|
12679
13559
|
}
|
|
@@ -12685,7 +13565,7 @@ var MediaManager = class {
|
|
|
12685
13565
|
}
|
|
12686
13566
|
case "reverse": {
|
|
12687
13567
|
const input = validateInputPath(opts.input);
|
|
12688
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13568
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12689
13569
|
await this.ffmpeg(["-i", input, "-af", "areverse", "-y", out]);
|
|
12690
13570
|
return this.fileResult(out);
|
|
12691
13571
|
}
|
|
@@ -12694,7 +13574,7 @@ var MediaManager = class {
|
|
|
12694
13574
|
const dur = clampNumber(opts.fadeDuration, 0.1, 3600, 3);
|
|
12695
13575
|
const probe = await this.ffprobe(input);
|
|
12696
13576
|
const totalDur = parseFloat(probe.format?.duration || "0");
|
|
12697
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13577
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12698
13578
|
let af;
|
|
12699
13579
|
if (opts.fadeType === "in") af = `afade=t=in:st=0:d=${dur}`;
|
|
12700
13580
|
else if (opts.fadeType === "out") af = `afade=t=out:st=${Math.max(0, totalDur - dur)}:d=${dur}`;
|
|
@@ -12724,7 +13604,7 @@ var MediaManager = class {
|
|
|
12724
13604
|
}));
|
|
12725
13605
|
return {
|
|
12726
13606
|
ok: true,
|
|
12727
|
-
file: (0,
|
|
13607
|
+
file: (0, import_node_path13.basename)(path2),
|
|
12728
13608
|
format: info.format?.format_long_name,
|
|
12729
13609
|
duration: info.format?.duration,
|
|
12730
13610
|
sizeBytes: parseInt(info.format?.size || "0", 10),
|
|
@@ -12737,7 +13617,7 @@ var MediaManager = class {
|
|
|
12737
13617
|
async videoEdit(opts) {
|
|
12738
13618
|
if (opts.action === "concatenate") return this.videoConcatenate(opts);
|
|
12739
13619
|
const input = validateInputPath(opts.input);
|
|
12740
|
-
const srcExt = safeExtension(null, (0,
|
|
13620
|
+
const srcExt = safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4");
|
|
12741
13621
|
switch (opts.action) {
|
|
12742
13622
|
case "trim": {
|
|
12743
13623
|
const out = this.outPath("vid", srcExt);
|
|
@@ -12759,7 +13639,7 @@ var MediaManager = class {
|
|
|
12759
13639
|
const dir2 = this.outDir("frames");
|
|
12760
13640
|
const interval = clampNumber(opts.interval, 0.01, 3600, 1);
|
|
12761
13641
|
await this.ffmpeg(
|
|
12762
|
-
["-i", input, "-vf", `fps=1/${interval}`, (0,
|
|
13642
|
+
["-i", input, "-vf", `fps=1/${interval}`, (0, import_node_path13.join)(dir2, "frame-%04d.png"), "-y"],
|
|
12763
13643
|
TIMEOUT_LONG
|
|
12764
13644
|
);
|
|
12765
13645
|
return { ok: true, filePath: dir2, sizeBytes: 0, outputDir: dir2 };
|
|
@@ -12860,7 +13740,7 @@ var MediaManager = class {
|
|
|
12860
13740
|
}
|
|
12861
13741
|
}
|
|
12862
13742
|
async videoColorGrade(input, opts) {
|
|
12863
|
-
const out = this.outPath("vid", safeExtension(null, (0,
|
|
13743
|
+
const out = this.outPath("vid", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4"));
|
|
12864
13744
|
let vf;
|
|
12865
13745
|
if (opts.lutPath) {
|
|
12866
13746
|
const lut = validateInputPath(opts.lutPath, "lutPath");
|
|
@@ -12918,7 +13798,7 @@ var MediaManager = class {
|
|
|
12918
13798
|
}
|
|
12919
13799
|
async videoTextOverlay(input, opts) {
|
|
12920
13800
|
if (!opts.text) throw new Error("text is required for text_overlay");
|
|
12921
|
-
const out = this.outPath("vid", safeExtension(null, (0,
|
|
13801
|
+
const out = this.outPath("vid", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4"));
|
|
12922
13802
|
const probeV = await this.ffprobe(input);
|
|
12923
13803
|
const vStream = (probeV.streams || []).find((s) => s.codec_type === "video");
|
|
12924
13804
|
const vw = vStream?.width || 1920;
|
|
@@ -13187,7 +14067,7 @@ var MediaManager = class {
|
|
|
13187
14067
|
files.forEach((f, i) => validateInputPath(f, `files[${i}]`));
|
|
13188
14068
|
const out = this.outPath("vid", "mp4");
|
|
13189
14069
|
const listFile = this.outPath("concat", "txt");
|
|
13190
|
-
(0,
|
|
14070
|
+
(0, import_node_fs12.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
|
|
13191
14071
|
try {
|
|
13192
14072
|
try {
|
|
13193
14073
|
await this.ffmpeg(["-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", "-y", out], TIMEOUT_LONG);
|
|
@@ -13316,9 +14196,9 @@ var MediaManager = class {
|
|
|
13316
14196
|
const bg = bgColors[i % bgColors.length];
|
|
13317
14197
|
const sizeMult = chunk.wc <= 2 ? 1.4 : chunk.wc <= 3 ? 1.1 : 1;
|
|
13318
14198
|
const fontSize = Math.round(baseFont * sizeMult);
|
|
13319
|
-
const txtPng = (0,
|
|
13320
|
-
const bgPng = (0,
|
|
13321
|
-
const finalPng = (0,
|
|
14199
|
+
const txtPng = (0, import_node_path13.join)(captionDir, `txt-${i}.png`);
|
|
14200
|
+
const bgPng = (0, import_node_path13.join)(captionDir, `bg-${i}.png`);
|
|
14201
|
+
const finalPng = (0, import_node_path13.join)(captionDir, `c-${String(i).padStart(4, "0")}.png`);
|
|
13322
14202
|
await this.magick([
|
|
13323
14203
|
"-size",
|
|
13324
14204
|
`${maxTextW}x`,
|
|
@@ -13442,12 +14322,12 @@ var MediaManager = class {
|
|
|
13442
14322
|
for (let i = 0; i < frameCount; i++) {
|
|
13443
14323
|
const t = i * interval;
|
|
13444
14324
|
if (t >= totalDur && totalDur > 0) break;
|
|
13445
|
-
const framePath = (0,
|
|
14325
|
+
const framePath = (0, import_node_path13.join)(frameDir, `frame-${String(i).padStart(3, "0")}.jpg`);
|
|
13446
14326
|
await this.ffmpeg(["-ss", String(t), "-i", input, "-frames:v", "1", "-q:v", "3", "-y", framePath], TIMEOUT_FAST);
|
|
13447
14327
|
frames.push({ time: t, path: framePath });
|
|
13448
14328
|
}
|
|
13449
14329
|
const transcript = [];
|
|
13450
|
-
if (opts.whisperModel && (0,
|
|
14330
|
+
if (opts.whisperModel && (0, import_node_fs12.existsSync)(opts.whisperModel) && detectBinary("whisper").available) {
|
|
13451
14331
|
const whisper = requireBinary("whisper");
|
|
13452
14332
|
const wavPath = this.outPath("understand-audio", "wav");
|
|
13453
14333
|
await this.ffmpeg(["-i", input, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", "-y", wavPath], TIMEOUT_FAST);
|
|
@@ -13483,7 +14363,7 @@ var MediaManager = class {
|
|
|
13483
14363
|
});
|
|
13484
14364
|
return {
|
|
13485
14365
|
ok: true,
|
|
13486
|
-
video: (0,
|
|
14366
|
+
video: (0, import_node_path13.basename)(input),
|
|
13487
14367
|
duration: totalDur,
|
|
13488
14368
|
resolution: rotation ? `${vH}x${vW} (rotated ${rotation})` : `${vW}x${vH}`,
|
|
13489
14369
|
totalFramesExtracted: frames.length,
|
|
@@ -13511,11 +14391,11 @@ var MediaManager = class {
|
|
|
13511
14391
|
if (!opts.refText || typeof opts.refText !== "string") {
|
|
13512
14392
|
throw new Error("refText is required for voice_clone (the transcript of the reference audio)");
|
|
13513
14393
|
}
|
|
13514
|
-
const pythonBin = opts.pythonBin && (0,
|
|
14394
|
+
const pythonBin = opts.pythonBin && (0, import_node_path13.isAbsolute)(opts.pythonBin) ? validateInputPath(opts.pythonBin, "pythonBin") : requireBinary("python");
|
|
13515
14395
|
const device = typeof opts.device === "string" && /^[a-z0-9]+$/i.test(opts.device) ? opts.device : "cpu";
|
|
13516
14396
|
const outWav = this.outPath("voiceclone", "wav");
|
|
13517
14397
|
const paramsFile = this.outPath("voiceclone-params", "json");
|
|
13518
|
-
(0,
|
|
14398
|
+
(0, import_node_fs12.writeFileSync)(paramsFile, JSON.stringify({
|
|
13519
14399
|
ref_file: refAudio,
|
|
13520
14400
|
ref_text: opts.refText,
|
|
13521
14401
|
gen_text: opts.text,
|
|
@@ -13556,7 +14436,7 @@ var MediaManager = class {
|
|
|
13556
14436
|
outOgg,
|
|
13557
14437
|
"-y"
|
|
13558
14438
|
]);
|
|
13559
|
-
if ((0,
|
|
14439
|
+
if ((0, import_node_fs12.existsSync)(outOgg)) return this.fileResult(outOgg, { format: "ogg" });
|
|
13560
14440
|
} catch {
|
|
13561
14441
|
}
|
|
13562
14442
|
}
|
|
@@ -13566,18 +14446,18 @@ var MediaManager = class {
|
|
|
13566
14446
|
/** Parse a whisper-produced SRT (located by stem) into timed segments. */
|
|
13567
14447
|
parseSrt(srtStem) {
|
|
13568
14448
|
let srtFile = `${srtStem}.srt`;
|
|
13569
|
-
if (!(0,
|
|
13570
|
-
const dir2 = (0,
|
|
13571
|
-
const stem2 = (0,
|
|
14449
|
+
if (!(0, import_node_fs12.existsSync)(srtFile)) {
|
|
14450
|
+
const dir2 = (0, import_node_path13.dirname)(srtStem);
|
|
14451
|
+
const stem2 = (0, import_node_path13.basename)(srtStem);
|
|
13572
14452
|
try {
|
|
13573
|
-
const candidates = (0,
|
|
13574
|
-
if (candidates.length > 0) srtFile = (0,
|
|
14453
|
+
const candidates = (0, import_node_fs12.readdirSync)(dir2).filter((f) => f.includes(stem2) && f.endsWith(".srt"));
|
|
14454
|
+
if (candidates.length > 0) srtFile = (0, import_node_path13.join)(dir2, candidates[0]);
|
|
13575
14455
|
} catch {
|
|
13576
14456
|
}
|
|
13577
14457
|
}
|
|
13578
|
-
if (!(0,
|
|
14458
|
+
if (!(0, import_node_fs12.existsSync)(srtFile)) return [];
|
|
13579
14459
|
const out = [];
|
|
13580
|
-
const content = (0,
|
|
14460
|
+
const content = (0, import_node_fs12.readFileSync)(srtFile, "utf8");
|
|
13581
14461
|
for (const block of content.trim().split(/\n\n+/)) {
|
|
13582
14462
|
const lines = block.trim().split("\n");
|
|
13583
14463
|
if (lines.length < 3) continue;
|
|
@@ -13593,7 +14473,7 @@ var MediaManager = class {
|
|
|
13593
14473
|
/** Unlink a file, swallowing any error (cleanup best-effort). */
|
|
13594
14474
|
tryUnlink(path2) {
|
|
13595
14475
|
try {
|
|
13596
|
-
(0,
|
|
14476
|
+
(0, import_node_fs12.unlinkSync)(path2);
|
|
13597
14477
|
} catch {
|
|
13598
14478
|
}
|
|
13599
14479
|
}
|
|
@@ -13601,10 +14481,10 @@ var MediaManager = class {
|
|
|
13601
14481
|
tryUnlinkSrt(srtStem) {
|
|
13602
14482
|
this.tryUnlink(`${srtStem}.srt`);
|
|
13603
14483
|
try {
|
|
13604
|
-
const dir2 = (0,
|
|
13605
|
-
const stem2 = (0,
|
|
13606
|
-
for (const f of (0,
|
|
13607
|
-
if (f.includes(stem2) && f.endsWith(".srt")) this.tryUnlink((0,
|
|
14484
|
+
const dir2 = (0, import_node_path13.dirname)(srtStem);
|
|
14485
|
+
const stem2 = (0, import_node_path13.basename)(srtStem);
|
|
14486
|
+
for (const f of (0, import_node_fs12.readdirSync)(dir2)) {
|
|
14487
|
+
if (f.includes(stem2) && f.endsWith(".srt")) this.tryUnlink((0, import_node_path13.join)(dir2, f));
|
|
13608
14488
|
}
|
|
13609
14489
|
} catch {
|
|
13610
14490
|
}
|
|
@@ -13612,7 +14492,7 @@ var MediaManager = class {
|
|
|
13612
14492
|
/** Recursively remove a directory, swallowing errors. */
|
|
13613
14493
|
tryRmDir(dir2) {
|
|
13614
14494
|
try {
|
|
13615
|
-
(0,
|
|
14495
|
+
(0, import_node_fs12.rmSync)(dir2, { recursive: true, force: true });
|
|
13616
14496
|
} catch {
|
|
13617
14497
|
}
|
|
13618
14498
|
}
|
|
@@ -13652,10 +14532,10 @@ function threadIdFor(input) {
|
|
|
13652
14532
|
}
|
|
13653
14533
|
|
|
13654
14534
|
// src/threading/thread-cache.ts
|
|
13655
|
-
var
|
|
13656
|
-
var
|
|
13657
|
-
var
|
|
13658
|
-
var CACHE_DIR_DEFAULT = (0,
|
|
14535
|
+
var import_node_fs13 = require("fs");
|
|
14536
|
+
var import_node_os11 = require("os");
|
|
14537
|
+
var import_node_path14 = require("path");
|
|
14538
|
+
var CACHE_DIR_DEFAULT = (0, import_node_path14.join)((0, import_node_os11.homedir)(), ".agenticmail", "thread-cache");
|
|
13659
14539
|
var DEFAULT_K_MESSAGES = 10;
|
|
13660
14540
|
var DEFAULT_LRU_CAP = 5e3;
|
|
13661
14541
|
var PREVIEW_MAX_CHARS = 240;
|
|
@@ -13668,22 +14548,22 @@ var ThreadCache = class {
|
|
|
13668
14548
|
this.k = opts.k ?? DEFAULT_K_MESSAGES;
|
|
13669
14549
|
this.lruCap = opts.lruCap ?? DEFAULT_LRU_CAP;
|
|
13670
14550
|
try {
|
|
13671
|
-
(0,
|
|
14551
|
+
(0, import_node_fs13.mkdirSync)(this.dir, { recursive: true });
|
|
13672
14552
|
} catch {
|
|
13673
14553
|
}
|
|
13674
14554
|
}
|
|
13675
14555
|
pathFor(threadId) {
|
|
13676
|
-
return (0,
|
|
14556
|
+
return (0, import_node_path14.join)(this.dir, `${threadId}.json`);
|
|
13677
14557
|
}
|
|
13678
14558
|
read(threadId) {
|
|
13679
14559
|
const p = this.pathFor(threadId);
|
|
13680
|
-
if (!(0,
|
|
14560
|
+
if (!(0, import_node_fs13.existsSync)(p)) return null;
|
|
13681
14561
|
try {
|
|
13682
|
-
const raw = (0,
|
|
14562
|
+
const raw = (0, import_node_fs13.readFileSync)(p, "utf-8");
|
|
13683
14563
|
return JSON.parse(raw);
|
|
13684
14564
|
} catch {
|
|
13685
14565
|
try {
|
|
13686
|
-
(0,
|
|
14566
|
+
(0, import_node_fs13.rmSync)(p, { force: true });
|
|
13687
14567
|
} catch {
|
|
13688
14568
|
}
|
|
13689
14569
|
return null;
|
|
@@ -13724,7 +14604,7 @@ var ThreadCache = class {
|
|
|
13724
14604
|
/** Permanently remove a thread's cache (called on [FINAL] / [DONE] / [CLOSED] / [WRAP]). */
|
|
13725
14605
|
delete(threadId) {
|
|
13726
14606
|
try {
|
|
13727
|
-
(0,
|
|
14607
|
+
(0, import_node_fs13.rmSync)(this.pathFor(threadId), { force: true });
|
|
13728
14608
|
} catch {
|
|
13729
14609
|
}
|
|
13730
14610
|
}
|
|
@@ -13744,8 +14624,8 @@ var ThreadCache = class {
|
|
|
13744
14624
|
writeAtomic(threadId, entry) {
|
|
13745
14625
|
const p = this.pathFor(threadId);
|
|
13746
14626
|
const tmp = `${p}.tmp`;
|
|
13747
|
-
(0,
|
|
13748
|
-
(0,
|
|
14627
|
+
(0, import_node_fs13.writeFileSync)(tmp, JSON.stringify(entry), "utf-8");
|
|
14628
|
+
(0, import_node_fs13.renameSync)(tmp, p);
|
|
13749
14629
|
}
|
|
13750
14630
|
/**
|
|
13751
14631
|
* Best-effort LRU eviction. Runs at most every 256 writes (we
|
|
@@ -13757,15 +14637,15 @@ var ThreadCache = class {
|
|
|
13757
14637
|
if (Math.random() > 1 / 256) return;
|
|
13758
14638
|
let files;
|
|
13759
14639
|
try {
|
|
13760
|
-
files = (0,
|
|
14640
|
+
files = (0, import_node_fs13.readdirSync)(this.dir).filter((f) => f.endsWith(".json"));
|
|
13761
14641
|
} catch {
|
|
13762
14642
|
return;
|
|
13763
14643
|
}
|
|
13764
14644
|
if (files.length <= this.lruCap) return;
|
|
13765
14645
|
const stats = files.map((f) => {
|
|
13766
|
-
const p = (0,
|
|
14646
|
+
const p = (0, import_node_path14.join)(this.dir, f);
|
|
13767
14647
|
try {
|
|
13768
|
-
return { p, mtime: (0,
|
|
14648
|
+
return { p, mtime: (0, import_node_fs13.statSync)(p).mtimeMs };
|
|
13769
14649
|
} catch {
|
|
13770
14650
|
return { p, mtime: 0 };
|
|
13771
14651
|
}
|
|
@@ -13774,7 +14654,7 @@ var ThreadCache = class {
|
|
|
13774
14654
|
const dropCount = Math.max(1, Math.floor(this.lruCap * 0.1));
|
|
13775
14655
|
for (let i = 0; i < dropCount; i++) {
|
|
13776
14656
|
try {
|
|
13777
|
-
(0,
|
|
14657
|
+
(0, import_node_fs13.rmSync)(stats[i].p, { force: true });
|
|
13778
14658
|
} catch {
|
|
13779
14659
|
}
|
|
13780
14660
|
}
|
|
@@ -13793,30 +14673,30 @@ function dedupAndCap(messages, k) {
|
|
|
13793
14673
|
}
|
|
13794
14674
|
|
|
13795
14675
|
// src/threading/agent-memory.ts
|
|
13796
|
-
var
|
|
13797
|
-
var
|
|
13798
|
-
var
|
|
13799
|
-
var MEMORY_DIR_DEFAULT = (0,
|
|
14676
|
+
var import_node_fs14 = require("fs");
|
|
14677
|
+
var import_node_os12 = require("os");
|
|
14678
|
+
var import_node_path15 = require("path");
|
|
14679
|
+
var MEMORY_DIR_DEFAULT = (0, import_node_path15.join)((0, import_node_os12.homedir)(), ".agenticmail", "agent-memory");
|
|
13800
14680
|
var AgentMemoryStore = class {
|
|
13801
14681
|
dir;
|
|
13802
14682
|
constructor(opts = {}) {
|
|
13803
14683
|
this.dir = opts.memoryDir ?? MEMORY_DIR_DEFAULT;
|
|
13804
14684
|
try {
|
|
13805
|
-
(0,
|
|
14685
|
+
(0, import_node_fs14.mkdirSync)(this.dir, { recursive: true });
|
|
13806
14686
|
} catch {
|
|
13807
14687
|
}
|
|
13808
14688
|
}
|
|
13809
14689
|
dirFor(agentId) {
|
|
13810
|
-
return (0,
|
|
14690
|
+
return (0, import_node_path15.join)(this.dir, sanitizeId(agentId));
|
|
13811
14691
|
}
|
|
13812
14692
|
pathFor(agentId, threadId) {
|
|
13813
|
-
return (0,
|
|
14693
|
+
return (0, import_node_path15.join)(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
|
|
13814
14694
|
}
|
|
13815
14695
|
read(agentId, threadId) {
|
|
13816
14696
|
const p = this.pathFor(agentId, threadId);
|
|
13817
|
-
if (!(0,
|
|
14697
|
+
if (!(0, import_node_fs14.existsSync)(p)) return null;
|
|
13818
14698
|
try {
|
|
13819
|
-
const raw = (0,
|
|
14699
|
+
const raw = (0, import_node_fs14.readFileSync)(p, "utf-8");
|
|
13820
14700
|
const parsed = parse(raw);
|
|
13821
14701
|
return { ...parsed, raw };
|
|
13822
14702
|
} catch {
|
|
@@ -13826,18 +14706,18 @@ var AgentMemoryStore = class {
|
|
|
13826
14706
|
write(agentId, threadId, fields) {
|
|
13827
14707
|
const agentDir = this.dirFor(agentId);
|
|
13828
14708
|
try {
|
|
13829
|
-
(0,
|
|
14709
|
+
(0, import_node_fs14.mkdirSync)(agentDir, { recursive: true });
|
|
13830
14710
|
} catch {
|
|
13831
14711
|
}
|
|
13832
14712
|
const body = render({ ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13833
14713
|
const p = this.pathFor(agentId, threadId);
|
|
13834
14714
|
const tmp = `${p}.tmp`;
|
|
13835
|
-
(0,
|
|
13836
|
-
(0,
|
|
14715
|
+
(0, import_node_fs14.writeFileSync)(tmp, body, "utf-8");
|
|
14716
|
+
(0, import_node_fs14.renameSync)(tmp, p);
|
|
13837
14717
|
}
|
|
13838
14718
|
delete(agentId, threadId) {
|
|
13839
14719
|
try {
|
|
13840
|
-
(0,
|
|
14720
|
+
(0, import_node_fs14.rmSync)(this.pathFor(agentId, threadId), { force: true });
|
|
13841
14721
|
} catch {
|
|
13842
14722
|
}
|
|
13843
14723
|
}
|
|
@@ -13880,415 +14760,22 @@ function parse(raw) {
|
|
|
13880
14760
|
const out = {};
|
|
13881
14761
|
const m = raw.match(/^---\n([\s\S]*?)\n---\n/);
|
|
13882
14762
|
if (m) {
|
|
13883
|
-
for (const line of m[1].split("\n")) {
|
|
13884
|
-
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
13885
|
-
if (!kv) continue;
|
|
13886
|
-
if (kv[1] === "updated_at") out.updatedAt = kv[2].trim();
|
|
13887
|
-
else if (kv[1] === "last_uid") {
|
|
13888
|
-
const n = parseInt(kv[2], 10);
|
|
13889
|
-
if (!isNaN(n)) out.lastUid = n;
|
|
13890
|
-
}
|
|
13891
|
-
}
|
|
13892
|
-
}
|
|
13893
|
-
return out;
|
|
13894
|
-
}
|
|
13895
|
-
|
|
13896
|
-
// src/memory/manager.ts
|
|
13897
|
-
var import_node_crypto8 = require("crypto");
|
|
13898
|
-
|
|
13899
|
-
// src/memory/text-search.ts
|
|
13900
|
-
var BM25_K1 = 1.2;
|
|
13901
|
-
var BM25_B = 0.75;
|
|
13902
|
-
var FIELD_WEIGHT_TITLE = 3;
|
|
13903
|
-
var FIELD_WEIGHT_TAGS = 2;
|
|
13904
|
-
var FIELD_WEIGHT_CONTENT = 1;
|
|
13905
|
-
var PREFIX_MATCH_PENALTY = 0.7;
|
|
13906
|
-
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
13907
|
-
"a",
|
|
13908
|
-
"about",
|
|
13909
|
-
"above",
|
|
13910
|
-
"after",
|
|
13911
|
-
"again",
|
|
13912
|
-
"against",
|
|
13913
|
-
"all",
|
|
13914
|
-
"am",
|
|
13915
|
-
"an",
|
|
13916
|
-
"and",
|
|
13917
|
-
"any",
|
|
13918
|
-
"are",
|
|
13919
|
-
"as",
|
|
13920
|
-
"at",
|
|
13921
|
-
"be",
|
|
13922
|
-
"because",
|
|
13923
|
-
"been",
|
|
13924
|
-
"before",
|
|
13925
|
-
"being",
|
|
13926
|
-
"below",
|
|
13927
|
-
"between",
|
|
13928
|
-
"both",
|
|
13929
|
-
"but",
|
|
13930
|
-
"by",
|
|
13931
|
-
"can",
|
|
13932
|
-
"could",
|
|
13933
|
-
"did",
|
|
13934
|
-
"do",
|
|
13935
|
-
"does",
|
|
13936
|
-
"doing",
|
|
13937
|
-
"down",
|
|
13938
|
-
"during",
|
|
13939
|
-
"each",
|
|
13940
|
-
"either",
|
|
13941
|
-
"every",
|
|
13942
|
-
"few",
|
|
13943
|
-
"for",
|
|
13944
|
-
"from",
|
|
13945
|
-
"further",
|
|
13946
|
-
"get",
|
|
13947
|
-
"got",
|
|
13948
|
-
"had",
|
|
13949
|
-
"has",
|
|
13950
|
-
"have",
|
|
13951
|
-
"having",
|
|
13952
|
-
"he",
|
|
13953
|
-
"her",
|
|
13954
|
-
"here",
|
|
13955
|
-
"hers",
|
|
13956
|
-
"herself",
|
|
13957
|
-
"him",
|
|
13958
|
-
"himself",
|
|
13959
|
-
"his",
|
|
13960
|
-
"how",
|
|
13961
|
-
"i",
|
|
13962
|
-
"if",
|
|
13963
|
-
"in",
|
|
13964
|
-
"into",
|
|
13965
|
-
"is",
|
|
13966
|
-
"it",
|
|
13967
|
-
"its",
|
|
13968
|
-
"itself",
|
|
13969
|
-
"just",
|
|
13970
|
-
"may",
|
|
13971
|
-
"me",
|
|
13972
|
-
"might",
|
|
13973
|
-
"more",
|
|
13974
|
-
"most",
|
|
13975
|
-
"must",
|
|
13976
|
-
"my",
|
|
13977
|
-
"myself",
|
|
13978
|
-
"neither",
|
|
13979
|
-
"no",
|
|
13980
|
-
"nor",
|
|
13981
|
-
"not",
|
|
13982
|
-
"now",
|
|
13983
|
-
"of",
|
|
13984
|
-
"off",
|
|
13985
|
-
"on",
|
|
13986
|
-
"once",
|
|
13987
|
-
"only",
|
|
13988
|
-
"or",
|
|
13989
|
-
"other",
|
|
13990
|
-
"ought",
|
|
13991
|
-
"our",
|
|
13992
|
-
"ours",
|
|
13993
|
-
"ourselves",
|
|
13994
|
-
"out",
|
|
13995
|
-
"over",
|
|
13996
|
-
"own",
|
|
13997
|
-
"same",
|
|
13998
|
-
"shall",
|
|
13999
|
-
"she",
|
|
14000
|
-
"should",
|
|
14001
|
-
"so",
|
|
14002
|
-
"some",
|
|
14003
|
-
"such",
|
|
14004
|
-
"than",
|
|
14005
|
-
"that",
|
|
14006
|
-
"the",
|
|
14007
|
-
"their",
|
|
14008
|
-
"theirs",
|
|
14009
|
-
"them",
|
|
14010
|
-
"themselves",
|
|
14011
|
-
"then",
|
|
14012
|
-
"there",
|
|
14013
|
-
"these",
|
|
14014
|
-
"they",
|
|
14015
|
-
"this",
|
|
14016
|
-
"those",
|
|
14017
|
-
"through",
|
|
14018
|
-
"to",
|
|
14019
|
-
"too",
|
|
14020
|
-
"under",
|
|
14021
|
-
"until",
|
|
14022
|
-
"up",
|
|
14023
|
-
"us",
|
|
14024
|
-
"very",
|
|
14025
|
-
"was",
|
|
14026
|
-
"we",
|
|
14027
|
-
"were",
|
|
14028
|
-
"what",
|
|
14029
|
-
"when",
|
|
14030
|
-
"where",
|
|
14031
|
-
"which",
|
|
14032
|
-
"while",
|
|
14033
|
-
"who",
|
|
14034
|
-
"whom",
|
|
14035
|
-
"why",
|
|
14036
|
-
"will",
|
|
14037
|
-
"with",
|
|
14038
|
-
"would",
|
|
14039
|
-
"yet",
|
|
14040
|
-
"you",
|
|
14041
|
-
"your",
|
|
14042
|
-
"yours",
|
|
14043
|
-
"yourself",
|
|
14044
|
-
"yourselves"
|
|
14045
|
-
]);
|
|
14046
|
-
var STEM_RULES = [
|
|
14047
|
-
// Step 1: plurals and past participles
|
|
14048
|
-
[/ies$/, "i", 3],
|
|
14049
|
-
// policies → polici,eries → eri
|
|
14050
|
-
[/sses$/, "ss", 4],
|
|
14051
|
-
// addresses → address
|
|
14052
|
-
[/([^s])s$/, "$1", 3],
|
|
14053
|
-
// items → item, but not "ss"
|
|
14054
|
-
[/eed$/, "ee", 4],
|
|
14055
|
-
// agreed → agree
|
|
14056
|
-
[/ed$/, "", 3],
|
|
14057
|
-
// configured → configur, but min length 3
|
|
14058
|
-
[/ing$/, "", 4],
|
|
14059
|
-
// running → runn → run (handled below)
|
|
14060
|
-
// Step 2: derivational suffixes
|
|
14061
|
-
[/ational$/, "ate", 6],
|
|
14062
|
-
// relational → relate
|
|
14063
|
-
[/tion$/, "t", 5],
|
|
14064
|
-
// adoption → adopt
|
|
14065
|
-
[/ness$/, "", 5],
|
|
14066
|
-
// awareness → aware
|
|
14067
|
-
[/ment$/, "", 5],
|
|
14068
|
-
// deployment → deploy
|
|
14069
|
-
[/able$/, "", 5],
|
|
14070
|
-
// configurable → configur
|
|
14071
|
-
[/ible$/, "", 5],
|
|
14072
|
-
// accessible → access
|
|
14073
|
-
[/ful$/, "", 5],
|
|
14074
|
-
// powerful → power
|
|
14075
|
-
[/ous$/, "", 5],
|
|
14076
|
-
// dangerous → danger
|
|
14077
|
-
[/ive$/, "", 5],
|
|
14078
|
-
// interactive → interact
|
|
14079
|
-
[/ize$/, "", 4],
|
|
14080
|
-
// normalize → normal
|
|
14081
|
-
[/ise$/, "", 4],
|
|
14082
|
-
// organise → organ
|
|
14083
|
-
[/ally$/, "", 5],
|
|
14084
|
-
// automatically → automat
|
|
14085
|
-
[/ly$/, "", 4],
|
|
14086
|
-
// quickly → quick
|
|
14087
|
-
[/er$/, "", 4]
|
|
14088
|
-
// handler → handl
|
|
14089
|
-
];
|
|
14090
|
-
var DOUBLE_CONSONANT = /([^aeiou])\1$/;
|
|
14091
|
-
function stem(word) {
|
|
14092
|
-
if (word.length < 3) return word;
|
|
14093
|
-
let stemmed = word;
|
|
14094
|
-
for (const [pattern, replacement, minLen] of STEM_RULES) {
|
|
14095
|
-
if (stemmed.length >= minLen && pattern.test(stemmed)) {
|
|
14096
|
-
stemmed = stemmed.replace(pattern, replacement);
|
|
14097
|
-
break;
|
|
14098
|
-
}
|
|
14099
|
-
}
|
|
14100
|
-
if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
|
|
14101
|
-
stemmed = stemmed.slice(0, -1);
|
|
14102
|
-
}
|
|
14103
|
-
return stemmed;
|
|
14104
|
-
}
|
|
14105
|
-
function tokenize(text) {
|
|
14106
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
|
|
14107
|
-
}
|
|
14108
|
-
var MemorySearchIndex = class {
|
|
14109
|
-
/** Posting lists: stemmed term → Set of memory IDs containing it */
|
|
14110
|
-
postings = /* @__PURE__ */ new Map();
|
|
14111
|
-
/** Per-document metadata for BM25 scoring */
|
|
14112
|
-
docs = /* @__PURE__ */ new Map();
|
|
14113
|
-
/** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
|
|
14114
|
-
idf = /* @__PURE__ */ new Map();
|
|
14115
|
-
idfStale = true;
|
|
14116
|
-
/** 3-character prefix map for prefix matching: prefix → Set of full stems */
|
|
14117
|
-
prefixMap = /* @__PURE__ */ new Map();
|
|
14118
|
-
/** Total weighted document length (for computing average) */
|
|
14119
|
-
totalWeightedLen = 0;
|
|
14120
|
-
get docCount() {
|
|
14121
|
-
return this.docs.size;
|
|
14122
|
-
}
|
|
14123
|
-
get avgDocLen() {
|
|
14124
|
-
return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
|
|
14125
|
-
}
|
|
14126
|
-
/**
|
|
14127
|
-
* Index a memory entry. Extracts stems from title, content, and tags
|
|
14128
|
-
* with field-specific weighting and builds posting lists.
|
|
14129
|
-
*/
|
|
14130
|
-
addDocument(id, entry) {
|
|
14131
|
-
if (this.docs.has(id)) this.removeDocument(id);
|
|
14132
|
-
const titleTokens = tokenize(entry.title);
|
|
14133
|
-
const contentTokens = tokenize(entry.content);
|
|
14134
|
-
const tagTokens = entry.tags.flatMap((t) => tokenize(t));
|
|
14135
|
-
const weightedTf = /* @__PURE__ */ new Map();
|
|
14136
|
-
for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
|
|
14137
|
-
for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
|
|
14138
|
-
for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
|
|
14139
|
-
const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
|
|
14140
|
-
const allStems = /* @__PURE__ */ new Set();
|
|
14141
|
-
for (const t of weightedTf.keys()) allStems.add(t);
|
|
14142
|
-
const stemSequence = [...titleTokens, ...contentTokens];
|
|
14143
|
-
const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
|
|
14144
|
-
this.docs.set(id, docRecord);
|
|
14145
|
-
this.totalWeightedLen += weightedLen;
|
|
14146
|
-
for (const term of allStems) {
|
|
14147
|
-
let posting = this.postings.get(term);
|
|
14148
|
-
if (!posting) {
|
|
14149
|
-
posting = /* @__PURE__ */ new Set();
|
|
14150
|
-
this.postings.set(term, posting);
|
|
14151
|
-
}
|
|
14152
|
-
posting.add(id);
|
|
14153
|
-
if (term.length >= 3) {
|
|
14154
|
-
const prefix = term.slice(0, 3);
|
|
14155
|
-
let prefixSet = this.prefixMap.get(prefix);
|
|
14156
|
-
if (!prefixSet) {
|
|
14157
|
-
prefixSet = /* @__PURE__ */ new Set();
|
|
14158
|
-
this.prefixMap.set(prefix, prefixSet);
|
|
14159
|
-
}
|
|
14160
|
-
prefixSet.add(term);
|
|
14161
|
-
}
|
|
14162
|
-
}
|
|
14163
|
-
this.idfStale = true;
|
|
14164
|
-
}
|
|
14165
|
-
/** Remove a document from the index. */
|
|
14166
|
-
removeDocument(id) {
|
|
14167
|
-
const doc = this.docs.get(id);
|
|
14168
|
-
if (!doc) return;
|
|
14169
|
-
this.totalWeightedLen -= doc.weightedLen;
|
|
14170
|
-
this.docs.delete(id);
|
|
14171
|
-
for (const term of doc.allStems) {
|
|
14172
|
-
const posting = this.postings.get(term);
|
|
14173
|
-
if (posting) {
|
|
14174
|
-
posting.delete(id);
|
|
14175
|
-
if (posting.size === 0) {
|
|
14176
|
-
this.postings.delete(term);
|
|
14177
|
-
if (term.length >= 3) {
|
|
14178
|
-
const prefixSet = this.prefixMap.get(term.slice(0, 3));
|
|
14179
|
-
if (prefixSet) {
|
|
14180
|
-
prefixSet.delete(term);
|
|
14181
|
-
if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
|
|
14182
|
-
}
|
|
14183
|
-
}
|
|
14184
|
-
}
|
|
14185
|
-
}
|
|
14186
|
-
}
|
|
14187
|
-
this.idfStale = true;
|
|
14188
|
-
}
|
|
14189
|
-
/** Recompute IDF values for all terms. Called lazily before search. */
|
|
14190
|
-
refreshIdf() {
|
|
14191
|
-
if (!this.idfStale) return;
|
|
14192
|
-
const N = this.docs.size;
|
|
14193
|
-
this.idf.clear();
|
|
14194
|
-
for (const [term, posting] of this.postings) {
|
|
14195
|
-
const df = posting.size;
|
|
14196
|
-
this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
|
|
14197
|
-
}
|
|
14198
|
-
this.idfStale = false;
|
|
14199
|
-
}
|
|
14200
|
-
/**
|
|
14201
|
-
* Expand query terms with prefix matches.
|
|
14202
|
-
* "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
|
|
14203
|
-
*/
|
|
14204
|
-
expandQueryTerms(queryStems) {
|
|
14205
|
-
const expanded = /* @__PURE__ */ new Map();
|
|
14206
|
-
for (const qs of queryStems) {
|
|
14207
|
-
if (this.postings.has(qs)) {
|
|
14208
|
-
expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
|
|
14209
|
-
}
|
|
14210
|
-
if (qs.length >= 3) {
|
|
14211
|
-
const prefix = qs.slice(0, 3);
|
|
14212
|
-
const candidates = this.prefixMap.get(prefix);
|
|
14213
|
-
if (candidates) {
|
|
14214
|
-
for (const candidate of candidates) {
|
|
14215
|
-
if (candidate !== qs && candidate.startsWith(qs)) {
|
|
14216
|
-
expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
|
|
14217
|
-
}
|
|
14218
|
-
}
|
|
14219
|
-
}
|
|
14220
|
-
}
|
|
14221
|
-
}
|
|
14222
|
-
return expanded;
|
|
14223
|
-
}
|
|
14224
|
-
/**
|
|
14225
|
-
* Compute bigram proximity boost: if two query terms appear adjacent
|
|
14226
|
-
* in the document's stem sequence, boost the score.
|
|
14227
|
-
*/
|
|
14228
|
-
bigramProximityBoost(docId, queryStems) {
|
|
14229
|
-
if (queryStems.length < 2) return 0;
|
|
14230
|
-
const doc = this.docs.get(docId);
|
|
14231
|
-
if (!doc || doc.stemSequence.length < 2) return 0;
|
|
14232
|
-
let boost = 0;
|
|
14233
|
-
const seq = doc.stemSequence;
|
|
14234
|
-
const querySet = new Set(queryStems);
|
|
14235
|
-
for (let i = 0; i < seq.length - 1; i++) {
|
|
14236
|
-
if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
|
|
14237
|
-
boost += 0.5;
|
|
14238
|
-
}
|
|
14239
|
-
}
|
|
14240
|
-
return Math.min(boost, 2);
|
|
14241
|
-
}
|
|
14242
|
-
/**
|
|
14243
|
-
* Search the index for documents matching a query.
|
|
14244
|
-
* Returns scored results sorted by BM25F relevance.
|
|
14245
|
-
*
|
|
14246
|
-
* @param query - Raw query string
|
|
14247
|
-
* @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
|
|
14248
|
-
* @returns Array of { id, score } sorted by descending score
|
|
14249
|
-
*/
|
|
14250
|
-
search(query, candidateIds) {
|
|
14251
|
-
const queryStems = tokenize(query);
|
|
14252
|
-
if (queryStems.length === 0) return [];
|
|
14253
|
-
this.refreshIdf();
|
|
14254
|
-
const expandedTerms = this.expandQueryTerms(queryStems);
|
|
14255
|
-
if (expandedTerms.size === 0) return [];
|
|
14256
|
-
const avgDl = this.avgDocLen;
|
|
14257
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
14258
|
-
for (const term of expandedTerms.keys()) {
|
|
14259
|
-
const posting = this.postings.get(term);
|
|
14260
|
-
if (posting) {
|
|
14261
|
-
for (const docId of posting) {
|
|
14262
|
-
if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
|
|
14263
|
-
}
|
|
14763
|
+
for (const line of m[1].split("\n")) {
|
|
14764
|
+
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
14765
|
+
if (!kv) continue;
|
|
14766
|
+
if (kv[1] === "updated_at") out.updatedAt = kv[2].trim();
|
|
14767
|
+
else if (kv[1] === "last_uid") {
|
|
14768
|
+
const n = parseInt(kv[2], 10);
|
|
14769
|
+
if (!isNaN(n)) out.lastUid = n;
|
|
14264
14770
|
}
|
|
14265
14771
|
}
|
|
14266
|
-
const results = [];
|
|
14267
|
-
for (const docId of candidates) {
|
|
14268
|
-
const doc = this.docs.get(docId);
|
|
14269
|
-
if (!doc) continue;
|
|
14270
|
-
let score = 0;
|
|
14271
|
-
for (const [term, weight] of expandedTerms) {
|
|
14272
|
-
const tf = doc.weightedTf.get(term) || 0;
|
|
14273
|
-
if (tf === 0) continue;
|
|
14274
|
-
const termIdf = this.idf.get(term) || 0;
|
|
14275
|
-
const numerator = tf * (BM25_K1 + 1);
|
|
14276
|
-
const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
|
|
14277
|
-
score += termIdf * (numerator / denominator) * weight;
|
|
14278
|
-
}
|
|
14279
|
-
score += this.bigramProximityBoost(docId, queryStems);
|
|
14280
|
-
if (score > 0) results.push({ id: docId, score });
|
|
14281
|
-
}
|
|
14282
|
-
results.sort((a, b) => b.score - a.score);
|
|
14283
|
-
return results;
|
|
14284
|
-
}
|
|
14285
|
-
/** Check if a document exists in the index. */
|
|
14286
|
-
has(id) {
|
|
14287
|
-
return this.docs.has(id);
|
|
14288
14772
|
}
|
|
14289
|
-
|
|
14773
|
+
return out;
|
|
14774
|
+
}
|
|
14290
14775
|
|
|
14291
14776
|
// src/memory/manager.ts
|
|
14777
|
+
var import_node_crypto8 = require("crypto");
|
|
14778
|
+
init_text_search();
|
|
14292
14779
|
function sj(v, fb = {}) {
|
|
14293
14780
|
if (!v) return fb;
|
|
14294
14781
|
try {
|
|
@@ -14772,284 +15259,11 @@ var AgentMemoryManager = class {
|
|
|
14772
15259
|
}
|
|
14773
15260
|
};
|
|
14774
15261
|
|
|
14775
|
-
// src/
|
|
14776
|
-
|
|
14777
|
-
var import_node_path15 = require("path");
|
|
14778
|
-
var import_node_url = require("url");
|
|
14779
|
-
var import_node_os12 = require("os");
|
|
14780
|
-
var import_meta4 = {};
|
|
14781
|
-
function builtInDir() {
|
|
14782
|
-
const here = (0, import_node_path15.dirname)((0, import_node_url.fileURLToPath)(import_meta4.url));
|
|
14783
|
-
return (0, import_node_path15.join)(here, "built-in");
|
|
14784
|
-
}
|
|
14785
|
-
function userDir() {
|
|
14786
|
-
const dir2 = (0, import_node_path15.join)((0, import_node_os12.homedir)(), ".agenticmail", "skills");
|
|
14787
|
-
try {
|
|
14788
|
-
if (!(0, import_node_fs14.existsSync)(dir2)) (0, import_node_fs14.mkdirSync)(dir2, { recursive: true });
|
|
14789
|
-
} catch {
|
|
14790
|
-
}
|
|
14791
|
-
return dir2;
|
|
14792
|
-
}
|
|
14793
|
-
var cache = { ts: 0, byId: null };
|
|
14794
|
-
var CACHE_TTL_MS = 5e3;
|
|
14795
|
-
function loadAllSkillsFromDisk() {
|
|
14796
|
-
const all = /* @__PURE__ */ new Map();
|
|
14797
|
-
const dirs = [builtInDir(), userDir()];
|
|
14798
|
-
for (const dir2 of dirs) {
|
|
14799
|
-
if (!(0, import_node_fs14.existsSync)(dir2)) continue;
|
|
14800
|
-
let entries;
|
|
14801
|
-
try {
|
|
14802
|
-
entries = (0, import_node_fs14.readdirSync)(dir2);
|
|
14803
|
-
} catch {
|
|
14804
|
-
continue;
|
|
14805
|
-
}
|
|
14806
|
-
for (const entry of entries) {
|
|
14807
|
-
if (!entry.endsWith(".json")) continue;
|
|
14808
|
-
const fullPath = (0, import_node_path15.join)(dir2, entry);
|
|
14809
|
-
try {
|
|
14810
|
-
const st = (0, import_node_fs14.statSync)(fullPath);
|
|
14811
|
-
if (!st.isFile()) continue;
|
|
14812
|
-
const raw = (0, import_node_fs14.readFileSync)(fullPath, "utf-8");
|
|
14813
|
-
const parsed = JSON.parse(raw);
|
|
14814
|
-
const errors = validateSkill(parsed);
|
|
14815
|
-
if (errors.length > 0) {
|
|
14816
|
-
console.warn(`[skills] ${entry} invalid, skipping: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
14817
|
-
continue;
|
|
14818
|
-
}
|
|
14819
|
-
all.set(parsed.id, parsed);
|
|
14820
|
-
} catch (err) {
|
|
14821
|
-
console.warn(`[skills] could not load ${fullPath}: ${err.message}`);
|
|
14822
|
-
}
|
|
14823
|
-
}
|
|
14824
|
-
}
|
|
14825
|
-
return all;
|
|
14826
|
-
}
|
|
14827
|
-
function ensureLoaded() {
|
|
14828
|
-
const now = Date.now();
|
|
14829
|
-
if (cache.byId && now - cache.ts < CACHE_TTL_MS) return cache.byId;
|
|
14830
|
-
const fresh = loadAllSkillsFromDisk();
|
|
14831
|
-
cache.byId = fresh;
|
|
14832
|
-
cache.ts = now;
|
|
14833
|
-
return fresh;
|
|
14834
|
-
}
|
|
14835
|
-
function invalidateSkillCache() {
|
|
14836
|
-
cache.byId = null;
|
|
14837
|
-
cache.ts = 0;
|
|
14838
|
-
}
|
|
14839
|
-
function validateSkill(s) {
|
|
14840
|
-
const errs = [];
|
|
14841
|
-
if (!s || typeof s !== "object" || Array.isArray(s)) {
|
|
14842
|
-
return [{ path: "$", message: "skill must be a JSON object" }];
|
|
14843
|
-
}
|
|
14844
|
-
const sk = s;
|
|
14845
|
-
const requireString = (key, minLen = 1) => {
|
|
14846
|
-
const v = sk[key];
|
|
14847
|
-
if (typeof v !== "string" || v.length < minLen) {
|
|
14848
|
-
errs.push({ path: key, message: `must be a non-empty string` });
|
|
14849
|
-
}
|
|
14850
|
-
};
|
|
14851
|
-
const requireArray = (key, minLen = 1) => {
|
|
14852
|
-
const v = sk[key];
|
|
14853
|
-
if (!Array.isArray(v) || v.length < minLen) {
|
|
14854
|
-
errs.push({ path: key, message: `must be a non-empty array` });
|
|
14855
|
-
}
|
|
14856
|
-
};
|
|
14857
|
-
requireString("id");
|
|
14858
|
-
if (typeof sk.id === "string" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(sk.id)) {
|
|
14859
|
-
errs.push({ path: "id", message: "must be lowercase-hyphenated slug (a-z, 0-9, -)" });
|
|
14860
|
-
}
|
|
14861
|
-
requireString("name");
|
|
14862
|
-
requireString("version");
|
|
14863
|
-
requireString("description");
|
|
14864
|
-
requireString("category");
|
|
14865
|
-
const validCategories = [
|
|
14866
|
-
"negotiation",
|
|
14867
|
-
"customer-service",
|
|
14868
|
-
"reservations",
|
|
14869
|
-
"medical-admin",
|
|
14870
|
-
"legal-admin",
|
|
14871
|
-
"finance-admin",
|
|
14872
|
-
"real-estate",
|
|
14873
|
-
"travel",
|
|
14874
|
-
"subscription",
|
|
14875
|
-
"home-services",
|
|
14876
|
-
"social",
|
|
14877
|
-
"civic",
|
|
14878
|
-
"employment",
|
|
14879
|
-
"debt-collection",
|
|
14880
|
-
"other"
|
|
14881
|
-
];
|
|
14882
|
-
if (typeof sk.category === "string" && !validCategories.includes(sk.category)) {
|
|
14883
|
-
errs.push({ path: "category", message: `unknown category "${sk.category}"; must be one of: ${validCategories.join(", ")}` });
|
|
14884
|
-
}
|
|
14885
|
-
if (!Array.isArray(sk.tags)) errs.push({ path: "tags", message: "must be an array of strings" });
|
|
14886
|
-
if (sk.disclaimer !== null && typeof sk.disclaimer !== "string") {
|
|
14887
|
-
errs.push({ path: "disclaimer", message: "must be a string or null" });
|
|
14888
|
-
}
|
|
14889
|
-
if (!sk.context || typeof sk.context !== "object") {
|
|
14890
|
-
errs.push({ path: "context", message: "must be an object" });
|
|
14891
|
-
} else {
|
|
14892
|
-
const ctx = sk.context;
|
|
14893
|
-
if (typeof ctx.when_to_use !== "string") errs.push({ path: "context.when_to_use", message: "must be a string" });
|
|
14894
|
-
if (!Array.isArray(ctx.preconditions)) errs.push({ path: "context.preconditions", message: "must be an array" });
|
|
14895
|
-
if (typeof ctx.estimated_call_duration_minutes !== "number") errs.push({ path: "context.estimated_call_duration_minutes", message: "must be a number" });
|
|
14896
|
-
}
|
|
14897
|
-
requireArray("principles", 2);
|
|
14898
|
-
if (!sk.phrases || typeof sk.phrases !== "object") errs.push({ path: "phrases", message: "must be an object of {key: phrase}" });
|
|
14899
|
-
if (!Array.isArray(sk.tactics) || sk.tactics.length === 0) {
|
|
14900
|
-
errs.push({ path: "tactics", message: "must be a non-empty array" });
|
|
14901
|
-
} else {
|
|
14902
|
-
sk.tactics.forEach((t, i) => {
|
|
14903
|
-
if (!t || typeof t !== "object") {
|
|
14904
|
-
errs.push({ path: `tactics[${i}]`, message: "must be an object" });
|
|
14905
|
-
return;
|
|
14906
|
-
}
|
|
14907
|
-
const tactic = t;
|
|
14908
|
-
if (typeof tactic.name !== "string") errs.push({ path: `tactics[${i}].name`, message: "must be a string" });
|
|
14909
|
-
if (typeof tactic.when !== "string") errs.push({ path: `tactics[${i}].when`, message: "must be a string" });
|
|
14910
|
-
if (typeof tactic.script !== "string" || tactic.script.length < 5) {
|
|
14911
|
-
errs.push({ path: `tactics[${i}].script`, message: "must be a non-empty string (>= 5 chars)" });
|
|
14912
|
-
}
|
|
14913
|
-
});
|
|
14914
|
-
}
|
|
14915
|
-
requireArray("boundaries", 1);
|
|
14916
|
-
if (!Array.isArray(sk.success_signals)) errs.push({ path: "success_signals", message: "must be an array" });
|
|
14917
|
-
if (!Array.isArray(sk.failure_signals)) errs.push({ path: "failure_signals", message: "must be an array" });
|
|
14918
|
-
if (!sk.exit_strategy || typeof sk.exit_strategy !== "object") {
|
|
14919
|
-
errs.push({ path: "exit_strategy", message: "must be an object" });
|
|
14920
|
-
} else {
|
|
14921
|
-
const xs = sk.exit_strategy;
|
|
14922
|
-
if (typeof xs.on_success !== "string") errs.push({ path: "exit_strategy.on_success", message: "must be a string" });
|
|
14923
|
-
if (typeof xs.on_failure !== "string") errs.push({ path: "exit_strategy.on_failure", message: "must be a string" });
|
|
14924
|
-
}
|
|
14925
|
-
if (!Array.isArray(sk.required_user_info)) errs.push({ path: "required_user_info", message: "must be an array" });
|
|
14926
|
-
if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
|
|
14927
|
-
return errs;
|
|
14928
|
-
}
|
|
14929
|
-
function summarize(s) {
|
|
14930
|
-
return {
|
|
14931
|
-
id: s.id,
|
|
14932
|
-
name: s.name,
|
|
14933
|
-
category: s.category,
|
|
14934
|
-
tags: s.tags,
|
|
14935
|
-
description: s.description,
|
|
14936
|
-
version: s.version,
|
|
14937
|
-
disclaimer_required: !!s.disclaimer,
|
|
14938
|
-
estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
|
|
14939
|
-
};
|
|
14940
|
-
}
|
|
14941
|
-
function listSkills(opts = {}) {
|
|
14942
|
-
const all = Array.from(ensureLoaded().values());
|
|
14943
|
-
const filtered = all.filter((s) => {
|
|
14944
|
-
if (opts.category && s.category !== opts.category) return false;
|
|
14945
|
-
if (opts.tag && !s.tags.includes(opts.tag.toLowerCase())) return false;
|
|
14946
|
-
return true;
|
|
14947
|
-
});
|
|
14948
|
-
return filtered.sort((a, b) => a.name.localeCompare(b.name)).map(summarize);
|
|
14949
|
-
}
|
|
14950
|
-
function searchSkills(query, limit = 20) {
|
|
14951
|
-
const q = query.toLowerCase().trim();
|
|
14952
|
-
if (!q) return listSkills();
|
|
14953
|
-
const all = Array.from(ensureLoaded().values());
|
|
14954
|
-
const scored = [];
|
|
14955
|
-
for (const s of all) {
|
|
14956
|
-
let score = 0;
|
|
14957
|
-
if (s.id.toLowerCase().includes(q)) score += 10;
|
|
14958
|
-
if (s.name.toLowerCase().includes(q)) score += 8;
|
|
14959
|
-
if (s.tags.some((t) => t.toLowerCase().includes(q))) score += 5;
|
|
14960
|
-
if (s.category.toLowerCase().includes(q)) score += 4;
|
|
14961
|
-
if (s.description.toLowerCase().includes(q)) score += 3;
|
|
14962
|
-
if (s.principles.some((p) => p.toLowerCase().includes(q))) score += 2;
|
|
14963
|
-
if (Object.values(s.phrases).some((p) => p.toLowerCase().includes(q))) score += 2;
|
|
14964
|
-
if (s.tactics.some((t) => t.script.toLowerCase().includes(q) || t.name.toLowerCase().includes(q))) score += 2;
|
|
14965
|
-
if (score > 0) scored.push({ skill: s, score });
|
|
14966
|
-
}
|
|
14967
|
-
scored.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
14968
|
-
return scored.slice(0, limit).map((x) => summarize(x.skill));
|
|
14969
|
-
}
|
|
14970
|
-
function loadSkill(id) {
|
|
14971
|
-
return ensureLoaded().get(id) ?? null;
|
|
14972
|
-
}
|
|
14973
|
-
function saveUserSkill(skill) {
|
|
14974
|
-
const errors = validateSkill(skill);
|
|
14975
|
-
if (errors.length > 0) {
|
|
14976
|
-
throw new Error(`skill validation failed: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
14977
|
-
}
|
|
14978
|
-
const dir2 = userDir();
|
|
14979
|
-
const path2 = (0, import_node_path15.join)(dir2, `${skill.id}.json`);
|
|
14980
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14981
|
-
const out = {
|
|
14982
|
-
...skill,
|
|
14983
|
-
created_at: skill.created_at ?? now,
|
|
14984
|
-
updated_at: now
|
|
14985
|
-
};
|
|
14986
|
-
(0, import_node_fs14.writeFileSync)(path2, JSON.stringify(out, null, 2), "utf-8");
|
|
14987
|
-
invalidateSkillCache();
|
|
14988
|
-
return { path: path2 };
|
|
14989
|
-
}
|
|
14990
|
-
function userSkillsDir() {
|
|
14991
|
-
return userDir();
|
|
14992
|
-
}
|
|
15262
|
+
// src/memory/index.ts
|
|
15263
|
+
init_text_search();
|
|
14993
15264
|
|
|
14994
|
-
// src/
|
|
14995
|
-
|
|
14996
|
-
const lines = [];
|
|
14997
|
-
lines.push(`=== SKILL LOADED: ${skill.name} (v${skill.version}) ===`);
|
|
14998
|
-
lines.push(`Category: ${skill.category} Tags: ${skill.tags.join(", ")}`);
|
|
14999
|
-
lines.push("");
|
|
15000
|
-
lines.push(skill.description);
|
|
15001
|
-
lines.push("");
|
|
15002
|
-
if (skill.disclaimer) {
|
|
15003
|
-
lines.push("REQUIRED DISCLAIMER (recite at start of the substantive turn):");
|
|
15004
|
-
lines.push(` "${skill.disclaimer}"`);
|
|
15005
|
-
lines.push("");
|
|
15006
|
-
}
|
|
15007
|
-
lines.push("WHEN TO USE THIS:");
|
|
15008
|
-
lines.push(` ${skill.context.when_to_use}`);
|
|
15009
|
-
if (skill.context.preconditions.length > 0) {
|
|
15010
|
-
lines.push("Preconditions:");
|
|
15011
|
-
for (const p of skill.context.preconditions) lines.push(` - ${p}`);
|
|
15012
|
-
}
|
|
15013
|
-
lines.push("");
|
|
15014
|
-
lines.push("PRINCIPLES:");
|
|
15015
|
-
for (const p of skill.principles) lines.push(` - ${p}`);
|
|
15016
|
-
lines.push("");
|
|
15017
|
-
if (Object.keys(skill.phrases).length > 0) {
|
|
15018
|
-
lines.push("PHRASES (paraphrase to match your voice; keep the structural move):");
|
|
15019
|
-
for (const [k, v] of Object.entries(skill.phrases)) lines.push(` [${k}] "${v}"`);
|
|
15020
|
-
lines.push("");
|
|
15021
|
-
}
|
|
15022
|
-
if (skill.tactics.length > 0) {
|
|
15023
|
-
lines.push("TACTICS (try in order; fall back to next on failure):");
|
|
15024
|
-
skill.tactics.forEach((t, i) => {
|
|
15025
|
-
lines.push(` ${i + 1}. ${t.name}`);
|
|
15026
|
-
lines.push(` When: ${t.when}`);
|
|
15027
|
-
lines.push(` Script: "${t.script}"`);
|
|
15028
|
-
});
|
|
15029
|
-
lines.push("");
|
|
15030
|
-
}
|
|
15031
|
-
if (skill.boundaries.length > 0) {
|
|
15032
|
-
lines.push("HARD BOUNDARIES \u2014 do not cross:");
|
|
15033
|
-
for (const b of skill.boundaries) lines.push(` - ${b}`);
|
|
15034
|
-
lines.push("");
|
|
15035
|
-
}
|
|
15036
|
-
lines.push("SUCCESS SIGNALS:");
|
|
15037
|
-
for (const s of skill.success_signals) lines.push(` - ${s}`);
|
|
15038
|
-
lines.push("");
|
|
15039
|
-
lines.push("FAILURE SIGNALS \u2014 when these appear, escalate or exit:");
|
|
15040
|
-
for (const s of skill.failure_signals) lines.push(` - ${s}`);
|
|
15041
|
-
lines.push("");
|
|
15042
|
-
lines.push("EXIT:");
|
|
15043
|
-
lines.push(` On success: ${skill.exit_strategy.on_success}`);
|
|
15044
|
-
lines.push(` On failure: ${skill.exit_strategy.on_failure}`);
|
|
15045
|
-
if (skill.exit_strategy.follow_ups && skill.exit_strategy.follow_ups.length > 0) {
|
|
15046
|
-
lines.push(" Follow-ups (after the call):");
|
|
15047
|
-
for (const f of skill.exit_strategy.follow_ups) lines.push(` - ${f}`);
|
|
15048
|
-
}
|
|
15049
|
-
lines.push("");
|
|
15050
|
-
lines.push("=== END SKILL ===");
|
|
15051
|
-
return lines.join("\n");
|
|
15052
|
-
}
|
|
15265
|
+
// src/index.ts
|
|
15266
|
+
init_skills();
|
|
15053
15267
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15054
15268
|
0 && (module.exports = {
|
|
15055
15269
|
AGENT_ROLES,
|
|
@@ -15080,6 +15294,7 @@ function renderSkillAsPrompt(skill) {
|
|
|
15080
15294
|
GET_DATETIME_TOOL,
|
|
15081
15295
|
GatewayManager,
|
|
15082
15296
|
InboxWatcher,
|
|
15297
|
+
LOAD_SKILL_TOOL,
|
|
15083
15298
|
MEMORY_CATEGORIES,
|
|
15084
15299
|
MailReceiver,
|
|
15085
15300
|
MailSender,
|
|
@@ -15116,6 +15331,7 @@ function renderSkillAsPrompt(skill) {
|
|
|
15116
15331
|
RelayBridge,
|
|
15117
15332
|
RelayGateway,
|
|
15118
15333
|
SEARCH_EMAIL_TOOL,
|
|
15334
|
+
SEARCH_SKILLS_TOOL,
|
|
15119
15335
|
SPAM_THRESHOLD,
|
|
15120
15336
|
ServiceManager,
|
|
15121
15337
|
SetupManager,
|