@electric-ax/agents 0.4.3 → 0.4.5
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/entrypoint.js +9 -434
- package/dist/index.cjs +9 -434
- package/dist/index.d.cts +4 -30
- package/dist/index.d.ts +4 -30
- package/dist/index.js +9 -434
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mergeElectricPrincipalHeader } from "./server-headers-KD5yHFYT.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
4
|
+
import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, createSkillTools, createSkillsRegistry, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import pino from "pino";
|
|
7
7
|
import { eq, not, queryOnce } from "@durable-streams/state";
|
|
@@ -13,7 +13,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
13
13
|
import { load } from "sqlite-vec";
|
|
14
14
|
import { nanoid } from "nanoid";
|
|
15
15
|
import { getModels } from "@mariozechner/pi-ai";
|
|
16
|
-
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createFetchUrlTool, createReadFileTool, createWriteTool, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
|
|
16
|
+
import { braveSearchTool, braveSearchTool as braveSearchTool$1, createBashTool, createEditTool, createFetchUrlTool, createReadFileTool, createSendTool, createWriteTool, fetchUrlTool } from "@electric-ax/agents-runtime/tools";
|
|
17
17
|
import { bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, keychainPersistence, loadConfig, mcp, watchConfig } from "@electric-ax/agents-mcp";
|
|
18
18
|
|
|
19
19
|
//#region src/log.ts
|
|
@@ -692,184 +692,6 @@ function createHortonDocsSupport(workingDirectory, opts = {}) {
|
|
|
692
692
|
};
|
|
693
693
|
}
|
|
694
694
|
|
|
695
|
-
//#endregion
|
|
696
|
-
//#region src/skills/tools.ts
|
|
697
|
-
function skillContextId(name) {
|
|
698
|
-
return `skill:${name}`;
|
|
699
|
-
}
|
|
700
|
-
function createSkillTools(registry, ctx) {
|
|
701
|
-
const useSkill = {
|
|
702
|
-
name: `use_skill`,
|
|
703
|
-
label: `Use Skill`,
|
|
704
|
-
description: `Load a skill into your context. Call with a skill name to load it. Pass args if the skill accepts arguments.`,
|
|
705
|
-
parameters: Type.Object({
|
|
706
|
-
name: Type.String({ description: `Name of the skill to load` }),
|
|
707
|
-
args: Type.Optional(Type.String({ description: `Arguments to pass to the skill (space-separated, or quoted for multi-word values)` }))
|
|
708
|
-
}),
|
|
709
|
-
execute: async (_toolCallId, params) => {
|
|
710
|
-
const { name, args } = params;
|
|
711
|
-
const meta = registry.catalog.get(name);
|
|
712
|
-
if (!meta) {
|
|
713
|
-
const available = Array.from(registry.catalog.keys()).join(`, `);
|
|
714
|
-
return {
|
|
715
|
-
content: [{
|
|
716
|
-
type: `text`,
|
|
717
|
-
text: `Skill "${name}" not found. Available skills: ${available || `none`}`
|
|
718
|
-
}],
|
|
719
|
-
details: { loaded: false }
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
const contextId = skillContextId(name);
|
|
723
|
-
if (ctx.getContext(contextId)) return {
|
|
724
|
-
content: [{
|
|
725
|
-
type: `text`,
|
|
726
|
-
text: `Skill "${name}" is already loaded.`
|
|
727
|
-
}],
|
|
728
|
-
details: {
|
|
729
|
-
loaded: false,
|
|
730
|
-
alreadyLoaded: true
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
let content = await registry.readContent(name);
|
|
734
|
-
if (content === null) return {
|
|
735
|
-
content: [{
|
|
736
|
-
type: `text`,
|
|
737
|
-
text: `Error: could not read skill file for "${name}".`
|
|
738
|
-
}],
|
|
739
|
-
details: { loaded: false }
|
|
740
|
-
};
|
|
741
|
-
let truncated = false;
|
|
742
|
-
if (content.length > meta.max) {
|
|
743
|
-
truncated = true;
|
|
744
|
-
content = content.slice(0, meta.max);
|
|
745
|
-
}
|
|
746
|
-
if (args) content = substituteArgs(content, args, meta.arguments);
|
|
747
|
-
ctx.insertContext(contextId, {
|
|
748
|
-
name: `skill_instructions`,
|
|
749
|
-
attrs: {
|
|
750
|
-
skill: name,
|
|
751
|
-
type: `directive`
|
|
752
|
-
},
|
|
753
|
-
content
|
|
754
|
-
});
|
|
755
|
-
const skillDir = path.join(path.dirname(meta.source), name);
|
|
756
|
-
const truncNote = truncated ? `\n\nWARNING: Content was truncated from ${meta.charCount.toLocaleString()} to ${meta.max.toLocaleString()} chars. Inform the user.` : ``;
|
|
757
|
-
const allRefFiles = listRefFiles(skillDir);
|
|
758
|
-
const mdFiles = allRefFiles.filter((f) => f.endsWith(`.md`));
|
|
759
|
-
const refContents = [];
|
|
760
|
-
for (const f of mdFiles) try {
|
|
761
|
-
const refContent = await fs$1.readFile(path.join(skillDir, f), `utf-8`);
|
|
762
|
-
const refId = `${skillContextId(name)}:${f}`;
|
|
763
|
-
ctx.insertContext(refId, {
|
|
764
|
-
name: `skill_reference`,
|
|
765
|
-
attrs: {
|
|
766
|
-
skill: name,
|
|
767
|
-
file: f
|
|
768
|
-
},
|
|
769
|
-
content: refContent
|
|
770
|
-
});
|
|
771
|
-
refContents.push(`--- ${f} ---\n${refContent}`);
|
|
772
|
-
} catch {}
|
|
773
|
-
const hasRefDir = allRefFiles.length > 0;
|
|
774
|
-
const dirNote = hasRefDir ? `\nSkill directory: ${skillDir}` : ``;
|
|
775
|
-
const refSection = refContents.length > 0 ? `\n\n${refContents.join(`\n\n`)}` : ``;
|
|
776
|
-
const toolResult = `SKILL ACTIVATED: "${name}". The instructions below override your default behavior. Follow them exactly. Do not read any files to find this content — it is all here.\n${dirNote}${truncNote}\n\n${content}${refSection}`;
|
|
777
|
-
return {
|
|
778
|
-
content: [{
|
|
779
|
-
type: `text`,
|
|
780
|
-
text: toolResult
|
|
781
|
-
}],
|
|
782
|
-
details: {
|
|
783
|
-
loaded: true,
|
|
784
|
-
truncated,
|
|
785
|
-
chars: content.length
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
const removeSkill = {
|
|
791
|
-
name: `remove_skill`,
|
|
792
|
-
label: `Remove Skill`,
|
|
793
|
-
description: `Unload a previously loaded skill from your context.`,
|
|
794
|
-
parameters: Type.Object({ name: Type.String({ description: `Name of the skill to remove` }) }),
|
|
795
|
-
execute: async (_toolCallId, params) => {
|
|
796
|
-
const { name } = params;
|
|
797
|
-
ctx.removeContext(skillContextId(name));
|
|
798
|
-
const meta = registry.catalog.get(name);
|
|
799
|
-
if (meta) {
|
|
800
|
-
const skillDir = path.join(path.dirname(meta.source), name);
|
|
801
|
-
for (const f of listRefFiles(skillDir)) ctx.removeContext(`${skillContextId(name)}:${f}`);
|
|
802
|
-
}
|
|
803
|
-
return {
|
|
804
|
-
content: [{
|
|
805
|
-
type: `text`,
|
|
806
|
-
text: `Skill "${name}" removed from context.`
|
|
807
|
-
}],
|
|
808
|
-
details: { removed: true }
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
};
|
|
812
|
-
return [useSkill, removeSkill];
|
|
813
|
-
}
|
|
814
|
-
function parseArgs(raw) {
|
|
815
|
-
const args = [];
|
|
816
|
-
let current = ``;
|
|
817
|
-
let inQuote = false;
|
|
818
|
-
let quoteChar = ``;
|
|
819
|
-
for (const ch of raw) if (inQuote) if (ch === quoteChar) inQuote = false;
|
|
820
|
-
else current += ch;
|
|
821
|
-
else if (ch === `"` || ch === `'`) {
|
|
822
|
-
inQuote = true;
|
|
823
|
-
quoteChar = ch;
|
|
824
|
-
} else if (ch === ` ` || ch === `\t`) {
|
|
825
|
-
if (current.length > 0) {
|
|
826
|
-
args.push(current);
|
|
827
|
-
current = ``;
|
|
828
|
-
}
|
|
829
|
-
} else current += ch;
|
|
830
|
-
if (current.length > 0) args.push(current);
|
|
831
|
-
return args;
|
|
832
|
-
}
|
|
833
|
-
function substituteArgs(content, rawArgs, argNames) {
|
|
834
|
-
const parsed = parseArgs(rawArgs);
|
|
835
|
-
let result = content;
|
|
836
|
-
let matched = false;
|
|
837
|
-
if (argNames) for (let i = 0; i < argNames.length && i < parsed.length; i++) {
|
|
838
|
-
const pattern = new RegExp(`\\$${argNames[i]}\\b`, `g`);
|
|
839
|
-
if (pattern.test(result)) {
|
|
840
|
-
result = result.replace(pattern, parsed[i]);
|
|
841
|
-
matched = true;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
845
|
-
const pattern = new RegExp(`\\$${i}\\b`, `g`);
|
|
846
|
-
if (pattern.test(result)) {
|
|
847
|
-
result = result.replace(pattern, parsed[i]);
|
|
848
|
-
matched = true;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
if (result.includes(`$ARGUMENTS`)) {
|
|
852
|
-
result = result.replace(/\$ARGUMENTS/g, rawArgs);
|
|
853
|
-
matched = true;
|
|
854
|
-
}
|
|
855
|
-
if (!matched) result += `\n\nArguments: ${rawArgs}`;
|
|
856
|
-
return result;
|
|
857
|
-
}
|
|
858
|
-
function listRefFiles(dir, prefix = ``) {
|
|
859
|
-
try {
|
|
860
|
-
const results = [];
|
|
861
|
-
for (const entry of fs.readdirSync(dir)) {
|
|
862
|
-
const full = path.join(dir, entry);
|
|
863
|
-
const rel = prefix ? `${prefix}/${entry}` : entry;
|
|
864
|
-
if (fs.statSync(full).isDirectory()) results.push(...listRefFiles(full, rel));
|
|
865
|
-
else results.push(rel);
|
|
866
|
-
}
|
|
867
|
-
return results;
|
|
868
|
-
} catch {
|
|
869
|
-
return [];
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
695
|
//#endregion
|
|
874
696
|
//#region src/tools/spawn-worker.ts
|
|
875
697
|
const WORKER_TOOL_NAMES = [
|
|
@@ -879,7 +701,8 @@ const WORKER_TOOL_NAMES = [
|
|
|
879
701
|
`edit`,
|
|
880
702
|
`web_search`,
|
|
881
703
|
`fetch_url`,
|
|
882
|
-
`spawn_worker
|
|
704
|
+
`spawn_worker`,
|
|
705
|
+
`send`
|
|
883
706
|
];
|
|
884
707
|
function createSpawnWorkerTool(ctx, modelConfig) {
|
|
885
708
|
return {
|
|
@@ -1229,6 +1052,7 @@ When a user opens with a greeting ("hi", "hello", "hey", etc.) or a broad statem
|
|
|
1229
1052
|
- web_search: search the web
|
|
1230
1053
|
- fetch_url: fetch and convert a URL to markdown
|
|
1231
1054
|
- spawn_worker: dispatch a subagent for an isolated task
|
|
1055
|
+
- send: send a message to an Electric Agent/entity by entity URL
|
|
1232
1056
|
${docsTools}${skillsTools}
|
|
1233
1057
|
|
|
1234
1058
|
# Working with files
|
|
@@ -1276,6 +1100,7 @@ function createHortonTools(workingDirectory, ctx, readSet, opts = {}) {
|
|
|
1276
1100
|
logPrefix: opts.logPrefix ?? `[horton]`
|
|
1277
1101
|
})] : [fetchUrlTool],
|
|
1278
1102
|
createSpawnWorkerTool(ctx, opts.modelConfig),
|
|
1103
|
+
createSendTool(ctx.send),
|
|
1279
1104
|
...opts.docsSearchTool ? [opts.docsSearchTool] : []
|
|
1280
1105
|
];
|
|
1281
1106
|
}
|
|
@@ -1524,6 +1349,9 @@ function buildToolsForWorker(tools, workingDirectory, ctx, readSet) {
|
|
|
1524
1349
|
case `spawn_worker`:
|
|
1525
1350
|
out.push(createSpawnWorkerTool(ctx));
|
|
1526
1351
|
break;
|
|
1352
|
+
case `send`:
|
|
1353
|
+
out.push(createSendTool(ctx.send));
|
|
1354
|
+
break;
|
|
1527
1355
|
}
|
|
1528
1356
|
return out;
|
|
1529
1357
|
}
|
|
@@ -1648,259 +1476,6 @@ function registerWorker(registry, options) {
|
|
|
1648
1476
|
});
|
|
1649
1477
|
}
|
|
1650
1478
|
|
|
1651
|
-
//#endregion
|
|
1652
|
-
//#region src/skills/preamble.ts
|
|
1653
|
-
function parsePreamble(content) {
|
|
1654
|
-
const lines = content.split(`\n`);
|
|
1655
|
-
if (lines[0]?.trim() !== `---`) return {};
|
|
1656
|
-
let closingIndex = -1;
|
|
1657
|
-
for (let i = 1; i < Math.min(lines.length, 25); i++) if (lines[i]?.trim() === `---`) {
|
|
1658
|
-
closingIndex = i;
|
|
1659
|
-
break;
|
|
1660
|
-
}
|
|
1661
|
-
if (closingIndex === -1) return {};
|
|
1662
|
-
const result = {};
|
|
1663
|
-
for (let i = 1; i < closingIndex; i++) {
|
|
1664
|
-
const line = lines[i];
|
|
1665
|
-
const colonIndex = line.indexOf(`:`);
|
|
1666
|
-
if (colonIndex === -1) continue;
|
|
1667
|
-
const key = line.slice(0, colonIndex).trim();
|
|
1668
|
-
const rawValue = line.slice(colonIndex + 1).trim();
|
|
1669
|
-
switch (key) {
|
|
1670
|
-
case `description`:
|
|
1671
|
-
result.description = stripQuotes(rawValue);
|
|
1672
|
-
break;
|
|
1673
|
-
case `whenToUse`:
|
|
1674
|
-
result.whenToUse = stripQuotes(rawValue);
|
|
1675
|
-
break;
|
|
1676
|
-
case `keywords`: {
|
|
1677
|
-
if (rawValue.length === 0) {
|
|
1678
|
-
const items = [];
|
|
1679
|
-
for (let j = i + 1; j < closingIndex; j++) {
|
|
1680
|
-
const next = lines[j];
|
|
1681
|
-
const match = next.match(/^\s+-\s+(.+)$/);
|
|
1682
|
-
if (match) {
|
|
1683
|
-
items.push(match[1].trim());
|
|
1684
|
-
i = j;
|
|
1685
|
-
} else break;
|
|
1686
|
-
}
|
|
1687
|
-
result.keywords = items;
|
|
1688
|
-
} else result.keywords = parseKeywords(rawValue);
|
|
1689
|
-
break;
|
|
1690
|
-
}
|
|
1691
|
-
case `arguments`: {
|
|
1692
|
-
if (rawValue.length === 0) {
|
|
1693
|
-
const items = [];
|
|
1694
|
-
for (let j = i + 1; j < closingIndex; j++) {
|
|
1695
|
-
const next = lines[j];
|
|
1696
|
-
const match = next.match(/^\s+-\s+(.+)$/);
|
|
1697
|
-
if (match) {
|
|
1698
|
-
items.push(match[1].trim());
|
|
1699
|
-
i = j;
|
|
1700
|
-
} else break;
|
|
1701
|
-
}
|
|
1702
|
-
result.arguments = items;
|
|
1703
|
-
} else result.arguments = parseKeywords(rawValue);
|
|
1704
|
-
break;
|
|
1705
|
-
}
|
|
1706
|
-
case `argument-hint`:
|
|
1707
|
-
result.argumentHint = stripQuotes(rawValue);
|
|
1708
|
-
break;
|
|
1709
|
-
case `user-invocable`:
|
|
1710
|
-
result.userInvocable = rawValue === `true`;
|
|
1711
|
-
break;
|
|
1712
|
-
case `max`: {
|
|
1713
|
-
const num = parseInt(rawValue, 10);
|
|
1714
|
-
if (!Number.isNaN(num) && num > 0) result.max = num;
|
|
1715
|
-
break;
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
return result;
|
|
1720
|
-
}
|
|
1721
|
-
function parseKeywords(raw) {
|
|
1722
|
-
const stripped = raw.replace(/^\[/, ``).replace(/\]$/, ``);
|
|
1723
|
-
return stripped.split(`,`).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1724
|
-
}
|
|
1725
|
-
function stripQuotes(value) {
|
|
1726
|
-
if (value.length >= 2 && value.startsWith(`"`) && value.endsWith(`"`)) return value.slice(1, -1);
|
|
1727
|
-
return value;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
//#endregion
|
|
1731
|
-
//#region src/skills/extract-meta.ts
|
|
1732
|
-
const DEFAULT_MAX = 1e4;
|
|
1733
|
-
async function extractSkillMeta(name, content) {
|
|
1734
|
-
const preamble = parsePreamble(content);
|
|
1735
|
-
if (preamble.description && preamble.whenToUse && preamble.keywords) return {
|
|
1736
|
-
description: preamble.description,
|
|
1737
|
-
whenToUse: preamble.whenToUse,
|
|
1738
|
-
keywords: preamble.keywords,
|
|
1739
|
-
...preamble.arguments && { arguments: preamble.arguments },
|
|
1740
|
-
...preamble.argumentHint && { argumentHint: preamble.argumentHint },
|
|
1741
|
-
...preamble.userInvocable && { userInvocable: true },
|
|
1742
|
-
max: preamble.max ?? DEFAULT_MAX
|
|
1743
|
-
};
|
|
1744
|
-
try {
|
|
1745
|
-
return await llmExtract(name, content, preamble);
|
|
1746
|
-
} catch (err) {
|
|
1747
|
-
serverLog.warn(`[skills] LLM metadata extraction failed for "${name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1748
|
-
}
|
|
1749
|
-
return {
|
|
1750
|
-
description: preamble.description ?? humanize(name),
|
|
1751
|
-
whenToUse: preamble.whenToUse ?? `User asks about ${humanize(name).toLowerCase()}`,
|
|
1752
|
-
keywords: preamble.keywords ?? [name],
|
|
1753
|
-
max: preamble.max ?? DEFAULT_MAX
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
async function llmExtract(name, content, partial) {
|
|
1757
|
-
const truncated = content.slice(0, 8e3);
|
|
1758
|
-
const prompt = `Analyze this skill document and extract metadata. The skill is named "${name}".
|
|
1759
|
-
|
|
1760
|
-
<skill>
|
|
1761
|
-
${truncated}
|
|
1762
|
-
</skill>
|
|
1763
|
-
|
|
1764
|
-
Return ONLY a JSON object with these fields:
|
|
1765
|
-
- "description": one-line summary of what this skill provides (max 100 chars)
|
|
1766
|
-
- "whenToUse": when should an AI agent load this skill (max 200 chars)
|
|
1767
|
-
- "keywords": array of 3-8 relevant keywords
|
|
1768
|
-
|
|
1769
|
-
Return raw JSON, no markdown fences.`;
|
|
1770
|
-
const text = await completeWithLowCostModel({
|
|
1771
|
-
purpose: `skill metadata extraction`,
|
|
1772
|
-
systemPrompt: `Extract metadata from skill documents. Return only valid JSON that matches the requested schema.`,
|
|
1773
|
-
prompt,
|
|
1774
|
-
maxTokens: 256,
|
|
1775
|
-
log: (message) => serverLog.info(message),
|
|
1776
|
-
logPrefix: `[skills]`
|
|
1777
|
-
});
|
|
1778
|
-
const parsed = JSON.parse(text);
|
|
1779
|
-
return {
|
|
1780
|
-
description: partial.description ?? parsed.description ?? humanize(name),
|
|
1781
|
-
whenToUse: partial.whenToUse ?? parsed.whenToUse ?? `User asks about ${name}`,
|
|
1782
|
-
keywords: partial.keywords ?? parsed.keywords ?? [name],
|
|
1783
|
-
max: partial.max ?? DEFAULT_MAX
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
function humanize(name) {
|
|
1787
|
-
return name.replace(/[-_]/g, ` `).replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
//#endregion
|
|
1791
|
-
//#region src/skills/registry.ts
|
|
1792
|
-
const CACHE_FILENAME = `skills-cache.json`;
|
|
1793
|
-
async function createSkillsRegistry(opts) {
|
|
1794
|
-
const { baseSkillsDir, appSkillsDir, cacheDir } = opts;
|
|
1795
|
-
const cachePath = path.join(cacheDir, CACHE_FILENAME);
|
|
1796
|
-
const existingCache = await loadCache(cachePath);
|
|
1797
|
-
const files = new Map();
|
|
1798
|
-
await scanDir(baseSkillsDir, files);
|
|
1799
|
-
if (appSkillsDir) await scanDir(appSkillsDir, files);
|
|
1800
|
-
const catalog = new Map();
|
|
1801
|
-
for (const [name, filePath] of files) {
|
|
1802
|
-
const content = await fs$1.readFile(filePath, `utf-8`);
|
|
1803
|
-
const hash = sha256(content);
|
|
1804
|
-
const cached = existingCache[name];
|
|
1805
|
-
if (cached && cached.contentHash === hash && cached.source === filePath) {
|
|
1806
|
-
catalog.set(name, cached);
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
serverLog.info(`[skills] extracting metadata for "${name}"`);
|
|
1810
|
-
const meta = await extractSkillMeta(name, content);
|
|
1811
|
-
const entry = {
|
|
1812
|
-
name,
|
|
1813
|
-
...meta,
|
|
1814
|
-
charCount: content.length,
|
|
1815
|
-
contentHash: hash,
|
|
1816
|
-
source: filePath
|
|
1817
|
-
};
|
|
1818
|
-
catalog.set(name, entry);
|
|
1819
|
-
}
|
|
1820
|
-
await saveCache(cachePath, catalog, cacheDir);
|
|
1821
|
-
return {
|
|
1822
|
-
catalog,
|
|
1823
|
-
renderCatalog(budget) {
|
|
1824
|
-
if (catalog.size === 0) return ``;
|
|
1825
|
-
const skills = Array.from(catalog.values());
|
|
1826
|
-
const full = renderSkillList(skills, `full`);
|
|
1827
|
-
if (!budget || full.length <= budget) return full;
|
|
1828
|
-
const compact = renderSkillList(skills, `compact`);
|
|
1829
|
-
if (compact.length <= budget) return compact;
|
|
1830
|
-
return renderSkillList(skills, `names`);
|
|
1831
|
-
},
|
|
1832
|
-
async readContent(name) {
|
|
1833
|
-
const meta = catalog.get(name);
|
|
1834
|
-
if (!meta) return null;
|
|
1835
|
-
try {
|
|
1836
|
-
return await fs$1.readFile(meta.source, `utf-8`);
|
|
1837
|
-
} catch {
|
|
1838
|
-
return null;
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
async function scanDir(dir, out) {
|
|
1844
|
-
let entries;
|
|
1845
|
-
try {
|
|
1846
|
-
entries = await fs$1.readdir(dir, { withFileTypes: true });
|
|
1847
|
-
} catch {
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
for (const entry of entries) {
|
|
1851
|
-
if (!entry.isFile() || !entry.name.endsWith(`.md`)) continue;
|
|
1852
|
-
const name = entry.name.slice(0, -3);
|
|
1853
|
-
out.set(name, path.resolve(dir, entry.name));
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
async function loadCache(cachePath) {
|
|
1857
|
-
try {
|
|
1858
|
-
const raw = await fs$1.readFile(cachePath, `utf-8`);
|
|
1859
|
-
return JSON.parse(raw);
|
|
1860
|
-
} catch {
|
|
1861
|
-
return {};
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
async function saveCache(cachePath, catalog, cacheDir) {
|
|
1865
|
-
const obj = {};
|
|
1866
|
-
for (const [name, meta] of catalog) obj[name] = meta;
|
|
1867
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1868
|
-
await fs$1.writeFile(path.join(cacheDir, `.gitignore`), `*\n`, `utf-8`);
|
|
1869
|
-
await fs$1.writeFile(cachePath, JSON.stringify(obj, null, 2), `utf-8`);
|
|
1870
|
-
}
|
|
1871
|
-
function sha256(content) {
|
|
1872
|
-
return createHash(`sha256`).update(content).digest(`hex`);
|
|
1873
|
-
}
|
|
1874
|
-
function renderSkillList(skills, mode) {
|
|
1875
|
-
const invocable = skills.filter((s) => s.userInvocable);
|
|
1876
|
-
const others = skills.filter((s) => !s.userInvocable);
|
|
1877
|
-
const lines = [`Available skills:`];
|
|
1878
|
-
if (invocable.length > 0 && mode !== `names`) {
|
|
1879
|
-
lines.push(`\nUser-invocable (the user can trigger these directly):`);
|
|
1880
|
-
for (const meta of invocable) {
|
|
1881
|
-
const hint = meta.argumentHint ? ` ${meta.argumentHint}` : ``;
|
|
1882
|
-
lines.push(`- /${meta.name}${hint} — ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
|
|
1883
|
-
}
|
|
1884
|
-
if (others.length > 0) lines.push(``);
|
|
1885
|
-
}
|
|
1886
|
-
const all = mode === `names` ? skills : others.length > 0 ? others : invocable.length === 0 ? skills : [];
|
|
1887
|
-
for (const meta of all) {
|
|
1888
|
-
if (mode === `names`) {
|
|
1889
|
-
const prefix = meta.userInvocable ? `/${meta.name}` : meta.name;
|
|
1890
|
-
lines.push(`- ${prefix}: ${truncate(meta.description, 60)}`);
|
|
1891
|
-
continue;
|
|
1892
|
-
}
|
|
1893
|
-
lines.push(`- ${meta.name} (${meta.charCount.toLocaleString()} chars): ${mode === `compact` ? truncate(meta.description, 100) : meta.description}`);
|
|
1894
|
-
lines.push(` Use when: ${meta.whenToUse}`);
|
|
1895
|
-
if (mode === `full`) lines.push(` Keywords: ${meta.keywords.join(`, `)}`);
|
|
1896
|
-
if (meta.argumentHint) lines.push(` Usage: use_skill("${meta.name}", "${meta.argumentHint}")`);
|
|
1897
|
-
}
|
|
1898
|
-
return lines.join(`\n`);
|
|
1899
|
-
}
|
|
1900
|
-
function truncate(str, max) {
|
|
1901
|
-
return str.length <= max ? str : str.slice(0, max - 3) + `...`;
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
1479
|
//#endregion
|
|
1905
1480
|
//#region src/bootstrap.ts
|
|
1906
1481
|
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@electric-ax/agents",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "Built-in Electric Agents runtimes such as Horton and worker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"./package.json": "./package.json"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@
|
|
41
|
+
"@durable-streams/state": "https://pkg.pr.new/durable-streams/durable-streams/@durable-streams/state@5d5c217",
|
|
42
42
|
"@mariozechner/pi-agent-core": "^0.70.2",
|
|
43
43
|
"@mariozechner/pi-ai": "^0.70.2",
|
|
44
44
|
"@sinclair/typebox": "^0.34.48",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"sqlite-vec": "^0.1.9",
|
|
50
50
|
"zod": "^4.3.6",
|
|
51
51
|
"@electric-ax/agents-mcp": "0.2.2",
|
|
52
|
-
"@electric-ax/agents-runtime": "0.
|
|
52
|
+
"@electric-ax/agents-runtime": "0.3.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/better-sqlite3": "^7.6.13",
|