@agenticmail/core 0.9.23 → 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/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,
@@ -851,6 +1593,7 @@ __export(index_exports, {
851
1593
  getTelegramWebhookInfo: () => getTelegramWebhookInfo,
852
1594
  hostSessionStoragePath: () => hostSessionStoragePath,
853
1595
  inferPhoneRegion: () => inferPhoneRegion,
1596
+ invalidateSkillCache: () => invalidateSkillCache,
854
1597
  isInternalEmail: () => isInternalEmail,
855
1598
  isLoopbackMailHost: () => isLoopbackMailHost,
856
1599
  isOperatorReplySender: () => isOperatorReplySender,
@@ -859,7 +1602,9 @@ __export(index_exports, {
859
1602
  isTelegramChatAllowed: () => isTelegramChatAllowed,
860
1603
  isTelegramStopCommand: () => isTelegramStopCommand,
861
1604
  isValidPhoneNumber: () => isValidPhoneNumber,
1605
+ listSkills: () => listSkills,
862
1606
  loadHostSession: () => loadHostSession,
1607
+ loadSkill: () => loadSkill,
863
1608
  mapProviderSmsStatus: () => mapProviderSmsStatus,
864
1609
  nextTelegramOffset: () => nextTelegramOffset,
865
1610
  normalizeAddress: () => normalizeAddress,
@@ -884,6 +1629,7 @@ __export(index_exports, {
884
1629
  redactSecret: () => redactSecret,
885
1630
  redactSmsConfig: () => redactSmsConfig,
886
1631
  redactTelegramConfig: () => redactTelegramConfig,
1632
+ renderSkillAsPrompt: () => renderSkillAsPrompt,
887
1633
  requireBinary: () => requireBinary,
888
1634
  requireWhisperModel: () => requireWhisperModel,
889
1635
  resolveConfig: () => resolveConfig,
@@ -892,8 +1638,10 @@ __export(index_exports, {
892
1638
  sanitizeEmail: () => sanitizeEmail,
893
1639
  saveConfig: () => saveConfig,
894
1640
  saveHostSession: () => saveHostSession,
1641
+ saveUserSkill: () => saveUserSkill,
895
1642
  scanOutboundEmail: () => scanOutboundEmail,
896
1643
  scoreEmail: () => scoreEmail,
1644
+ searchSkills: () => searchSkills,
897
1645
  sendTelegramMessage: () => sendTelegramMessage,
898
1646
  setOperatorEmail: () => setOperatorEmail,
899
1647
  setTelegramWebhook: () => setTelegramWebhook,
@@ -906,10 +1654,12 @@ __export(index_exports, {
906
1654
  threadIdFor: () => threadIdFor,
907
1655
  tokenize: () => tokenize,
908
1656
  tryJoin: () => tryJoin,
1657
+ userSkillsDir: () => userSkillsDir,
909
1658
  validateApiUrl: () => validateApiUrl,
910
1659
  validatePhoneMissionPolicy: () => validatePhoneMissionPolicy,
911
1660
  validatePhoneMissionStart: () => validatePhoneMissionStart,
912
1661
  validatePhoneTransportProfile: () => validatePhoneTransportProfile,
1662
+ validateSkill: () => validateSkill,
913
1663
  validateTwilioSignature: () => validateTwilioSignature,
914
1664
  webSearch: () => webSearch
915
1665
  });
@@ -2016,14 +2766,14 @@ var StalwartAdmin = class {
2016
2766
  if (!isValidDomain(domain)) {
2017
2767
  throw new Error(`Invalid domain format: "${domain}"`);
2018
2768
  }
2019
- const { readFileSync: readFileSync10, writeFileSync: writeFileSync11 } = await import("fs");
2020
- const { homedir: homedir13 } = await import("os");
2021
- const { join: join16 } = await import("path");
2022
- const configPath = join16(homedir13(), ".agenticmail", "stalwart.toml");
2769
+ const { readFileSync: readFileSync11, writeFileSync: writeFileSync12 } = await import("fs");
2770
+ const { homedir: homedir14 } = await import("os");
2771
+ const { join: join17 } = await import("path");
2772
+ const configPath = join17(homedir14(), ".agenticmail", "stalwart.toml");
2023
2773
  try {
2024
- let config = readFileSync10(configPath, "utf-8");
2774
+ let config = readFileSync11(configPath, "utf-8");
2025
2775
  config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
2026
- writeFileSync11(configPath, config);
2776
+ writeFileSync12(configPath, config);
2027
2777
  console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
2028
2778
  } catch (err) {
2029
2779
  throw new Error(`Failed to set config server.hostname=${domain}`);
@@ -2032,15 +2782,15 @@ var StalwartAdmin = class {
2032
2782
  // --- DKIM ---
2033
2783
  /** Path to the host-side stalwart.toml (mounted read-only into container) */
2034
2784
  get configPath() {
2035
- const { homedir: homedir13 } = require("os");
2036
- const { join: join16 } = require("path");
2037
- return join16(homedir13(), ".agenticmail", "stalwart.toml");
2785
+ const { homedir: homedir14 } = require("os");
2786
+ const { join: join17 } = require("path");
2787
+ return join17(homedir14(), ".agenticmail", "stalwart.toml");
2038
2788
  }
2039
2789
  /** Path to host-side DKIM key directory */
2040
2790
  get dkimDir() {
2041
- const { homedir: homedir13 } = require("os");
2042
- const { join: join16 } = require("path");
2043
- return join16(homedir13(), ".agenticmail");
2791
+ const { homedir: homedir14 } = require("os");
2792
+ const { join: join17 } = require("path");
2793
+ return join17(homedir14(), ".agenticmail");
2044
2794
  }
2045
2795
  /**
2046
2796
  * Create/reuse a DKIM signing key for a domain.
@@ -2141,12 +2891,12 @@ var StalwartAdmin = class {
2141
2891
  * This bypasses the need for a PTR record on the sending IP.
2142
2892
  */
2143
2893
  async configureOutboundRelay(config) {
2144
- const { readFileSync: readFileSync10, writeFileSync: writeFileSync11 } = await import("fs");
2145
- const { homedir: homedir13 } = await import("os");
2146
- const { join: join16 } = await import("path");
2894
+ const { readFileSync: readFileSync11, writeFileSync: writeFileSync12 } = await import("fs");
2895
+ const { homedir: homedir14 } = await import("os");
2896
+ const { join: join17 } = await import("path");
2147
2897
  const routeName = config.routeName ?? "gmail";
2148
- const tomlPath = join16(homedir13(), ".agenticmail", "stalwart.toml");
2149
- let toml = readFileSync10(tomlPath, "utf-8");
2898
+ const tomlPath = join17(homedir14(), ".agenticmail", "stalwart.toml");
2899
+ let toml = readFileSync11(tomlPath, "utf-8");
2150
2900
  toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
2151
2901
  toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
2152
2902
  const safeRouteName = routeName.replace(/[^a-zA-Z0-9_-]/g, "");
@@ -2166,7 +2916,7 @@ auth.secret = "${escapeTomlString(config.password)}"
2166
2916
  route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
2167
2917
  { else = "'${safeRouteName}'" } ]
2168
2918
  `;
2169
- writeFileSync11(tomlPath, toml, "utf-8");
2919
+ writeFileSync12(tomlPath, toml, "utf-8");
2170
2920
  await this.restartContainer();
2171
2921
  }
2172
2922
  };
@@ -8372,12 +9122,12 @@ var GatewayManager = class {
8372
9122
  zone = await this.cfClient.createZone(domain);
8373
9123
  }
8374
9124
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
8375
- const { homedir: homedir13 } = await import("os");
8376
- const backupDir = (0, import_node_path4.join)(homedir13(), ".agenticmail");
9125
+ const { homedir: homedir14 } = await import("os");
9126
+ const backupDir = (0, import_node_path4.join)(homedir14(), ".agenticmail");
8377
9127
  const backupPath = (0, import_node_path4.join)(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
8378
- const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync12 } = await import("fs");
8379
- mkdirSync12(backupDir, { recursive: true });
8380
- writeFileSync11(backupPath, JSON.stringify({
9128
+ const { writeFileSync: writeFileSync12, mkdirSync: mkdirSync13 } = await import("fs");
9129
+ mkdirSync13(backupDir, { recursive: true });
9130
+ writeFileSync12(backupPath, JSON.stringify({
8381
9131
  domain,
8382
9132
  zoneId: zone.id,
8383
9133
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9661,12 +10411,46 @@ var SEARCH_EMAIL_TOOL = {
9661
10411
  additionalProperties: false
9662
10412
  }
9663
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
+ };
9664
10446
  var REALTIME_TOOL_DEFINITIONS = {
9665
10447
  ask_operator: ASK_OPERATOR_TOOL,
9666
10448
  web_search: WEB_SEARCH_TOOL,
9667
10449
  recall_memory: RECALL_MEMORY_TOOL,
9668
10450
  get_datetime: GET_DATETIME_TOOL,
9669
- search_email: SEARCH_EMAIL_TOOL
10451
+ search_email: SEARCH_EMAIL_TOOL,
10452
+ search_skills: SEARCH_SKILLS_TOOL,
10453
+ load_skill: LOAD_SKILL_TOOL
9670
10454
  };
9671
10455
  function buildRealtimeToolGuidance(tools) {
9672
10456
  if (tools.length === 0) return "";
@@ -9685,6 +10469,16 @@ function buildRealtimeToolGuidance(tools) {
9685
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.'
9686
10470
  );
9687
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
+ }
9688
10482
  return lines.join("\n");
9689
10483
  }
9690
10484
  function toolErrorText(err) {
@@ -9870,6 +10664,7 @@ var REALTIME_AUDIO_SAMPLE_RATE = 24e3;
9870
10664
  var REALTIME_MAX_AUDIO_FRAME_BASE64 = 256 * 1024;
9871
10665
  var MAX_PENDING_AUDIO_FRAMES = 200;
9872
10666
  var REALTIME_TOOL_CALL_TIMEOUT_MS = 6 * 6e4;
10667
+ var MAX_LOADED_SKILLS = 2;
9873
10668
  var MAX_IN_FLIGHT_TOOL_CALLS = 8;
9874
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.";
9875
10670
  function buildRealtimeInstructions(opts) {
@@ -9961,6 +10756,26 @@ var RealtimeVoiceBridge = class {
9961
10756
  toolCallNames = /* @__PURE__ */ new Map();
9962
10757
  /** `call_id`s whose tool call is currently executing. */
9963
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 = "";
9964
10779
  constructor(opts) {
9965
10780
  const carrier = opts.carrier ?? opts.elks;
9966
10781
  if (!carrier) {
@@ -9997,6 +10812,10 @@ var RealtimeVoiceBridge = class {
9997
10812
  handleOpenAIOpen() {
9998
10813
  if (this.ended || this.openaiReady) return;
9999
10814
  this.openaiReady = true;
10815
+ const sess = this.sessionConfig?.session;
10816
+ if (sess && typeof sess.instructions === "string") {
10817
+ this.baseInstructions = sess.instructions;
10818
+ }
10000
10819
  this.safeSend(this.openai, this.sessionConfig);
10001
10820
  this.safeSend(this.openai, { type: "response.create" });
10002
10821
  for (const audio of this.pendingAudio.splice(0)) {
@@ -10007,6 +10826,74 @@ var RealtimeVoiceBridge = class {
10007
10826
  handleOpenAIClose() {
10008
10827
  this.end("openai-closed");
10009
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
+ }
10010
10897
  /** Call when the OpenAI socket errors. */
10011
10898
  handleOpenAIError(err) {
10012
10899
  this.emitTranscript("system", `OpenAI Realtime error: ${errorText(err)}`);
@@ -10467,7 +11354,7 @@ try {
10467
11354
  }
10468
11355
 
10469
11356
  // src/util/safe-path.ts
10470
- var import_node_path5 = require("path");
11357
+ var import_node_path6 = require("path");
10471
11358
  var PathTraversalError = class extends Error {
10472
11359
  constructor(baseDir, parts) {
10473
11360
  super(
@@ -10490,14 +11377,14 @@ function safeJoin(baseDir, ...partsAndOpts) {
10490
11377
  }
10491
11378
  if (!opts.allowAbsolute) {
10492
11379
  for (const part of parts) {
10493
- if ((0, import_node_path5.isAbsolute)(part)) {
11380
+ if ((0, import_node_path6.isAbsolute)(part)) {
10494
11381
  throw new PathTraversalError(baseDir, parts);
10495
11382
  }
10496
11383
  }
10497
11384
  }
10498
- const resolvedBase = (0, import_node_path5.resolve)(baseDir);
10499
- const resolved = (0, import_node_path5.resolve)(resolvedBase, ...parts);
10500
- if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + import_node_path5.sep)) {
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)) {
10501
11388
  throw new PathTraversalError(baseDir, parts);
10502
11389
  }
10503
11390
  return resolved;
@@ -10511,9 +11398,9 @@ function tryJoin(baseDir, ...parts) {
10511
11398
  }
10512
11399
  }
10513
11400
  function assertWithinBase(baseDir, candidate) {
10514
- const resolvedBase = (0, import_node_path5.resolve)(baseDir);
10515
- const resolvedCandidate = (0, import_node_path5.resolve)(candidate);
10516
- if (resolvedCandidate !== resolvedBase && !resolvedCandidate.startsWith(resolvedBase + import_node_path5.sep)) {
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)) {
10517
11404
  throw new PathTraversalError(baseDir, [candidate]);
10518
11405
  }
10519
11406
  return resolvedCandidate;
@@ -10566,19 +11453,19 @@ function redactObject(input, _depth = 0) {
10566
11453
  }
10567
11454
 
10568
11455
  // src/operator-prefs.ts
10569
- var import_node_fs4 = require("fs");
10570
- var import_node_os4 = require("os");
10571
- var import_node_path6 = require("path");
11456
+ var import_node_fs5 = require("fs");
11457
+ var import_node_os5 = require("os");
11458
+ var import_node_path7 = require("path");
10572
11459
  function dir() {
10573
- return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".agenticmail");
11460
+ return (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".agenticmail");
10574
11461
  }
10575
11462
  function path() {
10576
- return (0, import_node_path6.join)(dir(), "operator-prefs.json");
11463
+ return (0, import_node_path7.join)(dir(), "operator-prefs.json");
10577
11464
  }
10578
11465
  function readFile() {
10579
- if (!(0, import_node_fs4.existsSync)(path())) return { version: 1 };
11466
+ if (!(0, import_node_fs5.existsSync)(path())) return { version: 1 };
10580
11467
  try {
10581
- const raw = (0, import_node_fs4.readFileSync)(path(), "utf-8");
11468
+ const raw = (0, import_node_fs5.readFileSync)(path(), "utf-8");
10582
11469
  if (!raw.trim()) return { version: 1 };
10583
11470
  const parsed = JSON.parse(raw);
10584
11471
  return { version: 1, operatorEmail: typeof parsed.operatorEmail === "string" ? parsed.operatorEmail : void 0 };
@@ -10589,10 +11476,10 @@ function readFile() {
10589
11476
  function writeFile(shape) {
10590
11477
  const d = dir();
10591
11478
  const p = path();
10592
- if (!(0, import_node_fs4.existsSync)(d)) (0, import_node_fs4.mkdirSync)(d, { recursive: true });
11479
+ if (!(0, import_node_fs5.existsSync)(d)) (0, import_node_fs5.mkdirSync)(d, { recursive: true });
10593
11480
  const tmp = `${p}.agenticmail-tmp-${process.pid}`;
10594
- (0, import_node_fs4.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
10595
- (0, import_node_fs4.renameSync)(tmp, p);
11481
+ (0, import_node_fs5.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
11482
+ (0, import_node_fs5.renameSync)(tmp, p);
10596
11483
  }
10597
11484
  function getOperatorEmail() {
10598
11485
  const shape = readFile();
@@ -10621,21 +11508,21 @@ function operatorPrefsStoragePath() {
10621
11508
  }
10622
11509
 
10623
11510
  // src/host-sessions.ts
10624
- var import_node_fs5 = require("fs");
10625
- var import_node_path7 = require("path");
10626
- var import_node_os5 = require("os");
11511
+ var import_node_fs6 = require("fs");
11512
+ var import_node_path8 = require("path");
11513
+ var import_node_os6 = require("os");
10627
11514
  function storageDir() {
10628
- return (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".agenticmail");
11515
+ return (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".agenticmail");
10629
11516
  }
10630
11517
  function storagePath() {
10631
- return (0, import_node_path7.join)(storageDir(), "host-sessions.json");
11518
+ return (0, import_node_path8.join)(storageDir(), "host-sessions.json");
10632
11519
  }
10633
11520
  var DEFAULT_SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
10634
11521
  function readFile2() {
10635
11522
  const p = storagePath();
10636
- if (!(0, import_node_fs5.existsSync)(p)) return { version: 1, sessions: {} };
11523
+ if (!(0, import_node_fs6.existsSync)(p)) return { version: 1, sessions: {} };
10637
11524
  try {
10638
- const raw = (0, import_node_fs5.readFileSync)(p, "utf-8");
11525
+ const raw = (0, import_node_fs6.readFileSync)(p, "utf-8");
10639
11526
  if (!raw.trim()) return { version: 1, sessions: {} };
10640
11527
  const parsed = JSON.parse(raw);
10641
11528
  return {
@@ -10649,10 +11536,10 @@ function readFile2() {
10649
11536
  function writeFile2(shape) {
10650
11537
  const dir2 = storageDir();
10651
11538
  const p = storagePath();
10652
- if (!(0, import_node_fs5.existsSync)(dir2)) (0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
11539
+ if (!(0, import_node_fs6.existsSync)(dir2)) (0, import_node_fs6.mkdirSync)(dir2, { recursive: true });
10653
11540
  const tmp = `${p}.agenticmail-tmp-${process.pid}`;
10654
- (0, import_node_fs5.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
10655
- (0, import_node_fs5.renameSync)(tmp, p);
11541
+ (0, import_node_fs6.writeFileSync)(tmp, JSON.stringify(shape, null, 2), "utf-8");
11542
+ (0, import_node_fs6.renameSync)(tmp, p);
10656
11543
  }
10657
11544
  function saveHostSession(host, session) {
10658
11545
  if (!session.sessionId) return;
@@ -10827,15 +11714,15 @@ function buildApiUrl(baseOrigin, pathAndQuery) {
10827
11714
 
10828
11715
  // src/setup/index.ts
10829
11716
  var import_node_crypto6 = require("crypto");
10830
- var import_node_fs9 = require("fs");
10831
- var import_node_path11 = require("path");
10832
- var import_node_os9 = require("os");
11717
+ var import_node_fs10 = require("fs");
11718
+ var import_node_path12 = require("path");
11719
+ var import_node_os10 = require("os");
10833
11720
 
10834
11721
  // src/setup/deps.ts
10835
11722
  var import_node_child_process2 = require("child_process");
10836
- var import_node_fs6 = require("fs");
10837
- var import_node_path8 = require("path");
10838
- var import_node_os6 = require("os");
11723
+ var import_node_fs7 = require("fs");
11724
+ var import_node_path9 = require("path");
11725
+ var import_node_os7 = require("os");
10839
11726
  var DependencyChecker = class {
10840
11727
  async checkAll() {
10841
11728
  return Promise.all([
@@ -10885,8 +11772,8 @@ var DependencyChecker = class {
10885
11772
  }
10886
11773
  }
10887
11774
  async checkCloudflared() {
10888
- const binPath = (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".agenticmail", "bin", "cloudflared");
10889
- if ((0, import_node_fs6.existsSync)(binPath)) {
11775
+ const binPath = (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".agenticmail", "bin", "cloudflared");
11776
+ if ((0, import_node_fs7.existsSync)(binPath)) {
10890
11777
  let version;
10891
11778
  try {
10892
11779
  const output = (0, import_node_child_process2.execFileSync)(binPath, ["--version"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
@@ -10908,10 +11795,10 @@ var DependencyChecker = class {
10908
11795
 
10909
11796
  // src/setup/installer.ts
10910
11797
  var import_node_child_process3 = require("child_process");
10911
- var import_node_fs7 = require("fs");
11798
+ var import_node_fs8 = require("fs");
10912
11799
  var import_promises2 = require("fs/promises");
10913
- var import_node_path9 = require("path");
10914
- var import_node_os7 = require("os");
11800
+ var import_node_path10 = require("path");
11801
+ var import_node_os8 = require("os");
10915
11802
  function runShellWithRollingOutput(cmd, opts = {}) {
10916
11803
  const maxLines = opts.maxLines ?? 20;
10917
11804
  const timeout = opts.timeout ?? 3e5;
@@ -11021,7 +11908,7 @@ var DependencyInstaller = class {
11021
11908
  */
11022
11909
  async installDocker() {
11023
11910
  if (this.isDockerReady()) return;
11024
- const os = (0, import_node_os7.platform)();
11911
+ const os = (0, import_node_os8.platform)();
11025
11912
  if (os === "darwin") {
11026
11913
  await this.installDockerMac();
11027
11914
  } else if (os === "linux") {
@@ -11087,15 +11974,15 @@ var DependencyInstaller = class {
11087
11974
  try {
11088
11975
  const composeBin = (0, import_node_child_process3.execFileSync)("which", ["docker-compose"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
11089
11976
  if (!composeBin) return;
11090
- const pluginDir = (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".docker", "cli-plugins");
11091
- const pluginPath = (0, import_node_path9.join)(pluginDir, "docker-compose");
11092
- if ((0, import_node_fs7.existsSync)(pluginPath)) return;
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;
11093
11980
  try {
11094
- (0, import_node_fs7.mkdirSync)(pluginDir, { recursive: true });
11981
+ (0, import_node_fs8.mkdirSync)(pluginDir, { recursive: true });
11095
11982
  } catch {
11096
11983
  }
11097
11984
  try {
11098
- (0, import_node_fs7.symlinkSync)(composeBin, pluginPath);
11985
+ (0, import_node_fs8.symlinkSync)(composeBin, pluginPath);
11099
11986
  } catch {
11100
11987
  }
11101
11988
  } catch {
@@ -11154,9 +12041,9 @@ var DependencyInstaller = class {
11154
12041
  return;
11155
12042
  }
11156
12043
  this.onProgress("__progress__:5:Installing Docker Engine...");
11157
- const tmpDir = (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".agenticmail", "tmp");
12044
+ const tmpDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "tmp");
11158
12045
  await (0, import_promises2.mkdir)(tmpDir, { recursive: true });
11159
- const scriptPath = (0, import_node_path9.join)(tmpDir, "install-docker.sh");
12046
+ const scriptPath = (0, import_node_path10.join)(tmpDir, "install-docker.sh");
11160
12047
  const dlResult = await runShellWithRollingOutput(
11161
12048
  `curl -fsSL https://get.docker.com -o "${scriptPath}" && sudo sh "${scriptPath}"`,
11162
12049
  { timeout: 3e5 }
@@ -11224,8 +12111,8 @@ var DependencyInstaller = class {
11224
12111
  }
11225
12112
  try {
11226
12113
  const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
11227
- const dockerExe = (0, import_node_path9.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
11228
- if ((0, import_node_fs7.existsSync)(dockerExe)) {
12114
+ const dockerExe = (0, import_node_path10.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
12115
+ if ((0, import_node_fs8.existsSync)(dockerExe)) {
11229
12116
  (0, import_node_child_process3.execSync)(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
11230
12117
  }
11231
12118
  } catch {
@@ -11275,8 +12162,8 @@ var DependencyInstaller = class {
11275
12162
  this.onProgress("__progress__:70:Docker Desktop installed. Starting...");
11276
12163
  try {
11277
12164
  const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
11278
- const dockerExe = (0, import_node_path9.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
11279
- if ((0, import_node_fs7.existsSync)(dockerExe)) {
12165
+ const dockerExe = (0, import_node_path10.join)(programFiles, "Docker", "Docker", "Docker Desktop.exe");
12166
+ if ((0, import_node_fs8.existsSync)(dockerExe)) {
11280
12167
  (0, import_node_child_process3.execSync)(`start "" "${dockerExe}"`, { timeout: 1e4, stdio: "ignore", shell: "cmd.exe" });
11281
12168
  }
11282
12169
  } catch {
@@ -11313,12 +12200,12 @@ var DependencyInstaller = class {
11313
12200
  * Start the Stalwart mail server Docker container.
11314
12201
  */
11315
12202
  async startStalwart(composePath) {
11316
- if (!(0, import_node_fs7.existsSync)(composePath)) {
12203
+ if (!(0, import_node_fs8.existsSync)(composePath)) {
11317
12204
  throw new Error(`docker-compose.yml not found at: ${composePath}`);
11318
12205
  }
11319
12206
  if (!this.isDockerReady()) {
11320
12207
  this.onProgress("Starting Docker...");
11321
- const os = (0, import_node_os7.platform)();
12208
+ const os = (0, import_node_os8.platform)();
11322
12209
  if (os === "darwin") {
11323
12210
  await this.startColima();
11324
12211
  } else if (os === "win32") {
@@ -11336,7 +12223,7 @@ var DependencyInstaller = class {
11336
12223
  }
11337
12224
  }
11338
12225
  this.onProgress("__progress__:10:Pulling mail server image...");
11339
- if ((0, import_node_os7.platform)() === "darwin") this.linkComposePlugin();
12226
+ if ((0, import_node_os8.platform)() === "darwin") this.linkComposePlugin();
11340
12227
  let composeResult = await runSilent("docker", ["compose", "-f", composePath, "up", "-d"], { timeout: 12e4 });
11341
12228
  if (composeResult.exitCode !== 0 && hasCommand("docker-compose")) {
11342
12229
  composeResult = await runSilent("docker-compose", ["-f", composePath, "up", "-d"], { timeout: 12e4 });
@@ -11371,22 +12258,22 @@ var DependencyInstaller = class {
11371
12258
  * Returns the path to the installed binary.
11372
12259
  */
11373
12260
  async installCloudflared() {
11374
- const os = (0, import_node_os7.platform)();
11375
- const binDir = (0, import_node_path9.join)((0, import_node_os7.homedir)(), ".agenticmail", "bin");
12261
+ const os = (0, import_node_os8.platform)();
12262
+ const binDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "bin");
11376
12263
  const binName = os === "win32" ? "cloudflared.exe" : "cloudflared";
11377
- const binPath = (0, import_node_path9.join)(binDir, binName);
11378
- if ((0, import_node_fs7.existsSync)(binPath)) {
12264
+ const binPath = (0, import_node_path10.join)(binDir, binName);
12265
+ if ((0, import_node_fs8.existsSync)(binPath)) {
11379
12266
  return binPath;
11380
12267
  }
11381
12268
  try {
11382
12269
  const whichCmd = os === "win32" ? "where" : "which";
11383
12270
  const sysPath = (0, import_node_child_process3.execFileSync)(whichCmd, ["cloudflared"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split("\n")[0];
11384
- if (sysPath && (0, import_node_fs7.existsSync)(sysPath)) return sysPath;
12271
+ if (sysPath && (0, import_node_fs8.existsSync)(sysPath)) return sysPath;
11385
12272
  } catch {
11386
12273
  }
11387
12274
  this.onProgress("Downloading cloudflared...");
11388
12275
  await (0, import_promises2.mkdir)(binDir, { recursive: true });
11389
- const cpu = (0, import_node_os7.arch)();
12276
+ const cpu = (0, import_node_os8.arch)();
11390
12277
  const archName = cpu === "arm64" ? "arm64" : "amd64";
11391
12278
  let downloadUrl;
11392
12279
  if (os === "darwin") {
@@ -11404,7 +12291,7 @@ var DependencyInstaller = class {
11404
12291
  }
11405
12292
  const buffer = Buffer.from(await response.arrayBuffer());
11406
12293
  if (os === "darwin") {
11407
- const tgzPath = (0, import_node_path9.join)(binDir, "cloudflared.tgz");
12294
+ const tgzPath = (0, import_node_path10.join)(binDir, "cloudflared.tgz");
11408
12295
  await (0, import_promises2.writeFile)(tgzPath, buffer);
11409
12296
  try {
11410
12297
  (0, import_node_child_process3.execFileSync)("tar", ["-xzf", tgzPath, "-C", binDir, "cloudflared"], { timeout: 15e3, stdio: "ignore" });
@@ -11421,7 +12308,7 @@ var DependencyInstaller = class {
11421
12308
  if (os !== "win32") await (0, import_promises2.chmod)(tmpPath, 493);
11422
12309
  await (0, import_promises2.rename)(tmpPath, binPath);
11423
12310
  }
11424
- if (!(0, import_node_fs7.existsSync)(binPath)) {
12311
+ if (!(0, import_node_fs8.existsSync)(binPath)) {
11425
12312
  throw new Error("cloudflared download succeeded but binary not found after extraction");
11426
12313
  }
11427
12314
  this.onProgress("cloudflared installed");
@@ -11439,23 +12326,23 @@ var DependencyInstaller = class {
11439
12326
 
11440
12327
  // src/setup/service.ts
11441
12328
  var import_node_child_process4 = require("child_process");
11442
- var import_node_fs8 = require("fs");
11443
- var import_node_path10 = require("path");
11444
- var import_node_os8 = require("os");
12329
+ var import_node_fs9 = require("fs");
12330
+ var import_node_path11 = require("path");
12331
+ var import_node_os9 = require("os");
11445
12332
  var import_node_module2 = require("module");
11446
- var import_meta2 = {};
12333
+ var import_meta3 = {};
11447
12334
  var PLIST_LABEL = "com.agenticmail.server";
11448
12335
  var SYSTEMD_UNIT = "agenticmail.service";
11449
12336
  var ServiceManager = class {
11450
- os = (0, import_node_os8.platform)();
12337
+ os = (0, import_node_os9.platform)();
11451
12338
  /**
11452
12339
  * Get the path to the service file.
11453
12340
  */
11454
12341
  getServicePath() {
11455
12342
  if (this.os === "darwin") {
11456
- return (0, import_node_path10.join)((0, import_node_os8.homedir)(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
12343
+ return (0, import_node_path11.join)((0, import_node_os9.homedir)(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
11457
12344
  } else {
11458
- return (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".config", "systemd", "user", SYSTEMD_UNIT);
12345
+ return (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".config", "systemd", "user", SYSTEMD_UNIT);
11459
12346
  }
11460
12347
  }
11461
12348
  /**
@@ -11487,42 +12374,42 @@ var ServiceManager = class {
11487
12374
  */
11488
12375
  getApiEntryPath() {
11489
12376
  try {
11490
- const req = (0, import_node_module2.createRequire)(import_meta2.url);
12377
+ const req = (0, import_node_module2.createRequire)(import_meta3.url);
11491
12378
  const resolved = req.resolve("@agenticmail/api");
11492
- if ((0, import_node_fs8.existsSync)(resolved)) return resolved;
12379
+ if ((0, import_node_fs9.existsSync)(resolved)) return resolved;
11493
12380
  } catch {
11494
12381
  }
11495
12382
  const parentPackages = [
11496
- (0, import_node_path10.join)("@agenticmail", "cli"),
12383
+ (0, import_node_path11.join)("@agenticmail", "cli"),
11497
12384
  // current scoped package
11498
12385
  "agenticmail"
11499
12386
  // legacy unscoped package
11500
12387
  ];
11501
12388
  const baseDirs = [
11502
12389
  // user-local install
11503
- (0, import_node_path10.join)((0, import_node_os8.homedir)(), "node_modules")
12390
+ (0, import_node_path11.join)((0, import_node_os9.homedir)(), "node_modules")
11504
12391
  ];
11505
12392
  try {
11506
12393
  const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
11507
- baseDirs.push((0, import_node_path10.join)(prefix, "lib", "node_modules"));
11508
- baseDirs.push((0, import_node_path10.join)(prefix, "node_modules"));
12394
+ baseDirs.push((0, import_node_path11.join)(prefix, "lib", "node_modules"));
12395
+ baseDirs.push((0, import_node_path11.join)(prefix, "node_modules"));
11509
12396
  } catch {
11510
12397
  }
11511
12398
  baseDirs.push("/opt/homebrew/lib/node_modules");
11512
12399
  baseDirs.push("/usr/local/lib/node_modules");
11513
12400
  for (const base of baseDirs) {
11514
- const sibling = (0, import_node_path10.join)(base, "@agenticmail", "api", "dist", "index.js");
11515
- if ((0, import_node_fs8.existsSync)(sibling)) return sibling;
12401
+ const sibling = (0, import_node_path11.join)(base, "@agenticmail", "api", "dist", "index.js");
12402
+ if ((0, import_node_fs9.existsSync)(sibling)) return sibling;
11516
12403
  for (const parent of parentPackages) {
11517
- const nested = (0, import_node_path10.join)(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
11518
- if ((0, import_node_fs8.existsSync)(nested)) return nested;
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;
11519
12406
  }
11520
12407
  }
11521
- const dataDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail");
11522
- const entryCache = (0, import_node_path10.join)(dataDir, "api-entry.path");
11523
- if ((0, import_node_fs8.existsSync)(entryCache)) {
11524
- const cached = (0, import_node_fs8.readFileSync)(entryCache, "utf-8").trim();
11525
- if (cached && (0, import_node_fs8.existsSync)(cached)) return cached;
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;
11526
12413
  }
11527
12414
  throw new Error("Could not find @agenticmail/api entry point. Run `agenticmail start` first to populate the cache.");
11528
12415
  }
@@ -11530,9 +12417,9 @@ var ServiceManager = class {
11530
12417
  * Cache the API entry path so the service can find it later.
11531
12418
  */
11532
12419
  cacheApiEntryPath(entryPath) {
11533
- const dataDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail");
11534
- if (!(0, import_node_fs8.existsSync)(dataDir)) (0, import_node_fs8.mkdirSync)(dataDir, { recursive: true });
11535
- (0, import_node_fs8.writeFileSync)((0, import_node_path10.join)(dataDir, "api-entry.path"), entryPath);
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);
11536
12423
  }
11537
12424
  /**
11538
12425
  * Get the current package version.
@@ -11543,38 +12430,38 @@ var ServiceManager = class {
11543
12430
  */
11544
12431
  getVersion() {
11545
12432
  try {
11546
- const req = (0, import_node_module2.createRequire)(import_meta2.url);
12433
+ const req = (0, import_node_module2.createRequire)(import_meta3.url);
11547
12434
  const pkgJson = req.resolve("@agenticmail/cli/package.json");
11548
- if ((0, import_node_fs8.existsSync)(pkgJson)) {
11549
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)(pkgJson, "utf-8"));
12435
+ if ((0, import_node_fs9.existsSync)(pkgJson)) {
12436
+ const pkg = JSON.parse((0, import_node_fs9.readFileSync)(pkgJson, "utf-8"));
11550
12437
  if (pkg.version) return pkg.version;
11551
12438
  }
11552
12439
  } catch {
11553
12440
  }
11554
12441
  try {
11555
12442
  const apiEntry = this.getApiEntryPath();
11556
- const apiPkg = (0, import_node_path10.join)(apiEntry, "..", "..", "package.json");
11557
- if ((0, import_node_fs8.existsSync)(apiPkg)) {
11558
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)(apiPkg, "utf-8"));
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"));
11559
12446
  if (pkg.version) return pkg.version;
11560
12447
  }
11561
12448
  } catch {
11562
12449
  }
11563
12450
  const candidates = [
11564
- (0, import_node_path10.join)((0, import_node_os8.homedir)(), "node_modules", "@agenticmail", "cli", "package.json"),
11565
- (0, import_node_path10.join)((0, import_node_os8.homedir)(), "node_modules", "agenticmail", "package.json"),
11566
- (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "package-version.json")
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")
11567
12454
  ];
11568
12455
  try {
11569
12456
  const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
11570
- candidates.push((0, import_node_path10.join)(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
11571
- candidates.push((0, import_node_path10.join)(prefix, "lib", "node_modules", "agenticmail", "package.json"));
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"));
11572
12459
  } catch {
11573
12460
  }
11574
12461
  for (const p of candidates) {
11575
12462
  try {
11576
- if ((0, import_node_fs8.existsSync)(p)) {
11577
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)(p, "utf-8"));
12463
+ if ((0, import_node_fs9.existsSync)(p)) {
12464
+ const pkg = JSON.parse((0, import_node_fs9.readFileSync)(p, "utf-8"));
11578
12465
  if (pkg.version) return pkg.version;
11579
12466
  }
11580
12467
  } catch {
@@ -11587,9 +12474,9 @@ var ServiceManager = class {
11587
12474
  * This ensures AgenticMail doesn't fail on boot when Docker is still loading.
11588
12475
  */
11589
12476
  generateStartScript(nodePath, apiEntry) {
11590
- const scriptPath = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "bin", "start-server.sh");
11591
- const scriptDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "bin");
11592
- if (!(0, import_node_fs8.existsSync)(scriptDir)) (0, import_node_fs8.mkdirSync)(scriptDir, { recursive: true });
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 });
11593
12480
  const script = [
11594
12481
  "#!/bin/bash",
11595
12482
  "# AgenticMail auto-start script",
@@ -11640,7 +12527,7 @@ var ServiceManager = class {
11640
12527
  `log "Starting API server: ${nodePath} ${apiEntry}"`,
11641
12528
  `exec "${nodePath}" "${apiEntry}"`
11642
12529
  ].join("\n") + "\n";
11643
- (0, import_node_fs8.writeFileSync)(scriptPath, script, { mode: 493 });
12530
+ (0, import_node_fs9.writeFileSync)(scriptPath, script, { mode: 493 });
11644
12531
  return scriptPath;
11645
12532
  }
11646
12533
  /**
@@ -11653,9 +12540,9 @@ var ServiceManager = class {
11653
12540
  * - Service version tracking in env vars
11654
12541
  */
11655
12542
  generatePlist(nodePath, apiEntry, configPath) {
11656
- const config = JSON.parse((0, import_node_fs8.readFileSync)(configPath, "utf-8"));
11657
- const logDir = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "logs");
11658
- if (!(0, import_node_fs8.existsSync)(logDir)) (0, import_node_fs8.mkdirSync)(logDir, { recursive: true });
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 });
11659
12546
  const version = this.getVersion();
11660
12547
  const startScript = this.generateStartScript(nodePath, apiEntry);
11661
12548
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -11676,9 +12563,9 @@ var ServiceManager = class {
11676
12563
  <key>EnvironmentVariables</key>
11677
12564
  <dict>
11678
12565
  <key>HOME</key>
11679
- <string>${(0, import_node_os8.homedir)()}</string>
12566
+ <string>${(0, import_node_os9.homedir)()}</string>
11680
12567
  <key>AGENTICMAIL_DATA_DIR</key>
11681
- <string>${config.dataDir || (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail")}</string>
12568
+ <string>${config.dataDir || (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail")}</string>
11682
12569
  <key>PATH</key>
11683
12570
  <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
11684
12571
  <key>AGENTICMAIL_SERVICE_VERSION</key>
@@ -11731,8 +12618,8 @@ var ServiceManager = class {
11731
12618
  * - Proper dependency ordering
11732
12619
  */
11733
12620
  generateSystemdUnit(nodePath, apiEntry, configPath) {
11734
- const config = JSON.parse((0, import_node_fs8.readFileSync)(configPath, "utf-8"));
11735
- const dataDir = config.dataDir || (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail");
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");
11736
12623
  const version = this.getVersion();
11737
12624
  const startScript = this.generateStartScript(nodePath, apiEntry);
11738
12625
  return `[Unit]
@@ -11749,7 +12636,7 @@ Restart=always
11749
12636
  RestartSec=15
11750
12637
  TimeoutStartSec=660
11751
12638
  LimitNOFILE=8192
11752
- Environment=HOME=${(0, import_node_os8.homedir)()}
12639
+ Environment=HOME=${(0, import_node_os9.homedir)()}
11753
12640
  Environment=AGENTICMAIL_DATA_DIR=${dataDir}
11754
12641
  Environment=PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin
11755
12642
  Environment=AGENTICMAIL_SERVICE_VERSION=${version}
@@ -11762,8 +12649,8 @@ WantedBy=default.target
11762
12649
  * Install the auto-start service.
11763
12650
  */
11764
12651
  install() {
11765
- const configPath = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "config.json");
11766
- if (!(0, import_node_fs8.existsSync)(configPath)) {
12652
+ const configPath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "config.json");
12653
+ if (!(0, import_node_fs9.existsSync)(configPath)) {
11767
12654
  return { installed: false, message: "Config not found. Run agenticmail setup first." };
11768
12655
  }
11769
12656
  const nodePath = this.getNodePath();
@@ -11775,17 +12662,17 @@ WantedBy=default.target
11775
12662
  }
11776
12663
  const servicePath = this.getServicePath();
11777
12664
  if (this.os === "darwin") {
11778
- const dir2 = (0, import_node_path10.join)((0, import_node_os8.homedir)(), "Library", "LaunchAgents");
11779
- if (!(0, import_node_fs8.existsSync)(dir2)) (0, import_node_fs8.mkdirSync)(dir2, { recursive: true });
11780
- if ((0, import_node_fs8.existsSync)(servicePath)) {
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)) {
11781
12668
  try {
11782
12669
  (0, import_node_child_process4.execFileSync)("launchctl", ["unload", servicePath], { timeout: 1e4, stdio: "ignore" });
11783
12670
  } catch {
11784
12671
  }
11785
12672
  }
11786
12673
  const plist = this.generatePlist(nodePath, apiEntry, configPath);
11787
- (0, import_node_fs8.writeFileSync)(servicePath, plist);
11788
- (0, import_node_fs8.chmodSync)(servicePath, 384);
12674
+ (0, import_node_fs9.writeFileSync)(servicePath, plist);
12675
+ (0, import_node_fs9.chmodSync)(servicePath, 384);
11789
12676
  try {
11790
12677
  (0, import_node_child_process4.execFileSync)("launchctl", ["load", servicePath], { timeout: 1e4, stdio: "ignore" });
11791
12678
  } catch (err) {
@@ -11793,11 +12680,11 @@ WantedBy=default.target
11793
12680
  }
11794
12681
  return { installed: true, message: `Service installed at ${servicePath}` };
11795
12682
  } else if (this.os === "linux") {
11796
- const dir2 = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".config", "systemd", "user");
11797
- if (!(0, import_node_fs8.existsSync)(dir2)) (0, import_node_fs8.mkdirSync)(dir2, { recursive: true });
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 });
11798
12685
  const unit = this.generateSystemdUnit(nodePath, apiEntry, configPath);
11799
- (0, import_node_fs8.writeFileSync)(servicePath, unit);
11800
- (0, import_node_fs8.chmodSync)(servicePath, 384);
12686
+ (0, import_node_fs9.writeFileSync)(servicePath, unit);
12687
+ (0, import_node_fs9.chmodSync)(servicePath, 384);
11801
12688
  try {
11802
12689
  (0, import_node_child_process4.execFileSync)("systemctl", ["--user", "daemon-reload"], { timeout: 1e4, stdio: "ignore" });
11803
12690
  (0, import_node_child_process4.execFileSync)("systemctl", ["--user", "enable", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
@@ -11819,7 +12706,7 @@ WantedBy=default.target
11819
12706
  */
11820
12707
  uninstall() {
11821
12708
  const servicePath = this.getServicePath();
11822
- if (!(0, import_node_fs8.existsSync)(servicePath)) {
12709
+ if (!(0, import_node_fs9.existsSync)(servicePath)) {
11823
12710
  return { removed: false, message: "Service is not installed." };
11824
12711
  }
11825
12712
  if (this.os === "darwin") {
@@ -11828,7 +12715,7 @@ WantedBy=default.target
11828
12715
  } catch {
11829
12716
  }
11830
12717
  try {
11831
- (0, import_node_fs8.unlinkSync)(servicePath);
12718
+ (0, import_node_fs9.unlinkSync)(servicePath);
11832
12719
  } catch {
11833
12720
  }
11834
12721
  return { removed: true, message: "Service removed." };
@@ -11839,7 +12726,7 @@ WantedBy=default.target
11839
12726
  } catch {
11840
12727
  }
11841
12728
  try {
11842
- (0, import_node_fs8.unlinkSync)(servicePath);
12729
+ (0, import_node_fs9.unlinkSync)(servicePath);
11843
12730
  } catch {
11844
12731
  }
11845
12732
  try {
@@ -11857,7 +12744,7 @@ WantedBy=default.target
11857
12744
  status() {
11858
12745
  const servicePath = this.getServicePath();
11859
12746
  const plat = this.os === "darwin" ? "launchd" : this.os === "linux" ? "systemd" : "unsupported";
11860
- const installed = (0, import_node_fs8.existsSync)(servicePath);
12747
+ const installed = (0, import_node_fs9.existsSync)(servicePath);
11861
12748
  let running = false;
11862
12749
  if (installed) {
11863
12750
  if (this.os === "darwin") {
@@ -11912,34 +12799,34 @@ WantedBy=default.target
11912
12799
  needsRepair() {
11913
12800
  if (this.os !== "darwin" && this.os !== "linux") return null;
11914
12801
  const servicePath = this.getServicePath();
11915
- if (!(0, import_node_fs8.existsSync)(servicePath)) return null;
12802
+ if (!(0, import_node_fs9.existsSync)(servicePath)) return null;
11916
12803
  let serviceContent = "";
11917
12804
  try {
11918
- serviceContent = (0, import_node_fs8.readFileSync)(servicePath, "utf-8");
12805
+ serviceContent = (0, import_node_fs9.readFileSync)(servicePath, "utf-8");
11919
12806
  } catch {
11920
12807
  return { reason: "Service file unreadable" };
11921
12808
  }
11922
- const startScript = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "bin", "start-server.sh");
12809
+ const startScript = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "bin", "start-server.sh");
11923
12810
  if (serviceContent.includes(startScript)) {
11924
- if (!(0, import_node_fs8.existsSync)(startScript)) {
12811
+ if (!(0, import_node_fs9.existsSync)(startScript)) {
11925
12812
  return { reason: "start-server.sh is missing" };
11926
12813
  }
11927
12814
  let scriptContent = "";
11928
12815
  try {
11929
- scriptContent = (0, import_node_fs8.readFileSync)(startScript, "utf-8");
12816
+ scriptContent = (0, import_node_fs9.readFileSync)(startScript, "utf-8");
11930
12817
  } catch {
11931
12818
  return { reason: "start-server.sh unreadable" };
11932
12819
  }
11933
12820
  const apiPathMatch = scriptContent.match(/(\/[^"\s]+@agenticmail\/api\/dist\/index\.js)/);
11934
12821
  if (apiPathMatch) {
11935
12822
  const referenced = apiPathMatch[1];
11936
- if (!(0, import_node_fs8.existsSync)(referenced)) {
12823
+ if (!(0, import_node_fs9.existsSync)(referenced)) {
11937
12824
  return { reason: `start-server.sh references missing path: ${referenced}` };
11938
12825
  }
11939
12826
  }
11940
12827
  if (/node_modules\/agenticmail\/(?!.*@agenticmail\/cli)/.test(scriptContent)) {
11941
12828
  const stale = /(\S*node_modules\/agenticmail\/\S*)/.exec(scriptContent)?.[1];
11942
- if (stale && !(0, import_node_fs8.existsSync)(stale)) {
12829
+ if (stale && !(0, import_node_fs9.existsSync)(stale)) {
11943
12830
  return { reason: `start-server.sh references legacy unscoped path: ${stale}` };
11944
12831
  }
11945
12832
  }
@@ -11952,11 +12839,11 @@ WantedBy=default.target
11952
12839
  return { reason: `Service version drift (current CLI is v${currentVersion})` };
11953
12840
  }
11954
12841
  }
11955
- const entryCache = (0, import_node_path10.join)((0, import_node_os8.homedir)(), ".agenticmail", "api-entry.path");
11956
- if ((0, import_node_fs8.existsSync)(entryCache)) {
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)) {
11957
12844
  try {
11958
- const cached = (0, import_node_fs8.readFileSync)(entryCache, "utf-8").trim();
11959
- if (cached && !(0, import_node_fs8.existsSync)(cached)) {
12845
+ const cached = (0, import_node_fs9.readFileSync)(entryCache, "utf-8").trim();
12846
+ if (cached && !(0, import_node_fs9.existsSync)(cached)) {
11960
12847
  return { reason: `Cached API entry path no longer exists: ${cached}` };
11961
12848
  }
11962
12849
  } catch {
@@ -12007,13 +12894,13 @@ var SetupManager = class {
12007
12894
  * falls back to monorepo location.
12008
12895
  */
12009
12896
  getComposePath() {
12010
- const standalonePath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "docker-compose.yml");
12011
- if ((0, import_node_fs9.existsSync)(standalonePath)) return standalonePath;
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;
12012
12899
  const cwd = process.cwd();
12013
- const candidates = [cwd, (0, import_node_path11.join)(cwd, "..")];
12900
+ const candidates = [cwd, (0, import_node_path12.join)(cwd, "..")];
12014
12901
  for (const dir2 of candidates) {
12015
- const p = (0, import_node_path11.join)(dir2, "docker-compose.yml");
12016
- if ((0, import_node_fs9.existsSync)(p)) return p;
12902
+ const p = (0, import_node_path12.join)(dir2, "docker-compose.yml");
12903
+ if ((0, import_node_fs10.existsSync)(p)) return p;
12017
12904
  }
12018
12905
  return standalonePath;
12019
12906
  }
@@ -12023,19 +12910,19 @@ var SetupManager = class {
12023
12910
  * Always regenerates Docker files to keep passwords in sync.
12024
12911
  */
12025
12912
  initConfig() {
12026
- const dataDir = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
12027
- const configPath = (0, import_node_path11.join)(dataDir, "config.json");
12028
- const envPath = (0, import_node_path11.join)(dataDir, ".env");
12029
- if ((0, import_node_fs9.existsSync)(configPath)) {
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)) {
12030
12917
  try {
12031
- const existing = JSON.parse((0, import_node_fs9.readFileSync)(configPath, "utf-8"));
12918
+ const existing = JSON.parse((0, import_node_fs10.readFileSync)(configPath, "utf-8"));
12032
12919
  this.generateDockerFiles(existing);
12033
12920
  return { configPath, envPath, config: existing, isNew: false };
12034
12921
  } catch {
12035
12922
  }
12036
12923
  }
12037
- if (!(0, import_node_fs9.existsSync)(dataDir)) {
12038
- (0, import_node_fs9.mkdirSync)(dataDir, { recursive: true });
12924
+ if (!(0, import_node_fs10.existsSync)(dataDir)) {
12925
+ (0, import_node_fs10.mkdirSync)(dataDir, { recursive: true });
12039
12926
  }
12040
12927
  const masterKey = `mk_${(0, import_node_crypto6.randomBytes)(24).toString("hex")}`;
12041
12928
  const stalwartPassword = (0, import_node_crypto6.randomBytes)(16).toString("hex");
@@ -12051,8 +12938,8 @@ var SetupManager = class {
12051
12938
  api: { port: 3829, host: "127.0.0.1" },
12052
12939
  dataDir
12053
12940
  };
12054
- (0, import_node_fs9.writeFileSync)(configPath, JSON.stringify(config, null, 2));
12055
- (0, import_node_fs9.chmodSync)(configPath, 384);
12941
+ (0, import_node_fs10.writeFileSync)(configPath, JSON.stringify(config, null, 2));
12942
+ (0, import_node_fs10.chmodSync)(configPath, 384);
12056
12943
  const envContent = `# Auto-generated by agenticmail setup
12057
12944
  STALWART_ADMIN_USER=admin
12058
12945
  STALWART_ADMIN_PASSWORD=${stalwartPassword}
@@ -12067,8 +12954,8 @@ SMTP_PORT=587
12067
12954
  IMAP_HOST=localhost
12068
12955
  IMAP_PORT=143
12069
12956
  `;
12070
- (0, import_node_fs9.writeFileSync)(envPath, envContent);
12071
- (0, import_node_fs9.chmodSync)(envPath, 384);
12957
+ (0, import_node_fs10.writeFileSync)(envPath, envContent);
12958
+ (0, import_node_fs10.chmodSync)(envPath, 384);
12072
12959
  this.generateDockerFiles(config);
12073
12960
  return { configPath, envPath, config, isNew: true };
12074
12961
  }
@@ -12077,13 +12964,13 @@ IMAP_PORT=143
12077
12964
  * with the correct admin password from config.
12078
12965
  */
12079
12966
  generateDockerFiles(config) {
12080
- const dataDir = config.dataDir || (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail");
12081
- if (!(0, import_node_fs9.existsSync)(dataDir)) {
12082
- (0, import_node_fs9.mkdirSync)(dataDir, { recursive: true });
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 });
12083
12970
  }
12084
12971
  const password = config.stalwart?.adminPassword || "changeme";
12085
- const composePath = (0, import_node_path11.join)(dataDir, "docker-compose.yml");
12086
- (0, import_node_fs9.writeFileSync)(composePath, `services:
12972
+ const composePath = (0, import_node_path12.join)(dataDir, "docker-compose.yml");
12973
+ (0, import_node_fs10.writeFileSync)(composePath, `services:
12087
12974
  stalwart:
12088
12975
  # Pinned to v0.15.5 \u2014 Stalwart 0.16+ moved its config to JSON
12089
12976
  # at /etc/stalwart/config.json (hardcoded into the container
@@ -12112,9 +12999,9 @@ IMAP_PORT=143
12112
12999
  volumes:
12113
13000
  stalwart-data:
12114
13001
  `);
12115
- (0, import_node_fs9.chmodSync)(composePath, 384);
12116
- const tomlPath = (0, import_node_path11.join)(dataDir, "stalwart.toml");
12117
- (0, import_node_fs9.writeFileSync)(tomlPath, `# Stalwart Mail Server \u2014 AgenticMail Configuration
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
12118
13005
 
12119
13006
  [server]
12120
13007
  hostname = "localhost"
@@ -12164,27 +13051,27 @@ enable = true
12164
13051
  user = "admin"
12165
13052
  secret = "${password}"
12166
13053
  `);
12167
- (0, import_node_fs9.chmodSync)(tomlPath, 384);
13054
+ (0, import_node_fs10.chmodSync)(tomlPath, 384);
12168
13055
  }
12169
13056
  /**
12170
13057
  * Check if config has already been initialized.
12171
13058
  */
12172
13059
  isInitialized() {
12173
- const configPath = (0, import_node_path11.join)((0, import_node_os9.homedir)(), ".agenticmail", "config.json");
12174
- return (0, import_node_fs9.existsSync)(configPath);
13060
+ const configPath = (0, import_node_path12.join)((0, import_node_os10.homedir)(), ".agenticmail", "config.json");
13061
+ return (0, import_node_fs10.existsSync)(configPath);
12175
13062
  }
12176
13063
  };
12177
13064
 
12178
13065
  // src/media/manager.ts
12179
13066
  var import_node_child_process6 = require("child_process");
12180
13067
  var import_node_util = require("util");
12181
- var import_node_fs11 = require("fs");
12182
- var import_node_path12 = require("path");
13068
+ var import_node_fs12 = require("fs");
13069
+ var import_node_path13 = require("path");
12183
13070
 
12184
13071
  // src/media/binaries.ts
12185
13072
  var import_node_child_process5 = require("child_process");
12186
- var import_node_fs10 = require("fs");
12187
- var import_meta3 = {};
13073
+ var import_node_fs11 = require("fs");
13074
+ var import_meta4 = {};
12188
13075
  var BINARY_SPECS = {
12189
13076
  ffmpeg: {
12190
13077
  binary: "ffmpeg",
@@ -12257,7 +13144,7 @@ function probeCommand(command, spec) {
12257
13144
  }
12258
13145
  function detectEdgeTts(spec) {
12259
13146
  try {
12260
- const resolved = import_meta3.resolve?.("node-edge-tts");
13147
+ const resolved = import_meta4.resolve?.("node-edge-tts");
12261
13148
  if (resolved) {
12262
13149
  return {
12263
13150
  binary: "edge-tts",
@@ -12324,7 +13211,7 @@ function requireWhisperModel(modelPath) {
12324
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."
12325
13212
  );
12326
13213
  }
12327
- if (!(0, import_node_fs10.existsSync)(modelPath)) {
13214
+ if (!(0, import_node_fs11.existsSync)(modelPath)) {
12328
13215
  throw new Error(`whisper model file not found: ${modelPath}`);
12329
13216
  }
12330
13217
  return modelPath;
@@ -12376,7 +13263,7 @@ function validateInputPath(path2, label = "input") {
12376
13263
  `${label} file path may not start with "-" \u2014 pass an absolute path so it cannot be parsed as a command flag`
12377
13264
  );
12378
13265
  }
12379
- if (!(0, import_node_fs11.existsSync)(path2)) {
13266
+ if (!(0, import_node_fs12.existsSync)(path2)) {
12380
13267
  throw new Error(`${label} file not found: ${path2}`);
12381
13268
  }
12382
13269
  return path2;
@@ -12398,32 +13285,32 @@ var MediaManager = class {
12398
13285
  if (options.outputDir) {
12399
13286
  this.outputDir = options.outputDir;
12400
13287
  } else if (options.dataDir) {
12401
- this.outputDir = (0, import_node_path12.join)(options.dataDir, "media");
13288
+ this.outputDir = (0, import_node_path13.join)(options.dataDir, "media");
12402
13289
  } else {
12403
13290
  const tmp = process.env.TMPDIR || process.env.TEMP || "/tmp";
12404
- this.outputDir = (0, import_node_path12.join)(tmp, "agenticmail-media");
13291
+ this.outputDir = (0, import_node_path13.join)(tmp, "agenticmail-media");
12405
13292
  }
12406
13293
  }
12407
13294
  /** Ensure the output directory exists; returns it. */
12408
13295
  ensureOutputDir() {
12409
- if (!(0, import_node_fs11.existsSync)(this.outputDir)) {
12410
- (0, import_node_fs11.mkdirSync)(this.outputDir, { recursive: true });
13296
+ if (!(0, import_node_fs12.existsSync)(this.outputDir)) {
13297
+ (0, import_node_fs12.mkdirSync)(this.outputDir, { recursive: true });
12411
13298
  }
12412
13299
  return this.outputDir;
12413
13300
  }
12414
13301
  /** Build an output path inside the managed output dir. */
12415
13302
  outPath(prefix, ext) {
12416
- return (0, import_node_path12.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}.${ext}`);
13303
+ return (0, import_node_path13.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}.${ext}`);
12417
13304
  }
12418
13305
  /** Build a sub-directory inside the managed output dir. */
12419
13306
  outDir(prefix) {
12420
- const dir2 = (0, import_node_path12.join)(this.ensureOutputDir(), `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
12421
- (0, import_node_fs11.mkdirSync)(dir2, { recursive: true });
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 });
12422
13309
  return dir2;
12423
13310
  }
12424
13311
  /** Stat a produced file into a {@link MediaFileResult} envelope. */
12425
13312
  fileResult(path2, extra = {}) {
12426
- const stat = (0, import_node_fs11.statSync)(path2);
13313
+ const stat = (0, import_node_fs12.statSync)(path2);
12427
13314
  return { ok: true, filePath: path2, sizeBytes: stat.size, ...extra };
12428
13315
  }
12429
13316
  // ─── binary invocation helpers (execFile, arg arrays, no shell) ────
@@ -12537,7 +13424,7 @@ var MediaManager = class {
12537
13424
  /** Edit an image with ImageMagick. */
12538
13425
  async imageEdit(opts) {
12539
13426
  const input = validateInputPath(opts.input);
12540
- const ext = safeExtension(opts.format, (0, import_node_path12.extname)(input).slice(1) || "png");
13427
+ const ext = safeExtension(opts.format, (0, import_node_path13.extname)(input).slice(1) || "png");
12541
13428
  const out = this.outPath("img", ext);
12542
13429
  switch (opts.action) {
12543
13430
  case "resize": {
@@ -12624,7 +13511,7 @@ var MediaManager = class {
12624
13511
  switch (opts.action) {
12625
13512
  case "trim": {
12626
13513
  const input = validateInputPath(opts.input);
12627
- const out = this.outPath("aud", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp3"));
13514
+ const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
12628
13515
  const a = ["-i", input];
12629
13516
  if (opts.start) a.push("-ss", String(opts.start));
12630
13517
  if (opts.end) a.push("-to", String(opts.end));
@@ -12645,8 +13532,8 @@ var MediaManager = class {
12645
13532
  if (files.length < 2) throw new Error("At least 2 files are required for merge");
12646
13533
  files.forEach((f, i) => validateInputPath(f, `files[${i}]`));
12647
13534
  const listFile = this.outPath("concat", "txt");
12648
- (0, import_node_fs11.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
12649
- const out = this.outPath("aud", safeExtension(null, (0, import_node_path12.extname)(files[0]).slice(1) || "mp3"));
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"));
12650
13537
  try {
12651
13538
  await this.ffmpeg(["-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", "-y", out]);
12652
13539
  } finally {
@@ -12657,7 +13544,7 @@ var MediaManager = class {
12657
13544
  case "volume": {
12658
13545
  const input = validateInputPath(opts.input);
12659
13546
  if (!opts.volume) throw new Error('volume is required (e.g. "1.5" or "10dB")');
12660
- const out = this.outPath("aud", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp3"));
13547
+ const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
12661
13548
  await this.ffmpeg(["-i", input, "-af", `volume=${opts.volume}`, "-y", out]);
12662
13549
  return this.fileResult(out);
12663
13550
  }
@@ -12665,7 +13552,7 @@ var MediaManager = class {
12665
13552
  const input = validateInputPath(opts.input);
12666
13553
  const factor = clampNumber(opts.speedFactor, 0.5, 100, 0);
12667
13554
  if (!factor) throw new Error("speedFactor is required for speed");
12668
- const out = this.outPath("aud", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp3"));
13555
+ const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
12669
13556
  await this.ffmpeg(["-i", input, "-af", `atempo=${factor}`, "-y", out]);
12670
13557
  return this.fileResult(out);
12671
13558
  }
@@ -12677,7 +13564,7 @@ var MediaManager = class {
12677
13564
  }
12678
13565
  case "reverse": {
12679
13566
  const input = validateInputPath(opts.input);
12680
- const out = this.outPath("aud", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp3"));
13567
+ const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
12681
13568
  await this.ffmpeg(["-i", input, "-af", "areverse", "-y", out]);
12682
13569
  return this.fileResult(out);
12683
13570
  }
@@ -12686,7 +13573,7 @@ var MediaManager = class {
12686
13573
  const dur = clampNumber(opts.fadeDuration, 0.1, 3600, 3);
12687
13574
  const probe = await this.ffprobe(input);
12688
13575
  const totalDur = parseFloat(probe.format?.duration || "0");
12689
- const out = this.outPath("aud", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp3"));
13576
+ const out = this.outPath("aud", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp3"));
12690
13577
  let af;
12691
13578
  if (opts.fadeType === "in") af = `afade=t=in:st=0:d=${dur}`;
12692
13579
  else if (opts.fadeType === "out") af = `afade=t=out:st=${Math.max(0, totalDur - dur)}:d=${dur}`;
@@ -12716,7 +13603,7 @@ var MediaManager = class {
12716
13603
  }));
12717
13604
  return {
12718
13605
  ok: true,
12719
- file: (0, import_node_path12.basename)(path2),
13606
+ file: (0, import_node_path13.basename)(path2),
12720
13607
  format: info.format?.format_long_name,
12721
13608
  duration: info.format?.duration,
12722
13609
  sizeBytes: parseInt(info.format?.size || "0", 10),
@@ -12729,7 +13616,7 @@ var MediaManager = class {
12729
13616
  async videoEdit(opts) {
12730
13617
  if (opts.action === "concatenate") return this.videoConcatenate(opts);
12731
13618
  const input = validateInputPath(opts.input);
12732
- const srcExt = safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp4");
13619
+ const srcExt = safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4");
12733
13620
  switch (opts.action) {
12734
13621
  case "trim": {
12735
13622
  const out = this.outPath("vid", srcExt);
@@ -12751,7 +13638,7 @@ var MediaManager = class {
12751
13638
  const dir2 = this.outDir("frames");
12752
13639
  const interval = clampNumber(opts.interval, 0.01, 3600, 1);
12753
13640
  await this.ffmpeg(
12754
- ["-i", input, "-vf", `fps=1/${interval}`, (0, import_node_path12.join)(dir2, "frame-%04d.png"), "-y"],
13641
+ ["-i", input, "-vf", `fps=1/${interval}`, (0, import_node_path13.join)(dir2, "frame-%04d.png"), "-y"],
12755
13642
  TIMEOUT_LONG
12756
13643
  );
12757
13644
  return { ok: true, filePath: dir2, sizeBytes: 0, outputDir: dir2 };
@@ -12852,7 +13739,7 @@ var MediaManager = class {
12852
13739
  }
12853
13740
  }
12854
13741
  async videoColorGrade(input, opts) {
12855
- const out = this.outPath("vid", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp4"));
13742
+ const out = this.outPath("vid", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4"));
12856
13743
  let vf;
12857
13744
  if (opts.lutPath) {
12858
13745
  const lut = validateInputPath(opts.lutPath, "lutPath");
@@ -12910,7 +13797,7 @@ var MediaManager = class {
12910
13797
  }
12911
13798
  async videoTextOverlay(input, opts) {
12912
13799
  if (!opts.text) throw new Error("text is required for text_overlay");
12913
- const out = this.outPath("vid", safeExtension(null, (0, import_node_path12.extname)(input).slice(1) || "mp4"));
13800
+ const out = this.outPath("vid", safeExtension(null, (0, import_node_path13.extname)(input).slice(1) || "mp4"));
12914
13801
  const probeV = await this.ffprobe(input);
12915
13802
  const vStream = (probeV.streams || []).find((s) => s.codec_type === "video");
12916
13803
  const vw = vStream?.width || 1920;
@@ -13179,7 +14066,7 @@ var MediaManager = class {
13179
14066
  files.forEach((f, i) => validateInputPath(f, `files[${i}]`));
13180
14067
  const out = this.outPath("vid", "mp4");
13181
14068
  const listFile = this.outPath("concat", "txt");
13182
- (0, import_node_fs11.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
14069
+ (0, import_node_fs12.writeFileSync)(listFile, files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n"));
13183
14070
  try {
13184
14071
  try {
13185
14072
  await this.ffmpeg(["-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", "-y", out], TIMEOUT_LONG);
@@ -13308,9 +14195,9 @@ var MediaManager = class {
13308
14195
  const bg = bgColors[i % bgColors.length];
13309
14196
  const sizeMult = chunk.wc <= 2 ? 1.4 : chunk.wc <= 3 ? 1.1 : 1;
13310
14197
  const fontSize = Math.round(baseFont * sizeMult);
13311
- const txtPng = (0, import_node_path12.join)(captionDir, `txt-${i}.png`);
13312
- const bgPng = (0, import_node_path12.join)(captionDir, `bg-${i}.png`);
13313
- const finalPng = (0, import_node_path12.join)(captionDir, `c-${String(i).padStart(4, "0")}.png`);
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`);
13314
14201
  await this.magick([
13315
14202
  "-size",
13316
14203
  `${maxTextW}x`,
@@ -13434,12 +14321,12 @@ var MediaManager = class {
13434
14321
  for (let i = 0; i < frameCount; i++) {
13435
14322
  const t = i * interval;
13436
14323
  if (t >= totalDur && totalDur > 0) break;
13437
- const framePath = (0, import_node_path12.join)(frameDir, `frame-${String(i).padStart(3, "0")}.jpg`);
14324
+ const framePath = (0, import_node_path13.join)(frameDir, `frame-${String(i).padStart(3, "0")}.jpg`);
13438
14325
  await this.ffmpeg(["-ss", String(t), "-i", input, "-frames:v", "1", "-q:v", "3", "-y", framePath], TIMEOUT_FAST);
13439
14326
  frames.push({ time: t, path: framePath });
13440
14327
  }
13441
14328
  const transcript = [];
13442
- if (opts.whisperModel && (0, import_node_fs11.existsSync)(opts.whisperModel) && detectBinary("whisper").available) {
14329
+ if (opts.whisperModel && (0, import_node_fs12.existsSync)(opts.whisperModel) && detectBinary("whisper").available) {
13443
14330
  const whisper = requireBinary("whisper");
13444
14331
  const wavPath = this.outPath("understand-audio", "wav");
13445
14332
  await this.ffmpeg(["-i", input, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", "-y", wavPath], TIMEOUT_FAST);
@@ -13475,7 +14362,7 @@ var MediaManager = class {
13475
14362
  });
13476
14363
  return {
13477
14364
  ok: true,
13478
- video: (0, import_node_path12.basename)(input),
14365
+ video: (0, import_node_path13.basename)(input),
13479
14366
  duration: totalDur,
13480
14367
  resolution: rotation ? `${vH}x${vW} (rotated ${rotation})` : `${vW}x${vH}`,
13481
14368
  totalFramesExtracted: frames.length,
@@ -13503,11 +14390,11 @@ var MediaManager = class {
13503
14390
  if (!opts.refText || typeof opts.refText !== "string") {
13504
14391
  throw new Error("refText is required for voice_clone (the transcript of the reference audio)");
13505
14392
  }
13506
- const pythonBin = opts.pythonBin && (0, import_node_path12.isAbsolute)(opts.pythonBin) ? validateInputPath(opts.pythonBin, "pythonBin") : requireBinary("python");
14393
+ const pythonBin = opts.pythonBin && (0, import_node_path13.isAbsolute)(opts.pythonBin) ? validateInputPath(opts.pythonBin, "pythonBin") : requireBinary("python");
13507
14394
  const device = typeof opts.device === "string" && /^[a-z0-9]+$/i.test(opts.device) ? opts.device : "cpu";
13508
14395
  const outWav = this.outPath("voiceclone", "wav");
13509
14396
  const paramsFile = this.outPath("voiceclone-params", "json");
13510
- (0, import_node_fs11.writeFileSync)(paramsFile, JSON.stringify({
14397
+ (0, import_node_fs12.writeFileSync)(paramsFile, JSON.stringify({
13511
14398
  ref_file: refAudio,
13512
14399
  ref_text: opts.refText,
13513
14400
  gen_text: opts.text,
@@ -13548,7 +14435,7 @@ var MediaManager = class {
13548
14435
  outOgg,
13549
14436
  "-y"
13550
14437
  ]);
13551
- if ((0, import_node_fs11.existsSync)(outOgg)) return this.fileResult(outOgg, { format: "ogg" });
14438
+ if ((0, import_node_fs12.existsSync)(outOgg)) return this.fileResult(outOgg, { format: "ogg" });
13552
14439
  } catch {
13553
14440
  }
13554
14441
  }
@@ -13558,18 +14445,18 @@ var MediaManager = class {
13558
14445
  /** Parse a whisper-produced SRT (located by stem) into timed segments. */
13559
14446
  parseSrt(srtStem) {
13560
14447
  let srtFile = `${srtStem}.srt`;
13561
- if (!(0, import_node_fs11.existsSync)(srtFile)) {
13562
- const dir2 = (0, import_node_path12.dirname)(srtStem);
13563
- const stem2 = (0, import_node_path12.basename)(srtStem);
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);
13564
14451
  try {
13565
- const candidates = (0, import_node_fs11.readdirSync)(dir2).filter((f) => f.includes(stem2) && f.endsWith(".srt"));
13566
- if (candidates.length > 0) srtFile = (0, import_node_path12.join)(dir2, candidates[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]);
13567
14454
  } catch {
13568
14455
  }
13569
14456
  }
13570
- if (!(0, import_node_fs11.existsSync)(srtFile)) return [];
14457
+ if (!(0, import_node_fs12.existsSync)(srtFile)) return [];
13571
14458
  const out = [];
13572
- const content = (0, import_node_fs11.readFileSync)(srtFile, "utf8");
14459
+ const content = (0, import_node_fs12.readFileSync)(srtFile, "utf8");
13573
14460
  for (const block of content.trim().split(/\n\n+/)) {
13574
14461
  const lines = block.trim().split("\n");
13575
14462
  if (lines.length < 3) continue;
@@ -13585,7 +14472,7 @@ var MediaManager = class {
13585
14472
  /** Unlink a file, swallowing any error (cleanup best-effort). */
13586
14473
  tryUnlink(path2) {
13587
14474
  try {
13588
- (0, import_node_fs11.unlinkSync)(path2);
14475
+ (0, import_node_fs12.unlinkSync)(path2);
13589
14476
  } catch {
13590
14477
  }
13591
14478
  }
@@ -13593,10 +14480,10 @@ var MediaManager = class {
13593
14480
  tryUnlinkSrt(srtStem) {
13594
14481
  this.tryUnlink(`${srtStem}.srt`);
13595
14482
  try {
13596
- const dir2 = (0, import_node_path12.dirname)(srtStem);
13597
- const stem2 = (0, import_node_path12.basename)(srtStem);
13598
- for (const f of (0, import_node_fs11.readdirSync)(dir2)) {
13599
- if (f.includes(stem2) && f.endsWith(".srt")) this.tryUnlink((0, import_node_path12.join)(dir2, f));
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));
13600
14487
  }
13601
14488
  } catch {
13602
14489
  }
@@ -13604,7 +14491,7 @@ var MediaManager = class {
13604
14491
  /** Recursively remove a directory, swallowing errors. */
13605
14492
  tryRmDir(dir2) {
13606
14493
  try {
13607
- (0, import_node_fs11.rmSync)(dir2, { recursive: true, force: true });
14494
+ (0, import_node_fs12.rmSync)(dir2, { recursive: true, force: true });
13608
14495
  } catch {
13609
14496
  }
13610
14497
  }
@@ -13644,10 +14531,10 @@ function threadIdFor(input) {
13644
14531
  }
13645
14532
 
13646
14533
  // src/threading/thread-cache.ts
13647
- var import_node_fs12 = require("fs");
13648
- var import_node_os10 = require("os");
13649
- var import_node_path13 = require("path");
13650
- var CACHE_DIR_DEFAULT = (0, import_node_path13.join)((0, import_node_os10.homedir)(), ".agenticmail", "thread-cache");
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");
13651
14538
  var DEFAULT_K_MESSAGES = 10;
13652
14539
  var DEFAULT_LRU_CAP = 5e3;
13653
14540
  var PREVIEW_MAX_CHARS = 240;
@@ -13660,22 +14547,22 @@ var ThreadCache = class {
13660
14547
  this.k = opts.k ?? DEFAULT_K_MESSAGES;
13661
14548
  this.lruCap = opts.lruCap ?? DEFAULT_LRU_CAP;
13662
14549
  try {
13663
- (0, import_node_fs12.mkdirSync)(this.dir, { recursive: true });
14550
+ (0, import_node_fs13.mkdirSync)(this.dir, { recursive: true });
13664
14551
  } catch {
13665
14552
  }
13666
14553
  }
13667
14554
  pathFor(threadId) {
13668
- return (0, import_node_path13.join)(this.dir, `${threadId}.json`);
14555
+ return (0, import_node_path14.join)(this.dir, `${threadId}.json`);
13669
14556
  }
13670
14557
  read(threadId) {
13671
14558
  const p = this.pathFor(threadId);
13672
- if (!(0, import_node_fs12.existsSync)(p)) return null;
14559
+ if (!(0, import_node_fs13.existsSync)(p)) return null;
13673
14560
  try {
13674
- const raw = (0, import_node_fs12.readFileSync)(p, "utf-8");
14561
+ const raw = (0, import_node_fs13.readFileSync)(p, "utf-8");
13675
14562
  return JSON.parse(raw);
13676
14563
  } catch {
13677
14564
  try {
13678
- (0, import_node_fs12.rmSync)(p, { force: true });
14565
+ (0, import_node_fs13.rmSync)(p, { force: true });
13679
14566
  } catch {
13680
14567
  }
13681
14568
  return null;
@@ -13716,7 +14603,7 @@ var ThreadCache = class {
13716
14603
  /** Permanently remove a thread's cache (called on [FINAL] / [DONE] / [CLOSED] / [WRAP]). */
13717
14604
  delete(threadId) {
13718
14605
  try {
13719
- (0, import_node_fs12.rmSync)(this.pathFor(threadId), { force: true });
14606
+ (0, import_node_fs13.rmSync)(this.pathFor(threadId), { force: true });
13720
14607
  } catch {
13721
14608
  }
13722
14609
  }
@@ -13736,8 +14623,8 @@ var ThreadCache = class {
13736
14623
  writeAtomic(threadId, entry) {
13737
14624
  const p = this.pathFor(threadId);
13738
14625
  const tmp = `${p}.tmp`;
13739
- (0, import_node_fs12.writeFileSync)(tmp, JSON.stringify(entry), "utf-8");
13740
- (0, import_node_fs12.renameSync)(tmp, p);
14626
+ (0, import_node_fs13.writeFileSync)(tmp, JSON.stringify(entry), "utf-8");
14627
+ (0, import_node_fs13.renameSync)(tmp, p);
13741
14628
  }
13742
14629
  /**
13743
14630
  * Best-effort LRU eviction. Runs at most every 256 writes (we
@@ -13749,15 +14636,15 @@ var ThreadCache = class {
13749
14636
  if (Math.random() > 1 / 256) return;
13750
14637
  let files;
13751
14638
  try {
13752
- files = (0, import_node_fs12.readdirSync)(this.dir).filter((f) => f.endsWith(".json"));
14639
+ files = (0, import_node_fs13.readdirSync)(this.dir).filter((f) => f.endsWith(".json"));
13753
14640
  } catch {
13754
14641
  return;
13755
14642
  }
13756
14643
  if (files.length <= this.lruCap) return;
13757
14644
  const stats = files.map((f) => {
13758
- const p = (0, import_node_path13.join)(this.dir, f);
14645
+ const p = (0, import_node_path14.join)(this.dir, f);
13759
14646
  try {
13760
- return { p, mtime: (0, import_node_fs12.statSync)(p).mtimeMs };
14647
+ return { p, mtime: (0, import_node_fs13.statSync)(p).mtimeMs };
13761
14648
  } catch {
13762
14649
  return { p, mtime: 0 };
13763
14650
  }
@@ -13766,7 +14653,7 @@ var ThreadCache = class {
13766
14653
  const dropCount = Math.max(1, Math.floor(this.lruCap * 0.1));
13767
14654
  for (let i = 0; i < dropCount; i++) {
13768
14655
  try {
13769
- (0, import_node_fs12.rmSync)(stats[i].p, { force: true });
14656
+ (0, import_node_fs13.rmSync)(stats[i].p, { force: true });
13770
14657
  } catch {
13771
14658
  }
13772
14659
  }
@@ -13785,30 +14672,30 @@ function dedupAndCap(messages, k) {
13785
14672
  }
13786
14673
 
13787
14674
  // src/threading/agent-memory.ts
13788
- var import_node_fs13 = require("fs");
13789
- var import_node_os11 = require("os");
13790
- var import_node_path14 = require("path");
13791
- var MEMORY_DIR_DEFAULT = (0, import_node_path14.join)((0, import_node_os11.homedir)(), ".agenticmail", "agent-memory");
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");
13792
14679
  var AgentMemoryStore = class {
13793
14680
  dir;
13794
14681
  constructor(opts = {}) {
13795
14682
  this.dir = opts.memoryDir ?? MEMORY_DIR_DEFAULT;
13796
14683
  try {
13797
- (0, import_node_fs13.mkdirSync)(this.dir, { recursive: true });
14684
+ (0, import_node_fs14.mkdirSync)(this.dir, { recursive: true });
13798
14685
  } catch {
13799
14686
  }
13800
14687
  }
13801
14688
  dirFor(agentId) {
13802
- return (0, import_node_path14.join)(this.dir, sanitizeId(agentId));
14689
+ return (0, import_node_path15.join)(this.dir, sanitizeId(agentId));
13803
14690
  }
13804
14691
  pathFor(agentId, threadId) {
13805
- return (0, import_node_path14.join)(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
14692
+ return (0, import_node_path15.join)(this.dirFor(agentId), `${sanitizeId(threadId)}.md`);
13806
14693
  }
13807
14694
  read(agentId, threadId) {
13808
14695
  const p = this.pathFor(agentId, threadId);
13809
- if (!(0, import_node_fs13.existsSync)(p)) return null;
14696
+ if (!(0, import_node_fs14.existsSync)(p)) return null;
13810
14697
  try {
13811
- const raw = (0, import_node_fs13.readFileSync)(p, "utf-8");
14698
+ const raw = (0, import_node_fs14.readFileSync)(p, "utf-8");
13812
14699
  const parsed = parse(raw);
13813
14700
  return { ...parsed, raw };
13814
14701
  } catch {
@@ -13818,18 +14705,18 @@ var AgentMemoryStore = class {
13818
14705
  write(agentId, threadId, fields) {
13819
14706
  const agentDir = this.dirFor(agentId);
13820
14707
  try {
13821
- (0, import_node_fs13.mkdirSync)(agentDir, { recursive: true });
14708
+ (0, import_node_fs14.mkdirSync)(agentDir, { recursive: true });
13822
14709
  } catch {
13823
14710
  }
13824
14711
  const body = render({ ...fields, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
13825
14712
  const p = this.pathFor(agentId, threadId);
13826
14713
  const tmp = `${p}.tmp`;
13827
- (0, import_node_fs13.writeFileSync)(tmp, body, "utf-8");
13828
- (0, import_node_fs13.renameSync)(tmp, p);
14714
+ (0, import_node_fs14.writeFileSync)(tmp, body, "utf-8");
14715
+ (0, import_node_fs14.renameSync)(tmp, p);
13829
14716
  }
13830
14717
  delete(agentId, threadId) {
13831
14718
  try {
13832
- (0, import_node_fs13.rmSync)(this.pathFor(agentId, threadId), { force: true });
14719
+ (0, import_node_fs14.rmSync)(this.pathFor(agentId, threadId), { force: true });
13833
14720
  } catch {
13834
14721
  }
13835
14722
  }
@@ -13887,400 +14774,7 @@ function parse(raw) {
13887
14774
 
13888
14775
  // src/memory/manager.ts
13889
14776
  var import_node_crypto8 = require("crypto");
13890
-
13891
- // src/memory/text-search.ts
13892
- var BM25_K1 = 1.2;
13893
- var BM25_B = 0.75;
13894
- var FIELD_WEIGHT_TITLE = 3;
13895
- var FIELD_WEIGHT_TAGS = 2;
13896
- var FIELD_WEIGHT_CONTENT = 1;
13897
- var PREFIX_MATCH_PENALTY = 0.7;
13898
- var STOP_WORDS = /* @__PURE__ */ new Set([
13899
- "a",
13900
- "about",
13901
- "above",
13902
- "after",
13903
- "again",
13904
- "against",
13905
- "all",
13906
- "am",
13907
- "an",
13908
- "and",
13909
- "any",
13910
- "are",
13911
- "as",
13912
- "at",
13913
- "be",
13914
- "because",
13915
- "been",
13916
- "before",
13917
- "being",
13918
- "below",
13919
- "between",
13920
- "both",
13921
- "but",
13922
- "by",
13923
- "can",
13924
- "could",
13925
- "did",
13926
- "do",
13927
- "does",
13928
- "doing",
13929
- "down",
13930
- "during",
13931
- "each",
13932
- "either",
13933
- "every",
13934
- "few",
13935
- "for",
13936
- "from",
13937
- "further",
13938
- "get",
13939
- "got",
13940
- "had",
13941
- "has",
13942
- "have",
13943
- "having",
13944
- "he",
13945
- "her",
13946
- "here",
13947
- "hers",
13948
- "herself",
13949
- "him",
13950
- "himself",
13951
- "his",
13952
- "how",
13953
- "i",
13954
- "if",
13955
- "in",
13956
- "into",
13957
- "is",
13958
- "it",
13959
- "its",
13960
- "itself",
13961
- "just",
13962
- "may",
13963
- "me",
13964
- "might",
13965
- "more",
13966
- "most",
13967
- "must",
13968
- "my",
13969
- "myself",
13970
- "neither",
13971
- "no",
13972
- "nor",
13973
- "not",
13974
- "now",
13975
- "of",
13976
- "off",
13977
- "on",
13978
- "once",
13979
- "only",
13980
- "or",
13981
- "other",
13982
- "ought",
13983
- "our",
13984
- "ours",
13985
- "ourselves",
13986
- "out",
13987
- "over",
13988
- "own",
13989
- "same",
13990
- "shall",
13991
- "she",
13992
- "should",
13993
- "so",
13994
- "some",
13995
- "such",
13996
- "than",
13997
- "that",
13998
- "the",
13999
- "their",
14000
- "theirs",
14001
- "them",
14002
- "themselves",
14003
- "then",
14004
- "there",
14005
- "these",
14006
- "they",
14007
- "this",
14008
- "those",
14009
- "through",
14010
- "to",
14011
- "too",
14012
- "under",
14013
- "until",
14014
- "up",
14015
- "us",
14016
- "very",
14017
- "was",
14018
- "we",
14019
- "were",
14020
- "what",
14021
- "when",
14022
- "where",
14023
- "which",
14024
- "while",
14025
- "who",
14026
- "whom",
14027
- "why",
14028
- "will",
14029
- "with",
14030
- "would",
14031
- "yet",
14032
- "you",
14033
- "your",
14034
- "yours",
14035
- "yourself",
14036
- "yourselves"
14037
- ]);
14038
- var STEM_RULES = [
14039
- // Step 1: plurals and past participles
14040
- [/ies$/, "i", 3],
14041
- // policies → polici,eries → eri
14042
- [/sses$/, "ss", 4],
14043
- // addresses → address
14044
- [/([^s])s$/, "$1", 3],
14045
- // items → item, but not "ss"
14046
- [/eed$/, "ee", 4],
14047
- // agreed → agree
14048
- [/ed$/, "", 3],
14049
- // configured → configur, but min length 3
14050
- [/ing$/, "", 4],
14051
- // running → runn → run (handled below)
14052
- // Step 2: derivational suffixes
14053
- [/ational$/, "ate", 6],
14054
- // relational → relate
14055
- [/tion$/, "t", 5],
14056
- // adoption → adopt
14057
- [/ness$/, "", 5],
14058
- // awareness → aware
14059
- [/ment$/, "", 5],
14060
- // deployment → deploy
14061
- [/able$/, "", 5],
14062
- // configurable → configur
14063
- [/ible$/, "", 5],
14064
- // accessible → access
14065
- [/ful$/, "", 5],
14066
- // powerful → power
14067
- [/ous$/, "", 5],
14068
- // dangerous → danger
14069
- [/ive$/, "", 5],
14070
- // interactive → interact
14071
- [/ize$/, "", 4],
14072
- // normalize → normal
14073
- [/ise$/, "", 4],
14074
- // organise → organ
14075
- [/ally$/, "", 5],
14076
- // automatically → automat
14077
- [/ly$/, "", 4],
14078
- // quickly → quick
14079
- [/er$/, "", 4]
14080
- // handler → handl
14081
- ];
14082
- var DOUBLE_CONSONANT = /([^aeiou])\1$/;
14083
- function stem(word) {
14084
- if (word.length < 3) return word;
14085
- let stemmed = word;
14086
- for (const [pattern, replacement, minLen] of STEM_RULES) {
14087
- if (stemmed.length >= minLen && pattern.test(stemmed)) {
14088
- stemmed = stemmed.replace(pattern, replacement);
14089
- break;
14090
- }
14091
- }
14092
- if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
14093
- stemmed = stemmed.slice(0, -1);
14094
- }
14095
- return stemmed;
14096
- }
14097
- function tokenize(text) {
14098
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
14099
- }
14100
- var MemorySearchIndex = class {
14101
- /** Posting lists: stemmed term → Set of memory IDs containing it */
14102
- postings = /* @__PURE__ */ new Map();
14103
- /** Per-document metadata for BM25 scoring */
14104
- docs = /* @__PURE__ */ new Map();
14105
- /** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
14106
- idf = /* @__PURE__ */ new Map();
14107
- idfStale = true;
14108
- /** 3-character prefix map for prefix matching: prefix → Set of full stems */
14109
- prefixMap = /* @__PURE__ */ new Map();
14110
- /** Total weighted document length (for computing average) */
14111
- totalWeightedLen = 0;
14112
- get docCount() {
14113
- return this.docs.size;
14114
- }
14115
- get avgDocLen() {
14116
- return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
14117
- }
14118
- /**
14119
- * Index a memory entry. Extracts stems from title, content, and tags
14120
- * with field-specific weighting and builds posting lists.
14121
- */
14122
- addDocument(id, entry) {
14123
- if (this.docs.has(id)) this.removeDocument(id);
14124
- const titleTokens = tokenize(entry.title);
14125
- const contentTokens = tokenize(entry.content);
14126
- const tagTokens = entry.tags.flatMap((t) => tokenize(t));
14127
- const weightedTf = /* @__PURE__ */ new Map();
14128
- for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
14129
- for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
14130
- for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
14131
- const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
14132
- const allStems = /* @__PURE__ */ new Set();
14133
- for (const t of weightedTf.keys()) allStems.add(t);
14134
- const stemSequence = [...titleTokens, ...contentTokens];
14135
- const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
14136
- this.docs.set(id, docRecord);
14137
- this.totalWeightedLen += weightedLen;
14138
- for (const term of allStems) {
14139
- let posting = this.postings.get(term);
14140
- if (!posting) {
14141
- posting = /* @__PURE__ */ new Set();
14142
- this.postings.set(term, posting);
14143
- }
14144
- posting.add(id);
14145
- if (term.length >= 3) {
14146
- const prefix = term.slice(0, 3);
14147
- let prefixSet = this.prefixMap.get(prefix);
14148
- if (!prefixSet) {
14149
- prefixSet = /* @__PURE__ */ new Set();
14150
- this.prefixMap.set(prefix, prefixSet);
14151
- }
14152
- prefixSet.add(term);
14153
- }
14154
- }
14155
- this.idfStale = true;
14156
- }
14157
- /** Remove a document from the index. */
14158
- removeDocument(id) {
14159
- const doc = this.docs.get(id);
14160
- if (!doc) return;
14161
- this.totalWeightedLen -= doc.weightedLen;
14162
- this.docs.delete(id);
14163
- for (const term of doc.allStems) {
14164
- const posting = this.postings.get(term);
14165
- if (posting) {
14166
- posting.delete(id);
14167
- if (posting.size === 0) {
14168
- this.postings.delete(term);
14169
- if (term.length >= 3) {
14170
- const prefixSet = this.prefixMap.get(term.slice(0, 3));
14171
- if (prefixSet) {
14172
- prefixSet.delete(term);
14173
- if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
14174
- }
14175
- }
14176
- }
14177
- }
14178
- }
14179
- this.idfStale = true;
14180
- }
14181
- /** Recompute IDF values for all terms. Called lazily before search. */
14182
- refreshIdf() {
14183
- if (!this.idfStale) return;
14184
- const N = this.docs.size;
14185
- this.idf.clear();
14186
- for (const [term, posting] of this.postings) {
14187
- const df = posting.size;
14188
- this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
14189
- }
14190
- this.idfStale = false;
14191
- }
14192
- /**
14193
- * Expand query terms with prefix matches.
14194
- * "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
14195
- */
14196
- expandQueryTerms(queryStems) {
14197
- const expanded = /* @__PURE__ */ new Map();
14198
- for (const qs of queryStems) {
14199
- if (this.postings.has(qs)) {
14200
- expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
14201
- }
14202
- if (qs.length >= 3) {
14203
- const prefix = qs.slice(0, 3);
14204
- const candidates = this.prefixMap.get(prefix);
14205
- if (candidates) {
14206
- for (const candidate of candidates) {
14207
- if (candidate !== qs && candidate.startsWith(qs)) {
14208
- expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
14209
- }
14210
- }
14211
- }
14212
- }
14213
- }
14214
- return expanded;
14215
- }
14216
- /**
14217
- * Compute bigram proximity boost: if two query terms appear adjacent
14218
- * in the document's stem sequence, boost the score.
14219
- */
14220
- bigramProximityBoost(docId, queryStems) {
14221
- if (queryStems.length < 2) return 0;
14222
- const doc = this.docs.get(docId);
14223
- if (!doc || doc.stemSequence.length < 2) return 0;
14224
- let boost = 0;
14225
- const seq = doc.stemSequence;
14226
- const querySet = new Set(queryStems);
14227
- for (let i = 0; i < seq.length - 1; i++) {
14228
- if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
14229
- boost += 0.5;
14230
- }
14231
- }
14232
- return Math.min(boost, 2);
14233
- }
14234
- /**
14235
- * Search the index for documents matching a query.
14236
- * Returns scored results sorted by BM25F relevance.
14237
- *
14238
- * @param query - Raw query string
14239
- * @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
14240
- * @returns Array of { id, score } sorted by descending score
14241
- */
14242
- search(query, candidateIds) {
14243
- const queryStems = tokenize(query);
14244
- if (queryStems.length === 0) return [];
14245
- this.refreshIdf();
14246
- const expandedTerms = this.expandQueryTerms(queryStems);
14247
- if (expandedTerms.size === 0) return [];
14248
- const avgDl = this.avgDocLen;
14249
- const candidates = /* @__PURE__ */ new Set();
14250
- for (const term of expandedTerms.keys()) {
14251
- const posting = this.postings.get(term);
14252
- if (posting) {
14253
- for (const docId of posting) {
14254
- if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
14255
- }
14256
- }
14257
- }
14258
- const results = [];
14259
- for (const docId of candidates) {
14260
- const doc = this.docs.get(docId);
14261
- if (!doc) continue;
14262
- let score = 0;
14263
- for (const [term, weight] of expandedTerms) {
14264
- const tf = doc.weightedTf.get(term) || 0;
14265
- if (tf === 0) continue;
14266
- const termIdf = this.idf.get(term) || 0;
14267
- const numerator = tf * (BM25_K1 + 1);
14268
- const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
14269
- score += termIdf * (numerator / denominator) * weight;
14270
- }
14271
- score += this.bigramProximityBoost(docId, queryStems);
14272
- if (score > 0) results.push({ id: docId, score });
14273
- }
14274
- results.sort((a, b) => b.score - a.score);
14275
- return results;
14276
- }
14277
- /** Check if a document exists in the index. */
14278
- has(id) {
14279
- return this.docs.has(id);
14280
- }
14281
- };
14282
-
14283
- // src/memory/manager.ts
14777
+ init_text_search();
14284
14778
  function sj(v, fb = {}) {
14285
14779
  if (!v) return fb;
14286
14780
  try {
@@ -14763,6 +15257,12 @@ var AgentMemoryManager = class {
14763
15257
  };
14764
15258
  }
14765
15259
  };
15260
+
15261
+ // src/memory/index.ts
15262
+ init_text_search();
15263
+
15264
+ // src/index.ts
15265
+ init_skills();
14766
15266
  // Annotate the CommonJS export names for ESM import in node:
14767
15267
  0 && (module.exports = {
14768
15268
  AGENT_ROLES,
@@ -14793,6 +15293,7 @@ var AgentMemoryManager = class {
14793
15293
  GET_DATETIME_TOOL,
14794
15294
  GatewayManager,
14795
15295
  InboxWatcher,
15296
+ LOAD_SKILL_TOOL,
14796
15297
  MEMORY_CATEGORIES,
14797
15298
  MailReceiver,
14798
15299
  MailSender,
@@ -14829,6 +15330,7 @@ var AgentMemoryManager = class {
14829
15330
  RelayBridge,
14830
15331
  RelayGateway,
14831
15332
  SEARCH_EMAIL_TOOL,
15333
+ SEARCH_SKILLS_TOOL,
14832
15334
  SPAM_THRESHOLD,
14833
15335
  ServiceManager,
14834
15336
  SetupManager,
@@ -14908,6 +15410,7 @@ var AgentMemoryManager = class {
14908
15410
  getTelegramWebhookInfo,
14909
15411
  hostSessionStoragePath,
14910
15412
  inferPhoneRegion,
15413
+ invalidateSkillCache,
14911
15414
  isInternalEmail,
14912
15415
  isLoopbackMailHost,
14913
15416
  isOperatorReplySender,
@@ -14916,7 +15419,9 @@ var AgentMemoryManager = class {
14916
15419
  isTelegramChatAllowed,
14917
15420
  isTelegramStopCommand,
14918
15421
  isValidPhoneNumber,
15422
+ listSkills,
14919
15423
  loadHostSession,
15424
+ loadSkill,
14920
15425
  mapProviderSmsStatus,
14921
15426
  nextTelegramOffset,
14922
15427
  normalizeAddress,
@@ -14941,6 +15446,7 @@ var AgentMemoryManager = class {
14941
15446
  redactSecret,
14942
15447
  redactSmsConfig,
14943
15448
  redactTelegramConfig,
15449
+ renderSkillAsPrompt,
14944
15450
  requireBinary,
14945
15451
  requireWhisperModel,
14946
15452
  resolveConfig,
@@ -14949,8 +15455,10 @@ var AgentMemoryManager = class {
14949
15455
  sanitizeEmail,
14950
15456
  saveConfig,
14951
15457
  saveHostSession,
15458
+ saveUserSkill,
14952
15459
  scanOutboundEmail,
14953
15460
  scoreEmail,
15461
+ searchSkills,
14954
15462
  sendTelegramMessage,
14955
15463
  setOperatorEmail,
14956
15464
  setTelegramWebhook,
@@ -14963,10 +15471,12 @@ var AgentMemoryManager = class {
14963
15471
  threadIdFor,
14964
15472
  tokenize,
14965
15473
  tryJoin,
15474
+ userSkillsDir,
14966
15475
  validateApiUrl,
14967
15476
  validatePhoneMissionPolicy,
14968
15477
  validatePhoneMissionStart,
14969
15478
  validatePhoneTransportProfile,
15479
+ validateSkill,
14970
15480
  validateTwilioSignature,
14971
15481
  webSearch
14972
15482
  });