@agenticmail/core 0.9.24 → 0.9.26

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