@agenticmail/core 0.9.24 → 0.9.25
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 +1142 -927
- package/dist/index.d.cts +94 -9
- package/dist/index.d.ts +94 -9
- package/dist/index.js +176 -696
- 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,
|
|
@@ -9669,12 +10411,46 @@ var SEARCH_EMAIL_TOOL = {
|
|
|
9669
10411
|
additionalProperties: false
|
|
9670
10412
|
}
|
|
9671
10413
|
};
|
|
10414
|
+
var SEARCH_SKILLS_TOOL = {
|
|
10415
|
+
type: "function",
|
|
10416
|
+
name: "search_skills",
|
|
10417
|
+
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.",
|
|
10418
|
+
parameters: {
|
|
10419
|
+
type: "object",
|
|
10420
|
+
properties: {
|
|
10421
|
+
query: {
|
|
10422
|
+
type: "string",
|
|
10423
|
+
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".'
|
|
10424
|
+
}
|
|
10425
|
+
},
|
|
10426
|
+
required: ["query"],
|
|
10427
|
+
additionalProperties: false
|
|
10428
|
+
}
|
|
10429
|
+
};
|
|
10430
|
+
var LOAD_SKILL_TOOL = {
|
|
10431
|
+
type: "function",
|
|
10432
|
+
name: "load_skill",
|
|
10433
|
+
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.',
|
|
10434
|
+
parameters: {
|
|
10435
|
+
type: "object",
|
|
10436
|
+
properties: {
|
|
10437
|
+
id: {
|
|
10438
|
+
type: "string",
|
|
10439
|
+
description: 'Skill id (lowercase-hyphenated), e.g. "negotiate-bill-reduction". Get it from search_skills.'
|
|
10440
|
+
}
|
|
10441
|
+
},
|
|
10442
|
+
required: ["id"],
|
|
10443
|
+
additionalProperties: false
|
|
10444
|
+
}
|
|
10445
|
+
};
|
|
9672
10446
|
var REALTIME_TOOL_DEFINITIONS = {
|
|
9673
10447
|
ask_operator: ASK_OPERATOR_TOOL,
|
|
9674
10448
|
web_search: WEB_SEARCH_TOOL,
|
|
9675
10449
|
recall_memory: RECALL_MEMORY_TOOL,
|
|
9676
10450
|
get_datetime: GET_DATETIME_TOOL,
|
|
9677
|
-
search_email: SEARCH_EMAIL_TOOL
|
|
10451
|
+
search_email: SEARCH_EMAIL_TOOL,
|
|
10452
|
+
search_skills: SEARCH_SKILLS_TOOL,
|
|
10453
|
+
load_skill: LOAD_SKILL_TOOL
|
|
9678
10454
|
};
|
|
9679
10455
|
function buildRealtimeToolGuidance(tools) {
|
|
9680
10456
|
if (tools.length === 0) return "";
|
|
@@ -9693,6 +10469,16 @@ function buildRealtimeToolGuidance(tools) {
|
|
|
9693
10469
|
'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
10470
|
);
|
|
9695
10471
|
}
|
|
10472
|
+
if (names.has("search_skills") && names.has("load_skill")) {
|
|
10473
|
+
lines.push(
|
|
10474
|
+
`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:
|
|
10475
|
+
1. Tell the caller you need a moment: "Hold on one moment \u2014 let me check something."
|
|
10476
|
+
2. Call search_skills with a one-line description of the situation.
|
|
10477
|
+
3. Call load_skill with the id of the best match.
|
|
10478
|
+
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.
|
|
10479
|
+
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.`
|
|
10480
|
+
);
|
|
10481
|
+
}
|
|
9696
10482
|
return lines.join("\n");
|
|
9697
10483
|
}
|
|
9698
10484
|
function toolErrorText(err) {
|
|
@@ -9878,6 +10664,7 @@ var REALTIME_AUDIO_SAMPLE_RATE = 24e3;
|
|
|
9878
10664
|
var REALTIME_MAX_AUDIO_FRAME_BASE64 = 256 * 1024;
|
|
9879
10665
|
var MAX_PENDING_AUDIO_FRAMES = 200;
|
|
9880
10666
|
var REALTIME_TOOL_CALL_TIMEOUT_MS = 6 * 6e4;
|
|
10667
|
+
var MAX_LOADED_SKILLS = 2;
|
|
9881
10668
|
var MAX_IN_FLIGHT_TOOL_CALLS = 8;
|
|
9882
10669
|
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
10670
|
function buildRealtimeInstructions(opts) {
|
|
@@ -9969,6 +10756,26 @@ var RealtimeVoiceBridge = class {
|
|
|
9969
10756
|
toolCallNames = /* @__PURE__ */ new Map();
|
|
9970
10757
|
/** `call_id`s whose tool call is currently executing. */
|
|
9971
10758
|
inFlightToolCalls = /* @__PURE__ */ new Set();
|
|
10759
|
+
/**
|
|
10760
|
+
* Mid-call skills loaded into the session so far, FIFO. Earliest at
|
|
10761
|
+
* index 0; cap at {@link MAX_LOADED_SKILLS}. When a (cap+1)th skill
|
|
10762
|
+
* is loaded the oldest one drops out — the model can't usefully
|
|
10763
|
+
* hold five playbooks in working memory at once, so we keep the
|
|
10764
|
+
* working set narrow on purpose.
|
|
10765
|
+
*/
|
|
10766
|
+
loadedSkills = [];
|
|
10767
|
+
/**
|
|
10768
|
+
* The original `instructions` string from the session.update sent at
|
|
10769
|
+
* open. We keep a private copy because every mid-call skill load
|
|
10770
|
+
* issues a fresh `session.update` whose `instructions` is built as:
|
|
10771
|
+
*
|
|
10772
|
+
* baseInstructions + "\n\n" + renderedSkill1 + "\n\n" + renderedSkill2 …
|
|
10773
|
+
*
|
|
10774
|
+
* Without this snapshot, successive loads would compound — the second
|
|
10775
|
+
* load would see "base + skill1" as the base and append skill2 to
|
|
10776
|
+
* THAT, eventually drifting unboundedly.
|
|
10777
|
+
*/
|
|
10778
|
+
baseInstructions = "";
|
|
9972
10779
|
constructor(opts) {
|
|
9973
10780
|
const carrier = opts.carrier ?? opts.elks;
|
|
9974
10781
|
if (!carrier) {
|
|
@@ -10005,6 +10812,10 @@ var RealtimeVoiceBridge = class {
|
|
|
10005
10812
|
handleOpenAIOpen() {
|
|
10006
10813
|
if (this.ended || this.openaiReady) return;
|
|
10007
10814
|
this.openaiReady = true;
|
|
10815
|
+
const sess = this.sessionConfig?.session;
|
|
10816
|
+
if (sess && typeof sess.instructions === "string") {
|
|
10817
|
+
this.baseInstructions = sess.instructions;
|
|
10818
|
+
}
|
|
10008
10819
|
this.safeSend(this.openai, this.sessionConfig);
|
|
10009
10820
|
this.safeSend(this.openai, { type: "response.create" });
|
|
10010
10821
|
for (const audio of this.pendingAudio.splice(0)) {
|
|
@@ -10015,6 +10826,74 @@ var RealtimeVoiceBridge = class {
|
|
|
10015
10826
|
handleOpenAIClose() {
|
|
10016
10827
|
this.end("openai-closed");
|
|
10017
10828
|
}
|
|
10829
|
+
/**
|
|
10830
|
+
* Load a skill playbook into the live OpenAI Realtime session for
|
|
10831
|
+
* the rest of the call.
|
|
10832
|
+
*
|
|
10833
|
+
* Mechanics:
|
|
10834
|
+
* 1. Resolve the skill JSON via the skills registry (file on disk).
|
|
10835
|
+
* 2. Append the rendered skill text to the agent's working
|
|
10836
|
+
* instructions and re-send a `session.update` carrying ONLY
|
|
10837
|
+
* the new `instructions` field. The OpenAI Realtime API
|
|
10838
|
+
* supports partial session.update — we don't have to re-send
|
|
10839
|
+
* audio config, tools, voice, etc.
|
|
10840
|
+
* 3. Track which skills are loaded so we (a) FIFO-evict the
|
|
10841
|
+
* oldest when the cap is hit and (b) include every still-
|
|
10842
|
+
* loaded skill in the next composed instructions.
|
|
10843
|
+
* 4. Emit a transcript marker so the mission record shows the
|
|
10844
|
+
* adaptation ("[skill loaded: Negotiate a Bill Reduction
|
|
10845
|
+
* v1.0.0]"). Useful for post-call review and for the build
|
|
10846
|
+
* farm's telemetry on which skills actually got reached for.
|
|
10847
|
+
*
|
|
10848
|
+
* Returns an object the {@link load_skill} tool handler can serialise
|
|
10849
|
+
* back to the model: `ok: true` plus the skill name + version on
|
|
10850
|
+
* success, `ok: false` plus a short reason on failure (unknown id,
|
|
10851
|
+
* call ended, registry I/O error). Never throws — a buggy registry
|
|
10852
|
+
* or a missing file must not crash the bridge mid-call.
|
|
10853
|
+
*
|
|
10854
|
+
* Phase 2 of the skill library (`docs/skill-library-plan.md`).
|
|
10855
|
+
*/
|
|
10856
|
+
async loadSkillIntoSession(skillId) {
|
|
10857
|
+
if (this.ended) return { ok: false, message: "Call has already ended; cannot load a skill now." };
|
|
10858
|
+
if (!this.openaiReady) return { ok: false, message: "Session is not ready yet; try again in a moment." };
|
|
10859
|
+
if (this.loadedSkills.some((s) => s.id === skillId)) {
|
|
10860
|
+
const existing = this.loadedSkills.find((s) => s.id === skillId);
|
|
10861
|
+
return { ok: true, message: `Skill "${skillId}" is already loaded.`, name: skillId, version: existing.version };
|
|
10862
|
+
}
|
|
10863
|
+
let loadSkill2;
|
|
10864
|
+
let renderSkillAsPrompt2;
|
|
10865
|
+
try {
|
|
10866
|
+
({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await Promise.resolve().then(() => (init_skills(), skills_exports)));
|
|
10867
|
+
} catch (err) {
|
|
10868
|
+
return { ok: false, message: `Skill registry unavailable: ${errorText(err)}` };
|
|
10869
|
+
}
|
|
10870
|
+
const skill = loadSkill2(skillId);
|
|
10871
|
+
if (!skill) {
|
|
10872
|
+
return { ok: false, message: `No skill found with id "${skillId}". Call search_skills first to find the right id.` };
|
|
10873
|
+
}
|
|
10874
|
+
const rendered = renderSkillAsPrompt2(skill);
|
|
10875
|
+
while (this.loadedSkills.length >= MAX_LOADED_SKILLS) {
|
|
10876
|
+
const dropped = this.loadedSkills.shift();
|
|
10877
|
+
if (dropped) {
|
|
10878
|
+
this.emitTranscript("system", `[skill unloaded for working-memory limit: ${dropped.id} v${dropped.version}]`);
|
|
10879
|
+
}
|
|
10880
|
+
}
|
|
10881
|
+
this.loadedSkills.push({ id: skill.id, version: skill.version, renderedPrompt: rendered });
|
|
10882
|
+
const composed = [
|
|
10883
|
+
this.baseInstructions,
|
|
10884
|
+
...this.loadedSkills.map((s) => s.renderedPrompt)
|
|
10885
|
+
].filter((s) => s && s.length > 0).join("\n\n");
|
|
10886
|
+
this.safeSend(this.openai, {
|
|
10887
|
+
type: "session.update",
|
|
10888
|
+
session: { instructions: composed }
|
|
10889
|
+
});
|
|
10890
|
+
this.emitTranscript("system", `[skill loaded: ${skill.name} v${skill.version}]`);
|
|
10891
|
+
return { ok: true, message: `Loaded skill: ${skill.name} (v${skill.version})`, name: skill.name, version: skill.version };
|
|
10892
|
+
}
|
|
10893
|
+
/** The list of skills currently loaded into the session (FIFO-ordered). */
|
|
10894
|
+
get loadedSkillIds() {
|
|
10895
|
+
return this.loadedSkills.map((s) => s.id);
|
|
10896
|
+
}
|
|
10018
10897
|
/** Call when the OpenAI socket errors. */
|
|
10019
10898
|
handleOpenAIError(err) {
|
|
10020
10899
|
this.emitTranscript("system", `OpenAI Realtime error: ${errorText(err)}`);
|
|
@@ -10475,7 +11354,7 @@ try {
|
|
|
10475
11354
|
}
|
|
10476
11355
|
|
|
10477
11356
|
// src/util/safe-path.ts
|
|
10478
|
-
var
|
|
11357
|
+
var import_node_path6 = require("path");
|
|
10479
11358
|
var PathTraversalError = class extends Error {
|
|
10480
11359
|
constructor(baseDir, parts) {
|
|
10481
11360
|
super(
|
|
@@ -10498,14 +11377,14 @@ function safeJoin(baseDir, ...partsAndOpts) {
|
|
|
10498
11377
|
}
|
|
10499
11378
|
if (!opts.allowAbsolute) {
|
|
10500
11379
|
for (const part of parts) {
|
|
10501
|
-
if ((0,
|
|
11380
|
+
if ((0, import_node_path6.isAbsolute)(part)) {
|
|
10502
11381
|
throw new PathTraversalError(baseDir, parts);
|
|
10503
11382
|
}
|
|
10504
11383
|
}
|
|
10505
11384
|
}
|
|
10506
|
-
const resolvedBase = (0,
|
|
10507
|
-
const resolved = (0,
|
|
10508
|
-
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase +
|
|
11385
|
+
const resolvedBase = (0, import_node_path6.resolve)(baseDir);
|
|
11386
|
+
const resolved = (0, import_node_path6.resolve)(resolvedBase, ...parts);
|
|
11387
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + import_node_path6.sep)) {
|
|
10509
11388
|
throw new PathTraversalError(baseDir, parts);
|
|
10510
11389
|
}
|
|
10511
11390
|
return resolved;
|
|
@@ -10519,9 +11398,9 @@ function tryJoin(baseDir, ...parts) {
|
|
|
10519
11398
|
}
|
|
10520
11399
|
}
|
|
10521
11400
|
function assertWithinBase(baseDir, candidate) {
|
|
10522
|
-
const resolvedBase = (0,
|
|
10523
|
-
const resolvedCandidate = (0,
|
|
10524
|
-
if (resolvedCandidate !== resolvedBase && !resolvedCandidate.startsWith(resolvedBase +
|
|
11401
|
+
const resolvedBase = (0, import_node_path6.resolve)(baseDir);
|
|
11402
|
+
const resolvedCandidate = (0, import_node_path6.resolve)(candidate);
|
|
11403
|
+
if (resolvedCandidate !== resolvedBase && !resolvedCandidate.startsWith(resolvedBase + import_node_path6.sep)) {
|
|
10525
11404
|
throw new PathTraversalError(baseDir, [candidate]);
|
|
10526
11405
|
}
|
|
10527
11406
|
return resolvedCandidate;
|
|
@@ -10574,19 +11453,19 @@ function redactObject(input, _depth = 0) {
|
|
|
10574
11453
|
}
|
|
10575
11454
|
|
|
10576
11455
|
// src/operator-prefs.ts
|
|
10577
|
-
var
|
|
10578
|
-
var
|
|
10579
|
-
var
|
|
11456
|
+
var import_node_fs5 = require("fs");
|
|
11457
|
+
var import_node_os5 = require("os");
|
|
11458
|
+
var import_node_path7 = require("path");
|
|
10580
11459
|
function dir() {
|
|
10581
|
-
return (0,
|
|
11460
|
+
return (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".agenticmail");
|
|
10582
11461
|
}
|
|
10583
11462
|
function path() {
|
|
10584
|
-
return (0,
|
|
11463
|
+
return (0, import_node_path7.join)(dir(), "operator-prefs.json");
|
|
10585
11464
|
}
|
|
10586
11465
|
function readFile() {
|
|
10587
|
-
if (!(0,
|
|
11466
|
+
if (!(0, import_node_fs5.existsSync)(path())) return { version: 1 };
|
|
10588
11467
|
try {
|
|
10589
|
-
const raw = (0,
|
|
11468
|
+
const raw = (0, import_node_fs5.readFileSync)(path(), "utf-8");
|
|
10590
11469
|
if (!raw.trim()) return { version: 1 };
|
|
10591
11470
|
const parsed = JSON.parse(raw);
|
|
10592
11471
|
return { version: 1, operatorEmail: typeof parsed.operatorEmail === "string" ? parsed.operatorEmail : void 0 };
|
|
@@ -10597,10 +11476,10 @@ function readFile() {
|
|
|
10597
11476
|
function writeFile(shape) {
|
|
10598
11477
|
const d = dir();
|
|
10599
11478
|
const p = path();
|
|
10600
|
-
if (!(0,
|
|
11479
|
+
if (!(0, import_node_fs5.existsSync)(d)) (0, import_node_fs5.mkdirSync)(d, { recursive: true });
|
|
10601
11480
|
const tmp = `${p}.agenticmail-tmp-${process.pid}`;
|
|
10602
|
-
(0,
|
|
10603
|
-
(0,
|
|
11481
|
+
(0, import_node_fs5.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
|
|
11482
|
+
(0, import_node_fs5.renameSync)(tmp, p);
|
|
10604
11483
|
}
|
|
10605
11484
|
function getOperatorEmail() {
|
|
10606
11485
|
const shape = readFile();
|
|
@@ -10629,21 +11508,21 @@ function operatorPrefsStoragePath() {
|
|
|
10629
11508
|
}
|
|
10630
11509
|
|
|
10631
11510
|
// src/host-sessions.ts
|
|
10632
|
-
var
|
|
10633
|
-
var
|
|
10634
|
-
var
|
|
11511
|
+
var import_node_fs6 = require("fs");
|
|
11512
|
+
var import_node_path8 = require("path");
|
|
11513
|
+
var import_node_os6 = require("os");
|
|
10635
11514
|
function storageDir() {
|
|
10636
|
-
return (0,
|
|
11515
|
+
return (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".agenticmail");
|
|
10637
11516
|
}
|
|
10638
11517
|
function storagePath() {
|
|
10639
|
-
return (0,
|
|
11518
|
+
return (0, import_node_path8.join)(storageDir(), "host-sessions.json");
|
|
10640
11519
|
}
|
|
10641
11520
|
var DEFAULT_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
10642
11521
|
function readFile2() {
|
|
10643
11522
|
const p = storagePath();
|
|
10644
|
-
if (!(0,
|
|
11523
|
+
if (!(0, import_node_fs6.existsSync)(p)) return { version: 1, sessions: {} };
|
|
10645
11524
|
try {
|
|
10646
|
-
const raw = (0,
|
|
11525
|
+
const raw = (0, import_node_fs6.readFileSync)(p, "utf-8");
|
|
10647
11526
|
if (!raw.trim()) return { version: 1, sessions: {} };
|
|
10648
11527
|
const parsed = JSON.parse(raw);
|
|
10649
11528
|
return {
|
|
@@ -10657,10 +11536,10 @@ function readFile2() {
|
|
|
10657
11536
|
function writeFile2(shape) {
|
|
10658
11537
|
const dir2 = storageDir();
|
|
10659
11538
|
const p = storagePath();
|
|
10660
|
-
if (!(0,
|
|
11539
|
+
if (!(0, import_node_fs6.existsSync)(dir2)) (0, import_node_fs6.mkdirSync)(dir2, { recursive: true });
|
|
10661
11540
|
const tmp = `${p}.agenticmail-tmp-${process.pid}`;
|
|
10662
|
-
(0,
|
|
10663
|
-
(0,
|
|
11541
|
+
(0, import_node_fs6.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
|
|
11542
|
+
(0, import_node_fs6.renameSync)(tmp, p);
|
|
10664
11543
|
}
|
|
10665
11544
|
function saveHostSession(host, session) {
|
|
10666
11545
|
if (!session.sessionId) return;
|
|
@@ -10835,15 +11714,15 @@ function buildApiUrl(baseOrigin, pathAndQuery) {
|
|
|
10835
11714
|
|
|
10836
11715
|
// src/setup/index.ts
|
|
10837
11716
|
var import_node_crypto6 = require("crypto");
|
|
10838
|
-
var
|
|
10839
|
-
var
|
|
10840
|
-
var
|
|
11717
|
+
var import_node_fs10 = require("fs");
|
|
11718
|
+
var import_node_path12 = require("path");
|
|
11719
|
+
var import_node_os10 = require("os");
|
|
10841
11720
|
|
|
10842
11721
|
// src/setup/deps.ts
|
|
10843
11722
|
var import_node_child_process2 = require("child_process");
|
|
10844
|
-
var
|
|
10845
|
-
var
|
|
10846
|
-
var
|
|
11723
|
+
var import_node_fs7 = require("fs");
|
|
11724
|
+
var import_node_path9 = require("path");
|
|
11725
|
+
var import_node_os7 = require("os");
|
|
10847
11726
|
var DependencyChecker = class {
|
|
10848
11727
|
async checkAll() {
|
|
10849
11728
|
return Promise.all([
|
|
@@ -10893,8 +11772,8 @@ var DependencyChecker = class {
|
|
|
10893
11772
|
}
|
|
10894
11773
|
}
|
|
10895
11774
|
async checkCloudflared() {
|
|
10896
|
-
const binPath = (0,
|
|
10897
|
-
if ((0,
|
|
11775
|
+
const binPath = (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".agenticmail", "bin", "cloudflared");
|
|
11776
|
+
if ((0, import_node_fs7.existsSync)(binPath)) {
|
|
10898
11777
|
let version;
|
|
10899
11778
|
try {
|
|
10900
11779
|
const output = (0, import_node_child_process2.execFileSync)(binPath, ["--version"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
@@ -10916,10 +11795,10 @@ var DependencyChecker = class {
|
|
|
10916
11795
|
|
|
10917
11796
|
// src/setup/installer.ts
|
|
10918
11797
|
var import_node_child_process3 = require("child_process");
|
|
10919
|
-
var
|
|
11798
|
+
var import_node_fs8 = require("fs");
|
|
10920
11799
|
var import_promises2 = require("fs/promises");
|
|
10921
|
-
var
|
|
10922
|
-
var
|
|
11800
|
+
var import_node_path10 = require("path");
|
|
11801
|
+
var import_node_os8 = require("os");
|
|
10923
11802
|
function runShellWithRollingOutput(cmd, opts = {}) {
|
|
10924
11803
|
const maxLines = opts.maxLines ?? 20;
|
|
10925
11804
|
const timeout = opts.timeout ?? 3e5;
|
|
@@ -11029,7 +11908,7 @@ var DependencyInstaller = class {
|
|
|
11029
11908
|
*/
|
|
11030
11909
|
async installDocker() {
|
|
11031
11910
|
if (this.isDockerReady()) return;
|
|
11032
|
-
const os = (0,
|
|
11911
|
+
const os = (0, import_node_os8.platform)();
|
|
11033
11912
|
if (os === "darwin") {
|
|
11034
11913
|
await this.installDockerMac();
|
|
11035
11914
|
} else if (os === "linux") {
|
|
@@ -11095,15 +11974,15 @@ var DependencyInstaller = class {
|
|
|
11095
11974
|
try {
|
|
11096
11975
|
const composeBin = (0, import_node_child_process3.execFileSync)("which", ["docker-compose"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
11097
11976
|
if (!composeBin) return;
|
|
11098
|
-
const pluginDir = (0,
|
|
11099
|
-
const pluginPath = (0,
|
|
11100
|
-
if ((0,
|
|
11977
|
+
const pluginDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".docker", "cli-plugins");
|
|
11978
|
+
const pluginPath = (0, import_node_path10.join)(pluginDir, "docker-compose");
|
|
11979
|
+
if ((0, import_node_fs8.existsSync)(pluginPath)) return;
|
|
11101
11980
|
try {
|
|
11102
|
-
(0,
|
|
11981
|
+
(0, import_node_fs8.mkdirSync)(pluginDir, { recursive: true });
|
|
11103
11982
|
} catch {
|
|
11104
11983
|
}
|
|
11105
11984
|
try {
|
|
11106
|
-
(0,
|
|
11985
|
+
(0, import_node_fs8.symlinkSync)(composeBin, pluginPath);
|
|
11107
11986
|
} catch {
|
|
11108
11987
|
}
|
|
11109
11988
|
} catch {
|
|
@@ -11162,9 +12041,9 @@ var DependencyInstaller = class {
|
|
|
11162
12041
|
return;
|
|
11163
12042
|
}
|
|
11164
12043
|
this.onProgress("__progress__:5:Installing Docker Engine...");
|
|
11165
|
-
const tmpDir = (0,
|
|
12044
|
+
const tmpDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "tmp");
|
|
11166
12045
|
await (0, import_promises2.mkdir)(tmpDir, { recursive: true });
|
|
11167
|
-
const scriptPath = (0,
|
|
12046
|
+
const scriptPath = (0, import_node_path10.join)(tmpDir, "install-docker.sh");
|
|
11168
12047
|
const dlResult = await runShellWithRollingOutput(
|
|
11169
12048
|
`curl -fsSL https://get.docker.com -o "${scriptPath}" && sudo sh "${scriptPath}"`,
|
|
11170
12049
|
{ timeout: 3e5 }
|
|
@@ -11232,8 +12111,8 @@ var DependencyInstaller = class {
|
|
|
11232
12111
|
}
|
|
11233
12112
|
try {
|
|
11234
12113
|
const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
11235
|
-
const dockerExe = (0,
|
|
11236
|
-
if ((0,
|
|
12114
|
+
const dockerExe = (0, import_node_path10.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
|
|
12115
|
+
if ((0, import_node_fs8.existsSync)(dockerExe)) {
|
|
11237
12116
|
(0, import_node_child_process3.execSync)(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
|
|
11238
12117
|
}
|
|
11239
12118
|
} catch {
|
|
@@ -11283,8 +12162,8 @@ var DependencyInstaller = class {
|
|
|
11283
12162
|
this.onProgress("__progress__:70:Docker Desktop installed. Starting...");
|
|
11284
12163
|
try {
|
|
11285
12164
|
const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
|
|
11286
|
-
const dockerExe = (0,
|
|
11287
|
-
if ((0,
|
|
12165
|
+
const dockerExe = (0, import_node_path10.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
|
|
12166
|
+
if ((0, import_node_fs8.existsSync)(dockerExe)) {
|
|
11288
12167
|
(0, import_node_child_process3.execSync)(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
|
|
11289
12168
|
}
|
|
11290
12169
|
} catch {
|
|
@@ -11321,12 +12200,12 @@ var DependencyInstaller = class {
|
|
|
11321
12200
|
* Start the Stalwart mail server Docker container.
|
|
11322
12201
|
*/
|
|
11323
12202
|
async startStalwart(composePath) {
|
|
11324
|
-
if (!(0,
|
|
12203
|
+
if (!(0, import_node_fs8.existsSync)(composePath)) {
|
|
11325
12204
|
throw new Error(`docker-compose.yml not found at: ${composePath}`);
|
|
11326
12205
|
}
|
|
11327
12206
|
if (!this.isDockerReady()) {
|
|
11328
12207
|
this.onProgress("Starting Docker...");
|
|
11329
|
-
const os = (0,
|
|
12208
|
+
const os = (0, import_node_os8.platform)();
|
|
11330
12209
|
if (os === "darwin") {
|
|
11331
12210
|
await this.startColima();
|
|
11332
12211
|
} else if (os === "win32") {
|
|
@@ -11344,7 +12223,7 @@ var DependencyInstaller = class {
|
|
|
11344
12223
|
}
|
|
11345
12224
|
}
|
|
11346
12225
|
this.onProgress("__progress__:10:Pulling mail server image...");
|
|
11347
|
-
if ((0,
|
|
12226
|
+
if ((0, import_node_os8.platform)() === "darwin") this.linkComposePlugin();
|
|
11348
12227
|
let composeResult = await runSilent("docker", ["compose", "-f", composePath, "up", "-d"], { timeout: 12e4 });
|
|
11349
12228
|
if (composeResult.exitCode !== 0 && hasCommand("docker-compose")) {
|
|
11350
12229
|
composeResult = await runSilent("docker-compose", ["-f", composePath, "up", "-d"], { timeout: 12e4 });
|
|
@@ -11379,22 +12258,22 @@ var DependencyInstaller = class {
|
|
|
11379
12258
|
* Returns the path to the installed binary.
|
|
11380
12259
|
*/
|
|
11381
12260
|
async installCloudflared() {
|
|
11382
|
-
const os = (0,
|
|
11383
|
-
const binDir = (0,
|
|
12261
|
+
const os = (0, import_node_os8.platform)();
|
|
12262
|
+
const binDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "bin");
|
|
11384
12263
|
const binName = os === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
11385
|
-
const binPath = (0,
|
|
11386
|
-
if ((0,
|
|
12264
|
+
const binPath = (0, import_node_path10.join)(binDir, binName);
|
|
12265
|
+
if ((0, import_node_fs8.existsSync)(binPath)) {
|
|
11387
12266
|
return binPath;
|
|
11388
12267
|
}
|
|
11389
12268
|
try {
|
|
11390
12269
|
const whichCmd = os === "win32" ? "where" : "which";
|
|
11391
12270
|
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,
|
|
12271
|
+
if (sysPath && (0, import_node_fs8.existsSync)(sysPath)) return sysPath;
|
|
11393
12272
|
} catch {
|
|
11394
12273
|
}
|
|
11395
12274
|
this.onProgress("Downloading cloudflared...");
|
|
11396
12275
|
await (0, import_promises2.mkdir)(binDir, { recursive: true });
|
|
11397
|
-
const cpu = (0,
|
|
12276
|
+
const cpu = (0, import_node_os8.arch)();
|
|
11398
12277
|
const archName = cpu === "arm64" ? "arm64" : "amd64";
|
|
11399
12278
|
let downloadUrl;
|
|
11400
12279
|
if (os === "darwin") {
|
|
@@ -11412,7 +12291,7 @@ var DependencyInstaller = class {
|
|
|
11412
12291
|
}
|
|
11413
12292
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
11414
12293
|
if (os === "darwin") {
|
|
11415
|
-
const tgzPath = (0,
|
|
12294
|
+
const tgzPath = (0, import_node_path10.join)(binDir, "cloudflared.tgz");
|
|
11416
12295
|
await (0, import_promises2.writeFile)(tgzPath, buffer);
|
|
11417
12296
|
try {
|
|
11418
12297
|
(0, import_node_child_process3.execFileSync)("tar", ["-xzf", tgzPath, "-C", binDir, "cloudflared"], { timeout: 15e3, stdio: "ignore" });
|
|
@@ -11429,7 +12308,7 @@ var DependencyInstaller = class {
|
|
|
11429
12308
|
if (os !== "win32") await (0, import_promises2.chmod)(tmpPath, 493);
|
|
11430
12309
|
await (0, import_promises2.rename)(tmpPath, binPath);
|
|
11431
12310
|
}
|
|
11432
|
-
if (!(0,
|
|
12311
|
+
if (!(0, import_node_fs8.existsSync)(binPath)) {
|
|
11433
12312
|
throw new Error("cloudflared download succeeded but binary not found after extraction");
|
|
11434
12313
|
}
|
|
11435
12314
|
this.onProgress("cloudflared installed");
|
|
@@ -11447,23 +12326,23 @@ var DependencyInstaller = class {
|
|
|
11447
12326
|
|
|
11448
12327
|
// src/setup/service.ts
|
|
11449
12328
|
var import_node_child_process4 = require("child_process");
|
|
11450
|
-
var
|
|
11451
|
-
var
|
|
11452
|
-
var
|
|
12329
|
+
var import_node_fs9 = require("fs");
|
|
12330
|
+
var import_node_path11 = require("path");
|
|
12331
|
+
var import_node_os9 = require("os");
|
|
11453
12332
|
var import_node_module2 = require("module");
|
|
11454
|
-
var
|
|
12333
|
+
var import_meta3 = {};
|
|
11455
12334
|
var PLIST_LABEL = "com.agenticmail.server";
|
|
11456
12335
|
var SYSTEMD_UNIT = "agenticmail.service";
|
|
11457
12336
|
var ServiceManager = class {
|
|
11458
|
-
os = (0,
|
|
12337
|
+
os = (0, import_node_os9.platform)();
|
|
11459
12338
|
/**
|
|
11460
12339
|
* Get the path to the service file.
|
|
11461
12340
|
*/
|
|
11462
12341
|
getServicePath() {
|
|
11463
12342
|
if (this.os === "darwin") {
|
|
11464
|
-
return (0,
|
|
12343
|
+
return (0, import_node_path11.join)((0, import_node_os9.homedir)(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
11465
12344
|
} else {
|
|
11466
|
-
return (0,
|
|
12345
|
+
return (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
11467
12346
|
}
|
|
11468
12347
|
}
|
|
11469
12348
|
/**
|
|
@@ -11495,42 +12374,42 @@ var ServiceManager = class {
|
|
|
11495
12374
|
*/
|
|
11496
12375
|
getApiEntryPath() {
|
|
11497
12376
|
try {
|
|
11498
|
-
const req = (0, import_node_module2.createRequire)(
|
|
12377
|
+
const req = (0, import_node_module2.createRequire)(import_meta3.url);
|
|
11499
12378
|
const resolved = req.resolve("@agenticmail/api");
|
|
11500
|
-
if ((0,
|
|
12379
|
+
if ((0, import_node_fs9.existsSync)(resolved)) return resolved;
|
|
11501
12380
|
} catch {
|
|
11502
12381
|
}
|
|
11503
12382
|
const parentPackages = [
|
|
11504
|
-
(0,
|
|
12383
|
+
(0, import_node_path11.join)("@agenticmail", "cli"),
|
|
11505
12384
|
// current scoped package
|
|
11506
12385
|
"agenticmail"
|
|
11507
12386
|
// legacy unscoped package
|
|
11508
12387
|
];
|
|
11509
12388
|
const baseDirs = [
|
|
11510
12389
|
// user-local install
|
|
11511
|
-
(0,
|
|
12390
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules")
|
|
11512
12391
|
];
|
|
11513
12392
|
try {
|
|
11514
12393
|
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,
|
|
12394
|
+
baseDirs.push((0, import_node_path11.join)(prefix, "lib", "node_modules"));
|
|
12395
|
+
baseDirs.push((0, import_node_path11.join)(prefix, "node_modules"));
|
|
11517
12396
|
} catch {
|
|
11518
12397
|
}
|
|
11519
12398
|
baseDirs.push("/opt/homebrew/lib/node_modules");
|
|
11520
12399
|
baseDirs.push("/usr/local/lib/node_modules");
|
|
11521
12400
|
for (const base of baseDirs) {
|
|
11522
|
-
const sibling = (0,
|
|
11523
|
-
if ((0,
|
|
12401
|
+
const sibling = (0, import_node_path11.join)(base, "@agenticmail", "api", "dist", "index.js");
|
|
12402
|
+
if ((0, import_node_fs9.existsSync)(sibling)) return sibling;
|
|
11524
12403
|
for (const parent of parentPackages) {
|
|
11525
|
-
const nested = (0,
|
|
11526
|
-
if ((0,
|
|
12404
|
+
const nested = (0, import_node_path11.join)(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
|
|
12405
|
+
if ((0, import_node_fs9.existsSync)(nested)) return nested;
|
|
11527
12406
|
}
|
|
11528
12407
|
}
|
|
11529
|
-
const dataDir = (0,
|
|
11530
|
-
const entryCache = (0,
|
|
11531
|
-
if ((0,
|
|
11532
|
-
const cached = (0,
|
|
11533
|
-
if (cached && (0,
|
|
12408
|
+
const dataDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
|
|
12409
|
+
const entryCache = (0, import_node_path11.join)(dataDir, "api-entry.path");
|
|
12410
|
+
if ((0, import_node_fs9.existsSync)(entryCache)) {
|
|
12411
|
+
const cached = (0, import_node_fs9.readFileSync)(entryCache, "utf-8").trim();
|
|
12412
|
+
if (cached && (0, import_node_fs9.existsSync)(cached)) return cached;
|
|
11534
12413
|
}
|
|
11535
12414
|
throw new Error("Could not find @agenticmail/api entry point. Run `agenticmail start` first to populate the cache.");
|
|
11536
12415
|
}
|
|
@@ -11538,9 +12417,9 @@ var ServiceManager = class {
|
|
|
11538
12417
|
* Cache the API entry path so the service can find it later.
|
|
11539
12418
|
*/
|
|
11540
12419
|
cacheApiEntryPath(entryPath) {
|
|
11541
|
-
const dataDir = (0,
|
|
11542
|
-
if (!(0,
|
|
11543
|
-
(0,
|
|
12420
|
+
const dataDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
|
|
12421
|
+
if (!(0, import_node_fs9.existsSync)(dataDir)) (0, import_node_fs9.mkdirSync)(dataDir, { recursive: true });
|
|
12422
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path11.join)(dataDir, "api-entry.path"), entryPath);
|
|
11544
12423
|
}
|
|
11545
12424
|
/**
|
|
11546
12425
|
* Get the current package version.
|
|
@@ -11551,38 +12430,38 @@ var ServiceManager = class {
|
|
|
11551
12430
|
*/
|
|
11552
12431
|
getVersion() {
|
|
11553
12432
|
try {
|
|
11554
|
-
const req = (0, import_node_module2.createRequire)(
|
|
12433
|
+
const req = (0, import_node_module2.createRequire)(import_meta3.url);
|
|
11555
12434
|
const pkgJson = req.resolve("@agenticmail/cli/package.json");
|
|
11556
|
-
if ((0,
|
|
11557
|
-
const pkg = JSON.parse((0,
|
|
12435
|
+
if ((0, import_node_fs9.existsSync)(pkgJson)) {
|
|
12436
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(pkgJson, "utf-8"));
|
|
11558
12437
|
if (pkg.version) return pkg.version;
|
|
11559
12438
|
}
|
|
11560
12439
|
} catch {
|
|
11561
12440
|
}
|
|
11562
12441
|
try {
|
|
11563
12442
|
const apiEntry = this.getApiEntryPath();
|
|
11564
|
-
const apiPkg = (0,
|
|
11565
|
-
if ((0,
|
|
11566
|
-
const pkg = JSON.parse((0,
|
|
12443
|
+
const apiPkg = (0, import_node_path11.join)(apiEntry, "..", "..", "package.json");
|
|
12444
|
+
if ((0, import_node_fs9.existsSync)(apiPkg)) {
|
|
12445
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(apiPkg, "utf-8"));
|
|
11567
12446
|
if (pkg.version) return pkg.version;
|
|
11568
12447
|
}
|
|
11569
12448
|
} catch {
|
|
11570
12449
|
}
|
|
11571
12450
|
const candidates = [
|
|
11572
|
-
(0,
|
|
11573
|
-
(0,
|
|
11574
|
-
(0,
|
|
12451
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules", "@agenticmail", "cli", "package.json"),
|
|
12452
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules", "agenticmail", "package.json"),
|
|
12453
|
+
(0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "package-version.json")
|
|
11575
12454
|
];
|
|
11576
12455
|
try {
|
|
11577
12456
|
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,
|
|
12457
|
+
candidates.push((0, import_node_path11.join)(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
|
|
12458
|
+
candidates.push((0, import_node_path11.join)(prefix, "lib", "node_modules", "agenticmail", "package.json"));
|
|
11580
12459
|
} catch {
|
|
11581
12460
|
}
|
|
11582
12461
|
for (const p of candidates) {
|
|
11583
12462
|
try {
|
|
11584
|
-
if ((0,
|
|
11585
|
-
const pkg = JSON.parse((0,
|
|
12463
|
+
if ((0, import_node_fs9.existsSync)(p)) {
|
|
12464
|
+
const pkg = JSON.parse((0, import_node_fs9.readFileSync)(p, "utf-8"));
|
|
11586
12465
|
if (pkg.version) return pkg.version;
|
|
11587
12466
|
}
|
|
11588
12467
|
} catch {
|
|
@@ -11595,9 +12474,9 @@ var ServiceManager = class {
|
|
|
11595
12474
|
* This ensures AgenticMail doesn't fail on boot when Docker is still loading.
|
|
11596
12475
|
*/
|
|
11597
12476
|
generateStartScript(nodePath, apiEntry) {
|
|
11598
|
-
const scriptPath = (0,
|
|
11599
|
-
const scriptDir = (0,
|
|
11600
|
-
if (!(0,
|
|
12477
|
+
const scriptPath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin", "start-server.sh");
|
|
12478
|
+
const scriptDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin");
|
|
12479
|
+
if (!(0, import_node_fs9.existsSync)(scriptDir)) (0, import_node_fs9.mkdirSync)(scriptDir, { recursive: true });
|
|
11601
12480
|
const script = [
|
|
11602
12481
|
"#!/bin/bash",
|
|
11603
12482
|
"# AgenticMail auto-start script",
|
|
@@ -11648,7 +12527,7 @@ var ServiceManager = class {
|
|
|
11648
12527
|
`log "Starting API server: ${nodePath} ${apiEntry}"`,
|
|
11649
12528
|
`exec "${nodePath}" "${apiEntry}"`
|
|
11650
12529
|
].join("\n") + "\n";
|
|
11651
|
-
(0,
|
|
12530
|
+
(0, import_node_fs9.writeFileSync)(scriptPath, script, { mode: 493 });
|
|
11652
12531
|
return scriptPath;
|
|
11653
12532
|
}
|
|
11654
12533
|
/**
|
|
@@ -11661,9 +12540,9 @@ var ServiceManager = class {
|
|
|
11661
12540
|
* - Service version tracking in env vars
|
|
11662
12541
|
*/
|
|
11663
12542
|
generatePlist(nodePath, apiEntry, configPath) {
|
|
11664
|
-
const config = JSON.parse((0,
|
|
11665
|
-
const logDir = (0,
|
|
11666
|
-
if (!(0,
|
|
12543
|
+
const config = JSON.parse((0, import_node_fs9.readFileSync)(configPath, "utf-8"));
|
|
12544
|
+
const logDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "logs");
|
|
12545
|
+
if (!(0, import_node_fs9.existsSync)(logDir)) (0, import_node_fs9.mkdirSync)(logDir, { recursive: true });
|
|
11667
12546
|
const version = this.getVersion();
|
|
11668
12547
|
const startScript = this.generateStartScript(nodePath, apiEntry);
|
|
11669
12548
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -11684,9 +12563,9 @@ var ServiceManager = class {
|
|
|
11684
12563
|
<key>EnvironmentVariables</key>
|
|
11685
12564
|
<dict>
|
|
11686
12565
|
<key>HOME</key>
|
|
11687
|
-
<string>${(0,
|
|
12566
|
+
<string>${(0, import_node_os9.homedir)()}</string>
|
|
11688
12567
|
<key>AGENTICMAIL_DATA_DIR</key>
|
|
11689
|
-
<string>${config.dataDir || (0,
|
|
12568
|
+
<string>${config.dataDir || (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail")}</string>
|
|
11690
12569
|
<key>PATH</key>
|
|
11691
12570
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
11692
12571
|
<key>AGENTICMAIL_SERVICE_VERSION</key>
|
|
@@ -11739,8 +12618,8 @@ var ServiceManager = class {
|
|
|
11739
12618
|
* - Proper dependency ordering
|
|
11740
12619
|
*/
|
|
11741
12620
|
generateSystemdUnit(nodePath, apiEntry, configPath) {
|
|
11742
|
-
const config = JSON.parse((0,
|
|
11743
|
-
const dataDir = config.dataDir || (0,
|
|
12621
|
+
const config = JSON.parse((0, import_node_fs9.readFileSync)(configPath, "utf-8"));
|
|
12622
|
+
const dataDir = config.dataDir || (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
|
|
11744
12623
|
const version = this.getVersion();
|
|
11745
12624
|
const startScript = this.generateStartScript(nodePath, apiEntry);
|
|
11746
12625
|
return `[Unit]
|
|
@@ -11757,7 +12636,7 @@ Restart=always
|
|
|
11757
12636
|
RestartSec=15
|
|
11758
12637
|
TimeoutStartSec=660
|
|
11759
12638
|
LimitNOFILE=8192
|
|
11760
|
-
Environment=HOME=${(0,
|
|
12639
|
+
Environment=HOME=${(0, import_node_os9.homedir)()}
|
|
11761
12640
|
Environment=AGENTICMAIL_DATA_DIR=${dataDir}
|
|
11762
12641
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin
|
|
11763
12642
|
Environment=AGENTICMAIL_SERVICE_VERSION=${version}
|
|
@@ -11770,8 +12649,8 @@ WantedBy=default.target
|
|
|
11770
12649
|
* Install the auto-start service.
|
|
11771
12650
|
*/
|
|
11772
12651
|
install() {
|
|
11773
|
-
const configPath = (0,
|
|
11774
|
-
if (!(0,
|
|
12652
|
+
const configPath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "config.json");
|
|
12653
|
+
if (!(0, import_node_fs9.existsSync)(configPath)) {
|
|
11775
12654
|
return { installed: false, message: "Config not found. Run agenticmail setup first." };
|
|
11776
12655
|
}
|
|
11777
12656
|
const nodePath = this.getNodePath();
|
|
@@ -11783,17 +12662,17 @@ WantedBy=default.target
|
|
|
11783
12662
|
}
|
|
11784
12663
|
const servicePath = this.getServicePath();
|
|
11785
12664
|
if (this.os === "darwin") {
|
|
11786
|
-
const dir2 = (0,
|
|
11787
|
-
if (!(0,
|
|
11788
|
-
if ((0,
|
|
12665
|
+
const dir2 = (0, import_node_path11.join)((0, import_node_os9.homedir)(), "Library", "LaunchAgents");
|
|
12666
|
+
if (!(0, import_node_fs9.existsSync)(dir2)) (0, import_node_fs9.mkdirSync)(dir2, { recursive: true });
|
|
12667
|
+
if ((0, import_node_fs9.existsSync)(servicePath)) {
|
|
11789
12668
|
try {
|
|
11790
12669
|
(0, import_node_child_process4.execFileSync)("launchctl", ["unload", servicePath], { timeout: 1e4, stdio: "ignore" });
|
|
11791
12670
|
} catch {
|
|
11792
12671
|
}
|
|
11793
12672
|
}
|
|
11794
12673
|
const plist = this.generatePlist(nodePath, apiEntry, configPath);
|
|
11795
|
-
(0,
|
|
11796
|
-
(0,
|
|
12674
|
+
(0, import_node_fs9.writeFileSync)(servicePath, plist);
|
|
12675
|
+
(0, import_node_fs9.chmodSync)(servicePath, 384);
|
|
11797
12676
|
try {
|
|
11798
12677
|
(0, import_node_child_process4.execFileSync)("launchctl", ["load", servicePath], { timeout: 1e4, stdio: "ignore" });
|
|
11799
12678
|
} catch (err) {
|
|
@@ -11801,11 +12680,11 @@ WantedBy=default.target
|
|
|
11801
12680
|
}
|
|
11802
12681
|
return { installed: true, message: `Service installed at ${servicePath}` };
|
|
11803
12682
|
} else if (this.os === "linux") {
|
|
11804
|
-
const dir2 = (0,
|
|
11805
|
-
if (!(0,
|
|
12683
|
+
const dir2 = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".config", "systemd", "user");
|
|
12684
|
+
if (!(0, import_node_fs9.existsSync)(dir2)) (0, import_node_fs9.mkdirSync)(dir2, { recursive: true });
|
|
11806
12685
|
const unit = this.generateSystemdUnit(nodePath, apiEntry, configPath);
|
|
11807
|
-
(0,
|
|
11808
|
-
(0,
|
|
12686
|
+
(0, import_node_fs9.writeFileSync)(servicePath, unit);
|
|
12687
|
+
(0, import_node_fs9.chmodSync)(servicePath, 384);
|
|
11809
12688
|
try {
|
|
11810
12689
|
(0, import_node_child_process4.execFileSync)("systemctl", ["--user", "daemon-reload"], { timeout: 1e4, stdio: "ignore" });
|
|
11811
12690
|
(0, import_node_child_process4.execFileSync)("systemctl", ["--user", "enable", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
|
|
@@ -11827,7 +12706,7 @@ WantedBy=default.target
|
|
|
11827
12706
|
*/
|
|
11828
12707
|
uninstall() {
|
|
11829
12708
|
const servicePath = this.getServicePath();
|
|
11830
|
-
if (!(0,
|
|
12709
|
+
if (!(0, import_node_fs9.existsSync)(servicePath)) {
|
|
11831
12710
|
return { removed: false, message: "Service is not installed." };
|
|
11832
12711
|
}
|
|
11833
12712
|
if (this.os === "darwin") {
|
|
@@ -11836,7 +12715,7 @@ WantedBy=default.target
|
|
|
11836
12715
|
} catch {
|
|
11837
12716
|
}
|
|
11838
12717
|
try {
|
|
11839
|
-
(0,
|
|
12718
|
+
(0, import_node_fs9.unlinkSync)(servicePath);
|
|
11840
12719
|
} catch {
|
|
11841
12720
|
}
|
|
11842
12721
|
return { removed: true, message: "Service removed." };
|
|
@@ -11847,7 +12726,7 @@ WantedBy=default.target
|
|
|
11847
12726
|
} catch {
|
|
11848
12727
|
}
|
|
11849
12728
|
try {
|
|
11850
|
-
(0,
|
|
12729
|
+
(0, import_node_fs9.unlinkSync)(servicePath);
|
|
11851
12730
|
} catch {
|
|
11852
12731
|
}
|
|
11853
12732
|
try {
|
|
@@ -11865,7 +12744,7 @@ WantedBy=default.target
|
|
|
11865
12744
|
status() {
|
|
11866
12745
|
const servicePath = this.getServicePath();
|
|
11867
12746
|
const plat = this.os === "darwin" ? "launchd" : this.os === "linux" ? "systemd" : "unsupported";
|
|
11868
|
-
const installed = (0,
|
|
12747
|
+
const installed = (0, import_node_fs9.existsSync)(servicePath);
|
|
11869
12748
|
let running = false;
|
|
11870
12749
|
if (installed) {
|
|
11871
12750
|
if (this.os === "darwin") {
|
|
@@ -11920,34 +12799,34 @@ WantedBy=default.target
|
|
|
11920
12799
|
needsRepair() {
|
|
11921
12800
|
if (this.os !== "darwin" && this.os !== "linux") return null;
|
|
11922
12801
|
const servicePath = this.getServicePath();
|
|
11923
|
-
if (!(0,
|
|
12802
|
+
if (!(0, import_node_fs9.existsSync)(servicePath)) return null;
|
|
11924
12803
|
let serviceContent = "";
|
|
11925
12804
|
try {
|
|
11926
|
-
serviceContent = (0,
|
|
12805
|
+
serviceContent = (0, import_node_fs9.readFileSync)(servicePath, "utf-8");
|
|
11927
12806
|
} catch {
|
|
11928
12807
|
return { reason: "Service file unreadable" };
|
|
11929
12808
|
}
|
|
11930
|
-
const startScript = (0,
|
|
12809
|
+
const startScript = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin", "start-server.sh");
|
|
11931
12810
|
if (serviceContent.includes(startScript)) {
|
|
11932
|
-
if (!(0,
|
|
12811
|
+
if (!(0, import_node_fs9.existsSync)(startScript)) {
|
|
11933
12812
|
return { reason: "start-server.sh is missing" };
|
|
11934
12813
|
}
|
|
11935
12814
|
let scriptContent = "";
|
|
11936
12815
|
try {
|
|
11937
|
-
scriptContent = (0,
|
|
12816
|
+
scriptContent = (0, import_node_fs9.readFileSync)(startScript, "utf-8");
|
|
11938
12817
|
} catch {
|
|
11939
12818
|
return { reason: "start-server.sh unreadable" };
|
|
11940
12819
|
}
|
|
11941
12820
|
const apiPathMatch = scriptContent.match(/(\/[^"\s]+@agenticmail\/api\/dist\/index\.js)/);
|
|
11942
12821
|
if (apiPathMatch) {
|
|
11943
12822
|
const referenced = apiPathMatch[1];
|
|
11944
|
-
if (!(0,
|
|
12823
|
+
if (!(0, import_node_fs9.existsSync)(referenced)) {
|
|
11945
12824
|
return { reason: `start-server.sh references missing path: ${referenced}` };
|
|
11946
12825
|
}
|
|
11947
12826
|
}
|
|
11948
12827
|
if (/node_modules\/agenticmail\/(?!.*@agenticmail\/cli)/.test(scriptContent)) {
|
|
11949
12828
|
const stale = /(\S*node_modules\/agenticmail\/\S*)/.exec(scriptContent)?.[1];
|
|
11950
|
-
if (stale && !(0,
|
|
12829
|
+
if (stale && !(0, import_node_fs9.existsSync)(stale)) {
|
|
11951
12830
|
return { reason: `start-server.sh references legacy unscoped path: ${stale}` };
|
|
11952
12831
|
}
|
|
11953
12832
|
}
|
|
@@ -11960,11 +12839,11 @@ WantedBy=default.target
|
|
|
11960
12839
|
return { reason: `Service version drift (current CLI is v${currentVersion})` };
|
|
11961
12840
|
}
|
|
11962
12841
|
}
|
|
11963
|
-
const entryCache = (0,
|
|
11964
|
-
if ((0,
|
|
12842
|
+
const entryCache = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "api-entry.path");
|
|
12843
|
+
if ((0, import_node_fs9.existsSync)(entryCache)) {
|
|
11965
12844
|
try {
|
|
11966
|
-
const cached = (0,
|
|
11967
|
-
if (cached && !(0,
|
|
12845
|
+
const cached = (0, import_node_fs9.readFileSync)(entryCache, "utf-8").trim();
|
|
12846
|
+
if (cached && !(0, import_node_fs9.existsSync)(cached)) {
|
|
11968
12847
|
return { reason: `Cached API entry path no longer exists: ${cached}` };
|
|
11969
12848
|
}
|
|
11970
12849
|
} catch {
|
|
@@ -12015,13 +12894,13 @@ var SetupManager = class {
|
|
|
12015
12894
|
* falls back to monorepo location.
|
|
12016
12895
|
*/
|
|
12017
12896
|
getComposePath() {
|
|
12018
|
-
const standalonePath = (0,
|
|
12019
|
-
if ((0,
|
|
12897
|
+
const standalonePath = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail", "docker-compose.yml");
|
|
12898
|
+
if ((0, import_node_fs10.existsSync)(standalonePath)) return standalonePath;
|
|
12020
12899
|
const cwd = process.cwd();
|
|
12021
|
-
const candidates = [cwd, (0,
|
|
12900
|
+
const candidates = [cwd, (0, import_node_path12.join)(cwd, "..")];
|
|
12022
12901
|
for (const dir2 of candidates) {
|
|
12023
|
-
const p = (0,
|
|
12024
|
-
if ((0,
|
|
12902
|
+
const p = (0, import_node_path12.join)(dir2, "docker-compose.yml");
|
|
12903
|
+
if ((0, import_node_fs10.existsSync)(p)) return p;
|
|
12025
12904
|
}
|
|
12026
12905
|
return standalonePath;
|
|
12027
12906
|
}
|
|
@@ -12031,19 +12910,19 @@ var SetupManager = class {
|
|
|
12031
12910
|
* Always regenerates Docker files to keep passwords in sync.
|
|
12032
12911
|
*/
|
|
12033
12912
|
initConfig() {
|
|
12034
|
-
const dataDir = (0,
|
|
12035
|
-
const configPath = (0,
|
|
12036
|
-
const envPath = (0,
|
|
12037
|
-
if ((0,
|
|
12913
|
+
const dataDir = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail");
|
|
12914
|
+
const configPath = (0, import_node_path12.join)(dataDir, "config.json");
|
|
12915
|
+
const envPath = (0, import_node_path12.join)(dataDir, ".env");
|
|
12916
|
+
if ((0, import_node_fs10.existsSync)(configPath)) {
|
|
12038
12917
|
try {
|
|
12039
|
-
const existing = JSON.parse((0,
|
|
12918
|
+
const existing = JSON.parse((0, import_node_fs10.readFileSync)(configPath, "utf-8"));
|
|
12040
12919
|
this.generateDockerFiles(existing);
|
|
12041
12920
|
return { configPath, envPath, config: existing, isNew: false };
|
|
12042
12921
|
} catch {
|
|
12043
12922
|
}
|
|
12044
12923
|
}
|
|
12045
|
-
if (!(0,
|
|
12046
|
-
(0,
|
|
12924
|
+
if (!(0, import_node_fs10.existsSync)(dataDir)) {
|
|
12925
|
+
(0, import_node_fs10.mkdirSync)(dataDir, { recursive: true });
|
|
12047
12926
|
}
|
|
12048
12927
|
const masterKey = `mk_${(0, import_node_crypto6.randomBytes)(24).toString("hex")}`;
|
|
12049
12928
|
const stalwartPassword = (0, import_node_crypto6.randomBytes)(16).toString("hex");
|
|
@@ -12059,8 +12938,8 @@ var SetupManager = class {
|
|
|
12059
12938
|
api: { port: 3829, host: "127.0.0.1" },
|
|
12060
12939
|
dataDir
|
|
12061
12940
|
};
|
|
12062
|
-
(0,
|
|
12063
|
-
(0,
|
|
12941
|
+
(0, import_node_fs10.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
12942
|
+
(0, import_node_fs10.chmodSync)(configPath, 384);
|
|
12064
12943
|
const envContent = `# Auto-generated by agenticmail setup
|
|
12065
12944
|
STALWART_ADMIN_USER=admin
|
|
12066
12945
|
STALWART_ADMIN_PASSWORD=${stalwartPassword}
|
|
@@ -12075,8 +12954,8 @@ SMTP_PORT=587
|
|
|
12075
12954
|
IMAP_HOST=localhost
|
|
12076
12955
|
IMAP_PORT=143
|
|
12077
12956
|
`;
|
|
12078
|
-
(0,
|
|
12079
|
-
(0,
|
|
12957
|
+
(0, import_node_fs10.writeFileSync)(envPath, envContent);
|
|
12958
|
+
(0, import_node_fs10.chmodSync)(envPath, 384);
|
|
12080
12959
|
this.generateDockerFiles(config);
|
|
12081
12960
|
return { configPath, envPath, config, isNew: true };
|
|
12082
12961
|
}
|
|
@@ -12085,13 +12964,13 @@ IMAP_PORT=143
|
|
|
12085
12964
|
* with the correct admin password from config.
|
|
12086
12965
|
*/
|
|
12087
12966
|
generateDockerFiles(config) {
|
|
12088
|
-
const dataDir = config.dataDir || (0,
|
|
12089
|
-
if (!(0,
|
|
12090
|
-
(0,
|
|
12967
|
+
const dataDir = config.dataDir || (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail");
|
|
12968
|
+
if (!(0, import_node_fs10.existsSync)(dataDir)) {
|
|
12969
|
+
(0, import_node_fs10.mkdirSync)(dataDir, { recursive: true });
|
|
12091
12970
|
}
|
|
12092
12971
|
const password = config.stalwart?.adminPassword || "changeme";
|
|
12093
|
-
const composePath = (0,
|
|
12094
|
-
(0,
|
|
12972
|
+
const composePath = (0, import_node_path12.join)(dataDir, "docker-compose.yml");
|
|
12973
|
+
(0, import_node_fs10.writeFileSync)(composePath, `services:
|
|
12095
12974
|
stalwart:
|
|
12096
12975
|
# Pinned to v0.15.5 \u2014 Stalwart 0.16+ moved its config to JSON
|
|
12097
12976
|
# at /etc/stalwart/config.json (hardcoded into the container
|
|
@@ -12120,9 +12999,9 @@ IMAP_PORT=143
|
|
|
12120
12999
|
volumes:
|
|
12121
13000
|
stalwart-data:
|
|
12122
13001
|
`);
|
|
12123
|
-
(0,
|
|
12124
|
-
const tomlPath = (0,
|
|
12125
|
-
(0,
|
|
13002
|
+
(0, import_node_fs10.chmodSync)(composePath, 384);
|
|
13003
|
+
const tomlPath = (0, import_node_path12.join)(dataDir, "stalwart.toml");
|
|
13004
|
+
(0, import_node_fs10.writeFileSync)(tomlPath, `# Stalwart Mail Server \u2014 AgenticMail Configuration
|
|
12126
13005
|
|
|
12127
13006
|
[server]
|
|
12128
13007
|
hostname = "localhost"
|
|
@@ -12172,27 +13051,27 @@ enable = true
|
|
|
12172
13051
|
user = "admin"
|
|
12173
13052
|
secret = "${password}"
|
|
12174
13053
|
`);
|
|
12175
|
-
(0,
|
|
13054
|
+
(0, import_node_fs10.chmodSync)(tomlPath, 384);
|
|
12176
13055
|
}
|
|
12177
13056
|
/**
|
|
12178
13057
|
* Check if config has already been initialized.
|
|
12179
13058
|
*/
|
|
12180
13059
|
isInitialized() {
|
|
12181
|
-
const configPath = (0,
|
|
12182
|
-
return (0,
|
|
13060
|
+
const configPath = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail", "config.json");
|
|
13061
|
+
return (0, import_node_fs10.existsSync)(configPath);
|
|
12183
13062
|
}
|
|
12184
13063
|
};
|
|
12185
13064
|
|
|
12186
13065
|
// src/media/manager.ts
|
|
12187
13066
|
var import_node_child_process6 = require("child_process");
|
|
12188
13067
|
var import_node_util = require("util");
|
|
12189
|
-
var
|
|
12190
|
-
var
|
|
13068
|
+
var import_node_fs12 = require("fs");
|
|
13069
|
+
var import_node_path13 = require("path");
|
|
12191
13070
|
|
|
12192
13071
|
// src/media/binaries.ts
|
|
12193
13072
|
var import_node_child_process5 = require("child_process");
|
|
12194
|
-
var
|
|
12195
|
-
var
|
|
13073
|
+
var import_node_fs11 = require("fs");
|
|
13074
|
+
var import_meta4 = {};
|
|
12196
13075
|
var BINARY_SPECS = {
|
|
12197
13076
|
ffmpeg: {
|
|
12198
13077
|
binary: "ffmpeg",
|
|
@@ -12265,7 +13144,7 @@ function probeCommand(command, spec) {
|
|
|
12265
13144
|
}
|
|
12266
13145
|
function detectEdgeTts(spec) {
|
|
12267
13146
|
try {
|
|
12268
|
-
const resolved =
|
|
13147
|
+
const resolved = import_meta4.resolve?.("node-edge-tts");
|
|
12269
13148
|
if (resolved) {
|
|
12270
13149
|
return {
|
|
12271
13150
|
binary: "edge-tts",
|
|
@@ -12332,7 +13211,7 @@ function requireWhisperModel(modelPath) {
|
|
|
12332
13211
|
"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
13212
|
);
|
|
12334
13213
|
}
|
|
12335
|
-
if (!(0,
|
|
13214
|
+
if (!(0, import_node_fs11.existsSync)(modelPath)) {
|
|
12336
13215
|
throw new Error(`whisper model file not found: ${modelPath}`);
|
|
12337
13216
|
}
|
|
12338
13217
|
return modelPath;
|
|
@@ -12384,7 +13263,7 @@ function validateInputPath(path2, label = "input") {
|
|
|
12384
13263
|
`${label} file path may not start with "-" \u2014 pass an absolute path so it cannot be parsed as a command flag`
|
|
12385
13264
|
);
|
|
12386
13265
|
}
|
|
12387
|
-
if (!(0,
|
|
13266
|
+
if (!(0, import_node_fs12.existsSync)(path2)) {
|
|
12388
13267
|
throw new Error(`${label} file not found: ${path2}`);
|
|
12389
13268
|
}
|
|
12390
13269
|
return path2;
|
|
@@ -12406,32 +13285,32 @@ var MediaManager = class {
|
|
|
12406
13285
|
if (options.outputDir) {
|
|
12407
13286
|
this.outputDir = options.outputDir;
|
|
12408
13287
|
} else if (options.dataDir) {
|
|
12409
|
-
this.outputDir = (0,
|
|
13288
|
+
this.outputDir = (0, import_node_path13.join)(options.dataDir, "media");
|
|
12410
13289
|
} else {
|
|
12411
13290
|
const tmp = process.env.TMPDIR || process.env.TEMP || "/tmp";
|
|
12412
|
-
this.outputDir = (0,
|
|
13291
|
+
this.outputDir = (0, import_node_path13.join)(tmp, "agenticmail-media");
|
|
12413
13292
|
}
|
|
12414
13293
|
}
|
|
12415
13294
|
/** Ensure the output directory exists; returns it. */
|
|
12416
13295
|
ensureOutputDir() {
|
|
12417
|
-
if (!(0,
|
|
12418
|
-
(0,
|
|
13296
|
+
if (!(0, import_node_fs12.existsSync)(this.outputDir)) {
|
|
13297
|
+
(0, import_node_fs12.mkdirSync)(this.outputDir, { recursive: true });
|
|
12419
13298
|
}
|
|
12420
13299
|
return this.outputDir;
|
|
12421
13300
|
}
|
|
12422
13301
|
/** Build an output path inside the managed output dir. */
|
|
12423
13302
|
outPath(prefix, ext) {
|
|
12424
|
-
return (0,
|
|
13303
|
+
return (0, import_node_path13.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}.${ext}`);
|
|
12425
13304
|
}
|
|
12426
13305
|
/** Build a sub-directory inside the managed output dir. */
|
|
12427
13306
|
outDir(prefix) {
|
|
12428
|
-
const dir2 = (0,
|
|
12429
|
-
(0,
|
|
13307
|
+
const dir2 = (0, import_node_path13.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
13308
|
+
(0, import_node_fs12.mkdirSync)(dir2, { recursive: true });
|
|
12430
13309
|
return dir2;
|
|
12431
13310
|
}
|
|
12432
13311
|
/** Stat a produced file into a {@link MediaFileResult} envelope. */
|
|
12433
13312
|
fileResult(path2, extra = {}) {
|
|
12434
|
-
const stat = (0,
|
|
13313
|
+
const stat = (0, import_node_fs12.statSync)(path2);
|
|
12435
13314
|
return { ok: true, filePath: path2, sizeBytes: stat.size, ...extra };
|
|
12436
13315
|
}
|
|
12437
13316
|
// ─── binary invocation helpers (execFile, arg arrays, no shell) ────
|
|
@@ -12545,7 +13424,7 @@ var MediaManager = class {
|
|
|
12545
13424
|
/** Edit an image with ImageMagick. */
|
|
12546
13425
|
async imageEdit(opts) {
|
|
12547
13426
|
const input = validateInputPath(opts.input);
|
|
12548
|
-
const ext = safeExtension(opts.format, (0,
|
|
13427
|
+
const ext = safeExtension(opts.format, (0, import_node_path13.extname)(input).slice(1) || "png");
|
|
12549
13428
|
const out = this.outPath("img", ext);
|
|
12550
13429
|
switch (opts.action) {
|
|
12551
13430
|
case "resize": {
|
|
@@ -12632,7 +13511,7 @@ var MediaManager = class {
|
|
|
12632
13511
|
switch (opts.action) {
|
|
12633
13512
|
case "trim": {
|
|
12634
13513
|
const input = validateInputPath(opts.input);
|
|
12635
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13514
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12636
13515
|
const a = ["-i", input];
|
|
12637
13516
|
if (opts.start) a.push("-ss", String(opts.start));
|
|
12638
13517
|
if (opts.end) a.push("-to", String(opts.end));
|
|
@@ -12653,8 +13532,8 @@ var MediaManager = class {
|
|
|
12653
13532
|
if (files.length < 2) throw new Error("At least 2 files are required for merge");
|
|
12654
13533
|
files.forEach((f, i) => validateInputPath(f, `files[${i}]`));
|
|
12655
13534
|
const listFile = this.outPath("concat", "txt");
|
|
12656
|
-
(0,
|
|
12657
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13535
|
+
(0, import_node_fs12.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
|
|
13536
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(files[0]).slice(1) || "mp3"));
|
|
12658
13537
|
try {
|
|
12659
13538
|
await this.ffmpeg(["-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", "-y", out]);
|
|
12660
13539
|
} finally {
|
|
@@ -12665,7 +13544,7 @@ var MediaManager = class {
|
|
|
12665
13544
|
case "volume": {
|
|
12666
13545
|
const input = validateInputPath(opts.input);
|
|
12667
13546
|
if (!opts.volume) throw new Error('volume is required (e.g. "1.5" or "10dB")');
|
|
12668
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13547
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12669
13548
|
await this.ffmpeg(["-i", input, "-af", `volume=${opts.volume}`, "-y", out]);
|
|
12670
13549
|
return this.fileResult(out);
|
|
12671
13550
|
}
|
|
@@ -12673,7 +13552,7 @@ var MediaManager = class {
|
|
|
12673
13552
|
const input = validateInputPath(opts.input);
|
|
12674
13553
|
const factor = clampNumber(opts.speedFactor, 0.5, 100, 0);
|
|
12675
13554
|
if (!factor) throw new Error("speedFactor is required for speed");
|
|
12676
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13555
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12677
13556
|
await this.ffmpeg(["-i", input, "-af", `atempo=${factor}`, "-y", out]);
|
|
12678
13557
|
return this.fileResult(out);
|
|
12679
13558
|
}
|
|
@@ -12685,7 +13564,7 @@ var MediaManager = class {
|
|
|
12685
13564
|
}
|
|
12686
13565
|
case "reverse": {
|
|
12687
13566
|
const input = validateInputPath(opts.input);
|
|
12688
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13567
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12689
13568
|
await this.ffmpeg(["-i", input, "-af", "areverse", "-y", out]);
|
|
12690
13569
|
return this.fileResult(out);
|
|
12691
13570
|
}
|
|
@@ -12694,7 +13573,7 @@ var MediaManager = class {
|
|
|
12694
13573
|
const dur = clampNumber(opts.fadeDuration, 0.1, 3600, 3);
|
|
12695
13574
|
const probe = await this.ffprobe(input);
|
|
12696
13575
|
const totalDur = parseFloat(probe.format?.duration || "0");
|
|
12697
|
-
const out = this.outPath("aud", safeExtension(null, (0,
|
|
13576
|
+
const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
|
|
12698
13577
|
let af;
|
|
12699
13578
|
if (opts.fadeType === "in") af = `afade=t=in:st=0:d=${dur}`;
|
|
12700
13579
|
else if (opts.fadeType === "out") af = `afade=t=out:st=${Math.max(0, totalDur - dur)}:d=${dur}`;
|
|
@@ -12724,7 +13603,7 @@ var MediaManager = class {
|
|
|
12724
13603
|
}));
|
|
12725
13604
|
return {
|
|
12726
13605
|
ok: true,
|
|
12727
|
-
file: (0,
|
|
13606
|
+
file: (0, import_node_path13.basename)(path2),
|
|
12728
13607
|
format: info.format?.format_long_name,
|
|
12729
13608
|
duration: info.format?.duration,
|
|
12730
13609
|
sizeBytes: parseInt(info.format?.size || "0", 10),
|
|
@@ -12737,7 +13616,7 @@ var MediaManager = class {
|
|
|
12737
13616
|
async videoEdit(opts) {
|
|
12738
13617
|
if (opts.action === "concatenate") return this.videoConcatenate(opts);
|
|
12739
13618
|
const input = validateInputPath(opts.input);
|
|
12740
|
-
const srcExt = safeExtension(null, (0,
|
|
13619
|
+
const srcExt = safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4");
|
|
12741
13620
|
switch (opts.action) {
|
|
12742
13621
|
case "trim": {
|
|
12743
13622
|
const out = this.outPath("vid", srcExt);
|
|
@@ -12759,7 +13638,7 @@ var MediaManager = class {
|
|
|
12759
13638
|
const dir2 = this.outDir("frames");
|
|
12760
13639
|
const interval = clampNumber(opts.interval, 0.01, 3600, 1);
|
|
12761
13640
|
await this.ffmpeg(
|
|
12762
|
-
["-i", input, "-vf", `fps=1/${interval}`, (0,
|
|
13641
|
+
["-i", input, "-vf", `fps=1/${interval}`, (0, import_node_path13.join)(dir2, "frame-%04d.png"), "-y"],
|
|
12763
13642
|
TIMEOUT_LONG
|
|
12764
13643
|
);
|
|
12765
13644
|
return { ok: true, filePath: dir2, sizeBytes: 0, outputDir: dir2 };
|
|
@@ -12860,7 +13739,7 @@ var MediaManager = class {
|
|
|
12860
13739
|
}
|
|
12861
13740
|
}
|
|
12862
13741
|
async videoColorGrade(input, opts) {
|
|
12863
|
-
const out = this.outPath("vid", safeExtension(null, (0,
|
|
13742
|
+
const out = this.outPath("vid", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4"));
|
|
12864
13743
|
let vf;
|
|
12865
13744
|
if (opts.lutPath) {
|
|
12866
13745
|
const lut = validateInputPath(opts.lutPath, "lutPath");
|
|
@@ -12918,7 +13797,7 @@ var MediaManager = class {
|
|
|
12918
13797
|
}
|
|
12919
13798
|
async videoTextOverlay(input, opts) {
|
|
12920
13799
|
if (!opts.text) throw new Error("text is required for text_overlay");
|
|
12921
|
-
const out = this.outPath("vid", safeExtension(null, (0,
|
|
13800
|
+
const out = this.outPath("vid", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4"));
|
|
12922
13801
|
const probeV = await this.ffprobe(input);
|
|
12923
13802
|
const vStream = (probeV.streams || []).find((s) => s.codec_type === "video");
|
|
12924
13803
|
const vw = vStream?.width || 1920;
|
|
@@ -13187,7 +14066,7 @@ var MediaManager = class {
|
|
|
13187
14066
|
files.forEach((f, i) => validateInputPath(f, `files[${i}]`));
|
|
13188
14067
|
const out = this.outPath("vid", "mp4");
|
|
13189
14068
|
const listFile = this.outPath("concat", "txt");
|
|
13190
|
-
(0,
|
|
14069
|
+
(0, import_node_fs12.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
|
|
13191
14070
|
try {
|
|
13192
14071
|
try {
|
|
13193
14072
|
await this.ffmpeg(["-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", "-y", out], TIMEOUT_LONG);
|
|
@@ -13316,9 +14195,9 @@ var MediaManager = class {
|
|
|
13316
14195
|
const bg = bgColors[i % bgColors.length];
|
|
13317
14196
|
const sizeMult = chunk.wc <= 2 ? 1.4 : chunk.wc <= 3 ? 1.1 : 1;
|
|
13318
14197
|
const fontSize = Math.round(baseFont * sizeMult);
|
|
13319
|
-
const txtPng = (0,
|
|
13320
|
-
const bgPng = (0,
|
|
13321
|
-
const finalPng = (0,
|
|
14198
|
+
const txtPng = (0, import_node_path13.join)(captionDir, `txt-${i}.png`);
|
|
14199
|
+
const bgPng = (0, import_node_path13.join)(captionDir, `bg-${i}.png`);
|
|
14200
|
+
const finalPng = (0, import_node_path13.join)(captionDir, `c-${String(i).padStart(4, "0")}.png`);
|
|
13322
14201
|
await this.magick([
|
|
13323
14202
|
"-size",
|
|
13324
14203
|
`${maxTextW}x`,
|
|
@@ -13442,12 +14321,12 @@ var MediaManager = class {
|
|
|
13442
14321
|
for (let i = 0; i < frameCount; i++) {
|
|
13443
14322
|
const t = i * interval;
|
|
13444
14323
|
if (t >= totalDur && totalDur > 0) break;
|
|
13445
|
-
const framePath = (0,
|
|
14324
|
+
const framePath = (0, import_node_path13.join)(frameDir, `frame-${String(i).padStart(3, "0")}.jpg`);
|
|
13446
14325
|
await this.ffmpeg(["-ss", String(t), "-i", input, "-frames:v", "1", "-q:v", "3", "-y", framePath], TIMEOUT_FAST);
|
|
13447
14326
|
frames.push({ time: t, path: framePath });
|
|
13448
14327
|
}
|
|
13449
14328
|
const transcript = [];
|
|
13450
|
-
if (opts.whisperModel && (0,
|
|
14329
|
+
if (opts.whisperModel && (0, import_node_fs12.existsSync)(opts.whisperModel) && detectBinary("whisper").available) {
|
|
13451
14330
|
const whisper = requireBinary("whisper");
|
|
13452
14331
|
const wavPath = this.outPath("understand-audio", "wav");
|
|
13453
14332
|
await this.ffmpeg(["-i", input, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", "-y", wavPath], TIMEOUT_FAST);
|
|
@@ -13483,7 +14362,7 @@ var MediaManager = class {
|
|
|
13483
14362
|
});
|
|
13484
14363
|
return {
|
|
13485
14364
|
ok: true,
|
|
13486
|
-
video: (0,
|
|
14365
|
+
video: (0, import_node_path13.basename)(input),
|
|
13487
14366
|
duration: totalDur,
|
|
13488
14367
|
resolution: rotation ? `${vH}x${vW} (rotated ${rotation})` : `${vW}x${vH}`,
|
|
13489
14368
|
totalFramesExtracted: frames.length,
|
|
@@ -13511,11 +14390,11 @@ var MediaManager = class {
|
|
|
13511
14390
|
if (!opts.refText || typeof opts.refText !== "string") {
|
|
13512
14391
|
throw new Error("refText is required for voice_clone (the transcript of the reference audio)");
|
|
13513
14392
|
}
|
|
13514
|
-
const pythonBin = opts.pythonBin && (0,
|
|
14393
|
+
const pythonBin = opts.pythonBin && (0, import_node_path13.isAbsolute)(opts.pythonBin) ? validateInputPath(opts.pythonBin, "pythonBin") : requireBinary("python");
|
|
13515
14394
|
const device = typeof opts.device === "string" && /^[a-z0-9]+$/i.test(opts.device) ? opts.device : "cpu";
|
|
13516
14395
|
const outWav = this.outPath("voiceclone", "wav");
|
|
13517
14396
|
const paramsFile = this.outPath("voiceclone-params", "json");
|
|
13518
|
-
(0,
|
|
14397
|
+
(0, import_node_fs12.writeFileSync)(paramsFile, JSON.stringify({
|
|
13519
14398
|
ref_file: refAudio,
|
|
13520
14399
|
ref_text: opts.refText,
|
|
13521
14400
|
gen_text: opts.text,
|
|
@@ -13556,7 +14435,7 @@ var MediaManager = class {
|
|
|
13556
14435
|
outOgg,
|
|
13557
14436
|
"-y"
|
|
13558
14437
|
]);
|
|
13559
|
-
if ((0,
|
|
14438
|
+
if ((0, import_node_fs12.existsSync)(outOgg)) return this.fileResult(outOgg, { format: "ogg" });
|
|
13560
14439
|
} catch {
|
|
13561
14440
|
}
|
|
13562
14441
|
}
|
|
@@ -13566,18 +14445,18 @@ var MediaManager = class {
|
|
|
13566
14445
|
/** Parse a whisper-produced SRT (located by stem) into timed segments. */
|
|
13567
14446
|
parseSrt(srtStem) {
|
|
13568
14447
|
let srtFile = `${srtStem}.srt`;
|
|
13569
|
-
if (!(0,
|
|
13570
|
-
const dir2 = (0,
|
|
13571
|
-
const stem2 = (0,
|
|
14448
|
+
if (!(0, import_node_fs12.existsSync)(srtFile)) {
|
|
14449
|
+
const dir2 = (0, import_node_path13.dirname)(srtStem);
|
|
14450
|
+
const stem2 = (0, import_node_path13.basename)(srtStem);
|
|
13572
14451
|
try {
|
|
13573
|
-
const candidates = (0,
|
|
13574
|
-
if (candidates.length > 0) srtFile = (0,
|
|
14452
|
+
const candidates = (0, import_node_fs12.readdirSync)(dir2).filter((f) => f.includes(stem2) && f.endsWith(".srt"));
|
|
14453
|
+
if (candidates.length > 0) srtFile = (0, import_node_path13.join)(dir2, candidates[0]);
|
|
13575
14454
|
} catch {
|
|
13576
14455
|
}
|
|
13577
14456
|
}
|
|
13578
|
-
if (!(0,
|
|
14457
|
+
if (!(0, import_node_fs12.existsSync)(srtFile)) return [];
|
|
13579
14458
|
const out = [];
|
|
13580
|
-
const content = (0,
|
|
14459
|
+
const content = (0, import_node_fs12.readFileSync)(srtFile, "utf8");
|
|
13581
14460
|
for (const block of content.trim().split(/\n\n+/)) {
|
|
13582
14461
|
const lines = block.trim().split("\n");
|
|
13583
14462
|
if (lines.length < 3) continue;
|
|
@@ -13593,7 +14472,7 @@ var MediaManager = class {
|
|
|
13593
14472
|
/** Unlink a file, swallowing any error (cleanup best-effort). */
|
|
13594
14473
|
tryUnlink(path2) {
|
|
13595
14474
|
try {
|
|
13596
|
-
(0,
|
|
14475
|
+
(0, import_node_fs12.unlinkSync)(path2);
|
|
13597
14476
|
} catch {
|
|
13598
14477
|
}
|
|
13599
14478
|
}
|
|
@@ -13601,10 +14480,10 @@ var MediaManager = class {
|
|
|
13601
14480
|
tryUnlinkSrt(srtStem) {
|
|
13602
14481
|
this.tryUnlink(`${srtStem}.srt`);
|
|
13603
14482
|
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,
|
|
14483
|
+
const dir2 = (0, import_node_path13.dirname)(srtStem);
|
|
14484
|
+
const stem2 = (0, import_node_path13.basename)(srtStem);
|
|
14485
|
+
for (const f of (0, import_node_fs12.readdirSync)(dir2)) {
|
|
14486
|
+
if (f.includes(stem2) && f.endsWith(".srt")) this.tryUnlink((0, import_node_path13.join)(dir2, f));
|
|
13608
14487
|
}
|
|
13609
14488
|
} catch {
|
|
13610
14489
|
}
|
|
@@ -13612,7 +14491,7 @@ var MediaManager = class {
|
|
|
13612
14491
|
/** Recursively remove a directory, swallowing errors. */
|
|
13613
14492
|
tryRmDir(dir2) {
|
|
13614
14493
|
try {
|
|
13615
|
-
(0,
|
|
14494
|
+
(0, import_node_fs12.rmSync)(dir2, { recursive: true, force: true });
|
|
13616
14495
|
} catch {
|
|
13617
14496
|
}
|
|
13618
14497
|
}
|
|
@@ -13652,10 +14531,10 @@ function threadIdFor(input) {
|
|
|
13652
14531
|
}
|
|
13653
14532
|
|
|
13654
14533
|
// src/threading/thread-cache.ts
|
|
13655
|
-
var
|
|
13656
|
-
var
|
|
13657
|
-
var
|
|
13658
|
-
var CACHE_DIR_DEFAULT = (0,
|
|
14534
|
+
var import_node_fs13 = require("fs");
|
|
14535
|
+
var import_node_os11 = require("os");
|
|
14536
|
+
var import_node_path14 = require("path");
|
|
14537
|
+
var CACHE_DIR_DEFAULT = (0, import_node_path14.join)((0, import_node_os11.homedir)(), ".agenticmail", "thread-cache");
|
|
13659
14538
|
var DEFAULT_K_MESSAGES = 10;
|
|
13660
14539
|
var DEFAULT_LRU_CAP = 5e3;
|
|
13661
14540
|
var PREVIEW_MAX_CHARS = 240;
|
|
@@ -13668,22 +14547,22 @@ var ThreadCache = class {
|
|
|
13668
14547
|
this.k = opts.k ?? DEFAULT_K_MESSAGES;
|
|
13669
14548
|
this.lruCap = opts.lruCap ?? DEFAULT_LRU_CAP;
|
|
13670
14549
|
try {
|
|
13671
|
-
(0,
|
|
14550
|
+
(0, import_node_fs13.mkdirSync)(this.dir, { recursive: true });
|
|
13672
14551
|
} catch {
|
|
13673
14552
|
}
|
|
13674
14553
|
}
|
|
13675
14554
|
pathFor(threadId) {
|
|
13676
|
-
return (0,
|
|
14555
|
+
return (0, import_node_path14.join)(this.dir, `${threadId}.json`);
|
|
13677
14556
|
}
|
|
13678
14557
|
read(threadId) {
|
|
13679
14558
|
const p = this.pathFor(threadId);
|
|
13680
|
-
if (!(0,
|
|
14559
|
+
if (!(0, import_node_fs13.existsSync)(p)) return null;
|
|
13681
14560
|
try {
|
|
13682
|
-
const raw = (0,
|
|
14561
|
+
const raw = (0, import_node_fs13.readFileSync)(p, "utf-8");
|
|
13683
14562
|
return JSON.parse(raw);
|
|
13684
14563
|
} catch {
|
|
13685
14564
|
try {
|
|
13686
|
-
(0,
|
|
14565
|
+
(0, import_node_fs13.rmSync)(p, { force: true });
|
|
13687
14566
|
} catch {
|
|
13688
14567
|
}
|
|
13689
14568
|
return null;
|
|
@@ -13724,7 +14603,7 @@ var ThreadCache = class {
|
|
|
13724
14603
|
/** Permanently remove a thread's cache (called on [FINAL] / [DONE] / [CLOSED] / [WRAP]). */
|
|
13725
14604
|
delete(threadId) {
|
|
13726
14605
|
try {
|
|
13727
|
-
(0,
|
|
14606
|
+
(0, import_node_fs13.rmSync)(this.pathFor(threadId), { force: true });
|
|
13728
14607
|
} catch {
|
|
13729
14608
|
}
|
|
13730
14609
|
}
|
|
@@ -13744,8 +14623,8 @@ var ThreadCache = class {
|
|
|
13744
14623
|
writeAtomic(threadId, entry) {
|
|
13745
14624
|
const p = this.pathFor(threadId);
|
|
13746
14625
|
const tmp = `${p}.tmp`;
|
|
13747
|
-
(0,
|
|
13748
|
-
(0,
|
|
14626
|
+
(0, import_node_fs13.writeFileSync)(tmp, JSON.stringify(entry), "utf-8");
|
|
14627
|
+
(0, import_node_fs13.renameSync)(tmp, p);
|
|
13749
14628
|
}
|
|
13750
14629
|
/**
|
|
13751
14630
|
* Best-effort LRU eviction. Runs at most every 256 writes (we
|
|
@@ -13757,15 +14636,15 @@ var ThreadCache = class {
|
|
|
13757
14636
|
if (Math.random() > 1 / 256) return;
|
|
13758
14637
|
let files;
|
|
13759
14638
|
try {
|
|
13760
|
-
files = (0,
|
|
14639
|
+
files = (0, import_node_fs13.readdirSync)(this.dir).filter((f) => f.endsWith(".json"));
|
|
13761
14640
|
} catch {
|
|
13762
14641
|
return;
|
|
13763
14642
|
}
|
|
13764
14643
|
if (files.length <= this.lruCap) return;
|
|
13765
14644
|
const stats = files.map((f) => {
|
|
13766
|
-
const p = (0,
|
|
14645
|
+
const p = (0, import_node_path14.join)(this.dir, f);
|
|
13767
14646
|
try {
|
|
13768
|
-
return { p, mtime: (0,
|
|
14647
|
+
return { p, mtime: (0, import_node_fs13.statSync)(p).mtimeMs };
|
|
13769
14648
|
} catch {
|
|
13770
14649
|
return { p, mtime: 0 };
|
|
13771
14650
|
}
|
|
@@ -13774,7 +14653,7 @@ var ThreadCache = class {
|
|
|
13774
14653
|
const dropCount = Math.max(1, Math.floor(this.lruCap * 0.1));
|
|
13775
14654
|
for (let i = 0; i < dropCount; i++) {
|
|
13776
14655
|
try {
|
|
13777
|
-
(0,
|
|
14656
|
+
(0, import_node_fs13.rmSync)(stats[i].p, { force: true });
|
|
13778
14657
|
} catch {
|
|
13779
14658
|
}
|
|
13780
14659
|
}
|
|
@@ -13793,30 +14672,30 @@ function dedupAndCap(messages, k) {
|
|
|
13793
14672
|
}
|
|
13794
14673
|
|
|
13795
14674
|
// src/threading/agent-memory.ts
|
|
13796
|
-
var
|
|
13797
|
-
var
|
|
13798
|
-
var
|
|
13799
|
-
var MEMORY_DIR_DEFAULT = (0,
|
|
14675
|
+
var import_node_fs14 = require("fs");
|
|
14676
|
+
var import_node_os12 = require("os");
|
|
14677
|
+
var import_node_path15 = require("path");
|
|
14678
|
+
var MEMORY_DIR_DEFAULT = (0, import_node_path15.join)((0, import_node_os12.homedir)(), ".agenticmail", "agent-memory");
|
|
13800
14679
|
var AgentMemoryStore = class {
|
|
13801
14680
|
dir;
|
|
13802
14681
|
constructor(opts = {}) {
|
|
13803
14682
|
this.dir = opts.memoryDir ?? MEMORY_DIR_DEFAULT;
|
|
13804
14683
|
try {
|
|
13805
|
-
(0,
|
|
14684
|
+
(0, import_node_fs14.mkdirSync)(this.dir, { recursive: true });
|
|
13806
14685
|
} catch {
|
|
13807
14686
|
}
|
|
13808
14687
|
}
|
|
13809
14688
|
dirFor(agentId) {
|
|
13810
|
-
return (0,
|
|
14689
|
+
return (0, import_node_path15.join)(this.dir, sanitizeId(agentId));
|
|
13811
14690
|
}
|
|
13812
14691
|
pathFor(agentId, threadId) {
|
|
13813
|
-
return (0,
|
|
14692
|
+
return (0, import_node_path15.join)(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
|
|
13814
14693
|
}
|
|
13815
14694
|
read(agentId, threadId) {
|
|
13816
14695
|
const p = this.pathFor(agentId, threadId);
|
|
13817
|
-
if (!(0,
|
|
14696
|
+
if (!(0, import_node_fs14.existsSync)(p)) return null;
|
|
13818
14697
|
try {
|
|
13819
|
-
const raw = (0,
|
|
14698
|
+
const raw = (0, import_node_fs14.readFileSync)(p, "utf-8");
|
|
13820
14699
|
const parsed = parse(raw);
|
|
13821
14700
|
return { ...parsed, raw };
|
|
13822
14701
|
} catch {
|
|
@@ -13826,18 +14705,18 @@ var AgentMemoryStore = class {
|
|
|
13826
14705
|
write(agentId, threadId, fields) {
|
|
13827
14706
|
const agentDir = this.dirFor(agentId);
|
|
13828
14707
|
try {
|
|
13829
|
-
(0,
|
|
14708
|
+
(0, import_node_fs14.mkdirSync)(agentDir, { recursive: true });
|
|
13830
14709
|
} catch {
|
|
13831
14710
|
}
|
|
13832
14711
|
const body = render({ ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13833
14712
|
const p = this.pathFor(agentId, threadId);
|
|
13834
14713
|
const tmp = `${p}.tmp`;
|
|
13835
|
-
(0,
|
|
13836
|
-
(0,
|
|
14714
|
+
(0, import_node_fs14.writeFileSync)(tmp, body, "utf-8");
|
|
14715
|
+
(0, import_node_fs14.renameSync)(tmp, p);
|
|
13837
14716
|
}
|
|
13838
14717
|
delete(agentId, threadId) {
|
|
13839
14718
|
try {
|
|
13840
|
-
(0,
|
|
14719
|
+
(0, import_node_fs14.rmSync)(this.pathFor(agentId, threadId), { force: true });
|
|
13841
14720
|
} catch {
|
|
13842
14721
|
}
|
|
13843
14722
|
}
|
|
@@ -13880,415 +14759,22 @@ function parse(raw) {
|
|
|
13880
14759
|
const out = {};
|
|
13881
14760
|
const m = raw.match(/^---\n([\s\S]*?)\n---\n/);
|
|
13882
14761
|
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
|
-
}
|
|
14762
|
+
for (const line of m[1].split("\n")) {
|
|
14763
|
+
const kv = line.match(/^(\w+):\s*(.*)$/);
|
|
14764
|
+
if (!kv) continue;
|
|
14765
|
+
if (kv[1] === "updated_at") out.updatedAt = kv[2].trim();
|
|
14766
|
+
else if (kv[1] === "last_uid") {
|
|
14767
|
+
const n = parseInt(kv[2], 10);
|
|
14768
|
+
if (!isNaN(n)) out.lastUid = n;
|
|
14264
14769
|
}
|
|
14265
14770
|
}
|
|
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
14771
|
}
|
|
14289
|
-
|
|
14772
|
+
return out;
|
|
14773
|
+
}
|
|
14290
14774
|
|
|
14291
14775
|
// src/memory/manager.ts
|
|
14776
|
+
var import_node_crypto8 = require("crypto");
|
|
14777
|
+
init_text_search();
|
|
14292
14778
|
function sj(v, fb = {}) {
|
|
14293
14779
|
if (!v) return fb;
|
|
14294
14780
|
try {
|
|
@@ -14772,284 +15258,11 @@ var AgentMemoryManager = class {
|
|
|
14772
15258
|
}
|
|
14773
15259
|
};
|
|
14774
15260
|
|
|
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
|
-
}
|
|
15261
|
+
// src/memory/index.ts
|
|
15262
|
+
init_text_search();
|
|
14993
15263
|
|
|
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
|
-
}
|
|
15264
|
+
// src/index.ts
|
|
15265
|
+
init_skills();
|
|
15053
15266
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15054
15267
|
0 && (module.exports = {
|
|
15055
15268
|
AGENT_ROLES,
|
|
@@ -15080,6 +15293,7 @@ function renderSkillAsPrompt(skill) {
|
|
|
15080
15293
|
GET_DATETIME_TOOL,
|
|
15081
15294
|
GatewayManager,
|
|
15082
15295
|
InboxWatcher,
|
|
15296
|
+
LOAD_SKILL_TOOL,
|
|
15083
15297
|
MEMORY_CATEGORIES,
|
|
15084
15298
|
MailReceiver,
|
|
15085
15299
|
MailSender,
|
|
@@ -15116,6 +15330,7 @@ function renderSkillAsPrompt(skill) {
|
|
|
15116
15330
|
RelayBridge,
|
|
15117
15331
|
RelayGateway,
|
|
15118
15332
|
SEARCH_EMAIL_TOOL,
|
|
15333
|
+
SEARCH_SKILLS_TOOL,
|
|
15119
15334
|
SPAM_THRESHOLD,
|
|
15120
15335
|
ServiceManager,
|
|
15121
15336
|
SetupManager,
|