@agenticmail/core 0.9.24 → 0.9.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-J6JINNJ3.js +724 -0
- package/dist/index.cjs +1142 -927
- package/dist/index.d.cts +94 -9
- package/dist/index.d.ts +94 -9
- package/dist/index.js +176 -696
- package/dist/skills-RE3S767B.js +23 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,19 @@ import {
|
|
|
4
4
|
isInternalEmail,
|
|
5
5
|
scoreEmail
|
|
6
6
|
} from "./chunk-TIAKW5DC.js";
|
|
7
|
+
import {
|
|
8
|
+
MemorySearchIndex,
|
|
9
|
+
invalidateSkillCache,
|
|
10
|
+
listSkills,
|
|
11
|
+
loadSkill,
|
|
12
|
+
renderSkillAsPrompt,
|
|
13
|
+
saveUserSkill,
|
|
14
|
+
searchSkills,
|
|
15
|
+
stem,
|
|
16
|
+
tokenize,
|
|
17
|
+
userSkillsDir,
|
|
18
|
+
validateSkill
|
|
19
|
+
} from "./chunk-J6JINNJ3.js";
|
|
7
20
|
import {
|
|
8
21
|
__require
|
|
9
22
|
} from "./chunk-3RG5ZIWI.js";
|
|
@@ -1109,14 +1122,14 @@ var StalwartAdmin = class {
|
|
|
1109
1122
|
if (!isValidDomain(domain)) {
|
|
1110
1123
|
throw new Error(`Invalid domain format: "${domain}"`);
|
|
1111
1124
|
}
|
|
1112
|
-
const { readFileSync:
|
|
1113
|
-
const { homedir:
|
|
1114
|
-
const { join:
|
|
1115
|
-
const configPath =
|
|
1125
|
+
const { readFileSync: readFileSync10, writeFileSync: writeFileSync11 } = await import("fs");
|
|
1126
|
+
const { homedir: homedir13 } = await import("os");
|
|
1127
|
+
const { join: join16 } = await import("path");
|
|
1128
|
+
const configPath = join16(homedir13(), ".agenticmail", "stalwart.toml");
|
|
1116
1129
|
try {
|
|
1117
|
-
let config =
|
|
1130
|
+
let config = readFileSync10(configPath, "utf-8");
|
|
1118
1131
|
config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
|
|
1119
|
-
|
|
1132
|
+
writeFileSync11(configPath, config);
|
|
1120
1133
|
console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
|
|
1121
1134
|
} catch (err) {
|
|
1122
1135
|
throw new Error(`Failed to set config server.hostname=${domain}`);
|
|
@@ -1125,15 +1138,15 @@ var StalwartAdmin = class {
|
|
|
1125
1138
|
// --- DKIM ---
|
|
1126
1139
|
/** Path to the host-side stalwart.toml (mounted read-only into container) */
|
|
1127
1140
|
get configPath() {
|
|
1128
|
-
const { homedir:
|
|
1129
|
-
const { join:
|
|
1130
|
-
return
|
|
1141
|
+
const { homedir: homedir13 } = __require("os");
|
|
1142
|
+
const { join: join16 } = __require("path");
|
|
1143
|
+
return join16(homedir13(), ".agenticmail", "stalwart.toml");
|
|
1131
1144
|
}
|
|
1132
1145
|
/** Path to host-side DKIM key directory */
|
|
1133
1146
|
get dkimDir() {
|
|
1134
|
-
const { homedir:
|
|
1135
|
-
const { join:
|
|
1136
|
-
return
|
|
1147
|
+
const { homedir: homedir13 } = __require("os");
|
|
1148
|
+
const { join: join16 } = __require("path");
|
|
1149
|
+
return join16(homedir13(), ".agenticmail");
|
|
1137
1150
|
}
|
|
1138
1151
|
/**
|
|
1139
1152
|
* Create/reuse a DKIM signing key for a domain.
|
|
@@ -1234,12 +1247,12 @@ var StalwartAdmin = class {
|
|
|
1234
1247
|
* This bypasses the need for a PTR record on the sending IP.
|
|
1235
1248
|
*/
|
|
1236
1249
|
async configureOutboundRelay(config) {
|
|
1237
|
-
const { readFileSync:
|
|
1238
|
-
const { homedir:
|
|
1239
|
-
const { join:
|
|
1250
|
+
const { readFileSync: readFileSync10, writeFileSync: writeFileSync11 } = await import("fs");
|
|
1251
|
+
const { homedir: homedir13 } = await import("os");
|
|
1252
|
+
const { join: join16 } = await import("path");
|
|
1240
1253
|
const routeName = config.routeName ?? "gmail";
|
|
1241
|
-
const tomlPath =
|
|
1242
|
-
let toml =
|
|
1254
|
+
const tomlPath = join16(homedir13(), ".agenticmail", "stalwart.toml");
|
|
1255
|
+
let toml = readFileSync10(tomlPath, "utf-8");
|
|
1243
1256
|
toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
|
|
1244
1257
|
toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
|
|
1245
1258
|
const safeRouteName = routeName.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
@@ -1259,7 +1272,7 @@ auth.secret = "${escapeTomlString(config.password)}"
|
|
|
1259
1272
|
route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
|
|
1260
1273
|
{ else = "'${safeRouteName}'" } ]
|
|
1261
1274
|
`;
|
|
1262
|
-
|
|
1275
|
+
writeFileSync11(tomlPath, toml, "utf-8");
|
|
1263
1276
|
await this.restartContainer();
|
|
1264
1277
|
}
|
|
1265
1278
|
};
|
|
@@ -7460,12 +7473,12 @@ var GatewayManager = class {
|
|
|
7460
7473
|
zone = await this.cfClient.createZone(domain);
|
|
7461
7474
|
}
|
|
7462
7475
|
const existingRecords = await this.cfClient.listDnsRecords(zone.id);
|
|
7463
|
-
const { homedir:
|
|
7464
|
-
const backupDir = join4(
|
|
7476
|
+
const { homedir: homedir13 } = await import("os");
|
|
7477
|
+
const backupDir = join4(homedir13(), ".agenticmail");
|
|
7465
7478
|
const backupPath = join4(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
|
|
7466
|
-
const { writeFileSync:
|
|
7467
|
-
|
|
7468
|
-
|
|
7479
|
+
const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync12 } = await import("fs");
|
|
7480
|
+
mkdirSync12(backupDir, { recursive: true });
|
|
7481
|
+
writeFileSync11(backupPath, JSON.stringify({
|
|
7469
7482
|
domain,
|
|
7470
7483
|
zoneId: zone.id,
|
|
7471
7484
|
backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8749,12 +8762,46 @@ var SEARCH_EMAIL_TOOL = {
|
|
|
8749
8762
|
additionalProperties: false
|
|
8750
8763
|
}
|
|
8751
8764
|
};
|
|
8765
|
+
var SEARCH_SKILLS_TOOL = {
|
|
8766
|
+
type: "function",
|
|
8767
|
+
name: "search_skills",
|
|
8768
|
+
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.",
|
|
8769
|
+
parameters: {
|
|
8770
|
+
type: "object",
|
|
8771
|
+
properties: {
|
|
8772
|
+
query: {
|
|
8773
|
+
type: "string",
|
|
8774
|
+
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".'
|
|
8775
|
+
}
|
|
8776
|
+
},
|
|
8777
|
+
required: ["query"],
|
|
8778
|
+
additionalProperties: false
|
|
8779
|
+
}
|
|
8780
|
+
};
|
|
8781
|
+
var LOAD_SKILL_TOOL = {
|
|
8782
|
+
type: "function",
|
|
8783
|
+
name: "load_skill",
|
|
8784
|
+
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.',
|
|
8785
|
+
parameters: {
|
|
8786
|
+
type: "object",
|
|
8787
|
+
properties: {
|
|
8788
|
+
id: {
|
|
8789
|
+
type: "string",
|
|
8790
|
+
description: 'Skill id (lowercase-hyphenated), e.g. "negotiate-bill-reduction". Get it from search_skills.'
|
|
8791
|
+
}
|
|
8792
|
+
},
|
|
8793
|
+
required: ["id"],
|
|
8794
|
+
additionalProperties: false
|
|
8795
|
+
}
|
|
8796
|
+
};
|
|
8752
8797
|
var REALTIME_TOOL_DEFINITIONS = {
|
|
8753
8798
|
ask_operator: ASK_OPERATOR_TOOL,
|
|
8754
8799
|
web_search: WEB_SEARCH_TOOL,
|
|
8755
8800
|
recall_memory: RECALL_MEMORY_TOOL,
|
|
8756
8801
|
get_datetime: GET_DATETIME_TOOL,
|
|
8757
|
-
search_email: SEARCH_EMAIL_TOOL
|
|
8802
|
+
search_email: SEARCH_EMAIL_TOOL,
|
|
8803
|
+
search_skills: SEARCH_SKILLS_TOOL,
|
|
8804
|
+
load_skill: LOAD_SKILL_TOOL
|
|
8758
8805
|
};
|
|
8759
8806
|
function buildRealtimeToolGuidance(tools) {
|
|
8760
8807
|
if (tools.length === 0) return "";
|
|
@@ -8773,6 +8820,16 @@ function buildRealtimeToolGuidance(tools) {
|
|
|
8773
8820
|
'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.'
|
|
8774
8821
|
);
|
|
8775
8822
|
}
|
|
8823
|
+
if (names.has("search_skills") && names.has("load_skill")) {
|
|
8824
|
+
lines.push(
|
|
8825
|
+
`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:
|
|
8826
|
+
1. Tell the caller you need a moment: "Hold on one moment \u2014 let me check something."
|
|
8827
|
+
2. Call search_skills with a one-line description of the situation.
|
|
8828
|
+
3. Call load_skill with the id of the best match.
|
|
8829
|
+
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.
|
|
8830
|
+
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.`
|
|
8831
|
+
);
|
|
8832
|
+
}
|
|
8776
8833
|
return lines.join("\n");
|
|
8777
8834
|
}
|
|
8778
8835
|
function toolErrorText(err) {
|
|
@@ -8958,6 +9015,7 @@ var REALTIME_AUDIO_SAMPLE_RATE = 24e3;
|
|
|
8958
9015
|
var REALTIME_MAX_AUDIO_FRAME_BASE64 = 256 * 1024;
|
|
8959
9016
|
var MAX_PENDING_AUDIO_FRAMES = 200;
|
|
8960
9017
|
var REALTIME_TOOL_CALL_TIMEOUT_MS = 6 * 6e4;
|
|
9018
|
+
var MAX_LOADED_SKILLS = 2;
|
|
8961
9019
|
var MAX_IN_FLIGHT_TOOL_CALLS = 8;
|
|
8962
9020
|
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.";
|
|
8963
9021
|
function buildRealtimeInstructions(opts) {
|
|
@@ -9049,6 +9107,26 @@ var RealtimeVoiceBridge = class {
|
|
|
9049
9107
|
toolCallNames = /* @__PURE__ */ new Map();
|
|
9050
9108
|
/** `call_id`s whose tool call is currently executing. */
|
|
9051
9109
|
inFlightToolCalls = /* @__PURE__ */ new Set();
|
|
9110
|
+
/**
|
|
9111
|
+
* Mid-call skills loaded into the session so far, FIFO. Earliest at
|
|
9112
|
+
* index 0; cap at {@link MAX_LOADED_SKILLS}. When a (cap+1)th skill
|
|
9113
|
+
* is loaded the oldest one drops out — the model can't usefully
|
|
9114
|
+
* hold five playbooks in working memory at once, so we keep the
|
|
9115
|
+
* working set narrow on purpose.
|
|
9116
|
+
*/
|
|
9117
|
+
loadedSkills = [];
|
|
9118
|
+
/**
|
|
9119
|
+
* The original `instructions` string from the session.update sent at
|
|
9120
|
+
* open. We keep a private copy because every mid-call skill load
|
|
9121
|
+
* issues a fresh `session.update` whose `instructions` is built as:
|
|
9122
|
+
*
|
|
9123
|
+
* baseInstructions + "\n\n" + renderedSkill1 + "\n\n" + renderedSkill2 …
|
|
9124
|
+
*
|
|
9125
|
+
* Without this snapshot, successive loads would compound — the second
|
|
9126
|
+
* load would see "base + skill1" as the base and append skill2 to
|
|
9127
|
+
* THAT, eventually drifting unboundedly.
|
|
9128
|
+
*/
|
|
9129
|
+
baseInstructions = "";
|
|
9052
9130
|
constructor(opts) {
|
|
9053
9131
|
const carrier = opts.carrier ?? opts.elks;
|
|
9054
9132
|
if (!carrier) {
|
|
@@ -9085,6 +9163,10 @@ var RealtimeVoiceBridge = class {
|
|
|
9085
9163
|
handleOpenAIOpen() {
|
|
9086
9164
|
if (this.ended || this.openaiReady) return;
|
|
9087
9165
|
this.openaiReady = true;
|
|
9166
|
+
const sess = this.sessionConfig?.session;
|
|
9167
|
+
if (sess && typeof sess.instructions === "string") {
|
|
9168
|
+
this.baseInstructions = sess.instructions;
|
|
9169
|
+
}
|
|
9088
9170
|
this.safeSend(this.openai, this.sessionConfig);
|
|
9089
9171
|
this.safeSend(this.openai, { type: "response.create" });
|
|
9090
9172
|
for (const audio of this.pendingAudio.splice(0)) {
|
|
@@ -9095,6 +9177,74 @@ var RealtimeVoiceBridge = class {
|
|
|
9095
9177
|
handleOpenAIClose() {
|
|
9096
9178
|
this.end("openai-closed");
|
|
9097
9179
|
}
|
|
9180
|
+
/**
|
|
9181
|
+
* Load a skill playbook into the live OpenAI Realtime session for
|
|
9182
|
+
* the rest of the call.
|
|
9183
|
+
*
|
|
9184
|
+
* Mechanics:
|
|
9185
|
+
* 1. Resolve the skill JSON via the skills registry (file on disk).
|
|
9186
|
+
* 2. Append the rendered skill text to the agent's working
|
|
9187
|
+
* instructions and re-send a `session.update` carrying ONLY
|
|
9188
|
+
* the new `instructions` field. The OpenAI Realtime API
|
|
9189
|
+
* supports partial session.update — we don't have to re-send
|
|
9190
|
+
* audio config, tools, voice, etc.
|
|
9191
|
+
* 3. Track which skills are loaded so we (a) FIFO-evict the
|
|
9192
|
+
* oldest when the cap is hit and (b) include every still-
|
|
9193
|
+
* loaded skill in the next composed instructions.
|
|
9194
|
+
* 4. Emit a transcript marker so the mission record shows the
|
|
9195
|
+
* adaptation ("[skill loaded: Negotiate a Bill Reduction
|
|
9196
|
+
* v1.0.0]"). Useful for post-call review and for the build
|
|
9197
|
+
* farm's telemetry on which skills actually got reached for.
|
|
9198
|
+
*
|
|
9199
|
+
* Returns an object the {@link load_skill} tool handler can serialise
|
|
9200
|
+
* back to the model: `ok: true` plus the skill name + version on
|
|
9201
|
+
* success, `ok: false` plus a short reason on failure (unknown id,
|
|
9202
|
+
* call ended, registry I/O error). Never throws — a buggy registry
|
|
9203
|
+
* or a missing file must not crash the bridge mid-call.
|
|
9204
|
+
*
|
|
9205
|
+
* Phase 2 of the skill library (`docs/skill-library-plan.md`).
|
|
9206
|
+
*/
|
|
9207
|
+
async loadSkillIntoSession(skillId) {
|
|
9208
|
+
if (this.ended) return { ok: false, message: "Call has already ended; cannot load a skill now." };
|
|
9209
|
+
if (!this.openaiReady) return { ok: false, message: "Session is not ready yet; try again in a moment." };
|
|
9210
|
+
if (this.loadedSkills.some((s) => s.id === skillId)) {
|
|
9211
|
+
const existing = this.loadedSkills.find((s) => s.id === skillId);
|
|
9212
|
+
return { ok: true, message: `Skill "${skillId}" is already loaded.`, name: skillId, version: existing.version };
|
|
9213
|
+
}
|
|
9214
|
+
let loadSkill2;
|
|
9215
|
+
let renderSkillAsPrompt2;
|
|
9216
|
+
try {
|
|
9217
|
+
({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await import("./skills-RE3S767B.js"));
|
|
9218
|
+
} catch (err) {
|
|
9219
|
+
return { ok: false, message: `Skill registry unavailable: ${errorText(err)}` };
|
|
9220
|
+
}
|
|
9221
|
+
const skill = loadSkill2(skillId);
|
|
9222
|
+
if (!skill) {
|
|
9223
|
+
return { ok: false, message: `No skill found with id "${skillId}". Call search_skills first to find the right id.` };
|
|
9224
|
+
}
|
|
9225
|
+
const rendered = renderSkillAsPrompt2(skill);
|
|
9226
|
+
while (this.loadedSkills.length >= MAX_LOADED_SKILLS) {
|
|
9227
|
+
const dropped = this.loadedSkills.shift();
|
|
9228
|
+
if (dropped) {
|
|
9229
|
+
this.emitTranscript("system", `[skill unloaded for working-memory limit: ${dropped.id} v${dropped.version}]`);
|
|
9230
|
+
}
|
|
9231
|
+
}
|
|
9232
|
+
this.loadedSkills.push({ id: skill.id, version: skill.version, renderedPrompt: rendered });
|
|
9233
|
+
const composed = [
|
|
9234
|
+
this.baseInstructions,
|
|
9235
|
+
...this.loadedSkills.map((s) => s.renderedPrompt)
|
|
9236
|
+
].filter((s) => s && s.length > 0).join("\n\n");
|
|
9237
|
+
this.safeSend(this.openai, {
|
|
9238
|
+
type: "session.update",
|
|
9239
|
+
session: { instructions: composed }
|
|
9240
|
+
});
|
|
9241
|
+
this.emitTranscript("system", `[skill loaded: ${skill.name} v${skill.version}]`);
|
|
9242
|
+
return { ok: true, message: `Loaded skill: ${skill.name} (v${skill.version})`, name: skill.name, version: skill.version };
|
|
9243
|
+
}
|
|
9244
|
+
/** The list of skills currently loaded into the session (FIFO-ordered). */
|
|
9245
|
+
get loadedSkillIds() {
|
|
9246
|
+
return this.loadedSkills.map((s) => s.id);
|
|
9247
|
+
}
|
|
9098
9248
|
/** Call when the OpenAI socket errors. */
|
|
9099
9249
|
handleOpenAIError(err) {
|
|
9100
9250
|
this.emitTranscript("system", `OpenAI Realtime error: ${errorText(err)}`);
|
|
@@ -12998,400 +13148,6 @@ function parse(raw) {
|
|
|
12998
13148
|
|
|
12999
13149
|
// src/memory/manager.ts
|
|
13000
13150
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
13001
|
-
|
|
13002
|
-
// src/memory/text-search.ts
|
|
13003
|
-
var BM25_K1 = 1.2;
|
|
13004
|
-
var BM25_B = 0.75;
|
|
13005
|
-
var FIELD_WEIGHT_TITLE = 3;
|
|
13006
|
-
var FIELD_WEIGHT_TAGS = 2;
|
|
13007
|
-
var FIELD_WEIGHT_CONTENT = 1;
|
|
13008
|
-
var PREFIX_MATCH_PENALTY = 0.7;
|
|
13009
|
-
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
13010
|
-
"a",
|
|
13011
|
-
"about",
|
|
13012
|
-
"above",
|
|
13013
|
-
"after",
|
|
13014
|
-
"again",
|
|
13015
|
-
"against",
|
|
13016
|
-
"all",
|
|
13017
|
-
"am",
|
|
13018
|
-
"an",
|
|
13019
|
-
"and",
|
|
13020
|
-
"any",
|
|
13021
|
-
"are",
|
|
13022
|
-
"as",
|
|
13023
|
-
"at",
|
|
13024
|
-
"be",
|
|
13025
|
-
"because",
|
|
13026
|
-
"been",
|
|
13027
|
-
"before",
|
|
13028
|
-
"being",
|
|
13029
|
-
"below",
|
|
13030
|
-
"between",
|
|
13031
|
-
"both",
|
|
13032
|
-
"but",
|
|
13033
|
-
"by",
|
|
13034
|
-
"can",
|
|
13035
|
-
"could",
|
|
13036
|
-
"did",
|
|
13037
|
-
"do",
|
|
13038
|
-
"does",
|
|
13039
|
-
"doing",
|
|
13040
|
-
"down",
|
|
13041
|
-
"during",
|
|
13042
|
-
"each",
|
|
13043
|
-
"either",
|
|
13044
|
-
"every",
|
|
13045
|
-
"few",
|
|
13046
|
-
"for",
|
|
13047
|
-
"from",
|
|
13048
|
-
"further",
|
|
13049
|
-
"get",
|
|
13050
|
-
"got",
|
|
13051
|
-
"had",
|
|
13052
|
-
"has",
|
|
13053
|
-
"have",
|
|
13054
|
-
"having",
|
|
13055
|
-
"he",
|
|
13056
|
-
"her",
|
|
13057
|
-
"here",
|
|
13058
|
-
"hers",
|
|
13059
|
-
"herself",
|
|
13060
|
-
"him",
|
|
13061
|
-
"himself",
|
|
13062
|
-
"his",
|
|
13063
|
-
"how",
|
|
13064
|
-
"i",
|
|
13065
|
-
"if",
|
|
13066
|
-
"in",
|
|
13067
|
-
"into",
|
|
13068
|
-
"is",
|
|
13069
|
-
"it",
|
|
13070
|
-
"its",
|
|
13071
|
-
"itself",
|
|
13072
|
-
"just",
|
|
13073
|
-
"may",
|
|
13074
|
-
"me",
|
|
13075
|
-
"might",
|
|
13076
|
-
"more",
|
|
13077
|
-
"most",
|
|
13078
|
-
"must",
|
|
13079
|
-
"my",
|
|
13080
|
-
"myself",
|
|
13081
|
-
"neither",
|
|
13082
|
-
"no",
|
|
13083
|
-
"nor",
|
|
13084
|
-
"not",
|
|
13085
|
-
"now",
|
|
13086
|
-
"of",
|
|
13087
|
-
"off",
|
|
13088
|
-
"on",
|
|
13089
|
-
"once",
|
|
13090
|
-
"only",
|
|
13091
|
-
"or",
|
|
13092
|
-
"other",
|
|
13093
|
-
"ought",
|
|
13094
|
-
"our",
|
|
13095
|
-
"ours",
|
|
13096
|
-
"ourselves",
|
|
13097
|
-
"out",
|
|
13098
|
-
"over",
|
|
13099
|
-
"own",
|
|
13100
|
-
"same",
|
|
13101
|
-
"shall",
|
|
13102
|
-
"she",
|
|
13103
|
-
"should",
|
|
13104
|
-
"so",
|
|
13105
|
-
"some",
|
|
13106
|
-
"such",
|
|
13107
|
-
"than",
|
|
13108
|
-
"that",
|
|
13109
|
-
"the",
|
|
13110
|
-
"their",
|
|
13111
|
-
"theirs",
|
|
13112
|
-
"them",
|
|
13113
|
-
"themselves",
|
|
13114
|
-
"then",
|
|
13115
|
-
"there",
|
|
13116
|
-
"these",
|
|
13117
|
-
"they",
|
|
13118
|
-
"this",
|
|
13119
|
-
"those",
|
|
13120
|
-
"through",
|
|
13121
|
-
"to",
|
|
13122
|
-
"too",
|
|
13123
|
-
"under",
|
|
13124
|
-
"until",
|
|
13125
|
-
"up",
|
|
13126
|
-
"us",
|
|
13127
|
-
"very",
|
|
13128
|
-
"was",
|
|
13129
|
-
"we",
|
|
13130
|
-
"were",
|
|
13131
|
-
"what",
|
|
13132
|
-
"when",
|
|
13133
|
-
"where",
|
|
13134
|
-
"which",
|
|
13135
|
-
"while",
|
|
13136
|
-
"who",
|
|
13137
|
-
"whom",
|
|
13138
|
-
"why",
|
|
13139
|
-
"will",
|
|
13140
|
-
"with",
|
|
13141
|
-
"would",
|
|
13142
|
-
"yet",
|
|
13143
|
-
"you",
|
|
13144
|
-
"your",
|
|
13145
|
-
"yours",
|
|
13146
|
-
"yourself",
|
|
13147
|
-
"yourselves"
|
|
13148
|
-
]);
|
|
13149
|
-
var STEM_RULES = [
|
|
13150
|
-
// Step 1: plurals and past participles
|
|
13151
|
-
[/ies$/, "i", 3],
|
|
13152
|
-
// policies → polici,eries → eri
|
|
13153
|
-
[/sses$/, "ss", 4],
|
|
13154
|
-
// addresses → address
|
|
13155
|
-
[/([^s])s$/, "$1", 3],
|
|
13156
|
-
// items → item, but not "ss"
|
|
13157
|
-
[/eed$/, "ee", 4],
|
|
13158
|
-
// agreed → agree
|
|
13159
|
-
[/ed$/, "", 3],
|
|
13160
|
-
// configured → configur, but min length 3
|
|
13161
|
-
[/ing$/, "", 4],
|
|
13162
|
-
// running → runn → run (handled below)
|
|
13163
|
-
// Step 2: derivational suffixes
|
|
13164
|
-
[/ational$/, "ate", 6],
|
|
13165
|
-
// relational → relate
|
|
13166
|
-
[/tion$/, "t", 5],
|
|
13167
|
-
// adoption → adopt
|
|
13168
|
-
[/ness$/, "", 5],
|
|
13169
|
-
// awareness → aware
|
|
13170
|
-
[/ment$/, "", 5],
|
|
13171
|
-
// deployment → deploy
|
|
13172
|
-
[/able$/, "", 5],
|
|
13173
|
-
// configurable → configur
|
|
13174
|
-
[/ible$/, "", 5],
|
|
13175
|
-
// accessible → access
|
|
13176
|
-
[/ful$/, "", 5],
|
|
13177
|
-
// powerful → power
|
|
13178
|
-
[/ous$/, "", 5],
|
|
13179
|
-
// dangerous → danger
|
|
13180
|
-
[/ive$/, "", 5],
|
|
13181
|
-
// interactive → interact
|
|
13182
|
-
[/ize$/, "", 4],
|
|
13183
|
-
// normalize → normal
|
|
13184
|
-
[/ise$/, "", 4],
|
|
13185
|
-
// organise → organ
|
|
13186
|
-
[/ally$/, "", 5],
|
|
13187
|
-
// automatically → automat
|
|
13188
|
-
[/ly$/, "", 4],
|
|
13189
|
-
// quickly → quick
|
|
13190
|
-
[/er$/, "", 4]
|
|
13191
|
-
// handler → handl
|
|
13192
|
-
];
|
|
13193
|
-
var DOUBLE_CONSONANT = /([^aeiou])\1$/;
|
|
13194
|
-
function stem(word) {
|
|
13195
|
-
if (word.length < 3) return word;
|
|
13196
|
-
let stemmed = word;
|
|
13197
|
-
for (const [pattern, replacement, minLen] of STEM_RULES) {
|
|
13198
|
-
if (stemmed.length >= minLen && pattern.test(stemmed)) {
|
|
13199
|
-
stemmed = stemmed.replace(pattern, replacement);
|
|
13200
|
-
break;
|
|
13201
|
-
}
|
|
13202
|
-
}
|
|
13203
|
-
if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
|
|
13204
|
-
stemmed = stemmed.slice(0, -1);
|
|
13205
|
-
}
|
|
13206
|
-
return stemmed;
|
|
13207
|
-
}
|
|
13208
|
-
function tokenize(text) {
|
|
13209
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
|
|
13210
|
-
}
|
|
13211
|
-
var MemorySearchIndex = class {
|
|
13212
|
-
/** Posting lists: stemmed term → Set of memory IDs containing it */
|
|
13213
|
-
postings = /* @__PURE__ */ new Map();
|
|
13214
|
-
/** Per-document metadata for BM25 scoring */
|
|
13215
|
-
docs = /* @__PURE__ */ new Map();
|
|
13216
|
-
/** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
|
|
13217
|
-
idf = /* @__PURE__ */ new Map();
|
|
13218
|
-
idfStale = true;
|
|
13219
|
-
/** 3-character prefix map for prefix matching: prefix → Set of full stems */
|
|
13220
|
-
prefixMap = /* @__PURE__ */ new Map();
|
|
13221
|
-
/** Total weighted document length (for computing average) */
|
|
13222
|
-
totalWeightedLen = 0;
|
|
13223
|
-
get docCount() {
|
|
13224
|
-
return this.docs.size;
|
|
13225
|
-
}
|
|
13226
|
-
get avgDocLen() {
|
|
13227
|
-
return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
|
|
13228
|
-
}
|
|
13229
|
-
/**
|
|
13230
|
-
* Index a memory entry. Extracts stems from title, content, and tags
|
|
13231
|
-
* with field-specific weighting and builds posting lists.
|
|
13232
|
-
*/
|
|
13233
|
-
addDocument(id, entry) {
|
|
13234
|
-
if (this.docs.has(id)) this.removeDocument(id);
|
|
13235
|
-
const titleTokens = tokenize(entry.title);
|
|
13236
|
-
const contentTokens = tokenize(entry.content);
|
|
13237
|
-
const tagTokens = entry.tags.flatMap((t) => tokenize(t));
|
|
13238
|
-
const weightedTf = /* @__PURE__ */ new Map();
|
|
13239
|
-
for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
|
|
13240
|
-
for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
|
|
13241
|
-
for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
|
|
13242
|
-
const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
|
|
13243
|
-
const allStems = /* @__PURE__ */ new Set();
|
|
13244
|
-
for (const t of weightedTf.keys()) allStems.add(t);
|
|
13245
|
-
const stemSequence = [...titleTokens, ...contentTokens];
|
|
13246
|
-
const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
|
|
13247
|
-
this.docs.set(id, docRecord);
|
|
13248
|
-
this.totalWeightedLen += weightedLen;
|
|
13249
|
-
for (const term of allStems) {
|
|
13250
|
-
let posting = this.postings.get(term);
|
|
13251
|
-
if (!posting) {
|
|
13252
|
-
posting = /* @__PURE__ */ new Set();
|
|
13253
|
-
this.postings.set(term, posting);
|
|
13254
|
-
}
|
|
13255
|
-
posting.add(id);
|
|
13256
|
-
if (term.length >= 3) {
|
|
13257
|
-
const prefix = term.slice(0, 3);
|
|
13258
|
-
let prefixSet = this.prefixMap.get(prefix);
|
|
13259
|
-
if (!prefixSet) {
|
|
13260
|
-
prefixSet = /* @__PURE__ */ new Set();
|
|
13261
|
-
this.prefixMap.set(prefix, prefixSet);
|
|
13262
|
-
}
|
|
13263
|
-
prefixSet.add(term);
|
|
13264
|
-
}
|
|
13265
|
-
}
|
|
13266
|
-
this.idfStale = true;
|
|
13267
|
-
}
|
|
13268
|
-
/** Remove a document from the index. */
|
|
13269
|
-
removeDocument(id) {
|
|
13270
|
-
const doc = this.docs.get(id);
|
|
13271
|
-
if (!doc) return;
|
|
13272
|
-
this.totalWeightedLen -= doc.weightedLen;
|
|
13273
|
-
this.docs.delete(id);
|
|
13274
|
-
for (const term of doc.allStems) {
|
|
13275
|
-
const posting = this.postings.get(term);
|
|
13276
|
-
if (posting) {
|
|
13277
|
-
posting.delete(id);
|
|
13278
|
-
if (posting.size === 0) {
|
|
13279
|
-
this.postings.delete(term);
|
|
13280
|
-
if (term.length >= 3) {
|
|
13281
|
-
const prefixSet = this.prefixMap.get(term.slice(0, 3));
|
|
13282
|
-
if (prefixSet) {
|
|
13283
|
-
prefixSet.delete(term);
|
|
13284
|
-
if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
|
|
13285
|
-
}
|
|
13286
|
-
}
|
|
13287
|
-
}
|
|
13288
|
-
}
|
|
13289
|
-
}
|
|
13290
|
-
this.idfStale = true;
|
|
13291
|
-
}
|
|
13292
|
-
/** Recompute IDF values for all terms. Called lazily before search. */
|
|
13293
|
-
refreshIdf() {
|
|
13294
|
-
if (!this.idfStale) return;
|
|
13295
|
-
const N = this.docs.size;
|
|
13296
|
-
this.idf.clear();
|
|
13297
|
-
for (const [term, posting] of this.postings) {
|
|
13298
|
-
const df = posting.size;
|
|
13299
|
-
this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
|
|
13300
|
-
}
|
|
13301
|
-
this.idfStale = false;
|
|
13302
|
-
}
|
|
13303
|
-
/**
|
|
13304
|
-
* Expand query terms with prefix matches.
|
|
13305
|
-
* "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
|
|
13306
|
-
*/
|
|
13307
|
-
expandQueryTerms(queryStems) {
|
|
13308
|
-
const expanded = /* @__PURE__ */ new Map();
|
|
13309
|
-
for (const qs of queryStems) {
|
|
13310
|
-
if (this.postings.has(qs)) {
|
|
13311
|
-
expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
|
|
13312
|
-
}
|
|
13313
|
-
if (qs.length >= 3) {
|
|
13314
|
-
const prefix = qs.slice(0, 3);
|
|
13315
|
-
const candidates = this.prefixMap.get(prefix);
|
|
13316
|
-
if (candidates) {
|
|
13317
|
-
for (const candidate of candidates) {
|
|
13318
|
-
if (candidate !== qs && candidate.startsWith(qs)) {
|
|
13319
|
-
expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
|
|
13320
|
-
}
|
|
13321
|
-
}
|
|
13322
|
-
}
|
|
13323
|
-
}
|
|
13324
|
-
}
|
|
13325
|
-
return expanded;
|
|
13326
|
-
}
|
|
13327
|
-
/**
|
|
13328
|
-
* Compute bigram proximity boost: if two query terms appear adjacent
|
|
13329
|
-
* in the document's stem sequence, boost the score.
|
|
13330
|
-
*/
|
|
13331
|
-
bigramProximityBoost(docId, queryStems) {
|
|
13332
|
-
if (queryStems.length < 2) return 0;
|
|
13333
|
-
const doc = this.docs.get(docId);
|
|
13334
|
-
if (!doc || doc.stemSequence.length < 2) return 0;
|
|
13335
|
-
let boost = 0;
|
|
13336
|
-
const seq = doc.stemSequence;
|
|
13337
|
-
const querySet = new Set(queryStems);
|
|
13338
|
-
for (let i = 0; i < seq.length - 1; i++) {
|
|
13339
|
-
if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
|
|
13340
|
-
boost += 0.5;
|
|
13341
|
-
}
|
|
13342
|
-
}
|
|
13343
|
-
return Math.min(boost, 2);
|
|
13344
|
-
}
|
|
13345
|
-
/**
|
|
13346
|
-
* Search the index for documents matching a query.
|
|
13347
|
-
* Returns scored results sorted by BM25F relevance.
|
|
13348
|
-
*
|
|
13349
|
-
* @param query - Raw query string
|
|
13350
|
-
* @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
|
|
13351
|
-
* @returns Array of { id, score } sorted by descending score
|
|
13352
|
-
*/
|
|
13353
|
-
search(query, candidateIds) {
|
|
13354
|
-
const queryStems = tokenize(query);
|
|
13355
|
-
if (queryStems.length === 0) return [];
|
|
13356
|
-
this.refreshIdf();
|
|
13357
|
-
const expandedTerms = this.expandQueryTerms(queryStems);
|
|
13358
|
-
if (expandedTerms.size === 0) return [];
|
|
13359
|
-
const avgDl = this.avgDocLen;
|
|
13360
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
13361
|
-
for (const term of expandedTerms.keys()) {
|
|
13362
|
-
const posting = this.postings.get(term);
|
|
13363
|
-
if (posting) {
|
|
13364
|
-
for (const docId of posting) {
|
|
13365
|
-
if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
|
|
13366
|
-
}
|
|
13367
|
-
}
|
|
13368
|
-
}
|
|
13369
|
-
const results = [];
|
|
13370
|
-
for (const docId of candidates) {
|
|
13371
|
-
const doc = this.docs.get(docId);
|
|
13372
|
-
if (!doc) continue;
|
|
13373
|
-
let score = 0;
|
|
13374
|
-
for (const [term, weight] of expandedTerms) {
|
|
13375
|
-
const tf = doc.weightedTf.get(term) || 0;
|
|
13376
|
-
if (tf === 0) continue;
|
|
13377
|
-
const termIdf = this.idf.get(term) || 0;
|
|
13378
|
-
const numerator = tf * (BM25_K1 + 1);
|
|
13379
|
-
const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
|
|
13380
|
-
score += termIdf * (numerator / denominator) * weight;
|
|
13381
|
-
}
|
|
13382
|
-
score += this.bigramProximityBoost(docId, queryStems);
|
|
13383
|
-
if (score > 0) results.push({ id: docId, score });
|
|
13384
|
-
}
|
|
13385
|
-
results.sort((a, b) => b.score - a.score);
|
|
13386
|
-
return results;
|
|
13387
|
-
}
|
|
13388
|
-
/** Check if a document exists in the index. */
|
|
13389
|
-
has(id) {
|
|
13390
|
-
return this.docs.has(id);
|
|
13391
|
-
}
|
|
13392
|
-
};
|
|
13393
|
-
|
|
13394
|
-
// src/memory/manager.ts
|
|
13395
13151
|
function sj(v, fb = {}) {
|
|
13396
13152
|
if (!v) return fb;
|
|
13397
13153
|
try {
|
|
@@ -13874,284 +13630,6 @@ var AgentMemoryManager = class {
|
|
|
13874
13630
|
};
|
|
13875
13631
|
}
|
|
13876
13632
|
};
|
|
13877
|
-
|
|
13878
|
-
// src/skills/registry.ts
|
|
13879
|
-
import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync11 } from "fs";
|
|
13880
|
-
import { join as join16, dirname as dirname3, basename as basename2 } from "path";
|
|
13881
|
-
import { fileURLToPath } from "url";
|
|
13882
|
-
import { homedir as homedir13 } from "os";
|
|
13883
|
-
function builtInDir() {
|
|
13884
|
-
const here = dirname3(fileURLToPath(import.meta.url));
|
|
13885
|
-
return join16(here, "built-in");
|
|
13886
|
-
}
|
|
13887
|
-
function userDir() {
|
|
13888
|
-
const dir2 = join16(homedir13(), ".agenticmail", "skills");
|
|
13889
|
-
try {
|
|
13890
|
-
if (!existsSync14(dir2)) mkdirSync12(dir2, { recursive: true });
|
|
13891
|
-
} catch {
|
|
13892
|
-
}
|
|
13893
|
-
return dir2;
|
|
13894
|
-
}
|
|
13895
|
-
var cache = { ts: 0, byId: null };
|
|
13896
|
-
var CACHE_TTL_MS = 5e3;
|
|
13897
|
-
function loadAllSkillsFromDisk() {
|
|
13898
|
-
const all = /* @__PURE__ */ new Map();
|
|
13899
|
-
const dirs = [builtInDir(), userDir()];
|
|
13900
|
-
for (const dir2 of dirs) {
|
|
13901
|
-
if (!existsSync14(dir2)) continue;
|
|
13902
|
-
let entries;
|
|
13903
|
-
try {
|
|
13904
|
-
entries = readdirSync3(dir2);
|
|
13905
|
-
} catch {
|
|
13906
|
-
continue;
|
|
13907
|
-
}
|
|
13908
|
-
for (const entry of entries) {
|
|
13909
|
-
if (!entry.endsWith(".json")) continue;
|
|
13910
|
-
const fullPath = join16(dir2, entry);
|
|
13911
|
-
try {
|
|
13912
|
-
const st = statSync3(fullPath);
|
|
13913
|
-
if (!st.isFile()) continue;
|
|
13914
|
-
const raw = readFileSync10(fullPath, "utf-8");
|
|
13915
|
-
const parsed = JSON.parse(raw);
|
|
13916
|
-
const errors = validateSkill(parsed);
|
|
13917
|
-
if (errors.length > 0) {
|
|
13918
|
-
console.warn(`[skills] ${entry} invalid, skipping: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
13919
|
-
continue;
|
|
13920
|
-
}
|
|
13921
|
-
all.set(parsed.id, parsed);
|
|
13922
|
-
} catch (err) {
|
|
13923
|
-
console.warn(`[skills] could not load ${fullPath}: ${err.message}`);
|
|
13924
|
-
}
|
|
13925
|
-
}
|
|
13926
|
-
}
|
|
13927
|
-
return all;
|
|
13928
|
-
}
|
|
13929
|
-
function ensureLoaded() {
|
|
13930
|
-
const now = Date.now();
|
|
13931
|
-
if (cache.byId && now - cache.ts < CACHE_TTL_MS) return cache.byId;
|
|
13932
|
-
const fresh = loadAllSkillsFromDisk();
|
|
13933
|
-
cache.byId = fresh;
|
|
13934
|
-
cache.ts = now;
|
|
13935
|
-
return fresh;
|
|
13936
|
-
}
|
|
13937
|
-
function invalidateSkillCache() {
|
|
13938
|
-
cache.byId = null;
|
|
13939
|
-
cache.ts = 0;
|
|
13940
|
-
}
|
|
13941
|
-
function validateSkill(s) {
|
|
13942
|
-
const errs = [];
|
|
13943
|
-
if (!s || typeof s !== "object" || Array.isArray(s)) {
|
|
13944
|
-
return [{ path: "$", message: "skill must be a JSON object" }];
|
|
13945
|
-
}
|
|
13946
|
-
const sk = s;
|
|
13947
|
-
const requireString = (key, minLen = 1) => {
|
|
13948
|
-
const v = sk[key];
|
|
13949
|
-
if (typeof v !== "string" || v.length < minLen) {
|
|
13950
|
-
errs.push({ path: key, message: `must be a non-empty string` });
|
|
13951
|
-
}
|
|
13952
|
-
};
|
|
13953
|
-
const requireArray = (key, minLen = 1) => {
|
|
13954
|
-
const v = sk[key];
|
|
13955
|
-
if (!Array.isArray(v) || v.length < minLen) {
|
|
13956
|
-
errs.push({ path: key, message: `must be a non-empty array` });
|
|
13957
|
-
}
|
|
13958
|
-
};
|
|
13959
|
-
requireString("id");
|
|
13960
|
-
if (typeof sk.id === "string" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(sk.id)) {
|
|
13961
|
-
errs.push({ path: "id", message: "must be lowercase-hyphenated slug (a-z, 0-9, -)" });
|
|
13962
|
-
}
|
|
13963
|
-
requireString("name");
|
|
13964
|
-
requireString("version");
|
|
13965
|
-
requireString("description");
|
|
13966
|
-
requireString("category");
|
|
13967
|
-
const validCategories = [
|
|
13968
|
-
"negotiation",
|
|
13969
|
-
"customer-service",
|
|
13970
|
-
"reservations",
|
|
13971
|
-
"medical-admin",
|
|
13972
|
-
"legal-admin",
|
|
13973
|
-
"finance-admin",
|
|
13974
|
-
"real-estate",
|
|
13975
|
-
"travel",
|
|
13976
|
-
"subscription",
|
|
13977
|
-
"home-services",
|
|
13978
|
-
"social",
|
|
13979
|
-
"civic",
|
|
13980
|
-
"employment",
|
|
13981
|
-
"debt-collection",
|
|
13982
|
-
"other"
|
|
13983
|
-
];
|
|
13984
|
-
if (typeof sk.category === "string" && !validCategories.includes(sk.category)) {
|
|
13985
|
-
errs.push({ path: "category", message: `unknown category "${sk.category}"; must be one of: ${validCategories.join(", ")}` });
|
|
13986
|
-
}
|
|
13987
|
-
if (!Array.isArray(sk.tags)) errs.push({ path: "tags", message: "must be an array of strings" });
|
|
13988
|
-
if (sk.disclaimer !== null && typeof sk.disclaimer !== "string") {
|
|
13989
|
-
errs.push({ path: "disclaimer", message: "must be a string or null" });
|
|
13990
|
-
}
|
|
13991
|
-
if (!sk.context || typeof sk.context !== "object") {
|
|
13992
|
-
errs.push({ path: "context", message: "must be an object" });
|
|
13993
|
-
} else {
|
|
13994
|
-
const ctx = sk.context;
|
|
13995
|
-
if (typeof ctx.when_to_use !== "string") errs.push({ path: "context.when_to_use", message: "must be a string" });
|
|
13996
|
-
if (!Array.isArray(ctx.preconditions)) errs.push({ path: "context.preconditions", message: "must be an array" });
|
|
13997
|
-
if (typeof ctx.estimated_call_duration_minutes !== "number") errs.push({ path: "context.estimated_call_duration_minutes", message: "must be a number" });
|
|
13998
|
-
}
|
|
13999
|
-
requireArray("principles", 2);
|
|
14000
|
-
if (!sk.phrases || typeof sk.phrases !== "object") errs.push({ path: "phrases", message: "must be an object of {key: phrase}" });
|
|
14001
|
-
if (!Array.isArray(sk.tactics) || sk.tactics.length === 0) {
|
|
14002
|
-
errs.push({ path: "tactics", message: "must be a non-empty array" });
|
|
14003
|
-
} else {
|
|
14004
|
-
sk.tactics.forEach((t, i) => {
|
|
14005
|
-
if (!t || typeof t !== "object") {
|
|
14006
|
-
errs.push({ path: `tactics[${i}]`, message: "must be an object" });
|
|
14007
|
-
return;
|
|
14008
|
-
}
|
|
14009
|
-
const tactic = t;
|
|
14010
|
-
if (typeof tactic.name !== "string") errs.push({ path: `tactics[${i}].name`, message: "must be a string" });
|
|
14011
|
-
if (typeof tactic.when !== "string") errs.push({ path: `tactics[${i}].when`, message: "must be a string" });
|
|
14012
|
-
if (typeof tactic.script !== "string" || tactic.script.length < 5) {
|
|
14013
|
-
errs.push({ path: `tactics[${i}].script`, message: "must be a non-empty string (>= 5 chars)" });
|
|
14014
|
-
}
|
|
14015
|
-
});
|
|
14016
|
-
}
|
|
14017
|
-
requireArray("boundaries", 1);
|
|
14018
|
-
if (!Array.isArray(sk.success_signals)) errs.push({ path: "success_signals", message: "must be an array" });
|
|
14019
|
-
if (!Array.isArray(sk.failure_signals)) errs.push({ path: "failure_signals", message: "must be an array" });
|
|
14020
|
-
if (!sk.exit_strategy || typeof sk.exit_strategy !== "object") {
|
|
14021
|
-
errs.push({ path: "exit_strategy", message: "must be an object" });
|
|
14022
|
-
} else {
|
|
14023
|
-
const xs = sk.exit_strategy;
|
|
14024
|
-
if (typeof xs.on_success !== "string") errs.push({ path: "exit_strategy.on_success", message: "must be a string" });
|
|
14025
|
-
if (typeof xs.on_failure !== "string") errs.push({ path: "exit_strategy.on_failure", message: "must be a string" });
|
|
14026
|
-
}
|
|
14027
|
-
if (!Array.isArray(sk.required_user_info)) errs.push({ path: "required_user_info", message: "must be an array" });
|
|
14028
|
-
if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
|
|
14029
|
-
return errs;
|
|
14030
|
-
}
|
|
14031
|
-
function summarize(s) {
|
|
14032
|
-
return {
|
|
14033
|
-
id: s.id,
|
|
14034
|
-
name: s.name,
|
|
14035
|
-
category: s.category,
|
|
14036
|
-
tags: s.tags,
|
|
14037
|
-
description: s.description,
|
|
14038
|
-
version: s.version,
|
|
14039
|
-
disclaimer_required: !!s.disclaimer,
|
|
14040
|
-
estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
|
|
14041
|
-
};
|
|
14042
|
-
}
|
|
14043
|
-
function listSkills(opts = {}) {
|
|
14044
|
-
const all = Array.from(ensureLoaded().values());
|
|
14045
|
-
const filtered = all.filter((s) => {
|
|
14046
|
-
if (opts.category && s.category !== opts.category) return false;
|
|
14047
|
-
if (opts.tag && !s.tags.includes(opts.tag.toLowerCase())) return false;
|
|
14048
|
-
return true;
|
|
14049
|
-
});
|
|
14050
|
-
return filtered.sort((a, b) => a.name.localeCompare(b.name)).map(summarize);
|
|
14051
|
-
}
|
|
14052
|
-
function searchSkills(query, limit = 20) {
|
|
14053
|
-
const q = query.toLowerCase().trim();
|
|
14054
|
-
if (!q) return listSkills();
|
|
14055
|
-
const all = Array.from(ensureLoaded().values());
|
|
14056
|
-
const scored = [];
|
|
14057
|
-
for (const s of all) {
|
|
14058
|
-
let score = 0;
|
|
14059
|
-
if (s.id.toLowerCase().includes(q)) score += 10;
|
|
14060
|
-
if (s.name.toLowerCase().includes(q)) score += 8;
|
|
14061
|
-
if (s.tags.some((t) => t.toLowerCase().includes(q))) score += 5;
|
|
14062
|
-
if (s.category.toLowerCase().includes(q)) score += 4;
|
|
14063
|
-
if (s.description.toLowerCase().includes(q)) score += 3;
|
|
14064
|
-
if (s.principles.some((p) => p.toLowerCase().includes(q))) score += 2;
|
|
14065
|
-
if (Object.values(s.phrases).some((p) => p.toLowerCase().includes(q))) score += 2;
|
|
14066
|
-
if (s.tactics.some((t) => t.script.toLowerCase().includes(q) || t.name.toLowerCase().includes(q))) score += 2;
|
|
14067
|
-
if (score > 0) scored.push({ skill: s, score });
|
|
14068
|
-
}
|
|
14069
|
-
scored.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
14070
|
-
return scored.slice(0, limit).map((x) => summarize(x.skill));
|
|
14071
|
-
}
|
|
14072
|
-
function loadSkill(id) {
|
|
14073
|
-
return ensureLoaded().get(id) ?? null;
|
|
14074
|
-
}
|
|
14075
|
-
function saveUserSkill(skill) {
|
|
14076
|
-
const errors = validateSkill(skill);
|
|
14077
|
-
if (errors.length > 0) {
|
|
14078
|
-
throw new Error(`skill validation failed: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
14079
|
-
}
|
|
14080
|
-
const dir2 = userDir();
|
|
14081
|
-
const path2 = join16(dir2, `${skill.id}.json`);
|
|
14082
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14083
|
-
const out = {
|
|
14084
|
-
...skill,
|
|
14085
|
-
created_at: skill.created_at ?? now,
|
|
14086
|
-
updated_at: now
|
|
14087
|
-
};
|
|
14088
|
-
writeFileSync11(path2, JSON.stringify(out, null, 2), "utf-8");
|
|
14089
|
-
invalidateSkillCache();
|
|
14090
|
-
return { path: path2 };
|
|
14091
|
-
}
|
|
14092
|
-
function userSkillsDir() {
|
|
14093
|
-
return userDir();
|
|
14094
|
-
}
|
|
14095
|
-
|
|
14096
|
-
// src/skills/index.ts
|
|
14097
|
-
function renderSkillAsPrompt(skill) {
|
|
14098
|
-
const lines = [];
|
|
14099
|
-
lines.push(`=== SKILL LOADED: ${skill.name} (v${skill.version}) ===`);
|
|
14100
|
-
lines.push(`Category: ${skill.category} Tags: ${skill.tags.join(", ")}`);
|
|
14101
|
-
lines.push("");
|
|
14102
|
-
lines.push(skill.description);
|
|
14103
|
-
lines.push("");
|
|
14104
|
-
if (skill.disclaimer) {
|
|
14105
|
-
lines.push("REQUIRED DISCLAIMER (recite at start of the substantive turn):");
|
|
14106
|
-
lines.push(` "${skill.disclaimer}"`);
|
|
14107
|
-
lines.push("");
|
|
14108
|
-
}
|
|
14109
|
-
lines.push("WHEN TO USE THIS:");
|
|
14110
|
-
lines.push(` ${skill.context.when_to_use}`);
|
|
14111
|
-
if (skill.context.preconditions.length > 0) {
|
|
14112
|
-
lines.push("Preconditions:");
|
|
14113
|
-
for (const p of skill.context.preconditions) lines.push(` - ${p}`);
|
|
14114
|
-
}
|
|
14115
|
-
lines.push("");
|
|
14116
|
-
lines.push("PRINCIPLES:");
|
|
14117
|
-
for (const p of skill.principles) lines.push(` - ${p}`);
|
|
14118
|
-
lines.push("");
|
|
14119
|
-
if (Object.keys(skill.phrases).length > 0) {
|
|
14120
|
-
lines.push("PHRASES (paraphrase to match your voice; keep the structural move):");
|
|
14121
|
-
for (const [k, v] of Object.entries(skill.phrases)) lines.push(` [${k}] "${v}"`);
|
|
14122
|
-
lines.push("");
|
|
14123
|
-
}
|
|
14124
|
-
if (skill.tactics.length > 0) {
|
|
14125
|
-
lines.push("TACTICS (try in order; fall back to next on failure):");
|
|
14126
|
-
skill.tactics.forEach((t, i) => {
|
|
14127
|
-
lines.push(` ${i + 1}. ${t.name}`);
|
|
14128
|
-
lines.push(` When: ${t.when}`);
|
|
14129
|
-
lines.push(` Script: "${t.script}"`);
|
|
14130
|
-
});
|
|
14131
|
-
lines.push("");
|
|
14132
|
-
}
|
|
14133
|
-
if (skill.boundaries.length > 0) {
|
|
14134
|
-
lines.push("HARD BOUNDARIES \u2014 do not cross:");
|
|
14135
|
-
for (const b of skill.boundaries) lines.push(` - ${b}`);
|
|
14136
|
-
lines.push("");
|
|
14137
|
-
}
|
|
14138
|
-
lines.push("SUCCESS SIGNALS:");
|
|
14139
|
-
for (const s of skill.success_signals) lines.push(` - ${s}`);
|
|
14140
|
-
lines.push("");
|
|
14141
|
-
lines.push("FAILURE SIGNALS \u2014 when these appear, escalate or exit:");
|
|
14142
|
-
for (const s of skill.failure_signals) lines.push(` - ${s}`);
|
|
14143
|
-
lines.push("");
|
|
14144
|
-
lines.push("EXIT:");
|
|
14145
|
-
lines.push(` On success: ${skill.exit_strategy.on_success}`);
|
|
14146
|
-
lines.push(` On failure: ${skill.exit_strategy.on_failure}`);
|
|
14147
|
-
if (skill.exit_strategy.follow_ups && skill.exit_strategy.follow_ups.length > 0) {
|
|
14148
|
-
lines.push(" Follow-ups (after the call):");
|
|
14149
|
-
for (const f of skill.exit_strategy.follow_ups) lines.push(` - ${f}`);
|
|
14150
|
-
}
|
|
14151
|
-
lines.push("");
|
|
14152
|
-
lines.push("=== END SKILL ===");
|
|
14153
|
-
return lines.join("\n");
|
|
14154
|
-
}
|
|
14155
13633
|
export {
|
|
14156
13634
|
AGENT_ROLES,
|
|
14157
13635
|
ASK_OPERATOR_TOOL,
|
|
@@ -14181,6 +13659,7 @@ export {
|
|
|
14181
13659
|
GET_DATETIME_TOOL,
|
|
14182
13660
|
GatewayManager,
|
|
14183
13661
|
InboxWatcher,
|
|
13662
|
+
LOAD_SKILL_TOOL,
|
|
14184
13663
|
MEMORY_CATEGORIES,
|
|
14185
13664
|
MailReceiver,
|
|
14186
13665
|
MailSender,
|
|
@@ -14217,6 +13696,7 @@ export {
|
|
|
14217
13696
|
RelayBridge,
|
|
14218
13697
|
RelayGateway,
|
|
14219
13698
|
SEARCH_EMAIL_TOOL,
|
|
13699
|
+
SEARCH_SKILLS_TOOL,
|
|
14220
13700
|
SPAM_THRESHOLD,
|
|
14221
13701
|
ServiceManager,
|
|
14222
13702
|
SetupManager,
|