@agenticmail/core 0.9.24 → 0.9.25

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